开始审核样式

This commit is contained in:
rain
2026-07-15 17:23:19 +08:00
parent 241a7af54c
commit 0dec90ffdd
46 changed files with 999 additions and 964 deletions
+1
View File
@@ -4,3 +4,4 @@
/docs/design/screens/runtime/ /docs/design/screens/runtime/
/tmp-g01-icon-audit.png /tmp-g01-icon-audit.png
/node_modules/ /node_modules/
/.superpowers/
Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

+2
View File
@@ -1,5 +1,7 @@
# 家谱 APP 视觉设计交接手册 # 家谱 APP 视觉设计交接手册
> **2026-07-15 状态覆盖说明:** 本手册中的截图方法、资产规则和验证流程继续有效;“54 条路由、7 页已验收/冻结”的旧状态不再有效。当前为 53 条最终路由、53 页全部重新待审核、0 页冻结,正在返工 A01。接管时先读 `docs/交接记录.md` 与 `docs/验收规划.md`。
> 更新日期:2026-07-14 > 更新日期:2026-07-14
> 用途:在新电脑或交给新的 GPT 后,按本手册恢复当前工作;它只记录已发生的事实,不把待验收写成已完成。 > 用途:在新电脑或交给新的 GPT 后,按本手册恢复当前工作;它只记录已发生的事实,不把待验收写成已完成。
+6 -1
View File
@@ -1,5 +1,10 @@
# 视觉证据索引 # 视觉证据索引
## 当前换机检查点
- A01 当前短信登录态(待审核,不代表通过):[412×915 真实运行代表图](screens/handoff/2026-07-15/A01-current-sms-generated-412x915.png)
- 当前实际为 53 页全部重新待审核;下方旧“已验收”图片只作为历史视觉基础,不计入本轮完成状态。
> 本仓库只保留低负担交接证据:每页一张代表图,复杂模块使用联系表。完整 before/after、多尺寸、历史废稿和全部运行截图保存在项目外本地归档,不进入 Git。 > 本仓库只保留低负担交接证据:每页一张代表图,复杂模块使用联系表。完整 before/after、多尺寸、历史废稿和全部运行截图保存在项目外本地归档,不进入 Git。
## 完整本地归档 ## 完整本地归档
@@ -8,7 +13,7 @@ Windows 路径:`C:\Users\Rain\Desktop\job\jiapuapp-design-evidence-archive-202
归档内的 `design-full/` 是精简前完整 `docs/design/``tmp/` 是 ImageGen 与调试临时源文件。新电脑默认不需要这份归档;只有追溯历史决策或检查旧尺寸时才单独复制。 归档内的 `design-full/` 是精简前完整 `docs/design/``tmp/` 是 ImageGen 与调试临时源文件。新电脑默认不需要这份归档;只有追溯历史决策或检查旧尺寸时才单独复制。
## 已验收视觉基准 ## 历史视觉基准(不计本轮通过)
- A01[启动/登录引导](screens/handoff/2026-07-13/A01-360x800.png) - A01[启动/登录引导](screens/handoff/2026-07-13/A01-360x800.png)
- A02[账号登录](screens/handoff/2026-07-13/A02-after-360x800.png) - A02[账号登录](screens/handoff/2026-07-13/A02-after-360x800.png)
+267 -35
View File
@@ -1,48 +1,280 @@
# 家谱 APP 交接记录 # 家谱 APP 换机交接记录
> 更新日期2026-07-15 > 最后更新:2026-07-15
> 这是更换电脑或交给新 GPT 的唯一快速入口。详细操作见 [视觉设计交接手册](design/视觉设计交接手册.md),页面审核状态以 [总规划](规划.md) 为准 > 用途:更换电脑、重新打开 Codex/GPT 的唯一接管入口
> 当前阶段:只做页面样式与视觉流程验收;不对接接口,不做功能验收。
> 当前工作页:A01 登录页,尚未通过用户验收。
## 1. 当前准确停点 ## 1. 接管后先复述的准确停点
- `pages.json` 当前登记 **54 条最终路由**;重复状态已经收敛到同一页面,例如 G02→G01、G04→G03、G07→G06、T02→T01 - `pages.json` 当前实际注册 **53 条最终路由**
- 当前有 **7 个已验收**页面:A01、A02、A04、A05、A06、G01、G03。除非用户提出明确调整,不得改变这些页面已经验收的可见效果 - A02 已删除并合入 A01;不存在需要保留的 A02 兼容入口
- 其余 **47 个待审核候选**已经完成页面实现、H5 运行截图和内部视觉审视,统一在 `docs/规划.md` 标记为 `[~]`。候选不等于用户验收,只有用户明确通过后才能改为 `[x]`;要求返工时改为 `[!]` - `docs/验收规划.md` 当前统计为:**53 个待审核、0 个已验收、0 个返工标记**
- 下一步是按 `docs/规划.md` 逐页视觉审核,不再批量新增页面。接口资料只用于理解功能、字段和状态位置;本阶段不对接接口 - 历史上曾标记通过的 A01、A02、A04、A05、A06、G01、G03 只保留为历史视觉证据,**本轮全部重新审核**
- H5 Chrome 截图是候选审计证据,最终 Android/HBuilderX 真机或模拟器复核仍需在视觉审核后完成 - 当前没有任何页面处于视觉冻结状态;只有用户在本轮明确说“通过”后,对应页面才能改为 `[x]` 并冻结
- 当前停在 **A01 登录页视觉返工**。用户认为整页与选定设计稿仍有明显差距,A01 不得写成“完成”或“通过”。
- 最新完成的局部修正是:闭眼图标保留中心瞳孔;短信验证码输入图标改为自生成的“消息气泡 + 三点”,不再复用锁、勾选或临时第三方 SVG。
- 下一步不是跳去 G05,也不是批量改其他页面;应先继续完成 A01 的真实运行截图审视、修正和用户确认。
## 2. 新 GPT 启动顺序 如果其他文档仍写“54 条路由、7 个已验收或冻结”,那是旧阶段记录。当前状态和验收数量以本文件、实际 `pages.json``docs/验收规划.md` 为准。
1. 全程使用中文。先完整阅读 `AGENTS.md`、本文件、`docs/规划.md``docs/design/视觉设计交接手册.md``docs/design/D1_安卓视觉规范与页面壳.md``docs/design/P00_页面结构与资产清单.md``design-qa.md` ## 2. 新电脑启动时必须完整阅读
2. 运行 `git status --short`,识别并保留全部未提交改动。不要把旧提交状态当成当前工作区状态。
3. 先向用户简洁复述:54 条路由、7 个已验收、47 个待审核,以及准备审核的具体页面;在用户给出页面反馈前不要盲目重做。
4. 审核时先看 [视觉证据索引](design/视觉证据索引.md) 的默认代表图;发现具体问题后再展开多尺寸、before/after 或历史废稿。
## 3. 必须调用的能力 按以下顺序阅读,不要跳过:
新 GPT 必须实际具备并按触发条件调用: 1. `AGENTS.md`
2. `docs/交接记录.md`(本文件)
3. `docs/验收规划.md`
4. `docs/规划.md`(用于项目范围和历史结构;其中旧验收数字不再有效)
5. `docs/design/视觉设计交接手册.md`(用于截图、资产和审视方法;其中旧冻结状态不再有效)
6. `docs/design/视觉证据索引.md`
7. `docs/design/D1_安卓视觉规范与页面壳.md`
8. `docs/design/P00_页面结构与资产清单.md`
9. `design-qa.md`(历史内部 QA 证据不等于本轮用户验收)
- `product-design:index`:开始设计、审视或评价页面时先调用,用于选择正确的产品设计工作流。 阅读后先运行:
- `product-design:audit`:必须基于真实运行截图审视页面,不能只看源码判断好不好看。
- `imagegen`:仅在缺少完整位图视觉资产时调用。完整按钮、卡片、边框和装饰面必须使用独立位图,不能用 CSS 伪造。
- `superpowers:test-driven-development`:修改页面行为、路由或可审计视觉结构前,先写或更新最小契约测试并确认 RED。
- `superpowers:systematic-debugging`:测试、截图、路由或构建出现异常时,先复现并定位根因再修改。
- `superpowers:verification-before-completion`:宣称完成前,运行受影响测试、真实截图复核和 `git diff --check`
能力缺失时应先安装或启用同等能力,不得假装已经完成视觉审计、位图生成或验证。 ```powershell
git status --short
```
## 4. 视觉审核降噪规则 必须保留全部已有未提交改动。不要因为文件是未跟踪状态就删除,也不要用旧提交覆盖当前工作区。
- 默认每页只看一张 360×800 代表截图 随后先向用户复述:53 条最终路由、53 页全部待审核、当前无冻结页、当前停在 A01、准备使用的技能、A01 的下一步。复述准确前不要继续修改页面
- 同页有多个关键状态时,优先看一张状态联系表,不连续展示多张相似截图。
- 412×915、小屏、多尺寸和前后对比图仅在发现比例问题、回归问题或用户要求时展开。
- 历史废稿和 before/after 证据不删除,但只保留在 `docs/design/视觉证据索引.md` 的历史审计区。
- 审核结论记录在 `docs/规划.md`;不要因为内部测试或 Product Design 审视通过就替用户勾选。
## 5. 禁止事项与完成验证 ## 3. 文档权威顺序
- 不使用 worktree 或多代理。 发生冲突时按以下顺序判断:
- 不执行 `git add`、commit、push、上传、reset、checkout,也不覆盖用户现有文件。
- 不擅自对接接口、修改接口契约、打 Android 包或把截图放入 `static/assets/` 当运行资源 1. 用户在当前对话中的最新明确决定
- 只修改用户当前反馈涉及的页面和对应测试、截图、设计记录;不要顺手重构冻结页或相邻模块 2. `docs/交接记录.md`:真实停点、风险、下一步
- 完成一次页面返工前,至少运行对应契约测试、运行时 smoke、真实路由截图复核和 `git diff --check`;无法执行的检查必须明确说明 3. `docs/验收规划.md`:53 页流程顺序、状态合同、视觉验收规则和勾选状态
4. 实际代码、`pages.json` 和测试:当前实现事实。
5. `docs/design/视觉证据索引.md`:默认展示证据。
6. `docs/design/视觉设计交接手册.md``docs/规划.md`:历史方法、范围和背景。
7. `design-qa.md`:历史内部审视记录。
内部测试、旧 QA 的 `passed`、旧截图或 GPT 自己的判断都不能替代用户本轮确认。
## 4. 当前 A01 的真实状态
### 4.1 已确定的页面合同
- APP 没有游客模式,启动入口是 A01。
- A01 同页包含密码登录、短信验证码登录、微信登录、注册、忘记密码和协议入口。
- 首次默认密码登录;后续只恢复用户上次选择的密码/短信方式,不保存密码。
- 登录在本地字段与协议校验通过后才显示行为验证弹层。
- 未勾选协议时使用页内高亮、错误文案和轻微震动,不使用原生 Toast 或原生确认框。
- 320×568 下核心入口必须同屏可见,不依赖纵向滚动。
- 微信登录保留入口,真实登录留到功能阶段。
- 行为验证当前只审核弹层外观状态;真实 TAC、`validToken` 和接口续接留到功能阶段。
### 4.2 当前代码与关键资产
- 页面:`pages/auth/a01-entry.vue`
- 路由:`pages/auth/a01-entry`
- 契约测试:`tests/a01-a02-ui-contract.ps1`
- 页面头部:`static/assets/modules/auth/opaque/a01-login-header-v1.png`
- 精确头部叠层:`static/assets/modules/auth/opaque/a01-login-header-exact-v2.png`
- 卷轴框:`static/assets/modules/auth/transparent/a01-login-scroll-frame-v1.png`
- 标题分隔:`static/assets/modules/auth/opaque/a01-title-divider-v2.png`
- 手机图标:`static/assets/modules/auth/transparent/a01-icon-phone-v1.png`
- 密码图标:`static/assets/modules/auth/transparent/a01-icon-lock-v1.png`
- 睁眼图标:`static/assets/modules/auth/transparent/a01-icon-eye-open-v1.png`
- 闭眼图标:`static/assets/modules/auth/transparent/a01-icon-eye-closed-pupil-v2.png`
- 短信验证码图标:`static/assets/modules/auth/transparent/a01-icon-sms-code-v2.png`
- 最新可跨电脑查看的代表图:`docs/design/screens/handoff/2026-07-15/A01-current-sms-generated-412x915.png`
`static/assets/modules/auth/transparent/a01-icon-sms-code-v1.svg``a01-icon-verification-v1.png` 等未引用候选仍可能存在于工作区。当前不要顺手清理;页面样式全部完成后再单独做文档与资产清理。
### 4.3 最新验证事实
- `tests/a01-a02-ui-contract.ps1` 已要求页面引用 `a01-icon-sms-code-v2.png`,并禁止回退到勾选图标和临时 SVG。
- 最新真实运行截图已确认短信验证码行显示棕色消息气泡三点图标。
- A01 整页仍缺少本轮用户最终确认;深色主题、四档尺寸完整复核和 Android 真机复核均不能写成已通过。
- 旧的 `A01-password-icons-412x915.png` 实际仍是短信 Tab 且使用旧临时图标,不能当密码态证据。
## 5. 下一步严格执行顺序
### 第一步:恢复环境与确认文件完整
```powershell
python --version
node --version
git status --short
```
项目没有可依赖的 `package.json` 启动脚本。使用 HBuilderX 启动 H5,确认:
```text
http://localhost:5173/#/pages/auth/a01-entry
```
换机前用户需要自行把当前修改和未跟踪的最终资产加入 Git 并同步;GPT 不执行 Git 暂存、提交、推送或上传。特别确认本节列出的 A01 PNG 资产、测试、本文件和交接截图已被用户纳入 Git,否则换电脑后会缺图。
### 第二步:继续 A01,而不是切换页面
1. 打开 A01 密码登录态,确认手机、锁、闭眼/睁眼图标在真实尺寸下清晰且风格统一。
2. 打开短信登录态,使用当前生成的消息气泡三点图标;不要恢复锁、勾选或第三方临时 SVG。
3. 对照当前选定方向检查整体层级:红色宗祠顶部、完整宣纸卷轴登录区、标题、Tab、输入区、主按钮、微信入口、注册与协议必须形成一个整体,不能只修局部 CSS。
4. 优先修用户已指出的明显问题;每轮只处理一个可验证问题,避免盲目大改导致新的比例错误。
5. 先完成浅色代表态,再补深色主题;两套主题必须在同一个 A01 验收项内提交。
6. 内部检查 320×568、360×640、360×800、412×915;默认只向用户展示一张代表图。密码/短信、明/暗等复杂状态用一张联系表,不连续发送大量对比图。
7. 用户明确确认 A01 通过后,才把 `docs/验收规划.md` 中 A01 从 `[~]` 改为 `[x]` 并冻结;否则保持 `[~]`,明确返工时改 `[!]`
### 第三步:A01 通过后按真实用户流程继续
严格按 `docs/验收规划.md` 的 53 页表推进:
```text
A01 登录
→ A04 注册
→ A05 重置密码
→ A06 阻断登录状态
→ G01 我的家谱
→ G06 搜索/邀请码加入
→ G08 加入申请
→ G09 我的申请
→ G03 创建家谱/录入始祖
→ G05 家谱总览
→ 后续管理、世系、家族内容、档案、消息和个人中心
```
真实入口优先。开发者直达路由只用于排错,不能冒充流程可达证据。不同用户身份、空态、加载、失败、权限、审核中、被拒绝、已退出/移除等状态必须在同一页面验收项下覆盖。
## 6. 必须使用的技能与触发时机
每次调用技能前在中文进度消息中告诉用户“正在用什么、为什么”。
| 技能 | 必须使用的场景 | 本项目的正确做法 |
| --- | --- | --- |
| `product-design:index` | 开始设计、审视、复刻或评价页面时 | 先选择正确的产品设计流程,不能把任务当成普通 CSS 调整 |
| `product-design:audit` | 审核已运行页面或完整用户流程时 | 必须先获取真实运行截图,再根据截图报告层级、可用性、适配和无障碍问题 |
| `product-design:ideate` | 没有明确视觉真源,需要探索多个设计方向时 | 给可比较的视觉方案,用户选定后再实施;不在代码里盲试方向 |
| `product-design:image-to-code` | 已有选定截图、效果稿或设计图,需要一比一落地时 | 以选定图为视觉真源,做响应式还原,再用真实截图比对 |
| `imagegen` | 缺少完整位图视觉资产时 | 只生成缺失资产;参考现有页面风格,验证尺寸、Alpha、边缘和缩小后的清晰度后再接入 |
| `superpowers:brainstorming` | 新模块、复杂交互、用户流程或视觉方向尚未明确时 | 先把目标、状态、身份、错误路径和验收标准问清楚 |
| `superpowers:writing-plans` | 多步骤页面或模块准备实施时 | 写成可勾选、可验证的小步骤;本项目禁止加入提交 Git 的步骤 |
| `superpowers:test-driven-development` | 修改页面行为、路由、状态合同或可审计结构前 | 先改最小契约测试并看到 RED,再做最小实现至 GREEN |
| `superpowers:systematic-debugging` | 截图、构建、路由、样式、测试或热更新异常时 | 先稳定复现并定位根因;不能靠删测试、乱改 rpx/px 或堆覆盖样式掩盖问题 |
| `superpowers:verification-before-completion` | 任何“完成、修好、通过”表述前 | 重新运行受影响测试、真实截图复核和 `git diff --check`;没有证据就只能报告未验证 |
截图审视时必须调用 Product Design 审核流程;只有看源码不能判断页面是否好看。图像生成只用于缺失的完整位图资产,已有可用资产不重复生成。
## 7. 截图与视觉证据规则
先启动独立 Chrome 调试实例:
```powershell
$chrome = "$env:ProgramFiles\Google\Chrome\Application\chrome.exe"
Start-Process -FilePath $chrome -WindowStyle Hidden -ArgumentList `
'--remote-debugging-port=9222', `
"--user-data-dir=$env:TEMP\jiapu-chrome-debug", `
'http://localhost:5173/#/pages/auth/a01-entry'
```
确认调试页:
```powershell
Invoke-RestMethod 'http://127.0.0.1:9222/json/list' | ConvertTo-Json -Depth 3
```
A01 短信态截图示例:
```powershell
node scripts/capture-chrome-page.js `
'http://localhost:5173/#/pages/auth/a01-entry' `
'#app' `
'docs/design/screens/runtime/<日期>/A01-sms-412x915.png' `
412 915 sms-tab
```
- 新截图先放 `docs/design/screens/runtime/<日期>/`;该目录默认不进入 Git。
- 用户确认需要跨电脑保留时,只复制一张代表图或一张联系表到 `docs/design/screens/handoff/<日期>/` 并更新索引。
- 不把运行截图放进 `static/assets/` 当页面资源。
- 默认每页展示一张代表图;复杂状态展示一张联系表。
- 历史 before/after、多尺寸和废稿只在定位问题时从视觉证据索引展开。
- H5 Chrome 截图是候选证据,不能替代最终 Android/HBuilderX 真机或模拟器复核。
## 8. 页面样式验收的全局底线
- 不是简单改 CSS;必须同时考虑用户流程、身份、错误操作、状态反馈、弹窗、加载、空态、主题、适配和性能。
- 所有提示、确认、结果、底部选择和加载效果使用项目自定义组件;禁止直接展示原生 `uni.showToast``uni.showModal``uni.showLoading``uni.showActionSheet`
- 主题必须提供“浅色、深色、跟随系统”三个选项;深色主题沿用同一国风体系,不使用系统自动反色。
- 列表接近底部时使用项目自定义“正在展开更多……”视觉;失败保留已加载内容并允许重试;结束显示“已阅至末尾”。
- 兼容 320×568、360×640、360×800、412×915、系统字体放大、状态栏、安全区、软键盘和返回手势。
- 动画只使用 `transform``opacity` 等低成本属性;避免大面积高频重绘、同步阻塞、重复图片解码和无边界监听。
- 代码必须精准、可读、方便修改;禁止压缩代码、复制堆叠样式、无意义抽象和难以解释的补丁。
- 关键状态、兼容处理和非显然逻辑写中文注释;不要给一眼能看懂的代码堆无用注释。
- 接口文档只用于理解功能、字段和状态位置;当前禁止接接口或伪造接口已通过。
## 9. 禁止事项
- 不使用 worktree。
- 不使用多代理或子代理。
- 不执行 `git add``git commit``git push`、上传、`git reset``git checkout`
- 不删除、覆盖或清理用户已有未提交改动。
- 不因“整理代码”顺手修改当前验收项以外的页面。
- 不对接接口,不修改接口合同,不打 Android 包。
- 不用原生 UniApp 弹窗、Toast、Loading 或 ActionSheet 作为最终视觉。
- 不用 CSS 伪造本应是完整位图的卷轴、牌匾、按钮、卡片或装饰面。
- 不把直接路由打开当作用户流程已经可达。
- 不把内部测试、GPT 审视、旧 QA 或 H5 截图写成用户已经验收。
- 不连续展示大量相似截图。
- 不在页面样式验收完成前集中清理 `docs/``scripts/``tmp/``tests/` 或未引用候选资产。
## 10. 每轮完成前验证
先跑当前页面的聚焦验证。A01 当前最小集合:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/a01-a02-ui-contract.ps1
node scripts/capture-chrome-page.js `
'http://localhost:5173/#/pages/auth/a01-entry' `
'#app' `
'docs/design/screens/runtime/<日期>/A01-sms-412x915.png' `
412 915 sms-tab
git diff --check
```
页面准备提交用户审核时,还需完成:
1. 受影响的 PowerShell 契约测试。
2. 适用的运行时 smoke。
3. 真实路由截图并人工查看,不只确认文件生成。
4. 四档尺寸内部复核。
5. 浅色、深色与跟随系统入口复核。
6. `git diff --check`
7. 用户明确确认。
任何一项没有执行,都必须明确写“未验证”,不得使用“已完成、已修好、已通过”。CRLF 警告本身不等于失败;真正的空白错误或非零退出码才算失败。
## 11. 换机前用户需要自行确认的 Git 文件
GPT 不操作 Git。用户切换电脑前应自行确认以下内容已纳入版本控制并同步:
- `pages.json`
- `pages/auth/a01-entry.vue`
- A02 删除记录及相关路由/测试收敛文件
- `tests/a01-a02-ui-contract.ps1`
- `tests/a02-route-removal-contract.ps1`
- `docs/验收规划.md`
- `docs/交接记录.md`
- `docs/design/screens/handoff/2026-07-15/A01-current-sms-generated-412x915.png`
- A01 当前页面实际引用的全部新增 PNG 资产
- `static/vendor/tac/`(后续行为验证功能阶段需要)
`docs/design/screens/runtime/``tmp/` 和本机 Chrome 调试目录不能作为换机后的唯一资料来源。最终运行资产必须在 `static/assets/`,长期代表证据必须在 `docs/design/screens/handoff/`,决策与下一步必须写进本文件或 `docs/验收规划.md`
## 12. 仓库体积与本地缓存规则
2026-07-15 已完成一次安全瘦身:项目总占用从约 1.03GB 降到约 232.76MB`tmp/` 从 922.89MB 降到 22.18MB。被清理的是 7 组可再生成的 `tmp/chrome-qa-*` 无头 Chrome 用户目录,没有删除源码、最终资产、代表证据或 ImageGen 源稿。
- `.git` 当前约 101.64MB`git fsck --full` 退出码为 0,只有可忽略的 dangling tree/blob,没有对象损坏报告。
- 当前受版本控制的最大单文件约 2.91MB,不存在单文件 100MB 上传阻断。
- 本轮需要用户纳入 Git 的新增规划、A01 资产、TAC 和代表图合计约 4.73MB。
- `.gitignore` 已忽略 `/tmp/``/unpackage/``/node_modules/``/docs/design/screens/runtime/``/.superpowers/` 和临时 G01 审计图。
- 后续启动截图 Chrome 时,`--user-data-dir` 必须放在 `$env:TEMP`,禁止再把 Chrome 用户目录放进项目 `tmp/`
- `tmp/` 只允许保存短期诊断图和 ImageGen 中间源,最终资产必须进入 `static/assets/`,换机代表证据必须进入 `docs/design/screens/handoff/`
- 不要执行 `git add .` 后盲目提交;用户应先用 `git status --short --ignored` 确认缓存目录仍为 `!!` 忽略状态。
- 当前不重写 Git 历史、不执行 `git gc`、不删除 `.git` 内的孤立索引;若远端仍拒绝推送,应保留完整错误信息,再按远端限制单独排查,不能用 reset 或重建仓库规避。
+2
View File
@@ -1,5 +1,7 @@
# 家谱 APP 总规划 # 家谱 APP 总规划
> **2026-07-15 状态覆盖说明:** 本文件保留旧阶段范围与历史记录,其中“54 条路由、7 页已验收/冻结”的状态已经失效。当前实际为 `pages.json` 53 条最终路由、`docs/验收规划.md` 中 53 页全部 `[~]` 待审核、0 页冻结;准确停点与下一步以 `docs/交接记录.md` 和 `docs/验收规划.md` 为准。
> 电脑或 GPT 更换时,先阅读:[项目交接记录](交接记录.md)。 > 电脑或 GPT 更换时,先阅读:[项目交接记录](交接记录.md)。
> 当前阶段:Product Design 视觉精修持续推进;54 条最终路由可浏览,A01、A02、A04、A05、A06、G01 两种状态与 G03 `create|ancestor` 双步骤已获用户视觉验收;G 模块与 T 模块候选稿已完成,统一等待全量设计完成后逐页审核。 > 当前阶段:Product Design 视觉精修持续推进;54 条最终路由可浏览,A01、A02、A04、A05、A06、G01 两种状态与 G03 `create|ancestor` 双步骤已获用户视觉验收;G 模块与 T 模块候选稿已完成,统一等待全量设计完成后逐页审核。
+327
View File
@@ -0,0 +1,327 @@
# 家谱 APP 页面样式验收规划
> 更新日期:2026-07-15
> 适用范围:Android `uni-app` 用户端
> 当前实现:`pages.json` 仍有 54 条路由;A02 已确定并入 A01,完成收敛后的最终验收目标为 53 条路由。历史上的 7 个验收结果仅作为旧视觉证据,不计入本轮完成状态。
> 本文件只管理逐页视觉、页面状态外观、弹窗与加载样式、手机适配和表现质量;不替代 `docs/规划.md` 对项目范围和最终路由的管理,也不承担接口与业务功能验收。功能验收规则后续单独编写。
## 1. 标记与审核规则
| 标记 | 含义 |
| --- | --- |
| `[ ]` | 尚未设计或尚未开始全局验收项 |
| `[~]` | 页面候选已实现,等待用户按本文件逐项审核 |
| `[x]` | 用户已明确完成基础页面样式验收;可见效果冻结,不代表接口或业务功能通过 |
| `[!]` | 用户要求返工;只修改该项涉及的页面、组件、资产、测试和证据 |
| `⚠` | 当前没有真实可达入口或存在明显流程断点,不能用直接路由冒充流程通过 |
页面只有同时满足以下条件才能从 `[~]` 改为 `[x]`
1. 页面在真实运行环境中打开;流程页优先从上游可见入口进入,尚未实现入口时明确标记 `⚠`,不能冒充流程设计完整。
2. 正常、加载、空、失败、权限、校验和成功等适用状态的视觉呈现均已检查;本阶段只验状态外观,不验证后台结果真伪。
3. 浅色、深色两套主题在 320×568、360×640、360×800、412×915 四个尺寸均无溢出、裁切、遮挡或错位;“跟随系统”使用当前系统主题,但不得依赖系统自动反色。
4. 默认系统字号和约 1.3 倍系统字号均可完整操作;关键按钮、表单错误、姓名、关系和操作结果不得截断,空间不足时允许合理换行或滚动,不得强行缩小文字或遮挡主操作,并使用超长族谱名、生僻姓名完成边界检查。
5. 状态栏、底部手势区、软键盘、系统返回和长文本不会破坏页面。
6. 点击反馈、加载、滚动、动画和图片解码在当前静态/Mock 数据下无明显卡顿。
7. 完成页面样式契约、运行时 smoke、真实运行截图复核和 `git diff --check`;不以接口测试作为本阶段通过条件。
8. 用户明确确认通过;内部测试或 Product Design 审视不能替代用户确认。
9. 当前页面涉及的源码通过“工程质量底线”检查;即使真实截图视觉合格,只要存在大段复制样式、难以理解的堆叠逻辑、关键处理缺少中文说明、明显性能隐患或不可维护实现,仍必须标记 `[!]` 返工,不得标记 `[x]`
一个页面编号仍只计算一个验收项,但必须在页面下记录正常态、空态、加载态、失败态、权限态和业务状态等适用视觉子项。全部必测视觉子项通过后,页面才能标记 `[x]`;不为同一路由的不同状态重复增加页面编号。复杂页面用一张联系表展示状态证据。
标记 `⚠` 的页面必须执行“真实入口优先”:审核到其上游页面时,先确认入口的目标用户、所在位置、显示条件和不可用状态,再从真实可见入口进入下游页面审核。开发者直达路由只允许用于内部排错,不得作为流程完整或页面可达的验收证据。受权限控制的入口对无权用户应隐藏,同时仍须通过受控状态单独审核下游无权限画面。
页面标记 `[x]` 后立即进入视觉冻结:不得在后续页面实施中顺手修改。公共弹窗、导航、字体、色彩、间距、圆角、加载组件或设计变量发生变更前,必须列出全部受影响的冻结页面并执行四档尺寸检查和真实运行截图回归;确认没有视觉变化时保留 `[x]`,发生任何可见变化时自动退回 `[~]` 等待用户复核。紧急缺陷允许解除冻结,但必须记录修改原因、影响范围和新的运行证据,复核通过后重新冻结。
接口尚未提供不阻止基础页面样式标记 `[x]`。微信登录、行为验证、邀请码、真实审核、上传、支付、权限控制和数据提交均留到后续功能验收;当前可以审核其入口、容器、加载、成功、失败和禁用状态是否好看,但不得把静态/Mock 画面描述为功能已通过。
本轮不沿用任何页面的历史完成标记。A01、A04、A05、A06、G01、G03 也必须重新完成页面结构、视觉状态、兼容性、弹窗加载样式和表现质量检查;用户重新确认后才能标记 `[x]`
审核证据默认每页只展示一张 360×800 代表图;需要同时确认明暗主题时,将浅色与深色代表态合成一张联系表,不连续展示两张前后对比图。其余尺寸在内部完成检查,只有发现问题或用户要求时才展开。
每个页面必须在同一验收项内完成浅色与深色主题,不拆分为两轮:两套主题的代表态、适用关键状态、四档尺寸、代码质量和性能全部通过后,才可提交一张明暗联系表供用户确认;用户确认后两套主题同时标记 `[x]` 并冻结。任一主题未完成或未通过时,整个页面保持 `[~]``[!]`
## 2. 已确认的核心用户流程合同
本章用于决定页面应该展示什么、入口放在哪里以及不同状态如何变化,是页面样式设计的上下文;不表示相关接口和业务功能已实现或已验收。
### 2.1 账户前置与登录后落点
- APP 不设游客模式;启动入口为登录页,所有家谱搜索、加入、创建、浏览和管理操作都必须建立在已有账户和有效登录状态上。
- A01 是认证流程唯一登录页,保留品牌视觉并合入原 A02 的密码登录、短信登录、注册、忘记密码和协议入口;微信登录继续保留并登记为后续接口依赖。
- A01 首次安装默认展示密码登录;用户主动切换密码/短信方式后,本机只记录上次选择的登录方式,下次打开恢复该方式。不得保存密码;手机号只允许按后续隐私与安全规则合规保存或脱敏展示。微信登录始终保持为独立入口,不参与密码/短信方式记忆。
- A04 注册成功后直接建立登录状态并进入 G01“我的家谱”,不得要求用户返回 A01 重复输入账号密码;注册接口能否返回有效登录凭证在后续功能验收核对,当前页面样式验收先覆盖提交中、成功过渡和进入 G01 的视觉连续性。
- A05 重置密码成功后不自动登录:先显示短暂的自定义成功结果,再返回 A01,自动保留刚才使用的手机号、切换到密码登录并聚焦密码框,让用户使用新密码完成登录;不得清空手机号或要求重新寻找登录方式。
- A01 内部直接处理密码错误、验证码错误和手机号未注册,使用就近校验,不跳转 A06;微信授权取消仍留在 A01,视为用户取消而非登录失败。A06 只承接账号冻结、停用、风险限制或其他无法继续登录的阻断性状态,必须说明原因、影响范围和明确恢复入口,不得成为普通错误的汇总页。
- A01 在 320×568 最小验收尺寸下不得依赖纵向滚动:品牌区、密码/验证码切换、当前登录表单、登录按钮、微信登录、注册、忘记密码和协议入口必须同屏可见。小屏允许明显压缩品牌头与山水装饰高度,但不得缩小输入框、按钮和协议的有效触控区。
- A01 未勾选协议时采用页内校验:协议区域轻微震动并高亮,下方显示明确错误文案;不弹原生 Toast 或确认弹窗,不跳转。用户勾选后错误样式立即消失。
- A02 从最终路由和验收表移除,不保留兼容入口;A04、A05、A06 的登录返回统一指向 A01。实际代码收敛完成前,`pages.json` 的 54 条是实施现状,不是最终验收数量。
- A01、A04、A05 共用同一行为验证弹层,但触发时机不同。A01 登录在账号信息、协议等本地校验通过并点击“登录”后弹出,验证成功后自动继续登录;A05 忘记密码在用户请求发送短信验证码前弹出,验证成功后自动发送短信,重置提交时不重复验证。A04 注册当前因短信注册接口尚未提供,临时按 A01 方式在完整填写并点击“注册”后弹出;注册短信接口提供即为临时契约的移除条件,届时必须迁移为“发送短信验证码前验证”,并删除注册提交前的旧触发路径,不保留双重验证。用户关闭或验证失败时必须保留原表单内容。当前页面样式阶段只审核弹层初始、拖动、成功、失败和关闭状态,不审核真实滑动轨迹、`validToken` 或接口自动续接。原始 TAC 运行库保存在 `static/vendor/tac/`,留待功能阶段使用。
- 短信验证码按钮只设计两种稳定状态:“获取验证码”和“Ns 后重试”倒计时。发送请求期间仅在原按钮内部显示短暂进度并阻止重复点击,不新增独立按钮形态;发送失败立即恢复可发送状态并在按钮附近就近说明原因;频繁限制有明确剩余秒数时进入倒计时,没有剩余秒数时保持可发送并显示限制说明。倒计时的真实到期时间、前后台恢复和接口限流结果归入后续功能验收。
- 登录成功后统一进入 G01“我的家谱”,不直接跳入上次浏览的家谱。
- APP 启动时按登录状态分流:首次打开或没有有效凭证进入 A01;已登录且凭证有效时直接进入 G01,不重复显示登录页;凭证过期时先回到 A01,再显示项目自定义单按钮信息对话框,标题“登录状态已过期”,正文“为保障账号安全,请重新登录”,按钮“重新登录”,点击后关闭并聚焦登录表单,不使用 Toast 或原生 `uni.showModal`;主动退出登录后清除登录凭证,但可保留用户上次选择的密码/短信登录方式。凭证有效性、过期检测和启动恢复归入后续功能验收。
- G01 没有任何家谱和申请记录时,使用同页空状态提供“搜索家谱、邀请码加入、创建家谱”三个入口,不新增独立引导页。
- G01 空状态的三个入口不做等权堆叠:“搜索家谱”作为主入口,优先引导用户确认家族是否已有家谱;“邀请码加入”作为快捷入口;“创建家谱”作为次入口,并提示“确认没有现有家谱后再创建”,降低重复建谱风险。三个入口始终同时可见,不因视觉降级删除创建能力。
- 一个账号允许加入或创建多个家谱。
### 2.2 G01 我的家谱状态合同
- 列表固定分为“我创建的”“我加入的”“加入申请”三组;审核中和被拒绝记录放在“加入申请”中,但仍可点击并切换顶部状态区。
- 顶部大卡片是“当前选中家谱/申请”的状态展示区;点击下方列表卡片只切换当前项,不直接进入详情。
- 已加入或我创建:顶部展示家谱信息和快捷入口;点击顶部大卡片进入 G05 家谱总览。
- 审核中:顶部快捷入口替换为审核进度和“撤回申请”,顶部大卡片不可进入家谱。
- 被拒绝:顶部快捷入口替换为拒绝原因和“修改申请并重新提交”,顶部大卡片进入申请修改,不进入家谱总览。
- 已退出或被移除:顶部展示状态原因和“重新申请加入”,不允许访问原家谱内容。
- 默认选择顺序为:恢复本机保存的上次选择;记录无效时选择第一个可操作家谱;没有可操作家谱时选择列表第一项。上次选择只保存家谱/申请 ID,不依赖新增后台接口。
- G01 顶部当前选中的“可用家谱”同时作为全局家谱上下文;家族、世系、成员、家谱设置等家谱相关页面顶部必须明确显示当前谱名。用户在 G01 切换可用家谱后,其他业务 Tab 同步使用新上下文。审核中、被拒绝、已退出/移除或待录入始祖的记录仍可作为 G01 顶部展示项,但不得覆盖最近一个可用家谱的全局上下文;没有任何可用家谱时,家谱相关 Tab 显示引导状态并返回 G01 搜索、邀请码加入或创建,严禁展示上一个已失效家谱的缓存内容。
- 非空列表末尾保留“添加家谱”功能卡;点击后使用自定义底部弹层展示“搜索家谱、邀请码加入、创建家谱”。
### 2.3 加入与创建分流
- G06 统一命名为“加入家谱”,使用“搜索家谱/邀请码加入”双模式定位目标家谱。
- G06 搜索结果必须帮助用户区分同名家谱、大家族与下属小家族/支系:结果卡至少展示谱名、姓氏、地区、堂号、所属上级谱、当前支系、管理者或认证信息、成员规模和最近更新时间。用户只申请当前明确选中的家谱或支系;在接口没有明确父子成员继承规则前,不得用文案、勾选状态或成功结果暗示加入小家谱会自动获得上级大家谱身份。
- G06 结果卡按当前用户关系展示唯一操作:未加入显示“申请加入”;已加入显示“已加入”,点击后返回 G01 并选中该家谱;审核中显示“审核中”,点击进入 G09 查看进度;曾被拒绝显示“修改后重新申请”;已退出或被移除显示“重新申请”,进入表单前说明原状态;当前用户创建的家谱显示“我创建的”,点击后返回 G01 并选中。任何状态都不得再次创建重复申请或把状态按钮做成含义不明的“进入”。
- 未加入用户点击 G06 结果卡时复用 G05 的“公开预览”状态,不新增路由:只展示家谱身份、来源简介、地区、堂号、支系和公开说明,隐藏成员隐私、世系详情及全部管理入口,底部固定“申请加入”。已加入成员进入同一 G05 时再展示成员可见内容;公开字段范围最终以接口权限规则为准。
- 搜索模式通过地区筛选和谱名搜索定位家谱;邀请码模式验证邀请码后定位家谱。两种模式都进入 G08 填写真实姓名和亲属关系。
- G08 当前申请字段以现有可解释契约为准:`realName` 真实姓名必填;`relation` 与家谱关系必填,采用文字输入并提示“请以家谱中一位已知长辈为参照,例如:汤正华堂侄”;`message` 申请说明用于补充祖居地、长辈姓名等核验信息。现有接口没有参照成员 ID、推荐人或结构化关系字段,当前不得虚构关系树选择器;接口提供相关字段后,必须以新结构化契约替换文字关系旧路径并同步更新校验、页面和测试。
- G08 是同一路由的双来源状态。搜索来源显示“申请说明”,主按钮为“提交申请”,成功后进入 G09 查看审核进度;邀请码来源仍要求真实姓名和关系,将第三字段改为可选的“补充信息”,主按钮为“确认加入”,并明确提示“提交后直接加入,无需等待审核”,成功后返回 G01、刷新列表并选中新加入的家谱。两种来源共用结构与视觉组件,但标题、说明、按钮、成功结果和下一步必须匹配当前状态,不得混用“申请”与“直接加入”语义。
- 搜索来源的 G08 提交后进入管理员审核,记录在 G01“加入申请”和 G09“我的申请”中。
- 邀请码来源的 G08 提交后直接加入,不进入审核记录;成功后返回 G01、刷新列表并选中新加入的家谱。
- 当前 OpenAPI 尚未提供邀请码验证和直接加入接口;本轮只审核相关入口和页面状态外观,不伪造接口已通过,不在 UI 验收阶段对接接口。
- 创建家谱不需要平台审核:G03 创建成功后直接成为创建者,继续录入首代;始祖保存完成后必须进入 G05 家谱总览,不得沿用当前直接跳入 T01 世系树的旧路径。G05 对新建家谱显示“家谱已创建,可继续完善世系”的引导卡,用户主动点击后再进入 T01。
- 当前 G03 仅创建独立家谱。现有契约只有姓氏、谱名、堂号、所在地和访问规则,没有上级家谱 ID、独立/支系类型或支系管理权限;因此页面不得展示无法提交的“创建下属支系”。进入创建前及填写姓氏、地区、谱名后,应提示用户先搜索可能已有的家谱,减少重复建谱。“在大家谱下创建支系”登记为产品/接口依赖,待上级家谱字段和权限规则明确后再设计并接入。
- 家谱记录已创建但始祖尚未录入时,G01“我创建的”必须保留该记录并标记“待录入始祖”,不得静默丢失或再次创建;该记录参与顶部当前项选择,但顶部主操作只能显示“继续完善”,点击恢复 G03 始祖步骤。始祖完成前不进入正式 G05,也不开放世系与管理入口;保存成功后状态转为正常并进入 G05。中断恢复、重复创建防护和真实状态持久化归入后续功能验收,当前先审核相关页面状态外观。
### 2.4 G05 家谱总览与权限
- G05 首要任务是浏览和确认家谱信息,世系图属于次级入口,不占据首屏最强视觉位置。
- 新建家谱首次进入 G05 时显示一次“家谱已创建,可继续完善世系”的引导卡;卡片提供进入 T01 的明确操作,但不得自动跳转。引导关闭或完成后不持续占据首屏主位,后续仍从常规次级入口进入世系树。
- G05 同一路由包含未加入用户的公开预览态和已加入成员的完整浏览态;两种状态必须使用明确身份标签和不同操作区,禁止把隐藏内容留出空白占位,也禁止在公开态短暂闪现成员数据后再隐藏。
- 首屏必须完整呈现三层信息:身份确认(谱名、姓氏、地区、堂号/支系);来源确认(始祖、祖籍或迁徙来源);可信度确认(家谱简介、成员规模、最近更新时间)。
- 当前接口只明确家谱所有者和普通成员能力,未定义管理员授权、成员功能开关或权限清单。本轮采用最小权限模型:所有者显示管理入口;普通成员只显示浏览和本人相关入口,管理操作直接隐藏。
- 即使入口已隐藏,接口返回无权限时仍显示自定义无权限页面状态,不使用原生提示。
- 管理员授权和功能开关登记为产品/接口依赖,不自行虚构授权页面。
## 3. 用户身份与非理想操作
审核不能只走理想路径,至少覆盖以下身份和行为:
- 未登录/首次打开 APP 的用户;除认证、注册、协议和找回密码外不能进入业务页面。
- 新注册且没有家谱的用户。
- 创建家谱后的谱主/管理员。
- 搜索家谱并申请加入的普通用户。
- 已加入家谱的普通成员。
- 回访用户、资料不完整用户、无权限用户和账号异常用户。
- 未勾选协议、漏填、填错、长文本、重复点击、快速返回、切换 Tab、网络失败、空数据、权限拒绝、上传取消和操作中断。
## 4. 全局视觉、适配与表现质量验收项
所有用户可见提示必须使用项目自定义组件。禁止直接展示 `uni.showToast``uni.showModal``uni.showLoading``uni.showActionSheet` 等原生 UniApp 样式。当前代码中共有 40 处原生提示调用:37 处 `uni.showToast`、3 处 `uni.showModal`,均纳入后续替换范围。
弹窗、卡片、按钮和装饰面继续遵守完整位图规则:完整视觉面由独立不透明位图承载,透明 PNG 只作叠加装饰;CSS 只负责布局、文字、遮罩、状态和轻量动效。
所有表单采用“就近显示错误”:单字段错误显示在对应输入框下;关联错误同时标记相关字段;页面级错误显示在提交区附近;网络或系统错误使用自定义结果弹层。提交时错误字段执行一次 200–260ms、3–5px 的横向视觉抖动,只使用 `transform`,不调用手机振动、不循环闪烁;动画必须与静态错误描边和文字同时存在,减少动态效果时仅保留静态提示。
主题模式统一提供“浅色、深色、跟随系统”三个选项。入口放在 M10“设置与关于”页面顶部,由 M01 的“设置”菜单进入,不新增独立路由。浅色与深色共用页面结构、组件层级、交互规则和国风视觉语言,但必须通过统一主题变量分别定义背景、文字、边框、遮罩、状态色、状态栏图标和必要的位图资产;严禁使用系统自动反色或在页面内散落颜色覆盖。用户选择后当前页面立即生效,不重启、不重新进入页面、不闪白屏;页面、弹窗和状态栏使用一次 150–200ms 的轻微明暗过渡同步完成切换,不得出现局部延迟或颜色割裂。当前页面样式验收检查三选项控件和两套主题的完整视觉;选项持久化、重启恢复、系统主题变化后的自动联动归入后续功能验收。
所有分页列表共用同一套底部加载视觉:接近列表末尾时在内容流内显示轻量水墨/印章动效和“正在展开更多……”;失败时保留已加载内容并显示“加载失败,点击重试”;数据结束时显示“已阅至末尾”;数据不足一页时不强行显示结束文案,也不循环触发加载。底部分页不得使用全屏遮罩、清空现有列表、改变当前滚动位置或造成列表跳动。
所有需要获取最新状态的列表页共用下拉刷新视觉,并与底部触底加载明确区分:下拉过程使用短促的水墨聚合/印章落定动效;刷新期间保留原列表,不白屏、不清空内容、不跳动;有新内容时使用自定义轻提示“内容已更新”,内容无变化时安静收起;失败时保留原内容并在列表顶部显示可重试提示。纯静态说明页和不具备刷新语义的短内容不强行加入下拉刷新。
所有弹窗按用户决策成本统一分为四级:轻提示用于普通提醒和操作成功,短暂悬浮后自动消失;底部弹层用于选择、筛选、添加家谱等多选项任务;居中确认弹窗用于退出、撤回申请、拒绝审核等需要明确决定的操作;结果弹窗用于提交失败、网络异常、权限不足等结果说明,并必须给出下一步。删除家谱、移除成员等不可逆或高风险操作使用危险色、明确后果说明和二次确认;普通操作不得滥用二次确认。四类弹窗均使用项目自定义组件,不得直接显示原生 UniApp 弹窗或提示。
弹窗关闭行为必须统一适配 Android 返回键:普通底部弹层点击遮罩或按返回键均可关闭;弹层存在未保存输入时,关闭前提示是否放弃;普通确认弹窗的返回键等同“取消”;危险操作的二次确认不得因遮罩或返回键而直接确认或无提示消失,必须通过明确按钮完成或取消;加载任务可取消时显示“取消”,不可取消时禁用关闭并持续显示清晰的进行状态。任何关闭方式都不得把取消误判为成功。
空状态必须按原因设计,禁止用同一张“暂无数据”画面覆盖所有情况:首次使用时解释页面用途并提供一个主入口;搜索/筛选无结果时保留当前条件并提供清除筛选或更换关键词;确实暂无内容时说明内容如何产生,并按普通成员与管理者权限显示不同操作;加载失败和无权限不得伪装成空数据,必须分别提供重试或权限说明。每种空状态只突出一个主操作,最多提供一个次操作,避免按钮堆叠。
从列表进入详情后返回,必须恢复用户离开前的现场,包括滚动位置、搜索关键词、筛选条件、已展开分组、G01 当前选中的家谱和已加载页数;只有用户主动刷新、切换账号或原数据已失效时才重置。返回过程不得闪回列表顶部后再二次跳转。
### 4.1 工程质量底线
- 项目自有源码必须精准、清晰、可读,不得提交压缩、混淆或机器堆叠后无法维护的代码来代替源代码;第三方原始发行文件可保持压缩,但必须集中隔离在 `static/vendor/` 等明确目录,不得直接改写其压缩内容。
- 关键业务判断、状态转换、Android/UniApp 兼容处理、生命周期清理、性能取舍和非直观算法必须使用准确的中文注释说明“为什么这样做”;禁止用逐行复述代码的无效注释制造噪音。
- 组件、样式和状态命名必须表达真实用途;函数保持单一职责,控制嵌套深度,及时清除本次修改产生的废弃分支、无用变量和重复实现,不得遗留临时补丁、神秘常量或无法追溯的复制代码。
- 弹窗、加载、空状态、表单反馈、色彩、间距、圆角、字号和安全区等共用规则必须有单一维护入口;页面消费统一组件或设计变量,不得在各页复制一套近似实现。只有出现真实复用需求时才抽取公共层,禁止为了“看起来高级”制造过度抽象。
- 所有页面代码必须便于后续修改:结构、内容、状态和装饰职责清楚;数据状态不得散落在模板和样式中;布局不得依赖偶然可用的固定高度、负边距或设备特例堆叠。
- 样式实现必须同时通过 320×568、360×640、360×800、412×915 四档尺寸,并覆盖 Android 安全区、系统字体、键盘、长文本和返回手势;动画优先使用 `transform``opacity`,避免大面积重绘、重复监听和无清理定时器。
- 性能验收以 4GB 内存的中低端 Android 真机为底线:长列表使用至少 500 条 Mock 数据测试滚动、下拉刷新、触底加载和返回现场恢复;同一详情连续进入/退出 20 次不得越来越慢、白屏或串状态;快速切换 Tab、重复开关弹窗不得堆叠实例或残留遮罩;页面点击 100ms 内出现反馈,动画不得出现肉眼可见的持续掉帧;图片必须按显示槽位解码,禁止同页同时加载多张原始超大图。
- 每次实现坚持最小、可验证、可回退的外科式修改,只触碰当前验收项必需的文件;实现前明确状态契约,完成后执行相关测试、运行时检查、真机截图复核和 `git diff --check`。未获得真实证据不得宣称完成。
### 4.2 全局视觉、适配与表现质量门禁
| 状态 | 编号 | 验收项 | 通过标准 |
| --- | --- | --- | --- |
| `[ ]` | C01 | 自定义轻提示 | 普通提示、校验错误、操作成功均使用统一家谱视觉;替换 `uni.showToast` |
| `[ ]` | C02 | 自定义信息弹窗 | 审核说明、关系冲突和功能说明使用单按钮信息弹窗 |
| `[ ]` | C03 | 自定义确认弹窗 | 通过、拒绝、删除、退出登录等场景区分主按钮、次按钮和危险操作 |
| `[ ]` | C04 | 自定义结果弹窗 | 保存成功、提交失败、网络失败和可重试结果语义明确 |
| `[ ]` | C05 | 页面级加载 | 首屏使用结构稳定的骨架屏;按钮在原位置显示内部进度;局部卡片仅覆盖局部;列表统一下拉刷新与底部加载状态;仅在页面结构完全不可预知时使用全屏加载,不闪白屏或突然跳布局 |
| `[ ]` | C06 | 按钮级加载 | 登录、提交、保存期间立即禁用重复点击并显示明确进度 |
| `[ ]` | C07 | 权限与进度 | 相册、上传、分享权限及上传进度、取消、失败重试均有自定义反馈 |
| `[ ]` | C08 | 主题与四尺寸适配 | 浅色、深色均通过 320×568、360×640、360×800、412×915;跟随系统不自动反色,状态栏、弹窗和页面颜色一致且无溢出、裁切和错位 |
| `[ ]` | C09 | 安全区适配 | 状态栏、刘海、底部手势区和固定导航不遮挡页面内容或操作 |
| `[ ]` | C10 | 软键盘适配 | 输入框、校验信息和提交按钮不被软键盘遮挡,页面可滚动到当前字段 |
| `[ ]` | C11 | 字体与长文本 | 默认字号与约 1.3 倍系统字号下,Android 字体回退、超长族谱名、生僻姓名和长文案均不重叠、不截断关键含义、不遮挡主操作 |
| `[ ]` | C12 | 触控与误操作 | 点击区不小于约 44dp;连续点击不重复提交;返回前保护未保存内容 |
| `[ ]` | C13 | 图片性能 | 图片按显示槽位使用;避免同页同时解码多张超大位图;屏外图片延迟加载 |
| `[ ]` | C14 | 列表性能 | 使用至少 500 条 Mock 数据测试;长列表分批渲染并使用稳定 `key`;下拉刷新和触底加载均保留已有内容、滚动位置与布局高度;短列表不循环触发,滚动时不反复重绘整页 |
| `[ ]` | C15 | 动画性能 | 优先使用 `transform``opacity`;不使用大面积模糊、复杂滤镜和重阴影动画;主题切换在 150–200ms 内同步完成且不白屏、不持续掉帧 |
| `[ ]` | C16 | 响应反馈 | 点击后 100ms 内出现反馈;超过 300ms 的操作必须显示自定义加载状态 |
| `[ ]` | C17 | 页面生命周期 | 离开页面时清理定时器、监听器、上传任务和动画状态,避免越用越卡 |
| `[ ]` | C18 | 路由压力 | 同一详情连续进入/退出 20 次,快速切 Tab、重复开关弹窗,不白屏、不串状态、不重复堆栈、不残留遮罩且不逐次变慢 |
| `[ ]` | C19 | Android 真机性能 | 以 4GB 内存中低端 Android 为底线,真机复核启动、键盘、500 条长列表、图片解码、页面切换和系统返回手势,无肉眼可见的持续掉帧 |
| `[ ]` | C20 | 运行与构建洁净度 | 无控制台错误、资源 404、未处理 Promise、明显重复渲染和构建错误 |
## 5. 53 页真实用户流程验收表
### 5.1 首次打开与认证
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
| --- | ---: | --- | --- |
| `[~]` | 1 | A01 登录 | APP 首次打开;首次默认密码登录、后续恢复上次密码/短信方式,品牌区、微信登录入口、注册、忘记密码、协议未勾选/已勾选、空输入、错误提示、完整输入后点击登录再显示行为验证、按钮加载、键盘遮挡;320×568 全部核心入口同屏且不滚动 |
| `[~]` | 2 | A04 注册账号 | A01→注册;协议、手机号错误、密码不一致、注册中、成功/失败页面样式;当前临时在完整填写并点击注册后显示行为验证,注册短信接口提供后迁移为发送验证码前验证;注册成功后自动建立登录状态并进入 G01 |
| `[~]` | 3 | A05 重置密码 | A01→忘记密码;手机号、发送短信前行为验证、验证码、新密码、两次不一致、重设中、成功/失败页面样式;成功后返回 A01,保留手机号、切到密码登录并聚焦密码框,不自动登录 |
| `[~]` ⚠ | 4 | A06 登录状态 | A01 登录结果→阻断状态;仅覆盖账号冻结、停用、风险限制等无法继续登录的状态,必须展示原因、影响范围和恢复入口;普通密码/验证码错误、未注册及微信取消均留在 A01;当前缺少真实结果跳转 |
### 5.2 登录后进入我的家谱并完成首次分流
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
| --- | ---: | --- | --- |
| `[~]` | 5 | G01 我的家谱 | 登录成功→同页多状态;空态保留搜索/邀请码/创建三入口,以搜索为主、邀请码快捷、创建为次并提示先确认无现有家谱;三分组列表、顶部当前项切换、上次选择恢复、待录入始祖/继续完善、审核中、被拒绝、已退出/被移除、加载、失败、添加家谱弹层、通知入口 |
| `[~]` | 6 | G03 创建家谱 | G01 空态或“添加家谱”→创建独立家谱;姓氏、谱名、堂号、所在地、访问规则,创建前提示搜索潜在已有家谱;覆盖必填校验、创建中、失败和已创建但始祖未完成的中断恢复;成功后直接成为创建者→录入始祖→进入 G05 总览,不直接跳 T01;下属支系创建待接口提供上级谱与权限字段 |
| `[~]` | 7 | G06 加入家谱 | G01 空态或“添加家谱”→加入;搜索/邀请码双模式、地区与谱名筛选;结果卡展示谱名、姓氏、地区、堂号、所属上级谱、当前支系、管理/认证信息、成员规模和更新时间,并覆盖未加入、已加入、审核中、被拒绝、已退出/移除、我创建的六种操作状态;邀请码验证、初始、加载、结果、无结果、无效/过期邀请码、失败、清空输入 |
| `[~]` | 8 | G08 关系确认与入谱 | G06 选定家谱→填写真实姓名和以已知长辈为参照的文字关系;搜索态显示申请说明/提交申请并在成功后进入 G09,邀请码态显示可选补充信息/确认加入并明确无需审核,成功后返回 G01 选中新家谱;覆盖校验、重复提交、提交中、成功/失败、返回保护;结构化关系待接口提供后迁移 |
| `[~]` | 9 | G09 我的申请 | G08 搜索来源提交成功→申请记录;待审、通过、拒绝、修改后重新提交、撤回、空、失败;邀请码来源不进入本页 |
| `[~]` | 10 | G05 家谱总览 | G06 未加入结果卡→公开预览,或 G01 顶部可用家谱大卡片/G09 已通过→成员总览;公开态仅展示身份、来源、地区、堂号、支系、公开说明和申请入口;成员态展示首屏三层家谱信息;覆盖加载、正常、空、失败、所有者/成员入口差异、无权限,世系图降为次级入口 |
### 5.3 所有者管理家谱
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
| --- | ---: | --- | --- |
| `[~]` | 11 | G10 入谱审核 | G05/G01/N01→审核;列表、空、失败、通过/拒绝确认、无权限 |
| `[~]` | 12 | G11 家谱设置 | G05→设置;名称校验、公开范围、访问说明、保存中、成功/失败、无权限 |
| `[~]` | 13 | G12 字辈诗 | G01/G05→字辈诗;列表、空、编辑、校验、保存失败、无权限 |
### 5.4 世系树与成员
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
| --- | ---: | --- | --- |
| `[~]` | 14 | T01 世系树 | G05 新建引导卡或常规世系入口→世系树;G03 始祖保存不得直接跳入;覆盖加载、树、空、失败、横向提示、节点选中 |
| `[~]` | 15 | T04 新增亲属 | T01→新增;首位成员/普通亲属、关系选择、必填、保存中、结果 |
| `[~]` | 16 | T07 成员目录 | T01→成员目录;列表、搜索、无结果、失败、成员选择 |
| `[~]` | 17 | T03 成员档案 | T01/T07→成员;正常、失败、隐私字段、编辑入口 |
| `[~]` | 18 | T05 编辑成员 | T03→编辑;字段校验、保存中、成功/失败、无权限 |
| `[~]` | 19 | T06 编辑关系 | T01→编辑关系;冲突、循环关系、说明弹窗、保存失败 |
| `[~]` ⚠ | 20 | T08 成员状态 | 应由 T03 进入;去世、隐私隐藏、无权限;当前无真实入口 |
### 5.5 家族动态与内容
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
| --- | ---: | --- | --- |
| `[~]` | 21 | F01 家族首页 | 底部“家族”Tab;加载、列表、空、失败、发布及内容入口 |
| `[~]` | 22 | F02 发布动态 | F01→发布;空内容、键盘、提交中、成功/失败、重复提交 |
| `[~]` | 23 | F03 动态详情 | F01 动态卡→详情;正文、评论、操作结果、内容失效 |
| `[~]` | 24 | F04 谱文列表 | F01→谱文;分类、列表、空、失败、新建入口 |
| `[~]` ⚠ | 25 | F05 谱文详情 | 应由 F04 文章进入;详情、收藏、失效内容;当前卡片只弹提示 |
| `[~]` ⚠ | 26 | F06 编辑谱文 | 应由 F04 新建/F05 编辑进入;草稿、校验、保存、失败 |
| `[~]` | 27 | F07 相册列表 | F01→相册;列表、空、失败、新建相册 |
| `[~]` ⚠ | 28 | F08 相册详情 | 应由 F07 相册进入;照片墙、空相册、预览、失效;当前无跳转 |
| `[~]` ⚠ | 29 | F09 上传照片 | 应由 F08→添加照片;权限、选择、上传进度、失败重试 |
| `[~]` ⚠ | 30 | F10 家族视频 | 应由 F01 内容入口进入;未开放、空、列表、失败;当前无入口 |
### 5.6 人物、礼仪与档案
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
| --- | ---: | --- | --- |
| `[~]` ⚠ | 31 | R01 人物录 | 应由家族档案入口进入;列表、搜索、空、失败、新建 |
| `[~]` ⚠ | 32 | R02 人物详情 | 应由 R01 人物进入;详情、编辑、隐私、内容失效 |
| `[~]` ⚠ | 33 | R03 贺礼列表 | 应由家族工具进入;列表、空、失败、新增 |
| `[~]` ⚠ | 34 | R04 编辑贺礼 | 应由 R03 进入;新增/编辑、校验、保存、删除确认 |
| `[~]` | 35 | R05 礼仪列表 | F01→礼仪;列表、空、失败、新建 |
| `[~]` ⚠ | 36 | R06 礼仪详情 | 应由 R05 活动进入;详情、编辑、活动失效 |
| `[~]` ⚠ | 37 | R07 编辑礼仪 | 应由 R05 新建/R06 编辑进入;校验、保存、失败 |
| `[~]` ⚠ | 38 | R08 成长日志 | 应由人物/成员档案进入;时间轴、空、失败、新增 |
| `[~]` ⚠ | 39 | R09 人生事 | 应由人物/成员档案进入;时间轴、空、失败、新增 |
| `[~]` | 40 | R10 家族备忘 | F01→备忘;列表、空、失败、新增 |
| `[~]` ⚠ | 41 | R11 功德记录 | 应由家族档案入口进入;列表、空、失败、新增 |
### 5.7 消息
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
| --- | ---: | --- | --- |
| `[~]` | 42 | N01 消息中心 | G01/M01→消息;加载、未读、已读、全部已读、空、失败、审核跳转 |
| `[~]` ⚠ | 43 | N02 消息详情 | 应由 N01 消息卡进入;详情、已读、失效消息、业务跳转;当前点击只改已读 |
### 5.8 个人中心
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
| --- | ---: | --- | --- |
| `[~]` | 44 | M01 我的首页 | 底部“我的”Tab;资料、提醒、菜单、失败状态 |
| `[~]` | 45 | M02 个人资料 | M01→资料;头像权限、必填、保存中、成功/失败 |
| `[~]` | 46 | M03 账号与安全 | M01→安全;密码、手机号、设备、异常提醒 |
| `[~]` ⚠ | 47 | M04 修改密码 | 应由 M03 进入;旧密码、新密码、不一致、成功/失败 |
| `[~]` ⚠ | 48 | M05 修改手机号 | 应由 M03 进入;验证码、错误号码、成功/失败 |
| `[~]` | 49 | M06 帮助中心 | M01→帮助;常见问题、无结果、联系入口 |
| `[~]` ⚠ | 50 | M07 意见反馈 | 应由 M06 进入;校验、提交中、成功/失败 |
| `[~]` ⚠ | 51 | M08 应用推广 | 应由 M01 进入;分享权限、生成海报、取消、失败 |
| `[~]` ⚠ | 52 | M09 VIP 与订单 | 应由 M01 进入;无订单、权益、订单列表、加载失败 |
| `[~]` | 53 | M10 设置与关于 | M01→设置;页面顶部外观模式(浅色/深色/跟随系统)、协议、隐私、版本、退出确认/取消;明暗主题视觉均需审核 |
## 6. 当前视觉流程断点与后续功能依赖
除 APP 启动页 A01 外,目前有 22 个最终页面没有被其他页面真实跳转到:
- A06。
- T08。
- F05、F06、F08、F09、F10。
- R01、R02、R03、R04、R06、R07、R08、R09、R11。
- N02。
- M04、M05、M07、M08、M09。
这些页面必须先确定合理的上游入口和返回位置,再做用户流程验收。直接输入路由只能用于内部开发定位,不能作为用户流程证据。
后续功能验收必须覆盖:A01 微信登录、A01/A04/A05 公共行为验证、G06 邀请码验证、G08 邀请码直接加入、角色/权限清单和功能开关。它们不属于本文件的完成门槛,也不得因为页面样式通过而宣称真实数据链路通过。
## 7. 每项固定执行顺序
1. 从本表上一项或明确的分支入口进入页面。
2. 捕获本轮真实运行截图并检查是否为正确页面、正确状态、非加载中画面。
3. 默认展示一张代表图;明暗主题或复杂状态展示一张联系表,不连续铺陈对比截图。
4. 确认浅色与深色的关键状态、四档尺寸、代码质量和性能均已通过;任一主题未通过时不得提交冻结。
5. 用户确认通过则两套主题同时标 `[x]` 并冻结;用户指出问题则整个页面标 `[!]`
6. 返工前先更新最小契约测试并确认 RED,再做最小实现。
7. 测试、构建、截图或路由异常时先复现并定位根因,不盲改样式。
8. 返工后运行受影响测试、运行时 smoke、两套主题四尺寸复核、真实路由截图和 `git diff --check`
9. 接口资料只用于理解功能、字段和状态位置;本阶段不对接接口,也不执行接口功能验收。
## 8. 单项验收记录模板
```md
### <编号> <页面或全局项名称>
- 当前标记:`[~]`
- 真实入口:
- 用户身份:
- 本轮状态:
- 状态子项:正常 / 空 / 加载 / 失败 / 权限 / 业务状态
- 代表截图:
- 四尺寸结果:320×568 / 360×640 / 360×800 / 412×915
- 可见交互与误操作样式:
- 加载动画与表现质量:
- 页面样式契约与 smoke
- 功能/接口状态:不在本阶段验收
- 用户结论:通过 / 返工
- 返工范围:
```
## 9. 全量结束条件
- C01C20 全部为 `[x]`
- A02 已并入 A01`pages.json` 最终路由数为 53,且不存在旧入口、重定向或兼容分支。
- 53 个页面全部为 `[x]`,不存在 `[~]``[!]` 或未解释的 `⚠`
- 每个页面的适用状态子项全部通过,不以单张正常态截图代替状态验收。
- 22 个现有流程断点均已通过真实入口和返回路径验证。
- 40 处原生 UniApp 提示均已替换,运行页面不出现原生弹窗或 Toast 样式。
- 四尺寸检查、Android 真机复核、受影响测试、运行时 smoke、真实截图和 `git diff --check` 均有最新证据。
- 页面样式验收阶段未对接业务接口、未修改接口契约、未使用截图作为运行资产;后续另建功能验收规则验证真实数据链路。
- 未执行 `git add`、commit、push、reset、checkout 或上传。
-7
View File
@@ -7,13 +7,6 @@
"enablePullDownRefresh": false "enablePullDownRefresh": false
} }
}, },
{
"path": "pages/auth/a02-login",
"style": {
"navigationStyle": "custom",
"enablePullDownRefresh": false
}
},
{ {
"path": "pages/auth/a04-register", "path": "pages/auth/a04-register",
"style": { "style": {
+238 -240
View File
@@ -1,62 +1,82 @@
<!-- 页面编号A-01用途启动登录引导视觉基准已由用户验收地基迁移不得改变可见效果 --> <!-- 页面编号A-01用途APP 唯一登录入口承载密码验证码与微信登录入口 -->
<template> <template>
<view class="auth-page entry-page"> <view class="auth-page login-page">
<!-- 不透明纸纹底图承担页面底色透明山水只作为上层装饰 -->
<image class="paper-background" src="/static/assets/foundation/opaque/auth-page-paper.jpg" mode="aspectFill" /> <image class="paper-background" src="/static/assets/foundation/opaque/auth-page-paper.jpg" mode="aspectFill" />
<image class="scenery-background" src="/static/assets/foundation/transparent/auth-ink-scenery.png" mode="aspectFill" /> <image class="scenery-background" src="/static/assets/foundation/transparent/auth-ink-scenery.png" mode="aspectFill" />
<view class="auth-content"> <view class="auth-content">
<view class="auth-header"> <view class="auth-header">
<image class="header-background" src="/static/assets/foundation/opaque/auth-header.png" mode="aspectFill" /> <image class="header-background" src="/static/assets/modules/auth/opaque/a01-login-header-v1.png" mode="scaleToFill" />
<image class="header-seal" src="/static/assets/foundation/transparent/brand-seal.png" mode="aspectFit" />
</view> </view>
<view class="brand-intro"> <view class="login-scroll">
<view class="title-decoration"> <image class="login-scroll__skin" src="/static/assets/modules/auth/transparent/a01-login-scroll-frame-v1.png" mode="scaleToFill" />
<image class="title-cloud title-cloud-left" src="/static/assets/foundation/transparent/auth-title-cloud.png" mode="aspectFit" /> <view class="login-scroll__content">
<text class="brand-title">家谱</text> <view class="login-heading">
<image class="title-cloud title-cloud-right" src="/static/assets/foundation/transparent/auth-title-cloud.png" mode="aspectFit" /> <image class="heading-cloud" src="/static/assets/foundation/transparent/auth-title-cloud.png" mode="aspectFit" />
<text class="login-title">登录家谱</text>
<image class="heading-cloud heading-cloud--right" src="/static/assets/foundation/transparent/auth-title-cloud.png" mode="aspectFit" />
</view> </view>
<view class="brand-divider brand-divider--top"> <image class="title-divider" src="/static/assets/modules/auth/opaque/a01-title-divider-v2.png" mode="scaleToFill" />
<view class="brand-divider__line" />
<image class="brand-divider__ornament" src="/static/assets/foundation/transparent/auth-divider-knot.png" mode="aspectFit" /> <view class="login-tabs">
<view class="brand-divider__line" /> <view class="login-tab" :class="{ active: activeLoginMethod === 'password' }" hover-class="tap-fade" @click="switchLoginMethod('password')">密码登录</view>
<view class="login-tab" :class="{ active: activeLoginMethod === 'sms' }" hover-class="tap-fade" @click="switchLoginMethod('sms')">验证码登录</view>
</view> </view>
<text class="brand-subtitle">为家族留存可传承的记忆</text>
<view class="brand-divider brand-divider--bottom"> <view class="form-content">
<view class="brand-divider__line" /> <view class="input-row">
<image class="brand-divider__ornament" src="/static/assets/foundation/transparent/auth-divider-knot.png" mode="aspectFit" /> <image class="input-icon" src="/static/assets/modules/auth/transparent/a01-icon-phone-v1.png" mode="aspectFit" />
<view class="brand-divider__line" /> <input v-model.trim="phone" class="auth-input" type="number" maxlength="11" placeholder="手机号" placeholder-class="input-placeholder" confirm-type="next" />
</view>
<view v-if="activeLoginMethod === 'password'" class="input-row">
<image class="input-icon" src="/static/assets/modules/auth/transparent/a01-icon-lock-v1.png" mode="aspectFit" />
<input v-model="password" class="auth-input" :password="!passwordVisible" maxlength="32" placeholder="密码" placeholder-class="input-placeholder" confirm-type="done" @confirm="submitLogin" />
<view class="password-toggle" hover-class="tap-fade" @click="togglePasswordVisibility">
<image class="password-toggle__icon" :src="passwordVisible ? '/static/assets/modules/auth/transparent/a01-icon-eye-open-v1.png' : '/static/assets/modules/auth/transparent/a01-icon-eye-closed-pupil-v2.png'" mode="aspectFit" />
</view> </view>
</view> </view>
<view class="entry-actions"> <view v-else class="input-row">
<view class="entry-button entry-button--primary" hover-class="entry-button--pressed" @click="openLogin"> <image class="input-icon" src="/static/assets/modules/auth/transparent/a01-icon-sms-code-v2.png" mode="aspectFit" />
<!-- 完整不透明按钮皮肤承载底色边框与角纹内容组只保留可访问的图标和文案 --> <input v-model.trim="verificationCode" class="auth-input" type="number" maxlength="6" placeholder="验证码" placeholder-class="input-placeholder" confirm-type="done" @confirm="submitLogin" />
<image class="entry-button__skin" src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /> <view class="get-code" hover-class="tap-fade" @click="prepareGetCode">获取验证码</view>
<view class="entry-button__content"> </view>
<image class="entry-button__icon" src="/static/assets/foundation/transparent/auth-login-outline.png" mode="aspectFit" />
<text>登录</text> <view class="form-secondary-row">
<view v-if="activeLoginMethod === 'password'" class="forgot-password" hover-class="tap-fade" @click="prepareForgotPassword">忘记密码</view>
</view> </view>
</view> </view>
<view class="entry-button entry-button--secondary" hover-class="entry-button--pressed" @click="prepareWechatLogin"> <view class="login-submit" hover-class="button-pressed" @click="submitLogin">
<image class="entry-button__skin" src="/static/assets/foundation/opaque/a01-secondary-button.png" mode="scaleToFill" /> <image class="button-skin" src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<view class="entry-button__content"> <text class="login-submit__copy">登录</text>
<image class="entry-button__icon entry-button__icon--wechat" src="/static/assets/foundation/transparent/auth-wechat.png" mode="aspectFit" /> </view>
<view class="other-login-divider">
<view class="divider-line" />
<image class="divider-knot" src="/static/assets/foundation/transparent/auth-divider-knot.png" mode="aspectFit" />
<text>其他登录方式</text>
<image class="divider-knot divider-knot--right" src="/static/assets/foundation/transparent/auth-divider-knot.png" mode="aspectFit" />
<view class="divider-line" />
</view>
<view class="wechat-login" hover-class="button-pressed" @click="prepareWechatLogin">
<image class="button-skin" src="/static/assets/foundation/opaque/a01-secondary-button.png" mode="scaleToFill" />
<view class="wechat-login__content">
<image class="wechat-icon" src="/static/assets/foundation/transparent/auth-wechat.png" mode="aspectFit" />
<text>微信登录</text> <text>微信登录</text>
</view> </view>
</view> </view>
<view class="register-link" hover-class="link-hover" @click="prepareRegister"> <view class="register-entry">
<text>还没有账号<text class="register-emphasis">注册账号</text></text> <text>还没有账号</text>
</view> <view class="register-link" hover-class="tap-fade" @click="prepareRegister">注册账号</view>
</view> </view>
<view class="agreement-row" @click="toggleAgreement"> <view class="agreement-row" @click="toggleAgreement">
<view class="agreement-check" :class="{ checked: agreed }"> <image class="agreement-icon" :src="agreed ? '/static/assets/modules/auth/transparent/a02-agreement-checked.png' : '/static/assets/modules/auth/transparent/a02-agreement-unchecked.png'" mode="aspectFit" />
<text v-if="agreed"></text>
</view>
<view class="agreement-copy"> <view class="agreement-copy">
<text>我已阅读并同意</text> <text>我已阅读并同意</text>
<text class="agreement-link" @click.stop="prepareAgreement">用户协议</text> <text class="agreement-link" @click.stop="prepareAgreement">用户协议</text>
@@ -66,251 +86,229 @@
</view> </view>
</view> </view>
</view> </view>
</view>
<image class="exact-header-overlay" src="/static/assets/modules/auth/opaque/a01-login-header-exact-v2.png" mode="scaleToFill" />
<!-- 先验收自定义容器真实滑块数据留到接口阶段接入 -->
<view v-if="verificationVisible" class="verification-layer" @click="closeVerification">
<view class="verification-dialog" @click.stop>
<image class="verification-dialog__skin" src="/static/assets/modules/auth/opaque/a02-login-panel.png" mode="scaleToFill" />
<view class="verification-dialog__content">
<text class="verification-title">安全验证</text>
<text class="verification-copy">{{ verificationPurpose }}前需要完成行为验证</text>
<text class="verification-note">验证数据将在接口接入后启用</text>
<view class="verification-actions">
<view class="verification-action" hover-class="tap-fade" @click="closeVerification">取消</view>
<view class="verification-action verification-action--primary" hover-class="tap-fade" @click="confirmVerification">知道了</view>
</view>
</view>
</view>
</view>
<view v-if="feedbackVisible" class="feedback-toast">
<image class="feedback-toast__skin" src="/static/assets/foundation/opaque/a01-secondary-button.png" mode="scaleToFill" />
<text class="feedback-toast__copy">{{ feedbackMessage }}</text>
</view>
</view>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { onLoad, onUnload } from '@dcloudio/uni-app'
const LOGIN_METHOD_STORAGE_KEY = 'a01:last-login-method'
const activeLoginMethod = ref('password')
const passwordVisible = ref(false)
const phone = ref('')
const password = ref('')
const verificationCode = ref('')
const agreed = ref(false) const agreed = ref(false)
const verificationVisible = ref(false)
const verificationPurpose = ref('登录')
const feedbackVisible = ref(false)
const feedbackMessage = ref('')
let feedbackTimer = null
onLoad(() => {
const storedMethod = uni.getStorageSync(LOGIN_METHOD_STORAGE_KEY)
if (storedMethod === 'sms') activeLoginMethod.value = 'sms'
})
onUnload(() => {
if (feedbackTimer) clearTimeout(feedbackTimer)
})
const showFeedback = (message) => {
feedbackMessage.value = message
feedbackVisible.value = true
if (feedbackTimer) clearTimeout(feedbackTimer)
feedbackTimer = setTimeout(() => {
feedbackVisible.value = false
feedbackTimer = null
}, 2200)
}
const switchLoginMethod = (method) => {
if (method !== 'password' && method !== 'sms') return
activeLoginMethod.value = method
uni.setStorageSync(LOGIN_METHOD_STORAGE_KEY, method)
}
const togglePasswordVisibility = () => {
passwordVisible.value = !passwordVisible.value
}
const toggleAgreement = () => { const toggleAgreement = () => {
agreed.value = !agreed.value agreed.value = !agreed.value
} }
const requireAgreement = () => { const validatePhone = () => {
if (agreed.value) return true if (/^1\d{10}$/.test(phone.value)) return true
uni.showToast({ title: '请先阅读并同意相关协议', icon: 'none' }) showFeedback('请输入正确的手机号')
return false return false
} }
const openLogin = () => { const requireAgreement = () => {
if (!requireAgreement()) return if (agreed.value) return true
uni.navigateTo({ url: '/pages/auth/a02-login?agreed=1' }) showFeedback('请先阅读并同意用户协议与隐私政策')
return false
} }
const openVerification = (purpose) => {
verificationPurpose.value = purpose
verificationVisible.value = true
}
const closeVerification = () => {
verificationVisible.value = false
}
const confirmVerification = () => {
closeVerification()
showFeedback('行为验证接口待接入')
}
const prepareGetCode = () => {
if (!validatePhone() || !requireAgreement()) return
openVerification('发送验证码')
}
const submitLogin = () => {
if (!validatePhone()) return
if (activeLoginMethod.value === 'password' && !password.value) {
showFeedback('请输入密码')
return
}
if (activeLoginMethod.value === 'sms' && !/^\d{6}$/.test(verificationCode.value)) {
showFeedback('请输入 6 位验证码')
return
}
if (!requireAgreement()) return
openVerification('登录')
}
const prepareForgotPassword = () => uni.navigateTo({ url: '/pages/auth/a05-reset-password' })
const prepareRegister = () => uni.navigateTo({ url: '/pages/auth/a04-register' })
const prepareWechatLogin = () => { const prepareWechatLogin = () => {
if (!requireAgreement()) return if (!requireAgreement()) return
uni.showToast({ title: '微信登录功能准备中', icon: 'none' }) showFeedback('微信登录功能准备中')
} }
const prepareRegister = () => { const prepareAgreement = () => showFeedback('协议页面准备中')
if (!requireAgreement()) return
uni.showToast({ title: '注册页面准备中', icon: 'none' })
}
const prepareAgreement = () => {
uni.showToast({ title: '协议页面准备中', icon: 'none' })
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.auth-page { .auth-page {
position: relative; position: relative;
min-height: 100vh; box-sizing: border-box;
background: #f7f1e6; width: 100%;
height: 100dvh;
min-height: 100%;
overflow-x: hidden;
overflow-y: auto;
background: #f6efe2;
color: #493323;
} }
.paper-background, .paper-background,
.scenery-background { .scenery-background {
position: absolute; position: fixed;
inset: 0; inset: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
}
.paper-background { opacity: 0.96; }
.scenery-background { opacity: 0.82; }
.auth-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
box-sizing: border-box;
min-height: 100vh;
padding-bottom: calc(44rpx + env(safe-area-inset-bottom));
}
.auth-header {
position: relative;
height: 253rpx;
overflow: hidden;
}
.header-background {
width: 100%;
height: 100%;
}
.header-seal {
position: absolute;
top: 70rpx;
left: 50%;
width: 136rpx;
height: 136rpx;
transform: translateX(-50%);
}
.brand-intro {
display: flex;
flex-direction: column;
align-items: center;
padding: 150rpx 56rpx 0;
color: #3f2c1d;
}
.title-decoration {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.brand-title {
margin: 0 12rpx;
font-family: STKaiti, KaiTi, serif;
font-size: 112rpx;
font-weight: 700;
letter-spacing: 12rpx;
line-height: 1;
}
.title-cloud { width: 64rpx; height: 40rpx; }
.title-cloud-left { margin-right: 0; }
.title-cloud-right { margin-left: 0; transform: scaleX(-1); }
.brand-divider {
display: flex;
align-items: center;
justify-content: center;
width: 426rpx;
}
.brand-divider--top { margin: 47rpx 0 21rpx; }
.brand-divider--bottom { margin: 34rpx 0 30rpx; }
.brand-divider__line {
flex: 1;
height: 1rpx;
background: #d5a34f;
}
.brand-divider__ornament {
width: 44rpx;
height: 24rpx;
margin: 0 14rpx;
}
.brand-subtitle {
color: #624631;
font-family: STKaiti, KaiTi, serif;
font-size: 36rpx;
letter-spacing: 4rpx;
}
.entry-actions {
margin: 95rpx 110rpx 0;
}
.entry-button {
position: relative;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 100%;
height: 118rpx;
font-family: STKaiti, KaiTi, serif;
font-size: 38rpx;
letter-spacing: 4rpx;
}
.entry-button__skin {
position: absolute;
inset: 0;
z-index: 1;
width: 100%;
height: 100%;
pointer-events: none; pointer-events: none;
} }
.paper-background { opacity: 0.98; }
.scenery-background { opacity: 0.72; }
.exact-header-overlay { position: fixed; z-index: 5; top: 0; left: 0; width: 100%; height: 267rpx; pointer-events: none; }
.entry-button--primary { .auth-content { position: relative; z-index: 1; min-height: 100%; padding-bottom: env(safe-area-inset-bottom); }
color: #fffaf0; .auth-header { position: relative; height: 304rpx; }
} .header-background { width: 100%; height: 100%; }
.entry-button--secondary { .login-scroll { position: relative; z-index: 4; box-sizing: border-box; height: 1520rpx; margin: -80rpx 12rpx 0; }
margin-top: 51rpx; .login-scroll__skin { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
color: #60442f; .login-scroll__content { position: relative; z-index: 1; box-sizing: border-box; height: 100%; padding: 224rpx 97rpx 66rpx; }
}
.entry-button__icon { .login-heading { display: flex; align-items: center; justify-content: center; height: 74rpx; }
width: 46rpx; .login-title { margin: 0 8rpx; color: #6f140f; font-family: STKaiti, KaiTi, serif; font-size: 64rpx; font-weight: 700; letter-spacing: 8rpx; line-height: 1; }
height: 46rpx; .heading-cloud { width: 48rpx; height: 34rpx; }
margin-right: 18rpx; .heading-cloud--right { transform: scaleX(-1); }
} .title-divider { position: absolute; top: 310rpx; left: 50%; display: block; width: 286rpx; height: 38rpx; transform: translateX(-50%); }
.entry-button__icon--wechat { width: 52rpx; } .login-tabs { display: flex; align-items: flex-end; height: 104rpx; margin: 14rpx 0 0; border-bottom: 1rpx solid rgba(193, 142, 67, 0.48); }
.entry-button--primary .entry-button__icon { filter: brightness(0) invert(1); } .login-tab { position: relative; display: flex; flex: 1; align-items: center; justify-content: center; min-height: 88rpx; color: #7f6856; font-family: STKaiti, KaiTi, serif; font-size: 33rpx; letter-spacing: 2rpx; }
.entry-button__content { .login-tab:first-child::before { position: absolute; top: 50%; right: 0; width: 1rpx; height: 38rpx; background: rgba(193, 142, 67, 0.42); content: ''; transform: translateY(-50%); }
position: relative; .login-tab.active { color: #a9160d; font-weight: 700; }
z-index: 3; .login-tab.active::after { position: absolute; right: 102rpx; bottom: -2rpx; left: 102rpx; height: 5rpx; border-radius: 5rpx; background: #ad160d; content: ''; }
display: flex;
align-items: center;
transform: translateX(-20rpx);
}
.entry-button--pressed { opacity: 0.84; }
.register-link { .form-content { margin-top: 16rpx; }
display: flex; .input-row { display: flex; align-items: center; box-sizing: border-box; min-height: 128rpx; padding-left: 14rpx; border-bottom: 1rpx solid rgba(197, 145, 70, 0.62); }
align-items: center; .input-icon { flex: 0 0 auto; width: 58rpx; height: 58rpx; margin-right: 22rpx; }
justify-content: center; .auth-input { flex: 1; min-width: 0; height: 116rpx; color: #493323; font-size: 32rpx; }
margin-top: 98rpx; .input-placeholder { color: #a79e94; }
color: #60442f; .password-toggle { display: flex; flex: 0 0 auto; align-items: center; justify-content: center; width: 88rpx; height: 88rpx; margin-right: 0; }
font-family: STKaiti, KaiTi, serif; .password-toggle__icon { width: 56rpx; height: 56rpx; }
font-size: 30rpx; .get-code { display: flex; flex: 0 0 auto; align-items: center; min-height: 88rpx; padding-left: 18rpx; color: #a9160d; font-size: 24rpx; white-space: nowrap; }
letter-spacing: 2rpx; .form-secondary-row { display: flex; align-items: center; justify-content: flex-end; min-height: 80rpx; }
white-space: nowrap; .forgot-password { display: flex; align-items: center; min-height: 80rpx; color: #a9160d; font-size: 28rpx; }
}
.register-emphasis { color: #b7170d; } .login-submit,
.wechat-login { position: relative; display: flex; align-items: center; justify-content: center; overflow: hidden; }
.login-submit { height: 112rpx; margin-top: 34rpx; }
.button-skin { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.login-submit__copy { position: relative; z-index: 1; color: #fffaf1; font-family: STKaiti, KaiTi, serif; font-size: 42rpx; letter-spacing: 8rpx; }
.other-login-divider { display: flex; align-items: center; height: 74rpx; margin-top: 36rpx; color: #b07b31; font-family: STKaiti, KaiTi, serif; font-size: 27rpx; white-space: nowrap; }
.divider-line { flex: 1; height: 1rpx; margin: 0 10rpx; background: rgba(194, 139, 60, 0.58); }
.divider-knot { width: 18rpx; height: 18rpx; margin-right: 8rpx; }
.divider-knot--right { margin: 0 0 0 8rpx; transform: scaleX(-1); }
.wechat-login { height: 114rpx; margin-top: 8rpx; color: #493323; font-family: STKaiti, KaiTi, serif; font-size: 34rpx; letter-spacing: 4rpx; }
.wechat-login__content { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; }
.wechat-icon { width: 48rpx; height: 48rpx; margin-right: 14rpx; filter: sepia(1) saturate(2.7) hue-rotate(52deg) brightness(0.78); }
.register-entry { display: flex; align-items: center; justify-content: center; min-height: 66rpx; margin-top: 32rpx; color: #493323; font-size: 27rpx; }
.register-link { display: flex; align-items: center; min-height: 66rpx; margin-left: 10rpx; color: #a9160d; }
.agreement-row { display: flex; align-items: center; justify-content: center; min-height: 68rpx; margin-top: 24rpx; color: #58483c; font-size: 20rpx; line-height: 30rpx; }
.agreement-icon { flex: 0 0 auto; width: 32rpx; height: 32rpx; margin-right: 10rpx; }
.agreement-copy { display: flex; flex: 0 1 auto; align-items: center; min-width: 0; white-space: nowrap; }
.agreement-link { color: #a9160d; }
.tap-fade { opacity: 0.62; }
.button-pressed { opacity: 0.82; }
.link-hover { opacity: 0.65; } .feedback-toast { position: fixed; z-index: 30; top: calc(env(safe-area-inset-top) + 32rpx); left: 50%; display: flex; align-items: center; justify-content: center; width: 590rpx; min-height: 88rpx; transform: translateX(-50%); }
.feedback-toast__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.feedback-toast__copy { position: relative; z-index: 1; padding: 18rpx 40rpx; color: #5c4330; font-size: 25rpx; text-align: center; }
.agreement-row { .verification-layer { position: fixed; z-index: 40; inset: 0; display: flex; align-items: center; justify-content: center; padding: 40rpx; background: rgba(31, 20, 13, 0.56); }
display: flex; .verification-dialog { position: relative; width: 620rpx; height: 520rpx; }
align-items: center; .verification-dialog__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
justify-content: center; .verification-dialog__content { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; box-sizing: border-box; height: 100%; padding: 90rpx 64rpx 54rpx; }
margin-top: 141rpx; .verification-title { color: #6f140f; font-family: STKaiti, KaiTi, serif; font-size: 42rpx; font-weight: 700; }
padding: 0 44rpx calc(32rpx + env(safe-area-inset-bottom)); .verification-copy { margin-top: 42rpx; color: #493323; font-size: 28rpx; text-align: center; }
color: #6b5a4a; .verification-note { margin-top: 20rpx; color: #8d7a68; font-size: 23rpx; }
font-size: 20rpx; .verification-actions { display: flex; width: 100%; margin-top: auto; }
line-height: 30rpx; .verification-action { display: flex; flex: 1; align-items: center; justify-content: center; min-height: 88rpx; color: #745b46; font-size: 28rpx; }
} .verification-action--primary { color: #a9160d; font-weight: 700; }
.agreement-check {
display: flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 30rpx;
height: 30rpx;
margin: 0 12rpx 0 0;
border: 2rpx solid #b88d4b;
border-radius: 50%;
color: #fffaf0;
font-size: 20rpx;
line-height: 1;
}
.agreement-check.checked { background: #b7170d; border-color: #b7170d; }
.agreement-copy {
display: flex;
align-items: center;
line-height: 30rpx;
text-align: center;
white-space: nowrap;
}
.agreement-link { color: #b7170d; }
</style> </style>
-446
View File
@@ -1,446 +0,0 @@
<!-- 页面编号A-02用途账号登录当前仅保留已确认的本地登录界面 -->
<template>
<view class="auth-page login-page">
<!-- A01 共用的登录页底图层先不透明纸纹后透明山水 -->
<image class="paper-background" src="/static/assets/foundation/opaque/auth-page-paper.jpg" mode="aspectFill" />
<image class="scenery-background" src="/static/assets/foundation/transparent/auth-ink-scenery.png" mode="aspectFill" />
<view class="auth-content">
<view class="auth-header">
<image class="header-background" src="/static/assets/foundation/opaque/auth-header.png" mode="aspectFill" />
<image class="header-seal" src="/static/assets/foundation/transparent/brand-seal.png" mode="aspectFit" />
</view>
<view class="login-panel">
<image class="login-panel__skin" src="/static/assets/modules/auth/opaque/a02-login-panel.png" mode="scaleToFill" />
<view class="login-panel__content">
<view class="page-heading">
<image class="login-title-cloud" src="/static/assets/foundation/transparent/auth-title-cloud.png" mode="aspectFit" />
<view class="back-button" hover-class="back-hover" @click="goBack">
<image class="back-button__icon" src="/static/assets/foundation/transparent/chevron-right.png" mode="aspectFit" />
</view>
<view class="page-heading__copy">
<text class="page-title">登录家谱</text>
<text class="page-subtitle">欢迎回到家族记忆</text>
</view>
</view>
<view class="login-tabs">
<view class="login-tab" :class="{ active: activeTab === 'password' }" @click="activeTab = 'password'">
<text>账号密码登录</text>
</view>
<view class="login-tab" :class="{ active: activeTab === 'sms' }" @click="activeTab = 'sms'">
<text>手机验证码登录</text>
</view>
</view>
<view v-if="activeTab === 'password'" class="form-content">
<view class="input-row">
<text class="input-label">账号</text>
<input v-model.trim="account" class="auth-input" placeholder="请输入账号或手机号" placeholder-class="placeholder" />
</view>
<view class="input-row">
<text class="input-label">密码</text>
<input v-model="password" class="auth-input" password placeholder="请输入密码" placeholder-class="placeholder" />
</view>
<view class="secondary-action" hover-class="text-hover" @click="prepareForgotPassword">忘记密码</view>
</view>
<view v-else class="form-content">
<view class="input-row">
<text class="input-label">手机</text>
<input v-model.trim="smsPhone" class="auth-input" type="number" maxlength="11" placeholder="请输入手机号" placeholder-class="placeholder" />
</view>
<view class="input-row code-row">
<text class="input-label">验证码</text>
<input v-model.trim="verificationCode" class="auth-input" type="number" maxlength="6" placeholder="请输入验证码" placeholder-class="placeholder" />
<view class="get-code" hover-class="text-hover" @click="prepareGetCode">获取验证码</view>
</view>
</view>
<view class="login-submit" hover-class="button-hover" @click="submitLogin">
<image class="login-submit__skin" src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<text class="login-submit__content">登录</text>
</view>
<view class="agreement-row" @click="toggleAgreement">
<image class="agreement-icon" :src="agreed ? '/static/assets/modules/auth/transparent/a02-agreement-checked.png' : '/static/assets/modules/auth/transparent/a02-agreement-unchecked.png'" mode="aspectFit" />
<view class="agreement-copy">
<text>我已阅读并同意</text>
<text class="agreement-link" @click.stop="prepareAgreement">用户协议</text>
<text></text>
<text class="agreement-link" @click.stop="prepareAgreement">隐私政策</text>
</view>
</view>
<view class="register-entry">
<text>还没有账号</text>
<view class="register-link" hover-class="text-hover" @click="prepareRegister">注册账号</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
const activeTab = ref('password')
const account = ref('')
const password = ref('')
const smsPhone = ref('')
const verificationCode = ref('')
const agreed = ref(false)
const isAgreedRoute = () => {
if (typeof location !== 'undefined') {
const query = new URLSearchParams(location.hash.split('?')[1] || '')
return query.get('agreed') === '1'
}
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
return currentPage?.options?.agreed === '1'
}
const syncAgreementFromRoute = () => {
agreed.value = isAgreedRoute()
}
onLoad(() => {
syncAgreementFromRoute()
})
onShow(() => {
syncAgreementFromRoute()
})
const goBack = () => {
uni.navigateBack()
}
const prepareRegister = () => uni.navigateTo({ url: '/pages/auth/a04-register' })
const prepareForgotPassword = () => uni.navigateTo({ url: '/pages/auth/a05-reset-password' })
const prepareGetCode = () => {
uni.showToast({ title: '获取验证码功能待接入', icon: 'none' })
}
const toggleAgreement = () => {
agreed.value = !agreed.value
}
const prepareAgreement = () => {
uni.showToast({ title: '协议页面准备中', icon: 'none' })
}
const canSubmit = () => {
if (agreed.value) return true
uni.showToast({ title: '请先阅读并同意相关协议', icon: 'none' })
return false
}
const submitLogin = () => {
if (!canSubmit()) return
if (activeTab.value === 'password') {
if (!account.value) {
uni.showToast({ title: '请输入账号或手机号', icon: 'none' })
return
}
if (!password.value) {
uni.showToast({ title: '请输入密码', icon: 'none' })
return
}
} else {
if (!/^1\d{10}$/.test(smsPhone.value)) {
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
return
}
if (!verificationCode.value) {
uni.showToast({ title: '请输入验证码', icon: 'none' })
return
}
}
uni.showToast({ title: '登录接口待接入', icon: 'none' })
}
</script>
<style scoped lang="scss">
.auth-page {
position: relative;
min-height: 100vh;
background: #f7f1e6;
}
.paper-background,
.scenery-background {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.paper-background { opacity: 0.96; }
.scenery-background { opacity: 0.76; }
.auth-content {
position: relative;
z-index: 1;
box-sizing: border-box;
min-height: 100vh;
padding-bottom: calc(44rpx + env(safe-area-inset-bottom));
}
.auth-header {
position: relative;
height: 253rpx;
overflow: hidden;
}
.header-background {
width: 100%;
height: 100%;
}
.header-seal {
position: absolute;
top: 70rpx;
left: 50%;
width: 136rpx;
height: 136rpx;
transform: translateX(-50%);
}
.login-panel {
position: relative;
min-height: 1184rpx;
margin: 32rpx 42rpx 0;
}
.login-panel__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.login-panel__content {
position: relative;
z-index: 1;
box-sizing: border-box;
min-height: 1184rpx;
padding: 66rpx 50rpx 76rpx;
}
.page-heading {
position: relative;
display: flex;
align-items: center;
}
.login-title-cloud {
position: absolute;
top: -26rpx;
right: 2rpx;
width: 154rpx;
height: 92rpx;
opacity: 0.62;
}
.page-heading__copy {
position: relative;
z-index: 1;
}
.back-button {
display: flex;
align-items: center;
justify-content: center;
width: 56rpx;
height: 56rpx;
margin-right: 18rpx;
}
.back-hover { opacity: 0.62; }
.back-button__icon {
width: 42rpx;
height: 42rpx;
transform: scaleX(-1);
}
.page-title,
.page-subtitle {
display: block;
}
.page-title {
color: #3f2c1d;
font-family: STKaiti, KaiTi, serif;
font-size: 50rpx;
font-weight: 700;
letter-spacing: 4rpx;
}
.page-subtitle {
margin-top: 10rpx;
color: #8c7864;
font-size: 24rpx;
letter-spacing: 2rpx;
}
.login-tabs {
display: flex;
margin-top: 62rpx;
border-bottom: 1rpx solid #dcc29a;
}
.login-tab {
position: relative;
flex: 1;
padding: 0 0 28rpx;
color: #89715a;
font-family: STKaiti, KaiTi, serif;
font-size: 30rpx;
text-align: center;
}
.login-tab.active {
color: #a7160c;
font-weight: 700;
}
.login-tab.active::after {
position: absolute;
right: 28rpx;
bottom: -1rpx;
left: 28rpx;
height: 4rpx;
border-radius: 4rpx;
background: #b7170d;
content: '';
}
.form-content { margin-top: 36rpx; }
.input-row {
display: flex;
align-items: center;
min-height: 112rpx;
border-bottom: 1rpx solid #e5d4b7;
}
.input-label {
width: 100rpx;
color: #674b35;
font-family: STKaiti, KaiTi, serif;
font-size: 31rpx;
}
.auth-input {
flex: 1;
height: 104rpx;
color: #3f2c1d;
font-size: 29rpx;
}
.placeholder { color: #b3a895; }
.code-row .auth-input { min-width: 0; }
.get-code {
flex: 0 0 auto;
padding-left: 20rpx;
border-left: 1rpx solid #ddc7a3;
color: #a7160c;
font-size: 25rpx;
}
.secondary-action {
margin-top: 34rpx;
color: #a7160c;
font-size: 25rpx;
text-align: right;
}
.text-hover { opacity: 0.64; }
.login-submit {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 128rpx;
margin-top: 76rpx;
overflow: hidden;
}
.login-submit__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.login-submit__content {
position: relative;
z-index: 1;
color: #fffaf0;
font-family: STKaiti, KaiTi, serif;
font-size: 36rpx;
letter-spacing: 6rpx;
}
.button-hover { opacity: 0.84; }
.agreement-row {
display: flex;
align-items: center;
justify-content: center;
min-height: 88rpx;
margin-top: 22rpx;
padding: 0 4rpx;
color: #6b5a4a;
font-size: 24rpx;
line-height: 32rpx;
}
.agreement-icon {
flex: 0 0 auto;
width: 32rpx;
height: 32rpx;
margin-right: 12rpx;
}
.agreement-copy {
display: flex;
align-items: center;
text-align: center;
white-space: nowrap;
}
.agreement-link {
color: #a7160c;
}
.register-entry {
display: flex;
align-items: center;
justify-content: center;
margin-top: 2rpx;
color: #6b5a4a;
font-size: 24rpx;
}
.register-link {
display: flex;
align-items: center;
min-height: 88rpx;
margin-left: 8rpx;
color: #a7160c;
}
</style>
+2 -2
View File
@@ -2,7 +2,7 @@
<template> <template>
<view class="auth-page register-page"> <view class="auth-page register-page">
<image class="paper-background" src="/static/assets/foundation/opaque/auth-page-paper.jpg" mode="aspectFill" /> <image class="paper-background" src="/static/assets/foundation/opaque/auth-page-paper.jpg" mode="aspectFill" />
<image class="scenery-background" src="/static/assets/foundation/transparent/auth-ink-scenery.png" mode="aspectFill" /> <image class="scenery-background" src="/static/assets/foundation/transparent/auth-design-scenery-v1.png" mode="scaleToFill" />
<view class="auth-content"> <view class="auth-content">
<view class="auth-header"> <view class="auth-header">
@@ -78,7 +78,7 @@ const goBack = () => {
uni.navigateBack() uni.navigateBack()
} }
const prepareLogin = () => uni.navigateTo({ url: '/pages/auth/a02-login' }) const prepareLogin = () => uni.navigateTo({ url: '/pages/auth/a01-entry' })
const prepareAgreement = () => { const prepareAgreement = () => {
uni.showToast({ title: '协议页面准备中', icon: 'none' }) uni.showToast({ title: '协议页面准备中', icon: 'none' })
+2 -2
View File
@@ -3,7 +3,7 @@
<view class="auth-page reset-page"> <view class="auth-page reset-page">
<!-- 认证页背景复用已验收纸纹与淡墨山水不绘制 CSS 装饰面 --> <!-- 认证页背景复用已验收纸纹与淡墨山水不绘制 CSS 装饰面 -->
<image class="paper-background" src="/static/assets/foundation/opaque/auth-page-paper.jpg" mode="aspectFill" /> <image class="paper-background" src="/static/assets/foundation/opaque/auth-page-paper.jpg" mode="aspectFill" />
<image class="scenery-background" src="/static/assets/foundation/transparent/auth-ink-scenery.png" mode="aspectFill" /> <image class="scenery-background" src="/static/assets/foundation/transparent/auth-design-scenery-v1.png" mode="scaleToFill" />
<view class="auth-content"> <view class="auth-content">
<!-- 朱砂祠堂头和谱印与 A02/A04 保持同一认证入口 --> <!-- 朱砂祠堂头和谱印与 A02/A04 保持同一认证入口 -->
@@ -78,7 +78,7 @@ const goBack = () => {
uni.navigateBack() uni.navigateBack()
} }
const prepareLogin = () => uni.navigateTo({ url: '/pages/auth/a02-login' }) const prepareLogin = () => uni.navigateTo({ url: '/pages/auth/a01-entry' })
const prepareGetCode = () => { const prepareGetCode = () => {
uni.showToast({ title: '验证码功能待接入', icon: 'none' }) uni.showToast({ title: '验证码功能待接入', icon: 'none' })
+2 -2
View File
@@ -3,7 +3,7 @@
<view class="auth-page status-page"> <view class="auth-page status-page">
<!-- 认证入口沿用纸纹淡墨与祠堂头图不以 CSS 重绘完整装饰面 --> <!-- 认证入口沿用纸纹淡墨与祠堂头图不以 CSS 重绘完整装饰面 -->
<image class="paper-background" src="/static/assets/foundation/opaque/auth-page-paper.jpg" mode="aspectFill" /> <image class="paper-background" src="/static/assets/foundation/opaque/auth-page-paper.jpg" mode="aspectFill" />
<image class="scenery-background" src="/static/assets/foundation/transparent/auth-ink-scenery.png" mode="aspectFill" /> <image class="scenery-background" src="/static/assets/foundation/transparent/auth-design-scenery-v1.png" mode="scaleToFill" />
<view class="auth-content"> <view class="auth-content">
<view class="auth-header"> <view class="auth-header">
@@ -131,7 +131,7 @@ onShow(() => {
}) })
const goLogin = () => { const goLogin = () => {
uni.navigateTo({ url: '/pages/auth/a02-login' }) uni.navigateTo({ url: '/pages/auth/a01-entry' })
} }
const goRegister = () => { const goRegister = () => {
+2 -1
View File
@@ -3,6 +3,7 @@ const path = require('path')
const [url, selector, output, width = '360', height = '800', action] = process.argv.slice(2) const [url, selector, output, width = '360', height = '800', action] = process.argv.slice(2)
const renderSettleMilliseconds = 900 const renderSettleMilliseconds = 900
const chromeDebuggingPort = process.env.CHROME_DEBUGGING_PORT || '9222'
if (!url || !selector || !output) { if (!url || !selector || !output) {
throw new Error('Usage: node scripts/capture-chrome-page.js <url> <selector> <output> [width] [height] [sms-tab|g06-results|g06-empty|fresh-navigation]') throw new Error('Usage: node scripts/capture-chrome-page.js <url> <selector> <output> [width] [height] [sms-tab|g06-results|g06-empty|fresh-navigation]')
@@ -11,7 +12,7 @@ if (!url || !selector || !output) {
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)) const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))
const connect = async () => { const connect = async () => {
const pages = await (await fetch('http://127.0.0.1:9222/json/list')).json() const pages = await (await fetch(`http://127.0.0.1:${chromeDebuggingPort}/json/list`)).json()
const page = pages.find((item) => item.type === 'page' && item.url.startsWith('http://localhost:5173')) const page = pages.find((item) => item.type === 'page' && item.url.startsWith('http://localhost:5173'))
if (!page) throw new Error('Chrome debugging has no localhost:5173 page') if (!page) throw new Error('Chrome debugging has no localhost:5173 page')
Binary file not shown.

