跳转正文

YEditorPanel ​

适用范围:Legacy Admin Kit / Schema(@yoyoboot/yoyo-admin-kit、旧 admin-vue)。新 admin-nuxtui 使用 Nuxt UI;本页不是新宿主的必经开发路线。

定位 / Owner ​

YEditorPanel 是标准编辑任务的 Application Composition。它组合 Dialog/Drawer、TaskActions、dirty-close confirm、loading/error/empty、busy、submit/cancel 与可选的“保存后继续编辑”,不是新的 Form state owner。

页面级选型、owner 与真实 consumer 见 Editor Task Application UI。本文只维护 YEditorPanel 的组件合同。

什么时候使用 ​

  • 标准 Create/Edit task。
  • 需要 dirty close、saving lock、loading/error/retry、empty state。
  • feature 不应重复 footer 与 discard confirm。

基础用法 ​

vue
<YEditorPanel
  v-model:open="open"
  title="编辑租户"
  :dirty="dirty"
  :submitting="saving"
  @submit="save"
>
  <TenantForm />
</YEditorPanel>

文档内可运行示例 ​

下面的记录、保存等待和结果提示都是确定性的浏览器内存演示,不连接 Admin API 或数据库。运行示例与“查看同源 Vue 代码”读取同一个 BasicEditor.vue;实际业务仍应由自己的 Form、mutation 和权限 owner 提供字段与写入。

Public API 摘要 ​

可运行示例

校验、写入与回读失败

无 formId 的事件提交;可分别体验 Modal / Drawer。观察重试只增加回读次数。

内存写入 0 次,回读 0 次;当前记录:演示记录

查看同源 Vue 代码
<script setup lang="ts">
import { computed, ref } from 'vue'
import { YButton, YCheckbox, YEditorPanel, YInput } from '@yoyoboot/yoyo-admin-kit'

// This fixture is intentionally local. A real consumer delegates these calls
// to its existing Form/session and mutation owner, not to EditorPanel.
const open = ref(false)
const surface = ref<'modal' | 'drawer'>('drawer')
const serverName = ref('演示记录')
const baseline = ref(serverName.value)
const draft = ref(serverName.value)
const dirty = computed(() => draft.value !== baseline.value)
const submitting = ref(false)
const continueEditing = ref(true)
const saved = ref(false)
const failWrite = ref(false)
const failRead = ref(true)
const error = ref('')
const postSaveError = ref<string | null>(null)
const writes = ref(0)
const reads = ref(0)
const delay = () => new Promise(resolve => setTimeout(resolve, 250))

function begin(value: 'modal' | 'drawer') {
  surface.value = value
  draft.value = baseline.value = serverName.value
  error.value = ''
  postSaveError.value = null
  saved.value = false
  open.value = true
}
async function readBack() {
  reads.value++
  await delay()
  if (failRead.value) {
    postSaveError.value = '演示回读失败。取消“回读失败”后重新同步,不会再次写入。'
    return
  }
  draft.value = baseline.value = serverName.value
  postSaveError.value = null
  if (!continueEditing.value) open.value = false
}
async function retryRead() {
  if (submitting.value) return
  submitting.value = true
  try { await readBack() } finally { submitting.value = false }
}
async function save() {
  if (submitting.value || postSaveError.value || !dirty.value) return
  error.value = ''
  if (!draft.value.trim()) { error.value = '名称不能为空。'; return }
  submitting.value = true
  try {
    await delay()
    if (failWrite.value) { error.value = '演示写入失败,草稿仍保留。'; return }
    serverName.value = draft.value
    writes.value++
    saved.value = true
    await readBack()
  } finally { submitting.value = false }
}
</script>

<template>
  <div style="display: grid; gap: 0.75rem">
    <YButton variant="outline" @click="begin('drawer')">抽屉失败恢复</YButton>
    <YButton variant="outline" @click="begin('modal')">弹窗失败恢复</YButton>
    <p role="status">内存写入 {{ writes }} 次,回读 {{ reads }} 次;当前记录:{{ serverName }}</p>
  </div>
  <YEditorPanel v-model:open="open" v-model:continue-editing="continueEditing" title="保存与回读恢复" :surface="surface" :dirty="dirty" :saved="saved" :submitting="submitting" :submit-disabled="!dirty" :post-save-error="postSaveError" allow-save-and-continue @submit="save" @retry="retryRead">
    <!-- No formId: Enter and the panel event explicitly share this one handler. -->
    <form style="display: grid; gap: 1rem" @submit.prevent="save">
      <label>名称<YInput v-model="draft" aria-label="恢复示例名称" /></label>
      <p v-if="error" role="alert">{{ error }}</p>
      <YCheckbox v-model="failWrite" label="模拟写入失败" :disabled="submitting" />
      <YCheckbox v-model="failRead" label="模拟回读失败" :disabled="submitting" />
      <p role="status">成功写入 {{ writes }} 次 / 回读 {{ reads }} 次</p>
      <p>先修改名称并保存。默认回读失败;取消模拟回读失败,再点重新同步。</p>
    </form>
  </YEditorPanel>
</template>

选型与接入 ​

普通详情用 YModal;标准编辑任务用 YEditorPanel,不在 feature 重写 dirty/busy/footer。只需要侧边详情可直接用 YDrawer。上述两个示例全部是内存演示,不代表真实 API 验收。

从 @yoyoboot/yoyo-admin-kit 包根导入,并在宿主入口加载 styles.css 与 theme.css。Panel 不要求业务 provider,不拥有字段、校验或 mutation。BasicEditor 使用 HTML required/email 校验和 native form submit;RecoveryEditor 显式校验名称并展示恢复路径,示例辅助代码都在同源 Vue 文件中。

