Files
jiapuapp/pages/genealogy/g06-search-genealogies.vue
T
2026-07-23 08:24:07 +08:00

719 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 页面编号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>