完成70%

This commit is contained in:
2026-07-24 07:56:22 +08:00
parent bb6431b319
commit c7f278fe79
92 changed files with 4741 additions and 14440 deletions
+4 -718
View File
@@ -1,718 +1,4 @@
<!-- 页面编号G-06用途搜索家谱邀请码加入双模式定位目标家谱 -->
<template>
<view class="search-page">
<GenealogyPageBackground />
<view class="search-page__header"><PageHeader title="加入家谱" /></view>
<view class="search-page__content">
<view class="mode-tabs">
<view
class="mode-tab"
:class="{ 'mode-tab--active': mode === 'search' }"
@click="switchMode('search')"
>搜索家谱</view
>
<view
class="mode-tab"
:class="{ 'mode-tab--active': mode === 'invite' }"
@click="switchMode('invite')"
>邀请码加入</view
>
</view>
<template v-if="mode === 'search'">
<view class="search-controls">
<view class="search-field">
<input
v-model.trim="keyword"
class="search-input"
placeholder="姓氏、谱名、地区或堂号"
placeholder-class="search-input__placeholder"
@confirm="search"
/>
<view v-if="keyword" class="search-clear" @click="clearSearch"
>清空</view
>
</view>
<view class="search-action" @click="search">
<text class="search-action__label">搜索</text>
</view>
</view>
<view class="filter-row">
<text class="filter-label">地区</text>
<view
v-for="area in areas"
:key="area"
class="filter-chip"
:class="{ 'filter-chip--active': selectedArea === area }"
@click="selectedArea = area"
>{{ area }}</view
>
</view>
<image
class="search-divider"
src="/static/assets/modules/genealogy/transparent/section-divider.png"
mode="scaleToFill"
/>
<AppLoading
v-if="searchState === 'loading'"
variant="section"
text="正在检索公开家谱"
description="请稍候,正在整理匹配结果。"
/>
<view
v-else-if="searchState === 'initial'"
class="search-status search-initial"
>
<text class="search-status__lead">先确认家族是否已有家谱</text>
<text class="search-status__copy"
>可按姓氏谱名地区或堂号查找避免重复创建</text
>
<image
class="search-status__hall"
src="/static/assets/foundation/transparent/root-header-hall.png"
mode="aspectFit"
/>
</view>
<view v-else-if="searchState === 'results'" class="search-results">
<view class="search-results__head"
><text>检索到 {{ results.length }} 部家谱</text
><text>请核对支系与管理信息</text></view
>
<view
v-for="item in results"
:key="item.id"
class="result-card genealogy-card"
@click="openPreview(item)"
>
<view class="result-card__content">
<view class="result-card__head">
<view>
<text class="result-card__name">{{ item.name }}</text>
<text class="result-card__identity"
>{{ item.surname }} · {{ item.hall }}</text
>
</view>
<text
class="result-card__relation"
:class="`result-card__relation--${item.relation}`"
>{{ resultRelationLabel(item) }}</text
>
</view>
<view class="result-card__facts">
<text>地区{{ item.location }}</text>
<text>所属上级谱{{ item.parentName }}</text>
<text>当前支系{{ item.branchName }}</text>
<text
>管理信息{{ item.manager }} · {{ item.certification }}</text
>
<text
>{{ item.memberCount }} 位成员 · 更新于
{{ item.updatedAt }}</text
>
</view>
<view
v-if="resultActionLabel(item)"
class="result-card__action"
@click.stop="handleResultAction(item)"
>{{ resultActionLabel(item) }}</view
>
</view>
</view>
</view>
<view
v-else-if="searchState === 'empty'"
class="search-status search-empty"
>
<image
class="search-status__cloud"
src="/static/assets/modules/genealogy/transparent/create-cloud.png"
mode="aspectFit"
/>
<text class="search-status__lead">暂未找到匹配家谱</text>
<text class="search-status__copy"
>可调整地区或关键词后重试也可使用邀请码加入</text
>
</view>
<view v-else class="search-status search-error">
<text class="search-status__lead">检索暂时失败</text>
<text class="search-status__copy"
>当前仅展示失败样式请稍后重新搜索</text
>
<view class="status-retry" @click="search">
<text>重新搜索</text>
</view>
</view>
</template>
<template v-else>
<view class="invite-block">
<text class="invite-title">输入家谱邀请码</text>
<text class="invite-copy"
>邀请码用于定位指定家谱加入前仍需确认真实姓名和关系</text
>
<view class="invite-controls">
<view class="invite-field">
<input
v-model.trim="inviteCode"
class="invite-input"
maxlength="12"
placeholder="请输入邀请码"
placeholder-class="search-input__placeholder"
@input="resetInvite"
/>
</view>
<view class="search-action" @click="verifyInvite">
<text class="search-action__label">验证</text>
</view>
</view>
<text class="invite-demo">样式验证可输入 JP2026</text>
</view>
<image
class="search-divider"
src="/static/assets/modules/genealogy/transparent/section-divider.png"
mode="scaleToFill"
/>
<view
v-if="inviteState === 'initial'"
class="search-status invite-initial"
>
<text class="search-status__lead">通过邀请码直接定位家谱</text>
<text class="search-status__copy"
>验证有效后仅显示目标家谱仍需填写身份关系本地验证不会变更成员身份</text
>
</view>
<view
v-else-if="inviteState === 'invalid'"
class="search-status invite-invalid"
>
<text class="search-status__lead">邀请码无效或已过期</text>
<text class="search-status__copy"
>请核对邀请码或向家谱管理员重新获取</text
>
</view>
<view v-else class="invite-result">
<text class="invite-result__label">已定位目标家谱</text>
<view class="result-card genealogy-card">
<view class="result-card__content">
<text class="result-card__name">{{ inviteTarget.name }}</text>
<view class="result-card__facts">
<text
>{{ inviteTarget.surname }} · {{ inviteTarget.hall }} ·
{{ inviteTarget.location }}</text
>
<text>所属上级谱{{ inviteTarget.parentName }}</text>
<text>当前支系{{ inviteTarget.branchName }}</text>
<text
>{{ inviteTarget.manager }} ·
{{ inviteTarget.certification }}</text
>
</view>
<view class="result-card__action" @click="confirmInvite"
>填写关系信息</view
>
</view>
</view>
<text class="invite-result__note">当前为样式验证不会变更成员身份</text>
</view>
</template>
</view>
</view>
</template>
<script setup>
import { computed, ref } from "vue";
import { onLoad, onUnload } from "@dcloudio/uni-app";
import AppLoading from "@/components/AppLoading.vue";
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
getGenealogyFixtureAccess,
isGenealogySearchVisible,
publicGenealogies,
} from "@/data/mock.js";
import { goRoot, openPage } from "@/utils/navigation.js";
const mode = ref("search");
const keyword = ref("");
const selectedArea = ref("全部");
const searchState = ref("initial");
const inviteCode = ref("");
const inviteState = ref("initial");
const results = ref([]);
let searchTimer = null;
const invalidateSearch = () => {
const timer = searchTimer;
searchTimer = null;
if (timer) clearTimeout(timer);
};
const areas = ["全部", "河南", "山东"];
const inviteTarget = computed(() => publicGenealogies[0]);
const syncModeFromRoute = (query = {}) => {
mode.value = query?.mode === "invite" ? "invite" : "search";
if (mode.value === "search" && query?.state === "loading")
searchState.value = "loading";
};
onLoad((query) => syncModeFromRoute(query));
onUnload(invalidateSearch);
const switchMode = (nextMode) => {
invalidateSearch();
mode.value = nextMode;
results.value = [];
searchState.value = "initial";
inviteState.value = "initial";
};
const search = () => {
invalidateSearch();
searchState.value = "loading";
const value = keyword.value.trim();
const area = selectedArea.value;
const timer = setTimeout(() => {
if (searchTimer !== timer || mode.value !== "search") return;
searchTimer = null;
if (value === "失败") {
searchState.value = "error";
return;
}
results.value = publicGenealogies.filter((item) => {
const matchesKeyword =
!value ||
`${item.name}${item.surname}${item.location}${item.hall}`.includes(
value,
);
const matchesArea =
area === "全部" || item.location.includes(area);
return isGenealogySearchVisible(String(item.id)) && matchesKeyword && matchesArea;
});
searchState.value = results.value.length ? "results" : "empty";
}, 260);
searchTimer = timer;
};
const clearSearch = () => {
invalidateSearch();
keyword.value = "";
results.value = [];
searchState.value = "initial";
};
const openPreview = (item) => {
if (
item.relation === "available" &&
getGenealogyFixtureAccess(String(item.id)).canApply
) {
return openPage(
"G05",
{ genealogyId: String(item.id) },
"G06",
);
}
return false;
};
const resultActionLabel = (item) => {
if (item.relation === "pending") return "查看申请进度";
if (item.relation === "joined") return "切换到该家谱";
if (item.relation === "owned") return "进入我的家谱";
if (!getGenealogyFixtureAccess(String(item.id)).canApply) return "";
return {
available: "申请加入",
rejected: "修改后重新申请",
removed: "重新申请",
}[item.relation] || "";
};
const resultRelationLabel = (item) =>
({
available: "可申请",
joined: "已加入",
pending: "审核中",
rejected: "已拒绝",
removed: "已退出",
owned: "我创建的",
})[item.relation] || "关系待确认";
const handleResultAction = (item) => {
if (item.relation === "pending")
return openPage("G09", { status: "pending" }, "G06");
if (
["available", "rejected", "removed"].includes(item.relation) &&
getGenealogyFixtureAccess(String(item.id)).canApply
)
return openPage(
"G08",
{ genealogyId: String(item.id), source: "search" },
"G06",
);
if (item.relation === "joined" || item.relation === "owned")
return goRoot("G01", { genealogyId: String(item.id) });
return false;
};
const verifyInvite = () => {
inviteState.value =
inviteCode.value.toUpperCase() === "JP2026" ? "valid" : "invalid";
};
const resetInvite = () => {
inviteState.value = "initial";
};
const confirmInvite = () => {
if (inviteState.value !== "valid") return false;
if (inviteCode.value.trim().toUpperCase() !== "JP2026") return false;
return openPage(
"G08",
{
genealogyId: String(inviteTarget.value.id),
source: "invite",
},
"G06",
);
};
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.search-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.search-page__header {
z-index: 2;
}
.search-page__content {
z-index: 3;
min-height: calc(100vh - 104rpx);
padding: 30rpx 24rpx calc(58rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.mode-tabs {
display: flex;
width: 500rpx;
margin: 30rpx auto 0;
border-bottom: 1rpx solid rgba(181, 138, 75, 0.45);
}
.mode-tab {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
min-height: 70rpx;
color: $ink-muted;
font-family: "STKaiti", "KaiTi", serif;
font-size: 28rpx;
}
.mode-tab--active {
margin-bottom: -2rpx;
background: linear-gradient($brand-red, $brand-red) center bottom /
calc(100% - 84rpx) 4rpx no-repeat;
color: $brand-red;
font-weight: 700;
}
.search-controls {
display: flex;
align-items: center;
gap: 20rpx;
margin-top: 32rpx;
}
.invite-controls {
display: flex;
align-items: center;
gap: 20rpx;
margin-top: 32rpx;
}
.search-field {
@include adaptive.adaptive-g06-search-field;
display: grid;
width: 430rpx;
min-height: 95rpx;
}
.search-input {
z-index: 1;
grid-area: 1 / 1;
width: 100%;
height: 100%;
padding: 0 82rpx 0 30rpx;
box-sizing: border-box;
color: $ink;
font-size: 25rpx;
}
.search-input__placeholder {
color: #ab9c83;
}
.search-clear {
z-index: 2;
display: flex;
grid-area: 1 / 1;
justify-self: end;
height: 100%;
align-items: center;
margin-right: 20rpx;
color: $brand-red;
font-size: 21rpx;
}
.search-action {
@include adaptive.adaptive-g06-search-action;
display: flex;
width: 200rpx;
min-height: 88rpx;
align-items: center;
justify-content: center;
}
.search-action__label {
z-index: 1;
color: #fff4dc;
font-family: "STKaiti", "KaiTi", serif;
font-size: 31rpx;
font-weight: 700;
letter-spacing: 5rpx;
}
.filter-row {
display: flex;
align-items: center;
gap: 12rpx;
margin: 16rpx 14rpx 0;
}
.filter-label {
color: $ink-muted;
font-size: 22rpx;
}
.filter-chip {
display: flex;
min-width: 82rpx;
min-height: 52rpx;
align-items: center;
justify-content: center;
color: $ink-muted;
font-size: 22rpx;
}
.filter-chip--active {
color: $brand-red;
font-weight: 700;
}
.search-divider {
display: block;
width: 100%;
height: 30rpx;
margin: 24rpx 0 24rpx;
}
.search-status {
display: flex;
min-height: 240rpx;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16rpx 28rpx 24rpx;
box-sizing: border-box;
text-align: center;
}
.search-status__cloud {
width: 92rpx;
height: 46rpx;
margin-bottom: 4rpx;
opacity: 0.78;
}
.search-status__lead {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 30rpx;
font-weight: 700;
letter-spacing: 2rpx;
}
.search-status__copy {
margin-top: 12rpx;
color: $ink-muted;
font-size: 22rpx;
line-height: 36rpx;
}
.search-status__hall {
width: 330rpx;
height: 132rpx;
margin-top: 210rpx;
opacity: 0.22;
filter: grayscale(1);
}
.status-retry {
display: flex;
width: 300rpx;
min-height: 76rpx;
align-items: center;
justify-content: center;
margin-top: 24rpx;
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
center / contain no-repeat;
}
.status-retry text {
z-index: 1;
color: #fff9ed;
font-size: 25rpx;
font-weight: 700;
}
.search-results {
display: flex;
flex-direction: column;
gap: 16rpx;
margin-top: 4rpx;
}
.search-results__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16rpx;
color: $ink-muted;
font-size: 21rpx;
}
.search-results__head text:first-child {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 27rpx;
font-weight: 700;
}
.result-card {
@include adaptive.adaptive-genealogy-list-card;
min-height: 314rpx;
box-sizing: border-box;
padding: 28rpx 30rpx;
}
.result-card__content {
z-index: 1;
}
.result-card__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.result-card__name,
.result-card__identity {
display: block;
}
.result-card__name {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 33rpx;
font-weight: 700;
}
.result-card__identity {
margin-top: 5rpx;
color: $ink-muted;
font-size: 22rpx;
}
.result-card__relation {
flex: 0 0 auto;
max-width: 190rpx;
margin-left: 12rpx;
color: #9a641f;
font-size: 21rpx;
text-align: right;
}
.result-card__relation--rejected,
.result-card__relation--removed {
color: $brand-red;
}
.result-card__facts {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8rpx 18rpx;
margin-top: 16rpx;
color: $ink-muted;
font-size: 23rpx;
line-height: 32rpx;
}
.result-card__facts text:nth-child(4),
.result-card__facts text:nth-child(5) {
grid-column: 1 / -1;
}
.result-card__action {
display: flex;
min-height: 58rpx;
align-items: center;
justify-content: center;
margin-top: 14rpx;
color: $brand-red;
font-family: "STKaiti", "KaiTi", serif;
font-size: 27rpx;
font-weight: 700;
}
.invite-block {
padding: 24rpx 18rpx 0;
}
.invite-title {
display: block;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 31rpx;
font-weight: 700;
}
.invite-copy {
display: block;
margin-top: 10rpx;
color: $ink-muted;
font-size: 22rpx;
line-height: 35rpx;
}
.invite-field {
@include adaptive.adaptive-g06-search-field;
display: grid;
flex: 1;
min-height: 95rpx;
}
.invite-input {
z-index: 1;
grid-area: 1 / 1;
width: 100%;
height: 100%;
padding: 0 30rpx;
box-sizing: border-box;
color: $ink;
font-size: 25rpx;
letter-spacing: 3rpx;
}
.invite-demo {
display: block;
margin-top: 8rpx;
color: #9b876f;
font-size: 19rpx;
}
.invite-result__label {
display: block;
margin: 0 12rpx 12rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 27rpx;
font-weight: 700;
}
.invite-result__note {
display: block;
margin-top: 12rpx;
color: $brand-red;
font-size: 22rpx;
text-align: center;
}
@media screen and (max-width: 340px) {
.search-page__content {
padding-right: 20rpx;
padding-left: 20rpx;
}
.search-field {
width: 430rpx;
}
.result-card {
padding-right: 24rpx;
padding-left: 24rpx;
}
.result-card__facts {
grid-template-columns: 1fr;
}
.result-card__facts text {
grid-column: 1 !important;
}
}
</style>
<!-- 页面编号G-06用途搜索公开家谱公开列表 item DTO 未声明时保持关闭 -->
<template><view class="search-page"><GenealogyPageBackground /><view class="page-header"><PageHeader title="搜索家谱" custom-back @back="backToGenealogies" /></view><view class="page-content"><view class="state-card"><text>公开家谱搜索待后端字段合同</text><text>公开家谱读取没有声明家谱 ID名称地区简介成员数或可申请状态字段页面不再展示 fixture 搜索结果也不从猜测条目进入申请</text><AppButton block label="返回我的家谱" @click="backToGenealogies" /></view></view></view></template>
<script setup>import AppButton from "@/components/AppButton.vue";import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";import PageHeader from "@/components/PageHeader.vue";import {returnTo} from "@/utils/navigation.js";const backToGenealogies=()=>returnTo("G01");</script>
<style scoped lang="scss">@use "../../styles/adaptive-frame-profiles.scss" as adaptive;.search-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-header,.page-content{z-index:1}.page-content{padding:18rpx 24rpx 72rpx}.state-card{box-sizing:border-box;min-height:340rpx;padding:78rpx 52rpx 50rpx;text-align:center;@include adaptive.adaptive-genealogy-state-panel}.state-card text{display:block}.state-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:35rpx;font-weight:700}.state-card text:nth-child(2){margin-top:18rpx;color:$ink-muted;font-size:24rpx;line-height:1.65}.state-card .app-button{margin-top:28rpx}</style>