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

291 lines
20 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="search-title-strip">
<image class="search-title-strip__skin" src="/static/assets/modules/genealogy/opaque/g06-search-title-strip.png" mode="scaleToFill" />
<image class="search-title-strip__seal" src="/static/assets/foundation/transparent/brand-seal.png" mode="aspectFit" />
<text class="search-title-strip__text">加入家谱</text>
</view>
<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">
<image class="search-field__skin" src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" />
<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">
<image class="search-action__skin" src="/static/assets/modules/genealogy/opaque/g06-search-button.png" mode="scaleToFill" />
<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" />
<view v-if="searchState === 'loading'" class="search-status search-loading">
<text class="search-status__lead">正在检索公开家谱</text>
<text class="search-status__copy">请稍候正在整理匹配结果</text>
</view>
<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)">
<image class="result-card__skin" src="/static/assets/modules/genealogy/transparent/list-slip-frame.png" mode="scaleToFill" />
<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}`">{{ item.actionLabel }}</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 class="result-card__action" @click.stop="handleResultAction(item)">{{ item.actionLabel }}</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">重新搜索</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">
<image class="search-field__skin" src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" />
<input v-model.trim="inviteCode" class="invite-input" maxlength="12" placeholder="请输入邀请码" placeholder-class="search-input__placeholder" />
</view>
<view class="search-action" @click="verifyInvite">
<image class="search-action__skin" src="/static/assets/modules/genealogy/opaque/g06-search-button.png" mode="scaleToFill" />
<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">
<image class="result-card__skin" src="/static/assets/modules/genealogy/transparent/list-slip-frame.png" mode="scaleToFill" />
<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, onShow } from '@dcloudio/uni-app'
import GenealogyPageBackground from '@/components/GenealogyPageBackground.vue'
import PageHeader from '@/components/PageHeader.vue'
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 areas = ['全部', '河南', '山东']
const resultFixtures = [
{ id: 2001, surname: '汤', name: '汤氏南阳宗谱', hall: '敦睦堂', location: '河南·南阳', parentName: '汤氏中华总谱', branchName: '南阳主支', manager: '管理员 汤文礼', certification: '资料已认证', memberCount: 428, updatedAt: '2026-07-12', relation: 'available', actionLabel: '申请加入' },
{ id: 2002, surname: '汤', name: '汤氏洛阳家谱', hall: '承志堂', location: '河南·洛阳', parentName: '汤氏中华总谱', branchName: '洛阳二支', manager: '管理员 汤文远', certification: '资料已认证', memberCount: 158, updatedAt: '2026-07-10', relation: 'joined', actionLabel: '已加入' },
{ id: 2003, surname: '汤', name: '汤氏济宁宗谱', hall: '敬宗堂', location: '山东·济宁', parentName: '汤氏鲁西总谱', branchName: '济宁主支', manager: '管理员 汤正明', certification: '管理员已实名', memberCount: 286, updatedAt: '2026-07-08', relation: 'pending', actionLabel: '审核中' },
{ id: 2004, surname: '汤', name: '汤氏清河家谱', hall: '思源堂', location: '山东·临清', parentName: '汤氏鲁西总谱', branchName: '清河支系', manager: '管理员 汤志成', certification: '资料已认证', memberCount: 96, updatedAt: '2026-07-05', relation: 'rejected', actionLabel: '修改后重新申请' },
{ id: 2005, surname: '汤', name: '汤氏汝南支谱', hall: '崇本堂', location: '河南·驻马店', parentName: '汤氏中原总谱', branchName: '汝南三支', manager: '管理员 汤国安', certification: '管理员已实名', memberCount: 72, updatedAt: '2026-07-02', relation: 'removed', actionLabel: '重新申请' },
{ id: 2006, surname: '汤', name: '汤氏新安家谱', hall: '继善堂', location: '河南·三门峡', parentName: '无上级谱', branchName: '新安主支', manager: '创建者 当前用户', certification: '资料待完善', memberCount: 34, updatedAt: '2026-06-28', relation: 'owned', actionLabel: '我创建的' }
]
const inviteTarget = computed(() => resultFixtures[0])
const syncModeFromRoute = () => {
let requestedMode = 'search'
if (typeof location !== 'undefined') {
requestedMode = new URLSearchParams(location.hash.split('?')[1] || '').get('mode') || 'search'
} else {
const pages = getCurrentPages()
requestedMode = pages[pages.length - 1]?.options?.mode || 'search'
}
mode.value = requestedMode === 'invite' ? 'invite' : 'search'
}
onLoad(syncModeFromRoute)
onShow(syncModeFromRoute)
onUnload(() => { if (searchTimer) clearTimeout(searchTimer) })
const switchMode = (nextMode) => {
mode.value = nextMode
searchState.value = 'initial'
inviteState.value = 'initial'
}
const search = () => {
if (searchTimer) clearTimeout(searchTimer)
searchState.value = 'loading'
searchTimer = setTimeout(() => {
const value = keyword.value.trim()
if (value === '失败') {
searchState.value = 'error'
return
}
results.value = resultFixtures.filter((item) => {
const matchesKeyword = !value || `${item.name}${item.surname}${item.location}${item.hall}`.includes(value)
const matchesArea = selectedArea.value === '全部' || item.location.includes(selectedArea.value)
return matchesKeyword && matchesArea
})
searchState.value = results.value.length ? 'results' : 'empty'
searchTimer = null
}, 260)
}
const clearSearch = () => {
keyword.value = ''
results.value = []
searchState.value = 'initial'
}
const openPreview = (item) => {
if (item.relation === 'available') {
uni.navigateTo({ url: `/pages/genealogy/g05-genealogy-overview?mode=public&genealogyId=${item.id}` })
}
}
const handleResultAction = (item) => {
if (item.relation === 'available') return uni.navigateTo({ url: `/pages/genealogy/g08-join-application?source=search&genealogyId=${item.id}` })
if (item.relation === 'pending') return uni.navigateTo({ url: '/pages/genealogy/g09-my-applications?status=pending' })
if (item.relation === 'rejected' || item.relation === 'removed') return uni.navigateTo({ url: `/pages/genealogy/g08-join-application?source=search&previous=${item.relation}&genealogyId=${item.id}` })
return uni.reLaunch({ url: `/pages/genealogy/g01-my-genealogies?genealogyId=${item.id}` })
}
const verifyInvite = () => {
inviteState.value = inviteCode.value.toUpperCase() === 'JP2026' ? 'valid' : 'invalid'
}
const confirmInvite = () => uni.navigateTo({ url: `/pages/genealogy/g08-join-application?source=invite&genealogyId=${inviteTarget.value.id}` })
</script>
<style scoped lang="scss">
.search-page { position: relative; min-height: 100vh; overflow-x: hidden; background: $paper; }
.search-page__header { position: relative; z-index: 2; }
.search-page__content { position: relative; z-index: 3; min-height: calc(100vh - 104rpx); padding: 30rpx 24rpx calc(58rpx + env(safe-area-inset-bottom)); box-sizing: border-box; }
.search-title-strip { position: relative; width: 100%; height: 138rpx; }
.search-title-strip__skin { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.search-title-strip__seal { position: absolute; top: 27rpx; left: 52rpx; z-index: 1; width: 72rpx; height: 84rpx; }
.search-title-strip__text { position: absolute; inset: 0 170rpx 0 156rpx; z-index: 1; display: flex; align-items: center; justify-content: center; color: #f5d596; font-family: 'STKaiti', 'KaiTi', serif; font-size: 39rpx; font-weight: 700; letter-spacing: 5rpx; }
.mode-tabs { display: flex; width: 500rpx; margin: 30rpx auto 0; border-bottom: 1rpx solid rgba(181, 138, 75, .45); }
.mode-tab { position: relative; 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 { color: $brand-red; font-weight: 700; }
.mode-tab--active::after { position: absolute; right: 42rpx; bottom: -2rpx; left: 42rpx; height: 4rpx; background: $brand-red; content: ''; }
.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 { position: relative; width: 430rpx; height: 95rpx; }
.search-field__skin,
.search-action__skin { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.search-input { position: relative; z-index: 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 { position: absolute; z-index: 2; top: 0; right: 20rpx; display: flex; height: 100%; align-items: center; color: $brand-red; font-size: 21rpx; }
.search-action { position: relative; display: flex; width: 200rpx; height: 88rpx; align-items: center; justify-content: center; }
.search-action__label { position: relative; 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: .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: .22; filter: grayscale(1); }
.status-retry { display: flex; min-height: 70rpx; align-items: center; margin-top: 18rpx; color: $brand-red; font-size: 24rpx; }
.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 { position: relative; min-height: 314rpx; box-sizing: border-box; padding: 28rpx 30rpx; overflow: hidden; }
.result-card__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.result-card__content { position: relative; 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 { position: relative; flex: 1; height: 95rpx; }
.invite-input { position: relative; z-index: 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-title-strip__seal { left: 44rpx; }
.search-title-strip__text { right: 148rpx; left: 138rpx; font-size: 37rpx; }
.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>