家谱APP首页样式设计完成,落地80%
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user