diff --git a/server/rules/finance-rules/公司通信费报销规则.xlsx b/server/rules/finance-rules/公司通信费报销规则.xlsx index 63bf7ef..510877a 100644 Binary files a/server/rules/finance-rules/公司通信费报销规则.xlsx and b/server/rules/finance-rules/公司通信费报销规则.xlsx differ diff --git a/web/package-lock.json b/web/package-lock.json index efae457..9afab4f 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -17,7 +17,6 @@ "element-plus": "^2.14.0", "markdown-it": "^14.1.1", "pg": "^8.13.1", - "primeicons": "^7.0.0", "vite": "^5.4.19", "vue": "^3.5.13", "vue-chartjs": "^5.3.3", @@ -2754,12 +2753,6 @@ "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": { "version": "2.3.1", "resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz", diff --git a/web/package.json b/web/package.json index 1fdabb9..64d8ee3 100644 --- a/web/package.json +++ b/web/package.json @@ -19,7 +19,6 @@ "element-plus": "^2.14.0", "markdown-it": "^14.1.1", "pg": "^8.13.1", - "primeicons": "^7.0.0", "vite": "^5.4.19", "vue": "^3.5.13", "vue-chartjs": "^5.3.3", diff --git a/web/src/assets/styles/app.css b/web/src/assets/styles/app.css index d54784c..2faa116 100644 --- a/web/src/assets/styles/app.css +++ b/web/src/assets/styles/app.css @@ -69,68 +69,6 @@ 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 { transition: opacity 180ms var(--ease); } @@ -231,7 +169,6 @@ .main.policies-main, .main.audit-main, .main.digital-employees-main, -.main.logs-main, .main.employees-main, .main.settings-main { height: var(--desktop-stage-height, 100dvh); @@ -250,7 +187,6 @@ .workarea.policies-workarea, .workarea.audit-workarea, .workarea.digital-employees-workarea, -.workarea.logs-workarea, .workarea.employees-workarea, .workarea.settings-workarea { min-height: 0; @@ -267,28 +203,6 @@ 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 { from { opacity: 0; @@ -401,8 +315,6 @@ transition-duration: 120ms, 120ms !important; } - .login-entry-card, - .login-entry-progress::after, .app.login-entry-active .app-sidebar, .app.login-entry-active > .main { animation: none !important; diff --git a/web/src/assets/styles/components/enterprise-page-shell.css b/web/src/assets/styles/components/enterprise-page-shell.css index d098090..8ae497c 100644 --- a/web/src/assets/styles/components/enterprise-page-shell.css +++ b/web/src/assets/styles/components/enterprise-page-shell.css @@ -13,6 +13,12 @@ margin-top: 14px; border-bottom: 1px solid #dbe4ee; overflow-x: auto; + overflow-y: hidden; + scrollbar-width: none; +} + +.enterprise-list-page .status-tabs::-webkit-scrollbar { + display: none; } .enterprise-list-page .status-tabs button { @@ -65,6 +71,7 @@ } .enterprise-list-page .filter-set { + flex: 1 1 auto; display: flex; align-items: center; gap: 12px; @@ -74,7 +81,14 @@ .enterprise-list-page .list-search { 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 { @@ -202,13 +216,32 @@ 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 { min-height: 400px; margin-top: 10px; overflow: auto; border: 1px solid #edf2f7; - border-radius: 4px; - background: linear-gradient(180deg, #fcfeff 0%, var(--theme-primary-light-9) 100%); + border-radius: 10px; + background: linear-gradient(180deg, #fcfefd 0%, #f4f8f6 100%); display: flex; flex-direction: column; align-items: stretch; @@ -263,7 +296,8 @@ font-weight: 750; } -.enterprise-list-page table { +.enterprise-list-page table, +.enterprise-list-page .table-wrap > table { width: 100%; min-width: 1080px; align-self: flex-start; @@ -272,7 +306,9 @@ } .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; border-bottom: 1px solid #edf2f7; color: #24324a; @@ -285,7 +321,8 @@ text-overflow: ellipsis; } -.enterprise-list-page th { +.enterprise-list-page th, +.enterprise-list-page .table-wrap th { position: sticky; top: 0; z-index: 1; @@ -295,16 +332,20 @@ font-weight: 800; } -.enterprise-list-page tbody tr { +.enterprise-list-page tbody tr, +.enterprise-list-page .table-wrap tbody tr { cursor: pointer; } .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)); } -.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; } @@ -313,11 +354,119 @@ 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 { flex: 0 0 auto; } -.enterprise-list-page .list-foot { +.enterprise-list-page .list-foot, +.enterprise-list-page.enterprise-list-page .list-foot { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; @@ -325,27 +474,30 @@ margin-top: 12px; } -.enterprise-list-page .page-summary { +.enterprise-list-page .page-summary, +.enterprise-list-page.enterprise-list-page .page-summary { color: #64748b; font-size: 14px; font-weight: 650; } -.enterprise-list-page .pager { +.enterprise-list-page .pager, +.enterprise-list-page.enterprise-list-page .pager { display: inline-flex; justify-content: center; gap: 6px; padding: 4px; border: 1px solid #e2e8f0; - border-radius: 4px; + border-radius: 12px; background: #f8fafc; } -.enterprise-list-page .pager button { +.enterprise-list-page .pager button, +.enterprise-list-page.enterprise-list-page .pager button { width: 32px; height: 32px; border: 0; - border-radius: 4px; + border-radius: 9px; background: transparent; color: #334155; font-size: 14px; @@ -353,19 +505,22 @@ 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; color: var(--theme-primary-active); box-shadow: 0 1px 4px rgba(15, 23, 42, 0.08); } -.enterprise-list-page .pager button.active { - background: var(--theme-primary); +.enterprise-list-page .pager button.active, +.enterprise-list-page.enterprise-list-page .pager button.active { + background: var(--theme-primary-active); color: #fff; 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; opacity: 0.45; box-shadow: none; @@ -394,6 +549,86 @@ 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 { align-items: flex-start; } diff --git a/web/src/assets/styles/global.css b/web/src/assets/styles/global.css index 4bde223..7fca2f9 100644 --- a/web/src/assets/styles/global.css +++ b/web/src/assets/styles/global.css @@ -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; } } -.table-loading__spinner { - width: 38px; - height: 38px; - display: inline-grid; - place-items: center; - border: 3px solid #e2e8f0; - border-top-color: var(--primary); - border-radius: 50%; - animation: table-spinner-rotate .8s linear infinite !important; +.table-state:has(.table-loading.screen-floating), +.table-state:has(.table-loading-anchor), +.table-loading-row:has(.table-loading.screen-floating), +.table-loading-row:has(.table-loading-anchor) { + min-height: 0 !important; + padding: 0 !important; + background: transparent !important; } -.table-loading.sky .table-loading__spinner { - border-top-color: var(--primary); -} - -.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; -} - -@keyframes table-spinner-rotate { - to { - transform: rotate(360deg); - } +tr:has(> .table-loading-row .table-loading.screen-floating), +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-row:has(.table-loading.screen-floating), +.table-loading-row:has(.table-loading-anchor) { + height: 0 !important; + border: 0 !important; + line-height: 0 !important; + background: transparent !important; } /* Global Scrollbar Styles */ diff --git a/web/src/assets/styles/views/digital-employees-view.css b/web/src/assets/styles/views/digital-employees-view.css index 67dcde7..c7f4884 100644 --- a/web/src/assets/styles/views/digital-employees-view.css +++ b/web/src/assets/styles/views/digital-employees-view.css @@ -5,49 +5,81 @@ .digital-employees-list { height: 100%; + min-height: 0; + display: flex; + flex-direction: column; + padding: 16px 18px; + overflow: hidden; } .digital-employees-list > .status-tabs { 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 { - min-height: 0; +.digital-employees-list > .status-tabs::-webkit-scrollbar { + 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 { min-width: 1060px; - table-layout: fixed; } -.digital-employees-table .col-skill { width: 27%; } -.digital-employees-table .col-schedule { width: 16%; } -.digital-employees-table .col-mode { width: 12%; } +/* Default first column left alignment */ +.digital-employees-table th:first-child, +.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-status { width: 11%; } -.digital-employees-table .col-enabled { width: 11%; } -.digital-employees-table .col-updated { width: 12%; } - -.digital-employees-table td { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.digital-employees-table tbody tr { - cursor: pointer; -} +.digital-employees-table .col-owner { width: 11%; } +.digital-employees-table .col-schedule { width: 16%; } +.digital-employees-table .col-mode { width: 10%; } +.digital-employees-table .col-status { width: 10%; } +.digital-employees-table .col-enabled { width: 10%; } +.digital-employees-table .col-updated { width: 10%; } .digital-refresh-action i { font-size: 16px; } -.skill-type-pill { - border-color: #dbeafe; - background: #eff6ff; - color: #1d4ed8; -} - .digital-employee-detail { height: 100%; } diff --git a/web/src/assets/styles/views/logs-view.css b/web/src/assets/styles/views/logs-view.css index b6ec537..f7287ac 100644 --- a/web/src/assets/styles/views/logs-view.css +++ b/web/src/assets/styles/views/logs-view.css @@ -54,6 +54,26 @@ 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 { position: absolute; top: calc(100% + 8px); diff --git a/web/src/assets/styles/views/policies-view.css b/web/src/assets/styles/views/policies-view.css index 7677fc2..42fd40c 100644 --- a/web/src/assets/styles/views/policies-view.css +++ b/web/src/assets/styles/views/policies-view.css @@ -423,14 +423,6 @@ th { text-align: center; } -.table-loading-row { - padding: 0; -} - -.table-loading-row > .table-loading { - min-height: 220px; -} - .list-foot { display: grid; grid-template-columns: 1fr auto 1fr; diff --git a/web/src/assets/styles/views/settings-view.css b/web/src/assets/styles/views/settings-view.css index 218a1d8..eefa4b1 100644 --- a/web/src/assets/styles/views/settings-view.css +++ b/web/src/assets/styles/views/settings-view.css @@ -206,6 +206,16 @@ 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 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -680,6 +690,19 @@ 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 { display: flex; diff --git a/web/src/assets/styles/views/setup-view.css b/web/src/assets/styles/views/setup-view.css index 717eb66..b9acd57 100644 --- a/web/src/assets/styles/views/setup-view.css +++ b/web/src/assets/styles/views/setup-view.css @@ -591,9 +591,9 @@ background: rgba(6, 78, 59, 0.34); } -.setup-startup-spinner .pi { - font-size: 22px; -} +.setup-startup-spinner .mdi { + font-size: 22px; +} .setup-startup-spinner strong { color: #ffffff; @@ -626,10 +626,10 @@ background: rgba(15, 23, 42, 0.24); } -.setup-startup-step .pi { - margin-top: 2px; +.setup-startup-step .mdi { + margin-top: 2px; color: color-mix(in srgb, var(--theme-primary-soft) 46%, transparent); -} +} .setup-startup-step strong { color: #f8fffb; @@ -648,25 +648,25 @@ border-color: rgba(59, 130, 246, 0.34); } -.setup-startup-step.is-running .pi { - color: #93c5fd; -} +.setup-startup-step.is-running .mdi { + color: #93c5fd; +} .setup-startup-step.is-success { 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); -} +} .setup-startup-step.is-error { border-color: rgba(248, 113, 113, 0.36); } -.setup-startup-step.is-error .pi { - color: #f87171; -} +.setup-startup-step.is-error .mdi { + color: #f87171; +} .setup-startup-console { min-height: 0; diff --git a/web/src/components/audit/DigitalEmployeeListPanel.vue b/web/src/components/audit/DigitalEmployeeListPanel.vue index 80cf598..c6b4e4e 100644 --- a/web/src/components/audit/DigitalEmployeeListPanel.vue +++ b/web/src/components/audit/DigitalEmployeeListPanel.vue @@ -6,13 +6,21 @@ :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" - hint="归集后台自动执行的数字员工技能,可查看技能内容、执行计划、启动状态和最近版本。" + @update:current-page="currentPage = $event" > - - @@ -228,6 +206,9 @@ const pageNumbers = computed(() => { const start = Math.max(1, Math.min(currentPage.value - 3, total - 6)) return Array.from({ length: 7 }, (_, index) => start + index) }) +const paginationSummary = computed(() => + `共 ${props.visibleEmployees.length} 条,目前第 ${currentPage.value} / ${totalPages.value} 页` +) const emptyState = { eyebrow: '数字员工', title: '暂无匹配的数字员工', @@ -261,7 +242,7 @@ function selectFilter(type, value) { } - + diff --git a/web/src/components/audit/DigitalEmployeeWorkRecords.vue b/web/src/components/audit/DigitalEmployeeWorkRecords.vue index eaf5057..9783714 100644 --- a/web/src/components/audit/DigitalEmployeeWorkRecords.vue +++ b/web/src/components/audit/DigitalEmployeeWorkRecords.vue @@ -2,104 +2,114 @@
-
-
-
- - - + + + + + + + + + +
@@ -298,11 +283,15 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from '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 { fetchAgentRunDetail, fetchAgentRuns } from '../../services/agentAssets.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 { formatWorkRecordDateTime, formatWorkRecordSummary, @@ -330,6 +319,7 @@ const detailLoading = ref(false) const detailError = ref('') const selectedRunId = ref('') const selectedRunDetail = ref(null) +const refreshInterval = ref(DEFAULT_REFRESH_INTERVAL_MS) watch(detailOpen, (newVal) => { emit('detail-open-change', newVal) @@ -392,6 +382,14 @@ const listKeyword = ref('') const activeModule = ref('全部') const activeStatus = ref('全部') const activeFilterPopover = ref('') +const workRecordsEmptyState = { + eyebrow: '工作记录', + title: '暂无匹配的工作记录', + description: '当前没有符合搜索条件的数字员工工作记录。', + icon: 'mdi mdi-clipboard-text-clock-outline', + tone: 'theme', + artLabel: 'RECORDS' +} const modulePickerOptions = computed(() => { const set = new Set(runs.value.map((run) => resolveWorkRecordModuleLabel(run))) @@ -408,6 +406,11 @@ const statusPickerOptions = computed(() => { ...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 tokens = [] @@ -449,6 +452,9 @@ const pageNumbers = computed(() => { const start = Math.max(1, Math.min(currentPage.value - 3, total - 6)) return Array.from({ length: 7 }, (_, index) => start + index) }) +const paginationSummary = computed(() => + `共 ${filteredRuns.value.length} 条,目前第 ${currentPage.value} / ${totalPages.value} 页` +) watch( () => [listKeyword.value, activeModule.value, activeStatus.value], @@ -486,6 +492,12 @@ function selectStatus(val) { closeFilterPopover() } +function changeRefreshInterval(value) { + refreshInterval.value = Number(value) || DEFAULT_REFRESH_INTERVAL_MS + closeFilterPopover() + startPolling() +} + function resetFilters() { listKeyword.value = '' activeModule.value = '全部' @@ -564,7 +576,7 @@ function startPolling() { stopPolling() pollTimer = window.setInterval(() => { loadWorkRecords(false) - }, AGENT_RUN_POLL_INTERVAL_MS) + }, refreshInterval.value) } function stopPolling() { @@ -586,86 +598,104 @@ onBeforeUnmount(() => { - - diff --git a/web/src/components/layout/SidebarRail.vue b/web/src/components/layout/SidebarRail.vue index 2623f24..158f0f5 100644 --- a/web/src/components/layout/SidebarRail.vue +++ b/web/src/components/layout/SidebarRail.vue @@ -150,6 +150,7 @@ const { startDocumentInboxPolling, stopDocumentInboxPolling } = useDocumentCenterInbox() +let inboxInitialRefreshTimer = null const sidebarMeta = { overview: { label: '分析看板' }, @@ -159,7 +160,6 @@ const sidebarMeta = { policies: { label: '知识管理' }, audit: { label: '规则中心' }, digitalEmployees: { label: '数字员工' }, - logs: { label: '系统日志' }, employees: { label: '员工管理' }, settings: { label: '系统设置' } } @@ -173,8 +173,27 @@ const decoratedNavItems = computed(() => })) ) +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() + }, props.activeView === 'documents' ? 1200 : 6000) +} + onMounted(() => { - void refreshDocumentInbox() + scheduleInboxInitialRefresh() startDocumentInboxPolling() }) @@ -271,7 +290,18 @@ watch( } ) +watch( + () => props.activeView, + (activeView, previousView) => { + if (activeView === 'documents' && previousView !== 'documents') { + clearInboxInitialRefreshTimer() + void refreshDocumentInbox({ force: true }) + } + } +) + onBeforeUnmount(() => { + clearInboxInitialRefreshTimer() stopDocumentInboxPolling() closeCollapsedUserMenuNow() }) diff --git a/web/src/components/layout/TopBar.vue b/web/src/components/layout/TopBar.vue index d886e75..395f847 100644 --- a/web/src/components/layout/TopBar.vue +++ b/web/src/components/layout/TopBar.vue @@ -149,16 +149,6 @@
- - - - + return 'mdi mdi-circle-outline' +} + + + diff --git a/web/src/views/scripts/AuditView.js b/web/src/views/scripts/AuditView.js index 724827f..dfb387e 100644 --- a/web/src/views/scripts/AuditView.js +++ b/web/src/views/scripts/AuditView.js @@ -177,12 +177,6 @@ export default { () => normalizeText(selectedSkill.value?.ruleDocument?.file_name) || '未上传规则表' ) - const selectedSpreadsheetModeLabel = computed(() => { - if (selectedSkill.value?.isPreviewMock) { - return canEditSpreadsheetInline.value ? '可编辑' : '只读' - } - return canEditSpreadsheetInline.value ? '在线可编辑' : '只读' - }) const { versionSwitchTarget, versionTimelineOpen, @@ -438,11 +432,7 @@ export default { const auditDetailTopBar = computed(() => buildAuditDetailTopBar({ skill: selectedSkill.value, - usesJsonRiskRule: selectedSkillUsesJsonRisk.value, - usesSpreadsheetRule: selectedSkillUsesSpreadsheet.value, - spreadsheetModeLabel: selectedSpreadsheetModeLabel.value, - spreadsheetFileName: selectedSpreadsheetFileName.value, - canEditSpreadsheetInline: canEditSpreadsheetInline.value + usesJsonRiskRule: selectedSkillUsesJsonRisk.value }) ) @@ -711,7 +701,6 @@ export default { selectedSkillUsesSpreadsheet, selectedSkillUsesJsonRisk, selectedSpreadsheetFileName, - selectedSpreadsheetModeLabel, selectedVersionTimelineItems, selectedSpreadsheetChangeRecords, detailBusy, diff --git a/web/src/views/scripts/BudgetCenterView.js b/web/src/views/scripts/BudgetCenterView.js index 2e0b2ae..175778d 100644 --- a/web/src/views/scripts/BudgetCenterView.js +++ b/web/src/views/scripts/BudgetCenterView.js @@ -3,6 +3,7 @@ import { ElButton, ElInput, ElPagination, ElTable, ElTableColumn } from 'element import BudgetTrendChart from '../../components/charts/BudgetTrendChart.vue' import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue' +import TableLoadingState from '../../components/shared/TableLoadingState.vue' import { fetchBudgetSummary } from '../../services/budgets.js' import { fetchEmployeeMeta } from '../../services/employees.js' import { @@ -217,6 +218,7 @@ export default { components: { BudgetTrendChart, EnterpriseSelect, + TableLoadingState, ElButton, ElInput, ElPagination, @@ -238,7 +240,7 @@ export default { const budgetTableKeyword = ref('') const budgetRows = ref([]) const budgetSummary = ref(null) - const budgetLoading = ref(false) + const budgetLoading = ref(true) const budgetError = ref('') const canEditBudget = computed(() => canEditBudgetCenter(props.currentUser)) const canSwitchDepartments = computed(() => canSwitchBudgetDepartments(props.currentUser)) @@ -424,6 +426,7 @@ export default { } async function loadDepartments() { + budgetLoading.value = true try { const payload = await fetchEmployeeMeta() const options = Array.isArray(payload?.organizationOptions) ? payload.organizationOptions : [] diff --git a/web/src/views/scripts/LogsView.js b/web/src/views/scripts/LogsView.js index d530d9e..b89df98 100644 --- a/web/src/views/scripts/LogsView.js +++ b/web/src/views/scripts/LogsView.js @@ -5,8 +5,12 @@ import EnterpriseListPage from '../../components/shared/EnterpriseListPage.vue' import { useSystemState } from '../../composables/useSystemState.js' import { useToast } from '../../composables/useToast.js' import { fetchSystemLogEntries } from '../../services/systemLogs.js' -import { AGENT_RUN_POLL_INTERVAL_MS } from '../../utils/agentRunMonitor.js' import { isManagerUser } from '../../utils/accessControl.js' +import { + DEFAULT_REFRESH_INTERVAL_MS, + REFRESH_INTERVAL_OPTIONS, + formatRefreshInterval +} from '../../utils/refreshIntervalOptions.js' function formatDateTime(value) { if (!value) { @@ -79,6 +83,8 @@ export default { const pageSize = ref(10) const pageSizes = [10, 20, 50] const pageSizeOptions = pageSizes.map((size) => ({ label: `${size} 条/页`, value: size })) + const refreshInterval = ref(DEFAULT_REFRESH_INTERVAL_MS) + const refreshIntervalOptions = REFRESH_INTERVAL_OPTIONS let pollTimer = 0 const isAdmin = computed(() => isManagerUser(currentUser.value)) @@ -102,6 +108,7 @@ export default { const systemEventTypeFilterLabel = computed(() => systemEventTypeFilterOptions.value.find((item) => item.value === systemEventTypeFilter.value)?.label || '全部类型' ) + const refreshIntervalLabel = computed(() => formatRefreshInterval(refreshInterval.value)) const hasActiveFilters = computed(() => Boolean(systemSearchKeyword.value.trim() || systemLevelFilter.value || systemEventTypeFilter.value) ) @@ -175,6 +182,12 @@ export default { openFilterKey.value = '' } + function changeRefreshInterval(value) { + refreshInterval.value = Number(value) || DEFAULT_REFRESH_INTERVAL_MS + openFilterKey.value = '' + startPolling() + } + function resetFilters() { systemSearchKeyword.value = '' systemLevelFilter.value = '' @@ -211,7 +224,7 @@ export default { stopPolling() pollTimer = window.setInterval(() => { loadSystemLogs(false) - }, AGENT_RUN_POLL_INTERVAL_MS) + }, refreshInterval.value) } function stopPolling() { @@ -253,6 +266,7 @@ export default { return { changePageSize, + changeRefreshInterval, currentPage, filteredSystemLogEntries, formatDateTime, @@ -263,6 +277,9 @@ export default { openFilterKey, pageSize, pageSizeOptions, + refreshInterval, + refreshIntervalLabel, + refreshIntervalOptions, resetFilters, resolveSystemLevelTone, resolveSystemOutcomeTone, diff --git a/web/src/views/scripts/SettingsView.js b/web/src/views/scripts/SettingsView.js index 81a0521..0d638b5 100644 --- a/web/src/views/scripts/SettingsView.js +++ b/web/src/views/scripts/SettingsView.js @@ -1,5 +1,7 @@ import HermesEmployeeSettingsPanel from '../HermesEmployeeSettingsPanel.vue' import LlmSettingsPanel from '../LlmSettingsPanel.vue' +import LogDetailView from '../LogDetailView.vue' +import LogsView from '../LogsView.vue' import MailSettingsPanel from '../MailSettingsPanel.vue' import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue' import { useSettings } from '../../composables/useSettings.js' @@ -10,6 +12,8 @@ export default { HermesEmployeeSettingsPanel, EnterpriseSelect, LlmSettingsPanel, + LogDetailView, + LogsView, MailSettingsPanel }, setup() { diff --git a/web/src/views/scripts/auditViewDetailTopBar.js b/web/src/views/scripts/auditViewDetailTopBar.js index b264d07..9b1c703 100644 --- a/web/src/views/scripts/auditViewDetailTopBar.js +++ b/web/src/views/scripts/auditViewDetailTopBar.js @@ -10,11 +10,7 @@ function resolveRiskScoreCardColor(level) { export function buildAuditDetailTopBar({ skill, - usesJsonRiskRule = false, - usesSpreadsheetRule = false, - spreadsheetModeLabel = '', - spreadsheetFileName = '', - canEditSpreadsheetInline = false + usesJsonRiskRule = false } = {}) { if (!skill) return null @@ -39,15 +35,6 @@ export function buildAuditDetailTopBar({ : 'up', color: resolveRiskScoreCardColor(scoreLevel) }) - } else if (usesSpreadsheetRule) { - kpis.push({ - label: '编辑模式', - value: spreadsheetModeLabel, - unit: '', - meta: spreadsheetFileName, - trend: canEditSpreadsheetInline ? 'up' : 'down', - color: canEditSpreadsheetInline ? 'var(--success)' : '#64748b' - }) } return { diff --git a/web/src/views/scripts/auditViewModel.js b/web/src/views/scripts/auditViewModel.js index 0387a03..3eed016 100644 --- a/web/src/views/scripts/auditViewModel.js +++ b/web/src/views/scripts/auditViewModel.js @@ -7,7 +7,10 @@ import { VERSION_STATE_META } from './auditViewMetadata.js' import { + formatRiskRuleAge, + resolveRiskRuleFlow, resolveRiskRuleScore, + resolveRiskRuleScoreDetail, resolveRiskRuleScoreLabel, resolveRiskRuleScoreLevel, resolveRiskRuleSeverity, @@ -71,6 +74,7 @@ import { applyRiskRuleJsonState, resolveRiskRuleBusinessStage, resolveRiskRuleEnabled, + resolveLastOperationLabel, resolveRiskRuleOnlineMeta } from './auditViewRiskRuleState.js' import { diff --git a/web/src/views/scripts/auditViewRiskRuleState.js b/web/src/views/scripts/auditViewRiskRuleState.js index 3e69fe1..056f7db 100644 --- a/web/src/views/scripts/auditViewRiskRuleState.js +++ b/web/src/views/scripts/auditViewRiskRuleState.js @@ -20,6 +20,7 @@ import { } from './auditViewDataUtils.js' import { formatDateTime } from './auditViewFormatters.js' import { + buildRiskListSubtitle, resolveRiskRuleCategory, resolveRiskRuleDescription, resolveRiskRuleSourceRef @@ -83,7 +84,7 @@ export function resolveRiskRuleOnlineMeta(statusValue) { return { label: '待上线', tone: 'draft', online: false } } -function resolveLastOperationLabel(source, fallback = {}) { +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' @@ -129,9 +130,12 @@ export function applyRiskRuleJsonState(target, payload, apiPayload) { let publishedAt = target.publishedAt || '-' if (apiPayload?.recent_versions) { - const history = buildHistory(apiPayload.recent_versions, { ...target, config_json: payload }) - const publishedVersionObj = history.find((item) => item.isPublished || item.lifecycleState === 'published') - publishedAt = publishedVersionObj ? publishedVersionObj.time : (apiPayload?.latest_review?.reviewed_at ? formatDateTime(apiPayload.latest_review.reviewed_at) : '-') + 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) } diff --git a/web/tests/accessControl.test.mjs b/web/tests/accessControl.test.mjs index 9479727..f17d843 100644 --- a/web/tests/accessControl.test.mjs +++ b/web/tests/accessControl.test.mjs @@ -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, 'approval'), false) assert.equal(canAccessAppView(adminUser, 'archive'), false) + assert.equal(canAccessAppView(adminUser, 'logs'), false) assert.equal(canAccessAppView(adminUser, 'documents'), true) }) diff --git a/web/tests/minimum-visible-state.test.mjs b/web/tests/minimum-visible-state.test.mjs new file mode 100644 index 0000000..bb2ce73 --- /dev/null +++ b/web/tests/minimum-visible-state.test.mjs @@ -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() +}) diff --git a/web/tests/navigation-route-resolution.test.mjs b/web/tests/navigation-route-resolution.test.mjs index c9af1f8..21dae01 100644 --- a/web/tests/navigation-route-resolution.test.mjs +++ b/web/tests/navigation-route-resolution.test.mjs @@ -8,9 +8,9 @@ import { } from '../src/composables/useNavigation.js' 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-policies', meta: { appView: 'logs' } }), 'policies') + assert.equal(resolveAppViewFromRoute({ name: 'app-policies', meta: { appView: 'settings' } }), 'policies') } function testFallsBackToValidMeta() { @@ -19,7 +19,7 @@ function testFallsBackToValidMeta() { } function testResolvesMainRouteNames() { - assert.equal(resolveTargetRouteName('logs'), 'app-logs') + assert.equal(resolveTargetRouteName('logs'), 'app-settings') assert.equal(resolveTargetRouteName('policies'), 'app-policies') assert.equal(resolveTargetRouteName('requests'), 'app-overview') assert.equal(resolveTargetRouteName('approval'), 'app-overview') @@ -31,7 +31,8 @@ function testLegacyCentersAreRemovedFromNavigation() { assert.equal(appViews.includes('requests'), false) assert.equal(appViews.includes('approval'), 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() { diff --git a/web/tests/refresh-interval-controls.test.mjs b/web/tests/refresh-interval-controls.test.mjs new file mode 100644 index 0000000..0946047 --- /dev/null +++ b/web/tests/refresh-interval-controls.test.mjs @@ -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/) +}) diff --git a/web/tests/settings-system-logs-section.test.mjs b/web/tests/settings-system-logs-section.test.mjs new file mode 100644 index 0000000..c699c5a --- /dev/null +++ b/web/tests/settings-system-logs-section.test.mjs @@ -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, //) + 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' \} \}\)/) +}) diff --git a/web/vite.config.js b/web/vite.config.js index 1a8132f..03ffaa9 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -1053,17 +1053,31 @@ export default defineConfig({ if (!id.includes('node_modules')) { return undefined } - if (id.includes('element-plus') || id.includes('@element-plus')) { + 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 (id.includes('echarts') || id.includes('zrender')) { + if (normalizedId.includes('echarts') || normalizedId.includes('zrender')) { return 'vendor-echarts' } - if (id.includes('@vueuse')) { - return 'vendor-vueuse' + if (normalizedId.includes('@antv/g6')) { + return 'vendor-g6' } - if (id.includes('primeicons') || id.includes('primevue')) { - return 'vendor-prime' + 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' }