291 lines
20 KiB
Vue
291 lines
20 KiB
Vue
<!-- 页面编号: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>
|