After

Width:  |  Height:  |  Size: 619 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-message-code" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.7" stroke="#684e33" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M4 21v-13a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v6a3 3 0 0 1 -3 3h-9l-4 4" />
<path d="M10 9l-2 2l2 2" />
<path d="M14 9l2 2l-2 2" />
</svg>

After

Width:  |  Height:  |  Size: 433 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

+7
View File
@@ -0,0 +1,7 @@
#tianai-captcha-parent{box-shadow:0 0 11px 0 #999;width:318px;height:318px;overflow:hidden;position:relative;z-index:997;box-sizing:border-box;border-radius:5px;padding:8px}#tianai-captcha-parent #tianai-captcha-box{height:260px;width:100%;position:relative;overflow:hidden}#tianai-captcha-parent #tianai-captcha-box .loading{width:120px;height:20px;-webkit-mask:linear-gradient(90deg, #000 70%, rgba(0, 0, 0, 0) 0) 0/20%;background:linear-gradient(#f7b645 0 0) 0/0% no-repeat rgba(221,221,221,.4196078431);animation:cartoon 1s infinite steps(6);margin:120px auto}@keyframes cartoon{100%{background-size:120%}}#tianai-captcha-parent #tianai-captcha-box #tianai-captcha{transform-style:preserve-3d;will-change:transform;transition-duration:.45s;transform:translateX(-300px)}#tianai-captcha-parent #tianai-captcha-bg-img{background-color:#fff;background-position:top;background-size:cover;z-index:-1;width:100%;height:100%;top:0;left:0;position:absolute;border-radius:6px}#tianai-captcha-parent .slider-bottom{height:19px;width:100%}#tianai-captcha-parent .slider-bottom .close-btn{width:20px;height:20px;background-image:url(../../tac/images/icon.png);background-repeat:no-repeat;background-position:0 -14px;float:right;margin-right:2px;cursor:pointer}#tianai-captcha-parent .slider-bottom .refresh-btn{width:20px;height:20px;background-image:url(../../tac/images/icon.png);background-position:0 -167px;background-repeat:no-repeat;float:right;margin-right:10px;cursor:pointer}#tianai-captcha-parent .slider-bottom .logo{height:30px;float:left}#tianai-captcha-parent .slider-move-shadow{animation:myanimation 2s infinite;height:100%;width:5px;background-color:#fff;position:absolute;top:0;left:0;filter:opacity(0.5);box-shadow:1px 1px 1px #fff;border-radius:50%}#tianai-captcha-parent #tianai-captcha-slider-move-track-mask{border-width:1px;border-style:solid;border-color:#00f4ab;width:0;height:32px;background-color:#a9ffe5;opacity:.5;position:absolute;top:-1px;left:-1px;border-radius:5px}
#tianai-captcha{text-align:left;box-sizing:content-box;width:300px;height:260px;z-index:999}#tianai-captcha .slider-bottom .logo{height:30px}#tianai-captcha .slider-bottom{height:19px;width:100%}#tianai-captcha .content .tianai-captcha-tips{height:25px;width:100%;position:absolute;bottom:-25px;left:0;z-index:999;font-size:15px;line-height:25px;color:#fff;text-align:center;transition:bottom .3s ease-in-out}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-error{background-color:#ff5d39}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-success{background-color:#39c522}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-on{bottom:0}#tianai-captcha .content #tianai-captcha-loading{z-index:9999;background-color:#f5f5f5;text-align:center;height:100%;overflow:hidden;position:relative;display:flex;justify-content:center;align-items:center}#tianai-captcha .content #tianai-captcha-loading img{display:block;width:45px;height:45px}#tianai-captcha #tianai-captcha-slider-bg-canvas{position:absolute;left:0;top:0;width:100%;height:100%;border-radius:5px}#tianai-captcha #tianai-captcha-slider-bg-div{position:absolute;left:0;top:0;width:100%;height:100%;border-radius:5px}#tianai-captcha #tianai-captcha-slider-bg-div .tianai-captcha-slider-bg-div-slice{position:absolute}@keyframes myanimation{from{left:0}to{left:289px}}
#tianai-captcha.tianai-captcha-slider{z-index:999;position:absolute;left:0;top:0;user-select:none}#tianai-captcha.tianai-captcha-slider .content{width:100%;height:180px;position:relative;overflow:hidden}#tianai-captcha.tianai-captcha-slider .bg-img-div{width:100%;height:100%;position:absolute;transform:translate(0px, 0px)}#tianai-captcha.tianai-captcha-slider .bg-img-div img{height:100%;width:100%;border-radius:5px}#tianai-captcha.tianai-captcha-slider .slider-img-div{height:100%;position:absolute;left:0;transform:translate(0px, 0px)}#tianai-captcha.tianai-captcha-slider .slider-img-div #tianai-captcha-slider-move-img{height:100%}#tianai-captcha.tianai-captcha-slider .slider-move{height:34px;width:100%;margin:11px 0;position:relative}#tianai-captcha.tianai-captcha-slider .slider-move-track{position:relative;height:32px;line-height:32px;text-align:center;background:#f5f5f5;color:#999;transition:0s;font-size:14px;box-sizing:content-box;border:1px solid #f5f5f5;border-radius:4px}#tianai-captcha.tianai-captcha-slider .refresh-btn,#tianai-captcha.tianai-captcha-slider .close-btn{display:inline-block}#tianai-captcha.tianai-captcha-slider .slider-move{line-height:38px;font-size:14px;text-align:center;white-space:nowrap;color:#88949d;-moz-user-select:none;-webkit-user-select:none;user-select:none;filter:opacity(0.8)}#tianai-captcha.tianai-captcha-slider .slider-move .slider-move-btn{transform:translate(0px, 0px);position:absolute;top:-6px;left:0;width:63px;height:45px;background-color:#fff;background-repeat:no-repeat;background-size:contain;border-radius:5px}#tianai-captcha.tianai-captcha-slider .slider-tip{margin-bottom:5px;font-weight:bold;font-size:15px;line-height:normal;color:#000}#tianai-captcha.tianai-captcha-slider .slider-move-btn:hover{cursor:pointer}
#tianai-captcha.tianai-captcha-rotate .rotate-img-div{height:100%;text-align:center}#tianai-captcha.tianai-captcha-rotate .rotate-img-div img{height:100%;transform:rotate(0deg);display:inline-block}
#tianai-captcha.tianai-captcha-concat .tianai-captcha-slider-concat-img-div{background-size:100% 180px;position:absolute;transform:translate(0px, 0px);z-index:1;width:100%}#tianai-captcha.tianai-captcha-concat .tianai-captcha-slider-concat-bg-img{width:100%;height:100%;position:absolute;transform:translate(0px, 0px);background-size:100% 180px}
#tianai-captcha.tianai-captcha-disable{z-index:999;position:absolute;left:0;top:0}#tianai-captcha.tianai-captcha-disable .content{width:100%;height:180px;position:relative;overflow:hidden}#tianai-captcha.tianai-captcha-disable .content .bg-img-div{background-image:url(../../tac/images/dun.jpeg);width:100%;height:100%;overflow:hidden}#tianai-captcha.tianai-captcha-disable .content .bg-img-div #content-span{color:#fff;overflow:hidden;margin-top:132px;display:block;text-align:center}
#tianai-captcha.tianai-captcha-word-click{box-sizing:border-box}#tianai-captcha.tianai-captcha-word-click .click-tip{position:relative;height:40px;width:100%}#tianai-captcha.tianai-captcha-word-click .click-tip .tip-img{height:35px;position:absolute;right:15px}#tianai-captcha.tianai-captcha-word-click .click-tip #tianai-captcha-click-track-font{font-size:18px;display:inline-block;height:40px;line-height:40px;position:absolute}#tianai-captcha.tianai-captcha-word-click .slider-bottom{position:relative;top:6px}#tianai-captcha.tianai-captcha-word-click .content #bg-img-click-mask{width:100%;height:100%;position:absolute;left:0;top:0}#tianai-captcha.tianai-captcha-word-click .content #bg-img-click-mask .click-span{position:absolute;left:0;top:0;border-radius:50px;background-color:#409eff;width:20px;height:20px;text-align:center;line-height:20px;color:#fff;border:2px solid #fff;box-sizing:content-box}#tianai-captcha.tianai-captcha-word-click .click-confirm-btn{width:100%;height:35px;border-radius:4px;background-image:linear-gradient(173deg, hsl(38.09, 91%, 57.89%) 0%, hsl(38.09, 89.38%, 71.74%) 100%);font-size:15px;text-align:center;box-sizing:border-box;line-height:35px;color:#fff;margin-top:3px}#tianai-captcha.tianai-captcha-word-click .click-confirm-btn:hover{cursor:pointer}
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

