chore: remove prototype files and unused UI assets

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-05-01 00:39:24 +08:00
parent 64537119e0
commit 7d6dbc4ac0
21 changed files with 772 additions and 2544 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,8 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lxgw-wenkai/1.501/lxgw-wenkai.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css" />
<title>ReimburseOps - 企业报销智能运营台</title>
</head>
<body>

View File

@@ -1,12 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ReimburseOps - 企业报销智能运营台</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -21,7 +21,9 @@
:class="{
'chat-main': activeView === 'chat',
'overview-main': activeView === 'overview',
'requests-main': activeView === 'requests'
'requests-main': activeView === 'requests',
'approval-main': activeView === 'approval',
'policies-main': activeView === 'policies'
}"
>
<TopBar
@@ -40,7 +42,7 @@
/>
<FilterBar
v-if="activeView !== 'chat' && activeView !== 'overview' && activeView !== 'requests'"
v-if="activeView !== 'chat' && activeView !== 'overview' && activeView !== 'requests' && activeView !== 'approval' && activeView !== 'policies'"
:compact="activeView === 'overview'"
:filters="filters"
:ranges="ranges"
@@ -52,7 +54,9 @@
class="workarea"
:class="{
'chat-workarea': activeView === 'chat',
'requests-workarea': activeView === 'requests'
'requests-workarea': activeView === 'requests',
'approval-workarea': activeView === 'approval',
'policies-workarea': activeView === 'policies'
}"
>
<OverviewView
@@ -95,6 +99,8 @@
@create-request="openTravelCreate"
/>
<ApprovalCenterView v-else-if="activeView === 'approval'" />
<PoliciesView v-else-if="activeView === 'policies'" />
<AuditView v-else />
@@ -118,6 +124,7 @@ import OverviewView from './views/OverviewView.vue'
import ChatView from './views/ChatView.vue'
import TravelReimbursementCreateView from './views/TravelReimbursementCreateView.vue'
import RequestsView from './views/RequestsView.vue'
import ApprovalCenterView from './views/ApprovalCenterView.vue'
import PoliciesView from './views/PoliciesView.vue'
import AuditView from './views/AuditView.vue'
@@ -219,7 +226,9 @@ function backToRequests() {
grid-template-rows: auto minmax(0, 1fr);
overflow: hidden;
}
.main.requests-main {
.main.requests-main,
.main.approval-main,
.main.policies-main {
height: 100dvh;
grid-template-rows: auto minmax(0, 1fr);
overflow: hidden;
@@ -229,7 +238,9 @@ function backToRequests() {
min-height: 0;
overflow: hidden;
}
.workarea.requests-workarea {
.workarea.requests-workarea,
.workarea.approval-workarea,
.workarea.policies-workarea {
min-height: 0;
overflow: hidden;
padding: 20px 24px;

View File

@@ -23,11 +23,12 @@
--nav-muted: #7d89a5;
--radius: 8px;
--ease: cubic-bezier(.2, .8, .2, 1);
font-family: Inter, "SF Pro Display", "Segoe UI", "PingFang SC", "Microsoft YaHei", Arial, sans-serif;
font-family: "LXGW WenKai", Inter, "SF Pro Display", "PingFang SC", sans-serif;
}
* { box-sizing: border-box; }
body { margin: 0; min-height: 100dvh; color: var(--text); background: var(--bg); }
.mdi { line-height: 1; vertical-align: middle; }
button, input, select, textarea { font: inherit; }
button { cursor: pointer; }
button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 3px solid rgba(16,185,129,.20); outline-offset: 2px; }

View File

@@ -26,6 +26,7 @@ import {
Tooltip,
Legend
} from 'chart.js'
import { useAnimationProgress } from '../../composables/useAnimationProgress.js'
ChartJS.register(ArcElement, Tooltip, Legend)
@@ -35,10 +36,12 @@ const props = defineProps({
centerLabel: { type: String, required: true }
})
const progress = useAnimationProgress([() => props.items], 1150)
const chartData = computed(() => ({
labels: props.items.map((i) => i.name),
datasets: [{
data: props.items.map((i) => i.value),
data: props.items.map((i) => Math.max(Number((i.value * progress.value).toFixed(1)), 0.001)),
backgroundColor: props.items.map((i) => i.color),
borderWidth: 0,
cutout: '68%',
@@ -50,6 +53,19 @@ const chartData = computed(() => ({
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
animation: {
animateRotate: true,
animateScale: true,
duration: 900,
easing: 'easeOutQuart'
},
transitions: {
active: {
animation: {
duration: 180
}
}
},
plugins: {
legend: { display: false },
tooltip: {
@@ -64,7 +80,7 @@ const chartOptions = {
usePointStyle: true,
callbacks: {
label: (ctx) => {
const total = ctx.dataset.data.reduce((a, b) => a + b, 0)
const total = ctx.dataset.data.reduce((a, b) => a + b, 0) || 1
const pct = ((ctx.parsed / total) * 100).toFixed(1)
return ` ${ctx.label}: ${pct}%`
}
@@ -98,6 +114,8 @@ const chartOptions = {
place-content: center;
pointer-events: none;
text-align: center;
animation: donutCenterIn 620ms ease both;
animation-delay: 360ms;
}
.donut-center strong {
@@ -117,6 +135,8 @@ const chartOptions = {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px 16px;
animation: donutLegendIn 560ms ease both;
animation-delay: 480ms;
}
.legend-row {
@@ -142,4 +162,33 @@ const chartOptions = {
color: #94a3b8;
font-size: 11px;
}
@keyframes donutCenterIn {
from {
opacity: 0;
transform: scale(.92);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes donutLegendIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.donut-center,
.donut-legend {
animation: none;
}
}
</style>

View File

@@ -3,7 +3,7 @@
<div class="gauge-body">
<Doughnut :data="chartData" :options="chartOptions" />
<div class="gauge-center">
<strong>{{ ratio }}%</strong>
<strong>{{ animatedRatio }}%</strong>
<span>已执行</span>
</div>
</div>
@@ -32,6 +32,7 @@ import {
ArcElement,
Tooltip
} from 'chart.js'
import { useAnimationProgress } from '../../composables/useAnimationProgress.js'
ChartJS.register(ArcElement, Tooltip)
@@ -43,11 +44,13 @@ const props = defineProps({
})
const ratioValue = computed(() => Number(props.ratio))
const progress = useAnimationProgress([() => props.ratio], 1150)
const animatedRatio = computed(() => Number((ratioValue.value * progress.value).toFixed(0)))
const chartData = computed(() => ({
labels: ['已执行', '剩余'],
datasets: [{
data: [ratioValue.value, 100 - ratioValue.value],
data: [animatedRatio.value, 100 - animatedRatio.value],
backgroundColor: ['#10b981', '#e2e8f0'],
borderWidth: 0
}]
@@ -59,6 +62,11 @@ const chartOptions = {
rotation: -90,
circumference: 180,
cutout: '65%',
animation: {
animateRotate: true,
duration: 900,
easing: 'easeOutQuart'
},
plugins: {
legend: { display: false },
tooltip: { enabled: false }
@@ -88,6 +96,8 @@ const chartOptions = {
transform: translateX(-50%);
text-align: center;
pointer-events: none;
animation: gaugeCenterIn 620ms ease both;
animation-delay: 360ms;
}
.gauge-center strong {
@@ -109,6 +119,8 @@ const chartOptions = {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
text-align: center;
animation: gaugeSummaryIn 560ms ease both;
animation-delay: 500ms;
}
.gauge-summary span {
@@ -124,4 +136,33 @@ const chartOptions = {
font-size: 13px;
font-weight: 500;
}
</style>
@keyframes gaugeCenterIn {
from {
opacity: 0;
transform: translateX(-50%) translateY(8px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
@keyframes gaugeSummaryIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.gauge-center,
.gauge-summary {
animation: none;
}
}
</style>

View File

@@ -25,6 +25,7 @@ import {
Tooltip,
Legend
} from 'chart.js'
import { useAnimationProgress } from '../../composables/useAnimationProgress.js'
ChartJS.register(CategoryScale, LinearScale, BarElement, PointElement, LineElement, Filler, Tooltip, Legend)
@@ -35,12 +36,22 @@ const props = defineProps({
avgHours: { type: Array, required: true }
})
const progress = useAnimationProgress([
() => props.labels,
() => props.applications,
() => props.approved,
() => props.avgHours
], 1200)
const scaleSeries = (series, decimals = 0) =>
series.map((value) => Number((Number(value) * progress.value).toFixed(decimals)))
const chartData = computed(() => ({
labels: props.labels,
datasets: [
{
label: '申请量(单)',
data: props.applications,
data: scaleSeries(props.applications),
backgroundColor: '#10b981',
borderRadius: 4,
barPercentage: 0.6,
@@ -49,7 +60,7 @@ const chartData = computed(() => ({
},
{
label: '审批完成量(单)',
data: props.approved,
data: scaleSeries(props.approved),
backgroundColor: '#3b82f6',
borderRadius: 4,
barPercentage: 0.6,
@@ -58,7 +69,7 @@ const chartData = computed(() => ({
},
{
label: '平均审批时长(小时)',
data: props.avgHours,
data: scaleSeries(props.avgHours, 1),
borderColor: '#8b5cf6',
backgroundColor: 'transparent',
borderWidth: 2,
@@ -77,6 +88,10 @@ const chartData = computed(() => ({
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 900,
easing: 'easeOutQuart'
},
interaction: {
mode: 'index',
intersect: false

View File

@@ -9,7 +9,7 @@
</div>
<strong class="brand-name">星海科技</strong>
<button class="brand-toggle" type="button" aria-label="打开 AI 助手" @click="emit('openChat')">
<i class="pi pi-angle-double-left"></i>
<i class="mdi mdi-chevron-double-left"></i>
</button>
</div>
@@ -34,7 +34,7 @@
<strong>张晓明</strong>
<span>财务管理员</span>
</span>
<i class="pi pi-angle-down"></i>
<i class="mdi mdi-chevron-down"></i>
</button>
</aside>
</template>
@@ -52,8 +52,9 @@ const emit = defineEmits(['navigate', 'openChat'])
const sidebarMeta = {
overview: { label: '总览' },
requests: { label: '差旅申请/报销' },
approval: { label: '审批中心', badge: '12' },
chat: { label: 'AI助手' },
policies: { label: '政策规则' },
policies: { label: '知识管理' },
audit: { label: '审计追踪' }
}
@@ -278,7 +279,7 @@ const decoratedNavItems = computed(() =>
text-overflow: ellipsis;
}
.rail-user .pi {
.rail-user .mdi {
justify-self: end;
color: #718096;
font-size: 13px;

View File

@@ -7,8 +7,8 @@
</div>
<div class="top-actions">
<span v-if="isChat || !isOverview" class="search-wrap" :class="{ wide: isChat }">
<i class="pi pi-search search-icon"></i>
<span v-if="isChat" class="search-wrap" :class="{ wide: isChat }">
<i class="mdi mdi-magnify search-icon"></i>
<input
:value="search"
type="search"
@@ -19,7 +19,7 @@
<template v-if="isChat">
<button class="icon-btn notification" type="button" aria-label="查看通知">
<i class="pi pi-bell"></i>
<i class="mdi mdi-bell-outline"></i>
<span>1</span>
</button>
<button class="profile-btn" type="button" aria-label="打开用户菜单">
@@ -28,7 +28,7 @@
<strong>张晓明</strong>
<small>财务管理员</small>
</span>
<i class="pi pi-angle-down"></i>
<i class="mdi mdi-chevron-down"></i>
</button>
</template>
@@ -36,7 +36,7 @@
<div v-if="isOverview" class="range-combo" aria-label="首页时间范围">
<div class="range-shell">
<span class="range-meta">
<i class="pi pi-calendar"></i>
<i class="mdi mdi-calendar"></i>
<span>{{ activeDateLabel }}</span>
</span>
@@ -64,7 +64,7 @@
aria-haspopup="dialog"
@click="calendarOpen = !calendarOpen"
>
<i class="pi pi-calendar-plus"></i>
<i class="mdi mdi-calendar-plus"></i>
<span>选择时间段</span>
</button>
@@ -72,7 +72,7 @@
<header>
<strong>选择看板时间段</strong>
<button type="button" aria-label="关闭日期选择" @click="calendarOpen = false">
<i class="pi pi-times"></i>
<i class="mdi mdi-close"></i>
</button>
</header>
@@ -87,12 +87,6 @@
</label>
</div>
<div class="quick-presets" aria-label="快捷时间段">
<button type="button" @click="applyDraft('2024-07-12', '2024-07-12')">今日</button>
<button type="button" @click="applyDraft('2024-07-06', '2024-07-12')">本周</button>
<button type="button" @click="applyDraft('2024-07-01', '2024-07-31')">本月</button>
</div>
<footer>
<button class="ghost-btn" type="button" @click="calendarOpen = false">取消</button>
<button class="apply-btn" type="button" :disabled="!canApplyCustomRange" @click="applyCustomRange">
@@ -103,35 +97,30 @@
</div>
</div>
<template v-else-if="isRequests">
<button class="month-chip" type="button" aria-label="选择报销月份">
<i class="pi pi-calendar"></i>
<span>2024-07</span>
<i class="pi pi-angle-down"></i>
<template v-if="isPolicies || isRequests"></template>
<template v-else-if="isApproval">
<button class="month-chip" type="button" aria-label="筛选审批日期">
<i class="mdi mdi-calendar"></i>
<span>筛选 / 日期</span>
<i class="mdi mdi-chevron-down"></i>
</button>
<button class="icon-btn notification" type="button" aria-label="查看通知">
<i class="pi pi-bell"></i>
<span>3</span>
<i class="mdi mdi-bell-outline"></i>
<span>9</span>
</button>
<button class="profile-btn" type="button" aria-label="打开用户菜单">
<span class="profile-avatar"></span>
<span class="profile-copy">
<strong>张晓明</strong>
<small>财务管理</small>
<small>财务管理</small>
</span>
<i class="pi pi-angle-down"></i>
<i class="mdi mdi-chevron-down"></i>
</button>
</template>
<div v-else class="date-chip">
<i class="pi pi-calendar"></i>
<span>2024-07-06 ~ 2024-07-12</span>
</div>
<template v-else-if="isRequests"></template>
<button v-if="!isRequests" class="top-btn primary" type="button" @click="emit('openChat')">
<i class="pi pi-sparkles"></i>
<span>AI 助手</span>
</button>
</template>
</div>
</header>
@@ -164,6 +153,8 @@ const emit = defineEmits([
const isChat = computed(() => props.activeView === 'chat')
const isOverview = computed(() => props.activeView === 'overview')
const isRequests = computed(() => props.activeView === 'requests')
const isApproval = computed(() => props.activeView === 'approval')
const isPolicies = computed(() => props.activeView === 'policies')
const calendarOpen = ref(false)
const draftStart = ref(props.customRange.start)
const draftEnd = ref(props.customRange.end)
@@ -357,7 +348,7 @@ function formatRangeLabel(start, end) {
white-space: nowrap;
}
.range-meta .pi {
.range-meta .mdi {
color: #10b981;
}
@@ -514,28 +505,6 @@ function formatRangeLabel(start, end) {
outline: none;
}
.quick-presets {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.quick-presets button {
height: 34px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #f8fafc;
color: #334155;
font-size: 13px;
font-weight: 700;
}
.quick-presets button:hover {
border-color: rgba(16, 185, 129, .28);
background: #ecfdf5;
color: #0f9f78;
}
.ghost-btn,
.apply-btn {
height: 36px;
@@ -562,20 +531,6 @@ function formatRangeLabel(start, end) {
background: #cbd5e1;
}
.date-chip {
height: 40px;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 0 32px 0 12px;
border: 1px solid #cbd5e1;
border-radius: 6px;
background: #fff;
color: var(--text);
font-size: 14px;
font-weight: 400;
}
.month-chip {
height: 42px;
display: inline-flex;
@@ -590,7 +545,7 @@ function formatRangeLabel(start, end) {
font-weight: 750;
}
.month-chip .pi:first-child {
.month-chip .mdi:first-child {
color: #64748b;
}
@@ -599,32 +554,6 @@ function formatRangeLabel(start, end) {
color: #0f9f78;
}
.date-chip .pi {
color: #94a3b8;
}
.top-btn {
height: 40px;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 0 16px;
border: 0;
border-radius: 6px;
font-size: 14px;
font-weight: 650;
transition: background 160ms ease;
}
.top-btn.primary {
background: var(--primary);
color: #fff;
}
.top-btn.primary:hover {
background: #0ea672;
}
.icon-btn,
.profile-btn {
border: 0;
@@ -705,7 +634,7 @@ function formatRangeLabel(start, end) {
font-size: 12px;
}
.profile-btn .pi {
.profile-btn .mdi {
color: #64748b;
}

View File

@@ -18,6 +18,14 @@ export const navItems = [
title: '差旅申请/报销',
desc: '查看员工差旅报销单据、跟踪进度、发起新申请'
},
{
id: 'approval',
label: '审批中心',
navHint: '待审批单据与批量处理',
icon: icons.approval,
title: '审批中心',
desc: '统一处理待审批单据,聚焦效率、风险与 SLA'
},
{
id: 'chat',
label: 'AI助手',
@@ -28,11 +36,11 @@ export const navItems = [
},
{
id: 'policies',
label: '政策规则',
navHint: '制度与校验规则',
label: '知识管理',
navHint: '制度、文档与知识图谱',
icon: icons.file,
title: '政策规则中心',
desc: '维护差旅、招待、采购和发票校验规则。'
title: '财务知识管理中心',
desc: '上传制度文档、沉淀财务知识、构建知识图谱,面向员工问答与知识管理'
},
{
id: 'audit',

View File

@@ -3,6 +3,7 @@ const iconPath = (content) => `<svg viewBox="0 0 24 24" aria-hidden="true">${con
export const icons = {
dashboard: iconPath('<path d="M3 13h8V3H3z"/><path d="M13 21h8V11h-8z"/><path d="M13 3h8v6h-8z"/><path d="M3 21h8v-6H3z"/>'),
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"/>'),
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"/>'),
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"/>'),

View File

@@ -4,7 +4,7 @@ export const metricBlueprints = [
label: '待审批单据',
unit: '单',
accent: '#10b981',
icon: 'pi pi-file',
icon: 'mdi mdi-file-document-outline',
trend: 'down',
change: '12.5%',
delta: '较昨日 -18 单'
@@ -13,7 +13,7 @@ export const metricBlueprints = [
key: 'pendingAmount',
label: '待处理金额',
accent: '#3b82f6',
icon: 'pi pi-wallet',
icon: 'mdi mdi-wallet',
trend: 'up',
change: '8.3%',
delta: '较昨日 +¥27,400'
@@ -23,7 +23,7 @@ export const metricBlueprints = [
label: '平均审批时长',
unit: 'h',
accent: '#8b5cf6',
icon: 'pi pi-clock',
icon: 'mdi mdi-clock-outline',
trend: 'down',
change: '14.8%',
delta: '较昨日 -1.2h'
@@ -33,7 +33,7 @@ export const metricBlueprints = [
label: '自动审单通过率',
unit: '%',
accent: '#16a34a',
icon: 'pi pi-shield',
icon: 'mdi mdi-shield-outline',
trend: 'up',
change: '6.2%',
delta: '较昨日 +4.6%'
@@ -43,7 +43,7 @@ export const metricBlueprints = [
label: '异常预警单',
unit: '单',
accent: '#ef4444',
icon: 'pi pi-exclamation-triangle',
icon: 'mdi mdi-alert',
trend: 'up',
change: '16.7%',
delta: '较昨日 +2 单'
@@ -53,7 +53,7 @@ export const metricBlueprints = [
label: 'SLA 达成率',
unit: '%',
accent: '#10b981',
icon: 'pi pi-check-circle',
icon: 'mdi mdi-check-circle',
trend: 'up',
change: '3.1%',
delta: '较昨日 +2.9%'

View File

@@ -5,7 +5,7 @@
<header class="panel-head">
<h2>今日你可以这样问我</h2>
<button class="text-action" type="button" @click="rotatePrompts">
<i class="pi pi-refresh"></i>
<i class="mdi mdi-refresh"></i>
<span>换一换</span>
</button>
</header>
@@ -29,11 +29,11 @@
<p>今天我最应该关注哪些问题</p>
<time>09:41</time>
</div>
<span class="chat-avatar user-avatar"><i class="pi pi-user"></i></span>
<span class="chat-avatar user-avatar"><i class="mdi mdi-account-outline"></i></span>
</div>
<div class="message-row assistant">
<span class="chat-avatar assistant-avatar"><i class="pi pi-sparkles"></i></span>
<span class="chat-avatar assistant-avatar"><i class="mdi mdi-sparkles"></i></span>
<div class="message-bubble">
<p>基于当前数据你优先关注以下 3 </p>
<ol>
@@ -46,25 +46,25 @@
</div>
<div class="message-row assistant">
<span class="chat-avatar assistant-avatar"><i class="pi pi-sparkles"></i></span>
<span class="chat-avatar assistant-avatar"><i class="mdi mdi-sparkles"></i></span>
<div class="message-bubble">
<p>为你生成行动建议</p>
<ul class="action-list">
<li><i class="pi pi-exclamation-circle danger"></i><strong>紧急处理</strong>处理即将超时的 3 笔单据避免 SLA 逾期</li>
<li><i class="pi pi-info-circle warning"></i><strong>风险关注</strong>审核市场部高风险差旅报销重点关注差旅与超标费用</li>
<li><i class="pi pi-arrow-circle-up info"></i><strong>信息补齐</strong>提醒申请人补齐酒店入住清单加快审批进度</li>
<li><i class="pi pi-check-circle success"></i><strong>效率优化</strong>当前审批瓶颈在财务审批建议分配或优先处理</li>
<li><i class="mdi mdi-alert-circle danger"></i><strong>紧急处理</strong>处理即将超时的 3 笔单据避免 SLA 逾期</li>
<li><i class="mdi mdi-information-outline warning"></i><strong>风险关注</strong>审核市场部高风险差旅报销重点关注差旅与超标费用</li>
<li><i class="mdi mdi-arrow-up-circle info"></i><strong>信息补齐</strong>提醒申请人补齐酒店入住清单加快审批进度</li>
<li><i class="mdi mdi-check-circle success"></i><strong>效率优化</strong>当前审批瓶颈在财务审批建议分配或优先处理</li>
</ul>
<time>09:42</time>
</div>
</div>
<div v-for="message in messages" :key="message.id" class="message-row" :class="message.role === 'user' ? 'user' : 'assistant'">
<span v-if="message.role !== 'user'" class="chat-avatar assistant-avatar"><i class="pi pi-sparkles"></i></span>
<span v-if="message.role !== 'user'" class="chat-avatar assistant-avatar"><i class="mdi mdi-sparkles"></i></span>
<div class="message-bubble">
<p>{{ message.text }}</p>
</div>
<span v-if="message.role === 'user'" class="chat-avatar user-avatar"><i class="pi pi-user"></i></span>
<span v-if="message.role === 'user'" class="chat-avatar user-avatar"><i class="mdi mdi-account-outline"></i></span>
</div>
</div>
@@ -79,19 +79,19 @@
<div class="composer-actions">
<div class="input-tools">
<button type="button" aria-label="上传附件" @click="uploadInput?.click()">
<i class="pi pi-paperclip"></i>
<i class="mdi mdi-paperclip"></i>
</button>
<button type="button" aria-label="上传图片" @click="uploadInput?.click()">
<i class="pi pi-image"></i>
<i class="mdi mdi-image-outline"></i>
</button>
<button type="button" aria-label="语音输入">
<i class="pi pi-microphone"></i>
<i class="mdi mdi-microphone"></i>
</button>
<input ref="uploadInput" class="sr-only" type="file" multiple @change="emit('upload', $event)" />
</div>
<span class="counter">{{ draft.length }}/2000</span>
<button class="send-btn" type="button" aria-label="发送消息" @click="emit('send')">
<i class="pi pi-send"></i>
<i class="mdi mdi-send"></i>
</button>
</div>
</div>
@@ -115,13 +115,13 @@
<article class="insight-card panel">
<header>
<h3>B. 场景建议</h3>
<button type="button">查看全部 <i class="pi pi-angle-right"></i></button>
<button type="button">查看全部 <i class="mdi mdi-chevron-right"></i></button>
</header>
<div class="suggestion-list">
<button v-for="item in suggestions" :key="item.text" type="button" @click="applyPrompt(item.text)">
<i :class="item.icon"></i>
<span>{{ item.text }}</span>
<i class="pi pi-angle-right"></i>
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
</article>
@@ -142,7 +142,7 @@
<header>
<h3>D. 最近提问 / 常用问题</h3>
<button type="button" @click="rotatePrompts">
<i class="pi pi-refresh"></i>
<i class="mdi mdi-refresh"></i>
<span>换一换</span>
</button>
</header>
@@ -176,14 +176,14 @@ const uploadInput = ref(null)
const promptPage = ref(0)
const prompts = [
{ icon: 'pi pi-chart-line', text: '今天有哪些关键指标异常?' },
{ icon: 'pi pi-file-check', text: '哪些单据最需要优先处理?' },
{ icon: 'pi pi-shield', text: '高风险报销主要集中在哪些部门?' },
{ icon: 'pi pi-clock', text: '本周审批效率相比昨天如何?' },
{ icon: 'pi pi-lightbulb', text: '给我当前报销场景的处理建议' },
{ icon: 'pi pi-building', text: '生成一份运营简报' },
{ icon: 'pi pi-filter', text: '找出即将超时的单据' },
{ icon: 'pi pi-wallet', text: '分析本月预算执行压力' }
{ icon: 'mdi mdi-chart-line', text: '今天有哪些关键指标异常?' },
{ icon: 'mdi mdi-file-document-outline-check', text: '哪些单据最需要优先处理?' },
{ icon: 'mdi mdi-shield-outline', text: '高风险报销主要集中在哪些部门?' },
{ icon: 'mdi mdi-clock-outline', text: '本周审批效率相比昨天如何?' },
{ icon: 'mdi mdi-lightbulb-outline', text: '给我当前报销场景的处理建议' },
{ icon: 'mdi mdi-office-building', text: '生成一份运营简报' },
{ icon: 'mdi mdi-filter-outline', text: '找出即将超时的单据' },
{ icon: 'mdi mdi-wallet', text: '分析本月预算执行压力' }
]
const visiblePrompts = computed(() => {
@@ -192,23 +192,23 @@ const visiblePrompts = computed(() => {
})
const focusItems = [
{ icon: 'pi pi-star-fill', tone: 'danger', label: '3 单即将超时', value: '30 分钟内超时' },
{ icon: 'pi pi-exclamation-triangle', tone: 'warning', label: '市场部高风险占比最高', value: '高风险 2 单' },
{ icon: 'pi pi-arrow-right-arrow-left', tone: 'info', label: '重复报销风险 1 笔', value: '待核查' },
{ icon: 'pi pi-check-circle', tone: 'success', label: '1 笔缺失附件', value: '待补充' }
{ icon: 'mdi mdi-star', tone: 'danger', label: '3 单即将超时', value: '30 分钟内超时' },
{ icon: 'mdi mdi-alert', tone: 'warning', label: '市场部高风险占比最高', value: '高风险 2 单' },
{ icon: 'mdi mdi-swap-horizontal', tone: 'info', label: '重复报销风险 1 笔', value: '待核查' },
{ icon: 'mdi mdi-check-circle', tone: 'success', label: '1 笔缺失附件', value: '待补充' }
]
const suggestions = [
{ icon: 'pi pi-send', text: '优先处理差旅报销(占待审 62%' },
{ icon: 'pi pi-file-plus', text: '先补齐缺失附件再提交审批' },
{ icon: 'pi pi-car', text: '对超标出租车费用要求补充说明' }
{ icon: 'mdi mdi-send', text: '优先处理差旅报销(占待审 62%' },
{ icon: 'mdi mdi-file-document-outline-plus', text: '先补齐缺失附件再提交审批' },
{ icon: 'mdi mdi-car', text: '对超标出租车费用要求补充说明' }
]
const analysisActions = [
{ icon: 'pi pi-wave-pulse', text: '异常原因分析' },
{ icon: 'pi pi-building', text: '部门对比' },
{ icon: 'pi pi-shield', text: '风险趋势' },
{ icon: 'pi pi-filter', text: '审批瓶颈' }
{ icon: 'mdi mdi-waveform', text: '异常原因分析' },
{ icon: 'mdi mdi-office-building', text: '部门对比' },
{ icon: 'mdi mdi-shield-outline', text: '风险趋势' },
{ icon: 'mdi mdi-filter-outline', text: '审批瓶颈' }
]
const recentQuestions = [

View File

@@ -26,7 +26,7 @@
<div class="document-card">
<span>报销单</span>
<i></i><i></i><i></i>
<b class="doc-check"><i class="pi pi-check"></i></b>
<b class="doc-check"><i class="mdi mdi-check"></i></b>
</div>
<img class="shield-art" src="../assets/security-shield.png" alt="" />
@@ -35,7 +35,7 @@
<div class="metric-card risk">
<span>风险预警</span>
<strong><i class="pi pi-exclamation-triangle"></i> 14 </strong>
<strong><i class="mdi mdi-alert"></i> 14 </strong>
<small>较昨日 <b class="danger"> 16.7%</b></small>
</div>
@@ -76,13 +76,13 @@
<form class="login-form" @submit.prevent="emit('login', { username, password })">
<label class="field">
<span class="sr-only">账号</span>
<i class="pi pi-user"></i>
<i class="mdi mdi-account-outline"></i>
<input v-model="username" type="text" placeholder="请输入账号 / 邮箱 / 手机号" autocomplete="username" required />
</label>
<label class="field">
<span class="sr-only">密码</span>
<i class="pi pi-lock"></i>
<i class="mdi mdi-lock-outline"></i>
<input
v-model="password"
:type="showPassword ? 'text' : 'password'"
@@ -96,16 +96,16 @@
:aria-label="showPassword ? '隐藏密码' : '显示密码'"
@click="showPassword = !showPassword"
>
<i :class="showPassword ? 'pi pi-eye' : 'pi pi-eye-slash'"></i>
<i :class="showPassword ? 'mdi mdi-eye' : 'mdi mdi-eye-slash'"></i>
</button>
</label>
<label class="field">
<span class="sr-only">企业或租户</span>
<i class="pi pi-building"></i>
<i class="mdi mdi-office-building"></i>
<input v-model="tenant" type="text" placeholder="请输入企业 / 租户(选填)" />
<button class="field-icon-btn" type="button" aria-label="展开企业列表">
<i class="pi pi-angle-down"></i>
<i class="mdi mdi-chevron-down"></i>
</button>
</label>
@@ -122,13 +122,13 @@
<div class="divider"><span></span></div>
<button class="sso-btn" type="button" @click="emit('sso-login')">
<i class="pi pi-shield"></i>
<i class="mdi mdi-shield-outline"></i>
<span>SSO 单点登录</span>
</button>
</form>
<footer class="security-note">
<i class="pi pi-lock"></i>
<i class="mdi mdi-lock-outline"></i>
<span>安全登录 · 数据加密传输 · 如需帮助请联系管理员</span>
</footer>
</section>
@@ -147,9 +147,9 @@ const remember = ref(true)
const showPassword = ref(false)
const features = [
{ title: '智能审单', desc: 'AI 自动识别票据与规则,提升准确率与效率', icon: 'pi pi-file', tone: 'green' },
{ title: '异常预警', desc: '多维风险识别与预警,主动防控风险', icon: 'pi pi-bell', tone: 'red' },
{ title: 'SLA 监控', desc: '实时监控服务水平协议,保障审批及时性', icon: 'pi pi-sync', tone: 'blue' }
{ title: '智能审单', desc: 'AI 自动识别票据与规则,提升准确率与效率', icon: 'mdi mdi-file-document-outline', tone: 'green' },
{ title: '异常预警', desc: '多维风险识别与预警,主动防控风险', icon: 'mdi mdi-bell-outline', tone: 'red' },
{ title: 'SLA 监控', desc: '实时监控服务水平协议,保障审批及时性', icon: 'mdi mdi-sync', tone: 'blue' }
]
const LogoMark = {
@@ -593,7 +593,7 @@ const LogoMark = {
min-height: 52px;
}
.field > .pi {
.field > .mdi {
position: absolute;
left: 16px;
color: #64748b;
@@ -740,7 +740,7 @@ const LogoMark = {
font-size: 13px;
}
.security-note .pi {
.security-note .mdi {
color: #94a3b8;
}

View File

@@ -5,23 +5,19 @@
v-for="metric in kpiMetrics"
:key="metric.label"
class="kpi-card panel"
:style="{ '--accent': metric.accent }"
:style="{ '--accent': metric.accent, '--delay': `${metric.delay}ms` }"
>
<div class="kpi-top">
<div class="kpi-icon">
<i :class="metric.icon"></i>
</div>
<div>
<p>{{ metric.label }}</p>
<strong>{{ metric.displayValue }}</strong>
</div>
<div class="kpi-head">
<span class="kpi-icon"><i :class="metric.icon"></i></span>
<span class="kpi-label">{{ metric.label }}</span>
</div>
<div class="kpi-bottom" :class="metric.trend">
<span>
<i :class="metric.trend === 'down' ? 'pi pi-arrow-down' : 'pi pi-arrow-up'"></i>
<strong class="kpi-value">{{ metric.displayValue }}</strong>
<div class="kpi-trend">
<span class="kpi-badge" :class="metric.trend">
<i :class="metric.trend === 'down' ? 'mdi mdi-arrow-down' : 'mdi mdi-arrow-up'"></i>
{{ metric.changeText }}
</span>
<small>{{ metric.delta }}</small>
<span class="kpi-delta">{{ metric.delta }}</span>
</div>
</article>
</div>
@@ -29,7 +25,7 @@
<div class="content-grid top-grid">
<article class="panel dashboard-card trend-panel">
<div class="card-head">
<h3>报销申请与审批趋势 <i class="pi pi-info-circle"></i></h3>
<h3>报销申请与审批趋势 <i class="mdi mdi-information-outline"></i></h3>
<select v-model="activeTrendRange" class="card-select" aria-label="趋势时间范围">
<option v-for="range in trendRanges" :key="range">{{ range }}</option>
</select>
@@ -45,7 +41,7 @@
<article class="panel dashboard-card donut-panel">
<div class="card-head">
<h3>费用结构 <i class="pi pi-info-circle"></i></h3>
<h3>费用结构 <i class="mdi mdi-information-outline"></i></h3>
</div>
<DonutChart :items="spendLegend" center-value="¥361.6K" center-label="待处理金额" />
<p class="panel-note">* 百分比为占待处理金额比例</p>
@@ -53,7 +49,7 @@
<article class="panel dashboard-card donut-panel">
<div class="card-head">
<h3>风险异常分布 <i class="pi pi-info-circle"></i></h3>
<h3>风险异常分布 <i class="mdi mdi-information-outline"></i></h3>
</div>
<DonutChart :items="riskLegend" :center-value="`${riskTotal}`" center-label="异常预警单" />
<p class="panel-note">* 近30天数据</p>
@@ -63,7 +59,7 @@
<div class="content-grid bottom-grid">
<article class="panel dashboard-card rank-panel">
<div class="card-head">
<h3>部门报销排行待处理金额 <i class="pi pi-info-circle"></i></h3>
<h3>部门报销排行待处理金额 <i class="mdi mdi-information-outline"></i></h3>
<select v-model="activeDepartmentRange" class="card-select" aria-label="部门排行时间范围">
<option v-for="range in departmentRangeOptions" :key="range">{{ range }}</option>
</select>
@@ -74,11 +70,16 @@
<article class="panel dashboard-card bottleneck-panel">
<div class="card-head">
<h3>审批瓶颈平均处理时长 <i class="pi pi-info-circle"></i></h3>
<h3>审批瓶颈平均处理时长 <i class="mdi mdi-information-outline"></i></h3>
</div>
<div class="bottleneck-list">
<div v-for="item in bottlenecks" :key="item.name" class="bottleneck-row">
<div
v-for="(item, index) in bottlenecks"
:key="item.name"
class="bottleneck-row"
:style="{ '--delay': `${index * 70}ms` }"
>
<div class="reviewer">
<div class="reviewer-avatar">{{ item.avatar }}</div>
<div>
@@ -93,12 +94,12 @@
</div>
</div>
<button type="button" class="text-link">查看全部 <i class="pi pi-angle-right"></i></button>
<button type="button" class="text-link">查看全部 <i class="mdi mdi-chevron-right"></i></button>
</article>
<article class="panel dashboard-card budget-panel">
<div class="card-head">
<h3>预算执行率本月 <i class="pi pi-info-circle"></i></h3>
<h3>预算执行率本月 <i class="mdi mdi-information-outline"></i></h3>
</div>
<GaugeChart
@@ -108,7 +109,7 @@
:left="budgetSummary.left"
/>
<button type="button" class="text-link">查看详情 <i class="pi pi-angle-right"></i></button>
<button type="button" class="text-link">查看详情 <i class="mdi mdi-chevron-right"></i></button>
</article>
</div>
</section>
@@ -163,18 +164,23 @@ const formatCompact = (value) => {
const formatCurrency = (value) => formatCompact(value)
const kpiMetrics = computed(() => metricBlueprints.map((metric) => {
const formatMetricValue = (metric, value) => {
if (metric.key === 'pendingAmount') return formatCurrency(Math.round(value))
if (metric.key === 'avgSla') return `${value.toFixed(1)} ${metric.unit}`
if (metric.unit === '%') return `${Math.round(value)} ${metric.unit}`
if (metric.unit) return `${Math.round(value)} ${metric.unit}`
return `${Math.round(value)}`
}
const kpiMetrics = computed(() => metricBlueprints.map((metric, index) => {
const rawValue = demoTotals[metric.key]
const displayValue = metric.key === 'pendingAmount'
? formatCurrency(rawValue)
: metric.unit && !String(rawValue).endsWith(metric.unit)
? `${rawValue} ${metric.unit}`
: `${rawValue}`
const displayValue = formatMetricValue(metric, rawValue)
return {
...metric,
displayValue,
changeText: metric.change
changeText: metric.change,
delay: index * 55
}
}))
@@ -222,86 +228,104 @@ const rankedDepartments = computed(() => {
}
.kpi-card {
padding: 20px;
position: relative;
padding: 20px 20px 16px;
display: flex;
flex-direction: column;
gap: 0;
border-left: 3px solid var(--accent);
animation: dashboardItemIn 520ms var(--ease) both;
animation-delay: var(--delay, 0ms);
transition: box-shadow 200ms ease, transform 200ms ease;
}
.kpi-top {
flex: 1;
.kpi-card:hover {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
transform: translateY(-1px);
}
.kpi-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.kpi-top > div {
min-width: 0;
overflow: hidden;
.kpi-icon {
width: 36px;
height: 36px;
display: grid;
place-items: center;
border-radius: 8px;
background: color-mix(in srgb, var(--accent) 10%, white);
color: var(--accent);
font-size: 18px;
flex: 0 0 auto;
animation: iconPop 560ms var(--ease) both;
animation-delay: calc(var(--delay, 0ms) + 100ms);
}
.kpi-top p {
margin: 0 0 6px;
.kpi-label {
color: #64748b;
font-size: 13px;
font-weight: 500;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.kpi-top strong {
.kpi-value {
display: block;
color: #1e293b;
font-size: clamp(18px, 1.6vw, 22px);
line-height: 1.2;
font-weight: 700;
height: 32px;
line-height: 32px;
color: #0f172a;
font-size: clamp(20px, 1.6vw, 26px);
line-height: 1;
font-weight: 800;
font-variant-numeric: tabular-nums;
white-space: nowrap;
margin-bottom: 16px;
letter-spacing: 0;
}
.kpi-icon {
width: 44px;
height: 44px;
display: grid;
place-items: center;
border-radius: 10px;
background: color-mix(in srgb, var(--accent) 10%, white);
color: var(--accent);
font-size: 20px;
flex: 0 0 auto;
}
.kpi-bottom {
.kpi-trend {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #f1f5f9;
}
.kpi-bottom span {
.kpi-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 13px;
font-weight: 600;
gap: 3px;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 700;
line-height: 1.6;
}
.kpi-bottom.up span {
.kpi-badge.up {
background: rgba(239, 68, 68, 0.08);
color: #dc2626;
}
.kpi-bottom.down span {
.kpi-badge.down {
background: rgba(22, 163, 74, 0.08);
color: #16a34a;
}
.kpi-bottom small {
.kpi-badge .mdi {
font-size: 13px;
}
.kpi-delta {
color: #94a3b8;
font-size: 12px;
text-align: right;
white-space: nowrap;
}
.content-grid {
@@ -313,8 +337,16 @@ const rankedDepartments = computed(() => {
.dashboard-card {
padding: 20px;
transition: box-shadow 200ms ease, transform 200ms ease;
animation: dashboardItemIn 560ms var(--ease) both;
}
.top-grid .dashboard-card:nth-child(1) { animation-delay: 80ms; }
.top-grid .dashboard-card:nth-child(2) { animation-delay: 150ms; }
.top-grid .dashboard-card:nth-child(3) { animation-delay: 220ms; }
.bottom-grid .dashboard-card:nth-child(1) { animation-delay: 290ms; }
.bottom-grid .dashboard-card:nth-child(2) { animation-delay: 360ms; }
.bottom-grid .dashboard-card:nth-child(3) { animation-delay: 430ms; }
.dashboard-card:hover {
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
transform: translateY(-1px);
@@ -346,7 +378,7 @@ const rankedDepartments = computed(() => {
line-height: 1.35;
}
.card-head .pi {
.card-head .mdi {
color: #94a3b8;
font-size: 12px;
vertical-align: 1px;
@@ -393,6 +425,8 @@ const rankedDepartments = computed(() => {
align-items: center;
justify-content: space-between;
gap: 12px;
animation: listRowIn 460ms var(--ease) both;
animation-delay: var(--delay, 0ms);
}
.reviewer {
@@ -472,6 +506,51 @@ const rankedDepartments = computed(() => {
font-size: 14px;
}
@keyframes dashboardItemIn {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes listRowIn {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes iconPop {
0% {
opacity: 0;
transform: scale(.82);
}
70% {
opacity: 1;
transform: scale(1.04);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
.kpi-card,
.dashboard-card,
.bottleneck-row {
animation: none;
}
}
@media (max-width: 1320px) {
.kpi-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));

View File

@@ -6,7 +6,7 @@
<article v-for="item in metrics" :key="item.label" class="metric-card" :style="{ '--accent': item.accent }">
<span class="metric-icon"><i :class="item.icon"></i></span>
<div>
<p>{{ item.label }} <i v-if="item.help" class="pi pi-question-circle"></i></p>
<p>{{ item.label }} <i v-if="item.help" class="mdi mdi-help-circle-outline"></i></p>
<strong>{{ item.value }}</strong>
<small>{{ item.meta }}</small>
</div>
@@ -21,9 +21,9 @@
<div class="library-body">
<aside class="folder-rail">
<label class="folder-search">
<i class="pi pi-search"></i>
<i class="mdi mdi-magnify"></i>
<input type="search" placeholder="搜索文件夹" />
<button type="button" aria-label="新增文件夹"><i class="pi pi-plus"></i></button>
<button type="button" aria-label="新增文件夹"><i class="mdi mdi-plus"></i></button>
</label>
<nav class="folder-tree" aria-label="知识库文件夹">
@@ -35,14 +35,14 @@
</nav>
<button class="new-folder-btn" type="button">
<i class="pi pi-plus"></i>
<i class="mdi mdi-plus"></i>
<span>新建文件夹</span>
</button>
</aside>
<section class="document-area">
<div class="upload-zone">
<i class="pi pi-cloud-upload"></i>
<i class="mdi mdi-cloud-upload"></i>
<strong>拖拽文档到此处或点击上传</strong>
<span>支持 PDF / Word / Excel / PPT 文档单个文件不超过 100MB</span>
</div>
@@ -53,7 +53,7 @@
<tr>
<th>文件名称</th>
<th>标签</th>
<th>上传时间 <i class="pi pi-arrow-down"></i></th>
<th>上传时间 <i class="mdi mdi-arrow-down"></i></th>
<th>版本</th>
<th>状态</th>
<th>上传人</th>
@@ -75,7 +75,7 @@
<td>{{ doc.version }}</td>
<td><span class="state-tag" :class="doc.stateTone">{{ doc.state }}</span></td>
<td>{{ doc.owner }}</td>
<td><button class="more-btn" type="button" aria-label="更多操作"><i class="pi pi-ellipsis-h"></i></button></td>
<td><button class="more-btn" type="button" aria-label="更多操作"><i class="mdi mdi-dots-horizontal"></i></button></td>
</tr>
</tbody>
</table>
@@ -83,12 +83,12 @@
<footer class="list-foot">
<span> 18 </span>
<button type="button">10/ <i class="pi pi-angle-down"></i></button>
<button type="button">10/ <i class="mdi mdi-chevron-down"></i></button>
<div class="pager" aria-label="分页">
<button type="button" aria-label="上一页"><i class="pi pi-angle-left"></i></button>
<button type="button" aria-label="上一页"><i class="mdi mdi-chevron-left"></i></button>
<button class="active" type="button" aria-current="page">1</button>
<button type="button">2</button>
<button type="button" aria-label="下一页"><i class="pi pi-angle-right"></i></button>
<button type="button" aria-label="下一页"><i class="mdi mdi-chevron-right"></i></button>
</div>
<label>前往 <input value="1" aria-label="页码" /> </label>
</footer>
@@ -100,7 +100,7 @@
<aside class="analytics-column">
<article class="ops-card panel">
<header class="card-head">
<h2>知识运营概览 <i class="pi pi-question-circle"></i></h2>
<h2>知识运营概览 <i class="mdi mdi-help-circle-outline"></i></h2>
</header>
<div class="ops-grid">
<div v-for="item in opsMetrics" :key="item.label" class="ops-item" :style="{ '--accent': item.accent }">
@@ -116,8 +116,8 @@
<article class="top-card panel">
<header class="card-head">
<h2>热门问题 TOP5 <i class="pi pi-question-circle"></i></h2>
<button type="button">更多 <i class="pi pi-angle-right"></i></button>
<h2>热门问题 TOP5 <i class="mdi mdi-help-circle-outline"></i></h2>
<button type="button">更多 <i class="mdi mdi-chevron-right"></i></button>
</header>
<ol class="hot-list">
<li v-for="item in hotQuestions" :key="item.title">
@@ -130,28 +130,28 @@
<article class="feedback-card panel">
<header class="card-head">
<h2>用户点赞最多 <i class="pi pi-question-circle"></i></h2>
<button type="button">更多 <i class="pi pi-angle-right"></i></button>
<h2>用户点赞最多 <i class="mdi mdi-help-circle-outline"></i></h2>
<button type="button">更多 <i class="mdi mdi-chevron-right"></i></button>
</header>
<ul class="feedback-list positive">
<li v-for="(item, idx) in likedAnswers" :key="item.title">
<span class="rank-badge" :class="idx === 0 ? 'hot' : idx < 3 ? 'warm' : 'normal'">{{ idx + 1 }}</span>
<span>{{ item.title }}</span>
<b><i class="pi pi-thumbs-up-fill"></i>{{ item.count }}</b>
<b><i class="mdi mdi-thumb-up"></i>{{ item.count }}</b>
</li>
</ul>
</article>
<article class="feedback-card panel">
<header class="card-head">
<h2>用户点踩较多 / 待优化 <i class="pi pi-question-circle"></i></h2>
<button type="button">更多 <i class="pi pi-angle-right"></i></button>
<h2>用户点踩较多 / 待优化 <i class="mdi mdi-help-circle-outline"></i></h2>
<button type="button">更多 <i class="mdi mdi-chevron-right"></i></button>
</header>
<ul class="feedback-list negative">
<li v-for="(item, idx) in dislikedAnswers" :key="item.title">
<span class="rank-badge" :class="idx === 0 ? 'hot' : idx < 3 ? 'warm' : 'normal'">{{ idx + 1 }}</span>
<span>{{ item.title }}</span>
<b><i class="pi pi-thumbs-down-fill"></i>{{ item.count }}</b>
<b><i class="mdi mdi-thumb-down"></i>{{ item.count }}</b>
<em>待优化</em>
</li>
</ul>
@@ -159,8 +159,8 @@
<article class="trend-card panel">
<header class="card-head">
<h2>近7天提问趋势 <i class="pi pi-question-circle"></i></h2>
<button type="button">更多 <i class="pi pi-angle-right"></i></button>
<h2>近7天提问趋势 <i class="mdi mdi-help-circle-outline"></i></h2>
<button type="button">更多 <i class="mdi mdi-chevron-right"></i></button>
</header>
<svg class="trend-chart" viewBox="0 0 420 190" role="img" aria-label="近7天提问趋势">
<line v-for="tick in chartTicks" :key="tick" x1="38" :y1="tick.y" x2="398" :y2="tick.y" />
@@ -173,8 +173,8 @@
<article class="recent-card panel">
<header class="card-head">
<h2>最近更新知识 <i class="pi pi-question-circle"></i></h2>
<button type="button">更多 <i class="pi pi-angle-right"></i></button>
<h2>最近更新知识 <i class="mdi mdi-help-circle-outline"></i></h2>
<button type="button">更多 <i class="mdi mdi-chevron-right"></i></button>
</header>
<ul class="recent-list">
<li v-for="item in recentKnowledge" :key="item.name">
@@ -193,40 +193,40 @@
import { computed } from 'vue'
const metrics = [
{ label: '文档总数', value: '1,248', meta: '较上周 +68', icon: 'pi pi-file', accent: '#10b981', help: true },
{ label: '文件夹总数', value: '36', meta: '较上周 +2', icon: 'pi pi-folder', accent: '#3b82f6' },
{ label: '问答总量', value: '8,562', meta: '较上周 +321', icon: 'pi pi-comments', accent: '#8b5cf6' },
{ label: '知识命中率', value: '87.3%', meta: '较上周 +1.2%', icon: 'pi pi-bullseye', accent: '#f59e0b' }
{ label: '文档总数', value: '1,248', meta: '较上周 +68', icon: 'mdi mdi-file-document-outline', accent: '#10b981', help: true },
{ label: '文件夹总数', value: '36', meta: '较上周 +2', icon: 'mdi mdi-folder', accent: '#3b82f6' },
{ label: '问答总量', value: '8,562', meta: '较上周 +321', icon: 'mdi mdi-comment-text-multiple-outline', accent: '#8b5cf6' },
{ label: '知识命中率', value: '87.3%', meta: '较上周 +1.2%', icon: 'mdi mdi-bullseye-arrow', accent: '#f59e0b' }
]
const folders = [
{ name: '财务知识库', count: 36, icon: 'pi pi-folder', active: false },
{ name: '制度政策', count: 8, icon: 'pi pi-folder', active: false },
{ name: '报销制度', count: 12, icon: 'pi pi-folder-open', active: false },
{ name: '差旅规范', count: 18, icon: 'pi pi-folder', active: true },
{ name: '发票管理', count: 14, icon: 'pi pi-folder', active: false },
{ name: '税务合规', count: 16, icon: 'pi pi-folder', active: false },
{ name: '预算管理', count: 9, icon: 'pi pi-folder', active: false },
{ name: '财务共享', count: 7, icon: 'pi pi-folder', active: false },
{ name: '培训资料', count: 6, icon: 'pi pi-folder', active: false },
{ name: '常见问答', count: 11, icon: 'pi pi-folder', active: false }
{ name: '财务知识库', count: 36, icon: 'mdi mdi-folder', active: false },
{ name: '制度政策', count: 8, icon: 'mdi mdi-folder', active: false },
{ name: '报销制度', count: 12, icon: 'mdi mdi-folder-open', active: false },
{ name: '差旅规范', count: 18, icon: 'mdi mdi-folder', active: true },
{ name: '发票管理', count: 14, icon: 'mdi mdi-folder', active: false },
{ name: '税务合规', count: 16, icon: 'mdi mdi-folder', active: false },
{ name: '预算管理', count: 9, icon: 'mdi mdi-folder', active: false },
{ name: '财务共享', count: 7, icon: 'mdi mdi-folder', active: false },
{ name: '培训资料', count: 6, icon: 'mdi mdi-folder', active: false },
{ name: '常见问答', count: 11, icon: 'mdi mdi-folder', active: false }
]
const documents = [
{ name: '差旅报销管理办法2024版', tag: '差旅 / 制度', time: '2024-05-12 14:35', version: 'v3.2', state: '已生效', stateTone: 'success', owner: '张明', icon: 'pi pi-file-pdf pdf' },
{ name: '发票查验规范及操作指引', tag: '发票 / 操作', time: '2024-05-10 10:22', version: 'v1.5', state: '已生效', stateTone: 'success', owner: '李娜', icon: 'pi pi-file-word word' },
{ name: '费用报销标准细则2024', tag: '报销 / 标准', time: '2024-05-08 09:16', version: 'v2.1', state: '已生效', stateTone: 'success', owner: '王磊', icon: 'pi pi-file-pdf pdf' },
{ name: '差旅费用标准对照表(国内)', tag: '差旅 / 标准', time: '2024-05-05 08:20', version: 'v1.3', state: '审批中', stateTone: 'warning', owner: '陈杰', icon: 'pi pi-file-excel excel' },
{ name: '借款管理办法及流程', tag: '借款 / 流程', time: '2024-05-03 11:05', version: 'v1.0', state: '已生效', stateTone: 'success', owner: '刘洋', icon: 'pi pi-file-pdf pdf' }
{ name: '差旅报销管理办法2024版', tag: '差旅 / 制度', time: '2024-05-12 14:35', version: 'v3.2', state: '已生效', stateTone: 'success', owner: '张明', icon: 'mdi mdi-file-document-outline-pdf pdf' },
{ name: '发票查验规范及操作指引', tag: '发票 / 操作', time: '2024-05-10 10:22', version: 'v1.5', state: '已生效', stateTone: 'success', owner: '李娜', icon: 'mdi mdi-file-document-outline-word word' },
{ name: '费用报销标准细则2024', tag: '报销 / 标准', time: '2024-05-08 09:16', version: 'v2.1', state: '已生效', stateTone: 'success', owner: '王磊', icon: 'mdi mdi-file-document-outline-pdf pdf' },
{ name: '差旅费用标准对照表(国内)', tag: '差旅 / 标准', time: '2024-05-05 08:20', version: 'v1.3', state: '审批中', stateTone: 'warning', owner: '陈杰', icon: 'mdi mdi-file-document-outline-excel excel' },
{ name: '借款管理办法及流程', tag: '借款 / 流程', time: '2024-05-03 11:05', version: 'v1.0', state: '已生效', stateTone: 'success', owner: '刘洋', icon: 'mdi mdi-file-document-outline-pdf pdf' }
]
const opsMetrics = [
{ label: '本周新增问题', value: '328', meta: '较上周 +46', icon: 'pi pi-question-circle', accent: '#3b82f6' },
{ label: '已解决问题', value: '286', meta: '解决率 87.2%', icon: 'pi pi-check', accent: '#10b981' },
{ label: '平均响应时长', value: '2.4h', meta: '较上周 -0.6h', icon: 'pi pi-clock', accent: '#3b82f6' },
{ label: '用户满意度', value: '91.2%', meta: '较上周 +2.1%', icon: 'pi pi-face-smile', accent: '#10b981' },
{ label: '收藏次数', value: '1,236', meta: '较上周 +128', icon: 'pi pi-star', accent: '#f59e0b' },
{ label: '点赞总数', value: '3,582', meta: '较上周 +312', icon: 'pi pi-thumbs-up', accent: '#f59e0b' }
{ label: '本周新增问题', value: '328', meta: '较上周 +46', icon: 'mdi mdi-help-circle-outline', accent: '#3b82f6' },
{ label: '已解决问题', value: '286', meta: '解决率 87.2%', icon: 'mdi mdi-check', accent: '#10b981' },
{ label: '平均响应时长', value: '2.4h', meta: '较上周 -0.6h', icon: 'mdi mdi-clock-outline', accent: '#3b82f6' },
{ label: '用户满意度', value: '91.2%', meta: '较上周 +2.1%', icon: 'mdi mdi-emoticon-happy-outline', accent: '#10b981' },
{ label: '收藏次数', value: '1,236', meta: '较上周 +128', icon: 'mdi mdi-star-outline', accent: '#f59e0b' },
{ label: '点赞总数', value: '3,582', meta: '较上周 +312', icon: 'mdi mdi-thumb-up-outline', accent: '#f59e0b' }
]
const hotQuestions = [
@@ -277,9 +277,9 @@ const chartTicks = [
const trendPoints = computed(() => trendData.map((point) => `${point.x},${point.y}`).join(' '))
const recentKnowledge = [
{ name: '差旅报销管理办法2024版v3.2', time: '05-12 14:35', icon: 'pi pi-file-pdf pdf' },
{ name: '发票查验规范及操作指引 v1.5', time: '05-10 10:22', icon: 'pi pi-file-word word' },
{ name: '费用报销标准细则2024v2.1', time: '05-08 09:16', icon: 'pi pi-file-pdf pdf' }
{ name: '差旅报销管理办法2024版v3.2', time: '05-12 14:35', icon: 'mdi mdi-file-document-outline-pdf pdf' },
{ name: '发票查验规范及操作指引 v1.5', time: '05-10 10:22', icon: 'mdi mdi-file-document-outline-word word' },
{ name: '费用报销标准细则2024v2.1', time: '05-08 09:16', icon: 'mdi mdi-file-document-outline-pdf pdf' }
]
</script>

View File

@@ -13,8 +13,8 @@
<article class="travel-list panel">
<header class="list-head">
<h2>我的差旅报销单</h2>
</header>
<h2>我的差旅报销单</h2>
</header>
<nav class="status-tabs" aria-label="差旅报销状态">
<button
@@ -30,34 +30,79 @@
<div class="list-toolbar">
<div class="filter-set">
<div class="list-search">
<i class="mdi mdi-magnify"></i>
<input type="search" placeholder="搜索申请人、单号、费用类型..." />
</div>
<div class="date-range-filter" :class="{ open: datePopover }">
<button class="filter-btn date-range-trigger" type="button" @click="datePopover = !datePopover">
<span class="date-range-label">{{ dateRangeLabel }}</span>
<i class="mdi mdi-calendar"></i>
</button>
<div v-if="datePopover" class="date-range-popover" role="dialog" aria-label="选择时间段">
<header>
<strong>选择时间段</strong>
<button type="button" aria-label="关闭" @click="datePopover = false"><i class="mdi mdi-close"></i></button>
</header>
<div class="date-range-fields">
<label>
<span>开始日期</span>
<input v-model="rangeStart" type="date" />
</label>
<label>
<span>结束日期</span>
<input v-model="rangeEnd" type="date" />
</label>
</div>
<footer>
<button class="ghost-btn" type="button" @click="datePopover = false">取消</button>
<button class="apply-btn" type="button" :disabled="!rangeStart || !rangeEnd" @click="applyDateRange">应用</button>
</footer>
</div>
</div>
<button v-for="filter in filters" :key="filter" type="button" class="filter-btn">
<span>{{ filter }}</span>
<i :class="filter === '出差月份' ? 'pi pi-calendar' : 'pi pi-angle-down'"></i>
<i class="mdi mdi-chevron-down"></i>
</button>
</div>
<div class="toolbar-actions">
<button class="export-btn" type="button">
<i class="pi pi-upload"></i>
<i class="mdi mdi-upload"></i>
<span>导出</span>
</button>
<button class="create-btn" type="button" @click="emit('createRequest')">
<i class="pi pi-plus"></i>
<i class="mdi mdi-plus"></i>
<span>发起报销</span>
</button>
</div>
</div>
<p class="hint"><i class="pi pi-info-circle"></i> 点击任意行可查看单据详情</p>
<p class="hint"><i class="mdi mdi-information-outline"></i> 点击任意行可查看单据详情</p>
<div class="table-wrap">
<table>
<colgroup>
<col class="col-id">
<col class="col-reason">
<col class="col-city">
<col class="col-period">
<col class="col-apply">
<col class="col-amount">
<col class="col-node">
<col class="col-approval">
<col class="col-travel">
<col class="col-actions">
</colgroup>
<thead>
<tr>
<th>单号</th>
<th>出差事由</th>
<th>出差城市</th>
<th>出差时间</th>
<th>申请时间</th>
<th>申请金额</th>
<th>当前节点</th>
<th>审批状态</th>
@@ -71,6 +116,7 @@
<td>{{ row.reason }}</td>
<td>{{ row.city }}</td>
<td>{{ row.period }}</td>
<td>{{ row.applyTime }}</td>
<td>{{ row.amount }}</td>
<td>{{ row.node }}</td>
<td><span class="status-tag" :class="row.approvalTone">{{ row.approval }}</span></td>
@@ -79,7 +125,7 @@
<div class="row-actions">
<button type="button" @click.stop="emit('ask', row)">查看</button>
<button type="button" aria-label="更多操作" @click.stop>
<i class="pi pi-ellipsis-h"></i>
<i class="mdi mdi-dots-horizontal"></i>
</button>
</div>
</td>
@@ -89,22 +135,20 @@
</div>
<footer class="list-foot">
<span class="page-summary"> 26 目前第 1 </span>
<span class="page-summary"> {{ totalCount }} 目前第 {{ currentPage }} </span>
<div class="pager" aria-label="分页">
<button class="page-nav" type="button" aria-label="上一页"><i class="pi pi-angle-left"></i></button>
<button class="page-number active" type="button" aria-current="page">1</button>
<button class="page-number" type="button">2</button>
<button class="page-number" type="button">3</button>
<button class="page-nav" type="button" aria-label="下一页"><i class="pi pi-angle-right"></i></button>
<button class="page-nav" type="button" :disabled="currentPage === 1" aria-label="上一页" @click="currentPage--"><i class="mdi mdi-chevron-left"></i></button>
<button v-for="p in totalPages" :key="p" class="page-number" :class="{ active: currentPage === p }" type="button" :aria-current="currentPage === p ? 'page' : undefined" @click="currentPage = p">{{ p }}</button>
<button class="page-nav" type="button" :disabled="currentPage === totalPages" aria-label="下一页" @click="currentPage++"><i class="mdi mdi-chevron-right"></i></button>
</div>
<button class="page-size" type="button">10 / <i class="pi pi-angle-down"></i></button>
<button class="page-size" type="button">{{ pageSize }} / <i class="mdi mdi-chevron-down"></i></button>
</footer>
</article>
</section>
</template>
<script setup>
import { computed, ref } from 'vue'
import { computed, ref, watch } from 'vue'
defineProps({
filteredRequests: { type: Array, required: true }
@@ -114,30 +158,83 @@ const emit = defineEmits(['ask', 'approve', 'reject', 'createRequest'])
const activeTab = ref('全部')
const tabs = ['全部', '待提交', '审批中', '待出行', '已完成']
const filters = ['出差月份', '报销状态', '出差城市', '费用类型']
const filters = ['报销状态', '出差城市', '费用类型']
const datePopover = ref(false)
const rangeStart = ref('')
const rangeEnd = ref('')
const appliedStart = ref('')
const appliedEnd = ref('')
const dateRangeLabel = computed(() => {
if (appliedStart.value && appliedEnd.value) return `${appliedStart.value} ~ ${appliedEnd.value}`
return '选择时间段'
})
function applyDateRange() {
if (!rangeStart.value || !rangeEnd.value) return
appliedStart.value = rangeStart.value
appliedEnd.value = rangeEnd.value
datePopover.value = false
}
const kpis = [
{ label: '全部单据', value: 26, delta: '+8', trend: 'up good', arrow: 'pi pi-arrow-up', icon: 'pi pi-clipboard', accent: '#10b981' },
{ label: '待提交', value: 4, delta: '-1', trend: 'down good', arrow: 'pi pi-arrow-down', icon: 'pi pi-send', accent: '#f59e0b' },
{ label: '审批中', value: 7, delta: '+2', trend: 'up bad', arrow: 'pi pi-arrow-up', icon: 'pi pi-clock', accent: '#3b82f6' },
{ label: '已完成', value: 15, delta: '+7', trend: 'up good', arrow: 'pi pi-arrow-up', icon: 'pi pi-check', accent: '#10b981' }
{ label: '全部单据', value: 30, delta: '+8', trend: 'up good', arrow: 'mdi mdi-arrow-up', icon: 'mdi mdi-clipboard-text-outline', accent: '#10b981' },
{ label: '待提交', value: 5, delta: '-1', trend: 'down good', arrow: 'mdi mdi-arrow-down', icon: 'mdi mdi-send', accent: '#f59e0b' },
{ label: '审批中', value: 8, delta: '+2', trend: 'up bad', arrow: 'mdi mdi-arrow-up', icon: 'mdi mdi-clock-outline', accent: '#3b82f6' },
{ label: '已完成', value: 17, delta: '+7', trend: 'up good', arrow: 'mdi mdi-arrow-up', icon: 'mdi mdi-check', accent: '#10b981' }
]
const rows = [
{ id: 'BR240712001', reason: '华东区域客户拜访', city: '上海、苏州、杭州', period: '07-08~07-11 (4天)', amount: '¥3,680.00', node: '部门负责人审批', approval: '审批中', approvalTone: 'info', travel: '订酒店/机票', travelTone: 'success' },
{ id: 'BR240711008', reason: '产品培训与交流', city: '深圳', period: '07-10~07-12 (3天)', amount: '¥2,150.00', node: '待提交', approval: '待提交', approvalTone: 'info', travel: '待订机票', travelTone: 'warning' },
{ id: 'BR240709005', reason: '客户方案汇报', city: '北京', period: '07-06~07-08 (3天)', amount: '¥1,980.00', node: '财务审核', approval: '审批中', approvalTone: 'info', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240708012', reason: '供应商现场考察', city: '广州', period: '07-04~07-05 (2天)', amount: '¥860.00', node: '部门负责人审批', approval: '审批中', approvalTone: 'info', travel: '无需预订', travelTone: 'neutral' },
{ id: 'BR240707003', reason: '项目启动会', city: '成都', period: '07-01~07-03 (3天)', amount: '¥2,420.00', node: '财务审核', approval: '审批中', approvalTone: 'info', travel: '待预订酒店', travelTone: 'warning' },
{ id: 'BR240706009', reason: '客户拜访与市场调研', city: '南京、合肥', period: '06-28~06-30 (3天)', amount: '¥1,750.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240705007', reason: '技术交流会', city: '武汉', period: '06-25~06-26 (2天)', amount: '¥1,120.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '无需预订', travelTone: 'neutral' },
{ id: 'BR240704004', reason: '渠道合作洽谈', city: '西安', period: '06-20~06-21 (2天)', amount: '¥780.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '已订酒店/机票', travelTone: 'success' }
{ id: 'BR240715001', reason: '华东区域客户拜访', city: '上海、苏州、杭州', period: '07-14~07-17 (4天)', applyTime: '2024-07-13', amount: '¥4,280.00', node: '部门负责人审批', approval: '审批中', approvalTone: 'info', travel: '待预订酒店', travelTone: 'warning' },
{ id: 'BR240714010', reason: '年度战略合作伙伴会议', city: '北京', period: '07-15~07-16 (2天)', applyTime: '2024-07-12', amount: '¥1,860.00', node: '待提交', approval: '待提交', approvalTone: 'info', travel: '待订机票', travelTone: 'warning' },
{ id: 'BR240713008', reason: '产品培训与交流', city: '深圳', period: '07-10~07-12 (3天)', applyTime: '2024-07-09', amount: '¥2,150.00', node: '部门负责人审批', approval: '审批中', approvalTone: 'info', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240712001', reason: '客户方案汇报', city: '上海', period: '07-08~07-11 (4天)', applyTime: '2024-07-07', amount: '¥3,680.00', node: '财务审核', approval: '审批中', approvalTone: 'info', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240711005', reason: '华南区域市场调研', city: '广州、佛山', period: '07-09~07-11 (3天)', applyTime: '2024-07-06', amount: '¥1,920.00', node: '待提交', approval: '待提交', approvalTone: 'info', travel: '无需预订', travelTone: 'neutral' },
{ id: 'BR240710003', reason: '供应商现场考察', city: '东莞', period: '07-06~07-07 (2天)', applyTime: '2024-07-05', amount: '¥680.00', node: '部门负责人审批', approval: '审批中', approvalTone: 'info', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240709005', reason: '客户方案汇报', city: '北京', period: '07-06~07-08 (3天)', applyTime: '2024-07-05', amount: '¥1,980.00', node: '财务审核', approval: '审批中', approvalTone: 'info', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240708012', reason: '供应商现场考察', city: '广州', period: '07-04~07-05 (2天)', applyTime: '2024-07-03', amount: '¥860.00', node: '部门负责人审批', approval: '审批中', approvalTone: 'info', travel: '无需预订', travelTone: 'neutral' },
{ id: 'BR240707003', reason: '项目启动会', city: '成都', period: '07-01~07-03 (3天)', applyTime: '2024-06-29', amount: '¥2,420.00', node: '财务审核', approval: '审批中', approvalTone: 'info', travel: '待预订酒店', travelTone: 'warning' },
{ id: 'BR240706009', reason: '客户拜访与市场调研', city: '南京、合肥', period: '06-28~06-30 (3天)', applyTime: '2024-06-26', amount: '¥1,750.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240705007', reason: '技术交流会', city: '武汉', period: '06-25~06-26 (2天)', applyTime: '2024-06-23', amount: '¥1,120.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '无需预订', travelTone: 'neutral' },
{ id: 'BR240704004', reason: '渠道合作洽谈', city: '西安', period: '06-20~06-21 (2天)', applyTime: '2024-06-18', amount: '¥780.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240703011', reason: '新员工入职培训', city: '长沙', period: '06-18~06-19 (2天)', applyTime: '2024-06-16', amount: '¥920.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240702006', reason: '季度业绩复盘会', city: '杭州', period: '06-15~06-16 (2天)', applyTime: '2024-06-13', amount: '¥1,350.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240701002', reason: '智慧金融峰会参展', city: '上海', period: '06-12~06-14 (3天)', applyTime: '2024-06-10', amount: '¥5,680.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240630009', reason: '西南区域渠道拓展', city: '重庆、贵阳', period: '06-10~06-13 (4天)', applyTime: '2024-06-08', amount: '¥3,450.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240629003', reason: '信息安全合规审计', city: '深圳', period: '06-08~06-09 (2天)', applyTime: '2024-06-06', amount: '¥1,180.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '无需预订', travelTone: 'neutral' },
{ id: 'BR240628007', reason: '产学研合作对接', city: '南京', period: '06-05~06-07 (3天)', applyTime: '2024-06-03', amount: '¥2,260.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240627001', reason: 'ERP系统上线支持', city: '青岛', period: '06-03~06-05 (3天)', applyTime: '2024-06-01', amount: '¥1,960.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240626004', reason: '大客户续约洽谈', city: '天津', period: '06-01~06-02 (2天)', applyTime: '2024-05-29', amount: '¥890.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240625010', reason: '区域销售团队建设', city: '厦门', period: '05-28~05-30 (3天)', applyTime: '2024-05-26', amount: '¥2,780.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240624002', reason: '供应链管理系统演示', city: '苏州', period: '05-25~05-26 (2天)', applyTime: '2024-05-23', amount: '¥650.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '无需预订', travelTone: 'neutral' },
{ id: 'BR240623008', reason: '行业白皮书发布会', city: '北京', period: '05-22~05-23 (2天)', applyTime: '2024-05-20', amount: '¥1,560.00', node: '待提交', approval: '待提交', approvalTone: 'info', travel: '待订机票', travelTone: 'warning' },
{ id: 'BR240622005', reason: '跨部门协同工作坊', city: '大连', period: '05-20~05-22 (3天)', applyTime: '2024-05-18', amount: '¥2,340.00', node: '待提交', approval: '待提交', approvalTone: 'info', travel: '无需预订', travelTone: 'neutral' },
{ id: 'BR240621003', reason: '数字化转型的客户交流', city: '深圳、珠海', period: '05-16~05-18 (3天)', applyTime: '2024-05-14', amount: '¥3,120.00', node: '待提交', approval: '待提交', approvalTone: 'info', travel: '待预订酒店', travelTone: 'warning' },
{ id: 'BR240620006', reason: '年中预算评审会', city: '上海', period: '05-13~05-14 (2天)', applyTime: '2024-05-11', amount: '¥1,480.00', node: '财务审核', approval: '审批中', approvalTone: 'info', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240619001', reason: '医疗行业解决方案展', city: '成都', period: '05-10~05-12 (3天)', applyTime: '2024-05-08', amount: '¥3,860.00', node: '部门负责人审批', approval: '审批中', approvalTone: 'info', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240618009', reason: '东北区域客户回访', city: '沈阳、长春', period: '05-06~05-09 (4天)', applyTime: '2024-05-04', amount: '¥4,520.00', node: '部门负责人审批', approval: '审批中', approvalTone: 'info', travel: '待订机票', travelTone: 'warning' },
{ id: 'BR240617007', reason: '大数据平台技术对接', city: '杭州', period: '05-03~05-05 (3天)', applyTime: '2024-05-01', amount: '¥2,180.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '已订酒店/机票', travelTone: 'success' },
{ id: 'BR240616004', reason: '国际业务合规培训', city: '北京', period: '04-28~04-30 (3天)', applyTime: '2024-04-26', amount: '¥2,960.00', node: '已完成', approval: '已完成', approvalTone: 'success', travel: '已订酒店/机票', travelTone: 'success' }
]
const visibleRows = computed(() => {
const currentPage = ref(1)
const pageSize = 10
const filteredRows = computed(() => {
if (activeTab.value === '全部') return rows
return rows.filter((row) => row.approval === activeTab.value || row.travel.includes(activeTab.value.replace('待出行', '待订')))
})
const totalCount = computed(() => filteredRows.value.length)
const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / pageSize)))
const visibleRows = computed(() => {
const start = (currentPage.value - 1) * pageSize
return filteredRows.value.slice(start, start + pageSize)
})
watch(activeTab, () => { currentPage.value = 1 })
</script>
<style scoped>
@@ -229,6 +326,42 @@ const visibleRows = computed(() => {
font-weight: 850;
}
.list-search {
position: relative;
width: 220px;
}
.list-search .mdi {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #64748b;
font-size: 15px;
}
.list-search input {
width: 100%;
height: 38px;
padding: 0 12px 0 36px;
border: 1px solid #d7e0ea;
border-radius: 8px;
background: #fff;
color: #0f172a;
font-size: 13px;
transition: border-color 160ms ease, box-shadow 160ms ease;
}
.list-search input::placeholder {
color: #8da0b4;
}
.list-search input:focus {
border-color: #10b981;
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.14);
outline: none;
}
.status-tabs {
display: flex;
gap: 28px;
@@ -302,10 +435,128 @@ const visibleRows = computed(() => {
}
.filter-btn {
min-width: 132px;
min-width: 120px;
justify-content: space-between;
}
.date-range-filter {
position: relative;
}
.date-range-trigger {
min-width: 160px;
}
.date-range-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 110px;
}
.date-range-popover {
position: absolute;
top: calc(100% + 8px);
left: 0;
width: 320px;
z-index: 40;
display: grid;
gap: 14px;
padding: 16px;
border: 1px solid #d7e0ea;
border-radius: 12px;
background: #fff;
box-shadow: 0 18px 42px rgba(15, 23, 42, .16);
}
.date-range-popover header,
.date-range-popover footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.date-range-popover header strong {
color: #0f172a;
font-size: 15px;
}
.date-range-popover header button {
width: 30px;
height: 30px;
display: grid;
place-items: center;
border: 0;
border-radius: 8px;
background: transparent;
color: #64748b;
}
.date-range-popover header button:hover {
background: #f1f5f9;
color: #0f172a;
}
.date-range-fields {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.date-range-fields label {
display: grid;
gap: 6px;
}
.date-range-fields span {
color: #64748b;
font-size: 12px;
font-weight: 700;
}
.date-range-fields input {
width: 100%;
height: 38px;
padding: 0 9px;
border: 1px solid #d7e0ea;
border-radius: 8px;
color: #0f172a;
font-size: 13px;
}
.date-range-fields input:focus {
border-color: #10b981;
box-shadow: 0 0 0 3px rgba(16, 185, 129, .12);
outline: none;
}
.ghost-btn,
.apply-btn {
height: 36px;
padding: 0 14px;
border-radius: 8px;
font-size: 13px;
font-weight: 750;
}
.ghost-btn {
border: 1px solid #d7e0ea;
background: #fff;
color: #334155;
}
.apply-btn {
border: 0;
background: #10b981;
color: #fff;
}
.apply-btn:disabled {
cursor: not-allowed;
background: #cbd5e1;
}
.filter-btn:hover,
.export-btn:hover,
.page-size:hover {
@@ -333,12 +584,12 @@ const visibleRows = computed(() => {
font-size: 13px;
}
.hint .pi {
.hint .mdi {
color: #64748b;
}
.table-wrap {
min-height: 0;
min-height: 495px;
margin-top: 10px;
overflow-x: auto;
overflow-y: auto;
@@ -348,19 +599,27 @@ const visibleRows = computed(() => {
table {
width: 100%;
min-width: 1040px;
min-width: 1140px;
border-collapse: collapse;
table-layout: fixed;
}
colgroup col {
width: 10%;
}
th,
td {
padding: 15px 12px;
padding: 13px 12px;
border-bottom: 1px solid #edf2f7;
color: #24324a;
font-size: 14px;
line-height: 1.35;
text-align: left;
text-align: center;
vertical-align: middle;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
th {
@@ -368,14 +627,16 @@ th {
color: #64748b;
font-size: 13px;
font-weight: 800;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
tbody tr {
cursor: pointer;
}
tbody tr:hover,
tbody tr:nth-child(2) {
tbody tr:hover {
background: linear-gradient(90deg, rgba(16, 185, 129, .08), rgba(16, 185, 129, .03));
}
@@ -447,7 +708,7 @@ tbody tr:last-child td {
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 16px;
margin-top: 12px;
margin-top: 24px;
}
.page-summary {

View File

@@ -3,7 +3,7 @@
<article class="assistant-panel panel">
<header class="assistant-head">
<button class="back-btn" type="button" aria-label="返回差旅申请/报销" @click="emit('backToRequests')">
<i class="pi pi-arrow-left"></i>
<i class="mdi mdi-arrow-left"></i>
<span>返回</span>
</button>
</header>
@@ -12,25 +12,25 @@
<div class="message-row user">
<div class="message-bubble user-bubble">
<p>我要报销昨天去上海出差的费用</p>
<time>09:41 <i class="pi pi-check"></i></time>
<time>09:41 <i class="mdi mdi-check"></i></time>
</div>
<span class="avatar user-avatar"><i class="pi pi-user"></i></span>
<span class="avatar user-avatar"><i class="mdi mdi-account-outline"></i></span>
</div>
<div class="message-row assistant">
<span class="avatar ai-avatar"><i class="pi pi-sparkles"></i></span>
<span class="avatar ai-avatar"><i class="mdi mdi-sparkles"></i></span>
<div class="message-bubble">
<p>好的我已识别到这是一次差旅报销我建议你先补充行程日期交通票据和酒店发票</p>
<div class="message-actions">
<button type="button" aria-label="赞同"><i class="pi pi-thumbs-up"></i></button>
<button type="button" aria-label="不赞同"><i class="pi pi-thumbs-down"></i></button>
<button type="button" aria-label="赞同"><i class="mdi mdi-thumb-up-outline"></i></button>
<button type="button" aria-label="不赞同"><i class="mdi mdi-thumb-down-outline"></i></button>
</div>
<time>09:42</time>
</div>
</div>
<div class="message-row assistant">
<span class="avatar ai-avatar"><i class="pi pi-sparkles"></i></span>
<span class="avatar ai-avatar"><i class="mdi mdi-sparkles"></i></span>
<div class="message-bubble wide">
<p>已自动识别</p>
<div class="detected-grid">
@@ -47,24 +47,24 @@
<div class="message-row user">
<div class="message-bubble user-bubble compact">
<p>现在到哪一步了</p>
<time>09:43 <i class="pi pi-check"></i></time>
<time>09:43 <i class="mdi mdi-check"></i></time>
</div>
<span class="avatar user-avatar"><i class="pi pi-user"></i></span>
<span class="avatar user-avatar"><i class="mdi mdi-account-outline"></i></span>
</div>
<div class="message-row assistant">
<span class="avatar ai-avatar"><i class="pi pi-sparkles"></i></span>
<span class="avatar ai-avatar"><i class="mdi mdi-sparkles"></i></span>
<div class="message-bubble">
<p>当前已完成票据识别与费用归类下一步请确认费用明细并提交审批</p>
<ul class="check-list">
<li><i class="pi pi-check"></i>票据识别已完成</li>
<li><i class="pi pi-check"></i>费用归类已完成</li>
<li><i class="pi pi-circle"></i>明细确认待你确认</li>
<li><i class="pi pi-circle"></i>提交审批待提交</li>
<li><i class="mdi mdi-check"></i>票据识别已完成</li>
<li><i class="mdi mdi-check"></i>费用归类已完成</li>
<li><i class="mdi mdi-circle"></i>明细确认待你确认</li>
<li><i class="mdi mdi-circle"></i>提交审批待提交</li>
</ul>
<div class="message-actions">
<button type="button" aria-label="赞同"><i class="pi pi-thumbs-up"></i></button>
<button type="button" aria-label="不赞同"><i class="pi pi-thumbs-down"></i></button>
<button type="button" aria-label="赞同"><i class="mdi mdi-thumb-up-outline"></i></button>
<button type="button" aria-label="不赞同"><i class="mdi mdi-thumb-down-outline"></i></button>
</div>
<time>09:43</time>
</div>
@@ -82,12 +82,12 @@
<textarea rows="2" placeholder="请输入你的问题,或让 AI 帮你填写报销..."></textarea>
<div class="composer-foot">
<div class="tool-set">
<button type="button" aria-label="上传附件"><i class="pi pi-paperclip"></i></button>
<button type="button" aria-label="上传图片"><i class="pi pi-image"></i></button>
<button type="button" aria-label="语音输入"><i class="pi pi-microphone"></i></button>
<button type="button" aria-label="上传附件"><i class="mdi mdi-paperclip"></i></button>
<button type="button" aria-label="上传图片"><i class="mdi mdi-image-outline"></i></button>
<button type="button" aria-label="语音输入"><i class="mdi mdi-microphone"></i></button>
</div>
<button class="send-btn" type="submit" aria-label="发送">
<i class="pi pi-send"></i>
<i class="mdi mdi-send"></i>
</button>
</div>
</form>
@@ -112,7 +112,7 @@
<article class="side-card panel">
<header>
<h3>当前摘要</h3>
<button type="button">查看详情 <i class="pi pi-angle-right"></i></button>
<button type="button">查看详情 <i class="mdi mdi-chevron-right"></i></button>
</header>
<dl class="summary-list">
<template v-for="item in summaryItems" :key="item.label">
@@ -141,7 +141,7 @@
<span>{{ item.text }}</span>
</li>
</ul>
<button class="more-link" type="button">查看更多建议 <i class="pi pi-angle-right"></i></button>
<button class="more-link" type="button">查看更多建议 <i class="mdi mdi-chevron-right"></i></button>
</article>
<article class="side-card panel">
@@ -149,14 +149,14 @@
<ul class="notice-list">
<li v-for="item in notices" :key="item">{{ item }}</li>
</ul>
<button class="more-link" type="button">查看差旅管理制度 <i class="pi pi-angle-right"></i></button>
<button class="more-link" type="button">查看差旅管理制度 <i class="mdi mdi-chevron-right"></i></button>
</article>
</div>
<article class="recent-card panel">
<header>
<h3>最近操作</h3>
<button type="button">查看全部 <i class="pi pi-angle-right"></i></button>
<button type="button">查看全部 <i class="mdi mdi-chevron-right"></i></button>
</header>
<ol>
<li v-for="item in recentOps" :key="item.time">
@@ -173,17 +173,17 @@
const emit = defineEmits(['backToRequests'])
const detectedItems = [
{ label: '机票', count: '2 张', icon: 'pi pi-send' },
{ label: '酒店发票', count: '1 张', icon: 'pi pi-building' },
{ label: '出租车发票', count: '3 张', icon: 'pi pi-car' },
{ label: '其他票据', count: '0 张', icon: 'pi pi-file' }
{ label: '机票', count: '2 张', icon: 'mdi mdi-send' },
{ label: '酒店发票', count: '1 张', icon: 'mdi mdi-office-building' },
{ label: '出租车发票', count: '3 张', icon: 'mdi mdi-car' },
{ label: '其他票据', count: '0 张', icon: 'mdi mdi-file-document-outline' }
]
const quickActions = [
{ label: '一键生成报销单', icon: 'pi pi-file-edit' },
{ label: '检查缺失材料', icon: 'pi pi-verified' },
{ label: '查看审批进度', icon: 'pi pi-list-check' },
{ label: '差旅标准查询', icon: 'pi pi-search' }
{ label: '一键生成报销单', icon: 'mdi mdi-file-document-outline-edit' },
{ label: '检查缺失材料', icon: 'mdi mdi-check-decagram' },
{ label: '查看审批进度', icon: 'mdi mdi-clipboard-check-outline' },
{ label: '差旅标准查询', icon: 'mdi mdi-magnify' }
]
const progressSteps = [
@@ -195,24 +195,24 @@ const progressSteps = [
]
const summaryItems = [
{ label: '报销类型', value: '差旅报销', icon: 'pi pi-tags' },
{ label: '申请人', value: '张晓明', icon: 'pi pi-user' },
{ label: '金额预估', value: '¥3,680', icon: 'pi pi-wallet', money: true },
{ label: '出差时间', value: '07-11 ~ 07-12', icon: 'pi pi-calendar' },
{ label: '单据数量', value: '6', icon: 'pi pi-copy' }
{ label: '报销类型', value: '差旅报销', icon: 'mdi mdi-tag-multiple' },
{ label: '申请人', value: '张晓明', icon: 'mdi mdi-account-outline' },
{ label: '金额预估', value: '¥3,680', icon: 'mdi mdi-wallet', money: true },
{ label: '出差时间', value: '07-11 ~ 07-12', icon: 'mdi mdi-calendar' },
{ label: '单据数量', value: '6', icon: 'mdi mdi-content-copy' }
]
const todos = [
{ text: '缺少酒店入住清单', tag: '缺失', tone: 'danger', icon: 'pi pi-times-circle' },
{ text: '发票抬头已识别为个人,建议核对', tag: '警告', tone: 'warning', icon: 'pi pi-exclamation-triangle' },
{ text: '有 1 笔出租车费用超过标准', tag: '注意', tone: 'notice', icon: 'pi pi-info-circle' },
{ text: '请在今日 18:00 前提交以保证本周审批', tag: '提醒', tone: 'success', icon: 'pi pi-clock' }
{ text: '缺少酒店入住清单', tag: '缺失', tone: 'danger', icon: 'mdi mdi-close-circle' },
{ text: '发票抬头已识别为个人,建议核对', tag: '警告', tone: 'warning', icon: 'mdi mdi-alert' },
{ text: '有 1 笔出租车费用超过标准', tag: '注意', tone: 'notice', icon: 'mdi mdi-information-outline' },
{ text: '请在今日 18:00 前提交以保证本周审批', tag: '提醒', tone: 'success', icon: 'mdi mdi-clock-outline' }
]
const advice = [
{ text: '建议补充酒店入住清单,提升审批通过率', icon: 'pi pi-lightbulb' },
{ text: '出租车费用超标 ¥28建议说明原因或调整', icon: 'pi pi-car' },
{ text: '可合并同类票据,减少审批批次', icon: 'pi pi-check-circle' }
{ text: '建议补充酒店入住清单,提升审批通过率', icon: 'mdi mdi-lightbulb-outline' },
{ text: '出租车费用超标 ¥28建议说明原因或调整', icon: 'mdi mdi-car' },
{ text: '可合并同类票据,减少审批批次', icon: 'mdi mdi-check-circle' }
]
const notices = [