|
|
const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun,
|
|
|
Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink,
|
|
|
InternalHyperlink, Bookmark, FootnoteReferenceRun, PositionalTab,
|
|
|
PositionalTabAlignment, PositionalTabRelativeTo, PositionalTabLeader,
|
|
|
TabStopType, TabStopPosition, Column, SectionType,
|
|
|
TableOfContents, HeadingLevel, BorderStyle, WidthType, ShadingType,
|
|
|
VerticalAlign, PageNumber, PageBreak } = require('docx');
|
|
|
const fs = require('fs');
|
|
|
|
|
|
// 辅助函数:创建代码块样式段落
|
|
|
function codeBlock(lines) {
|
|
|
const border = { style: BorderStyle.SINGLE, size: 1, color: "E0E0E0" };
|
|
|
const borders = { top: border, bottom: border, left: border, right: border };
|
|
|
return new Table({
|
|
|
width: { size: 9360, type: WidthType.DXA },
|
|
|
columnWidths: [9360],
|
|
|
rows: [
|
|
|
new TableRow({
|
|
|
children: [
|
|
|
new TableCell({
|
|
|
borders,
|
|
|
width: { size: 9360, type: WidthType.DXA },
|
|
|
shading: { fill: "F5F5F5", type: ShadingType.CLEAR },
|
|
|
margins: { top: 100, bottom: 100, left: 120, right: 120 },
|
|
|
children: lines.map(line => new Paragraph({
|
|
|
spacing: { before: 0, after: 0, line: 276 },
|
|
|
children: [new TextRun({ font: "Consolas", size: 18, text: line || " " })]
|
|
|
}))
|
|
|
})
|
|
|
]
|
|
|
})
|
|
|
]
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 辅助函数:创建正文段落
|
|
|
function bodyPara(text, opts = {}) {
|
|
|
return new Paragraph({
|
|
|
spacing: { before: 120, after: 120, line: 360 },
|
|
|
children: [new TextRun({ font: "宋体", size: 24, text, ...opts })]
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 辅助函数:创建加粗正文
|
|
|
function boldPara(text) {
|
|
|
return bodyPara(text, { bold: true });
|
|
|
}
|
|
|
|
|
|
// 辅助函数:创建小节标题
|
|
|
function subHeading(text, level = HeadingLevel.HEADING_2) {
|
|
|
return new Paragraph({
|
|
|
heading: level,
|
|
|
spacing: { before: 240, after: 120 },
|
|
|
children: [new TextRun({ text, bold: true, font: "黑体", size: level === HeadingLevel.HEADING_1 ? 32 : (level === HeadingLevel.HEADING_2 ? 28 : 26) })]
|
|
|
});
|
|
|
}
|
|
|
|
|
|
const doc = new Document({
|
|
|
styles: {
|
|
|
default: { document: { run: { font: "宋体", size: 24 } } },
|
|
|
paragraphStyles: [
|
|
|
{ id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
|
|
|
run: { size: 32, bold: true, font: "黑体" },
|
|
|
paragraph: { spacing: { before: 400, after: 200 }, outlineLevel: 0 } },
|
|
|
{ id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
|
|
|
run: { size: 28, bold: true, font: "黑体" },
|
|
|
paragraph: { spacing: { before: 300, after: 150 }, outlineLevel: 1 } },
|
|
|
{ id: "Heading3", name: "Heading 3", basedOn: "Normal", next: "Normal", quickFormat: true,
|
|
|
run: { size: 26, bold: true, font: "黑体" },
|
|
|
paragraph: { spacing: { before: 200, after: 100 }, outlineLevel: 2 } },
|
|
|
]
|
|
|
},
|
|
|
numbering: {
|
|
|
config: [
|
|
|
{ reference: "bullets",
|
|
|
levels: [{ level: 0, format: LevelFormat.BULLET, text: "•", alignment: AlignmentType.LEFT,
|
|
|
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
|
|
|
{ reference: "numbers",
|
|
|
levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
|
|
|
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
|
|
|
]
|
|
|
},
|
|
|
sections: [{
|
|
|
properties: {
|
|
|
page: {
|
|
|
size: { width: 11906, height: 16838 },
|
|
|
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }
|
|
|
}
|
|
|
},
|
|
|
headers: {
|
|
|
default: new Header({ children: [new Paragraph({
|
|
|
alignment: AlignmentType.RIGHT,
|
|
|
children: [new TextRun({ font: "宋体", size: 18, color: "666666", text: "智途投送 - 单兵终端APP设计文档" })]
|
|
|
})] })
|
|
|
},
|
|
|
footers: {
|
|
|
default: new Footer({ children: [new Paragraph({
|
|
|
alignment: AlignmentType.CENTER,
|
|
|
children: [
|
|
|
new TextRun({ font: "宋体", size: 18, text: "第 " }),
|
|
|
new TextRun({ children: [PageNumber.CURRENT], font: "宋体", size: 18 }),
|
|
|
new TextRun({ font: "宋体", size: 18, text: " 页" })
|
|
|
]
|
|
|
})] })
|
|
|
},
|
|
|
children: [
|
|
|
// 封面标题
|
|
|
new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 2400, after: 400 },
|
|
|
children: [new TextRun({ font: "黑体", size: 44, bold: true, text: "单兵终端APP" })] }),
|
|
|
new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 200, after: 2400 },
|
|
|
children: [new TextRun({ font: "黑体", size: 36, text: "软件架构与基本功能实现说明" })] }),
|
|
|
new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 400, after: 200 },
|
|
|
children: [new TextRun({ font: "宋体", size: 24, text: "智途投送软件系统" })] }),
|
|
|
new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 100, after: 100 },
|
|
|
children: [new TextRun({ font: "宋体", size: 24, text: new Date().toISOString().split('T')[0] })] }),
|
|
|
new Paragraph({ children: [new PageBreak()] }),
|
|
|
|
|
|
// 目录
|
|
|
new TableOfContents("目录", { hyperlink: true, headingStyleRange: "1-3" }),
|
|
|
new Paragraph({ children: [new PageBreak()] }),
|
|
|
|
|
|
// 第一章 项目概述
|
|
|
subHeading("一、项目概述", HeadingLevel.HEADING_1),
|
|
|
bodyPara("单兵终端APP是“智途投送”系统的移动端前端应用,供前线士兵使用。它基于 Capacitor 混合应用框架开发,以 HTML5 + JavaScript 实现业务逻辑,通过 Android WebView 渲染,并借助 Capacitor 插件调用原生能力(如 GPS 定位)。"),
|
|
|
bodyPara("APP 主要功能包括:士兵登录/注册、实时 GPS 定位与自动上报、紧急物资需求填报、投放点地图选点、任务进度监控、无人机状态查看、一键 SOS 求救等。"),
|
|
|
|
|
|
subHeading("1.1 技术栈", HeadingLevel.HEADING_2),
|
|
|
new Paragraph({ numbering: { reference: "bullets", level: 0 }, spacing: { before: 80, after: 80 },
|
|
|
children: [new TextRun({ font: "宋体", size: 24, text: "前端框架:原生 HTML5 + JavaScript(ES6+),无额外前端框架" })] }),
|
|
|
new Paragraph({ numbering: { reference: "bullets", level: 0 }, spacing: { before: 80, after: 80 },
|
|
|
children: [new TextRun({ font: "宋体", size: 24, text: "混合容器:Capacitor 6.x(生成 Android 原生工程)" })] }),
|
|
|
new Paragraph({ numbering: { reference: "bullets", level: 0 }, spacing: { before: 80, after: 80 },
|
|
|
children: [new TextRun({ font: "宋体", size: 24, text: "原生插件:@capacitor/geolocation(GPS 定位)" })] }),
|
|
|
new Paragraph({ numbering: { reference: "bullets", level: 0 }, spacing: { before: 80, after: 80 },
|
|
|
children: [new TextRun({ font: "宋体", size: 24, text: "地图服务:高德地图 JS API 2.0(动态地图 + 静态地图 fallback)" })] }),
|
|
|
new Paragraph({ numbering: { reference: "bullets", level: 0 }, spacing: { before: 80, after: 80 },
|
|
|
children: [new TextRun({ font: "宋体", size: 24, text: "后端通信:RESTful API(Flask 后端),fetch + AbortController 超时控制" })] }),
|
|
|
new Paragraph({ numbering: { reference: "bullets", level: 0 }, spacing: { before: 80, after: 80 },
|
|
|
children: [new TextRun({ font: "宋体", size: 24, text: "数据持久化:localStorage(会话信息、服务器地址缓存)" })] }),
|
|
|
|
|
|
subHeading("1.2 项目结构", HeadingLevel.HEADING_2),
|
|
|
codeBlock([
|
|
|
"单兵终端APP/",
|
|
|
"├── capacitor.config.json # Capacitor 配置(应用ID、明文传输等)",
|
|
|
"├── package.json # 依赖:@capacitor/core、geolocation、android",
|
|
|
"├── index.html # SPA 单页结构(所有页面 div 容器)",
|
|
|
"├── css/style.css # 全局样式",
|
|
|
"├── js/",
|
|
|
"│ ├── app.js # 主应用:路由、状态、UI 交互",
|
|
|
"│ ├── api.js # API 封装:REST 请求 + Mock 降级",
|
|
|
"│ └── location.js # GPS 定位 + 高德地图封装",
|
|
|
"└── android/ # Capacitor 生成的 Android 工程"
|
|
|
]),
|
|
|
|
|
|
// 第二章 软件架构
|
|
|
new Paragraph({ children: [new PageBreak()] }),
|
|
|
subHeading("二、软件架构", HeadingLevel.HEADING_1),
|
|
|
bodyPara("单兵终端APP采用经典的分层架构,从上到下依次为:表现层(UI 层)、业务逻辑层、数据访问层、原生能力层。整体为单页应用(SPA)模式,所有页面通过 JavaScript 动态切换,避免原生 Activity 跳转带来的开发复杂度。"),
|
|
|
|
|
|
subHeading("2.1 架构分层", HeadingLevel.HEADING_2),
|
|
|
// 架构表格
|
|
|
new Table({
|
|
|
width: { size: 9360, type: WidthType.DXA },
|
|
|
columnWidths: [2340, 7020],
|
|
|
rows: [
|
|
|
new TableRow({ children: [
|
|
|
new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} },
|
|
|
width: { size: 2340, type: WidthType.DXA }, shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, margins: { top: 80, bottom: 80, left: 120, right: 120 },
|
|
|
children: [new Paragraph({ children: [new TextRun({ bold: true, font: "宋体", size: 22, text: "分层" })] })] }),
|
|
|
new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} },
|
|
|
width: { size: 7020, type: WidthType.DXA }, shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, margins: { top: 80, bottom: 80, left: 120, right: 120 },
|
|
|
children: [new Paragraph({ children: [new TextRun({ bold: true, font: "宋体", size: 22, text: "职责与对应文件" })] })] })
|
|
|
]}),
|
|
|
new TableRow({ children: [
|
|
|
new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} },
|
|
|
width: { size: 2340, type: WidthType.DXA }, margins: { top: 80, bottom: 80, left: 120, right: 120 },
|
|
|
children: [new Paragraph({ children: [new TextRun({ font: "宋体", size: 22, text: "表现层(UI)" })] })] }),
|
|
|
new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} },
|
|
|
width: { size: 7020, type: WidthType.DXA }, margins: { top: 80, bottom: 80, left: 120, right: 120 },
|
|
|
children: [new Paragraph({ children: [new TextRun({ font: "宋体", size: 22, text: "index.html(单页多视图)+ css/style.css;负责页面布局、控件渲染、事件绑定" })] })] })
|
|
|
]}),
|
|
|
new TableRow({ children: [
|
|
|
new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} },
|
|
|
width: { size: 2340, type: WidthType.DXA }, margins: { top: 80, bottom: 80, left: 120, right: 120 },
|
|
|
children: [new Paragraph({ children: [new TextRun({ font: "宋体", size: 22, text: "业务逻辑层" })] })] }),
|
|
|
new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} },
|
|
|
width: { size: 7020, type: WidthType.DXA }, margins: { top: 80, bottom: 80, left: 120, right: 120 },
|
|
|
children: [new Paragraph({ children: [new TextRun({ font: "宋体", size: 22, text: "js/app.js;路由管理、状态管理、页面切换、表单校验、业务事件处理" })] })] })
|
|
|
]}),
|
|
|
new TableRow({ children: [
|
|
|
new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} },
|
|
|
width: { size: 2340, type: WidthType.DXA }, margins: { top: 80, bottom: 80, left: 120, right: 120 },
|
|
|
children: [new Paragraph({ children: [new TextRun({ font: "宋体", size: 22, text: "数据访问层" })] })] }),
|
|
|
new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} },
|
|
|
width: { size: 7020, type: WidthType.DXA }, margins: { top: 80, bottom: 80, left: 120, right: 120 },
|
|
|
children: [new Paragraph({ children: [new TextRun({ font: "宋体", size: 22, text: "js/api.js;封装 HTTP 请求、超时控制、后端不可用时自动降级为 Mock 数据" })] })] })
|
|
|
]}),
|
|
|
new TableRow({ children: [
|
|
|
new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} },
|
|
|
width: { size: 2340, type: WidthType.DXA }, margins: { top: 80, bottom: 80, left: 120, right: 120 },
|
|
|
children: [new Paragraph({ children: [new TextRun({ font: "宋体", size: 22, text: "原生能力层" })] })] }),
|
|
|
new TableCell({ borders: { top: {style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, bottom:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, left:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"}, right:{style:BorderStyle.SINGLE,size:1,color:"CCCCCC"} },
|
|
|
width: { size: 7020, type: WidthType.DXA }, margins: { top: 80, bottom: 80, left: 120, right: 120 },
|
|
|
children: [new Paragraph({ children: [new TextRun({ font: "宋体", size: 22, text: "js/location.js + Capacitor 插件;GPS 定位、高德地图渲染、逆地理编码" })] })] })
|
|
|
]}),
|
|
|
]
|
|
|
}),
|
|
|
|
|
|
subHeading("2.2 核心模块关系", HeadingLevel.HEADING_2),
|
|
|
bodyPara("App 启动时,app.js 读取 localStorage 中的会话信息,若已登录则进入首页并启动两个定时任务:"),
|
|
|
new Paragraph({ numbering: { reference: "numbers", level: 0 }, spacing: { before: 80, after: 80 },
|
|
|
children: [new TextRun({ font: "宋体", size: 24, text: "轮询任务(每 5 秒):当前页为“任务”或“无人机”时,自动拉取最新数据。" })] }),
|
|
|
new Paragraph({ numbering: { reference: "numbers", level: 0 }, spacing: { before: 80, after: 80 },
|
|
|
children: [new TextRun({ font: "宋体", size: 24, text: "定位上报任务(每 10 秒):通过 LocationModule 获取坐标并上传后端。" })] }),
|
|
|
bodyPara("用户操作(如提交需求、选择投放点)由 app.js 收集表单数据,调用 API 模块发送请求;地图相关操作统一委托给 LocationModule 处理,避免业务层直接依赖地图 SDK。"),
|
|
|
|
|
|
// 第三章 基本功能实现
|
|
|
new Paragraph({ children: [new PageBreak()] }),
|
|
|
subHeading("三、基本功能实现", HeadingLevel.HEADING_1),
|
|
|
bodyPara("以下按功能模块逐一说明核心实现逻辑,并给出关键代码片段。"),
|
|
|
|
|
|
// 3.1 项目配置
|
|
|
subHeading("3.1 项目配置与入口", HeadingLevel.HEADING_2),
|
|
|
bodyPara("Capacitor 配置开启了 Android 明文传输(cleartext),便于局域网调试 Flask 后端;应用 ID 为 com.zhitu.soldier。"),
|
|
|
boldPara("capacitor.config.json"),
|
|
|
codeBlock([
|
|
|
'{',
|
|
|
' "appId": "com.zhitu.soldier",',
|
|
|
' "appName": "智途投送-单兵终端",',
|
|
|
' "webDir": "www",',
|
|
|
' "server": {',
|
|
|
' "androidScheme": "http",',
|
|
|
' "cleartext": true',
|
|
|
' },',
|
|
|
' "plugins": {',
|
|
|
' "Geolocation": { "enabled": true }',
|
|
|
' }',
|
|
|
'}'
|
|
|
]),
|
|
|
|
|
|
// 3.2 登录注册
|
|
|
subHeading("3.2 登录与注册", HeadingLevel.HEADING_2),
|
|
|
bodyPara("登录模块支持两套机制:演示账号本地登录(无需后端即可体验)和真实后端账号登录。会话信息以 JSON 形式存储在 localStorage 中,退出时清除。"),
|
|
|
boldPara("js/app.js - 登录逻辑"),
|
|
|
codeBlock([
|
|
|
'async function doLogin() {',
|
|
|
' const id = document.getElementById("login-id").value.trim();',
|
|
|
' const pwd = document.getElementById("login-pwd").value.trim();',
|
|
|
' // 演示账号快速登录',
|
|
|
' const demoAccounts = {',
|
|
|
' "soldier_001": { name: "张三", unit: "第3步兵师/1连" },',
|
|
|
' "soldier_002": { name: "李四", unit: "第3步兵师/2连" }',
|
|
|
' };',
|
|
|
' if (demoAccounts[id] && pwd === "123456") {',
|
|
|
' localStorage.setItem("soldier_session", JSON.stringify({',
|
|
|
' soldier_id: id, name: demoAccounts[id].name, ...',
|
|
|
' }));',
|
|
|
' router("home");',
|
|
|
' startPolling();',
|
|
|
' startLocationReporting();',
|
|
|
' return;',
|
|
|
' }',
|
|
|
' // 真实后端登录',
|
|
|
' const result = await API.login(id, pwd);',
|
|
|
' if (result.ok) { ... }',
|
|
|
'}'
|
|
|
]),
|
|
|
|
|
|
// 3.3 路由管理
|
|
|
subHeading("3.3 路由与页面管理", HeadingLevel.HEADING_2),
|
|
|
bodyPara(`APP 为单页应用(SPA),所有页面以 <div class="page"> 形式放在同一 HTML 中。通过 CSS class active 控制显示/隐藏,配合 pageStack 实现返回上一页。底部 Tab 栏仅在首页、任务、无人机、我的四个主页面显示。`),
|
|
|
boldPara("js/app.js - 路由切换"),
|
|
|
codeBlock([
|
|
|
'function router(page) {',
|
|
|
' document.querySelectorAll(".page").forEach(p => p.classList.remove("active"));',
|
|
|
' const target = document.getElementById("page-" + page);',
|
|
|
' if (target) target.classList.add("active");',
|
|
|
' // Tab 高亮与显隐控制',
|
|
|
' document.querySelectorAll(".tab-item").forEach(t => t.classList.remove("active"));',
|
|
|
' const tabItem = document.querySelector(\'.tab-item[data-page="\' + page + \'"]\');',
|
|
|
' if (tabItem) tabItem.classList.add("active");',
|
|
|
' const tabBar = document.getElementById("tab-bar");',
|
|
|
' tabBar.style.display = TAB_PAGES.includes(page) ? "flex" : "none";',
|
|
|
' pageStack.push(page);',
|
|
|
' currentPage = page;',
|
|
|
' onPageEnter(page); // 页面专属初始化',
|
|
|
'}'
|
|
|
]),
|
|
|
|
|
|
// 3.4 GPS定位
|
|
|
subHeading("3.4 GPS 定位与自动上报", HeadingLevel.HEADING_2),
|
|
|
bodyPara("定位模块实现了四级降级策略:高德 JS 定位 → Capacitor 原生 GPS → 浏览器 Geolocation → IP 网络定位 → 默认坐标。确保在各种网络与权限环境下都能获得可用位置。"),
|
|
|
boldPara("js/location.js - 四级定位降级"),
|
|
|
codeBlock([
|
|
|
'async function getCurrentPosition() {',
|
|
|
' // 1. 高德定位',
|
|
|
' try { return await getAmapPosition(); }',
|
|
|
' catch (e) { errors.push("高德:" + e.message); }',
|
|
|
' // 2. Capacitor 原生定位',
|
|
|
' try { return await getCapacitorPosition(); }',
|
|
|
' catch (e) { errors.push("原生:" + e.message); }',
|
|
|
' // 3. 浏览器定位',
|
|
|
' try { return await getBrowserPosition(); }',
|
|
|
' catch (e) { errors.push("浏览器:" + e.message); }',
|
|
|
' // 4. IP 定位',
|
|
|
' try { return await getIpPosition(); }',
|
|
|
' catch (e) { errors.push("IP:" + e.message); }',
|
|
|
' // 5. 默认兜底',
|
|
|
' return { lat: 30.2500, lng: 120.1600, accuracy: 100, source: "default" };',
|
|
|
'}'
|
|
|
]),
|
|
|
bodyPara("登录成功后,app.js 调用 LocationModule.startReporting() 启动定时上报,每隔 10 秒将坐标发送至后端 /api/soldier/location。"),
|
|
|
boldPara("js/app.js - 启动自动上报"),
|
|
|
codeBlock([
|
|
|
'function startLocationReporting() {',
|
|
|
' LocationModule.startReporting(CONFIG.soldierId, CONFIG.soldierName, 10000);',
|
|
|
'}'
|
|
|
]),
|
|
|
|
|
|
// 3.5 物资需求上报
|
|
|
subHeading("3.5 物资需求上报", HeadingLevel.HEADING_2),
|
|
|
bodyPara("士兵在“需求上报”页选择物资类型、数量、紧急程度,并关联投放点。提交时组装 JSON 对象,通过 POST /api/demand 发送至后端。投放点数据可以是地图选点结果,也可以是系统推荐列表中的安全点。"),
|
|
|
boldPara("js/app.js - 提交需求"),
|
|
|
codeBlock([
|
|
|
'async function submitDemand() {',
|
|
|
' const type = document.getElementById("demand-type").value;',
|
|
|
' const qty = document.getElementById("demand-qty").value;',
|
|
|
' const urgency = document.querySelector("#urgency-group .radio-label.active")',
|
|
|
' .dataset.value;',
|
|
|
' const demand = {',
|
|
|
' soldier_id: CONFIG.soldierId,',
|
|
|
' type, quantity: parseInt(qty), urgency,',
|
|
|
' drop_point: selectedDropPoint,',
|
|
|
' status: "待处理",',
|
|
|
' created_at: new Date().toISOString()',
|
|
|
' };',
|
|
|
' await API.postDemand(demand);',
|
|
|
' showToast("✅ 需求上报成功!");',
|
|
|
' router("home");',
|
|
|
'}'
|
|
|
]),
|
|
|
|
|
|
// 3.6 投放点与地图
|
|
|
subHeading("3.6 投放点选择与地图集成", HeadingLevel.HEADING_2),
|
|
|
bodyPara("投放点选择页整合了三种交互方式:地图直接点击选点、地点关键词搜索、附近推荐列表。地图基于高德 JS API 2.0 动态初始化,支持逆地理编码获取地址名称;若动态地图加载失败,自动降级为静态地图图片。"),
|
|
|
boldPara("js/location.js - 地图选点初始化"),
|
|
|
codeBlock([
|
|
|
'async function initPickerMap(containerId, onSelectCallback) {',
|
|
|
' const AMap = await loadAmapScript();',
|
|
|
' const container = document.getElementById(containerId);',
|
|
|
' container.style.width = "100%";',
|
|
|
' container.style.height = "280px";',
|
|
|
' container.innerHTML = "";',
|
|
|
' pickerMap = new AMap.Map(containerId, {',
|
|
|
' zoom: 15, center: [center.lng, center.lat], resizeEnable: true',
|
|
|
' });',
|
|
|
' // 点击地图选点',
|
|
|
' pickerMap.on("click", (e) => {',
|
|
|
' const lng = e.lnglat.lng, lat = e.lnglat.lat;',
|
|
|
' pickerGeocoder.getAddress([lng, lat], (status, result) => {',
|
|
|
' let address = result.regeocode.formattedAddress;',
|
|
|
' onSelectCallback({ lat, lng, name, address });',
|
|
|
' });',
|
|
|
' });',
|
|
|
'}'
|
|
|
]),
|
|
|
|
|
|
// 3.7 任务监控
|
|
|
subHeading("3.7 任务进度监控", HeadingLevel.HEADING_2),
|
|
|
bodyPara("任务页展示当前运输任务的进度、预计到达时间、飞行路径与投放点安全系数。进入页面时调用 API.getCurrentTask() 获取数据;若后端不可用,返回 Mock 数据保证界面不空白。"),
|
|
|
boldPara("js/app.js - 加载任务信息"),
|
|
|
codeBlock([
|
|
|
'async function loadTaskInfo() {',
|
|
|
' const task = await API.getCurrentTask(CONFIG.soldierId);',
|
|
|
' if (task) {',
|
|
|
' document.getElementById("task-id").textContent = task.id;',
|
|
|
' document.getElementById("task-status").textContent =',
|
|
|
' statusMap[task.status]?.text || task.status;',
|
|
|
' const prog = task.progress || 0;',
|
|
|
' const filled = Math.round(20 * prog / 100);',
|
|
|
' document.getElementById("task-progress-text").textContent =',
|
|
|
' "█".repeat(filled) + "░".repeat(20 - filled) + " " + prog + "%";',
|
|
|
' }',
|
|
|
'}'
|
|
|
]),
|
|
|
|
|
|
// 3.8 无人机状态
|
|
|
subHeading("3.8 无人机状态查看", HeadingLevel.HEADING_2),
|
|
|
bodyPara("无人机页展示实时飞行数据:速度、高度、电量、温度、距离目标等,以及最近动态日志。与任务页共用轮询机制,每 5 秒自动刷新。"),
|
|
|
boldPara("js/app.js - 加载无人机状态"),
|
|
|
codeBlock([
|
|
|
'async function loadDroneStatus() {',
|
|
|
' const status = await API.getDroneStatus();',
|
|
|
' if (status) {',
|
|
|
' document.getElementById("drone-battery").textContent = status.battery + "%";',
|
|
|
' document.getElementById("drone-speed").textContent = status.speed + "m/s";',
|
|
|
' document.getElementById("drone-alt").textContent = status.altitude + "m";',
|
|
|
' document.getElementById("drone-dist").textContent = status.distance + "m";',
|
|
|
' }',
|
|
|
' const logs = await API.getDroneLogs();',
|
|
|
' document.getElementById("drone-logs").innerHTML =',
|
|
|
' logs.map(l => `<div class="log-row">...`).join("");',
|
|
|
'}'
|
|
|
]),
|
|
|
|
|
|
// 3.9 SOS
|
|
|
subHeading("3.9 一键 SOS 求救", HeadingLevel.HEADING_2),
|
|
|
bodyPara("设置页提供紧急求救按钮,点击后二次确认,随后获取当前 GPS 坐标并立即上报后端 /api/sos。上报内容包含士兵ID、姓名、坐标和时间戳。"),
|
|
|
boldPara("js/app.js - SOS 求救"),
|
|
|
codeBlock([
|
|
|
'async function triggerSOS() {',
|
|
|
' if (!confirm("确认发送求救信号?此操作将立即上报您的当前位置。")) return;',
|
|
|
' const pos = await LocationModule.getCurrentPosition();',
|
|
|
' await API.sendSOS({',
|
|
|
' soldier_id: CONFIG.soldierId,',
|
|
|
' soldier_name: CONFIG.soldierName,',
|
|
|
' lat: pos.lat, lng: pos.lng,',
|
|
|
' time: new Date().toISOString()',
|
|
|
' });',
|
|
|
' showToast("🚨 求救信号已发送!");',
|
|
|
'}'
|
|
|
]),
|
|
|
|
|
|
// 3.10 API封装
|
|
|
subHeading("3.10 API 通信封装与离线降级", HeadingLevel.HEADING_2),
|
|
|
bodyPara("api.js 封装了所有后端接口请求,统一处理超时(5 秒)、JSON 序列化和错误捕获。对于投放点、任务、无人机状态等查询类接口,若后端不可用或超时,自动返回 Mock 数据,确保 APP 在离线/演示场景下仍可正常使用。"),
|
|
|
boldPara("js/api.js - 统一请求与超时控制"),
|
|
|
codeBlock([
|
|
|
'async function request(url, options = {}) {',
|
|
|
' const fullUrl = url.startsWith("http") ? url : BASE + url;',
|
|
|
' const controller = new AbortController();',
|
|
|
' const timeoutId = setTimeout(() => controller.abort(), 5000);',
|
|
|
' try {',
|
|
|
' const resp = await fetch(fullUrl, {',
|
|
|
' headers: { "Content-Type": "application/json" },',
|
|
|
' signal: controller.signal, ...options',
|
|
|
' });',
|
|
|
' clearTimeout(timeoutId);',
|
|
|
' return resp.json();',
|
|
|
' } catch (e) {',
|
|
|
' clearTimeout(timeoutId);',
|
|
|
' if (e.name === "AbortError")',
|
|
|
' throw new Error("请求超时,请检查后端是否启动");',
|
|
|
' throw e;',
|
|
|
' }',
|
|
|
'}'
|
|
|
]),
|
|
|
boldPara("js/api.js - Mock 降级示例"),
|
|
|
codeBlock([
|
|
|
'async function getDropPoints() {',
|
|
|
' try {',
|
|
|
' const data = await request("/api/drop-points");',
|
|
|
' return data.drop_points || data;',
|
|
|
' } catch (e) {',
|
|
|
' return getMockDropPoints(); // 离线兜底',
|
|
|
' }',
|
|
|
'}'
|
|
|
]),
|
|
|
|
|
|
// 第四章 总结
|
|
|
new Paragraph({ children: [new PageBreak()] }),
|
|
|
subHeading("四、设计特点与总结", HeadingLevel.HEADING_1),
|
|
|
bodyPara("单兵终端APP在设计上遵循“简单、可靠、可演示”的原则,主要特点如下:"),
|
|
|
new Paragraph({ numbering: { reference: "numbers", level: 0 }, spacing: { before: 80, after: 80 },
|
|
|
children: [new TextRun({ font: "宋体", size: 24, text: "混合架构,跨平台成本低:基于 Capacitor 将 Web 技术打包为 Android APK,一套代码同时支持手机浏览器预览和真机安装。" })] }),
|
|
|
new Paragraph({ numbering: { reference: "numbers", level: 0 }, spacing: { before: 80, after: 80 },
|
|
|
children: [new TextRun({ font: "宋体", size: 24, text: "多级定位降级,适应战场复杂环境:四级定位策略 + 手动修正,确保在城市、室内、弱网环境下仍能获取可用坐标。" })] }),
|
|
|
new Paragraph({ numbering: { reference: "numbers", level: 0 }, spacing: { before: 80, after: 80 },
|
|
|
children: [new TextRun({ font: "宋体", size: 24, text: "Mock 数据兜底,支持离线演示:所有查询类 API 在后端不可用时自动返回模拟数据,便于开发调试与现场演示。" })] }),
|
|
|
new Paragraph({ numbering: { reference: "numbers", level: 0 }, spacing: { before: 80, after: 80 },
|
|
|
children: [new TextRun({ font: "宋体", size: 24, text: "模块职责清晰,易于维护:app.js 负责业务与UI、api.js 负责网络、location.js 负责定位与地图,三层之间通过明确接口协作。" })] }),
|
|
|
new Paragraph({ numbering: { reference: "numbers", level: 0 }, spacing: { before: 80, after: 80 },
|
|
|
children: [new TextRun({ font: "宋体", size: 24, text: "安全与权限考虑:Android 开启明文传输用于局域网调试;高德地图、GPS 权限在运行时动态申请;SOS 操作提供二次确认防止误触。" })] }),
|
|
|
bodyPara("综上所述,单兵终端APP通过简洁的分层架构和稳健的降级策略,实现了前线士兵在复杂战场环境下的物资需求上报、位置共享与任务协同功能。"),
|
|
|
]
|
|
|
}]
|
|
|
});
|
|
|
|
|
|
Packer.toBuffer(doc).then(buffer => {
|
|
|
fs.writeFileSync("单兵终端APP_架构与功能实现说明.docx", buffer);
|
|
|
console.log("文档已生成:单兵终端APP_架构与功能实现说明.docx");
|
|
|
});
|