Editable 原地编辑

在原位置编辑文本,确认或取消修改。

项目信息

单击文字修改。回车保存,Esc 取消;多行文本用 Ctrl / ⌘ + Enter 保存。

<script setup lang="ts">
  import { ref } from 'vue'
  import { Card, Editable, FormField, Stack, Text } from '@hina-ui/vue'

  const title = ref('Hina UI 组件库')
  const description = ref('为内容站与管理后台提供一致、克制的交互体验。')
</script>

<template>
  <Card class="w-full max-w-lg">
    <Stack gap="lg">
      <Text weight="medium">项目信息</Text>
      <FormField label="项目名称">
        <Editable v-model="title" :maxlength="60" />
      </FormField>
      <FormField label="简介">
        <Editable v-model="description" multiline :rows="3" />
      </FormField>
      <Text size="sm" tone="muted">
        单击文字修改。回车保存,Esc 取消;多行文本用 Ctrl / ⌘ + Enter 保存。
      </Text>
    </Stack>
  </Card>
</template>
vue

用法

import { Editable } from '@hina-ui/vue'
ts

用于名称、标题、备注等平时以文本展示、需要时就地修改的字段。v-model 是已确认的值,键入内容只修改内部草稿;保存后才更新,取消恢复原值。需要始终显示输入框、逐字同步表单值时使用 Input 或 Textarea。

默认单击进入编辑,自动聚焦并选中文字。回车或焦点离开整个组件时保存,Esc 取消。焦点在输入框与保存、取消按钮之间移动不会提交,取消按钮也不会先触发失焦保存。

已保存:未命名文档

<script setup lang="ts">
  import { ref } from 'vue'
  import { Editable, Stack, Text } from '@hina-ui/vue'

  const title = ref('未命名文档')
</script>

<template>
  <Stack class="w-full max-w-sm">
    <Editable v-model="title" aria-label="文档名称" />
    <Text size="sm" tone="muted">已保存:{{ title || '—' }}</Text>
  </Stack>
</template>
vue

示例

激活与确认方式

activationMode 支持单击、双击与手动触发。单击与双击模式都支持聚焦后用 Enter、空格进入编辑;仅用 Tab 聚焦不会直接修改内容。手动模式通过编辑按钮、v-model:editing 或实例的 edit() 进入。

submitMode 决定快捷提交方式,显式保存按钮始终可用:

值行为
both回车或焦点离开组件时保存,默认值
enter回车保存;焦点移出后保留草稿与编辑态
blur焦点离开组件时保存;回车不提交
manual仅显式保存;回车、焦点移出都保留草稿

首个示例中的 multiline 使用多行输入:Enter 换行,Ctrl / ⌘ + Enter 按 submitMode 的回车规则保存。中文等输入法组词期间不处理保存和取消。

多行编辑复用 Textarea,超出 rows 后由 ScrollArea 接管滚动,输入时保持光标可见。

键盘聚焦后仍可按 Enter 或空格进入。

按编辑按钮修改

草稿一直保留到按下保存或取消。

<script setup lang="ts">
  import { reactive } from 'vue'
  import { Editable, FormField, Stack } from '@hina-ui/vue'

  const values = reactive({ double: '双击修改文档标题', manual: '按编辑按钮修改' })
</script>

<template>
  <Stack class="w-full max-w-sm" gap="lg">
    <FormField label="双击进入" description="键盘聚焦后仍可按 Enter 或空格进入。">
      <Editable v-model="values.double" activation-mode="dblclick" />
    </FormField>
    <FormField label="手动进入" description="草稿一直保留到按下保存或取消。">
      <Editable v-model="values.manual" activation-mode="manual" submit-mode="manual" />
    </FormField>
  </Stack>
</template>
vue

异步保存与失败重试

通过 :on-save="save" 提供保存函数。它接收新值、旧值,可以返回 Promise;成功后才更新 v-model 并触发 submit。值没有变化时直接结束编辑,不重复保存。

保存期间保留草稿和输入框,显示加载态,禁止重复保存与取消。抛出 Error 会显示其 message,其他异常使用通用失败提示,并触发 error;用户可以修改后重试或取消。修改草稿会清除旧错误。不要在异步 @submit 监听器里执行需要组件等待的请求,submit 是保存成功后的通知。

