Compare commits

...

28 Commits

Author SHA1 Message Date
rain 5d99bae3f2 feat: unify project responsive layouts 2026-07-21 20:59:10 +08:00
rain a0d19f4aab test: tighten G03 shared header contract 2026-07-21 13:56:23 +08:00
rain 9362c47b95 refactor: reuse shared header on G03 2026-07-21 13:52:59 +08:00
rain 2f22df61b3 fix: adapt shared page header to safe area 2026-07-21 13:46:16 +08:00
rain 779a44279c docs: plan shared header safe area implementation 2026-07-21 13:34:47 +08:00
rain 0542fa9102 docs: define shared header safe area 2026-07-21 13:28:13 +08:00
rain 668b4ef55f fix: sync G01 role shortcuts 2026-07-21 13:06:36 +08:00
rain 8468cc0f89 docs: define G01 role shortcut rules 2026-07-21 12:50:38 +08:00
rain ec1295de0e 记录认证页安卓自适应验收 2026-07-21 12:15:00 +08:00
rain cd3dbb2ab1 同步封存A06自适应骨架 2026-07-21 12:13:07 +08:00
rain b774ce8212 迁移A05并恢复认证页水墨背景 2026-07-21 12:08:15 +08:00
rain 75c8e4fcbb 测试A05重设密码页自适应结构 2026-07-21 11:51:58 +08:00
rain a28678ca0b 迁移A04为自适应文档流 2026-07-21 11:49:46 +08:00
rain 5a86803f9b 测试A04注册页自适应结构 2026-07-21 11:33:48 +08:00
rain 40947a1fe5 提取认证页共享自适应骨架 2026-07-21 11:32:12 +08:00
rain 8f88d6ecdf 测试认证页共享自适应骨架 2026-07-21 11:26:59 +08:00
rain c0f3233e26 规划认证页自适应迁移 2026-07-21 11:25:57 +08:00
rain 9c7ceb9a02 记录A01验证码态与Toast验收 2026-07-21 11:23:25 +08:00
rain daccf7ba3d 记录A01安卓自适应验收 2026-07-21 11:15:13 +08:00
rain bc4061c8dc 调整A01安卓视觉比例 2026-07-21 11:13:26 +08:00
rain c5af1f2414 验证A01多视口自适应结构 2026-07-21 10:47:47 +08:00
rain 119d820308 重构A01为自适应文档流 2026-07-21 10:45:59 +08:00
rain 15cce0aba8 修正A01固定高度合同匹配 2026-07-21 10:45:59 +08:00
rain 086fc3c7e4 增加手机可用视口基础变量 2026-07-21 10:43:51 +08:00
rain 47afb100cd 测试A01自适应页面结构 2026-07-21 10:43:05 +08:00
rain 69592868d0 规划A01自适应结构证明 2026-07-21 10:37:20 +08:00
rain 2d33ee71e4 重写全项目手机自适应架构 2026-07-21 10:30:38 +08:00
rain 4b338ebefc 设计全项目手机视口自适应合同 2026-07-21 09:30:34 +08:00
116 changed files with 4453 additions and 925 deletions
+25 -1
View File
@@ -3,7 +3,11 @@
class="app-button"
:class="[
`app-button--${type}`,
{ 'app-button--block': block, 'app-button--disabled': disabled },
{
'app-button--block': block,
'app-button--compact': compact,
'app-button--disabled': disabled,
},
]"
:disabled="disabled"
:aria-label="label"
@@ -24,6 +28,7 @@ const props = defineProps({
label: { type: String, default: "" },
type: { type: String, default: "primary" },
block: { type: Boolean, default: false },
compact: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
});
const emit = defineEmits(["click"]);
@@ -40,6 +45,8 @@ const handleClick = (event) => {
</script>
<style scoped lang="scss">
@use "../styles/adaptive-frame-profiles.scss" as adaptive;
.app-button {
display: inline-grid;
width: 420rpx;
@@ -60,6 +67,23 @@ const handleClick = (event) => {
display: grid;
width: 100%;
}
.app-button--compact {
}
.app-button--compact.app-button--primary {
@include adaptive.adaptive-scroll-button(primary);
}
.app-button--compact.app-button--secondary {
@include adaptive.adaptive-scroll-button(secondary);
}
.app-button--compact .app-button__skin {
display: none;
}
.app-button--compact .app-button__label {
padding: 0;
font-size: 23rpx;
letter-spacing: 0;
white-space: nowrap;
}
.app-button__skin {
width: 100%;
height: 100%;
+29 -11
View File
@@ -15,9 +15,11 @@
@keydown.esc.stop="cancel"
>
<scroll-view class="app-dialog__content" scroll-y>
<text v-if="eyebrow" class="app-dialog__eyebrow">{{ eyebrow }}</text>
<text class="app-dialog__title">{{ title }}</text>
<text v-if="message" class="app-dialog__message">{{ message }}</text>
<view class="app-dialog__copy">
<text v-if="eyebrow" class="app-dialog__eyebrow">{{ eyebrow }}</text>
<text class="app-dialog__title">{{ title }}</text>
<text v-if="message" class="app-dialog__message">{{ message }}</text>
</view>
<slot />
<view
class="app-dialog__actions"
@@ -27,9 +29,14 @@
v-if="showCancel"
type="secondary"
:label="cancelText"
:compact="compactActions && showCancel"
@click="cancel"
/>
<AppButton :label="confirmText" @click="$emit('confirm')" />
<AppButton
:label="confirmText"
:compact="compactActions && showCancel"
@click="$emit('confirm')"
/>
</view>
</scroll-view>
</view>
@@ -48,6 +55,7 @@ const props = defineProps({
confirmText: { type: String, default: "我知道了" },
cancelText: { type: String, default: "取消" },
showCancel: { type: Boolean, default: false },
compactActions: { type: Boolean, default: false },
closeOnMask: { type: Boolean, default: true },
});
const emit = defineEmits(["confirm", "cancel", "close"]);
@@ -75,6 +83,8 @@ const cancel = () => {
</script>
<style scoped lang="scss">
@use "../styles/adaptive-frame-profiles.scss" as adaptive;
.app-dialog-layer {
position: fixed;
z-index: 80;
@@ -89,22 +99,30 @@ const cancel = () => {
.app-dialog {
width: 650rpx;
max-width: 100%;
min-height: 520rpx;
background: url("/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png")
center / contain no-repeat;
@include adaptive.adaptive-auth-dialog;
}
.app-dialog__content {
z-index: 1;
display: flex;
min-height: 520rpx;
max-height: calc(100vh - 80rpx);
max-height: calc(var(--app-viewport-height, 100vh) - 160rpx);
flex-direction: column;
align-items: center;
box-sizing: border-box;
padding: 74rpx 66rpx 54rpx;
padding: 16rpx 20rpx 12rpx;
text-align: center;
overflow-y: auto;
}
.app-dialog__copy {
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
}
.app-dialog__title,
.app-dialog__message {
display: block;
width: 100%;
}
.app-dialog__eyebrow {
color: #9f170f;
font-size: 22rpx;
@@ -131,7 +149,7 @@ const cancel = () => {
width: 100%;
grid-template-columns: 1fr 1fr;
gap: 16rpx;
margin-top: auto;
margin-top: 26rpx;
}
.app-dialog__actions--single {
display: flex;
+1 -1
View File
@@ -60,7 +60,7 @@ const switchTab = (item) => {
left: 0;
z-index: 20;
display: flex;
height: 112rpx;
min-height: 112rpx;
padding-bottom: env(safe-area-inset-bottom);
box-sizing: content-box;
border-top: 1rpx solid $gold-soft;
+3 -7
View File
@@ -18,6 +18,8 @@ defineProps({
</script>
<style scoped lang="scss">
@use "../styles/adaptive-frame-profiles.scss" as adaptive;
.app-toast {
position: fixed;
z-index: 100;
@@ -29,13 +31,7 @@ defineProps({
min-height: 82rpx;
align-items: center;
justify-content: center;
box-sizing: border-box;
border: 16rpx solid transparent;
border-width: 16rpx 74rpx;
border-image-source: url("/static/assets/foundation/transparent/a01-scroll-toast-v3.png");
border-image-slice: 58 280 fill;
border-image-width: 16rpx 74rpx;
border-image-repeat: stretch;
@include adaptive.adaptive-feedback-toast;
transform: translateX(-50%);
pointer-events: none;
}
+78
View File
@@ -0,0 +1,78 @@
<template>
<view class="auth-shell">
<view class="auth-shell__header">
<image
class="auth-shell__header-image"
src="/static/assets/modules/auth/opaque/a01-vnext-header-v1.png"
mode="widthFix"
/>
<image
class="auth-shell__seal"
src="/static/assets/foundation/transparent/brand-seal.png"
mode="aspectFit"
/>
</view>
<view class="auth-shell__paper">
<slot />
</view>
<slot name="overlay" />
</view>
</template>
<style scoped lang="scss">
.auth-shell {
display: flex;
flex-direction: column;
box-sizing: border-box;
width: 100%;
min-height: var(--app-viewport-height);
overflow-x: hidden;
background: #940400;
color: #493323;
}
.auth-shell__header {
position: relative;
flex: 0 0 auto;
box-sizing: border-box;
width: 100%;
height: calc(var(--app-safe-top) + min(36.5vw, 175px));
overflow: hidden;
padding-top: var(--app-safe-top);
background: #940400;
}
.auth-shell__header-image {
display: block;
width: 100%;
height: auto;
pointer-events: none;
}
.auth-shell__seal {
position: absolute;
top: calc(var(--app-safe-top) + clamp(46px, 11.5vw, 58px));
left: 50%;
width: clamp(56px, 17vw, 78px);
height: clamp(67px, 20.4vw, 94px);
transform: translateX(-50%);
pointer-events: none;
}
.auth-shell__paper {
display: flex;
flex: 1;
box-sizing: border-box;
width: 100%;
padding-bottom: var(--app-safe-bottom);
background-color: #f7f0e5;
background-image:
url("/static/assets/modules/auth/opaque/a01-red-hall-ink-backdrop-v1.png"),
url("/static/assets/foundation/opaque/auth-page-paper.jpg");
background-position: center calc(-48.18vw), center top;
background-size: 100% auto, 100% auto;
background-repeat: no-repeat, repeat-y;
}
</style>
+9 -7
View File
@@ -249,6 +249,8 @@ onUnmounted(() => {
</script>
<style scoped lang="scss">
@use "../styles/adaptive-frame-profiles.scss" as adaptive;
.module-page {
display: flex;
min-height: 100vh;
@@ -267,18 +269,18 @@ onUnmounted(() => {
padding: 16rpx 26rpx 56rpx;
}
.module-lead {
display: flex;
min-height: 56rpx;
align-items: center;
justify-content: center;
margin: 0 12rpx 18rpx;
color: $ink-muted;
font-size: 23rpx;
text-align: center;
background: url("/static/assets/modules/genealogy/transparent/section-divider.png") center / 100% 100% no-repeat;
background: url("/static/assets/modules/genealogy/transparent/section-divider.png") center / 100% auto no-repeat;
}
.module-lead text {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
display: block;
}
.module-panel {
width: 100%;
@@ -304,7 +306,7 @@ onUnmounted(() => {
}
.form-row,
.settings-row {
background: var(--module-field-asset) center / 100% 100% no-repeat;
@include adaptive.adaptive-module-field;
}
.form-row > text {
color: $ink;
@@ -350,7 +352,7 @@ onUnmounted(() => {
.detail-card,
.timeline-row,
.status-card {
background: var(--module-content-asset) center / 100% 100% no-repeat;
@include adaptive.adaptive-module-content;
}
.list-card__copy {
padding: 34rpx 46rpx 28rpx;
+10 -2
View File
@@ -74,9 +74,16 @@ const props = defineProps({
root: { type: Boolean, default: false },
unreadCount: { type: Number, default: 0 },
fallbackUrl: { type: String, default: "/pages/genealogy/g01-my-genealogies" },
customBack: { type: Boolean, default: false },
});
const emit = defineEmits(["brand", "notice", "action", "back"]);
const goBack = () => {
if (props.customBack) {
emit("back");
return;
}
const stack = getCurrentPages();
if (stack.length > 1) {
uni.navigateBack();
@@ -88,7 +95,7 @@ const goBack = () => {
<style scoped lang="scss">
.page-header-slot {
height: 104rpx;
height: calc(104rpx + var(--status-bar-height, 0px));
flex: none;
}
@@ -104,8 +111,9 @@ const goBack = () => {
z-index: 30;
display: flex;
align-items: center;
height: 104rpx;
height: calc(104rpx + var(--status-bar-height, 0px));
padding: 0 24rpx;
padding-top: var(--status-bar-height, 0px);
box-sizing: border-box;
background: $brand-red;
color: #fff9ed;
+5 -6
View File
@@ -107,7 +107,7 @@ const configs = {
action: "保存资料",
note: "隐私字段只向本人和有权限的家谱管理员展示。",
fields: [
{ key: "name", label: "姓名", placeholder: "汤文远" },
{ key: "name", label: "姓名", placeholder: "请输入首位成员姓名" },
{ key: "generation", label: "字辈", placeholder: "例如:文" },
{ key: "birthDate", label: "出生日期", placeholder: "1940年" },
{ key: "summary", label: "人物简介", placeholder: "简要记录生平" },
@@ -121,7 +121,7 @@ const configs = {
action: "保存关系",
note: "若形成重复父母、代际倒置等情况,页面会先提示冲突。",
fields: [
{ key: "source", label: "当前成员", placeholder: "汤文远" },
{ key: "source", label: "当前成员", placeholder: "请选择当前成员" },
{ key: "target", label: "关联成员", placeholder: "请选择家谱成员" },
{ key: "relation", label: "关系类型", placeholder: "父子、配偶等" },
{ key: "effectiveDate", label: "生效日期", placeholder: "选填" },
@@ -195,6 +195,7 @@ const showConflictHelp = () => {
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.member-form-page {
display: flex;
min-height: 100vh;
@@ -205,14 +206,13 @@ const showConflictHelp = () => {
z-index: 3;
}
.member-form-panel {
@include adaptive.adaptive-tree-panel;
z-index: 2;
width: calc(100% - 32rpx);
min-height: min(640px, calc((100vw - 16px) * 1.48));
margin: 18rpx auto 0;
padding: 7.5% 8%;
box-sizing: border-box;
background: url("/static/assets/modules/tree/transparent/t01-state-panel.png")
center / 100% 100% no-repeat;
}
.member-form,
.member-form-result {
@@ -240,6 +240,7 @@ const showConflictHelp = () => {
line-height: 1.5;
}
.member-field {
@include adaptive.adaptive-tree-field;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
min-height: 78rpx;
@@ -248,8 +249,6 @@ const showConflictHelp = () => {
margin-top: 12rpx;
padding: 12rpx 22rpx;
box-sizing: border-box;
background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png")
center / 100% 100% no-repeat;
}
.member-field > text {
z-index: 1;
@@ -0,0 +1,368 @@
# A01 自适应结构证明 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将 A01 从“整页合成背景 + 交互内容叠层定位”改为真正的纵向 Flex 文档流,并在当前 MuMu `720×1280 / 320dpi` 上证明默认密码态和验证码态无需滑动即可完整显示。
**Architecture:** A01 根容器只负责最小可用视口和自然溢出;固定比例品牌头图、独立宣纸内容区和登录内容形成真实布局层级。品牌印章可以在头图内部作为装饰定位,所有标题、输入、按钮和协议都在宣纸区正常流中,不再使用背景地标作为坐标。
**Tech Stack:** uni-app、Vue 3、SCSS、PowerShell 合同测试、Node.js CDP 辅助检查、HBuilderX Android、MuMu、ADB。
## Global Constraints
- 规格源:`docs/superpowers/specs/2026-07-21-project-mobile-viewport-adaptation-design.md`
- 本计划只覆盖 A01;用户确认 MuMu 画面前不迁移其他页面。
- 不改变登录业务、字段、校验、协议、反馈、路由和文案语义。
- 删除 A01 对整页合成背景、`scaleToFill``page-canvas``page-backdrop` 和整页网格叠层的依赖。
- 头图使用 `a01-vnext-header-v1.png`,保持原始 `824:340` 比例。
- 宣纸区使用 `auth-page-paper.jpg` 作可重复装饰背景,不承担内容坐标。
- MuMu 约 `360×640dp` 默认密码态和验证码态不滚动;`320×568`、错误、大字号或键盘空间不足时允许自然滚动但不得裁切。
- 交互目标最小高度 `44px`;禁止整页缩放。
- H5 只作结构辅助验证,最终视觉放行必须来自 MuMu 原生截图。
---
### Task 1: 撤销被否定的旧视口实验
**Files:**
- Revert: 当前工作区中上一版 viewport 实验修改的组件、页面、样式和测试
- Delete: `docs/superpowers/plans/2026-07-21-project-mobile-viewport-adaptation.md`
- Delete: `tests/mobile-viewport-page-contract.json`
- Delete: `tests/mobile-viewport-page-contract.ps1`
- Delete: `tests/mobile-viewport-runtime-smoke.js`
**Interfaces:**
- Consumes: 已提交的新设计规格 `2d33ee7`
- Produces: 无上一版实验残留的 A01 实施基线
- [ ] **Step 1: 核对待撤销集合**
Run: `git status --short``git diff --name-only`
Expected: 修改集合只包含上一版实验触及的组件、页面、样式、A 系列/共享测试和风险白名单。发现未知修改时停止整批恢复并逐文件保留。
- [ ] **Step 2: 精确恢复已确认属于旧实验的跟踪文件**
仅在 Step 1 的集合完全吻合后运行:
```powershell
$experimentFiles = git diff --name-only
foreach ($file in $experimentFiles) { git restore -- $file }
```
禁止运行 `git restore .`;不得恢复设计规格和本计划。
- [ ] **Step 3: 用 `apply_patch` 精确删除四个旧实验新增文件**
只删除本任务 Files 中列出的旧计划和三个 `mobile-viewport-*` 文件。
- [ ] **Step 4: 验证基线**
Run: `powershell -ExecutionPolicy Bypass -File tests/a01-a02-ui-contract.ps1`
Expected: `A01-LOGIN-MERGE-CONTRACT PASS`,且 `git status --short` 只显示本计划。
---
### Task 2: 用失败合同锁定新结构
**Files:**
- Modify: `tests/a01-a02-ui-contract.ps1`
- Test: `tests/a01-a02-ui-contract.ps1`
**Interfaces:**
- Produces: A01 头图、宣纸和正常流静态合同
- [ ] **Step 1: 替换旧坐标断言**
保留业务、文案、交互、按钮皮肤、Toast 和 Dialog 断言。删除要求整页背景、品牌印章网格对齐及 `--auth-paper-start` 的断言,加入:
```powershell
foreach ($required in @('class="auth-header"','class="auth-header__image"','a01-vnext-header-v1.png','mode="widthFix"','class="auth-paper"','auth-page-paper.jpg')) {
Assert-Contains -Content $entry -Expected $required -Message "A01 adaptive structure is missing: $required"
}
foreach ($forbidden in @('class="page-canvas"','class="page-backdrop"','a01-red-hall-ink-backdrop-v1.png','mode="scaleToFill"','--auth-paper-start','1665rpx')) {
Assert-NotContains -Content $entry -Unexpected $forbidden -Message "A01 still uses rejected page coordinates: $forbidden"
}
$pageRule = [regex]::Match($entry, '(?ms)^\.auth-page\s*\{(?<Body>.*?)^\}')
$paperRule = [regex]::Match($entry, '(?ms)^\.auth-paper\s*\{(?<Body>.*?)^\}')
$contentRule = [regex]::Match($entry, '(?ms)^\.login-content\s*\{(?<Body>.*?)^\}')
Assert-Contains $pageRule.Groups['Body'].Value 'display: flex;' 'A01 root must use flex flow.'
Assert-Contains $pageRule.Groups['Body'].Value 'min-height: var(--app-viewport-height);' 'A01 root must fill without locking content height.'
Assert-NotContains $pageRule.Groups['Body'].Value 'height: var(--app-viewport-height);' 'A01 root must not lock the viewport.'
Assert-Contains $paperRule.Groups['Body'].Value 'flex: 1;' 'A01 paper must receive remaining space.'
Assert-Contains $contentRule.Groups['Body'].Value 'justify-content: space-between;' 'A01 must distribute spare height in normal flow.'
Assert-NotContains $contentRule.Groups['Body'].Value 'grid-area:' 'A01 interactive content must not overlap page artwork.'
```
- [ ] **Step 2: 验证 RED**
Run: `powershell -ExecutionPolicy Bypass -File tests/a01-a02-ui-contract.ps1`
Expected: FAIL,提示缺少 `auth-header` 或仍存在 `page-canvas`
- [ ] **Step 3: 提交失败合同**
Run: `git add tests/a01-a02-ui-contract.ps1`,然后 `git commit -m "测试A01自适应页面结构"`
---
### Task 3: 建立最小全局视口原语
**Files:**
- Modify: `styles/global.scss`
**Interfaces:**
- Produces: `--app-viewport-height``--app-safe-top``--app-safe-bottom``--app-touch-min`
- Consumed by: `pages/auth/a01-entry.vue`
- [ ] **Step 1: 在现有 `page` 规则中加入变量**
```scss
page {
--app-viewport-height: 100vh;
--app-safe-top: env(safe-area-inset-top, 0px);
--app-safe-bottom: env(safe-area-inset-bottom, 0px);
--app-touch-min: 44px;
}
@supports (height: 100dvh) {
page { --app-viewport-height: 100dvh; }
}
```
本任务不增加全项目页面类,不修改共享组件或其他路由。
- [ ] **Step 2: 验证并提交**
Run: `git diff --check -- styles/global.scss`
Expected: exit 0,差异只有四个变量和 `100dvh` 能力覆盖。
Run: `git add styles/global.scss`,然后 `git commit -m "增加手机可用视口基础变量"`
---
### Task 4: 将 A01 重构为正常流
**Files:**
- Modify: `pages/auth/a01-entry.vue`
- Test: `tests/a01-a02-ui-contract.ps1`
**Interfaces:**
- Consumes: Task 3 的四个变量
- Produces: `.auth-header``.auth-header__image``.auth-paper`、正常流 `.login-content`
- [ ] **Step 1: 替换顶层模板**
```diff
- <view class="auth-page login-page">
- <view class="page-canvas">
- <image
- class="page-backdrop"
- src="/static/assets/modules/auth/opaque/a01-red-hall-ink-backdrop-v1.png"
- mode="scaleToFill"
- />
+ <view class="auth-page login-page">
+ <view class="auth-header">
+ <image
+ class="auth-header__image"
+ src="/static/assets/modules/auth/opaque/a01-vnext-header-v1.png"
+ mode="widthFix"
+ />
<image
class="brand-seal"
src="/static/assets/foundation/transparent/brand-seal.png"
mode="aspectFit"
/>
+ </view>
+ <view class="auth-paper">
<view class="login-content">
```
该局部替换后,现有 `.login-content` 的全部子节点及其紧随的两个闭合 `</view>` 不变:第一个仍关闭 `.login-content`,第二个改为关闭 `.auth-paper``verification-layer``feedback-toast` 继续保留在 `.auth-page` 根节点内。
- [ ] **Step 2: 写入页面骨架样式**
```scss
.auth-page {
display: flex;
flex-direction: column;
box-sizing: border-box;
width: 100%;
min-height: var(--app-viewport-height);
overflow-x: hidden;
background: #940400;
color: #493323;
}
.auth-header {
position: relative;
flex: 0 0 auto;
box-sizing: border-box;
width: 100%;
padding-top: var(--app-safe-top);
background: #940400;
}
.auth-header__image { display: block; width: 100%; height: auto; pointer-events: none; }
.brand-seal {
position: absolute;
top: calc(var(--app-safe-top) + clamp(8px, 2vw, 14px));
left: 50%;
width: clamp(60px, 19vw, 86px);
height: clamp(72px, 23vw, 104px);
transform: translateX(-50%);
pointer-events: none;
}
.auth-paper {
display: flex;
flex: 1;
box-sizing: border-box;
width: 100%;
padding-bottom: var(--app-safe-bottom);
background: #f7f0e5 url("/static/assets/foundation/opaque/auth-page-paper.jpg") center top / 100% auto repeat-y;
}
.login-content {
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
box-sizing: border-box;
width: 100%;
max-width: 480px;
min-width: 0;
margin: 0 auto;
padding: clamp(10px, 2.2vh, 20px) clamp(24px, 8.5vw, 44px);
}
```
- [ ] **Step 3: 收敛结构高度而不建立页面坐标**
```scss
.login-heading { min-height: 56px; }
.login-tabs, .input-row, .form-secondary-row, .login-submit,
.wechat-login, .register-entry, .agreement-row { min-height: var(--app-touch-min); }
.login-submit, .wechat-login { height: var(--app-touch-min); }
.other-login-divider { min-height: 30px; }
```
删除旧视口固定 `height`、旧五个 `--auth-*height` 变量及对应 `@supports`。按钮内部皮肤与文案的局部网格叠放可以保留;Verification 和 Toast 等真实 Overlay 可以保留定位。
- [ ] **Step 4: 验证 GREEN**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/a01-a02-ui-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/a01-asset-alpha-audit.ps1
git diff --check
```
Expected: 两个合同 PASSdiff check exit 0。
- [ ] **Step 5: 提交实现**
Run: `git add pages/auth/a01-entry.vue`,然后 `git commit -m "重构A01为自适应文档流"`
---
### Task 5: 修订多视口运行合同
**Files:**
- Modify: `tests/a01-responsive-runtime-smoke.js`
**Interfaces:**
- Consumes: `.auth-header__image``.auth-paper``.login-content`
- Produces: 六档视口结构、比例、首屏和可达性断言
- [ ] **Step 1: 采集新指标**
`320×568``360×616``360×640``360×800``412×915``480×1040` 读取 root/document scrollWidth/scrollHeight、头图运行尺寸和自然尺寸、header bottom、paper top、content top、agreement bottom。
- [ ] **Step 2: 写入准确断言**
```js
assert(metrics.scrollWidth <= size.width + 1)
assert.deepStrictEqual([metrics.headerNaturalWidth, metrics.headerNaturalHeight], [824, 340])
assert(Math.abs(metrics.headerImageHeight / metrics.headerImageWidth - 340 / 824) < 0.01)
assert(metrics.paperTop >= metrics.headerBottom - 1)
assert(metrics.contentTop >= metrics.paperTop)
if (size.width >= 360 && size.height >= 640) {
assert(metrics.rootScrollHeight <= size.height + 1)
assert(metrics.agreementBottom <= size.height + 1)
} else {
assert(metrics.agreementBottom <= metrics.documentScrollHeight + 1)
}
```
密码态和验证码态均执行首屏断言。保留图标、协议错误、Toast、Dialog 和登录方式切换测试。
- [ ] **Step 3: 运行辅助检查并提交**
前提:复用 HBuilderX 已运行的 `http://localhost:5173` 和 Chrome `9222`;不存在时报告阻塞,不打开浏览器冒充 Android。
Run: `node tests/a01-responsive-runtime-smoke.js`
Expected: `A01-RESPONSIVE-RUNTIME-SMOKE PASS`
Run: `git add tests/a01-responsive-runtime-smoke.js`,然后 `git commit -m "验证A01多视口自适应结构"`
---
### Task 6: MuMu 原生验收门
**Files:**
- Create ignored evidence: `%TEMP%\jiapuapp-audit\2026-07-21\A01-adaptive-*.png`
- Modify after user acceptance only: `docs/验收规划.md`
- Modify after user acceptance only: `docs/交接记录.md`
**Interfaces:**
- Consumes: HBuilderX Android 应用 `io.dcloud.HBuilder`
- Produces: 当前 MuMu 的密码、验证码、协议错误和键盘状态证据
- [ ] **Step 1: 确认设备**
```powershell
& 'C:\Users\Administrator\Desktop\HBuilderX\plugins\launcher-tools\tools\adbs\adb.exe' devices
& 'C:\Users\Administrator\Desktop\HBuilderX\plugins\launcher-tools\tools\adbs\adb.exe' -s emulator-5554 shell wm size
& 'C:\Users\Administrator\Desktop\HBuilderX\plugins\launcher-tools\tools\adbs\adb.exe' -s emulator-5554 shell wm density
```
Expected: `emulator-5554 device``720x1280``320`。按实际结果记录,不改变系统分辨率伪造验证。
- [ ] **Step 2: 截取密码态与验证码态**
确认 HBuilderX 热更新后重新进入 A01,随后运行:
```powershell
$evidence = Join-Path $env:TEMP 'jiapuapp-audit\2026-07-21'
New-Item -ItemType Directory -Force -Path $evidence | Out-Null
$adb = 'C:\Users\Administrator\Desktop\HBuilderX\plugins\launcher-tools\tools\adbs\adb.exe'
& $adb -s emulator-5554 shell screencap -p /sdcard/A01-adaptive-password.png
& $adb -s emulator-5554 pull /sdcard/A01-adaptive-password.png (Join-Path $evidence 'A01-adaptive-password.png')
```
切换验证码态后,将两处 `password` 明确替换为 `sms` 并再次执行。不得使用浏览器截图替代。
- [ ] **Step 3: 验证默认态不滑动**
密码态和验证码态分别记录滑动前截图,向上滑动后再截图;默认态内容位置不得移动。检查头图无拉伸、印章在头图内、标题完全位于宣纸、全部控件和协议完整、安全区无遮挡。
- [ ] **Step 4: 验证异常自然滚动**
触发未勾选协议错误并聚焦第二输入框。错误文案必须完整;键盘不得永久遮挡焦点字段和当前操作;需要时页面允许自然滚动,键盘收起后恢复合理位置。
- [ ] **Step 5: 用户视觉放行**
向用户展示 MuMu 原生截图。只有用户明确确认后才更新验收规划和交接记录;记录必须区分自动合同、当前 MuMu 已验证状态和其他真机未验证状态。未确认时不开始 A04/A05/A06。
- [ ] **Step 6: 最终回归**
```powershell
powershell -ExecutionPolicy Bypass -File tests/a01-a02-ui-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/a01-asset-alpha-audit.ps1
node tests/a01-responsive-runtime-smoke.js
git diff --check
git status --short
```
Expected: 可运行项全部通过;只存在本阶段有意修改。用户确认后才提交验收记录,提交信息为 `记录A01安卓自适应验收`
@@ -0,0 +1,322 @@
# 认证页自适应迁移 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 从已在 MuMu 放行的 A01 提取最小认证页骨架,并将 A04、A05 与封存 A06 从固定设计稿画布迁移为可自然增长的手机自适应文档流。
**Architecture:** `components/AuthPageShell.vue` 单一拥有认证页的可用视口、顶部品牌头图、已确认的 Logo 位置、宣纸内容背景与安全区。A01、A04、A05、A06 只拥有各自表单、状态、弹层和业务逻辑;长表单在空间不足、错误或键盘状态下自然滚动,不通过整页缩放或机型断点硬塞进首屏。
**Tech Stack:** uni-app、Vue 3、SCSS、PowerShell 静态合同、Node.js CDP 运行时辅助检查、HBuilderX Android、MuMu、ADB。
## Global Constraints
- 规格源:`docs/superpowers/specs/2026-07-21-project-mobile-viewport-adaptation-design.md`
- A01 已确认的头图比例、Logo 尺寸与位置、字号体系和内容比例不得因抽取骨架发生可见回退。
- 不改变注册、重设密码、账号状态的字段、校验、状态机、文案语义和路由行为。
- A06 保留源码和测试但不恢复到 `pages.json`
- 禁止认证页继续使用 `page-canvas``page-backdrop`、整页 `scaleToFill``1665rpx` 固定画布或交互内容整页叠层。
- 共享骨架只拥有背景、视口与安全区,不接收页面编号或业务配置。
- Toast 统一消费 `--status-bar-height`;同一安全区不得被骨架和业务页面重复计入。
- H5 只作自动结构验证;每个活动认证页最终视觉放行必须来自 MuMu。
---
### Task 1: 用失败合同锁定共享认证页骨架
**Files:**
- Create: `tests/auth-page-shell-contract.ps1`
- Test: `tests/auth-page-shell-contract.ps1`
**Interfaces:**
- Produces: `AuthPageShell` 的单一职责合同,以及首个消费者 A01 禁止旧画布的合同;其余页面由各自任务的合同接管
- [ ] **Step 1: 写入失败合同**
合同读取 `components/AuthPageShell.vue` 和四个认证页,并断言:
```powershell
$shellPath = Join-Path $root 'components/AuthPageShell.vue'
if (-not (Test-Path -LiteralPath $shellPath)) { throw 'AuthPageShell is missing' }
$shell = Get-Content -LiteralPath $shellPath -Raw -Encoding utf8
foreach ($required in @(
'class="auth-shell__header"',
'class="auth-shell__header-image"',
'a01-vnext-header-v1.png',
'mode="widthFix"',
'class="auth-shell__paper"',
'auth-page-paper.jpg',
'<slot />',
'<slot name="overlay" />',
'min-height: var(--app-viewport-height);'
)) { Assert-Contains $shell $required "AuthPageShell is missing: $required" }
foreach ($pageName in @('a01-entry.vue')) {
$page = Get-Content -LiteralPath (Join-Path $root "pages/auth/$pageName") -Raw -Encoding utf8
Assert-Contains $page '<AuthPageShell' "$pageName must consume AuthPageShell"
foreach ($forbidden in @('class="page-canvas"','class="page-backdrop"','mode="scaleToFill"','1665rpx')) {
Assert-NotContains $page $forbidden "$pageName retains rejected page coordinates: $forbidden"
}
}
```
- [ ] **Step 2: 验证 RED**
Run: `powershell -ExecutionPolicy Bypass -File tests/auth-page-shell-contract.ps1`
Expected: FAIL,提示 `AuthPageShell is missing`
- [ ] **Step 3: 提交失败合同**
Run: `git add tests/auth-page-shell-contract.ps1`,然后 `git commit -m "测试认证页共享自适应骨架"`
---
### Task 2: 提取骨架并无损迁移 A01
**Files:**
- Create: `components/AuthPageShell.vue`
- Modify: `pages/auth/a01-entry.vue`
- Test: `tests/auth-page-shell-contract.ps1`
- Test: `tests/a01-a02-ui-contract.ps1`
- Test: `tests/a01-responsive-runtime-smoke.js`
**Interfaces:**
- Produces: `<AuthPageShell><slot /><template #overlay>…</template></AuthPageShell>`
- Consumes: `--app-viewport-height``--app-safe-top``--app-safe-bottom`
- [ ] **Step 1: 创建最小共享组件**
组件模板固定为品牌头图、Logo、宣纸默认槽和根级 overlay 槽:
```vue
<template>
<view class="auth-shell">
<view class="auth-shell__header">
<image class="auth-shell__header-image" src="/static/assets/modules/auth/opaque/a01-vnext-header-v1.png" mode="widthFix" />
<image class="auth-shell__seal" src="/static/assets/foundation/transparent/brand-seal.png" mode="aspectFit" />
</view>
<view class="auth-shell__paper"><slot /></view>
<slot name="overlay" />
</view>
</template>
```
根、头图、Logo 和宣纸样式逐项迁入组件;Logo 使用 A01 已放行的 `top: calc(var(--app-safe-top) + clamp(46px, 11.5vw, 58px))``width: clamp(56px, 17vw, 78px)``height: clamp(67px, 20.4vw, 94px)`
- [ ] **Step 2: 迁移 A01 模板**
`AuthPageShell` 包住 `.login-content`,将验证层和 Toast 放入 `#overlay`。删除 A01 内已迁移到组件的 `.auth-page``.auth-header``.auth-header__image``.brand-seal``.auth-paper` 样式;登录业务和其余样式不动。
- [ ] **Step 3: 验证 GREEN**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/auth-page-shell-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/a01-a02-ui-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/a01-asset-alpha-audit.ps1
node tests/a01-responsive-runtime-smoke.js
git diff --check
```
Expected: 四项 PASSdiff check exit 0MuMu A01 默认密码态与已放行截图无可见回退。
- [ ] **Step 4: 提交骨架**
Run: `git add components/AuthPageShell.vue pages/auth/a01-entry.vue tests/a01-a02-ui-contract.ps1`,然后 `git commit -m "提取认证页共享自适应骨架"`
---
### Task 3: 用失败合同锁定 A04 自适应结构
**Files:**
- Modify: `tests/a04-registration-contract.ps1`
- Modify: `tests/a04-registration-runtime-smoke.js`
**Interfaces:**
- Consumes: `AuthPageShell`
- Produces: A04 正常流、自然滚动、六档宽度和 Toast 状态栏合同
- [ ] **Step 1: 替换旧视觉坐标断言**
删除要求旧背景、`184rpx × 221rpx` Logo 和固定画布的断言;增加 `AuthPageShell``.register-content` Flex 文档流、`--app-touch-min``var(--status-bar-height, 0px)` 断言,并禁止 `page-canvas``page-backdrop``scaleToFill``1665rpx`
- [ ] **Step 2: 扩展运行时尺寸**
运行数组统一为 `320×568``360×616``360×640``360×800``412×915``480×1040`。采集 `scrollWidth``scrollHeight`、头图自然尺寸和比例、paper/header 边界、最后登录入口底部;断言无横向溢出、头图为 `824:340`、短屏允许自然滚动且末项可达。
- [ ] **Step 3: 验证 RED 并提交**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/a04-registration-contract.ps1
node tests/a04-registration-runtime-smoke.js
```
Expected: 至少一项因 A04 仍使用旧画布而 FAIL。
Run: `git add tests/a04-registration-contract.ps1 tests/a04-registration-runtime-smoke.js`,然后 `git commit -m "测试A04注册页自适应结构"`
---
### Task 4: 将 A04 迁移为长表单文档流
**Files:**
- Modify: `pages/auth/a04-register.vue`
- Test: `tests/auth-page-shell-contract.ps1`
- Test: `tests/a04-registration-contract.ps1`
- Test: `tests/a04-registration-runtime-smoke.js`
**Interfaces:**
- Consumes: `AuthPageShell`
- Produces: A04 默认、错误、协议和 Toast 状态的自然流布局
- [ ] **Step 1: 替换模板骨架**
导入 `AuthPageShell`,删除 `page-canvas``page-backdrop`,将 `.register-content` 放入默认槽,将 Toast 放入 `#overlay`
- [ ] **Step 2: 写入页面自有布局**
`.register-content` 使用 `display:flex; flex:1; flex-direction:column; box-sizing:border-box; width:100%; max-width:480px; margin:0 auto; padding:clamp(10px,2.2vh,20px) clamp(24px,8.5vw,44px)`。标题、输入行、按钮、协议和返回登录均处于普通流;输入行和操作目标最小高度为 `var(--app-touch-min)`;删除只为旧画布服务的 `@media max-width:340px` 坐标补丁。
- [ ] **Step 3: 统一 Toast 安全区**
将 Toast 顶部改为:
```scss
top: calc(var(--status-bar-height, 0px) + 24rpx);
```
- [ ] **Step 4: 验证 GREEN**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/auth-page-shell-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/a04-registration-contract.ps1
node tests/a04-registration-runtime-smoke.js
git diff --check
```
Expected: 三项 PASSdiff check exit 0。
- [ ] **Step 5: MuMu 用户门与提交**
在 MuMu 展示 A04 默认空表单;用户确认后再展示空提交错误和 Toast。只有用户明确通过当前状态后,提交相关源码和测试,提交信息为 `迁移A04为自适应文档流`
---
### Task 5: 迁移 A05 并保留结果弹层滚动所有权
**Files:**
- Modify: `tests/a05-reset-password-contract.ps1`
- Modify: `tests/a05-reset-password-runtime-smoke.js`
- Modify: `pages/auth/a05-reset-password.vue`
**Interfaces:**
- Consumes: `AuthPageShell`
- Produces: A05 四字段长表单、Toast 和成功结果弹层
- [ ] **Step 1: 写失败合同并验证 RED**
静态和运行合同采用与 A04 相同的六档结构断言,额外保留获取验证码、密码不一致、成功层和返回 A01 行为;成功层必须使用 `max-height: calc(var(--app-viewport-height) - 40px)` 和内部唯一纵向滚动。
Run: `powershell -ExecutionPolicy Bypass -File tests/a05-reset-password-contract.ps1`
Expected: 因旧画布或缺少共享骨架而 FAIL。
- [ ] **Step 2: 实现并验证 GREEN**
`AuthPageShell` 包住 `.reset-content`overlay 槽承载成功层和 Toast;页面表单改为正常 Flex 文档流,Toast 使用状态栏高度,结果弹层保留 fixed overlay 与内部滚动。
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/auth-page-shell-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/a05-reset-password-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/a05-success-asset-contract.ps1
node tests/a05-reset-password-runtime-smoke.js
git diff --check
```
Expected: 四项 PASSdiff check exit 0。
- [ ] **Step 3: MuMu 用户门与提交**
依次展示默认、字段错误、Toast、成功弹层;用户明确通过后提交,提交信息为 `迁移A05为自适应文档流`
---
### Task 6: 同步封存 A06 源码但不恢复路由
**Files:**
- Modify: `tests/a06-auth-status-contract.ps1`
- Modify: `pages/auth/a06-auth-status.vue`
**Interfaces:**
- Consumes: `AuthPageShell`
- Produces: 与活动认证页一致的封存源码;`pages.json` 继续不含 A06
- [ ] **Step 1: 写失败合同并验证 RED**
删除旧背景和固定 Logo 断言,要求 `AuthPageShell` 且禁止旧画布;继续断言 `pages.json` 不包含 A06、runtime smoke 明确 SKIP。
Run: `powershell -ExecutionPolicy Bypass -File tests/a06-auth-status-contract.ps1`
Expected: 因 A06 尚未消费共享骨架而 FAIL。
- [ ] **Step 2: 实现、验证并提交**
迁移页面骨架和内容流,不改变状态配置、恢复弹层、返回逻辑或路由清单。
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/auth-page-shell-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/a06-auth-status-contract.ps1
node tests/a06-auth-status-runtime-smoke.js
git diff --check
```
Expected: 静态合同 PASSruntime 输出 `A06-AUTH-STATUS-RUNTIME-SMOKE SKIP archived route`diff check exit 0。
Run: `git add pages/auth/a06-auth-status.vue tests/a06-auth-status-contract.ps1`,然后 `git commit -m "同步封存A06自适应骨架"`
---
### Task 7: 更新记录并执行认证页总回归
**Files:**
- Modify: `docs/交接记录.md`
- Modify: `docs/验收规划.md`
**Interfaces:**
- Consumes: 用户逐状态 MuMu 结论
- Produces: 区分自动合同、当前 MuMu 状态与未验证真机的准确记录
- [ ] **Step 1: 只记录用户明确通过的状态**
A04、A05 未逐态确认的状态继续保持待审核;不得把共享骨架合同通过写成整页或功能通过。A06 继续标明封存且不计入活动页。
- [ ] **Step 2: 运行总回归**
```powershell
powershell -ExecutionPolicy Bypass -File tests/auth-page-shell-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/a01-a02-ui-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/a04-registration-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/a05-reset-password-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/a06-auth-status-contract.ps1
node tests/a01-responsive-runtime-smoke.js
node tests/a04-registration-runtime-smoke.js
node tests/a05-reset-password-runtime-smoke.js
node tests/a06-auth-status-runtime-smoke.js
git diff --check
git status --short
```
Expected: 活动页合同和 runtime 全部 PASSA06 runtime 明确 SKIP;工作区只包含本阶段有意记录。
- [ ] **Step 3: 提交记录**
Run: `git add docs/交接记录.md docs/验收规划.md`,然后 `git commit -m "记录认证页安卓自适应验收"`
@@ -0,0 +1,166 @@
# G01 Role And Shortcut State Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make G01 display the selected genealogy's real role and hide the application-review shortcut for ordinary members.
**Architecture:** Keep `membership` on the selected genealogy as the sole role source. Derive the role label and visible shortcut list inside G01 so initial render and switcher changes share the same reactive rule.
**Tech Stack:** Vue 3 Composition API, UniApp, PowerShell contract tests, existing G01 browser runtime smoke, MuMu Android manual verification.
## Global Constraints
- `membership === "created"` displays “管理员” and four shortcuts.
- `membership === "joined"` displays “成员” and only “世系图、成员、字辈诗”.
- Do not add a replacement shortcut or an invisible application-review hit target for members.
- Preserve the existing switcher, list scroll reset, assets, layout, and routes.
- MuMu screenshots are the visual acceptance source; browser runtime is logic regression evidence only.
---
### Task 1: Drive G01 Role And Shortcuts From Membership
**Files:**
- Create: `tests/g01-role-shortcut-contract.ps1`
- Modify: `tests/g01-switch-dialog-runtime-smoke.js`
- Modify: `pages/genealogy/g01-my-genealogies.vue`
**Interfaces:**
- Consumes: `currentGenealogy.value.membership`, where the supported values are `created` and `joined`.
- Produces: `currentRoleLabel: ComputedRef<string>` and `visibleShortcuts: ComputedRef<Array<{ key: string, label: string, icon: string }>>` for the existing G01 template.
- [x] **Step 1: Write the failing source contract**
```powershell
$ErrorActionPreference = 'Stop'
$page = Get-Content -Raw 'pages/genealogy/g01-my-genealogies.vue'
function Assert-Contains([string]$Content, [string]$Pattern, [string]$Message) {
if (-not $Content.Contains($Pattern)) { throw $Message }
}
Assert-Contains $page 'v-for="item in visibleShortcuts"' 'G01 must render the membership-filtered shortcut list'
Assert-Contains $page '{{ currentRoleLabel }}' 'G01 must render the membership-derived role label'
Assert-Contains $page 'currentGenealogy.value?.membership === "created"' 'G01 must derive owner state from membership'
Assert-Contains $page 'item.key !== "applications"' 'G01 must remove application review for joined members'
Write-Output 'G01-ROLE-SHORTCUT-CONTRACT PASS'
```
- [x] **Step 2: Run the source contract and verify RED**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/g01-role-shortcut-contract.ps1
```
Expected: FAIL with `G01 must render the membership-filtered shortcut list` because G01 still renders `shortcuts` directly.
- [x] **Step 3: Extend the runtime smoke with role-state assertions**
Immediately after selecting the second switcher item, add:
```js
assert(
(await valueOf(send, "document.querySelector('.current-meta')?.textContent"))?.includes('成员'),
'Joined genealogy did not display the member role'
)
assert(
(await valueOf(send, "document.querySelectorAll('.shortcut-item').length")) === 3,
'Joined genealogy did not hide the application-review shortcut'
)
assert(
!(await valueOf(send, "document.querySelector('.shortcut-grid')?.textContent"))?.includes('申请审核'),
'Joined genealogy still exposed application review'
)
```
Immediately after restoring the first switcher item, add:
```js
assert(
(await valueOf(send, "document.querySelector('.current-meta')?.textContent"))?.includes('管理员'),
'Created genealogy did not restore the administrator role'
)
assert(
(await valueOf(send, "document.querySelectorAll('.shortcut-item').length")) === 4,
'Created genealogy did not restore all four shortcuts'
)
```
- [x] **Step 4: Implement the minimal reactive role rule**
Change the shortcut loop and role text in the template:
```vue
<view
v-for="item in visibleShortcuts"
:key="item.key"
class="shortcut-item"
@click="openShortcut(item.key)"
>
```
```vue
<text class="current-meta-item-text">{{ currentRoleLabel }}</text>
```
After `currentGenealogy`, add:
```js
const isCurrentGenealogyOwner = computed(
() => currentGenealogy.value?.membership === "created",
);
const currentRoleLabel = computed(() =>
isCurrentGenealogyOwner.value ? "管理员" : "成员",
);
```
After the existing `shortcuts` array, add:
```js
const visibleShortcuts = computed(() =>
isCurrentGenealogyOwner.value
? shortcuts
: shortcuts.filter((item) => item.key !== "applications"),
);
```
- [x] **Step 5: Run focused automated verification**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/g01-role-shortcut-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/g01-genealogy-context-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/g01-empty-state-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/g01-error-state-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
git diff --check
```
Expected: every contract prints `PASS`; `git diff --check` exits 0.
If the H5 debug page and Chrome remote-debugging port `9222` are available, also run:
```powershell
node tests/g01-switch-dialog-runtime-smoke.js
```
Expected: `PASS G-01 switch dialog runtime smoke`.
- [x] **Step 6: Verify both roles in MuMu Android**
1. Open G01 with the created genealogy selected and capture the administrator state.
2. Open the switcher and select the joined genealogy.
3. Confirm the top role reads “成员”, exactly three shortcuts are visible, and there is no blank fourth slot or hidden application-review target.
4. Switch back to the created genealogy and confirm “管理员” plus four shortcuts return.
5. Confirm the list scroll resets to the top and the bottom tabbar remains unobstructed.
- [x] **Step 7: Commit the tested fix**
```powershell
git add tests/g01-role-shortcut-contract.ps1 tests/g01-switch-dialog-runtime-smoke.js pages/genealogy/g01-my-genealogies.vue docs/superpowers/plans/2026-07-21-g01-role-shortcut.md
git commit -m "fix: sync G01 role shortcuts"
```
@@ -0,0 +1,162 @@
# G09 Nine-Slice Application Cards Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 修复 G09“我的申请”列表卡片装饰边框被纵向拉伸的问题,并收紧卡片内部及卡片之间的间距。
**Architecture:** 保留现有 `list-slip-frame.png` 真实素材和卡片 DOM 结构,将 `.application-card` 从整图背景拉伸改为 CSS `border-image` 九宫格切片。视觉合同负责固定切片值、边框宽度和紧凑布局,并禁止旧的 `100% 100%` 背景方案回归。
**Tech Stack:** uni-app、Vue 3、SCSS、PowerShell 静态合同、MuMu Android 模拟器
## Global Constraints
- 仅修改 G09 列表卡片,不修改空数据、加载和错误状态卡片。
- 使用 `/static/assets/modules/genealogy/transparent/list-slip-frame.png`,切片为 `24 32`
- 卡片边框宽度为 `18rpx 24rpx`,卡片最小高度为 `210rpx`,卡片间距为 `12rpx`
- 正文最小高度为 `174rpx`,网格间距为 `8rpx 14rpx`,内距为 `26rpx 28rpx 22rpx 40rpx`
- 保留 `.application-card__action``white-space: nowrap`
- 不修改文案、业务状态、模拟数据、路由和交互。
- 视觉验收只使用 MuMu Android 原生画面,不使用浏览器。
- 不执行 Git 操作,不启用多代理。
---
### Task 1: 固定 G09 九宫格卡片视觉合同
**Files:**
- Modify: `tests/g09-all-states-visual-contract.ps1`
- Test: `tests/g09-all-states-visual-contract.ps1`
**Interfaces:**
- Consumes: `pages/genealogy/g09-my-applications.vue``<style scoped lang="scss">` 的卡片样式。
- Produces: 对九宫格素材、切片值、边框宽度、紧凑尺寸及旧拉伸背景禁用规则的静态合同。
- [ ] **Step 1: 写入九宫格和紧凑间距断言**
在现有 `$rules` 中加入以下规则:
```powershell
'(?s)\.application-card\s*\{[^}]*min-height:\s*210rpx;[^}]*margin-bottom:\s*12rpx;[^}]*border-width:\s*18rpx 24rpx;[^}]*border-style:\s*solid;[^}]*border-color:\s*transparent;[^}]*border-image-source:\s*url\("/static/assets/modules/genealogy/transparent/list-slip-frame\.png"\);[^}]*border-image-slice:\s*24 32;[^}]*border-image-width:\s*18rpx 24rpx;[^}]*border-image-repeat:\s*stretch;[^}]*box-sizing:\s*border-box;',
'(?s)\.application-card__body\s*\{[^}]*min-height:\s*174rpx;[^}]*gap:\s*8rpx 14rpx;[^}]*padding:\s*26rpx 28rpx 22rpx 40rpx;'
```
在循环前加入旧方案禁用断言:
```powershell
if ($page -match '(?s)\.application-card\s*\{[^}]*background:\s*url\("/static/assets/modules/genealogy/transparent/list-slip-frame\.png"\)[^}]*100%\s+100%') {
throw 'G09 application card must not stretch the complete frame asset to 100% 100%'
}
```
- [ ] **Step 2: 运行合同并确认先失败**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g09-all-states-visual-contract.ps1
```
Expected: FAIL,错误指出缺少 `210rpx``border-image-source` 或相关九宫格规则。
### Task 2: 将列表卡片改为九宫格边框
**Files:**
- Modify: `pages/genealogy/g09-my-applications.vue`
- Test: `tests/g09-all-states-visual-contract.ps1`
- Test: `tests/g09-document-flow-contract.ps1`
**Interfaces:**
- Consumes: Task 1 固定的样式合同及现有 `list-slip-frame.png`
- Produces: 不拉伸四角、保持文档流并缩短内部间隔的 G09 列表卡片。
- [ ] **Step 1: 替换 `.application-card` 样式**
`.application-card` 精确改为:
```scss
.application-card {
display: grid;
width: 100%;
min-height: 210rpx;
margin-bottom: 12rpx;
border-width: 18rpx 24rpx;
border-style: solid;
border-color: transparent;
border-image-source: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png");
border-image-slice: 24 32;
border-image-width: 18rpx 24rpx;
border-image-repeat: stretch;
box-sizing: border-box;
}
```
- [ ] **Step 2: 收紧 `.application-card__body` 布局**
保留现有网格列定义和 `box-sizing`,将尺寸精确改为:
```scss
min-height: 174rpx;
gap: 8rpx 14rpx;
padding: 26rpx 28rpx 22rpx 40rpx;
```
- [ ] **Step 3: 运行 G09 视觉和文档流合同**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g09-all-states-visual-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g09-document-flow-contract.ps1
```
Expected:
```text
G09-ALL-STATES-VISUAL-CONTRACT PASS
G09-DOCUMENT-FLOW-CONTRACT PASS
```
如文档流合同仍只接受 `background` 素材引用,将该合同的 G09 列表素材要求改为接受 `border-image-source`;主操作按钮素材要求保持不变。
### Task 3: 回归验证与 MuMu 原生验收
**Files:**
- Verify: `pages/genealogy/g09-my-applications.vue`
- Verify: `tests/g09-all-states-visual-contract.ps1`
- Verify: `tests/g09-document-flow-contract.ps1`
- Verify: `tests/g09-g10-business-state-contract.ps1`
**Interfaces:**
- Consumes: Task 2 的最终样式。
- Produces: 合同输出与 MuMu 原生截图证据,供用户决定当前页面是否通过。
- [ ] **Step 1: 运行三项聚焦合同**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g09-all-states-visual-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g09-document-flow-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g09-g10-business-state-contract.ps1
```
Expected: 三项均输出 `PASS`
- [ ] **Step 2: 在 MuMu 中刷新 G09 并截图**
使用当前已连接的 `emulator-5554`,刷新 `/pages/genealogy/g09-my-applications`,通过 Android `screencap` 保存新截图到 `tmp/G09-nine-slice-native.png`
Expected: 三张列表卡片四角比例正常,边线不显得纵向膨胀;卡片之间和卡片内部更紧凑;“修改后重新提交”保持单行且没有越界。
- [ ] **Step 3: 对照用户上一张截图检查**
只检查以下视觉差异:
```text
1. 金色装饰角不再纵向拉长。
2. 每张卡片的上下留白减少,但文字不压线。
3. 三张卡片之间的距离缩短。
4. 时间、状态、操作文字没有重叠或溢出。
```
若任一项失败,仅调整本计划中 `.application-card``.application-card__body` 的边框/间距值,再重复三项合同和 MuMu 截图;不改其他页面或业务逻辑。
@@ -0,0 +1,265 @@
# G10 All-States Responsive Visual Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 修复 G10 列表卡框、双操作按钮和审核弹窗在 MuMu 中的拉伸、细条及标题正文连行问题,并覆盖全部审核状态。
**Architecture:** `AppButton` 提供默认关闭的紧凑九宫格变体,`AppDialog` 只在显式请求且存在双操作时消费该变体。G10 使用共享变体替换页面内直接图片按钮,并把列表、结果及状态卡全部迁移到真实素材九宫格边框。
**Tech Stack:** uni-app、Vue 3、SCSS、PowerShell 静态合同、MuMu Android 模拟器
## Global Constraints
- `AppButton.compact``AppDialog.compactActions` 默认值均为 `false`
- 紧凑按钮切片固定为 `56 300 fill`,边框宽度固定为 `10rpx 26rpx`
- G10 卡框切片固定为 `24 32`,边框宽度固定为 `18rpx 24rpx`
- G10 双操作继续左右并排,按钮尺寸为 `148rpx × 68rpx`
- G10 结果条尺寸为 `308rpx × 68rpx`
- 不修改文案、模拟数据、路由、状态枚举、校验和审核逻辑。
- 视觉验收只使用 MuMu Android 原生画面。
- 不执行 Git 操作,不启用多代理。
---
### Task 1: AppButton 紧凑九宫格变体
**Files:**
- Create: `tests/app-button-compact-contract.ps1`
- Modify: `components/AppButton.vue`
- Test: `tests/shared-interaction-accessibility-contract.ps1`
- Test: `tests/shared-component-document-flow-contract.ps1`
**Interfaces:**
- Consumes: `type: "primary" | "secondary"`、现有点击和禁用接口。
- Produces: `compact: Boolean = false`;根类 `app-button--compact`
- [ ] **Step 1: 写入失败合同**
合同读取 `components/AppButton.vue` 并要求以下标记:
```powershell
foreach ($token in @(
'compact: { type: Boolean, default: false }',
"'app-button--compact': compact",
'border-image-slice: 56 300 fill;',
'border-width: 10rpx 26rpx;',
'a01-scroll-primary-v3.png',
'a01-scroll-secondary-v3.png'
)) {
if (-not $button.Contains($token)) { throw "AppButton compact contract missing: $token" }
}
```
并断言 `.app-button--compact .app-button__skin``display: none`
- [ ] **Step 2: 运行合同并确认失败**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/app-button-compact-contract.ps1`
Expected: FAIL,指出缺少 `compact`
- [ ] **Step 3: 实现最小变体**
在属性中加入:
```js
compact: { type: Boolean, default: false },
```
在根类对象中加入:
```js
'app-button--compact': compact
```
为主/次紧凑按钮分别设置对应卷轴 `border-image-source`,共同使用:
```scss
border-width: 10rpx 26rpx;
border-style: solid;
border-color: transparent;
border-image-slice: 56 300 fill;
border-image-width: 10rpx 26rpx;
border-image-repeat: stretch;
```
隐藏紧凑模式中的原图片皮肤,保留标签和原有事件。
- [ ] **Step 4: 运行组件合同**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/app-button-compact-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/shared-interaction-accessibility-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/shared-component-document-flow-contract.ps1
```
Expected: 三项均输出 `PASS`
### Task 2: AppDialog 标题分行与显式紧凑双操作
**Files:**
- Create: `tests/app-dialog-responsive-content-contract.ps1`
- Modify: `components/AppDialog.vue`
**Interfaces:**
- Consumes: Task 1 的 `AppButton.compact`
- Produces: `compactActions: Boolean = false``.app-dialog__copy` 独立文本容器。
- [ ] **Step 1: 写入失败合同**
合同要求:
```powershell
foreach ($token in @(
'compactActions: { type: Boolean, default: false }',
'class="app-dialog__copy"',
':compact="compactActions && showCancel"'
)) {
if (-not $dialog.Contains($token)) { throw "AppDialog responsive contract missing: $token" }
}
```
并要求 `.app-dialog__copy` 为纵向 flex、宽度 `100%`,标题和说明为 `display: block; width: 100%`
- [ ] **Step 2: 运行合同并确认失败**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/app-dialog-responsive-content-contract.ps1`
Expected: FAIL,指出缺少 `compactActions`
- [ ] **Step 3: 实现弹窗布局**
使用 `.app-dialog__copy` 包裹 eyebrow、title 和 message;在属性中加入:
```js
compactActions: { type: Boolean, default: false },
```
两个 `AppButton` 都传入:
```vue
:compact="compactActions && showCancel"
```
单按钮因 `showCancel === false` 保持默认宽卷轴。
- [ ] **Step 4: 运行弹窗及共享合同**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/app-dialog-responsive-content-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/shared-interaction-accessibility-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/shared-component-document-flow-contract.ps1
```
Expected: 三项均输出 `PASS`
### Task 3: G10 列表、结果与状态卡迁移
**Files:**
- Modify: `pages/genealogy/g10-application-review.vue`
- Modify: `tests/g10-all-states-visual-contract.ps1`
- Modify: `tests/g10-document-flow-contract.ps1`
- Test: `tests/g08-g10-application-flow-contract.ps1`
- Test: `tests/g09-g10-business-state-contract.ps1`
**Interfaces:**
- Consumes: `AppButton compact``AppDialog compact-actions`
- Produces: G10 九宫格卡框、左右紧凑操作、九宫格结果条和完整状态卡。
- [ ] **Step 1: 将 G10 合同切换到新契约**
删除 `g10-document-flow-contract.ps1` 对以下旧值的要求:
```text
background ... list-slip-frame.png ... 100% 100%
padding: 44rpx 52rpx 29rpx 64rpx
```
在视觉/文档流合同中要求:
```text
border-image-source: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png")
border-image-slice: 24 32
border-width: 18rpx 24rpx
min-height: 218rpx
margin-bottom: 12rpx
body min-height: 182rpx
gap: 8rpx 0
padding: 28rpx 28rpx 22rpx 40rpx
<AppButton compact type="secondary"
<AppButton compact
<AppDialog compact-actions
```
并禁止 `.application-card``.review-state-card` 继续使用 `100% 100%` 的完整背景拉伸。
- [ ] **Step 2: 运行 G10 合同并确认失败**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g10-all-states-visual-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g10-document-flow-contract.ps1
```
Expected: FAIL,指出缺少九宫格卡框或紧凑按钮。
- [ ] **Step 3: 修改 G10 模板和样式**
- 导入 `AppButton`
- 用两个 `AppButton compact` 替换 `.review-action` 内的直接图片。
-`AppDialog` 增加 `compact-actions`
- `.application-card` 使用 `24 32` 九宫格切片和规格中的 `218rpx` 尺寸。
- `.application-card__body` 使用 `182rpx``8rpx 0``28rpx 28rpx 22rpx 40rpx`
- `.review-result` 根据 approved/rejected 类使用对应卷轴九宫格并移除直接图片。
- `.review-state-card` 使用同一卡框九宫格,保留其文案布局。
- [ ] **Step 4: 运行页面合同**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g10-all-states-visual-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g10-document-flow-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g08-g10-application-flow-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g09-g10-business-state-contract.ps1
```
Expected: 四项均输出 `PASS`
### Task 4: MuMu 全状态验证
**Files:**
- Verify: `components/AppButton.vue`
- Verify: `components/AppDialog.vue`
- Verify: `pages/genealogy/g10-application-review.vue`
- Save screenshots: `tmp/G10-responsive-*.png`
**Interfaces:**
- Consumes: Task 13 的最终实现。
- Produces: 11 个 G10 状态的 MuMu 原生视觉证据。
- [ ] **Step 1: 运行全部聚焦合同**
运行 Task 1–3 的九项唯一合同,任何一项失败即停止视觉验收。
- [ ] **Step 2: 捕获 11 个 MuMu 状态**
依次捕获列表、说明、通过确认、通过完成、拒绝确认、拒绝空校验、拒绝完成、空、加载、错误、无权限。
- [ ] **Step 3: 逐项检查**
```text
1. 卡框四角不拉伸,卡片上下留白收紧。
2. 左右双按钮皮肤完整,文字居中,不重叠、不越界。
3. 标题和说明独立成行。
4. 结果条完整且不可点击。
5. 空/错/无权限卡框不变形。
6. 加载、重试、拒绝校验和审核业务行为不回退。
```
若任一状态不满足,只调整本规格定义的紧凑切片、G10 卡片间距或弹窗文本布局,并重新运行全部聚焦合同与对应 MuMu 状态。
@@ -0,0 +1,152 @@
# 全项目通用提示文案实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 清除全项目输入提示、示例和辅助说明中的具体人物或家族名称,同时完整保留页面模拟数据。
**Architecture:** 新增一个只扫描用户提示上下文的 PowerShell 合同,作为规则唯一验证入口。生产改动限定在 G03、G08 与共享 `TreeMemberForm` 的六处提示文案,不触碰任何 fixture、卡片或详情数据。
**Tech Stack:** uni-app、Vue 3、PowerShell 静态合同、现有页面合同、MuMu、ADB。
## Global Constraints
- 只检查 `placeholder`、带“例如/比如/如:”的示例和输入框附近辅助说明。
- 模拟数据、成员目录、详情卡、申请记录和测试输入值保持不变。
- G08 使用“某某某堂侄”表达关系格式;其他字段优先使用动作型提示。
- 不执行 Git 操作,不启用多代理。
- 最终视觉结论只依据 MuMu 原生画面。
---
### Task 1: 建立提示文案全项目合同
**Files:**
- Create: `tests/generic-guidance-copy-contract.ps1`
**Interfaces:**
- Consumes: `pages/**/*.vue``components/**/*.vue` 中的用户提示文案。
- Produces: 只针对提示上下文禁止具体姓名的项目级合同。
- [ ] **Step 1: 写入失败合同**
创建 `tests/generic-guidance-copy-contract.ps1`
```powershell
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$vueFiles = @(
Get-ChildItem -LiteralPath (Join-Path $root 'pages') -Recurse -Filter '*.vue'
Get-ChildItem -LiteralPath (Join-Path $root 'components') -Recurse -Filter '*.vue'
)
$specificCopy = @('汤正华', '汤正国', '汤文远', '汤氏家谱', '如:汤')
foreach ($file in $vueFiles) {
$lineNumber = 0
foreach ($line in Get-Content -LiteralPath $file.FullName -Encoding utf8) {
$lineNumber += 1
if ($line -notmatch 'placeholder|例如|比如|如:') { continue }
foreach ($token in $specificCopy) {
if ($line.Contains($token)) {
throw "User guidance must not contain specific copy '$token': $($file.FullName):$lineNumber"
}
}
}
}
$g08 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g08-join-application.vue') -Raw -Encoding utf8
foreach ($required in @('placeholder="例如:某某某堂侄"', '请以家谱中一位已知长辈为参照,例如:某某某堂侄')) {
if (-not $g08.Contains($required)) { throw "G08 generic relationship guidance missing: $required" }
}
Write-Output 'GENERIC-GUIDANCE-COPY-CONTRACT PASS'
```
- [ ] **Step 2: 运行合同并确认 RED**
```powershell
& powershell -ExecutionPolicy Bypass -File tests/generic-guidance-copy-contract.ps1
```
Expected: FAIL,首先报告 G03、G08 或 `TreeMemberForm` 仍含具体提示文案。
### Task 2: 替换六处提示并回归
**Files:**
- Modify: `pages/genealogy/g03-create-genealogy.vue`
- Modify: `pages/genealogy/g08-join-application.vue`
- Modify: `components/tree/TreeMemberForm.vue`
**Interfaces:**
- Consumes: Task 1 的提示文案合同。
- Produces: 不含具体姓名的动作型提示与通用关系示例。
- [ ] **Step 1: 做六处精确替换**
`pages/genealogy/g03-create-genealogy.vue`
```vue
placeholder="请输入姓氏"
placeholder="请输入家谱名称"
```
`pages/genealogy/g08-join-application.vue`
```vue
placeholder="例如:某某某堂侄"
```
```text
请以家谱中一位已知长辈为参照,例如:某某某堂侄
```
`components/tree/TreeMemberForm.vue`
```js
{ key: "name", label: "姓名", placeholder: "请输入首位成员姓名" },
{ key: "source", label: "当前成员", placeholder: "请选择当前成员" },
```
- [ ] **Step 2: 运行静态合同**
```powershell
$tests = @(
'tests/generic-guidance-copy-contract.ps1',
'tests/g03-create-flow-contract.ps1',
'tests/g03-visual-states-contract.ps1',
'tests/g08-g10-application-flow-contract.ps1',
'tests/g08-document-flow-contract.ps1',
'tests/tree-member-form-document-flow-contract.ps1'
)
foreach ($test in $tests) { & powershell -ExecutionPolicy Bypass -File $test }
```
Expected: 六项全部输出各自 `PASS`
- [ ] **Step 3: 运行 G08 流程回归**
```powershell
node tests/g08-g10-application-flow-runtime-smoke.js http://localhost:5173
```
Expected: 输出 `G08-G10-APPLICATION-FLOW-RUNTIME-SMOKE PASS`。若 H5 服务未运行,记录为环境阻塞,不用浏览器截图代替 MuMu 视觉验收。
### Task 3: MuMu 原生复核
**Files:**
- Create: `tmp/G08-generic-guidance-native.png`
**Interfaces:**
- Consumes: 已同步到 MuMu 的 G08 最新页面。
- Produces: 通用关系提示的原生证据。
- [ ] **Step 1: 捕获 G08 当前画面**
```powershell
$adb = 'C:\Users\Administrator\Desktop\HBuilderX\plugins\launcher-tools\tools\adbs\adb.exe'
& $adb -s emulator-5554 shell screencap -p /sdcard/G08-generic-guidance-native.png
& $adb -s emulator-5554 pull /sdcard/G08-generic-guidance-native.png tmp/G08-generic-guidance-native.png
```
- [ ] **Step 2: 检查并交给用户确认**
使用 `view_image` 原图确认关系输入和下方辅助说明都显示“某某某堂侄”,页面不再出现具体人物姓名。只报告当前默认状态,不把未查看的校验或提交结果状态标记为通过。
@@ -0,0 +1,335 @@
# 公共页头安全区与 G03 复用实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让所有普通二级页的公共页头自动避开 Android 状态栏,并让 G03 删除自建页头后复用同一组件。
**Architecture:** `components/PageHeader.vue` 继续作为根页面与二级页面页头的唯一实现;普通变体统一拥有“状态栏高度 + 104rpx 内容区”,根变体保持现状。G03 只提供动态标题和自定义返回处理,不再拥有页头结构、图标、背景或安全区样式。
**Tech Stack:** uni-app、Vue 3 Composition API、SCSS、PowerShell 静态契约、现有 Node.js CDP smoke、HBuilderX Android、MuMu、ADB。
## Global Constraints
- 普通二级页头总高度必须是 `calc(104rpx + var(--status-bar-height, 0px))`,状态栏补偿只能由 `PageHeader.vue` 持有。
- 根页面 `root` 变体继续使用现有 `calc(124rpx + var(--status-bar-height, 0px))`,不得改变 G01 已放行布局。
- `customBack` 默认值必须为 `false`;默认返回与深链 fallback 行为保持不变。
- G03 第一、第二步都必须使用公共 `PageHeader`,且第二步返回第一步的原业务行为保持不变。
- 不修改 G03 表单、弹层、按钮、背景和业务数据;原生 `step=ancestor` 路由参数问题另行修复。
- 自动检查覆盖 320px、360px、412px CSS 视口;最终视觉结论只依据 MuMu 原生画面。
---
### Task 1: 为公共 PageHeader 建立安全区与返回契约
**Files:**
- Modify: `tests/shared-component-document-flow-contract.ps1`
- Modify: `tests/shared-interaction-accessibility-contract.ps1`
- Modify: `components/PageHeader.vue`
**Interfaces:**
- Consumes: uni-app 提供的 `var(--status-bar-height, 0px)``getCurrentPages()``uni.navigateBack()``uni.reLaunch()`
- Produces: `customBack: Boolean = false` 属性和 `back` 事件;普通页头及占位槽均使用安全区总高度。
- [ ] **Step 1: 写入普通页头安全区的失败契约**
`tests/shared-component-document-flow-contract.ps1` 的 PageHeader 断言中加入:
```powershell
Assert-Match $header '(?s)\.page-header-slot\s*\{[^}]*height:\s*calc\(104rpx \+ var\(--status-bar-height, 0px\)\);' 'PageHeader secondary slot must reserve status bar plus 104rpx content height'
Assert-Match $header '(?s)\.page-header\s*\{[^}]*height:\s*calc\(104rpx \+ var\(--status-bar-height, 0px\)\);[^}]*padding-top:\s*var\(--status-bar-height, 0px\);' 'PageHeader secondary header must place its 104rpx content below the status bar'
Assert-Match $header '(?s)\.page-header-slot--root\s*\{[^}]*height:\s*calc\(124rpx \+ var\(--status-bar-height, 0px\)\);' 'PageHeader root slot height must remain unchanged'
Assert-Match $header '(?s)\.page-header--root\s*\{[^}]*height:\s*calc\(124rpx \+ var\(--status-bar-height, 0px\)\);' 'PageHeader root height must remain unchanged'
```
- [ ] **Step 2: 写入自定义返回的失败契约**
`tests/shared-interaction-accessibility-contract.ps1` 的 PageHeader 检查中加入:
```powershell
foreach ($token in @(
'customBack: { type: Boolean, default: false }',
'const emit = defineEmits(["brand", "notice", "action", "back"]);',
'if (props.customBack) {',
'emit("back");',
'uni.navigateBack();',
'uni.reLaunch({ url: props.fallbackUrl });'
)) {
if (-not $header.Contains($token)) { throw "PageHeader back contract missing: $token" }
}
```
- [ ] **Step 3: 运行聚焦契约并确认 RED**
Run:
```powershell
& powershell -ExecutionPolicy Bypass -File tests/shared-component-document-flow-contract.ps1
& powershell -ExecutionPolicy Bypass -File tests/shared-interaction-accessibility-contract.ps1
```
Expected: 两项均 FAIL,分别指出普通页头仍为固定 `104rpx``customBack` 合同不存在。
- [ ] **Step 4: 实现最小公共组件改动**
`components/PageHeader.vue` 中补充属性和事件:
```js
const props = defineProps({
title: { type: String, required: true },
action: { type: String, default: "" },
root: { type: Boolean, default: false },
unreadCount: { type: Number, default: 0 },
fallbackUrl: { type: String, default: "/pages/genealogy/g01-my-genealogies" },
customBack: { type: Boolean, default: false },
});
const emit = defineEmits(["brand", "notice", "action", "back"]);
const goBack = () => {
if (props.customBack) {
emit("back");
return;
}
const stack = getCurrentPages();
if (stack.length > 1) {
uni.navigateBack();
return;
}
uni.reLaunch({ url: props.fallbackUrl });
};
```
把普通变体的尺寸改为:
```scss
.page-header-slot {
height: calc(104rpx + var(--status-bar-height, 0px));
flex: none;
}
.page-header {
height: calc(104rpx + var(--status-bar-height, 0px));
padding: 0 24rpx;
padding-top: var(--status-bar-height, 0px);
}
```
保留 `.page-header-slot--root``.page-header--root` 的现有 `124rpx` 规则不变。
- [ ] **Step 5: 运行聚焦契约并确认 GREEN**
Run:
```powershell
& powershell -ExecutionPolicy Bypass -File tests/shared-component-document-flow-contract.ps1
& powershell -ExecutionPolicy Bypass -File tests/shared-interaction-accessibility-contract.ps1
git diff --check
```
Expected: 两项输出 `PASS``git diff --check` 退出码为 0。
- [ ] **Step 6: 提交公共组件改动**
```powershell
git add components/PageHeader.vue tests/shared-component-document-flow-contract.ps1 tests/shared-interaction-accessibility-contract.ps1
git commit -m "fix: adapt shared page header to safe area"
```
### Task 2: 用公共 PageHeader 替换 G03 自建页头
**Files:**
- Modify: `tests/g03-visual-states-contract.ps1`
- Modify: `tests/g03-create-flow-contract.ps1`
- Modify: `tests/g03-document-flow-contract.ps1`
- Modify: `pages/genealogy/g03-create-genealogy.vue`
**Interfaces:**
- Consumes: Task 1 的 `<PageHeader :title="String" custom-back @back="Function" />`
- Produces: G03 动态标题 `isAncestorStep ? "录入首代人物" : "创建家谱"`,以及现有 `goBack()` 对第一、第二步的业务分流。
- [ ] **Step 1: 将 G03 契约改成公共页头要求**
`tests/g03-visual-states-contract.ps1` 删除 `.flow-header__back``.flow-header__side``.flow-header__back-icon` 尺寸断言,改为:
```powershell
Assert-Match -Content $g03 -Pattern '(?s)<PageHeader\s+:title="isAncestorStep \? .录入首代人物. : .创建家谱."\s+custom-back\s+@back="goBack"\s*/>' -Message 'G03 must use PageHeader with its dynamic title and custom back handler'
if ($g03 -match 'flow-header|flow-header__back|flow-header__title') { throw 'G03 must not retain a private header implementation' }
```
`tests/g03-create-flow-contract.ps1` 的 required 项中加入:
```powershell
'import PageHeader from "@/components/PageHeader.vue";',
'<PageHeader',
'custom-back',
'@back="goBack"'
```
并从 required 项删除 `'root-header-cinnabar.jpg'`;在 forbidden 项加入:
```powershell
'class="flow-header"',
'flow-header__back',
'root-header-cinnabar.jpg'
```
`tests/g03-document-flow-contract.ps1` 删除页头背景的 required 项,并加入:
```powershell
foreach ($forbidden in @('flow-header', 'root-header-cinnabar.jpg')) {
if ($page -match [regex]::Escape($forbidden)) { throw "G03 must not retain private header token: $forbidden" }
}
```
- [ ] **Step 2: 运行 G03 聚焦契约并确认 RED**
Run:
```powershell
& powershell -ExecutionPolicy Bypass -File tests/g03-visual-states-contract.ps1
& powershell -ExecutionPolicy Bypass -File tests/g03-create-flow-contract.ps1
& powershell -ExecutionPolicy Bypass -File tests/g03-document-flow-contract.ps1
```
Expected: 契约因 G03 仍含私有 `flow-header` 且尚未导入 `PageHeader` 而 FAIL。
- [ ] **Step 3: 替换 G03 页头结构并删除私有样式**
`pages/genealogy/g03-create-genealogy.vue` 中把整个 `.flow-header` 模板替换为:
```vue
<PageHeader
:title="isAncestorStep ? '录入首代人物' : '创建家谱'"
custom-back
@back="goBack"
/>
```
`<script setup>` 中加入:
```js
import PageHeader from "@/components/PageHeader.vue";
```
完整删除 `.flow-header``.flow-header__content``.flow-header__back``.flow-header__side``.flow-header__back-icon``.flow-header__title` 六组样式。保留现有 `goBack()`:第二步 `redirectTo` 第一页,第一步 `navigateBack`
- [ ] **Step 4: 运行 G03 聚焦契约并确认 GREEN**
Run:
```powershell
& powershell -ExecutionPolicy Bypass -File tests/g03-visual-states-contract.ps1
& powershell -ExecutionPolicy Bypass -File tests/g03-create-flow-contract.ps1
& powershell -ExecutionPolicy Bypass -File tests/g03-document-flow-contract.ps1
git diff --check
```
Expected: 三项输出 `PASS``git diff --check` 退出码为 0。
- [ ] **Step 5: 提交 G03 接入改动**
```powershell
git add pages/genealogy/g03-create-genealogy.vue tests/g03-visual-states-contract.ps1 tests/g03-create-flow-contract.ps1 tests/g03-document-flow-contract.ps1
git commit -m "refactor: reuse shared header on G03"
```
### Task 3: 自动回归公共页头消费者与响应式边界
**Files:**
- Verify: `components/PageHeader.vue`
- Verify: `pages/genealogy/g03-create-genealogy.vue`
- Verify: `pages/genealogy/g06-search-genealogies.vue`
- Verify: `pages/genealogy/g01-my-genealogies.vue`
**Interfaces:**
- Consumes: Task 1 与 Task 2 的公共页头合同。
- Produces: 公共组件、G03、G06 与 G01 无静态合同回退的验证记录。
- [ ] **Step 1: 运行全部聚焦合同**
Run:
```powershell
$tests = @(
'tests/shared-component-document-flow-contract.ps1',
'tests/shared-interaction-accessibility-contract.ps1',
'tests/g03-visual-states-contract.ps1',
'tests/g03-create-flow-contract.ps1',
'tests/g03-document-flow-contract.ps1',
'tests/g06-search-flow-contract.ps1',
'tests/g06-document-flow-contract.ps1',
'tests/g01-visual-contract.ps1'
)
foreach ($test in $tests) { & powershell -ExecutionPolicy Bypass -File $test }
```
Expected: 八项均输出各自 `PASS`PowerShell 退出码为 0。
- [ ] **Step 2: 运行现有 H5 逻辑和宽度辅助检查**
在本地 H5 服务已运行时执行:
```powershell
node tests/g03-create-flow-runtime-smoke.js http://localhost:5173
node tests/g06-search-flow-runtime-smoke.js http://localhost:5173
```
Expected: 分别输出 `G03-CREATE-FLOW-RUNTIME-SMOKE PASS``G06-SEARCH-FLOW-RUNTIME-SMOKE PASS`320、360、412px 视口无横向溢出。若本地服务端口不同,只替换 URL,不改变测试内容。
- [ ] **Step 3: 检查最终差异**
Run:
```powershell
git diff --check
git status --short
```
Expected: `git diff --check` 退出码为 0;工作区只允许存在本计划明确列出的验证证据或尚未提交的计划文件。
### Task 4: MuMu 原生视觉验收门
**Files:**
- Create: `tmp/G03-shared-header-mumu.png`
- Create: `tmp/G06-shared-header-mumu.png`
- Create: `tmp/G01-root-header-regression-mumu.png`
**Interfaces:**
- Consumes: HBuilderX 已同步到 MuMu 的最新 Android 包与设备 `emulator-5554`
- Produces: G03、G06、G01 三张原生截图及用户逐页结论。
- [ ] **Step 1: 确认设备尺寸与密度**
Run:
```powershell
$adb = 'C:\Users\Administrator\Desktop\HBuilderX\plugins\launcher-tools\tools\adbs\adb.exe'
& $adb devices
& $adb -s emulator-5554 shell wm size
& $adb -s emulator-5554 shell wm density
```
Expected: `emulator-5554``device`,并记录当前 MuMu 的物理尺寸与密度。
- [ ] **Step 2: 捕获 G03、G06、G01 原生截图**
每次人工导航到指定页面后执行同一组命令,仅替换文件名:
```powershell
& $adb -s emulator-5554 shell screencap -p /sdcard/G03-shared-header-mumu.png
& $adb -s emulator-5554 pull /sdcard/G03-shared-header-mumu.png tmp/G03-shared-header-mumu.png
```
G06 使用 `G06-shared-header-mumu.png`G01 使用 `G01-root-header-regression-mumu.png`
- [ ] **Step 3: 按页面验收并向用户展示**
检查:
```text
G03:返回按钮和“创建家谱”完整位于系统状态栏下方,正文紧随占位槽,无裁切或横向溢出。
G06:公共二级页头获得相同安全区,正文没有被推入页头或遮挡。
G01:根页头高度、logo、标题和操作区与已放行状态一致。
```
只有用户明确确认三页后,才能把本次公共页头改动报告为视觉通过。G03 原生第二步仍因既有路由参数问题单独记为未通过,不得用本次页头结论覆盖。
@@ -0,0 +1,228 @@
# Responsive Foundation Phase 1 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 建立全项目响应式基础契约,并让 AppDialog、AppButton 与 ModulePage 使用统一九宫格素材档案和内容驱动高度。
**Architecture:** `styles/adaptive-frame-profiles.scss` 是九宫格切片的唯一所有者;组件只调用具名 mixin。`project-responsive-layout-contract.ps1` 根据覆盖清单扫描已迁移文件,根据唯一白名单放行结构性固定尺寸;第一阶段覆盖三个公共内容组件,后续阶段只增加覆盖范围。
**Tech Stack:** uni-app、Vue 3、SCSS、PowerShell、MuMu Android
## Global Constraints
- 内容容器不得使用固定高度表达内容容量。
- 可变内容背景不得使用 `100% 100%` 整图拉伸。
- 固定高度和固定装饰例外只能由 `tests/responsive-layout-allowlist.json` 所有。
- 第一阶段覆盖 `AppDialog.vue``AppButton.vue``ModulePage.vue`;已覆盖文件不得退出。
- AppDialog 短内容自然收紧、长内容自然增高、超过视口才滚动。
- 不修改业务逻辑、路由、文案和数据状态。
- 不执行 Git 操作,不启用多代理。
---
### Task 1: 唯一九宫格素材档案
**Files:**
- Create: `styles/adaptive-frame-profiles.scss`
- Create: `tests/adaptive-frame-profiles-contract.ps1`
**Interfaces:**
- Produces: `adaptive-auth-dialog``adaptive-scroll-button($type)``adaptive-module-content``adaptive-module-field` 四个 SCSS mixin。
- [ ] **Step 1: 写入失败合同**
合同要求档案存在并包含:
```powershell
foreach ($token in @(
'@mixin adaptive-auth-dialog',
'@mixin adaptive-scroll-button($type)',
'@mixin adaptive-module-content',
'@mixin adaptive-module-field',
'a01-scroll-dialog-v3.png',
'border-image-slice: 260 240 360 240 fill;',
'border-image-slice: 56 300 fill;',
'border-image-slice: 105 150;',
'border-image-slice: 70 100 fill;'
))
```
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/adaptive-frame-profiles-contract.ps1`
Expected: FAIL,素材档案不存在。
- [ ] **Step 2: 创建素材档案**
`adaptive-auth-dialog` 使用:
```scss
border-width: 48rpx 34rpx 72rpx;
border-image-slice: 260 240 360 240 fill;
border-image-width: 48rpx 34rpx 72rpx;
```
`adaptive-scroll-button` 使用现有主/次卷轴素材、`10rpx 26rpx` 边框和 `56 300 fill` 切片。
`adaptive-module-content` 使用动态 `var(--module-content-asset)``18rpx 20rpx` 边框和 `105 150` 切片。
`adaptive-module-field` 使用动态 `var(--module-field-asset)``12rpx 16rpx` 边框和 `70 100 fill` 切片。
所有 mixin 使用透明实体边框、`border-image-repeat: stretch``box-sizing: border-box`
- [ ] **Step 3: 验证档案合同**
Expected: `ADAPTIVE-FRAME-PROFILES-CONTRACT PASS`
### Task 2: 全局扫描合同、覆盖清单与例外白名单
**Files:**
- Create: `tests/project-responsive-layout-contract.ps1`
- Create: `tests/responsive-layout-coverage.json`
- Create: `tests/responsive-layout-allowlist.json`
**Interfaces:**
- Consumes: 覆盖文件列表与按文件/选择器/属性登记的例外。
- Produces: 已迁移文件中固定内容高度、可变背景整图拉伸和重复九宫格切片的失败门禁。
- [ ] **Step 1: 创建第一阶段覆盖清单**
```json
{
"covered": [
"components/AppButton.vue",
"components/AppDialog.vue",
"components/ModulePage.vue"
]
}
```
- [ ] **Step 2: 创建唯一白名单**
第一阶段只允许 `ModulePage.vue``.module-lead` 使用固定分隔线背景;不允许 AppDialog、AppButton 或 ModulePage 内容表面使用固定内容高度/整图拉伸。
- [ ] **Step 3: 创建扫描合同并确认失败**
合同解析覆盖文件的样式块,遇到以下情况失败:
```text
height: <数字>rpx(不含 min-height/max-height
background: ... 100% 100%
页面或组件直接出现 border-image-slice(素材档案本身除外)
```
白名单按规范化文件路径、选择器和属性精确匹配。覆盖清单中的路径不存在也失败。
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/project-responsive-layout-contract.ps1`
Expected: FAIL,指出 AppDialog 固定 `520rpx` 或 ModulePage 内容表面整图拉伸。
### Task 3: AppDialog 与 AppButton 消费统一档案
**Files:**
- Modify: `components/AppDialog.vue`
- Modify: `components/AppButton.vue`
- Modify: `tests/app-dialog-responsive-content-contract.ps1`
- Modify: `tests/app-button-compact-contract.ps1`
**Interfaces:**
- Consumes: Task 1 的 `adaptive-auth-dialog``adaptive-scroll-button`
- Produces: 内容驱动的全局弹窗和默认关闭的紧凑按钮。
- [ ] **Step 1: 更新失败合同**
AppDialog 合同禁止 `min-height: 520rpx`、旧 `background ... contain``margin-top: auto`,要求:
```text
@include adaptive-auth-dialog;
max-height: calc(var(--app-viewport-height, 100vh) - 160rpx)
margin-top: 26rpx
```
AppButton 合同要求调用 `adaptive-scroll-button(primary|secondary)`,不再在组件内重复切片值。
- [ ] **Step 2: 运行合同并确认失败**
Expected: AppDialog 因固定高度失败,AppButton 因本地切片重复失败。
- [ ] **Step 3: 修改 AppDialog**
在 scoped style 开头导入档案。`.app-dialog` 删除固定最小高度和旧背景,调用 `adaptive-auth-dialog``.app-dialog__content` 删除固定最小高度,使用:
```scss
max-height: calc(var(--app-viewport-height, 100vh) - 160rpx);
padding: 16rpx 20rpx 12rpx;
overflow-y: auto;
```
操作区改为 `margin-top: 26rpx`,不再使用 `auto`
- [ ] **Step 4: 修改 AppButton**
导入档案;主/次紧凑类分别调用 `@include adaptive-scroll-button(primary)``secondary`;删除组件内重复边框切片声明。
- [ ] **Step 5: 运行共享组件合同**
Run AppDialog、AppButton、共享无障碍、共享文档流和全局响应式合同,Expected: 全部 `PASS`
### Task 4: ModulePage 内容表面迁移
**Files:**
- Modify: `components/ModulePage.vue`
- Modify: `tests/module-page-visual-contract.ps1`
- Test: `tests/module-series-responsive-runtime-smoke.js`
**Interfaces:**
- Consumes: Task 1 的 module-content 与 module-field mixin。
- Produces: F/R/N/M 通用页面的内容驱动卡片和表单表面。
- [ ] **Step 1: 更新失败合同**
要求 ModulePage 导入档案,字段组调用 `adaptive-module-field`,内容组调用 `adaptive-module-content`;禁止内容表面 `100% 100%`
- [ ] **Step 2: 运行合同并确认失败**
Expected: FAIL,指出旧 `var(--module-*-asset) center / 100% 100%`
- [ ] **Step 3: 修改 ModulePage**
- `.form-row, .settings-row` 调用 `adaptive-module-field`
- `.list-card, .detail-card, .timeline-row, .status-card` 调用 `adaptive-module-content`
- `.module-lead` 自己承担 flex 居中,删除子文本的 `height: 100%`
- 保留现有 `min-height` 作为最低视觉基线,内容可继续增高。
- [ ] **Step 4: 验证 ModulePage**
运行视觉合同、响应式 runtime smoke 与全局响应式合同;若 H5 服务未运行,只记录 runtime 环境阻塞,不用浏览器代替 MuMu。
### Task 5: MuMu 三档基础验证
**Files:**
- Verify: `components/AppDialog.vue`
- Verify: `components/AppButton.vue`
- Verify: `components/ModulePage.vue`
- Save screenshots: `tmp/responsive-foundation-*.png`
**Interfaces:**
- Produces: G10 通过/拒绝弹窗和一个 ModulePage 页面在三档 Android 逻辑视口的原生证据。
- [ ] **Step 1: 运行全部聚焦合同**
必须包含素材档案、全局响应式、AppDialog、AppButton、ModulePage、共享无障碍和共享文档流合同。
- [ ] **Step 2: MuMu 验证 G10**
`360×640dp``360×800dp``412×915dp` 依次检查:
```text
通过确认:无固定大空白。
拒绝确认:输入框和按钮不压边。
拒绝错误:错误文字出现后仍可达按钮。
```
- [ ] **Step 3: MuMu 验证 ModulePage**
选择一个 ready 和一个 error 状态,确认内容卡随文字增高、边角不拉伸、按钮可达。
- [ ] **Step 4: 恢复 MuMu 原始尺寸**
每档验证后恢复用户当前 `720×1280` 物理尺寸和原密度,并把 G10 恢复到默认列表。
@@ -0,0 +1,37 @@
# G01 当前家谱角色与快捷入口联动设计
## 目标
修复 G01 切换到“我加入的家谱”后仍显示“管理员”和“申请审核”的状态错误,使当前家谱身份与可见入口始终由家谱自身的 `membership` 字段决定。
## 单一规则
`data/mock.js` 中每条家谱记录的 `membership` 是 G01 当前身份的唯一来源:
- `created`:当前用户是管理员,顶部显示“管理员”,快捷入口包含“世系图、成员、字辈诗、申请审核”。
- `joined`:当前用户是普通成员,顶部显示“成员”,隐藏“申请审核”,仅显示“世系图、成员、字辈诗”。
不增加兼容字段,不根据家谱所在列表、当前索引或名称推断身份。
## 页面行为
- 首次进入页面时,根据当前选中家谱立即显示正确身份和入口。
- 在切换弹层选择另一家谱后,身份文字和快捷入口同步更新。
- 三个或四个快捷入口继续使用现有等宽布局,不增加占位项。
- 切换家谱后维持现有的列表滚动归零行为。
- 普通成员状态不提供“申请审核”的隐藏点击区域或导航路径。
## 修改范围
- `pages/genealogy/g01-my-genealogies.vue`:从当前家谱计算身份文字和可见快捷入口。
- G01 相关契约或运行时测试:覆盖管理员与普通成员双向切换。
不修改其他页面、视觉资产、数据结构和权限接口。
## 验收标准
1. `created` 家谱显示“管理员”和四个快捷入口。
2. `joined` 家谱显示“成员”,且页面中不存在“申请审核”入口。
3. 从管理员切到成员,再切回管理员,两种状态都立即恢复正确。
4. 现有 G01 布局、弹层、空态、加载态、错误态和滚动行为测试继续通过。
5. MuMu 模拟器中管理员态与成员态均无错位、空白占位或底部遮挡。
@@ -0,0 +1,22 @@
# G09 申请卡片九宫格边框设计
## 问题
`list-slip-frame.png` 原始尺寸为 `720×144`,比例为 `5:1`。G09 卡片当前约为 `672×228rpx`,比例约 `2.95:1`,并使用 `background-size: 100% 100%`,导致整张边框素材被纵向拉伸。卡片上、下内距和卡片间距也偏宽。
## 方案
- `.application-card` 停止使用拉伸背景,改用 CSS `border-image` 九宫格切片。
- 使用原素材 `list-slip-frame.png`,切片固定为上/下 `24px`、左/右 `32px`;四角不变,只拉伸中间边线。
- 卡片边框宽度使用上/下 `18rpx`、左/右 `24rpx`
- 卡片最小高度从 `228rpx` 收紧到 `210rpx`,卡片间距从 `18rpx` 收紧到 `12rpx`
- 卡片正文最小高度使用 `174rpx`,网格间距使用 `8rpx 14rpx`,内距使用 `26rpx 28rpx 22rpx 40rpx`
- 保留 `white-space: nowrap`,不修改操作文案、状态、模拟数据和交互。
## 验证
- 合同禁止 `.application-card` 继续使用 `background: ... list-slip-frame.png ... 100% 100%`
- 合同要求 `border-image-source``border-image-slice: 24 32`、边框宽度和收紧后的布局值。
- G09 视觉、文档流和业务状态合同通过。
- MuMu 原生画面中四角比例正常,卡片内容间距更紧凑,“修改后重新提交”保持单行且不溢出。
- 不执行 Git 操作,不启用多代理。
@@ -0,0 +1,94 @@
# G10 全状态响应式视觉调整设计
## 问题与目标
MuMu 原生画面确认 G10“入谱审核”存在三类问题:
1. 列表卡片和状态卡使用 `list-slip-frame.png` 整图 `100% 100%` 拉伸,四角与上下边框纵向变形。
2. 列表双操作和确认弹窗双操作把比例约 `6.2:1``6.8:1` 的卷轴素材放入窄按钮后使用 `aspectFit`,可见皮肤缩成细横条。
3. `AppDialog` 的标题和说明是相邻文本节点,MuMu 中会连成同一行,信息层级不清楚。
目标是在不改变审核业务逻辑的前提下,让列表、弹窗、审核结果及空/错/无权限状态在 MuMu 中保持真实素材比例、清晰层级和完整触控视觉。
## 组件边界
### AppButton
- 新增布尔属性 `compact`,默认值为 `false`
- 只有显式设置 `compact` 时启用紧凑九宫格皮肤;其他已通过页面继续使用当前 `aspectFit` 皮肤,默认视觉不变。
- 紧凑按钮隐藏原 `<image>` 皮肤,在按钮根节点使用对应的真实卷轴素材作为 `border-image`
- 主按钮:`a01-scroll-primary-v3.png`
- 次按钮:`a01-scroll-secondary-v3.png`
- 紧凑切片固定为 `border-image-slice: 56 300 fill`,边框宽度固定为 `10rpx 26rpx`,重复方式为 `stretch`
- 紧凑按钮继续保留原有点击、禁用、按下态、无障碍标签和文字层。
### AppDialog
- 新增布尔属性 `compactActions`,默认值为 `false`
-`compactActions` 为真且 `showCancel` 为真时,取消和确认按钮继续左右并排,并向两个 `AppButton` 传递 `compact`
- 单按钮弹窗不启用紧凑模式,继续显示当前完整宽卷轴按钮。
- 标题与说明放入独立的 `.app-dialog__copy` 容器;标题和说明均为独立块级行、宽度 `100%`,说明保持 `22rpx` 上间距。
- 不修改弹窗遮罩、背景画、关闭规则、焦点恢复和事件名称。
## G10 页面设计
### 列表与状态卡
- `.application-card` 改用 `list-slip-frame.png` 九宫格边框:
- `border-image-slice: 24 32`
- `border-width: 18rpx 24rpx`
- `border-image-width: 18rpx 24rpx`
- `border-image-repeat: stretch`
- 列表卡片最小高度使用 `218rpx`,卡片间距使用 `12rpx`
- `.application-card__body` 最小高度使用 `182rpx`,间距使用 `8rpx 0`,内距使用 `28rpx 28rpx 22rpx 40rpx`
- 关系说明上间距从 `15rpx` 收紧为 `8rpx`
- `.review-state-card` 同样使用 `24 32` 九宫格切片,不再整图拉伸;状态文案、加载分支和重试行为不变。
### 待审核操作
- G10 列表改为复用 `AppButton compact`,拒绝和通过继续左右并排。
- 每个按钮保持 `148rpx × 68rpx` 的可见尺寸,间距保持 `12rpx`
- 拒绝使用 `secondary`,通过使用 `primary`;按钮文字和点击回调保持不变。
- 页面不再直接渲染 `a01-primary-button-v2.png``a01-secondary-button-v2.png``aspectFit` 图片。
### 审核结果
- `review-result` 保持 `308rpx × 68rpx`,使用与紧凑按钮相同的九宫格切片规则。
- 已通过使用 `a01-scroll-primary-v3.png`,已拒绝使用 `a01-scroll-secondary-v3.png`
- 结果状态是静态展示,不改成可点击按钮,不增加隐藏点击区域。
### 审核弹窗
- G10 的 `AppDialog` 显式传入 `compact-actions`
- 审核说明:标题“审核说明”和说明正文分行;单个“我知道了”按钮保持完整宽卷轴。
- 通过确认:标题与说明分行;取消/确认通过左右并排并使用紧凑九宫格皮肤。
- 拒绝确认:标题与说明分行;拒绝原因输入框保持现有结构;取消/确认拒绝左右并排并使用紧凑九宫格皮肤。
- 不改变拒绝原因必填校验、审核状态写入、反馈提示和弹窗关闭行为。
## 状态覆盖
MuMu 原生验收依次覆盖:
1. 待审核列表。
2. 审核说明。
3. 通过确认。
4. 通过完成与反馈提示。
5. 拒绝确认。
6. 空拒绝原因校验。
7. 拒绝完成与反馈提示。
8. 空数据。
9. 加载。
10. 错误与重新查看。
11. 无权限。
## 合同与验证
- 先补充失败合同,再修改组件和页面。
- `AppButton` 合同要求 `compact` 默认关闭,并固定紧凑九宫格素材与切片值。
- `AppDialog` 合同要求 `compactActions` 默认关闭、双按钮显式传递紧凑模式,并固定标题/说明独立布局。
- G10 视觉合同禁止列表卡和状态卡继续使用完整背景 `100% 100%` 拉伸。
- G10 视觉合同固定列表卡、状态卡、操作按钮和结果状态的九宫格规则与尺寸。
- 运行 AppButton、AppDialog、G08G10 流程、G09/G10 业务状态和 G10 全状态视觉合同。
- 每个可见状态只以 MuMu Android 原生截图验收,不使用浏览器截图代替。
- 不修改文案、模拟数据、状态枚举、路由、审核结果和交互流程。
- 不执行 Git 操作,不启用多代理。
@@ -0,0 +1,38 @@
# 全项目通用提示文案设计
## 目标
所有用户可见的输入提示、示例和辅助说明不得使用具体人物姓名或具体家族名称。页面模拟数据继续使用现有姓名,不纳入本次修改。
## 适用范围
纳入检查:
- `placeholder` 属性和表单配置中的 `placeholder` 字段。
- 带“例如”“比如”“如:”的用户示例文案。
- 紧邻输入框、用于说明填写格式的辅助文字。
排除检查:
- 成员目录、人物档案、申请记录、审核记录等模拟数据。
- 自动化测试填写的模拟输入值。
- 已录入数据在详情页或卡片中的展示。
## 本轮替换
- G08 关系输入:`例如:汤正华堂侄``例如:某某某堂侄`
- G08 关系辅助说明:`请以家谱中一位已知长辈为参照,例如:汤正华堂侄``请以家谱中一位已知长辈为参照,例如:某某某堂侄`
- G03 姓氏:`如:汤``请输入姓氏`
- G03 谱名:`如:汤氏家谱``请输入家谱名称`
- `TreeMemberForm` 首位成员姓名:`汤文远``请输入首位成员姓名`
- `TreeMemberForm` 当前成员:`汤文远``请选择当前成员`
其他不含具体姓名的格式示例保留,例如“长子、配偶”“1992年”“春节团圆”“家风家训”。
## 合同与验收
- 新增提示文案合同,只扫描 `.vue` 文件中含 `placeholder``例如``比如``如:` 的行,以及 G08 的关系辅助说明。
- 合同禁止上述提示上下文出现当前模拟人物姓名或具体家族名称,不扫描普通模拟数据行。
- 聚焦合同与受影响页面现有合同通过。
- MuMu 复核 G08 默认表单,关系输入和辅助说明必须显示“某某某堂侄”。
- 不执行 Git 操作,不启用多代理。
@@ -0,0 +1,45 @@
# 公共二级页头安全区与 G03 复用设计
## 目标
统一修正普通二级页面导航栏与 Android 状态栏距离过近的问题,并删除 G03 自建的 `flow-header`,让其复用公共 `PageHeader`
## 单一组件规则
`components/PageHeader.vue` 是项目根页面和二级页面导航栏的唯一实现:
- 根页面继续使用现有 `root` 变体。
- 普通二级页面总高度由“系统状态栏高度 + 104rpx 导航内容高度”组成。
- 普通二级页面顶部内边距使用 `var(--status-bar-height, 0px)`,标题和返回按钮在状态栏下方的 104rpx 内容区中垂直居中。
- 页面正文继续由 `page-header-slot` 占位,不允许被固定页头覆盖。
不在 G03 或其他业务页面重复设置状态栏补偿。
## G03 接入
- G03 删除 `flow-header`、返回图标和标题的自建模板与样式。
- G03 使用动态公共页头标题:第一步为“创建家谱”,第二步为“录入首代人物”。
- G03 仍保留第二步返回第一步的原有业务行为。
- 为支持这一行为,`PageHeader` 增加可选 `customBack` 布尔属性和 `back` 事件:
- `customBack``false` 时保持现有默认返回和深链回退逻辑。
- `customBack``true` 时只触发 `back` 事件,由页面处理返回。
- G03 使用 `<PageHeader :title="..." custom-back @back="goBack" />`
## 范围
修改:
- `components/PageHeader.vue`
- `pages/genealogy/g03-create-genealogy.vue`
- 公共页头与 G03 相关契约、运行时验证
不修改 G03 表单、弹层、按钮、背景和业务数据。已发现的 G03 原生第二步路由参数问题另行修复,不与本次页头调整混在同一个改动中。
## 验收标准
1. MuMu 中 G03 标题和返回按钮完整位于系统状态栏下方。
2. G03 第一、第二步均使用公共 `PageHeader`,不存在 `flow-header` 残留。
3. 已使用公共组件的 G06 等二级页面同步获得相同安全区,正文不被推入页头或遮挡。
4. 根页面 G01 的现有头部高度和布局不变。
5. 普通页面默认返回、深链回退及 G03 自定义返回均有测试覆盖。
6. 320px 至 412px CSS 视口无横向溢出,MuMu 原生画面无标题贴顶或内容裁切。
@@ -0,0 +1,135 @@
# 全项目手机自适应重构设计
## 决策
采用分阶段治理:先重构 A01 并在 MuMu 验收,再迁移 A04、A05 和封存 A06,最后按页面类型治理其余活动路由。不得继续在旧的整屏叠层结构上追加 `vh``rpx``clamp()` 或机型专用补丁。
## 根因
当前问题不是 uni-app 的适配能力不足,而是页面把固定比例设计稿当成运行时坐标系:
- A 系列使用 `824×1830` 整页合成背景并以 `scaleToFill` 拉伸到任意屏幕比例。
- 背景、印章和交互内容叠放在同一个 `grid-area: 1 / 1`,表单位置依赖背景中的视觉地标。
- 旧 A01 使用 `1665rpx` 组织纵向画布;`rpx` 随屏宽换算,不能表达手机可用高度。在 MuMu 的约 `360×640dp` 视口中,该画布约为 `799dp`,必然超高。
- 固定视口高度、整页叠层和独立的内容定位形成多套坐标;调整其中一套只能修复一个屏幕比例。
对照项目 `3dyjsapp` 使用 `min-height: 100vh`、普通 Flex 文档流和装饰背景。内容尺寸由自身决定,空间不足时自然增长,因此不会依赖背景坐标或互相覆盖。
## 架构原则
### 背景与布局解耦
- 背景只负责装饰,不得决定标题、输入框、按钮或列表的位置。
- 禁止普通页面使用整页合成图的 `scaleToFill` 作为布局基准。
- 屋檐、横幅等有明确比例的头图必须放入独立槽位,使用 `widthFix` 或显式 `aspect-ratio`,高度由宽度推导,不做纵向拉伸。
- 宣纸、云纹、树影等可重复或可裁切纹理作为内容区背景,允许 `cover` 或分层装饰,但不得承载交互坐标。
- 装饰层可以重叠;交互内容必须处于正常文档流,除弹窗、浮层和真实画布节点外不得整页绝对定位或整页网格叠放。
### 单一视口所有者
`styles/global.scss` 只拥有以下全局原语:动态视口高度、安全区、共享 Header/Tabbar 占位、最小触控尺寸和少量连续间距变量。页面消费这些原语,但不得按品牌或具体型号复制视口规则。
全局视口变量只解决浏览器或 WebView 的可用高度差异,不能用于重新创建一套设计稿坐标系。
### 页面类型
每个活动路由必须且只能归入一种布局类型:
1. `document-flow`:列表、详情、文章和长表单。根容器使用最小视口高度,内容自然撑高,页面是唯一纵向滚动所有者。
2. `compact-flow`:登录、短确认和固定步骤页。默认字号、无键盘时应在目标基线视口完整显示;内容仍使用正常文档流,极端小屏、大字号、错误文案或键盘状态允许自然滚动。
3. `split-viewport`:固定 Header/筛选区加独立内容滚动区。页面自身不与内容区形成双重纵向滚动。
4. `interactive-canvas`:家谱关系图等真实画布。画布占据计算后的可用视口,内部负责缩放和平移;页面表单、说明和操作栏仍遵守正常流与安全区。
分类只定义高度和滚动所有权,不承载业务字段、状态或视觉内容。
## A 系列结构
新增共享认证页骨架,供 A01、A04、A05 和封存 A06 使用。骨架只拥有:
- 顶部安全区;
- 固定比例的品牌头图槽;
- 独立的宣纸内容区背景;
- 正常文档流内容槽;
- 底部安全区和溢出回退。
各认证页面继续拥有自己的标题、字段、状态、操作和业务逻辑。共享骨架不得成为业务配置中心,也不得用页面编号切换业务 DOM。
A01 默认态的纵向压缩顺序为:装饰留白、装饰尺寸、非关键间距、辅助说明;标题、正文、输入框、按钮和协议触控区域不得低于可读和可触控下限。禁止整页 `transform: scale()`
## 安全区、键盘与字体
- 同一安全区只能由一个层级计入;共享 Header、Tabbar 或认证页骨架拥有后,业务页面不得重复叠加。
- 默认系统字号执行各页面类型的首屏或滚动合同。
- 约 1.3 倍系统字号下允许 `compact-flow` 回退为自然滚动,但不得裁字、遮挡或丢失操作。
- 键盘弹出时必须能滚动到焦点字段、就近错误信息和当前主操作;键盘收起后页面恢复合理位置。
- 弹窗、底部弹层和全屏预览各自拥有最大高度、安全区和内部滚动,不计入页面默认内容高度。
## 分阶段迁移
### 阶段一:A01 结构证明
- 删除 A01 的整页合成背景布局职责和全屏叠层坐标。
- 将品牌头图、宣纸内容区和登录内容拆为独立结构。
- 保持登录方式、校验、协议和跳转逻辑不变。
- 只有 A01 在 MuMu 默认密码态和验证码态均通过后,才能提取共享认证页骨架。
### 阶段二:认证页迁移
- 从已验证的 A01 结构中提取最小共享骨架。
- 迁移 A04、A05 和封存 A06,不恢复 A06 路由。
- 验证长表单、错误、提交中、成功、键盘和返回保护状态。
### 阶段三:项目页面分类治理
- 建立活动路由到四类页面的唯一清单。
- 优先处理现有整屏 `grid-area: 1 / 1``scaleToFill`、固定大块高度和 `overflow: hidden` 风险页。
- 再迁移普通列表、详情、文章、表单和共享 Header/Tabbar/Dialog。
- 真正的家谱画布保留独立视口策略,不套用普通文档页规则。
每一批迁移必须独立验收,不允许一次性机械替换全部页面后再集中修复。
## 静态合同
新增或修订项目级合同,至少检查:
- `pages.json` 的全部活动路由均有且只有一个布局分类,封存页面单独声明。
- 普通交互页禁止整页 `scaleToFill` 合成背景、整页交互叠层和固定设计稿画布高度。
- `document-flow` 只有一个纵向滚动所有者,末项可达。
- `compact-flow` 保留自然溢出回退,禁止通过裁切或整页缩放满足首屏。
- `split-viewport` 明确固定区和唯一内容滚动区。
- 固定高度白名单必须精确到文件、选择器、风险类型和理由,失效条目使合同失败。
## 运行时与设备验收
自动化视口检查覆盖至少:`320×568``360×640``375×812``390×844``412×915``480×1040` CSS px。
共同要求:
- 无水平溢出、关键控件越界、正文裁切和双重纵向滚动。
- 固定 Header/Tabbar 与安全区不遮挡首项、末项或主操作。
- 头图保持比例,背景裁切不改变内容坐标。
- 大字号、错误文案和键盘状态下所有字段及操作可达。
Android 视觉结论必须来自模拟器或真机,H5 只能作为自动化辅助,不能替代 Android 验收。第一基线为当前 MuMu `720×1280 / 320dpi`(约 `360×640dp`)。
A01 阶段一完成条件:
- 默认密码态和验证码态无需纵向滑动即可看到完整协议区。
- 页面没有屋檐与标题重叠,宣纸分界与内容之间保留稳定留白。
- 头图无纵向拉伸,输入、按钮和协议区域满足最小触控尺寸。
- 未勾选协议、输入错误、键盘弹出和键盘收起状态均无裁切,操作可达。
- 用户查看 MuMu 原生画面并确认视觉方向后,才迁移其他认证页。
## 非目标
- 不改变业务接口、路由、字段、状态机和文案语义。
- 不借自适应重构重新设计整个视觉主题。
- 不为单个品牌或型号建立专用断点。
- 不承诺在任意内容量、任意字号和键盘同时打开时完全无滚动;承诺默认基线首屏完整,异常条件下无裁切且可自然到达。
## 完成条件
- A01、认证页和其余活动路由按阶段完成真实设备验收。
- 所有活动路由拥有唯一布局分类,并通过对应静态和运行时合同。
- 旧固定设计稿画布、整页合成背景坐标、重复视口规则和失效白名单从运行时、测试、设计记录与交接记录中同步移除。
- Android 结论只覆盖实际验证的设备、分辨率和状态,不把单一模拟器结果扩写为所有真机已通过。
@@ -0,0 +1,180 @@
# 全项目响应式布局与装饰素材自适应设计
## 背景
当前项目共有 65 个 Vue 文件。静态扫描发现:
- 27 个文件包含固定 `height`
- 29 个文件包含 `background-size: 100% 100%` 或等价整图拉伸。
这些规则混合了两类完全不同的用途:导航栏、图标和最低触控目标需要稳定尺寸;弹窗、表单、卡片、列表、状态说明等内容容器必须由内容和屏幕决定尺寸。当前缺少统一契约,导致同一装饰素材在不同页面被重复拉伸,短内容产生大块空白,长内容被压缩或溢出。
本设计建立全项目唯一响应式规则,不再针对单页写专用高度。
## 总体原则
### 内容容量
- 内容容器不得使用固定 `height` 表达内容容量。
- 卡片、弹窗、表单面板、状态面板、列表项和正文区域由内容自然撑开。
- 允许使用 `min-height` 保证最小视觉基线或触控目标,但内容增加时必须继续增高。
- 只有受到视口限制的浮层允许 `max-height`;超过上限时在浮层内部滚动,不压缩正文和操作区。
- 页面主区域使用正常文档流;除 Toast、遮罩、必要悬浮反馈外,不用绝对定位补偿尺寸。
### 宽度与重排
- 页面内容区使用 `width: 100%``max-width: 100%``box-sizing: border-box`
- flex/grid 的可变文本列必须使用 `min-width: 0`
- 数据文本默认允许换行,并使用 `overflow-wrap: anywhere` 保护窄屏。
- 只有短、固定语义的操作按钮允许 `white-space: nowrap`;按钮皮肤和文字容量必须同时验证。
- 多列布局在紧凑宽度无法保证文字容量时改为单列或分行,不使用缩小字体规避。
### 高度与安全区
- `PageHeader``AppTabbar`、状态栏占位、图标、头像和媒体缩略图可以保留固定尺寸或 `aspect-ratio`
- 按钮可以保留最低触控高度,不得用固定高度截断多行标签。
- 页面最小高度使用 `min-height: 100vh`;底部操作同时考虑安全区。
- 键盘弹出、状态栏高度变化和短屏不能让主要操作移出可达区域。
## 装饰素材规则
### 可变内容表面
弹窗、内容卡、表单框、状态框、通知卡、人员卡等承载可变内容的装饰素材必须使用九宫格:
- 使用真实 PNG 作为 `border-image-source`
- 切片值和渲染边框宽度由唯一的素材档案维护。
- 四角、卷轴端头和边缘纹样不拉伸,只拉伸中间区域。
- 不允许在这些容器上使用 `background-size: 100% 100%`
### 固定比例素材
Logo、图标、头像、印章、插画、完整按钮图和媒体缩略图可以使用 `aspectFit``widthFix``contain` 或明确的 `aspect-ratio`
完整按钮图只有在按钮槽位比例与素材比例兼容时使用 `aspectFit`;窄按钮使用紧凑九宫格皮肤,不得缩成细条。
### 允许例外
分隔线、纯纹理层、固定比例的非内容装饰和明确允许裁切的背景可以继续使用拉伸或裁切,但必须登记到统一白名单并说明理由。页面不得自行新增例外。
## 单一所有者
### 素材档案
新增 `styles/adaptive-frame-profiles.scss` 作为九宫格素材的唯一所有者。每个档案包含:
- 档案名称。
- PNG 路径。
- `border-image-slice`
- `border-width` / `border-image-width`
- 是否使用 `fill`
首批档案至少覆盖:
- `auth-dialog`
- `genealogy-slip`
- `module-content`
- `module-field`
- `tree-panel`
- `tree-field`
- `notification-card`
- `profile-summary`
- `records-content`
- `records-field`
页面和组件只消费档案,不重复定义切片值。
### 响应式例外白名单
新增 `tests/responsive-layout-allowlist.json`,作为固定高度和整图拉伸例外的唯一所有者。每条记录必须包含:
- 文件路径。
- 选择器。
- 允许的属性。
- 固定尺寸或拉伸成立的具体原因。
旧例外路径不保留在其他测试或文档中;新增例外必须修改该文件和全局合同。
### 全局合同
新增 `tests/project-responsive-layout-contract.ps1` 扫描 `components/**/*.vue``pages/**/*.vue`
- 内容容器出现固定 `height` 且不在白名单时失败。
- 可变内容表面出现 `100% 100%` 整图拉伸且不在白名单时失败。
- 九宫格档案在页面重复定义切片值时失败。
- 需要换行的数据区域出现无理由 `nowrap` 时失败。
- 共享组件的默认响应式契约缺失时失败。
新增 `tests/responsive-layout-coverage.json` 作为分阶段覆盖清单。第一阶段先纳入公共组件;后续每完成一个模块,就把该模块全部 Vue 文件加入清单。已经纳入的文件不得移出,最终阶段要求清单与 `components/**/*.vue``pages/**/*.vue` 的实际文件集合完全一致,并删除所有仅用于迁移过程的覆盖缺口。该清单不是例外白名单,不能登记允许违规的理由。
## 共享组件设计
### AppDialog
- 删除 `.app-dialog``.app-dialog__content` 的固定 `520rpx` 最小高度。
- `a01-scroll-dialog-v3.png` 改为 `auth-dialog` 九宫格档案。
- 弹窗使用 `width: 650rpx; max-width: 100%`,外层保留视口安全间距,避免依赖额外的 CSS 函数兼容性。
- 内容高度由标题、正文、插槽和操作区自然撑开。
- 操作区使用正常间距,不再使用 `margin-top: auto` 制造固定空白。
- 弹窗使用视口最大高度;内容超出时内部滚动。
- 通过确认会自动收紧;拒绝原因出现后弹窗自动增高;极短屏才滚动。
### AppButton
- 保留默认完整宽按钮和显式 `compact` 九宫格变体。
- 使用 `min-height` 保证触控目标,不用固定高度截断标签。
- 紧凑按钮根据容器宽度保持四字操作容量。
### ModulePage
- `module-content``module-field` 表面改为统一九宫格档案。
- 模块页正文、状态卡和表单由内容撑开。
- 分隔线等固定非内容装饰可登记白名单。
### PageHeader 与 AppTabbar
- 作为结构性导航保留固定内容高度和安全区计算。
- 标题区保留 `min-width: 0`,长标题按既定规则缩放或换行,不影响左右操作区。
- 这些固定高度必须进入白名单并说明为导航结构,不允许页面复制其值。
## 模块迁移顺序
全项目迁移拆分为可独立验收的阶段,不在一次修改中混合 65 个页面:
1. **基础层**:素材档案、全局合同、白名单、AppDialog、AppButton、ModulePage。
2. **A/G 模块**:登录注册、家谱列表、创建、搜索、申请、审核、设置、字辈诗。
3. **T 模块**:树图、成员档案、添加/编辑关系、成员目录与状态页。
4. **F 模块**:动态、文章、相册和媒体上传。
5. **R/N/M 模块**:人物档案、消息通知、个人中心和设置页。
每个阶段必须满足:合同先失败、实现后通过、MuMu 关键页面通过,才能进入下一阶段。
## MuMu 响应式验证矩阵
视觉验收只使用 MuMu Android 原生画面。每个阶段至少验证三种 Android 逻辑视口:
- 紧凑屏:`360 × 640dp`
- 常规长屏:`360 × 800dp`
- 宽长屏:`412 × 915dp`
使用 MuMu 的 Android 分辨率/密度配置或 `adb shell wm size``wm density` 临时切换;每轮结束恢复用户当前配置。不得用浏览器截图代替。
每个页面至少覆盖默认、加载、空、错误和主要弹窗/表单校验状态;没有某状态的页面记录为不适用。
## 验收标准
- 短内容容器不出现由固定高度造成的大块空白。
- 长内容容器不拥挤、不压边、不遮挡操作区。
- 装饰四角、卷轴端头、边框纹样不变形。
- 文本放大、长姓名、长关系说明和错误文案不会被截断。
- 主要操作始终可见或可通过页面/浮层内部滚动到达。
- 三种 MuMu 视口均无水平滚动、非预期裁切或点击区域错位。
- 已通过页面的业务逻辑、路由、数据状态和文案不因布局迁移改变。
## 约束
- 不修改业务接口、状态枚举、模拟数据和页面路由。
- 不以缩小字体作为适配方案。
- 不新增页面专用固定高度补丁。
- 不保留旧拉伸规则作为回退路径。
- 不执行 Git 操作,不启用多代理。
+11 -1
View File
@@ -1,10 +1,20 @@
# 家谱 APP 换机交接记录
> 最后更新:2026-07-20
> 最后更新:2026-07-21
> 用途:更换电脑、重新打开 Codex/GPT 后的唯一接管入口。
> 当前阶段:只做页面样式与视觉流程验收;不对接接口,不做功能验收。
> 当前施工停点:全项目文档流迁移的源码与定位合同已完成收敛,正在做综合回归和用户复核交接。合同已覆盖包括 `relative` 在内的全部 `position` 声明,当前为零未白名单违规,且会拒绝失效白名单。旧“剩余 112 条”永久作废。现有 `[x]` 仅沿用迁移前用户明确通过的范围;本轮没有自动新增 `[x]`,也没有把自动截图对比冒充用户再次确认。下一步是向用户交付迁移后的同尺寸 H5 复核结论,再恢复其他页面审核。Android、接口和业务功能不在本次结论内。最新事实与换机提示见 `docs/夜间批量收敛交接_2026-07-20.md`。
## 0A. 2026-07-21 A01 自适应结构证明
- A01 已从整页合成背景坐标系统迁移为纵向 Flex:`a01-vnext-header-v1.png` 保持原始 `824:340` 比例并在独立品牌槽内裁切底部门板,`auth-page-paper.jpg` 只作为宣纸装饰背景,标题、表单、按钮和协议全部处于正常文档流。
- 当前 MuMu 为 `720×1280 / 320dpi`。密码默认态、验证码默认态和协议错误态均能显示完整协议区;默认态向上滑动后内容位置不变。用户已明确通过密码默认态和验证码默认态,包括统一缩小后的字号体系,以及保持原大小、下移到屋脊中央的 Logo 位置。
- 用户已明确通过“请输入正确的手机号”卷轴 Toast;其定位改由 uni-app 的 `--status-bar-height` 负责,Toast 从系统状态栏下方开始布局,不再依赖 MuMu 沉浸式状态下为 `0``env(safe-area-inset-top)`
- A01 验证后已提取 `components/AuthPageShell.vue`,单一维护认证页可用视口、品牌头图、Logo、宣纸纹理、水墨枝叶/山景和安全区;A01、A04、A05 与封存 A06 均已消费该骨架。旧 `a01-red-hall-ink-backdrop-v1.png` 只在共享骨架中作为裁切后的宣纸装饰层,不再承担表单坐标。
- A04 已在 MuMu 明确通过自适应默认态与标题修正版;标题、副标题和分隔线相对整个内容宽度居中,返回按钮不再参与中心计算。A05 已在 MuMu 明确通过默认态、完整水墨背景和空提交四字段错误态;用户随后明确表示“都通过了,下一个页面”。A04/A05 的软键盘、A05 Toast 和成功弹层未单独展示,不得凭自动合同扩写为 Android 全状态完成。
- 聚焦证据:`A01-LOGIN-MERGE-CONTRACT PASS``A01-ASSET-ALPHA-AUDIT PASS`、六档 `A01-RESPONSIVE-RUNTIME-SMOKE PASS`。临时 Android 截图位于 `%TEMP%\jiapuapp-audit\2026-07-21\`,不提交仓库。
- 不得把本次确认扩写为认证流程或 Android 全面完成:MuMu 的 ADB 截图没有合成已显示的搜狗输入法窗口,软键盘遮挡仍待人工复核;验证码发送、微信授权、行为验证、真实接口、系统字体放大和其他真机仍未验证。A01、A04、A05 保持 `[~]`;A06 继续封存且不计入活动页。下一活动页面进入 G01 的项目级自适应复核。
## 0. 2026-07-20 最新接管覆盖说明
本节覆盖下方仍保留的 2026-07-19 历史停点;不要再从 G01 正常态重新开始。
+10 -4
View File
@@ -1,6 +1,6 @@
# 家谱 APP 页面样式验收规划
> 更新日期:2026-07-20
> 更新日期:2026-07-21
> 适用范围:Android `uni-app` 用户端
> 当前实现:`pages.json` 已收敛为 52 条活动路由;A02 已并入 A01,A06 已从活动路由与验收队列封存。历史上的 7 个验收结果仅作为旧视觉证据,不计入本轮完成状态。
> 本文件只管理逐页视觉、页面状态外观、弹窗与加载样式、手机适配和表现质量;不替代 `docs/规划.md` 对项目范围和最终路由的管理,也不承担接口与业务功能验收。功能验收规则后续单独编写。
@@ -15,6 +15,12 @@
| `[!]` | 用户要求返工;只修改该项涉及的页面、组件、资产、测试和证据 |
| `⚠` | 当前没有真实可达入口或存在明显流程断点,不能用直接路由冒充流程通过 |
### 2026-07-21 A01 Android 自适应复核
- A01 已删除 `824×1830` 整页合成背景、`1665rpx` 固定画布和整页交互叠层,改为固定比例头图、独立宣纸背景与正常 Flex 文档流;MuMu `720×1280 / 320dpi` 默认密码态无需滑动即可看到完整协议区。
- 用户已明确确认最终默认密码态通过:当前较紧凑字号体系、头图可见高度以及保持原大小并下移到屋脊中央的 Logo 位置冻结。密码态上滑前后内容不移动;静态合同、资产审计和六档 H5 结构 smoke 通过。
- 本条只通过 A01 默认密码态的 Android 视觉与自适应结构,不扩大为验证码交互、真实软键盘遮挡、系统字体放大、微信授权、行为验证或接口功能通过。MuMu 报告输入法已显示,但 ADB 截图未合成输入法窗口,因此键盘仍保持待验;A01 整页继续标记 `[~]`
### 2026-07-20 夜间批量收敛说明
- 全项目文档流迁移已按新口径完成源码收敛:合同扫描包括 `relative` 在内的全部 `position` 声明;从 35 个文件、390 条未白名单声明(其中 278 条 `relative`)收敛到零未白名单违规,并新增失效白名单检查。旧“剩余 112 条”不再使用。
@@ -202,9 +208,9 @@ G01、G03、G05、G06、G08、G09、G10、G11、G12 共 9 个活动 G 页面统
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
| --- | ---: | --- | --- |
| `[~]` | 1 | A01 登录 | **2026-07-16 用户已确认通过除真实滑动验证外的 7 类 H5 视觉状态**:密码默认、密码掩码、密码明文、短信默认、字段错误 Toast、协议错误、协议已勾选;品牌区、双按钮和 Logo 缩小版同时通过本轮审核。真实滑动验证采用接口组件自带样式,待接入后单独审核;Android/HBuilderX 复核前页面不冻结 |
| `[~]` | 2 | A04 注册账号 | **2026-07-16 用户已确认通过当前可实现的 8 类 H5 视觉状态**:默认空表单、空提交全量错误、手机号单项错误、两次密码不一致、协议未勾选、填写完成待提交、滑动验证待接入 Toast、协议入口 Toast;Logo、全局楷体、标题分隔、朱砂主按钮和浅色卷轴 Toast 同时通过本轮审核。真实滑动验证采用接口组件自带样式,注册成功/失败和进入 G01 的真实链路待接口阶段审核;Android/HBuilderX 复核前页面不冻结 |
| `[~]` | 3 | A05 重置密码 | A01→忘记密码;**2026-07-16 用户已确认通过当前 7 类 H5 视觉状态**:默认空表单、空提交全量错误、手机号单项错误、滑动验证待接入 Toast、两次新密码不一致、密码重设成功卷轴、返回 A01 密码登录画面;Logo、全局楷体、标题分隔、两处朱砂按钮和 Toast 同时通过本轮审核。用户决定“成功后回填手机号并聚焦密码框”不阻塞当前样式验收,随真实重置接口链路接入时解决;真实滑动验证、真实短信/重设结果及 Android/HBuilderX 仍待复核,页面保持 `[~]`、不冻结 |
| `[~]` | 1 | A01 登录 | **2026-07-21 用户已在 MuMu `720×1280 / 320dpi` 明确通过自适应后的密码默认态、验证码默认态和手机号错误卷轴 Toast**;Logo 保持确认尺寸并下移到屋脊中央,Toast 使用 uni-app 状态栏高度避开沉浸式系统栏。软键盘遮挡、真实滑动验证、验证码发送、微信授权、接口、系统字体放大和其他真机仍待验证,页面不冻结 |
| `[~]` | 2 | A04 注册账号 | **2026-07-21 用户已在 MuMu `720×1280 / 320dpi` 明确通过共享自适应骨架后的默认态和标题居中修正版**;完整品牌头图、Logo、宣纸水墨背景、三字段表单、按钮、协议和返回登录均可见。标题中心六档运行误差不超过 `1px`。软键盘、真实滑动验证、注册结果、系统字体放大和其他真机仍待验证,页面不冻结 |
| `[~]` | 3 | A05 重置密码 | A01→忘记密码;**2026-07-21 用户已在 MuMu 明确通过共享自适应骨架后的默认态、完整水墨背景和空提交四字段错误态,并表示“都通过了,下一个页面”**。A05 Toast、密码不一致、成功弹层和软键盘未逐张展示;自动合同与六档 runtime 已覆盖其结构和行为,但不替代 Android 视觉结论。真实短信/重设结果、系统字体放大和其他真机仍待验证,页面保持 `[~]`、不冻结 |
### 5.2 登录后进入我的家谱并完成首次分流
| 状态 | 序号 | 页面 | 真实入口与必须覆盖的可能性 |
+80 -127
View File
@@ -1,18 +1,6 @@
<!-- 页面编号A-01用途APP 唯一登录入口承载密码验证码与微信登录入口 -->
<template>
<view class="auth-page login-page">
<view class="page-canvas">
<image
class="page-backdrop"
src="/static/assets/modules/auth/opaque/a01-red-hall-ink-backdrop-v1.png"
mode="scaleToFill"
/>
<image
class="brand-seal"
src="/static/assets/foundation/transparent/brand-seal.png"
mode="aspectFit"
/>
<AuthPageShell class="auth-page login-page">
<view class="login-content">
<view class="login-heading">
<text class="login-title">登录家谱</text>
@@ -215,53 +203,55 @@
>
</view>
</view>
</view>
<!-- 当前只验收自定义容器外观真实行为验证数据留到接口阶段接入 -->
<view
v-if="verificationVisible"
class="verification-layer"
@click="closeVerification"
>
<view class="verification-dialog" @click.stop>
<image
class="verification-dialog__skin"
src="/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png"
mode="aspectFit"
/>
<view class="verification-dialog__content">
<text class="verification-title">安全验证</text>
<text class="verification-copy"
>{{ verificationPurpose }}前需要完成行为验证</text
>
<text class="verification-note">验证数据将在接口接入后启用</text>
<view class="verification-actions">
<view
class="verification-action"
hover-class="tap-fade"
@click="closeVerification"
>取消</view
>
<view
class="verification-action verification-action--primary"
hover-class="tap-fade"
@click="confirmVerification"
>知道了</view
<template #overlay>
<!-- 当前只验收自定义容器外观真实行为验证数据留到接口阶段接入 -->
<view
v-if="verificationVisible"
class="verification-layer"
@click="closeVerification"
>
<view class="verification-dialog" @click.stop>
<image
class="verification-dialog__skin"
src="/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png"
mode="aspectFit"
/>
<view class="verification-dialog__content">
<text class="verification-title">安全验证</text>
<text class="verification-copy"
>{{ verificationPurpose }}前需要完成行为验证</text
>
<text class="verification-note">验证数据将在接口接入后启用</text>
<view class="verification-actions">
<view
class="verification-action"
hover-class="tap-fade"
@click="closeVerification"
>取消</view
>
<view
class="verification-action verification-action--primary"
hover-class="tap-fade"
@click="confirmVerification"
>知道了</view
>
</view>
</view>
</view>
</view>
</view>
<view v-if="feedbackVisible" class="feedback-toast">
<text class="feedback-toast__copy">{{ feedbackMessage }}</text>
</view>
</view>
<view v-if="feedbackVisible" class="feedback-toast">
<text class="feedback-toast__copy">{{ feedbackMessage }}</text>
</view>
</template>
</AuthPageShell>
</template>
<script setup>
import { ref } from "vue";
import { onLoad, onUnload } from "@dcloudio/uni-app";
import AuthPageShell from "@/components/AuthPageShell.vue";
const LOGIN_METHOD_STORAGE_KEY = "a01:last-login-method";
const activeLoginMethod = ref("password");
@@ -373,50 +363,19 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
</script>
<style scoped lang="scss">
.auth-page {
box-sizing: border-box;
width: 100%;
height: 100dvh;
min-height: 100%;
overflow-y: auto;
background: #f7f0e5;
color: #493323;
}
.page-canvas {
display: grid;
box-sizing: border-box;
width: 100%;
min-height: calc(1665rpx + env(safe-area-inset-bottom));
padding-bottom: env(safe-area-inset-bottom);
}
.page-backdrop {
z-index: 0;
grid-area: 1 / 1;
align-self: start;
width: 100%;
height: 1665rpx;
pointer-events: none;
}
.brand-seal {
z-index: 1;
grid-area: 1 / 1;
align-self: start;
justify-self: center;
width: 184rpx;
height: 221rpx;
margin-top: 64rpx;
pointer-events: none;
}
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.login-content {
z-index: 2;
grid-area: 1 / 1;
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
box-sizing: border-box;
width: 100%;
padding: 480rpx 64rpx 34rpx;
max-width: 480px;
min-width: 0;
margin: 0 auto;
padding: clamp(10px, 2.2vh, 20px) clamp(24px, 8.5vw, 44px);
}
.login-heading {
@@ -424,29 +383,28 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 142rpx;
min-height: 50px;
}
.login-title {
color: #9f170f;
font-size: 82rpx;
font-size: 58rpx;
font-weight: 700;
letter-spacing: 8rpx;
letter-spacing: 5rpx;
line-height: 1;
}
.title-divider {
flex: none;
width: 360rpx;
height: 60rpx;
width: 280rpx;
height: 46rpx;
filter: brightness(0.68) saturate(1.5) contrast(1.2);
}
.login-tabs {
display: flex;
align-items: flex-end;
height: 114rpx;
margin-top: 10rpx;
min-height: var(--app-touch-min);
}
.login-tab {
@@ -456,7 +414,7 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
justify-content: center;
min-height: 88rpx;
color: #80684f;
font-size: 40rpx;
font-size: 34rpx;
letter-spacing: 2rpx;
}
@@ -486,7 +444,7 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
display: flex;
align-items: center;
box-sizing: border-box;
min-height: 112rpx;
min-height: var(--app-touch-min);
padding-left: 12rpx;
border-bottom: 1rpx solid rgba(182, 116, 43, 0.68);
}
@@ -506,9 +464,9 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
.auth-input {
flex: 1;
min-width: 0;
min-height: 104rpx;
min-height: var(--app-touch-min);
color: #493323;
font-size: 40rpx;
font-size: 30rpx;
}
.input-placeholder {
@@ -521,7 +479,7 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
align-items: center;
justify-content: center;
width: 88rpx;
height: 88rpx;
height: var(--app-touch-min);
}
.password-toggle__icon {
@@ -535,24 +493,24 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
align-items: center;
justify-content: flex-end;
min-width: 142rpx;
min-height: 88rpx;
min-height: var(--app-touch-min);
color: #a9160d;
font-size: 32rpx;
font-size: 28rpx;
}
.form-secondary-row {
display: flex;
align-items: center;
justify-content: flex-end;
min-height: 110rpx;
min-height: var(--app-touch-min);
}
.forgot-password {
display: flex;
align-items: center;
min-height: 88rpx;
min-height: var(--app-touch-min);
color: #a9160d;
font-size: 34rpx;
font-size: 28rpx;
}
.login-submit,
@@ -562,7 +520,8 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
}
.login-submit {
min-height: 92rpx;
min-height: var(--app-touch-min);
height: var(--app-touch-min);
}
.button-skin {
@@ -576,16 +535,16 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
z-index: 1;
grid-area: 1 / 1;
color: #fffaf1;
font-size: 52rpx;
letter-spacing: 8rpx;
font-size: 36rpx;
letter-spacing: 5rpx;
}
.other-login-divider {
display: flex;
align-items: center;
min-height: 100rpx;
min-height: 30px;
color: #9f6a27;
font-size: 32rpx;
font-size: 28rpx;
white-space: nowrap;
}
@@ -608,10 +567,11 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
}
.wechat-login {
min-height: 100rpx;
min-height: var(--app-touch-min);
height: var(--app-touch-min);
color: #493323;
font-size: 40rpx;
letter-spacing: 3rpx;
font-size: 30rpx;
letter-spacing: 2rpx;
}
.wechat-login__content {
@@ -633,21 +593,20 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
display: flex;
align-items: center;
justify-content: center;
min-height: 100rpx;
min-height: var(--app-touch-min);
color: #493323;
font-size: 31rpx;
font-size: 28rpx;
}
.register-link {
display: flex;
align-items: center;
min-height: 88rpx;
min-height: var(--app-touch-min);
margin-left: 8rpx;
color: #a9160d;
}
.agreement-area {
min-height: 106rpx;
color: #58483c;
}
@@ -655,7 +614,7 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
display: flex;
align-items: center;
justify-content: center;
min-height: 74rpx;
min-height: var(--app-touch-min);
font-size: 24rpx;
line-height: 32rpx;
}
@@ -713,20 +672,14 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
.feedback-toast {
position: fixed;
z-index: 30;
top: calc(env(safe-area-inset-top) + 24rpx);
top: calc(var(--status-bar-height, 0px) + 24rpx);
left: 50%;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
@include adaptive.adaptive-feedback-toast;
width: 590rpx;
min-height: 82rpx;
border: 16rpx solid transparent;
border-width: 16rpx 74rpx;
border-image-source: url("/static/assets/foundation/transparent/a01-scroll-toast-v3.png");
border-image-slice: 58 280 fill;
border-image-width: 16rpx 74rpx;
border-image-repeat: stretch;
background: transparent;
transform: translateX(-50%);
}
+42 -81
View File
@@ -1,18 +1,6 @@
<!-- 页面编号A-04用途注册账号与本页服务协议确认 -->
<template>
<view class="auth-page register-page">
<view class="page-canvas">
<image
class="page-backdrop"
src="/static/assets/modules/auth/opaque/a01-red-hall-ink-backdrop-v1.png"
mode="scaleToFill"
/>
<image
class="brand-seal"
src="/static/assets/foundation/transparent/brand-seal.png"
mode="aspectFit"
/>
<AuthPageShell class="auth-page register-page">
<view class="register-content">
<view class="page-heading">
<view class="back-button" hover-class="tap-fade" @click="goBack">
@@ -149,17 +137,19 @@
>
</view>
</view>
</view>
<view v-if="feedbackVisible" class="feedback-toast">
<text class="feedback-toast__copy">{{ feedbackMessage }}</text>
</view>
</view>
<template #overlay>
<view v-if="feedbackVisible" class="feedback-toast">
<text class="feedback-toast__copy">{{ feedbackMessage }}</text>
</view>
</template>
</AuthPageShell>
</template>
<script setup>
import { ref } from "vue";
import { onUnload } from "@dcloudio/uni-app";
import AuthPageShell from "@/components/AuthPageShell.vue";
const phone = ref("");
const password = ref("");
@@ -228,51 +218,24 @@ const submitRegister = () => {
</script>
<style scoped lang="scss">
.auth-page {
width: 100%;
height: 100dvh;
min-height: 100%;
overflow-y: auto;
background: #f7f0e4;
color: #493323;
}
.page-canvas {
display: grid;
box-sizing: border-box;
width: 100%;
min-height: calc(1665rpx + env(safe-area-inset-bottom));
padding-bottom: env(safe-area-inset-bottom);
}
.page-backdrop {
grid-area: 1 / 1;
align-self: start;
width: 100%;
height: 1665rpx;
pointer-events: none;
}
.brand-seal {
z-index: 1;
grid-area: 1 / 1;
align-self: start;
justify-self: center;
width: 184rpx;
height: 221rpx;
margin-top: 64rpx;
pointer-events: none;
}
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.register-content {
z-index: 2;
grid-area: 1 / 1;
display: flex;
flex: 1;
flex-direction: column;
justify-content: flex-start;
box-sizing: border-box;
padding: 430rpx 64rpx 46rpx;
width: 100%;
max-width: 480px;
min-width: 0;
margin: 0 auto;
padding: clamp(10px, 2.2vh, 20px) clamp(24px, 8.5vw, 44px);
}
.page-heading {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto auto auto;
justify-items: center;
min-height: 154rpx;
@@ -286,8 +249,8 @@ const submitRegister = () => {
justify-self: start;
align-items: center;
justify-content: center;
width: 88rpx;
height: 88rpx;
width: var(--app-touch-min);
height: var(--app-touch-min);
margin-top: 10rpx;
}
@@ -298,14 +261,16 @@ const submitRegister = () => {
}
.page-title {
grid-column: 1;
grid-row: 1;
color: #9f170f;
font-size: 66rpx;
font-size: 58rpx;
font-weight: 700;
letter-spacing: 7rpx;
}
.page-subtitle {
grid-column: 1;
grid-row: 2;
margin-top: 12rpx;
color: #806c58;
@@ -314,6 +279,7 @@ const submitRegister = () => {
}
.register-divider {
grid-column: 1;
grid-row: 3;
width: 300rpx;
height: 50rpx;
@@ -331,26 +297,26 @@ const submitRegister = () => {
.input-row {
display: flex;
align-items: center;
min-height: 104rpx;
min-height: var(--app-touch-min);
border-bottom: 1rpx solid rgba(182, 116, 43, 0.68);
}
.input-label {
flex: 0 0 152rpx;
color: #674b35;
font-size: 31rpx;
font-size: 30rpx;
}
.auth-input {
flex: 1;
min-width: 0;
min-height: 96rpx;
color: #3f2c1d;
font-size: 29rpx;
min-height: var(--app-touch-min);
color: #493323;
font-size: 30rpx;
}
.placeholder {
color: #a79e94;
color: #9f968d;
}
.field-error,
@@ -372,7 +338,7 @@ const submitRegister = () => {
.register-submit {
display: grid;
place-items: center;
min-height: 104rpx;
min-height: var(--app-touch-min);
margin-top: 36rpx;
}
@@ -386,7 +352,7 @@ const submitRegister = () => {
z-index: 1;
grid-area: 1 / 1;
color: #fffaf0;
font-size: 39rpx;
font-size: 36rpx;
letter-spacing: 6rpx;
}
@@ -399,9 +365,9 @@ const submitRegister = () => {
display: flex;
align-items: center;
justify-content: center;
min-height: 76rpx;
color: #58483c;
font-size: 22rpx;
min-height: var(--app-touch-min);
color: #493323;
font-size: 24rpx;
line-height: 30rpx;
}
@@ -446,14 +412,15 @@ const submitRegister = () => {
align-items: center;
justify-content: center;
min-height: 82rpx;
color: #6b5a4a;
font-size: 25rpx;
margin-top: auto;
color: #493323;
font-size: 28rpx;
}
.login-entry__link {
display: flex;
align-items: center;
min-height: 82rpx;
min-height: var(--app-touch-min);
margin-left: 8rpx;
color: #a7160c;
}
@@ -468,20 +435,14 @@ const submitRegister = () => {
.feedback-toast {
position: fixed;
z-index: 30;
top: calc(env(safe-area-inset-top) + 24rpx);
top: calc(var(--status-bar-height, 0px) + 24rpx);
left: 50%;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
@include adaptive.adaptive-feedback-toast;
width: 590rpx;
min-height: 82rpx;
border: 16rpx solid transparent;
border-width: 16rpx 74rpx;
border-image-source: url("/static/assets/foundation/transparent/a01-scroll-toast-v3.png");
border-image-slice: 58 280 fill;
border-image-width: 16rpx 74rpx;
border-image-repeat: stretch;
background: transparent;
transform: translateX(-50%);
}
+64 -115
View File
@@ -1,18 +1,6 @@
<!-- 页面编号A-05用途通过手机号验证后重设登录密码 -->
<template>
<view class="auth-page reset-page">
<view class="page-canvas">
<image
class="page-backdrop"
src="/static/assets/modules/auth/opaque/a01-red-hall-ink-backdrop-v1.png"
mode="scaleToFill"
/>
<image
class="brand-seal"
src="/static/assets/foundation/transparent/brand-seal.png"
mode="aspectFit"
/>
<AuthPageShell class="auth-page reset-page">
<view class="reset-content">
<view class="page-heading">
<view class="back-button" hover-class="tap-fade" @click="goBack">
@@ -147,48 +135,50 @@
>
</view>
</view>
</view>
<view v-if="successVisible" class="success-layer">
<view class="success-dialog">
<image
class="success-dialog__skin"
src="/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png"
mode="aspectFit"
/>
<view class="success-dialog__content">
<template #overlay>
<view v-if="successVisible" class="success-layer">
<view class="success-dialog">
<image
class="success-mark"
src="/static/assets/foundation/transparent/brand-seal.png"
class="success-dialog__skin"
src="/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png"
mode="aspectFit"
/>
<text class="success-title">密码已重设</text>
<text class="success-copy">请返回登录页使用新密码登录</text>
<view
class="success-action"
hover-class="button-hover"
@click="prepareLogin"
>
<view class="success-dialog__content">
<image
class="success-action__skin"
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
class="success-mark"
src="/static/assets/foundation/transparent/brand-seal.png"
mode="aspectFit"
/>
<text class="success-action__copy">返回登录</text>
<text class="success-title">密码已重设</text>
<text class="success-copy">请返回登录页使用新密码登录</text>
<view
class="success-action"
hover-class="button-hover"
@click="prepareLogin"
>
<image
class="success-action__skin"
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="aspectFit"
/>
<text class="success-action__copy">返回登录</text>
</view>
</view>
</view>
</view>
</view>
<view v-if="feedbackVisible" class="feedback-toast">
<text class="feedback-toast__copy">{{ feedbackMessage }}</text>
</view>
</view>
<view v-if="feedbackVisible" class="feedback-toast">
<text class="feedback-toast__copy">{{ feedbackMessage }}</text>
</view>
</template>
</AuthPageShell>
</template>
<script setup>
import { ref } from "vue";
import { onUnload } from "@dcloudio/uni-app";
import AuthPageShell from "@/components/AuthPageShell.vue";
const phone = ref("");
const verificationCode = ref("");
@@ -262,51 +252,24 @@ const submitReset = () => {
</script>
<style scoped lang="scss">
.auth-page {
width: 100%;
height: 100dvh;
min-height: 100%;
overflow-y: auto;
background: #f7f0e4;
color: #493323;
}
.page-canvas {
display: grid;
box-sizing: border-box;
width: 100%;
min-height: calc(1665rpx + env(safe-area-inset-bottom));
padding-bottom: env(safe-area-inset-bottom);
}
.page-backdrop {
grid-area: 1 / 1;
align-self: start;
width: 100%;
height: 1665rpx;
pointer-events: none;
}
.brand-seal {
z-index: 1;
grid-area: 1 / 1;
align-self: start;
justify-self: center;
width: 184rpx;
height: 221rpx;
margin-top: 64rpx;
pointer-events: none;
}
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.reset-content {
z-index: 2;
grid-area: 1 / 1;
display: flex;
flex: 1;
flex-direction: column;
justify-content: flex-start;
box-sizing: border-box;
padding: 430rpx 64rpx 46rpx;
width: 100%;
max-width: 480px;
min-width: 0;
margin: 0 auto;
padding: clamp(10px, 2.2vh, 20px) clamp(24px, 8.5vw, 44px);
}
.page-heading {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto auto auto;
justify-items: center;
min-height: 154rpx;
@@ -320,8 +283,8 @@ const submitReset = () => {
justify-self: start;
align-items: center;
justify-content: center;
width: 88rpx;
height: 88rpx;
width: var(--app-touch-min);
height: var(--app-touch-min);
margin-top: 10rpx;
}
@@ -331,13 +294,15 @@ const submitReset = () => {
transform: scaleX(-1);
}
.page-title {
grid-column: 1;
grid-row: 1;
color: #9f170f;
font-size: 66rpx;
font-size: 58rpx;
font-weight: 700;
letter-spacing: 7rpx;
}
.page-subtitle {
grid-column: 1;
grid-row: 2;
margin-top: 12rpx;
color: #806c58;
@@ -345,6 +310,7 @@ const submitReset = () => {
letter-spacing: 1rpx;
}
.reset-divider {
grid-column: 1;
grid-row: 3;
width: 300rpx;
height: 50rpx;
@@ -361,31 +327,31 @@ const submitReset = () => {
.input-row {
display: flex;
align-items: center;
min-height: 84rpx;
min-height: var(--app-touch-min);
border-bottom: 1rpx solid #cfaa70;
}
.input-label {
flex: 0 0 162rpx;
color: #674b35;
font-size: 29rpx;
font-size: 30rpx;
}
.auth-input {
flex: 1;
min-width: 0;
min-height: 82rpx;
color: #3f2c1d;
font-size: 27rpx;
min-height: var(--app-touch-min);
color: #493323;
font-size: 30rpx;
}
.placeholder {
color: #a79e94;
color: #9f968d;
}
.get-code {
flex: 0 0 auto;
padding-left: 14rpx;
border-left: 1rpx solid #d7bd94;
color: #a7160c;
font-size: 23rpx;
font-size: 26rpx;
}
.field-error {
display: block;
@@ -402,7 +368,7 @@ const submitReset = () => {
.reset-submit {
display: grid;
place-items: center;
min-height: 104rpx;
min-height: var(--app-touch-min);
margin-top: 26rpx;
}
@@ -416,28 +382,29 @@ const submitReset = () => {
z-index: 1;
grid-area: 1 / 1;
color: #fffaf0;
font-size: 39rpx;
font-size: 36rpx;
letter-spacing: 6rpx;
}
.reset-tip {
display: block;
margin-top: 22rpx;
color: #806c58;
font-size: 22rpx;
font-size: 24rpx;
text-align: center;
}
.login-entry {
display: flex;
align-items: center;
justify-content: center;
min-height: 78rpx;
color: #6b5a4a;
font-size: 24rpx;
min-height: var(--app-touch-min);
margin-top: auto;
color: #493323;
font-size: 28rpx;
}
.login-entry__link {
display: flex;
align-items: center;
min-height: 78rpx;
min-height: var(--app-touch-min);
margin-left: 8rpx;
color: #a7160c;
}
@@ -458,7 +425,7 @@ const submitReset = () => {
display: grid;
width: 620rpx;
min-height: 520rpx;
max-height: calc(100vh - 80rpx);
max-height: calc(var(--app-viewport-height) - 40px);
}
.success-dialog__skin {
grid-area: 1 / 1;
@@ -512,20 +479,14 @@ const submitReset = () => {
.feedback-toast {
position: fixed;
z-index: 30;
top: calc(env(safe-area-inset-top) + 24rpx);
top: calc(var(--status-bar-height, 0px) + 24rpx);
left: 50%;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
@include adaptive.adaptive-feedback-toast;
width: 590rpx;
min-height: 82rpx;
border: 16rpx solid transparent;
border-width: 16rpx 74rpx;
border-image-source: url("/static/assets/foundation/transparent/a01-scroll-toast-v3.png");
border-image-slice: 58 280 fill;
border-image-width: 16rpx 74rpx;
border-image-repeat: stretch;
background: transparent;
transform: translateX(-50%);
}
@@ -541,16 +502,4 @@ const submitReset = () => {
opacity: 0.72;
}
@media screen and (max-width: 340px) {
.reset-content {
padding-right: 48rpx;
padding-left: 48rpx;
}
.page-title {
font-size: 60rpx;
}
.input-label {
flex-basis: 150rpx;
}
}
</style>
+52 -94
View File
@@ -1,18 +1,6 @@
<!-- 页面编号A-06用途只承接账号冻结停用与风险限制等阻断登录状态 -->
<template>
<view class="auth-page status-page">
<view class="page-canvas">
<image
class="page-backdrop"
src="/static/assets/modules/auth/opaque/a01-red-hall-ink-backdrop-v1.png"
mode="scaleToFill"
/>
<image
class="brand-seal"
src="/static/assets/foundation/transparent/brand-seal.png"
mode="aspectFit"
/>
<AuthPageShell class="auth-page status-page">
<view class="status-content">
<view class="page-heading">
<view class="back-button" hover-class="tap-fade" @click="goBack">
@@ -85,42 +73,48 @@
>返回登录</view
>
</view>
</view>
<view v-if="recoveryVisible" class="recovery-layer" @click="closeRecovery">
<view class="recovery-dialog" @click.stop>
<image
class="recovery-dialog__skin"
src="/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png"
mode="aspectFit"
/>
<view class="recovery-dialog__content">
<text class="recovery-title">{{ currentState.recoveryTitle }}</text>
<text class="recovery-copy">{{ currentState.recoveryDetail }}</text>
<text class="recovery-note"
>账号恢复服务将在功能阶段接入当前仅审核页面样式与状态位置</text
>
<view
class="recovery-action"
hover-class="button-hover"
@click="closeRecovery"
>
<image
class="recovery-action__skin"
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="aspectFit"
/>
<text class="recovery-action__copy">我知道了</text>
<template #overlay>
<view
v-if="recoveryVisible"
class="recovery-layer"
@click="closeRecovery"
>
<view class="recovery-dialog" @click.stop>
<image
class="recovery-dialog__skin"
src="/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png"
mode="aspectFit"
/>
<view class="recovery-dialog__content">
<text class="recovery-title">{{ currentState.recoveryTitle }}</text>
<text class="recovery-copy">{{ currentState.recoveryDetail }}</text>
<text class="recovery-note"
>账号恢复服务将在功能阶段接入当前仅审核页面样式与状态位置</text
>
<view
class="recovery-action"
hover-class="button-hover"
@click="closeRecovery"
>
<image
class="recovery-action__skin"
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="aspectFit"
/>
<text class="recovery-action__copy">我知道了</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
</AuthPageShell>
</template>
<script setup>
import { computed, ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import AuthPageShell from "@/components/AuthPageShell.vue";
const status = ref("risk");
const recoveryVisible = ref(false);
@@ -203,47 +197,20 @@ const goBack = () => uni.navigateBack();
</script>
<style scoped lang="scss">
.auth-page {
width: 100%;
height: 100dvh;
min-height: 100%;
overflow-y: auto;
background: #f7f0e4;
color: #493323;
}
.page-canvas {
display: grid;
box-sizing: border-box;
width: 100%;
min-height: calc(1665rpx + env(safe-area-inset-bottom));
padding-bottom: env(safe-area-inset-bottom);
}
.page-backdrop {
grid-area: 1 / 1;
align-self: start;
width: 100%;
height: 1665rpx;
pointer-events: none;
}
.brand-seal {
z-index: 1;
grid-area: 1 / 1;
align-self: start;
justify-self: center;
width: 184rpx;
height: 221rpx;
margin-top: 64rpx;
pointer-events: none;
}
.status-content {
z-index: 2;
grid-area: 1 / 1;
display: flex;
flex: 1;
flex-direction: column;
box-sizing: border-box;
padding: 430rpx 64rpx 46rpx;
width: 100%;
max-width: 480px;
min-width: 0;
margin: 0 auto;
padding: clamp(10px, 2.2vh, 20px) clamp(24px, 8.5vw, 44px);
}
.page-heading {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto auto auto;
justify-items: center;
min-height: 154rpx;
@@ -256,8 +223,8 @@ const goBack = () => uni.navigateBack();
justify-self: start;
align-items: center;
justify-content: center;
width: 88rpx;
height: 88rpx;
width: var(--app-touch-min);
height: var(--app-touch-min);
margin-top: 10rpx;
}
.back-button__icon {
@@ -266,13 +233,15 @@ const goBack = () => uni.navigateBack();
transform: scaleX(-1);
}
.page-title {
grid-column: 1;
grid-row: 1;
color: #9f170f;
font-size: 66rpx;
font-size: 58rpx;
font-weight: 700;
letter-spacing: 7rpx;
}
.page-subtitle {
grid-column: 1;
grid-row: 2;
margin-top: 12rpx;
color: #806c58;
@@ -280,6 +249,7 @@ const goBack = () => uni.navigateBack();
letter-spacing: 1rpx;
}
.status-divider {
grid-column: 1;
grid-row: 3;
width: 300rpx;
height: 50rpx;
@@ -348,7 +318,7 @@ const goBack = () => uni.navigateBack();
.status-primary {
display: grid;
place-items: center;
min-height: 104rpx;
min-height: var(--app-touch-min);
margin-top: 28rpx;
}
.status-primary__skin,
@@ -368,7 +338,7 @@ const goBack = () => uni.navigateBack();
display: flex;
align-items: center;
justify-content: center;
min-height: 82rpx;
min-height: var(--app-touch-min);
color: #a7160c;
font-size: 24rpx;
}
@@ -388,7 +358,7 @@ const goBack = () => uni.navigateBack();
display: grid;
width: 620rpx;
min-height: 520rpx;
max-height: calc(100vh - 80rpx);
max-height: calc(var(--app-viewport-height) - 40px);
}
.recovery-dialog__skin {
grid-area: 1 / 1;
@@ -444,16 +414,4 @@ const goBack = () => uni.navigateBack();
opacity: 0.72;
}
@media screen and (max-width: 340px) {
.status-content {
padding-right: 48rpx;
padding-left: 48rpx;
}
.page-title {
font-size: 60rpx;
}
.status-detail__label {
flex-basis: 108rpx;
}
}
</style>
+5 -3
View File
@@ -144,6 +144,8 @@ const openSection = (key) => {
};
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.family-page {
display: flex;
min-height: 100vh;
@@ -173,12 +175,12 @@ const openSection = (key) => {
font-size: 23rpx;
}
.feed-shortcuts {
@include adaptive.adaptive-scroll-button(secondary);
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
width: 100%;
min-height: 48px;
margin-top: 15rpx;
background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png") center / 100% 100% no-repeat;
}
.feed-shortcut {
width: 100%;
@@ -196,6 +198,7 @@ const openSection = (key) => {
}
.feed-card,
.feed-state-card {
@include adaptive.adaptive-family-letter;
display: flex;
width: 100%;
min-height: 230rpx;
@@ -204,7 +207,6 @@ const openSection = (key) => {
justify-content: center;
margin-top: 17rpx;
box-sizing: border-box;
background: url("/static/assets/modules/family/transparent/f01-family-letter-card.png") center / 100% 100% no-repeat;
}
.feed-card__copy {
padding: 15% 9%;
@@ -259,11 +261,11 @@ const openSection = (key) => {
line-height: 1.45;
}
.feed-action {
@include adaptive.adaptive-scroll-button(primary);
width: 514rpx;
max-width: 100%;
min-height: 76rpx;
margin: 19rpx auto 0;
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / 100% 100% no-repeat;
}
.feed-action text {
display: flex;
+4 -2
View File
@@ -80,6 +80,8 @@ onUnmounted(() => {
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.publish-page {
display: flex;
min-height: 100vh;
@@ -88,6 +90,7 @@ onUnmounted(() => {
}
.publish-page__header,
.publish-panel {
@include adaptive.adaptive-family-content;
z-index: 1;
}
.publish-panel {
@@ -96,7 +99,6 @@ onUnmounted(() => {
margin: 18rpx auto 0;
padding: 9%;
box-sizing: border-box;
background: url("/static/assets/modules/family/transparent/module-content-frame.png") center / 100% 100% no-repeat;
}
.publish-form > text,
.publish-result > text {
@@ -117,12 +119,12 @@ onUnmounted(() => {
line-height: 1.6;
}
.publish-field {
@include adaptive.adaptive-family-field;
display: flex;
min-height: 250rpx;
margin-top: 24rpx;
padding: 25rpx;
box-sizing: border-box;
background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat;
}
.publish-field textarea {
width: 100%;
+5 -3
View File
@@ -121,11 +121,13 @@ onUnmounted(() => {
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.feed-detail-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.feed-detail-header, .feed-detail-loading, .feed-detail-content { z-index: 1; }
.feed-detail-loading { min-height: calc(100vh - 100rpx); }
.feed-detail-content { padding: 18rpx 24rpx 72rpx; }
.feed-article-card, .feed-comments-panel, .feed-comment-form, .feed-state-card { width: 100%; box-sizing: border-box; background: url("/static/assets/modules/family/transparent/module-content-frame.png") center / 100% 100% no-repeat; }
.feed-article-card, .feed-comments-panel, .feed-comment-form, .feed-state-card { @include adaptive.adaptive-family-content; width: 100%; }
.feed-article-card { padding: 46rpx 48rpx 42rpx; }
.feed-article-card text, .feed-state-card > text { display: block; }
.feed-article-card__meta, .feed-article-card__author { color: $ink-muted; font-size: 22rpx; }
@@ -135,7 +137,7 @@ onUnmounted(() => {
.feed-comments-panel { margin-top: 18rpx; padding: 38rpx 38rpx 34rpx; }
.feed-comments-heading { display: flex; align-items: center; justify-content: space-between; color: $brand-red; font-size: 24rpx; font-weight: 700; }
.feed-comments-heading text:last-child { color: $ink-muted; font-size: 21rpx; font-weight: 400; }
.feed-comment-card { margin-top: 16rpx; padding: 20rpx 22rpx; background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat; }
.feed-comment-card { @include adaptive.adaptive-family-field; margin-top: 16rpx; padding: 20rpx 22rpx; }
.feed-comment-card > view { display: flex; justify-content: space-between; gap: 18rpx; color: $ink-muted; font-size: 20rpx; }
.feed-comment-card > text { display: block; margin-top: 9rpx; color: $ink; font-size: 24rpx; line-height: 1.55; }
.feed-comments-empty { padding: 34rpx 10rpx 20rpx; text-align: center; }
@@ -143,7 +145,7 @@ onUnmounted(() => {
.feed-comments-empty text:first-child { color: $ink; font-size: 28rpx; font-weight: 700; }
.feed-comment-form { margin-top: 18rpx; padding: 38rpx 40rpx 42rpx; }
.feed-comment-form > text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 30rpx; font-weight: 700; }
.feed-comment-form textarea { width: auto; min-width: 0; min-height: 126rpx; margin-top: 16rpx; padding: 20rpx 22rpx; box-sizing: border-box; color: $ink; font-size: 24rpx; line-height: 1.55; background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat; }
.feed-comment-form textarea { @include adaptive.adaptive-family-field; width: auto; min-width: 0; min-height: 126rpx; margin-top: 16rpx; padding: 20rpx 22rpx; color: $ink; font-size: 24rpx; line-height: 1.55; }
.feed-comment-error { display: block; margin-top: 10rpx; color: $brand-red; font-size: 22rpx; }
.feed-comment-form .app-button { margin-top: 22rpx; }
.feed-state-card { min-height: 340rpx; margin-top: 36rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
+5 -3
View File
@@ -89,19 +89,21 @@ const resetFilters = () => { keyword.value = ""; activeCategory.value = "全部"
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.article-list-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.article-list-header, .article-list-loading, .article-list-content { z-index: 1; }
.article-list-loading { min-height: calc(100vh - 100rpx); }
.article-list-content { padding: 18rpx 24rpx 72rpx; }
.article-search { min-height: 82rpx; padding: 12rpx 26rpx; box-sizing: border-box; background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat; }
.article-search { @include adaptive.adaptive-family-field; min-height: 82rpx; padding: 12rpx 26rpx; }
.article-search input { width: 100%; min-height: 58rpx; color: $ink; font-size: 24rpx; }
.article-categories { width: 100%; margin-top: 16rpx; }
.article-categories__row { display: flex; flex-wrap: wrap; gap: 12rpx; padding: 2rpx 4rpx 8rpx; }
.article-category { min-height: 58rpx; padding: 0 28rpx; color: $ink-muted; font-size: 23rpx; background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat; }
.article-category { @include adaptive.adaptive-family-field; min-height: 58rpx; padding: 0 28rpx; color: $ink-muted; font-size: 23rpx; }
.article-category text { display: flex; min-height: 58rpx; align-items: center; }
.article-category--active { color: $brand-red; font-weight: 700; }
.article-list { display: flex; flex-direction: column; gap: 16rpx; margin-top: 10rpx; }
.article-card, .article-list-state-card { width: 100%; box-sizing: border-box; background: url("/static/assets/modules/family/transparent/module-content-frame.png") center / 100% 100% no-repeat; }
.article-card, .article-list-state-card { @include adaptive.adaptive-family-content; width: 100%; }
.article-card { min-height: 196rpx; padding: 34rpx 46rpx 30rpx; }
.article-card > text { display: block; }
.article-card__category { color: $brand-red; font-size: 21rpx; font-weight: 700; letter-spacing: 2rpx; }
+3 -1
View File
@@ -91,11 +91,13 @@ onUnmounted(() => { if (toastTimer) clearTimeout(toastTimer); });
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.article-detail-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.article-detail-header, .article-detail-loading, .article-detail-content { z-index: 1; }
.article-detail-loading { min-height: calc(100vh - 100rpx); }
.article-detail-content { padding: 18rpx 24rpx 72rpx; }
.article-paper, .article-state-card { width: 100%; box-sizing: border-box; background: url("/static/assets/modules/family/transparent/module-content-frame.png") center / 100% 100% no-repeat; }
.article-paper, .article-state-card { @include adaptive.adaptive-family-content; width: 100%; }
.article-paper { padding: 50rpx 48rpx 54rpx; }
.article-paper > text { display: block; }
.article-paper__category { color: $brand-red; font-size: 22rpx; font-weight: 700; letter-spacing: 3rpx; }
+4 -7
View File
@@ -220,6 +220,8 @@ onUnload(() => {
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.article-editor-page {
display: flex;
min-height: 100vh;
@@ -239,13 +241,8 @@ onUnload(() => {
}
.editor-panel,
.editor-result-card {
@include adaptive.adaptive-family-panel;
width: 100%;
border: 20rpx solid transparent;
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;
box-sizing: border-box;
}
.editor-panel__body {
padding: 38rpx 34rpx 42rpx;
@@ -285,10 +282,10 @@ onUnload(() => {
font-weight: 700;
}
.editor-control {
@include adaptive.adaptive-family-field;
display: flex;
min-height: 82rpx;
align-items: center;
background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat;
}
.editor-control input,
.editor-control textarea {
+5 -3
View File
@@ -102,13 +102,15 @@ onUnmounted(() => { if (toastTimer) clearTimeout(toastTimer); });
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.album-list-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.album-list-header, .album-list-loading, .album-list-content { z-index: 1; }
.album-list-loading { min-height: calc(100vh - 100rpx); }
.album-list-content { padding: 18rpx 24rpx 72rpx; }
.album-list-lead { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8rpx 18rpx; min-height: 62rpx; padding: 0 20rpx; color: $ink-muted; font-size: 22rpx; background: url("/static/assets/modules/genealogy/transparent/section-divider.png") center / 100% 100% no-repeat; }
.album-list-lead { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8rpx 18rpx; min-height: 62rpx; padding: 0 20rpx; color: $ink-muted; font-size: 22rpx; background: url("/static/assets/modules/genealogy/transparent/section-divider.png") center / 100% auto no-repeat; }
.album-list { display: flex; flex-direction: column; gap: 16rpx; margin-top: 16rpx; }
.album-card, .album-state-card { width: 100%; box-sizing: border-box; background: url("/static/assets/modules/family/transparent/module-content-frame.png") center / 100% 100% no-repeat; }
.album-card, .album-state-card { @include adaptive.adaptive-family-content; width: 100%; }
.album-card { display: grid; grid-template-columns: minmax(150rpx, 0.7fr) minmax(0, 1.3fr); min-height: 210rpx; gap: 22rpx; padding: 30rpx 38rpx; }
.album-card__media { width: 100%; aspect-ratio: 4 / 3; align-self: center; }
.album-card__cover { display: block; width: 100%; height: 100%; }
@@ -125,7 +127,7 @@ onUnmounted(() => { if (toastTimer) clearTimeout(toastTimer); });
.album-state-card .app-button { margin-top: 28rpx; }
.album-dialog-field { width: 100%; margin: 24rpx 0; text-align: left; }
.album-dialog-field > text:first-child { display: block; color: $ink; font-size: 23rpx; font-weight: 700; }
.album-dialog-field input { width: 100%; min-height: 76rpx; margin-top: 10rpx; padding: 0 22rpx; box-sizing: border-box; color: $ink; font-size: 24rpx; background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat; }
.album-dialog-field input { @include adaptive.adaptive-family-field; width: 100%; min-height: 76rpx; margin-top: 10rpx; padding: 0 22rpx; color: $ink; font-size: 24rpx; }
.album-dialog-field > text:last-child { display: block; margin-top: 8rpx; color: $brand-red; font-size: 21rpx; }
@media (max-width: 340px) { .album-card { grid-template-columns: 130rpx minmax(0, 1fr); gap: 16rpx; padding-right: 30rpx; padding-left: 30rpx; } }
</style>
+3 -6
View File
@@ -158,6 +158,8 @@ onBackPress(() => {
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.album-detail-page {
display: flex;
min-height: 100vh;
@@ -255,14 +257,9 @@ onBackPress(() => {
}
.album-empty-state,
.album-expired-state {
@include adaptive.adaptive-family-panel;
width: 100%;
min-height: 430rpx;
box-sizing: border-box;
border: 20rpx solid transparent;
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;
}
.album-state-card__body {
padding: 104rpx 40rpx 62rpx;
+4 -10
View File
@@ -342,6 +342,8 @@ onLoad((query) => {
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.media-upload-page {
display: flex;
min-height: 100vh;
@@ -356,6 +358,7 @@ onLoad((query) => {
padding: 20rpx 24rpx calc(48rpx + env(safe-area-inset-bottom));
}
.media-album-card {
@include adaptive.adaptive-family-panel;
display: grid;
min-height: 154rpx;
box-sizing: border-box;
@@ -363,11 +366,6 @@ onLoad((query) => {
gap: 20rpx;
align-items: center;
padding: 14rpx 20rpx;
border: 20rpx solid transparent;
border-image-source: url("/static/assets/modules/family/transparent/module-content-frame.png");
border-image-slice: 72 fill;
border-image-width: 58rpx;
border-image-repeat: stretch;
}
.media-album-card__cover {
width: 132rpx;
@@ -604,15 +602,11 @@ onLoad((query) => {
}
.media-permission-card,
.media-success-card {
@include adaptive.adaptive-family-panel;
min-height: 410rpx;
margin-top: 24rpx;
box-sizing: border-box;
padding: 98rpx 46rpx 58rpx;
border: 20rpx solid transparent;
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;
text-align: center;
}
.media-state-card__eyebrow {
+23 -22
View File
@@ -82,14 +82,14 @@
src="/static/assets/foundation/transparent/meta-admin.png"
mode="aspectFit"
/>
<text class="current-meta-item-text">管理员</text>
<text class="current-meta-item-text">{{ currentRoleLabel }}</text>
</view>
</view>
</view>
<view class="shortcut-grid">
<view
v-for="item in shortcuts"
v-for="item in visibleShortcuts"
:key="item.key"
class="shortcut-item"
@click="openShortcut(item.key)"
@@ -420,6 +420,12 @@ const currentGenealogy = computed(
(item) => item.id === selectedGenealogyId.value,
) || availableGenealogies.value[0],
);
const isCurrentGenealogyOwner = computed(
() => currentGenealogy.value?.membership === "created",
);
const currentRoleLabel = computed(() =>
isCurrentGenealogyOwner.value ? "管理员" : "成员",
);
// 仅用于页面样式阶段覆盖加入申请的关键状态,不作为接口数据。
const applicationRecords = [
@@ -468,6 +474,11 @@ const shortcuts = [
icon: "/static/assets/modules/genealogy/transparent/shortcut-application.png",
},
];
const visibleShortcuts = computed(() =>
isCurrentGenealogyOwner.value
? shortcuts
: shortcuts.filter((item) => item.key !== "applications"),
);
const openGenealogy = (genealogy) => {
genealogyContext.setCurrentGenealogyId(genealogy.id);
@@ -550,6 +561,8 @@ const openShortcut = (key) => {
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.genealogy-index {
display: flex;
min-height: 100vh;
@@ -592,11 +605,10 @@ const openShortcut = (key) => {
}
.current-slip {
@include adaptive.adaptive-genealogy-current-slip;
display: block;
min-height: 268rpx;
padding: 30rpx 34rpx 28rpx;
box-sizing: border-box;
background: url("/static/assets/modules/genealogy/transparent/current-slip-frame.png") center / 100% 100% no-repeat;
}
.current-seal {
@@ -607,7 +619,7 @@ const openShortcut = (key) => {
align-items: center;
justify-content: center;
margin-right: 24rpx;
background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png") center / 100% 100% no-repeat;
background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png") center / contain no-repeat;
color: #fff7e7;
}
.current-seal-title {
@@ -740,12 +752,11 @@ const openShortcut = (key) => {
}
.application-record {
@include adaptive.adaptive-genealogy-list-card;
display: flex;
min-height: 138rpx;
align-items: center;
box-sizing: border-box;
padding: 22rpx 26rpx;
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png") center / 100% 100% no-repeat;
}
.application-record + .application-record {
margin-top: 12rpx;
@@ -864,9 +875,9 @@ const openShortcut = (key) => {
}
.state-panel--error {
@include adaptive.adaptive-genealogy-state-panel;
min-height: 1120rpx;
padding: 0;
background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png") center / 100% 100% no-repeat;
}
.error-panel__content {
@@ -922,8 +933,8 @@ const openShortcut = (key) => {
}
.empty-panel {
@include adaptive.adaptive-genealogy-state-panel;
min-height: 1120rpx;
background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png") center / 100% 100% no-repeat;
}
.empty-panel__content {
@@ -943,7 +954,7 @@ const openShortcut = (key) => {
height: 184rpx;
align-items: center;
justify-content: center;
background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png") center / 100% 100% no-repeat;
background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png") center / contain no-repeat;
}
.empty-seal__text {
@@ -1056,15 +1067,10 @@ const openShortcut = (key) => {
background: rgba(34, 20, 12, 0.68);
}
.add-dialog {
@include adaptive.adaptive-g01-add-sheet;
width: 100%;
min-height: 780rpx;
max-height: calc(100vh - 80rpx);
box-sizing: border-box;
border: 1px solid transparent;
border-image-source: url("/static/assets/modules/genealogy/transparent/g01-add-sheet-background-v3.png");
border-image-slice: 220 0 1 0 fill;
border-image-width: 118rpx 0 1rpx;
border-image-repeat: stretch;
}
.add-dialog__content {
display: flex;
@@ -1132,16 +1138,11 @@ const openShortcut = (key) => {
background: rgba(34, 20, 12, 0.58);
}
.genealogy-switcher {
@include adaptive.adaptive-g01-switcher;
width: 670rpx;
max-width: 100%;
min-height: 600rpx;
max-height: calc(100vh - 120rpx);
box-sizing: border-box;
border: 1px solid transparent;
border-image-source: url("/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png");
border-image-slice: 300 260 360 260 fill;
border-image-width: 110rpx 48rpx 132rpx 48rpx;
border-image-repeat: stretch;
}
.genealogy-switcher__content {
display: grid;
+10 -60
View File
@@ -3,21 +3,11 @@
<view class="flow-page">
<GenealogyPageBackground />
<view class="flow-header">
<view class="flow-header__content">
<view class="flow-header__back" @click="goBack">
<image
class="flow-header__back-icon"
src="/static/assets/foundation/transparent/chevron-right.png"
mode="aspectFit"
/>
</view>
<text class="flow-header__title">{{
isAncestorStep ? "录入首代人物" : "创建家谱"
}}</text>
<view class="flow-header__side" />
</view>
</view>
<PageHeader
:title="isAncestorStep ? '录入首代人物' : '创建家谱'"
custom-back
@back="goBack"
/>
<view class="flow-content">
<view class="create-flow-panel">
@@ -34,7 +24,7 @@
<input
v-model="createForm.surname"
maxlength="4"
placeholder="如:汤"
placeholder="请输入姓氏"
placeholder-class="placeholder"
@input="clearFieldError('surname')"
/>
@@ -47,7 +37,7 @@
<input
v-model="createForm.name"
maxlength="24"
placeholder="如:汤氏家谱"
placeholder="请输入家谱名称"
placeholder-class="placeholder"
@input="clearFieldError('name')"
/>
@@ -248,6 +238,7 @@
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
const currentStep = ref("create");
const genealogyId = ref("");
@@ -416,6 +407,7 @@ const enterOverview = () =>
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.flow-page {
display: flex;
min-height: 100vh;
@@ -423,56 +415,14 @@ const enterOverview = () =>
background: #f9f6ef;
}
.flow-header {
z-index: 3;
height: 112rpx;
overflow: hidden;
background: url("/static/assets/foundation/opaque/root-header-cinnabar.jpg")
center / 100% 100% no-repeat;
}
.flow-header__content {
z-index: 1;
display: flex;
height: 100%;
align-items: center;
justify-content: space-between;
padding: 0 26rpx;
box-sizing: border-box;
}
.flow-header__back,
.flow-header__side {
display: flex;
width: 72rpx;
align-items: center;
}
.flow-header__back-icon {
width: 34rpx;
height: 34rpx;
transform: rotate(180deg);
}
.flow-header__title {
flex: 1;
color: #ffe4a7;
font-family: "STKaiti", "KaiTi", serif;
font-size: 39rpx;
font-weight: 700;
letter-spacing: 4rpx;
text-align: center;
}
.flow-content {
z-index: 2;
padding: 24rpx 32rpx 48rpx;
}
.create-flow-panel {
@include adaptive.adaptive-genealogy-state-panel;
min-height: 1000rpx;
background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")
center / 100% 100% no-repeat;
}
.create-flow-panel__content {
+3 -4
View File
@@ -304,6 +304,7 @@ const applyToJoin = () =>
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.overview-page {
display: flex;
min-height: 100vh;
@@ -314,12 +315,11 @@ const applyToJoin = () =>
z-index: 3;
}
.overview-surface {
@include adaptive.adaptive-g05-overview-surface;
z-index: 2;
width: 100%;
min-height: min(483px, calc(100vw * 1.3422));
flex: 0 0 auto;
background: url("/static/assets/modules/genealogy/opaque/g05-overview-surface.png")
center / 100% 100% no-repeat;
}
.overview-surface--state {
display: flex;
@@ -437,13 +437,12 @@ const applyToJoin = () =>
font-weight: 700;
}
.overview-state {
@include adaptive.adaptive-genealogy-state-panel;
display: flex;
width: 100%;
min-height: 0;
align-items: center;
justify-content: center;
background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")
center / 100% 100% no-repeat;
text-align: center;
}
.overview-state__content {
+5 -52
View File
@@ -5,15 +5,6 @@
<view class="search-page__header"><PageHeader title="加入家谱" /></view>
<view class="search-page__content">
<view class="search-title-strip">
<image
class="search-title-strip__seal"
src="/static/assets/foundation/transparent/brand-seal.png"
mode="aspectFit"
/>
<text class="search-title-strip__text">加入家谱</text>
</view>
<view class="mode-tabs">
<view
class="mode-tab"
@@ -439,6 +430,7 @@ const confirmInvite = () =>
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.search-page {
display: flex;
min-height: 100vh;
@@ -454,32 +446,6 @@ const confirmInvite = () =>
padding: 30rpx 24rpx calc(58rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.search-title-strip {
display: grid;
width: 100%;
height: 138rpx;
grid-template-columns: 156rpx 1fr 170rpx;
align-items: center;
background: url("/static/assets/modules/genealogy/opaque/g06-search-title-strip.png")
center / 100% 100% no-repeat;
}
.search-title-strip__seal {
z-index: 1;
width: 72rpx;
height: 84rpx;
margin-left: 52rpx;
}
.search-title-strip__text {
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
color: #f5d596;
font-family: "STKaiti", "KaiTi", serif;
font-size: 39rpx;
font-weight: 700;
letter-spacing: 5rpx;
}
.mode-tabs {
display: flex;
width: 500rpx;
@@ -516,11 +482,10 @@ const confirmInvite = () =>
margin-top: 32rpx;
}
.search-field {
@include adaptive.adaptive-g06-search-field;
display: grid;
width: 430rpx;
min-height: 95rpx;
background: url("/static/assets/modules/genealogy/opaque/g06-search-input-wide.png")
center / 100% 100% no-repeat;
}
.search-input {
z-index: 1;
@@ -547,13 +512,12 @@ const confirmInvite = () =>
font-size: 21rpx;
}
.search-action {
@include adaptive.adaptive-g06-search-action;
display: flex;
width: 200rpx;
min-height: 88rpx;
align-items: center;
justify-content: center;
background: url("/static/assets/modules/genealogy/opaque/g06-search-button.png")
center / 100% 100% no-repeat;
}
.search-action__label {
z-index: 1;
@@ -665,11 +629,10 @@ const confirmInvite = () =>
font-weight: 700;
}
.result-card {
@include adaptive.adaptive-genealogy-list-card;
min-height: 314rpx;
box-sizing: border-box;
padding: 28rpx 30rpx;
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png")
center / 100% 100% no-repeat;
}
.result-card__content {
z-index: 1;
@@ -748,11 +711,10 @@ const confirmInvite = () =>
line-height: 35rpx;
}
.invite-field {
@include adaptive.adaptive-g06-search-field;
display: grid;
flex: 1;
min-height: 95rpx;
background: url("/static/assets/modules/genealogy/opaque/g06-search-input-wide.png")
center / 100% 100% no-repeat;
}
.invite-input {
z-index: 1;
@@ -792,15 +754,6 @@ const confirmInvite = () =>
padding-right: 20rpx;
padding-left: 20rpx;
}
.search-title-strip__seal {
margin-left: 44rpx;
}
.search-title-strip {
grid-template-columns: 138rpx 1fr 148rpx;
}
.search-title-strip__text {
font-size: 37rpx;
}
.search-field {
width: 430rpx;
}
+5 -6
View File
@@ -40,7 +40,7 @@
<text>与家谱关系</text
><input
v-model="form.relation"
placeholder="例如:汤正华堂侄"
placeholder="例如:某某某堂侄"
placeholder-class="join-placeholder"
@input="clearFieldError('relation')"
/>
@@ -49,7 +49,7 @@
fieldErrors.relation
}}</text>
<text class="relation-help"
>请以家谱中一位已知长辈为参照例如汤正华堂侄</text
>请以家谱中一位已知长辈为参照例如某某某堂侄</text
>
<view class="join-field join-field--message">
<text>{{ sourceContract.thirdFieldLabel }}</text
@@ -224,6 +224,7 @@ const completeFlow = () =>
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.join-page {
display: flex;
min-height: 100vh;
@@ -234,14 +235,13 @@ const completeFlow = () =>
z-index: 3;
}
.join-panel {
@include adaptive.adaptive-genealogy-state-panel;
z-index: 2;
width: calc(100% - 32rpx);
min-height: min(590px, calc((100vw - 16px) * 1.337));
margin: 22rpx auto 0;
padding: 70rpx 8%;
box-sizing: border-box;
background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")
center / 100% 100% no-repeat;
}
.join-form {
min-width: 0;
@@ -271,11 +271,10 @@ const completeFlow = () =>
line-height: 1.6;
}
.join-field {
@include adaptive.adaptive-genealogy-form-field;
display: grid;
min-height: 92rpx;
margin-top: 18rpx;
background: url("/static/assets/modules/genealogy/transparent/g-form-field-frame.png")
center / 100% 100% no-repeat;
}
.join-form__previous {
display: block;
+10 -9
View File
@@ -218,6 +218,8 @@ const toSearch = () =>
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.application-page {
display: flex;
min-height: 100vh;
@@ -248,20 +250,19 @@ const toSearch = () =>
font-size: 24rpx;
}
.application-card {
@include adaptive.adaptive-genealogy-list-card;
display: grid;
width: 100%;
min-height: 228rpx;
margin-bottom: 18rpx;
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png")
center / 100% 100% no-repeat;
min-height: 210rpx;
margin-bottom: 12rpx;
}
.application-card__body {
display: grid;
min-width: 0;
min-height: 228rpx;
min-height: 174rpx;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12rpx 18rpx;
padding: 42rpx 52rpx 34rpx 64rpx;
gap: 8rpx 14rpx;
padding: 26rpx 28rpx 22rpx 40rpx;
box-sizing: border-box;
}
.application-card__name {
@@ -332,8 +333,10 @@ const toSearch = () =>
color: $brand-red;
font-size: 23rpx;
font-weight: 700;
white-space: nowrap;
}
.application-state-card {
@include adaptive.adaptive-genealogy-list-card;
display: flex;
width: 100%;
min-height: 228rpx;
@@ -342,8 +345,6 @@ const toSearch = () =>
margin-top: 80rpx;
padding: 48rpx 12%;
box-sizing: border-box;
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png")
center / 100% 100% no-repeat;
}
.application-state-card__copy {
display: flex;
+36 -75
View File
@@ -32,18 +32,17 @@
<text class="application-card__time">{{ item.appliedAt }}</text>
<text class="application-card__relation">{{ item.relation }}</text>
<view v-if="item.status === 'PENDING'" class="review-actions">
<view class="review-action" @click="confirmAudit(item, false)">
<image
src="/static/assets/foundation/transparent/a01-secondary-button-v2.png"
mode="aspectFit"
/><text>拒绝申请</text>
</view>
<view class="review-action" @click="confirmAudit(item, true)">
<image
src="/static/assets/foundation/transparent/a01-primary-button-v2.png"
mode="aspectFit"
/><text>通过申请</text>
</view>
<AppButton
compact
type="secondary"
label="拒绝申请"
@click="confirmAudit(item, false)"
/>
<AppButton
compact
label="通过申请"
@click="confirmAudit(item, true)"
/>
</view>
<view
v-else
@@ -54,14 +53,6 @@
: 'review-result--rejected'
"
>
<image
:src="
item.status === 'APPROVED'
? '/static/assets/foundation/transparent/a01-scroll-primary-v3.png'
: '/static/assets/foundation/transparent/a01-scroll-secondary-v3.png'
"
mode="aspectFit"
/>
<text class="application-card__status">{{
item.status === "APPROVED" ? "已通过" : "已拒绝"
}}</text>
@@ -81,16 +72,12 @@
<text>{{ stateCopy }}</text>
</view>
</view>
<view
<AppButton
v-if="reviewState === 'error'"
class="review-page__retry"
label="重新查看"
@click="loadApplications({ genealogyId })"
>
<image
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="aspectFit"
/><text>重新查看</text>
</view>
/>
</view>
<AppDialog
@@ -117,6 +104,7 @@
: '确认拒绝'
"
:show-cancel="!!confirmation"
compact-actions
@confirm="helpVisible ? closeDialog() : applyAudit()"
@cancel="closeDialog"
@close="closeDialog"
@@ -146,6 +134,7 @@
import { computed, ref } from "vue";
import { onLoad, onUnload } from "@dcloudio/uni-app";
import AppDialog from "@/components/AppDialog.vue";
import AppButton from "@/components/AppButton.vue";
import AppLoading from "@/components/AppLoading.vue";
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
@@ -271,6 +260,8 @@ const showHelp = () => {
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.review-page {
display: flex;
min-height: 100vh;
@@ -282,7 +273,7 @@ const showHelp = () => {
}
.rejection-field { width: 100%; margin: 20rpx 0; text-align: left; }
.rejection-field > text:first-child { display: block; color: $ink; font-size: 23rpx; font-weight: 700; }
.rejection-field textarea { width: 100%; min-height: 110rpx; margin-top: 10rpx; padding: 18rpx; box-sizing: border-box; color: $ink; font-size: 23rpx; line-height: 1.55; background: url("/static/assets/modules/genealogy/transparent/g-form-field-frame.png") center / 100% 100% no-repeat; }
.rejection-field textarea { @include adaptive.adaptive-genealogy-form-field; width: 100%; min-height: 110rpx; margin-top: 10rpx; padding: 18rpx; color: $ink; font-size: 23rpx; line-height: 1.55; }
.rejection-field__error { display: block; margin-top: 8rpx; color: $brand-red; font-size: 21rpx; }
.review-content {
z-index: 1;
@@ -308,19 +299,19 @@ const showHelp = () => {
font-size: 24rpx;
}
.application-card {
@include adaptive.adaptive-genealogy-list-card;
width: 100%;
min-height: 228rpx;
margin-bottom: 18rpx;
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png") center / 100% 100% no-repeat;
min-height: 218rpx;
margin-bottom: 12rpx;
}
.application-card__body {
display: grid;
width: 100%;
min-height: 228rpx;
min-height: 182rpx;
grid-template-columns: auto minmax(0, 1fr) auto;
grid-template-rows: auto auto auto;
gap: 12rpx 0;
padding: 44rpx 52rpx 29rpx 64rpx;
gap: 8rpx 0;
padding: 28rpx 28rpx 22rpx 40rpx;
box-sizing: border-box;
}
.application-card__name {
@@ -347,7 +338,7 @@ const showHelp = () => {
display: block;
grid-column: 1 / span 2;
grid-row: 2;
margin-top: 15rpx;
margin-top: 8rpx;
color: $ink-muted;
font-size: 24rpx;
line-height: 1.35;
@@ -359,33 +350,12 @@ const showHelp = () => {
justify-self: end;
gap: 12rpx;
}
.review-action {
display: grid;
.review-actions .app-button {
width: 148rpx;
min-height: 68rpx;
}
.review-action image {
grid-area: 1 / 1;
width: 100%;
height: 100%;
}
.review-action text {
z-index: 1;
display: flex;
grid-area: 1 / 1;
align-items: center;
justify-content: center;
height: 100%;
color: $ink;
font-family: "Microsoft YaHei", sans-serif;
font-size: 24rpx;
font-weight: 700;
letter-spacing: 1rpx;
}
.review-action:last-child text {
color: #fff9ed;
}
.review-result {
@include adaptive.adaptive-scroll-button(primary);
display: grid;
grid-column: 2 / 4;
grid-row: 3;
@@ -393,11 +363,6 @@ const showHelp = () => {
width: 308rpx;
min-height: 68rpx;
}
.review-result image {
grid-area: 1 / 1;
width: 100%;
height: 100%;
}
.application-card__status {
z-index: 1;
display: flex;
@@ -412,14 +377,20 @@ const showHelp = () => {
.review-result--approved .application-card__status {
color: #fff9ed;
}
.review-result--approved {
border-image-source: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png");
}
.review-result--rejected .application-card__status {
color: $ink;
}
.review-result--rejected {
border-image-source: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png");
}
.review-state-card {
@include adaptive.adaptive-genealogy-list-card;
width: 100%;
min-height: 228rpx;
margin-top: 70rpx;
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png") center / 100% 100% no-repeat;
}
.review-state-card__copy {
display: flex;
@@ -446,18 +417,9 @@ const showHelp = () => {
width: 420rpx;
min-height: 82rpx;
margin: 32rpx auto 0;
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / contain no-repeat;
}
.review-page__retry text {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #fff9ed;
font-size: 26rpx;
font-weight: 700;
}
.review-feedback {
@include adaptive.adaptive-feedback-toast;
position: fixed;
z-index: 40;
top: 140rpx;
@@ -468,7 +430,6 @@ const showHelp = () => {
align-items: center;
justify-content: center;
padding: 0 36rpx;
background: url("/static/assets/foundation/transparent/a01-scroll-toast-v3.png") center / 100% 100% no-repeat;
transform: translateX(-50%);
}
.review-feedback text {
+3 -4
View File
@@ -209,6 +209,7 @@ const saveSettings = () => {
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.settings-page {
display: flex;
min-height: 100vh;
@@ -219,14 +220,13 @@ const saveSettings = () => {
z-index: 3;
}
.settings-panel {
@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;
background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")
center / 100% 100% no-repeat;
}
.settings-panel > .app-loading {
grid-area: 1 / 1 / -1 / -1;
@@ -259,11 +259,10 @@ const saveSettings = () => {
line-height: 1.55;
}
.settings-field {
@include adaptive.adaptive-genealogy-form-field;
display: grid;
min-height: 92rpx;
margin-top: 17rpx;
background: url("/static/assets/modules/genealogy/transparent/g-form-field-frame.png")
center / 100% 100% no-repeat;
}
.settings-field > text {
z-index: 1;
+4 -6
View File
@@ -237,6 +237,7 @@ const savePoems = () => {
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.poem-page {
display: flex;
min-height: 100vh;
@@ -247,14 +248,13 @@ const savePoems = () => {
z-index: 3;
}
.poem-panel {
@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;
background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")
center / 100% 100% no-repeat;
}
.poem-panel > .app-loading {
grid-area: 1 / 1 / -1 / -1;
@@ -289,13 +289,12 @@ const savePoems = () => {
margin-top: 18rpx;
}
.poem-row {
@include adaptive.adaptive-genealogy-form-field;
display: grid;
min-height: 72rpx;
margin-top: 10rpx;
grid-template-columns: 48% auto 1fr auto;
align-items: center;
background: url("/static/assets/modules/genealogy/transparent/g-form-field-frame.png")
center / 100% 100% no-repeat;
}
.poem-row__number {
z-index: 1;
@@ -347,11 +346,10 @@ const savePoems = () => {
letter-spacing: 2rpx;
}
.poem-field {
@include adaptive.adaptive-genealogy-form-field;
display: grid;
min-height: 118rpx;
margin-top: 22rpx;
background: url("/static/assets/modules/genealogy/transparent/g-form-field-frame.png")
center / 100% 100% no-repeat;
}
.poem-field > text {
z-index: 1;
+3 -1
View File
@@ -165,6 +165,8 @@ onUnmounted(() => {
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.notice-page {
display: flex;
min-height: 100vh;
@@ -185,9 +187,9 @@ onUnmounted(() => {
gap: 18rpx;
}
.notice-card {
@include adaptive.adaptive-notification-content;
width: 100%;
min-height: 220rpx;
background: url("/static/assets/modules/notification/transparent/n01-notice-card.png") center / 100% 100% no-repeat;
}
.notice-card__copy {
display: flex;
+3 -1
View File
@@ -93,10 +93,12 @@ onUnmounted(() => toastTimer && clearTimeout(toastTimer));
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.notice-detail-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.page-layer { z-index: 1; }
.notice-content { flex: 1; padding: 28rpx 30rpx 72rpx; }
.paper-panel { background: url("/static/assets/modules/notification/transparent/n01-notice-card.png") center / 100% 100% no-repeat; }
.paper-panel { @include adaptive.adaptive-notification-content; }
.notice-card { min-height: 430rpx; padding: 54rpx 52rpx; box-sizing: border-box; }
.notice-meta { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8rpx 20rpx; color: $ink-muted; font-size: 22rpx; }
.notice-meta .is-unread { color: $brand-red; font-weight: 700; }
+4 -3
View File
@@ -177,6 +177,8 @@ const openItem = (item) => uni.navigateTo({ url: item.url });
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.profile-page {
display: flex;
min-height: 100vh;
@@ -192,11 +194,10 @@ const openItem = (item) => uni.navigateTo({ url: item.url });
padding: 26rpx 30rpx 190rpx;
}
.profile-hero {
@include adaptive.adaptive-profile-summary;
display: grid;
width: 100%;
min-height: 252rpx;
box-sizing: border-box;
background: url("/static/assets/modules/profile/transparent/m01-profile-summary-card.png") center / 100% 100% no-repeat;
}
.profile-hero__content {
z-index: 1;
@@ -284,10 +285,10 @@ const openItem = (item) => uni.navigateTo({ url: item.url });
letter-spacing: 2rpx;
}
.profile-scroll-notice {
@include adaptive.adaptive-scroll-button(primary);
width: 88%;
min-height: 112rpx;
margin: 22rpx auto 0;
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / 100% 100% no-repeat;
}
.profile-scroll-notice__copy {
display: flex;
+3 -1
View File
@@ -55,10 +55,12 @@ onUnmounted(() => clearTimeout(timer));
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.profile-edit-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.page-layer { z-index: 1; }
.page-content { flex: 1; padding: 26rpx 30rpx 70rpx; }
.profile-avatar-card, .form-panel { background: var(--module-field-asset, url("/static/assets/modules/profile/transparent/module-field-frame.png")) center / 100% 100% no-repeat; }
.profile-avatar-card, .form-panel { @include adaptive.adaptive-profile-field; }
.profile-avatar-card { display: grid; grid-template-columns: 92rpx minmax(0,1fr) auto; align-items: center; gap: 20rpx; min-height: 150rpx; padding: 24rpx 34rpx; box-sizing: border-box; }
.avatar-seal { width: 82rpx; height: auto; max-height: 92rpx; aspect-ratio: 82 / 92; }
.avatar-copy { min-width: 0; }
+3 -1
View File
@@ -43,5 +43,7 @@ onUnmounted(() => clearTimeout(timer));
</script>
<style scoped lang="scss">
.security-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.security-summary{min-height:190rpx;padding:42rpx 48rpx;box-sizing:border-box;background:url("/static/assets/modules/profile/transparent/m01-profile-summary-card.png") center/100% 100% no-repeat;text-align:center}.security-summary text{display:block;overflow-wrap:anywhere}.security-summary text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:34rpx;font-weight:700}.security-summary text:last-child{margin-top:12rpx;color:$ink-muted;font-size:23rpx;line-height:1.55}.security-list{margin-top:22rpx}.security-row{display:grid;grid-template-columns:56rpx minmax(0,1fr) 34rpx;min-height:110rpx;align-items:center;gap:18rpx;padding:16rpx 26rpx;box-sizing:border-box;background:url("/static/assets/modules/profile/transparent/module-field-frame.png") center/100% 100% no-repeat}.security-row+ .security-row{margin-top:12rpx}.security-row>image{width:50rpx;height:50rpx}.security-row .chevron{width:30rpx;height:30rpx}.security-row view{min-width:0}.security-row text{display:block;overflow-wrap:anywhere}.security-row text:first-child{color:$ink;font-size:25rpx;font-weight:700}.security-row text:last-child{margin-top:6rpx;color:$ink-muted;font-size:21rpx}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}}
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.security-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.security-summary{@include adaptive.adaptive-profile-summary;min-height:190rpx;padding:42rpx 48rpx;text-align:center}.security-summary text{display:block;overflow-wrap:anywhere}.security-summary text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:34rpx;font-weight:700}.security-summary text:last-child{margin-top:12rpx;color:$ink-muted;font-size:23rpx;line-height:1.55}.security-list{margin-top:22rpx}.security-row{@include adaptive.adaptive-profile-field;display:grid;grid-template-columns:56rpx minmax(0,1fr) 34rpx;min-height:110rpx;align-items:center;gap:18rpx;padding:16rpx 26rpx}.security-row+ .security-row{margin-top:12rpx}.security-row>image{width:50rpx;height:50rpx}.security-row .chevron{width:30rpx;height:30rpx}.security-row view{min-width:0}.security-row text{display:block;overflow-wrap:anywhere}.security-row text:first-child{color:$ink;font-size:25rpx;font-weight:700}.security-row text:last-child{margin-top:6rpx;color:$ink-muted;font-size:21rpx}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}}
</style>
+2 -1
View File
@@ -55,5 +55,6 @@ onUnmounted(() => clearTimeout(timer));
</script>
<style scoped lang="scss">
.password-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.security-tip,.form-panel{background:url("/static/assets/modules/profile/transparent/module-content-frame.png") center/100% 100% no-repeat}.security-tip{min-height:170rpx;padding:38rpx 44rpx;box-sizing:border-box;text-align:center}.security-tip text{display:block}.security-tip text:first-child{color:$ink;font-size:32rpx;font-weight:700}.security-tip text:last-child{margin-top:10rpx;color:$ink-muted;font-size:22rpx;line-height:1.55}.form-panel{margin-top:20rpx;padding:22rpx 34rpx 30rpx;box-sizing:border-box}.field-block+.field-block{margin-top:6rpx}.form-row{display:grid;grid-template-columns:142rpx minmax(0,1fr) 74rpx;min-height:92rpx;align-items:center;gap:12rpx;border-bottom:1px solid rgba(181,137,63,.42)}.form-row>text{color:$ink;font-size:23rpx;font-weight:700}.form-row input{width:auto;min-width:0;min-height:68rpx;color:$ink;font-size:23rpx}.password-toggle{display:flex;min-height:68rpx;align-items:center;justify-content:flex-end;color:$brand-red;font-size:21rpx}.field-error{display:block;padding-top:7rpx;color:#b42318;font-size:20rpx;line-height:1.4;text-align:right}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.form-panel{padding-right:26rpx;padding-left:26rpx}.form-row{grid-template-columns:120rpx minmax(0,1fr) 64rpx;gap:8rpx}}
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.password-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.security-tip,.form-panel{@include adaptive.adaptive-profile-content}.security-tip{min-height:170rpx;padding:38rpx 44rpx;text-align:center}.security-tip text{display:block}.security-tip text:first-child{color:$ink;font-size:32rpx;font-weight:700}.security-tip text:last-child{margin-top:10rpx;color:$ink-muted;font-size:22rpx;line-height:1.55}.form-panel{margin-top:20rpx;padding:22rpx 34rpx 30rpx}.field-block+.field-block{margin-top:6rpx}.form-row{display:grid;grid-template-columns:142rpx minmax(0,1fr) 74rpx;min-height:92rpx;align-items:center;gap:12rpx;border-bottom:1px solid rgba(181,137,63,.42)}.form-row>text{color:$ink;font-size:23rpx;font-weight:700}.form-row input{width:auto;min-width:0;min-height:68rpx;color:$ink;font-size:23rpx}.password-toggle{display:flex;min-height:68rpx;align-items:center;justify-content:flex-end;color:$brand-red;font-size:21rpx}.field-error{display:block;padding-top:7rpx;color:#b42318;font-size:20rpx;line-height:1.4;text-align:right}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.form-panel{padding-right:26rpx;padding-left:26rpx}.form-row{grid-template-columns:120rpx minmax(0,1fr) 64rpx;gap:8rpx}}
</style>
+2 -1
View File
@@ -51,5 +51,6 @@ onUnmounted(() => { clearInterval(countdownTimer); clearTimeout(stateTimer); cle
</script>
<style scoped lang="scss">
.phone-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.current-phone,.form-panel{background:url("/static/assets/modules/profile/transparent/module-content-frame.png") center/100% 100% no-repeat}.current-phone{min-height:210rpx;padding:40rpx 48rpx;box-sizing:border-box;text-align:center}.current-phone text{display:block}.current-phone text:first-child{color:$ink-muted;font-size:22rpx}.current-phone text:nth-child(2){margin-top:8rpx;color:$ink;font-size:38rpx;font-weight:700;letter-spacing:3rpx}.current-phone text:last-child{margin-top:12rpx;color:$ink-muted;font-size:21rpx;line-height:1.5}.form-panel{margin-top:22rpx;padding:24rpx 34rpx 32rpx;box-sizing:border-box}.form-row{display:grid;grid-template-columns:130rpx minmax(0,1fr);min-height:94rpx;align-items:center;gap:16rpx;border-bottom:1px solid rgba(181,137,63,.42)}.code-row{grid-template-columns:130rpx minmax(0,1fr) auto}.form-row>text{color:$ink;font-size:23rpx;font-weight:700}.form-row input{width:auto;min-width:0;min-height:70rpx;color:$ink;font-size:23rpx}.code-action{display:flex;min-width:126rpx;min-height:70rpx;align-items:center;justify-content:flex-end;color:$brand-red;font-size:21rpx}.field-error{display:block;padding-top:7rpx;color:#b42318;font-size:20rpx;text-align:right}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.form-panel{padding-right:26rpx;padding-left:26rpx}.form-row{grid-template-columns:112rpx minmax(0,1fr)}.code-row{grid-template-columns:112rpx minmax(0,1fr);}.code-action{grid-column:2;justify-content:flex-start}}
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.phone-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.current-phone,.form-panel{@include adaptive.adaptive-profile-content}.current-phone{min-height:210rpx;padding:40rpx 48rpx;text-align:center}.current-phone text{display:block}.current-phone text:first-child{color:$ink-muted;font-size:22rpx}.current-phone text:nth-child(2){margin-top:8rpx;color:$ink;font-size:38rpx;font-weight:700;letter-spacing:3rpx}.current-phone text:last-child{margin-top:12rpx;color:$ink-muted;font-size:21rpx;line-height:1.5}.form-panel{margin-top:22rpx;padding:24rpx 34rpx 32rpx}.form-row{display:grid;grid-template-columns:130rpx minmax(0,1fr);min-height:94rpx;align-items:center;gap:16rpx;border-bottom:1px solid rgba(181,137,63,.42)}.code-row{grid-template-columns:130rpx minmax(0,1fr) auto}.form-row>text{color:$ink;font-size:23rpx;font-weight:700}.form-row input{width:auto;min-width:0;min-height:70rpx;color:$ink;font-size:23rpx}.code-action{display:flex;min-width:126rpx;min-height:70rpx;align-items:center;justify-content:flex-end;color:$brand-red;font-size:21rpx}.field-error{display:block;padding-top:7rpx;color:#b42318;font-size:20rpx;text-align:right}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.form-panel{padding-right:26rpx;padding-left:26rpx}.form-row{grid-template-columns:112rpx minmax(0,1fr)}.code-row{grid-template-columns:112rpx minmax(0,1fr);}.code-action{grid-column:2;justify-content:flex-start}}
</style>
+2 -1
View File
@@ -44,5 +44,6 @@ const contactSupport = () => uni.navigateTo({ url: "/pages/profile/m07-feedback"
</script>
<style scoped lang="scss">
.help-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:24rpx 28rpx 72rpx}.search-box{display:grid;grid-template-columns:minmax(0,1fr) auto;min-height:86rpx;align-items:center;gap:16rpx;padding:0 30rpx;background:url("/static/assets/modules/profile/transparent/module-field-frame.png") center/100% 100% no-repeat}.search-box input{width:auto;min-width:0;min-height:68rpx;color:$ink;font-size:23rpx}.search-box text{color:$ink-muted;font-size:20rpx}.category-scroll{width:100%;margin-top:18rpx}.category-row{display:flex;width:max-content;gap:12rpx;padding:2rpx}.category-chip{display:flex;min-width:100rpx;min-height:64rpx;align-items:center;justify-content:center;padding:0 22rpx;box-sizing:border-box;color:$ink-muted;font-size:22rpx}.category-chip.active{background:url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png") center/100% 100% no-repeat;color:$brand-red;font-weight:700}.question-list{display:flex;flex-direction:column;gap:14rpx;margin-top:18rpx}.question-card,.empty-card{background:url("/static/assets/modules/profile/transparent/module-content-frame.png") center/100% 100% no-repeat}.question-card{min-height:104rpx;padding:24rpx 34rpx;box-sizing:border-box}.question-heading{display:grid;grid-template-columns:minmax(0,1fr) auto;min-height:58rpx;align-items:center;gap:18rpx}.question-heading text:first-child{color:$ink;font-size:24rpx;font-weight:700;overflow-wrap:anywhere}.question-heading text:last-child{color:$brand-red;font-size:20rpx}.answer{display:block;padding:14rpx 4rpx 6rpx;border-top:1px solid rgba(181,137,63,.32);color:$ink-muted;font-size:22rpx;line-height:1.65;overflow-wrap:anywhere}.empty-card{min-height:220rpx;padding:58rpx 44rpx;box-sizing:border-box;text-align:center}.empty-card text{display:block}.empty-card text:first-child{color:$ink;font-size:29rpx;font-weight:700}.empty-card text:last-child{margin-top:12rpx;color:$ink-muted;font-size:22rpx;line-height:1.5}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:20rpx;padding-left:20rpx}}
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.help-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:24rpx 28rpx 72rpx}.search-box{@include adaptive.adaptive-profile-field;display:grid;grid-template-columns:minmax(0,1fr) auto;min-height:86rpx;align-items:center;gap:16rpx;padding:0 30rpx}.search-box input{width:auto;min-width:0;min-height:68rpx;color:$ink;font-size:23rpx}.search-box text{color:$ink-muted;font-size:20rpx}.category-scroll{width:100%;margin-top:18rpx}.category-row{display:flex;width:max-content;gap:12rpx;padding:2rpx}.category-chip{display:flex;min-width:100rpx;min-height:64rpx;align-items:center;justify-content:center;padding:0 22rpx;box-sizing:border-box;color:$ink-muted;font-size:22rpx}.category-chip.active{@include adaptive.adaptive-scroll-button(secondary);color:$brand-red;font-weight:700}.question-list{display:flex;flex-direction:column;gap:14rpx;margin-top:18rpx}.question-card,.empty-card{@include adaptive.adaptive-profile-content}.question-card{min-height:104rpx;padding:24rpx 34rpx}.question-heading{display:grid;grid-template-columns:minmax(0,1fr) auto;min-height:58rpx;align-items:center;gap:18rpx}.question-heading text:first-child{color:$ink;font-size:24rpx;font-weight:700;overflow-wrap:anywhere}.question-heading text:last-child{color:$brand-red;font-size:20rpx}.answer{display:block;padding:14rpx 4rpx 6rpx;border-top:1px solid rgba(181,137,63,.32);color:$ink-muted;font-size:22rpx;line-height:1.65;overflow-wrap:anywhere}.empty-card{min-height:220rpx;padding:58rpx 44rpx;text-align:center}.empty-card text{display:block}.empty-card text:first-child{color:$ink;font-size:29rpx;font-weight:700}.empty-card text:last-child{margin-top:12rpx;color:$ink-muted;font-size:22rpx;line-height:1.5}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:20rpx;padding-left:20rpx}}
</style>
+2 -1
View File
@@ -48,5 +48,6 @@ onUnmounted(() => clearTimeout(timer));
</script>
<style scoped lang="scss">
.feedback-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:26rpx 30rpx 72rpx}.lead{display:block;color:$ink-muted;font-size:23rpx;line-height:1.5;text-align:center}.type-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14rpx;margin-top:20rpx}.type-chip{display:flex;min-height:76rpx;align-items:center;justify-content:center;padding:10rpx 16rpx;box-sizing:border-box;background:url("/static/assets/modules/profile/transparent/module-field-frame.png") center/100% 100% no-repeat;color:$ink-muted;font-size:22rpx;text-align:center}.type-chip.active{color:$brand-red;font-weight:700;filter:saturate(1.2)}.form-panel{margin-top:20rpx;padding:30rpx 34rpx;background:url("/static/assets/modules/profile/transparent/module-content-frame.png") center/100% 100% no-repeat}.form-panel textarea{display:block;width:auto;min-width:0;min-height:210rpx;color:$ink;font-size:24rpx;line-height:1.65}.counter{display:block;color:$ink-muted;font-size:20rpx;text-align:right}.contact-row{display:grid;grid-template-columns:130rpx minmax(0,1fr);min-height:88rpx;align-items:center;gap:16rpx;margin-top:16rpx;border-top:1px solid rgba(181,137,63,.38)}.contact-row text{color:$ink;font-size:23rpx;font-weight:700}.contact-row input{width:auto;min-width:0;min-height:68rpx;color:$ink;font-size:22rpx;text-align:right}.field-error{display:block;margin-top:7rpx;color:#b42318;font-size:20rpx;line-height:1.4;text-align:right}.privacy-note{display:block;margin-top:15rpx;color:$ink-muted;font-size:20rpx;line-height:1.5;text-align:center}.page-content>.app-button{margin-top:24rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.form-panel{padding-right:26rpx;padding-left:26rpx}}
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.feedback-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:26rpx 30rpx 72rpx}.lead{display:block;color:$ink-muted;font-size:23rpx;line-height:1.5;text-align:center}.type-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14rpx;margin-top:20rpx}.type-chip{@include adaptive.adaptive-profile-field;display:flex;min-height:76rpx;align-items:center;justify-content:center;padding:10rpx 16rpx;color:$ink-muted;font-size:22rpx;text-align:center}.type-chip.active{color:$brand-red;font-weight:700;filter:saturate(1.2)}.form-panel{@include adaptive.adaptive-profile-content;margin-top:20rpx;padding:30rpx 34rpx}.form-panel textarea{display:block;width:auto;min-width:0;min-height:210rpx;color:$ink;font-size:24rpx;line-height:1.65}.counter{display:block;color:$ink-muted;font-size:20rpx;text-align:right}.contact-row{display:grid;grid-template-columns:130rpx minmax(0,1fr);min-height:88rpx;align-items:center;gap:16rpx;margin-top:16rpx;border-top:1px solid rgba(181,137,63,.38)}.contact-row text{color:$ink;font-size:23rpx;font-weight:700}.contact-row input{width:auto;min-width:0;min-height:68rpx;color:$ink;font-size:22rpx;text-align:right}.field-error{display:block;margin-top:7rpx;color:#b42318;font-size:20rpx;line-height:1.4;text-align:right}.privacy-note{display:block;margin-top:15rpx;color:$ink-muted;font-size:20rpx;line-height:1.5;text-align:center}.page-content>.app-button{margin-top:24rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.form-panel{padding-right:26rpx;padding-left:26rpx}}
</style>
+2 -1
View File
@@ -42,5 +42,6 @@ onUnmounted(() => clearTimeout(timer));
</script>
<style scoped lang="scss">
.promotion-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.invite-hero,.invite-code-card,.steps-card{background:url("/static/assets/modules/profile/transparent/module-content-frame.png") center/100% 100% no-repeat}.invite-hero{display:flex;min-height:300rpx;flex-direction:column;align-items:center;justify-content:center;padding:38rpx 48rpx;box-sizing:border-box;text-align:center}.invite-hero image{width:90rpx;height:auto;max-height:102rpx;aspect-ratio:90/102}.invite-hero text{display:block}.invite-hero text:nth-child(2){margin-top:12rpx;color:$ink;font-family:STKaiti,KaiTi,serif;font-size:34rpx;font-weight:700}.invite-hero text:last-child{margin-top:12rpx;color:$ink-muted;font-size:22rpx;line-height:1.55}.invite-code-card{min-height:220rpx;margin-top:20rpx;padding:38rpx 44rpx;box-sizing:border-box;text-align:center}.invite-code-card>text:first-child{display:block;color:$ink-muted;font-size:21rpx}.invite-code{display:block;margin-top:10rpx;color:$brand-red;font-size:42rpx;font-weight:700;letter-spacing:5rpx;overflow-wrap:anywhere}.copy-action{display:flex;min-height:62rpx;align-items:center;justify-content:center;color:$brand-red;font-size:22rpx}.steps-card{display:flex;min-height:260rpx;flex-direction:column;gap:10rpx;margin-top:20rpx;padding:34rpx 44rpx;box-sizing:border-box;color:$ink-muted;font-size:22rpx}.steps-card text:first-child{margin-bottom:4rpx;color:$ink;font-size:27rpx;font-weight:700}.page-content>.app-button{margin-top:26rpx}.poster-seal{width:96rpx;height:auto;max-height:110rpx;aspect-ratio:96/110;margin:18rpx auto 24rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.invite-code{font-size:35rpx;letter-spacing:3rpx}}
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.promotion-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.invite-hero,.invite-code-card,.steps-card{@include adaptive.adaptive-profile-content}.invite-hero{display:flex;min-height:300rpx;flex-direction:column;align-items:center;justify-content:center;padding:38rpx 48rpx;text-align:center}.invite-hero image{width:90rpx;height:auto;max-height:102rpx;aspect-ratio:90/102}.invite-hero text{display:block}.invite-hero text:nth-child(2){margin-top:12rpx;color:$ink;font-family:STKaiti,KaiTi,serif;font-size:34rpx;font-weight:700}.invite-hero text:last-child{margin-top:12rpx;color:$ink-muted;font-size:22rpx;line-height:1.55}.invite-code-card{min-height:220rpx;margin-top:20rpx;padding:38rpx 44rpx;text-align:center}.invite-code-card>text:first-child{display:block;color:$ink-muted;font-size:21rpx}.invite-code{display:block;margin-top:10rpx;color:$brand-red;font-size:42rpx;font-weight:700;letter-spacing:5rpx;overflow-wrap:anywhere}.copy-action{display:flex;min-height:62rpx;align-items:center;justify-content:center;color:$brand-red;font-size:22rpx}.steps-card{display:flex;min-height:260rpx;flex-direction:column;gap:10rpx;margin-top:20rpx;padding:34rpx 44rpx;color:$ink-muted;font-size:22rpx}.steps-card text:first-child{margin-bottom:4rpx;color:$ink;font-size:27rpx;font-weight:700}.page-content>.app-button{margin-top:26rpx}.poster-seal{width:96rpx;height:auto;max-height:110rpx;aspect-ratio:96/110;margin:18rpx auto 24rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.invite-code{font-size:35rpx;letter-spacing:3rpx}}
</style>
+2 -1
View File
@@ -35,5 +35,6 @@ const openServiceNotice = () => { serviceNoticeVisible.value = true; };
</script>
<style scoped lang="scss">
.orders-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.service-card,.benefit-card,.order-card,.empty-card{background:url("/static/assets/modules/profile/transparent/module-content-frame.png") center/100% 100% no-repeat}.service-card{min-height:220rpx;padding:42rpx 48rpx;box-sizing:border-box;text-align:center}.service-card text{display:block}.service-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:34rpx;font-weight:700}.service-card text:nth-child(2){margin-top:10rpx;color:$brand-red;font-size:23rpx;font-weight:700}.service-card text:last-child{margin-top:12rpx;color:$ink-muted;font-size:21rpx;line-height:1.55}.benefit-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12rpx;margin-top:18rpx}.benefit-card{min-height:150rpx;padding:28rpx 18rpx;box-sizing:border-box;text-align:center}.benefit-card text{display:block;overflow-wrap:anywhere}.benefit-card text:first-child{color:$ink;font-size:23rpx;font-weight:700}.benefit-card text:last-child{margin-top:8rpx;color:$ink-muted;font-size:19rpx;line-height:1.45}.section-heading{display:flex;flex-wrap:wrap;justify-content:space-between;gap:8rpx 20rpx;margin:28rpx 6rpx 14rpx}.section-heading text:first-child{color:$ink;font-size:27rpx;font-weight:700}.section-heading text:last-child{color:$ink-muted;font-size:21rpx}.order-list{display:flex;flex-direction:column;gap:14rpx}.order-card{display:grid;grid-template-columns:minmax(0,1fr) auto;min-height:130rpx;align-items:center;gap:20rpx;padding:26rpx 34rpx;box-sizing:border-box}.order-card text{display:block;overflow-wrap:anywhere}.order-card view:first-child text:first-child{color:$ink;font-size:23rpx;font-weight:700}.order-card text:last-child{margin-top:6rpx;color:$ink-muted;font-size:20rpx}.order-card view:last-child{text-align:right}.order-card view:last-child text:first-child{color:$brand-red;font-size:23rpx;font-weight:700}.empty-card{min-height:190rpx;padding:48rpx 42rpx;box-sizing:border-box;text-align:center}.empty-card text{display:block}.empty-card text:first-child{color:$ink;font-size:28rpx;font-weight:700}.empty-card text:last-child{margin-top:12rpx;color:$ink-muted;font-size:21rpx;line-height:1.55}.page-content>.app-button{margin-top:26rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.benefit-grid{grid-template-columns:1fr}.benefit-card{min-height:100rpx}.order-card{grid-template-columns:1fr}.order-card view:last-child{text-align:left}}
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.orders-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.service-card,.benefit-card,.order-card,.empty-card{@include adaptive.adaptive-profile-content}.service-card{min-height:220rpx;padding:42rpx 48rpx;text-align:center}.service-card text{display:block}.service-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:34rpx;font-weight:700}.service-card text:nth-child(2){margin-top:10rpx;color:$brand-red;font-size:23rpx;font-weight:700}.service-card text:last-child{margin-top:12rpx;color:$ink-muted;font-size:21rpx;line-height:1.55}.benefit-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12rpx;margin-top:18rpx}.benefit-card{min-height:150rpx;padding:28rpx 18rpx;text-align:center}.benefit-card text{display:block;overflow-wrap:anywhere}.benefit-card text:first-child{color:$ink;font-size:23rpx;font-weight:700}.benefit-card text:last-child{margin-top:8rpx;color:$ink-muted;font-size:19rpx;line-height:1.45}.section-heading{display:flex;flex-wrap:wrap;justify-content:space-between;gap:8rpx 20rpx;margin:28rpx 6rpx 14rpx}.section-heading text:first-child{color:$ink;font-size:27rpx;font-weight:700}.section-heading text:last-child{color:$ink-muted;font-size:21rpx}.order-list{display:flex;flex-direction:column;gap:14rpx;margin-top:18rpx}.order-card{display:grid;grid-template-columns:minmax(0,1fr) auto;min-height:130rpx;align-items:center;gap:20rpx;padding:26rpx 34rpx}.order-card text{display:block;overflow-wrap:anywhere}.order-card view:first-child text:first-child{color:$ink;font-size:23rpx;font-weight:700}.order-card text:last-child{margin-top:6rpx;color:$ink-muted;font-size:20rpx}.order-card view:last-child{text-align:right}.order-card view:last-child text:first-child{color:$brand-red;font-size:23rpx;font-weight:700}.empty-card{min-height:190rpx;padding:48rpx 42rpx;text-align:center}.empty-card text{display:block}.empty-card text:first-child{color:$ink;font-size:28rpx;font-weight:700}.empty-card text:last-child{margin-top:12rpx;color:$ink-muted;font-size:21rpx;line-height:1.55}.page-content>.app-button{margin-top:26rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.benefit-grid{grid-template-columns:1fr}.benefit-card{min-height:100rpx}.order-card{grid-template-columns:1fr}.order-card view:last-child{text-align:left}}
</style>
+2 -1
View File
@@ -38,5 +38,6 @@ onUnmounted(() => clearTimeout(timer));
</script>
<style scoped lang="scss">
.about-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.brand-card{display:flex;min-height:300rpx;flex-direction:column;align-items:center;justify-content:center;padding:36rpx 44rpx;box-sizing:border-box;background:url("/static/assets/modules/profile/transparent/m01-profile-summary-card.png") center/100% 100% no-repeat;text-align:center}.brand-card image{width:92rpx;height:106rpx}.brand-card text{display:block}.brand-card text:nth-child(2){margin-top:6rpx;color:$ink;font-family:STKaiti,KaiTi,serif;font-size:40rpx;font-weight:700;letter-spacing:4rpx}.brand-card text:nth-child(3){margin-top:8rpx;color:$ink-muted;font-size:22rpx;line-height:1.5}.brand-card text:last-child{margin-top:10rpx;color:$ink-muted;font-size:20rpx}.settings-list{display:flex;flex-direction:column;gap:12rpx;margin-top:22rpx}.settings-row{display:grid;grid-template-columns:minmax(0,1fr) 32rpx;min-height:104rpx;align-items:center;gap:18rpx;padding:18rpx 30rpx;box-sizing:border-box;background:url("/static/assets/modules/profile/transparent/module-field-frame.png") center/100% 100% no-repeat}.settings-row view{min-width:0}.settings-row text{display:block;overflow-wrap:anywhere}.settings-row text:first-child{color:$ink;font-size:24rpx;font-weight:700}.settings-row text:last-child{margin-top:6rpx;color:$ink-muted;font-size:20rpx}.settings-row image{width:30rpx;height:30rpx}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}}
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.about-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.brand-card{@include adaptive.adaptive-profile-summary;display:flex;min-height:300rpx;flex-direction:column;align-items:center;justify-content:center;padding:36rpx 44rpx;text-align:center}.brand-card image{width:92rpx;height:106rpx}.brand-card text{display:block}.brand-card text:nth-child(2){margin-top:6rpx;color:$ink;font-family:STKaiti,KaiTi,serif;font-size:40rpx;font-weight:700;letter-spacing:4rpx}.brand-card text:nth-child(3){margin-top:8rpx;color:$ink-muted;font-size:22rpx;line-height:1.5}.brand-card text:last-child{margin-top:10rpx;color:$ink-muted;font-size:20rpx}.settings-list{display:flex;flex-direction:column;gap:12rpx;margin-top:22rpx}.settings-row{@include adaptive.adaptive-profile-field;display:grid;grid-template-columns:minmax(0,1fr) 32rpx;min-height:104rpx;align-items:center;gap:18rpx;padding:18rpx 30rpx}.settings-row view{min-width:0}.settings-row text{display:block;overflow-wrap:anywhere}.settings-row text:first-child{color:$ink;font-size:24rpx;font-weight:700}.settings-row text:last-child{margin-top:6rpx;color:$ink-muted;font-size:20rpx}.settings-row image{width:30rpx;height:30rpx}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}}
</style>
+4 -3
View File
@@ -163,6 +163,8 @@ onUnmounted(() => {
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.people-page {
display: flex;
min-height: 100vh;
@@ -178,12 +180,12 @@ onUnmounted(() => {
padding: 22rpx 24rpx 100rpx;
}
.people-search {
@include adaptive.adaptive-records-field;
display: grid;
grid-template-columns: minmax(0, 1fr) 126rpx;
width: 100%;
min-height: 44px;
align-items: center;
background: url("/static/assets/modules/records/transparent/r01-search-input-frame.png") center / 100% 100% no-repeat;
}
.people-search input {
z-index: 1;
@@ -214,14 +216,13 @@ onUnmounted(() => {
margin-top: 12px;
}
.person-card {
@include adaptive.adaptive-records-person;
display: flex;
width: 100%;
height: clamp(92px, 190rpx, 108px);
align-items: center;
margin-top: 12px;
padding: 16% 10%;
box-sizing: border-box;
background: url("/static/assets/modules/records/transparent/r01-person-name-card.png") center / 100% 100% no-repeat;
}
.person-card:first-child {
margin-top: 0;
+5 -3
View File
@@ -141,24 +141,26 @@ onUnmounted(() => { if (toastTimer) clearTimeout(toastTimer); });
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.person-detail-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.person-detail-header,.person-detail-loading,.person-detail-content { z-index: 1; }
.person-detail-loading { min-height: calc(100vh - 100rpx); }
.person-detail-content { padding: 18rpx 24rpx 72rpx; }
.person-identity-card { display: flex; min-height: 190rpx; padding: 30rpx 10%; box-sizing: border-box; background: url("/static/assets/modules/records/transparent/r01-person-name-card.png") center / 100% 100% no-repeat; }
.person-identity-card { @include adaptive.adaptive-records-person; display: flex; min-height: 190rpx; padding: 30rpx 10%; }
.person-identity-card__copy { display: flex; flex: 1; flex-direction: column; justify-content: center; }
.person-identity-card__copy text { display: block; }
.person-identity-card__name { color: $ink; font-family: STKaiti,KaiTi,serif; font-size: 38rpx; font-weight: 700; }
.person-identity-card__meta { margin-top: 8rpx; color: $ink-muted; font-size: 25rpx; font-weight: 600; }
.person-identity-card__hint { margin-top: 8rpx; color: #806a51; font-size: 21rpx; }
.person-archive-card { min-height: 154rpx; margin-top: 14rpx; padding: 32rpx 42rpx; box-sizing: border-box; }
.person-archive-card,.person-long-field,.person-state-card { background: url("/static/assets/modules/records/transparent/module-content-frame.png") center / 100% 100% no-repeat; }
.person-archive-card,.person-long-field,.person-state-card { @include adaptive.adaptive-records-content; }
.person-archive-card text { display: block; }
.person-archive-card text:first-child,.person-long-field__label { color: $brand-red; font-size: 24rpx; font-weight: 700; }
.person-archive-card text:last-child { margin-top: 11rpx; color: $ink; font-size: 24rpx; line-height: 1.55; }
.person-edit-action { margin-top: 20rpx; }
.person-related-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14rpx; margin-top: 18rpx; }
.person-field { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 8rpx 24rpx; min-height: 92rpx; margin-top: 12rpx; padding: 18rpx 28rpx; box-sizing: border-box; background: url("/static/assets/modules/records/transparent/module-field-frame.png") center / 100% 100% no-repeat; }
.person-field { @include adaptive.adaptive-records-field; display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 8rpx 24rpx; min-height: 92rpx; margin-top: 12rpx; padding: 18rpx 28rpx; }
.person-field__label { color: $ink; font-size: 23rpx; font-weight: 700; }
.person-field input { width: 100%; min-width: 0; min-height: 56rpx; color: $ink; font-size: 23rpx; text-align: right; }
.person-field-error { grid-column: 1 / -1; display: block; color: $brand-red; font-size: 21rpx; text-align: right; }
+3 -2
View File
@@ -114,6 +114,8 @@ const restoreGifts = () => {
};
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.gift-page {
display: flex;
min-height: 100vh;
@@ -137,8 +139,7 @@ const restoreGifts = () => {
.record-card,
.state-card {
box-sizing: border-box;
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
center/100% 100% no-repeat;
@include adaptive.adaptive-records-content;
}
.record-card {
min-height: 190rpx;
+4 -4
View File
@@ -168,6 +168,8 @@ onUnmounted(() => {
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.gift-editor-page {
display: flex;
min-height: 100vh;
@@ -190,8 +192,7 @@ onUnmounted(() => {
.state-card {
box-sizing: border-box;
padding: 46rpx;
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
center/100% 100% no-repeat;
@include adaptive.adaptive-records-content;
}
.form-card > text:first-child,
.detail-card > text:first-child,
@@ -211,8 +212,7 @@ onUnmounted(() => {
margin-top: 14rpx;
padding: 14rpx 24rpx;
box-sizing: border-box;
background: url("/static/assets/modules/records/transparent/module-field-frame.png")
center/100% 100% no-repeat;
@include adaptive.adaptive-records-field;
}
.field-row > text:first-child,
.detail-card > view > text:first-child {
+3 -2
View File
@@ -104,6 +104,8 @@ const restoreRituals = () => {
};
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.ritual-page {
display: flex;
min-height: 100vh;
@@ -127,8 +129,7 @@ const restoreRituals = () => {
.record-card,
.state-card {
box-sizing: border-box;
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
center/100% 100% no-repeat;
@include adaptive.adaptive-records-content;
}
.record-card {
min-height: 190rpx;
+3 -2
View File
@@ -126,6 +126,8 @@ const backToRituals = () =>
uni.redirectTo({ url: "/pages/records/r05-ritual-list" });
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.ritual-detail-page {
display: flex;
min-height: 100vh;
@@ -151,8 +153,7 @@ const backToRituals = () =>
.state-card {
box-sizing: border-box;
padding: 42rpx 46rpx;
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
center/100% 100% no-repeat;
@include adaptive.adaptive-records-content;
}
.detail-card > text {
display: block;
+4 -4
View File
@@ -139,6 +139,8 @@ onUnmounted(() => {
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.ritual-editor-page {
display: flex;
min-height: 100vh;
@@ -156,8 +158,7 @@ onUnmounted(() => {
.state-card {
box-sizing: border-box;
padding: 46rpx;
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
center/100% 100% no-repeat;
@include adaptive.adaptive-records-content;
}
.form-card > text:first-child,
.state-card > text:first-child {
@@ -175,8 +176,7 @@ onUnmounted(() => {
margin-top: 14rpx;
padding: 14rpx 24rpx;
box-sizing: border-box;
background: url("/static/assets/modules/records/transparent/module-field-frame.png")
center/100% 100% no-repeat;
@include adaptive.adaptive-records-field;
}
.field-row > text:first-child {
color: $ink;
+5 -5
View File
@@ -140,6 +140,8 @@ onUnmounted(() => {
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.timeline-page {
display: flex;
min-height: 100vh;
@@ -171,7 +173,7 @@ onUnmounted(() => {
color: $ink-muted;
font-size: 22rpx;
background: url("/static/assets/modules/genealogy/transparent/section-divider.png")
center/100% 100% no-repeat;
center/100% auto no-repeat;
}
.person-lead text:first-child {
color: $brand-red;
@@ -181,8 +183,7 @@ onUnmounted(() => {
.state-card {
box-sizing: border-box;
padding: 34rpx 46rpx;
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
center/100% 100% no-repeat;
@include adaptive.adaptive-records-content;
}
.timeline-card > text,
.state-card > text {
@@ -241,8 +242,7 @@ onUnmounted(() => {
box-sizing: border-box;
color: $ink;
font-size: 23rpx;
background: url("/static/assets/modules/records/transparent/module-field-frame.png")
center/100% 100% no-repeat;
@include adaptive.adaptive-records-field;
}
.dialog-form text {
display: block;
+5 -5
View File
@@ -147,6 +147,8 @@ onUnmounted(() => {
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.timeline-page {
display: flex;
min-height: 100vh;
@@ -178,7 +180,7 @@ onUnmounted(() => {
color: $ink-muted;
font-size: 22rpx;
background: url("/static/assets/modules/genealogy/transparent/section-divider.png")
center/100% 100% no-repeat;
center/100% auto no-repeat;
}
.person-lead text:first-child {
color: $brand-red;
@@ -188,8 +190,7 @@ onUnmounted(() => {
.state-card {
box-sizing: border-box;
padding: 34rpx 46rpx;
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
center/100% 100% no-repeat;
@include adaptive.adaptive-records-content;
}
.timeline-card > text,
.state-card > text {
@@ -248,8 +249,7 @@ onUnmounted(() => {
box-sizing: border-box;
color: $ink;
font-size: 22rpx;
background: url("/static/assets/modules/records/transparent/module-field-frame.png")
center/100% 100% no-repeat;
@include adaptive.adaptive-records-field;
}
.dialog-form text {
display: block;
+4 -4
View File
@@ -143,6 +143,8 @@ onUnmounted(() => {
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.memo-page {
display: flex;
min-height: 100vh;
@@ -167,8 +169,7 @@ onUnmounted(() => {
.state-card {
box-sizing: border-box;
padding: 34rpx 46rpx;
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
center/100% 100% no-repeat;
@include adaptive.adaptive-records-content;
}
.memo-card > view {
display: flex;
@@ -234,8 +235,7 @@ onUnmounted(() => {
box-sizing: border-box;
color: $ink;
font-size: 23rpx;
background: url("/static/assets/modules/records/transparent/module-field-frame.png")
center/100% 100% no-repeat;
@include adaptive.adaptive-records-field;
}
.dialog-form text {
display: block;
+4 -4
View File
@@ -149,6 +149,8 @@ onUnmounted(() => {
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.merit-page {
display: flex;
min-height: 100vh;
@@ -174,8 +176,7 @@ onUnmounted(() => {
.state-card {
box-sizing: border-box;
padding: 34rpx 46rpx;
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
center/100% 100% no-repeat;
@include adaptive.adaptive-records-content;
}
.merit-summary {
text-align: center;
@@ -254,8 +255,7 @@ onUnmounted(() => {
box-sizing: border-box;
color: $ink;
font-size: 22rpx;
background: url("/static/assets/modules/records/transparent/module-field-frame.png")
center/100% 100% no-repeat;
@include adaptive.adaptive-records-field;
}
.dialog-form text {
display: block;
+7 -5
View File
@@ -134,12 +134,14 @@ const toTree = () => uni.navigateBack();
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.member-page { display: flex; min-height: 100vh; flex-direction: column; padding-bottom: 28rpx; box-sizing: border-box; background: $paper; }
.member-page__header, .member-context, .member-panel { z-index: 2; }
.member-context { width: calc(100% - 32rpx); margin: 18rpx auto 0; padding: 16rpx 24rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
.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 { width: calc(100% - 32rpx); min-height: min(620px, calc((100vw - 16px) * 1.42)); margin: 16rpx auto 0; padding: 9% 8%; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t01-state-panel.png") center / 100% 100% no-repeat; }
.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 > .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; }
@@ -147,18 +149,18 @@ const toTree = () => uni.navigateBack();
.member-heading > view:last-child text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
.member-heading > view:last-child text:last-child { font-size: 23rpx; }
.member-heading > view:last-child text:first-child { color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 36rpx; font-weight: 700; }
.member-status-entry { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 14rpx; margin-top: 18rpx; padding: 18rpx 22rpx; background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png") center / 100% 100% no-repeat; }
.member-status-entry { @include adaptive.adaptive-genealogy-list-card; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 14rpx; margin-top: 18rpx; padding: 18rpx 22rpx; }
.member-status-entry text { color: $ink-muted; font-size: 22rpx; line-height: 1.4; }
.member-status-entry text:first-child, .member-status-entry text:last-child { color: $brand-red; font-weight: 700; }
.member-section-title { display: block; margin-top: 24rpx; color: $brand-red; font-size: 24rpx; font-weight: 700; }
.member-info-row { display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 64rpx; align-items: center; gap: 18rpx; margin-top: 8rpx; padding: 8rpx 18rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
.member-info-row { @include adaptive.adaptive-tree-field; display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 64rpx; align-items: center; gap: 18rpx; margin-top: 8rpx; padding: 8rpx 18rpx; }
.member-info-row text { color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
.member-info-row text:last-child { color: $ink; text-align: right; }
.member-relatives { margin-top: 8rpx; font-size: 23rpx; }
.member-relatives > view { display: flex; justify-content: space-between; gap: 20rpx; padding: 12rpx 18rpx; color: $ink; font-size: 23rpx; }
.member-relatives > view text:last-child { color: $brand-red; }
.member-relatives > text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.5; }
.member-profile-action { display: flex; min-height: 76rpx; align-items: center; justify-content: center; margin-top: 22rpx; background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / 100% 100% no-repeat; }
.member-profile-action { @include adaptive.adaptive-scroll-button(primary); display: flex; min-height: 76rpx; align-items: center; justify-content: center; margin-top: 22rpx; }
.member-profile-action text { color: #fff9ed; font-size: 24rpx; font-weight: 700; }
.member-error { margin-top: 30%; text-align: center; }
.member-error > text { display: block; color: $ink-muted; font-size: 24rpx; line-height: 1.55; }
+4 -2
View File
@@ -220,13 +220,15 @@ const discardAndBack = () => {
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.add-relative-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.add-relative-page__header { z-index: 3; }
.add-relative-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%; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t01-state-panel.png") center / 100% 100% no-repeat; }
.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%; }
.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; }
.member-context, .form-field { display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 78rpx; align-items: center; gap: 20rpx; margin-top: 14rpx; padding: 12rpx 22rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
.member-context, .form-field { @include adaptive.adaptive-tree-field; display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 78rpx; align-items: center; gap: 20rpx; margin-top: 14rpx; padding: 12rpx 22rpx; }
.member-context text:first-child, .form-field > text:first-child { color: $ink; font-size: 24rpx; font-weight: 700; }
.member-context text:last-child, .form-field > text:last-child, .form-field input, .form-field textarea { min-width: 0; color: $ink; font-size: 24rpx; line-height: 1.45; text-align: right; }
.form-field textarea { width: auto; min-height: 54rpx; text-align: left; }
+4 -2
View File
@@ -169,13 +169,15 @@ const discardAndBack = () => {
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.edit-member-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.edit-member-page__header { z-index: 3; }
.edit-member-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%; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t01-state-panel.png") center / 100% 100% no-repeat; }
.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%; }
.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; }
.member-context, .form-field { display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 78rpx; align-items: center; gap: 20rpx; margin-top: 14rpx; padding: 12rpx 22rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
.member-context, .form-field { @include adaptive.adaptive-tree-field; display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 78rpx; align-items: center; gap: 20rpx; margin-top: 14rpx; padding: 12rpx 22rpx; }
.member-context text:first-child, .form-field > text:first-child { color: $ink; font-size: 24rpx; font-weight: 700; }
.member-context text:last-child, .form-field > text:last-child, .form-field input, .form-field textarea { min-width: 0; color: $ink; font-size: 24rpx; line-height: 1.45; text-align: right; }
.form-field textarea { width: auto; min-height: 54rpx; text-align: left; }
+5 -3
View File
@@ -188,17 +188,19 @@ const handleResultAction = () => {
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.relationship-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.relationship-page__header { z-index: 3; }
.relationship-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%; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t01-state-panel.png") center / 100% 100% no-repeat; }
.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%; }
.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; }
.form-field { display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 78rpx; align-items: center; gap: 20rpx; margin-top: 14rpx; padding: 12rpx 22rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
.form-field { @include adaptive.adaptive-tree-field; display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 78rpx; align-items: center; gap: 20rpx; margin-top: 14rpx; padding: 12rpx 22rpx; }
.form-field text:first-child { color: $ink; font-size: 24rpx; font-weight: 700; }
.form-field text:last-child { min-width: 0; color: $ink; font-size: 24rpx; line-height: 1.45; text-align: right; }
.field-error { display: block; margin: 5rpx 18rpx 0; color: $brand-red; font-size: 22rpx; line-height: 32rpx; }
.relationship-preview { margin-top: 18rpx; padding: 20rpx 22rpx; background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png") center / 100% 100% no-repeat; }
.relationship-preview { @include adaptive.adaptive-genealogy-list-card; margin-top: 18rpx; padding: 20rpx 22rpx; }
.relationship-preview text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.5; }
.relationship-preview text:first-child { color: $brand-red; font-weight: 700; }
.form-note { text-align: center; }
+5 -8
View File
@@ -152,6 +152,8 @@ const openMember = (item) =>
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.directory-page {
display: flex;
min-height: 100vh;
@@ -184,12 +186,11 @@ const openMember = (item) =>
font-weight: 500;
}
.directory-search {
@include adaptive.adaptive-tree-field;
display: grid;
width: calc(100% - 48rpx);
min-height: 44px;
margin: 20rpx auto 0;
background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png")
center / 100% 100% no-repeat;
}
.directory-search input {
z-index: 1;
@@ -237,15 +238,13 @@ const openMember = (item) =>
font-size: 23rpx;
}
.directory-card {
@include adaptive.adaptive-genealogy-list-card;
display: flex;
width: 100%;
min-height: 196rpx;
align-items: center;
box-sizing: border-box;
margin-bottom: 16rpx;
padding: 18rpx 28rpx;
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png")
center / 100% 100% no-repeat;
}
.directory-card__copy {
z-index: 1;
@@ -280,6 +279,7 @@ const openMember = (item) =>
line-height: 30rpx;
}
.directory-state-card {
@include adaptive.adaptive-genealogy-list-card;
display: flex;
width: 100%;
min-height: 220rpx;
@@ -287,9 +287,6 @@ const openMember = (item) =>
justify-content: center;
margin-bottom: 16rpx;
padding: 48rpx 12%;
box-sizing: border-box;
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png")
center / 100% 100% no-repeat;
}
.directory-state-card {
margin-top: 70rpx;
+5 -3
View File
@@ -114,17 +114,19 @@ const handleAction = () => {
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.member-status-page { display: flex; min-height: 100vh; flex-direction: column; padding-bottom: 34rpx; box-sizing: border-box; background: $paper; }
.member-status-page__header, .member-status-context, .status-card, .status-guidance, .status-action { z-index: 2; }
.member-status-context { width: calc(100% - 32rpx); margin: 18rpx auto 0; padding: 16rpx 24rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
.member-status-context { @include adaptive.adaptive-tree-field; width: calc(100% - 32rpx); margin: 18rpx auto 0; padding: 16rpx 24rpx; }
.member-status-context text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
.member-status-context text:first-child { color: $ink; font-size: 26rpx; font-weight: 700; }
.status-card { width: calc(100% - 32rpx); min-height: 330rpx; margin: 18rpx auto 0; padding: 12% 10%; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t01-state-panel.png") center / 100% 100% no-repeat; }
.status-card { @include adaptive.adaptive-tree-panel; width: calc(100% - 32rpx); min-height: 330rpx; margin: 18rpx auto 0; padding: 12% 10%; }
.status-card__copy text { display: block; color: $ink-muted; font-size: 24rpx; line-height: 1.55; text-align: center; }
.status-card__copy text:first-child { color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
.status-card__copy text:nth-child(2) { margin-top: 12rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 32rpx; font-weight: 700; }
.status-card__copy text:last-child { margin-top: 14rpx; font-size: 24rpx; }
.status-guidance { width: calc(100% - 48rpx); margin: 16rpx auto 0; padding: 24rpx 28rpx; box-sizing: border-box; background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png") center / 100% 100% no-repeat; }
.status-guidance { @include adaptive.adaptive-genealogy-list-card; width: calc(100% - 48rpx); margin: 16rpx auto 0; padding: 24rpx 28rpx; }
.status-guidance text { display: block; color: $ink-muted; font-size: 24rpx; line-height: 1.6; }
.status-guidance text:first-child { margin-bottom: 8rpx; color: $ink; font-size: 26rpx; font-weight: 700; }
.status-action { display: grid; width: calc(100% - 72rpx); min-height: 76rpx; margin: 22rpx auto 0; }
+265
View File
@@ -0,0 +1,265 @@
@mixin adaptive-auth-dialog {
box-sizing: border-box;
border-width: 48rpx 34rpx 72rpx;
border-style: solid;
border-color: transparent;
border-image-source: url("/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png");
border-image-slice: 260 240 360 240 fill;
border-image-width: 48rpx 34rpx 72rpx;
border-image-repeat: stretch;
}
@mixin adaptive-scroll-button($type) {
box-sizing: border-box;
border-width: 10rpx 26rpx;
border-style: solid;
border-color: transparent;
border-image-slice: 56 300 fill;
border-image-width: 10rpx 26rpx;
border-image-repeat: stretch;
@if $type == secondary {
border-image-source: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png");
} @else {
border-image-source: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png");
}
}
@mixin adaptive-module-content {
box-sizing: border-box;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image-source: var(--module-content-asset);
border-image-slice: 105 150;
border-image-width: 18rpx 20rpx;
border-image-repeat: stretch;
}
@mixin adaptive-module-field {
box-sizing: border-box;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image-source: var(--module-field-asset);
border-image-slice: 70 100 fill;
border-image-width: 12rpx 16rpx;
border-image-repeat: stretch;
}
@mixin adaptive-feedback-toast {
box-sizing: border-box;
border-width: 16rpx 74rpx;
border-style: solid;
border-color: transparent;
border-image-source: url("/static/assets/foundation/transparent/a01-scroll-toast-v3.png");
border-image-slice: 58 280 fill;
border-image-width: 16rpx 74rpx;
border-image-repeat: stretch;
}
@mixin adaptive-genealogy-current-slip {
box-sizing: border-box;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image-source: url("/static/assets/modules/genealogy/transparent/current-slip-frame.png");
border-image-slice: 24 24 60 24 fill;
border-image-width: 12rpx 12rpx 30rpx;
border-image-repeat: stretch;
}
@mixin adaptive-genealogy-list-card {
box-sizing: border-box;
border-width: 18rpx 24rpx;
border-style: solid;
border-color: transparent;
border-image-source: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png");
border-image-slice: 24 32;
border-image-width: 18rpx 24rpx;
border-image-repeat: stretch;
}
@mixin adaptive-genealogy-state-panel {
box-sizing: border-box;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image-source: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png");
border-image-slice: 84 70 360 70 fill;
border-image-width: 42rpx 28rpx 180rpx;
border-image-repeat: stretch;
}
@mixin adaptive-genealogy-form-field {
box-sizing: border-box;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image-source: url("/static/assets/modules/genealogy/transparent/g-form-field-frame.png");
border-image-slice: 70 100 fill;
border-image-width: 12rpx 16rpx;
border-image-repeat: stretch;
}
@mixin adaptive-g05-overview-surface {
@include adaptive-asset-frame(
"/static/assets/modules/genealogy/opaque/g05-overview-surface.png",
120 100 fill,
42rpx 36rpx
);
}
@mixin adaptive-g06-search-field {
@include adaptive-asset-frame(
"/static/assets/modules/genealogy/opaque/g06-search-input-wide.png",
70 100 fill,
12rpx 16rpx
);
}
@mixin adaptive-g06-search-action {
@include adaptive-asset-frame(
"/static/assets/modules/genealogy/opaque/g06-search-button.png",
36 64 fill,
12rpx 20rpx
);
}
@mixin adaptive-g01-add-sheet {
box-sizing: border-box;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image-source: url("/static/assets/modules/genealogy/transparent/g01-add-sheet-background-v3.png");
border-image-slice: 220 0 1 0 fill;
border-image-width: 118rpx 0 1rpx;
border-image-repeat: stretch;
}
@mixin adaptive-g01-switcher {
box-sizing: border-box;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image-source: url("/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png");
border-image-slice: 300 260 360 260 fill;
border-image-width: 110rpx 48rpx 132rpx 48rpx;
border-image-repeat: stretch;
}
@mixin adaptive-asset-frame($source, $slice, $width, $layout-border: 1px) {
box-sizing: border-box;
border-width: $layout-border;
border-style: solid;
border-color: transparent;
border-image-source: url($source);
border-image-slice: $slice;
border-image-width: $width;
border-image-repeat: stretch;
}
@mixin adaptive-family-content {
@include adaptive-asset-frame(
"/static/assets/modules/family/transparent/module-content-frame.png",
105 150 fill,
18rpx 20rpx
);
}
@mixin adaptive-family-panel {
@include adaptive-asset-frame(
"/static/assets/modules/family/transparent/module-content-frame.png",
72 fill,
64rpx,
20rpx
);
}
@mixin adaptive-family-field {
@include adaptive-asset-frame(
"/static/assets/modules/family/transparent/module-field-frame.png",
70 100 fill,
12rpx 16rpx
);
}
@mixin adaptive-family-letter {
@include adaptive-asset-frame(
"/static/assets/modules/family/transparent/f01-family-letter-card.png",
105 150 fill,
18rpx 20rpx
);
}
@mixin adaptive-notification-content {
@include adaptive-asset-frame(
"/static/assets/modules/notification/transparent/n01-notice-card.png",
105 150 fill,
18rpx 20rpx
);
}
@mixin adaptive-profile-content {
@include adaptive-asset-frame(
"/static/assets/modules/profile/transparent/module-content-frame.png",
105 150 fill,
18rpx 20rpx
);
}
@mixin adaptive-profile-field {
@include adaptive-asset-frame(
"/static/assets/modules/profile/transparent/module-field-frame.png",
70 100 fill,
12rpx 16rpx
);
}
@mixin adaptive-profile-summary {
@include adaptive-asset-frame(
"/static/assets/modules/profile/transparent/m01-profile-summary-card.png",
105 150 fill,
18rpx 20rpx
);
}
@mixin adaptive-records-content {
@include adaptive-asset-frame(
"/static/assets/modules/records/transparent/module-content-frame.png",
105 150 fill,
18rpx 20rpx
);
}
@mixin adaptive-records-field {
@include adaptive-asset-frame(
"/static/assets/modules/records/transparent/module-field-frame.png",
70 100 fill,
12rpx 16rpx
);
}
@mixin adaptive-records-person {
@include adaptive-asset-frame(
"/static/assets/modules/records/transparent/r01-person-name-card.png",
105 150 fill,
18rpx 20rpx
);
}
@mixin adaptive-tree-panel {
@include adaptive-asset-frame(
"/static/assets/modules/tree/transparent/t01-state-panel.png",
105 150 fill,
18rpx 20rpx
);
}
@mixin adaptive-tree-field {
@include adaptive-asset-frame(
"/static/assets/modules/tree/transparent/t07-search-input-frame.png",
70 100 fill,
12rpx 16rpx
);
}
+10
View File
@@ -1,9 +1,19 @@
page {
--app-viewport-height: 100vh;
--app-safe-top: env(safe-area-inset-top, 0px);
--app-safe-bottom: env(safe-area-inset-bottom, 0px);
--app-touch-min: 44px;
background: $paper;
color: $ink;
font-family: "STKaiti", "KaiTi", serif
}
@supports (height: 100dvh) {
page {
--app-viewport-height: 100dvh;
}
}
.page-shell {
min-height: 100vh;
box-sizing: border-box;
+58 -13
View File
@@ -31,9 +31,11 @@ function Assert-NotContains {
$root = Split-Path -Parent $PSScriptRoot
$entryPath = Join-Path $root 'pages/auth/a01-entry.vue'
$shellPath = Join-Path $root 'components/AuthPageShell.vue'
$legacyLoginPath = Join-Path $root 'pages/auth/a02-login.vue'
$pagesPath = Join-Path $root 'pages.json'
$globalStylesPath = Join-Path $root 'styles/global.scss'
$profilePath = Join-Path $root 'styles/adaptive-frame-profiles.scss'
if (-not (Test-Path -LiteralPath $entryPath)) {
throw 'Missing the single A01 login page.'
@@ -54,7 +56,53 @@ if ($pages.pages.Count -ne 52) {
}
$entry = Get-Content -LiteralPath $entryPath -Raw -Encoding utf8
$shell = Get-Content -LiteralPath $shellPath -Raw -Encoding utf8
$globalStyles = Get-Content -LiteralPath $globalStylesPath -Raw -Encoding utf8
$profiles = Get-Content -LiteralPath $profilePath -Raw -Encoding utf8
# A01 owns content in normal document flow. Decorative artwork must not become
# a second full-page coordinate system.
foreach ($required in @(
'class="auth-shell__header"',
'class="auth-shell__header-image"',
'a01-vnext-header-v1.png',
'mode="widthFix"',
'class="auth-shell__paper"',
'auth-page-paper.jpg'
)) {
Assert-Contains -Content $shell -Expected $required -Message "A01 adaptive shell is missing: $required"
}
Assert-Contains -Content $entry -Expected '<AuthPageShell' -Message 'A01 must consume the shared adaptive shell.'
foreach ($forbidden in @(
'class="page-canvas"',
'class="page-backdrop"',
'mode="scaleToFill"',
'--auth-paper-start',
'1665rpx'
)) {
Assert-NotContains -Content $entry -Unexpected $forbidden -Message "A01 still uses rejected page coordinates: $forbidden"
Assert-NotContains -Content $shell -Unexpected $forbidden -Message "The adaptive shell uses rejected page coordinates: $forbidden"
}
Assert-Contains -Content $shell -Expected 'a01-red-hall-ink-backdrop-v1.png' -Message 'The shared auth shell must preserve the approved paper scenery as decoration.'
$pageRule = [regex]::Match($shell, '(?ms)^\.auth-shell\s*\{(?<Body>.*?)^\}')
$headerRule = [regex]::Match($shell, '(?ms)^\.auth-shell__header\s*\{(?<Body>.*?)^\}')
$paperRule = [regex]::Match($shell, '(?ms)^\.auth-shell__paper\s*\{(?<Body>.*?)^\}')
$contentRule = [regex]::Match($entry, '(?ms)^\.login-content\s*\{(?<Body>.*?)^\}')
foreach ($rule in @($pageRule, $headerRule, $paperRule, $contentRule)) {
if (-not $rule.Success) { throw 'A01 adaptive layout rule is missing.' }
}
Assert-Contains -Content $pageRule.Groups['Body'].Value -Expected 'display: flex;' -Message 'A01 root must use flex flow.'
Assert-Contains -Content $pageRule.Groups['Body'].Value -Expected 'flex-direction: column;' -Message 'A01 root must stack header and paper.'
Assert-Contains -Content $pageRule.Groups['Body'].Value -Expected 'min-height: var(--app-viewport-height);' -Message 'A01 root must fill without locking content height.'
if ($pageRule.Groups['Body'].Value -match '(?m)^\s*height:\s*var\(--app-viewport-height\);') {
throw 'A01 root must not lock content to the viewport.'
}
Assert-Contains -Content $headerRule.Groups['Body'].Value -Expected 'height: calc(var(--app-safe-top) + min(36.5vw, 175px));' -Message 'A01 header must use the approved compact visible slot without distorting its image.'
Assert-Contains -Content $headerRule.Groups['Body'].Value -Expected 'overflow: hidden;' -Message 'A01 header must crop only the excess lower doorway inside its decorative slot.'
Assert-Contains -Content $paperRule.Groups['Body'].Value -Expected 'flex: 1;' -Message 'A01 paper must receive remaining viewport space.'
Assert-Contains -Content $contentRule.Groups['Body'].Value -Expected 'justify-content: space-between;' -Message 'A01 must distribute spare height in normal flow.'
Assert-NotContains -Content $contentRule.Groups['Body'].Value -Unexpected 'grid-area:' -Message 'A01 interactive content must not overlap page artwork.'
# Typography has one owner, and the title decoration participates in normal flow.
Assert-Contains -Content $globalStyles -Expected 'font-family: "STKaiti", "KaiTi", serif' -Message 'Global styles must own the STKaiti/KaiTi font contract.'
@@ -80,23 +128,22 @@ if (-not $dividerRule.Success) {
foreach ($positioning in @('position: absolute', 'left: 50%', 'translateX(')) {
Assert-NotContains -Content $dividerRule.Groups['Body'].Value -Unexpected $positioning -Message "A01 title divider must not use manual positioning: $positioning"
}
Assert-Contains -Content $dividerRule.Groups['Body'].Value -Expected 'width: 360rpx;' -Message 'A01 title divider must be wide enough to remain visible on paper.'
Assert-Contains -Content $dividerRule.Groups['Body'].Value -Expected 'height: 60rpx;' -Message 'A01 title divider must retain a legible knot and line weight.'
Assert-Contains -Content $dividerRule.Groups['Body'].Value -Expected 'width: 280rpx;' -Message 'A01 compact title divider must remain visible on paper.'
Assert-Contains -Content $dividerRule.Groups['Body'].Value -Expected 'height: 46rpx;' -Message 'A01 compact title divider must retain a legible knot and line weight.'
Assert-Contains -Content $dividerRule.Groups['Body'].Value -Expected 'filter: brightness(0.68) saturate(1.5) contrast(1.2);' -Message 'A01 title divider must keep sufficient contrast against the paper background.'
$brandSealRule = [regex]::Match($entry, '(?ms)^\.brand-seal\s*\{(?<Body>.*?)^\}')
$brandSealRule = [regex]::Match($shell, '(?ms)^\.auth-shell__seal\s*\{(?<Body>.*?)^\}')
if (-not $brandSealRule.Success) {
throw 'A01 is missing the brand seal style rule.'
}
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'width: 184rpx;' -Message 'A01 brand seal must use the approved smaller width.'
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'height: 221rpx;' -Message 'A01 brand seal must preserve its aspect ratio at the approved smaller height.'
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'margin-top: 64rpx;' -Message 'A01 brand seal must keep its approved vertical spacing in document flow.'
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'justify-self: center;' -Message 'A01 brand seal must remain horizontally centered by grid alignment.'
Assert-NotContains -Content $brandSealRule.Groups['Body'].Value -Unexpected 'position:' -Message 'A01 brand seal must not use positioning for ordinary layout.'
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'position: absolute;' -Message 'A01 brand seal must be scoped as decoration inside the header.'
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'left: 50%;' -Message 'A01 brand seal must remain centered inside the header.'
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'transform: translateX(-50%);' -Message 'A01 brand seal must use header-local centering.'
# Final decorative surfaces must come from real bitmap assets.
foreach ($asset in @(
'a01-red-hall-ink-backdrop-v1.png',
'a01-vnext-header-v1.png',
'auth-page-paper.jpg',
'brand-seal.png',
'a01-vnext-divider-v1.png',
'a01-scroll-primary-v3.png',
@@ -112,7 +159,7 @@ foreach ($asset in @(
'a02-agreement-unchecked.png',
'a02-agreement-checked.png'
)) {
Assert-Contains -Content $entry -Expected $asset -Message "A01 is missing asset reference: $asset"
Assert-Contains -Content ($entry + $shell + $profiles) -Expected $asset -Message "A01 is missing asset reference: $asset"
}
foreach ($obsoleteVisualAsset in @(
'auth-header.png',
@@ -199,7 +246,5 @@ $feedbackToastRule = [regex]::Match($entry, '(?ms)^\.feedback-toast\s*\{(?<Body>
if (-not $feedbackToastRule.Success) {
throw 'A01 is missing the custom feedback Toast style rule.'
}
Assert-Contains -Content $feedbackToastRule.Groups['Body'].Value -Expected '/static/assets/foundation/transparent/a01-scroll-toast-v3.png' -Message 'A01 feedback Toast must use the approved v3 nine-slice asset.'
Assert-Contains -Content $feedbackToastRule.Groups['Body'].Value -Expected 'border-image-slice:' -Message 'A01 feedback Toast must declare fixed decorative slices.'
Assert-Contains -Content $feedbackToastRule.Groups['Body'].Value -Expected 'fill' -Message 'A01 feedback Toast nine-slice must fill the paper center.'
Assert-Contains -Content $feedbackToastRule.Groups['Body'].Value -Expected '@include adaptive.adaptive-feedback-toast;' -Message 'A01 feedback Toast must consume the approved shared nine-slice profile.'
Write-Output 'A01-LOGIN-MERGE-CONTRACT PASS'
+53 -15
View File
@@ -3,9 +3,11 @@ const assert = require('assert')
const url = 'http://localhost:5173/#/pages/auth/a01-entry'
const sizes = [
{ width: 320, height: 568 },
{ width: 360, height: 616 },
{ width: 360, height: 640 },
{ width: 360, height: 800 },
{ width: 412, height: 915 }
{ width: 412, height: 915 },
{ width: 480, height: 1040 }
]
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))
@@ -83,20 +85,44 @@ const run = async () => {
await send('Page.reload')
await waitForFreshDocument(send, previousTimeOrigin)
await waitFor(send, "document.querySelectorAll('.login-tab').length === 2", `A01 did not render at ${size.width}x${size.height}`)
if (size.width === 320 && size.height === 568) {
await waitFor(send, "document.querySelector('.auth-page')?.scrollHeight > document.querySelector('.auth-page')?.clientHeight", 'A01 did not establish natural scrolling at 320x568')
}
const metrics = await valueOf(send, `({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
scrollWidth: document.documentElement.scrollWidth,
scrollHeight: document.documentElement.scrollHeight,
pageClientHeight: document.querySelector('.auth-page')?.clientHeight,
pageScrollHeight: document.querySelector('.auth-page')?.scrollHeight
})`)
await waitFor(send, "document.querySelector('.auth-shell__header-image img')?.naturalWidth === 824", `A01 header did not load at ${size.width}x${size.height}`)
const metrics = await valueOf(send, `(() => {
const root = document.querySelector('.auth-page')
const header = document.querySelector('.auth-shell__header')
const headerImage = document.querySelector('.auth-shell__header-image img')
const paper = document.querySelector('.auth-shell__paper')
const content = document.querySelector('.login-content')
const headerRect = header?.getBoundingClientRect()
const paperRect = paper?.getBoundingClientRect()
const contentRect = content?.getBoundingClientRect()
return {
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
scrollWidth: document.documentElement.scrollWidth,
documentScrollHeight: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight),
rootScrollHeight: root?.scrollHeight,
headerBottom: headerRect?.bottom,
headerImageWidth: headerImage?.getBoundingClientRect().width,
headerImageHeight: headerImage?.getBoundingClientRect().height,
headerNaturalWidth: headerImage?.naturalWidth,
headerNaturalHeight: headerImage?.naturalHeight,
paperTop: paperRect?.top,
contentTop: contentRect?.top,
agreementBottom: document.querySelector('.agreement-area')?.getBoundingClientRect().bottom
}
})()`)
assert.strictEqual(metrics.innerWidth, size.width, `A01 viewport width mismatch at ${size.width}x${size.height}`)
assert(metrics.scrollWidth <= size.width + 1, `A01 has horizontal overflow at ${size.width}x${size.height}: ${metrics.scrollWidth}`)
assert.deepStrictEqual([metrics.headerNaturalWidth, metrics.headerNaturalHeight], [824, 340], `A01 header asset mismatch at ${size.width}x${size.height}`)
assert(Math.abs(metrics.headerImageHeight / metrics.headerImageWidth - 340 / 824) < 0.01, `A01 header is distorted at ${size.width}x${size.height}`)
assert(metrics.paperTop >= metrics.headerBottom - 1, `A01 paper overlaps the header at ${size.width}x${size.height}`)
assert(metrics.contentTop >= metrics.paperTop, `A01 content escapes paper flow at ${size.width}x${size.height}`)
if (size.width >= 360 && size.height >= 640) {
assert(metrics.rootScrollHeight <= metrics.innerHeight + 1, `A01 password state must fit at ${size.width}x${size.height}`)
assert(metrics.agreementBottom <= metrics.innerHeight + 1, `A01 password agreement must remain visible at ${size.width}x${size.height}`)
} else {
assert(metrics.agreementBottom <= metrics.documentScrollHeight + 1, `A01 password agreement must remain reachable at ${size.width}x${size.height}`)
}
const buttonAssets = await valueOf(send, `Array.from(document.querySelectorAll('.button-skin img')).map((image) => ({
src: image.currentSrc || image.src,
@@ -111,9 +137,21 @@ const run = async () => {
[[1866, 276], [1866, 300]],
`A01 v3 button assets have unexpected runtime dimensions at ${size.width}x${size.height}`
)
if (size.width === 320 && size.height === 568) {
assert(metrics.pageScrollHeight > metrics.pageClientHeight, 'A01 must scroll naturally at 320x568')
await valueOf(send, "document.querySelectorAll('.login-tab')[1].click()")
await waitFor(send, "document.querySelectorAll('.login-tab')[1].classList.contains('active')", `A01 SMS tab did not activate at ${size.width}x${size.height}`)
const smsMetrics = await valueOf(send, `({
documentScrollHeight: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight),
rootScrollHeight: document.querySelector('.auth-page')?.scrollHeight,
agreementBottom: document.querySelector('.agreement-area')?.getBoundingClientRect().bottom
})`)
if (size.width >= 360 && size.height >= 640) {
assert(smsMetrics.rootScrollHeight <= metrics.innerHeight + 1, `A01 SMS state must fit at ${size.width}x${size.height}`)
assert(smsMetrics.agreementBottom <= metrics.innerHeight + 1, `A01 SMS agreement must remain visible at ${size.width}x${size.height}`)
} else {
assert(smsMetrics.agreementBottom <= smsMetrics.documentScrollHeight + 1, `A01 SMS agreement must remain reachable at ${size.width}x${size.height}`)
}
await valueOf(send, "document.querySelectorAll('.login-tab')[0].click()")
await waitFor(send, "document.querySelectorAll('.login-tab')[0].classList.contains('active')", `A01 password tab did not reactivate at ${size.width}x${size.height}`)
}
await valueOf(send, "document.querySelector('.login-submit').click()")
+40 -7
View File
@@ -28,11 +28,11 @@ $sliderPendingCopy = ConvertFrom-Utf8Base64 '5ruR5Yqo6aqM6K+B5b6F5o6l5Y+j5o6l5YW
if (-not ($pages.pages.path -contains 'pages/auth/a04-register')) { throw 'A-04 route must remain declared in pages.json' }
Assert-NotContains -Content $register -Unexpected 'ModulePage' -Message 'A-04 must not remain a ModulePage shell'
foreach ($required in @(
'a01-red-hall-ink-backdrop-v1.png',
'brand-seal.png',
'<AuthPageShell',
'import AuthPageShell from "@/components/AuthPageShell.vue";',
'a01-vnext-divider-v1.png',
'a01-scroll-primary-v3.png',
'a01-scroll-toast-v3.png',
'adaptive.adaptive-feedback-toast',
'a02-agreement-unchecked.png',
'a02-agreement-checked.png',
'v-model.trim="phone"',
@@ -58,7 +58,9 @@ Assert-Contains -Content $register -Expected $registerCopy -Message 'A-04 must v
Assert-Contains -Content $register -Expected $loginCopy -Message 'A-04 must provide a login return path'
Assert-NotContains -Content $catalog -Unexpected 'a04:' -Message 'A-04 must not remain in the temporary page catalog'
Assert-NotContains -Content $register -Unexpected 'register-intro' -Message 'A-04 must not retain the redundant registration intro copy'
Assert-NotContains -Content $register -Unexpected 'auth-design-scenery-v1.png' -Message 'A-04 must use the shared red-hall ink backdrop instead of the rejected scenery layer'
foreach ($forbidden in @('class="page-canvas"', 'class="page-backdrop"', 'mode="scaleToFill"', '1665rpx', 'a01-red-hall-ink-backdrop-v1.png')) {
Assert-NotContains -Content $register -Unexpected $forbidden -Message "A04 retains rejected page coordinates: $forbidden"
}
Assert-Contains -Content $globalStyles -Expected 'font-family: "STKaiti", "KaiTi", serif' -Message 'Global styles must own the A04 font contract.'
Assert-NotContains -Content $register -Unexpected 'font-family:' -Message 'A04 must inherit the global font contract instead of overriding it locally.'
foreach ($obsoleteVisual in @(
@@ -77,13 +79,44 @@ foreach ($nativeUi in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'un
foreach ($required in @(
'register-divider',
'mode="aspectFit"',
'width: 184rpx;',
'height: 221rpx;',
'border-image-slice: 58 280 fill;'
'adaptive.adaptive-feedback-toast;',
'top: calc(var(--status-bar-height, 0px) + 24rpx);'
)) {
Assert-Contains -Content $register -Expected $required -Message "Missing A-04 visual balance contract: $required"
}
$contentRule = [regex]::Match($register, '(?ms)^\.register-content\s*\{(?<Body>.*?)^\}')
if (-not $contentRule.Success) { throw 'A04 register content rule is missing.' }
foreach ($required in @('display: flex;', 'flex: 1;', 'flex-direction: column;', 'justify-content: flex-start;', 'max-width: 480px;', 'margin: 0 auto;')) {
Assert-Contains -Content $contentRule.Groups['Body'].Value -Expected $required -Message "A04 register content must use document flow: $required"
}
$headingRule = [regex]::Match($register, '(?ms)^\.page-heading\s*\{(?<Body>.*?)^\}')
if (-not $headingRule.Success) { throw 'A04 page heading rule is missing.' }
Assert-Contains -Content $headingRule.Groups['Body'].Value -Expected 'grid-template-columns: minmax(0, 1fr);' -Message 'A04 heading must center against the full content width.'
foreach ($selector in @('page-title', 'page-subtitle', 'register-divider')) {
$rule = [regex]::Match($register, "(?ms)^\.$selector\s*\{(?<Body>.*?)^\}")
if (-not $rule.Success) { throw "A04 heading child rule is missing: $selector" }
Assert-Contains -Content $rule.Groups['Body'].Value -Expected 'grid-column: 1;' -Message "A04 heading child must remain in the full-width grid column: $selector"
}
foreach ($ruleContract in @(
@{ Selector = 'register-submit__content'; Expected = 'font-size: 36rpx;' },
@{ Selector = 'agreement-row'; Expected = 'font-size: 24rpx;' },
@{ Selector = 'agreement-row'; Expected = 'color: #493323;' },
@{ Selector = 'input-label'; Expected = 'font-size: 30rpx;' },
@{ Selector = 'auth-input'; Expected = 'font-size: 30rpx;' },
@{ Selector = 'auth-input'; Expected = 'color: #493323;' },
@{ Selector = 'placeholder'; Expected = 'color: #9f968d;' },
@{ Selector = 'login-entry'; Expected = 'font-size: 28rpx;' },
@{ Selector = 'login-entry'; Expected = 'color: #493323;' },
@{ Selector = 'login-entry'; Expected = 'margin-top: auto;' }
)) {
$rule = [regex]::Match($register, "(?ms)^\.$($ruleContract.Selector)\s*\{(?<Body>.*?)^\}")
if (-not $rule.Success) { throw "A04 style rule is missing: $($ruleContract.Selector)" }
Assert-Contains -Content $rule.Groups['Body'].Value -Expected $ruleContract.Expected -Message "A04 typography must match A01: $($ruleContract.Selector) $($ruleContract.Expected)"
}
foreach ($forbidden in @(
'(?s)\.register-panel\s*\{[^}]*\bborder\s*:',
'(?s)\.register-panel\s*\{[^}]*\bbackground\s*:',
+33 -7
View File
@@ -68,16 +68,42 @@ const run = async () => {
await valueOf(send, "document.querySelector('.register-link').click()")
await waitFor(send, "location.href.includes('/pages/auth/a04-register')", 'A01 registration entry did not open A04')
for (const size of [{ width: 320, height: 568 }, { width: 360, height: 640 }, { width: 360, height: 800 }, { width: 412, height: 915 }]) {
for (const size of [
{ width: 320, height: 568 },
{ width: 360, height: 616 },
{ width: 360, height: 640 },
{ width: 360, height: 800 },
{ width: 412, height: 915 },
{ width: 480, height: 1040 }
]) {
await send('Emulation.setDeviceMetricsOverride', { ...size, deviceScaleFactor: 1, mobile: true })
await navigate(send, a04Url, '.register-submit')
const metrics = await valueOf(send, `({
width: document.documentElement.scrollWidth,
pageClientHeight: document.querySelector('.auth-page').clientHeight,
pageScrollHeight: document.querySelector('.auth-page').scrollHeight
})`)
await waitFor(send, "document.querySelector('.auth-shell__header-image img')?.naturalWidth === 824", `A04 header did not load at ${size.width}x${size.height}`)
const metrics = await valueOf(send, `(() => {
const header = document.querySelector('.auth-shell__header')
const headerImage = document.querySelector('.auth-shell__header-image img')
const paper = document.querySelector('.auth-shell__paper')
const title = document.querySelector('.page-title')
const loginEntry = document.querySelector('.login-entry')
return {
width: document.documentElement.scrollWidth,
documentScrollHeight: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight),
headerBottom: header?.getBoundingClientRect().bottom,
headerImageWidth: headerImage?.getBoundingClientRect().width,
headerImageHeight: headerImage?.getBoundingClientRect().height,
headerNaturalWidth: headerImage?.naturalWidth,
headerNaturalHeight: headerImage?.naturalHeight,
paperTop: paper?.getBoundingClientRect().top,
titleCenter: title ? title.getBoundingClientRect().left + title.getBoundingClientRect().width / 2 : null,
loginEntryBottom: loginEntry?.getBoundingClientRect().bottom + window.scrollY
}
})()`)
assert(metrics.width <= size.width + 1, `A04 has horizontal overflow at ${size.width}x${size.height}`)
if (size.width === 320) assert(metrics.pageScrollHeight > metrics.pageClientHeight, 'A04 must scroll naturally at 320x568')
assert.deepStrictEqual([metrics.headerNaturalWidth, metrics.headerNaturalHeight], [824, 340], `A04 header asset changed at ${size.width}x${size.height}`)
assert(Math.abs(metrics.headerImageHeight / metrics.headerImageWidth - 340 / 824) < 0.01, `A04 header is distorted at ${size.width}x${size.height}`)
assert(metrics.paperTop >= metrics.headerBottom - 1, `A04 paper overlaps its header at ${size.width}x${size.height}`)
assert(Math.abs(metrics.titleCenter - size.width / 2) <= 1, `A04 title is not centered at ${size.width}x${size.height}: ${metrics.titleCenter}`)
assert(metrics.loginEntryBottom <= metrics.documentScrollHeight + 1, `A04 last action is unreachable at ${size.width}x${size.height}`)
}
await valueOf(send, "document.querySelector('.register-submit').click()")
+24 -7
View File
@@ -29,11 +29,11 @@ $sliderPendingCopy = ConvertFrom-Utf8Base64 '5ruR5Yqo6aqM6K+B5b6F5o6l5Y+j5o6l5YW
if (-not ($pages.pages.path -contains 'pages/auth/a05-reset-password')) { throw 'A-05 route must remain declared in pages.json' }
Assert-NotContains -Content $page -Unexpected 'ModulePage' -Message 'A-05 must not remain a ModulePage shell'
foreach ($required in @(
'a01-red-hall-ink-backdrop-v1.png',
'brand-seal.png',
'<AuthPageShell',
'import AuthPageShell from "@/components/AuthPageShell.vue";',
'a01-vnext-divider-v1.png',
'a01-scroll-primary-v3.png',
'a01-scroll-toast-v3.png',
'adaptive.adaptive-feedback-toast',
'a01-scroll-dialog-v3.png',
'chevron-right.png',
'v-model.trim="phone"',
@@ -52,9 +52,9 @@ foreach ($required in @(
'if (!/^\d{6}$/.test(verificationCode.value))',
'if (!password.value)',
'if (password.value !== confirmPassword.value)',
'width: 184rpx;',
'height: 221rpx;',
'border-image-slice: 58 280 fill;'
'adaptive.adaptive-feedback-toast;',
'top: calc(var(--status-bar-height, 0px) + 24rpx);',
'max-height: calc(var(--app-viewport-height) - 40px);'
)) {
Assert-Contains -Content $page -Expected $required -Message "Missing A-05 reset-password contract: $required"
}
@@ -64,7 +64,9 @@ Assert-Contains -Content $page -Expected $getCodeCopy -Message 'A-05 must expose
Assert-Contains -Content $page -Expected $loginCopy -Message 'A-05 must provide the A01 return path'
Assert-Contains -Content $page -Expected $sliderPendingCopy -Message 'A-05 must defer the real slider to the interface stage'
Assert-NotContains -Content $catalog -Unexpected 'a05:' -Message 'A-05 must not remain in the temporary page catalog'
Assert-NotContains -Content $page -Unexpected 'auth-design-scenery-v1.png' -Message 'A-05 must use the selected red-hall ink backdrop'
foreach ($forbidden in @('class="page-canvas"', 'class="page-backdrop"', 'mode="scaleToFill"', '1665rpx', 'a01-red-hall-ink-backdrop-v1.png')) {
Assert-NotContains -Content $page -Unexpected $forbidden -Message "A05 retains rejected page coordinates: $forbidden"
}
foreach ($obsoleteVisual in @(
'a01-primary-button.png',
'a01-secondary-button.png',
@@ -96,4 +98,19 @@ foreach ($forbidden in @(
if ($page -match $forbidden) { throw "A-05 retains forbidden CSS visual construction: $forbidden" }
}
$contentRule = [regex]::Match($page, '(?ms)^\.reset-content\s*\{(?<Body>.*?)^\}')
if (-not $contentRule.Success) { throw 'A05 reset content rule is missing.' }
foreach ($required in @('display: flex;', 'flex: 1;', 'flex-direction: column;', 'max-width: 480px;', 'margin: 0 auto;')) {
Assert-Contains -Content $contentRule.Groups['Body'].Value -Expected $required -Message "A05 reset content must use document flow: $required"
}
$headingRule = [regex]::Match($page, '(?ms)^\.page-heading\s*\{(?<Body>.*?)^\}')
if (-not $headingRule.Success) { throw 'A05 page heading rule is missing.' }
Assert-Contains -Content $headingRule.Groups['Body'].Value -Expected 'grid-template-columns: minmax(0, 1fr);' -Message 'A05 heading must center against the full content width.'
foreach ($selector in @('page-title', 'page-subtitle', 'reset-divider')) {
$rule = [regex]::Match($page, "(?ms)^\.$selector\s*\{(?<Body>.*?)^\}")
if (-not $rule.Success) { throw "A05 heading child rule is missing: $selector" }
Assert-Contains -Content $rule.Groups['Body'].Value -Expected 'grid-column: 1;' -Message "A05 heading child must remain in the full-width grid column: $selector"
}
Write-Output 'A05-RESET-PASSWORD-CONTRACT PASS'
+33 -7
View File
@@ -78,16 +78,42 @@ const run = async () => {
await valueOf(send, "document.querySelector('.forgot-password').click()")
await waitFor(send, "location.href.includes('/pages/auth/a05-reset-password')", 'A01 forgot-password entry did not open A05')
for (const size of [{ width: 320, height: 568 }, { width: 360, height: 640 }, { width: 360, height: 800 }, { width: 412, height: 915 }]) {
for (const size of [
{ width: 320, height: 568 },
{ width: 360, height: 616 },
{ width: 360, height: 640 },
{ width: 360, height: 800 },
{ width: 412, height: 915 },
{ width: 480, height: 1040 }
]) {
await send('Emulation.setDeviceMetricsOverride', { ...size, deviceScaleFactor: 1, mobile: true })
await navigate(send, a05Url, '.reset-submit')
const metrics = await valueOf(send, `({
width: document.documentElement.scrollWidth,
pageClientHeight: document.querySelector('.auth-page').clientHeight,
pageScrollHeight: document.querySelector('.auth-page').scrollHeight
})`)
await waitFor(send, "document.querySelector('.auth-shell__header-image img')?.naturalWidth === 824", `A05 header did not load at ${size.width}x${size.height}`)
const metrics = await valueOf(send, `(() => {
const header = document.querySelector('.auth-shell__header')
const headerImage = document.querySelector('.auth-shell__header-image img')
const paper = document.querySelector('.auth-shell__paper')
const title = document.querySelector('.page-title')
const loginEntry = document.querySelector('.login-entry')
return {
width: document.documentElement.scrollWidth,
documentScrollHeight: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight),
headerBottom: header?.getBoundingClientRect().bottom,
headerImageWidth: headerImage?.getBoundingClientRect().width,
headerImageHeight: headerImage?.getBoundingClientRect().height,
headerNaturalWidth: headerImage?.naturalWidth,
headerNaturalHeight: headerImage?.naturalHeight,
paperTop: paper?.getBoundingClientRect().top,
titleCenter: title ? title.getBoundingClientRect().left + title.getBoundingClientRect().width / 2 : null,
loginEntryBottom: loginEntry?.getBoundingClientRect().bottom + window.scrollY
}
})()`)
assert(metrics.width <= size.width + 1, `A05 has horizontal overflow at ${size.width}x${size.height}`)
if (size.width === 320) assert(metrics.pageScrollHeight > metrics.pageClientHeight, 'A05 must scroll naturally at 320x568')
assert.deepStrictEqual([metrics.headerNaturalWidth, metrics.headerNaturalHeight], [824, 340], `A05 header asset changed at ${size.width}x${size.height}`)
assert(Math.abs(metrics.headerImageHeight / metrics.headerImageWidth - 340 / 824) < 0.01, `A05 header is distorted at ${size.width}x${size.height}`)
assert(metrics.paperTop >= metrics.headerBottom - 1, `A05 paper overlaps its header at ${size.width}x${size.height}`)
assert(Math.abs(metrics.titleCenter - size.width / 2) <= 1, `A05 title is not centered at ${size.width}x${size.height}: ${metrics.titleCenter}`)
assert(metrics.loginEntryBottom <= metrics.documentScrollHeight + 1, `A05 last action is unreachable at ${size.width}x${size.height}`)
}
await valueOf(send, "document.querySelector('.get-code').click()")
+22 -6
View File
@@ -39,17 +39,16 @@ foreach ($required in @(
'const openRecovery = () =>',
'const closeRecovery = () =>',
'const goBack = () =>',
'a01-red-hall-ink-backdrop-v1.png',
'brand-seal.png',
'<AuthPageShell',
'import AuthPageShell from "@/components/AuthPageShell.vue";',
'a01-vnext-divider-v1.png',
'a01-scroll-primary-v3.png',
'a01-scroll-dialog-v3.png',
'chevron-right.png',
'auth-login-outline.png',
'class="recovery-layer"',
'width: 184rpx;',
'height: 221rpx;',
'mode="aspectFit"'
'mode="aspectFit"',
'max-height: calc(var(--app-viewport-height) - 40px);'
)) {
Assert-Contains -Content $page -Expected $required -Message "Missing A-06 auth-status contract: $required"
}
@@ -60,7 +59,9 @@ foreach ($removedState in @('normal:', 'failed:', 'restricted:', "'register-pend
foreach ($nativeUi in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet')) {
Assert-NotContains -Content $page -Unexpected $nativeUi -Message "A-06 must not use native UniApp feedback: $nativeUi"
}
Assert-NotContains -Content $page -Unexpected 'auth-design-scenery-v1.png' -Message 'A-06 must use the selected red-hall ink backdrop'
foreach ($forbidden in @('class="page-canvas"', 'class="page-backdrop"', 'mode="scaleToFill"', '1665rpx', 'a01-red-hall-ink-backdrop-v1.png')) {
Assert-NotContains -Content $page -Unexpected $forbidden -Message "A06 retains rejected page coordinates: $forbidden"
}
foreach ($obsoleteVisual in @(
'a01-primary-button.png',
'a02-login-panel.png',
@@ -82,4 +83,19 @@ foreach ($forbidden in @(
if ($page -match $forbidden) { throw "A-06 retains forbidden CSS visual construction: $forbidden" }
}
$contentRule = [regex]::Match($page, '(?ms)^\.status-content\s*\{(?<Body>.*?)^\}')
if (-not $contentRule.Success) { throw 'A06 status content rule is missing.' }
foreach ($required in @('display: flex;', 'flex: 1;', 'flex-direction: column;', 'max-width: 480px;', 'margin: 0 auto;')) {
Assert-Contains -Content $contentRule.Groups['Body'].Value -Expected $required -Message "A06 status content must use document flow: $required"
}
$headingRule = [regex]::Match($page, '(?ms)^\.page-heading\s*\{(?<Body>.*?)^\}')
if (-not $headingRule.Success) { throw 'A06 page heading rule is missing.' }
Assert-Contains -Content $headingRule.Groups['Body'].Value -Expected 'grid-template-columns: minmax(0, 1fr);' -Message 'A06 heading must center against the full content width.'
foreach ($selector in @('page-title', 'page-subtitle', 'status-divider')) {
$rule = [regex]::Match($page, "(?ms)^\.$selector\s*\{(?<Body>.*?)^\}")
if (-not $rule.Success) { throw "A06 heading child rule is missing: $selector" }
Assert-Contains -Content $rule.Groups['Body'].Value -Expected 'grid-column: 1;' -Message "A06 heading child must remain in the full-width grid column: $selector"
}
Write-Output 'A06-AUTH-STATUS-CONTRACT PASS'
@@ -0,0 +1,49 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$profilePath = Join-Path $root 'styles/adaptive-frame-profiles.scss'
if (-not (Test-Path -LiteralPath $profilePath)) {
throw 'Missing adaptive frame profile owner: styles/adaptive-frame-profiles.scss'
}
$profile = Get-Content -LiteralPath $profilePath -Raw -Encoding UTF8
foreach ($token in @(
'@mixin adaptive-auth-dialog',
'@mixin adaptive-scroll-button($type)',
'@mixin adaptive-module-content',
'@mixin adaptive-module-field',
'@mixin adaptive-feedback-toast',
'@mixin adaptive-genealogy-current-slip',
'@mixin adaptive-genealogy-list-card',
'@mixin adaptive-genealogy-state-panel',
'@mixin adaptive-genealogy-form-field',
'@mixin adaptive-g05-overview-surface',
'@mixin adaptive-g06-search-field',
'@mixin adaptive-g06-search-action',
'@mixin adaptive-g01-add-sheet',
'@mixin adaptive-g01-switcher',
'@mixin adaptive-family-content',
'@mixin adaptive-family-panel',
'@mixin adaptive-family-field',
'@mixin adaptive-family-letter',
'@mixin adaptive-notification-content',
'@mixin adaptive-profile-content',
'@mixin adaptive-profile-field',
'@mixin adaptive-profile-summary',
'@mixin adaptive-records-content',
'@mixin adaptive-records-field',
'@mixin adaptive-records-person',
'@mixin adaptive-tree-panel',
'@mixin adaptive-tree-field',
'a01-scroll-dialog-v3.png',
'border-image-slice: 260 240 360 240 fill;',
'border-image-slice: 56 300 fill;',
'border-image-slice: 105 150;',
'border-image-slice: 70 100 fill;'
)) {
if (-not $profile.Contains($token)) {
throw "Adaptive frame profile missing: $token"
}
}
Write-Output 'ADAPTIVE-FRAME-PROFILES-CONTRACT PASS'
+54
View File
@@ -0,0 +1,54 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
function Read-Utf8([string]$path) {
Get-Content -LiteralPath (Join-Path $root $path) -Raw -Encoding UTF8
}
$expected = @{
'components/AppToast.vue' = @('adaptive.adaptive-feedback-toast')
'pages/auth/a01-entry.vue' = @('adaptive.adaptive-feedback-toast')
'pages/auth/a04-register.vue' = @('adaptive.adaptive-feedback-toast')
'pages/auth/a05-reset-password.vue' = @('adaptive.adaptive-feedback-toast')
'pages/genealogy/g01-my-genealogies.vue' = @(
'adaptive.adaptive-genealogy-current-slip',
'adaptive.adaptive-genealogy-list-card',
'adaptive.adaptive-genealogy-state-panel',
'adaptive.adaptive-g01-add-sheet',
'adaptive.adaptive-g01-switcher'
)
'pages/genealogy/g09-my-applications.vue' = @('adaptive.adaptive-genealogy-list-card')
'pages/genealogy/g10-application-review.vue' = @(
'adaptive.adaptive-genealogy-form-field',
'adaptive.adaptive-genealogy-list-card',
'adaptive.adaptive-scroll-button',
'adaptive.adaptive-feedback-toast'
)
}
foreach ($path in $expected.Keys) {
$source = Read-Utf8 $path
if (-not $source.Contains('@use "../../styles/adaptive-frame-profiles.scss" as adaptive;') -and
-not $source.Contains('@use "../styles/adaptive-frame-profiles.scss" as adaptive;')) {
throw "$path must import the adaptive frame profile owner"
}
foreach ($token in $expected[$path]) {
if (-not $source.Contains($token)) { throw "$path missing adaptive surface: $token" }
}
if ($source -match '(?im)border-image-slice\s*:') {
throw "$path must not own border-image slice values"
}
}
$g01 = Read-Utf8 'pages/genealogy/g01-my-genealogies.vue'
$g10 = Read-Utf8 'pages/genealogy/g10-application-review.vue'
foreach ($source in @($g01, $g10)) {
if ($source -match '(?im)background\s*:[^;\r\n]*100%\s+100%[^;\r\n]*;') {
throw 'A/G variable surfaces must not stretch full images to 100% 100%'
}
}
$tabbar = Read-Utf8 'components/AppTabbar.vue'
if ($tabbar -match '(?im)(?<![-\w])height\s*:\s*112rpx;') { throw 'AppTabbar must use a minimum baseline instead of fixed content height' }
Write-Output 'AG-RESPONSIVE-SURFACES-CONTRACT PASS'
+28
View File
@@ -0,0 +1,28 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$button = Get-Content -LiteralPath (Join-Path $root 'components/AppButton.vue') -Raw -Encoding UTF8
foreach ($token in @(
'compact: { type: Boolean, default: false }',
"'app-button--compact': compact",
'@use "../styles/adaptive-frame-profiles.scss" as adaptive;',
'@include adaptive.adaptive-scroll-button(primary);',
'@include adaptive.adaptive-scroll-button(secondary);',
'a01-scroll-primary-v3.png',
'a01-scroll-secondary-v3.png'
)) {
if (-not $button.Contains($token)) { throw "AppButton compact contract missing: $token" }
}
if ($button.Contains('border-image-slice: 56 300 fill;')) {
throw 'AppButton must consume the shared adaptive frame profile instead of owning slice values'
}
if ($button -notmatch '(?s)\.app-button--compact \.app-button__skin\s*\{[^}]*display:\s*none;') {
throw 'AppButton compact mode must hide the aspectFit image skin'
}
if ($button -notmatch '(?s)\.app-button--compact \.app-button__label\s*\{[^}]*padding:\s*0;[^}]*font-size:\s*23rpx;[^}]*letter-spacing:\s*0;[^}]*white-space:\s*nowrap;') {
throw 'AppButton compact label must preserve four-character action capacity'
}
Write-Output 'APP-BUTTON-COMPACT-CONTRACT PASS'
@@ -0,0 +1,32 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$dialog = Get-Content -LiteralPath (Join-Path $root 'components/AppDialog.vue') -Raw -Encoding UTF8
foreach ($token in @(
'compactActions: { type: Boolean, default: false }',
'class="app-dialog__copy"',
':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);',
'margin-top: 26rpx;'
)) {
if (-not $dialog.Contains($token)) { throw "AppDialog responsive contract missing: $token" }
}
foreach ($forbidden in @(
'min-height: 520rpx;',
'center / contain no-repeat',
'margin-top: auto;'
)) {
if ($dialog.Contains($forbidden)) { throw "AppDialog responsive contract forbids: $forbidden" }
}
foreach ($rule in @(
'(?s)\.app-dialog__copy\s*\{[^}]*display:\s*flex;[^}]*width:\s*100%;[^}]*flex-direction:\s*column;',
'(?s)\.app-dialog__title,\s*\.app-dialog__message\s*\{[^}]*display:\s*block;[^}]*width:\s*100%;'
)) {
if ($dialog -notmatch $rule) { throw "AppDialog responsive layout rule missing: $rule" }
}
Write-Output 'APP-DIALOG-RESPONSIVE-CONTENT-CONTRACT PASS'
+41
View File
@@ -0,0 +1,41 @@
$ErrorActionPreference = 'Stop'
function Assert-Contains {
param([string]$Content, [string]$Expected, [string]$Message)
if ($Content -notmatch [regex]::Escape($Expected)) { throw $Message }
}
function Assert-NotContains {
param([string]$Content, [string]$Unexpected, [string]$Message)
if ($Content -match [regex]::Escape($Unexpected)) { throw $Message }
}
$root = Split-Path -Parent $PSScriptRoot
$shellPath = Join-Path $root 'components/AuthPageShell.vue'
if (-not (Test-Path -LiteralPath $shellPath)) { throw 'AuthPageShell is missing' }
$shell = Get-Content -LiteralPath $shellPath -Raw -Encoding utf8
foreach ($required in @(
'class="auth-shell__header"',
'class="auth-shell__header-image"',
'a01-vnext-header-v1.png',
'mode="widthFix"',
'class="auth-shell__paper"',
'a01-red-hall-ink-backdrop-v1.png',
'auth-page-paper.jpg',
'background-position: center calc(-48.18vw), center top;',
'background-repeat: no-repeat, repeat-y;',
'<slot />',
'<slot name="overlay" />',
'min-height: var(--app-viewport-height);'
)) {
Assert-Contains -Content $shell -Expected $required -Message "AuthPageShell is missing: $required"
}
$page = Get-Content -LiteralPath (Join-Path $root 'pages/auth/a01-entry.vue') -Raw -Encoding utf8
Assert-Contains -Content $page -Expected '<AuthPageShell' -Message 'A01 must consume AuthPageShell'
foreach ($forbidden in @('class="page-canvas"', 'class="page-backdrop"', 'mode="scaleToFill"', '1665rpx')) {
Assert-NotContains -Content $page -Unexpected $forbidden -Message "A01 retains rejected page coordinates: $forbidden"
}
Write-Output 'AUTH-PAGE-SHELL-CONTRACT PASS'
+3 -3
View File
@@ -3,9 +3,9 @@ $root = Split-Path $PSScriptRoot -Parent
$page = [System.IO.File]::ReadAllText((Join-Path $root 'pages/genealogy/g01-my-genealogies.vue'), [System.Text.Encoding]::UTF8)
foreach ($expected in @(
'background: url("/static/assets/modules/genealogy/transparent/current-slip-frame.png") center / 100% 100% no-repeat',
'background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png") center / 100% 100% no-repeat',
'background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png") center / 100% 100% no-repeat',
'@include adaptive.adaptive-genealogy-current-slip',
'@include adaptive.adaptive-genealogy-list-card',
'@include adaptive.adaptive-genealogy-state-panel',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / contain no-repeat',
'grid-template-columns: minmax(0, 1fr) 96rpx'
)) {
+13
View File
@@ -0,0 +1,13 @@
$ErrorActionPreference = 'Stop'
$page = Get-Content -Raw 'pages/genealogy/g01-my-genealogies.vue'
function Assert-Contains([string]$Content, [string]$Pattern, [string]$Message) {
if (-not $Content.Contains($Pattern)) { throw $Message }
}
Assert-Contains $page 'v-for="item in visibleShortcuts"' 'G01 must render the membership-filtered shortcut list'
Assert-Contains $page '{{ currentRoleLabel }}' 'G01 must render the membership-derived role label'
Assert-Contains $page 'currentGenealogy.value?.membership === "created"' 'G01 must derive owner state from membership'
Assert-Contains $page 'item.key !== "applications"' 'G01 must remove application review for joined members'
Write-Output 'G01-ROLE-SHORTCUT-CONTRACT PASS'
+20
View File
@@ -261,10 +261,30 @@ const run = async () => {
await valueOf(send, "document.querySelectorAll('.switcher-item')[1].click()")
await waitFor(send, "!document.querySelector('.genealogy-switcher')", 'Selecting the second genealogy did not close the switcher')
assert((await valueOf(send, "document.querySelector('.current-slip')?.textContent"))?.includes('山东'), 'Selecting the second genealogy did not update the current genealogy')
assert(
(await valueOf(send, "document.querySelector('.current-meta')?.textContent"))?.includes('成员'),
'Joined genealogy did not display the member role'
)
assert(
(await valueOf(send, "document.querySelectorAll('.shortcut-item').length")) === 3,
'Joined genealogy did not hide the application-review shortcut'
)
assert(
!(await valueOf(send, "document.querySelector('.shortcut-grid')?.textContent"))?.includes('申请审核'),
'Joined genealogy still exposed application review'
)
await openSwitcher(send)
await valueOf(send, "document.querySelectorAll('.switcher-item')[0].click()")
await waitFor(send, "!document.querySelector('.genealogy-switcher')", 'Restoring the first genealogy did not close the switcher')
assert(
(await valueOf(send, "document.querySelector('.current-meta')?.textContent"))?.includes('管理员'),
'Created genealogy did not restore the administrator role'
)
assert(
(await valueOf(send, "document.querySelectorAll('.shortcut-item').length")) === 4,
'Created genealogy did not restore all four shortcuts'
)
await openSwitcher(send)
await clearClones(send)
+5 -4
View File
@@ -59,6 +59,7 @@ if ($tabbar -notmatch 'font-size:\s*30rpx') { throw 'Tabbar label size does not
if ($tabbar -notmatch 'width:\s*64rpx') { throw 'Tabbar icon size does not match the approved visual weight.' }
$page = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/genealogy/g01-my-genealogies.vue')
$profiles = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'styles/adaptive-frame-profiles.scss')
if ($page -match "from '@/utils/api\.js'") { throw 'G-01 visual phase must not call the remote API.' }
foreach ($state in @('isLoading', 'hasGenealogies', 'createdGenealogies', 'joinedGenealogies')) {
if ($page -notmatch $state) { throw "G-01 is missing presentation state: $state" }
@@ -127,10 +128,10 @@ if ($page -match '(?s)\.add-dialog__heading\s*\{[^}]*top\s*:') {
if ($page -notmatch '(?s)\.add-dialog__close\s*\{[^}]*grid-column:\s*2;[^}]*grid-row:\s*1\s*/\s*span\s+2;[^}]*margin-right:\s*-22rpx;') {
throw 'G-01 add sheet close control does not follow the heading grid.'
}
if ($page -notmatch [regex]::Escape('/static/assets/modules/genealogy/transparent/g01-add-sheet-background-v3.png')) {
if ($page -notmatch 'adaptive\.adaptive-g01-add-sheet' -or $profiles -notmatch [regex]::Escape('/static/assets/modules/genealogy/transparent/g01-add-sheet-background-v3.png')) {
throw 'G-01 add sheet does not use the approved complete background asset.'
}
if ($page -notmatch '(?s)\.add-dialog\s*\{[^}]*border-image-source:\s*url\("/static/assets/modules/genealogy/transparent/g01-add-sheet-background-v3\.png"\);[^}]*border-image-slice:\s*220\s+0\s+1\s+0\s+fill;[^}]*border-image-width:\s*118rpx\s+0\s+1rpx;') {
if ($profiles -notmatch '(?s)@mixin\s+adaptive-g01-add-sheet\s*\{[^}]*border-image-source:\s*url\("/static/assets/modules/genealogy/transparent/g01-add-sheet-background-v3\.png"\);[^}]*border-image-slice:\s*220\s+0\s+1\s+0\s+fill;[^}]*border-image-width:\s*118rpx\s+0\s+1rpx;') {
throw 'G-01 add sheet does not preserve the complete sheet top while stretching only the paper body.'
}
if ($page -notmatch '(?s)\.add-dialog__close-icon\s*\{[^}]*width:\s*80rpx;[^}]*height:\s*80rpx;') {
@@ -155,7 +156,7 @@ if ($page -notmatch '(?s)\.genealogy-switcher-layer\s*\{[^}]*align-items:\s*cent
if ($page -match 'class="genealogy-switcher__skin"') {
throw 'G-01 switcher must not render the complete background as a fixed aspectFit image.'
}
if ($page -notmatch '(?s)\.genealogy-switcher\s*\{[^}]*min-height:\s*600rpx;[^}]*max-height:\s*calc\(100vh\s*-\s*120rpx\);[^}]*border-image-source:\s*url\("/static/assets/modules/auth/transparent/a01-scroll-dialog-v3\.png"\);[^}]*border-image-slice:\s*300\s+260\s+360\s+260\s+fill;[^}]*border-image-width:\s*110rpx\s+48rpx\s+132rpx\s+48rpx;') {
if ($page -notmatch '(?s)\.genealogy-switcher\s*\{[^}]*@include\s+adaptive\.adaptive-g01-switcher;[^}]*min-height:\s*600rpx;[^}]*max-height:\s*calc\(100vh\s*-\s*120rpx\);' -or $profiles -notmatch '(?s)@mixin\s+adaptive-g01-switcher\s*\{[^}]*border-image-source:\s*url\("/static/assets/modules/auth/transparent/a01-scroll-dialog-v3\.png"\);[^}]*border-image-slice:\s*300\s+260\s+360\s+260\s+fill;[^}]*border-image-width:\s*110rpx\s+48rpx\s+132rpx\s+48rpx;') {
throw 'G-01 switcher does not use the approved stretchable complete background.'
}
if ($page -notmatch '(?s)\.genealogy-switcher__content\s*\{[^}]*min-height:\s*600rpx;[^}]*max-height:\s*calc\(100vh\s*-\s*120rpx\);[^}]*padding:\s*120rpx\s+58rpx\s+140rpx;') {
@@ -182,7 +183,7 @@ foreach ($nativeUi in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'un
foreach ($asset in @('shortcut-tree.png', 'shortcut-members.png', 'shortcut-generation-poem.png', 'shortcut-application.png', 'add.png')) {
if ($page -notmatch [regex]::Escape($asset)) { throw "G-01 does not consume $asset" }
}
if ($page -notmatch 'current-slip-frame\.png') { throw 'G-01 does not use the approved current genealogy frame asset.' }
if ($page -notmatch 'adaptive\.adaptive-genealogy-current-slip' -or $profiles -notmatch 'current-slip-frame\.png') { throw 'G-01 does not consume the approved current genealogy frame profile.' }
foreach ($token in @('current-summary', 'current-meta-item', 'current-seal-frame', 'meta-location.png', 'meta-member.png', 'meta-admin.png', 'create-cloud.png')) {
if ($page -notmatch $token) { throw "G-01 current panel is missing $token" }
}
+9 -4
View File
@@ -17,6 +17,7 @@ $root = Split-Path -Parent $PSScriptRoot
$pages = Get-Content -LiteralPath (Join-Path $root 'pages.json') -Raw -Encoding utf8 | ConvertFrom-Json
$paths = @($pages.pages.path)
$g03 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g03-create-genealogy.vue') -Raw -Encoding utf8
$profiles = Get-Content -LiteralPath (Join-Path $root 'styles/adaptive-frame-profiles.scss') -Raw -Encoding utf8
$g04 = Join-Path $root 'pages/genealogy/g04-first-ancestor.vue'
$panelPath = Join-Path $root 'static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png'
@@ -36,16 +37,20 @@ foreach ($required in @(
'class="duplicate-reminder-layer"',
'if (isSubmitting.value) return;',
'/pages/genealogy/g05-genealogy-overview?genealogyId=',
'g01-empty-panel-frame.png',
'root-header-cinnabar.jpg',
'adaptive.adaptive-genealogy-state-panel',
'a01-scroll-primary-v3.png',
'create-flow-panel',
'flow-primary-action'
'flow-primary-action',
'import PageHeader from "@/components/PageHeader.vue";',
'<PageHeader',
'custom-back',
'@back="goBack"'
)) {
Assert-Contains -Content $g03 -Expected $required -Message "Missing G03 flow contract: $required"
}
Assert-Contains -Content $profiles -Expected 'g01-empty-panel-frame.png' -Message 'Adaptive genealogy panel profile must own the G03 panel asset'
foreach ($forbidden in @("from '@/utils/api.js'", 'appApi.', 'uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet', '/pages/tree/t01-tree-overview?genealogyId=')) {
foreach ($forbidden in @("from '@/utils/api.js'", 'appApi.', 'uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet', '/pages/tree/t01-tree-overview?genealogyId=', 'class="flow-header"', 'flow-header__back', 'root-header-cinnabar.jpg')) {
if ($g03 -match [regex]::Escape($forbidden)) { throw "G03 retains forbidden implementation: $forbidden" }
}
+3 -1
View File
@@ -7,9 +7,11 @@ 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/foundation/opaque/root-header-cinnabar.jpg")',
'background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")',
'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" } }
foreach ($forbidden in @('flow-header', 'root-header-cinnabar.jpg')) {
if ($page -match [regex]::Escape($forbidden)) { throw "G03 must not retain private header token: $forbidden" }
}
Write-Output 'G03-DOCUMENT-FLOW-CONTRACT PASS'
+2 -2
View File
@@ -9,8 +9,8 @@ function Assert-Match {
}
if ($g03 -match '<text>返回</text>') { throw 'G03 header must use only the approved image back arrow' }
Assert-Match -Content $g03 -Pattern '(?s)\.flow-header__back,\s*\.flow-header__side\s*\{[^}]*width:\s*72rpx;' -Message 'G03 header side slots must preserve centered title after removing back text'
Assert-Match -Content $g03 -Pattern '(?s)\.flow-header__back-icon\s*\{[^}]*width:\s*34rpx;[^}]*height:\s*34rpx;' -Message 'G03 back arrow must use the approved visible size'
Assert-Match -Content $g03 -Pattern '(?s)<PageHeader\s+:title="isAncestorStep \? ''\u5F55\u5165\u9996\u4EE3\u4EBA\u7269'' : ''\u521B\u5EFA\u5BB6\u8C31''"\s+custom-back\s+@back="goBack"\s*/>' -Message 'G03 must use PageHeader with its dynamic title and custom back handler'
if ($g03 -match 'flow-header|flow-header__back|flow-header__title') { throw 'G03 must not retain a private header implementation' }
Assert-Match -Content $g03 -Pattern '(?s)\.flow-note,\s*\.flow-rule__note\s*\{[^}]*font-size:\s*25rpx;' -Message 'G03 guidance copy must use the approved readable size'
Assert-Match -Content $g03 -Pattern '(?s)\.flow-rule__option\s*\{[^}]*font-size:\s*23rpx;' -Message 'G03 visibility options must use the approved readable size'
Assert-Match -Content $g03 -Pattern '(?s)\.field-error\s*\{[^}]*font-size:\s*24rpx;[^}]*line-height:\s*34rpx;' -Message 'G03 field errors must use the approved readable size'
+3 -1
View File
@@ -17,6 +17,7 @@ function Assert-NoCssSurface {
$root = Split-Path -Parent $PSScriptRoot
$g01 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g01-my-genealogies.vue') -Raw -Encoding utf8
$g05 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g05-genealogy-overview.vue') -Raw -Encoding utf8
$profiles = Get-Content -LiteralPath (Join-Path $root 'styles/adaptive-frame-profiles.scss') -Raw -Encoding utf8
$runtimeSmoke = Get-Content -LiteralPath (Join-Path $root 'tests/g05-overview-runtime-smoke.js') -Raw -Encoding utf8
$asset = Join-Path $root 'static/assets/modules/genealogy/opaque/g05-overview-surface.png'
@@ -34,7 +35,7 @@ foreach ($required in @(
'overview-state--empty',
'overview-state--error',
'overview-state--no-permission',
'g05-overview-surface.png',
'adaptive.adaptive-g05-overview-surface',
'/pages/genealogy/g03-create-genealogy?step=ancestor&genealogyId=',
'/pages/tree/t01-tree-overview?genealogyId=',
'/pages/genealogy/g10-application-review?genealogyId=',
@@ -44,6 +45,7 @@ foreach ($required in @(
)) {
Assert-Contains $g05 $required "Missing G05 overview contract: $required"
}
Assert-Contains $profiles 'g05-overview-surface.png' 'Adaptive G05 surface profile must own the overview asset'
foreach ($forbidden in @("from '@/utils/api.js'", 'appApi.', 'uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet')) {
if ($g05 -match [regex]::Escape($forbidden)) { throw "G05 retains forbidden implementation: $forbidden" }
}
+7 -2
View File
@@ -13,17 +13,22 @@ if ($positions.Count -ne 0) {
}
foreach ($required in @(
'background: url("/static/assets/modules/genealogy/opaque/g06-search-title-strip.png")',
'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")',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")'
)) {
if ($page -notmatch [regex]::Escape($required)) {
if ($styleWithoutComments -notmatch [regex]::Escape($required)) {
throw "G06 document-flow migration is missing asset-backed surface: $required"
}
}
foreach ($forbidden in @('search-title-strip', 'g06-search-title-strip.png')) {
if ($page -match [regex]::Escape($forbidden)) {
throw "G06 must not retain removed title-strip token: $forbidden"
}
}
foreach ($obsoleteClass in @('search-title-strip__skin', 'search-field__skin', 'search-action__skin', 'result-card__skin')) {
if ($page -match "class=`"$obsoleteClass`"") {
throw "G06 must not retain decorative image layer .$obsoleteClass"
+8 -8
View File
@@ -17,8 +17,8 @@ $root = Split-Path -Parent $PSScriptRoot
$pages = Get-Content -LiteralPath (Join-Path $root 'pages.json') -Raw -Encoding utf8 | ConvertFrom-Json
$paths = @($pages.pages.path)
$g06 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g06-search-genealogies.vue') -Raw -Encoding utf8
$profiles = Get-Content -LiteralPath (Join-Path $root 'styles/adaptive-frame-profiles.scss') -Raw -Encoding utf8
$g07 = Join-Path $root 'pages/genealogy/g07-search-result.vue'
$titleStripPath = Join-Path $root 'static/assets/modules/genealogy/opaque/g06-search-title-strip.png'
$inputSkinPath = Join-Path $root 'static/assets/modules/genealogy/opaque/g06-search-input-wide.png'
$buttonSkinPath = Join-Path $root 'static/assets/modules/genealogy/opaque/g06-search-button.png'
@@ -45,10 +45,8 @@ foreach ($required in @(
'/pages/genealogy/g08-join-application?source=invite&genealogyId=',
'/pages/genealogy/g05-genealogy-overview?mode=public&genealogyId=',
'search-page__header',
'g06-search-input-wide.png',
'g06-search-button.png',
'g06-search-title-strip.png',
'search-title-strip',
'adaptive.adaptive-g06-search-field',
'adaptive.adaptive-g06-search-action',
'section-divider.png',
'root-header-hall.png',
'search-controls',
@@ -59,8 +57,11 @@ foreach ($required in @(
)) {
Assert-Contains -Content $g06 -Expected $required -Message "Missing G06 flow contract: $required"
}
foreach ($assetName in @('g06-search-input-wide.png', 'g06-search-button.png')) {
Assert-Contains $profiles $assetName "Adaptive G06 profile must own: $assetName"
}
foreach ($forbidden in @("from '@/utils/api.js'", 'appApi.', 'uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet')) {
foreach ($forbidden in @("from '@/utils/api.js'", 'appApi.', 'uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet', 'search-title-strip', 'g06-search-title-strip.png')) {
if ($g06 -match [regex]::Escape($forbidden)) { throw "G06 retains forbidden implementation: $forbidden" }
}
@@ -69,7 +70,7 @@ if ($g06 -match [regex]::Escape('g06-search-panel-v2.png')) { throw 'G06 must no
if ($g06 -match [regex]::Escape('search-hero')) { throw 'G06 must replace the floating hero with the selected title-strip hierarchy' }
if ($g06 -match [regex]::Escape('g06-search-input.png')) { throw 'G06 must use the wide selected-design input skin instead of the tall legacy skin' }
foreach ($className in @('search-title-strip', 'search-field', 'search-action')) {
foreach ($className in @('search-field', 'search-action')) {
Assert-NoCssSurface -Content $g06 -ClassName $className
}
@@ -84,7 +85,6 @@ if ($g06 -notmatch '(?s)\.status-retry\s*\{[^}]*width:\s*300rpx;[^}]*height:\s*7
Add-Type -AssemblyName System.Drawing
foreach ($asset in @(
@{ Path = $titleStripPath; Width = 1122; Height = 220; Label = 'search-title-strip' },
@{ Path = $inputSkinPath; Width = 1120; Height = 248; Label = 'search-input' },
@{ Path = $buttonSkinPath; Width = 300; Height = 132; Label = 'search-button' }
)) {

Some files were not shown because too many files have changed in this diff Show More