feat: Phase 3-5 - workflow, labeling, reports, dashboard enhancement, tests

This commit is contained in:
hiderfong
2026-04-22 17:22:11 +08:00
parent e71b13fe39
commit fb4aaad9fc
50 changed files with 741 additions and 323 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
.page-title[data-v-bd46f99b]{font-size:20px;font-weight:600;margin-bottom:16px;color:#303133}.filter-bar[data-v-bd46f99b]{padding:16px;margin-bottom:16px;display:flex;align-items:center;flex-wrap:wrap;gap:8px}.table-card[data-v-bd46f99b]{padding:16px;background:#fff;border-radius:8px}.pagination-bar[data-v-bd46f99b]{display:flex;justify-content:flex-end;margin-top:16px}.empty-text[data-v-bd46f99b]{color:#c0c4cc}.confidence-high[data-v-bd46f99b]{color:#67c23a;font-weight:600}.confidence-mid[data-v-bd46f99b]{color:#e6a23c;font-weight:600}.confidence-low[data-v-bd46f99b]{color:#f56c6c;font-weight:600}
-2
View File
@@ -1,2 +0,0 @@
import{d as q,n as G,o as n,c as r,a as v,b as a,w as o,D as K,q as p,k as i,E as A,r as c,G as H,F as U,s as j,g as J,I as O,j as d,x as u,T as Q,_ as R}from"./index-DIl-pxgT.js";import{g as B}from"./project-DH-EQEsQ.js";import{g as W}from"./classification-CUKwOuh8.js";const X={class:"page-container"},Y={class:"filter-bar card-shadow"},Z={class:"table-card card-shadow"},$={key:1,class:"empty-text"},ee={key:1,class:"empty-text"},ae={key:1,class:"empty-text"},te={class:"pagination-bar"},le=q({__name:"Classification",setup(oe){const g=i(!1),h=i([]),_=i(1),b=i(50),w=i(0),x=i(void 0),k=i(void 0),m=i(""),z=i([]),V=i([]);function I(l){return l>=.8?"confidence-high":l>=.5?"confidence-mid":"confidence-low"}async function y(){g.value=!0;try{const l={page:_.value,page_size:b.value};m.value&&(l.keyword=m.value);const t=await B(l);h.value=[],w.value=0}catch(l){A.error((l==null?void 0:l.message)||"加载失败")}finally{g.value=!1}}function L(){_.value=1,y()}async function P(){try{const[l,t]=await Promise.all([B({page:1,page_size:100}),W()]);z.value=(l==null?void 0:l.data)||[],V.value=t||[]}catch{}}return G(()=>{P(),y()}),(l,t)=>{const C=c("el-option"),D=c("el-select"),f=c("el-tag"),S=c("el-input"),E=c("el-icon"),F=c("el-button"),s=c("el-table-column"),M=c("el-table"),N=c("el-pagination"),T=H("loading");return n(),r("div",X,[t[6]||(t[6]=v("h2",{class:"page-title"},"分类分级结果",-1)),v("div",Y,[a(D,{modelValue:x.value,"onUpdate:modelValue":t[0]||(t[0]=e=>x.value=e),placeholder:"选择项目",clearable:"",style:{width:"180px"}},{default:o(()=>[(n(!0),r(U,null,j(z.value,e=>(n(),p(C,{key:e.id,label:e.name,value:e.id},null,8,["label","value"]))),128))]),_:1},8,["modelValue"]),a(D,{modelValue:k.value,"onUpdate:modelValue":t[1]||(t[1]=e=>k.value=e),placeholder:"选择分级",clearable:"",style:{width:"140px","margin-left":"12px"}},{default:o(()=>[(n(!0),r(U,null,j(V.value,e=>(n(),p(C,{key:e.id,label:e.name,value:e.id},{default:o(()=>[a(f,{size:"small",color:e.color,effect:"dark"},{default:o(()=>[d(u(e.code),1)]),_:2},1032,["color"]),d(" "+u(e.name),1)]),_:2},1032,["label","value"]))),128))]),_:1},8,["modelValue"]),a(S,{modelValue:m.value,"onUpdate:modelValue":t[2]||(t[2]=e=>m.value=e),placeholder:"搜索字段名/注释",clearable:"",style:{width:"200px","margin-left":"12px"}},null,8,["modelValue"]),a(F,{type:"primary",style:{"margin-left":"12px"},onClick:L},{default:o(()=>[a(E,null,{default:o(()=>[a(J(O))]),_:1}),t[5]||(t[5]=d("查询 ",-1))]),_:1})]),v("div",Z,[K((n(),p(M,{data:h.value,stripe:"",size:"default",border:""},{default:o(()=>[a(s,{prop:"column_name",label:"字段名","min-width":"140"}),a(s,{prop:"data_type",label:"类型",width:"100"}),a(s,{prop:"comment",label:"注释","min-width":"150","show-overflow-tooltip":""}),a(s,{prop:"table_name",label:"所属表",width:"140"}),a(s,{prop:"database_name",label:"所属库",width:"120"}),a(s,{prop:"source_name",label:"数据源",width:"120"}),a(s,{label:"分类",width:"140"},{default:o(({row:e})=>[e.category_name?(n(),p(f,{key:0,size:"small",type:"info"},{default:o(()=>[d(u(e.category_name),1)]),_:2},1024)):(n(),r("span",$,"--"))]),_:1}),a(s,{label:"分级",width:"100"},{default:o(({row:e})=>[e.level_name?(n(),p(f,{key:0,size:"small",color:e.level_color,effect:"dark"},{default:o(()=>[d(u(e.level_name),1)]),_:2},1032,["color"])):(n(),r("span",ee,"--"))]),_:1}),a(s,{label:"来源",width:"90"},{default:o(({row:e})=>[a(f,{size:"small",type:e.source==="auto"?"warning":"success"},{default:o(()=>[d(u(e.source==="auto"?"自动":"人工"),1)]),_:2},1032,["type"])]),_:1}),a(s,{prop:"confidence",label:"置信度",width:"90"},{default:o(({row:e})=>[e.confidence>0?(n(),r("span",{key:0,class:Q(I(e.confidence))},u((e.confidence*100).toFixed(0))+"% ",3)):(n(),r("span",ae,"--"))]),_:1})]),_:1},8,["data"])),[[T,g.value]]),v("div",te,[a(N,{"current-page":_.value,"onUpdate:currentPage":t[3]||(t[3]=e=>_.value=e),"page-size":b.value,"onUpdate:pageSize":t[4]||(t[4]=e=>b.value=e),total:w.value,layout:"total, prev, pager, next",onChange:y},null,8,["current-page","page-size","total"])])])])}}}),ce=R(le,[["__scopeId","data-v-bd46f99b"]]);export{ce as default};
//# sourceMappingURL=Classification-DrtlaA73.js.map
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
-1
View File
@@ -1 +0,0 @@
.dashboard .page-title[data-v-ad5b3874]{font-size:20px;font-weight:600;margin-bottom:20px;color:#303133}.stat-row[data-v-ad5b3874]{margin-bottom:16px}.stat-card[data-v-ad5b3874]{display:flex;align-items:center;padding:20px;margin-bottom:16px}.stat-card .stat-icon[data-v-ad5b3874]{width:56px;height:56px;border-radius:12px;display:flex;align-items:center;justify-content:center;margin-right:16px;flex-shrink:0}.stat-card .stat-info .stat-value[data-v-ad5b3874]{font-size:24px;font-weight:700;color:#303133;line-height:1.2}.stat-card .stat-info .stat-label[data-v-ad5b3874]{font-size:13px;color:#909399;margin-top:4px}.chart-row[data-v-ad5b3874]{margin-bottom:16px}.chart-card[data-v-ad5b3874]{padding:20px;margin-bottom:16px}.chart-card .chart-title[data-v-ad5b3874]{font-size:16px;font-weight:600;margin-bottom:16px;color:#303133}.chart-card .chart[data-v-ad5b3874]{width:100%;height:300px}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-2
View File
@@ -1,2 +0,0 @@
import{d as H,u as J,n as K,p as O,o as u,q as i,w as e,r as a,k as L,a as l,b as t,g as c,h as R,c as B,s as D,F as I,t as N,v as Q,x as p,y as X,z as Y,j as Z,A as v,f as ee,B as te,C as S,_ as oe}from"./index-DIl-pxgT.js";const ne={class:"logo"},ae={class:"header-left"},le={class:"header-title"},se={class:"header-right"},de={class:"user-info"},ue={class:"username"},re={class:"logo"},ie=H({__name:"Layout",setup(ce){const w=te(),h=ee(),_=J(),m=L(!1),x=L(window.innerWidth),g=v(()=>x.value<768),y=v(()=>w.path),U=v(()=>{var s;return((s=w.meta)==null?void 0:s.title)||"PropDataGuard"}),b=v(()=>{const s=h.getRoutes().find(o=>o.name==="Layout");return(s==null?void 0:s.children.filter(o=>{var d;return(d=o.meta)==null?void 0:d.title}))||[]});function E(s){s==="logout"&&(_.logout(),h.push("/login"))}function k(){x.value=window.innerWidth}return K(()=>{window.addEventListener("resize",k),!_.userInfo&&_.token&&_.fetchUserInfo()}),O(()=>{window.removeEventListener("resize",k)}),(s,o)=>{const d=a("el-icon"),z=a("el-menu-item"),C=a("el-menu"),G=a("el-aside"),P=a("el-button"),W=a("el-avatar"),F=a("el-dropdown-item"),M=a("el-dropdown-menu"),T=a("el-dropdown"),$=a("el-header"),j=a("router-view"),q=a("el-main"),V=a("el-container"),A=a("el-drawer");return u(),i(V,{class:"layout-container"},{default:e(()=>[g.value?N("",!0):(u(),i(G,{key:0,width:"220px",class:"layout-aside"},{default:e(()=>[l("div",ne,[t(d,{size:"28",color:"#fff"},{default:e(()=>[t(c(R))]),_:1}),o[3]||(o[3]=l("span",{class:"logo-text"},"PropDataGuard",-1))]),t(C,{"default-active":y.value,router:"","background-color":"#1a2b4a","text-color":"#b0c4de","active-text-color":"#fff",class:"layout-menu"},{default:e(()=>[(u(!0),B(I,null,D(b.value,n=>(u(),i(z,{key:n.path,index:n.path},{default:e(()=>{var r;return[t(d,null,{default:e(()=>{var f;return[(u(),i(S((f=n.meta)==null?void 0:f.icon)))]}),_:2},1024),l("span",null,p((r=n.meta)==null?void 0:r.title),1)]}),_:2},1032,["index"]))),128))]),_:1},8,["default-active"])]),_:1})),t(V,{direction:"vertical"},{default:e(()=>[t($,{class:"layout-header",height:"56px"},{default:e(()=>[l("div",ae,[g.value?(u(),i(P,{key:0,type:"primary",text:"",onClick:o[0]||(o[0]=n=>m.value=!0)},{default:e(()=>[t(d,{size:"22"},{default:e(()=>[t(c(Q))]),_:1})]),_:1})):N("",!0),l("span",le,p(U.value),1)]),l("div",se,[t(T,{onCommand:E},{dropdown:e(()=>[t(M,null,{default:e(()=>[t(F,{command:"logout"},{default:e(()=>[...o[4]||(o[4]=[Z("退出登录",-1)])]),_:1})]),_:1})]),default:e(()=>{var n,r;return[l("span",de,[t(W,{size:32,icon:c(X)},null,8,["icon"]),l("span",ue,p(((n=c(_).userInfo)==null?void 0:n.real_name)||((r=c(_).userInfo)==null?void 0:r.username)),1),t(d,null,{default:e(()=>[t(c(Y))]),_:1})])]}),_:1})])]),_:1}),t(q,{class:"layout-main"},{default:e(()=>[t(j)]),_:1})]),_:1}),t(A,{modelValue:m.value,"onUpdate:modelValue":o[2]||(o[2]=n=>m.value=n),direction:"ltr",size:"220px","with-header":!1,class:"mobile-drawer"},{default:e(()=>[l("div",re,[t(d,{size:"28",color:"#fff"},{default:e(()=>[t(c(R))]),_:1}),o[5]||(o[5]=l("span",{class:"logo-text"},"PropDataGuard",-1))]),t(C,{"default-active":y.value,router:"","background-color":"#1a2b4a","text-color":"#b0c4de","active-text-color":"#fff",class:"layout-menu",onSelect:o[1]||(o[1]=n=>m.value=!1)},{default:e(()=>[(u(!0),B(I,null,D(b.value,n=>(u(),i(z,{key:n.path,index:n.path},{default:e(()=>{var r;return[t(d,null,{default:e(()=>{var f;return[(u(),i(S((f=n.meta)==null?void 0:f.icon)))]}),_:2},1024),l("span",null,p((r=n.meta)==null?void 0:r.title),1)]}),_:2},1032,["index"]))),128))]),_:1},8,["default-active"])]),_:1},8,["modelValue"])]),_:1})}}}),fe=oe(ie,[["__scopeId","data-v-6b05a74f"]]);export{fe as default};
//# sourceMappingURL=Layout-DzZsTvlW.js.map
File diff suppressed because one or more lines are too long
-2
View File
@@ -1,2 +0,0 @@
import{d as V,u as k,o as h,c as C,a,b as o,w as l,e as B,r as n,f as E,g as u,h as L,i as N,l as R,j as U,k as f,m as q,E as _,_ as z}from"./index-DIl-pxgT.js";const K={class:"login-page"},S={class:"login-box card-shadow"},j={class:"login-header"},D=V({__name:"Login",setup(G){const g=E(),w=k(),r=f(!1),p=f(),s=q({username:"admin",password:"admin123"}),v={username:[{required:!0,message:"请输入用户名",trigger:"blur"}],password:[{required:!0,message:"请输入密码",trigger:"blur"}]};async function c(){var e;if(await((e=p.value)==null?void 0:e.validate().catch(()=>!1))){r.value=!0;try{await w.login(s.username,s.password),_.success("登录成功"),g.push("/")}catch(t){_.error((t==null?void 0:t.message)||"登录失败")}finally{r.value=!1}}}return(b,e)=>{const t=n("el-icon"),m=n("el-input"),i=n("el-form-item"),x=n("el-button"),y=n("el-form");return h(),C("div",K,[a("div",S,[a("div",j,[o(t,{size:"48",color:"#1a2b4a"},{default:l(()=>[o(u(L))]),_:1}),e[2]||(e[2]=a("h1",{class:"login-title"},"PropDataGuard",-1)),e[3]||(e[3]=a("p",{class:"login-subtitle"},"财险数据分级分类管理平台",-1))]),o(y,{ref_key:"formRef",ref:p,model:s,rules:v,size:"large",onKeyup:B(c,["enter"])},{default:l(()=>[o(i,{prop:"username"},{default:l(()=>[o(m,{modelValue:s.username,"onUpdate:modelValue":e[0]||(e[0]=d=>s.username=d),placeholder:"用户名","prefix-icon":u(N),clearable:""},null,8,["modelValue","prefix-icon"])]),_:1}),o(i,{prop:"password"},{default:l(()=>[o(m,{modelValue:s.password,"onUpdate:modelValue":e[1]||(e[1]=d=>s.password=d),type:"password",placeholder:"密码","prefix-icon":u(R),"show-password":"",clearable:""},null,8,["modelValue","prefix-icon"])]),_:1}),o(i,null,{default:l(()=>[o(x,{type:"primary",loading:r.value,class:"login-btn",onClick:c},{default:l(()=>[...e[4]||(e[4]=[U(" 登录 ",-1)])]),_:1},8,["loading"])]),_:1})]),_:1},8,["model"]),e[5]||(e[5]=a("div",{class:"login-footer"},[a("p",null,"默认管理员:admin / admin123")],-1))])])}}}),M=z(D,[["__scopeId","data-v-8c2e034d"]]);export{M as default};
//# sourceMappingURL=Login-Blg9KWw-.js.map
File diff suppressed because one or more lines are too long
-2
View File
@@ -1,2 +0,0 @@
import{K as T,d as I,L as D,n as O,o,c as v,a as c,b as a,w as l,E as A,r as d,k as i,q as u,g as b,M as F,N as H,O as J,x as p,t as k,j as B,D as P,G as Q,_ as W}from"./index-DIl-pxgT.js";function X(f){return T.get("/metadata/tree",{params:{source_id:f}})}function Y(f){return T.get("/metadata/columns",{params:f})}const Z={class:"page-container metadata-page"},$={class:"tree-card card-shadow"},ee={class:"custom-tree-node"},te={class:"node-label"},ae={key:3,class:"node-badge"},le={class:"detail-card card-shadow"},oe={class:"detail-header"},se={class:"detail-title"},ne={key:0},ce={key:1,class:"placeholder"},de={class:"sample-text"},re=I({__name:"Metadata",setup(f){const x=i([]),y=i(""),C=i(),s=i(null),V=i([]),w=i(!1),h=i("");D(y,e=>{var t;(t=C.value)==null||t.filter(e)});function E(e,t){return e?t.name.toLowerCase().includes(e.toLowerCase()):!0}async function K(){try{const e=await X();x.value=e||[]}catch(e){A.error((e==null?void 0:e.message)||"加载失败")}}async function q(e){e.type==="table"&&(s.value=e,await z(e.id))}async function z(e){w.value=!0;try{const t=await Y({table_id:e,keyword:h.value||void 0,page:1,page_size:500});V.value=t.data||[]}finally{w.value=!1}}return D(h,()=>{s.value&&z(s.value.id)}),O(K),(e,t)=>{const N=d("el-input"),g=d("el-icon"),R=d("el-tree"),L=d("el-col"),M=d("el-tag"),m=d("el-table-column"),S=d("el-table"),U=d("el-empty"),j=d("el-row"),G=Q("loading");return o(),v("div",Z,[t[3]||(t[3]=c("h2",{class:"page-title"},"数据资产",-1)),a(j,{gutter:16,class:"content-row"},{default:l(()=>[a(L,{xs:24,md:7,lg:6},{default:l(()=>[c("div",$,[t[2]||(t[2]=c("div",{class:"tree-header"},"资产目录",-1)),a(N,{modelValue:y.value,"onUpdate:modelValue":t[0]||(t[0]=_=>y.value=_),placeholder:"搜索表名",clearable:"",size:"small",class:"tree-search"},null,8,["modelValue"]),a(R,{data:x.value,props:{children:"children",label:"name"},"filter-node-method":E,ref_key:"treeRef",ref:C,"highlight-current":"","default-expand-all":"",onNodeClick:q},{default:l(({node:_,data:r})=>{var n;return[c("span",ee,[r.type==="source"?(o(),u(g,{key:0,size:"14"},{default:l(()=>[a(b(F))]),_:1})):r.type==="database"?(o(),u(g,{key:1,size:"14"},{default:l(()=>[a(b(H))]),_:1})):(o(),u(g,{key:2,size:"14"},{default:l(()=>[a(b(J))]),_:1})),c("span",te,p(_.label),1),r.type==="table"&&((n=r.meta)!=null&&n.column_count)?(o(),v("span",ae,p(r.meta.column_count),1)):k("",!0)])]}),_:1},8,["data"])])]),_:1}),a(L,{xs:24,md:17,lg:18},{default:l(()=>{var _,r;return[c("div",le,[c("div",oe,[c("div",se,[s.value?(o(),v("span",ne,p(s.value.name),1)):(o(),v("span",ce,"请从左侧选择数据表")),(r=(_=s.value)==null?void 0:_.meta)!=null&&r.row_count?(o(),u(M,{key:2,size:"small",type:"info"},{default:l(()=>[B(" 约 "+p(s.value.meta.row_count.toLocaleString())+" 行 ",1)]),_:1})):k("",!0)]),s.value?(o(),u(N,{key:0,modelValue:h.value,"onUpdate:modelValue":t[1]||(t[1]=n=>h.value=n),placeholder:"搜索字段",clearable:"",size:"small",style:{width:"200px"}},null,8,["modelValue"])):k("",!0)]),s.value?P((o(),u(S,{key:0,data:V.value,stripe:"",size:"default",height:"calc(100vh - 280px)"},{default:l(()=>[a(m,{prop:"name",label:"字段名","min-width":"140"}),a(m,{prop:"data_type",label:"类型",width:"120"}),a(m,{prop:"comment",label:"注释","min-width":"180","show-overflow-tooltip":""}),a(m,{prop:"is_nullable",label:"可空",width:"80"},{default:l(({row:n})=>[a(M,{size:"small",type:n.is_nullable?"info":"success"},{default:l(()=>[B(p(n.is_nullable?"是":"否"),1)]),_:2},1032,["type"])]),_:1}),a(m,{prop:"sample_data",label:"采样数据","min-width":"200","show-overflow-tooltip":""},{default:l(({row:n})=>[c("span",de,p(n.sample_data),1)]),_:1})]),_:1},8,["data"])),[[G,w.value]]):(o(),u(U,{key:1,description:"请选择左侧数据表查看字段详情"}))])]}),_:1})]),_:1})])}}}),ue=W(re,[["__scopeId","data-v-866ea4fc"]]);export{ue as default};
//# sourceMappingURL=Metadata-DQHjLzoq.js.map
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
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
.page-title[data-v-5f4e75fc]{font-size:20px;font-weight:600;margin-bottom:20px;color:#303133}.chart-row[data-v-5f4e75fc]{margin-bottom:16px}.chart-card[data-v-5f4e75fc]{padding:20px;margin-bottom:16px;background:#fff;border-radius:8px}.chart-card .chart-title[data-v-5f4e75fc]{font-size:16px;font-weight:600;margin-bottom:16px;color:#303133}.chart-card .chart[data-v-5f4e75fc]{width:100%;height:300px}
-2
View File
@@ -1,2 +0,0 @@
import{d as I,n as S,o as d,c as y,a as i,b as e,w as a,k as c,E as U,r as s,g as E,I as L,j as _,D as N,q as w,x as m,F as j,s as F,G as M,_ as T}from"./index-DIl-pxgT.js";const $={class:"page-container"},q={class:"table-card card-shadow"},A={class:"table-header"},G={class:"table-card card-shadow"},K=I({__name:"System",setup(R){const f=c("users"),b=c([]),u=c(!1),p=c("");async function v(){u.value=!0;try{const t=await(await fetch(`/api/v1/users?keyword=${encodeURIComponent(p.value)}`,{headers:{Authorization:`Bearer ${localStorage.getItem("pdg_token")||""}`}})).json();b.value=t.data||[]}catch(n){U.error((n==null?void 0:n.message)||"加载失败")}finally{u.value=!1}}return S(v),(n,t)=>{const V=s("el-input"),k=s("el-icon"),x=s("el-button"),o=s("el-table-column"),g=s("el-tag"),z=s("el-table"),h=s("el-tab-pane"),B=s("el-empty"),C=s("el-tabs"),D=M("loading");return d(),y("div",$,[t[3]||(t[3]=i("h2",{class:"page-title"},"系统管理",-1)),e(C,{modelValue:f.value,"onUpdate:modelValue":t[1]||(t[1]=l=>f.value=l),class:"system-tabs"},{default:a(()=>[e(h,{label:"用户管理",name:"users"},{default:a(()=>[i("div",q,[i("div",A,[e(V,{modelValue:p.value,"onUpdate:modelValue":t[0]||(t[0]=l=>p.value=l),placeholder:"搜索用户",clearable:"",style:{width:"220px"}},null,8,["modelValue"]),e(x,{type:"primary",size:"small",onClick:v},{default:a(()=>[e(k,null,{default:a(()=>[e(E(L))]),_:1}),t[2]||(t[2]=_("查询 ",-1))]),_:1})]),N((d(),w(z,{data:b.value,stripe:"",size:"default"},{default:a(()=>[e(o,{prop:"username",label:"用户名","min-width":"120"}),e(o,{prop:"real_name",label:"真实姓名","min-width":"120"}),e(o,{prop:"email",label:"邮箱","min-width":"160"}),e(o,{prop:"dept.name",label:"部门","min-width":"120"},{default:a(({row:l})=>{var r;return[_(m(((r=l.dept)==null?void 0:r.name)||"--"),1)]}),_:1}),e(o,{prop:"roles",label:"角色","min-width":"180"},{default:a(({row:l})=>[(d(!0),y(j,null,F(l.roles,r=>(d(),w(g,{key:r.id,size:"small",style:{"margin-right":"4px"}},{default:a(()=>[_(m(r.name),1)]),_:2},1024))),128))]),_:1}),e(o,{prop:"is_active",label:"状态",width:"90"},{default:a(({row:l})=>[e(g,{type:l.is_active?"success":"danger",size:"small"},{default:a(()=>[_(m(l.is_active?"正常":"禁用"),1)]),_:2},1032,["type"])]),_:1})]),_:1},8,["data"])),[[D,u.value]])])]),_:1}),e(h,{label:"操作日志",name:"logs"},{default:a(()=>[i("div",G,[e(B,{description:"日志功能开发中"})])]),_:1})]),_:1},8,["modelValue"])])}}}),J=T(K,[["__scopeId","data-v-d05c8e34"]]);export{J as default};
//# sourceMappingURL=System-BEPbsg-j.js.map
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
.page-title[data-v-ac23f7b2]{font-size:20px;font-weight:600;margin-bottom:16px;color:#303133}.task-tabs[data-v-ac23f7b2]{background:#fff;padding:16px;border-radius:8px}.label-header[data-v-ac23f7b2]{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.empty-text[data-v-ac23f7b2]{color:#c0c4cc}.inline-select[data-v-ac23f7b2] .el-input__wrapper{padding:0 4px}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-2
View File
@@ -1,2 +0,0 @@
import{K as t}from"./index-DIl-pxgT.js";function i(){return t.get("/classifications/categories/tree")}function n(e){return t.post("/classifications/categories",e)}function r(e,s){return t.put(`/classifications/categories/${e}`,s)}function c(e){return t.delete(`/classifications/categories/${e}`)}function u(){return t.get("/classifications/levels")}function o(e){return t.get("/classifications/rules",{params:e})}function l(e){return t.post("/classifications/rules",e)}function f(e,s){return t.put(`/classifications/rules/${e}`,s)}function g(e){return t.delete(`/classifications/rules/${e}`)}function p(){return t.get("/classifications/templates")}export{i as a,o as b,p as c,n as d,f as e,l as f,u as g,g as h,c as i,r as u};
//# sourceMappingURL=classification-CUKwOuh8.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"classification-CUKwOuh8.js","sources":["../../src/api/classification.ts"],"sourcesContent":["import request from './request'\n\nexport interface CategoryItem {\n id: number\n parent_id?: number\n level: number\n code: string\n name: string\n description?: string\n sort_order: number\n children?: CategoryItem[]\n}\n\nexport interface DataLevel {\n id: number\n code: string\n name: string\n description?: string\n color: string\n control_requirements?: Record<string, any>\n}\n\nexport interface RecognitionRule {\n id: number\n template_id: number\n category_id?: number\n level_id?: number\n rule_type: string\n rule_name?: string\n rule_content: string\n target_field: string\n priority: number\n is_active: boolean\n hit_count: number\n category_name?: string\n level_name?: string\n level_color?: string\n}\n\nexport function getCategoryTree() {\n return request.get('/classifications/categories/tree')\n}\n\nexport function createCategory(data: Partial<CategoryItem>) {\n return request.post('/classifications/categories', data)\n}\n\nexport function updateCategory(id: number, data: Partial<CategoryItem>) {\n return request.put(`/classifications/categories/${id}`, data)\n}\n\nexport function deleteCategory(id: number) {\n return request.delete(`/classifications/categories/${id}`)\n}\n\nexport function getDataLevels() {\n return request.get('/classifications/levels')\n}\n\nexport function getRules(params?: { template_id?: number; keyword?: string; page?: number; page_size?: number }) {\n return request.get('/classifications/rules', { params })\n}\n\nexport function createRule(data: Partial<RecognitionRule>) {\n return request.post('/classifications/rules', data)\n}\n\nexport function updateRule(id: number, data: Partial<RecognitionRule>) {\n return request.put(`/classifications/rules/${id}`, data)\n}\n\nexport function deleteRule(id: number) {\n return request.delete(`/classifications/rules/${id}`)\n}\n\nexport function getTemplates() {\n return request.get('/classifications/templates')\n}\n"],"names":["getCategoryTree","request","createCategory","data","updateCategory","id","deleteCategory","getDataLevels","getRules","params","createRule","updateRule","deleteRule","getTemplates"],"mappings":"wCAuCO,SAASA,GAAkB,CAChC,OAAOC,EAAQ,IAAI,kCAAkC,CACvD,CAEO,SAASC,EAAeC,EAA6B,CAC1D,OAAOF,EAAQ,KAAK,8BAA+BE,CAAI,CACzD,CAEO,SAASC,EAAeC,EAAYF,EAA6B,CACtE,OAAOF,EAAQ,IAAI,+BAA+BI,CAAE,GAAIF,CAAI,CAC9D,CAEO,SAASG,EAAeD,EAAY,CACzC,OAAOJ,EAAQ,OAAO,+BAA+BI,CAAE,EAAE,CAC3D,CAEO,SAASE,GAAgB,CAC9B,OAAON,EAAQ,IAAI,yBAAyB,CAC9C,CAEO,SAASO,EAASC,EAAwF,CAC/G,OAAOR,EAAQ,IAAI,yBAA0B,CAAE,OAAAQ,EAAQ,CACzD,CAEO,SAASC,EAAWP,EAAgC,CACzD,OAAOF,EAAQ,KAAK,yBAA0BE,CAAI,CACpD,CAEO,SAASQ,EAAWN,EAAYF,EAAgC,CACrE,OAAOF,EAAQ,IAAI,0BAA0BI,CAAE,GAAIF,CAAI,CACzD,CAEO,SAASS,EAAWP,EAAY,CACrC,OAAOJ,EAAQ,OAAO,0BAA0BI,CAAE,EAAE,CACtD,CAEO,SAASQ,GAAe,CAC7B,OAAOZ,EAAQ,IAAI,4BAA4B,CACjD"}
-2
View File
@@ -1,2 +0,0 @@
import{K as e}from"./index-DIl-pxgT.js";function n(t){return e.get("/datasources",{params:t})}function r(t){return e.post("/datasources",t)}function o(t,a){return e.put(`/datasources/${t}`,a)}function u(t){return e.delete(`/datasources/${t}`)}function c(t){return e.post("/datasources/test-connection",t)}function d(t){return e.post(`/metadata/sync/${t}`)}export{r as c,u as d,n as g,d as s,c as t,o as u};
//# sourceMappingURL=datasource-idO2fJD3.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"datasource-idO2fJD3.js","sources":["../../src/api/datasource.ts"],"sourcesContent":["import request from './request'\n\nexport interface DataSourceItem {\n id: number\n name: string\n source_type: string\n host?: string\n port?: number\n database_name?: string\n username?: string\n status: string\n created_at: string\n}\n\nexport interface DataSourceForm {\n name: string\n source_type: string\n host?: string\n port?: number\n database_name?: string\n username?: string\n password?: string\n extra_params?: string\n dept_id?: number\n}\n\nexport function getDataSources(params?: { page?: number; page_size?: number; keyword?: string }) {\n return request.get('/datasources', { params })\n}\n\nexport function createDataSource(data: DataSourceForm) {\n return request.post('/datasources', data)\n}\n\nexport function updateDataSource(id: number, data: Partial<DataSourceForm>) {\n return request.put(`/datasources/${id}`, data)\n}\n\nexport function deleteDataSource(id: number) {\n return request.delete(`/datasources/${id}`)\n}\n\nexport function testConnection(data: Partial<DataSourceForm>) {\n return request.post('/datasources/test-connection', data)\n}\n\nexport function syncMetadata(sourceId: number) {\n return request.post(`/metadata/sync/${sourceId}`)\n}\n"],"names":["getDataSources","params","request","createDataSource","data","updateDataSource","id","deleteDataSource","testConnection","syncMetadata","sourceId"],"mappings":"wCA0BO,SAASA,EAAeC,EAAkE,CAC/F,OAAOC,EAAQ,IAAI,eAAgB,CAAE,OAAAD,EAAQ,CAC/C,CAEO,SAASE,EAAiBC,EAAsB,CACrD,OAAOF,EAAQ,KAAK,eAAgBE,CAAI,CAC1C,CAEO,SAASC,EAAiBC,EAAYF,EAA+B,CAC1E,OAAOF,EAAQ,IAAI,gBAAgBI,CAAE,GAAIF,CAAI,CAC/C,CAEO,SAASG,EAAiBD,EAAY,CAC3C,OAAOJ,EAAQ,OAAO,gBAAgBI,CAAE,EAAE,CAC5C,CAEO,SAASE,EAAeJ,EAA+B,CAC5D,OAAOF,EAAQ,KAAK,+BAAgCE,CAAI,CAC1D,CAEO,SAASK,EAAaC,EAAkB,CAC7C,OAAOR,EAAQ,KAAK,kBAAkBQ,CAAQ,EAAE,CAClD"}
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
-2
View File
@@ -1,2 +0,0 @@
import{K as e}from"./index-DIl-pxgT.js";function o(t){return e.get("/projects",{params:t})}function s(t){return e.post("/projects",null,{params:t})}function c(t){return e.delete(`/projects/${t}`)}function a(t){return e.post(`/projects/${t}/auto-classify`)}export{a,s as c,c as d,o as g};
//# sourceMappingURL=project-DH-EQEsQ.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"project-DH-EQEsQ.js","sources":["../../src/api/project.ts"],"sourcesContent":["import request from './request'\n\nexport interface ProjectItem {\n id: number\n name: string\n template_id: number\n status: string\n description?: string\n target_source_ids?: string\n planned_start?: string\n planned_end?: string\n created_at: string\n stats?: {\n total: number\n auto: number\n manual: number\n reviewed: number\n }\n}\n\nexport function getProjects(params?: { page?: number; page_size?: number; keyword?: string }) {\n return request.get('/projects', { params })\n}\n\nexport function getProject(id: number) {\n return request.get(`/projects/${id}`)\n}\n\nexport function createProject(data: { name: string; template_id: number; target_source_ids?: string; description?: string }) {\n return request.post('/projects', null, { params: data })\n}\n\nexport function deleteProject(id: number) {\n return request.delete(`/projects/${id}`)\n}\n\nexport function autoClassifyProject(id: number) {\n return request.post(`/projects/${id}/auto-classify`)\n}\n"],"names":["getProjects","params","request","createProject","data","deleteProject","id","autoClassifyProject"],"mappings":"wCAoBO,SAASA,EAAYC,EAAkE,CAC5F,OAAOC,EAAQ,IAAI,YAAa,CAAE,OAAAD,EAAQ,CAC5C,CAMO,SAASE,EAAcC,EAA+F,CAC3H,OAAOF,EAAQ,KAAK,YAAa,KAAM,CAAE,OAAQE,EAAM,CACzD,CAEO,SAASC,EAAcC,EAAY,CACxC,OAAOJ,EAAQ,OAAO,aAAaI,CAAE,EAAE,CACzC,CAEO,SAASC,EAAoBD,EAAY,CAC9C,OAAOJ,EAAQ,KAAK,aAAaI,CAAE,gBAAgB,CACrD"}
+1 -1
View File
@@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>PropDataGuard - 财险数据分级分类平台</title>
<script type="module" crossorigin src="/assets/index-DIl-pxgT.js"></script>
<script type="module" crossorigin src="/assets/index-DveMB2K5.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-s_XEM0GP.css">
</head>
<body>
+9
View File
@@ -57,3 +57,12 @@ DEPRECATION WARNING [legacy-js-api]: The legacy JS API is deprecated and will be
More info: https://sass-lang.com/d/legacy-js-api
17:11:45 [vite] hmr update /src/views/task/Task.vue
17:13:45 [vite] hmr update /src/views/task/Task.vue, /src/views/task/Task.vue?vue&type=style&index=0&scoped=71d31924&lang.scss
DEPRECATION WARNING [legacy-js-api]: The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0.
More info: https://sass-lang.com/d/legacy-js-api
17:15:44 [vite] hmr update /src/views/dashboard/Dashboard.vue, /src/views/dashboard/Dashboard.vue?vue&type=style&index=0&scoped=1c6c8216&lang.scss
17:18:49 [vite] hmr update /src/views/report/Report.vue, /src/views/report/Report.vue?vue&type=style&index=0&scoped=977d41b6&lang.scss
17:19:54 [vite] hmr update /src/views/classification/Classification.vue
+19 -2
View File
@@ -4,6 +4,7 @@ export interface TaskItem {
id: number
name: string
project_id: number
project_name?: string
status: string
deadline?: string
created_at: string
@@ -32,6 +33,22 @@ export function getMyTasks(params?: { status?: string }) {
return request.get('/tasks/my-tasks', { params })
}
export function getTaskItems(taskId: number) {
return request.get(`/tasks/my-tasks/${taskId}/items`)
export function getTaskItems(taskId: number, params?: { keyword?: string }) {
return request.get(`/tasks/my-tasks/${taskId}/items`, { params })
}
export function startTask(taskId: number) {
return request.post(`/tasks/my-tasks/${taskId}/start`)
}
export function completeTask(taskId: number) {
return request.post(`/tasks/my-tasks/${taskId}/complete`)
}
export function labelResult(resultId: number, data: { category_id: number; level_id: number }) {
return request.post(`/tasks/results/${resultId}/label`, null, { params: data })
}
export function createTask(projectId: number, data: { name: string; assignee_id: number; target_type?: string }) {
return request.post(`/tasks/projects/${projectId}/create-task`, null, { params: data })
}
@@ -76,6 +76,7 @@ import { ElMessage } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import { getProjects } from '@/api/project'
import { getDataLevels } from '@/api/classification'
import { getTaskItems } from '@/api/task'
const loading = ref(false)
const resultList = ref<any[]>([])
@@ -99,12 +100,14 @@ function confidenceClass(v: number) {
async function fetchData() {
loading.value = true
try {
// Use project API to get results (simplified for demo)
const params: any = { page: page.value, page_size: pageSize.value }
if (filterKeyword.value) params.keyword = filterKeyword.value
// In full implementation, call dedicated results API
const res: any = await getProjects(params)
// Mock data for demo
if (!filterProjectId.value) {
resultList.value = []
total.value = 0
return
}
const res: any = await getTaskItems(-1) // use a special endpoint or fetch from project
// Actually we need a dedicated results API. For now, fetch all items for project via task workaround
// In real implementation, call: GET /api/v1/projects/{id}/results
resultList.value = []
total.value = 0
} catch (e: any) {
@@ -131,7 +134,6 @@ async function fetchMeta() {
onMounted(() => {
fetchMeta()
fetchData()
})
</script>
+50 -6
View File
@@ -85,18 +85,28 @@
</div>
</el-col>
</el-row>
<el-row :gutter="16" class="chart-row">
<el-col :xs="24">
<div class="chart-card card-shadow">
<div class="chart-title">敏感数据分布热力图按数据源</div>
<v-chart class="chart" :option="heatmapOption" autoresize />
</div>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart, BarChart } from 'echarts/charts'
import { TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
import { PieChart, BarChart, HeatmapChart } from 'echarts/charts'
import { TooltipComponent, LegendComponent, GridComponent, VisualMapComponent } from 'echarts/components'
import VChart from 'vue-echarts'
import { getProjects } from '@/api/project'
use([CanvasRenderer, PieChart, BarChart, TooltipComponent, LegendComponent, GridComponent])
use([CanvasRenderer, PieChart, BarChart, HeatmapChart, TooltipComponent, LegendComponent, GridComponent, VisualMapComponent])
const stats = reactive({
dataSources: 12,
@@ -144,6 +154,20 @@ const categoryOption = ref({
],
})
const heatmapOption = ref({
tooltip: { position: 'top' },
grid: { height: '50%', top: '10%' },
xAxis: { type: 'category', data: ['L1', 'L2', 'L3', 'L4', 'L5'], splitArea: { show: true } },
yAxis: { type: 'category', data: ['核心系统', '理赔系统', '保单系统', '财务系统', '渠道系统'], splitArea: { show: true } },
visualMap: { min: 0, max: 10000, calculable: true, orient: 'horizontal', left: 'center', bottom: '15%', inRange: { color: ['#e0f3f8', '#abd9e9', '#74add1', '#4575b4', '#313695'] } },
series: [{
type: 'heatmap',
data: [[0,0,500],[0,1,1200],[0,2,800],[0,3,600],[0,4,400],[1,0,2000],[1,1,3500],[1,2,2800],[1,3,2200],[1,4,1800],[2,0,1500],[2,1,4200],[2,2,3100],[2,3,2600],[2,4,1400],[3,0,800],[3,1,5800],[3,2,2100],[3,3,1900],[3,4,1200],[4,0,200],[4,1,900],[4,2,400],[4,3,300],[4,4,100]],
label: { show: true },
emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
}]
})
const projectList = ref([
{ name: '2024年数据分类分级专项', status: 'labeling', progress: 68, planned_end: '2024-08-30' },
{ name: '核心系统敏感数据梳理', status: 'reviewing', progress: 92, planned_end: '2024-07-15' },
@@ -175,6 +199,24 @@ function statusText(status: string) {
}
return map[status] || status
}
async function fetchProjects() {
try {
const res: any = await getProjects({ page: 1, page_size: 10 })
if (res?.data?.length) {
projectList.value = res.data.map((p: any) => ({
name: p.name,
status: p.status,
progress: p.stats?.total ? Math.round((p.stats.reviewed / p.stats.total) * 100) : 0,
planned_end: p.planned_end ? p.planned_end.slice(0, 10) : '--',
}))
}
} catch (e) {
// ignore
}
}
onMounted(fetchProjects)
</script>
<style scoped lang="scss">
@@ -195,7 +237,8 @@ function statusText(status: string) {
display: flex;
align-items: center;
padding: 20px;
margin-bottom: 16px;
background: #fff;
border-radius: 8px;
.stat-icon {
width: 56px;
@@ -229,7 +272,8 @@ function statusText(status: string) {
.chart-card {
padding: 20px;
margin-bottom: 16px;
background: #fff;
border-radius: 8px;
.chart-title {
font-size: 16px;
+53 -7
View File
@@ -1,6 +1,14 @@
<template>
<div class="page-container">
<h2 class="page-title">报表统计</h2>
<div class="page-header">
<h2 class="page-title">报表统计</h2>
<el-select v-model="selectedProject" placeholder="选择项目生成报告" clearable style="width: 260px">
<el-option v-for="p in projects" :key="p.id" :label="p.name" :value="p.id" />
</el-select>
<el-button type="primary" :disabled="!selectedProject" @click="downloadReport">
<el-icon><Download /></el-icon>下载报告
</el-button>
</div>
<el-row :gutter="16" class="chart-row">
<el-col :xs="24" :md="12">
@@ -35,15 +43,20 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart, BarChart, LineChart } from 'echarts/charts'
import { TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
import VChart from 'vue-echarts'
import { Download } from '@element-plus/icons-vue'
import { getProjects } from '@/api/project'
use([CanvasRenderer, PieChart, BarChart, LineChart, TooltipComponent, LegendComponent, GridComponent])
const selectedProject = ref<number | undefined>(undefined)
const projects = ref<any[]>([])
const levelOption = ref({
tooltip: { trigger: 'item' },
legend: { bottom: '0%', left: 'center' },
@@ -114,14 +127,48 @@ const trendOption = ref({
},
],
})
function downloadReport() {
if (!selectedProject.value) return
const token = localStorage.getItem('pdg_token')
const url = `/api/v1/reports/projects/${selectedProject.value}/download`
const a = document.createElement('a')
a.href = url
a.download = `report_project_${selectedProject.value}.docx`
if (token) {
a.setAttribute('data-token', token)
}
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
async function fetchProjects() {
try {
const res: any = await getProjects({ page: 1, page_size: 100 })
projects.value = res?.data || []
} catch (e) {
// ignore
}
}
onMounted(fetchProjects)
</script>
<style scoped lang="scss">
.page-title {
font-size: 20px;
font-weight: 600;
.page-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
color: #303133;
flex-wrap: wrap;
.page-title {
font-size: 20px;
font-weight: 600;
color: #303133;
margin-right: auto;
}
}
.chart-row {
@@ -130,7 +177,6 @@ const trendOption = ref({
.chart-card {
padding: 20px;
margin-bottom: 16px;
background: #fff;
border-radius: 8px;
+161 -53
View File
@@ -4,80 +4,107 @@
<el-tabs v-model="activeTab" class="task-tabs">
<el-tab-pane label="待处理" name="pending">
<TaskTable :tasks="pendingTasks" @refresh="fetchData" />
<TaskTable :tasks="pendingTasks" @refresh="fetchData" @label="openLabel" />
</el-tab-pane>
<el-tab-pane label="进行中" name="in_progress">
<TaskTable :tasks="inProgressTasks" @refresh="fetchData" />
<TaskTable :tasks="inProgressTasks" @refresh="fetchData" @label="openLabel" />
</el-tab-pane>
<el-tab-pane label="已完成" name="completed">
<TaskTable :tasks="completedTasks" @refresh="fetchData" />
<TaskTable :tasks="completedTasks" @refresh="fetchData" @label="openLabel" />
</el-tab-pane>
</el-tabs>
<!-- Label Dialog -->
<el-dialog
v-model="labelDialogVisible"
title="数据打标"
width="90%"
top="5vh"
:title="`数据打标 - ${currentTask?.name || ''}`"
width="92%"
top="4vh"
destroy-on-close
class="label-dialog"
>
<div class="label-header">
<span> {{ labelItems.length }} 个字段</span>
<el-input v-model="labelKeyword" placeholder="搜索字段" clearable size="small" style="width: 200px" />
<div class="label-stats">
<span> {{ labelItems.length }} 个字段</span>
<el-tag type="success" size="small">已保存: {{ savedCount }}</el-tag>
<el-tag type="warning" size="small">待保存: {{ unsavedCount }}</el-tag>
</div>
<el-input v-model="labelKeyword" placeholder="搜索字段/表/注释" clearable size="small" style="width: 220px" />
</div>
<el-table :data="filteredLabelItems" height="60vh" stripe size="default" border>
<el-table
:data="filteredLabelItems"
height="calc(100vh - 260px)"
stripe
size="default"
border
@cell-click="handleCellClick"
>
<el-table-column prop="column_name" label="字段名" width="150" />
<el-table-column prop="data_type" label="类型" width="100" />
<el-table-column prop="comment" label="注释" min-width="150" show-overflow-tooltip />
<el-table-column prop="table_name" label="所属表" width="140" />
<el-table-column prop="source_name" label="数据源" width="120" />
<el-table-column label="当前分类" width="140">
<el-table-column prop="data_type" label="类型" width="90" />
<el-table-column prop="comment" label="注释" min-width="140" show-overflow-tooltip />
<el-table-column prop="table_name" label="所属表" width="130" />
<el-table-column prop="source_name" label="数据源" width="110" />
<el-table-column label="分类" width="150">
<template #default="{ row }">
<el-tag v-if="row.category_name" size="small">{{ row.category_name }}</el-tag>
<span v-else class="empty-text">--</span>
<el-select
v-model="row._category_id"
placeholder="分类"
size="small"
style="width: 130px"
@change="markUnsaved(row)"
>
<el-option
v-for="c in flatCategories"
:key="c.id"
:label="c.name"
:value="c.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="当前分级" width="100">
<el-table-column label="分级" width="110">
<template #default="{ row }">
<el-tag v-if="row.level_name" size="small" :color="row.level_color" effect="dark">{{ row.level_name }}</el-tag>
<span v-else class="empty-text">--</span>
<el-select
v-model="row._level_id"
placeholder="分级"
size="small"
style="width: 90px"
@change="markUnsaved(row)"
>
<el-option v-for="l in levels" :key="l.id" :value="l.id">
<el-tag size="small" :color="l.color" effect="dark">{{ l.code }}</el-tag>
</el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="来源" width="90">
<el-table-column label="来源" width="80">
<template #default="{ row }">
<el-tag size="small" :type="row.source === 'auto' ? 'warning' : 'success'">
{{ row.source === 'auto' ? '自动' : '人工' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<el-table-column label="置信度" width="80">
<template #default="{ row }">
<el-select
v-model="row._category_id"
placeholder="分类"
size="small"
style="width: 90px"
class="inline-select"
>
<el-option v-for="c in flatCategories" :key="c.id" :label="c.name" :value="c.id" />
</el-select>
<el-select
v-model="row._level_id"
placeholder="分级"
size="small"
style="width: 70px; margin-left: 4px"
class="inline-select"
>
<el-option v-for="l in levels" :key="l.id" :label="l.code" :value="l.id" />
</el-select>
<span v-if="row.confidence > 0" :class="confidenceClass(row.confidence)">
{{ (row.confidence * 100).toFixed(0) }}%
</span>
<span v-else class="empty-text">--</span>
</template>
</el-table-column>
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag v-if="row._unsaved" size="small" type="danger">待保存</el-tag>
<el-tag v-else size="small" type="success">已保存</el-tag>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="labelDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleBatchSave">批量保存</el-button>
<el-button @click="labelDialogVisible = false">关闭</el-button>
<el-button type="primary" :loading="saveLoading" @click="handleBatchSave">批量保存</el-button>
<el-button type="success" :loading="completeLoading" @click="handleCompleteTask">完成任务</el-button>
</template>
</el-dialog>
</div>
@@ -85,23 +112,29 @@
<script setup lang="ts">
import { ref, computed, onMounted, h } from 'vue'
import { ElMessage, ElButton } from 'element-plus'
import { getMyTasks, getTaskItems } from '@/api/task'
import { ElMessage, ElMessageBox, ElButton, ElTag, ElSelect, ElOption } from 'element-plus'
import { getMyTasks, getTaskItems, startTask, completeTask, labelResult } from '@/api/task'
import { getCategoryTree, getDataLevels } from '@/api/classification'
import type { TaskItem, TaskResultItem } from '@/api/task'
const activeTab = ref('pending')
const tasks = ref<TaskItem[]>([])
const currentTask = ref<TaskItem | null>(null)
const pendingTasks = computed(() => tasks.value.filter((t) => t.status === 'pending'))
const inProgressTasks = computed(() => tasks.value.filter((t) => t.status === 'in_progress'))
const completedTasks = computed(() => tasks.value.filter((t) => t.status === 'completed'))
const labelDialogVisible = ref(false)
const labelItems = ref<(TaskResultItem & { _category_id?: number; _level_id?: number })[]>([])
const labelItems = ref<(TaskResultItem & { _category_id?: number; _level_id?: number; _unsaved?: boolean })[]>([])
const labelKeyword = ref('')
const levels = ref<any[]>([])
const flatCategories = ref<any[]>([])
const saveLoading = ref(false)
const completeLoading = ref(false)
const savedCount = computed(() => labelItems.value.filter((i) => !i._unsaved).length)
const unsavedCount = computed(() => labelItems.value.filter((i) => i._unsaved).length)
const filteredLabelItems = computed(() => {
if (!labelKeyword.value) return labelItems.value
@@ -146,12 +179,18 @@ function flattenCategories(tree: any[]): any[] {
}
async function openLabel(task: TaskItem) {
currentTask.value = task
if (task.status === 'pending') {
try { await startTask(task.id) } catch (e) { /* ignore */ }
fetchData()
}
try {
const res: any = await getTaskItems(task.id)
labelItems.value = (res || []).map((item: any) => ({
...item,
_category_id: item.category_id,
_level_id: item.level_id,
_unsaved: false,
}))
labelDialogVisible.value = true
} catch (e: any) {
@@ -159,16 +198,71 @@ async function openLabel(task: TaskItem) {
}
}
function markUnsaved(row: any) {
row._unsaved = true
}
function handleCellClick(row: any, column: any) {
// Auto-focus for better mobile experience
}
async function handleBatchSave() {
ElMessage.success('保存成功(演示模式)')
labelDialogVisible.value = false
fetchData()
const unsaved = labelItems.value.filter((i) => i._unsaved && i._category_id && i._level_id)
if (unsaved.length === 0) {
ElMessage.info('没有需要保存的变更')
return
}
saveLoading.value = true
try {
for (const item of unsaved) {
await labelResult(item.result_id, {
category_id: item._category_id!,
level_id: item._level_id!,
})
item._unsaved = false
}
ElMessage.success(`成功保存 ${unsaved.length} 条记录`)
} catch (e: any) {
ElMessage.error(e?.message || '保存失败')
} finally {
saveLoading.value = false
}
}
async function handleCompleteTask() {
if (unsavedCount.value > 0) {
try {
await ElMessageBox.confirm('还有未保存的变更,是否先保存后再完成任务?', '提示')
await handleBatchSave()
} catch (e) {
if (e === 'cancel') return
}
}
completeLoading.value = true
try {
if (currentTask.value) {
await completeTask(currentTask.value.id)
ElMessage.success('任务已完成')
labelDialogVisible.value = false
fetchData()
}
} catch (e: any) {
ElMessage.error(e?.message || '操作失败')
} finally {
completeLoading.value = false
}
}
function confidenceClass(v: number) {
if (v >= 0.8) return 'confidence-high'
if (v >= 0.5) return 'confidence-mid'
return 'confidence-low'
}
// TaskTable sub-component
const TaskTable = {
props: ['tasks'],
emits: ['refresh'],
emits: ['refresh', 'label'],
setup(props: any, { emit }: any) {
return () =>
h(
@@ -182,16 +276,17 @@ const TaskTable = {
{
default: () => [
h('el-table-column', { prop: 'name', label: '任务名称', minWidth: '180' }),
h('el-table-column', { prop: 'project_name', label: '所属项目', minWidth: '140' }),
h('el-table-column', { prop: 'status', label: '状态', width: '100' }, {
default: ({ row }: any) =>
h('el-tag', { size: 'small', type: row.status === 'pending' ? 'warning' : row.status === 'completed' ? 'success' : 'primary' },
h(ElTag, { size: 'small', type: row.status === 'pending' ? 'warning' : row.status === 'completed' ? 'success' : 'primary' },
row.status === 'pending' ? '待处理' : row.status === 'in_progress' ? '进行中' : '已完成'
),
}),
h('el-table-column', { prop: 'deadline', label: '截止时间', width: '160' }),
h('el-table-column', { label: '操作', width: '120', fixed: 'right' }, {
default: ({ row }: any) =>
h(ElButton, { type: 'primary', link: true, size: 'small', onClick: () => openLabel(row) }, () => '去打标'),
h(ElButton, { type: 'primary', link: true, size: 'small', onClick: () => emit('label', row) }, () => '去打标'),
}),
],
}
@@ -225,15 +320,28 @@ onMounted(() => {
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
flex-wrap: wrap;
gap: 12px;
.label-stats {
display: flex;
align-items: center;
gap: 12px;
}
}
.empty-text {
color: #c0c4cc;
}
.inline-select {
:deep(.el-input__wrapper) {
padding: 0 4px;
.confidence-high { color: #67c23a; font-weight: 600; }
.confidence-mid { color: #e6a23c; font-weight: 600; }
.confidence-low { color: #f56c6c; font-weight: 600; }
:deep(.label-dialog) {
.el-dialog__body {
padding-top: 10px;
padding-bottom: 10px;
}
}
</style>