feat: 归档L03与L09审批发布能力

- 新增统一审批中心与审批管理页面,支持流程配置、审批详情与角色/用户审批对象

- 接入聊天助手、知识库、工作流的发布与删除审批,并补齐发布态校验与快照展示
This commit is contained in:
2026-04-07 14:41:52 +08:00
parent 7e7c236c2a
commit 3f128e977a
138 changed files with 13035 additions and 346 deletions

View File

@@ -20,6 +20,7 @@
} = $props();
let { data } = options;
let initialViewport = null;
if (typeof data === 'string') {
try {
@@ -28,7 +29,12 @@
console.error('Invalid JSON data:', data);
}
}
store.init((data as TinyflowData)?.nodes || [], (data as TinyflowData)?.edges || []);
initialViewport = (data as TinyflowData)?.viewport || null;
store.init(
(data as TinyflowData)?.nodes || [],
(data as TinyflowData)?.edges || [],
initialViewport,
);
setContext('tinyflow_options', options);
</script>

View File

@@ -57,9 +57,11 @@
let flowRootEl = $state<HTMLDivElement | null>(null);
let inlineNodePickerEl = $state<HTMLDivElement | null>(null);
let connectStartPoint = $state<{ x: number; y: number } | null>(null);
let canvasLocked = $state(false);
const asString = (value: unknown) => (value == null ? '' : String(value));
const options = getOptions();
const readonly = options.readonly === true;
let canvasLocked = $state(readonly);
const hideBottomDock = options.hideBottomDock === true;
const availableNodes = getAvailableNodes(options);
const onRunTest = options.onRunTest;
@@ -567,6 +569,9 @@
};
function handleGlobalPointerDown(event: PointerEvent) {
if (readonly) {
return;
}
if (!nodePickerVisible || !inlineNodePickerEl) {
return;
}
@@ -579,15 +584,19 @@
onMount(() => {
store.updateEdges((edges) => edges.map((edge) => ensureEdgeVisualDefaults(edge)));
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('paste', handleGlobalPaste);
window.addEventListener('pointerdown', handleGlobalPointerDown);
if (!readonly) {
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('paste', handleGlobalPaste);
window.addEventListener('pointerdown', handleGlobalPointerDown);
}
});
onDestroy(() => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('paste', handleGlobalPaste);
window.removeEventListener('pointerdown', handleGlobalPointerDown);
if (!readonly) {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('paste', handleGlobalPaste);
window.removeEventListener('pointerdown', handleGlobalPointerDown);
}
});
const customNodeTypes = {
@@ -630,24 +639,30 @@
nodesDraggable={!canvasLocked}
nodesConnectable={!canvasLocked}
elementsSelectable={!canvasLocked}
panOnDrag={!canvasLocked}
zoomOnScroll={!canvasLocked}
zoomOnDoubleClick={!canvasLocked}
ondrop={onDrop}
ondragover={onDragOver}
panOnDrag={readonly ? true : !canvasLocked}
zoomOnScroll={readonly ? true : !canvasLocked}
zoomOnDoubleClick={readonly ? true : !canvasLocked}
ondrop={readonly ? undefined : onDrop}
ondragover={readonly ? undefined : onDragOver}
isValidConnection={isValidConnection}
onconnectend={onconnectend}
onconnectstart={onconnectstart}
onconnect={onconnect}
onconnectend={readonly ? undefined : onconnectend}
onconnectstart={readonly ? undefined : onconnectstart}
onconnect={readonly ? undefined : onconnect}
connectionRadius={50}
connectionLineComponent={FlowConnectionLine}
onedgeclick={(e) => {
if (readonly) {
return;
}
showEdgePanel = true;
currentEdge = e.edge;
}}
onbeforeconnect={(edge: any) => normalizeEdgeBeforeConnect(edge)}
ondelete={onDelete}
ondelete={readonly ? undefined : onDelete}
onclick={(e) => {
if (readonly) {
return;
}
const el = e.target as HTMLElement;
if (el.classList.contains("svelte-flow__edge-interaction")
|| el.classList.contains('panel-content')
@@ -750,6 +765,7 @@
<NodePicker nodes={availableNodes} onSelect={handlePickerSelectNode} />
</div>
{/if}
{#if !hideBottomDock}
<div class="tf-bottom-dock">
<div class="tf-unified-bar">
<!-- 缩放百分比选择器 -->
@@ -818,6 +834,7 @@
</button>
{/if}
</div>
{/if}
</div>
<style>

View File

@@ -2,27 +2,14 @@ import { store } from '#store/stores.svelte';
import { genShortId } from '#components/utils/IdGen';
import { type Edge, type Node, useSvelteFlow } from '@xyflow/svelte';
import { sanitizeEdge, sanitizeNode } from '#utils/sanitize';
interface ClipboardData {
tinyflowNodes: Node[];
tinyflowEdges?: Edge[];
version: string;
}
/**
* 清理节点中不可序列化的字段,确保可安全 JSON.stringify
*/
function sanitizeNode(node: Node): Node {
const { id, type, position, data, parentId } = node;
return {
id,
type,
position: { x: position.x, y: position.y },
parentId: parentId ? parentId : undefined,
data: data ? JSON.parse(JSON.stringify(data)) : {},
...(parentId !== undefined && { parentId }),
};
}
/**
* 对 nodes 数组排序,确保每个父节点出现在其所有子节点之前。
* 使用 Kahn 算法(拓扑排序)处理任意嵌套层级。
@@ -83,22 +70,6 @@ export function sortNodesForSvelteFlow(nodes: Node[]): Node[] {
return result;
}
/**
* 清理边中不可序列化的字段
*/
function sanitizeEdge(edge: Edge): Edge {
const { id, source, target, sourceHandle, targetHandle, type, data } = edge;
return {
id,
source,
target,
...(sourceHandle !== undefined && { sourceHandle }),
...(targetHandle !== undefined && { targetHandle }),
...(type !== undefined && { type }),
data: data ? JSON.parse(JSON.stringify(data)) : {},
};
}
/**
* 递归遍历对象,仅当遇到 { refType: 'ref', ref: string } 时重写 ref 的 id
*/