feat: improve ChatView, add LoginView and demo reference page
- ChatView: enhanced AI chat interface with improved message rendering - LoginView: new login page component - demo/main_demo.html: reference implementation for Chart.js dashboards Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
834
demo/main_demo.html
Normal file
834
demo/main_demo.html
Normal file
@@ -0,0 +1,834 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>企业报销智能运营台</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#10b981',
|
||||
secondary: '#3b82f6',
|
||||
purple: '#8b5cf6',
|
||||
orange: '#f59e0b',
|
||||
red: '#ef4444',
|
||||
gray: {
|
||||
50: '#f8fafc',
|
||||
100: '#f1f5f9',
|
||||
200: '#e2e8f0',
|
||||
300: '#cbd5e1',
|
||||
400: '#94a3b8',
|
||||
500: '#64748b',
|
||||
600: '#475569',
|
||||
700: '#334155',
|
||||
800: '#1e293b',
|
||||
900: '#0f172a',
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
inter: ['Inter', 'sans-serif'],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style type="text/tailwindcss">
|
||||
@layer utilities {
|
||||
.content-auto {
|
||||
content-visibility: auto;
|
||||
}
|
||||
.card-shadow {
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.sidebar-item-active {
|
||||
background-color: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="font-inter bg-gray-50 text-gray-800 min-h-screen flex flex-col">
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<!-- 左侧侧边栏 -->
|
||||
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col">
|
||||
<!-- Logo区域 -->
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-8 h-8 bg-primary rounded-md flex items-center justify-center">
|
||||
<i class="fa fa-leaf text-white"></i>
|
||||
</div>
|
||||
<span class="font-semibold text-lg">星海科技</span>
|
||||
<button class="ml-auto text-gray-400 hover:text-gray-600">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
<nav class="flex-1 py-4 overflow-y-auto">
|
||||
<ul class="space-y-1 px-3">
|
||||
<li>
|
||||
<a href="#" class="flex items-center px-3 py-2.5 rounded-md sidebar-item-active">
|
||||
<i class="fa fa-th-large w-5 text-center"></i>
|
||||
<span class="ml-3">总览</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="flex items-center px-3 py-2.5 rounded-md text-gray-600 hover:bg-gray-100">
|
||||
<i class="fa fa-file-text-o w-5 text-center"></i>
|
||||
<span class="ml-3">报销单</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="flex items-center px-3 py-2.5 rounded-md text-gray-600 hover:bg-gray-100">
|
||||
<i class="fa fa-check-square-o w-5 text-center"></i>
|
||||
<span class="ml-3">审批中心</span>
|
||||
<span class="ml-auto bg-red text-white text-xs px-2 py-0.5 rounded-full">128</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="flex items-center px-3 py-2.5 rounded-md text-gray-600 hover:bg-gray-100">
|
||||
<i class="fa fa-exclamation-triangle w-5 text-center"></i>
|
||||
<span class="ml-3">风险预警</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="flex items-center px-3 py-2.5 rounded-md text-gray-600 hover:bg-gray-100">
|
||||
<i class="fa fa-ticket w-5 text-center"></i>
|
||||
<span class="ml-3">发票中心</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="flex items-center px-3 py-2.5 rounded-md text-gray-600 hover:bg-gray-100">
|
||||
<i class="fa fa-pie-chart w-5 text-center"></i>
|
||||
<span class="ml-3">预算控制</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="flex items-center px-3 py-2.5 rounded-md text-gray-600 hover:bg-gray-100">
|
||||
<i class="fa fa-users w-5 text-center"></i>
|
||||
<span class="ml-3">员工服务</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="flex items-center px-3 py-2.5 rounded-md text-gray-600 hover:bg-gray-100">
|
||||
<i class="fa fa-cog w-5 text-center"></i>
|
||||
<span class="ml-3">设置</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<div class="p-4 border-t border-gray-200">
|
||||
<div class="flex items-center space-x-3">
|
||||
<img src="https://picsum.photos/id/64/40/40" alt="用户头像" class="w-8 h-8 rounded-full object-cover">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 truncate">张晓明</p>
|
||||
<p class="text-xs text-gray-500 truncate">财务管理员</p>
|
||||
</div>
|
||||
<button class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fa fa-angle-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="flex-1 overflow-y-auto bg-gray-50">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="bg-white border-b border-gray-200 sticky top-0 z-10">
|
||||
<div class="flex items-center justify-between p-4">
|
||||
<div>
|
||||
<div class="text-xs text-primary font-medium uppercase tracking-wider">Smart Expense Operations</div>
|
||||
<h1 class="text-2xl font-bold text-gray-800 mt-1">企业报销智能运营台</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">面向财务共享中心的审批、风控、SLA与自动化运营看板</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- 搜索框 -->
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i class="fa fa-search text-gray-400"></i>
|
||||
</div>
|
||||
<input type="text" placeholder="搜索申请人、单号、费用类型..." class="pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary w-64">
|
||||
</div>
|
||||
|
||||
<!-- 日期选择器 -->
|
||||
<div class="relative">
|
||||
<select class="appearance-none pl-3 pr-8 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary bg-white">
|
||||
<option>2024-07-06 ~ 2024-07-12</option>
|
||||
<option>2024-07-01 ~ 2024-07-07</option>
|
||||
<option>2024-06-30 ~ 2024-07-06</option>
|
||||
</select>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||
<i class="fa fa-calendar-o text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI助手按钮 -->
|
||||
<button class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-md flex items-center space-x-2 transition-colors">
|
||||
<i class="fa fa-magic"></i>
|
||||
<span>AI助手</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间筛选标签 -->
|
||||
<div class="flex items-center justify-end px-4 pb-3 space-x-2">
|
||||
<button class="px-3 py-1 text-sm rounded-md bg-white border border-gray-300 hover:bg-gray-50">今日</button>
|
||||
<button class="px-3 py-1 text-sm rounded-md bg-gray-100 border border-gray-300 font-medium">本周</button>
|
||||
<button class="px-3 py-1 text-sm rounded-md bg-white border border-gray-300 hover:bg-gray-50">本月</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 仪表盘内容 -->
|
||||
<div class="p-6">
|
||||
<!-- 统计卡片行 -->
|
||||
<div class="grid grid-cols-6 gap-6 mb-6">
|
||||
<!-- 待审批单据 -->
|
||||
<div class="bg-white rounded-lg p-5 card-shadow">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">待审批单据</p>
|
||||
<p class="text-2xl font-bold text-gray-800">128 <span class="text-sm font-normal text-gray-500">单</span></p>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-green-50 flex items-center justify-center">
|
||||
<i class="fa fa-file-text-o text-primary text-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center text-sm">
|
||||
<span class="text-green-600 flex items-center">
|
||||
<i class="fa fa-arrow-down mr-1"></i> 12.5%
|
||||
</span>
|
||||
<span class="text-gray-500 ml-2">较昨日 -18 单</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 待处理金额 -->
|
||||
<div class="bg-white rounded-lg p-5 card-shadow">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">待处理金额</p>
|
||||
<p class="text-2xl font-bold text-gray-800">¥361,600</p>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center">
|
||||
<i class="fa fa-yen text-secondary text-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center text-sm">
|
||||
<span class="text-red-600 flex items-center">
|
||||
<i class="fa fa-arrow-up mr-1"></i> 8.3%
|
||||
</span>
|
||||
<span class="text-gray-500 ml-2">较昨日 +¥27,400</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 平均审批时长 -->
|
||||
<div class="bg-white rounded-lg p-5 card-shadow">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">平均审批时长</p>
|
||||
<p class="text-2xl font-bold text-gray-800">6.8 <span class="text-sm font-normal text-gray-500">h</span></p>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-purple-50 flex items-center justify-center">
|
||||
<i class="fa fa-clock-o text-purple text-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center text-sm">
|
||||
<span class="text-green-600 flex items-center">
|
||||
<i class="fa fa-arrow-down mr-1"></i> 14.8%
|
||||
</span>
|
||||
<span class="text-gray-500 ml-2">较昨日 -1.2h</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自动审单通过率 -->
|
||||
<div class="bg-white rounded-lg p-5 card-shadow">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">自动审单通过率</p>
|
||||
<p class="text-2xl font-bold text-gray-800">78 <span class="text-sm font-normal text-gray-500">%</span></p>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-green-50 flex items-center justify-center">
|
||||
<i class="fa fa-shield text-primary text-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center text-sm">
|
||||
<span class="text-green-600 flex items-center">
|
||||
<i class="fa fa-arrow-up mr-1"></i> 6.2%
|
||||
</span>
|
||||
<span class="text-gray-500 ml-2">较昨日 +4.6%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 异常预警单 -->
|
||||
<div class="bg-white rounded-lg p-5 card-shadow">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">异常预警单</p>
|
||||
<p class="text-2xl font-bold text-gray-800">14 <span class="text-sm font-normal text-gray-500">单</span></p>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center">
|
||||
<i class="fa fa-exclamation-triangle text-red text-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center text-sm">
|
||||
<span class="text-red-600 flex items-center">
|
||||
<i class="fa fa-arrow-up mr-1"></i> 16.7%
|
||||
</span>
|
||||
<span class="text-gray-500 ml-2">较昨日 +2 单</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SLA达成率 -->
|
||||
<div class="bg-white rounded-lg p-5 card-shadow">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">SLA达成率</p>
|
||||
<p class="text-2xl font-bold text-gray-800">96 <span class="text-sm font-normal text-gray-500">%</span></p>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-green-50 flex items-center justify-center">
|
||||
<i class="fa fa-check-circle text-primary text-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center text-sm">
|
||||
<span class="text-green-600 flex items-center">
|
||||
<i class="fa fa-arrow-up mr-1"></i> 3.1%
|
||||
</span>
|
||||
<span class="text-gray-500 ml-2">较昨日 +2.9%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表行1 -->
|
||||
<div class="grid grid-cols-12 gap-6 mb-6">
|
||||
<!-- 报销申请与审批趋势 -->
|
||||
<div class="col-span-6 bg-white rounded-lg p-5 card-shadow">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-semibold text-gray-800">报销申请与审批趋势 <i class="fa fa-info-circle text-gray-400 text-xs ml-1"></i></h3>
|
||||
<select class="text-sm border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-primary">
|
||||
<option>近12天</option>
|
||||
<option>近7天</option>
|
||||
<option>近30天</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="h-64">
|
||||
<canvas id="trendChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 费用结构 -->
|
||||
<div class="col-span-3 bg-white rounded-lg p-5 card-shadow">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-semibold text-gray-800">费用结构 <i class="fa fa-info-circle text-gray-400 text-xs ml-1"></i></h3>
|
||||
</div>
|
||||
<div class="flex items-center justify-center h-48">
|
||||
<canvas id="expenseChart"></canvas>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 text-center mt-2">* 百分比为占待处理金额比例</p>
|
||||
</div>
|
||||
|
||||
<!-- 风险异常分布 -->
|
||||
<div class="col-span-3 bg-white rounded-lg p-5 card-shadow">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-semibold text-gray-800">风险异常分布 <i class="fa fa-info-circle text-gray-400 text-xs ml-1"></i></h3>
|
||||
</div>
|
||||
<div class="flex items-center justify-center h-48">
|
||||
<canvas id="riskChart"></canvas>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 text-center mt-2">* 近30天数据</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表行2 -->
|
||||
<div class="grid grid-cols-12 gap-6">
|
||||
<!-- 部门报销排行 -->
|
||||
<div class="col-span-6 bg-white rounded-lg p-5 card-shadow">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-semibold text-gray-800">部门报销排行(待处理金额) <i class="fa fa-info-circle text-gray-400 text-xs ml-1"></i></h3>
|
||||
<select class="text-sm border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-primary">
|
||||
<option>本周</option>
|
||||
<option>本月</option>
|
||||
<option>本季度</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center">
|
||||
<span class="w-6 h-6 rounded-full bg-orange text-white flex items-center justify-center text-xs font-medium mr-3">1</span>
|
||||
<span class="w-16 text-sm text-gray-600">销售部</span>
|
||||
<div class="flex-1 h-6 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-primary rounded-full" style="width: 100%"></div>
|
||||
</div>
|
||||
<span class="ml-3 text-sm font-medium text-gray-700">¥182,000</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="w-6 h-6 rounded-full bg-gray-400 text-white flex items-center justify-center text-xs font-medium mr-3">2</span>
|
||||
<span class="w-16 text-sm text-gray-600">研发中心</span>
|
||||
<div class="flex-1 h-6 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-secondary rounded-full" style="width: 80%"></div>
|
||||
</div>
|
||||
<span class="ml-3 text-sm font-medium text-gray-700">¥146,000</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="w-6 h-6 rounded-full bg-orange/80 text-white flex items-center justify-center text-xs font-medium mr-3">3</span>
|
||||
<span class="w-16 text-sm text-gray-600">市场部</span>
|
||||
<div class="flex-1 h-6 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-orange rounded-full" style="width: 53%"></div>
|
||||
</div>
|
||||
<span class="ml-3 text-sm font-medium text-gray-700">¥96,000</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="w-6 h-6 rounded-full bg-gray-500 text-white flex items-center justify-center text-xs font-medium mr-3">4</span>
|
||||
<span class="w-16 text-sm text-gray-600">运营部</span>
|
||||
<div class="flex-1 h-6 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-purple rounded-full" style="width: 38%"></div>
|
||||
</div>
|
||||
<span class="ml-3 text-sm font-medium text-gray-700">¥68,600</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="w-6 h-6 rounded-full bg-gray-600 text-white flex items-center justify-center text-xs font-medium mr-3">5</span>
|
||||
<span class="w-16 text-sm text-gray-600">行政部</span>
|
||||
<div class="flex-1 h-6 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-blue-500 rounded-full" style="width: 27%"></div>
|
||||
</div>
|
||||
<span class="ml-3 text-sm font-medium text-gray-700">¥48,300</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审批瓶颈 -->
|
||||
<div class="col-span-3 bg-white rounded-lg p-5 card-shadow">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-semibold text-gray-800">审批瓶颈(平均处理时长) <i class="fa fa-info-circle text-gray-400 text-xs ml-1"></i></h3>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<img src="https://picsum.photos/id/64/32/32" alt="李文静" class="w-8 h-8 rounded-full mr-3">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-800">李文静</p>
|
||||
<p class="text-xs text-gray-500">财务经理</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-medium text-gray-800">12.4 h</p>
|
||||
<span class="text-xs bg-red/10 text-red px-2 py-0.5 rounded">较慢</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<img src="https://picsum.photos/id/91/32/32" alt="王志强" class="w-8 h-8 rounded-full mr-3">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-800">王志强</p>
|
||||
<p class="text-xs text-gray-500">财务专员</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-medium text-gray-800">8.7 h</p>
|
||||
<span class="text-xs bg-orange/10 text-orange px-2 py-0.5 rounded">偏慢</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<img src="https://picsum.photos/id/55/32/32" alt="刘思雨" class="w-8 h-8 rounded-full mr-3">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-800">刘思雨</p>
|
||||
<p class="text-xs text-gray-500">费用审核员</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-medium text-gray-800">5.2 h</p>
|
||||
<span class="text-xs bg-green/10 text-green-600 px-2 py-0.5 rounded">正常</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t border-gray-100">
|
||||
<a href="#" class="text-sm text-primary hover:text-primary/80 flex items-center justify-between">
|
||||
查看全部
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预算执行率 -->
|
||||
<div class="col-span-3 bg-white rounded-lg p-5 card-shadow">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-semibold text-gray-800">预算执行率(本月) <i class="fa fa-info-circle text-gray-400 text-xs ml-1"></i></h3>
|
||||
</div>
|
||||
<div class="flex items-center justify-center h-32">
|
||||
<canvas id="budgetChart"></canvas>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2 mt-4 text-center">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500">预算总额</p>
|
||||
<p class="text-sm font-medium text-gray-800">¥2,800,000</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500">已执行</p>
|
||||
<p class="text-sm font-medium text-gray-800">¥2,128,000</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500">剩余可用</p>
|
||||
<p class="text-sm font-medium text-gray-800">¥672,000</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t border-gray-100">
|
||||
<a href="#" class="text-sm text-primary hover:text-primary/80 flex items-center justify-between">
|
||||
查看详情
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 报销申请与审批趋势图
|
||||
const trendCtx = document.getElementById('trendChart').getContext('2d');
|
||||
new Chart(trendCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['07-01', '07-02', '07-03', '07-04', '07-05', '07-06', '07-07', '07-08', '07-09', '07-10', '07-12'],
|
||||
datasets: [
|
||||
{
|
||||
label: '申请量(单)',
|
||||
data: [140, 105, 175, 195, 155, 70, 65, 60, 185, 200, 220],
|
||||
backgroundColor: '#10b981',
|
||||
borderRadius: 4,
|
||||
barPercentage: 0.6,
|
||||
categoryPercentage: 0.5
|
||||
},
|
||||
{
|
||||
label: '审批完成量(单)',
|
||||
data: [110, 85, 130, 125, 110, 60, 55, 50, 145, 150, 170],
|
||||
backgroundColor: '#3b82f6',
|
||||
borderRadius: 4,
|
||||
barPercentage: 0.6,
|
||||
categoryPercentage: 0.5
|
||||
},
|
||||
{
|
||||
label: '平均审批时长(小时)',
|
||||
data: [10, 8, 9, 7, 7, 6.8, 6, 6.5, 7, 8, 7.5],
|
||||
borderColor: '#8b5cf6',
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 2,
|
||||
pointBackgroundColor: '#ffffff',
|
||||
pointBorderColor: '#8b5cf6',
|
||||
pointBorderWidth: 2,
|
||||
pointRadius: 4,
|
||||
type: 'line',
|
||||
yAxisID: 'y1'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
align: 'start',
|
||||
labels: {
|
||||
boxWidth: 10,
|
||||
usePointStyle: true,
|
||||
pointStyle: 'circle'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
titleColor: '#1e293b',
|
||||
bodyColor: '#64748b',
|
||||
borderColor: '#e2e8f0',
|
||||
borderWidth: 1,
|
||||
padding: 12,
|
||||
boxPadding: 6,
|
||||
usePointStyle: true,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += context.parsed.y;
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
color: '#64748b',
|
||||
font: {
|
||||
size: 11
|
||||
}
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 250,
|
||||
grid: {
|
||||
color: '#f1f5f9'
|
||||
},
|
||||
ticks: {
|
||||
color: '#64748b',
|
||||
font: {
|
||||
size: 11
|
||||
},
|
||||
stepSize: 50
|
||||
}
|
||||
},
|
||||
y1: {
|
||||
position: 'right',
|
||||
beginAtZero: true,
|
||||
max: 15,
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
color: '#64748b',
|
||||
font: {
|
||||
size: 11
|
||||
},
|
||||
stepSize: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 费用结构图
|
||||
const expenseCtx = document.getElementById('expenseChart').getContext('2d');
|
||||
new Chart(expenseCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['机票', '酒店', '火车/用车', '餐补及杂费'],
|
||||
datasets: [{
|
||||
data: [182000, 146000, 78600, 55000],
|
||||
backgroundColor: ['#10b981', '#3b82f6', '#f59e0b', '#8b5cf6'],
|
||||
borderWidth: 0,
|
||||
borderRadius: 0,
|
||||
cutout: '70%'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: {
|
||||
boxWidth: 10,
|
||||
usePointStyle: true,
|
||||
pointStyle: 'circle',
|
||||
padding: 15,
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
titleColor: '#1e293b',
|
||||
bodyColor: '#64748b',
|
||||
borderColor: '#e2e8f0',
|
||||
borderWidth: 1,
|
||||
padding: 12,
|
||||
boxPadding: 6,
|
||||
usePointStyle: true,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = ((value / total) * 100).toFixed(1);
|
||||
return `${label}: ¥${value.toLocaleString()} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [{
|
||||
id: 'centerText',
|
||||
beforeDraw: function(chart) {
|
||||
const width = chart.width;
|
||||
const height = chart.height;
|
||||
const ctx = chart.ctx;
|
||||
ctx.restore();
|
||||
|
||||
const fontSize = (height / 160).toFixed(2);
|
||||
ctx.font = `bold ${fontSize}em Inter`;
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.textAlign = 'center';
|
||||
|
||||
const text = '¥361,600';
|
||||
const textX = width / 2;
|
||||
const textY = height / 2 - 10;
|
||||
ctx.fillStyle = '#1e293b';
|
||||
ctx.fillText(text, textX, textY);
|
||||
|
||||
ctx.font = `${fontSize * 0.6}em Inter`;
|
||||
ctx.fillStyle = '#64748b';
|
||||
ctx.fillText('待处理金额', textX, textY + 20);
|
||||
|
||||
ctx.save();
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
// 风险异常分布图
|
||||
const riskCtx = document.getElementById('riskChart').getContext('2d');
|
||||
new Chart(riskCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['住宿超标', '重复报销', '行程缺失', '发票异常'],
|
||||
datasets: [{
|
||||
data: [5, 3, 3, 3],
|
||||
backgroundColor: ['#ef4444', '#f59e0b', '#8b5cf6', '#3b82f6'],
|
||||
borderWidth: 0,
|
||||
borderRadius: 0,
|
||||
cutout: '70%'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: {
|
||||
boxWidth: 10,
|
||||
usePointStyle: true,
|
||||
pointStyle: 'circle',
|
||||
padding: 15,
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
titleColor: '#1e293b',
|
||||
bodyColor: '#64748b',
|
||||
borderColor: '#e2e8f0',
|
||||
borderWidth: 1,
|
||||
padding: 12,
|
||||
boxPadding: 6,
|
||||
usePointStyle: true,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = ((value / total) * 100).toFixed(1);
|
||||
return `${label}: ${value} 单 (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [{
|
||||
id: 'centerText',
|
||||
beforeDraw: function(chart) {
|
||||
const width = chart.width;
|
||||
const height = chart.height;
|
||||
const ctx = chart.ctx;
|
||||
ctx.restore();
|
||||
|
||||
const fontSize = (height / 160).toFixed(2);
|
||||
ctx.font = `bold ${fontSize}em Inter`;
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.textAlign = 'center';
|
||||
|
||||
const text = '14';
|
||||
const textX = width / 2;
|
||||
const textY = height / 2 - 10;
|
||||
ctx.fillStyle = '#1e293b';
|
||||
ctx.fillText(text, textX, textY);
|
||||
|
||||
ctx.font = `${fontSize * 0.6}em Inter`;
|
||||
ctx.fillStyle = '#64748b';
|
||||
ctx.fillText('异常预警单', textX, textY + 20);
|
||||
|
||||
ctx.save();
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
// 预算执行率图
|
||||
const budgetCtx = document.getElementById('budgetChart').getContext('2d');
|
||||
new Chart(budgetCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
datasets: [{
|
||||
data: [76, 24],
|
||||
backgroundColor: ['#10b981', '#e2e8f0'],
|
||||
borderWidth: 0,
|
||||
borderRadius: 0,
|
||||
cutout: '75%'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
rotation: -90,
|
||||
circumference: 180
|
||||
},
|
||||
plugins: [{
|
||||
id: 'centerText',
|
||||
beforeDraw: function(chart) {
|
||||
const width = chart.width;
|
||||
const height = chart.height;
|
||||
const ctx = chart.ctx;
|
||||
ctx.restore();
|
||||
|
||||
const fontSize = (height / 80).toFixed(2);
|
||||
ctx.font = `bold ${fontSize}em Inter`;
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.textAlign = 'center';
|
||||
|
||||
const text = '76%';
|
||||
const textX = width / 2;
|
||||
const textY = height / 2 + 10;
|
||||
ctx.fillStyle = '#10b981';
|
||||
ctx.fillText(text, textX, textY);
|
||||
|
||||
ctx.font = `${fontSize * 0.5}em Inter`;
|
||||
ctx.fillStyle = '#64748b';
|
||||
ctx.fillText('已执行', textX, textY + 25);
|
||||
|
||||
ctx.save();
|
||||
}
|
||||
}]
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,81 +1,157 @@
|
||||
<template>
|
||||
<section class="view chat-view">
|
||||
<article class="panel chat-shell">
|
||||
<header class="chat-hero">
|
||||
<div>
|
||||
<div class="eyebrow">Compliance conversation</div>
|
||||
<h2>上传单据,询问 AI 是否合规</h2>
|
||||
<p>把发票、行程单、合同附件或审批说明放到同一个上下文里,AI 会按制度、预算和审计留痕给出建议。</p>
|
||||
</div>
|
||||
<label class="upload-card">
|
||||
<span class="upload-icon" v-html="fileIcon"></span>
|
||||
<strong>上传单据</strong>
|
||||
<small>PDF、图片、Excel 或压缩包</small>
|
||||
<input type="file" multiple @change="emit('upload', $event)" />
|
||||
</label>
|
||||
</header>
|
||||
<section class="view full">
|
||||
<div class="queue-panel">
|
||||
<DataTable
|
||||
:value="documents"
|
||||
:paginator="true"
|
||||
:rows="pageSize"
|
||||
:rowsPerPageOptions="[5, 10, 20, 50]"
|
||||
:totalRecords="documents.length"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown JumpToPageInput"
|
||||
currentPageReportTemplate="共 {totalRecords} 条"
|
||||
stripedRows
|
||||
size="small"
|
||||
:rowHover="false"
|
||||
tableStyle="min-width: 960px"
|
||||
>
|
||||
<Column field="id" header="单据编号">
|
||||
<template #body="{ data }">
|
||||
<strong>{{ data.id }}</strong>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<div class="upload-list" aria-live="polite">
|
||||
<span v-for="file in uploadedFiles" :key="file.name" class="file-pill">{{ file.name }}</span>
|
||||
<span v-if="!uploadedFiles.length" class="file-pill muted">尚未上传文件,可直接选择现有报销单追问。</span>
|
||||
</div>
|
||||
<Column field="type" header="申请类型">
|
||||
<template #body="{ data }">
|
||||
<span class="type-tag" :class="data.typeTag">{{ data.type }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<div class="dialog-body chat-body">
|
||||
<section
|
||||
v-motion
|
||||
class="review-summary"
|
||||
aria-label="审核摘要"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0, transition: { delay: 0.08, duration: 0.3 } }"
|
||||
>
|
||||
<div class="risk-ring"><strong>82</strong><span>可信分</span></div>
|
||||
<Column field="applicant" header="申请人">
|
||||
<template #body="{ data }">
|
||||
<strong>{{ data.applicant }}</strong>
|
||||
<p class="sub-text">{{ data.dept }}</p>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="destination" header="目的地 / 行程" />
|
||||
|
||||
<Column field="date" header="申请日期" sortable />
|
||||
|
||||
<Column field="amount" header="金额" sortable>
|
||||
<template #body="{ data }">
|
||||
<strong>{{ data.amount }}</strong>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="status" header="状态">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="data.status" :severity="statusSeverity(data.statusClass)" />
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="conclusion" header="AI 结论">
|
||||
<template #body="{ data }">
|
||||
<span class="conclusion" :class="data.statusClass">{{ data.conclusion }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="操作" style="width: 120px">
|
||||
<template #body="{ data }">
|
||||
<div class="row-actions">
|
||||
<Button label="对话" icon="pi pi-comment" size="small" severity="secondary" rounded @click="openCaseDialog(data)" />
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<!-- AI Travel Assistant Dialog -->
|
||||
<Dialog
|
||||
v-model:visible="dialogOpen"
|
||||
:modal="true"
|
||||
:draggable="false"
|
||||
:closable="true"
|
||||
:header="dialogCase ? dialogCase.type + ' · ' + dialogCase.id : '智能差旅助手'"
|
||||
:style="{ width: '720px' }"
|
||||
:breakpoints="{ '760px': '95vw' }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="dialog-header-custom">
|
||||
<div>
|
||||
<h3>建议有条件通过</h3>
|
||||
<p>发票验真与预算归属通过,当前风险集中在 {{ activeCase?.risk }}。</p>
|
||||
<div class="summary-pills">
|
||||
<span>制度命中 3</span>
|
||||
<span>附件完整度 86%</span>
|
||||
<span>SLA {{ activeCase?.sla }}</span>
|
||||
</div>
|
||||
<span class="eyebrow">AI Travel Assistant</span>
|
||||
<h3>{{ dialogCase ? dialogCase.type + ' · ' + dialogCase.id : '智能差旅助手' }}</h3>
|
||||
<p v-if="dialogCase">{{ dialogCase.applicant }} · {{ dialogCase.destination }} · {{ dialogCase.days }}天</p>
|
||||
</div>
|
||||
</section>
|
||||
<div class="messages" ref="messageList" aria-live="polite">
|
||||
<TransitionGroup name="message-list">
|
||||
<div v-for="message in messages" :key="message.id" class="message" :class="message.role">
|
||||
<span>{{ message.role === 'user' ? 'Reviewer' : 'Finance AI' }}</span>
|
||||
<p>{{ message.text }}</p>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
<aside class="case-panel">
|
||||
<h3>单据上下文</h3>
|
||||
<dl>
|
||||
<div><dt>单据编号</dt><dd>{{ activeCase?.id }}</dd></div>
|
||||
<div><dt>申请人</dt><dd>{{ activeCase?.person }}</dd></div>
|
||||
<div><dt>金额</dt><dd>{{ activeCase?.amount }}</dd></div>
|
||||
<div><dt>风险点</dt><dd>{{ activeCase?.risk }}</dd></div>
|
||||
</dl>
|
||||
<h3>快捷追问</h3>
|
||||
<div class="quick-prompts">
|
||||
<button v-for="prompt in quickPrompts" :key="prompt" class="chip" type="button" @click="emit('draft', prompt)">{{ prompt }}</button>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<!-- Quick Service Cards (for new request) -->
|
||||
<div v-if="!dialogCase" class="service-cards">
|
||||
<button v-for="svc in services" :key="svc.label" class="service-card" @click="applyService(svc)">
|
||||
<i :class="svc.piIcon" class="svc-icon"></i>
|
||||
<strong>{{ svc.label }}</strong>
|
||||
<small>{{ svc.desc }}</small>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<footer class="dialog-foot chat-foot">
|
||||
<textarea v-model="localDraft" placeholder="询问合规问题,例如:这张住宿发票是否超标?需要补哪些材料?" @keydown.ctrl.enter.prevent="emit('send')"></textarea>
|
||||
<button class="btn success" type="button" @click="emit('approveCase')">通过</button>
|
||||
<button class="btn danger" type="button" @click="emit('rejectCase')">转人工</button>
|
||||
<button class="btn primary" type="button" @click="emit('send')">发送</button>
|
||||
</footer>
|
||||
</article>
|
||||
<!-- Case Summary -->
|
||||
<div v-if="dialogCase" class="review-summary">
|
||||
<div class="risk-ring">
|
||||
<strong>{{ dialogCase.status === '已通过' || dialogCase.status === '已完成' ? '96' : '78' }}</strong>
|
||||
<span>合规分</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4>{{ dialogCase.conclusion }}</h4>
|
||||
<p>{{ dialogCase.type }} · {{ dialogCase.destination }} · {{ dialogCase.amount }} · {{ dialogCase.days }}天行程</p>
|
||||
<div class="summary-pills">
|
||||
<span>{{ dialogCase.date }}</span>
|
||||
<span>{{ dialogCase.status }}</span>
|
||||
<span>{{ dialogCase.applicant }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div class="messages" ref="messageList" aria-live="polite">
|
||||
<TransitionGroup name="msg-list">
|
||||
<div v-for="message in messages" :key="message.id" class="message" :class="message.role">
|
||||
<span>{{ message.role === 'user' ? '你' : '差旅助手' }}</span>
|
||||
<p>{{ message.text }}</p>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
<div v-if="!messages.length" class="messages-empty">
|
||||
<p>告诉我您的差旅需求,我来帮您安排 ✈️</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Prompts -->
|
||||
<div class="quick-bar">
|
||||
<Button v-for="p in quickPrompts" :key="p" :label="p" size="small" severity="secondary" rounded outlined @click="applyPrompt(p)" />
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
<div class="dialog-input">
|
||||
<Textarea v-model="localDraft" rows="2" autoResize placeholder="描述您的差旅需求…" @keydown.ctrl.enter.prevent="emit('send')" />
|
||||
<div class="input-actions">
|
||||
<Button label="发送" icon="pi pi-send" size="small" @click="emit('send')" />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { icons } from '../data/icons.js'
|
||||
import { ref, computed } from 'vue'
|
||||
import DataTable from 'primevue/datatable'
|
||||
import Column from 'primevue/column'
|
||||
import Button from 'primevue/button'
|
||||
import Tag from 'primevue/tag'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import Textarea from 'primevue/textarea'
|
||||
|
||||
const props = defineProps({
|
||||
documents: { type: Array, required: true },
|
||||
docSearch: { type: String, default: '' },
|
||||
messages: { type: Array, required: true },
|
||||
uploadedFiles: { type: Array, required: true },
|
||||
activeCase: { type: Object, default: null },
|
||||
@@ -84,84 +160,156 @@ const props = defineProps({
|
||||
messageList: { type: Object, default: null }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['send', 'upload', 'draft', 'approveCase', 'rejectCase', 'update:draft'])
|
||||
const emit = defineEmits(['send', 'upload', 'draft', 'approveCase', 'rejectCase', 'update:draft', 'selectCase'])
|
||||
|
||||
const fileIcon = icons.file
|
||||
const dialogOpen = ref(false)
|
||||
const dialogCase = ref(null)
|
||||
const pageSize = ref(10)
|
||||
|
||||
const services = [
|
||||
{ label: '出差申请', desc: '提交出差申请单', piIcon: 'pi pi-file', prompt: '我需要提交一份出差申请' },
|
||||
{ label: '预订机票', desc: '查询并预订机票', piIcon: 'pi pi-send', prompt: '帮我预订机票' },
|
||||
{ label: '预订酒店', desc: '查询并预订酒店', piIcon: 'pi pi-building', prompt: '帮我预订酒店' },
|
||||
{ label: '预订火车票', desc: '查询并预订火车票', piIcon: 'pi pi-map-marker', prompt: '帮我预订火车票' }
|
||||
]
|
||||
|
||||
import { computed } from 'vue'
|
||||
const localDraft = computed({
|
||||
get: () => props.draft,
|
||||
set: (val) => emit('update:draft', val)
|
||||
})
|
||||
|
||||
function statusSeverity(cls) {
|
||||
if (cls === 'success') return 'success'
|
||||
if (cls === 'danger') return 'danger'
|
||||
return 'warn'
|
||||
}
|
||||
|
||||
function openCaseDialog(doc) {
|
||||
dialogCase.value = doc
|
||||
emit('selectCase', doc)
|
||||
dialogOpen.value = true
|
||||
}
|
||||
|
||||
function applyService(svc) {
|
||||
emit('draft', svc.prompt)
|
||||
}
|
||||
|
||||
function applyPrompt(p) {
|
||||
emit('draft', p)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-view { max-width: 1280px; }
|
||||
/* ── List View ──────────────────────────────────── */
|
||||
.view { display: grid; gap: 22px; animation: fadeUp 220ms var(--ease) both; }
|
||||
.chat-shell { min-height: calc(100dvh - 210px); display: grid; grid-template-rows: auto auto minmax(420px, 1fr) auto; overflow: hidden; padding: 0; }
|
||||
.chat-hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0,1fr) 260px;
|
||||
gap: 18px;
|
||||
align-items: stretch;
|
||||
padding: 24px;
|
||||
.view.full { width: 100%; }
|
||||
.queue-panel {
|
||||
border: 1px solid var(--line); border-radius: var(--radius);
|
||||
background: var(--surface); overflow: hidden;
|
||||
}
|
||||
|
||||
.sub-text { margin-top: 3px; color: var(--muted); font-size: 11px; }
|
||||
|
||||
/* Type tags */
|
||||
.type-tag {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
padding: 3px 10px; border-radius: 999px;
|
||||
font-size: 12px; font-weight: 750; white-space: nowrap;
|
||||
}
|
||||
.type-tag.travel { background: var(--primary-soft); color: var(--primary); }
|
||||
.type-tag.flight { background: #eef3ff; color: #335cff; }
|
||||
.type-tag.hotel { background: var(--warning-soft); color: var(--warning); }
|
||||
.type-tag.train { background: var(--success-soft); color: var(--success); }
|
||||
|
||||
.conclusion { color: var(--muted); font-size: 12px; }
|
||||
.conclusion.success { color: var(--success); }
|
||||
.conclusion.warning { color: var(--warning); }
|
||||
.conclusion.danger { color: var(--danger); }
|
||||
|
||||
.row-actions { display: flex; gap: 6px; }
|
||||
|
||||
/* ── Dialog Custom Header ───────────────────────── */
|
||||
.dialog-header-custom h3 { margin: 4px 0 0; color: var(--ink); font-size: 20px; }
|
||||
.dialog-header-custom p { margin-top: 4px; color: var(--muted); font-size: 12px; }
|
||||
|
||||
/* ── Service Cards ──────────────────────────────── */
|
||||
.service-cards {
|
||||
display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.service-card {
|
||||
display: grid; gap: 6px; place-items: center; text-align: center;
|
||||
padding: 16px 8px; border: 1px solid var(--line); border-radius: var(--radius);
|
||||
background: #fff; cursor: pointer;
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease;
|
||||
}
|
||||
.service-card:hover { border-color: rgba(51,92,255,.28); box-shadow: 0 8px 24px rgba(51,92,255,.08); }
|
||||
.svc-icon { font-size: 22px; color: var(--primary); }
|
||||
.service-card strong { color: var(--ink); font-size: 13px; }
|
||||
.service-card small { color: var(--muted); font-size: 11px; }
|
||||
|
||||
/* ── Summary ────────────────────────────────────── */
|
||||
.review-summary {
|
||||
display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 16px;
|
||||
padding: 14px 0; margin-bottom: 12px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background:
|
||||
radial-gradient(circle at 12% 10%, rgba(51,92,255,.14), transparent 28%),
|
||||
linear-gradient(135deg, #fff, #f7fbff);
|
||||
}
|
||||
.chat-hero h2 { margin-top: 4px; color: var(--ink); font-size: 28px; }
|
||||
.chat-hero p { max-width: 780px; margin-top: 8px; color: var(--muted); line-height: 1.6; }
|
||||
.upload-card {
|
||||
position: relative;
|
||||
min-height: 148px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
align-content: center;
|
||||
gap: 6px;
|
||||
padding: 18px;
|
||||
border: 1px dashed rgba(51,92,255,.36);
|
||||
border-radius: var(--radius);
|
||||
background: rgba(255,255,255,.72);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
transition: transform 180ms var(--ease), border-color 180ms var(--ease), box-shadow 180ms var(--ease);
|
||||
.risk-ring {
|
||||
width: 68px; aspect-ratio: 1; display: grid; place-items: center;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle,#fff 0 55%,transparent 56%), conic-gradient(var(--success) 0 82%, #e4e7ec 82% 100%);
|
||||
}
|
||||
.upload-card:hover { transform: translateY(-2px); border-color: var(--primary); box-shadow: 0 18px 42px rgba(51,92,255,.12); }
|
||||
.upload-card input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
|
||||
.upload-icon { width: 42px; height: 42px; display: grid; place-items: center; border-radius: 12px; background: var(--primary-soft); color: var(--primary); }
|
||||
.upload-icon svg { width: 20px; height: 20px; stroke: currentColor; stroke-width: 2; fill: none; }
|
||||
.upload-card strong { color: var(--ink); }
|
||||
.upload-card small { color: var(--muted); }
|
||||
.upload-list { display: flex; flex-wrap: wrap; gap: 8px; padding: 14px 24px; border-bottom: 1px solid var(--line); background: #fff; }
|
||||
.file-pill { min-height: 30px; display: inline-flex; align-items: center; padding: 0 10px; border-radius: 999px; background: var(--success-soft); color: var(--success); font-size: 12px; font-weight: 750; }
|
||||
.file-pill.muted { background: var(--surface-soft); color: var(--muted); }
|
||||
.dialog-body { min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr) 300px; grid-template-rows: auto minmax(0,1fr); }
|
||||
.chat-body { border-bottom: 1px solid var(--line); }
|
||||
.review-summary { grid-column: 1 / -1; display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 16px; padding: 18px 24px; border-bottom: 1px solid var(--line); background: #fff; }
|
||||
.risk-ring { width: 82px; aspect-ratio: 1; display: grid; place-items: center; border-radius: 50%; background: radial-gradient(circle,#fff 0 55%,transparent 56%), conic-gradient(var(--success) 0 82%, #e4e7ec 82% 100%); }
|
||||
.risk-ring strong { color: var(--ink); font-size: 24px; line-height: 1; }
|
||||
.risk-ring span { color: var(--muted); font-size: 11px; }
|
||||
.review-summary h3 { color: var(--ink); }
|
||||
.review-summary p { margin-top: 5px; color: var(--muted); }
|
||||
.summary-pills { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
||||
.summary-pills span { min-height: 28px; display: inline-flex; align-items: center; padding: 0 10px; border-radius: 999px; background: var(--primary-soft); color: var(--primary); font-size: 12px; font-weight: 750; }
|
||||
.messages { min-height: 0; overflow: auto; display: grid; align-content: start; gap: 12px; padding: 22px 24px; background: linear-gradient(180deg,#fbfcff,#f6f8fb); }
|
||||
.message { max-width: 82%; display: grid; gap: 6px; will-change: transform, opacity; }
|
||||
.risk-ring strong { color: var(--ink); font-size: 20px; line-height: 1; }
|
||||
.risk-ring span { color: var(--muted); font-size: 10px; }
|
||||
.review-summary h4 { color: var(--ink); font-size: 14px; }
|
||||
.review-summary p { margin-top: 3px; color: var(--muted); font-size: 13px; }
|
||||
.summary-pills { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
||||
.summary-pills span {
|
||||
min-height: 24px; display: inline-flex; align-items: center;
|
||||
padding: 0 8px; border-radius: 999px;
|
||||
background: var(--primary-soft); color: var(--primary); font-size: 11px; font-weight: 750;
|
||||
}
|
||||
|
||||
/* ── Messages ───────────────────────────────────── */
|
||||
.messages {
|
||||
min-height: 160px; max-height: 360px; overflow-y: auto;
|
||||
display: grid; align-content: start; gap: 12px;
|
||||
padding: 14px 0; background: linear-gradient(180deg, #fbfcff, #f6f8fb);
|
||||
border-radius: var(--radius); margin-bottom: 12px;
|
||||
}
|
||||
.message { max-width: 82%; display: grid; gap: 4px; }
|
||||
.message.user { justify-self: end; }
|
||||
.message span { color: var(--muted); font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
|
||||
.message p { padding: 13px 15px; border: 1px solid var(--line); border-radius: 16px 16px 16px 6px; background: #fff; box-shadow: 0 8px 24px rgba(16,24,40,.05); }
|
||||
.message.user p { border-color: transparent; border-radius: 16px 16px 6px 16px; background: linear-gradient(135deg,var(--primary),#2446d8); color: #fff; box-shadow: 0 14px 30px rgba(51,92,255,.20); }
|
||||
.case-panel { overflow: auto; padding: 22px; border-left: 1px solid var(--line); background: rgba(255,255,255,.72); }
|
||||
.case-panel h3 { margin: 0 0 12px; color: var(--ink); }
|
||||
dl { margin: 0 0 18px; display: grid; gap: 10px; }
|
||||
dl div { display: flex; justify-content: space-between; gap: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--line); }
|
||||
dt { color: var(--muted); font-size: 12px; }
|
||||
dd { margin: 0; color: var(--ink); font-weight: 750; text-align: right; }
|
||||
.quick-prompts { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.chip { min-height: 34px; padding: 0 10px; border: 1px solid var(--line); border-radius: 999px; background: #fff; color: var(--text); font-size: 12px; font-weight: 700; transition: transform 160ms var(--ease), border-color 160ms var(--ease), background 160ms var(--ease); }
|
||||
.chip:hover { border-color: rgba(51,92,255,.28); background: var(--primary-soft); color: var(--primary); }
|
||||
.dialog-foot { display: grid; grid-template-columns: minmax(0,1fr) auto auto auto; gap: 10px; padding: 16px 20px; border-top: 1px solid var(--line); background: #fff; }
|
||||
.chat-foot { border-top: 0; }
|
||||
textarea { min-height: 48px; max-height: 116px; resize: vertical; padding: 12px; border: 1px solid var(--line); border-radius: var(--radius); }
|
||||
.message span { color: var(--muted); font-size: 10px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
|
||||
.message p {
|
||||
padding: 12px 14px; border: 1px solid var(--line);
|
||||
border-radius: 14px 14px 14px 4px; background: #fff;
|
||||
font-size: 13px; line-height: 1.6; white-space: pre-line;
|
||||
}
|
||||
.message.user p {
|
||||
border-color: transparent; border-radius: 14px 14px 4px 14px;
|
||||
background: linear-gradient(135deg, var(--primary), #2446d8);
|
||||
color: #fff;
|
||||
}
|
||||
.messages-empty { display: grid; place-items: center; padding: 32px; }
|
||||
.messages-empty p { color: var(--muted); font-size: 13px; }
|
||||
.msg-list-enter-active { transition: opacity 200ms var(--ease), transform 200ms var(--ease); }
|
||||
.msg-list-leave-active { transition: opacity 120ms ease; }
|
||||
.msg-list-enter-from { opacity: 0; transform: translateY(6px); }
|
||||
.msg-list-leave-to { opacity: 0; }
|
||||
|
||||
/* ── Quick Prompts ──────────────────────────────── */
|
||||
.quick-bar { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
|
||||
|
||||
/* ── Input ──────────────────────────────────────── */
|
||||
.dialog-input {
|
||||
display: grid; grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px; align-items: start;
|
||||
}
|
||||
.input-actions { display: grid; gap: 6px; padding-top: 2px; }
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.dialog-input { grid-template-columns: 1fr; }
|
||||
.input-actions { display: flex; }
|
||||
.service-cards { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
</style>
|
||||
|
||||
497
src/views/LoginView.vue
Normal file
497
src/views/LoginView.vue
Normal file
@@ -0,0 +1,497 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<!-- Left: Enterprise Brand Hero -->
|
||||
<aside class="hero">
|
||||
<div class="hero-grid"></div>
|
||||
<div class="hero-content">
|
||||
<div class="brand">
|
||||
<div class="logo-box">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2L2 7L12 12L22 7L12 2Z" fill="currentColor"/>
|
||||
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="brand-name">X-Financial Ops</span>
|
||||
</div>
|
||||
|
||||
<header class="hero-header">
|
||||
<h1>智能财务报销<br/><span class="text-gradient">全流程运营中台</span></h1>
|
||||
<p>基于 AI Agent 的合规预审与自动化处理引擎,助力企业财务实现数字化治理与运营效率的跨越式提升。</p>
|
||||
</header>
|
||||
|
||||
<div class="value-prop">
|
||||
<div v-for="item in features" :key="item.title" class="prop-item">
|
||||
<div class="prop-icon" :style="{ color: item.color }">
|
||||
<component :is="item.icon" />
|
||||
</div>
|
||||
<div class="prop-text">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<p>{{ item.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-footer">
|
||||
<div class="trust-badge">
|
||||
<div class="avatars">
|
||||
<div v-for="i in 3" :key="i" class="avatar-circle"></div>
|
||||
</div>
|
||||
<span>已为 500+ 大中型企业提供智能化财务转型支持</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview UI Element: Simplified Dashboard Part -->
|
||||
<div class="hero-preview">
|
||||
<div class="preview-card">
|
||||
<div class="preview-header">
|
||||
<div class="dots"><span></span><span></span><span></span></div>
|
||||
<div class="preview-title">系统合规性看板</div>
|
||||
</div>
|
||||
<div class="preview-body">
|
||||
<div class="preview-stat">
|
||||
<div class="stat-circle">
|
||||
<svg viewBox="0 0 36 36">
|
||||
<path class="circle-bg" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
|
||||
<path class="circle" stroke-dasharray="82, 100" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
|
||||
</svg>
|
||||
<div class="stat-val">82%</div>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<strong>综合自动通过率</strong>
|
||||
<p>较上月提升 12.4%</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-list">
|
||||
<div v-for="i in 3" :key="i" class="list-item">
|
||||
<div class="item-bar" :style="{ width: [85, 62, 45][i-1] + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Right: Login Form (Refined) -->
|
||||
<main class="login-main">
|
||||
<div class="login-form-wrap">
|
||||
<header class="login-header">
|
||||
<h2>系统登录</h2>
|
||||
<p>请输入您的凭据以访问控制台</p>
|
||||
</header>
|
||||
|
||||
<form class="login-form" @submit.prevent="emit('login', { username, password })">
|
||||
<div class="input-group">
|
||||
<label>用户名</label>
|
||||
<div class="input-control">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
<input v-model="username" type="text" placeholder="账户名称 / 手机号" required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>密码</label>
|
||||
<div class="input-control">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
<input v-model="password" type="password" placeholder="请输入登录密码" required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<label class="remember-me">
|
||||
<input type="checkbox" v-model="remember" />
|
||||
<span>记住我</span>
|
||||
</label>
|
||||
<a href="#" class="forgot-link">忘记密码?</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="submit-btn">
|
||||
<span>登录系统</span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<footer class="login-footer">
|
||||
<p>© 2026 X-Financial Technology. All Rights Reserved.</p>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, markRaw } from 'vue'
|
||||
|
||||
const emit = defineEmits(['login'])
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const remember = ref(false)
|
||||
|
||||
const ShieldCheck = {
|
||||
template: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="m9 12 2 2 4-4"/></svg>`
|
||||
}
|
||||
const Activity = {
|
||||
template: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>`
|
||||
}
|
||||
const Zap = {
|
||||
template: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>`
|
||||
}
|
||||
|
||||
const features = [
|
||||
{ title: '多维合规引擎', desc: '内置 500+ 企业财务制度,实现毫秒级自动预审。', color: '#3b82f6', icon: markRaw(ShieldCheck) },
|
||||
{ title: '全链路风控监控', desc: '覆盖票据验真、重复报销及异常交易的深度挖掘。', color: '#10b981', icon: markRaw(Activity) },
|
||||
{ title: '流程自动化编排', desc: 'AI Agent 驱动的自动分类与审批建议,节省 80% 人力。', color: '#f59e0b', icon: markRaw(Zap) }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ── Main Layout ─────────────────────────────────── */
|
||||
.login-page {
|
||||
min-height: 100dvh;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(600px, 1.2fr) minmax(400px, 0.8fr);
|
||||
background: #fff;
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
|
||||
/* ── Hero Side ───────────────────────────────────── */
|
||||
.hero {
|
||||
position: relative;
|
||||
background: #0f172a; /* Slate 900 */
|
||||
color: #f8fafc;
|
||||
padding: 60px 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(59, 130, 246, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(59, 130, 246, 0.05) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
mask-image: radial-gradient(circle at 20% 30%, black, transparent 70%);
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.logo-box {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #3b82f6;
|
||||
border-radius: 10px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
.logo-box svg { width: 24px; height: 24px; }
|
||||
.brand-name {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hero-header h1 {
|
||||
font-size: 42px;
|
||||
line-height: 1.2;
|
||||
font-weight: 800;
|
||||
margin-bottom: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
.text-gradient {
|
||||
background: linear-gradient(135deg, #60a5fa, #3b82f6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.hero-header p {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.value-prop {
|
||||
display: grid;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.prop-item {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.prop-icon {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.prop-icon svg { width: 100%; height: 100%; }
|
||||
|
||||
.prop-text strong {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
color: #f1f5f9;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.prop-text p {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.hero-footer {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
.trust-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 99px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.avatars {
|
||||
display: flex;
|
||||
}
|
||||
.avatar-circle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: #334155;
|
||||
border: 2px solid #0f172a;
|
||||
margin-left: -8px;
|
||||
}
|
||||
.avatar-circle:first-child { margin-left: 0; }
|
||||
|
||||
.trust-badge span {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── Hero Preview Element ────────────────────────── */
|
||||
.hero-preview {
|
||||
position: absolute;
|
||||
right: -40px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 440px;
|
||||
opacity: 0.8;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
background: #1e293b;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 40px 80px rgba(0,0,0,0.4);
|
||||
overflow: hidden;
|
||||
animation: floatPreview 6s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes floatPreview {
|
||||
from { transform: translateY(0); }
|
||||
to { transform: translateY(-20px); }
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
padding: 12px 16px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.dots { display: flex; gap: 6px; }
|
||||
.dots span { width: 8px; height: 8px; border-radius: 50%; background: #334155; }
|
||||
.preview-title { font-size: 11px; color: #94a3b8; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
|
||||
.preview-body { padding: 24px; }
|
||||
|
||||
.preview-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-circle {
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
.stat-circle svg { transform: rotate(-90deg); }
|
||||
.circle-bg { fill: none; stroke: #334155; stroke-width: 3; }
|
||||
.circle { fill: none; stroke: #3b82f6; stroke-width: 3; stroke-linecap: round; }
|
||||
.stat-val {
|
||||
position: absolute; inset: 0; display: grid; place-items: center;
|
||||
font-size: 14px; font-weight: 800; color: #fff;
|
||||
}
|
||||
|
||||
.stat-info strong { display: block; font-size: 16px; color: #fff; }
|
||||
.stat-info p { font-size: 12px; color: #10b981; margin-top: 2px; }
|
||||
|
||||
.preview-list { display: grid; gap: 12px; }
|
||||
.list-item { height: 12px; background: #334155; border-radius: 6px; overflow: hidden; }
|
||||
.item-bar { height: 100%; background: linear-gradient(90deg, #3b82f6, #60a5fa); border-radius: inherit; }
|
||||
|
||||
/* ── Login Main ──────────────────────────────────── */
|
||||
.login-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.login-form-wrap {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.login-header h2 {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.login-header p {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.input-control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-control .icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.input-control input {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
padding-left: 40px;
|
||||
padding-right: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.input-control input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.remember-me {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
font-size: 13px;
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background: #0f172a;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background: #1e293b;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.submit-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
margin-top: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
.login-footer p {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.hero { display: none; }
|
||||
.login-page { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user