家谱APP首页样式设计完成,落地80%
@@ -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.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
onLaunch() {
|
||||||
|
console.log('家谱 App 已启动')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import './styles/global.scss';
|
||||||
|
</style>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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' }
|
||||||
|
]
|
||||||
@@ -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。
|
||||||
|
- 暂不实现为可用功能:邀请码、视频与多管理员分级权限;接口文档尚未提供对应端点。
|
||||||
@@ -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` | 图标视觉大小可为 `40–48rpx`。 |
|
||||||
|
| 列表行高度 | `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` 状态轴和 20–48dp 光学尺寸;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 核心家谱页面壳
|
||||||
|
|
||||||
|
适用于:家谱总览、世系图、成员目录、人物档案。
|
||||||
|
|
||||||
|
- 顶部 `256–288rpx`:朱砂红,左下放置低对比祠堂屋檐线描,禁止使用满屏建筑照片。
|
||||||
|
- 根页面左上角放项目 Logo(祠堂屋檐 + “谱”字印章,透明 PNG,视觉 `40rpx`、触控区 `88rpx`);详情页该位置优先保留返回按钮,Logo 收入建筑纹样中的小印记。
|
||||||
|
- 搜索、通知、更多均为 `88rpx` 触控框;图标为暖白或金色线稿 PNG。通知铃铛仅在未读消息数大于 0 时显示朱砂小圆点;未读为 0 时不显示圆点和数字。
|
||||||
|
- 家谱名称位于视觉中心,最大两行;名称下可放地域或代系提示,但不能与图标重叠。
|
||||||
|
- 红头结束后立即进入纸面 Tab/分段导航,使用古金底线标记当前项。
|
||||||
|
|
||||||
|
### 4.2 普通列表与详情页面壳
|
||||||
|
|
||||||
|
适用于:家族动态、谱文、相册、人物录、礼仪、消息、我的。
|
||||||
|
|
||||||
|
- 顶部使用 `144–176rpx` 紧凑朱砂题签,保留建筑线描的局部纹样。
|
||||||
|
- 内容区采用纸白承载面与古金细分割线,列表项不使用夸张圆角;圆角最大 `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 新截图完成视觉验收。
|
||||||
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
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 2–3.
|
||||||
|
- 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 1–3 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 1–2 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 宽度装不下,按本文件定义隐藏更新时间,而不是压缩全部文字。
|
||||||
@@ -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 提供同尺寸截图复核。
|
||||||
@@ -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 打包。
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
import { createSSRApp } from 'vue'
|
||||||
|
|
||||||
|
export function createApp() {
|
||||||
|
const app = createSSRApp(App)
|
||||||
|
return { app }
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 875 B |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -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;
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
@@ -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'
|
||||||