生成具有像素级设计品质的企业级可交互原型。
> 一份构建专业 H5 Demo 的完整指南,帮助你打动客户和利益相关者。
当你需要创建以下内容时使用此技能:
| 等级 | 特点 | 适用场景 |
|------|------|----------|
| 线框图 | 基本布局、占位数据 | 概念讨论 |
| 中保真 | 已样式化 UI、最少交互 | 内部评审 |
| 高保真 | 像素级精确、完整交互、真实数据 | 客户演示、利益相关者汇报 |
本技能聚焦于高保真质量。
Vue 3 CDN + Vue Router 4 + localStorage + Font Awesome 6
<!-- Vue 3(优先使用国内 CDN) -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<!-- 国内镜像(备选) -->
<!-- <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.4.21/vue.global.prod.min.js"></script> -->
<!-- Vue Router -->
<script src="https://unpkg.com/vue-router@4/dist/vue-router.global.prod.js"></script>
<!-- 国内镜像(备选) -->
<!-- <script src="https://cdn.bootcdn.net/ajax/libs/vue-router/4.3.0/vue-router.global.prod.min.js"></script> -->
<!-- Font Awesome 图标库 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- 国内镜像(备选) -->
<!-- <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.5.1/css/all.min.css"> -->
离线环境解决方案:
如果网络受限,可以将上述文件下载到本地 vendor/ 目录:
vendor/
├── vue.global.prod.js
├── vue-router.global.prod.js
└── fontawesome.all.min.css
然后修改引用:
<script src="vendor/vue.global.prod.js"></script>
<script src="vendor/vue-router.global.prod.js"></script>
<link rel="stylesheet" href="vendor/fontawesome.all.min.css">
技术栈优势:
project/
├── index.html # 入口:角色选择页(可选)
├── h5.html # H5 移动端 SPA(多角色合一)
├── admin.html # PC 管理端 SPA(可选)
├── exec.html # 高管驾驶舱 SPA(可选)
├── css/
│ ├── variables.css # 设计令牌(颜色/字号/间距/阴影/动效)
│ ├── base.css # 全局重置、按钮、卡片、标签、动画
│ ├── h5.css # H5 端专用样式
│ ├── admin.css # PC 管理端专用样式
│ └── exec.css # 高管端专用样式
└── js/
├── store.js # 数据管理层(Store + DataManager + 种子数据)
└── utils.js # 工具函数(日期/格式化/AI模拟)
:root {
/* === 品牌色 === */
--color-primary: #07C160; /* 微信绿 */
--color-primary-light: #38D97A;
--color-primary-dark: #06AD56;
--color-primary-bg: rgba(7, 193, 96, 0.08);
/* === 辅色 === */
--color-secondary: #576B95;
--color-secondary-bg: rgba(87, 107, 149, 0.08);
/* === 功能色 === */
--color-success: #07C160;
--color-warning: #FAAD14;
--color-warning-orange: #FA8C16;
--color-danger: #FF4D4F;
--color-info: #1890FF;
/* === 预警三级色 === */
--color-alert-yellow: #FAAD14;
--color-alert-yellow-bg: rgba(250, 173, 20, 0.1);
--color-alert-orange: #FA8C16;
--color-alert-orange-bg: rgba(250, 140, 22, 0.1);
--color-alert-red: #FF4D4F;
--color-alert-red-bg: rgba(255, 77, 79, 0.1);
/* === 文字色 === */
--color-text-primary: #1A1A1A;
--color-text-secondary: #666666;
--color-text-tertiary: #999999;
--color-text-placeholder: #CCCCCC;
/* === 背景色 === */
--color-bg-page: #F5F6FA;
--color-bg-card: #FFFFFF;
--color-bg-hover: #F7F8FA;
--color-bg-overlay: rgba(0, 0, 0, 0.5);
/* === 边框色 === */
--color-border: #E8E8EC;
--color-border-light: #F0F0F3;
/* === 高管紫色系 === */
--color-exec-primary: #6C5CE7;
--color-exec-primary-light: #A29BFE;
/* === 字体 === */
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
--font-size-h1: 24px;
--font-size-h2: 20px;
--font-size-h3: 17px;
--font-size-body: 15px;
--font-size-caption: 13px;
--font-size-mini: 11px;
/* === 间距(4px 基准) === */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 12px;
--space-base: 16px;
--space-lg: 20px;
--space-xl: 24px;
--space-2xl: 32px;
/* === 圆角 === */
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-xl: 18px;
--radius-full: 9999px;
/* === 阴影 === */
--shadow-card: 0 1px 3px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.03);
--shadow-card-hover: 0 8px 28px rgba(0,0,0,0.09), 0 2px 8px rgba(0,0,0,0.04);
--shadow-float: 0 12px 36px rgba(0,0,0,0.15);
/* === 动效 === */
--duration-fast: 150ms;
--duration-normal: 250ms;
--duration-slow: 350ms;
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
/* === 布局尺寸 === */
--header-height: 56px;
--tabbar-height: 50px;
--sidebar-width: 220px;
}
在 variables.css 之后引入:
const STORAGE_PREFIX = 'demo_'; // 每个项目使用唯一前缀
const Store = {
get(key) {
try {
const data = localStorage.getItem(STORAGE_PREFIX + key);
return data ? JSON.parse(data) : null;
} catch (e) { return null; }
},
set(key, value) {
try {
localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(value));
return true;
} catch (e) { return false; }
},
remove(key) { localStorage.removeItem(STORAGE_PREFIX + key); },
clear() {
const keys = [];
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
if (k && k.startsWith(STORAGE_PREFIX)) keys.push(k);
}
keys.forEach(k => localStorage.removeItem(k));
}
};
function genId(prefix = '') {
return prefix + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
}
// ========== 种子数据生成函数 ==========
function genDate(daysOffset = 0) {
const d = new Date();
d.setDate(d.getDate() + daysOffset);
return d.toISOString().slice(0, 10);
}
function genTime() {
const h = String(Math.floor(Math.random() * 12) + 8).padStart(2, '0');
const m = String(Math.floor(Math.random() * 60)).padStart(2, '0');
return `${h}:${m}`;
}
const DataManager = {
init() {
if (!Store.get('initialized')) {
this.seedAll();
Store.set('initialized', true);
}
},
// 重置所有数据(从 Profile 页面调用)
reset() {
Store.clear();
this.seedAll();
Store.set('initialized', true);
// 通知页面刷新
window.dispatchEvent(new CustomEvent('data-reset'));
},
// ========== 完整的种子数据示例 ==========
seedAll() {
// 当前用户
Store.set('current_user', {
id: 'user001',
name: '张明',
role: 'employee',
department: '产品研发部',
avatar: '张'
});
// 任务列表(3 个任务,涵盖不同状态)
Store.set('tasks', [
{
id: 'task001',
title: '完成产品需求文档 V2.0',
description: '整理第四季度需求,优化用户体验',
progress: 75,
status: 'in_progress',
priority: 'high',
deadline: genDate(3),
assignee: '张明',
createdAt: genDate(-5)
},
{
id: 'task002',
title: '用户调研报告整理',
description: '汇总 20 份用户访谈记录',
progress: 100,
status: 'completed',
priority: 'medium',
deadline: genDate(-1),
assignee: '张明',
createdAt: genDate(-10)
},
{
id: 'task003',
title: '竞品分析更新',
description: '跟踪竞品动态,更新功能对比表',
progress: 30,
status: 'in_progress',
priority: 'low',
deadline: genDate(7),
assignee: '张明',
createdAt: genDate(-2)
}
]);
// 报告列表(2 份报告)
Store.set('reports', [
{
id: 'report001',
title: 'Q4 产品规划报告',
type: 'planning',
score: 92,
createdAt: genDate(-3) + ' ' + genTime(),
author: '张明'
},
{
id: 'report002',
title: '用户增长分析月报',
type: 'analysis',
score: 85,
createdAt: genDate(-7) + ' ' + genTime(),
author: '张明'
}
]);
// 预警列表(3 条预警,黄/橙/红各一)
Store.set('alerts', [
{
id: 'alert001',
level: 'warning', // yellow
title: '项目进度预警',
message: '产品需求文档距截止日期还有 3 天',
createdAt: genDate(0) + ' ' + genTime()
},
{
id: 'alert002',
level: 'orange', // orange
title: '数据异常通知',
message: '昨日活跃用户数较上周同期下降 15%',
createdAt: genDate(-1) + ' ' + genTime()
},
{
id: 'alert003',
level: 'danger', // red
title: '紧急:服务器负载过高',
message: 'CPU 使用率持续超过 90%,建议扩容',
createdAt: genDate(0) + ' ' + genTime()
}
]);
// 审批列表(2 条待审批)
Store.set('approvals', [
{
id: 'approval001',
title: '差旅报销申请',
amount: 2850.00,
currency: '¥',
applicant: '李华',
department: '市场部',
status: 'pending',
type: 'expense',
createdAt: genDate(-2) + ' ' + genTime()
},
{
id: 'approval002',
title: '请假申请',
duration: '3天',
startDate: genDate(5),
applicant: '王芳',
department: '产品部',
status: 'pending',
type: 'leave',
createdAt: genDate(-1) + ' ' + genTime()
}
]);
},
// ========== CRUD 操作 ==========
getCurrentUser() { return Store.get('current_user'); },
setCurrentUser(user) { Store.set('current_user', user); },
getTasks() { return Store.get('tasks') || []; },
saveTask(task) {
const tasks = this.getTasks();
const idx = tasks.findIndex(t => t.id === task.id);
if (idx >= 0) tasks[idx] = task;
else tasks.push(task);
Store.set('tasks', tasks);
},
getReports() { return Store.get('reports') || []; },
saveReport(report) {
const reports = this.getReports();
const idx = reports.findIndex(r => r.id === report.id);
if (idx >= 0) reports[idx] = report;
else reports.push(report);
Store.set('reports', reports);
},
getAlerts() { return Store.get('alerts') || []; },
getApprovals() { return Store.get('approvals') || []; }
};
const Utils = {
// 打字机效果(用于 AI 模拟)
typeWriter(text, callback, speed = 30) {
let i = 0;
const timer = setInterval(() => {
if (i < text.length) {
callback(text.slice(0, i + 1), i === text.length - 1);
i++;
} else {
clearInterval(timer);
}
}, speed);
return timer;
},
// Promise 延迟
sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); },
// ========== 内置 AI 模拟实现 ==========
// 基于关键词匹配返回预设场景
// 预设场景定义
scenarios: [
{
keywords: ['报销', '差旅', '发票', '费用'],
intent: 'expense',
icon: 'fa-receipt',
color: '#07C160',
title: '差旅报销',
subtitle: '已识别为差旅费用类请求',
result: {
type: 'form',
title: '新建差旅报销',
fields: ['出发地', '目的地', '日期范围', '金额', '事由']
},
actions: [
{ label: '填写报销单', type: 'primary', action: 'fill_expense' },
{ label: '查看历史', type: 'outline', action: 'view_history' }
]
},
{
keywords: ['请假', '休息', '度假', '假期'],
intent: 'leave',
icon: 'fa-plane',
color: '#1890FF',
title: '请假申请',
subtitle: '已识别为请假类请求',
result: {
type: 'form',
title: '新建请假申请',
fields: ['请假类型', '开始日期', '结束日期', '事由']
},
actions: [
{ label: '填写请假单', type: 'primary', action: 'fill_leave' },
{ label: '查看剩余假期', type: 'outline', action: 'view_leave_balance' }
]
},
{
keywords: ['数据', '分析', '报表', '统计'],
intent: 'report',
icon: 'fa-chart-bar',
color: '#722ED1',
title: '数据查询',
subtitle: '已识别为数据查询类请求',
result: {
type: 'list',
title: '相关数据报告',
items: ['用户增长趋势', '收入分析报告', '运营周报']
},
actions: [
{ label: '查看报告', type: 'primary', action: 'view_reports' },
{ label: '生成新报告', type: 'outline', action: 'create_report' }
]
},
{
keywords: ['任务', '待办', '分配', '指派'],
intent: 'task',
icon: 'fa-tasks',
color: '#FAAD14',
title: '任务管理',
subtitle: '已识别为任务管理类请求',
result: {
type: 'task_panel',
title: '我的任务',
counts: { pending: 3, inProgress: 2, completed: 8 }
},
actions: [
{ label: '查看任务列表', type: 'primary', action: 'view_tasks' },
{ label: '创建新任务', type: 'outline', action: 'create_task' }
]
},
{
keywords: ['审批', '通过', '拒绝', '确认'],
intent: 'approval',
icon: 'fa-check-double',
color: '#FA8C16',
title: '审批流程',
subtitle: '已识别为审批类请求',
result: {
type: 'approval_panel',
title: '待我审批',
count: 5
},
actions: [
{ label: '查看待审批', type: 'primary', action: 'view_approvals' },
{ label: '审批历史', type: 'outline', action: 'view_approval_history' }
]
}
],
// AI 场景模拟
simulateAI(input) {
const lowerInput = input.toLowerCase();
// 遍历场景,匹配关键词
for (const scenario of this.scenarios) {
for (const keyword of scenario.keywords) {
if (lowerInput.includes(keyword)) {
return { ...scenario, matchedKeyword: keyword };
}
}
}
// 默认场景(未匹配到任何关键词)
return {
intent: 'general',
icon: 'fa-robot',
color: '#576B95',
title: '智能助手',
subtitle: '正在处理您的请求...',
result: {
type: 'text',
content: `已收到您的请求:"${input}",正在分析中...`
},
actions: [
{ label: '重新描述', type: 'outline', action: 'rephrase' }
]
};
},
// 报告/数据集评分
scoreDataset(content) {
// 模拟评分逻辑
const standard = Math.floor(Math.random() * 15) + 80;
const completion = Math.floor(Math.random() * 20) + 75;
const quality = Math.floor(Math.random() * 20) + 78;
return {
standard,
completion,
quality,
total: Math.round((standard + completion + quality) / 3)
};
},
// 数字动画
animateNumber(el, target, duration = 800) {
const start = parseInt(el.textContent) || 0;
const startTime = performance.now();
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
el.textContent = Math.round(start + (target - start) * eased);
if (progress < 1) requestAnimationFrame(update);
}
requestAnimationFrame(update);
},
// 进度环 SVG
progressRingSVG(progress, size = 72, stroke = 6, color = '#07C160') {
const r = (size - stroke) / 2;
const c = Math.PI * 2 * r;
const offset = c * (1 - progress / 100);
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
<circle cx="${size/2}" cy="${size/2}" r="${r}" fill="none" stroke="#F0F0F3" stroke-width="${stroke}"/>
<circle cx="${size/2}" cy="${size/2}" r="${r}" fill="none" stroke="${color}" stroke-width="${stroke}"
stroke-linecap="round" stroke-dasharray="${c}" stroke-dashoffset="${offset}"
transform="rotate(-90 ${size/2} ${size/2})"
style="transition: stroke-dashoffset 0.8s cubic-bezier(0.22,1,0.36,1)"/>
<text x="${size/2}" y="${size/2}" text-anchor="middle" dominant-baseline="central"
font-size="${size/4}" font-weight="700" fill="${color}">${progress}%</text>
</svg>`;
}
};
window.Utils = Utils;
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>应用名称</title>
<!-- CSS 文件(按顺序加载) -->
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="css/h5.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>[v-cloak] { display: none !important; }</style>
</head>
<body>
<div id="app" v-cloak>
<!-- 角色选择遮罩 -->
<div v-if="!role" class="role-select-overlay">
<div class="role-select-title">应用名称</div>
<div class="role-select-subtitle">请选择您的角色</div>
<div class="role-select-cards">
<div class="role-select-card employee" @click="selectRole('employee')">
<div class="role-icon"><i class="fas fa-user"></i></div>
<div class="role-info">
<div class="role-name">员工</div>
<div class="role-desc">员工自助服务入口</div>
</div>
</div>
<div class="role-select-card manager" @click="selectRole('manager')">
<div class="role-icon"><i class="fas fa-user-tie"></i></div>
<div class="role-info">
<div class="role-name">经理</div>
<div class="role-desc">团队管理入口</div>
</div>
</div>
</div>
</div>
<!-- 主应用 -->
<div v-else class="h5-app">
<router-view></router-view>
<!-- TabBar -->
<div v-if="currentPageMeta?.showTabbar !== false" class="h5-tabbar">
<div v-for="tab in tabs" :key="tab.path"
class="h5-tabbar-item" :class="{ active: isActive(tab.path) }"
@click="navigate(tab.path)">
<i class="h5-tabbar-item-icon" :class="tab.icon"></i>
<span class="h5-tabbar-item-label">{{ tab.label }}</span>
<span v-if="tab.badge" class="badge">{{ tab.badge }}</span>
</div>
</div>
</div>
</div>
<!-- JS 依赖 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/vue-router@4/dist/vue-router.global.prod.js"></script>
<script src="js/store.js"></script>
<script src="js/utils.js"></script>
<script>
// 初始化数据
DataManager.init();
// ========== TabBar 配置 ==========
const tabs = [
{ path: '/', icon: 'fas fa-home', label: '首页' },
{ path: '/tasks', icon: 'fas fa-list-check', label: '任务' },
{ path: '/reports', icon: 'fas fa-file-lines', label: '报告' },
{ path: '/approvals', icon: 'fas fa-check-double', label: '审批', badge: null },
{ path: '/profile', icon: 'fas fa-user', label: '我的' }
];
// ========== 页面组件(内联示例)==========
const HomePage = {
setup() {
const tasks = ref(DataManager.getTasks());
const alerts = ref(DataManager.getAlerts());
// 监听数据重置
onMounted(() => {
window.addEventListener('data-reset', () => {
tasks.value = DataManager.getTasks();
alerts.value = DataManager.getAlerts();
});
});
return { tasks, alerts };
},
template: `
<div class="h5-page">
<div class="h5-navbar">
<span class="h5-navbar-title">首页</span>
</div>
<!-- 预警栏 -->
<div v-for="alert in alerts.slice(0, 1)" :key="alert.id"
class="h5-alert-bar"
:class="'h5-alert-' + alert.level">
<i :class="'fas fa-exclamation-circle'"></i>
<span>{{ alert.title }}</span>
</div>
<!-- 任务卡片 -->
<div class="section-title">我的任务</div>
<div v-for="task in tasks" :key="task.id" class="card" style="margin-bottom:12px">
<div class="card-body">
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="font-weight:600">{{ task.title }}</span>
<span class="tag" :class="'tag-' + (task.status === 'completed' ? 'success' : 'default')">
{{ task.status === 'completed' ? '已完成' : '进行中' }}
</span>
</div>
<div class="progress-bar" style="margin-top:8px">
<div class="progress-bar-fill" :style="{ width: task.progress + '%' }"></div>
</div>
</div>
</div>
</div>
`
};
// 更多页面组件...
// ========== 路由配置(含权限守卫)==========
const routes = [
{ path: '/', component: HomePage, meta: { title: '首页', showTabbar: true, role: 'employee' } },
{ path: '/tasks', component: { template: '<div class="h5-page"><div class="h5-navbar"><span class="h5-navbar-title">任务</span></div><div class="empty-state"><i class="fas fa-tasks empty-state-icon"></i><div class="empty-state-text">任务列表</div></div></div>' }, meta: { title: '任务', showTabbar: true, role: 'employee' } },
{ path: '/reports', component: { template: '<div class="h5-page"><div class="h5-navbar"><span class="h5-navbar-title">报告</span></div><div class="empty-state"><i class="fas fa-file-lines empty-state-icon"></i><div class="empty-state-text">报告列表</div></div></div>' }, meta: { title: '报告', showTabbar: true, role: 'employee' } },
{ path: '/approvals', component: { template: '<div class="h5-page"><div class="h5-navbar"><span class="h5-navbar-title">审批</span></div><div class="empty-state"><i class="fas fa-check-double empty-state-icon"></i><div class="empty-state-text">审批列表</div></div></div>' }, meta: { title: '审批', showTabbar: true, role: 'employee' } },
{ path: '/profile', component: { template: '<div class="h5-page"><div class="h5-navbar"><span class="h5-navbar-title">我的</span></div><div class="card" style="margin-top:16px"><div class="card-body"><div style="text-align:center;margin-bottom:16px"><div style="width:64px;height:64px;background:var(--color-primary-bg);color:var(--color-primary);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:24px;margin:0 auto">{{ DataManager.getCurrentUser().avatar }}</div><div style="font-weight:600;margin-top:8px">{{ DataManager.getCurrentUser().name }}</div><div style="color:var(--color-text-secondary);font-size:13px">{{ DataManager.getCurrentUser().department }}</div></div><button class="btn btn-outline btn-block" @click="handleReset"><i class="fas fa-redo"></i> 重置演示数据</button></div></div></div>' }, meta: { title: '我的', showTabbar: true, role: 'employee' } }
];
const router = createRouter({
history: createWebHashHistory(),
routes
});
// ========== 完整的路由守卫(权限控制)==========
router.beforeEach((to, from, next) => {
const role = localStorage.getItem('demo_role');
// 如果没有选择角色,只能访问根路径
if (!role) {
if (to.path === '/') {
next();
} else {
next('/');
}
return;
}
// 如果已选择角色但访问选择页,重定向
if (to.path === '/' && role) {
next();
return;
}
// 检查路由权限
const routeRole = to.meta.role;
if (routeRole && routeRole !== role) {
// 角色不匹配,重定向到首页
next('/');
return;
}
next();
});
// ========== Vue 应用 ==========
const app = createApp({
setup() {
const role = ref(localStorage.getItem('demo_role') || '');
const currentPageMeta = ref({});
const selectRole = (r) => {
localStorage.setItem('demo_role', r);
role.value = r;
};
const isActive = (path) => {
return router.currentRoute.value.path === path;
};
const navigate = (path) => {
router.push(path);
};
// 监听路由变化更新 TabBar 状态
watch(() => router.currentRoute.value, (route) => {
currentPageMeta.value = route.meta || {};
}, { immediate: true });
// 重置数据
const handleReset = () => {
if (confirm('确定要重置所有演示数据吗?')) {
DataManager.reset();
alert('数据已重置');
}
};
return { role, currentPageMeta, selectRole, isActive, navigate, tabs, handleReset, DataManager };
}
});
app.use(router);
app.mount('#app');
</script>
</body>
</html>
<div class="card" style="margin-bottom:12px">
<div class="card-body">
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="font-weight:600">{{ item.title }}</span>
<span class="tag" :class="statusTagClass">{{ statusText }}</span>
</div>
<div class="progress-bar" style="margin-top:8px">
<div class="progress-bar-fill" :style="{ width: item.progress + '%' }"></div>
</div>
<div style="display:flex;justify-content:space-between;font-size:12px;color:#999;margin-top:6px">
<span><i class="fas fa-clock"></i> {{ item.deadline }}</span>
<span>{{ item.progress }}%</span>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px">
<div :style="{ width:'40px',height:'40px',borderRadius:'10px',background:`${color}20`,display:'flex',alignItems:'center',justifyContent:'center' }">
<i :class="icon" :style="{ color, fontSize:'20px' }"></i>
</div>
<div style="flex:1">
<div style="font-weight:600">{{ title }}</div>
<div style="font-size:12px;color:#999">{{ subtitle }}</div>
</div>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button v-for="action in actions" :key="action.label"
class="btn btn-sm" :class="action.type || 'btn-outline'">
{{ action.label }}
</button>
</div>
</div>
</div>
const openDetail = (item, type) => {
selectedItem.value = item;
selectedItemType.value = type;
showDetailModal.value = true;
// 自动生成 AI 分析
generateAIAnalysis(type, item);
};
const closeDetail = () => {
showDetailModal.value = false;
selectedItem.value = null;
aiAnalysis.value = null;
};
<div class="h5-alert-bar" :class="'h5-alert-' + alert.level">
<i :class="'fas fa-exclamation-circle'"></i>
<span>{{ alert.message }}</span>
</div>
H5 应用会自动适配桌面端,带有手机模拟器边框:
@media (min-width: 501px) {
body {
background: #1a1a2e;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
overflow: hidden; /* 防止双滚动条 */
}
.h5-app {
width: 390px;
height: 844px;
border-radius: 40px;
box-shadow: 0 0 0 8px #333, 0 0 0 10px #555;
overflow: hidden; /* 改为 hidden,内容在内部滚动 */
position: relative;
padding-top: 44px;
padding-bottom: 34px;
}
/* 模拟器刘海 */
.h5-app::before {
content: '';
position: absolute;
top: 8px;
left: 50%;
transform: translateX(-50%);
width: 120px;
height: 28px;
background: #333;
border-radius: 0 0 16px 16px;
z-index: 101;
}
/* 模拟器下巴指示条 */
.h5-app::after {
content: '';
position: absolute;
bottom: 8px;
left: 50%;
transform: translateX(-50%);
width: 140px;
height: 5px;
background: #555;
border-radius: 3px;
z-index: 101;
}
/* TabBar 在桌面端模拟器内定位 */
.h5-tabbar {
position: fixed;
bottom: 34px;
left: calc(50% - 195px);
width: 390px;
border-radius: 0 0 40px 40px;
}
/* 页面内容内部滚动 */
.h5-page {
height: calc(100% - 44px);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
/* 隐藏页面滚动条但保持滚动功能 */
.h5-page::-webkit-scrollbar {
width: 0;
height: 0;
}
/* 弹窗定位修复 */
.h5-modal-overlay {
position: absolute !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 84px !important;
}
}
生成文件后,可以选择以下方式预览:
直接用浏览器打开 h5.html 文件即可。
# Python 3
cd /path/to/project
python -m http.server 8080
# Node.js (npx)
npx serve .
# PHP
php -S localhost:8080
然后访问 http://localhost:8080/h5.html
安装 Live Server 插件,右键点击 h5.html → "Open with Live Server"
admin.html 是独立的 PC 管理后台 SPA,可选择性实现。如果暂时不需要,可以忽略此文件。
最小可用模板:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理后台</title>
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="css/admin.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
</head>
<body>
<div class="admin-layout">
<!-- 侧边栏 -->
<aside class="admin-sidebar">
<div class="admin-sidebar-header">
<div class="admin-sidebar-logo">
<div class="admin-sidebar-logo-icon"><i class="fas fa-cube"></i></div>
<span>管理后台</span>
</div>
</div>
<nav class="admin-sidebar-nav">
<div class="admin-nav-group">
<div class="admin-nav-group-title">概览</div>
<div class="admin-nav-item active">
<i class="admin-nav-item-icon fas fa-chart-pie"></i>
<span class="admin-nav-item-label">数据看板</span>
</div>
</div>
</nav>
</aside>
<!-- 主内容区 -->
<main class="admin-main">
<header class="admin-header">
<div class="admin-header-left">
<div class="admin-header-title">数据看板</div>
</div>
<div class="admin-header-right">
<div class="admin-header-action">
<i class="fas fa-bell"></i>
</div>
<div class="admin-header-avatar">管</div>
</div>
</header>
<div class="admin-content">
<div style="padding: 24px;">
<h2>欢迎使用管理后台</h2>
<p style="color: var(--color-text-secondary); margin-top: 8px;">
这是一个最小可用的 PC 管理后台模板。
</p>
</div>
</div>
</main>
</div>
</body>
</html>
完整的颜色系统、字体规范、间距系统、组件规格、动画关键帧等详见:
references/design-system.md
共 4 个版本