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 @@
{{ step.title }}
- {{ ui.resolveFlowStepStatusLabel(step) }}
@@ -169,7 +174,7 @@

{{ step.error }}

-
+
@@ -349,12 +354,18 @@
-
+
{{ index + 1 }} @@ -364,7 +375,6 @@
{{ step.title }}
- {{ ui.resolveFlowStepStatusLabel(step) }}
@@ -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 @@ + - + + - -
+ + +
@@ -453,10 +473,10 @@
-
-
+
+ - + 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, /
{