feat: 新增数字员工管理页面与工作台首页重构
后端优化 agent 资产种子初始化和常量配置,前端新增数字员工 视图和调度对话框组件,重构个人工作台首页布局和洞察面板, 完善审计页面数字员工详情和运行时模型,优化侧边栏导航和图 标配置,新增工作台摘要和工作台数据模块,补充单元测试。
This commit is contained in:
BIN
web/src/assets/homepage_backgraound.png
Normal file
BIN
web/src/assets/homepage_backgraound.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -135,6 +135,7 @@
|
||||
.main.archive-main,
|
||||
.main.policies-main,
|
||||
.main.audit-main,
|
||||
.main.digital-employees-main,
|
||||
.main.logs-main,
|
||||
.main.employees-main,
|
||||
.main.settings-main {
|
||||
@@ -151,10 +152,12 @@
|
||||
.workarea { min-height: 0; overflow: auto; padding: 24px; }
|
||||
.workarea.requests-workarea,
|
||||
.workarea.documents-workarea,
|
||||
.workarea.workbench-workarea,
|
||||
.workarea.approval-workarea,
|
||||
.workarea.archive-workarea,
|
||||
.workarea.policies-workarea,
|
||||
.workarea.audit-workarea,
|
||||
.workarea.digital-employees-workarea,
|
||||
.workarea.logs-workarea,
|
||||
.workarea.employees-workarea,
|
||||
.workarea.settings-workarea {
|
||||
@@ -162,6 +165,10 @@
|
||||
overflow: hidden;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
.workarea.workbench-workarea {
|
||||
overflow: hidden;
|
||||
padding: 12px 14px 14px;
|
||||
}
|
||||
.workarea.settings-workarea {
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
@@ -195,6 +202,7 @@
|
||||
}
|
||||
|
||||
.workarea { padding: 18px 16px 28px; }
|
||||
.workarea.workbench-workarea { overflow: auto; padding: 14px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
.digital-schedule-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1200;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
padding: 32px;
|
||||
background: rgba(15, 23, 42, 0.28);
|
||||
}
|
||||
|
||||
.digital-schedule-dialog {
|
||||
width: min(520px, calc(100vw - 64px));
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.22);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.digital-schedule-head,
|
||||
.digital-schedule-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 18px 20px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.digital-schedule-head h3 {
|
||||
margin: 4px 0 0;
|
||||
color: #0f172a;
|
||||
font-size: 18px;
|
||||
font-weight: 820;
|
||||
}
|
||||
|
||||
.digital-schedule-kicker {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 760;
|
||||
}
|
||||
|
||||
.digital-schedule-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid #dbe4ee;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.digital-schedule-body {
|
||||
padding: 18px 20px 20px;
|
||||
}
|
||||
|
||||
.digital-schedule-mode-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.digital-schedule-mode {
|
||||
min-height: 36px;
|
||||
border: 1px solid #dbe4ee;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
font-weight: 760;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.digital-schedule-mode.active {
|
||||
border-color: rgba(var(--theme-primary-rgb), 0.42);
|
||||
background: var(--theme-primary-soft);
|
||||
color: var(--theme-primary-active);
|
||||
}
|
||||
|
||||
.digital-schedule-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.digital-schedule-fields label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
font-weight: 760;
|
||||
}
|
||||
|
||||
.digital-schedule-fields input,
|
||||
.digital-schedule-fields select {
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
border: 1px solid #dbe4ee;
|
||||
border-radius: 6px;
|
||||
padding: 0 10px;
|
||||
color: #0f172a;
|
||||
font: inherit;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.digital-schedule-cron-field {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.digital-schedule-preview {
|
||||
margin: 16px 0 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.digital-schedule-error {
|
||||
margin: 10px 0 0;
|
||||
color: #b91c1c;
|
||||
font-size: 13px;
|
||||
font-weight: 760;
|
||||
}
|
||||
|
||||
.digital-schedule-actions {
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.digital-schedule-actions .success-action {
|
||||
border-color: var(--success-line);
|
||||
background: var(--success-soft);
|
||||
color: var(--success-hover);
|
||||
}
|
||||
|
||||
.digital-schedule-dialog-enter-active,
|
||||
.digital-schedule-dialog-leave-active {
|
||||
transition: opacity 160ms ease;
|
||||
}
|
||||
|
||||
.digital-schedule-dialog-enter-from,
|
||||
.digital-schedule-dialog-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.digital-schedule-overlay {
|
||||
align-items: flex-end;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.digital-schedule-dialog {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.digital-schedule-mode-grid,
|
||||
.digital-schedule-fields {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
135
web/src/assets/styles/components/personal-workbench-insights.css
Normal file
135
web/src/assets/styles/components/personal-workbench-insights.css
Normal file
@@ -0,0 +1,135 @@
|
||||
.side-column {
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 0.94fr) minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
padding: 10px 12px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.side-panel .section-head {
|
||||
min-height: 24px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.side-panel .section-head h2 {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.side-card-head {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.detail-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
min-height: 24px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--workbench-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-action:hover {
|
||||
color: var(--workbench-primary-active);
|
||||
}
|
||||
|
||||
.detail-action i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.expense-stats-panel,
|
||||
.usage-profile-panel {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.insight-metric-list,
|
||||
.insight-profile-list {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-auto-rows: minmax(0, 1fr);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.insight-metric-row,
|
||||
.insight-profile-card {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 0;
|
||||
padding: 6px 0;
|
||||
border-top: 1px solid var(--workbench-line-soft);
|
||||
}
|
||||
|
||||
.insight-metric-row:first-child,
|
||||
.insight-profile-card:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.insight-metric-label,
|
||||
.insight-profile-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--workbench-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.insight-metric-value,
|
||||
.insight-profile-value {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--workbench-ink);
|
||||
font-size: 14px;
|
||||
font-weight: 850;
|
||||
line-height: 1.15;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.insight-metric-value small,
|
||||
.insight-profile-value small {
|
||||
margin-left: 2px;
|
||||
color: var(--workbench-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.insight-metric-row--amount .insight-metric-value {
|
||||
color: var(--workbench-primary-active);
|
||||
}
|
||||
|
||||
.insight-metric-row--warning .insight-metric-value {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.insight-metric-row--info .insight-metric-value {
|
||||
color: var(--workbench-chart-blue);
|
||||
}
|
||||
|
||||
.insight-profile-icon,
|
||||
.insight-profile-hint {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.insight-profile-copy {
|
||||
min-width: 0;
|
||||
display: contents;
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/* 1080p / 小高度屏:进一步压缩 AI 助手卡片高度 */
|
||||
@media (max-height: 980px) {
|
||||
.workbench {
|
||||
--hero-padding-top: 20px;
|
||||
--hero-padding-bottom: 12px;
|
||||
--hero-title-size: 28px;
|
||||
--hero-copy-gap: 5px;
|
||||
--composer-min-height: 108px;
|
||||
--composer-textarea-height: 48px;
|
||||
--composer-padding-block: 10px;
|
||||
--quick-prompts-gap-top: 8px;
|
||||
--capability-row-height: 96px;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.assistant-hero {
|
||||
--assistant-art-width: min(560px, 42vw);
|
||||
padding: var(--hero-padding-top) 18px var(--hero-padding-bottom) 44px;
|
||||
}
|
||||
|
||||
.assistant-copy p {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.assistant-composer textarea {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.composer-icon-button,
|
||||
.composer-related-button,
|
||||
.composer-send-button {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.composer-send-button {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 2K 宽屏但内容区仍偏高时,略收紧(避免 hero 独占过多纵向空间) */
|
||||
@media (min-width: 1920px) and (max-height: 1100px) {
|
||||
.workbench {
|
||||
--hero-padding-top: 22px;
|
||||
--hero-title-size: 29px;
|
||||
--composer-min-height: 114px;
|
||||
--composer-textarea-height: 50px;
|
||||
--capability-row-height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.workbench {
|
||||
grid-template-rows: auto var(--capability-row-height) minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.assistant-hero {
|
||||
--assistant-art-width: min(620px, 44vw);
|
||||
--assistant-art-x: 48px;
|
||||
--assistant-art-y: -10px;
|
||||
padding: var(--hero-padding-top) 18px var(--hero-padding-bottom) 44px;
|
||||
}
|
||||
|
||||
.assistant-copy {
|
||||
width: min(1040px, 92%);
|
||||
}
|
||||
|
||||
.assistant-copy h1 {
|
||||
font-size: 33px;
|
||||
}
|
||||
|
||||
.capability-card {
|
||||
padding: 17px 12px 17px 22px;
|
||||
}
|
||||
|
||||
.capability-copy {
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.workbench-content-grid {
|
||||
grid-template-columns: minmax(300px, 0.92fr) minmax(480px, 1.34fr) minmax(270px, 0.76fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.workbench-card {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.progress-row {
|
||||
grid-template-columns: minmax(126px, 0.9fr) minmax(270px, 1.35fr) minmax(86px, auto);
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.assistant-hero {
|
||||
--assistant-art-width: min(540px, 50vw);
|
||||
--assistant-art-x: 36px;
|
||||
--assistant-art-y: -8px;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.97) 0%, rgba(255, 255, 255, 0.9) 56%, rgba(255, 255, 255, 0.22) 100%),
|
||||
linear-gradient(135deg, #ffffff 0%, color-mix(in srgb, var(--workbench-primary-soft) 48%, #ffffff) 58%, color-mix(in srgb, var(--workbench-secondary) 8%, #ffffff) 100%);
|
||||
}
|
||||
|
||||
.assistant-copy {
|
||||
width: min(820px, 92%);
|
||||
}
|
||||
|
||||
.capability-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.workbench-content-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.side-column {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.workbench {
|
||||
height: auto;
|
||||
grid-template-rows: none;
|
||||
gap: 14px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.assistant-hero {
|
||||
min-height: auto;
|
||||
--assistant-art-width: min(380px, 78vw);
|
||||
--assistant-art-x: 12px;
|
||||
--assistant-art-y: -6px;
|
||||
padding: 24px 18px 20px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.94) 100%),
|
||||
color-mix(in srgb, var(--workbench-primary-soft) 22%, #ffffff);
|
||||
}
|
||||
|
||||
.assistant-copy {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.assistant-copy h1 {
|
||||
max-width: 320px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.assistant-composer {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.composer-toolbar {
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.composer-count {
|
||||
order: 4;
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.composer-send-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.capability-grid,
|
||||
.side-column {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.todo-row,
|
||||
.progress-row {
|
||||
grid-template-columns: 1fr;
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.todo-row {
|
||||
grid-template-columns: 48px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.todo-meta,
|
||||
.progress-result {
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.todo-meta {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.progress-steps {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -207,6 +207,11 @@
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.nav-btn :deep(svg text) {
|
||||
fill: currentColor;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
@@ -1810,6 +1810,40 @@
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.digital-worker-source-card {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.digital-worker-source-card .card-head {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.digital-worker-source-editor {
|
||||
width: 100%;
|
||||
min-height: 360px;
|
||||
resize: vertical;
|
||||
border: 1px solid #dbe4ef;
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.7;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.digital-worker-source-editor:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.14);
|
||||
}
|
||||
|
||||
.digital-worker-source-editor[readonly] {
|
||||
color: #cbd5e1;
|
||||
background: #1e293b;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.risk-rule-create-form,
|
||||
.json-risk-meta-grid {
|
||||
|
||||
94
web/src/assets/styles/views/digital-employees-view.css
Normal file
94
web/src/assets/styles/views/digital-employees-view.css
Normal file
@@ -0,0 +1,94 @@
|
||||
.digital-employees-view {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.digital-employees-list {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.digital-employees-list .table-wrap {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.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%; }
|
||||
.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-refresh-action i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.skill-type-pill {
|
||||
border-color: #dbeafe;
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.digital-employee-detail {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.digital-employee-detail .detail-scroll {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.digital-employee-detail-actions .detail-action-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.digital-employee-detail-actions .success-action {
|
||||
border-color: var(--success-line);
|
||||
background: var(--success-soft);
|
||||
color: var(--success-hover);
|
||||
}
|
||||
|
||||
.digital-employee-detail-actions .enable-action.is-on {
|
||||
border-color: #fed7aa;
|
||||
background: #fff7ed;
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.digital-employees-table {
|
||||
min-width: 1040px;
|
||||
}
|
||||
|
||||
.digital-employee-detail-actions {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.digital-employee-detail-actions .detail-action-group {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.digital-employee-detail-actions .detail-action-group button {
|
||||
flex: 1 1 0;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
@@ -258,7 +258,7 @@
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
min-width: 690px;
|
||||
min-width: 780px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
@@ -365,10 +365,9 @@ th {
|
||||
.state-cell {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.state-time {
|
||||
.ingest-time-cell {
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
|
||||
105
web/src/components/audit/AuditDigitalEmployeeDetail.vue
Normal file
105
web/src/components/audit/AuditDigitalEmployeeDetail.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<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">
|
||||
<section class="json-risk-main-stage">
|
||||
<article class="detail-card panel json-risk-summary-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h3>基本信息</h3>
|
||||
<p>展示技能编号、维护人、执行计划和当前版本。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="json-risk-meta-grid">
|
||||
<div
|
||||
v-for="row in basicRows"
|
||||
:key="row.label"
|
||||
class="json-risk-meta-item"
|
||||
>
|
||||
<span class="json-risk-meta-label">{{ row.label }}</span>
|
||||
<span class="json-risk-meta-value">{{ row.value || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="detail-card panel json-risk-description-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h3>功能说明</h3>
|
||||
<p>面向管理员查看这个技能解决什么问题。</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="json-risk-description-text">
|
||||
{{ digitalEmployee.description || selectedSkill.summary || '暂无功能说明。' }}
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="detail-card panel json-risk-flow-card digital-worker-source-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h3>Skills Markdown 源文件</h3>
|
||||
<p>管理员可直接修改当前技能源文件内容。</p>
|
||||
</div>
|
||||
<button
|
||||
class="mini-btn primary"
|
||||
type="button"
|
||||
:disabled="!canEdit || detailBusy"
|
||||
@click="emit('save-source')"
|
||||
>
|
||||
<i class="mdi mdi-content-save-outline"></i>
|
||||
<span>{{ actionState === 'save-digital-source' ? '保存中...' : '保存' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
v-model="selectedSkill.markdownContent"
|
||||
class="digital-worker-source-editor"
|
||||
:readonly="!canEdit || detailBusy"
|
||||
spellcheck="false"
|
||||
aria-label="Skills Markdown 源文件"
|
||||
></textarea>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'AuditDigitalEmployeeDetail'
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
selectedSkill: { type: Object, required: true },
|
||||
canEdit: { type: Boolean, default: false },
|
||||
detailBusy: { type: Boolean, default: false },
|
||||
actionState: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['save-source'])
|
||||
|
||||
const digitalEmployee = computed(() => props.selectedSkill.digitalEmployee || {})
|
||||
const basicRows = computed(() => digitalEmployee.value.basicRows || [])
|
||||
</script>
|
||||
|
||||
<style scoped src="../../assets/styles/views/audit-view.css"></style>
|
||||
<style scoped src="../../assets/styles/views/audit-view-part2.css"></style>
|
||||
139
web/src/components/audit/DigitalEmployeeScheduleDialog.vue
Normal file
139
web/src/components/audit/DigitalEmployeeScheduleDialog.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="digital-schedule-dialog">
|
||||
<div
|
||||
v-if="open"
|
||||
class="digital-schedule-overlay"
|
||||
@click.self="emit('close')"
|
||||
>
|
||||
<section class="digital-schedule-dialog panel" role="dialog" aria-modal="true">
|
||||
<header class="digital-schedule-head">
|
||||
<div>
|
||||
<span class="digital-schedule-kicker">定时设置</span>
|
||||
<h3>{{ targetName || '数字员工' }}</h3>
|
||||
</div>
|
||||
<button
|
||||
class="digital-schedule-close"
|
||||
type="button"
|
||||
:disabled="busy"
|
||||
aria-label="关闭定时设置"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<i class="mdi mdi-close"></i>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="digital-schedule-body">
|
||||
<div class="digital-schedule-mode-grid">
|
||||
<button
|
||||
v-for="mode in scheduleModes"
|
||||
:key="mode.value"
|
||||
class="digital-schedule-mode"
|
||||
:class="{ active: modelValue.mode === mode.value }"
|
||||
type="button"
|
||||
:disabled="busy"
|
||||
@click="updateField('mode', mode.value)"
|
||||
>
|
||||
{{ mode.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="digital-schedule-fields">
|
||||
<label v-if="modelValue.mode === 'daily' || modelValue.mode === 'weekly'">
|
||||
<span>执行时间</span>
|
||||
<input
|
||||
:value="modelValue.time"
|
||||
type="time"
|
||||
:disabled="busy"
|
||||
@input="updateField('time', $event.target.value)"
|
||||
>
|
||||
</label>
|
||||
|
||||
<label v-if="modelValue.mode === 'weekly'">
|
||||
<span>执行日期</span>
|
||||
<select
|
||||
:value="modelValue.weekday"
|
||||
:disabled="busy"
|
||||
@change="updateField('weekday', $event.target.value)"
|
||||
>
|
||||
<option
|
||||
v-for="weekday in weekdayOptions"
|
||||
:key="weekday.value"
|
||||
:value="weekday.value"
|
||||
>
|
||||
{{ weekday.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label v-if="modelValue.mode === 'custom'" class="digital-schedule-cron-field">
|
||||
<span>Cron 表达式</span>
|
||||
<input
|
||||
:value="modelValue.cron"
|
||||
type="text"
|
||||
:disabled="busy"
|
||||
placeholder="例如:0 9 * * *"
|
||||
@input="updateField('cron', $event.target.value)"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p class="digital-schedule-preview">
|
||||
当前计划:{{ previewLabel }}
|
||||
</p>
|
||||
<p v-if="errorMessage" class="digital-schedule-error">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<footer class="digital-schedule-actions">
|
||||
<button class="minor-action" type="button" :disabled="busy" @click="emit('close')">
|
||||
取消
|
||||
</button>
|
||||
<button class="minor-action success-action" type="button" :disabled="!canSave || busy" @click="emit('save')">
|
||||
{{ busy ? '保存中...' : '保存设置' }}
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
DIGITAL_EMPLOYEE_SCHEDULE_MODES,
|
||||
DIGITAL_EMPLOYEE_WEEKDAY_OPTIONS
|
||||
} from '../../views/scripts/digitalEmployeeScheduleModel.js'
|
||||
|
||||
defineOptions({
|
||||
name: 'DigitalEmployeeScheduleDialog'
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({ mode: 'manual', time: '00:00', weekday: '1', cron: '' })
|
||||
},
|
||||
targetName: { type: String, default: '' },
|
||||
previewLabel: { type: String, default: '手动触发' },
|
||||
errorMessage: { type: String, default: '' },
|
||||
busy: { type: Boolean, default: false },
|
||||
canSave: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close', 'save'])
|
||||
|
||||
const scheduleModes = DIGITAL_EMPLOYEE_SCHEDULE_MODES
|
||||
const weekdayOptions = DIGITAL_EMPLOYEE_WEEKDAY_OPTIONS
|
||||
|
||||
function updateField(field, value) {
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
[field]: value
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="../../assets/styles/components/digital-employee-schedule-dialog.css"></style>
|
||||
@@ -1,37 +1,71 @@
|
||||
<template>
|
||||
<section class="workbench">
|
||||
<template>
|
||||
<section class="workbench" aria-label="个人工作台">
|
||||
<PanelHead
|
||||
v-if="showHeader"
|
||||
eyebrow="Personal Workspace"
|
||||
title="个人工作台"
|
||||
note="把今天要处理的待办、报销进度和制度更新集中到一个入口。"
|
||||
note="把费用申请、报销进度、制度问答和待办处理集中到一个入口。"
|
||||
/>
|
||||
|
||||
<article class="panel assistant-hero">
|
||||
<div class="assistant-visual" aria-hidden="true">
|
||||
<span class="assistant-glow"></span>
|
||||
<img class="assistant-image" :src="robotAssistant" alt="" />
|
||||
</div>
|
||||
|
||||
<article class="panel assistant-hero" :style="{ '--assistant-bg-image': `url(${homepageBackground})` }">
|
||||
<div class="assistant-copy">
|
||||
<h3>嗨,{{ assistantGreetingName }},描述您想做的事,AI 会直接帮您处理</h3>
|
||||
<p>我会自动识别您的意图,协助完成费用申请、报销、查询和制度问答等业务工作,耐心把事情推进到可执行的下一步。</p>
|
||||
<h1>你的专属 <span>AI 财务助手</span></h1>
|
||||
<p>智能理解财务业务,提供数据洞察与方案建议,高效处理日常事务</p>
|
||||
|
||||
<div class="assistant-input">
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
class="assistant-file-input"
|
||||
type="file"
|
||||
multiple
|
||||
accept=".pdf,.jpg,.jpeg,.png,.webp,.doc,.docx,.xls,.xlsx"
|
||||
@change="handleWorkbenchFilesChange"
|
||||
/>
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
class="assistant-file-input"
|
||||
type="file"
|
||||
multiple
|
||||
accept=".pdf,.jpg,.jpeg,.png,.webp,.doc,.docx,.xls,.xlsx"
|
||||
@change="handleWorkbenchFilesChange"
|
||||
/>
|
||||
|
||||
<div class="assistant-composer">
|
||||
<textarea
|
||||
ref="assistantInputRef"
|
||||
v-model="assistantDraft"
|
||||
rows="1"
|
||||
placeholder="例如:我昨天请客户吃饭花了 860 元,还打车去了客户公司"
|
||||
maxlength="1000"
|
||||
rows="2"
|
||||
placeholder="请输入费用申请、报销问题、预算查询或制度问答..."
|
||||
@keydown.enter.prevent="handleWorkbenchEnter"
|
||||
/>
|
||||
|
||||
<div class="composer-toolbar">
|
||||
<button
|
||||
type="button"
|
||||
class="composer-icon-button"
|
||||
title="上传附件"
|
||||
aria-label="上传附件"
|
||||
:disabled="Boolean(pendingAction)"
|
||||
@click="triggerFileUpload"
|
||||
>
|
||||
<i class="mdi mdi-paperclip"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="composer-related-button"
|
||||
:disabled="Boolean(pendingAction)"
|
||||
@click="triggerFileUpload"
|
||||
>
|
||||
<i class="mdi mdi-source-branch"></i>
|
||||
<span>关联单据</span>
|
||||
<i class="mdi mdi-chevron-down"></i>
|
||||
</button>
|
||||
|
||||
<span class="composer-count">{{ assistantDraft.length }}/1000</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="composer-send-button"
|
||||
:disabled="Boolean(pendingAction)"
|
||||
:aria-label="pendingAction === 'expense' ? '处理中' : expenseActionLabel"
|
||||
@click="handleExpenseConversationAction"
|
||||
>
|
||||
<i :class="pendingAction === 'expense' ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-send'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedFiles.length" class="assistant-file-strip">
|
||||
@@ -40,115 +74,206 @@
|
||||
<button type="button" class="assistant-file-clear" @click="clearSelectedFiles">清空</button>
|
||||
</div>
|
||||
|
||||
<div class="assistant-tools">
|
||||
<button type="button" class="ghost-action" :disabled="Boolean(pendingAction)" @click="triggerFileUpload">
|
||||
<i class="mdi mdi-upload-outline"></i>
|
||||
<span>上传票据</span>
|
||||
</button>
|
||||
<div class="quick-prompts" aria-label="常用提问">
|
||||
<span>常用提问:</span>
|
||||
<button
|
||||
v-for="prompt in quickPromptItems"
|
||||
:key="prompt"
|
||||
type="button"
|
||||
class="hero-action"
|
||||
:disabled="Boolean(pendingAction)"
|
||||
@click="handleExpenseConversationAction"
|
||||
@click="applyQuickPrompt(prompt)"
|
||||
>
|
||||
<i :class="pendingAction === 'expense' ? 'mdi mdi-loading mdi-spin' : expenseActionIcon"></i>
|
||||
<span>{{ pendingAction === 'expense' ? '处理中...' : expenseActionLabel }}</span>
|
||||
{{ prompt }}
|
||||
</button>
|
||||
<button type="button" class="quick-more" @click="emit('open-assistant')">
|
||||
更多
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div class="workbench-grid">
|
||||
<article class="panel list-panel">
|
||||
<div class="section-head">
|
||||
<div class="title-with-badge">
|
||||
<h3>报销待办</h3>
|
||||
<span class="alert-badge">{{ todoAlertCount }}</span>
|
||||
</div>
|
||||
<button type="button" class="link-action">查看全部 <i class="mdi mdi-chevron-right"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="list-body">
|
||||
<div v-for="item in todoItems" :key="item.title" class="todo-row">
|
||||
<WorkbenchListIcon
|
||||
:icon-key="item.iconKey"
|
||||
:color="item.color"
|
||||
:accent="item.accent"
|
||||
/>
|
||||
|
||||
<div class="todo-copy">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<p class="todo-advice">
|
||||
<span class="todo-advice-label">{{ item.tipLabel }}</span>
|
||||
<span class="todo-advice-text">{{ item.suggestion }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button type="button" class="row-action" @click="emit('open-assistant')">{{ item.action }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel list-panel">
|
||||
<div class="section-head">
|
||||
<div class="title-with-badge">
|
||||
<h3>报销进度</h3>
|
||||
<span class="alert-badge">{{ progressAlertCount }}</span>
|
||||
</div>
|
||||
<button type="button" class="link-action">查看全部 <i class="mdi mdi-chevron-right"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="list-body">
|
||||
<div v-for="item in progressItems" :key="item.id" class="progress-row">
|
||||
<WorkbenchListIcon
|
||||
:icon-key="item.iconKey"
|
||||
:color="item.color"
|
||||
:accent="item.accent"
|
||||
/>
|
||||
|
||||
<div class="todo-copy progress-copy">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<p>提交时间:{{ item.date }}</p>
|
||||
</div>
|
||||
|
||||
<strong class="progress-amount">{{ item.amount }}</strong>
|
||||
<span class="progress-status" :class="item.tone">{{ item.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<div class="capability-grid" aria-label="AI 财务助手能力">
|
||||
<button
|
||||
v-for="item in assistantCapabilities"
|
||||
:key="item.title"
|
||||
type="button"
|
||||
class="capability-card panel"
|
||||
:class="`capability-card--${item.tone}`"
|
||||
@click="openPromptAssistant(item.prompt)"
|
||||
>
|
||||
<span class="capability-icon"><i :class="item.icon"></i></span>
|
||||
<span class="capability-copy">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<small>{{ item.primary }}</small>
|
||||
<small>{{ item.secondary }}</small>
|
||||
</span>
|
||||
<i class="mdi mdi-chevron-right capability-arrow"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<article class="panel policy-panel">
|
||||
<div class="section-head">
|
||||
<h3>最新报销制度</h3>
|
||||
<button type="button" class="link-action">查看全部 <i class="mdi mdi-chevron-right"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="policy-table">
|
||||
<div class="policy-head policy-row">
|
||||
<span class="policy-title-cell">制度名称</span>
|
||||
<span class="policy-summary-cell">摘要</span>
|
||||
<span class="policy-date-cell">发布日期</span>
|
||||
<div class="workbench-content-grid">
|
||||
<article class="panel workbench-card todo-panel">
|
||||
<div class="section-head">
|
||||
<div class="title-with-badge">
|
||||
<h2>我的待办</h2>
|
||||
<span class="soft-badge">{{ todoAlertCount }}</span>
|
||||
</div>
|
||||
<button type="button" class="link-action">全部待办 <i class="mdi mdi-chevron-right"></i></button>
|
||||
</div>
|
||||
|
||||
<div v-for="item in policyItems" :key="item.name" class="policy-row">
|
||||
<strong class="policy-title-cell">{{ item.name }}</strong>
|
||||
<span class="policy-summary-cell">{{ item.summary }}</span>
|
||||
<span class="policy-date-cell">{{ item.date }}</span>
|
||||
<div class="todo-list">
|
||||
<button
|
||||
v-for="item in visibleTodoItems"
|
||||
:key="item.title"
|
||||
type="button"
|
||||
class="todo-row"
|
||||
@click="openPromptAssistant(`帮我处理:${item.title},${item.description}`)"
|
||||
>
|
||||
<WorkbenchListIcon
|
||||
:icon-key="item.iconKey"
|
||||
:color="item.color"
|
||||
:accent="item.accent"
|
||||
/>
|
||||
<span class="todo-copy">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<small>{{ item.description }}</small>
|
||||
</span>
|
||||
<span class="todo-meta">
|
||||
<span class="todo-status" :class="`todo-status--${item.statusTone}`">{{ item.status }}</span>
|
||||
<small>{{ item.due }}</small>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</article>
|
||||
|
||||
<article class="panel workbench-card progress-panel">
|
||||
<div class="section-head">
|
||||
<h2>费用进度</h2>
|
||||
<button type="button" class="link-action">全部进度 <i class="mdi mdi-chevron-right"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="progress-list">
|
||||
<button
|
||||
v-for="item in visibleProgressItems"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="progress-row"
|
||||
@click="openPromptAssistant(`查询 ${item.id} 的费用进度`)"
|
||||
>
|
||||
<span class="progress-identity">
|
||||
<strong>{{ item.id }}</strong>
|
||||
<small>{{ item.title }}</small>
|
||||
</span>
|
||||
|
||||
<span class="progress-steps" aria-hidden="true">
|
||||
<span
|
||||
v-for="(step, index) in progressSteps"
|
||||
:key="step"
|
||||
class="progress-step"
|
||||
:class="{
|
||||
'is-done': index < item.activeStep,
|
||||
'is-current': index === item.activeStep,
|
||||
'is-future': index > item.activeStep
|
||||
}"
|
||||
>
|
||||
<i :class="index <= item.activeStep ? 'mdi mdi-check' : 'mdi mdi-minus'"></i>
|
||||
<small>{{ step }}</small>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="progress-result">
|
||||
<span class="progress-status" :class="`progress-status--${item.statusTone}`">{{ item.status }}</span>
|
||||
<strong>{{ item.amount }}</strong>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
|
||||
<aside class="side-column">
|
||||
<article class="panel workbench-card side-panel expense-stats-panel">
|
||||
<div class="section-head side-card-head">
|
||||
<h2>费用统计</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="detail-action"
|
||||
@click="openPromptAssistant('查看我的费用统计详情,并说明本月报销金额、审批中和待付款的主要变化。')"
|
||||
>
|
||||
<span>查看详情</span>
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="insight-metric-list" aria-label="费用统计">
|
||||
<div
|
||||
v-for="item in visibleExpenseStatItems"
|
||||
:key="item.key"
|
||||
class="insight-metric-row"
|
||||
:class="`insight-metric-row--${item.tone}`"
|
||||
>
|
||||
<span class="insight-metric-label">{{ item.label }}</span>
|
||||
<strong class="insight-metric-value">
|
||||
{{ item.value }}<small v-if="item.unit">{{ item.unit }}</small>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel workbench-card side-panel usage-profile-panel">
|
||||
<div class="section-head side-card-head">
|
||||
<h2>费用画像</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="detail-action"
|
||||
@click="openPromptAssistant('查看我的费用画像详情,并总结 AI 使用、提单效率和预审通过率。')"
|
||||
>
|
||||
<span>查看详情</span>
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="insight-profile-list" aria-label="费用画像">
|
||||
<div
|
||||
v-for="metric in visibleUsageProfileMetrics"
|
||||
:key="metric.key"
|
||||
class="insight-profile-card"
|
||||
:class="`insight-profile-card--${metric.tone}`"
|
||||
>
|
||||
<span class="insight-profile-icon" aria-hidden="true">
|
||||
<i :class="metric.icon"></i>
|
||||
</span>
|
||||
<div class="insight-profile-copy">
|
||||
<span class="insight-profile-label">{{ metric.label }}</span>
|
||||
<strong class="insight-profile-value">
|
||||
{{ metric.value }}<small>{{ metric.unit }}</small>
|
||||
</strong>
|
||||
<span class="insight-profile-hint">{{ metric.hint }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import PanelHead from '../shared/PanelHead.vue'
|
||||
import WorkbenchListIcon from '../shared/WorkbenchListIcon.vue'
|
||||
import robotAssistant from '../../assets/robot-helper.png'
|
||||
import homepageBackground from '../../assets/homepage_backgraound.png'
|
||||
import { useSystemState } from '../../composables/useSystemState.js'
|
||||
import { useToast } from '../../composables/useToast.js'
|
||||
import {
|
||||
assistantCapabilities,
|
||||
buildExpenseStatItems,
|
||||
progressItems,
|
||||
progressSteps,
|
||||
quickPromptItems,
|
||||
todoItems,
|
||||
usageProfileMetrics
|
||||
} from '../../data/personalWorkbench.js'
|
||||
import { clearUserConversations, fetchLatestConversation } from '../../services/orchestrator.js'
|
||||
import {
|
||||
ASSISTANT_SESSION_SNAPSHOT_EVENT,
|
||||
@@ -157,13 +282,15 @@ import {
|
||||
|
||||
const props = defineProps({
|
||||
showHeader: { type: Boolean, default: true },
|
||||
assistantModalOpen: { type: Boolean, default: false }
|
||||
assistantModalOpen: { type: Boolean, default: false },
|
||||
workbenchSummary: { type: Object, default: () => ({}) }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['open-assistant'])
|
||||
const { currentUser } = useSystemState()
|
||||
const { toast } = useToast()
|
||||
const assistantDraft = ref('')
|
||||
const assistantInputRef = ref(null)
|
||||
const fileInputRef = ref(null)
|
||||
const selectedFiles = ref([])
|
||||
const pendingAction = ref('')
|
||||
@@ -178,11 +305,22 @@ const hasExpenseConversation = computed(() =>
|
||||
|| hasLocalExpenseSnapshot.value
|
||||
)
|
||||
const expenseActionLabel = computed(() => (hasExpenseConversation.value ? '继续报销' : '新建报销'))
|
||||
const expenseActionIcon = computed(() => (hasExpenseConversation.value ? 'mdi mdi-history' : 'mdi mdi-magnify-scan'))
|
||||
const assistantGreetingName = computed(() => {
|
||||
const user = currentUser.value || {}
|
||||
return String(user.name || user.username || '同事').trim() || '同事'
|
||||
const expenseStatItems = computed(() => buildExpenseStatItems(props.workbenchSummary))
|
||||
const visibleExpenseStatItems = computed(() => {
|
||||
const preferredKeys = ['monthly-amount', 'monthly-count', 'in-review', 'pending-payment']
|
||||
return preferredKeys
|
||||
.map((key) => expenseStatItems.value.find((item) => item.key === key))
|
||||
.filter(Boolean)
|
||||
})
|
||||
const visibleUsageProfileMetrics = computed(() => {
|
||||
const preferredKeys = ['ai-usage', 'submit-efficiency', 'auto-pass-rate', 'audit-duration']
|
||||
return preferredKeys
|
||||
.map((key) => usageProfileMetrics.find((item) => item.key === key))
|
||||
.filter(Boolean)
|
||||
})
|
||||
const visibleTodoItems = computed(() => todoItems.slice(0, 5))
|
||||
const visibleProgressItems = computed(() => progressItems.slice(0, 5))
|
||||
const todoAlertCount = computed(() => visibleTodoItems.value.length)
|
||||
|
||||
function buildSelectedFileKey(file) {
|
||||
return [file?.name, file?.size, file?.lastModified, file?.type].join('__')
|
||||
@@ -255,6 +393,30 @@ async function loadLatestConversation() {
|
||||
return payload?.found ? payload.conversation || null : null
|
||||
}
|
||||
|
||||
function focusAssistantInput() {
|
||||
nextTick(() => {
|
||||
assistantInputRef.value?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
function applyQuickPrompt(prompt) {
|
||||
assistantDraft.value = String(prompt || '').trim()
|
||||
focusAssistantInput()
|
||||
}
|
||||
|
||||
function openPromptAssistant(prompt) {
|
||||
if (pendingAction.value) {
|
||||
return
|
||||
}
|
||||
|
||||
emitAssistant({
|
||||
prompt: String(prompt || '').trim(),
|
||||
source: 'workbench',
|
||||
files: Array.from(selectedFiles.value),
|
||||
conversation: null
|
||||
})
|
||||
}
|
||||
|
||||
function handleWorkbenchEnter(event) {
|
||||
if (event.isComposing) {
|
||||
return
|
||||
@@ -340,99 +502,6 @@ async function handleExpenseConversationAction() {
|
||||
}
|
||||
}
|
||||
|
||||
const todoItems = [
|
||||
{
|
||||
title: '业务招待报销建议补参与人员',
|
||||
tipLabel: 'AI 建议',
|
||||
suggestion: '补充客户单位、客户人数、我方陪同人员',
|
||||
action: '去补充',
|
||||
iconKey: 'hospitality',
|
||||
color: 'var(--theme-primary-active)',
|
||||
accent: 'var(--theme-primary-soft-strong)'
|
||||
},
|
||||
{
|
||||
title: '差旅报销单待提交',
|
||||
tipLabel: 'AI 建议',
|
||||
suggestion: '补齐出发交通,可直接生成报销单',
|
||||
action: '继续填写',
|
||||
iconKey: 'travelDraft',
|
||||
color: 'var(--success-hover)',
|
||||
accent: 'var(--success-line)'
|
||||
},
|
||||
{
|
||||
title: '有 5 张票据未关联报销单',
|
||||
tipLabel: 'AI 建议',
|
||||
suggestion: '其中 3 张疑似交通费,可合并生成交通报销',
|
||||
action: '去整理',
|
||||
iconKey: 'receipts',
|
||||
color: 'var(--chart-blue)',
|
||||
accent: 'var(--theme-primary-soft-strong)'
|
||||
}
|
||||
]
|
||||
|
||||
const todoAlertCount = todoItems.length
|
||||
|
||||
const progressItems = [
|
||||
{
|
||||
id: 'travel',
|
||||
title: '差旅报销',
|
||||
amount: '¥3,280',
|
||||
date: '2026-05-03',
|
||||
status: '主管审批中',
|
||||
tone: 'success',
|
||||
iconKey: 'flight',
|
||||
color: 'var(--theme-primary-active)',
|
||||
accent: 'var(--theme-primary-soft-strong)'
|
||||
},
|
||||
{
|
||||
id: 'transport',
|
||||
title: '交通报销',
|
||||
amount: '¥126',
|
||||
date: '2026-05-02',
|
||||
status: '财务复核中',
|
||||
tone: 'info',
|
||||
iconKey: 'transport',
|
||||
color: 'var(--chart-blue)',
|
||||
accent: 'var(--theme-primary-soft-strong)'
|
||||
},
|
||||
{
|
||||
id: 'office',
|
||||
title: '办公采购',
|
||||
amount: '¥458',
|
||||
date: '2026-05-01',
|
||||
status: '已到账',
|
||||
tone: 'mint',
|
||||
iconKey: 'procurement',
|
||||
color: 'var(--success)',
|
||||
accent: 'var(--success-line)'
|
||||
}
|
||||
]
|
||||
|
||||
const progressAlertCount = progressItems.filter((item) => item.status !== '已到账').length
|
||||
|
||||
const policyItems = [
|
||||
{
|
||||
name: '差旅报销管理办法(2026版)',
|
||||
summary: '更新住宿标准与交通等级规则',
|
||||
date: '2026-05-04'
|
||||
},
|
||||
{
|
||||
name: '业务招待费用报销规范',
|
||||
summary: '明确参与人员与事由填写要求',
|
||||
date: '2026-05-02'
|
||||
},
|
||||
{
|
||||
name: '交通费用报销细则',
|
||||
summary: '补充网约车与停车费报销说明',
|
||||
date: '2026-04-28'
|
||||
},
|
||||
{
|
||||
name: '票据与附件提交规范通知',
|
||||
summary: '统一附件命名与上传要求',
|
||||
date: '2026-04-25'
|
||||
}
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
refreshLocalExpenseSnapshot()
|
||||
refreshLatestExpenseConversation()
|
||||
@@ -454,3 +523,5 @@ watch(
|
||||
</script>
|
||||
|
||||
<style scoped src="../../assets/styles/components/personal-workbench.css"></style>
|
||||
<style scoped src="../../assets/styles/components/personal-workbench-insights.css"></style>
|
||||
<style scoped src="../../assets/styles/components/personal-workbench-responsive.css"></style>
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
:content="userTooltipContent"
|
||||
placement="top"
|
||||
effect="light"
|
||||
:disabled="!collapsed"
|
||||
:disabled="!collapsed || userMenuOpen"
|
||||
:show-after="180"
|
||||
:hide-after="0"
|
||||
:offset="10"
|
||||
@@ -157,7 +157,8 @@ const sidebarMeta = {
|
||||
documents: { label: '单据中心' },
|
||||
budget: { label: '预算中心' },
|
||||
policies: { label: '知识管理' },
|
||||
audit: { label: '任务规则中心' },
|
||||
audit: { label: '规则中心' },
|
||||
digitalEmployees: { label: '数字员工' },
|
||||
logs: { label: '日志管理' },
|
||||
employees: { label: '员工管理' },
|
||||
settings: { label: '系统设置' }
|
||||
|
||||
@@ -34,7 +34,7 @@ const iconStyle = computed(() => iconMeta.value.style)
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
flex-shrink: 0;
|
||||
color: var(--icon-color);
|
||||
color: var(--icon-color, var(--theme-primary));
|
||||
}
|
||||
|
||||
.workbench-list-icon__halo {
|
||||
@@ -43,10 +43,10 @@ const iconStyle = computed(() => iconMeta.value.style)
|
||||
border-radius: 20px;
|
||||
background: radial-gradient(
|
||||
circle at 50% 40%,
|
||||
color-mix(in srgb, var(--icon-accent, var(--icon-color)) 42%, transparent) 0%,
|
||||
color-mix(in srgb, var(--icon-color, var(--theme-primary)) 24%, transparent) 0%,
|
||||
transparent 72%
|
||||
);
|
||||
opacity: 0.9;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.workbench-list-icon__panel {
|
||||
@@ -58,19 +58,19 @@ const iconStyle = computed(() => iconMeta.value.style)
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
border-radius: 18px;
|
||||
border: 1px solid color-mix(in srgb, var(--icon-color) 20%, #e2e8f0);
|
||||
border: 1px solid color-mix(in srgb, var(--icon-color, var(--theme-primary)) 18%, var(--line, #e2e8f0));
|
||||
background:
|
||||
radial-gradient(circle at 24% 16%, rgba(255, 255, 255, 0.98), transparent 46%),
|
||||
linear-gradient(
|
||||
160deg,
|
||||
color-mix(in srgb, var(--icon-accent, var(--icon-color)) 24%, #fff) 0%,
|
||||
#fff 42%,
|
||||
color-mix(in srgb, var(--icon-color) 6%, #f8fafc) 100%
|
||||
color-mix(in srgb, var(--icon-accent, var(--theme-primary-soft)) 72%, #fff) 0%,
|
||||
#fff 44%,
|
||||
color-mix(in srgb, var(--icon-color, var(--theme-primary)) 7%, var(--surface-soft, #f8fafc)) 100%
|
||||
);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.98),
|
||||
0 1px 2px rgba(15, 23, 42, 0.04),
|
||||
0 12px 24px color-mix(in srgb, var(--icon-color) 14%, transparent);
|
||||
0 10px 20px color-mix(in srgb, var(--icon-color, var(--theme-primary)) 12%, transparent);
|
||||
}
|
||||
|
||||
.workbench-list-icon__shine {
|
||||
@@ -93,8 +93,8 @@ const iconStyle = computed(() => iconMeta.value.style)
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: block;
|
||||
color: var(--icon-color);
|
||||
filter: drop-shadow(0 2px 6px color-mix(in srgb, var(--icon-color) 22%, transparent));
|
||||
color: color-mix(in srgb, var(--icon-color, var(--theme-primary)) 86%, var(--theme-primary-active));
|
||||
filter: drop-shadow(0 2px 5px color-mix(in srgb, var(--icon-color, var(--theme-primary)) 18%, transparent));
|
||||
}
|
||||
|
||||
.workbench-list-icon--outline .workbench-list-icon__art :deep(.workbench-heroicon) {
|
||||
|
||||
@@ -3,7 +3,18 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { icons } from '../data/icons.js'
|
||||
|
||||
export const appViews = ['overview', 'workbench', 'documents', 'budget', 'audit', 'employees', 'policies', 'logs', 'settings']
|
||||
export const appViews = [
|
||||
'overview',
|
||||
'workbench',
|
||||
'documents',
|
||||
'budget',
|
||||
'audit',
|
||||
'digitalEmployees',
|
||||
'employees',
|
||||
'policies',
|
||||
'logs',
|
||||
'settings'
|
||||
]
|
||||
|
||||
export const navItems = [
|
||||
{
|
||||
@@ -40,11 +51,19 @@ export const navItems = [
|
||||
},
|
||||
{
|
||||
id: 'audit',
|
||||
label: '任务规则中心',
|
||||
label: '规则中心',
|
||||
navHint: '查看和管理规则配置',
|
||||
icon: icons.skill,
|
||||
title: '任务规则中心',
|
||||
desc: '集中管理财务规则、风险规则、技能与外部 MCP 服务。'
|
||||
title: '规则中心',
|
||||
desc: '集中管理财务规则、风险规则与外部 MCP 服务。'
|
||||
},
|
||||
{
|
||||
id: 'digitalEmployees',
|
||||
label: '数字员工',
|
||||
navHint: '查看和管理后台自动执行的技能',
|
||||
icon: icons.digitalEmployee,
|
||||
title: '数字员工',
|
||||
desc: '集中查看后台自动执行的技能、执行计划和运行状态。'
|
||||
},
|
||||
{
|
||||
id: 'employees',
|
||||
@@ -87,6 +106,7 @@ const viewRouteNames = {
|
||||
budget: 'app-budget',
|
||||
policies: 'app-policies',
|
||||
audit: 'app-audit',
|
||||
digitalEmployees: 'app-digitalEmployees',
|
||||
logs: 'app-logs',
|
||||
employees: 'app-employees',
|
||||
settings: 'app-settings'
|
||||
|
||||
@@ -12,6 +12,7 @@ export const icons = {
|
||||
book: iconPath('<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>'),
|
||||
library: iconPath('<path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/><path d="M2 20h20"/>'),
|
||||
skill: iconPath('<path d="M12 3 9.5 8.5 3 11l6.5 2.5L12 19l2.5-5.5L21 11l-6.5-2.5z"/><path d="M19 19l.9 2 .9-2 2-.9-2-.9-.9-2-.9 2-2 .9z"/><path d="M5 5l.6 1.4L7 7l-1.4.6L5 9l-.6-1.4L3 7l1.4-.6z"/>'),
|
||||
digitalEmployee: iconPath('<text x="12" y="12.2" text-anchor="middle" dominant-baseline="middle" font-size="16" font-weight="800" fill="currentColor" stroke="none" font-family="system-ui, -apple-system, Segoe UI, Arial, sans-serif" letter-spacing="-0.45">AI</text>'),
|
||||
users: iconPath('<path d="M16 21v-2a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v2"/><circle cx="9.5" cy="7" r="4"/><path d="M20 8v6"/><path d="M23 11h-6"/>'),
|
||||
audit: iconPath('<path d="M12 8v4l3 3"/><path d="M3.05 11a9 9 0 1 1 .5 4"/><path d="M3 4v7h7"/>'),
|
||||
logs: iconPath('<path d="M4 5h16"/><path d="M4 12h16"/><path d="M4 19h10"/><path d="M18 17v4"/><path d="M16 19h4"/>'),
|
||||
|
||||
260
web/src/data/personalWorkbench.js
Normal file
260
web/src/data/personalWorkbench.js
Normal file
@@ -0,0 +1,260 @@
|
||||
export const quickPromptItems = [
|
||||
'如何申请差旅费用?',
|
||||
'报销标准是多少?',
|
||||
'预算有哪些科目余额?',
|
||||
'发票丢失了如何处理?'
|
||||
]
|
||||
|
||||
export const assistantCapabilities = [
|
||||
{
|
||||
title: '费用申请助手',
|
||||
primary: '智能识别规划',
|
||||
secondary: '快速创建申请',
|
||||
icon: 'mdi mdi-file-document-plus-outline',
|
||||
tone: 'green',
|
||||
prompt: '帮我新建一笔费用申请,并检查需要补充哪些信息。'
|
||||
},
|
||||
{
|
||||
title: '报销助手',
|
||||
primary: '票据 OCR 归票',
|
||||
secondary: '自动价格分摊',
|
||||
icon: 'mdi mdi-wallet-outline',
|
||||
tone: 'blue',
|
||||
prompt: '帮我整理票据并生成一笔可提交的报销单。'
|
||||
},
|
||||
{
|
||||
title: '预算控制助手',
|
||||
primary: '预算查询分析',
|
||||
secondary: '超标预警提醒',
|
||||
icon: 'mdi mdi-chart-pie',
|
||||
tone: 'emerald',
|
||||
prompt: '帮我查询本月预算使用情况,并提醒可能超标的费用。'
|
||||
},
|
||||
{
|
||||
title: '审批问答助手',
|
||||
primary: '审批凭证说明',
|
||||
secondary: '进度实时查询',
|
||||
icon: 'mdi mdi-clipboard-check-outline',
|
||||
tone: 'violet',
|
||||
prompt: '帮我查询报销审批进度,并说明当前卡在哪个节点。'
|
||||
},
|
||||
{
|
||||
title: '差旅助手',
|
||||
primary: '行程预订建议',
|
||||
secondary: '标准合规校验',
|
||||
icon: 'mdi mdi-airplane',
|
||||
tone: 'cyan',
|
||||
prompt: '帮我检查差旅安排是否符合公司差旅标准。'
|
||||
},
|
||||
{
|
||||
title: '制度知识助手',
|
||||
primary: '制度检索解读',
|
||||
secondary: '政策实时更新',
|
||||
icon: 'mdi mdi-book-open-page-variant-outline',
|
||||
tone: 'amber',
|
||||
prompt: '帮我解释最新报销制度,并列出需要注意的条款。'
|
||||
}
|
||||
]
|
||||
|
||||
export const todoItems = [
|
||||
{
|
||||
title: '待补材料',
|
||||
description: '差旅报销-北京市市场调研差旅费',
|
||||
status: '待补材料',
|
||||
statusTone: 'warning',
|
||||
due: '今天 18:00 截止',
|
||||
iconKey: 'receipts',
|
||||
color: 'var(--warning)',
|
||||
accent: 'var(--warning-soft)'
|
||||
},
|
||||
{
|
||||
title: '待提交申请',
|
||||
description: '费用申请-5月市场活动费用申请',
|
||||
status: '待提交',
|
||||
statusTone: 'success',
|
||||
due: '明天 10:00 截止',
|
||||
iconKey: 'travelDraft',
|
||||
color: 'var(--theme-primary)',
|
||||
accent: 'var(--theme-primary-soft)'
|
||||
},
|
||||
{
|
||||
title: '退回修改',
|
||||
description: '办公用品采购报销单',
|
||||
status: '退回修改',
|
||||
statusTone: 'danger',
|
||||
due: '2026-05-03',
|
||||
iconKey: 'procurement',
|
||||
color: 'var(--danger)',
|
||||
accent: 'var(--danger-soft)'
|
||||
},
|
||||
{
|
||||
title: '待确认票据',
|
||||
description: '招待费报销单',
|
||||
status: '待确认',
|
||||
statusTone: 'info',
|
||||
due: '2026-05-04',
|
||||
iconKey: 'hospitality',
|
||||
color: 'var(--chart-blue)',
|
||||
accent: 'color-mix(in srgb, var(--chart-blue) 10%, #ffffff)'
|
||||
},
|
||||
{
|
||||
title: '待审批',
|
||||
description: '培训费报销单',
|
||||
status: '待审批',
|
||||
statusTone: 'orange',
|
||||
due: '2026-05-05',
|
||||
iconKey: 'transport',
|
||||
color: 'var(--chart-amber)',
|
||||
accent: 'color-mix(in srgb, var(--chart-amber) 12%, #ffffff)'
|
||||
}
|
||||
]
|
||||
|
||||
export const progressSteps = ['草稿中', '审批中', '待付款', '已完成']
|
||||
|
||||
export const progressItems = [
|
||||
{
|
||||
id: 'BX-202605-0031',
|
||||
title: '北京市市场调研差旅费',
|
||||
amount: '¥3,280.00',
|
||||
status: '审批中',
|
||||
statusTone: 'success',
|
||||
activeStep: 1
|
||||
},
|
||||
{
|
||||
id: 'BX-202605-0028',
|
||||
title: '5月市场活动费用申请',
|
||||
amount: '¥8,750.00',
|
||||
status: '审批中',
|
||||
statusTone: 'success',
|
||||
activeStep: 1
|
||||
},
|
||||
{
|
||||
id: 'BX-202605-0022',
|
||||
title: '办公用品采购报销单',
|
||||
amount: '¥1,126.00',
|
||||
status: '待付款',
|
||||
statusTone: 'warning',
|
||||
activeStep: 2
|
||||
},
|
||||
{
|
||||
id: 'BX-202604-0049',
|
||||
title: '上海出差差旅费',
|
||||
amount: '¥2,458.00',
|
||||
status: '已完成',
|
||||
statusTone: 'muted',
|
||||
activeStep: 3
|
||||
},
|
||||
{
|
||||
id: 'BX-202604-0033',
|
||||
title: '客户招待费',
|
||||
amount: '¥945.00',
|
||||
status: '已完成',
|
||||
statusTone: 'muted',
|
||||
activeStep: 3
|
||||
}
|
||||
]
|
||||
|
||||
export function buildExpenseStatItems(summary = {}) {
|
||||
return [
|
||||
{
|
||||
key: 'monthly-count',
|
||||
label: '本月报销笔数',
|
||||
value: summary.monthlyCount ?? 0,
|
||||
unit: '笔',
|
||||
tone: 'primary'
|
||||
},
|
||||
{
|
||||
key: 'monthly-amount',
|
||||
label: '本月报销金额',
|
||||
value: summary.monthlyAmountLabel || '¥0',
|
||||
unit: '',
|
||||
tone: 'amount'
|
||||
},
|
||||
{
|
||||
key: 'total-count',
|
||||
label: '累计报销笔数',
|
||||
value: summary.totalCount ?? 0,
|
||||
unit: '笔',
|
||||
tone: 'muted'
|
||||
},
|
||||
{
|
||||
key: 'total-amount',
|
||||
label: '累计报销金额',
|
||||
value: summary.totalAmountLabel || '¥0',
|
||||
unit: '',
|
||||
tone: 'amount'
|
||||
},
|
||||
{
|
||||
key: 'in-review',
|
||||
label: '审批中单据',
|
||||
value: summary.inReviewCount ?? 0,
|
||||
unit: '笔',
|
||||
tone: 'warning'
|
||||
},
|
||||
{
|
||||
key: 'pending-payment',
|
||||
label: '待付款单据',
|
||||
value: summary.pendingPaymentCount ?? 0,
|
||||
unit: '笔',
|
||||
tone: 'info'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/** 费用画像:待后端接入后替换为真实用户行为统计 */
|
||||
export const usageProfileMetrics = [
|
||||
{
|
||||
key: 'stay-duration',
|
||||
label: '系统停留时长',
|
||||
value: '128.5',
|
||||
unit: '小时',
|
||||
hint: '近 30 天累计',
|
||||
icon: 'mdi mdi-clock-outline',
|
||||
tone: 'primary'
|
||||
},
|
||||
{
|
||||
key: 'ai-usage',
|
||||
label: 'AI 使用次数',
|
||||
value: '86',
|
||||
unit: '次',
|
||||
hint: '含报销助手与制度问答',
|
||||
icon: 'mdi mdi-robot-outline',
|
||||
tone: 'violet'
|
||||
},
|
||||
{
|
||||
key: 'token-usage',
|
||||
label: 'Token 消耗',
|
||||
value: '1.42',
|
||||
unit: '万',
|
||||
hint: '本月模型调用累计',
|
||||
icon: 'mdi mdi-lightning-bolt-outline',
|
||||
tone: 'amber'
|
||||
},
|
||||
{
|
||||
key: 'audit-duration',
|
||||
label: '平均审核时长',
|
||||
value: '18.6',
|
||||
unit: '小时',
|
||||
hint: '本月已办结单据',
|
||||
icon: 'mdi mdi-timer-sand',
|
||||
tone: 'success'
|
||||
},
|
||||
{
|
||||
key: 'submit-efficiency',
|
||||
label: '平均提单耗时',
|
||||
value: '12',
|
||||
unit: '分钟',
|
||||
hint: '从打开助手到提交',
|
||||
icon: 'mdi mdi-speedometer',
|
||||
tone: 'cyan'
|
||||
},
|
||||
{
|
||||
key: 'auto-pass-rate',
|
||||
label: '智能预审通过率',
|
||||
value: '92',
|
||||
unit: '%',
|
||||
hint: 'AI 预审后无需补材',
|
||||
icon: 'mdi mdi-shield-check-outline',
|
||||
tone: 'emerald'
|
||||
}
|
||||
]
|
||||
@@ -5,7 +5,8 @@ export const DEFAULT_APP_VIEW_ORDER = [
|
||||
'budget',
|
||||
'policies',
|
||||
'audit',
|
||||
'logs',
|
||||
'digitalEmployees',
|
||||
'logs',
|
||||
'employees',
|
||||
'settings'
|
||||
]
|
||||
@@ -15,6 +16,7 @@ const VIEW_ROLE_RULES = {
|
||||
overview: ['finance', 'executive'],
|
||||
budget: ['budget_monitor', 'executive'],
|
||||
audit: ['finance'],
|
||||
digitalEmployees: ['finance'],
|
||||
logs: ['manager'],
|
||||
employees: ['manager'],
|
||||
settings: ['manager']
|
||||
|
||||
@@ -69,6 +69,14 @@ export function buildWorkbenchSummary(requests, currentUser) {
|
||||
|
||||
const monthlyCount = monthlyClaims.length
|
||||
const monthlyAmount = monthlyClaims.reduce((sum, item) => sum + parseNumber(item.amount), 0)
|
||||
const totalCount = ownedRequests.length
|
||||
const totalAmount = ownedRequests.reduce((sum, item) => sum + parseNumber(item.amount), 0)
|
||||
const inReviewCount = ownedRequests.filter((item) => item.approvalKey === 'in_progress').length
|
||||
const pendingPaymentCount = ownedRequests.filter((item) => {
|
||||
const status = String(item.status || item.approvalStatus || '').trim()
|
||||
return status.includes('待付款') || status.includes('待支付')
|
||||
}).length
|
||||
const completedCount = ownedRequests.filter((item) => item.approvalKey === 'completed').length
|
||||
const returnCount = ownedRequests.filter((item) => item.approvalKey === 'rejected').length
|
||||
const highRiskCount = monthlyClaims.filter((item) => hasHighRiskFlag(item)).length
|
||||
|
||||
@@ -76,6 +84,12 @@ export function buildWorkbenchSummary(requests, currentUser) {
|
||||
monthlyCount,
|
||||
monthlyAmount,
|
||||
monthlyAmountLabel: formatCurrency(monthlyAmount),
|
||||
totalCount,
|
||||
totalAmount,
|
||||
totalAmountLabel: formatCurrency(totalAmount),
|
||||
inReviewCount,
|
||||
pendingPaymentCount,
|
||||
completedCount,
|
||||
returnCount,
|
||||
highRiskCount
|
||||
}
|
||||
|
||||
@@ -1,224 +1,230 @@
|
||||
<template>
|
||||
<div class="app" :class="{ 'sidebar-collapsed': sidebarCollapsed }">
|
||||
<div class="app-sidebar">
|
||||
<SidebarRail
|
||||
:nav-items="filteredNavItems"
|
||||
:active-view="activeView"
|
||||
:company-name="companyProfile.name"
|
||||
:company-logo="companyProfile.logo"
|
||||
:current-user="currentUser"
|
||||
:collapsed="sidebarCollapsed"
|
||||
@navigate="handleNavigate"
|
||||
@open-chat="openSmartEntry"
|
||||
@logout="handleLogout"
|
||||
@toggle-collapse="toggleSidebarCollapsed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<main
|
||||
class="main"
|
||||
:class="{
|
||||
<template>
|
||||
<div class="app" :class="{ 'sidebar-collapsed': sidebarCollapsed }">
|
||||
<div class="app-sidebar">
|
||||
<SidebarRail
|
||||
:nav-items="filteredNavItems"
|
||||
:active-view="activeView"
|
||||
:company-name="companyProfile.name"
|
||||
:company-logo="companyProfile.logo"
|
||||
:current-user="currentUser"
|
||||
:collapsed="sidebarCollapsed"
|
||||
@navigate="handleNavigate"
|
||||
@open-chat="openSmartEntry"
|
||||
@logout="handleLogout"
|
||||
@toggle-collapse="toggleSidebarCollapsed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<main
|
||||
class="main"
|
||||
:class="{
|
||||
'overview-main': activeView === 'overview',
|
||||
'workbench-main': activeView === 'workbench',
|
||||
'documents-main': activeView === 'documents',
|
||||
'budget-main': activeView === 'budget',
|
||||
'policies-main': activeView === 'policies',
|
||||
'audit-main': activeView === 'audit',
|
||||
'audit-detail-main': activeView === 'audit' && auditDetailOpen,
|
||||
'logs-main': activeView === 'logs',
|
||||
'employees-main': activeView === 'employees',
|
||||
'settings-main': activeView === 'settings'
|
||||
}"
|
||||
>
|
||||
<TopBar
|
||||
v-if="activeView !== 'settings' && !(activeView === 'audit' && auditDetailOpen)"
|
||||
:current-view="topBarView"
|
||||
:search="search"
|
||||
:active-view="activeView"
|
||||
:ranges="ranges"
|
||||
:active-range="activeRange"
|
||||
:employee-summary="employeeSummary"
|
||||
:knowledge-summary="knowledgeSummary"
|
||||
:logs-summary="logsSummary"
|
||||
:request-summary="requestSummary"
|
||||
:document-summary="documentSummary"
|
||||
:workbench-summary="workbenchSummary"
|
||||
:detail-mode="detailMode"
|
||||
:log-detail-mode="logDetailMode"
|
||||
:detail-alerts="detailAlerts"
|
||||
:custom-range="customRange"
|
||||
@update:search="search = $event"
|
||||
@update:active-range="activeRange = $event"
|
||||
@update:custom-range="customRange = $event"
|
||||
@batch-approve="toast('已批量通过 23 条审批任务。')"
|
||||
@new-application="openExpenseApplicationCreate"
|
||||
/>
|
||||
|
||||
<FilterBar
|
||||
v-if="activeView !== 'overview' && activeView !== 'workbench' && activeView !== 'documents' && activeView !== 'budget' && activeView !== 'policies' && activeView !== 'audit' && activeView !== 'logs' && activeView !== 'employees' && activeView !== 'settings'"
|
||||
:compact="activeView === 'overview'"
|
||||
:filters="filters"
|
||||
:ranges="ranges"
|
||||
:active-range="activeRange"
|
||||
@update:active-range="activeRange = $event"
|
||||
/>
|
||||
|
||||
<section
|
||||
class="workarea"
|
||||
:class="{
|
||||
'audit-main': activeView === 'audit',
|
||||
'audit-detail-main': activeView === 'audit' && auditDetailOpen,
|
||||
'digital-employees-main': activeView === 'digitalEmployees',
|
||||
'logs-main': activeView === 'logs',
|
||||
'employees-main': activeView === 'employees',
|
||||
'settings-main': activeView === 'settings'
|
||||
}"
|
||||
>
|
||||
<TopBar
|
||||
v-if="activeView !== 'settings' && !(activeView === 'audit' && auditDetailOpen)"
|
||||
:current-view="topBarView"
|
||||
:search="search"
|
||||
:active-view="activeView"
|
||||
:ranges="ranges"
|
||||
:active-range="activeRange"
|
||||
:employee-summary="employeeSummary"
|
||||
:knowledge-summary="knowledgeSummary"
|
||||
:logs-summary="logsSummary"
|
||||
:request-summary="requestSummary"
|
||||
:document-summary="documentSummary"
|
||||
:workbench-summary="workbenchSummary"
|
||||
:detail-mode="detailMode"
|
||||
:log-detail-mode="logDetailMode"
|
||||
:detail-alerts="detailAlerts"
|
||||
:custom-range="customRange"
|
||||
@update:search="search = $event"
|
||||
@update:active-range="activeRange = $event"
|
||||
@update:custom-range="customRange = $event"
|
||||
@batch-approve="toast('已批量通过 23 条审批任务。')"
|
||||
@new-application="openExpenseApplicationCreate"
|
||||
/>
|
||||
|
||||
<FilterBar
|
||||
v-if="activeView !== 'overview' && activeView !== 'workbench' && activeView !== 'documents' && activeView !== 'budget' && activeView !== 'policies' && activeView !== 'audit' && activeView !== 'digitalEmployees' && activeView !== 'logs' && activeView !== 'employees' && activeView !== 'settings'"
|
||||
:compact="activeView === 'overview'"
|
||||
:filters="filters"
|
||||
:ranges="ranges"
|
||||
:active-range="activeRange"
|
||||
@update:active-range="activeRange = $event"
|
||||
/>
|
||||
|
||||
<section
|
||||
class="workarea"
|
||||
:class="{
|
||||
'documents-workarea': activeView === 'documents',
|
||||
'workbench-workarea': activeView === 'workbench',
|
||||
'budget-workarea': activeView === 'budget',
|
||||
'policies-workarea': activeView === 'policies',
|
||||
'audit-workarea': activeView === 'audit',
|
||||
'logs-workarea': activeView === 'logs',
|
||||
'employees-workarea': activeView === 'employees',
|
||||
'settings-workarea': activeView === 'settings'
|
||||
}"
|
||||
>
|
||||
<OverviewView
|
||||
v-if="activeView === 'overview'"
|
||||
:filtered-requests="filteredRequests"
|
||||
@approve="handleApprove"
|
||||
@reject="handleReject"
|
||||
/>
|
||||
|
||||
<PersonalWorkbenchView
|
||||
v-else-if="activeView === 'workbench'"
|
||||
:assistant-modal-open="smartEntryOpen"
|
||||
@open-assistant="openSmartEntry"
|
||||
/>
|
||||
|
||||
'audit-workarea': activeView === 'audit',
|
||||
'digital-employees-workarea': activeView === 'digitalEmployees',
|
||||
'logs-workarea': activeView === 'logs',
|
||||
'employees-workarea': activeView === 'employees',
|
||||
'settings-workarea': activeView === 'settings'
|
||||
}"
|
||||
>
|
||||
<OverviewView
|
||||
v-if="activeView === 'overview'"
|
||||
:filtered-requests="filteredRequests"
|
||||
@approve="handleApprove"
|
||||
@reject="handleReject"
|
||||
/>
|
||||
|
||||
<PersonalWorkbenchView
|
||||
v-else-if="activeView === 'workbench'"
|
||||
:assistant-modal-open="smartEntryOpen"
|
||||
:workbench-summary="workbenchSummary"
|
||||
@open-assistant="openSmartEntry"
|
||||
/>
|
||||
|
||||
<TravelRequestDetailView
|
||||
v-else-if="activeView === 'documents' && detailMode && selectedRequest"
|
||||
:request="selectedRequest"
|
||||
back-label="返回单据中心"
|
||||
@back-to-requests="closeRequestDetail"
|
||||
@open-assistant="openSmartEntry"
|
||||
@request-updated="handleRequestUpdated"
|
||||
@request-deleted="handleRequestDeleted"
|
||||
/>
|
||||
|
||||
<DocumentsCenterView
|
||||
v-else-if="activeView === 'documents'"
|
||||
:filtered-requests="filteredRequests"
|
||||
:has-data="requests.length > 0"
|
||||
:loading="requestsLoading"
|
||||
:error="requestsError"
|
||||
@open-document="openRequestDetail"
|
||||
@create-request="openTravelCreate"
|
||||
@create-application="openExpenseApplicationCreate"
|
||||
@reload="reloadRequests"
|
||||
@summary-change="documentSummary = $event"
|
||||
/>
|
||||
|
||||
@open-assistant="openSmartEntry"
|
||||
@request-updated="handleRequestUpdated"
|
||||
@request-deleted="handleRequestDeleted"
|
||||
/>
|
||||
|
||||
<DocumentsCenterView
|
||||
v-else-if="activeView === 'documents'"
|
||||
:filtered-requests="filteredRequests"
|
||||
:has-data="requests.length > 0"
|
||||
:loading="requestsLoading"
|
||||
:error="requestsError"
|
||||
@open-document="openRequestDetail"
|
||||
@create-request="openTravelCreate"
|
||||
@create-application="openExpenseApplicationCreate"
|
||||
@reload="reloadRequests"
|
||||
@summary-change="documentSummary = $event"
|
||||
/>
|
||||
|
||||
<BudgetCenterView
|
||||
v-else-if="activeView === 'budget'"
|
||||
:current-user="currentUser"
|
||||
@open-assistant="openSmartEntry"
|
||||
/>
|
||||
<PoliciesView v-else-if="activeView === 'policies'" @summary-change="knowledgeSummary = $event" />
|
||||
<AuditView v-else-if="activeView === 'audit'" @detail-open-change="auditDetailOpen = $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" />
|
||||
<SettingsView v-else />
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<TravelReimbursementCreateView
|
||||
v-if="smartEntryOpen"
|
||||
:key="smartEntrySessionId"
|
||||
:initial-prompt="smartEntryContext.prompt"
|
||||
:initial-files="smartEntryContext.files"
|
||||
:initial-conversation="smartEntryContext.conversation"
|
||||
<AuditView v-else-if="activeView === 'audit'" @detail-open-change="auditDetailOpen = $event" />
|
||||
<DigitalEmployeesView v-else-if="activeView === 'digitalEmployees'" />
|
||||
<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" />
|
||||
<SettingsView v-else />
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<TravelReimbursementCreateView
|
||||
v-if="smartEntryOpen"
|
||||
:key="smartEntrySessionId"
|
||||
:initial-prompt="smartEntryContext.prompt"
|
||||
:initial-files="smartEntryContext.files"
|
||||
:initial-conversation="smartEntryContext.conversation"
|
||||
:entry-source="smartEntryContext.source"
|
||||
:request-context="smartEntryContext.request"
|
||||
:invalidated-draft-claim-id="smartEntryInvalidatedDraftClaimId"
|
||||
:reopen-token="smartEntryRevealToken"
|
||||
@close="closeSmartEntry"
|
||||
@draft-saved="handleDraftSaved"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import SidebarRail from '../components/layout/SidebarRail.vue'
|
||||
import TopBar from '../components/layout/TopBar.vue'
|
||||
import FilterBar from '../components/layout/FilterBar.vue'
|
||||
import OverviewView from './OverviewView.vue'
|
||||
import PersonalWorkbenchView from './PersonalWorkbenchView.vue'
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import SidebarRail from '../components/layout/SidebarRail.vue'
|
||||
import TopBar from '../components/layout/TopBar.vue'
|
||||
import FilterBar from '../components/layout/FilterBar.vue'
|
||||
import OverviewView from './OverviewView.vue'
|
||||
import PersonalWorkbenchView from './PersonalWorkbenchView.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 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 { useSystemState } from '../composables/useSystemState.js'
|
||||
import { filterNavItemsByAccess } from '../utils/accessControl.js'
|
||||
|
||||
const employeeSummary = ref(null)
|
||||
const knowledgeSummary = ref(null)
|
||||
const logsSummary = ref(null)
|
||||
const documentSummary = ref(null)
|
||||
const auditDetailOpen = ref(false)
|
||||
const sidebarCollapsed = ref(true)
|
||||
|
||||
function toggleSidebarCollapsed() {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
}
|
||||
|
||||
const {
|
||||
activeRange,
|
||||
activeView,
|
||||
closeRequestDetail,
|
||||
closeSmartEntry,
|
||||
customRange,
|
||||
detailAlerts,
|
||||
detailMode,
|
||||
logDetailMode,
|
||||
filteredRequests,
|
||||
filters,
|
||||
handleApprove,
|
||||
handleDraftSaved,
|
||||
handleNavigate,
|
||||
handleReject,
|
||||
handleRequestDeleted,
|
||||
handleRequestUpdated,
|
||||
navItems,
|
||||
openExpenseApplicationCreate,
|
||||
openRequestDetail,
|
||||
openSmartEntry,
|
||||
openTravelCreate,
|
||||
ranges,
|
||||
requestSummary,
|
||||
workbenchSummary,
|
||||
requestsError,
|
||||
requestsLoading,
|
||||
reloadRequests,
|
||||
requests,
|
||||
search,
|
||||
selectedRequest,
|
||||
smartEntryContext,
|
||||
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 { useSystemState } from '../composables/useSystemState.js'
|
||||
import { filterNavItemsByAccess } from '../utils/accessControl.js'
|
||||
|
||||
const employeeSummary = ref(null)
|
||||
const knowledgeSummary = ref(null)
|
||||
const logsSummary = ref(null)
|
||||
const documentSummary = ref(null)
|
||||
const auditDetailOpen = ref(false)
|
||||
const sidebarCollapsed = ref(false)
|
||||
|
||||
function toggleSidebarCollapsed() {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
}
|
||||
|
||||
const {
|
||||
activeRange,
|
||||
activeView,
|
||||
closeRequestDetail,
|
||||
closeSmartEntry,
|
||||
customRange,
|
||||
detailAlerts,
|
||||
detailMode,
|
||||
logDetailMode,
|
||||
filteredRequests,
|
||||
filters,
|
||||
handleApprove,
|
||||
handleDraftSaved,
|
||||
handleNavigate,
|
||||
handleReject,
|
||||
handleRequestDeleted,
|
||||
handleRequestUpdated,
|
||||
navItems,
|
||||
openExpenseApplicationCreate,
|
||||
openRequestDetail,
|
||||
openSmartEntry,
|
||||
openTravelCreate,
|
||||
ranges,
|
||||
requestSummary,
|
||||
workbenchSummary,
|
||||
requestsError,
|
||||
requestsLoading,
|
||||
reloadRequests,
|
||||
requests,
|
||||
search,
|
||||
selectedRequest,
|
||||
smartEntryContext,
|
||||
smartEntryInvalidatedDraftClaimId,
|
||||
smartEntryOpen,
|
||||
smartEntryRevealToken,
|
||||
smartEntrySessionId,
|
||||
toast,
|
||||
topBarView
|
||||
} = useAppShell()
|
||||
|
||||
const { companyProfile, currentUser, logout } = useSystemState()
|
||||
const filteredNavItems = computed(() => filterNavItemsByAccess(navItems, currentUser.value))
|
||||
|
||||
function handleLogout() {
|
||||
logout('manual')
|
||||
}
|
||||
</script>
|
||||
toast,
|
||||
topBarView
|
||||
} = useAppShell()
|
||||
|
||||
const { companyProfile, currentUser, logout } = useSystemState()
|
||||
const filteredNavItems = computed(() => filterNavItemsByAccess(navItems, currentUser.value))
|
||||
|
||||
function handleLogout() {
|
||||
logout('manual')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -112,7 +112,6 @@
|
||||
:risk-rule-test-passed="riskRuleTestPassed"
|
||||
/>
|
||||
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="detail-grid"
|
||||
|
||||
737
web/src/views/DigitalEmployeesView.vue
Normal file
737
web/src/views/DigitalEmployeesView.vue
Normal file
@@ -0,0 +1,737 @@
|
||||
<template>
|
||||
<section class="digital-employees-view skill-center">
|
||||
<Transition name="skill-view" mode="out-in">
|
||||
<article
|
||||
v-if="selectedEmployee"
|
||||
key="detail"
|
||||
class="skill-detail digital-employee-detail"
|
||||
>
|
||||
<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
|
||||
v-else
|
||||
:selected-skill="selectedEmployee"
|
||||
:can-edit="canEditDigitalEmployeeSource"
|
||||
:detail-busy="detailBusy"
|
||||
:action-state="actionState"
|
||||
@save-source="saveDigitalEmployeeSource"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<footer class="detail-actions digital-employee-detail-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
|
||||
class="minor-action enable-action"
|
||||
:class="{ 'is-on': selectedEmployee.statusValue === 'active' }"
|
||||
type="button"
|
||||
:disabled="!canOperateDigitalEmployee || detailBusy"
|
||||
@click="toggleDigitalEmployeeRunning(selectedEmployee)"
|
||||
>
|
||||
<i :class="selectedEmployee.statusValue === 'active' ? 'mdi mdi-toggle-switch' : 'mdi mdi-toggle-switch-off-outline'"></i>
|
||||
<span>{{ selectedEmployee.statusValue === 'active' ? '停止运行' : '启用运行' }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="minor-action"
|
||||
type="button"
|
||||
:disabled="!canOperateDigitalEmployee || detailBusy"
|
||||
@click="openDigitalEmployeeSchedule(selectedEmployee)"
|
||||
>
|
||||
<i class="mdi mdi-clock-edit-outline"></i>
|
||||
<span>定时设置</span>
|
||||
</button>
|
||||
<button
|
||||
class="minor-action success-action"
|
||||
type="button"
|
||||
:disabled="!canOperateDigitalEmployee || detailBusy"
|
||||
@click="runDigitalEmployeeNow(selectedEmployee)"
|
||||
>
|
||||
<i class="mdi mdi-play-circle-outline"></i>
|
||||
<span>{{ actionBusy(selectedEmployee.id, 'run-digital-now') ? '运行中...' : '立即运行' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<article v-else key="list" class="skill-list panel digital-employees-list">
|
||||
<nav class="status-tabs" aria-label="数字员工类型">
|
||||
<button class="active" type="button">数字员工</button>
|
||||
</nav>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="filter-set">
|
||||
<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"
|
||||
:label="selectedStatusLabel"
|
||||
:options="statusOptions"
|
||||
:selected-value="selectedStatus"
|
||||
@toggle="toggleFilterPopover"
|
||||
@close="closeFilterPopover"
|
||||
@select="selectFilter('status', $event)"
|
||||
/>
|
||||
|
||||
<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>
|
||||
</article>
|
||||
</Transition>
|
||||
|
||||
<DigitalEmployeeScheduleDialog
|
||||
v-model="scheduleForm"
|
||||
:open="scheduleEditorOpen"
|
||||
:target-name="scheduleTarget?.name || ''"
|
||||
:preview-label="schedulePreviewLabel"
|
||||
:error-message="scheduleEditorError"
|
||||
:busy="scheduleEditorBusy"
|
||||
:can-save="canOperateDigitalEmployee"
|
||||
@close="closeDigitalEmployeeSchedule"
|
||||
@save="saveDigitalEmployeeSchedule"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import AuditDigitalEmployeeDetail from '../components/audit/AuditDigitalEmployeeDetail.vue'
|
||||
import AuditPickerFilter from '../components/audit/AuditPickerFilter.vue'
|
||||
import DigitalEmployeeScheduleDialog from '../components/audit/DigitalEmployeeScheduleDialog.vue'
|
||||
import TableEmptyState from '../components/shared/TableEmptyState.vue'
|
||||
import TableLoadingState from '../components/shared/TableLoadingState.vue'
|
||||
import { useSystemState } from '../composables/useSystemState.js'
|
||||
import { useToast } from '../composables/useToast.js'
|
||||
import {
|
||||
activateAgentAsset,
|
||||
createAgentAssetVersion,
|
||||
fetchAgentAssetDetail,
|
||||
fetchAgentAssets,
|
||||
updateAgentAsset
|
||||
} from '../services/agentAssets.js'
|
||||
import { runOrchestrator } from '../services/orchestrator.js'
|
||||
import { isPlatformAdminUser } from '../utils/accessControl.js'
|
||||
import {
|
||||
DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS,
|
||||
buildDigitalEmployeeDetailMeta,
|
||||
buildDigitalEmployeeListMeta,
|
||||
formatDigitalEmployeeCron,
|
||||
isDigitalEmployeeAsset
|
||||
} from './scripts/auditViewDigitalEmployeeModel.js'
|
||||
import {
|
||||
buildDigitalEmployeeScheduleConfig,
|
||||
buildDigitalEmployeeScheduleCron,
|
||||
createDigitalEmployeeScheduleForm,
|
||||
resolveDigitalEmployeeScheduleValue
|
||||
} from './scripts/digitalEmployeeScheduleModel.js'
|
||||
import { incrementVersion } from './scripts/auditViewRuntimeModel.js'
|
||||
import {
|
||||
ENABLED_STATE_OPTIONS,
|
||||
formatDateTime,
|
||||
normalizeText,
|
||||
resolveStatusMeta,
|
||||
STATUS_OPTIONS
|
||||
} from './scripts/auditViewModel.js'
|
||||
|
||||
const { currentUser } = useSystemState()
|
||||
const { toast } = useToast()
|
||||
|
||||
const employees = ref([])
|
||||
const selectedEmployee = ref(null)
|
||||
const selectedEmployeeId = ref('')
|
||||
const keyword = ref('')
|
||||
const selectedStatus = ref('')
|
||||
const selectedEnabledState = ref('')
|
||||
const selectedExecutionMode = ref('')
|
||||
const activeFilterPopover = ref('')
|
||||
const loading = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const detailError = ref('')
|
||||
const actionState = ref('')
|
||||
const busyEmployeeId = ref('')
|
||||
const scheduleEditorOpen = ref(false)
|
||||
const scheduleTarget = ref(null)
|
||||
const scheduleForm = ref(createDigitalEmployeeScheduleForm())
|
||||
const scheduleEditorError = ref('')
|
||||
|
||||
const isAdmin = computed(() => isPlatformAdminUser(currentUser.value))
|
||||
const detailBusy = computed(() => Boolean(detailLoading.value || actionState.value))
|
||||
const canOperateDigitalEmployee = computed(() => isAdmin.value && Boolean(selectedEmployee.value))
|
||||
const canEditDigitalEmployeeSource = computed(() => canOperateDigitalEmployee.value)
|
||||
const scheduleEditorBusy = computed(() => actionState.value === 'save-digital-schedule')
|
||||
|
||||
const statusOptions = STATUS_OPTIONS
|
||||
const enabledStateOptions = ENABLED_STATE_OPTIONS
|
||||
const executionModeOptions = [
|
||||
{ value: '', label: '全部执行方式' },
|
||||
{ value: 'timed', label: '定时执行' },
|
||||
{ value: 'manual', label: '手动触发' }
|
||||
]
|
||||
|
||||
const selectedStatusLabel = computed(() =>
|
||||
statusOptions.find((item) => item.value === selectedStatus.value)?.label || '全部状态'
|
||||
)
|
||||
const selectedEnabledLabel = computed(() =>
|
||||
enabledStateOptions.find((item) => item.value === selectedEnabledState.value)?.label || '全部启动状态'
|
||||
)
|
||||
const selectedExecutionModeLabel = computed(() =>
|
||||
executionModeOptions.find((item) => item.value === selectedExecutionMode.value)?.label || '全部执行方式'
|
||||
)
|
||||
const activeFilterTokens = computed(() => {
|
||||
const tokens = []
|
||||
if (selectedStatus.value) {
|
||||
tokens.push(`资产状态:${selectedStatusLabel.value}`)
|
||||
}
|
||||
if (selectedEnabledState.value) {
|
||||
tokens.push(`启动状态:${selectedEnabledLabel.value}`)
|
||||
}
|
||||
if (selectedExecutionMode.value) {
|
||||
tokens.push(`执行方式:${selectedExecutionModeLabel.value}`)
|
||||
}
|
||||
return tokens
|
||||
})
|
||||
|
||||
const schedulePreviewLabel = computed(() => {
|
||||
try {
|
||||
return formatDigitalEmployeeCron(buildDigitalEmployeeScheduleCron(scheduleForm.value))
|
||||
} catch {
|
||||
return '表达式待确认'
|
||||
}
|
||||
})
|
||||
|
||||
const visibleEmployees = computed(() => {
|
||||
const searchText = normalizeText(keyword.value).toLowerCase()
|
||||
return employees.value.filter((item) => {
|
||||
const matchesKeyword = searchText
|
||||
? [
|
||||
item.name,
|
||||
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
|
||||
})
|
||||
})
|
||||
|
||||
function toggleFilterPopover(id) {
|
||||
activeFilterPopover.value = activeFilterPopover.value === id ? '' : id
|
||||
}
|
||||
|
||||
function closeFilterPopover() {
|
||||
activeFilterPopover.value = ''
|
||||
}
|
||||
|
||||
function selectFilter(type, value) {
|
||||
if (type === 'status') {
|
||||
selectedStatus.value = value
|
||||
}
|
||||
if (type === 'enabled') {
|
||||
selectedEnabledState.value = value
|
||||
}
|
||||
if (type === 'executionMode') {
|
||||
selectedExecutionMode.value = value
|
||||
}
|
||||
closeFilterPopover()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
keyword.value = ''
|
||||
selectedStatus.value = ''
|
||||
selectedEnabledState.value = ''
|
||||
selectedExecutionMode.value = ''
|
||||
closeFilterPopover()
|
||||
}
|
||||
|
||||
function resolveActor() {
|
||||
const user = currentUser.value || {}
|
||||
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() {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const payload = await fetchAgentAssets({ assetType: 'task' })
|
||||
const items = Array.isArray(payload)
|
||||
? payload.filter(isDigitalEmployeeAsset).map(buildEmployeeListItem)
|
||||
: []
|
||||
|
||||
employees.value = sortEmployees(items)
|
||||
} catch (error) {
|
||||
errorMessage.value = error?.message || '数字员工数据加载失败,请稍后重试。'
|
||||
toast(errorMessage.value)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEmployeeDetail(assetId, placeholder = null, options = {}) {
|
||||
if (!assetId) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedEmployeeId.value = assetId
|
||||
selectedEmployee.value = placeholder
|
||||
? buildEmployeePlaceholder(placeholder)
|
||||
: selectedEmployee.value || { id: assetId, name: '数字员工', loading: true, digitalEmployee: {} }
|
||||
detailLoading.value = true
|
||||
detailError.value = ''
|
||||
|
||||
try {
|
||||
const detail = await fetchAgentAssetDetail(assetId)
|
||||
selectedEmployee.value = buildEmployeeDetail(detail)
|
||||
} catch (error) {
|
||||
detailError.value = error?.message || '数字员工详情加载失败,请稍后重试。'
|
||||
if (!options.silent) {
|
||||
toast(detailError.value)
|
||||
}
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openEmployeeDetail(employee) {
|
||||
loadEmployeeDetail(employee.id, employee).catch(() => {})
|
||||
}
|
||||
|
||||
function closeEmployeeDetail() {
|
||||
closeDigitalEmployeeSchedule()
|
||||
selectedEmployee.value = null
|
||||
selectedEmployeeId.value = ''
|
||||
detailError.value = ''
|
||||
detailLoading.value = false
|
||||
}
|
||||
|
||||
async function refreshAfterMutation(assetId) {
|
||||
await loadEmployees()
|
||||
if (selectedEmployee.value && selectedEmployeeId.value === assetId) {
|
||||
const placeholder = employees.value.find((item) => item.id === assetId) || selectedEmployee.value
|
||||
await loadEmployeeDetail(assetId, placeholder, { silent: true })
|
||||
}
|
||||
}
|
||||
|
||||
function actionBusy(assetId, action) {
|
||||
return busyEmployeeId.value === assetId && actionState.value === action
|
||||
}
|
||||
|
||||
async function toggleDigitalEmployeeRunning(employee) {
|
||||
if (!employee || !canOperateDigitalEmployee.value || actionState.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const assetId = employee.id
|
||||
const shouldEnable = employee.statusValue !== 'active'
|
||||
actionState.value = 'toggle-digital-running'
|
||||
busyEmployeeId.value = assetId
|
||||
|
||||
try {
|
||||
await (shouldEnable
|
||||
? activateAgentAsset(assetId, { actor: resolveActor() })
|
||||
: updateAgentAsset(assetId, { status: 'disabled' }, { actor: resolveActor() }))
|
||||
await refreshAfterMutation(assetId)
|
||||
toast(shouldEnable ? '已启用运行。' : '已停止运行。')
|
||||
} catch (error) {
|
||||
toast(error?.message || '运行状态更新失败,请稍后重试。')
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
busyEmployeeId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDigitalEmployeeSource() {
|
||||
if (!selectedEmployee.value || !canEditDigitalEmployeeSource.value || detailBusy.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const markdown = normalizeText(selectedEmployee.value.markdownContent)
|
||||
if (!markdown) {
|
||||
toast('Skills Markdown 源文件不能为空。')
|
||||
return
|
||||
}
|
||||
|
||||
const nextVersion = incrementVersion(selectedEmployee.value.currentVersion)
|
||||
actionState.value = 'save-digital-source'
|
||||
busyEmployeeId.value = selectedEmployee.value.id
|
||||
|
||||
try {
|
||||
await createAgentAssetVersion(
|
||||
selectedEmployee.value.id,
|
||||
{
|
||||
version: nextVersion,
|
||||
content: markdown,
|
||||
content_type: 'markdown',
|
||||
change_note: '通过数字员工页面更新 Skills Markdown 源文件。',
|
||||
created_by: resolveActor()
|
||||
},
|
||||
{ actor: resolveActor() }
|
||||
)
|
||||
await refreshAfterMutation(selectedEmployee.value.id)
|
||||
toast(`Skills 源文件已保存为 ${nextVersion}。`)
|
||||
} catch (error) {
|
||||
toast(error?.message || 'Skills 源文件保存失败,请稍后重试。')
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
busyEmployeeId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function openDigitalEmployeeSchedule(employee) {
|
||||
if (!employee || !canOperateDigitalEmployee.value) {
|
||||
return
|
||||
}
|
||||
scheduleTarget.value = employee
|
||||
scheduleForm.value = createDigitalEmployeeScheduleForm(resolveDigitalEmployeeScheduleValue(employee))
|
||||
scheduleEditorError.value = ''
|
||||
scheduleEditorOpen.value = true
|
||||
}
|
||||
|
||||
function closeDigitalEmployeeSchedule() {
|
||||
if (scheduleEditorBusy.value) {
|
||||
return
|
||||
}
|
||||
scheduleEditorOpen.value = false
|
||||
scheduleTarget.value = null
|
||||
scheduleEditorError.value = ''
|
||||
}
|
||||
|
||||
async function saveDigitalEmployeeSchedule() {
|
||||
const employee = scheduleTarget.value || selectedEmployee.value
|
||||
if (!employee || !canOperateDigitalEmployee.value || actionState.value) {
|
||||
return
|
||||
}
|
||||
|
||||
let cron = ''
|
||||
try {
|
||||
cron = buildDigitalEmployeeScheduleCron(scheduleForm.value)
|
||||
} catch (error) {
|
||||
scheduleEditorError.value = error?.message || '定时计划格式不正确。'
|
||||
return
|
||||
}
|
||||
|
||||
const nextConfig = buildDigitalEmployeeScheduleConfig(employee.configJson, cron)
|
||||
nextConfig.skill_category = employee.digitalEmployee?.skillCategory || employee.skillCategory || '整理'
|
||||
nextConfig.skill_category_options = DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS
|
||||
|
||||
actionState.value = 'save-digital-schedule'
|
||||
busyEmployeeId.value = employee.id
|
||||
|
||||
try {
|
||||
await updateAgentAsset(employee.id, { config_json: nextConfig }, { actor: resolveActor() })
|
||||
await refreshAfterMutation(employee.id)
|
||||
scheduleEditorOpen.value = false
|
||||
scheduleTarget.value = null
|
||||
scheduleEditorError.value = ''
|
||||
toast(cron ? `定时计划已更新为 ${formatDigitalEmployeeCron(cron)}。` : '已改为手动触发。')
|
||||
} catch (error) {
|
||||
scheduleEditorError.value = error?.message || '定时计划保存失败,请稍后重试。'
|
||||
toast(scheduleEditorError.value)
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
busyEmployeeId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function runDigitalEmployeeNow(employee) {
|
||||
if (!employee || !canOperateDigitalEmployee.value || actionState.value) {
|
||||
return
|
||||
}
|
||||
|
||||
actionState.value = 'run-digital-now'
|
||||
busyEmployeeId.value = employee.id
|
||||
|
||||
try {
|
||||
const result = await runOrchestrator({
|
||||
source: 'schedule',
|
||||
task_id: employee.id,
|
||||
message: employee.name,
|
||||
context_json: {
|
||||
manual_trigger: true,
|
||||
entry: 'digital_employees'
|
||||
}
|
||||
})
|
||||
toast(`已发起立即运行,Run ID:${result?.run_id || '-'}`)
|
||||
} catch (error) {
|
||||
toast(error?.message || '立即运行失败,请稍后重试。')
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
busyEmployeeId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadEmployees().catch(() => {})
|
||||
})
|
||||
</script>
|
||||
|
||||
<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/digital-employees-view.css"></style>
|
||||
@@ -2,6 +2,7 @@
|
||||
<PersonalWorkbench
|
||||
:show-header="false"
|
||||
:assistant-modal-open="assistantModalOpen"
|
||||
:workbench-summary="workbenchSummary"
|
||||
@open-assistant="emit('open-assistant', $event)"
|
||||
/>
|
||||
</template>
|
||||
@@ -10,7 +11,8 @@
|
||||
import PersonalWorkbench from '../components/business/PersonalWorkbench.vue'
|
||||
|
||||
defineProps({
|
||||
assistantModalOpen: { type: Boolean, default: false }
|
||||
assistantModalOpen: { type: Boolean, default: false },
|
||||
workbenchSummary: { type: Object, default: () => ({}) }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['open-assistant'])
|
||||
|
||||
@@ -70,11 +70,12 @@
|
||||
<tr>
|
||||
<th>文件名称</th>
|
||||
<th>标签</th>
|
||||
<th>上传时间 <i class="mdi mdi-arrow-down"></i></th>
|
||||
<th>版本</th>
|
||||
<th>状态</th>
|
||||
<th>上传人</th>
|
||||
<th>操作</th>
|
||||
<th>上传时间 <i class="mdi mdi-arrow-down"></i></th>
|
||||
<th>版本</th>
|
||||
<th>状态</th>
|
||||
<th>归纳时间</th>
|
||||
<th>上传人</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -96,40 +97,40 @@
|
||||
</td>
|
||||
<td>{{ doc.time }}</td>
|
||||
<td>{{ doc.version }}</td>
|
||||
<td>
|
||||
<div class="state-cell">
|
||||
<span class="state-tag" :class="doc.stateTone">{{ doc.state }}</span>
|
||||
<span v-if="doc.ingestTime" class="state-time">归纳时间:{{ doc.ingestTime }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ doc.owner }}</td>
|
||||
<td>
|
||||
<div class="row-actions" @click.stop>
|
||||
<td>
|
||||
<div class="state-cell">
|
||||
<span class="state-tag" :class="doc.stateTone">{{ doc.state }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="ingest-time-cell">{{ doc.ingestTime || '—' }}</td>
|
||||
<td>{{ doc.owner }}</td>
|
||||
<td>
|
||||
<div class="row-actions" @click.stop>
|
||||
<button
|
||||
class="more-btn"
|
||||
type="button"
|
||||
aria-label="下载文件"
|
||||
title="归纳时间:?"
|
||||
@click="handleDownload(doc)"
|
||||
>
|
||||
class="more-btn"
|
||||
type="button"
|
||||
aria-label="下载文件"
|
||||
title="下载文件"
|
||||
@click="handleDownload(doc)"
|
||||
>
|
||||
<i class="mdi mdi-download"></i>
|
||||
</button>
|
||||
<button
|
||||
v-if="isAdmin"
|
||||
class="more-btn danger"
|
||||
type="button"
|
||||
:disabled="deletingId === doc.id || Number(doc.stateCode || 0) === 2"
|
||||
aria-label="删除文件"
|
||||
title="归纳时间:?"
|
||||
@click="handleDelete(doc)"
|
||||
>
|
||||
type="button"
|
||||
:disabled="deletingId === doc.id || Number(doc.stateCode || 0) === 2"
|
||||
aria-label="删除文件"
|
||||
title="删除文件"
|
||||
@click="handleDelete(doc)"
|
||||
>
|
||||
<i class="mdi mdi-delete-outline"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="loading && !visibleDocuments.length">
|
||||
<td colspan="7" class="empty-row table-loading-row">
|
||||
<td colspan="8" class="empty-row table-loading-row">
|
||||
<TableLoadingState
|
||||
title="知识库文件同步中"
|
||||
message="正在加载当前文件夹的知识库文件"
|
||||
@@ -138,7 +139,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else-if="!visibleDocuments.length">
|
||||
<td colspan="7" class="empty-row">
|
||||
<td colspan="8" class="empty-row">
|
||||
当前文件夹暂无文件
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -154,7 +154,6 @@ export default {
|
||||
const assetBuckets = ref({
|
||||
financialRules: [],
|
||||
riskRules: [],
|
||||
skills: [],
|
||||
mcp: []
|
||||
})
|
||||
|
||||
@@ -173,7 +172,7 @@ export default {
|
||||
const showVersionColumn = computed(() => activeMeta.value.showVersionColumn !== false)
|
||||
const showStatusColumn = computed(() => activeMeta.value.showStatusColumn !== false)
|
||||
const showOnlineColumn = computed(() => false)
|
||||
const showEnabledColumn = computed(() => false)
|
||||
const showEnabledColumn = computed(() => activeMeta.value.showEnabledColumn === true)
|
||||
const selectedSkillIsRule = computed(() => selectedSkill.value?.type === 'rules')
|
||||
const selectedSkillUsesSpreadsheet = computed(
|
||||
() => selectedSkillIsRule.value && Boolean(selectedSkill.value?.usesSpreadsheetRule)
|
||||
@@ -241,7 +240,7 @@ export default {
|
||||
() => selectedSkillUsesJsonRisk.value && canManageSelected.value
|
||||
)
|
||||
const riskRuleCreateBusy = computed(() => actionState.value === 'generate-risk-rule')
|
||||
const canEditMarkdown = computed(() => canEditSelected.value && selectedSkillIsRule.value)
|
||||
const canEditMarkdown = computed(() => selectedSkillIsRule.value && canEditSelected.value)
|
||||
const isDisplayingWorkingVersion = computed(
|
||||
() => selectedSkill.value?.displayVersion === selectedSkill.value?.workingVersion
|
||||
)
|
||||
@@ -1401,7 +1400,7 @@ export default {
|
||||
version: nextVersion,
|
||||
content: buildMarkdownVersionContent(selectedSkill.value.markdownContent, runtimeRule),
|
||||
content_type: 'markdown',
|
||||
change_note: '通过任务规则中心保存 Markdown 规则内容,并同步运行时 JSON。',
|
||||
change_note: '通过规则中心保存 Markdown 规则内容,并同步运行时 JSON。',
|
||||
created_by: resolveActor()
|
||||
},
|
||||
{ actor: resolveActor() }
|
||||
@@ -1449,7 +1448,7 @@ export default {
|
||||
version: nextVersion,
|
||||
content: buildMarkdownVersionContent(selectedSkill.value.markdownContent, runtimeRule),
|
||||
content_type: 'markdown',
|
||||
change_note: '通过任务规则中心保存运行时 JSON 配置。',
|
||||
change_note: '通过规则中心保存运行时 JSON 配置。',
|
||||
created_by: resolveActor()
|
||||
},
|
||||
{ actor: resolveActor() }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const DIGITAL_EMPLOYEE_AGENT = 'hermes'
|
||||
export const DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS = ['积累', '升级', '整理', '评估']
|
||||
|
||||
const TASK_TYPE_LABELS = {
|
||||
daily_risk_scan: '每日风险巡检',
|
||||
@@ -7,11 +8,25 @@ const TASK_TYPE_LABELS = {
|
||||
weekly_expense_report: '周度费用洞察',
|
||||
rule_review_digest: '规则待审摘要',
|
||||
knowledge_index_sync: '知识库归集',
|
||||
llm_wiki_rule_formation: '知识库归集',
|
||||
x_financial_callback: '任务回调上报'
|
||||
}
|
||||
|
||||
const TASK_TYPE_SKILL_CATEGORIES = {
|
||||
daily_risk_scan: '评估',
|
||||
global_risk_scan: '评估',
|
||||
weekly_ar_summary: '整理',
|
||||
weekly_expense_report: '整理',
|
||||
rule_review_digest: '升级',
|
||||
knowledge_index_sync: '积累',
|
||||
llm_wiki_rule_formation: '积累',
|
||||
x_financial_callback: '升级'
|
||||
}
|
||||
|
||||
const CONTENT_LABELS = {
|
||||
task_type: '技能类型',
|
||||
task_type: '任务类型',
|
||||
skill_category: '技能类型',
|
||||
skill_category_options: '技能类型范围',
|
||||
schedule: '执行计划',
|
||||
cron: '调度表达式',
|
||||
folder: '归集范围',
|
||||
@@ -45,6 +60,14 @@ export function sanitizeDigitalEmployeeText(value, fallback = '') {
|
||||
return text || fallback
|
||||
}
|
||||
|
||||
export function sanitizeDigitalEmployeeSource(value, fallback = '') {
|
||||
const text = normalizeDigitalEmployeeText(value)
|
||||
.replace(/hermes/gi, '数字员工')
|
||||
.replace(/赫尔墨斯/g, '数字员工')
|
||||
.trim()
|
||||
return text || fallback
|
||||
}
|
||||
|
||||
export function sanitizeDigitalEmployeeName(value, fallback = '数字员工技能') {
|
||||
const text = sanitizeDigitalEmployeeText(value, fallback)
|
||||
.replace(/^数字员工[\s·::-]*/i, '')
|
||||
@@ -80,6 +103,22 @@ export function resolveDigitalEmployeeTaskType(source = {}, content = {}) {
|
||||
return raw.replace(/[-.]/g, '_')
|
||||
}
|
||||
|
||||
export function resolveDigitalEmployeeSkillCategory(source = {}, content = {}) {
|
||||
const config = source.config_json || source.configJson || {}
|
||||
const taskType = resolveDigitalEmployeeTaskType(source, content)
|
||||
const explicitCategory =
|
||||
normalizeDigitalEmployeeText(config.skill_category) ||
|
||||
normalizeDigitalEmployeeText(config.skillCategory) ||
|
||||
normalizeDigitalEmployeeText(content.skill_category) ||
|
||||
normalizeDigitalEmployeeText(content.skillCategory)
|
||||
|
||||
if (DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS.includes(explicitCategory)) {
|
||||
return explicitCategory
|
||||
}
|
||||
|
||||
return TASK_TYPE_SKILL_CATEGORIES[taskType] || '整理'
|
||||
}
|
||||
|
||||
export function isDigitalEmployeeAsset(source = {}) {
|
||||
const config = source.config_json || source.configJson || {}
|
||||
const haystack = [
|
||||
@@ -145,10 +184,10 @@ export function formatDigitalEmployeeCron(value) {
|
||||
export function resolveDigitalEmployeeSchedule(source = {}, content = {}) {
|
||||
const config = source.config_json || source.configJson || {}
|
||||
const raw =
|
||||
normalizeDigitalEmployeeText(content.schedule) ||
|
||||
normalizeDigitalEmployeeText(config.cron) ||
|
||||
normalizeDigitalEmployeeText(config.schedule) ||
|
||||
normalizeDigitalEmployeeText(config.cron_expression)
|
||||
normalizeDigitalEmployeeText(config.cron_expression) ||
|
||||
normalizeDigitalEmployeeText(content.schedule)
|
||||
return {
|
||||
value: raw,
|
||||
label: formatDigitalEmployeeCron(raw)
|
||||
@@ -205,9 +244,85 @@ export function buildDigitalEmployeeContentPreview(content = {}) {
|
||||
return sanitizeDigitalEmployeeText(JSON.stringify(visiblePayload, null, 2))
|
||||
}
|
||||
|
||||
function resolveDigitalEmployeeMarkdownFromContent(content = {}, config = {}) {
|
||||
const candidates = [
|
||||
content.skill_markdown,
|
||||
content.skills_markdown,
|
||||
content.source_markdown,
|
||||
content.markdown,
|
||||
content.skill_source,
|
||||
config.skill_markdown,
|
||||
config.skills_markdown,
|
||||
config.source_markdown,
|
||||
config.skill_source
|
||||
]
|
||||
return candidates.find((item) => normalizeDigitalEmployeeText(item)) || ''
|
||||
}
|
||||
|
||||
function buildDefaultDigitalEmployeeSource(source = {}, listMeta = {}, schedule = {}) {
|
||||
const name = listMeta.name || '数字员工技能'
|
||||
const description =
|
||||
listMeta.summary ||
|
||||
sanitizeDigitalEmployeeText(source.description, '该技能用于后台自动执行指定任务。')
|
||||
|
||||
return [
|
||||
'---',
|
||||
`name: ${listMeta.code || 'digital.skill'}`,
|
||||
`description: ${description}`,
|
||||
'---',
|
||||
'',
|
||||
`# ${name}`,
|
||||
'',
|
||||
'## 功能说明',
|
||||
'',
|
||||
description,
|
||||
'',
|
||||
'## 执行方式',
|
||||
'',
|
||||
`- 技能类型:${listMeta.skillCategory || '整理'}`,
|
||||
`- 可选类型:${DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS.join('、')}`,
|
||||
`- 执行计划:${schedule.label || '手动触发'}`,
|
||||
`- 触发方式:${listMeta.executionMode || '手动触发'}`,
|
||||
'',
|
||||
'## 操作要求',
|
||||
'',
|
||||
'- 按任务参数读取业务数据。',
|
||||
'- 运行完成后写回业务结果或运行日志。'
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export function buildDigitalEmployeeSourceMarkdown(source = {}, content = {}, listMeta = {}) {
|
||||
const config = source.config_json || source.configJson || {}
|
||||
if (
|
||||
normalizeDigitalEmployeeText(source.current_version_content_type) === 'markdown' &&
|
||||
typeof source.current_version_content === 'string'
|
||||
) {
|
||||
return sanitizeDigitalEmployeeSource(source.current_version_content)
|
||||
}
|
||||
|
||||
const schedule = resolveDigitalEmployeeSchedule(source, content)
|
||||
const sourceMarkdown = resolveDigitalEmployeeMarkdownFromContent(content, config)
|
||||
return sanitizeDigitalEmployeeSource(
|
||||
sourceMarkdown,
|
||||
buildDefaultDigitalEmployeeSource(source, listMeta, schedule)
|
||||
)
|
||||
}
|
||||
|
||||
function buildDigitalEmployeeBasicRows(source = {}, listMeta = {}, schedule = {}) {
|
||||
return [
|
||||
{ label: '技能编号', value: listMeta.code },
|
||||
{ label: '技能类型', value: listMeta.skillCategory },
|
||||
{ label: '维护人', value: listMeta.owner },
|
||||
{ label: '执行计划', value: schedule.label },
|
||||
{ label: '当前版本', value: source.working_version || source.current_version || '-' },
|
||||
{ label: '最近更新', value: source.updated_at || '-' }
|
||||
]
|
||||
}
|
||||
|
||||
export function buildDigitalEmployeeListMeta(source = {}) {
|
||||
const content = parseDigitalEmployeeContent(source.current_version_content)
|
||||
const taskType = resolveDigitalEmployeeTaskType(source, content)
|
||||
const skillCategory = resolveDigitalEmployeeSkillCategory(source, content)
|
||||
const schedule = resolveDigitalEmployeeSchedule(source, content)
|
||||
const enabled = resolveDigitalEmployeeEnabled(source)
|
||||
const fallbackName = TASK_TYPE_LABELS[taskType] || '数字员工技能'
|
||||
@@ -217,6 +332,8 @@ export function buildDigitalEmployeeListMeta(source = {}) {
|
||||
code: resolveDigitalEmployeeDisplayCode(source, content),
|
||||
summary: sanitizeDigitalEmployeeText(source.description, '面向后台自动执行的数字员工技能。'),
|
||||
category: '数字员工',
|
||||
skillCategory,
|
||||
skillCategoryOptions: DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS,
|
||||
owner: sanitizeDigitalEmployeeText(source.owner, '平台运营'),
|
||||
reviewer: sanitizeDigitalEmployeeText(source.reviewer, '系统'),
|
||||
scope: schedule.label,
|
||||
@@ -237,6 +354,7 @@ export function buildDigitalEmployeeDetailMeta(source = {}) {
|
||||
})
|
||||
const schedule = resolveDigitalEmployeeSchedule(source, content)
|
||||
const contentRows = buildDigitalEmployeeContentRows(content)
|
||||
const sourceMarkdown = buildDigitalEmployeeSourceMarkdown(source, content, listMeta)
|
||||
|
||||
return {
|
||||
...listMeta,
|
||||
@@ -245,13 +363,16 @@ export function buildDigitalEmployeeDetailMeta(source = {}) {
|
||||
source.description,
|
||||
'该技能由后台数字员工按计划执行,并把结果沉淀到对应业务资产或运行日志中。'
|
||||
),
|
||||
sourceMarkdown,
|
||||
basicRows: buildDigitalEmployeeBasicRows(source, listMeta, schedule),
|
||||
contentRows,
|
||||
contentPreview: buildDigitalEmployeeContentPreview(content),
|
||||
scheduleRows: [
|
||||
{ label: '执行计划', value: schedule.label },
|
||||
{ label: '调度表达式', value: schedule.value || '手动触发' },
|
||||
{ label: '启动状态', value: listMeta.enabledLabel, tone: listMeta.enabledTone },
|
||||
{ label: '执行方式', value: listMeta.executionMode }
|
||||
{ label: '执行方式', value: listMeta.executionMode },
|
||||
{ label: '技能类型', value: listMeta.skillCategory }
|
||||
],
|
||||
overviewRows: [
|
||||
{ label: '能力编号', value: listMeta.code },
|
||||
|
||||
@@ -22,24 +22,6 @@ export const TYPE_META = {
|
||||
typeLabel: '规则',
|
||||
tableColumns: RULE_TABLE_COLUMNS
|
||||
},
|
||||
skills: {
|
||||
assetType: 'skill',
|
||||
label: '技能',
|
||||
typeLabel: '技能',
|
||||
createButtonLabel: '技能已接入',
|
||||
hintText: '技能页签已接到真实资产 API,可查看输入、输出、依赖和场景信息。',
|
||||
searchPlaceholder: '搜索技能名称、编码或负责人',
|
||||
showMetricColumn: false,
|
||||
tableColumns: {
|
||||
name: '技能名称',
|
||||
category: '业务域',
|
||||
owner: '负责人',
|
||||
scope: '适用场景',
|
||||
runtime: '输入摘要',
|
||||
version: '当前版本',
|
||||
metric: ''
|
||||
}
|
||||
},
|
||||
mcp: {
|
||||
assetType: 'mcp',
|
||||
label: 'MCP',
|
||||
@@ -87,41 +69,10 @@ export const TAB_META = {
|
||||
showStatusColumn: true,
|
||||
badgeTone: 'rose'
|
||||
},
|
||||
skills: {
|
||||
...TYPE_META.skills,
|
||||
typeKey: 'skills',
|
||||
badgeTone: 'blue'
|
||||
},
|
||||
mcp: {
|
||||
...TYPE_META.mcp,
|
||||
typeKey: 'mcp',
|
||||
badgeTone: 'amber'
|
||||
},
|
||||
digitalWorkers: {
|
||||
assetType: 'task',
|
||||
typeKey: 'digitalWorkers',
|
||||
label: '数字员工',
|
||||
typeLabel: '数字员工',
|
||||
createButtonLabel: '数字员工已接入',
|
||||
hintText: '归集后台自动执行的数字员工技能,可查看技能内容、执行计划、启动状态和最近版本。',
|
||||
searchPlaceholder: '搜索数字员工技能、编号、执行计划或维护人',
|
||||
showMetricColumn: true,
|
||||
showRuntimeColumn: true,
|
||||
showVersionColumn: true,
|
||||
showStatusColumn: true,
|
||||
showEnabledColumn: true,
|
||||
tableColumns: {
|
||||
name: '技能名称',
|
||||
category: '归集标签',
|
||||
owner: '维护归口',
|
||||
scope: '执行计划',
|
||||
runtime: '触发方式',
|
||||
version: '当前版本',
|
||||
status: '资产状态',
|
||||
metric: '运行方式',
|
||||
updatedAt: '最近更新'
|
||||
},
|
||||
badgeTone: 'violet'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,24 +150,6 @@ export const DETAIL_TITLES = {
|
||||
publishTitle: '上线控制',
|
||||
publishDesc: '正式上线会调用后端激活接口,审核未通过时会被拦截。'
|
||||
},
|
||||
skills: {
|
||||
configTitle: '技能配置',
|
||||
configDesc: '展示技能编码、输入摘要、版本和业务域。',
|
||||
detailTitle: '技能结构',
|
||||
detailDesc: '按输入、输出和依赖组织技能定义。',
|
||||
outputTitle: '输出契约',
|
||||
outputDesc: '技能详情重点展示输入参数、输出参数和依赖能力。',
|
||||
ruleListTitle: '输出要求',
|
||||
checkListTitle: '当前快照',
|
||||
triggerTitle: '适用场景',
|
||||
triggerDesc: '当前技能注册到的场景标签',
|
||||
toolTitle: '依赖能力',
|
||||
toolDesc: '技能当前依赖的数据库或其他能力',
|
||||
historyTitle: '版本历史',
|
||||
historyDesc: '最近版本记录',
|
||||
publishTitle: '发布状态',
|
||||
publishDesc: '技能当前状态由资产中心统一管理。'
|
||||
},
|
||||
mcp: {
|
||||
configTitle: 'MCP 连接配置',
|
||||
configDesc: '展示服务地址、超时和调用方式。',
|
||||
@@ -234,24 +167,6 @@ export const DETAIL_TITLES = {
|
||||
historyDesc: '最近版本记录',
|
||||
publishTitle: '服务状态',
|
||||
publishDesc: 'MCP 资产已接入规则中心,但真实外部调用仍以后续链路集成为准。'
|
||||
},
|
||||
digitalWorkers: {
|
||||
configTitle: '技能档案',
|
||||
configDesc: '展示数字员工技能的编号、归口、执行计划和启停状态。',
|
||||
detailTitle: '技能内容',
|
||||
detailDesc: '展示当前版本记录的任务类型、调度范围和执行参数。',
|
||||
outputTitle: '执行安排',
|
||||
outputDesc: '展示什么时候执行、是否启动,以及当前运行方式。',
|
||||
ruleListTitle: '技能参数',
|
||||
checkListTitle: '启动状态',
|
||||
triggerTitle: '执行计划',
|
||||
triggerDesc: '当前技能的计划执行时间或触发方式。',
|
||||
toolTitle: '运行归口',
|
||||
toolDesc: '数字员工技能由后台调度执行,运行结果进入对应日志或业务资产。',
|
||||
historyTitle: '版本记录',
|
||||
historyDesc: '最近的技能配置快照。',
|
||||
publishTitle: '启动状态',
|
||||
publishDesc: '数字员工技能由资产状态和调度配置共同决定是否启动。'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,13 +34,6 @@ import {
|
||||
resolveRiskRuleSeverity,
|
||||
resolveRiskRuleSeverityLabel
|
||||
} from './auditViewRiskRuleModel.js'
|
||||
import {
|
||||
buildDigitalEmployeeContentRows,
|
||||
buildDigitalEmployeeDetailMeta,
|
||||
buildDigitalEmployeeListMeta,
|
||||
isDigitalEmployeeAsset,
|
||||
sanitizeDigitalEmployeeText
|
||||
} from './auditViewDigitalEmployeeModel.js'
|
||||
|
||||
const EXPENSE_TYPE_SCENARIO_LABELS = {
|
||||
travel: '差旅费',
|
||||
@@ -342,9 +335,6 @@ export function resolveTabId(source, typeKey) {
|
||||
if (typeKey === 'rules') {
|
||||
return resolveRuleTabId(source)
|
||||
}
|
||||
if (typeKey === 'digitalWorkers') {
|
||||
return isDigitalEmployeeAsset(source) ? 'digitalWorkers' : ''
|
||||
}
|
||||
return typeKey
|
||||
}
|
||||
|
||||
@@ -899,15 +889,9 @@ export function resolveTypeKey(assetType) {
|
||||
if (assetType === 'rule') {
|
||||
return 'rules'
|
||||
}
|
||||
if (assetType === 'skill') {
|
||||
return 'skills'
|
||||
}
|
||||
if (assetType === 'mcp') {
|
||||
return 'mcp'
|
||||
}
|
||||
if (assetType === 'task') {
|
||||
return 'digitalWorkers'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -965,15 +949,9 @@ export function buildRowRuntime(asset, typeKey) {
|
||||
if (typeKey === 'rules') {
|
||||
return formatSeverity(asset.config_json?.severity)
|
||||
}
|
||||
if (typeKey === 'skills') {
|
||||
return formatInputSummary(asset.config_json?.input_schema)
|
||||
}
|
||||
if (typeKey === 'mcp') {
|
||||
return normalizeText(asset.config_json?.endpoint) || '未配置地址'
|
||||
}
|
||||
if (typeKey === 'digitalWorkers') {
|
||||
return buildDigitalEmployeeListMeta(asset).executionMode
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -981,15 +959,9 @@ export function buildRowMetric(asset, typeKey) {
|
||||
if (typeKey === 'rules') {
|
||||
return normalizeText(asset.modified_by) || '未记录'
|
||||
}
|
||||
if (typeKey === 'skills') {
|
||||
return '进入详情查看输出'
|
||||
}
|
||||
if (typeKey === 'mcp') {
|
||||
return asset.config_json?.timeout_ms ? `${asset.config_json.timeout_ms} ms` : '未配置超时'
|
||||
}
|
||||
if (typeKey === 'digitalWorkers') {
|
||||
return buildDigitalEmployeeListMeta(asset).executionMode
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -1061,19 +1033,16 @@ export function buildListItem(asset) {
|
||||
? resolveRiskRuleScoreLabel(asset.config_json, asset.config_json) || resolveRiskRuleSeverityLabel(asset.config_json)
|
||||
: resolveRiskRuleSeverityLabel(asset.config_json)
|
||||
: ''
|
||||
const digitalMeta = typeKey === 'digitalWorkers' ? buildDigitalEmployeeListMeta(asset) : null
|
||||
const displayName = digitalMeta?.name || asset.name
|
||||
const displayCode = digitalMeta?.code || asset.code
|
||||
const displaySummary = digitalMeta?.summary || listSubtitle
|
||||
const displayOwner = digitalMeta?.owner || (isRiskRule ? creator : asset.owner)
|
||||
const displayReviewer = digitalMeta?.reviewer || reviewer
|
||||
const displayCategory = digitalMeta?.category || resolveDomainLabel(asset.domain)
|
||||
const displayScope =
|
||||
digitalMeta?.scope ||
|
||||
(typeKey === 'rules' ? ruleScenarioCategory || '閫氱敤' : formatScenarioList(asset.scenario_json))
|
||||
const displayEnabledValue = digitalMeta ? digitalMeta.enabled : isEnabledValue
|
||||
const displayEnabledLabel = digitalMeta?.enabledLabel || (isEnabledValue ? '鏄? : '鍚?)
|
||||
const displayEnabledTone = digitalMeta?.enabledTone || (isEnabledValue ? 'success' : 'disabled')
|
||||
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'
|
||||
|
||||
return {
|
||||
id: asset.id,
|
||||
@@ -1093,7 +1062,6 @@ export function buildListItem(asset) {
|
||||
category: displayCategory,
|
||||
owner: displayOwner,
|
||||
reviewer: displayReviewer,
|
||||
scope: typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(asset.scenario_json),
|
||||
scope: displayScope,
|
||||
riskCategory: ruleScenarioCategory,
|
||||
scenarioList: ruleScenarioList,
|
||||
@@ -1117,9 +1085,6 @@ export function buildListItem(asset) {
|
||||
isOnlineValue,
|
||||
isOnlineLabel: onlineMeta.label,
|
||||
isOnlineTone: onlineMeta.tone,
|
||||
isEnabledValue,
|
||||
isEnabledLabel: isEnabledValue ? '是' : '否',
|
||||
isEnabledTone: isEnabledValue ? 'success' : 'disabled',
|
||||
isEnabledValue: displayEnabledValue,
|
||||
isEnabledLabel: displayEnabledLabel,
|
||||
isEnabledTone: displayEnabledTone,
|
||||
@@ -1163,22 +1128,6 @@ export function buildRuleFields(detail) {
|
||||
]
|
||||
}
|
||||
|
||||
export function buildSkillFields(detail) {
|
||||
const content = detail.current_version_content || {}
|
||||
return [
|
||||
{ label: '技能编码', value: detail.code },
|
||||
{ label: '业务域', value: resolveDomainLabel(detail.domain) },
|
||||
{
|
||||
label: '输入参数',
|
||||
value: Array.isArray(content.inputs) && content.inputs.length ? content.inputs.join('、') : '未配置'
|
||||
},
|
||||
{
|
||||
label: '输出参数',
|
||||
value: Array.isArray(content.outputs) && content.outputs.length ? content.outputs.join('、') : '未配置'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export function buildMcpFields(detail, latestCall) {
|
||||
const content = detail.current_version_content || {}
|
||||
return [
|
||||
@@ -1196,9 +1145,6 @@ export function buildFields(detail, typeKey, latestCall) {
|
||||
if (typeKey === 'rules') {
|
||||
return buildRuleFields(detail)
|
||||
}
|
||||
if (typeKey === 'skills') {
|
||||
return buildSkillFields(detail)
|
||||
}
|
||||
if (typeKey === 'mcp') {
|
||||
return buildMcpFields(detail, latestCall)
|
||||
}
|
||||
@@ -1208,29 +1154,6 @@ export function buildFields(detail, typeKey, latestCall) {
|
||||
export function buildPromptSections(detail, typeKey) {
|
||||
const content = detail.current_version_content || {}
|
||||
|
||||
if (typeKey === 'skills') {
|
||||
return [
|
||||
{
|
||||
title: '输入参数',
|
||||
intent: '技能入口',
|
||||
content: Array.isArray(content.inputs) && content.inputs.length ? content.inputs.join('\n') : '未配置输入参数。'
|
||||
},
|
||||
{
|
||||
title: '输出参数',
|
||||
intent: '技能产出',
|
||||
content: Array.isArray(content.outputs) && content.outputs.length ? content.outputs.join('\n') : '未配置输出参数。'
|
||||
},
|
||||
{
|
||||
title: '依赖能力',
|
||||
intent: '外部依赖',
|
||||
content:
|
||||
Array.isArray(content.dependencies) && content.dependencies.length
|
||||
? content.dependencies.join('\n')
|
||||
: '当前技能未声明外部依赖。'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (typeKey === 'mcp') {
|
||||
return [
|
||||
{
|
||||
@@ -1274,14 +1197,6 @@ export function buildOutputRules(detail, typeKey) {
|
||||
]
|
||||
}
|
||||
|
||||
if (typeKey === 'skills') {
|
||||
return [
|
||||
`输入参数:${Array.isArray(content.inputs) && content.inputs.length ? content.inputs.join('、') : '未配置'}`,
|
||||
`输出参数:${Array.isArray(content.outputs) && content.outputs.length ? content.outputs.join('、') : '未配置'}`,
|
||||
`依赖能力:${Array.isArray(content.dependencies) && content.dependencies.length ? content.dependencies.join('、') : '未声明'}`
|
||||
]
|
||||
}
|
||||
|
||||
if (typeKey === 'mcp') {
|
||||
return [
|
||||
`服务地址:${normalizeText(detail.config_json?.endpoint) || '未配置'}`,
|
||||
@@ -1312,24 +1227,6 @@ export function buildTests(detail, typeKey, latestCall) {
|
||||
]
|
||||
}
|
||||
|
||||
if (typeKey === 'skills') {
|
||||
const content = detail.current_version_content || {}
|
||||
return [
|
||||
{
|
||||
name: '输入数量',
|
||||
input: detail.current_version || '暂无版本',
|
||||
result: `${content.inputs?.length || 0} 项`,
|
||||
tone: 'success'
|
||||
},
|
||||
{
|
||||
name: '输出数量',
|
||||
input: detail.current_version || '暂无版本',
|
||||
result: `${content.outputs?.length || 0} 项`,
|
||||
tone: 'success'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (typeKey === 'mcp') {
|
||||
return [
|
||||
{
|
||||
@@ -1356,15 +1253,6 @@ export function buildTests(detail, typeKey, latestCall) {
|
||||
export function buildTools(detail, typeKey, latestCall) {
|
||||
const content = detail.current_version_content || {}
|
||||
|
||||
if (typeKey === 'skills') {
|
||||
return (content.dependencies || []).map((item) => ({
|
||||
name: item,
|
||||
scope: '技能依赖',
|
||||
mode: '读取',
|
||||
tone: 'safe'
|
||||
}))
|
||||
}
|
||||
|
||||
if (typeKey === 'mcp') {
|
||||
return [
|
||||
{
|
||||
@@ -1454,40 +1342,32 @@ export function buildDetailViewModel(detail, runs) {
|
||||
const initialRiskRuleScore = resolveRiskRuleScore(configJson, configJson)
|
||||
const initialRiskRuleScoreLevel = resolveRiskRuleScoreLevel(configJson, configJson)
|
||||
const initialRiskRuleSeverity = initialRiskRuleScoreLevel || resolveRiskRuleSeverity(configJson)
|
||||
const digitalMeta = typeKey === 'digitalWorkers'
|
||||
? buildDigitalEmployeeDetailMeta({
|
||||
...detail,
|
||||
updated_at: formatDateTime(detail.updated_at)
|
||||
})
|
||||
: null
|
||||
const detailName = digitalMeta?.name || detail.name
|
||||
const detailCode = digitalMeta?.code || detail.code
|
||||
const detailSummary = digitalMeta?.description ||
|
||||
(usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : detail.description)
|
||||
const detailOwner = digitalMeta?.owner || detail.owner
|
||||
const detailReviewer = digitalMeta?.reviewer || detail.reviewer || detail.latest_review?.reviewer || '寰呭垎閰?
|
||||
const detailCategory = digitalMeta?.category || resolveDomainLabel(detail.domain)
|
||||
const detailScope =
|
||||
digitalMeta?.scope ||
|
||||
(typeKey === 'rules' ? ruleScenarioCategory || '閫氱敤' : formatScenarioList(detail.scenario_json))
|
||||
const detailEnabledValue = digitalMeta ? digitalMeta.enabled : isEnabledValue
|
||||
const detailEnabledLabel = digitalMeta?.enabledLabel || (isEnabledValue ? '鏄? : '鍚?)
|
||||
const detailEnabledTone = digitalMeta?.enabledTone || (isEnabledValue ? 'success' : 'disabled')
|
||||
const detailName = detail.name
|
||||
const detailCode = detail.code
|
||||
const detailSummary = usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : detail.description
|
||||
const detailOwner = detail.owner
|
||||
const detailReviewer = detail.reviewer || detail.latest_review?.reviewer || '待分配'
|
||||
const detailCategory = resolveDomainLabel(detail.domain)
|
||||
const detailScope = typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(detail.scenario_json)
|
||||
const detailEnabledValue = isEnabledValue
|
||||
const detailEnabledLabel = isEnabledValue ? '是' : '否'
|
||||
const detailEnabledTone = isEnabledValue ? 'success' : 'disabled'
|
||||
|
||||
return {
|
||||
id: detail.id,
|
||||
tabId,
|
||||
type: typeKey,
|
||||
typeLabel: tabMeta.typeLabel,
|
||||
short: makeShort(detail.name),
|
||||
name: detail.name,
|
||||
code: detail.code,
|
||||
summary: usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : detail.description,
|
||||
listSubtitle: usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : normalizeText(detail.description),
|
||||
owner: detail.owner,
|
||||
reviewer: detail.reviewer || detail.latest_review?.reviewer || '待分配',
|
||||
category: resolveDomainLabel(detail.domain),
|
||||
scope: typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(detail.scenario_json),
|
||||
short: makeShort(detailName),
|
||||
name: detailName,
|
||||
code: detailCode,
|
||||
rawCode: detail.code,
|
||||
summary: detailSummary,
|
||||
listSubtitle: normalizeText(detailSummary),
|
||||
owner: detailOwner,
|
||||
reviewer: detailReviewer,
|
||||
category: detailCategory,
|
||||
scope: detailScope,
|
||||
businessStageValue: businessStage.value,
|
||||
businessStageLabel: businessStage.label,
|
||||
version: detail.working_version || detail.current_version || '-',
|
||||
@@ -1524,9 +1404,9 @@ export function buildDetailViewModel(detail, runs) {
|
||||
isOnlineValue: onlineMeta.online,
|
||||
isOnlineLabel: onlineMeta.label,
|
||||
isOnlineTone: onlineMeta.tone,
|
||||
isEnabledValue,
|
||||
isEnabledLabel: isEnabledValue ? '是' : '否',
|
||||
isEnabledTone: isEnabledValue ? 'success' : 'disabled',
|
||||
isEnabledValue: detailEnabledValue,
|
||||
isEnabledLabel: detailEnabledLabel,
|
||||
isEnabledTone: detailEnabledTone,
|
||||
publisher:
|
||||
detail.status === 'active'
|
||||
? normalizeText(detail.published_by) ||
|
||||
|
||||
@@ -16,12 +16,12 @@ export function incrementVersion(version) {
|
||||
|
||||
export function buildReviewNote(status) {
|
||||
if (status === 'approved') {
|
||||
return '通过任务规则中心审核。'
|
||||
return '通过规则中心审核。'
|
||||
}
|
||||
if (status === 'rejected') {
|
||||
return '在任务规则中心驳回当前版本。'
|
||||
return '在规则中心驳回当前版本。'
|
||||
}
|
||||
return '提交任务规则中心待审核。'
|
||||
return '提交规则中心待审核。'
|
||||
}
|
||||
|
||||
export function buildRuleConfigPayload(asset, runtimeRule) {
|
||||
|
||||
122
web/src/views/scripts/digitalEmployeeScheduleModel.js
Normal file
122
web/src/views/scripts/digitalEmployeeScheduleModel.js
Normal file
@@ -0,0 +1,122 @@
|
||||
const DEFAULT_SCHEDULE_TIME = '00:00'
|
||||
const CRON_TOKEN_PATTERN = /^[\d*/,?\-]+$/
|
||||
|
||||
export const DIGITAL_EMPLOYEE_SCHEDULE_MODES = [
|
||||
{ value: 'manual', label: '手动触发' },
|
||||
{ value: 'daily', label: '每天执行' },
|
||||
{ value: 'weekly', label: '每周执行' },
|
||||
{ value: 'custom', label: 'Cron 表达式' }
|
||||
]
|
||||
|
||||
export const DIGITAL_EMPLOYEE_WEEKDAY_OPTIONS = [
|
||||
{ value: '1', label: '周一' },
|
||||
{ value: '2', label: '周二' },
|
||||
{ value: '3', label: '周三' },
|
||||
{ value: '4', label: '周四' },
|
||||
{ value: '5', label: '周五' },
|
||||
{ value: '6', label: '周六' },
|
||||
{ value: '0', label: '周日' }
|
||||
]
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value ?? '').trim()
|
||||
}
|
||||
|
||||
function normalizeTime(value) {
|
||||
const raw = normalizeText(value)
|
||||
return /^\d{2}:\d{2}$/.test(raw) ? raw : DEFAULT_SCHEDULE_TIME
|
||||
}
|
||||
|
||||
function toTime(hour, minute) {
|
||||
const hourNumber = Number(hour)
|
||||
const minuteNumber = Number(minute)
|
||||
if (!Number.isInteger(hourNumber) || hourNumber < 0 || hourNumber > 23) {
|
||||
return DEFAULT_SCHEDULE_TIME
|
||||
}
|
||||
if (!Number.isInteger(minuteNumber) || minuteNumber < 0 || minuteNumber > 59) {
|
||||
return DEFAULT_SCHEDULE_TIME
|
||||
}
|
||||
return `${String(hourNumber).padStart(2, '0')}:${String(minuteNumber).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
export function createDigitalEmployeeScheduleForm(cron = '') {
|
||||
const normalizedCron = normalizeText(cron)
|
||||
if (!normalizedCron) {
|
||||
return { mode: 'manual', time: DEFAULT_SCHEDULE_TIME, weekday: '1', cron: '' }
|
||||
}
|
||||
|
||||
const parts = normalizedCron.split(/\s+/)
|
||||
if (parts.length !== 5) {
|
||||
return { mode: 'custom', time: DEFAULT_SCHEDULE_TIME, weekday: '1', cron: normalizedCron }
|
||||
}
|
||||
|
||||
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts
|
||||
const time = toTime(hour, minute)
|
||||
|
||||
if (dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
|
||||
return { mode: 'daily', time, weekday: '1', cron: normalizedCron }
|
||||
}
|
||||
|
||||
if (dayOfMonth === '*' && month === '*' && dayOfWeek !== '*') {
|
||||
return { mode: 'weekly', time, weekday: dayOfWeek || '1', cron: normalizedCron }
|
||||
}
|
||||
|
||||
return { mode: 'custom', time, weekday: '1', cron: normalizedCron }
|
||||
}
|
||||
|
||||
export function resolveDigitalEmployeeScheduleValue(employee = {}) {
|
||||
const config = employee.configJson || {}
|
||||
return (
|
||||
normalizeText(employee.digitalEmployee?.scheduleValue) ||
|
||||
normalizeText(config.cron) ||
|
||||
normalizeText(config.schedule) ||
|
||||
normalizeText(config.cron_expression)
|
||||
)
|
||||
}
|
||||
|
||||
export function buildDigitalEmployeeScheduleCron(form = {}) {
|
||||
const mode = normalizeText(form.mode) || 'manual'
|
||||
if (mode === 'manual') {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (mode === 'custom') {
|
||||
const cron = normalizeText(form.cron)
|
||||
const parts = cron.split(/\s+/).filter(Boolean)
|
||||
if (parts.length !== 5 || !parts.every((part) => CRON_TOKEN_PATTERN.test(part))) {
|
||||
throw new Error('请输入 5 段 Cron 表达式。')
|
||||
}
|
||||
return parts.join(' ')
|
||||
}
|
||||
|
||||
const [hour, minute] = normalizeTime(form.time).split(':')
|
||||
if (mode === 'daily') {
|
||||
return `${Number(minute)} ${Number(hour)} * * *`
|
||||
}
|
||||
|
||||
if (mode === 'weekly') {
|
||||
const weekday = normalizeText(form.weekday) || '1'
|
||||
return `${Number(minute)} ${Number(hour)} * * ${weekday}`
|
||||
}
|
||||
|
||||
throw new Error('请选择有效的执行方式。')
|
||||
}
|
||||
|
||||
export function buildDigitalEmployeeScheduleConfig(config = {}, cron = '') {
|
||||
const nextConfig = config && typeof config === 'object' && !Array.isArray(config)
|
||||
? { ...config }
|
||||
: {}
|
||||
const normalizedCron = normalizeText(cron)
|
||||
|
||||
if (normalizedCron) {
|
||||
nextConfig.cron = normalizedCron
|
||||
nextConfig.schedule = normalizedCron
|
||||
nextConfig.cron_expression = normalizedCron
|
||||
return nextConfig
|
||||
}
|
||||
|
||||
delete nextConfig.cron
|
||||
delete nextConfig.schedule
|
||||
delete nextConfig.cron_expression
|
||||
return nextConfig
|
||||
}
|
||||
Reference in New Issue
Block a user