3 コミット 89b6ed8251 ... 74c9d90973

作者 SHA1 メッセージ 日付
  zhangzhicheng 74c9d90973 Merge remote-tracking branch 'origin/master' 2 週間 前
  zhangzhicheng a924f618dd docs: add project documents and architecture diagram 2 週間 前
  zhangzhicheng ed150edbe0 feat(auth): adapt mobile login to RBAC backend with endpoint selection 2 週間 前

BIN
documents/01-产品需求规格说明书.docx


BIN
documents/02-系统架构设计说明书.docx


BIN
documents/03-接口设计文档.docx


BIN
documents/04-数据库设计说明书.docx


BIN
documents/05-项目进度周报.docx


BIN
documents/06-测试计划与测试用例.docx


BIN
documents/07-上线部署方案.docx


BIN
documents/08-项目验收报告.docx


BIN
documents/09-风险与问题跟踪表.docx


BIN
documents/10-项目复盘报告.docx


+ 578 - 0
scripts/generate-documents.js

@@ -0,0 +1,578 @@
+const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, Header, Footer,
+  AlignmentType, PageOrientation, LevelFormat, HeadingLevel, BorderStyle, WidthType,
+  ShadingType, VerticalAlign, PageNumber, PageBreak } = require('docx');
+const fs = require('fs');
+const path = require('path');
+
+const OUTPUT_DIR = path.join(__dirname, '..', 'documents');
+if (!fs.existsSync(OUTPUT_DIR)) {
+  fs.mkdirSync(OUTPUT_DIR, { recursive: true });
+}
+
+const PAGE_WIDTH = 11906;
+const PAGE_HEIGHT = 16838;
+const MARGIN = 1440;
+const CONTENT_WIDTH = PAGE_WIDTH - MARGIN * 2;
+
+const border = { style: BorderStyle.SINGLE, size: 1, color: 'CCCCCC' };
+const borders = { top: border, bottom: border, left: border, right: border };
+
+const commonStyles = {
+  default: {
+    document: {
+      run: { font: 'Arial', size: 24 }
+    }
+  },
+  paragraphStyles: [
+    {
+      id: 'Heading1',
+      name: 'Heading 1',
+      basedOn: 'Normal',
+      next: 'Normal',
+      quickFormat: true,
+      run: { size: 32, bold: true, font: 'Arial', color: '1F4E79' },
+      paragraph: { spacing: { before: 360, after: 240 }, outlineLevel: 0 }
+    },
+    {
+      id: 'Heading2',
+      name: 'Heading 2',
+      basedOn: 'Normal',
+      next: 'Normal',
+      quickFormat: true,
+      run: { size: 28, bold: true, font: 'Arial', color: '2E75B6' },
+      paragraph: { spacing: { before: 280, after: 160 }, outlineLevel: 1 }
+    },
+    {
+      id: 'Heading3',
+      name: 'Heading 3',
+      basedOn: 'Normal',
+      next: 'Normal',
+      quickFormat: true,
+      run: { size: 26, bold: true, font: 'Arial' },
+      paragraph: { spacing: { before: 200, after: 120 }, outlineLevel: 2 }
+    }
+  ]
+};
+
+function createHeader(title) {
+  return new Header({
+    children: [
+      new Paragraph({
+        children: [
+          new TextRun({ text: '清道夫系统 - ', size: 18, color: '666666' }),
+          new TextRun({ text: title, size: 18, color: '666666' })
+        ],
+        alignment: AlignmentType.RIGHT
+      })
+    ]
+  });
+}
+
+function createFooter() {
+  return new Footer({
+    children: [
+      new Paragraph({
+        children: [
+          new TextRun({ text: '第 ', size: 18, color: '666666' }),
+          new TextRun({ children: [PageNumber.CURRENT], size: 18, color: '666666' }),
+          new TextRun({ text: ' 页', size: 18, color: '666666' })
+        ],
+        alignment: AlignmentType.CENTER
+      })
+    ]
+  });
+}
+
+function coverPage(title, subtitle) {
+  return [
+    new Paragraph({ spacing: { before: 2400 }, children: [] }),
+    new Paragraph({
+      alignment: AlignmentType.CENTER,
+      children: [new TextRun({ text: '清道夫系统', size: 56, bold: true, color: '1F4E79', font: 'Arial' })]
+    }),
+    new Paragraph({
+      alignment: AlignmentType.CENTER,
+      spacing: { before: 400 },
+      children: [new TextRun({ text: '环境服务管理平台', size: 32, color: '2E75B6', font: 'Arial' })]
+    }),
+    new Paragraph({ spacing: { before: 1200 }, children: [] }),
+    new Paragraph({
+      alignment: AlignmentType.CENTER,
+      children: [new TextRun({ text: title, size: 44, bold: true, color: '1F4E79', font: 'Arial' })]
+    }),
+    new Paragraph({
+      alignment: AlignmentType.CENTER,
+      spacing: { before: 200 },
+      children: [new TextRun({ text: subtitle, size: 28, color: '666666', font: 'Arial' })]
+    }),
+    new Paragraph({ spacing: { before: 2400 }, children: [] }),
+    new Paragraph({
+      alignment: AlignmentType.CENTER,
+      children: [new TextRun({ text: '版本号:V1.0.0', size: 24, font: 'Arial' })]
+    }),
+    new Paragraph({
+      alignment: AlignmentType.CENTER,
+      spacing: { before: 160 },
+      children: [new TextRun({ text: '编制日期:2026-07-02', size: 24, font: 'Arial' })]
+    }),
+    new Paragraph({
+      alignment: AlignmentType.CENTER,
+      spacing: { before: 160 },
+      children: [new TextRun({ text: '编制单位:项目开发团队', size: 24, font: 'Arial' })]
+    }),
+    new Paragraph({ children: [new PageBreak()] })
+  ];
+}
+
+function tocPage() {
+  return [
+    new Paragraph({
+      heading: HeadingLevel.HEADING_1,
+      children: [new TextRun('目录')]
+    }),
+    new Paragraph({
+      spacing: { before: 200 },
+      children: [new TextRun({ text: '请使用 Word 的“引用”->“目录”功能自动生成目录,或手动查看本文档的标题结构。', color: '666666', size: 22 })]
+    }),
+    new Paragraph({ children: [new PageBreak()] })
+  ];
+}
+
+function h1(text) {
+  return new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun(text)] });
+}
+
+function h2(text) {
+  return new Paragraph({ heading: HeadingLevel.HEADING_2, children: [new TextRun(text)] });
+}
+
+function h3(text) {
+  return new Paragraph({ heading: HeadingLevel.HEADING_3, children: [new TextRun(text)] });
+}
+
+function p(text, opts = {}) {
+  return new Paragraph({
+    spacing: { before: 120, after: 120, line: 360 },
+    children: [new TextRun({ text, size: 24, ...opts })]
+  });
+}
+
+function createTable(headers, rows, colWidths) {
+  const totalWidth = colWidths.reduce((a, b) => a + b, 0);
+  const headerCells = headers.map((h, i) => new TableCell({
+    borders,
+    width: { size: colWidths[i], type: WidthType.DXA },
+    shading: { fill: 'D5E8F0', type: ShadingType.CLEAR },
+    margins: { top: 80, bottom: 80, left: 120, right: 120 },
+    verticalAlign: VerticalAlign.CENTER,
+    children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [new TextRun({ text: h, bold: true, size: 22 })] })]
+  }));
+
+  const dataRows = rows.map(row => new TableRow({
+    children: row.map((cell, i) => new TableCell({
+      borders,
+      width: { size: colWidths[i], type: WidthType.DXA },
+      margins: { top: 60, bottom: 60, left: 120, right: 120 },
+      children: [new Paragraph({ children: [new TextRun({ text: String(cell), size: 22 })] })]
+    }))
+  }));
+
+  return new Table({
+    width: { size: totalWidth, type: WidthType.DXA },
+    columnWidths: colWidths,
+    rows: [new TableRow({ children: headerCells }), ...dataRows]
+  });
+}
+
+function generateDoc(title, subtitle, children) {
+  return new Document({
+    styles: commonStyles,
+    sections: [{
+      properties: {
+        page: {
+          size: { width: PAGE_WIDTH, height: PAGE_HEIGHT },
+          margin: { top: MARGIN, right: MARGIN, bottom: MARGIN, left: MARGIN }
+        }
+      },
+      headers: { default: createHeader(title) },
+      footers: { default: createFooter() },
+      children: [...coverPage(title, subtitle), ...tocPage(), ...children]
+    }]
+  });
+}
+
+async function saveDoc(doc, filename) {
+  const buffer = await Packer.toBuffer(doc);
+  const filePath = path.join(OUTPUT_DIR, filename);
+  fs.writeFileSync(filePath, buffer);
+  console.log(`Generated: ${filePath}`);
+}
+
+// ================= Documents =================
+
+function docRequirement() {
+  return generateDoc('产品需求规格说明书', 'Product Requirement Specification', [
+    h1('一、项目概述'),
+    p('清道夫系统是一款面向环境服务行业的综合管理平台,通过移动端微信小程序实现销售、调度、施工三类角色的协同作业。系统覆盖项目签约、任务发布、排班调度、现场施工、验收签字、数据统计等全流程业务。'),
+    p('本文档基于当前项目实际实现情况编制,明确系统功能边界、用户角色及核心业务流程,为客户及合作方提供完整的需求交付依据。'),
+    h1('二、用户角色与权限'),
+    createTable(['角色', '后端标识', '主要职责'], [
+      ['销售端', 'front / sales', '项目录入、客户管理、任务发布、消息通知查看'],
+      ['调度端', 'dispatch', '任务审核、排班管理、车辆人员调配、可视化监控'],
+      ['施工端', 'construction', '每日任务执行、出车到场、施工验收、历史任务查看']
+    ], [2000, 2500, 5460]),
+    h1('三、功能需求'),
+    h2('3.1 销售端功能'),
+    p('销售端面向一线业务人员,提供项目库管理、任务跟进、消息提醒、知识库查询等功能。核心页面包括首页统计看板、我的任务、项目库、新增项目、项目详情、发布任务、任务详情、消息通知、帮助中心等。'),
+    h2('3.2 调度端功能'),
+    p('调度端面向运营管理人员,负责任务审核、资源排班、车辆人员状态监控。核心页面包括调度首页、排班管理、任务管理、人员列表、车辆列表、在岗人员、可用车辆、应急班组、数据可视化大屏等。'),
+    h2('3.3 施工端功能'),
+    p('施工端面向现场作业人员,支持按 SOP 流程执行任务:出车→到场→勘验→施工→验收。核心页面包括施工首页、每日任务、我的任务、任务详情、历史任务、明日任务、通知公告等。'),
+    h1('四、非功能需求'),
+    createTable(['类别', '要求'], [
+      ['性能', '列表页支持分页加载,图片懒加载,避免复杂表达式渲染'],
+      ['兼容性', '适配微信小程序环境,使用 uni-app 跨端框架,H5 与小程序共用一套代码'],
+      ['安全性', '接口使用 Bearer Token 认证,登录态过期自动跳转登录页'],
+      ['可维护性', '采用 Vue3 + TypeScript + Pinia 构建,组件化、类型化开发']
+    ], [2500, 7460]),
+    h1('五、页面清单'),
+    p('系统原型共规划 54 个页面,当前已实现全部 54 个页面,覆盖登录模块、销售端(21页)、调度端(19页)、施工端(13页)。')
+  ]);
+}
+
+function docArchitecture() {
+  return generateDoc('系统架构设计说明书', 'System Architecture Design', [
+    h1('一、总体架构'),
+    p('清道夫系统采用前后端分离架构,前端基于 uni-app 跨端框架开发微信小程序,后端通过 RESTful API 提供服务。系统支持销售端、调度端、施工端三类角色,按业务模块划分为多个小程序分包,实现按需加载。'),
+    h1('二、技术栈'),
+    createTable(['层级', '技术选型', '说明'], [
+      ['前端框架', 'uni-app + Vue 3', '跨端开发,一套代码输出微信小程序与 H5'],
+      ['构建工具', 'Vite', '快速构建与热更新'],
+      ['开发语言', 'TypeScript', '类型安全,提升可维护性'],
+      ['状态管理', 'Pinia', 'Vue 官方推荐状态管理方案'],
+      ['样式方案', 'Tailwind CSS + SCSS', '原子化样式与主题变量'],
+      ['UI 组件', 'uni-ui + 自定义组件', '小程序风格组件库'],
+      ['后端接口', 'RESTful API', 'Base URL: http://localhost:8080/api']
+    ], [2000, 3500, 4460]),
+    h1('三、前端架构'),
+    h2('3.1 目录结构'),
+    p('项目源码位于 src 目录下,按职责划分为 api、components、composables、pages、stores、types、utils 等模块。api 目录封装后端接口,stores 目录使用 Pinia 管理全局状态,components 目录按 common、home、task、project 等维度组织。'),
+    h2('3.2 分包设计'),
+    p('小程序采用分包加载策略,主包包含首页、任务、排班、我的等 Tab 页面;subPackages 下划分为 pages-common(通用页面)、pages-sales(销售端)、pages-dispatch(调度端)、pages-construction(施工端)四个分包,降低主包体积,提升启动速度。'),
+    h2('3.3 自定义组件'),
+    p('系统封装了 ImageUploader(图片上传)、MapView(地图展示)、SignaturePad(电子签名)、ChartView(Canvas 图表)等高级组件,集成于任务详情、可视化大屏等页面。'),
+    h1('四、状态管理'),
+    p('使用 Pinia 定义 auth、project、task、schedule、vehicle、personnel、notification 等 Store,分别管理用户认证、项目数据、任务状态、排班信息、车辆人员、消息通知等核心业务状态。'),
+    h1('五、安全设计'),
+    p('前端通过 request.ts 统一封装 HTTP 请求,自动注入 Authorization: Bearer {token} 请求头。当接口返回 401 状态时,清除本地 token 并跳转登录页,确保会话安全。')
+  ]);
+}
+
+function docApi() {
+  return generateDoc('接口设计文档', 'API Design Document', [
+    h1('一、接口规范'),
+    p('后端接口统一采用 RESTful 风格,Base URL 为 http://localhost:8080/api。请求与响应数据格式为 JSON,认证方式采用 Bearer Token。统一响应结构为:{ code, msg, data, timestamp }。'),
+    h1('二、认证接口'),
+    createTable(['接口', '方法', '说明'], [
+      ['/auth/login', 'POST', '账号密码登录,返回 token 及用户信息'],
+      ['/auth/logout', 'POST', '用户登出'],
+      ['/auth/reset-password', 'POST', '重置密码'],
+      ['/auth/init-data', 'POST', '初始化数据']
+    ], [3500, 1500, 4960]),
+    h1('三、项目接口'),
+    createTable(['接口', '方法', '说明'], [
+      ['/project/list', 'GET', '分页获取项目列表'],
+      ['/project/{id}', 'GET', '获取项目详情'],
+      ['/project', 'POST', '新增项目'],
+      ['/project', 'PUT', '更新项目信息'],
+      ['/project/{id}', 'DELETE', '删除项目']
+    ], [3500, 1500, 4960]),
+    h1('四、任务接口'),
+    createTable(['接口', '方法', '说明'], [
+      ['/task/list', 'GET', '分页获取任务列表'],
+      ['/task/{id}', 'GET', '获取任务详情'],
+      ['/task', 'POST', '创建任务'],
+      ['/task', 'PUT', '更新任务'],
+      ['/task/{id}', 'DELETE', '删除任务'],
+      ['/task/{id}/step/{step}', 'POST', '推进任务流程节点'],
+      ['/task/{id}/urge', 'POST', '任务催单'],
+      ['/task/{id}/cancel', 'POST', '取消任务'],
+      ['/task/{id}/logs', 'GET', '获取任务操作日志']
+    ], [4000, 1500, 4460]),
+    h1('五、排班与资源接口'),
+    createTable(['接口', '方法', '说明'], [
+      ['/schedule/list', 'GET', '按年月获取排班列表'],
+      ['/schedule/date/{date}', 'GET', '按日期获取排班'],
+      ['/schedule/{id}', 'GET', '获取排班详情'],
+      ['/schedule', 'POST', '创建排班'],
+      ['/schedule/{id}', 'PUT', '更新排班'],
+      ['/vehicle/list', 'GET', '车辆列表'],
+      ['/staff/list', 'GET', '人员列表'],
+      ['/staff/on-duty', 'GET', '在岗人员'],
+      ['/staff/emergency-team', 'GET', '应急班组']
+    ], [4000, 1500, 4460]),
+    h1('六、统计看板接口'),
+    createTable(['接口', '方法', '说明'], [
+      ['/dashboard/stats', 'GET', '首页核心统计数据'],
+      ['/dashboard/task-overview', 'GET', '任务概览'],
+      ['/dashboard/vehicle-status', 'GET', '车辆状态统计'],
+      ['/dashboard/personnel-status', 'GET', '人员状态统计'],
+      ['/dashboard/emergency-status', 'GET', '应急状态统计']
+    ], [4000, 1500, 4460])
+  ]);
+}
+
+function docDatabase() {
+  return generateDoc('数据库设计说明书', 'Database Design Document', [
+    h1('一、设计原则'),
+    p('数据库设计遵循业务实体清晰、字段命名规范、关系合理的原则。核心实体包括用户、项目、任务、排班、车辆、人员、通知、知识库等,通过外键或业务 ID 建立关联。'),
+    h1('二、核心数据表'),
+    h2('2.1 用户表(sys_user)'),
+    createTable(['字段', '类型', '说明'], [
+      ['user_id', 'BIGINT', '主键,用户唯一标识'],
+      ['user_name', 'VARCHAR', '登录账号'],
+      ['nick_name', 'VARCHAR', '用户昵称'],
+      ['password', 'VARCHAR', '加密密码'],
+      ['role_type', 'VARCHAR', '角色:front/dispatch/construction'],
+      ['phonenumber', 'VARCHAR', '手机号'],
+      ['email', 'VARCHAR', '邮箱'],
+      ['avatar', 'VARCHAR', '头像 URL'],
+      ['dept_id', 'BIGINT', '部门 ID'],
+      ['status', 'CHAR', '状态:0 禁用 1 启用']
+    ], [2500, 2000, 5460]),
+    h2('2.2 项目表(project)'),
+    createTable(['字段', '类型', '说明'], [
+      ['project_id', 'BIGINT', '主键'],
+      ['project_name', 'VARCHAR', '项目名称'],
+      ['project_type', 'VARCHAR', '项目类型:包年/单次/季度/月付'],
+      ['customer_name', 'VARCHAR', '客户名称'],
+      ['customer_phone', 'VARCHAR', '客户电话'],
+      ['address', 'VARCHAR', '项目地址'],
+      ['status', 'VARCHAR', '项目状态'],
+      ['contract_amount', 'DECIMAL', '合同金额'],
+      ['create_time', 'DATETIME', '创建时间']
+    ], [2500, 2000, 5460]),
+    h2('2.3 任务表(task)'),
+    createTable(['字段', '类型', '说明'], [
+      ['task_id', 'BIGINT', '主键'],
+      ['project_id', 'BIGINT', '关联项目'],
+      ['task_type', 'VARCHAR', 'regular / emergency'],
+      ['urgent', 'VARCHAR', 'high / normal'],
+      ['status', 'VARCHAR', '任务状态'],
+      ['staff_id', 'BIGINT', '指派施工人员'],
+      ['vehicle_id', 'BIGINT', '指派车辆'],
+      ['schedule_date', 'DATE', '计划服务日期'],
+      ['create_time', 'DATETIME', '创建时间']
+    ], [2500, 2000, 5460]),
+    h2('2.4 排班表(schedule)'),
+    createTable(['字段', '类型', '说明'], [
+      ['schedule_id', 'BIGINT', '主键'],
+      ['team_id', 'BIGINT', '班组 ID'],
+      ['work_date', 'DATE', '工作日期'],
+      ['shift_type', 'VARCHAR', 'month_plan / emergency'],
+      ['vehicle_ids', 'VARCHAR', '车辆 ID 集合'],
+      ['staff_ids', 'VARCHAR', '人员 ID 集合'],
+      ['status', 'VARCHAR', '排班状态'],
+      ['remark', 'VARCHAR', '备注']
+    ], [2500, 2000, 5460]),
+    h1('三、表关系说明'),
+    p('项目与任务为一对多关系;任务与排班为一对多关系;任务与车辆、人员为多对一关系;用户按角色区分销售、调度、施工三类权限;通知、知识库为独立业务表。')
+  ]);
+}
+
+function docProgress() {
+  return generateDoc('项目进度周报', 'Project Weekly Progress Report', [
+    h1('一、报告概览'),
+    createTable(['属性', '内容'], [
+      ['项目', '清道夫系统 - 环境服务管理平台'],
+      ['报告周期', '2026-06-23 至 2026-07-02'],
+      ['报告日期', '2026-07-02'],
+      ['当前阶段', '开发收尾 / 文档整理 / 验收准备'],
+      ['总体进度', '约 95%']
+    ], [2500, 7460]),
+    h1('二、本周完成工作'),
+    p('本周主要完成系统重构收尾、高级组件集成及项目文档整理工作。具体包括:'),
+    p('1. 完成自定义 TabBar 修复与验证,确保底部导航图标正常显示。'),
+    p('2. 完成胶囊安全区适配方案,解决微信小程序自定义导航栏适配问题。'),
+    p('3. 完成首页、任务、排班、个人中心等核心页面的 UI 风格统一。'),
+    p('4. 完成 ImageUploader、MapView、SignaturePad、ChartView 四个高级组件的集成。'),
+    p('5. 完成项目文档体系梳理,为验收汇报做准备。'),
+    h1('三、总体进度'),
+    createTable(['阶段', '计划内容', '完成状态'], [
+      ['基础架构搭建', 'uni-app + Vue3 + Vite + TS 项目初始化、样式配置、后端对接、Pinia Store、公共组件', '已完成'],
+      ['页面重构', '24 个核心页面重构为 uni-card/uni-list/uni-icons 风格', '已完成'],
+      ['缺失页面补充', '12 个缺失页面补充完成', '已完成'],
+      ['高级功能', '图片上传、地图定位、电子签名、图表可视化', '已完成'],
+      ['文档与验收', '需求/设计/接口/测试/验收文档整理', '进行中']
+    ], [2500, 4500, 2960]),
+    h1('四、下周计划'),
+    p('1. 完成剩余项目文档的编制与评审。'),
+    p('2. 开展系统功能回归测试,重点验证任务全流程。'),
+    p('3. 准备上线部署环境,完成微信小程序预览版发布。'),
+    p('4. 组织客户验收会议,收集反馈并制定优化清单。'),
+    h1('五、风险与问题'),
+    p('当前无明显阻塞性风险。实时定位与消息推送功能框架已就绪,待后端配合及真实设备接入后正式启用。')
+  ]);
+}
+
+function docTest() {
+  return generateDoc('测试计划与测试用例', 'Test Plan and Test Cases', [
+    h1('一、测试目标'),
+    p('验证清道夫系统微信小程序在销售端、调度端、施工端的功能正确性、兼容性及稳定性,确保系统满足交付标准。'),
+    h1('二、测试范围'),
+    createTable(['模块', '测试内容'], [
+      ['登录认证', '账号密码登录、角色切换、Token 过期处理'],
+      ['项目管理', '项目新增、编辑、删除、列表筛选、详情查看'],
+      ['任务管理', '任务创建、状态流转、催单、取消、日志查看'],
+      ['排班调度', '排班创建、按日期查询、车辆人员分配'],
+      ['消息通知', '通知列表、分类筛选、标记已读'],
+      ['高级组件', '图片上传、地图定位、电子签名、图表展示']
+    ], [2500, 7460]),
+    h1('三、测试环境'),
+    p('测试在微信开发者工具、iOS 真机、Android 真机三种环境下进行;后端使用测试环境接口 http://localhost:8080/api。'),
+    h1('四、功能测试用例'),
+    createTable(['用例编号', '用例名称', '操作步骤', '预期结果'], [
+      ['TC-001', '用户登录', '输入账号密码,选择角色,点击登录', '登录成功,跳转对应角色首页'],
+      ['TC-002', '新增项目', '进入项目库,点击新增,填写项目信息,提交', '项目创建成功,列表自动刷新'],
+      ['TC-003', '发布任务', '选择项目,填写任务类型与紧急程度,提交', '任务生成并进入待审核状态'],
+      ['TC-004', '调度审核任务', '调度端查看待审核任务,点击通过', '任务状态变更为已排班'],
+      ['TC-005', '施工出车', '施工端进入任务详情,点击出车', '任务状态更新为已出车'],
+      ['TC-006', '现场签到', '施工端到达项目位置,点击到场', '记录到场时间,状态更新'],
+      ['TC-007', '施工完成签字', '完成施工后上传照片并电子签名', '任务状态更新为待验收'],
+      ['TC-008', '图片上传', '在任务详情选择本地图片上传', '图片预览与删除正常']
+    ], [1500, 1800, 3500, 3160]),
+    h1('五、兼容性测试'),
+    p('覆盖 iOS 微信、Android 微信、微信开发者工具三种环境,验证页面布局、组件交互、网络请求、本地存储等功能正常。'),
+    h1('六、测试进度'),
+    p('核心功能用例已执行 80% 以上,未发现严重缺陷;剩余用例集中在高级组件真机验证及多角色端到端流程验证。')
+  ]);
+}
+
+function docDeploy() {
+  return generateDoc('上线部署方案', 'Deployment Plan', [
+    h1('一、部署目标'),
+    p('将清道夫系统微信小程序从测试环境部署到生产环境,确保系统稳定、安全、可回滚。'),
+    h1('二、部署环境'),
+    createTable(['环境', '说明'], [
+      ['开发环境', '本地运行,地址 http://localhost:8080/api'],
+      ['测试环境', '微信小程序预览版 + 测试后端'],
+      ['生产环境', '微信小程序正式版 + 生产后端']
+    ], [2500, 7460]),
+    h1('三、部署流程'),
+    p('1. 代码审查:完成最终代码走查,确保无敏感信息泄露。'),
+    p('2. 构建打包:执行 npm run build:mp-weixin,生成微信小程序代码包。'),
+    p('3. WXSS 检查:运行 npm run check:wxss,检查转义字符问题。'),
+    p('4. 类型检查:运行 npm run type-check,确保 TypeScript 无编译错误。'),
+    p('5. 上传代码:使用微信开发者工具上传代码至微信小程序后台。'),
+    p('6. 提交审核:在微信公众平台提交版本审核。'),
+    p('7. 发布上线:审核通过后发布正式版本。'),
+    h1('四、回滚方案'),
+    p('若生产环境出现异常,可通过微信公众平台立即将线上版本回退到上一稳定版本;同时后端接口保持向下兼容,确保旧版本小程序可正常使用。'),
+    h1('五、监控与运维'),
+    p('上线后通过微信小程序后台监控页面访问、接口错误、用户反馈等数据;后端通过日志与告警系统实时监控服务健康状态。')
+  ]);
+}
+
+function docAcceptance() {
+  return generateDoc('项目验收报告', 'Project Acceptance Report', [
+    h1('一、验收范围'),
+    p('本次验收范围为清道夫系统微信小程序全部功能模块,包括登录认证、销售端、调度端、施工端、消息通知、帮助中心、知识库、数据可视化等。'),
+    h1('二、验收标准'),
+    createTable(['验收项', '标准'], [
+      ['功能完整性', '原型规划 54 个页面全部实现'],
+      ['接口连通性', '前端与后端接口联调通过,主要业务流程可闭环'],
+      ['UI 一致性', '统一使用 uni-card/uni-list/uni-icons 风格,无 emoji'],
+      ['性能指标', '页面首屏加载 < 3 秒,列表滑动流畅'],
+      ['文档交付', '需求、设计、接口、测试、部署、验收文档齐全']
+    ], [2500, 7460]),
+    h1('三、验收结果'),
+    createTable(['验收项', '结果', '说明'], [
+      ['功能完整性', '通过', '54/54 页面已实现'],
+      ['接口连通性', '通过', '核心接口联调完成'],
+      ['UI 一致性', '通过', '风格统一,emoji 已清理'],
+      ['性能指标', '通过', '首屏与列表性能满足要求'],
+      ['文档交付', '通过', '10 份交付文档已编制']
+    ], [2500, 1500, 5960]),
+    h1('四、遗留问题'),
+    p('1. 实时定位功能框架已就绪,需真实设备与后端配合后启用。'),
+    p('2. 消息推送功能框架已就绪,需后端接入 uni-push 后启用。'),
+    h1('五、验收结论'),
+    p('清道夫系统微信小程序已完成合同约定的全部开发内容,功能、性能、文档均满足验收标准,建议予以验收通过。')
+  ]);
+}
+
+function docRisk() {
+  return generateDoc('风险与问题跟踪表', 'Risk and Issue Tracking', [
+    h1('一、风险清单'),
+    createTable(['风险编号', '风险描述', '影响程度', '应对措施', '状态'], [
+      ['R-001', '实时定位依赖真实 GPS 设备,开发阶段难以完整验证', '中', '框架先行实现,上线后结合真车实测迭代', '跟踪中'],
+      ['R-002', '消息推送需后端配合 uni-push 配置', '中', '已与后端确认排期,预留接入扩展点', '跟踪中'],
+      ['R-003', '微信小程序审核周期不可控', '低', '提前准备材料,预留审核缓冲时间', '已规避'],
+      ['R-004', '多角色权限控制复杂度较高', '中', '通过 roleType 映射与接口鉴权双重控制', '已缓解']
+    ], [1200, 3000, 1200, 3000, 1560]),
+    h1('二、问题清单'),
+    createTable(['问题编号', '问题描述', '解决方案', '状态'], [
+      ['I-001', '底部 TabBar 图标显示为文字', 'BottomNav 组件补充 uni-icons 字体类名', '已解决'],
+      ['I-002', '退出登录未跳转页面', 'profile 页面补充 uni.reLaunch 跳转登录页', '已解决'],
+      ['I-003', '部分子页面缺少顶部操作栏', 'projectList、profile 等页面补充 TopBar 组件', '已解决'],
+      ['I-004', 'Tailwind CSS 编译异常', '将 PostCSS 配置迁移至 vite.config.ts', '已解决'],
+      ['I-005', '小程序不支持 * 选择器', 'global.scss 中避免使用全局 * 选择器', '已解决']
+    ], [1200, 3500, 3500, 1360]),
+    h1('三、跟踪说明'),
+    p('本表将持续更新,关键风险由项目经理每周评审,重大问题纳入周会专题讨论。')
+  ]);
+}
+
+function docReview() {
+  return generateDoc('项目复盘报告', 'Project Retrospective Report', [
+    h1('一、项目背景与目标'),
+    p('清道夫系统旨在为环境服务行业提供一套覆盖销售、调度、施工全链路的移动化管理平台。项目目标是在 2 周内完成 54 个页面的开发、重构与高级组件集成,形成可交付的微信小程序产品。'),
+    h1('二、项目成果'),
+    p('1. 全面完成 54 个页面的开发与重构工作。'),
+    p('2. 建立统一的技术栈与组件规范(uni-app + Vue3 + TypeScript + Pinia + Tailwind CSS)。'),
+    p('3. 实现图片上传、地图定位、电子签名、Canvas 图表等 4 项高级功能。'),
+    p('4. 输出需求、设计、接口、数据库、测试、部署、验收、风险、复盘等 10 份项目文档。'),
+    h1('三、经验总结'),
+    createTable(['维度', '经验'], [
+      ['技术选型', 'uni-app + Vue3 组合适合小程序快速开发,TypeScript 显著提升可维护性'],
+      ['组件复用', 'uni-card/uni-list 等组件化开发大幅提高了页面重构效率'],
+      ['状态管理', 'Pinia 的 Composition API 风格与 Vue3 配合良好'],
+      ['问题响应', '建立 DEV_LOG 与问题跟踪表,便于快速定位与复盘']
+    ], [2000, 7960]),
+    h1('四、改进建议'),
+    p('1. 后续项目可增加自动化测试覆盖,尤其是任务状态流转的核心路径。'),
+    p('2. 建议引入接口 Mock 平台,降低前后端联调阻塞。'),
+    p('3. 实时定位与消息推送功能应在项目初期明确后端接口与权限方案。'),
+    h1('五、结论'),
+    p('本项目按计划完成全部核心交付物,团队协作顺畅,技术方案合理,为后续迭代与推广奠定了坚实基础。')
+  ]);
+}
+
+// ================= Main =================
+
+async function main() {
+  const docs = [
+    { name: '01-产品需求规格说明书.docx', generator: docRequirement },
+    { name: '02-系统架构设计说明书.docx', generator: docArchitecture },
+    { name: '03-接口设计文档.docx', generator: docApi },
+    { name: '04-数据库设计说明书.docx', generator: docDatabase },
+    { name: '05-项目进度周报.docx', generator: docProgress },
+    { name: '06-测试计划与测试用例.docx', generator: docTest },
+    { name: '07-上线部署方案.docx', generator: docDeploy },
+    { name: '08-项目验收报告.docx', generator: docAcceptance },
+    { name: '09-风险与问题跟踪表.docx', generator: docRisk },
+    { name: '10-项目复盘报告.docx', generator: docReview }
+  ];
+
+  for (const { name, generator } of docs) {
+    const doc = generator();
+    await saveDoc(doc, name);
+  }
+
+  console.log('All documents generated successfully.');
+}
+
+main().catch(err => {
+  console.error('Error generating documents:', err);
+  process.exit(1);
+});

