@@ -55,6 +55,8 @@ const baseGuid = ref("");
/** 载入源定义的状态(已发布/停用版本进入编辑器时提示保存会新建草稿版本) */
const sourceStatus = ref ( 0 ) ;
const sourceVersion = ref ( 0 ) ;
/** 源流程线 flowGuid(新建态为空,右面板显示「保存后生成」) */
const sourceFlowGuid = ref ( "" ) ;
/** 归属应用(flw_def.app_key;按应用筛选维度,空=未分配) */
const appKey = ref ( "" ) ;
/** 应用清单下拉(应用设置白名单 flw_app enabled=1,显示应用名),拉取失败降级为仅当前值 */
@@ -71,6 +73,44 @@ const routers = reactive<RouterOption[]>([]);
const issues = computed < Issue [ ] > ( ( ) => validateModel ( model , depts , routers ) ) ;
const defJson = computed ( ( ) => toContentJson ( model ) ) ;
/** 结构统计(右面板分区4):walkItems 已递归条件/并行分支的子树 */
const stats = computed ( ( ) => {
const s : Record < string , number > = { approval : 0 , notify : 0 , condition : 0 , parallel : 0 } ;
walkItems ( model . list , ( item ) => {
if ( s [ item . kind ] !== undefined ) s [ item . kind ] += 1 ;
} ) ;
return s ;
} ) ;
/** 校验清单(右面板分区3):错误项在前、警告项在后(原型 E2 顺序) */
const issueList = computed < Issue [ ] > ( ( ) => [
... issues . value . filter ( ( i ) => i . level === "error" ) ,
... issues . value . filter ( ( i ) => i . level !== "error" ) ,
] ) ;
/** 点清单项 → 画布滚动到对应节点并高亮 ~3s;uid 为空(空容器类)不可定位 */
function locateIssue ( uid : string ) : void {
const el = document . getElementById ( "nd-" + uid ) ;
if ( ! el ) return ;
el . scrollIntoView ( { behavior : "smooth" , block : "center" } ) ;
el . classList . add ( "nd-locate" ) ;
window . setTimeout ( ( ) => el . classList . remove ( "nd-locate" ) , 2800 ) ;
}
/** flowGuid 复制:clipboard 不可用/被拒时降级 console,不打断编辑 */
function copyGuid ( ) : void {
const g = sourceFlowGuid . value ;
if ( ! g ) return ;
if ( navigator . clipboard ? . writeText ) {
navigator . clipboard . writeText ( g ) . then (
( ) => Message . success ( "flowGuid 已复制" ) ,
( ) => console . warn ( "flowGuid 复制失败,请手工选取:" , g ) ,
) ;
return ;
}
console . warn ( "flowGuid 复制失败,请手工选取:" , g ) ;
}
/** 归一化(去 layout 坐标、节点按 id 排序):卡片树投影与画布序列化表示不同但内容等价,脏比较必须同表示 */
function canonicalJson ( content : any ) : string {
const copy : any = JSON . parse ( JSON . stringify ( content ) ) ;
@@ -201,6 +241,7 @@ async function load(): Promise<void> {
const d : any = await flowApi . defGet ( queryId ) . send ( ) ;
sourceStatus . value = d ? . status ? ? 0 ;
sourceVersion . value = d ? . version ? ? 0 ;
sourceFlowGuid . value = d ? . flowGuid || queryGuid || "" ;
// 非草稿进入提示(2026-10-10 议题2):结构改动保存将升版本;仅改名称/归属应用请回列表用「编辑信息」
if ( d ? . status === 1 || d ? . status === 2 ) {
Message . info ( "结构改动保存将升版本;仅改名称/归属应用请返回列表用「编辑信息」(不升版本)" ) ;
@@ -536,6 +577,7 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
< / div >
< / header >
< div class = "ed-body" >
< main v-if = "!loading" class="ed-canvas" >
< div class = "ed-hint" > 点击卡片配置节点 · 悬停虚线处 + 插入节点 ( 审批 / 抄送 / 并行分支 ) < / div >
< div class = "ed-flow" >
@@ -544,10 +586,90 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
< div class = "capsule end" > 流程结束 < / div >
< / div >
< / main >
< main v-else-if = "loading" class= "ed-canvas loading">
< main v-else class = "ed-canvas loading" >
< Spin size = "large" fix / >
< / main >
<!-- 议题一 : 右信息面板 ( 五分区 , 规格对齐原型 E1 ) -- >
< aside class = "ed-info" >
< section class = "ip-sec" >
< h4 > 流程信息 < span class = "ip-mini" > 可编辑 · 保存走现有 defSave < / span > < / h4 >
< div class = "ip-fld" >
< label > 名称 < / label >
< Input v-model = "model.name" placeholder="流程名称" :maxlength="30" / >
< / div >
< div class = "ip-fld" >
< label > 编码 < span class = "ip-mini" > 升版本态禁用 ( 发布后不可改 ) < / span > < / label >
< Input v-model = "model.code" placeholder="内部管理键,发布后不可改"
: disabled = "!!editId || !!baseGuid" :maxlength = "40" / >
< / div >
< div class = "ip-fld" >
< label > 归属应用 < span style = "color: #ed4014" > * < / span > < / label >
< Select v-model = "appKey" placeholder="归属应用(必选)" filterable >
< Option v-for = "a in appOptions" :key="a.appKey" :value="a.appKey" > {{ a.appName }} < / Option >
< / Select >
< / div >
< / section >
< section class = "ip-sec" >
< h4 > 状态与版本 < span class = "ip-mini" > 只读 < / span > < / h4 >
< div class = "ip-tags" >
< Tag v-if = "editId && sourceStatus === 0" color="orange" > 草稿 < / Tag >
< Tag v-else-if = "editId" color="red" > {{ DEF_STATUS [ sourceStatus ] }} · 保存将新建草稿版本 < / Tag >
< Tag v-else-if = "baseGuid" color="blue" > 升版本 · 基于 v {{ sourceVersion }} < / Tag >
< Tag v-else color = "green" > 新建 < / Tag >
< Tag v-if = "sourceVersion" color="gray" > v {{ sourceVersion }} < / Tag >
< / div >
< div class = "ip-guid" >
< span v-if = "sourceFlowGuid" class="ip-mono" > {{ sourceFlowGuid }} < / span >
< span v-else class = "ip-mini" > 保存后生成 < / span >
< Button v-if = "sourceFlowGuid" size="small" @click="copyGuid" > 复制 < / Button >
< / div >
< div class = "ip-tip" > flowGuid 发布后不变 , 业务方绑定流程用此键 < / div >
< / section >
< section class = "ip-sec" >
< h4 > 校验问题清单
< span class = "ip-mini" > { { countErrors ( issues ) . length ? ` ${ issueList . length } 项待处理 `
: ( countWarns ( issues ) . length ? ` ${ countWarns ( issues ) . length } 个警告 ` : "无问题" ) } } < / span >
< / h4 >
< div v-if = "!issueList.length" class="ip-ok" > ✓ 校验通过 < / div >
< div v-else-if = "!countErrors(issues).length" class="ip-ok" >
✓ 校验通过 ( {{ countWarns ( issues ) .length }} 个警告 )
< / div >
< div v-for = "(i, idx) in issueList" :key="`${i.uid}-${idx}`" class="ip-issue"
: class = "[(i.level === 'error' ? 'err' : 'warn'), (i.uid ? 'loc' : 'fix')]"
@click ="i.uid && locateIssue(i.uid)" >
< span class = "ip-ic" > { { i . level === "error" ? "✗" : "!" } } < / span >
< span class = "ip-tx" > { { i . path } } : { { i . msg } }
< span v-if = "i.level !== 'error'" class="ip-sub" > ( 警告 , 不阻塞保存 ) < / span >
< / span >
< span v-if = "i.uid" class="ip-go" > 定位 → < / span >
< / div >
< / section >
< section class = "ip-sec" >
< h4 > 结构统计 < / h4 >
< div class = "ip-stats" >
< div class = "ip-stat" > < b > { { stats . approval } } < / b > < span > 审批节点 < / span > < / div >
< div class = "ip-stat" > < b > { { stats . notify } } < / b > < span > 抄送节点 < / span > < / div >
< div class = "ip-stat" > < b > { { stats . condition } } < / b > < span > 条件分支 < / span > < / div >
< div class = "ip-stat" > < b > { { stats . parallel } } < / b > < span > 并行分支 < / span > < / div >
< / div >
< / section >
< section class = "ip-sec" >
< h4 > 快捷入口 < / h4 >
< div class = "ip-quick" >
< Button size = "small" @click ="settingsDrawer = true" > 流程设置 < / Button >
< Button size = "small" @click ="jsonDrawer = true" > 源码 < / Button >
< Button size = "small" :disabled = "!editId" @click ="onOpenDiagram" > 图示预览 < / Button >
< / div >
< div class = "ip-tip" > 「 图示预览 」 仅已载入的草稿 / 版本可用 ( 同现状 : disabled = "!editId" ) < / div >
< / section >
< / aside >
< / div >
< footer class = "ed-status" : class = "{ bad: countErrors(issues).length }" >
< span v-if = "countErrors(issues).length" class="st-bad" >
✗ {{ countErrors ( issues ) .length }} 个问题 : {{ countErrors ( issues ) .map ( ( i ) = > i . text ) . join ( "; " ) } }
@@ -645,7 +767,7 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
flex : 1 ;
display : flex ;
flex - direction : column ;
align - items : center ;
align - items : flex - start ;
padding : 20 px 16 px 60 px ;
overflow : auto ;
position : relative ;
@@ -756,4 +878,146 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
color : # b0b6c1 ;
line - height : 32 px ;
}
/* 议题一:左画布 + 右信息面板(原型 E1,面板宽 340) */
. ed - body {
flex : 1 ;
display : flex ;
min - height : 0 ;
}
. ed - info {
width : 340 px ;
flex : none ;
overflow : auto ;
border - left : 1 px solid # e8eaec ;
background : # fff ;
padding : 4 px 14 px 20 px ;
}
. ip - sec {
padding : 12 px 0 ;
border - bottom : 1 px solid # f0f1f3 ;
}
. ip - sec : last - child {
border - bottom : none ;
}
. ip - sec h4 {
font - size : 13 px ;
font - weight : 600 ;
color : # 17233 d ;
margin : 0 0 10 px ;
display : flex ;
align - items : baseline ;
gap : 8 px ;
}
. ip - mini {
font - weight : 400 ;
font - size : 12 px ;
color : # a5abb8 ;
}
. ip - fld {
margin - bottom : 10 px ;
}
. ip - fld label {
display : block ;
font - size : 12 px ;
color : # 808695 ;
margin - bottom : 4 px ;
}
. ip - tags {
display : flex ;
flex - wrap : wrap ;
gap : 6 px ;
margin - bottom : 8 px ;
}
. ip - guid {
display : flex ;
align - items : center ;
gap : 8 px ;
}
. ip - mono {
font - family : Consolas , Monaco , "Courier New" , monospace ;
font - size : 12 px ;
color : # 515 a6e ;
word - break : break - all ;
}
. ip - tip {
margin - top : 6 px ;
font - size : 12 px ;
color : # a5abb8 ;
line - height : 1.6 ;
}
. ip - ok {
font - size : 12.5 px ;
color : # 19 be6b ;
}
. ip - issue {
display : flex ;
gap : 6 px ;
align - items : flex - start ;
font - size : 12.5 px ;
line - height : 1.6 ;
padding : 5 px 6 px ;
border - radius : 4 px ;
margin - bottom : 4 px ;
}
. ip - issue . err {
background : # fff5f5 ;
color : # ed4014 ;
}
. ip - issue . warn {
background : # fff9f0 ;
color : # ff9900 ;
}
. ip - issue . loc {
cursor : pointer ;
}
. ip - issue . loc : hover {
filter : brightness ( 0.97 ) ;
}
. ip - issue . fix {
cursor : default ;
}
. ip - ic {
flex : none ;
font - weight : 700 ;
}
. ip - tx {
flex : 1 ;
}
. ip - sub {
color : # a5abb8 ;
}
. ip - go {
flex : none ;
font - size : 12 px ;
}
. ip - stats {
display : flex ;
gap : 8 px ;
}
. ip - stat {
flex : 1 ;
text - align : center ;
background : # f7f9fc ;
border - radius : 6 px ;
padding : 8 px 0 ;
}
. ip - stat b {
display : block ;
font - size : 16 px ;
color : # 17233 d ;
}
. ip - stat span {
font - size : 12 px ;
color : # 808695 ;
}
. ip - quick {
display : flex ;
gap : 8 px ;
flex - wrap : wrap ;
}
/* 校验清单定位高亮(原型 E2,用项目错误红不做原型紫);卡片在子组件内,需 :deep 命中 */
: deep ( . nd - locate ) {
box - shadow : 0 0 0 4 px rgba ( 237 , 64 , 20 , 0.18 ) ! important ;
}
< / style >