外部更新 modelValue、关闭 editing、设为禁用或只读、卸载组件,都会使旧的保存结果失效,防止旧值回写。网络请求本身是否取消由业务方负责。

保存完成前保留编辑态,失败后可继续修改或重试。

<script setup lang="ts">
  import { ref } from 'vue'
  import { Editable, FormField, Stack, Switch, Text } from '@hina-ui/vue'

  const name = ref('创作者工作台')
  const fail = ref(false)
  const saved = ref(false)

  async function save(value: string) {
    saved.value = false
    if (!value.trim()) throw new Error('工作区名称不能为空')
    await new Promise(resolve => setTimeout(resolve, 800))
    if (fail.value) throw new Error('暂时无法保存,请关闭模拟失败后重试')
    saved.value = true
  }
</script>

<template>
  <Stack class="w-full max-w-sm">
    <FormField label="工作区名称" description="保存完成前保留编辑态,失败后可继续修改或重试。">
      <Editable v-model="name" :on-save="save" submit-mode="enter" />
    </FormField>
    <FormField label="模拟保存失败" orientation="horizontal">
      <Switch v-model="fail" />
    </FormField>
    <Text v-if="saved" role="status" size="sm" tone="muted">工作区名称已保存。</Text>
  </Stack>
</template>
vue

自定义展示与操作区

#preview 自定义文本的展示形态,不替换组件的激活和焦点逻辑。单击或双击模式下预览由按钮承载,不要在插槽内嵌套按钮、链接等交互元素。#actions 接收编辑状态和操作方法,可换成文字按钮。

:controls="false" 可隐藏默认操作区;与手动模式组合时,需要通过插槽或实例方法提供进入、保存和取消的入口。

v1.8.0
<script setup lang="ts">
  import { ref } from 'vue'
  import { Button, Editable, FormField, Stack, Tag } from '@hina-ui/vue'

  const version = ref('1.8.0')
</script>

<template>
  <Stack class="w-full max-w-sm">
    <FormField label="发布版本">
      <Editable v-model="version" activation-mode="manual" submit-mode="manual">
        <template #preview="{ value }">
          <Tag tone="info">v{{ value }}</Tag>
        </template>
        <template #actions="{ editing, saving, edit, submit, cancel }">
          <template v-if="editing">
            <Button size="sm" :loading="saving" @click="submit">保存版本</Button>
            <Button size="sm" variant="ghost" tone="neutral" :disabled="saving" @click="cancel">
              取消
            </Button>
          </template>
          <Button v-else size="sm" variant="outline" tone="neutral" @click="edit">修改版本</Button>
        </template>
      </Editable>
    </FormField>
  </Stack>
</template>
vue

尺寸与状态

三档字号、输入高度和内边距与 Input 对齐。单行预览和输入使用相同宽度与文字起点,操作区显示在下方,编辑时不会挤窄文本。只读内容可以选择与复制,禁用状态禁止进入编辑。

只读项目名称

内容可以选择与复制。

<script setup lang="ts">
  import { Editable, FormField, Stack } from '@hina-ui/vue'
</script>

<template>
  <Stack class="w-full max-w-sm">
    <Editable size="sm" model-value="小号文本" aria-label="小号" />
    <Editable size="md" model-value="默认文本" aria-label="默认" />
    <Editable size="lg" model-value="大号文本" aria-label="大号" />
    <FormField label="只读" description="内容可以选择与复制。">
      <Editable model-value="只读项目名称" readonly />
    </FormField>
    <FormField label="禁用">
      <Editable model-value="暂时无法修改" disabled />
    </FormField>
    <FormField label="尚未填写">
      <Editable placeholder="添加备注…" />
    </FormField>
  </Stack>
</template>
vue

在表单中

FormField 自动关联标签、描述、错误和禁用状态。字段完成编辑后,Form 对已确认的值进行校验。示例用 v-model:editing 在编辑期间禁用整份表单的提交按钮,避免提交尚未确认的草稿。

required、maxlength 是 Editable 自身的输入限制;复杂规则使用 Form 的规则,或在 onSave 内校验并抛出带提示的异常。name 让原生 FormData 收集已确认值。

