0
0
0

前端与客户端约定

文章摘要
|

来源:intranet-tunnel/docs/ui-refactor.md(整篇)(原文 17732 字符)

Web 管理端 UI 现代化改造说明

本文件记录本次「UI 现代化 + 图形验证码 + 短信验证码登录」改造的范围、约定与回滚方式。

  • 改造分支:feature/ui-refactor
  • 基线提交:7d18358 chore: 建立 UI 改造前的代码基线

一、改造目标与红线遵守情况

红线要求落实情况
禁止修改业务 API 接口、路径、参数未改动任何既有业务接口。仅 /api/auth/login 新增可选字段 captcha_id / captcha_code(不传时行为与改造前完全一致)。
禁止修改路由结构、路由守卫、鉴权逻辑router/index.js 的既有路由与 beforeEach 守卫逐字未动,仅新增 /account 一条路由。鉴权中间件新增的是独立的 optionalAuthMiddleware,不影响 authMiddleware
禁止修改 store 的 state/actions/getters 语义web/src/store/auth.js 零改动。短信登录返回与口令登录完全相同的 tokenPair 结构,因此复用 auth.setSession()
禁止修改业务组件的 props/emits/v-model 语义12 个既有页面的 <script setup> 仅新增组件 import(个别页面额外新增了只被模板使用的状态映射函数),业务逻辑与 props/emits/v-model 语义逐字未动。
禁止删除任何现有功能入口侧栏 12 个入口全部保留(仅重新分组),另新增「账号安全」。
所有改动可回滚、Git 分支隔离全部改动位于 feature/ui-refactor 分支,main 保持基线状态。

关于技术栈的一处偏离(重要)

提示词要求 TypeScript 5.3+Pinia,但本项目实际不存在 TypeScript 与 Pinia(全部为 .js / <script setup> + reactive store)。

为什么不做全量迁移

  1. 强制 TS 化意味着改写全部 .vue 文件,与「禁止修改业务组件语义」「可回滚」两条红线直接冲突;
  2. 引入 Pinia 会替换现有 store,与「禁止修改 store 语义」冲突;
  3. 本次改造的核心价值(设计令牌、1Panel 风格、暗色主题、验证码与短信登录)不依赖这两者。

不做全量迁移 ≠ 完全不用

这两件事需要分开看,否则会漏掉零成本的收益:

  • 新增文件可以用 TS,无需迁移旧文件。Vite 原生支持 .ts.vue 混编,新写的 composable、API 封装、类型定义完全可以用 TS,旧 .vue 保持 JS。本次受时间约束未采用,但后续新增代码应默认用 TS。
  • 「禁止修改 store 语义」≠「禁止新增 store」。跨组件状态若无规划会散落各处。本项目现有做法是:composables/ 下的模块级单例(见 useTheme.js)配合组件内 ref。这是刻意选择的替代路径——它与旧 store 完全隔离,不触碰 store/auth.js 的任何语义,同时满足「状态只有一份」的要求。

