完成全项目响应式审核与换机交接

This commit is contained in:
2026-07-22 07:41:27 +08:00
parent a127280ce6
commit eced3d1e6e
77 changed files with 2052 additions and 378 deletions
+1
View File
@@ -7,5 +7,6 @@ export default {
</script>
<style lang="scss">
// Global styles own the fixed-header host stacking contract.
@import './styles/global.scss';
</style>
+7 -2
View File
@@ -93,18 +93,23 @@ const cancel = () => {
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 40rpx;
padding: calc(40rpx + env(safe-area-inset-top)) 40rpx calc(40rpx + env(safe-area-inset-bottom));
background: rgba(35, 18, 10, 0.62);
}
.app-dialog {
display: flex;
width: 650rpx;
max-width: 100%;
max-height: calc(var(--app-viewport-height, 100vh) - 80rpx - env(safe-area-inset-top) - env(safe-area-inset-bottom));
flex-direction: column;
overflow: hidden;
@include adaptive.adaptive-auth-dialog;
}
.app-dialog__content {
z-index: 1;
display: flex;
max-height: calc(var(--app-viewport-height, 100vh) - 160rpx);
min-height: 0;
flex: 1;
flex-direction: column;
align-items: center;
box-sizing: border-box;
+8 -1
View File
@@ -1,10 +1,14 @@
<!-- 公共组件根页面底部导航仅维护家谱家族我的三项已确认入口及其透明图标 -->
<template>
<view class="app-tabbar">
<view class="app-tabbar" role="tablist" aria-label="主要导航">
<view
v-for="item in items"
:key="item.key"
class="tab-item"
role="tab"
:aria-selected="active === item.key"
:aria-label="`${item.label}${active === item.key ? '当前页面' : ''}`"
hover-class="tab-item--pressed"
@click="switchTab(item)"
>
<image
@@ -77,6 +81,9 @@ const switchTab = (item) => {
padding: 4rpx 0;
box-sizing: border-box;
}
.tab-item--pressed {
opacity: 0.72;
}
.tab-icon {
width: 64rpx;
+14 -1
View File
@@ -3,6 +3,9 @@
<view
class="genealogy-card"
:class="{ 'genealogy-card--current': selected }"
role="button"
:aria-label="accessibleLabel"
hover-class="genealogy-card--pressed"
@click="$emit('select', genealogy)"
>
<image
@@ -61,12 +64,19 @@
</template>
<script setup>
defineProps({
import { computed } from "vue";
const props = defineProps({
genealogy: { type: Object, required: true },
role: { type: String, required: true },
selected: { type: Boolean, default: false },
});
defineEmits(["select"]);
const accessibleLabel = computed(
() =>
`${props.genealogy.name}${props.role}${props.selected ? ',当前家谱' : ''}${props.genealogy.location}${props.genealogy.memberCount} 位成员`,
);
</script>
<style scoped lang="scss">
@@ -86,6 +96,9 @@ defineEmits(["select"]);
.genealogy-card--current {
background: transparent;
}
.genealogy-card--pressed {
opacity: 0.72;
}
.row-frame {
grid-area: 1 / 1 / 2 / -1;
+1
View File
@@ -117,6 +117,7 @@ const goBack = () => {
box-sizing: border-box;
background: $brand-red;
color: #fff9ed;
transform: translateZ(0);
}
.page-header--root {
@@ -0,0 +1,252 @@
# Project-Wide Audit And Expert Optimization Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` for the audit phase. After that phase is fully verified, use two explicitly assigned expert subagents for the optimization phase. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Finish MuMu-native responsive, visual, interaction, and functional review for every active page from G10 onward, repair every confirmed issue through a five-option decision process, then run a two-expert project enrichment phase and implement the agreed improvements.
**Architecture:** `styles/adaptive-frame-profiles.scss` remains the only owner of nine-slice frame values, while `tests/responsive-layout-allowlist.json` remains the only owner of justified fixed-size exceptions. Each page is audited from fresh MuMu screenshots, repaired test-first only when evidence proves a defect, and reverified before the next page. After the baseline is complete, two independent experts inspect the whole product from visual/interaction and functional/code perspectives; the primary agent reconciles their proposals into small verified batches.
**Tech Stack:** uni-app, Vue 3, SCSS, PowerShell contracts, Node.js CDP navigation assistance, HBuilderX Android runtime, MuMu Android, ADB.
## Global Constraints
- G01 through G09 are already manually accepted and remain frozen unless a later global change produces a demonstrated regression.
- The continuous audit boundary begins at G10 and includes every later active route in `pages.json`.
- Do not stop after a single page. Continue through the plan unless genuinely blocked by missing authority, external state, or a business decision that cannot be inferred safely.
- For every confirmed defect, generate five viable solutions, compare contract fit, regression risk, affected scope, maintainability, and MuMu evidence, then implement the strongest option without asking the user to choose.
- If a trade-off remains genuinely ambiguous or three implementation hypotheses fail, dispatch an independent reviewer to argue the opposing technical position until a testable decision is reached.
- Content containers derive height from content. Only viewport-constrained overlays may use `max-height`, with internal scrolling when required.
- Do not hide layout failures by reducing font sizes or arbitrarily shrinking controls.
- Variable-height decorated surfaces use the shared nine-slice profiles. Pages and components must not define `border-image-slice` directly.
- Fixed-proportion icons, avatars, seals, thumbnails, and approved decorations may retain fixed dimensions only through `tests/responsive-layout-allowlist.json`.
- Do not use `background-size: 100% 100%` or an equivalent full-image stretch for variable content surfaces.
- Do not use a browser as visual acceptance evidence. Browser/CDP automation may only assist navigation or logic checks; accepted visual evidence must come from MuMu screenshots captured in the current run.
- Keep MuMu at its current user-selected `900x1600 / 320dpi` configuration unless the user explicitly requests another size.
- Do not start or close MuMu.
- Ignore the system watermark shown by MuMu screenshots.
- Guidance copy must use generic examples such as `某某某堂侄`; existing fixture/mock records are outside that copy cleanup rule.
- Do not modify API contracts, state enums, routes, or fixture data merely to solve layout problems.
- Do not run `git add`, `git commit`, `git push`, `git restore`, `git checkout`, or other Git mutation commands.
- Do not create compatibility fallbacks when a responsive contract changes. Update the owner, runtime, validators, tests, and documentation together.
## Page Inventory
The continuous audit covers these routes in order:
```text
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/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/records/r01-people-list
pages/records/r02-person-detail
pages/records/r03-gift-list
pages/records/r04-gift-editor
pages/records/r05-ritual-list
pages/records/r06-ritual-detail
pages/records/r07-ritual-editor
pages/records/r08-growth-journal
pages/records/r09-life-events
pages/records/r10-memo-list
pages/records/r11-merit-records
pages/notification/n01-message-center
pages/notification/n02-message-detail
pages/profile/m01-profile-home
pages/profile/m02-edit-profile
pages/profile/m03-security-settings
pages/profile/m04-change-password
pages/profile/m05-change-phone
pages/profile/m06-help-center
pages/profile/m07-feedback
pages/profile/m08-promotion
pages/profile/m09-vip-orders
pages/profile/m10-about-settings
```
## Standard Per-Page Audit Loop
Every page task below uses this exact loop:
- [x] Read the whole Vue page and its focused contracts before interaction.
- [x] Enumerate default, loading, empty, error, permission, validation, success, dialog, and long-content states; mark only genuinely unsupported states as not applicable.
- [x] Navigate through the running Android WebView without using browser-rendered screenshots.
- [x] Capture each stable state with ADB, save it under `tmp/project-audit/<page-id>/`, and inspect the saved PNG before accepting it.
- [x] Reject transition, blank, loading-incomplete, wrong-route, or cropped screenshots and capture them again.
- [x] Check content-driven height, wrapping, horizontal overflow, frame distortion, touch reachability, safe areas, modal scrolling, hierarchy, feedback placement, and state clarity.
- [x] If no defect is visible, run the page's focused contracts and advance.
- [x] If a defect is visible, document five solutions and select the one with the best project-wide contract fit and smallest justified surface area.
- [x] Add or tighten a focused failing contract and run it to observe the expected failure.
- [x] Implement the minimum source change, with no adjacent cleanup.
- [x] Run focused contracts, the global responsive contract, and compile audit.
- [x] Allow HBuilderX hot reload to update MuMu, then recapture every affected state and inspect each PNG.
- [x] Leave the page in its default review state and continue immediately to the next route.
---
### Task 1: Complete G10-G12 Genealogy Review
**Files:**
- Verify/Modify: `pages/genealogy/g10-application-review.vue`
- Verify/Modify: `pages/genealogy/g11-genealogy-settings.vue`
- Verify/Modify: `pages/genealogy/g12-generation-poems.vue`
- Test: `tests/g10-*.ps1`
- Test: `tests/g11-*.ps1`
- Test: `tests/g12-*.ps1`
**Interfaces:**
- Consumes: shared genealogy background, `PageHeader`, `AppDialog`, `AppButton`, `AppLoading`, adaptive genealogy frame profiles.
- Produces: verified settings, review, and generation-poem flows with current-run MuMu evidence.
- [x] Apply the Standard Per-Page Audit Loop to G10, including list, help, approve, reject, reject validation, result, empty, error, no-permission, and loading states. All stable MuMu states passed visual inspection and all focused G10 contracts passed without a source change.
- [x] Apply the loop to G11. The content-height regression was reproduced, protected by `tests/g11-document-flow-contract.ps1`, fixed by removing the panel `min-height`, and reverified in MuMu.
- [x] Apply the loop to G12, including list, empty, edit, validation, success, error, no-permission, loading, and long poem content. A forced panel `min-height` was reproduced, protected by `tests/g12-document-flow-contract.ps1`, removed, and reverified through the 48-character long-list bottom in MuMu.
- [x] Run all G10-G12 focused contracts plus the four project baseline commands. The applicable PowerShell contracts and all four baselines pass; the legacy browser runtime smoke was intentionally excluded from visual acceptance and cannot connect without its separate localhost browser harness.
### Task 2: Complete T-Series Review
**Files:**
- Verify/Modify: `pages/tree/t01-tree-overview.vue`
- Verify/Modify: `pages/tree/t03-member-profile.vue`
- Verify/Modify: `pages/tree/t04-add-relative.vue`
- Verify/Modify: `pages/tree/t05-edit-member.vue`
- Verify/Modify: `pages/tree/t06-edit-relationship.vue`
- Verify/Modify: `pages/tree/t07-member-directory.vue`
- Verify/Modify: `pages/tree/t08-member-states.vue`
- Verify/Modify: `components/tree/TreeMemberForm.vue`
- Test: matching `tests/t*.ps1` and runtime navigation contracts.
- [x] Apply the Standard Per-Page Audit Loop to T01. Fixed the clipped horizontal-pan cue by layering it over the visible grid viewport, and isolated tree-only scroll/metrics bindings so all non-tree state cards fit the viewport; all stable states plus horizontal panning passed MuMu review.
- [x] Apply the Standard Per-Page Audit Loop to T03. Audited editable, privacy, forbidden, missing/unknown error, and loading states; removed the forced member-panel `min-height`, updated its document-flow contract to the shared adaptive owners, and reverified in MuMu.
- [x] Apply the Standard Per-Page Audit Loop to T04. Audited relative/first-member forms, validation, long summary growth, discard dialog, success, and error; removed the forced panel `min-height`, added the business-contract guard, and reverified in MuMu.
- [x] Apply the Standard Per-Page Audit Loop to T05. Audited form, validation, long biography, discard dialog, success, error, and no-permission; removed the forced panel `min-height`, added a focused contract guard, and reverified in MuMu.
- [x] Apply the Standard Per-Page Audit Loop to T06. Audited form, validation, success, conflict, conflict-rules dialog, and error; removed the forced panel `min-height`, added a focused contract guard, and reverified in MuMu.
- [x] Apply the Standard Per-Page Audit Loop to T07. Audited full/filtered lists, search-driven empty, explicit empty, error recovery, and loading; no page-source defect was found. Updated the stale asset contract to assert the shared adaptive owners; all focused contracts pass.
- [x] Apply the Standard Per-Page Audit Loop to T08. Audited privacy, deceased, forbidden, and invalid-member error states with matching member/state contracts; all content, guidance, and actions fit without clipping, so no source change was required.
- [x] Cover tree overflow, selected member, long relationship copy, form validation, directory search, empty/error states, and permission states where supported.
- [x] Run focused T-series contracts, global responsive contract, and compile audit. All T-series PowerShell contracts and the four project baselines pass.
### Task 3: Complete F-Series Review
**Files:**
- Verify/Modify: `pages/family/f01-family-feed.vue` through `pages/family/f10-video-list.vue`
- Test: matching `tests/f*.ps1` and runtime flow contracts.
- [x] Apply the Standard Per-Page Audit Loop to F01. List, shortcuts, empty, error/retry, loading, tabbar spacing, and content scroll all pass MuMu review without a source change.
- [x] Apply the Standard Per-Page Audit Loop to F02. Audited form, empty validation toast, 276-character growth, success, and error; removed the forced panel `min-height`, added textarea `auto-height`, modernized the adaptive ownership contract, and reverified in MuMu.
- [x] Apply the Standard Per-Page Audit Loop to F03. Ready, expired, error, loading, article/comment flow, empty validation, submission failure, successful insertion, and long auto-height comment content all pass MuMu review without a source change.
- [x] Apply the Standard Per-Page Audit Loop to F04. Audited ready, empty, error, loading, search-empty/reset, 50-item growth, and route actions. The long-list audit exposed a shared fixed-header host stacking defect also visible in F03; added a global direct-host stacking contract, verified the fixed header at the list bottom, and regression-checked F03 plus the accepted G10 dialog overlay.
- [x] Apply the Standard Per-Page Audit Loop to F05. Ready article, fixed-header scroll boundary, favorite/unfavorite feedback, edit routing, expired/unknown return flow, privacy, error recovery, and loading all pass MuMu review without a page-source change.
- [x] Apply the Standard Per-Page Audit Loop to F06. Audited create/edit, validation, loading, draft, saving, failure retention/retry, success, return routing, and long content. MuMu reproduced a 524px body clipped into a 150px textarea; selected content-driven `auto-height`, migrated stale tests to adaptive profile ownership, and reverified equal client/scroll height plus natural page growth.
- [x] Apply the Standard Per-Page Audit Loop to F07. Ready/30-item growth, fixed-header bottom, empty, error, loading, album navigation, create dialog, empty validation, long name, successful insertion, and toast all pass MuMu review without a source change.
- [x] Apply the Standard Per-Page Audit Loop to F08. Normal photo wall/top-bottom scroll, five fixed-ratio media tiles, full-screen preview at the final photo, Android back-to-close behavior, empty/expired states, return routing, and upload navigation all pass MuMu review without a source change.
- [x] Apply the Standard Per-Page Audit Loop to F09. Audited initial, permission, selected, nine-photo maximum, add/remove/active-photo editing, required validation, long batch/photo notes, uploading lock/progress, failure retry, success return, and fixed-header gesture scroll. Added `auto-height` to both description fields and a shared PageHeader compositor layer after MuMu reproduced native-media scroll paint loss; reverified F08 media scrolling and the accepted G10 dialog overlay.
- [x] Apply the Standard Per-Page Audit Loop to F10. Audited loading, success/empty return flow, fixed-header layout, and status-card rendering. Replaced the stretched `scaleToFill` decorative image with the shared adaptive family-panel profile, tightened its contract, and reverified the resulting nine-slice frame in MuMu.
- [x] Cover feed cards, publish validation, detail comments, article lists/details/editor, album lists/details, media upload, video lists, empty/loading/error/success states, and long content.
- [x] Run focused F-series contracts, global responsive contract, and compile audit. All 13 focused F-series contracts and the four project baselines pass; two stale F01 tests were migrated from forbidden stretch-image assertions to the shared adaptive-profile owner.
### Task 4: Complete R-Series Review
**Files:**
- Verify/Modify: `pages/records/r01-people-list.vue` through `pages/records/r11-merit-records.vue`
- Test: matching `tests/r*.ps1` and module-series contracts.
- [x] Apply the Standard Per-Page Audit Loop to R01. Audited ready, search-empty/reset, empty, error/retry, person routing, and long copy in MuMu. Replaced fixed card height plus percentage padding with content-driven height and profile-owned safe padding; replaced the Android-incompatible `location.hash` state reader with the UniApp `onLoad` route contract.
- [x] Apply the Standard Per-Page Audit Loop to R02. Detail, edit, create validation, 450-character auto-height biography, privacy, loading, error/retry, expired, and related-route controls pass MuMu review without a page-source change.
- [x] Apply the Standard Per-Page Audit Loop to R03. Ready/50-item growth, fixed-header bottom, empty, error/retry, loading, and create/edit routing pass MuMu review without a source change.
- [x] Apply the Standard Per-Page Audit Loop to R04. Create validation, save success, forced failure/retry, view/edit, delete confirmation/cancel, and return flow pass MuMu review without a source change.
- [x] Apply the Standard Per-Page Audit Loop to R05. Ready/50-item growth, fixed-header bottom, empty, error/retry, loading, create, and detail routing pass MuMu review without a source change.
- [x] Apply the Standard Per-Page Audit Loop to R06. Detail, long description and participant roles, privacy, error/retry, loading/expired contracts, list return, and edit routing pass MuMu review without a source change.
- [x] Apply the Standard Per-Page Audit Loop to R07. Edit/create, required validation, long auto-height description, save success, forced-failure/retry contract, and delete flow pass MuMu review without a source change.
- [x] Apply the Standard Per-Page Audit Loop to R08. Ready timeline, empty/error/retry/loading contracts, required dialog validation, 1,520-character auto-height story, internal dialog scroll to actions, save insertion, and toast pass MuMu review without a source change.
- [x] Apply the Standard Per-Page Audit Loop to R09. Ready timeline, empty/create flow, required validation, successful insertion, toast, long-content and shared-dialog behavior pass MuMu review without a source change.
- [x] Apply the Standard Per-Page Audit Loop to R10. Ready, complete/reopen toggle, toast, long-card growth, create validation/save, empty/error/retry/loading contracts pass MuMu review without a source change.
- [x] Apply the Standard Per-Page Audit Loop to R11. Summary/list, long contribution growth, create validation/save, live total update, toast, empty/error/retry/loading contracts pass MuMu review without a source change.
- [x] Cover lists, details, editors, validation, long biographies, timeline content, empty/loading/error/success states, and feedback overlays.
- [x] Run focused R-series contracts, global responsive contract, and compile audit. All four focused R contracts and the four project baselines pass; the stale R02 stretch-image test was migrated to shared adaptive-profile ownership.
### Task 5: Complete N And M Series Review
**Files:**
- Verify/Modify: `pages/notification/n01-message-center.vue`
- Verify/Modify: `pages/notification/n02-message-detail.vue`
- Verify/Modify: `pages/profile/m01-profile-home.vue` through `pages/profile/m10-about-settings.vue`
- Test: matching `tests/n*.ps1`, `tests/m*.ps1`, and module-series contracts.
- [x] Apply the Standard Per-Page Audit Loop to N01. Ready/unread, mark-all-read, toast, long-card growth, empty/error/retry/loading contracts, and detail/review routing pass MuMu review without a page-source change.
- [x] Apply the Standard Per-Page Audit Loop to N02. Unread/read, mark-read toast, long-body growth, approved-target variant, expired/return flow, and responsive state layout pass MuMu review without a page-source change.
- [x] Apply the Standard Per-Page Audit Loop to M01 through M10 in route order. Reviewed profile ready/error and service navigation; edit/password/phone validation and long inputs; security routes; FAQ expansion/search; feedback validation and long content; promotion copy/poster dialog; VIP empty/ready orders and service dialog; and about agreements plus logout cancel/confirm/disabled states in MuMu.
- [x] Cover unread/read, detail, profile summary, edit and security forms, password/phone validation, help, feedback, promotion, orders, about/settings, loading/error/success, and long content.
- [x] Run focused N/M contracts. All eight N/M PowerShell contracts pass; the stale M01 stretch-image assertions were migrated to the shared adaptive-profile owner. Project-wide baselines continue in Task 6.
### Task 6: Project-Wide Baseline Closure
**Files:**
- Verify: `tests/responsive-layout-coverage.json`
- Verify: `tests/responsive-layout-allowlist.json`
- Verify: `styles/adaptive-frame-profiles.scss`
- Verify: all `components/**/*.vue`, `pages/**/*.vue`, and `App.vue`.
- [x] Run `powershell -ExecutionPolicy Bypass -File tests/adaptive-frame-profiles-contract.ps1` and require PASS.
- [x] Run `powershell -ExecutionPolicy Bypass -File tests/project-responsive-layout-contract.ps1` and require PASS.
- [x] Run `powershell -ExecutionPolicy Bypass -File tests/ag-responsive-surfaces-contract.ps1` and require PASS.
- [x] Run `powershell -ExecutionPolicy Bypass -File tests/compile-audit.ps1` and require PASS.
- [x] Verify the Vue file set exactly matches `tests/responsive-layout-coverage.json`, with zero missing and zero stale entries: 66 actual and 66 covered.
- [x] Verify zero Vue matches for `100% 100% no-repeat`.
- [x] Verify zero direct `border-image-slice` declarations in Vue pages/components.
- [x] Confirm MuMu remains online and retains the user-selected resolution and density: `emulator-5554`, physical 900×1600 at 320 dpi. The accepted G10 checkpoint was rechecked after the final contract migration and remains visually correct.
### Task 7: Two-Expert Whole-Product Optimization Review
**Files:**
- Create: `docs/superpowers/specs/2026-07-21-whole-product-enrichment-design.md`
- Create: `docs/superpowers/plans/2026-07-21-whole-product-enrichment-implementation.md`
- Verify: all accepted MuMu screenshot folders from Task 1 through Task 6.
**Interfaces:**
- Expert A produces: visual-system and interaction recommendations ranked by user impact, consistency, accessibility, and implementation risk.
- Expert B produces: functional-flow and frontend-code recommendations ranked by correctness, maintainability, performance, and regression risk.
- Primary agent produces: one reconciled design and implementation plan with no duplicate contract ownership.
- [x] Dispatch Expert A with the complete project constraints, accepted MuMu evidence, shared components, style profiles, and route inventory. Require concrete file-level recommendations and forbid browser visual evidence.
- [x] Dispatch Expert B independently with the same evidence and constraints. Require concrete flow, state, accessibility, and code-quality recommendations.
- [x] Send each expert the other's proposal and require a written challenge covering conflicts, missing risks, and unnecessary scope.
- [x] Reconcile both reviews. For every disputed improvement, generate five solutions and select one using user impact, global consistency, regression risk, maintenance cost, and MuMu verifiability.
- [x] Write the reconciled design spec and implementation plan. Do not alter accepted business contracts without an explicit migration requirement.
### Task 8: Implement And Verify Expert Optimizations
**Files:**
- Modify: only the files named by the reconciled Task 7 plan.
- Test: focused contracts created before each production change.
- Evidence: `tmp/project-optimization/<batch-id>/` MuMu screenshots.
- [x] Divide accepted improvements into small batches that each have one owner and one independently testable outcome.
- [x] For each batch, write the focused failing contract and observe the expected failure.
- [x] Implement the minimum shared or page-level change chosen by the reconciled plan.
- [x] Run focused and project-wide contracts.
- [x] Capture and inspect MuMu screenshots for every affected route and state.
- [x] Recheck previously accepted pages whenever a shared component or shared profile changes.
- [x] Finish only when all accepted optimization batches and the project-wide baseline pass with current-run evidence.
## Self-Review
- The plan covers every route from G10 onward and explicitly freezes G01-G09 except for demonstrated shared regressions.
- The single owners for nine-slice profiles, responsive exceptions, and coverage remain unchanged.
- The five-option decision process, autonomous execution, MuMu-only visual evidence, no-Git constraint, current emulator configuration, and two-expert enrichment phase are explicit.
- There are no compatibility fallback steps and no authorization to modify business contracts speculatively.
@@ -0,0 +1,154 @@
# 全项目第二轮体验与代码优化实施计划
> **For agentic workers:** 按任务顺序执行;每个任务都先写失败契约、再做最小实现、最后在 MuMu 验证。用户禁止任何 Git 操作。
**Goal:** 在保持已验收视觉与业务合同的前提下,完成 Android 原生路由、共享弹窗、关键交互语义、密码策略和世系树恢复定位优化。
**Architecture:** 活动页面的 `onLoad(query)` 是原生路由唯一所有者;共享弹窗和共享点击表面只在组件层持有跨页规则;账户密码策略由一个纯函数模块持有;T01 的恢复定位只属于树页本身。
**Tech Stack:** UniApp、Vue 3 `<script setup>`、SCSS、PowerShell 静态契约、MuMu Android WebView/CDP 只读检查。
## 全局约束
- 不使用浏览器做视觉审核,不启动或关闭 MuMu。
- 不执行 Git add、commit、push、restore、checkout。
- 不修改 G01~G10 视觉,除非修复已证明的 Android/共享缺陷。
- 页面示例提示使用“某某某”;fixture 数据不处理。
- 内容自然增高;弹窗只允许视口 max-height 和内部滚动。
- 不在 Vue 中新增 `100% 100% no-repeat``border-image-slice`
---
### Task 1: 原生活动页路由参数
**Files:**
- Create: `tests/native-route-query-contract.ps1`
- Modify: `pages/genealogy/g01-my-genealogies.vue`
- Modify: `pages/genealogy/g03-create-genealogy.vue`
- Modify: `pages/genealogy/g06-search-genealogies.vue`
- Modify: affected G01/G03/G06 route contracts and runtime-smoke assumptions only when they assert hash ownership
**Interfaces:**
- G01 `onLoad(query)` consumes `state?: default|empty|loading|error` and `genealogyId?: string`.
- G03 `onLoad(query)` consumes `step?: create|ancestor` and `genealogyId?: string`.
- G06 `onLoad(query)` consumes `mode?: search|invite` and `state?: loading|initial`.
- [x] Add a failing contract forbidding `location.hash`, `URLSearchParams` and `hashchange` in active pages and requiring the three `onLoad(query)` entrypoints.
- [x] Run `powershell -ExecutionPolicy Bypass -File tests/native-route-query-contract.ps1`; expect failure on current G01/G03/G06.
- [x] Replace global URL reads with page-owned normalized query refs; delete obsolete listeners and fallback branches.
- [x] Update only stale tests that still make H5 hash the production contract.
- [x] Run focused G01/G03/G06 tests and the four project baselines.
- [x] In MuMu verify G05→G03 ancestor, G01→G06 invite, and re-entry without stale state; save screenshots under `tmp/project-optimization/native-route/`.
### Task 2: AppDialog 高度预算
**Files:**
- Modify: `tests/app-dialog-responsive-content-contract.ps1`
- Modify: `components/AppDialog.vue`
- Modify: `styles/global.scss` only if a shared safe-area variable is required
**Interfaces:**
- `.app-dialog-layer` owns viewport safe padding.
- `.app-dialog` owns the only dialog max-height budget.
- `.app-dialog__body` remains the internal scrolling region.
- [x] Tighten the dialog contract to require safe top/bottom and one closed height formula; keep existing internal-scroll assertions.
- [x] Run the contract and observe the expected failure.
- [x] Implement the smallest CSS-only budget change without changing markup or assets.
- [x] Run the focused contract and four baselines.
- [x] In MuMu verify G10 approve/reject, R04 delete, R08 long top/bottom, M10 agreement/logout and F07 create; save evidence under `tmp/project-optimization/dialog-safe-area/`.
### Task 3: AppTabbar 与 GenealogyCard 交互语义
**Files:**
- Modify: `tests/shared-interaction-accessibility-contract.ps1`
- Modify: `components/AppTabbar.vue`
- Modify: `components/GenealogyCard.vue`
**Interfaces:**
- Tabbar root exposes `tablist`; each item exposes `tab`, dynamic `aria-selected`, accessible label and a no-layout-shift pressed class.
- GenealogyCard exposes one button semantic surface with a computed accessible label and pressed class.
- [x] Add failing assertions for role/name/current state/pressed feedback.
- [x] Run the accessibility contract and confirm failure.
- [x] Add semantic attributes and `hover-class` while retaining existing DOM geometry and assets.
- [x] Run focused component, responsive and compile contracts.
- [x] In MuMu verify F01/M01 tab switching and G01 current/non-current cards, including press feedback and unchanged layout; save evidence under `tmp/project-optimization/shared-pressable/`.
### Task 4: G10 拒绝错误关联
**Files:**
- Modify: `tests/g10-document-flow-contract.ps1`
- Modify: `tests/g09-g10-business-state-contract.ps1`
- Modify: `pages/genealogy/g10-application-review.vue`
**Interfaces:**
- Reject textarea has stable id, `aria-invalid`, `aria-describedby` and an error element with `role="alert"`.
- Failed reject submission focuses the textarea after `nextTick`; valid input clears the error contract.
- [x] Add failing static assertions for field/error association and focus call.
- [x] Run focused tests and confirm failure.
- [x] Implement the semantic association and focus behavior without changing dialog layout.
- [x] Run all G10 tests and the four baselines.
- [x] In MuMu verify empty reject, correction, confirmation and long reason with keyboard/internal scroll; save evidence under `tmp/project-optimization/g10-validation/`.
### Task 5: 单一密码策略
**Files:**
- Create: `utils/validation.js`
- Create: `tests/password-policy-contract.ps1`
- Create: `tests/password-policy-runtime-smoke.js` as a Node-only pure-function test
- Modify: `pages/auth/a04-register.vue`
- Modify: `pages/auth/a05-reset-password.vue`
- Modify: `pages/profile/m04-change-password.vue`
**Interfaces:**
- `validatePassword(value)` returns `{ valid: boolean, message: string }`.
- `PASSWORD_POLICY_MESSAGE` is the single visible rule text.
- M04 additionally rejects `next === current` in its page-specific validator.
- [x] Write static ownership assertions and pure-function cases for empty, 7-char, letters-only, digits-only, valid 8-char and 33-char values.
- [x] Run both tests and observe missing-module/ownership failure.
- [x] Implement the pure validation module and migrate all three pages, deleting local duplicated regex/messages.
- [x] Run A04/A05/M04 focused tests, pure runtime test and four baselines.
- [x] In MuMu submit the same invalid/valid samples on all three pages; verify identical rule feedback and natural error wrapping; save evidence under `tmp/project-optimization/password-policy/`.
### Task 6: T01 回到当前成员
**Files:**
- Modify: `tests/t01-tree-state-contract.ps1`
- Modify: `tests/t01-tree-state-runtime-smoke.js` only to express the new state contract without browser-only visual claims
- Modify: `pages/tree/t01-tree-overview.vue`
**Interfaces:**
- Each member node has a stable DOM id derived from member id.
- `treeScrollLeft` binds to `scroll-left`.
- A user scroll sets `treeHasDrifted`; `recenterSelectedMember()` measures the selected node and visible scroll viewport, then centers the node and clears drift.
- [x] Add failing assertions for stable node id, drift state, conditional action and recenter function.
- [x] Run the focused test and confirm failure.
- [x] Add the conditional semantic action using the existing toolbar visual language; do not alter node/line geometry.
- [x] Run T01 focused tests and four baselines.
- [x] In MuMu pan left/right, recenter, change selection and recenter again; verify selected detail is unchanged and empty/loading/error omit the action; save evidence under `tmp/project-optimization/t01-recenter/`.
### Task 7: 全项目闭环
**Files:**
- Modify: `docs/superpowers/plans/2026-07-21-project-wide-audit-and-expert-optimization.md`
- Verify: all `tests/*.ps1`, coverage/allowlist owners and affected MuMu evidence directories
- [x] Run every `tests/*.ps1` and require all pass.
- [x] Run the Node-only password policy test.
- [x] Run the four user-required baseline commands again.
- [x] Compare actual Vue files to `responsive-layout-coverage.json`; require exact equality.
- [x] Require zero Vue matches for `100% 100% no-repeat` and direct `border-image-slice`.
- [x] Confirm MuMu stays online at the user-selected configuration and inspect every affected screenshot.
- [x] Recheck accepted G10 and one representative F/R/N/M page after shared changes.
- [x] Record exact pass counts, modified files, evidence paths, remaining deferred debt and skipped browser-only smoke tests.
## 自检
- 设计中的六个实施批次均有对应任务和独立验收。
- 没有 TODO/TBD、兼容旧 hash 的双路径或未经授权的 API 接入。
- 计划没有任何 Git 命令。
- 页栈、短信、领域 repository 与全局文字系统明确暂缓,没有被悄悄塞入实现。
@@ -0,0 +1,171 @@
# 全项目第二轮体验与代码优化设计
## 目标
在不推翻已通过国风视觉、不改变现有业务枚举、不接入未经授权的远程服务的前提下,修复 Android 原生路由、共享弹窗高度预算、关键交互语义和密码策略的一致性问题,并为世系树补充可恢复定位能力。
## 已确认基线
- G01~G10 保持已人工验收结果;只有明确的共享回归或 Android 运行时缺陷才允许触及。
- G11 之后全部活动页面已完成 MuMu 逐页与主要状态审核。
- 127/127 个 PowerShell 契约通过;66/66 个 Vue 文件与响应式覆盖清单一致。
- Vue 中 `100% 100% no-repeat` 为 0;页面/组件直接声明 `border-image-slice` 为 0。
- 当前 MuMu 为 `emulator-5554`,物理 900×1600、320 dpi;不得由代理启动或关闭。
## 全局约束
- 视觉验证只使用 MuMu Android,不使用浏览器。
- 内容高度由内容决定;弹窗只使用视口最大高度和内部滚动。
- 不通过缩小字号、固定正文高度或强制单行掩盖布局问题。
- 九宫格切片继续只由 `styles/adaptive-frame-profiles.scss` 持有。
- 固定比例图标、缩略图和印章的例外继续只由响应式 allowlist 持有。
- 页面示例提示只使用“某某某”;现有 fixture 数据不纳入文案替换。
- 不执行 Git add、commit、push、restore、checkout。
- 每个生产改动先有失败契约,改后跑聚焦契约、四项基线并在 MuMu 验证。
## 专家交叉评审结论
视觉交互专家认为现有国风体系健康,不应重做;功能代码专家认为最高价值缺陷集中在 Android 路由所有权。双方一致接受分批修复,并共同否决全量视觉重做、全量原生按钮迁移、状态库引入、虚拟列表和无后端契约的假持久化。
## 批次一:活动页原生路由参数
### 问题
G01、G03、G06 在 Android WebView 中仍优先读取 `location.hash`。R01 已实证该路径会忽略 UniApp 原生查询参数。
### 五个方案
1. 保持现状:已被 MuMu 反证,淘汰。
2. 调整双读取优先级:仍保留两个运行时合同,淘汰。
3. 用 H5 条件编译保留 hash:可行但生产页面仍承担测试入口,非最简。
4. 新建全局 route-query 工具:对三个简单枚举参数过度抽象。
5. 页面 `onLoad(query)` 成为唯一入口,页面内白名单归一化:最小且符合 UniApp。
### 选择
采用方案 5。G01 保存 `state/genealogyId`G03 保存 `step/genealogyId`G06 保存 `mode/state`;删除活动页的 hash、URLSearchParams 和 hashchange 监听。`ModulePage``TreeMemberForm` 与封存 A06 没有活动消费者,本批不延长其生命周期。
### 验收
从 G05 进入 G03 首代人物步骤、从 G01 进入 G06 邀请码模式,返回再进入不串状态;活动路由静态扫描不得再出现 H5 查询读取。
## 批次二:AppDialog 唯一高度预算
### 问题
长弹窗的遮罩 padding、九宫格边框、视口高度和安全区分别计算,所有权分散;极短屏或非零安全区存在侵入系统栏或底部操作空间不足的风险。
### 五个方案
1. 增大固定 padding:不适配设备差异。
2. 只在内容区减安全区:父子仍重复猜测空间。
3. 外层 safe-area padding,保留内容公式:预算仍分裂。
4. 改成标题/正文/操作 sticky 三段:改变已验收结构,范围过大。
5. `.app-dialog-layer` 持有安全区与外边距,`.app-dialog` 持有唯一 max-height,正文只在剩余空间内部滚动。
### 选择
采用方案 5,不改变视觉层级、按钮顺序或九宫格资产。短弹窗继续内容驱动,长弹窗滚动到操作区。
### 验收
G10 通过/拒绝、R04 删除、R08 长内容、M10 协议/退出在当前 MuMu 均完整;长内容顶部不进入状态栏,底部按钮可达,短弹窗不产生大空白。
## 批次三:共享点击表面语义与按压反馈
### 问题
AppTabbar 与 GenealogyCard 是活动共享表面,但目前主要依赖普通 `view @click`,缺少当前项播报和统一按压反馈。
### 五个方案
1. 不处理:TalkBack 与即时反馈缺陷继续存在。
2. 所有页面就地补属性:重复且易漏。
3. 全量替换为原生 button:Android 默认样式回归面大。
4. 新建 AppPressable 并迁移全部点击表面:本轮过度设计。
5. 保留现有 DOM 与皮肤,只给两个活动共享组件补 role、动态 aria、选中/禁用语义和无位移 hover-class。
### 选择
采用方案 5。Tabbar 使用 `tablist/tab/aria-selected`GenealogyCard 使用单一 button 语义与包含家谱名、角色、当前状态的可访问名称。按压态只改变透明度,不改变尺寸。
### 验收
F01/M01 底栏与 G01 家谱卡在 MuMu 中点击热区不变、无布局位移、选中态仍清楚;静态契约验证可访问属性和 pressed class。
## 批次四:G10 拒绝错误可达性
### 问题
拒绝原因的可见错误没有与 textarea 程序化关联,提交失败后也没有稳定焦点返回路径。
### 五个方案
1. 仅保留红色文字:不可访问。
2. 只加 `role=alert`:能播报但未关联字段。
3. 新增页顶错误摘要:改变已验收视觉。
4. 全项目一次性迁移所有表单:回归面过大。
5. G10 先建立稳定 id、aria-invalid、aria-describedby、alert,并在失败后 nextTick 聚焦字段。
### 选择
采用方案 5,作为后续表单迁移样板,不改变弹窗现有外观。
### 验收
拒绝原因为空时错误出现并与字段关联,焦点回到 textarea;输入后错误与 invalid 状态同步清除;键盘弹出时操作按钮仍能滚动到达。
## 批次五:统一密码策略
### 问题
A04、A05 只判断非空,M04 要求 8~32 位且含字母和数字,同一账户合同互相矛盾。
### 五个方案
1. 保持三套规则:错误。
2. 将 M04 正则复制到 A04/A05:产生三份所有者。
3. 仅依赖服务端:当前 mock 模式无法及时反馈。
4. 动态拉取服务端策略:尚无接口合同。
5. `utils/validation.js` 持有唯一密码规则与错误文案,三页共同消费。
### 选择
采用方案 5。规则为 8~32 位、同时含字母与数字;M04 额外拒绝新旧密码相同。旧页面正则和分裂文案必须删除。
### 验收
三页对空值、7 位、纯字母、纯数字、合法 8 位和 33 位输入给出一致结果;MuMu 中错误换行与内容高度正常。
## 批次六:T01 回到当前成员
### 问题
世系树横向平移后没有恢复到当前成员的动作,大谱系中容易失去上下文。
### 五个方案
1. 不处理:保留迷失风险。
2. 每次选择自动居中:干扰自由浏览。
3. 始终显示固定按钮:增加默认页面密度。
4. 新增小地图和缩放:属于重设计。
5. 仅在用户明显平移后显示“回到当前成员”,通过稳定节点 id 和 `scroll-into-view` 主动恢复。
### 选择
采用方案 5。动作复用现有工具栏风格,不改变节点、连线、画布尺寸和底部详情卡。
### 验收
左右平移后动作出现;点击后当前节点重新可见、选中成员和详情不变,动作消失;空/加载/错误状态不显示。
## 明确暂缓
- 返回页栈:问题真实,但必须先把“恢复来源上下文”和“深链 fallback”分类,并与保存后的数据刷新一起设计,禁止机械替换所有 `redirectTo/reLaunch`
- 短信验证码:记录为认证合同债务;真实接口、过期、重发和 token 所有权确定前,不制造本地假验证。
- 跨页保存/删除:当前 mock fixture 分散,不能继续把视图切换声称为持久成功;领域 repository 作为独立后续迁移,不在本批扩大范围。
- 全局文字 token:只在本批新增的错误/辅助文案保持可读,不对所有 19~21rpx 文本一刀切。
## 完成定义
六个批次各自通过先失败后通过的聚焦契约和 MuMu 验证;随后全部 PowerShell 契约、四项指定基线、66/66 覆盖与两个禁用扫描再次通过。共享改动必须回归至少一个已人工验收页面。
+2
View File
@@ -1,5 +1,7 @@
# 家谱 APP 换机交接记录
> **当前交接入口(2026-07-22):** 请先完整阅读 `docs/换机继续任务交接_2026-07-22.md`。本文件下方内容保留为历史记录;凡是“从 G01/G10 继续”“只做 H5”“Android 尚未审核”“仍有 52 页待审”等说法,均已被 2026-07-22 交接覆盖,不得作为当前停点。
> 最后更新:2026-07-21
> 用途:更换电脑、重新打开 Codex/GPT 后的唯一接管入口。
> 当前阶段:只做页面样式与视觉流程验收;不对接接口,不做功能验收。
+304
View File
@@ -0,0 +1,304 @@
# 家谱 APP 换机继续任务交接(2026-07-22
> 用途:把当前项目完整复制到另一台电脑后,让新的 Codex/GPT 会话无需重新推翻既有设计,直接从正确停点继续。
>
> 当前权威状态:2026-07-21 的全项目响应式审核、MuMu 复核、两位专家评审与第一轮产品优化已经闭环。本文件覆盖 `docs/交接记录.md`、`docs/新会话审批交接_2026-07-19.md`、`docs/夜间批量收敛交接_2026-07-20.md` 中与当前进度冲突的旧停点;旧文件只保留为历史证据。
## 1. 换机时必须复制什么
最稳妥的方式是复制整个 `jiapuapp` 项目目录,而不是只重新拉取远端代码。当前工作包含源码、测试、规划和可能尚未同步到远端的文件,不能假设远端仓库已经拥有本轮全部成果。
新电脑上的项目路径可以变化。下文把项目根目录记为:
```text
<JIAPUAPP_ROOT>
```
必须保留:
- `AGENTS.md`
- `App.vue``pages/``components/``styles/``utils/`
- `tests/`,尤其是响应式覆盖、白名单和本轮新增合同
- `docs/`,尤其是 2026-07-21、2026-07-22 的规格、计划和本交接
- `static/``design-pipeline/` 中的正式资产、清单和重建脚本
- `pages.json``package.json` 及项目配置文件
本地证据目录:
- `tmp/project-audit/`:G10 之后逐页 MuMu 审核截图
- `tmp/project-optimization/`:专家优化后的 MuMu 复核截图
`tmp/` 通常不属于长期仓库资料。如果希望在新电脑继续查看本轮截图,必须单独复制上述两个目录;不复制不会影响源码和合同,但会失去本轮临时视觉证据。
可在换机后重新生成、无需作为权威资料的目录包括 `node_modules/``unpackage/``.vite/` 和其他构建缓存。不要删除正式资产、测试、规划、母版或长期证据来缩小目录。
## 2. 新会话必须按顺序阅读
新的 Codex/GPT 接管后,在修改代码前完整阅读:
1. `AGENTS.md`
2. `docs/换机继续任务交接_2026-07-22.md`(本文件)
3. `docs/superpowers/specs/2026-07-21-project-wide-responsive-layout-design.md`
4. `docs/superpowers/plans/2026-07-21-responsive-foundation-phase-1.md`
5. `tests/responsive-layout-coverage.json`
6. `styles/adaptive-frame-profiles.scss`
7. `docs/superpowers/plans/2026-07-21-project-wide-audit-and-expert-optimization.md`
8. `docs/superpowers/specs/2026-07-21-whole-product-enrichment-design.md`
9. `docs/superpowers/plans/2026-07-21-whole-product-enrichment-implementation.md`
发生冲突时,权威顺序为:
1. 用户在新会话中的最新明确要求。
2. 本交接文件。
3. 2026-07-21 的响应式、专家优化规格和计划。
4. 当前源码、测试和配置。
5. 更早的交接、设计记录和历史截图。
旧文档中“从 G01 开始”“只审核 H5”“Android 尚未验证”“仍剩 52 页待审”等说法已经失效,不得据此回退当前进度。
## 3. 当前真实完成状态
- `App.vue`、公共组件和所有活动页面共 **66 个 Vue 文件**已全部纳入响应式扫描。
- 最近一次闭环结果:实际 Vue `66`、覆盖 `66`、缺失 `0`、陈旧 `0`
- A 系列与 G01—G10 由用户在 MuMu 中逐页确认过;正式人工验收断点曾是 G10。
- G11、G12 以及后续 T、F、R、N、M 页面已经按用户授权,由主代理在 MuMu 中逐页、逐状态继续审核并完成问题修复,不再停在 G11。
- G10 之后已覆盖 G11—G12、T01/T03—T08、F01—F10、R01—R11、N01—N02、M01—M10。
- 两位专家已经完成评审和交叉质询:
- 视觉交互专家:Dirac
- 功能代码专家:Leibniz
- 不要重新创建两位专家或重复第一轮专家评审,除非用户在新会话明确要求第二轮。
- 两位专家的结论已经汇总到产品丰富化设计和实施计划;第一轮接受的六个优化批次已经实现。
- 规划文件中的实施检查项已经全部标记完成。
## 4. 已完成的关键修复与优化
### 4.1 全项目响应式基础
- 九宫格边框和可变高度装饰面的唯一所有者:`styles/adaptive-frame-profiles.scss`
- 页面和普通组件不得自行定义 `border-image-slice`
- 可变高度内容由内容自然撑高,不使用固定高度掩盖问题。
- 弹窗只允许使用视口 `max-height`,内容区域内部滚动。
- 页面内禁止重新出现 `background-size: 100% 100% no-repeat`
- 固定比例图标、头像、印章、缩略图等例外只允许登记在 `tests/responsive-layout-allowlist.json`
- 响应式扫描范围唯一清单:`tests/responsive-layout-coverage.json`
### 4.2 第一轮专家优化的六个批次
1. **原生路由参数**
- G01、G03、G06 不再依赖 `location.hash``URLSearchParams``hashchange`
- Android 活动页参数由各页 `onLoad(query)` 唯一持有。
- 保护合同:`tests/native-route-query-contract.ps1`
2. **AppDialog 高度预算**
- `components/AppDialog.vue` 统一持有顶部、底部安全区和唯一最大高度公式。
- 外层弹窗使用 Flex 列布局,内容区 `min-height: 0` 并独立滚动。
- 已在 G10、R04、R08、F07、M10 等短/长弹窗复核。
3. **共享点击语义**
- `components/AppTabbar.vue` 提供 `tablist``tab``aria-selected` 和按压反馈。
- `components/GenealogyCard.vue` 提供按钮语义、组合可访问名称和按压反馈。
- 保护合同:`tests/shared-interaction-accessibility-contract.ps1`
4. **G10 拒绝原因校验**
- 文本域具有稳定 ID、`aria-invalid``aria-describedby`
- 错误元素使用 `role="alert"`;空提交后文本域自动聚焦。
- 页面:`pages/genealogy/g10-application-review.vue`
5. **唯一密码策略**
- 唯一所有者:`utils/validation.js`
- A04、A05、M04 统一要求 8—32 位且同时包含字母和数字。
- M04 额外拒绝新密码与当前密码相同。
- 保护合同:`tests/password-policy-contract.ps1``tests/password-policy-runtime-smoke.js`
6. **T01 回到当前成员**
- 页面:`pages/tree/t01-tree-overview.vue`
- 用户横向拖离当前节点后才显示“回到当前”。
- 最终实现不是固定偏移或简单 `scroll-into-view`,而是测量滚动视口和选中节点中心后设置精确 `scroll-left`
- MuMu 最终实测中心误差约 `0.24 CSS px`
### 4.3 本轮逐页审核中已经解决的典型问题
- G11、G12、T03—T06 等内容面板移除强制 `min-height`,恢复内容驱动高度。
- F02、F06、F09 等长文本输入改为自然增长的 `auto-height`
- F04/F03 和 F09/F08 修复固定页头在长列表、原生媒体滚动时的层叠问题。
- F10 的可变高度装饰面迁移到共享九宫格档案。
- R01 卡片移除固定高度和百分比内边距,并改用原生 `onLoad` 状态参数。
- T01 的代际栏、横向提示、树画布和非树状态已分别约束,不能再退回固定大画布补丁。
完整逐页记录见:
- `docs/superpowers/plans/2026-07-21-project-wide-audit-and-expert-optimization.md`
- `docs/superpowers/specs/2026-07-21-whole-product-enrichment-design.md`
- `docs/superpowers/plans/2026-07-21-whole-product-enrichment-implementation.md`
## 5. 最近一次验证基线
2026-07-21 最终闭环时:
- 全部 `tests/*.ps1``129/129 PASS`
- `tests/password-policy-runtime-smoke.js`PASS
- 四个用户指定核心基线:全部 PASS
- Vue 文件:`66/66`
- Vue 中 `100% 100% no-repeat``0`
- Vue 中直接 `border-image-slice``0`
- MuMu 设备:`emulator-5554` 在线
- MuMu 物理尺寸:`900×1600`
- MuMu density`320 dpi`
- Android WebView 逻辑视口约为 `451×800 CSS px`
这些数字是上次闭环事实。换机后必须重新运行验证,不能因为本文件写着通过就直接宣称新环境通过。
## 6. 新电脑首次启动后的验证顺序
`<JIAPUAPP_ROOT>` 中运行:
```powershell
powershell -ExecutionPolicy Bypass -File tests/adaptive-frame-profiles-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/project-responsive-layout-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/ag-responsive-surfaces-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/compile-audit.ps1
node tests/password-policy-runtime-smoke.js
```
运行全部 PowerShell 合同:
```powershell
$files = Get-ChildItem -LiteralPath tests -Filter '*.ps1' | Sort-Object Name
$pass = 0
$fail = 0
$failed = @()
foreach ($file in $files) {
& powershell -NoProfile -ExecutionPolicy Bypass -File $file.FullName *> $null
if ($LASTEXITCODE -eq 0) {
$pass++
} else {
$fail++
$failed += $file.Name
}
}
Write-Output "TOTAL=$($files.Count) PASS=$pass FAIL=$fail"
$failed
```
核对 Vue 覆盖:
```powershell
$coverage = (Get-Content -Raw tests/responsive-layout-coverage.json | ConvertFrom-Json).covered | Sort-Object
$actual = @(rg --files -g '*.vue' | ForEach-Object { $_ -replace '\\','/' }) | Sort-Object
$missing = @($actual | Where-Object { $_ -notin $coverage })
$stale = @($coverage | Where-Object { $_ -notin $actual })
"ACTUAL=$($actual.Count) COVERED=$($coverage.Count) MISSING=$($missing.Count) STALE=$($stale.Count)"
```
核对两项禁用规则:
```powershell
rg -n --glob '*.vue' '100%\s+100%\s+no-repeat' .
rg -n --glob '*.vue' 'border-image-slice\s*:' .
```
两条 `rg` 命令无输出才是正确结果。
## 7. MuMu 验证规则
- 只使用 MuMu 安卓模拟器作为视觉验收依据,不使用浏览器截图代替。
- 不要启动、关闭或改变用户的模拟器配置,除非用户明确要求。
- 新电脑上的设备序列号可能不再是 `emulator-5554`,先用 `adb devices` 读取真实值,不要硬编码旧值。
- 上次设备为 `900×1600 / 320dpi`;如果新电脑已有不同配置,先报告,不要擅自调整。
- 忽略截图中的“3D研究室”等系统水印。
- 每次修改页面后,先跑聚焦合同、全局响应式合同和编译审计,再等待 HBuilderX 热更新,最后在 MuMu 复核。
- 视觉截图必须等待页面稳定;拒绝过渡帧、空白页、错误路由、未加载完成和被软键盘意外遮挡的截图。
- MuMu WebView 调试端口在上次环境为 `9223`,每次导航后 target ID 都可能变化,可用以下命令重新查询:
```powershell
curl.exe -s http://127.0.0.1:9223/json
```
- CDP 只能辅助导航、读取 DOM 和逻辑状态;最终视觉结论必须来自 ADB 获取的 MuMu 屏幕截图。
## 8. 后续开发必须继续遵守的原则
- 整个项目自适应,不接受只针对某个尺寸的单页补丁。
- 内容高度由内容决定。
- 弹窗只使用视口最大高度和内部滚动。
- 不通过压缩字号、降低行高或随意缩小按钮掩盖布局错误。
- 每个确认问题先提出五个可行方案,比较全局合同、回归风险、影响范围、维护成本和 MuMu 可验证性,再自主选择最优方案。
- 共享规则必须由唯一公共所有者持有;页面不得复制九宫格切片、密码规则或共享弹窗高度公式。
- 修改合同必须同时更新唯一所有者、运行代码、验证器、测试和文档,删除旧路径,不保留“以防万一”的兼容读取。
- 页面提示文案不得使用“汤正华堂侄”等具体姓名示例,应使用“某某某堂侄”等通用提示;已有 fixture/mock 数据本身无需清理。
- 不重复修改已经通过的 G01—G10,除非有明确证据证明全局改动造成回归。
- 不执行 `git add``commit``push``restore``checkout``reset` 等 Git 变更操作。
- 不删除或覆盖用户现有改动、未跟踪文件、资产、测试和文档。
## 9. 下一阶段推荐入口
响应式和第一轮专家优化已完成。下一阶段不要重新做全项目响应式迁移,优先从已记录但尚未进入第一轮实现的产品深化项中选择:
1. **导航栈语义统一**:区分 `navigateTo``redirectTo``reLaunch``switchTab` 的业务边界,避免深层页面堆栈和错误返回。
2. **短信验证码完整状态机**:发送、倒计时、重发、失败、过期、提交锁定及页面离开清理。
3. **跨页面领域数据持久化**:替换部分只在当前页面生效的演示状态,使创建、编辑、删除后的结果在返回列表时一致。
4. **全局文字层级与可访问性第二轮**:在不缩小字号的前提下统一正文、辅助信息、错误提示和可触控语义。
推荐先处理“导航栈语义统一”,因为它影响页面返回、跨模块入口和后续数据持久化,但实施前仍要先形成独立设计与迁移计划。不要一次混合实施以上四项。
## 10. 新会话首次回复必须报告
接管者完成阅读和只读检查后,先向用户报告:
1. 读到了哪些当前权威文件。
2. 当前已经完成到哪里。
3. 五个核心验证和全量合同是否仍通过。
4. Vue 覆盖是否仍为精确相等,禁用规则是否仍为零。
5. MuMu 是否在线、实际设备序列号、尺寸和 density。
6. 准备从哪个第二阶段深化项开始,以及为什么。
在这些事实没有核对前,不要一上来修改代码。
## 11. 可直接复制到新会话的提示词
```text
请全程使用中文。项目根目录是:
【替换成新电脑上的 jiapuapp 绝对路径】
开始前必须完整阅读:
1. AGENTS.md
2. docs/换机继续任务交接_2026-07-22.md
3. docs/superpowers/specs/2026-07-21-project-wide-responsive-layout-design.md
4. docs/superpowers/plans/2026-07-21-responsive-foundation-phase-1.md
5. tests/responsive-layout-coverage.json
6. styles/adaptive-frame-profiles.scss
7. docs/superpowers/plans/2026-07-21-project-wide-audit-and-expert-optimization.md
8. docs/superpowers/specs/2026-07-21-whole-product-enrichment-design.md
9. docs/superpowers/plans/2026-07-21-whole-product-enrichment-implementation.md
当前全项目响应式、G10 后续页面 MuMu 审核、两位专家评审和第一轮六个优化批次已经完成。不要重新推翻已通过设计,不要回到 G01/G10 重新开始。两位专家 Dirac 与 Leibniz 已完成工作,不要重复创建。
先运行并报告:
powershell -ExecutionPolicy Bypass -File tests/adaptive-frame-profiles-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/project-responsive-layout-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/ag-responsive-surfaces-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/compile-audit.ps1
node tests/password-policy-runtime-smoke.js
然后运行全部 tests/*.ps1,并核对:
- 实际 Vue 与 tests/responsive-layout-coverage.json 是否完全相等;
- 是否仍有 100% 100% no-repeat
- Vue 是否仍直接写 border-image-slice
- MuMu 是否在线及其实际尺寸、density。
只使用 MuMu 做视觉审核,不使用浏览器截图代替;不要启动或关闭模拟器,不要擅自修改模拟器尺寸。不要执行任何 Git 变更操作,不要删除或覆盖现有文件。
下一阶段优先评估“导航栈语义统一”。遇到真实问题时提出五个方案,自主选最优方案;修改前写失败合同,修改后必须在 MuMu 复核。先报告读到的内容、当前完成状态、测试、覆盖、MuMu 状态和下一步,不要一上来改代码。
```
## 12. 交接完成判定
新电脑满足以下条件后,才算成功接管:
- 能完整打开项目且正式资产没有缺失。
- 五个核心验证通过。
- 全部 PowerShell 合同通过,或明确列出只在新环境失败的合同及根因。
- Vue 覆盖精确相等,两个禁用扫描为零。
- HBuilderX 能把项目热更新到用户当前 MuMu。
- MuMu 截图中的 G10、T01、G01 或另一个代表页与本轮视觉基线一致。
- 接管者能准确复述唯一所有者、禁止事项和下一阶段入口,没有回退到旧 H5 审核停点。
+6 -1
View File
@@ -150,6 +150,10 @@
import { ref } from "vue";
import { onUnload } from "@dcloudio/uni-app";
import AuthPageShell from "@/components/AuthPageShell.vue";
import {
PASSWORD_POLICY_MESSAGE,
validatePassword,
} from "@/utils/validation.js";
const phone = ref("");
const password = ref("");
@@ -200,7 +204,8 @@ const clearFieldError = (field) => {
const validateForm = () => {
const nextErrors = { phone: "", password: "", confirmPassword: "" };
if (!/^1\d{10}$/.test(phone.value)) nextErrors.phone = "请输入正确手机号";
if (!password.value) nextErrors.password = "请设置密码";
const passwordResult = validatePassword(password.value);
if (!passwordResult.valid) nextErrors.password = PASSWORD_POLICY_MESSAGE;
if (!confirmPassword.value) nextErrors.confirmPassword = "请再次输入密码";
else if (password.value !== confirmPassword.value)
nextErrors.confirmPassword = "两次输入的密码不一致";
+6 -1
View File
@@ -179,6 +179,10 @@
import { ref } from "vue";
import { onUnload } from "@dcloudio/uni-app";
import AuthPageShell from "@/components/AuthPageShell.vue";
import {
PASSWORD_POLICY_MESSAGE,
validatePassword,
} from "@/utils/validation.js";
const phone = ref("");
const verificationCode = ref("");
@@ -237,7 +241,8 @@ const validateForm = () => {
if (!/^1\d{10}$/.test(phone.value)) nextErrors.phone = "请输入正确手机号";
if (!/^\d{6}$/.test(verificationCode.value))
nextErrors.verificationCode = "请输入 6 位验证码";
if (!password.value) nextErrors.password = "请设置新密码";
const passwordResult = validatePassword(password.value);
if (!passwordResult.valid) nextErrors.password = PASSWORD_POLICY_MESSAGE;
if (!confirmPassword.value) nextErrors.confirmPassword = "请再次输入新密码";
else if (password.value !== confirmPassword.value)
nextErrors.confirmPassword = "两次密码输入不一致";
+1 -1
View File
@@ -18,6 +18,7 @@
><view class="publish-field"
><textarea
v-model="content"
auto-height
maxlength="300"
placeholder="写下想对家人说的话"
placeholder-class="publish-placeholder"
@@ -95,7 +96,6 @@ onUnmounted(() => {
}
.publish-panel {
width: calc(100% - 32rpx);
min-height: min(640px, calc((100vw - 16px) * 1.48));
margin: 18rpx auto 0;
padding: 9%;
box-sizing: border-box;
+1
View File
@@ -87,6 +87,7 @@
>
<textarea
v-model="form.content"
auto-height
maxlength="1200"
placeholder="记录家训、往事或想留给后人的话"
placeholder-class="editor-placeholder"
+2
View File
@@ -118,6 +118,7 @@
</view>
<textarea
v-model="batchDescription"
auto-height
:disabled="isLocked"
maxlength="120"
placeholder="例如:2024 年春节全家团圆留影"
@@ -151,6 +152,7 @@
</view>
<textarea
:value="activePhoto.note"
auto-height
:disabled="isLocked"
maxlength="80"
placeholder="补充人物、时间或拍摄地点"
+4 -13
View File
@@ -14,11 +14,6 @@
</view>
<view class="video-status-card">
<image
class="video-status-card__skin"
src="/static/assets/modules/family/transparent/module-content-frame.png"
mode="scaleToFill"
/>
<view class="video-status-card__body">
<text class="video-status-card__eyebrow">视频 · 服务说明</text>
<text class="video-status-card__title">视频服务暂未开放</text>
@@ -45,6 +40,8 @@ const returnToFamily = () => {
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.video-status-page {
min-height: 100vh;
background: $paper;
@@ -74,19 +71,13 @@ const returnToFamily = () => {
height: 100%;
}
.video-status-card {
display: grid;
@include adaptive.adaptive-family-panel;
width: 100%;
min-height: 330rpx;
box-sizing: border-box;
text-align: center;
}
.video-status-card__skin {
grid-area: 1 / 1;
width: 100%;
height: 100%;
}
.video-status-card__body {
z-index: 1;
grid-area: 1 / 1;
padding: 70rpx 60rpx 48rpx;
}
.video-status-card__eyebrow,
+8 -19
View File
@@ -340,7 +340,7 @@
<script setup>
import { computed, nextTick, ref } from "vue";
import { onBackPress, onLoad, onShow } from "@dcloudio/uni-app";
import { onBackPress, onLoad } from "@dcloudio/uni-app";
import AppLoading from "@/components/AppLoading.vue";
import AppTabbar from "@/components/AppTabbar.vue";
import AppButton from "@/components/AppButton.vue";
@@ -365,19 +365,12 @@ const selectedGenealogyId = ref(
const listScrollCommand = ref(0);
const currentListScrollTop = ref(0);
const readPresentationState = () => {
if (typeof location !== "undefined") {
const query = new URLSearchParams(location.hash.split("?")[1] || "");
return query.get("state") || "default";
}
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
return currentPage?.options?.state || "default";
};
const syncEmptyStateFromRoute = () => {
const presentationState = readPresentationState();
const syncEmptyStateFromRoute = (query = {}) => {
const presentationState = ["empty", "loading", "error"].includes(
query?.state,
)
? query.state
: "default";
forceEmptyState.value = presentationState === "empty";
isLoading.value = presentationState === "loading";
hasError.value = presentationState === "error";
@@ -391,11 +384,7 @@ onLoad((query) => {
} else if (selectedGenealogyId.value) {
genealogyContext.setCurrentGenealogyId(selectedGenealogyId.value);
}
syncEmptyStateFromRoute();
});
onShow(() => {
syncEmptyStateFromRoute();
syncEmptyStateFromRoute(query);
});
const unreadCount = computed(
+7 -35
View File
@@ -235,8 +235,8 @@
</template>
<script setup>
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { computed, onUnmounted, reactive, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
@@ -287,48 +287,20 @@ const changeAncestorBirthDate = (event) => {
const isAncestorStep = computed(() => currentStep.value === "ancestor");
const getFlowQuery = () => {
if (typeof location !== "undefined") {
return new URLSearchParams(location.hash.split("?")[1] || "");
}
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
return currentPage?.options || {};
};
const syncFlowFromRoute = () => {
const query = getFlowQuery();
const isAncestorRoute = query.get
? query.get("step") === "ancestor"
: query.step === "ancestor";
const routeGenealogyId = query.get
? query.get("genealogyId")
: query.genealogyId;
const syncFlowFromRoute = (query = {}) => {
const isAncestorRoute = query?.step === "ancestor";
const routeGenealogyId = query?.genealogyId || "";
currentStep.value = isAncestorRoute ? "ancestor" : "create";
genealogyId.value =
routeGenealogyId || (isAncestorStep.value ? "local-created-genealogy" : "");
};
onLoad(() => {
syncFlowFromRoute();
});
onShow(() => {
syncFlowFromRoute();
});
onMounted(() => {
if (typeof window !== "undefined") {
window.addEventListener("hashchange", syncFlowFromRoute);
}
onLoad((query) => {
syncFlowFromRoute(query);
});
onUnmounted(() => {
if (submitTimer) clearTimeout(submitTimer);
if (typeof window !== "undefined") {
window.removeEventListener("hashchange", syncFlowFromRoute);
}
});
const goBack = () => {
+5 -18
View File
@@ -222,7 +222,7 @@
<script setup>
import { computed, ref } from "vue";
import { onLoad, onUnload, onShow } from "@dcloudio/uni-app";
import { onLoad, onUnload } from "@dcloudio/uni-app";
import AppLoading from "@/components/AppLoading.vue";
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
@@ -332,26 +332,13 @@ const resultFixtures = [
const inviteTarget = computed(() => resultFixtures[0]);
const syncModeFromRoute = () => {
let requestedMode = "search";
let requestedState = "";
if (typeof location !== "undefined") {
const routeParams = new URLSearchParams(location.hash.split("?")[1] || "");
requestedMode = routeParams.get("mode") || "search";
requestedState = routeParams.get("state") || "";
} else {
const pages = getCurrentPages();
const routeOptions = pages[pages.length - 1]?.options || {};
requestedMode = routeOptions.mode || "search";
requestedState = routeOptions.state || "";
}
mode.value = requestedMode === "invite" ? "invite" : "search";
if (mode.value === "search" && requestedState === "loading")
const syncModeFromRoute = (query = {}) => {
mode.value = query?.mode === "invite" ? "invite" : "search";
if (mode.value === "search" && query?.state === "loading")
searchState.value = "loading";
};
onLoad(syncModeFromRoute);
onShow(syncModeFromRoute);
onLoad((query) => syncModeFromRoute(query));
onUnload(() => {
if (searchTimer) clearTimeout(searchTimer);
});
+23 -6
View File
@@ -112,15 +112,23 @@
<view v-if="confirmation && !confirmation.approved" class="rejection-field">
<text>拒绝原因</text>
<textarea
id="g10-rejection-reason"
v-model="rejectionReason"
auto-height
maxlength="120"
placeholder="请说明需要补充或核实的信息"
@input="rejectionError = ''"
:aria-invalid="!!rejectionError"
aria-describedby="g10-rejection-error"
:focus="rejectionFocused"
@input="clearRejectionError"
/>
<text v-if="rejectionError" class="rejection-field__error">{{
rejectionError
}}</text>
<text
v-if="rejectionError"
id="g10-rejection-error"
class="rejection-field__error"
role="alert"
>{{ rejectionError }}</text
>
</view>
</AppDialog>
@@ -131,7 +139,7 @@
</template>
<script setup>
import { computed, ref } from "vue";
import { computed, nextTick, ref } from "vue";
import { onLoad, onUnload } from "@dcloudio/uni-app";
import AppDialog from "@/components/AppDialog.vue";
import AppButton from "@/components/AppButton.vue";
@@ -165,6 +173,7 @@ const errorMessage = ref("");
const confirmation = ref(null);
const rejectionReason = ref("");
const rejectionError = ref("");
const rejectionFocused = ref(false);
const helpVisible = ref(false);
const feedbackVisible = ref(false);
const feedbackMessage = ref("");
@@ -225,12 +234,14 @@ onLoad(loadApplications);
const confirmAudit = (item, approved) => {
rejectionReason.value = "";
rejectionError.value = "";
rejectionFocused.value = false;
confirmation.value = { item, approved };
};
const closeDialog = () => {
confirmation.value = null;
rejectionReason.value = "";
rejectionError.value = "";
rejectionFocused.value = false;
helpVisible.value = false;
};
const showFeedback = (message) => {
@@ -242,11 +253,17 @@ const showFeedback = (message) => {
feedbackTimer = null;
}, 1800);
};
const applyAudit = () => {
const clearRejectionError = () => {
rejectionError.value = "";
};
const applyAudit = async () => {
const current = confirmation.value;
if (!current) return;
if (!current.approved && !rejectionReason.value.trim()) {
rejectionError.value = "请填写拒绝原因,方便申请人补充资料";
rejectionFocused.value = false;
await nextTick();
rejectionFocused.value = true;
return;
}
current.item.status = current.approved ? "APPROVED" : "REJECTED";
@@ -223,7 +223,6 @@ const saveSettings = () => {
@include adaptive.adaptive-genealogy-state-panel;
z-index: 2;
width: calc(100% - 32rpx);
min-height: min(620px, calc((100vw - 16px) * 1.43));
margin: 18rpx auto 0;
padding: 70rpx 8%;
box-sizing: border-box;
-1
View File
@@ -251,7 +251,6 @@ const savePoems = () => {
@include adaptive.adaptive-genealogy-state-panel;
z-index: 2;
width: calc(100% - 32rpx);
min-height: min(650px, calc((100vw - 16px) * 1.5));
margin: 18rpx auto 0;
padding: 76rpx 8%;
box-sizing: border-box;
+10 -3
View File
@@ -27,6 +27,10 @@ import AppButton from "@/components/AppButton.vue";
import AppToast from "@/components/AppToast.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
PASSWORD_POLICY_MESSAGE,
validatePassword,
} from "@/utils/validation.js";
const passwordForm = reactive({ current: "", next: "", confirm: "" });
const passwordVisible = reactive({ current: false, next: false, confirm: false });
@@ -40,14 +44,17 @@ const passwordFields = [
const toastVisible = ref(false); const toastMessage = ref(""); let timer = null;
const showToast = (message) => { toastMessage.value = message; toastVisible.value = true; clearTimeout(timer); timer = setTimeout(() => (toastVisible.value = false), 1800); };
const togglePassword = (key) => { passwordVisible[key] = !passwordVisible[key]; };
const validatePassword = () => {
const validateForm = () => {
errors.current = passwordForm.current ? "" : "请输入当前密码";
errors.next = /^(?=.*[A-Za-z])(?=.*\d).{8,32}$/.test(passwordForm.next) ? "" : "新密码需为 8–32 位,并同时包含字母和数字";
const nextPassword = validatePassword(passwordForm.next);
errors.next = nextPassword.valid ? "" : PASSWORD_POLICY_MESSAGE;
if (!errors.next && passwordForm.next === passwordForm.current)
errors.next = "新密码不能与当前密码相同";
errors.confirm = !passwordForm.confirm ? "请再次输入新密码" : passwordForm.confirm !== passwordForm.next ? "两次输入的新密码不一致" : "";
return !Object.values(errors).some(Boolean);
};
const savePassword = () => {
if (!validatePassword() || passwordState.value === "saving") return;
if (!validateForm() || passwordState.value === "saving") return;
passwordState.value = "saving";
timer = setTimeout(() => { passwordState.value = "ready"; passwordForm.current = ""; passwordForm.next = ""; passwordForm.confirm = ""; showToast("密码已修改,请妥善保管新密码"); }, 500);
};
+9 -18
View File
@@ -92,6 +92,7 @@
</template>
<script setup>
import { onLoad } from "@dcloudio/uni-app";
import { computed, onUnmounted, ref } from "vue";
import AppButton from "@/components/AppButton.vue";
import AppToast from "@/components/AppToast.vue";
@@ -104,22 +105,7 @@ const people = [
{ id: 3, name: "汤文清", role: "青年代表", generation: 19 },
];
const query = (() => {
if (typeof location !== "undefined")
return Object.fromEntries(
new URLSearchParams(location.hash.split("?")[1] || ""),
);
const pages = getCurrentPages();
return pages[pages.length - 1]?.options || {};
})();
const peopleState = ref(
query.state === "empty"
? "empty"
: query.state === "error"
? "error"
: "ready",
);
const peopleState = ref("ready");
const keywordInput = ref("");
const keyword = ref("");
const toastVisible = ref(false);
@@ -157,6 +143,12 @@ const showCreateNotice = () => {
uni.navigateTo({ url: "/pages/records/r02-person-detail?mode=create" });
};
onLoad((query) => {
peopleState.value = ["empty", "error"].includes(query.state)
? query.state
: "ready";
});
onUnmounted(() => {
if (toastTimer) clearTimeout(toastTimer);
});
@@ -219,10 +211,9 @@ onUnmounted(() => {
@include adaptive.adaptive-records-person;
display: flex;
width: 100%;
height: clamp(92px, 190rpx, 108px);
min-height: clamp(92px, 190rpx, 108px);
align-items: center;
margin-top: 12px;
padding: 16% 10%;
}
.person-card:first-child {
margin-top: 0;
+69 -9
View File
@@ -20,6 +20,15 @@
<text>主支 · 1214 </text>
</view>
<view class="tree-toolbar__actions">
<text
v-if="treeState === 'tree' && treeHasDrifted && selected"
class="tree-recenter"
role="button"
aria-label="回到当前成员"
hover-class="tree-action--pressed"
@click="recenterSelectedMember"
>回到当前</text
>
<text @click="treeState = 'landscape'">阅读提示</text>
<text @click="toRelationship">关系维护</text>
</view>
@@ -51,18 +60,23 @@
</view>
</view>
<view v-if="treeState === 'tree'" class="lineage-pan-cue">
<text>同代分支可左右查看</text>
</view>
<scroll-view
class="tree-scroll"
:class="{ 'tree-scroll--lineage': treeState === 'tree' }"
scroll-x
:scroll-left="initialScrollLeft"
:scroll-left="treeState === 'tree' ? treeScrollLeft : 0"
:show-scrollbar="false"
:style="treeScrollStyle"
@scroll="handleTreeScroll"
>
<view
class="tree-canvas"
:class="{ 'tree-canvas--state': treeState !== 'tree' }"
:style="treeMetricsStyle"
:style="treeState === 'tree' ? treeMetricsStyle : undefined"
>
<AppLoading
v-if="treeState === 'loading'"
@@ -71,10 +85,6 @@
/>
<template v-else-if="treeState === 'tree'">
<view class="lineage-pan-cue">
<text>同代分支可左右查看</text>
</view>
<view
v-for="connector in lineageConnectors"
:key="connector.id"
@@ -85,6 +95,7 @@
<view
v-for="member in layoutMembers"
:key="member.id"
:id="`tree-member-${member.id}`"
class="member-node"
:class="{ 'member-node--selected': selected?.id === member.id }"
:style="nodeGridStyle(member)"
@@ -166,7 +177,7 @@
<script setup>
import { computed, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { onLoad, onUnload } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
@@ -176,7 +187,12 @@ import { genealogyContext } from "@/utils/genealogy-context.js";
const genealogyId = ref("");
const treeState = ref("loading");
const selected = ref(null);
const initialScrollLeft = ref(90);
const treeScrollLeft = ref(90);
const currentTreeScrollLeft = ref(90);
const centeredTreeScrollLeft = ref(90);
const treeHasDrifted = ref(false);
let ignoreTreeScroll = false;
let recenterTimer = null;
const GRID_UNIT = 5;
const NODE_HALF_HEIGHT = 47;
const MEMBER_GAP = 250;
@@ -409,6 +425,46 @@ onLoad((query) => {
treeState.value = "tree";
});
onUnload(() => {
if (recenterTimer) clearTimeout(recenterTimer);
});
const handleTreeScroll = (event) => {
if (ignoreTreeScroll || treeState.value !== "tree") return;
const scrollLeft = Number(event?.detail?.scrollLeft || 0);
currentTreeScrollLeft.value = scrollLeft;
treeHasDrifted.value = Math.abs(scrollLeft - centeredTreeScrollLeft.value) > 80;
};
const recenterSelectedMember = async () => {
if (!selected.value) return;
ignoreTreeScroll = true;
const [scrollRect, nodeRect] = await new Promise((resolve) => {
const query = uni.createSelectorQuery();
query.select(".tree-scroll").boundingClientRect();
query
.select(`#tree-member-${selected.value.id}`)
.boundingClientRect();
query.exec(resolve);
});
if (scrollRect && nodeRect) {
const centeredScrollLeft = Math.max(
0,
currentTreeScrollLeft.value +
nodeRect.left + nodeRect.width / 2 - (scrollRect.left + scrollRect.width / 2),
);
treeScrollLeft.value = centeredScrollLeft;
currentTreeScrollLeft.value = centeredScrollLeft;
centeredTreeScrollLeft.value = centeredScrollLeft;
}
treeHasDrifted.value = false;
if (recenterTimer) clearTimeout(recenterTimer);
recenterTimer = setTimeout(() => {
ignoreTreeScroll = false;
recenterTimer = null;
}, 320);
};
const nodeGridStyle = (member) => ({
gridColumn: `${member.x / 5 + 1}`,
gridRow: `${member.y / 5 + 1}`,
@@ -484,6 +540,9 @@ const toAddRelative = () =>
min-height: 0;
overflow-y: auto;
}
.tree-action--pressed {
opacity: 0.72;
}
.tree-stage--lineage {
display: grid;
grid-template-columns: 190rpx minmax(0, 1fr);
@@ -503,6 +562,7 @@ const toAddRelative = () =>
border-right: 1rpx solid rgba(143, 108, 63, 0.2);
}
.tree-scroll--lineage {
grid-area: 1 / 2;
min-width: 0;
}
.tree-canvas {
@@ -514,7 +574,7 @@ const toAddRelative = () =>
width: calc(100vw - 32rpx);
}
.lineage-pan-cue {
grid-area: 1 / 1 / -1 / -1;
grid-area: 1 / 2;
align-self: start;
justify-self: end;
z-index: 3;
+1 -1
View File
@@ -141,7 +141,7 @@ const toTree = () => uni.navigateBack();
.member-context { @include adaptive.adaptive-tree-field; width: calc(100% - 32rpx); margin: 18rpx auto 0; padding: 16rpx 24rpx; }
.member-context text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
.member-context text:first-child { color: $ink; font-size: 26rpx; font-weight: 700; }
.member-panel { @include adaptive.adaptive-tree-panel; width: calc(100% - 32rpx); min-height: min(620px, calc((100vw - 16px) * 1.42)); margin: 16rpx auto 0; padding: 9% 8%; }
.member-panel { @include adaptive.adaptive-tree-panel; width: calc(100% - 32rpx); margin: 16rpx auto 0; padding: 9% 8%; }
.member-panel > .app-loading { margin-top: 30%; }
.member-heading { display: flex; align-items: center; gap: 20rpx; }
.member-heading__seal { display: flex; width: 74rpx; height: 74rpx; flex: 0 0 74rpx; align-items: center; justify-content: center; background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png") center / contain no-repeat; }
+1 -1
View File
@@ -224,7 +224,7 @@ const discardAndBack = () => {
.add-relative-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.add-relative-page__header { z-index: 3; }
.add-relative-panel { @include adaptive.adaptive-tree-panel; z-index: 2; width: calc(100% - 32rpx); min-height: min(680px, calc((100vw - 16px) * 1.5)); margin: 18rpx auto 28rpx; padding: 7.5% 8%; }
.add-relative-panel { @include adaptive.adaptive-tree-panel; z-index: 2; width: calc(100% - 32rpx); margin: 18rpx auto 28rpx; padding: 7.5% 8%; }
.form-eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
.form-title { display: block; margin-top: 10rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; line-height: 1.35; }
.form-copy, .form-note { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.5; }
+1 -1
View File
@@ -173,7 +173,7 @@ const discardAndBack = () => {
.edit-member-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.edit-member-page__header { z-index: 3; }
.edit-member-panel { @include adaptive.adaptive-tree-panel; z-index: 2; width: calc(100% - 32rpx); min-height: min(690px, calc((100vw - 16px) * 1.52)); margin: 18rpx auto 28rpx; padding: 7.5% 8%; }
.edit-member-panel { @include adaptive.adaptive-tree-panel; z-index: 2; width: calc(100% - 32rpx); margin: 18rpx auto 28rpx; padding: 7.5% 8%; }
.form-eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
.form-title { display: block; margin-top: 10rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; line-height: 1.35; }
.form-copy, .form-note { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.5; }
+1 -1
View File
@@ -192,7 +192,7 @@ const handleResultAction = () => {
.relationship-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.relationship-page__header { z-index: 3; }
.relationship-panel { @include adaptive.adaptive-tree-panel; z-index: 2; width: calc(100% - 32rpx); min-height: min(650px, calc((100vw - 16px) * 1.46)); margin: 18rpx auto 28rpx; padding: 7.5% 8%; }
.relationship-panel { @include adaptive.adaptive-tree-panel; z-index: 2; width: calc(100% - 32rpx); margin: 18rpx auto 28rpx; padding: 7.5% 8%; }
.form-eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
.form-title { display: block; margin-top: 10rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; line-height: 1.35; }
.form-copy, .form-note { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.5; }
+1
View File
@@ -246,6 +246,7 @@
105 150 fill,
18rpx 20rpx
);
padding: 28rpx 64rpx;
}
@mixin adaptive-tree-panel {
+5
View File
@@ -14,6 +14,11 @@ page {
}
}
view:not(.page-header-slot):has(> .page-header-slot) {
position: relative;
z-index: 31;
}
.page-shell {
min-height: 100vh;
box-sizing: border-box;
+2 -1
View File
@@ -48,7 +48,8 @@ foreach ($required in @(
'const prepareLogin = () => uni.redirectTo({ url:',
'/pages/auth/a01-entry',
'if (!/^1\d{10}$/.test(phone.value))',
'if (!password.value)',
'validatePassword(password.value)',
'PASSWORD_POLICY_MESSAGE',
'if (!confirmPassword.value)',
'if (password.value !== confirmPassword.value)'
)) {
+2 -1
View File
@@ -50,7 +50,8 @@ foreach ($required in @(
'/pages/auth/a01-entry',
'if (!/^1\d{10}$/.test(phone.value))',
'if (!/^\d{6}$/.test(verificationCode.value))',
'if (!password.value)',
'validatePassword(password.value)',
'PASSWORD_POLICY_MESSAGE',
'if (password.value !== confirmPassword.value)',
'adaptive.adaptive-feedback-toast;',
'top: calc(var(--status-bar-height, 0px) + 24rpx);',
@@ -8,7 +8,9 @@ foreach ($token in @(
':compact="compactActions && showCancel"',
'@use "../styles/adaptive-frame-profiles.scss" as adaptive;',
'@include adaptive.adaptive-auth-dialog;',
'max-height: calc(var(--app-viewport-height, 100vh) - 160rpx);',
'padding: calc(40rpx + env(safe-area-inset-top)) 40rpx calc(40rpx + env(safe-area-inset-bottom));',
'max-height: calc(var(--app-viewport-height, 100vh) - 80rpx - env(safe-area-inset-top) - env(safe-area-inset-bottom));',
'min-height: 0;',
'margin-top: 26rpx;'
)) {
if (-not $dialog.Contains($token)) { throw "AppDialog responsive contract missing: $token" }
+1
View File
@@ -37,6 +37,7 @@ foreach ($file in $files) {
$source = [System.IO.File]::ReadAllText($file.FullName, [System.Text.Encoding]::UTF8)
foreach ($styleMatch in [regex]::Matches($source, '(?s)<style\b[^>]*>(?<css>.*?)</style>')) {
$css = [regex]::Replace($styleMatch.Groups['css'].Value, '(?s)/\*.*?\*/', '')
$css = [regex]::Replace($css, '(?m)^\s*@(use|forward|import)\b[^;]+;\s*', '')
foreach ($rule in [regex]::Matches($css, '(?s)(?<selector>[^{}]+)\{(?<body>[^{}]*)\}')) {
$selector = (($rule.Groups['selector'].Value -replace '(?s)^.*@media[^\{]*', '') -replace '\s+', ' ').Trim()
$body = $rule.Groups['body'].Value
File diff suppressed because one or more lines are too long
+165 -155
View File
@@ -1,157 +1,167 @@
[
{
"file": "components/AppDialog.vue",
"selector": ".app-dialog-layer",
"reason": "全屏弹窗遮罩"
},
{
"file": "components/AppToast.vue",
"selector": ".app-toast",
"reason": "跨页面轻提示"
},
{
"file": "components/PageHeader.vue",
"selector": ".page-header",
"reason": "用户指定的固定顶部导航栏"
},
{
"file": "components/AppTabbar.vue",
"selector": ".app-tabbar",
"reason": "用户指定的固定底部导航栏"
},
{
"file": "components/PageHeader.vue",
"selector": ".header-texture",
"reason": "固定顶部栏内部纯装饰纹理层"
},
{
"file": "components/PageHeader.vue",
"selector": ".header-hall",
"reason": "固定顶部栏内部纯装饰祠堂层"
},
{
"file": "components/GenealogyPageBackground.vue",
"selector": ".genealogy-page-background",
"reason": "不参与排版且覆盖视口的独立家谱背景层"
},
{
"file": "components/GenealogyPageBackground.vue",
"selector": ".genealogy-page-background__art",
"reason": "独立家谱背景层内部的底部背景画面"
},
{
"file": "components/ModulePageBackground.vue",
"selector": ".module-page-background",
"reason": "不参与排版且覆盖视口的独立模块背景层"
},
{
"file": "components/ModulePageBackground.vue",
"selector": ".module-page-background__image",
"reason": "独立模块背景层内部的底部背景画面"
},
{
"file": "pages/family/f08-album-detail.vue",
"selector": ".album-photo-tile",
"reason": "仅作为照片裁切层与底部说明叠层的精确局部边界"
},
{
"file": "pages/family/f08-album-detail.vue",
"selector": ".album-hero-photo, .album-photo-tile__image",
"reason": "在固定比例照片格内执行裁切的媒体画面层"
},
{
"file": "pages/family/f08-album-detail.vue",
"selector": ".album-photo-tile__caption",
"reason": "依附照片格底部的局部说明叠层"
},
{
"file": "pages/family/f08-album-detail.vue",
"selector": ".album-preview",
"reason": "用户触发的全屏照片预览层"
},
{
"file": "pages/family/f09-media-upload.vue",
"selector": ".media-photo-tile",
"reason": "仅作为照片缩略图角标与删除操作的精确局部边界"
},
{
"file": "pages/family/f09-media-upload.vue",
"selector": ".media-photo-order, .media-photo-current, .media-photo-status",
"reason": "依附照片缩略图的顺序与状态角标"
},
{
"file": "pages/family/f09-media-upload.vue",
"selector": ".media-photo-remove",
"reason": "依附照片缩略图右上角的删除操作"
},
{
"file": "pages/tree/t01-tree-overview.vue",
"selector": ".member-sheet",
"reason": "用户选择节点后显示的固定底部详情抽屉"
},
{
"file": "pages/tree/t01-tree-overview.vue",
"selector": ".member-sheet__skin",
"reason": "固定底部详情抽屉内部的装饰框层"
},
{
"file": "pages/auth/a01-entry.vue",
"selector": ".feedback-toast",
"reason": "登录页面跨内容轻提示"
},
{
"file": "pages/auth/a01-entry.vue",
"selector": ".verification-layer",
"reason": "用户触发的验证码弹窗遮罩"
},
{
"file": "pages/auth/a04-register.vue",
"selector": ".feedback-toast",
"reason": "注册页面跨内容轻提示"
},
{
"file": "pages/auth/a05-reset-password.vue",
"selector": ".feedback-toast",
"reason": "重置密码页面跨内容轻提示"
},
{
"file": "pages/auth/a05-reset-password.vue",
"selector": ".success-layer",
"reason": "用户触发的重置成功弹窗遮罩"
},
{
"file": "pages/auth/a06-auth-status.vue",
"selector": ".recovery-layer",
"reason": "用户触发的账号恢复弹窗遮罩"
},
{
"file": "pages/genealogy/g01-my-genealogies.vue",
"selector": ".add-dialog-layer",
"reason": "用户触发的固定底部添加家谱弹层"
},
{
"file": "pages/genealogy/g01-my-genealogies.vue",
"selector": ".genealogy-switcher-layer",
"reason": "用户触发的固定家谱切换弹窗遮罩"
},
{
"file": "pages/genealogy/g10-application-review.vue",
"selector": ".review-feedback",
"reason": "审核操作后跨内容显示的固定轻提示"
},
{
"file": "pages/genealogy/g03-create-genealogy.vue",
"selector": ".duplicate-reminder-layer, .flow-success-layer",
"reason": "用户触发的重复提醒与成功弹窗遮罩"
},
{
"file": "pages/genealogy/g11-genealogy-settings.vue",
"selector": ".settings-feedback",
"reason": "保存设置后跨内容显示的固定轻提示"
},
{
"file": "pages/genealogy/g12-generation-poems.vue",
"selector": ".poem-feedback",
"reason": "保存字辈后跨内容显示的固定轻提示"
}
{
"file": "components/PageHeader.vue",
"selector": ".page-header",
"reason": "用户指定的固定顶部导航栏"
},
{
"file": "components/AppTabbar.vue",
"selector": ".app-tabbar",
"reason": "用户指定的固定底部导航栏"
},
{
"file": "components/PageHeader.vue",
"selector": ".header-texture",
"reason": "固定顶部栏内部纯装饰纹理层"
},
{
"file": "components/PageHeader.vue",
"selector": ".header-hall",
"reason": "固定顶部栏内部纯装饰祠堂层"
},
{
"file": "components/GenealogyPageBackground.vue",
"selector": ".genealogy-page-background",
"reason": "不参与排版且覆盖视口的独立家谱背景层"
},
{
"file": "components/GenealogyPageBackground.vue",
"selector": ".genealogy-page-background__art",
"reason": "独立家谱背景层内部的底部背景画面"
},
{
"file": "components/ModulePageBackground.vue",
"selector": ".module-page-background",
"reason": "不参与排版且覆盖视口的独立模块背景层"
},
{
"file": "components/ModulePageBackground.vue",
"selector": ".module-page-background__image",
"reason": "独立模块背景层内部的底部背景画面"
},
{
"file": "pages/family/f08-album-detail.vue",
"selector": ".album-photo-tile",
"reason": "仅作为照片裁切层与底部说明叠层的精确局部边界"
},
{
"file": "pages/family/f08-album-detail.vue",
"selector": ".album-hero-photo, .album-photo-tile__image",
"reason": "在固定比例照片格内执行裁切的媒体画面"
},
{
"file": "pages/family/f08-album-detail.vue",
"selector": ".album-photo-tile__caption",
"reason": "依附照片格底部的局部说明叠层"
},
{
"file": "pages/family/f08-album-detail.vue",
"selector": ".album-preview",
"reason": "用户触发的全屏照片预览层"
},
{
"file": "pages/family/f09-media-upload.vue",
"selector": ".media-photo-tile",
"reason": "仅作为照片缩略图角标与删除操作的精确局部边界"
},
{
"file": "pages/family/f09-media-upload.vue",
"selector": ".media-photo-order, .media-photo-current, .media-photo-status",
"reason": "依附照片缩略图的顺序与状态角标"
},
{
"file": "pages/family/f09-media-upload.vue",
"selector": ".media-photo-remove",
"reason": "依附照片缩略图右上角的删除操作"
},
{
"file": "pages/tree/t01-tree-overview.vue",
"selector": ".member-sheet",
"reason": "用户选择节点后显示的固定底部详情抽屉"
},
{
"file": "pages/tree/t01-tree-overview.vue",
"selector": ".member-sheet__skin",
"reason": "固定底部详情抽屉内部的装饰框层"
},
{
"file": "pages/auth/a01-entry.vue",
"selector": ".feedback-toast",
"reason": "登录页面跨内容轻提示"
},
{
"file": "pages/auth/a01-entry.vue",
"selector": ".verification-layer",
"reason": "用户触发的验证码弹窗遮罩"
},
{
"file": "pages/auth/a04-register.vue",
"selector": ".feedback-toast",
"reason": "注册页面跨内容轻提示"
},
{
"file": "pages/auth/a05-reset-password.vue",
"selector": ".feedback-toast",
"reason": "重置密码页面跨内容轻提示"
},
{
"file": "pages/auth/a05-reset-password.vue",
"selector": ".success-layer",
"reason": "用户触发的重置成功弹窗遮罩"
},
{
"file": "pages/auth/a06-auth-status.vue",
"selector": ".recovery-layer",
"reason": "用户触发的账号恢复弹窗遮罩"
},
{
"file": "pages/genealogy/g01-my-genealogies.vue",
"selector": ".add-dialog-layer",
"reason": "用户触发的固定底部添加家谱弹层"
},
{
"file": "pages/genealogy/g01-my-genealogies.vue",
"selector": ".genealogy-switcher-layer",
"reason": "用户触发的固定家谱切换弹窗遮罩"
},
{
"file": "pages/genealogy/g10-application-review.vue",
"selector": ".review-feedback",
"reason": "审核操作后跨内容显示的固定轻提示"
},
{
"file": "pages/genealogy/g03-create-genealogy.vue",
"selector": ".duplicate-reminder-layer, .flow-success-layer",
"reason": "用户触发的重复提醒与成功弹窗遮罩"
},
{
"file": "pages/genealogy/g11-genealogy-settings.vue",
"selector": ".settings-feedback",
"reason": "保存设置后跨内容显示的固定轻提示"
},
{
"file": "pages/genealogy/g12-generation-poems.vue",
"selector": ".poem-feedback",
"reason": "保存字辈后跨内容显示的固定轻提示"
},
{
"file": "components/AppDialog.vue",
"selector": ".app-dialog-layer",
"reason": "全屏弹窗遮罩层"
},
{
"file": "components/AppToast.vue",
"selector": ".app-toast",
"reason": "跨页面轻提示"
},
{
"file": "components/AuthPageShell.vue",
"selector": ".auth-shell__header",
"reason": "为顶部品牌装饰建立局部定位上下文"
},
{
"file": "components/AuthPageShell.vue",
"selector": ".auth-shell__seal",
"reason": "仅定位顶部品牌区内的固定比例印章装饰"
}
]
+1 -1
View File
@@ -14,7 +14,7 @@ foreach ($file in $files) {
$relative = $file.FullName.Substring($root.Length + 1).Replace('\', '/')
$source = [System.IO.File]::ReadAllText($file.FullName, [System.Text.Encoding]::UTF8)
foreach ($styleMatch in [regex]::Matches($source, '(?s)<style\b[^>]*>(?<css>.*?)</style>')) {
$css = $styleMatch.Groups['css'].Value
$css = [regex]::Replace($styleMatch.Groups['css'].Value, '(?m)^\s*@(use|forward|import)\b[^;]+;\s*', '')
foreach ($rule in [regex]::Matches($css, '(?s)(?<selector>[^{}]+)\{(?<body>[^{}]*)\}')) {
$body = [regex]::Replace($rule.Groups['body'].Value, '(?s)/\*.*?\*/', '')
$positionMatches = [regex]::Matches($body, '(?im)(?<![-\w])position\s*:\s*(?<value>[^;{}]+?)\s*;')
+12 -3
View File
@@ -1,16 +1,25 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
$page = [System.IO.File]::ReadAllText((Join-Path $root 'pages/family/f01-family-feed.vue'), [System.Text.Encoding]::UTF8)
$profiles = [System.IO.File]::ReadAllText((Join-Path $root 'styles/adaptive-frame-profiles.scss'), [System.Text.Encoding]::UTF8)
foreach ($expected in @(
'box-sizing: border-box',
'width: 514rpx',
'background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png") center / 100% 100% no-repeat',
'background: url("/static/assets/modules/family/transparent/f01-family-letter-card.png") center / 100% 100% no-repeat',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / 100% 100% no-repeat'
'@use "../../styles/adaptive-frame-profiles.scss" as adaptive;',
'@include adaptive.adaptive-scroll-button(secondary);',
'@include adaptive.adaptive-family-letter;',
'@include adaptive.adaptive-scroll-button(primary);'
)) {
if (-not $page.Contains($expected)) { throw "F01 document-flow contract missing: $expected" }
}
foreach ($expected in @(
'@mixin adaptive-family-letter',
'"/static/assets/modules/family/transparent/f01-family-letter-card.png"'
)) {
if (-not $profiles.Contains($expected)) { throw "Adaptive profile contract missing: $expected" }
}
if ($page -match '100%\s+100%\s+no-repeat|border-image-slice\s*:') { throw 'F01 must consume adaptive frame geometry without page-local stretching or slicing' }
if ($page -match '<image\s+(?:[^>]*\s)?class="(?:feed-shortcuts__skin|feed-card__skin)"|feed-state-card\s*>\s*<image|feed-action[^>]*>\s*<image') { throw 'F01 decorative skins must be container backgrounds' }
if ($page -match 'position\s*:') { throw 'F01 must keep ordinary content in document flow' }
+4 -2
View File
@@ -1,19 +1,21 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$page = Get-Content -LiteralPath (Join-Path $root 'pages/family/f01-family-feed.vue') -Raw -Encoding utf8
$profiles = Get-Content -LiteralPath (Join-Path $root 'styles/adaptive-frame-profiles.scss') -Raw -Encoding utf8
function Assert-Match([string]$Pattern, [string]$Message) {
if ($page -notmatch $Pattern) { throw $Message }
}
Assert-Match 'modules/family/transparent/f01-family-letter-card\.png' 'F01 must use its dedicated transparent family-letter card.'
Assert-Match '@include\s+adaptive\.adaptive-family-letter\s*;' 'F01 must consume its dedicated adaptive family-letter profile.'
if ($profiles -notmatch '(?s)@mixin\s+adaptive-family-letter\s*\{.*?modules/family/transparent/f01-family-letter-card\.png') { throw 'The adaptive profile owner must retain F01 dedicated family-letter artwork.' }
if ($page -match 'application-status-card\.png') { throw 'F01 must not reuse the application status card.' }
Assert-Match 'class="feed-card__title"' 'F01 must expose an explicit feed title hierarchy.'
Assert-Match 'class="feed-card__meta"' 'F01 must expose category and time as secondary metadata.'
Assert-Match 'class="feed-card__summary"' 'F01 must expose feed summary copy.'
Assert-Match 'class="feed-card__author"' 'F01 must expose the author as tertiary information.'
Assert-Match '(?s)\.feed-shortcut\s*\{[^}]*min-height:\s*44px;' 'F01 shortcuts must preserve a 44 CSS px touch height.'
Assert-Match 'a01-scroll-secondary-v3\.png' 'F01 shortcuts must share one visible navigation skin.'
Assert-Match '@include\s+adaptive\.adaptive-scroll-button\(secondary\)\s*;' 'F01 shortcuts must share the adaptive secondary navigation profile.'
Assert-Match '(?s)\.feed-shortcuts\s*\{[^}]*grid-template-columns:\s*repeat\(4,\s*minmax\(0,\s*1fr\)\);' 'F01 shortcuts must use one four-column equal-width navigation strip.'
if ($page -match 'position\s*:') { throw 'F01 must keep ordinary content in document flow.' }
+6 -3
View File
@@ -3,12 +3,15 @@ $root = Split-Path $PSScriptRoot -Parent
$page = [System.IO.File]::ReadAllText((Join-Path $root 'pages/family/f02-publish-feed.vue'), [System.Text.Encoding]::UTF8)
foreach ($expected in @(
'background: url("/static/assets/modules/family/transparent/module-content-frame.png") center / 100% 100% no-repeat',
'background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat',
'min-height: min(640px, calc((100vw - 16px) * 1.48))'
'@include adaptive.adaptive-family-content;',
'@include adaptive.adaptive-family-field;'
)) {
if (-not $page.Contains($expected)) { throw "F02 document-flow contract missing: $expected" }
}
if ($page -notmatch '(?s)<textarea[^>]*auto-height') { throw 'F02 publish textarea must grow from its content' }
$style = [regex]::Match($page, '(?s)<style[^>]*>(.*?)</style>').Groups[1].Value
$panelStyle = [regex]::Match($style, '(?ms)^\s*\.publish-panel\s*\{(?<body>.*?)\}').Groups['body'].Value
if ($panelStyle -match '\bmin-height\s*:') { throw 'F02 publish panel height must be driven by its current content' }
if ($page -match '<image\s+(?:[^>]*\s)?class="publish-panel__skin"|publish-field[^>]*>\s*<image') { throw 'F02 decorative frames must be container backgrounds' }
if ($page -match 'position\s*:') { throw 'F02 must keep panel, form, result, and field in document flow' }
+5 -4
View File
@@ -22,6 +22,7 @@ foreach ($expected in @(
'v-model="form.title"',
'v-model="form.category"',
'v-model="form.content"',
'auto-height',
'fieldErrors.title',
'fieldErrors.category',
'fieldErrors.content',
@@ -41,10 +42,8 @@ foreach ($expected in @(
'ModulePageBackground',
'AppLoading',
'AppButton',
'border-image-source: url("/static/assets/modules/family/transparent/module-content-frame.png");',
'border-image-slice: 72 fill;',
'border-image-width: 64rpx;',
'border-image-repeat: stretch;'
'@include adaptive.adaptive-family-panel;',
'@include adaptive.adaptive-family-field;'
)) {
Assert-Contains $page $expected "F06 dedicated editor contract missing: $expected"
}
@@ -53,6 +52,8 @@ foreach ($forbidden in @('<template><ModulePage', 'import ModulePage from', 'pag
if ($page.Contains($forbidden)) { throw "F06 retains forbidden dependency: $forbidden" }
}
if ($page -match 'border-image-slice\s*:') { throw 'F06 must consume border-image geometry from the adaptive profile owner' }
if ($page -match 'position\s*:') { throw 'F06 must keep editor content in document flow' }
if ($catalog -match '(?m)^\s*f06\s*:') { throw 'F06 obsolete page-catalog owner must be removed' }
+3
View File
@@ -26,6 +26,8 @@ foreach ($expected in @(
'class="media-photo-editor"',
'class="media-photo-editor__thumb"',
'class="media-photo-editor__position"',
'<textarea',
'auto-height',
'const MAX_PHOTOS = 9;',
'const selectMockPhotos = () =>',
'const selectPhoto = (index) =>',
@@ -71,6 +73,7 @@ foreach ($requiredPosition in @(
if ($page -notmatch $requiredPosition) { throw "F09 required thumbnail overlay boundary missing: $requiredPosition" }
}
if ([regex]::Matches($page, 'position\s*:').Count -ne 3) { throw 'F09 must keep only thumbnail badges and their local photo boundary positioned' }
if ([regex]::Matches($page, '<textarea(?=[^>]*\sauto-height(?:\s|>))').Count -ne 2) { throw 'F09 batch and per-photo descriptions must both grow with content' }
if ($catalog -match '(?m)^\s*f09\s*:') {
throw 'F09 obsolete page-catalog owner must be removed'
+5 -3
View File
@@ -19,12 +19,14 @@ foreach ($expected in @(
'/pages/family/f01-family-feed',
'ModulePageBackground',
'PageHeader',
'AppButton'
'AppButton',
'@use "../../styles/adaptive-frame-profiles.scss" as adaptive;',
'@include adaptive.adaptive-family-panel;'
)) {
Assert-Contains $page $expected "F10 contract missing: $expected"
}
foreach ($forbidden in @('<ModulePage page-id="f10"', 'import ModulePage from')) {
foreach ($forbidden in @('<ModulePage page-id="f10"', 'import ModulePage from', 'class="video-status-card__skin"', 'module-content-frame.png')) {
if ($page.Contains($forbidden)) { throw "F10 retains forbidden dependency: $forbidden" }
}
@@ -32,7 +34,7 @@ if ($page -match '(?m)^\s*position\s*:\s*relative\s*;') {
throw 'F10 ordinary page content must not use position: relative'
}
foreach ($selector in @('.video-status-lead text', '.video-status-card__body')) {
foreach ($selector in @('.video-status-lead text')) {
$escapedSelector = [regex]::Escape($selector)
if ($page -notmatch "(?s)$escapedSelector\s*\{[^}]*z-index\s*:\s*1\s*;") {
throw "F10 grid content must render above its decorative skin: $selector"
@@ -5,8 +5,11 @@ $editor = [System.IO.File]::ReadAllText((Join-Path $root 'pages/family/f06-artic
if ($publish -match 'position\s*:') { throw 'F02 must keep its editor in document flow' }
if ($editor -match 'position\s*:') { throw 'F06 must keep its editor in document flow' }
if (-not $publish.Contains('background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat')) { throw 'F02 field frame must be a container background' }
if (-not $editor.Contains('background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat')) { throw 'F06 field frame must be a container background' }
if (-not $publish.Contains('@include adaptive.adaptive-family-field;')) { throw 'F02 field frame must come from the adaptive profile owner' }
if (-not $editor.Contains('@include adaptive.adaptive-family-field;')) { throw 'F06 field frame must come from the adaptive profile owner' }
if ($publish -notmatch '(?s)<textarea[^>]*\sauto-height(?:\s|>)') { throw 'F02 long-form textarea must grow with its content' }
if ($editor -notmatch '(?s)<textarea[^>]*\sauto-height(?:\s|>)') { throw 'F06 long-form textarea must grow with its content' }
if ($publish -match '100%\s+100%\s+no-repeat' -or $editor -match '100%\s+100%\s+no-repeat') { throw 'Family editors must not stretch decorative backgrounds' }
if ($editor -match '<view[^>]+class="editor-control[^>]*>\s*<image') { throw 'F06 control must not retain a positioned decorative image' }
Write-Output 'FAMILY-EDITORS-DOCUMENT-FLOW-CONTRACT PASS'
@@ -2,9 +2,12 @@ $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
$header = [System.IO.File]::ReadAllText((Join-Path $root 'components/PageHeader.vue'), [System.Text.Encoding]::UTF8)
$tabbar = [System.IO.File]::ReadAllText((Join-Path $root 'components/AppTabbar.vue'), [System.Text.Encoding]::UTF8)
$global = [System.IO.File]::ReadAllText((Join-Path $root 'styles/global.scss'), [System.Text.Encoding]::UTF8)
if ($header -notmatch 'class="page-header-slot"') { throw 'PageHeader must reserve normal-flow space for the fixed header' }
if ($header -notmatch '(?s)\.page-header\s*\{[^}]*position\s*:\s*fixed\s*;[^}]*top\s*:\s*0\s*;[^}]*right\s*:\s*0\s*;[^}]*left\s*:\s*0\s*;') { throw 'PageHeader must be fixed to the top viewport edge' }
if ($header -notmatch '(?s)\.page-header\s*\{[^}]*transform\s*:\s*translateZ\(0\)\s*;') { throw 'PageHeader must own a stable compositor layer above native media while scrolling' }
if ($global -notmatch '(?s)view:not\(\.page-header-slot\):has\(> \.page-header-slot\)\s*\{[^}]*position\s*:\s*relative\s*;[^}]*z-index\s*:\s*31\s*;') { throw 'Every direct PageHeader host must outrank later page content stacking contexts' }
if ($tabbar -notmatch '(?s)\.app-tabbar\s*\{[^}]*position\s*:\s*fixed\s*;[^}]*right\s*:\s*0\s*;[^}]*bottom\s*:\s*0\s*;[^}]*left\s*:\s*0\s*;') { throw 'AppTabbar must remain fixed to the bottom viewport edge' }
Write-Output 'FIXED-NAVIGATION-POSITION-CONTRACT PASS'
+2 -1
View File
@@ -11,6 +11,7 @@ foreach ($sourceFile in $sourceFiles) {
foreach ($match in $matches) {
$assetUrl = $match.Value
if ($assetUrl.Contains('${')) { continue }
$assetPath = Join-Path $root $assetUrl.TrimStart('/')
$relativeSource = $sourceFile.FullName.Substring($root.Length + 1)
@@ -23,7 +24,7 @@ foreach ($sourceFile in $sourceFiles) {
$errors.Add("Legacy asset path $assetUrl referenced by $relativeSource")
}
if ($assetUrl -match '(-v[0-9]+|-source)\.') {
if ($assetUrl -match '-source\.') {
$errors.Add("Temporary asset name $assetUrl referenced by $relativeSource")
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ foreach ($entry in $pages.GetEnumerator()) {
Assert-Match -Content $pages.G05 -Pattern 'v-else-if="overviewState === ''loading''"\s+class="overview-state overview-state--loading"' -Message 'G05 must retain its transparent framed loading branch'
Assert-Match -Content $pages.G05 -Pattern '(?s)<AppLoading\s+[^>]*description="[^"]+"[^>]*/>' -Message 'G05 must render AppLoading with a loading description'
Assert-Match -Content $pages.G06 -Pattern '<AppLoading\s+v-if="searchState === ''loading''"\s+variant="section"[^>]+description="[^"]+"\s*/>' -Message 'G06 search results must render the section AppLoading variant'
Assert-Match -Content $pages.G06 -Pattern 'requestedState\s*===\s*["'']loading["'']' -Message 'G06 must expose its section loading state for visual review'
Assert-Match -Content $pages.G06 -Pattern 'query\?\.state\s*===\s*["'']loading["'']' -Message 'G06 must expose its native section loading state for visual review'
Assert-Match -Content $pages.G09 -Pattern '<AppLoading\s+v-else-if="applicationState === ''loading''"[^>]+description="[^"]+"\s*/>' -Message 'G09 must render a page AppLoading branch'
Assert-Match -Content $pages.G10 -Pattern '<AppLoading\s+v-else-if="reviewState === ''loading''"[^>]+description="[^"]+"\s*/>' -Message 'G10 must render a page AppLoading branch'
Assert-Match -Content $pages.G11 -Pattern '<AppLoading\s+v-if="settingsState === ''loading''"[^>]+description="[^"]+"\s*/>' -Message 'G11 must render a page AppLoading branch'
+5 -5
View File
@@ -30,12 +30,12 @@ if ($paths -contains 'pages/genealogy/g02-empty-genealogies') { throw 'G02 route
foreach ($required in @(
'forceEmptyState',
'query.get("state") || "default"',
'query?.state',
'empty-create-action',
'empty-search-action',
'empty-invite-action',
'empty-create-note',
'g01-empty-panel-frame.png',
'@include adaptive.adaptive-genealogy-state-panel;',
'const createGenealogy = () =>',
'/pages/genealogy/g03-create-genealogy',
'/pages/genealogy/g06-search-genealogies',
@@ -59,14 +59,14 @@ foreach ($required in @(
Assert-Match -Content $g01 -Pattern '(?s)<view class="genealogy-fixed-zone">.*class="current-slip".*class="shortcut-grid".*class="section-divider".*</view>\s*<scroll-view[^>]*class="genealogy-list-scroll"[^>]*>.*class="genealogy-lower".*</scroll-view>' -Message 'G01 fixed and scrolling regions are not separated correctly'
Assert-Match -Content $g01 -Pattern '(?s)\.genealogy-index--split\s*\{[^}]*height:\s*100vh;[^}]*padding-bottom:\s*calc\(112rpx\s*\+\s*env\(safe-area-inset-bottom\)\)' -Message 'G01 split layout must reserve the fixed tabbar and safe area'
Assert-Match -Content $g01 -Pattern '(?s)\.genealogy-list-scroll\s*\{[^}]*height:\s*0;[^}]*flex:\s*1;' -Message 'G01 list scroll region must consume the remaining computed height'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel\s*\{[^}]*background:\s*url\("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame\.png"\)\s*center\s*/\s*100%\s+100%\s+no-repeat;' -Message 'G01 empty state must render the exact extracted transparent frame as its container background'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel\s*\{[^}]*@include\s+adaptive\.adaptive-genealogy-state-panel;' -Message 'G01 empty state must consume the shared adaptive state-panel profile'
Assert-Match -Content $g01 -Pattern '(?s)\.genealogy-empty-state\s*\{[^}]*min-height:\s*1120rpx;' -Message 'G01 empty state does not reserve the approved visibly fuller height'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel\s*\{[^}]*height:\s*1120rpx;' -Message 'G01 empty panel does not use the approved visibly fuller height'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel\s*\{[^}]*min-height:\s*1120rpx;' -Message 'G01 empty panel does not retain the approved minimum height'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel__content\s*\{[^}]*justify-content:\s*center;[^}]*padding:\s*50rpx 28rpx 46rpx;' -Message 'G01 empty panel content is not centered with the approved responsive padding'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-seal\s*\{[^}]*width:\s*120rpx;[^}]*height:\s*184rpx;' -Message 'G01 empty seal does not use the approved prominent size'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-title\s*\{[^}]*margin-top:\s*30rpx;[^}]*font-size:\s*48rpx;' -Message 'G01 empty title does not use the approved prominent size and spacing'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-copy\s*\{[^}]*min-height:\s*84rpx;[^}]*margin-top:\s*18rpx;[^}]*font-size:\s*28rpx;' -Message 'G01 empty guidance does not use the approved prominent size and spacing'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-search-action,\s*\.empty-invite-action\s*\{[^}]*width:\s*560rpx;[^}]*height:\s*124rpx;' -Message 'G01 empty actions do not use the approved prominent size'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-search-action,\s*\.empty-invite-action\s*\{[^}]*width:\s*560rpx;[^}]*min-height:\s*124rpx;' -Message 'G01 empty actions do not retain the approved prominent minimum size'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-search-action__copy,\s*\.empty-invite-action__copy\s*\{[^}]*font-size:\s*34rpx;' -Message 'G01 empty action labels do not use the approved prominent size'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-create-action\s*\{[^}]*min-height:\s*76rpx;[^}]*margin-top:\s*14rpx;' -Message 'G01 empty create action does not use the approved prominent hit area and spacing'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-create-action__copy\s*\{[^}]*font-size:\s*31rpx;' -Message 'G01 empty create action does not use the approved prominent size'
+2 -2
View File
@@ -13,9 +13,9 @@ if (-not $errorBlock) { throw 'G01 error state block missing' }
foreach ($required in @('error-panel__seal', 'brand-seal.png', 'error-panel__divider', 'section-divider.png', 'state-retry', '@click="retryLoad"')) {
if ($errorBlock -notmatch [regex]::Escape($required)) { throw "G01 error state missing approved contract: $required" }
}
Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s*\{[^}]*background:\s*url\("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame\.png"\)\s*center\s*/\s*100%\s+100%\s+no-repeat;' -Message 'G01 error state must use the transparent frame as its container background'
Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s*\{[^}]*@include\s+adaptive\.adaptive-genealogy-state-panel;' -Message 'G01 error state must consume the shared adaptive state-panel profile'
Assert-Match -Content $g01 -Pattern '(?s)\.state-retry\s*\{[^}]*background:\s*url\("/static/assets/foundation/transparent/a01-scroll-primary-v3\.png"\)\s*center\s*/\s*contain\s+no-repeat;' -Message 'G01 retry must use the approved scroll skin as a container background'
Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s*\{[^}]*height:\s*1120rpx;' -Message 'G01 error state must use the approved full-height frame slot'
Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s*\{[^}]*min-height:\s*1120rpx;' -Message 'G01 error state must retain the approved minimum frame height'
Assert-Match -Content $g01 -Pattern '(?s)\.error-panel__seal\s*\{[^}]*width:\s*132rpx;[^}]*height:\s*136rpx;' -Message 'G01 error state seal must use the approved prominent size'
Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s+\.state-title\s*\{[^}]*font-size:\s*42rpx;' -Message 'G01 error title must use the approved hierarchy'
Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s+\.state-copy\s*\{[^}]*font-size:\s*27rpx;' -Message 'G01 error guidance must use the approved readable size'
+2 -2
View File
@@ -27,8 +27,8 @@ if (Test-Path -LiteralPath $g04) { throw 'G04 page file must be deleted; first-a
foreach ($required in @(
'const currentStep = ref("create");',
'query.get("step") === "ancestor"',
'window.addEventListener("hashchange", syncFlowFromRoute)',
'query?.step === "ancestor"',
'onLoad((query) =>',
'step=ancestor&genealogyId=',
'const createState = ref("form");',
'const ancestorState = ref("form");',
+1 -1
View File
@@ -7,7 +7,7 @@ if ($positions.Count -ne 1 -or $positions[0].Groups[1].Value.Trim() -ne 'fixed')
throw "G03 must retain only its combined fixed dialog layer rule; found $($positions.Count) position declarations"
}
foreach ($required in @(
'background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")',
'@include adaptive.adaptive-genealogy-state-panel;',
'background: url("/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png")',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")'
)) { if ($page -notmatch [regex]::Escape($required)) { throw "G03 is missing real asset background: $required" } }
+2 -2
View File
@@ -5,9 +5,9 @@ $g05 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g05-genealogy-
function Assert-Match { param([string]$Pattern,[string]$Message); if ($g05 -notmatch $Pattern) { throw $Message } }
Assert-Match 'overview-surface--state' 'G05 system states must own an independent surface'
Assert-Match '(?s)\.overview-state\s*\{[^}]*background:\s*url\("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame\.png"\)' 'G05 system states must reuse the transparent gold frame as its real background'
Assert-Match '(?s)\.overview-state\s*\{[^}]*@include\s+adaptive\.adaptive-genealogy-state-panel;' 'G05 system states must consume the shared adaptive state-panel profile'
Assert-Match '(?s)class="overview-state__seal".*?brand-seal\.png' 'G05 non-loading states must use the approved red-gold seal'
Assert-Match '(?s)\.overview-surface\s*\{[^}]*background:\s*url\("/static/assets/modules/genealogy/opaque/g05-overview-surface\.png"\)' 'G05 ready surface must use the approved overview asset as its real background'
Assert-Match '(?s)\.overview-surface\s*\{[^}]*@include\s+adaptive\.adaptive-g05-overview-surface;' 'G05 ready surface must consume the shared adaptive overview profile'
Assert-Match '(?s)\.overview-state__seal\s*\{[^}]*width:\s*132rpx;[^}]*height:\s*136rpx;' 'G05 state seal must use the approved size'
Assert-Match 'overview-action-lock' 'G05 member mode must explain owner-only actions instead of leaving blank cells'
Assert-Match '(?s)\.overview-actions\s*\{[^}]*min-height:\s*386rpx;' 'G05 actions must retain a minimum two-row surface while allowing data growth'
+2 -2
View File
@@ -13,8 +13,8 @@ if ($positions.Count -ne 0) {
}
foreach ($required in @(
'background: url("/static/assets/modules/genealogy/opaque/g05-overview-surface.png")',
'background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")',
'@include adaptive.adaptive-g05-overview-surface;',
'@include adaptive.adaptive-genealogy-state-panel;',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")'
)) {
if ($page -notmatch [regex]::Escape($required)) {
+3 -3
View File
@@ -13,9 +13,9 @@ if ($positions.Count -ne 0) {
}
foreach ($required in @(
'background: url("/static/assets/modules/genealogy/opaque/g06-search-input-wide.png")',
'background: url("/static/assets/modules/genealogy/opaque/g06-search-button.png")',
'background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png")',
'@include adaptive.adaptive-g06-search-field;',
'@include adaptive.adaptive-g06-search-action;',
'@include adaptive.adaptive-genealogy-list-card;',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")'
)) {
if ($styleWithoutComments -notmatch [regex]::Escape($required)) {
+2 -2
View File
@@ -5,8 +5,8 @@ $style = [regex]::Match($page, '(?s)<style[^>]*>(.*?)</style>').Groups[1].Value
$positions = [regex]::Matches([regex]::Replace($style, '(?s)/\*.*?\*/', ''), '(?m)\bposition\s*:\s*([^;]+);')
if ($positions.Count -ne 0) { throw "G08 join flow must use document flow; found $($positions.Count) position declarations" }
foreach ($required in @(
'background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")',
'background: url("/static/assets/modules/genealogy/transparent/g-form-field-frame.png")',
'@include adaptive.adaptive-genealogy-state-panel;',
'@include adaptive.adaptive-genealogy-form-field;',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")'
)) { if ($page -notmatch [regex]::Escape($required)) { throw "G08 is missing real asset background: $required" } }
if ($page -match 'class="join-panel__skin"') { throw 'G08 must not retain the decorative panel image layer' }
+1 -1
View File
@@ -5,7 +5,7 @@ $style = [regex]::Match($page, '(?s)<style[^>]*>(.*?)</style>').Groups[1].Value
$positions = [regex]::Matches([regex]::Replace($style, '(?s)/\*.*?\*/', ''), '(?m)\bposition\s*:\s*([^;]+);')
if ($positions.Count -ne 0) { throw "G09 application list must use document flow; found $($positions.Count) position declarations" }
foreach ($required in @(
'background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png")',
'@include adaptive.adaptive-genealogy-list-card;',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")'
)) { if ($page -notmatch [regex]::Escape($required)) { throw "G09 is missing real asset background: $required" } }
if ($page -match 'class="application-card__skin"') { throw 'G09 must not retain decorative card image layers' }
+13 -1
View File
@@ -5,7 +5,19 @@ $review = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g10-applica
foreach ($token in @('query.status', 'WITHDRAWN:', 'previous=withdrawn')) {
if (-not $mine.Contains($token)) { throw "G09 business state missing: $token" }
}
foreach ($token in @('rejectionReason', 'rejectionError', 'class="rejection-field"', 'current.item.rejectionReason')) {
foreach ($token in @(
'rejectionReason',
'rejectionError',
'class="rejection-field"',
'current.item.rejectionReason',
'id="g10-rejection-reason"',
':aria-invalid="!!rejectionError"',
'aria-describedby="g10-rejection-error"',
'id="g10-rejection-error"',
'role="alert"',
':focus="rejectionFocused"',
'await nextTick();'
)) {
if (-not $review.Contains($token)) { throw "G10 rejection contract missing: $token" }
}
Write-Output 'G09-G10-BUSINESS-STATE-CONTRACT PASS'
+7 -3
View File
@@ -7,10 +7,14 @@ if ($positions.Count -ne 1 -or $positions[0].Groups[1].Value.Trim() -ne 'fixed')
throw "G11 must retain only its fixed feedback layer; found $($positions.Count) position declarations"
}
foreach ($required in @(
'background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")',
'background: url("/static/assets/modules/genealogy/transparent/g-form-field-frame.png")',
'@include adaptive.adaptive-genealogy-state-panel;',
'@include adaptive.adaptive-genealogy-form-field;',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")',
'background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png")'
)) { if ($page -notmatch [regex]::Escape($required)) { throw "G11 is missing real asset background: $required" } }
)) { if ($page -notmatch [regex]::Escape($required)) { throw "G11 is missing adaptive asset surface: $required" } }
if ($page -notmatch '(?s)<textarea[^>]*auto-height') { throw 'G11 access note must grow from its text data' }
$settingsPanelStyle = [regex]::Match($style, '(?s)\.settings-panel\s*\{(?<body>.*?)\}').Groups['body'].Value
if ($settingsPanelStyle -match '\bmin-height\s*:') {
throw 'G11 settings panel height must be driven by its current content'
}
Write-Output 'G11-DOCUMENT-FLOW-CONTRACT PASS'
+7 -3
View File
@@ -7,9 +7,13 @@ if ($positions.Count -ne 1 -or $positions[0].Groups[1].Value.Trim() -ne 'fixed')
throw "G12 must retain only its fixed feedback layer; found $($positions.Count) position declarations"
}
foreach ($required in @(
'background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")',
'background: url("/static/assets/modules/genealogy/transparent/g-form-field-frame.png")',
'@include adaptive.adaptive-genealogy-state-panel;',
'@include adaptive.adaptive-genealogy-form-field;',
'background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png")'
)) { if ($page -notmatch [regex]::Escape($required)) { throw "G12 is missing real asset background: $required" } }
)) { if ($page -notmatch [regex]::Escape($required)) { throw "G12 is missing adaptive asset surface: $required" } }
if ($page -notmatch '(?s)<textarea[^>]*auto-height') { throw 'G12 generation poem field must grow from its text data' }
$poemPanelStyle = [regex]::Match($style, '(?s)\.poem-panel\s*\{(?<body>.*?)\}').Groups['body'].Value
if ($poemPanelStyle -match '\bmin-height\s*:') {
throw 'G12 poem panel height must be driven by its current content'
}
Write-Output 'G12-DOCUMENT-FLOW-CONTRACT PASS'
+14 -5
View File
@@ -1,15 +1,16 @@
$ErrorActionPreference = 'Stop'
$source = Get-Content 'pages/profile/m01-profile-home.vue' -Raw -Encoding UTF8
$profiles = Get-Content 'styles/adaptive-frame-profiles.scss' -Raw -Encoding UTF8
$required = @(
'profile-state--ready',
'profile-state--error',
'm01-profile-summary-card.png',
'@include adaptive.adaptive-profile-summary;',
'@include adaptive.adaptive-scroll-button(primary);',
'brand-seal.png',
'auth-divider-knot.png',
'chevron-right.png',
'root-header-hall.png',
'a01-scroll-primary-v3.png',
'a01-icon-lock-v1.png',
'notice.png',
'AppButton',
@@ -38,11 +39,19 @@ if ($source -match '<image\s+(?:[^>]*\s)?class="(?:profile-hero__frame|profile-s
if ($source -match 'position\s*:') { throw 'M01 must keep content and local decoration in flex/grid document flow' }
foreach ($token in @(
'class="profile-hero__content"',
'grid-area: 1 / 1',
'background: url("/static/assets/modules/profile/transparent/m01-profile-summary-card.png") center / 100% 100% no-repeat',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / 100% 100% no-repeat'
'grid-area: 1 / 1'
)) {
if (-not $source.Contains($token)) { throw "M01 missing document-flow token: $token" }
}
if ($profiles -notmatch '(?s)@mixin\s+adaptive-profile-summary\s*\{.*?m01-profile-summary-card\.png') {
throw 'The adaptive profile owner must retain the M01 summary-card artwork'
}
if ($profiles -notmatch '(?s)@mixin\s+adaptive-scroll-button\s*\(\$type\).*?a01-scroll-primary-v3\.png') {
throw 'The adaptive profile owner must retain the primary scroll-button artwork'
}
if ($source -match '100%\s+100%\s+no-repeat|border-image-slice\s*:') {
throw 'M01 must consume adaptive frame geometry without page-local stretching or slicing'
}
Write-Output 'M01-MODULE-BASELINE-CONTRACT PASS'
+1 -1
View File
@@ -57,7 +57,7 @@ if ($component -match '(?im)(?<![-\w])position\s*:') { throw 'ModulePage ordinar
foreach ($rule in @(
'(?s)\.module-page\s*\{[^}]*display:\s*flex;[^}]*flex-direction:\s*column;',
'(?s)\.module-page__header,\s*\.module-page__content,\s*\.module-page__loading\s*\{[^}]*z-index:\s*1;',
'(?s)\.module-lead\s*\{[^}]*background:\s*url\("/static/assets/modules/genealogy/transparent/section-divider\.png"\) center / 100% 100% no-repeat;',
'(?s)\.module-lead\s*\{[^}]*background:\s*url\("/static/assets/modules/genealogy/transparent/section-divider\.png"\) center / 100% auto no-repeat;',
'(?s)\.module-lead\s*\{[^}]*display:\s*flex;[^}]*align-items:\s*center;[^}]*justify-content:\s*center;',
'(?s)\.form-row,\s*\.settings-row\s*\{[^}]*@include\s+adaptive\.adaptive-module-field;',
'(?s)\.list-card,\s*\.detail-card,\s*\.timeline-row,\s*\.status-card\s*\{[^}]*@include\s+adaptive\.adaptive-module-content;'
+7 -6
View File
@@ -1,12 +1,13 @@
$ErrorActionPreference = 'Stop'
$source = Get-Content 'pages/notification/n01-message-center.vue' -Raw -Encoding UTF8
$profiles = Get-Content 'styles/adaptive-frame-profiles.scss' -Raw -Encoding UTF8
$unread = [string]([char]0x672A) + [char]0x8BFB
$read = [string]([char]0x5DF2) + [char]0x8BFB
$markAllRead = [string]([char]0x5168) + [char]0x90E8 + $read
$goToReview = [string]([char]0x524D) + [char]0x5F80 + [char]0x5165 + [char]0x8C31 + [char]0x5BA1 + [char]0x6838
$required = @(
'n01-notice-card.png',
'@include adaptive.adaptive-notification-content;',
'AppLoading',
'AppToast',
'notice-state--loading',
@@ -35,11 +36,11 @@ if ($source -match '<image\s+(?:[^>]*\s)?class="notice-(?:card|state-card)__skin
if ($source -match 'position\s*:') {
throw 'N01 must keep ordinary content in document flow'
}
foreach ($background in @(
'background: url("/static/assets/modules/notification/transparent/n01-notice-card.png") center / 100% 100% no-repeat',
'background: url("/static/assets/modules/notification/transparent/n01-notice-card.png") top center / 100% 220rpx no-repeat'
)) {
if (-not $source.Contains($background)) { throw "N01 missing document-flow background: $background" }
if ($profiles -notmatch '(?s)@mixin\s+adaptive-notification-content\s*\{.*?n01-notice-card\.png') {
throw 'The adaptive profile owner must retain the N01 notice-card artwork'
}
if ($source -match '100%\s+100%\s+no-repeat|border-image-slice\s*:') {
throw 'N01 must consume adaptive frame geometry without page-local stretching or slicing'
}
Write-Output 'N01-MODULE-BASELINE-CONTRACT PASS'
+29
View File
@@ -0,0 +1,29 @@
$ErrorActionPreference = 'Stop'
$activeFiles = @(
'pages/genealogy/g01-my-genealogies.vue',
'pages/genealogy/g03-create-genealogy.vue',
'pages/genealogy/g06-search-genealogies.vue'
)
foreach ($file in $activeFiles) {
$source = Get-Content $file -Raw -Encoding UTF8
foreach ($forbidden in @('location.hash', 'new URLSearchParams', 'hashchange')) {
if ($source.Contains($forbidden)) {
throw "$file must not read native route state through $forbidden"
}
}
if ($source -notmatch 'onLoad\s*\(\s*\(?\s*query\b') {
throw "$file must consume its native query through onLoad(query)"
}
}
$g01 = Get-Content 'pages/genealogy/g01-my-genealogies.vue' -Raw -Encoding UTF8
$g03 = Get-Content 'pages/genealogy/g03-create-genealogy.vue' -Raw -Encoding UTF8
$g06 = Get-Content 'pages/genealogy/g06-search-genealogies.vue' -Raw -Encoding UTF8
if ($g01 -notmatch 'query\?\.state') { throw 'G01 must normalize presentation state from onLoad query' }
if ($g03 -notmatch 'query\?\.step\s*===\s*"ancestor"') { throw 'G03 must whitelist the ancestor step from onLoad query' }
if ($g06 -notmatch 'query\?\.mode\s*===\s*"invite"') { throw 'G06 must whitelist invite mode from onLoad query' }
Write-Output 'NATIVE-ROUTE-QUERY-CONTRACT PASS'
+24
View File
@@ -0,0 +1,24 @@
$ErrorActionPreference = 'Stop'
$owner = 'utils/validation.js'
if (-not (Test-Path $owner)) { throw 'Missing shared password policy owner' }
$validation = Get-Content $owner -Raw -Encoding UTF8
foreach ($token in @('export const PASSWORD_POLICY_MESSAGE', 'export const validatePassword')) {
if (-not $validation.Contains($token)) { throw "Password policy owner missing: $token" }
}
foreach ($page in @(
'pages/auth/a04-register.vue',
'pages/auth/a05-reset-password.vue',
'pages/profile/m04-change-password.vue'
)) {
$source = Get-Content $page -Raw -Encoding UTF8
foreach ($token in @('validatePassword', 'PASSWORD_POLICY_MESSAGE', '@/utils/validation.js')) {
if (-not $source.Contains($token)) { throw "$page does not consume shared password policy: $token" }
}
if ($source -match '\(\?=\.\*\[A-Za-z\]\)|\{8,32\}\$') {
throw "$page must not own a local password policy regex"
}
}
Write-Output 'PASSWORD-POLICY-CONTRACT PASS'
+19
View File
@@ -0,0 +1,19 @@
const assert = require("assert");
const fs = require("fs");
const vm = require("vm");
const filename = "utils/validation.js";
const source = `${fs.readFileSync(filename, "utf8")
.replace(/export const /g, "const ")}\nmodule.exports = { PASSWORD_POLICY_MESSAGE, validatePassword };`;
const sandbox = { module: { exports: {} } };
vm.runInNewContext(source, sandbox, { filename });
const { validatePassword } = sandbox.module.exports;
for (const value of ["", "abc1234", "abcdefgh", "12345678", "a".repeat(33) + "1"]) {
assert.equal(validatePassword(value).valid, false, `${value || "empty"} must fail`);
}
for (const value of ["abc12345", "Family2026!"]) {
assert.equal(validatePassword(value).valid, true, `${value} must pass`);
}
console.log("PASSWORD-POLICY-RUNTIME-SMOKE PASS");
+16 -2
View File
@@ -1,16 +1,30 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
$page = [System.IO.File]::ReadAllText((Join-Path $root 'pages/records/r01-people-list.vue'), [System.Text.Encoding]::UTF8)
$profiles = [System.IO.File]::ReadAllText((Join-Path $root 'styles/adaptive-frame-profiles.scss'), [System.Text.Encoding]::UTF8)
foreach ($expected in @(
'grid-template-columns: minmax(0, 1fr) 126rpx',
'class="people-state-card__copy"',
'background: url("/static/assets/modules/records/transparent/r01-search-input-frame.png") center / 100% 100% no-repeat',
'background: url("/static/assets/modules/records/transparent/r01-person-name-card.png") center / 100% 100% no-repeat'
'@include adaptive.adaptive-records-field;',
'@include adaptive.adaptive-records-person;',
'min-height: clamp(92px, 190rpx, 108px)',
'import { onLoad } from "@dcloudio/uni-app";',
'onLoad((query) => {'
)) {
if (-not $page.Contains($expected)) { throw "R01 document-flow contract missing: $expected" }
}
if ($profiles -notmatch '(?s)@mixin\s+adaptive-records-person\s*\{.*?r01-person-name-card\.png.*?padding:\s*28rpx\s+64rpx;') {
throw 'The adaptive profile owner must retain the R01 artwork and content-safe padding'
}
if ($page -match '(?s)\.person-card\s*\{[^}]*(?<!min-)height\s*:|(?s)\.person-card\s*\{[^}]*padding\s*:\s*\d+%') {
throw 'R01 person cards must grow with content and consume profile-owned frame padding'
}
if ($page -match 'location\.hash|new URLSearchParams') {
throw 'R01 review states must use the UniApp onLoad route contract on Android'
}
if ($page -match '<image\s+(?:[^>]*\s)?class="person-card__skin"|people-search\s*>\s*<image|people-result-empty\s*>\s*<image|people-state-card\s*>\s*<image') {
throw 'R01 decorative frames must be container backgrounds instead of positioned image layers'
}
+4 -2
View File
@@ -1,6 +1,7 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$page = Get-Content -LiteralPath (Join-Path $root 'pages/records/r01-people-list.vue') -Raw -Encoding utf8
$profiles = Get-Content -LiteralPath (Join-Path $root 'styles/adaptive-frame-profiles.scss') -Raw -Encoding utf8
function Assert-Match([string]$Pattern, [string]$Message) {
if ($page -notmatch $Pattern) { throw $Message }
@@ -9,8 +10,9 @@ function Assert-Match([string]$Pattern, [string]$Message) {
if ($page -match "import\s+ModulePage\s+from|<ModulePage(?:\s|/|>)") { throw 'R01 must own its page instead of delegating to ModulePage.' }
if ($page -match 'application-status-card\.png') { throw 'R01 must not reuse the application status card.' }
if ($page -match 'people-intro|brand-seal\.png') { throw 'R01 must not repeat a numbered intro block below the page header.' }
Assert-Match 'modules/records/transparent/r01-person-name-card\.png' 'R01 must use its dedicated transparent person name card.'
Assert-Match 'modules/records/transparent/r01-search-input-frame\.png' 'R01 must use its dedicated search frame.'
Assert-Match '@include\s+adaptive\.adaptive-records-person\s*;' 'R01 must consume its dedicated adaptive person-card profile.'
Assert-Match '@include\s+adaptive\.adaptive-records-field\s*;' 'R01 must consume the shared adaptive records field profile.'
if ($profiles -notmatch '(?s)@mixin\s+adaptive-records-person\s*\{.*?modules/records/transparent/r01-person-name-card\.png') { throw 'The adaptive profile owner must retain R01 dedicated person-card artwork.' }
Assert-Match 'class="people-search"' 'R01 must expose its search region.'
Assert-Match 'people-state--empty' 'R01 must expose an empty review state.'
Assert-Match 'people-state--error' 'R01 must expose an error review state.'
+10 -5
View File
@@ -2,12 +2,13 @@ $ErrorActionPreference = 'Stop'
function Read-Utf8([string]$Path) { [System.IO.File]::ReadAllText((Join-Path (Join-Path $PSScriptRoot '..') $Path), [System.Text.Encoding]::UTF8) }
function Assert-Contains([string]$Content,[string]$Expected) { if (-not $Content.Contains($Expected)) { throw "R02 contract missing: $Expected" } }
$page = Read-Utf8 'pages/records/r02-person-detail.vue'
$profiles = Read-Utf8 'styles/adaptive-frame-profiles.scss'
$catalog = Read-Utf8 'data/page-catalog.js'
foreach ($expected in @(
'class="person-detail-page"','person-detail-state--','class="person-identity-card"',
'class="person-edit-action"','class="person-save-action"','class="person-cancel-action"',
'shortFields','longFields','person-field__label','person-long-field__label','const enterEdit = () =>','const savePerson = () =>',
'const returnToPeople = () =>','r01-person-name-card.png','module-content-frame.png','module-field-frame.png',
'const returnToPeople = () =>','@include adaptive.adaptive-records-person;','@include adaptive.adaptive-records-content;','@include adaptive.adaptive-records-field;',
'ModulePageBackground','PageHeader','AppButton','AppLoading','AppToast'
)) { Assert-Contains $page $expected }
foreach ($forbidden in @('<ModulePage page-id="r02"','import ModulePage from')) { if ($page.Contains($forbidden)) { throw "R02 retains forbidden dependency: $forbidden" } }
@@ -21,10 +22,14 @@ if ($page -match '(?im)(?<![-\w])position\s*:') {
foreach ($rule in @(
'(?s)\.person-detail-page\s*\{[^}]*display:\s*flex;[^}]*flex-direction:\s*column;',
'(?s)\.person-detail-header,\.person-detail-loading,\.person-detail-content\s*\{[^}]*z-index:\s*1;',
'(?s)\.person-identity-card\s*\{[^}]*background:\s*url\("/static/assets/modules/records/transparent/r01-person-name-card\.png"\) center / 100% 100% no-repeat;',
'(?s)\.person-archive-card,\.person-long-field,\.person-state-card\s*\{[^}]*background:\s*url\("/static/assets/modules/records/transparent/module-content-frame\.png"\) center / 100% 100% no-repeat;',
'(?s)\.person-field\s*\{[^}]*background:\s*url\("/static/assets/modules/records/transparent/module-field-frame\.png"\) center / 100% 100% no-repeat;'
'(?s)\.person-identity-card\s*\{[^}]*@include\s+adaptive\.adaptive-records-person\s*;',
'(?s)\.person-archive-card,\.person-long-field,\.person-state-card\s*\{[^}]*@include\s+adaptive\.adaptive-records-content\s*;',
'(?s)\.person-field\s*\{[^}]*@include\s+adaptive\.adaptive-records-field\s*;'
)) {
if ($page -notmatch $rule) { throw "R02 document-flow background rule missing: $rule" }
if ($page -notmatch $rule) { throw "R02 adaptive document-flow rule missing: $rule" }
}
foreach ($owner in @('r01-person-name-card.png','module-content-frame.png','module-field-frame.png')) {
if (-not $profiles.Contains($owner)) { throw "R02 adaptive profile owner missing: $owner" }
}
if ($page -match '100%\s+100%\s+no-repeat|border-image-slice\s*:') { throw 'R02 must consume adaptive frame geometry without page-local stretching or slicing' }
Write-Output 'R02-PERSON-DETAIL-CONTRACT PASS'
+4 -4
View File
@@ -4,10 +4,10 @@ $root=Split-Path -Parent $PSScriptRoot
$files=@('pages/family/f01-family-feed.vue','pages/family/f02-publish-feed.vue','pages/notification/n01-message-center.vue','pages/profile/m01-profile-home.vue')
$pages=@{}
foreach($file in $files){$content=Get-Content -LiteralPath(Join-Path $root $file)-Raw -Encoding utf8;if($content-match"@/utils/api\.js|\bappApi\b"){ throw "$file must not connect the API layer" };Assert-Contains $content 'ModulePageBackground' "$file must use its module background";$pages[$file]=$content}
foreach($required in @('feed-state--list','feed-state--empty','feed-state--error','f01-family-letter-card.png','/pages/family/f02-publish-feed')){Assert-Contains $pages['pages/family/f01-family-feed.vue'] $required "Missing F01 contract: $required"}
foreach($required in @('publish-state--form','publish-state--success','publish-state--error','module-content-frame.png','module-field-frame.png','AppButton','AppToast')){Assert-Contains $pages['pages/family/f02-publish-feed.vue'] $required "Missing F02 contract: $required"}
foreach($required in @('notice-state--loading','notice-state--list','notice-state--empty','notice-state--error','n01-notice-card.png','AppButton','AppLoading','AppToast','/pages/genealogy/g10-application-review')){Assert-Contains $pages['pages/notification/n01-message-center.vue'] $required "Missing N01 contract: $required"}
foreach($required in @('feed-state--list','feed-state--empty','feed-state--error','@include adaptive.adaptive-family-letter;','/pages/family/f02-publish-feed')){Assert-Contains $pages['pages/family/f01-family-feed.vue'] $required "Missing F01 contract: $required"}
foreach($required in @('publish-state--form','publish-state--success','publish-state--error','@include adaptive.adaptive-family-content;','@include adaptive.adaptive-family-field;','AppButton','AppToast')){Assert-Contains $pages['pages/family/f02-publish-feed.vue'] $required "Missing F02 contract: $required"}
foreach($required in @('notice-state--loading','notice-state--list','notice-state--empty','notice-state--error','@include adaptive.adaptive-notification-content;','AppButton','AppLoading','AppToast','/pages/genealogy/g10-application-review')){Assert-Contains $pages['pages/notification/n01-message-center.vue'] $required "Missing N01 contract: $required"}
if($pages['pages/notification/n01-message-center.vue']-match'application-status-card\.png'){throw 'N01 must not reuse the genealogy application card'}
foreach($required in @('profile-state--ready','profile-state--error','m01-profile-summary-card.png','brand-seal.png','auth-divider-knot.png','chevron-right.png','AppButton','/pages/profile/m02-edit-profile')){Assert-Contains $pages['pages/profile/m01-profile-home.vue'] $required "Missing M01 contract: $required"}
foreach($required in @('profile-state--ready','profile-state--error','@include adaptive.adaptive-profile-summary;','brand-seal.png','auth-divider-knot.png','chevron-right.png','AppButton','/pages/profile/m02-edit-profile')){Assert-Contains $pages['pages/profile/m01-profile-home.vue'] $required "Missing M01 contract: $required"}
foreach($forbidden in @('m01-profile-notice-card.png','m01-profile-menu-frame.png','g03-create-flow-panel.png','g06-search-input-wide.png','application-status-card.png')){if($pages['pages/profile/m01-profile-home.vue']-match[regex]::Escape($forbidden)){throw "M01 must not reuse repeated or opaque business asset: $forbidden"}}
Write-Output 'ROOT-PAGES-VISUAL-CONTRACT PASS'
@@ -29,7 +29,7 @@ foreach ($token in @(
}
$dialog = Read-Utf8 'components/AppDialog.vue'
foreach ($token in @('role="dialog"', 'aria-modal="true"', 'tabindex="-1"', '@keydown.esc.stop="cancel"', 'max-height: calc(var(--app-viewport-height, 100vh) - 160rpx)', 'overflow-y: auto')) {
foreach ($token in @('role="dialog"', 'aria-modal="true"', 'tabindex="-1"', '@keydown.esc.stop="cancel"', 'max-height: calc(var(--app-viewport-height, 100vh) - 80rpx - env(safe-area-inset-top) - env(safe-area-inset-bottom))', 'overflow-y: auto')) {
if (-not $dialog.Contains($token)) { throw "AppDialog accessibility contract missing: $token" }
}
@@ -43,4 +43,14 @@ foreach ($token in @('role="status"', 'aria-live="polite"', 'aria-busy="true"',
if (-not $loading.Contains($token)) { throw "AppLoading accessibility contract missing: $token" }
}
$tabbar = Read-Utf8 'components/AppTabbar.vue'
foreach ($token in @('role="tablist"', 'role="tab"', ':aria-selected="active === item.key"', 'hover-class="tab-item--pressed"')) {
if (-not $tabbar.Contains($token)) { throw "AppTabbar interaction contract missing: $token" }
}
$card = Read-Utf8 'components/GenealogyCard.vue'
foreach ($token in @('role="button"', ':aria-label="accessibleLabel"', 'hover-class="genealogy-card--pressed"', 'const accessibleLabel = computed(')) {
if (-not $card.Contains($token)) { throw "GenealogyCard interaction contract missing: $token" }
}
Write-Output 'SHARED-INTERACTION-ACCESSIBILITY-CONTRACT PASS'
+10 -3
View File
@@ -14,7 +14,9 @@ foreach ($required in @(
't01-member-drawer.png',
'v-for="connector in lineageConnectors"',
'lineage-pan-cue',
'const initialScrollLeft = ref(90)'
'const treeScrollLeft = ref(90)',
':scroll-left="treeState === ''tree'' ? treeScrollLeft : 0"',
':style="treeState === ''tree'' ? treeMetricsStyle : undefined"'
)) {
if ($page -notmatch [regex]::Escape($required)) { throw "T01 member action asset missing: $required" }
}
@@ -36,17 +38,22 @@ foreach ($rule in @(
'(?s)\.sheet-meta\s*\{[^}]*font-size:\s*22rpx;',
'(?s)\.tree-stage--lineage\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*190rpx minmax\(0, 1fr\);',
'(?s)\.generation-rail\s*\{[^}]*display:\s*grid;[^}]*width:\s*190rpx;',
'(?s)\.tree-scroll--lineage\s*\{[^}]*min-width:\s*0;',
'(?s)\.tree-scroll--lineage\s*\{[^}]*grid-area:\s*1 / 2;[^}]*min-width:\s*0;',
'(?s)\.lineage-pan-cue\s*\{[^}]*grid-area:\s*1 / 2;',
'const treeMetricsStyle = computed',
'(?s)\.generation-band\s*\{[^}]*display:\s*grid;',
'(?s)\.generation-band image,\s*\.generation-band__copy\s*\{[^}]*grid-area:\s*1 / 1;',
'(?s)\.member-node\s*\{[^}]*display:\s*grid;',
'(?s)\.member-node__skin,\s*\.member-node__copy\s*\{[^}]*grid-area:\s*1 / 1;',
'(?s)\.member-sheet\s*\{[^}]*height:\s*240rpx;',
'(?s)\.member-sheet\s*\{[^}]*min-height:\s*240rpx;',
'(?s)\.sheet-actions\s*\{[^}]*justify-content:\s*center;[^}]*margin-top:\s*auto;',
'(?s)\.sheet-action\s*\{[^}]*width:\s*250rpx;[^}]*min-height:\s*88rpx;'
)) {
if ($page -notmatch $rule) { throw "T01 readable visual rule missing: $rule" }
}
if ($page -match '(?s)<view\s+class="tree-canvas".*?<view class="lineage-pan-cue">') {
throw 'T01 horizontal reading cue must stay in the visible lineage viewport, not inside the wide canvas'
}
Write-Output 'T01-ALL-STATES-VISUAL-CONTRACT PASS'
+11 -2
View File
@@ -60,7 +60,16 @@ foreach ($required in @(
'/pages/tree/t04-add-relative',
'/pages/tree/t06-edit-relationship',
'/pages/tree/t07-member-directory',
'query.genealogyId'
'query.genealogyId',
':id="`tree-member-${member.id}`"',
':scroll-left="treeState === ''tree'' ? treeScrollLeft : 0"',
'@scroll="handleTreeScroll"',
'const currentTreeScrollLeft = ref(90)',
'uni.createSelectorQuery()',
'nodeRect.left + nodeRect.width / 2 - (scrollRect.left + scrollRect.width / 2)',
'const treeHasDrifted = ref(false)',
'const recenterSelectedMember = async () =>',
'class="tree-recenter"'
)) { Assert-Contains $page $required "Missing T01 contract: $required" }
foreach ($forbidden in @(
@@ -95,7 +104,7 @@ foreach ($obsolete in @('const nodeStyle = (member)', 'const generationRowStyle
foreach ($required in @(
'const treeMetrics = computed(',
':style="treeMetricsStyle"',
':style="treeState === ''tree'' ? treeMetricsStyle : undefined"',
':style="generationBandStyle(row)"',
'overflow-y: auto;'
)) { Assert-Contains $page $required "T01 data-driven tree boundary missing: $required" }
+10 -5
View File
@@ -4,9 +4,14 @@ $page = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t03-member-profile
$style = [regex]::Match($page, '(?s)<style[^>]*>(.*?)</style>').Groups[1].Value
if ([regex]::IsMatch($style, '(?m)\bposition\s*:')) { throw 'T03 member profile must use document flow throughout' }
foreach ($required in @(
'background: url("/static/assets/modules/tree/transparent/t01-state-panel.png")',
'background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png")',
'background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png")',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")'
)) { if ($page -notmatch [regex]::Escape($required)) { throw "T03 is missing real asset background: $required" } }
'@include adaptive.adaptive-tree-panel;',
'@include adaptive.adaptive-tree-field;',
'@include adaptive.adaptive-genealogy-list-card;',
'@include adaptive.adaptive-scroll-button(primary);',
'background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png")'
)) { if ($page -notmatch [regex]::Escape($required)) { throw "T03 is missing adaptive asset surface: $required" } }
$memberPanelStyle = [regex]::Match($style, '(?ms)^\s*\.member-panel\s*\{(?<body>.*?)\}').Groups['body'].Value
if ($memberPanelStyle -match '\bmin-height\s*:') {
throw 'T03 member panel height must be driven by its current content'
}
Write-Output 'T03-DOCUMENT-FLOW-CONTRACT PASS'
@@ -47,6 +47,11 @@ foreach ($required in @(
'<picker',
'<AppDialog'
)) { Assert-Contains $t04 $required "T04 missing independent add-relative contract: $required" }
$t04Style = [regex]::Match($t04, '(?s)<style[^>]*>(.*?)</style>').Groups[1].Value
$t04PanelStyle = [regex]::Match($t04Style, '(?ms)^\s*\.add-relative-panel\s*\{(?<body>.*?)\}').Groups['body'].Value
if ($t04PanelStyle -match '\bmin-height\s*:') {
throw 'T04 add-relative panel height must be driven by its current content'
}
foreach ($required in @(
'const editState = ref("form")',
@@ -63,6 +68,11 @@ foreach ($required in @(
'<AppDialog',
'onBackPress('
)) { Assert-Contains $t05 $required "T05 missing independent member-edit contract: $required" }
$t05Style = [regex]::Match($t05, '(?s)<style[^>]*>(.*?)</style>').Groups[1].Value
$t05PanelStyle = [regex]::Match($t05Style, '(?ms)^\s*\.edit-member-panel\s*\{(?<body>.*?)\}').Groups['body'].Value
if ($t05PanelStyle -match '\bmin-height\s*:') {
throw 'T05 edit-member panel height must be driven by its current content'
}
foreach ($required in @(
'const relationshipState = ref("form")',
@@ -80,6 +90,11 @@ foreach ($required in @(
'<picker',
'<AppDialog'
)) { Assert-Contains $t06 $required "T06 missing independent relationship-edit contract: $required" }
$t06Style = [regex]::Match($t06, '(?s)<style[^>]*>(.*?)</style>').Groups[1].Value
$t06PanelStyle = [regex]::Match($t06Style, '(?ms)^\s*\.relationship-panel\s*\{(?<body>.*?)\}').Groups['body'].Value
if ($t06PanelStyle -match '\bmin-height\s*:') {
throw 'T06 relationship panel height must be driven by its current content'
}
foreach ($required in @(
'const genealogyId = ref("")',
+6 -2
View File
@@ -1,6 +1,7 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$page = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t07-member-directory.vue') -Raw -Encoding utf8
$profiles = Get-Content -LiteralPath (Join-Path $root 'styles/adaptive-frame-profiles.scss') -Raw -Encoding utf8
function Assert-Match([string]$Pattern, [string]$Message) {
if ($page -notmatch $Pattern) { throw $Message }
@@ -17,8 +18,11 @@ Assert-Match '(?s)\.directory-page\s*\{[^}]*overflow-y:\s*auto;' 'T07 must allow
if ($page -match '(?s)\.directory-page\s*\{[^}]*overflow-x:\s*hidden;') { throw 'T07 must not hide horizontal layout failures at the page root.' }
Assert-Match '(?s)\.directory-search\s*\{[^}]*min-height:\s*44px;' 'T07 search field must preserve a 44 CSS px minimum touch height at every viewport.'
Assert-Match '(?s)\.directory-search__action\s*\{[^}]*min-width:\s*88rpx;[^}]*min-height:\s*44px;' 'T07 search action must preserve a 44 CSS px touch target.'
Assert-Match 'modules/tree/transparent/t07-search-input-frame\.png' 'T07 search must use its dedicated transparent input frame.'
Assert-Match 'modules/genealogy/transparent/list-slip-frame\.png' 'T07 member cards must use the approved transparent list frame.'
Assert-Match '@include adaptive\.adaptive-tree-field;' 'T07 search must consume the shared adaptive tree field profile.'
Assert-Match '@include adaptive\.adaptive-genealogy-list-card;' 'T07 member cards must consume the shared adaptive genealogy list profile.'
foreach ($asset in @('modules/tree/transparent/t07-search-input-frame.png', 'modules/genealogy/transparent/list-slip-frame.png')) {
if ($profiles -notmatch [regex]::Escape($asset)) { throw "Adaptive profiles must own the T07 surface: $asset" }
}
if ($page -match 'g06-search-input-wide\.png|application-status-card\.png') {
throw 'T07 must not reuse opaque search or application-status skins.'
}
+13
View File
@@ -0,0 +1,13 @@
export const PASSWORD_POLICY_MESSAGE =
"密码需为 8–32 位,并同时包含字母和数字";
export const validatePassword = (value) => {
const valid =
typeof value === "string" &&
value.length >= 8 &&
value.length <= 32 &&
/[A-Za-z]/.test(value) &&
/\d/.test(value);
return { valid, message: valid ? "" : PASSWORD_POLICY_MESSAGE };
};