Compare commits
3 Commits
codex/list
...
v0.1.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e080105f9f | ||
|
|
64cc76c970 | ||
|
|
99e90798d2 |
Binary file not shown.
7
web/package-lock.json
generated
7
web/package-lock.json
generated
@@ -17,7 +17,6 @@
|
|||||||
"element-plus": "^2.14.0",
|
"element-plus": "^2.14.0",
|
||||||
"markdown-it": "^14.1.1",
|
"markdown-it": "^14.1.1",
|
||||||
"pg": "^8.13.1",
|
"pg": "^8.13.1",
|
||||||
"primeicons": "^7.0.0",
|
|
||||||
"vite": "^5.4.19",
|
"vite": "^5.4.19",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-chartjs": "^5.3.3",
|
"vue-chartjs": "^5.3.3",
|
||||||
@@ -2754,12 +2753,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/primeicons": {
|
|
||||||
"version": "7.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
|
|
||||||
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/punycode.js": {
|
"node_modules/punycode.js": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
|
"resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
"element-plus": "^2.14.0",
|
"element-plus": "^2.14.0",
|
||||||
"markdown-it": "^14.1.1",
|
"markdown-it": "^14.1.1",
|
||||||
"pg": "^8.13.1",
|
"pg": "^8.13.1",
|
||||||
"primeicons": "^7.0.0",
|
|
||||||
"vite": "^5.4.19",
|
"vite": "^5.4.19",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-chartjs": "^5.3.3",
|
"vue-chartjs": "^5.3.3",
|
||||||
|
|||||||
@@ -69,68 +69,6 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-entry-card {
|
|
||||||
width: min(360px, calc(100% - 48px));
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 42px minmax(0, 1fr);
|
|
||||||
gap: 12px 14px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 22px 24px 20px;
|
|
||||||
border: 1px solid rgba(148, 163, 184, 0.26);
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 20px 46px rgba(15, 23, 42, 0.14);
|
|
||||||
animation: loginEntryCardIn 360ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-entry-mark {
|
|
||||||
width: 42px;
|
|
||||||
height: 42px;
|
|
||||||
display: inline-grid;
|
|
||||||
place-items: center;
|
|
||||||
border: 1px solid rgba(var(--theme-primary-rgb, 58, 124, 165), 0.2);
|
|
||||||
border-radius: 4px;
|
|
||||||
background: var(--theme-primary-soft);
|
|
||||||
color: var(--theme-primary-active);
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-entry-copy {
|
|
||||||
min-width: 0;
|
|
||||||
display: grid;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-entry-copy strong {
|
|
||||||
color: var(--ink);
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.35;
|
|
||||||
font-weight: 750;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-entry-copy span {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-entry-progress {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
height: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #edf2f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-entry-progress::after {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: var(--theme-primary);
|
|
||||||
transform-origin: left center;
|
|
||||||
animation: loginEntryProgress 840ms cubic-bezier(0.2, 0, 0, 1) both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-entry-veil-enter-active {
|
.login-entry-veil-enter-active {
|
||||||
transition: opacity 180ms var(--ease);
|
transition: opacity 180ms var(--ease);
|
||||||
}
|
}
|
||||||
@@ -231,7 +169,6 @@
|
|||||||
.main.policies-main,
|
.main.policies-main,
|
||||||
.main.audit-main,
|
.main.audit-main,
|
||||||
.main.digital-employees-main,
|
.main.digital-employees-main,
|
||||||
.main.logs-main,
|
|
||||||
.main.employees-main,
|
.main.employees-main,
|
||||||
.main.settings-main {
|
.main.settings-main {
|
||||||
height: var(--desktop-stage-height, 100dvh);
|
height: var(--desktop-stage-height, 100dvh);
|
||||||
@@ -241,10 +178,6 @@
|
|||||||
.main.settings-main {
|
.main.settings-main {
|
||||||
grid-template-rows: minmax(0, 1fr);
|
grid-template-rows: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
.main.audit-detail-main,
|
|
||||||
.main.digital-employees-detail-main {
|
|
||||||
grid-template-rows: minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
.workarea { min-height: 0; overflow: auto; padding: 24px; }
|
.workarea { min-height: 0; overflow: auto; padding: 24px; }
|
||||||
.workarea.requests-workarea,
|
.workarea.requests-workarea,
|
||||||
.workarea.documents-workarea,
|
.workarea.documents-workarea,
|
||||||
@@ -254,7 +187,6 @@
|
|||||||
.workarea.policies-workarea,
|
.workarea.policies-workarea,
|
||||||
.workarea.audit-workarea,
|
.workarea.audit-workarea,
|
||||||
.workarea.digital-employees-workarea,
|
.workarea.digital-employees-workarea,
|
||||||
.workarea.logs-workarea,
|
|
||||||
.workarea.employees-workarea,
|
.workarea.employees-workarea,
|
||||||
.workarea.settings-workarea {
|
.workarea.settings-workarea {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -271,28 +203,6 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes loginEntryCardIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale3d(0.92, 0.92, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale3d(1, 1, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes loginEntryProgress {
|
|
||||||
from {
|
|
||||||
transform: scaleX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: scaleX(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes loginEntrySidebarIn {
|
@keyframes loginEntrySidebarIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -405,8 +315,6 @@
|
|||||||
transition-duration: 120ms, 120ms !important;
|
transition-duration: 120ms, 120ms !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-entry-card,
|
|
||||||
.login-entry-progress::after,
|
|
||||||
.app.login-entry-active .app-sidebar,
|
.app.login-entry-active .app-sidebar,
|
||||||
.app.login-entry-active > .main {
|
.app.login-entry-active > .main {
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
|
|||||||
@@ -13,6 +13,12 @@
|
|||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
border-bottom: 1px solid #dbe4ee;
|
border-bottom: 1px solid #dbe4ee;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .status-tabs::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.enterprise-list-page .status-tabs button {
|
.enterprise-list-page .status-tabs button {
|
||||||
@@ -65,6 +71,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.enterprise-list-page .filter-set {
|
.enterprise-list-page .filter-set {
|
||||||
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -74,7 +81,14 @@
|
|||||||
|
|
||||||
.enterprise-list-page .list-search {
|
.enterprise-list-page .list-search {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: min(280px, 100%);
|
flex: 0 1 280px;
|
||||||
|
width: 280px;
|
||||||
|
min-width: 220px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .picker-filter {
|
||||||
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.enterprise-list-page .list-search .mdi {
|
.enterprise-list-page .list-search .mdi {
|
||||||
@@ -202,13 +216,32 @@
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .active-filter-strip {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .active-filter-chip {
|
||||||
|
min-height: 28px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--theme-primary-soft);
|
||||||
|
color: var(--theme-primary-active);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
.enterprise-list-page .table-wrap {
|
.enterprise-list-page .table-wrap {
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border: 1px solid #edf2f7;
|
border: 1px solid #edf2f7;
|
||||||
border-radius: 4px;
|
border-radius: 10px;
|
||||||
background: linear-gradient(180deg, #fcfeff 0%, var(--theme-primary-light-9) 100%);
|
background: linear-gradient(180deg, #fcfefd 0%, #f4f8f6 100%);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@@ -263,7 +296,8 @@
|
|||||||
font-weight: 750;
|
font-weight: 750;
|
||||||
}
|
}
|
||||||
|
|
||||||
.enterprise-list-page table {
|
.enterprise-list-page table,
|
||||||
|
.enterprise-list-page .table-wrap > table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 1080px;
|
min-width: 1080px;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
@@ -272,7 +306,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.enterprise-list-page th,
|
.enterprise-list-page th,
|
||||||
.enterprise-list-page td {
|
.enterprise-list-page td,
|
||||||
|
.enterprise-list-page .table-wrap th,
|
||||||
|
.enterprise-list-page .table-wrap td {
|
||||||
padding: 13px 12px;
|
padding: 13px 12px;
|
||||||
border-bottom: 1px solid #edf2f7;
|
border-bottom: 1px solid #edf2f7;
|
||||||
color: #24324a;
|
color: #24324a;
|
||||||
@@ -285,7 +321,8 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.enterprise-list-page th {
|
.enterprise-list-page th,
|
||||||
|
.enterprise-list-page .table-wrap th {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -295,16 +332,20 @@
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.enterprise-list-page tbody tr {
|
.enterprise-list-page tbody tr,
|
||||||
|
.enterprise-list-page .table-wrap tbody tr {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.enterprise-list-page tbody tr:hover,
|
.enterprise-list-page tbody tr:hover,
|
||||||
.enterprise-list-page tbody tr.spotlight {
|
.enterprise-list-page tbody tr.spotlight,
|
||||||
|
.enterprise-list-page .table-wrap tbody tr:hover,
|
||||||
|
.enterprise-list-page .table-wrap tbody tr.spotlight {
|
||||||
background: linear-gradient(90deg, rgba(var(--theme-primary-rgb, 58, 124, 165), 0.08), rgba(var(--theme-primary-rgb, 58, 124, 165), 0.03));
|
background: linear-gradient(90deg, rgba(var(--theme-primary-rgb, 58, 124, 165), 0.08), rgba(var(--theme-primary-rgb, 58, 124, 165), 0.03));
|
||||||
}
|
}
|
||||||
|
|
||||||
.enterprise-list-page tbody tr:last-child td {
|
.enterprise-list-page tbody tr:last-child td,
|
||||||
|
.enterprise-list-page .table-wrap tbody tr:last-child td {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,11 +354,119 @@
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .doc-kind-tag,
|
||||||
|
.enterprise-list-page .type-tag,
|
||||||
|
.enterprise-list-page .status-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .doc-kind-tag {
|
||||||
|
min-height: 26px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 7px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .doc-kind-tag.reimbursement {
|
||||||
|
background: var(--theme-primary-light-9);
|
||||||
|
color: var(--theme-primary-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .doc-kind-tag.application {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .type-tag {
|
||||||
|
min-height: 26px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .type-tag.travel,
|
||||||
|
.enterprise-list-page .type-tag.hotel,
|
||||||
|
.enterprise-list-page .type-tag.transport {
|
||||||
|
background: var(--theme-primary-light-9);
|
||||||
|
color: var(--theme-primary-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .type-tag.entertainment,
|
||||||
|
.enterprise-list-page .type-tag.meal {
|
||||||
|
background: #fff7ed;
|
||||||
|
color: #ea580c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .type-tag.office {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .type-tag.meeting,
|
||||||
|
.enterprise-list-page .type-tag.training {
|
||||||
|
background: #eef2ff;
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .type-tag.other {
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .status-tag {
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0 9px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 750;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .status-tag.info {
|
||||||
|
border-color: #bfdbfe;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .status-tag.success,
|
||||||
|
.enterprise-list-page .status-tag.archived {
|
||||||
|
border-color: var(--success-line);
|
||||||
|
background: var(--success-soft);
|
||||||
|
color: var(--success-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .status-tag.warning,
|
||||||
|
.enterprise-list-page .status-tag.draft {
|
||||||
|
border-color: #fed7aa;
|
||||||
|
background: #fff7ed;
|
||||||
|
color: #f97316;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .status-tag.danger {
|
||||||
|
border-color: #fecaca;
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-list-page .status-tag.neutral,
|
||||||
|
.enterprise-list-page .status-tag.muted,
|
||||||
|
.enterprise-list-page .status-tag.disabled {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
.enterprise-pagination {
|
.enterprise-pagination {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.enterprise-list-page .list-foot {
|
.enterprise-list-page .list-foot,
|
||||||
|
.enterprise-list-page.enterprise-list-page .list-foot {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto 1fr;
|
grid-template-columns: 1fr auto 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -325,27 +474,30 @@
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.enterprise-list-page .page-summary {
|
.enterprise-list-page .page-summary,
|
||||||
|
.enterprise-list-page.enterprise-list-page .page-summary {
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
}
|
}
|
||||||
|
|
||||||
.enterprise-list-page .pager {
|
.enterprise-list-page .pager,
|
||||||
|
.enterprise-list-page.enterprise-list-page .pager {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 4px;
|
border-radius: 12px;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.enterprise-list-page .pager button {
|
.enterprise-list-page .pager button,
|
||||||
|
.enterprise-list-page.enterprise-list-page .pager button {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 4px;
|
border-radius: 9px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -353,19 +505,22 @@
|
|||||||
transition: background 160ms ease, color 160ms ease, box-shadow 160ms ease;
|
transition: background 160ms ease, color 160ms ease, box-shadow 160ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.enterprise-list-page .pager button:hover:not(.active) {
|
.enterprise-list-page .pager button:hover:not(.active),
|
||||||
|
.enterprise-list-page.enterprise-list-page .pager button:hover:not(.active) {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: var(--theme-primary-active);
|
color: var(--theme-primary-active);
|
||||||
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.08);
|
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.enterprise-list-page .pager button.active {
|
.enterprise-list-page .pager button.active,
|
||||||
background: var(--theme-primary);
|
.enterprise-list-page.enterprise-list-page .pager button.active {
|
||||||
|
background: var(--theme-primary-active);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
box-shadow: 0 8px 16px var(--theme-primary-shadow);
|
box-shadow: 0 8px 16px var(--theme-primary-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.enterprise-list-page .pager button:disabled {
|
.enterprise-list-page .pager button:disabled,
|
||||||
|
.enterprise-list-page.enterprise-list-page .pager button:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.45;
|
opacity: 0.45;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
@@ -394,6 +549,86 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.enterprise-detail-page .detail-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 0 0;
|
||||||
|
border-top: 1px solid #e5eaf0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-detail-page .detail-action-group {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-detail-page .back-action,
|
||||||
|
.enterprise-detail-page .minor-action,
|
||||||
|
.enterprise-detail-page .major-action {
|
||||||
|
height: 38px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 760;
|
||||||
|
transition: transform 160ms ease, border-color 160ms ease, color 160ms ease, box-shadow 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-detail-page .back-action,
|
||||||
|
.enterprise-detail-page .minor-action {
|
||||||
|
border: 1px solid #d7e0ea;
|
||||||
|
background: #fff;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-detail-page .minor-action.success-action {
|
||||||
|
border-color: var(--success-line);
|
||||||
|
background: var(--success-soft);
|
||||||
|
color: var(--success-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-detail-page .minor-action.enable-action {
|
||||||
|
border-color: rgba(100, 116, 139, 0.26);
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-detail-page .minor-action.enable-action.is-on {
|
||||||
|
border-color: rgba(var(--success-rgb), 0.26);
|
||||||
|
color: var(--success-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-detail-page .minor-action.danger-action {
|
||||||
|
border-color: rgba(220, 38, 38, 0.2);
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-detail-page .major-action {
|
||||||
|
border: 1px solid var(--theme-primary);
|
||||||
|
background: var(--theme-primary);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 4px 12px var(--theme-primary-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-detail-page .back-action:hover,
|
||||||
|
.enterprise-detail-page .minor-action:hover,
|
||||||
|
.enterprise-detail-page .major-action:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-detail-page .back-action:disabled,
|
||||||
|
.enterprise-detail-page .minor-action:disabled,
|
||||||
|
.enterprise-detail-page .major-action:disabled {
|
||||||
|
opacity: 0.52;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
.enterprise-detail-card .card-head {
|
.enterprise-detail-card .card-head {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,6 +243,22 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-topbar-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-kpi-chips {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-kpi-chip {
|
||||||
|
min-width: 142px;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-alert-strip {
|
.detail-alert-strip {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -147,40 +147,25 @@ h1 { margin-top: 4px; color: var(--ink); font-size: 24px; line-height: 1.25; fon
|
|||||||
*, *::before, *::after { animation-duration: 1ms !important; transition-duration: 1ms !important; scroll-behavior: auto !important; }
|
*, *::before, *::after { animation-duration: 1ms !important; transition-duration: 1ms !important; scroll-behavior: auto !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-loading__spinner {
|
.table-state:has(.table-loading.screen-floating),
|
||||||
width: 38px;
|
.table-state:has(.table-loading-anchor),
|
||||||
height: 38px;
|
.table-loading-row:has(.table-loading.screen-floating),
|
||||||
display: inline-grid;
|
.table-loading-row:has(.table-loading-anchor) {
|
||||||
place-items: center;
|
min-height: 0 !important;
|
||||||
border: 3px solid #e2e8f0;
|
padding: 0 !important;
|
||||||
border-top-color: var(--primary);
|
background: transparent !important;
|
||||||
border-radius: 50%;
|
|
||||||
animation: table-spinner-rotate .8s linear infinite !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-loading.sky .table-loading__spinner {
|
tr:has(> .table-loading-row .table-loading.screen-floating),
|
||||||
border-top-color: var(--primary);
|
tr:has(> .table-loading-row .table-loading-anchor),
|
||||||
}
|
tr:has(> .table-loading-row .table-loading.screen-floating) > td,
|
||||||
|
tr:has(> .table-loading-row .table-loading-anchor) > td,
|
||||||
.table-loading.detail .table-loading__spinner {
|
.table-loading-row:has(.table-loading.screen-floating),
|
||||||
width: 34px;
|
.table-loading-row:has(.table-loading-anchor) {
|
||||||
height: 34px;
|
height: 0 !important;
|
||||||
}
|
border: 0 !important;
|
||||||
|
line-height: 0 !important;
|
||||||
.table-loading.banner .table-loading__spinner {
|
background: transparent !important;
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
border-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-loading__spinner i {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes table-spinner-rotate {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Global Scrollbar Styles */
|
/* Global Scrollbar Styles */
|
||||||
|
|||||||
@@ -1337,213 +1337,6 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-detail-topbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 14px 0 10px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
background: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-detail-topbar.panel {
|
|
||||||
padding: 14px 0 10px;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-detail-topbar-main {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-detail-topbar-main h2 {
|
|
||||||
margin: 0;
|
|
||||||
color: #0f172a;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 850;
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-detail-topbar-main p {
|
|
||||||
flex-basis: 100%;
|
|
||||||
margin: 0;
|
|
||||||
max-width: 860px;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-detail-topbar-meta {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-detail-topbar .hero-review-meta {
|
|
||||||
flex-basis: 100%;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-detail-topbar .review-note-block {
|
|
||||||
flex-basis: 100%;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-detail-topbar .hero-stats {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-detail-topbar .hero-stat {
|
|
||||||
min-height: 30px;
|
|
||||||
padding: 0 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-detail-topbar .hero-stat span {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-detail-topbar .hero-stat strong {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-editor-head {
|
|
||||||
align-items: center;
|
|
||||||
padding-top: 4px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-score-ring {
|
|
||||||
--score-ring: #f97316;
|
|
||||||
--score-ring-bg: #fff7ed;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: 82px;
|
|
||||||
height: 82px;
|
|
||||||
border-radius: 999px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
align-content: center;
|
|
||||||
gap: 1px;
|
|
||||||
border: 2px solid var(--score-ring);
|
|
||||||
background: var(--score-ring-bg);
|
|
||||||
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-score-ring strong {
|
|
||||||
color: #0f172a;
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 900;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-score-ring span,
|
|
||||||
.json-risk-score-ring em {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 11px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-score-ring em {
|
|
||||||
color: var(--score-ring);
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-score-ring.low {
|
|
||||||
--score-ring: #2563eb;
|
|
||||||
--score-ring-bg: #eff6ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-score-ring.medium {
|
|
||||||
--score-ring: #f97316;
|
|
||||||
--score-ring-bg: #fff7ed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-score-ring.high {
|
|
||||||
--score-ring: #dc2626;
|
|
||||||
--score-ring-bg: #fef2f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-score-ring.critical {
|
|
||||||
--score-ring: #991b1b;
|
|
||||||
--score-ring-bg: #fff1f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-editor-title {
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-head-copy {
|
|
||||||
min-width: 0;
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-head-title-row {
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-editor-title h2 {
|
|
||||||
color: #0f172a;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 850;
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-editor-title p {
|
|
||||||
margin-top: 2px;
|
|
||||||
max-width: 760px;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-head-subtitle {
|
|
||||||
display: -webkit-box;
|
|
||||||
margin: 0;
|
|
||||||
max-width: 760px;
|
|
||||||
overflow: hidden;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.55;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-head-meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-head-meta span {
|
|
||||||
min-height: 24px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 8px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #f8fafc;
|
|
||||||
color: #475569;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 750;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-name-cell .skill-list-subtitle {
|
.skill-name-cell .skill-list-subtitle {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -1554,40 +1347,6 @@
|
|||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-risk-editor-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-mode-pill {
|
|
||||||
min-height: 28px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #fff1f2;
|
|
||||||
color: #be123c;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-mode-pill.high {
|
|
||||||
background: #fef2f2;
|
|
||||||
color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-mode-pill.medium {
|
|
||||||
background: #fff7ed;
|
|
||||||
color: #ea580c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-mode-pill.low {
|
|
||||||
background: var(--success-soft);
|
|
||||||
color: var(--success-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-editor-body {
|
.json-risk-editor-body {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -1853,15 +1612,3 @@
|
|||||||
grid-column: span 1;
|
grid-column: span 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 860px) {
|
|
||||||
.json-risk-editor-head {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-editor-actions {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -609,7 +609,8 @@ tbody tr.is-disabled:hover {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skill-badge {
|
.skill-badge,
|
||||||
|
.skill-detail :deep(.skill-badge) {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 24px;
|
min-height: 24px;
|
||||||
@@ -620,11 +621,16 @@ tbody tr.is-disabled:hover {
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skill-badge.primary { background: var(--theme-gradient-primary); }
|
.skill-badge.primary,
|
||||||
.skill-badge.rose { background: linear-gradient(135deg, #f43f5e, #e11d48); }
|
.skill-detail :deep(.skill-badge.primary) { background: var(--theme-gradient-primary); }
|
||||||
.skill-badge.violet { background: linear-gradient(135deg, #8b5cf6, #7c3aed); }
|
.skill-badge.rose,
|
||||||
.skill-badge.blue { background: linear-gradient(135deg, #3b82f6, #2563eb); }
|
.skill-detail :deep(.skill-badge.rose) { background: linear-gradient(135deg, #f43f5e, #e11d48); }
|
||||||
.skill-badge.amber { background: linear-gradient(135deg, #f59e0b, #ea580c); }
|
.skill-badge.violet,
|
||||||
|
.skill-detail :deep(.skill-badge.violet) { background: linear-gradient(135deg, #8b5cf6, #7c3aed); }
|
||||||
|
.skill-badge.blue,
|
||||||
|
.skill-detail :deep(.skill-badge.blue) { background: linear-gradient(135deg, #3b82f6, #2563eb); }
|
||||||
|
.skill-badge.amber,
|
||||||
|
.skill-detail :deep(.skill-badge.amber) { background: linear-gradient(135deg, #f59e0b, #ea580c); }
|
||||||
|
|
||||||
.hero-title h2 {
|
.hero-title h2 {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
@@ -868,41 +874,6 @@ tbody tr.is-disabled:hover {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spreadsheet-editor-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spreadsheet-editor-title {
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spreadsheet-editor-title h2 {
|
|
||||||
color: #0f172a;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 850;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spreadsheet-editor-title p {
|
|
||||||
margin-top: 2px;
|
|
||||||
max-width: 760px;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spreadsheet-editor-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spreadsheet-editor-meta {
|
.spreadsheet-editor-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
@@ -5,49 +5,81 @@
|
|||||||
|
|
||||||
.digital-employees-list {
|
.digital-employees-list {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 16px 18px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.digital-employees-list > .status-tabs {
|
.digital-employees-list > .status-tabs {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 28px;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: 1px solid #dbe4ee;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.digital-employees-list .table-wrap {
|
.digital-employees-list > .status-tabs::-webkit-scrollbar {
|
||||||
min-height: 0;
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.digital-employees-list > .status-tabs button {
|
||||||
|
position: relative;
|
||||||
|
min-height: 36px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 750;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.digital-employees-list > .status-tabs button.active {
|
||||||
|
color: var(--theme-primary-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.digital-employees-list > .status-tabs button.active::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: -1px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 999px 999px 0 0;
|
||||||
|
background: var(--theme-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.digital-employees-table {
|
.digital-employees-table {
|
||||||
min-width: 1060px;
|
min-width: 1060px;
|
||||||
table-layout: fixed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.digital-employees-table .col-skill { width: 27%; }
|
/* Default first column left alignment */
|
||||||
.digital-employees-table .col-schedule { width: 16%; }
|
.digital-employees-table th:first-child,
|
||||||
.digital-employees-table .col-mode { width: 12%; }
|
.digital-employees-table td:first-child {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.digital-employees-table .col-skill { width: 22%; }
|
||||||
.digital-employees-table .col-skill-type { width: 11%; }
|
.digital-employees-table .col-skill-type { width: 11%; }
|
||||||
.digital-employees-table .col-status { width: 11%; }
|
.digital-employees-table .col-owner { width: 11%; }
|
||||||
.digital-employees-table .col-enabled { width: 11%; }
|
.digital-employees-table .col-schedule { width: 16%; }
|
||||||
.digital-employees-table .col-updated { width: 12%; }
|
.digital-employees-table .col-mode { width: 10%; }
|
||||||
|
.digital-employees-table .col-status { width: 10%; }
|
||||||
.digital-employees-table td {
|
.digital-employees-table .col-enabled { width: 10%; }
|
||||||
white-space: nowrap;
|
.digital-employees-table .col-updated { width: 10%; }
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-employees-table tbody tr {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-refresh-action i {
|
.digital-refresh-action i {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skill-type-pill {
|
|
||||||
border-color: #dbeafe;
|
|
||||||
background: #eff6ff;
|
|
||||||
color: #1d4ed8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-employee-detail {
|
.digital-employee-detail {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,26 @@
|
|||||||
color: var(--theme-primary);
|
color: var(--theme-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.system-logs-list .refresh-interval-filter,
|
||||||
|
.system-logs-list .refresh-interval-trigger,
|
||||||
|
.system-logs-list .refresh-interval-menu {
|
||||||
|
min-width: 148px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-logs-list .refresh-interval-trigger > .mdi:first-child {
|
||||||
|
color: var(--theme-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-logs-list .icon-refresh-action {
|
||||||
|
width: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-logs-list .icon-refresh-action .mdi {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.system-logs-list .document-filter-menu {
|
.system-logs-list .document-filter-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 8px);
|
top: calc(100% + 8px);
|
||||||
|
|||||||
@@ -423,14 +423,6 @@ th {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-loading-row {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-loading-row > .table-loading {
|
|
||||||
min-height: 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-foot {
|
.list-foot {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto 1fr;
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
|||||||
@@ -206,6 +206,16 @@
|
|||||||
padding: 20px 24px;
|
padding: 20px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-content-fill {
|
||||||
|
overflow: hidden;
|
||||||
|
align-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content-fill .settings-logs-view,
|
||||||
|
.settings-content-fill .settings-log-detail-view {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.model-grid {
|
.model-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -680,6 +690,19 @@
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.log-policy-card .card-head {
|
||||||
|
padding-top: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-policy-card > *:not(.card-head) {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-policy-card > *:not(.card-head):last-child {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 大语言模型配置卡片图标与标题排版 */
|
/* 大语言模型配置卡片图标与标题排版 */
|
||||||
.card-title-with-icon {
|
.card-title-with-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -591,7 +591,7 @@
|
|||||||
background: rgba(6, 78, 59, 0.34);
|
background: rgba(6, 78, 59, 0.34);
|
||||||
}
|
}
|
||||||
|
|
||||||
.setup-startup-spinner .pi {
|
.setup-startup-spinner .mdi {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,7 +626,7 @@
|
|||||||
background: rgba(15, 23, 42, 0.24);
|
background: rgba(15, 23, 42, 0.24);
|
||||||
}
|
}
|
||||||
|
|
||||||
.setup-startup-step .pi {
|
.setup-startup-step .mdi {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
color: color-mix(in srgb, var(--theme-primary-soft) 46%, transparent);
|
color: color-mix(in srgb, var(--theme-primary-soft) 46%, transparent);
|
||||||
}
|
}
|
||||||
@@ -648,7 +648,7 @@
|
|||||||
border-color: rgba(59, 130, 246, 0.34);
|
border-color: rgba(59, 130, 246, 0.34);
|
||||||
}
|
}
|
||||||
|
|
||||||
.setup-startup-step.is-running .pi {
|
.setup-startup-step.is-running .mdi {
|
||||||
color: #93c5fd;
|
color: #93c5fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,7 +656,7 @@
|
|||||||
border-color: rgba(var(--theme-primary-rgb), 0.32);
|
border-color: rgba(var(--theme-primary-rgb), 0.32);
|
||||||
}
|
}
|
||||||
|
|
||||||
.setup-startup-step.is-success .pi {
|
.setup-startup-step.is-success .mdi {
|
||||||
color: var(--theme-primary-light-5);
|
color: var(--theme-primary-light-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,7 +664,7 @@
|
|||||||
border-color: rgba(248, 113, 113, 0.36);
|
border-color: rgba(248, 113, 113, 0.36);
|
||||||
}
|
}
|
||||||
|
|
||||||
.setup-startup-step.is-error .pi {
|
.setup-startup-step.is-error .mdi {
|
||||||
color: #f87171;
|
color: #f87171;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<article class="skill-list panel">
|
<EnterpriseListPage
|
||||||
<nav class="status-tabs" aria-label="能力类型">
|
variant="skill-list"
|
||||||
<button
|
:tabs="shellTabs"
|
||||||
v-for="tab in tabs"
|
:active-tab="activeType"
|
||||||
:key="tab.id"
|
tabs-label="能力类型"
|
||||||
type="button"
|
:loading="loading"
|
||||||
:class="{ active: activeType === tab.id }"
|
:error="errorMessage"
|
||||||
@click="emit('update:activeType', tab.id)"
|
:empty="!visibleSkills.length"
|
||||||
|
:empty-state="auditEmptyState"
|
||||||
|
:loading-title="`${activeTabLabel}资产同步中`"
|
||||||
|
:loading-message="`正在加载${activeTabLabel}资产`"
|
||||||
|
loading-icon="mdi mdi-view-list-outline"
|
||||||
|
:hint="hintText"
|
||||||
|
@update:active-tab="emit('update:activeType', $event)"
|
||||||
|
@empty-action="emit('empty-action')"
|
||||||
>
|
>
|
||||||
{{ tab.label }}
|
<template #filters>
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="list-toolbar">
|
|
||||||
<div class="filter-set">
|
|
||||||
<label class="search-filter">
|
<label class="search-filter">
|
||||||
<i class="mdi mdi-magnify"></i>
|
<i class="mdi mdi-magnify"></i>
|
||||||
<input
|
<input
|
||||||
@@ -120,9 +122,9 @@
|
|||||||
@close="emit('close-filter-popover')"
|
@close="emit('close-filter-popover')"
|
||||||
@select="selectFilter('status', $event)"
|
@select="selectFilter('status', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<div class="toolbar-actions">
|
<template #actions>
|
||||||
<button
|
<button
|
||||||
v-if="activeFilterTokens.length"
|
v-if="activeFilterTokens.length"
|
||||||
class="ghost-filter-btn"
|
class="ghost-filter-btn"
|
||||||
@@ -142,50 +144,23 @@
|
|||||||
<i class="mdi mdi-plus"></i>
|
<i class="mdi mdi-plus"></i>
|
||||||
<span>{{ createButtonLabel }}</span>
|
<span>{{ createButtonLabel }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="hint"><i class="mdi mdi-information-outline"></i> {{ hintText }}</p>
|
|
||||||
|
|
||||||
|
<template #meta>
|
||||||
<div v-if="activeFilterTokens.length" class="active-filter-strip">
|
<div v-if="activeFilterTokens.length" class="active-filter-strip">
|
||||||
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
|
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
|
||||||
{{ token }}
|
{{ token }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div
|
<template #error>
|
||||||
class="table-wrap"
|
|
||||||
:class="{ 'is-empty': !loading && !errorMessage && !visibleSkills.length }"
|
|
||||||
>
|
|
||||||
<div v-if="loading" class="table-state">
|
|
||||||
<TableLoadingState
|
|
||||||
variant="panel"
|
|
||||||
:title="`${activeTabLabel}资产同步中`"
|
|
||||||
:message="`正在加载${activeTabLabel}资产`"
|
|
||||||
icon="mdi mdi-view-list-outline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="errorMessage" class="table-state error">
|
|
||||||
<i class="mdi mdi-alert-circle-outline"></i>
|
<i class="mdi mdi-alert-circle-outline"></i>
|
||||||
<p>{{ errorMessage }}</p>
|
<p>{{ errorMessage }}</p>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<TableEmptyState
|
<template #table>
|
||||||
v-else-if="!visibleSkills.length"
|
<table>
|
||||||
:eyebrow="auditEmptyState.eyebrow"
|
|
||||||
:title="auditEmptyState.title"
|
|
||||||
:description="auditEmptyState.desc"
|
|
||||||
:icon="auditEmptyState.icon"
|
|
||||||
:action-label="auditEmptyState.actionLabel"
|
|
||||||
:action-icon="auditEmptyState.actionIcon"
|
|
||||||
:tone="auditEmptyState.tone"
|
|
||||||
:art-label="auditEmptyState.artLabel"
|
|
||||||
:tips="auditEmptyState.tips"
|
|
||||||
@action="emit('empty-action')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<table v-else>
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ tableColumns.name }}</th>
|
<th>{{ tableColumns.name }}</th>
|
||||||
@@ -245,24 +220,27 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
<footer v-if="!loading && !errorMessage && visibleSkills.length" class="list-foot">
|
<footer v-if="!loading && !errorMessage && visibleSkills.length" class="list-foot">
|
||||||
<span class="page-summary">当前展示 {{ visibleSkills.length }} 条资产</span>
|
<span class="page-summary">当前展示 {{ visibleSkills.length }} 条资产</span>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</template>
|
||||||
|
</EnterpriseListPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import AuditPickerFilter from './AuditPickerFilter.vue'
|
import AuditPickerFilter from './AuditPickerFilter.vue'
|
||||||
import TableEmptyState from '../shared/TableEmptyState.vue'
|
import EnterpriseListPage from '../shared/EnterpriseListPage.vue'
|
||||||
import TableLoadingState from '../shared/TableLoadingState.vue'
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'AuditAssetList'
|
name: 'AuditAssetList'
|
||||||
})
|
})
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
tabs: { type: Array, default: () => [] },
|
tabs: { type: Array, default: () => [] },
|
||||||
activeType: { type: String, default: '' },
|
activeType: { type: String, default: '' },
|
||||||
activeTabLabel: { type: String, default: '' },
|
activeTabLabel: { type: String, default: '' },
|
||||||
@@ -325,6 +303,13 @@ const emit = defineEmits([
|
|||||||
'open-asset-detail'
|
'open-asset-detail'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const shellTabs = computed(() =>
|
||||||
|
props.tabs.map((tab) => ({
|
||||||
|
value: tab.id,
|
||||||
|
label: tab.label
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
function selectFilter(type, value) {
|
function selectFilter(type, value) {
|
||||||
emit('select-filter', type, value)
|
emit('select-filter', type, value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="json-risk-editor-shell panel digital-worker-detail-shell">
|
<section class="json-risk-editor-shell panel digital-worker-detail-shell">
|
||||||
<header class="json-risk-editor-head asset-detail-topbar list-toolbar">
|
|
||||||
<div class="json-risk-editor-title asset-detail-topbar-main filter-set">
|
|
||||||
<div class="json-risk-head-copy">
|
|
||||||
<div class="json-risk-head-title-row">
|
|
||||||
<h2>{{ selectedSkill.name }}</h2>
|
|
||||||
</div>
|
|
||||||
<p class="json-risk-head-subtitle">
|
|
||||||
{{ selectedSkill.summary || '后台自动执行的数字员工技能。' }}
|
|
||||||
</p>
|
|
||||||
<div class="json-risk-head-meta">
|
|
||||||
<span>技能编号:{{ selectedSkill.code || '-' }}</span>
|
|
||||||
<span>执行计划:{{ digitalEmployee.scheduleLabel || selectedSkill.scope || '-' }}</span>
|
|
||||||
<span>最近更新:{{ selectedSkill.updatedAt || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="json-risk-editor-body">
|
<div class="json-risk-editor-body">
|
||||||
<section class="json-risk-main-stage">
|
<section class="json-risk-main-stage">
|
||||||
<article class="detail-card panel json-risk-summary-card">
|
<article class="detail-card panel json-risk-summary-card">
|
||||||
|
|||||||
@@ -1,31 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="json-risk-editor-shell panel">
|
<section class="json-risk-editor-shell panel">
|
||||||
<header class="json-risk-editor-head asset-detail-topbar list-toolbar">
|
|
||||||
<div class="json-risk-editor-title asset-detail-topbar-main filter-set">
|
|
||||||
<div class="json-risk-head-copy">
|
|
||||||
<div class="json-risk-head-title-row">
|
|
||||||
<h2>{{ selectedSkill.name }}</h2>
|
|
||||||
</div>
|
|
||||||
<p class="json-risk-head-subtitle">
|
|
||||||
{{ selectedSkill.riskRuleSubtitle || '平台通用风险规则' }}
|
|
||||||
</p>
|
|
||||||
<div class="json-risk-head-meta">
|
|
||||||
<span v-if="selectedSkill.riskCategory">适用场景:{{ selectedSkill.riskCategory }}</span>
|
|
||||||
<span>业务域:{{ selectedSkill.category || '-' }}</span>
|
|
||||||
<span>最近更新:{{ selectedSkill.updatedAt || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="json-risk-score-ring"
|
|
||||||
:class="selectedSkill.riskRuleScoreLevel || selectedSkill.riskRuleSeverity"
|
|
||||||
>
|
|
||||||
<strong>{{ selectedSkill.riskRuleScore ?? '--' }}</strong>
|
|
||||||
<span>风险分</span>
|
|
||||||
<em>{{ selectedSkill.riskRuleScoreLabel || selectedSkill.riskRuleSeverityLabel }}</em>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="selectedSkill.riskRuleGenerationFailed"
|
v-if="selectedSkill.riskRuleGenerationFailed"
|
||||||
class="json-risk-generation-failure"
|
class="json-risk-generation-failure"
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="spreadsheet-editor-shell panel">
|
<section class="spreadsheet-editor-shell panel">
|
||||||
<header class="spreadsheet-editor-head asset-detail-topbar list-toolbar">
|
|
||||||
<div class="spreadsheet-editor-title asset-detail-topbar-main filter-set">
|
|
||||||
<div class="skill-badge" :class="selectedSkill.badgeTone">{{ selectedSkill.typeLabel }}</div>
|
|
||||||
<div>
|
|
||||||
<h2>{{ selectedSkill.name }}</h2>
|
|
||||||
<p>{{ selectedSkill.summary || '当前资产尚未补充说明。' }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="spreadsheet-editor-actions asset-detail-topbar-meta toolbar-actions">
|
|
||||||
<span class="spreadsheet-mode-pill">
|
|
||||||
{{ selectedSpreadsheetModeLabel }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
ref="fileInput"
|
ref="fileInput"
|
||||||
class="spreadsheet-upload-input"
|
class="spreadsheet-upload-input"
|
||||||
@@ -126,7 +110,6 @@ defineOptions({
|
|||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
selectedSkill: { type: Object, required: true },
|
selectedSkill: { type: Object, required: true },
|
||||||
selectedSpreadsheetModeLabel: { type: String, default: '' },
|
|
||||||
selectedSpreadsheetFileName: { type: String, default: '' },
|
selectedSpreadsheetFileName: { type: String, default: '' },
|
||||||
selectedSpreadsheetChangeRecords: { type: Array, default: () => [] },
|
selectedSpreadsheetChangeRecords: { type: Array, default: () => [] },
|
||||||
spreadsheetOnlyOfficeHostId: { type: String, required: true },
|
spreadsheetOnlyOfficeHostId: { type: String, required: true },
|
||||||
|
|||||||
@@ -1,8 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="digital-employee-list-panel">
|
<EnterpriseListPage
|
||||||
<div class="list-toolbar">
|
variant="digital-employee-list-panel"
|
||||||
<div class="filter-set">
|
:panel="false"
|
||||||
<label class="search-filter">
|
:loading="loading"
|
||||||
|
:error="errorMessage"
|
||||||
|
:empty="!visibleEmployees.length"
|
||||||
|
:empty-state="emptyState"
|
||||||
|
:show-pagination="!loading && !errorMessage && visibleEmployees.length > 0"
|
||||||
|
:current-page="currentPage"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:pages="pageNumbers"
|
||||||
|
:show-page-size="false"
|
||||||
|
:summary="paginationSummary"
|
||||||
|
:total="visibleEmployees.length"
|
||||||
|
:total-pages="totalPages"
|
||||||
|
loading-title="数字员工资产同步中"
|
||||||
|
loading-message="正在加载数字员工资产"
|
||||||
|
loading-icon="mdi mdi-view-list-outline"
|
||||||
|
@update:current-page="currentPage = $event"
|
||||||
|
>
|
||||||
|
<template #filters>
|
||||||
|
<label class="list-search">
|
||||||
<i class="mdi mdi-magnify"></i>
|
<i class="mdi mdi-magnify"></i>
|
||||||
<input
|
<input
|
||||||
:value="keyword"
|
:value="keyword"
|
||||||
@@ -50,9 +68,9 @@
|
|||||||
@close="emit('close-filter-popover')"
|
@close="emit('close-filter-popover')"
|
||||||
@select="selectFilter('executionMode', $event)"
|
@select="selectFilter('executionMode', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<div class="toolbar-actions">
|
<template #actions>
|
||||||
<button
|
<button
|
||||||
v-if="keyword || activeFilterTokens.length"
|
v-if="keyword || activeFilterTokens.length"
|
||||||
class="ghost-filter-btn"
|
class="ghost-filter-btn"
|
||||||
@@ -71,50 +89,23 @@
|
|||||||
<i class="mdi mdi-refresh"></i>
|
<i class="mdi mdi-refresh"></i>
|
||||||
<span>{{ loading ? '刷新中...' : '刷新' }}</span>
|
<span>{{ loading ? '刷新中...' : '刷新' }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="hint">
|
|
||||||
<i class="mdi mdi-information-outline"></i>
|
|
||||||
归集后台自动执行的数字员工技能,可查看技能内容、执行计划、启动状态和最近版本。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
<template #meta>
|
||||||
<div v-if="activeFilterTokens.length" class="active-filter-strip">
|
<div v-if="activeFilterTokens.length" class="active-filter-strip">
|
||||||
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
|
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
|
||||||
{{ token }}
|
{{ token }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div
|
<template #error>
|
||||||
class="table-wrap digital-table-wrap"
|
|
||||||
:class="{ 'is-empty': !loading && !errorMessage && !visibleEmployees.length }"
|
|
||||||
>
|
|
||||||
<div v-if="loading" class="table-state">
|
|
||||||
<TableLoadingState
|
|
||||||
variant="panel"
|
|
||||||
title="数字员工资产同步中"
|
|
||||||
message="正在加载数字员工资产"
|
|
||||||
icon="mdi mdi-view-list-outline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="errorMessage" class="table-state error">
|
|
||||||
<i class="mdi mdi-alert-circle-outline"></i>
|
<i class="mdi mdi-alert-circle-outline"></i>
|
||||||
<p>{{ errorMessage }}</p>
|
<p>{{ errorMessage }}</p>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<TableEmptyState
|
<template #table>
|
||||||
v-else-if="!visibleEmployees.length"
|
<table class="digital-employees-table">
|
||||||
eyebrow="数字员工"
|
|
||||||
title="暂无匹配的数字员工"
|
|
||||||
description="当前没有符合搜索条件的后台执行技能。"
|
|
||||||
icon="mdi mdi-account-cog-outline"
|
|
||||||
tone="theme"
|
|
||||||
art-label="STAFF"
|
|
||||||
:tips="['数字员工已从规则中心拆出为独立入口', '运行与定时操作统一进入详情后处理']"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<table v-else class="digital-employees-table">
|
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col class="col-skill">
|
<col class="col-skill">
|
||||||
<col class="col-skill-type">
|
<col class="col-skill-type">
|
||||||
@@ -144,58 +135,29 @@
|
|||||||
@click="emit('open-employee-detail', employee)"
|
@click="emit('open-employee-detail', employee)"
|
||||||
>
|
>
|
||||||
<td>
|
<td>
|
||||||
<div class="skill-name-cell">
|
<strong class="doc-id">{{ employee.name }}</strong>
|
||||||
<span class="skill-avatar" :class="employee.badgeTone">{{ employee.short }}</span>
|
|
||||||
<div>
|
|
||||||
<strong>{{ employee.name }}</strong>
|
|
||||||
<span class="skill-list-subtitle">{{ employee.summary || employee.code }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td><span class="scope-pill skill-type-pill">{{ employee.skillCategory }}</span></td>
|
<td><span class="doc-kind-tag application">{{ employee.skillCategory }}</span></td>
|
||||||
<td>{{ employee.owner }}</td>
|
<td>{{ employee.owner }}</td>
|
||||||
<td><span class="scope-pill">{{ employee.scope }}</span></td>
|
<td><span class="type-tag other">{{ employee.scope }}</span></td>
|
||||||
<td>{{ employee.executionMode }}</td>
|
<td>{{ employee.executionMode }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span :class="['status-pill', employee.statusTone]">{{ employee.status }}</span>
|
<span :class="['status-tag', employee.statusTone]">{{ employee.status }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td><span :class="['status-pill', employee.enabledTone]">{{ employee.enabledLabel }}</span></td>
|
<td><span :class="['status-tag', employee.enabledTone]">{{ employee.enabledLabel }}</span></td>
|
||||||
<td>{{ employee.updatedAt || '-' }}</td>
|
<td>{{ employee.updatedAt || '-' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</template>
|
||||||
|
</EnterpriseListPage>
|
||||||
<footer v-if="!loading && !errorMessage && visibleEmployees.length" class="list-foot digital-employee-pagination">
|
|
||||||
<span class="page-summary">共 {{ visibleEmployees.length }} 条,目前第 {{ currentPage }} / {{ totalPages }} 页</span>
|
|
||||||
<div class="pager" aria-label="员工技能分页">
|
|
||||||
<button class="page-nav" type="button" :disabled="currentPage === 1" @click="currentPage--">
|
|
||||||
<i class="mdi mdi-chevron-left"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-for="page in pageNumbers"
|
|
||||||
:key="page"
|
|
||||||
class="page-number"
|
|
||||||
:class="{ active: currentPage === page }"
|
|
||||||
type="button"
|
|
||||||
@click="currentPage = page"
|
|
||||||
>
|
|
||||||
{{ page }}
|
|
||||||
</button>
|
|
||||||
<button class="page-nav" type="button" :disabled="currentPage === totalPages" @click="currentPage++">
|
|
||||||
<i class="mdi mdi-chevron-right"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</section>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
import AuditPickerFilter from './AuditPickerFilter.vue'
|
import AuditPickerFilter from './AuditPickerFilter.vue'
|
||||||
import TableEmptyState from '../shared/TableEmptyState.vue'
|
import EnterpriseListPage from '../shared/EnterpriseListPage.vue'
|
||||||
import TableLoadingState from '../shared/TableLoadingState.vue'
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'DigitalEmployeeListPanel'
|
name: 'DigitalEmployeeListPanel'
|
||||||
@@ -244,6 +206,18 @@ const pageNumbers = computed(() => {
|
|||||||
const start = Math.max(1, Math.min(currentPage.value - 3, total - 6))
|
const start = Math.max(1, Math.min(currentPage.value - 3, total - 6))
|
||||||
return Array.from({ length: 7 }, (_, index) => start + index)
|
return Array.from({ length: 7 }, (_, index) => start + index)
|
||||||
})
|
})
|
||||||
|
const paginationSummary = computed(() =>
|
||||||
|
`共 ${props.visibleEmployees.length} 条,目前第 ${currentPage.value} / ${totalPages.value} 页`
|
||||||
|
)
|
||||||
|
const emptyState = {
|
||||||
|
eyebrow: '数字员工',
|
||||||
|
title: '暂无匹配的数字员工',
|
||||||
|
description: '当前没有符合搜索条件的后台执行技能。',
|
||||||
|
icon: 'mdi mdi-account-cog-outline',
|
||||||
|
tone: 'theme',
|
||||||
|
artLabel: 'STAFF',
|
||||||
|
tips: ['数字员工已从规则中心拆出为独立入口', '运行与定时操作统一进入详情后处理']
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [props.keyword, props.selectedStatus, props.selectedEnabledState, props.selectedExecutionMode],
|
() => [props.keyword, props.selectedStatus, props.selectedEnabledState, props.selectedExecutionMode],
|
||||||
@@ -268,7 +242,7 @@ function selectFilter(type, value) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped src="../../assets/styles/views/audit-view.css"></style>
|
<style scoped src="../../assets/styles/views/digital-employees-view.css"></style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.digital-employee-list-panel {
|
.digital-employee-list-panel {
|
||||||
@@ -276,62 +250,8 @@ function selectFilter(type, value) {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.digital-employee-list-panel .digital-table-wrap {
|
|
||||||
flex: 1 1 0;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-employee-list-panel .digital-employee-pagination {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto 1fr;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-employee-list-panel .digital-employee-pagination .page-summary {
|
|
||||||
justify-self: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-employee-list-panel .pager {
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 4px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-employee-list-panel .pager button {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 9px;
|
|
||||||
background: transparent;
|
|
||||||
color: #334155;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 800;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-employee-list-panel .pager button:hover:not(.active):not(:disabled) {
|
|
||||||
background: #fff;
|
|
||||||
color: var(--theme-primary-active);
|
|
||||||
box-shadow: 0 1px 4px rgba(15, 23, 42, .08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-employee-list-panel .pager button.active {
|
|
||||||
background: var(--theme-primary-active);
|
|
||||||
color: #fff;
|
|
||||||
box-shadow: 0 8px 16px var(--theme-primary-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-employee-list-panel .pager button:disabled {
|
|
||||||
color: #cbd5e1;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,10 +2,31 @@
|
|||||||
<section class="digital-employee-list-panel digital-work-records">
|
<section class="digital-employee-list-panel digital-work-records">
|
||||||
<Transition name="skill-view" mode="out-in">
|
<Transition name="skill-view" mode="out-in">
|
||||||
<!-- 列表视图 -->
|
<!-- 列表视图 -->
|
||||||
<div v-if="!selectedRunDetail" key="list" class="digital-work-records-list-stage">
|
<EnterpriseListPage
|
||||||
<div class="list-toolbar">
|
v-if="!selectedRunDetail"
|
||||||
<div class="filter-set">
|
key="list"
|
||||||
<label class="search-filter">
|
variant="digital-employee-list-panel digital-work-records-list-stage"
|
||||||
|
:panel="false"
|
||||||
|
:loading="loading && !runs.length"
|
||||||
|
:error="errorMessage"
|
||||||
|
:empty="!visibleRuns.length"
|
||||||
|
:empty-state="workRecordsEmptyState"
|
||||||
|
:show-pagination="!loading && !errorMessage && visibleRuns.length > 0"
|
||||||
|
:current-page="currentPage"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:pages="pageNumbers"
|
||||||
|
:show-page-size="false"
|
||||||
|
:summary="paginationSummary"
|
||||||
|
:total="filteredRuns.length"
|
||||||
|
:total-pages="totalPages"
|
||||||
|
loading-title="工作记录同步中"
|
||||||
|
loading-message="正在读取数字员工近期执行记录"
|
||||||
|
loading-icon="mdi mdi-clipboard-text-clock-outline"
|
||||||
|
error-title="工作记录加载失败"
|
||||||
|
@update:current-page="currentPage = $event"
|
||||||
|
>
|
||||||
|
<template #filters>
|
||||||
|
<label class="list-search">
|
||||||
<i class="mdi mdi-magnify"></i>
|
<i class="mdi mdi-magnify"></i>
|
||||||
<input
|
<input
|
||||||
v-model="listKeyword"
|
v-model="listKeyword"
|
||||||
@@ -39,9 +60,9 @@
|
|||||||
@close="closeFilterPopover"
|
@close="closeFilterPopover"
|
||||||
@select="selectStatus"
|
@select="selectStatus"
|
||||||
/>
|
/>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<div class="toolbar-actions">
|
<template #actions>
|
||||||
<button
|
<button
|
||||||
v-if="listKeyword || activeFilterTokens.length"
|
v-if="listKeyword || activeFilterTokens.length"
|
||||||
class="ghost-filter-btn"
|
class="ghost-filter-btn"
|
||||||
@@ -51,55 +72,44 @@
|
|||||||
<i class="mdi mdi-filter-remove-outline"></i>
|
<i class="mdi mdi-filter-remove-outline"></i>
|
||||||
<span>清空筛选</span>
|
<span>清空筛选</span>
|
||||||
</button>
|
</button>
|
||||||
|
<AuditPickerFilter
|
||||||
|
id="refreshInterval"
|
||||||
|
title="选择刷新时间"
|
||||||
|
close-label="关闭刷新时间选择"
|
||||||
|
:active-filter-popover="activeFilterPopover"
|
||||||
|
:label="`刷新时间 ${refreshIntervalLabel}`"
|
||||||
|
:options="refreshIntervalPickerOptions"
|
||||||
|
:selected-value="refreshInterval"
|
||||||
|
@toggle="toggleFilterPopover"
|
||||||
|
@close="closeFilterPopover"
|
||||||
|
@select="changeRefreshInterval"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
class="create-btn digital-refresh-action"
|
class="create-btn digital-refresh-action digital-refresh-now"
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
|
aria-label="立即刷新工作记录"
|
||||||
@click="loadWorkRecords(true)"
|
@click="loadWorkRecords(true)"
|
||||||
>
|
>
|
||||||
<i class="mdi mdi-refresh"></i>
|
<i :class="loading ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-refresh'"></i>
|
||||||
<span>{{ loading ? '刷新中...' : '刷新' }}</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="hint">
|
|
||||||
<i class="mdi mdi-information-outline"></i>
|
|
||||||
查看数字员工近期执行记录、状态和结果摘要。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
<template #meta>
|
||||||
<div v-if="activeFilterTokens.length" class="active-filter-strip">
|
<div v-if="activeFilterTokens.length" class="active-filter-strip">
|
||||||
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
|
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
|
||||||
{{ token }}
|
{{ token }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="table-wrap digital-table-wrap" :class="{ 'is-empty': !loading && !errorMessage && !visibleRuns.length }">
|
<template #error>
|
||||||
<div v-if="loading && !runs.length" class="table-state">
|
|
||||||
<TableLoadingState
|
|
||||||
variant="panel"
|
|
||||||
title="工作记录同步中"
|
|
||||||
message="正在读取数字员工近期执行记录"
|
|
||||||
icon="mdi mdi-clipboard-text-clock-outline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="errorMessage" class="table-state error">
|
|
||||||
<i class="mdi mdi-alert-circle-outline"></i>
|
<i class="mdi mdi-alert-circle-outline"></i>
|
||||||
<p>{{ errorMessage }}</p>
|
<p>{{ errorMessage }}</p>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<TableEmptyState
|
<template #table>
|
||||||
v-else-if="!visibleRuns.length"
|
<table class="digital-work-records-table">
|
||||||
eyebrow="工作记录"
|
|
||||||
title="暂无匹配的工作记录"
|
|
||||||
description="当前没有符合搜索条件的数字员工工作记录。"
|
|
||||||
icon="mdi mdi-clipboard-text-clock-outline"
|
|
||||||
tone="theme"
|
|
||||||
art-label="RECORDS"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<table v-else class="digital-employees-table digital-work-records-table">
|
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col class="col-time">
|
<col class="col-time">
|
||||||
<col class="col-module">
|
<col class="col-module">
|
||||||
@@ -132,12 +142,9 @@
|
|||||||
<td>{{ resolveWorkRecordModuleLabel(run) }}</td>
|
<td>{{ resolveWorkRecordModuleLabel(run) }}</td>
|
||||||
<td>{{ resolveWorkRecordSourceLabel(run.source) }}</td>
|
<td>{{ resolveWorkRecordSourceLabel(run.source) }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="work-record-status-stack">
|
<span class="status-tag" :class="resolveWorkRecordStatusTone(run)">
|
||||||
<span class="status-pill" :class="resolveWorkRecordStatusTone(run)">
|
|
||||||
{{ resolveWorkRecordStatusLabel(run) }}
|
{{ resolveWorkRecordStatusLabel(run) }}
|
||||||
</span>
|
</span>
|
||||||
<span>{{ resolveWorkRecordStatusNote(run) }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="work-record-summary-cell">
|
<td class="work-record-summary-cell">
|
||||||
<strong>{{ resolveWorkRecordTitle(run) }}</strong>
|
<strong>{{ resolveWorkRecordTitle(run) }}</strong>
|
||||||
@@ -148,59 +155,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</template>
|
||||||
|
</EnterpriseListPage>
|
||||||
<footer v-if="!loading && !errorMessage && visibleRuns.length" class="list-foot digital-employee-pagination">
|
|
||||||
<span class="page-summary">共 {{ filteredRuns.length }} 条,目前第 {{ currentPage }} / {{ totalPages }} 页</span>
|
|
||||||
<div class="pager" aria-label="工作记录分页">
|
|
||||||
<button class="page-nav" type="button" :disabled="currentPage === 1" @click="currentPage--">
|
|
||||||
<i class="mdi mdi-chevron-left"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-for="page in pageNumbers"
|
|
||||||
:key="page"
|
|
||||||
class="page-number"
|
|
||||||
:class="{ active: currentPage === page }"
|
|
||||||
type="button"
|
|
||||||
@click="currentPage = page"
|
|
||||||
>
|
|
||||||
{{ page }}
|
|
||||||
</button>
|
|
||||||
<button class="page-nav" type="button" :disabled="currentPage === totalPages" @click="currentPage++">
|
|
||||||
<i class="mdi mdi-chevron-right"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 详情视图 (全屏样式,参考 AuditJsonRiskRuleDetail) -->
|
<!-- 详情视图 (全屏样式,参考 AuditJsonRiskRuleDetail) -->
|
||||||
<div v-else key="detail" class="json-risk-editor-shell panel work-records-detail-stage">
|
<div v-else key="detail" class="json-risk-editor-shell panel work-records-detail-stage">
|
||||||
<header class="json-risk-editor-head asset-detail-topbar list-toolbar">
|
|
||||||
<div class="json-risk-editor-title asset-detail-topbar-main filter-set">
|
|
||||||
<div class="json-risk-head-copy">
|
|
||||||
<div class="json-risk-head-title-row">
|
|
||||||
<h2>{{ resolveWorkRecordTitle(selectedRunDetail) }}</h2>
|
|
||||||
</div>
|
|
||||||
<p class="json-risk-head-subtitle">
|
|
||||||
执行工作流:{{ resolveWorkRecordModuleLabel(selectedRunDetail) }}
|
|
||||||
</p>
|
|
||||||
<div class="json-risk-head-meta">
|
|
||||||
<span>Run ID:{{ selectedRunDetail.run_id }}</span>
|
|
||||||
<span>触发来源:{{ resolveWorkRecordSourceLabel(selectedRunDetail.source) }}</span>
|
|
||||||
<span>开始时间:{{ formatWorkRecordDateTime(selectedRunDetail.started_at) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="json-risk-score-ring"
|
|
||||||
:class="selectedRunDetail.status"
|
|
||||||
>
|
|
||||||
<strong style="font-size: 16px; font-weight: 900;">{{ resolveWorkRecordStatusLabel(selectedRunDetail) }}</strong>
|
|
||||||
<span>运行状态</span>
|
|
||||||
<em>{{ resolveWorkRecordStatusNote(selectedRunDetail) || '执行完毕' }}</em>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div v-if="detailLoading" class="work-record-detail-state panel" style="min-height: 200px; display: grid; place-items: center; border: 0;">
|
<div v-if="detailLoading" class="work-record-detail-state panel" style="min-height: 200px; display: grid; place-items: center; border: 0;">
|
||||||
<TableLoadingState
|
<TableLoadingState
|
||||||
variant="panel"
|
variant="panel"
|
||||||
@@ -324,11 +283,15 @@
|
|||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
import AuditPickerFilter from './AuditPickerFilter.vue'
|
import AuditPickerFilter from './AuditPickerFilter.vue'
|
||||||
import TableEmptyState from '../shared/TableEmptyState.vue'
|
import EnterpriseListPage from '../shared/EnterpriseListPage.vue'
|
||||||
import TableLoadingState from '../shared/TableLoadingState.vue'
|
import TableLoadingState from '../shared/TableLoadingState.vue'
|
||||||
import { fetchAgentRunDetail, fetchAgentRuns } from '../../services/agentAssets.js'
|
import { fetchAgentRunDetail, fetchAgentRuns } from '../../services/agentAssets.js'
|
||||||
import { useToast } from '../../composables/useToast.js'
|
import { useToast } from '../../composables/useToast.js'
|
||||||
import { AGENT_RUN_POLL_INTERVAL_MS } from '../../utils/agentRunMonitor.js'
|
import {
|
||||||
|
DEFAULT_REFRESH_INTERVAL_MS,
|
||||||
|
REFRESH_INTERVAL_OPTIONS,
|
||||||
|
formatRefreshInterval
|
||||||
|
} from '../../utils/refreshIntervalOptions.js'
|
||||||
import {
|
import {
|
||||||
formatWorkRecordDateTime,
|
formatWorkRecordDateTime,
|
||||||
formatWorkRecordSummary,
|
formatWorkRecordSummary,
|
||||||
@@ -345,7 +308,7 @@ defineOptions({
|
|||||||
name: 'DigitalEmployeeWorkRecords'
|
name: 'DigitalEmployeeWorkRecords'
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['summary-change', 'detail-open-change'])
|
const emit = defineEmits(['summary-change', 'detail-open-change', 'detail-topbar-change'])
|
||||||
|
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
const runs = ref([])
|
const runs = ref([])
|
||||||
@@ -356,20 +319,77 @@ const detailLoading = ref(false)
|
|||||||
const detailError = ref('')
|
const detailError = ref('')
|
||||||
const selectedRunId = ref('')
|
const selectedRunId = ref('')
|
||||||
const selectedRunDetail = ref(null)
|
const selectedRunDetail = ref(null)
|
||||||
|
const refreshInterval = ref(DEFAULT_REFRESH_INTERVAL_MS)
|
||||||
|
|
||||||
watch(detailOpen, (newVal) => {
|
watch(detailOpen, (newVal) => {
|
||||||
emit('detail-open-change', newVal)
|
emit('detail-open-change', newVal)
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
let pollTimer = 0
|
let pollTimer = 0
|
||||||
|
|
||||||
const totalCount = computed(() => runs.value.length)
|
const workRecordSummary = computed(() =>
|
||||||
const successCount = computed(() => runs.value.filter((run) => run.status === 'succeeded').length)
|
runs.value.reduce(
|
||||||
const failedCount = computed(() => runs.value.filter((run) => run.status === 'failed').length)
|
(summary, run) => {
|
||||||
|
summary.total += 1
|
||||||
|
if (run.status === 'succeeded') {
|
||||||
|
summary.succeeded += 1
|
||||||
|
} else if (run.status === 'failed') {
|
||||||
|
summary.failed += 1
|
||||||
|
}
|
||||||
|
return summary
|
||||||
|
},
|
||||||
|
{ total: 0, succeeded: 0, failed: 0 }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const workRecordDetailTopBar = computed(() => {
|
||||||
|
const detail = selectedRunDetail.value
|
||||||
|
if (!detail) return null
|
||||||
|
|
||||||
|
const status = String(detail.status || '').toLowerCase()
|
||||||
|
const statusColor =
|
||||||
|
status === 'failed'
|
||||||
|
? '#ef4444'
|
||||||
|
: status === 'succeeded'
|
||||||
|
? 'var(--success)'
|
||||||
|
: '#3b82f6'
|
||||||
|
|
||||||
|
return {
|
||||||
|
view: {
|
||||||
|
title: resolveWorkRecordTitle(detail),
|
||||||
|
desc: `执行工作流:${resolveWorkRecordModuleLabel(detail)}`
|
||||||
|
},
|
||||||
|
kpis: [
|
||||||
|
{
|
||||||
|
label: '运行状态',
|
||||||
|
value: resolveWorkRecordStatusLabel(detail),
|
||||||
|
unit: '',
|
||||||
|
meta: resolveWorkRecordStatusNote(detail) || '执行完毕',
|
||||||
|
trend: status === 'failed' ? 'down' : 'up',
|
||||||
|
color: statusColor
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
workRecordDetailTopBar,
|
||||||
|
(value) => {
|
||||||
|
emit('detail-topbar-change', value)
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
const listKeyword = ref('')
|
const listKeyword = ref('')
|
||||||
const activeModule = ref('全部')
|
const activeModule = ref('全部')
|
||||||
const activeStatus = ref('全部')
|
const activeStatus = ref('全部')
|
||||||
const activeFilterPopover = ref('')
|
const activeFilterPopover = ref('')
|
||||||
|
const workRecordsEmptyState = {
|
||||||
|
eyebrow: '工作记录',
|
||||||
|
title: '暂无匹配的工作记录',
|
||||||
|
description: '当前没有符合搜索条件的数字员工工作记录。',
|
||||||
|
icon: 'mdi mdi-clipboard-text-clock-outline',
|
||||||
|
tone: 'theme',
|
||||||
|
artLabel: 'RECORDS'
|
||||||
|
}
|
||||||
|
|
||||||
const modulePickerOptions = computed(() => {
|
const modulePickerOptions = computed(() => {
|
||||||
const set = new Set(runs.value.map((run) => resolveWorkRecordModuleLabel(run)))
|
const set = new Set(runs.value.map((run) => resolveWorkRecordModuleLabel(run)))
|
||||||
@@ -386,6 +406,11 @@ const statusPickerOptions = computed(() => {
|
|||||||
...Array.from(set).map(s => ({ label: s, value: s }))
|
...Array.from(set).map(s => ({ label: s, value: s }))
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
const refreshIntervalPickerOptions = REFRESH_INTERVAL_OPTIONS.map((option) => ({
|
||||||
|
label: `每 ${option.label}`,
|
||||||
|
value: option.value
|
||||||
|
}))
|
||||||
|
const refreshIntervalLabel = computed(() => formatRefreshInterval(refreshInterval.value))
|
||||||
|
|
||||||
const activeFilterTokens = computed(() => {
|
const activeFilterTokens = computed(() => {
|
||||||
const tokens = []
|
const tokens = []
|
||||||
@@ -427,6 +452,9 @@ const pageNumbers = computed(() => {
|
|||||||
const start = Math.max(1, Math.min(currentPage.value - 3, total - 6))
|
const start = Math.max(1, Math.min(currentPage.value - 3, total - 6))
|
||||||
return Array.from({ length: 7 }, (_, index) => start + index)
|
return Array.from({ length: 7 }, (_, index) => start + index)
|
||||||
})
|
})
|
||||||
|
const paginationSummary = computed(() =>
|
||||||
|
`共 ${filteredRuns.value.length} 条,目前第 ${currentPage.value} / ${totalPages.value} 页`
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [listKeyword.value, activeModule.value, activeStatus.value],
|
() => [listKeyword.value, activeModule.value, activeStatus.value],
|
||||||
@@ -464,6 +492,12 @@ function selectStatus(val) {
|
|||||||
closeFilterPopover()
|
closeFilterPopover()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changeRefreshInterval(value) {
|
||||||
|
refreshInterval.value = Number(value) || DEFAULT_REFRESH_INTERVAL_MS
|
||||||
|
closeFilterPopover()
|
||||||
|
startPolling()
|
||||||
|
}
|
||||||
|
|
||||||
function resetFilters() {
|
function resetFilters() {
|
||||||
listKeyword.value = ''
|
listKeyword.value = ''
|
||||||
activeModule.value = '全部'
|
activeModule.value = '全部'
|
||||||
@@ -479,9 +513,9 @@ async function loadWorkRecords(showToast = false) {
|
|||||||
const payload = await fetchAgentRuns({ agent: 'hermes', limit: 100 })
|
const payload = await fetchAgentRuns({ agent: 'hermes', limit: 100 })
|
||||||
runs.value = Array.isArray(payload) ? payload : []
|
runs.value = Array.isArray(payload) ? payload : []
|
||||||
emit('summary-change', {
|
emit('summary-change', {
|
||||||
total: totalCount.value,
|
total: workRecordSummary.value.total,
|
||||||
succeeded: successCount.value,
|
succeeded: workRecordSummary.value.succeeded,
|
||||||
failed: failedCount.value
|
failed: workRecordSummary.value.failed
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage.value = error?.message || '工作记录加载失败,请稍后重试。'
|
errorMessage.value = error?.message || '工作记录加载失败,请稍后重试。'
|
||||||
@@ -542,7 +576,7 @@ function startPolling() {
|
|||||||
stopPolling()
|
stopPolling()
|
||||||
pollTimer = window.setInterval(() => {
|
pollTimer = window.setInterval(() => {
|
||||||
loadWorkRecords(false)
|
loadWorkRecords(false)
|
||||||
}, AGENT_RUN_POLL_INTERVAL_MS)
|
}, refreshInterval.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopPolling() {
|
function stopPolling() {
|
||||||
@@ -564,101 +598,104 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<style scoped src="../../assets/styles/views/audit-view.css"></style>
|
<style scoped src="../../assets/styles/views/audit-view.css"></style>
|
||||||
<style scoped src="../../assets/styles/views/audit-view-part2.css"></style>
|
<style scoped src="../../assets/styles/views/audit-view-part2.css"></style>
|
||||||
<style scoped src="../../assets/styles/views/digital-employees-view.css"></style>
|
|
||||||
<style scoped src="../../assets/styles/components/digital-employee-work-records.css"></style>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.digital-employee-list-panel {
|
.digital-work-records {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.digital-employee-list-panel,
|
||||||
|
.digital-work-records-list-stage {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.digital-employee-list-panel .digital-table-wrap {
|
.digital-work-records-table {
|
||||||
|
min-width: 1180px;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.digital-work-records-table .col-time { width: 14%; }
|
||||||
|
.digital-work-records-table .col-module { width: 13%; }
|
||||||
|
.digital-work-records-table .col-source { width: 10%; }
|
||||||
|
.digital-work-records-table .col-status { width: 16%; }
|
||||||
|
.digital-work-records-table .col-summary { width: 31%; }
|
||||||
|
.digital-work-records-table .col-trace { width: 16%; }
|
||||||
|
|
||||||
|
.work-record-row {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-record-row:focus-visible {
|
||||||
|
box-shadow: inset 0 0 0 2px rgba(58, 124, 165, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-record-summary-cell {
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-record-summary-cell strong,
|
||||||
|
.work-record-summary-cell span,
|
||||||
|
.work-record-summary-cell em {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-record-summary-cell strong {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-record-summary-cell span {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-record-summary-cell em {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-record-trace-cell {
|
||||||
|
color: #2563eb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-records-detail-stage,
|
||||||
|
.work-record-detail-shell {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.digital-employee-list-panel .digital-employee-pagination {
|
.digital-work-records :deep(.toolbar-actions .picker-filter),
|
||||||
flex: 0 0 auto;
|
.digital-work-records :deep(.toolbar-actions .picker-trigger) {
|
||||||
display: grid;
|
min-width: 148px;
|
||||||
grid-template-columns: 1fr auto 1fr;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.digital-employee-list-panel .digital-employee-pagination .page-summary {
|
.digital-refresh-now {
|
||||||
justify-self: start;
|
width: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.digital-employee-list-panel .pager {
|
.digital-refresh-now .mdi {
|
||||||
display: inline-flex;
|
font-size: 18px;
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 4px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.digital-employee-list-panel .pager button {
|
.work-record-detail-body.inline-detail {
|
||||||
width: 32px;
|
padding: 0;
|
||||||
height: 32px;
|
overflow: visible;
|
||||||
border: 0;
|
|
||||||
border-radius: 9px;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #334155;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 800;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.digital-employee-list-panel .pager button:hover:not(.active):not(:disabled) {
|
|
||||||
background: #fff;
|
|
||||||
color: var(--theme-primary-active);
|
|
||||||
box-shadow: 0 1px 4px rgba(15, 23, 42, .08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-employee-list-panel .pager button.active {
|
|
||||||
background: var(--theme-primary-active);
|
|
||||||
color: #fff;
|
|
||||||
box-shadow: 0 8px 16px var(--theme-primary-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-employee-list-panel .pager button:disabled {
|
|
||||||
color: #cbd5e1;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-work-records-list-stage {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.work-records-detail-stage {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 风险环的成功、失败、执行中状态配色 */
|
|
||||||
.json-risk-score-ring.succeeded {
|
|
||||||
--score-ring: #16a34a;
|
|
||||||
--score-ring-bg: #f0fdf4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-score-ring.failed {
|
|
||||||
--score-ring: #dc2626;
|
|
||||||
--score-ring-bg: #fef2f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-risk-score-ring.running {
|
|
||||||
--score-ring: #2563eb;
|
|
||||||
--score-ring-bg: #eff6ff;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ const {
|
|||||||
startDocumentInboxPolling,
|
startDocumentInboxPolling,
|
||||||
stopDocumentInboxPolling
|
stopDocumentInboxPolling
|
||||||
} = useDocumentCenterInbox()
|
} = useDocumentCenterInbox()
|
||||||
|
let inboxInitialRefreshTimer = null
|
||||||
|
|
||||||
const sidebarMeta = {
|
const sidebarMeta = {
|
||||||
overview: { label: '分析看板' },
|
overview: { label: '分析看板' },
|
||||||
@@ -159,7 +160,6 @@ const sidebarMeta = {
|
|||||||
policies: { label: '知识管理' },
|
policies: { label: '知识管理' },
|
||||||
audit: { label: '规则中心' },
|
audit: { label: '规则中心' },
|
||||||
digitalEmployees: { label: '数字员工' },
|
digitalEmployees: { label: '数字员工' },
|
||||||
logs: { label: '系统日志' },
|
|
||||||
employees: { label: '员工管理' },
|
employees: { label: '员工管理' },
|
||||||
settings: { label: '系统设置' }
|
settings: { label: '系统设置' }
|
||||||
}
|
}
|
||||||
@@ -173,8 +173,27 @@ const decoratedNavItems = computed(() =>
|
|||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
function clearInboxInitialRefreshTimer() {
|
||||||
|
if (inboxInitialRefreshTimer && typeof window !== 'undefined') {
|
||||||
|
window.clearTimeout(inboxInitialRefreshTimer)
|
||||||
|
inboxInitialRefreshTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleInboxInitialRefresh() {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInboxInitialRefreshTimer()
|
||||||
|
inboxInitialRefreshTimer = window.setTimeout(() => {
|
||||||
|
inboxInitialRefreshTimer = null
|
||||||
void refreshDocumentInbox()
|
void refreshDocumentInbox()
|
||||||
|
}, props.activeView === 'documents' ? 1200 : 6000)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
scheduleInboxInitialRefresh()
|
||||||
startDocumentInboxPolling()
|
startDocumentInboxPolling()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -271,7 +290,18 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.activeView,
|
||||||
|
(activeView, previousView) => {
|
||||||
|
if (activeView === 'documents' && previousView !== 'documents') {
|
||||||
|
clearInboxInitialRefreshTimer()
|
||||||
|
void refreshDocumentInbox({ force: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
clearInboxInitialRefreshTimer()
|
||||||
stopDocumentInboxPolling()
|
stopDocumentInboxPolling()
|
||||||
closeCollapsedUserMenuNow()
|
closeCollapsedUserMenuNow()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -84,7 +84,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="isRequestDetail">
|
<template v-else-if="isRequestDetail">
|
||||||
<div class="detail-alert-strip">
|
<div class="detail-topbar-actions">
|
||||||
|
<div v-if="detailKpis.length" class="kpi-chips detail-kpi-chips">
|
||||||
|
<div
|
||||||
|
v-for="kpi in detailKpis"
|
||||||
|
:key="kpi.label"
|
||||||
|
class="kpi-chip detail-kpi-chip"
|
||||||
|
:style="{ '--chip-color': kpi.color }"
|
||||||
|
>
|
||||||
|
<span class="chip-value">{{ kpi.value }}<small>{{ kpi.unit }}</small></span>
|
||||||
|
<span class="chip-label">{{ kpi.label }}</span>
|
||||||
|
<span class="chip-delta" :class="kpi.trend">{{ kpi.meta }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="detailAlerts.length" class="detail-alert-strip">
|
||||||
<span
|
<span
|
||||||
v-for="alert in detailAlerts"
|
v-for="alert in detailAlerts"
|
||||||
:key="alert.label"
|
:key="alert.label"
|
||||||
@@ -95,6 +108,7 @@
|
|||||||
<span>{{ alert.label }}</span>
|
<span>{{ alert.label }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="isWorkbench">
|
<template v-else-if="isWorkbench">
|
||||||
@@ -135,16 +149,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="isLogs">
|
|
||||||
<div class="kpi-chips">
|
|
||||||
<div v-for="kpi in logsKpis" :key="kpi.label" class="kpi-chip" :style="{ '--chip-color': kpi.color }">
|
|
||||||
<span class="chip-value">{{ kpi.value }}<small>{{ kpi.unit }}</small></span>
|
|
||||||
<span class="chip-label">{{ kpi.label }}</span>
|
|
||||||
<span class="chip-delta" :class="kpi.trend">{{ kpi.meta }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="showDigitalEmployeeWorkRecordKpis">
|
<template v-else-if="showDigitalEmployeeWorkRecordKpis">
|
||||||
<div class="kpi-chips">
|
<div class="kpi-chips">
|
||||||
<div
|
<div
|
||||||
@@ -215,10 +219,6 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => null
|
default: () => null
|
||||||
},
|
},
|
||||||
logsSummary: {
|
|
||||||
type: Object,
|
|
||||||
default: () => null
|
|
||||||
},
|
|
||||||
requestSummary: {
|
requestSummary: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => null
|
default: () => null
|
||||||
@@ -239,14 +239,14 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
logDetailMode: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
detailAlerts: {
|
detailAlerts: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
detailKpis: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
customRange: {
|
customRange: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({ start: '2024-07-06', end: '2024-07-12' })
|
default: () => ({ start: '2024-07-06', end: '2024-07-12' })
|
||||||
@@ -265,10 +265,9 @@ const emit = defineEmits([
|
|||||||
const isChat = computed(() => props.activeView === 'chat')
|
const isChat = computed(() => props.activeView === 'chat')
|
||||||
const isOverview = computed(() => props.activeView === 'overview')
|
const isOverview = computed(() => props.activeView === 'overview')
|
||||||
const isWorkbench = computed(() => props.activeView === 'workbench')
|
const isWorkbench = computed(() => props.activeView === 'workbench')
|
||||||
const isRequestDetail = computed(() => ['requests', 'documents'].includes(props.activeView) && props.detailMode)
|
const isRequestDetail = computed(() => ['requests', 'documents', 'audit', 'digitalEmployees'].includes(props.activeView) && props.detailMode)
|
||||||
const isDocuments = computed(() => props.activeView === 'documents' && !props.detailMode)
|
const isDocuments = computed(() => props.activeView === 'documents' && !props.detailMode)
|
||||||
const isRequests = computed(() => props.activeView === 'requests')
|
const isRequests = computed(() => props.activeView === 'requests')
|
||||||
const isLogs = computed(() => props.activeView === 'logs' && !props.logDetailMode)
|
|
||||||
const isDigitalEmployees = computed(() => props.activeView === 'digitalEmployees')
|
const isDigitalEmployees = computed(() => props.activeView === 'digitalEmployees')
|
||||||
const isApproval = computed(() => props.activeView === 'approval')
|
const isApproval = computed(() => props.activeView === 'approval')
|
||||||
const isPolicies = computed(() => props.activeView === 'policies')
|
const isPolicies = computed(() => props.activeView === 'policies')
|
||||||
@@ -310,21 +309,6 @@ const documentKpis = computed(() => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const logsKpis = computed(() => {
|
|
||||||
const summary = props.logsSummary ?? {}
|
|
||||||
const total = Number(summary.total ?? 0)
|
|
||||||
const errors = Number(summary.errors ?? 0)
|
|
||||||
const warnings = Number(summary.warnings ?? 0)
|
|
||||||
const info = Number(summary.info ?? 0)
|
|
||||||
|
|
||||||
return [
|
|
||||||
{ label: '系统日志', value: total, unit: '条', meta: '当前', trend: 'up', color: 'var(--theme-primary)' },
|
|
||||||
{ label: '错误数量', value: errors, unit: '条', meta: errors > 0 ? '需要关注' : '运行正常', trend: errors > 0 ? 'down' : 'up', color: '#ef4444' },
|
|
||||||
{ label: '告警数量', value: warnings, unit: '条', meta: warnings > 0 ? '建议排查' : '暂无告警', trend: warnings > 0 ? 'down' : 'up', color: '#f59e0b' },
|
|
||||||
{ label: '正常数量', value: info, unit: '条', meta: total ? `占比 ${Math.round((info / total) * 100)}%` : '等待数据', trend: 'up', color: 'var(--success)' }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
const showDigitalEmployeeWorkRecordKpis = computed(() => {
|
const showDigitalEmployeeWorkRecordKpis = computed(() => {
|
||||||
const summary = props.digitalEmployeeSummary ?? {}
|
const summary = props.digitalEmployeeSummary ?? {}
|
||||||
return isDigitalEmployees.value && summary.section === 'workRecords'
|
return isDigitalEmployees.value && summary.section === 'workRecords'
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<article class="enterprise-detail-page" :class="variant">
|
<article class="enterprise-detail-page" :class="variant">
|
||||||
<div class="detail-scroll">
|
<div class="detail-scroll">
|
||||||
<TableLoadingState
|
<section v-if="error" class="detail-inline-state panel error">
|
||||||
v-if="loading"
|
|
||||||
class="detail-loading-state panel"
|
|
||||||
variant="panel"
|
|
||||||
:title="loadingTitle"
|
|
||||||
:message="loadingMessage"
|
|
||||||
:icon="loadingIcon"
|
|
||||||
:show-skeleton="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<section v-else-if="error" class="detail-inline-state panel error">
|
|
||||||
<slot name="error" :error="error">
|
<slot name="error" :error="error">
|
||||||
<i class="mdi mdi-alert-circle-outline"></i>
|
<i class="mdi mdi-alert-circle-outline"></i>
|
||||||
<div>
|
<div>
|
||||||
@@ -21,6 +11,16 @@
|
|||||||
</slot>
|
</slot>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<TableLoadingState
|
||||||
|
v-else-if="loading"
|
||||||
|
class="detail-loading-state panel"
|
||||||
|
variant="panel"
|
||||||
|
:title="loadingTitle"
|
||||||
|
:message="loadingMessage"
|
||||||
|
:icon="loadingIcon"
|
||||||
|
:show-skeleton="false"
|
||||||
|
/>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<section v-if="$slots.hero" class="detail-hero panel">
|
<section v-if="$slots.hero" class="detail-hero panel">
|
||||||
<slot name="hero"></slot>
|
<slot name="hero"></slot>
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer v-if="backLabel || $slots.actions" class="detail-actions">
|
<footer v-if="backLabel || $slots.actions" class="detail-actions" :class="actionsClass">
|
||||||
<button v-if="backLabel" class="back-action" type="button" @click="emit('back')">
|
<button v-if="backLabel" class="back-action" type="button" @click="emit('back')">
|
||||||
<i class="mdi mdi-arrow-left"></i>
|
<i class="mdi mdi-arrow-left"></i>
|
||||||
<span>{{ backLabel }}</span>
|
<span>{{ backLabel }}</span>
|
||||||
@@ -57,6 +57,7 @@
|
|||||||
import TableLoadingState from './TableLoadingState.vue'
|
import TableLoadingState from './TableLoadingState.vue'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
|
actionsClass: { type: [String, Array, Object], default: '' },
|
||||||
backLabel: { type: String, default: '' },
|
backLabel: { type: String, default: '' },
|
||||||
error: { type: String, default: '' },
|
error: { type: String, default: '' },
|
||||||
errorTitle: { type: String, default: '详情加载失败' },
|
errorTitle: { type: String, default: '详情加载失败' },
|
||||||
@@ -64,7 +65,7 @@ defineProps({
|
|||||||
loadingIcon: { type: String, default: 'mdi mdi-file-document-outline' },
|
loadingIcon: { type: String, default: 'mdi mdi-file-document-outline' },
|
||||||
loadingMessage: { type: String, default: '' },
|
loadingMessage: { type: String, default: '' },
|
||||||
loadingTitle: { type: String, default: '正在加载详情' },
|
loadingTitle: { type: String, default: '正在加载详情' },
|
||||||
variant: { type: String, default: '' }
|
variant: { type: [String, Array, Object], default: '' }
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['back'])
|
const emit = defineEmits(['back'])
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<article class="enterprise-list-page panel" :class="variant">
|
<article class="enterprise-list-page" :class="[variant, { panel }]">
|
||||||
<slot name="before"></slot>
|
<slot name="before"></slot>
|
||||||
|
|
||||||
<nav v-if="hasTabs" class="status-tabs" :aria-label="tabsLabel">
|
<nav v-if="hasTabs" class="status-tabs" :aria-label="tabsLabel">
|
||||||
@@ -51,12 +51,15 @@
|
|||||||
</slot>
|
</slot>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<slot name="meta"></slot>
|
||||||
|
|
||||||
<div class="table-wrap" :class="{ 'is-empty': empty, 'has-error': Boolean(error) }">
|
<div class="table-wrap" :class="{ 'is-empty': empty, 'has-error': Boolean(error) }">
|
||||||
<div v-if="loading" class="table-state">
|
<div v-if="loading" class="table-state">
|
||||||
<TableLoadingState
|
<TableLoadingState
|
||||||
:title="loadingTitle"
|
:title="loadingTitle"
|
||||||
:message="loadingMessage"
|
:message="loadingMessage"
|
||||||
:icon="loadingIcon"
|
:icon="loadingIcon"
|
||||||
|
floating
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -135,6 +138,7 @@ const props = defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
panel: { type: Boolean, default: true },
|
||||||
pages: {
|
pages: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
|||||||
267
web/src/components/shared/FloatingLightBandWindow.vue
Normal file
267
web/src/components/shared/FloatingLightBandWindow.vue
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
<template>
|
||||||
|
<div class="floating-light-band-window" :class="[variant, tone, motionClass]">
|
||||||
|
<span class="floating-light-band-window__mark" aria-hidden="true">
|
||||||
|
<i :class="icon"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div v-if="hasCopy" class="floating-light-band-window__copy">
|
||||||
|
<strong v-if="displayTitle">{{ displayTitle }}</strong>
|
||||||
|
<span v-if="displayMessage">{{ displayMessage }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="floating-light-band-window__progress" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
icon: { type: String, default: 'mdi mdi-loading' },
|
||||||
|
message: { type: String, default: '' },
|
||||||
|
motion: {
|
||||||
|
type: String,
|
||||||
|
default: 'loop',
|
||||||
|
validator: (value) => ['loop', 'entry'].includes(value)
|
||||||
|
},
|
||||||
|
title: { type: String, default: '' },
|
||||||
|
tone: {
|
||||||
|
type: String,
|
||||||
|
default: 'theme',
|
||||||
|
validator: (value) => ['theme', 'sky', 'success'].includes(value)
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: 'panel',
|
||||||
|
validator: (value) => ['entry', 'panel', 'detail', 'overlay', 'drawer', 'banner'].includes(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const displayTitle = computed(() => (props.variant === 'banner' && props.message ? '' : props.title))
|
||||||
|
const displayMessage = computed(() => props.message || (props.variant === 'banner' ? props.title : ''))
|
||||||
|
const hasCopy = computed(() => Boolean(displayTitle.value || displayMessage.value))
|
||||||
|
const motionClass = computed(() => `motion-${props.motion}`)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.floating-light-band-window {
|
||||||
|
--accent: var(--theme-primary);
|
||||||
|
--accent-deep: var(--theme-primary-active);
|
||||||
|
--accent-rgb: var(--theme-primary-rgb, 58, 124, 165);
|
||||||
|
|
||||||
|
width: min(380px, 100%);
|
||||||
|
max-width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 42px minmax(0, 1fr);
|
||||||
|
gap: 12px 14px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 22px 24px 20px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.26);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 20px 46px rgba(15, 23, 42, 0.14);
|
||||||
|
color: var(--muted);
|
||||||
|
animation: floatingLightBandWindowIn 360ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window.theme,
|
||||||
|
.floating-light-band-window.sky {
|
||||||
|
--accent: var(--theme-primary);
|
||||||
|
--accent-deep: var(--theme-primary-active);
|
||||||
|
--accent-rgb: var(--theme-primary-rgb, 58, 124, 165);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window.success {
|
||||||
|
--accent: var(--success);
|
||||||
|
--accent-deep: var(--success-hover);
|
||||||
|
--accent-rgb: var(--success-rgb, 47, 133, 90);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window.entry {
|
||||||
|
width: min(360px, calc(100% - 48px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window.detail,
|
||||||
|
.floating-light-band-window.drawer {
|
||||||
|
grid-template-columns: 38px minmax(0, 1fr);
|
||||||
|
padding: 18px 20px 16px;
|
||||||
|
box-shadow: 0 16px 34px rgba(15, 23, 42, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window.overlay {
|
||||||
|
width: min(360px, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window.banner {
|
||||||
|
width: 100%;
|
||||||
|
grid-template-columns: 28px minmax(0, 1fr);
|
||||||
|
gap: 8px 10px;
|
||||||
|
padding: 8px 10px 7px;
|
||||||
|
border-color: rgba(var(--accent-rgb), 0.18);
|
||||||
|
background: rgba(255, 255, 255, 0.78);
|
||||||
|
box-shadow: none;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window__mark {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: color-mix(in srgb, var(--accent) 10%, white);
|
||||||
|
color: var(--accent-deep);
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window.detail .floating-light-band-window__mark,
|
||||||
|
.floating-light-band-window.drawer .floating-light-band-window__mark {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window.banner .floating-light-band-window__mark {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window__copy {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window__copy strong {
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.35;
|
||||||
|
font-weight: 750;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window__copy span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window.detail .floating-light-band-window__copy strong,
|
||||||
|
.floating-light-band-window.drawer .floating-light-band-window__copy strong {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window.banner .floating-light-band-window__copy {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window.banner .floating-light-band-window__copy strong,
|
||||||
|
.floating-light-band-window.banner .floating-light-band-window__copy span {
|
||||||
|
color: var(--accent-deep);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 760;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window__progress {
|
||||||
|
position: relative;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
height: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(var(--accent-rgb), 0.08),
|
||||||
|
rgba(var(--accent-rgb), 0.16),
|
||||||
|
rgba(var(--accent-rgb), 0.08)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window.banner .floating-light-band-window__progress {
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window__progress::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 auto 0 0;
|
||||||
|
width: 46%;
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(var(--accent-rgb), 0),
|
||||||
|
var(--accent),
|
||||||
|
var(--accent-deep),
|
||||||
|
color-mix(in srgb, var(--accent) 40%, white),
|
||||||
|
rgba(var(--accent-rgb), 0)
|
||||||
|
);
|
||||||
|
box-shadow: 0 0 12px rgba(var(--accent-rgb), 0.36);
|
||||||
|
transform: translateX(-105%);
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window.motion-loop .floating-light-band-window__progress::before {
|
||||||
|
animation: floatingLightBandSweep 980ms cubic-bezier(0.2, 0, 0, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window.motion-entry .floating-light-band-window__progress::before {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--accent);
|
||||||
|
transform-origin: left center;
|
||||||
|
animation: floatingLightBandEntry 840ms cubic-bezier(0.2, 0, 0, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes floatingLightBandWindowIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale3d(0.92, 0.92, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale3d(1, 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes floatingLightBandSweep {
|
||||||
|
from {
|
||||||
|
transform: translateX(-105%);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateX(215%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes floatingLightBandEntry {
|
||||||
|
from {
|
||||||
|
transform: scaleX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.floating-light-band-window {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window.motion-entry .floating-light-band-window__progress::before {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--accent);
|
||||||
|
animation: none !important;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-light-band-window.motion-loop .floating-light-band-window__progress::before {
|
||||||
|
animation: floatingLightBandSweep 1800ms linear infinite !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,25 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<span v-if="floating" class="table-loading-anchor" aria-hidden="true"></span>
|
||||||
|
<Teleport to="body" :disabled="!floating">
|
||||||
<div
|
<div
|
||||||
class="table-loading"
|
class="table-loading"
|
||||||
:class="[variant, tone]"
|
:class="[variant, tone, { 'screen-floating': floating, 'modal-floating': floating && blocking }]"
|
||||||
role="status"
|
role="status"
|
||||||
:aria-label="ariaLabel"
|
:aria-label="ariaLabel"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
>
|
>
|
||||||
<span class="table-loading__spinner" aria-hidden="true">
|
<FloatingLightBandWindow
|
||||||
<i :class="icon"></i>
|
:icon="icon"
|
||||||
</span>
|
:message="message"
|
||||||
|
:motion="motion"
|
||||||
<div v-if="hasCopy" class="table-loading__copy">
|
:title="title"
|
||||||
<strong v-if="title">{{ title }}</strong>
|
:tone="tone"
|
||||||
<p v-if="message">{{ message }}</p>
|
:variant="variant"
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import FloatingLightBandWindow from './FloatingLightBandWindow.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
variant: {
|
variant: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -34,50 +39,75 @@ const props = defineProps({
|
|||||||
title: { type: String, default: '' },
|
title: { type: String, default: '' },
|
||||||
message: { type: String, default: '' },
|
message: { type: String, default: '' },
|
||||||
icon: { type: String, default: 'mdi mdi-loading' },
|
icon: { type: String, default: 'mdi mdi-loading' },
|
||||||
|
motion: {
|
||||||
|
type: String,
|
||||||
|
default: 'loop',
|
||||||
|
validator: (value) => ['loop', 'entry'].includes(value)
|
||||||
|
},
|
||||||
|
floating: { type: Boolean, default: false },
|
||||||
|
blocking: { type: Boolean, default: false },
|
||||||
showSkeleton: { type: Boolean, default: true },
|
showSkeleton: { type: Boolean, default: true },
|
||||||
skeletonRows: { type: Number, default: 5 }
|
skeletonRows: { type: Number, default: 5 }
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasCopy = computed(() => Boolean(props.title || props.message))
|
|
||||||
|
|
||||||
const ariaLabel = computed(() => [props.title, props.message].filter(Boolean).join(', ') || 'Loading')
|
const ariaLabel = computed(() => [props.title, props.message].filter(Boolean).join(', ') || 'Loading')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.table-loading {
|
.table-loading {
|
||||||
--accent: var(--theme-primary);
|
|
||||||
--accent-deep: var(--theme-primary-active);
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-loading.theme,
|
.table-loading-anchor {
|
||||||
.table-loading.sky {
|
display: block;
|
||||||
--accent: var(--theme-primary);
|
width: 0;
|
||||||
--accent-deep: var(--theme-primary-active);
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-loading.success {
|
.table-loading.screen-floating {
|
||||||
--accent: var(--success);
|
position: fixed;
|
||||||
--accent-deep: var(--success-hover);
|
inset: 0;
|
||||||
|
z-index: 430;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100dvh;
|
||||||
|
min-height: 100dvh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
background: rgba(248, 250, 252, 0.08);
|
||||||
|
backdrop-filter: blur(0.5px);
|
||||||
|
-webkit-backdrop-filter: blur(0.5px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-loading.screen-floating.modal-floating {
|
||||||
|
z-index: 560;
|
||||||
|
background: rgba(15, 23, 42, 0.18);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
-webkit-backdrop-filter: blur(2px);
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-loading.panel {
|
.table-loading.panel {
|
||||||
min-height: 220px;
|
min-height: 220px;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
gap: 12px;
|
|
||||||
padding: 28px 24px;
|
padding: 28px 24px;
|
||||||
text-align: center;
|
}
|
||||||
|
|
||||||
|
.table-loading.panel.screen-floating {
|
||||||
|
min-height: 100dvh;
|
||||||
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-loading.detail {
|
.table-loading.detail {
|
||||||
min-height: 180px;
|
min-height: 180px;
|
||||||
display: flex;
|
display: grid;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px;
|
justify-items: center;
|
||||||
padding: 22px 24px;
|
padding: 22px 24px;
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-loading.overlay,
|
.table-loading.overlay,
|
||||||
@@ -97,86 +127,9 @@ const ariaLabel = computed(() => [props.title, props.message].filter(Boolean).jo
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-loading.banner {
|
.table-loading.banner {
|
||||||
display: inline-flex;
|
display: block;
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: #255b7d;
|
color: #255b7d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-loading__spinner {
|
|
||||||
width: 38px;
|
|
||||||
height: 38px;
|
|
||||||
display: inline-grid;
|
|
||||||
place-items: center;
|
|
||||||
border: 3px solid #e2e8f0;
|
|
||||||
border-top-color: var(--accent);
|
|
||||||
border-radius: 50%;
|
|
||||||
color: var(--accent-deep);
|
|
||||||
animation: table-spinner-rotate 0.8s linear infinite !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-loading.detail .table-loading__spinner {
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-loading.banner .table-loading__spinner {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
border-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-loading__spinner i {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-loading__copy {
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-loading.panel .table-loading__copy,
|
|
||||||
.table-loading.overlay .table-loading__copy,
|
|
||||||
.table-loading.drawer .table-loading__copy {
|
|
||||||
max-width: 360px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-loading.detail .table-loading__copy {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-loading.banner .table-loading__copy {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-loading__copy strong {
|
|
||||||
color: #0f172a;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 850;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-loading__copy p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-loading.banner .table-loading__copy strong {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-loading.banner .table-loading__copy p {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes table-spinner-rotate {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export function useAppShell() {
|
|||||||
filteredRequests,
|
filteredRequests,
|
||||||
approveRequest,
|
approveRequest,
|
||||||
rejectRequest,
|
rejectRequest,
|
||||||
|
ensureLoaded: ensureRequestsLoaded,
|
||||||
reload: reloadRequests
|
reload: reloadRequests
|
||||||
} = useRequests()
|
} = useRequests()
|
||||||
const { currentUser } = useSystemState()
|
const { currentUser } = useSystemState()
|
||||||
@@ -83,23 +84,19 @@ export function useAppShell() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const detailMode = computed(() => route.name === 'app-document-detail')
|
const detailMode = computed(() => route.name === 'app-document-detail')
|
||||||
const logDetailMode = computed(() => route.name === 'app-log-detail')
|
|
||||||
const detailAlerts = computed(() => (detailMode.value ? buildDetailAlerts(selectedRequest.value) : []))
|
const detailAlerts = computed(() => (detailMode.value ? buildDetailAlerts(selectedRequest.value) : []))
|
||||||
|
|
||||||
const documentsListActive = computed(() => activeView.value === 'documents' && !detailMode.value)
|
const requestsNeeded = computed(() => ['documents', 'workbench'].includes(activeView.value))
|
||||||
const workbenchActive = computed(() => activeView.value === 'workbench')
|
|
||||||
|
|
||||||
watch(documentsListActive, (isActive, wasActive) => {
|
watch(
|
||||||
if (isActive && !wasActive) {
|
requestsNeeded,
|
||||||
void reloadRequests()
|
(isNeeded) => {
|
||||||
|
if (isNeeded) {
|
||||||
|
void ensureRequestsLoaded()
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
{ immediate: true }
|
||||||
watch(workbenchActive, (isActive, wasActive) => {
|
)
|
||||||
if (isActive && !wasActive) {
|
|
||||||
void reloadRequests()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const workbenchSummary = computed(() =>
|
const workbenchSummary = computed(() =>
|
||||||
buildWorkbenchSummary(requests.value, currentUser.value)
|
buildWorkbenchSummary(requests.value, currentUser.value)
|
||||||
@@ -118,13 +115,6 @@ export function useAppShell() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logDetailMode.value) {
|
|
||||||
return {
|
|
||||||
title: '日志详情',
|
|
||||||
desc: '查看单条日志的解析结果、上下文信息与原始记录。'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentView.value
|
return currentView.value
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -353,7 +343,6 @@ export function useAppShell() {
|
|||||||
currentView,
|
currentView,
|
||||||
customRange,
|
customRange,
|
||||||
detailMode,
|
detailMode,
|
||||||
logDetailMode,
|
|
||||||
filteredRequests,
|
filteredRequests,
|
||||||
filters,
|
filters,
|
||||||
handleApprove,
|
handleApprove,
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ const SOURCE_PRIORITY = {
|
|||||||
const documentRows = ref([])
|
const documentRows = ref([])
|
||||||
const viewedDocumentKeys = ref(readViewedDocumentKeys())
|
const viewedDocumentKeys = ref(readViewedDocumentKeys())
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const INBOX_CACHE_TTL_MS = 30000
|
||||||
let refreshTimer = null
|
let refreshTimer = null
|
||||||
|
let refreshPromise = null
|
||||||
|
let lastRefreshAt = 0
|
||||||
let viewedKeysListenerAttached = false
|
let viewedKeysListenerAttached = false
|
||||||
|
|
||||||
function normalizeClaimText(...values) {
|
function normalizeClaimText(...values) {
|
||||||
@@ -125,10 +128,22 @@ export function useDocumentCenterInbox() {
|
|||||||
const unreadCount = computed(() => countNewDocuments(documentRows.value, viewedDocumentKeys.value))
|
const unreadCount = computed(() => countNewDocuments(documentRows.value, viewedDocumentKeys.value))
|
||||||
const hasUnread = computed(() => unreadCount.value > 0)
|
const hasUnread = computed(() => unreadCount.value > 0)
|
||||||
|
|
||||||
async function refreshDocumentInbox() {
|
async function refreshDocumentInbox(options = {}) {
|
||||||
|
const force = Boolean(options.force)
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
if (refreshPromise) {
|
||||||
|
return refreshPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force && lastRefreshAt && now - lastRefreshAt < INBOX_CACHE_TTL_MS) {
|
||||||
|
refreshViewedDocumentKeys()
|
||||||
|
return documentRows.value
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
try {
|
refreshPromise = (async () => {
|
||||||
const [ownedResult, approvalResult, archiveResult] = await Promise.allSettled([
|
const [ownedResult, approvalResult, archiveResult] = await Promise.allSettled([
|
||||||
readClaimList(fetchExpenseClaims),
|
readClaimList(fetchExpenseClaims),
|
||||||
readClaimList(fetchApprovalExpenseClaims),
|
readClaimList(fetchApprovalExpenseClaims),
|
||||||
@@ -140,13 +155,21 @@ export function useDocumentCenterInbox() {
|
|||||||
approvalClaims: approvalResult.status === 'fulfilled' ? approvalResult.value : [],
|
approvalClaims: approvalResult.status === 'fulfilled' ? approvalResult.value : [],
|
||||||
archivedClaims: archiveResult.status === 'fulfilled' ? archiveResult.value : []
|
archivedClaims: archiveResult.status === 'fulfilled' ? archiveResult.value : []
|
||||||
})
|
})
|
||||||
|
lastRefreshAt = Date.now()
|
||||||
refreshViewedDocumentKeys()
|
refreshViewedDocumentKeys()
|
||||||
|
|
||||||
|
return documentRows.value
|
||||||
|
})()
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await refreshPromise
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
refreshPromise = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startDocumentInboxPolling(intervalMs = 45000) {
|
function startDocumentInboxPolling(intervalMs = 120000) {
|
||||||
stopDocumentInboxPolling()
|
stopDocumentInboxPolling()
|
||||||
|
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
|
|||||||
78
web/src/composables/useMinimumVisibleState.js
Normal file
78
web/src/composables/useMinimumVisibleState.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { computed, getCurrentScope, onScopeDispose, ref, unref, watch } from 'vue'
|
||||||
|
|
||||||
|
export const DEFAULT_MIN_VISIBLE_MS = 650
|
||||||
|
|
||||||
|
function resolveBooleanSource(source) {
|
||||||
|
return Boolean(typeof source === 'function' ? source() : unref(source))
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMinVisibleMs(value) {
|
||||||
|
const nextValue = Number(value)
|
||||||
|
return Number.isFinite(nextValue) && nextValue >= 0 ? nextValue : DEFAULT_MIN_VISIBLE_MS
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMinimumVisibleState(source, options = {}) {
|
||||||
|
const minVisibleMs = computed(() => resolveMinVisibleMs(unref(options.minVisibleMs)))
|
||||||
|
const visible = ref(resolveBooleanSource(source))
|
||||||
|
let visibleStartedAt = visible.value ? Date.now() : 0
|
||||||
|
let hideTimer = null
|
||||||
|
|
||||||
|
function clearHideTimer() {
|
||||||
|
if (!hideTimer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.clearTimeout(hideTimer)
|
||||||
|
hideTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
clearHideTimer()
|
||||||
|
|
||||||
|
if (!visible.value) {
|
||||||
|
visibleStartedAt = Date.now()
|
||||||
|
visible.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
clearHideTimer()
|
||||||
|
|
||||||
|
if (!visible.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingMs = Math.max(0, minVisibleMs.value - (Date.now() - visibleStartedAt))
|
||||||
|
if (remainingMs <= 0) {
|
||||||
|
visible.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hideTimer = globalThis.setTimeout(() => {
|
||||||
|
hideTimer = null
|
||||||
|
visible.value = false
|
||||||
|
}, remainingMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopWatch = watch(
|
||||||
|
() => resolveBooleanSource(source),
|
||||||
|
(active) => {
|
||||||
|
if (active) {
|
||||||
|
show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hide()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (getCurrentScope()) {
|
||||||
|
onScopeDispose(() => {
|
||||||
|
clearHideTimer()
|
||||||
|
stopWatch()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return visible
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ export const appViews = [
|
|||||||
'digitalEmployees',
|
'digitalEmployees',
|
||||||
'employees',
|
'employees',
|
||||||
'policies',
|
'policies',
|
||||||
'logs',
|
|
||||||
'settings'
|
'settings'
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -81,14 +80,6 @@ export const navItems = [
|
|||||||
title: '制度与知识库',
|
title: '制度与知识库',
|
||||||
desc: '统一管理制度文档、检索入口与知识资产。'
|
desc: '统一管理制度文档、检索入口与知识资产。'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'logs',
|
|
||||||
label: '系统日志',
|
|
||||||
navHint: '查看系统运行日志',
|
|
||||||
icon: icons.logs,
|
|
||||||
title: '系统日志',
|
|
||||||
desc: '集中查看系统运行日志、结构化事件和请求追踪信息。'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'settings',
|
id: 'settings',
|
||||||
label: '系统设置',
|
label: '系统设置',
|
||||||
@@ -107,18 +98,21 @@ const viewRouteNames = {
|
|||||||
policies: 'app-policies',
|
policies: 'app-policies',
|
||||||
audit: 'app-audit',
|
audit: 'app-audit',
|
||||||
digitalEmployees: 'app-digitalEmployees',
|
digitalEmployees: 'app-digitalEmployees',
|
||||||
logs: 'app-logs',
|
|
||||||
employees: 'app-employees',
|
employees: 'app-employees',
|
||||||
settings: 'app-settings'
|
settings: 'app-settings'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const legacyViewRouteNames = {
|
||||||
|
logs: 'app-settings'
|
||||||
|
}
|
||||||
|
|
||||||
const routeNameViews = Object.fromEntries(
|
const routeNameViews = Object.fromEntries(
|
||||||
Object.entries(viewRouteNames).map(([view, routeName]) => [routeName, view])
|
Object.entries(viewRouteNames).map(([view, routeName]) => [routeName, view])
|
||||||
)
|
)
|
||||||
|
|
||||||
routeNameViews['app-request-detail'] = 'documents'
|
routeNameViews['app-request-detail'] = 'documents'
|
||||||
routeNameViews['app-document-detail'] = 'documents'
|
routeNameViews['app-document-detail'] = 'documents'
|
||||||
routeNameViews['app-log-detail'] = 'logs'
|
routeNameViews['app-log-detail'] = 'settings'
|
||||||
|
|
||||||
export function resolveAppViewFromRoute(route) {
|
export function resolveAppViewFromRoute(route) {
|
||||||
const routeName = String(route?.name || '').trim()
|
const routeName = String(route?.name || '').trim()
|
||||||
@@ -131,7 +125,7 @@ export function resolveAppViewFromRoute(route) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveTargetRouteName(view) {
|
export function resolveTargetRouteName(view) {
|
||||||
return viewRouteNames[view] || viewRouteNames.overview
|
return viewRouteNames[view] || legacyViewRouteNames[view] || viewRouteNames.overview
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useNavigation() {
|
export function useNavigation() {
|
||||||
|
|||||||
@@ -1024,6 +1024,7 @@ function resolveRangeMatch(activeRange, item) {
|
|||||||
export function useRequests() {
|
export function useRequests() {
|
||||||
const requests = ref([])
|
const requests = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const loaded = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const filters = reactive({ entity: '全部主体', category: '全部类型', risk: '全部状态' })
|
const filters = reactive({ entity: '全部主体', category: '全部类型', risk: '全部状态' })
|
||||||
@@ -1060,6 +1061,7 @@ export function useRequests() {
|
|||||||
try {
|
try {
|
||||||
const payload = await fetchExpenseClaims()
|
const payload = await fetchExpenseClaims()
|
||||||
requests.value = Array.isArray(payload) ? payload.map((item) => mapExpenseClaimToRequest(item)) : []
|
requests.value = Array.isArray(payload) ? payload.map((item) => mapExpenseClaimToRequest(item)) : []
|
||||||
|
loaded.value = true
|
||||||
} catch (nextError) {
|
} catch (nextError) {
|
||||||
requests.value = []
|
requests.value = []
|
||||||
error.value = nextError instanceof Error ? nextError.message : '个人报销列表加载失败。'
|
error.value = nextError instanceof Error ? nextError.message : '个人报销列表加载失败。'
|
||||||
@@ -1076,11 +1078,14 @@ export function useRequests() {
|
|||||||
return `${request.id} 未执行本地状态变更,列表当前只展示后端真实数据。`
|
return `${request.id} 未执行本地状态变更,列表当前只展示后端真实数据。`
|
||||||
}
|
}
|
||||||
|
|
||||||
void reload()
|
function ensureLoaded() {
|
||||||
|
return loaded.value ? Promise.resolve() : reload()
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
requests,
|
requests,
|
||||||
loading,
|
loading,
|
||||||
|
loaded,
|
||||||
error,
|
error,
|
||||||
search,
|
search,
|
||||||
filters,
|
filters,
|
||||||
@@ -1089,6 +1094,7 @@ export function useRequests() {
|
|||||||
filteredRequests,
|
filteredRequests,
|
||||||
approveRequest,
|
approveRequest,
|
||||||
rejectRequest,
|
rejectRequest,
|
||||||
|
ensureLoaded,
|
||||||
reload
|
reload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import { useSystemState } from './useSystemState.js'
|
import { useSystemState } from './useSystemState.js'
|
||||||
import { useThemeSkin } from './useThemeSkin.js'
|
import { useThemeSkin } from './useThemeSkin.js'
|
||||||
@@ -26,7 +27,20 @@ import {
|
|||||||
readStoredSettings
|
readStoredSettings
|
||||||
} from '../utils/settingsModelHelper.js'
|
} from '../utils/settingsModelHelper.js'
|
||||||
|
|
||||||
|
const sectionIds = new Set(SECTION_DEFINITIONS.map((section) => section.id))
|
||||||
|
|
||||||
|
function resolveSectionId(value) {
|
||||||
|
const sectionId = String(value || '').trim()
|
||||||
|
return sectionIds.has(sectionId) ? sectionId : 'profile'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveInitialSectionId(route) {
|
||||||
|
return route.name === 'app-log-detail' ? 'systemLogs' : resolveSectionId(route.query.section)
|
||||||
|
}
|
||||||
|
|
||||||
export function useSettings() {
|
export function useSettings() {
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
const { companyProfile, currentUser, updateCompanyProfilePreview } = useSystemState()
|
const { companyProfile, currentUser, updateCompanyProfilePreview } = useSystemState()
|
||||||
const {
|
const {
|
||||||
@@ -38,7 +52,7 @@ export function useSettings() {
|
|||||||
|
|
||||||
const buildResolvedDefaults = () => buildDefaultState(companyProfile.value, currentUser.value)
|
const buildResolvedDefaults = () => buildDefaultState(companyProfile.value, currentUser.value)
|
||||||
const pageState = ref(mergeState(buildResolvedDefaults(), readStoredSettings()))
|
const pageState = ref(mergeState(buildResolvedDefaults(), readStoredSettings()))
|
||||||
const activeSection = ref('profile')
|
const activeSection = ref(resolveInitialSectionId(route))
|
||||||
const sessionRetentionPickerOpen = ref(false)
|
const sessionRetentionPickerOpen = ref(false)
|
||||||
const sessionRetentionPickerRef = ref(null)
|
const sessionRetentionPickerRef = ref(null)
|
||||||
const logoInputRef = ref(null)
|
const logoInputRef = ref(null)
|
||||||
@@ -55,6 +69,7 @@ export function useSettings() {
|
|||||||
|
|
||||||
const sectionStatus = computed(() => computeSectionStatus(pageState.value))
|
const sectionStatus = computed(() => computeSectionStatus(pageState.value))
|
||||||
const completedSectionCount = computed(() => Object.values(sectionStatus.value).filter(Boolean).length)
|
const completedSectionCount = computed(() => Object.values(sectionStatus.value).filter(Boolean).length)
|
||||||
|
const systemLogDetailMode = computed(() => route.name === 'app-log-detail')
|
||||||
const activeSectionConfig = computed(
|
const activeSectionConfig = computed(
|
||||||
() => sections.find((section) => section.id === activeSection.value) || sections[0]
|
() => sections.find((section) => section.id === activeSection.value) || sections[0]
|
||||||
)
|
)
|
||||||
@@ -150,9 +165,37 @@ export function useSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function activateSection(sectionId) {
|
function syncActiveSectionRoute(sectionId) {
|
||||||
|
if (route.name !== 'app-settings') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextQuery = { ...route.query }
|
||||||
|
if (sectionId === 'profile') {
|
||||||
|
delete nextQuery.section
|
||||||
|
} else {
|
||||||
|
nextQuery.section = sectionId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(route.query.section || '') === String(nextQuery.section || '')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
void router.replace({
|
||||||
|
name: 'app-settings',
|
||||||
|
query: nextQuery,
|
||||||
|
hash: route.hash
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateSection(sectionId, options = {}) {
|
||||||
|
const nextSectionId = resolveSectionId(sectionId)
|
||||||
sessionRetentionPickerOpen.value = false
|
sessionRetentionPickerOpen.value = false
|
||||||
activeSection.value = sectionId
|
activeSection.value = nextSectionId
|
||||||
|
|
||||||
|
if (!options.skipRouteSync) {
|
||||||
|
syncActiveSectionRoute(nextSectionId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleBoolean(formKey, field) {
|
function toggleBoolean(formKey, field) {
|
||||||
@@ -447,6 +490,10 @@ export function useSettings() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activeSection.value === 'systemLogs') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (activeSection.value === 'rendering') {
|
if (activeSection.value === 'rendering') {
|
||||||
await saveRenderingSection()
|
await saveRenderingSection()
|
||||||
return
|
return
|
||||||
@@ -462,6 +509,16 @@ export function useSettings() {
|
|||||||
loadSettingsSnapshot()
|
loadSettingsSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [route.name, route.query.section],
|
||||||
|
() => {
|
||||||
|
const nextSectionId = resolveInitialSectionId(route)
|
||||||
|
if (activeSection.value !== nextSectionId) {
|
||||||
|
activateSection(nextSectionId, { skipRouteSync: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== 'undefined') {
|
||||||
document.removeEventListener('pointerdown', handleDocumentPointerDown)
|
document.removeEventListener('pointerdown', handleDocumentPointerDown)
|
||||||
@@ -512,6 +569,7 @@ export function useSettings() {
|
|||||||
saveActiveSection,
|
saveActiveSection,
|
||||||
sectionStatus,
|
sectionStatus,
|
||||||
sections,
|
sections,
|
||||||
|
systemLogDetailMode,
|
||||||
selectThemeSkin,
|
selectThemeSkin,
|
||||||
selectSessionRetentionDays,
|
selectSessionRetentionDays,
|
||||||
themeSkinOptions,
|
themeSkinOptions,
|
||||||
|
|||||||
@@ -259,10 +259,10 @@ export function useSetupView(props, emit) {
|
|||||||
})
|
})
|
||||||
const testButtonIcon = computed(() => {
|
const testButtonIcon = computed(() => {
|
||||||
if ((activeSection.value === 'runtime' && props.runtimeTesting) || (activeSection.value === 'database' && props.databaseTesting)) {
|
if ((activeSection.value === 'runtime' && props.runtimeTesting) || (activeSection.value === 'database' && props.databaseTesting)) {
|
||||||
return 'pi pi-spin pi-spinner'
|
return 'mdi mdi-loading mdi-spin'
|
||||||
}
|
}
|
||||||
|
|
||||||
return activeSection.value === 'runtime' ? 'pi pi-server' : 'pi pi-database'
|
return activeSection.value === 'runtime' ? 'mdi mdi-server' : 'mdi mdi-database'
|
||||||
})
|
})
|
||||||
|
|
||||||
const canRuntimeTest = computed(() => Boolean(runtimeInputsReady.value && !props.submitting && !props.runtimeTesting && !props.databaseTesting))
|
const canRuntimeTest = computed(() => Boolean(runtimeInputsReady.value && !props.submitting && !props.runtimeTesting && !props.databaseTesting))
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { MotionPlugin } from '@vueuse/motion'
|
|||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
import 'primeicons/primeicons.css'
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router/index.js'
|
import router from './router/index.js'
|
||||||
import { installThemeSkin } from './composables/useThemeSkin.js'
|
import { installThemeSkin } from './composables/useThemeSkin.js'
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { checkBackendHealth } from '../composables/useBackendHealth.js'
|
|||||||
import { appViews } from '../composables/useNavigation.js'
|
import { appViews } from '../composables/useNavigation.js'
|
||||||
import { useSystemState } from '../composables/useSystemState.js'
|
import { useSystemState } from '../composables/useSystemState.js'
|
||||||
import { canAccessAppView } from '../utils/accessControl.js'
|
import { canAccessAppView } from '../utils/accessControl.js'
|
||||||
import AppShellRouteView from '../views/AppShellRouteView.vue'
|
|
||||||
import BackendUnavailableRouteView from '../views/BackendUnavailableRouteView.vue'
|
const AppShellRouteView = () => import('../views/AppShellRouteView.vue')
|
||||||
import LoginRouteView from '../views/LoginRouteView.vue'
|
const BackendUnavailableRouteView = () => import('../views/BackendUnavailableRouteView.vue')
|
||||||
import SetupRouteView from '../views/SetupRouteView.vue'
|
const LoginRouteView = () => import('../views/LoginRouteView.vue')
|
||||||
|
const SetupRouteView = () => import('../views/SetupRouteView.vue')
|
||||||
|
|
||||||
const appChildRoutes = appViews
|
const appChildRoutes = appViews
|
||||||
.filter((view) => view !== 'documents')
|
.filter((view) => view !== 'documents')
|
||||||
@@ -92,11 +93,24 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/app/logs/:logKind/:logId',
|
path: '/app/logs/:logKind/:logId',
|
||||||
|
redirect: (to) => ({
|
||||||
|
name: 'app-log-detail',
|
||||||
|
params: { logKind: to.params.logKind, logId: to.params.logId },
|
||||||
|
query: to.query,
|
||||||
|
hash: to.hash
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/app/logs',
|
||||||
|
redirect: { name: 'app-settings', query: { section: 'systemLogs' } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/app/settings/logs/:logKind/:logId',
|
||||||
name: 'app-log-detail',
|
name: 'app-log-detail',
|
||||||
component: AppShellRouteView,
|
component: AppShellRouteView,
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
appView: 'logs'
|
appView: 'settings'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
...appChildRoutes.map((route) => ({
|
...appChildRoutes.map((route) => ({
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export const DEFAULT_APP_VIEW_ORDER = [
|
|||||||
'overview',
|
'overview',
|
||||||
'policies',
|
'policies',
|
||||||
'digitalEmployees',
|
'digitalEmployees',
|
||||||
'logs',
|
|
||||||
'employees',
|
'employees',
|
||||||
'settings'
|
'settings'
|
||||||
]
|
]
|
||||||
@@ -17,7 +16,6 @@ const VIEW_ROLE_RULES = {
|
|||||||
budget: ['budget_monitor', 'executive'],
|
budget: ['budget_monitor', 'executive'],
|
||||||
audit: ['finance'],
|
audit: ['finance'],
|
||||||
digitalEmployees: ['finance'],
|
digitalEmployees: ['finance'],
|
||||||
logs: ['manager'],
|
|
||||||
employees: ['manager'],
|
employees: ['manager'],
|
||||||
settings: ['manager']
|
settings: ['manager']
|
||||||
}
|
}
|
||||||
|
|||||||
16
web/src/utils/refreshIntervalOptions.js
Normal file
16
web/src/utils/refreshIntervalOptions.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export const REFRESH_INTERVAL_OPTIONS = [
|
||||||
|
{ label: '1s', value: 1000 },
|
||||||
|
{ label: '3s', value: 3000 },
|
||||||
|
{ label: '5s', value: 5000 },
|
||||||
|
{ label: '10s', value: 10000 },
|
||||||
|
{ label: '30s', value: 30000 },
|
||||||
|
{ label: '60s', value: 60000 },
|
||||||
|
{ label: '180s', value: 180000 }
|
||||||
|
]
|
||||||
|
|
||||||
|
export const DEFAULT_REFRESH_INTERVAL_MS = 60000
|
||||||
|
|
||||||
|
export function formatRefreshInterval(value) {
|
||||||
|
const option = REFRESH_INTERVAL_OPTIONS.find((item) => item.value === Number(value))
|
||||||
|
return option?.label || '60s'
|
||||||
|
}
|
||||||
@@ -70,11 +70,19 @@ export const SECTION_DEFINITIONS = [
|
|||||||
{
|
{
|
||||||
id: 'logs',
|
id: 'logs',
|
||||||
label: '日志策略',
|
label: '日志策略',
|
||||||
title: '日志与审计策略',
|
title: '日志策略',
|
||||||
desc: '日志级别、留存与脱敏',
|
desc: '日志级别、留存与路径',
|
||||||
longDesc: '定义系统日志级别、留存周期和审计策略,保证问题排查和合规审计可追溯。',
|
longDesc: '定义系统日志级别、留存周期和写入路径,保证问题排查过程可追溯。',
|
||||||
actionLabel: '保存日志策略'
|
actionLabel: '保存日志策略'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'systemLogs',
|
||||||
|
label: '系统日志',
|
||||||
|
title: '系统日志',
|
||||||
|
desc: '运行事件、请求追踪与异常排查',
|
||||||
|
longDesc: '查看系统运行日志、结构化事件和请求追踪信息,作为系统设置下的排障与审计子项。',
|
||||||
|
actionLabel: ''
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'mail',
|
id: 'mail',
|
||||||
label: '邮箱设置',
|
label: '邮箱设置',
|
||||||
@@ -465,6 +473,7 @@ export function computeSectionStatus(state) {
|
|||||||
Number(state.logForm.retentionDays) > 0 &&
|
Number(state.logForm.retentionDays) > 0 &&
|
||||||
normalizeValue(state.logForm.logPath)
|
normalizeValue(state.logForm.logPath)
|
||||||
),
|
),
|
||||||
|
systemLogs: true,
|
||||||
mail: Boolean(
|
mail: Boolean(
|
||||||
normalizeValue(state.mailForm.smtpHost) &&
|
normalizeValue(state.mailForm.smtpHost) &&
|
||||||
Number(state.mailForm.port) > 0 &&
|
Number(state.mailForm.port) > 0 &&
|
||||||
|
|||||||
@@ -10,16 +10,13 @@
|
|||||||
<div class="mobile-overlay" aria-hidden="true" @click="mobileSidebarOpen = false"></div>
|
<div class="mobile-overlay" aria-hidden="true" @click="mobileSidebarOpen = false"></div>
|
||||||
<Transition name="login-entry-veil">
|
<Transition name="login-entry-veil">
|
||||||
<div v-if="loginEntryAnimating" class="login-entry-veil" aria-live="polite" aria-label="登录成功,正在进入工作台">
|
<div v-if="loginEntryAnimating" class="login-entry-veil" aria-live="polite" aria-label="登录成功,正在进入工作台">
|
||||||
<div class="login-entry-card">
|
<FloatingLightBandWindow
|
||||||
<span class="login-entry-mark" aria-hidden="true">
|
icon="mdi mdi-shield-check-outline"
|
||||||
<i class="mdi mdi-shield-check-outline"></i>
|
message="正在进入工作台"
|
||||||
</span>
|
motion="entry"
|
||||||
<div class="login-entry-copy">
|
title="登录成功"
|
||||||
<strong>登录成功</strong>
|
variant="entry"
|
||||||
<span>正在进入工作台</span>
|
/>
|
||||||
</div>
|
|
||||||
<span class="login-entry-progress" aria-hidden="true"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
<div class="app-sidebar">
|
<div class="app-sidebar">
|
||||||
@@ -49,28 +46,26 @@
|
|||||||
'audit-detail-main': activeView === 'audit' && auditDetailOpen,
|
'audit-detail-main': activeView === 'audit' && auditDetailOpen,
|
||||||
'digital-employees-detail-main': activeView === 'digitalEmployees' && digitalEmployeeDetailOpen,
|
'digital-employees-detail-main': activeView === 'digitalEmployees' && digitalEmployeeDetailOpen,
|
||||||
'digital-employees-main': activeView === 'digitalEmployees',
|
'digital-employees-main': activeView === 'digitalEmployees',
|
||||||
'logs-main': activeView === 'logs',
|
|
||||||
'employees-main': activeView === 'employees',
|
'employees-main': activeView === 'employees',
|
||||||
'settings-main': activeView === 'settings'
|
'settings-main': activeView === 'settings'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<TopBar
|
<TopBar
|
||||||
v-if="activeView !== 'settings' && !(activeView === 'audit' && auditDetailOpen) && !(activeView === 'digitalEmployees' && digitalEmployeeDetailOpen)"
|
v-if="activeView !== 'settings'"
|
||||||
:current-view="topBarView"
|
:current-view="resolvedTopBarView"
|
||||||
:search="search"
|
:search="search"
|
||||||
:active-view="activeView"
|
:active-view="activeView"
|
||||||
:ranges="ranges"
|
:ranges="ranges"
|
||||||
:active-range="activeRange"
|
:active-range="activeRange"
|
||||||
:employee-summary="employeeSummary"
|
:employee-summary="employeeSummary"
|
||||||
:knowledge-summary="knowledgeSummary"
|
:knowledge-summary="knowledgeSummary"
|
||||||
:logs-summary="logsSummary"
|
|
||||||
:request-summary="requestSummary"
|
:request-summary="requestSummary"
|
||||||
:document-summary="documentSummary"
|
:document-summary="documentSummary"
|
||||||
:digital-employee-summary="digitalEmployeeSummary"
|
:digital-employee-summary="digitalEmployeeSummary"
|
||||||
:company-name="ENTERPRISE_DISPLAY_NAME"
|
:company-name="ENTERPRISE_DISPLAY_NAME"
|
||||||
:detail-mode="detailMode"
|
:detail-mode="resolvedDetailMode"
|
||||||
:log-detail-mode="logDetailMode"
|
:detail-alerts="resolvedDetailAlerts"
|
||||||
:detail-alerts="detailAlerts"
|
:detail-kpis="resolvedDetailKpis"
|
||||||
:custom-range="customRange"
|
:custom-range="customRange"
|
||||||
@update:search="search = $event"
|
@update:search="search = $event"
|
||||||
@update:active-range="activeRange = $event"
|
@update:active-range="activeRange = $event"
|
||||||
@@ -80,7 +75,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<FilterBar
|
<FilterBar
|
||||||
v-if="activeView !== 'overview' && activeView !== 'workbench' && activeView !== 'documents' && activeView !== 'budget' && activeView !== 'policies' && activeView !== 'audit' && activeView !== 'digitalEmployees' && activeView !== 'logs' && activeView !== 'employees' && activeView !== 'settings'"
|
v-if="activeView !== 'overview' && activeView !== 'workbench' && activeView !== 'documents' && activeView !== 'budget' && activeView !== 'policies' && activeView !== 'audit' && activeView !== 'digitalEmployees' && activeView !== 'employees' && activeView !== 'settings'"
|
||||||
:compact="activeView === 'overview'"
|
:compact="activeView === 'overview'"
|
||||||
:filters="filters"
|
:filters="filters"
|
||||||
:ranges="ranges"
|
:ranges="ranges"
|
||||||
@@ -97,7 +92,6 @@
|
|||||||
'policies-workarea': activeView === 'policies',
|
'policies-workarea': activeView === 'policies',
|
||||||
'audit-workarea': activeView === 'audit',
|
'audit-workarea': activeView === 'audit',
|
||||||
'digital-employees-workarea': activeView === 'digitalEmployees',
|
'digital-employees-workarea': activeView === 'digitalEmployees',
|
||||||
'logs-workarea': activeView === 'logs',
|
|
||||||
'employees-workarea': activeView === 'employees',
|
'employees-workarea': activeView === 'employees',
|
||||||
'settings-workarea': activeView === 'settings'
|
'settings-workarea': activeView === 'settings'
|
||||||
}"
|
}"
|
||||||
@@ -145,14 +139,17 @@
|
|||||||
@open-assistant="openSmartEntry"
|
@open-assistant="openSmartEntry"
|
||||||
/>
|
/>
|
||||||
<PoliciesView v-else-if="activeView === 'policies'" @summary-change="knowledgeSummary = $event" />
|
<PoliciesView v-else-if="activeView === 'policies'" @summary-change="knowledgeSummary = $event" />
|
||||||
<AuditView v-else-if="activeView === 'audit'" @detail-open-change="auditDetailOpen = $event" />
|
<AuditView
|
||||||
|
v-else-if="activeView === 'audit'"
|
||||||
|
@detail-open-change="auditDetailOpen = $event"
|
||||||
|
@detail-topbar-change="detailTopBarPayload = $event"
|
||||||
|
/>
|
||||||
<DigitalEmployeesView
|
<DigitalEmployeesView
|
||||||
v-else-if="activeView === 'digitalEmployees'"
|
v-else-if="activeView === 'digitalEmployees'"
|
||||||
@summary-change="digitalEmployeeSummary = $event"
|
@summary-change="digitalEmployeeSummary = $event"
|
||||||
@detail-open-change="digitalEmployeeDetailOpen = $event"
|
@detail-open-change="digitalEmployeeDetailOpen = $event"
|
||||||
|
@detail-topbar-change="detailTopBarPayload = $event"
|
||||||
/>
|
/>
|
||||||
<LogDetailView v-else-if="activeView === 'logs' && logDetailMode" />
|
|
||||||
<LogsView v-else-if="activeView === 'logs'" @summary-change="logsSummary = $event" />
|
|
||||||
<EmployeeManagementView v-else-if="activeView === 'employees'" @overview-change="employeeSummary = $event" />
|
<EmployeeManagementView v-else-if="activeView === 'employees'" @overview-change="employeeSummary = $event" />
|
||||||
<SettingsView v-else />
|
<SettingsView v-else />
|
||||||
</section>
|
</section>
|
||||||
@@ -175,35 +172,51 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, defineAsyncComponent, h, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import SidebarRail from '../components/layout/SidebarRail.vue'
|
import SidebarRail from '../components/layout/SidebarRail.vue'
|
||||||
import TopBar from '../components/layout/TopBar.vue'
|
import TopBar from '../components/layout/TopBar.vue'
|
||||||
import FilterBar from '../components/layout/FilterBar.vue'
|
import FilterBar from '../components/layout/FilterBar.vue'
|
||||||
import OverviewView from './OverviewView.vue'
|
import FloatingLightBandWindow from '../components/shared/FloatingLightBandWindow.vue'
|
||||||
import PersonalWorkbenchView from './PersonalWorkbenchView.vue'
|
import TableLoadingState from '../components/shared/TableLoadingState.vue'
|
||||||
import TravelReimbursementCreateView from './TravelReimbursementCreateView.vue'
|
|
||||||
import TravelRequestDetailView from './TravelRequestDetailView.vue'
|
|
||||||
import DocumentsCenterView from './DocumentsCenterView.vue'
|
|
||||||
import BudgetCenterView from './BudgetCenterView.vue'
|
|
||||||
import PoliciesView from './PoliciesView.vue'
|
|
||||||
import AuditView from './AuditView.vue'
|
|
||||||
import DigitalEmployeesView from './DigitalEmployeesView.vue'
|
|
||||||
import LogsView from './LogsView.vue'
|
|
||||||
import LogDetailView from './LogDetailView.vue'
|
|
||||||
import EmployeeManagementView from './EmployeeManagementView.vue'
|
|
||||||
import SettingsView from './SettingsView.vue'
|
|
||||||
|
|
||||||
import { useAppShell } from '../composables/useAppShell.js'
|
import { useAppShell } from '../composables/useAppShell.js'
|
||||||
import { useSystemState } from '../composables/useSystemState.js'
|
import { useSystemState } from '../composables/useSystemState.js'
|
||||||
import { filterNavItemsByAccess } from '../utils/accessControl.js'
|
import { filterNavItemsByAccess } from '../utils/accessControl.js'
|
||||||
import { consumeLoginEntryTransition } from '../utils/loginEntryTransition.js'
|
import { consumeLoginEntryTransition } from '../utils/loginEntryTransition.js'
|
||||||
|
|
||||||
|
const OverviewView = defineAsyncComponent(() => import('./OverviewView.vue'))
|
||||||
|
const PersonalWorkbenchView = defineAsyncComponent(() => import('./PersonalWorkbenchView.vue'))
|
||||||
|
const TravelReimbursementCreateView = defineAsyncComponent(() => import('./TravelReimbursementCreateView.vue'))
|
||||||
|
const TravelRequestDetailView = defineAsyncComponent(() => import('./TravelRequestDetailView.vue'))
|
||||||
|
const DocumentsCenterView = defineAsyncComponent(() => import('./DocumentsCenterView.vue'))
|
||||||
|
const BudgetCenterRouteLoading = {
|
||||||
|
name: 'BudgetCenterRouteLoading',
|
||||||
|
render: () =>
|
||||||
|
h(TableLoadingState, {
|
||||||
|
title: '预算数据同步中',
|
||||||
|
message: '正在加载预算中心模块与预算数据',
|
||||||
|
icon: 'mdi mdi-chart-donut',
|
||||||
|
floating: true,
|
||||||
|
blocking: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const BudgetCenterView = defineAsyncComponent({
|
||||||
|
loader: () => import('./BudgetCenterView.vue'),
|
||||||
|
loadingComponent: BudgetCenterRouteLoading,
|
||||||
|
delay: 0
|
||||||
|
})
|
||||||
|
const PoliciesView = defineAsyncComponent(() => import('./PoliciesView.vue'))
|
||||||
|
const AuditView = defineAsyncComponent(() => import('./AuditView.vue'))
|
||||||
|
const DigitalEmployeesView = defineAsyncComponent(() => import('./DigitalEmployeesView.vue'))
|
||||||
|
const EmployeeManagementView = defineAsyncComponent(() => import('./EmployeeManagementView.vue'))
|
||||||
|
const SettingsView = defineAsyncComponent(() => import('./SettingsView.vue'))
|
||||||
|
|
||||||
const employeeSummary = ref(null)
|
const employeeSummary = ref(null)
|
||||||
const knowledgeSummary = ref(null)
|
const knowledgeSummary = ref(null)
|
||||||
const logsSummary = ref(null)
|
|
||||||
const documentSummary = ref(null)
|
const documentSummary = ref(null)
|
||||||
const digitalEmployeeSummary = ref(null)
|
const digitalEmployeeSummary = ref(null)
|
||||||
|
const detailTopBarPayload = ref(null)
|
||||||
const auditDetailOpen = ref(false)
|
const auditDetailOpen = ref(false)
|
||||||
const digitalEmployeeDetailOpen = ref(false)
|
const digitalEmployeeDetailOpen = ref(false)
|
||||||
const loginEntryAnimating = ref(false)
|
const loginEntryAnimating = ref(false)
|
||||||
@@ -246,7 +259,6 @@ const {
|
|||||||
customRange,
|
customRange,
|
||||||
detailAlerts,
|
detailAlerts,
|
||||||
detailMode,
|
detailMode,
|
||||||
logDetailMode,
|
|
||||||
filteredRequests,
|
filteredRequests,
|
||||||
filters,
|
filters,
|
||||||
handleApprove,
|
handleApprove,
|
||||||
@@ -282,6 +294,37 @@ const { companyProfile, currentUser, logout } = useSystemState()
|
|||||||
const PRODUCT_DISPLAY_NAME = '易财费控'
|
const PRODUCT_DISPLAY_NAME = '易财费控'
|
||||||
const ENTERPRISE_DISPLAY_NAME = '远光软件股份有限公司'
|
const ENTERPRISE_DISPLAY_NAME = '远光软件股份有限公司'
|
||||||
const filteredNavItems = computed(() => filterNavItemsByAccess(navItems, currentUser.value))
|
const filteredNavItems = computed(() => filterNavItemsByAccess(navItems, currentUser.value))
|
||||||
|
const DETAIL_TOPBAR_FALLBACKS = {
|
||||||
|
audit: {
|
||||||
|
title: '规则中心详情',
|
||||||
|
desc: '查看规则配置、版本审核、测试结果与上线状态。'
|
||||||
|
},
|
||||||
|
digitalEmployees: {
|
||||||
|
title: '数字员工详情',
|
||||||
|
desc: '查看数字员工配置、执行计划、运行记录与源文件。'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const customDetailTopBarActive = computed(() => (
|
||||||
|
(activeView.value === 'audit' && auditDetailOpen.value) ||
|
||||||
|
(activeView.value === 'digitalEmployees' && digitalEmployeeDetailOpen.value)
|
||||||
|
))
|
||||||
|
const resolvedTopBarView = computed(() => (
|
||||||
|
customDetailTopBarActive.value
|
||||||
|
? detailTopBarPayload.value?.view || DETAIL_TOPBAR_FALLBACKS[activeView.value] || topBarView.value
|
||||||
|
: topBarView.value
|
||||||
|
))
|
||||||
|
const resolvedDetailMode = computed(() => (
|
||||||
|
detailMode.value ||
|
||||||
|
customDetailTopBarActive.value
|
||||||
|
))
|
||||||
|
const resolvedDetailAlerts = computed(() => (
|
||||||
|
customDetailTopBarActive.value
|
||||||
|
? detailTopBarPayload.value?.alerts || []
|
||||||
|
: detailAlerts.value
|
||||||
|
))
|
||||||
|
const resolvedDetailKpis = computed(() => (
|
||||||
|
customDetailTopBarActive.value ? detailTopBarPayload.value?.kpis || [] : []
|
||||||
|
))
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
logout('manual')
|
logout('manual')
|
||||||
|
|||||||
@@ -11,67 +11,6 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="detail-scroll">
|
<div class="detail-scroll">
|
||||||
<section
|
|
||||||
v-if="!selectedSkill.usesSpreadsheetRule && !selectedSkill.usesJsonRiskRule"
|
|
||||||
class="detail-hero panel asset-detail-topbar list-toolbar"
|
|
||||||
>
|
|
||||||
<div class="hero-title asset-detail-topbar-main filter-set">
|
|
||||||
<div class="skill-badge" :class="selectedSkill.badgeTone">{{ selectedSkill.typeLabel }}</div>
|
|
||||||
<h2>{{ selectedSkill.name }}</h2>
|
|
||||||
<p>{{ selectedSkill.summary || '当前资产尚未补充说明。' }}</p>
|
|
||||||
|
|
||||||
<div class="hero-review-meta">
|
|
||||||
<span>
|
|
||||||
<i class="mdi mdi-code-tags"></i>
|
|
||||||
{{ selectedSkill.code }}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<i class="mdi mdi-account-outline"></i>
|
|
||||||
负责人:{{ selectedSkill.owner }}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<i class="mdi mdi-account-check-outline"></i>
|
|
||||||
审核人:{{ selectedSkill.reviewer }}
|
|
||||||
</span>
|
|
||||||
<b :class="['status-pill', selectedSkill.statusTone]">{{ selectedSkill.status }}</b>
|
|
||||||
<b
|
|
||||||
v-if="selectedSkillIsRule"
|
|
||||||
:class="['status-pill', selectedSkill.reviewStatusTone]"
|
|
||||||
>
|
|
||||||
{{ selectedSkill.reviewStatusLabel }}
|
|
||||||
</b>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="selectedSkillIsRule" class="review-note-block">
|
|
||||||
<strong>上线约束</strong>
|
|
||||||
<p>{{ activateBlockedReason || '当前规则版本审核通过后可正式上线。' }}</p>
|
|
||||||
<span v-if="showReviewNote">
|
|
||||||
审核时间:{{ selectedSkill.reviewTimeLabel }}
|
|
||||||
<template v-if="selectedSkill.reviewNote"> · 审核意见:{{ selectedSkill.reviewNote }}</template>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hero-stats asset-detail-topbar-meta toolbar-actions">
|
|
||||||
<div class="hero-stat">
|
|
||||||
<span>资产编码</span>
|
|
||||||
<strong>{{ selectedSkill.code }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="hero-stat">
|
|
||||||
<span>业务域</span>
|
|
||||||
<strong>{{ selectedSkill.category }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="hero-stat">
|
|
||||||
<span>{{ selectedSkillIsRule ? '当前展示版本' : '当前版本' }}</span>
|
|
||||||
<strong>{{ selectedSkill.displayVersion || selectedSkill.version }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="hero-stat">
|
|
||||||
<span>最近更新</span>
|
|
||||||
<strong>{{ selectedSkill.updatedAt }}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="detailError" class="detail-inline-state panel error">
|
<section v-if="detailError" class="detail-inline-state panel error">
|
||||||
<i class="mdi mdi-alert-circle-outline"></i>
|
<i class="mdi mdi-alert-circle-outline"></i>
|
||||||
<div>
|
<div>
|
||||||
@@ -94,7 +33,6 @@
|
|||||||
v-else-if="selectedSkill.usesSpreadsheetRule"
|
v-else-if="selectedSkill.usesSpreadsheetRule"
|
||||||
ref="spreadsheetUploadInput"
|
ref="spreadsheetUploadInput"
|
||||||
:selected-skill="selectedSkill"
|
:selected-skill="selectedSkill"
|
||||||
:selected-spreadsheet-mode-label="selectedSpreadsheetModeLabel"
|
|
||||||
:selected-spreadsheet-file-name="selectedSpreadsheetFileName"
|
:selected-spreadsheet-file-name="selectedSpreadsheetFileName"
|
||||||
:selected-spreadsheet-change-records="selectedSpreadsheetChangeRecords"
|
:selected-spreadsheet-change-records="selectedSpreadsheetChangeRecords"
|
||||||
:spreadsheet-only-office-host-id="spreadsheetOnlyOfficeHostId"
|
:spreadsheet-only-office-host-id="spreadsheetOnlyOfficeHostId"
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="budget-center-page">
|
<section class="budget-center-page">
|
||||||
|
<TableLoadingState
|
||||||
|
v-if="budgetLoading"
|
||||||
|
title="预算数据同步中"
|
||||||
|
message="正在加载预算额度、使用情况与预警明细"
|
||||||
|
icon="mdi mdi-chart-donut"
|
||||||
|
floating
|
||||||
|
blocking
|
||||||
|
/>
|
||||||
|
|
||||||
<section class="budget-summary-grid" aria-label="预算概览">
|
<section class="budget-summary-grid" aria-label="预算概览">
|
||||||
<article
|
<article
|
||||||
|
|||||||
@@ -1,47 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="digital-employees-view skill-center">
|
<section class="digital-employees-view skill-center">
|
||||||
<Transition name="skill-view" mode="out-in">
|
<Transition name="skill-view" mode="out-in">
|
||||||
<article
|
<EnterpriseDetailPage
|
||||||
v-if="selectedEmployee"
|
v-if="selectedEmployee"
|
||||||
key="detail"
|
key="detail"
|
||||||
class="skill-detail digital-employee-detail json-risk-skill-detail"
|
variant="skill-detail digital-employee-detail json-risk-skill-detail"
|
||||||
|
actions-class="digital-employee-detail-actions"
|
||||||
|
:error="detailError"
|
||||||
|
error-title="数字员工详情加载失败"
|
||||||
|
:loading="detailLoading && selectedEmployee.loading"
|
||||||
|
loading-title="正在加载数字员工详情"
|
||||||
|
loading-message="列表数据已就绪,正在补充 Skills 源文件和执行配置"
|
||||||
|
loading-icon="mdi mdi-account-cog-outline"
|
||||||
|
back-label="返回数字员工列表"
|
||||||
|
@back="closeEmployeeDetail"
|
||||||
>
|
>
|
||||||
<div class="detail-scroll">
|
|
||||||
<section v-if="detailError" class="detail-inline-state panel error">
|
|
||||||
<i class="mdi mdi-alert-circle-outline"></i>
|
|
||||||
<div>
|
|
||||||
<strong>数字员工详情加载失败</strong>
|
|
||||||
<p>{{ detailError }}</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<TableLoadingState
|
|
||||||
v-else-if="detailLoading && selectedEmployee.loading"
|
|
||||||
class="detail-loading-state panel"
|
|
||||||
variant="panel"
|
|
||||||
title="正在加载数字员工详情"
|
|
||||||
message="列表数据已就绪,正在补充 Skills 源文件和执行配置"
|
|
||||||
icon="mdi mdi-account-cog-outline"
|
|
||||||
:show-skeleton="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AuditDigitalEmployeeDetail
|
<AuditDigitalEmployeeDetail
|
||||||
v-else
|
|
||||||
:selected-skill="selectedEmployee"
|
:selected-skill="selectedEmployee"
|
||||||
:can-edit="canEditDigitalEmployeeSource"
|
:can-edit="canEditDigitalEmployeeSource"
|
||||||
:detail-busy="detailBusy"
|
:detail-busy="detailBusy"
|
||||||
:action-state="actionState"
|
:action-state="actionState"
|
||||||
@save-source="saveDigitalEmployeeSource"
|
@save-source="saveDigitalEmployeeSource"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="detail-actions digital-employee-detail-actions">
|
<template #actions>
|
||||||
<button class="back-action" type="button" @click="closeEmployeeDetail">
|
|
||||||
<i class="mdi mdi-arrow-left"></i>
|
|
||||||
<span>返回数字员工列表</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="detail-action-group">
|
|
||||||
<button
|
<button
|
||||||
class="minor-action enable-action"
|
class="minor-action enable-action"
|
||||||
:class="{ 'is-on': selectedEmployee.statusValue === 'active' }"
|
:class="{ 'is-on': selectedEmployee.statusValue === 'active' }"
|
||||||
@@ -70,9 +52,8 @@
|
|||||||
<i class="mdi mdi-play-circle-outline"></i>
|
<i class="mdi mdi-play-circle-outline"></i>
|
||||||
<span>{{ actionBusy(selectedEmployee.id, 'run-digital-now') ? '运行中...' : '立即运行' }}</span>
|
<span>{{ actionBusy(selectedEmployee.id, 'run-digital-now') ? '运行中...' : '立即运行' }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</template>
|
||||||
</footer>
|
</EnterpriseDetailPage>
|
||||||
</article>
|
|
||||||
|
|
||||||
<article
|
<article
|
||||||
v-else
|
v-else
|
||||||
@@ -86,7 +67,7 @@
|
|||||||
:class="{ active: activeSection === 'skills' }"
|
:class="{ active: activeSection === 'skills' }"
|
||||||
@click="activeSection = 'skills'"
|
@click="activeSection = 'skills'"
|
||||||
>
|
>
|
||||||
数字员工
|
员工能力
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -98,168 +79,29 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<template v-if="activeSection === 'skills'">
|
<template v-if="activeSection === 'skills'">
|
||||||
<div class="list-toolbar">
|
<DigitalEmployeeListPanel
|
||||||
<div class="filter-set">
|
v-model:keyword="keyword"
|
||||||
<label class="search-filter">
|
|
||||||
<i class="mdi mdi-magnify"></i>
|
|
||||||
<input v-model="keyword" type="search" placeholder="搜索技能名称、编号、执行计划或维护人" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<AuditPickerFilter
|
|
||||||
id="status"
|
|
||||||
title="选择资产状态"
|
|
||||||
close-label="关闭资产状态选择"
|
|
||||||
:active-filter-popover="activeFilterPopover"
|
:active-filter-popover="activeFilterPopover"
|
||||||
:label="selectedStatusLabel"
|
:selected-status="selectedStatus"
|
||||||
:options="statusOptions"
|
:selected-status-label="selectedStatusLabel"
|
||||||
:selected-value="selectedStatus"
|
:status-options="statusOptions"
|
||||||
@toggle="toggleFilterPopover"
|
:selected-enabled-state="selectedEnabledState"
|
||||||
@close="closeFilterPopover"
|
:selected-enabled-label="selectedEnabledLabel"
|
||||||
@select="selectFilter('status', $event)"
|
:enabled-state-options="enabledStateOptions"
|
||||||
|
:selected-execution-mode="selectedExecutionMode"
|
||||||
|
:selected-execution-mode-label="selectedExecutionModeLabel"
|
||||||
|
:execution-mode-options="executionModeOptions"
|
||||||
|
:active-filter-tokens="activeFilterTokens"
|
||||||
|
:loading="loading"
|
||||||
|
:error-message="errorMessage"
|
||||||
|
:visible-employees="visibleEmployees"
|
||||||
|
@toggle-filter-popover="toggleFilterPopover"
|
||||||
|
@close-filter-popover="closeFilterPopover"
|
||||||
|
@select-filter="selectFilter"
|
||||||
|
@reset-filters="resetFilters"
|
||||||
|
@load-employees="loadEmployees"
|
||||||
|
@open-employee-detail="openEmployeeDetail"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AuditPickerFilter
|
|
||||||
id="enabled"
|
|
||||||
title="选择启动状态"
|
|
||||||
close-label="关闭启动状态选择"
|
|
||||||
:active-filter-popover="activeFilterPopover"
|
|
||||||
:label="selectedEnabledLabel"
|
|
||||||
:options="enabledStateOptions"
|
|
||||||
:selected-value="selectedEnabledState"
|
|
||||||
@toggle="toggleFilterPopover"
|
|
||||||
@close="closeFilterPopover"
|
|
||||||
@select="selectFilter('enabled', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AuditPickerFilter
|
|
||||||
id="executionMode"
|
|
||||||
title="选择执行方式"
|
|
||||||
close-label="关闭执行方式选择"
|
|
||||||
:active-filter-popover="activeFilterPopover"
|
|
||||||
:label="selectedExecutionModeLabel"
|
|
||||||
:options="executionModeOptions"
|
|
||||||
:selected-value="selectedExecutionMode"
|
|
||||||
@toggle="toggleFilterPopover"
|
|
||||||
@close="closeFilterPopover"
|
|
||||||
@select="selectFilter('executionMode', $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="toolbar-actions">
|
|
||||||
<button
|
|
||||||
v-if="keyword || activeFilterTokens.length"
|
|
||||||
class="ghost-filter-btn"
|
|
||||||
type="button"
|
|
||||||
@click="resetFilters"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-filter-remove-outline"></i>
|
|
||||||
<span>清空筛选</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="create-btn digital-refresh-action"
|
|
||||||
type="button"
|
|
||||||
:disabled="loading"
|
|
||||||
@click="loadEmployees"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-refresh"></i>
|
|
||||||
<span>{{ loading ? '刷新中...' : '刷新' }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="hint">
|
|
||||||
<i class="mdi mdi-information-outline"></i>
|
|
||||||
集中查看后台自动执行的技能、执行计划和运行状态。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div v-if="activeFilterTokens.length" class="active-filter-strip">
|
|
||||||
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
|
|
||||||
{{ token }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="table-wrap digital-table-wrap"
|
|
||||||
:class="{ 'is-empty': !loading && !errorMessage && !visibleEmployees.length }"
|
|
||||||
>
|
|
||||||
<div v-if="loading" class="table-state">
|
|
||||||
<TableLoadingState
|
|
||||||
variant="panel"
|
|
||||||
title="数字员工同步中"
|
|
||||||
message="正在读取后台自动执行技能列表"
|
|
||||||
icon="mdi mdi-account-cog-outline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="errorMessage" class="table-state error">
|
|
||||||
<i class="mdi mdi-alert-circle-outline"></i>
|
|
||||||
<strong>数字员工加载失败</strong>
|
|
||||||
<p>{{ errorMessage }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TableEmptyState
|
|
||||||
v-else-if="!visibleEmployees.length"
|
|
||||||
eyebrow="数字员工"
|
|
||||||
title="暂无匹配的数字员工"
|
|
||||||
description="当前没有符合搜索条件的后台执行技能。"
|
|
||||||
icon="mdi mdi-account-cog-outline"
|
|
||||||
tone="theme"
|
|
||||||
art-label="STAFF"
|
|
||||||
:tips="['数字员工已从规则中心拆出为独立入口', '运行与定时操作统一进入详情后处理']"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<table v-else class="digital-employees-table">
|
|
||||||
<colgroup>
|
|
||||||
<col class="col-skill">
|
|
||||||
<col class="col-schedule">
|
|
||||||
<col class="col-mode">
|
|
||||||
<col class="col-skill-type">
|
|
||||||
<col class="col-status">
|
|
||||||
<col class="col-enabled">
|
|
||||||
<col class="col-updated">
|
|
||||||
</colgroup>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>技能名称</th>
|
|
||||||
<th>执行计划</th>
|
|
||||||
<th>触发方式</th>
|
|
||||||
<th>技能类型</th>
|
|
||||||
<th>资产状态</th>
|
|
||||||
<th>启动状态</th>
|
|
||||||
<th>最近更新</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
v-for="employee in visibleEmployees"
|
|
||||||
:key="employee.id"
|
|
||||||
@click="openEmployeeDetail(employee)"
|
|
||||||
>
|
|
||||||
<td>
|
|
||||||
<div class="skill-name-cell">
|
|
||||||
<span class="skill-avatar" :class="employee.badgeTone">{{ employee.short }}</span>
|
|
||||||
<div>
|
|
||||||
<strong>{{ employee.name }}</strong>
|
|
||||||
<span class="skill-list-subtitle">{{ employee.code }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td><span class="scope-pill">{{ employee.scope }}</span></td>
|
|
||||||
<td>{{ employee.executionMode }}</td>
|
|
||||||
<td><span class="scope-pill skill-type-pill">{{ employee.skillCategory }}</span></td>
|
|
||||||
<td>
|
|
||||||
<span :class="['status-pill', employee.statusTone]">{{ employee.status }}</span>
|
|
||||||
</td>
|
|
||||||
<td><span :class="['status-pill', employee.enabledTone]">{{ employee.enabledLabel }}</span></td>
|
|
||||||
<td>{{ employee.updatedAt || '-' }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer v-if="!loading && !errorMessage && visibleEmployees.length" class="list-foot">
|
|
||||||
<span class="page-summary">当前展示 {{ visibleEmployees.length }} 条数字员工</span>
|
|
||||||
</footer>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<DigitalEmployeeWorkRecords
|
<DigitalEmployeeWorkRecords
|
||||||
@@ -267,6 +109,7 @@
|
|||||||
class="digital-work-records-section"
|
class="digital-work-records-section"
|
||||||
@summary-change="emit('summary-change', $event)"
|
@summary-change="emit('summary-change', $event)"
|
||||||
@detail-open-change="workRecordDetailOpen = $event"
|
@detail-open-change="workRecordDetailOpen = $event"
|
||||||
|
@detail-topbar-change="workRecordDetailTopBar = $event"
|
||||||
/>
|
/>
|
||||||
</article>
|
</article>
|
||||||
</Transition>
|
</Transition>
|
||||||
@@ -289,11 +132,10 @@
|
|||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
import AuditDigitalEmployeeDetail from '../components/audit/AuditDigitalEmployeeDetail.vue'
|
import AuditDigitalEmployeeDetail from '../components/audit/AuditDigitalEmployeeDetail.vue'
|
||||||
import AuditPickerFilter from '../components/audit/AuditPickerFilter.vue'
|
import DigitalEmployeeListPanel from '../components/audit/DigitalEmployeeListPanel.vue'
|
||||||
import DigitalEmployeeScheduleDialog from '../components/audit/DigitalEmployeeScheduleDialog.vue'
|
import DigitalEmployeeScheduleDialog from '../components/audit/DigitalEmployeeScheduleDialog.vue'
|
||||||
import DigitalEmployeeWorkRecords from '../components/audit/DigitalEmployeeWorkRecords.vue'
|
import DigitalEmployeeWorkRecords from '../components/audit/DigitalEmployeeWorkRecords.vue'
|
||||||
import TableEmptyState from '../components/shared/TableEmptyState.vue'
|
import EnterpriseDetailPage from '../components/shared/EnterpriseDetailPage.vue'
|
||||||
import TableLoadingState from '../components/shared/TableLoadingState.vue'
|
|
||||||
import { useSystemState } from '../composables/useSystemState.js'
|
import { useSystemState } from '../composables/useSystemState.js'
|
||||||
import { useToast } from '../composables/useToast.js'
|
import { useToast } from '../composables/useToast.js'
|
||||||
import {
|
import {
|
||||||
@@ -307,8 +149,6 @@ import { runOrchestrator } from '../services/orchestrator.js'
|
|||||||
import { isPlatformAdminUser } from '../utils/accessControl.js'
|
import { isPlatformAdminUser } from '../utils/accessControl.js'
|
||||||
import {
|
import {
|
||||||
DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS,
|
DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS,
|
||||||
buildDigitalEmployeeDetailMeta,
|
|
||||||
buildDigitalEmployeeListMeta,
|
|
||||||
formatDigitalEmployeeCron,
|
formatDigitalEmployeeCron,
|
||||||
isDigitalEmployeeAsset
|
isDigitalEmployeeAsset
|
||||||
} from './scripts/auditViewDigitalEmployeeModel.js'
|
} from './scripts/auditViewDigitalEmployeeModel.js'
|
||||||
@@ -319,28 +159,55 @@ import {
|
|||||||
resolveDigitalEmployeeScheduleValue
|
resolveDigitalEmployeeScheduleValue
|
||||||
} from './scripts/digitalEmployeeScheduleModel.js'
|
} from './scripts/digitalEmployeeScheduleModel.js'
|
||||||
import { incrementVersion } from './scripts/auditViewRuntimeModel.js'
|
import { incrementVersion } from './scripts/auditViewRuntimeModel.js'
|
||||||
|
import {
|
||||||
|
DIGITAL_EMPLOYEE_EXECUTION_MODE_OPTIONS,
|
||||||
|
buildDigitalEmployeeDetailTopBar,
|
||||||
|
buildEmployeeDetail,
|
||||||
|
buildEmployeeListItem,
|
||||||
|
buildEmployeePlaceholder,
|
||||||
|
filterDigitalEmployees,
|
||||||
|
sortEmployees
|
||||||
|
} from './scripts/digitalEmployeesViewModel.js'
|
||||||
import {
|
import {
|
||||||
ENABLED_STATE_OPTIONS,
|
ENABLED_STATE_OPTIONS,
|
||||||
formatDateTime,
|
|
||||||
normalizeText,
|
normalizeText,
|
||||||
resolveStatusMeta,
|
|
||||||
STATUS_OPTIONS
|
STATUS_OPTIONS
|
||||||
} from './scripts/auditViewModel.js'
|
} from './scripts/auditViewModel.js'
|
||||||
|
|
||||||
const { currentUser } = useSystemState()
|
const { currentUser } = useSystemState()
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
const emit = defineEmits(['summary-change', 'detail-open-change'])
|
const emit = defineEmits(['summary-change', 'detail-open-change', 'detail-topbar-change'])
|
||||||
|
|
||||||
const employees = ref([])
|
const employees = ref([])
|
||||||
const selectedEmployee = ref(null)
|
const selectedEmployee = ref(null)
|
||||||
const selectedEmployeeId = ref('')
|
const selectedEmployeeId = ref('')
|
||||||
const activeSection = ref('skills')
|
const activeSection = ref('skills')
|
||||||
const workRecordDetailOpen = ref(false)
|
const workRecordDetailOpen = ref(false)
|
||||||
|
const workRecordDetailTopBar = ref(null)
|
||||||
const isDetailOpen = computed(() => Boolean(selectedEmployee.value) || (activeSection.value === 'workRecords' && workRecordDetailOpen.value))
|
const isDetailOpen = computed(() => Boolean(selectedEmployee.value) || (activeSection.value === 'workRecords' && workRecordDetailOpen.value))
|
||||||
|
const digitalEmployeeDetailTopBar = computed(() => {
|
||||||
|
const employee = selectedEmployee.value
|
||||||
|
if (employee) {
|
||||||
|
return buildDigitalEmployeeDetailTopBar(employee)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeSection.value === 'workRecords' && workRecordDetailOpen.value) {
|
||||||
|
return workRecordDetailTopBar.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
watch(isDetailOpen, (newVal) => {
|
watch(isDetailOpen, (newVal) => {
|
||||||
emit('detail-open-change', newVal)
|
emit('detail-open-change', newVal)
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
watch(
|
||||||
|
digitalEmployeeDetailTopBar,
|
||||||
|
(value) => {
|
||||||
|
emit('detail-topbar-change', value)
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
)
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
const selectedStatus = ref('')
|
const selectedStatus = ref('')
|
||||||
const selectedEnabledState = ref('')
|
const selectedEnabledState = ref('')
|
||||||
@@ -365,11 +232,7 @@ const scheduleEditorBusy = computed(() => actionState.value === 'save-digital-sc
|
|||||||
|
|
||||||
const statusOptions = STATUS_OPTIONS
|
const statusOptions = STATUS_OPTIONS
|
||||||
const enabledStateOptions = ENABLED_STATE_OPTIONS
|
const enabledStateOptions = ENABLED_STATE_OPTIONS
|
||||||
const executionModeOptions = [
|
const executionModeOptions = DIGITAL_EMPLOYEE_EXECUTION_MODE_OPTIONS
|
||||||
{ value: '', label: '全部执行方式' },
|
|
||||||
{ value: 'timed', label: '定时执行' },
|
|
||||||
{ value: 'manual', label: '手动触发' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const selectedStatusLabel = computed(() =>
|
const selectedStatusLabel = computed(() =>
|
||||||
statusOptions.find((item) => item.value === selectedStatus.value)?.label || '全部状态'
|
statusOptions.find((item) => item.value === selectedStatus.value)?.label || '全部状态'
|
||||||
@@ -403,32 +266,11 @@ const schedulePreviewLabel = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const visibleEmployees = computed(() => {
|
const visibleEmployees = computed(() => {
|
||||||
const searchText = normalizeText(keyword.value).toLowerCase()
|
return filterDigitalEmployees(employees.value, {
|
||||||
return employees.value.filter((item) => {
|
keyword: keyword.value,
|
||||||
const matchesKeyword = searchText
|
selectedEnabledState: selectedEnabledState.value,
|
||||||
? [
|
selectedExecutionMode: selectedExecutionMode.value,
|
||||||
item.name,
|
selectedStatus: selectedStatus.value
|
||||||
item.code,
|
|
||||||
item.summary,
|
|
||||||
item.owner,
|
|
||||||
item.scope,
|
|
||||||
item.executionMode,
|
|
||||||
item.skillCategory,
|
|
||||||
item.status,
|
|
||||||
item.enabledLabel
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.some((value) => String(value).toLowerCase().includes(searchText))
|
|
||||||
: true
|
|
||||||
const matchesStatus = selectedStatus.value ? item.statusValue === selectedStatus.value : true
|
|
||||||
const matchesEnabled = selectedEnabledState.value
|
|
||||||
? (selectedEnabledState.value === 'enabled') === Boolean(item.isEnabledValue)
|
|
||||||
: true
|
|
||||||
const matchesExecutionMode = selectedExecutionMode.value
|
|
||||||
? item.executionModeValue === selectedExecutionMode.value
|
|
||||||
: true
|
|
||||||
|
|
||||||
return matchesKeyword && matchesStatus && matchesEnabled && matchesExecutionMode
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -466,91 +308,6 @@ function resolveActor() {
|
|||||||
return normalizeText(user.name) || normalizeText(user.username) || 'system'
|
return normalizeText(user.name) || normalizeText(user.username) || 'system'
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEmployeeListItem(asset) {
|
|
||||||
const meta = buildDigitalEmployeeListMeta(asset)
|
|
||||||
const statusMeta = resolveStatusMeta(asset.status)
|
|
||||||
const displayName = meta.name || '数字员工技能'
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: asset.id,
|
|
||||||
rawCode: asset.code,
|
|
||||||
short: displayName.slice(0, 2),
|
|
||||||
badgeTone: 'blue',
|
|
||||||
name: displayName,
|
|
||||||
code: meta.code,
|
|
||||||
summary: meta.summary,
|
|
||||||
owner: meta.owner,
|
|
||||||
scope: meta.scope,
|
|
||||||
executionMode: meta.executionMode,
|
|
||||||
executionModeValue: meta.executionMode === '定时执行' ? 'timed' : 'manual',
|
|
||||||
skillCategory: meta.skillCategory,
|
|
||||||
version: asset.working_version || asset.current_version || '-',
|
|
||||||
currentVersion: asset.current_version || '-',
|
|
||||||
status: statusMeta.label,
|
|
||||||
statusValue: asset.status,
|
|
||||||
statusTone: statusMeta.tone,
|
|
||||||
enabledLabel: meta.enabledLabel,
|
|
||||||
enabledTone: meta.enabledTone,
|
|
||||||
isEnabledValue: meta.enabled,
|
|
||||||
configJson: asset.config_json || {},
|
|
||||||
updatedAt: formatDateTime(asset.updated_at),
|
|
||||||
updatedAtRaw: asset.updated_at || '',
|
|
||||||
digitalEmployee: meta
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildEmployeePlaceholder(employee) {
|
|
||||||
return {
|
|
||||||
...employee,
|
|
||||||
type: 'digitalEmployees',
|
|
||||||
typeLabel: '数字员工',
|
|
||||||
currentVersion: employee.currentVersion || employee.version || '-',
|
|
||||||
workingVersion: employee.version || '-',
|
|
||||||
markdownContent: '',
|
|
||||||
loading: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildEmployeeDetail(asset) {
|
|
||||||
const meta = buildDigitalEmployeeDetailMeta({
|
|
||||||
...asset,
|
|
||||||
updated_at: formatDateTime(asset.updated_at)
|
|
||||||
})
|
|
||||||
const statusMeta = resolveStatusMeta(asset.status)
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: asset.id,
|
|
||||||
type: 'digitalEmployees',
|
|
||||||
typeLabel: '数字员工',
|
|
||||||
rawCode: asset.code,
|
|
||||||
short: meta.name.slice(0, 2),
|
|
||||||
name: meta.name,
|
|
||||||
code: meta.code,
|
|
||||||
summary: meta.description,
|
|
||||||
owner: meta.owner,
|
|
||||||
reviewer: meta.reviewer,
|
|
||||||
category: meta.category,
|
|
||||||
scope: meta.scope,
|
|
||||||
version: asset.working_version || asset.current_version || '-',
|
|
||||||
currentVersion: asset.current_version || '-',
|
|
||||||
workingVersion: asset.working_version || asset.current_version || '-',
|
|
||||||
status: statusMeta.label,
|
|
||||||
statusValue: asset.status,
|
|
||||||
statusTone: statusMeta.tone,
|
|
||||||
configJson: asset.config_json || {},
|
|
||||||
updatedAt: formatDateTime(asset.updated_at),
|
|
||||||
markdownContent: meta.sourceMarkdown,
|
|
||||||
digitalEmployee: meta,
|
|
||||||
loading: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortEmployees(items) {
|
|
||||||
return [...items].sort((left, right) =>
|
|
||||||
String(right.updatedAtRaw || '').localeCompare(String(left.updatedAtRaw || ''))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadEmployees() {
|
async function loadEmployees() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
|||||||
@@ -140,6 +140,7 @@
|
|||||||
title="单据数据同步中"
|
title="单据数据同步中"
|
||||||
message="正在汇总当前报销、审批待办与归档单据"
|
message="正在汇总当前报销、审批待办与归档单据"
|
||||||
icon="mdi mdi-file-document-multiple-outline"
|
icon="mdi mdi-file-document-multiple-outline"
|
||||||
|
floating
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -246,6 +247,7 @@ import { computed, onMounted, ref, watch } from 'vue'
|
|||||||
import EnterpriseSelect from '../components/shared/EnterpriseSelect.vue'
|
import EnterpriseSelect from '../components/shared/EnterpriseSelect.vue'
|
||||||
import TableEmptyState from '../components/shared/TableEmptyState.vue'
|
import TableEmptyState from '../components/shared/TableEmptyState.vue'
|
||||||
import TableLoadingState from '../components/shared/TableLoadingState.vue'
|
import TableLoadingState from '../components/shared/TableLoadingState.vue'
|
||||||
|
import { useMinimumVisibleState } from '../composables/useMinimumVisibleState.js'
|
||||||
import { mapExpenseClaimToRequest } from '../composables/useRequests.js'
|
import { mapExpenseClaimToRequest } from '../composables/useRequests.js'
|
||||||
import { fetchApprovalExpenseClaims, fetchArchivedExpenseClaims } from '../services/reimbursements.js'
|
import { fetchApprovalExpenseClaims, fetchArchivedExpenseClaims } from '../services/reimbursements.js'
|
||||||
import { countNewDocuments, isNewDocument, markDocumentViewed, readDocumentScope, readViewedDocumentKeys, writeDocumentScope } from '../utils/documentCenterNewState.js'
|
import { countNewDocuments, isNewDocument, markDocumentViewed, readDocumentScope, readViewedDocumentKeys, writeDocumentScope } from '../utils/documentCenterNewState.js'
|
||||||
@@ -262,6 +264,7 @@ const DOCUMENT_SCOPE_REIMBURSEMENT = '报销单'
|
|||||||
const DOCUMENT_SCOPE_REVIEW = '审核单'
|
const DOCUMENT_SCOPE_REVIEW = '审核单'
|
||||||
const DOCUMENT_SCOPE_ARCHIVE = '归档'
|
const DOCUMENT_SCOPE_ARCHIVE = '归档'
|
||||||
const scopeTabs = [DOCUMENT_SCOPE_ALL, DOCUMENT_SCOPE_APPLICATION, DOCUMENT_SCOPE_REIMBURSEMENT, DOCUMENT_SCOPE_REVIEW, DOCUMENT_SCOPE_ARCHIVE]
|
const scopeTabs = [DOCUMENT_SCOPE_ALL, DOCUMENT_SCOPE_APPLICATION, DOCUMENT_SCOPE_REIMBURSEMENT, DOCUMENT_SCOPE_REVIEW, DOCUMENT_SCOPE_ARCHIVE]
|
||||||
|
const DOCUMENT_LOADING_MIN_VISIBLE_MS = 720
|
||||||
const statusTabs = ['全部', '草稿', '待提交', '审批中', '待补充', '待付款', '已完成']
|
const statusTabs = ['全部', '草稿', '待提交', '审批中', '待补充', '待付款', '已完成']
|
||||||
const FILTER_CONFIG_BY_SCOPE = {
|
const FILTER_CONFIG_BY_SCOPE = {
|
||||||
[DOCUMENT_SCOPE_ALL]: {
|
[DOCUMENT_SCOPE_ALL]: {
|
||||||
@@ -465,7 +468,11 @@ const visibleRows = computed(() => {
|
|||||||
return filteredRows.value.slice(start, start + pageSize.value)
|
return filteredRows.value.slice(start, start + pageSize.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const showLoading = computed(() => (props.loading || supportingLoading.value) && !visibleRows.value.length)
|
const documentLoadingSource = computed(() => (props.loading || supportingLoading.value) && !visibleRows.value.length)
|
||||||
|
const visibleDocumentLoading = useMinimumVisibleState(documentLoadingSource, {
|
||||||
|
minVisibleMs: DOCUMENT_LOADING_MIN_VISIBLE_MS
|
||||||
|
})
|
||||||
|
const showLoading = computed(() => visibleDocumentLoading.value)
|
||||||
const showError = computed(() => Boolean(props.error) && !visibleRows.value.length)
|
const showError = computed(() => Boolean(props.error) && !visibleRows.value.length)
|
||||||
const errorMessage = computed(() => props.error || supportingError.value || '单据中心加载失败。')
|
const errorMessage = computed(() => props.error || supportingError.value || '单据中心加载失败。')
|
||||||
const showEmpty = computed(() => !showLoading.value && !showError.value && visibleRows.value.length === 0)
|
const showEmpty = computed(() => !showLoading.value && !showError.value && visibleRows.value.length === 0)
|
||||||
|
|||||||
@@ -567,6 +567,7 @@
|
|||||||
title="员工数据同步中"
|
title="员工数据同步中"
|
||||||
message="正在加载员工档案与角色权限"
|
message="正在加载员工档案与角色权限"
|
||||||
icon="mdi mdi-account-group-outline"
|
icon="mdi mdi-account-group-outline"
|
||||||
|
floating
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</article>
|
</article>
|
||||||
|
|
||||||
<template v-else-if="isHermes && hermesRun">
|
<template v-else-if="isHermes && hermesRun">
|
||||||
<article v-if="!isKnowledgeIngestRunDetail" class="detail-hero panel">
|
<article class="detail-hero panel">
|
||||||
<div class="hero-copy">
|
<div class="hero-copy">
|
||||||
<div class="hero-tags">
|
<div class="hero-tags">
|
||||||
<span class="level-pill" :class="resolveLevelTone(resolveRunLevel(hermesRun))">
|
<span class="level-pill" :class="resolveLevelTone(resolveRunLevel(hermesRun))">
|
||||||
@@ -43,12 +43,7 @@
|
|||||||
{{ hermesRunAlert.message }}
|
{{ hermesRunAlert.message }}
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<KnowledgeIngestRunPanel
|
<div class="detail-grid">
|
||||||
v-if="isKnowledgeIngestRunDetail"
|
|
||||||
:run="hermesRun"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div v-if="!isKnowledgeIngestRunDetail" class="detail-grid">
|
|
||||||
<article class="panel detail-card wide">
|
<article class="panel detail-card wide">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<h3>基本信息</h3>
|
<h3>基本信息</h3>
|
||||||
@@ -68,7 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article v-if="!isKnowledgeIngestRunDetail" class="panel detail-card">
|
<article class="panel detail-card">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<h3>处理链路</h3>
|
<h3>处理链路</h3>
|
||||||
<p>按工具调用顺序查看执行链。</p>
|
<p>按工具调用顺序查看执行链。</p>
|
||||||
@@ -97,7 +92,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article v-if="selectedToolCall && !isKnowledgeIngestRunDetail" class="panel detail-card">
|
<article v-if="selectedToolCall" class="panel detail-card">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<h3>当前 ToolCall</h3>
|
<h3>当前 ToolCall</h3>
|
||||||
<p>查看当前工具调用的请求与返回。</p>
|
<p>查看当前工具调用的请求与返回。</p>
|
||||||
@@ -189,7 +184,7 @@
|
|||||||
<footer class="detail-actions">
|
<footer class="detail-actions">
|
||||||
<button class="back-action" type="button" @click="backToLogs">
|
<button class="back-action" type="button" @click="backToLogs">
|
||||||
<i class="mdi mdi-arrow-left"></i>
|
<i class="mdi mdi-arrow-left"></i>
|
||||||
<span>返回日志列表</span>
|
<span>返回系统日志</span>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
@@ -199,7 +194,6 @@
|
|||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import KnowledgeIngestRunPanel from '../components/logs/KnowledgeIngestRunPanel.vue'
|
|
||||||
import { fetchAgentRunDetail } from '../services/agentAssets.js'
|
import { fetchAgentRunDetail } from '../services/agentAssets.js'
|
||||||
import { fetchSystemLogEntry } from '../services/systemLogs.js'
|
import { fetchSystemLogEntry } from '../services/systemLogs.js'
|
||||||
import {
|
import {
|
||||||
@@ -210,7 +204,6 @@ import {
|
|||||||
resolveAgentRunHeartbeat,
|
resolveAgentRunHeartbeat,
|
||||||
resolveAgentRunStatus
|
resolveAgentRunStatus
|
||||||
} from '../utils/agentRunMonitor.js'
|
} from '../utils/agentRunMonitor.js'
|
||||||
import { isKnowledgeIngestRun } from '../utils/knowledgeIngestLogModel.js'
|
|
||||||
|
|
||||||
const SOURCE_LABELS = {
|
const SOURCE_LABELS = {
|
||||||
schedule: '定时任务',
|
schedule: '定时任务',
|
||||||
@@ -230,7 +223,6 @@ let pollTimer = 0
|
|||||||
|
|
||||||
const isHermes = computed(() => route.params.logKind === 'hermes')
|
const isHermes = computed(() => route.params.logKind === 'hermes')
|
||||||
const isSystem = computed(() => route.params.logKind === 'system')
|
const isSystem = computed(() => route.params.logKind === 'system')
|
||||||
const isKnowledgeIngestRunDetail = computed(() => isKnowledgeIngestRun(hermesRun.value))
|
|
||||||
const selectedToolCall = computed(() =>
|
const selectedToolCall = computed(() =>
|
||||||
(hermesRun.value?.tool_calls || []).find((item) => item.id === selectedToolCallId.value) || null
|
(hermesRun.value?.tool_calls || []).find((item) => item.id === selectedToolCallId.value) || null
|
||||||
)
|
)
|
||||||
@@ -444,7 +436,7 @@ async function loadDetail(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function backToLogs() {
|
function backToLogs() {
|
||||||
router.push({ name: 'app-logs' })
|
router.push({ name: 'app-settings', query: { section: 'systemLogs' } })
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -100,14 +100,44 @@
|
|||||||
<i class="mdi mdi-filter-remove-outline"></i>
|
<i class="mdi mdi-filter-remove-outline"></i>
|
||||||
<span>清空筛选</span>
|
<span>清空筛选</span>
|
||||||
</button>
|
</button>
|
||||||
|
<div class="document-filter refresh-interval-filter" :class="{ open: openFilterKey === 'refreshInterval' }">
|
||||||
|
<button
|
||||||
|
class="filter-btn refresh-interval-trigger"
|
||||||
|
type="button"
|
||||||
|
:aria-expanded="openFilterKey === 'refreshInterval'"
|
||||||
|
@click="toggleFilter('refreshInterval')"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-clock-outline"></i>
|
||||||
|
<span>刷新时间 {{ refreshIntervalLabel }}</span>
|
||||||
|
<i class="mdi mdi-chevron-down"></i>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="openFilterKey === 'refreshInterval'"
|
||||||
|
class="document-filter-menu refresh-interval-menu"
|
||||||
|
role="listbox"
|
||||||
|
aria-label="刷新时间"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="option in refreshIntervalOptions"
|
||||||
|
:key="option.value"
|
||||||
|
type="button"
|
||||||
|
role="option"
|
||||||
|
:aria-selected="refreshInterval === option.value"
|
||||||
|
:class="{ active: refreshInterval === option.value }"
|
||||||
|
@click="changeRefreshInterval(option.value)"
|
||||||
|
>
|
||||||
|
每 {{ option.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="create-request-btn"
|
class="create-request-btn icon-refresh-action"
|
||||||
:disabled="systemLogLoading"
|
:disabled="systemLogLoading"
|
||||||
|
aria-label="立即刷新系统日志"
|
||||||
@click="loadSystemLogs(true)"
|
@click="loadSystemLogs(true)"
|
||||||
>
|
>
|
||||||
<i class="mdi mdi-refresh"></i>
|
<i :class="systemLogLoading ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-refresh'"></i>
|
||||||
<span>{{ systemLogLoading ? '刷新中...' : '刷新日志' }}</span>
|
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="doc-table-wrap">
|
<div class="doc-table-wrap">
|
||||||
|
<TableLoadingState
|
||||||
|
v-if="loading && !visibleDocuments.length"
|
||||||
|
title="知识库文件同步中"
|
||||||
|
message="正在加载当前文件夹的知识库文件"
|
||||||
|
icon="mdi mdi-folder-table-outline"
|
||||||
|
floating
|
||||||
|
/>
|
||||||
|
|
||||||
<table class="knowledge-document-table">
|
<table class="knowledge-document-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -129,16 +137,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="loading && !visibleDocuments.length">
|
<tr v-if="!loading && !visibleDocuments.length">
|
||||||
<td colspan="8" class="empty-row table-loading-row">
|
|
||||||
<TableLoadingState
|
|
||||||
title="知识库文件同步中"
|
|
||||||
message="正在加载当前文件夹的知识库文件"
|
|
||||||
icon="mdi mdi-folder-table-outline"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-else-if="!visibleDocuments.length">
|
|
||||||
<td colspan="8" class="empty-row">
|
<td colspan="8" class="empty-row">
|
||||||
当前文件夹暂无文件
|
当前文件夹暂无文件
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -36,14 +36,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-toolbar-actions">
|
<div class="settings-toolbar-actions">
|
||||||
<button class="save-button" type="button" @click="saveActiveSection">
|
<button v-if="activeSectionConfig.actionLabel" class="save-button" type="button" @click="saveActiveSection">
|
||||||
<i class="mdi mdi-content-save-outline"></i>
|
<i class="mdi mdi-content-save-outline"></i>
|
||||||
<span>{{ activeSectionConfig.actionLabel }}</span>
|
<span>{{ activeSectionConfig.actionLabel }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="settings-content">
|
<div class="settings-content" :class="{ 'settings-content-fill': activeSection === 'systemLogs' }">
|
||||||
<template v-if="activeSection === 'profile'">
|
<template v-if="activeSection === 'profile'">
|
||||||
<section class="settings-card">
|
<section class="settings-card">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
@@ -380,7 +380,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="activeSection === 'logs'">
|
<template v-else-if="activeSection === 'logs'">
|
||||||
<section class="settings-card">
|
<section class="settings-card log-policy-card">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<div class="card-title-with-icon">
|
<div class="card-title-with-icon">
|
||||||
<div class="model-icon-box slate">
|
<div class="model-icon-box slate">
|
||||||
@@ -388,7 +388,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4>日志级别与留存</h4>
|
<h4>日志级别与留存</h4>
|
||||||
<p>定义系统记录粒度、归档周期和告警接收人,方便后续审计与排障。</p>
|
<p>定义系统记录粒度、归档周期、写入路径和告警接收人,方便后续排障追踪。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -432,46 +432,11 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
<section class="settings-card">
|
<template v-else-if="activeSection === 'systemLogs'">
|
||||||
<div class="card-head">
|
<LogDetailView v-if="systemLogDetailMode" class="settings-log-detail-view" />
|
||||||
<div class="card-title-with-icon">
|
<LogsView v-else class="settings-logs-view" />
|
||||||
<div class="model-icon-box slate">
|
|
||||||
<i class="mdi mdi-eye-check-outline"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4>审计策略</h4>
|
|
||||||
<p>决定是否记录关键操作、登录行为以及是否对敏感字段进行脱敏处理。</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="switch-group">
|
|
||||||
<button class="switch-row" type="button" @click="toggleBoolean('logForm', 'operationAudit')">
|
|
||||||
<span class="switch-copy">
|
|
||||||
<strong>记录关键操作日志</strong>
|
|
||||||
<small>保存配置修改、审批动作和账户管理等重要事件。</small>
|
|
||||||
</span>
|
|
||||||
<span class="switch-btn" :class="{ active: pageState.logForm.operationAudit }"><i></i></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="switch-row" type="button" @click="toggleBoolean('logForm', 'loginAudit')">
|
|
||||||
<span class="switch-copy">
|
|
||||||
<strong>记录登录审计</strong>
|
|
||||||
<small>追踪登录来源、登录结果和异常登录行为。</small>
|
|
||||||
</span>
|
|
||||||
<span class="switch-btn" :class="{ active: pageState.logForm.loginAudit }"><i></i></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="switch-row" type="button" @click="toggleBoolean('logForm', 'maskSensitive')">
|
|
||||||
<span class="switch-copy">
|
|
||||||
<strong>敏感字段脱敏</strong>
|
|
||||||
<small>日志写入时自动隐藏密码、密钥与认证令牌。</small>
|
|
||||||
</span>
|
|
||||||
<span class="switch-btn" :class="{ active: pageState.logForm.maskSensitive }"><i></i></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="activeSection === 'mail'">
|
<template v-else-if="activeSection === 'mail'">
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<strong>{{ section.title }}</strong>
|
<strong>{{ section.title }}</strong>
|
||||||
<small>{{ section.desc }}</small>
|
<small>{{ section.desc }}</small>
|
||||||
</span>
|
</span>
|
||||||
<i v-if="section.complete" class="pi pi-check setup-nav-check"></i>
|
<i v-if="section.complete" class="mdi mdi-check setup-nav-check"></i>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -42,11 +42,11 @@
|
|||||||
<div v-if="canSubmit" class="setup-complete">
|
<div v-if="canSubmit" class="setup-complete">
|
||||||
<p>所有必要步骤已通过检测,可以写入配置并进入登录界面。</p>
|
<p>所有必要步骤已通过检测,可以写入配置并进入登录界面。</p>
|
||||||
<button class="primary-btn setup-complete-btn" type="button" :disabled="submitting" @click="submitForm">
|
<button class="primary-btn setup-complete-btn" type="button" :disabled="submitting" @click="submitForm">
|
||||||
<i :class="['pi', submitting ? 'pi-spin pi-spinner' : 'pi-check']"></i>
|
<i :class="['mdi', submitting ? 'mdi-loading mdi-spin' : 'mdi-check']"></i>
|
||||||
<span>{{ submitting ? '写入配置中...' : '完成初始化并进入登录' }}</span>
|
<span>{{ submitting ? '写入配置中...' : '完成初始化并进入登录' }}</span>
|
||||||
</button>
|
</button>
|
||||||
<p v-if="progressMessage" class="setup-complete-progress">
|
<p v-if="progressMessage" class="setup-complete-progress">
|
||||||
<i class="pi pi-spin pi-spinner"></i>
|
<i class="mdi mdi-loading mdi-spin"></i>
|
||||||
<span>{{ progressMessage }}</span>
|
<span>{{ progressMessage }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -240,7 +240,7 @@
|
|||||||
<span>{{ progressMessage || '正在准备后端服务...' }}</span>
|
<span>{{ progressMessage || '正在准备后端服务...' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="setup-startup-spinner" aria-hidden="true">
|
<div class="setup-startup-spinner" aria-hidden="true">
|
||||||
<i v-if="!startupCountdownSeconds" class="pi pi-spin pi-spinner"></i>
|
<i v-if="!startupCountdownSeconds" class="mdi mdi-loading mdi-spin"></i>
|
||||||
<strong v-else>{{ startupCountdownSeconds }}</strong>
|
<strong v-else>{{ startupCountdownSeconds }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -358,18 +358,18 @@ const {
|
|||||||
|
|
||||||
function startupStepIcon(status) {
|
function startupStepIcon(status) {
|
||||||
if (status === 'success') {
|
if (status === 'success') {
|
||||||
return 'pi pi-check-circle'
|
return 'mdi mdi-check-circle'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'error') {
|
if (status === 'error') {
|
||||||
return 'pi pi-times-circle'
|
return 'mdi mdi-close-circle'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'running') {
|
if (status === 'running') {
|
||||||
return 'pi pi-spin pi-spinner'
|
return 'mdi mdi-loading mdi-spin'
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'pi pi-circle'
|
return 'mdi mdi-circle-outline'
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ import { ElButton, ElInput, ElPagination, ElTable, ElTableColumn } from 'element
|
|||||||
|
|
||||||
import BudgetTrendChart from '../../components/charts/BudgetTrendChart.vue'
|
import BudgetTrendChart from '../../components/charts/BudgetTrendChart.vue'
|
||||||
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
|
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
|
||||||
|
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
|
||||||
import { fetchBudgetSummary } from '../../services/budgets.js'
|
import { fetchBudgetSummary } from '../../services/budgets.js'
|
||||||
import { fetchEmployeeMeta } from '../../services/employees.js'
|
import { fetchEmployeeMeta } from '../../services/employees.js'
|
||||||
import {
|
import {
|
||||||
@@ -217,6 +218,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
BudgetTrendChart,
|
BudgetTrendChart,
|
||||||
EnterpriseSelect,
|
EnterpriseSelect,
|
||||||
|
TableLoadingState,
|
||||||
ElButton,
|
ElButton,
|
||||||
ElInput,
|
ElInput,
|
||||||
ElPagination,
|
ElPagination,
|
||||||
@@ -238,7 +240,7 @@ export default {
|
|||||||
const budgetTableKeyword = ref('')
|
const budgetTableKeyword = ref('')
|
||||||
const budgetRows = ref([])
|
const budgetRows = ref([])
|
||||||
const budgetSummary = ref(null)
|
const budgetSummary = ref(null)
|
||||||
const budgetLoading = ref(false)
|
const budgetLoading = ref(true)
|
||||||
const budgetError = ref('')
|
const budgetError = ref('')
|
||||||
const canEditBudget = computed(() => canEditBudgetCenter(props.currentUser))
|
const canEditBudget = computed(() => canEditBudgetCenter(props.currentUser))
|
||||||
const canSwitchDepartments = computed(() => canSwitchBudgetDepartments(props.currentUser))
|
const canSwitchDepartments = computed(() => canSwitchBudgetDepartments(props.currentUser))
|
||||||
@@ -424,6 +426,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadDepartments() {
|
async function loadDepartments() {
|
||||||
|
budgetLoading.value = true
|
||||||
try {
|
try {
|
||||||
const payload = await fetchEmployeeMeta()
|
const payload = await fetchEmployeeMeta()
|
||||||
const options = Array.isArray(payload?.organizationOptions) ? payload.organizationOptions : []
|
const options = Array.isArray(payload?.organizationOptions) ? payload.organizationOptions : []
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ import EnterpriseListPage from '../../components/shared/EnterpriseListPage.vue'
|
|||||||
import { useSystemState } from '../../composables/useSystemState.js'
|
import { useSystemState } from '../../composables/useSystemState.js'
|
||||||
import { useToast } from '../../composables/useToast.js'
|
import { useToast } from '../../composables/useToast.js'
|
||||||
import { fetchSystemLogEntries } from '../../services/systemLogs.js'
|
import { fetchSystemLogEntries } from '../../services/systemLogs.js'
|
||||||
import { AGENT_RUN_POLL_INTERVAL_MS } from '../../utils/agentRunMonitor.js'
|
|
||||||
import { isManagerUser } from '../../utils/accessControl.js'
|
import { isManagerUser } from '../../utils/accessControl.js'
|
||||||
|
import {
|
||||||
|
DEFAULT_REFRESH_INTERVAL_MS,
|
||||||
|
REFRESH_INTERVAL_OPTIONS,
|
||||||
|
formatRefreshInterval
|
||||||
|
} from '../../utils/refreshIntervalOptions.js'
|
||||||
|
|
||||||
function formatDateTime(value) {
|
function formatDateTime(value) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -79,6 +83,8 @@ export default {
|
|||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
const pageSizes = [10, 20, 50]
|
const pageSizes = [10, 20, 50]
|
||||||
const pageSizeOptions = pageSizes.map((size) => ({ label: `${size} 条/页`, value: size }))
|
const pageSizeOptions = pageSizes.map((size) => ({ label: `${size} 条/页`, value: size }))
|
||||||
|
const refreshInterval = ref(DEFAULT_REFRESH_INTERVAL_MS)
|
||||||
|
const refreshIntervalOptions = REFRESH_INTERVAL_OPTIONS
|
||||||
let pollTimer = 0
|
let pollTimer = 0
|
||||||
|
|
||||||
const isAdmin = computed(() => isManagerUser(currentUser.value))
|
const isAdmin = computed(() => isManagerUser(currentUser.value))
|
||||||
@@ -102,6 +108,7 @@ export default {
|
|||||||
const systemEventTypeFilterLabel = computed(() =>
|
const systemEventTypeFilterLabel = computed(() =>
|
||||||
systemEventTypeFilterOptions.value.find((item) => item.value === systemEventTypeFilter.value)?.label || '全部类型'
|
systemEventTypeFilterOptions.value.find((item) => item.value === systemEventTypeFilter.value)?.label || '全部类型'
|
||||||
)
|
)
|
||||||
|
const refreshIntervalLabel = computed(() => formatRefreshInterval(refreshInterval.value))
|
||||||
const hasActiveFilters = computed(() =>
|
const hasActiveFilters = computed(() =>
|
||||||
Boolean(systemSearchKeyword.value.trim() || systemLevelFilter.value || systemEventTypeFilter.value)
|
Boolean(systemSearchKeyword.value.trim() || systemLevelFilter.value || systemEventTypeFilter.value)
|
||||||
)
|
)
|
||||||
@@ -175,6 +182,12 @@ export default {
|
|||||||
openFilterKey.value = ''
|
openFilterKey.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changeRefreshInterval(value) {
|
||||||
|
refreshInterval.value = Number(value) || DEFAULT_REFRESH_INTERVAL_MS
|
||||||
|
openFilterKey.value = ''
|
||||||
|
startPolling()
|
||||||
|
}
|
||||||
|
|
||||||
function resetFilters() {
|
function resetFilters() {
|
||||||
systemSearchKeyword.value = ''
|
systemSearchKeyword.value = ''
|
||||||
systemLevelFilter.value = ''
|
systemLevelFilter.value = ''
|
||||||
@@ -211,7 +224,7 @@ export default {
|
|||||||
stopPolling()
|
stopPolling()
|
||||||
pollTimer = window.setInterval(() => {
|
pollTimer = window.setInterval(() => {
|
||||||
loadSystemLogs(false)
|
loadSystemLogs(false)
|
||||||
}, AGENT_RUN_POLL_INTERVAL_MS)
|
}, refreshInterval.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopPolling() {
|
function stopPolling() {
|
||||||
@@ -253,6 +266,7 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
changePageSize,
|
changePageSize,
|
||||||
|
changeRefreshInterval,
|
||||||
currentPage,
|
currentPage,
|
||||||
filteredSystemLogEntries,
|
filteredSystemLogEntries,
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
@@ -263,6 +277,9 @@ export default {
|
|||||||
openFilterKey,
|
openFilterKey,
|
||||||
pageSize,
|
pageSize,
|
||||||
pageSizeOptions,
|
pageSizeOptions,
|
||||||
|
refreshInterval,
|
||||||
|
refreshIntervalLabel,
|
||||||
|
refreshIntervalOptions,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
resolveSystemLevelTone,
|
resolveSystemLevelTone,
|
||||||
resolveSystemOutcomeTone,
|
resolveSystemOutcomeTone,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import HermesEmployeeSettingsPanel from '../HermesEmployeeSettingsPanel.vue'
|
import HermesEmployeeSettingsPanel from '../HermesEmployeeSettingsPanel.vue'
|
||||||
import LlmSettingsPanel from '../LlmSettingsPanel.vue'
|
import LlmSettingsPanel from '../LlmSettingsPanel.vue'
|
||||||
|
import LogDetailView from '../LogDetailView.vue'
|
||||||
|
import LogsView from '../LogsView.vue'
|
||||||
import MailSettingsPanel from '../MailSettingsPanel.vue'
|
import MailSettingsPanel from '../MailSettingsPanel.vue'
|
||||||
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
|
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
|
||||||
import { useSettings } from '../../composables/useSettings.js'
|
import { useSettings } from '../../composables/useSettings.js'
|
||||||
@@ -10,6 +12,8 @@ export default {
|
|||||||
HermesEmployeeSettingsPanel,
|
HermesEmployeeSettingsPanel,
|
||||||
EnterpriseSelect,
|
EnterpriseSelect,
|
||||||
LlmSettingsPanel,
|
LlmSettingsPanel,
|
||||||
|
LogDetailView,
|
||||||
|
LogsView,
|
||||||
MailSettingsPanel
|
MailSettingsPanel
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
|
|||||||
17
web/src/views/scripts/auditViewDataUtils.js
Normal file
17
web/src/views/scripts/auditViewDataUtils.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export function normalizeText(value) {
|
||||||
|
return String(value || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPlainObject(value) {
|
||||||
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readConfigJson(value) {
|
||||||
|
if (isPlainObject(value?.configJson)) {
|
||||||
|
return value.configJson
|
||||||
|
}
|
||||||
|
if (isPlainObject(value?.config_json)) {
|
||||||
|
return value.config_json
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
44
web/src/views/scripts/auditViewDetailTopBar.js
Normal file
44
web/src/views/scripts/auditViewDetailTopBar.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { normalizeText } from './auditViewModel.js'
|
||||||
|
|
||||||
|
function resolveRiskScoreCardColor(level) {
|
||||||
|
const normalized = normalizeText(level).toLowerCase()
|
||||||
|
if (['critical', 'high'].includes(normalized)) return '#ef4444'
|
||||||
|
if (['medium', 'warning'].includes(normalized)) return '#f59e0b'
|
||||||
|
if (['low', 'success'].includes(normalized)) return 'var(--success)'
|
||||||
|
return 'var(--theme-primary)'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAuditDetailTopBar({
|
||||||
|
skill,
|
||||||
|
usesJsonRiskRule = false
|
||||||
|
} = {}) {
|
||||||
|
if (!skill) return null
|
||||||
|
|
||||||
|
const title = normalizeText(skill.name) || '规则中心详情'
|
||||||
|
const desc =
|
||||||
|
normalizeText(skill.riskRuleSubtitle) ||
|
||||||
|
normalizeText(skill.summary) ||
|
||||||
|
normalizeText(skill.configDesc) ||
|
||||||
|
'查看规则配置、版本审核、测试结果与上线状态。'
|
||||||
|
const kpis = []
|
||||||
|
|
||||||
|
if (usesJsonRiskRule) {
|
||||||
|
const scoreLevel = skill.riskRuleScoreLevel || skill.riskRuleSeverity
|
||||||
|
const score = skill.riskRuleScore ?? '--'
|
||||||
|
kpis.push({
|
||||||
|
label: '风险分',
|
||||||
|
value: String(score),
|
||||||
|
unit: score === '--' ? '' : '分',
|
||||||
|
meta: normalizeText(skill.riskRuleScoreLabel || skill.riskRuleSeverityLabel) || '待评估',
|
||||||
|
trend: ['critical', 'high', 'medium', 'warning'].includes(normalizeText(scoreLevel).toLowerCase())
|
||||||
|
? 'down'
|
||||||
|
: 'up',
|
||||||
|
color: resolveRiskScoreCardColor(scoreLevel)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
view: { title, desc },
|
||||||
|
kpis
|
||||||
|
}
|
||||||
|
}
|
||||||
128
web/src/views/scripts/auditViewFormatters.js
Normal file
128
web/src/views/scripts/auditViewFormatters.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import {
|
||||||
|
DOMAIN_LABELS,
|
||||||
|
REVIEW_META,
|
||||||
|
SCENARIO_LABELS,
|
||||||
|
STATUS_META
|
||||||
|
} from './auditViewMetadata.js'
|
||||||
|
import { normalizeText } from './auditViewDataUtils.js'
|
||||||
|
|
||||||
|
export function makeShort(value) {
|
||||||
|
const text = normalizeText(value).replace(/\s+/g, '')
|
||||||
|
if (!text) {
|
||||||
|
return 'AG'
|
||||||
|
}
|
||||||
|
return text.slice(0, 2).toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDateTime(value) {
|
||||||
|
if (!value) {
|
||||||
|
return '未记录'
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
})
|
||||||
|
.format(date)
|
||||||
|
.replace(/\//g, '-')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDomainLabel(value) {
|
||||||
|
return DOMAIN_LABELS[value] || normalizeText(value) || '未分类'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveStatusMeta(value) {
|
||||||
|
return STATUS_META[value] || { label: normalizeText(value) || '未知状态', tone: 'draft' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveReviewMeta(value) {
|
||||||
|
return REVIEW_META[value] || { label: '暂无审核', tone: 'draft' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTimelineEventMeta(value) {
|
||||||
|
return {
|
||||||
|
created: { label: '创建工作稿', icon: 'mdi mdi-file-document-edit-outline', tone: 'draft' },
|
||||||
|
submitted: { label: '提交审核', icon: 'mdi mdi-send-outline', tone: 'warning' },
|
||||||
|
approved: { label: '审核通过', icon: 'mdi mdi-check-decagram-outline', tone: 'success' },
|
||||||
|
rejected: { label: '审核驳回', icon: 'mdi mdi-close-octagon-outline', tone: 'danger' },
|
||||||
|
published: { label: '正式上线', icon: 'mdi mdi-rocket-launch-outline', tone: 'success' },
|
||||||
|
restored: { label: '恢复生成工作稿', icon: 'mdi mdi-history', tone: 'info' }
|
||||||
|
}[normalizeText(value)] || { label: normalizeText(value) || '版本事件', icon: 'mdi mdi-circle-medium', tone: 'draft' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDiffChangeMeta(value) {
|
||||||
|
return {
|
||||||
|
added: { label: '新增', tone: 'success' },
|
||||||
|
removed: { label: '删除', tone: 'danger' },
|
||||||
|
modified: { label: '修改', tone: 'warning' }
|
||||||
|
}[normalizeText(value)] || { label: normalizeText(value) || '变化', tone: 'draft' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatScenarioList(items) {
|
||||||
|
if (!Array.isArray(items) || !items.length) {
|
||||||
|
return '未配置场景'
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
.map((item) => SCENARIO_LABELS[item] || item)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' / ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTypeKey(assetType) {
|
||||||
|
if (assetType === 'rule') {
|
||||||
|
return 'rules'
|
||||||
|
}
|
||||||
|
if (assetType === 'mcp') {
|
||||||
|
return 'mcp'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSeverity(value) {
|
||||||
|
const severity = normalizeText(value).toLowerCase()
|
||||||
|
if (severity === 'high') {
|
||||||
|
return '高风险'
|
||||||
|
}
|
||||||
|
if (severity === 'medium') {
|
||||||
|
return '中风险'
|
||||||
|
}
|
||||||
|
if (severity === 'low') {
|
||||||
|
return '低风险'
|
||||||
|
}
|
||||||
|
return '未配置'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatInputSummary(items) {
|
||||||
|
if (!Array.isArray(items) || !items.length) {
|
||||||
|
return '无输入'
|
||||||
|
}
|
||||||
|
return `${items.length} 项输入`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatOutputSummary(items) {
|
||||||
|
if (!Array.isArray(items) || !items.length) {
|
||||||
|
return '无输出'
|
||||||
|
}
|
||||||
|
return `${items.length} 项输出`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSpreadsheetChangeSummary(summary) {
|
||||||
|
const normalized = normalizeText(summary)
|
||||||
|
return (
|
||||||
|
normalized
|
||||||
|
.replace(/^(ONLYOFFICE\s*)?在线编辑[::]\s*/i, '')
|
||||||
|
.replace(/^ONLYOFFICE\s*在线编辑保存[。.]?\s*/i, '')
|
||||||
|
.replace(/^保存表格[::]\s*/i, '')
|
||||||
|
.trim() || '表格内容已保存。'
|
||||||
|
)
|
||||||
|
}
|
||||||
235
web/src/views/scripts/auditViewListFilters.js
Normal file
235
web/src/views/scripts/auditViewListFilters.js
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ENABLED_STATE_OPTIONS,
|
||||||
|
ONLINE_STATE_OPTIONS,
|
||||||
|
RISK_SCENARIO_OPTIONS,
|
||||||
|
STATUS_OPTIONS
|
||||||
|
} from './auditViewMetadata.js'
|
||||||
|
import { RISK_RULE_LEVEL_OPTIONS } from './auditViewRiskRuleModel.js'
|
||||||
|
import {
|
||||||
|
normalizeText,
|
||||||
|
resolveDomainLabel,
|
||||||
|
resolveStatusMeta
|
||||||
|
} from './auditViewModel.js'
|
||||||
|
import { filterAuditAssets } from './auditViewRuntimeModel.js'
|
||||||
|
|
||||||
|
function buildOptions(items, valueGetter, labelGetter, defaultLabel) {
|
||||||
|
const values = []
|
||||||
|
const seen = new Set()
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
const value = normalizeText(valueGetter(item))
|
||||||
|
if (!value || seen.has(value)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen.add(value)
|
||||||
|
values.push(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ value: '', label: defaultLabel },
|
||||||
|
...values.map((value) => ({
|
||||||
|
value,
|
||||||
|
label: labelGetter(value)
|
||||||
|
}))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuditListFilters({
|
||||||
|
activeType,
|
||||||
|
activeTabLabel,
|
||||||
|
currentAssets,
|
||||||
|
keyword,
|
||||||
|
selectedDomain,
|
||||||
|
selectedOwner,
|
||||||
|
selectedRiskLevel,
|
||||||
|
selectedStatus,
|
||||||
|
selectedRiskScenario,
|
||||||
|
selectedOnlineState,
|
||||||
|
selectedEnabledState
|
||||||
|
}) {
|
||||||
|
const showRiskScenarioFilter = computed(() =>
|
||||||
|
['financialRules', 'riskRules'].includes(activeType.value)
|
||||||
|
)
|
||||||
|
const showOwnerFilter = computed(() => activeType.value !== 'riskRules')
|
||||||
|
const showRiskLevelFilter = computed(() => activeType.value === 'riskRules')
|
||||||
|
const showStatusFilter = computed(() => true)
|
||||||
|
const showOnlineFilter = computed(() => false)
|
||||||
|
const showEnabledFilter = computed(() => false)
|
||||||
|
|
||||||
|
const domainOptions = computed(() =>
|
||||||
|
buildOptions(
|
||||||
|
currentAssets.value,
|
||||||
|
(item) => item.domainValue,
|
||||||
|
(value) => resolveDomainLabel(value),
|
||||||
|
'全部业务域'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const ownerOptions = computed(() =>
|
||||||
|
buildOptions(
|
||||||
|
currentAssets.value,
|
||||||
|
(item) => item.owner,
|
||||||
|
(value) => value,
|
||||||
|
'全部负责人'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const riskLevelOptions = computed(() => [
|
||||||
|
{ value: '', label: '全部风险等级' },
|
||||||
|
...RISK_RULE_LEVEL_OPTIONS
|
||||||
|
])
|
||||||
|
|
||||||
|
const selectedDomainLabel = computed(
|
||||||
|
() => domainOptions.value.find((item) => item.value === selectedDomain.value)?.label || '业务域'
|
||||||
|
)
|
||||||
|
const selectedOwnerLabel = computed(
|
||||||
|
() =>
|
||||||
|
ownerOptions.value.find((item) => item.value === selectedOwner.value)?.label ||
|
||||||
|
'负责人'
|
||||||
|
)
|
||||||
|
const selectedRiskLevelLabel = computed(
|
||||||
|
() =>
|
||||||
|
riskLevelOptions.value.find((item) => item.value === selectedRiskLevel.value)?.label ||
|
||||||
|
'风险等级'
|
||||||
|
)
|
||||||
|
const selectedStatusLabel = computed(
|
||||||
|
() => STATUS_OPTIONS.find((item) => item.value === selectedStatus.value)?.label || '状态'
|
||||||
|
)
|
||||||
|
const selectedRiskScenarioLabel = computed(
|
||||||
|
() =>
|
||||||
|
RISK_SCENARIO_OPTIONS.find((item) => item.value === selectedRiskScenario.value)?.label ||
|
||||||
|
'使用场景'
|
||||||
|
)
|
||||||
|
const selectedOnlineStateLabel = computed(
|
||||||
|
() =>
|
||||||
|
ONLINE_STATE_OPTIONS.find((item) => item.value === selectedOnlineState.value)?.label ||
|
||||||
|
'是否上线'
|
||||||
|
)
|
||||||
|
const selectedEnabledStateLabel = computed(
|
||||||
|
() =>
|
||||||
|
ENABLED_STATE_OPTIONS.find((item) => item.value === selectedEnabledState.value)?.label ||
|
||||||
|
'是否启用'
|
||||||
|
)
|
||||||
|
|
||||||
|
const activeFilterTokens = computed(() => {
|
||||||
|
const tokens = []
|
||||||
|
|
||||||
|
if (selectedDomain.value) {
|
||||||
|
tokens.push(`业务域:${resolveDomainLabel(selectedDomain.value)}`)
|
||||||
|
}
|
||||||
|
if (showRiskScenarioFilter.value && selectedRiskScenario.value) {
|
||||||
|
tokens.push(`使用场景:${selectedRiskScenario.value}`)
|
||||||
|
}
|
||||||
|
if (showStatusFilter.value && selectedStatus.value) {
|
||||||
|
tokens.push(`状态:${resolveStatusMeta(selectedStatus.value).label}`)
|
||||||
|
}
|
||||||
|
if (showOnlineFilter.value && selectedOnlineState.value) {
|
||||||
|
tokens.push(`是否上线:${selectedOnlineStateLabel.value}`)
|
||||||
|
}
|
||||||
|
if (showEnabledFilter.value && selectedEnabledState.value) {
|
||||||
|
tokens.push(`是否启用:${selectedEnabledStateLabel.value}`)
|
||||||
|
}
|
||||||
|
if (showOwnerFilter.value && selectedOwner.value) {
|
||||||
|
tokens.push(`负责人:${selectedOwner.value}`)
|
||||||
|
}
|
||||||
|
if (showRiskLevelFilter.value && selectedRiskLevel.value) {
|
||||||
|
tokens.push(`风险等级:${selectedRiskLevelLabel.value}`)
|
||||||
|
}
|
||||||
|
if (keyword.value.trim()) {
|
||||||
|
tokens.push(`搜索:${keyword.value.trim()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
})
|
||||||
|
|
||||||
|
const visibleSkills = computed(() =>
|
||||||
|
filterAuditAssets(currentAssets.value, {
|
||||||
|
keyword: keyword.value,
|
||||||
|
selectedDomain: selectedDomain.value,
|
||||||
|
selectedOwner: selectedOwner.value,
|
||||||
|
selectedRiskLevel: selectedRiskLevel.value,
|
||||||
|
selectedStatus: selectedStatus.value,
|
||||||
|
selectedRiskScenario: selectedRiskScenario.value,
|
||||||
|
selectedOnlineState: selectedOnlineState.value,
|
||||||
|
selectedEnabledState: selectedEnabledState.value,
|
||||||
|
showStatusFilter: showStatusFilter.value,
|
||||||
|
showRiskScenarioFilter: showRiskScenarioFilter.value,
|
||||||
|
showOnlineFilter: showOnlineFilter.value,
|
||||||
|
showEnabledFilter: showEnabledFilter.value
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const auditEmptyState = computed(() => {
|
||||||
|
const hasFilters = activeFilterTokens.value.length > 0
|
||||||
|
const supportedFilters = [
|
||||||
|
'业务域',
|
||||||
|
...(showOwnerFilter.value ? ['负责人'] : []),
|
||||||
|
...(showRiskLevelFilter.value ? ['风险等级'] : []),
|
||||||
|
...(showRiskScenarioFilter.value ? ['使用场景'] : []),
|
||||||
|
...(showStatusFilter.value ? ['状态'] : []),
|
||||||
|
...(showOnlineFilter.value ? ['是否上线'] : []),
|
||||||
|
...(showEnabledFilter.value ? ['是否启用'] : []),
|
||||||
|
'关键词'
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!currentAssets.value.length) {
|
||||||
|
return {
|
||||||
|
eyebrow: `${activeTabLabel.value}资产`,
|
||||||
|
title: `${activeTabLabel.value}列表暂时还是空的`,
|
||||||
|
desc: `当前环境里还没有可展示的${activeTabLabel.value}资产。完成接入或同步后,会统一展示在这里。`,
|
||||||
|
icon: 'mdi mdi-database-search-outline',
|
||||||
|
actionLabel: '',
|
||||||
|
actionIcon: '',
|
||||||
|
tone: 'amber',
|
||||||
|
artLabel: 'ASSET',
|
||||||
|
tips: [
|
||||||
|
'切换页签可查看其他资产类型',
|
||||||
|
`支持按${supportedFilters.slice(0, -1).join('、')}和关键词做过滤`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
eyebrow: '筛选结果为空',
|
||||||
|
title: `没有找到匹配的${activeTabLabel.value}`,
|
||||||
|
desc: hasFilters
|
||||||
|
? `试试清空${supportedFilters.join('、')}筛选,再重新查看。`
|
||||||
|
: `当前列表中还没有满足展示条件的${activeTabLabel.value}资产。`,
|
||||||
|
icon: hasFilters ? 'mdi mdi-tune-variant' : 'mdi mdi-view-grid-outline',
|
||||||
|
actionLabel: hasFilters ? '清空筛选' : '',
|
||||||
|
actionIcon: hasFilters ? 'mdi mdi-filter-remove-outline' : '',
|
||||||
|
tone: hasFilters ? 'primary' : 'slate',
|
||||||
|
artLabel: hasFilters ? 'FILTER' : 'QUEUE',
|
||||||
|
tips: hasFilters
|
||||||
|
? [
|
||||||
|
`${supportedFilters.join('、')}会叠加过滤`,
|
||||||
|
showRiskScenarioFilter.value
|
||||||
|
? '可以换个规则名称或场景分类继续搜索'
|
||||||
|
: '可以换个编码、名称或负责人关键词继续搜索'
|
||||||
|
]
|
||||||
|
: ['列表展示来自真实资产 API', '切换资产类型后会自动重新拉取数据']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeFilterTokens,
|
||||||
|
auditEmptyState,
|
||||||
|
domainOptions,
|
||||||
|
ownerOptions,
|
||||||
|
riskLevelOptions,
|
||||||
|
selectedDomainLabel,
|
||||||
|
selectedEnabledStateLabel,
|
||||||
|
selectedOnlineStateLabel,
|
||||||
|
selectedOwnerLabel,
|
||||||
|
selectedRiskLevelLabel,
|
||||||
|
selectedRiskScenarioLabel,
|
||||||
|
selectedStatusLabel,
|
||||||
|
showEnabledFilter,
|
||||||
|
showOnlineFilter,
|
||||||
|
showOwnerFilter,
|
||||||
|
showRiskLevelFilter,
|
||||||
|
showRiskScenarioFilter,
|
||||||
|
showStatusFilter,
|
||||||
|
visibleSkills
|
||||||
|
}
|
||||||
|
}
|
||||||
205
web/src/views/scripts/auditViewListItemModel.js
Normal file
205
web/src/views/scripts/auditViewListItemModel.js
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import {
|
||||||
|
resolveRiskRuleScoreLabel,
|
||||||
|
resolveRiskRuleScoreLevel,
|
||||||
|
resolveRiskRuleSeverity,
|
||||||
|
resolveRiskRuleSeverityLabel
|
||||||
|
} from './auditViewRiskRuleModel.js'
|
||||||
|
import { normalizeText } from './auditViewDataUtils.js'
|
||||||
|
import {
|
||||||
|
formatDateTime,
|
||||||
|
formatScenarioList,
|
||||||
|
formatSeverity,
|
||||||
|
makeShort,
|
||||||
|
resolveDomainLabel,
|
||||||
|
resolveStatusMeta,
|
||||||
|
resolveTypeKey
|
||||||
|
} from './auditViewFormatters.js'
|
||||||
|
import {
|
||||||
|
buildRiskListSubtitle,
|
||||||
|
isJsonRiskRuleSource,
|
||||||
|
isSpreadsheetRuleSource,
|
||||||
|
readRuleDocumentMeta,
|
||||||
|
resolveRuleScenarioCategory,
|
||||||
|
resolveRuleScenarioList,
|
||||||
|
resolveTabId,
|
||||||
|
resolveTabMeta
|
||||||
|
} from './auditViewRuleClassifier.js'
|
||||||
|
import {
|
||||||
|
resolveRiskRuleBusinessStage,
|
||||||
|
resolveRiskRuleEnabled,
|
||||||
|
resolveRiskRuleOnlineMeta
|
||||||
|
} from './auditViewRiskRuleState.js'
|
||||||
|
export function findLatestMcpCall(runs, assetCode) {
|
||||||
|
const expectedToolName = normalizeText(assetCode).replace(/^mcp\./, '')
|
||||||
|
|
||||||
|
for (const run of runs) {
|
||||||
|
for (const toolCall of run.tool_calls || []) {
|
||||||
|
const toolName = normalizeText(toolCall.tool_name)
|
||||||
|
if (
|
||||||
|
toolName === expectedToolName ||
|
||||||
|
toolName.endsWith(expectedToolName) ||
|
||||||
|
expectedToolName.endsWith(toolName)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
run,
|
||||||
|
toolCall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildRowRuntime(asset, typeKey) {
|
||||||
|
if (typeKey === 'rules') {
|
||||||
|
return formatSeverity(asset.config_json?.severity)
|
||||||
|
}
|
||||||
|
if (typeKey === 'mcp') {
|
||||||
|
return normalizeText(asset.config_json?.endpoint) || '未配置地址'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildRowMetric(asset, typeKey) {
|
||||||
|
if (typeKey === 'rules') {
|
||||||
|
return normalizeText(asset.modified_by) || '未记录'
|
||||||
|
}
|
||||||
|
if (typeKey === 'mcp') {
|
||||||
|
return asset.config_json?.timeout_ms ? `${asset.config_json.timeout_ms} ms` : '未配置超时'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function buildListItem(asset) {
|
||||||
|
const typeKey = resolveTypeKey(asset.asset_type)
|
||||||
|
const tabId = resolveTabId(asset, typeKey)
|
||||||
|
if (!tabId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabMeta = resolveTabMeta(tabId, typeKey)
|
||||||
|
const statusMeta = resolveStatusMeta(asset.status)
|
||||||
|
const workingVersion = asset.working_version || asset.current_version || '-'
|
||||||
|
const changeCount =
|
||||||
|
typeof asset.change_count === 'number'
|
||||||
|
? asset.change_count
|
||||||
|
: Array.isArray(asset.recent_versions)
|
||||||
|
? Math.max(asset.recent_versions.length - 1, 0)
|
||||||
|
: 0
|
||||||
|
const modifiedBy =
|
||||||
|
normalizeText(asset.modified_by) ||
|
||||||
|
normalizeText(
|
||||||
|
Array.isArray(asset.recent_versions)
|
||||||
|
? asset.recent_versions.find((item) => item.version === workingVersion)?.created_by
|
||||||
|
: ''
|
||||||
|
)
|
||||||
|
const isRiskRule = tabId === 'riskRules'
|
||||||
|
const usesSpreadsheetRule = typeKey === 'rules' && isSpreadsheetRuleSource(asset)
|
||||||
|
const usesJsonRiskRule = typeKey === 'rules' && isJsonRiskRuleSource(asset)
|
||||||
|
const ruleDocument = readRuleDocumentMeta(asset)
|
||||||
|
const ruleScenarioCategory = typeKey === 'rules' ? resolveRuleScenarioCategory(asset, tabId) : ''
|
||||||
|
const listSubtitle = isRiskRule
|
||||||
|
? buildRiskListSubtitle(asset.description)
|
||||||
|
: normalizeText(asset.description)
|
||||||
|
const onlineMeta = resolveRiskRuleOnlineMeta(asset.status)
|
||||||
|
const isOnlineValue = onlineMeta.online
|
||||||
|
const isEnabledValue = usesJsonRiskRule ? resolveRiskRuleEnabled(asset) : true
|
||||||
|
const reviewer = normalizeText(asset.reviewer) || '待分配'
|
||||||
|
const creator =
|
||||||
|
normalizeText(asset.owner) ||
|
||||||
|
normalizeText(asset.config_json?.generation_request?.actor) ||
|
||||||
|
modifiedBy ||
|
||||||
|
'未知'
|
||||||
|
const publisher = isRiskRule ? creator : ''
|
||||||
|
const riskRuleCreatedAt = formatDateTime(asset.created_at || asset.updated_at)
|
||||||
|
const businessStage = usesJsonRiskRule
|
||||||
|
? resolveRiskRuleBusinessStage(asset)
|
||||||
|
: { value: '', label: '' }
|
||||||
|
const ruleScenarioList = typeKey === 'rules' ? resolveRuleScenarioList(asset, tabId) : []
|
||||||
|
const riskScoreLevel = usesJsonRiskRule
|
||||||
|
? resolveRiskRuleScoreLevel(asset.config_json, asset.config_json)
|
||||||
|
: ''
|
||||||
|
const riskLevelValue = usesJsonRiskRule
|
||||||
|
? riskScoreLevel || resolveRiskRuleSeverity(asset.config_json)
|
||||||
|
: ''
|
||||||
|
const riskLevelLabel = usesJsonRiskRule
|
||||||
|
? riskScoreLevel
|
||||||
|
? resolveRiskRuleScoreLabel(asset.config_json, asset.config_json) || resolveRiskRuleSeverityLabel(asset.config_json)
|
||||||
|
: resolveRiskRuleSeverityLabel(asset.config_json)
|
||||||
|
: ''
|
||||||
|
const displayName = asset.name
|
||||||
|
const displayCode = asset.code
|
||||||
|
const displaySummary = listSubtitle
|
||||||
|
const displayOwner = isRiskRule ? creator : asset.owner
|
||||||
|
const displayReviewer = reviewer
|
||||||
|
const displayCategory = resolveDomainLabel(asset.domain)
|
||||||
|
const displayScope = typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(asset.scenario_json)
|
||||||
|
const displayEnabledValue = isEnabledValue
|
||||||
|
const displayEnabledLabel = isEnabledValue ? '是' : '否'
|
||||||
|
const displayEnabledTone = isEnabledValue ? 'success' : 'disabled'
|
||||||
|
const searchText = [
|
||||||
|
displayName,
|
||||||
|
displayCode,
|
||||||
|
displaySummary,
|
||||||
|
displayOwner,
|
||||||
|
displayScope,
|
||||||
|
riskLevelLabel
|
||||||
|
]
|
||||||
|
.map((value) => normalizeText(value).toLowerCase())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: asset.id,
|
||||||
|
tabId,
|
||||||
|
type: typeKey,
|
||||||
|
isPreviewMock: Boolean(asset.isPreviewMock),
|
||||||
|
usesSpreadsheetRule,
|
||||||
|
usesJsonRiskRule,
|
||||||
|
ruleDocument,
|
||||||
|
typeLabel: tabMeta.typeLabel,
|
||||||
|
short: makeShort(displayName),
|
||||||
|
name: displayName,
|
||||||
|
code: displayCode,
|
||||||
|
rawCode: asset.code,
|
||||||
|
summary: displaySummary,
|
||||||
|
listSubtitle: displaySummary,
|
||||||
|
category: displayCategory,
|
||||||
|
owner: displayOwner,
|
||||||
|
reviewer: displayReviewer,
|
||||||
|
scope: displayScope,
|
||||||
|
riskCategory: ruleScenarioCategory,
|
||||||
|
scenarioList: ruleScenarioList,
|
||||||
|
businessStageValue: businessStage.value,
|
||||||
|
businessStageLabel: businessStage.label,
|
||||||
|
riskLevelValue,
|
||||||
|
riskLevelLabel,
|
||||||
|
riskLevelTone: riskLevelValue,
|
||||||
|
model: buildRowRuntime(asset, typeKey),
|
||||||
|
version: workingVersion,
|
||||||
|
versionDisplay: typeKey === 'rules' ? `${changeCount} 次` : workingVersion,
|
||||||
|
publishedVersion: asset.published_version || '-',
|
||||||
|
workingVersion,
|
||||||
|
status: statusMeta.label,
|
||||||
|
statusValue: asset.status,
|
||||||
|
statusTone: statusMeta.tone,
|
||||||
|
hitRate: isRiskRule ? publisher : buildRowMetric({ ...asset, modified_by: modifiedBy }, typeKey),
|
||||||
|
creator,
|
||||||
|
publisher,
|
||||||
|
publishedAt: isOnlineValue ? formatDateTime(asset.published_at || asset.updated_at) : '-',
|
||||||
|
isOnlineValue,
|
||||||
|
isOnlineLabel: onlineMeta.label,
|
||||||
|
isOnlineTone: onlineMeta.tone,
|
||||||
|
isEnabledValue: displayEnabledValue,
|
||||||
|
isEnabledLabel: displayEnabledLabel,
|
||||||
|
isEnabledTone: displayEnabledTone,
|
||||||
|
modifiedBy,
|
||||||
|
changeCount,
|
||||||
|
updatedAt: isRiskRule ? riskRuleCreatedAt : formatDateTime(asset.updated_at),
|
||||||
|
badgeTone: tabMeta.badgeTone,
|
||||||
|
domainValue: asset.domain,
|
||||||
|
searchText
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
197
web/src/views/scripts/auditViewRiskRuleState.js
Normal file
197
web/src/views/scripts/auditViewRiskRuleState.js
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import {
|
||||||
|
buildRiskRuleFieldSummary,
|
||||||
|
formatRiskRuleAge,
|
||||||
|
resolveRiskRuleBusinessDescription,
|
||||||
|
resolveRiskRuleCreatedAt,
|
||||||
|
resolveRiskRuleFields,
|
||||||
|
resolveRiskRuleFlow,
|
||||||
|
resolveRiskRuleFlowDiagramSvg,
|
||||||
|
resolveRiskRuleScore,
|
||||||
|
resolveRiskRuleScoreDetail,
|
||||||
|
resolveRiskRuleScoreLabel,
|
||||||
|
resolveRiskRuleScoreLevel,
|
||||||
|
resolveRiskRuleSeverity,
|
||||||
|
resolveRiskRuleSeverityLabel
|
||||||
|
} from './auditViewRiskRuleModel.js'
|
||||||
|
import {
|
||||||
|
isPlainObject,
|
||||||
|
normalizeText,
|
||||||
|
readConfigJson
|
||||||
|
} from './auditViewDataUtils.js'
|
||||||
|
import { formatDateTime } from './auditViewFormatters.js'
|
||||||
|
import {
|
||||||
|
buildRiskListSubtitle,
|
||||||
|
resolveRiskRuleCategory,
|
||||||
|
resolveRiskRuleDescription,
|
||||||
|
resolveRiskRuleSourceRef
|
||||||
|
} from './auditViewRuleClassifier.js'
|
||||||
|
export function resolveRiskRuleEnabled(source, rulePayload = null) {
|
||||||
|
const configJson = readConfigJson(source)
|
||||||
|
if (isPlainObject(rulePayload) && rulePayload.enabled === false) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (source?.enabled === false || configJson.enabled === false) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const LAST_OPERATION_LABELS = {
|
||||||
|
generate: '开始生成',
|
||||||
|
create: '创建',
|
||||||
|
test: '测试',
|
||||||
|
online: '上线',
|
||||||
|
offline: '下线',
|
||||||
|
delete: '删除',
|
||||||
|
update: '更新'
|
||||||
|
}
|
||||||
|
|
||||||
|
const RISK_RULE_BUSINESS_STAGE_LABELS = {
|
||||||
|
expense_application: '费用申请',
|
||||||
|
reimbursement: '费用报销'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRiskRuleBusinessStage(source, rulePayload = null) {
|
||||||
|
const configJson = readConfigJson(source)
|
||||||
|
const metadata = rulePayload && typeof rulePayload === 'object' ? rulePayload.metadata || {} : {}
|
||||||
|
const stage =
|
||||||
|
normalizeText(configJson.business_stage) ||
|
||||||
|
normalizeText(metadata.business_stage) ||
|
||||||
|
normalizeText(rulePayload?.business_stage)
|
||||||
|
const label =
|
||||||
|
normalizeText(configJson.business_stage_label) ||
|
||||||
|
normalizeText(metadata.business_stage_label) ||
|
||||||
|
RISK_RULE_BUSINESS_STAGE_LABELS[stage]
|
||||||
|
return {
|
||||||
|
value: stage || 'reimbursement',
|
||||||
|
label: label || '费用报销'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRiskRuleOnlineMeta(statusValue) {
|
||||||
|
if (statusValue === 'active') {
|
||||||
|
return { label: '已上线', tone: 'success', online: true }
|
||||||
|
}
|
||||||
|
if (statusValue === 'disabled') {
|
||||||
|
return { label: '已下线', tone: 'disabled', online: false }
|
||||||
|
}
|
||||||
|
if (statusValue === 'generating') {
|
||||||
|
return { label: '生成中', tone: 'info', online: false }
|
||||||
|
}
|
||||||
|
if (statusValue === 'failed') {
|
||||||
|
return { label: '生成失败', tone: 'danger', online: false }
|
||||||
|
}
|
||||||
|
return { label: '待上线', tone: 'draft', online: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveLastOperationLabel(source, fallback = {}) {
|
||||||
|
const configJson = readConfigJson(source)
|
||||||
|
const operation = isPlainObject(configJson.last_operation) ? configJson.last_operation : {}
|
||||||
|
const action = normalizeText(operation.action) || normalizeText(fallback.action) || 'create'
|
||||||
|
const actor = normalizeText(operation.actor) || normalizeText(fallback.actor) || '系统'
|
||||||
|
const at = normalizeText(operation.at) || normalizeText(fallback.at)
|
||||||
|
const actionLabel = LAST_OPERATION_LABELS[action] || action
|
||||||
|
const timeLabel = formatDateTime(at)
|
||||||
|
return timeLabel && timeLabel !== '-' ? `${actionLabel}:${actor} · ${timeLabel}` : `${actionLabel}:${actor}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyRiskRuleJsonState(target, payload, apiPayload) {
|
||||||
|
const rulePayload = isPlainObject(payload) ? payload : {}
|
||||||
|
const metadata = rulePayload.metadata && typeof rulePayload.metadata === 'object'
|
||||||
|
? rulePayload.metadata
|
||||||
|
: {}
|
||||||
|
const apiConfig = apiPayload?.config_json && typeof apiPayload.config_json === 'object'
|
||||||
|
? apiPayload.config_json
|
||||||
|
: {}
|
||||||
|
const fullDescription =
|
||||||
|
resolveRiskRuleDescription(rulePayload) ||
|
||||||
|
normalizeText(apiPayload?.description) ||
|
||||||
|
normalizeText(target.riskRuleDescription)
|
||||||
|
const riskCategory =
|
||||||
|
normalizeText(metadata.expense_category_label) ||
|
||||||
|
normalizeText(apiConfig.expense_category_label) ||
|
||||||
|
normalizeText(rulePayload.risk_category) ||
|
||||||
|
resolveRiskRuleCategory({ ...target, risk_category: rulePayload.risk_category, config_json: rulePayload })
|
||||||
|
const businessStage = resolveRiskRuleBusinessStage(target, rulePayload)
|
||||||
|
const riskRuleFields = resolveRiskRuleFields(rulePayload)
|
||||||
|
const riskRuleCreatedAt = resolveRiskRuleCreatedAt(rulePayload, target.createdAt || target.updatedAt)
|
||||||
|
const riskRuleScoreLevel = resolveRiskRuleScoreLevel(rulePayload, apiConfig)
|
||||||
|
|
||||||
|
const statusValue = apiPayload?.status || target.statusValue || 'draft'
|
||||||
|
const onlineMeta = resolveRiskRuleOnlineMeta(statusValue)
|
||||||
|
const isEnabledValue = resolveRiskRuleEnabled(target, rulePayload)
|
||||||
|
|
||||||
|
const publisher =
|
||||||
|
target.creator ||
|
||||||
|
normalizeText(apiPayload?.owner) ||
|
||||||
|
normalizeText(metadata.created_by) ||
|
||||||
|
normalizeText(apiPayload?.recent_versions?.[0]?.created_by) ||
|
||||||
|
'未知'
|
||||||
|
|
||||||
|
let publishedAt = target.publishedAt || '-'
|
||||||
|
if (apiPayload?.recent_versions) {
|
||||||
|
const publishedVersionObj = apiPayload.recent_versions.find((item) =>
|
||||||
|
item?.is_current || item?.version === apiPayload?.published_version
|
||||||
|
)
|
||||||
|
publishedAt = publishedVersionObj?.created_at
|
||||||
|
? formatDateTime(publishedVersionObj.created_at)
|
||||||
|
: (apiPayload?.latest_review?.reviewed_at ? formatDateTime(apiPayload.latest_review.reviewed_at) : '-')
|
||||||
|
} else if (apiPayload?.latest_review?.reviewed_at) {
|
||||||
|
publishedAt = formatDateTime(apiPayload.latest_review.reviewed_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...target,
|
||||||
|
riskRuleDescription: fullDescription,
|
||||||
|
riskRuleBusinessDescription: resolveRiskRuleBusinessDescription(rulePayload, fullDescription),
|
||||||
|
riskRuleSubtitle: buildRiskListSubtitle(fullDescription, 48),
|
||||||
|
riskCategory,
|
||||||
|
businessStageValue: businessStage.value,
|
||||||
|
businessStageLabel: businessStage.label,
|
||||||
|
scope: riskCategory,
|
||||||
|
riskRuleSourceRef: resolveRiskRuleSourceRef(rulePayload),
|
||||||
|
riskRuleSeverity: riskRuleScoreLevel || resolveRiskRuleSeverity(rulePayload),
|
||||||
|
riskRuleSeverityLabel: riskRuleScoreLevel
|
||||||
|
? resolveRiskRuleScoreLabel(rulePayload, apiConfig)
|
||||||
|
: resolveRiskRuleSeverityLabel(rulePayload),
|
||||||
|
riskRuleScore: resolveRiskRuleScore(rulePayload, apiConfig),
|
||||||
|
riskRuleScoreLabel: resolveRiskRuleScoreLabel(rulePayload, apiConfig),
|
||||||
|
riskRuleScoreLevel: riskRuleScoreLevel || resolveRiskRuleSeverity(rulePayload),
|
||||||
|
riskRuleScoreDetail: resolveRiskRuleScoreDetail(rulePayload, apiConfig),
|
||||||
|
riskRuleCreatedAt: formatDateTime(riskRuleCreatedAt),
|
||||||
|
riskRuleAgeLabel: formatRiskRuleAge(riskRuleCreatedAt),
|
||||||
|
riskRuleFields,
|
||||||
|
riskRuleFieldSummary: buildRiskRuleFieldSummary(riskRuleFields),
|
||||||
|
riskRuleFlow: resolveRiskRuleFlow(rulePayload, riskRuleFields),
|
||||||
|
riskRuleFlowDiagramSvg: resolveRiskRuleFlowDiagramSvg({
|
||||||
|
...rulePayload,
|
||||||
|
flow_diagram_svg: normalizeText(apiPayload?.flow_diagram_svg) || rulePayload?.flow_diagram_svg
|
||||||
|
}),
|
||||||
|
riskRuleRequiresAttachment: Boolean(
|
||||||
|
rulePayload.requires_attachment ||
|
||||||
|
metadata.requires_attachment ||
|
||||||
|
apiConfig.requires_attachment ||
|
||||||
|
target.configJson?.requires_attachment
|
||||||
|
),
|
||||||
|
riskRuleSummary: {
|
||||||
|
name: apiPayload?.name || target.name,
|
||||||
|
evaluator: apiPayload?.evaluator || rulePayload.evaluator || '',
|
||||||
|
ontologySignal: apiPayload?.ontology_signal || rulePayload.ontology_signal || '',
|
||||||
|
inputs: apiPayload?.inputs || rulePayload.inputs || {},
|
||||||
|
outcomes: apiPayload?.outcomes || rulePayload.outcomes || {}
|
||||||
|
},
|
||||||
|
riskRuleJsonText: JSON.stringify(rulePayload, null, 2),
|
||||||
|
isOnlineValue: onlineMeta.online,
|
||||||
|
isOnlineLabel: onlineMeta.label,
|
||||||
|
isOnlineTone: onlineMeta.tone,
|
||||||
|
isEnabledValue,
|
||||||
|
isEnabledLabel: isEnabledValue ? '是' : '否',
|
||||||
|
isEnabledTone: isEnabledValue ? 'success' : 'disabled',
|
||||||
|
lastOperationLabel: resolveLastOperationLabel(target, {
|
||||||
|
actor: publisher,
|
||||||
|
at: riskRuleCreatedAt
|
||||||
|
}),
|
||||||
|
publisher,
|
||||||
|
publishedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
328
web/src/views/scripts/auditViewRuleClassifier.js
Normal file
328
web/src/views/scripts/auditViewRuleClassifier.js
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
import {
|
||||||
|
JSON_RISK_DETAIL_MODE,
|
||||||
|
LEGACY_RISK_SCENARIO_KEYS,
|
||||||
|
RISK_SCENARIO_VALUES,
|
||||||
|
RULE_TAB_TAG_ALIASES,
|
||||||
|
SPREADSHEET_DETAIL_MODE,
|
||||||
|
TAB_META,
|
||||||
|
TYPE_META
|
||||||
|
} from './auditViewMetadata.js'
|
||||||
|
import {
|
||||||
|
isPlainObject,
|
||||||
|
normalizeText,
|
||||||
|
readConfigJson
|
||||||
|
} from './auditViewDataUtils.js'
|
||||||
|
import { formatScenarioList } from './auditViewFormatters.js'
|
||||||
|
const EXPENSE_TYPE_SCENARIO_LABELS = {
|
||||||
|
travel: '差旅费',
|
||||||
|
hotel: '住宿费',
|
||||||
|
transport: '交通费',
|
||||||
|
meal: '业务招待费',
|
||||||
|
meeting: '会务费',
|
||||||
|
marketing: '市场推广费',
|
||||||
|
office: '办公用品费',
|
||||||
|
training: '培训费',
|
||||||
|
software: '软件服务费',
|
||||||
|
communication: '通信费',
|
||||||
|
welfare: '福利费'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readRuleDocumentMeta(value) {
|
||||||
|
const configJson = readConfigJson(value)
|
||||||
|
return isPlainObject(configJson.rule_document) ? configJson.rule_document : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSpreadsheetRuleSource(value) {
|
||||||
|
const configJson = readConfigJson(value)
|
||||||
|
return normalizeText(configJson.detail_mode || configJson.rule_detail_mode).toLowerCase() === SPREADSHEET_DETAIL_MODE
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isJsonRiskRuleSource(value) {
|
||||||
|
const configJson = readConfigJson(value)
|
||||||
|
return normalizeText(configJson.detail_mode || configJson.rule_detail_mode).toLowerCase() === JSON_RISK_DETAIL_MODE
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeRuleTagValue(value) {
|
||||||
|
return normalizeText(value).toLowerCase().replace(/[\s_-]+/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectRuleTagValues(source) {
|
||||||
|
const configJson = readConfigJson(source)
|
||||||
|
const rawValues = [
|
||||||
|
configJson.tag,
|
||||||
|
configJson.rule_tag,
|
||||||
|
...(Array.isArray(configJson.tags) ? configJson.tags : []),
|
||||||
|
...(Array.isArray(configJson.rule_tags) ? configJson.rule_tags : [])
|
||||||
|
]
|
||||||
|
|
||||||
|
return rawValues.map((item) => normalizeText(item)).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRuleTabId(source) {
|
||||||
|
const code = normalizeText(source?.code || '').toLowerCase()
|
||||||
|
if (code.startsWith('risk.')) {
|
||||||
|
return 'riskRules'
|
||||||
|
}
|
||||||
|
if (isJsonRiskRuleSource(source)) {
|
||||||
|
return 'riskRules'
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedTags = collectRuleTagValues(source).map((item) => normalizeRuleTagValue(item))
|
||||||
|
|
||||||
|
if (normalizedTags.some((item) => RULE_TAB_TAG_ALIASES.riskRules.has(item))) {
|
||||||
|
return 'riskRules'
|
||||||
|
}
|
||||||
|
if (normalizedTags.some((item) => RULE_TAB_TAG_ALIASES.financialRules.has(item))) {
|
||||||
|
return 'financialRules'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTabId(source, typeKey) {
|
||||||
|
if (typeKey === 'rules') {
|
||||||
|
return resolveRuleTabId(source)
|
||||||
|
}
|
||||||
|
return typeKey
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTabMeta(tabId, typeKey) {
|
||||||
|
if (TAB_META[tabId]) {
|
||||||
|
return TAB_META[tabId]
|
||||||
|
}
|
||||||
|
if (typeKey === 'rules') {
|
||||||
|
return {
|
||||||
|
...TYPE_META.rules,
|
||||||
|
typeKey: 'rules',
|
||||||
|
badgeTone: 'primary'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TAB_META[typeKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRiskRuleDescription(payload) {
|
||||||
|
if (!isPlainObject(payload)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return normalizeText(payload.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRiskRuleSourceRef(payload) {
|
||||||
|
if (!isPlainObject(payload)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const metadata = isPlainObject(payload.metadata) ? payload.metadata : {}
|
||||||
|
return normalizeText(metadata.source_ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inferRiskCategoryFromCode(code) {
|
||||||
|
const normalized = normalizeText(code).toLowerCase()
|
||||||
|
if (normalized.startsWith('risk.travel.')) {
|
||||||
|
return '差旅'
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('risk.invoice.')) {
|
||||||
|
return '发票'
|
||||||
|
}
|
||||||
|
if (normalized.includes('entertainment') || normalized.includes('meal_localized')) {
|
||||||
|
return '餐饮招待'
|
||||||
|
}
|
||||||
|
if (normalized.includes('consecutive_transport')) {
|
||||||
|
return '交通出行'
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('risk.expense.')) {
|
||||||
|
return '费用科目'
|
||||||
|
}
|
||||||
|
return '通用'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeRiskScenarioCategory(value) {
|
||||||
|
const normalized = normalizeText(value)
|
||||||
|
const alias = normalized === '通讯费' ? '通信费' : normalized
|
||||||
|
return RISK_SCENARIO_VALUES.has(alias) ? alias : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeExpenseTypeScenarioLabels(value) {
|
||||||
|
const values = Array.isArray(value) ? value : normalizeText(value) ? [value] : []
|
||||||
|
const labels = []
|
||||||
|
const seen = new Set()
|
||||||
|
|
||||||
|
values.forEach((item) => {
|
||||||
|
const key = normalizeText(item).toLowerCase()
|
||||||
|
const label = EXPENSE_TYPE_SCENARIO_LABELS[key] || normalizeRiskScenarioCategory(item)
|
||||||
|
if (!label || seen.has(label)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen.add(label)
|
||||||
|
labels.push(label)
|
||||||
|
})
|
||||||
|
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readRiskRuleExpenseTypes(source) {
|
||||||
|
const configJson = readConfigJson(source)
|
||||||
|
const metadata = isPlainObject(configJson.metadata) ? configJson.metadata : {}
|
||||||
|
const appliesTo = isPlainObject(configJson.applies_to) ? configJson.applies_to : {}
|
||||||
|
const values = []
|
||||||
|
|
||||||
|
;[
|
||||||
|
configJson.expense_types,
|
||||||
|
metadata.expense_types,
|
||||||
|
appliesTo.expense_types,
|
||||||
|
source?.expense_types
|
||||||
|
].forEach((item) => {
|
||||||
|
if (Array.isArray(item)) {
|
||||||
|
values.push(...item)
|
||||||
|
} else if (normalizeText(item)) {
|
||||||
|
values.push(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readScenarioItems(source) {
|
||||||
|
if (Array.isArray(source?.scenario_json)) {
|
||||||
|
return source.scenario_json
|
||||||
|
}
|
||||||
|
if (Array.isArray(source?.scenarioList)) {
|
||||||
|
return source.scenarioList
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRiskRuleCategory(source) {
|
||||||
|
const configJson = readConfigJson(source)
|
||||||
|
const expenseScenarioLabels = normalizeExpenseTypeScenarioLabels(readRiskRuleExpenseTypes(source))
|
||||||
|
if (expenseScenarioLabels.length) {
|
||||||
|
return formatScenarioList(expenseScenarioLabels)
|
||||||
|
}
|
||||||
|
|
||||||
|
const expenseCategoryLabel =
|
||||||
|
normalizeText(configJson.expense_category_label) ||
|
||||||
|
normalizeText(configJson.metadata?.expense_category_label) ||
|
||||||
|
normalizeText(source?.expense_category_label)
|
||||||
|
if (expenseCategoryLabel) {
|
||||||
|
return expenseCategoryLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
const explicit = normalizeRiskScenarioCategory(configJson.risk_category)
|
||||||
|
if (explicit) {
|
||||||
|
return explicit
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadCategory = normalizeRiskScenarioCategory(source?.risk_category)
|
||||||
|
if (payloadCategory) {
|
||||||
|
return payloadCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
const scenarioItems = readScenarioItems(source)
|
||||||
|
const businessScenario = scenarioItems
|
||||||
|
.map((item) => normalizeText(item))
|
||||||
|
.find((item) => item && !LEGACY_RISK_SCENARIO_KEYS.has(item) && RISK_SCENARIO_VALUES.has(item))
|
||||||
|
if (businessScenario) {
|
||||||
|
return businessScenario
|
||||||
|
}
|
||||||
|
|
||||||
|
return inferRiskCategoryFromCode(source?.code)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inferFinancialRuleCategory(source) {
|
||||||
|
const configJson = readConfigJson(source)
|
||||||
|
const explicit =
|
||||||
|
normalizeRiskScenarioCategory(configJson.scenario_category) ||
|
||||||
|
normalizeRiskScenarioCategory(configJson.ai_review_category) ||
|
||||||
|
normalizeRiskScenarioCategory(configJson.risk_category) ||
|
||||||
|
normalizeRiskScenarioCategory(source?.scenario_category) ||
|
||||||
|
normalizeRiskScenarioCategory(source?.risk_category)
|
||||||
|
if (explicit) {
|
||||||
|
return explicit
|
||||||
|
}
|
||||||
|
|
||||||
|
const scenarioCategory = readScenarioItems(source)
|
||||||
|
.map((item) => normalizeRiskScenarioCategory(item))
|
||||||
|
.find(Boolean)
|
||||||
|
if (scenarioCategory) {
|
||||||
|
return scenarioCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
const configRuntimeRule = isPlainObject(configJson.runtime_rule) ? configJson.runtime_rule : {}
|
||||||
|
const haystack = [
|
||||||
|
source?.code,
|
||||||
|
source?.name,
|
||||||
|
source?.description,
|
||||||
|
configJson.runtime_kind,
|
||||||
|
configRuntimeRule.kind,
|
||||||
|
configRuntimeRule.scenario,
|
||||||
|
configRuntimeRule.template_key,
|
||||||
|
...readScenarioItems(source)
|
||||||
|
]
|
||||||
|
.map((item) => normalizeText(item).toLowerCase())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
|
||||||
|
if (!haystack) {
|
||||||
|
return '通用'
|
||||||
|
}
|
||||||
|
if (/(travel|trip|差旅|出差|住宿|酒店)/i.test(haystack)) {
|
||||||
|
return '差旅'
|
||||||
|
}
|
||||||
|
if (/(invoice|receipt|attachment|票据|发票|单据|附件)/i.test(haystack)) {
|
||||||
|
return '发票'
|
||||||
|
}
|
||||||
|
if (/(meal|dining|entertainment|餐饮|招待|餐费|用餐)/i.test(haystack)) {
|
||||||
|
return '餐饮招待'
|
||||||
|
}
|
||||||
|
if (/(transport|traffic|taxi|交通|出行|打车|机票|火车|高铁|地铁|公交)/i.test(haystack)) {
|
||||||
|
return '交通出行'
|
||||||
|
}
|
||||||
|
if (/(office|material|suppl|办公|物料|耗材)/i.test(haystack)) {
|
||||||
|
return '办公物料'
|
||||||
|
}
|
||||||
|
if (/(communication|telecom|phone|通信|通讯|手机)/i.test(haystack)) {
|
||||||
|
return '通信费'
|
||||||
|
}
|
||||||
|
if (/(welfare|福利)/i.test(haystack)) {
|
||||||
|
return '福利费'
|
||||||
|
}
|
||||||
|
if (/(expense_standard|费用科目|费用标准|补贴|科目)/i.test(haystack)) {
|
||||||
|
return '费用科目'
|
||||||
|
}
|
||||||
|
return '通用'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRuleScenarioCategory(source, tabId = '') {
|
||||||
|
const scenarioList = resolveRuleScenarioList(source, tabId)
|
||||||
|
if (scenarioList.length) {
|
||||||
|
return formatScenarioList(scenarioList)
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRuleScenarioList(source, tabId = '') {
|
||||||
|
const resolvedTabId = tabId || resolveRuleTabId(source)
|
||||||
|
if (resolvedTabId === 'riskRules' || isJsonRiskRuleSource(source)) {
|
||||||
|
const expenseScenarioLabels = normalizeExpenseTypeScenarioLabels(readRiskRuleExpenseTypes(source))
|
||||||
|
if (expenseScenarioLabels.length) {
|
||||||
|
return expenseScenarioLabels
|
||||||
|
}
|
||||||
|
const riskCategory = resolveRiskRuleCategory(source)
|
||||||
|
return riskCategory ? [riskCategory] : []
|
||||||
|
}
|
||||||
|
if (resolvedTabId === 'financialRules') {
|
||||||
|
const financialCategory = inferFinancialRuleCategory(source)
|
||||||
|
return financialCategory ? [financialCategory] : []
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildRiskListSubtitle(text, maxLength = 42) {
|
||||||
|
const normalized = normalizeText(text)
|
||||||
|
if (!normalized) {
|
||||||
|
return '平台内置风险规则'
|
||||||
|
}
|
||||||
|
const firstSentence = normalized.split(/[。;;!?\n]/)[0] || normalized
|
||||||
|
if (firstSentence.length <= maxLength) {
|
||||||
|
return firstSentence
|
||||||
|
}
|
||||||
|
return `${firstSentence.slice(0, maxLength)}…`
|
||||||
|
}
|
||||||
117
web/src/views/scripts/auditViewRuleContentModel.js
Normal file
117
web/src/views/scripts/auditViewRuleContentModel.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import {
|
||||||
|
EXPENSE_RULE_BLOCK_PATTERN,
|
||||||
|
RULE_SPREADSHEET_BLOCK_PATTERN,
|
||||||
|
RULE_TEMPLATE_LABELS
|
||||||
|
} from './auditViewMetadata.js'
|
||||||
|
import {
|
||||||
|
isPlainObject,
|
||||||
|
normalizeText,
|
||||||
|
readConfigJson
|
||||||
|
} from './auditViewDataUtils.js'
|
||||||
|
|
||||||
|
export function cloneJsonObject(value) {
|
||||||
|
if (!isPlainObject(value)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(value))
|
||||||
|
} catch {
|
||||||
|
return { ...value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRuleTemplateLabel(value) {
|
||||||
|
const templateKey = normalizeText(value)
|
||||||
|
return RULE_TEMPLATE_LABELS[templateKey] || templateKey || '未指定模板'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractRuntimeRuleFromMarkdown(markdown) {
|
||||||
|
const match = String(markdown || '').match(EXPENSE_RULE_BLOCK_PATTERN)
|
||||||
|
if (!match) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(match[1])
|
||||||
|
return isPlainObject(payload) ? payload : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractSpreadsheetMetaFromMarkdown(markdown) {
|
||||||
|
const match = String(markdown || '').match(RULE_SPREADSHEET_BLOCK_PATTERN)
|
||||||
|
if (!match) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(match[1])
|
||||||
|
return isPlainObject(payload) ? payload : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripRuntimeRuleBlock(markdown) {
|
||||||
|
return String(markdown || '')
|
||||||
|
.replace(EXPENSE_RULE_BLOCK_PATTERN, '')
|
||||||
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stringifyRuntimeRule(runtimeRule) {
|
||||||
|
return JSON.stringify(isPlainObject(runtimeRule) ? runtimeRule : {}, null, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRuntimeRuleText(runtimeRuleText) {
|
||||||
|
const text = normalizeText(runtimeRuleText)
|
||||||
|
if (!text) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(text)
|
||||||
|
return isPlainObject(payload) ? payload : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDefaultRuntimeRule(source) {
|
||||||
|
const configJson = readConfigJson(source)
|
||||||
|
const scenarioItems = Array.isArray(source?.scenario_json)
|
||||||
|
? source.scenario_json
|
||||||
|
: Array.isArray(source?.scenarioList)
|
||||||
|
? source.scenarioList
|
||||||
|
: []
|
||||||
|
const configRuntimeRule = cloneJsonObject(configJson.runtime_rule)
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: normalizeText(configRuntimeRule?.kind || configJson.runtime_kind) || 'policy_rule_draft',
|
||||||
|
version:
|
||||||
|
typeof configRuntimeRule?.version === 'number' && Number.isFinite(configRuntimeRule.version)
|
||||||
|
? configRuntimeRule.version
|
||||||
|
: 1,
|
||||||
|
template_key:
|
||||||
|
normalizeText(configRuntimeRule?.template_key || configJson.rule_template_key) || 'general_policy_v1',
|
||||||
|
rule_name: normalizeText(configRuntimeRule?.rule_name || source?.name) || '未命名规则',
|
||||||
|
scenario: normalizeText(configRuntimeRule?.scenario || scenarioItems[0]) || 'expense',
|
||||||
|
review_required:
|
||||||
|
typeof configRuntimeRule?.review_required === 'boolean' ? configRuntimeRule.review_required : true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRuntimeRuleForVersion(source, rawMarkdown, runtimeRuleFallback = null) {
|
||||||
|
return (
|
||||||
|
cloneJsonObject(extractRuntimeRuleFromMarkdown(rawMarkdown)) ||
|
||||||
|
cloneJsonObject(runtimeRuleFallback) ||
|
||||||
|
buildDefaultRuntimeRule(source)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMarkdownVersionContent(markdownContent, runtimeRule) {
|
||||||
|
const body = stripRuntimeRuleBlock(markdownContent)
|
||||||
|
const runtimeBlock = ['```expense-rule', stringifyRuntimeRule(runtimeRule), '```'].join('\n')
|
||||||
|
return body ? `${body}\n\n${runtimeBlock}` : runtimeBlock
|
||||||
|
}
|
||||||
@@ -84,50 +84,53 @@ export function buildSpreadsheetChangeRecordKey(records = []) {
|
|||||||
|
|
||||||
export function filterAuditAssets(assets = [], filters = {}) {
|
export function filterAuditAssets(assets = [], filters = {}) {
|
||||||
const normalizedKeyword = normalizeText(filters.keyword).toLowerCase()
|
const normalizedKeyword = normalizeText(filters.keyword).toLowerCase()
|
||||||
|
const hasKeyword = Boolean(normalizedKeyword)
|
||||||
|
const hasDomain = Boolean(filters.selectedDomain)
|
||||||
|
const hasOwner = Boolean(filters.selectedOwner)
|
||||||
|
const hasRiskLevel = Boolean(filters.selectedRiskLevel)
|
||||||
|
const hasStatus = Boolean(filters.showStatusFilter && filters.selectedStatus)
|
||||||
|
const hasRiskScenario = Boolean(filters.showRiskScenarioFilter && filters.selectedRiskScenario)
|
||||||
|
const hasOnline = Boolean(filters.showOnlineFilter && filters.selectedOnlineState)
|
||||||
|
const hasEnabled = Boolean(filters.showEnabledFilter && filters.selectedEnabledState)
|
||||||
|
|
||||||
return assets.filter((item) => {
|
return assets.filter((item) => {
|
||||||
const matchesKeyword = normalizedKeyword
|
if (hasKeyword) {
|
||||||
? [item.name, item.code, item.summary, item.owner, item.scope, item.riskLevelLabel]
|
const searchText = item.searchText || [item.name, item.code, item.summary, item.owner, item.scope, item.riskLevelLabel]
|
||||||
|
.map((value) => normalizeText(value).toLowerCase())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.some((value) => String(value).toLowerCase().includes(normalizedKeyword))
|
.join(' ')
|
||||||
: true
|
if (!searchText.includes(normalizedKeyword)) {
|
||||||
const matchesDomain = filters.selectedDomain ? item.domainValue === filters.selectedDomain : true
|
return false
|
||||||
const matchesOwner = filters.selectedOwner ? item.owner === filters.selectedOwner : true
|
}
|
||||||
const matchesRiskLevel = filters.selectedRiskLevel
|
}
|
||||||
? item.riskLevelValue === filters.selectedRiskLevel
|
if (hasDomain && item.domainValue !== filters.selectedDomain) {
|
||||||
: true
|
return false
|
||||||
const matchesStatus = filters.showStatusFilter
|
}
|
||||||
? filters.selectedStatus
|
if (hasOwner && item.owner !== filters.selectedOwner) {
|
||||||
? item.statusValue === filters.selectedStatus
|
return false
|
||||||
: true
|
}
|
||||||
: true
|
if (hasRiskLevel && item.riskLevelValue !== filters.selectedRiskLevel) {
|
||||||
const matchesRiskScenario = filters.showRiskScenarioFilter
|
return false
|
||||||
? filters.selectedRiskScenario
|
}
|
||||||
? Array.isArray(item.scenarioList) && item.scenarioList.length
|
if (hasStatus && item.statusValue !== filters.selectedStatus) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (hasRiskScenario) {
|
||||||
|
const hasScenarioList = Array.isArray(item.scenarioList) && item.scenarioList.length
|
||||||
|
const matched = hasScenarioList
|
||||||
? item.scenarioList.includes(filters.selectedRiskScenario)
|
? item.scenarioList.includes(filters.selectedRiskScenario)
|
||||||
: item.riskCategory === filters.selectedRiskScenario
|
: item.riskCategory === filters.selectedRiskScenario
|
||||||
: true
|
if (!matched) {
|
||||||
: true
|
return false
|
||||||
const matchesOnline = filters.showOnlineFilter
|
}
|
||||||
? filters.selectedOnlineState
|
}
|
||||||
? (filters.selectedOnlineState === 'online') === Boolean(item.isOnlineValue)
|
if (hasOnline && (filters.selectedOnlineState === 'online') !== Boolean(item.isOnlineValue)) {
|
||||||
: true
|
return false
|
||||||
: true
|
}
|
||||||
const matchesEnabled = filters.showEnabledFilter
|
if (hasEnabled && (filters.selectedEnabledState === 'enabled') !== Boolean(item.isEnabledValue)) {
|
||||||
? filters.selectedEnabledState
|
return false
|
||||||
? (filters.selectedEnabledState === 'enabled') === Boolean(item.isEnabledValue)
|
}
|
||||||
: true
|
|
||||||
: true
|
|
||||||
|
|
||||||
return (
|
return true
|
||||||
matchesKeyword &&
|
|
||||||
matchesDomain &&
|
|
||||||
matchesOwner &&
|
|
||||||
matchesRiskLevel &&
|
|
||||||
matchesStatus &&
|
|
||||||
matchesRiskScenario &&
|
|
||||||
matchesOnline &&
|
|
||||||
matchesEnabled
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
180
web/src/views/scripts/digitalEmployeesViewModel.js
Normal file
180
web/src/views/scripts/digitalEmployeesViewModel.js
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import {
|
||||||
|
buildDigitalEmployeeDetailMeta,
|
||||||
|
buildDigitalEmployeeListMeta
|
||||||
|
} from './auditViewDigitalEmployeeModel.js'
|
||||||
|
import {
|
||||||
|
formatDateTime,
|
||||||
|
normalizeText,
|
||||||
|
resolveStatusMeta
|
||||||
|
} from './auditViewModel.js'
|
||||||
|
|
||||||
|
export const DIGITAL_EMPLOYEE_EXECUTION_MODE_OPTIONS = [
|
||||||
|
{ value: '', label: '全部执行方式' },
|
||||||
|
{ value: 'timed', label: '定时执行' },
|
||||||
|
{ value: 'manual', label: '手动触发' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export function resolveDigitalEmployeeStatusColor(statusValue) {
|
||||||
|
const normalized = normalizeText(statusValue).toLowerCase()
|
||||||
|
if (normalized === 'active') return 'var(--success)'
|
||||||
|
if (['failed', 'error'].includes(normalized)) return '#ef4444'
|
||||||
|
if (['disabled', 'inactive'].includes(normalized)) return '#f59e0b'
|
||||||
|
return 'var(--theme-primary)'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDigitalEmployeeDetailTopBar(employee) {
|
||||||
|
if (!employee) return null
|
||||||
|
|
||||||
|
const meta = employee.digitalEmployee || {}
|
||||||
|
const statusValue = normalizeText(employee.statusValue).toLowerCase()
|
||||||
|
|
||||||
|
return {
|
||||||
|
view: {
|
||||||
|
title: normalizeText(employee.name) || '数字员工详情',
|
||||||
|
desc:
|
||||||
|
normalizeText(meta.description) ||
|
||||||
|
normalizeText(employee.summary) ||
|
||||||
|
'查看数字员工配置、执行计划、运行记录与源文件。'
|
||||||
|
},
|
||||||
|
kpis: [
|
||||||
|
{
|
||||||
|
label: '运行状态',
|
||||||
|
value: normalizeText(employee.status) || (statusValue === 'active' ? '运行中' : '未运行'),
|
||||||
|
unit: '',
|
||||||
|
meta:
|
||||||
|
normalizeText(meta.scheduleLabel) ||
|
||||||
|
normalizeText(employee.scope) ||
|
||||||
|
normalizeText(employee.executionMode) ||
|
||||||
|
'待配置执行计划',
|
||||||
|
trend: statusValue === 'active' ? 'up' : 'down',
|
||||||
|
color: resolveDigitalEmployeeStatusColor(statusValue)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildEmployeeListItem(asset) {
|
||||||
|
const meta = buildDigitalEmployeeListMeta(asset)
|
||||||
|
const statusMeta = resolveStatusMeta(asset.status)
|
||||||
|
const displayName = meta.name || '数字员工技能'
|
||||||
|
const executionModeValue = meta.executionMode === '定时执行' ? 'timed' : 'manual'
|
||||||
|
const searchText = [
|
||||||
|
displayName,
|
||||||
|
meta.code,
|
||||||
|
meta.summary,
|
||||||
|
meta.owner,
|
||||||
|
meta.scope,
|
||||||
|
meta.executionMode,
|
||||||
|
meta.skillCategory,
|
||||||
|
statusMeta.label,
|
||||||
|
meta.enabledLabel
|
||||||
|
]
|
||||||
|
.map((value) => normalizeText(value).toLowerCase())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: asset.id,
|
||||||
|
rawCode: asset.code,
|
||||||
|
short: displayName.slice(0, 2),
|
||||||
|
badgeTone: 'blue',
|
||||||
|
name: displayName,
|
||||||
|
code: meta.code,
|
||||||
|
summary: meta.summary,
|
||||||
|
owner: meta.owner,
|
||||||
|
scope: meta.scope,
|
||||||
|
executionMode: meta.executionMode,
|
||||||
|
executionModeValue,
|
||||||
|
skillCategory: meta.skillCategory,
|
||||||
|
version: asset.working_version || asset.current_version || '-',
|
||||||
|
currentVersion: asset.current_version || '-',
|
||||||
|
status: statusMeta.label,
|
||||||
|
statusValue: asset.status,
|
||||||
|
statusTone: statusMeta.tone,
|
||||||
|
enabledLabel: meta.enabledLabel,
|
||||||
|
enabledTone: meta.enabledTone,
|
||||||
|
isEnabledValue: meta.enabled,
|
||||||
|
configJson: asset.config_json || {},
|
||||||
|
updatedAt: formatDateTime(asset.updated_at),
|
||||||
|
updatedAtRaw: asset.updated_at || '',
|
||||||
|
digitalEmployee: meta,
|
||||||
|
searchText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildEmployeePlaceholder(employee) {
|
||||||
|
return {
|
||||||
|
...employee,
|
||||||
|
type: 'digitalEmployees',
|
||||||
|
typeLabel: '数字员工',
|
||||||
|
currentVersion: employee.currentVersion || employee.version || '-',
|
||||||
|
workingVersion: employee.version || '-',
|
||||||
|
markdownContent: '',
|
||||||
|
loading: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildEmployeeDetail(asset) {
|
||||||
|
const meta = buildDigitalEmployeeDetailMeta({
|
||||||
|
...asset,
|
||||||
|
updated_at: formatDateTime(asset.updated_at)
|
||||||
|
})
|
||||||
|
const statusMeta = resolveStatusMeta(asset.status)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: asset.id,
|
||||||
|
type: 'digitalEmployees',
|
||||||
|
typeLabel: '数字员工',
|
||||||
|
rawCode: asset.code,
|
||||||
|
short: meta.name.slice(0, 2),
|
||||||
|
name: meta.name,
|
||||||
|
code: meta.code,
|
||||||
|
summary: meta.description,
|
||||||
|
owner: meta.owner,
|
||||||
|
reviewer: meta.reviewer,
|
||||||
|
category: meta.category,
|
||||||
|
scope: meta.scope,
|
||||||
|
version: asset.working_version || asset.current_version || '-',
|
||||||
|
currentVersion: asset.current_version || '-',
|
||||||
|
workingVersion: asset.working_version || asset.current_version || '-',
|
||||||
|
status: statusMeta.label,
|
||||||
|
statusValue: asset.status,
|
||||||
|
statusTone: statusMeta.tone,
|
||||||
|
configJson: asset.config_json || {},
|
||||||
|
updatedAt: formatDateTime(asset.updated_at),
|
||||||
|
markdownContent: meta.sourceMarkdown,
|
||||||
|
digitalEmployee: meta,
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortEmployees(items) {
|
||||||
|
return [...items].sort((left, right) =>
|
||||||
|
String(right.updatedAtRaw || '').localeCompare(String(left.updatedAtRaw || ''))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterDigitalEmployees(items = [], filters = {}) {
|
||||||
|
const searchText = normalizeText(filters.keyword).toLowerCase()
|
||||||
|
const hasKeyword = Boolean(searchText)
|
||||||
|
const hasStatus = Boolean(filters.selectedStatus)
|
||||||
|
const hasEnabled = Boolean(filters.selectedEnabledState)
|
||||||
|
const hasExecutionMode = Boolean(filters.selectedExecutionMode)
|
||||||
|
|
||||||
|
return items.filter((item) => {
|
||||||
|
if (hasKeyword && !normalizeText(item.searchText).includes(searchText)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (hasStatus && item.statusValue !== filters.selectedStatus) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (hasEnabled && (filters.selectedEnabledState === 'enabled') !== Boolean(item.isEnabledValue)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (hasExecutionMode && item.executionModeValue !== filters.selectedExecutionMode) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
214
web/src/views/scripts/useAuditAssetData.js
Normal file
214
web/src/views/scripts/useAuditAssetData.js
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
fetchAgentAssetDetail,
|
||||||
|
fetchAgentAssets,
|
||||||
|
fetchAgentRuns
|
||||||
|
} from '../../services/agentAssets.js'
|
||||||
|
import {
|
||||||
|
buildDetailViewModel,
|
||||||
|
buildListItem
|
||||||
|
} from './auditViewModel.js'
|
||||||
|
|
||||||
|
export function useAuditAssetData({
|
||||||
|
activeType,
|
||||||
|
activeMeta,
|
||||||
|
selectedSkill,
|
||||||
|
loadVersionTimeline,
|
||||||
|
loadSpreadsheetChangeRecords,
|
||||||
|
loadRiskRuleJson,
|
||||||
|
toast
|
||||||
|
}) {
|
||||||
|
const loading = ref(false)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const detailLoading = ref(false)
|
||||||
|
const detailError = ref('')
|
||||||
|
const runLoading = ref(false)
|
||||||
|
const runs = ref([])
|
||||||
|
const assetBuckets = ref({
|
||||||
|
financialRules: [],
|
||||||
|
riskRules: [],
|
||||||
|
mcp: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentAssets = computed(() => assetBuckets.value[activeType.value] || [])
|
||||||
|
|
||||||
|
async function loadRuns(options = {}) {
|
||||||
|
if (runLoading.value && !options.force) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runLoading.value = true
|
||||||
|
try {
|
||||||
|
const payload = await fetchAgentRuns({ limit: 50 })
|
||||||
|
runs.value = Array.isArray(payload) ? payload : []
|
||||||
|
} finally {
|
||||||
|
runLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAssets(options = {}) {
|
||||||
|
const shouldShowLoading = !options.silent && !options.background
|
||||||
|
if (shouldShowLoading) {
|
||||||
|
loading.value = true
|
||||||
|
}
|
||||||
|
if (!options.silent) {
|
||||||
|
errorMessage.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await fetchAgentAssets({ assetType: activeMeta.value.assetType })
|
||||||
|
const items = Array.isArray(payload) ? payload.map(buildListItem).filter(Boolean) : []
|
||||||
|
|
||||||
|
if (activeMeta.value.assetType === 'rule') {
|
||||||
|
const nextBuckets = {
|
||||||
|
financialRules: [],
|
||||||
|
riskRules: []
|
||||||
|
}
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
if (item?.tabId === 'financialRules' || item?.tabId === 'riskRules') {
|
||||||
|
nextBuckets[item.tabId].push(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
assetBuckets.value = {
|
||||||
|
...assetBuckets.value,
|
||||||
|
...nextBuckets
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assetBuckets.value = {
|
||||||
|
...assetBuckets.value,
|
||||||
|
[activeType.value]: items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (options.silent || options.background) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (activeMeta.value.assetType === 'rule') {
|
||||||
|
assetBuckets.value = {
|
||||||
|
...assetBuckets.value,
|
||||||
|
financialRules:
|
||||||
|
activeType.value === 'financialRules' ? [] : assetBuckets.value.financialRules,
|
||||||
|
riskRules: []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assetBuckets.value = {
|
||||||
|
...assetBuckets.value,
|
||||||
|
[activeType.value]: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errorMessage.value = error?.message || '资产数据加载失败,请稍后重试。'
|
||||||
|
toast(errorMessage.value)
|
||||||
|
} finally {
|
||||||
|
if (shouldShowLoading) {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshCurrentAssets() {
|
||||||
|
await loadAssets({ force: true, silent: true, background: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSelectedAssetDetail(assetId) {
|
||||||
|
detailLoading.value = true
|
||||||
|
detailError.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!runs.value.length) {
|
||||||
|
await loadRuns()
|
||||||
|
}
|
||||||
|
const detail = await fetchAgentAssetDetail(assetId)
|
||||||
|
selectedSkill.value = buildDetailViewModel(detail, runs.value)
|
||||||
|
if (selectedSkill.value?.type !== 'rules') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedSkill.value.usesSpreadsheetRule && !selectedSkill.value.usesJsonRiskRule) {
|
||||||
|
loadVersionTimeline(assetId, { silent: true }).catch(() => {})
|
||||||
|
}
|
||||||
|
if (selectedSkill.value.usesSpreadsheetRule) {
|
||||||
|
loadSpreadsheetChangeRecords(assetId).catch(() => {})
|
||||||
|
}
|
||||||
|
if (!selectedSkill.value.usesJsonRiskRule) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (selectedSkill.value.riskRuleGenerationFailed || selectedSkill.value.riskRuleGenerationBusy) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadRiskRuleJson(assetId)
|
||||||
|
} catch (jsonError) {
|
||||||
|
console.warn('Failed to load risk rule JSON:', jsonError)
|
||||||
|
const jsonMessage =
|
||||||
|
jsonError?.message || '风险规则 JSON 文件缺失或无法读取,请同步规则库后重试。'
|
||||||
|
toast(jsonMessage)
|
||||||
|
selectedSkill.value = {
|
||||||
|
...selectedSkill.value,
|
||||||
|
riskRuleJsonText: '{}',
|
||||||
|
riskRuleDescription:
|
||||||
|
selectedSkill.value.riskRuleDescription ||
|
||||||
|
'规则 JSON 尚未就绪,请联系管理员执行平台风险规则同步。'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
detailError.value = error?.message || '资产详情加载失败,请稍后重试。'
|
||||||
|
toast(detailError.value)
|
||||||
|
} finally {
|
||||||
|
detailLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeSelectedRuleLifecycle(detail) {
|
||||||
|
if (!selectedSkill.value || !detail) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = buildDetailViewModel(detail, runs.value)
|
||||||
|
selectedSkill.value = {
|
||||||
|
...selectedSkill.value,
|
||||||
|
status: next.status,
|
||||||
|
statusValue: next.statusValue,
|
||||||
|
statusTone: next.statusTone,
|
||||||
|
publishedVersion: next.publishedVersion,
|
||||||
|
workingVersion: next.workingVersion,
|
||||||
|
currentVersion: next.currentVersion,
|
||||||
|
displayVersion: next.displayVersion,
|
||||||
|
reviewer: next.reviewer,
|
||||||
|
publisher: next.publisher,
|
||||||
|
publishedAt: next.publishedAt,
|
||||||
|
isOnlineValue: next.isOnlineValue,
|
||||||
|
isOnlineLabel: next.isOnlineLabel,
|
||||||
|
isOnlineTone: next.isOnlineTone,
|
||||||
|
isEnabledValue: next.isEnabledValue,
|
||||||
|
isEnabledLabel: next.isEnabledLabel,
|
||||||
|
isEnabledTone: next.isEnabledTone,
|
||||||
|
latestTestSummary: next.latestTestSummary,
|
||||||
|
lastOperationLabel: next.lastOperationLabel,
|
||||||
|
lastOperationTone: next.lastOperationTone,
|
||||||
|
publishMeta: next.publishMeta,
|
||||||
|
publishState: next.publishState,
|
||||||
|
updatedAt: next.updatedAt,
|
||||||
|
configJson: next.configJson
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
errorMessage,
|
||||||
|
detailLoading,
|
||||||
|
detailError,
|
||||||
|
runLoading,
|
||||||
|
runs,
|
||||||
|
assetBuckets,
|
||||||
|
currentAssets,
|
||||||
|
loadRuns,
|
||||||
|
loadAssets,
|
||||||
|
refreshCurrentAssets,
|
||||||
|
loadSelectedAssetDetail,
|
||||||
|
mergeSelectedRuleLifecycle
|
||||||
|
}
|
||||||
|
}
|
||||||
226
web/src/views/scripts/useAuditRiskRuleActions.js
Normal file
226
web/src/views/scripts/useAuditRiskRuleActions.js
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
deleteAgentAsset,
|
||||||
|
fetchAgentAssetDetail,
|
||||||
|
publishRiskRuleAsset,
|
||||||
|
returnRiskRuleAsset,
|
||||||
|
setRiskRuleAssetEnabled
|
||||||
|
} from '../../services/agentAssets.js'
|
||||||
|
import { normalizeText } from './auditViewModel.js'
|
||||||
|
|
||||||
|
export function useAuditRiskRuleActions({
|
||||||
|
selectedSkill,
|
||||||
|
detailBusy,
|
||||||
|
actionState,
|
||||||
|
canOpenRiskRuleTest,
|
||||||
|
canDeleteRiskRule,
|
||||||
|
canReturnRiskRule,
|
||||||
|
canPublishRiskRule,
|
||||||
|
canToggleRiskRuleEnabled,
|
||||||
|
riskRuleTestPassed,
|
||||||
|
refreshCurrentAssets,
|
||||||
|
loadSelectedAssetDetail,
|
||||||
|
mergeSelectedRuleLifecycle,
|
||||||
|
closeDetail,
|
||||||
|
resolveActor,
|
||||||
|
toast
|
||||||
|
}) {
|
||||||
|
const riskRuleTestOpen = ref(false)
|
||||||
|
const riskRuleDeleteOpen = ref(false)
|
||||||
|
const riskRuleReturnOpen = ref(false)
|
||||||
|
const riskRulePublishOpen = ref(false)
|
||||||
|
const riskRuleReturnNote = ref('')
|
||||||
|
|
||||||
|
function resetRiskRuleActionDialogs() {
|
||||||
|
riskRuleTestOpen.value = false
|
||||||
|
riskRuleDeleteOpen.value = false
|
||||||
|
riskRuleReturnOpen.value = false
|
||||||
|
riskRulePublishOpen.value = false
|
||||||
|
riskRuleReturnNote.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRiskRuleTestDialog() {
|
||||||
|
if (detailBusy.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!canOpenRiskRuleTest.value) {
|
||||||
|
if (!selectedSkill.value?.id) {
|
||||||
|
toast('规则详情还没有加载完成,请稍后再测试。')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
riskRuleTestOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeRiskRuleTestDialog() {
|
||||||
|
riskRuleTestOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRiskRuleReportSaved(summary) {
|
||||||
|
if (selectedSkill.value) {
|
||||||
|
selectedSkill.value.latestTestSummary = summary
|
||||||
|
}
|
||||||
|
await refreshCurrentAssets()
|
||||||
|
if (selectedSkill.value?.id) {
|
||||||
|
const detail = await fetchAgentAssetDetail(selectedSkill.value.id)
|
||||||
|
mergeSelectedRuleLifecycle(detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDeleteRiskRuleDialog() {
|
||||||
|
if (!canDeleteRiskRule.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
riskRuleDeleteOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDeleteRiskRuleDialog() {
|
||||||
|
if (detailBusy.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
riskRuleDeleteOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSelectedRiskRule() {
|
||||||
|
if (!selectedSkill.value || !canDeleteRiskRule.value || detailBusy.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actionState.value = 'delete-risk-rule'
|
||||||
|
try {
|
||||||
|
await deleteAgentAsset(selectedSkill.value.id, { actor: resolveActor() })
|
||||||
|
riskRuleDeleteOpen.value = false
|
||||||
|
const deletedName = selectedSkill.value.name
|
||||||
|
closeDetail()
|
||||||
|
await refreshCurrentAssets()
|
||||||
|
toast(`风险规则“${deletedName}”已删除。`)
|
||||||
|
} catch (error) {
|
||||||
|
toast(error?.message || '风险规则删除失败,请稍后重试。')
|
||||||
|
} finally {
|
||||||
|
actionState.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openReturnRiskRuleDialog() {
|
||||||
|
if (!canReturnRiskRule.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
riskRuleReturnNote.value = ''
|
||||||
|
riskRuleReturnOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeReturnRiskRuleDialog() {
|
||||||
|
if (detailBusy.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
riskRuleReturnOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function returnSelectedRiskRule() {
|
||||||
|
if (!selectedSkill.value || !canReturnRiskRule.value || detailBusy.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const note = normalizeText(riskRuleReturnNote.value)
|
||||||
|
if (!note) {
|
||||||
|
toast('请填写回退原因。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actionState.value = 'return-risk-rule'
|
||||||
|
try {
|
||||||
|
await returnRiskRuleAsset(selectedSkill.value.id, { note }, { actor: resolveActor() })
|
||||||
|
riskRuleReturnOpen.value = false
|
||||||
|
await refreshCurrentAssets()
|
||||||
|
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||||
|
toast('风险规则已回退到草稿。')
|
||||||
|
} catch (error) {
|
||||||
|
toast(error?.message || '风险规则回退失败,请稍后重试。')
|
||||||
|
} finally {
|
||||||
|
actionState.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPublishRiskRuleDialog() {
|
||||||
|
if (!canPublishRiskRule.value) {
|
||||||
|
if (!riskRuleTestPassed.value) {
|
||||||
|
toast('请先确认测试报告通过,再发布上线。')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
riskRulePublishOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePublishRiskRuleDialog() {
|
||||||
|
if (detailBusy.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
riskRulePublishOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function publishSelectedRiskRule() {
|
||||||
|
if (!selectedSkill.value || !canPublishRiskRule.value || detailBusy.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actionState.value = 'publish-risk-rule'
|
||||||
|
try {
|
||||||
|
await publishRiskRuleAsset(selectedSkill.value.id, { actor: resolveActor() })
|
||||||
|
riskRulePublishOpen.value = false
|
||||||
|
await refreshCurrentAssets()
|
||||||
|
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||||
|
toast('风险规则已发布上线。')
|
||||||
|
} catch (error) {
|
||||||
|
toast(error?.message || '风险规则发布失败,请稍后重试。')
|
||||||
|
} finally {
|
||||||
|
actionState.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleSelectedRiskRuleEnabled() {
|
||||||
|
if (!selectedSkill.value || !canToggleRiskRuleEnabled.value || detailBusy.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetId = selectedSkill.value.id
|
||||||
|
const nextEnabled = !selectedSkill.value.isOnlineValue
|
||||||
|
actionState.value = 'toggle-risk-rule-enabled'
|
||||||
|
try {
|
||||||
|
const detail = await setRiskRuleAssetEnabled(assetId, nextEnabled, { actor: resolveActor() })
|
||||||
|
mergeSelectedRuleLifecycle(detail)
|
||||||
|
await refreshCurrentAssets()
|
||||||
|
toast(
|
||||||
|
nextEnabled
|
||||||
|
? '风险规则已上线。'
|
||||||
|
: '风险规则已下线,不会进入业务扫描。'
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
toast(error?.message || '风险规则上线状态更新失败,请稍后重试。')
|
||||||
|
} finally {
|
||||||
|
actionState.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
riskRuleTestOpen,
|
||||||
|
riskRuleDeleteOpen,
|
||||||
|
riskRuleReturnOpen,
|
||||||
|
riskRulePublishOpen,
|
||||||
|
riskRuleReturnNote,
|
||||||
|
resetRiskRuleActionDialogs,
|
||||||
|
openRiskRuleTestDialog,
|
||||||
|
closeRiskRuleTestDialog,
|
||||||
|
handleRiskRuleReportSaved,
|
||||||
|
openDeleteRiskRuleDialog,
|
||||||
|
closeDeleteRiskRuleDialog,
|
||||||
|
deleteSelectedRiskRule,
|
||||||
|
openReturnRiskRuleDialog,
|
||||||
|
closeReturnRiskRuleDialog,
|
||||||
|
returnSelectedRiskRule,
|
||||||
|
openPublishRiskRuleDialog,
|
||||||
|
closePublishRiskRuleDialog,
|
||||||
|
publishSelectedRiskRule,
|
||||||
|
toggleSelectedRiskRuleEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
133
web/src/views/scripts/useAuditRiskRuleCreateFlow.js
Normal file
133
web/src/views/scripts/useAuditRiskRuleCreateFlow.js
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import { generateRiskRuleAsset } from '../../services/agentAssets.js'
|
||||||
|
import { normalizeText } from './auditViewModel.js'
|
||||||
|
import {
|
||||||
|
createDefaultRiskRuleForm
|
||||||
|
} from './auditViewRiskRuleModel.js'
|
||||||
|
|
||||||
|
export function useAuditRiskRuleCreateFlow({
|
||||||
|
activeType,
|
||||||
|
isRuleManager,
|
||||||
|
detailBusy,
|
||||||
|
actionState,
|
||||||
|
assetBuckets,
|
||||||
|
refreshCurrentAssets,
|
||||||
|
resolveActor,
|
||||||
|
toast
|
||||||
|
}) {
|
||||||
|
const riskRuleCreateOpen = ref(false)
|
||||||
|
const riskRuleCreateForm = ref(createDefaultRiskRuleForm())
|
||||||
|
const riskRuleGenerationPollTimers = new Map()
|
||||||
|
|
||||||
|
const riskRuleCreateBusy = computed(() => actionState.value === 'generate-risk-rule')
|
||||||
|
const canCreateRiskRule = computed(
|
||||||
|
() => activeType.value === 'riskRules' && isRuleManager.value && !detailBusy.value
|
||||||
|
)
|
||||||
|
|
||||||
|
function openRiskRuleCreateDialog() {
|
||||||
|
if (activeType.value !== 'riskRules') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
riskRuleCreateForm.value = createDefaultRiskRuleForm()
|
||||||
|
riskRuleCreateOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeRiskRuleCreateDialog() {
|
||||||
|
if (riskRuleCreateBusy.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
riskRuleCreateOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitRiskRuleCreate() {
|
||||||
|
if (!canCreateRiskRule.value || riskRuleCreateBusy.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const naturalLanguage = String(riskRuleCreateForm.value.natural_language || '').trim()
|
||||||
|
const ruleTitle = String(riskRuleCreateForm.value.rule_title || '').trim()
|
||||||
|
if (ruleTitle.length < 2) {
|
||||||
|
toast('请输入至少 2 个字的规则标题。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (naturalLanguage.length < 8) {
|
||||||
|
toast('请至少输入 8 个字的风险规则描述。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actionState.value = 'generate-risk-rule'
|
||||||
|
try {
|
||||||
|
const detail = await generateRiskRuleAsset(
|
||||||
|
{
|
||||||
|
business_domain: 'expense',
|
||||||
|
business_stage: riskRuleCreateForm.value.business_stage,
|
||||||
|
expense_category: riskRuleCreateForm.value.expense_category,
|
||||||
|
rule_title: ruleTitle,
|
||||||
|
requires_attachment: Boolean(riskRuleCreateForm.value.requires_attachment),
|
||||||
|
natural_language: naturalLanguage
|
||||||
|
},
|
||||||
|
{ actor: resolveActor() }
|
||||||
|
)
|
||||||
|
riskRuleCreateOpen.value = false
|
||||||
|
await refreshCurrentAssets()
|
||||||
|
scheduleRiskRuleGenerationPoll(detail.id)
|
||||||
|
toast('风险规则已进入后台生成,列表会先显示生成中。')
|
||||||
|
} catch (error) {
|
||||||
|
toast(error?.message || '风险规则生成失败,请稍后重试。')
|
||||||
|
} finally {
|
||||||
|
actionState.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopRiskRuleGenerationPoll(assetId) {
|
||||||
|
const timer = riskRuleGenerationPollTimers.get(assetId)
|
||||||
|
if (timer) {
|
||||||
|
window.clearTimeout(timer)
|
||||||
|
riskRuleGenerationPollTimers.delete(assetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleRiskRuleGenerationPoll(assetId, attempt = 0) {
|
||||||
|
const normalizedAssetId = normalizeText(assetId)
|
||||||
|
if (!normalizedAssetId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stopRiskRuleGenerationPoll(normalizedAssetId)
|
||||||
|
const timer = window.setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await refreshCurrentAssets()
|
||||||
|
const latest = (assetBuckets.value.riskRules || []).find((item) => item.id === normalizedAssetId)
|
||||||
|
if (!latest || latest.statusValue !== 'generating' || attempt >= 59) {
|
||||||
|
riskRuleGenerationPollTimers.delete(normalizedAssetId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scheduleRiskRuleGenerationPoll(normalizedAssetId, attempt + 1)
|
||||||
|
} catch {
|
||||||
|
if (attempt < 59) {
|
||||||
|
scheduleRiskRuleGenerationPoll(normalizedAssetId, attempt + 1)
|
||||||
|
} else {
|
||||||
|
riskRuleGenerationPollTimers.delete(normalizedAssetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, attempt === 0 ? 1200 : 3000)
|
||||||
|
riskRuleGenerationPollTimers.set(normalizedAssetId, timer)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAllRiskRuleGenerationPolls() {
|
||||||
|
riskRuleGenerationPollTimers.forEach((timer) => window.clearTimeout(timer))
|
||||||
|
riskRuleGenerationPollTimers.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
canCreateRiskRule,
|
||||||
|
riskRuleCreateOpen,
|
||||||
|
riskRuleCreateForm,
|
||||||
|
riskRuleCreateBusy,
|
||||||
|
openRiskRuleCreateDialog,
|
||||||
|
closeRiskRuleCreateDialog,
|
||||||
|
submitRiskRuleCreate,
|
||||||
|
stopAllRiskRuleGenerationPolls
|
||||||
|
}
|
||||||
|
}
|
||||||
101
web/src/views/scripts/useAuditRiskRuleJsonEditor.js
Normal file
101
web/src/views/scripts/useAuditRiskRuleJsonEditor.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import {
|
||||||
|
fetchAgentAssetRuleJson,
|
||||||
|
saveAgentAssetRuleJson
|
||||||
|
} from '../../services/agentAssets.js'
|
||||||
|
import {
|
||||||
|
applyRiskRuleJsonState,
|
||||||
|
resolveRiskRuleDescription
|
||||||
|
} from './auditViewModel.js'
|
||||||
|
|
||||||
|
function readJsonPayload(payload) {
|
||||||
|
return payload?.payload && typeof payload.payload === 'object' ? payload.payload : payload
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadTextFile({ content, fileName, type }) {
|
||||||
|
const blob = new Blob([content], { type })
|
||||||
|
const objectUrl = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = objectUrl
|
||||||
|
link.download = fileName
|
||||||
|
link.click()
|
||||||
|
URL.revokeObjectURL(objectUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuditRiskRuleJsonEditor({
|
||||||
|
selectedSkill,
|
||||||
|
canEditMarkdown,
|
||||||
|
actionState,
|
||||||
|
toast
|
||||||
|
}) {
|
||||||
|
async function loadRiskRuleJson(assetId) {
|
||||||
|
if (!assetId || !selectedSkill.value?.usesJsonRiskRule) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await fetchAgentAssetRuleJson(assetId)
|
||||||
|
selectedSkill.value = applyRiskRuleJsonState(
|
||||||
|
selectedSkill.value,
|
||||||
|
readJsonPayload(payload),
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveRiskRuleJson() {
|
||||||
|
if (!selectedSkill.value?.id || !canEditMarkdown.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actionState.value = 'save-risk-json'
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(String(selectedSkill.value.riskRuleJsonText || '{}'))
|
||||||
|
const saved = await saveAgentAssetRuleJson(selectedSkill.value.id, { payload: parsed })
|
||||||
|
selectedSkill.value = applyRiskRuleJsonState(
|
||||||
|
selectedSkill.value,
|
||||||
|
readJsonPayload(saved),
|
||||||
|
saved
|
||||||
|
)
|
||||||
|
toast('风险规则 JSON 已保存。')
|
||||||
|
} catch (error) {
|
||||||
|
toast(error?.message || '风险规则 JSON 保存失败。')
|
||||||
|
} finally {
|
||||||
|
actionState.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRiskRuleJson() {
|
||||||
|
if (!selectedSkill.value?.usesJsonRiskRule) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(String(selectedSkill.value.riskRuleJsonText || '{}'))
|
||||||
|
selectedSkill.value = applyRiskRuleJsonState(selectedSkill.value, parsed, {
|
||||||
|
name: selectedSkill.value.name,
|
||||||
|
description: resolveRiskRuleDescription(parsed)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
toast(error?.message || 'JSON 格式无效,无法格式化。')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadRiskRuleJson() {
|
||||||
|
if (!selectedSkill.value?.usesJsonRiskRule) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadTextFile({
|
||||||
|
content: String(selectedSkill.value.riskRuleJsonText || '{}'),
|
||||||
|
fileName:
|
||||||
|
selectedSkill.value.ruleDocument?.file_name ||
|
||||||
|
`${selectedSkill.value.code || 'risk-rule'}.json`,
|
||||||
|
type: 'application/json;charset=utf-8'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadRiskRuleJson,
|
||||||
|
saveRiskRuleJson,
|
||||||
|
formatRiskRuleJson,
|
||||||
|
downloadRiskRuleJson
|
||||||
|
}
|
||||||
|
}
|
||||||
187
web/src/views/scripts/useAuditRuleReviewFlow.js
Normal file
187
web/src/views/scripts/useAuditRuleReviewFlow.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import { fetchEmployees } from '../../services/employees.js'
|
||||||
|
import { createAgentAssetReview } from '../../services/agentAssets.js'
|
||||||
|
import {
|
||||||
|
buildReviewNote
|
||||||
|
} from './auditViewRuntimeModel.js'
|
||||||
|
import {
|
||||||
|
normalizeText,
|
||||||
|
resolveReviewMeta
|
||||||
|
} from './auditViewModel.js'
|
||||||
|
|
||||||
|
export function useAuditRuleReviewFlow({
|
||||||
|
selectedSkill,
|
||||||
|
selectedSkillIsRule,
|
||||||
|
selectedSkillUsesJsonRisk,
|
||||||
|
canEditSelected,
|
||||||
|
canManageSelected,
|
||||||
|
isDisplayingWorkingVersion,
|
||||||
|
canOpenRiskRuleReviewSubmit,
|
||||||
|
riskRuleTestPassed,
|
||||||
|
detailBusy,
|
||||||
|
actionState,
|
||||||
|
refreshCurrentAssets,
|
||||||
|
loadSelectedAssetDetail,
|
||||||
|
resolveActor,
|
||||||
|
toast
|
||||||
|
}) {
|
||||||
|
const reviewSubmitOpen = ref(false)
|
||||||
|
const reviewSubmitVersion = ref('')
|
||||||
|
const reviewSubmitReviewer = ref('')
|
||||||
|
const reviewSubmitReviewerLoading = ref(false)
|
||||||
|
const reviewSubmitReviewerOptions = ref([])
|
||||||
|
|
||||||
|
const canSubmitReview = computed(
|
||||||
|
() =>
|
||||||
|
!selectedSkillUsesJsonRisk.value &&
|
||||||
|
canEditSelected.value &&
|
||||||
|
selectedSkillIsRule.value &&
|
||||||
|
isDisplayingWorkingVersion.value
|
||||||
|
)
|
||||||
|
const hasReviewSubmitReviewers = computed(() => reviewSubmitReviewerOptions.value.length > 0)
|
||||||
|
const canReviewSelected = computed(
|
||||||
|
() => canManageSelected.value && selectedSkillIsRule.value && isDisplayingWorkingVersion.value
|
||||||
|
)
|
||||||
|
|
||||||
|
async function reviewSelectedRule(reviewStatus) {
|
||||||
|
if (!selectedSkill.value || !selectedSkillIsRule.value || detailBusy.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (reviewStatus === 'pending' && !canSubmitReview.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (reviewStatus !== 'pending' && !canReviewSelected.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actionState.value = `review-${reviewStatus}`
|
||||||
|
try {
|
||||||
|
await createAgentAssetReview(
|
||||||
|
selectedSkill.value.id,
|
||||||
|
{
|
||||||
|
version: selectedSkill.value.workingVersion,
|
||||||
|
reviewer: resolveActor(),
|
||||||
|
review_status: reviewStatus,
|
||||||
|
review_note: buildReviewNote(reviewStatus)
|
||||||
|
},
|
||||||
|
{ actor: resolveActor() }
|
||||||
|
)
|
||||||
|
await refreshCurrentAssets()
|
||||||
|
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||||
|
toast(`当前规则版本已标记为${resolveReviewMeta(reviewStatus).label}。`)
|
||||||
|
} catch (error) {
|
||||||
|
toast(error?.message || '规则审核提交失败,请稍后重试。')
|
||||||
|
} finally {
|
||||||
|
actionState.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadReviewSubmitReviewers() {
|
||||||
|
reviewSubmitReviewerLoading.value = true
|
||||||
|
try {
|
||||||
|
const employees = await fetchEmployees()
|
||||||
|
reviewSubmitReviewerOptions.value = (Array.isArray(employees) ? employees : [])
|
||||||
|
.filter(
|
||||||
|
(item) =>
|
||||||
|
item.status === '在职' &&
|
||||||
|
Array.isArray(item.roleCodes) &&
|
||||||
|
item.roleCodes.includes('manager')
|
||||||
|
)
|
||||||
|
.map((item) => ({
|
||||||
|
value: item.name,
|
||||||
|
label: `${item.name} · ${item.position || '高级管理员'}`
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
reviewSubmitReviewerOptions.value = []
|
||||||
|
toast(error?.message || '审核人列表加载失败,请稍后重试。')
|
||||||
|
} finally {
|
||||||
|
reviewSubmitReviewerLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openSubmitReviewDialog() {
|
||||||
|
if (
|
||||||
|
selectedSkillUsesJsonRisk.value &&
|
||||||
|
!canOpenRiskRuleReviewSubmit.value
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!selectedSkill.value || !canSubmitReview.value || detailBusy.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reviewSubmitVersion.value = selectedSkill.value.workingVersion || selectedSkill.value.displayVersion || ''
|
||||||
|
reviewSubmitReviewer.value = selectedSkill.value.reviewer || ''
|
||||||
|
reviewSubmitOpen.value = true
|
||||||
|
await loadReviewSubmitReviewers()
|
||||||
|
if (!reviewSubmitReviewerOptions.value.some((item) => item.value === reviewSubmitReviewer.value)) {
|
||||||
|
reviewSubmitReviewer.value = reviewSubmitReviewerOptions.value[0]?.value || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSubmitReviewDialog() {
|
||||||
|
if (detailBusy.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reviewSubmitOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitSelectedRuleForReview() {
|
||||||
|
if (!selectedSkill.value || !canSubmitReview.value || detailBusy.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (selectedSkillUsesJsonRisk.value && !riskRuleTestPassed.value) {
|
||||||
|
toast('当前规则版本尚未确认测试通过,不能提交审核。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = normalizeText(reviewSubmitVersion.value)
|
||||||
|
const reviewer = normalizeText(reviewSubmitReviewer.value)
|
||||||
|
if (!version) {
|
||||||
|
toast('请输入送审版本号。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!reviewer) {
|
||||||
|
toast('请选择审核人。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actionState.value = 'review-pending'
|
||||||
|
try {
|
||||||
|
await createAgentAssetReview(
|
||||||
|
selectedSkill.value.id,
|
||||||
|
{
|
||||||
|
version,
|
||||||
|
reviewer,
|
||||||
|
review_status: 'pending',
|
||||||
|
review_note: buildReviewNote('pending')
|
||||||
|
},
|
||||||
|
{ actor: resolveActor() }
|
||||||
|
)
|
||||||
|
reviewSubmitOpen.value = false
|
||||||
|
await refreshCurrentAssets()
|
||||||
|
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||||
|
toast(`规则版本 ${version} 已提交给 ${reviewer} 审核。`)
|
||||||
|
} catch (error) {
|
||||||
|
toast(error?.message || '规则审核提交失败,请稍后重试。')
|
||||||
|
} finally {
|
||||||
|
actionState.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
reviewSubmitOpen,
|
||||||
|
reviewSubmitVersion,
|
||||||
|
reviewSubmitReviewer,
|
||||||
|
reviewSubmitReviewerLoading,
|
||||||
|
reviewSubmitReviewerOptions,
|
||||||
|
canSubmitReview,
|
||||||
|
hasReviewSubmitReviewers,
|
||||||
|
canReviewSelected,
|
||||||
|
reviewSelectedRule,
|
||||||
|
openSubmitReviewDialog,
|
||||||
|
closeSubmitReviewDialog,
|
||||||
|
submitSelectedRuleForReview
|
||||||
|
}
|
||||||
|
}
|
||||||
151
web/src/views/scripts/useAuditRuleVersionActions.js
Normal file
151
web/src/views/scripts/useAuditRuleVersionActions.js
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import {
|
||||||
|
activateAgentAsset,
|
||||||
|
createAgentAssetVersion,
|
||||||
|
restoreAgentAssetVersion,
|
||||||
|
updateAgentAsset
|
||||||
|
} from '../../services/agentAssets.js'
|
||||||
|
import {
|
||||||
|
buildRuleConfigPayload,
|
||||||
|
incrementVersion
|
||||||
|
} from './auditViewRuntimeModel.js'
|
||||||
|
import {
|
||||||
|
buildMarkdownVersionContent,
|
||||||
|
normalizeText,
|
||||||
|
parseRuntimeRuleText
|
||||||
|
} from './auditViewModel.js'
|
||||||
|
|
||||||
|
export function useAuditRuleVersionActions({
|
||||||
|
selectedSkill,
|
||||||
|
selectedSkillIsRule,
|
||||||
|
canEditMarkdown,
|
||||||
|
canManageSelected,
|
||||||
|
actionState,
|
||||||
|
detailBusy,
|
||||||
|
refreshCurrentAssets,
|
||||||
|
loadSelectedAssetDetail,
|
||||||
|
resolveActor,
|
||||||
|
toast
|
||||||
|
}) {
|
||||||
|
async function persistRuleRuntimeConfig(asset, runtimeRule) {
|
||||||
|
await updateAgentAsset(
|
||||||
|
asset.id,
|
||||||
|
{
|
||||||
|
config_json: buildRuleConfigPayload(asset, runtimeRule)
|
||||||
|
},
|
||||||
|
{ actor: resolveActor() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveRuleVersion({ action, changeNote, successLabel }) {
|
||||||
|
if (
|
||||||
|
!selectedSkill.value ||
|
||||||
|
!selectedSkillIsRule.value ||
|
||||||
|
selectedSkill.value.usesSpreadsheetRule ||
|
||||||
|
!canEditMarkdown.value ||
|
||||||
|
detailBusy.value
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!normalizeText(selectedSkill.value.markdownContent)) {
|
||||||
|
toast('规则 Markdown 内容不能为空。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtimeRule = parseRuntimeRuleText(selectedSkill.value.runtimeRuleText)
|
||||||
|
if (!runtimeRule) {
|
||||||
|
toast('运行时 JSON 必须是合法的对象。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextVersion = incrementVersion(selectedSkill.value.currentVersion)
|
||||||
|
actionState.value = action
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createAgentAssetVersion(
|
||||||
|
selectedSkill.value.id,
|
||||||
|
{
|
||||||
|
version: nextVersion,
|
||||||
|
content: buildMarkdownVersionContent(selectedSkill.value.markdownContent, runtimeRule),
|
||||||
|
content_type: 'markdown',
|
||||||
|
change_note: changeNote,
|
||||||
|
created_by: resolveActor()
|
||||||
|
},
|
||||||
|
{ actor: resolveActor() }
|
||||||
|
)
|
||||||
|
await persistRuleRuntimeConfig(selectedSkill.value, runtimeRule)
|
||||||
|
await refreshCurrentAssets()
|
||||||
|
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||||
|
toast(`${successLabel} ${nextVersion}。`)
|
||||||
|
} catch (error) {
|
||||||
|
toast(error?.message || `${successLabel}失败,请稍后重试。`)
|
||||||
|
} finally {
|
||||||
|
actionState.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveRuleMarkdown() {
|
||||||
|
await saveRuleVersion({
|
||||||
|
action: 'save-markdown',
|
||||||
|
changeNote: '通过规则中心保存 Markdown 规则内容,并同步运行时 JSON。',
|
||||||
|
successLabel: '规则 Markdown 已保存为'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveRuleRuntimeJson() {
|
||||||
|
await saveRuleVersion({
|
||||||
|
action: 'save-runtime-json',
|
||||||
|
changeNote: '通过规则中心保存运行时 JSON 配置。',
|
||||||
|
successLabel: '规则 JSON 已保存为'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function activateSelectedRule() {
|
||||||
|
if (!selectedSkill.value || !selectedSkillIsRule.value || !canManageSelected.value || detailBusy.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actionState.value = 'activate'
|
||||||
|
try {
|
||||||
|
await activateAgentAsset(selectedSkill.value.id, { actor: resolveActor() })
|
||||||
|
await refreshCurrentAssets()
|
||||||
|
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||||
|
toast('规则已正式上线。')
|
||||||
|
} catch (error) {
|
||||||
|
toast(error?.message || '规则上线失败,请稍后重试。')
|
||||||
|
} finally {
|
||||||
|
actionState.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreSelectedVersion(version) {
|
||||||
|
if (
|
||||||
|
!selectedSkill.value ||
|
||||||
|
!selectedSkillIsRule.value ||
|
||||||
|
!canManageSelected.value ||
|
||||||
|
detailBusy.value ||
|
||||||
|
!version
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actionState.value = `restore-${version}`
|
||||||
|
try {
|
||||||
|
await restoreAgentAssetVersion(selectedSkill.value.id, version, { actor: resolveActor() })
|
||||||
|
await refreshCurrentAssets()
|
||||||
|
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||||
|
toast(`已基于 ${version} 生成新的工作版本。`)
|
||||||
|
} catch (error) {
|
||||||
|
toast(error?.message || '历史版本恢复失败,请稍后重试。')
|
||||||
|
} finally {
|
||||||
|
actionState.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
saveRuleMarkdown,
|
||||||
|
saveRuleRuntimeJson,
|
||||||
|
activateSelectedRule,
|
||||||
|
restoreSelectedVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
423
web/src/views/scripts/useAuditSpreadsheetEditor.js
Normal file
423
web/src/views/scripts/useAuditSpreadsheetEditor.js
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
import { computed, nextTick, ref } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
fetchAgentAssetSpreadsheetBlob,
|
||||||
|
fetchAgentAssetSpreadsheetChangeRecords,
|
||||||
|
fetchAgentAssetSpreadsheetOnlyOfficeConfig,
|
||||||
|
importAgentAssetSpreadsheetContent
|
||||||
|
} from '../../services/agentAssets.js'
|
||||||
|
import { loadOnlyOfficeApi } from '../../services/onlyoffice.js'
|
||||||
|
import { buildOnlyOfficeEditorConfig } from './onlyOfficePreviewConfig.js'
|
||||||
|
import { buildSpreadsheetChangeRecordKey } from './auditViewRuntimeModel.js'
|
||||||
|
import {
|
||||||
|
formatDateTime,
|
||||||
|
formatSpreadsheetChangeSummary,
|
||||||
|
normalizeText,
|
||||||
|
resolveDiffChangeMeta
|
||||||
|
} from './auditViewModel.js'
|
||||||
|
|
||||||
|
export function useAuditSpreadsheetEditor({
|
||||||
|
selectedSkill,
|
||||||
|
selectedSkillUsesSpreadsheet,
|
||||||
|
canEditSpreadsheetInline,
|
||||||
|
canUploadSpreadsheet,
|
||||||
|
canDownloadSpreadsheet,
|
||||||
|
selectedSpreadsheetFileName,
|
||||||
|
actionState,
|
||||||
|
refreshCurrentAssets,
|
||||||
|
loadSelectedAssetDetail,
|
||||||
|
resolveActor,
|
||||||
|
toast
|
||||||
|
}) {
|
||||||
|
const spreadsheetUploadInput = ref(null)
|
||||||
|
const spreadsheetOnlyOfficeLoading = ref(false)
|
||||||
|
const spreadsheetOnlyOfficeError = ref('')
|
||||||
|
const spreadsheetOnlyOfficeEditor = ref(null)
|
||||||
|
const spreadsheetOnlyOfficeReady = ref(false)
|
||||||
|
const spreadsheetOnlyOfficeHostId = ref('audit-rule-onlyoffice')
|
||||||
|
const spreadsheetChangeRecordsByAsset = ref({})
|
||||||
|
const spreadsheetChangeDetailOpen = ref(false)
|
||||||
|
const selectedSpreadsheetChangeRecord = ref(null)
|
||||||
|
|
||||||
|
let spreadsheetOnlyOfficeMountSeq = 0
|
||||||
|
let spreadsheetOnlyOfficeLoadTimer = null
|
||||||
|
let spreadsheetOnlyOfficeHadLocalEdits = false
|
||||||
|
let spreadsheetOnlyOfficeSyncSeq = 0
|
||||||
|
let spreadsheetOnlyOfficeChangePollTimer = null
|
||||||
|
|
||||||
|
const selectedSpreadsheetChangeRecords = computed(() => {
|
||||||
|
if (!selectedSkillUsesSpreadsheet.value || !selectedSkill.value?.id) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return (spreadsheetChangeRecordsByAsset.value[selectedSkill.value.id] || [])
|
||||||
|
.filter((item) => item?.changed_at)
|
||||||
|
.map((item) => {
|
||||||
|
const sheetNames = [
|
||||||
|
...(Array.isArray(item.sheet_changes)
|
||||||
|
? item.sheet_changes.map((change) => normalizeText(change.sheet_name))
|
||||||
|
: []),
|
||||||
|
...(Array.isArray(item.cell_changes)
|
||||||
|
? item.cell_changes.map((change) => normalizeText(change.sheet_name))
|
||||||
|
: [])
|
||||||
|
].filter(Boolean)
|
||||||
|
const changedSheetNames = [...new Set(sheetNames)]
|
||||||
|
const previewChanges = Array.isArray(item.cell_changes) ? item.cell_changes.slice(0, 3) : []
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
time: formatDateTime(item.changed_at),
|
||||||
|
summary: formatSpreadsheetChangeSummary(item.summary),
|
||||||
|
changeCountLabel: item.changed_cell_count
|
||||||
|
? `${item.changed_cell_count} 处改动`
|
||||||
|
: `${item.changed_sheet_count || changedSheetNames.length || 0} 个工作表`,
|
||||||
|
changedSheetNames,
|
||||||
|
sheetPreview: changedSheetNames.slice(0, 4),
|
||||||
|
remainingSheetCount: Math.max(changedSheetNames.length - 4, 0),
|
||||||
|
previewChanges,
|
||||||
|
remainingChangeCount: Math.max((item.changed_cell_count || 0) - previewChanges.length, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedSpreadsheetChangeSheetRows = computed(() =>
|
||||||
|
Array.isArray(selectedSpreadsheetChangeRecord.value?.sheet_changes)
|
||||||
|
? selectedSpreadsheetChangeRecord.value.sheet_changes.map((item) => ({
|
||||||
|
...item,
|
||||||
|
meta: resolveDiffChangeMeta(item.change_type)
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectedSpreadsheetChangeCellRows = computed(() =>
|
||||||
|
Array.isArray(selectedSpreadsheetChangeRecord.value?.cell_changes)
|
||||||
|
? selectedSpreadsheetChangeRecord.value.cell_changes.map((item) => ({
|
||||||
|
...item,
|
||||||
|
meta: resolveDiffChangeMeta(item.change_type)
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
)
|
||||||
|
|
||||||
|
function stopSpreadsheetOnlyOfficeChangeSync() {
|
||||||
|
if (spreadsheetOnlyOfficeChangePollTimer) {
|
||||||
|
window.clearTimeout(spreadsheetOnlyOfficeChangePollTimer)
|
||||||
|
spreadsheetOnlyOfficeChangePollTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroySpreadsheetOnlyOfficeEditor() {
|
||||||
|
if (spreadsheetOnlyOfficeLoadTimer) {
|
||||||
|
window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
|
||||||
|
spreadsheetOnlyOfficeLoadTimer = null
|
||||||
|
}
|
||||||
|
stopSpreadsheetOnlyOfficeChangeSync()
|
||||||
|
spreadsheetOnlyOfficeHadLocalEdits = false
|
||||||
|
spreadsheetOnlyOfficeSyncSeq += 1
|
||||||
|
if (spreadsheetOnlyOfficeEditor.value?.destroyEditor) {
|
||||||
|
spreadsheetOnlyOfficeEditor.value.destroyEditor()
|
||||||
|
}
|
||||||
|
spreadsheetOnlyOfficeEditor.value = null
|
||||||
|
spreadsheetOnlyOfficeReady.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestSpreadsheetChangeKey(assetId) {
|
||||||
|
return buildSpreadsheetChangeRecordKey(spreadsheetChangeRecordsByAsset.value[assetId] || [])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSpreadsheetChangeRecords(assetId) {
|
||||||
|
if (!assetId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const payload = await fetchAgentAssetSpreadsheetChangeRecords(assetId, 30)
|
||||||
|
spreadsheetChangeRecordsByAsset.value = {
|
||||||
|
...spreadsheetChangeRecordsByAsset.value,
|
||||||
|
[assetId]: Array.isArray(payload) ? payload : []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshSpreadsheetChangeRecordsAfterSave(assetId, previousLatestKey = '', attempt = 0) {
|
||||||
|
const normalizedAssetId = normalizeText(assetId)
|
||||||
|
if (!normalizedAssetId || selectedSkill.value?.id !== normalizedAssetId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadSpreadsheetChangeRecords(normalizedAssetId)
|
||||||
|
const nextLatestKey = getLatestSpreadsheetChangeKey(normalizedAssetId)
|
||||||
|
if (nextLatestKey && nextLatestKey !== previousLatestKey) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (attempt >= 9) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => window.setTimeout(resolve, 800))
|
||||||
|
return refreshSpreadsheetChangeRecordsAfterSave(normalizedAssetId, previousLatestKey, attempt + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleSpreadsheetOnlyOfficeChangeSync(assetId, attempt = 0) {
|
||||||
|
const normalizedAssetId = normalizeText(assetId)
|
||||||
|
if (!normalizedAssetId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncSeq = ++spreadsheetOnlyOfficeSyncSeq
|
||||||
|
stopSpreadsheetOnlyOfficeChangeSync()
|
||||||
|
const previousLatestChangeKey = getLatestSpreadsheetChangeKey(normalizedAssetId)
|
||||||
|
|
||||||
|
const runSync = async () => {
|
||||||
|
if (syncSeq !== spreadsheetOnlyOfficeSyncSeq || selectedSkill.value?.id !== normalizedAssetId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const changeRecordRefreshed = await refreshSpreadsheetChangeRecordsAfterSave(
|
||||||
|
normalizedAssetId,
|
||||||
|
previousLatestChangeKey
|
||||||
|
)
|
||||||
|
if (changeRecordRefreshed) {
|
||||||
|
await refreshCurrentAssets()
|
||||||
|
stopSpreadsheetOnlyOfficeChangeSync()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 临时轮询失败不打断编辑器,继续在窗口期内重试。
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncSeq !== spreadsheetOnlyOfficeSyncSeq || selectedSkill.value?.id !== normalizedAssetId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (attempt >= 29) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
spreadsheetOnlyOfficeChangePollTimer = window.setTimeout(() => {
|
||||||
|
scheduleSpreadsheetOnlyOfficeChangeSync(normalizedAssetId, attempt + 1)
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
spreadsheetOnlyOfficeChangePollTimer = window.setTimeout(() => {
|
||||||
|
runSync().catch(() => {})
|
||||||
|
}, attempt === 0 ? 800 : 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId) {
|
||||||
|
return (
|
||||||
|
mountSeq !== spreadsheetOnlyOfficeMountSeq ||
|
||||||
|
!selectedSkillUsesSpreadsheet.value ||
|
||||||
|
selectedSkill.value?.id !== assetId ||
|
||||||
|
selectedSkill.value?.loading
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mountSpreadsheetOnlyOfficeEditor(retryAttempt = 0) {
|
||||||
|
if (!selectedSkillUsesSpreadsheet.value || !selectedSkill.value?.id || selectedSkill.value?.loading) {
|
||||||
|
destroySpreadsheetOnlyOfficeEditor()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const mountSeq = ++spreadsheetOnlyOfficeMountSeq
|
||||||
|
const assetId = selectedSkill.value.id
|
||||||
|
const editable = canEditSpreadsheetInline.value
|
||||||
|
|
||||||
|
spreadsheetOnlyOfficeLoading.value = true
|
||||||
|
spreadsheetOnlyOfficeError.value = ''
|
||||||
|
spreadsheetOnlyOfficeReady.value = false
|
||||||
|
destroySpreadsheetOnlyOfficeEditor()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await fetchAgentAssetSpreadsheetOnlyOfficeConfig(assetId)
|
||||||
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadOnlyOfficeApi(payload.documentServerUrl)
|
||||||
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!window.DocsAPI?.DocEditor) {
|
||||||
|
throw new Error('表格编辑器未正确加载。')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ONLYOFFICE 会改写宿主节点;每次挂载使用新 id 避免复用脏容器。
|
||||||
|
spreadsheetOnlyOfficeHostId.value = `audit-rule-onlyoffice-${assetId}-${mountSeq}`
|
||||||
|
await nextTick()
|
||||||
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = buildOnlyOfficeEditorConfig(payload.config, {
|
||||||
|
viewportHeight: window.innerHeight,
|
||||||
|
editable,
|
||||||
|
fillContainer: true
|
||||||
|
})
|
||||||
|
const upstreamEvents = config.events || {}
|
||||||
|
spreadsheetOnlyOfficeLoadTimer = window.setTimeout(() => {
|
||||||
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (retryAttempt < 1) {
|
||||||
|
destroySpreadsheetOnlyOfficeEditor()
|
||||||
|
spreadsheetOnlyOfficeLoading.value = true
|
||||||
|
window.setTimeout(() => {
|
||||||
|
mountSpreadsheetOnlyOfficeEditor(retryAttempt + 1).catch(() => {})
|
||||||
|
}, 600)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
spreadsheetOnlyOfficeError.value = '表格加载超时,请退出详情后重试。'
|
||||||
|
spreadsheetOnlyOfficeLoading.value = false
|
||||||
|
destroySpreadsheetOnlyOfficeEditor()
|
||||||
|
}, 15000)
|
||||||
|
config.events = {
|
||||||
|
...upstreamEvents,
|
||||||
|
onAppReady(event) {
|
||||||
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (spreadsheetOnlyOfficeLoadTimer) {
|
||||||
|
window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
|
||||||
|
spreadsheetOnlyOfficeLoadTimer = null
|
||||||
|
}
|
||||||
|
spreadsheetOnlyOfficeReady.value = true
|
||||||
|
spreadsheetOnlyOfficeLoading.value = false
|
||||||
|
upstreamEvents.onAppReady?.(event)
|
||||||
|
},
|
||||||
|
onError(event) {
|
||||||
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (spreadsheetOnlyOfficeLoadTimer) {
|
||||||
|
window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
|
||||||
|
spreadsheetOnlyOfficeLoadTimer = null
|
||||||
|
}
|
||||||
|
const errorCode = event?.data?.errorCode
|
||||||
|
const errorDescription = event?.data?.errorDescription
|
||||||
|
spreadsheetOnlyOfficeError.value = errorDescription
|
||||||
|
? `表格加载失败:${errorDescription}`
|
||||||
|
: `表格加载失败${errorCode ? `(错误码 ${errorCode})` : '。'}`
|
||||||
|
spreadsheetOnlyOfficeLoading.value = false
|
||||||
|
upstreamEvents.onError?.(event)
|
||||||
|
},
|
||||||
|
onDocumentStateChange(event) {
|
||||||
|
const hasChanges = Boolean(event?.data)
|
||||||
|
if (hasChanges) {
|
||||||
|
spreadsheetOnlyOfficeHadLocalEdits = true
|
||||||
|
if (!spreadsheetOnlyOfficeChangePollTimer) {
|
||||||
|
scheduleSpreadsheetOnlyOfficeChangeSync(assetId)
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
spreadsheetOnlyOfficeHadLocalEdits &&
|
||||||
|
editable &&
|
||||||
|
!isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)
|
||||||
|
) {
|
||||||
|
spreadsheetOnlyOfficeHadLocalEdits = false
|
||||||
|
scheduleSpreadsheetOnlyOfficeChangeSync(assetId)
|
||||||
|
}
|
||||||
|
upstreamEvents.onDocumentStateChange?.(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spreadsheetOnlyOfficeEditor.value = new window.DocsAPI.DocEditor(
|
||||||
|
spreadsheetOnlyOfficeHostId.value,
|
||||||
|
config
|
||||||
|
)
|
||||||
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||||
|
destroySpreadsheetOnlyOfficeEditor()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
spreadsheetOnlyOfficeError.value = error?.message || '规则表加载失败,请稍后重试。'
|
||||||
|
spreadsheetOnlyOfficeLoading.value = false
|
||||||
|
toast(spreadsheetOnlyOfficeError.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerSpreadsheetUpload() {
|
||||||
|
if (!canUploadSpreadsheet.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
spreadsheetUploadInput.value?.click?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadSpreadsheetFile() {
|
||||||
|
if (!canDownloadSpreadsheet.value || !selectedSkill.value?.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actionState.value = 'download-spreadsheet'
|
||||||
|
try {
|
||||||
|
const blob = await fetchAgentAssetSpreadsheetBlob(
|
||||||
|
selectedSkill.value.id,
|
||||||
|
'attachment'
|
||||||
|
)
|
||||||
|
const objectUrl = URL.createObjectURL(blob)
|
||||||
|
const anchor = document.createElement('a')
|
||||||
|
anchor.href = objectUrl
|
||||||
|
anchor.download = selectedSpreadsheetFileName.value || '规则表.xlsx'
|
||||||
|
document.body.appendChild(anchor)
|
||||||
|
anchor.click()
|
||||||
|
anchor.remove()
|
||||||
|
URL.revokeObjectURL(objectUrl)
|
||||||
|
} catch (error) {
|
||||||
|
toast(error?.message || '规则表下载失败,请稍后重试。')
|
||||||
|
} finally {
|
||||||
|
actionState.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadSpreadsheetFile(file) {
|
||||||
|
if (!file || !selectedSkill.value?.id || !canUploadSpreadsheet.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actionState.value = 'upload-spreadsheet'
|
||||||
|
try {
|
||||||
|
await importAgentAssetSpreadsheetContent(selectedSkill.value.id, file, {
|
||||||
|
actor: resolveActor()
|
||||||
|
})
|
||||||
|
await refreshCurrentAssets()
|
||||||
|
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||||
|
await loadSpreadsheetChangeRecords(selectedSkill.value.id)
|
||||||
|
toast(`已导入 ${file.name} 的表格内容,右侧会记录本次修改。`)
|
||||||
|
} catch (error) {
|
||||||
|
toast(error?.message || '规则表内容导入失败,请稍后重试。')
|
||||||
|
} finally {
|
||||||
|
actionState.value = ''
|
||||||
|
spreadsheetUploadInput.value?.reset?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSpreadsheetFileInput(event) {
|
||||||
|
await uploadSpreadsheetFile(event?.target?.files?.[0] || null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSpreadsheetChangeDetail(item) {
|
||||||
|
if (!item?.changed_at) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedSpreadsheetChangeRecord.value = item
|
||||||
|
spreadsheetChangeDetailOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSpreadsheetChangeDetail() {
|
||||||
|
spreadsheetChangeDetailOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
spreadsheetUploadInput,
|
||||||
|
spreadsheetOnlyOfficeLoading,
|
||||||
|
spreadsheetOnlyOfficeError,
|
||||||
|
spreadsheetOnlyOfficeReady,
|
||||||
|
spreadsheetOnlyOfficeHostId,
|
||||||
|
spreadsheetChangeDetailOpen,
|
||||||
|
selectedSpreadsheetChangeRecord,
|
||||||
|
selectedSpreadsheetChangeRecords,
|
||||||
|
selectedSpreadsheetChangeSheetRows,
|
||||||
|
selectedSpreadsheetChangeCellRows,
|
||||||
|
destroySpreadsheetOnlyOfficeEditor,
|
||||||
|
mountSpreadsheetOnlyOfficeEditor,
|
||||||
|
triggerSpreadsheetUpload,
|
||||||
|
downloadSpreadsheetFile,
|
||||||
|
uploadSpreadsheetFile,
|
||||||
|
handleSpreadsheetFileInput,
|
||||||
|
loadSpreadsheetChangeRecords,
|
||||||
|
openSpreadsheetChangeDetail,
|
||||||
|
closeSpreadsheetChangeDetail
|
||||||
|
}
|
||||||
|
}
|
||||||
169
web/src/views/scripts/useAuditVersionTimeline.js
Normal file
169
web/src/views/scripts/useAuditVersionTimeline.js
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import { fetchAgentAssetVersionTimeline } from '../../services/agentAssets.js'
|
||||||
|
import {
|
||||||
|
buildDefaultRuntimeRule,
|
||||||
|
formatDateTime,
|
||||||
|
normalizeText,
|
||||||
|
resolveRuleTemplateLabel,
|
||||||
|
resolveTimelineEventMeta,
|
||||||
|
stringifyRuntimeRule
|
||||||
|
} from './auditViewModel.js'
|
||||||
|
|
||||||
|
const VERSION_TIMELINE_CACHE_TTL = 60 * 1000
|
||||||
|
|
||||||
|
function normalizeAssetId(assetId) {
|
||||||
|
return normalizeText(assetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function readVersionTimelineCache(timelineCache, assetId) {
|
||||||
|
const key = normalizeAssetId(assetId)
|
||||||
|
if (!key) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = timelineCache.get(key)
|
||||||
|
if (!cached) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExpired = Date.now() - cached.timestamp > VERSION_TIMELINE_CACHE_TTL
|
||||||
|
return isExpired ? null : cached.items
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeVersionTimelineCache(timelineCache, assetId, items) {
|
||||||
|
const key = normalizeAssetId(assetId)
|
||||||
|
if (!key) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
timelineCache.set(key, {
|
||||||
|
items,
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyVersionPayloadToRulePreview(skill, version) {
|
||||||
|
if (!skill || !version) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedVersion = version.version
|
||||||
|
skill.displayVersion = selectedVersion
|
||||||
|
skill.displayVersionChangeNote = version.note || '无版本说明'
|
||||||
|
|
||||||
|
if (skill.usesSpreadsheetRule) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof version.markdownContent === 'string') {
|
||||||
|
skill.markdownContent = version.markdownContent
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtimeRule = version.runtimeRule || buildDefaultRuntimeRule(skill)
|
||||||
|
skill.runtimeRuleText = stringifyRuntimeRule(runtimeRule)
|
||||||
|
skill.runtimeKind = normalizeText(runtimeRule.kind) || skill.runtimeKind || 'policy_rule_draft'
|
||||||
|
skill.ruleTemplateKey = normalizeText(runtimeRule.template_key) || skill.ruleTemplateKey
|
||||||
|
skill.ruleTemplateLabel = resolveRuleTemplateLabel(skill.ruleTemplateKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuditVersionTimeline({ selectedSkill, toast }) {
|
||||||
|
const versionSwitchTarget = ref(null)
|
||||||
|
const versionTimelineOpen = ref(false)
|
||||||
|
const versionTimelineLoading = ref(false)
|
||||||
|
const versionTimelineError = ref('')
|
||||||
|
const versionTimelineItems = ref([])
|
||||||
|
const versionTimelineCache = new Map()
|
||||||
|
|
||||||
|
const selectedVersionTimelineItems = computed(() =>
|
||||||
|
versionTimelineItems.value.map((item) => ({
|
||||||
|
...item,
|
||||||
|
meta: resolveTimelineEventMeta(item.event_type),
|
||||||
|
timeLabel: formatDateTime(item.event_time)
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
async function loadVersionTimeline(assetId = selectedSkill.value?.id, options = {}) {
|
||||||
|
if (!assetId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedItems = options.force ? null : readVersionTimelineCache(versionTimelineCache, assetId)
|
||||||
|
if (cachedItems) {
|
||||||
|
versionTimelineItems.value = cachedItems
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
versionTimelineLoading.value = true
|
||||||
|
versionTimelineError.value = ''
|
||||||
|
try {
|
||||||
|
const payload = await fetchAgentAssetVersionTimeline(assetId)
|
||||||
|
const nextItems = Array.isArray(payload) ? payload : []
|
||||||
|
versionTimelineItems.value = nextItems
|
||||||
|
writeVersionTimelineCache(versionTimelineCache, assetId, nextItems)
|
||||||
|
} catch (error) {
|
||||||
|
versionTimelineError.value = error?.message || '操作记录加载失败,请稍后重试。'
|
||||||
|
if (!options.silent) {
|
||||||
|
toast(versionTimelineError.value)
|
||||||
|
}
|
||||||
|
versionTimelineItems.value = []
|
||||||
|
writeVersionTimelineCache(versionTimelineCache, assetId, [])
|
||||||
|
} finally {
|
||||||
|
versionTimelineLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openVersionTimeline() {
|
||||||
|
if (!selectedSkill.value?.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
versionTimelineOpen.value = true
|
||||||
|
await loadVersionTimeline(selectedSkill.value.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeVersionTimeline() {
|
||||||
|
versionTimelineOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearVersionTimelineState() {
|
||||||
|
versionTimelineOpen.value = false
|
||||||
|
versionTimelineItems.value = []
|
||||||
|
versionTimelineError.value = ''
|
||||||
|
versionSwitchTarget.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function openVersionSwitch(version) {
|
||||||
|
if (!selectedSkill.value || version.version === selectedSkill.value.displayVersion) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
versionSwitchTarget.value = version
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelVersionSwitch() {
|
||||||
|
versionSwitchTarget.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmVersionSwitch() {
|
||||||
|
if (!selectedSkill.value || !versionSwitchTarget.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
applyVersionPayloadToRulePreview(selectedSkill.value, versionSwitchTarget.value)
|
||||||
|
versionSwitchTarget.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
versionSwitchTarget,
|
||||||
|
versionTimelineOpen,
|
||||||
|
versionTimelineLoading,
|
||||||
|
versionTimelineError,
|
||||||
|
selectedVersionTimelineItems,
|
||||||
|
loadVersionTimeline,
|
||||||
|
openVersionTimeline,
|
||||||
|
closeVersionTimeline,
|
||||||
|
clearVersionTimelineState,
|
||||||
|
openVersionSwitch,
|
||||||
|
cancelVersionSwitch,
|
||||||
|
confirmVersionSwitch
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,6 +65,7 @@ test('legacy reimbursement approval and archive centers are no longer accessible
|
|||||||
assert.equal(canAccessAppView(adminUser, 'requests'), false)
|
assert.equal(canAccessAppView(adminUser, 'requests'), false)
|
||||||
assert.equal(canAccessAppView(adminUser, 'approval'), false)
|
assert.equal(canAccessAppView(adminUser, 'approval'), false)
|
||||||
assert.equal(canAccessAppView(adminUser, 'archive'), false)
|
assert.equal(canAccessAppView(adminUser, 'archive'), false)
|
||||||
|
assert.equal(canAccessAppView(adminUser, 'logs'), false)
|
||||||
assert.equal(canAccessAppView(adminUser, 'documents'), true)
|
assert.equal(canAccessAppView(adminUser, 'documents'), true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
56
web/tests/minimum-visible-state.test.mjs
Normal file
56
web/tests/minimum-visible-state.test.mjs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
import { effectScope, nextTick, ref } from 'vue'
|
||||||
|
|
||||||
|
import { useMinimumVisibleState } from '../src/composables/useMinimumVisibleState.js'
|
||||||
|
|
||||||
|
function wait(ms) {
|
||||||
|
return new Promise((resolve) => globalThis.setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
|
|
||||||
|
test('minimum visible state stays visible after a fast loading toggle', async () => {
|
||||||
|
const scope = effectScope()
|
||||||
|
const state = scope.run(() => {
|
||||||
|
const loading = ref(false)
|
||||||
|
const visible = useMinimumVisibleState(loading, { minVisibleMs: 35 })
|
||||||
|
return { loading, visible }
|
||||||
|
})
|
||||||
|
|
||||||
|
state.loading.value = true
|
||||||
|
await nextTick()
|
||||||
|
assert.equal(state.visible.value, true)
|
||||||
|
|
||||||
|
state.loading.value = false
|
||||||
|
await nextTick()
|
||||||
|
assert.equal(state.visible.value, true)
|
||||||
|
|
||||||
|
await wait(50)
|
||||||
|
assert.equal(state.visible.value, false)
|
||||||
|
scope.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('minimum visible state cancels a pending hide when loading restarts', async () => {
|
||||||
|
const scope = effectScope()
|
||||||
|
const state = scope.run(() => {
|
||||||
|
const loading = ref(false)
|
||||||
|
const visible = useMinimumVisibleState(loading, { minVisibleMs: 40 })
|
||||||
|
return { loading, visible }
|
||||||
|
})
|
||||||
|
|
||||||
|
state.loading.value = true
|
||||||
|
await nextTick()
|
||||||
|
state.loading.value = false
|
||||||
|
await nextTick()
|
||||||
|
await wait(10)
|
||||||
|
|
||||||
|
state.loading.value = true
|
||||||
|
await nextTick()
|
||||||
|
await wait(40)
|
||||||
|
assert.equal(state.visible.value, true)
|
||||||
|
|
||||||
|
state.loading.value = false
|
||||||
|
await nextTick()
|
||||||
|
await wait(50)
|
||||||
|
assert.equal(state.visible.value, false)
|
||||||
|
scope.stop()
|
||||||
|
})
|
||||||
@@ -8,9 +8,9 @@ import {
|
|||||||
} from '../src/composables/useNavigation.js'
|
} from '../src/composables/useNavigation.js'
|
||||||
|
|
||||||
function testDerivesViewFromRouteName() {
|
function testDerivesViewFromRouteName() {
|
||||||
assert.equal(resolveAppViewFromRoute({ name: 'app-log-detail', meta: {} }), 'logs')
|
assert.equal(resolveAppViewFromRoute({ name: 'app-log-detail', meta: {} }), 'settings')
|
||||||
assert.equal(resolveAppViewFromRoute({ name: 'app-request-detail', meta: {} }), 'documents')
|
assert.equal(resolveAppViewFromRoute({ name: 'app-request-detail', meta: {} }), 'documents')
|
||||||
assert.equal(resolveAppViewFromRoute({ name: 'app-policies', meta: { appView: 'logs' } }), 'policies')
|
assert.equal(resolveAppViewFromRoute({ name: 'app-policies', meta: { appView: 'settings' } }), 'policies')
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFallsBackToValidMeta() {
|
function testFallsBackToValidMeta() {
|
||||||
@@ -19,7 +19,7 @@ function testFallsBackToValidMeta() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function testResolvesMainRouteNames() {
|
function testResolvesMainRouteNames() {
|
||||||
assert.equal(resolveTargetRouteName('logs'), 'app-logs')
|
assert.equal(resolveTargetRouteName('logs'), 'app-settings')
|
||||||
assert.equal(resolveTargetRouteName('policies'), 'app-policies')
|
assert.equal(resolveTargetRouteName('policies'), 'app-policies')
|
||||||
assert.equal(resolveTargetRouteName('requests'), 'app-overview')
|
assert.equal(resolveTargetRouteName('requests'), 'app-overview')
|
||||||
assert.equal(resolveTargetRouteName('approval'), 'app-overview')
|
assert.equal(resolveTargetRouteName('approval'), 'app-overview')
|
||||||
@@ -31,7 +31,8 @@ function testLegacyCentersAreRemovedFromNavigation() {
|
|||||||
assert.equal(appViews.includes('requests'), false)
|
assert.equal(appViews.includes('requests'), false)
|
||||||
assert.equal(appViews.includes('approval'), false)
|
assert.equal(appViews.includes('approval'), false)
|
||||||
assert.equal(appViews.includes('archive'), false)
|
assert.equal(appViews.includes('archive'), false)
|
||||||
assert.equal(navItems.some((item) => ['requests', 'approval', 'archive'].includes(item.id)), false)
|
assert.equal(appViews.includes('logs'), false)
|
||||||
|
assert.equal(navItems.some((item) => ['requests', 'approval', 'archive', 'logs'].includes(item.id)), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function run() {
|
function run() {
|
||||||
|
|||||||
34
web/tests/refresh-interval-controls.test.mjs
Normal file
34
web/tests/refresh-interval-controls.test.mjs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
const refreshOptions = readFileSync(new URL('../src/utils/refreshIntervalOptions.js', import.meta.url), 'utf8')
|
||||||
|
const logsView = readFileSync(new URL('../src/views/LogsView.vue', import.meta.url), 'utf8')
|
||||||
|
const logsScript = readFileSync(new URL('../src/views/scripts/LogsView.js', import.meta.url), 'utf8')
|
||||||
|
const workRecords = readFileSync(
|
||||||
|
new URL('../src/components/audit/DigitalEmployeeWorkRecords.vue', import.meta.url),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
|
||||||
|
test('shared refresh interval options default to 60 seconds', () => {
|
||||||
|
assert.match(refreshOptions, /DEFAULT_REFRESH_INTERVAL_MS\s*=\s*60000/)
|
||||||
|
for (const value of [1000, 3000, 5000, 10000, 30000, 60000, 180000]) {
|
||||||
|
assert.match(refreshOptions, new RegExp(`value:\\s*${value}`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('system logs list exposes refresh interval control', () => {
|
||||||
|
assert.match(logsScript, /refreshInterval\s*=\s*ref\(DEFAULT_REFRESH_INTERVAL_MS\)/)
|
||||||
|
assert.match(logsScript, /window\.setInterval\([\s\S]*refreshInterval\.value/)
|
||||||
|
assert.match(logsView, /刷新时间 \{\{ refreshIntervalLabel \}\}/)
|
||||||
|
assert.match(logsView, /v-for="option in refreshIntervalOptions"/)
|
||||||
|
assert.doesNotMatch(logsView, /刷新日志/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('digital employee work records expose refresh interval control', () => {
|
||||||
|
assert.match(workRecords, /refreshInterval\s*=\s*ref\(DEFAULT_REFRESH_INTERVAL_MS\)/)
|
||||||
|
assert.match(workRecords, /refreshIntervalPickerOptions\s*=\s*REFRESH_INTERVAL_OPTIONS/)
|
||||||
|
assert.match(workRecords, /window\.setInterval\([\s\S]*refreshInterval\.value/)
|
||||||
|
assert.match(workRecords, /刷新时间 \$\{refreshIntervalLabel\}/)
|
||||||
|
assert.doesNotMatch(workRecords, /AGENT_RUN_POLL_INTERVAL_MS/)
|
||||||
|
})
|
||||||
22
web/tests/settings-system-logs-section.test.mjs
Normal file
22
web/tests/settings-system-logs-section.test.mjs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
|
const settingsModel = readFileSync(new URL('../src/utils/settingsModelHelper.js', import.meta.url), 'utf8')
|
||||||
|
const settingsView = readFileSync(new URL('../src/views/SettingsView.vue', import.meta.url), 'utf8')
|
||||||
|
const settingsScript = readFileSync(new URL('../src/views/scripts/SettingsView.js', import.meta.url), 'utf8')
|
||||||
|
const router = readFileSync(new URL('../src/router/index.js', import.meta.url), 'utf8')
|
||||||
|
const logDetailView = readFileSync(new URL('../src/views/LogDetailView.vue', import.meta.url), 'utf8')
|
||||||
|
|
||||||
|
test('system logs are nested under system settings instead of sidebar navigation', () => {
|
||||||
|
assert.match(settingsModel, /id:\s*'systemLogs'[\s\S]*label:\s*'系统日志'/)
|
||||||
|
assert.match(settingsView, /activeSection === 'systemLogs'/)
|
||||||
|
assert.match(settingsView, /<LogsView v-else class="settings-logs-view" \/>/)
|
||||||
|
assert.match(settingsScript, /import LogsView from '\.\.\/LogsView\.vue'/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('log detail keeps the settings context and legacy logs URLs redirect', () => {
|
||||||
|
assert.match(router, /path:\s*'\/app\/settings\/logs\/:logKind\/:logId'[\s\S]*appView:\s*'settings'/)
|
||||||
|
assert.match(router, /path:\s*'\/app\/logs'[\s\S]*section:\s*'systemLogs'/)
|
||||||
|
assert.match(logDetailView, /router\.push\(\{ name:\s*'app-settings', query:\s*\{ section:\s*'systemLogs' \} \}\)/)
|
||||||
|
})
|
||||||
@@ -1046,5 +1046,43 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id) {
|
||||||
|
if (!id.includes('node_modules')) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const normalizedId = id.replace(/\\/g, '/')
|
||||||
|
if (
|
||||||
|
normalizedId.includes('/node_modules/vue/') ||
|
||||||
|
normalizedId.includes('/node_modules/@vue/') ||
|
||||||
|
normalizedId.includes('/node_modules/vue-router/')
|
||||||
|
) {
|
||||||
|
return 'vendor-vue'
|
||||||
|
}
|
||||||
|
if (normalizedId.includes('element-plus') || normalizedId.includes('@element-plus')) {
|
||||||
|
return 'vendor-element-plus'
|
||||||
|
}
|
||||||
|
if (normalizedId.includes('echarts') || normalizedId.includes('zrender')) {
|
||||||
|
return 'vendor-echarts'
|
||||||
|
}
|
||||||
|
if (normalizedId.includes('@antv/g6')) {
|
||||||
|
return 'vendor-g6'
|
||||||
|
}
|
||||||
|
if (normalizedId.includes('chart.js') || normalizedId.includes('vue-chartjs')) {
|
||||||
|
return 'vendor-chartjs'
|
||||||
|
}
|
||||||
|
if (normalizedId.includes('markdown-it')) {
|
||||||
|
return 'vendor-markdown'
|
||||||
|
}
|
||||||
|
if (normalizedId.includes('@vueuse')) {
|
||||||
|
return 'vendor-vueuse'
|
||||||
|
}
|
||||||
|
return 'vendor'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
plugins: [vue(), localSetupPlugin()]
|
plugins: [vue(), localSetupPlugin()]
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user