chore: 更新个人工作台和差旅报销相关功能
This commit is contained in:
@@ -234,6 +234,255 @@
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.flow-status-chip {
|
||||
min-height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.flow-status-chip.running {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.flow-status-chip.completed {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.flow-status-chip.failed {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.flow-icon-btn {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(203, 213, 225, 0.86);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: #475569;
|
||||
font-size: 16px;
|
||||
box-shadow: 0 6px 14px rgba(148, 163, 184, 0.12);
|
||||
}
|
||||
|
||||
.flow-icon-btn:hover:not(:disabled) {
|
||||
border-color: rgba(37, 99, 235, 0.28);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.flow-icon-btn:disabled {
|
||||
opacity: 0.46;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.flow-step-item {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.flow-step-item:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.flow-step-rail {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flow-step-rail::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 34px;
|
||||
bottom: -12px;
|
||||
left: 50%;
|
||||
width: 2px;
|
||||
transform: translateX(-50%);
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.flow-step-item:last-child .flow-step-rail::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flow-step-rail span {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 999px;
|
||||
background: #e2e8f0;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.flow-step-item.completed .flow-step-rail span {
|
||||
background: #10b981;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.flow-step-item.running .flow-step-rail span {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 0 5px rgba(37, 99, 235, 0.12);
|
||||
}
|
||||
|
||||
.flow-step-item.failed .flow-step-rail span {
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.flow-step-card {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
padding: 13px 14px;
|
||||
border: 1px solid #e5edf5;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
box-shadow: 0 8px 22px rgba(226, 232, 240, 0.34);
|
||||
}
|
||||
|
||||
.flow-step-item.running .flow-step-card {
|
||||
border-color: rgba(37, 99, 235, 0.42);
|
||||
background: linear-gradient(180deg, #f8fbff 0%, #eef6ff 100%);
|
||||
}
|
||||
|
||||
.flow-step-card header,
|
||||
.flow-step-side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.flow-step-card header {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flow-step-card strong {
|
||||
min-width: 0;
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.flow-step-side {
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.flow-step-status {
|
||||
min-height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 9px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.flow-step-status.completed {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.flow-step-status.running {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.flow-step-status.failed {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.flow-step-side time {
|
||||
min-width: 36px;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.flow-step-tool,
|
||||
.flow-step-detail,
|
||||
.flow-step-error {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.flow-step-detail {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.flow-step-error {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.flow-empty-state {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
align-content: center;
|
||||
gap: 8px;
|
||||
padding: 30px;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flow-empty-state i {
|
||||
font-size: 34px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.flow-empty-state strong {
|
||||
color: #0f172a;
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.flow-empty-state p {
|
||||
max-width: 260px;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@keyframes flowPulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.12);
|
||||
}
|
||||
}
|
||||
|
||||
.assistant-layout {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
@@ -1562,6 +1811,23 @@
|
||||
box-shadow: 0 6px 14px rgba(239, 68, 68, 0.16);
|
||||
}
|
||||
|
||||
.review-insight-switch-icon-btn.flow.available {
|
||||
border-color: rgba(37, 99, 235, 0.28);
|
||||
background: rgba(239, 246, 255, 0.96);
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.review-insight-switch-icon-btn.flow.active {
|
||||
border-color: rgba(37, 99, 235, 0.42);
|
||||
background: rgba(219, 234, 254, 0.98);
|
||||
color: #1d4ed8;
|
||||
box-shadow: 0 6px 14px rgba(37, 99, 235, 0.16);
|
||||
}
|
||||
|
||||
.review-insight-switch-icon-btn.flow.running i {
|
||||
animation: flowPulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.review-insight-switch-icon-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
@@ -2542,6 +2808,49 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.review-flow-panel {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.review-flow-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 34px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.review-flow-summary .flow-icon-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.review-flow-list {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 0;
|
||||
overflow-y: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.review-flow-panel .flow-step-card {
|
||||
border-radius: 14px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.review-flow-panel .flow-empty-state {
|
||||
min-height: 260px;
|
||||
}
|
||||
|
||||
.flow-empty-state.compact {
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.review-message-block {
|
||||
margin-top: 12px;
|
||||
}
|
||||
@@ -2558,12 +2867,14 @@
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 15px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(16, 185, 129, 0.14);
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(37, 99, 235, 0.14);
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(34, 197, 94, 0.08), transparent 28%),
|
||||
linear-gradient(180deg, #fbfffd 0%, #f6fbf9 100%);
|
||||
box-shadow: 0 8px 20px rgba(226, 232, 240, 0.28);
|
||||
radial-gradient(circle at top right, rgba(37, 99, 235, 0.08), transparent 30%),
|
||||
linear-gradient(180deg, #ffffff 0%, #f7fafc 100%);
|
||||
box-shadow:
|
||||
0 14px 30px rgba(15, 23, 42, 0.06),
|
||||
0 1px 0 rgba(255, 255, 255, 0.9) inset;
|
||||
}
|
||||
|
||||
.review-flow-card {
|
||||
@@ -2651,6 +2962,294 @@
|
||||
padding: 12px 4px 0;
|
||||
}
|
||||
|
||||
.review-followup-panel {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.review-followup-panel.pending {
|
||||
border-color: #e2e8f0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.review-followup-panel.ready {
|
||||
border-color: #d1fae5;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.review-followup-panel summary {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.review-followup-panel summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.review-followup-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s ease;
|
||||
}
|
||||
|
||||
.review-followup-head:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.review-followup-head-main {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.review-followup-mark {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #64748b;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.review-followup-panel.pending .review-followup-mark {
|
||||
background: #fffbeb;
|
||||
border-color: #fde68a;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.review-followup-panel.ready .review-followup-mark {
|
||||
background: #ecfdf5;
|
||||
border-color: #bbf7d0;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.review-followup-title-copy {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.review-followup-title-copy strong {
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.review-followup-title-copy p {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.review-followup-preview {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.review-followup-panel[open] .review-followup-preview {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.review-followup-preview span {
|
||||
min-height: 22px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 160px;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #475569;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.review-followup-side {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.review-followup-count {
|
||||
min-height: 26px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: none;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
font-size: 11px;
|
||||
font-weight: 850;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.review-followup-panel.pending .review-followup-count {
|
||||
border-color: #fde68a;
|
||||
background: #fffbeb;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.review-followup-panel.ready .review-followup-count {
|
||||
border-color: rgba(16, 185, 129, 0.22);
|
||||
background: #ecfdf5;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.review-followup-chevron {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 999px;
|
||||
color: #94a3b8;
|
||||
font-size: 16px;
|
||||
transition: transform 0.18s ease, color 0.18s ease;
|
||||
}
|
||||
|
||||
.review-followup-panel[open] .review-followup-chevron {
|
||||
transform: rotate(180deg);
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.review-followup-body {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 0 12px 12px;
|
||||
}
|
||||
|
||||
.review-followup-list {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.review-followup-item {
|
||||
display: grid;
|
||||
grid-template-columns: 30px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 52px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.review-followup-item:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.review-followup-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 9px;
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.review-followup-item.warning .review-followup-icon {
|
||||
background: #fffbeb;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.review-followup-item.danger .review-followup-icon {
|
||||
background: #fff1f2;
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.review-followup-item.ready .review-followup-icon {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.review-followup-copy {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.review-followup-copy strong {
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
font-weight: 850;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.review-followup-copy p {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.review-followup-status {
|
||||
min-height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #64748b;
|
||||
font-size: 10px;
|
||||
font-weight: 850;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.review-followup-item.warning .review-followup-status {
|
||||
border-color: #fde68a;
|
||||
background: transparent;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.review-followup-item.danger .review-followup-status {
|
||||
border-color: #fecdd3;
|
||||
background: transparent;
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.review-followup-item.ready .review-followup-status {
|
||||
border-color: #bbf7d0;
|
||||
background: transparent;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.review-followup-helper {
|
||||
margin: 0;
|
||||
padding: 9px 10px;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #eef2f7;
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.review-card-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -2671,10 +3270,10 @@
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #22c55e, #10b981);
|
||||
background: linear-gradient(135deg, #2563eb, #0f766e);
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
box-shadow: 0 8px 16px rgba(16, 185, 129, 0.16);
|
||||
box-shadow: 0 8px 16px rgba(37, 99, 235, 0.16);
|
||||
}
|
||||
|
||||
.review-card-head-copy {
|
||||
@@ -3674,6 +4273,57 @@
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.welcome-quick-actions {
|
||||
margin-top: 14px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed rgba(203, 213, 225, 0.82);
|
||||
}
|
||||
|
||||
.welcome-quick-actions-title {
|
||||
margin: 0 0 10px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.welcome-quick-action-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.welcome-quick-action-btn {
|
||||
min-height: 36px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid rgba(191, 219, 254, 0.92);
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(239, 246, 255, 0.94) 100%);
|
||||
color: #1d4ed8;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
box-shadow: 0 6px 14px rgba(59, 130, 246, 0.08);
|
||||
transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
|
||||
}
|
||||
|
||||
.welcome-quick-action-btn i {
|
||||
font-size: 15px;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.welcome-quick-action-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(59, 130, 246, 0.34);
|
||||
box-shadow: 0 10px 18px rgba(59, 130, 246, 0.14);
|
||||
}
|
||||
|
||||
.welcome-quick-action-btn:disabled {
|
||||
opacity: 0.48;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.welcome-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
@@ -3864,6 +4514,7 @@
|
||||
.insight-panel-shell:not(.collapsed) {
|
||||
max-height: min(34dvh, 360px);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
@@ -3900,6 +4551,10 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.flow-step-card header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.assistant-layout {
|
||||
padding: 14px;
|
||||
}
|
||||
@@ -3962,6 +4617,19 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.review-followup-head {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.review-followup-side {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.review-followup-count {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -3981,6 +4649,15 @@
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.review-followup-item {
|
||||
grid-template-columns: 34px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.review-followup-status {
|
||||
grid-column: 2;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.review-footer-btn-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -280,8 +280,21 @@ async function handleExpenseConversationAction() {
|
||||
return
|
||||
}
|
||||
|
||||
pendingAction.value = 'expense'
|
||||
const nextPayload = buildAssistantPayload()
|
||||
const shouldOpenImmediately = Boolean(nextPayload.prompt || nextPayload.files.length)
|
||||
|
||||
if (shouldOpenImmediately) {
|
||||
emitAssistant({
|
||||
...nextPayload,
|
||||
conversation: null
|
||||
})
|
||||
void clearKnowledgeHistoryBeforeExpense().catch((error) => {
|
||||
console.warn('Failed to clear knowledge history before expense:', error)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
pendingAction.value = 'expense'
|
||||
|
||||
try {
|
||||
await clearKnowledgeHistoryBeforeExpense()
|
||||
@@ -1131,4 +1144,3 @@ watch(
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
<template>
|
||||
<PersonalWorkbench :show-header="false" @open-assistant="emit('openAssistant', $event)" />
|
||||
<PersonalWorkbench
|
||||
:show-header="false"
|
||||
:assistant-modal-open="assistantModalOpen"
|
||||
@open-assistant="emit('openAssistant', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import PersonalWorkbench from '../components/business/PersonalWorkbench.vue'
|
||||
|
||||
defineProps({
|
||||
assistantModalOpen: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['openAssistant'])
|
||||
</script>
|
||||
|
||||
@@ -46,7 +46,13 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="assistant-layout" :class="{ 'can-show-insight': hasInsightPanelContent, 'has-insight': showInsightPanel }">
|
||||
<div
|
||||
class="assistant-layout"
|
||||
:class="{
|
||||
'can-show-insight': hasInsightPanelContent,
|
||||
'has-insight': showInsightPanel
|
||||
}"
|
||||
>
|
||||
<section class="dialog-panel">
|
||||
<div v-if="shortcuts.length" class="dialog-toolbar">
|
||||
<button
|
||||
@@ -72,7 +78,7 @@
|
||||
<span class="message-avatar">
|
||||
<img
|
||||
:src="message.role === 'assistant' ? aiAvatar : userAvatar"
|
||||
:alt="message.role === 'assistant' ? 'AI 助手头像' : '用户头像'"
|
||||
:alt="message.role === 'assistant' ? '财务助手头像' : '用户头像'"
|
||||
/>
|
||||
</span>
|
||||
|
||||
@@ -92,9 +98,9 @@
|
||||
v-else-if="message.text && message.role === 'assistant'"
|
||||
class="message-answer-content message-answer-markdown"
|
||||
v-html="renderMarkdown(message.text)"
|
||||
></motion>
|
||||
></div>
|
||||
|
||||
<motion
|
||||
<div
|
||||
v-if="message.role === 'assistant' && message.welcomeQuickActions?.length"
|
||||
class="welcome-quick-actions"
|
||||
>
|
||||
@@ -111,12 +117,12 @@
|
||||
<i :class="action.icon"></i>
|
||||
<span>{{ action.label }}</span>
|
||||
</button>
|
||||
</motion>
|
||||
</motion>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.meta?.length" class="message-meta-row">
|
||||
<span v-for="item in message.meta" :key="item" class="message-meta-chip">{{ item }}</span>
|
||||
</motion>
|
||||
</div>
|
||||
|
||||
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.riskFlags?.length" class="message-detail-block">
|
||||
<strong>风险标签</strong>
|
||||
@@ -256,35 +262,62 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<details class="review-disclosure-card" :open="shouldOpenReviewDisclosure(message.reviewPayload)">
|
||||
<summary class="review-disclosure-summary">
|
||||
<div class="review-disclosure-copy">
|
||||
<strong>{{ buildReviewDisclosureTitle(message.reviewPayload) }}</strong>
|
||||
<p>{{ buildReviewDisclosureHint(message.reviewPayload) }}</p>
|
||||
<details
|
||||
class="review-followup-panel"
|
||||
:class="buildReviewStateTone(message.reviewPayload, message.draftPayload)"
|
||||
:open="shouldOpenReviewDisclosure(message.reviewPayload)"
|
||||
>
|
||||
<summary class="review-followup-head">
|
||||
<div class="review-followup-head-main">
|
||||
<span class="review-followup-mark">
|
||||
<i :class="buildReviewStateTone(message.reviewPayload, message.draftPayload) === 'ready' ? 'mdi mdi-clipboard-check-outline' : 'mdi mdi-clipboard-text-clock-outline'"></i>
|
||||
</span>
|
||||
<div class="review-followup-title-copy">
|
||||
<strong>{{ buildReviewTodoSectionTitle(message.reviewPayload) }}</strong>
|
||||
<p>{{ buildReviewDisclosureHint(message.reviewPayload) }}</p>
|
||||
<div class="review-followup-preview">
|
||||
<span
|
||||
v-for="item in buildReviewTodoItems(message.reviewPayload).slice(0, 2)"
|
||||
:key="`${message.id}-preview-${item.key}`"
|
||||
>
|
||||
{{ item.title }}
|
||||
</span>
|
||||
<span v-if="buildReviewTodoItems(message.reviewPayload).length > 2">
|
||||
+{{ buildReviewTodoItems(message.reviewPayload).length - 2 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="review-followup-side">
|
||||
<span class="review-followup-count">{{ buildReviewTodoSectionMeta(message.reviewPayload) }}</span>
|
||||
<span class="review-followup-chevron">
|
||||
<i class="mdi mdi-chevron-down"></i>
|
||||
</span>
|
||||
</div>
|
||||
<span class="review-disclosure-toggle">
|
||||
<i class="mdi mdi-chevron-down"></i>
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
<div class="review-disclosure-body">
|
||||
<div class="review-pending-list plain">
|
||||
<!-- 待补充信息项 -->
|
||||
<div class="review-followup-body">
|
||||
<div class="review-followup-list">
|
||||
<article
|
||||
v-for="item in buildReviewTodoItems(message.reviewPayload)"
|
||||
:key="`${message.id}-${item.key}`"
|
||||
class="review-pending-item"
|
||||
class="review-followup-item"
|
||||
:class="item.tone"
|
||||
>
|
||||
<span class="review-pending-icon">
|
||||
<span class="review-followup-icon">
|
||||
<i :class="item.icon"></i>
|
||||
</span>
|
||||
<div class="review-pending-copy">
|
||||
<div class="review-followup-copy">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<p>{{ item.hint }}</p>
|
||||
</div>
|
||||
<span class="review-pending-status" :class="item.tone">{{ item.status }}</span>
|
||||
<span class="review-followup-status">{{ item.status }}</span>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<p v-if="buildReviewDecisionHint(message.reviewPayload)" class="review-followup-helper">
|
||||
{{ buildReviewDecisionHint(message.reviewPayload) }}
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -538,7 +571,7 @@
|
||||
:placeholder="composerPlaceholder"
|
||||
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
||||
@input="handleComposerInput"
|
||||
@keydown.enter.exact.stop
|
||||
@keydown.enter.exact.prevent="handleComposerEnter"
|
||||
@keydown.ctrl.enter.prevent="submitComposer"
|
||||
/>
|
||||
</div>
|
||||
@@ -558,9 +591,13 @@
|
||||
:aria-hidden="(!showInsightPanel).toString()"
|
||||
>
|
||||
<aside class="insight-panel">
|
||||
<div v-if="!isKnowledgeSession" class="insight-head" :class="{ 'review-mode': activeReviewPayload }">
|
||||
<div
|
||||
v-if="!isKnowledgeSession"
|
||||
class="insight-head"
|
||||
:class="{ 'review-mode': activeReviewPayload || isReviewFlowDrawer }"
|
||||
>
|
||||
<div>
|
||||
<div v-if="!activeReviewPayload" class="insight-head-eyebrow">
|
||||
<div v-if="!activeReviewPayload && !isReviewFlowDrawer" class="insight-head-eyebrow">
|
||||
<span class="intent-pill" :class="currentInsight.intent">{{ currentIntentLabel }}</span>
|
||||
</div>
|
||||
<div v-else class="review-insight-title-row">
|
||||
@@ -568,12 +605,13 @@
|
||||
<h3>{{ reviewDrawerTitle }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<h3 v-if="!activeReviewPayload">{{ currentInsight.title }}</h3>
|
||||
<p v-if="!activeReviewPayload">{{ currentInsight.summary }}</p>
|
||||
<h3 v-if="!activeReviewPayload && !isReviewFlowDrawer">{{ currentInsight.title }}</h3>
|
||||
<p v-if="!activeReviewPayload && !isReviewFlowDrawer">{{ currentInsight.summary }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="activeReviewPayload" class="review-insight-tools">
|
||||
<div v-if="activeReviewPayload || isReviewFlowDrawer" class="review-insight-tools">
|
||||
<button
|
||||
v-if="activeReviewPayload"
|
||||
type="button"
|
||||
class="review-insight-switch-icon-btn"
|
||||
:class="{
|
||||
@@ -589,6 +627,7 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="activeReviewPayload"
|
||||
type="button"
|
||||
class="review-insight-switch-icon-btn risk"
|
||||
:class="{
|
||||
@@ -602,9 +641,25 @@
|
||||
>
|
||||
<i :class="reviewRiskDrawerIcon"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="review-insight-switch-icon-btn flow"
|
||||
:class="{
|
||||
available: reviewFlowDrawerAvailable,
|
||||
active: reviewFlowDrawerAvailable && isReviewFlowDrawer,
|
||||
running: flowOverallStatusTone === 'running'
|
||||
}"
|
||||
:disabled="!reviewFlowDrawerAvailable || submitting || reviewActionBusy"
|
||||
:title="reviewFlowDrawerLabel"
|
||||
:aria-label="reviewFlowDrawerLabel"
|
||||
@click="toggleReviewFlowDrawer"
|
||||
>
|
||||
<i :class="reviewFlowDrawerIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="confidence-card" v-if="!activeReviewPayload">
|
||||
<div class="confidence-card" v-if="!activeReviewPayload && !isReviewFlowDrawer">
|
||||
<span>{{ currentInsight.metricLabel }}</span>
|
||||
<strong>{{ currentInsight.metricValue }}</strong>
|
||||
</div>
|
||||
@@ -639,9 +694,60 @@
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isReviewFlowDrawer">
|
||||
<section class="review-flow-panel">
|
||||
<div class="review-flow-summary">
|
||||
<span class="flow-status-chip" :class="flowOverallStatusTone">{{ flowOverallStatusText }}</span>
|
||||
<span>总耗时 {{ flowTotalDurationText }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="flow-icon-btn"
|
||||
:disabled="!flowRunId || flowRefreshBusy"
|
||||
title="刷新流程"
|
||||
aria-label="刷新流程"
|
||||
@click="refreshFlowRunDetail"
|
||||
>
|
||||
<i :class="flowRefreshBusy ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-refresh'"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="flowSteps.length" class="review-flow-list">
|
||||
<article
|
||||
v-for="(step, index) in flowSteps"
|
||||
:key="step.key"
|
||||
class="flow-step-item"
|
||||
:class="step.status"
|
||||
>
|
||||
<div class="flow-step-rail">
|
||||
<span>{{ index + 1 }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flow-step-card">
|
||||
<header>
|
||||
<strong>{{ step.title }}</strong>
|
||||
<div class="flow-step-side">
|
||||
<span class="flow-step-status" :class="step.status">{{ resolveFlowStepStatusLabel(step) }}</span>
|
||||
<time>{{ formatFlowStepDuration(step) }}</time>
|
||||
</div>
|
||||
</header>
|
||||
<p class="flow-step-tool">工具:{{ step.tool }}</p>
|
||||
<p class="flow-step-detail">{{ resolveFlowStepDetail(step) }}</p>
|
||||
<p v-if="step.error" class="flow-step-error">{{ step.error }}</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-else class="flow-empty-state compact">
|
||||
<i class="mdi mdi-timeline-question-outline"></i>
|
||||
<strong>暂无识别流程</strong>
|
||||
<p>发起识别后,这里会显示调用步骤和耗时。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="currentInsight.intent === 'agent' && currentInsight.agent">
|
||||
<template v-if="activeReviewPayload">
|
||||
<template v-if="!isReviewDocumentDrawer && !isReviewRiskDrawer">
|
||||
<template v-if="!isReviewDocumentDrawer && !isReviewRiskDrawer && !isReviewFlowDrawer">
|
||||
<section class="review-side-card review-side-overview-card">
|
||||
<div class="review-side-intent-row">
|
||||
<i class="mdi mdi-account-outline"></i>
|
||||
@@ -778,6 +884,57 @@
|
||||
|
||||
</template>
|
||||
|
||||
<template v-else-if="isReviewFlowDrawer">
|
||||
<section class="review-flow-panel">
|
||||
<div class="review-flow-summary">
|
||||
<span class="flow-status-chip" :class="flowOverallStatusTone">{{ flowOverallStatusText }}</span>
|
||||
<span>总耗时 {{ flowTotalDurationText }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="flow-icon-btn"
|
||||
:disabled="!flowRunId || flowRefreshBusy"
|
||||
title="刷新流程"
|
||||
aria-label="刷新流程"
|
||||
@click="refreshFlowRunDetail"
|
||||
>
|
||||
<i :class="flowRefreshBusy ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-refresh'"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="flowSteps.length" class="review-flow-list">
|
||||
<article
|
||||
v-for="(step, index) in flowSteps"
|
||||
:key="step.key"
|
||||
class="flow-step-item"
|
||||
:class="step.status"
|
||||
>
|
||||
<div class="flow-step-rail">
|
||||
<span>{{ index + 1 }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flow-step-card">
|
||||
<header>
|
||||
<strong>{{ step.title }}</strong>
|
||||
<div class="flow-step-side">
|
||||
<span class="flow-step-status" :class="step.status">{{ resolveFlowStepStatusLabel(step) }}</span>
|
||||
<time>{{ formatFlowStepDuration(step) }}</time>
|
||||
</div>
|
||||
</header>
|
||||
<p class="flow-step-tool">工具:{{ step.tool }}</p>
|
||||
<p class="flow-step-detail">{{ resolveFlowStepDetail(step) }}</p>
|
||||
<p v-if="step.error" class="flow-step-error">{{ step.error }}</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-else class="flow-empty-state compact">
|
||||
<i class="mdi mdi-timeline-question-outline"></i>
|
||||
<strong>暂无识别流程</strong>
|
||||
<p>发起识别后,这里会显示调用步骤和耗时。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isReviewDocumentDrawer">
|
||||
<section class="review-side-card review-document-switch-card review-ticket-drawer">
|
||||
<div class="review-side-head review-document-switch-head">
|
||||
@@ -996,6 +1153,7 @@
|
||||
</Transition>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -185,6 +185,7 @@ const SESSION_TYPE_KNOWLEDGE = 'knowledge'
|
||||
const REVIEW_DRAWER_MODE_REVIEW = 'review'
|
||||
const REVIEW_DRAWER_MODE_DOCUMENTS = 'documents'
|
||||
const REVIEW_DRAWER_MODE_RISK = 'risk'
|
||||
const REVIEW_DRAWER_MODE_FLOW = 'flow'
|
||||
const FLOW_STEP_STATUS_PENDING = 'pending'
|
||||
const FLOW_STEP_STATUS_RUNNING = 'running'
|
||||
const FLOW_STEP_STATUS_COMPLETED = 'completed'
|
||||
@@ -208,6 +209,12 @@ const FLOW_STEP_FALLBACKS = {
|
||||
runningText: '正在识别票据附件...',
|
||||
completedText: '票据识别完成'
|
||||
},
|
||||
agent: {
|
||||
title: '智能体编排',
|
||||
tool: 'UserAgent',
|
||||
runningText: '正在调用财务智能体...',
|
||||
completedText: '智能体处理完成'
|
||||
},
|
||||
result: {
|
||||
title: '生成结果',
|
||||
tool: 'ResultGenerator',
|
||||
@@ -230,7 +237,7 @@ const EXPENSE_WELCOME_QUICK_ACTIONS = [
|
||||
},
|
||||
{
|
||||
label: '交通费报销',
|
||||
prompt: '我要报销交通出行费用,请帮我识别常见票据类型和报销注意事项。',
|
||||
prompt: '我要报销交通出行费用,请帮我识别场景并列出待补充信息。',
|
||||
icon: 'mdi mdi-car-outline'
|
||||
},
|
||||
{
|
||||
@@ -250,41 +257,6 @@ const EXPENSE_WELCOME_QUICK_ACTIONS = [
|
||||
}
|
||||
]
|
||||
|
||||
const ASSISTANT_DISPLAY_NAME = '财务助手'
|
||||
|
||||
const EXPENSE_WELCOME_QUICK_ACTIONS = [
|
||||
{
|
||||
label: '发起差旅报销',
|
||||
prompt: '我要报销一笔出差费用,请帮我说明需要准备的材料,并引导我上传票据。',
|
||||
icon: 'mdi mdi-bag-suitcase-outline'
|
||||
},
|
||||
{
|
||||
label: '招待费报销',
|
||||
prompt: '我要报销客户招待餐费,请告诉我需要补充的客户、参与人员和票据要求。',
|
||||
icon: 'mdi mdi-food-fork-drink'
|
||||
},
|
||||
{
|
||||
label: '交通费报销',
|
||||
prompt: '我要报销交通出行费用,请帮我识别场景并列出待补充信息。',
|
||||
icon: 'mdi mdi-car-outline'
|
||||
},
|
||||
{
|
||||
label: '上传票据识别',
|
||||
prompt: '我已准备好票据,请帮我识别票据内容并生成报销草稿。',
|
||||
icon: 'mdi mdi-file-upload-outline'
|
||||
},
|
||||
{
|
||||
label: '查询近期报销',
|
||||
prompt: '帮我查询近10天的报销记录和金额汇总。',
|
||||
icon: 'mdi mdi-chart-timeline-variant'
|
||||
},
|
||||
{
|
||||
label: '解释报销风险',
|
||||
prompt: '请结合公司制度,说明酒店超标、发票抬头不一致等常见报销风险与处理方式。',
|
||||
icon: 'mdi mdi-shield-alert-outline'
|
||||
}
|
||||
]
|
||||
|
||||
const HOT_KNOWLEDGE_QUESTIONS = [
|
||||
'差旅住宿标准按什么规则执行?',
|
||||
'酒店超标后如何申请例外报销?',
|
||||
@@ -373,8 +345,39 @@ function formatMessageTime(value) {
|
||||
})
|
||||
}
|
||||
|
||||
function createFlowSteps() {
|
||||
return []
|
||||
function createFlowSteps(options = {}) {
|
||||
const keys = []
|
||||
if (options.includeIntent) {
|
||||
keys.push('intent')
|
||||
}
|
||||
if (options.includeOcr) {
|
||||
keys.push('ocr')
|
||||
}
|
||||
if (options.includeExtraction) {
|
||||
keys.push('extraction')
|
||||
}
|
||||
if (options.includeAgent) {
|
||||
keys.push('agent')
|
||||
}
|
||||
if (options.includeResult) {
|
||||
keys.push('result')
|
||||
}
|
||||
|
||||
return keys.map((key, index) => {
|
||||
const definition = FLOW_STEP_FALLBACKS[key] || {}
|
||||
return {
|
||||
key,
|
||||
index: index + 1,
|
||||
title: definition.title || '智能体工具调用',
|
||||
tool: definition.tool || 'AgentTool',
|
||||
status: FLOW_STEP_STATUS_PENDING,
|
||||
detail: '',
|
||||
durationMs: null,
|
||||
startedAt: 0,
|
||||
finishedAt: 0,
|
||||
error: ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function formatSemanticEntityValue(entity) {
|
||||
@@ -2888,7 +2891,6 @@ export default {
|
||||
url: ''
|
||||
})
|
||||
const sessionSwitchBusy = ref(false)
|
||||
const flowPanelOpen = ref(false)
|
||||
const flowRunId = ref('')
|
||||
const flowStartedAt = ref(0)
|
||||
const flowFinishedAt = ref(0)
|
||||
@@ -2950,8 +2952,24 @@ export default {
|
||||
}
|
||||
return total ? `待执行 0/${total}` : '暂无流程'
|
||||
})
|
||||
const flowTotalDurationText = computed(() => {
|
||||
if (!flowStartedAt.value) {
|
||||
return '--'
|
||||
}
|
||||
|
||||
const finishedAt = flowFinishedAt.value || (runningFlowStep.value ? flowTick.value : 0)
|
||||
if (finishedAt > flowStartedAt.value) {
|
||||
return formatFlowDuration(finishedAt - flowStartedAt.value)
|
||||
}
|
||||
|
||||
const measuredDuration = flowSteps.value.reduce((total, step) => {
|
||||
const duration = Number(step.durationMs)
|
||||
return total + (Number.isFinite(duration) && duration > 0 ? duration : 0)
|
||||
}, 0)
|
||||
return measuredDuration ? formatFlowDuration(measuredDuration) : '--'
|
||||
})
|
||||
const hasInsightPanelContent = computed(
|
||||
() => isKnowledgeSession.value || currentInsight.value.intent !== 'welcome'
|
||||
() => isKnowledgeSession.value || currentInsight.value.intent !== 'welcome' || flowSteps.value.length > 0
|
||||
)
|
||||
const showInsightPanel = computed(() => hasInsightPanelContent.value && !insightPanelCollapsed.value)
|
||||
const insightPanelToggleLabel = computed(() =>
|
||||
@@ -3027,18 +3045,22 @@ export default {
|
||||
const reviewDocumentDrawerAvailable = computed(() => reviewDocumentCount.value > 0)
|
||||
const reviewRiskDrawerAvailable = computed(() => !reviewRiskEmpty.value)
|
||||
const reviewRiskActionAvailable = computed(() => reviewRiskItems.value.length > 0)
|
||||
const reviewFlowDrawerAvailable = computed(() => flowSteps.value.length > 0)
|
||||
const recognizedNarratives = computed(() => buildReviewRecognizedLines(activeReviewPayload.value))
|
||||
const reviewRecognitionNotes = computed(() => buildReviewRecognitionNotes(activeReviewPayload.value))
|
||||
const reviewDocumentSummaries = computed(() => buildReviewDocumentSummaries(activeReviewPayload.value))
|
||||
const reviewDocumentCount = computed(() => reviewDocumentDrafts.value.length)
|
||||
const isReviewDocumentDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_DOCUMENTS)
|
||||
const isReviewRiskDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_RISK)
|
||||
const isReviewFlowDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW)
|
||||
const reviewDrawerTitle = computed(() => (
|
||||
isReviewDocumentDrawer.value
|
||||
? '票据识别结果'
|
||||
: isReviewRiskDrawer.value
|
||||
? '风险提示'
|
||||
: '报销识别核对'
|
||||
: isReviewFlowDrawer.value
|
||||
? '调用流程'
|
||||
: '报销识别核对'
|
||||
))
|
||||
const reviewDocumentDrawerLabel = computed(() => (
|
||||
isReviewDocumentDrawer.value ? '显示核对' : '显示票据'
|
||||
@@ -3056,6 +3078,14 @@ export default {
|
||||
? 'mdi mdi-shield-alert'
|
||||
: 'mdi mdi-shield-alert-outline'
|
||||
))
|
||||
const reviewFlowDrawerLabel = computed(() => (
|
||||
isReviewFlowDrawer.value ? '显示核对' : '显示流程'
|
||||
))
|
||||
const reviewFlowDrawerIcon = computed(() => (
|
||||
isReviewFlowDrawer.value
|
||||
? 'mdi mdi-timeline-clock'
|
||||
: 'mdi mdi-timeline-clock-outline'
|
||||
))
|
||||
const activeReviewDocument = computed(() => reviewDocumentDrafts.value[activeReviewDocumentIndex.value] ?? null)
|
||||
const activeReviewDocumentPreview = computed(() =>
|
||||
activeReviewDocument.value
|
||||
@@ -3305,6 +3335,15 @@ export default {
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => reviewFlowDrawerAvailable.value,
|
||||
(available) => {
|
||||
if (!available && reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW) {
|
||||
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => composerDraft.value,
|
||||
() => {
|
||||
@@ -3377,7 +3416,6 @@ export default {
|
||||
flowStartedAt.value = 0
|
||||
flowFinishedAt.value = 0
|
||||
flowSteps.value = createFlowSteps()
|
||||
flowPanelOpen.value = false
|
||||
}
|
||||
|
||||
function adjustComposerTextareaHeight() {
|
||||
@@ -3408,14 +3446,6 @@ export default {
|
||||
submitComposer()
|
||||
}
|
||||
|
||||
function toggleFlowPanel() {
|
||||
flowPanelOpen.value = !flowPanelOpen.value
|
||||
}
|
||||
|
||||
function openFlowPanel() {
|
||||
flowPanelOpen.value = true
|
||||
}
|
||||
|
||||
function clearFlowSimulationTimers() {
|
||||
while (flowSimulationTimers.length) {
|
||||
const timerId = flowSimulationTimers.pop()
|
||||
@@ -3424,25 +3454,22 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleFlowPanelAutoCollapse(delayMs = 1200) {
|
||||
const collapseTimer = window.setTimeout(() => {
|
||||
if (runningFlowStep.value || flowRefreshBusy.value || submitting.value) {
|
||||
return
|
||||
}
|
||||
if (flowSteps.value.length && !flowSteps.value.some((step) => step.status === FLOW_STEP_STATUS_FAILED)) {
|
||||
flowPanelOpen.value = false
|
||||
}
|
||||
}, delayMs)
|
||||
flowSimulationTimers.push(collapseTimer)
|
||||
}
|
||||
|
||||
function resetFlowRun() {
|
||||
function resetFlowRun(options = {}) {
|
||||
clearFlowSimulationTimers()
|
||||
flowPanelOpen.value = true
|
||||
flowRunId.value = ''
|
||||
flowStartedAt.value = Date.now()
|
||||
flowFinishedAt.value = 0
|
||||
flowSteps.value = createFlowSteps()
|
||||
reviewDrawerMode.value = REVIEW_DRAWER_MODE_FLOW
|
||||
insightPanelCollapsed.value = false
|
||||
const hasText = Boolean(String(options.rawText || '').trim())
|
||||
const attachmentCount = Number(options.attachmentCount || 0)
|
||||
flowSteps.value = createFlowSteps({
|
||||
includeIntent: hasText,
|
||||
includeOcr: attachmentCount > 0,
|
||||
includeExtraction: hasText || attachmentCount > 0,
|
||||
includeAgent: true,
|
||||
includeResult: true
|
||||
})
|
||||
}
|
||||
|
||||
function findFlowDefinition(key) {
|
||||
@@ -3476,10 +3503,22 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeFlowStepIndexes(steps) {
|
||||
return steps.map((step, index) => ({ ...step, index: index + 1 }))
|
||||
}
|
||||
|
||||
function upsertFlowStep(key, patch) {
|
||||
const existingStep = flowSteps.value.find((step) => step.key === key)
|
||||
if (!existingStep) {
|
||||
flowSteps.value = [...flowSteps.value, createFlowStep(key, patch)]
|
||||
const nextStep = createFlowStep(key, patch)
|
||||
const resultIndex = flowSteps.value.findIndex((step) => step.key === 'result')
|
||||
if (resultIndex !== -1 && key !== 'result') {
|
||||
const nextSteps = [...flowSteps.value]
|
||||
nextSteps.splice(resultIndex, 0, nextStep)
|
||||
flowSteps.value = normalizeFlowStepIndexes(nextSteps)
|
||||
return
|
||||
}
|
||||
flowSteps.value = normalizeFlowStepIndexes([...flowSteps.value, nextStep])
|
||||
return
|
||||
}
|
||||
const normalizedPatch = normalizeFlowStepPatch(key, patch)
|
||||
@@ -3660,6 +3699,13 @@ export default {
|
||||
)
|
||||
}
|
||||
|
||||
if (flowSteps.value.some((step) => step.key === 'agent')) {
|
||||
completePendingFlowStep(
|
||||
'agent',
|
||||
toolCalls.length ? `已完成 ${toolCalls.length} 个工具调用` : FLOW_STEP_FALLBACKS.agent.completedText
|
||||
)
|
||||
}
|
||||
|
||||
toolCalls.forEach((toolCall, index) => {
|
||||
const meta = resolveToolCallFlowMeta(toolCall, index)
|
||||
const failed = String(toolCall?.status || '').toLowerCase() === 'failed'
|
||||
@@ -3687,10 +3733,17 @@ export default {
|
||||
if (!answer && !payload?.result) {
|
||||
return
|
||||
}
|
||||
flowSteps.value
|
||||
.filter((step) => step.key !== 'result' && ![FLOW_STEP_STATUS_COMPLETED, FLOW_STEP_STATUS_FAILED].includes(step.status))
|
||||
.forEach((step) => {
|
||||
completeFlowStep(step.key, resolveFlowStepDetail({ ...step, status: FLOW_STEP_STATUS_COMPLETED }))
|
||||
})
|
||||
startFlowStep('result', '正在返回处理结果...')
|
||||
completeFlowStep('result', '结果已返回到对话区', resolveResultStepDurationMs(run))
|
||||
flowFinishedAt.value = Date.now()
|
||||
scheduleFlowPanelAutoCollapse()
|
||||
if (reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW) {
|
||||
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshFlowRunDetail() {
|
||||
@@ -3717,6 +3770,38 @@ export default {
|
||||
return formatFlowDuration(step?.durationMs)
|
||||
}
|
||||
|
||||
function resolveFlowStepStatusLabel(step) {
|
||||
const status = String(step?.status || '').trim()
|
||||
if (status === FLOW_STEP_STATUS_COMPLETED) {
|
||||
return '完成'
|
||||
}
|
||||
if (status === FLOW_STEP_STATUS_RUNNING) {
|
||||
return '执行中'
|
||||
}
|
||||
if (status === FLOW_STEP_STATUS_FAILED) {
|
||||
return '异常'
|
||||
}
|
||||
return '待执行'
|
||||
}
|
||||
|
||||
function resolveFlowStepDetail(step) {
|
||||
const detail = String(step?.detail || '').trim()
|
||||
if (detail) {
|
||||
return detail
|
||||
}
|
||||
const definition = findFlowDefinition(step?.key)
|
||||
if (step?.status === FLOW_STEP_STATUS_COMPLETED) {
|
||||
return definition?.completedText || '步骤已完成'
|
||||
}
|
||||
if (step?.status === FLOW_STEP_STATUS_RUNNING) {
|
||||
return definition?.runningText || '正在执行当前步骤...'
|
||||
}
|
||||
if (step?.status === FLOW_STEP_STATUS_FAILED) {
|
||||
return step?.error || '步骤执行异常'
|
||||
}
|
||||
return definition?.runningText ? `等待${definition.title || '当前步骤'}...` : '等待智能体调度...'
|
||||
}
|
||||
|
||||
function buildComposerBusinessTimeLabel() {
|
||||
if (composerDateMode.value === 'single') {
|
||||
return `业务发生时间:${composerSingleDate.value}`
|
||||
@@ -4064,6 +4149,16 @@ export default {
|
||||
: REVIEW_DRAWER_MODE_RISK
|
||||
}
|
||||
|
||||
function toggleReviewFlowDrawer() {
|
||||
if (!reviewFlowDrawerAvailable.value) {
|
||||
return
|
||||
}
|
||||
reviewDrawerMode.value =
|
||||
reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW
|
||||
? REVIEW_DRAWER_MODE_REVIEW
|
||||
: REVIEW_DRAWER_MODE_FLOW
|
||||
}
|
||||
|
||||
function setInlineReviewFieldError(key, message) {
|
||||
reviewInlineErrors.value = {
|
||||
...reviewInlineErrors.value,
|
||||
@@ -4438,7 +4533,7 @@ export default {
|
||||
return null
|
||||
}
|
||||
|
||||
resetFlowRun()
|
||||
resetFlowRun({ rawText, attachmentCount: files.length })
|
||||
if (rawText) {
|
||||
startFlowStep('intent', '正在识别业务意图...')
|
||||
startSemanticFlowPreview(rawText, { attachmentCount: files.length })
|
||||
@@ -4524,6 +4619,17 @@ export default {
|
||||
extraContext.review_action = 'create_new_claim_from_documents'
|
||||
}
|
||||
|
||||
const runningExtractionStep = flowSteps.value.find(
|
||||
(step) => step.key === 'extraction' && step.status === FLOW_STEP_STATUS_RUNNING
|
||||
)
|
||||
if (runningExtractionStep) {
|
||||
completeFlowStep(
|
||||
'extraction',
|
||||
runningExtractionStep.detail || FLOW_STEP_FALLBACKS.extraction.completedText
|
||||
)
|
||||
}
|
||||
startFlowStep('agent', FLOW_STEP_FALLBACKS.agent.runningText)
|
||||
|
||||
const backendMessage = buildBackendMessage(rawText, effectiveFileNames, effectiveOcrSummary)
|
||||
const payload = await runOrchestrator(
|
||||
{
|
||||
@@ -4887,12 +4993,13 @@ export default {
|
||||
closeComposerDatePicker,
|
||||
setComposerDateMode,
|
||||
removeComposerBusinessTimeTag,
|
||||
flowPanelOpen,
|
||||
flowSteps,
|
||||
flowRunId,
|
||||
flowRefreshBusy,
|
||||
completedFlowStepCount,
|
||||
flowOverallStatusTone,
|
||||
flowOverallStatusText,
|
||||
flowTotalDurationText,
|
||||
attachedFiles,
|
||||
composerFilesExpanded,
|
||||
visibleAttachedFiles,
|
||||
@@ -4918,13 +5025,17 @@ export default {
|
||||
reviewDrawerMode,
|
||||
isReviewDocumentDrawer,
|
||||
isReviewRiskDrawer,
|
||||
isReviewFlowDrawer,
|
||||
reviewDrawerTitle,
|
||||
reviewDocumentDrawerAvailable,
|
||||
reviewRiskDrawerAvailable,
|
||||
reviewFlowDrawerAvailable,
|
||||
reviewDocumentDrawerLabel,
|
||||
reviewDocumentDrawerIcon,
|
||||
reviewRiskDrawerLabel,
|
||||
reviewRiskDrawerIcon,
|
||||
reviewFlowDrawerLabel,
|
||||
reviewFlowDrawerIcon,
|
||||
activeReviewDocument,
|
||||
activeReviewDocumentIndex,
|
||||
activeReviewDocumentPreview,
|
||||
@@ -5005,13 +5116,14 @@ export default {
|
||||
askHotKnowledgeQuestion,
|
||||
resolveKnowledgeRankLabel,
|
||||
resolveKnowledgeRankTone,
|
||||
toggleFlowPanel,
|
||||
openFlowPanel,
|
||||
refreshFlowRunDetail,
|
||||
formatFlowStepDuration,
|
||||
resolveFlowStepStatusLabel,
|
||||
resolveFlowStepDetail,
|
||||
toggleInsightPanel,
|
||||
toggleReviewDocumentDrawer,
|
||||
toggleReviewRiskDrawer,
|
||||
toggleReviewFlowDrawer,
|
||||
toggleAttachedFilesExpanded,
|
||||
removeAttachedFile,
|
||||
clearAttachedFiles,
|
||||
|
||||
Reference in New Issue
Block a user