diff --git a/web/src/assets/styles/components/sidebar-rail.css b/web/src/assets/styles/components/sidebar-rail.css
index 79d6e75..6e6d207 100644
--- a/web/src/assets/styles/components/sidebar-rail.css
+++ b/web/src/assets/styles/components/sidebar-rail.css
@@ -92,8 +92,9 @@
z-index: 30;
width: 28px;
height: 28px;
- display: inline-grid;
- place-items: center;
+ display: flex;
+ align-items: center;
+ justify-content: center;
cursor: pointer;
border: 1px solid #dbe4ee;
border-radius: 999px;
@@ -125,7 +126,17 @@
.rail-collapse-btn .mdi {
font-size: 17px;
- line-height: 1;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.rail-collapse-btn .mdi-chevron-left {
+ margin-left: 1px;
+}
+
+.rail-collapse-btn .mdi-chevron-right {
+ margin-right: 1px;
}
.rail-nav {
diff --git a/web/src/assets/styles/components/travel-reimbursement-insight-panel.css b/web/src/assets/styles/components/travel-reimbursement-insight-panel.css
index 9000ff7..2ea88e3 100644
--- a/web/src/assets/styles/components/travel-reimbursement-insight-panel.css
+++ b/web/src/assets/styles/components/travel-reimbursement-insight-panel.css
@@ -6,11 +6,14 @@
min-height: 0;
max-width: 100%;
overflow: hidden;
- transition: width 360ms cubic-bezier(0.22, 1, 0.36, 1);
+ transition:
+ width 360ms cubic-bezier(0.22, 1, 0.36, 1),
+ opacity 260ms cubic-bezier(0.22, 1, 0.36, 1);
}
.insight-panel-shell.collapsed {
width: 0;
+ opacity: 0;
}
.insight-panel {
@@ -24,6 +27,20 @@
border-radius: 16px;
background: #ffffff;
box-shadow: 0 14px 32px rgba(148, 163, 184, 0.16);
+ opacity: 1;
+ transform: translateX(0) scale(1);
+ transform-origin: right center;
+ transition:
+ opacity 260ms cubic-bezier(0.22, 1, 0.36, 1),
+ transform 320ms cubic-bezier(0.22, 1, 0.36, 1),
+ box-shadow 320ms cubic-bezier(0.22, 1, 0.36, 1);
+ will-change: opacity, transform;
+}
+
+.insight-panel-shell.collapsed .insight-panel {
+ opacity: 0;
+ transform: translateX(28px) scale(0.985);
+ pointer-events: none;
}
.insight-head {
@@ -286,28 +303,156 @@
.review-flow-list {
position: relative;
+ gap: 0;
+ padding: 2px 0 0;
}
.flow-step-item {
+ position: relative;
display: grid;
- grid-template-columns: 28px minmax(0, 1fr);
- gap: 8px;
+ grid-template-columns: 30px minmax(0, 1fr);
+ gap: 10px;
+ padding-bottom: 10px;
+}
+
+.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: 28px;
+ bottom: -10px;
+ left: 50%;
+ width: 2px;
+ transform: translateX(-50%);
+ border-radius: 999px;
+ background: #e2e8f0;
+ opacity: 0;
+ transform-origin: top;
+ transition: opacity 0.18s ease, transform 0.32s ease, background 0.18s ease;
+ transform: translateX(-50%) scaleY(0);
+}
+
+.flow-step-item:not(:last-child) .flow-step-rail::after {
+ opacity: 1;
+ transform: translateX(-50%) scaleY(1);
}
.flow-step-rail span {
+ position: relative;
+ z-index: 1;
width: 26px;
height: 26px;
display: grid;
place-items: center;
border-radius: 999px;
- background: #eff6ff;
- color: #2563eb;
+ border: 1px solid #dbe4ee;
+ background: #f8fafc;
+ color: #64748b;
font-size: 11px;
- font-weight: 850;
+ font-weight: 900;
+ font-variant-numeric: tabular-nums;
+ transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}
.flow-step-card {
- padding: 10px;
+ min-width: 0;
+ display: grid;
+ gap: 7px;
+ padding: 10px 11px;
+ border-radius: 8px;
+ background: #f8fafc;
+ box-shadow: none;
+ transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
+}
+
+.flow-step-item.completed .flow-step-rail::after {
+ background: #1e293b;
+}
+
+.flow-step-item.completed .flow-step-rail span {
+ border-color: #0f172a;
+ background: #0f172a;
+ color: #ffffff;
+ box-shadow: 0 0 0 4px rgba(15, 23, 42, 0.08);
+}
+
+.flow-step-item.completed .flow-step-card {
+ border-color: #1e293b;
+ background: #1e293b;
+ box-shadow: 0 10px 18px rgba(15, 23, 42, 0.14);
+}
+
+.flow-step-item.completed .flow-step-card strong {
+ color: #ffffff;
+}
+
+.flow-step-item.completed .flow-step-tool,
+.flow-step-item.completed .flow-step-detail,
+.flow-step-item.completed .flow-step-card time {
+ color: #cbd5e1;
+}
+
+.flow-step-item.completed .flow-step-status.completed {
+ background: rgba(255, 255, 255, 0.14);
+ color: #ffffff;
+}
+
+.flow-step-item.running .flow-step-rail span {
+ border-color: #2563eb;
+ background: #2563eb;
+ color: #ffffff;
+ box-shadow: 0 0 0 5px rgba(37, 99, 235, 0.12);
+}
+
+.flow-step-item.running .flow-step-card {
+ border-color: rgba(37, 99, 235, 0.38);
+ background: #eff6ff;
+}
+
+.flow-step-item.failed .flow-step-rail span {
+ border-color: #dc2626;
+ background: #dc2626;
+ color: #ffffff;
+}
+
+.flow-step-item.failed .flow-step-card {
+ border-color: rgba(220, 38, 38, 0.34);
+ background: #fef2f2;
+}
+
+.flow-step-reveal-enter-active,
+.flow-step-reveal-leave-active {
+ transition:
+ opacity 0.24s ease,
+ transform 0.28s ease,
+ filter 0.28s ease;
+}
+
+.flow-step-reveal-enter-from,
+.flow-step-reveal-leave-to {
+ opacity: 0;
+ filter: blur(2px);
+ transform: translateY(-8px);
+}
+
+.flow-step-reveal-enter-to,
+.flow-step-reveal-leave-from {
+ opacity: 1;
+ filter: blur(0);
+ transform: translateY(0);
+}
+
+.flow-step-reveal-move {
+ transition: transform 0.24s ease;
}
.flow-empty-state,
diff --git a/web/src/assets/styles/components/travel-reimbursement-message-item.css b/web/src/assets/styles/components/travel-reimbursement-message-item.css
index 17192ff..8190e16 100644
--- a/web/src/assets/styles/components/travel-reimbursement-message-item.css
+++ b/web/src/assets/styles/components/travel-reimbursement-message-item.css
@@ -118,6 +118,56 @@
font-weight: 850;
}
+.message-answer-markdown :deep(.markdown-table-wrap) {
+ width: 100%;
+ max-width: 100%;
+ margin: 10px 0 12px;
+ overflow-x: auto;
+ border: 1px solid #dbe4ee;
+ border-radius: 10px;
+ background: #ffffff;
+ box-shadow: 0 8px 20px rgba(15, 23, 42, 0.05);
+}
+
+.message-answer-markdown :deep(table) {
+ width: 100%;
+ min-width: 460px;
+ border: 0;
+ border-collapse: separate;
+ border-spacing: 0;
+ background: #ffffff;
+ font-size: inherit;
+}
+
+.message-answer-markdown :deep(th),
+.message-answer-markdown :deep(td) {
+ padding: 8px 10px;
+ border-bottom: 1px solid #e2e8f0;
+ text-align: left;
+ vertical-align: top;
+ white-space: normal;
+}
+
+.message-answer-markdown :deep(th) {
+ background: #f8fafc;
+ color: #0f172a;
+ font-weight: 760;
+ border-bottom-color: #cbd5e1;
+}
+
+.message-answer-markdown :deep(td) {
+ color: #334155;
+ font-weight: 520;
+}
+
+.message-answer-markdown :deep(tbody tr:nth-child(even) td) {
+ background: #fbfdff;
+}
+
+.message-answer-markdown :deep(tbody tr:last-child td) {
+ border-bottom: 0;
+}
+
.welcome-quick-actions {
margin-top: 14px;
padding-top: 12px;
@@ -202,9 +252,12 @@
margin-top: 12px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ grid-auto-rows: 1fr;
+ gap: 8px;
}
.message-suggested-action-btn {
+ height: 100%;
min-height: 54px;
display: grid;
grid-template-columns: 30px minmax(0, 1fr) auto;
@@ -244,7 +297,6 @@
}
.message-detail-block,
-.application-preview-table,
.draft-preview {
margin-top: 12px;
padding: 12px;
@@ -263,16 +315,51 @@
}
.application-preview-table {
+ margin-top: 12px;
display: grid;
padding: 0;
overflow: hidden;
+ border: 1px solid #d7e4f2;
+ border-radius: 8px;
+ background: #ffffff;
+ color: #334155;
+ font-size: var(--wb-fs-bubble, 13px);
}
.application-preview-row {
+ position: relative;
display: grid;
- grid-template-columns: minmax(96px, 0.42fr) minmax(0, 1fr);
+ grid-template-columns: 108px minmax(0, 1fr);
min-height: 38px;
- border-top: 1px solid #e2e8f0;
+ border-top: 1px solid #e6edf5;
+}
+
+.application-preview-row.editable {
+ cursor: pointer;
+}
+
+.application-preview-row.editable:hover {
+ background: #f8fbff;
+}
+
+.application-preview-row.editable:hover .application-preview-label,
+.application-preview-row.editable:hover .application-preview-value {
+ background: #f8fbff;
+}
+
+.application-preview-row.editable.missing:hover .application-preview-label {
+ background: color-mix(in srgb, var(--theme-primary-soft, #eaf4fa) 82%, #ffffff);
+}
+
+.application-preview-row.editable.missing:hover .application-preview-value {
+ background: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.09);
+}
+
+.application-preview-row.editable:focus-visible {
+ position: relative;
+ z-index: 1;
+ outline: 2px solid rgba(var(--theme-primary-rgb, 58, 124, 165), 0.45);
+ outline-offset: -2px;
}
.application-preview-row:first-child {
@@ -280,9 +367,10 @@
}
.application-preview-row.head {
- background: #eff6ff;
- color: #334155;
- font-size: 12px;
+ min-height: 34px;
+ background: #f8fafc;
+ color: #475569;
+ font-size: var(--wb-fs-caption, 12px);
font-weight: 850;
}
@@ -291,37 +379,157 @@
align-items: center;
gap: 8px;
min-width: 0;
- padding: 8px 10px;
+ padding: 8px 12px;
}
.application-preview-label {
+ border-right: 1px solid #e6edf5;
+ background: #fbfdff;
color: #64748b;
- border-right: 1px solid #e2e8f0;
+ font-weight: 760;
}
.application-preview-value {
+ position: relative;
color: #0f172a;
- font-weight: 750;
+ font-weight: 650;
+}
+
+.application-preview-row.highlight .application-preview-label {
+ background: var(--theme-primary-soft, #eaf4fa);
+ color: var(--theme-primary-active, #255b7d);
+}
+
+.application-preview-row.highlight .application-preview-value {
+ background: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.08);
+ color: var(--theme-primary-active, #255b7d);
+ font-weight: 780;
+}
+
+.application-preview-row.missing {
+ background: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.035);
+ box-shadow: inset 3px 0 0 rgba(var(--theme-primary-rgb, 58, 124, 165), 0.42);
+}
+
+.application-preview-row.missing .application-preview-label {
+ background: color-mix(in srgb, var(--theme-primary-soft, #eaf4fa) 76%, #ffffff);
+ color: var(--theme-primary-active, #255b7d);
+ font-weight: 880;
+}
+
+.application-preview-row.missing .application-preview-value {
+ background: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.07);
+ color: #0f172a;
+ font-weight: 850;
+}
+
+.application-preview-text {
+ min-width: 0;
+ overflow-wrap: anywhere;
+ line-height: 1.45;
}
.application-preview-input {
width: 100%;
+ min-width: 0;
min-height: 32px;
- border: 1px solid #cbd5e1;
- border-radius: 8px;
+ height: 30px;
+ padding: 0 9px;
+ border: 1px solid rgba(var(--theme-primary-rgb, 58, 124, 165), 0.48);
+ border-radius: 6px;
background: #ffffff;
color: #0f172a;
+ font: inherit;
+ font-weight: 650;
+ line-height: 1.2;
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(var(--theme-primary-rgb, 58, 124, 165), 0.12);
+}
+
+.application-preview-select {
+ min-width: 0;
+ width: 100%;
+ cursor: pointer;
}
.application-preview-edit-btn {
- width: 28px;
- height: 28px;
+ flex: 0 0 auto;
+ width: 24px;
+ height: 24px;
display: inline-grid;
place-items: center;
- border: 1px solid #cbd5e1;
- border-radius: 8px;
- background: #ffffff;
- color: #2563eb;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ background: var(--theme-primary-soft, #eaf4fa);
+ color: var(--theme-primary-active, #255b7d);
+ cursor: pointer;
+ opacity: 0;
+ transition: opacity 0.16s ease, border-color 0.16s ease, background 0.16s ease;
+}
+
+.application-preview-edit-btn i {
+ font-size: 14px;
+}
+
+.application-preview-row:hover .application-preview-edit-btn,
+.application-preview-edit-btn:focus-visible {
+ opacity: 1;
+}
+
+.application-preview-edit-btn:hover:not(:disabled),
+.application-preview-edit-btn:focus-visible {
+ border-color: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.35);
+ background: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.14);
+}
+
+.application-preview-edit-btn:disabled {
+ cursor: not-allowed;
+ opacity: 0.42;
+}
+
+.application-preview-footer-missing {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px 6px;
+ margin-top: 48px;
+ padding: 2px 0 0;
+ border: 0;
+ background: transparent;
+ color: #334155;
+ font-size: 13px;
+ font-weight: 780;
+ line-height: 1.7;
+}
+
+.application-preview-missing-prefix,
+.application-preview-missing-suffix {
+ color: #334155;
+ font-weight: 850;
+}
+
+.application-preview-missing-list {
+ display: inline-flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 4px;
+}
+
+.application-preview-missing-chip {
+ display: inline-flex;
+ align-items: center;
+ min-height: 22px;
+ padding: 0 7px;
+ border: 0;
+ border-radius: 6px;
+ background: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.1);
+ color: var(--theme-primary-active, #255b7d);
+ font-weight: 880;
+}
+
+.application-preview-missing-separator {
+ color: var(--theme-primary-active, #255b7d);
+ font-weight: 820;
}
.expense-query-record-list,
diff --git a/web/src/assets/styles/views/approval-center-view.css b/web/src/assets/styles/views/approval-center-view.css
index 5306a16..842b164 100644
--- a/web/src/assets/styles/views/approval-center-view.css
+++ b/web/src/assets/styles/views/approval-center-view.css
@@ -74,7 +74,7 @@
height: 38px;
padding: 0 12px 0 36px;
border: 1px solid #d7e0ea;
- border-radius: 8px;
+ border-radius: 4px;
background: #fff;
color: #0f172a;
font-size: 13px;
@@ -98,7 +98,7 @@
justify-content: center;
gap: 9px;
padding: 0 14px;
- border-radius: 8px;
+ border-radius: 4px;
font-size: 14px;
font-weight: 750;
white-space: nowrap;
diff --git a/web/src/assets/styles/views/audit-view.css b/web/src/assets/styles/views/audit-view.css
index 56fb1bb..287d6e9 100644
--- a/web/src/assets/styles/views/audit-view.css
+++ b/web/src/assets/styles/views/audit-view.css
@@ -306,7 +306,7 @@
display: inline-flex;
align-items: center;
padding: 0 10px;
- border-radius: 999px;
+ border-radius: 4px;
background: var(--theme-primary-soft);
color: var(--theme-primary-active);
font-size: 12px;
diff --git a/web/src/assets/styles/views/documents-center-view.css b/web/src/assets/styles/views/documents-center-view.css
index dc60a1d..abf7691 100644
--- a/web/src/assets/styles/views/documents-center-view.css
+++ b/web/src/assets/styles/views/documents-center-view.css
@@ -295,7 +295,7 @@
gap: 8px;
padding: 0 18px;
border: 0;
- border-radius: 10px;
+ border-radius: 4px;
background: linear-gradient(135deg, var(--theme-primary), var(--theme-primary-active));
color: #fff;
font-size: 14px;
diff --git a/web/src/assets/styles/views/employee-management-view.css b/web/src/assets/styles/views/employee-management-view.css
index 0f429bc..9210880 100644
--- a/web/src/assets/styles/views/employee-management-view.css
+++ b/web/src/assets/styles/views/employee-management-view.css
@@ -379,9 +379,9 @@
align-items: center;
justify-content: center;
padding: 0 10px;
- border-radius: 999px;
- background: #eff6ff;
- color: #2563eb;
+ border-radius: 4px;
+ background: var(--theme-primary-soft);
+ color: var(--theme-primary-active);
font-size: 12px;
font-weight: 800;
}
@@ -880,7 +880,7 @@ tbody tr:last-child td {
gap: 10px;
padding: 10px 12px;
border: 1px solid #d7e0ea;
- border-radius: 10px;
+ border-radius: 4px;
background: #fff;
color: #0f172a;
font-size: 13px;
@@ -909,7 +909,7 @@ tbody tr:last-child td {
gap: 10px;
padding: 12px;
border: 1px solid #d7e0ea;
- border-radius: 12px;
+ border-radius: 4px;
background: #fff;
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.16);
}
@@ -917,7 +917,7 @@ tbody tr:last-child td {
.manager-picker-panel input[type='search'] {
width: 100%;
border: 1px solid #d7e0ea;
- border-radius: 10px;
+ border-radius: 4px;
background: #fff;
color: #0f172a;
font-size: 13px;
@@ -943,7 +943,7 @@ tbody tr:last-child td {
width: 100%;
padding: 10px 12px;
border: 1px solid #edf2f7;
- border-radius: 10px;
+ border-radius: 4px;
background: #fbfdff;
text-align: left;
}
diff --git a/web/src/assets/styles/views/travel-reimbursement-create-view-part4.css b/web/src/assets/styles/views/travel-reimbursement-create-view-part4.css
index 0b8a8a2..88df86e 100644
--- a/web/src/assets/styles/views/travel-reimbursement-create-view-part4.css
+++ b/web/src/assets/styles/views/travel-reimbursement-create-view-part4.css
@@ -323,9 +323,13 @@
--assistant-viewport-inset: 10px;
}
+ :global(.assistant-el-overlay) {
+ --assistant-viewport-inset: 10px;
+ }
+
.assistant-modal,
.assistant-modal-stage {
- border-radius: 18px;
+ border-radius: 10px;
}
.assistant-header {
diff --git a/web/src/assets/styles/views/travel-reimbursement-create-view.css b/web/src/assets/styles/views/travel-reimbursement-create-view.css
index a1b15e5..63fa483 100644
--- a/web/src/assets/styles/views/travel-reimbursement-create-view.css
+++ b/web/src/assets/styles/views/travel-reimbursement-create-view.css
@@ -1,3 +1,115 @@
+:global(.assistant-el-overlay) {
+ --assistant-viewport-inset: clamp(10px, 1.25vmin, 18px);
+ background:
+ linear-gradient(180deg, rgba(239, 246, 255, 0.98) 0%, rgba(248, 250, 252, 0.98) 100%),
+ rgba(241, 245, 249, 0.98);
+}
+
+:global(.assistant-el-overlay .el-overlay-dialog) {
+ display: flex;
+ align-items: stretch;
+ justify-content: stretch;
+ width: 100vw;
+ height: 100dvh;
+ max-height: 100dvh;
+ padding: var(--assistant-viewport-inset);
+ box-sizing: border-box;
+ overflow: hidden;
+}
+
+:global(.assistant-el-dialog.el-dialog.is-fullscreen) {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ border: 0;
+ border-radius: 10px;
+ background: transparent;
+ box-shadow: none;
+ overflow: hidden;
+}
+
+:global(.assistant-el-dialog .el-dialog__header) {
+ display: none;
+}
+
+:global(.assistant-el-dialog .assistant-el-dialog-body) {
+ flex: 1;
+ min-width: 0;
+ min-height: 0;
+ width: 100%;
+ display: flex;
+ padding: 0;
+ overflow: hidden;
+}
+
+:global(.assistant-dialog-zoom-enter-active),
+:global(.assistant-dialog-zoom-leave-active) {
+ transition: opacity 180ms cubic-bezier(0.2, 0, 0, 1);
+ transition-duration: 180ms !important;
+}
+
+:global(.assistant-dialog-zoom-leave-active) {
+ transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
+ transition-duration: 260ms !important;
+}
+
+:global(.assistant-dialog-zoom-enter-active .assistant-modal-stage),
+:global(.assistant-dialog-zoom-leave-active .assistant-modal-stage) {
+ transform-origin: center center;
+ will-change: transform, opacity;
+}
+
+:global(.assistant-dialog-zoom-enter-active .assistant-modal-stage) {
+ animation: assistantWorkbenchScaleIn 260ms cubic-bezier(0.2, 0, 0, 1) both;
+ animation-duration: 260ms !important;
+}
+
+:global(.assistant-dialog-zoom-leave-active .assistant-modal-stage) {
+ animation: assistantWorkbenchScaleOut 260ms cubic-bezier(0.22, 1, 0.36, 1) both;
+ animation-duration: 260ms !important;
+}
+
+:global(.assistant-dialog-zoom-enter-from),
+:global(.assistant-dialog-zoom-leave-to) {
+ opacity: 0;
+}
+
+@keyframes assistantWorkbenchScaleIn {
+ 0% {
+ opacity: 0;
+ transform: scale3d(0.82, 0.82, 1);
+ }
+
+ 72% {
+ opacity: 1;
+ transform: scale3d(1.012, 1.012, 1);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+@keyframes assistantWorkbenchScaleOut {
+ 0% {
+ opacity: 1;
+ transform: scale3d(1, 1, 1);
+ }
+
+ 62% {
+ opacity: 0.72;
+ transform: scale3d(0.972, 0.972, 1);
+ }
+
+ 100% {
+ opacity: 0;
+ transform: scale3d(0.94, 0.94, 1);
+ }
+}
+
.assistant-overlay {
/* 距屏幕边缘 10-18px,随视口微调;高度使用 dvh 适配浏览器工具栏 */
--assistant-viewport-inset: clamp(10px, 1.25vmin, 18px);
@@ -30,7 +142,7 @@
background: transparent;
box-shadow: none;
border: 0;
- border-radius: 24px;
+ border-radius: 10px;
backdrop-filter: none;
-webkit-backdrop-filter: none;
overflow: hidden;
@@ -67,7 +179,7 @@
display: grid;
grid-template-rows: auto minmax(0, 1fr);
transform: none;
- border-radius: 24px;
+ border-radius: 10px;
background:
linear-gradient(180deg, #f8fbff 0%, #edf5ff 100%);
box-shadow:
@@ -78,6 +190,7 @@
background-clip: padding-box;
overflow: hidden;
isolation: isolate;
+ will-change: transform, opacity;
}
.assistant-header {
@@ -87,8 +200,8 @@
gap: 16px;
flex-shrink: 0;
padding: clamp(14px, 2vh, 22px) clamp(148px, 11vw, 172px) clamp(12px, 1.6vh, 18px) clamp(18px, 2vw, 26px);
- border-bottom: 1px solid rgba(203, 213, 225, 0.78);
- background: linear-gradient(180deg, rgba(247, 250, 249, 0.82) 0%, rgba(240, 246, 244, 0.7) 100%);
+ border-bottom: none;
+ background: #fff;
}
.assistant-header-main {
@@ -118,21 +231,46 @@
color: #c2410c;
}
-.assistant-header h2 {
+.assistant-title {
color: #0f172a;
- font-size: clamp(17px, 1.1vw, var(--wb-fs-title));
- font-weight: 900;
- letter-spacing: 0.01em;
+ font-size: 19px;
+ font-weight: 650;
+ letter-spacing: 0;
line-height: 1.25;
+ margin: 0;
+ cursor: default;
+ text-decoration: none;
+ user-select: text;
+ border: 0;
+ outline: 0;
+ background: transparent;
}
-.assistant-header p {
- margin-top: 4px;
+.assistant-title + p {
+ margin: 4px 0 0;
color: #64748b;
font-size: clamp(11px, 0.85vw, var(--wb-fs-desc));
line-height: 1.55;
}
+.assistant-subtitle {
+ margin: 4px 0 0;
+ color: #64748b;
+ font-size: clamp(11px, 0.85vw, var(--wb-fs-desc));
+ line-height: 1.55;
+ cursor: default;
+}
+
+.assistant-header p {
+ color: #64748b;
+ font-size: clamp(11px, 0.85vw, var(--wb-fs-desc));
+ line-height: 1.55;
+}
+
+.assistant-header-main {
+ cursor: default;
+}
+
.assistant-header-actions {
position: absolute;
top: 16px;
@@ -537,37 +675,82 @@
.dialog-toolbar {
display: flex;
- gap: 12px;
+ align-items: center;
+ gap: 10px;
flex-wrap: wrap;
- padding: 16px 18px 12px;
+ padding: 12px 16px;
border-bottom: 1px solid rgba(238, 242, 247, 0.9);
+ background: #ffffff;
+}
+
+.dialog-toolbar-label {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ color: #475569;
+ font-size: 12px;
+ font-weight: 700;
+ white-space: nowrap;
+ letter-spacing: 0.2px;
+ padding: 0;
+ background: transparent;
+ border: 0;
+ border-radius: 0;
+ margin-right: 4px;
+}
+
+.dialog-toolbar-label i {
+ color: #475569;
+ opacity: 0.85;
+ font-size: 14px;
+ line-height: 1;
}
.shortcut-chip {
- min-height: 36px;
+ margin-right: 0;
+ flex: 0 0 auto;
+ min-height: 34px;
display: inline-flex;
align-items: center;
- gap: 7px;
- padding: 0 14px;
+ gap: 6px;
+ padding: 0 13px;
border: 1px solid rgba(219, 230, 240, 0.9);
- border-radius: 999px;
+ border-radius: 8px;
background: rgba(255, 255, 255, 0.95);
color: #334155;
- font-size: var(--wb-fs-chip);
- font-weight: 750;
- box-shadow: 0 4px 12px rgba(241, 245, 249, 0.78);
+ font-size: 13px;
+ font-weight: 650;
+ box-shadow: none;
white-space: nowrap;
+ transition:
+ border-color 0.2s ease,
+ color 0.2s ease,
+ background-color 0.2s ease;
+}
+
+.shortcut-chip-wrap {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+}
+
+.shortcut-chip:hover:not(:disabled) {
+ border-color: rgba(59, 130, 246, 0.45);
+ background: rgba(239, 246, 255, 0.95);
+ color: var(--theme-primary-active, #1d4ed8);
}
.shortcut-chip i {
color: var(--theme-primary);
+ font-size: 14px;
}
.shortcut-chip.active {
- border-color: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.38);
+ border-color: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.55);
background: var(--theme-primary-soft);
color: var(--theme-primary-active);
- box-shadow: none;
+ box-shadow: 0 2px 6px rgba(37, 99, 235, 0.18);
}
.shortcut-chip.active i {
@@ -580,6 +763,11 @@
box-shadow: none;
}
+.shortcut-chip:focus-visible {
+ outline: 2px solid rgba(59, 130, 246, 0.55);
+ outline-offset: 2px;
+}
+
.message-list {
min-height: 0;
display: grid;
diff --git a/web/src/components/travel/TravelReimbursementInsightPanel.vue b/web/src/components/travel/TravelReimbursementInsightPanel.vue
index 5c46e20..7909f98 100644
--- a/web/src/components/travel/TravelReimbursementInsightPanel.vue
+++ b/web/src/components/travel/TravelReimbursementInsightPanel.vue
@@ -145,12 +145,18 @@
-
+
{{ index + 1 }}
@@ -160,7 +166,6 @@
@@ -169,7 +174,7 @@
{{ step.error }}
-
+
@@ -349,12 +354,18 @@
-
+
{{ index + 1 }}
@@ -364,7 +375,6 @@
@@ -373,7 +383,7 @@
{{ step.error }}
-
+
@@ -616,6 +626,13 @@ export default {
type: Object,
required: true
}
+ },
+ methods: {
+ resolveFlowStepStyle(index) {
+ return {
+ transitionDelay: `${Math.min(Number(index) || 0, 5) * 70}ms`
+ }
+ }
}
}
diff --git a/web/src/components/travel/TravelReimbursementMessageItem.vue b/web/src/components/travel/TravelReimbursementMessageItem.vue
index 5284b54..334e182 100644
--- a/web/src/components/travel/TravelReimbursementMessageItem.vue
+++ b/web/src/components/travel/TravelReimbursementMessageItem.vue
@@ -76,7 +76,7 @@
+
+
-
+
diff --git a/web/src/views/scripts/TravelReimbursementCreateView.js b/web/src/views/scripts/TravelReimbursementCreateView.js
index 0449fb4..4ae6d84 100644
--- a/web/src/views/scripts/TravelReimbursementCreateView.js
+++ b/web/src/views/scripts/TravelReimbursementCreateView.js
@@ -163,16 +163,19 @@ import {
HOT_KNOWLEDGE_QUESTIONS,
INTENT_LABELS,
SCENARIO_LABELS,
+ SESSION_TYPE_BUDGET,
SESSION_TYPE_APPLICATION,
SESSION_TYPE_APPROVAL,
SESSION_TYPE_EXPENSE,
SESSION_TYPE_KNOWLEDGE,
+ canUseBudgetAssistantSession,
aiAvatar,
buildExpenseIntentConfirmationMessage,
buildExpenseSceneSelectionMessage,
buildMessageMeta,
buildWelcomeInsight,
createMessage,
+ filterAssistantSessionModes,
resolveAssistantSessionMode,
resolveKnowledgeRankLabel,
resolveKnowledgeRankTone,
@@ -610,11 +613,12 @@ export default {
const isKnowledgeSession = computed(() => activeSessionType.value === SESSION_TYPE_KNOWLEDGE)
const isApplicationSession = computed(() => activeSessionType.value === SESSION_TYPE_APPLICATION)
const activeAssistantMode = computed(() => resolveAssistantSessionMode(activeSessionType.value))
- const assistantHeaderTitle = computed(() => activeAssistantMode.value?.label || '财务助手')
- const assistantHeaderDescription = computed(() => activeAssistantMode.value?.description || '个人财务中心')
+ const assistantHeaderTitle = computed(() => '个人工作台')
+ const assistantHeaderDescription = computed(() => '个人工作窗,一站式费控解决枢纽')
const {
flowRunId,
flowSteps,
+ visibleFlowSteps,
flowRefreshBusy,
completedFlowStepCount,
flowOverallStatusTone,
@@ -1084,7 +1088,7 @@ export default {
const isReviewOverviewDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_REVIEW)
const shortcuts = computed(() =>
- ASSISTANT_SESSION_MODE_OPTIONS.map((mode) => ({
+ filterAssistantSessionModes(ASSISTANT_SESSION_MODE_OPTIONS, currentUser.value).map((mode) => ({
label: mode.label,
icon: mode.icon,
action: 'switch_view',
@@ -1099,7 +1103,11 @@ export default {
// reviewDrawerMode.value = resolveReviewRiskBriefs(payload).length
// ? REVIEW_DRAWER_MODE_RISK
// : REVIEW_DRAWER_MODE_REVIEW
+ const shouldKeepFlowDrawer = reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW && flowSteps.value.length > 0
resetReviewDrawerFromPayload(payload)
+ if (shouldKeepFlowDrawer) {
+ reviewDrawerMode.value = REVIEW_DRAWER_MODE_FLOW
+ }
},
{ immediate: true }
)
@@ -1359,6 +1367,10 @@ export default {
async function runShortcut(shortcut) {
if (shortcut?.action === 'switch_view' && shortcut?.targetSessionType) {
+ if (shortcut.targetSessionType === SESSION_TYPE_BUDGET && !canUseBudgetAssistantSession(currentUser.value)) {
+ toast('目前暂无权限访问预算编制助手')
+ return
+ }
if (shortcut.active) {
return
}
@@ -1444,6 +1456,10 @@ export default {
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
const targetSessionType = String(actionPayload.session_type || '').trim()
if (!targetSessionType) return
+ if (targetSessionType === SESSION_TYPE_BUDGET && !canUseBudgetAssistantSession(currentUser.value)) {
+ toast('目前暂无权限访问预算编制助手')
+ return
+ }
const carryText = String(actionPayload.carry_text || '').trim()
const carryFiles = actionPayload.carry_files ? Array.from(attachedFiles.value || []) : []
if (!lockSuggestedActionMessage(message, action)) return
@@ -1714,6 +1730,14 @@ export default {
return buildApplicationPreviewFooterMessage(message.applicationPreview)
}
+ function resolveApplicationPreviewMissingFields(message) {
+ if (!message?.applicationPreview) {
+ return []
+ }
+ const normalizedPreview = normalizeApplicationPreview(message.applicationPreview)
+ return Array.isArray(normalizedPreview.missingFields) ? normalizedPreview.missingFields : []
+ }
+
function openApplicationSubmitConfirm(message) {
if (!message) {
return
@@ -2128,6 +2152,7 @@ export default {
resolveApplicationPreviewRows,
resolveApplicationPreviewEditorControl,
resolveApplicationPreviewEditorOptions,
+ resolveApplicationPreviewMissingFields,
isApplicationPreviewEditing,
openApplicationPreviewEditor,
commitApplicationPreviewEditor,
@@ -2193,6 +2218,7 @@ export default {
flowRefreshBusy: flowRefreshBusy.value,
refreshFlowRunDetail,
flowSteps: flowSteps.value,
+ visibleFlowSteps: visibleFlowSteps.value,
resolveFlowStepStatusLabel,
formatFlowStepDuration,
resolveFlowStepDetail,
diff --git a/web/src/views/scripts/travelReimbursementConversationModel.js b/web/src/views/scripts/travelReimbursementConversationModel.js
index f6ce1a8..bf24058 100644
--- a/web/src/views/scripts/travelReimbursementConversationModel.js
+++ b/web/src/views/scripts/travelReimbursementConversationModel.js
@@ -2,6 +2,7 @@ import { buildSuggestedActionKey } from '../../utils/suggestedActionKey.js'
import { normalizeExpenseQueryPayload } from './travelReimbursementExpenseQueryModel.js'
import { buildAgentInsight, buildReviewFilePreviewsFromReviewPayload } from './travelReimbursementAttachmentModel.js'
import { resolveExpenseTypeLabel } from './travelReimbursementReviewModel.js'
+import { isBudgetMonitorUser, isExecutiveUser } from '../../utils/accessControl.js'
import {
GUIDED_ACTION_OPEN_TRAVEL_CALCULATOR,
GUIDED_ACTION_START_APPLICATION,
@@ -13,12 +14,14 @@ export const SESSION_TYPE_EXPENSE = 'expense'
export const SESSION_TYPE_APPLICATION = 'application'
export const SESSION_TYPE_APPROVAL = 'approval'
export const SESSION_TYPE_KNOWLEDGE = 'knowledge'
+export const SESSION_TYPE_BUDGET = 'budget'
export const ASSISTANT_SESSION_TYPES = [
SESSION_TYPE_APPLICATION,
SESSION_TYPE_EXPENSE,
SESSION_TYPE_APPROVAL,
- SESSION_TYPE_KNOWLEDGE
+ SESSION_TYPE_KNOWLEDGE,
+ SESSION_TYPE_BUDGET
]
export const ASSISTANT_SESSION_MODE_OPTIONS = [
@@ -45,9 +48,39 @@ export const ASSISTANT_SESSION_MODE_OPTIONS = [
label: '财务知识助手',
icon: 'mdi mdi-book-open-page-variant-outline',
description: '只处理财务制度、标准规则、票据要求和政策解释'
+ },
+ {
+ key: SESSION_TYPE_BUDGET,
+ label: '预算编制助手',
+ icon: 'mdi mdi-calculator-variant-outline',
+ description: '帮助你进行预算编制与预算相关问题的整理'
}
]
+export function canUseBudgetAssistantSession(user = null) {
+ return Boolean(isBudgetMonitorUser(user) || isExecutiveUser(user))
+}
+
+function canUseAssistantSessionType(sessionType, user = null) {
+ const normalized = String(sessionType || '').trim()
+ if (normalized === SESSION_TYPE_BUDGET) {
+ return canUseBudgetAssistantSession(user)
+ }
+ return true
+}
+
+export function filterAssistantSessionModes(sessionModes = [], user = null) {
+ return Array.isArray(sessionModes)
+ ? sessionModes.filter((mode) => canUseAssistantSessionType(mode?.key, user))
+ : []
+}
+
+export function filterAssistantSessionTypes(sessionTypes = [], user = null) {
+ return Array.isArray(sessionTypes)
+ ? sessionTypes.filter((sessionType) => canUseAssistantSessionType(String(sessionType || '').trim(), user))
+ : []
+}
+
export function normalizeAssistantSessionType(sessionType, fallback = SESSION_TYPE_EXPENSE) {
const normalized = String(sessionType || '').trim()
if (ASSISTANT_SESSION_TYPES.includes(normalized)) {
diff --git a/web/src/views/scripts/useTravelReimbursementFlow.js b/web/src/views/scripts/useTravelReimbursementFlow.js
index 7a93112..51be778 100644
--- a/web/src/views/scripts/useTravelReimbursementFlow.js
+++ b/web/src/views/scripts/useTravelReimbursementFlow.js
@@ -100,6 +100,16 @@ export function useTravelReimbursementFlow({
const runningFlowStep = computed(
() => flowSteps.value.find((step) => step.status === FLOW_STEP_STATUS_RUNNING) || null
)
+ const visibleFlowSteps = computed(() => {
+ const visibleSteps = []
+ for (const step of flowSteps.value) {
+ visibleSteps.push(step)
+ if (step.status !== FLOW_STEP_STATUS_COMPLETED) {
+ break
+ }
+ }
+ return visibleSteps
+ })
const flowOverallStatusTone = computed(() => {
if (flowSteps.value.some((step) => step.status === FLOW_STEP_STATUS_FAILED)) {
return 'failed'
@@ -209,7 +219,8 @@ export function useTravelReimbursementFlow({
durationMs: normalizedPatch.durationMs ?? null,
startedAt: normalizedPatch.startedAt || 0,
finishedAt: normalizedPatch.finishedAt || 0,
- error: normalizedPatch.error || ''
+ error: normalizedPatch.error || '',
+ deferredCompletion: Boolean(normalizedPatch.deferredCompletion)
}
}
@@ -262,8 +273,15 @@ export function useTravelReimbursementFlow({
startedAt,
finishedAt: now,
durationMs: hasExplicitDuration ? explicitDuration : Math.max(0, now - startedAt),
- error: ''
+ error: '',
+ deferredCompletion: false
})
+ if (
+ flowSteps.value.length
+ && flowSteps.value.every((step) => [FLOW_STEP_STATUS_COMPLETED, FLOW_STEP_STATUS_FAILED].includes(step.status))
+ ) {
+ flowFinishedAt.value = now
+ }
}
function failFlowStep(key, detail = '', error = '', patch = {}) {
@@ -291,19 +309,18 @@ export function useTravelReimbursementFlow({
const normalizedDuration = Number(durationMs)
const hasMeasuredDuration = Number.isFinite(normalizedDuration) && normalizedDuration > 0
if (!currentStep || currentStep.status === FLOW_STEP_STATUS_PENDING) {
- if (!hasMeasuredDuration && !currentStep?.startedAt) {
- upsertFlowStep(key, {
- ...patch,
- status: FLOW_STEP_STATUS_COMPLETED,
- detail: detail || findFlowDefinition(key)?.completedText || '',
- startedAt: 0,
- finishedAt: 0,
- durationMs: null,
- error: ''
- })
- return
- }
- startFlowStep(key, patch)
+ const revealOrder = flowSteps.value.length
+ startFlowStep(key, { ...patch, deferredCompletion: true })
+ const completionTimer = window.setTimeout(() => {
+ completeFlowStep(
+ key,
+ detail || findFlowDefinition(key)?.completedText || '',
+ hasMeasuredDuration ? normalizedDuration : null,
+ { deferredCompletion: false }
+ )
+ }, 280 + Math.min(revealOrder, 4) * 180)
+ flowSimulationTimers.push(completionTimer)
+ return
}
completeFlowStep(key, detail, hasMeasuredDuration ? normalizedDuration : null, patch)
}
@@ -606,16 +623,18 @@ export function useTravelReimbursementFlow({
const sceneSelectionPending = isExpenseSceneSelectionResult(payload)
flowSteps.value
.filter((step) => ![FLOW_STEP_STATUS_COMPLETED, FLOW_STEP_STATUS_FAILED].includes(step.status))
+ .filter((step) => !step.deferredCompletion)
.forEach((step) => {
const detail = sceneSelectionPending && step.key === 'expense-scene-selection'
? '已暂停后续识别,请先在主对话中选择报销场景。'
: resolveFlowStepDetail({ ...step, status: FLOW_STEP_STATUS_COMPLETED })
completeFlowStep(step.key, detail)
})
- flowFinishedAt.value = Date.now()
- if (reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW && !sceneSelectionPending) {
- reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
- }
+ flowFinishedAt.value = flowSteps.value.some(
+ (step) => ![FLOW_STEP_STATUS_COMPLETED, FLOW_STEP_STATUS_FAILED].includes(step.status)
+ )
+ ? 0
+ : Date.now()
}
async function refreshFlowRunDetail() {
@@ -679,6 +698,7 @@ export function useTravelReimbursementFlow({
flowStartedAt,
flowFinishedAt,
flowSteps,
+ visibleFlowSteps,
flowRefreshBusy,
flowTick,
completedFlowStepCount,
diff --git a/web/src/views/scripts/useTravelReimbursementSessionState.js b/web/src/views/scripts/useTravelReimbursementSessionState.js
index 573fd92..f873b9f 100644
--- a/web/src/views/scripts/useTravelReimbursementSessionState.js
+++ b/web/src/views/scripts/useTravelReimbursementSessionState.js
@@ -12,6 +12,7 @@ import {
} from './travelReimbursementAttachmentModel.js'
import {
ASSISTANT_SESSION_TYPES,
+ filterAssistantSessionTypes,
SESSION_TYPE_APPLICATION,
SESSION_TYPE_EXPENSE,
buildInitialInsightFromConversation,
@@ -43,6 +44,25 @@ export function useTravelReimbursementSessionState({
scrollToBottom,
getSessionRuntimeRefs = () => ({})
}) {
+ function resolveAccessibleSessionTypes() {
+ return filterAssistantSessionTypes(ASSISTANT_SESSION_TYPES, currentUser.value)
+ }
+
+ function resolveAccessibleSessionType(rawSessionType, fallback = resolveDefaultSessionTypeFromEntry()) {
+ const normalized = normalizeAssistantSessionType(rawSessionType, fallback)
+ const accessible = resolveAccessibleSessionTypes()
+ if (accessible.includes(normalized)) {
+ return normalized
+ }
+
+ const fallbackNormalized = String(fallback || '').trim()
+ if (accessible.includes(fallbackNormalized)) {
+ return fallbackNormalized
+ }
+
+ return accessible[0] || SESSION_TYPE_APPLICATION
+ }
+
function resolveDefaultSessionTypeFromEntry() {
return props.entrySource === 'application' ? SESSION_TYPE_APPLICATION : SESSION_TYPE_EXPENSE
}
@@ -65,7 +85,10 @@ export function useTravelReimbursementSessionState({
}
function buildConversationSessionState(conversation, fallbackSessionType = resolveDefaultSessionTypeFromEntry()) {
- const sessionType = resolveInitialSessionType(conversation, fallbackSessionType)
+ const sessionType = resolveAccessibleSessionType(
+ resolveInitialSessionType(conversation, fallbackSessionType),
+ fallbackSessionType
+ )
const restoredMessages = refreshWelcomeQuickActions(normalizeInitialConversationMessages(conversation), sessionType)
const initialInsight = buildInitialInsightFromConversation(conversation)
const restoredReviewFilePreviews = buildReviewFilePreviewsFromMessages(restoredMessages)
@@ -90,7 +113,10 @@ export function useTravelReimbursementSessionState({
}
function buildEmptySessionState(sessionType) {
- const normalizedSessionType = normalizeAssistantSessionType(sessionType, resolveDefaultSessionTypeFromEntry())
+ const normalizedSessionType = resolveAccessibleSessionType(
+ sessionType,
+ resolveDefaultSessionTypeFromEntry()
+ )
return {
sessionType: normalizedSessionType,
messages: [
@@ -120,7 +146,7 @@ export function useTravelReimbursementSessionState({
return null
}
- const sessionType = normalizeAssistantSessionType(
+ const sessionType = resolveAccessibleSessionType(
state.sessionType || snapshot.sessionType || fallbackSessionType,
fallbackSessionType
)
@@ -159,9 +185,12 @@ export function useTravelReimbursementSessionState({
}
const defaultInitialSessionType = resolveDefaultSessionTypeFromEntry()
- const initialSessionType = props.initialConversation
- ? resolveInitialSessionType(props.initialConversation, defaultInitialSessionType)
- : defaultInitialSessionType
+ const initialSessionType = resolveAccessibleSessionType(
+ props.initialConversation
+ ? resolveInitialSessionType(props.initialConversation, defaultInitialSessionType)
+ : defaultInitialSessionType,
+ defaultInitialSessionType
+ )
const shouldPersistLocalSnapshot = props.entrySource !== 'detail'
const conversationInitialState = props.initialConversation
? buildConversationSessionState(props.initialConversation, initialSessionType)
@@ -188,7 +217,7 @@ export function useTravelReimbursementSessionState({
const draftClaimId = ref(initialSessionState.draftClaimId)
const reviewFilePreviews = ref(initialSessionState.reviewFilePreviews)
const sessionSnapshots = ref(
- ASSISTANT_SESSION_TYPES.reduce((result, sessionType) => {
+ resolveAccessibleSessionTypes().reduce((result, sessionType) => {
result[sessionType] = null
return result
}, {})
@@ -202,7 +231,7 @@ export function useTravelReimbursementSessionState({
function buildPersistableSessionState(sessionState) {
const state = sessionState || captureCurrentSessionState()
return {
- sessionType: normalizeAssistantSessionType(state.sessionType, resolveDefaultSessionTypeFromEntry()),
+ sessionType: resolveAccessibleSessionType(state.sessionType, resolveDefaultSessionTypeFromEntry()),
messages: serializeSessionMessages(state.messages),
conversationId: String(state.conversationId || '').trim(),
draftClaimId: String(state.draftClaimId || '').trim(),
@@ -258,7 +287,10 @@ export function useTravelReimbursementSessionState({
function applySessionState(sessionState) {
const runtimeRefs = getSessionRuntimeRefs()
const nextState = sessionState || buildEmptySessionState(activeSessionType.value)
- activeSessionType.value = normalizeAssistantSessionType(nextState.sessionType, resolveDefaultSessionTypeFromEntry())
+ activeSessionType.value = resolveAccessibleSessionType(
+ nextState.sessionType,
+ resolveDefaultSessionTypeFromEntry()
+ )
messages.value = Array.isArray(nextState.messages) && nextState.messages.length
? nextState.messages
: [
@@ -301,7 +333,7 @@ export function useTravelReimbursementSessionState({
}
async function loadLatestSessionState(targetSessionType) {
- const normalizedTarget = normalizeAssistantSessionType(targetSessionType, resolveDefaultSessionTypeFromEntry())
+ const normalizedTarget = resolveAccessibleSessionType(targetSessionType, resolveDefaultSessionTypeFromEntry())
const payload = await fetchLatestConversation(resolveCurrentUserId(), normalizedTarget, {
preferRecoverable: normalizedTarget === SESSION_TYPE_EXPENSE
})
@@ -312,7 +344,7 @@ export function useTravelReimbursementSessionState({
}
async function switchSessionType(targetSessionType) {
- const normalizedTarget = normalizeAssistantSessionType(targetSessionType, resolveDefaultSessionTypeFromEntry())
+ const normalizedTarget = resolveAccessibleSessionType(targetSessionType, resolveDefaultSessionTypeFromEntry())
if (normalizedTarget === activeSessionType.value || sessionSwitchBusy.value) {
return
}
diff --git a/web/tests/expense-application-fast-preview.test.mjs b/web/tests/expense-application-fast-preview.test.mjs
index d8e5000..10f5d98 100644
--- a/web/tests/expense-application-fast-preview.test.mjs
+++ b/web/tests/expense-application-fast-preview.test.mjs
@@ -16,6 +16,7 @@ import {
normalizeApplicationPreview,
shouldUseLocalApplicationPreview
} from '../src/utils/expenseApplicationPreview.js'
+import { renderMarkdown } from '../src/utils/markdown.js'
const submitComposerScript = readFileSync(
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSubmitComposer.js', import.meta.url)),
@@ -29,6 +30,14 @@ const createViewTemplate = readFileSync(
fileURLToPath(new URL('../src/views/TravelReimbursementCreateView.vue', import.meta.url)),
'utf8'
)
+const messageItemTemplate = readFileSync(
+ fileURLToPath(new URL('../src/components/travel/TravelReimbursementMessageItem.vue', import.meta.url)),
+ 'utf8'
+)
+const messageItemStyles = readFileSync(
+ fileURLToPath(new URL('../src/assets/styles/components/travel-reimbursement-message-item.css', import.meta.url)),
+ 'utf8'
+)
const conversationModelScript = readFileSync(
fileURLToPath(new URL('../src/views/scripts/travelReimbursementConversationModel.js', import.meta.url)),
'utf8'
@@ -96,6 +105,7 @@ test('application preview renders ordered editable rows and submit text uses edi
)
assert.equal(rows.find((row) => row.key === 'amount')?.value, '1900元')
assert.equal(rows.find((row) => row.key === 'amount')?.highlight, true)
+ assert.equal(rows.find((row) => row.key === 'grade')?.editable, false)
assert.equal(rows.find((row) => row.key === 'lodgingDailyCap')?.editable, false)
assert.match(buildApplicationPreviewSubmitText(editedPreview), /事由:客户现场项目支持/)
assert.match(buildApplicationPreviewSubmitText(editedPreview), /用户预估费用:1900元/)
@@ -200,6 +210,7 @@ test('application quick start renders a template without model review', () => {
assert.equal(preview.fields.applicant, '李文静')
assert.equal(preview.fields.department, '财务部')
assert.equal(preview.fields.grade, 'P5')
+ assert.equal(buildApplicationPreviewRows(preview).find((row) => row.key === 'grade')?.editable, false)
assert.match(message, /不调用大模型/)
assert.match(message, /点击对应行直接填写/)
assert.doesNotMatch(message, /#application-submit/)
@@ -238,26 +249,56 @@ test('application session shows intent flow, persists preview, and supports inli
assert.match(conversationModelScript, /applicationPreview: null/)
assert.match(conversationModelScript, /applicationPreview: message\.applicationPreview \|\| null/)
- assert.match(createViewTemplate, /class="application-preview-table"/)
- assert.match(createViewTemplate, /class="application-preview-footer message-answer-content message-answer-markdown"/)
- assert.match(createViewTemplate, /v-html="renderMarkdown\(buildApplicationPreviewFooterText\(message\)\)"/)
+ assert.match(messageItemTemplate, /class="application-preview-table"/)
+ assert.match(messageItemTemplate, /class="application-preview-footer application-preview-footer-missing"/)
+ assert.match(messageItemTemplate, /application-preview-missing-chip/)
+ assert.match(messageItemTemplate, /当前还需要补充:/)
+ assert.match(messageItemTemplate, /补齐后我再帮您提交申请。/)
+ assert.match(messageItemTemplate, /class="application-preview-footer message-answer-content message-answer-markdown"/)
+ assert.match(messageItemTemplate, /v-html="ui\.renderMarkdown\(ui\.buildApplicationPreviewFooterText\(message\)\)"/)
assert.match(createViewTemplate, /'has-insight': hasInsightPanelContent && showInsightPanel/)
- assert.match(createViewTemplate, /v-model="applicationPreviewEditor\.draftValue"/)
- assert.match(createViewTemplate, /application-preview-select/)
- assert.match(createViewTemplate, /resolveApplicationPreviewEditorOptions/)
- assert.match(createViewTemplate, /row\.editable && !isApplicationPreviewEditing\(message, row\.key\).*openApplicationPreviewEditor\(message, row\.key, row\.value\)"/)
- assert.match(createViewTemplate, /@keydown\.enter\.prevent="row\.editable && !isApplicationPreviewEditing\(message, row\.key\).*openApplicationPreviewEditor\(message, row\.key, row\.value\)"/)
- assert.match(createViewTemplate, /@keydown\.stop="handleApplicationPreviewEditorKeydown\(\$event, message\)"/)
- assert.match(createViewTemplate, /mdi mdi-pencil-outline/)
- assert.match(createViewTemplate, /@click\.stop="openApplicationPreviewEditor\(message, row\.key, row\.value\)"/)
- assert.match(createViewTemplate, /openApplicationPreviewEditor/)
- assert.match(createViewTemplate, /commitApplicationPreviewEditor/)
+ assert.match(messageItemTemplate, /v-model="ui\.applicationPreviewEditor\.draftValue"/)
+ assert.match(messageItemTemplate, /application-preview-select/)
+ assert.match(messageItemTemplate, /resolveApplicationPreviewEditorOptions/)
+ assert.match(messageItemTemplate, /row\.editable && !ui\.isApplicationPreviewEditing\(message, row\.key\).*ui\.openApplicationPreviewEditor\(message, row\.key, row\.value\)"/)
+ assert.match(messageItemTemplate, /@keydown\.enter\.prevent="row\.editable && !ui\.isApplicationPreviewEditing\(message, row\.key\).*ui\.openApplicationPreviewEditor\(message, row\.key, row\.value\)"/)
+ assert.match(messageItemTemplate, /@keydown\.stop="ui\.handleApplicationPreviewEditorKeydown\(\$event, message\)"/)
+ assert.match(messageItemTemplate, /mdi mdi-pencil-outline/)
+ assert.match(messageItemTemplate, /@click\.stop="ui\.openApplicationPreviewEditor\(message, row\.key, row\.value\)"/)
+ assert.match(messageItemTemplate, /openApplicationPreviewEditor/)
+ assert.match(messageItemTemplate, /commitApplicationPreviewEditor/)
+ assert.match(createViewScript, /resolveApplicationPreviewMissingFields/)
assert.match(previewEditorScript, /normalizeApplicationPreview/)
assert.match(previewEditorScript, /APPLICATION_TRANSPORT_MODE_OPTIONS/)
assert.match(previewEditorScript, /buildLocalApplicationPreviewMessage/)
assert.match(previewEditorScript, /targetRow\.editable === false/)
assert.match(previewEditorScript, /\[editor\.fieldKey\]: nextValue/)
+
+ assert.match(messageItemStyles, /\.application-preview-row\.missing \{[\s\S]*--theme-primary-rgb/)
+ assert.match(messageItemStyles, /\.application-preview-table \{[\s\S]*border: 1px solid #d7e4f2;[\s\S]*background: #ffffff;/)
+ assert.match(messageItemStyles, /\.application-preview-row \{[\s\S]*grid-template-columns: 108px minmax\(0, 1fr\);/)
+ assert.match(messageItemStyles, /\.application-preview-text \{[\s\S]*overflow-wrap: anywhere;/)
+ assert.match(messageItemStyles, /\.application-preview-select \{[\s\S]*width: 100%;/)
+ assert.match(messageItemStyles, /\.application-preview-footer-missing \{[\s\S]*margin-top: 48px;[\s\S]*background: transparent;/)
+ assert.match(messageItemStyles, /\.application-preview-missing-chip \{[\s\S]*background: rgba\(var\(--theme-primary-rgb/)
+})
+
+test('assistant markdown tables render with component-scoped table styling', () => {
+ const rendered = renderMarkdown([
+ '| 项目 | 标准口径 | 天数 | 小计 |',
+ '| --- | --- | ---: | ---: |',
+ '| 住宿费 | 武汉 / P5 标准:330.00 元/天 | 1 | 330.00 元 |',
+ '| 出差补贴 | 其他地区:伙食 55.00 元 + 基本 35.00 元 | 1 | 90.00 元 |'
+ ].join('\n'))
+
+ assert.match(rendered, //)
+ assert.match(rendered, /
/)
+ assert.match(rendered, /| {
|