Review changes batch 6 of 6

This commit is contained in:
2026-07-20 06:52:33 +08:00
parent db97d3da27
commit 5a31a75da0
160 changed files with 9206 additions and 572 deletions
+1
View File
@@ -9,5 +9,6 @@
/docs/design/assets/a01-vnext/candidates/
/docs/design/assets/a01-vnext/review/
/.superpowers/
/runtime/
**/__pycache__/
*.py[cod]
+82
View File
@@ -0,0 +1,82 @@
<template>
<view
class="app-button"
:class="[
`app-button--${type}`,
{ 'app-button--block': block, 'app-button--disabled': disabled },
]"
:hover-class="disabled ? 'none' : 'app-button--pressed'"
@click="handleClick"
>
<image class="app-button__skin" :src="skin" mode="aspectFit" />
<text class="app-button__label"
><slot>{{ label }}</slot></text
>
</view>
</template>
<script setup>
import { computed } from "vue";
const props = defineProps({
label: { type: String, default: "" },
type: { type: String, default: "primary" },
block: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
});
const emit = defineEmits(["click"]);
const skin = computed(() =>
props.type === "secondary"
? "/static/assets/foundation/transparent/a01-scroll-secondary-v3.png"
: "/static/assets/foundation/transparent/a01-scroll-primary-v3.png",
);
const handleClick = (event) => {
if (!props.disabled) emit("click", event);
};
</script>
<style scoped lang="scss">
.app-button {
position: relative;
display: inline-flex;
width: 420rpx;
max-width: 100%;
min-height: 88rpx;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.app-button--block {
display: flex;
width: 100%;
}
.app-button__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.app-button__label {
position: relative;
z-index: 1;
padding: 0 38rpx;
color: #fffaf0;
font-size: 29rpx;
font-weight: 700;
letter-spacing: 3rpx;
text-align: center;
}
.app-button--secondary .app-button__label {
color: #5c4330;
}
.app-button--disabled {
opacity: 0.48;
}
.app-button--pressed {
opacity: 0.78;
transform: translateY(1rpx);
}
</style>
+127
View File
@@ -0,0 +1,127 @@
<template>
<view
v-if="visible"
class="app-dialog-layer"
@click="closeOnMask && cancel()"
>
<view class="app-dialog" @click.stop>
<image
class="app-dialog__skin"
src="/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png"
mode="aspectFit"
/>
<view class="app-dialog__content">
<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>
<slot />
<view
class="app-dialog__actions"
:class="{ 'app-dialog__actions--single': !showCancel }"
>
<AppButton
v-if="showCancel"
type="secondary"
:label="cancelText"
@click="cancel"
/>
<AppButton :label="confirmText" @click="$emit('confirm')" />
</view>
</view>
</view>
</view>
</template>
<script setup>
import AppButton from "@/components/AppButton.vue";
defineProps({
visible: { type: Boolean, default: false },
eyebrow: { type: String, default: "" },
title: { type: String, required: true },
message: { type: String, default: "" },
confirmText: { type: String, default: "我知道了" },
cancelText: { type: String, default: "取消" },
showCancel: { type: Boolean, default: false },
closeOnMask: { type: Boolean, default: true },
});
const emit = defineEmits(["confirm", "cancel", "close"]);
const cancel = () => {
emit("cancel");
emit("close");
};
</script>
<style scoped lang="scss">
.app-dialog-layer {
position: fixed;
z-index: 80;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 40rpx;
background: rgba(35, 18, 10, 0.62);
}
.app-dialog {
position: relative;
width: 650rpx;
max-width: 100%;
min-height: 520rpx;
}
.app-dialog__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.app-dialog__content {
position: relative;
z-index: 1;
display: flex;
min-height: 520rpx;
flex-direction: column;
align-items: center;
box-sizing: border-box;
padding: 74rpx 66rpx 54rpx;
text-align: center;
}
.app-dialog__eyebrow {
color: #9f170f;
font-size: 22rpx;
font-weight: 700;
letter-spacing: 4rpx;
}
.app-dialog__title {
color: #8f160f;
font-size: 42rpx;
font-weight: 700;
letter-spacing: 4rpx;
}
.app-dialog__eyebrow + .app-dialog__title {
margin-top: 10rpx;
}
.app-dialog__message {
margin-top: 22rpx;
color: #513a28;
font-size: 27rpx;
line-height: 1.65;
}
.app-dialog__actions {
display: grid;
width: 100%;
grid-template-columns: 1fr 1fr;
gap: 16rpx;
margin-top: auto;
}
.app-dialog__actions--single {
display: flex;
justify-content: center;
}
.app-dialog__actions .app-button {
width: 100%;
min-height: 82rpx;
}
</style>
+155
View File
@@ -0,0 +1,155 @@
<template>
<view class="app-loading" :class="`app-loading--${variant}`">
<view class="app-loading__emblem" aria-hidden="true">
<image
class="app-loading__seal"
src="/static/assets/foundation/transparent/brand-seal.png"
mode="aspectFit"
/>
<image
class="app-loading__knot"
src="/static/assets/foundation/transparent/auth-divider-knot.png"
mode="aspectFit"
/>
</view>
<text class="app-loading__copy">{{ text }}</text>
<text v-if="description" class="app-loading__description">{{
description
}}</text>
</view>
</template>
<script setup>
defineProps({
variant: {
type: String,
default: "page",
validator: (value) => ["page", "section"].includes(value),
},
text: { type: String, default: "正在展开,请稍候…" },
description: { type: String, default: "" },
});
</script>
<style scoped lang="scss">
.app-loading {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #62584c;
text-align: center;
}
.app-loading__emblem {
display: flex;
flex-direction: column;
align-items: center;
}
.app-loading__seal {
animation: app-loading-seal-breathe 1.6s ease-in-out infinite;
}
.app-loading__knot {
margin-top: 8rpx;
opacity: 0.72;
animation: app-loading-knot-breathe 1.6s ease-in-out infinite;
}
.app-loading__copy {
font-weight: 500;
letter-spacing: 2rpx;
}
.app-loading__description {
color: #62584c;
line-height: 1.65;
}
.app-loading--page {
min-height: 320rpx;
}
.app-loading--page .app-loading__seal {
width: 132rpx;
height: 136rpx;
}
.app-loading--page .app-loading__knot {
width: 56rpx;
height: 18rpx;
}
.app-loading--page .app-loading__copy {
margin-top: 22rpx;
font-size: 30rpx;
}
.app-loading--page .app-loading__description {
margin-top: 14rpx;
font-size: 24rpx;
}
.app-loading--section {
min-height: 180rpx;
}
.app-loading--section .app-loading__seal {
width: 88rpx;
height: 90rpx;
}
.app-loading--section .app-loading__knot {
width: 42rpx;
height: 14rpx;
margin-top: 5rpx;
}
.app-loading--section .app-loading__copy {
margin-top: 14rpx;
font-size: 24rpx;
}
.app-loading--section .app-loading__description {
margin-top: 10rpx;
font-size: 22rpx;
}
@keyframes app-loading-seal-breathe {
0%,
100% {
opacity: 0.82;
transform: scale(0.96);
}
50% {
opacity: 1;
transform: scale(1);
}
}
@keyframes app-loading-knot-breathe {
0%,
100% {
opacity: 0.44;
}
50% {
opacity: 0.76;
}
}
@keyframes app-loading-seal-essential-pulse {
0%,
100% {
opacity: 0.58;
}
50% {
opacity: 1;
}
}
@keyframes app-loading-knot-essential-pulse {
0%,
100% {
opacity: 0.28;
}
50% {
opacity: 0.86;
}
}
@media (prefers-reduced-motion: reduce) {
.app-loading__seal {
animation: app-loading-seal-essential-pulse 1.2s ease-in-out infinite;
transform: none;
}
.app-loading__knot {
animation: app-loading-knot-essential-pulse 1.2s ease-in-out 0.2s infinite;
transform: none;
}
}
</style>
+44 -11
View File
@@ -1,25 +1,55 @@
<!-- 公共组件根页面底部导航仅维护家谱家族我的三项已确认入口及其透明图标 -->
<template>
<view class="app-tabbar">
<view v-for="item in items" :key="item.key" class="tab-item" @click="switchTab(item)">
<image class="tab-icon" :src="active === item.key ? item.activeIcon : item.icon" mode="aspectFit" />
<text :class="['tab-label', { active: active === item.key }]">{{ item.label }}</text>
<view
v-for="item in items"
:key="item.key"
class="tab-item"
@click="switchTab(item)"
>
<image
class="tab-icon"
:src="active === item.key ? item.activeIcon : item.icon"
mode="aspectFit"
/>
<text :class="['tab-label', { active: active === item.key }]">{{
item.label
}}</text>
</view>
</view>
</template>
<script setup>
const props = defineProps({ active: { type: String, required: true } })
const props = defineProps({ active: { type: String, required: true } });
const items = [
{ key: 'genealogy', label: '家谱', path: '/pages/genealogy/g01-my-genealogies', icon: '/static/assets/foundation/transparent/tab-genealogy.png', activeIcon: '/static/assets/foundation/transparent/tab-genealogy-active.png' },
{ key: 'family', label: '家族', path: '/pages/family/f01-family-feed', icon: '/static/assets/foundation/transparent/tab-family.png', activeIcon: '/static/assets/foundation/transparent/tab-family-active.png' },
{ key: 'profile', label: '我的', path: '/pages/profile/m01-profile-home', icon: '/static/assets/foundation/transparent/tab-profile.png', activeIcon: '/static/assets/foundation/transparent/tab-profile-active.png' }
]
{
key: "genealogy",
label: "家谱",
path: "/pages/genealogy/g01-my-genealogies",
icon: "/static/assets/foundation/transparent/tab-genealogy.png",
activeIcon:
"/static/assets/foundation/transparent/tab-genealogy-active.png",
},
{
key: "family",
label: "家族",
path: "/pages/family/f01-family-feed",
icon: "/static/assets/foundation/transparent/tab-family.png",
activeIcon: "/static/assets/foundation/transparent/tab-family-active.png",
},
{
key: "profile",
label: "我的",
path: "/pages/profile/m01-profile-home",
icon: "/static/assets/foundation/transparent/tab-profile.png",
activeIcon: "/static/assets/foundation/transparent/tab-profile-active.png",
},
];
const switchTab = (item) => {
if (item.key !== props.active) uni.reLaunch({ url: item.path })
}
if (item.key !== props.active) uni.reLaunch({ url: item.path });
};
</script>
<style scoped lang="scss">
@@ -61,5 +91,8 @@ const switchTab = (item) => {
line-height: 1.2;
}
.tab-label.active { color: $brand-red; font-weight: 700; }
.tab-label.active {
color: $brand-red;
font-weight: 700;
}
</style>
+46
View File
@@ -0,0 +1,46 @@
<template>
<view v-if="visible" class="app-toast" aria-live="polite">
<text class="app-toast__copy">{{ message }}</text>
</view>
</template>
<script setup>
defineProps({
visible: { type: Boolean, default: false },
message: { type: String, default: "" },
});
</script>
<style scoped lang="scss">
.app-toast {
position: fixed;
z-index: 100;
top: calc(env(safe-area-inset-top) + 24rpx);
left: 50%;
display: flex;
width: 590rpx;
max-width: calc(100vw - 48rpx);
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;
transform: translateX(-50%);
pointer-events: none;
}
.app-toast__copy {
position: relative;
z-index: 1;
padding: 14rpx 30rpx;
color: #5c4330;
font-size: 25rpx;
font-weight: 500;
line-height: 1.45;
text-align: center;
}
</style>
+21 -9
View File
@@ -34,6 +34,9 @@
</view>
</view>
<view class="card-detail-row">
<view class="card-updated">
<text>更新于 {{ genealogy.updatedAt }}</text>
</view>
<view class="card-metas">
<view class="card-meta-item">
<image
@@ -52,9 +55,6 @@
<text class="card-meta">{{ genealogy.memberCount }} 位成员</text>
</view>
</view>
<view class="card-updated">
<text>更新于 {{ genealogy.updatedAt }}</text>
</view>
</view>
</view>
</view>
@@ -73,7 +73,7 @@ defineEmits(["select"]);
.genealogy-card {
position: relative;
display: flex;
min-height: 156rpx;
min-height: 178rpx;
align-items: center;
padding: 18rpx 24rpx;
box-sizing: border-box;
@@ -177,25 +177,34 @@ defineEmits(["select"]);
.card-detail-row {
display: flex;
min-width: 0;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
margin-top: 12rpx;
margin-top: 8rpx;
}
.card-metas {
display: flex;
min-width: 0;
width: 100%;
align-items: center;
margin-top: 2rpx;
}
.card-meta-item {
display: flex;
min-width: 0;
align-items: center;
margin-right: 14rpx;
margin-right: 0;
}
.card-meta-item + .card-meta-item {
margin-left: 18rpx;
}
.card-meta-icon {
width: 28rpx;
height: 28rpx;
width: 46rpx;
height: 46rpx;
flex: 0 0 auto;
margin-right: 6rpx;
opacity: 1;
filter: saturate(1.35) brightness(0.82) contrast(1.15);
}
.card-meta {
color: #62584c;
@@ -204,9 +213,12 @@ defineEmits(["select"]);
}
.card-updated {
display: flex;
width: 100%;
flex: 0 0 auto;
align-items: center;
margin-left: 10rpx;
justify-content: flex-end;
margin-top: 0;
margin-left: 0;
color: #62584c;
font-size: 24rpx;
font-weight: 500;
+30
View File
@@ -0,0 +1,30 @@
<template>
<view class="genealogy-page-background">
<image
class="genealogy-page-background__art"
src="/static/assets/modules/genealogy/opaque/genealogy-page-background-long.png"
mode="widthFix"
/>
</view>
</template>
<style scoped>
.genealogy-page-background {
position: fixed;
inset: 0;
z-index: 0;
overflow: hidden;
background: #e7ded1;
pointer-events: none;
}
.genealogy-page-background__art {
position: absolute;
bottom: 0;
right: 0;
left: 0;
display: block;
width: 100%;
opacity: 0.28;
}
</style>
+65
View File
@@ -0,0 +1,65 @@
<template>
<view
class="module-page-background"
:class="`module-page-background--${module}`"
aria-hidden="true"
>
<image
class="module-page-background__image"
:src="source"
mode="widthFix"
/>
</view>
</template>
<script setup>
import { computed } from "vue";
const props = defineProps({
module: { type: String, required: true },
});
const sources = {
tree: "/static/assets/modules/tree/opaque/tree-page-background-long.png",
family:
"/static/assets/modules/family/opaque/family-page-background-long.png",
records:
"/static/assets/modules/records/opaque/records-page-background-long.png",
notification:
"/static/assets/modules/notification/opaque/notification-page-background-long.png",
profile:
"/static/assets/modules/profile/opaque/profile-page-background-long.png",
};
const source = computed(() => sources[props.module] || sources.profile);
</script>
<style scoped lang="scss">
.module-page-background {
position: fixed;
z-index: 0;
inset: 0;
overflow: hidden;
background: #f5eee2;
pointer-events: none;
}
.module-page-background__image {
position: absolute;
right: 0;
bottom: 0;
left: 0;
width: 100%;
opacity: 0.36;
}
.module-page-background--family .module-page-background__image {
opacity: 0.32;
}
.module-page-background--records .module-page-background__image {
opacity: 0.3;
}
.module-page-background--notification .module-page-background__image {
opacity: 0.28;
}
.module-page-background--profile .module-page-background__image {
opacity: 0.3;
}
</style>
+88 -24
View File
@@ -2,25 +2,58 @@
<template>
<view class="page-header" :class="{ 'page-header--root': root }">
<!-- 根页头部使用不透明朱砂底图与透明祠堂线稿两层完整资产 -->
<image v-if="root" class="header-texture" src="/static/assets/foundation/opaque/root-header-cinnabar.jpg" mode="scaleToFill" />
<image v-if="root" class="header-hall" src="/static/assets/foundation/transparent/root-header-hall.png" mode="aspectFit" />
<image
v-if="root"
class="header-texture"
src="/static/assets/foundation/opaque/root-header-cinnabar.jpg"
mode="scaleToFill"
/>
<image
v-if="root"
class="header-hall"
src="/static/assets/foundation/transparent/root-header-hall.png"
mode="aspectFit"
/>
<view class="header-side header-side--left">
<view v-if="root" class="header-icon-button" @click="$emit('brand')">
<image class="header-logo" src="/static/assets/foundation/transparent/brand-seal.png" mode="aspectFit" />
<image
class="header-logo"
src="/static/assets/foundation/transparent/brand-seal.png"
mode="aspectFit"
/>
</view>
<view v-else class="header-back" hover-class="header-back--pressed" @click="goBack">
<image class="header-back__icon" src="/static/assets/foundation/transparent/chevron-right.png" mode="aspectFit" />
<view
v-else
class="header-back"
hover-class="header-back--pressed"
@click="goBack"
>
<image
class="header-back__icon"
src="/static/assets/foundation/transparent/chevron-right.png"
mode="aspectFit"
/>
</view>
</view>
<text class="header-title">{{ title }}</text>
<view class="header-side header-side--right">
<view v-if="root" class="header-icon-button header-notice" @click="$emit('notice')">
<image class="header-notice-icon" src="/static/assets/foundation/transparent/notice.png" mode="aspectFit" />
<view
v-if="root"
class="header-icon-button header-notice"
@click="$emit('notice')"
>
<image
class="header-notice-icon"
src="/static/assets/foundation/transparent/notice.png"
mode="aspectFit"
/>
<view v-if="unreadCount > 0" class="notice-dot"></view>
</view>
<view v-else class="header-action" @click="$emit('action')">{{ action }}</view>
<view v-else class="header-action" @click="$emit('action')">{{
action
}}</view>
</view>
</view>
</template>
@@ -28,12 +61,12 @@
<script setup>
const props = defineProps({
title: { type: String, required: true },
action: { type: String, default: '' },
action: { type: String, default: "" },
root: { type: Boolean, default: false },
unreadCount: { type: Number, default: 0 }
})
unreadCount: { type: Number, default: 0 },
});
const goBack = () => uni.navigateBack()
const goBack = () => uni.navigateBack();
</script>
<style scoped lang="scss">
@@ -75,12 +108,15 @@ const goBack = () => uni.navigateBack()
z-index: 1;
width: 476rpx;
height: 166rpx;
opacity: .29;
opacity: 0.29;
pointer-events: none;
}
.page-header--root .header-side,
.page-header--root .header-title { position: relative; z-index: 2; }
.page-header--root .header-title {
position: relative;
z-index: 2;
}
.header-side {
display: flex;
@@ -89,8 +125,12 @@ const goBack = () => uni.navigateBack()
align-items: center;
}
.header-side--left { justify-content: flex-start; }
.header-side--right { justify-content: flex-end; }
.header-side--left {
justify-content: flex-start;
}
.header-side--right {
justify-content: flex-end;
}
.header-icon-button {
position: relative;
@@ -101,8 +141,14 @@ const goBack = () => uni.navigateBack()
justify-content: center;
}
.header-logo { width: 66rpx; height: 66rpx; }
.header-notice-icon { width: 48rpx; height: 48rpx; }
.header-logo {
width: 66rpx;
height: 66rpx;
}
.header-notice-icon {
width: 48rpx;
height: 48rpx;
}
.notice-dot {
position: absolute;
@@ -120,10 +166,26 @@ const goBack = () => uni.navigateBack()
font-size: 27rpx;
}
.header-back { display: flex; width: 88rpx; height: 88rpx; align-items: center; justify-content: flex-start; }
.header-back__icon { width: 42rpx; height: 42rpx; filter: brightness(0) invert(1); transform: scaleX(-1); }
.header-back--pressed { opacity: .62; }
.header-action { color: #ffe3a7; text-align: right; }
.header-back {
display: flex;
width: 88rpx;
height: 88rpx;
align-items: center;
justify-content: flex-start;
}
.header-back__icon {
width: 42rpx;
height: 42rpx;
filter: brightness(0) invert(1);
transform: scaleX(-1);
}
.header-back--pressed {
opacity: 0.62;
}
.header-action {
color: #ffe3a7;
text-align: right;
}
.header-title {
flex: 1;
@@ -140,7 +202,7 @@ const goBack = () => uni.navigateBack()
.page-header--root .header-title {
color: #ffe3a7;
font-family: 'STKaiti', 'KaiTi', serif;
font-family: "STKaiti", "KaiTi", serif;
font-size: 48rpx;
letter-spacing: 5rpx;
transform: translateY(2rpx);
@@ -152,5 +214,7 @@ const goBack = () => uni.navigateBack()
transform: translate(-6rpx, -6rpx);
}
.page-header--root .header-notice { transform: translateY(4rpx); }
.page-header--root .header-notice {
transform: translateY(4rpx);
}
</style>
+294 -87
View File
@@ -1,28 +1,51 @@
<!-- T04-T06 共用成员表单仅复用视觉与交互骨架各页面通过 kind 拥有独立任务和文案 -->
<template>
<view class="member-form-page" :class="{
'form-state--form': formState === 'form',
'form-state--success': formState === 'success',
'form-state--conflict': formState === 'conflict',
'form-state--error': formState === 'error'
}">
<view
class="member-form-page"
:class="{
'form-state--form': formState === 'form',
'form-state--success': formState === 'success',
'form-state--conflict': formState === 'conflict',
'form-state--error': formState === 'error',
}"
>
<ModulePageBackground module="tree" />
<view class="member-form-page__header"><PageHeader :title="config.pageTitle" /></view>
<view class="member-form-page__header"
><PageHeader :title="config.pageTitle"
/></view>
<view class="member-form-panel">
<image class="member-form-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<image
class="member-form-panel__skin"
src="/static/assets/modules/tree/transparent/t01-state-panel.png"
mode="scaleToFill"
/>
<view v-if="formState === 'form'" class="member-form">
<text class="member-form__eyebrow">{{ config.eyebrow }}</text>
<text class="member-form__title">{{ config.title }}</text>
<text class="member-form__copy">{{ config.copy }}</text>
<view v-for="field in config.fields" :key="field.key" class="member-field">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" />
<view
v-for="field in config.fields"
:key="field.key"
class="member-field"
>
<image
src="/static/assets/modules/tree/transparent/t07-search-input-frame.png"
mode="scaleToFill"
/>
<text>{{ field.label }}</text>
<input v-model="form[field.key]" :placeholder="field.placeholder" placeholder-class="member-placeholder" />
<input
v-model="form[field.key]"
:placeholder="field.placeholder"
placeholder-class="member-placeholder"
/>
</view>
<text class="member-form__note">{{ config.note }}</text>
<view class="member-form-action" @click="saveForm">
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" /><text>{{ config.action }}</text>
<image
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="aspectFit"
/><text>{{ config.action }}</text>
</view>
</view>
@@ -32,14 +55,23 @@
<text class="member-form__copy">{{ resultCopy.copy }}</text>
<view v-if="formState === 'conflict'" class="conflict-actions">
<view class="member-form-action" @click="formState = 'form'">
<image src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png" mode="aspectFit" /><text class="member-form-action__secondary">返回核对</text>
<image
src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png"
mode="aspectFit"
/><text class="member-form-action__secondary">返回核对</text>
</view>
<view class="member-form-action" @click="showConflictHelp">
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" /><text>查看冲突</text>
<image
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="aspectFit"
/><text>查看冲突</text>
</view>
</view>
<view v-else class="member-form-action" @click="formState = 'form'">
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" /><text>{{ formState === 'success' ? '继续完善' : '重新填写' }}</text>
<image
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="aspectFit"
/><text>{{ formState === "success" ? "继续完善" : "重新填写" }}</text>
</view>
</view>
</view>
@@ -56,93 +88,268 @@
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import AppDialog from '@/components/AppDialog.vue'
import ModulePageBackground from '@/components/ModulePageBackground.vue'
import PageHeader from '@/components/PageHeader.vue'
import { computed, reactive, ref } from "vue";
import AppDialog from "@/components/AppDialog.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
const props = defineProps({ kind: { type: String, required: true } })
const props = defineProps({ kind: { type: String, required: true } });
const configs = {
add: {
pageTitle: '新增亲属', eyebrow: '补全家族关系', title: '为汤文远添加一位亲属',
copy: '先确认与当前成员的关系,再录入基础身份信息。', action: '保存亲属', note: '保存后将在世系图中显示,可继续补充详细档案。',
pageTitle: "新增亲属",
eyebrow: "补全家族关系",
title: "为汤文远添加一位亲属",
copy: "先确认与当前成员的关系,再录入基础身份信息。",
action: "保存亲属",
note: "保存后将在世系图中显示,可继续补充详细档案。",
fields: [
{ key: 'name', label: '姓名', placeholder: '请输入真实姓名' },
{ key: 'relation', label: '与本人关系', placeholder: '例如:长子、配偶' },
{ key: 'birthDate', label: '出生日期', placeholder: '例如:1992年' },
{ key: 'gender', label: '性别', placeholder: '请选择或输入' }
]
{ key: "name", label: "姓名", placeholder: "请输入真实姓名" },
{ key: "relation", label: "与本人关系", placeholder: "例如:长子、配偶" },
{ key: "birthDate", label: "出生日期", placeholder: "例如:1992年" },
{ key: "gender", label: "性别", placeholder: "请选择或输入" },
],
},
edit: {
pageTitle: '编辑成员', eyebrow: '成员档案维护', title: '完善汤文远的生命记录',
copy: '基础身份用于世系展示,生平信息可在成员档案中继续补充。', action: '保存资料', note: '隐私字段只向本人和有权限的家谱管理员展示。',
pageTitle: "编辑成员",
eyebrow: "成员档案维护",
title: "完善汤文远的生命记录",
copy: "基础身份用于世系展示,生平信息可在成员档案中继续补充。",
action: "保存资料",
note: "隐私字段只向本人和有权限的家谱管理员展示。",
fields: [
{ key: 'name', label: '姓名', placeholder: '汤文远' },
{ key: 'generation', label: '字辈', placeholder: '例如:文' },
{ key: 'birthDate', label: '出生日期', placeholder: '1940年' },
{ key: 'summary', label: '人物简介', placeholder: '简要记录生平' }
]
{ key: "name", label: "姓名", placeholder: "汤文远" },
{ key: "generation", label: "字辈", placeholder: "例如:文" },
{ key: "birthDate", label: "出生日期", placeholder: "1940年" },
{ key: "summary", label: "人物简介", placeholder: "简要记录生平" },
],
},
relation: {
pageTitle: '关系维护', eyebrow: '亲属关系校正', title: '确认两位成员的家族关系',
copy: '调整关系会影响世系位置,保存前请核对成员和关系类型。', action: '保存关系', note: '若形成重复父母、代际倒置等情况,页面会先提示冲突。',
pageTitle: "关系维护",
eyebrow: "亲属关系校正",
title: "确认两位成员的家族关系",
copy: "调整关系会影响世系位置,保存前请核对成员和关系类型。",
action: "保存关系",
note: "若形成重复父母、代际倒置等情况,页面会先提示冲突。",
fields: [
{ key: 'source', label: '当前成员', placeholder: '汤文远' },
{ key: 'target', label: '关联成员', placeholder: '请选择家谱成员' },
{ key: 'relation', label: '关系类型', placeholder: '父子、配偶等' },
{ key: 'effectiveDate', label: '生效日期', placeholder: '选填' }
]
}
}
const config = computed(() => configs[props.kind])
const form = reactive({ name: '', relation: '', birthDate: '', gender: '', generation: '', summary: '', source: '', target: '', effectiveDate: '' })
{ key: "source", label: "当前成员", placeholder: "汤文远" },
{ key: "target", label: "关联成员", placeholder: "请选择家谱成员" },
{ key: "relation", label: "关系类型", placeholder: "父子、配偶等" },
{ key: "effectiveDate", label: "生效日期", placeholder: "选填" },
],
},
};
const config = computed(() => configs[props.kind]);
const form = reactive({
name: "",
relation: "",
birthDate: "",
gender: "",
generation: "",
summary: "",
source: "",
target: "",
effectiveDate: "",
});
const query = (() => {
if (typeof location !== 'undefined') return Object.fromEntries(new URLSearchParams(location.hash.split('?')[1] || ''))
const pages = getCurrentPages()
return pages[pages.length - 1]?.options || {}
})()
const formState = ref(query.state === 'success' ? 'success' : query.state === 'conflict' ? 'conflict' : query.state === 'error' ? 'error' : 'form')
const conflictHelpVisible = ref(false)
const resultCopy = computed(() => ({
success: { eyebrow: '本地预览已更新', title: `${config.value.pageTitle}内容已保存`, copy: '返回世系树后可查看新的展示位置;正式数据将在接口阶段接入。' },
conflict: { eyebrow: '发现关系冲突', title: '这段关系会造成代际矛盾', copy: '目标成员已经存在父级关系,请先核对原关系,再决定是否调整。' },
error: { eyebrow: '内容未保存', title: '暂时无法完成这次操作', copy: '请返回成员档案重新进入,当前填写内容不会写入正式数据。' }
}[formState.value] || {}))
if (typeof location !== "undefined")
return Object.fromEntries(
new URLSearchParams(location.hash.split("?")[1] || ""),
);
const pages = getCurrentPages();
return pages[pages.length - 1]?.options || {};
})();
const formState = ref(
query.state === "success"
? "success"
: query.state === "conflict"
? "conflict"
: query.state === "error"
? "error"
: "form",
);
const conflictHelpVisible = ref(false);
const resultCopy = computed(
() =>
({
success: {
eyebrow: "本地预览已更新",
title: `${config.value.pageTitle}内容已保存`,
copy: "返回世系树后可查看新的展示位置;正式数据将在接口阶段接入。",
},
conflict: {
eyebrow: "发现关系冲突",
title: "这段关系会造成代际矛盾",
copy: "目标成员已经存在父级关系,请先核对原关系,再决定是否调整。",
},
error: {
eyebrow: "内容未保存",
title: "暂时无法完成这次操作",
copy: "请返回成员档案重新进入,当前填写内容不会写入正式数据。",
},
})[formState.value] || {},
);
const saveForm = () => {
if (props.kind === 'relation' && (!form.target.trim() || !form.relation.trim())) {
formState.value = 'conflict'
return
if (
props.kind === "relation" &&
(!form.target.trim() || !form.relation.trim())
) {
formState.value = "conflict";
return;
}
formState.value = 'success'
}
const showConflictHelp = () => { conflictHelpVisible.value = true }
formState.value = "success";
};
const showConflictHelp = () => {
conflictHelpVisible.value = true;
};
</script>
<style scoped lang="scss">
.member-form-page { position: relative; min-height: 100vh; overflow: hidden; background: $paper; }
.member-form-page__header { position: relative; z-index: 3; }
.member-form-panel { position: relative; z-index: 2; width: calc(100% - 32rpx); height: min(640px, calc((100vw - 16px) * 1.48)); margin: 18rpx auto 0; }
.member-form-panel__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.member-form, .member-form-result { position: absolute; inset: 7.5% 8%; }
.member-form__eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
.member-form__title { display: block; margin-top: 10rpx; color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 34rpx; font-weight: 700; }
.member-form__copy { display: block; margin-top: 9rpx; color: $ink-muted; font-size: 20rpx; line-height: 1.5; }
.member-field { position: relative; height: 78rpx; margin-top: 12rpx; }
.member-field image { position: absolute; inset: 0; width: 100%; height: 100%; }
.member-field > text { position: absolute; top: 27rpx; left: 22rpx; z-index: 1; color: $ink; font-size: 21rpx; font-weight: 700; }
.member-field input { position: absolute; top: 0; right: 18rpx; bottom: 0; left: 164rpx; z-index: 1; height: 78rpx; color: $ink; font-size: 21rpx; line-height: 78rpx; }
.member-placeholder { color: #a79884; }
.member-form__note { display: block; margin-top: 14rpx; color: $ink-muted; font-size: 19rpx; line-height: 1.45; text-align: center; }
.member-form-action { position: relative; width: 100%; height: 76rpx; margin-top: 17rpx; }
.member-form-action image { position: absolute; inset: 0; width: 100%; height: 100%; }
.member-form-action text { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; color: #fff9ed; font-size: 24rpx; font-weight: 700; }
.member-form-action .member-form-action__secondary { color: $ink; }
.member-form-result { top: 29%; text-align: center; }
.member-form-result .member-form__eyebrow { text-align: center; }
.member-form-result .member-form__copy { margin-top: 20rpx; }
.member-form-result > .member-form-action { width: 420rpx; max-width: 100%; margin: 32rpx auto 0; }
.conflict-actions { display: flex; gap: 14rpx; margin-top: 30rpx; }
.conflict-actions .member-form-action { width: calc(50% - 7rpx); margin-top: 0; }
@media (min-width: 400px) { .member-form-panel { width: calc(100% - 48rpx); } }
.member-form-page {
position: relative;
min-height: 100vh;
overflow: hidden;
background: $paper;
}
.member-form-page__header {
position: relative;
z-index: 3;
}
.member-form-panel {
position: relative;
z-index: 2;
width: calc(100% - 32rpx);
height: min(640px, calc((100vw - 16px) * 1.48));
margin: 18rpx auto 0;
}
.member-form-panel__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.member-form,
.member-form-result {
position: absolute;
inset: 7.5% 8%;
}
.member-form__eyebrow {
display: block;
color: $brand-red;
font-size: 22rpx;
letter-spacing: 3rpx;
}
.member-form__title {
display: block;
margin-top: 10rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 34rpx;
font-weight: 700;
}
.member-form__copy {
display: block;
margin-top: 9rpx;
color: $ink-muted;
font-size: 24rpx;
line-height: 1.5;
}
.member-field {
position: relative;
height: 78rpx;
margin-top: 12rpx;
}
.member-field image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.member-field > text {
position: absolute;
top: 27rpx;
left: 22rpx;
z-index: 1;
color: $ink;
font-size: 24rpx;
font-weight: 700;
}
.member-field input {
position: absolute;
top: 0;
right: 18rpx;
bottom: 0;
left: 164rpx;
z-index: 1;
height: 78rpx;
color: $ink;
font-size: 24rpx;
line-height: 78rpx;
}
.member-placeholder {
color: #a79884;
}
.member-form__note {
display: block;
margin-top: 14rpx;
color: $ink-muted;
font-size: 22rpx;
line-height: 1.45;
text-align: center;
}
.member-form-action {
position: relative;
width: 100%;
height: 76rpx;
margin-top: 17rpx;
}
.member-form-action image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.member-form-action text {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #fff9ed;
font-size: 24rpx;
font-weight: 700;
}
.member-form-action .member-form-action__secondary {
color: $ink;
}
.member-form-result {
top: 29%;
text-align: center;
}
.member-form-result .member-form__eyebrow {
text-align: center;
}
.member-form-result .member-form__copy {
margin-top: 20rpx;
}
.member-form-result > .member-form-action {
width: 420rpx;
max-width: 100%;
margin: 32rpx auto 0;
}
.conflict-actions {
display: flex;
gap: 14rpx;
margin-top: 30rpx;
}
.conflict-actions .member-form-action {
width: calc(50% - 7rpx);
margin-top: 0;
}
@media (min-width: 400px) {
.member-form-panel {
width: calc(100% - 48rpx);
}
}
</style>
@@ -0,0 +1,32 @@
{
"version": 1,
"width": 1440,
"height": 3600,
"backgrounds": [
{
"module": "tree",
"source": "docs/design/assets/module-backgrounds/masters/tree-page-background-imagegen-source.png",
"output": "static/assets/modules/tree/opaque/tree-page-background-long.png"
},
{
"module": "family",
"source": "docs/design/assets/module-backgrounds/masters/family-page-background-imagegen-source.png",
"output": "static/assets/modules/family/opaque/family-page-background-long.png"
},
{
"module": "records",
"source": "docs/design/assets/module-backgrounds/masters/records-page-background-imagegen-source.png",
"output": "static/assets/modules/records/opaque/records-page-background-long.png"
},
{
"module": "notification",
"source": "docs/design/assets/module-backgrounds/masters/notification-page-background-imagegen-source.png",
"output": "static/assets/modules/notification/opaque/notification-page-background-long.png"
},
{
"module": "profile",
"source": "docs/design/assets/module-backgrounds/masters/profile-page-background-imagegen-source.png",
"output": "static/assets/modules/profile/opaque/profile-page-background-long.png"
}
]
}
+2 -1
View File
@@ -11,7 +11,8 @@
"verify:assets": "node scripts/verify-assets.mjs",
"build:a01-scroll-skins": "node scripts/build-a01-scroll-skins.mjs",
"verify:a01-scroll-skins": "node scripts/verify-a01-scroll-skins.mjs",
"build:g01-background-candidates": "node scripts/build-g01-backgrounds.mjs"
"build:g01-background-candidates": "node scripts/build-g01-backgrounds.mjs",
"build:g01-empty-frame": "node scripts/build-g01-empty-frame.mjs"
},
"dependencies": {
"sharp": "0.34.5"
@@ -0,0 +1,51 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import sharp from 'sharp'
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url))
const projectRoot = path.resolve(scriptDirectory, '..', '..')
const sourcePath = path.join(projectRoot, 'static', 'assets', 'modules', 'genealogy', 'opaque', 'g01-empty-panel.png')
const outputPath = path.join(projectRoot, 'static', 'assets', 'modules', 'genealogy', 'transparent', 'g01-empty-panel-frame.png')
const borderBand = 110
const { data, info } = await sharp(sourcePath)
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true })
for (let y = 0; y < info.height; y += 1) {
for (let x = 0; x < info.width; x += 1) {
const offset = (y * info.width + x) * info.channels
const red = data[offset]
const green = data[offset + 1]
const blue = data[offset + 2]
const alpha = data[offset + 3]
const distanceToEdge = Math.min(x, y, info.width - 1 - x, info.height - 1 - y)
const isWarmGold = red > green
&& green > blue
&& red - green >= 15
&& green - blue >= 12
&& red - blue >= 35
&& red < 245
&& blue < 180
if (distanceToEdge >= borderBand || !isWarmGold) {
data[offset + 3] = 0
continue
}
const edgeAlpha = Math.max(0, Math.min(255, (red - blue - 25) * 6))
data[offset + 3] = Math.min(alpha, edgeAlpha)
}
}
await sharp(data, {
raw: {
width: info.width,
height: info.height,
channels: info.channels
}
}).png().toFile(outputPath)
process.stdout.write(`BUILT ${path.relative(projectRoot, outputPath)}\n`)
@@ -0,0 +1,61 @@
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from PIL import Image
ROOT = Path(__file__).resolve().parents[2]
MANIFEST = ROOT / "design-pipeline/manifests/module-page-backgrounds.json"
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def normalize(source: Path, output: Path, width: int, height: int) -> None:
with Image.open(source) as image:
image = image.convert("RGB")
scale = max(width / image.width, height / image.height)
resized = image.resize(
(round(image.width * scale), round(image.height * scale)),
Image.Resampling.LANCZOS,
)
left = max(0, (resized.width - width) // 2)
top = max(0, (resized.height - height) // 2)
normalized = resized.crop((left, top, left + width, top + height))
output.parent.mkdir(parents=True, exist_ok=True)
normalized.save(output, format="PNG", optimize=True)
def main() -> None:
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
width = int(manifest["width"])
height = int(manifest["height"])
report = []
for item in manifest["backgrounds"]:
source = ROOT / item["source"]
output = ROOT / item["output"]
if not source.is_file():
raise FileNotFoundError(source)
normalize(source, output, width, height)
report.append(
{
"module": item["module"],
"source": item["source"],
"output": item["output"],
"size": [width, height],
"sha256": sha256(output),
}
)
print(json.dumps(report, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
@@ -0,0 +1,46 @@
# 全模块长背景与 A 系列控件交接
## 当前决定
- A 系列现有按钮、弹窗、Toast 和对话框视觉已经由用户确认,作为全局唯一标准。
- G01 是 G 系列风格母体;9 个活动 G 页面继续共用 `GenealogyPageBackground.vue` 与既定 G01 长背景。
- T、F、R、N、M 各自保留模块性格,但共享浅色国风、楷体、宣纸、朱砂、古金与 A 系列控件。
- 本轮按用户要求不生成新的运行截图;页面仍需用户逐页审核和 Android/HBuilderX 复核。
## 公共组件
- `components/AppButton.vue`A 系列主/次卷轴按钮。
- `components/AppDialog.vue`A 系列单/双按钮卷轴对话框。
- `components/AppToast.vue`A 系列卷轴轻提示。
- `components/AppLoading.vue`:项目自定义水墨加载。
- `components/PageHeader.vue`:普通页使用真实返回箭头,根页保留品牌头部。
- `components/ModulePageBackground.vue`:T、F、R、N、M 背景唯一映射入口。
## 可恢复输入与运行资产
| 模块 | ImageGen 原始输入 | APP 运行资产 | SHA-256 |
| --- | --- | --- | --- |
| T | `tree-page-background-imagegen-source.png` | `static/assets/modules/tree/opaque/tree-page-background-long.png` | `b4322e6d8294c1e30d03baa005aa77122dd49257504da2462db74bfe79e44b37` |
| F | `family-page-background-imagegen-source.png` | `static/assets/modules/family/opaque/family-page-background-long.png` | `9c65aa0526d392e6d03e6c0216e9f5f9f9f917aa539e15c045283195882b5fec` |
| R | `records-page-background-imagegen-source.png` | `static/assets/modules/records/opaque/records-page-background-long.png` | `a420ac3791acc13e25ae68f68782e07754c0040b7f9fbde037bf1588f2ff7007` |
| N | `notification-page-background-imagegen-source.png` | `static/assets/modules/notification/opaque/notification-page-background-long.png` | `7f707153f6ecf53183fc360e6a34b4312975babeb5ccb3f36e048f1f64c65ca5` |
| M | `profile-page-background-imagegen-source.png` | `static/assets/modules/profile/opaque/profile-page-background-long.png` | `c5adb199e5773582775f9f0bab1fef62e0948755c9f22aca647d236f8d4ef8b8` |
所有原始输入位于 `docs/design/assets/module-backgrounds/masters/`;运行资产统一为 `1440×3600` RGB PNG。
## 换机重建
```powershell
design-pipeline/.venv/Scripts/python.exe design-pipeline/scripts/build_module_page_backgrounds.py
powershell -NoProfile -ExecutionPolicy Bypass -File tests/global-heritage-visual-system-contract.ps1
```
清单:`design-pipeline/manifests/module-page-backgrounds.json`
## 验证边界
- 当前静态合同和无截图 H5 运行 smoke 已覆盖主要页面状态、按钮事件、导航与自定义弹层。
- 尚未完成 Android/HBuilderX 字体、状态栏、返回手势、软键盘与不同面板亮度复核。
- 五张 `1440×3600` 图片在 4GB Android 上的解码、切页与回收仍需专项检查。
- 当前仅为全线基础样式候选,不表示 52 页已由用户正式验收或冻结。
@@ -0,0 +1,143 @@
# G01 次要文字可读性优化实施计划
> **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:** 提高 G01“加入申请”卡片说明与状态文字在浅色山水背景上的可读性,同时保持原有信息层级、卡片尺寸和滚动结构。
**Architecture:** 样式所有权继续留在 `pages/genealogy/g01-my-genealogies.vue`,不修改共享 `GenealogyCard.vue`。静态视觉契约锁定四类文字的字号、颜色和字重;现有 G01 运行 smoke 与五档截图验证没有重叠、裁切和滚动回归。
**Tech Stack:** uni-app、Vue 3、SCSS、PowerShell 静态契约、Node.js Chrome DevTools Protocol 运行 smoke。
## Global Constraints
- 只修改 G01 申请卡片说明与状态样式,不修改共享家谱卡、28% 公共背景或其他页面。
- 申请说明固定为 `28rpx / 500 / #62584c / 1.4`;状态固定为 `27rpx / 600`,审核中 `#7f4f16`,被拒绝 `#a7160c`,已退出 `#62584c`
- 卡片高度、内边距、相邻 `12rpx` 间距、箭头、文案和点击行为保持不变。
- H5 证据不等于 Android/HBuilderX 已通过,也不等于 G01 整页已验收。
- 不使用多代理或 worktree,不执行 `git add``commit``push``reset``checkout`
---
### Task 1: 用视觉契约锁定可读性样式
**Files:**
- Modify: `tests/g01-visual-contract.ps1`
- Test: `tests/g01-visual-contract.ps1`
**Interfaces:**
- Consumes: G01 的 `.application-record__copy``.application-record__status` 及两个状态修饰类。
- Produces: 已确认字号、颜色、行高和字重的静态合同。
- [ ] **Step 1: 写入失败断言**
在申请卡片间距断言之后加入:
```powershell
if ($page -notmatch '(?s)\.application-record__copy\s*\{[^}]*color:\s*#62584c;[^}]*font-size:\s*28rpx;[^}]*font-weight:\s*500;[^}]*line-height:\s*1\.4;') { throw 'G-01 application descriptions do not use the approved readable style.' }
if ($page -notmatch '(?s)\.application-record__status\s*\{[^}]*color:\s*#7f4f16;[^}]*font-size:\s*27rpx;[^}]*font-weight:\s*600;') { throw 'G-01 application statuses do not use the approved readable base style.' }
if ($page -notmatch '(?s)\.application-record__status--rejected\s*\{[^}]*color:\s*#a7160c;') { throw 'G-01 rejected status must preserve the approved semantic red.' }
if ($page -notmatch '(?s)\.application-record__status--muted\s*\{[^}]*color:\s*#62584c;') { throw 'G-01 muted status does not use the approved readable color.' }
```
- [ ] **Step 2: 运行契约并确认 RED**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
```
Expected: FAIL,错误包含 `application descriptions do not use the approved readable style`
---
### Task 2: 实施最小样式修改
**Files:**
- Modify: `pages/genealogy/g01-my-genealogies.vue`
- Test: `tests/g01-visual-contract.ps1`
**Interfaces:**
- Consumes: Task 1 的四组视觉契约。
- Produces: 仅 G01 申请卡片生效的增强可读性样式。
- [ ] **Step 1: 替换申请文字样式**
```scss
.application-record__status {
flex: 0 0 auto;
margin-left: 16rpx;
color: #7f4f16;
font-size: 27rpx;
font-weight: 600;
}
.application-record__status--rejected { color: #a7160c; }
.application-record__status--muted { color: #62584c; }
.application-record__copy {
display: block;
margin-top: 10rpx;
color: #62584c;
font-size: 28rpx;
font-weight: 500;
line-height: 1.4;
}
```
- [ ] **Step 2: 运行契约并确认 GREEN**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
```
Expected: `PASS G-01 visual contract`
- [ ] **Step 3: 证明契约可捕获回归**
临时把说明字号恢复为首轮 `26rpx`,运行契约确认失败;随后恢复 `28rpx` 并再次确认通过。临时变化不得保留。
---
### Task 3: 运行、截图和状态更新
**Files:**
- Modify: `docs/交接记录.md`
- Modify: `docs/验收规划.md`
- Modify: `design-qa.md`
- Evidence: `docs/design/screens/runtime/2026-07-16/g01-text-legibility-audit/`
**Interfaces:**
- Consumes: Task 2 已通过静态契约的 G01 页面。
- Produces: 五档响应式证据、修改前后 412×915 对比和准确的交接状态。
- [ ] **Step 1: 运行 G01 静态与运行回归**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-empty-state-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
node tests/g01-empty-state-runtime-smoke.js http://localhost:5173
```
Expected: 三组均输出 `PASS`
- [ ] **Step 2: 生成五档截图并检查布局**
覆盖 `320×568``360×640``360×800``412×915``412×1000`;确认长说明不与状态或箭头重叠,卡片高度和 12rpx 间距不变,底栏与独立滚动正常。
- [ ] **Step 3: 更新状态文档**
写明用户确认并实施 G01 申请次要文字增强;H5 只形成内部候选,Android/HBuilderX 与 G01 整页验收仍未完成。
- [ ] **Step 4: 最终检查**
Run:
```powershell
git diff --check
git status --short
```
Expected: `git diff --check` 退出码为 0;不覆盖、删除或清理任何已有工作区内容。
@@ -0,0 +1,144 @@
# G 模块公共背景 28% 透明度实施计划
> **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:** 让 9 个活动 G 页面通过共享背景组件统一使用 28% 不透明度的连续长背景画层,同时保持宣纸底色、内容层和现有布局不变。
**Architecture:** `components/GenealogyPageBackground.vue` 继续作为背景显示规则的唯一所有者,只在图片画层增加 `opacity: 0.28``tests/genealogy-shared-background-contract.ps1` 锁定该值及单一所有权,页面级运行 smoke 和截图负责验证布局与可读性没有回归。
**Tech Stack:** uni-app、Vue 3、SCSS/CSS、PowerShell 契约测试、Node.js Chrome DevTools Protocol 运行 smoke。
## Global Constraints
- 只修改共享背景图片画层,不修改背景 PNG、母版、裁切、贴底方式或页面滚动结构。
- G01、G03、G05、G06、G08、G09、G10、G11、G12 统一生效;各页面不得重复定义透明度。
- H5 截图仅作为内部候选;Android/HBuilderX 真机或模拟器仍待复核。
- 不使用多代理、worktree,不执行 `git add``commit``push``reset``checkout`
- 保留所有已有修改、未跟踪文件、测试、文档和证据。
---
### Task 1: 用共享背景契约锁定 28% 不透明度
**Files:**
- Modify: `tests/genealogy-shared-background-contract.ps1`
- Test: `tests/genealogy-shared-background-contract.ps1`
**Interfaces:**
- Consumes: `.genealogy-page-background__art` 作为共享长背景图片画层。
- Produces: 背景透明度唯一值 `opacity: 0.28` 的仓库契约。
- [ ] **Step 1: 写入会失败的透明度与单一所有权断言**
在组件画层断言之后加入:
```powershell
Assert-Contract ($componentSource -match '(?s)\.genealogy-page-background__art\s*\{[^}]*opacity:\s*0\.28;') 'shared artwork must use the approved 28% opacity'
```
在每个 G 页面循环中加入:
```powershell
Assert-Contract ($pageSource -notmatch '(?s)\.genealogy-page-background__art\s*\{[^}]*opacity:') "$route must not override the shared background opacity"
```
- [ ] **Step 2: 运行契约并确认按预期失败**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1
```
Expected: FAIL,错误包含 `shared artwork must use the approved 28% opacity`
---
### Task 2: 在共享组件实现 28% 画层不透明度
**Files:**
- Modify: `components/GenealogyPageBackground.vue`
- Test: `tests/genealogy-shared-background-contract.ps1`
**Interfaces:**
- Consumes: Task 1 的 `opacity: 0.28` 契约。
- Produces: 9 个活动 G 页面共用的淡化背景,无页面级覆盖。
- [ ] **Step 1: 最小修改共享画层样式**
将画层规则改为:
```css
.genealogy-page-background__art {
position: absolute;
bottom: 0;
right: 0;
left: 0;
display: block;
width: 100%;
opacity: 0.28;
}
```
- [ ] **Step 2: 运行契约并确认通过**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1
```
Expected: `PASS G genealogy shared background contract`
- [ ] **Step 3: 证明契约能捕获回归**
临时把 `opacity: 0.28` 改回无透明度,运行同一契约并确认失败;随后恢复 `opacity: 0.28`,再次运行并确认通过。临时变化不得保留。
---
### Task 3: 运行回归、截图并更新内部状态
**Files:**
- Modify: `docs/交接记录.md`
- Modify: `docs/验收规划.md`
- Modify: `docs/design/G01_列表背景候选与换机重建.md`
- Modify: `design-qa.md`
- Evidence: `docs/design/screens/runtime/2026-07-16/`
**Interfaces:**
- Consumes: Task 2 已通过契约的共享组件。
- Produces: 五档 G01 H5 内部候选证据、其余 G 页面运行回归结果,以及不夸大验收层级的交接记录。
- [ ] **Step 1: 顺序运行 G 页面 smoke,避免调试端口竞争**
Run:
```powershell
node tests/g01-empty-state-runtime-smoke.js http://localhost:5173
node tests/g03-create-flow-runtime-smoke.js http://localhost:5173
node tests/g05-overview-runtime-smoke.js http://localhost:5173
node tests/g06-search-flow-runtime-smoke.js http://localhost:5173
node tests/g08-g10-application-flow-runtime-smoke.js http://localhost:5173
node tests/g11-g12-settings-poems-runtime-smoke.js http://localhost:5173
```
Expected: 六组脚本全部输出 `PASS`,无浏览器异常或 `console.error`
- [ ] **Step 2: 生成并检查 G01 五档截图**
尺寸为 `320×568``360×640``360×800``412×915``412×1000`。截图必须确认:背景比原候选明显变淡;标题、卡片、文字、按钮和底栏未同步变淡;左右不裁剪;内部列表滚动后底栏仍固定。
- [ ] **Step 3: 更新权威状态文档**
文档统一写明:用户确认共享背景画层为 28% 不透明度;当前只有 H5 内部候选证据;G01 及其余 G 页面没有因此获得整页验收;Android/HBuilderX 与 4GB Android 长图性能仍待复核。
- [ ] **Step 4: 最终一致性检查**
Run:
```powershell
git diff --check
git status --short
```
Expected: `git diff --check` 退出码为 0;只报告已有换行符提示,不覆盖或清理工作区中的任何既有修改。
@@ -0,0 +1,83 @@
# G 类型页面 C 背景贴底自适应 Implementation Plan
> **For agentic workers:** 本计划由当前主代理在同一会话内执行;用户明确禁止多代理、worktree、git add、commit、push、reset 和 checkout。
**Goal:** 让现有 C 背景保持完整比例并贴底显示,把山水视觉集中到 G 页面下半区,减少加入申请区域的大块纯色空白。
**Architecture:** 保持 `GenealogyPageBackground.vue` 为 9 个 G 页面的唯一背景入口,不增加图片资产。只把现有图片从 `top: 0` 改为 `bottom: 0`,继续使用 `widthFix` 和满屏宣纸底色。
**Tech Stack:** uni-app、Vue 单文件组件、CSS、PowerShell 契约测试、Chrome CDP 截图。
## Global Constraints
- 只使用现有 `genealogy-page-background.png`,不生成或重做图片。
- 背景完整显示、左右不裁剪、不拉伸。
- 9 个活动 G 页面继续共用一个公共组件。
- 不改变 G01 内容、卡片、交互、路由或固定区/滚动区结构。
- 不使用多代理或 worktree,不执行任何 Git 写操作。
---
### Task 1: 背景贴底契约与最小实现
**Files:**
- Modify: `tests/genealogy-shared-background-contract.ps1`
- Modify: `components/GenealogyPageBackground.vue`
**Interfaces:**
- Consumes: 现有公共背景组件和 C 运行资产。
- Produces: `bottom: 0`、全宽、`widthFix` 的固定背景图层。
- [ ] **Step 1: 写失败契约**
在公共背景契约中要求 `.genealogy-page-background__art` 包含 `bottom: 0`,并禁止 `top: 0`
- [ ] **Step 2: 运行契约确认失败**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1`
Expected: 因当前组件仍使用 `top: 0` 而失败。
- [ ] **Step 3: 最小修改公共组件**
将图片定位从 `top: 0` 改为 `bottom: 0`,不修改资产、宽度、模式、层级或 9 个页面。
- [ ] **Step 4: 运行契约确认通过**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1`
Expected: `PASS G genealogy shared background contract`
### Task 2: 运行截图与回归
**Files:**
- Modify: `design-qa.md`
- Modify: `docs/交接记录.md`
- Modify: `docs/验收规划.md`
- Runtime evidence: `docs/design/screens/runtime/2026-07-16/`
**Interfaces:**
- Consumes: 贴底公共背景组件。
- Produces: 四档 G01 H5 内部候选截图和未验收状态记录。
- [ ] **Step 1: 四档真实截图**
在 320×568、360×640、360×800、412×915 下截取 G01,确认完整画面贴底、上方宣纸区被主要内容覆盖、下方申请区域不再大块空白。
- [ ] **Step 2: 源图与实现同轮对照**
同一比较输入内打开 C 源图与 412×915 实现截图,确认无裁剪、无拉伸、无横向溢出和文字对比度问题。
- [ ] **Step 3: 回归验证**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1`
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1`
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/full-page-visual-contract.ps1`
Run: G01、G03、G05、G06、G08-G10、G11-G12 现有运行 smoke。
Run: `git diff --check`
Expected: 所有命令退出码为 0;仍明确缺少 Android/HBuilderX 真机或模拟器复核。
@@ -0,0 +1,70 @@
# G 模块旗舰级连续长背景候选 Implementation Plan
> **For agentic workers:** 当前主代理内联执行;用户明确禁止多代理、worktree、git add、commit、push、reset 和 checkout。
**Goal:** 基于现有 C 方向生成连续长背景候选;用户确认后,将其固化为 1536×3840 母版、生成 1440×3600 运行图并接入共享组件。
**Architecture:** 使用内置 ImageGen 编辑现有 C 母版,生成完整不透明长图。候选先存入 `design-pipeline/generated/g01-background/`;用户确认后保留原稿,以锁定 Pillow/Lanczos 归一化母版并生成版本化运行图,由 `GenealogyPageBackground.vue` 单一接入。
**Tech Stack:** Built-in ImageGen、Pillow 12.3.0、现有 G01 资产流水线。
## Global Constraints
- 候选确认前不替换运行资产或页面引用;用户确认后只切换共享组件,不改 9 个页面业务结构。
- 目标母版 1536×3840,运行方向 1440×3600。
- 无文字、Logo、UI、卡片、红色、接缝和重复图案。
- 不删除、覆盖或清理现有母版、候选、截图和未跟踪文件。
---
### Task 1: 生成长图候选
**Files:**
- Read: `docs/design/assets/g01-background/masters/g01-list-background-direction-c-paper-master.png`
- Create: `design-pipeline/generated/g01-background/genealogy-page-background-long-flagship-candidate.png`
**Interfaces:**
- Consumes: C 母版的宣纸色、竹影、水墨山水、亭台和中央低对比构图。
- Produces: 一张未接入页面的长图候选 PNG。
- [x] **Step 1: 查看 C 母版并作为 ImageGen 编辑目标**
确认输入图无文字、无 UI,保持其暖宣纸和灰金水墨语言。
- [x] **Step 2: 使用内置 ImageGen 生成 2.5:1 候选**
提示中明确 2.5:1 连续纵向构图、顶部安全留白、中部低对比、下部山水、无接缝和禁止元素。
- [x] **Step 3: 将生成结果复制到被忽略的候选路径**
不覆盖现有 A/B/C 母版或 `genealogy-page-background.png`
### Task 2: 候选验证与展示
**Files:**
- Inspect: `design-pipeline/generated/g01-background/genealogy-page-background-long-flagship-candidate.png`
**Interfaces:**
- Consumes: ImageGen 候选。
- Produces: 实际尺寸、文件字节数、SHA-256 和视觉检查结果。
- [x] **Step 1: 用 Pillow 检查尺寸、模式和边缘连续性**
确认不透明 RGB/RGBA、实际像素尺寸及没有异常透明边。
- [x] **Step 2: 打开候选检查视觉**
检查文字/Logo 污染、明显横向色带、重复竹枝、中央过密和亭台位置。
- [x] **Step 3: 向用户展示候选**
只报告候选事实,不宣称已接入、已验收或已完成 Android 验证。
### Task 3: 用户确认后的正式接入
- [x] 保留 793×1983 ImageGen 原稿,并生成 1536×3840 sRGB 归一化母版。
- [x] 先新增失败契约,再扩展 manifest、Python 构建器和 Node 包装器为四候选。
- [x] 生成 1440×3600 版本化运行图,更新共享背景组件;旧 C 文件不覆盖、不删除。
- [x] 完成 G01 五档 H5 截图、G 页面运行 smoke、Python/Node/PowerShell 契约和内部设计 QA。
- [ ] Android/HBuilderX 与 4GB Android 图片解码、切页、回收复核。
- [ ] G01 固定区/独立滚动结构继续与用户讨论,不在本次背景接入中擅自实施。
@@ -0,0 +1,154 @@
# G 类型页面共用 C 背景 Implementation Plan
> **For agentic workers:** 本计划由当前主代理在同一会话内执行;用户明确禁止多代理、worktree、git add、commit、push、reset 和 checkout。
**Goal:** 让 9 个活动 G 页面通过一个公共组件完整显示 C 背景,左右不裁剪、不变形,并用宣纸底色延展剩余页面。
**Architecture:** `components/GenealogyPageBackground.vue` 是背景资产路径和显示规则的唯一所有者。各 G 页面移除旧背景图片与私有背景样式,只保留一个公共组件实例;测试从 `pages.json` 推导路由,防止页面清单漂移。
**Tech Stack:** uni-app、Vue 单文件组件、SCSS、PowerShell 契约测试、Node.js 运行冒烟测试。
## Global Constraints
- 仅修改 G 类型页面背景层,不改变页面内容、结构、交互和接口。
- C 背景按宽度等比显示,顶部对齐,左右不裁剪、不变形。
- 高屏和长页面未覆盖区域使用同色宣纸底色延展。
- 不实施 G01 固定区/列表独立滚动。
- 不使用多代理或 worktree,不执行任何 Git 写操作。
- H5 截图仅作为内部候选,不能替代 Android/HBuilderX 复核。
---
### Task 1: 建立公共背景契约
**Files:**
- Create: `tests/genealogy-shared-background-contract.ps1`
- Read: `pages.json`
**Interfaces:**
- Consumes: `pages.json` 中所有 `pages/genealogy/g*.vue` 活动路由。
- Produces: 每个 G 页面必须渲染一次 `<GenealogyPageBackground />` 的仓库契约。
- [ ] **Step 1: 写失败测试**
测试必须断言:公共组件存在;组件唯一引用 `/static/assets/modules/genealogy/opaque/genealogy-page-background.png`;使用 `mode="widthFix"`;包含固定满屏宣纸底层和宽度 `100%` 的图片;每个实际 G 页面恰好使用一次公共组件;页面不得直接引用旧纸纹、旧页脚或公共背景文件。
- [ ] **Step 2: 运行测试确认失败**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1`
Expected: 因公共组件和公共资产尚不存在而失败。
### Task 2: 实现单一公共背景入口
**Files:**
- Create: `components/GenealogyPageBackground.vue`
- Create: `static/assets/modules/genealogy/opaque/genealogy-page-background.png`
- Modify: `design-pipeline/manifests/g01-background-candidates.json`
- Modify: `design-pipeline/tests/g01-background-candidates.test.mjs`
**Interfaces:**
- Consumes: C 母版和可重建生成结果。
- Produces: 无属性、无事件的 `<GenealogyPageBackground />` 视觉组件。
- [ ] **Step 1: 将当前 C 生成结果复制为 G 模块公共运行资产**
源文件为 `design-pipeline/generated/g01-background/g01-list-background-direction-c.png`;目标文件为 `static/assets/modules/genealogy/opaque/genealogy-page-background.png`。复制后验证 SHA-256 仍为 `f14aeed81046d2d5e7ae78b1e62f572689c38261a2cc043142bef9f70200dc5b`
- [ ] **Step 2: 创建最小公共组件**
组件使用固定满屏 `view` 提供宣纸底色,并在顶部放置 `width: 100%``mode="widthFix"` 的 C 背景图片;组件必须 `pointer-events: none` 且位于页面内容下方。
- [ ] **Step 3: 更新流水线清单测试和运行输出所有权**
将 manifest 的 `runtimeOutput` 改为公共资产路径,并让 Node 测试断言新路径,避免 G01 私有资产继续成为正式入口。
### Task 3: 迁移 9 个 G 页面
**Files:**
- Modify: `pages/genealogy/g01-my-genealogies.vue`
- Modify: `pages/genealogy/g03-create-genealogy.vue`
- Modify: `pages/genealogy/g05-genealogy-overview.vue`
- Modify: `pages/genealogy/g06-search-genealogies.vue`
- Modify: `pages/genealogy/g08-join-application.vue`
- Modify: `pages/genealogy/g09-my-applications.vue`
- Modify: `pages/genealogy/g10-application-review.vue`
- Modify: `pages/genealogy/g11-genealogy-settings.vue`
- Modify: `pages/genealogy/g12-generation-poems.vue`
**Interfaces:**
- Consumes: `<GenealogyPageBackground />`
- Produces: 每个 G 页面一个公共背景实例,页面内容和交互保持原样。
- [ ] **Step 1: 替换模板背景层**
删除各页开头的旧纸纹和页脚山水图片,或 G01 的私有 C 背景图片,原位置统一放置 `<GenealogyPageBackground />`
- [ ] **Step 2: 删除仅服务于旧背景的样式**
删除 9 个页面中对应 `__paper``__footer``page-paper-texture``page-footer-landscape``page-background` 规则;不改相邻内容样式。
- [ ] **Step 3: 运行公共背景契约确认通过**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1`
Expected: `PASS G genealogy shared background contract`
### Task 4: 更新既有契约与权威资料
**Files:**
- Modify: `tests/g01-visual-contract.ps1`
- Modify: `docs/交接记录.md`
- Modify: `docs/验收规划.md`
- Modify: `docs/design/P00_页面结构与资产清单.md`
- Modify: `docs/design/设计资产生产流水线规范.md`
- Modify: `docs/design/G01_列表背景候选与换机重建.md`
- Modify: `design-qa.md`
**Interfaces:**
- Consumes: 公共组件、公共资产路径和用户本轮决定。
- Produces: 文档与实际代码一致的未验收状态说明。
- [ ] **Step 1: 更新 G01 视觉契约**
契约改为检查公共组件和公共资产 SHA-256,不再要求 G01 私有资产路径。
- [ ] **Step 2: 更新权威资料**
记录 C 已成为 9 个 G 页面公共背景;明确完整显示和宣纸延展规则,同时保留“G01 未整页验收、其他 G 页面未逐页截图复核、Android 复核缺失”。
### Task 5: 验证和截图审核
**Files:**
- Runtime evidence only: `docs/design/screens/runtime/2026-07-16/`
**Interfaces:**
- Consumes: H5 运行页和四个目标视口。
- Produces: 仅供内部审核的运行截图与测试证据。
- [ ] **Step 1: 运行聚焦验证**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1`
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1`
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-empty-state-contract.ps1`
Run: `node --test design-pipeline/tests/g01-background-candidates.test.mjs`
Run: `design-pipeline/.venv/Scripts/python.exe -m unittest design-pipeline/tests/test_build_g01_backgrounds.py -v`
Run: `node tests/g01-empty-state-runtime-smoke.js`
Run: `git diff --check`
Expected: 所有测试退出码为 0`git diff --check` 无空白错误。
- [ ] **Step 2: 四档 H5 截图复核 G01**
在 320×568、360×640、360×800、412×915 下确认完整左右边缘、无纵向拉伸、无横向溢出,图片下方自然过渡到宣纸底色。
- [ ] **Step 3: 报告验证边界**
报告 H5 内部候选结果,不宣称 G01 或其他 G 页面正式验收;保留 Android/HBuilderX 真机或模拟器复核缺口。
@@ -0,0 +1,106 @@
# 全模块国风视觉系统 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:** 以 A 系列已确认控件为全局唯一标准,完成 G、T、F、R、N、M 活动页面的按钮、对话框、提示、返回导航和模块背景统一。
**Architecture:** 新建公共视觉组件持有 A 系列资产,G 页面继续消费现有共享背景,T/F/R/N/M 通过模块背景组件与 `ModulePage` 统一壳消费各自长背景。页面只保留业务状态和模块内容,不再自行定义全局控件外观。
**Tech Stack:** uni-app、Vue 3 `<script setup>`、SCSS、PNG 资产、PowerShell/Node 合同测试。
## Global Constraints
- 不对接接口,不改变路由和数据契约。
- 不使用原生 `uni.showToast``uni.showModal``uni.showLoading``uni.showActionSheet`
- 不生成运行截图;只生成并验证对应视觉资产。
- 不执行任何 Git 写操作,不使用 worktree 或多代理。
- 保留所有已有修改、历史资产、测试和文档。
---
### Task 1: 全局视觉合同
**Files:**
- Create: `tests/global-heritage-visual-system-contract.ps1`
- [ ] 扫描 pages/components,要求无原生提示调用和旧不透明 A01 按钮引用。
- [ ] 约束 PageHeader 返回箭头、公共组件资产路径和六模块背景入口。
- [ ] 运行合同并确认旧代码失败。
### Task 2: A 系列公共控件
**Files:**
- Create: `components/AppButton.vue`
- Create: `components/AppDialog.vue`
- Create: `components/AppToast.vue`
- Create: `components/AppLoading.vue`
- Modify: `components/PageHeader.vue`
- [ ] 实现主/次卷轴按钮和点击状态。
- [ ] 实现单双操作卷轴对话框。
- [ ] 实现卷轴 Toast 与自定义 Loading。
- [ ] 把普通返回文字替换为真实箭头资产。
- [ ] 运行全局视觉合同并确认公共组件部分通过。
### Task 3: 模块长背景资产与页面壳
**Files:**
- Create: `static/assets/modules/tree/opaque/tree-page-background-long.png`
- Create: `static/assets/modules/family/opaque/family-page-background-long.png`
- Create: `static/assets/modules/records/opaque/records-page-background-long.png`
- Create: `static/assets/modules/notification/opaque/notification-page-background-long.png`
- Create: `static/assets/modules/profile/opaque/profile-page-background-long.png`
- Create: `components/ModulePageBackground.vue`
- Modify: `components/ModulePage.vue`
- [ ] 生成五张无文字、无 UI、1440×3600 长背景并验证尺寸。
- [ ] 实现模块到背景资产的单一映射。
- [ ] ModulePage 接入背景、A 系列按钮和自定义 Toast。
- [ ] 删除 ModulePage 原生 Toast 与旧背景路径。
### Task 4: G 系列完整迁移与 G01 指定可读性
**Files:**
- Modify: `pages/genealogy/*.vue`
- Modify: `components/GenealogyCard.vue`
- [ ] 将 G 页面旧按钮替换为 AppButton。
- [ ] 将 G01/G03/G10/G11/G12 页面自制弹窗迁移为 AppDialog。
- [ ] G01 地区、人数、角色、更新时间加深并使用中等字重。
- [ ] 确认 9 个 G 页面仍消费 GenealogyPageBackground。
### Task 5: T 系列完整迁移
**Files:**
- Modify: `components/tree/TreeMemberForm.vue`
- Modify: `pages/tree/*.vue`
- [ ] 接入 T 模块长背景。
- [ ] 迁移全部旧按钮和冲突对话框。
- [ ] 保留世系树、成员详情、表单和目录的现有结构与业务状态。
### Task 6: F、R、N、M 系列完整迁移
**Files:**
- Modify: `pages/family/f01-family-feed.vue`
- Modify: `pages/family/f02-publish-feed.vue`
- Modify: `pages/notification/n01-message-center.vue`
- Modify: `pages/profile/m01-profile-home.vue`
- Modify through ModulePage: remaining F/R/N/M pages
- [ ] 四个自定义根页面接入模块背景和 A 系列控件。
- [ ] F02、N01 原生 Toast 替换为 AppToast。
- [ ] 其余 29 个 ModulePage 消费页面自动获得统一视觉。
### Task 7: 文档与最终验证
**Files:**
- Modify: `docs/交接记录.md`
- Modify: `docs/验收规划.md`
- Modify: `design-qa.md`
- [ ] 运行全局视觉合同、现有 G/T/F/R/N/M 相关合同和运行 smoke。
- [ ] 运行资产尺寸与 SHA-256 清单检查。
- [ ] 运行 `git diff --check`,分别报告通过项和仍缺 Android/HBuilderX 验证。
- [ ] 文档只写成全模块 H5 视觉候选,不写成逐页正式验收。
@@ -0,0 +1,149 @@
# AppLoading 红金印牌重设计 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task. 本项目明确禁止多代理、worktree、`git add`、`commit`、`push`、`reset` 和 `checkout`。
**Goal:** 将公共 `AppLoading` 的临时细线方框替换为已批准的红金家谱印牌加载视觉,并保持页面级、区域级调用契约不变。
**Architecture:** `components/AppLoading.vue` 继续作为唯一加载视觉所有者,页面只传入 `variant``text``description`。组件复用 foundation 中的 `brand-seal.png``auth-divider-knot.png`,不让各业务页面重复定义加载资产或动画。
**Tech Stack:** uni-app、Vue 3 `<script setup>`、SCSS、PowerShell 契约测试、Chrome DevTools Protocol 截图脚本。
## Global Constraints
- 当前只做浅色国风主题,不接接口。
- 不修改 G01 状态判断、文案、背景、头部或底部导航。
- 不使用 CSS 绘制印章、SVG、emoji 或占位图形;只使用仓库内真实资产。
- 不使用多代理或 worktree,不执行任何 Git 写操作。
- 复用现有资产,不生成重复资产。
- H5 截图仅为内部候选证据;Android/HBuilderX 仍需复核。
---
### Task 1: 收紧公共加载视觉契约
**Files:**
- Modify: `tests/app-loading-contract.ps1`
- Test: `tests/app-loading-contract.ps1`
**Interfaces:**
- Consumes: `components/AppLoading.vue` 的模板与 scoped SCSS。
- Produces: 红金印牌、如意结、双尺寸和减弱动效的稳定契约。
- [ ] **Step 1: 写入失败契约**
`tests/app-loading-contract.ps1` 中将旧方框尺寸与 `1.35s` 断言替换为以下要求:
```powershell
Assert-Match -Content $component -Pattern 'class="app-loading__seal"\s+src="/static/assets/foundation/transparent/brand-seal\.png"' -Message 'AppLoading must render the approved real red-gold seal asset'
Assert-Match -Content $component -Pattern 'class="app-loading__knot"\s+src="/static/assets/foundation/transparent/auth-divider-knot\.png"' -Message 'AppLoading must render the approved real gold knot asset'
Assert-Match -Content $component -Pattern '(?s)\.app-loading--page\s+\.app-loading__seal\s*\{[^}]*width:\s*132rpx;[^}]*height:\s*136rpx;' -Message 'AppLoading page seal must use the approved size'
Assert-Match -Content $component -Pattern '(?s)\.app-loading--section\s+\.app-loading__seal\s*\{[^}]*width:\s*88rpx;[^}]*height:\s*90rpx;' -Message 'AppLoading section seal must use the approved size'
Assert-Match -Content $component -Pattern 'app-loading-seal-breathe\s+1\.6s' -Message 'AppLoading seal must use the approved restrained breathing rhythm'
Assert-Match -Content $component -Pattern '@media\s*\(prefers-reduced-motion:\s*reduce\)' -Message 'AppLoading must respect reduced-motion preferences'
if ($component -match 'app-loading__mark|border:\s*4rpx\s+double') { throw 'AppLoading must not retain the legacy CSS box mark' }
```
- [ ] **Step 2: 运行失败测试**
Run: `powershell -ExecutionPolicy Bypass -File tests/app-loading-contract.ps1`
Expected: FAIL,首个错误为缺少 `brand-seal.png` 真实印牌。
### Task 2: 实现红金印牌公共加载组件
**Files:**
- Modify: `components/AppLoading.vue`
- Test: `tests/app-loading-contract.ps1`
**Interfaces:**
- Consumes: `/static/assets/foundation/transparent/brand-seal.png``/static/assets/foundation/transparent/auth-divider-knot.png`
- Produces: 调用方式不变的 `AppLoading` 页面级与区域级视觉。
- [ ] **Step 1: 替换模板主体**
将旧 `.app-loading__mark` 替换为:
```vue
<view class="app-loading__emblem" aria-hidden="true">
<image class="app-loading__seal" src="/static/assets/foundation/transparent/brand-seal.png" mode="aspectFit" />
<image class="app-loading__knot" src="/static/assets/foundation/transparent/auth-divider-knot.png" mode="aspectFit" />
</view>
```
- [ ] **Step 2: 写入最小视觉实现**
使用以下边界实现,不改变 props:
```scss
.app-loading__emblem { display: flex; flex-direction: column; align-items: center; }
.app-loading__seal { animation: app-loading-seal-breathe 1.6s ease-in-out infinite; }
.app-loading__knot { margin-top: 8rpx; opacity: .72; animation: app-loading-knot-breathe 1.6s ease-in-out infinite; }
.app-loading--page .app-loading__seal { width: 132rpx; height: 136rpx; }
.app-loading--page .app-loading__knot { width: 56rpx; height: 18rpx; }
.app-loading--section .app-loading__seal { width: 88rpx; height: 90rpx; }
.app-loading--section .app-loading__knot { width: 42rpx; height: 14rpx; margin-top: 5rpx; }
@keyframes app-loading-seal-breathe { 0%, 100% { opacity: .82; transform: scale(.96); } 50% { opacity: 1; transform: scale(1); } }
@keyframes app-loading-knot-breathe { 0%, 100% { opacity: .44; } 50% { opacity: .76; } }
@media (prefers-reduced-motion: reduce) { .app-loading__seal, .app-loading__knot { animation: none; opacity: 1; transform: none; } }
```
- [ ] **Step 3: 调整文字间距**
页面级主文案从印牌组合下方 `22rpx` 开始,区域级从 `14rpx` 开始;保留页面级 `30rpx/24rpx` 与区域级 `24rpx/22rpx` 字号。
- [ ] **Step 4: 运行契约测试**
Run: `powershell -ExecutionPolicy Bypass -File tests/app-loading-contract.ps1`
Expected: `APP-LOADING-CONTRACT PASS`
### Task 3: 回归所有接入页面并完成视觉证据
**Files:**
- Verify: `pages/genealogy/g01-my-genealogies.vue`
- Verify: `pages/genealogy/g06-search-genealogies.vue`
- Verify: `tests/g01-loading-state-contract.ps1`
- Verify: `tests/g-series-app-loading-contract.ps1`
- Verify: `tests/module-app-loading-contract.ps1`
- Create: `docs/design/screens/runtime/2026-07-17/03-g01-loading-red-seal-412x900.png`
**Interfaces:**
- Consumes: 更新后的 `AppLoading` 公共视觉。
- Produces: 同一浏览器中的 G01 页面级与 G06 区域级回归证据。
- [ ] **Step 1: 运行加载契约和编译审计**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/g01-loading-state-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/g-series-app-loading-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/module-app-loading-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/compile-audit.ps1
```
Expected: 四项均输出 `PASS`
- [ ] **Step 2: 在同一个可控 Chrome 标签页截图 G01**
Run:
```powershell
node scripts/capture-chrome-page.js "http://localhost:5173/#/pages/genealogy/g01-my-genealogies?state=loading" ".state-panel--loading" "docs/design/screens/runtime/2026-07-17/03-g01-loading-red-seal-412x900.png" 412 900
```
Expected: 输出 `CAPTURED`,页面只有一个 Chrome 标签并显示红金印牌加载状态。
- [ ] **Step 3: 检查四档响应式尺寸**
分别使用 `320×568``360×640``360×800``412×915` 运行同一截图命令,确认印牌、文字和底栏无裁切、无横向滚动。
- [ ] **Step 4: 运行差异检查**
Run: `git diff --check -- components/AppLoading.vue tests/app-loading-contract.ps1`
Expected: 退出码 `0`;允许 Git 报告现有 LF/CRLF 提示,不允许空白错误。
## Execution Choice
按用户最新授权选择 **Inline Execution**:在当前会话使用 `superpowers:executing-plans` 顺序执行。由于用户明确禁止,不创建 worktree、不使用子代理、不执行提交。
@@ -0,0 +1,66 @@
# G01 Empty Frame Transparency 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:** 移除 G01 空状态的不透明面板底,并让透明框内的内容尺度与纵向节奏更饱满。
**Architecture:** 设计流水线从已确认的 `g01-empty-panel.png` 精确提取原双金线和四角纹样,输出透明 `g01-empty-panel-frame.png`。空状态只渲染该透明框;错误状态继续使用原不透明资产。
**Tech Stack:** uni-app、Vue 3、SCSS、PowerShell 合同测试、Chrome H5。
## Global Constraints
- 不生成或覆盖现有资产。
- 不改变空状态按钮、文案和跳转行为。
- 不使用多代理、worktree 或 Git 写操作。
- H5 证据不表示 Android/HBuilderX 或 G01 整页正式验收。
---
### Task 1: 空状态透明框
**Files:**
- Modify: `tests/g01-empty-state-contract.ps1`
- Modify: `pages/genealogy/g01-my-genealogies.vue`
**Interfaces:**
- Consumes: `list-slip-frame.png``.empty-panel``.empty-panel__content`
- Produces: 可重建的 `g01-empty-panel-frame.png``.empty-panel__frame` 透明框层和 `26rpx` `.empty-create-note`
- Produces: `1120rpx` 空状态框、垂直居中的内容组、放大的谱印/标题/说明/操作按钮,以及更明显的纵向节奏。
- [ ] **Step 1: 写入失败契约**
锁定空状态模板不再引用不透明面板、使用 `empty-panel__frame`,并设置 `1120rpx` 框高、垂直居中、`560×124rpx` 主操作与 `26rpx` 提示文字。
- [ ] **Step 2: 验证合同失败**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-empty-state-contract.ps1`
Expected: FAIL,指出空状态仍使用不透明面板或缺少透明框。
- [ ] **Step 3: 实施最小模板与样式修改**
```scss
.empty-panel__frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.empty-create-note { font-size: 26rpx; line-height: 38rpx; }
```
- [ ] **Step 4: 验证**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-empty-state-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/compile-audit.ps1
node tests/g01-empty-state-runtime-smoke.js http://localhost:5173
```
Expected: 全部 PASS。
@@ -0,0 +1,49 @@
# G01 读取失败状态重设计 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans`. 本项目禁止多代理、worktree 和所有 Git 写操作。
**Goal:** 让 G01 读取失败状态与已通过的空状态、加载状态共享同一红金国风视觉体系。
**Architecture:** G01 页面继续拥有错误状态结构与 `retryLoad` 行为;视觉只消费现有透明金框、foundation 印牌、分隔线和 A 系列按钮资产,不创建新的重复资产。
**Tech Stack:** uni-app、Vue 3、SCSS、PowerShell 契约、Chrome DevTools Protocol 截图。
## Global Constraints
- 只修改 G01 失败状态及其测试。
- 保留状态判断和重试行为。
- 不生成已有资产的副本。
- H5 不是 Android 最终验收。
### Task 1: 建立失败视觉契约
**Files:**
- Create: `tests/g01-error-state-contract.ps1`
- Test: `tests/g01-error-state-contract.ps1`
- [ ] 写入断言:错误态必须引用 `g01-empty-panel-frame.png``brand-seal.png``section-divider.png``a01-scroll-primary-v3.png`,必须保留 `retryLoad`,且错误态片段不得引用 `g01-empty-panel.png`
- [ ] 运行 `powershell -ExecutionPolicy Bypass -File tests/g01-error-state-contract.ps1`,预期因现有旧面板而失败。
### Task 2: 实现透明金框错误态
**Files:**
- Modify: `pages/genealogy/g01-my-genealogies.vue`
- Test: `tests/g01-error-state-contract.ps1`
- [ ]`.state-panel--error` 内部替换为透明框、红金印牌、标题、说明、分隔线和原重试按钮。
- [ ] 为错误态设置 `1120rpx` 高度、居中内容、`132rpx × 136rpx` 印牌、`42rpx` 标题、`27rpx` 说明和 `560rpx × 124rpx` 按钮。
- [ ] 运行契约,预期输出 `G01-ERROR-STATE-CONTRACT PASS`
### Task 3: 运行与视觉回归
**Files:**
- Create: `docs/design/screens/runtime/2026-07-17/04-g01-error-after-412x900.png`
- Verify: `tests/compile-audit.ps1`
- [ ] 运行 G01 失败态契约、空状态契约、加载契约和编译审计。
- [ ] 在同一 Chrome 标签页截图 `320×568``360×640``360×800``412×915`
- [ ] 逐张检查标题、按钮、框线、背景和底栏是否裁切,并运行 `git diff --check`
## Execution Choice
用户已授权自主选择最佳方案,采用当前会话 Inline Execution;不等待额外确认。
@@ -0,0 +1,100 @@
# G01 Meta Legibility 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:** 让 G01 列表地区与成员图标清楚,并将更新时间放在第二行、地区与成员放在第三行。
**Architecture:** 保持现有模板和资产不变,只修改 `g01-my-genealogies.vue` 的当前卡样式和 `GenealogyCard.vue` 的共享列表卡样式。由 G01 静态合同锁定精确数值,再用同一 Chrome 页面复核 412×915 与 320×568。
**Tech Stack:** uni-app、Vue 3、SCSS、PowerShell 合同测试、Chrome H5。
## Global Constraints
- 不改变卡框、卡高、谱印、背景、快捷入口和业务行为。
- 320px 小屏继续隐藏更新时间。
- 不使用多代理、worktree 或任何 Git 写操作。
- H5 证据不表示 Android/HBuilderX 或 G01 整页正式验收。
---
### Task 1: 锁定并实现元信息可读性
**Files:**
- Modify: `tests/g01-visual-contract.ps1`
- Modify: `pages/genealogy/g01-my-genealogies.vue`
- Modify: `components/GenealogyCard.vue`
**Interfaces:**
- Consumes: G01 当前卡 `.current-meta``.current-meta-icon`;列表卡 `.card-meta``.card-meta-icon``.card-role``.card-updated`
- Produces: 列表地区与成员小图标 `46rpx`、增强赭金对比;更新时间在右对齐第二行,元信息占满第三行,常规卡高 `178rpx`320px 卡高仍为 `148rpx`
- [ ] **Step 1: 写入失败合同**
`tests/g01-visual-contract.ps1` 增加精确断言:更新时间在 DOM 与视觉顺序中先于元信息并占第二行;元信息占第三行;常规卡高 `178rpx`,小屏卡高 `148rpx`
- [ ] **Step 2: 验证合同因旧数值失败**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1`
Expected: FAIL,指出至少一个新字号或透明度不存在。
- [ ] **Step 3: 实施最小样式调整**
`pages/genealogy/g01-my-genealogies.vue` 设置:
```scss
.current-switch-copy { font-size: 28rpx; }
.current-meta { font-size: 27rpx; }
.current-meta-icon { width: 36rpx; height: 36rpx; opacity: 1; }
@media (max-width: 340px) {
.current-meta-icon { width: 32rpx; height: 32rpx; }
}
```
`components/GenealogyCard.vue` 设置:
```scss
.card-meta-icon {
width: 46rpx;
height: 46rpx;
flex: 0 0 auto;
opacity: 1;
filter: saturate(1.35) brightness(0.82) contrast(1.15);
}
.card-meta-item { margin-right: 0; }
.card-meta-item + .card-meta-item { margin-left: 18rpx; }
.genealogy-card { min-height: 178rpx; }
.card-detail-row { flex-wrap: wrap; }
.card-metas { width: 100%; margin-top: 2rpx; }
.card-updated {
width: 100%;
justify-content: flex-end;
margin-top: 0;
margin-left: 0;
}
.card-meta { font-size: 26rpx; }
.card-updated { font-size: 24rpx; }
.card-role { font-size: 26rpx; }
```
- [ ] **Step 4: 验证合同与编译**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/compile-audit.ps1
```
Expected: 两项均 PASS。
- [ ] **Step 5: 复核真实页面**
在当前 Chrome 中捕获:
```powershell
node scripts/capture-chrome-page.js 'http://localhost:5173/#/pages/genealogy/g01-my-genealogies' '.genealogy-index' 'docs/design/screens/runtime/2026-07-17/G01-meta-legibility-412x915.png' 412 915
node scripts/capture-chrome-page.js 'http://localhost:5173/#/pages/genealogy/g01-my-genealogies' '.genealogy-index' 'docs/design/screens/runtime/2026-07-17/G01-meta-legibility-320x568.png' 320 568
```
Expected: 元信息更清晰,图标更实;无横向溢出,320px 下更新时间仍隐藏。
@@ -0,0 +1,241 @@
# Global Loading System 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:**`AppLoading.vue` 建成页面级/局部级统一加载组件,并接入首批 11 个页面及 G06 搜索结果区域。
**Architecture:** `AppLoading` 单一维护朱砂谱印、呼吸动效、两档尺寸和文案层级;页面只传入 `variant``text``description`,并继续拥有业务状态。按钮提交、列表刷新和上传进度不进入本组件。
**Tech Stack:** uni-app、Vue 3、SCSS、PowerShell 契约测试、Node/Chrome H5 runtime smoke。
## Global Constraints
- `variant` 只允许 `page``section`,默认 `page`
- 页面级尺寸为谱印 `96×96rpx`、印字 `40rpx`、主文案 `30rpx`、说明 `24rpx`、最小高度 `320rpx`
- 局部级尺寸为谱印 `64×64rpx`、印字 `28rpx`、主文案 `24rpx`、说明 `22rpx`、最小高度 `180rpx`
- 不创建全屏遮罩,不接管标题栏、底栏或业务状态。
- 不修改按钮提交、下拉刷新、触底加载和上传流程。
- 不使用多代理、worktree 或 Git 写操作。
- H5 证据不代表 Android/HBuilderX 或页面正式验收。
---
### Task 1: `AppLoading` 双变体合同
**Files:**
- Create: `tests/app-loading-contract.ps1`
- Modify: `components/AppLoading.vue`
- Modify: `tests/g01-loading-state-contract.ps1`
**Interfaces:**
- Consumes: `variant: 'page' | 'section'``text: string``description: string`
- Produces: `.app-loading--page``.app-loading--section``.app-loading__mark``.app-loading__copy``.app-loading__description`
- [ ] **Step 1: 写入失败契约**
契约锁定三个属性、两个变体类、设计尺寸、`1.35s` 动画和可选说明;G01 必须传入 `description`,不再在页面复制辅助说明。
- [ ] **Step 2: 验证失败**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/app-loading-contract.ps1`
Expected: FAIL,指出 `variant``description` 尚未实现。
- [ ] **Step 3: 实施组件**
```vue
<template>
<view class="app-loading" :class="`app-loading--${variant}`">
<view class="app-loading__mark"><text></text></view>
<text class="app-loading__copy">{{ text }}</text>
<text v-if="description" class="app-loading__description">{{ description }}</text>
</view>
</template>
<script setup>
defineProps({
variant: { type: String, default: 'page', validator: (value) => ['page', 'section'].includes(value) },
text: { type: String, default: '正在展开,请稍候…' },
description: { type: String, default: '' }
})
</script>
```
样式按 Global Constraints 的两档精确尺寸实现,动画继续由 `app-loading-breathe` 唯一维护。
- [ ] **Step 4: 更新 G01 消费方式**
```vue
<AppLoading text="正在整理家谱" description="请稍候,家族记忆正在归卷。" />
```
删除 G01 的 `.state-copy--loading` 独立说明节点。
- [ ] **Step 5: 验证通过**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/app-loading-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-loading-state-contract.ps1
```
Expected: 全部 PASS。
---
### Task 2: G 系列接入
**Files:**
- Create: `tests/g-series-app-loading-contract.ps1`
- Modify: `pages/genealogy/g05-genealogy-overview.vue`
- Modify: `pages/genealogy/g06-search-genealogies.vue`
- Modify: `pages/genealogy/g09-my-applications.vue`
- Modify: `pages/genealogy/g10-application-review.vue`
- Modify: `pages/genealogy/g11-genealogy-settings.vue`
- Modify: `pages/genealogy/g12-generation-poems.vue`
**Interfaces:**
- Consumes: Task 1 的 `AppLoading` 属性合同。
- Produces: G05/G09/G10/G11/G12 页面级 Loading 与 G06 局部 Loading。
- [ ] **Step 1: 写入失败契约**
每页必须导入 `AppLoading`G05、G09、G10、G11、G12 在自身 `loading` 分支渲染页面级组件;G06 搜索区渲染 `variant="section"`
- [ ] **Step 2: 验证失败**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/g-series-app-loading-contract.ps1`
Expected: FAIL,指出首个尚未接入的 G 页面。
- [ ] **Step 3: 接入页面级分支**
各页在现有成功/空/失败分支之前加入:
```vue
<AppLoading
v-if="pageState === 'loading'"
text="页面对应的已确认文案"
description="页面对应的辅助说明"
/>
```
其中 `pageState` 分别为 `overviewState``applicationState``reviewState``settingsState``poemState`;保留各页其他分支和操作。
- [ ] **Step 4: 接入 G06 局部加载**
```vue
<AppLoading
v-if="searchState === 'loading'"
variant="section"
text="正在检索公开家谱"
description="请稍候,正在整理匹配结果。"
/>
```
搜索框、模式切换、地区筛选和分隔线继续可见。
- [ ] **Step 5: 验证通过**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g-series-app-loading-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
node tests/module-page-runtime-smoke.js http://localhost:5173
```
Expected: 全部 PASS。
---
### Task 3: T/F/N 页面级接入
**Files:**
- Create: `tests/module-app-loading-contract.ps1`
- Modify: `pages/tree/t01-tree-overview.vue`
- Modify: `pages/tree/t03-member-profile.vue`
- Modify: `pages/tree/t07-member-directory.vue`
- Modify: `pages/family/f01-family-feed.vue`
- Modify: `pages/notification/n01-message-center.vue`
**Interfaces:**
- Consumes: Task 1 的页面级 `AppLoading`
- Produces: 五个页面独立且不会误落入错误卡的 `loading` 分支。
- [ ] **Step 1: 写入失败契约**
锁定五页导入组件并在现有列表/详情/树/错误判断之前单独处理 `loading`;Loading 期间不显示错误操作或发布/审核按钮。
- [ ] **Step 2: 验证失败**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/module-app-loading-contract.ps1`
Expected: FAIL,指出首个仍把 `loading` 当成错误/空状态的页面。
- [ ] **Step 3: 实施五页分支**
统一结构:
```vue
<AppLoading
v-if="pageState === 'loading'"
text="页面对应的已确认文案"
description="请稍候,正在读取页面数据。"
/>
<template v-else-if="pageState === 'ready-state'"></template>
<view v-else>原空错误状态</view>
```
只调整状态分支,不改变数据、入口、卡片和按钮行为。
- [ ] **Step 4: 验证通过**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/module-app-loading-contract.ps1
node tests/t01-tree-state-runtime-smoke.js http://localhost:5173
node tests/t03-t08-member-flow-runtime-smoke.js http://localhost:5173
node tests/root-pages-runtime-smoke.js http://localhost:5173
```
Expected: 全部 PASS。
---
### Task 4: 全量验证与视觉检查
**Files:**
- Verify only: all modified files.
**Interfaces:**
- Consumes: Tasks 13 的加载组件和页面状态分支。
- Produces: H5 内部候选证据,不改变验收状态。
- [ ] **Step 1: 静态与编译验证**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/app-loading-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-loading-state-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g-series-app-loading-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/module-app-loading-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/compile-audit.ps1
```
- [ ] **Step 2: 运行态验证**
Run G01、G 系列、T/F/N 现有 runtime smoke,任何失败均先定位状态分支,不放宽测试。
- [ ] **Step 3: 代表性截图**
捕获 G01 页面级与 G06 局部级在 320×568、360×640、360×800、412×915 的真实 H5 画面,检查无横向溢出、无错误态闪现、固定底栏不遮挡。
- [ ] **Step 4: 工作区检查**
Run: `git diff --check`
Expected: 无空白错误;现有 LF/CRLF 提示单独报告。
@@ -0,0 +1,160 @@
# AppLoading 必要加载动效实施计划
> **执行要求:** 使用 `superpowers:executing-plans` 在当前会话内逐步执行。用户明确禁止多代理、worktree、`git add`、commit、push、reset 和 checkout。
**目标:** 修复 G01 在 `prefers-reduced-motion: reduce` 下加载态完全静止的问题,使印牌和如意结保留清晰的透明度呼吸,同时不产生缩放、旋转或位移。
**架构:** 继续由 `components/AppLoading.vue` 单一维护加载动效,不在 G01 复制样式。正常模式保持现有动画;减少动态效果模式改用独立的透明度关键帧,并强制 `transform: none`。测试先锁定旧行为必须失败,再验证静态契约和当前 Chrome 运行时证据。
**技术栈:** Vue 3、uni-app、SCSS、PowerShell 契约测试、Chrome DevTools Protocol、Node.js。
## 全局约束
- 只修改 G01 加载态直接依赖的公共 `AppLoading`、聚焦测试和当轮内部证据。
- 不改变 `variant``text``description` 组件契约。
- 不改变 G01 状态逻辑、布局、背景、文案、标题栏或底部导航。
- 不新增图片、CSS 图形、JavaScript 定时器或业务接口。
- `prefers-reduced-motion: reduce` 下只允许透明度变化,`transform` 必须为 `none`
- H5 截图只是内部候选;Android/HBuilderX 仍未验证。
- 不执行任何 Git 写操作。
---
### Task 1:修复并验证减少动态效果模式的必要加载反馈
**文件:**
- 修改:`tests/app-loading-contract.ps1`
- 修改:`components/AppLoading.vue`
- 验证:`tests/g01-loading-state-contract.ps1`
- 验证:`tests/g-series-app-loading-contract.ps1`
- 验证:`tests/module-app-loading-contract.ps1`
- 内部证据:`docs/design/screens/runtime/2026-07-19/g01-approval/03-loading-essential-motion-412x915.png`
**接口:**
- 消费:`AppLoading` 现有 `variant``text``description` 属性。
- 产出:正常模式继续使用 `app-loading-seal-breathe``app-loading-knot-breathe`;减少动态效果模式使用 `app-loading-seal-essential-pulse``app-loading-knot-essential-pulse`
- 不增加新的组件属性、事件或页面状态。
- [ ] **Step 1:先补充会失败的公共契约**
`tests/app-loading-contract.ps1` 的现有动画断言之后加入:
```powershell
Assert-Match -Content $component -Pattern '@keyframes\s+app-loading-seal-essential-pulse' -Message 'Reduced-motion AppLoading must retain an essential seal opacity pulse'
Assert-Match -Content $component -Pattern '@keyframes\s+app-loading-knot-essential-pulse' -Message 'Reduced-motion AppLoading must retain an essential knot opacity pulse'
Assert-Match -Content $component -Pattern '(?s)@media\s*\(prefers-reduced-motion:\s*reduce\).*?\.app-loading__seal\s*\{[^}]*animation:\s*app-loading-seal-essential-pulse\s+1\.2s\s+ease-in-out\s+infinite;[^}]*transform:\s*none;' -Message 'Reduced-motion seal must pulse opacity without transform motion'
Assert-Match -Content $component -Pattern '(?s)@media\s*\(prefers-reduced-motion:\s*reduce\).*?\.app-loading__knot\s*\{[^}]*animation:\s*app-loading-knot-essential-pulse\s+1\.2s\s+ease-in-out\s+\.2s\s+infinite;[^}]*transform:\s*none;' -Message 'Reduced-motion knot must pulse opacity without transform motion'
if ($component -match '(?s)@media\s*\(prefers-reduced-motion:\s*reduce\).*?animation:\s*none') {
throw 'Reduced-motion AppLoading must not become completely static'
}
```
- [ ] **Step 2:运行聚焦契约并确认 RED**
运行:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/app-loading-contract.ps1
```
预期:退出码非 0,错误首先指向缺少 `app-loading-seal-essential-pulse`,证明测试捕获的是当前完全静止行为,而不是语法或路径错误。
- [ ] **Step 3:实现最小降动效透明度脉冲**
`components/AppLoading.vue` 中保留现有正常模式关键帧,并把当前媒体查询替换为:
```scss
@keyframes app-loading-seal-essential-pulse {
0%, 100% { opacity: .58; }
50% { opacity: 1; }
}
@keyframes app-loading-knot-essential-pulse {
0%, 100% { opacity: .28; }
50% { opacity: .86; }
}
@media (prefers-reduced-motion: reduce) {
.app-loading__seal {
animation: app-loading-seal-essential-pulse 1.2s ease-in-out infinite;
transform: none;
}
.app-loading__knot {
animation: app-loading-knot-essential-pulse 1.2s ease-in-out .2s infinite;
transform: none;
}
}
```
不得修改模板、属性、尺寸、文案或正常模式关键帧。
- [ ] **Step 4:运行聚焦契约并确认 GREEN**
运行:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/app-loading-contract.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/g01-loading-state-contract.ps1
```
预期:分别输出 `APP-LOADING-CONTRACT PASS``G01-LOADING-STATE-CONTRACT PASS`,退出码均为 0。
- [ ] **Step 5:在当前唯一 Chrome 标签页验证真实动画**
保持 G01 `state=loading` 和 412×915 视口。通过 9222 CDP 在同一次检查中读取:
```js
const seal = document.querySelector('.app-loading__seal')
const knot = document.querySelector('.app-loading__knot')
const sealStyle = getComputedStyle(seal)
const knotStyle = getComputedStyle(knot)
({
reduced: matchMedia('(prefers-reduced-motion: reduce)').matches,
sealAnimation: sealStyle.animationName,
sealTransform: sealStyle.transform,
sealOpacity: sealStyle.opacity,
sealAnimations: seal.getAnimations().length,
knotAnimation: knotStyle.animationName,
knotTransform: knotStyle.transform,
knotOpacity: knotStyle.opacity,
knotAnimations: knot.getAnimations().length
})
```
等待约 420ms 后再次读取。预期:
- `reduced``true`
- 两个 `animationName` 分别以新的 essential pulse 名称开头;Vue scoped CSS 可在运行时追加哈希后缀;
- 两个 `getAnimations().length` 均大于 0
- 两次读取的透明度不同;
- 两个 `transform` 均为 `none`
- 页面仍为 G01 加载态。
- [ ] **Step 6:捕获并人工检查当前候选帧**
使用当前唯一标签页保存:
```text
docs/design/screens/runtime/2026-07-19/g01-approval/03-loading-essential-motion-412x915.png
```
人工检查:标题栏、背景、印牌、如意结、两行文案和底部导航均完整;没有列表、失败态或原生 Loading 串入。静态截图不作为动画运行证明,动画结论以 Step 5 的多时点数据为准。
- [ ] **Step 7:运行受影响合同与空白检查**
运行:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/g-series-app-loading-contract.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/module-app-loading-contract.ps1
git diff --check -- components/AppLoading.vue tests/app-loading-contract.ps1 docs/superpowers/specs/2026-07-19-app-loading-essential-motion-design.md docs/superpowers/plans/2026-07-19-app-loading-essential-motion.md
```
预期:两个合同均输出各自的 `PASS``git diff --check` 退出码为 0。不得把 Android/HBuilderX 写成已验证。
- [ ] **Step 8:回到同一加载态等待用户复核**
保持浏览器停留在修正后的 G01 加载态,不自动切换到失败态。只有用户明确说加载态“通过”后,才继续 G01 下一状态;在此之前 G01 保持 `[!]`
@@ -0,0 +1,200 @@
# F01 Family Feed Baseline Redesign 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. This project explicitly forbids subagents, worktrees, staging, and commits.
**Goal:** Replace F01's application-status cards with a dedicated transparent family-letter content card and make all four shortcuts usable at every supported viewport.
**Architecture:** Keep F01 state and navigation logic unchanged. Add one F01-owned raster card skin under the family module, update only F01 markup/styles, and protect the asset, hierarchy, touch-target, and state contracts with a focused PowerShell test plus existing runtime coverage.
**Tech Stack:** uni-app, Vue 3 `<script setup>`, SCSS, built-in image editing/generation, PowerShell contract tests, Chrome DevTools Protocol runtime smoke.
## Global Constraints
- Modify only F01 and its directly related tests, candidate asset, specs, plan, and screenshots.
- Keep PageHeader, publish routing, family context, module background, AppTabbar, feed states, shortcut labels, and destination routes unchanged.
- Do not use `application-status-card.png` in F01.
- Use a real F01-owned transparent PNG for the family-letter card; do not draw the card with CSS, SVG, text symbols, or placeholders.
- Shortcut and primary-action touch heights must be at least 44 CSS px at 320, 360, and 412 widths.
- Do not run git add, commit, push, reset, or checkout.
- Reuse the existing Chrome project tab and debugging port 9222.
- H5 screenshots are candidate evidence only; Android/HBuilderX remains unverified.
---
### Task 1: Lock the F01 baseline contract
**Files:**
- Create: `tests/f01-module-baseline-contract.ps1`
- Test: `tests/f01-module-baseline-contract.ps1`
**Interfaces:**
- Consumes: UTF-8 source of `pages/family/f01-family-feed.vue`.
- Produces: assertions for the dedicated card asset, no application card, explicit content hierarchy, and 44px shortcut height.
- [ ] **Step 1: Write the failing contract**
Create a test that checks:
```powershell
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$page = Get-Content -LiteralPath (Join-Path $root 'pages/family/f01-family-feed.vue') -Raw -Encoding utf8
function Assert-Match([string]$Pattern, [string]$Message) {
if ($page -notmatch $Pattern) { throw $Message }
}
Assert-Match 'modules/family/transparent/f01-family-letter-card\.png' 'F01 must use its dedicated transparent family-letter card.'
if ($page -match 'application-status-card\.png') { throw 'F01 must not reuse the application status card.' }
Assert-Match 'class="feed-card__title"' 'F01 must expose an explicit feed title hierarchy.'
Assert-Match 'class="feed-card__meta"' 'F01 must expose category and time as secondary metadata.'
Assert-Match 'class="feed-card__summary"' 'F01 must expose feed summary copy.'
Assert-Match 'class="feed-card__author"' 'F01 must expose the author as tertiary information.'
Assert-Match '(?s)\.feed-shortcut\s*\{[^}]*min-height:\s*44px;' 'F01 shortcuts must preserve a 44 CSS px touch height.'
Write-Output 'F01-MODULE-BASELINE-CONTRACT PASS'
```
- [ ] **Step 2: Run and verify RED**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/f01-module-baseline-contract.ps1
```
Expected: FAIL because F01 still uses `application-status-card.png` and has no dedicated class hierarchy.
### Task 2: Create the dedicated family-letter card asset
**Files:**
- Create: `static/assets/modules/family/transparent/f01-family-letter-card.png`
- Preserve: every existing asset under `static/assets/modules/family/` and `static/assets/modules/genealogy/`.
**Interfaces:**
- Consumes: the accepted F01 screenshot and `family-page-background-long.png` as palette/context references.
- Produces: one transparent PNG content-card skin used by list, empty, and error states.
- [ ] **Step 1: Generate one chroma-key candidate with built-in image generation**
Use this exact design intent:
```text
Use case: precise-object-edit / UI asset generation
Asset type: F01 family-feed content-card skin
Primary request: create a wide traditional Chinese family-letter card with a warm ivory rice-paper interior, one restrained thin antique-gold border, subtle family-letter corner details, and no content text.
Composition: wide horizontal card, approximately 3.1:1, designed for a 386×124 CSS px slot.
Palette: match the existing family module background and cinnabar header; keep the card quiet enough for feed text.
Background: perfectly flat #00ff00 chroma key only outside the card.
Constraints: no red vertical line, no top-right status ornament, no buttons, no labels, no icons, no shadows, no white rectangle outside the gold border, no watermark.
```
- [ ] **Step 2: Convert chroma key to alpha and crop transparent bounds**
Use the installed `remove_chroma_key.py` with the real Python launcher at `C:\Users\Rain\AppData\Local\Python\bin\python.exe`, validate transparent corners and visible-card bounds, then save the non-destructive final file at the path above.
### Task 3: Implement F01 content hierarchy and touch targets
**Files:**
- Modify: `pages/family/f01-family-feed.vue`
- Modify: `tests/root-pages-visual-contract.ps1`
- Test: `tests/f01-module-baseline-contract.ps1`
**Interfaces:**
- Consumes: existing `feedState`, `feeds`, `shortcuts`, `toPublish`, `openDetail`, and `openSection` without signature changes.
- Produces: dedicated card markup/classes and 44px shortcut controls.
- [ ] **Step 1: Replace list-card markup**
Each `feed-card` must use the new asset and explicit hierarchy:
```vue
<image class="feed-card__skin" src="/static/assets/modules/family/transparent/f01-family-letter-card.png" mode="scaleToFill" />
<view class="feed-card__copy">
<text class="feed-card__meta">{{ item.tag }} · {{ item.time }}</text>
<text class="feed-card__title">{{ item.title }}</text>
<text class="feed-card__summary">{{ item.content }}</text>
<text class="feed-card__author">发布人{{ item.author }}</text>
</view>
```
- [ ] **Step 2: Replace empty/error panel skin**
Use the same F01 asset for `feed-state-card`, preserving current empty/error copy and retry behavior.
- [ ] **Step 3: Tighten styles**
Set `.feed-shortcut` to `min-height: 44px`, place one shared `a01-scroll-secondary-v3.png` skin behind a four-column equal-width shortcut grid, make the title the largest feed-card text, and keep two equal-height cards between 116 and 132 CSS px at 412px width. Do not add decorative CSS shapes.
- [ ] **Step 4: Update the root-page asset contract**
Replace only F01's required `application-status-card.png` entry with `f01-family-letter-card.png`; leave N01 and M01 requirements unchanged.
- [ ] **Step 5: Run and verify GREEN**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/f01-module-baseline-contract.ps1
```
Expected: `F01-MODULE-BASELINE-CONTRACT PASS`.
### Task 4: Regression and visual verification
**Files:**
- Verify: `pages/family/f01-family-feed.vue`
- Create evidence only under: `docs/design/screens/runtime/2026-07-19/f01-baseline-redesign/`
**Interfaces:**
- Consumes: running H5 service on 5173 and the unique existing Chrome project tab on 9222.
- Produces: regression output, responsive metrics, and accepted F01 screenshots.
- [ ] **Step 1: Run related contracts**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/f01-module-baseline-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/f-series-all-states-visual-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/root-pages-visual-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/module-app-loading-contract.ps1
```
Expected: every command reports PASS.
- [ ] **Step 2: Run runtime coverage**
Run:
```powershell
node tests/root-pages-runtime-smoke.js
node tests/module-series-responsive-runtime-smoke.js
```
Expected: both commands report PASS and only one project tab is used.
- [ ] **Step 3: Capture and inspect F01 states**
Capture list, loading, empty, and error at 412×915. First present only the normal list state for user approval; keep later states hidden until requested.
- [ ] **Step 4: Self-audit responsive sizes**
Capture 320×568, 360×640, 360×800, and 412×915 list state. Verify no overflow, clipped text, card mismatch, shortcut below 44px, or Tab overlap. Restore the tab to 412×915.
- [ ] **Step 5: Run source hygiene checks**
Run:
```powershell
git diff --check
git status --short
```
Expected: no whitespace errors; all pre-existing modified and untracked files remain present. Do not stage or commit anything.
## Self-Review
- Spec coverage: dedicated family card, no white gutter/application semantics, hierarchy, states, 44px shortcuts, responsive sizes, interactions, and evidence limits are covered.
- Placeholder scan: no TBD, TODO, deferred asset decision, or unspecified command remains.
- Interface consistency: F01 state and navigation functions stay unchanged; new class names match the focused contract and implementation steps.
@@ -0,0 +1,156 @@
# G01 添加家谱弹层按钮等宽 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. The user explicitly forbids subagents and worktrees, so execution must remain inline in the current workspace. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 仅在 G01“添加家谱”底部弹层内,让三个现有卷轴按钮以当前红色主按钮的可见宽度为基准左右对齐。
**Architecture:** 保留公共 `AppButton` 及两张现有按钮皮肤不变,在 G01 页面作用域内统一三个按钮根容器的响应式宽高并居中。容器比例按次按钮资产比例设置,使主、次两种 `aspectFit` 皮肤都由宽度约束,从而端点对齐且不横向拉伸。
**Tech Stack:** uni-app、Vue 3 `<script setup>`、SCSS、PowerShell 合同测试、Chrome DevTools Protocol 9222。
## Global Constraints
- 只修改 G01 当前“添加家谱”弹层,不修改公共 `AppButton`、按钮 PNG、其他状态或其他页面。
- 三个按钮使用 `595rpx × 96rpx` 响应式基准,`max-width: 100%`,水平居中。
- 相邻按钮间距保持 `24rpx`;弹层 `780rpx` 最低高度、内容安全居中和最大高度滚动合同保持不变。
- 继续复用 `a01-scroll-primary-v3.png``a01-scroll-secondary-v3.png`,保持 `aspectFit`,不得拉伸、重绘或生成替代资产。
- 复用 9222 上唯一的 `localhost:5173` 项目标签页,不打开第二个浏览器或第二个项目标签页。
- 不使用多代理、worktree;不执行 `git add``commit``push``reset``checkout`;不删除或清理任何现有文件。
- H5 证据仅为内部候选;Android/HBuilderX 仍为未验证项;未经用户明确“通过”不得标记或冻结 G01。
---
### Task 1: 为 G01 局部等宽规则建立合同并最小实现
**Files:**
- Modify: `tests/g01-visual-contract.ps1`
- Modify: `pages/genealogy/g01-my-genealogies.vue`
**Interfaces:**
- Consumes: `AppButton` 根节点已有 `.app-button``.app-button--block` 类;G01 已有 `.add-dialog__actions` 容器。
- Produces: 仅由 `.add-dialog__actions > .app-button` 消费的 `595rpx × 96rpx` 局部尺寸合同。
- [ ] **Step 1: 写入失败合同**
`tests/g01-visual-contract.ps1` 的添加家谱弹层样式断言中加入:
```powershell
if ($page -notmatch '(?s)\.add-dialog__actions\s*>\s*\.app-button\s*\{[^}]*width:\s*595rpx;[^}]*max-width:\s*100%;[^}]*min-height:\s*96rpx;') {
throw 'G-01 add sheet buttons do not keep the approved equal-width geometry.'
}
if ($page -notmatch '(?s)\.add-dialog__actions\s*\{[^}]*align-items:\s*center;') {
throw 'G-01 add sheet buttons are not centered after equal-width sizing.'
}
```
- [ ] **Step 2: 运行合同并确认 RED**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
```
Expected: FAIL,输出包含 `G-01 add sheet buttons do not keep the approved equal-width geometry.`
- [ ] **Step 3: 写入最小页面级实现**
将 G01 当前动作组样式调整为:
```scss
.add-dialog__actions { display: flex; flex-direction: column; align-items: center; margin: 62rpx -32rpx 0; }
.add-dialog__actions > .app-button { width: 595rpx; max-width: 100%; min-height: 96rpx; }
.add-dialog__actions > .app-button + .app-button { margin-top: 24rpx; }
```
不得修改 `components/AppButton.vue`,不得修改两个按钮 PNG。
- [ ] **Step 4: 运行聚焦合同并确认 GREEN**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
```
Expected: `PASS G-01 visual contract`
- [ ] **Step 5: 运行相邻 G01 状态合同**
Run each command independently:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-empty-state-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-loading-state-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-error-state-contract.ps1
git diff --check
```
Expected: 三个状态合同均输出 `PASS``git diff --check` 退出码为 0,允许仅出现工作区既有的 LF/CRLF 警告。
### Task 2: 在唯一 Chrome 标签页验证等宽、响应式与压力状态
**Files:**
- Modify: `design-qa.md`
- Create: `docs/design/screens/runtime/2026-07-19/g01-approval/05-add-dialog-equal-width-412x915.png`
- Create: `docs/design/screens/runtime/2026-07-19/g01-approval/05-add-dialog-equal-width-320x568.png`
- Create: `docs/design/screens/runtime/2026-07-19/g01-approval/05-add-dialog-equal-width-six-buttons-412x915.png`
**Interfaces:**
- Consumes: 9222 上唯一的 `localhost:5173` 页面及 G01 已打开的三按钮弹层。
- Produces: 用户可在原标签页审批的 412×915 三按钮状态,以及等宽、响应式和长内容内部证据。
- [ ] **Step 1: 确认服务和唯一项目标签页**
读取 `http://127.0.0.1:9222/json/list`,只选择 URL 以 `http://localhost:5173` 开头且 `type``page` 的项目页。
Expected: 项目页数量严格为 1;若不是 1,停止,不创建新浏览器或标签页。
- [ ] **Step 2: 验证 412×915 正常三按钮状态**
通过当前 CDP 会话将视口保持为 `412×915`,读取三个 `.add-dialog__actions .app-button` 的矩形和相邻间距,并捕获实现截图。
Expected:
```text
buttonCount = 3
每个按钮根容器宽度约 326.8px
三个按钮 left/right 偏差不超过 0.5px
相邻外层间距约 13.17px
dialogHeight 仍约 430.47px
body topSpace 与 bottomSpace 偏差不超过 1px
horizontalOverflow = false
```
同时目视确认红色与米色皮肤左右卷轴端点对齐、图案未横向变形、文字仍居中。
- [ ] **Step 3: 验证 320×568 响应式状态**
在同一标签页临时切换至 `320×568`,保持三个按钮和当前弹层打开。
Expected: 三个按钮左右端点对齐、完整可见、无横向溢出;标题、说明、关闭入口与按钮互不遮挡。
- [ ] **Step 4: 验证六按钮与超高滚动压力状态**
只通过当前页面运行时临时克隆按钮验证布局,不写入生产数据;先检查六按钮自然增高,再检查 18 按钮最大高度滚动。
Expected: 六按钮左右端点一致且间距稳定;18 按钮初始 `scrollTop = 0`、标题可达,滚动到底后末项可见;无横向溢出。验证后移除所有临时克隆。
- [ ] **Step 5: 恢复用户审批状态**
将同一标签页恢复为 `412×915`、三按钮、弹层打开,确认不存在临时克隆或注入样式。
Expected: 项目标签页数量仍为 1,用户只看到本轮等宽候选。
- [ ] **Step 6: 更新设计质检证据**
`design-qa.md` 当前 G01 添加家谱小节记录:源规格、三档截图、同尺寸对比、计算宽度、端点偏差、间距、居中、压力滚动、交互、控制台与证据限制。
Expected: H5 内部候选不存在可执行 P0/P1/P2 时写 `final result: passed`;同时明确“用户尚未通过”和“Android/HBuilderX 未验证”。
- [ ] **Step 7: 运行最终新鲜验证**
重新运行 Task 1 Step 4-5 的四个合同和 `git diff --check`,并重新读取当前浏览器最终状态。
Expected: 所有合同退出码为 0;唯一项目标签页为 `412×915`、三按钮、弹层打开、按钮等宽、无横向溢出。
@@ -0,0 +1,215 @@
# G01 可伸缩完整底板实施计划
> **执行方式:** 仅允许在当前会话内使用 `superpowers:executing-plans` 逐项执行。用户明确禁止多代理、worktree、`git add`、`commit`、`push`、`reset` 和 `checkout`,因此本计划不包含分派、提交或分支步骤。
**目标:** 将 G01“添加家谱”弹层从“矩形宣纸 + 顶部过渡图”拼接结构改为单张完整 PNG,并只伸缩中间宣纸区域,使三按钮状态与未来长内容状态共用同一底板且顶部装饰不变形。
**架构:** `g01-add-sheet-background-v3.png` 是弹层底板的唯一视觉源。G01 本地使用 CSS `border-image` 的纵向三段伸缩:固定源图顶部 `220px` 装饰安全区、固定底部 `1px` 收口、填充并纵向伸缩中间宣纸区域;标题、关闭图标和现有 `AppButton` 保持独立交互节点。本轮不抽取通用组件,也不修改其他页面。
**技术栈:** uni-app、Vue 3 `<script setup>`、SCSS、CSS `border-image`、现有 `AppButton`、PowerShell 契约测试、Chrome DevTools Protocol 9222。
## 全局约束
- 只修改 G01 当前“添加家谱”弹层、对应契约、规格、计划和内部候选证据。
- 保留所有已修改、未跟踪、候选、母版、截图和资产文件,不删除或覆盖历史文件。
- 不使用多代理或 worktree,不执行任何 Git 写操作。
- 三个按钮继续直接复用现有 `AppButton``a01-scroll-primary-v3.png``a01-scroll-secondary-v3.png`
- 不对接接口,不改变 `applyToJoin``joinByInvite``createGenealogy` 跳转逻辑。
- 只复用当前 9222 Chrome 窗口和唯一项目标签页,不打开第二个项目标签页。
- H5 截图只作为内部候选;Android/HBuilderX 继续标记为未验证。
- 未经用户明确“通过”,不得冻结 G01 或更新为 `[x]`
---
### 任务 1:用视觉契约锁定单张底板和伸缩规则
**文件:**
- 修改:`tests/g01-visual-contract.ps1`
- 测试:`tests/g01-visual-contract.ps1`
**接口:**
- 消费:规格中的唯一底板路径 `static/assets/modules/genealogy/transparent/g01-add-sheet-background-v3.png`
- 产出:模板、资产路径、旧拼接禁令和 `border-image` 切片参数的静态合同。
- [ ] **步骤 1:把旧拼接断言改为完整底板断言**
`$addMarkup` 合同中要求:
```powershell
$requiredAddTokens = @(
'class="add-dialog"',
'class="add-dialog__actions"',
'class="add-dialog__close-icon"'
)
if ($addMarkup -match 'add-dialog__paper|add-dialog__edge|page-paper\.jpg|a01-paper-transition-v1\.png') {
throw 'G-01 add sheet still assembles its background from separate paper and edge layers.'
}
if ($page -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.'
}
```
增加伸缩参数合同:
```powershell
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;') {
throw 'G-01 add sheet does not preserve the complete sheet top while stretching only the paper body.'
}
```
- [ ] **步骤 2:运行契约并确认按预期失败**
运行:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
```
预期:FAIL,错误明确指出仍存在 `add-dialog__paper` / `add-dialog__edge` 拼接结构或缺少完整底板资产引用;不能因语法、编码或文件缺失以外的原因失败。
---
### 任务 2:最小替换为完整可伸缩底板
**文件:**
- 修改:`pages/genealogy/g01-my-genealogies.vue`
- 使用:`static/assets/modules/genealogy/transparent/g01-add-sheet-background-v3.png`
- 测试:`tests/g01-visual-contract.ps1`
**接口:**
- 消费:任务 1 的资产路径和切片参数合同。
- 产出:`.add-dialog` 单背景伸缩结构;现有关闭和三个按钮交互保持不变。
- [ ] **步骤 1:删除模板中的两个拼接背景节点**
将:
```vue
<image class="add-dialog__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill" />
<view class="add-dialog__edge" aria-hidden="true">
<image class="add-dialog__edge-image" src="/static/assets/modules/auth/transparent/a01-paper-transition-v1.png" mode="widthFix" />
</view>
```
从当前添加弹层模板移除。不要删除对应静态资产文件;切换家谱弹层保持原样。
- [ ] **步骤 2:让 `.add-dialog` 直接消费完整底板**
用以下本地样式替换 `.add-dialog__paper``.add-dialog__edge``.add-dialog__edge-image`
```scss
.add-dialog {
position: relative;
width: 100%;
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``.add-dialog__heading`、关闭热区和 `.add-dialog__actions` 的内容结构;若完整底板使标题压到透明区,只允许微调 `.add-dialog__content` 的顶部内边距,不改变按钮资产或全局 `AppButton`
- [ ] **步骤 3:运行 G01 视觉契约确认转绿**
运行:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
```
预期:`PASS G-01 visual contract`
- [ ] **步骤 4:运行已通过状态的回归契约**
运行:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-empty-state-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-loading-state-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-error-state-contract.ps1
git diff --check
```
预期:四项均退出码 `0`;前三项分别输出 PASS`git diff --check` 不报告空白错误。不得修改或放宽既有阈值。
---
### 任务 3:验证三按钮高度与长内容压力高度
**文件:**
- 新增内部候选:`docs/design/screens/runtime/2026-07-19/g01-approval/05-add-dialog-complete-sheet-412x915.png`
- 新增内部候选:`docs/design/screens/runtime/2026-07-19/g01-approval/05-add-dialog-complete-sheet-pressure-412x915.png`
- 修改:`design-qa.md`
**接口:**
- 消费:任务 2 的 `.add-dialog``.add-dialog__actions` 和完整底板。
- 产出:同一资产在基准高度和压力高度下的 H5 运行证据;最终恢复正常三按钮状态供用户审批。
- [ ] **步骤 1:只读确认服务和唯一项目标签页**
运行:
```powershell
Invoke-WebRequest -UseBasicParsing 'http://127.0.0.1:5173' | Select-Object StatusCode
Invoke-RestMethod 'http://127.0.0.1:9222/json/list' | Where-Object { $_.type -eq 'page' -and $_.url -like 'http://localhost:5173*' } | Select-Object id,url
```
预期:5173 返回 `200`;项目 page 恰好一个。不启动 Chrome,不创建新标签页。
- [ ] **步骤 2:验证正常三按钮状态**
通过当前 page 的 CDP
```text
Emulation.setDeviceMetricsOverride -> 412×915
若 .add-dialog 不存在,仅点击当前 .create-action
读取 .add-dialog、.add-dialog__content、三个 .app-button 的矩形
读取 borderImageSource、borderImageSlice、borderImageWidth
Page.captureScreenshot
```
预期:弹层贴底、无横向溢出,标题、说明、关闭和三个按钮全部可见;计算样式引用 `g01-add-sheet-background-v3.png`,切片为 `220 0 1 fill`,顶部曲线和纸面无接缝。
- [ ] **步骤 3:验证长内容只扩展中间纸面**
仅在 CDP 当前文档中临时克隆三个 `.app-button``.add-dialog__actions`,不写入源码;等待布局稳定后读取弹层高度并捕获压力图。
预期:弹层高度大于基准高度;背景仍为同一资产;顶部曲线、如意结和圆肩的可见高度与基准图一致;中间纸面扩展;内容超过安全高度时 `.add-dialog__content` 可纵向滚动;无横向溢出。
- [ ] **步骤 4:恢复正常审批状态并复测交互**
通过同一 page 执行 reload 或移除临时克隆节点,重新打开添加弹层,恢复 `412×915`。依次验证关闭图标关闭、重新打开、点击弹层内部不关闭、点击遮罩关闭、重新打开后列表位置不变。
预期:最终唯一项目标签页停留在正常三按钮“添加家谱”弹层,供用户审批;不展示压力态给用户作为当前状态。
- [ ] **步骤 5:完成内部设计 QA 记录**
`design-qa.md` 追加本轮条目,必须记录:
```markdown
- source visual truth path: docs/design/mockups/2026-07-19/g01-add-dialog-paper-sheet-target.png
- implementation screenshot path: docs/design/screens/runtime/2026-07-19/g01-approval/05-add-dialog-complete-sheet-412x915.png
- pressure screenshot path: docs/design/screens/runtime/2026-07-19/g01-approval/05-add-dialog-complete-sheet-pressure-412x915.png
- viewport: 412×915(另查 320×568
- state: G01 添加家谱,正常三按钮与长内容压力态
- final result: passed 或 blocked
```
只有并排对照后不存在可执行 P0/P1/P2 问题时才写 `final result: passed`;否则保持 `blocked` 并继续只返工当前弹层。
---
## 完成条件
- 运行时底板只有 `g01-add-sheet-background-v3.png` 一个视觉源,不再拼接 `page-paper.jpg``a01-paper-transition-v1.png`
- 三按钮和长内容压力态共用同一底板,顶部装饰不随高度拉伸。
- 当前按钮样式、关闭交互、遮罩交互、Android 返回键和列表位置保持合同。
- G01 四项相关契约与 `git diff --check` 通过。
- 唯一 Chrome 标签页最终显示 412×915 正常三按钮添加弹层。
- G01 仍未冻结、未标 `[x]`Android/HBuilderX 仍明确未验证。
@@ -0,0 +1,216 @@
# N01 消息中心基准页 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:** 将 N01 重做为 N 系列消息中心视觉基准,并完成列表、已读、全部已读、加载、空、失败和审核跳转的 H5 审批状态。
**Architecture:** `pages/notification/n01-message-center.vue` 单独拥有本地演示数据、状态与交互;项目现有 `PageHeader``AppButton``AppLoading``AppToast``ModulePageBackground` 继续作为公共组件。N01 使用本模块独立的透明位图线框,不再引用家谱申请业务卡片。
**Tech Stack:** uni-app、Vue 3 `<script setup>`、SCSS、PowerShell 契约测试、Node CDP 运行时冒烟。
## Global Constraints
- 只处理 N01,不对接接口,不修改 N02 或其他模块页面。
- 不使用子代理或 worktree。
- 不执行 `git add``commit``push``reset``checkout`
- 保留当前全部已修改、未跟踪和忽略文件。
- 所有 Toast、Loading 和按钮使用项目自定义组件。
- 每次只向用户展示一个状态;用户全部明确“通过”后才标记 N01 为 `[x]`
- Android/HBuilderX 仍未验证,H5 截图只作为内部候选证据。
---
### Task 1: 锁定 N01 独立页面契约
**Files:**
- Create: `tests/n01-module-baseline-contract.ps1`
- Modify: `pages/notification/n01-message-center.vue`
**Interfaces:**
- Consumes: `AppButton``AppLoading``AppToast``ModulePageBackground``PageHeader`
- Produces: 可由 `?state=loading|empty|error` 复现的 N01 页面,以及 `.notice-state--list``.notice-state--loading``.notice-state--empty``.notice-state--error` DOM 状态类。
- [ ] **Step 1: 写入失败契约**
```powershell
$source = Get-Content 'pages/notification/n01-message-center.vue' -Raw -Encoding UTF8
$required = @(
'n01-notice-card.png', 'AppLoading', 'AppToast',
'notice-state--loading', 'notice-state--list',
'notice-state--empty', 'notice-state--error',
'未读', '已读', '全部已读', '前往入谱审核'
)
foreach ($token in $required) {
if (-not $source.Contains($token)) { throw "N01 missing contract token: $token" }
}
if ($source.Contains('application-status-card.png')) {
throw 'N01 must not reuse the genealogy application card'
}
Write-Output 'N01-MODULE-BASELINE-CONTRACT PASS'
```
- [ ] **Step 2: 运行契约并确认 RED**
Run: `powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/n01-module-baseline-contract.ps1`
Expected: FAIL,至少指出缺少 `n01-notice-card.png` 或仍引用 `application-status-card.png`
- [ ] **Step 3: 只添加后续实现所需状态类和资产引用,保持测试继续驱动页面实现**
将根节点状态类补齐为:
```vue
<view class="notice-page" :class="`notice-state--${noticeState}`">
```
资产引用统一为:
```vue
<image src="/static/assets/modules/notification/transparent/n01-notice-card.png" mode="scaleToFill" />
```
### Task 2: 制作 N01 透明消息线框资产
**Files:**
- Create: `static/assets/modules/notification/transparent/n01-notice-card.png`
- Preserve: `tmp/imagegen/` 下的生成源图与后处理证据。
**Interfaces:**
- Produces: 约 3:1 比例、四角透明、无文字无底色的古金色消息卡线框 PNG。
- [ ] **Step 1: 用内置 ImageGen 生成可抠色源图**
Prompt:
```text
Use case: ui-mockup
Asset type: scalable message-list card frame for a Chinese genealogy mobile app
Primary request: a restrained traditional Chinese archival card border made only from thin antique-gold linework
Style/medium: refined Song/Yuan inspired ornamental line art, visually consistent with the existing R01 transparent person card
Composition/framing: wide horizontal 3:1 frame, symmetrical corners, quiet center, generous inner text area
Scene/backdrop: perfectly flat solid #00ff00 chroma-key background inside and outside the border
Constraints: no fill panel, no paper texture, no white background, no red marks, no bell icon, no text, no badge, no shadow, no watermark; do not use #00ff00 in the ornament
```
- [ ] **Step 2: 使用 imagegen 技能自带抠色脚本输出透明 PNG**
Run:
```powershell
& 'C:\Users\Rain\AppData\Local\Python\bin\python.exe' 'C:\Users\Rain\.codex\skills\.system\imagegen\scripts\remove_chroma_key.py' --input 'tmp/imagegen/n01-notice-card-chroma.png' --out 'static/assets/modules/notification/transparent/n01-notice-card.png' --auto-key border --soft-matte --transparent-threshold 12 --opaque-threshold 220 --despill
```
- [ ] **Step 3: 检查资产透明度和边缘**
Run:
```powershell
& 'C:\Users\Rain\AppData\Local\Python\bin\python.exe' -c "from PIL import Image; p='static/assets/modules/notification/transparent/n01-notice-card.png'; im=Image.open(p); assert im.mode=='RGBA'; a=im.getchannel('A'); assert a.getpixel((0,0))==0; assert a.getbbox(); print('N01-ASSET-ALPHA PASS', im.size)"
```
Expected: `N01-ASSET-ALPHA PASS`
### Task 3: 实现 N01 列表与审批状态
**Files:**
- Modify: `pages/notification/n01-message-center.vue`
- Test: `tests/n01-module-baseline-contract.ps1`
**Interfaces:**
- `readNotice(item)`:把单条 `item.unread` 设为 `false`
- `markAllRead()`:把全部消息设为已读,并显示“已全部标记为已读”自定义 Toast。
- `restoreList()`:把错误态恢复为列表态。
- `toReview()`:保留 `/pages/genealogy/g10-application-review?genealogyId=...` 路由。
- [ ] **Step 1: 将模板整理为四个明确分支**
```vue
<AppLoading v-if="noticeState === 'loading'" text="正在整理消息" description="请稍候,正在同步家谱申请与家族提醒。" />
<view v-else-if="noticeState === 'list'" class="notice-list">...</view>
<view v-else class="notice-state-card">...</view>
```
列表卡片需使用以下文字层级:
```vue
<text class="notice-card__status" :class="{'is-unread': item.unread}">{{ item.unread ? '未读提醒' : '已读' }} · {{ item.time }}</text>
<text class="notice-card__title">{{ item.title }}</text>
<text class="notice-card__summary">{{ item.content }}</text>
```
- [ ] **Step 2: 统一空态与失败态操作**
```vue
<AppButton
block
:type="noticeState === 'error' ? 'secondary' : 'primary'"
:label="noticeState === 'error' ? '重新查看' : '前往入谱审核'"
@click="noticeState === 'error' ? restoreList() : toReview()"
/>
```
- [ ] **Step 3: 让卡片自适应内容并移除白底来源**
```scss
.notice-content { padding: 24rpx 28rpx 100rpx; }
.notice-list { display: flex; flex-direction: column; gap: 18rpx; }
.notice-card { position: relative; min-height: 220rpx; }
.notice-card__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.notice-card__copy { position: relative; z-index: 1; padding: 34rpx 44rpx; }
.notice-card__status { color: $ink-muted; }
.notice-card__status.is-unread { color: $brand-red; font-weight: 700; }
.notice-card__title { margin-top: 8rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-weight: 700; }
.notice-review-action { margin: 30rpx auto 0; }
```
- [ ] **Step 4: 运行聚焦契约并确认 GREEN**
Run: `powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/n01-module-baseline-contract.ps1`
Expected: `N01-MODULE-BASELINE-CONTRACT PASS`
### Task 4: 运行时验证并进入逐状态审批
**Files:**
- Create: `docs/design/screens/runtime/2026-07-19/n01-review/*.png`
- Modify only after all explicit approvals: `docs/验收规划.md`
**Interfaces:**
- Consumes: 单个现有 Chrome 项目标签页和调试端口 9222。
- Produces: 412×915 的逐状态审批画面,以及四档响应式内部证据。
- [ ] **Step 1: 运行静态和运行时检查**
Run:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/n01-module-baseline-contract.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/root-pages-visual-contract.ps1
node tests/root-pages-runtime-smoke.js
node tests/module-series-responsive-runtime-smoke.js
git diff --check
```
Expected: 所有测试输出 `PASS``git diff --check` 退出码为 0,允许已有行尾提示。
- [ ] **Step 2: 复用同一标签页显示 412×915 正常列表态**
Run:
```powershell
node scripts/capture-chrome-page.js 'http://localhost:5173/#/pages/notification/n01-message-center' '.notice-state--list' 'docs/design/screens/runtime/2026-07-19/n01-review/01-list-412x915.png' 412 915
```
Inspect: 打开保存的截图,确认没有白底块、裁切、错误页面或半加载内容,再等待用户审批。
- [ ] **Step 3: 按顺序展示其余状态**
顺序:单条已读 → 全部已读 Toast → 加载 → 空 → 失败 → 审核跳转。每次只显示一个状态并等待用户确认。
- [ ] **Step 4: 内部检查四档尺寸**
尺寸:`320×568``360×640``360×800``412×915`。发现问题只返工 N01;未发现问题不逐档打断用户。
- [ ] **Step 5: 所有状态明确通过后更新审批记录**
仅把 `docs/验收规划.md` 的 N01 行从 `[~]` 改为 `[x]`,随后重新运行 Task 4 Step 1 的全部验证。
@@ -0,0 +1,116 @@
# R01 People Directory Baseline 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. This project explicitly forbids subagents, worktrees, staging, and commits.
**Goal:** Replace R01's generic application-style list with a dedicated transparent people-directory page that supports local search, empty, and error review states.
**Architecture:** Keep R01 isolated in `pages/records/r01-people-list.vue` so the shared `ModulePage.vue` and every other R page remain unchanged. Reuse existing project components and controls, add one R01-owned raster card skin, and guard the new ownership and interaction contracts with focused tests.
**Tech Stack:** uni-app, Vue 3 `<script setup>`, SCSS, built-in image generation and alpha post-processing, PowerShell contracts, Chrome DevTools Protocol runtime capture.
## Global Constraints
- Modify only R01 and its directly related test, asset, spec, plan, QA note, and screenshots.
- Do not modify `ModulePage.vue`, other R pages, public components, API code, or routes.
- Do not use `application-status-card.png` or CSS/SVG drawings in R01.
- Keep every touch target at least 44 CSS px.
- Do not use subagents, worktrees, git add, commit, push, reset, or checkout.
- Reuse the existing Chrome project tab on port 9222.
- H5 evidence does not prove Android/HBuilderX behavior.
---
### Task 1: Lock the R01 ownership and visual contract
**Files:**
- Create: `tests/r01-module-baseline-contract.ps1`
- Test: `tests/r01-module-baseline-contract.ps1`
**Interfaces:**
- Consumes: UTF-8 source of `pages/records/r01-people-list.vue`.
- Produces: assertions for R01 ownership, card asset, search, state classes, custom feedback, and 44px targets.
- [ ] **Step 1: Write a failing contract**
Assert that R01 contains `r01-person-name-card.png`, `people-search`, `people-state--empty`, `people-state--error`, `AppButton`, `AppToast`, and a `min-height:44px` search action; reject `ModulePage` and `application-status-card.png`.
- [ ] **Step 2: Verify RED**
Run `powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/r01-module-baseline-contract.ps1`.
Expected: FAIL because the current page only renders `ModulePage`.
### Task 2: Create the R01 transparent name-card asset
**Files:**
- Create: `static/assets/modules/records/transparent/r01-person-name-card.png`
**Interfaces:**
- Consumes: R module background palette and the accepted line-only F01 card restraint as visual context.
- Produces: one wide transparent PNG with restrained antique-gold name-card linework and no paper fill, text, status marks, or white background.
- [ ] **Step 1: Generate the raster candidate**
Use built-in image generation for a wide 3.4:1 traditional Chinese archival name-card line frame on flat green chroma outside and inside the frame.
- [ ] **Step 2: Convert chroma and paper pixels to alpha**
Use the real Python launcher and Pillow to preserve only antique-gold linework, crop transparent bounds, save the final asset, and inspect the original-resolution PNG.
### Task 3: Implement the isolated R01 page
**Files:**
- Modify: `pages/records/r01-people-list.vue`
- Test: `tests/r01-module-baseline-contract.ps1`
**Interfaces:**
- Consumes: `PageHeader`, `ModulePageBackground`, `AppButton`, `AppToast`, and the R01 asset.
- Produces: local ready/empty/error/search-no-result rendering and local search over three people.
- [ ] **Step 1: Replace `ModulePage` with R01-owned markup**
Render the records background, page header, existing search-frame asset, filtered cards, state panel, action button, and custom Toast. Do not render a numbered intro block below the header.
- [ ] **Step 2: Add local state and interactions**
Read `state=empty|error` from the current hash/query; filter by name, role, or generation; clear search to restore all records; card clicks navigate to R02; new-person action opens `AppToast`; error action restores ready.
- [ ] **Step 3: Add scoped responsive styles**
Use transparent overlays, equal card sizes, clear hierarchy, no white wrapper backgrounds, no horizontal overflow, and minimum 44px controls.
- [ ] **Step 4: Verify GREEN**
Run the focused contract and require `R01-MODULE-BASELINE-CONTRACT PASS`.
### Task 4: Regression and visual verification
**Files:**
- Verify: `pages/records/r01-people-list.vue`
- Create evidence under: `docs/design/screens/runtime/2026-07-19/r01-baseline-redesign/`
**Interfaces:**
- Consumes: H5 service on 5173 and the unique existing Chrome project tab on 9222.
- Produces: test output, responsive evidence, and one user-visible state at a time.
- [ ] **Step 1: Run related contracts and runtime smoke**
Run the focused contract, R-series visual contract, root runtime smoke, responsive runtime smoke, and `git diff --check`.
- [ ] **Step 2: Capture and compare normal list**
Capture 412×915 and compare against `r01-baseline-audit/01-list-412x915.png`; fix P0/P1/P2 issues before showing it.
- [ ] **Step 3: Self-audit responsive sizes**
Capture 320×568, 360×640, 360×800, and 412×915; confirm no overflow or clipping and restore 412×915.
- [ ] **Step 4: Present states sequentially**
Present normal list, search no result, empty, failure, and new-person Toast one at a time. Do not mark R01 `[x]` until the user explicitly passes the page.
## Self-Review
- Spec coverage: ownership, asset, white-background removal, search, states, interactions, responsive targets, and evidence limits are covered.
- Placeholder scan: no unresolved design or implementation decision remains.
- Interface consistency: R01 owns its local data and state; shared components and other R pages remain unchanged.
@@ -0,0 +1,159 @@
# T07 Search and Member Card Redesign 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. This project explicitly forbids subagents and git commits.
**Goal:** Remove T07 search/card white gutters and rebuild the member list with approved transparent project assets and clearer information hierarchy.
**Architecture:** Keep all route state and interaction logic inside the existing T07 page. Replace only the two incorrect opaque skins with existing transparent G01-approved assets, then tighten T07-scoped markup and styles. Protect the asset and layout contract with the existing PowerShell contract test and verify behavior in the already-open Chrome tab.
**Tech Stack:** uni-app, Vue 3 `<script setup>`, SCSS, PowerShell contract tests, Chrome DevTools Protocol runtime smoke.
## Global Constraints
- Modify only T07 and its directly related test/evidence files.
- Do not modify page header, current-genealogy context, route states, search behavior, or member navigation behavior.
- Use `t07-search-input-frame.png` for search and `list-slip-frame.png` for cards; do not add a generation seal or draw replacements in CSS/SVG.
- Do not use `g06-search-input-wide.png` or `application-status-card.png` in T07.
- Do not run git add, commit, push, reset, or checkout.
- Reuse the existing Chrome project tab; do not open another browser or project tab.
- H5 evidence is only candidate evidence; Android/HBuilderX remains unverified.
---
### Task 1: Lock the transparent-asset contract
**Files:**
- Modify: `tests/t07-module-baseline-contract.ps1`
- Test: `tests/t07-module-baseline-contract.ps1`
**Interfaces:**
- Consumes: raw source of `pages/tree/t07-member-directory.vue`.
- Produces: assertions that T07 uses the approved transparent skins and no longer references the two opaque skins.
- [ ] **Step 1: Write the failing assertions**
Add assertions equivalent to:
```powershell
Assert-Match 'transparent/list-slip-frame\.png' 'T07 search and cards must use the approved transparent frame.'
if ($page -match 'g06-search-input-wide\.png|application-status-card\.png') {
throw 'T07 must not reuse opaque search or application-status skins.'
}
if ($page -match 'row-seal-frame\.png|directory-card__seal') {
throw 'T07 member cards must not repeat generation information in decorative seals.'
}
Assert-Match 'class="directory-card__status"' 'T07 member identity or profile state must use a dedicated status label.'
```
- [ ] **Step 2: Run the contract and verify RED**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/t07-module-baseline-contract.ps1
```
Expected: FAIL because the current page still references `g06-search-input-wide.png` and `application-status-card.png` and has no seal/status structure.
### Task 2: Implement the approved T07 visual structure
**Files:**
- Modify: `pages/tree/t07-member-directory.vue`
- Test: `tests/t07-module-baseline-contract.ps1`
**Interfaces:**
- Consumes: existing `members`, `filteredMembers`, `searchMembers`, `openMember`, and route-state behavior without signature changes.
- Produces: transparent framed search field and compact transparent member cards.
- [ ] **Step 1: Replace the search skin**
Use the approved transparent frame while retaining the existing input/action bindings:
```vue
<image class="directory-search__frame" src="/static/assets/modules/genealogy/transparent/list-slip-frame.png" mode="scaleToFill" />
```
Set the search field to a 44px-or-greater touch height, strengthen placeholder contrast, and keep the page background visible through the frame.
- [ ] **Step 2: Replace member-card markup**
Each `directory-card` must contain:
```vue
<image class="directory-card__frame" src="/static/assets/modules/genealogy/transparent/list-slip-frame.png" mode="scaleToFill" />
<view class="directory-card__copy">
<text class="directory-card__name">{{ item.name }}</text>
<text class="directory-card__meta"> {{ item.generation }} · {{ item.generationName }} · {{ item.branch }}</text>
<text class="directory-card__status">{{ item.note }}</text>
</view>
```
- [ ] **Step 3: Tighten card layout**
Use one consistent card height between 192rpx and 216rpx at 412px width, vertically center the seal/copy, keep the status visually subordinate, and add a max-width 340px media rule that reduces seal size and spacing without hiding the member name or metadata.
- [ ] **Step 4: Run the focused contract and verify GREEN**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/t07-module-baseline-contract.ps1
```
Expected: `T07-MODULE-BASELINE-CONTRACT PASS`.
### Task 3: Regression and runtime visual verification
**Files:**
- Verify: `pages/tree/t07-member-directory.vue`
- Verify: `tests/t07-module-baseline-runtime-smoke.js`
- Create evidence only under: `docs/design/screens/runtime/2026-07-19/t07-redesign/`
**Interfaces:**
- Consumes: already-running H5 service on 5173 and existing Chrome CDP endpoint on 9222.
- Produces: focused regression output, one-tab runtime results, and current-state screenshots.
- [ ] **Step 1: Run focused and related regression tests**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/t07-module-baseline-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/t07-t08-all-states-visual-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/module-app-loading-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/t03-t08-member-flow-contract.ps1
node tests/t07-module-baseline-runtime-smoke.js
```
Expected: every command reports PASS.
- [ ] **Step 2: Inspect the existing 412×915 tab**
Verify all of the following in the same project tab:
- Search frame has no white band outside its border.
- Member cards have no white rectangle or texture break.
- No red application-status line or top-right status ornament remains.
- Three cards have equal width/height and the name is the primary focus.
- Search restores results and each card retains its click target.
- [ ] **Step 3: Verify responsive sizes**
In the same tab, check 320×568, 360×640, 360×800, and 412×915. Expected: no horizontal overflow, no clipped name/metadata, and natural vertical scrolling.
- [ ] **Step 4: Run source hygiene checks**
Run:
```powershell
git diff --check
git status --short
```
Expected: no whitespace errors; all pre-existing modified/untracked files remain present. Do not stage or commit anything.
## Self-Review
- Spec coverage: transparent search, compact member cards, information hierarchy, responsive sizes, behavior preservation, and evidence limits are covered.
- Placeholder scan: no TBD, TODO, deferred implementation, or unspecified asset choice remains.
- Interface consistency: existing state/search/navigation functions remain unchanged; all new class names are defined in Task 2 and asserted in Task 1.
@@ -0,0 +1,40 @@
# M01 我的首页基准页 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:** 将 M01 重做为无整页白底、具有个人谱牒册页辨识度且便于后续增加功能的 M 系列视觉基准。
**Architecture:** `pages/profile/m01-profile-home.vue` 继续单独拥有本地状态与路由;复用项目公共页头、底栏、按钮和背景组件。M01 的透明线框资产存放在 profile 模块自己的命名空间。
**Tech Stack:** uni-app、Vue 3、SCSS、PowerShell 契约测试、Node CDP 运行时冒烟。
## Global Constraints
- 只修改 M01 及其聚焦测试、资产和审批文档。
- 不新增个人中心功能,不对接接口。
- 不使用子代理、worktree 或任何 Git 写操作。
- 每次只展示一个审批状态。
### Task 1: 契约 RED
- [ ] 创建 `tests/m01-module-baseline-contract.ps1`,要求 M01 使用 `brand-seal.png``auth-divider-knot.png``chevron-right.png``m01-profile-summary-card.png``AppButton`、ready/error 状态和现有五个目标路由,并禁止重复菜单卡框及三个旧 opaque 业务素材。
- [ ] 运行聚焦测试,确认因缺少新资产引用而失败。
### Task 2: 模块资产
- [ ] 保留一个已经用户审批通过的真实透明线框,专用于失败态;正常册页复用项目真实印章、分隔结与箭头资产。
- [ ] 使用 Pillow 验证失败态线框为 RGBA、透明角且存在非空 alpha 内容。
### Task 3: 页面实现
- [ ] 删除重复卡片结构,改为连续身份区、紧凑提醒栏和“服务与设置”分组列表。
- [ ] 保留资料、提醒及三个菜单路由;失败态增加项目 `AppButton`“重新查看”。
- [ ] 使用内容高度和最小触控高度控制卡片,不固定整页高度。
- [ ] 运行聚焦契约并确认通过。
### Task 4: 验证与审批
- [ ] 运行 M01 聚焦契约、根页面视觉契约、根页面运行时冒烟、模块响应式冒烟和 `git diff --check`
- [ ] 在同一 Chrome 标签页显示 412×915 正常首页,等待明确审批。
- [ ] 正常态通过后显示失败态,再逐一验证资料、提醒和菜单路由。
- [ ] 全部明确通过后才把 `docs/验收规划.md` 的 M01 更新为 `[x]`
@@ -0,0 +1,53 @@
# T01 纵向世系轴基准 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:** 将 T01 重构为纵向分代、关系线清晰、节点可选择的世系树基准页。
**Architecture:** 页面继续由 `treeState` 管理加载、树、阅读提示、空和失败状态。树态使用固定的三代模拟数据和绝对定位关系线,透明真实资产承载节点、状态卡和成员信息栏,横向滚动只服务同代分支扩展。
**Tech Stack:** uni-app、Vue 3、SCSS、现有 PNG 资产、PowerShell 静态契约、Chrome CDP 运行时验证。
## Global Constraints
- 只修改 T01 及其聚焦测试、设计文档和复制到 tree 命名空间的资产。
- 不接 API,不新增路由,不使用系统弹窗或加载。
- 不执行 Git add、commit、push、reset、checkout,不使用 worktree 或多代理。
- 不删除或覆盖现有修改、测试、文档、截图与资产。
### Task 1: 新视觉契约(RED
**Files:**
- Modify: `tests/t01-tree-state-contract.ps1`
- Modify: `tests/t01-all-states-visual-contract.ps1`
- [ ] 要求 `lineage-connector`、三代横向标签、透明节点/状态/信息栏资产和现有四条路由。
- [ ] 明确禁止 T01 继续引用三个旧不透明表面资产。
- [ ] 运行两个契约,确认因页面尚未实现新结构而失败。
### Task 2: 透明资产与纵向树(GREEN)
**Files:**
- Create: `static/assets/modules/tree/transparent/t01-member-node.png`
- Create: `static/assets/modules/tree/transparent/t01-member-node-selected.png`
- Create: `static/assets/modules/tree/transparent/t01-state-panel.png`
- Create: `static/assets/modules/tree/transparent/t01-member-drawer.png`
- Modify: `pages/tree/t01-tree-overview.vue`
- [ ] 复制经过检查的现有透明资产到 tree 模块命名空间,不改动来源文件。
- [ ] 重排三代节点,加入祖先到子代的垂直/水平关系线和明确世代带。
- [ ] 实现红色选中节点、底部透明成员栏和同代横向阅读提示。
- [ ] 保留 `AppLoading`、查询状态和 T03/T04/T06/T07 导航。
- [ ] 运行聚焦契约并修至通过。
### Task 3: 运行时与视觉复核
**Files:**
- Modify only if necessary: `tests/t01-tree-state-runtime-smoke.js`
- Create/update evidence under: `docs/design/screens/runtime/2026-07-20/t01-baseline/`
- [ ] 运行 T01 运行时 smoke,检查各状态和人物资料跳转。
- [ ] 在既有 Chrome 标签页检查四档响应式尺寸,重点排除节点裁切、断线和底栏遮挡。
- [ ] 对同一 `412×915` 状态做前后视觉对照并修复可见的 P0/P1/P2 问题。
- [ ] 只向用户展示正常树态,等待明确审批后再切下一状态;审批前不标记 `[x]`
@@ -0,0 +1,102 @@
# G01 固定信息区与独立列表滚动设计
## 决定
G01“已有家谱”的正常列表态改为两段式布局:上方信息区固定,下方列表区独立纵向滚动。该决定只作用于 G01,不改变其他 G 页面,也不改变 G01 的业务数据、入口、卡片样式、背景或弹窗。
用户已通过可操作对比稿确认该方向。
## 固定范围
由上到下固定显示:
1. 红色“我的家谱”标题栏;
2. 当前家谱大卡片;
3. 世系图、成员、字辈诗、申请审核四个快捷入口;
4. 快捷入口下方金色分隔线;
5. 底部三项导航栏。
固定区不随列表滚动,不做折叠、缩放、吸顶动画或滚动渐变。
## 独立滚动范围
金色分隔线下方到固定底部导航上方为唯一列表滚动区,包含:
- “我创建的”标题及卡片;
- “我加入的”标题及卡片;
- “加入申请”标题及卡片;
- “添加家谱”入口;
- 列表底部安全留白。
三个分组标题随列表一起滚动,不二次吸顶。现有“加入申请”连续卡片继续保持 `12rpx` 间距。
## 页面状态规则
`hasGenealogies` 正常列表态使用两段式布局。
- `loading`:保持当前页面级加载布局,不创建空的固定区或内部列表滚动区;
- `error`:保持完整错误面板自然纵向布局;
- `empty`:保持完整空状态面板自然纵向布局;
- 添加家谱弹层、当前家谱切换层:继续使用全屏固定遮罩,滚动区不得穿透;
- 背景:继续由 `GenealogyPageBackground.vue` 固定覆盖整页,不进入任何滚动容器。
## 尺寸与布局
正常列表态使用视口高度和弹性布局分配空间,不按某一台手机写死滚动区像素高度:
- 页面壳占满当前可用视口;
- 标题栏按现有真实状态栏高度占位;
- 底部导航按现有 `112rpx + safe-area-inset-bottom` 占位;
- 当前家谱、快捷入口和分隔线按现有视觉尺寸占位;
- 剩余空间全部分配给列表滚动区,滚动容器必须得到可计算高度;
- 320×568 不压缩字号、卡片或快捷入口,列表保留约一张半卡片的可视高度并允许自然滚动;
- 360×640、360×800、412×915 以及 412×1000 均不得出现横向溢出、底栏遮挡或列表无法滚动。
## 滚动反馈与位置规则
- 不把“绝对没有滚动条”写成合同;Android 滑动时允许出现短暂的系统滚动指示条;
- 不专门美化或强制隐藏系统滚动指示条,只验证其不挤压卡片、不造成横向错位;
- 不新增下拉刷新、回弹动画、滚动阴影或底部分页行为;
- 从 G01 打开详情/申请页后返回,保留离开前的列表滚动位置;
- 打开并关闭添加家谱弹层或当前家谱切换层,不改变列表滚动位置;
- 用户切换当前家谱后,列表明确回到顶部,避免继续停留在旧家谱的浏览位置;
- loading、error、empty 与正常态切换时,不错误复用正常列表态的旧滚动位置。
## 实现边界
- 正常列表态使用单一纵向区域滚动容器;禁止嵌套同方向滚动容器;
- 当前家谱卡、快捷入口和分隔线必须位于区域滚动容器之外;
- 只调整 G01 模板结构、G01 局部样式和直接对应的测试;
- 不修改 `PageHeader.vue``AppTabbar.vue``GenealogyPageBackground.vue` 或其他 G 页面;
- 不对接接口,不新增全局状态,不改变路由;
- 不使用原生 `uni.showToast``uni.showModal``uni.showLoading``uni.showActionSheet`
## 验证合同
### 静态合同
- 正常列表态存在且只存在一个纵向滚动容器;
- 当前家谱、快捷入口和分隔线位于滚动容器外;
- 三组列表和“添加家谱”入口位于滚动容器内;
- loading、error、empty 不被包进正常态固定结构;
- 未修改其他 G 页面共享组件引用。
### H5 运行验证
在 320×568、360×640、360×800、412×915 和 412×1000 下:
1. 滚动列表前后,标题栏、当前家谱卡、快捷入口、分隔线和底部导航的视口位置保持不变;
2. “我创建的”及其后内容发生纵向位移;
3. 可以滚动到最后一个申请卡和“添加家谱”入口;
4. 页面没有横向溢出、滚动穿透或底栏遮挡;
5. 卡片点击、添加家谱弹层、当前家谱切换层、快捷入口保持可操作;
6. 返回 G01 保留列表位置,切换当前家谱后列表回到顶部;
7. 浏览器无异常、资源 404 或 `console.error`
### 仍缺验证
H5 证据不能替代 Android/HBuilderX 真机或模拟器。最终仍需复核 Android 区域滚动手感、短暂系统滚动指示条、惯性、返回现场、状态栏、安全区和 4GB 设备性能。
## 验收状态边界
本次实现完成后仍只是 G01 新滚动结构的内部 H5 候选。背景方向已由用户确认,但 G01 整页未获得正式验收,不得因此改为 `[x]` 或冻结。
@@ -0,0 +1,63 @@
# G01 次要文字可读性优化设计
## 状态
- 日期:2026-07-16
- 来源:用户在 412×915 实际 H5 页面中指出申请说明与状态文字可能看不清。
- 用户决定:采用“字号、颜色和中等字重同时增强”的 B 方案;首轮 `26rpx / 25rpx` 实际生效但肉眼变化不够明显,第二轮确定为说明 `28rpx / 500`、状态 `27rpx / 600`
- 当前阶段:第二轮 B 方案已在正式样式中形成 H5 内部候选并完成五档截图检查;Android 仍未验收,G01 整页也尚未正式验收。
## 初始证据与首轮结果
初始实际渲染结果:
- `.application-record__copy``23rpx`,在 412px 宽视口约为 `12.63px`,颜色为 `$ink-muted``#776b5c`)。
- `.application-record__status``23rpx`,约为 `12.63px`;“审核中”为 `#9a641f`,“被拒绝”为 `#a7160c`,“已退出”为 `$ink-muted`
- muted 文字在纯宣纸 `#f6f0e5` 上的估算对比度约为 `4.58:1`,叠到较深的背景承接色 `#e7ded1` 时约为 `3.90:1`
- “审核中”在上述两种底色上的估算对比度约为 `4.39:1``3.74:1`
- “被拒绝”约为 `6.70:1``5.71:1`,无需更换颜色。
首轮实施后,说明约为 `14.28px`、状态约为 `13.73px`,颜色和对比度已改善,但用户查看实际页面后认为字号变化仍不明显,因此目标继续提高一档。
这些数值只用于定位 H5 风险,不等于 Android 真机无障碍合规结论。
## 方案比较
1. A 克制:说明 `400`、状态 `500`;层级最轻,但变化主要来自字号。
2. B 中等加粗:说明 `500`、状态 `600`;用户选择该方案,在清晰度和次要层级之间取平衡。
3. C 明显加粗:说明与状态均为 `700`;最醒目,但会让次要信息过度接近谱名和分组标题,不采用。
## 实施范围
仅修改 `pages/genealogy/g01-my-genealogies.vue` 中“加入申请”卡片的次要说明和状态样式:
- `.application-record__copy`
- `.application-record__status`
- `.application-record__status--rejected`
- `.application-record__status--muted`
不修改共享 `GenealogyCard.vue`,因为用户箭头所指的是 G01 申请卡片;“更新于”虽然同样偏小,但本轮不擅自扩大范围,后续可单独审核。
## 确定样式
- 申请说明:`font-size: 28rpx``font-weight: 500``color: #62584c``line-height: 1.4`
- 通用状态:`font-size: 27rpx``font-weight: 600`、默认“审核中”颜色为 `#7f4f16`
- “被拒绝”:保留 `#a7160c`,继承新的字号与字重。
- “已退出”:使用 `#62584c`,继承新的字号与字重。
- 卡片高度、内边距、12rpx 连续间距、箭头、文案和点击行为保持不变。
- 首轮 `26rpx / 25rpx` 只作为比较历史,不再是当前目标值,也不得保留兼容分支。
## 响应式与验证
- 覆盖 320×568、360×640、360×800、412×915,并保留 412×1000 压力档。
- 检查长说明不与状态或箭头重叠;320×568 允许自然纵向滚动。
- G01 运行 smoke 继续验证固定区、独立滚动、弹层位置保持、切换家谱回顶和底部入口可达。
- 生成修改前后 412×915 同状态截图进行视觉比较。
- H5 只作为内部候选;Android/HBuilderX 真机或模拟器仍需检查系统字体缩放、屏幕亮度和不同面板下的实际可读性。
## 非目标
- 不继续调整 28% 公共背景透明度。
- 不修改谱名、分组标题、当前家谱元信息或共享卡片更新时间。
- 不修改其他 G 页面,不对接接口。
- 不把本轮优化写成 G01 整页已验收或 Android 已通过。
@@ -0,0 +1,46 @@
# G 类型页面分层自适应背景设计
## 目标
解决 G 类型页面在高屏下半区只剩纯色宣纸、画面重心中断的问题,同时继续满足:左右不裁剪、背景不拉伸、不同屏幕比例可用、9 个活动 G 页面共用。
## 用户决定
采用现有 C 图贴底的自适应背景,不生成新图,也不重做一张依赖固定长宽比的超长整图。
## 视觉结构
公共背景由两个固定视口图层组成,统一归 `components/GenealogyPageBackground.vue` 管理:
1. 宣纸底层:覆盖完整视口,沿用与 C 母版底部匹配的暖灰宣纸色。
2. 底部山水层:继续使用用户选定的 C“云竹亭台连续宣纸景”,宽度 `100%``widthFix` 等比显示并贴底,完整保留左右画面,不裁剪、不拉伸。
不新增顶部竹枝资产,不重复渲染或裁切 C 图。高屏多出的宣纸区留在上方,主要由标题栏、当前家谱卡片和快捷入口覆盖;C 图的亭台、山水与竹影集中承托下方列表和加入申请区。
## 响应式规则
- 320×568:C 层贴底后接近页头,完整画面覆盖大部分视口。
- 360×640:C 层贴底,顶部只保留较小宣纸区,中央内容区保持可读。
- 360×800、412×915:高屏多出的宣纸区位于上方,C 层稳定承托“我创建的”“我加入的”“加入申请”等下半区。
- 所有尺寸都禁止 `aspectFill``scaleToFill`C 图片层按原比例显示。
- 背景固定于视口,页面内容继续自然滚动;本轮不改变 G01 固定区/独立滚动结构。
## 资产与可恢复性
- C 母版和现有确定性构建流程保持不变。
- 不新增任何背景候选或运行资产;继续使用 `static/assets/modules/genealogy/opaque/genealogy-page-background.png`
- manifest、构建脚本和测试继续以现有 C 母版为唯一可重建输入。
## 验证
- 更新公共组件前先写贴底定位失败契约并执行红绿循环。
- G01 在 320×568、360×640、360×800、412×915 下生成真实 H5 截图。
- 同一比较输入中查看 C 母版和实现截图,检查上方宣纸区、贴底位置、裁剪、拉伸、文字对比度和横向溢出。
- 其他 8 个 G 页面至少通过公共背景契约与现有运行 smoke;仍不得据此宣称逐页视觉验收。
- Android/HBuilderX 真机或模拟器复核继续保留为最终验证缺口。
## 非目标
- 不修改 G 页面内容、卡片、标题栏、路由、接口或状态逻辑。
- 不实施 G01 标题/当前家谱/快捷入口固定和列表独立滚动。
- 不删除 A/B/C 历史母版、未跟踪资产、截图或缓存。
@@ -0,0 +1,39 @@
# G 模块公共背景 28% 透明度设计
## 状态
- 日期:2026-07-16
- 用户决定:公共长背景画层使用 `28%` 不透明度。
- 当前阶段:设计已确认,尚未修改正式组件,尚未形成新的 H5 或 Android 验收结论。
## 目标
降低连续长背景中竹枝、山水和亭台的视觉重量,让标题、卡片、列表文字和状态信息成为明确主体,同时保留浅色国风宣纸氛围。
## 作用范围
唯一修改所有者为 `components/GenealogyPageBackground.vue`
该共享组件当前由 9 个活动 G 页面使用,因此透明度决定统一覆盖:G01、G03、G05、G06、G08、G09、G10、G11、G12。不得在各页面重复定义透明度,也不得只为 G01 添加局部覆盖。
## 视觉规则
- 仅对 `.genealogy-page-background__art` 图片画层设置 `opacity: 0.28`
- 组件底部的宣纸承接色 `#e7ded1` 保持不变,避免整页透出宿主页面颜色。
- 标题栏、当前家谱卡、快捷入口、列表卡、弹窗、文字、状态色和底部导航保持原有不透明度。
- 背景继续使用 `widthFix`、固定贴底、左右完整显示的现有规则;不得改变图片尺寸、裁切方式或滚动行为。
- 该透明度是 28%,不是把图片文件重新烘焙成半透明 PNG;运行资产和可恢复母版不重制。
## 验证
- 静态合同确认透明度由共享组件唯一持有,值为 `0.28`,各 G 页面没有重复覆盖。
- 运行验证覆盖 320×568、360×640、360×800、412×915,并保留 G01 的 412×1000 压力档。
- 视觉截图核对文字、卡框和按钮未被同步变淡,背景仍连续、左右不裁剪,底栏未被遮挡。
- H5 截图只作为内部候选证据;Android/HBuilderX 真机或模拟器仍需另行复核,特别关注 4GB Android 的长图解码、切页和回收。
## 非目标
- 不修改 G01 固定区/独立列表滚动结构。
- 不重做背景图,不新增主题或深色模式。
- 不修改其他模块页面,不对接接口。
- 不把本次透明度选择写成 G01 整页已验收或 Android 已通过。
@@ -0,0 +1,34 @@
# G 模块旗舰级连续长背景设计
## 决定
本方案替代此前“纯色宣纸 + 现有 C 图贴底”的方向。用户已确认生成稿并授权作为 9 个活动 G 页面的公共背景,用一张连续长图消除两层之间的明显横向分界线。
## 输出规格
- 生产母版目标:1536×3840,纵横比 2.5:1。
- APP 运行图目标:1440×3600,纵横比 2.5:1。
- 色彩:暖灰宣纸、淡金竹影、灰褐水墨山水,与现有 C 方向一致。
- 画面:顶部疏竹和淡云,中段低对比安全区,下半部亭台、湖面、远山及岸边竹影。
- 禁止文字、Logo、UI、卡片、按钮、红色元素、明显横向接缝、重复图案和突兀色带。
## 响应式规则
- 页面按宽度等比显示并固定贴底。
- 1024×25601536×3840 的 2.5:1 构图比 412×1000 的 2.427:1 更高,因此 412×915、412×1000 不产生上方纯色拼接。
- 320×568、360×640 等短屏只裁掉顶部安全留白,左右画面始终完整,不横向裁剪、不拉伸。
- 顶部 25% 不放唯一性强的关键主体,允许短屏裁切;核心山水和亭台放在中下部。
## 性能边界
- 1440×3600 RGBA 解码内存约 19.8MiB,只允许页面同时存在一个公共背景实例。
- H5 阶段检查 PNG 体积和五档截图;Android 4GB 内存设备上的解码、切页和回收仍是正式验收前置项。
- 母版用于可恢复生产,运行图通过确定性流水线缩放和写入 sRGB,不直接让页面加载 1536×3840 母版。
## 本轮范围
- 原始 ImageGen 输出原样保留,并确定性归一化为 1536×3840 稳定母版。
- 流水线生成 1440×3600 运行图,使用新版本化文件名接入 `GenealogyPageBackground.vue`;旧 C 文件不覆盖、不删除。
- 9 个 G 页面继续只消费一个共享背景组件,不跨页改变业务结构。
- 覆盖 320×568、360×640、360×800、412×915 和 412×1000 H5;不因用户确认背景方向而标记 G01 整页已验收。
- Android/HBuilderX 与 4GB Android 图片性能不在本轮 H5 证据内。
@@ -0,0 +1,37 @@
# G 类型页面共用 C 背景设计
## 目标
将已选定的 C“云竹亭台连续宣纸景”设为所有活动 G 类型页面的公共背景。背景必须完整显示左右画面、不裁剪、不变形;高屏和长页面超出图片高度的部分使用同色宣纸底色自然延展。
## 范围
- 覆盖 `pages.json` 中 9 个活动 G 路由:G01、G03、G05、G06、G08、G09、G10、G11、G12。
- 只替换页面最底层的旧纸纹和页脚山水,不改标题栏、内容卡片、交互、状态数据或路由。
- G01 的固定区/独立滚动结构不在本次范围内。
- 当前只形成 H5 内部候选证据;Android/HBuilderX 真机或模拟器复核仍然缺失。
## 单一所有者
新增 `components/GenealogyPageBackground.vue`,由它唯一持有:
- C 背景运行资产路径;
- 同色宣纸延展底色;
- 顶部对齐、按宽度等比显示、固定在页面底层且不接收指针事件的布局规则。
9 个页面只渲染该组件,不再分别引用 `page-paper.jpg``footer-mountain-bamboo.png` 或直接引用 C 背景资产。
## 显示规则
- 外层背景固定覆盖视口,使用宣纸底色填满未被图片覆盖的区域。
- C 背景图片宽度为 `100%`,使用 `mode="widthFix"` 等比计算高度并顶部对齐。
- 图片不使用 `aspectFill``scaleToFill`,因此左右不裁剪且画面不变形。
- 页面内容继续使用各页现有层级;公共背景位于 `z-index: 0`,不改变弹窗和操作层层级。
## 验证
- 契约测试必须解析 `pages.json` 得到实际 G 路由,并断言每个页面只使用公共背景组件。
- 契约测试必须禁止 G 页面继续引用旧纸纹、旧页脚山水或直接重复引用 C 背景。
- G01 原有视觉、空状态和运行冒烟测试继续通过。
- H5 在 320×568、360×640、360×800、412×915 下截图检查:左右完整、无拉伸、无横向溢出,底部宣纸延展连续。
@@ -0,0 +1,98 @@
# 全模块国风视觉系统设计
## 状态与授权
- 日期:2026-07-16。
- 用户明确确认:A 系列现有按钮、弹窗、轻提示和对话框样式已经定稿,必须作为全局标准。
- 用户明确要求:本轮连续完成 G、T、F、R、N、M 的视觉统一,不在中途等待逐页确认;后续逐页审核时允许局部调整。
- 本轮不对接接口、不生成运行截图、不把 H5 实现写成 Android 或整页验收完成。
## 现状问题
1. A 系列使用透明卷轴按钮、卷轴对话框和卷轴 Toast;其他模块仍大量引用旧的 `opaque/a01-primary-button.png``opaque/a01-secondary-button.png` 或页面自制矩形对话框。
2. 普通 `PageHeader` 左侧显示“返回”文字,与 A04/A05 已采用的箭头返回不一致。
3. G01 当前家谱与列表卡片的地区、人数、角色和更新时间仍偏浅偏细。
4. G 系列虽已共用 G01 长背景,但按钮和对话框没有全部收拢到 A 系列标准。
5. T、F、R、N、M 的页面壳、背景和操作控件仍由页面或 `ModulePage` 各自维护,视觉来源不唯一。
6. `ModulePage`、F02、N01 仍存在原生 `uni.showToast`,违反项目自定义反馈规则。
## 方案比较与决定
### 方案 A:只替换图片路径
改动最快,但按钮尺寸、文字、交互状态和弹窗结构仍分散在几十个页面,后续会再次出现样式漂移,不采用。
### 方案 B:建立全局视觉组件并迁移现有页面(采用)
由公共组件单一持有 A 系列资产,页面只传入文案、类型与事件。G 系列继续使用 G01 公共背景;T、F、R、N、M 使用统一页面壳和各自的长背景资产。改动范围较大,但能满足“全局统一、后续逐页再调”的目标。
### 方案 C:逐页完全重做
自由度最高,但会破坏现有页面状态、测试和业务入口,且难以在同一轮稳定完成,不采用。
## 单一视觉所有者
- `AppButton.vue`:唯一持有 A 系列主按钮与次按钮卷轴资产。
- `AppDialog.vue`:唯一持有 A 系列卷轴对话框资产和遮罩、标题、正文、单双按钮布局。
- `AppToast.vue`:唯一持有 A 系列卷轴轻提示边框资产。
- `AppLoading.vue`:统一加载文案与水墨印记动效,不调用原生 Loading。
- `PageHeader.vue`:唯一持有普通页面的返回箭头和根页面头部。
- `ModulePageBackground.vue`:统一承载 T、F、R、N、M 的页面背景;G 系列继续由 `GenealogyPageBackground.vue` 持有。
- `ModulePage.vue`F03F10、R01R11、N02、M02–M10 的统一内容母版,只消费上述公共组件。
旧的 `opaque/a01-primary-button.png``opaque/a01-secondary-button.png` 不再允许被活动页面或公共组件引用;文件本身保留,不删除历史资产。
## 已确认控件规范
### 按钮
- 主按钮使用 `/static/assets/foundation/transparent/a01-scroll-primary-v3.png`
- 次按钮使用 `/static/assets/foundation/transparent/a01-scroll-secondary-v3.png`
- 图片使用 `aspectFit`,文字保持独立节点;默认高度 `96rpx`,紧凑按钮最低 `76rpx`
- 主按钮文字 `#fffaf0`,次按钮文字 `#5c4330`;禁用态只降低整体不透明度,不替换另一套资产。
### 对话框与提示
- 对话框使用 `/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png`,不得再用普通卡片背景假装对话框。
- Toast 使用 `/static/assets/foundation/transparent/a01-scroll-toast-v3.png`
- 加载使用自定义水墨印记与文字,不调用 `uni.showLoading`
- 项目内不得保留活动代码中的 `uni.showToast``uni.showModal``uni.showLoading``uni.showActionSheet`
### 导航
- 非根页面左侧使用 `/static/assets/foundation/transparent/chevron-right.png` 水平翻转形成返回箭头。
- 点击区域至少 `88rpx × 88rpx`,不再显示“返回”文字。
- 根页面 G01 继续保留品牌印章与通知铃铛。
## 页面与背景体系
### G 系列
- G01 是 G 系列确定的风格母体;G03、G05、G06、G08、G09、G10、G11、G12 必须沿用 G01 的长背景、宣纸底、朱砂、古金、卡框语言与信息层级,保持当前 `GenealogyPageBackground.vue` 和 28% 长背景画层。
- 全部旧按钮和页面自制对话框迁移至 A 系列公共组件。
- G01 被标出的地区、人数、角色、更新时间统一加深并使用中等字重;不改变卡片结构和滚动范围。
### T、F、R、N、M
- 五个模块各自建立可辨认的页面风格,不直接复制 G01 的页面编排;它们仍共享浅色国风、楷体、宣纸材质、朱砂主操作、古金线框和 A 系列全局控件,确保主题统一。
- 每个模块使用一张 `1440×3600`、无文字、无 UI、浅色国风的长背景资产。
- T:淡墨古树谱系、远山与竹影;F:暖宣纸、庭院梅枝与生活气息;R:册页、书卷、墨梅与档案感;N:云纹、飞笺与淡金消息纹样;M:竹影、印章留白与安静个人空间。
- 图片固定贴底、按宽度等比显示、左右不裁剪;短屏允许裁掉顶部安全留白。
- 主要内容仍使用现有真实卡框和表单纸面资产,不把文字或按钮烘焙进背景图。
## 响应式与范围
- 覆盖 320×568、360×640、360×800、412×915;短屏允许自然纵向滚动。
- 本轮只调整视觉壳、公共控件、背景和 G01 指定文字,不改变路由、字段契约、数据结构和业务跳转。
- 不删除现有修改、未跟踪文件、历史资产、测试、文档或证据。
- 不执行 Git add、commit、push、reset、checkout,也不使用 worktree 或多代理。
## 验证标准
1. 活动代码不存在原生 UniApp 提示调用。
2. 活动页面和公共组件不存在旧不透明 A01 按钮引用。
3. 普通 PageHeader 只显示返回箭头,不显示“返回”文字。
4. 9 个 G 页面继续消费共享 G 背景。
5. T、F、R、N、M 的全部活动页面消费对应模块背景或统一 `ModulePage` 页面壳。
6. 现有模块契约与运行 smoke 不因视觉迁移失效。
7. 只生成资产与代码验证,不生成新的运行截图。
@@ -0,0 +1,43 @@
# 公共加载组件红金印牌重设计
## 目标
`components/AppLoading.vue` 当前的细红方框“谱”字替换为与浅色国风主题一致的红金家谱印牌,同时保持组件可在 G01 及其他已接入页面复用。
## 范围
- 只修改公共 `AppLoading` 的视觉表现及对应测试。
- G01 继续使用页面级加载形态,不改变状态判断、文案、页面背景、头部和底部导航。
- 已接入 `AppLoading` 的其他页面自动获得同一视觉体系,不改各页面业务逻辑。
- 不新增接口,不使用原生 Toast、Modal、Loading 或 ActionSheet。
## 视觉方案
- 加载主体使用已有真实资产 `/static/assets/foundation/transparent/brand-seal.png`
- 页面级印牌宽高约 `132rpx × 136rpx`;区域级约 `88rpx × 90rpx`
- 印牌下方使用已有真实资产 `/static/assets/foundation/transparent/auth-divider-knot.png`,作为轻量视觉连接,不新增 CSS 绘制图形。
- 主文案使用现有深墨色,保持约 `30rpx`,字重略加强。
- 说明文字保持约 `24rpx` 和现有克制色阶。
- 组件背景继续透明,不增加卡片、白底、边框或阴影。
## 动效
- 印牌使用约 `1.6s` 的轻呼吸动画。
- 缩放范围约为 `0.96``1`,透明度约为 `0.82``1`
- 不旋转、不快速闪烁、不移动页面布局。
- 如意结只做轻微透明度呼吸,避免与印牌争抢注意力。
- 尊重 `prefers-reduced-motion: reduce`:关闭动画并保持最终清晰状态。
## 组件契约
- 保留 `variant="page"``variant="section"`
- 保留 `text``description` 属性,不改变调用方式。
- 页面级与区域级仅在尺寸、间距和字号上区分,共用同一真实资产和动效语义。
## 验证
- 更新公共加载组件契约,确保使用 `brand-seal.png``auth-divider-knot.png`,并禁止旧的 CSS 方框标记回归。
- 运行公共加载组件、G01 加载状态、G 系列和其他模块加载契约。
- 运行编译审计和相关运行检查。
- 在同一个可控 Chrome 标签页检查 G01 `412×900`,并补查 `320×568``360×640``360×800``412×915`
- H5 结果只作为内部候选证据;Android/HBuilderX 真机或模拟器复核仍然缺失。
@@ -0,0 +1,22 @@
# F 系列全状态视觉优化
## 范围
- F01:列表、加载、空态、错误态。
- F02:表单、空内容提示、成功、错误。
- F03–F10:默认、空态、成功、错误;列表与设置页另核对点击提示。
## 设计决定
- 保留已经确认的浅色国风背景、金色内容框和 A 系列按钮/提示组件,不重新生成同类资产。
- F01 保持家族动态的信息流结构,仅提高标签、摘要、作者、状态说明的字号和对比度。
- F02 保持卷轴表单结构,提高说明、输入和结果说明的可读性;空内容继续使用全局 `AppToast`
- F03F10 由 `ModulePage.vue` 统一拥有共用壳的文字规格。标题、表单、列表、详情、时间线、设置和状态说明均采用适合 412×915 审核的可读字号。
- 不改变路由、业务数据和交互语义;所有状态通过真实运行页面呈现。
## 验证
- 为 F01/F02 与公共 ModulePage 增加视觉契约。
- 在同一个 Chrome 标签页逐状态生成 412×915 内部 H5 截图。
- 运行根页面、ModulePage、运行时 smoke、编译审计及 `git diff --check`
@@ -0,0 +1,23 @@
# G01 空状态透明框设计
## 目标
空状态只保留金色框线与操作内容,完整透出 G01 长背景,不再叠加第二张宣纸底或下沿山水。
## 已确认方案
- 移除空状态对不透明 `g01-empty-panel.png` 的引用;该资产继续保留给错误状态使用。
- 从已确认的 `g01-empty-panel.png` 确定性提取原双金线和四角纹样,生成透明 `g01-empty-panel-frame.png`;不重新设计线型。
- 提取脚本归属 `design-pipeline/scripts/build-g01-empty-frame.mjs`,可在换机后重建。
- 空状态框内不设置背景、阴影或额外底图。
- “确认没有现有家谱后再创建,避免重复建谱”调整为 `26rpx`,行高 `38rpx`
- 空状态框高调整为 `1120rpx`,内容整体垂直居中,并使用 `50rpx 28rpx 46rpx` 兼顾窄屏的内边距。
- 谱印为 `120×184rpx`,主标题 `48rpx`,说明文字 `28rpx`
- 两个主操作为 `560×124rpx`,按钮文字 `34rpx`,创建家谱文字 `31rpx`
- 保留“搜索已有家谱优先、邀请码其次、创建家谱为弱操作”的层级与原有交互。
## 验证
- 静态契约锁定透明框资源、九宫格参数、提示字号,并禁止空状态继续引用不透明面板。
- 运行空状态契约、G01 视觉契约、编译审计和运行冒烟。
- 在 412×900、320×568 截图检查框内外背景连续且无白色矩形底。
@@ -0,0 +1,23 @@
# G01 读取失败状态重设计
## 结论
当前失败态仍使用旧的 `g01-empty-panel.png` 不透明白底,视觉密度和空状态、加载状态不一致。本轮采用“透明金框 + 红金家谱印牌 + 单一重试主按钮”,复用已经确认的真实资产,不新增重复图片。
## 视觉结构
- 外层使用 `/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png`,与已通过的空状态共用 `1120rpx` 高度。
- 内容在透明金框内纵向居中,页面背景保持连续可见。
- 顶部使用 `/static/assets/foundation/transparent/brand-seal.png`,页面级宽高 `132rpx × 136rpx`
- 标题“暂时无法读取家谱”使用深墨色、`42rpx`、半粗字重。
- 说明“网络或服务暂不可用,请稍后重新查看。”使用 `27rpx`、舒展行距。
- 标题与按钮之间放置已有 `/static/assets/modules/genealogy/transparent/section-divider.png`
- “重新加载”继续使用 `/static/assets/foundation/transparent/a01-scroll-primary-v3.png`,尺寸与空状态主按钮一致为 `560rpx × 124rpx`
- 不增加第二操作,不增加白底、卡片、阴影或 CSS 绘制图形。
## 行为与验证
- 保留 `retryLoad` 行为和 `state=error` 展示入口。
- 新增 G01 失败态契约,禁止旧不透明面板回归。
- 在同一个 Chrome 标签页检查 `320×568``360×640``360×800``412×915`
- H5 只作为内部候选;Android/HBuilderX 仍需验证。
@@ -0,0 +1,25 @@
# G01 加载状态设计
## 目标
把 G01 当前空心金圈加载态收拢到项目已确定的全局 Loading 标准,同时保持 G 系列长背景和页面导航稳定,避免短暂加载状态显得空洞或过度装饰。
## 已确认方案
- 采用方案 AG01 加载态复用 `AppLoading.vue` 的朱砂“谱”印章呼吸动效。
- 主文案使用“正在整理家谱”,不增加新的业务含义。
- 加载内容位于页面视觉中心略偏上区域;保留 G01 公共长背景、标题栏和底部导航。
- 不使用空状态的大型透明金线框,不生成新资产,不制作骨架卡片。
- 不改变 `state=loading` 的状态入口、默认列表、空状态或失败状态。
## 组件边界
- `AppLoading.vue` 是全局加载视觉的唯一维护入口。
- `g01-my-genealogies.vue` 只负责放置组件和提供页面文案,不复制加载动效样式。
## 验证
- 静态契约锁定 G01 使用 `AppLoading`,并禁止保留旧 `loading-seal`
- 运行 G01 空状态契约、视觉契约、编译审计和运行态冒烟。
- 在 412×900 与 320×568 检查无横向溢出、底栏不遮挡且加载标识清晰。
- H5 结果仅作为内部候选,Android/HBuilderX 仍需后续复核。
@@ -0,0 +1,32 @@
# G01 元信息文字与图标可读性设计
## 目标
只放大 G01 当前家谱卡的“切换”文字,以及当前卡和列表卡中的地区/成员/管理员图标;其余元信息文字保持原字号。
## 已确认方案
- “切换”文字:`23rpx` 调整为 `28rpx`
- 当前家谱元信息文字保持 `27rpx`
- 当前家谱元信息图标:`32rpx` 调整为 `36rpx``opacity: 1`
- 列表卡地区与成员文字保持 `26rpx`
- 列表卡地区与成员小图标采用平衡尺寸 `46rpx``flex: 0 0 auto``opacity: 1`
-`saturate(1.35) brightness(0.82) contrast(1.15)` 加深现有赭金 PNG,而不是继续放大。
- 两组元信息之间保持 `18rpx`
- 更新时间位于独立第二行并右对齐;地区与成员信息位于第三行;常规卡高为 `178rpx`
- 320px 继续隐藏更新时间,卡高保持 `148rpx`
- 列表卡左侧“家谱”谱印保持原尺寸和状态透明度。
- 列表卡角色文字保持 `26rpx`,更新时间保持 `24rpx`
## 适配边界
- 不改变卡框、卡片高度、谱印、标题、快捷入口和背景。
- 320px 小屏继续隐藏更新时间;当前家谱图标的小屏覆盖值由 `28rpx` 同步提高到 `32rpx`
- 元信息保持单行,图标与文字垂直居中,不能造成横向溢出或遮挡右侧角色与箭头。
## 验证
- 更新 G01 静态视觉合同,锁定“切换”字号、图标尺寸与可见度,并锁定其余文字仍为原字号。
- 运行 G01 静态合同和编译审计。
- 在当前 H5 Chrome 中重新捕获 412×915,并检查 320×568,确认文字清晰且无横向溢出。
- 本轮只形成 H5 候选,不表示 Android/HBuilderX 或 G01 整页正式验收。
@@ -0,0 +1,20 @@
# G03 全状态视觉设计
## 状态范围
G03 实际包含创建默认、创建校验、重复建谱提醒、创建提交中、创建失败、首代默认、首代校验、首代提交中、首代失败和创建成功弹窗,共 10 类状态。
## 设计结论
- 保留现有宣纸表单面板、红金主卷轴按钮、重复提醒和成功弹窗;这些状态与 G 系列主题一致,不重新生成资产。
- 自定义标题栏移除“返回”文字,只保留向左真实箭头,继续保持左右等宽以确保标题居中。
- 表单说明由 `23rpx` 提升至 `25rpx`,访问规则选项由 `21rpx` 提升至 `23rpx`
- 字段校验由 `20rpx` 提升至 `24rpx`,保持右对齐;提交失败说明由 `21rpx` 提升至 `25rpx` 并增加行高。
- 提交中继续使用主按钮内文案变化,不叠加页面级 Loading,避免一个短暂操作出现两个加载焦点。
- 弹窗继续使用已确认的 A 系列 `a01-scroll-dialog-v3.png` 与主、次卷轴按钮,不生成重复资产。
## 验证
- 10 类状态均以真实交互触发并截图。
- 默认、校验、失败、弹窗至少检查 `412×915`;整页运行 smoke 覆盖基础四档。
- 重点核对标题居中、错误文字可读性、按钮可达性、弹窗遮罩和短屏自然滚动。
@@ -0,0 +1,5 @@
# G05 全状态视觉设计
G05 包含管理员总览、成员总览、公开预览、加载、空、失败和无权限七类状态。管理员总览保留;成员总览补齐两个“仅管理员可用”的非交互说明,公开预览将第五条资料改为整行并下移公开说明。四类系统状态隐藏旧总览功能格底图,统一使用透明金框、红金印牌、公共加载组件和 A 系列卷轴按钮。所有图片均复用现有资产。
系统状态区域占据标题栏以下可视高度;`320×568` 允许自然滚动。状态文案不叠加在功能格上,不新增原生系统弹层。H5 为内部候选,Android/HBuilderX 仍待复核。
@@ -0,0 +1,17 @@
# G09 全状态视觉优化
## 范围
覆盖列表、加载、空、失败、撤回确认和撤回完成六种实际状态。保留已确认的 G 系列长背景、申请状态卡、公共 Loading、A 系列按钮和自定义弹窗资产。
## 视觉决定
- 不改变卡片结构和状态语义,只提升列表说明、时间、关系、状态提示与操作文字的可读性。
- 空/失败状态继续使用既有状态卡与主卷轴按钮,不新增图片资产。
- 撤回确认继续使用全局 `AppDialog`;撤回后在原卡片位置显示“已撤回”,避免额外 Toast。
## 验证
- 六态均以同一 Chrome 标签页、`412×915` 截图检查。
- 运行 G08–G10 流程 smoke、页面视觉合同和编译审计。
- 仅作为 H5 内部候选,Android/HBuilderX 仍需后续复核。
@@ -0,0 +1,18 @@
# G10 全状态视觉优化
## 范围
覆盖列表、加载、空、失败、无权限、审核说明、通过确认、通过完成、拒绝确认、拒绝完成十种实际状态。
## 视觉决定
- 保留 G 系列背景、申请记录卡、公共 Loading、状态卡和全局 A 系列弹窗。
- 待审核卡的拒绝/通过操作扩大到可读、可点的双按钮,不再让底图槽位压住文字。
- 审核完成后使用一条完整卷轴覆盖底图的两个按钮槽,明确显示“已通过”或“已拒绝”,不保留空槽。
- 提升手机号、时间、关系、状态说明和空/失败说明的字号;不改变审核状态逻辑。
## 验证
- 十态均在同一 Chrome 标签页以 `412×915` 截图;通过/拒绝结果必须与对应确认弹窗成对复核。
- 运行 G08–G10 流程 smoke、视觉合同、编译审计和差异检查。
- 当前仅为 H5 内部候选,Android/HBuilderX 仍待验证。
@@ -0,0 +1,18 @@
# G11G12 全状态视觉优化
## 范围
- G11:表单、加载、成功、失败、无权限、校验、保存反馈,共 7 态。
- G12:列表、加载、空、编辑、失败、无权限、编辑校验、保存失败、保存反馈,共 9 态。
## 视觉决定
- 两页继续复用 G03 宣纸面板、G 系列背景、公共 Loading、A 系列按钮和现有输入框资产。
- 保留当前构图与业务语义,仅提升说明、字段、提示、校验、字辈行辅助信息的字号与行高。
- 保存成功使用页面内结果或自定义卷轴反馈,不引入系统 Toast/Loading。
## 验证
- 16 个状态均在同一 Chrome 标签页以 `412×915` 截图。
- 运行 G11/G12 合同、运行 smoke、编译审计和差异检查。
- 当前仅为 H5 内部候选,Android/HBuilderX、软键盘和系统字体仍待复核。
@@ -0,0 +1,77 @@
# 全局加载视觉系统设计
## 目标
建立一个全项目共用、职责明确的加载视觉系统:整页首次加载和页面局部加载统一使用 `AppLoading.vue`,按钮提交继续由 `AppButton.vue` 负责,下拉刷新、触底加载和文件上传保留各自符合场景的反馈方式。
## 已确认方案
采用分层方案,不使用一个整页 Loading 覆盖所有异步场景。
### 1. `AppLoading` 的职责
- `variant="page"`:页面首次进入且主体数据尚不可展示时使用。
- `variant="section"`:搜索结果或页面局部区域等待数据时使用,保留周围已有内容。
- `text`:当前正在执行的动作。
- `description`:可选辅助说明。
- 组件自身不创建全屏遮罩、不接管标题栏或底部导航、不控制业务状态。
- 朱砂谱印、呼吸动效、文字层级和间距由组件单一维护,页面不得复制或重写印章动画。
### 2. 两档视觉尺寸
页面级:
- 谱印 `96×96rpx`,印内文字 `40rpx`
- 主文案 `30rpx`,辅助说明 `24rpx`
- 内容区最小高度 `320rpx`
局部级:
- 谱印 `64×64rpx`,印内文字 `28rpx`
- 主文案 `24rpx`,辅助说明 `22rpx`
- 内容区最小高度 `180rpx`
两档均保持透明背景和现有 `1.35s` 呼吸节奏。
## 首批接入范围
### 页面级 Loading
| 页面 | 文案方向 |
|---|---|
| G01 我的家谱 | 正在整理家谱 |
| G05 家谱总览 | 正在展开家谱 |
| G09 我的申请 | 正在整理申请记录 |
| G10 入谱审核 | 正在整理审核申请 |
| G11 家谱设置 | 正在读取家谱设置 |
| G12 字辈诗 | 正在整理字辈诗 |
| T01 世系树 | 正在整理世系 |
| T03 成员档案 | 正在读取成员档案 |
| T07 成员目录 | 正在整理成员目录 |
| F01 家族动态 | 正在整理家族动态 |
| N01 消息中心 | 正在整理消息 |
### 局部 Loading
- G06 搜索家谱:只替换搜索结果区域,搜索框、模式切换和筛选条件保持可见。
## 明确排除
- A01、A04、A05、G03、G08、G11 保存、T04、T05、F02、M02、M07 的提交/保存过程使用 `AppButton` 的加载态,不使用 `AppLoading`
- 下拉刷新和触底加载必须保留已有列表及滚动位置,不显示整页 Loading。
- 头像、相册和动态图片上传显示上传进度,不显示整页 Loading。
- 错误、空数据和无权限状态不得伪装成 Loading。
## 状态与兼容
- 页面仍拥有自己的 `loading/list/empty/error/no-permission` 等业务状态;`AppLoading` 只消费状态结果。
- 现有用于 H5 样式审核的 `?state=loading` 入口继续保留。
- 页面从加载切换到成功、空、错误时必须移除 Loading,不与目标状态叠加。
- 320×568 允许自然纵向滚动,不允许横向溢出或遮挡固定底栏。
## 验证
- 组件契约锁定 `page/section` 两种变体、`text/description` 属性和唯一动画入口。
- 每个首批页面的静态契约必须证明使用 `AppLoading`,并禁止保留旧页面自制加载标识。
- 运行现有编译审计、各模块运行 smoke 和 `git diff --check`
- 代表性页面检查 320×568、360×640、360×800、412×915H5 证据不代表 Android/HBuilderX 正式验收。
@@ -0,0 +1,15 @@
# M 系列全状态视觉优化
## 范围
- M01:正常、错误。
- M02–M10:默认、空态、成功、错误。
- M03、M06、M10:设置项点击提示。
## 设计决定
- M01 原头像位为无内容的红色框,改为复用已确认的 `brand-seal.png` 真实“谱”印资产。
- 提高手机号/身份、待办说明、菜单标题、菜单说明、查看操作及错误说明的字号和对比度。
- M02M10 继续使用公共 `ModulePage`,保留个人中心园林背景和 A 系列按钮/提示组件。
- 不改变路由、菜单去向和状态语义。
@@ -0,0 +1,14 @@
# N 系列全状态视觉优化
## 范围
- N01:列表、加载、空态、错误态、单条已读、全部已读提示。
- N02:默认、空态、成功、错误。
## 设计决定
- 保留通知模块的云纹山水背景和公共金框。
- N01 提高消息标签、摘要、空/错态说明的可读性;底部操作改为公共 `AppButton` 的整行规格,避免左侧悬空。
- 单条已读和全部已读继续使用真实交互;全部已读提示复用 A 系列 `AppToast`
- N02 继续由公共 `ModulePage` 详情母版承载。
@@ -0,0 +1,18 @@
# R 系列全状态视觉候选
## 范围
- R01–R11:默认、空态、成功、错误,共 44 个页面状态。
- R01、R03、R05、R10、R11:列表点击提示,共 5 个交互状态。
## 设计决定
- R 系列采用档案、书卷、笔墨意象的独立浅色背景,内容区继续复用公共金框和 A 系列按钮/提示组件。
- 列表、详情、表单、时间线和全局状态均由 `ModulePage.vue` 的统一文字规格承载,避免同类记录页产生字号漂移。
- 不新造已有资产,不改变路由和数据语义。
## 验证
- 412×915 下在同一个 Chrome 标签页生成并逐张检查 49 张内部 H5 截图。
- 检查长标题、四字段表单、时间线、列表提示和错误态次按钮。
@@ -0,0 +1,17 @@
# T01 全状态视觉优化
## 范围
覆盖世系树、加载、横向阅读提示、空、失败五种页面状态,以及选中成员后的底部操作卡。
## 视觉决定
- 保留 T 系列宣纸山水背景、横向世系画布、成员节点和现有状态面板。
- 底部成员卡的两项操作改用无中心结遮挡的 A 系列 v2 主/次按钮资产,按钮字使用清晰无衬线体。
- 提升工具栏辅助信息、节点关系/年份、状态说明和底部成员元数据字号,不改变横向滚动结构。
## 验证
- 五种页面状态均以 `412×915` 截图;树态额外检查底部操作卡。
- 运行 T01 合同、运行 smoke、编译审计和差异检查。
- H5 只作为内部候选,Android 横向拖动、系统返回手势和触摸目标仍待复核。
@@ -0,0 +1,19 @@
# T03T06 全状态视觉优化
## 范围
- T03:成员详情、加载、失败。
- T04/T05/T06:各自表单、成功、冲突、失败;T06 另含冲突说明弹窗。
## 视觉决定
- 保留 T 系列背景、G03 宣纸面板、成员资料行、共享表单和全局 A 系列弹窗。
- T03 提升成员元数据、资料行、亲属关系和失败说明字号。
- T04–T06 共享表单提升说明、字段、输入和底部提示字号;页面仍分别使用各自任务文案。
- 不改业务状态转换,也不为共用页面重复生成资产。
## 验证
- T03 三态、T04/T05 各四态、T06 五态均以 `412×915` 截图。
- 运行 T03–T08 合同、运行 smoke、编译审计和差异检查。
- 当前仅为 H5 内部候选,Android/HBuilderX 软键盘、焦点滚动和触摸目标仍待验证。
@@ -0,0 +1,18 @@
# T07T08 全状态视觉优化
## 范围
- T07:成员列表、加载、空、失败。
- T08:隐私、纪念、无权限。
## 视觉决定
- 保留 T 系列背景、成员状态卡、输入框、标签按钮和说明面板。
- 提升 T07 搜索、人数摘要、成员元数据、身份状态与空/失败说明字号。
- 提升 T08 标签、状态摘要和三条说明文字字号,保持三种状态同一结构切换。
## 验证
- 七态均在同一 Chrome 标签页以 `412×915` 截图。
- 运行 T03–T08 合同、运行 smoke、编译审计和差异检查。
- 当前为 H5 内部候选,Android/HBuilderX 输入法、权限接口和字体缩放仍待复核。
@@ -0,0 +1,57 @@
# AppLoading 必要加载动效设计
## 背景与问题
G01 加载态当前使用公共 `components/AppLoading.vue`。组件原本包含印牌与如意结呼吸动画,但在 `prefers-reduced-motion: reduce` 下将两者统一设为 `animation: none`。当前 Chrome 正处于该模式,运行检查确认动画数量为 0,因此页面虽然显示“正在整理家谱”,视觉上却完全静止,用户无法快速判断加载是否仍在进行。
## 用户决定
- 采用“印牌明暗呼吸”方案。
- 即使系统开启“减少动态效果”,加载态仍须保留低幅度但明确可见的必要反馈。
- 减少动态效果模式只取消缩放、旋转和位移,不再把加载动画全部关闭。
- 当前只返工 G01 加载态及其直接依赖的公共加载组件、测试和证据;不切换到其他 G01 状态,不修改接口或业务逻辑。
## 视觉与动效
### 正常动态模式
- 保留现有红金家谱印牌与如意结真实资产,不新增图片、CSS 图形或文字图标。
- 印牌使用透明度与轻微缩放组合的循环呼吸,节奏清晰但不跳动,不改变布局。
- 如意结使用错峰透明度呼吸,作为辅助节奏,不抢夺印牌主视觉。
- 不旋转、不上下漂移、不闪烁,不改变加载文案。
### 减少动态效果模式
- 印牌与如意结保持固定尺寸和固定位置,`transform` 始终为 `none`
- 两者继续使用透明度循环;印牌为主节奏,如意结错峰变化。
- 明暗范围必须足以让用户在约 1 秒内感知页面仍在工作,但不得出现完全消失或高频闪烁。
- 加载文案和说明保持静态,避免文字跳动影响阅读。
## 实现边界
- 动效规则仍由 `components/AppLoading.vue` 单一维护,G01 不复制动画样式。
- 不改变 `variant``text``description` 组件契约。
- 不改变 G01 的 `isLoading`、路由状态、页面布局、背景、标题栏和底部导航。
- 公共组件的修正只涉及加载反馈语义;不得顺手调整其他页面布局或视觉。
- 不使用 JavaScript 定时器驱动动画,继续使用低成本的 `opacity``transform`
## 测试与验收
1. 先更新公共加载组件契约,使旧的 `prefers-reduced-motion``animation: none` 行为触发失败,并确认 RED。
2. 最小修改 `AppLoading.vue` 后确认聚焦契约 GREEN。
3. 在当前 412×915 Chrome G01 加载态检查:
- `prefers-reduced-motion: reduce` 为真;
- 印牌和如意结仍存在运行中的动画;
- 不同时间点透明度确实变化;
- `transform` 保持 `none`
- 页面仍是 G01 加载态,无错误态或列表内容串入。
4. 捕获并人工检查当前加载态截图;截图只能证明布局和静态帧,动画是否运行必须以运行时多时点证据为准。
5. 运行 G01 加载态、公共 AppLoading 及直接受影响的加载组件契约,再执行 `git diff --check`
6. H5 证据仅为内部候选;Android/HBuilderX 动画与性能仍未验证,不得宣称最终完成。
## 通过标准
- 用户能在当前系统的减少动态效果模式下明确感知加载仍在继续。
- 动画没有缩放、旋转或位移,且不会造成明显闪烁。
- 当前页面结构、文案和其他状态不发生无关变化。
- 用户重新审核 G01 加载态并明确说“通过”后,才继续展示失败态;G01 整页在全部状态通过前仍保持 `[!]`
@@ -0,0 +1,86 @@
# F01 家族动态基准页返工设计
## 目标
将 F01“家族动态”正常列表态重做为 F 模块视觉基准:消除动态卡框外白底和申请状态语义,强化内容阅读层级,并提升快捷入口的触控可用性。
## 修改范围
仅修改 F01 及其直接测试、候选资产和视觉证据:
- 四个内容快捷入口。
- 动态列表卡片。
- 空态与失败态共用的内容面板外观。
- 与以上结构直接相关的 F01 合同和运行时检查。
保持不变:
- 页头、“发布”入口、家族圈上下文、长背景和底部 Tab。
- 加载、列表、空、失败四种状态及现有路由行为。
- 发布按钮的项目现有真实资产。
- 其他 F 页面及公共组件。
## 视觉方案
### 家书动态卡
- 新增 F01 专属透明 PNG 内容卡资产,作为动态卡和空/失败内容面板的统一基底。
- 资产使用浅宣纸内面和克制的细金边,外框之外必须透明;不得出现白色矩形底、申请状态红竖线或右上状态短横。
- 设计语言偏“家书/家族记事”,与 T07 成员题签卡明显区分。
- 卡片宽度跟随内容区;412px 视口下单卡高度约 116–132px,内容在卡内垂直居中。
### 信息层级
- 标题为第一层级,使用楷体并保持最高字号与字重。
- 分类与时间位于标题上方,作为低权重辅助信息;只用文字强调,不增加装饰竖线。
- 正文位于标题下方,最多两行,颜色和字号低于标题。
- 发布人位于卡片底部,保持可读但不抢标题。
- 两张卡的宽度、高度和内边距完全一致。
### 快捷入口
- 保留“谱文、相册、礼仪、备忘”及现有真实卷轴按钮资产。
- 每个入口触控高度固定不低于 44 CSS px;320px 宽度下仍不得低于该值。
- 四个入口在一张全宽次级卷轴框内使用四列等宽布局,只保留一套卷轴端头和外框。
- 入口之间保持清楚间隔,不裁切文字,不出现横向页面溢出。
### 状态与操作
- 加载态继续使用 `AppLoading`
- 空态和失败态使用同一张 F01 专属透明内容卡,不复用申请状态卡。
- 失败态“重新查看”和正常/空态“发布家族动态”继续使用现有项目按钮资产。
## 响应式与可用性
- 自动检查 320×568、360×640、360×800、412×915。
- 四档均不得产生横向溢出、文字裁切、卡片错位或 Tab 遮挡。
- 快捷入口和主要操作触控高度均不低于 44 CSS px。
- 页面保持现有内容滚动和底部安全区域处理。
## 验收条件
- 动态卡和状态面板没有框外白底。
- F01 不再引用 `application-status-card.png`
- 动态卡不含申请状态式红竖线或右上短横。
- 标题是第一视觉焦点,分类/时间、正文、发布人层级清楚。
- 快捷入口四档尺寸均达到 44px 触控高度且没有页面横向溢出。
- 加载、列表、空、失败、发布和内容入口行为保持可用。
- F01 聚焦合同、既有 F 系列合同和 H5 运行时检查通过。
## 证据边界
- H5 截图只作为内部候选证据。
- Android/HBuilderX 真机或模拟器仍未验证,不据此宣称完成。
## 非目标
- 不修改其他 F 页面。
- 不抽取新的公共组件。
- 不连接真实接口。
- 不改动底部 Tab、页头或发布流程。
## 自检
- 无 TBD、TODO 或未决资产选择。
- 资产、布局、状态和验收条件没有冲突。
- 范围严格限定为 F01 及其直接测试和候选证据。
@@ -0,0 +1,78 @@
# G01 添加家谱底部弹层设计
## 视觉目标
唯一视觉目标为 `docs/design/mockups/2026-07-19/g01-add-dialog-paper-sheet-target.png`。采用用户选择的第 3 版结构、第 1 版卷轴按钮,并将右上“关闭”文字替换为关闭图标。
## 范围
只返工 G01“我的家谱”的“添加家谱”弹层。正常列表态、空态、加载态、失败态、切换家谱弹层、独立滚动及其他页面均不修改;三个入口继续调用现有跳转逻辑,不对接接口。
## 删除当前无用元素
- 弹层不再引用 `a01-scroll-dialog-v3.png` 完整卷轴背景。
- 删除完整封闭金框、四角祥云、底部山水和为这些装饰预留的空白区域。
- 删除标题与关闭入口为躲避祥云而增加的偏移样式。
- 删除文字“关闭”,不保留旧“取消”入口。
- 删除本轮被否决实现产生且不再被模板使用的 G01 添加弹层专用样式;不删除任何现有资产文件、候选截图、测试或文档。
## 背景资产与伸缩规则
- 用户确认采用“单张完整 PNG + 九宫格伸缩”方案。唯一底板资产为 `static/assets/modules/genealogy/transparent/g01-add-sheet-background-v3.png`,源尺寸 `1536×1024`
- 顶部透明轮廓、左右圆肩、古金收边、中央如意结和连续宣纸纹理全部属于同一张资产;弹层不得再把矩形宣纸、顶部过渡图或独立结饰拼接成底板。
- 资产按九宫格中的纵向三段规则渲染:顶部装饰安全区固定、底部收口固定,只允许中间无关键装饰的宣纸区域纵向伸缩。弹层变高时,顶部曲线、如意结、金线粗细和圆肩比例不得发生纵向变形。
- 当前三按钮状态的弹层最小高度为 `780rpx`;在 412px 宽审批视口中约为 `429px`,完整底板的可见弧形上沿应到达用户截图箭头所指的列表中部位置。
- 标题、说明和按钮组组成一个内容整体。三按钮状态中,该整体必须在弹层可用区域内垂直居中,新增高度不得只堆积为顶部或底部的大块空白。
- 居中使用内容容器的上下自动边距实现,不使用可导致超高内容顶部被裁掉的 `justify-content: center`,也不使用只适配三按钮数量的固定坐标。
- 未来出现更多按钮、表单或说明时,上下自动边距归零,同一底板随内容自然增高,中间纸面扩展;达到 `calc(100vh - 80rpx)` 后由内容区纵向滚动,并保证首项可滚动到可视区域顶部。
- 本轮只在 G01 落地该伸缩结构,不提前迁移其他页面,也不为尚未出现的第二个消费者抽取全局组件;后续真实页面需要同类底部弹层时,再以该资产和伸缩规则为单一视觉合同抽取复用。
## 新弹层结构
- 弹层是贴紧视口底部的全宽宣纸面板,处理底部安全区。
- 面板底面只使用完整底板资产 `static/assets/modules/genealogy/transparent/g01-add-sheet-background-v3.png`;不再引用 `page-paper.jpg``a01-paper-transition-v1.png` 拼接当前弹层。
- 左上依次显示标题“添加家谱”和说明“建议先搜索已有家谱,避免重复创建”。
- 标题、说明和三个操作统一放入 `add-dialog__body` 内容容器;该容器以 `margin-block: auto`(落地时使用兼容现有构建链的上下 `auto` 边距写法)完成三按钮状态的垂直居中。
- 右上使用独立关闭图标,实际可见朱砂叉号约 `34rpx`,触控区域不小于 `80rpx × 80rpx`。新增真实透明 PNG `static/assets/modules/genealogy/transparent/g01-dialog-close.png`,以选定目标中的朱砂细线关闭图标为视觉参考生成;由于源图透明画布约占六成,图片元素使用 `80rpx × 80rpx` 才能达到目标视觉尺寸。不从截图裁切,也不使用文字字符、CSS 线条或手绘 SVG 冒充。
- 关闭控件放入标题容器并相对标题定位,不能再使用相对整个弹层的固定顶部坐标;因此三按钮居中、六按钮增高或超高滚动时,关闭图标始终跟随标题。
- 三个操作直接复用项目现有 `AppButton`,不得根据效果图重新绘制、生成或近似实现按钮皮肤:
1. `AppButton` 主按钮继续使用 `static/assets/foundation/transparent/a01-scroll-primary-v3.png`,文案“搜索家谱”;
2. `AppButton` 次按钮继续使用 `static/assets/foundation/transparent/a01-scroll-secondary-v3.png`,文案“邀请码加入”;
3. `AppButton` 次按钮继续使用 `static/assets/foundation/transparent/a01-scroll-secondary-v3.png`,文案“继续创建家谱”。
- 三个按钮之间统一使用 `24rpx` 间距;在 412px 审批视口中约为 `13.2px`。间距只施加在相邻 `AppButton` 之间,不改变按钮图片或按钮内部文案。
- 三个按钮以当前红色主按钮的可见宽度为统一基准。仅在 G01“添加家谱”弹层内,将三个按钮容器设为响应式 `595rpx × 96rpx` 基准并水平居中;412px 审批视口中约为 `327px × 53px`,两张按钮皮肤均继续使用 `aspectFit`,使左右卷轴端点对齐且不做横向拉伸。
- 等宽规则不得修改公共 `AppButton` 的默认尺寸,不得影响其他页面,不得生成或改写按钮图片资产。按钮宽度受弹层可用宽度约束,在 320px 档位按 rpx 等比缩放并保持 `max-width: 100%`
- 按钮组增高后仍作为 `add-dialog__body` 的一部分整体居中;`780rpx` 最小高度和可见上沿位置不变,由上下自动边距等量缩小吸收新增高度,不得把按钮组单独推向顶部或底部。
- 遮罩使用当前已验证的深度,使后方列表退居背景但仍能辨认所在页面。
## 交互
- 点击关闭图标关闭弹层。
- 点击遮罩关闭弹层。
- Android 返回键优先关闭弹层,不离开 G01。
- 点击弹层内部不触发遮罩关闭。
- 三个入口继续复用现有 `applyToJoin``joinByInvite``createGenealogy`,不改变路由契约。
## 响应式与可访问性
- 320px 宽度下 `780rpx``24rpx` 按钮间距等比缩放,页面不得横向溢出,标题、说明、关闭图标和三个按钮不得互相遮挡。
- 568px 高度下三个操作必须同时可达;内容超过安全高度时弹层内容区纵向滚动,且滚动起点不得裁掉标题或关闭入口。
- 三按钮基准高度与长内容压力高度必须使用同一张底板;压力高度只扩展中间纸面,顶部曲线、圆肩和如意结尺寸保持一致。
- 关闭图标必须有可访问名称“关闭”,不能只依赖图形表达。
- 弹层打开时阻止后方独立列表滚动穿透,关闭前后保持列表位置。
## 验证
- 先修改 G01 视觉契约,使其因仍引用完整卷轴背景、仍使用文字关闭、仍拼接矩形宣纸与顶部过渡图,或缺少完整可伸缩底板而失败。
- 最小实现后运行 G01 视觉、空态、加载态和失败态契约,且不得放宽既有阈值。
- 运行时只检查当前“添加家谱”状态,在 320×568 和 412×915 验证 `780rpx` 最小高度、`24rpx` 按钮间距、三个按钮左右端点对齐、内容整体居中、关闭、遮罩与滚动位置;以六按钮验证自然增高,并以超过最大高度的长内容验证可从顶部开始滚动。最后恢复 412×915 三按钮状态并保持弹层打开。
- 在唯一 9222 Chrome 项目标签页中展示;不新开浏览器或第二个项目标签页。
- H5 截图只作为内部候选证据;Android/HBuilderX 仍单独保留为未验证项。
## 非目标
- 当前仅建立可伸缩视觉合同,不在只有一个消费者时抽取新的通用弹层组件。
- 不修改切换家谱弹层。
- 不删除任何现有静态资产文件或历史候选证据。
- 不更改接口、数据结构或业务跳转。
- 不冻结 G01,不更新为 `[x]`
@@ -0,0 +1,65 @@
# G01 切换家谱弹层设计规格
## 目标
只返工 G01“我的家谱”的“切换当前家谱”弹层。弹层在两条数据时紧凑、居中、易读;家谱数量增加时,完整背景随内容自然增高,达到视口安全上限后仅列表内部滚动。
## 范围
- 保留现有家谱数据、当前项判断、选择逻辑和遮罩关闭逻辑,不对接接口。
- 不修改 G01 其他状态,不修改公共 `AppDialog``AppButton` 或其他页面。
- 不生成、不覆盖、不删除任何图片资产。
- 当前状态未经用户明确通过,不标记 G01 为 `[x]`,不冻结页面。
## 完整背景与伸缩合同
- 唯一背景资产继续使用 `static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png`,源尺寸为 `1860×1560`
- 不再用绝对定位的 `<image mode="aspectFit">` 把整张背景限制在固定高度内;容器改为单张完整 PNG 的九宫格 `border-image` 渲染。
- 九宫格源切片使用 `300 260 360 260 fill`:顶部金框与祥云、左右边框和底部山水与如意结属于固定区;只有中间无关键装饰的宣纸区域允许纵向伸缩。
- 九宫格显示边宽使用 `110rpx 48rpx 132rpx 48rpx`,分别保留顶部装饰、左右边框和底部山水安全区。
- 顶部祥云、四角纹样、边框粗细、底部山水和中央如意结不得随弹层高度纵向拉长,也不得拆成多张图片拼接。
- 弹层宽度保持 `670rpx``max-width: 100%`;两项状态的最小高度为 `600rpx`,最大高度为 `calc(100vh - 120rpx)`
- 内容少时背景保持 `600rpx` 的紧凑基准;内容增加时背景和容器同步自然增高;达到最大高度后容器不再增高,家谱列表独立纵向滚动。
## 布局
- 弹层继续位于视口中央,遮罩深度沿用当前值。
- 内容安全区使用 `120rpx 58rpx 140rpx` 内边距,标题完整落在顶部金框和祥云下方,不得与任何装饰重叠。
- 标题“切换当前家谱”居中显示,沿用朱砂色和楷体层级。
- 标题下方使用独立列表容器;两条数据时列表不产生滚动条,也不在末项下方留下超过 `72rpx` 的纯内容空白。
- 每行最小高度 `112rpx`,整行均可点击;名称、元信息和右侧状态保持稳定左右对齐。
- 名称字号 `32rpx`;元信息字号从 `21rpx` 提升为 `24rpx`;右侧“当前/选择”字号从 `23rpx` 提升为 `25rpx`
- 当前项使用轻量朱砂底色和边线反馈,不只依赖名称变红;可切换项保持宣纸底色。
## 关闭与交互
- 移除底部不可见的文字“关闭”入口。
- 右上复用 `static/assets/modules/genealogy/transparent/g01-dialog-close.png`,图片画布 `80rpx × 80rpx`,点击热区不小于 `80rpx × 80rpx`,可访问名称为“关闭”。
- 关闭热区相对弹层定位为 `top: 78rpx; right: 34rpx`,可见朱砂叉号与标题处于同一视觉行,并避开顶部金框。
- 点击关闭图标或遮罩关闭弹层;点击弹层内部不关闭。
- Android 返回键在切换弹层打开时优先关闭切换弹层;若添加家谱弹层打开,则优先关闭添加家谱弹层;两者均未打开时交还页面导航。
- 点击非当前家谱后继续执行现有 `selectGenealogy(item)`:更新当前家谱并关闭弹层,不增加 Toast、Loading 或接口请求。
## 响应式和长内容
-`320×568``360×640``360×800``412×915` 下,弹层不得横向溢出,标题、关闭图标、列表和底部固定装饰不得互相遮挡。
- 两项状态在四档尺寸下均应保持紧凑且完整可见。
- 六项压力状态下背景自然增高;若仍低于最大高度,不显示列表滚动条。
- 十二项压力状态下弹层封顶,标题、关闭图标和背景顶部固定不动,只有列表可从第一项滚动到最后一项。
- 列表滚动不得穿透到后方 G01 独立列表;关闭前后保持后方列表位置。
## 验证
- 先在 `tests/g01-visual-contract.ps1` 增加失败合同,覆盖:移除固定 `720rpx`、使用完整背景九宫格、`600rpx` 最小高度、最大高度、标题安全区、列表滚动容器、关闭图标及 Android 返回键。
- 最小实现后运行 G01 视觉、空态、加载态和失败态合同,不放宽任何既有阈值。
- 只复用 9222 上唯一的 Chrome 项目标签页,依次验证四档两项状态、六项自然增高和十二项封顶滚动。
- 捕获同尺寸修改前/修改后组合证据,检查标题压线、无效空白、背景装饰变形、文本可读性和关闭入口。
- 最终恢复 `412×915`、两条真实数据、切换弹层打开的状态,等待用户审批。
- H5 截图仅为内部候选证据;Android/HBuilderX 真机或模拟器仍为未验证项。
## 非目标
- 不抽取新的全局弹层组件。
- 不改造“添加家谱”底部弹层。
- 不修改家谱切换业务数据或路由。
- 不执行 Git 提交、推送、重置或检出操作。
@@ -0,0 +1,94 @@
# 模块基准页加速视觉审核设计
## 目标
今晚完成剩余活动页面的 H5 浅色国风视觉统一与用户审核,不再逐页逐状态实时展示。Android/HBuilderX、真实接口、真机性能和业务功能继续明确保留为未完成项。
## 已确认基准
- A:现有 A 系列成果保持不变,本轮不返工。
- G:G01“我的家谱”已经完成全部适用状态与五档尺寸审核,作为 G 模块基准。
- T:T07“成员目录”作为常规页面基准。
- F:F01“家族动态”作为模块基准。
- RR01“人物录”作为模块基准。
- N:N01“消息中心”作为模块基准。
- MM01“我的”作为模块基准。
候选选择依据记录在:
`docs/design/screens/runtime/2026-07-19/module-baseline-audit/audit.md`
## 执行顺序
1. 完整审核并完善 T07,确定 T 模块的标题、背景、搜索、列表、卡片、字体、间距和状态标准。
2. 完整审核并完善 F01,确定 F 模块基准。
3. 完整审核并完善 R01,确定 R 模块基准。
4. 完整审核并完善 N01,确定 N 模块基准。
5. 完整审核并完善 M01,确定 M 模块基准。
6. 五个新增基准页全部通过后,再完善 T01 世系树。T01 是特殊结构页,保留树画布、世代关系和成员操作结构,只继承 T07 的视觉令牌与公共状态组件。
7. T01 通过后,批量处理剩余页面。
8. 普通页面按以下顺序推进:
- 按 G01 统一 G03、G05、G06、G08、G09、G10、G11、G12。
- 按 T07 统一 T03、T04、T05、T06、T08。
- 按 F01 统一 F02F10。
- 按 R01 统一 R02R11。
- 按 N01 统一 N02。
- 按 M01 统一 M02M10。
## 基准页负责的内容
每个模块基准页拥有该模块的:
- 长背景与内容层透明度关系。
- 页面标题栏、返回入口和右侧操作。
- 主标题、说明文字、状态文字和正文的字号、字重与颜色。
- 页面水平边距、模块间距、卡片间距和底部安全区。
- 常规卡片、列表行、筛选/搜索区和主要操作的视觉语言。
基准页不拥有其他页面的业务结构。列表、表单、详情、设置、时间轴、树画布和编辑器继续保留各自适合任务的内容组织方式。
## 全局组件规则
以下内容不随模块重新设计,统一使用已经建立的项目自定义组件和 A/G01 已确认标准:
- `AppButton`
- `AppDialog`
- `AppToast`
- `AppLoading`
- 空态、失败态、无权限态的标题层级和恢复动作
禁止重新引入原生 Toast、Modal、Loading 或 ActionSheet。弹层允许根据内容伸缩,但必须沿用项目已经确认的宣纸、朱砂、古金与关闭图标体系。
## 批量审核方式
- 基准页与 T01:仍由用户逐个查看关键状态和代表尺寸,明确通过后才能成为推广标准。
- 普通推广页:自动运行已有静态合同和 Chrome 运行检查,覆盖默认状态、适用关键状态、四档尺寸、横向溢出和基础交互。
- 每个模块生成一张联系表,包含全部页面的默认态和有视觉差异的关键状态。
- 用户按模块审核联系表;没有问题则整批通过,只有异常页才单独打开返工。
- 不使用单张默认态代替状态检查;联系表背后必须有真实渲染和自动验证证据。
## 特殊页面与例外
- T01:树画布特殊页,不能机械套用 T07 列表 DOM。
- T03:必须从带成员上下文的入口或受控参数进入,不能把直达失败态当作默认态。
-`⚠` 的无真实入口页面:可以按模块基准完成视觉候选,但必须继续标注流程入口缺口,不能宣称真实流程通过。
- 公共 `ModulePage` 当前存在页面介绍与面板标题重复的问题;推广时应消除重复层级,不把重复标题固化为基准。
## 通过条件
页面或模块批次只有同时满足以下条件才能记为 H5 视觉通过:
- 与所属模块基准一致,且保留自身任务结构。
- 适用状态完整,没有加载、空、失败、无权限或业务状态的明显断层。
- 320×568、360×640、360×800、412×915 无横向溢出、裁切、遮挡或不可达操作。
- 自定义弹层和反馈组件使用正确。
- 聚焦合同、运行检查和 `git diff --check` 通过;不放宽阈值换取绿灯。
- 用户通过基准页或模块联系表给出明确结论。
## 明确不在本轮完成范围
- 真实接口与数据持久化。
- Android/HBuilderX 真机或模拟器验证。
- 4GB Android 长背景解码、切页与内存回收。
- 系统字体放大、读屏和真实软键盘。
- 无入口页面的产品入口补全,除非用户另行授权进入功能流程设计。
@@ -0,0 +1,50 @@
# N01 消息中心基准页设计
## 目标
将 N01 作为 N 系列视觉基准页。只处理 H5 审批所需的本地演示状态,不对接接口,不修改其他页面。
当前审计证据:`docs/design/screens/runtime/2026-07-19/n01-audit/01-list-412x915.png`。现状的主要问题是消息卡带有边框外白底、错误复用入谱申请卡片素材、未读与已读层级不清。
## 页面结构
- 顶部继续使用项目 `PageHeader`,标题“消息中心”,右侧操作“全部已读”。
- 内容区只包含消息列表、页面状态卡和必要操作,不增加编号介绍块。
- 每条消息使用 N01 独立拥有的透明金色线框位图;线框内不带底色,不复用其他模块的业务卡片。
- 卡片信息顺序为:状态与时间、标题、摘要。未读状态使用克制的朱红色,已读状态降为灰褐色。
- 整张消息卡均可点击;点击未读消息后切换为已读,不增加额外图标或角标。
- “前往入谱审核”继续使用现有项目卷轴按钮,放在列表之后并与消息卡保持明显间距。
## 审批状态
N01 依次审批以下状态,每次只显示一个:
1. 正常列表(同时展示未读与已读层级)。
2. 单条消息切换为已读。
3. 全部已读及项目自定义 Toast。
4. 加载态,使用项目 `AppLoading` 动画。
5. 空态,使用同一透明线框和简短说明。
6. 失败态,使用同一透明线框及“重新查看”卷轴按钮。
7. 审核跳转操作的可用性验证。
## 响应式与交互
- 在 320×568、360×640、360×800、412×915 四档尺寸检查,不把响应式逐档交给用户审批;仅在发现问题时返工 N01。
- 卡片宽度随内容区拉伸,高度由内容和最小触控尺寸共同约束,不固定为某一截图高度。
- 所有可点击区域满足至少 44px 的触控高度。
- Toast、Loading 和按钮全部使用项目自定义组件。
## 数据与范围
- N01 页面文件是本轮状态与交互的唯一所有者:`pages/notification/n01-message-center.vue`
- 使用本地模拟消息,不连接 API。
- 保留现有入谱审核路由参数传递方式,不扩展审核业务。
- 不修改 N02 或其他 N/G/R/F/T/M 页面。
## 验收标准
- 列表、单条已读、全部已读、加载、空、失败和审核跳转均可独立复现。
- 页面中不存在消息卡边框外白底,也不引用 `application-status-card.png`
- 未读与已读无需依赖颜色也能通过文字识别。
- 聚焦契约、根页面视觉契约、运行时冒烟和响应式冒烟均通过。
- 只有用户对所有 N01 状态明确说“通过”后,才把 N01 标记为 `[x]`
@@ -0,0 +1,67 @@
# R01 人物录基准页返工设计
## 目标
将 R01“人物录”重做为 R 模块视觉基准:移除通用申请状态卡及框外白底,建立适合人物档案的透明名帖卡,并补齐本地搜索、空态和失败态的视觉审核入口。
## 已选方案
采用 R01 专用透明“人物名帖”线框卡+独立搜索区。
未选方案:
- 复用 T07 成员卡:开发较快,但会混淆人物目录与世系成员目录的模块语义。
- 仅给通用卡去白底:改动最少,但仍保留申请状态式红竖线和右上短横,不能成为 R 模块基准。
## 修改范围
-`pages/records/r01-people-list.vue` 从通用 `ModulePage` 改为 R01 独立页面。
- 新增 R01 自有透明人物名帖卡资产。
- 复用项目现有真实搜索框、按钮、Loading、Toast、页头和 R 模块背景。
- 新增 R01 聚焦契约和运行证据。
不修改 `ModulePage.vue`、其他 R 页面、接口、公共组件或底层路由合同。
## 正常列表态
- 页头只显示“人物录”;删除标题栏下方重复的印章、R-01 页码和说明文字。
- 搜索区直接承接标题栏,可按姓名、身份或世代本地筛选三条候选数据。
- 每张人物卡只显示姓名、身份、世代和低权重“查看人物档案”提示。
- 卡框外和卡内均透明,让 R 模块长背景连续显示;不出现白色矩形底、申请状态红竖线或右上状态短横。
- 三张卡等宽、等高、间距一致;姓名是第一视觉焦点。
- “新建人物”继续使用现有主卷轴按钮;当前视觉阶段点击后使用项目 `AppToast` 说明后续功能阶段开放。
## 状态
- 正常列表:三条人物记录。
- 搜索无结果:搜索框保留,同页显示明确无结果说明,清空关键词可恢复三条记录。
- 空态:说明尚未建立人物记录,保留“新建人物”入口。
- 失败态:说明人物录暂不可用,提供“重新查看”恢复入口。
- 不新增接口调用,不使用原生 Toast、Modal、Loading 或 ActionSheet。
## 响应式与可用性
- 自查 320×568、360×640、360×800、412×915。
- 搜索操作、新建人物和恢复操作触控高度不低于 44 CSS px。
- 四档均不得出现页面横向溢出、文字裁切、卡片错位或主要操作不可达。
## 验收条件
- R01 不再引用 `ModulePage``application-status-card.png`
- R01 不显示标题栏下方的印章、页面编号或重复说明块;后续页面审核遇到同类块时在各自页面范围内删除。
- R01 使用自有透明人物名帖卡,卡片区域没有白色矩形底。
- 搜索三条、无结果和清空恢复可在本地真实触发。
- 列表、空、失败和新建提示均使用项目组件与真实资产。
- 相关契约、R 系列视觉契约和 H5 运行检查通过。
## 证据边界
- H5 截图只作为内部候选证据。
- Android/HBuilderX 真机或模拟器仍未验证。
- 用户逐状态通过前不得把 R01 标记为 `[x]`
## 自检
- 无 TBD、TODO 或未决设计选择。
- 页面、资产、状态、交互和验收条件没有冲突。
- 修改范围严格限定为 R01 及其直接测试、资产、文档和截图。
@@ -0,0 +1,67 @@
# T07 搜索框与成员卡返工设计
## 目标
将 T07“成员目录”正常列表态修正为可作为 T 模块基准的页面,消除搜索框和成员卡框外白底,修正成员卡的视觉语义与信息密度。
## 范围
仅修改 T07
- 搜索框视觉与文字可读性。
- 成员卡底框、布局、层级和间距。
- 与上述视觉结构直接相关的 T07 合同测试与运行时验证。
保持不变:
- 页头、当前家谱上下文、整页背景。
- 搜索、空态、加载态、失败态及成员跳转行为。
- 其他任何页面与公共组件。
## 视觉方案
### 搜索框
- 停止使用带不透明框外白色像素的 `g06-search-input-wide.png`
- 改用 T07 独立的透明专用搜索框 `t07-search-input-frame.png`,避免与成员卡共用同一外形。
- 搜索框保留浅宣纸输入面,金色外框之外为透明,不额外叠加白色容器。
- 保留单行搜索结构和右侧“查找”操作;增强占位文字字号与对比度。
- 搜索框触控高度不低于 44 CSS px。
### 成员卡
- 停止使用 `application-status-card.png`,去除申请卡式红色竖线、右上短横和框外白底。
- 使用透明 `list-slip-frame.png` 作为成员卡边框。
- 不使用左侧世代印章;世代已经在第二行成员信息中呈现,避免重复信息和高饱和装饰抢夺姓名焦点。
- 单卡由约 133 CSS px 压缩到约 96108 CSS px;内容在卡内垂直居中。
- 信息层级:姓名为一级;“第 N 世 · 字辈 · 支系”为二级;身份或资料情况为三级小标签。
- 三张卡保持完全一致的宽度、高度与内边距。
## 响应式约束
- 320×568、360×640、360×800、412×915 均不得产生横向溢出。
- 320 宽度下允许缩小字号与横向间距,但不能截断姓名或主要世系信息。
- 页面保持纵向独立滚动。
## 验收条件
- 搜索框边框外没有白带或白色矩形底。
- 成员卡四周没有白色断层,页面背景纹理连续可见。
- 不再出现申请状态卡的红竖线和右上短横装饰。
- 姓名是每张卡的第一视觉焦点;三层信息可清楚区分。
- 搜索与成员点击行为保持可用。
- T07 聚焦合同测试、相关既有回归测试和四档 H5 运行时检查通过。
- 仅形成 H5 候选证据;Android/HBuilderX 验证仍不宣称完成。
## 非目标
- 不改造其他 T 页面。
- 不抽取新的公共组件。
- 不对接真实接口。
- 不生成新图片资产。
## 自检
- 无 TBD、TODO 或未决选择。
- 资产、布局、交互与验收条件无冲突。
- 修改范围严格限定为 T07 及其直接测试证据。
@@ -0,0 +1,23 @@
# M01 我的首页基准页设计
## 目标与范围
将 M01 做成 M 系列视觉基准。本轮只整理现有“资料、提醒、菜单、失败状态”的视觉与交互,不新增个人中心功能,不对接接口,不修改其他页面。
当前审计证据:`docs/design/screens/runtime/2026-07-20/m01-audit/01-ready-412x915.png`。现状的整页白色画框和待处理白底卡与页面背景割裂,内容层级不足。
## 页面结构
- 保留顶部“我的”和右侧“资料”入口,以及底部“我的”Tab。
- 删除覆盖大半屏的内层白色画框。
- 正常态采用一张连续的“个人谱牒册页”,不再把每个区域做成相同卡片。
- 册页上部突出印章头像、姓名、脱敏手机号与家谱身份,并以真实装饰分隔素材建立个人中心辨识度。
- 待处理提醒使用一条独立的紧凑提醒栏,以朱红标题强调,整栏跳转 N01。
- 下部设置“服务与设置”分组;账号与安全、帮助与反馈、关于家谱采用透明列表行、细分隔线和真实箭头图标,可自然向下增加后续功能。
- 失败态使用独立透明线框和项目自定义“重新查看”按钮。
## 状态与验收
依次审批正常首页和失败态;同时验证资料、提醒和三个菜单入口的目标路由。四档响应式尺寸由 Codex 内部检查,仅在发现问题时返工 M01。
只有全部状态和入口明确通过后,才将 M01 标记为 `[x]`。Android/HBuilderX 仍未验证,H5 截图只作为内部候选证据。
@@ -0,0 +1,38 @@
# T01 纵向世系轴基准设计
## 目标
将 T01 从“横向大画布上的独立成员卡”调整为可直接读懂亲缘关系的纵向世系轴,并作为后续世系树页面的视觉基准。本轮仅使用页面内模拟数据,不接接口。
## 已确认方向
- 始祖置于顶部中央,后代按世代向下分层。
- 同一世代横向排列;只有分支超过当前宽度时才横向拖动。
- 首屏必须完整看到始祖和主要下一代,不能以裁切节点暗示结构。
- 祖先到子代之间显示连续、清晰的关系线;选中成员以红色节点框和文字强调。
- 世代标题采用横向分层标签,不再使用分散的竖排小字。
- 选中成员信息放在底部透明信息栏中,保留“查看资料”“添加亲属”。
- 加载、阅读提示、空、失败继续由 T01 同页状态承载。
## 视觉资产
- 页面背景继续使用 T 系列 `ModulePageBackground`
- 成员节点复用现有透明卷框资产,树状态面板与底部信息栏复用现有透明金线框资产,并复制到 tree 模块命名空间。
- 移除 T01 对 `g03-create-flow-panel.png``g06-search-input-wide.png``application-status-card.png` 三个不透明表面资产的引用。
- 关系线属于数据关系可视化,可由布局元素绘制;不使用 CSS 仿造装饰图案、SVG 或表情符号。
## 状态与交互
- `loading`:保留 `AppLoading` 动画。
- `tree`:展示纵向三代世系,默认选中始祖;点击节点更新底部信息栏。
- `landscape`:说明“上下看世代、左右看同代分支”,操作后回到树态。
- `empty`:引导录入首代成员并跳转 T04。
- `error`:说明读取失败,重试后回到树态。
- 保留 T03、T04、T06、T07 路由入口。
## 响应式与验收
- 内部检查 `320×568``360×640``360×800``412×915`,不把响应式尺寸逐档交给用户确认。
- `412×915` 作为当前正常树态的首个视觉审批证据。
- H5 截图仅作内部候选;Android/HBuilderX 真机或模拟器验证仍未完成。
@@ -0,0 +1,84 @@
# 家谱 APP 夜间批量收敛交接(2026-07-20
## 1. 当前准确停点
- 当前阶段仍是 uni-app Android 项目的 H5 页面样式与视觉状态收敛,不对接接口。
- `pages.json` 注册 52 条活动路由;A02 已合并,A06 保持封存且源码、测试、文档和证据全部保留。
- 用户已明确通过 6 个模块基准:G01、T07、F01、R01、N01、M01。
- T01 已按用户最终选择的方案 1 完成“纵向世系轴”候选:始祖居上、后代逐代向下、同代横向展开、关系线连续、节点可选择、底部显示人物信息与操作。T01 尚未经过用户视觉审核,不得标记 `[x]`
- 其余未验收页面已按所属基准批量收敛,但仍保持 `[~]`;H5 自动检查不等于用户或 Android 验收。
## 2. 本轮实际修改
### T01
- `pages/tree/t01-tree-overview.vue` 改为纵向三代世系结构。
- 保留加载、树、阅读提示、空、失败、节点选中及 T03/T04/T06/T07 路由。
- 新增/复用 tree 模块透明资产:节点框、选中节点框、状态框、人物信息栏。
- 聚焦测试:`tests/t01-tree-state-contract.ps1``tests/t01-all-states-visual-contract.ps1`
### 普通 F/R/N/M 页面
- `components/ModulePage.vue` 是 28 个普通任务页的统一母版。
- 已删除重复的“页面编号 + 说明”介绍块,避免每页看起来像同一张编号展示页。
- 表单、列表、详情、时间轴、设置和状态仍是不同结构;加载统一使用 `AppLoading`
- 每个模块使用自己的长背景和 `module-content-frame.png` / `module-field-frame.png`,不再引用 G 模块旧不透明业务面板。
### 其他自定义页
- F02 改用 F01 所属 family 资产。
- T03、T04—T06、T08 改用 T01/T07 的 tree 透明资产;`TreeMemberForm.vue` 继续统一 T04—T06 的表单与结果结构。
- G03、G08—G12 改用 G01 的透明宣纸框、列表卷框和新 `g-form-field-frame.png`
- G05、G06 保留其特殊总览/搜索结构;G01 和六个已通过基准没有被批量母版覆盖。
## 3. 最新自动验证事实
- 5173 H5 服务仍在运行。
- 对 52 个活动页面和 2 个共享组件逐一请求 Vite 转换:`54/54` 返回 HTTP 200。
- PowerShell 静态契约总数 68:66 项通过,2 项为下述仓库治理失败。
- 页面与组件中的静态资产引用检查为 `0` 个缺失;活动页面和组件中的原生 `uni.showToast/showModal/showLoading/showActionSheet` 引用为 `0`
- `git diff --check` 退出码为 0;只有现有 LF/CRLF 转换提示,没有空白错误。
- `foundation-asset-audit.ps1` 仍因历史基础资产文件名含 `v1/v2/v3` 等候选式命名失败;这是既有已知问题,不能通过删除仍在使用的资产或放宽规则解决。
- `repository-handoff-size-contract.ps1``docs/design/screens` 现有 647 张历史截图超过 35 张上限失败。用户明确要求不删除或清理截图,所以保留并如实记录。
- 9222 Chrome 调试端口当夜未监听,但有 Chrome 进程连接 5173。为遵守“已有项目页时不得新开第二个浏览器或标签页”,没有另启浏览器,也没有伪造运行时 smoke、截图或四档响应式证据。
- Android/HBuilderX 真机或模拟器验证、系统字体放大、软键盘、4GB Android 长图性能仍未完成。
## 4. 明早接管顺序
1. 完整阅读:
- `AGENTS.md`
- `docs/交接记录.md`
- `docs/验收规划.md`
- `docs/夜间批量收敛交接_2026-07-20.md`
2. 运行:
- `git branch --show-current`
- `git log -1 --oneline`
- `git status --short`
- `git status --short --ignored`
3. 保留所有现有修改、未跟踪与忽略文件;不要执行 add、commit、push、reset、checkout,不要使用 worktree 或多代理。
4. 检查 5173 和 9222。若现有 Chrome 项目页仍存在,只能复用;若需要为了 CDP 重启 Chrome,必须先让用户明确确认关闭现有窗口后再启动唯一审批窗口。
5. 首先在 412×915 展示 T01 正常树态,等待用户结论;随后审核 T01 的加载、阅读提示、空、失败、节点选择和四档响应式。
6. T01 通过后,按模块抽查批量候选:G → T → F → R → N → M。优先检查带 `⚠` 的页面、表单长内容页和 320×568 小屏。
7. 只有用户明确通过的页面才能标 `[x]`。没有 CDP/截图/Android 证据时不得宣称最终完成。
## 5. Git 上传前注意
- 当前工作区本来就包含大量已修改和未跟踪文件,它们是用户要求保留的页面、测试、文档、截图、母版和候选资产,不属于可擅自清理的垃圾。
- 本轮没有执行任何 Git 暂存、提交、推送、重置或检出操作。
- 最后复核分支为 `main`HEAD 为 `887481a 验收完成40%``git status --short` 有 171 项,包含用户此前和本轮需要保留的大量修改/未跟踪内容。
- 上传前由用户自行审阅 `git status --short``git diff --check`;不要让新的 GPT 自动删除未跟踪文件来缩小提交。
- 若要解决 647 张截图导致的仓库大小契约失败,应由用户另行决定“哪些是长期证据、哪些只留本机”,获得明确授权后再做,不得在视觉施工会话里自行删除。
## 6. 可直接发给新 GPT 的提示词
```text
请全程使用中文。不要根据旧对话猜状态,不使用多代理或 worktree,不执行 git add、commit、push、reset、checkout,也不要删除、覆盖或清理任何现有修改、未跟踪文件、测试、文档、截图、母版和候选资产。
先完整阅读 AGENTS.md、docs/交接记录.md、docs/验收规划.md、docs/夜间批量收敛交接_2026-07-20.md,然后运行 git branch --show-current、git log -1 --oneline、git status --short、git status --short --ignored。
当前仍只做 uni-app Android 家谱项目的 H5 视觉审核,不接接口。G01、T07、F01、R01、N01、M01 六个模块基准已由用户明确通过。T01 已按方案1改成纵向世系轴,但尚未人工通过;其余页面已按模块基准批量收敛,仍是待验收候选。5173 当夜运行正常,52 页面 + 2 共享组件 Vite 转换为 54/54;9222 当夜未监听,因此不能把运行时 smoke、四档响应式或 Android 写成已完成。
先检查 5173 和 9222。如果已有 Chrome 项目页,只能复用同一个窗口和标签页;若必须重启以开放 9222,先征得我的明确同意。先在 412×915 展示 T01 正常树态,之后按加载、阅读提示、空、失败、节点选中和四档尺寸审核。只有我明确说“通过”才能标 [x]。发现问题只返工当前页。T01 通过后按 G、T、F、R、N、M 模块抽查批量候选,优先带 ⚠ 页面和 320×568 小屏。
请先简要复述当前停点,再开始检查;没有真实证据不得宣称完成。已知 foundation-asset-audit 和 repository-handoff-size-contract 的失败不能靠放宽阈值或删除用户文件处理。
```
+138
View File
@@ -0,0 +1,138 @@
# 家谱 APP 新会话审批交接(2026-07-19
> **历史文件提示(2026-07-20):** 本文件记录的 G01 停点已经失效。G01、T07、F01、R01、N01、M01 已通过;当前应先审核 T01,再按 `docs/夜间批量收敛交接_2026-07-20.md` 接管批量候选。保留本文件仅为历史证据,不得按下方旧提示重新打开 G01。
> 用途:当前会话结束后,在新的 Codex/GPT 会话中直接继续逐页视觉审批。
> 本文件记录的是审批停点与现有成果,不代表页面已经通过验收。
## 1. 当前准确停点
- 当前分支:`main`
- 当前提交:`887481a 验收完成40%`
- `pages.json` 实际注册 52 条活动路由。
- 52 个活动页面当前仍全部属于待用户审核候选;0 个已验收、0 个返工标记、0 个冻结。
- A06 已从 `pages.json` 和活动验收队列移除,但源码、测试、设计记录和历史证据保留,属于封存页面。
- 当前用户审核页:`G01 我的家谱`
- 当前展示状态:G01 正常列表态,地址为 `http://localhost:5173/#/pages/genealogy/g01-my-genealogies`412×915。
- 当前正常列表态只是已经在 Chrome 中打开,用户尚未给出“通过”或“返工”结论,不得修改为 `[x]`
- 当前没有未施工页面;后续主要工作是逐页、逐状态审批,发现问题后只返工当前审核页。
## 2. 新会话开始后的第一件事
1. 完整阅读 `AGENTS.md``docs/交接记录.md``docs/验收规划.md` 和本文件。
2. 运行 `git status --short`,保留全部已有修改和未跟踪文件。
3. 检查 `5173` H5 服务和 `9222` Chrome 调试端口是否仍在运行。
4. 如果现有 Chrome 已打开项目页面,必须复用当前窗口和当前标签页;不得再开第二个浏览器或第二个项目标签页。
5. 如果 Chrome 已关闭,才允许启动一个审批窗口;启动后后续所有状态都在同一标签页切换。
6. 先把 G01 正常列表态展示给用户,不修改代码;用户给出结论后再切下一状态。
内部截图脚本会复用已经打开的 `localhost:5173` 页面:
```powershell
node scripts/capture-chrome-page.js `
"http://localhost:5173/#/pages/genealogy/g01-my-genealogies" `
".genealogy-index" `
"docs/design/screens/runtime/<日期>/g01-approval/01-default-412x915.png" `
412 915
```
`docs/design/screens/runtime/` 是本机忽略缓存,只用于当轮查看,不能当作长期交接证据。
## 3. G01 必须继续审核的状态
按以下顺序在同一 Chrome 标签页逐个展示,每次只展示一个状态并等待用户结论:
1. 正常列表态:当前家谱、快捷入口、我创建的、我加入的、加入申请、底部添加入口。
2. 空态:搜索家谱、邀请码加入、创建家谱三个入口。
3. 加载态:公共 `AppLoading` 组件。
4. 失败态:错误说明与重新加载操作。
5. 添加家谱弹层:搜索、邀请码、创建三个入口与关闭操作。
6. 切换家谱弹层:候选列表、当前项、切换后的顶部信息和列表回顶。
7. 独立滚动态:标题栏、当前家谱大卡、四个快捷入口和分隔线固定;仅从“我创建的”开始滚动;底部“添加家谱”可到达。
8. 响应式复核:320×568、360×640、360×800、412×915G01 另做 412×1000 压力档。
只有以上适用状态全部看完,且用户明确说 G01 整页通过,才能把 G01 从 `[~]` 改成 `[x]` 并冻结。用户指出问题时改为 `[!]`,只返工 G01 相关代码、资产、测试和证据。
## 4. G01 已经确定、不得反复推翻的设计决定
- G 系列共用已经选定的连续长背景,不再使用旧 A/B/C 候选作为当前入口。
- 运行背景:`static/assets/modules/genealogy/opaque/genealogy-page-background-long.png`1440×3600。
- 公共组件:`components/GenealogyPageBackground.vue`
- 背景按页面宽度等比完整显示、固定贴底;左右不裁剪、不变形;共享图片画层不透明度为 28%。
- 顶部标题、当前家谱大卡片、四个快捷入口和金色分隔线固定,下方列表独立滚动。
- “加入申请”连续卡片之间为 12rpx 间距。
- “加入申请”说明采用 B 方案:`28rpx / 500 / #62584c / 1.4`;状态为 `27rpx / 600`
- 当前家谱和列表卡片的地区、成员数、角色、更新时间使用更深文字颜色与中等字重。
- 地区与成员数前的小图标已经放大并增强辨识度;不要误改谱封图标或文字字号。
- A 系列按钮、弹窗、Toast、Loading 是全局视觉标准;不得恢复原生 `uni.showToast``uni.showModal``uni.showLoading``uni.showActionSheet` 作为最终视觉。
- 普通页面返回操作使用左箭头,不使用“返回”两个文字。
## 5. 当前已经拥有的成果
### 5.1 页面和状态候选
- 52 个活动页面均已有基础内部 H5 样式候选。
- A 系列保留已确定的按钮、弹窗、Toast、Loading 和浅色国风视觉。
- G 系列以 G01 为风格母体,9 个活动 G 页面共用 G 长背景。
- T、F、R、N、M 已分别形成同一浅色国风主题下的独立模块长背景和页面内容气质。
- F、R、N、M 等模块的正常、空、加载、失败、成功、权限或业务状态候选已在前序内部审视中覆盖;这些只是候选,仍须按 `docs/验收规划.md` 由用户逐页审批。
### 5.2 公共组件
- `components/AppButton.vue`
- `components/AppDialog.vue`
- `components/AppToast.vue`
- `components/AppLoading.vue`
- `components/GenealogyPageBackground.vue`
- `components/ModulePageBackground.vue`
- `components/ModulePage.vue`
### 5.3 正式背景与可恢复输入
- G 长背景运行图:`static/assets/modules/genealogy/opaque/genealogy-page-background-long.png`
- T/F/R/N/M 长背景运行图位于各模块 `static/assets/modules/*/opaque/*-page-background-long.png`
- G 长背景正式母版与 ImageGen 原稿位于 `docs/design/assets/g01-background/masters/`
- T/F/R/N/M 正式母版与原稿位于 `docs/design/assets/module-backgrounds/masters/`
- 模块背景清单:`design-pipeline/manifests/module-page-backgrounds.json`
- 模块背景重建脚本:`design-pipeline/scripts/build_module_page_backgrounds.py`
- G01 历史 A/B/C 候选的正式可恢复输入、清单和脚本仍保留;`design-pipeline/generated/` 没有从 Git 下载时不代表资产遗失。
### 5.4 测试与证据
- 已有 G01、G03、G05、G06、G08G12、T、F、R、N、M 等页面的静态视觉合同和运行时 smoke。
- 最近一次内部结果记录为:主要 PowerShell 合同 60 项通过;14 项 JS 运行测试通过,A06 按封存合同跳过;编译检查和 `git diff --check` 通过。
- `foundation-asset-audit.ps1` 仍存在旧版资产引用审计失败,不能通过放宽阈值或删除仍在使用的资产来让它变绿。
- H5 截图只属于内部候选证据;最终仍缺 Android/HBuilderX 真机或模拟器复核。
- 长期代表证据位于 `docs/design/screens/handoff/`runtime、tmp、`.venv`、generated、unpackage 都是本机可再生成缓存。
## 6. G01 通过后的审批顺序
严格按照 `docs/验收规划.md` 的 52 项顺序推进,不跳过同一页面的适用状态:
- G 系列剩余页:G03、G06、G08、G09、G05、G10、G11、G12。
- T 系列:T01、T04、T07、T03、T05、T06、T08。
- F 系列:F01F10。
- R 系列:R01R11。
- N 系列:N01N02。
- M 系列:M01M10。
A01、A04、A05 仍保持 `[~]`,原因是 Android/HBuilderX 和真实滑动验证/真实接口链路尚未完成;不要把历史 H5 逐态确认误写成最终冻结。
## 7. 必须遵守的边界
- 不对接接口;当前只做页面样式和视觉状态审批。
- 不使用多代理,不使用 worktree。
- 不执行 `git add``commit``push``reset``checkout`
- 不删除、覆盖或清理现有修改、未跟踪文件、测试、文档、截图、母版和候选资产。
- 不为让测试变绿而放宽阈值,不删除仍被引用的历史证据。
- 不跨页修改与当前审核页无关的代码。
- 每次修改前先有真实运行截图或明确用户意见;修改后必须重新运行受影响合同、smoke、四尺寸截图和 `git diff --check`
- 没有 Android/HBuilderX 证据时,只能说 H5 内部候选通过检查,不能宣称最终完成。
## 8. 可直接复制到新会话的提示
```text
请全程使用中文。先完整阅读 AGENTS.md、docs/交接记录.md、docs/验收规划.md、docs/新会话审批交接_2026-07-19.md,并运行 git status --short。不得覆盖、删除、reset 或 checkout 当前任何修改和未跟踪文件;不得 git add、commit、push;不得使用多代理或 worktree。
现在继续逐页视觉审批,不对接接口。当前审核页是 G01,正常列表态已在 412×915 的唯一 Chrome 审批窗口中打开,但尚未通过。先检查 5173 和 9222 是否仍运行;如果已有 Chrome 项目页,只能复用当前窗口和当前标签页,不得再开浏览器。后续在同一标签页依次展示 G01 正常、空、加载、失败、添加家谱弹层、切换家谱弹层、独立滚动和四尺寸状态,每次只展示一个并等待我确认。没有我的明确“通过”,不得把页面标为 [x] 或冻结;我指出问题时只返工当前页面。H5 截图只是内部候选,Android/HBuilderX 仍缺验证。
```
+284 -18
View File
@@ -1,34 +1,300 @@
<!-- 页面编号F-01用途家族动态首页列表空态与失败状态 -->
<template>
<view class="family-page" :class="{ 'feed-state--list': feedState === 'list', 'feed-state--empty': feedState === 'empty', 'feed-state--error': feedState === 'error' }">
<view
class="family-page"
:class="{
'feed-state--list': feedState === 'list',
'feed-state--empty': feedState === 'empty',
'feed-state--error': feedState === 'error',
}"
>
<ModulePageBackground module="family" />
<view class="family-page__header"><PageHeader title="家族动态" action="发布" @action="toPublish" /></view>
<view class="family-page__header"
><PageHeader title="家族动态" action="发布" @action="toPublish"
/></view>
<view class="feed-content">
<view class="feed-heading"><text>汤氏家族圈</text><text>家宴通知与共同记忆</text></view>
<scroll-view class="feed-shortcuts" scroll-x :show-scrollbar="false">
<view v-for="item in shortcuts" :key="item.key" class="feed-shortcut" @click="openSection(item.key)"><image src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png" mode="aspectFit" /><text>{{ item.label }}</text></view>
</scroll-view>
<AppLoading
v-if="feedState === 'loading'"
text="正在整理家族动态"
description="请稍候,正在读取家宴、通知与共同记忆。"
/>
<view v-if="feedState !== 'loading'" class="feed-heading"
><text>汤氏家族圈</text><text>家宴通知与共同记忆</text></view
>
<view v-if="feedState !== 'loading'" class="feed-shortcuts">
<image
class="feed-shortcuts__skin"
src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png"
mode="scaleToFill"
/>
<view
v-for="item in shortcuts"
:key="item.key"
class="feed-shortcut"
@click="openSection(item.key)"
><text>{{ item.label }}</text></view
>
</view>
<template v-if="feedState === 'list'">
<view v-for="item in feeds" :key="item.id" class="feed-card" @click="openDetail(item)">
<image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill" />
<view><text>{{ item.tag }} · {{ item.time }}</text><text>{{ item.title }}</text><text>{{ item.content }}</text><text>发布人{{ item.author }}</text></view>
<view
v-for="item in feeds"
:key="item.id"
class="feed-card"
@click="openDetail(item)"
>
<image
class="feed-card__skin"
src="/static/assets/modules/family/transparent/f01-family-letter-card.png"
mode="scaleToFill"
/>
<view class="feed-card__copy">
<text class="feed-card__meta"
>{{ item.tag }} · {{ item.time }}</text
>
<text class="feed-card__title">{{ item.title }}</text>
<text class="feed-card__summary">{{ item.content }}</text>
<text class="feed-card__author">发布人{{ item.author }}</text>
</view>
</view>
</template>
<view v-else class="feed-state-card">
<image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill" />
<view><text>{{ feedState === 'empty' ? '还没有家族动态' : '家族动态暂不可用' }}</text><text>{{ feedState === 'empty' ? '发布第一条通知、家宴记录或家族故事。' : '请稍后重新进入,已有内容不会受到影响。' }}</text></view>
<view v-else-if="feedState !== 'loading'" class="feed-state-card">
<image
src="/static/assets/modules/family/transparent/f01-family-letter-card.png"
mode="scaleToFill"
/>
<view
><text>{{
feedState === "empty" ? "还没有家族动态" : "家族动态暂不可用"
}}</text
><text>{{
feedState === "empty"
? "发布第一条通知、家宴记录或家族故事。"
: "请稍后重新进入,已有内容不会受到影响。"
}}</text></view
>
</view>
<view class="feed-action" @click="feedState === 'error' ? feedState = 'list' : toPublish()"><image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" /><text>{{ feedState === 'error' ? '重新查看' : '发布家族动态' }}</text></view>
<view
v-if="feedState !== 'loading'"
class="feed-action"
@click="feedState === 'error' ? (feedState = 'list') : toPublish()"
><image
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="aspectFit"
/><text>{{
feedState === "error" ? "重新查看" : "发布家族动态"
}}</text></view
>
</view>
<AppTabbar active="family" />
</view>
</template>
<script setup>
import { ref } from 'vue';import { onLoad } from '@dcloudio/uni-app';import PageHeader from '@/components/PageHeader.vue';import AppTabbar from '@/components/AppTabbar.vue';import ModulePageBackground from '@/components/ModulePageBackground.vue';import{genealogyContext}from'@/utils/genealogy-context.js'
const genealogyId=ref('');const feedState=ref('loading');const feeds=[{id:1,tag:'团圆记忆',time:'今天 10:24',title:'端午家宴',content:'今年端午全家相聚,留下了许多温暖照片。',author:'汤正国'},{id:2,tag:'家族通知',time:'昨天 18:02',title:'修谱资料征集',content:'请家人补充老照片中的人物姓名与拍摄时间。',author:'谱主'}];const shortcuts=[{key:'articles',label:'谱文'},{key:'albums',label:'相册'},{key:'rituals',label:'礼仪'},{key:'memos',label:'备忘'}]
onLoad(query=>{genealogyId.value=query.genealogyId||genealogyContext.getCurrentGenealogyId()||'';feedState.value=query.state==='empty'?'empty':query.state==='error'?'error':'list'})
const toPublish=()=>uni.navigateTo({url:`/pages/family/f02-publish-feed?genealogyId=${genealogyId.value}`});const openDetail=item=>uni.navigateTo({url:`/pages/family/f03-feed-detail?feedId=${item.id}`});const openSection=key=>{const routes={articles:'/pages/family/f04-article-list',albums:'/pages/family/f07-album-list',rituals:'/pages/records/r05-ritual-list',memos:'/pages/records/r10-memo-list'};uni.navigateTo({url:routes[key]})}
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import AppLoading from "@/components/AppLoading.vue";
import PageHeader from "@/components/PageHeader.vue";
import AppTabbar from "@/components/AppTabbar.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import { genealogyContext } from "@/utils/genealogy-context.js";
const genealogyId = ref("");
const feedState = ref("loading");
const feeds = [
{
id: 1,
tag: "团圆记忆",
time: "今天 10:24",
title: "端午家宴",
content: "今年端午全家相聚,留下了许多温暖照片。",
author: "汤正国",
},
{
id: 2,
tag: "家族通知",
time: "昨天 18:02",
title: "修谱资料征集",
content: "请家人补充老照片中的人物姓名与拍摄时间。",
author: "谱主",
},
];
const shortcuts = [
{ key: "articles", label: "谱文" },
{ key: "albums", label: "相册" },
{ key: "rituals", label: "礼仪" },
{ key: "memos", label: "备忘" },
];
onLoad((query) => {
genealogyId.value =
query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
feedState.value =
query.state === "loading"
? "loading"
: query.state === "empty"
? "empty"
: query.state === "error"
? "error"
: "list";
});
const toPublish = () =>
uni.navigateTo({
url: `/pages/family/f02-publish-feed?genealogyId=${genealogyId.value}`,
});
const openDetail = (item) =>
uni.navigateTo({ url: `/pages/family/f03-feed-detail?feedId=${item.id}` });
const openSection = (key) => {
const routes = {
articles: "/pages/family/f04-article-list",
albums: "/pages/family/f07-album-list",
rituals: "/pages/records/r05-ritual-list",
memos: "/pages/records/r10-memo-list",
};
uni.navigateTo({ url: routes[key] });
};
</script>
<style scoped lang="scss">
.family-page{position:relative;min-height:100vh;overflow:hidden;background:$paper}.family-page__header,.feed-content{position:relative;z-index:2}.feed-content{padding:24rpx 24rpx 190rpx}.feed-heading text{display:block}.feed-heading text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:31rpx;font-weight:700}.feed-heading text:last-child{margin-top:6rpx;color:$ink-muted;font-size:20rpx}.feed-shortcuts{width:100%;margin-top:15rpx;white-space:nowrap}.feed-shortcut{position:relative;display:inline-block;width:150rpx;height:58rpx;margin-right:10rpx}.feed-shortcut image{position:absolute;inset:0;width:100%;height:100%}.feed-shortcut text{position:relative;z-index:1;display:flex;align-items:center;justify-content:center;height:100%;color:$ink;font-size:20rpx;font-weight:700}.feed-card,.feed-state-card{position:relative;width:100%;height:calc((100vw - 24px)*.34286);min-height:220rpx;margin-top:17rpx}.feed-card>image,.feed-state-card>image,.feed-action image{position:absolute;inset:0;width:100%;height:100%}.feed-card>view{position:absolute;inset:16% 9%;z-index:1}.feed-card text{display:block}.feed-card text:first-child{color:$brand-red;font-size:18rpx;letter-spacing:2rpx}.feed-card text:nth-child(2){margin-top:5rpx;color:$ink;font-family:STKaiti,KaiTi,serif;font-size:28rpx;font-weight:700}.feed-card text:nth-child(3){margin-top:6rpx;color:$ink-muted;font-size:19rpx}.feed-card text:last-child{margin-top:6rpx;color:#917b62;font-size:17rpx}.feed-state-card{margin-top:70rpx}.feed-state-card>view{position:absolute;inset:25% 12%;z-index:1;text-align:center}.feed-state-card text{display:block}.feed-state-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:30rpx;font-weight:700}.feed-state-card text:last-child{margin-top:13rpx;color:$ink-muted;font-size:20rpx}.feed-action{position:relative;width:100%;height:76rpx;margin-top:19rpx}.feed-action text{position:relative;z-index:1;display:flex;align-items:center;justify-content:center;height:100%;color:#fff9ed;font-size:24rpx;font-weight:700}
.family-page {
position: relative;
min-height: 100vh;
overflow: hidden;
background: $paper;
}
.family-page__header,
.feed-content {
position: relative;
z-index: 2;
}
.feed-content {
padding: 24rpx 24rpx 190rpx;
}
.feed-heading text {
display: block;
}
.feed-heading text:first-child {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: 31rpx;
font-weight: 700;
}
.feed-heading text:last-child {
margin-top: 6rpx;
color: $ink-muted;
font-size: 23rpx;
}
.feed-shortcuts {
position: relative;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
width: 100%;
height: 48px;
margin-top: 15rpx;
}
.feed-shortcuts__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.feed-shortcut {
position: relative;
z-index: 1;
width: 100%;
height: 48px;
min-height: 44px;
}
.feed-shortcut text {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: $ink;
font-size: 22rpx;
font-weight: 700;
}
.feed-card,
.feed-state-card {
position: relative;
width: 100%;
height: 230rpx;
min-height: 98px;
margin-top: 17rpx;
}
.feed-card__skin,
.feed-state-card > image,
.feed-action image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.feed-card__copy {
position: absolute;
inset: 15% 9%;
z-index: 1;
}
.feed-card text {
display: block;
}
.feed-card__meta {
color: #806a51;
font-size: 21rpx;
letter-spacing: 1rpx;
}
.feed-card__title {
margin-top: 5rpx;
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: 30rpx;
font-weight: 700;
}
.feed-card__summary {
margin-top: 6rpx;
color: $ink-muted;
font-size: 23rpx;
line-height: 1.4;
}
.feed-card__author {
margin-top: 6rpx;
color: $brand-red;
font-size: 21rpx;
}
.feed-state-card {
margin-top: 70rpx;
}
.feed-state-card > view {
position: absolute;
inset: 23% 10%;
z-index: 1;
text-align: center;
}
.feed-state-card text {
display: block;
}
.feed-state-card text:first-child {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: 31rpx;
font-weight: 700;
}
.feed-state-card text:last-child {
margin-top: 13rpx;
color: $ink-muted;
font-size: 23rpx;
line-height: 1.45;
}
.feed-action {
position: relative;
width: 100%;
height: 76rpx;
margin-top: 19rpx;
}
.feed-action text {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #fff9ed;
font-size: 24rpx;
font-weight: 700;
}
</style>
+167 -3
View File
@@ -1,4 +1,168 @@
<!-- 页面编号F-02用途发布家族动态与提交结果 -->
<template><view class="publish-page" :class="{'publish-state--form':publishState==='form','publish-state--success':publishState==='success','publish-state--error':publishState==='error'}"><ModulePageBackground module="family"/><view class="publish-page__header"><PageHeader title="发布动态"/></view><view class="publish-panel"><image class="publish-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill"/><view v-if="publishState==='form'" class="publish-form"><text>记录此刻</text><text>分享通知、活动、家族故事或一段共同记忆。</text><view class="publish-field"><image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill"/><textarea v-model="content" maxlength="300" placeholder="写下想对家人说的话" placeholder-class="publish-placeholder"/></view><AppButton block label="发布动态" @click="submit"/></view><view v-else class="publish-result"><text>{{publishState==='success'?'动态已发布':'动态未发布'}}</text><text>{{publishState==='success'?'家人现在可以在家族圈看到这条记录。':'请检查内容后重新发布,当前文字仍保留在页面中。'}}</text><AppButton block :label="publishState==='success'?'继续发布':'重新填写'" @click="publishState='form'"/></view></view><AppToast :visible="toastVisible" :message="toastMessage"/></view></template>
<script setup>import{onUnmounted,ref}from'vue';import{onLoad}from'@dcloudio/uni-app';import AppButton from'@/components/AppButton.vue';import AppToast from'@/components/AppToast.vue';import ModulePageBackground from'@/components/ModulePageBackground.vue';import PageHeader from'@/components/PageHeader.vue';const content=ref('');const publishState=ref('form');const toastVisible=ref(false);const toastMessage=ref('');let toastTimer=null;onLoad(query=>{publishState.value=query.state==='success'?'success':query.state==='error'?'error':'form'});const showToast=message=>{toastMessage.value=message;toastVisible.value=true;if(toastTimer)clearTimeout(toastTimer);toastTimer=setTimeout(()=>{toastVisible.value=false;toastTimer=null},1800)};const submit=()=>{if(!content.value.trim()){showToast('请先写下动态内容');return}publishState.value='success'};onUnmounted(()=>{if(toastTimer)clearTimeout(toastTimer)})</script>
<style scoped lang="scss">.publish-page{position:relative;min-height:100vh;overflow:hidden;background:$paper}.publish-page__header,.publish-panel{position:relative;z-index:2}.publish-panel{width:calc(100% - 32rpx);height:min(640px,calc((100vw - 16px)*1.48));margin:18rpx auto 0}.publish-panel__skin{position:absolute;inset:0;width:100%;height:100%}.publish-form,.publish-result{position:absolute;inset:9% 9%}.publish-form>text,.publish-result>text{display:block}.publish-form>text:first-child,.publish-result>text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:36rpx;font-weight:700}.publish-form>text:nth-child(2),.publish-result>text:nth-child(2){margin-top:12rpx;color:$ink-muted;font-size:21rpx;line-height:1.6}.publish-field{position:relative;height:250rpx;margin-top:24rpx}.publish-field image{position:absolute;inset:0;width:100%;height:100%}.publish-field textarea{position:absolute;inset:25rpx;z-index:1;width:auto;height:200rpx;color:$ink;font-size:22rpx;line-height:1.7}.publish-placeholder{color:#a79884}.publish-form>.app-button{margin-top:24rpx}.publish-result{top:30%;text-align:center}.publish-result>.app-button{margin:30rpx auto 0}</style>
<template>
<view
class="publish-page"
:class="{
'publish-state--form': publishState === 'form',
'publish-state--success': publishState === 'success',
'publish-state--error': publishState === 'error',
}"
><ModulePageBackground module="family" /><view class="publish-page__header"
><PageHeader title="发布动态" /></view
><view class="publish-panel"
><image
class="publish-panel__skin"
src="/static/assets/modules/family/transparent/module-content-frame.png"
mode="scaleToFill" /><view
v-if="publishState === 'form'"
class="publish-form"
><text>记录此刻</text
><text>分享通知活动家族故事或一段共同记忆</text
><view class="publish-field"
><image
src="/static/assets/modules/family/transparent/module-field-frame.png"
mode="scaleToFill"
/><textarea
v-model="content"
maxlength="300"
placeholder="写下想对家人说的话"
placeholder-class="publish-placeholder"
/></view
><AppButton block label="发布动态" @click="submit" /></view
><view v-else class="publish-result"
><text>{{
publishState === "success" ? "动态已发布" : "动态未发布"
}}</text
><text>{{
publishState === "success"
? "家人现在可以在家族圈看到这条记录。"
: "请检查内容后重新发布,当前文字仍保留在页面中。"
}}</text
><AppButton
block
:label="publishState === 'success' ? '继续发布' : '重新填写'"
@click="publishState = 'form'" /></view></view
><AppToast :visible="toastVisible" :message="toastMessage"
/></view>
</template>
<script setup>
import { onUnmounted, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppToast from "@/components/AppToast.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
const content = ref("");
const publishState = ref("form");
const toastVisible = ref(false);
const toastMessage = ref("");
let toastTimer = null;
onLoad((query) => {
publishState.value =
query.state === "success"
? "success"
: query.state === "error"
? "error"
: "form";
});
const showToast = (message) => {
toastMessage.value = message;
toastVisible.value = true;
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
toastVisible.value = false;
toastTimer = null;
}, 1800);
};
const submit = () => {
if (!content.value.trim()) {
showToast("请先写下动态内容");
return;
}
publishState.value = "success";
};
onUnmounted(() => {
if (toastTimer) clearTimeout(toastTimer);
});
</script>
<style scoped lang="scss">
.publish-page {
position: relative;
min-height: 100vh;
overflow: hidden;
background: $paper;
}
.publish-page__header,
.publish-panel {
position: relative;
z-index: 2;
}
.publish-panel {
width: calc(100% - 32rpx);
height: min(640px, calc((100vw - 16px) * 1.48));
margin: 18rpx auto 0;
}
.publish-panel__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.publish-form,
.publish-result {
position: absolute;
inset: 9% 9%;
}
.publish-form > text,
.publish-result > text {
display: block;
}
.publish-form > text:first-child,
.publish-result > text:first-child {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: 36rpx;
font-weight: 700;
}
.publish-form > text:nth-child(2),
.publish-result > text:nth-child(2) {
margin-top: 12rpx;
color: $ink-muted;
font-size: 23rpx;
line-height: 1.6;
}
.publish-field {
position: relative;
height: 250rpx;
margin-top: 24rpx;
}
.publish-field image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.publish-field textarea {
position: absolute;
inset: 25rpx;
z-index: 1;
width: auto;
height: 200rpx;
color: $ink;
font-size: 24rpx;
line-height: 1.7;
}
.publish-placeholder {
color: #8e806e;
}
.publish-form > .app-button {
margin-top: 24rpx;
}
.publish-result {
top: 29%;
text-align: center;
}
.publish-result > .app-button {
margin: 30rpx auto 0;
}
</style>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号F-03用途动态详情评论点赞与删除确认 -->
<template><ModulePage page-id="f03" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号F-04用途谱文分类与文章列表 -->
<template><ModulePage page-id="f04" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号F-05用途谱文详情 -->
<template><ModulePage page-id="f05" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号F-06用途新建与编辑谱文 -->
<template><ModulePage page-id="f06" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号F-07用途家族相册列表 -->
<template><ModulePage page-id="f07" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号F-08用途相册详情与照片墙 -->
<template><ModulePage page-id="f08" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号F-09用途图片预览上传与失败状态 -->
<template><ModulePage page-id="f09" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号F-10用途家族视频列表详情与待开放状态 -->
<template><ModulePage page-id="f10" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+352 -126
View File
@@ -2,50 +2,117 @@
<template>
<view class="join-page">
<GenealogyPageBackground />
<view class="join-page__header"><PageHeader :title="sourceContract.headerTitle" /></view>
<view class="join-page__header"
><PageHeader :title="sourceContract.headerTitle"
/></view>
<view class="join-panel" :class="{
'join-state--form': joinState === 'form',
'join-state--success': joinState === 'success',
'join-state--error': joinState === 'error'
}">
<image class="join-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view
class="join-panel"
:class="{
'join-state--form': joinState === 'form',
'join-state--success': joinState === 'success',
'join-state--error': joinState === 'error',
}"
>
<image
class="join-panel__skin"
src="/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png"
mode="scaleToFill"
/>
<view v-if="joinState === 'form'" class="join-form">
<text class="join-form__eyebrow">{{ sourceContract.eyebrow }}</text>
<text class="join-form__title">{{ sourceContract.formTitle }} {{ genealogyName }}</text>
<text class="join-form__title"
>{{ sourceContract.formTitle }} {{ genealogyName }}</text
>
<text class="join-form__copy">{{ sourceContract.formCopy }}</text>
<view class="join-field">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" />
<text>真实姓名</text><input v-model="form.realName" placeholder="请输入真实姓名" placeholder-class="join-placeholder" @input="clearFieldError('realName')" />
<image
src="/static/assets/modules/genealogy/transparent/g-form-field-frame.png"
mode="scaleToFill"
/>
<text>真实姓名</text
><input
v-model="form.realName"
placeholder="请输入真实姓名"
placeholder-class="join-placeholder"
@input="clearFieldError('realName')"
/>
</view>
<text v-if="fieldErrors.realName" class="join-field-error">{{ fieldErrors.realName }}</text>
<text v-if="fieldErrors.realName" class="join-field-error">{{
fieldErrors.realName
}}</text>
<view class="join-field">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" />
<text>与家谱关系</text><input v-model="form.relation" placeholder="例如:汤正华堂侄" placeholder-class="join-placeholder" @input="clearFieldError('relation')" />
<image
src="/static/assets/modules/genealogy/transparent/g-form-field-frame.png"
mode="scaleToFill"
/>
<text>与家谱关系</text
><input
v-model="form.relation"
placeholder="例如:汤正华堂侄"
placeholder-class="join-placeholder"
@input="clearFieldError('relation')"
/>
</view>
<text v-if="fieldErrors.relation" class="join-field-error">{{ fieldErrors.relation }}</text>
<text class="relation-help">请以家谱中一位已知长辈为参照例如汤正华堂侄</text>
<text v-if="fieldErrors.relation" class="join-field-error">{{
fieldErrors.relation
}}</text>
<text class="relation-help"
>请以家谱中一位已知长辈为参照例如汤正华堂侄</text
>
<view class="join-field join-field--message">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" />
<text>{{ sourceContract.thirdFieldLabel }}</text><textarea v-model="form.message" maxlength="80" :placeholder="sourceContract.thirdFieldPlaceholder" placeholder-class="join-placeholder" />
<image
src="/static/assets/modules/genealogy/transparent/g-form-field-frame.png"
mode="scaleToFill"
/>
<text>{{ sourceContract.thirdFieldLabel }}</text
><textarea
v-model="form.message"
maxlength="80"
:placeholder="sourceContract.thirdFieldPlaceholder"
placeholder-class="join-placeholder"
/>
</view>
<text class="join-form__note">{{ sourceContract.note }}</text>
<view class="join-action" @click="submitJoin">
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" />
<text>{{ isSubmitting ? sourceContract.submittingLabel : sourceContract.submitLabel }}</text>
<image
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="aspectFit"
/>
<text>{{
isSubmitting
? sourceContract.submittingLabel
: sourceContract.submitLabel
}}</text>
</view>
</view>
<view v-else class="join-result">
<text class="join-result__eyebrow">{{ joinState === 'success' ? sourceContract.successEyebrow : sourceContract.errorEyebrow }}</text>
<text class="join-result__title">{{ joinState === 'success' ? sourceContract.successTitle : sourceContract.errorTitle }}</text>
<text class="join-result__eyebrow">{{
joinState === "success"
? sourceContract.successEyebrow
: sourceContract.errorEyebrow
}}</text>
<text class="join-result__title">{{
joinState === "success"
? sourceContract.successTitle
: sourceContract.errorTitle
}}</text>
<text class="join-result__copy">{{ resultCopy }}</text>
<view class="join-action" @click="joinState === 'success' ? completeFlow() : retryForm()">
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" />
<text>{{ joinState === 'success' ? sourceContract.nextLabel : '重新填写' }}</text>
<view
class="join-action"
@click="joinState === 'success' ? completeFlow() : retryForm()"
>
<image
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="aspectFit"
/>
<text>{{
joinState === "success" ? sourceContract.nextLabel : "重新填写"
}}</text>
</view>
</view>
</view>
@@ -53,121 +120,280 @@
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import GenealogyPageBackground from '@/components/GenealogyPageBackground.vue'
import PageHeader from '@/components/PageHeader.vue'
import { computed, reactive, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
const genealogyId = ref('')
const source = ref('search')
const genealogyName = ref('这部家谱')
const genealogyId = ref("");
const source = ref("search");
const genealogyName = ref("这部家谱");
const genealogyPreview = {
'1001': '汤氏家谱',
'1002': '汝南汤氏家谱'
}
const joinState = ref('form')
const isSubmitting = ref(false)
const errorMessage = ref('')
const form = reactive({ realName: '', relation: '', message: '' })
const fieldErrors = reactive({ realName: '', relation: '' })
const sourceContract = computed(() => source.value === 'invite' ? {
headerTitle: '确认关系并加入',
eyebrow: '邀请码直接加入',
formTitle: '确认加入',
formCopy: '请填写真实身份和亲属关系;提交后直接加入,无需等待管理员审核。',
thirdFieldLabel: '补充信息',
thirdFieldPlaceholder: '选填:补充祖居地、长辈姓名等信息',
note: '邀请码来源提交后直接加入,无需等待审核。',
submitLabel: '确认加入',
submittingLabel: '正在加入…',
successEyebrow: '已加入家谱',
successTitle: '关系信息已提交',
errorEyebrow: '加入未完成',
errorTitle: '暂时无法加入家谱',
nextLabel: '返回我的家谱',
successCopy: `已直接加入“${genealogyName.value}”,无需等待审核;返回后将刷新并选中这部家谱。`
} : {
headerTitle: '申请加入家谱',
eyebrow: '公开家谱入谱申请',
formTitle: '申请加入',
formCopy: '请填写真实身份和亲属关系,管理员审核后会通过消息告知结果。',
thirdFieldLabel: '申请说明',
thirdFieldPlaceholder: '补充祖居地、长辈姓名等核验信息',
note: '提交后可在“我的申请”中查看审核进度。',
submitLabel: '提交申请',
submittingLabel: '正在提交…',
successEyebrow: '申请已送达',
successTitle: '等待管理员核实亲属关系',
errorEyebrow: '申请未提交',
errorTitle: '暂时无法提交申请',
nextLabel: '查看我的申请',
successCopy: `${genealogyName.value}”的管理员会在核实后给出结果,请留意消息中心。`
})
const resultCopy = computed(() => joinState.value === 'success'
? sourceContract.value.successCopy
: errorMessage.value || '请检查网络后重新填写;未成功提交的内容不会进入审核列表。')
1001: "汤氏家谱",
1002: "汝南汤氏家谱",
};
const joinState = ref("form");
const isSubmitting = ref(false);
const errorMessage = ref("");
const form = reactive({ realName: "", relation: "", message: "" });
const fieldErrors = reactive({ realName: "", relation: "" });
const sourceContract = computed(() =>
source.value === "invite"
? {
headerTitle: "确认关系并加入",
eyebrow: "邀请码直接加入",
formTitle: "确认加入",
formCopy:
"请填写真实身份和亲属关系;提交后直接加入,无需等待管理员审核。",
thirdFieldLabel: "补充信息",
thirdFieldPlaceholder: "选填:补充祖居地、长辈姓名等信息",
note: "邀请码来源提交后直接加入,无需等待审核。",
submitLabel: "确认加入",
submittingLabel: "正在加入…",
successEyebrow: "已加入家谱",
successTitle: "关系信息已提交",
errorEyebrow: "加入未完成",
errorTitle: "暂时无法加入家谱",
nextLabel: "返回我的家谱",
successCopy: `已直接加入“${genealogyName.value}”,无需等待审核;返回后将刷新并选中这部家谱。`,
}
: {
headerTitle: "申请加入家谱",
eyebrow: "公开家谱入谱申请",
formTitle: "申请加入",
formCopy: "请填写真实身份和亲属关系,管理员审核后会通过消息告知结果。",
thirdFieldLabel: "申请说明",
thirdFieldPlaceholder: "补充祖居地、长辈姓名等核验信息",
note: "提交后可在“我的申请”中查看审核进度。",
submitLabel: "提交申请",
submittingLabel: "正在提交…",
successEyebrow: "申请已送达",
successTitle: "等待管理员核实亲属关系",
errorEyebrow: "申请未提交",
errorTitle: "暂时无法提交申请",
nextLabel: "查看我的申请",
successCopy: `${genealogyName.value}”的管理员会在核实后给出结果,请留意消息中心。`,
},
);
const resultCopy = computed(() =>
joinState.value === "success"
? sourceContract.value.successCopy
: errorMessage.value ||
"请检查网络后重新填写;未成功提交的内容不会进入审核列表。",
);
onLoad((query) => {
genealogyId.value = query.genealogyId || ''
source.value = query.source === 'invite' ? 'invite' : 'search'
if (query.state === 'success') {
joinState.value = 'success'
return
genealogyId.value = query.genealogyId || "";
source.value = query.source === "invite" ? "invite" : "search";
if (query.state === "success") {
joinState.value = "success";
return;
}
if (!genealogyId.value) {
errorMessage.value = '没有找到要申请加入的家谱,请先返回公开家谱检索。'
joinState.value = 'error'
return
errorMessage.value = "没有找到要申请加入的家谱,请先返回公开家谱检索。";
joinState.value = "error";
return;
}
genealogyName.value = genealogyPreview[genealogyId.value] || '这部家谱'
})
genealogyName.value = genealogyPreview[genealogyId.value] || "这部家谱";
});
const submitJoin = () => {
if (isSubmitting.value) return
fieldErrors.realName = form.realName.trim() ? '' : '请填写真实姓名'
fieldErrors.relation = form.relation.trim() ? '' : '请填写与家谱的关系'
if (fieldErrors.realName || fieldErrors.relation) return
isSubmitting.value = true
if (isSubmitting.value) return;
fieldErrors.realName = form.realName.trim() ? "" : "请填写真实姓名";
fieldErrors.relation = form.relation.trim() ? "" : "请填写与家谱的关系";
if (fieldErrors.realName || fieldErrors.relation) return;
isSubmitting.value = true;
setTimeout(() => {
joinState.value = form.realName.trim() === '失败' ? 'error' : 'success'
if (joinState.value === 'error') errorMessage.value = source.value === 'invite' ? '邀请码加入暂未完成,请稍后重试。' : '申请暂未提交,请稍后重试。'
isSubmitting.value = false
}, 280)
}
const clearFieldError = (field) => { fieldErrors[field] = '' }
const retryForm = () => { joinState.value = 'form'; errorMessage.value = '' }
const toMyApplications = () => uni.redirectTo({ url: '/pages/genealogy/g09-my-applications' })
const toMyGenealogies = () => uni.reLaunch({ url: `/pages/genealogy/g01-my-genealogies?genealogyId=${genealogyId.value}` })
const completeFlow = () => source.value === 'invite' ? toMyGenealogies() : toMyApplications()
joinState.value = form.realName.trim() === "失败" ? "error" : "success";
if (joinState.value === "error")
errorMessage.value =
source.value === "invite"
? "邀请码加入暂未完成,请稍后重试。"
: "申请暂未提交,请稍后重试。";
isSubmitting.value = false;
}, 280);
};
const clearFieldError = (field) => {
fieldErrors[field] = "";
};
const retryForm = () => {
joinState.value = "form";
errorMessage.value = "";
};
const toMyApplications = () =>
uni.redirectTo({ url: "/pages/genealogy/g09-my-applications" });
const toMyGenealogies = () =>
uni.reLaunch({
url: `/pages/genealogy/g01-my-genealogies?genealogyId=${genealogyId.value}`,
});
const completeFlow = () =>
source.value === "invite" ? toMyGenealogies() : toMyApplications();
</script>
<style scoped lang="scss">
.join-page { position: relative; min-height: 100vh; overflow-x: hidden; background: $paper; }
.join-page__header { position: relative; z-index: 3; }
.join-panel { position: relative; z-index: 2; width: calc(100% - 32rpx); height: min(590px, calc((100vw - 16px) * 1.337)); margin: 22rpx auto 0; }
.join-panel__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.join-form { position: absolute; inset: 8.5% 8%; }
.join-form__eyebrow, .join-result__eyebrow { display: block; color: $brand-red; font-size: 23rpx; letter-spacing: 3rpx; }
.join-form__title, .join-result__title { display: block; margin-top: 12rpx; color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 36rpx; font-weight: 700; }
.join-form__copy, .join-result__copy { display: block; margin-top: 12rpx; color: $ink-muted; font-size: 22rpx; line-height: 1.6; }
.join-field { position: relative; height: 92rpx; margin-top: 18rpx; }
.join-field > image { position: absolute; inset: 0; width: 100%; height: 100%; }
.join-field > text { position: absolute; top: 31rpx; left: 24rpx; z-index: 1; color: $ink; font-size: 23rpx; font-weight: 700; }
.join-field input, .join-field textarea { position: absolute; top: 0; right: 20rpx; bottom: 0; left: 170rpx; z-index: 1; height: 92rpx; color: $ink; font-size: 23rpx; line-height: 92rpx; }
.join-field textarea { box-sizing: border-box; padding-top: 26rpx; line-height: 1.5; }
.join-placeholder { color: #a79884; }
.join-field-error { display: block; margin-top: 3rpx; color: $brand-red; font-size: 20rpx; text-align: right; }
.relation-help { display: block; margin-top: 5rpx; color: #8e7b67; font-size: 19rpx; line-height: 29rpx; }
.join-form__note { display: block; margin-top: 18rpx; color: $ink-muted; font-size: 21rpx; text-align: center; }
.join-action { position: relative; width: 100%; height: 82rpx; margin-top: 20rpx; }
.join-action image { position: absolute; inset: 0; width: 100%; height: 100%; }
.join-action text { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; color: #fff9ed; font-size: 27rpx; font-weight: 700; letter-spacing: 3rpx; }
.join-result { position: absolute; top: 28%; right: 12%; left: 12%; text-align: center; }
.join-result__eyebrow { text-align: center; }
.join-result__copy { margin-top: 22rpx; }
.join-result .join-action { width: 420rpx; max-width: 100%; margin: 34rpx auto 0; }
.join-page {
position: relative;
min-height: 100vh;
overflow-x: hidden;
background: $paper;
}
.join-page__header {
position: relative;
z-index: 3;
}
.join-panel {
position: relative;
z-index: 2;
width: calc(100% - 32rpx);
height: min(590px, calc((100vw - 16px) * 1.337));
margin: 22rpx auto 0;
}
.join-panel__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.join-form {
position: absolute;
inset: 8.5% 8%;
}
.join-form__eyebrow,
.join-result__eyebrow {
display: block;
color: $brand-red;
font-size: 23rpx;
letter-spacing: 3rpx;
}
.join-form__title,
.join-result__title {
display: block;
margin-top: 12rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 36rpx;
font-weight: 700;
}
.join-form__copy,
.join-result__copy {
display: block;
margin-top: 12rpx;
color: $ink-muted;
font-size: 22rpx;
line-height: 1.6;
}
.join-field {
position: relative;
height: 92rpx;
margin-top: 18rpx;
}
.join-field > image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.join-field > text {
position: absolute;
top: 31rpx;
left: 24rpx;
z-index: 1;
color: $ink;
font-size: 23rpx;
font-weight: 700;
}
.join-field input,
.join-field textarea {
position: absolute;
top: 0;
right: 20rpx;
bottom: 0;
left: 170rpx;
z-index: 1;
height: 92rpx;
color: $ink;
font-size: 23rpx;
line-height: 92rpx;
}
.join-field textarea {
box-sizing: border-box;
padding-top: 26rpx;
line-height: 1.5;
}
.join-placeholder {
color: #a79884;
}
.join-field-error {
display: block;
margin-top: 3rpx;
color: $brand-red;
font-size: 24rpx;
line-height: 34rpx;
text-align: right;
}
.relation-help {
display: block;
margin-top: 5rpx;
color: #8e7b67;
font-size: 19rpx;
line-height: 29rpx;
}
.join-form__note {
display: block;
margin-top: 18rpx;
color: $ink-muted;
font-size: 21rpx;
text-align: center;
}
.join-action {
position: relative;
width: 100%;
height: 82rpx;
margin-top: 20rpx;
}
.join-action image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.join-action text {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #fff9ed;
font-size: 27rpx;
font-weight: 700;
letter-spacing: 3rpx;
}
.join-result {
position: absolute;
top: 28%;
right: 12%;
left: 12%;
text-align: center;
}
.join-result__eyebrow {
text-align: center;
}
.join-result__copy {
margin-top: 22rpx;
}
.join-result .join-action {
width: 420rpx;
max-width: 100%;
margin: 34rpx auto 0;
}
@media (min-width: 400px) {
.join-panel { width: calc(100% - 48rpx); }
.join-form { right: 9%; left: 9%; }
.join-panel {
width: calc(100% - 48rpx);
}
.join-form {
right: 9%;
left: 9%;
}
}
</style>
+328 -88
View File
@@ -2,47 +2,94 @@
<template>
<view class="application-page">
<GenealogyPageBackground />
<view class="application-page__header"><PageHeader title="我的申请" /></view>
<view class="application-page__header"
><PageHeader title="我的申请"
/></view>
<view class="application-content" :class="{
'application-state--list': applicationState === 'list',
'application-state--empty': applicationState === 'empty',
'application-state--error': applicationState === 'error'
}">
<view
class="application-content"
:class="{
'application-state--list': applicationState === 'list',
'application-state--empty': applicationState === 'empty',
'application-state--error': applicationState === 'error',
}"
>
<template v-if="applicationState === 'list'">
<view class="application-intro">
<text>入谱申请进度</text><text>审核结果会同步到消息中心</text>
</view>
<view v-for="item in applications" :key="item.id" class="application-card">
<image class="application-card__skin" src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill" />
<view
v-for="item in applications"
:key="item.id"
class="application-card"
>
<image
class="application-card__skin"
src="/static/assets/modules/genealogy/transparent/list-slip-frame.png"
mode="scaleToFill"
/>
<view class="application-card__body">
<text class="application-card__name">{{ item.genealogyName }}</text>
<text class="application-card__time">{{ item.appliedAt }}</text>
<text class="application-card__relation">{{ item.relation }}</text>
<text class="application-card__status" :class="`application-card__status--${item.status.toLowerCase()}`">{{ statusLabel(item.status) }}</text>
<text class="application-card__hint">{{ statusHint(item.status) }}</text>
<view v-if="actionFor(item)" class="application-card__action" @click="handleApplicationAction(item)">{{ actionFor(item) }}</view>
<text
class="application-card__status"
:class="`application-card__status--${item.status.toLowerCase()}`"
>{{ statusLabel(item.status) }}</text
>
<text class="application-card__hint">{{
statusHint(item.status)
}}</text>
<view
v-if="actionFor(item)"
class="application-card__action"
@click="handleApplicationAction(item)"
>{{ actionFor(item) }}</view
>
</view>
</view>
</template>
<AppLoading
v-else-if="applicationState === 'loading'"
text="正在整理申请记录"
description="请稍候,正在同步审核状态。"
/>
<view v-else class="application-state-card">
<image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill" />
<image
src="/static/assets/modules/genealogy/transparent/list-slip-frame.png"
mode="scaleToFill"
/>
<view class="application-state-card__copy">
<text>{{ stateTitle }}</text>
<text>{{ stateCopy }}</text>
</view>
</view>
<view v-if="applicationState !== 'list'" class="application-page__action" @click="applicationState === 'error' ? loadApplications({}) : toSearch()">
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" />
<text>{{ applicationState === 'error' ? '重新查看' : '查找公开家谱' }}</text>
<view
v-if="applicationState !== 'list' && applicationState !== 'loading'"
class="application-page__action"
@click="
applicationState === 'error' ? loadApplications({}) : toSearch()
"
>
<image
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="aspectFit"
/>
<text>{{
applicationState === "error" ? "重新查看" : "查找公开家谱"
}}</text>
</view>
</view>
<AppDialog
:visible="!!withdrawTarget"
title="撤回加入申请"
:message="withdrawTarget ? `确认撤回对“${withdrawTarget.genealogyName}”的申请?撤回后如需加入,可重新提交。` : ''"
:message="
withdrawTarget
? `确认撤回对“${withdrawTarget.genealogyName}”的申请?撤回后如需加入,可重新提交。`
: ''
"
confirm-text="确认撤回"
cancel-text="暂不撤回"
show-cancel
@@ -54,89 +101,282 @@
</template>
<script setup>
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import AppDialog from '@/components/AppDialog.vue'
import GenealogyPageBackground from '@/components/GenealogyPageBackground.vue'
import PageHeader from '@/components/PageHeader.vue'
import { computed, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import AppDialog from "@/components/AppDialog.vue";
import AppLoading from "@/components/AppLoading.vue";
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
const applications = ref([])
const applicationState = ref('loading')
const errorMessage = ref('')
const withdrawTarget = ref(null)
const applications = ref([]);
const applicationState = ref("loading");
const errorMessage = ref("");
const withdrawTarget = ref(null);
const applicationSamples = [
{ id: 1, genealogyId: 1001, genealogyName: '汤氏家谱', relation: '自述为汤正华堂侄', appliedAt: '今天 10:24', status: 'PENDING' },
{ id: 2, genealogyId: 1002, genealogyName: '汝南汤氏家谱', relation: '祖居河南汝南', appliedAt: '昨天 18:02', status: 'APPROVED' },
{ id: 3, genealogyId: 1003, genealogyName: '清河汤氏家谱', relation: '补充材料不足', appliedAt: '7月12日 09:18', status: 'REJECTED' }
]
{
id: 1,
genealogyId: 1001,
genealogyName: "汤氏家谱",
relation: "自述为汤正华堂侄",
appliedAt: "今天 10:24",
status: "PENDING",
},
{
id: 2,
genealogyId: 1002,
genealogyName: "汝南汤氏家谱",
relation: "祖居河南汝南",
appliedAt: "昨天 18:02",
status: "APPROVED",
},
{
id: 3,
genealogyId: 1003,
genealogyName: "清河汤氏家谱",
relation: "补充材料不足",
appliedAt: "7月12日 09:18",
status: "REJECTED",
},
];
const statusLabel = (status) => ({
'PENDING': '审核中',
'APPROVED': '已通过',
'REJECTED': '未通过',
'WITHDRAWN': '已撤回'
}[status] || '状态未知')
const statusLabel = (status) =>
({
PENDING: "审核中",
APPROVED: "已通过",
REJECTED: "未通过",
WITHDRAWN: "已撤回",
})[status] || "状态未知";
const statusHint = (status) => ({
PENDING: '管理员尚未处理,可在审核前撤回',
APPROVED: '申请已通过,可进入这部家谱',
REJECTED: '请修改关系说明后重新提交',
WITHDRAWN: '申请已撤回,不再进入管理员审核'
}[status] || '')
const statusHint = (status) =>
({
PENDING: "管理员尚未处理,可在审核前撤回",
APPROVED: "申请已通过,可进入这部家谱",
REJECTED: "请修改关系说明后重新提交",
WITHDRAWN: "申请已撤回,不再进入管理员审核",
})[status] || "";
const actionFor = (item) => ({ PENDING: '撤回申请', APPROVED: '进入家谱', REJECTED: '修改后重新提交' }[item.status] || '')
const stateTitle = computed(() => applicationState.value === 'empty' ? '还没有入谱申请' : applicationState.value === 'loading' ? '正在整理申请记录' : '申请记录暂时无法读取')
const stateCopy = computed(() => applicationState.value === 'empty' ? '从家谱搜索提交的申请会显示在这里;邀请码直接加入不进入本页。' : applicationState.value === 'loading' ? '请稍候,正在同步审核状态。' : errorMessage.value || '请检查网络后重新查看。')
const actionFor = (item) =>
({ PENDING: "撤回申请", APPROVED: "进入家谱", REJECTED: "修改后重新提交" })[
item.status
] || "";
const stateTitle = computed(() =>
applicationState.value === "empty"
? "还没有入谱申请"
: applicationState.value === "loading"
? "正在整理申请记录"
: "申请记录暂时无法读取",
);
const stateCopy = computed(() =>
applicationState.value === "empty"
? "从家谱搜索提交的申请会显示在这里;邀请码直接加入不进入本页。"
: applicationState.value === "loading"
? "请稍候,正在同步审核状态。"
: errorMessage.value || "请检查网络后重新查看。",
);
const loadApplications = (query = {}) => {
applicationState.value = 'loading'
errorMessage.value = ''
if (query.state === 'empty') { applicationState.value = 'empty'; return }
if (query.state === 'error') { applicationState.value = 'error'; return }
if (query.state === 'loading') { applicationState.value = 'loading'; return }
applications.value = applicationSamples.map((item) => ({ ...item }))
applicationState.value = 'list'
}
applicationState.value = "loading";
errorMessage.value = "";
if (query.state === "empty") {
applicationState.value = "empty";
return;
}
if (query.state === "error") {
applicationState.value = "error";
return;
}
if (query.state === "loading") {
applicationState.value = "loading";
return;
}
applications.value = applicationSamples.map((item) => ({ ...item }));
applicationState.value = "list";
};
onLoad(loadApplications)
onLoad(loadApplications);
const handleApplicationAction = (item) => {
if (item.status === 'APPROVED') return uni.navigateTo({ url: `/pages/genealogy/g05-genealogy-overview?genealogyId=${item.genealogyId}` })
if (item.status === 'REJECTED') return uni.navigateTo({ url: `/pages/genealogy/g08-join-application?source=search&previous=rejected&genealogyId=${item.genealogyId}` })
if (item.status === 'PENDING') withdrawTarget.value = item
}
const cancelWithdraw = () => { withdrawTarget.value = null }
if (item.status === "APPROVED")
return uni.navigateTo({
url: `/pages/genealogy/g05-genealogy-overview?genealogyId=${item.genealogyId}`,
});
if (item.status === "REJECTED")
return uni.navigateTo({
url: `/pages/genealogy/g08-join-application?source=search&previous=rejected&genealogyId=${item.genealogyId}`,
});
if (item.status === "PENDING") withdrawTarget.value = item;
};
const cancelWithdraw = () => {
withdrawTarget.value = null;
};
const confirmWithdraw = () => {
const target = applications.value.find((item) => item.id === withdrawTarget.value?.id)
if (target) target.status = 'WITHDRAWN'
cancelWithdraw()
}
const toSearch = () => uni.navigateTo({ url: '/pages/genealogy/g06-search-genealogies' })
const target = applications.value.find(
(item) => item.id === withdrawTarget.value?.id,
);
if (target) target.status = "WITHDRAWN";
cancelWithdraw();
};
const toSearch = () =>
uni.navigateTo({ url: "/pages/genealogy/g06-search-genealogies" });
</script>
<style scoped lang="scss">
.application-page { position: relative; min-height: 100vh; overflow-x: hidden; background: $paper; }
.application-page__header { position: relative; z-index: 3; }
.application-content { position: relative; z-index: 2; padding: 28rpx 24rpx 60rpx; }
.application-intro { display: flex; align-items: baseline; justify-content: space-between; margin: 0 8rpx 20rpx; }
.application-intro text:first-child { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 31rpx; font-weight: 700; }
.application-intro text:last-child { color: $ink-muted; font-size: 21rpx; }
.application-card { position: relative; width: 100%; height: calc((100vw - 24px) * .34286); min-height: 228rpx; max-height: 282rpx; margin-bottom: 18rpx; }
.application-card__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.application-card__body { position: absolute; inset: 18% 7% 14% 8.5%; }
.application-card__name { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 30rpx; font-weight: 700; }
.application-card__time { position: absolute; top: 3rpx; right: 0; color: #998873; font-size: 20rpx; }
.application-card__relation { display: block; max-width: 70%; margin-top: 14rpx; color: $ink-muted; font-size: 22rpx; }
.application-card__status { position: absolute; right: 3%; bottom: 38%; color: $brand-red; font-size: 23rpx; font-weight: 700; }
.application-card__status--approved { color: #537368; }
.application-card__status--rejected { color: #7e6f62; }
.application-card__hint { position: absolute; bottom: 5%; left: 0; max-width: 62%; color: #8d7c67; font-size: 20rpx; }
.application-card__action { position: absolute; right: 2%; bottom: 2%; z-index: 2; display: flex; min-height: 54rpx; align-items: center; color: $brand-red; font-size: 21rpx; font-weight: 700; }
.application-state-card { position: relative; width: 100%; height: calc((100vw - 24px) * .34286); min-height: 228rpx; margin-top: 80rpx; }
.application-state-card > image { position: absolute; inset: 0; width: 100%; height: 100%; }
.application-state-card__copy { position: absolute; inset: 22% 12%; display: flex; flex-direction: column; justify-content: center; text-align: center; }
.application-state-card__copy text:first-child { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 31rpx; font-weight: 700; }
.application-state-card__copy text:last-child { margin-top: 16rpx; color: $ink-muted; font-size: 22rpx; line-height: 1.6; }
.application-page__action { position: relative; width: 420rpx; height: 82rpx; margin: 34rpx auto 0; }
.application-page__action image { position: absolute; inset: 0; width: 100%; height: 100%; }
.application-page__action text { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; color: #fff9ed; font-size: 26rpx; font-weight: 700; }
.application-page {
position: relative;
min-height: 100vh;
overflow-x: hidden;
background: $paper;
}
.application-page__header {
position: relative;
z-index: 3;
}
.application-content {
position: relative;
z-index: 2;
padding: 28rpx 24rpx 60rpx;
}
.application-intro {
display: flex;
align-items: baseline;
justify-content: space-between;
margin: 0 8rpx 20rpx;
}
.application-intro text:first-child {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 31rpx;
font-weight: 700;
}
.application-intro text:last-child {
color: $ink-muted;
font-size: 24rpx;
}
.application-card {
position: relative;
width: 100%;
height: calc((100vw - 24px) * 0.34286);
min-height: 228rpx;
max-height: 282rpx;
margin-bottom: 18rpx;
}
.application-card__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.application-card__body {
position: absolute;
inset: 18% 7% 14% 8.5%;
}
.application-card__name {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 30rpx;
font-weight: 700;
}
.application-card__time {
position: absolute;
top: 3rpx;
right: 0;
color: #998873;
font-size: 23rpx;
}
.application-card__relation {
display: block;
max-width: 70%;
margin-top: 14rpx;
color: $ink-muted;
font-size: 24rpx;
}
.application-card__status {
position: absolute;
right: 3%;
bottom: 38%;
color: $brand-red;
font-size: 25rpx;
font-weight: 700;
}
.application-card__status--approved {
color: #537368;
}
.application-card__status--rejected {
color: #7e6f62;
}
.application-card__hint {
position: absolute;
bottom: 5%;
left: 0;
max-width: 62%;
color: #766653;
font-size: 23rpx;
}
.application-card__action {
position: absolute;
right: 2%;
bottom: 2%;
z-index: 2;
display: flex;
min-height: 54rpx;
align-items: center;
color: $brand-red;
font-size: 23rpx;
font-weight: 700;
}
.application-state-card {
position: relative;
width: 100%;
height: calc((100vw - 24px) * 0.34286);
min-height: 228rpx;
margin-top: 80rpx;
}
.application-state-card > image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.application-state-card__copy {
position: absolute;
inset: 22% 12%;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.application-state-card__copy text:first-child {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 31rpx;
font-weight: 700;
}
.application-state-card__copy text:last-child {
margin-top: 16rpx;
color: $ink-muted;
font-size: 24rpx;
line-height: 1.6;
}
.application-page__action {
position: relative;
width: 420rpx;
height: 82rpx;
margin: 34rpx auto 0;
}
.application-page__action image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.application-page__action text {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #fff9ed;
font-size: 26rpx;
font-weight: 700;
}
</style>
+288 -3
View File
@@ -1,4 +1,289 @@
<!-- 页面编号N-01用途消息中心已读操作空态与失败状态 -->
<template><view class="notice-page" :class="{'notice-state--list':noticeState==='list','notice-state--empty':noticeState==='empty','notice-state--error':noticeState==='error'}"><ModulePageBackground module="notification"/><view class="notice-page__header"><PageHeader title="消息中心" action="全部已读" @action="markAllRead"/></view><view class="notice-content"><template v-if="noticeState==='list'"><view v-for="item in notices" :key="item.id" class="notice-card" @click="readNotice(item)"><image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill"/><view><text>{{item.unread?'未读提醒':'已读'}} · {{item.time}}</text><text>{{item.title}}</text><text>{{item.content}}</text></view></view></template><view v-else class="notice-state-card"><image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill"/><view><text>{{noticeState==='empty'?'暂时没有新消息':'消息中心暂不可用'}}</text><text>{{noticeState==='empty'?'家谱申请、审核结果和家族提醒会留在这里。':'请稍后重新进入,已读状态不会受到影响。'}}</text></view></view><AppButton type="secondary" :label="noticeState==='error'?'重新查看':'前往入谱审核'" @click="noticeState==='error'?noticeState='list':toReview()"/></view><AppToast :visible="toastVisible" :message="toastMessage"/></view></template>
<script setup>import{onUnmounted,ref}from'vue';import{onLoad}from'@dcloudio/uni-app';import AppButton from'@/components/AppButton.vue';import AppToast from'@/components/AppToast.vue';import ModulePageBackground from'@/components/ModulePageBackground.vue';import PageHeader from'@/components/PageHeader.vue';import{genealogyContext}from'@/utils/genealogy-context.js';const genealogyId=ref('');const noticeState=ref('loading');const toastVisible=ref(false);const toastMessage=ref('');let toastTimer=null;const notices=ref([{id:1,title:'申请待审核',content:'汤志成申请加入汤氏家谱,请核实亲属关系。',time:'今天 10:28',unread:true},{id:2,title:'入谱申请已通过',content:'你申请加入汝南汤氏家谱的请求已通过。',time:'昨天 18:10',unread:false}]);onLoad(query=>{genealogyId.value=query.genealogyId||genealogyContext.getCurrentGenealogyId()||'';noticeState.value=query.state==='empty'?'empty':query.state==='error'?'error':'list'});const readNotice=item=>{item.unread=false};const showToast=message=>{toastMessage.value=message;toastVisible.value=true;if(toastTimer)clearTimeout(toastTimer);toastTimer=setTimeout(()=>{toastVisible.value=false;toastTimer=null},1800)};const markAllRead=()=>{notices.value.forEach(x=>x.unread=false);showToast('已全部标记为已读')};const toReview=()=>uni.navigateTo({url:`/pages/genealogy/g10-application-review?genealogyId=${genealogyId.value}`});onUnmounted(()=>{if(toastTimer)clearTimeout(toastTimer)})</script>
<style scoped lang="scss">.notice-page{position:relative;min-height:100vh;overflow:hidden;background:$paper}.notice-page__header,.notice-content{position:relative;z-index:2}.notice-content{padding:24rpx}.notice-card,.notice-state-card{position:relative;width:100%;height:calc((100vw - 24px)*.34286);min-height:220rpx;margin-bottom:16rpx}.notice-card>image,.notice-state-card>image{position:absolute;inset:0;width:100%;height:100%}.notice-card>view{position:absolute;inset:17% 9%;z-index:1}.notice-card text{display:block}.notice-card text:first-child{color:$brand-red;font-size:18rpx;letter-spacing:2rpx}.notice-card text:nth-child(2){margin-top:6rpx;color:$ink;font-family:STKaiti,KaiTi,serif;font-size:28rpx;font-weight:700}.notice-card text:last-child{margin-top:7rpx;color:#62584c;font-size:22rpx;font-weight:500}.notice-state-card{margin-top:70rpx}.notice-state-card>view{position:absolute;inset:25% 12%;z-index:1;text-align:center}.notice-state-card text{display:block}.notice-state-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:30rpx;font-weight:700}.notice-state-card text:last-child{margin-top:13rpx;color:#62584c;font-size:22rpx}.notice-content>.app-button{margin:20rpx auto 0}</style>
<template>
<view
class="notice-page"
:class="{
'notice-state--loading': noticeState === 'loading',
'notice-state--list': noticeState === 'list',
'notice-state--empty': noticeState === 'empty',
'notice-state--error': noticeState === 'error',
}"
>
<ModulePageBackground module="notification" />
<view class="notice-page__header">
<PageHeader
title="消息中心"
:action="noticeState === 'list' ? '全部已读' : ''"
@action="markAllRead"
/>
</view>
<view class="notice-content">
<AppLoading
v-if="noticeState === 'loading'"
text="正在整理消息"
description="请稍候,正在同步家谱申请与家族提醒。"
/>
<template v-else-if="noticeState === 'list'">
<view class="notice-list">
<view
v-for="item in notices"
:key="item.id"
class="notice-card"
role="button"
:aria-label="`${item.unread ? '未读' : '已读'}消息${item.title}`"
@click="readNotice(item)"
>
<image
class="notice-card__skin"
src="/static/assets/modules/notification/transparent/n01-notice-card.png"
mode="scaleToFill"
/>
<view class="notice-card__copy">
<text
class="notice-card__status"
:class="{ 'is-unread': item.unread }"
>{{ item.unread ? "未读提醒" : "已读" }} · {{ item.time }}</text
>
<text class="notice-card__title">{{ item.title }}</text>
<text class="notice-card__summary">{{ item.content }}</text>
</view>
</view>
</view>
<AppButton
class="notice-review-action"
block
type="secondary"
label="前往入谱审核"
@click="toReview"
/>
</template>
<view v-else class="notice-state-card">
<image
class="notice-state-card__skin"
src="/static/assets/modules/notification/transparent/n01-notice-card.png"
mode="scaleToFill"
/>
<view class="notice-state-card__copy">
<text class="notice-state-card__title">{{
noticeState === "empty" ? "暂时没有新消息" : "消息中心暂不可用"
}}</text>
<text class="notice-state-card__description">{{
noticeState === "empty"
? "家谱申请、审核结果和家族提醒会留在这里。"
: "请稍后重新进入,已读状态不会受到影响。"
}}</text>
</view>
<AppButton
block
:type="noticeState === 'error' ? 'secondary' : 'primary'"
:label="noticeState === 'error' ? '重新查看' : '前往入谱审核'"
@click="noticeState === 'error' ? restoreList() : toReview()"
/>
</view>
</view>
<AppToast :visible="toastVisible" :message="toastMessage" />
</view>
</template>
<script setup>
import { onUnmounted, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppLoading from "@/components/AppLoading.vue";
import AppToast from "@/components/AppToast.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { genealogyContext } from "@/utils/genealogy-context.js";
const genealogyId = ref("");
const noticeState = ref("loading");
const toastVisible = ref(false);
const toastMessage = ref("");
let toastTimer = null;
const notices = ref([
{
id: 1,
title: "申请待审核",
content: "汤志成申请加入汤氏家谱,请核实亲属关系。",
time: "今天 10:28",
unread: true,
},
{
id: 2,
title: "入谱申请已通过",
content: "你申请加入汝南汤氏家谱的请求已通过。",
time: "昨天 18:10",
unread: false,
},
]);
onLoad((query) => {
genealogyId.value =
query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
noticeState.value =
query.state === "loading"
? "loading"
: query.state === "empty"
? "empty"
: query.state === "error"
? "error"
: "list";
});
const readNotice = (item) => {
item.unread = false;
};
const restoreList = () => {
noticeState.value = "list";
};
const showToast = (message) => {
toastMessage.value = message;
toastVisible.value = true;
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
toastVisible.value = false;
toastTimer = null;
}, 1800);
};
const markAllRead = () => {
notices.value.forEach((item) => {
item.unread = false;
});
showToast("已全部标记为已读");
};
const toReview = () =>
uni.navigateTo({
url: `/pages/genealogy/g10-application-review?genealogyId=${genealogyId.value}`,
});
onUnmounted(() => {
if (toastTimer) clearTimeout(toastTimer);
});
</script>
<style scoped lang="scss">
.notice-page {
position: relative;
min-height: 100vh;
overflow-x: hidden;
background: $paper;
}
.notice-page__header,
.notice-content {
position: relative;
z-index: 2;
}
.notice-content {
padding: 24rpx 28rpx 100rpx;
}
.notice-list {
display: flex;
flex-direction: column;
gap: 18rpx;
}
.notice-card {
position: relative;
width: 100%;
min-height: 220rpx;
}
.notice-card__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.notice-card__copy {
position: relative;
z-index: 1;
display: flex;
min-height: 220rpx;
box-sizing: border-box;
flex-direction: column;
justify-content: center;
padding: 30rpx 44rpx;
}
.notice-card__status {
display: block;
color: $ink-muted;
font-size: 21rpx;
font-weight: 600;
letter-spacing: 2rpx;
}
.notice-card__status.is-unread {
color: $brand-red;
font-weight: 700;
}
.notice-card__title {
display: block;
margin-top: 7rpx;
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: 30rpx;
font-weight: 700;
}
.notice-card__summary {
display: block;
margin-top: 7rpx;
color: #62584c;
font-size: 23rpx;
font-weight: 500;
line-height: 1.45;
}
.notice-review-action {
margin: 30rpx auto 0;
}
.notice-state-card {
position: relative;
margin-top: 38rpx;
text-align: center;
}
.notice-state-card__skin {
position: absolute;
top: 0;
right: 0;
left: 0;
width: 100%;
height: 220rpx;
min-height: 118px;
}
.notice-state-card__copy {
position: relative;
z-index: 1;
display: flex;
height: 220rpx;
min-height: 118px;
box-sizing: border-box;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 54rpx;
}
.notice-state-card__title {
display: block;
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: 31rpx;
font-weight: 700;
}
.notice-state-card__description {
display: block;
margin-top: 13rpx;
color: $ink-muted;
font-size: 23rpx;
line-height: 1.5;
}
.notice-state-card .app-button {
margin: 20rpx auto 0;
}
@media (min-width: 400px) {
.notice-content {
padding-right: 32rpx;
padding-left: 32rpx;
}
}
</style>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号N-02用途消息详情已读与空状态 -->
<template><ModulePage page-id="n02" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号M-02用途编辑个人资料 -->
<template><ModulePage page-id="m02" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号M-03用途账号与安全设置 -->
<template><ModulePage page-id="m03" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号M-04用途修改密码 -->
<template><ModulePage page-id="m04" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号M-05用途修改手机号 -->
<template><ModulePage page-id="m05" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号M-06用途帮助中心 -->
<template><ModulePage page-id="m06" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号M-07用途意见反馈 -->
<template><ModulePage page-id="m07" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号M-08用途应用推广 -->
<template><ModulePage page-id="m08" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号M-09用途VIP 服务与订单 -->
<template><ModulePage page-id="m09" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号M-10用途关于协议隐私与退出确认 -->
<template><ModulePage page-id="m10" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+344 -3
View File
@@ -1,3 +1,344 @@
<!-- 页面编号R-01用途人物录列表 -->
<template><ModulePage page-id="r01" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<!-- 页面编号R-01用途人物录列表搜索空态与失败状态 -->
<template>
<view
class="people-page"
:class="{
'people-state--ready': peopleState === 'ready',
'people-state--empty': peopleState === 'empty',
'people-state--error': peopleState === 'error',
}"
>
<ModulePageBackground module="records" />
<view class="people-page__header"><PageHeader title="人物录" /></view>
<view class="people-content">
<template v-if="peopleState === 'ready'">
<view class="people-search">
<image
src="/static/assets/modules/records/transparent/r01-search-input-frame.png"
mode="scaleToFill"
/>
<input
v-model="keywordInput"
confirm-type="search"
placeholder="搜索姓名、身份或世代"
placeholder-class="people-search__placeholder"
@confirm="applySearch"
/>
<view
class="people-search__action"
role="button"
aria-label="搜索人物"
@click="applySearch"
><text>{{ keyword ? "重置" : "搜索" }}</text></view
>
</view>
<view v-if="filteredPeople.length" class="people-list">
<view
v-for="person in filteredPeople"
:key="person.id"
class="person-card"
@click="openPerson(person)"
>
<image
class="person-card__skin"
src="/static/assets/modules/records/transparent/r01-person-name-card.png"
mode="scaleToFill"
/>
<view class="person-card__copy">
<text class="person-card__name">{{ person.name }}</text>
<text class="person-card__meta"
>{{ person.role }} · {{ person.generation }} </text
>
<text class="person-card__hint">查看人物档案</text>
</view>
</view>
<AppButton
class="people-primary-action"
block
label="新建人物"
@click="showCreateNotice"
/>
</view>
<view v-else class="people-result-empty">
<image
src="/static/assets/modules/records/transparent/r01-person-name-card.png"
mode="scaleToFill"
/>
<view
><text>没有找到相关人物</text
><text>请更换姓名身份或世代关键词后再试</text></view
>
<AppButton
type="secondary"
block
label="清空搜索"
@click="clearSearch"
/>
</view>
</template>
<view v-else class="people-state-card">
<image
src="/static/assets/modules/records/transparent/r01-person-name-card.png"
mode="scaleToFill"
/>
<view>
<text>{{
peopleState === "empty" ? "还没有人物记录" : "人物录暂不可用"
}}</text>
<text>{{
peopleState === "empty"
? "从第一位值得铭记的家人开始建立人物录。"
: "请稍后重新进入,已有档案不会受到影响。"
}}</text>
</view>
<AppButton
:type="peopleState === 'error' ? 'secondary' : 'primary'"
block
:label="peopleState === 'error' ? '重新查看' : '新建人物'"
@click="peopleState === 'error' ? restoreList() : showCreateNotice()"
/>
</view>
</view>
<AppToast :visible="toastVisible" message="新建人物将在后续功能阶段开放" />
</view>
</template>
<script setup>
import { computed, onUnmounted, ref } from "vue";
import AppButton from "@/components/AppButton.vue";
import AppToast from "@/components/AppToast.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
const people = [
{ id: 1, name: "汤文正", role: "家谱管理员", generation: 18 },
{ id: 2, name: "汤淑华", role: "家族长辈", generation: 17 },
{ id: 3, name: "汤文清", role: "青年代表", generation: 19 },
];
const query = (() => {
if (typeof location !== "undefined")
return Object.fromEntries(
new URLSearchParams(location.hash.split("?")[1] || ""),
);
const pages = getCurrentPages();
return pages[pages.length - 1]?.options || {};
})();
const peopleState = ref(
query.state === "empty"
? "empty"
: query.state === "error"
? "error"
: "ready",
);
const keywordInput = ref("");
const keyword = ref("");
const toastVisible = ref(false);
let toastTimer = null;
const filteredPeople = computed(() => {
const value = keyword.value.trim().toLowerCase();
if (!value) return people;
return people.filter((person) =>
`${person.name} ${person.role}${person.generation}${person.generation}`
.toLowerCase()
.includes(value),
);
});
const applySearch = () => {
if (keyword.value) {
clearSearch();
return;
}
keyword.value = keywordInput.value.trim();
};
const clearSearch = () => {
keywordInput.value = "";
keyword.value = "";
};
const restoreList = () => {
peopleState.value = "ready";
};
const openPerson = (person) =>
uni.navigateTo({
url: `/pages/records/r02-person-detail?personId=${person.id}`,
});
const showCreateNotice = () => {
toastVisible.value = true;
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
toastVisible.value = false;
toastTimer = null;
}, 1800);
};
onUnmounted(() => {
if (toastTimer) clearTimeout(toastTimer);
});
</script>
<style scoped lang="scss">
.people-page {
position: relative;
min-height: 100vh;
overflow-x: hidden;
background: $paper;
}
.people-page__header,
.people-content {
position: relative;
z-index: 2;
}
.people-content {
padding: 22rpx 24rpx 100rpx;
}
.people-search {
position: relative;
width: 100%;
height: 82rpx;
min-height: 44px;
}
.people-search > image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.people-search input {
position: absolute;
z-index: 1;
top: 0;
right: 126rpx;
bottom: 0;
left: 31rpx;
height: 100%;
color: $ink;
font-size: 24rpx;
}
.people-search__placeholder {
color: #8a7965;
}
.people-search__action {
position: absolute;
z-index: 2;
top: 0;
right: 0;
display: flex;
width: 126rpx;
height: 100%;
min-height: 44px;
align-items: center;
justify-content: center;
color: $brand-red;
font-size: 24rpx;
font-weight: 700;
}
.people-list {
margin-top: 12px;
}
.person-card {
position: relative;
width: 100%;
height: clamp(92px, 190rpx, 108px);
margin-top: 12px;
}
.person-card:first-child {
margin-top: 0;
}
.person-card__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.person-card__copy {
position: absolute;
z-index: 1;
inset: 16% 10%;
display: flex;
flex-direction: column;
justify-content: center;
}
.person-card__name {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: 31rpx;
font-weight: 700;
}
.person-card__meta {
margin-top: 7rpx;
color: $ink-muted;
font-size: 23rpx;
}
.person-card__hint {
margin-top: 8rpx;
color: #806a51;
font-size: 21rpx;
}
.people-primary-action {
margin: 22rpx auto 0;
}
.people-result-empty,
.people-state-card {
position: relative;
margin-top: 38rpx;
text-align: center;
}
.people-result-empty > image,
.people-state-card > image {
position: absolute;
top: 0;
right: 0;
left: 0;
width: 100%;
height: 220rpx;
min-height: 118px;
}
.people-result-empty > view,
.people-state-card > view {
position: relative;
z-index: 1;
display: flex;
height: 220rpx;
min-height: 118px;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 54rpx;
}
.people-result-empty text,
.people-state-card text {
display: block;
}
.people-result-empty text:first-child,
.people-state-card text:first-child {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: 31rpx;
font-weight: 700;
}
.people-result-empty text:last-child,
.people-state-card text:last-child {
margin-top: 13rpx;
color: $ink-muted;
font-size: 23rpx;
line-height: 1.5;
}
.people-result-empty .app-button,
.people-state-card .app-button {
margin: 20rpx auto 0;
}
@media (min-width: 400px) {
.people-content {
padding-right: 32rpx;
padding-left: 32rpx;
}
}
</style>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号R-02用途人物录详情与编辑 -->
<template><ModulePage page-id="r02" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号R-03用途贺礼列表 -->
<template><ModulePage page-id="r03" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>
+3 -1
View File
@@ -1,3 +1,5 @@
<!-- 页面编号R-04用途贺礼新增详情与删除确认 -->
<template><ModulePage page-id="r04" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<script setup>
import ModulePage from "@/components/ModulePage.vue";
</script>

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