feat: add skill center navigation and travel request detail view

- Refactor audit to skill center for skill management
- Add TravelRequestDetailView for requests detail page
- Update PersonalWorkbench with enhanced features
- Add UI preview screenshots

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-05 22:35:38 +08:00
parent ab9f132192
commit 2d2ed27861
10 changed files with 3870 additions and 591 deletions

BIN
UI/首页工作台.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 KiB

View File

@@ -24,7 +24,8 @@
'workbench-main': activeView === 'workbench', 'workbench-main': activeView === 'workbench',
'requests-main': activeView === 'requests', 'requests-main': activeView === 'requests',
'approval-main': activeView === 'approval', 'approval-main': activeView === 'approval',
'policies-main': activeView === 'policies' 'policies-main': activeView === 'policies',
'audit-main': activeView === 'audit'
}" }"
> >
<TopBar <TopBar
@@ -43,7 +44,7 @@
/> />
<FilterBar <FilterBar
v-if="activeView !== 'chat' && activeView !== 'overview' && activeView !== 'workbench' && activeView !== 'requests' && activeView !== 'approval' && activeView !== 'policies'" v-if="activeView !== 'chat' && activeView !== 'overview' && activeView !== 'workbench' && activeView !== 'requests' && activeView !== 'approval' && activeView !== 'policies' && activeView !== 'audit'"
:compact="activeView === 'overview'" :compact="activeView === 'overview'"
:filters="filters" :filters="filters"
:ranges="ranges" :ranges="ranges"
@@ -57,7 +58,8 @@
'chat-workarea': activeView === 'chat', 'chat-workarea': activeView === 'chat',
'requests-workarea': activeView === 'requests', 'requests-workarea': activeView === 'requests',
'approval-workarea': activeView === 'approval', 'approval-workarea': activeView === 'approval',
'policies-workarea': activeView === 'policies' 'policies-workarea': activeView === 'policies',
'audit-workarea': activeView === 'audit'
}" }"
> >
<OverviewView <OverviewView
@@ -96,15 +98,16 @@
@reject-case="toast(`${activeCase?.id} 已转人工复核`)" @reject-case="toast(`${activeCase?.id} 已转人工复核`)"
/> />
<TravelReimbursementCreateView <TravelRequestDetailView
v-else-if="activeView === 'requests' && detailMode" v-else-if="activeView === 'requests' && detailMode"
@back-to-requests="detailMode = false" :request="selectedTravelRequest"
@back-to-requests="closeRequestDetail"
/> />
<RequestsView <RequestsView
v-else-if="activeView === 'requests'" v-else-if="activeView === 'requests'"
:filtered-requests="filteredRequests" :filtered-requests="filteredRequests"
@ask="detailMode = true" @ask="openRequestDetail"
@approve="handleApprove" @approve="handleApprove"
@reject="handleReject" @reject="handleReject"
@create-request="openTravelCreate" @create-request="openTravelCreate"
@@ -135,6 +138,7 @@ import OverviewView from './views/OverviewView.vue'
import PersonalWorkbenchView from './views/PersonalWorkbenchView.vue' import PersonalWorkbenchView from './views/PersonalWorkbenchView.vue'
import ChatView from './views/ChatView.vue' import ChatView from './views/ChatView.vue'
import TravelReimbursementCreateView from './views/TravelReimbursementCreateView.vue' import TravelReimbursementCreateView from './views/TravelReimbursementCreateView.vue'
import TravelRequestDetailView from './views/TravelRequestDetailView.vue'
import RequestsView from './views/RequestsView.vue' import RequestsView from './views/RequestsView.vue'
import ApprovalCenterView from './views/ApprovalCenterView.vue' import ApprovalCenterView from './views/ApprovalCenterView.vue'
import PoliciesView from './views/PoliciesView.vue' import PoliciesView from './views/PoliciesView.vue'
@@ -150,6 +154,7 @@ import { documents } from './data/requests.js'
const loggedIn = ref(false) const loggedIn = ref(false)
const travelCreateMode = ref(false) const travelCreateMode = ref(false)
const detailMode = ref(false) const detailMode = ref(false)
const selectedTravelRequest = ref(null)
function handleLogin(credentials) { function handleLogin(credentials) {
if (credentials.username && credentials.password) { if (credentials.username && credentials.password) {
@@ -205,6 +210,7 @@ function handleReject(request) {
function handleNavigate(view) { function handleNavigate(view) {
travelCreateMode.value = false travelCreateMode.value = false
detailMode.value = false detailMode.value = false
selectedTravelRequest.value = null
setView(view) setView(view)
} }
@@ -215,6 +221,8 @@ function handleOpenChat(request) {
function openTravelCreate() { function openTravelCreate() {
travelCreateMode.value = true travelCreateMode.value = true
detailMode.value = false
selectedTravelRequest.value = null
activeView.value = 'chat' activeView.value = 'chat'
} }
@@ -222,6 +230,17 @@ function backToRequests() {
travelCreateMode.value = false travelCreateMode.value = false
activeView.value = 'requests' activeView.value = 'requests'
} }
function openRequestDetail(request) {
selectedTravelRequest.value = request
detailMode.value = true
activeView.value = 'requests'
}
function closeRequestDetail() {
detailMode.value = false
selectedTravelRequest.value = null
}
</script> </script>
<style scoped> <style scoped>
@@ -245,7 +264,8 @@ function backToRequests() {
} }
.main.requests-main, .main.requests-main,
.main.approval-main, .main.approval-main,
.main.policies-main { .main.policies-main,
.main.audit-main {
height: 100dvh; height: 100dvh;
grid-template-rows: auto minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr);
overflow: hidden; overflow: hidden;
@@ -257,7 +277,8 @@ function backToRequests() {
} }
.workarea.requests-workarea, .workarea.requests-workarea,
.workarea.approval-workarea, .workarea.approval-workarea,
.workarea.policies-workarea { .workarea.policies-workarea,
.workarea.audit-workarea {
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
padding: 20px 24px; padding: 20px 24px;

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 KiB

View File

@@ -10,7 +10,7 @@
<article class="panel assistant-hero"> <article class="panel assistant-hero">
<div class="assistant-visual" aria-hidden="true"> <div class="assistant-visual" aria-hidden="true">
<div class="assistant-core"> <div class="assistant-core">
<i class="mdi mdi-robot-happy-outline"></i> <img class="assistant-image" :src="robotAssistant" alt="" />
</div> </div>
</div> </div>
@@ -40,8 +40,11 @@
<div class="workbench-grid"> <div class="workbench-grid">
<article class="panel list-panel"> <article class="panel list-panel">
<div class="section-head"> <div class="section-head">
<div class="title-with-badge">
<h3>今日待办</h3> <h3>今日待办</h3>
<span class="count-chip">{{ todoItems.length }} </span> <span class="alert-badge">{{ todoAlertCount }}</span>
</div>
<button type="button" class="link-action">查看全部 <i class="mdi mdi-chevron-right"></i></button>
</div> </div>
<div class="list-body"> <div class="list-body">
@@ -52,7 +55,10 @@
<div class="todo-copy"> <div class="todo-copy">
<strong>{{ item.title }}</strong> <strong>{{ item.title }}</strong>
<p>{{ item.suggestion }}</p> <p class="todo-advice">
<span class="todo-advice-label">{{ item.tipLabel }}</span>
<span class="todo-advice-text">{{ item.suggestion }}</span>
</p>
</div> </div>
<button type="button" class="row-action" @click="emit('openAssistant')">{{ item.action }}</button> <button type="button" class="row-action" @click="emit('openAssistant')">{{ item.action }}</button>
@@ -62,7 +68,10 @@
<article class="panel list-panel"> <article class="panel list-panel">
<div class="section-head"> <div class="section-head">
<div class="title-with-badge">
<h3>报销进度</h3> <h3>报销进度</h3>
<span class="alert-badge">{{ progressAlertCount }}</span>
</div>
<button type="button" class="link-action">查看全部 <i class="mdi mdi-chevron-right"></i></button> <button type="button" class="link-action">查看全部 <i class="mdi mdi-chevron-right"></i></button>
</div> </div>
@@ -73,13 +82,11 @@
</div> </div>
<div class="todo-copy progress-copy"> <div class="todo-copy progress-copy">
<div class="progress-main">
<strong>{{ item.title }}</strong> <strong>{{ item.title }}</strong>
<strong class="amount">{{ item.amount }}</strong>
</div>
<p>提交时间{{ item.date }}</p> <p>提交时间{{ item.date }}</p>
</div> </div>
<strong class="progress-amount">{{ item.amount }}</strong>
<span class="progress-status" :class="item.tone">{{ item.status }}</span> <span class="progress-status" :class="item.tone">{{ item.status }}</span>
</div> </div>
</div> </div>
@@ -94,23 +101,15 @@
<div class="policy-table"> <div class="policy-table">
<div class="policy-head policy-row"> <div class="policy-head policy-row">
<span>制度名称</span> <span class="policy-title-cell">制度名称</span>
<span>摘要</span> <span class="policy-summary-cell">摘要</span>
<span>发布日期</span> <span class="policy-date-cell">发布日期</span>
<span>状态</span>
<span></span>
</div> </div>
<div v-for="item in policyItems" :key="item.name" class="policy-row"> <div v-for="item in policyItems" :key="item.name" class="policy-row">
<strong>{{ item.name }}</strong> <strong class="policy-title-cell">{{ item.name }}</strong>
<span>{{ item.summary }}</span> <span class="policy-summary-cell">{{ item.summary }}</span>
<span>{{ item.date }}</span> <span class="policy-date-cell">{{ item.date }}</span>
<span>
<b class="policy-status" :class="item.tone">{{ item.status }}</b>
</span>
<button type="button" class="row-link" :aria-label="`查看 ${item.name}`">
<i class="mdi mdi-chevron-right"></i>
</button>
</div> </div>
</div> </div>
</article> </article>
@@ -119,6 +118,7 @@
<script setup> <script setup>
import PanelHead from '../shared/PanelHead.vue' import PanelHead from '../shared/PanelHead.vue'
import robotAssistant from '../../assets/robot-assistant.png'
defineProps({ defineProps({
showHeader: { type: Boolean, default: true } showHeader: { type: Boolean, default: true }
@@ -131,27 +131,32 @@ const assistantSkills = ['识别报销类别', '检查缺少材料', '生成报
const todoItems = [ const todoItems = [
{ {
title: '业务招待报销建议补参与人员', title: '业务招待报销建议补参与人员',
suggestion: 'AI 建议:补充客户单位、客户人数、我方陪同人员', tipLabel: 'AI 建议',
suggestion: '补充客户单位、客户人数、我方陪同人员',
action: '去补充', action: '去补充',
icon: 'mdi mdi-account-group-outline', icon: 'mdi mdi-account-group-outline',
color: '#10b981' color: '#10b981'
}, },
{ {
title: '差旅报销单待提交', title: '差旅报销单待提交',
suggestion: 'AI 建议:补齐出发交通,可直接生成报销单', tipLabel: 'AI 建议',
suggestion: '补齐出发交通,可直接生成报销单',
action: '继续填写', action: '继续填写',
icon: 'mdi mdi-briefcase-outline', icon: 'mdi mdi-briefcase-outline',
color: '#16a34a' color: '#16a34a'
}, },
{ {
title: '有 5 张票据未关联报销单', title: '有 5 张票据未关联报销单',
suggestion: 'AI 建议:其中 3 张疑似交通费,可合并生成交通报销', tipLabel: 'AI 建议',
suggestion: '其中 3 张疑似交通费,可合并生成交通报销',
action: '去整理', action: '去整理',
icon: 'mdi mdi-receipt-text-outline', icon: 'mdi mdi-receipt-text-outline',
color: '#3b82f6' color: '#3b82f6'
} }
] ]
const todoAlertCount = todoItems.length
const progressItems = [ const progressItems = [
{ {
id: 'travel', id: 'travel',
@@ -185,34 +190,28 @@ const progressItems = [
} }
] ]
const progressAlertCount = progressItems.filter((item) => item.status !== '已到账').length
const policyItems = [ const policyItems = [
{ {
name: '差旅报销管理办法2026版', name: '差旅报销管理办法2026版',
summary: '更新住宿标准与交通等级规则', summary: '更新住宿标准与交通等级规则',
date: '2026-05-04', date: '2026-05-04'
status: '已生效',
tone: 'success'
}, },
{ {
name: '业务招待费用报销规范', name: '业务招待费用报销规范',
summary: '明确参与人员与事由填写要求', summary: '明确参与人员与事由填写要求',
date: '2026-05-02', date: '2026-05-02'
status: '更新',
tone: 'warning'
}, },
{ {
name: '交通费用报销细则', name: '交通费用报销细则',
summary: '补充网约车与停车费报销说明', summary: '补充网约车与停车费报销说明',
date: '2026-04-28', date: '2026-04-28'
status: '已生效',
tone: 'success'
}, },
{ {
name: '票据与附件提交规范通知', name: '票据与附件提交规范通知',
summary: '统一附件命名与上传要求', summary: '统一附件命名与上传要求',
date: '2026-04-25', date: '2026-04-25'
status: '通知',
tone: 'info'
} }
] ]
</script> </script>
@@ -306,6 +305,13 @@ const policyItems = [
font-size: 68px; font-size: 68px;
} }
.assistant-image {
width: 104px;
height: 104px;
object-fit: contain;
filter: drop-shadow(0 12px 20px rgba(15, 23, 42, 0.12));
}
.assistant-copy { .assistant-copy {
position: relative; position: relative;
z-index: 1; z-index: 1;
@@ -454,18 +460,27 @@ const policyItems = [
font-weight: 700; font-weight: 700;
} }
.count-chip { .title-with-badge {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.alert-badge {
min-width: 22px;
height: 22px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-width: 46px; padding: 0 7px;
height: 28px;
padding: 0 10px;
border-radius: 999px; border-radius: 999px;
background: #ecfdf5; background: #ef4444;
color: #0f9f78; color: #fff;
font-size: 12px; font-size: 12px;
font-weight: 800; font-weight: 800;
line-height: 1;
box-shadow: 0 6px 14px rgba(239, 68, 68, 0.22);
} }
.link-action { .link-action {
@@ -527,6 +542,30 @@ const policyItems = [
line-height: 1.5; 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 { .row-action {
height: 38px; height: 38px;
padding: 0 16px; padding: 0 16px;
@@ -538,27 +577,37 @@ const policyItems = [
white-space: nowrap; white-space: nowrap;
} }
.progress-copy .progress-main { .progress-row {
display: flex; grid-template-columns: 48px minmax(0, 1fr) minmax(84px, auto) minmax(104px, auto);
align-items: center; gap: 14px 16px;
justify-content: space-between;
gap: 12px;
} }
.progress-copy .amount { .progress-copy strong {
font-size: 17px; 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 { .progress-status {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 32px; min-width: 104px;
padding: 6px 12px; min-height: 34px;
padding: 6px 14px;
border-radius: 999px; border-radius: 999px;
font-size: 13px; font-size: 13px;
font-weight: 800; font-weight: 800;
white-space: nowrap; white-space: nowrap;
justify-self: end;
} }
.progress-status.success, .progress-status.success,
@@ -586,7 +635,7 @@ const policyItems = [
.policy-row { .policy-row {
display: grid; display: grid;
grid-template-columns: 2.1fr 2.2fr 1fr auto 40px; grid-template-columns: 2.2fr 2.4fr 1fr;
gap: 16px; gap: 16px;
align-items: center; align-items: center;
min-height: 56px; min-height: 56px;
@@ -622,30 +671,15 @@ const policyItems = [
font-size: 14px; font-size: 14px;
} }
.policy-status { .policy-title-cell,
display: inline-flex; .policy-summary-cell {
align-items: center; justify-self: stretch;
justify-content: center; text-align: left;
min-width: 66px;
min-height: 28px;
padding: 0 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 800;
} }
.policy-status.warning { .policy-date-cell {
background: #fff7e8; justify-self: center;
color: #f59e0b; text-align: center;
}
.row-link {
width: 32px;
height: 32px;
display: grid;
place-items: center;
border-radius: 8px;
color: #94a3b8;
} }
@media (max-width: 1320px) { @media (max-width: 1320px) {
@@ -654,7 +688,7 @@ const policyItems = [
} }
.policy-row { .policy-row {
grid-template-columns: 1.7fr 1.7fr 1fr auto 32px; grid-template-columns: 1.8fr 1.8fr 1fr;
} }
} }
@@ -701,6 +735,12 @@ const policyItems = [
grid-template-columns: 48px minmax(0, 1fr); grid-template-columns: 48px minmax(0, 1fr);
} }
.progress-amount {
grid-column: 2;
text-align: left;
font-size: 18px;
}
.row-action, .row-action,
.progress-status { .progress-status {
grid-column: 2; grid-column: 2;
@@ -727,9 +767,5 @@ const policyItems = [
.policy-row span { .policy-row span {
white-space: normal; white-space: normal;
} }
.policy-row > :last-child {
display: none;
}
} }
</style> </style>

View File

@@ -56,7 +56,7 @@ const sidebarMeta = {
approval: { label: '审批中心', badge: '12' }, approval: { label: '审批中心', badge: '12' },
chat: { label: 'AI助手' }, chat: { label: 'AI助手' },
policies: { label: '知识管理' }, policies: { label: '知识管理' },
audit: { label: '审计追踪' } audit: { label: '技能中心' }
} }
const decoratedNavItems = computed(() => const decoratedNavItems = computed(() =>

View File

@@ -52,11 +52,11 @@ export const navItems = [
}, },
{ {
id: 'audit', id: 'audit',
label: '审计追踪', label: '技能中心',
navHint: '关键动作与日志', navHint: 'Skill 设计与版本配置',
icon: icons.audit, icon: icons.skill,
title: '审计追踪', title: '技能中心',
desc: '查看关键审批动作、AI 建议和制度命中记录' desc: '统一管理技能的触发规则、提示词结构、输出约束与上线版本'
} }
] ]

View File

@@ -6,6 +6,7 @@ export const icons = {
list: iconPath('<path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01"/><path d="M3 12h.01"/><path d="M3 18h.01"/>'), list: iconPath('<path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01"/><path d="M3 12h.01"/><path d="M3 18h.01"/>'),
approval: iconPath('<path d="M9 11l2 2 4-5"/><path d="M20 12v5a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h8"/><path d="M17 3h4v4"/>'), approval: iconPath('<path d="M9 11l2 2 4-5"/><path d="M20 12v5a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h8"/><path d="M17 3h4v4"/>'),
file: iconPath('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8"/><path d="M8 17h5"/>'), file: iconPath('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8"/><path d="M8 17h5"/>'),
skill: iconPath('<path d="M12 3 9.5 8.5 3 11l6.5 2.5L12 19l2.5-5.5L21 11l-6.5-2.5z"/><path d="M19 19l.9 2 .9-2 2-.9-2-.9-.9-2-.9 2-2 .9z"/><path d="M5 5l.6 1.4L7 7l-1.4.6L5 9l-.6-1.4L3 7l1.4-.6z"/>'),
audit: iconPath('<path d="M12 8v4l3 3"/><path d="M3.05 11a9 9 0 1 1 .5 4"/><path d="M3 4v7h7"/>'), audit: iconPath('<path d="M12 8v4l3 3"/><path d="M3.05 11a9 9 0 1 1 .5 4"/><path d="M3 4v7h7"/>'),
search: iconPath('<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>'), search: iconPath('<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>'),
check: iconPath('<path d="M20 6 9 17l-5-5"/>'), check: iconPath('<path d="M20 6 9 17l-5-5"/>'),

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff