家谱APP首页样式设计完成,落地80%

This commit is contained in:
2026-07-13 00:23:04 +08:00
commit 1da4d8cf1f
117 changed files with 9476 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
# AGENTS.md
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
Tradeoff: These guidelines bias toward caution over speed. For trivial tasks, use judgment.
1. Think Before Coding
Don't assume. Don't hide confusion. Surface tradeoffs.
Before implementing:
State your assumptions explicitly. If uncertain, ask.
If multiple interpretations exist, present them instead of picking silently.
If a simpler approach exists, say so. Push back when warranted.
If something is unclear, stop. Name what is confusing. Ask.
2. Simplicity First
Minimum code that solves the problem. Nothing speculative.
No features beyond what was asked.
No abstractions for single-use code.
No "flexibility" or "configurability" that was not requested.
No error handling for impossible scenarios.
If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
3. Surgical Changes
Touch only what you must. Clean up only your own mess.
When editing existing code:
Do not "improve" adjacent code, comments, or formatting.
Do not refactor things that are not broken.
Match existing style, even if you would do it differently.
If you notice unrelated dead code, mention it instead of deleting it.
When your changes create orphans:
Remove imports, variables, functions, files, docs, and references that YOUR changes made unused.
Do not remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
4. Contract Discipline
When a new contract lands, make it the only contract in the same change.
A contract means any source-of-truth rule that code depends on: data shape, API boundary, schema, field name, config key, prompt format, file layout, runtime entrypoint, ownership boundary, or workflow.
When introducing or changing a contract, do all three in the same round:
Specify the single owner.
Identify the one module, function, schema, document, or service that owns the contract.
Do not leave the same rule duplicated across frontend, backend, scripts, prompts, docs, or tests.
If multiple places need the value, they should consume it from the owner rather than redefine it.
Delete old fields and old entrypoints.
Remove obsolete fields, fallback reads, compatibility branches, legacy routes, stale config keys, old scripts, and old docs.
Do not keep the old path "just in case" unless the user explicitly asks for a compatibility period.
If compatibility is required, name it as temporary, define the removal condition, and keep it narrow.
Tighten validators and runtime together.
Update schemas, validators, tests, fixtures, seed data, docs, and runtime code in the same change.
Make invalid old inputs fail early and loudly.
Do not allow runtime behavior to accept shapes that validators reject, or validators to allow shapes that runtime no longer supports.
The rule: New owner, old path removed, validator and runtime tightened. If one is missing, the contract migration is not complete.
5. Goal-Driven Execution
Define success criteria. Loop until verified.
Transform tasks into verifiable goals:
"Add validation" -> "Write tests for invalid inputs, then make them pass."
"Fix the bug" -> "Write a test that reproduces it, then make it pass."
"Refactor X" -> "Ensure tests pass before and after."
"Change a contract" -> "Identify owner, remove old paths, tighten validators and runtime, then verify old inputs fail."
For multi-step tasks, state a brief plan:
1. [Step] -> verify: [check]
2. [Step] -> verify: [check]
3. [Step] -> verify: [check]
Strong success criteria let you loop independently. Weak criteria like "make it work" require clarification.
6. Verification Before Closure
A change is not finished until the relevant behavior is checked.
Before reporting completion:
Run the smallest relevant test, script, typecheck, build, or manual verification.
Prefer focused verification over broad, noisy suites when the task is narrow.
If verification cannot be run, say exactly why.
If verification is only by inspection, say that clearly.
If the task changed a contract, verify both the new valid path and the old invalid path.
Do not claim success from intent. Claim success from evidence.
7. Reporting Results
Be concise, concrete, and honest.
When summarizing work:
Say what changed.
Say how it was verified.
Mention any remaining risk or skipped verification.
Do not bury important caveats in vague language.
Do not over-explain routine edits.
Good final answer shape:
Changed: [short summary]
Verified: [command/check]
Notes: [only if needed]
These guidelines are working if: fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, fewer hidden compatibility paths, clearer ownership of contracts, and clarifying questions happen before implementation rather than after mistakes.
+5287
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
<script>
export default {
onLaunch() {
console.log('家谱 App 已启动')
}
}
</script>
<style lang="scss">
@import './styles/global.scss';
</style>
+64
View File
@@ -0,0 +1,64 @@
<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>
</view>
</template>
<script setup>
const props = defineProps({ active: { type: String, required: true } })
const items = [
{ key: 'genealogy', label: '家谱', path: '/pages/genealogy/index', icon: '/static/assets/icons/tab/genealogy-v4.png', activeIcon: '/static/assets/icons/tab/genealogy-active-v4.png' },
{ key: 'family', label: '家族', path: '/pages/family/index', icon: '/static/assets/icons/tab/family-v4.png', activeIcon: '/static/assets/icons/tab/family-active-v4.png' },
{ key: 'profile', label: '我的', path: '/pages/profile/index', icon: '/static/assets/icons/tab/profile-v4.png', activeIcon: '/static/assets/icons/tab/profile-active-v4.png' }
]
const switchTab = (item) => {
if (item.key !== props.active) uni.reLaunch({ url: item.path })
}
</script>
<style scoped lang="scss">
.app-tabbar {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 20;
display: flex;
height: 128rpx;
padding-bottom: env(safe-area-inset-bottom);
box-sizing: content-box;
border-top: 1rpx solid $gold-soft;
background: $paper-white;
}
.tab-item {
display: flex;
min-height: 96rpx;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10rpx 0 8rpx;
box-sizing: border-box;
}
.tab-icon {
width: 56rpx;
height: 56rpx;
margin-bottom: 5rpx;
}
.tab-label {
color: $ink-muted;
font-size: 28rpx;
letter-spacing: 2rpx;
line-height: 1.2;
}
.tab-label.active { color: $brand-red; font-weight: 700; }
</style>
+138
View File
@@ -0,0 +1,138 @@
<template>
<view class="genealogy-card" :class="{ 'genealogy-card--current': selected }" @click="$emit('select', genealogy)">
<image class="row-frame" src="/static/assets/backgrounds/genealogy-list-slip-frame.png" mode="scaleToFill" />
<view class="surname-seal">
<image class="surname-seal-frame" src="/static/assets/icons/genealogy/seal-row-frame-v1.png" mode="scaleToFill" />
<view class="surname-seal-copy">
<text class="seal-title">家谱</text>
</view>
</view>
<view class="card-copy">
<text class="card-name">{{ genealogy.name }}</text>
<view class="card-detail-row">
<view class="card-metas">
<view class="card-meta-item">
<image class="card-meta-icon" src="/static/assets/icons/common/location-v1.png" mode="aspectFit" />
<text class="card-meta">{{ genealogy.location }}</text>
</view>
<view class="card-meta-item">
<image class="card-meta-icon" src="/static/assets/icons/common/member-meta-v1.png" mode="aspectFit" />
<text class="card-meta">{{ genealogy.memberCount }} 位成员</text>
</view>
</view>
<view class="card-updated">
<text>更新于 {{ genealogy.updatedAt }}</text>
</view>
</view>
</view>
<view class="card-side">
<text class="card-role">{{ role }}</text>
<image class="card-chevron" src="/static/assets/icons/common/chevron-right-v2.png" mode="aspectFit" />
</view>
</view>
</template>
<script setup>
defineProps({
genealogy: { type: Object, required: true },
role: { type: String, required: true },
selected: { type: Boolean, default: false }
})
defineEmits(['select'])
</script>
<style scoped lang="scss">
.genealogy-card {
position: relative;
display: flex;
min-height: 160rpx;
align-items: center;
padding: 20rpx 24rpx;
box-sizing: border-box;
background: transparent;
}
.genealogy-card--current { background: transparent; }
.row-frame {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.surname-seal {
position: relative;
z-index: 1;
display: flex;
width: 76rpx;
height: 118rpx;
flex: 0 0 auto;
align-items: center;
justify-content: center;
margin-right: 22rpx;
color: #fff5df;
}
.surname-seal-frame { position: absolute; top: 0; right: 0; bottom: 0; left: 0; z-index: 0; width: 100%; height: 100%; }
.surname-seal-copy { position: relative; z-index: 1; display: flex; height: 82rpx; align-items: center; justify-content: center; }
.seal-title { color: #fff5df; font-family: 'STKaiti', 'KaiTi', serif; font-size: 30rpx; font-weight: 700; letter-spacing: 2rpx; line-height: 1; writing-mode: vertical-rl; }
.genealogy-card:not(.genealogy-card--current) .surname-seal-frame { opacity: .32; }
.genealogy-card:not(.genealogy-card--current) .seal-title { color: $ink-muted; }
.card-copy {
position: relative;
z-index: 1;
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
}
.card-name,
.card-meta {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-name { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 37rpx; font-weight: 700; }
.card-detail-row { display: flex; min-width: 0; align-items: center; justify-content: space-between; margin-top: 12rpx; }
.card-metas { display: flex; min-width: 0; align-items: center; }
.card-meta-item { display: flex; min-width: 0; align-items: center; margin-right: 14rpx; }
.card-meta-icon { width: 24rpx; height: 24rpx; margin-right: 5rpx; }
.card-meta { color: $ink-muted; font-size: 22rpx; }
.card-updated { display: flex; flex: 0 0 auto; align-items: center; margin-left: 10rpx; color: $ink-muted; font-size: 20rpx; white-space: nowrap; }
.card-side {
position: relative;
z-index: 1;
display: flex;
width: 124rpx;
flex: 0 0 auto;
flex-direction: row;
align-items: center;
justify-content: flex-end;
margin-left: 10rpx;
}
.card-role { color: $ink-muted; font-size: 24rpx; white-space: nowrap; }
.card-chevron { width: 34rpx; height: 34rpx; margin-left: 14rpx; }
@media screen and (max-width: 340px) {
.genealogy-card { min-height: 148rpx; padding-right: 18rpx; padding-left: 18rpx; }
.surname-seal { width: 68rpx; height: 106rpx; margin-right: 16rpx; }
.card-name { font-size: 32rpx; }
.card-meta { font-size: 20rpx; }
.card-updated { display: none; }
.card-side { width: 92rpx; margin-left: 8rpx; }
.card-chevron { width: 28rpx; height: 28rpx; margin-left: 8rpx; }
}
</style>
+152
View File
@@ -0,0 +1,152 @@
<template>
<view class="page-header" :class="{ 'page-header--root': root }">
<image v-if="root" class="header-texture" src="/static/assets/backgrounds/header-cinnabar-texture-v2.jpg" mode="scaleToFill" />
<image v-if="root" class="header-hall" src="/static/assets/backgrounds/header-hall-lineart.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/icons/brand/jiapu-seal-logo-header.png" mode="aspectFit" />
</view>
<view v-else class="header-back" @click="goBack">{{ backLabel }}</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/icons/common/notice-v3.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>
</view>
</template>
<script setup>
const props = defineProps({
title: { type: String, required: true },
action: { type: String, default: '' },
backLabel: { type: String, default: '返回' },
root: { type: Boolean, default: false },
unreadCount: { type: Number, default: 0 }
})
const goBack = () => uni.navigateBack()
</script>
<style scoped lang="scss">
.page-header {
display: flex;
align-items: center;
height: 104rpx;
padding: 0 24rpx;
box-sizing: border-box;
background: $brand-red;
color: #fff9ed;
}
.page-header--root {
position: relative;
z-index: 3;
height: 184rpx;
padding-top: env(safe-area-inset-top);
background-color: #b52e22;
overflow: hidden;
}
.header-texture {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.header-hall {
position: absolute;
bottom: -8rpx;
left: -34rpx;
z-index: 1;
width: 368rpx;
height: 147rpx;
opacity: .16;
pointer-events: none;
}
.page-header--root .header-side,
.page-header--root .header-title { position: relative; z-index: 2; }
.header-side {
display: flex;
width: 120rpx;
min-height: 88rpx;
align-items: center;
}
.header-side--left { justify-content: flex-start; }
.header-side--right { justify-content: flex-end; }
.header-icon-button {
position: relative;
display: flex;
width: 88rpx;
height: 88rpx;
align-items: center;
justify-content: center;
}
.header-logo { width: 66rpx; height: 66rpx; }
.header-notice-icon { width: 48rpx; height: 48rpx; }
.notice-dot {
position: absolute;
top: 15rpx;
right: 13rpx;
width: 14rpx;
height: 14rpx;
border: 2rpx solid $brand-red;
border-radius: 50%;
background: #fff9ed;
}
.header-back,
.header-action {
width: 100%;
font-size: 27rpx;
}
.header-back { text-align: left; }
.header-action { color: #ffe3a7; text-align: right; }
.header-title {
flex: 1;
overflow: hidden;
color: #fff9ed;
font-family: serif;
font-size: 35rpx;
font-weight: 700;
letter-spacing: 4rpx;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.page-header--root .header-title {
color: #ffe3a7;
font-family: 'STKaiti', 'KaiTi', serif;
font-size: 48rpx;
letter-spacing: 5rpx;
transform: translateY(8rpx);
}
.page-header--root .header-logo {
width: 88rpx;
height: 88rpx;
transform: translate(-8rpx, -12rpx);
}
.page-header--root .header-notice { transform: translateY(10rpx); }
</style>
+68
View File
@@ -0,0 +1,68 @@
export const currentUser = {
id: 1,
name: '汤文远',
phone: '138****1024',
role: '创建者'
}
export const genealogies = [
{
id: 1001,
surname: '汤',
name: '汤氏家谱',
hall: '敦睦堂',
location: '河南·洛阳',
memberCount: 158,
updatedAt: '2024-05-12',
activeCount: 8,
visibility: '仅成员可见',
motto: '敦亲睦族,敬祖传家。'
},
{
id: 1002,
surname: '汤',
name: '汤氏宗谱',
hall: '承志堂',
location: '山东·济宁',
memberCount: 286,
updatedAt: '2024-04-28',
activeCount: 15,
visibility: '公开可申请',
motto: '继往开来,世守家风。'
}
]
export const treeMembers = [
{ id: 1, name: '汤文远', relation: '始祖', generation: 1, years: '1940—2012', branch: '主支', x: 50, y: 7 },
{ id: 2, name: '汤正国', relation: '长子', generation: 2, years: '1965—', branch: '主支', x: 20, y: 38 },
{ id: 3, name: '汤正华', relation: '次子', generation: 2, years: '1968—', branch: '主支', x: 50, y: 38 },
{ id: 4, name: '汤正强', relation: '三子', generation: 2, years: '1972—', branch: '支系', x: 80, y: 38 },
{ id: 5, name: '汤凯', relation: '长孙', generation: 3, years: '1992—', branch: '主支', x: 16, y: 70 },
{ id: 6, name: '汤悦', relation: '长女', generation: 3, years: '1995—', branch: '主支', x: 35, y: 70 },
{ id: 7, name: '汤晨', relation: '次子', generation: 3, years: '1998—', branch: '主支', x: 54, y: 70 },
{ id: 8, name: '汤昊', relation: '三子', generation: 3, years: '2001—', branch: '支系', x: 73, y: 70 },
{ id: 9, name: '汤宁', relation: '四女', generation: 3, years: '2005—', branch: '支系', x: 90, y: 70 }
]
export const familyFeeds = [
{ id: 1, author: '汤正华', title: '清明祭祖通知', content: '本周六上午举行清明祭祖,敬请各支系族亲相互转告。', date: '今天 09:30', type: '族务' },
{ id: 2, author: '汤悦', title: '老宅修缮旧影', content: '整理出一组敦睦堂老宅照片,已收入家族相册。', date: '昨天 20:18', type: '相册' }
]
export const familyContent = {
article: [{ id: 1, title: '敦睦堂家训摘录', summary: '孝友传家,勤俭立业,敬祖睦族。', date: '2025年4月' }],
album: [{ id: 1, title: '敦睦堂老宅旧影', summary: '收录修缮前后的珍贵照片,共 6 张。', date: '2025年3月' }],
ceremony: [{ id: 1, title: '清明祭祖', summary: '本周六上午举行祭祖仪式,请族人相互转告。', date: '3 天后' }],
record: [{ id: 1, title: '老宅修缮备忘', summary: '屋脊木构加固方案已确认,等待施工。', date: '今天' }]
}
export const notifications = [
{ id: 1, title: '有新的入谱申请', content: '陈先生申请加入四川武胜汤氏族谱,等待你的审核。', time: '10:24', unread: true },
{ id: 2, title: '祭祀活动提醒', content: '清明祭祖将在 3 天后开始。', time: '昨天', unread: true },
{ id: 3, title: '相册有新照片', content: '汤悦上传了 6 张老宅修缮照片。', time: '4月9日', unread: false }
]
export const joinApplications = [
{ id: 1, name: '汤志成', phone: '139****6421', relation: '自述为汤正华堂侄', appliedAt: '今天 10:24', status: 'PENDING' },
{ id: 2, name: '汤雨薇', phone: '136****2798', relation: '自述为汤正国之女', appliedAt: '昨天 18:02', status: 'PENDING' }
]
+34
View File
@@ -0,0 +1,34 @@
# 家谱 App 项目计划
## 已确定的产品规则
- 端:uni-app 用户端,面向 Android 与 iOS 的 HBuilderX 打包。
- 导航:家谱、家族、我的;通知入口只做提醒与跳转。
- 主流程:管理员创建家谱并录入首代关系,族人申请加入后由管理员审核。
- 访问规则:家谱默认仅成员可见;管理员可开放为可搜索、可申请加入。
- 视觉:家祠卷轴——朱砂红、宣纸暖白、黛青、鎏金细线、竖式人物牌位。
- 素材:自制图标均为透明背景 PNG;必要时采用可商用在线 PNG 图标并统一风格。
- 接口:`APP.openapi.yaml` 是 App 端接口契约;地址、`clientid``tenantId` 与测试账号通过本地配置注入。
## 进度
- [x] 阅读思维导图、现有界面稿、参考 uni-app 工程与 OpenAPI 文档。
- [x] 确定三 Tab 信息架构、核心创建流程、加入审核规则和视觉方向。
- [x] 创建 uni-app 工程骨架、设计令牌与透明 PNG 图标资产。
- [x] 完成登录、我的家谱、创建家谱、家谱总览、世系树、成员档案和申请审核的首版界面与本地交互闭环。
- [x] 实现登录、我的家谱、创建家谱与公开家谱申请加入。
- [x] 修复 Vue 3 工程入口:补齐根目录 `index.html`,并加入配置与入口自检。
- [x] 完成当前已实现页面的 HBuilderX Web 编译、全量语言诊断与缓存排查;本地 H5 发行构建仍受 HBuilderX 登录状态限制。
- [x] 参照完整 uni-app 工程修复 Sass 注入:令牌直接置于 `uni.scss`,重启 Vite 后登录页样式请求已验证为 200。
- [x] 完成显式 Mock/Remote 模式、密码登录链路、token 保存、接口 `data` 解包与当前家谱 ID 上下文。
- [x] 完成创建家谱后录入首代,并按当前家谱加载详情、世系树、成员档案、申请审核与通知已读。
- [x] 完成家族动态发布、谱文/相册/祭祀/族务内容入口,以及动态个人资料读取。
- [ ] 实现家谱总览、世系树、成员资料和亲属关系编辑。
- [ ] 实现家族圈、谱文、相册、祭祀、贺礼、成长记录、备忘和功德。
- [ ] 实现通知、个人中心、帮助、反馈、VIP 与接口配置。
- [ ] 在 HBuilderX 验证运行、Android/iOS 打包配置和关键流程。
## 当前接口限制
- 已支持:认证、家谱、成员、字辈、世系树、家族圈、文章、相册、祭祀、族务记录、通知、反馈与 VIP。
- 暂不实现为可用功能:邀请码、视频与多管理员分级权限;接口文档尚未提供对应端点。
+28
View File
@@ -0,0 +1,28 @@
# 参考 uni-app 工程结构核对
参考工程:`C:\Users\Rain\Desktop\软件\JOB\3dyjsapp`
## 已确认并采纳
- 参考工程是完整的 HBuilderX uni-app 工程,根目录使用 `App.vue``main.js``pages.json``manifest.json``index.html``uni.scss`
- 参考工程的 `manifest.json` 已声明 Vue 3`main.js` 同时保留 Vue 2 与 Vue 3 的条件入口。家谱项目固定使用 Vue 3,因此保留精简的 Vue 3 入口即可。
- 参考工程的 `uni.scss` 直接定义 Sass 变量,而不是在其中使用相对路径导入。家谱项目已按此方式调整:`uni.scss` 是唯一的设计令牌来源。
- 参考工程把页面清单集中在 `pages.json`,家谱项目已用自动审计校验每条路由都有对应页面文件。
## 本项目的对应实现
- `uni.scss`:朱砂红、宣纸暖白、墨色、鎏金等 Sass 令牌。
- `styles/global.scss`:只由 `App.vue` 从根目录引入的全局通用样式。
- `tests/compile-audit.ps1`:校验路由、入口、Sass 引用与模块引用。
- `tests/uni-scss-injection.ps1`:防止在 `uni.scss` 中再次写入会被页面按错误目录解析的相对 Sass 导入。
## 不照搬的部分
- 彩票业务页面、接口、数据模型与大量业务组件。
- 参考工程中的第三方 UI 模块、OAuth 配置和过宽设备权限;家谱 App 按实际功能逐项添加,避免无关权限与凭据进入工程。
## 后续仍需补齐
- 家谱 App 的 Android/iOS 应用图标尺寸与启动图资源。
- 登录后的真实接口配置与测试账号联调。
- HBuilderX 登录后的本地 H5 发行构建及 Android/iOS 云打包验证。
@@ -0,0 +1,265 @@
# D1:安卓视觉规范与页面壳
> 对应《规划》:P-02、P-03、P-04
> 适用范围:Android 手机端全部页面
> 设计锚点:用户提供的“汤氏家谱世系图”参考图
## 1. 设计锚点与不可偏离项
参考图不是只借用“红色”,而是整套视觉语言的依据。后续每一页必须具备以下共性:
1. **家祠红头图**:核心家谱页面使用朱砂红顶部,左侧为低对比古建筑线描,标题使用金色或暖白色。
2. **宣纸正文区**:主内容始终落在暖白纸面上,背景只允许极浅的山峦、竹影、亭台或纸纤维纹理,不能影响文字阅读。
3. **墨字与古金线**:正文使用墨褐色,分隔、牌位、题签和次级结构使用古金细线。
4. **礼制化状态**:当前选中、重要操作与未读提醒使用朱砂;不可用荧光色、大面积渐变或通用蓝色按钮替代。
5. **牌位不是卡片**:世系树、人物录核心信息使用竖式牌位;其他列表保留纸签和金线的秩序感,不能变成白底大圆角信息卡。
### 1.1 一比一还原的验收定义
“一比一”指以参考图为唯一视觉锚点,不能只借用红色后另行发挥。以 `360 × 800dp` 设计画布验收时,必须同时还原:朱砂红头的高度与留白、祠堂线描的位置和低对比度、金色标题比例、宣纸与山水底纹、古金牌位线框、朱砂选中描边、纸面底部抽屉与底部导航的层级效果。
不同业务页的内容结构可以不同,但不能把世系图页面原样复制到每个页面;它们必须在同一套材质、颜色、字体、线框和动效逻辑中变化。为适配 `320 × 568` 等小屏,元素可流式换行或缩放,**不得**为追求固定像素而造成重叠或不可点击。
每张后续设计稿都要和参考图并排核对上述八项;没有通过核对,不能标为设计完成。
## 2. Android 基础尺寸
设计坐标以 `750rpx` 宽度为基准。实现时允许流式收缩,但不得缩小关键点击区。
| 元素 | 设计值 | 小屏规则 |
| --- | ---: | --- |
| 页面左右安全边距 | `32rpx` | `320px` 小屏可缩至 `24rpx`。 |
| 页面标题 | `34rpx` | 不换成两行;长标题使用省略号。 |
| 主标题 | `48rpx` | 家谱名最大两行,超出截断。 |
| 正文 | `28rpx` | 不低于 `24rpx`。 |
| 辅助文字 | `24rpx` | 仅用于时间、状态、说明。 |
| 主按钮高度 | `88rpx` | 最小点击高度,不因小屏减小。 |
| 图标触控框 | `88rpx × 88rpx` | 图标视觉大小可为 `4048rpx`。 |
| 列表行高度 | `104rpx` 起 | 多行信息自动增高。 |
| 底部操作栏 | `128rpx` 起 | 加系统导航保底内边距。 |
## 3. 色彩、字体与图标
### 3.1 色彩
| Token | 色值 | 使用位置 |
| --- | --- | --- |
| `paper-page` | `#F6F0E5` | 页面整体纸底。 |
| `paper-surface` | `#FFF9EE` | 抽屉、表单、列表承载面。 |
| `cinnabar` | `#A33B2B` | 主操作、选中描边、徽章。 |
| `cinnabar-deep` | `#7E251C` | 顶部红底的层次和按下态。 |
| `ink` | `#342A24` | 标题、主正文。 |
| `ink-muted` | `#7A6958` | 次级正文、时间。 |
| `antique-gold` | `#B58A4B` | 牌位描边、细分割线、装饰。 |
| `gold-soft` | `#E4D4B8` | 弱分割线、未选中轮廓。 |
| `danger` | `#B63A2C` | 删除、审核拒绝;不额外引入鲜红。 |
绿色不进入页面的背景、导航、主按钮或状态色。若用户照片中含绿色,保持原图,不再叠加绿色 UI 元素。
### 3.2 字体
- 页面标题、家谱名、人物姓名:系统衬线回退链 `serif`;仅用于短标题和牌位姓名,体现家谱的书卷感。
- 正文、表单、数字、按钮:Android 系统无衬线字体 `sans-serif`;优先保证低版本设备的字形和行高稳定。
- 不下载网络字体,不依赖单一厂商字体,不用细于常规字重的中文字体。
- 姓名为两至四字时居中竖排;超过四字改为横排或两行,禁止压缩字距造成难读。
### 3.3 透明 PNG 图标规范
- 图标文件一律是 RGBA PNG,四角透明;不把乳白或绿色底色烘焙进图标。
- 功能图标以官方 `24dp × 24dp` 视口为唯一留白标准,导出为 `96 × 96px`(4 倍)透明 PNG;不得再从大幅生成图中裁出图标、不得人为叠加 `25%` 以上的额外空白。图标的边距只保留官方图形本身的标准边距。
- 正常态使用墨褐或古金单色;选中态使用朱砂;不使用渐变、投影或多色填充。
- 图标命名采用 `功能-状态.png`,例如 `tree-active.png``notice-default.png``profile-active.png`
- 首批需要的图标:返回、搜索、更多、通知、家谱、家族、我的、世系图、列表、编辑、新增、相册、礼仪、消息、设置、关闭。
### 3.4 资产优先规则
- 所有具备明确动作或状态的元素必须使用透明 PNG 图标:导航、返回、搜索、通知、更多、关闭、编辑、新增、上传、相册、成员、世系、礼仪、设置、删除、分享、点赞、评论。
- 禁止使用 Emoji、Unicode 符号、临时文字(例如“返回”“更多”)替代功能图标;功能图标旁的文字仅用于解释,不承担唯一识别职责。
- 文字只保留在用户必须阅读或选择的内容上:页面标题、家谱名称、成员资料、表单标签、状态说明与按钮动作。
- 每一个图标必须有默认态与需要时的选中态;选中态仅改变为朱砂色,不改变图形轮廓,保证低端 Android 设备上也能一眼识别。
- **后续强制规则**:任何后续页面设计中新增加的图标,必须同步生成对应的 RGBA 透明 PNG,保存到项目 `static/assets/icons/` 后才能将该页面标记为“设计完成”。设计图中的图形、文字占位、Emoji 或系统默认图标均不能替代实际资产。
- 每个新增图标都要在资产清单登记:用途、文件路径、默认 / 选中状态、设计页编号和透明背景检查结果;页面实现只能引用清单中的已验收资产。
### 3.4.1 功能图标的标准骨架与视觉风格
- Android / Material 的在线规范用于锁定**图标语义、24dp 视口、触控区、默认 / 选中状态和可识别性**,不是要求本项目的每个图标都长成默认黑色 Material 图标。
- 项目 Logo、祠堂纹样、宣纸、山水竹影属于品牌与背景插画,可以定制生成;顶部和通用操作图标允许采用本项目的“古金双线 + 克制云纹收口”风格,风格锚点见 [公共操作图标风格锚点](references/公共操作图标-风格锚点.png)。
- 自定义图标仍须保留标准语义:返回一眼识别为左箭头、搜索为放大镜、通知为铃铛、更多为三点、关闭为 X;不得因装饰而改变动作含义。
- 底部导航固定为三项且每项同时显示图标与短标签:`家谱``家族``我的`。现有 Tab 图标不通过验收,必须重设计为等视觉重量的简洁轮廓;不能出现云纹、枝叶和双层复杂装饰。选中项使用同一图形的朱砂色与填充 / 加粗状态,非选中项用墨褐或古金描边。
- 顶部与通用操作采用标准语义:返回、搜索、通知、更多、关闭、新增、编辑、上传、删除、分享、设置。图标可定制风格,但不以文字、Emoji 或难以识别的自造图形替代。
### 3.4.2 按钮图标风格分级
- **大部分按钮与顶部操作**:返回、搜索、通知、更多、关闭、新增、编辑、上传、删除、分享、设置,以及家谱 / 家族模块中的快捷入口,采用“古金双线 + 轻云纹收口”风格。
- **主操作按钮**:图标可置于“新建家谱、保存、发布、提交”等文字前;图标为古金双线简化版,按钮本身仍以朱砂描边或朱砂实底表达主次,不用纯文字按钮。
- **底部 Tab**:不得直接使用复杂云纹版本;使用更简洁的同色系轮廓,优先保证 24dp 识别度和三个目的地的差异。
- **状态与危险操作**:成功、失败、删除、无权限等不使用花纹干扰信息;保留标准清晰的符号,颜色按状态区分。
> 来源依据:Google Material Symbols 官方库提供适用于 Android 的 PNG / SVG 图标、`FILL` 状态轴和 2048dp 光学尺寸;Android 官方将导航栏用于 3–5 个同等级目的地。项目以这些规则做底线,并在其上保留家祠卷轴的视觉个性。
### 3.5 图标资产清单与路径
所有最终资产均保存在 `static/assets/icons/`,由页面直接引用;不使用在线 URL。
| 资产组 | 文件 | 状态 |
| --- | --- | --- |
| 项目标识 | `brand/jiapu-seal-logo.png` | 自定义生成,待缩小尺寸验收 |
| 顶部操作 | `common/back.png``common/search.png``common/notice.png``common/more.png``common/close.png` | 保留已确认的古金双线风格,待逐个紧凑导出 |
| 底部导航 | `tab/genealogy.png``tab/family.png``tab/profile.png` 及各自 `-active.png` | 待按标准 24dp 语义重绘为简洁同风格版本 |
| 家谱操作 | `genealogy/tree.png``members.png``generation-poem.png``application.png``create.png``edit.png` | 页面设计时按 Material Symbols 官方源导出 |
| 家族内容 | `family/feed.png``article.png``album.png``ceremony.png``gift.png``archive.png` | 页面设计时按 Material Symbols 官方源导出 |
| 通用操作 | `action/add.png``upload.png``delete.png``share.png``like.png``comment.png``setting.png` | 待按古金双线风格逐个紧凑导出 |
### 3.5 已否决的资产(不得引用)
`static/assets/icons/common/``static/assets/icons/tab/``static/assets/icons/action/` 当前是从大幅绿色底图裁切的原始试验文件,仍不得直接引用。其问题是**画布和缩放方式**,不是用户已确认的顶部操作图标美术方向。
**根因记录**:这些文件来自带大块绿色空白的 3 × 2 AI 图稿,再按格子裁切;生成图没有稳定的标准视口,导致图标周围出现过多空白、主体大小不一致。顶部操作图标的古金双线与轻云纹风格保留;Tab 图标的复杂枝叶、过多装饰和不统一轮廓不保留。
**新生产方式**:每个图标单独制作并紧凑导出到 `96px` PNG,实际图形占官方 24dp 视口的正常范围;不得再批量盲裁。顶部 / 操作图标以风格锚点为准,Tab 按简洁语义重新制作。每个文件在 `46rpx` 实际显示尺寸检查后才可引用。
### 3.6 功能图标验收
功能图标只有同时满足以下条件,才可标记为“已生成”:
1. 语义、视口与状态符合 Google Material Symbols / Android 规范;若为本项目定制图标,记录其对应的标准语义与风格锚点。
2. 默认 / 选中态为同一语义符号,保持 `24dp` 标准画布与固定留白;不得改变动作含义来冒充选中态。
3. 项目内文件为 RGBA 透明 PNG,四角透明;默认墨褐、选中朱砂;在纸白底和红头上均满足清晰可见。
4.`46rpx` Tab 显示尺寸及 `320 × 568` 小屏中检查可识别性、标签不换行、点击区不小于 `88rpx`
### 3.7 背景资产规则
- 所有装饰性背景都必须生成并保存在项目 `static/assets/backgrounds/`;禁止依赖网络背景图,也不能只用临时 CSS 渐变假装纸纹。
- 背景分层使用:`header-hall-lineart.png` 用于朱砂顶部;`paper-rice-texture.jpg` 用于宣纸底;`footer-mountain-bamboo.png` 用于页面底部的淡山水竹影。页面按需组合,不把它们烘焙成一张全屏超大图。
- 祠堂线描、山水、竹影等叠加背景应使用透明 PNG;宣纸纸纹使用压缩后的 JPG。所有背景必须无文字、无 Logo、无人物、无绿色调。
- 为兼容老款 Android:顶部纹样显示尺寸不超过 `750 × 288rpx`,底部山水显示尺寸不超过 `750 × 360rpx`;背景只在首屏和固定区域加载,不在长列表中重复解码。
- 背景完成验收需检查:`320 × 568` 下不遮挡标题或操作、纸纹不影响文字对比度、图片已压缩到适合移动端的尺寸。
- **页面背景完成条件**:每一个页面必须在其设计记录中写清“红头 / 纸纹 / 山水竹影 / 侧边竹影 / 无”的组合与对应资产路径;没有背景组合的页面不得标记为“设计完成”。背景可以复用基础资产,但不能是未设计的纯色空白页面或临时 CSS 渐变。
| 页面类型 | 必须使用的背景组合 |
| --- | --- |
| 家谱总览、世系树、成员档案 | 朱砂红头 + 祠堂线描 + 宣纸纸纹;底部或抽屉使用淡山水竹影。 |
| 家族动态、谱文、相册、礼仪、消息 | 紧凑朱砂题签 + 宣纸纸纹;内容尾部按需叠加竹影或山水。 |
| 创建、编辑、申请、设置、反馈 | 紧凑题签 + 宣纸纸纹;表单区域保持干净,不让山水压住输入内容。 |
| 登录、注册、找回密码 | 独立生成的红头 / 纸纹欢迎背景;不能直接复用列表页大山水。 |
| 无数据、失败、审核中 | 宣纸纸纹 + 对应低对比卷轴 / 印章背景资产;不能只显示文字。 |
| 背景资产 | 路径 | 用途 | 状态 |
| --- | --- | --- | --- |
| 祠堂线描 | `backgrounds/header-hall-lineart.png` | 家谱红头左侧或底部纹样 | 生成中 |
| 宣纸纸纹 | `backgrounds/paper-rice-texture.jpg` | 全局纸面底色 | 未生成 |
| 山水竹影 | `backgrounds/footer-mountain-bamboo.png` | 列表页、抽屉页底部装饰 | 未生成 |
## 4. 公共页面壳
### 4.1 核心家谱页面壳
适用于:家谱总览、世系图、成员目录、人物档案。
- 顶部 `256288rpx`:朱砂红,左下放置低对比祠堂屋檐线描,禁止使用满屏建筑照片。
- 根页面左上角放项目 Logo(祠堂屋檐 + “谱”字印章,透明 PNG,视觉 `40rpx`、触控区 `88rpx`);详情页该位置优先保留返回按钮,Logo 收入建筑纹样中的小印记。
- 搜索、通知、更多均为 `88rpx` 触控框;图标为暖白或金色线稿 PNG。通知铃铛仅在未读消息数大于 0 时显示朱砂小圆点;未读为 0 时不显示圆点和数字。
- 家谱名称位于视觉中心,最大两行;名称下可放地域或代系提示,但不能与图标重叠。
- 红头结束后立即进入纸面 Tab/分段导航,使用古金底线标记当前项。
### 4.2 普通列表与详情页面壳
适用于:家族动态、谱文、相册、人物录、礼仪、消息、我的。
- 顶部使用 `144176rpx` 紧凑朱砂题签,保留建筑线描的局部纹样。
- 内容区采用纸白承载面与古金细分割线,列表项不使用夸张圆角;圆角最大 `16rpx`
- 每页只保留一个视觉主操作:新增、发布、保存或提交。它固定在底部操作栏或右下角,不能同时出现两个醒目的红按钮。
### 4.3 表单与编辑页面壳
适用于:创建家谱、录入始祖、申请加入、发布内容、编辑成员、反馈、设置。
- 顶部为紧凑题签,正文使用逐行纸签式表单,标签在左、输入值在右或下一行。
- 必填项用朱砂小印记表示;校验说明用暖灰褐,不使用大面积红色报错框。
- 键盘出现时页面可滚动,底部“保存 / 提交”保持可操作。
### 4.4 底部导航与抽屉
- 三 Tab:家谱、家族、我的。纸白底、顶部古金细线;当前图标和文字用朱砂,未选中用墨褐。
- 无底部 Tab 的详情页,底部保留返回区域或单主操作栏,不能出现空白黑条。
- 成员、相册、礼仪的详情采用半屏至全屏底部抽屉:纸白底、顶部金色短握条、右上关闭按钮、淡竹影或山影作为低对比装饰。
## 5. 核心组件规则
### 5.1 世系人物牌位
- 牌位为暖白底、古金双线边框、上下折角;不使用普通矩形卡片。
- 始祖牌位比同层成员高一级;当前选中人物外框换成朱砂双线,并使用小圆点连接标识。
- 牌内信息顺序:姓名 → 字 / 号 → 生卒或年份;信息不足时显示“生卒不详”,不留大段空白。
- 代际标签固定在树左侧,不随横向滚动丢失;横向树区域与底部档案抽屉互不遮挡。
### 5.2 纸签列表
- 左侧为功能图标或缩略图,中间为标题和两行信息,右侧是状态或箭头。
- 每行以古金细线分隔;红色仅用于“待审核”“未读”“进行中”等需要注意的状态。
- 长标题最多两行;日期、人数、地域等元信息单独一行,不与操作按钮抢空间。
### 5.3 主操作与次操作
- 主操作:朱砂描边或朱砂实底,圆角 `44rpx` 的胶囊形;文字白色或纸白。
- 次操作:纸白底、古金描边、墨褐文字。
- 删除和退出:先显示纸面确认抽屉,再允许使用 `danger` 色;不可直接用红色文字替代确认流程。
### 5.4 空状态与异常
- 空状态使用纸纹中的淡印章、卷轴、山影或未展开牌位,不使用卡通人物。
- 网络失败使用“纸页未送达”图标、明确重试按钮;不把错误码直接展示给用户。
- 审核中使用朱砂印章和预计提示;无权限使用合拢卷轴与申请/返回入口。
## 6. 首屏结构:家谱首页(G-01)
G-01 是所有设计页的公共还原样板,不等同于最终代码。
1. 顶部:朱砂祠堂红头,左侧项目 Logo,中央为“我的家谱”,右侧通知 PNG 图标;只有未读消息时才带朱砂小圆点。
2. 主区:当前家谱以横向家谱题签展示名称、地域、成员数和身份;这个题签的朱砂印章代表“当前选中的家谱”,不是另一种家谱颜色。没有家谱时替换为卷轴空状态。
3. 快捷区:世系图、成员、字辈诗、申请审核四项,使用透明 PNG 图标与古金分隔。
4. 家谱列表:明确分为“我创建的”和“我加入的”两个标题区;每项使用同一套纸签和古金线,不用红色 / 金色印章暗示未说明的身份差异。当前选中项可用朱砂细边标记。
5. 底部:三 Tab;主操作“创建家谱”在内容末尾或空状态中出现,避免遮挡 Tab。
### 6.1 G-01 数据文案规则
- 家谱名称后不得出现无意义横线、破折号、模拟字符或“待补充”占位。
- 有数据时按真实字段显示,例如“河南省 · 洛阳市”“158 位成员”“管理员”;无数据时整行隐藏,或显示明确的“地区待完善 / 成员待录入”。
- “当前家谱”与下方列表不得无说明地重复同一名称;当前题签显示选中的家谱,下方列表用于切换,选中项需要清晰标记。
### 6.2 G-01 固定设计栅格(360 × 800dp
这是后续页面还原的硬尺寸基准,不再交给整页 AI 图自行猜测:
| 区域 | 尺寸 / 间距 | 约束 |
| --- | --- | --- |
| 系统状态栏 | `24dp` | 只预留系统空间,不放页面内容。 |
| 家祠红头 | `112dp`(含标题区) | Logo 左 `16dp`,标题居中,铃铛右 `16dp`;红头结束后不得出现大块空白。 |
| 当前家谱题签 | 顶距 `16dp`,高 `136dp`,左右 `16dp` | 只展示当前家谱一次;名称、成员数、地域、身份均为真实字段。 |
| 快捷入口 | 题签后 `16dp`,行高 `84dp` | 四项等宽;图标实际 `24dp`,标签 `14sp`,不使用大号装饰图。 |
| 分隔与列表标题 | 上距 `16dp`,标题区 `36dp` | 细古金线 + “我创建的 / 我加入的”,不能留无内容的整屏空白。 |
| 家谱列表项 | 每项 `72dp`,项间 `8dp` | 一项只显示名称、必要元信息、右箭头;不重复当前题签内容。 |
| 新建家谱 | 列表后 `16dp`,高 `48dp` | 朱砂描边胶囊按钮,不用巨大的上下留白烘托。 |
| 底部导航 | `64dp` + 系统导航保底 | 纸白底、三项等宽,图标 `24dp`、标签 `12sp`、每项触控区至少 `48dp`。 |
上述内容总高度在 `360 × 800dp` 首屏内保持紧凑;数据较多时由内容区滚动,而不是拉大首屏留白。`320 × 568` 下题签和快捷入口仍在首屏可见,列表向下滚动。
### 6.3 已否决的 G-01 生成整页图
此前 AI 生成的“我的家谱”整页图**保留为美术锚点**:[G01 我的家谱美术锚点](references/G01-我的家谱-美术锚点.png)。它明确了本项目应该保留的整体效果:朱砂祠堂红头、暖金标题、宣纸纸纹、金线题签、淡山水竹影、纸白底部导航和庄重的色彩层级。
该图**不直接作为尺寸与功能验收稿**,因此不计入 G-01 的“页面设计完成”。需要按 6.2 的固定栅格修正:红头后与题签之间的过大留白、快捷入口与列表之间的过大留白、缺失的左上 Logo、当前家谱与列表的重复、无意义横线占位、以及非标准来源图标。
从此以后,页面设计采用“双锚点”:本图锁定美术氛围和视觉效果;6.2 固定栅格锁定尺寸、内容密度与 Android 小屏可用性。AI 只可生成 Logo、背景等独立视觉资产,不能决定页面间距、文字排版、列表高度或交互层级。
### 6.4 标准来源验收备注
- Material Symbols 官方指南:<https://developers.google.com/fonts/docs/material_symbols>
- Android Navigation Bar 官方指南:<https://developer.android.com/develop/ui/compose/components/navigation-bar>
## 7. 设计状态记录
- P-02:完成。颜色、字体、尺寸、透明 PNG 图标规范已锁定。
- P-03:完成。核心、普通、表单三类页面壳及底部导航、抽屉已锁定。
- P-04:完成。加载、空数据、失败、无权限、审核中与确认操作规则已锁定。
- 下一阶段:D2 账户与首次使用页面;全部页面均按本规范出高保真稿后,才进入 uni-app 还原。
@@ -0,0 +1,106 @@
# G-01:我的家谱设计记录
> 状态:资产准备中
> 美术锚点:[G01 我的家谱美术锚点](references/G01-我的家谱-美术锚点.png)
> 布局锚点:[D1 安卓视觉规范与页面壳](D1_安卓视觉规范与页面壳.md#62-g-01-固定设计栅格360--800dp)
## 页面目标
让已登录用户一眼查看当前家谱、进入四个高频入口、切换已创建 / 已加入家谱,并在无家谱时创建或搜索加入。
## 背景组合
- 红头:`static/assets/backgrounds/header-hall-lineart.png` 叠加朱砂底色。
- 内容纸面:`static/assets/backgrounds/paper-rice-texture.jpg`
- 页面底部:`static/assets/backgrounds/footer-mountain-bamboo.png`,只用于列表尾部与新建按钮附近。
## 图标资产清单
所有项目完成后均为独立、紧凑的 RGBA 透明 PNG;不能从大图裁切后直接使用。
| 区域 | 语义 | 目标文件 | 风格 | 状态 |
| --- | --- | --- | --- | --- |
| 顶部左侧 | 项目 Logo | `brand/jiapu-seal-logo.png` | 用户确认的家祠 Logo 原图 | 已确认使用;待页面小尺寸摆放验收 |
| 顶部右侧 | 通知 | `common/notice-v3.png` | 古金双线 + 轻云纹 | 已生成;`70 × 94px` 主体通过紧凑尺寸检查 |
| 列表入口 | 右箭头 | `common/chevron-right-v2.png` | 古金简洁单线 | 已生成;`96 × 96px`、RGBA 四角透明、48px 检查通过 |
| 快捷入口 | 世系图 | `genealogy/tree-v2.png` | 古金双线简化版 | 已生成;`96 × 96px`、RGBA 四角透明、48px 检查通过 |
| 快捷入口 | 家族成员 | `genealogy/members-v2.png` | 古金双线简化版 | 已生成;`96 × 96px`、RGBA 四角透明、48px 检查通过 |
| 快捷入口 | 字辈诗 | `genealogy/generation-poem-v2.png` | 古金双线简化版 | 已生成;`96 × 96px`、RGBA 四角透明、48px 检查通过 |
| 快捷入口 | 申请审核 | `genealogy/application-v2.png` | 古金双线简化版 | 已生成;`96 × 96px`、RGBA 四角透明、48px 检查通过 |
| 主操作 | 新建家谱 | `action/add-v2.png` | 古金双线 + 轻云纹 | 已生成;`96 × 96px`、RGBA 四角透明、48px 检查通过 |
| 底部 Tab | 家谱 | `tab/genealogy-v4.png``genealogy-active-v4.png` | 简洁谱册 / 世系语义 | 已生成;同轮廓墨褐 / 朱砂成对导出,`96 × 96px`、48px 检查通过 |
| 底部 Tab | 家族 | `tab/family-v4.png``family-active-v4.png` | 简洁双人群组语义 | 已生成;同轮廓墨褐 / 朱砂成对导出,`96 × 96px`、48px 检查通过 |
| 底部 Tab | 我的 | `tab/profile-v4.png``profile-active-v4.png` | 简洁人物档案语义 | 已生成;同轮廓墨褐 / 朱砂成对导出,`96 × 96px`、48px 检查通过 |
## 页面完成条件
- [ ] 图标资产清单全部生成并在 `46rpx` / `24dp` 下逐个检查。
- [ ] 背景组合在 `320 × 568` 下不遮挡标题、按钮和列表。
- [ ] 按固定栅格完成正常态、无家谱态与加载态设计。
- [ ] 不存在横线占位、重复家谱层级或无业务含义的颜色差异。
## Tabbar 重设计规则
- 当前 `static/assets/icons/tab/``static/icons/tab-*.png` 的图标不通过验收,不得使用。
- 三个 Tab 重新采用等视觉重量的简洁符号:家谱为“世系关系 / 谱册”语义,家族为“双人群组”语义,我的为“人物档案”语义。
- 三个图标同为 `24dp` 标准视口、无文字嵌入、无枝叶和云纹等细碎装饰;默认态为墨褐 / 古金描边,选中态为朱砂色的同一图形加粗或填充。
- Logo 是品牌标识,不能拿来代替家谱 Tab 图标;Tab 图标只服务于快速识别目的地。
## 2026-07-11 当日确认记录
- 当前交接视觉稿:[G01-我的家谱-紧凑版设计稿-v5-Tabbar安全区.png](screens/G01-我的家谱-紧凑版设计稿-v5-Tabbar安全区.png)。它是明日施工时的美术与布局锚点,不替代真实页面和资产验收。
- 顶部品牌使用项目原有 `static/assets/icons/brand/jiapu-seal-logo.png`;缩小后固定在左上安全区,避免与居中标题及殿宇线稿争夺视觉焦点。
- 底部 Tabbar 仍然贴合屏幕底部;三个图标和文字整体上移至 Android 手势/三键导航安全区内,标签“家谱 / 家族 / 我的”加大,任何屏幕尺寸下不得被底边裁切。
- 上述确认只锁定 G-01 的方向和版式。真实透明 PNG 图标、背景组合、320 × 568 小屏校验、空状态与加载状态尚未完成,因此 **G-01 仍不勾选为设计完成**
- 明日从 G-01 的可施工项开始:先按 D1 规范落实真实图标与背景资产、再还原页面布局;全量页面设计完成前,不恢复接口联调或 Android 打包工作。
## 2026-07-12 施工记录
- 已完成 G-01 独立图标资产:四个快捷入口、主操作、列表右箭头,以及三组 Tab 默认 / 选中态;全部为 `96 × 96px` RGBA PNG,四角透明,并按实际 `48px` 显示尺寸检查。
- 已完成 G-01 首轮页面还原:朱砂祠堂红头、左上原 Logo、条件式消息圆点、宣纸与山水背景、当前家谱题签、快捷入口、已创建 / 已加入列表、空状态、加载状态和 Android 安全区 Tabbar。
- 本页暂时使用 `data/mock.js` 的展示数据;已移除 G-01 的远程接口调用,接口联调仍按总规划暂停。
- 已通过:`g01-visual-contract.ps1``compile-audit.ps1``uni-scss-injection.ps1``vue3-entry.ps1`
- 待完成:在已打开的 HBuilderX 中按 `360 × 800``320 × 568` 预览并记录无重叠、无底部裁切的证据。完成前不勾选本页“设计完成”条件。
## 2026-07-12 视觉校正记录
- 用户预览反馈首轮还原与确认稿差异过大。已定位为:祠堂线稿直接铺在红头导致视觉过满,且题签 / 列表使用普通圆角边框而不是确认稿的纸签资产。
- 红头已改为纯朱砂底 + 独立 `header-hall-lineart.png` 低对比背景层(不透明度 `0.22`);Logo 放大并上移,标题与通知保持独立层级。
- 已新增并接入 `backgrounds/genealogy-current-slip-frame.png``backgrounds/genealogy-list-slip-frame.png`;两者为透明 PNG 双金线纸签框,透明角检查通过。
- 此次只修正可见还原偏差,不改变 G-01 的数据、路由和状态范围;仍待用户重新运行后的两组小屏预览确认。
## 2026-07-12 尺寸校正记录
- 原始品牌 Logo 的透明留白导致顶部显示偏小,已从同一原图无损裁切为 `brand/jiapu-seal-logo-header.png` 后接入;不改变 Logo 的图形、配色或品牌含义。
- 快捷入口图标由 `52rpx` 调整为 `68rpx`Tab 图标由 `48rpx` 调整为 `56rpx`,标签由 `24rpx` 调整为 `28rpx`,使实际显示尺寸靠近确认稿。
- 校正后已重新通过 G-01 资产/页面契约、编译审计和 SCSS 注入审计;仍需 HBuilderX 截图作最终视觉依据。
## 2026-07-12 第三轮背景与栅格校正
- 红头高度由 `216rpx` 收至 `184rpx`,改用 `backgrounds/header-cinnabar-texture-v2.jpg`;祠堂线稿仅作为 `0.16` 不透明度的独立底纹层。
- 内容左右边距由 `32rpx` 调整为 `48rpx`,当前家谱题签高度收至 `250rpx`,并在快捷入口和列表之间恢复 `backgrounds/genealogy-section-divider-v2.png` 金线云纹分隔。
- 页面底色改用浅宣纸 `backgrounds/paper-rice-texture-v2.jpg`;山水竹影改用透明 `backgrounds/footer-mountain-bamboo-v2.png` 作为整页底部淡层,不再出现矩形背景块;已检查不含绿色像素。
- 标题、分区标题与家谱名称增加宋/楷书本机回退链;题签框统一压为古金 `#B58A4B`
- 已通过:G-01 页面/资产契约、编译审计、SCSS 注入审计及页脚透明区域检查。仍待新截图比对,页面不标记为完成。
## 2026-07-12 第四轮底图与题签边角校正
- 根因核验:`genealogy-current-slip-frame.png``genealogy-list-slip-frame.png` 的四个角均为 Alpha 0;白色直角边并非 PNG 资产本身,而是题签容器在透明镂空角下铺了接近白色的矩形底色。
- 已移除当前家谱题签和两条列表题签的矩形底色,让透明镂空角直接透出宣纸底纹;文字、边框和点击区域不变。
- 已撤回错误的 `footer-mountain-bamboo-v2.png`(该图是连续实心灰山,无法还原设计稿的淡墨层次),恢复使用带亭、远山和竹影的 `footer-mountain-bamboo.png`,以 `0.48` 透明度贴在 Tabbar 上沿,不形成独立灰色矩形带。
- `tests/g01-visual-contract.ps1` 先新增回归断言并确认旧实现失败,再完成最小改动后通过;仍需在 HBuilderX 的同一设备尺寸下由新截图确认最终观感,G-01 不标记为完成。
## 2026-07-12 第五轮信息层级与谱印施工
- 施工依据固定为 `screens/G01-我的家谱-紧凑版设计稿-v5-Tabbar安全区.png`,对应用户箭头指出的谱印、元信息、快捷入口、两条列表和新建按钮;未改接口、路由、页头、背景或 Tabbar。
- 新增并接入透明 PNG`common/location-v1.png``common/member-meta-v1.png``common/admin-v1.png``genealogy/seal-current-frame-v1.png``genealogy/seal-row-frame-v1.png``action/create-cloud-v1.png`。谱印框只承载朱砂和金白边框,谱印固定显示“家谱”,家谱名称和数值仍为动态文本。
- 当前家谱改为地点、成员数、管理员三组图标化元信息;快捷入口显示框提升至 `82rpx`;新建按钮加入两侧云纹 PNG。
- 家谱列表改为谱印框、标题、地点/成员元信息和“更新于”日期的两层结构;`320px` 小屏隐藏更新时间,优先保证标题、元信息、角色和箭头不重叠。
- 已新增资产透明角/有效图形审计。验证通过:`g01-visual-contract.ps1``g01-asset-alpha-audit.ps1``compile-audit.ps1``uni-scss-injection.ps1`。待 HBuilderX 新截图与参考稿并排确认后,G-01 才能标记设计完成。
## 2026-07-12 第六轮参考稿字段与右侧基线校正
- 预览字段已切换为参考稿:当前家谱“汤氏家谱 / 河南·洛阳 / 158 位成员 / 管理员”,已加入家谱“汤氏宗谱 / 山东·济宁 / 286 位成员 / 成员”;不再使用四川、达州、刘氏等旧演示字段。
- 当前题签恢复标题与元信息之间的细金线;短字段确保 430px 验收宽度内三组元信息同一行显示。
- 列表谱印固定为竖排“家谱”;右侧角色与箭头改为同一水平基线;“更新于”保留为文本,移除参考稿中不存在的日历图标和对应闲置资产。
- 新增字段/基线契约先失败后通过。完整验证再次通过:G-01 视觉契约、PNG 透明角审计、编译审计、SCSS 注入审计、Vue 3 入口审计。仍待用户提供 HBuilderX 新截图完成视觉验收。
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

@@ -0,0 +1,62 @@
# 真实接口核心流程 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:** 让家谱 App 的认证、当前家谱上下文、创建首代和世系浏览形成可验证的 Mock/远端双模式核心闭环。
**Architecture:** `utils/config.js` 显式决定数据模式;`utils/api.js` 是唯一远端响应解包和错误转换位置;`utils/session.js` 与新增的上下文模块持久化 token 和当前家谱 ID。页面只调用 `appApi` 并从路由读取对象 ID。
**Tech Stack:** uni-app Vue 3、HBuilderX、uni.request、PowerShell 合同测试。
## Global Constraints
- 不将测试账号、密码或 token 写入源码、测试或文档。
- 远端模式失败必须显示错误,不能自动返回 Mock 数据。
- 所有新增页面必须登记在 `pages.json` 并纳入编译审计。
---
### Task 1: API 会话与响应契约
**Files:**
- Modify: `utils/config.js`, `utils/session.js`, `utils/api.js`
- Create: `utils/genealogy-context.js`, `tests/core-flow-contract.ps1`
- [x] 写失败测试,要求存在显式模式、登录方法、`data` 解包、token 与当前家谱 ID 持久化。
- [x] 运行 PowerShell 合同测试并确认因方法缺失失败。
- [x] 实现最小 API 适配层和上下文模块。
- [x] 重跑合同测试,确认通过。
### Task 2: 认证与我的家谱
**Files:**
- Modify: `pages/auth/login.vue`, `pages/genealogy/index.vue`, `components/GenealogyCard.vue`
- Test: `tests/core-flow-contract.ps1`
- [x] 写失败测试,要求登录页调用 API、保存 token,家谱卡片和列表使用真实 ID。
- [x] 运行测试确认失败。
- [x] 实现密码登录、错误状态与列表加载状态。
- [ ] 重跑测试并在 Mock 模式下手动走通登录至列表。
### Task 3: 当前家谱与首代录入
**Files:**
- Modify: `pages.json`, `pages/genealogy/create.vue`, `pages/genealogy/detail.vue`, `pages/tree/index.vue`, `pages/member/detail.vue`
- Create: `pages/lineage/first-person.vue`
- Test: `tests/core-flow-contract.ps1`
- [x] 写失败测试,要求新页面、路由参数和 API 方法存在,且不得写死 `1001`/`3`
- [x] 运行测试确认失败。
- [x] 实现创建家谱后录入首代、按路由 ID 加载详情/树/成员。
- [x] 重跑合同测试与 HBuilderX Sass/语言诊断。
### Task 4: 加入审核与通知的上下文修复
**Files:**
- Modify: `pages/genealogy/search.vue`, `pages/genealogy/applications.vue`, `pages/notification/index.vue`, `utils/api.js`
- Test: `tests/core-flow-contract.ps1`
- [x] 写失败测试,要求申请和审核使用当前家谱 ID,通知读取操作有对应 API 方法。
- [x] 运行测试确认失败。
- [x] 实现上下文传递与明确的本地状态更新。
- [x] 重跑所有项目测试及 HBuilderX 实际样式/模块请求验证。
@@ -0,0 +1,230 @@
# G01 信息层级与谱印校正 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让“我的家谱”内容区按已确认的 v5 参考稿呈现谱印、元信息、快捷入口、列表和新建按钮的视觉层级。
**Architecture:** 视觉资产只放在 `static/assets/icons/`,页面和卡片组件只引用资产与渲染动态数据。`pages/genealogy/index.vue` 负责当前家谱、快捷入口和新建按钮;`components/GenealogyCard.vue` 负责每条列表的谱印、两层元信息和右侧操作列。
**Tech Stack:** uni-app Vue 3、SCSS、项目本地 RGBA PNG、PowerShell 契约审计。
## Global Constraints
- 视觉基准:`docs/design/screens/G01-我的家谱-紧凑版设计稿-v5-Tabbar安全区.png`
- Android only;继续使用 rpx、flex、小屏 `max-width: 340px` 规则,不使用 `clip-path` 或 CSS 绘图。
- 所有新增可见图标、谱印框、云纹均为项目内透明 PNG;动态姓名/数字使用 Vue 文本,不嵌死在图片中。
- 不改接口、路由、页头、背景、Tabbar 或其他页面;仅为现有展示用 `data/mock.js` 家谱条目补充 `updatedAt` 日期字段。
- 工作目录不是 Git 仓库,本轮不初始化仓库、不创建提交。
---
### Task 1: 谱印与元信息 PNG 资产
**Files:**
- Create: `static/assets/icons/common/location-v1.png`
- Create: `static/assets/icons/common/member-meta-v1.png`
- Create: `static/assets/icons/common/admin-v1.png`
- Create: `static/assets/icons/genealogy/seal-current-frame-v1.png`
- Create: `static/assets/icons/genealogy/seal-row-frame-v1.png`
- Create: `static/assets/icons/action/create-cloud-v1.png`
- Modify: `tests/g01-visual-contract.ps1`
**Interfaces:**
- Produces 96×96 RGBA metadata icons, separate transparent current/list seal-frame PNGs, and a transparent cloud ornament consumed by Tasks 23.
- Each asset corner alpha must be 0; the current seal frame must have taller visible bounds than the row seal frame.
- [x] **Step 1: Write the failing asset contract**
Add the six paths to `$requiredAssets`, then add these assertions to `tests/g01-visual-contract.ps1`:
```powershell
foreach ($asset in @('location-v1.png', 'member-meta-v1.png', 'admin-v1.png', 'seal-current-frame-v1.png', 'seal-row-frame-v1.png', 'create-cloud-v1.png')) {
if ($page -notmatch [regex]::Escape($asset) -and $card -notmatch [regex]::Escape($asset)) {
throw "G-01 does not consume $asset"
}
}
```
- [x] **Step 2: Run the contract and verify it fails**
Run: `powershell -ExecutionPolicy Bypass -File tests\g01-visual-contract.ps1`
Expected: `FAIL` because the asset files and component references do not exist.
- [x] **Step 3: Generate and inspect the assets**
Generate one asset at a time on a flat chroma-key background, remove the chroma key to alpha, and inspect the selected PNG before adding it. Use this visual language: antique-gold single/double outline, Chinese genealogy motif, no text, no shadow, transparent canvas. For the seal frames, generate only the red-and-gold frame; do not include surname or “家谱” text.
- [x] **Step 4: Verify transparent corners**
Run a PowerShell `System.Drawing.Bitmap` alpha audit that checks the four canvas corners of every new PNG are 0.
Expected: all six assets report `PASS ... transparent corners`.
### Task 2: 当前家谱题签、快捷入口与新建按钮
**Files:**
- Modify: `pages/genealogy/index.vue`
- Modify: `tests/g01-visual-contract.ps1`
**Interfaces:**
- Consumes the Task 1 metadata icons, `seal-current-frame-v1.png`, and `create-cloud-v1.png`.
- Keeps `currentGenealogy` as the data owner; does not add API calls.
- [x] **Step 1: Write the failing page-layout contract**
Require the current title block to render three `.current-meta-item` entries and the current seal to include a `.current-seal-frame` image. Require the shortcut display size and create cloud image:
```powershell
foreach ($token in @('current-meta-item', 'current-seal-frame', 'location-v1.png', 'member-meta-v1.png', 'admin-v1.png', 'create-cloud-v1.png')) {
if ($page -notmatch $token) { throw "G-01 current panel is missing $token" }
}
if ($page -notmatch 'width:\s*82rpx') { throw 'G-01 shortcut icons remain below the reference visual size.' }
```
- [x] **Step 2: Run the contract and verify it fails**
Run: `powershell -ExecutionPolicy Bypass -File tests\g01-visual-contract.ps1`
Expected: `FAIL` because current content is still a pure text metadata row and has no seal/cloud image references.
- [x] **Step 3: Implement the minimum current-panel markup**
Replace the pure red `current-seal` block with an image layer and dynamic text overlay:
```vue
<view class="current-seal">
<image class="current-seal-frame" src="/static/assets/icons/genealogy/seal-current-frame-v1.png" mode="scaleToFill" />
<text class="current-seal-title">家谱</text>
</view>
```
Render three icon/text metadata groups using the created `location-v1.png`, `member-meta-v1.png`, and `admin-v1.png`; retain `currentGenealogy.location` and `currentGenealogy.memberCount` as the text values.
- [x] **Step 4: Implement the spacing-only CSS changes**
Set shortcut image box to `82rpx`; increase label size and give the shortcut grid the reference-like vertical space. Use flex wrapping for current metadata and keep the existing `@media screen and (max-width: 340px)` behavior so no item overlaps.
- [x] **Step 5: Implement the create-button ornament**
Place two instances of `create-cloud-v1.png` inside `.create-action`; mirror only the right PNG with `transform: scaleX(-1)`. Keep the existing add PNG and navigation handler.
- [x] **Step 6: Run the contract and verify it passes**
Run: `powershell -ExecutionPolicy Bypass -File tests\g01-visual-contract.ps1`
Expected: `PASS G-01 visual contract`.
### Task 3: 家谱列表谱印与信息层级
**Files:**
- Modify: `components/GenealogyCard.vue`
- Modify: `data/mock.js`
- Modify: `tests/g01-visual-contract.ps1`
**Interfaces:**
- Consumes `genealogy` (`surname`, `name`, `location`, `memberCount`, `updatedAt`), `role`, and the Task 1 row seal asset.
- Emits the unchanged `select` event.
- [x] **Step 1: Write the failing card-layout contract**
Require `seal-row-frame-v1.png` and an explicit `.card-detail-row`:
```powershell
foreach ($token in @('seal-row-frame-v1.png', 'card-detail-row', 'card-updated')) {
if ($card -notmatch $token) { throw "G-01 list card is missing $token" }
}
if ($card -match 'background:\s*#b93b2e') { throw 'G-01 list seal is still a plain CSS red block.' }
```
- [x] **Step 2: Run the contract and verify it fails**
Run: `powershell -ExecutionPolicy Bypass -File tests\g01-visual-contract.ps1`
Expected: `FAIL` because the current list card has no frame asset or two-level detail markup.
- [x] **Step 3: Implement the minimal list-card markup**
Use a `.surname-seal` image frame with text overlay, then split metadata into title/detail/update rows:
```vue
<view class="card-detail-row">
<text class="card-meta">{{ genealogy.location }} · {{ genealogy.memberCount }} 位成员</text>
<view class="card-updated"><text>更新于 {{ genealogy.updatedAt }}</text></view>
</view>
```
On narrow screens, hide `.card-updated`; preserve the role and chevron column.
- [x] **Step 3a: Add the visual fixture dates**
Add `updatedAt: '2024-05-12'` to genealogy `1001` and `updatedAt: '2024-04-28'` to genealogy `1002` in `data/mock.js`. These fields are display-only fixtures and do not change the API contract.
- [x] **Step 4: Implement CSS sizing and small-screen fallback**
Increase card name/title prominence, keep row-frame as the only paper frame, set right-side role/chevron to a centered fixed column, and make `.card-updated` disappear at `max-width: 340px` before text can collide.
- [x] **Step 5: Run the contract and verify it passes**
Run: `powershell -ExecutionPolicy Bypass -File tests\g01-visual-contract.ps1`
Expected: `PASS G-01 visual contract`.
### Task 4: Documentation and full verification
**Files:**
- Modify: `docs/design/G01_我的家谱_设计记录.md`
- Modify: `docs/规划.md`
**Interfaces:**
- Records only completed implementation and verification; leaves G01 unchecked until an HBuilderX comparison screenshot is accepted.
- [x] **Step 1: Record the completed implementation scope**
Append the new asset paths, the explicit visual reference, and the small-screen fallback to the G01 design record. Mark the implementation subtask complete in `docs/规划.md`; do not mark the full G01 design complete.
- [x] **Step 2: Run focused verification**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests\g01-visual-contract.ps1
powershell -ExecutionPolicy Bypass -File tests\compile-audit.ps1
powershell -ExecutionPolicy Bypass -File tests\uni-scss-injection.ps1
```
Expected: three `PASS` lines and no warnings.
- [ ] **Step 3: Capture visual evidence in HBuilderX**
At the same phone viewport as the supplied comparison, capture G01 and compare it side-by-side with the v5 reference. Confirm the current title/meta row, all three seals, four shortcut icon sizes, list row hierarchy, and cloud button ornament. If a mismatch remains, record it rather than marking G01 complete.
## Plan self-review
- Spec coverage: Tasks 13 implement every asset, layout, small-screen, and visual hierarchy requirement; Task 4 records and verifies it.
- No placeholders: all new paths, CSS tokens, component classes, commands, and expected outputs are named.
- Consistency: page uses current panel assets; card uses row seal and metadata assets; both are enforced by one G01 visual contract.
### Task 5: 参考稿字段与列表基线校正
**Files:**
- Modify: `data/mock.js`
- Modify: `pages/genealogy/index.vue`
- Modify: `components/GenealogyCard.vue`
- Modify: `tests/g01-visual-contract.ps1`
**Interfaces:**
- Produces reference-matched visual fixture fields without changing any remote API contract.
- Keeps the existing navigation and card `select` event unchanged.
- [x] **Step 1: Write the failing visual-fixture contract**
Require the four reference names/locations/counts, `.current-info-divider`, a row-direction `.card-side`, and no `calendar-v1.png` use in the list component.
- [x] **Step 2: Run the contract and verify it fails**
Run: `powershell -ExecutionPolicy Bypass -File tests\g01-visual-contract.ps1`
Expected: `FAIL` because the old demonstration fields, missing current divider, vertical role/arrow, and calendar icon are still present.
- [x] **Step 3: Apply the smallest source correction**
Replace only G01 mock fixture fields with the strings in the approved reference. Add the current-panel divider; render fixed vertical “家谱” list seal text; remove the list calendar icon; align `.card-role` and `.card-chevron` horizontally.
- [x] **Step 4: Run focused verification**
Run the G01 visual contract, PNG alpha audit, compile audit, SCSS injection audit, and Vue 3 entry audit. Keep Task 4 visual capture unchecked until the user accepts a new HBuilderX screenshot.
@@ -0,0 +1,221 @@
# G-01 Visual Shell 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:** Rebuild the Android “我的家谱” G-01 page as the approved 家祠卷轴 design with verified local PNG assets and stable mock presentation data.
**Architecture:** pages/genealogy/index.vue owns local visual states only. It composes a heritage header, paper-slip list items, and safe bottom navigation; it does not make remote API calls during this visual-construction phase.
**Tech Stack:** uni-app, Vue 3 script setup, SCSS/rpx, local RGBA PNG/JPG assets, PowerShell checks.
## Global Constraints
- Android only; verify 360 × 800 and 320 × 568 without overlap.
- No API, authentication, packaging, dependency, or unrelated-flow changes.
- Every functional icon is a local RGBA PNG under static/assets/icons/.
- Header uses the confirmed original brand/jiapu-seal-logo.png in the left safe area; title is centered; notification dot renders only for unread messages.
- Tabbar is fixed at the bottom, has 64dp content plus Android navigation inset, and keeps the icon and label clear of the lower edge.
- Do not mark G-01 design complete until assets, normal/empty/loading states, and small-screen checks have evidence.
## File Responsibilities
| File | Responsibility |
| --- | --- |
| static/assets/icons/genealogy/*-v2.png | Four compact antique-gold G-01 shortcut icons. |
| static/assets/icons/action/add-v2.png | The compact 新建家谱 icon. |
| static/assets/icons/common/chevron-right-v2.png | Compact right-arrow asset for the paper-slip list row. |
| static/assets/icons/tab/*-v4.png | Default and active 24dp tab icon pairs. |
| components/PageHeader.vue | Heritage header, original Logo, notification PNG and optional dot. |
| components/AppTabbar.vue | Fixed Android-safe three-tab navigation. |
| components/GenealogyCard.vue | Compact paper-slip genealogy list row. |
| pages/genealogy/index.vue | G-01 content, local state and route actions. |
| tests/g01-visual-contract.ps1 | Asset and static page-contract checks. |
## Task 1: Produce and register G-01 visual assets
**Files:**
- Create: static/assets/icons/genealogy/tree-v2.png
- Create: static/assets/icons/genealogy/members-v2.png
- Create: static/assets/icons/genealogy/generation-poem-v2.png
- Create: static/assets/icons/genealogy/application-v2.png
- Create: static/assets/icons/action/add-v2.png
- Create: static/assets/icons/common/chevron-right-v2.png
- Create: static/assets/icons/tab/{genealogy,family,profile}-v4.png
- Create: static/assets/icons/tab/{genealogy,family,profile}-active-v4.png
- Create: tests/g01-visual-contract.ps1
- Modify: docs/design/G01_我的家谱_设计记录.md
**Interfaces:**
- Consumes: D1 icon rules and the G-01 asset table.
- Produces: the listed asset paths; no legacy static/icons/tab-*.png consumer remains.
- [x] **Step 1: Write the failing asset contract**
$root = Split-Path -Parent $PSScriptRoot
$requiredAssets = @(
'static/assets/icons/brand/jiapu-seal-logo.png',
'static/assets/icons/common/notice-v3.png',
'static/assets/icons/genealogy/tree-v2.png',
'static/assets/icons/genealogy/members-v2.png',
'static/assets/icons/genealogy/generation-poem-v2.png',
'static/assets/icons/genealogy/application-v2.png',
'static/assets/icons/action/add-v2.png',
'static/assets/icons/common/chevron-right-v2.png',
'static/assets/icons/tab/genealogy-v4.png',
'static/assets/icons/tab/genealogy-active-v4.png',
'static/assets/icons/tab/family-v4.png',
'static/assets/icons/tab/family-active-v4.png',
'static/assets/icons/tab/profile-v4.png',
'static/assets/icons/tab/profile-active-v4.png'
)
foreach ($asset in $requiredAssets) {
if (-not (Test-Path -LiteralPath (Join-Path $root $asset))) {
throw "Missing G-01 visual asset: $asset"
}
}
Write-Output 'PASS G-01 visual contract'
- [x] **Step 2: Run the contract and confirm it fails**
Run: powershell -ExecutionPolicy Bypass -File C:\Users\Rain\Desktop\job\jiapuapp\tests\g01-visual-contract.ps1
Expected: an error that names tree-v2.png as missing.
- [x] **Step 3: Generate and validate every icon**
Generate each asset separately as a tight 96 × 96px transparent PNG. Shortcut and add icons use antique-gold #B58A4B with the approved restrained double-line style. Tab pairs use the same 24dp silhouette in default ink #342A24 and selected cinnabar #A33B2B; no text, clouds, leaves, shadows, or excess transparent padding.
- [x] **Step 4: Inspect and register accepted assets**
Inspect Tabs at 46rpx and shortcuts at 48rpx. Reject opaque corners, excess canvas padding, unrecognizable semantics, and any default/selected silhouette mismatch. Update the G-01 asset table with actual path, 96 × 96px, transparency result, and display-size result. Do not tick page-completion boxes yet.
- [x] **Step 5: Re-run the asset contract**
Expected: PASS G-01 visual contract.
## Task 2: Rebuild header and Android-safe Tabbar
**Files:**
- Modify: components/PageHeader.vue
- Modify: components/AppTabbar.vue
- Modify: tests/g01-visual-contract.ps1
**Interfaces:**
- Produces: PageHeader title=我的家谱 with unreadCount, and AppTabbar active=genealogy.
- [x] **Step 1: Extend the failing static contract**
$header = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'components/PageHeader.vue')
$tabbar = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'components/AppTabbar.vue')
if ($header -notmatch 'jiapu-seal-logo\.png') { throw 'Header does not use the approved brand PNG.' }
if ($header -notmatch 'notice-v3\.png') { throw 'Header does not use the approved notice PNG.' }
if ($header -notmatch 'unreadCount\s*>\s*0') { throw 'Header badge is not conditional on unread count.' }
if ($tabbar -match '/static/icons/tab-') { throw 'Tabbar still references rejected legacy tab assets.' }
if ($tabbar -notmatch 'safe-area-inset-bottom') { throw 'Tabbar has no Android safe-area padding.' }
if ($tabbar -notmatch 'font-size:\s*24rpx') { throw 'Tabbar label size is below the approved G-01 size.' }
- [x] **Step 2: Confirm the contract fails before components change**
Run the focused contract.
Expected: failure describing the legacy header or Tabbar implementation.
- [x] **Step 3: Implement the header**
Use image-only controls: an 88rpx Logo touch area, centered title, and equal-width 88rpx notice touch area using notice-v3.png. Add v-if unreadCount > 0 for the cinnabar dot. Use the header line-art PNG over a cinnabar base. Do not render text labels for Logo or notification action.
- [x] **Step 4: Implement the Tabbar**
Map every item to its v4 default/active PNG paths and choose by active === item.key. Keep a paper-white fixed bar, visible content height 128rpx, padding-bottom env(safe-area-inset-bottom), icon 48rpx, label 24rpx, and each touch target at least 96rpx. Do not use gap.
- [x] **Step 5: Re-run the contract**
Expected: PASS G-01 visual contract.
## Task 3: Rebuild G-01 content and presentation states
**Files:**
- Modify: components/GenealogyCard.vue
- Modify: pages/genealogy/index.vue
- Modify: tests/g01-visual-contract.ps1
**Interfaces:**
- Consumes: genealogies and notifications from @/data/mock.js.
- Produces: isLoading, hasGenealogies, createdGenealogies, and joinedGenealogies visual branches.
- [x] **Step 1: Add the failing local-state contract**
$page = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/genealogy/index.vue')
if ($page -match "from '@/utils/api\.js'") { throw 'G-01 visual phase must not call the remote API.' }
foreach ($state in @('isLoading', 'hasGenealogies', 'createdGenealogies', 'joinedGenealogies')) {
if ($page -notmatch $state) { throw "G-01 is missing presentation state: $state" }
}
foreach ($asset in @('tree-v2.png', 'members-v2.png', 'generation-poem-v2.png', 'application-v2.png', 'add-v2.png')) {
if ($page -notmatch [regex]::Escape($asset)) { throw "G-01 does not consume $asset" }
}
- [x] **Step 2: Confirm the contract fails on the legacy page**
Expected: failure for the API import or missing visual state.
- [x] **Step 3: Implement the page state owner**
Remove appApi, genealogyContext, and the mounted API call. Import genealogies and notifications from @/data/mock.js, then define:
const isLoading = ref(false)
const list = ref(genealogies)
const hasGenealogies = computed(() => list.value.length > 0)
const createdGenealogies = computed(() => list.value.filter((item, index) => index === 0))
const joinedGenealogies = computed(() => list.value.filter((item, index) => index > 0))
Retain only route navigation: tree /pages/tree/index, members /pages/genealogy/detail, generation poem /pages/content/list?type=poem, applications /pages/genealogy/applications, create /pages/genealogy/create, search /pages/genealogy/search, and notifications /pages/notification/index.
- [x] **Step 4: Implement approved compact layout**
Compose heritage header, current-genealogy paper slip, four equal shortcut entries with v2 assets, separate 我创建的 / 我加入的 groups, compact paper-slip rows, and the add button. Implement a distinct loading state and an empty paper/scroll state with 新建家谱 and 搜索并申请加入. Apply paper texture to the page and mountain/bamboo only to the lower content region. Add bottom content padding that clears the fixed Tabbar.
- [x] **Step 5: Simplify GenealogyCard.vue**
Use a seal-style surname block, title, short location/member metadata, identity label, and common/chevron-right-v2.png. Remove the legacy navy cover, motto, and active-member count. Ensure long copy uses min-width: 0, overflow: hidden, and text-overflow: ellipsis.
- [x] **Step 6: Re-run the focused contract**
Expected: PASS G-01 visual contract.
## Task 4: Verify and update the project record
**Files:**
- Modify: docs/design/G01_我的家谱_设计记录.md
- Modify: docs/规划.md
- Test: tests/g01-visual-contract.ps1
- Test: tests/compile-audit.ps1
- Test: tests/uni-scss-injection.ps1
- [x] **Step 1: Run the automated checks**
Run:
powershell -ExecutionPolicy Bypass -File C:\Users\Rain\Desktop\job\jiapuapp\tests\g01-visual-contract.ps1
powershell -ExecutionPolicy Bypass -File C:\Users\Rain\Desktop\job\jiapuapp\tests\compile-audit.ps1
powershell -ExecutionPolicy Bypass -File C:\Users\Rain\Desktop\job\jiapuapp\tests\uni-scss-injection.ps1
Expected: each command prints its PASS result.
- [ ] **Step 2: Verify at Android reference sizes**
Inspect 360 × 800 and 320 × 568. Confirm no overlap among Logo, title, and bell; notice dot only appears with unread entries; shortcut labels remain one line; list text truncates instead of overlaps; create action clears the Tabbar; all three Tab labels remain fully visible above system navigation.
- [ ] **Step 3: Update only verified design state**
In the G-01 record, tick only conditions backed by Tasks 12 and the two viewport checks. In the overall planning document, leave G-01 unchecked if even one state, asset, or viewport condition remains incomplete; otherwise record visual shell implemented and verified.
- [ ] **Step 4: Commit only when Git becomes available**
If a Git worktree exists, commit changed visual-shell files with message: feat: build G01 genealogy visual shell. If Git remains unavailable, report that no commit was created and do not initialize a repository.
## Plan Self-Review
- Task 1 covers local assets and transparency checks.
- Task 2 covers the Logo, conditional notification dot, and safe Tabbar.
- Task 3 covers G-01 normal, loading, and empty states plus approved compact layout.
- Task 4 requires automated and small-screen verification before any completion status changes.
@@ -0,0 +1,29 @@
# 真实接口核心流程设计
## 目标
把现有静态界面升级为可在 Mock 和远端接口之间明确切换的核心流程:登录后进入我的家谱,按当前家谱 ID 查看详情、世系树和成员,并完成创建家谱后录入首代人物。
## 方案选择
- 采用显式 `mode: 'mock' | 'remote'` 配置;远端请求失败时显示错误,不自动切回 Mock。
- 认证支持密码登录和短信登录。密码登录用于已有测试账号;短信入口按 OpenAPI 请求验证码和登录。
- API 适配层统一解包 OpenAPI 的 `{ code, msg, data }` 响应,统一保存/清除 token,并在 401/403 时提示重新登录。
- 当前家谱 ID 以页面参数为主、持久化上下文为辅。详情、树、成员、审核、家族动态不得再写死 `1001`
## 范围
本阶段完成认证、会话、家谱上下文、创建家谱、录入首代、详情、世系树、成员档案的真实数据链路。
本阶段不实现谱文、相册、祭祀、VIP、帮助、反馈等内容模块;这些入口继续明确提示“后续开放”。
## 验收
1. Mock 模式下可完成:登录、创建家谱、录入首代、进入对应世系树、打开对应成员档案。
2. Remote 模式下所有请求携带 `clientid` 与已保存 token,并按接口响应的 `data` 字段消费。
3. 远端登录/接口失败时不会跳转成功、不会静默返回 Mock 数据,并能给出可理解提示。
4. 页面跳转不再依赖硬编码家谱 ID 或成员 ID。
## 已知外部限制
当前远端密码登录探测返回 HTTP 500,前端会保留远端模式和错误提示,但无法在服务端恢复前完成真实账号的端到端验收。
@@ -0,0 +1,62 @@
# G01 参考稿信息层级校正设计
## 已确认的目标
-`docs/design/screens/G01-我的家谱-紧凑版设计稿-v5-Tabbar安全区.png` 为唯一视觉基准;不再以当前浏览器页为准缩放设计。
- 本轮只校正用户箭头指出的内容区:当前家谱题签的信息层级、四个快捷入口、分隔线之后的两条列表和“新建家谱”按钮。
- 保留已确认的朱砂页头、宣纸底纹、淡墨山水和底部 Tabbar;不接入接口、不改路由;仅为现有展示用 `data/mock.js` 家谱条目补充参考稿需要的 `updatedAt` 日期字段。
- 预览阶段的展示字段必须与参考稿一致:当前家谱为“汤氏家谱 / 河南·洛阳 / 158 位成员 / 管理员”,已加入家谱为“汤氏宗谱 / 山东·济宁 / 286 位成员 / 成员”。视觉验收期间不得保留四川、达州、刘氏等旧演示字段。
## 方案比较与选择
1. 仅增大现有 CSS 字号和图标容器:改动少,但仍缺地点/成员/管理员图标和更新时间层级,不能还原参考稿。
2. 同时压缩所有内容区:会牺牲旧安卓小屏可读性,也无法解决信息结构错误。
3. **按参考稿重建四个内容区的信息栅格**:补齐独立 PNG 元信息图标,放大可见图标和标题字号,使用两行列表元信息与右侧角色列。选此方案。
## 页面规格
### 当前家谱题签
- 保持纸签框资产和透明四角;名称为最高层级,使用楷体回退链。
- 左侧“家谱”不是普通红色圆角块:改为独立的竖式朱砂谱印框 PNG,包含参考稿的双层金白描边与红印底;谱印固定显示“家谱”,家谱名称和数值仍由页面数据渲染,不嵌死在图片中。
- 当前家谱谱印比列表谱印更高;两者分别使用对应尺寸的 PNG 框,保持参考稿的层级,不能共用一个拉伸框。
- 元信息改为三组:地点、成员数、管理员;每组由 24dp 的透明 PNG 图标与文字构成,不能再只用文字串联。
- 三组在可用宽度内均匀排列;320px 小屏可换行但不重叠。
- 名称与三组元信息之间保留参考稿中的细金线分隔;在 430px 验收宽度三组必须同一行。
### 快捷入口与分隔线
- 快捷图标的显示框放大,使实际图形接近参考稿的视觉面积;标签同步提升,四项保持等宽。
- 快捷区与金线云纹分隔线保留明确留白,分区标题不得贴线。
### 家谱列表
- 每条维持纸签框和透明四角;标题提升为主要信息。
- 左侧使用较小的竖式朱砂谱印框 PNG;已加入家谱可保持低饱和底色,但边框、比例与“我创建的”谱印一致。
- 次行固定为“地点 + 成员数”;右侧固定为“角色 + 箭头”,并在宽屏下补充“更新于日期”。
- 小屏降级时优先保留标题、地点、成员数和角色;更新时间可隐藏,禁止文字压到箭头。
- 角色文字与右箭头使用同一水平基线;日期是文本信息,不增加参考稿中没有的日历图标。
- 列表谱印固定显示竖排“家谱”,不显示动态姓氏;已加入列表使用低饱和谱印状态。
### 新建按钮
- 保留独立 PNG 加号;放大文字和图标实际显示尺寸。
- 补齐参考稿的左右云纹装饰 PNG,不用 CSS 或文字模拟装饰。
## 资产与兼容性
- 新增地点、成员、管理员、更新时间和按钮云纹均为项目内 RGBA PNG;图标画布为 96px,并检查四角透明。
- 不用 `clip-path`、蒙版或其他旧 Android WebView 风险较高的裁切方案。
- 所有布局继续使用 rpx、弹性布局、单行省略和 `max-width: 340px` 小屏规则。
## 验收与验证
1. 自动契约先失败:要求新元信息图标、两行列表结构、放大的快捷图标和新建按钮云纹存在;旧的纯文字元信息实现必须不再通过。
2. 完成最小代码与 PNG 资产改动后,运行 G01 契约、编译审计、SCSS 注入审计和 PNG 透明角检查。
3. 使用 HBuilderX 在同一设备尺寸重新截图,与参考稿并排比对:重点确认箭头所指的字号、间距、图标面积、列表行高与按钮装饰。
## 自检
- 无占位项、无接口或路由扩展。
- 唯一视觉来源是已确认的 G01 v5 参考稿;本轮没有重设颜色、背景或页面功能。
- 若 320px 宽度装不下,按本文件定义隐藏更新时间,而不是压缩全部文字。
+105
View File
@@ -0,0 +1,105 @@
# 家谱 APP 交接记录
> 更新时间:2026-07-13
> 用途:更换电脑或更换 GPT 后,先读本文件即可恢复当前工作上下文。
> 保密说明:账号、密码、令牌和其他敏感信息不记录在本文件。
## 1. 新接手者先做什么
按以下顺序阅读,不要跳过:
1. 根目录 `AGENTS.md`:项目工作约束,要求先确认依据、最小修改、修改后验证。
2. `docs/规划.md`:全项目范围、完成状态与推进顺序。
3. `docs/design/D1_安卓视觉规范与页面壳.md`Android 视觉、兼容性、PNG 资产与背景规则。
4. `docs/design/G01_我的家谱_设计记录.md`G01 已完成的施工与尚未验收项。
5. `docs/superpowers/specs/2026-07-12-g01-reference-typography-design.md``docs/superpowers/plans/2026-07-12-g01-information-hierarchy.md`G01 的已确认规格与执行记录。
视觉唯一基准:
- `docs/design/screens/G01-我的家谱-紧凑版设计稿-v5-Tabbar安全区.png`
不要把“红色中国风”当作完成标准;G01 需要按此图逐区、逐字段、逐比例比对。
## 2. 项目与范围
- 项目:uni-app,目标为 Android,使用 HBuilderX 打包。
- 当前阶段:先完成全部页面视觉设计和小屏验收;**暂不恢复接口联调、真实账号联调或 Android 打包**。
- 当前正在处理:`G-01 我的家谱`
- 当前不做:iOS、PC 管理端、官网,以及新增业务接口。
- 本地展示数据的唯一来源:`data/mock.js`G01 当前不调用远程 API。
- 本目录当前不是 Git 仓库;不要擅自初始化仓库、重置文件或删除既有用户文件。
## 3. 用户已确认的视觉规则
- 风格是“家祠卷轴”:朱砂页头、暖宣纸、古金细线、墨褐文字、淡墨山水/竹影;禁止绿色页面背景。
- 需要使用的图标、谱印框与云纹必须是项目内的透明 PNG;不能用文字、emoji、CSS 图形或网络图片替代可见图标。
- 参照图中的布局、字号、字段、图标可见面积和留白需要尽量一比一还原;不要只做“差不多”的国风页面。
- 要兼容老款 Android:用 rpx、Flex、边距与基础圆角;避免 CSS Grid、`flex gap``clip-path`、复杂滤镜和大面积动画。
- 每次完成一个可验收子项,更新 `docs/规划.md` 和对应设计记录;没有 HBuilderX 同尺寸截图确认时,不能勾选整个页面完成。
## 4. G01 当前实际状态
### 已施工并通过静态检查
- 朱砂顶部、项目 Logo、条件式通知红点、宣纸底纹、淡墨底图、底部 Tabbar。
- 当前家谱题签、四个快捷入口、分隔金线、创建/加入列表、新建按钮。
- 透明 PNG 资产已接入:快捷入口、Tab、通知、箭头、元信息图标、谱印框和新建云纹。
- 当前演示字段已统一为参考稿:
- 当前家谱:`汤氏家谱 / 河南·洛阳 / 158 位成员 / 管理员`
- 已加入家谱:`汤氏宗谱 / 山东·济宁 / 286 位成员 / 成员`
- 当前题签有标题/元信息分隔线;列表右侧角色和箭头应为同一水平基线;日期为“更新于”文字,不显示参考稿中没有的日历图标。
### 必须继续做的事
1. 用 HBuilderX 刷新 G01,并使用与参考稿相同的手机预览尺寸截屏。
2. 将新截图与 v5 参考稿并排比对,重点检查:
- 顶部家谱题签的字体、字段、分隔线和三组元信息是否同一行;
- 四个快捷图标的实际可见面积与文字大小;
- 两条列表的谱印、标题、地点/成员、日期、右侧角色和箭头基线;
- 新建按钮的云纹、文字、底部山水和 Tabbar 安全区。
3. 有任何差异时,只改对应 G01 组件并重新截图;不要提前勾选 `G-01`
4. 最终还要按 `320 × 568``360 × 800` 做小屏检查,确认无重叠、无裁切、无底部遮挡。
## 5. G01 关键文件
| 作用 | 文件 |
| --- | --- |
| G01 页面与当前题签/快捷入口/新建按钮 | `pages/genealogy/index.vue` |
| G01 列表项 | `components/GenealogyCard.vue` |
| 顶部标题栏/Logo/通知点 | `components/PageHeader.vue` |
| 底部导航 | `components/AppTabbar.vue` |
| 展示数据 | `data/mock.js` |
| 视觉变量 | `styles/tokens.scss``styles/global.scss` |
| G01 图标 | `static/assets/icons/` |
| G01 背景 | `static/assets/backgrounds/` |
| G01 契约检查 | `tests/g01-visual-contract.ps1` |
| 新 PNG 透明检查 | `tests/g01-asset-alpha-audit.ps1` |
## 6. 已知技术注意项
- `tests/g01-visual-contract.ps1` 用 UTF-8 Base64 形式检查中文展示字段。这是为了避免 Windows PowerShell 5.1 将无 BOM UTF-8 脚本中的中文字符串误读为乱码;不要把该段直接改回裸中文字面量。
- `calendar-v1.png` 已因为参考稿没有该图标而删除;不要再次接入日期图标。
- G01 的背景资产应继续使用 `footer-mountain-bamboo.png`,不要换回实心灰山的 `footer-mountain-bamboo-v2.png`
- 题签透明角已经过检查;若再次出现白角,优先检查题签容器是否重新加上不透明的矩形底色。
- 所有新 PNG 在接入前都要检查:RGBA、四角透明、主体没有大量空白、显示尺寸符合 24dp/设计稿。
## 7. 复核命令
在项目根目录的 PowerShell 中运行:
```powershell
powershell -ExecutionPolicy Bypass -File tests\g01-visual-contract.ps1
powershell -ExecutionPolicy Bypass -File tests\g01-asset-alpha-audit.ps1
powershell -ExecutionPolicy Bypass -File tests\compile-audit.ps1
powershell -ExecutionPolicy Bypass -File tests\uni-scss-injection.ps1
powershell -ExecutionPolicy Bypass -File tests\vue3-entry.ps1
```
最近一次结果:五项均为 `PASS`。这只证明代码和资产契约通过,**不替代 HBuilderX 视觉验收**。
## 8. 换电脑操作
1. 完整复制整个 `jiapuapp` 项目文件夹,保留 `docs/``static/``tests/``data/` 和所有 Vue/SCSS 文件。
2. 新电脑安装 HBuilderX 后,直接“打开目录”选择复制后的项目根目录。
3. 将本文件和新的 HBuilderX 截图一起发给新的 GPT,并说明:“先读 `docs/交接记录.md``AGENTS.md`,继续 G01 的视觉验收,不要改接口。”
4. 新 GPT 修改后,先运行第 7 节命令,再由用户在 HBuilderX 提供同尺寸截图复核。
+207
View File
@@ -0,0 +1,207 @@
# 规划
> 电脑或 GPT 更换时,先阅读:[项目交接记录](交接记录.md)。
> 状态:执行中
> 建立日期:2026-07-11
> 唯一适用端:Android `uni-app`HBuilderX 打包)
> 执行原则:**先完成全部页面设计,再开始逐页实现;每完成一项,立即在本文件勾选。**
## 1. 本轮边界
- 只做手机 Android 用户端;不做 iOS、H5、PC 管理端、后台管理端、官网。
- 范围覆盖思维导图中的 Android 用户端:登录、家谱、家族、消息、我的,以及必要的空白、加载、异常、权限状态。
- 现有 uni-app 页面与接口代码保留为临时功能骨架,但**不计入任何页面设计完成项**,也不再决定最终视觉样式。
- 在“设计验收”完成前,暂停新增业务接口、真实账号联调和打包工作。
- 接口暂未提供的视频、邀请码、多管理员分级等功能:本轮可完成其页面视觉和无数据状态;真实功能接入留到接口阶段。
## 2. 视觉方向(已确定)
### 2.1 风格名称
**家祠卷轴**:喜庆而庄重。以宣纸的温度承载家族记忆,以朱砂表达礼序和重要操作,以古金表达传承;避免网红化、过度卡片化和大面积高饱和红色。
### 2.2 颜色与材质
| 用途 | 色彩 | 规则 |
| --- | --- | --- |
| 页面底色 | 宣纸暖白 `#F6F0E5` | 不使用绿色背景。 |
| 主操作 / 选中 | 朱砂 `#A33B2B` | 仅用于主按钮、当前状态、关键提醒。 |
| 标题 / 主要文字 | 墨褐 `#342A24` | 不使用偏绿的深色。 |
| 细分隔 / 装饰 | 古金 `#B58A4B` | 只做细线、印记、等级,不做大色块。 |
| 容器 | 纸白 `#FFF9EE` | 用轻边线或极浅阴影分层。 |
| 次级文字 | 暖灰褐 `#7A6958` | 保持正文可读性。 |
### 2.3 固定组件规则
- 顶部为简洁的家祠题签式标题栏;可回退页面保留清晰返回区,不能让装饰挤压标题。
- 底部导航固定为“家谱 / 家族 / 我的”,通知作为右上角入口,不单独占 Tab。
- 家谱树中的人物使用竖式人物牌位;姓名、字辈、关系与在世状态层次清楚,不能用装饰遮挡信息。
- 所有功能图标均使用透明背景 PNG,统一由 `static/icons/` 管理;不用网络图标字体,不混用不同画风。
- 图片内容优先使用用户上传的真实照片;没有照片时使用统一的纸纹占位图,不使用 AI 人像。
## 3. Android 老机型与小屏兼容(必须验收)
### 3.1 尺寸与排版
-`320 × 568``360 × 640` 为关键小屏;`360 × 800``412 × 915` 为常规屏进行核对。
- 布局使用流式纵向滚动;不以单一固定页面高度承载内容,不让底部按钮压住表单。
- 一行中可能出现长姓名、长家谱名、长地址时必须截断或换行,不允许文字重叠。
- 关键正文不小于 `24rpx`,主要按钮高度不小于 `88rpx`,所有可点击区域不小于约 `44dp`
- 双列内容在 `320px` 宽度下自动折为单列;人物树、照片墙等横向内容提供明确的横向滚动区域与边缘提示。
- 顶部、底部均预留系统状态栏和 Android 三键/手势导航空间;无安全区能力的设备也要有保底内边距。
### 3.2 性能与技术限制
- 不使用全屏大尺寸背景图、视频背景、复杂滤镜、毛玻璃和持续动画。
- 不依赖 `flex gap`、复杂 CSS Grid 或只在新 WebView 支持的视觉效果;以 Flex、边距和基础圆角为主。
- 首屏图标为小尺寸透明 PNG;列表图片按缩略图加载,避免一次性渲染大量高清图。
- 家谱树按层级或视区呈现,不在低端机一次渲染无边界的全部成员节点。
- 表单、弹窗、图片预览都要可关闭、可返回,并在键盘弹起时保持提交按钮可见。
### 3.3 兼容验收方式
- 每个页面设计完成后,先按 `320 × 568` 检查:标题、长文案、表单、底部操作、横向内容五项均不得重叠。
- 全部页面实现后,以 Android 真机或模拟器覆盖上述四组尺寸,记录在本文件“最终验收”中。
- 这里的“兼容”指页面尺寸、性能和交互可用性;具体最低 Android 系统版本在最终 HBuilderX 打包配置阶段按实际 SDK 再确认。
## 4. 页面设计的完成标准
一页只有同时满足以下四项,才能标记为“设计完成”:
1. 有明确的正常态布局、字体层级、颜色、图标、间距与操作层级。
2. 有该页需要的空数据、加载、失败、无权限或审核中状态。
3. 标明每个主要点击入口的去向、返回路径和确认操作。
4. 已按 `320 × 568` 小屏规则检查,无重叠、无被底部导航遮挡的内容。
## 5. 全量页面清单与完成状态
### 5.1 设计基础与通用状态
- [x] P-00 锁定 Android 端范围、家祠卷轴视觉方向与透明 PNG 图标规则。
- [x] P-01 锁定老机型 / 小屏适配原则与验收尺寸。
- [x] P-02 设计系统:颜色、字号、间距、圆角、按钮、标签、列表、表单、弹窗、空状态。见 [D1 安卓视觉规范与页面壳](design/D1_安卓视觉规范与页面壳.md)。
- [x] P-03 公共页面壳:顶部题签、返回栏、三 Tab、右上角通知入口、安全区与底部操作栏。见 [D1 安卓视觉规范与页面壳](design/D1_安卓视觉规范与页面壳.md)。
- [x] P-04 通用状态:首次加载、列表加载、网络失败、空数据、无权限、审核中、删除确认、操作成功提示。见 [D1 安卓视觉规范与页面壳](design/D1_安卓视觉规范与页面壳.md)。
- [ ] P-05 生成并验收项目 Logo、导航、通知与公共操作的透明 PNG 图标资产。见 [D1 安卓视觉规范与页面壳](design/D1_安卓视觉规范与页面壳.md)。
- [ ] P-06 生成并验收祠堂红头、宣纸纸纹、山水 / 竹影等项目内背景资产。见 [D1 安卓视觉规范与页面壳](design/D1_安卓视觉规范与页面壳.md)。
### 5.2 账户与首次使用
- [ ] A-01 启动 / 登录引导页。
- [ ] A-02 账号密码登录页。
- [ ] A-03 手机验证码登录页。
- [ ] A-04 注册页与服务协议确认。
- [ ] A-05 忘记密码 / 重设密码页。
- [ ] A-06 账号受限、登录失败与未完成注册状态。
### 5.3 家谱:创建、加入与管理入口
- [x] G-01 信息层级与谱印施工:元信息图标、谱印框、快捷入口、列表双层信息和新建云纹已实现并通过资产/编译审计;待 HBuilderX 同尺寸截图验收,故完整 G-01 仍未勾选。
- [ ] G-01 我的家谱列表(已创建、已加入、搜索入口、通知入口)。
- [ ] G-02 无家谱空状态(创建家谱 / 搜索加入)。
- [ ] G-03 创建家谱表单(名称、地域、简介、公开范围)。
- [ ] G-04 创建成功后的始祖录入页。
- [ ] G-05 家谱总览(名称、编号、地域、成员数、管理员、公开状态、快捷入口)。
- [ ] G-06 公开家谱搜索与筛选页。
- [ ] G-07 家谱搜索结果与家谱简介页。
- [ ] G-08 申请加入表单、提交成功与审核中页。
- [ ] G-09 我的申请记录页。
- [ ] G-10 管理员申请审核列表与审核确认页。
- [ ] G-11 家谱设置、公开范围与成员访问说明页。
- [ ] G-12 字辈诗列表、详情与管理员维护页。
### 5.4 世系树、成员与亲属关系
- [ ] T-01 世系树总览(缩放、横向移动、分支切换、当前人物定位)。
- [ ] T-02 世系树小屏提示与无成员 / 数据加载失败状态。
- [ ] T-03 成员档案详情(基本资料、代际、关系、照片、人生记录入口)。
- [ ] T-04 新增直系亲属页(父母、配偶、子女的不同上下文)。
- [ ] T-05 编辑成员资料页。
- [ ] T-06 编辑亲属关系页与关系冲突提示。
- [ ] T-07 成员目录与搜索页。
- [ ] T-08 成员去世、隐私隐藏、无编辑权限等特殊状态。
### 5.5 家族:动态、谱文、相册与影像
- [ ] F-01 家族首页 / 家族动态流。
- [ ] F-02 发布动态页(文字、图片、可见范围)。
- [ ] F-03 动态详情、评论、点赞与删除确认页。
- [ ] F-04 谱文分类与文章列表页。
- [ ] F-05 谱文详情页。
- [ ] F-06 新建 / 编辑谱文页。
- [ ] F-07 相册列表页。
- [ ] F-08 相册详情与照片墙页。
- [ ] F-09 图片预览、上传照片与上传失败状态。
- [ ] F-10 视频列表、详情、发布入口与“接口尚未开放”状态。
### 5.6 家族:人物、礼仪与档案
- [ ] R-01 人物录列表页。
- [ ] R-02 人物录详情与新增 / 编辑页。
- [ ] R-03 贺礼列表页。
- [ ] R-04 新增贺礼、贺礼详情与删除确认页。
- [ ] R-05 礼仪活动列表(婚礼、升学、生日、其他)。
- [ ] R-06 礼仪详情页。
- [ ] R-07 新建 / 编辑礼仪活动页。
- [ ] R-08 成长日志列表、详情与新增 / 编辑页。
- [ ] R-09 人生事列表、详情与新增 / 编辑页。
- [ ] R-10 备忘录列表、详情与新增 / 编辑页。
- [ ] R-11 功德记录列表、详情与新增 / 编辑页。
### 5.7 消息与个人中心
- [ ] N-01 消息中心(生日、贺礼、礼仪、应用通知、点赞评论、推广反馈分类)。
- [ ] N-02 消息详情、已读状态与空消息状态。
- [ ] M-01 我的首页(头像、身份、所属家谱、快捷入口)。
- [ ] M-02 个人资料查看 / 编辑页。
- [ ] M-03 安全设置页。
- [ ] M-04 修改密码页。
- [ ] M-05 修改手机号页。
- [ ] M-06 帮助中心与搜索页。
- [ ] M-07 意见反馈页(文字、图片、联系方式)。
- [ ] M-08 应用推广页。
- [ ] M-09 VIP 权益、套餐与订单状态页。
- [ ] M-10 关于我们、服务协议、隐私政策与退出登录确认页。
## 6. 设计推进顺序(开发前必须全部完成)
| 阶段 | 交付内容 | 状态 |
| --- | --- | --- |
| D1 | P-02 至 P-06:统一设计系统、Android 页面壳、公共图标与背景资产 | 进行中(P-02 至 P-04 已完成;P-05、P-06 资产生成中) |
| D2 | A-01 至 A-06:账户与首次使用全套页面 | 未开始 |
| D3 | G-01 至 G-12:家谱创建、加入、总览与管理全套页面 | 进行中(G-01 视觉壳已施工,待 HBuilderX 小屏预览验收;其余页面未开始) |
| D4 | T-01 至 T-08:世系树、成员、关系编辑全套页面 | 未开始 |
| D5 | F-01 至 F-10、R-01 至 R-11:家族内容全套页面 | 未开始 |
| D6 | N-01 至 N-02、M-01 至 M-10:消息与个人中心全套页面 | 未开始 |
| D7 | 全量小屏复核、视觉一致性复核、页面流程复核 | 未开始 |
| D8 | 逐页实现 uni-app、接入接口、Android 打包验证 | 未开始(D7 完成后才可开始) |
## 7. 页面流转主线(设计时必须保持一致)
1. 登录 → 我的家谱为空 → 创建家谱 → 录入始祖 → 家谱总览 → 世系树 → 成员档案 / 新增亲属。
2. 登录 → 搜索公开家谱 → 查看简介 → 申请加入 → 审核中 → 管理员审核 → 进入家谱总览。
3. 家族 Tab → 动态 / 谱文 / 相册 / 人物录 / 礼仪 / 档案 → 详情 → 发布或编辑(取决于权限)。
4. 任一页面的通知入口 → 消息中心 → 相应业务详情。
5. 我的 → 个人资料 / 安全 / 帮助 / 反馈 / VIP / 关于与协议。
## 8. 本轮执行纪律
- 每次只推进一个设计阶段;阶段内的每个页面完成后,更新本文件对应复选框。
- 未有高保真设计和小屏检查的页面,不得开始其业务开发。
- 已确认的视觉规范是代码还原依据;若实现与设计不一致,以设计为准修正实现。
- 参考图是视觉锚点;核心材质、层级、比例、选中效果必须逐项一比一核对,不能只保留“红色中国风”的笼统印象。
- 设计稿不得保留无业务意义的横线、破折号或模拟字符;颜色差异必须有可见且明确的业务含义。
- 后续每一个页面新增的功能图标,必须同步生成、保存并验收透明 PNG 实体资产;未生成资产的页面不得标记为“设计完成”。
- 功能图标必须遵循线上成熟标准的 24dp 语义、留白、触控与选中规则,并导出为透明 PNG;项目可在此基础上保留已确认的古金双线、轻云纹收口等家祠风格。当前从大图裁切的试验资产不得引用,需按单图标标准重新导出后验收。
- 大部分按钮、顶部操作和快捷入口图标采用已确认的古金双线、轻云纹收口风格;底部 Tab 使用简洁同色系版本。每一页必须指定并使用已生成的背景资产组合,未完成背景设计的页面不得标记为“设计完成”。
- 全页 AI 生成图可作为页面的美术效果锚点,但不能单独作为布局验收依据;页面必须同时按固定 Android 栅格、真实数据、真实 PNG 资产制作。已生成的“我的家谱”效果图保留其朱砂、宣纸、题签、山水和庄重层级,留白、Logo、占位文案与图标标准问题按设计栅格修正后才可计入完成项。
- 新增需求先写入本文件,再决定是否纳入本轮,避免在页面开发中临时扩张范围。
## 9. 最终验收(暂未开始)
- [ ] 所有 P / A / G / T / F / R / N / M 页面均有设计完成标记。
- [ ] 全部设计在 `320 × 568` 下无内容重叠、无遮挡、无不可点操作。
- [ ] 所有图标均为透明背景 PNG,且无绿色页面背景。
- [ ] 页面风格与“家祠卷轴”设计一致,代码页面与确认设计逐屏比对通过。
- [ ] 全量页面设计确认后,才解锁 uni-app 实现、接口联调和 Android 打包。
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title></title>
<script type="module" src="/main.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
import App from './App.vue'
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
return { app }
}
+35
View File
@@ -0,0 +1,35 @@
{
"name": "家谱",
"appid": "__UNI__JIAPUAPP",
"description": "家祠卷轴风格的家谱共建应用",
"versionName": "1.0.0",
"versionCode": 100,
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
"<uses-permission android:name=\"android.permission.READ_MEDIA_IMAGES\"/>"
]
},
"ios": {
"dSYMs": false
}
}
},
"mp-weixin": {
"usingComponents": true
},
"vueVersion": "3"
}
+66
View File
@@ -0,0 +1,66 @@
{
"pages": [
{
"path": "pages/auth/login",
"style": { "navigationStyle": "custom", "enablePullDownRefresh": false }
},
{
"path": "pages/genealogy/index",
"style": { "navigationStyle": "custom", "enablePullDownRefresh": false }
},
{
"path": "pages/genealogy/create",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/genealogy/detail",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/genealogy/search",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/genealogy/applications",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/lineage/first-person",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/content/list",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/content/editor",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/tree/index",
"style": { "navigationStyle": "custom", "disableScroll": true }
},
{
"path": "pages/member/detail",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/family/index",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/profile/index",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/notification/index",
"style": { "navigationStyle": "custom" }
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "家谱",
"navigationBarBackgroundColor": "#F6F0E5",
"backgroundColor": "#F6F0E5"
}
}
+84
View File
@@ -0,0 +1,84 @@
<template>
<view class="page-shell login-page">
<view class="ceremony-top">
<text class="seal-word"></text>
<text class="eyebrow">家脉绵延 · 生生不息</text>
<text class="brand-title">家谱</text>
<text class="brand-subtitle">为家族留存可传承的记忆</text>
</view>
<view class="login-paper">
<view class="section-title">登录家谱</view>
<text class="form-note">使用已注册手机号和密码登录</text>
<text class="form-label">手机号</text>
<input v-model="phone" class="line-input" type="number" maxlength="11" placeholder="请输入手机号" placeholder-class="placeholder" />
<text class="form-label">密码</text>
<input v-model="password" class="line-input" password placeholder="请输入密码" placeholder-class="placeholder" />
<text v-if="errorMessage" class="error-message">{{ errorMessage }}</text>
<button class="primary-button login-button" :loading="isSubmitting" :disabled="isSubmitting" @click="login">进入家谱</button>
<text class="login-tip">短信登录将在验证码服务联调后开放</text>
<text class="terms">登录即表示同意用户协议隐私政策</text>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { appApi } from '@/utils/api.js'
import { calcMD5 } from '@/utils/md5.js'
const phone = ref('')
const password = ref('')
const isSubmitting = ref(false)
const errorMessage = ref('')
const login = async () => {
if (!/^1\d{10}$/.test(phone.value)) {
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
return
}
if (!password.value) {
uni.showToast({ title: '请输入登录密码', icon: 'none' })
return
}
isSubmitting.value = true
errorMessage.value = ''
try {
await appApi.loginWithPassword({ phone: phone.value, passwordHash: calcMD5(password.value) })
uni.reLaunch({ url: '/pages/genealogy/index' })
} catch (error) {
errorMessage.value = error.message || '登录失败,请稍后重试'
} finally {
isSubmitting.value = false
}
}
</script>
<style scoped lang="scss">
.login-page { padding-top: 108rpx; }
.ceremony-top {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 40rpx 76rpx;
background: $brand-red;
color: #fff9ed;
}
.seal-word { display: flex; width: 66rpx; height: 66rpx; align-items: center; justify-content: center; box-sizing: border-box; border: 2rpx solid #f1d494; border-radius: 50%; color: #f5ddaa; font-family: serif; font-size: 37rpx; }
.eyebrow { margin-top: 24rpx; color: #f8dfad; font-size: 22rpx; letter-spacing: 5rpx; }
.brand-title { margin-top: 24rpx; font-family: serif; font-size: 68rpx; font-weight: 700; letter-spacing: 16rpx; }
.brand-subtitle { margin-top: 18rpx; color: #f5deae; font-size: 26rpx; letter-spacing: 3rpx; }
.login-paper { margin: -28rpx 28rpx 0; padding: 48rpx 42rpx; border: 1rpx solid $gold-soft; border-radius: 24rpx; background: $paper-white; }
.form-note { display: block; margin-top: 14rpx; color: $ink-muted; font-size: 23rpx; }
.form-label { display: block; margin-top: 40rpx; color: $ink-muted; font-size: 25rpx; }
.line-input { height: 78rpx; border-bottom: 1rpx solid $gold-soft; color: $ink; font-size: 30rpx; }
.placeholder { color: #b3a895; }
.error-message { display: block; margin-top: 22rpx; color: $brand-red; font-size: 23rpx; line-height: 1.5; }
.login-button { margin-top: 48rpx; }
.login-tip { display: block; margin-top: 24rpx; color: $ink-muted; font-size: 22rpx; text-align: center; }
.terms { display: block; margin-top: 16rpx; color: #a39785; font-size: 21rpx; text-align: center; }
</style>
+35
View File
@@ -0,0 +1,35 @@
<template>
<view class="page-shell editor-page">
<PageHeader title="发布家族动态" />
<view class="form-paper"><view class="section-title">记录此刻</view><text class="form-note">发布后当前家谱的成员可以在家族圈查看</text><textarea v-model="feedContent" maxlength="300" placeholder="写下通知、活动、家族故事或照片说明" placeholder-class="placeholder" /></view>
<view class="bottom-action"><button class="primary-button" :loading="isSubmitting" :disabled="isSubmitting" @click="submit">发布动态</button></view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import { appApi } from '@/utils/api.js'
import { genealogyContext } from '@/utils/genealogy-context.js'
const genealogyId = ref('')
const feedContent = ref('')
const isSubmitting = ref(false)
onLoad((query) => { genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() })
const submit = async () => {
if (!genealogyId.value) { uni.showToast({ title: '未找到当前家谱', icon: 'none' }); return }
if (!feedContent.value.trim()) { uni.showToast({ title: '请填写动态内容', icon: 'none' }); return }
isSubmitting.value = true
try { await appApi.createFeed(genealogyId.value, { feedType: 'text', feedContent: feedContent.value.trim() }); uni.navigateBack() } catch (error) { uni.showToast({ title: error.message || '发布失败,请稍后重试', icon: 'none' }) } finally { isSubmitting.value = false }
}
</script>
<style scoped lang="scss">
.editor-page { padding-bottom: 136rpx; }
.form-paper { margin: 28rpx; padding: 34rpx 30rpx; border: 1rpx solid $gold-soft; border-radius: 22rpx; background: $paper-white; }
.form-note { display: block; margin-top: 14rpx; color: $ink-muted; font-size: 23rpx; line-height: 1.65; }
textarea { width: 100%; height: 360rpx; margin-top: 30rpx; color: $ink; font-size: 28rpx; line-height: 1.7; }
.placeholder { color: #b7aa97; }
.bottom-action { position: fixed; right: 0; bottom: 0; left: 0; padding: 20rpx 28rpx calc(20rpx + env(safe-area-inset-bottom)); background: #fffaf0; border-top: 1rpx solid $gold-soft; }
</style>
+42
View File
@@ -0,0 +1,42 @@
<template>
<view class="page-shell content-page">
<PageHeader :title="config.title" />
<view class="content-intro"><text>{{ config.copy }}</text></view>
<view class="content-list"><view v-for="item in items" :key="item.id" class="content-card paper-card"><text class="content-title">{{ item.title || item.articleTitle || item.albumName || item.ceremonyName || '家族记录' }}</text><text class="content-summary">{{ item.summary || item.content || item.description || item.feedContent || '内容待补充' }}</text><text class="content-date">{{ item.date || item.createTime || '' }}</text></view><view v-if="!items.length && !loadError" class="empty-copy">暂无{{ config.title }}</view></view>
<text v-if="loadError" class="error-copy">{{ loadError }}</text>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import { appApi } from '@/utils/api.js'
import { genealogyContext } from '@/utils/genealogy-context.js'
const type = ref('article')
const items = ref([])
const loadError = ref('')
const definitions = { article: { title: '谱文', copy: '记录家族历史、家训与珍贵文字。' }, album: { title: '相册', copy: '珍存家人、故土与仪式的影像。' }, ceremony: { title: '祭祀', copy: '查看家族祭祀安排与纪念活动。' }, record: { title: '族务', copy: '共同记录成长、备忘与家族事务。' } }
const config = computed(() => definitions[type.value] || definitions.article)
onLoad(async (query) => {
type.value = definitions[query.type] ? query.type : 'article'
const genealogyId = query.genealogyId || genealogyContext.getCurrentGenealogyId()
if (!genealogyId) { loadError.value = '请先选择一个家谱。'; return }
genealogyContext.setCurrentGenealogyId(genealogyId)
try { items.value = await appApi.getContent(type.value, genealogyId) } catch (error) { loadError.value = error.message || '内容加载失败。' }
})
</script>
<style scoped lang="scss">
.content-page { padding-bottom: 36rpx; }
.content-intro { padding: 28rpx 30rpx; background: #efe3cd; color: $ink-muted; font-size: 24rpx; }
.content-list { display: flex; flex-direction: column; gap: 18rpx; margin: 26rpx 28rpx; }
.content-card { padding: 28rpx; }
.content-title { display: block; color: $ink; font-size: 31rpx; font-weight: 700; }
.content-summary { display: block; margin-top: 14rpx; color: $ink-muted; font-size: 25rpx; line-height: 1.65; }
.content-date { display: block; margin-top: 16rpx; color: $gold; font-size: 22rpx; }
.empty-copy { padding: 80rpx 0; color: $ink-muted; font-size: 26rpx; text-align: center; }
.error-copy { display: block; margin: 20rpx 28rpx; color: $brand-red; font-size: 24rpx; }
</style>
+60
View File
@@ -0,0 +1,60 @@
<template>
<view class="page-shell family-page">
<view class="family-top"><view><text class="top-kicker">家族共建</text><text class="page-name">家族记事</text></view><text class="publish" @click="publishFeed">发布动态</text></view>
<view v-if="loadError" class="error-copy">{{ loadError }}</view>
<view class="content-rail"><view v-for="item in sections" :key="item.type" class="rail-item" @click="openSection(item.type)"><text>{{ item.name }}</text><text>{{ item.detail }}</text></view></view>
<view class="feed-heading"><text class="section-title">家族近况</text><text class="feed-filter">全部动态</text></view>
<view class="feed-list"><view v-for="feed in feeds" :key="feed.id" class="feed-card paper-card"><view class="feed-meta"><text>{{ feed.type || feed.feedType || '动态' }}</text><text>{{ feed.date || feed.createTime || '' }}</text></view><text class="feed-title">{{ feed.title || '家族动态' }}</text><text class="feed-content">{{ feed.content || feed.feedContent }}</text><text class="feed-author">发布人:{{ feed.author || feed.creatorName || '族人' }}</text></view><view v-if="!feeds.length && !loadError" class="empty-copy">暂未发布家族动态</view></view>
<AppTabbar active="family" />
</view>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import AppTabbar from '@/components/AppTabbar.vue'
import { appApi } from '@/utils/api.js'
import { genealogyContext } from '@/utils/genealogy-context.js'
const feeds = ref([])
const loadError = ref('')
const genealogyId = ref('')
const sections = [
{ type: 'article', name: '谱文', detail: '族史与家训' },
{ type: 'album', name: '相册', detail: '珍存旧影' },
{ type: 'ceremony', name: '祭祀', detail: '慎终追远' },
{ type: 'record', name: '族务', detail: '共同记事' }
]
onMounted(async () => {
genealogyId.value = genealogyContext.getCurrentGenealogyId()
if (!genealogyId.value) { loadError.value = '请先从“我的家谱”选择一个家谱。'; return }
try { feeds.value = await appApi.getFeeds(genealogyId.value) } catch (error) { loadError.value = error.message || '家族动态加载失败。' }
})
const openSection = (type) => uni.navigateTo({ url: `/pages/content/list?type=${type}&genealogyId=${genealogyId.value}` })
const publishFeed = () => {
if (!genealogyId.value) { uni.showToast({ title: '请先选择家谱', icon: 'none' }); return }
uni.navigateTo({ url: `/pages/content/editor?type=feed&genealogyId=${genealogyId.value}` })
}
</script>
<style scoped lang="scss">
.family-page { padding: 42rpx 28rpx 178rpx; }
.family-top { display: flex; align-items: flex-start; justify-content: space-between; }
.top-kicker { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 5rpx; }
.page-name { display: block; margin-top: 10rpx; color: $ink; font-family: serif; font-size: 52rpx; font-weight: 700; letter-spacing: 5rpx; }
.publish { margin-top: 22rpx; color: $brand-red; font-size: 26rpx; }
.error-copy { margin-top: 24rpx; color: $brand-red; font-size: 24rpx; }
.content-rail { display: flex; gap: 12rpx; margin-top: 34rpx; overflow-x: auto; }
.rail-item { min-width: 150rpx; padding: 18rpx; border: 1rpx solid $gold-soft; background: #fffaf0; }
.rail-item text { display: block; color: $ink; font-size: 28rpx; font-weight: 700; }
.rail-item text + text { margin-top: 8rpx; color: $ink-muted; font-size: 20rpx; font-weight: 400; }
.feed-heading { display: flex; justify-content: space-between; align-items: center; margin-top: 38rpx; }
.feed-filter { color: $brand-red; font-size: 24rpx; }
.feed-list { display: flex; flex-direction: column; gap: 18rpx; margin-top: 20rpx; }
.feed-card { padding: 28rpx; }
.feed-meta { display: flex; justify-content: space-between; color: $gold; font-size: 21rpx; }
.feed-title { display: block; margin-top: 16rpx; color: $ink; font-size: 32rpx; font-weight: 700; }
.feed-content { display: block; margin-top: 12rpx; color: $ink-muted; font-size: 25rpx; line-height: 1.65; }
.feed-author { display: block; margin-top: 16rpx; color: #9a8c78; font-size: 21rpx; }
.empty-copy { padding: 80rpx 0; color: $ink-muted; font-size: 26rpx; text-align: center; }
</style>
+67
View File
@@ -0,0 +1,67 @@
<template>
<view class="page-shell applications-page">
<PageHeader title="入谱审核" action="审核说明" @action="showHelp" />
<view class="audit-tip"><text>{{ loadError || '通过前请核实申请人的亲属关系;审核结果会通过通知告知对方。' }}</text></view>
<view class="application-list">
<view v-for="item in applications" :key="item.id" class="application-card paper-card">
<view class="application-top"><view><text class="applicant-name">{{ item.name }}</text><text class="applicant-phone">{{ item.phone }}</text></view><text class="applicant-time">{{ item.appliedAt }}</text></view>
<text class="relation-copy">{{ item.relation }}</text>
<view v-if="item.status === 'PENDING'" class="audit-actions"><button class="reject-button" @click="audit(item, false)">拒绝</button><button class="approve-button" @click="audit(item, true)">通过</button></view>
<text v-else class="audit-status">{{ item.status === 'APPROVED' ? '已通过' : '已拒绝' }}</text>
</view>
<view v-if="!applications.length && !loadError" class="empty-copy">暂无待审核申请</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import { appApi } from '@/utils/api.js'
import { genealogyContext } from '@/utils/genealogy-context.js'
const applications = ref([])
const genealogyId = ref('')
const loadError = ref('')
onLoad(async (query) => {
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId()
if (!genealogyId.value) { loadError.value = '未找到当前家谱,请从家谱总览进入。'; return }
genealogyContext.setCurrentGenealogyId(genealogyId.value)
try {
applications.value = await appApi.getPendingApplications(genealogyId.value)
} catch (error) {
loadError.value = error.message || '审核列表加载失败。'
}
})
const audit = async (item, approved) => {
try {
await appApi.auditApplication(genealogyId.value, item.id, approved)
item.status = approved ? 'APPROVED' : 'REJECTED'
uni.showToast({ title: approved ? '已通过申请' : '已拒绝申请', icon: 'none' })
} catch (error) {
uni.showToast({ title: error.message || '审核失败,请稍后重试', icon: 'none' })
}
}
const showHelp = () => uni.showModal({ title: '审核说明', content: '仅确认与本家谱存在真实亲属关系的申请,避免错误入谱。', showCancel: false })
</script>
<style scoped lang="scss">
.applications-page { padding-bottom: 36rpx; }
.audit-tip { margin: 26rpx 28rpx 0; padding: 24rpx; border-left: 5rpx solid $brand-red; background: #efe3cd; color: $ink-muted; font-size: 24rpx; line-height: 1.6; }
.application-list { display: flex; flex-direction: column; gap: 20rpx; margin: 24rpx 28rpx; }
.application-card { padding: 28rpx; }
.application-top { display: flex; justify-content: space-between; }
.applicant-name { color: $ink; font-size: 32rpx; font-weight: 700; }
.applicant-phone { margin-left: 16rpx; color: $ink-muted; font-size: 22rpx; }
.applicant-time { color: #a39583; font-size: 21rpx; }
.relation-copy { display: block; margin-top: 16rpx; color: $ink-muted; font-size: 25rpx; }
.audit-actions { display: flex; justify-content: flex-end; gap: 16rpx; margin-top: 24rpx; }
.audit-actions button { min-width: 128rpx; min-height: 64rpx; margin: 0; border-radius: 8rpx; font-size: 24rpx; }
.reject-button { border: 1rpx solid $gold-soft; background: #fffaf0; color: $ink-muted; }
.approve-button { border: 1rpx solid $brand-red; background: $brand-red; color: #fff9ed; }
.audit-status { display: block; margin-top: 24rpx; color: $brand-red; font-size: 24rpx; text-align: right; }
.empty-copy { padding: 80rpx 0; color: $ink-muted; font-size: 26rpx; text-align: center; }
</style>
+68
View File
@@ -0,0 +1,68 @@
<template>
<view class="page-shell create-page">
<PageHeader title="创建家谱" />
<view class="form-paper">
<view class="section-title">立谱信息</view>
<text class="form-note">家谱建立后可继续完善名称与访问规则由创建者维护</text>
<view class="field-row">
<text>姓氏</text>
<input v-model="form.surname" maxlength="4" placeholder="如:汤" placeholder-class="placeholder" />
</view>
<view class="field-row">
<text>谱名</text>
<input v-model="form.name" maxlength="24" placeholder="如:四川武胜汤氏族谱" placeholder-class="placeholder" />
</view>
<view class="field-row">
<text>堂号</text>
<input v-model="form.hall" maxlength="12" placeholder="选填" placeholder-class="placeholder" />
</view>
<view class="field-row">
<text>所在地</text>
<input v-model="form.location" maxlength="30" placeholder="请选择或输入" placeholder-class="placeholder" />
</view>
</view>
<view class="privacy-paper">
<view class="privacy-heading">
<text class="section-title">访问规则</text>
<text class="privacy-value">默认仅成员可见</text>
</view>
<text class="form-note">保护族人资料创建者可在家谱设置中开放搜索与申请加入</text>
</view>
<view class="bottom-action"><button class="primary-button" @click="submit">创建并录入首代</button></view>
</view>
</template>
<script setup>
import { reactive } from 'vue'
import PageHeader from '@/components/PageHeader.vue'
import { appApi } from '@/utils/api.js'
import { genealogyContext } from '@/utils/genealogy-context.js'
const form = reactive({ surname: '', name: '', hall: '', location: '', visibility: 'MEMBER_ONLY' })
const submit = async () => {
if (!form.surname.trim() || !form.name.trim()) {
uni.showToast({ title: '请填写姓氏和谱名', icon: 'none' })
return
}
const created = await appApi.createGenealogy({ ...form })
genealogyContext.setCurrentGenealogyId(created.id)
uni.redirectTo({ url: `/pages/lineage/first-person?genealogyId=${created.id}` })
}
</script>
<style scoped lang="scss">
.create-page { padding-bottom: 136rpx; }
.form-paper, .privacy-paper { margin: 28rpx; padding: 34rpx 30rpx; border: 1rpx solid $gold-soft; border-radius: 22rpx; background: $paper-white; }
.form-note { display: block; margin-top: 14rpx; color: $ink-muted; font-size: 23rpx; line-height: 1.65; }
.field-row { display: flex; align-items: center; min-height: 104rpx; border-bottom: 1rpx solid #eadfc9; }
.field-row:last-child { border-bottom: 0; }
.field-row text { width: 130rpx; color: $ink-muted; font-size: 28rpx; }
.field-row input { flex: 1; color: $ink; font-size: 29rpx; }
.placeholder { color: #b7aa97; }
.privacy-heading { display: flex; align-items: center; justify-content: space-between; }
.privacy-value { color: $brand-red; font-size: 24rpx; }
.bottom-action { position: fixed; right: 0; bottom: 0; left: 0; padding: 20rpx 28rpx calc(20rpx + env(safe-area-inset-bottom)); background: #fffaf0; border-top: 1rpx solid $gold-soft; }
</style>
+83
View File
@@ -0,0 +1,83 @@
<template>
<view class="page-shell detail-page">
<PageHeader title="家谱总览" action="管理" @action="showSoon" />
<view v-if="genealogy" class="genealogy-hero">
<text class="hero-kicker">{{ genealogy.hall || '堂号待补' }} · {{ genealogy.location || '所在地待补' }}</text>
<text class="hero-title">{{ genealogy.name }}</text>
<text class="hero-motto">{{ genealogy.motto || '敦亲睦族,敬祖传家。' }}</text>
<view class="hero-stats"><text> {{ genealogy.memberCount || 0 }} </text><text>已激活 {{ genealogy.activeCount || 0 }} </text><text>{{ genealogy.visibility || '仅成员可见' }}</text></view>
</view>
<view v-else class="state-copy">{{ loadError || '正在载入家谱…' }}</view>
<template v-if="genealogy">
<view class="core-actions paper-card">
<view class="section-title">家谱核心</view>
<view class="action-grid">
<view class="action-cell primary-cell" @click="toTree"><text>世系树</text><text>查看家脉关系</text></view>
<view class="action-cell" @click="toFirstPerson"><text>录入族人</text><text>从首代开始完善</text></view>
<view class="action-cell" @click="showSoon"><text>字辈谱</text><text>传承行辈次序</text></view>
<view class="action-cell" @click="toApplications"><text>入谱审核</text><text>处理加入申请</text></view>
</view>
</view>
<view class="family-summary"><text class="section-title">家族近况</text><view class="summary-row" @click="toFamily"><text>进入家族圈查看动态</text><text>查看</text></view></view>
<view class="invite-block"><text class="invite-title">邀亲人共建家谱</text><text class="invite-copy">族人申请加入后由创建者审核确认</text><button class="primary-button" @click="showSoon">邀请亲人</button></view>
</template>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import { appApi } from '@/utils/api.js'
import { genealogyContext } from '@/utils/genealogy-context.js'
const genealogy = ref(null)
const genealogyId = ref('')
const loadError = ref('')
const loadGenealogy = async (id) => {
genealogyId.value = id || genealogyContext.getCurrentGenealogyId()
if (!genealogyId.value) {
loadError.value = '未找到当前家谱,请从“我的家谱”重新进入。'
return
}
genealogyContext.setCurrentGenealogyId(genealogyId.value)
try {
genealogy.value = await appApi.getOverview(genealogyId.value)
if (!genealogy.value) loadError.value = '家谱不存在或无权访问。'
} catch (error) {
loadError.value = error.message || '家谱加载失败。'
}
}
onLoad((query) => loadGenealogy(query.id))
const toTree = () => uni.navigateTo({ url: `/pages/tree/index?genealogyId=${genealogyId.value}` })
const toFirstPerson = () => uni.navigateTo({ url: `/pages/lineage/first-person?genealogyId=${genealogyId.value}` })
const toFamily = () => uni.reLaunch({ url: '/pages/family/index' })
const toApplications = () => uni.navigateTo({ url: `/pages/genealogy/applications?genealogyId=${genealogyId.value}` })
const showSoon = () => uni.showToast({ title: '该模块将在后续阶段接入', icon: 'none' })
</script>
<style scoped lang="scss">
.detail-page { padding-bottom: 48rpx; }
.genealogy-hero { padding: 48rpx 34rpx 38rpx; background: $navy; color: #fff8ec; }
.hero-kicker { display: block; color: #e6c57f; font-size: 22rpx; letter-spacing: 4rpx; }
.hero-title { display: block; margin-top: 20rpx; font-family: serif; font-size: 52rpx; font-weight: 700; letter-spacing: 4rpx; }
.hero-motto { display: block; margin-top: 16rpx; color: #eadcbf; font-size: 25rpx; }
.hero-stats { display: flex; flex-wrap: wrap; gap: 12rpx 20rpx; margin-top: 28rpx; color: #dac797; font-size: 22rpx; }
.state-copy { padding: 80rpx 40rpx; color: $ink-muted; font-size: 27rpx; text-align: center; }
.core-actions { margin: -18rpx 28rpx 0; padding: 30rpx; }
.action-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16rpx; margin-top: 26rpx; }
.action-cell { min-height: 122rpx; padding: 20rpx; box-sizing: border-box; border: 1rpx solid #e7d9be; background: #fbf5e9; }
.action-cell text { display: block; color: $ink; font-size: 28rpx; font-weight: 700; }
.action-cell text + text { margin-top: 10rpx; color: $ink-muted; font-size: 21rpx; font-weight: 400; }
.primary-cell { border-color: $brand-red; background: #f8ede3; }
.primary-cell text:first-child { color: $brand-red; }
.family-summary { margin: 38rpx 28rpx 0; }
.summary-row { display: flex; justify-content: space-between; margin-top: 14rpx; padding: 24rpx 6rpx; border-bottom: 1rpx solid #dfd1b6; color: $ink-muted; font-size: 25rpx; }
.summary-row text:last-child { color: $brand-red; }
.invite-block { margin: 42rpx 28rpx 0; padding: 32rpx; border: 1rpx solid $gold; background: #efe0c0; }
.invite-title { display: block; color: $brand-red; font-family: serif; font-size: 37rpx; font-weight: 700; }
.invite-copy { display: block; margin: 14rpx 0 24rpx; color: $ink-muted; font-size: 23rpx; }
</style>
+573
View File
@@ -0,0 +1,573 @@
<template>
<view class="page-shell genealogy-index">
<image
class="page-paper-texture"
src="/static/assets/backgrounds/paper-rice-texture-v2.jpg"
mode="scaleToFill"
/>
<image
class="page-footer-landscape"
src="/static/assets/backgrounds/footer-mountain-bamboo.png"
mode="scaleToFill"
/>
<PageHeader
root
title="我的家谱"
:unread-count="unreadCount"
@notice="toNotifications"
/>
<view class="genealogy-content">
<view v-if="isLoading" class="state-panel state-panel--loading">
<view class="loading-seal"></view>
<text class="state-title">正在整理家谱</text>
<text class="state-copy">请稍候家族记忆正在归卷</text>
</view>
<template v-else-if="hasGenealogies">
<view class="current-slip" @click="openGenealogy(currentGenealogy)">
<image
class="current-frame"
src="/static/assets/backgrounds/genealogy-current-slip-frame.png"
mode="scaleToFill"
/>
<view class="current-seal">
<image
class="current-seal-frame"
src="/static/assets/icons/genealogy/seal-current-frame-v1.png"
mode="scaleToFill"
/>
<text class="current-seal-title">家谱</text>
</view>
<view class="current-copy">
<text class="current-name">{{ currentGenealogy.name }}</text>
<view class="current-info-divider"></view>
<view class="current-meta">
<view class="current-meta-item">
<image
class="current-meta-icon"
src="/static/assets/icons/common/location-v1.png"
mode="aspectFit"
/>
<text class="current-meta-item-text">{{
currentGenealogy.location
}}</text>
</view>
<view class="current-meta-item">
<image
class="current-meta-icon"
src="/static/assets/icons/common/member-meta-v1.png"
mode="aspectFit"
/>
<text class="current-meta-item-text"
>{{ currentGenealogy.memberCount }} 位成员</text
>
</view>
<view class="current-meta-item">
<image
class="current-meta-icon"
src="/static/assets/icons/common/admin-v1.png"
mode="aspectFit"
/>
<text class="current-meta-item-text">管理员</text>
</view>
</view>
</view>
</view>
<view class="shortcut-grid">
<view
v-for="item in shortcuts"
:key="item.key"
class="shortcut-item"
@click="openShortcut(item.key)"
>
<image class="shortcut-icon" :src="item.icon" mode="aspectFit" />
<text class="shortcut-label">{{ item.label }}</text>
</view>
</view>
<image
class="section-divider"
src="/static/assets/backgrounds/genealogy-section-divider-v2.png"
mode="scaleToFill"
/>
<view class="genealogy-lower">
<view v-if="createdGenealogies.length" class="list-section">
<view class="section-heading"><text>我创建的</text></view>
<GenealogyCard
v-for="item in createdGenealogies"
:key="item.id"
:genealogy="item"
role="管理员"
:selected="item.id === currentGenealogy.id"
@select="openGenealogy"
/>
</view>
<view v-if="joinedGenealogies.length" class="list-section">
<view class="section-heading"><text>我加入的</text></view>
<GenealogyCard
v-for="item in joinedGenealogies"
:key="item.id"
:genealogy="item"
role="成员"
@select="openGenealogy"
/>
</view>
<view class="create-action" @click="createGenealogy">
<image
class="create-cloud"
src="/static/assets/icons/action/create-cloud-v1.png"
mode="aspectFit"
/>
<image
class="create-icon"
src="/static/assets/icons/action/add-v2.png"
mode="aspectFit"
/>
<text>新建家谱</text>
<image
class="create-cloud create-cloud--right"
src="/static/assets/icons/action/create-cloud-v1.png"
mode="aspectFit"
/>
</view>
</view>
</template>
<view v-else class="state-panel state-panel--empty">
<view class="empty-scroll"><text>待立家谱</text></view>
<text class="state-title">从第一位家人开始</text>
<text class="state-copy">为家族留下可传承的名字故事与记忆</text>
<view class="empty-primary" @click="createGenealogy">
<image
class="create-icon"
src="/static/assets/icons/action/add-v2.png"
mode="aspectFit"
/>
<text>新建家谱</text>
</view>
<view class="empty-secondary" @click="applyToJoin"
><text>搜索并申请加入</text></view
>
</view>
</view>
<AppTabbar active="genealogy" />
</view>
</template>
<script setup>
import { computed, ref } from "vue";
import AppTabbar from "@/components/AppTabbar.vue";
import GenealogyCard from "@/components/GenealogyCard.vue";
import PageHeader from "@/components/PageHeader.vue";
import { genealogies, notifications } from "@/data/mock.js";
const isLoading = ref(false);
const list = ref(genealogies);
const unreadCount = computed(
() => notifications.filter((item) => item.unread).length,
);
const hasGenealogies = computed(() => list.value.length > 0);
const createdGenealogies = computed(() =>
list.value.filter((item, index) => index === 0),
);
const joinedGenealogies = computed(() =>
list.value.filter((item, index) => index > 0),
);
const currentGenealogy = computed(() => list.value[0]);
const shortcuts = [
{
key: "tree",
label: "世系图",
icon: "/static/assets/icons/genealogy/tree-v2.png",
},
{
key: "members",
label: "成员",
icon: "/static/assets/icons/genealogy/members-v2.png",
},
{
key: "poem",
label: "字辈诗",
icon: "/static/assets/icons/genealogy/generation-poem-v2.png",
},
{
key: "applications",
label: "申请审核",
icon: "/static/assets/icons/genealogy/application-v2.png",
},
];
const openGenealogy = (genealogy) =>
uni.navigateTo({ url: `/pages/genealogy/detail?id=${genealogy.id}` });
const createGenealogy = () =>
uni.navigateTo({ url: "/pages/genealogy/create" });
const applyToJoin = () => uni.navigateTo({ url: "/pages/genealogy/search" });
const toNotifications = () =>
uni.navigateTo({ url: "/pages/notification/index" });
const openShortcut = (key) => {
const paths = {
tree: "/pages/tree/index",
members: `/pages/genealogy/detail?id=${currentGenealogy.value.id}`,
poem: "/pages/content/list?type=poem",
applications: "/pages/genealogy/applications",
};
uni.navigateTo({ url: paths[key] });
};
</script>
<style scoped lang="scss">
.genealogy-index {
position: relative;
min-height: 100vh;
overflow: hidden;
background: #f9f6ef;
}
.page-paper-texture {
position: absolute;
top: 184rpx;
right: 0;
bottom: 0;
left: 0;
z-index: 0;
width: 100%;
height: 100%;
opacity: 0.72;
pointer-events: none;
}
.page-footer-landscape {
position: absolute;
right: 0;
bottom: 128rpx;
left: 0;
z-index: 1;
width: 100%;
height: 360rpx;
opacity: 0.48;
pointer-events: none;
}
.genealogy-content {
position: relative;
z-index: 2;
padding: 28rpx 48rpx 194rpx;
}
.current-slip {
position: relative;
display: flex;
min-height: 272rpx;
align-items: center;
padding: 34rpx 30rpx;
box-sizing: border-box;
background: transparent;
}
.current-frame {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.current-seal {
position: relative;
z-index: 1;
display: flex;
width: 80rpx;
height: 136rpx;
flex: 0 0 auto;
align-items: center;
justify-content: center;
margin-right: 26rpx;
color: #fff7e7;
}
.current-seal-frame {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 0;
width: 100%;
height: 100%;
}
.current-seal-title {
position: relative;
z-index: 1;
color: #fff7e7;
font-family: "STKaiti", "KaiTi", serif;
font-size: 32rpx;
font-weight: 700;
letter-spacing: 2rpx;
writing-mode: vertical-rl;
}
.current-copy {
position: relative;
z-index: 1;
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
}
.current-name {
overflow: hidden;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 50rpx;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.current-info-divider {
width: 100%;
height: 1rpx;
margin: 20rpx 0 18rpx;
background: rgba(181, 138, 75, 0.48);
}
.current-meta {
display: flex;
flex-wrap: nowrap;
margin-top: 0;
color: $ink-muted;
font-size: 25rpx;
line-height: 1.4;
}
.current-meta-item {
display: flex;
align-items: center;
margin: 0 22rpx 8rpx 0;
white-space: nowrap;
.current-meta-item-text{
white-space: nowrap;
}
}
.current-meta-icon {
width: 32rpx;
height: 32rpx;
margin-right: 8rpx;
}
.shortcut-grid {
display: flex;
min-height: 208rpx;
align-items: center;
margin-top: 36rpx;
padding: 14rpx 0;
}
.shortcut-item {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
align-items: center;
}
.shortcut-icon {
width: 82rpx;
height: 82rpx;
}
.shortcut-label {
overflow: hidden;
width: 100%;
margin-top: 12rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 29rpx;
font-weight: 700;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.section-divider {
display: block;
width: 100%;
height: 30rpx;
margin: 10rpx 0 2rpx;
}
.genealogy-lower {
padding: 18rpx 0 44rpx;
}
.list-section {
margin-top: 22rpx;
}
.section-heading {
display: flex;
min-height: 64rpx;
align-items: center;
margin-bottom: 12rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 34rpx;
font-weight: 700;
}
.section-heading::before {
width: 8rpx;
height: 34rpx;
margin-right: 14rpx;
border-radius: 8rpx;
background: $brand-red;
content: "";
}
.list-section + .list-section {
margin-top: 26rpx;
}
.create-action,
.empty-primary,
.empty-secondary {
display: flex;
min-height: 96rpx;
align-items: center;
justify-content: center;
box-sizing: border-box;
font-size: 30rpx;
}
.create-action {
width: 420rpx;
margin: 42rpx auto 0;
border: 2rpx solid $brand-red;
border-radius: 48rpx;
background: rgba(255, 249, 238, 0.66);
color: $brand-red;
font-family: "STKaiti", "KaiTi", serif;
font-weight: 700;
letter-spacing: 3rpx;
}
.empty-primary {
width: 360rpx;
margin: 32rpx auto 0;
border: 2rpx solid $brand-red;
border-radius: 48rpx;
background: rgba(255, 249, 238, 0.94);
color: $brand-red;
font-family: serif;
font-weight: 700;
letter-spacing: 3rpx;
}
.create-icon {
width: 38rpx;
height: 38rpx;
margin-right: 12rpx;
}
.create-action .create-icon {
width: 44rpx;
height: 44rpx;
margin: 0 12rpx;
}
.create-cloud {
width: 64rpx;
height: 32rpx;
}
.create-cloud--right {
transform: scaleX(-1);
}
.state-panel {
display: flex;
min-height: 540rpx;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 42rpx;
box-sizing: border-box;
border: 2rpx solid $gold-soft;
border-radius: 18rpx;
background: rgba(255, 249, 238, 0.76);
text-align: center;
}
.state-title {
margin-top: 24rpx;
color: $ink;
font-family: serif;
font-size: 38rpx;
font-weight: 700;
}
.state-copy {
margin-top: 14rpx;
color: $ink-muted;
font-size: 25rpx;
line-height: 1.7;
}
.loading-seal,
.empty-scroll {
border: 2rpx solid $gold;
}
.loading-seal {
width: 92rpx;
height: 92rpx;
border-radius: 50%;
}
.empty-scroll {
display: flex;
width: 154rpx;
height: 196rpx;
align-items: center;
justify-content: center;
color: $gold;
font-family: serif;
font-size: 33rpx;
writing-mode: vertical-rl;
}
.empty-secondary {
margin-top: 16rpx;
color: $ink-muted;
font-size: 27rpx;
}
@media screen and (max-width: 340px) {
.genealogy-content {
padding-right: 24rpx;
padding-left: 24rpx;
}
.current-name {
font-size: 40rpx;
}
.shortcut-label {
font-size: 23rpx;
}
.current-meta-item {
margin-right: 12rpx;
}
.current-meta-icon {
width: 28rpx;
height: 28rpx;
}
.create-action {
width: 360rpx;
}
.create-cloud {
width: 48rpx;
}
}
</style>
+46
View File
@@ -0,0 +1,46 @@
<template>
<view class="page-shell search-page">
<PageHeader title="查找家谱" />
<view class="search-panel">
<input v-model="keyword" class="search-input" placeholder="输入姓氏、谱名或地区" placeholder-class="placeholder" @confirm="search" />
<text class="search-button" @click="search">搜索</text>
</view>
<text class="search-tip">仅展示已开放申请加入的家谱</text>
<view class="result-list">
<GenealogyCard v-for="item in results" :key="item.id" :genealogy="item" @select="openApply" />
<view v-if="searched && !results.length" class="empty-result"><text>暂未找到公开家谱</text><text>可联系家谱创建者开通公开申请</text></view>
</view>
</view>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import PageHeader from '@/components/PageHeader.vue'
import GenealogyCard from '@/components/GenealogyCard.vue'
import { appApi } from '@/utils/api.js'
const keyword = ref('')
const allResults = ref([])
const results = ref([])
const searched = ref(false)
onMounted(async () => { allResults.value = await appApi.getPublicGenealogies(); results.value = allResults.value })
const search = () => {
const value = keyword.value.trim()
results.value = value ? allResults.value.filter((item) => `${item.name}${item.surname}${item.location}`.includes(value)) : allResults.value
searched.value = true
}
const openApply = (genealogy) => uni.showModal({ title: '申请加入', content: `确认申请加入“${genealogy.name}”吗?`, success: async ({ confirm }) => { if (confirm) { await appApi.applyToJoin(genealogy.id, { message: '希望加入家谱并完善本人资料' }); uni.showToast({ title: '申请已提交,等待审核', icon: 'none' }) } } })
</script>
<style scoped lang="scss">
.search-page { padding-bottom: 36rpx; }
.search-panel { display: flex; align-items: center; gap: 18rpx; margin: 28rpx; padding: 0 22rpx; border: 1rpx solid $gold; background: #fffaf0; }
.search-input { flex: 1; height: 82rpx; color: $ink; font-size: 27rpx; }
.placeholder { color: #b6aa99; }
.search-button { color: $brand-red; font-size: 27rpx; font-weight: 700; }
.search-tip { display: block; margin: -8rpx 28rpx 22rpx; color: $ink-muted; font-size: 22rpx; }
.result-list { display: flex; flex-direction: column; gap: 18rpx; margin: 0 28rpx; }
.empty-result { display: flex; flex-direction: column; align-items: center; gap: 14rpx; padding: 90rpx 20rpx; color: $ink-muted; font-size: 26rpx; }
.empty-result text + text { color: #a69783; font-size: 22rpx; }
</style>
+60
View File
@@ -0,0 +1,60 @@
<template>
<view class="page-shell first-person-page">
<PageHeader title="录入首代人物" />
<view class="form-paper">
<view class="section-title">家谱从这一位开始</view>
<text class="form-note">可先录入姓名世代和简要生平其他资料后续随时补充</text>
<view class="field-row"><text>姓名</text><input v-model="form.personName" maxlength="20" placeholder="请输入首代姓名" placeholder-class="placeholder" /></view>
<view class="field-row"><text>世代</text><text class="fixed-value">第一世</text></view>
<view class="field-row"><text>出生日期</text><input v-model="form.birthDate" placeholder="如:1940年" placeholder-class="placeholder" /></view>
<view class="intro-field"><text>生平简述</text><textarea v-model="form.introduction" maxlength="200" placeholder="可选,记录家训、迁徙或重要经历" placeholder-class="placeholder" /></view>
</view>
<view class="bottom-action"><button class="primary-button" :loading="isSubmitting" :disabled="isSubmitting" @click="submit">保存并查看世系树</button></view>
</view>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import { appApi } from '@/utils/api.js'
import { genealogyContext } from '@/utils/genealogy-context.js'
const genealogyId = ref('')
const isSubmitting = ref(false)
const form = reactive({ personName: '', generationNo: 1, generationName: '一世', sex: '0', birthDate: '', introduction: '' })
onLoad((query) => {
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId()
if (genealogyId.value) genealogyContext.setCurrentGenealogyId(genealogyId.value)
})
const submit = async () => {
if (!genealogyId.value) { uni.showToast({ title: '未找到当前家谱', icon: 'none' }); return }
if (!form.personName.trim()) { uni.showToast({ title: '请填写首代姓名', icon: 'none' }); return }
isSubmitting.value = true
try {
const person = await appApi.createPerson(genealogyId.value, { ...form, personName: form.personName.trim() })
uni.redirectTo({ url: `/pages/tree/index?genealogyId=${genealogyId.value}&selectedId=${person.id}` })
} catch (error) {
uni.showToast({ title: error.message || '保存失败,请稍后重试', icon: 'none' })
} finally {
isSubmitting.value = false
}
}
</script>
<style scoped lang="scss">
.first-person-page { padding-bottom: 136rpx; }
.form-paper { margin: 28rpx; padding: 34rpx 30rpx; border: 1rpx solid $gold-soft; border-radius: 22rpx; background: $paper-white; }
.form-note { display: block; margin-top: 14rpx; color: $ink-muted; font-size: 23rpx; line-height: 1.65; }
.field-row { display: flex; align-items: center; min-height: 104rpx; border-bottom: 1rpx solid #eadfc9; }
.field-row text { width: 150rpx; color: $ink-muted; font-size: 28rpx; }
.field-row input { flex: 1; color: $ink; font-size: 29rpx; }
.fixed-value { color: $brand-red !important; }
.intro-field { padding-top: 30rpx; }
.intro-field > text { display: block; color: $ink-muted; font-size: 28rpx; }
.intro-field textarea { width: 100%; height: 180rpx; margin-top: 18rpx; color: $ink; font-size: 27rpx; line-height: 1.6; }
.placeholder { color: #b7aa97; }
.bottom-action { position: fixed; right: 0; bottom: 0; left: 0; padding: 20rpx 28rpx calc(20rpx + env(safe-area-inset-bottom)); background: #fffaf0; border-top: 1rpx solid $gold-soft; }
</style>
+58
View File
@@ -0,0 +1,58 @@
<template>
<view class="page-shell member-page">
<PageHeader title="成员档案" action="编辑" @action="showSoon" />
<view v-if="member" class="member-hero"><view class="member-seal">{{ member.name.slice(0, 1) }}</view><view><text class="member-name">{{ member.name }}</text><text class="member-role"> {{ member.generation }} · {{ member.relation }} · 家谱成员</text></view></view>
<view v-else class="state-copy">{{ loadError || '正在载入成员档案…' }}</view>
<template v-if="member">
<view class="profile-card paper-card"><text class="section-title">基本资料</text><view v-for="item in details" :key="item.label" class="info-row"><text>{{ item.label }}</text><text>{{ item.value }}</text></view></view>
<view class="profile-card paper-card"><text class="section-title">亲属关系</text><text class="bio">亲属关系将在编辑功能接入后完整维护</text></view>
<view v-if="member.introduction" class="profile-card paper-card"><text class="section-title">生平事迹</text><text class="bio">{{ member.introduction }}</text></view>
</template>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import { appApi } from '@/utils/api.js'
import { genealogyContext } from '@/utils/genealogy-context.js'
const member = ref(null)
const loadError = ref('')
const genealogyId = ref('')
const details = computed(() => member.value ? [
{ label: '字辈', value: member.value.generationName || `${member.value.generation}` },
{ label: '出生日期', value: member.value.birthDate || '待补' },
{ label: '生卒信息', value: member.value.years || '待补' },
{ label: '所属支系', value: member.value.branch || '主支' }
] : [])
onLoad(async (query) => {
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId()
if (!genealogyId.value || !query.personId) { loadError.value = '成员参数不完整,请从世系树进入。'; return }
genealogyContext.setCurrentGenealogyId(genealogyId.value)
try {
member.value = await appApi.getPerson(genealogyId.value, query.personId)
if (!member.value) loadError.value = '未找到该成员。'
} catch (error) {
loadError.value = error.message || '成员档案加载失败。'
}
})
const showSoon = () => uni.showToast({ title: '资料编辑将在下一阶段接入', icon: 'none' })
</script>
<style scoped lang="scss">
.member-page { padding-bottom: 36rpx; }
.member-hero { display: flex; align-items: center; gap: 24rpx; padding: 38rpx 34rpx; background: $navy; }
.member-seal { display: flex; width: 92rpx; height: 92rpx; align-items: center; justify-content: center; border: 3rpx solid #e0bd77; border-radius: 50%; color: #f4daa1; font-family: serif; font-size: 46rpx; }
.member-name { display: block; color: #fff8ec; font-family: serif; font-size: 46rpx; font-weight: 700; }
.member-role { display: block; margin-top: 10rpx; color: #e0c993; font-size: 23rpx; }
.state-copy { padding: 80rpx 40rpx; color: $ink-muted; font-size: 27rpx; text-align: center; }
.profile-card { margin: 26rpx 28rpx 0; padding: 30rpx; }
.info-row { display: flex; justify-content: space-between; padding: 25rpx 0; border-bottom: 1rpx solid #eadfc9; color: $ink-muted; font-size: 27rpx; }
.info-row text:last-child { color: $ink; }
.info-row:last-child { border-bottom: 0; }
.bio { display: block; margin-top: 22rpx; color: $ink-muted; font-size: 27rpx; line-height: 1.9; }
</style>
+54
View File
@@ -0,0 +1,54 @@
<template>
<view class="page-shell notification-page">
<PageHeader title="消息通知" action="全部已读" @action="readAll" />
<view class="notice-intro"><text>{{ loadError || '与家谱相关的提醒都会留在这里。' }}</text></view>
<view class="notice-list"><view v-for="item in list" :key="item.id" class="notice-row" :class="{ unread: item.unread }" @click="read(item)"><view class="notice-dot"></view><view class="notice-copy"><text class="notice-title">{{ item.title }}</text><text class="notice-content">{{ item.content }}</text></view><text class="notice-time">{{ item.time }}</text></view></view>
</view>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import PageHeader from '@/components/PageHeader.vue'
import { appApi } from '@/utils/api.js'
const list = ref([])
const loadError = ref('')
onMounted(async () => {
try {
list.value = await appApi.getNotifications()
} catch (error) {
loadError.value = error.message || '通知加载失败。'
}
})
const read = async (item) => {
if (!item.unread) return
try {
await appApi.markNotificationRead(item.id)
item.unread = false
} catch (error) {
uni.showToast({ title: error.message || '标记已读失败', icon: 'none' })
}
}
const readAll = async () => {
try {
await appApi.markAllNotificationsRead()
list.value.forEach((item) => { item.unread = false })
uni.showToast({ title: '已全部标记为已读', icon: 'none' })
} catch (error) {
uni.showToast({ title: error.message || '操作失败,请稍后重试', icon: 'none' })
}
}
</script>
<style scoped lang="scss">
.notification-page { padding-bottom: 36rpx; }
.notice-intro { padding: 28rpx 30rpx; background: #efe3cd; color: $ink-muted; font-size: 24rpx; }
.notice-list { margin: 0 28rpx; }
.notice-row { display: flex; align-items: flex-start; gap: 16rpx; padding: 28rpx 0; border-bottom: 1rpx solid #e4d8c0; }
.notice-dot { width: 14rpx; height: 14rpx; flex: 0 0 auto; margin-top: 13rpx; border-radius: 50%; background: transparent; }
.notice-row.unread .notice-dot { background: $brand-red; }
.notice-copy { min-width: 0; flex: 1; }
.notice-title { display: block; color: $ink; font-size: 30rpx; font-weight: 700; }
.notice-content { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.55; }
.notice-time { flex: 0 0 auto; color: #a19482; font-size: 20rpx; }
</style>
+48
View File
@@ -0,0 +1,48 @@
<template>
<view class="page-shell profile-page">
<view class="profile-hero"><view class="profile-seal">{{ profile.name ? profile.name.slice(0, 1) : '谱' }}</view><view class="profile-copy"><text class="profile-name">{{ profile.name || '家谱用户' }}</text><text class="profile-meta">{{ profile.phone || '资料加载中' }} · {{ profile.role || '家谱成员' }}</text></view><text class="profile-edit" @click="showSoon">资料</text></view>
<text v-if="loadError" class="error-copy">{{ loadError }}</text>
<view class="todo-card paper-card" @click="toNotifications"><view><text class="todo-title">待你处理</text><text class="todo-copy">家谱相关提醒与审核通知</text></view><text class="todo-count">{{ unreadCount }}</text></view>
<view class="menu-group paper-card"><view v-for="item in accountItems" :key="item" class="menu-row" @click="showSoon"><text>{{ item }}</text><text>查看</text></view></view>
<view class="menu-group paper-card"><view v-for="item in serviceItems" :key="item" class="menu-row" @click="showSoon"><text>{{ item }}</text><text>查看</text></view></view>
<AppTabbar active="profile" />
</view>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import AppTabbar from '@/components/AppTabbar.vue'
import { appApi } from '@/utils/api.js'
import { notifications } from '@/data/mock.js'
const profile = ref({})
const loadError = ref('')
const unreadCount = computed(() => notifications.filter((item) => item.unread).length)
const accountItems = ['我的资料', '我的加入申请', '账号与安全']
const serviceItems = ['帮助中心', '意见反馈', 'VIP 服务']
onMounted(async () => {
try { profile.value = await appApi.getProfile() } catch (error) { loadError.value = error.message || '个人资料加载失败。' }
})
const toNotifications = () => uni.navigateTo({ url: '/pages/notification/index' })
const showSoon = () => uni.showToast({ title: '该服务将按计划逐步开放', icon: 'none' })
</script>
<style scoped lang="scss">
.profile-page { padding-bottom: 178rpx; }
.profile-hero { display: flex; align-items: center; gap: 22rpx; padding: 48rpx 30rpx; background: $navy; }
.profile-seal { display: flex; width: 88rpx; height: 88rpx; align-items: center; justify-content: center; border: 3rpx solid #e0bd77; border-radius: 50%; color: #f4daa1; font-family: serif; font-size: 43rpx; }
.profile-copy { min-width: 0; flex: 1; }
.profile-name { display: block; color: #fff8ec; font-family: serif; font-size: 43rpx; font-weight: 700; }
.profile-meta { display: block; margin-top: 10rpx; color: #dfc590; font-size: 21rpx; }
.profile-edit { color: #f4dca8; font-size: 24rpx; }
.error-copy { display: block; margin: 18rpx 28rpx 0; color: $brand-red; font-size: 23rpx; }
.todo-card { display: flex; align-items: center; justify-content: space-between; margin: -14rpx 28rpx 0; padding: 28rpx; background: #fff8ea; }
.todo-title { display: block; color: $ink; font-size: 30rpx; font-weight: 700; }
.todo-copy { display: block; margin-top: 8rpx; color: $ink-muted; font-size: 22rpx; }
.todo-count { display: flex; width: 52rpx; height: 52rpx; align-items: center; justify-content: center; border-radius: 50%; background: $brand-red; color: #fff8ec; font-size: 26rpx; }
.menu-group { margin: 28rpx 28rpx 0; padding: 0 28rpx; }
.menu-row { display: flex; align-items: center; justify-content: space-between; min-height: 102rpx; border-bottom: 1rpx solid #eadfc9; color: $ink; font-size: 29rpx; }
.menu-row:last-child { border-bottom: 0; }
.menu-row text:last-child { color: $brand-red; font-size: 23rpx; }
</style>
+62
View File
@@ -0,0 +1,62 @@
<template>
<view class="tree-page">
<PageHeader title="世系树" action="树形视图" />
<view class="tree-toolbar"><view class="branch-select">当前家谱世系</view><view class="tree-tools"><text @click="showSoon">查找</text><text @click="showSoon">编辑</text></view></view>
<scroll-view class="tree-scroll" scroll-x scroll-y>
<view class="tree-canvas">
<view v-if="!members.length" class="tree-empty">{{ loadError || '暂未录入族人' }}</view>
<view v-for="member in members" :key="member.id" class="member-node" :class="{ selected: selected && selected.id === member.id }" :style="nodeStyle(member)" @click="selected = member"><text class="node-name">{{ member.name }}</text><text class="node-relation">{{ member.relation }}</text><text class="node-years">{{ member.years }}</text></view>
</view>
</scroll-view>
<view v-if="selected" class="member-sheet"><view><text class="sheet-name">{{ selected.name }}</text><text class="sheet-meta">第 {{ selected.generation }} 世 · {{ selected.relation }} · {{ selected.branch }}</text></view><view class="sheet-actions"><text @click="toMember">查看资料</text><text @click="showSoon">添加亲属</text></view></view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import { appApi } from '@/utils/api.js'
import { genealogyContext } from '@/utils/genealogy-context.js'
const members = ref([])
const selected = ref(null)
const genealogyId = ref('')
const loadError = ref('')
const loadTree = async (query) => {
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId()
if (!genealogyId.value) { loadError.value = '未找到当前家谱。'; return }
genealogyContext.setCurrentGenealogyId(genealogyId.value)
try {
members.value = await appApi.getTree(genealogyId.value)
selected.value = members.value.find((item) => String(item.id) === String(query.selectedId)) || members.value[0] || null
} catch (error) {
loadError.value = error.message || '世系树加载失败。'
}
}
onLoad(loadTree)
const nodeStyle = (member) => ({ left: `${member.x}%`, top: `${member.y}%` })
const toMember = () => uni.navigateTo({ url: `/pages/member/detail?genealogyId=${genealogyId.value}&personId=${selected.value.id}` })
const showSoon = () => uni.showToast({ title: '亲属关系编辑将在下一阶段接入', icon: 'none' })
</script>
<style scoped lang="scss">
.tree-page { height: 100vh; overflow: hidden; background: $paper; }
.tree-toolbar { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 28rpx; border-bottom: 1rpx solid $gold-soft; background: #fffaf0; }
.branch-select { padding: 12rpx 18rpx; border: 1rpx solid $gold; color: $ink; font-size: 25rpx; }
.tree-tools { display: flex; gap: 24rpx; color: $brand-red; font-size: 26rpx; }
.tree-scroll { height: calc(100vh - 382rpx); white-space: nowrap; }
.tree-canvas { position: relative; width: 1180rpx; height: 1120rpx; background: $paper; }
.tree-empty { padding: 240rpx 0; color: $ink-muted; font-size: 28rpx; text-align: center; }
.member-node { position: absolute; width: 150rpx; min-height: 148rpx; padding: 20rpx 12rpx; box-sizing: border-box; border: 2rpx solid $gold-soft; background: #fffaf0; transform: translateX(-50%); white-space: normal; text-align: center; }
.member-node.selected { border-color: $brand-red; box-shadow: 0 0 0 6rpx #f2dfcf; }
.node-name { display: block; color: $ink; font-family: serif; font-size: 32rpx; font-weight: 700; }
.node-relation { display: block; margin-top: 10rpx; color: $brand-red; font-size: 22rpx; }
.node-years { display: block; margin-top: 8rpx; color: $ink-muted; font-size: 20rpx; }
.member-sheet { position: fixed; right: 0; bottom: 0; left: 0; display: flex; align-items: center; justify-content: space-between; padding: 26rpx 28rpx calc(26rpx + env(safe-area-inset-bottom)); border-top: 1rpx solid $gold; background: $navy; color: #fff8ec; }
.sheet-name { display: block; font-family: serif; font-size: 36rpx; font-weight: 700; }
.sheet-meta { display: block; margin-top: 8rpx; color: #dfc590; font-size: 21rpx; }
.sheet-actions { display: flex; flex-direction: column; gap: 12rpx; color: #f3dba7; font-size: 24rpx; text-align: right; }
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+58
View File
@@ -0,0 +1,58 @@
page {
background: $paper;
color: $ink;
font-family: "STKaiti", "KaiTi", serif
}
.page-shell {
min-height: 100vh;
box-sizing: border-box;
background: $paper;
}
.paper-card {
background: $paper-white;
border: 1rpx solid $gold-soft;
border-radius: 24rpx;
box-sizing: border-box;
}
.section-title {
color: $ink;
font-size: 34rpx;
font-weight: 700;
letter-spacing: 2rpx;
}
.muted-copy {
color: $ink-muted;
font-size: 26rpx;
line-height: 1.7;
}
.primary-button {
display: flex;
align-items: center;
justify-content: center;
min-height: 92rpx;
border: 0;
border-radius: 14rpx;
background: $brand-red;
color: #fff8ec;
font-size: 31rpx;
font-weight: 700;
letter-spacing: 3rpx;
}
.secondary-button {
display: flex;
align-items: center;
justify-content: center;
min-height: 88rpx;
border: 1rpx solid $gold;
border-radius: 14rpx;
background: transparent;
color: $brand-red;
font-size: 29rpx;
font-weight: 600;
}
+66
View File
@@ -0,0 +1,66 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$errors = [System.Collections.Generic.List[string]]::new()
function Test-LocalImport {
param(
[string]$SourceFile,
[string]$Target,
[string[]]$Extensions
)
if ($Target -match '^(https?:|sass:)') { return $true }
if ($Target.StartsWith('@/')) {
$candidate = Join-Path $root $Target.Substring(2)
} elseif ($Target.StartsWith('./') -or $Target.StartsWith('../')) {
$candidate = Join-Path (Split-Path -Parent $SourceFile) $Target
} else {
return $true
}
if (Test-Path -LiteralPath $candidate) { return $true }
foreach ($extension in $Extensions) {
if (Test-Path -LiteralPath ($candidate + $extension)) { return $true }
}
return $false
}
$pages = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages.json') | ConvertFrom-Json
foreach ($page in $pages.pages) {
$pageFile = Join-Path $root ($page.path + '.vue')
if (-not (Test-Path -LiteralPath $pageFile)) {
$errors.Add("Route has no page file: $($page.path)")
}
}
$manifest = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'manifest.json') | ConvertFrom-Json
if ($manifest.vueVersion -eq '3' -and -not (Test-Path -LiteralPath (Join-Path $root 'index.html'))) {
$errors.Add('Vue 3 project is missing index.html.')
}
$sourceFiles = Get-ChildItem -Path $root -Recurse -File | Where-Object {
$_.FullName -notmatch '\\unpackage\\' -and $_.Extension -in '.vue', '.js', '.scss'
}
foreach ($source in $sourceFiles) {
$content = Get-Content -Raw -Encoding UTF8 $source.FullName
foreach ($match in [regex]::Matches($content, '@(?:import|use|forward)\s+["''](?<target>[^"'']+)["'']')) {
$target = $match.Groups['target'].Value
if (-not (Test-LocalImport -SourceFile $source.FullName -Target $target -Extensions @('.scss'))) {
$errors.Add("Missing Sass import in $($source.FullName.Substring($root.Length + 1)): $target")
}
}
foreach ($match in [regex]::Matches($content, '(?:from|import)\s+["''](?<target>[^"'']+)["'']')) {
$target = $match.Groups['target'].Value
if (-not (Test-LocalImport -SourceFile $source.FullName -Target $target -Extensions @('.js', '.vue', '.json'))) {
$errors.Add("Missing module import in $($source.FullName.Substring($root.Length + 1)): $target")
}
}
}
if ($errors.Count -gt 0) {
throw "Compile audit failed:`n$($errors -join "`n")"
}
Write-Output 'PASS compile audit'
+114
View File
@@ -0,0 +1,114 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$requiredFiles = @('utils/config.js', 'utils/session.js', 'utils/genealogy-context.js', 'utils/api.js', 'utils/md5.js', 'pages/auth/login.vue', 'pages/lineage/first-person.vue', 'pages/content/list.vue', 'pages/content/editor.vue')
foreach ($file in $requiredFiles) {
if (-not (Test-Path (Join-Path $root $file))) {
throw "Missing core-flow file: $file"
}
}
$config = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'utils/config.js')
foreach ($token in @('mode:', 'isMockMode')) {
if ($config -notmatch [regex]::Escape($token)) {
throw "Missing runtime mode contract: $token"
}
}
$context = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'utils/genealogy-context.js')
foreach ($method in @('getCurrentGenealogyId', 'setCurrentGenealogyId', 'clearCurrentGenealogyId')) {
if ($context -notmatch [regex]::Escape($method)) {
throw "Missing genealogy context method: $method"
}
}
$api = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'utils/api.js')
foreach ($method in @('unwrapResponse', 'sendSmsCode', 'loginWithPassword', 'loginWithSms')) {
if ($api -notmatch [regex]::Escape($method)) {
throw "Missing auth API method: $method"
}
}
foreach ($method in @('createPerson', 'getPerson')) {
if ($api -notmatch [regex]::Escape($method)) {
throw "Missing lineage API method: $method"
}
}
foreach ($method in @('getArticles', 'getAlbums', 'getCeremonies', 'getGrowthRecords', 'createFeed')) {
if ($api -notmatch [regex]::Escape($method)) {
throw "Missing family content API method: $method"
}
}
if ($api -notmatch 'getProfile') {
throw 'Missing current-user profile API method'
}
$login = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/auth/login.vue')
foreach ($token in @('calcMD5', 'loginWithPassword', 'password', 'isSubmitting')) {
if ($login -notmatch [regex]::Escape($token)) {
throw "Missing password login behavior: $token"
}
}
$pages = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages.json')
if ($pages -notmatch 'pages/lineage/first-person') {
throw 'Missing first-person page route'
}
foreach ($route in @('pages/content/list', 'pages/content/editor')) {
if ($pages -notmatch [regex]::Escape($route)) {
throw "Missing content page route: $route"
}
}
foreach ($pageFile in @('pages/genealogy/detail.vue', 'pages/tree/index.vue', 'pages/member/detail.vue')) {
$page = Get-Content -Raw -Encoding UTF8 (Join-Path $root $pageFile)
if ($page -match "id=1001|id=3") {
throw "Hard-coded route ID remains in $pageFile"
}
if ($page -notmatch 'genealogyContext') {
throw "Missing genealogy context in $pageFile"
}
}
foreach ($method in @('markNotificationRead', 'markAllNotificationsRead')) {
if ($api -notmatch [regex]::Escape($method)) {
throw "Missing notification API method: $method"
}
}
foreach ($pageFile in @('pages/genealogy/applications.vue', 'pages/notification/index.vue')) {
$page = Get-Content -Raw -Encoding UTF8 (Join-Path $root $pageFile)
if ($page -match '1001') {
throw "Hard-coded genealogy ID remains in $pageFile"
}
}
$applications = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/genealogy/applications.vue')
if ($applications -notmatch 'genealogyContext') {
throw 'Missing genealogy context in applications page'
}
$notifications = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/notification/index.vue')
foreach ($method in @('markNotificationRead', 'markAllNotificationsRead')) {
if ($notifications -notmatch [regex]::Escape($method)) {
throw "Notification page does not use $method"
}
}
$family = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/family/index.vue')
foreach ($token in @('genealogyContext', 'openSection', 'pages/content/list', 'pages/content/editor')) {
if ($family -notmatch [regex]::Escape($token)) {
throw "Missing family content flow: $token"
}
}
$profile = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/profile/index.vue')
foreach ($token in @('appApi', 'getProfile', 'profile.name')) {
if ($profile -notmatch [regex]::Escape($token)) {
throw "Missing dynamic profile behavior: $token"
}
}
Write-Output 'PASS core-flow session and auth contract'

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