如果将来要做迁移,建议路径

  1. 先加类型定义,不改实现:为 API 响应与领域模型新增 web/src/types/*.ts.vue 文件通过 JSDoc @type 引用,可在零改写的前提下获得大部分类型检查收益。
  2. 再逐个文件改 <script setup lang="ts">:从叶子组件开始(components/common/ 优先),每次一个文件、单独提交,保证可回滚。
  3. Pinia 只在确有跨页面共享需求时引入:当前 auth 的状态集合很小且语义清晰,迁移到 Pinia 属于形式收益。若有新的跨页面状态,建议为它单独建 store,而不是把 auth 一并搬过去。
  4. 全程保持 vue-tsc --noEmit 作为独立 CI 步骤,不与构建耦合。

devDependencies 中的 typescript / vue-tsc 已就位,可作为上述迁移的起点。


二、设计令牌体系

新增目录 web/src/styles/,导入顺序不可调换:

文件职责
tokens.scss定义全部 --app-* 令牌(亮色 + html.dark 暗色)
reset.scss必要的基础归一化、滚动条、工具类、prefers-reduced-motion
ep-bridge.scss--app-* 单向映射到 Element Plus 的 --el-*
ep-overrides.scss变量覆盖不到的结构细节(内边距、分隔线、字号)
transitions.scss过渡类与关键帧(状态点脉冲、骨架屏、路由切换)
legacy.scss旧页面兼容层(见下)
index.scss入口,按上述顺序 @use

命名空间隔离

所有令牌使用 --app- 前缀,与 Element Plus 的 --el- 完全隔离,避免升级 EP 时冲突。

单向桥接与 html.dark 重复展开

ep-bridge.scss@mixin el-token-bridge 组织映射,并在 :roothtml.dark 下各展开一次。

重复展开是必需的:Element Plus 官方暗色包(theme-chalk/dark/css-vars.css)会在 html.dark 下重新定义 --el-*,其特异性(0,1,1)高于 :root(0,1,0)。只在 :root 映射会导致暗色下主色被官方值覆盖。展开两处后特异性相同,再由导入顺序(本文件在官方 CSS 之后)稳定胜出。

旧页面兼容层

legacy.scss 保留了改造前 styles.css 的全部全局类名(.page / .card / .stat-card / .chart / .mono …),但把硬编码颜色替换为令牌引用。

效果:12 个既有页面零改动即获得暗色主题支持与统一视觉密度,同时满足「可回滚、不破坏现有页面」的约束。随着页面逐个迁移到新组件,该文件可整段删除。


三、新增通用组件

位于 web/src/components/common/

组件说明
PageContainer.vue页面版心(1440px 居中)、标题/描述/操作区、工具条插槽
DataCard.vue卡片容器:标题 / 描述 / 右上角操作 / 底部说明
ProTable.vue表格外壳:加载态、空状态、分页器、事件透传
EmptyState.vue轻量空状态
StatusDot.vue状态点(online 绿 + 2s 脉冲至 2.4 倍,offline 灰,error 红,warning 橙)
ThemeToggle.vue主题切换按钮
CaptchaImage.vue图形验证码图片(点击刷新、主题切换自动重取、暴露 refresh()
LoginMethodSwitcher.vue登录方式分段控件

ProTable 刻意不接管列定义:列仍由页面用 <el-table-column> 写在默认插槽里。原因是既有页面已有大量含自定义单元格与格式化函数的列定义,抽象成配置数组会显著降低可读性,也不利于回滚。

主题管理

web/src/composables/useTheme.js 基于 @vueuse/coreuseDark,采用模块级单例,避免同页面出现多份互不同步的暗色状态。存储键 app-theme,未设置过时跟随 prefers-color-scheme


四、登录增强 API 契约

1. GET /api/auth/config(新增,无需令牌)

登录页据此渲染登录方式,前端不硬编码假设。

{
  "password_login": true,
  "sms_login": true,
  "captcha_enabled": true,
  "captcha_sms_required": true,
  "sms_provider": "aliyun",
  "sms_mock": false,
  "bound_phone_count": 1,
  "sms_code_length": 6,
  "sms_interval": 60
}

sms_login 仅在「服务端启用短信」且「已存在绑定手机号的账号」时为 true。 这避免了用户填完手机号才发现无法登录。

2. GET /api/auth/captcha?theme=light|dark(新增,无需令牌)

{
  "captcha_id": "a1b2...c3.d4e5...f6",
  "image_base64": "iVBORw0KGgo...",
  "image_type": "image/png",
  "expires_in": 300,
  "code": "RW52"
}
  • code 仅在模拟模式下返回,用于联调与端到端测试;真实服务商配置下该字段被省略。
  • 主题参数影响图片配色(亮底深字 / 深底浅字)。

3. POST /api/auth/login(既有接口扩展)

{ "username": "admin", "password": "...", "captcha_id": "...", "captcha_code": "RW52" }

captcha_id / captcha_code可选字段:

  • 服务端未启用图形验证码时,不传即可,行为与改造前一致;
  • 服务端启用后不传,返回 400 {"error":"请输入图形验证码"}

服务端启用了图形验证码时,校验发生在 bcrypt 比对之前,因此图形验证码能有效拦截机器批量尝试,而不会让 bcrypt 成为 CPU 放大点。

4. POST /api/auth/sms/send(新增,无需令牌)

{ "phone": "13800138000", "purpose": "login", "captcha_id": "...", "captcha_code": "RW52" }
  • purpose 支持 login / bindbind 需要携带有效访问令牌。
  • 该路由挂载了 optionalAuthMiddleware(有令牌则注入声明,无令牌放行)。

成功:

{ "success": true, "message": "验证码已发送,请注意查收", "phone": "138****8000", "expires_in": 300, "interval": 60 }

限流(HTTP 429,带 Retry-After 头):

{ "success": false, "error": "验证码发送过于频繁", "retry_after": 59 }

5. POST /api/auth/sms/login(新增,无需令牌)

{ "phone": "13800138000", "code": "508295" }

成功时返回与口令登录完全一致tokenPair

{ "token": "...", "refresh_token": "...", "expires_at": "...", "expires_in": 3600, "user": { ... } }

因此前端 store 无需为短信登录维护第二套会话写入逻辑。

6. 手机号绑定(需令牌)

方法路径说明
POST/api/auth/phone{ phone, code } 校验验证码后绑定
DELETE/api/auth/phone解除绑定

7. 短信服务观测(需令牌)

方法路径说明
GET/api/sms/stats统计概览;模拟模式下附 mock_records(含明文验证码)
GET/api/sms/logs发送日志分页(手机号已脱敏)
POST/api/sms/test{ phone } 通道测试发送,用于设置页验证配置

/api/sms/test 的三点设计约定:

  1. 使用独立的 purpose=test,使测试短信在发送日志中可区分——测试短信真实计费,必须可对账;
  2. 不校验图形验证码:调用方已持有有效管理员令牌,再要求验证码只会阻碍排障;
  3. 仍完整受手机号与 IP 维度的频控保护,因此不会被拿来刷短信。

五、安全设计要点

  1. 验证码不可逆存储:短信验证码以 bcrypt 摘要入库,数据库泄露无法还原明文。
  2. 一次性消费:无论校验成功或失败,验证码条目立即失效,杜绝重放。
  3. 失败计数:同一验证码连续失败达 SMS_MAX_VERIFY_ATTEMPTS 次即作废,把在线暴力枚举窗口压缩到个位数。
  4. 图形验证码一次性:校验即删除,且条目 ID 带 HMAC 签名,无法伪造或遍历。
  5. 限流口径统一为手机号维度:最小发送间隔不区分用途,否则攻击者可轮换 purpose 绕过限制实施短信轰炸。
  6. 失败也计入配额:发送日志在成功与失败时都记录,统计口径包含失败——否则可用不可达号码无限刷接口。
  7. 凭据只从 .env 读取:短信服务商的 AccessKey / Secret 不在设置页白名单内,后台被攻破也不会连带泄露云账号密钥。
  8. 模拟模式显式告警mock 模式启动时打印 WARNING,且验证码明文仅在模拟模式下随响应返回。

六、数据库变更

新增两张表(AutoMigrate 自动创建,四库通用):

用途
sms_codes验证码记录(code_hash 为 bcrypt 摘要、usedattemptsexpires_at
sms_send_logs发送日志(成功与失败都记录,供限流统计与审计)

users 表新增列:phone(唯一索引,可空)、phone_verifiedphone_bound_atlast_login_ip

phone 使用指针类型*string):唯一索引在四种数据库下都允许多个 NULL,而空字符串在 MySQL 下会互相冲突。


七、配置项

图形验证码

变量默认说明
AUTH_CAPTCHA_ENABLEDtrue登录是否要求图形验证码
AUTH_CAPTCHA_SMS_REQUIREDtrue发送短信前是否强制图形验证码
AUTH_CAPTCHA_LENGTH4字符数(3-8)
AUTH_CAPTCHA_TTL5m有效期
AUTH_CAPTCHA_WIDTH / AUTH_CAPTCHA_HEIGHT120 / 40图片尺寸

短信服务

变量默认说明
SMS_ENABLEDfalse短信服务总开关
SMS_PROVIDERmockaliyun / tencent / huawei / webhook / mock
SMS_MOCK_MODEfalse强制使用模拟服务商
SMS_SIGN_NAME短信签名
SMS_TEMPLATE_CODE模板编号
SMS_CODE_LENGTH6验证码位数(4-8)
SMS_CODE_TTL5m有效期(1 分钟 - 1 小时)
SMS_SEND_INTERVAL60s同手机号最小发送间隔(≥ 30s)
SMS_MAX_PER_HOUR5单手机号每小时上限
SMS_MAX_PER_DAY10单手机号每日上限
SMS_MAX_PER_IP_HOUR20单 IP 每小时上限
SMS_MAX_VERIFY_ATTEMPTS5单个验证码连续失败上限(3-20)

各服务商凭据:ALIYUN_SMS_*TENCENT_SMS_*HUAWEI_SMS_*(含 HUAWEI_SMS_SENDER 通道号)、SMS_WEBHOOK_*

可在设置页热更新

sms.enabledsms.providersms.sign_namesms.template_codesms.code_lengthsms.code_ttlsms.send_intervalsms.max_per_hoursms.max_per_daysms.max_per_ip_hourauth.captcha_enabledauth.captcha_sms_requiredauth.captcha_length

热更新通过 sms.Manager.Reload() 整体原子替换运行时快照实现(配置 + 供应商实例一起换,避免出现「新配置配旧供应商」的中间态),同时保留图形验证码存储与内存限流计数。


八、短信服务商对接

server/internal/sms/ 下的 Provider 接口屏蔽了各厂商差异:

实现签名方式
aliyun.goRPC 风格 HMAC-SHA1(StringToSign = GET&%2F&percentEncode(query)
tencent.goTC3-HMAC-SHA256(规范请求串 → 待签串 → 逐级派生密钥)
huawei.goWSSE UsernameToken(PasswordDigest = Base64(SHA256(Nonce + Created + AppSecret))
webhook.go自定义 HTTP 端点,支持 URL / 方法 / 请求头 / 请求体模板(占位符 {{phone}} {{code}} 等)
mock.go不实际发送,验证码写入日志与内存缓冲(上限 200 条)

模板变量约定:

  • 阿里云:{"code":"1234"}(变量名固定为 code
  • 腾讯云:["1234"]TemplateParamSet
  • 华为云:["1234"]templateParas,JSON 数组字符串)

九、回滚方式

# 停止服务
git checkout main          # 回到基线(前端与后端全部回到改造前状态)
cd web && npm run build    # 重建前端产物
cd ../server && go build -o ../bin/tunnel-server.exe ./cmd/server

# 如需保留短信功能而只回滚 UI:
git checkout feature/ui-refactor
git revert <UI 相关提交>

数据库层面:新增表与新增列都是向后兼容的增补。回滚到旧版本时旧代码不会读取这些列/表,无需数据迁移。若确需清理,可手动 DROP TABLE sms_codes, sms_send_logs

测试期数据清理(本地):删除 server/data/tunnel.db 后重启即可。


十、验证记录

后端端到端(16 步全通过)

在本地以 SMS_MOCK_MODE=true + AUTH_CAPTCHA_ENABLED=true 启动服务端,脚本化验证:

步骤预期实测
1. /api/auth/config 初始状态sms_login=falsebound_phone_count=0一致
2. 不带验证码登录400{"error":"请输入图形验证码"}
3. 错误验证码登录400{"error":"图形验证码错误或已失效,请重新获取"}
4. 正确验证码登录200 + tokentoken 长度 299,expires_in=3600
5. 发送绑定短信200phone=138****8000expires_in=300
6. 读取模拟验证码拿到明文508295
7. 绑定手机号200手机号绑定成功
8. /api/auth/me反映绑定phone=13800138000phone_verified=true
9. 复用已消费的验证码400{"error":"请先获取短信验证码"}
10. 绑定后能力声明sms_login=truebound_phone_count=1
11. 短信验证码登录200 + tokensms_login_user=admin
12. 用短信登录令牌访问受保护接口200role=admin
13. 重放已消费的短信验证码401{"error":"请先获取短信验证码"}
14. 间隔内重发429retry_after=59
15. 缺少图形验证码发送短信400{"error":"请输入图形验证码"}
16. 非法手机号400{"error":"手机号格式不正确"}

单元测试

server/internal/captcha/captcha_test.go:12 个用例全部通过,覆盖生成/校验/一次性消费/过期/畸形 ID/nil 安全/双主题渲染/参数收敛/字符集排除易混淆字符/随机性与 ID 唯一性。

前端产物

  • npm run build 成功,无告警(已启用 Sass modern-compiler API)。
  • 静态资源与 history 深链接(/login/account/dashboard/settings/tunnels)均返回 200 且为 text/html
  • CSS 产物(393 KB)已确认包含 --app-* 令牌、暗色覆盖、状态点脉冲关键帧、路由过渡类与 EP 桥接映射。

服务端

  • go build ./...go vet ./...gofmt 全部干净。
  • 启动日志确认新模块装配:短信验证码登录已启用 provider=mock mock=true captcha=true

十一、逐页迁移完成情况(12/12)

改造采用「先铺基础设施、再逐页迁移」的两阶段策略:第一阶段通过 legacy.scss 兼容层保证旧页面零改动即可获得暗色支持;第二阶段逐页迁移到新组件。

已完成迁移

页面主要改动
DashboardView统计卡组收进 DataCard;内部类名 .stat-* 重命名为 .metric-*,避免与兼容层同名造成歧义
ClientsViewProTable + StatusDot(在线/离线)
TunnelsViewProTable;客户端列的在线/离线改用 StatusDot
StatsView4 张统计卡改 DataCard;图表与明细表各自入 DataCard
DdnsView统计卡改 DataCard + StatusDot(同步结果)
ProxiesViewProTable + StatusDot(启用/停用)
CertsViewProTable + StatusDot(有效/即将过期/已过期)
ForwardsViewProTable + StatusDot(启用/停用)
LogsView三套日志视图分别入 DataCard;筛选条移入 #toolbar;SSE 区块完整保留
SettingsView5 张卡片改 DataCard新增「图形验证码与短信登录」分组;修复底部操作栏定位缺陷
BackupView统计卡与各功能块改 DataCard;历史表改 ProTable
AccountView本次新增页面,直接使用新组件

验收标准(全部页面通过)

  • 行内 style=" 出现次数为 0
  • 无硬编码颜色(十六进制 / rgb / hsl / 颜色关键字);
  • 不引用兼容层旧类名(.page / .card / .stat-* / .mono / .muted);
  • 使用 @vue/compiler-sfc 对 parse + compileScript + compileTemplate + compileStyle(scss) 全量离线编译通过
  • npm run build 成功且无告警;服务端嵌入新产物后,13 条路由深链接全部返回 200。

功能等价性

各页面改造严格限定在模板结构与样式层面:<script setup> 仅新增组件 import (CertsView / DdnsView 额外新增了一个只被模板使用的状态映射函数), 业务函数、API 调用、事件绑定、v-model、提示文案逐字保留。

零功能增补(复核后修正)

迁移过程中曾出现过两处超出「功能等价」的改动,经复核后已全部回退

  1. TunnelsView 新增「状态」列(已删除)。原因:model.Tunnel.Status 虽然带 json:"status" tag 会随响应返回,但后端只在 enabled 变化时同步写它,而常量 TunnelStatusError 在全仓库只有定义、从无赋值——也就是说该字段目前只是 enabled 的镜像,error 属预留能力。新增该列属于重复表达,且 error 永远不会出现。
  2. ForwardsView / ProxiesView 在「启用」列内 switch + StatusDot 并列(已回退为纯 switch)。原因:两者读取同一个 row.enabled,属同一信息的两种呈现。

结论StatusDot 只用于表达独立的运行状态——客户端在线/离线、证书有效期、DDNS 同步结果、日志级别、短信通道状态;而「启用/停用」这类配置开关el-switch 表达,不再叠加状态点。

兼容层的去留

styles/legacy.scss 目前仍然保留:虽然 12 个页面都已不再引用其中的类名, 但删除它属于纯清理、与功能无关。建议在下一版本确认线上无回归后再删除, 届时一并移除 styles/index.scss 中对应的 @use


十二、视觉走查(无头浏览器 + 可编程审计)

方法与工具

本机没有可用的图像识别能力,无法"看"截图。因此把走查拆成两部分:

  1. 截图留档:用 Chrome 无头模式 + CDP 驱动真实浏览器渲染(Node 24 自带 WebSocket,零额外依赖,未安装 Playwright),产出 6 组基线图(亮/暗 × 1440/768/375)共 78 张。每张截图都附带自验证结果(实测 html.dark 类与 location.pathname),确保"截到的是目标页面且主题已生效"。
  2. 可编程审计:把视觉走查中可客观度量的部分自动化——横向溢出、元素越界、文字对比度(按 WCAG AA)、sticky 祖先 overflow 链、布局关键尺寸、触控目标尺寸。这几项恰好是暗色模式与响应式最常见的真实缺陷来源。

工具脚本为 .verify/shoot.mjs(截图)与 .verify/audit.mjs(审计),位于 .gitignore 内,属本地验证工具。

发现并修复的问题(均为代码审查看不到的)

#问题实测证据修复
1侧栏分组标题对比度不足#a9aeb8 on #fff = 2.23:1(AA 要求 4.5)改用 --app-text-secondary → 5.38:1
2各页说明文字对比度不足.metric-extra / .stat-extra / 空状态文案同为 2.23:1同上
3el-alert 文字对比度不足,且首次修复无效info alert 2.92:1;EP 的 description 规则是 .el-alert--info.is-light .el-alert__description(特异性 0,3,0),我最初的 (0,2,0) 被压过改为 .el-alert.is-light .el-alert__description(同特异性 + 导入顺序胜出)
4暗色下主色按钮文字对比度不足白字 on #3c7eff = 3.5:1暗色主色调为 #4d8bff,按钮文字改用 --app-text-inverse → 约 5.2:1
5暗色下侧栏激活项对比度不足#3c7eff on #1c2c48 = 3.9:1--app-primary-light 调深为 #16233a → 约 5.0:1
6第 4 项的修复误伤 plain 类型「刷新」「发送测试短信」跌到 1.05:1(深字压深底)选择器加 :not(.is-plain):not(.is-text):not(.is-link)
7分页器在窄屏横向溢出375px 下溢出到 797pxProTable 用 useMediaQuery 动态收窄 layoutpager-count
8设置页表单在窄屏横向溢出375px 下说明文字溢出到 444px窄屏下 el-form-item 改标签顶对齐、固定宽度控件占满整行
9sticky 祖先 overflow 的判定误报.content(滚动容器)被误判为阻塞审计脚本区分「可滚动祖先」与「裁剪祖先」

第 6 项值得特别注意:修复本身引入了新缺陷,若没有修复后的复审就会漏掉。

修复后验证结果

断点亮色暗色
1440结构性缺陷 0;低对比 8 页(均为 EP 固有,见下)结构性缺陷 0;低对比 2 页
768结构性缺陷 0(无任何溢出)
375结构性缺陷 0(无任何溢出)

图像能力复核补充(第二轮)

启用视觉识别能力后,对截图做了真正的"看图"复核,又发现三处问题:

#问题证据处理
10图形验证码完全不显示CaptchaImage.vue 写成 const { data } = await api.captcha(...),但 axios 响应拦截器已 return response.data,再次解构 .data 得到 undefineddata.captcha_id 抛错进 catch 并清空图片已修复为直接取值。复核确认:图片 naturalWidth×Height = 120×40imgComplete=true、占位图标消失、控制台与网络均无错误;截图可见验证码字符与 API 返回的 code 一致
11未选中的复选框几乎不可见EP 默认边框经映射后为 #e5e6eb on #fff = 1.2:1(暗色 #333335 on #232324 = 1.15:1)el-checkbox__inner / el-radio__inner 边框改用 --app-text-placeholder(亮 2.2:1 / 暗 3.0:1),截图复核确认方框清晰可见
12窄屏空状态被挤压375px 下多列表格横向滚动,空状态文案换行 4 次尝试修复后撤销,详见下

第 10 项暴露了此前的验证盲区:接口级端到端测试全部通过,却覆盖不到"前端组件是否正确消费了响应"。这正是必须做真实浏览器复核的理由。

第 12 项是一次修复引入回归的实例:最初的修法是给 .el-table__empty-blockposition: sticky; left: 0; min-width: 100%,截图复核发现空状态文案完全消失——min-width 的 100% 以表格内容宽度(而非滚动容器宽度)为基准,居中文案被推到可视区之外。已撤销并精确回滚(回滚后截图 SHA 与修复前一致)。

图像能力逐页复核(第三轮)

启用视觉识别后,对 6 组基线图做了逐页复核。为控制复核成本,用 System.Drawing 把多页拼成一张联系表(contact sheet,每格带页面名标签)后再看,一次可覆盖 6 页。

覆盖范围页数说明
暗色 144013 / 13全覆盖
亮色 14406settings / backup / logs / proxies / stats / account(最复杂的表单与列表页)
亮色 7686平板断点
亮色 3756小屏
暗色 3756小屏暗色

复核确认已修复项在真实渲染中确实生效

  • 图形验证码正常显示——截图中读到的字符为亮色 N6GF77YP,暗色 28CU78BX均与当次 API 返回的 code 一致
  • 未选中复选框边框在亮色、暗色、桌面、小屏下均清晰可辨;
  • el-alert 文案在两种主题下都可读(certs 页「证书自动化未启用」、backup 页「本地存储不加密」、settings 页默认口令警告、proxies 页上游安全策略);
  • 窄屏(375)下表单标签顶对齐、sticky 操作栏吸附在底部、无任何横向溢出;
  • 768 平板断点同样为标签顶对齐布局;
  • account 页的数据渲染(已绑定手机号、通道统计、发送记录与分页器)与 logs 页的级别 StatusDot 均正常。

未发现新的结构性问题。

未覆盖部分:亮色 1440 的 6 个页面(dashboard / clients / tunnels / ddns / certs / forwards)——它们的暗色版本已复核且结构一致,亮色下也已通过对比度审计;如需彻底覆盖可再跑一轮。

判断 4 的结论:sticky 操作栏有效

逐层实测输出:

sticky: position=sticky bottom=900 可见=true 裁剪祖先=[无] 滚动祖先=[content:hidden auto]
  • 元素 position: sticky 生效,bottom 值恒等于视口高度 → 确实吸附在视口底部
  • 祖先链中没有任何「裁剪且不可滚动」的元素裁剪祖先=[无]);
  • 唯一的 overflow 祖先是 .content,它正是布局层的滚动容器(scrollHeight > clientHeight),sticky 本就相对它定位——这是正常且必需的,不是失效场景。

三个断点(1440 / 1024 / 812)均验证可见。移动端抽屉 z-index 为 2000、遮罩 1500,操作栏为 1001,抽屉打开时遮罩会正常覆盖它,不存在穿透。

已知项(有意不修复,属 Element Plus 固有设计)

以下低对比项来自 Element Plus 组件的既定设计(与本项目的令牌映射无关)。本次有意不覆盖——过度定制第三方组件会显著增加后续升级成本:

场景对比度说明
el-tageffect="light"(成功/警告/模拟…)2.4–2.6:1语义标签有背景色块辅助识别,且多为短文本
表单控件的 placeholder(「请选择」)2.2–3.1:1placeholder 语义上应弱化;EP 默认即该量级
el-tabs 激活项4.4:1距 AA 差 0.1,实际观感无问题
link 类型按钮的语义色(危险操作)2.2–3.3:1EP 用语义色作为链接文字
窄屏(≤375px)下的表格空状态多列表格横向滚动导致空状态宽度受限、文案换行较多。纯 CSS 无法表达"滚动容器的可视宽度",sticky 方案已验证无效(见第 12 项)。有数据时不会出现,暂接受现状

如需进一步提升,建议新增 --app-*-text 系列的深色变体并映射到 EP,而不是直接改语义色令牌。

仍需人工复核的部分

截图已产出,但主观观感——配色是否协调、间距是否舒适、动效是否顺滑、图表可读性、长域名与长日志行的视觉表现——无法由脚本判定,仍需人眼过一遍:

.verify/shots/
├── light-1440/  dark-1440/     # 桌面
├── light-768/   dark-768/      # 平板
└── light-375/   dark-375/      # 手机

每个目录含 13 张页面截图(login.pngdashboard.pngclients.pngsettings.pngaccount.png)。


十三、仍未完成

  1. TypeScript 与 Pinia 迁移:见第一节的「如果将来要做迁移,建议路径」。

来源:intranet-tunnel/mobile/README.md(整篇)(原文 9376 字符)

内网穿透控制台 —— 移动端 App

uni-app(Vue 3)实现的移动端管理工具,是 Web 管理端的轻量补充: 聚焦「查看 + 轻操作 + 告警」,复杂配置仍在 Web 端完成。


一、当前进度

阶段内容状态
现状识别与技术选型✅ 完成(docs/mobile-phase1-discovery.md
项目初始化、设计 Token 同步、登录与鉴权、API 封装、安全存储✅ 完成
底部 Tab、仪表盘、客户端/隧道列表与详情✅ 完成
告警中心(列表/详情/已读/已处置/未读角标)✅ 完成
轻操作(隧道启停、强制下线、证书续期、DDNS 同步)✅ 完成
扩展(扫码、日志、备份、多环境切换、生物识别)⏳ 待开始
打磨与发布⏳ 待开始

阶段二已交付:启动分发、服务器地址配置(多环境)、登录页(口令 / 短信 / 邮箱 + 图形验证码)、 令牌安全存储与自动刷新、统一请求层与错误映射、Mock 层、亮暗双主题。

阶段三已交付

  • 底部四 Tab(首页 / 客户端 / 隧道 / 我的),图标由脚本生成并按主题切换两套配色
  • 仪表盘:四张摘要卡 + 流量趋势(1h/6h/24h/7d,24 根柱子)+ 最近告警 + 快捷入口
  • 客户端列表(搜索防抖、状态筛选、分页加载)与详情(基本信息 + 名下隧道)
  • 隧道列表(类型/状态筛选、搜索)与详情(实时流量 + 趋势图 + 配置摘要 + 一键复制入口)
  • 「我的」:账号、多环境、主题三选一、通知偏好(如实标注"仅本地")、关于、退出登录
  • 离线缓存:断网时列表与详情显示上次数据并标注时间(需求 5.4)

阶段三附带修复(契约 v1.0.6 的阻塞性变更): 服务端把口令登录改成两阶段(账号开启两步验证时返回质询而非令牌)。 App 若不识别 mfa_required,会把质询当会话存起来 —— 表现为「登录看着成功、进去却是未登录」且全程无报错。 现已适配:登录页增加第二步(6 位验证码 + 120 秒倒计时),判定逻辑抽成 utils/auth-flow.js 并由 19 项单元断言守住(npm run selftest 的 auth-flow 一节)。

阶段四已交付

  • 告警中心:列表(状态/级别/类型三级筛选)、详情、标记已读、标记已处置、未读角标
  • 进入详情自动标记已读;已处置的告警不显示"标记已读"(服务端不会把状态回落)
  • 告警消息一键复制、关联对象(客户端/隧道)跳转
  • 真实接口对接验证npm run verify-live,68 项断言):

打真实 HTTP,并直接 import App 的解析函数去处理真实响应 —— 这是唯一能发现"契约文档与实现不一致"的检查(见下)

阶段五已交付(轻操作):

  • 隧道:启停、编辑名称/备注、删除(要求输入隧道名称确认
  • 客户端:强制下线(含"不在线返回 409"的友好处理)
  • 证书:列表(数据库 + 存储目录两份数据源)、详情抽屉、手动续期(长超时 + 防重复提交)
  • DDNS:记录与状态、手动同步(原样透出 DNS 服务商的错误文案)
  • 端口转发:列表(实时连接数)、启停(必须提交完整规则,见 api/forwards.js
  • 首页快捷入口扩为 8 个(两行),覆盖全部已实现模块

⚠️ 本期不做(服务端契约明确不做,见 docs/mobile-api-contract.md §6): 推送通道与设备注册接口、异地登录告警、客户端 CPU/内存监控、流量配额告警、 RBAC 资源归属、多设备会话管理。App 侧已按此收敛,未写任何"调不通的代码"。 通知偏好的开关因此只存本地,界面上明确标注,不做"看起来能同步"的假象。


二、目录结构

mobile/
├── scripts/
│   ├── sync-tokens.mjs        设计 Token 同步(生成 CSS / SCSS / JS 三份)
│   ├── gen-tabbar-icons.mjs   tabBar 图标生成(16 个 PNG,亮暗两套)
│   ├── check-sfc.mjs          模板一致性检查(抓"漏 import"这类构建不报的错)
│   ├── selftest.mjs           纯逻辑与资源自测(219 项断言,Node 直接跑)
│   ├── verify-live-api.mjs    **真实接口对接验证**(打真实 HTTP,需先起本地实例)
│   ├── start-e2e-server.ps1   启动隔离的本地服务端(供上面那个脚本用)
│   └── _node-import-hook.mjs  让 Node 能 import 带 @/ 别名的源码模块
├── src/
│   ├── api/                   请求层与接口封装(页面禁止直接调 uni.request)
│   │   ├── error.js           ApiError(单独成文件以避免循环引用)
│   │   ├── request.js         统一请求、鉴权、刷新、错误映射、Mock 分流
│   │   ├── auth.js            登录 / 验证码 / 2FA / 刷新 / 探活
│   │   ├── dashboard.js       仪表盘摘要(含字段归一化,兼容契约漂移)
│   │   ├── clients.js         客户端列表 / 详情 / 名下隧道
│   │   ├── tunnels.js         隧道列表 / 详情
│   │   ├── stats.js           流量时间序列与抽稀
│   │   ├── alerts.js          告警列表 / 详情 / 已读 / 处置(返回归一化对象)
│   │   ├── certs.js           证书列表(含存储目录)/ 续期 / 到期判定
│   │   ├── ddns.js            DDNS 记录 / 状态 / 手动同步
│   │   └── forwards.js        端口转发列表 / 统计 / **安全的启停封装**
│   ├── components/            CaptchaImage / ConfirmDialog / EmptyState / SkeletonList / StatusDot /
│   │                          TrafficBars / TunnelActions
│   ├── composables/           useTheme(页面根节点必须绑 themeClass)、usePagedList
│   ├── config/                运行期配置、告警元数据(alert-meta)、路由映射(route-map)
│   ├── mock/                  Mock 数据(与**实现**同构,不是与文档同构)
│   ├── pages/                 launch / login / server / index / clients / tunnels / alerts /
│   │                          certs / ddns / forwards / mine
│   ├── store/                 Pinia:app(主题、服务器、角标)、auth(会话)
│   ├── styles/                tokens.scss(生成)、tokens.js(生成)、common.scss
│   ├── utils/                 storage / secure-storage / crypto / theme / url / format /
│   │                          toast / badge / offline-cache / notify-prefs /
│   │                          auth-flow(登录响应判定)/ alert-flow(告警解析)
│   ├── static/tabbar/         16 个 tabBar 图标(生成,勿手改)
│   ├── uni.scss               SCSS 变量映射(生成,勿手改)
│   ├── App.vue / main.js / pages.json / manifest.json
├── index.html
├── vite.config.js
└── package.json

三、快速开始

cd mobile
npm install

# H5 调试(默认代理到 http://127.0.0.1:47801,见 vite.config.js)
npm run dev:h5

# 连云端服务端
# 启动后在 App 内「服务器地址」页填 https://tunnel.sushike.cloud

# 构建
npm run build:h5
npm run build:app          # 需 HBuilderX 或 uni-app CLI + DCloud appid
npm run build:mp-weixin

自测与校验

npm run check          # 一次跑完下面三项(提交前必跑)
npm run check-tokens   # 设计 Token 是否与 Web 端一致
npm run check-sfc      # 模板有没有引用未 import 的标识符
npm run selftest       # 219 项纯逻辑与资源断言
npm run gen-tabbar     # 重新生成 tabBar 图标(改了 Token 颜色后执行)

# 真实接口对接验证:需要先起一个本地服务端实例
# (用法见 docs/mobile-test-checklist.md 的「十五、真实接口对接验证」)
npm run verify-live

三个静态检查各自防的是不同的坑:

检查防的是什么不跑会怎样
check-tokens有人改了 Web 端 tokens.scss 却没同步移动端两边品牌色慢慢分叉,只能靠肉眼发现
check-sfc模板里写了 {{ formatBytes(x) }} 但忘了 import构建照样成功,打开该页面才白屏
selftest纯逻辑回归(编解码、格式化、地址校验、路由映射、2FA 判定、告警解析、图标形状)改动悄悄破坏既有行为而无人察觉
verify-live契约文档与实现不一致样本本身是错的 → 自测全绿、真实对接取到 undefined

verify-live 的存在理由值得单独说明:前三项的断言样本都是"我按契约文档写的"。 本项目已经实测到两处文档与实现不符(告警未读数的嵌套结构、仪表盘的三个字段名), 都是"不报错、只让功能静默失效"的类型 —— 只有打真实 HTTP 才能发现。

check-sfc 的存在理由值得单独说明:Vue 的模板编译不检查 <script setup> 里的标识符。 本机也没有可用的浏览器自动化(模型不支持读图、无 puppeteer/modlens), 因此"页面能不能跑起来"只能靠这个静态检查 + 人工按测试清单走一遍来补。


四、设计 Token 同步

移动端的颜色、间距、圆角、字号不在本项目手工维护,而是由脚本从 web/src/styles/tokens.scss 生成三份文件:

生成物内容为什么这样拆
src/styles/tokens.scssCSS 变量定义(亮色 109 项 / 暗色 46 项)App.vue 里 import 一次
src/uni.scssSCSS 变量映射(91 个 $app-*uni-app 会注入每个组件,只能放变量声明
src/styles/tokens.js14 个常用颜色的 JS 取值 × 2 主题原生控件吃不到 CSS 变量(tabBar 配色与图标、将来的 canvas 填充色)

组件样式里不要 import tokens.js 取色 —— 那种场景直接用 CSS 变量 (var(--app-primary)),否则主题切换会失效。tokens.js 只服务"必须用 JS 赋值"的场合。

⚠️ 不要把 CSS 规则写进 src/uni.scss:它会被注入到每个组件, 规则会随之重复输出,产物体积随组件数线性膨胀。

跨端主题方案

  • 亮色:page, .theme-light { ... }
  • 暗色:html.dark, .theme-dark { ... }

每个页面的根节点都必须绑 themeClass(来自 composables/useTheme.js):

<template>
  <view class="page" :class="themeClass"> ... </view>
</template>

漏绑的后果是「这个页面在暗色模式下仍是亮色」——不会报错,只是看起来不对。 H5 端额外由 utils/theme.js<html>dark 类,让原生弹窗与滚动条一起跟随。

pages.json 里的导航栏颜色是编译期常量(无法引用 Token), 运行时由 applyNavigationBar() 按主题改写。


五、安全说明(务必读完再发布

实现说明
Token 存储utils/secure-storage.js三级自动降级:系统安全区(Android Keystore)→ 原生插件 → 混淆存储
当前安全等级「我的 → 关于 → 令牌存储」实时显示如实告知实际等级,不假装已接入安全区
通信加密utils/url.js 强制 HTTPS仅放行本机/内网地址的 http://,公网明文直接拒绝
明文入库禁止加密失败时放弃持久化,绝不退化为明文
设备绑定混淆存储层叠加设备标识存储目录被整体拷到另一台设备后解不开
备份导出打包时 allowBackup=false已实测adb backup 导不出 App 数据
截屏防护Android FLAG_SECURE(Native.js)默认开启,可在「我的 → 安全」关闭
退出登录清除令牌与本地缓存服务端无令牌撤销机制,故不发登出请求

令牌存储的三个等级

写入时按强度从高到低选择;读取时按数据自身的存储前缀决定用哪个后端 (k1: = 安全区,v1: = 混淆)—— 这样日后升级后端不会让老数据失效。

等级触发条件存储形态
keystoreAndroid 6.0+ 且 Native.js 调通AES-256-GCM,密钥在系统安全区、不可导出
native装了 DSH-SecureStorage 原生插件由插件自行实现(历史方案,保留兼容)
obfuscated以上都不可用(含 H5 / 小程序)随机 IV 流加密 + 设备绑定,不是安全区

⚠️ keystoreNative.jsplus.android),不需要任何原生插件, 因此云打包即可生效 —— 不必为此改走离线打包。

代价是那段代码无法在本机验证(没有 Android 运行时),只能在真机上通过 「关于」页显示的是哪一级来确认。src/utils/keystore.js 的文件头列出了 两个必须在真机上确认的不确定点;纯计算部分(UTF-8 / Base64 / 载荷格式) 已拆到 utils/keystore-codec.js 并被自测覆盖。

截屏防护

用窗口级 FLAG_SECURE(同样是 Native.js,无需插件):开启后截屏与录屏只会得到空白画面, 「最近任务」里的缩略图也会被遮住。

默认开启 —— 默认关闭的安全项等于没做(没人会主动去打开它)。 代价是你自己也不能截图了,所以「我的 → 安全」里有开关, 需要截图反馈问题时临时关掉即可。

⚠️ 只有 Android 有对应能力。不支持的平台上不显示开关,而是显示一行说明 —— 让用户拨一个永远没效果的开关,比不做这个功能更糟:他以为自己受保护了。

⚠️ 判定"到底有没有生效"要看最近任务的缩略图(生效时应为空白), 或用 adb shell screencap(生效时是黑屏)。 在电脑模拟器上,模拟器自带的截图按钮照样能截到内容 —— 它走的是宿主机窗口抓取,FLAG_SECURE 拦不住,不能拿它当判据。

仍然没有做的事(如实列出,不假装做了)

  1. 证书固定(Certificate Pinning):uni-app 网络层支持有限,当前依赖系统信任链。
  2. 反调试 / 反模拟器 / 完整性校验:未做。
  3. App 加固:未做(DCloud 提供付费加固)。
  4. iOS 的截屏防护FLAG_SECURE 是 Android 独有,iOS 需另一套做法(截屏检测),本期未实现。

六、API 契约与 Mock

  • 契约唯一来源docs/mobile-api-contract.md(服务端会话维护,v1.0.5)
  • 协作与边界docs/mobile-parallel-work.md
  • 契约差异集中在两处,服务端改字段只需要改这里:
    • src/api/ —— 所有请求与响应归一化(缺字段一律有默认值)
    • src/config/route-map.js —— 服务端 route(Web 路由)→ App 页面路由

Mock

服务端 MOBILE_ENABLED 默认 false,云上未升级时 /api/mobile/* 全部 404。 开 mock 可以让 UI 开发不被阻塞:

# mobile/.env.local
VITE_USE_MOCK=true

Mock 只覆盖读接口与登录;写接口(启停隧道等)不 mock —— 它们的"成功"没有意义,会掩盖真实错误。

容错要求(已实现,勿回退)

  1. 忽略未知字段;
  2. 缺字段有默认值;
  3. 能力探测 + 优雅降级/api/mobile/* 不可用时首页仍有降级路径,

角标不显示而不是弹错;

  1. 区分两种 404:总开关关闭的 404(看文案 移动端接口未启用)与资源不存在的 404,

ApiError.isMobileDisabled / isNotFound


七、联调前置条件

要求
服务端版本v1.0.5 或更高/healthzversion 字段确认)
MOBILE_ENABLEDtrue(默认 false;改动需 recreate 容器restart 不重读 env_file)
PANEL_OUTSIDE_ACCESStrue,否则公网来源调用管理 API 直接 403(云上已确认)
短信登录SMS_ENABLED=true 且账号已绑定手机号
图形验证码开启时 App 会自动出现验证码输入框(并支持暗色配色)

判据:公网无令牌访问 /api/overview 应返回 401(缺令牌),不是 403(被来源策略挡)。 401 说明链路是通的,只差认证。


八、测试

  • 自动化:node scripts/selftest.mjs(63 项,覆盖混淆编解码、格式化、地址校验、路由映射)
  • 手动清单:docs/mobile-test-checklist.md(登录、主题、错误处理、降级、退出登录、回归)

⚠️ 构建通过 ≠ 运行时正确。Vue 的模板编译不检查 <script setup> 里的标识符, 漏 import 也照样构建成功。因此每次改动后必须按手动清单在 H5 上实际走一遍。


来源:intranet-tunnel/docs/mobile-phase1-discovery.md## 七、技术选型对比与推荐(原文 2885 字符)

七、技术选型对比与推荐

7.1 四个方案的对比(结合本项目实际情况)

维度A. uni-app (Vue 3)B. FlutterC. React NativeD. 微信小程序原生
与现有 Web 栈的关系同语言同框架(Vue 3 SFC + 组合式 API)❌ 全新 Dart❌ React 生态⚠️ 相近但受限
复用设计 Tokentokens.scss--app-* 变量可直接搬(小程序/App 均支持 CSS 变量)⚠️ 需手工翻译成 Dart 常量⚠️ 需手工翻译✅ 同 CSS
复用 API 封装思路✅ axios 拦截器 → uni.request 拦截器,1:1 对应⚠️ Dio 拦截器,思路相同但需重写⚠️ axios 可直接用✅ 同 axios 风格
推送能力(本项目核心价值⚠️ 需 uniPush 2.0 或原生插件接厂商通道✅ 官方插件生态成熟,到达率可控✅ 生态最丰富无法后台常驻,只能订阅消息
生物识别 / Keychain / Keystore⚠️ 需原生插件或 uts✅ 官方 local_auth / flutter_secure_storage✅ 生态齐全❌ 无 Keystore
证书固定(Certificate Pinning)⚠️ 网络层支持弱,需原生插件HttpClient.badCertificateCallback + 自签校验⚠️ 需原生模块❌ 不可行
截图防护(FLAG_SECURE / 模糊)⚠️ 需原生插件✅ 插件可用
扫码uni.scanCode 开箱即用
一套代码覆盖小程序/H5独有优势
交付速度(本团队)最快⚠️ 仅限微信场景
性能中(webview 渲染,列表页足够)

7.2 推荐:方案 A(uni-app),理由与前提

推荐理由(按权重)

  1. 技术栈零切换成本:现有 Web 端是 Vue 3.5 + 组合式 API + SCSS Token,团队已具备;uni-app 的语法、<script setup>、组合式 API 基本一致,设计 Token 与 API 封装的迁移是"搬"而不是"翻译"
  2. 本项目移动端的定位是"轻量补充"(查看 + 轻操作 + 告警),不是重交互应用。列表/详情/表单/图表四类页面占 90%,webview 渲染的性能完全够用;而选 Flutter 换来的性能优势在这个场景里用不上。
  3. 可同时输出 H5 与微信小程序:运维场景下"不想装 App 时用小程序看一眼"是真实需求,方案 B/C 直接放弃这块。
  4. 限流与轮询压力下,代码复用带来的边际收益更高:双端共用一套请求/错误处理/刷新逻辑,能避免"Web 修了 401 重放、App 没修"这类分裂。

必须承认的三个短板与应对

短板应对
推送到达率依赖插件(厂商通道需逐个接)若推送到达率是不可妥协的指标,则改推 方案 B(Flutter);或 uni-app + 极光/友盟聚合推送(一次接入覆盖厂商通道),代价是引入第三方 SDK 与依赖
证书固定、Keychain/Keystore、截图防护需原生能力uts 插件或 uni-app 原生插件封装;这三项是安全底线,不能因为麻烦而降级为"可选"
小程序端不支持 EventSource(日志实时流)小程序端降级为轮询 /api/logs/*;App/H5 端保留 SSE

方案 B(Flutter)的触发条件(满足任一即改推 B)

  • 决定只做 iOS + Android,且推送到达率/后台保活是硬指标;
  • 团队已有移动端原生开发资源,且能接受 Dart 与 Vue 技术栈分裂;
  • 后续规划中有大量原生能力(蓝牙、后台常驻、复杂离线)。

方案 D(微信小程序原生)不推荐单独采用:它的核心缺陷是无法后台常驻,而本项目的移动端核心价值是"随时接收告警"——这与小程序的模型直接冲突。它可以作为 uni-app 的附加输出端存在,但不能作为主方案。

7.3 若确认方案 A,建议的技术形态

建议理由
框架uni-app(Vue 3 + Vite + <script setup>与 Web 端一致
语言JavaScript(与 Web 端一致,不引入 TS 工具链)Web 端是纯 JS;双端保持一致可减少心智负担。若团队有意向迁 TS,应 Web/移动端一起规划,不要只在新项目上引入
UI 库自建轻量组件(复用 Web 的设计 Token)+ 按需引入(可选 uview-plus)现有 Web 用的是 Element Plus,uni-app 生态无官方对应;需求强调的是 Token 一致而非组件库一致
状态管理Pinia(uni-app 官方支持 + 持久化插件)Web 端用的 reactive 单例在多环境账号切换场景下不够用;这里是有意识的不一致,需在文档中记录
请求层uni.request 封装 + 拦截器(对齐 web/src/api/client.js统一鉴权、错误映射、刷新串行化
安全存储原生插件(iOS Keychain / Android Keystore)需求底线。小程序端降级为 uni.setStorageSync 并在文档中明确标注风险
图表轻量 Canvas 手绘 或 uChartsECharts 在 uni-app 小程序端体积过大
Token 同步方式web/src/styles/tokens.scss 生成 mobile/styles/tokens.scss(人工同步 + CI 校验两者 Token 名一致)避免"改了一边忘了另一边"


来源:intranet-tunnel/docs/mobile-android-build.md## 一、打包:两条路线(原文 1498 字符)

一、打包:两条路线

路线怎么做建议
命令行cli packHBuilderX 自带 cli.exe,一条命令提交云打包推荐:可复现、不依赖找菜单
GUIHBuilderX 菜单「发行 → 原生App-云打包」备选(菜单在新版 IDE 里的位置可能变)

两条路线用的是同一套云打包服务,产物完全一样。

路线 A:命令行(推荐)

cd H:\Works\intranet-tunnel\mobile
powershell -ExecutionPolicy Bypass -File scripts\pack-android.ps1

脚本会:检查 cli.exe检查 appid 是否已填(否则提前报错,不必等云端返回)、 确认 HBuilderX 在运行(必要时 cli open 激活),然后提交打包并回显状态。

常用变体:

# 查询打包进度
powershell -ExecutionPolicy Bypass -File scripts\pack-android.ps1 -Status

# 取消排队中的任务
powershell -ExecutionPolicy Bypass -File scripts\pack-android.ps1 -Cancel

# 换成 DCloud 公共证书(默认用的是"老版证书",两者都是免费测试证书)
powershell -ExecutionPolicy Bypass -File scripts\pack-android.ps1 -CertType 1

直接调 CLI 也行(脚本只是把前置检查与 ANSI 清理包了一层):

& 'D:\HBuilderX\cli.exe' pack `
  --project 'H:\Works\intranet-tunnel\mobile' `
  --platform android `
  --android.packagename cloud.sushike.tunnel.console `
  --android.androidpacktype 2

⚠️ CLI 第一次打包会提示「正在下载依赖插件,请稍后重试」—— 那是 HBuilderX 在下载对应版本的编译器插件,等一两分钟重试即可(不是错误)。

⚠️ cli 需要 HBuilderX 正在运行才能通信(它通过本地通道与 IDE 交互)。 若报「没有可用的命令,尝试使用 cli.exe open」,先执行 cli.exe open

路线 B:GUI

见下方「三、GUI 打包步骤」。


前提说明:本机没有 Android 打包工具链 —— 无 JDK、无 Android SDK、无 Gradle;但 HBuilderX 已安装D:\HBuilderX)。 uni-app 的 APK 打包必须经由 HBuilderX 的云打包离线打包 SDK, 本机走的是前者:一条 cli 命令即可,不需要 JDK、SDK 或自备证书。

(本段此前写着"也没有 HBuilderX",与上面的状态表自相矛盾,已更正。)


支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论