Archived
版本号自己定:点编辑,整列版本进编辑态,取消/保存收尾
用户的目的是**自己改版本号**,不是让系统替他递增;顺带把上一轮那些
指示性交互全撤了 —— 这软件给专业业务设计人员用,不需要「点我改」的提示条。
界面:
· 顶栏 编辑 → 取消 / 保存(与流程图卡「编辑 → 保存」同一套手势);
· 行内两个字段:号(自己编)与名(各版不同),行尾 删除;
· 改动攒在草稿里,按保存一趟落库;取消原样退出;
· **删掉**:改名指示条、「点我改」虚下划线、`删掉 v4`/`激活 v4` 这类复述钮名的浮字。
只留下禁用态那一句(生效版先停用 / 还不闭合),那不是指示,是钮为什么点不动。
后端 —— 号从「系统递增」放开为「设计人员自己编」,两道闸是数据本身要求的:
· 撞号拒(VersionTakenError,409);
· 在途钉版拒改号(那些单正按这个号找定义,搬号等于抽它们脚下的地板);
· 已完结的单**跟着改指**:号在三处(包的列、definition、workflow.packageVersion)
一起改 —— 号只是这一版的名牌,搬号连引用一起搬,留痕仍指同一份定义。
先前写成「跑过单就不许改号」,实测等于任何试跑过的包永远编不了号,是过闸。
· 名不设闸:标签不参与流转,在途钉版也能改;改名只动这一版(推翻前一日的
「及于整条流」)—— 各版本正是靠名字互相区分,全同名等于没名字。
路由 POST /api/gov/process/:id/:version/edit 取代 …/:id/rename(单一消费者是本仓前端);
CONTRACT.md 与产品设计.md 同步,api-report 快照随本次提交。
实测(丁子乔账号,测后已 reset-runs 清运行数据):v4→v2→v4 往返,列表、画布图签、
库里三处同步;在途单存在时改号如实报 409 并原样退回。五闸绿。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -553,7 +553,10 @@
|
||||
/* 三卡同厚:头行内容一律 26 高(= 一枚方钮),没按钮的头也不塌 —— 三条底线同一水平 */
|
||||
.g-verslim .g-vercard-h { height: auto; min-height: 26px; padding: 14px 16px 11px; gap: 4px; }
|
||||
.g-verslim .g-vercard-list { padding: 12px; gap: 6px; }
|
||||
.g-verrow { display: flex; align-items: center; gap: 8px; width: 100%; padding: 7px 8px 7px 11px; border: 1px solid var(--sh-line); border-left: 3px solid var(--sh-steel); background: #fff; font-family: var(--sh-mono); font-size: 10px; color: #3a4452; text-align: left; cursor: pointer; transition: border-color var(--t-fast); }
|
||||
/* 版本行两行:上行 版本号 · 名字 · 动作;下行只放事实(状态 · 点边计数) */
|
||||
.g-verrow { display: flex; flex-direction: column; gap: 4px; width: 100%; padding: 7px 8px 8px 11px; border: 1px solid var(--sh-line); border-left: 3px solid var(--sh-steel); background: #fff; font-family: var(--sh-mono); font-size: 10px; color: #3a4452; text-align: left; cursor: pointer; transition: border-color var(--t-fast); }
|
||||
.g-verrow .ln { display: flex; align-items: center; gap: 7px; }
|
||||
.g-verrow .ln2 { display: flex; align-items: center; gap: 8px; padding-left: 1px; }
|
||||
/* 行尾小钮:启停(平时)/ 删除(编辑态)—— 比动作条矮一号,行不发胖 */
|
||||
.g-verrow .g-ver-act { height: 22px; padding: 0 8px; font-size: 10.5px; }
|
||||
.g-verrow:hover { border-color: #aeb9c6; }
|
||||
@@ -561,20 +564,16 @@
|
||||
.g-verrow.live { border-left-color: var(--sh-brass); background: var(--sh-brass-wash); }
|
||||
.g-verrow.live.on { border-color: #cbb98d; }
|
||||
.g-verrow:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
|
||||
.g-verrow > b { font-size: 13px; font-weight: 700; color: var(--sh-ink); }
|
||||
.g-verrow b { font-size: 13px; font-weight: 700; color: var(--sh-ink); }
|
||||
/* 版本名:占满余下、一行截断 —— 界面字体,不用图签的等宽 */
|
||||
.g-verrow .nm { flex: 1; min-width: 0; font: 500 11.5px/1.3 var(--ui-sans); color: var(--sh-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
/* 编辑态:号与名就是两个输入框(号自己编),光标默认,不再是可点切版的行 */
|
||||
.g-verrow.ed { cursor: default; }
|
||||
.g-verrow .g-input.vnum { flex: none; width: 42px; height: 22px; padding: 0 4px; font-family: var(--sh-mono); font-size: 11.5px; text-align: center; }
|
||||
.g-verrow .g-input.nm { flex: 1; min-width: 0; height: 22px; padding: 0 6px; font-size: 11.5px; }
|
||||
.g-verrow .st { font-size: 9px; letter-spacing: .12em; color: var(--sh-muted); }
|
||||
.g-verrow .st.live { color: var(--sh-brass); font-weight: 700; }
|
||||
.g-verrow .cnt { margin-left: auto; font-size: 9.5px; color: var(--sh-muted); white-space: nowrap; }
|
||||
.g-verrow .cnt + .g-ver-act { margin-left: 8px; }
|
||||
/* 版本记录编辑态:一条浅带 —— 改名钮与「行尾可删」的交代住这儿 */
|
||||
.g-veredit { flex: none; display: flex; align-items: center; gap: 8px; padding: 9px 16px; border-bottom: 1px solid var(--sh-line); background: #f9fafc; }
|
||||
.g-veredit .t { font-family: var(--sh-mono); font-size: 9px; line-height: 1.6; color: var(--sh-muted); }
|
||||
.g-veredit .g-verrename { flex: 1; padding: 0; border: 0; background: transparent; }
|
||||
|
||||
/* 改名条:编辑态里点「改名」就地展开 */
|
||||
.g-verrename { flex: none; display: flex; flex-direction: column; gap: 5px; padding: 9px 14px; border-bottom: 1px solid var(--sh-line); background: #f9fafc; }
|
||||
.g-verrename .g-input { height: 26px; font-size: 12px; }
|
||||
.g-verrename .t { font-family: var(--sh-mono); font-size: 8.5px; line-height: 1.7; color: var(--sh-muted); }
|
||||
.g-ver { display: flex; align-items: center; gap: 9px; padding: 9px 11px; border: 1px solid var(--sh-line); border-left: 3px solid var(--sh-steel); background: #fff; cursor: pointer; font-family: var(--sh-mono); font-size: 11px; color: #3a4452; }
|
||||
.g-ver:hover { border-color: #aeb9c6; }
|
||||
.g-ver.on { background: #f2f5f8; border-color: #9fb0c0; }
|
||||
|
||||
@@ -15,12 +15,13 @@
|
||||
* · 在途工作流按启动时钉住的版本跑完 —— 启停对它们零影响
|
||||
*/
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import type { KeyboardEvent } from 'react';
|
||||
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
|
||||
import type { ClosureReport, ProcessPackage, ProcessNode } from '@contracts';
|
||||
import {
|
||||
deleteProcessVersionRoute,
|
||||
processPackageSchema,
|
||||
renameProcessRoute,
|
||||
editProcessVersionRoute,
|
||||
saveProcessRoute,
|
||||
setProcessStateRoute,
|
||||
validateProcessRoute,
|
||||
@@ -359,13 +360,14 @@ function FlowLab({
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [report, setReport] = useState<ClosureReport | undefined>(undefined);
|
||||
const [ask, setAsk] = useState<{ spec: AskSpec; danger?: boolean } | undefined>(undefined);
|
||||
/** 改名就地开一个输入框(名字改的是整条流,不是某一版) */
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
/**
|
||||
* **版本记录**的编辑态(2026-07-30):开着才出改名条、行尾钮换成删除 ——
|
||||
* **版本记录**的编辑态:开着,每行的号与名就是输入框、行尾钮换成删除;
|
||||
* 顶栏两枚钮 取消 / 保存 —— 改动攒在草稿里,按保存才落库。
|
||||
* 它只管版本记录这一件事,与画布的手绘编辑(那是流程图卡自己的)互不相干。
|
||||
*/
|
||||
const [editingVersions, setEditingVersions] = useState(false);
|
||||
/** 版本记录草稿:原版本号 → 改后的号与名 */
|
||||
const [edits, setEdits] = useState<Record<number, { version: string; name: string }>>({});
|
||||
const canvasRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const nodes = graph?.nodes ?? [];
|
||||
@@ -463,15 +465,32 @@ function FlowLab({
|
||||
onError: (e: unknown) => setSaveErr((e as { message?: string }).message ?? '删不掉'),
|
||||
});
|
||||
|
||||
/** 改名:名字是这条流的身份 —— 一次改及于所有版本 */
|
||||
const rename = useMutation({
|
||||
mutationFn: (name: string) =>
|
||||
call<{ packages: ProcessPackage[] }>(renameProcessRoute, { params: { id: pkg.id }, body: { name } }),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov', 'process'] });
|
||||
setRenaming(false);
|
||||
/**
|
||||
* 存版本记录:逐行把改过的号与名送上去 —— 撞号、跑过单改号,服务端 409,判词照抄。
|
||||
* 一行不通,前面已落的不回滚:版本记录各自独立,没有「半条」可言。
|
||||
*/
|
||||
const saveVersions = useMutation({
|
||||
mutationFn: async (drafts: readonly { from: number; version: number; name: string }[]) => {
|
||||
let packages: ProcessPackage[] = [];
|
||||
for (const d of drafts) {
|
||||
const out = await call<{ packages: ProcessPackage[] }>(editProcessVersionRoute, {
|
||||
params: { id: pkg.id, version: String(d.from) },
|
||||
body: { version: d.version, name: d.name },
|
||||
});
|
||||
packages = out.packages;
|
||||
}
|
||||
return packages;
|
||||
},
|
||||
onError: (e: unknown) => setSaveErr((e as { message?: string }).message ?? '改名失败'),
|
||||
onSuccess: (_packages, drafts) => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov', 'process'] });
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov', 'process-reports'] });
|
||||
setEditingVersions(false);
|
||||
setEdits({});
|
||||
// 正看的这一版换了号 → 跟着落到新号上,别停在一个已经不存在的号
|
||||
const moved = drafts.find((d) => d.from === pkg.version);
|
||||
if (moved !== undefined && moved.version !== pkg.version) onSaved(moved.version);
|
||||
},
|
||||
onError: (e: unknown) => setSaveErr((e as { message?: string }).message ?? '版本记录没存上'),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -673,107 +692,162 @@ function FlowLab({
|
||||
<aside className="g-vercard g-verslim">
|
||||
<div className="g-vercard-h">
|
||||
版本
|
||||
<button className="g-ver-act" onClick={askClone} disabled={cloneVersion.isPending} title="以当前版为底克隆一版">
|
||||
<Ico name="plus" s={11} /> 新建
|
||||
</button>
|
||||
{/* 这枚「编辑」只作用于**版本记录**:改名与删除 —— 画布的编辑在流程图卡上 */}
|
||||
<button
|
||||
className={'g-ver-act' + (editingVersions ? ' t-go' : '')}
|
||||
title={editingVersions ? '完成:行尾钮变回启停' : '编辑版本记录:改名 · 删除'}
|
||||
onClick={() => {
|
||||
setEditingVersions(!editingVersions);
|
||||
setRenaming(false);
|
||||
}}
|
||||
>
|
||||
<Ico name={editingVersions ? 'check' : 'pen-line'} s={11} /> {editingVersions ? '完成' : '编辑'}
|
||||
</button>
|
||||
{/*
|
||||
* 编辑态两枚钮:取消 / 保存。改动攒在草稿里 —— 号与名一趟改完再落库,
|
||||
* 与画布的「编辑 → 保存」同一套手势。
|
||||
*/}
|
||||
{editingVersions ? (
|
||||
<>
|
||||
<button
|
||||
className="g-ver-act"
|
||||
disabled={saveVersions.isPending}
|
||||
onClick={() => {
|
||||
setEditingVersions(false);
|
||||
setEdits({});
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className="g-ver-act"
|
||||
disabled={saveVersions.isPending}
|
||||
onClick={() => {
|
||||
const drafts = versions.flatMap((v) => {
|
||||
const d = edits[v.version];
|
||||
if (d === undefined) return [];
|
||||
const num = Number(d.version);
|
||||
const name = d.name.trim();
|
||||
if (!Number.isInteger(num) || num < 1 || name === '') return [];
|
||||
if (num === v.version && name === v.name) return [];
|
||||
return [{ from: v.version, version: num, name }];
|
||||
});
|
||||
if (drafts.length === 0) {
|
||||
setEditingVersions(false);
|
||||
setEdits({});
|
||||
return;
|
||||
}
|
||||
saveVersions.mutate(drafts);
|
||||
}}
|
||||
>
|
||||
<Ico name="send" s={11} /> {saveVersions.isPending ? '保存中…' : '保存'}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button className="g-ver-act" onClick={askClone} disabled={cloneVersion.isPending}>
|
||||
<Ico name="plus" s={11} /> 新建
|
||||
</button>
|
||||
{/* 这枚「编辑」只作用于**版本记录**:号、名、删 —— 画布的编辑在流程图卡上 */}
|
||||
<button
|
||||
className="g-ver-act"
|
||||
onClick={() => {
|
||||
setEdits(Object.fromEntries(versions.map((v) => [v.version, { version: String(v.version), name: v.name }])));
|
||||
setEditingVersions(true);
|
||||
}}
|
||||
>
|
||||
<Ico name="pen-line" s={11} /> 编辑
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{editingVersions && (
|
||||
<div className="g-veredit">
|
||||
{renaming ? (
|
||||
<div className="g-verrename">
|
||||
<input
|
||||
className="g-input"
|
||||
autoFocus
|
||||
defaultValue={pkg.name}
|
||||
placeholder="流程名"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') rename.mutate((e.target as HTMLInputElement).value.trim());
|
||||
if (e.key === 'Escape') setRenaming(false);
|
||||
}}
|
||||
/>
|
||||
<span className="t">回车改名 · Esc 取消 —— 名字是这条流的身份,各版本同名</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<button className="g-ver-act" onClick={() => setRenaming(true)}>
|
||||
改名
|
||||
</button>
|
||||
<span className="t">行尾可删版本 —— 生效版先停用</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="g-vercard-list">
|
||||
{versions.map((v) => {
|
||||
const isActive = v.version === activeVersion;
|
||||
const isShown = v.version === pkg.version;
|
||||
const draft = edits[v.version];
|
||||
/*
|
||||
* 编辑态的行不再是「可点切版」的按钮 —— 它是一排字段:号、名、删。
|
||||
* 展示态才是按钮(点行切到这一版看)。
|
||||
*/
|
||||
const pickable = editingVersions
|
||||
? {}
|
||||
: {
|
||||
role: 'button',
|
||||
tabIndex: 0,
|
||||
onClick: () => onPick(v.version),
|
||||
onKeyDown: (e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onPick(v.version);
|
||||
}
|
||||
},
|
||||
};
|
||||
return (
|
||||
/* 行本身可点(切到这一版看);行尾一枚小钮 —— 平时是启停,编辑态是删除 */
|
||||
<div
|
||||
key={v.version}
|
||||
className={'g-verrow' + (isShown ? ' on' : '') + (isActive ? ' live' : '')}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onPick(v.version)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onPick(v.version);
|
||||
}
|
||||
}}
|
||||
className={'g-verrow' + (isShown ? ' on' : '') + (isActive ? ' live' : '') + (editingVersions ? ' ed' : '')}
|
||||
{...pickable}
|
||||
>
|
||||
<b>v{v.version}</b>
|
||||
<span className={'st' + (isActive ? ' live' : '')}>{isActive ? '生效' : '停用'}</span>
|
||||
<span className="cnt">
|
||||
{v.nodes.length} 节点 · {v.edges.length} 边
|
||||
</span>
|
||||
{editingVersions ? (
|
||||
<button
|
||||
className="g-ver-act t-bad"
|
||||
disabled={isActive || dropVersion.isPending}
|
||||
title={isActive ? '生效版删不掉 —— 先停用' : `删掉 v${v.version}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
askDrop(v);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
) : isActive ? (
|
||||
<button
|
||||
className="g-ver-act"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
askRetire(v);
|
||||
}}
|
||||
>
|
||||
停用
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="g-ver-act t-go"
|
||||
/* 不闭合点不了:结构闭合 + 活动装载两面清零才亮 */
|
||||
disabled={closedOf(v.version) !== true}
|
||||
title={closedOf(v.version) === true ? `激活 v${v.version}` : '还不闭合 —— 属性面会列出差哪几条'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
askActivate(v);
|
||||
}}
|
||||
>
|
||||
激活
|
||||
</button>
|
||||
)}
|
||||
<div className="ln">
|
||||
{/* 号与名都归设计人员自己定:号自己编,名各版不同 —— 版本靠这两样互相区分 */}
|
||||
{editingVersions && draft !== undefined ? (
|
||||
<>
|
||||
<b>v</b>
|
||||
<input
|
||||
className="g-input vnum"
|
||||
inputMode="numeric"
|
||||
value={draft.version}
|
||||
onChange={(e) =>
|
||||
setEdits({ ...edits, [v.version]: { ...draft, version: e.target.value.replace(/\D/g, '') } })
|
||||
}
|
||||
/>
|
||||
<input
|
||||
className="g-input nm"
|
||||
maxLength={40}
|
||||
value={draft.name}
|
||||
onChange={(e) => setEdits({ ...edits, [v.version]: { ...draft, name: e.target.value } })}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<b>v{v.version}</b>
|
||||
<span className="nm">{v.name}</span>
|
||||
</>
|
||||
)}
|
||||
{editingVersions ? (
|
||||
<button
|
||||
className="g-ver-act t-bad"
|
||||
disabled={isActive || dropVersion.isPending}
|
||||
title={isActive ? '生效版先停用' : ''}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
askDrop(v);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
) : isActive ? (
|
||||
<button
|
||||
className="g-ver-act"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
askRetire(v);
|
||||
}}
|
||||
>
|
||||
停用
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="g-ver-act t-go"
|
||||
/* 不闭合点不了:结构闭合 + 活动装载两面清零才亮 */
|
||||
disabled={closedOf(v.version) !== true}
|
||||
title={closedOf(v.version) === true ? '' : '还不闭合'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
askActivate(v);
|
||||
}}
|
||||
>
|
||||
激活
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{/* 第二行只放事实:状态 · 点边计数 */}
|
||||
<div className="ln2">
|
||||
<span className={'st' + (isActive ? ' live' : '')}>{isActive ? '生效' : '停用'}</span>
|
||||
<span className="cnt">
|
||||
{v.nodes.length} 节点 · {v.edges.length} 边
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user