先确认字段修改,再保存整份资料。

<script setup lang="ts">
  import { reactive, ref } from 'vue'
  import * as v from 'valibot'
  import { Button, Editable, Form, FormField, Text } from '@hina-ui/vue'

  const values = reactive({ displayName: '' })
  const editing = ref(false)
  const saved = ref('')
  const schema = v.object({
    displayName: v.pipe(v.string(), v.trim(), v.minLength(2, '显示名称至少需要 2 个字')),
  })

  async function save(data: unknown) {
    await new Promise(resolve => setTimeout(resolve, 600))
    saved.value = (data as { displayName: string }).displayName
  }
</script>

<template>
  <Form
    v-slot="{ submitting }"
    :values="values"
    :rules="schema"
    class="w-full max-w-sm"
    @submit="save"
  >
    <FormField
      name="displayName"
      label="显示名称"
      description="先确认字段修改,再保存整份资料。"
      required
    >
      <Editable
        v-model="values.displayName"
        v-model:editing="editing"
        name="displayName"
        placeholder="填写显示名称"
      />
    </FormField>
    <Button type="submit" :loading="submitting" :disabled="editing" class="self-start">
      保存资料
    </Button>
    <Text v-if="saved" role="status" size="sm" tone="muted">已保存:{{ saved }}</Text>
  </Form>
</template>
vue

键盘与焦点

  • 预览态:Tab 聚焦;Enter、空格进入编辑。手动模式使用编辑按钮。
  • 单行编辑:Enter 按 submitMode 保存;不会意外提交外层表单。
  • 多行编辑:Enter 换行,Ctrl / ⌘ + Enter 按 submitMode 保存。
  • Esc 取消当前草稿,不同时关闭所在的 Dialog;组词和保存期间不取消。
  • 保存、取消后,仍在组件内的焦点回到预览;如果用户已移到外部,不抢回焦点。
  • 初始 editing=true 可以在 SSR 中输出输入态,挂载时不会自动抢焦点。之后进入编辑时才自动聚焦。

API

Props

属性
类型
默认值
说明
modelValue
string
''
已确认文本,支持 v-model
editing
boolean
false
编辑状态,支持 v-model:editing
activationMode
'click' | 'dblclick' | 'manual'
'click'
进入编辑的方式
submitMode
'enter' | 'blur' | 'both' | 'manual'
'both'
快捷提交方式
selectOnFocus
boolean
true
进入编辑后全选文字
multiline
boolean
false
使用多行文本框
rows
number
3
多行输入的初始行数
controls
boolean
true
显示默认操作区
onSave
EditableSave
—
更新模型前等待的保存回调
placeholder
string
本地化文案
空值提示,同时用于输入框
name
string
—
原生表单字段名,收集已确认值
required
boolean
false
不允许确认空值
maxlength
number
—
输入长度上限
size
'sm' | 'md' | 'lg'
'md'
尺寸
disabled
boolean
false
禁用
readonly
boolean
false
只读,允许复制文本
invalid
boolean
false
标记校验失败
class
string
—
根节点样式

其余属性(如 id、aria-label、autocomplete)传给当前预览或输入节点。class 作用于外层;style 作用于当前预览或输入节点。

插槽

插槽
参数
说明
preview
{ value: string, empty: boolean }
展示已确认的值
actions
EditableControls
替换默认操作区

事件

事件
参数
说明
update:modelValue
string
保存成功后的新值
update:editing
boolean
编辑状态变化
edit
—
进入编辑
submit
value: string, previousValue: string
修改保存成功
cancel
draft: string
取消并丢弃的草稿
error
unknown
保存回调抛出的异常

实例与类型

实例暴露 edit()、submit(): Promise<boolean>、cancel()、focus(),以及当前的 input、draft、saving、error。input 仅在客户端编辑态存在。submit() 返回是否成功完成;保存期间 cancel() 不生效。

type EditableSave = (value: string, previousValue: string) => void | Promise<void>

interface EditableControls {
  editing: boolean
  draft: string
  dirty: boolean
  saving: boolean
  disabled: boolean
  error: string
  edit: () => void
  submit: () => Promise<boolean>
  cancel: () => void
}
ts