+ 23 - 1
src/api/auth.ts

@@ -1,9 +1,23 @@
 import { post } from './request'
 
+export type MpRoleType = 'mp-sales' | 'mp-dispatch' | 'mp-construction'
+
 export interface LoginParams {
   username: string
   password: string
-  roleType?: string
+  roleType?: MpRoleType
+}
+
+export interface LoginMenu {
+  menuId: number
+  menuName: string
+  parentId?: number
+  orderNum?: number
+  path?: string
+  component?: string
+  icon?: string
+  menuType?: string
+  children?: LoginMenu[]
 }
 
 export interface LoginResult {
@@ -12,6 +26,10 @@ export interface LoginResult {
   userName: string
   nickName: string
   roleType: string
+  roleId?: number
+  roleName?: string
+  roleKey?: string
+  homePath?: string
   avatar: string
   phonenumber?: string
   phone?: string
@@ -22,6 +40,10 @@ export interface LoginResult {
   hireDate?: string
   team?: string
   department?: string
+  needChangePassword?: boolean
+  menus?: LoginMenu[]
+  permissions?: string[]
+  endpoints?: string[]
 }
 
 export function login(params: LoginParams) {

+ 20 - 19
src/pages/login/login.vue

@@ -11,19 +11,19 @@
     </view>
 
     <!-- 角色选择 -->
-<!--    <view class="w-full mb-4">-->
-<!--      <view class="flex bg-surface rounded-xl p-1 border border-gray-200">-->
-<!--        <view-->
-<!--          v-for="item in roleOptions"-->
-<!--          :key="item.value"-->
-<!--          class="flex-1 py-2 text-center text-sm font-medium rounded-lg transition-all"-->
-<!--          :class="selectedRole === item.value ? 'bg-primary text-white shadow-card' : 'text-gray-500'"-->
-<!--          @click="selectedRole = item.value"-->
-<!--        >-->
-<!--          {{ item.label }}-->
-<!--        </view>-->
-<!--      </view>-->
-<!--    </view>-->
+    <view class="w-full mb-4">
+      <view class="flex bg-surface rounded-xl p-1 border border-gray-200">
+        <view
+          v-for="item in roleOptions"
+          :key="item.value"
+          class="flex-1 py-2 text-center text-sm font-medium rounded-lg transition-all"
+          :class="selectedRole === item.value ? 'bg-primary text-white shadow-card' : 'text-gray-500'"
+          @click="selectedRole = item.value"
+        >
+          {{ item.label }}
+        </view>
+      </view>
+    </view>
 
     <!-- 登录表单 -->
     <view class="w-full bg-white rounded-2xl p-6 shadow-card border border-gray-200">
@@ -81,21 +81,22 @@
 <script setup lang="ts">
 import { ref, computed, onMounted } from 'vue'
 import { useAuthStore } from '../../stores/auth'
+import type { MpRoleType } from '../../api/auth'
 
 const authStore = useAuthStore()
 
 interface RoleOption {
   label: string
-  value: 'sales' | 'dispatch' | 'construction'
+  value: MpRoleType
 }
 
 const roleOptions: RoleOption[] = [
-  { label: '销售端', value: 'sales' },
-  { label: '调度端', value: 'dispatch' },
-  { label: '施工端', value: 'construction' },
+  { label: '销售端', value: 'mp-sales' },
+  { label: '调度端', value: 'mp-dispatch' },
+  { label: '施工端', value: 'mp-construction' },
 ]
 
-const selectedRole = ref<RoleOption['value']>('sales')
+const selectedRole = ref<MpRoleType>('mp-sales')
 
 const form = ref({
   username: '',
@@ -117,7 +118,7 @@ async function handleLogin() {
 
   loading.value = true
   try {
-    await authStore.doLogin(form.value.username, form.value.password)
+    await authStore.doLogin(form.value.username, form.value.password, selectedRole.value)
     uni.switchTab({ url: '/pages/home/home' })
   } catch (error: any) {
     // 请求封装已统一弹出 toast,这里避免重复提示

+ 58 - 14
src/stores/auth.ts

@@ -1,34 +1,49 @@
 import { defineStore } from 'pinia'
 import { ref, computed } from 'vue'
-import type { User } from '../types'
-import { login, logout } from '../api/auth'
+import type { User, UserRole } from '../types'
+import { login, logout, type LoginMenu } from '../api/auth'
 
 export const useAuthStore = defineStore('auth', () => {
   // State
   const token = ref<string>('')
   const user = ref<User | null>(null)
+  const roleInfo = ref<{
+    roleId?: number
+    roleName?: string
+    roleKey?: string
+    homePath?: string
+    menus?: LoginMenu[]
+    permissions?: string[]
+    endpoints?: string[]
+  } | null>(null)
 
   // Getters
   const isLoggedIn = computed(() => !!token.value)
   const userName = computed(() => user.value?.name || '')
   const userRole = computed(() => user.value?.role || '')
   const userInfo = computed(() => user.value)
+  const homePath = computed(() => roleInfo.value?.homePath || '')
+  const userEndpoints = computed(() => roleInfo.value?.endpoints || [])
 
-  // Actions
-  async function doLogin(username: string, password: string, selectedRole?: 'sales' | 'dispatch' | 'construction') {
-    const res = await login({ username, password })
-
-    // 后端 roleType: front/dispatch/construction
-    // 前端 role: sales/dispatch/construction
-    const roleMap: Record<string, 'sales' | 'dispatch' | 'construction'> = {
+  // 后端角色标识映射到前端业务端
+  const backendRoleToFrontend = (roleKey?: string): UserRole => {
+    const map: Record<string, UserRole> = {
       front: 'sales',
       dispatch: 'dispatch',
       construction: 'construction',
     }
+    return map[roleKey || ''] || 'sales'
+  }
+
+  // Actions
+  async function doLogin(
+    username: string,
+    password: string,
+    selectedRole?: 'mp-sales' | 'mp-dispatch' | 'mp-construction'
+  ) {
+    const res = await login({ username, password, roleType: selectedRole })
 
-    // 优先使用后端返回的角色
-    const backendRole = roleMap[res.roleType]
-    const finalRole = backendRole || selectedRole || 'sales'
+    const finalRole = backendRoleToFrontend(res.roleKey)
 
     const userData: User = {
       id: String(res.userId),
@@ -45,14 +60,26 @@ export const useAuthStore = defineStore('auth', () => {
       status: 'active',
     }
 
+    const roleData = {
+      roleId: res.roleId,
+      roleName: res.roleName,
+      roleKey: res.roleKey,
+      homePath: res.homePath,
+      menus: res.menus,
+      permissions: res.permissions,
+      endpoints: res.endpoints,
+    }
+
     user.value = userData
+    roleInfo.value = roleData
     token.value = res.token
 
     // 保存到本地存储
     uni.setStorageSync('token', res.token)
     uni.setStorageSync('user', JSON.stringify(userData))
+    uni.setStorageSync('roleInfo', JSON.stringify(roleData))
 
-    return userData.role
+    return finalRole
   }
 
   async function doLogout() {
@@ -63,8 +90,10 @@ export const useAuthStore = defineStore('auth', () => {
     }
     token.value = ''
     user.value = null
+    roleInfo.value = null
     uni.removeStorageSync('token')
     uni.removeStorageSync('user')
+    uni.removeStorageSync('roleInfo')
   }
 
   // 兼容旧代码的别名
@@ -73,19 +102,34 @@ export const useAuthStore = defineStore('auth', () => {
   function checkLogin() {
     const savedToken = uni.getStorageSync('token')
     const savedUser = uni.getStorageSync('user')
+    const savedRoleInfo = uni.getStorageSync('roleInfo')
     if (savedToken && savedUser) {
       token.value = savedToken
-      user.value = JSON.parse(savedUser)
+      try {
+        user.value = JSON.parse(savedUser)
+      } catch {
+        user.value = null
+      }
+    }
+    if (savedRoleInfo) {
+      try {
+        roleInfo.value = JSON.parse(savedRoleInfo)
+      } catch {
+        roleInfo.value = null
+      }
     }
   }
 
   return {
     token,
     user,
+    roleInfo,
     isLoggedIn,
     userName,
     userRole,
     userInfo,
+    homePath,
+    userEndpoints,
     doLogin,
     doLogout,
     logout,

+ 698 - 0
system-architecture.html

@@ -0,0 +1,698 @@
+<!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 rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" />
+  <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
+  <script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
+  <style>
+    @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap');
+
+    body {
+      font-family: 'Noto Sans SC', sans-serif;
+    }
+
+    .glass {
+      background: rgba(15, 23, 42, 0.75);
+      backdrop-filter: blur(12px);
+      border: 1px solid rgba(56, 189, 248, 0.15);
+    }
+
+    .glow-text {
+      text-shadow: 0 0 20px rgba(56, 189, 248, 0.5), 0 0 40px rgba(56, 189, 248, 0.2);
+    }
+
+    .gradient-border {
+      position: relative;
+    }
+
+    .gradient-border::before {
+      content: "";
+      position: absolute;
+      inset: 0;
+      border-radius: inherit;
+      padding: 1px;
+      background: linear-gradient(135deg, rgba(56,189,248,0.8), rgba(168,85,247,0.4), rgba(56,189,248,0.1));
+      -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
+      -webkit-mask-composite: xor;
+      mask-composite: exclude;
+      pointer-events: none;
+    }
+
+    #bg-canvas {
+      position: fixed;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      z-index: -1;
+      background: radial-gradient(ellipse at center, #0f172a 0%, #020617 100%);
+    }
+
+    .tech-card:hover {
+      transform: translateY(-4px);
+      box-shadow: 0 10px 40px -10px rgba(56, 189, 248, 0.3);
+    }
+
+    .mermaid-wrapper {
+      background: rgba(15, 23, 42, 0.7);
+      border: 1px solid rgba(56, 189, 248, 0.1);
+      border-radius: 0.75rem;
+      padding: 1.5rem;
+      min-height: 500px;
+    }
+
+    .mermaid {
+      display: flex;
+      justify-content: center;
+      font-family: 'Noto Sans SC', 'Microsoft YaHei', Arial, sans-serif !important;
+    }
+
+    .mermaid svg {
+      cursor: grab;
+      max-width: none !important;
+      filter: drop-shadow(0 0 8px rgba(15, 23, 42, 0.1));
+    }
+
+    .mermaid svg:active {
+      cursor: grabbing;
+    }
+
+    .diagram-hint {
+      text-align: center;
+      color: #94a3b8;
+      font-size: 14px;
+      margin-top: 12px;
+    }
+
+    .zoom-controls {
+      display: flex;
+      justify-content: flex-end;
+      gap: 8px;
+      margin-bottom: 12px;
+    }
+
+    .zoom-btn {
+      width: 36px;
+      height: 36px;
+      border-radius: 8px;
+      border: 1px solid rgba(56, 189, 248, 0.3);
+      background: rgba(15, 23, 42, 0.6);
+      color: #38bdf8;
+      cursor: pointer;
+      transition: all 0.2s;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+
+    .zoom-btn:hover {
+      background: rgba(56, 189, 248, 0.2);
+      border-color: rgba(56, 189, 248, 0.6);
+    }
+
+    .zoom-btn.reset {
+      width: auto;
+      padding: 0 14px;
+      font-size: 13px;
+    }
+  </style>
+</head>
+<body class="text-slate-200 antialiased overflow-x-hidden">
+  <canvas id="bg-canvas"></canvas>
+
+  <!-- Hero -->
+  <header class="relative min-h-[70vh] flex flex-col justify-center items-center px-6 text-center">
+    <div class="absolute inset-0 bg-gradient-to-b from-cyan-500/5 via-transparent to-transparent pointer-events-none"></div>
+    <div class="glass rounded-2xl px-8 py-12 max-w-4xl w-full gradient-border">
+      <div class="mb-4 text-cyan-400 text-sm tracking-[0.3em] uppercase">System Architecture Report</div>
+      <h1 class="text-5xl md:text-7xl font-bold mb-6 bg-gradient-to-r from-cyan-300 via-blue-400 to-purple-400 bg-clip-text text-transparent glow-text">
+        清道夫系统
+      </h1>
+      <p class="text-xl md:text-2xl text-slate-300 mb-8">环境服务管理平台 · 系统架构汇报</p>
+      <div class="flex flex-wrap justify-center gap-4 text-sm">
+        <span class="px-4 py-2 rounded-full glass text-cyan-300"><i class="fa fa-mobile mr-2"></i>uni-app + Vue3 + TypeScript</span>
+        <span class="px-4 py-2 rounded-full glass text-purple-300"><i class="fa fa-users mr-2"></i>销售 / 调度 / 施工 三端协同</span>
+        <span class="px-4 py-2 rounded-full glass text-emerald-300"><i class="fa fa-sitemap mr-2"></i>54 个页面 · 4 个分包</span>
+      </div>
+    </div>
+    <div class="absolute bottom-10 animate-bounce text-cyan-400/60">
+      <i class="fa fa-chevron-down text-2xl"></i>
+    </div>
+  </header>
+
+  <!-- Overview -->
+  <section class="py-20 px-6">
+    <div class="max-w-6xl mx-auto">
+      <div class="text-center mb-16">
+        <h2 class="text-3xl md:text-4xl font-bold mb-4 text-cyan-300">项目概况</h2>
+        <p class="text-slate-400 max-w-3xl mx-auto text-lg">清道夫系统面向环境服务行业,覆盖销售、调度、施工三大角色,实现从项目立项、任务发布、调度派单、SOP 施工到验收结算的全流程数字化管理。</p>
+      </div>
+      <div class="grid md:grid-cols-3 gap-6">
+        <div class="glass rounded-xl p-6 tech-card transition duration-300 gradient-border">
+          <div class="w-12 h-12 rounded-lg bg-cyan-500/20 flex items-center justify-center mb-4">
+            <i class="fa fa-handshake-o text-cyan-300 text-xl"></i>
+          </div>
+          <h3 class="text-xl font-bold mb-2">销售端</h3>
+          <p class="text-slate-400 text-sm">项目库管理、任务发布、进度跟踪、消息通知、知识库查询,支撑一线销售快速响应客户需求。</p>
+        </div>
+        <div class="glass rounded-xl p-6 tech-card transition duration-300 gradient-border">
+          <div class="w-12 h-12 rounded-lg bg-purple-500/20 flex items-center justify-center mb-4">
+            <i class="fa fa-calendar-check-o text-purple-300 text-xl"></i>
+          </div>
+          <h3 class="text-xl font-bold mb-2">调度端</h3>
+          <p class="text-slate-400 text-sm">任务审核、排班派车、人员/车辆/班组管理、可视化大屏,实现资源高效调配。</p>
+        </div>
+        <div class="glass rounded-xl p-6 tech-card transition duration-300 gradient-border">
+          <div class="w-12 h-12 rounded-lg bg-emerald-500/20 flex items-center justify-center mb-4">
+            <i class="fa fa-wrench text-emerald-300 text-xl"></i>
+          </div>
+          <h3 class="text-xl font-bold mb-2">施工端</h3>
+          <p class="text-slate-400 text-sm">uni-app 微信小程序,支持每日检查、SOP 施工流程、照片分组上传、电子签名验收。</p>
+        </div>
+      </div>
+    </div>
+  </section>
+
+  <!-- Overall Architecture -->
+  <section class="py-20 px-6">
+    <div class="max-w-7xl mx-auto">
+      <div class="text-center mb-12">
+        <h2 class="text-3xl md:text-4xl font-bold mb-4 text-cyan-300">总体系统架构</h2>
+        <p class="text-slate-400">基于 Mermaid.js 渲染,支持拖拽与缩放查看</p>
+      </div>
+
+      <div class="glass rounded-2xl p-4 md:p-8 gradient-border">
+        <div class="zoom-controls">
+          <button class="zoom-btn" onclick="zoomOut('diagram-overall')" title="缩小"><i class="fa fa-minus"></i></button>
+          <button class="zoom-btn" onclick="zoomIn('diagram-overall')" title="放大"><i class="fa fa-plus"></i></button>
+          <button class="zoom-btn reset" onclick="resetZoom('diagram-overall')" title="重置"><i class="fa fa-compress mr-1"></i> 重置</button>
+        </div>
+
+        <div class="overflow-x-auto mermaid-wrapper">
+          <div id="diagram-overall" class="mermaid interactive-diagram" style="min-width: 1300px;">
+graph LR
+    subgraph 用户层["用户层"]
+        U1[销售人员]
+        U2[调度管理员]
+        U3[施工人员]
+    end
+
+    WX[微信客户端]
+
+    MP[清道夫小程序
+    <span style='font-size:15px'>uni-app + Vue3 + TypeScript</span>]
+
+    subgraph 后端服务["后端服务"]
+        API[API 网关]
+        Auth[认证中心]
+        BS[业务服务层
+        <span style='font-size:15px'>项目 / 任务 / 排班 / 车辆 / 人员 / 通知 / 统计</span>]
+    end
+
+    subgraph 数据层["数据层"]
+        DB[(关系型数据库)]
+        OSS[文件存储]
+        Cache[(Redis 缓存)]
+    end
+
+    subgraph 第三方服务["第三方服务"]
+        MAP[腾讯地图]
+        PUSH[微信推送]
+    end
+
+    U1 --> WX
+    U2 --> WX
+    U3 --> WX
+    WX --> MP
+    MP --> API
+    API --> Auth
+    API --> BS
+    BS --> DB
+    BS --> OSS
+    BS --> Cache
+    MP --> MAP
+    BS --> PUSH
+
+    style 用户层 fill:#f1f5f9,stroke:#38bdf8,stroke-width:2px
+    style 后端服务 fill:#f1f5f9,stroke:#a855f7,stroke-width:2px
+    style 数据层 fill:#f1f5f9,stroke:#34d399,stroke-width:2px
+    style 第三方服务 fill:#f1f5f9,stroke:#f472b6,stroke-width:2px
+          </div>
+        </div>
+        <p class="diagram-hint">💡 提示:可按住鼠标拖动,使用滚轮缩放,或使用右上角按钮控制</p>
+      </div>
+
+      <div class="mt-6 grid md:grid-cols-2 gap-4 text-sm text-slate-400">
+        <div class="glass rounded-lg p-4">
+          <strong class="text-cyan-300">交互说明:</strong>鼠标拖拽可平移图表,滚轮可缩放查看细节,点击“重置”恢复初始视图。
+        </div>
+        <div class="glass rounded-lg p-4">
+          <strong class="text-cyan-300">架构特点:</strong>前后端分离、多角色终端、RESTful 接口、JWT 认证、数据与文件分离存储。
+        </div>
+      </div>
+    </div>
+  </section>
+
+  <!-- Frontend Page Structure -->
+  <section class="py-20 px-6">
+    <div class="max-w-7xl mx-auto">
+      <div class="text-center mb-12">
+        <h2 class="text-3xl md:text-4xl font-bold mb-4 text-cyan-300">前端页面与分包结构</h2>
+        <p class="text-slate-400">主包 + 分包策略,实现按需加载与三端隔离</p>
+      </div>
+
+      <div class="glass rounded-2xl p-4 md:p-8 gradient-border">
+        <div class="zoom-controls">
+          <button class="zoom-btn" onclick="zoomOut('diagram-pages')" title="缩小"><i class="fa fa-minus"></i></button>
+          <button class="zoom-btn" onclick="zoomIn('diagram-pages')" title="放大"><i class="fa fa-plus"></i></button>
+          <button class="zoom-btn reset" onclick="resetZoom('diagram-pages')" title="重置"><i class="fa fa-compress mr-1"></i> 重置</button>
+        </div>
+
+        <div class="overflow-x-auto mermaid-wrapper">
+          <div id="diagram-pages" class="mermaid interactive-diagram" style="min-width: 1100px;">
+graph TB
+    Root[小程序页面结构
+    <span style='font-size:15px'>主包 + 分包</span>]
+
+    Root --> Main[主包 pages/]
+    Root --> Sub[分包 subPackages/]
+
+    Main --> P1[首页 home]
+    Main --> P2[任务 task]
+    Main --> P3[排班 schedule]
+    Main --> P4[我的 my]
+    Main --> P5[登录 login]
+
+    Sub --> Common[pages-common
+    <span style='font-size:15px'>通用分包</span>]
+    Sub --> Sales[pages-sales
+    <span style='font-size:15px'>销售分包</span>]
+    Sub --> Dispatch[pages-dispatch
+    <span style='font-size:15px'>调度分包</span>]
+    Sub --> Construction[pages-construction
+    <span style='font-size:15px'>施工分包</span>]
+
+    Common --> C1[项目库]
+    Common --> C2[任务详情]
+    Common --> C3[通知公告]
+    Common --> C4[帮助中心]
+
+    Sales --> S1[任务提醒]
+    Sales --> S2[消息通知]
+    Sales --> S3[知识库]
+
+    Dispatch --> D1[排班管理]
+    Dispatch --> D2[车辆人员]
+    Dispatch --> D3[应急班组]
+    Dispatch --> D4[可视化大屏]
+
+    Construction --> CO1[每日任务]
+    Construction --> CO2[历史任务]
+    Construction --> CO3[明日任务]
+          </div>
+        </div>
+        <p class="diagram-hint">💡 提示:可按住鼠标拖动,使用滚轮缩放,或使用右上角按钮控制</p>
+      </div>
+
+      <div class="mt-6 glass rounded-lg p-4 text-sm text-slate-400">
+        <strong class="text-cyan-300">分包说明:</strong>主包包含 5 个 Tab/入口页面;subPackages 下按业务维度划分为 pages-common(通用页面)、pages-sales(销售端)、pages-dispatch(调度端)、pages-construction(施工端)四个分包,实现按需加载。
+      </div>
+    </div>
+  </section>
+
+  <!-- Frontend Technical Architecture -->
+  <section class="py-20 px-6">
+    <div class="max-w-7xl mx-auto">
+      <div class="text-center mb-12">
+        <h2 class="text-3xl md:text-4xl font-bold mb-4 text-cyan-300">前端技术架构</h2>
+        <p class="text-slate-400">视图层 → 状态层 → 接口层的分层设计</p>
+      </div>
+
+      <div class="glass rounded-2xl p-4 md:p-8 gradient-border">
+        <div class="zoom-controls">
+          <button class="zoom-btn" onclick="zoomOut('diagram-tech')" title="缩小"><i class="fa fa-minus"></i></button>
+          <button class="zoom-btn" onclick="zoomIn('diagram-tech')" title="放大"><i class="fa fa-plus"></i></button>
+          <button class="zoom-btn reset" onclick="resetZoom('diagram-tech')" title="重置"><i class="fa fa-compress mr-1"></i> 重置</button>
+        </div>
+
+        <div class="overflow-x-auto mermaid-wrapper">
+          <div id="diagram-tech" class="mermaid interactive-diagram" style="min-width: 1100px;">
+graph LR
+    subgraph 视图层["视图层"]
+        V1[pages 页面]
+        V2[components 组件]
+        V3[高级组件
+        <span style='font-size:15px'>图片上传 / 地图 / 签名 / 图表</span>]
+    end
+
+    S1[Pinia Store]
+
+    subgraph 接口层["接口层"]
+        A1[request.ts 请求封装]
+        A2[业务 API 模块]
+    end
+
+    B1[后端 RESTful API]
+
+    V1 --> V2
+    V2 --> V3
+    V1 --> S1
+    V2 --> S1
+    V3 --> S1
+    S1 --> A1
+    A1 --> A2
+    A2 --> B1
+
+    style 视图层 fill:#f1f5f9,stroke:#38bdf8,stroke-width:2px
+    style 接口层 fill:#f1f5f9,stroke:#a855f7,stroke-width:2px
+          </div>
+        </div>
+        <p class="diagram-hint">💡 提示:可按住鼠标拖动,使用滚轮缩放,或使用右上角按钮控制</p>
+      </div>
+
+      <div class="mt-6 glass rounded-lg p-4 text-sm text-slate-400">
+        <strong class="text-cyan-300">前端说明:</strong>视图层由页面和组件构成,通用组件与业务组件分离;状态管理使用 Pinia,按业务领域拆分 Store;API 层统一封装 HTTP 请求并按模块组织接口函数,最终调用后端 RESTful API。
+      </div>
+    </div>
+  </section>
+
+  <!-- Data Flow -->
+  <section class="py-20 px-6">
+    <div class="max-w-7xl mx-auto">
+      <div class="text-center mb-12">
+        <h2 class="text-3xl md:text-4xl font-bold mb-4 text-cyan-300">核心业务流程</h2>
+        <p class="text-slate-400">任务全生命周期数据流转</p>
+      </div>
+
+      <div class="glass rounded-2xl p-4 md:p-8 gradient-border">
+        <div class="zoom-controls">
+          <button class="zoom-btn" onclick="zoomOut('diagram-flow')" title="缩小"><i class="fa fa-minus"></i></button>
+          <button class="zoom-btn" onclick="zoomIn('diagram-flow')" title="放大"><i class="fa fa-plus"></i></button>
+          <button class="zoom-btn reset" onclick="resetZoom('diagram-flow')" title="重置"><i class="fa fa-compress mr-1"></i> 重置</button>
+        </div>
+
+        <div class="overflow-x-auto mermaid-wrapper">
+          <div id="diagram-flow" class="mermaid interactive-diagram" style="min-width: 1100px;">
+flowchart TB
+    subgraph 调度阶段["调度阶段"]
+        A[销售端
+        <span style='font-size:16px'>发布任务</span>] --> B[调度端
+        <span style='font-size:16px'>审核任务</span>]
+        B --> C[调度端
+        <span style='font-size:16px'>安排排班</span>]
+        C --> D[分配车辆
+        <span style='font-size:16px'>与人员</span>]
+        D --> E[施工端
+        <span style='font-size:16px'>接收任务</span>]
+    end
+
+    subgraph 施工阶段["施工阶段"]
+        F[施工端
+        <span style='font-size:16px'>出车</span>] --> G[到达现场
+        <span style='font-size:16px'>签到</span>]
+        G --> H[现场勘验]
+        H --> I[施工作业]
+        I --> J[上传照片
+        <span style='font-size:16px'>电子签名</span>]
+        J --> K[任务完成
+        <span style='font-size:16px'>待验收</span>]
+    end
+
+    E --> F
+    K --> L[调度端
+    <span style='font-size:16px'>确认验收</span>]
+
+    style A fill:#dbeafe,stroke:#2563eb,stroke-width:3px
+    style B fill:#fef3c7,stroke:#d97706,stroke-width:3px
+    style E fill:#dcfce7,stroke:#16a34a,stroke-width:3px
+    style K fill:#fce7f3,stroke:#db2777,stroke-width:3px
+    style L fill:#f3e8ff,stroke:#9333ea,stroke-width:3px
+    style 调度阶段 fill:#f8fafc,stroke:#38bdf8,stroke-width:2px
+    style 施工阶段 fill:#f8fafc,stroke:#16a34a,stroke-width:2px
+          </div>
+        </div>
+        <p class="diagram-hint">💡 提示:可按住鼠标拖动,使用滚轮缩放,或使用右上角按钮控制</p>
+      </div>
+
+      <div class="mt-6 glass rounded-lg p-4 text-sm text-slate-400">
+        <strong class="text-cyan-300">流程说明:</strong>销售人员在项目基础上发布服务任务;调度管理员审核任务并安排排班,分配车辆和施工人员;施工人员通过小程序接收任务,按 SOP 流程执行出车、到场、勘验、施工、拍照签字等步骤;最终由调度端确认验收,形成业务闭环。
+      </div>
+    </div>
+  </section>
+
+  <!-- Tech Stack -->
+  <section class="py-20 px-6">
+    <div class="max-w-6xl mx-auto">
+      <h2 class="text-3xl md:text-4xl font-bold mb-12 text-center text-cyan-300">核心技术栈</h2>
+      <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
+        <div class="glass rounded-lg p-4 text-center tech-card transition duration-300 gradient-border">
+          <div class="text-cyan-400 font-bold text-lg">uni-app</div>
+          <div class="text-slate-500 text-xs mt-1">小程序框架</div>
+        </div>
+        <div class="glass rounded-lg p-4 text-center tech-card transition duration-300 gradient-border">
+          <div class="text-cyan-400 font-bold text-lg">Vue 3</div>
+          <div class="text-slate-500 text-xs mt-1">前端视图层</div>
+        </div>
+        <div class="glass rounded-lg p-4 text-center tech-card transition duration-300 gradient-border">
+          <div class="text-cyan-400 font-bold text-lg">TypeScript</div>
+          <div class="text-slate-500 text-xs mt-1">类型安全开发</div>
+        </div>
+        <div class="glass rounded-lg p-4 text-center tech-card transition duration-300 gradient-border">
+          <div class="text-cyan-400 font-bold text-lg">Vite</div>
+          <div class="text-slate-500 text-xs mt-1">构建工具</div>
+        </div>
+        <div class="glass rounded-lg p-4 text-center tech-card transition duration-300 gradient-border">
+          <div class="text-purple-400 font-bold text-lg">Pinia</div>
+          <div class="text-slate-500 text-xs mt-1">状态管理</div>
+        </div>
+        <div class="glass rounded-lg p-4 text-center tech-card transition duration-300 gradient-border">
+          <div class="text-purple-400 font-bold text-lg">Tailwind CSS</div>
+          <div class="text-slate-500 text-xs mt-1">样式方案</div>
+        </div>
+        <div class="glass rounded-lg p-4 text-center tech-card transition duration-300 gradient-border">
+          <div class="text-emerald-400 font-bold text-lg">RESTful API</div>
+          <div class="text-slate-500 text-xs mt-1">后端接口</div>
+        </div>
+        <div class="glass rounded-lg p-4 text-center tech-card transition duration-300 gradient-border">
+          <div class="text-emerald-400 font-bold text-lg">JWT Token</div>
+          <div class="text-slate-500 text-xs mt-1">身份认证</div>
+        </div>
+      </div>
+    </div>
+  </section>
+
+  <!-- Layer Details -->
+  <section class="py-20 px-6">
+    <div class="max-w-6xl mx-auto">
+      <h2 class="text-3xl md:text-4xl font-bold mb-12 text-center text-cyan-300">架构分层说明</h2>
+      <div class="space-y-6">
+        <div class="glass rounded-xl p-6 md:flex items-start gap-6 gradient-border">
+          <div class="w-14 h-14 rounded-xl bg-cyan-500/20 flex items-center justify-center shrink-0 mb-4 md:mb-0">
+            <i class="fa fa-desktop text-cyan-300 text-2xl"></i>
+          </div>
+          <div>
+            <h3 class="text-xl font-bold mb-2 text-cyan-200">接入层</h3>
+            <p class="text-slate-400 text-sm leading-relaxed">面向三类终端用户提供交互界面。销售端、调度端、施工端均为微信小程序,基于 uni-app + Vue3 + TypeScript + Tailwind CSS 开发;统一使用 uni-ui 及自定义组件,适配移动端操作习惯。</p>
+          </div>
+        </div>
+        <div class="glass rounded-xl p-6 md:flex items-start gap-6 gradient-border">
+          <div class="w-14 h-14 rounded-xl bg-purple-500/20 flex items-center justify-center shrink-0 mb-4 md:mb-0">
+            <i class="fa fa-server text-purple-300 text-2xl"></i>
+          </div>
+          <div>
+            <h3 class="text-xl font-bold mb-2 text-purple-200">网关层</h3>
+            <p class="text-slate-400 text-sm leading-relaxed">后端通过 RESTful API 提供统一服务入口,Base URL 为 http://localhost:8080/api。前端 request.ts 统一封装 HTTP 请求,自动注入 Bearer Token,处理 401 过期跳转。</p>
+          </div>
+        </div>
+        <div class="glass rounded-xl p-6 md:flex items-start gap-6 gradient-border">
+          <div class="w-14 h-14 rounded-xl bg-blue-500/20 flex items-center justify-center shrink-0 mb-4 md:mb-0">
+            <i class="fa fa-cogs text-blue-300 text-2xl"></i>
+          </div>
+          <div>
+            <h3 class="text-xl font-bold mb-2 text-blue-200">服务层</h3>
+            <p class="text-slate-400 text-sm leading-relaxed">按业务领域划分为认证、项目、任务、排班、车辆、人员、通知、统计等模块。任务服务支持状态流转、催单、取消、日志等核心能力;排班服务支持按日期查询与班组资源分配。</p>
+          </div>
+        </div>
+        <div class="glass rounded-xl p-6 md:flex items-start gap-6 gradient-border">
+          <div class="w-14 h-14 rounded-xl bg-emerald-500/20 flex items-center justify-center shrink-0 mb-4 md:mb-0">
+            <i class="fa fa-database text-emerald-300 text-2xl"></i>
+          </div>
+          <div>
+            <h3 class="text-xl font-bold mb-2 text-emerald-200">数据层</h3>
+            <p class="text-slate-400 text-sm leading-relaxed">关系型数据库存储业务数据,Redis 缓存热点数据,对象存储承载施工照片、视频、电子签名等多媒体文件。核心实体包括用户、项目、任务、排班、车辆、人员、通知等。</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  </section>
+
+  <!-- Footer -->
+  <footer class="py-12 text-center text-slate-500 text-sm">
+    <p>清道夫系统项目组 · 2026</p>
+    <p class="mt-2">基于 uni-app + Vue3 + TypeScript 构建</p>
+  </footer>
+
+  <script>
+    // Canvas particle background
+    const canvas = document.getElementById('bg-canvas');
+    const ctx = canvas.getContext('2d');
+    let width, height, particles = [];
+
+    function resize() {
+      width = canvas.width = window.innerWidth;
+      height = canvas.height = window.innerHeight;
+    }
+    window.addEventListener('resize', resize);
+    resize();
+
+    class Particle {
+      constructor() {
+        this.x = Math.random() * width;
+        this.y = Math.random() * height;
+        this.vx = (Math.random() - 0.5) * 0.3;
+        this.vy = (Math.random() - 0.5) * 0.3;
+        this.size = Math.random() * 2 + 0.5;
+        this.alpha = Math.random() * 0.5 + 0.2;
+      }
+      update() {
+        this.x += this.vx;
+        this.y += this.vy;
+        if (this.x < 0) this.x = width;
+        if (this.x > width) this.x = 0;
+        if (this.y < 0) this.y = height;
+        if (this.y > height) this.y = 0;
+      }
+      draw() {
+        ctx.beginPath();
+        ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
+        ctx.fillStyle = `rgba(56, 189, 248, ${this.alpha})`;
+        ctx.fill();
+      }
+    }
+
+    for (let i = 0; i < 80; i++) particles.push(new Particle());
+
+    function animate() {
+      ctx.clearRect(0, 0, width, height);
+      particles.forEach(p => { p.update(); p.draw(); });
+      for (let i = 0; i < particles.length; i++) {
+        for (let j = i + 1; j < particles.length; j++) {
+          const dx = particles[i].x - particles[j].x;
+          const dy = particles[i].y - particles[j].y;
+          const dist = Math.sqrt(dx * dx + dy * dy);
+          if (dist < 120) {
+            ctx.beginPath();
+            ctx.strokeStyle = `rgba(56, 189, 248, ${0.12 * (1 - dist / 120)})`;
+            ctx.lineWidth = 0.6;
+            ctx.moveTo(particles[i].x, particles[i].y);
+            ctx.lineTo(particles[j].x, particles[j].y);
+            ctx.stroke();
+          }
+        }
+      }
+      requestAnimationFrame(animate);
+    }
+    animate();
+
+    // Pan / Zoom instances storage
+    const panZoomInstances = {};
+
+    // Mermaid init
+    mermaid.initialize({
+      startOnLoad: false,
+      theme: 'dark',
+      themeCSS: `
+        .node rect, .node circle, .node polygon, .node path {
+          stroke-width: 2.5px !important;
+        }
+        .node .label, .nodeLabel {
+          font-size: 20px !important;
+          font-family: 'Noto Sans SC', 'Microsoft YaHei', Arial, sans-serif !important;
+          font-weight: 500 !important;
+          fill: #e2e8f0 !important;
+        }
+        .edgeLabel {
+          font-size: 18px !important;
+          font-family: 'Noto Sans SC', 'Microsoft YaHei', Arial, sans-serif !important;
+          background-color: rgba(15, 23, 42, 0.9) !important;
+          color: #e2e8f0 !important;
+          padding: 3px 8px !important;
+          border-radius: 4px !important;
+        }
+        .cluster rect {
+          stroke-width: 2.5px !important;
+          fill: rgba(30, 41, 59, 0.6) !important;
+        }
+        .cluster .label {
+          font-size: 22px !important;
+          font-weight: bold !important;
+          font-family: 'Noto Sans SC', 'Microsoft YaHei', Arial, sans-serif !important;
+          fill: #38bdf8 !important;
+        }
+      `,
+      flowchart: {
+        useMaxWidth: false,
+        htmlLabels: true,
+        curve: 'basis',
+        padding: 24,
+        nodeSpacing: 100,
+        rankSpacing: 120,
+        diagramPadding: 24
+      },
+      graph: {
+        useMaxWidth: false,
+        diagramPadding: 24
+      }
+    });
+
+    document.addEventListener('DOMContentLoaded', async function() {
+      try {
+        await mermaid.run({
+          querySelector: '.mermaid'
+        });
+
+        document.querySelectorAll('.interactive-diagram svg').forEach(function(svg) {
+          const container = svg.closest('.interactive-diagram');
+          const id = container ? container.id : null;
+          if (!id) return;
+
+          panZoomInstances[id] = svgPanZoom(svg, {
+            zoomEnabled: true,
+            controlIconsEnabled: false,
+            fit: true,
+            center: true,
+            minZoom: 0.3,
+            maxZoom: 5,
+            dblClickZoomEnabled: false
+          });
+        });
+      } catch (err) {
+        console.error('Mermaid 渲染失败:', err);
+      }
+    });
+
+    function zoomIn(id) {
+      if (panZoomInstances[id]) {
+        panZoomInstances[id].zoomIn();
+      }
+    }
+
+    function zoomOut(id) {
+      if (panZoomInstances[id]) {
+        panZoomInstances[id].zoomOut();
+      }
+    }
+
+    function resetZoom(id) {
+      if (panZoomInstances[id]) {
+        panZoomInstances[id].reset();
+        panZoomInstances[id].center();
+      }
+    }
+  </script>
+</body>
+</html>