feat(tickets): allow custom status key in status create/edit

- Validate status_key (^[a-z][a-z0-9_]{1,23}$, unique) on create/update
- Renaming a key moves aksrt_ticket.status in the same transaction
- Default statuses remain immutable

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-10-27 23:22:38 +08:00
1 parent 8d871c0828
commit 135fb4eaa4
4 files changed
+134 -7

No files matched your search

@@ -39,6 +39,22 @@
<n-form-item label="状态名称" path="name">
<n-input v-model:value="form.name" placeholder="如:处理中" maxlength="60" show-count />
</n-form-item>
<n-form-item label="标识" path="status_key">
<div class="aksrt-status-key">
<n-input
v-model:value="form.status_key"
class="aksrt-status-key__input"
:placeholder="editing ? '' : '留空则按名称自动生成'"
maxlength="24"
show-count
:input-props="{ autocomplete: 'off', spellcheck: false }"
@blur="form.status_key = normalizeKey(form.status_key)"
/>
<span class="aksrt-ticket-status__hint">
小写字母开头,仅含小写字母、数字与下划线(2–24 位)。修改后,使用该状态的工单会同步改为新标识。
</span>
</div>
</n-form-item>
<n-form-item label="状态颜色" path="color">
<div class="aksrt-status-color">
<div class="aksrt-status-color__swatches" role="radiogroup" aria-label="状态颜色">
@@ -132,7 +148,14 @@ const rows = ref([])
const DEFAULT_COLOR = '#6b7280'
const HEX_RE = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i
const form = ref({ name: '', color: DEFAULT_COLOR, is_closed: false })
/** 与 TicketStatusService::KEY_PATTERN 保持一致。 */
const KEY_RE = /^[a-z][a-z0-9_]{1,23}$/
const form = ref({ name: '', status_key: '', color: DEFAULT_COLOR, is_closed: false })
function normalizeKey(value) {
return String(value || '').trim().toLowerCase()
}
/**
* 归一化为 TicketStatusService::sanitize_color() 接受的小写 #rgb / #rrggbb;
@@ -149,6 +172,26 @@ const rules = {
{ required: true, message: '请填写状态名称', trigger: 'blur' },
{ max: 60, message: '状态名称不能超过 60 字', trigger: 'blur' },
],
status_key: [
{
// 新增时可留空(后端按名称生成);编辑时必须填写。
validator: (_rule, value) => {
const key = normalizeKey(value)
if (key === '') return !editing.value
return KEY_RE.test(key)
},
message: '标识需以小写字母开头,仅含小写字母、数字与下划线,长度 2–24 位',
trigger: ['blur', 'input'],
},
{
validator: (_rule, value) => {
const key = normalizeKey(value)
return key === '' || !rows.value.some((row) => row.status_key === key && row.id !== editing.value?.id)
},
message: '该标识已被其他状态使用',
trigger: ['blur', 'input'],
},
],
color: [
{
validator: (_rule, value) => toHex(value) !== '',
@@ -245,7 +288,7 @@ async function load() {
function openCreate() {
editing.value = null
form.value = { name: '', color: DEFAULT_COLOR, is_closed: false }
form.value = { name: '', status_key: '', color: DEFAULT_COLOR, is_closed: false }
visible.value = true
}
@@ -253,6 +296,7 @@ function openEdit(row) {
editing.value = row
form.value = {
name: String(row.name || ''),
status_key: String(row.status_key || ''),
color: toHex(row.color) || DEFAULT_COLOR,
is_closed: !!row.is_closed,
}
@@ -269,6 +313,7 @@ async function handleSubmit() {
try {
const payload = {
name: form.value.name.trim(),
status_key: normalizeKey(form.value.status_key),
color: currentHex.value,
is_closed: form.value.is_closed ? 1 : 0,
}
@@ -466,6 +511,18 @@ defineExpose({ reload: load })
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
}
/* —— 弹窗:标识 —— */
.aksrt-status-key {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
}
.aksrt-status-key__input {
font-family: var(--n-font-family-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
}
/* —— 弹窗:完结开关 —— */
.aksrt-status-closed {
display: flex;