后端优化编排器报销查询和本体检测精度,增强报销单草稿保 存和附件回填逻辑,前端重构侧边栏组件支持折叠和图标导 航,完善文档中心状态筛选和详情提示,报销创建和审批详情 页优化会话管理和费用明细交互,新增助手应用服务和预设动 作工具函数,补充单元测试覆盖。
1163 lines
25 KiB
Vue
1163 lines
25 KiB
Vue
<template>
|
||
<section class="workbench">
|
||
<PanelHead
|
||
v-if="showHeader"
|
||
eyebrow="Personal Workspace"
|
||
title="个人工作台"
|
||
note="把今天要处理的待办、报销进度和制度更新集中到一个入口。"
|
||
/>
|
||
|
||
<article class="panel assistant-hero">
|
||
<div class="assistant-visual" aria-hidden="true">
|
||
<span class="assistant-glow"></span>
|
||
<img class="assistant-image" :src="robotAssistant" alt="" />
|
||
</div>
|
||
|
||
<div class="assistant-copy">
|
||
<h3>嗨,{{ assistantGreetingName }},描述您想做的事,AI 会直接帮您处理</h3>
|
||
<p>我会自动识别您的意图,协助完成费用申请、报销、查询和制度问答等业务工作,耐心把事情推进到可执行的下一步。</p>
|
||
|
||
<div class="assistant-input">
|
||
<input
|
||
ref="fileInputRef"
|
||
class="assistant-file-input"
|
||
type="file"
|
||
multiple
|
||
accept=".pdf,.jpg,.jpeg,.png,.webp,.doc,.docx,.xls,.xlsx"
|
||
@change="handleWorkbenchFilesChange"
|
||
/>
|
||
<textarea
|
||
v-model="assistantDraft"
|
||
rows="1"
|
||
placeholder="例如:我昨天请客户吃饭花了 860 元,还打车去了客户公司"
|
||
@keydown.enter.prevent="handleWorkbenchEnter"
|
||
/>
|
||
</div>
|
||
|
||
<div v-if="selectedFiles.length" class="assistant-file-strip">
|
||
<span class="assistant-file-note">已带入 {{ selectedFiles.length }} 份附件</span>
|
||
<span v-for="file in selectedFiles" :key="file.name" class="assistant-file-chip">{{ file.name }}</span>
|
||
<button type="button" class="assistant-file-clear" @click="clearSelectedFiles">清空</button>
|
||
</div>
|
||
|
||
<div class="assistant-tools">
|
||
<button type="button" class="ghost-action" :disabled="Boolean(pendingAction)" @click="triggerFileUpload">
|
||
<i class="mdi mdi-upload-outline"></i>
|
||
<span>上传票据</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="hero-action"
|
||
:disabled="Boolean(pendingAction)"
|
||
@click="handleExpenseConversationAction"
|
||
>
|
||
<i :class="pendingAction === 'expense' ? 'mdi mdi-loading mdi-spin' : expenseActionIcon"></i>
|
||
<span>{{ pendingAction === 'expense' ? '处理中...' : expenseActionLabel }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
|
||
<div class="workbench-grid">
|
||
<article class="panel list-panel">
|
||
<div class="section-head">
|
||
<div class="title-with-badge">
|
||
<h3>报销待办</h3>
|
||
<span class="alert-badge">{{ todoAlertCount }}</span>
|
||
</div>
|
||
<button type="button" class="link-action">查看全部 <i class="mdi mdi-chevron-right"></i></button>
|
||
</div>
|
||
|
||
<div class="list-body">
|
||
<div v-for="item in todoItems" :key="item.title" class="todo-row">
|
||
<WorkbenchListIcon
|
||
:icon-key="item.iconKey"
|
||
:color="item.color"
|
||
:accent="item.accent"
|
||
/>
|
||
|
||
<div class="todo-copy">
|
||
<strong>{{ item.title }}</strong>
|
||
<p class="todo-advice">
|
||
<span class="todo-advice-label">{{ item.tipLabel }}</span>
|
||
<span class="todo-advice-text">{{ item.suggestion }}</span>
|
||
</p>
|
||
</div>
|
||
|
||
<button type="button" class="row-action" @click="emit('open-assistant')">{{ item.action }}</button>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="panel list-panel">
|
||
<div class="section-head">
|
||
<div class="title-with-badge">
|
||
<h3>报销进度</h3>
|
||
<span class="alert-badge">{{ progressAlertCount }}</span>
|
||
</div>
|
||
<button type="button" class="link-action">查看全部 <i class="mdi mdi-chevron-right"></i></button>
|
||
</div>
|
||
|
||
<div class="list-body">
|
||
<div v-for="item in progressItems" :key="item.id" class="progress-row">
|
||
<WorkbenchListIcon
|
||
:icon-key="item.iconKey"
|
||
:color="item.color"
|
||
:accent="item.accent"
|
||
/>
|
||
|
||
<div class="todo-copy progress-copy">
|
||
<strong>{{ item.title }}</strong>
|
||
<p>提交时间:{{ item.date }}</p>
|
||
</div>
|
||
|
||
<strong class="progress-amount">{{ item.amount }}</strong>
|
||
<span class="progress-status" :class="item.tone">{{ item.status }}</span>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
|
||
<article class="panel policy-panel">
|
||
<div class="section-head">
|
||
<h3>最新报销制度</h3>
|
||
<button type="button" class="link-action">查看全部 <i class="mdi mdi-chevron-right"></i></button>
|
||
</div>
|
||
|
||
<div class="policy-table">
|
||
<div class="policy-head policy-row">
|
||
<span class="policy-title-cell">制度名称</span>
|
||
<span class="policy-summary-cell">摘要</span>
|
||
<span class="policy-date-cell">发布日期</span>
|
||
</div>
|
||
|
||
<div v-for="item in policyItems" :key="item.name" class="policy-row">
|
||
<strong class="policy-title-cell">{{ item.name }}</strong>
|
||
<span class="policy-summary-cell">{{ item.summary }}</span>
|
||
<span class="policy-date-cell">{{ item.date }}</span>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
</section>
|
||
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||
import PanelHead from '../shared/PanelHead.vue'
|
||
import WorkbenchListIcon from '../shared/WorkbenchListIcon.vue'
|
||
import robotAssistant from '../../assets/robot-helper.png'
|
||
import { useSystemState } from '../../composables/useSystemState.js'
|
||
import { useToast } from '../../composables/useToast.js'
|
||
import { clearUserConversations, fetchLatestConversation } from '../../services/orchestrator.js'
|
||
import {
|
||
ASSISTANT_SESSION_SNAPSHOT_EVENT,
|
||
hasAssistantSessionSnapshot
|
||
} from '../../utils/assistantSessionSnapshot.js'
|
||
|
||
const props = defineProps({
|
||
showHeader: { type: Boolean, default: true },
|
||
assistantModalOpen: { type: Boolean, default: false }
|
||
})
|
||
|
||
const emit = defineEmits(['open-assistant'])
|
||
const { currentUser } = useSystemState()
|
||
const { toast } = useToast()
|
||
const assistantDraft = ref('')
|
||
const fileInputRef = ref(null)
|
||
const selectedFiles = ref([])
|
||
const pendingAction = ref('')
|
||
const latestExpenseConversation = ref(null)
|
||
const hasLocalExpenseSnapshot = ref(false)
|
||
const MAX_ATTACHMENTS = 10
|
||
const SESSION_TYPE_EXPENSE = 'expense'
|
||
const SESSION_TYPE_KNOWLEDGE = 'knowledge'
|
||
|
||
const hasExpenseConversation = computed(() =>
|
||
Boolean(latestExpenseConversation.value?.conversation_id || latestExpenseConversation.value?.conversationId)
|
||
|| hasLocalExpenseSnapshot.value
|
||
)
|
||
const expenseActionLabel = computed(() => (hasExpenseConversation.value ? '继续报销' : '新建报销'))
|
||
const expenseActionIcon = computed(() => (hasExpenseConversation.value ? 'mdi mdi-history' : 'mdi mdi-magnify-scan'))
|
||
const assistantGreetingName = computed(() => {
|
||
const user = currentUser.value || {}
|
||
return String(user.name || user.username || '同事').trim() || '同事'
|
||
})
|
||
|
||
function buildSelectedFileKey(file) {
|
||
return [file?.name, file?.size, file?.lastModified, file?.type].join('__')
|
||
}
|
||
|
||
function mergeSelectedFiles(existingFiles, incomingFiles) {
|
||
const nextFiles = []
|
||
const seen = new Set()
|
||
|
||
for (const file of existingFiles) {
|
||
const key = buildSelectedFileKey(file)
|
||
if (seen.has(key)) continue
|
||
seen.add(key)
|
||
nextFiles.push(file)
|
||
}
|
||
|
||
let overflowCount = 0
|
||
|
||
for (const file of incomingFiles) {
|
||
const key = buildSelectedFileKey(file)
|
||
if (seen.has(key)) continue
|
||
if (nextFiles.length >= MAX_ATTACHMENTS) {
|
||
overflowCount += 1
|
||
continue
|
||
}
|
||
seen.add(key)
|
||
nextFiles.push(file)
|
||
}
|
||
|
||
return {
|
||
files: nextFiles,
|
||
overflowCount
|
||
}
|
||
}
|
||
|
||
function resolveCurrentUserId() {
|
||
const user = currentUser.value || {}
|
||
return String(user.username || user.name || 'anonymous').trim() || 'anonymous'
|
||
}
|
||
|
||
function buildAssistantPayload() {
|
||
return {
|
||
prompt: assistantDraft.value.trim(),
|
||
source: 'workbench',
|
||
files: Array.from(selectedFiles.value)
|
||
}
|
||
}
|
||
|
||
function clearSelectedFiles() {
|
||
selectedFiles.value = []
|
||
if (fileInputRef.value) {
|
||
fileInputRef.value.value = ''
|
||
}
|
||
}
|
||
|
||
function resetWorkbenchDraft() {
|
||
assistantDraft.value = ''
|
||
clearSelectedFiles()
|
||
}
|
||
|
||
function emitAssistant(payload) {
|
||
emit('open-assistant', payload)
|
||
resetWorkbenchDraft()
|
||
}
|
||
|
||
async function loadLatestConversation() {
|
||
const payload = await fetchLatestConversation(resolveCurrentUserId(), SESSION_TYPE_EXPENSE, {
|
||
preferRecoverable: true
|
||
})
|
||
return payload?.found ? payload.conversation || null : null
|
||
}
|
||
|
||
function handleWorkbenchEnter(event) {
|
||
if (event.isComposing) {
|
||
return
|
||
}
|
||
|
||
handleExpenseConversationAction()
|
||
}
|
||
|
||
function triggerFileUpload() {
|
||
fileInputRef.value?.click()
|
||
}
|
||
|
||
function handleWorkbenchFilesChange(event) {
|
||
const mergeResult = mergeSelectedFiles(selectedFiles.value, Array.from(event.target.files ?? []))
|
||
selectedFiles.value = mergeResult.files
|
||
if (mergeResult.overflowCount > 0) {
|
||
toast(`一次最多上传 ${MAX_ATTACHMENTS} 份附件,已保留前 ${MAX_ATTACHMENTS} 份。`)
|
||
}
|
||
if (fileInputRef.value) {
|
||
fileInputRef.value.value = ''
|
||
}
|
||
}
|
||
|
||
async function refreshLatestExpenseConversation() {
|
||
refreshLocalExpenseSnapshot()
|
||
try {
|
||
latestExpenseConversation.value = await loadLatestConversation()
|
||
} catch (error) {
|
||
console.warn('Failed to refresh latest expense conversation:', error)
|
||
latestExpenseConversation.value = null
|
||
}
|
||
}
|
||
|
||
function refreshLocalExpenseSnapshot() {
|
||
hasLocalExpenseSnapshot.value = hasAssistantSessionSnapshot(resolveCurrentUserId(), SESSION_TYPE_EXPENSE)
|
||
}
|
||
|
||
function handleAssistantSessionSnapshotChange(event) {
|
||
const sessionType = String(event?.detail?.sessionType || '').trim()
|
||
if (!sessionType || sessionType === SESSION_TYPE_EXPENSE) {
|
||
refreshLocalExpenseSnapshot()
|
||
}
|
||
}
|
||
|
||
async function clearKnowledgeHistoryBeforeExpense() {
|
||
await clearUserConversations(resolveCurrentUserId(), SESSION_TYPE_KNOWLEDGE)
|
||
}
|
||
|
||
async function handleExpenseConversationAction() {
|
||
if (pendingAction.value) {
|
||
return
|
||
}
|
||
|
||
const nextPayload = buildAssistantPayload()
|
||
const shouldOpenImmediately = Boolean(nextPayload.prompt || nextPayload.files.length)
|
||
|
||
if (shouldOpenImmediately) {
|
||
emitAssistant({
|
||
...nextPayload,
|
||
conversation: null
|
||
})
|
||
void clearKnowledgeHistoryBeforeExpense().catch((error) => {
|
||
console.warn('Failed to clear knowledge history before expense:', error)
|
||
})
|
||
return
|
||
}
|
||
|
||
pendingAction.value = 'expense'
|
||
|
||
try {
|
||
await clearKnowledgeHistoryBeforeExpense()
|
||
const conversation = await loadLatestConversation()
|
||
latestExpenseConversation.value = conversation
|
||
emitAssistant({
|
||
...nextPayload,
|
||
conversation
|
||
})
|
||
} catch (error) {
|
||
console.warn('Failed to open expense conversation:', error)
|
||
toast(error?.message || '打开报销会话失败,请稍后重试。')
|
||
} finally {
|
||
pendingAction.value = ''
|
||
}
|
||
}
|
||
|
||
const todoItems = [
|
||
{
|
||
title: '业务招待报销建议补参与人员',
|
||
tipLabel: 'AI 建议',
|
||
suggestion: '补充客户单位、客户人数、我方陪同人员',
|
||
action: '去补充',
|
||
iconKey: 'hospitality',
|
||
color: '#0d9668',
|
||
accent: '#6ee7b7'
|
||
},
|
||
{
|
||
title: '差旅报销单待提交',
|
||
tipLabel: 'AI 建议',
|
||
suggestion: '补齐出发交通,可直接生成报销单',
|
||
action: '继续填写',
|
||
iconKey: 'travelDraft',
|
||
color: '#15803d',
|
||
accent: '#86efac'
|
||
},
|
||
{
|
||
title: '有 5 张票据未关联报销单',
|
||
tipLabel: 'AI 建议',
|
||
suggestion: '其中 3 张疑似交通费,可合并生成交通报销',
|
||
action: '去整理',
|
||
iconKey: 'receipts',
|
||
color: '#2563eb',
|
||
accent: '#93c5fd'
|
||
}
|
||
]
|
||
|
||
const todoAlertCount = todoItems.length
|
||
|
||
const progressItems = [
|
||
{
|
||
id: 'travel',
|
||
title: '差旅报销',
|
||
amount: '¥3,280',
|
||
date: '2026-05-03',
|
||
status: '主管审批中',
|
||
tone: 'success',
|
||
iconKey: 'flight',
|
||
color: '#0d9668',
|
||
accent: '#6ee7b7'
|
||
},
|
||
{
|
||
id: 'transport',
|
||
title: '交通报销',
|
||
amount: '¥126',
|
||
date: '2026-05-02',
|
||
status: '财务复核中',
|
||
tone: 'info',
|
||
iconKey: 'transport',
|
||
color: '#2563eb',
|
||
accent: '#93c5fd'
|
||
},
|
||
{
|
||
id: 'office',
|
||
title: '办公采购',
|
||
amount: '¥458',
|
||
date: '2026-05-01',
|
||
status: '已到账',
|
||
tone: 'mint',
|
||
iconKey: 'procurement',
|
||
color: '#059669',
|
||
accent: '#a7f3d0'
|
||
}
|
||
]
|
||
|
||
const progressAlertCount = progressItems.filter((item) => item.status !== '已到账').length
|
||
|
||
const policyItems = [
|
||
{
|
||
name: '差旅报销管理办法(2026版)',
|
||
summary: '更新住宿标准与交通等级规则',
|
||
date: '2026-05-04'
|
||
},
|
||
{
|
||
name: '业务招待费用报销规范',
|
||
summary: '明确参与人员与事由填写要求',
|
||
date: '2026-05-02'
|
||
},
|
||
{
|
||
name: '交通费用报销细则',
|
||
summary: '补充网约车与停车费报销说明',
|
||
date: '2026-04-28'
|
||
},
|
||
{
|
||
name: '票据与附件提交规范通知',
|
||
summary: '统一附件命名与上传要求',
|
||
date: '2026-04-25'
|
||
}
|
||
]
|
||
|
||
onMounted(() => {
|
||
refreshLocalExpenseSnapshot()
|
||
refreshLatestExpenseConversation()
|
||
window.addEventListener(ASSISTANT_SESSION_SNAPSHOT_EVENT, handleAssistantSessionSnapshotChange)
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
window.removeEventListener(ASSISTANT_SESSION_SNAPSHOT_EVENT, handleAssistantSessionSnapshotChange)
|
||
})
|
||
|
||
watch(
|
||
() => props.assistantModalOpen,
|
||
(open, previous) => {
|
||
if (previous && !open) {
|
||
refreshLatestExpenseConversation()
|
||
}
|
||
}
|
||
)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.workbench {
|
||
min-width: 0;
|
||
display: grid;
|
||
gap: 16px;
|
||
padding-bottom: 10px;
|
||
}
|
||
|
||
.assistant-hero {
|
||
position: relative;
|
||
overflow: hidden;
|
||
display: grid;
|
||
grid-template-columns: 228px minmax(0, 1fr);
|
||
gap: 18px;
|
||
padding: 20px 24px 20px 18px;
|
||
border: 1px solid rgba(16, 185, 129, 0.12);
|
||
background:
|
||
radial-gradient(circle at top left, rgba(16, 185, 129, 0.12), transparent 34%),
|
||
radial-gradient(circle at right 20%, rgba(59, 130, 246, 0.07), transparent 28%),
|
||
linear-gradient(135deg, #f7fffb 0%, #ffffff 48%, #f5fbff 100%);
|
||
}
|
||
|
||
.assistant-hero::before,
|
||
.assistant-hero::after {
|
||
content: "";
|
||
position: absolute;
|
||
border-radius: 999px;
|
||
background: rgba(16, 185, 129, 0.06);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.assistant-hero::before {
|
||
right: -48px;
|
||
bottom: -58px;
|
||
width: 220px;
|
||
height: 220px;
|
||
}
|
||
|
||
.assistant-hero::after {
|
||
right: 92px;
|
||
top: -44px;
|
||
width: 140px;
|
||
height: 140px;
|
||
}
|
||
|
||
.assistant-visual {
|
||
position: relative;
|
||
min-height: 196px;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: flex-start;
|
||
padding: 0 0 10px 8px;
|
||
}
|
||
|
||
.assistant-visual::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: auto auto -78px -58px;
|
||
width: 264px;
|
||
height: 228px;
|
||
border-radius: 50%;
|
||
background: radial-gradient(circle at 48% 38%, rgba(255, 255, 255, 0.92) 0%, rgba(220, 252, 231, 0.84) 58%, rgba(220, 252, 231, 0) 100%);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.assistant-visual::after {
|
||
content: "";
|
||
position: absolute;
|
||
left: 52px;
|
||
bottom: 18px;
|
||
width: 132px;
|
||
height: 18px;
|
||
border-radius: 999px;
|
||
background: rgba(16, 185, 129, 0.14);
|
||
filter: blur(12px);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.assistant-glow {
|
||
position: absolute;
|
||
left: 24px;
|
||
bottom: 22px;
|
||
width: 176px;
|
||
height: 176px;
|
||
border-radius: 50%;
|
||
background: radial-gradient(circle, rgba(255, 255, 255, 0.98) 0%, rgba(236, 253, 245, 0.9) 58%, rgba(236, 253, 245, 0) 100%);
|
||
box-shadow: 0 24px 48px rgba(16, 185, 129, 0.12);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.assistant-image {
|
||
position: relative;
|
||
z-index: 1;
|
||
width: 184px;
|
||
max-width: 100%;
|
||
height: auto;
|
||
object-fit: contain;
|
||
object-position: left bottom;
|
||
filter: drop-shadow(0 22px 28px rgba(15, 23, 42, 0.16));
|
||
}
|
||
|
||
.assistant-copy {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: grid;
|
||
gap: 10px;
|
||
align-content: center;
|
||
}
|
||
|
||
.assistant-copy h3 {
|
||
color: #0f172a;
|
||
font-size: 26px;
|
||
line-height: 1.25;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.assistant-copy p {
|
||
max-width: 760px;
|
||
color: #5b6b83;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.assistant-input {
|
||
display: flex;
|
||
align-items: center;
|
||
min-height: 48px;
|
||
padding: 4px 14px;
|
||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||
border-radius: 12px;
|
||
background: rgba(255, 255, 255, 0.92);
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||
}
|
||
|
||
.assistant-file-input {
|
||
display: none;
|
||
}
|
||
|
||
.assistant-input textarea {
|
||
min-width: 0;
|
||
flex: 1;
|
||
height: 22px;
|
||
min-height: 22px;
|
||
max-height: 22px;
|
||
resize: none;
|
||
border: 0;
|
||
padding: 1px 0;
|
||
background: transparent;
|
||
color: #0f172a;
|
||
font-size: 15px;
|
||
line-height: 22px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.assistant-input textarea::placeholder {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.assistant-input textarea:focus {
|
||
outline: none;
|
||
}
|
||
|
||
.hero-action,
|
||
.secondary-action,
|
||
.ghost-action,
|
||
.row-action,
|
||
.link-action,
|
||
.row-link {
|
||
border: 0;
|
||
background: transparent;
|
||
}
|
||
|
||
.hero-action {
|
||
height: 40px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
padding: 0 16px;
|
||
border-radius: 10px;
|
||
background: linear-gradient(135deg, #10b981, #059669);
|
||
color: #fff;
|
||
font-size: 14px;
|
||
font-weight: 800;
|
||
white-space: nowrap;
|
||
box-shadow: 0 10px 22px rgba(16, 185, 129, 0.18);
|
||
}
|
||
|
||
.hero-action .mdi,
|
||
.secondary-action .mdi,
|
||
.ghost-action .mdi {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.hero-action span,
|
||
.secondary-action span,
|
||
.ghost-action span {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
line-height: 1;
|
||
}
|
||
|
||
.assistant-tools {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.assistant-file-strip {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.assistant-file-note,
|
||
.assistant-file-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
min-height: 30px;
|
||
padding: 0 12px;
|
||
border-radius: 999px;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.assistant-file-note {
|
||
background: rgba(16, 185, 129, 0.1);
|
||
color: #047857;
|
||
}
|
||
|
||
.assistant-file-chip {
|
||
max-width: 220px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
border: 1px solid rgba(148, 163, 184, 0.24);
|
||
background: rgba(255, 255, 255, 0.9);
|
||
color: #475569;
|
||
}
|
||
|
||
.assistant-file-clear {
|
||
border: 0;
|
||
background: transparent;
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.ghost-action {
|
||
height: 40px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
padding: 0 16px;
|
||
border: 1px solid rgba(15, 118, 110, 0.18);
|
||
border-radius: 10px;
|
||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(244, 250, 247, 0.88));
|
||
color: #0f766e;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
box-shadow:
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.9),
|
||
0 6px 14px rgba(15, 118, 110, 0.06);
|
||
}
|
||
|
||
.ghost-action .mdi {
|
||
color: #10b981;
|
||
}
|
||
|
||
.secondary-action {
|
||
height: 40px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
padding: 0 16px;
|
||
border: 1px solid rgba(59, 130, 246, 0.18);
|
||
border-radius: 10px;
|
||
background: linear-gradient(180deg, rgba(244, 249, 255, 0.96), rgba(234, 244, 255, 0.9));
|
||
color: #1d4ed8;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
white-space: nowrap;
|
||
box-shadow:
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.92),
|
||
0 6px 14px rgba(37, 99, 235, 0.08);
|
||
}
|
||
|
||
.secondary-action .mdi {
|
||
color: #2563eb;
|
||
}
|
||
|
||
.hero-action:disabled,
|
||
.secondary-action:disabled,
|
||
.ghost-action:disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.68;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.workbench-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 20px;
|
||
}
|
||
|
||
.list-panel,
|
||
.policy-panel {
|
||
padding: 20px 22px;
|
||
}
|
||
|
||
.section-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.section-head h3 {
|
||
color: #0f172a;
|
||
font-size: 17px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.title-with-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.alert-badge {
|
||
min-width: 22px;
|
||
height: 22px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 7px;
|
||
border-radius: 999px;
|
||
background: #ef4444;
|
||
color: #fff;
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
line-height: 1;
|
||
box-shadow: 0 6px 14px rgba(239, 68, 68, 0.22);
|
||
}
|
||
|
||
.link-action {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
color: #10b981;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.list-body {
|
||
display: grid;
|
||
}
|
||
|
||
.todo-row,
|
||
.progress-row {
|
||
display: grid;
|
||
grid-template-columns: 56px minmax(0, 1fr) auto;
|
||
gap: 14px;
|
||
align-items: center;
|
||
padding: 14px 0;
|
||
border-top: 1px solid #edf2f7;
|
||
}
|
||
|
||
.todo-row:first-child,
|
||
.progress-row:first-child {
|
||
padding-top: 4px;
|
||
border-top: 0;
|
||
}
|
||
|
||
.todo-copy {
|
||
min-width: 0;
|
||
}
|
||
|
||
.todo-copy strong {
|
||
display: block;
|
||
color: #0f172a;
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.todo-copy p {
|
||
margin-top: 4px;
|
||
color: #6b7280;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.todo-advice {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.todo-advice-label {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
min-height: 22px;
|
||
padding: 0 8px;
|
||
border-radius: 999px;
|
||
background: #ecfdf5;
|
||
color: #059669;
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.todo-advice-text {
|
||
color: #64748b;
|
||
}
|
||
|
||
.row-action {
|
||
height: 38px;
|
||
padding: 0 16px;
|
||
border: 1px solid rgba(16, 185, 129, 0.36);
|
||
border-radius: 10px;
|
||
color: #10b981;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.progress-row {
|
||
grid-template-columns: 56px minmax(0, 1fr) minmax(84px, auto) minmax(104px, auto);
|
||
gap: 14px 16px;
|
||
}
|
||
|
||
.progress-copy strong {
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.progress-amount {
|
||
color: #0f172a;
|
||
font-size: 20px;
|
||
font-weight: 800;
|
||
line-height: 1;
|
||
text-align: right;
|
||
font-variant-numeric: tabular-nums;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.progress-status {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 104px;
|
||
min-height: 34px;
|
||
padding: 6px 14px;
|
||
border-radius: 999px;
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
white-space: nowrap;
|
||
justify-self: end;
|
||
}
|
||
|
||
.progress-status.success,
|
||
.policy-status.success {
|
||
background: #eafaf2;
|
||
color: #16935f;
|
||
}
|
||
|
||
.progress-status.info,
|
||
.policy-status.info {
|
||
background: #eff6ff;
|
||
color: #3b82f6;
|
||
}
|
||
|
||
.progress-status.mint {
|
||
background: #edfdf5;
|
||
color: #10b981;
|
||
}
|
||
|
||
.policy-table {
|
||
border: 1px solid #e7edf5;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.policy-row {
|
||
display: grid;
|
||
grid-template-columns: 2.2fr 2.4fr 1fr;
|
||
gap: 16px;
|
||
align-items: center;
|
||
min-height: 56px;
|
||
padding: 0 18px;
|
||
border-top: 1px solid #edf2f7;
|
||
}
|
||
|
||
.policy-head {
|
||
min-height: 44px;
|
||
background: #f8fbff;
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
border-top: 0;
|
||
}
|
||
|
||
.policy-row strong,
|
||
.policy-row span {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.policy-row strong {
|
||
color: #0f172a;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.policy-row span {
|
||
color: #64748b;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.policy-title-cell,
|
||
.policy-summary-cell {
|
||
justify-self: stretch;
|
||
text-align: left;
|
||
}
|
||
|
||
.policy-date-cell {
|
||
justify-self: center;
|
||
text-align: center;
|
||
}
|
||
|
||
@media (max-width: 1320px) {
|
||
.assistant-copy h3 {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.policy-row {
|
||
grid-template-columns: 1.8fr 1.8fr 1fr;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1440px) {
|
||
.workbench {
|
||
gap: 14px;
|
||
}
|
||
|
||
.assistant-hero {
|
||
gap: 16px;
|
||
padding: 18px 20px 18px 16px;
|
||
}
|
||
|
||
.assistant-copy h3 {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.assistant-visual {
|
||
min-height: 184px;
|
||
}
|
||
|
||
.assistant-image {
|
||
width: 172px;
|
||
}
|
||
|
||
.workbench-grid {
|
||
gap: 16px;
|
||
}
|
||
|
||
.list-panel,
|
||
.policy-panel {
|
||
padding: 18px 20px;
|
||
}
|
||
|
||
.policy-row {
|
||
min-height: 52px;
|
||
padding: 0 16px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1080px) {
|
||
.assistant-hero {
|
||
grid-template-columns: 1fr;
|
||
gap: 8px;
|
||
}
|
||
|
||
.assistant-visual {
|
||
min-height: 188px;
|
||
justify-content: center;
|
||
padding: 0 0 8px;
|
||
}
|
||
|
||
.assistant-visual::before,
|
||
.assistant-visual::after,
|
||
.assistant-glow {
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
}
|
||
|
||
.assistant-visual::before {
|
||
inset: auto auto -82px 50%;
|
||
}
|
||
|
||
.assistant-image {
|
||
width: 176px;
|
||
}
|
||
|
||
.workbench-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 860px) {
|
||
.assistant-hero,
|
||
.list-panel,
|
||
.policy-panel {
|
||
padding: 18px;
|
||
}
|
||
|
||
.assistant-input {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
padding: 14px;
|
||
}
|
||
|
||
.assistant-visual {
|
||
min-height: 160px;
|
||
}
|
||
|
||
.assistant-glow {
|
||
width: 148px;
|
||
height: 148px;
|
||
}
|
||
|
||
.assistant-image {
|
||
width: 150px;
|
||
}
|
||
|
||
.assistant-input textarea {
|
||
height: 40px;
|
||
min-height: 40px;
|
||
max-height: 40px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.hero-action,
|
||
.secondary-action,
|
||
.ghost-action,
|
||
.row-action {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
|
||
.assistant-file-chip {
|
||
max-width: 100%;
|
||
}
|
||
|
||
.todo-row,
|
||
.progress-row {
|
||
grid-template-columns: 56px minmax(0, 1fr);
|
||
}
|
||
|
||
.progress-amount {
|
||
grid-column: 2;
|
||
text-align: left;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.row-action,
|
||
.progress-status {
|
||
grid-column: 2;
|
||
justify-self: start;
|
||
}
|
||
|
||
.policy-table {
|
||
border: 0;
|
||
border-radius: 0;
|
||
}
|
||
|
||
.policy-head {
|
||
display: none;
|
||
}
|
||
|
||
.policy-row {
|
||
grid-template-columns: 1fr;
|
||
gap: 8px;
|
||
padding: 16px 0;
|
||
border-top: 1px solid #edf2f7;
|
||
}
|
||
|
||
.policy-row strong,
|
||
.policy-row span {
|
||
white-space: normal;
|
||
}
|
||
}
|
||
</style>
|