feat: simplify SOP to immediate-effect configuration
This commit is contained in:
1
web/dist/assets/BookOutlined-Canuw-q-.js
vendored
1
web/dist/assets/BookOutlined-Canuw-q-.js
vendored
@@ -1 +0,0 @@
|
||||
import{F as e,a as t}from"./client-Bk7n1O1W.js";var n={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32zm-260 72h96v209.9L621.5 312 572 347.4V136zm220 752H232V136h280v296.9c0 3.3 1 6.6 3 9.3a15.9 15.9 0 0022.3 3.7l83.8-59.9 81.4 59.4c2.7 2 6 3.1 9.4 3.1 8.8 0 16-7.2 16-16V136h64v752z`}}]},name:`book`,theme:`outlined`};function r(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]==null?{}:Object(arguments[t]),r=Object.keys(n);typeof Object.getOwnPropertySymbols==`function`&&(r=r.concat(Object.getOwnPropertySymbols(n).filter(function(e){return Object.getOwnPropertyDescriptor(n,e).enumerable}))),r.forEach(function(t){i(e,t,n[t])})}return e}function i(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var a=function(i,a){var o=r({},i,a.attrs);return e(t,r({},o,{icon:n}),null)};a.displayName=`BookOutlined`,a.inheritAttrs=!1;export{a as t};
|
||||
1
web/dist/assets/CopyOutlined-lDB-4CmK.js
vendored
Normal file
1
web/dist/assets/CopyOutlined-lDB-4CmK.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{F as e,a as t}from"./client-Bk7n1O1W.js";var n={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z`}}]},name:`copy`,theme:`outlined`};function r(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]==null?{}:Object(arguments[t]),r=Object.keys(n);typeof Object.getOwnPropertySymbols==`function`&&(r=r.concat(Object.getOwnPropertySymbols(n).filter(function(e){return Object.getOwnPropertyDescriptor(n,e).enumerable}))),r.forEach(function(t){i(e,t,n[t])})}return e}function i(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var a=function(i,a){var o=r({},i,a.attrs);return e(t,r({},o,{icon:n}),null)};a.displayName=`CopyOutlined`,a.inheritAttrs=!1;export{a as t};
|
||||
1
web/dist/assets/DashboardView-Btr47lQ_.css
vendored
1
web/dist/assets/DashboardView-Btr47lQ_.css
vendored
@@ -1 +0,0 @@
|
||||
.metric-strip[data-v-81958fcc]{grid-template-columns:repeat(4,1fr);display:grid;overflow:hidden}.metric[data-v-81958fcc]{border-right:1px solid var(--line);align-items:center;gap:16px;min-height:116px;padding:22px;display:flex}.metric[data-v-81958fcc]:last-child{border-right:0}.metric-icon[data-v-81958fcc],.completion[data-v-81958fcc]{border-radius:5px;place-items:center;width:42px;height:42px;font-size:19px;display:grid}.metric-icon.green[data-v-81958fcc]{color:#16775b;background:#e5f3ee}.metric-icon.coral[data-v-81958fcc]{color:#c4573f;background:#faece7}.metric-icon.amber[data-v-81958fcc]{color:#a86e1e;background:#f8efdf}.completion[data-v-81958fcc]{color:#202825;background:#e8ecea;font-size:12px;font-weight:800}.metric div[data-v-81958fcc]{flex-direction:column;display:flex}.metric b[data-v-81958fcc]{font-family:Noto Serif SC,serif;font-size:28px;line-height:1}.metric small[data-v-81958fcc]{color:var(--muted);margin-top:8px}.work-grid[data-v-81958fcc]{grid-template-columns:1.05fr .95fr;gap:18px;margin-top:18px;display:grid}.action-panel[data-v-81958fcc],.doctrine-panel[data-v-81958fcc]{padding:22px}.panel-kicker[data-v-81958fcc]{color:#7c8883;text-transform:uppercase;margin-bottom:14px;font-size:11px;font-weight:700}.action-panel button[data-v-81958fcc]{width:100%;color:var(--ink);text-align:left;border:0;border-bottom:1px solid var(--line);cursor:pointer;background:0 0;justify-content:space-between;align-items:center;padding:15px 0;display:flex}.action-panel button[data-v-81958fcc]:last-child{border-bottom:0}.action-panel button[data-v-81958fcc]:hover{color:var(--green)}.action-panel button span[data-v-81958fcc]{flex-direction:column;font-weight:650;display:flex}.action-panel button small[data-v-81958fcc]{color:var(--muted);margin-top:4px;font-weight:400}.doctrine-panel[data-v-81958fcc]{color:#fff;background:#202825;border-color:#202825}.doctrine-panel .panel-kicker[data-v-81958fcc]{color:#89a099}blockquote[data-v-81958fcc]{margin:30px 0 42px;font-family:Noto Serif SC,serif;font-size:22px;line-height:1.65}.flow-note[data-v-81958fcc]{color:#9cafaa;align-items:center;font-size:12px;display:flex}.flow-note i[data-v-81958fcc]{background:#4a5a54;flex:1;height:1px;margin:0 10px}@media (width<=900px){.metric-strip[data-v-81958fcc]{grid-template-columns:repeat(2,1fr)}.metric[data-v-81958fcc]:nth-child(2){border-right:0}.metric[data-v-81958fcc]:nth-child(-n+2){border-bottom:1px solid var(--line)}.work-grid[data-v-81958fcc]{grid-template-columns:1fr}}@media (width<=520px){.metric[data-v-81958fcc]{min-height:100px;padding:16px}.metric b[data-v-81958fcc]{font-size:24px}.metric-icon[data-v-81958fcc],.completion[data-v-81958fcc]{display:none}}
|
||||
1
web/dist/assets/DashboardView-CFwHesUj.css
vendored
Normal file
1
web/dist/assets/DashboardView-CFwHesUj.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.metric-strip[data-v-46574c4c]{grid-template-columns:repeat(4,1fr);display:grid;overflow:hidden}.metric[data-v-46574c4c]{border-right:1px solid var(--line);align-items:center;gap:16px;min-height:116px;padding:22px;display:flex}.metric[data-v-46574c4c]:last-child{border-right:0}.metric-icon[data-v-46574c4c],.completion[data-v-46574c4c]{border-radius:5px;place-items:center;width:42px;height:42px;font-size:19px;display:grid}.metric-icon.green[data-v-46574c4c]{color:#16775b;background:#e5f3ee}.metric-icon.coral[data-v-46574c4c]{color:#c4573f;background:#faece7}.metric-icon.amber[data-v-46574c4c]{color:#a86e1e;background:#f8efdf}.completion[data-v-46574c4c]{color:#202825;background:#e8ecea;font-size:12px;font-weight:800}.metric div[data-v-46574c4c]{flex-direction:column;display:flex}.metric b[data-v-46574c4c]{font-family:Noto Serif SC,serif;font-size:28px;line-height:1}.metric small[data-v-46574c4c]{color:var(--muted);margin-top:8px}.work-grid[data-v-46574c4c]{grid-template-columns:1.05fr .95fr;gap:18px;margin-top:18px;display:grid}.action-panel[data-v-46574c4c],.doctrine-panel[data-v-46574c4c]{padding:22px}.panel-kicker[data-v-46574c4c]{color:#7c8883;text-transform:uppercase;margin-bottom:14px;font-size:11px;font-weight:700}.action-panel button[data-v-46574c4c]{width:100%;color:var(--ink);text-align:left;border:0;border-bottom:1px solid var(--line);cursor:pointer;background:0 0;justify-content:space-between;align-items:center;padding:15px 0;display:flex}.action-panel button[data-v-46574c4c]:last-child{border-bottom:0}.action-panel button[data-v-46574c4c]:hover{color:var(--green)}.action-panel button span[data-v-46574c4c]{flex-direction:column;font-weight:650;display:flex}.action-panel button small[data-v-46574c4c]{color:var(--muted);margin-top:4px;font-weight:400}.doctrine-panel[data-v-46574c4c]{color:#fff;background:#202825;border-color:#202825}.doctrine-panel .panel-kicker[data-v-46574c4c]{color:#89a099}blockquote[data-v-46574c4c]{margin:30px 0 42px;font-family:Noto Serif SC,serif;font-size:22px;line-height:1.65}.flow-note[data-v-46574c4c]{color:#9cafaa;align-items:center;font-size:12px;display:flex}.flow-note i[data-v-46574c4c]{background:#4a5a54;flex:1;height:1px;margin:0 10px}@media (width<=900px){.metric-strip[data-v-46574c4c]{grid-template-columns:repeat(2,1fr)}.metric[data-v-46574c4c]:nth-child(2){border-right:0}.metric[data-v-46574c4c]:nth-child(-n+2){border-bottom:1px solid var(--line)}.work-grid[data-v-46574c4c]{grid-template-columns:1fr}}@media (width<=520px){.metric[data-v-46574c4c]{min-height:100px;padding:16px}.metric b[data-v-46574c4c]{font-size:24px}.metric-icon[data-v-46574c4c],.completion[data-v-46574c4c]{display:none}}
|
||||
1
web/dist/assets/DashboardView-DJ4b96AE.js
vendored
1
web/dist/assets/DashboardView-DJ4b96AE.js
vendored
@@ -1 +0,0 @@
|
||||
import{A as e,Bt as t,F as n,I as r,O as i,P as a,Q as o,Tt as s,Y as c,i as l,j as u,lt as d,r as f,t as p,tt as m,vt as h}from"./client-Bk7n1O1W.js";import{t as g}from"./auth-D6Tj4g1X.js";import{t as _}from"./PlusOutlined-CosM7zFe.js";import{n as v,t as y}from"./AppstoreOutlined-BD2QJkU0.js";import{t as b}from"./PlayCircleOutlined-DjeS67m_.js";import{h as x}from"./useApi-CROJJdhE-C8LJOf_8.js";var S={class:`page-shell`},C={class:`page-heading`},w={key:0,class:`page-actions`},T={class:`metric-strip surface`},E={class:`metric`},D={class:`metric-icon green`},O={class:`metric`},k={class:`metric-icon coral`},A={class:`metric`},j={class:`metric-icon amber`},M={class:`metric`},N={class:`completion`},P={class:`work-grid`},F={class:`surface action-panel`},I=f(r({__name:`DashboardView`,setup(r){let f=x(),I=g(),L=h(!0),R=h({scenarios:0,published_sops:0,runs:0,completed_runs:0});return c(async()=>{try{R.value=await p.get(`/dashboard/summary`)}finally{L.value=!1}}),(r,c)=>{let p=m(`a-button`),h=m(`a-skeleton`);return o(),u(`div`,S,[i(`div`,C,[c[7]||=i(`div`,null,[i(`h1`,null,`今天从经验开始`),i(`p`,null,`查看知识沉淀和一线执行的最新状态。`)],-1),s(I).can(`scenario.edit`)?(o(),u(`div`,w,[n(p,{type:`primary`,onClick:c[0]||=e=>s(f).push(`/scenarios`)},{default:d(()=>[n(s(_)),c[6]||=a(`创建场景`,-1)]),_:1})])):e(``,!0)]),n(h,{loading:L.value,active:``},{default:d(()=>[i(`section`,T,[i(`div`,E,[i(`span`,D,[n(s(y))]),i(`div`,null,[i(`b`,null,t(R.value.scenarios),1),c[8]||=i(`small`,null,`可用场景`,-1)])]),i(`div`,O,[i(`span`,k,[n(s(l))]),i(`div`,null,[i(`b`,null,t(R.value.published_sops),1),c[9]||=i(`small`,null,`已发布 SOP`,-1)])]),i(`div`,A,[i(`span`,j,[n(s(b))]),i(`div`,null,[i(`b`,null,t(R.value.runs),1),c[10]||=i(`small`,null,`累计执行`,-1)])]),i(`div`,M,[i(`span`,N,t(R.value.runs?Math.round(R.value.completed_runs/R.value.runs*100):0)+`%`,1),i(`div`,null,[i(`b`,null,t(R.value.completed_runs),1),c[11]||=i(`small`,null,`完成执行`,-1)])])])]),_:1},8,[`loading`]),i(`section`,P,[i(`div`,F,[c[17]||=i(`div`,{class:`panel-kicker`},`常用入口`,-1),s(I).can(`scenario.edit`)?(o(),u(`button`,{key:0,onClick:c[1]||=e=>s(f).push(`/scenarios`)},[c[12]||=i(`span`,null,[a(`配置新的业务场景`),i(`small`,null,`定义字段、目标和触发条件`)],-1),n(s(v))])):e(``,!0),s(I).can(`sop.review`)?(o(),u(`button`,{key:1,onClick:c[2]||=e=>s(f).push(`/reviews`)},[c[13]||=i(`span`,null,[a(`处理待审核 SOP`),i(`small`,null,`核对流程、口径和风险出口`)],-1),n(s(v))])):e(``,!0),s(I).can(`sop.execute`)?(o(),u(`button`,{key:2,onClick:c[3]||=e=>s(f).push(`/execute`)},[c[14]||=i(`span`,null,[a(`开始执行已发布 SOP`),i(`small`,null,`根据客户回答逐步推进`)],-1),n(s(v))])):e(``,!0),s(I).can(`knowledge.edit`)?(o(),u(`button`,{key:3,onClick:c[4]||=e=>s(f).push(`/knowledge`)},[c[15]||=i(`span`,null,[a(`维护审核知识卡`),i(`small`,null,`统一口径与风险提示`)],-1),n(s(v))])):e(``,!0),s(I).can(`runs.view_all`)||s(I).can(`runs.view_own`)?(o(),u(`button`,{key:4,onClick:c[5]||=e=>s(f).push(`/runs`)},[c[16]||=i(`span`,null,[a(`复盘执行记录`),i(`small`,null,`查看节点轨迹、回答与反馈`)],-1),n(s(v))])):e(``,!0)]),c[18]||=i(`div`,{class:`surface doctrine-panel`},[i(`div`,{class:`panel-kicker`},`平台原则`),i(`blockquote`,null,`一句好话术不应只被收藏,它应该知道何时出现、下一步去哪,以及是否真的有效。`),i(`div`,{class:`flow-note`},[i(`span`,null,`创建`),i(`i`),i(`span`,null,`发布`),i(`i`),i(`span`,null,`执行`),i(`i`),i(`span`,null,`复盘`)])],-1)])])}}}),[[`__scopeId`,`data-v-81958fcc`]]);export{I as default};
|
||||
1
web/dist/assets/DashboardView-Vhr2bwL8.js
vendored
Normal file
1
web/dist/assets/DashboardView-Vhr2bwL8.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{A as e,Bt as t,F as n,I as r,O as i,P as a,Q as o,Tt as s,Y as c,i as l,j as u,lt as d,r as f,t as p,tt as m,vt as h}from"./client-Bk7n1O1W.js";import{t as g}from"./auth-D6Tj4g1X.js";import{t as _}from"./PlusOutlined-CosM7zFe.js";import{n as v,t as y}from"./AppstoreOutlined-BD2QJkU0.js";import{t as b}from"./PlayCircleOutlined-DjeS67m_.js";import{h as x}from"./useApi-CROJJdhE-C8LJOf_8.js";var S={class:`page-shell`},C={class:`page-heading`},w={key:0,class:`page-actions`},T={class:`metric-strip surface`},E={class:`metric`},D={class:`metric-icon green`},O={class:`metric`},k={class:`metric-icon coral`},A={class:`metric`},j={class:`metric-icon amber`},M={class:`metric`},N={class:`completion`},P={class:`work-grid`},F={class:`surface action-panel`},I=f(r({__name:`DashboardView`,setup(r){let f=x(),I=g(),L=h(!0),R=h({scenarios:0,published_sops:0,runs:0,completed_runs:0});return c(async()=>{try{R.value=await p.get(`/dashboard/summary`)}finally{L.value=!1}}),(r,c)=>{let p=m(`a-button`),h=m(`a-skeleton`);return o(),u(`div`,S,[i(`div`,C,[c[6]||=i(`div`,null,[i(`h1`,null,`今天从经验开始`),i(`p`,null,`查看知识沉淀和一线执行的最新状态。`)],-1),s(I).can(`scenario.edit`)?(o(),u(`div`,w,[n(p,{type:`primary`,onClick:c[0]||=e=>s(f).push(`/scenarios`)},{default:d(()=>[n(s(_)),c[5]||=a(`创建场景`,-1)]),_:1})])):e(``,!0)]),n(h,{loading:L.value,active:``},{default:d(()=>[i(`section`,T,[i(`div`,E,[i(`span`,D,[n(s(y))]),i(`div`,null,[i(`b`,null,t(R.value.scenarios),1),c[7]||=i(`small`,null,`可用场景`,-1)])]),i(`div`,O,[i(`span`,k,[n(s(l))]),i(`div`,null,[i(`b`,null,t(R.value.published_sops),1),c[8]||=i(`small`,null,`可用 SOP`,-1)])]),i(`div`,A,[i(`span`,j,[n(s(b))]),i(`div`,null,[i(`b`,null,t(R.value.runs),1),c[9]||=i(`small`,null,`累计执行`,-1)])]),i(`div`,M,[i(`span`,N,t(R.value.runs?Math.round(R.value.completed_runs/R.value.runs*100):0)+`%`,1),i(`div`,null,[i(`b`,null,t(R.value.completed_runs),1),c[10]||=i(`small`,null,`完成执行`,-1)])])])]),_:1},8,[`loading`]),i(`section`,P,[i(`div`,F,[c[15]||=i(`div`,{class:`panel-kicker`},`常用入口`,-1),s(I).can(`scenario.edit`)?(o(),u(`button`,{key:0,onClick:c[1]||=e=>s(f).push(`/scenarios`)},[c[11]||=i(`span`,null,[a(`配置新的业务场景`),i(`small`,null,`定义字段、目标和触发条件`)],-1),n(s(v))])):e(``,!0),s(I).can(`sop.execute`)?(o(),u(`button`,{key:1,onClick:c[2]||=e=>s(f).push(`/execute`)},[c[12]||=i(`span`,null,[a(`开始执行 SOP`),i(`small`,null,`根据客户回答逐步推进`)],-1),n(s(v))])):e(``,!0),s(I).can(`knowledge.edit`)?(o(),u(`button`,{key:2,onClick:c[3]||=e=>s(f).push(`/scenarios`)},[c[13]||=i(`span`,null,[a(`配置场景知识卡`),i(`small`,null,`在所属场景内维护输入关联与话术`)],-1),n(s(v))])):e(``,!0),s(I).can(`runs.view_all`)||s(I).can(`runs.view_own`)?(o(),u(`button`,{key:3,onClick:c[4]||=e=>s(f).push(`/runs`)},[c[14]||=i(`span`,null,[a(`复盘执行记录`),i(`small`,null,`查看节点轨迹、回答与反馈`)],-1),n(s(v))])):e(``,!0)]),c[16]||=i(`div`,{class:`surface doctrine-panel`},[i(`div`,{class:`panel-kicker`},`平台原则`),i(`blockquote`,null,`一句好话术不应只被收藏,它应该知道何时出现、下一步去哪,以及是否真的有效。`),i(`div`,{class:`flow-note`},[i(`span`,null,`创建`),i(`i`),i(`span`,null,`配置`),i(`i`),i(`span`,null,`执行`),i(`i`),i(`span`,null,`复盘`)])],-1)])])}}}),[[`__scopeId`,`data-v-46574c4c`]]);export{I as default};
|
||||
1
web/dist/assets/ExecuteView-BrNPNrQz.js
vendored
1
web/dist/assets/ExecuteView-BrNPNrQz.js
vendored
File diff suppressed because one or more lines are too long
1
web/dist/assets/ExecuteView-CAUSQPH9.js
vendored
Normal file
1
web/dist/assets/ExecuteView-CAUSQPH9.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/assets/ExecuteView-CTC6GSK6.css
vendored
1
web/dist/assets/ExecuteView-CTC6GSK6.css
vendored
@@ -1 +0,0 @@
|
||||
.execution-shell[data-v-937f7610]{max-width:1220px}.sop-catalog[data-v-937f7610]{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;display:grid}.sop-entry[data-v-937f7610]{text-align:left;cursor:pointer;grid-template-columns:40px 1fr 42px;align-items:start;gap:14px;min-height:150px;padding:22px;display:grid}.sop-entry[data-v-937f7610]:hover{border-color:#9ac8b8;box-shadow:0 8px 24px #20282512}.entry-seq[data-v-937f7610]{color:#8c9893;font-family:monospace;font-size:12px}.sop-entry small[data-v-937f7610]{color:var(--green);font-weight:650}.sop-entry h2[data-v-937f7610]{margin:8px 0 7px;font-family:Noto Serif SC,serif;font-size:20px}.sop-entry p[data-v-937f7610]{color:var(--muted);margin:0;line-height:1.6}.play[data-v-937f7610]{color:#fff;background:#202825;border-radius:4px;place-items:center;width:38px;height:38px;font-size:18px;display:grid}.run-workspace[data-v-937f7610]{grid-template-columns:270px minmax(0,1fr);gap:18px;display:grid}.run-context[data-v-937f7610]{color:#fff;background:#202825;border-radius:6px;align-self:start;padding:24px;position:sticky;top:86px}.run-label[data-v-937f7610]{color:#72c9ab;font-size:11px;font-weight:700}.run-context h2[data-v-937f7610]{margin:12px 0 8px;font-family:Noto Serif SC,serif}.run-context>p[data-v-937f7610]{color:#aebdb7;margin:0 0 28px;line-height:1.6}.context-meta[data-v-937f7610]{border-top:1px solid #3a4742;justify-content:space-between;align-items:center;padding:13px 0;display:flex}.context-meta span[data-v-937f7610]{color:#9eada7;font-size:12px}.privacy-note[data-v-937f7610]{color:#889b93;gap:8px;margin-top:28px;font-size:11px;display:flex}.conversation[data-v-937f7610]{min-height:590px;padding:30px 34px}.conversation-progress[data-v-937f7610]{color:#66736d;align-items:center;gap:8px;font-size:12px;display:flex}.conversation-progress span[data-v-937f7610]{background:#d08736;border-radius:50%;width:8px;height:8px;box-shadow:0 0 0 4px #faefe1}.conversation-progress span.done[data-v-937f7610]{background:var(--green);box-shadow:0 0 0 4px #e4f1ec}.node-content[data-v-937f7610]{padding:44px 0 26px}.node-content small[data-v-937f7610]{color:var(--green);font-size:10px;font-weight:800}.node-content h1[data-v-937f7610]{margin:8px 0 22px;font-family:Noto Serif SC,serif;font-size:28px}.node-content blockquote[data-v-937f7610]{color:#29342f;border-left:3px solid var(--green);background:#f2f6f4;margin:0;padding:18px 20px;font-size:17px;line-height:1.8}.answer-form[data-v-937f7610]{max-width:680px}.choice-group[data-v-937f7610]{flex-wrap:wrap;display:flex}.run-actions[data-v-937f7610]{border-top:1px solid var(--line);color:var(--muted);justify-content:space-between;align-items:center;gap:16px;margin:24px -34px -30px;padding:18px 34px;font-size:12px;display:flex}.run-actions>div[data-v-937f7610]{align-items:center;gap:9px;display:flex}.completed-state[data-v-937f7610]{text-align:center;padding:40px 0}.completed-state>span[data-v-937f7610]{color:var(--green);font-size:50px}.completed-state h3[data-v-937f7610]{margin:14px 0 8px;font-family:Noto Serif SC,serif;font-size:24px}.completed-state p[data-v-937f7610]{color:var(--muted);margin:0 0 24px}.quick-feedback[data-v-937f7610]{text-align:left;border-top:1px solid var(--line);border-bottom:1px solid var(--line);background:#f5f8f6;grid-template-columns:180px 1fr auto;align-items:center;gap:12px;max-width:620px;margin:0 auto 18px;padding:16px;display:grid}.quick-feedback>span[data-v-937f7610]{font-size:12px;font-weight:650}.quick-feedback[data-v-937f7610] .ant-rate{font-size:18px}.quick-feedback[data-v-937f7610] .ant-input{grid-column:1/3}.feedback-thanks[data-v-937f7610]{max-width:520px;color:var(--green);background:#edf6f2;margin:0 auto 18px;padding:12px;font-size:12px}.knowledge-snapshot[data-v-937f7610]{border-top:1px solid var(--line);border-bottom:1px solid var(--line);margin:0 0 24px}.knowledge-head[data-v-937f7610]{justify-content:space-between;align-items:center;padding:12px 0;display:flex}.knowledge-head span[data-v-937f7610]{color:var(--muted);font-size:11px}.standard-copy[data-v-937f7610],.knowledge-note[data-v-937f7610]{border-left:3px solid var(--green);background:#f2f7f5;padding:14px 16px}.standard-copy small[data-v-937f7610],.knowledge-note small[data-v-937f7610]{font-size:10px;font-weight:750}.standard-copy p[data-v-937f7610],.knowledge-note p[data-v-937f7610]{margin:6px 0 0;line-height:1.7}.knowledge-note[data-v-937f7610]{background:#fbf3f0;border-left-color:#c4573f;margin-top:8px}.knowledge-note.risk[data-v-937f7610]{background:#fbf6eb;border-left-color:#bd7b24}@media (width<=800px){.sop-catalog[data-v-937f7610],.run-workspace[data-v-937f7610]{grid-template-columns:1fr}.run-context[data-v-937f7610]{position:static}.conversation[data-v-937f7610]{padding:22px}.run-actions[data-v-937f7610]{margin:24px -22px -22px;padding:16px 22px}.run-actions span[data-v-937f7610]{display:none}.quick-feedback[data-v-937f7610]{grid-template-columns:1fr}.quick-feedback[data-v-937f7610] .ant-input{grid-column:auto}}
|
||||
1
web/dist/assets/ExecuteView-D6wWGKCU.css
vendored
Normal file
1
web/dist/assets/ExecuteView-D6wWGKCU.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/assets/EyeOutlined-C1VmLQCG.js
vendored
1
web/dist/assets/EyeOutlined-C1VmLQCG.js
vendored
@@ -1 +0,0 @@
|
||||
import{F as e,a as t}from"./client-Bk7n1O1W.js";var n={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z`}}]},name:`check`,theme:`outlined`};function r(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]==null?{}:Object(arguments[t]),r=Object.keys(n);typeof Object.getOwnPropertySymbols==`function`&&(r=r.concat(Object.getOwnPropertySymbols(n).filter(function(e){return Object.getOwnPropertyDescriptor(n,e).enumerable}))),r.forEach(function(t){i(e,t,n[t])})}return e}function i(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var a=function(i,a){var o=r({},i,a.attrs);return e(t,r({},o,{icon:n}),null)};a.displayName=`CheckOutlined`,a.inheritAttrs=!1;var o={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z`}},{tag:`path`,attrs:{d:`M686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8H488c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.8-11.2z`}}]},name:`clock-circle`,theme:`outlined`};function s(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]==null?{}:Object(arguments[t]),r=Object.keys(n);typeof Object.getOwnPropertySymbols==`function`&&(r=r.concat(Object.getOwnPropertySymbols(n).filter(function(e){return Object.getOwnPropertyDescriptor(n,e).enumerable}))),r.forEach(function(t){c(e,t,n[t])})}return e}function c(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var l=function(n,r){var i=s({},n,r.attrs);return e(t,s({},i,{icon:o}),null)};l.displayName=`ClockCircleOutlined`,l.inheritAttrs=!1;var u={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z`}}]},name:`eye`,theme:`outlined`};function d(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]==null?{}:Object(arguments[t]),r=Object.keys(n);typeof Object.getOwnPropertySymbols==`function`&&(r=r.concat(Object.getOwnPropertySymbols(n).filter(function(e){return Object.getOwnPropertyDescriptor(n,e).enumerable}))),r.forEach(function(t){f(e,t,n[t])})}return e}function f(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var p=function(n,r){var i=d({},n,r.attrs);return e(t,d({},i,{icon:u}),null)};p.displayName=`EyeOutlined`,p.inheritAttrs=!1;export{l as n,a as r,p as t};
|
||||
1
web/dist/assets/HistoryOutlined-ABEqcIfz.js
vendored
1
web/dist/assets/HistoryOutlined-ABEqcIfz.js
vendored
@@ -1 +0,0 @@
|
||||
import{F as e,a as t}from"./client-Bk7n1O1W.js";var n={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M536.1 273H488c-4.4 0-8 3.6-8 8v275.3c0 2.6 1.2 5 3.3 6.5l165.3 120.7c3.6 2.6 8.6 1.9 11.2-1.7l28.6-39c2.7-3.7 1.9-8.7-1.7-11.2L544.1 528.5V281c0-4.4-3.6-8-8-8zm219.8 75.2l156.8 38.3c5 1.2 9.9-2.6 9.9-7.7l.8-161.5c0-6.7-7.7-10.5-12.9-6.3L752.9 334.1a8 8 0 003 14.1zm167.7 301.1l-56.7-19.5a8 8 0 00-10.1 4.8c-1.9 5.1-3.9 10.1-6 15.1-17.8 42.1-43.3 80-75.9 112.5a353 353 0 01-112.5 75.9 352.18 352.18 0 01-137.7 27.8c-47.8 0-94.1-9.3-137.7-27.8a353 353 0 01-112.5-75.9c-32.5-32.5-58-70.4-75.9-112.5A353.44 353.44 0 01171 512c0-47.8 9.3-94.2 27.8-137.8 17.8-42.1 43.3-80 75.9-112.5a353 353 0 01112.5-75.9C430.6 167.3 477 158 524.8 158s94.1 9.3 137.7 27.8A353 353 0 01775 261.7c10.2 10.3 19.8 21 28.6 32.3l59.8-46.8C784.7 146.6 662.2 81.9 524.6 82 285 82.1 92.6 276.7 95 516.4 97.4 751.9 288.9 942 524.8 942c185.5 0 343.5-117.6 403.7-282.3 1.5-4.2-.7-8.9-4.9-10.4z`}}]},name:`history`,theme:`outlined`};function r(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]==null?{}:Object(arguments[t]),r=Object.keys(n);typeof Object.getOwnPropertySymbols==`function`&&(r=r.concat(Object.getOwnPropertySymbols(n).filter(function(e){return Object.getOwnPropertyDescriptor(n,e).enumerable}))),r.forEach(function(t){i(e,t,n[t])})}return e}function i(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var a=function(i,a){var o=r({},i,a.attrs);return e(t,r({},o,{icon:n}),null)};a.displayName=`HistoryOutlined`,a.inheritAttrs=!1;export{a as t};
|
||||
1
web/dist/assets/KnowledgeView-BZbzCYjc.js
vendored
1
web/dist/assets/KnowledgeView-BZbzCYjc.js
vendored
File diff suppressed because one or more lines are too long
1
web/dist/assets/KnowledgeView-DgJgbg9L.css
vendored
1
web/dist/assets/KnowledgeView-DgJgbg9L.css
vendored
@@ -1 +0,0 @@
|
||||
.knowledge-grid[data-v-eaece4e4]{grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;display:grid}.knowledge-card[data-v-eaece4e4]{flex-direction:column;min-height:300px;padding:20px;display:flex}.knowledge-card header[data-v-eaece4e4]{justify-content:space-between;display:flex}.card-icon[data-v-eaece4e4]{width:36px;height:36px;color:var(--green);background:#e8f3ef;border-radius:4px;place-items:center;display:grid}.knowledge-card h2[data-v-eaece4e4]{margin:20px 0 10px;font-family:Noto Serif SC,serif;font-size:19px}.standard-copy[data-v-eaece4e4]{color:#4f5c56;margin:0;line-height:1.7}.copy-note[data-v-eaece4e4]{border-left:2px solid;grid-template-columns:58px 1fr;gap:8px;margin-top:12px;padding:9px 11px;font-size:12px;display:grid}.copy-note b[data-v-eaece4e4]{font-size:11px}.copy-note.forbidden[data-v-eaece4e4]{color:#7a4d4d;background:#faf0f0;border-color:#ba7474}.copy-note.risk[data-v-eaece4e4]{color:#7f591c;background:#fbf3e5;border-color:#c68a37}.knowledge-card footer[data-v-eaece4e4]{color:var(--muted);justify-content:space-between;align-items:center;margin-top:auto;padding-top:18px;font-size:12px;display:flex}.card-actions[data-v-eaece4e4]{display:flex}.version-row[data-v-eaece4e4]{border-bottom:1px solid var(--line);padding:18px 0}.version-row[data-v-eaece4e4]:first-child{padding-top:0}.version-head[data-v-eaece4e4]{align-items:center;gap:8px;display:flex}.version-head time[data-v-eaece4e4]{color:var(--muted);margin-left:auto;font-size:11px}.version-row>p[data-v-eaece4e4]{color:#45524c;margin:12px 0;line-height:1.7}.history-note[data-v-eaece4e4]{color:#6f7b76;margin-top:7px;font-size:12px}.history-note b[data-v-eaece4e4]{color:#39463f;margin-right:8px}@media (width<=1050px){.knowledge-grid[data-v-eaece4e4]{grid-template-columns:repeat(2,1fr)}}@media (width<=650px){.knowledge-grid[data-v-eaece4e4]{grid-template-columns:1fr}.version-head[data-v-eaece4e4]{flex-wrap:wrap}.version-head time[data-v-eaece4e4]{width:100%;margin-left:0}}
|
||||
1
web/dist/assets/LoginView-BYK68e31.css
vendored
1
web/dist/assets/LoginView-BYK68e31.css
vendored
@@ -1 +0,0 @@
|
||||
.login-page[data-v-b1da8a6b]{background:#f8faf9;grid-template-columns:minmax(420px,1.05fr) minmax(420px,.95fr);min-height:100vh;display:grid}.identity-panel[data-v-b1da8a6b]{color:#fff;background:#202825;flex-direction:column;min-height:640px;padding:42px 54px;display:flex;position:relative;overflow:hidden}.identity-grid[data-v-b1da8a6b]{opacity:.12;background-image:linear-gradient(#b7c4be 1px,#0000 1px),linear-gradient(90deg,#b7c4be 1px,#0000 1px);background-size:44px 44px;position:absolute;inset:0}.brand-lockup[data-v-b1da8a6b]{align-items:center;gap:12px;font-family:Noto Serif SC,serif;font-size:18px;font-weight:700;display:flex;position:relative}.brand-symbol[data-v-b1da8a6b]{border:1px solid #ffffff59;border-radius:5px;grid-template-columns:repeat(3,1fr);align-items:end;gap:3px;width:34px;height:34px;padding:6px;display:grid}.brand-symbol i[data-v-b1da8a6b]{background:#6cc7a8;height:8px;display:block}.brand-symbol i[data-v-b1da8a6b]:nth-child(2){height:15px}.brand-symbol i[data-v-b1da8a6b]:nth-child(3){background:#ed8e6a;height:22px}.identity-copy[data-v-b1da8a6b]{max-width:600px;margin:auto 0;position:relative}.eyebrow[data-v-b1da8a6b]{font-size:12px;font-weight:700;color:#86d3b8!important}.identity-copy h1[data-v-b1da8a6b]{letter-spacing:0;margin:20px 0 24px;font-family:Noto Serif SC,Songti SC,serif;font-size:clamp(38px,4.2vw,64px);line-height:1.2}.identity-copy p[data-v-b1da8a6b]{color:#bdc8c3;max-width:510px;font-size:16px;line-height:1.8}.signal-line[data-v-b1da8a6b]{color:#91a29b;align-items:center;gap:12px;font-size:12px;display:flex;position:relative}.signal-line b[data-v-b1da8a6b]{background:#52615b;width:70px;height:1px}.login-panel[data-v-b1da8a6b]{place-items:center;padding:40px;display:grid}.login-form-wrap[data-v-b1da8a6b]{width:min(100%,390px)}.login-form-wrap h2[data-v-b1da8a6b]{color:#202825;letter-spacing:0;margin:0;font-family:Noto Serif SC,serif;font-size:28px}.login-form-wrap>p[data-v-b1da8a6b]{color:#728079;margin:8px 0 30px}.login-form-wrap[data-v-b1da8a6b] .ant-form-item-label label{color:#46514c;font-weight:600}.login-form-wrap[data-v-b1da8a6b] .ant-input-affix-wrapper{padding:10px 12px}.login-form-wrap[data-v-b1da8a6b] .ant-btn-lg{height:44px;margin-top:8px}.login-note[data-v-b1da8a6b]{color:#8a9691;align-items:center;gap:8px;margin-top:20px;font-size:12px;display:flex}.login-note span[data-v-b1da8a6b]{background:#d5644a;border-radius:50%;width:7px;height:7px}.mobile-brand[data-v-b1da8a6b]{display:none}@media (width<=860px){.login-page[data-v-b1da8a6b]{grid-template-columns:1fr}.identity-panel[data-v-b1da8a6b]{display:none}.login-panel[data-v-b1da8a6b]{min-height:100vh;padding:28px}.mobile-brand[data-v-b1da8a6b]{color:#16775b;margin-bottom:48px;font-family:Noto Serif SC,serif;font-weight:700;display:block}}
|
||||
File diff suppressed because one or more lines are too long
1
web/dist/assets/LoginView-Fff71f7Q.css
vendored
Normal file
1
web/dist/assets/LoginView-Fff71f7Q.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.login-page[data-v-29188541]{background:#f8faf9;grid-template-columns:minmax(420px,1.05fr) minmax(420px,.95fr);min-height:100vh;display:grid}.identity-panel[data-v-29188541]{color:#fff;background:#202825;flex-direction:column;min-height:640px;padding:42px 54px;display:flex;position:relative;overflow:hidden}.identity-grid[data-v-29188541]{opacity:.12;background-image:linear-gradient(#b7c4be 1px,#0000 1px),linear-gradient(90deg,#b7c4be 1px,#0000 1px);background-size:44px 44px;position:absolute;inset:0}.brand-lockup[data-v-29188541]{align-items:center;gap:12px;font-family:Noto Serif SC,serif;font-size:18px;font-weight:700;display:flex;position:relative}.brand-symbol[data-v-29188541]{border:1px solid #ffffff59;border-radius:5px;grid-template-columns:repeat(3,1fr);align-items:end;gap:3px;width:34px;height:34px;padding:6px;display:grid}.brand-symbol i[data-v-29188541]{background:#6cc7a8;height:8px;display:block}.brand-symbol i[data-v-29188541]:nth-child(2){height:15px}.brand-symbol i[data-v-29188541]:nth-child(3){background:#ed8e6a;height:22px}.identity-copy[data-v-29188541]{max-width:600px;margin:auto 0;position:relative}.eyebrow[data-v-29188541]{font-size:12px;font-weight:700;color:#86d3b8!important}.identity-copy h1[data-v-29188541]{letter-spacing:0;margin:20px 0 24px;font-family:Noto Serif SC,Songti SC,serif;font-size:clamp(38px,4.2vw,64px);line-height:1.2}.identity-copy p[data-v-29188541]{color:#bdc8c3;max-width:510px;font-size:16px;line-height:1.8}.signal-line[data-v-29188541]{color:#91a29b;align-items:center;gap:12px;font-size:12px;display:flex;position:relative}.signal-line b[data-v-29188541]{background:#52615b;width:70px;height:1px}.login-panel[data-v-29188541]{place-items:center;padding:40px;display:grid}.login-form-wrap[data-v-29188541]{width:min(100%,390px)}.login-form-wrap h2[data-v-29188541]{color:#202825;letter-spacing:0;margin:0;font-family:Noto Serif SC,serif;font-size:28px}.login-form-wrap>p[data-v-29188541]{color:#728079;margin:8px 0 30px}.login-form-wrap[data-v-29188541] .ant-form-item-label label{color:#46514c;font-weight:600}.login-form-wrap[data-v-29188541] .ant-input-affix-wrapper{padding:10px 12px}.login-form-wrap[data-v-29188541] .ant-btn-lg{height:44px;margin-top:8px}.login-note[data-v-29188541]{color:#8a9691;align-items:center;gap:8px;margin-top:20px;font-size:12px;display:flex}.login-note span[data-v-29188541]{background:#d5644a;border-radius:50%;width:7px;height:7px}.mobile-brand[data-v-29188541]{display:none}@media (width<=860px){.login-page[data-v-29188541]{grid-template-columns:1fr}.identity-panel[data-v-29188541]{display:none}.login-panel[data-v-29188541]{min-height:100vh;padding:28px}.mobile-brand[data-v-29188541]{color:#16775b;margin-bottom:48px;font-family:Noto Serif SC,serif;font-weight:700;display:block}}
|
||||
1
web/dist/assets/MembersView-6hfD_lrO.js
vendored
1
web/dist/assets/MembersView-6hfD_lrO.js
vendored
File diff suppressed because one or more lines are too long
1
web/dist/assets/MembersView-BSaD6Qkq.css
vendored
Normal file
1
web/dist/assets/MembersView-BSaD6Qkq.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.members-page[data-v-bcafb28a]{max-width:1180px}.member-summary[data-v-bcafb28a]{color:#fff;background:#202825;border-radius:6px;grid-template-columns:38px 90px 1fr;align-items:center;gap:14px;min-height:86px;margin-bottom:16px;padding:16px 20px;display:grid}.member-summary>span[data-v-bcafb28a]{color:#6bc5a7;font-size:25px}.member-summary>div[data-v-bcafb28a]{flex-direction:column;display:flex}.member-summary b[data-v-bcafb28a]{font-family:Noto Serif SC,serif;font-size:24px}.member-summary div span[data-v-bcafb28a]{color:#a9b8b2;font-size:11px}.member-summary p[data-v-bcafb28a]{color:#b6c3be;margin:0;line-height:1.7}.member-cell[data-v-bcafb28a]{align-items:center;gap:11px;display:flex}.member-cell>span[data-v-bcafb28a]{color:#17664f;background:#e5f2ed;border-radius:4px;place-items:center;width:34px;height:34px;font-weight:700;display:grid}.member-cell>div[data-v-bcafb28a]{flex-direction:column;display:flex}.member-cell small[data-v-bcafb28a]{color:var(--muted);margin-top:3px}.form-grid[data-v-bcafb28a]{grid-template-columns:1fr 1fr;gap:12px;display:grid}.editing-name[data-v-bcafb28a]{border-left:3px solid var(--green);background:#f4f7f5;flex-direction:column;margin-bottom:18px;padding:12px 14px;display:flex}.editing-name span[data-v-bcafb28a]{color:var(--muted);margin-top:4px;font-size:12px}@media (width<=650px){.member-summary[data-v-bcafb28a]{grid-template-columns:34px 1fr}.member-summary p[data-v-bcafb28a]{grid-column:1/3}.form-grid[data-v-bcafb28a]{grid-template-columns:1fr;gap:0}}
|
||||
1
web/dist/assets/MembersView-CHAbDE44.css
vendored
1
web/dist/assets/MembersView-CHAbDE44.css
vendored
@@ -1 +0,0 @@
|
||||
.members-page[data-v-00e79f6a]{max-width:1180px}.member-summary[data-v-00e79f6a]{color:#fff;background:#202825;border-radius:6px;grid-template-columns:38px 90px 1fr;align-items:center;gap:14px;min-height:86px;margin-bottom:16px;padding:16px 20px;display:grid}.member-summary>span[data-v-00e79f6a]{color:#6bc5a7;font-size:25px}.member-summary>div[data-v-00e79f6a]{flex-direction:column;display:flex}.member-summary b[data-v-00e79f6a]{font-family:Noto Serif SC,serif;font-size:24px}.member-summary div span[data-v-00e79f6a]{color:#a9b8b2;font-size:11px}.member-summary p[data-v-00e79f6a]{color:#b6c3be;margin:0;line-height:1.7}.member-cell[data-v-00e79f6a]{align-items:center;gap:11px;display:flex}.member-cell>span[data-v-00e79f6a]{color:#17664f;background:#e5f2ed;border-radius:4px;place-items:center;width:34px;height:34px;font-weight:700;display:grid}.member-cell>div[data-v-00e79f6a]{flex-direction:column;display:flex}.member-cell small[data-v-00e79f6a]{color:var(--muted);margin-top:3px}.form-grid[data-v-00e79f6a]{grid-template-columns:1fr 1fr;gap:12px;display:grid}.editing-name[data-v-00e79f6a]{border-left:3px solid var(--green);background:#f4f7f5;flex-direction:column;margin-bottom:18px;padding:12px 14px;display:flex}.editing-name span[data-v-00e79f6a]{color:var(--muted);margin-top:4px;font-size:12px}@media (width<=650px){.member-summary[data-v-00e79f6a]{grid-template-columns:34px 1fr}.member-summary p[data-v-00e79f6a]{grid-column:1/3}.form-grid[data-v-00e79f6a]{grid-template-columns:1fr;gap:0}}
|
||||
1
web/dist/assets/MembersView-CJIp3zFH.js
vendored
Normal file
1
web/dist/assets/MembersView-CJIp3zFH.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/assets/ReviewCenterView-DanGoVob.js
vendored
1
web/dist/assets/ReviewCenterView-DanGoVob.js
vendored
@@ -1 +0,0 @@
|
||||
import{A as e,Bt as t,F as n,I as r,O as i,P as a,Q as o,Tt as s,Y as c,_t as l,j as u,k as d,lt as f,n as p,r as m,t as h,tt as g,vt as _}from"./client-Bk7n1O1W.js";import{a as v}from"./config-provider-CxhqvDI_.js";import{n as y,r as b,t as x}from"./EyeOutlined-C1VmLQCG.js";import{t as S}from"./StopOutlined-X-yne_Zl.js";import{h as C}from"./useApi-CROJJdhE-C8LJOf_8.js";var w={class:`page-shell review-page`},T={class:`page-heading`},E={class:`queue-count`},D={class:`surface review-table`},O={key:0,class:`sop-cell`},k={key:2,class:`waiting`},A={key:3,class:`row-actions`},j={class:`review-empty`},M={class:`modal-copy`},N=m(r({__name:`ReviewCenterView`,setup(r){let m=C(),N=_(!1),P=_([]),F=_(null),I=_(!1),L=l({reason:``}),R=[{title:`SOP / 场景`,key:`sop`},{title:`版本`,dataIndex:`version`,width:90},{title:`提交人`,dataIndex:`creator_name`,width:140},{title:`等待时间`,dataIndex:`submitted_at`,width:180},{title:``,key:`action`,width:250}];async function z(){N.value=!0;try{let e=await h.get(`/reviews`);P.value=e.items||[]}catch(e){v.error(p(e))}finally{N.value=!1}}async function B(e){I.value=!0;try{await h.post(`/sops/${e.sop_id}/publish`),v.success(`“${e.sop_name}”已通过并发布`),await z()}catch(e){v.error(p(e))}finally{I.value=!1}}function V(e){F.value=e,L.reason=``}async function H(){if(!F.value||!L.reason.trim())return v.warning(`请填写退回原因`);I.value=!0;try{await h.post(`/sops/${F.value.sop_id}/reject`,{reason:L.reason}),v.success(`已退回编辑者修改`),F.value=null,await z()}catch(e){v.error(p(e))}finally{I.value=!1}}function U(e){let t=Math.max(1,Math.round((Date.now()-new Date(e).getTime())/6e4));if(t<60)return`${t} 分钟`;let n=Math.round(t/60);return n<48?`${n} 小时`:`${Math.round(n/24)} 天`}return c(z),(r,c)=>{let l=g(`a-tag`),p=g(`a-button`),h=g(`a-table`),_=g(`a-textarea`),v=g(`a-modal`);return o(),u(`div`,w,[i(`div`,T,[c[3]||=i(`div`,null,[i(`h1`,null,`审核中心`),i(`p`,null,`检查流程完整性、风险分支和话术口径后再决定发布。`)],-1),i(`div`,E,[n(s(y)),i(`b`,null,t(P.value.length),1),c[2]||=i(`span`,null,`待审核`,-1)])]),i(`section`,D,[c[9]||=i(`div`,{class:`review-guideline`},[i(`span`,null,`审核关注`),i(`b`,null,`字段完整`),i(`i`),i(`b`,null,`条件可达`),i(`i`),i(`b`,null,`高风险有出口`),i(`i`),i(`b`,null,`知识卡已发布`)],-1),n(h,{"row-key":`version_id`,loading:N.value,"data-source":P.value,columns:R,pagination:!1,scroll:{x:850}},{bodyCell:f(({column:r,record:h})=>[r.key===`sop`?(o(),u(`div`,O,[i(`b`,null,t(h.sop_name),1),i(`span`,null,t(h.scenario_name)+` · `+t(h.description||`暂无说明`),1)])):r.dataIndex===`version`?(o(),d(l,{key:1},{default:f(()=>[a(`V`+t(h.version),1)]),_:2},1024)):r.dataIndex===`submitted_at`?(o(),u(`span`,k,t(U(h.submitted_at)),1)):r.key===`action`?(o(),u(`div`,A,[n(p,{onClick:e=>s(m).push(`/sops/${h.sop_id}`)},{default:f(()=>[n(s(x)),c[4]||=a(`查看`,-1)]),_:1},8,[`onClick`]),n(p,{danger:``,onClick:e=>V(h)},{default:f(()=>[n(s(S)),c[5]||=a(`退回`,-1)]),_:1},8,[`onClick`]),n(p,{type:`primary`,loading:I.value,onClick:e=>B(h)},{default:f(()=>[n(s(b)),c[6]||=a(`通过`,-1)]),_:1},8,[`loading`,`onClick`])])):e(``,!0)]),emptyText:f(()=>[i(`div`,j,[n(s(b)),c[7]||=i(`b`,null,`审核队列已清空`,-1),c[8]||=i(`span`,null,`新提交的 SOP 会出现在这里。`,-1)])]),_:1},8,[`loading`,`data-source`])]),n(v,{open:!!F.value,title:`退回修改`,"ok-text":`确认退回`,"confirm-loading":I.value,onOk:H,onCancel:c[1]||=e=>F.value=null},{default:f(()=>[i(`p`,M,`请说明“`+t(F.value?.sop_name)+`”需要调整的具体内容,编辑者可据此修改后重新提交。`,1),n(_,{value:L.reason,"onUpdate:value":c[0]||=e=>L.reason=e,rows:5,maxlength:1e3,"show-count":``,placeholder:`例如:急症判断缺少默认路径,请补充非急症分支。`},null,8,[`value`])]),_:1},8,[`open`,`confirm-loading`])])}}}),[[`__scopeId`,`data-v-88d0e1a8`]]);export{N as default};
|
||||
@@ -1 +0,0 @@
|
||||
.queue-count[data-v-88d0e1a8]{color:#3f4c46;border:1px solid var(--line);background:#fff;border-radius:6px;grid-template-columns:24px 32px auto;align-items:center;gap:8px;height:54px;padding:0 16px;display:grid}.queue-count>span[data-v-88d0e1a8]:first-child{color:var(--amber)}.queue-count b[data-v-88d0e1a8]{font-family:Noto Serif SC,serif;font-size:24px}.queue-count span[data-v-88d0e1a8]:last-child{color:var(--muted);font-size:12px}.review-table[data-v-88d0e1a8]{overflow:hidden}.review-guideline[data-v-88d0e1a8]{color:#637069;border-bottom:1px solid var(--line);background:#f7f9f8;align-items:center;gap:12px;min-height:48px;padding:0 16px;font-size:11px;display:flex}.review-guideline>span[data-v-88d0e1a8]{color:#28332e;font-weight:750}.review-guideline i[data-v-88d0e1a8]{background:#a7b2ad;border-radius:50%;width:3px;height:3px}.sop-cell[data-v-88d0e1a8]{flex-direction:column;max-width:520px;display:flex}.sop-cell b[data-v-88d0e1a8]{font-size:14px}.sop-cell span[data-v-88d0e1a8]{color:var(--muted);white-space:nowrap;text-overflow:ellipsis;margin-top:5px;font-size:12px;overflow:hidden}.waiting[data-v-88d0e1a8]{color:#8a641e}.row-actions[data-v-88d0e1a8]{justify-content:flex-end;gap:7px;display:flex}.review-empty[data-v-88d0e1a8]{color:#9aa59f;flex-direction:column;justify-content:center;align-items:center;min-height:220px;display:flex}.review-empty>span[data-v-88d0e1a8]:first-child{color:#4eaa88;font-size:34px}.review-empty b[data-v-88d0e1a8]{color:#415049;margin-top:12px}.review-empty span[data-v-88d0e1a8]:last-child{margin-top:5px;font-size:12px}.modal-copy[data-v-88d0e1a8]{color:var(--muted);margin:0 0 14px;line-height:1.7}@media (width<=650px){.review-guideline[data-v-88d0e1a8],.queue-count[data-v-88d0e1a8]{display:none}}
|
||||
1
web/dist/assets/RunDetailView-BFxue3EE.js
vendored
Normal file
1
web/dist/assets/RunDetailView-BFxue3EE.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/assets/RunDetailView-D7a859PI.css
vendored
Normal file
1
web/dist/assets/RunDetailView-D7a859PI.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/assets/RunDetailView-W8qPXltZ.js
vendored
1
web/dist/assets/RunDetailView-W8qPXltZ.js
vendored
File diff suppressed because one or more lines are too long
1
web/dist/assets/RunDetailView-z649jgwe.css
vendored
1
web/dist/assets/RunDetailView-z649jgwe.css
vendored
File diff suppressed because one or more lines are too long
1
web/dist/assets/RunHistoryView-BHDOSNhY.css
vendored
1
web/dist/assets/RunHistoryView-BHDOSNhY.css
vendored
@@ -1 +0,0 @@
|
||||
.history-toolbar[data-v-d3f4eec1]{border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:12px;min-height:58px;padding:10px 14px;display:flex}.filter-set[data-v-d3f4eec1]{color:#7a8781;flex-wrap:wrap;align-items:center;gap:9px;display:flex}.filter-set[data-v-d3f4eec1] .ant-select{width:150px}.filter-set[data-v-d3f4eec1] .ant-picker{width:240px}.run-sop[data-v-d3f4eec1]{flex-direction:column;display:flex}.run-sop small[data-v-d3f4eec1]{color:var(--muted);margin-top:4px}.surface[data-v-d3f4eec1] .ant-table-row{cursor:pointer}.surface[data-v-d3f4eec1] .ant-table-row:hover .row-arrow{color:var(--green);transform:translate(2px)}code[data-v-d3f4eec1]{color:#17654f;background:#eef5f2;border-radius:3px;padding:2px 5px}.row-arrow[data-v-d3f4eec1]{color:#9ba6a1;transition:all .16s}@media (width<=700px){.history-toolbar[data-v-d3f4eec1]{align-items:flex-start}.filter-set[data-v-d3f4eec1]{width:100%}.filter-set[data-v-d3f4eec1] .ant-select{width:calc(50% - 18px)}.filter-set[data-v-d3f4eec1] .ant-picker{width:100%}}
|
||||
1
web/dist/assets/RunHistoryView-De1_-1wD.css
vendored
Normal file
1
web/dist/assets/RunHistoryView-De1_-1wD.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.history-toolbar[data-v-d1bbd21f]{border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:12px;min-height:58px;padding:10px 14px;display:flex}.filter-set[data-v-d1bbd21f]{color:#7a8781;flex-wrap:wrap;align-items:center;gap:9px;display:flex}.filter-set[data-v-d1bbd21f] .ant-select{width:150px}.filter-set[data-v-d1bbd21f] .ant-picker{width:240px}.run-sop[data-v-d1bbd21f]{flex-direction:column;display:flex}.run-sop small[data-v-d1bbd21f]{color:var(--muted);margin-top:4px}.surface[data-v-d1bbd21f] .ant-table-row{cursor:pointer}.surface[data-v-d1bbd21f] .ant-table-row:hover .row-arrow{color:var(--green);transform:translate(2px)}code[data-v-d1bbd21f]{color:#17654f;background:#eef5f2;border-radius:3px;padding:2px 5px}.row-arrow[data-v-d1bbd21f]{color:#9ba6a1;transition:all .16s}@media (width<=700px){.history-toolbar[data-v-d1bbd21f]{align-items:flex-start}.filter-set[data-v-d1bbd21f]{width:100%}.filter-set[data-v-d1bbd21f] .ant-select{width:calc(50% - 18px)}.filter-set[data-v-d1bbd21f] .ant-picker{width:100%}}
|
||||
File diff suppressed because one or more lines are too long
3
web/dist/assets/SOPEditorView-CNOWYqSp.js
vendored
Normal file
3
web/dist/assets/SOPEditorView-CNOWYqSp.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/assets/SOPEditorView-CnsIl3I7.css
vendored
1
web/dist/assets/SOPEditorView-CnsIl3I7.css
vendored
File diff suppressed because one or more lines are too long
1
web/dist/assets/SOPEditorView-Cp_YrNIF.css
vendored
Normal file
1
web/dist/assets/SOPEditorView-Cp_YrNIF.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.editor-page[data-v-fb77a209]{background:#eef1ef;min-height:calc(100vh - 62px)}.editor-header[data-v-fb77a209]{border-bottom:1px solid var(--line);z-index:10;background:#fff;justify-content:space-between;align-items:center;gap:16px;min-height:66px;padding:10px 24px;display:flex;position:sticky;top:62px}.editor-title[data-v-fb77a209]{align-items:center;gap:8px;display:flex}.editor-title>div[data-v-fb77a209]{flex-direction:column;display:flex}.editor-title b[data-v-fb77a209]{font-size:15px}.editor-title span[data-v-fb77a209]{color:var(--muted);margin-top:3px;font-size:11px}.page-actions[data-v-fb77a209]{flex-wrap:wrap;justify-content:flex-end;gap:8px;display:flex}.editor-workbench[data-v-fb77a209]{grid-template-columns:280px minmax(0,1fr);gap:16px;max-width:1500px;margin:0 auto;padding:18px;display:grid}.node-rail[data-v-fb77a209]{align-self:start;position:sticky;top:146px;overflow:hidden}.rail-title[data-v-fb77a209]{border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;height:50px;padding:0 14px;font-weight:650;display:flex}.node-list[data-v-fb77a209]{padding:8px}.node-list-item[data-v-fb77a209]{text-align:left;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:4px;grid-template-columns:34px minmax(0,1fr) auto;align-items:center;gap:9px;width:100%;min-height:58px;padding:7px 8px;display:grid}.node-list-item[data-v-fb77a209]:hover{background:#f5f8f6}.node-list-item.active[data-v-fb77a209]{background:#edf6f2;border-color:#b9d8cc}.node-index[data-v-fb77a209]{color:#88958f;font-family:monospace;font-size:11px}.node-meta[data-v-fb77a209]{flex-direction:column;min-width:0;display:flex}.node-meta b[data-v-fb77a209]{text-overflow:ellipsis;white-space:nowrap;font-size:13px;overflow:hidden}.node-meta small[data-v-fb77a209]{color:var(--muted);margin-top:3px;font-size:11px}.node-actions[data-v-fb77a209]{gap:1px;display:flex}.node-actions .ant-btn[data-v-fb77a209]{color:#87938e;width:24px;padding:0}.node-actions .ant-btn[data-v-fb77a209]:hover{color:var(--green)}.node-actions .ant-btn-dangerous[data-v-fb77a209]:hover{color:#c34c4c}.node-editor[data-v-fb77a209]{align-self:start;min-height:600px;padding:24px}.node-editor-head[data-v-fb77a209]{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;padding-bottom:18px;display:flex}.node-editor-head h2[data-v-fb77a209]{margin:6px 0 0;font-family:Noto Serif SC,serif;font-size:22px}.node-type-mark[data-v-fb77a209]{color:var(--green);font-size:10px;font-weight:750}.property-form[data-v-fb77a209]{padding-top:22px}.property-grid[data-v-fb77a209]{grid-template-columns:1fr 1fr;gap:16px;display:grid}.transition-head[data-v-fb77a209]{border-top:1px solid var(--line);justify-content:space-between;margin:8px -24px 0;padding:18px 24px 10px;display:flex}.transition-head>div[data-v-fb77a209]{flex-direction:column;display:flex}.transition-head span[data-v-fb77a209]{color:var(--muted);margin-top:4px;font-size:11px}.transition-row[data-v-fb77a209]{border-bottom:1px solid #edf0ee;grid-template-columns:20px minmax(120px,1fr) auto 36px;align-items:center;gap:10px;min-height:48px;display:grid}.route-line[data-v-fb77a209]{background:var(--green);width:16px;height:1px}.route-target[data-v-fb77a209]{font-weight:600}.route-empty[data-v-fb77a209]{color:var(--muted);padding:20px 0;font-size:12px}.edge-builder[data-v-fb77a209]{padding-top:16px}.edge-main[data-v-fb77a209]{grid-template-columns:minmax(220px,1fr) auto auto;align-items:center;gap:10px;display:grid}.condition-toggle[data-v-fb77a209]{color:var(--muted);align-items:center;gap:7px;font-size:12px;display:flex}.condition-builder[data-v-fb77a209]{border:1px solid var(--line);background:#f7f9f8;border-radius:4px;margin-top:12px;padding:14px}.condition-mode[data-v-fb77a209]{color:var(--muted);justify-content:space-between;align-items:center;margin-bottom:10px;font-size:12px;display:flex}.condition-rule[data-v-fb77a209]{grid-template-columns:1.2fr 130px 1fr 34px;gap:8px;margin-top:8px;display:grid}.editor-empty[data-v-fb77a209]{place-items:center;min-height:400px;display:grid}.snapshot-note[data-v-fb77a209]{color:var(--muted);margin-top:6px;font-size:11px}@media (width<=1050px){.editor-workbench[data-v-fb77a209]{grid-template-columns:230px minmax(0,1fr)}.condition-rule[data-v-fb77a209]{grid-template-columns:1fr 120px 1fr 34px}}@media (width<=760px){.editor-header[data-v-fb77a209]{flex-direction:column;align-items:flex-start;padding:12px;top:62px}.page-actions[data-v-fb77a209]{justify-content:flex-start}.editor-workbench[data-v-fb77a209]{grid-template-columns:1fr;padding:10px}.node-rail[data-v-fb77a209]{position:static}.property-grid[data-v-fb77a209]{grid-template-columns:1fr}.node-editor[data-v-fb77a209]{padding:16px}.transition-head[data-v-fb77a209]{margin:8px -16px 0;padding:16px}.edge-main[data-v-fb77a209],.condition-rule[data-v-fb77a209]{grid-template-columns:1fr}.condition-rule .ant-btn[data-v-fb77a209]{justify-self:end}}
|
||||
3
web/dist/assets/SOPEditorView-DSYlIMXk.js
vendored
3
web/dist/assets/SOPEditorView-DSYlIMXk.js
vendored
File diff suppressed because one or more lines are too long
1
web/dist/assets/SaveOutlined-BbHZub0N.js
vendored
Normal file
1
web/dist/assets/SaveOutlined-BbHZub0N.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{F as e,a as t}from"./client-Bk7n1O1W.js";var n={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M893.3 293.3L730.7 130.7c-7.5-7.5-16.7-13-26.7-16V112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V338.5c0-17-6.7-33.2-18.7-45.2zM384 184h256v104H384V184zm456 656H184V184h136v136c0 17.7 14.3 32 32 32h320c17.7 0 32-14.3 32-32V205.8l136 136V840zM512 442c-79.5 0-144 64.5-144 144s64.5 144 144 144 144-64.5 144-144-64.5-144-144-144zm0 224c-44.2 0-80-35.8-80-80s35.8-80 80-80 80 35.8 80 80-35.8 80-80 80z`}}]},name:`save`,theme:`outlined`};function r(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]==null?{}:Object(arguments[t]),r=Object.keys(n);typeof Object.getOwnPropertySymbols==`function`&&(r=r.concat(Object.getOwnPropertySymbols(n).filter(function(e){return Object.getOwnPropertyDescriptor(n,e).enumerable}))),r.forEach(function(t){i(e,t,n[t])})}return e}function i(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var a=function(i,a){var o=r({},i,a.attrs);return e(t,r({},o,{icon:n}),null)};a.displayName=`SaveOutlined`,a.inheritAttrs=!1;export{a as t};
|
||||
@@ -1 +0,0 @@
|
||||
.back-link[data-v-fb83e22e]{color:var(--muted);margin:-4px 0 12px -14px}.detail-heading[data-v-fb83e22e]{color:#fff;background:#202825;border-radius:6px;justify-content:space-between;align-items:flex-end;gap:24px;min-height:180px;padding:28px;display:flex}.scenario-summary[data-v-fb83e22e]{min-width:0}.detail-heading h1[data-v-fb83e22e]{letter-spacing:0;margin:10px 0 8px;font-family:Noto Serif SC,serif;font-size:28px}.detail-heading p[data-v-fb83e22e]{color:#c3cec9;max-width:720px;margin:0}.heading-tags[data-v-fb83e22e]{color:#a8b6b0;align-items:center;gap:10px;font-size:12px;display:flex}.trigger-copy[data-v-fb83e22e]{color:#aab8b2;max-width:800px;margin-top:14px;font-size:12px}.trigger-copy span[data-v-fb83e22e]{color:#68c6a6;margin-right:9px;font-weight:700}.heading-actions[data-v-fb83e22e]{flex-direction:column;justify-content:space-between;align-self:stretch;align-items:flex-end;display:flex}.heading-actions>div[data-v-fb83e22e]{gap:8px;display:flex}.scenario-code[data-v-fb83e22e]{color:#70817a;font-size:12px}.detail-tabs[data-v-fb83e22e]{margin-top:18px}.toolbar>div[data-v-fb83e22e]{align-items:baseline;gap:12px;display:flex}.toolbar-note[data-v-fb83e22e]{color:var(--muted);font-size:12px}.field-grid[data-v-fb83e22e]{grid-template-columns:1fr 1fr;gap:14px;display:grid}.row-actions[data-v-fb83e22e]{display:flex}code[data-v-fb83e22e]{color:#17654f;background:#eef5f2;border-radius:3px;padding:2px 5px}.sop-list button[data-v-fb83e22e]{text-align:left;border:0;border-bottom:1px solid var(--line);cursor:pointer;background:#fff;grid-template-columns:42px 1fr auto 100px;align-items:center;gap:14px;width:100%;min-height:74px;padding:12px 18px;display:grid}.sop-list button[data-v-fb83e22e]:hover{background:#f8faf9}.sop-icon[data-v-fb83e22e]{width:38px;height:38px;color:var(--green);background:#e8f3ef;border-radius:4px;place-items:center;display:grid}.sop-copy[data-v-fb83e22e]{flex-direction:column;min-width:0;display:flex}.sop-copy small[data-v-fb83e22e]{color:var(--muted);text-overflow:ellipsis;white-space:nowrap;margin-top:4px;overflow:hidden}.sop-date[data-v-fb83e22e]{color:var(--muted);text-align:right;font-size:12px}.empty-state[data-v-fb83e22e]{color:#82908a;text-align:center;padding:64px 20px}.empty-state>span[data-v-fb83e22e]{font-size:32px}.empty-state p[data-v-fb83e22e]{margin:12px 0 0}@media (width<=650px){.detail-heading[data-v-fb83e22e]{flex-direction:column;align-items:flex-start;padding:20px}.heading-actions[data-v-fb83e22e]{align-items:flex-start;gap:14px;width:100%}.scenario-code[data-v-fb83e22e]{display:none}.field-grid[data-v-fb83e22e]{grid-template-columns:1fr;gap:0}.sop-list button[data-v-fb83e22e]{grid-template-columns:36px 1fr auto}.sop-date[data-v-fb83e22e],.toolbar-note[data-v-fb83e22e]{display:none}}
|
||||
1
web/dist/assets/ScenarioDetailView-CMMv-gnv.css
vendored
Normal file
1
web/dist/assets/ScenarioDetailView-CMMv-gnv.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.knowledge-editor[data-v-6df41aa9]{padding:18px}.knowledge-editor header[data-v-6df41aa9]{justify-content:space-between;align-items:center;gap:16px;margin-bottom:14px;display:flex}.knowledge-editor header>div[data-v-6df41aa9]{align-items:center;gap:10px;display:flex}.knowledge-editor header div div[data-v-6df41aa9]{flex-direction:column;display:flex}.knowledge-editor small[data-v-6df41aa9]{color:var(--muted);margin-top:3px}.icon[data-v-6df41aa9]{width:34px;height:34px;color:var(--green);background:#e8f3ef;border-radius:4px;place-items:center;display:grid}.json-input[data-v-6df41aa9]{margin-top:14px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6}.knowledge-editor footer[data-v-6df41aa9]{color:var(--muted);justify-content:space-between;margin-top:10px;font-size:11px;display:flex}@media (width<=700px){.knowledge-editor header[data-v-6df41aa9]{flex-direction:column;align-items:flex-start}.knowledge-editor footer[data-v-6df41aa9]{flex-direction:column;gap:8px}}.contract-editor[data-v-51006677]{padding:18px}.sdk-identifiers[data-v-51006677]{color:var(--muted);flex-wrap:wrap;gap:18px;margin:12px 0;font-size:12px;display:flex}.contract-json[data-v-51006677]{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6}.preview-layout[data-v-51006677]{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px;display:grid}.preview-input[data-v-51006677],.preview-output[data-v-51006677]{min-width:0;padding:18px}.preview-output pre[data-v-51006677]{color:#dbe8e3;white-space:pre-wrap;overflow-wrap:anywhere;background:#202825;border-radius:4px;max-height:680px;margin:0;padding:16px;font:12px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:auto}.preview-output[data-v-51006677] .ant-empty{padding:100px 0}.back-link[data-v-51006677]{color:var(--muted);margin:-4px 0 12px -14px}.detail-heading[data-v-51006677]{color:#fff;background:#202825;border-radius:6px;justify-content:space-between;align-items:flex-end;gap:24px;min-height:180px;padding:28px;display:flex}.scenario-summary[data-v-51006677]{min-width:0}.detail-heading h1[data-v-51006677]{letter-spacing:0;margin:10px 0 8px;font-family:Noto Serif SC,serif;font-size:28px}.detail-heading p[data-v-51006677]{color:#c3cec9;max-width:720px;margin:0}.heading-tags[data-v-51006677]{color:#a8b6b0;align-items:center;gap:10px;font-size:12px;display:flex}.trigger-copy[data-v-51006677]{color:#aab8b2;max-width:800px;margin-top:14px;font-size:12px}.trigger-copy span[data-v-51006677]{color:#68c6a6;margin-right:9px;font-weight:700}.heading-actions[data-v-51006677]{flex-direction:column;justify-content:space-between;align-self:stretch;align-items:flex-end;display:flex}.heading-actions>div[data-v-51006677]{gap:8px;display:flex}.scenario-code[data-v-51006677]{color:#70817a;font-size:12px}.detail-tabs[data-v-51006677]{margin-top:18px}.toolbar>div[data-v-51006677]{align-items:baseline;gap:12px;display:flex}.toolbar-note[data-v-51006677]{color:var(--muted);font-size:12px}.field-grid[data-v-51006677]{grid-template-columns:1fr 1fr;gap:14px;display:grid}.row-actions[data-v-51006677]{display:flex}code[data-v-51006677]{color:#17654f;background:#eef5f2;border-radius:3px;padding:2px 5px}.sop-list button[data-v-51006677]{text-align:left;border:0;border-bottom:1px solid var(--line);cursor:pointer;background:#fff;grid-template-columns:42px 1fr 100px;align-items:center;gap:14px;width:100%;min-height:74px;padding:12px 18px;display:grid}.sop-list button[data-v-51006677]:hover{background:#f8faf9}.sop-icon[data-v-51006677]{width:38px;height:38px;color:var(--green);background:#e8f3ef;border-radius:4px;place-items:center;display:grid}.sop-copy[data-v-51006677]{flex-direction:column;min-width:0;display:flex}.sop-copy small[data-v-51006677]{color:var(--muted);text-overflow:ellipsis;white-space:nowrap;margin-top:4px;overflow:hidden}.sop-date[data-v-51006677]{color:var(--muted);text-align:right;font-size:12px}.empty-state[data-v-51006677]{color:#82908a;text-align:center;padding:64px 20px}.empty-state>span[data-v-51006677]{font-size:32px}.empty-state p[data-v-51006677]{margin:12px 0 0}@media (width<=900px){.preview-layout[data-v-51006677]{grid-template-columns:1fr}}@media (width<=650px){.detail-heading[data-v-51006677]{flex-direction:column;align-items:flex-start;padding:20px}.heading-actions[data-v-51006677]{align-items:flex-start;gap:14px;width:100%}.scenario-code[data-v-51006677]{display:none}.field-grid[data-v-51006677]{grid-template-columns:1fr;gap:0}.sop-list button[data-v-51006677]{grid-template-columns:36px 1fr}.sop-date[data-v-51006677],.toolbar-note[data-v-51006677]{display:none}}
|
||||
File diff suppressed because one or more lines are too long
20
web/dist/assets/ScenarioDetailView-svKKSu2G.js
vendored
Normal file
20
web/dist/assets/ScenarioDetailView-svKKSu2G.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
web/dist/assets/StopOutlined-X-yne_Zl.js
vendored
1
web/dist/assets/StopOutlined-X-yne_Zl.js
vendored
@@ -1 +0,0 @@
|
||||
import{F as e,a as t}from"./client-Bk7n1O1W.js";var n={icon:{tag:`svg`,attrs:{viewBox:`64 64 896 896`,focusable:`false`},children:[{tag:`path`,attrs:{d:`M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372 0-89 31.3-170.8 83.5-234.8l523.3 523.3C682.8 852.7 601 884 512 884zm288.5-137.2L277.2 223.5C341.2 171.3 423 140 512 140c205.4 0 372 166.6 372 372 0 89-31.3 170.8-83.5 234.8z`}}]},name:`stop`,theme:`outlined`};function r(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]==null?{}:Object(arguments[t]),r=Object.keys(n);typeof Object.getOwnPropertySymbols==`function`&&(r=r.concat(Object.getOwnPropertySymbols(n).filter(function(e){return Object.getOwnPropertyDescriptor(n,e).enumerable}))),r.forEach(function(t){i(e,t,n[t])})}return e}function i(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var a=function(i,a){var o=r({},i,a.attrs);return e(t,r({},o,{icon:n}),null)};a.displayName=`StopOutlined`,a.inheritAttrs=!1;export{a as t};
|
||||
55
web/dist/assets/config-provider-B-t02-Z0.js
vendored
Normal file
55
web/dist/assets/config-provider-B-t02-Z0.js
vendored
Normal file
File diff suppressed because one or more lines are too long
55
web/dist/assets/config-provider-CxhqvDI_.js
vendored
55
web/dist/assets/config-provider-CxhqvDI_.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
12
web/dist/index.html
vendored
12
web/dist/index.html
vendored
@@ -5,25 +5,23 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#202825" />
|
||||
<title>销冠 SOP 平台</title>
|
||||
<script type="module" crossorigin src="/assets/index-DpE4cxr4.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-Cu-5ZSv-.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/client-Bk7n1O1W.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/config-provider-CxhqvDI_.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/config-provider-B-t02-Z0.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/auth-D6Tj4g1X.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/modal-C5Vo2Ug5.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/EyeOutlined-C1VmLQCG.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/modal-Dwr7ZWlh.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/SearchOutlined-D6c8vQZ_.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/ReloadOutlined-Cih9hKpY.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/PlusOutlined-CosM7zFe.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/ArrowLeftOutlined-B1KoeknA.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/AppstoreOutlined-BD2QJkU0.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/DeleteOutlined-Bt9-1HZZ.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/CopyOutlined-lDB-4CmK.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/EditOutlined-BIUW-zYI.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/BookOutlined-Canuw-q-.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/HistoryOutlined-ABEqcIfz.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/PlayCircleOutlined-DjeS67m_.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/TeamOutlined-DCK_-ckK.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/useApi-CROJJdhE-C8LJOf_8.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D6NKEiQp.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CA3U5glB.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
29
web/src/components/ScenarioKnowledgeTab.vue
Normal file
29
web/src/components/ScenarioKnowledgeTab.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { CodeOutlined, SaveOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { api, apiMessage } from '@/api/client'
|
||||
import type { ScenarioField } from '@/types'
|
||||
|
||||
interface KnowledgeItem { id?:number; key?:string; item_key?:string; name:string; type:string; content:Record<string,unknown>; status:string; sort_order:number }
|
||||
interface KnowledgeRelation { from?:string; to?:string; relation_type:string; from_knowledge_id?:number; to_knowledge_id?:number; condition?:Record<string,unknown>; sort_order:number }
|
||||
const props = defineProps<{ scenarioId:number; fields:ScenarioField[]; editable:boolean }>()
|
||||
const loading=ref(false);const saving=ref(false);const jsonText=ref('{\n "items": [],\n "relations": []\n}')
|
||||
const parsed=computed(()=>{try{return JSON.parse(jsonText.value)}catch{return null}})
|
||||
async function load(){loading.value=true;try{const data=await api.get<{items:KnowledgeItem[];relations:KnowledgeRelation[]}>(`/scenarios/${props.scenarioId}/knowledge-graph`);const keyById=new Map(data.items.map(item=>[item.id,item.item_key]));jsonText.value=JSON.stringify({items:data.items.map(item=>({key:item.item_key,name:item.name,type:item.type,content:item.content||{},status:item.status,sort_order:item.sort_order})),relations:data.relations.map(rel=>({from:keyById.get(rel.from_knowledge_id),relation_type:rel.relation_type,to:keyById.get(rel.to_knowledge_id),condition:rel.condition||{},sort_order:rel.sort_order}))},null,2)}catch(error){message.error(apiMessage(error))}finally{loading.value=false}}
|
||||
watch(()=>props.scenarioId,load,{immediate:true})
|
||||
async function save(){if(!parsed.value)return message.warning('知识 JSON 格式不正确');saving.value=true;try{await api.put(`/scenarios/${props.scenarioId}/knowledge-graph`,parsed.value);message.success('场景知识已保存');await load()}catch(error){message.error(apiMessage(error))}finally{saving.value=false}}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="surface knowledge-editor">
|
||||
<header><div><span class="icon"><CodeOutlined /></span><div><b>场景知识 JSON</b><small>定义知识实体与关系;知识类型和关系类型均由场景决定</small></div></div><a-button v-if="editable" type="primary" :loading="saving" @click="save"><SaveOutlined />保存知识</a-button></header>
|
||||
<a-alert type="info" show-icon message="通用格式:items + relations。宠物场景也支持 symptoms[].diseases[],一个症状可关联多个疾病。" />
|
||||
<a-spin :spinning="loading"><a-textarea v-model:value="jsonText" class="json-input" :disabled="!editable" :auto-size="{minRows:22,maxRows:38}" spellcheck="false" /></a-spin>
|
||||
<footer><span>{{ parsed ? 'JSON 格式有效' : 'JSON 格式错误' }}</span><span>{{ fields.length }} 个场景输入字段可供规则和话术引用</span></footer>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.knowledge-editor{padding:18px}.knowledge-editor header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:14px}.knowledge-editor header>div{display:flex;align-items:center;gap:10px}.knowledge-editor header div div{display:flex;flex-direction:column}.knowledge-editor small{margin-top:3px;color:var(--muted)}.icon{width:34px;height:34px;display:grid;place-items:center;color:var(--green);background:#e8f3ef;border-radius:4px}.json-input{margin-top:14px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6}.knowledge-editor footer{display:flex;justify-content:space-between;margin-top:10px;color:var(--muted);font-size:11px}@media(max-width:700px){.knowledge-editor header{align-items:flex-start;flex-direction:column}.knowledge-editor footer{gap:8px;flex-direction:column}}
|
||||
</style>
|
||||
@@ -4,8 +4,6 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
AuditOutlined,
|
||||
BookOutlined,
|
||||
DashboardOutlined,
|
||||
HistoryOutlined,
|
||||
LogoutOutlined,
|
||||
@@ -25,22 +23,20 @@ const selectedKeys = computed(() => {
|
||||
if (route.path.startsWith('/scenarios') || route.path.startsWith('/sops')) return ['scenarios']
|
||||
if (route.path.startsWith('/execute')) return ['execute']
|
||||
if (route.path.startsWith('/runs')) return ['runs']
|
||||
if (route.path.startsWith('/reviews')) return ['reviews']
|
||||
if (route.path.startsWith('/members')) return ['members']
|
||||
if (route.path.startsWith('/knowledge')) return ['knowledge']
|
||||
return ['dashboard']
|
||||
})
|
||||
|
||||
const titles: Record<string, string> = {
|
||||
dashboard: '工作台', scenarios: '场景与 SOP', reviews: '审核中心', execute: '执行话术', runs: '执行记录', knowledge: '知识卡', members: '企业成员',
|
||||
dashboard: '工作台', scenarios: '场景与 SOP', execute: '执行话术', runs: '执行记录', members: '企业成员',
|
||||
}
|
||||
const pageTitle = computed(() => titles[selectedKeys.value[0]] || '销冠 SOP')
|
||||
const roleText = computed(() => ({admin:'企业管理员',editor:'SOP 编辑者',reviewer:'审核者',operator:'一线执行者'}[auth.user?.role_code||'']||auth.user?.role_code||''))
|
||||
const roleText = computed(() => ({admin:'企业管理员',editor:'SOP 编辑者',operator:'一线执行者'}[auth.user?.role_code||'']||auth.user?.role_code||''))
|
||||
|
||||
onMounted(() => auth.loadUser().catch(() => auth.logout()))
|
||||
|
||||
function navigate({ key }: { key: string }) {
|
||||
const target: Record<string, string> = { dashboard: '/', scenarios: '/scenarios', reviews: '/reviews', execute: '/execute', runs: '/runs', knowledge: '/knowledge', members: '/members' }
|
||||
const target: Record<string, string> = { dashboard: '/', scenarios: '/scenarios', execute: '/execute', runs: '/runs', members: '/members' }
|
||||
mobileOpen.value = false
|
||||
router.push(target[key])
|
||||
}
|
||||
@@ -64,10 +60,8 @@ function logout() {
|
||||
<a-menu mode="inline" theme="dark" :selected-keys="selectedKeys" @click="navigate">
|
||||
<a-menu-item key="dashboard"><DashboardOutlined /><span>工作台</span></a-menu-item>
|
||||
<a-menu-item v-if="auth.can('scenario.view')" key="scenarios"><AppstoreOutlined /><span>场景与 SOP</span></a-menu-item>
|
||||
<a-menu-item v-if="auth.can('sop.review')" key="reviews"><AuditOutlined /><span>审核中心</span></a-menu-item>
|
||||
<a-menu-item v-if="auth.can('sop.execute')" key="execute"><PlayCircleOutlined /><span>执行话术</span></a-menu-item>
|
||||
<a-menu-item v-if="auth.can('runs.view_all')||auth.can('runs.view_own')" key="runs"><HistoryOutlined /><span>执行记录</span></a-menu-item>
|
||||
<a-menu-item v-if="auth.can('knowledge.view')" key="knowledge"><BookOutlined /><span>知识卡</span></a-menu-item>
|
||||
<a-menu-item v-if="auth.can('member.manage')" key="members"><TeamOutlined /><span>企业成员</span></a-menu-item>
|
||||
</a-menu>
|
||||
<div class="sider-foot" :class="{ compact: collapsed }">
|
||||
@@ -109,10 +103,8 @@ function logout() {
|
||||
<a-menu mode="inline" theme="dark" :selected-keys="selectedKeys" @click="navigate">
|
||||
<a-menu-item key="dashboard"><DashboardOutlined /><span>工作台</span></a-menu-item>
|
||||
<a-menu-item v-if="auth.can('scenario.view')" key="scenarios"><AppstoreOutlined /><span>场景与 SOP</span></a-menu-item>
|
||||
<a-menu-item v-if="auth.can('sop.review')" key="reviews"><AuditOutlined /><span>审核中心</span></a-menu-item>
|
||||
<a-menu-item v-if="auth.can('sop.execute')" key="execute"><PlayCircleOutlined /><span>执行话术</span></a-menu-item>
|
||||
<a-menu-item v-if="auth.can('runs.view_all')||auth.can('runs.view_own')" key="runs"><HistoryOutlined /><span>执行记录</span></a-menu-item>
|
||||
<a-menu-item v-if="auth.can('knowledge.view')" key="knowledge"><BookOutlined /><span>知识卡</span></a-menu-item>
|
||||
<a-menu-item v-if="auth.can('member.manage')" key="members"><TeamOutlined /><span>企业成员</span></a-menu-item>
|
||||
</a-menu>
|
||||
</a-drawer>
|
||||
|
||||
@@ -16,9 +16,7 @@ const router = createRouter({
|
||||
{ path: 'execute', name: 'execute', component: () => import('@/views/ExecuteView.vue') },
|
||||
{ path: 'runs', name: 'runs', component: () => import('@/views/RunHistoryView.vue') },
|
||||
{ path: 'runs/:id', name: 'run-detail', component: () => import('@/views/RunDetailView.vue') },
|
||||
{ path: 'reviews', name: 'reviews', component: () => import('@/views/ReviewCenterView.vue') },
|
||||
{ path: 'members', name: 'members', component: () => import('@/views/MembersView.vue') },
|
||||
{ path: 'knowledge', name: 'knowledge', component: () => import('@/views/KnowledgeView.vue') },
|
||||
{ path: 'members', name: 'members', component: () => import('@/views/MembersView.vue') },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
export interface BaseEntity { id: number; created_at: string; updated_at: string }
|
||||
export interface User { user_id: number; tenant_id: number; role_code: string; permissions: string[]; username: string; display_name: string }
|
||||
export interface Scenario extends BaseEntity { tenant_id: number; name: string; industry: string; role_name: string; goal: string; trigger_text: string; visibility: string; status: string; created_by: number }
|
||||
export interface ScenarioField extends BaseEntity { scenario_id: number; field_key: string; field_name: string; field_type: string; required: boolean; options: unknown[]; validation: Record<string, unknown>; sort_order: number }
|
||||
export interface Scenario extends BaseEntity { tenant_id: number; scenario_key:string; public_key:string; allowed_origins:string[]; name: string; industry: string; role_name: string; goal: string; trigger_text: string; visibility: string; status: string; created_by: number; input_schema:Record<string,unknown>; output_schema:Record<string,unknown>; result_schema:Record<string,unknown> }
|
||||
export interface ScenarioField extends BaseEntity { scenario_id: number; field_key: string; field_name: string; source_path:string; field_type: string; required: boolean; options: unknown[]; validation: Record<string, unknown>; sort_order: number }
|
||||
export interface SOP extends BaseEntity { scenario_id: number; name: string; description: string; status: string }
|
||||
export interface SOPVersion extends BaseEntity { sop_id: number; version: number; status: string; start_node_key: string; published_at?: string; created_by?:number; reviewed_by?:number; creator_name?:string; reviewer_name?:string }
|
||||
export interface KnowledgeSnapshot { card_id:number; card_version_id:number; version:number; title:string; standard_copy:string; forbidden_copy:string; risk_note:string }
|
||||
export interface SOPNode extends BaseEntity { node_key: string; type: string; title: string; content: string; config: Record<string, any>; position_x: number; position_y: number; knowledge?:KnowledgeSnapshot }
|
||||
export interface KnowledgeCopy { id:string; title:string; content:string }
|
||||
export interface KnowledgeSnapshot { card_id:number; card_version_id:number; version:number; title:string; standard_copy:string; forbidden_copy:string; risk_note:string; copies:KnowledgeCopy[] }
|
||||
export interface KnowledgeItemView { key:string; name:string; type:string; content:Record<string,any> }
|
||||
export interface KnowledgeGroup extends KnowledgeItemView { relations:Record<string,KnowledgeItemView[]>; suggested?:boolean }
|
||||
export interface PublicFieldView { key:string; name:string; type:string; required:boolean; options:string[]; validation:Record<string,unknown> }
|
||||
export interface KnowledgeCollectionOption { value:string; label:string; parents?:string[]; suggested?:boolean }
|
||||
export interface KnowledgeCollectionStep { field_key:string; name:string; required:boolean; multiple:boolean; from_field?:string; options:KnowledgeCollectionOption[] }
|
||||
export interface KnowledgeCollection { context_fields:PublicFieldView[]; context_title?:string; context_hint?:string; selection_title?:string; selection_hint?:string; steps:KnowledgeCollectionStep[] }
|
||||
export interface PresentationField { key:string; label:string; kind:'text'|'image'; value:unknown }
|
||||
export interface NodePresentation { summary:PresentationField[]; items:Array<{fields:PresentationField[]}>; opening?:{title:string; content:string} }
|
||||
export interface SOPNode extends BaseEntity { node_key: string; type: string; title: string; content: string; config: Record<string, any>; position_x: number; position_y: number; presentation?:NodePresentation; knowledge?:KnowledgeSnapshot; outputs?:KnowledgeGroup[]; collection?:KnowledgeCollection }
|
||||
export interface SOPEdge extends BaseEntity { source_node_key: string; target_node_key: string; condition: Record<string, any>; priority: number }
|
||||
export interface SOPRun extends BaseEntity { sop_id: number; sop_version_id: number; operator_id:number; current_node_key: string; status: string; answers: Record<string, any>; result: string; started_at: string; completed_at?: string; sop_name?: string; scenario_name?:string; version?:number; operator_name?:string }
|
||||
export interface KnowledgeCard extends BaseEntity { scenario_id: number; title: string; status: string; version?: number; content?: Record<string, any> }
|
||||
export interface KnowledgeCardVersion extends BaseEntity { knowledge_card_id:number; version:number; status:string; content:Record<string,any> }
|
||||
export interface ReviewItem { sop_id:number; sop_name:string; description:string; scenario_id:number; scenario_name:string; version_id:number; version:number; creator_id:number; creator_name:string; submitted_at:string }
|
||||
export interface RunEvent extends BaseEntity { run_id:number; node_key:string; action:string; payload:Record<string,any>; node_title:string; node_type:string; node_content:string; knowledge?:KnowledgeSnapshot }
|
||||
export interface SOPRun extends BaseEntity { sop_id: number; sop_version_id: number; operator_id:number; external_ref?:string; current_node_key: string; status: string; answers: Record<string, any>; input:Record<string,any>; derived:Record<string,any>; outputs:unknown[]; result: string; final_result?:Record<string,unknown>|null; started_at: string; completed_at?: string; sop_name?: string; scenario_name?:string; operator_name?:string }
|
||||
export interface RunEvent extends BaseEntity { run_id:number; node_key:string; action:string; payload:Record<string,any>; node_title:string; node_type:string; node_content:string; knowledge?:KnowledgeSnapshot; outputs?:KnowledgeGroup[] }
|
||||
export interface RunFeedback extends BaseEntity { run_id:number; user_id:number; score:number; comment:string; user_name:string }
|
||||
export interface RunDetail extends SOPRun { scenario_name:string; version:number; operator_name:string }
|
||||
export interface RunDetail extends SOPRun { scenario_name:string; operator_name:string }
|
||||
export interface Role extends BaseEntity { name:string; code:string; permissions:string[] }
|
||||
export interface Member { id:number; user_id:number; username:string; display_name:string; role_code:string; role_name:string; status:string }
|
||||
|
||||
@@ -25,7 +25,7 @@ onMounted(async () => {
|
||||
<a-skeleton :loading="loading" active>
|
||||
<section class="metric-strip surface">
|
||||
<div class="metric"><span class="metric-icon green"><AppstoreOutlined /></span><div><b>{{ summary.scenarios }}</b><small>可用场景</small></div></div>
|
||||
<div class="metric"><span class="metric-icon coral"><CheckCircleOutlined /></span><div><b>{{ summary.published_sops }}</b><small>已发布 SOP</small></div></div>
|
||||
<div class="metric"><span class="metric-icon coral"><CheckCircleOutlined /></span><div><b>{{ summary.published_sops }}</b><small>可用 SOP</small></div></div>
|
||||
<div class="metric"><span class="metric-icon amber"><PlayCircleOutlined /></span><div><b>{{ summary.runs }}</b><small>累计执行</small></div></div>
|
||||
<div class="metric"><span class="completion">{{ summary.runs ? Math.round(summary.completed_runs / summary.runs * 100) : 0 }}%</span><div><b>{{ summary.completed_runs }}</b><small>完成执行</small></div></div>
|
||||
</section>
|
||||
@@ -35,15 +35,14 @@ onMounted(async () => {
|
||||
<div class="surface action-panel">
|
||||
<div class="panel-kicker">常用入口</div>
|
||||
<button v-if="auth.can('scenario.edit')" @click="router.push('/scenarios')"><span>配置新的业务场景<small>定义字段、目标和触发条件</small></span><ArrowRightOutlined /></button>
|
||||
<button v-if="auth.can('sop.review')" @click="router.push('/reviews')"><span>处理待审核 SOP<small>核对流程、口径和风险出口</small></span><ArrowRightOutlined /></button>
|
||||
<button v-if="auth.can('sop.execute')" @click="router.push('/execute')"><span>开始执行已发布 SOP<small>根据客户回答逐步推进</small></span><ArrowRightOutlined /></button>
|
||||
<button v-if="auth.can('knowledge.edit')" @click="router.push('/knowledge')"><span>维护审核知识卡<small>统一口径与风险提示</small></span><ArrowRightOutlined /></button>
|
||||
<button v-if="auth.can('sop.execute')" @click="router.push('/execute')"><span>开始执行 SOP<small>根据客户回答逐步推进</small></span><ArrowRightOutlined /></button>
|
||||
<button v-if="auth.can('knowledge.edit')" @click="router.push('/scenarios')"><span>配置场景知识卡<small>在所属场景内维护输入关联与话术</small></span><ArrowRightOutlined /></button>
|
||||
<button v-if="auth.can('runs.view_all')||auth.can('runs.view_own')" @click="router.push('/runs')"><span>复盘执行记录<small>查看节点轨迹、回答与反馈</small></span><ArrowRightOutlined /></button>
|
||||
</div>
|
||||
<div class="surface doctrine-panel">
|
||||
<div class="panel-kicker">平台原则</div>
|
||||
<blockquote>一句好话术不应只被收藏,它应该知道何时出现、下一步去哪,以及是否真的有效。</blockquote>
|
||||
<div class="flow-note"><span>创建</span><i></i><span>发布</span><i></i><span>执行</span><i></i><span>复盘</span></div>
|
||||
<div class="flow-note"><span>创建</span><i></i><span>配置</span><i></i><span>执行</span><i></i><span>复盘</span></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
||||
import { CheckCircleOutlined, PlayCircleOutlined, SafetyCertificateOutlined, StopOutlined } from '@ant-design/icons-vue'
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { CheckCircleOutlined, CopyOutlined, PlayCircleOutlined, SafetyCertificateOutlined, StopOutlined } from '@ant-design/icons-vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { api, apiMessage } from '@/api/client'
|
||||
import type { ScenarioField, SOPNode, SOPRun } from '@/types'
|
||||
import type { KnowledgeCollectionStep, PresentationField, PublicFieldView, ScenarioField, SOPNode, SOPRun } from '@/types'
|
||||
|
||||
interface PublishedSOP { id:number; name:string; description:string; scenario_id:number; scenario_name:string; version:number }
|
||||
interface RunView { run:SOPRun; node:SOPNode; fields:ScenarioField[] }
|
||||
interface AvailableSOP { id:number; name:string; description:string; scenario_id:number; scenario_name:string }
|
||||
interface ResultSchema { fields:Array<Record<string,any>> }
|
||||
interface RunView { run:SOPRun; node:SOPNode; fields:ScenarioField[]; result_schema?:ResultSchema }
|
||||
const petDoctorDemoInput={
|
||||
order:{id:'ORDER-DEMO-001',items:[{product_id:'SKU-DEMO-001',product_name:'肠胃护理商品',image_url:'https://placehold.co/160x160/f3f6f4/17654f?text=Pet+Product',symptom_tags:['腹泻']}]},
|
||||
customer:{name:'王女士'},
|
||||
pet:{name:'团子',species:'猫'},
|
||||
}
|
||||
const route=useRoute();const router=useRouter()
|
||||
const loading=ref(true);const sops=ref<PublishedSOP[]>([]);const active=ref<RunView|null>(null);const answers=reactive<Record<string,any>>({});const submitting=ref(false)
|
||||
const loading=ref(true);const sops=ref<AvailableSOP[]>([]);const active=ref<RunView|null>(null);const answers=reactive<Record<string,any>>({});const submitting=ref(false)
|
||||
const startOpen=ref(false);const selectedSOP=ref<AvailableSOP|null>(null)
|
||||
const startForm=reactive({external_ref:'',input_text:'{}',initial_values:{} as Record<string,any>})
|
||||
const finalResultText=ref('{}')
|
||||
const resultSaved=ref(false)
|
||||
const answerForm=ref<FormInstance>()
|
||||
const feedback=reactive({score:5,comment:''});const feedbackSent=ref(false)
|
||||
const currentConfig=computed(()=>active.value?.node.config||{})
|
||||
const currentFields=computed(()=>{if(!active.value)return[];const node=active.value.node;if(node.type==='question'||node.type==='choice')return active.value.fields.filter(f=>f.field_key===currentConfig.value.field_key);if(node.type==='form')return active.value.fields.filter(f=>(currentConfig.value.field_keys||[]).includes(f.field_key));return[]})
|
||||
async function load(){loading.value=true;try{sops.value=(await api.get<{items:PublishedSOP[]}>('/published-sops')).items;const runID=Number(route.query.run||0);if(runID>0)active.value=await api.get<RunView>(`/runs/${runID}`)}catch(error){message.error(apiMessage(error));await router.replace({query:{}})}finally{loading.value=false}}
|
||||
const collectionFields=computed(()=>active.value?.node.collection?.context_fields||[])
|
||||
const collectionSteps=computed(()=>active.value?.node.collection?.steps||[])
|
||||
const visibleKnowledgeGroups=computed(()=>{const groups=active.value?.node.outputs||[];const root=collectionSteps.value[0];if(!root)return groups;return groups.filter(group=>group.suggested||isSelected(root.field_key,group.name))})
|
||||
const startPresentation=computed(()=>active.value?.node.type==='start'?active.value.node.presentation:undefined)
|
||||
function presentationText(value:unknown){if(Array.isArray(value))return value.join('、');if(value===null||value===undefined||value==='')return '-';return String(value)}
|
||||
function itemImage(item:{fields:PresentationField[]}){return item.fields.find(field=>field.kind==='image')}
|
||||
function itemDetails(item:{fields:PresentationField[]}){return item.fields.filter(field=>field.kind!=='image')}
|
||||
function hideBrokenImage(event:Event){const image=event.target as HTMLImageElement;image.style.display='none';image.parentElement?.classList.add('no-image')}
|
||||
async function copyOpening(){const content=startPresentation.value?.opening?.content;if(!content)return;try{await navigator.clipboard.writeText(content);message.success('开场话术已复制')}catch{message.warning('复制失败,请手动选择话术') }}
|
||||
function collectionOptions(step:KnowledgeCollectionStep){if(!step.from_field)return step.options;const value=answers[step.from_field];const parents=Array.isArray(value)?value:value?[value]:[];return step.options.filter(option=>(option.parents||[]).some(parent=>parents.includes(parent)))}
|
||||
function collectionBlocked(step:KnowledgeCollectionStep){if(!step.from_field)return false;const value=answers[step.from_field];return !Array.isArray(value)||value.length===0}
|
||||
function onCollectionChange(step:KnowledgeCollectionStep){let clear=false;for(const item of collectionSteps.value){if(clear){const allowed=new Set(collectionOptions(item).map(option=>option.value));const current=Array.isArray(answers[item.field_key])?answers[item.field_key]:[];answers[item.field_key]=current.filter((value:string)=>allowed.has(value))}if(item.field_key===step.field_key)clear=true}}
|
||||
function publicFieldRules(field:PublicFieldView){return field.required?[{required:true,message:`请填写${field.name}`,trigger:['change','blur']}]:[]}
|
||||
function collectionStepRules(step:KnowledgeCollectionStep){return step.required?[{required:true,type:step.multiple?'array':'string',message:`请选择${step.name}`,trigger:'change'}]:[]}
|
||||
function collectionStep(index:number){return collectionSteps.value[index]}
|
||||
function selectedValues(fieldKey:string){return Array.isArray(answers[fieldKey])?answers[fieldKey]:[]}
|
||||
function isSelected(fieldKey:string,value:string){return selectedValues(fieldKey).includes(value)}
|
||||
function toggleKnowledge(step:KnowledgeCollectionStep,value:string,checked:boolean){const current=selectedValues(step.field_key);answers[step.field_key]=checked?[...new Set([...current,value])]:current.filter((item:string)=>item!==value);onCollectionChange(step)}
|
||||
function relationLabel(relation:string){return currentConfig.value.knowledge_selector?.relation_labels?.[relation]||relation}
|
||||
async function load(){loading.value=true;try{sops.value=(await api.get<{items:AvailableSOP[]}>('/available-sops')).items;const runID=Number(route.query.run||0);if(runID>0){active.value=await api.get<RunView>(`/runs/${runID}`);await prepareNodeAnswers()}else{active.value=null;await clearAnswers()}}catch(error){message.error(apiMessage(error));await router.replace({query:{}})}finally{loading.value=false}}
|
||||
async function clearAnswers(){Object.keys(answers).forEach(k=>delete answers[k]);await nextTick();answerForm.value?.clearValidate()}
|
||||
async function start(sopID:number){try{active.value=await api.post<RunView>('/runs',{sop_id:sopID});await router.replace({query:{run:String(active.value.run.id)}});await clearAnswers();Object.assign(feedback,{score:5,comment:''});feedbackSent.value=false}catch(error){message.error(apiMessage(error))}}
|
||||
async function next(){if(!active.value)return;try{await answerForm.value?.validate()}catch{return}submitting.value=true;try{active.value=await api.post<RunView>(`/runs/${active.value.run.id}/answer`,{node_key:active.value.node.node_key,answers:{...answers}});await clearAnswers()}catch(error){message.error(apiMessage(error))}finally{submitting.value=false}}
|
||||
async function prepareNodeAnswers(){await clearAnswers();if(!active.value)return;for(const field of currentFields.value){const value=active.value.run.answers?.[field.field_key];if(value!==undefined&&value!==null)answers[field.field_key]=value}}
|
||||
async function openStart(item:AvailableSOP){selectedSOP.value=item;startForm.external_ref=`business:${Date.now()}`;startForm.input_text=item.scenario_name==='宠物医生问诊问药'?JSON.stringify(petDoctorDemoInput,null,2):'{}';startForm.initial_values={};startOpen.value=true}
|
||||
async function start(){if(!selectedSOP.value)return;let input:Record<string,any>;try{input=JSON.parse(startForm.input_text)}catch{return message.warning('业务数据 JSON 格式不正确')}submitting.value=true;try{active.value=await api.post<RunView>('/runs',{sop_id:selectedSOP.value.id,input,external_ref:startForm.external_ref,initial_values:startForm.initial_values});startOpen.value=false;await router.replace({query:{run:String(active.value.run.id)}});await prepareNodeAnswers();Object.assign(feedback,{score:5,comment:''});feedbackSent.value=false;resultSaved.value=Boolean(active.value.run.final_result)}catch(error){message.error(apiMessage(error))}finally{submitting.value=false}}
|
||||
async function next(){if(!active.value)return;try{await answerForm.value?.validate()}catch{return}for(const step of collectionSteps.value){if(step.required&&!selectedValues(step.field_key).length)return message.warning(`请选择${step.name}`)}submitting.value=true;try{active.value=await api.post<RunView>(`/runs/${active.value.run.id}/answer`,{node_key:active.value.node.node_key,answers:{...answers}});await prepareNodeAnswers()}catch(error){message.error(apiMessage(error))}finally{submitting.value=false}}
|
||||
function finishManually(){
|
||||
if(!active.value)return
|
||||
Modal.confirm({
|
||||
@@ -36,7 +67,7 @@ function finishManually(){
|
||||
}
|
||||
function inputFor(field:ScenarioField){return field.field_type}
|
||||
function validationNumber(field:ScenarioField,key:string){const value=field.validation?.[key];return typeof value==='number'?value:undefined}
|
||||
function fieldRequired(field:ScenarioField){return field.required||(['question','choice'].includes(active.value?.node.type||'')&¤tConfig.value.required===true)}
|
||||
function fieldRequired(field:ScenarioField){return field.required||(currentConfig.value.required_field_keys||[]).includes(field.field_key)||(['question','choice'].includes(active.value?.node.type||'')&¤tConfig.value.required===true)}
|
||||
function fieldRules(field:ScenarioField){
|
||||
const rules:any[]=[]
|
||||
if(fieldRequired(field))rules.push({required:true,message:`请填写${field.field_name}`,trigger:['change','blur']})
|
||||
@@ -54,17 +85,19 @@ function fieldRules(field:ScenarioField){
|
||||
return rules
|
||||
}
|
||||
async function sendFeedback(){if(!active.value)return;submitting.value=true;try{await api.post(`/runs/${active.value.run.id}/feedback`,feedback);feedbackSent.value=true;message.success('反馈已提交')}catch(error){message.error(apiMessage(error))}finally{submitting.value=false}}
|
||||
async function saveFinalResult(){if(!active.value)return;let finalResult:Record<string,any>|null;try{finalResult=finalResultText.value.trim()?JSON.parse(finalResultText.value):null}catch{return message.warning('最终结果 JSON 格式不正确')}submitting.value=true;try{active.value=await api.post<RunView>(`/runs/${active.value.run.id}/finish`,{result:active.value.run.result||'finish',final_result:finalResult});resultSaved.value=true;message.success('最终结果已保存到本次执行')}catch(error){message.error(apiMessage(error))}finally{submitting.value=false}}
|
||||
async function reset(){active.value=null;await router.replace({query:{}});await load()}
|
||||
watch(()=>route.query.run,()=>load())
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-shell execution-shell">
|
||||
<div class="page-heading"><div><h1>执行话术</h1><p>选择已发布 SOP,系统会根据客户回答给出下一步。</p></div></div>
|
||||
<div class="page-heading"><div><h1>执行话术</h1><p>选择 SOP,系统会根据客户回答给出下一步。</p></div></div>
|
||||
<a-spin :spinning="loading">
|
||||
<div v-if="!active" class="sop-catalog">
|
||||
<button v-for="item in sops" :key="item.id" class="sop-entry surface" @click="start(item.id)"><span class="entry-seq">{{ String(item.id).padStart(2,'0') }}</span><div><small>{{ item.scenario_name }} · V{{ item.version }}</small><h2>{{ item.name }}</h2><p>{{ item.description || '按标准步骤执行这套话术流程。' }}</p></div><span class="play"><PlayCircleOutlined /></span></button>
|
||||
<a-empty v-if="!sops.length" description="还没有已发布的 SOP" />
|
||||
<button v-for="item in sops" :key="item.id" class="sop-entry surface" @click="openStart(item)"><span class="entry-seq">{{ String(item.id).padStart(2,'0') }}</span><div><small>{{ item.scenario_name }}</small><h2>{{ item.name }}</h2><p>{{ item.description || '按标准步骤执行这套话术流程。' }}</p></div><span class="play"><PlayCircleOutlined /></span></button>
|
||||
<a-empty v-if="!sops.length" description="还没有可用的 SOP" />
|
||||
</div>
|
||||
<div v-else class="run-workspace">
|
||||
<aside class="run-context">
|
||||
@@ -76,13 +109,36 @@ onMounted(load)
|
||||
<main class="conversation surface">
|
||||
<div class="conversation-progress"><span :class="{done:active.run.status==='completed'}"></span><b>{{ active.run.status==='completed'?'流程已完成':'正在执行' }}</b></div>
|
||||
<div class="node-content"><small>{{ active.node.type.toUpperCase() }}</small><h1>{{ active.node.title }}</h1><blockquote v-if="active.node.content">{{ active.node.content }}</blockquote></div>
|
||||
<section v-if="startPresentation" class="start-presentation">
|
||||
<div v-if="startPresentation.summary.length" class="start-summary">
|
||||
<div v-for="field in startPresentation.summary" :key="field.key"><small>{{ field.label }}</small><b>{{ presentationText(field.value) }}</b></div>
|
||||
</div>
|
||||
<div v-if="startPresentation.items.length" class="order-products">
|
||||
<div class="presentation-heading"><b>订单商品</b><span>{{ startPresentation.items.length }} 件</span></div>
|
||||
<article v-for="(item,index) in startPresentation.items" :key="index" class="order-product" :class="{'no-image':!itemImage(item)}">
|
||||
<img v-if="itemImage(item)" :src="String(itemImage(item)?.value||'')" alt="订单商品图片" @error="hideBrokenImage" />
|
||||
<div><span v-for="field in itemDetails(item)" :key="field.key"><small>{{ field.label }}</small><b>{{ presentationText(field.value) }}</b></span></div>
|
||||
</article>
|
||||
</div>
|
||||
<div v-if="startPresentation.opening" class="opening-copy">
|
||||
<div class="presentation-heading"><b>{{ startPresentation.opening.title }}</b><a-tooltip title="复制话术"><a-button type="text" aria-label="复制开场话术" @click="copyOpening"><CopyOutlined /></a-button></a-tooltip></div>
|
||||
<p>{{ startPresentation.opening.content }}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="active.node.knowledge" class="knowledge-snapshot">
|
||||
<div class="knowledge-head"><b>{{ active.node.knowledge.title }}</b><span>知识卡 V{{ active.node.knowledge.version }}</span></div>
|
||||
<div class="standard-copy"><small>标准话术</small><p>{{ active.node.knowledge.standard_copy }}</p></div>
|
||||
<div class="knowledge-head"><b>{{ active.node.knowledge.title }}</b><span>历史知识</span></div>
|
||||
<div v-for="copy in active.node.knowledge.copies" :key="copy.id" class="standard-copy"><small>{{ copy.title }}</small><p>{{ copy.content }}</p></div>
|
||||
<div v-if="!active.node.knowledge.copies?.length" class="standard-copy"><small>标准话术</small><p>{{ active.node.knowledge.standard_copy }}</p></div>
|
||||
<div v-if="active.node.knowledge.forbidden_copy" class="knowledge-note forbidden"><small>禁用表达</small><p>{{ active.node.knowledge.forbidden_copy }}</p></div>
|
||||
<div v-if="active.node.knowledge.risk_note" class="knowledge-note risk"><small>风险提醒</small><p>{{ active.node.knowledge.risk_note }}</p></div>
|
||||
</section>
|
||||
<div v-if="active.run.status==='completed'" class="completed-state"><CheckCircleOutlined /><h3>{{ active.run.result==='manual'?'已人工结束':active.node.type==='escalate'?'已转交处理':'本次执行已完成' }}</h3><p>{{ active.run.result==='manual'?'本次执行由操作人员在当前节点结束。':active.node.content }}</p><div v-if="!feedbackSent" class="quick-feedback"><span>这套 SOP 是否清晰好用?</span><a-rate v-model:value="feedback.score"/><a-textarea v-model:value="feedback.comment" :rows="2" :maxlength="2000" placeholder="可填写需要改进的步骤"/><a-button type="primary" :loading="submitting" @click="sendFeedback">提交反馈</a-button></div><div v-else class="feedback-thanks">反馈已记录,将用于后续优化。</div><a-button @click="reset">执行另一套 SOP</a-button></div>
|
||||
<section v-if="visibleKnowledgeGroups.length" class="knowledge-groups">
|
||||
<article v-for="group in visibleKnowledgeGroups" :key="group.key" class="knowledge-group">
|
||||
<div class="knowledge-head"><a-checkbox v-if="active.node.collection&&collectionStep(0)" :checked="isSelected(collectionStep(0).field_key,group.name)" @change="toggleKnowledge(collectionStep(0),group.name,$event.target.checked)"><b>{{ group.name }}</b></a-checkbox><b v-else>{{ group.name }}</b><span>{{ group.suggested?'系统推断':group.type }}</span></div>
|
||||
<div v-for="(items,relation) in group.relations" :key="relation" class="relation-block"><small>{{ relationLabel(String(relation)) }}</small><div v-for="item in items" :key="item.key" class="standard-copy selectable-knowledge"><a-checkbox v-if="active.node.collection&&collectionStep(1)?.from_field===collectionStep(0)?.field_key&¤tConfig.knowledge_collection?.steps?.[1]?.relation_type===relation" :checked="isSelected(collectionStep(1).field_key,item.name)" :disabled="!isSelected(collectionStep(0).field_key,group.name)" @change="toggleKnowledge(collectionStep(1),item.name,$event.target.checked)"><b>{{ item.name }}</b></a-checkbox><b v-else>{{ item.name }}</b><p>{{ item.content.template || item.content.standard_copy || item.content.product_name || item.name }}</p><small v-if="item.content.sku_code">SKU:{{ item.content.sku_code }}</small></div></div>
|
||||
</article>
|
||||
</section>
|
||||
<div v-if="active.run.status==='completed'" class="completed-state"><CheckCircleOutlined /><h3>{{ active.run.result==='manual'?'已人工结束':active.node.type==='escalate'?'已转交处理':'本次执行已完成' }}</h3><p>{{ active.run.result==='manual'?'本次执行由操作人员在当前节点结束。':active.node.content }}</p><section class="result-collector"><div><b>最终结果</b><span>格式由当前场景定义,也可以留空</span></div><pre>{{ JSON.stringify(active.result_schema||{fields:[]},null,2) }}</pre><a-textarea v-model:value="finalResultText" :rows="10" spellcheck="false"/><a-button type="primary" :loading="submitting" @click="saveFinalResult">{{ resultSaved?'更新最终结果':'保存最终结果' }}</a-button></section><div v-if="!feedbackSent" class="quick-feedback"><span>这套 SOP 是否清晰好用?</span><a-rate v-model:value="feedback.score"/><a-textarea v-model:value="feedback.comment" :rows="2" :maxlength="2000" placeholder="可填写需要改进的步骤"/><a-button type="primary" :loading="submitting" @click="sendFeedback">提交反馈</a-button></div><div v-else class="feedback-thanks">反馈已记录,将用于后续优化。</div><a-button @click="reset">执行另一套 SOP</a-button></div>
|
||||
<template v-else>
|
||||
<a-form ref="answerForm" :model="answers" layout="vertical" class="answer-form">
|
||||
<a-form-item v-for="field in currentFields" :key="field.id" :name="field.field_key" :label="field.field_name" :required="fieldRequired(field)" :rules="fieldRules(field)">
|
||||
@@ -96,17 +152,39 @@ onMounted(load)
|
||||
<a-date-picker v-else-if="inputFor(field)==='date'" v-model:value="answers[field.field_key]" value-format="YYYY-MM-DD" style="width:100%" />
|
||||
<a-input v-else v-model:value="answers[field.field_key]" />
|
||||
</a-form-item>
|
||||
<template v-if="active.node.collection">
|
||||
<div class="collection-heading"><b>{{ active.node.collection.context_title||'补充信息' }}</b><span>{{ active.node.collection.context_hint }}</span></div>
|
||||
<a-form-item v-for="field in collectionFields" :key="field.key" :name="field.key" :label="field.name" :required="field.required" :rules="publicFieldRules(field)">
|
||||
<a-input v-if="field.type==='text'" v-model:value="answers[field.key]" />
|
||||
<a-textarea v-else-if="field.type==='textarea'" v-model:value="answers[field.key]" :rows="3" />
|
||||
<a-input-number v-else-if="field.type==='number'" v-model:value="answers[field.key]" style="width:100%" />
|
||||
<a-select v-else-if="field.type==='select'" v-model:value="answers[field.key]" :options="(field.options||[]).map(value=>({value,label:value}))" />
|
||||
</a-form-item>
|
||||
<div class="collection-heading selection-summary"><b>{{ active.node.collection.selection_title||'确认选择' }}</b><span>{{ active.node.collection.selection_hint }}</span></div>
|
||||
<a-form-item v-for="step in collectionSteps" :key="`selector-${step.field_key}`" :name="step.field_key" :label="step.name" :required="step.required" :rules="collectionStepRules(step)">
|
||||
<a-select v-model:value="answers[step.field_key]" :mode="step.multiple?'multiple':undefined" show-search allow-clear :disabled="collectionBlocked(step)" :placeholder="collectionBlocked(step)?'请先选择上一级':`搜索并选择${step.name}`" :options="collectionOptions(step).map(option=>({value:option.value,label:option.suggested?`${option.label}(系统推断)`:option.label}))" @change="onCollectionChange(step)" />
|
||||
</a-form-item>
|
||||
<div class="selected-summary" v-for="step in collectionSteps" :key="step.field_key"><span>{{ step.name }}</span><b>{{ selectedValues(step.field_key).join('、')||'尚未选择' }}</b></div>
|
||||
</template>
|
||||
</a-form>
|
||||
<div class="run-actions"><span>{{ currentFields.length ? '填写后继续下一步' : '确认当前话术已完成' }}</span><div><a-button danger :disabled="submitting" @click="finishManually"><StopOutlined />人工结束</a-button><a-button type="primary" size="large" :loading="submitting" @click="next">继续下一步</a-button></div></div>
|
||||
<div class="run-actions"><span>{{ currentFields.length||active.node.collection ? '填写并确认后继续下一步' : '确认当前话术已完成' }}</span><div><a-button danger :disabled="submitting" @click="finishManually"><StopOutlined />人工结束</a-button><a-button type="primary" size="large" :loading="submitting" @click="next">继续下一步</a-button></div></div>
|
||||
</template>
|
||||
</main>
|
||||
</div>
|
||||
</a-spin>
|
||||
<a-modal v-model:open="startOpen" title="启动场景执行" ok-text="创建执行" :confirm-loading="submitting" width="760px" @ok="start">
|
||||
<a-form layout="vertical"><a-form-item label="业务关联标识"><a-input v-model:value="startForm.external_ref" placeholder="例如 order:ORDER-001"/></a-form-item><a-form-item label="业务数据 JSON"><a-textarea v-model:value="startForm.input_text" :rows="15" spellcheck="false" class="json-input"/></a-form-item></a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.execution-shell{max-width:1220px}.sop-catalog{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.sop-entry{min-height:150px;display:grid;grid-template-columns:40px 1fr 42px;align-items:start;gap:14px;padding:22px;text-align:left;cursor:pointer}.sop-entry:hover{border-color:#9ac8b8;box-shadow:0 8px 24px rgba(32,40,37,.07)}.entry-seq{color:#8c9893;font-family:monospace;font-size:12px}.sop-entry small{color:var(--green);font-weight:650}.sop-entry h2{margin:8px 0 7px;font-family:"Noto Serif SC",serif;font-size:20px}.sop-entry p{margin:0;color:var(--muted);line-height:1.6}.play{width:38px;height:38px;display:grid;place-items:center;color:white;background:#202825;border-radius:4px;font-size:18px}.run-workspace{display:grid;grid-template-columns:270px minmax(0,1fr);gap:18px}.run-context{align-self:start;padding:24px;color:white;background:#202825;border-radius:6px;position:sticky;top:86px}.run-label{color:#72c9ab;font-size:11px;font-weight:700}.run-context h2{margin:12px 0 8px;font-family:"Noto Serif SC",serif}.run-context>p{margin:0 0 28px;color:#aebdb7;line-height:1.6}.context-meta{display:flex;align-items:center;justify-content:space-between;padding:13px 0;border-top:1px solid #3a4742}.context-meta span{color:#9eada7;font-size:12px}.privacy-note{display:flex;gap:8px;margin-top:28px;color:#889b93;font-size:11px}.conversation{min-height:590px;padding:30px 34px}.conversation-progress{display:flex;align-items:center;gap:8px;color:#66736d;font-size:12px}.conversation-progress span{width:8px;height:8px;border-radius:50%;background:#d08736;box-shadow:0 0 0 4px #faefe1}.conversation-progress span.done{background:var(--green);box-shadow:0 0 0 4px #e4f1ec}.node-content{padding:44px 0 26px}.node-content small{color:var(--green);font-size:10px;font-weight:800}.node-content h1{margin:8px 0 22px;font-family:"Noto Serif SC",serif;font-size:28px}.node-content blockquote{margin:0;padding:18px 20px;color:#29342f;background:#f2f6f4;border-left:3px solid var(--green);font-size:17px;line-height:1.8}.answer-form{max-width:680px}.choice-group{display:flex;flex-wrap:wrap}.run-actions{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:24px -34px -30px;padding:18px 34px;border-top:1px solid var(--line);color:var(--muted);font-size:12px}.run-actions>div{display:flex;align-items:center;gap:9px}.completed-state{padding:40px 0;text-align:center}.completed-state>span{color:var(--green);font-size:50px}.completed-state h3{margin:14px 0 8px;font-family:"Noto Serif SC",serif;font-size:24px}.completed-state p{margin:0 0 24px;color:var(--muted)}.quick-feedback{max-width:620px;display:grid;grid-template-columns:180px 1fr auto;align-items:center;gap:12px;margin:0 auto 18px;padding:16px;text-align:left;background:#f5f8f6;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}.quick-feedback>span{font-size:12px;font-weight:650}.quick-feedback :deep(.ant-rate){font-size:18px}.quick-feedback :deep(.ant-input){grid-column:1/3}.feedback-thanks{max-width:520px;margin:0 auto 18px;padding:12px;color:var(--green);background:#edf6f2;font-size:12px}
|
||||
.knowledge-snapshot{margin:0 0 24px;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}.knowledge-head{display:flex;align-items:center;justify-content:space-between;padding:12px 0}.knowledge-head span{color:var(--muted);font-size:11px}.standard-copy,.knowledge-note{padding:14px 16px;border-left:3px solid var(--green);background:#f2f7f5}.standard-copy small,.knowledge-note small{font-size:10px;font-weight:750}.standard-copy p,.knowledge-note p{margin:6px 0 0;line-height:1.7}.knowledge-note{margin-top:8px;border-left-color:#c4573f;background:#fbf3f0}.knowledge-note.risk{border-left-color:#bd7b24;background:#fbf6eb}
|
||||
@media(max-width:800px){.sop-catalog{grid-template-columns:1fr}.run-workspace{grid-template-columns:1fr}.run-context{position:static}.conversation{padding:22px}.run-actions{margin:24px -22px -22px;padding:16px 22px}.run-actions span{display:none}.quick-feedback{grid-template-columns:1fr}.quick-feedback :deep(.ant-input){grid-column:auto}}
|
||||
.knowledge-groups{display:grid;gap:14px;margin-bottom:24px}.knowledge-group{padding:0 16px 16px;border:1px solid var(--line);border-left:3px solid var(--green);background:#fff}.relation-block{margin-top:10px}.relation-block>small{display:block;margin-bottom:6px;color:var(--muted);font-weight:700}.relation-block .standard-copy{margin-top:6px}.relation-block .standard-copy b{font-size:12px}
|
||||
.start-presentation{display:grid;gap:22px;margin-bottom:28px}.start-summary{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));border-top:1px solid var(--line);border-bottom:1px solid var(--line)}.start-summary>div{min-width:0;display:flex;flex-direction:column;gap:5px;padding:14px 16px}.start-summary>div:nth-child(odd){border-right:1px solid var(--line)}.start-summary small,.order-product small{color:var(--muted);font-size:10px}.start-summary b,.order-product b{overflow-wrap:anywhere;font-size:13px}.presentation-heading{min-height:36px;display:flex;align-items:center;justify-content:space-between;gap:12px}.presentation-heading>span{color:var(--muted);font-size:11px}.order-products{display:grid;gap:8px}.order-product{min-height:82px;display:grid;grid-template-columns:72px minmax(0,1fr);gap:14px;align-items:center;padding:10px 0;border-bottom:1px solid var(--line)}.order-product.no-image{grid-template-columns:1fr}.order-product img{width:72px;height:72px;object-fit:cover;border:1px solid var(--line);border-radius:4px;background:#f4f6f5}.order-product>div{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(100px,1fr);gap:12px}.order-product span{min-width:0;display:flex;flex-direction:column;gap:5px}.opening-copy{padding:16px 18px;border-left:3px solid var(--green);background:#edf6f2}.opening-copy .presentation-heading{min-height:28px}.opening-copy p{margin:9px 0 0;color:#21302a;font-size:16px;line-height:1.8}.opening-copy :deep(.ant-btn){color:var(--green)}
|
||||
.collection-heading{display:flex;justify-content:space-between;gap:16px;margin:26px 0 16px;padding-bottom:10px;border-bottom:1px solid var(--line)}.collection-heading span{color:var(--muted);font-size:11px}
|
||||
.selectable-knowledge :deep(.ant-checkbox-wrapper){display:flex;align-items:center}.selected-summary{display:grid;grid-template-columns:130px 1fr;gap:12px;padding:9px 0;border-bottom:1px solid var(--line)}.selected-summary span{color:var(--muted);font-size:12px}.selected-summary b{font-size:13px}.selection-summary{margin-top:20px}
|
||||
.json-input,.result-collector :deep(textarea){font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.result-collector{max-width:700px;margin:0 auto 22px;padding:18px;text-align:left;background:#f5f8f6;border:1px solid var(--line)}.result-collector>div{display:flex;justify-content:space-between;margin-bottom:10px}.result-collector>div span{color:var(--muted);font-size:11px}.result-collector pre{max-height:160px;overflow:auto;padding:10px;background:#202825;color:#dbe8e3;font-size:11px;white-space:pre-wrap}.result-collector button{margin-top:10px}
|
||||
@media(max-width:800px){.sop-catalog{grid-template-columns:1fr}.run-workspace{grid-template-columns:1fr}.run-context{position:static}.conversation{padding:22px}.run-actions{margin:24px -22px -22px;padding:16px 22px}.run-actions span{display:none}.quick-feedback{grid-template-columns:1fr}.quick-feedback :deep(.ant-input){grid-column:auto}.start-summary{grid-template-columns:1fr}.start-summary>div:nth-child(odd){border-right:0}.start-summary>div+div{border-top:1px solid var(--line)}.order-product>div{grid-template-columns:1fr}}
|
||||
</style>
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { BookOutlined, DeleteOutlined, EditOutlined, HistoryOutlined, PlusOutlined } from '@ant-design/icons-vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { api, apiMessage } from '@/api/client'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import type { KnowledgeCard, KnowledgeCardVersion, Scenario } from '@/types'
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const open = ref(false)
|
||||
const historyOpen = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
const historyCard = ref<KnowledgeCard | null>(null)
|
||||
const items = ref<KnowledgeCard[]>([])
|
||||
const versions = ref<KnowledgeCardVersion[]>([])
|
||||
const scenarios = ref<Scenario[]>([])
|
||||
const auth = useAuthStore()
|
||||
const form = reactive({ scenario_id: undefined as number | undefined, title: '', content: '', forbidden: '', risk_note: '' })
|
||||
const scenarioOptions = computed(() => scenarios.value.map(item => ({ value: item.id, label: item.name })))
|
||||
const drawerTitle = computed(() => editingId.value ? '编辑知识卡' : '新建知识卡')
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [cards, sceneData] = await Promise.all([api.get<{ items: KnowledgeCard[] }>('/knowledge-cards'), api.get<{ items: Scenario[] }>('/scenarios')])
|
||||
items.value = cards.items || []
|
||||
scenarios.value = sceneData.items || []
|
||||
} catch (error) {
|
||||
message.error(apiMessage(error))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
editingId.value = null
|
||||
Object.assign(form, { scenario_id: undefined, title: '', content: '', forbidden: '', risk_note: '' })
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
resetForm()
|
||||
open.value = true
|
||||
}
|
||||
|
||||
function openEdit(item: KnowledgeCard) {
|
||||
editingId.value = item.id
|
||||
Object.assign(form, {
|
||||
scenario_id: item.scenario_id, title: item.title, content: contentOf(item, 'standard_copy'),
|
||||
forbidden: contentOf(item, 'forbidden_copy'), risk_note: contentOf(item, 'risk_note'),
|
||||
})
|
||||
open.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
const content = { standard_copy: form.content, forbidden_copy: form.forbidden, risk_note: form.risk_note }
|
||||
try {
|
||||
if (editingId.value) await api.put(`/knowledge-cards/${editingId.value}`, { title: form.title, content })
|
||||
else await api.post('/knowledge-cards', { scenario_id: form.scenario_id, title: form.title, content })
|
||||
message.success(editingId.value ? '知识卡新版本已发布' : '知识卡已发布')
|
||||
open.value = false
|
||||
resetForm()
|
||||
await load()
|
||||
} catch (error) {
|
||||
message.error(apiMessage(error))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function showHistory(item: KnowledgeCard) {
|
||||
historyCard.value = item
|
||||
try {
|
||||
const data = await api.get<{ items: KnowledgeCardVersion[] }>(`/knowledge-cards/${item.id}/versions`)
|
||||
versions.value = data.items || []
|
||||
historyOpen.value = true
|
||||
} catch (error) { message.error(apiMessage(error)) }
|
||||
}
|
||||
|
||||
function remove(item: KnowledgeCard) {
|
||||
Modal.confirm({
|
||||
title: `归档“${item.title}”?`, content: '已发布或历史 SOP 版本引用的知识卡不能归档。', okText: '归档知识卡', okType: 'danger',
|
||||
async onOk() {
|
||||
try { await api.delete(`/knowledge-cards/${item.id}`); message.success('知识卡已归档'); await load() }
|
||||
catch (error) { message.error(apiMessage(error)) }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function contentOf(item: { content?: Record<string, any> }, key: string) { return String(item.content?.[key] || '') }
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<div class="page-heading"><div><h1>知识卡</h1><p>沉淀经过审核的标准表达、禁用语和风险提醒。</p></div><a-button v-if="auth.can('knowledge.edit')" type="primary" @click="openCreate"><PlusOutlined />新建知识卡</a-button></div>
|
||||
<a-spin :spinning="loading">
|
||||
<div v-if="items.length" class="knowledge-grid">
|
||||
<article v-for="item in items" :key="item.id" class="knowledge-card surface">
|
||||
<header><span class="card-icon"><BookOutlined /></span><div><a-tag color="green">已发布</a-tag><a-tag>V{{ item.version || 1 }}</a-tag></div></header>
|
||||
<h2>{{ item.title }}</h2>
|
||||
<p class="standard-copy">{{ contentOf(item, 'standard_copy') || '暂无标准话术' }}</p>
|
||||
<div v-if="contentOf(item, 'forbidden_copy')" class="copy-note forbidden"><b>禁用表达</b><span>{{ contentOf(item, 'forbidden_copy') }}</span></div>
|
||||
<div v-if="contentOf(item, 'risk_note')" class="copy-note risk"><b>风险提醒</b><span>{{ contentOf(item, 'risk_note') }}</span></div>
|
||||
<footer><span>{{ scenarios.find(scene => scene.id === item.scenario_id)?.name || '未分类场景' }}</span><div class="card-actions"><a-tooltip title="版本历史"><a-button type="text" aria-label="查看版本历史" @click="showHistory(item)"><HistoryOutlined /></a-button></a-tooltip><a-tooltip v-if="auth.can('knowledge.edit')" title="编辑知识卡"><a-button type="text" aria-label="编辑知识卡" @click="openEdit(item)"><EditOutlined /></a-button></a-tooltip><a-tooltip v-if="auth.can('knowledge.edit')" title="归档知识卡"><a-button type="text" danger aria-label="归档知识卡" @click="remove(item)"><DeleteOutlined /></a-button></a-tooltip></div></footer>
|
||||
</article>
|
||||
</div>
|
||||
<a-empty v-else description="还没有知识卡" />
|
||||
</a-spin>
|
||||
|
||||
<a-drawer v-if="auth.can('knowledge.edit')" v-model:open="open" :title="drawerTitle" width="520" @close="resetForm">
|
||||
<a-form layout="vertical" :model="form" @finish="save">
|
||||
<a-form-item label="所属场景" name="scenario_id" :rules="[{required:true,message:'请选择场景'}]"><a-select v-model:value="form.scenario_id" :disabled="Boolean(editingId)" :options="scenarioOptions" /></a-form-item>
|
||||
<a-form-item label="标题" name="title" :rules="[{required:true,message:'请输入标题'}]"><a-input v-model:value="form.title" /></a-form-item>
|
||||
<a-form-item label="标准话术" name="content" :rules="[{required:true,message:'请输入标准话术'}]"><a-textarea v-model:value="form.content" :rows="5" /></a-form-item>
|
||||
<a-form-item label="禁用表达"><a-textarea v-model:value="form.forbidden" :rows="3" /></a-form-item>
|
||||
<a-form-item label="风险提醒"><a-textarea v-model:value="form.risk_note" :rows="3" /></a-form-item>
|
||||
<a-button type="primary" html-type="submit" block :loading="saving">{{ editingId ? '发布新版本' : '发布知识卡' }}</a-button>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
|
||||
<a-drawer v-model:open="historyOpen" :title="`${historyCard?.title || '知识卡'} · 版本历史`" width="560">
|
||||
<div class="version-list">
|
||||
<section v-for="item in versions" :key="item.id" class="version-row">
|
||||
<div class="version-head"><b>V{{ item.version }}</b><a-tag :color="item.status === 'published' ? 'green' : 'default'">{{ item.status === 'published' ? '当前版本' : '历史版本' }}</a-tag><time>{{ new Date(item.created_at).toLocaleString('zh-CN') }}</time></div>
|
||||
<p>{{ contentOf(item, 'standard_copy') }}</p>
|
||||
<div v-if="contentOf(item, 'forbidden_copy')" class="history-note"><b>禁用表达</b>{{ contentOf(item, 'forbidden_copy') }}</div>
|
||||
<div v-if="contentOf(item, 'risk_note')" class="history-note"><b>风险提醒</b>{{ contentOf(item, 'risk_note') }}</div>
|
||||
</section>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.knowledge-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.knowledge-card{min-height:300px;padding:20px;display:flex;flex-direction:column}.knowledge-card header{display:flex;justify-content:space-between}.card-icon{width:36px;height:36px;display:grid;place-items:center;color:var(--green);background:#e8f3ef;border-radius:4px}.knowledge-card h2{margin:20px 0 10px;font-family:"Noto Serif SC",serif;font-size:19px}.standard-copy{margin:0;color:#4f5c56;line-height:1.7}.copy-note{display:grid;grid-template-columns:58px 1fr;gap:8px;margin-top:12px;padding:9px 11px;font-size:12px;border-left:2px solid}.copy-note b{font-size:11px}.copy-note.forbidden{color:#7a4d4d;background:#faf0f0;border-color:#ba7474}.copy-note.risk{color:#7f591c;background:#fbf3e5;border-color:#c68a37}.knowledge-card footer{display:flex;align-items:center;justify-content:space-between;margin-top:auto;padding-top:18px;color:var(--muted);font-size:12px}.card-actions{display:flex}.version-row{padding:18px 0;border-bottom:1px solid var(--line)}.version-row:first-child{padding-top:0}.version-head{display:flex;align-items:center;gap:8px}.version-head time{margin-left:auto;color:var(--muted);font-size:11px}.version-row>p{margin:12px 0;color:#45524c;line-height:1.7}.history-note{margin-top:7px;color:#6f7b76;font-size:12px}.history-note b{margin-right:8px;color:#39463f}@media(max-width:1050px){.knowledge-grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:650px){.knowledge-grid{grid-template-columns:1fr}.version-head{flex-wrap:wrap}.version-head time{width:100%;margin-left:0}}
|
||||
</style>
|
||||
@@ -35,7 +35,7 @@ async function submit() {
|
||||
<div class="identity-copy">
|
||||
<p class="eyebrow">SALES PRACTICE SYSTEM</p>
|
||||
<h1>把经验,变成<br />每天可执行的步骤。</h1>
|
||||
<p>场景配置、话术编排、版本发布与一线执行,在同一个工作台完成。</p>
|
||||
<p>场景契约、知识关系、话术编排与一线执行,在同一个工作台完成。</p>
|
||||
</div>
|
||||
<div class="signal-line"><span>01</span><b></b><span>结构化经验</span></div>
|
||||
</section>
|
||||
|
||||
@@ -20,8 +20,8 @@ onMounted(load)
|
||||
|
||||
<template>
|
||||
<div class="page-shell members-page">
|
||||
<div class="page-heading"><div><h1>企业成员</h1><p>分配 SOP 编辑、审核和一线执行职责,权限变更会立即生效。</p></div><a-button type="primary" @click="open=true"><PlusOutlined />添加成员</a-button></div>
|
||||
<section class="member-summary"><TeamOutlined /><div><b>{{ items.filter(item=>item.status==='active').length }}</b><span>在用账号</span></div><p>管理员拥有全部权限;编辑者维护场景与草稿;审核者负责发布;执行者只使用已发布 SOP 和自己的记录。</p></section>
|
||||
<div class="page-heading"><div><h1>企业成员</h1><p>分配场景维护、SOP 编辑和一线执行职责,权限变更会立即生效。</p></div><a-button type="primary" @click="open=true"><PlusOutlined />添加成员</a-button></div>
|
||||
<section class="member-summary"><TeamOutlined /><div><b>{{ items.filter(item=>item.status==='active').length }}</b><span>在用账号</span></div><p>管理员拥有全部权限;维护者配置场景契约、知识和 SOP;执行者使用可用 SOP 和自己的记录。</p></section>
|
||||
<section class="surface"><a-table row-key="id" :loading="loading" :data-source="items" :columns="columns" :pagination="false"><template #bodyCell="{column,record}"><template v-if="column.key==='member'"><div class="member-cell"><span>{{ record.display_name.slice(0,1) }}</span><div><b>{{ record.display_name }}</b><small>{{ record.username }}</small></div></div></template><template v-else-if="column.dataIndex==='role_name'"><a-tag>{{ record.role_name }}</a-tag></template><template v-else-if="column.dataIndex==='status'"><a-tag :color="record.status==='active'?'green':'default'">{{ record.status==='active'?'使用中':'已停用' }}</a-tag></template><template v-else-if="column.key==='action'"><a-button v-if="record.user_id!==auth.user?.user_id" type="text" aria-label="编辑成员权限" @click="edit(record)"><EditOutlined /></a-button></template></template></a-table></section>
|
||||
<a-drawer v-model:open="open" title="添加企业成员" width="480"><a-form layout="vertical" :model="form" @finish="create"><div class="form-grid"><a-form-item label="登录用户名" name="username" :rules="[{required:true,message:'请输入用户名'}]"><a-input v-model:value="form.username" /></a-form-item><a-form-item label="显示名称" name="display_name" :rules="[{required:true,message:'请输入显示名称'}]"><a-input v-model:value="form.display_name" /></a-form-item></div><a-form-item label="初始密码" name="password" :rules="[{required:true,min:6,message:'密码至少 6 位'}]"><a-input-password v-model:value="form.password" /></a-form-item><a-form-item label="角色" name="role_code" :rules="[{required:true}]"><a-select v-model:value="form.role_code" :options="roleOptions" /></a-form-item><a-button type="primary" html-type="submit" block :loading="saving">创建成员</a-button></a-form></a-drawer>
|
||||
<a-modal :open="Boolean(editing)" title="调整成员权限" ok-text="保存" :confirm-loading="saving" @ok="update" @cancel="editing=null"><div v-if="editing" class="editing-name"><b>{{ editing.display_name }}</b><span>{{ editing.username }}</span></div><a-form layout="vertical"><a-form-item label="角色"><a-select v-model:value="editForm.role_code" :options="roleOptions" /></a-form-item><a-form-item label="账号状态"><a-segmented v-model:value="editForm.status" :options="[{value:'active',label:'使用中'},{value:'disabled',label:'停用'}]" /></a-form-item></a-form></a-modal>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { CheckOutlined, ClockCircleOutlined, EyeOutlined, StopOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { api, apiMessage } from '@/api/client'
|
||||
import type { ReviewItem } from '@/types'
|
||||
|
||||
const router=useRouter();const loading=ref(false);const items=ref<ReviewItem[]>([]);const rejecting=ref<ReviewItem|null>(null);const submitting=ref(false);const rejectForm=reactive({reason:''})
|
||||
const columns=[{title:'SOP / 场景',key:'sop'},{title:'版本',dataIndex:'version',width:90},{title:'提交人',dataIndex:'creator_name',width:140},{title:'等待时间',dataIndex:'submitted_at',width:180},{title:'',key:'action',width:250}]
|
||||
async function load(){loading.value=true;try{const data=await api.get<{items:ReviewItem[]}>('/reviews');items.value=data.items||[]}catch(error){message.error(apiMessage(error))}finally{loading.value=false}}
|
||||
async function approve(item:ReviewItem){submitting.value=true;try{await api.post(`/sops/${item.sop_id}/publish`);message.success(`“${item.sop_name}”已通过并发布`);await load()}catch(error){message.error(apiMessage(error))}finally{submitting.value=false}}
|
||||
function openReject(item:ReviewItem){rejecting.value=item;rejectForm.reason=''}
|
||||
async function reject(){if(!rejecting.value||!rejectForm.reason.trim())return message.warning('请填写退回原因');submitting.value=true;try{await api.post(`/sops/${rejecting.value.sop_id}/reject`,{reason:rejectForm.reason});message.success('已退回编辑者修改');rejecting.value=null;await load()}catch(error){message.error(apiMessage(error))}finally{submitting.value=false}}
|
||||
function waiting(value:string){const minutes=Math.max(1,Math.round((Date.now()-new Date(value).getTime())/60000));if(minutes<60)return`${minutes} 分钟`;const hours=Math.round(minutes/60);return hours<48?`${hours} 小时`:`${Math.round(hours/24)} 天`}
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-shell review-page">
|
||||
<div class="page-heading"><div><h1>审核中心</h1><p>检查流程完整性、风险分支和话术口径后再决定发布。</p></div><div class="queue-count"><ClockCircleOutlined /><b>{{ items.length }}</b><span>待审核</span></div></div>
|
||||
<section class="surface review-table">
|
||||
<div class="review-guideline"><span>审核关注</span><b>字段完整</b><i></i><b>条件可达</b><i></i><b>高风险有出口</b><i></i><b>知识卡已发布</b></div>
|
||||
<a-table row-key="version_id" :loading="loading" :data-source="items" :columns="columns" :pagination="false" :scroll="{x:850}">
|
||||
<template #bodyCell="{column,record}"><template v-if="column.key==='sop'"><div class="sop-cell"><b>{{ record.sop_name }}</b><span>{{ record.scenario_name }} · {{ record.description||'暂无说明' }}</span></div></template><template v-else-if="column.dataIndex==='version'"><a-tag>V{{ record.version }}</a-tag></template><template v-else-if="column.dataIndex==='submitted_at'"><span class="waiting">{{ waiting(record.submitted_at) }}</span></template><template v-else-if="column.key==='action'"><div class="row-actions"><a-button @click="router.push(`/sops/${record.sop_id}`)"><EyeOutlined />查看</a-button><a-button danger @click="openReject(record)"><StopOutlined />退回</a-button><a-button type="primary" :loading="submitting" @click="approve(record)"><CheckOutlined />通过</a-button></div></template></template>
|
||||
<template #emptyText><div class="review-empty"><CheckOutlined /><b>审核队列已清空</b><span>新提交的 SOP 会出现在这里。</span></div></template>
|
||||
</a-table>
|
||||
</section>
|
||||
<a-modal :open="Boolean(rejecting)" title="退回修改" ok-text="确认退回" :confirm-loading="submitting" @ok="reject" @cancel="rejecting=null"><p class="modal-copy">请说明“{{ rejecting?.sop_name }}”需要调整的具体内容,编辑者可据此修改后重新提交。</p><a-textarea v-model:value="rejectForm.reason" :rows="5" :maxlength="1000" show-count placeholder="例如:急症判断缺少默认路径,请补充非急症分支。" /></a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.queue-count{height:54px;display:grid;grid-template-columns:24px 32px auto;align-items:center;gap:8px;padding:0 16px;color:#3f4c46;background:#fff;border:1px solid var(--line);border-radius:6px}.queue-count>span:first-child{color:var(--amber)}.queue-count b{font-family:"Noto Serif SC",serif;font-size:24px}.queue-count span:last-child{color:var(--muted);font-size:12px}.review-table{overflow:hidden}.review-guideline{min-height:48px;display:flex;align-items:center;gap:12px;padding:0 16px;color:#637069;background:#f7f9f8;border-bottom:1px solid var(--line);font-size:11px}.review-guideline>span{font-weight:750;color:#28332e}.review-guideline i{width:3px;height:3px;border-radius:50%;background:#a7b2ad}.sop-cell{display:flex;flex-direction:column;max-width:520px}.sop-cell b{font-size:14px}.sop-cell span{margin-top:5px;color:var(--muted);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.waiting{color:#8a641e}.row-actions{display:flex;justify-content:flex-end;gap:7px}.review-empty{min-height:220px;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#9aa59f}.review-empty>span:first-child{font-size:34px;color:#4eaa88}.review-empty b{margin-top:12px;color:#415049}.review-empty span:last-child{margin-top:5px;font-size:12px}.modal-copy{margin:0 0 14px;color:var(--muted);line-height:1.7}@media(max-width:650px){.review-guideline{display:none}.queue-count{display:none}}
|
||||
</style>
|
||||
@@ -11,9 +11,10 @@ interface DetailResponse{run:RunDetail;events:RunEvent[];feedback:RunFeedback[];
|
||||
const route=useRoute();const router=useRouter();const auth=useAuthStore();const loading=ref(true);const detail=ref<DetailResponse|null>(null);const submitting=ref(false);const feedback=reactive({score:5,comment:''})
|
||||
const fieldMap=computed(()=>new Map((detail.value?.fields||[]).map(field=>[field.field_key,field.field_name])))
|
||||
const answers=computed(()=>Object.entries(detail.value?.run.answers||{}))
|
||||
const finalResult=computed(()=>detail.value?.run.final_result||null)
|
||||
const canFeedback=computed(()=>auth.can('runs.feedback')&&detail.value?.run.status==='completed'&&!detail.value.feedback.some(item=>item.user_id===auth.user?.user_id))
|
||||
function resultText(v:string){return{finish:'正常结束',escalate:'转人工 / 转诊',manual:'人工结束'}[v]||v||'进行中'}
|
||||
function actionText(v:string){return{start:'开始执行',enter:'进入节点',answer:'提交回答',finish:'人工结束'}[v]||v}
|
||||
function actionText(v:string){return{start:'开始执行',enter:'进入节点',answer:'提交回答',finish:'结束执行',final_result:'保存最终结果'}[v]||v}
|
||||
function valueText(value:any){if(value===true)return'是';if(value===false)return'否';if(Array.isArray(value))return value.join('、');if(value===null||value===undefined||value==='')return'-';return String(value)}
|
||||
function answerEntries(event:RunEvent){const values=event.payload?.answers||{};return Object.entries(values)}
|
||||
async function load(){loading.value=true;try{detail.value=await api.get<DetailResponse>(`/runs/${route.params.id}/detail`)}catch(error){message.error(apiMessage(error));router.replace('/runs')}finally{loading.value=false}}
|
||||
@@ -27,7 +28,7 @@ onMounted(load)
|
||||
<a-spin :spinning="loading">
|
||||
<template v-if="detail">
|
||||
<header class="detail-heading">
|
||||
<div><div class="run-code">RUN-{{ String(detail.run.id).padStart(5,'0') }}</div><h1>{{ detail.run.sop_name }}</h1><p>{{ detail.run.scenario_name }} · V{{ detail.run.version }} · {{ detail.run.operator_name }}</p></div>
|
||||
<div><div class="run-code">RUN-{{ String(detail.run.id).padStart(5,'0') }}</div><h1>{{ detail.run.sop_name }}</h1><p>{{ detail.run.scenario_name }} · {{ detail.run.operator_name }}</p></div>
|
||||
<div class="result-block" :class="detail.run.result==='escalate'?'warning':'success'"><SafetyCertificateOutlined v-if="detail.run.result==='escalate'"/><CheckCircleOutlined v-else/><span><small>{{ detail.run.status==='completed'?'执行结果':'当前状态' }}</small><b>{{ resultText(detail.run.result) }}</b></span></div>
|
||||
</header>
|
||||
|
||||
@@ -38,7 +39,8 @@ onMounted(load)
|
||||
<a-timeline-item v-for="event in detail.events" :key="event.id" :color="event.action==='finish'||event.node_type==='escalate'?'red':event.action==='answer'?'blue':'green'">
|
||||
<div class="event-head"><b>{{ event.node_title||event.node_key }}</b><a-tag>{{ actionText(event.action) }}</a-tag><time>{{ new Date(event.created_at).toLocaleString('zh-CN') }}</time></div>
|
||||
<p v-if="event.action==='enter'&&event.node_content">{{ event.node_content }}</p>
|
||||
<div v-if="event.action==='enter'&&event.knowledge" class="event-knowledge"><div><b>{{ event.knowledge.title }}</b><small>V{{ event.knowledge.version }}</small></div><p>{{ event.knowledge.standard_copy }}</p><p v-if="event.knowledge.risk_note" class="risk-note">{{ event.knowledge.risk_note }}</p></div>
|
||||
<div v-if="event.action==='enter'&&event.knowledge" class="event-knowledge"><div><b>{{ event.knowledge.title }}</b><small>历史知识</small></div><p>{{ event.knowledge.standard_copy }}</p><p v-if="event.knowledge.risk_note" class="risk-note">{{ event.knowledge.risk_note }}</p></div>
|
||||
<div v-for="group in event.outputs||[]" :key="group.key" class="event-knowledge"><div><b>{{ group.name }}</b><small>{{ group.type }}</small></div><div v-for="(items,relation) in group.relations" :key="relation"><p><b>{{ relation }}</b></p><p v-for="item in items" :key="item.key">{{ item.content.template || item.content.standard_copy || item.name }}</p></div></div>
|
||||
<div v-if="answerEntries(event).length" class="event-answers"><span v-for="[key,value] in answerEntries(event)" :key="key"><small>{{ fieldMap.get(key)||key }}</small><b>{{ valueText(value) }}</b></span></div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
@@ -47,6 +49,7 @@ onMounted(load)
|
||||
<aside class="detail-aside">
|
||||
<section class="surface summary-panel"><div class="section-title"><span>执行信息</span></div><dl><div><dt>状态</dt><dd><a-tag :color="detail.run.status==='completed'?'green':'orange'">{{ detail.run.status==='completed'?'已完成':'进行中' }}</a-tag></dd></div><div><dt>开始时间</dt><dd>{{ new Date(detail.run.started_at).toLocaleString('zh-CN') }}</dd></div><div><dt>完成时间</dt><dd>{{ detail.run.completed_at?new Date(detail.run.completed_at).toLocaleString('zh-CN'):'-' }}</dd></div><div><dt>当前节点</dt><dd>{{ detail.run.current_node_key }}</dd></div></dl></section>
|
||||
<section class="surface answer-panel"><div class="section-title"><span>已采集信息</span><small>{{ answers.length }} 项</small></div><div v-if="answers.length" class="answer-list"><div v-for="[key,value] in answers" :key="key"><span>{{ fieldMap.get(key)||key }}</span><b>{{ valueText(value) }}</b></div></div><div v-else class="aside-empty">尚未采集字段</div></section>
|
||||
<section class="surface answer-panel"><div class="section-title"><span>最终结果</span></div><pre v-if="finalResult" class="final-result">{{ JSON.stringify(finalResult,null,2) }}</pre><div v-else class="aside-empty">本次执行未收集最终结果</div></section>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
@@ -64,4 +67,5 @@ onMounted(load)
|
||||
<style scoped>
|
||||
.run-detail-page{max-width:1320px}.back-link{height:auto;margin:0 0 14px;padding:0}.detail-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:20px}.run-code{color:var(--green);font-family:monospace;font-size:12px;font-weight:750}.detail-heading h1{margin:7px 0 5px;font-family:"Noto Serif SC",serif;font-size:28px}.detail-heading p{margin:0;color:var(--muted)}.result-block{min-width:210px;display:flex;align-items:center;gap:12px;padding:15px 17px;border-left:3px solid}.result-block>span:first-child{font-size:25px}.result-block>span:last-child{display:flex;flex-direction:column}.result-block small{font-size:10px}.result-block b{margin-top:3px}.result-block.success{color:#17664f;background:#eaf4f0;border-color:#25866a}.result-block.warning{color:#8d5b16;background:#fbf2e3;border-color:#c08026}.detail-layout{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:16px}.timeline-panel{min-height:560px;padding:22px 24px}.section-title{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:22px}.section-title span{font-weight:700}.section-title small{color:var(--muted);font-size:11px}.run-timeline{padding-top:5px}.event-head{display:flex;align-items:center;gap:9px;min-height:27px}.event-head time{margin-left:auto;color:#8b9691;font-size:11px}.run-timeline p{margin:8px 0 0;color:#56635d;line-height:1.65}.event-answers{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px;margin-top:10px}.event-answers span{display:flex;justify-content:space-between;gap:10px;padding:8px 10px;background:#f4f7f5;border-left:2px solid #a6cbbd}.event-answers small{color:var(--muted)}.event-answers b{font-size:12px}.detail-aside{display:flex;flex-direction:column;gap:16px}.summary-panel,.answer-panel{padding:20px}.summary-panel dl{margin:0}.summary-panel dl>div{display:flex;justify-content:space-between;gap:15px;padding:10px 0;border-bottom:1px solid #edf0ee}.summary-panel dl>div:last-child{border-bottom:0}.summary-panel dt{color:var(--muted);font-size:12px}.summary-panel dd{margin:0;text-align:right;font-size:12px}.answer-list>div{display:flex;flex-direction:column;padding:10px 0;border-bottom:1px solid #edf0ee}.answer-list>div:last-child{border-bottom:0}.answer-list span{color:var(--muted);font-size:11px}.answer-list b{margin-top:4px;font-size:13px}.aside-empty{padding:24px 0;color:#929d98;text-align:center;font-size:12px}.feedback-band{margin-top:16px;padding:22px 24px}.feedback-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.feedback-list article{padding:14px 0;border-top:1px solid var(--line)}.feedback-list article>div{display:flex;align-items:center;justify-content:space-between}.feedback-list :deep(.ant-rate){font-size:15px}.feedback-list p{margin:9px 0;color:#54615b}.feedback-list time{color:#929d98;font-size:11px}.feedback-form{display:grid;grid-template-columns:220px minmax(0,1fr) auto;align-items:center;gap:14px;padding-top:16px;border-top:1px solid var(--line)}.feedback-form>div{display:flex;flex-direction:column;gap:6px}.feedback-form>div>span{font-size:12px;font-weight:650}.feedback-form :deep(.ant-rate){font-size:20px}@media(max-width:900px){.detail-layout{grid-template-columns:1fr}.detail-aside{display:grid;grid-template-columns:1fr 1fr}.feedback-form{grid-template-columns:1fr}.feedback-list{grid-template-columns:1fr}}@media(max-width:620px){.detail-heading{align-items:flex-start;flex-direction:column}.result-block{width:100%}.detail-aside{grid-template-columns:1fr}.timeline-panel{padding:18px}.event-head{align-items:flex-start;flex-wrap:wrap}.event-head time{width:100%;margin:0}.event-answers{grid-template-columns:1fr}}
|
||||
.event-knowledge{margin-top:10px;padding:12px 14px;background:#f2f7f5;border-left:3px solid var(--green)}.event-knowledge>div{display:flex;justify-content:space-between;gap:12px}.event-knowledge small{color:var(--muted)}.event-knowledge .risk-note{padding-top:8px;border-top:1px solid #dce9e4;color:#8d5b16}
|
||||
.final-result{max-height:280px;overflow:auto;margin:0;padding:12px;color:#33413b;background:#f4f7f5;font:12px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;overflow-wrap:anywhere}
|
||||
</style>
|
||||
|
||||
@@ -38,7 +38,7 @@ onMounted(()=>{load();loadOptions()})
|
||||
<a-button type="text" aria-label="刷新执行记录" @click="load()"><ReloadOutlined /></a-button>
|
||||
</div>
|
||||
<a-table row-key="id" :loading="loading" :data-source="items" :columns="columns" :pagination="{current:page,pageSize,total,showSizeChanger:true,showTotal:(value:number)=>`共 ${value} 条`}" :scroll="{x:980}" :custom-row="rowProps" @change="tableChange">
|
||||
<template #bodyCell="{column,record}"><template v-if="column.dataIndex==='id'"><code>RUN-{{ String(record.id).padStart(5,'0') }}</code></template><template v-else-if="column.key==='sop'"><div class="run-sop"><b>{{ record.sop_name }}</b><small>{{ record.scenario_name }} · V{{ record.version }}</small></div></template><template v-else-if="column.dataIndex==='status'"><a-tag :color="record.status==='completed'?'green':'orange'">{{ statusText(record.status) }}</a-tag></template><template v-else-if="column.dataIndex==='result'">{{ resultText(record.result) }}</template><template v-else-if="column.dataIndex==='started_at'||column.dataIndex==='completed_at'">{{ record[column.dataIndex]?new Date(record[column.dataIndex]).toLocaleString('zh-CN'):'-' }}</template><template v-else-if="column.key==='action'"><a-tooltip v-if="canContinue(record)" title="继续执行"><a-button type="text" aria-label="继续执行" @click.stop="continueRun(record)"><PlayCircleOutlined /></a-button></a-tooltip><RightOutlined v-else class="row-arrow" /></template></template>
|
||||
<template #bodyCell="{column,record}"><template v-if="column.dataIndex==='id'"><code>RUN-{{ String(record.id).padStart(5,'0') }}</code></template><template v-else-if="column.key==='sop'"><div class="run-sop"><b>{{ record.sop_name }}</b><small>{{ record.scenario_name }}</small></div></template><template v-else-if="column.dataIndex==='status'"><a-tag :color="record.status==='completed'?'green':'orange'">{{ statusText(record.status) }}</a-tag></template><template v-else-if="column.dataIndex==='result'">{{ resultText(record.result) }}</template><template v-else-if="column.dataIndex==='started_at'||column.dataIndex==='completed_at'">{{ record[column.dataIndex]?new Date(record[column.dataIndex]).toLocaleString('zh-CN'):'-' }}</template><template v-else-if="column.key==='action'"><a-tooltip v-if="canContinue(record)" title="继续执行"><a-button type="text" aria-label="继续执行" @click.stop="continueRun(record)"><PlayCircleOutlined /></a-button></a-tooltip><RightOutlined v-else class="row-arrow" /></template></template>
|
||||
<template #emptyText><div class="empty-copy">当前筛选条件下没有执行记录。</div></template>
|
||||
</a-table>
|
||||
</section>
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowDownOutlined, ArrowLeftOutlined, ArrowUpOutlined, DeleteOutlined, HistoryOutlined, PlusOutlined, RollbackOutlined, SaveOutlined, SendOutlined } from '@ant-design/icons-vue'
|
||||
import { ArrowDownOutlined, ArrowLeftOutlined, ArrowUpOutlined, DeleteOutlined, PlusOutlined, SaveOutlined } from '@ant-design/icons-vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { api, apiMessage } from '@/api/client'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import type { ScenarioField, SOP, SOPEdge, SOPNode, SOPVersion } from '@/types'
|
||||
|
||||
interface KnowledgeCard { id:number; scenario_id:number; title:string }
|
||||
import type { ScenarioField, SOP, SOPEdge, SOPNode } from '@/types'
|
||||
|
||||
const route = useRoute(); const router = useRouter(); const id = Number(route.params.id)
|
||||
const auth = useAuthStore()
|
||||
const loading = ref(true); const saving = ref(false)
|
||||
const historyOpen = ref(false); const rollingBack = ref(false)
|
||||
const sop = ref<SOP | null>(null); const version = ref<SOPVersion | null>(null)
|
||||
const versions = ref<SOPVersion[]>([])
|
||||
const sop = ref<SOP | null>(null); const graph = ref<{start_node_key:string} | null>(null)
|
||||
const nodes = ref<SOPNode[]>([]); const edges = ref<SOPEdge[]>([]); const selectedKey = ref('')
|
||||
const fields = ref<ScenarioField[]>([]); const knowledgeCards = ref<KnowledgeCard[]>([])
|
||||
const fields = ref<ScenarioField[]>([])
|
||||
const selected = computed(() => nodes.value.find(n => n.node_key === selectedKey.value))
|
||||
const editable = computed(() => version.value?.status === 'draft')
|
||||
const canEdit = computed(() => editable.value&&auth.can('sop.edit'))
|
||||
const reviewing = computed(() => version.value?.status === 'reviewing')
|
||||
const isLatestVersion = computed(() => version.value?.version === Math.max(0, ...versions.value.map(item => item.version)))
|
||||
const hasPendingVersion = computed(() => versions.value.some(item => ['draft','reviewing'].includes(item.status)))
|
||||
const startImageFieldOptions = computed(() => {
|
||||
const keys = selected.value?.config?.presentation?.item_field_keys || []
|
||||
return fields.value.filter(field=>keys.includes(field.field_key)).map(field=>({value:field.field_key,label:`${field.field_name} (${field.field_key})`}))
|
||||
})
|
||||
const formRequiredFieldOptions = computed(() => {
|
||||
const keys = selected.value?.config?.field_keys || []
|
||||
return fields.value.filter(field=>keys.includes(field.field_key)).map(field=>({value:field.field_key,label:`${field.field_name} (${field.field_key})`}))
|
||||
})
|
||||
const canEdit = computed(() => auth.can('sop.edit'))
|
||||
const nodeTypes = [{value:'message',label:'标准话术'},{value:'question',label:'单项提问'},{value:'form',label:'信息表单'},{value:'choice',label:'选择判断'},{value:'condition',label:'条件节点'},{value:'knowledge',label:'知识卡'},{value:'escalate',label:'转人工/转诊'},{value:'finish',label:'结束'}]
|
||||
interface ConditionRuleDraft { field:string; operator:string; value:string }
|
||||
const newConditionRule = ():ConditionRuleDraft => ({ field:'', operator:'equals', value:'' })
|
||||
@@ -31,15 +31,10 @@ const edgeDraft = reactive({ target_node_key: '', conditional: false, mode: 'all
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const requestedVersion = Number(route.query.version || 0)
|
||||
const data = await api.get<{ sop:SOP; version:SOPVersion; nodes:SOPNode[]; edges:SOPEdge[] }>(`/sops/${id}`, { params: requestedVersion ? { version: requestedVersion } : {} })
|
||||
sop.value=data.sop; version.value=data.version; nodes.value=data.nodes.map(n=>({...n,config:n.config||{}})); edges.value=data.edges.map(e=>({...e,condition:e.condition||{}})); selectedKey.value = nodes.value[0]?.node_key || ''
|
||||
const [scenarioData, cardData, versionData] = await Promise.all([
|
||||
api.get<{ fields:ScenarioField[] }>(`/scenarios/${data.sop.scenario_id}`),
|
||||
api.get<{ items:KnowledgeCard[] }>('/knowledge-cards'),
|
||||
api.get<{ items:SOPVersion[] }>(`/sops/${id}/versions`),
|
||||
])
|
||||
fields.value=scenarioData.fields; knowledgeCards.value=(cardData.items||[]).filter(card=>card.scenario_id===data.sop.scenario_id); versions.value=versionData.items||[]
|
||||
const data = await api.get<{ sop:SOP; edit:{start_node_key:string}; nodes:SOPNode[]; edges:SOPEdge[] }>(`/sops/${id}`)
|
||||
sop.value=data.sop; graph.value=data.edit; nodes.value=data.nodes.map(n=>({...n,config:{...(n.config||{}),...(n.type==='knowledge'?{knowledge_selector:n.config?.knowledge_selector||{derived_field:'',knowledge_types:[],knowledge_keys:[],relation_types:[]}}:{}),...(n.type==='start'?{presentation:n.config?.presentation||{summary_field_keys:[],item_field_keys:[],image_field_keys:[],opening_title:'开场话术',opening_template:''}}:{}),...(n.type==='form'?{required_field_keys:n.config?.required_field_keys||[]}:{})}})); edges.value=data.edges.map(e=>({...e,condition:e.condition||{}})); selectedKey.value = nodes.value[0]?.node_key || ''
|
||||
const scenarioData = await api.get<{ fields:ScenarioField[] }>(`/scenarios/${data.sop.scenario_id}`)
|
||||
fields.value=scenarioData.fields
|
||||
} catch(error) { message.error(apiMessage(error)) } finally { loading.value=false }
|
||||
}
|
||||
function addNode() {
|
||||
@@ -70,43 +65,46 @@ function removeEdge(edge:SOPEdge){edges.value=edges.value.filter(item=>item!==ed
|
||||
function addConditionRule(){edgeDraft.rules.push(newConditionRule())}
|
||||
function removeConditionRule(index:number){if(edgeDraft.rules.length>1)edgeDraft.rules.splice(index,1)}
|
||||
function parseValue(value:string){if(value==='true')return true;if(value==='false')return false;if(value!==''&&!Number.isNaN(Number(value)))return Number(value);return value}
|
||||
function knowledgeCardChanged(){if(selected.value)delete selected.value.config.knowledge_card_version_id}
|
||||
function csvValues(value:unknown){return Array.isArray(value)?value.join(', '):''}
|
||||
function setSelectorList(key:string,value:string){if(!selected.value)return;selected.value.config.knowledge_selector ||= {};selected.value.config.knowledge_selector[key]=value.split(',').map(item=>item.trim()).filter(Boolean)}
|
||||
function conditionText(condition:Record<string,any>):string{if(!condition||Object.keys(condition).length===0)return '默认路径';if(Array.isArray(condition.any))return `任一:${condition.any.map(conditionText).join(';')}`;if(Array.isArray(condition.all))return `全部:${condition.all.map(conditionText).join(';')}`;if(!condition.field)return '未识别条件';const names:Record<string,string>={equals:'等于',not_equals:'不等于',contains:'包含',greater_than:'大于',less_than:'小于',in:'属于列表',exists:'已填写',not_exists:'未填写'};const fieldName=fields.value.find(field=>field.field_key===condition.field)?.field_name||condition.field;const value=Array.isArray(condition.value)?condition.value.join('、'):(condition.value??'');return `${fieldName} ${names[condition.operator]||condition.operator} ${value}`}
|
||||
async function save():Promise<boolean>{saving.value=true;try{await api.put(`/sops/${id}/draft`,{start_node_key:version.value?.start_node_key||'start',nodes:nodes.value.map(({node_key,type,title,content,config,position_x,position_y})=>({node_key,type,title,content,config,position_x,position_y})),edges:edges.value.map(({source_node_key,target_node_key,condition,priority})=>({source_node_key,target_node_key,condition,priority}))});message.success('草稿已保存');await load();return true}catch(error){message.error(apiMessage(error));return false}finally{saving.value=false}}
|
||||
async function validate(){try{const suffix=route.query.version?`?version=${encodeURIComponent(String(route.query.version))}`:'';const data=await api.post<{valid:boolean;problems:string[]}>(`/sops/${id}/validate${suffix}`);data.valid?message.success('流程校验通过'):Modal.warning({title:'流程还不能发布',content:data.problems.join(';')})}catch(error){message.error(apiMessage(error))}}
|
||||
async function submitReview(){try{if(!(await save()))return;await api.post(`/sops/${id}/submit-review`);message.success('SOP 已提交审核');await load()}catch(error){message.error(apiMessage(error))}}
|
||||
async function publish(){try{if(canEdit.value&&!(await save()))return;await api.post(`/sops/${id}/publish`);message.success('SOP 已发布');await load()}catch(error){message.error(apiMessage(error))}}
|
||||
async function offline(){try{await api.post(`/sops/${id}/offline`);message.success('SOP 已下线');await load()}catch(error){message.error(apiMessage(error))}}
|
||||
async function createVersion(){try{await api.post(`/sops/${id}/versions`);await router.replace({query:{}});message.success('已创建新草稿版本');await load()}catch(error){message.error(apiMessage(error))}}
|
||||
async function viewVersion(value:number){await router.replace({query:{version:String(value)}});historyOpen.value=false;await load()}
|
||||
function rollbackVersion(item:SOPVersion){Modal.confirm({title:`回滚到 V${item.version}?`,content:'系统会复制该历史版本并生成一个新的已发布版本,当前已发布版本将保留为历史记录。',okText:'确认回滚',async onOk(){rollingBack.value=true;try{const data=await api.post<{version:number}>(`/sops/${id}/rollback`,{version:item.version});await router.replace({query:{}});historyOpen.value=false;message.success(`已回滚并发布为 V${data.version}`);await load()}catch(error){message.error(apiMessage(error))}finally{rollingBack.value=false}}})}
|
||||
function versionStatus(value:string){return({draft:'草稿',reviewing:'审核中',published:'已发布',superseded:'历史版本',offline:'已下线'} as Record<string,string>)[value]||value}
|
||||
async function save():Promise<boolean>{saving.value=true;try{await api.put(`/sops/${id}/graph`,{start_node_key:graph.value?.start_node_key||'start',nodes:nodes.value.map(({node_key,type,title,content,config,position_x,position_y})=>({node_key,type,title,content,config,position_x,position_y})),edges:edges.value.map(({source_node_key,target_node_key,condition,priority})=>({source_node_key,target_node_key,condition,priority}))});message.success('SOP 已保存并生效');await load();return true}catch(error){message.error(apiMessage(error));return false}finally{saving.value=false}}
|
||||
async function validate(){try{const data=await api.post<{valid:boolean;problems:string[]}>(`/sops/${id}/validate`);data.valid?message.success('流程校验通过'):Modal.warning({title:'流程配置不完整',content:data.problems.join(';')})}catch(error){message.error(apiMessage(error))}}
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="editor-page">
|
||||
<header class="editor-header">
|
||||
<div class="editor-title"><a-button type="text" aria-label="返回场景" @click="router.push(`/scenarios/${sop?.scenario_id}`)"><ArrowLeftOutlined /></a-button><div><b>{{ sop?.name || 'SOP 编辑器' }}</b><span>版本 {{ version?.version }} · {{ versionStatus(version?.status || '') }}</span></div></div>
|
||||
<div class="page-actions"><a-button @click="historyOpen=true"><HistoryOutlined />版本历史</a-button><a-button @click="validate">校验流程</a-button><a-button v-if="canEdit" :loading="saving" @click="save"><SaveOutlined />保存</a-button><a-button v-if="canEdit&&auth.can('sop.submit_review')" @click="submitReview"><SendOutlined />提交审核</a-button><a-button v-if="auth.can('sop.publish')&&(reviewing||(editable&&auth.can('*')))" type="primary" @click="publish"><SendOutlined />发布</a-button><a-button v-if="version?.status === 'published'&&auth.can('sop.publish')" danger @click="offline">下线</a-button><a-button v-if="auth.can('sop.edit')&&isLatestVersion&&!hasPendingVersion&&!editable&&version?.status !== 'reviewing'" type="primary" @click="createVersion"><PlusOutlined />创建新版本</a-button></div>
|
||||
<div class="editor-title"><a-button type="text" aria-label="返回场景" @click="router.push(`/scenarios/${sop?.scenario_id}`)"><ArrowLeftOutlined /></a-button><div><b>{{ sop?.name || 'SOP 编辑器' }}</b><span>流程配置</span></div></div>
|
||||
<div class="page-actions"><a-button @click="validate">校验流程</a-button><a-button v-if="canEdit" type="primary" :loading="saving" @click="save"><SaveOutlined />保存并生效</a-button></div>
|
||||
</header>
|
||||
<a-spin :spinning="loading">
|
||||
<main class="editor-workbench">
|
||||
<aside class="node-rail surface">
|
||||
<div class="rail-title"><span>流程节点</span><a-button v-if="canEdit" type="text" aria-label="添加节点" @click="addNode"><PlusOutlined /></a-button></div>
|
||||
<div class="node-list">
|
||||
<button v-for="(node,index) in nodes" :key="node.node_key" :class="{active:selectedKey===node.node_key}" @click="selectedKey=node.node_key"><span class="node-index">{{ String(index+1).padStart(2,'0') }}</span><span class="node-meta"><b>{{ node.title }}</b><small>{{ nodeTypes.find(t=>t.value===node.type)?.label || node.type }}</small></span><span v-if="canEdit&&node.type!=='start'" class="node-move"><ArrowUpOutlined @click.stop="move(index,-1)"/><ArrowDownOutlined @click.stop="move(index,1)"/></span></button>
|
||||
<div v-for="(node,index) in nodes" :key="node.node_key" class="node-list-item" :class="{active:selectedKey===node.node_key}" role="button" tabindex="0" @click="selectedKey=node.node_key" @keydown.enter="selectedKey=node.node_key"><span class="node-index">{{ String(index+1).padStart(2,'0') }}</span><span class="node-meta"><b>{{ node.title }}</b><small>{{ nodeTypes.find(t=>t.value===node.type)?.label || node.type }}</small></span><span v-if="node.type!=='start'" class="node-actions"><a-tooltip :title="canEdit ? '上移节点' : '无编辑权限' "><a-button type="text" size="small" aria-label="上移节点" :disabled="!canEdit" @click.stop="move(index,-1)"><ArrowUpOutlined /></a-button></a-tooltip><a-tooltip :title="canEdit ? '下移节点' : '无编辑权限' "><a-button type="text" size="small" aria-label="下移节点" :disabled="!canEdit" @click.stop="move(index,1)"><ArrowDownOutlined /></a-button></a-tooltip><a-tooltip :title="canEdit ? '删除节点' : '无编辑权限' "><a-button type="text" size="small" danger aria-label="删除节点" :disabled="!canEdit" @click.stop="removeNode(node)"><DeleteOutlined /></a-button></a-tooltip></span></div>
|
||||
</div>
|
||||
</aside>
|
||||
<section v-if="selected" class="node-editor surface">
|
||||
<div class="node-editor-head"><div><span class="node-type-mark">{{ selected.type.toUpperCase() }}</span><h2>{{ selected.title }}</h2></div><a-button v-if="canEdit&&selected.type!=='start'" danger type="text" @click="removeNode(selected)"><DeleteOutlined />删除</a-button></div>
|
||||
<div class="node-editor-head"><div><span class="node-type-mark">{{ selected.type.toUpperCase() }}</span><h2>{{ selected.title }}</h2></div><a-tooltip v-if="selected.type!=='start'" :title="canEdit ? '删除节点及其关联路径' : '无编辑权限' "><a-button danger type="text" :disabled="!canEdit" @click="removeNode(selected)"><DeleteOutlined />删除</a-button></a-tooltip></div>
|
||||
<a-form layout="vertical" class="property-form">
|
||||
<div class="property-grid"><a-form-item label="节点名称"><a-input v-model:value="selected.title" :disabled="!canEdit" /></a-form-item><a-form-item label="节点类型"><a-select v-model:value="selected.type" :disabled="!canEdit||selected.type==='start'" :options="nodeTypes" /></a-form-item></div>
|
||||
<a-form-item label="标准话术 / 操作提示"><a-textarea v-model:value="selected.content" :disabled="!canEdit" :rows="5" placeholder="执行到此节点时展示给一线人员的内容" /></a-form-item>
|
||||
<template v-if="selected.type==='start'">
|
||||
<div class="property-grid"><a-form-item label="摘要展示字段"><a-select v-model:value="selected.config.presentation.summary_field_keys" mode="multiple" :disabled="!canEdit" show-search option-filter-prop="label" placeholder="订单、客户、宠物等单值字段" :options="fields.map(field=>({value:field.field_key,label:`${field.field_name} (${field.field_key})`}))" /></a-form-item><a-form-item label="列表展示字段"><a-select v-model:value="selected.config.presentation.item_field_keys" mode="multiple" :disabled="!canEdit" show-search option-filter-prop="label" placeholder="商品名称、图片、ID 等数组字段" :options="fields.map(field=>({value:field.field_key,label:`${field.field_name} (${field.field_key})`}))" /></a-form-item></div>
|
||||
<div class="property-grid"><a-form-item label="图片字段"><a-select v-model:value="selected.config.presentation.image_field_keys" mode="multiple" :disabled="!canEdit" show-search option-filter-prop="label" placeholder="选择需要按图片渲染的字段" :options="startImageFieldOptions" /></a-form-item><a-form-item label="话术标题"><a-input v-model:value="selected.config.presentation.opening_title" :disabled="!canEdit" /></a-form-item></div>
|
||||
<a-form-item label="开场话术模板"><a-textarea v-model:value="selected.config.presentation.opening_template" :disabled="!canEdit" :rows="4" placeholder="例如:您好,{{input.customer_name}}。看到您购买了{{input.product_names}}。" /></a-form-item>
|
||||
</template>
|
||||
<template v-if="['question','choice'].includes(selected.type)"><div class="property-grid"><a-form-item label="写入场景字段"><a-select v-model:value="selected.config.field_key" :disabled="!canEdit" show-search option-filter-prop="label" placeholder="选择需要采集的字段" :options="fields.map(field=>({value:field.field_key,label:`${field.field_name} (${field.field_key})`}))" /></a-form-item><a-form-item label="是否必填"><a-switch v-model:checked="selected.config.required" :disabled="!canEdit" /></a-form-item></div></template>
|
||||
<a-form-item v-if="selected.type==='form'" label="表单采集字段"><a-select v-model:value="selected.config.field_keys" mode="multiple" :disabled="!canEdit" show-search option-filter-prop="label" placeholder="选择本步骤需要采集的字段" :options="fields.map(field=>({value:field.field_key,label:`${field.field_name} (${field.field_key})`}))" /></a-form-item>
|
||||
<template v-if="selected.type==='form'"><a-form-item label="表单采集字段"><a-select v-model:value="selected.config.field_keys" mode="multiple" :disabled="!canEdit" show-search option-filter-prop="label" placeholder="选择本步骤需要采集的字段" :options="fields.map(field=>({value:field.field_key,label:`${field.field_name} (${field.field_key})`}))" /></a-form-item><a-form-item label="节点必填字段"><a-select v-model:value="selected.config.required_field_keys" mode="multiple" :disabled="!canEdit" show-search option-filter-prop="label" placeholder="选择必须在本节点完成的字段" :options="formRequiredFieldOptions" /></a-form-item></template>
|
||||
<a-form-item v-if="selected.type==='choice'" label="可选项(每行一个)"><a-textarea :value="(selected.config.options||[]).join('\n')" :disabled="!canEdit" :rows="4" @change="selected.config.options=($event.target as HTMLTextAreaElement).value.split('\n').filter(Boolean)" /></a-form-item>
|
||||
<a-form-item v-if="selected.type==='knowledge'" label="关联知识卡"><a-select v-model:value="selected.config.knowledge_card_id" :disabled="!canEdit" placeholder="选择已发布知识卡" :options="knowledgeCards.map(card=>({value:card.id,label:card.title}))" @change="knowledgeCardChanged" /><div v-if="selected.config.knowledge_card_version_id" class="snapshot-note">已发布快照:知识卡版本 ID {{ selected.config.knowledge_card_version_id }}</div></a-form-item>
|
||||
<template v-if="selected.type==='knowledge'">
|
||||
<a-form-item label="派生知识字段"><a-input v-model:value="selected.config.knowledge_selector.derived_field" :disabled="!canEdit" placeholder="例如 matched_symptoms;值为知识 key 数组" /></a-form-item>
|
||||
<div class="property-grid"><a-form-item label="根知识类型"><a-input :value="csvValues(selected.config.knowledge_selector.knowledge_types)" :disabled="!canEdit" placeholder="symptom" @change="setSelectorList('knowledge_types',($event.target as HTMLInputElement).value)" /></a-form-item><a-form-item label="固定知识 key"><a-input :value="csvValues(selected.config.knowledge_selector.knowledge_keys)" :disabled="!canEdit" placeholder="多个 key 用逗号分隔" @change="setSelectorList('knowledge_keys',($event.target as HTMLInputElement).value)" /></a-form-item></div>
|
||||
<a-form-item label="允许返回的关系类型"><a-input :value="csvValues(selected.config.knowledge_selector.relation_types)" :disabled="!canEdit" placeholder="recommended_copy, possible_disease" @change="setSelectorList('relation_types',($event.target as HTMLInputElement).value)" /></a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
<div class="transition-head"><div><b>下一步路径</b><span>条件路径优先匹配,默认路径始终排在最后</span></div></div>
|
||||
<div class="transition-list"><div v-for="edge in outgoing(selectedKey)" :key="`${edge.source_node_key}-${edge.target_node_key}-${edge.priority}`" class="transition-row"><span class="route-line"></span><span class="route-target">{{ nodes.find(n=>n.node_key===edge.target_node_key)?.title || edge.target_node_key }}</span><a-tag>{{ conditionText(edge.condition) }}</a-tag><a-button v-if="canEdit" type="text" danger aria-label="删除路径" @click="removeEdge(edge)"><DeleteOutlined /></a-button></div><div v-if="!outgoing(selectedKey).length" class="route-empty">还没有下一步路径</div></div>
|
||||
@@ -122,20 +120,11 @@ onMounted(load)
|
||||
<a-empty v-else description="请选择一个节点" class="surface editor-empty" />
|
||||
</main>
|
||||
</a-spin>
|
||||
<a-drawer v-model:open="historyOpen" title="SOP 版本历史" width="580">
|
||||
<div class="version-list">
|
||||
<section v-for="item in versions" :key="item.id" class="version-row" :class="{current:item.version===version?.version}">
|
||||
<div class="version-number">V{{ item.version }}</div>
|
||||
<div class="version-copy"><div><a-tag :color="item.status==='published'?'green':item.status==='reviewing'?'orange':'default'">{{ versionStatus(item.status) }}</a-tag><span>{{ item.creator_name || '未知创建人' }}</span></div><time>{{ new Date(item.published_at || item.created_at).toLocaleString('zh-CN') }}</time></div>
|
||||
<div class="version-actions"><a-button size="small" @click="viewVersion(item.version)">查看</a-button><a-tooltip v-if="auth.can('sop.publish')&&['superseded','offline'].includes(item.status)&&!hasPendingVersion" title="复制该版本并作为新版本发布"><a-button size="small" :loading="rollingBack" @click="rollbackVersion(item)"><RollbackOutlined />回滚</a-button></a-tooltip></div>
|
||||
</section>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.editor-page{min-height:calc(100vh - 62px);background:#eef1ef}.editor-header{min-height:66px;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:10px 24px;background:white;border-bottom:1px solid var(--line);position:sticky;top:62px;z-index:10}.editor-title{display:flex;align-items:center;gap:8px}.editor-title>div{display:flex;flex-direction:column}.editor-title b{font-size:15px}.editor-title span{margin-top:3px;color:var(--muted);font-size:11px}.page-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}.editor-workbench{display:grid;grid-template-columns:280px minmax(0,1fr);gap:16px;max-width:1500px;margin:0 auto;padding:18px}.node-rail{align-self:start;overflow:hidden;position:sticky;top:146px}.rail-title{height:50px;display:flex;align-items:center;justify-content:space-between;padding:0 14px;border-bottom:1px solid var(--line);font-weight:650}.node-list{padding:8px}.node-list button{width:100%;min-height:58px;display:grid;grid-template-columns:34px 1fr auto;align-items:center;gap:9px;padding:7px 8px;text-align:left;background:transparent;border:1px solid transparent;border-radius:4px;cursor:pointer}.node-list button:hover{background:#f5f8f6}.node-list button.active{background:#edf6f2;border-color:#b9d8cc}.node-index{color:#88958f;font-family:monospace;font-size:11px}.node-meta{min-width:0;display:flex;flex-direction:column}.node-meta b{overflow:hidden;font-size:13px;text-overflow:ellipsis;white-space:nowrap}.node-meta small{margin-top:3px;color:var(--muted);font-size:11px}.node-move{display:flex;gap:4px;color:#87938e}.node-move>*:hover{color:var(--green)}.node-editor{align-self:start;min-height:600px;padding:24px}.node-editor-head{display:flex;align-items:flex-start;justify-content:space-between;padding-bottom:18px;border-bottom:1px solid var(--line)}.node-editor-head h2{margin:6px 0 0;font-family:"Noto Serif SC",serif;font-size:22px}.node-type-mark{color:var(--green);font-size:10px;font-weight:750}.property-form{padding-top:22px}.property-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.transition-head{display:flex;justify-content:space-between;margin:8px -24px 0;padding:18px 24px 10px;border-top:1px solid var(--line)}.transition-head>div{display:flex;flex-direction:column}.transition-head span{margin-top:4px;color:var(--muted);font-size:11px}.transition-row{min-height:48px;display:grid;grid-template-columns:20px minmax(120px,1fr) auto 36px;align-items:center;gap:10px;border-bottom:1px solid #edf0ee}.route-line{width:16px;height:1px;background:var(--green)}.route-target{font-weight:600}.route-empty{padding:20px 0;color:var(--muted);font-size:12px}.edge-builder{padding-top:16px}.edge-main{display:grid;grid-template-columns:minmax(220px,1fr) auto auto;align-items:center;gap:10px}.condition-toggle{display:flex;align-items:center;gap:7px;color:var(--muted);font-size:12px}.condition-builder{margin-top:12px;padding:14px;background:#f7f9f8;border:1px solid var(--line);border-radius:4px}.condition-mode{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;color:var(--muted);font-size:12px}.condition-rule{display:grid;grid-template-columns:1.2fr 130px 1fr 34px;gap:8px;margin-top:8px}.editor-empty{min-height:400px;display:grid;place-items:center}.version-row{display:grid;grid-template-columns:54px 1fr auto;align-items:center;gap:12px;padding:16px 8px;border-bottom:1px solid var(--line)}.version-row.current{background:#f1f7f4}.version-number{font-weight:750;color:#245f4d}.version-copy>div{display:flex;align-items:center;gap:6px}.version-copy time{display:block;margin-top:5px;color:var(--muted);font-size:11px}.version-actions{display:flex;gap:6px}
|
||||
.editor-page{min-height:calc(100vh - 62px);background:#eef1ef}.editor-header{min-height:66px;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:10px 24px;background:white;border-bottom:1px solid var(--line);position:sticky;top:62px;z-index:10}.editor-title{display:flex;align-items:center;gap:8px}.editor-title>div{display:flex;flex-direction:column}.editor-title b{font-size:15px}.editor-title span{margin-top:3px;color:var(--muted);font-size:11px}.page-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}.editor-workbench{display:grid;grid-template-columns:280px minmax(0,1fr);gap:16px;max-width:1500px;margin:0 auto;padding:18px}.node-rail{align-self:start;overflow:hidden;position:sticky;top:146px}.rail-title{height:50px;display:flex;align-items:center;justify-content:space-between;padding:0 14px;border-bottom:1px solid var(--line);font-weight:650}.node-list{padding:8px}.node-list-item{width:100%;min-height:58px;display:grid;grid-template-columns:34px minmax(0,1fr) auto;align-items:center;gap:9px;padding:7px 8px;text-align:left;background:transparent;border:1px solid transparent;border-radius:4px;cursor:pointer}.node-list-item:hover{background:#f5f8f6}.node-list-item.active{background:#edf6f2;border-color:#b9d8cc}.node-index{color:#88958f;font-family:monospace;font-size:11px}.node-meta{min-width:0;display:flex;flex-direction:column}.node-meta b{overflow:hidden;font-size:13px;text-overflow:ellipsis;white-space:nowrap}.node-meta small{margin-top:3px;color:var(--muted);font-size:11px}.node-actions{display:flex;gap:1px}.node-actions .ant-btn{width:24px;padding:0;color:#87938e}.node-actions .ant-btn:hover{color:var(--green)}.node-actions .ant-btn-dangerous:hover{color:#c34c4c}.node-editor{align-self:start;min-height:600px;padding:24px}.node-editor-head{display:flex;align-items:flex-start;justify-content:space-between;padding-bottom:18px;border-bottom:1px solid var(--line)}.node-editor-head h2{margin:6px 0 0;font-family:"Noto Serif SC",serif;font-size:22px}.node-type-mark{color:var(--green);font-size:10px;font-weight:750}.property-form{padding-top:22px}.property-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.transition-head{display:flex;justify-content:space-between;margin:8px -24px 0;padding:18px 24px 10px;border-top:1px solid var(--line)}.transition-head>div{display:flex;flex-direction:column}.transition-head span{margin-top:4px;color:var(--muted);font-size:11px}.transition-row{min-height:48px;display:grid;grid-template-columns:20px minmax(120px,1fr) auto 36px;align-items:center;gap:10px;border-bottom:1px solid #edf0ee}.route-line{width:16px;height:1px;background:var(--green)}.route-target{font-weight:600}.route-empty{padding:20px 0;color:var(--muted);font-size:12px}.edge-builder{padding-top:16px}.edge-main{display:grid;grid-template-columns:minmax(220px,1fr) auto auto;align-items:center;gap:10px}.condition-toggle{display:flex;align-items:center;gap:7px;color:var(--muted);font-size:12px}.condition-builder{margin-top:12px;padding:14px;background:#f7f9f8;border:1px solid var(--line);border-radius:4px}.condition-mode{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;color:var(--muted);font-size:12px}.condition-rule{display:grid;grid-template-columns:1.2fr 130px 1fr 34px;gap:8px;margin-top:8px}.editor-empty{min-height:400px;display:grid;place-items:center}
|
||||
.snapshot-note{margin-top:6px;color:var(--muted);font-size:11px}
|
||||
@media(max-width:1050px){.editor-workbench{grid-template-columns:230px minmax(0,1fr)}.condition-rule{grid-template-columns:1fr 120px 1fr 34px}}@media(max-width:760px){.editor-header{top:62px;align-items:flex-start;flex-direction:column;padding:12px}.page-actions{justify-content:flex-start}.editor-workbench{grid-template-columns:1fr;padding:10px}.node-rail{position:static}.property-grid{grid-template-columns:1fr}.node-editor{padding:16px}.transition-head{margin:8px -16px 0;padding:16px}.edge-main,.condition-rule{grid-template-columns:1fr}.condition-rule .ant-btn{justify-self:end}.version-row{grid-template-columns:46px 1fr}.version-actions{grid-column:2}}
|
||||
@media(max-width:1050px){.editor-workbench{grid-template-columns:230px minmax(0,1fr)}.condition-rule{grid-template-columns:1fr 120px 1fr 34px}}@media(max-width:760px){.editor-header{top:62px;align-items:flex-start;flex-direction:column;padding:12px}.page-actions{justify-content:flex-start}.editor-workbench{grid-template-columns:1fr;padding:10px}.node-rail{position:static}.property-grid{grid-template-columns:1fr}.node-editor{padding:16px}.transition-head{margin:8px -16px 0;padding:16px}.edge-main,.condition-rule{grid-template-columns:1fr}.condition-rule .ant-btn{justify-self:end}}
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowLeftOutlined, BranchesOutlined, DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons-vue'
|
||||
import { ArrowLeftOutlined, BranchesOutlined, DeleteOutlined, EditOutlined, PlusOutlined, SaveOutlined } from '@ant-design/icons-vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { api, apiMessage } from '@/api/client'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import type { Scenario, ScenarioField, SOP } from '@/types'
|
||||
import ScenarioKnowledgeTab from '@/components/ScenarioKnowledgeTab.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const scenarioId = Number(route.params.id)
|
||||
const activeTab = ref(String(route.query.tab || 'fields'))
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const scenario = ref<Scenario | null>(null)
|
||||
@@ -21,13 +23,17 @@ const fieldOpen = ref(false)
|
||||
const sopOpen = ref(false)
|
||||
const editingFieldId = ref<number | null>(null)
|
||||
const scenarioForm = reactive({ name: '', industry: '', role_name: '', goal: '', trigger_text: '', visibility: 'tenant' })
|
||||
const fieldForm = reactive({ field_key: '', field_name: '', field_type: 'text', required: false, options_text: '', min: undefined as number | undefined, max: undefined as number | undefined, min_length: undefined as number | undefined, max_length: undefined as number | undefined })
|
||||
const fieldForm = reactive({ field_key: '', field_name: '', source_path: '', field_type: 'text', required: false, options_text: '', min: undefined as number | undefined, max: undefined as number | undefined, min_length: undefined as number | undefined, max_length: undefined as number | undefined })
|
||||
const sopForm = reactive({ name: '', description: '' })
|
||||
const contractText = ref('{\n "output_schema": {"fields": []},\n "result_schema": {"fields": []},\n "rules": [],\n "allowed_origins": []\n}')
|
||||
const previewText = ref('{\n "input": {},\n "initial_values": {},\n "knowledge_selector": {\n "derived_field": "",\n "knowledge_types": [],\n "knowledge_keys": [],\n "relation_types": []\n }\n}')
|
||||
const previewResult = ref<any>(null)
|
||||
const previewing = ref(false)
|
||||
const statusColor = computed(() => scenario.value?.status === 'active' ? 'green' : 'default')
|
||||
const fieldModalTitle = computed(() => editingFieldId.value ? '编辑场景字段' : '添加场景字段')
|
||||
const fieldModalTitle = computed(() => editingFieldId.value ? '编辑场景输入字段' : '添加场景输入字段')
|
||||
const fieldTypeOptions = [
|
||||
{ value: 'text', label: '单行文本' }, { value: 'textarea', label: '多行文本' }, { value: 'number', label: '数字' },
|
||||
{ value: 'boolean', label: '是/否' }, { value: 'select', label: '单选' }, { value: 'multiselect', label: '多选' }, { value: 'date', label: '日期' },
|
||||
{ value: 'boolean', label: '是/否' }, { value: 'select', label: '单选' }, { value: 'multiselect', label: '多选' }, { value: 'array', label: '数组' }, { value: 'date', label: '日期' },
|
||||
]
|
||||
|
||||
async function load() {
|
||||
@@ -44,6 +50,10 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadContract(){try{const data=await api.get<any>(`/scenarios/${scenarioId}/contract`);contractText.value=JSON.stringify({output_schema:data.output_schema||{fields:[]},result_schema:data.result_schema||{fields:[]},rules:(data.rules||[]).map((rule:any)=>({rule_key:rule.rule_key,name:rule.name,condition:rule.condition,actions:rule.actions,priority:rule.priority,status:rule.status})),allowed_origins:data.allowed_origins||[]},null,2)}catch(error){message.error(apiMessage(error))}}
|
||||
async function saveContract(){let payload:any;try{payload=JSON.parse(contractText.value)}catch{return message.warning('规则与输出 JSON 格式不正确')}saving.value=true;try{await api.put(`/scenarios/${scenarioId}/contract`,payload);message.success('规则与输出契约已保存');await loadContract()}catch(error){message.error(apiMessage(error))}finally{saving.value=false}}
|
||||
async function previewScenario(){let payload:any;try{payload=JSON.parse(previewText.value)}catch{return message.warning('预览 JSON 格式不正确')}previewing.value=true;try{previewResult.value=await api.post(`/scenarios/${scenarioId}/preview`,payload);message.success('预览完成')}catch(error){message.error(apiMessage(error))}finally{previewing.value=false}}
|
||||
|
||||
function openScenarioEditor() {
|
||||
if (!scenario.value) return
|
||||
Object.assign(scenarioForm, {
|
||||
@@ -70,7 +80,7 @@ async function updateScenario() {
|
||||
function archiveScenario() {
|
||||
Modal.confirm({
|
||||
title: `归档“${scenario.value?.name}”?`,
|
||||
content: '归档后场景不再出现在列表中。存在已发布或审核中 SOP 时,需要先处理对应版本。',
|
||||
content: '归档后场景及其 SOP 将不再用于新执行。',
|
||||
okText: '归档场景', okType: 'danger', cancelText: '取消',
|
||||
async onOk() {
|
||||
try {
|
||||
@@ -84,7 +94,7 @@ function archiveScenario() {
|
||||
|
||||
function resetFieldForm() {
|
||||
editingFieldId.value = null
|
||||
Object.assign(fieldForm, { field_key: '', field_name: '', field_type: 'text', required: false, options_text: '', min: undefined, max: undefined, min_length: undefined, max_length: undefined })
|
||||
Object.assign(fieldForm, { field_key: '', field_name: '', source_path: '', field_type: 'text', required: false, options_text: '', min: undefined, max: undefined, min_length: undefined, max_length: undefined })
|
||||
}
|
||||
|
||||
function openNewField() {
|
||||
@@ -96,7 +106,7 @@ function openFieldEditor(field: ScenarioField) {
|
||||
const validation = field.validation || {}
|
||||
editingFieldId.value = field.id
|
||||
Object.assign(fieldForm, {
|
||||
field_key: field.field_key, field_name: field.field_name, field_type: field.field_type, required: field.required,
|
||||
field_key: field.field_key, field_name: field.field_name, source_path: field.source_path || '', field_type: field.field_type, required: field.required,
|
||||
options_text: Array.isArray(field.options) ? field.options.join('\n') : '',
|
||||
min: typeof validation.min === 'number' ? validation.min : undefined,
|
||||
max: typeof validation.max === 'number' ? validation.max : undefined,
|
||||
@@ -135,7 +145,7 @@ async function saveField() {
|
||||
|
||||
async function deleteField(field: ScenarioField) {
|
||||
Modal.confirm({
|
||||
title: `删除字段“${field.field_name}”?`, content: '已发布或历史 SOP 版本引用的字段不能删除。', okText: '删除字段', okType: 'danger',
|
||||
title: `删除字段“${field.field_name}”?`, content: '当前 SOP 引用的字段不能删除。', okText: '删除字段', okType: 'danger',
|
||||
async onOk() {
|
||||
try { await api.delete(`/scenario-fields/${field.id}`); message.success('字段已删除'); await load() }
|
||||
catch (error) { message.error(apiMessage(error)) }
|
||||
@@ -147,7 +157,7 @@ async function createSOP() {
|
||||
saving.value = true
|
||||
try {
|
||||
const data = await api.post<{ sop: SOP }>(`/scenarios/${scenarioId}/sops`, sopForm)
|
||||
message.success('SOP 草稿已创建')
|
||||
message.success('SOP 已创建')
|
||||
sopOpen.value = false
|
||||
router.push(`/sops/${data.sop.id}`)
|
||||
} catch (error) {
|
||||
@@ -157,10 +167,9 @@ async function createSOP() {
|
||||
}
|
||||
}
|
||||
|
||||
function statusText(value: string) { return ({ draft: '草稿', reviewing: '审核中', published: '已发布', offline: '已下线' } as Record<string, string>)[value] || value }
|
||||
function fieldTypeText(value: string) { return fieldTypeOptions.find(item => item.value === value)?.label || value }
|
||||
function visibilityText(value: string) { return ({ tenant: '全企业', team: '团队', private: '仅自己' } as Record<string, string>)[value] || value }
|
||||
onMounted(load)
|
||||
onMounted(()=>{load();loadContract()})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -179,18 +188,18 @@ onMounted(load)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-tabs default-active-key="fields" class="detail-tabs">
|
||||
<a-tab-pane key="fields" tab="场景字段">
|
||||
<a-tabs v-model:active-key="activeTab" class="detail-tabs">
|
||||
<a-tab-pane key="fields" tab="场景输入">
|
||||
<section class="surface">
|
||||
<div class="toolbar"><div><b>采集字段</b><span class="toolbar-note">执行过程中收集的信息及校验规则</span></div><a-button v-if="auth.can('scenario.edit')" type="primary" @click="openNewField"><PlusOutlined />添加字段</a-button></div>
|
||||
<a-table :data-source="fields" row-key="id" :pagination="false" :scroll="{x:760}" :columns="[{title:'字段名称',dataIndex:'field_name'},{title:'字段标识',dataIndex:'field_key'},{title:'类型',dataIndex:'field_type',width:130},{title:'必填',dataIndex:'required',width:90},{title:'',key:'action',width:100}]">
|
||||
<div class="toolbar"><div><b>业务输入字段</b><span class="toolbar-note">定义订单、订单商品、用户、宠物等传入信息及校验规则</span></div><a-button v-if="auth.can('scenario.edit')" type="primary" @click="openNewField"><PlusOutlined />添加输入字段</a-button></div>
|
||||
<a-table :data-source="fields" row-key="id" :pagination="false" :scroll="{x:920}" :columns="[{title:'字段名称',dataIndex:'field_name'},{title:'字段标识',dataIndex:'field_key'},{title:'外部数据路径',dataIndex:'source_path'},{title:'类型',dataIndex:'field_type',width:130},{title:'必填',dataIndex:'required',width:90},{title:'',key:'action',width:100}]">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'field_key'"><code>{{ record.field_key }}</code></template>
|
||||
<template v-else-if="column.dataIndex === 'field_type'">{{ fieldTypeText(record.field_type) }}</template>
|
||||
<template v-else-if="column.dataIndex === 'required'"><a-tag :color="record.required ? 'orange' : 'default'">{{ record.required ? '必填' : '选填' }}</a-tag></template>
|
||||
<template v-else-if="column.key === 'action'"><div v-if="auth.can('scenario.edit')" class="row-actions"><a-button type="text" aria-label="编辑字段" @click="openFieldEditor(record)"><EditOutlined /></a-button><a-button type="text" danger aria-label="删除字段" @click="deleteField(record)"><DeleteOutlined /></a-button></div></template>
|
||||
</template>
|
||||
<template #emptyText><div class="empty-copy">还没有字段。先添加执行过程中需要收集的客户信息。</div></template>
|
||||
<template #emptyText><div class="empty-copy">还没有输入字段。可添加 order_、order_item_、customer_、pet_ 等业务字段。</div></template>
|
||||
</a-table>
|
||||
</section>
|
||||
</a-tab-pane>
|
||||
@@ -198,20 +207,30 @@ onMounted(load)
|
||||
<section class="surface">
|
||||
<div class="toolbar"><div><b>话术流程</b><span class="toolbar-note">一个场景可以包含多套 SOP</span></div><a-button v-if="auth.can('sop.edit')" type="primary" @click="sopOpen = true"><PlusOutlined />创建 SOP</a-button></div>
|
||||
<div v-if="sops.length" class="sop-list">
|
||||
<button v-for="item in sops" :key="item.id" @click="router.push(`/sops/${item.id}`)"><span class="sop-icon"><BranchesOutlined /></span><span class="sop-copy"><b>{{ item.name }}</b><small>{{ item.description || '暂无说明' }}</small></span><a-tag :color="item.status === 'published' ? 'green' : 'default'">{{ statusText(item.status) }}</a-tag><span class="sop-date">{{ new Date(item.updated_at).toLocaleDateString('zh-CN') }}</span></button>
|
||||
<button v-for="item in sops" :key="item.id" @click="router.push(`/sops/${item.id}`)"><span class="sop-icon"><BranchesOutlined /></span><span class="sop-copy"><b>{{ item.name }}</b><small>{{ item.description || '暂无说明' }}</small></span><span class="sop-date">{{ new Date(item.updated_at).toLocaleDateString('zh-CN') }}</span></button>
|
||||
</div>
|
||||
<div v-else class="empty-state"><BranchesOutlined /><p>还没有 SOP。创建一套流程,将经验变成连续动作。</p></div>
|
||||
</section>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane v-if="auth.can('knowledge.view')" key="knowledge" tab="知识卡">
|
||||
<ScenarioKnowledgeTab :scenario-id="scenarioId" :fields="fields" :editable="auth.can('knowledge.edit') && auth.can('scenario.edit')" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="contract" tab="规则与输出">
|
||||
<section class="surface contract-editor"><div class="toolbar"><div><b>场景运行契约</b><span class="toolbar-note">定义派生规则、过程输出、最终结果格式和允许接入的域名</span></div><a-button v-if="auth.can('scenario.edit')" type="primary" :loading="saving" @click="saveContract"><SaveOutlined />保存契约</a-button></div><div class="sdk-identifiers"><span>scenarioKey <code>{{ scenario?.scenario_key }}</code></span><span>publicKey <code>{{ scenario?.public_key }}</code></span></div><a-textarea v-model:value="contractText" :disabled="!auth.can('scenario.edit')" class="contract-json" :auto-size="{minRows:20,maxRows:36}" spellcheck="false" /></section>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="preview" tab="预览">
|
||||
<section class="preview-layout"><div class="surface preview-input"><div class="toolbar"><div><b>样例业务数据</b><span class="toolbar-note">使用真实映射、规则、知识和输出服务运行</span></div><a-button type="primary" :loading="previewing" @click="previewScenario">运行预览</a-button></div><a-textarea v-model:value="previewText" class="contract-json" :auto-size="{minRows:24,maxRows:40}" spellcheck="false" /></div><div class="surface preview-output"><div class="toolbar"><div><b>运行结果</b><span class="toolbar-note">输入映射、派生事实、命中规则、知识和输出</span></div></div><pre v-if="previewResult">{{ JSON.stringify(previewResult,null,2) }}</pre><a-empty v-else description="运行预览后查看结果" /></div></section>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-skeleton>
|
||||
|
||||
<a-modal v-if="auth.can('scenario.edit')" v-model:open="scenarioOpen" title="编辑场景资料" ok-text="保存修改" :confirm-loading="saving" @ok="updateScenario">
|
||||
<a-form layout="vertical" :model="scenarioForm"><a-form-item label="场景名称"><a-input v-model:value="scenarioForm.name" /></a-form-item><div class="field-grid"><a-form-item label="所属行业"><a-input v-model:value="scenarioForm.industry" /></a-form-item><a-form-item label="适用角色"><a-input v-model:value="scenarioForm.role_name" /></a-form-item></div><a-form-item label="执行目标"><a-textarea v-model:value="scenarioForm.goal" :rows="3" /></a-form-item><a-form-item label="触发条件"><a-textarea v-model:value="scenarioForm.trigger_text" :rows="3" /></a-form-item><a-form-item label="可见范围"><a-radio-group v-model:value="scenarioForm.visibility"><a-radio value="tenant">全企业</a-radio><a-radio value="team">团队</a-radio><a-radio value="private">仅自己</a-radio></a-radio-group></a-form-item></a-form>
|
||||
</a-modal>
|
||||
<a-modal v-if="auth.can('scenario.edit')" v-model:open="fieldOpen" :title="fieldModalTitle" :ok-text="editingFieldId ? '保存字段' : '添加字段'" :confirm-loading="saving" @ok="saveField" @cancel="resetFieldForm">
|
||||
<a-modal v-if="auth.can('scenario.edit')" v-model:open="fieldOpen" :title="fieldModalTitle" :ok-text="editingFieldId ? '保存输入字段' : '添加输入字段'" :confirm-loading="saving" @ok="saveField" @cancel="resetFieldForm">
|
||||
<a-form layout="vertical" :model="fieldForm">
|
||||
<div class="field-grid"><a-form-item label="字段名称"><a-input v-model:value="fieldForm.field_name" placeholder="宠物体重" /></a-form-item><a-form-item label="字段标识"><a-input v-model:value="fieldForm.field_key" placeholder="pet_weight" /></a-form-item></div>
|
||||
<div class="field-grid"><a-form-item label="字段名称"><a-input v-model:value="fieldForm.field_name" placeholder="宠物体重" /></a-form-item><a-form-item label="字段标识"><a-input v-model:value="fieldForm.field_key" placeholder="pet_weight、order_no、customer_name" /></a-form-item></div>
|
||||
<a-form-item label="外部数据路径"><a-input v-model:value="fieldForm.source_path" placeholder="customer.name 或 order.items[*].product_id" /></a-form-item>
|
||||
<a-form-item label="字段类型"><a-select v-model:value="fieldForm.field_type" :options="fieldTypeOptions" /></a-form-item>
|
||||
<a-form-item v-if="['select','multiselect'].includes(fieldForm.field_type)" label="选项(每行一个)"><a-textarea v-model:value="fieldForm.options_text" :rows="4" /></a-form-item>
|
||||
<div v-if="fieldForm.field_type === 'number'" class="field-grid"><a-form-item label="最小值"><a-input-number v-model:value="fieldForm.min" style="width:100%" /></a-form-item><a-form-item label="最大值"><a-input-number v-model:value="fieldForm.max" style="width:100%" /></a-form-item></div>
|
||||
@@ -219,12 +238,14 @@ onMounted(load)
|
||||
<a-checkbox v-model:checked="fieldForm.required">执行时必须填写</a-checkbox>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<a-modal v-if="auth.can('sop.edit')" v-model:open="sopOpen" title="创建 SOP 草稿" :footer="null">
|
||||
<a-modal v-if="auth.can('sop.edit')" v-model:open="sopOpen" title="创建 SOP" :footer="null">
|
||||
<a-form layout="vertical" :model="sopForm" @finish="createSOP"><a-form-item label="SOP 名称" name="name" :rules="[{required:true,message:'请输入 SOP 名称'}]"><a-input v-model:value="sopForm.name" placeholder="问诊问药标准流程" /></a-form-item><a-form-item label="说明"><a-textarea v-model:value="sopForm.description" :rows="4" placeholder="这套流程适用于什么情况" /></a-form-item><a-button type="primary" html-type="submit" block :loading="saving">创建并编排</a-button></a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.back-link{margin:-4px 0 12px -14px;color:var(--muted)}.detail-heading{min-height:180px;display:flex;align-items:flex-end;justify-content:space-between;gap:24px;padding:28px;color:white;background:#202825;border-radius:6px}.scenario-summary{min-width:0}.detail-heading h1{margin:10px 0 8px;font-family:"Noto Serif SC",serif;font-size:28px;letter-spacing:0}.detail-heading p{max-width:720px;margin:0;color:#c3cec9}.heading-tags{display:flex;align-items:center;gap:10px;color:#a8b6b0;font-size:12px}.trigger-copy{max-width:800px;margin-top:14px;color:#aab8b2;font-size:12px}.trigger-copy span{margin-right:9px;color:#68c6a6;font-weight:700}.heading-actions{align-self:stretch;display:flex;flex-direction:column;align-items:flex-end;justify-content:space-between}.heading-actions>div{display:flex;gap:8px}.scenario-code{color:#70817a;font-size:12px}.detail-tabs{margin-top:18px}.toolbar>div{display:flex;align-items:baseline;gap:12px}.toolbar-note{color:var(--muted);font-size:12px}.field-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.row-actions{display:flex}code{padding:2px 5px;color:#17654f;background:#eef5f2;border-radius:3px}.sop-list button{width:100%;min-height:74px;display:grid;grid-template-columns:42px 1fr auto 100px;align-items:center;gap:14px;padding:12px 18px;text-align:left;background:white;border:0;border-bottom:1px solid var(--line);cursor:pointer}.sop-list button:hover{background:#f8faf9}.sop-icon{width:38px;height:38px;display:grid;place-items:center;color:var(--green);background:#e8f3ef;border-radius:4px}.sop-copy{min-width:0;display:flex;flex-direction:column}.sop-copy small{overflow:hidden;margin-top:4px;color:var(--muted);text-overflow:ellipsis;white-space:nowrap}.sop-date{color:var(--muted);font-size:12px;text-align:right}.empty-state{padding:64px 20px;color:#82908a;text-align:center}.empty-state>span{font-size:32px}.empty-state p{margin:12px 0 0}@media(max-width:650px){.detail-heading{align-items:flex-start;flex-direction:column;padding:20px}.heading-actions{width:100%;align-items:flex-start;gap:14px}.scenario-code{display:none}.field-grid{grid-template-columns:1fr;gap:0}.sop-list button{grid-template-columns:36px 1fr auto}.sop-date{display:none}.toolbar-note{display:none}}
|
||||
.contract-editor{padding:18px}.sdk-identifiers{display:flex;flex-wrap:wrap;gap:18px;margin:12px 0;color:var(--muted);font-size:12px}.contract-json{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6}
|
||||
.preview-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px}.preview-input,.preview-output{min-width:0;padding:18px}.preview-output pre{max-height:680px;overflow:auto;margin:0;padding:16px;color:#dbe8e3;background:#202825;border-radius:4px;font:12px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.preview-output :deep(.ant-empty){padding:100px 0}
|
||||
.back-link{margin:-4px 0 12px -14px;color:var(--muted)}.detail-heading{min-height:180px;display:flex;align-items:flex-end;justify-content:space-between;gap:24px;padding:28px;color:white;background:#202825;border-radius:6px}.scenario-summary{min-width:0}.detail-heading h1{margin:10px 0 8px;font-family:"Noto Serif SC",serif;font-size:28px;letter-spacing:0}.detail-heading p{max-width:720px;margin:0;color:#c3cec9}.heading-tags{display:flex;align-items:center;gap:10px;color:#a8b6b0;font-size:12px}.trigger-copy{max-width:800px;margin-top:14px;color:#aab8b2;font-size:12px}.trigger-copy span{margin-right:9px;color:#68c6a6;font-weight:700}.heading-actions{align-self:stretch;display:flex;flex-direction:column;align-items:flex-end;justify-content:space-between}.heading-actions>div{display:flex;gap:8px}.scenario-code{color:#70817a;font-size:12px}.detail-tabs{margin-top:18px}.toolbar>div{display:flex;align-items:baseline;gap:12px}.toolbar-note{color:var(--muted);font-size:12px}.field-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.row-actions{display:flex}code{padding:2px 5px;color:#17654f;background:#eef5f2;border-radius:3px}.sop-list button{width:100%;min-height:74px;display:grid;grid-template-columns:42px 1fr 100px;align-items:center;gap:14px;padding:12px 18px;text-align:left;background:white;border:0;border-bottom:1px solid var(--line);cursor:pointer}.sop-list button:hover{background:#f8faf9}.sop-icon{width:38px;height:38px;display:grid;place-items:center;color:var(--green);background:#e8f3ef;border-radius:4px}.sop-copy{min-width:0;display:flex;flex-direction:column}.sop-copy small{overflow:hidden;margin-top:4px;color:var(--muted);text-overflow:ellipsis;white-space:nowrap}.sop-date{color:var(--muted);font-size:12px;text-align:right}.empty-state{padding:64px 20px;color:#82908a;text-align:center}.empty-state>span{font-size:32px}.empty-state p{margin:12px 0 0}@media(max-width:900px){.preview-layout{grid-template-columns:1fr}}@media(max-width:650px){.detail-heading{align-items:flex-start;flex-direction:column;padding:20px}.heading-actions{width:100%;align-items:flex-start;gap:14px}.scenario-code{display:none}.field-grid{grid-template-columns:1fr;gap:0}.sop-list button{grid-template-columns:36px 1fr}.sop-date{display:none}.toolbar-note{display:none}}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user