File diff suppressed because one or more lines are too long
+74 -153
View File
@@ -31,179 +31,100 @@ function Assert-NotContains {
$root = Split-Path -Parent $PSScriptRoot $root = Split-Path -Parent $PSScriptRoot
$entryPath = Join-Path $root 'pages/auth/a01-entry.vue' $entryPath = Join-Path $root 'pages/auth/a01-entry.vue'
$loginPath = Join-Path $root 'pages/auth/a02-login.vue' $legacyLoginPath = Join-Path $root 'pages/auth/a02-login.vue'
$pagesPath = Join-Path $root 'pages.json' $pagesPath = Join-Path $root 'pages.json'
if (-not (Test-Path -LiteralPath $entryPath)) { if (-not (Test-Path -LiteralPath $entryPath)) {
throw 'Missing A-01 entry page: pages/auth/a01-entry.vue' throw 'Missing the single A01 login page.'
}
if (Test-Path -LiteralPath $legacyLoginPath) {
throw 'A02 was merged into A01, so the legacy page file must be removed.'
} }
$pages = Get-Content -LiteralPath $pagesPath -Raw -Encoding utf8 | ConvertFrom-Json $pages = Get-Content -LiteralPath $pagesPath -Raw -Encoding utf8 | ConvertFrom-Json
if ($pages.pages[0].path -ne 'pages/auth/a01-entry') { if ($pages.pages[0].path -ne 'pages/auth/a01-entry') {
throw 'A-01 entry page must be the first route in pages.json' throw 'A01 must remain the first application route.'
}
if ($pages.pages.path -contains 'pages/auth/a02-login') {
throw 'pages.json must not retain the legacy A02 route.'
}
if ($pages.pages.Count -ne 53) {
throw "Expected 53 final routes after the A02 merge, found $($pages.pages.Count)."
} }
$entry = Get-Content -LiteralPath $entryPath -Raw -Encoding utf8 $entry = Get-Content -LiteralPath $entryPath -Raw -Encoding utf8
$login = Get-Content -LiteralPath $loginPath -Raw -Encoding utf8
$accountPasswordLogin = ConvertFrom-Utf8Base64 '6LSm5Y+35a+G56CB55m75b2V' # Final decorative surfaces must come from real bitmap assets.
$wechatLogin = ConvertFrom-Utf8Base64 '5b6u5L+h55m75b2V' foreach ($asset in @(
$registerPrompt = ConvertFrom-Utf8Base64 '6L+Y5rKh5pyJ6LSm5Y+377yf' 'a01-login-header-v1.png',
$registerEntry = ConvertFrom-Utf8Base64 '5rOo5YaM6LSm5Y+3' 'a01-login-header-exact-v2.png',
$loginEntry = ConvertFrom-Utf8Base64 '55m75b2V' 'auth-ink-scenery.png',
$dividerGlyph = ConvertFrom-Utf8Base64 '4oyY' 'a01-login-scroll-frame-v1.png',
$agreementRequired = ConvertFrom-Utf8Base64 '6K+35YWI6ZiF6K+75bm25ZCM5oSP55u45YWz5Y2P6K6u' 'a01-title-divider-v2.png',
$wechatPending = ConvertFrom-Utf8Base64 '5b6u5L+h55m75b2V5Yqf6IO95YeG5aSH5Lit'
$registerPending = ConvertFrom-Utf8Base64 '5rOo5YaM6aG16Z2i5YeG5aSH5Lit'
$agreementPending = ConvertFrom-Utf8Base64 '5Y2P6K6u6aG16Z2i5YeG5aSH5Lit'
$smsLogin = ConvertFrom-Utf8Base64 '5omL5py66aqM6K+B56CB55m75b2V'
$forgotPassword = ConvertFrom-Utf8Base64 '5b+Y6K6w5a+G56CB'
$getCode = ConvertFrom-Utf8Base64 '6I635Y+W6aqM6K+B56CB'
$loginPending = ConvertFrom-Utf8Base64 '55m75b2V5o6l5Y+j5b6F5o6l5YWl'
$forgotPasswordPending = ConvertFrom-Utf8Base64 '5b+Y6K6w5a+G56CB6aG16Z2i5YeG5aSH5Lit'
$getCodePending = ConvertFrom-Utf8Base64 '6I635Y+W6aqM6K+B56CB5Yqf6IO95b6F5o6l5YWl'
$implicitAgreement = ConvertFrom-Utf8Base64 '55m75b2V5Y2z6KGo56S65ZCM5oSP'
$agreementConfirm = ConvertFrom-Utf8Base64 '5oiR5bey6ZiF6K+75bm25ZCM5oSP'
$agreementGuard = ConvertFrom-Utf8Base64 '6K+35YWI6ZiF6K+75bm25ZCM5oSP55u45YWz5Y2P6K6u'
$legacyAgreementReminder = ConvertFrom-Utf8Base64 '6K+35YWI5Zyo55m75b2V5byV5a+86aG16Z2i5YeG5aSH5Lit6ZiF6K+75bm25ZCM5oSP5Y2P6K6u'
foreach ($requiredEntryCopy in @($wechatLogin, $registerPrompt, $registerEntry, $agreementRequired, $wechatPending, $registerPending, $agreementPending)) {
Assert-Contains -Content $entry -Expected $requiredEntryCopy -Message "Missing A-01 copy: $requiredEntryCopy"
}
Assert-Contains -Content $entry -Expected "url: '/pages/auth/a02-login?agreed=1'" -Message 'A-01 account login must navigate with agreed=1'
Assert-Contains -Content $entry -Expected "<text>$loginEntry</text>" -Message 'A-01 primary entry copy must be 登录'
foreach ($requiredClass in @('auth-header', 'brand-intro', 'entry-actions', 'agreement-row')) {
Assert-Contains -Content $entry -Expected $requiredClass -Message "Missing A-01 page structure: $requiredClass"
}
foreach ($requiredVisualAnchor in @(
'brand-divider--top',
'brand-divider--bottom',
'entry-button--primary',
'entry-button--secondary',
'height: 253rpx',
'padding: 150rpx 56rpx 0',
'width: 426rpx',
'margin: 95rpx 110rpx 0',
'height: 118rpx',
'margin-top: 51rpx',
'margin-top: 98rpx',
'margin-top: 141rpx',
'top: 70rpx',
'width: 64rpx',
'entry-button__content',
'translateX(-20rpx)',
'auth-divider-knot.png',
'auth-title-cloud.png',
'a01-primary-button.png', 'a01-primary-button.png',
'a01-secondary-button.png', 'a01-secondary-button.png',
'entry-button__skin', 'auth-wechat.png',
'align-items: center', 'a01-icon-phone-v1.png',
'white-space: nowrap', 'a01-icon-lock-v1.png',
'font-size: 20rpx', 'a01-icon-eye-open-v1.png',
'width: 30rpx', 'a01-icon-eye-closed-pupil-v2.png',
'height: 30rpx' 'a01-icon-sms-code-v2.png',
)) {
Assert-Contains -Content $entry -Expected $requiredVisualAnchor -Message "Missing A-01 calibrated visual anchor: $requiredVisualAnchor"
}
foreach ($rejectedVisualConstruction in @('link-line', 'button-corner--legacy', 'class="auth-button primary-button"', 'auth-button-corner.png', 'button-corner-asset', '.entry-button::after')) {
Assert-NotContains -Content $entry -Unexpected $rejectedVisualConstruction -Message "A-01 retains rejected visual construction: $rejectedVisualConstruction"
}
foreach ($forbiddenClass in @('entryGuideVisual', 'guide-art', 'entry-hit', 'width: 750rpx', 'min-height: 1625rpx')) {
Assert-NotContains -Content $entry -Unexpected $forbiddenClass -Message "A-01 must not render the whole design image with $forbiddenClass"
}
Assert-NotContains -Content $entry -Unexpected '/static/assets/icons/action/create-cloud-v2.png' -Message 'A-01 must not reuse the mismatched create-cloud asset for its title or divider ornaments'
Assert-Contains -Content $entry -Expected 'filter: brightness(0) invert(1)' -Message 'A-01 primary login icon must render in white'
Assert-NotContains -Content $entry -Unexpected $dividerGlyph -Message 'A-01 divider must not use a text glyph as ornament'
if (([regex]::Matches($entry, 'class="entry-button__skin"')).Count -ne 2) {
throw 'A-01 must render one complete opaque skin image for each entry button'
}
if (([regex]::Matches($entry, 'auth-divider-knot\.png')).Count -ne 2) {
throw 'A-01 must render the dedicated divider-knot asset above and below the subtitle'
}
if ($entry -match '(?s)\.entry-button\s*\{[^}]*\bborder\s*:') {
throw 'A-01 entry buttons must not draw their final border with CSS'
}
if ($entry -match '(?s)\.entry-button--(?:primary|secondary)\s*\{[^}]*\bbackground\s*:') {
throw 'A-01 entry buttons must not draw their final surface with CSS'
}
if ($entry -notmatch '(?s)\.agreement-row\s*\{[^}]*align-items:\s*center') {
throw 'A-01 agreement checkbox and copy must align on one horizontal center line'
}
if ($entry -notmatch '(?s)\.agreement-copy\s*\{[^}]*white-space:\s*nowrap') {
throw 'A-01 agreement copy must remain on a single line'
}
if ($entry -match '(?s)\.auth-page\s*\{[^}]*overflow\s*:\s*hidden') {
throw 'A-01 must allow vertical scrolling on small screens'
}
foreach ($requiredLoginCopy in @($accountPasswordLogin, $smsLogin, $forgotPassword, $getCode, $loginPending, $agreementConfirm, $agreementGuard, $getCodePending)) {
Assert-Contains -Content $login -Expected $requiredLoginCopy -Message "Missing A-02 copy: $requiredLoginCopy"
}
Assert-Contains -Content $login -Expected "url: '/pages/auth/a05-reset-password'" -Message 'A-02 forgot-password entry must navigate to A-05'
foreach ($requiredLoginVisualAnchor in @(
'auth-title-cloud.png',
'login-title-cloud',
'a02-login-panel.png',
'login-panel__skin',
'a01-primary-button.png',
'login-submit__skin',
'login-submit__content',
'agreement-row',
'agreement-icon',
'a02-agreement-unchecked.png', 'a02-agreement-unchecked.png',
'a02-agreement-checked.png' 'a02-agreement-checked.png'
)) { )) {
Assert-Contains -Content $login -Expected $requiredLoginVisualAnchor -Message "Missing A-02 calibrated visual anchor: $requiredLoginVisualAnchor" Assert-Contains -Content $entry -Expected $asset -Message "A01 is missing asset reference: $asset"
} }
if (([regex]::Matches($login, 'class="login-submit__skin"')).Count -ne 1) { foreach ($obsoleteHeaderAsset in @('auth-header.png', 'brand-seal.png', 'a01-paper-transition-v1.png')) {
throw 'A-02 must render one complete opaque skin image for its login button' Assert-NotContains -Content $entry -Unexpected $obsoleteHeaderAsset -Message "A01 must use the selected design's single exact header bitmap instead of composing $obsoleteHeaderAsset."
}
if (([regex]::Matches($login, 'class="login-panel__skin"')).Count -ne 1) {
throw 'A-02 must render one complete opaque skin image for its login panel'
}
if ($login -match '(?s)\.login-panel\s*\{[^}]*\bborder\s*:') {
throw 'A-02 login panel must not draw its final frame with CSS'
}
if ($login -match '(?s)\.login-panel\s*\{[^}]*\bbackground\s*:') {
throw 'A-02 login panel must not draw its final paper surface with CSS'
}
if ($login -match '(?s)\.login-submit\s*\{[^}]*\bborder\s*:') {
throw 'A-02 login button must not draw its final border with CSS'
}
if ($login -match '(?s)\.login-submit\s*\{[^}]*\bbackground\s*:') {
throw 'A-02 login button must not draw its final surface with CSS'
}
if ($login -notmatch '<view class="agreement-row" @click="toggleAgreement">') {
throw 'A-02 must provide one directly clickable agreement row'
}
if (([regex]::Matches($login, '@click\.stop="prepareAgreement"')).Count -ne 2) {
throw 'A-02 agreement row must retain two non-toggling agreement links'
}
foreach ($requiredAgreementState in @('const agreed = ref(false)', 'const isAgreedRoute = () =>', "if (typeof location !== 'undefined') {", "new URLSearchParams(location.hash.split('?')[1] || '')", 'const pages = getCurrentPages()', 'const currentPage = pages[pages.length - 1]', "return currentPage?.options?.agreed === '1'", 'const syncAgreementFromRoute = () =>', 'agreed.value = isAgreedRoute()', 'onShow(() => {', 'syncAgreementFromRoute()', 'const toggleAgreement = () =>', 'if (agreed.value) return true')) {
Assert-Contains -Content $login -Expected $requiredAgreementState -Message "Missing A-02 agreement state: $requiredAgreementState"
}
Assert-NotContains -Content $login -Unexpected 'onLoad((options) =>' -Message 'A-02 must not leave agreement state initialized only on first load'
Assert-NotContains -Content $login -Unexpected $legacyAgreementReminder -Message 'A-02 must not retain its passive return-to-entry agreement reminder'
Assert-NotContains -Content $login -Unexpected 'agreedFromEntry' -Message 'A-02 must use its local agreed state as the submit gate'
if ($login -match '(?s)\.agreement-icon\s*\{[^}]*\bborder\s*:') {
throw 'A-02 agreement icon must not draw its final circle with CSS'
}
if ($login -match '(?s)\.agreement-icon\s*\{[^}]*\bbackground\s*:') {
throw 'A-02 agreement icon must not draw its final state with CSS'
}
if ($login -match '\.agreement-icon::(?:before|after)') {
throw 'A-02 agreement icon must not draw its final state with a pseudo element'
} }
if ($login -match '(?s)\.auth-page\s*\{[^}]*overflow\s*:\s*hidden') { $requiredCopy = @(
throw 'A-02 must allow vertical scrolling on small screens' '55m75b2V5a626LCx',
'5a+G56CB55m75b2V',
'6aqM6K+B56CB55m75b2V',
'5omL5py65Y+3',
'5a+G56CB',
'6aqM6K+B56CB',
'5b+Y6K6w5a+G56CB',
'6I635Y+W6aqM6K+B56CB',
'5b6u5L+h55m75b2V',
'6L+Y5rKh5pyJ6LSm5Y+377yf',
'5rOo5YaM6LSm5Y+3',
'44CK55So5oi35Y2P6K6u44CL',
'44CK6ZqQ56eB5pS/562W44CL'
) | ForEach-Object { ConvertFrom-Utf8Base64 $_ }
foreach ($copy in $requiredCopy) {
Assert-Contains -Content $entry -Expected $copy -Message "A01 is missing required visible copy: $copy"
} }
foreach ($forbidden in @('appApi', 'calcMD5', 'loginWithPassword', 'uni.reLaunch', $implicitAgreement)) { foreach ($contract in @(
Assert-NotContains -Content $entry -Unexpected $forbidden -Message "A-01 must not contain legacy auth behavior: $forbidden" "const activeLoginMethod = ref('password')",
Assert-NotContains -Content $login -Unexpected $forbidden -Message "A-02 must not contain legacy auth behavior: $forbidden" 'const passwordVisible = ref(false)',
"const LOGIN_METHOD_STORAGE_KEY = 'a01:last-login-method'",
'uni.getStorageSync(LOGIN_METHOD_STORAGE_KEY)',
'uni.setStorageSync(LOGIN_METHOD_STORAGE_KEY, method)',
'const switchLoginMethod = (method) =>',
'const togglePasswordVisibility = () =>',
"url: '/pages/auth/a04-register'",
"url: '/pages/auth/a05-reset-password'",
'class="feedback-toast"',
'class="verification-layer"',
'class="login-submit"'
)) {
Assert-Contains -Content $entry -Expected $contract -Message "A01 is missing state or interaction contract: $contract"
} }
Write-Output 'A01-A02-UI-CONTRACT PASS' foreach ($nativeUi in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet')) {
Assert-NotContains -Content $entry -Unexpected $nativeUi -Message "A01 must not use native UniApp feedback: $nativeUi"
}
Assert-NotContains -Content $entry -Unexpected "url: '/pages/auth/a02-login" -Message 'A01 must not navigate to A02.'
Assert-NotContains -Content $entry -Unexpected 'brand-intro' -Message 'A01 must remove the legacy oversized brand introduction.'
Assert-NotContains -Content $entry -Unexpected 'min-height: 100vh' -Message 'A01 must not depend on 100vh for its page shell.'
Assert-NotContains -Content $entry -Unexpected 'exact-chrome-overlay' -Message 'A01 must not stack a second full-screen scroll perimeter over the complete scroll frame.'
Assert-NotContains -Content $entry -Unexpected 'auth-design-scenery-v1.png' -Message 'A01 must not use the scenery bitmap that contains stray scroll handles.'
Assert-NotContains -Content $entry -Unexpected 'a01-icon-eye-closed-v1.png' -Message 'A01 hidden-password icon must retain the center pupil instead of using the hollow crossed eye.'
Assert-NotContains -Content $entry -Unexpected 'a01-icon-verification-v1.png' -Message 'A01 SMS field must use a message-code icon instead of a success-state checkmark.'
Assert-NotContains -Content $entry -Unexpected 'a01-icon-sms-code-v1.svg' -Message 'A01 SMS field must use the custom generated icon instead of the temporary third-party SVG.'
Assert-NotContains -Content $entry -Unexpected '@media (max-height:' -Message 'A01 must preserve the design ratio on short screens and scroll instead of compressing with pixel breakpoints.'
Write-Output 'A01-LOGIN-MERGE-CONTRACT PASS'
-48
View File
@@ -1,48 +0,0 @@
$ErrorActionPreference = 'Stop'
function ConvertFrom-Utf8Base64 {
param([string]$Value)
return [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Value))
}
function Assert-Contains {
param(
[string]$Content,
[string]$Expected,
[string]$Message
)
if ($Content -notmatch [regex]::Escape($Expected)) {
throw $Message
}
}
$root = Split-Path -Parent $PSScriptRoot
$loginPath = Join-Path $root 'pages/auth/a02-login.vue'
$pagesPath = Join-Path $root 'pages.json'
$login = Get-Content -LiteralPath $loginPath -Raw -Encoding utf8
$pages = Get-Content -LiteralPath $pagesPath -Raw -Encoding utf8 | ConvertFrom-Json
$registerPrompt = ConvertFrom-Utf8Base64 '6L+Y5rKh5pyJ6LSm5Y+377yf'
$registerEntry = ConvertFrom-Utf8Base64 '5rOo5YaM6LSm5Y+3'
Assert-Contains -Content $login -Expected 'class="register-entry"' -Message 'A-02 must expose a dedicated registration entry below its agreement row'
Assert-Contains -Content $login -Expected $registerPrompt -Message 'A-02 must explain the registration path to new users'
Assert-Contains -Content $login -Expected 'class="register-link" hover-class="text-hover" @click="prepareRegister"' -Message 'A-02 registration copy must provide a focused tap target'
Assert-Contains -Content $login -Expected (">$registerEntry</view>") -Message 'A-02 registration action must be visible'
Assert-Contains -Content $login -Expected "const prepareRegister = () => uni.navigateTo({ url: '/pages/auth/a04-register' })" -Message 'A-02 registration entry must navigate to A-04'
$agreementIndex = $login.IndexOf('class="agreement-row"')
$registerIndex = $login.IndexOf('class="register-entry"')
if ($registerIndex -le $agreementIndex) {
throw 'A-02 registration entry must follow its agreement row in reading order'
}
if ($login -notmatch '(?s)\.register-link\s*\{[^}]*min-height:\s*88rpx') {
throw 'A-02 registration entry must retain a touch-friendly height'
}
if (-not ($pages.pages.path -contains 'pages/auth/a04-register')) {
throw 'A-02 registration entry target must remain declared in pages.json'
}
Write-Output 'A02-REGISTER-ENTRY-CONTRACT PASS'
+32
View File
@@ -0,0 +1,32 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$pages = Get-Content -LiteralPath (Join-Path $root 'pages.json') -Raw -Encoding utf8 | ConvertFrom-Json
$legacyPage = Join-Path $root 'pages/auth/a02-login.vue'
if ($pages.pages.Count -ne 53) { throw "Expected 53 routes, found $($pages.pages.Count)." }
if ($pages.pages.path -contains 'pages/auth/a02-login') { throw 'Legacy A02 route still exists.' }
if (Test-Path -LiteralPath $legacyPage) { throw 'Legacy A02 page file still exists.' }
$ownedFiles = @(
'pages/auth/a01-entry.vue',
'pages/auth/a04-register.vue',
'pages/auth/a05-reset-password.vue',
'pages/auth/a06-auth-status.vue',
'scripts/capture-chrome-page.js'
)
foreach ($relativePath in $ownedFiles) {
$content = Get-Content -LiteralPath (Join-Path $root $relativePath) -Raw -Encoding utf8
if ($content -match '/pages/auth/a02-login') {
throw "Legacy A02 navigation remains in $relativePath."
}
}
foreach ($returnPage in @('pages/auth/a04-register.vue', 'pages/auth/a05-reset-password.vue', 'pages/auth/a06-auth-status.vue')) {
$content = Get-Content -LiteralPath (Join-Path $root $returnPage) -Raw -Encoding utf8
if ($content -notmatch [regex]::Escape("url: '/pages/auth/a01-entry'")) {
throw "$returnPage must return to A01."
}
}
Write-Output 'A02-ROUTE-REMOVAL-CONTRACT PASS'
+1 -1
View File
@@ -8,6 +8,6 @@ $a03File = Join-Path $root 'pages/auth/a03-mobile-verify.vue'
if ($pages.pages.path -contains 'pages/auth/a03-mobile-verify') { throw 'A-03 must not remain in pages.json' } if ($pages.pages.path -contains 'pages/auth/a03-mobile-verify') { throw 'A-03 must not remain in pages.json' }
if (Test-Path -LiteralPath $a03File) { throw 'A-03 page file must be deleted' } if (Test-Path -LiteralPath $a03File) { throw 'A-03 page file must be deleted' }
if ($catalog -match '(?m)^\s*a03\s*:') { throw 'A-03 catalog entry must be deleted' } if ($catalog -match '(?m)^\s*a03\s*:') { throw 'A-03 catalog entry must be deleted' }
if ($pages.pages.Count -ne 54) { throw "Expected 54 final routes after the approved G02-to-G01, G04-to-G03, G07-to-G06 and T02-to-T01 state merges, found $($pages.pages.Count)." } if ($pages.pages.Count -ne 53) { throw "Expected 53 final routes after the approved page-state merges, found $($pages.pages.Count)." }
Write-Output 'A03-ROUTE-REMOVAL-CONTRACT PASS' Write-Output 'A03-ROUTE-REMOVAL-CONTRACT PASS'
+1 -1
View File
@@ -38,7 +38,7 @@ foreach ($required in @(
'const agreed = ref(false)', 'const agreed = ref(false)',
'const toggleAgreement = () =>', 'const toggleAgreement = () =>',
'const submitRegister = () =>', 'const submitRegister = () =>',
"const prepareLogin = () => uni.navigateTo({ url: '/pages/auth/a02-login' })", "const prepareLogin = () => uni.navigateTo({ url: '/pages/auth/a01-entry' })",
'if (!/^1\d{10}$/.test(phone.value))', 'if (!/^1\d{10}$/.test(phone.value))',
'if (!password.value)', 'if (!password.value)',
'if (password.value !== confirmPassword.value)' 'if (password.value !== confirmPassword.value)'
+2 -2
View File
@@ -40,7 +40,7 @@ foreach ($required in @(
'v-model="confirmPassword"', 'v-model="confirmPassword"',
'const prepareGetCode = () =>', 'const prepareGetCode = () =>',
'const submitReset = () =>', 'const submitReset = () =>',
"const prepareLogin = () => uni.navigateTo({ url: '/pages/auth/a02-login' })", "const prepareLogin = () => uni.navigateTo({ url: '/pages/auth/a01-entry' })",
'if (!/^1\d{10}$/.test(phone.value))', 'if (!/^1\d{10}$/.test(phone.value))',
'if (!/^\d{6}$/.test(verificationCode.value))', 'if (!/^\d{6}$/.test(verificationCode.value))',
'if (!password.value)', 'if (!password.value)',
@@ -51,7 +51,7 @@ foreach ($required in @(
Assert-Contains -Content $page -Expected $resetCopy -Message 'A-05 must visibly identify the password-reset task' Assert-Contains -Content $page -Expected $resetCopy -Message 'A-05 must visibly identify the password-reset task'
Assert-Contains -Content $page -Expected $confirmPasswordCopy -Message 'A-05 must require password confirmation' Assert-Contains -Content $page -Expected $confirmPasswordCopy -Message 'A-05 must require password confirmation'
Assert-Contains -Content $page -Expected $getCodeCopy -Message 'A-05 must expose the verification-code action' Assert-Contains -Content $page -Expected $getCodeCopy -Message 'A-05 must expose the verification-code action'
Assert-Contains -Content $page -Expected $loginCopy -Message 'A-05 must provide the A02 return path' Assert-Contains -Content $page -Expected $loginCopy -Message 'A-05 must provide the A01 return path'
Assert-NotContains -Content $catalog -Unexpected 'a05:' -Message 'A-05 must not remain in the temporary page catalog' Assert-NotContains -Content $catalog -Unexpected 'a05:' -Message 'A-05 must not remain in the temporary page catalog'
foreach ($forbidden in @( foreach ($forbidden in @(
+1 -1
View File
@@ -16,7 +16,7 @@ $pages = Get-Content -LiteralPath (Join-Path $root 'pages.json') -Raw -Encoding
$catalog = Get-Content -LiteralPath (Join-Path $root 'data/page-catalog.js') -Raw -Encoding utf8 $catalog = Get-Content -LiteralPath (Join-Path $root 'data/page-catalog.js') -Raw -Encoding utf8
if (-not ($pages.pages.path -contains 'pages/auth/a06-auth-status')) { throw 'A-06 route must remain declared in pages.json' } if (-not ($pages.pages.path -contains 'pages/auth/a06-auth-status')) { throw 'A-06 route must remain declared in pages.json' }
if (-not ($pages.pages.path -contains 'pages/auth/a02-login')) { throw 'A-06 must retain the A02 return target' } if (-not ($pages.pages.path -contains 'pages/auth/a01-entry')) { throw 'A-06 must retain the A01 return target' }
if (-not ($pages.pages.path -contains 'pages/auth/a04-register')) { throw 'A-06 must retain the A04 registration target' } if (-not ($pages.pages.path -contains 'pages/auth/a04-register')) { throw 'A-06 must retain the A04 registration target' }
Assert-NotContains -Content $page -Unexpected 'ModulePage' -Message 'A-06 must not remain a ModulePage shell' Assert-NotContains -Content $page -Unexpected 'ModulePage' -Message 'A-06 must not remain a ModulePage shell'
+1 -1
View File
@@ -76,7 +76,7 @@ const run = async () => {
await navigate(send, baseUrl, expected.normal) await navigate(send, baseUrl, expected.normal)
await valueOf(send, "document.querySelector('.status-primary')?.click()") await valueOf(send, "document.querySelector('.status-primary')?.click()")
await waitFor(send, "location.href.includes('/pages/auth/a02-login')", 'Default A06 action did not return to A02') await waitFor(send, "location.href.includes('/pages/auth/a01-entry')", 'Default A06 action did not return to A01')
await navigate(send, `${baseUrl}?status=restricted`, expected.restricted) await navigate(send, `${baseUrl}?status=restricted`, expected.restricted)
await valueOf(send, "document.querySelector('.status-primary')?.click()") await valueOf(send, "document.querySelector('.status-primary')?.click()")
+3
View File
@@ -62,4 +62,7 @@ if ($captureScript -notmatch "item\.url\.startsWith\('http://localhost:5173'\)")
if ($captureScript -match "item\.url\.includes\('localhost:5173'\)") { if ($captureScript -match "item\.url\.includes\('localhost:5173'\)") {
throw 'Chrome capture must not use an ambiguous localhost substring when selecting its CDP target' throw 'Chrome capture must not use an ambiguous localhost substring when selecting its CDP target'
} }
if ($captureScript -notmatch "process\.env\.CHROME_DEBUGGING_PORT \|\| '9222'") {
throw 'Chrome capture must preserve port 9222 by default and allow an isolated QA port.'
}
Write-Output 'CAPTURE-CHROME-PAGE-CONTRACT PASS' Write-Output 'CAPTURE-CHROME-PAGE-CONTRACT PASS'
-1
View File
@@ -38,7 +38,6 @@ function Test-AssetDirectory {
# P-00 final routes: each file name identifies the page and its design ID. # P-00 final routes: each file name identifies the page and its design ID.
Test-ExpectedPage -ExpectedPath 'pages/auth/a01-entry' -PageId 'A-01' Test-ExpectedPage -ExpectedPath 'pages/auth/a01-entry' -PageId 'A-01'
Test-ExpectedPage -ExpectedPath 'pages/auth/a02-login' -PageId 'A-02'
Test-ExpectedPage -ExpectedPath 'pages/genealogy/g01-my-genealogies' -PageId 'G-01' Test-ExpectedPage -ExpectedPath 'pages/genealogy/g01-my-genealogies' -PageId 'G-01'
Test-ExpectedPage -ExpectedPath 'pages/genealogy/g03-create-genealogy' -PageId 'G-03' Test-ExpectedPage -ExpectedPath 'pages/genealogy/g03-create-genealogy' -PageId 'G-03'
Test-ExpectedPage -ExpectedPath 'pages/genealogy/g05-genealogy-overview' -PageId 'G-05' Test-ExpectedPage -ExpectedPath 'pages/genealogy/g05-genealogy-overview' -PageId 'G-05'
+4 -4
View File
@@ -9,7 +9,7 @@ $root = Split-Path -Parent $PSScriptRoot
$pagesPath = Join-Path $root 'pages.json' $pagesPath = Join-Path $root 'pages.json'
$pageHeader = ConvertFrom-Utf8Base64 '6aG16Z2i57yW5Y+377ya' $pageHeader = ConvertFrom-Utf8Base64 '6aG16Z2i57yW5Y+377ya'
$expectedPages = @( $expectedPages = @(
'pages/auth/a01-entry', 'pages/auth/a02-login', 'pages/auth/a04-register', 'pages/auth/a05-reset-password', 'pages/auth/a06-auth-status', 'pages/auth/a01-entry', 'pages/auth/a04-register', 'pages/auth/a05-reset-password', 'pages/auth/a06-auth-status',
'pages/genealogy/g01-my-genealogies', 'pages/genealogy/g03-create-genealogy', 'pages/genealogy/g05-genealogy-overview', 'pages/genealogy/g06-search-genealogies', 'pages/genealogy/g08-join-application', 'pages/genealogy/g09-my-applications', 'pages/genealogy/g10-application-review', 'pages/genealogy/g11-genealogy-settings', 'pages/genealogy/g12-generation-poems', 'pages/genealogy/g01-my-genealogies', 'pages/genealogy/g03-create-genealogy', 'pages/genealogy/g05-genealogy-overview', 'pages/genealogy/g06-search-genealogies', 'pages/genealogy/g08-join-application', 'pages/genealogy/g09-my-applications', 'pages/genealogy/g10-application-review', 'pages/genealogy/g11-genealogy-settings', 'pages/genealogy/g12-generation-poems',
'pages/tree/t01-tree-overview', 'pages/tree/t03-member-profile', 'pages/tree/t04-add-relative', 'pages/tree/t05-edit-member', 'pages/tree/t06-edit-relationship', 'pages/tree/t07-member-directory', 'pages/tree/t08-member-states', 'pages/tree/t01-tree-overview', 'pages/tree/t03-member-profile', 'pages/tree/t04-add-relative', 'pages/tree/t05-edit-member', 'pages/tree/t06-edit-relationship', 'pages/tree/t07-member-directory', 'pages/tree/t08-member-states',
'pages/family/f01-family-feed', 'pages/family/f02-publish-feed', 'pages/family/f03-feed-detail', 'pages/family/f04-article-list', 'pages/family/f05-article-detail', 'pages/family/f06-article-editor', 'pages/family/f07-album-list', 'pages/family/f08-album-detail', 'pages/family/f09-media-upload', 'pages/family/f10-video-list', 'pages/family/f01-family-feed', 'pages/family/f02-publish-feed', 'pages/family/f03-feed-detail', 'pages/family/f04-article-list', 'pages/family/f05-article-detail', 'pages/family/f06-article-editor', 'pages/family/f07-album-list', 'pages/family/f08-album-detail', 'pages/family/f09-media-upload', 'pages/family/f10-video-list',
@@ -20,11 +20,11 @@ $expectedPages = @(
$pages = Get-Content -LiteralPath $pagesPath -Raw -Encoding utf8 | ConvertFrom-Json $pages = Get-Content -LiteralPath $pagesPath -Raw -Encoding utf8 | ConvertFrom-Json
$actualPaths = @($pages.pages.path) $actualPaths = @($pages.pages.path)
if ($actualPaths.Count -ne 54) { if ($actualPaths.Count -ne 53) {
throw "Expected 54 visual routes after the approved G02-to-G01, G04-to-G03, G07-to-G06 and T02-to-T01 state merges, found $($actualPaths.Count)." throw "Expected 53 visual routes after the approved page-state merges, found $($actualPaths.Count)."
} }
if (Compare-Object -ReferenceObject $expectedPages -DifferenceObject $actualPaths) { if (Compare-Object -ReferenceObject $expectedPages -DifferenceObject $actualPaths) {
throw 'pages.json routes do not match the 54-route visual delivery contract.' throw 'pages.json routes do not match the 53-route visual delivery contract.'
} }
foreach ($page in $expectedPages) { foreach ($page in $expectedPages) {
-2
View File
@@ -14,7 +14,6 @@ $requiredFiles = @(
'components/AppTabbar.vue', 'components/AppTabbar.vue',
'components/PageHeader.vue', 'components/PageHeader.vue',
'pages/auth/a01-entry.vue', 'pages/auth/a01-entry.vue',
'pages/auth/a02-login.vue',
'pages/genealogy/g01-my-genealogies.vue', 'pages/genealogy/g01-my-genealogies.vue',
'pages/genealogy/g03-create-genealogy.vue', 'pages/genealogy/g03-create-genealogy.vue',
'pages/genealogy/g05-genealogy-overview.vue', 'pages/genealogy/g05-genealogy-overview.vue',
@@ -40,7 +39,6 @@ $pages = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages.json') | Conver
$paths = @($pages.pages | ForEach-Object { $_.path }) $paths = @($pages.pages | ForEach-Object { $_.path })
$requiredRoutes = @( $requiredRoutes = @(
'pages/auth/a01-entry', 'pages/auth/a01-entry',
'pages/auth/a02-login',
'pages/genealogy/g01-my-genealogies', 'pages/genealogy/g01-my-genealogies',
'pages/genealogy/g03-create-genealogy', 'pages/genealogy/g03-create-genealogy',
'pages/genealogy/g05-genealogy-overview', 'pages/genealogy/g05-genealogy-overview',