提交方式按钮Enter
formId只触发关联 form 的 submit,不 emit submit交给同一 form handler
无 formId只 emit submitconsumer 的 form @submit.prevent 绑定同一 handler

保存 handler 仍须维护自身 busy/重复写入保护。requestClose() 是安全离开入口:busy 拒绝,dirty 只显示一次确认。父级直接 open=false 是权威覆盖,不触发 dirty 确认;不要拿它代替用户取消。

Public API 摘要(分组) ​

  • models:open、discardConfirmationOpen
  • optional model:continueEditing
  • 基础内容:title、description
  • surface:surface: modal | drawer(默认 modal,旧 dialog 输入仍兼容)、size: sm | md | lg、allowFullscreen
  • task text:submitText、saveAndContinueText、submittingText、cancelText、closeText
  • save continuation:allowSaveAndContinue(默认关闭)、saved、postSaveError、postSaveErrorTitle
  • dirty confirmation:discardTitle、discardDescription、discardConfirmText、discardCancelText、discardTestIdPrefix
  • state:loading、loadingTitle、loadingDescription、error、errorTitle、empty、emptyTitle、emptyDescription、submitting、submitDisabled、dirty
  • form integration:formId
  • focus:restoreFocusTo
  • test hooks:testIds.root/loading/error/cancel/submit
  • events:submit、retry
  • slots:default、header-actions、actions-prefix

Composition ​

Props / Models / Events / Slots / Expose ​

Prop类型默认值/说明
titlestring必填
descriptionstring未设置
surfacemodal / drawer / dialog(deprecated)modal
sizesm / md / lg未设置,跟随容器
allowFullscreenbooleanfalse
formIdstring未设置;设置后只走原生表单提交
submitTextstring保存
saveAndContinueTextstring保存并继续
submittingTextstring保存中...
cancelTextstring取消
closeTextstring关闭
allowSaveAndContinuebooleanfalse
savedbooleanfalse;consumer 提供成功事实
postSaveErrorstring / nullnull;锁重复保存但保留字段
postSaveErrorTitlestring已保存,编辑信息同步失败
restoreFocusToHTMLElement / nullnull;跟随容器恢复策略
discardTitlestring放弃未保存的修改?
discardDescriptionstring关闭后,本次未保存的编辑内容将丢失。
discardConfirmTextstring放弃修改
discardCancelTextstring继续编辑
discardTestIdPrefixstringy-editor-discard
loadingbooleanfalse
loadingTitlestring正在加载编辑信息
loadingDescriptionstring请稍候,系统正在同步当前条目的详情数据。
errorstring / nullnull;加载错误,不是写入错误万能入口
errorTitlestring编辑信息加载失败
emptybooleanfalse
emptyTitlestring暂无可编辑记录
emptyDescriptionstring请返回列表后重新选择一条记录。
submittingbooleanfalse;锁关闭与提交
submitDisabledbooleanfalse;consumer 决定是否可提交
dirtybooleanfalse;Form/session 拥有判断
testIds未设置;值均为 string
接口类型/默认值时机与职责
v-model:openboolean / false权威显隐
v-model:discardConfirmationOpenboolean / false放弃确认显隐,主任务关闭会同步关闭
v-model:continueEditingboolean / false保存后意图,不执行写入
submit无参数仅无 formId 时的主按钮操作
retry无参数加载重试或写后重新同步;consumer 区分,禁止回读重试重复写
defaultslot字段与写入错误 UI
header-actionsslot容器标题次级操作
actions-prefixslot操作区辅助信息
requestClose()expose / void共用 busy/dirty 离开流程

旧 surface="dialog" 仍映射为 modal。非模态窗口、拖动缩放、全局弹窗服务后置。本页是当前源码合同与内存示例,不表示新版本已发布或真实业务已验收。

YEditorPanel 拥有 task surface;TanStack Form/feature 拥有字段值与 validation;Data Workbench 可以打开 EditorPanel,但 Data Workbench state 仍是 authoritative orchestration owner。

“保存后继续编辑”只在 consumer 明确 opt-in 的既有记录编辑中使用。它不是第二个提交按钮:checkbox 只改变唯一主按钮的文案与保存后意图,键盘原生 form submit 使用同一选择。保存成功后,Data Workbench 必须重新调用 getForEdit 获取权威字段和最新并发令牌,再把新值设为 clean baseline。

如果写入失败,保留 dirty draft 并显示写入错误;如果写入已经成功但权威回读失败,必须显示“已保存但同步失败”,锁住再次保存,并且重试只能回读、不能重复写入。列表刷新失败只影响列表反馈,不能把已成功的保存伪装成写入失败。

Production Example ​

接入时从包根导入,并加载 styles.css 与 theme.css;演示保存不连接数据库,也不表示当前源码已经发布 npm。

Identity Users 资料编辑(显式启用保存后继续)、Tenant Create/Edit、Edition editor、Data Workbench CRUD editor。计量单位等未 opt-in consumer 仍保持保存后关闭。

Live Workbench ​

Admin Vue 稳定路径:

/frontend-module-demo/component-workbench/editor-panel

该路径只标识 live scenario,不固化部署 Host;官方文档站与 Admin Vue 可以独立部署。

Verification / Source ​

  • packages/yoyo-admin/admin-kit/src/ui/components/editor-panel/YEditorPanel.vue
  • tests/unit/ui/task-surface.spec.ts
  • Live Component Workbench

YoyoBoot · 从真实项目走向可复用能力