完成40%
This commit is contained in:
+91
-11
@@ -62,7 +62,19 @@
|
||||
<image class="login-submit__skin" src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
|
||||
<text class="login-submit__content">登录</text>
|
||||
</view>
|
||||
<text class="agreement-reminder">请先在登录引导页阅读并确认协议</text>
|
||||
<view class="agreement-row" @click="toggleAgreement">
|
||||
<image class="agreement-icon" :src="agreed ? '/static/assets/modules/auth/transparent/a02-agreement-checked.png' : '/static/assets/modules/auth/transparent/a02-agreement-unchecked.png'" mode="aspectFit" />
|
||||
<view class="agreement-copy">
|
||||
<text>我已阅读并同意</text>
|
||||
<text class="agreement-link" @click.stop="prepareAgreement">《用户协议》</text>
|
||||
<text>与</text>
|
||||
<text class="agreement-link" @click.stop="prepareAgreement">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="register-entry">
|
||||
<text>还没有账号?</text>
|
||||
<view class="register-link" hover-class="text-hover" @click="prepareRegister">注册账号</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -71,32 +83,61 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
|
||||
const activeTab = ref('password')
|
||||
const account = ref('')
|
||||
const password = ref('')
|
||||
const smsPhone = ref('')
|
||||
const verificationCode = ref('')
|
||||
const agreedFromEntry = ref(false)
|
||||
const agreed = ref(false)
|
||||
|
||||
onLoad((options) => {
|
||||
agreedFromEntry.value = options?.agreed === '1'
|
||||
const isAgreedRoute = () => {
|
||||
if (typeof location !== 'undefined') {
|
||||
const query = new URLSearchParams(location.hash.split('?')[1] || '')
|
||||
return query.get('agreed') === '1'
|
||||
}
|
||||
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
return currentPage?.options?.agreed === '1'
|
||||
}
|
||||
|
||||
const syncAgreementFromRoute = () => {
|
||||
agreed.value = isAgreedRoute()
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
syncAgreementFromRoute()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
syncAgreementFromRoute()
|
||||
})
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
const prepareRegister = () => uni.navigateTo({ url: '/pages/auth/a04-register' })
|
||||
|
||||
const prepareForgotPassword = () => uni.navigateTo({ url: '/pages/auth/a05-reset-password' })
|
||||
|
||||
const prepareGetCode = () => {
|
||||
uni.showToast({ title: '获取验证码功能待接入', icon: 'none' })
|
||||
}
|
||||
|
||||
const toggleAgreement = () => {
|
||||
agreed.value = !agreed.value
|
||||
}
|
||||
|
||||
const prepareAgreement = () => {
|
||||
uni.showToast({ title: '协议页面准备中', icon: 'none' })
|
||||
}
|
||||
|
||||
const canSubmit = () => {
|
||||
if (agreedFromEntry.value) return true
|
||||
uni.showToast({ title: '请先返回登录引导确认协议', icon: 'none' })
|
||||
if (agreed.value) return true
|
||||
uni.showToast({ title: '请先阅读并同意相关协议', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -356,11 +397,50 @@ const submitLogin = () => {
|
||||
|
||||
.button-hover { opacity: 0.84; }
|
||||
|
||||
.agreement-reminder {
|
||||
display: block;
|
||||
margin-top: 34rpx;
|
||||
color: #9a8773;
|
||||
.agreement-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 88rpx;
|
||||
margin-top: 22rpx;
|
||||
padding: 0 4rpx;
|
||||
color: #6b5a4a;
|
||||
font-size: 24rpx;
|
||||
line-height: 32rpx;
|
||||
}
|
||||
|
||||
.agreement-icon {
|
||||
flex: 0 0 auto;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.agreement-copy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.agreement-link {
|
||||
color: #a7160c;
|
||||
}
|
||||
|
||||
.register-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 2rpx;
|
||||
color: #6b5a4a;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.register-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 88rpx;
|
||||
margin-left: 8rpx;
|
||||
color: #a7160c;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<!-- 页面编号:A-03;用途:手机验证码登录与授权回跳状态。 -->
|
||||
<template><ModulePage page-id="a03" /></template>
|
||||
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
|
||||
+345
-3
@@ -1,3 +1,345 @@
|
||||
<!-- 页面编号:A-04;用途:注册账号与服务协议确认。 -->
|
||||
<template><ModulePage page-id="a04" /></template>
|
||||
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
|
||||
<!-- 页面编号:A-04;用途:注册账号与本页服务协议确认。 -->
|
||||
<template>
|
||||
<view class="auth-page register-page">
|
||||
<image class="paper-background" src="/static/assets/foundation/opaque/auth-page-paper.jpg" mode="aspectFill" />
|
||||
<image class="scenery-background" src="/static/assets/foundation/transparent/auth-ink-scenery.png" mode="aspectFill" />
|
||||
|
||||
<view class="auth-content">
|
||||
<view class="auth-header">
|
||||
<image class="header-background" src="/static/assets/foundation/opaque/auth-header.png" mode="aspectFill" />
|
||||
<image class="header-seal" src="/static/assets/foundation/transparent/brand-seal.png" mode="aspectFit" />
|
||||
</view>
|
||||
|
||||
<view class="register-panel">
|
||||
<image class="register-panel__skin" src="/static/assets/modules/auth/opaque/a02-login-panel.png" mode="scaleToFill" />
|
||||
<view class="register-panel__content">
|
||||
<view class="page-heading">
|
||||
<image class="register-title-cloud" src="/static/assets/foundation/transparent/auth-title-cloud.png" mode="aspectFit" />
|
||||
<view class="back-button" hover-class="back-hover" @click="goBack">
|
||||
<image class="back-button__icon" src="/static/assets/foundation/transparent/chevron-right.png" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="page-heading__copy">
|
||||
<text class="page-title">注册账号</text>
|
||||
<text class="page-subtitle">创建属于你的家谱账号</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<image class="register-divider" src="/static/assets/foundation/transparent/auth-divider-knot.png" mode="aspectFit" />
|
||||
|
||||
<view class="form-content">
|
||||
<view class="input-row">
|
||||
<text class="input-label">手机号</text>
|
||||
<input v-model.trim="phone" class="auth-input" type="number" maxlength="11" placeholder="请输入手机号" placeholder-class="placeholder" />
|
||||
</view>
|
||||
<view class="input-row">
|
||||
<text class="input-label">设置密码</text>
|
||||
<input v-model="password" class="auth-input" password placeholder="请设置登录密码" placeholder-class="placeholder" />
|
||||
</view>
|
||||
<view class="input-row">
|
||||
<text class="input-label">确认密码</text>
|
||||
<input v-model="confirmPassword" class="auth-input" password placeholder="请再次输入密码" placeholder-class="placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="register-submit" hover-class="button-hover" @click="submitRegister">
|
||||
<image class="register-submit__skin" src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
|
||||
<text class="register-submit__content">注册账号</text>
|
||||
</view>
|
||||
|
||||
<view class="agreement-row" @click="toggleAgreement">
|
||||
<image class="agreement-icon" :src="agreed ? '/static/assets/modules/auth/transparent/a02-agreement-checked.png' : '/static/assets/modules/auth/transparent/a02-agreement-unchecked.png'" mode="aspectFit" />
|
||||
<view class="agreement-copy">
|
||||
<text>我已阅读并同意</text>
|
||||
<text class="agreement-link" @click.stop="prepareAgreement">《用户协议》</text>
|
||||
<text>与</text>
|
||||
<text class="agreement-link" @click.stop="prepareAgreement">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="login-entry">
|
||||
<text>已有账号?</text>
|
||||
<view class="login-entry__link" hover-class="text-hover" @click="prepareLogin">登录</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const phone = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const agreed = ref(false)
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
const prepareLogin = () => uni.navigateTo({ url: '/pages/auth/a02-login' })
|
||||
|
||||
const prepareAgreement = () => {
|
||||
uni.showToast({ title: '协议页面准备中', icon: 'none' })
|
||||
}
|
||||
|
||||
const toggleAgreement = () => {
|
||||
agreed.value = !agreed.value
|
||||
}
|
||||
|
||||
const submitRegister = () => {
|
||||
if (!agreed.value) {
|
||||
uni.showToast({ title: '请先阅读并同意相关协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!/^1\d{10}$/.test(phone.value)) {
|
||||
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!password.value) {
|
||||
uni.showToast({ title: '请设置密码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (password.value !== confirmPassword.value) {
|
||||
uni.showToast({ title: '两次输入的密码不一致', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '注册服务待接入', icon: 'none' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.auth-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background: #f7f1e6;
|
||||
}
|
||||
|
||||
.paper-background,
|
||||
.scenery-background {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.paper-background { opacity: 0.96; }
|
||||
.scenery-background { opacity: 0.76; }
|
||||
|
||||
.auth-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
min-height: 100vh;
|
||||
padding-bottom: calc(44rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
position: relative;
|
||||
height: 253rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header-seal {
|
||||
position: absolute;
|
||||
top: 70rpx;
|
||||
left: 50%;
|
||||
width: 136rpx;
|
||||
height: 136rpx;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.register-panel {
|
||||
position: relative;
|
||||
min-height: 1184rpx;
|
||||
margin: 32rpx 42rpx 0;
|
||||
}
|
||||
|
||||
.register-panel__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.register-panel__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
min-height: 1184rpx;
|
||||
padding: 66rpx 50rpx 76rpx;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.register-title-cloud {
|
||||
position: absolute;
|
||||
top: -26rpx;
|
||||
right: 2rpx;
|
||||
width: 154rpx;
|
||||
height: 92rpx;
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.page-heading__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
margin-right: 18rpx;
|
||||
}
|
||||
|
||||
.back-hover { opacity: 0.62; }
|
||||
|
||||
.back-button__icon {
|
||||
width: 42rpx;
|
||||
height: 42rpx;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.page-title,
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: #3f2c1d;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 50rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin-top: 10rpx;
|
||||
color: #8c7864;
|
||||
font-size: 24rpx;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.register-divider {
|
||||
display: block;
|
||||
width: 154rpx;
|
||||
height: 42rpx;
|
||||
margin: 40rpx auto 0;
|
||||
}
|
||||
|
||||
.form-content { margin-top: 30rpx; }
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 112rpx;
|
||||
border-bottom: 1rpx solid #e5d4b7;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
width: 154rpx;
|
||||
color: #674b35;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 31rpx;
|
||||
}
|
||||
|
||||
.auth-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 104rpx;
|
||||
color: #3f2c1d;
|
||||
font-size: 29rpx;
|
||||
}
|
||||
|
||||
.placeholder { color: #b3a895; }
|
||||
|
||||
.register-submit {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 128rpx;
|
||||
margin-top: 60rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.register-submit__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.register-submit__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #fffaf0;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 36rpx;
|
||||
letter-spacing: 6rpx;
|
||||
}
|
||||
|
||||
.button-hover { opacity: 0.84; }
|
||||
|
||||
.agreement-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 88rpx;
|
||||
margin-top: 22rpx;
|
||||
padding: 0 4rpx;
|
||||
color: #6b5a4a;
|
||||
font-size: 24rpx;
|
||||
line-height: 32rpx;
|
||||
}
|
||||
|
||||
.agreement-icon {
|
||||
flex: 0 0 auto;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.agreement-copy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.agreement-link { color: #a7160c; }
|
||||
|
||||
.login-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 2rpx;
|
||||
color: #6b5a4a;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.login-entry__link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 88rpx;
|
||||
margin-left: 8rpx;
|
||||
color: #a7160c;
|
||||
}
|
||||
|
||||
.text-hover { opacity: 0.64; }
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,328 @@
|
||||
<!-- 页面编号:A-05;用途:忘记密码与重设密码。 -->
|
||||
<template><ModulePage page-id="a05" /></template>
|
||||
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
|
||||
<!-- 页面编号:A-05;用途:通过手机号验证后重设登录密码。 -->
|
||||
<template>
|
||||
<view class="auth-page reset-page">
|
||||
<!-- 认证页背景复用已验收纸纹与淡墨山水,不绘制 CSS 装饰面。 -->
|
||||
<image class="paper-background" src="/static/assets/foundation/opaque/auth-page-paper.jpg" mode="aspectFill" />
|
||||
<image class="scenery-background" src="/static/assets/foundation/transparent/auth-ink-scenery.png" mode="aspectFill" />
|
||||
|
||||
<view class="auth-content">
|
||||
<!-- 朱砂祠堂头和谱印与 A02/A04 保持同一认证入口。 -->
|
||||
<view class="auth-header">
|
||||
<image class="header-background" src="/static/assets/foundation/opaque/auth-header.png" mode="aspectFill" />
|
||||
<image class="header-seal" src="/static/assets/foundation/transparent/brand-seal.png" mode="aspectFit" />
|
||||
</view>
|
||||
|
||||
<view class="reset-panel">
|
||||
<image class="reset-panel__skin" src="/static/assets/modules/auth/opaque/a02-login-panel.png" mode="scaleToFill" />
|
||||
<view class="reset-panel__content">
|
||||
<!-- 页标题只说明当前任务,不暴露内部页面编号。 -->
|
||||
<view class="page-heading">
|
||||
<image class="reset-title-cloud" src="/static/assets/foundation/transparent/auth-title-cloud.png" mode="aspectFit" />
|
||||
<view class="back-button" hover-class="back-hover" @click="goBack">
|
||||
<image class="back-button__icon" src="/static/assets/foundation/transparent/chevron-right.png" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="page-heading__copy">
|
||||
<text class="page-title">重设密码</text>
|
||||
<text class="page-subtitle">验证手机号后重新设置登录密码</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<image class="reset-divider" src="/static/assets/foundation/transparent/auth-divider-knot.png" mode="aspectFit" />
|
||||
|
||||
<!-- 四项真实输入在当前页完成身份验证与两次密码确认。 -->
|
||||
<view class="form-content">
|
||||
<view class="input-row">
|
||||
<text class="input-label">手机号</text>
|
||||
<input v-model.trim="phone" class="auth-input" type="number" maxlength="11" placeholder="请输入手机号" placeholder-class="placeholder" />
|
||||
</view>
|
||||
<view class="input-row code-row">
|
||||
<text class="input-label">验证码</text>
|
||||
<input v-model.trim="verificationCode" class="auth-input" type="number" maxlength="6" placeholder="请输入验证码" placeholder-class="placeholder" />
|
||||
<view class="get-code" hover-class="text-hover" @click="prepareGetCode">获取验证码</view>
|
||||
</view>
|
||||
<view class="input-row">
|
||||
<text class="input-label">新密码</text>
|
||||
<input v-model="password" class="auth-input" password placeholder="请设置新密码" placeholder-class="placeholder" />
|
||||
</view>
|
||||
<view class="input-row">
|
||||
<text class="input-label">确认新密码</text>
|
||||
<input v-model="confirmPassword" class="auth-input" password placeholder="请再次输入新密码" placeholder-class="placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="reset-submit" hover-class="button-hover" @click="submitReset">
|
||||
<image class="reset-submit__skin" src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
|
||||
<text class="reset-submit__content">确认重设</text>
|
||||
</view>
|
||||
|
||||
<text class="reset-tip">重设完成后,请使用新密码登录</text>
|
||||
<view class="login-entry">
|
||||
<text>想起密码了?</text>
|
||||
<view class="login-entry__link" hover-class="text-hover" @click="prepareLogin">返回登录</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const phone = ref('')
|
||||
const verificationCode = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
const prepareLogin = () => uni.navigateTo({ url: '/pages/auth/a02-login' })
|
||||
|
||||
const prepareGetCode = () => {
|
||||
uni.showToast({ title: '验证码功能待接入', icon: 'none' })
|
||||
}
|
||||
|
||||
const submitReset = () => {
|
||||
if (!/^1\d{10}$/.test(phone.value)) {
|
||||
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!/^\d{6}$/.test(verificationCode.value)) {
|
||||
uni.showToast({ title: '请输入 6 位验证码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!password.value) {
|
||||
uni.showToast({ title: '请设置新密码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (password.value !== confirmPassword.value) {
|
||||
uni.showToast({ title: '两次密码输入不一致', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '密码重设服务待接入', icon: 'none' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.auth-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background: #f7f1e6;
|
||||
}
|
||||
|
||||
.paper-background,
|
||||
.scenery-background {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.paper-background { opacity: 0.96; }
|
||||
.scenery-background { opacity: 0.76; }
|
||||
|
||||
.auth-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
min-height: 100vh;
|
||||
padding-bottom: calc(44rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
position: relative;
|
||||
height: 253rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header-seal {
|
||||
position: absolute;
|
||||
top: 70rpx;
|
||||
left: 50%;
|
||||
width: 136rpx;
|
||||
height: 136rpx;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.reset-panel {
|
||||
position: relative;
|
||||
min-height: 1240rpx;
|
||||
margin: 32rpx 42rpx 0;
|
||||
}
|
||||
|
||||
.reset-panel__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.reset-panel__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
min-height: 1240rpx;
|
||||
padding: 66rpx 50rpx 72rpx;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.reset-title-cloud {
|
||||
position: absolute;
|
||||
top: -26rpx;
|
||||
right: 2rpx;
|
||||
width: 154rpx;
|
||||
height: 92rpx;
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.page-heading__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
margin-right: 18rpx;
|
||||
}
|
||||
|
||||
.back-hover { opacity: 0.62; }
|
||||
|
||||
.back-button__icon {
|
||||
width: 42rpx;
|
||||
height: 42rpx;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.page-title,
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: #3f2c1d;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 50rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin-top: 10rpx;
|
||||
color: #8c7864;
|
||||
font-size: 24rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.reset-divider {
|
||||
display: block;
|
||||
width: 154rpx;
|
||||
height: 42rpx;
|
||||
margin: 40rpx auto 0;
|
||||
}
|
||||
|
||||
.form-content { margin-top: 30rpx; }
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 112rpx;
|
||||
border-bottom: 1rpx solid #e5d4b7;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
width: 174rpx;
|
||||
color: #674b35;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 31rpx;
|
||||
}
|
||||
|
||||
.auth-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 104rpx;
|
||||
color: #3f2c1d;
|
||||
font-size: 29rpx;
|
||||
}
|
||||
|
||||
.placeholder { color: #b3a895; }
|
||||
|
||||
.get-code {
|
||||
flex: 0 0 auto;
|
||||
padding-left: 18rpx;
|
||||
border-left: 1rpx solid #ddc7a3;
|
||||
color: #a7160c;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.reset-submit {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 128rpx;
|
||||
margin-top: 52rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.reset-submit__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.reset-submit__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #fffaf0;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 36rpx;
|
||||
letter-spacing: 6rpx;
|
||||
}
|
||||
|
||||
.button-hover { opacity: 0.84; }
|
||||
.text-hover { opacity: 0.64; }
|
||||
|
||||
.reset-tip {
|
||||
display: block;
|
||||
margin-top: 26rpx;
|
||||
color: #8c7864;
|
||||
font-size: 22rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 2rpx;
|
||||
color: #6b5a4a;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.login-entry__link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 82rpx;
|
||||
margin-left: 8rpx;
|
||||
color: #a7160c;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,371 @@
|
||||
<!-- 页面编号:A-06;用途:账号受限、登录失败与授权取消状态。 -->
|
||||
<template><ModulePage page-id="a06" /></template>
|
||||
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
|
||||
<!-- 页面编号:A-06;用途:承接登录未完成、失败、受限与授权结果。 -->
|
||||
<template>
|
||||
<view class="auth-page status-page">
|
||||
<!-- 认证入口沿用纸纹、淡墨与祠堂头图,不以 CSS 重绘完整装饰面。 -->
|
||||
<image class="paper-background" src="/static/assets/foundation/opaque/auth-page-paper.jpg" mode="aspectFill" />
|
||||
<image class="scenery-background" src="/static/assets/foundation/transparent/auth-ink-scenery.png" mode="aspectFill" />
|
||||
|
||||
<view class="auth-content">
|
||||
<view class="auth-header">
|
||||
<image class="header-background" src="/static/assets/foundation/opaque/auth-header.png" mode="aspectFill" />
|
||||
<image class="header-seal" src="/static/assets/foundation/transparent/brand-seal.png" mode="aspectFit" />
|
||||
</view>
|
||||
|
||||
<view class="status-panel">
|
||||
<image class="status-panel__skin" src="/static/assets/modules/auth/opaque/a02-login-panel.png" mode="scaleToFill" />
|
||||
<view class="status-panel__content">
|
||||
<view class="page-heading">
|
||||
<image class="status-title-cloud" src="/static/assets/foundation/transparent/auth-title-cloud.png" mode="aspectFit" />
|
||||
<view class="back-button" hover-class="back-hover" @click="goBack">
|
||||
<image class="back-button__icon" src="/static/assets/foundation/transparent/chevron-right.png" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="page-heading__copy">
|
||||
<text class="page-title">登录提示</text>
|
||||
<text class="page-subtitle">请确认当前状态后继续操作</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<image class="status-divider" src="/static/assets/foundation/transparent/auth-divider-knot.png" mode="aspectFit" />
|
||||
|
||||
<view class="status-body">
|
||||
<image class="status-icon" src="/static/assets/foundation/transparent/auth-login-outline.png" mode="aspectFit" />
|
||||
<text class="status-eyebrow">{{ currentState.eyebrow }}</text>
|
||||
<text class="status-title">{{ currentState.title }}</text>
|
||||
<text class="status-description">{{ currentState.description }}</text>
|
||||
</view>
|
||||
|
||||
<view class="status-primary" hover-class="button-hover" @click="handlePrimary">
|
||||
<image class="status-primary__skin" src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
|
||||
<text class="status-primary__copy">{{ currentState.primaryLabel }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="currentState.secondaryLabel" class="status-secondary" hover-class="text-hover" @click="goLogin">
|
||||
{{ currentState.secondaryLabel }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
|
||||
const status = ref('normal')
|
||||
|
||||
const statusConfig = {
|
||||
normal: {
|
||||
eyebrow: '登录提示',
|
||||
title: '登录未完成',
|
||||
description: '登录流程尚未完成,请选择一种方式继续。',
|
||||
primaryLabel: '返回登录',
|
||||
action: 'login',
|
||||
secondaryLabel: ''
|
||||
},
|
||||
failed: {
|
||||
eyebrow: '登录提示',
|
||||
title: '登录失败',
|
||||
description: '当前无法完成登录,请稍后重试或改用其他方式。',
|
||||
primaryLabel: '返回登录',
|
||||
action: 'login',
|
||||
secondaryLabel: ''
|
||||
},
|
||||
restricted: {
|
||||
eyebrow: '账号安全',
|
||||
title: '账号暂时受限',
|
||||
description: '为保护家谱资料,当前账号暂不能登录。',
|
||||
primaryLabel: '查看帮助',
|
||||
action: 'help',
|
||||
secondaryLabel: '返回登录'
|
||||
},
|
||||
'register-pending': {
|
||||
eyebrow: '注册提示',
|
||||
title: '注册尚未完成',
|
||||
description: '请返回注册页完善信息后再登录。',
|
||||
primaryLabel: '继续注册',
|
||||
action: 'register',
|
||||
secondaryLabel: '返回登录'
|
||||
},
|
||||
'wechat-cancelled': {
|
||||
eyebrow: '微信登录',
|
||||
title: '微信授权已取消',
|
||||
description: '未获得微信授权,不会影响账号安全。',
|
||||
primaryLabel: '返回登录',
|
||||
action: 'login',
|
||||
secondaryLabel: ''
|
||||
},
|
||||
'wechat-failed': {
|
||||
eyebrow: '微信登录',
|
||||
title: '微信授权失败',
|
||||
description: '当前无法完成微信授权,请使用账号密码登录。',
|
||||
primaryLabel: '返回登录',
|
||||
action: 'login',
|
||||
secondaryLabel: ''
|
||||
}
|
||||
}
|
||||
|
||||
const currentState = computed(() => statusConfig[status.value])
|
||||
|
||||
const resolveStatus = () => {
|
||||
let requestedStatus = 'normal'
|
||||
|
||||
if (typeof location !== 'undefined') {
|
||||
const query = new URLSearchParams(location.hash.split('?')[1] || '')
|
||||
requestedStatus = query.get('status') || 'normal'
|
||||
} else {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
requestedStatus = currentPage?.options?.status || 'normal'
|
||||
}
|
||||
|
||||
status.value = Object.prototype.hasOwnProperty.call(statusConfig, requestedStatus) ? requestedStatus : 'normal'
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
resolveStatus()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
resolveStatus()
|
||||
})
|
||||
|
||||
const goLogin = () => {
|
||||
uni.navigateTo({ url: '/pages/auth/a02-login' })
|
||||
}
|
||||
|
||||
const goRegister = () => {
|
||||
uni.navigateTo({ url: '/pages/auth/a04-register' })
|
||||
}
|
||||
|
||||
const handlePrimary = () => {
|
||||
if (currentState.value.action === 'register') {
|
||||
goRegister()
|
||||
return
|
||||
}
|
||||
if (currentState.value.action === 'help') {
|
||||
uni.showToast({ title: '账号申诉功能待接入', icon: 'none' })
|
||||
return
|
||||
}
|
||||
goLogin()
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.auth-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background: #f7f1e6;
|
||||
}
|
||||
|
||||
.paper-background,
|
||||
.scenery-background {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.paper-background { opacity: 0.96; }
|
||||
.scenery-background { opacity: 0.76; }
|
||||
|
||||
.auth-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
min-height: 100vh;
|
||||
padding-bottom: calc(44rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
position: relative;
|
||||
height: 253rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header-seal {
|
||||
position: absolute;
|
||||
top: 70rpx;
|
||||
left: 50%;
|
||||
width: 136rpx;
|
||||
height: 136rpx;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.status-panel {
|
||||
position: relative;
|
||||
min-height: 1030rpx;
|
||||
margin: 32rpx 42rpx 0;
|
||||
}
|
||||
|
||||
.status-panel__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.status-panel__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
min-height: 1030rpx;
|
||||
padding: 66rpx 50rpx 72rpx;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-title-cloud {
|
||||
position: absolute;
|
||||
top: -26rpx;
|
||||
right: 2rpx;
|
||||
width: 154rpx;
|
||||
height: 92rpx;
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
margin-right: 18rpx;
|
||||
}
|
||||
|
||||
.back-hover { opacity: 0.62; }
|
||||
|
||||
.back-button__icon {
|
||||
width: 42rpx;
|
||||
height: 42rpx;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.page-heading__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.page-title,
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: #3f2c1d;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 50rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin-top: 10rpx;
|
||||
color: #8c7864;
|
||||
font-size: 24rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.status-divider {
|
||||
display: block;
|
||||
width: 154rpx;
|
||||
height: 42rpx;
|
||||
margin: 40rpx auto 0;
|
||||
}
|
||||
|
||||
.status-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-height: 430rpx;
|
||||
padding-top: 54rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 132rpx;
|
||||
height: 132rpx;
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.status-eyebrow {
|
||||
display: block;
|
||||
margin-top: 28rpx;
|
||||
color: #a7160c;
|
||||
font-size: 24rpx;
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
|
||||
.status-title {
|
||||
display: block;
|
||||
margin-top: 18rpx;
|
||||
color: #3f2c1d;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.status-description {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 64rpx;
|
||||
margin-top: 20rpx;
|
||||
padding: 0 8rpx;
|
||||
color: #786654;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.status-primary {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 128rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.status-primary__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.status-primary__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #fffaf0;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 36rpx;
|
||||
letter-spacing: 6rpx;
|
||||
}
|
||||
|
||||
.status-secondary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 88rpx;
|
||||
color: #a7160c;
|
||||
font-size: 25rpx;
|
||||
}
|
||||
|
||||
.button-hover { opacity: 0.84; }
|
||||
.text-hover { opacity: 0.64; }
|
||||
</style>
|
||||
|
||||
@@ -139,21 +139,67 @@
|
||||
</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">
|
||||
<view v-else class="genealogy-empty-state">
|
||||
<view class="empty-panel">
|
||||
<image
|
||||
class="create-icon"
|
||||
src="/static/assets/modules/genealogy/transparent/add.png"
|
||||
mode="aspectFit"
|
||||
class="empty-panel__skin"
|
||||
src="/static/assets/modules/genealogy/opaque/g01-empty-panel.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text>新建家谱</text>
|
||||
<view class="empty-panel__content">
|
||||
<view class="empty-seal">
|
||||
<image
|
||||
class="empty-seal__skin"
|
||||
src="/static/assets/modules/genealogy/transparent/current-seal-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text class="empty-seal__text">家谱</text>
|
||||
</view>
|
||||
<text class="empty-title">还没有加入任何家谱</text>
|
||||
<text class="empty-copy">先为自己的家族立一部谱,或搜索家谱申请加入</text>
|
||||
<image
|
||||
class="empty-divider"
|
||||
src="/static/assets/modules/genealogy/transparent/section-divider.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view
|
||||
class="empty-create-action"
|
||||
hover-class="action-hover"
|
||||
@click="createGenealogy"
|
||||
>
|
||||
<image
|
||||
class="empty-create-action__skin"
|
||||
src="/static/assets/foundation/opaque/a01-primary-button.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="empty-create-action__content">
|
||||
<image
|
||||
class="empty-create-action__icon"
|
||||
src="/static/assets/modules/genealogy/transparent/add.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>新建家谱</text>
|
||||
<image
|
||||
class="empty-create-action__cloud"
|
||||
src="/static/assets/modules/genealogy/transparent/create-cloud.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="empty-search-action"
|
||||
hover-class="action-hover"
|
||||
@click="applyToJoin"
|
||||
>
|
||||
<image
|
||||
class="empty-search-action__skin"
|
||||
src="/static/assets/foundation/opaque/a01-secondary-button.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text class="empty-search-action__copy">搜索并申请加入</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-secondary" @click="applyToJoin"
|
||||
><text>搜索并申请加入</text></view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -163,6 +209,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import AppTabbar from "@/components/AppTabbar.vue";
|
||||
import GenealogyCard from "@/components/GenealogyCard.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
@@ -170,10 +217,35 @@ import { genealogies, notifications } from "@/data/mock.js";
|
||||
|
||||
const isLoading = ref(false);
|
||||
const list = ref(genealogies);
|
||||
const forceEmptyState = ref(false);
|
||||
|
||||
const isEmptyStateRoute = () => {
|
||||
if (typeof location !== "undefined") {
|
||||
const query = new URLSearchParams(location.hash.split("?")[1] || "");
|
||||
return query.get("state") === "empty";
|
||||
}
|
||||
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
return currentPage?.options?.state === "empty";
|
||||
};
|
||||
|
||||
const syncEmptyStateFromRoute = () => {
|
||||
forceEmptyState.value = isEmptyStateRoute();
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
syncEmptyStateFromRoute();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
syncEmptyStateFromRoute();
|
||||
});
|
||||
|
||||
const unreadCount = computed(
|
||||
() => notifications.filter((item) => item.unread).length,
|
||||
);
|
||||
const hasGenealogies = computed(() => list.value.length > 0);
|
||||
const hasGenealogies = computed(() => !forceEmptyState.value && list.value.length > 0);
|
||||
const createdGenealogies = computed(() =>
|
||||
list.value.filter((item, index) => index === 0),
|
||||
);
|
||||
@@ -440,9 +512,7 @@ const openShortcut = (key) => {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.create-action,
|
||||
.empty-primary,
|
||||
.empty-secondary {
|
||||
.create-action {
|
||||
display: flex;
|
||||
min-height: 96rpx;
|
||||
align-items: center;
|
||||
@@ -464,18 +534,6 @@ const openShortcut = (key) => {
|
||||
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;
|
||||
@@ -527,8 +585,7 @@ const openShortcut = (key) => {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.loading-seal,
|
||||
.empty-scroll {
|
||||
.loading-seal {
|
||||
border: 2rpx solid $gold;
|
||||
}
|
||||
.loading-seal {
|
||||
@@ -537,22 +594,151 @@ const openShortcut = (key) => {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.empty-scroll {
|
||||
.genealogy-empty-state {
|
||||
min-height: 856rpx;
|
||||
}
|
||||
|
||||
.empty-panel {
|
||||
position: relative;
|
||||
height: 856rpx;
|
||||
}
|
||||
|
||||
.empty-panel__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.empty-panel__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 154rpx;
|
||||
height: 196rpx;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 58rpx 60rpx 54rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-seal {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 92rpx;
|
||||
height: 142rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $gold;
|
||||
font-family: serif;
|
||||
font-size: 33rpx;
|
||||
}
|
||||
|
||||
.empty-seal__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.empty-seal__text {
|
||||
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;
|
||||
}
|
||||
|
||||
.empty-secondary {
|
||||
.empty-title {
|
||||
display: block;
|
||||
margin-top: 24rpx;
|
||||
color: #392719;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.empty-copy {
|
||||
display: block;
|
||||
min-height: 74rpx;
|
||||
margin-top: 14rpx;
|
||||
color: #786654;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.empty-divider {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 30rpx;
|
||||
margin-top: 16rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 27rpx;
|
||||
}
|
||||
|
||||
.empty-create-action,
|
||||
.empty-search-action {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 468rpx;
|
||||
height: 104rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.empty-create-action {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.empty-search-action {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.empty-create-action__skin,
|
||||
.empty-search-action__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.empty-create-action__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff9ec;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.empty-create-action__icon {
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.empty-create-action__cloud {
|
||||
width: 46rpx;
|
||||
height: 24rpx;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.empty-search-action__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #7b4e24;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 29rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
|
||||
.action-hover {
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 340px) {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<!-- 页面编号:G-02;用途:未加入家谱时的空状态。 -->
|
||||
<template><ModulePage page-id="g02" /></template>
|
||||
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
|
||||
@@ -1,69 +1,504 @@
|
||||
<!-- 页面编号:G-03;用途:创建家谱表单。 -->
|
||||
<!-- 页面编号:G-03;用途:创建家谱与录入首代人物的同路由两步流程。 -->
|
||||
<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 class="flow-page">
|
||||
<!-- 纸纹、页头和山水均由完整位图承载,流程内容只叠加文字和控件。 -->
|
||||
<image
|
||||
class="page-paper-texture"
|
||||
src="/static/assets/foundation/opaque/page-paper.jpg"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<image
|
||||
class="page-footer-landscape"
|
||||
src="/static/assets/foundation/transparent/footer-mountain-bamboo.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
|
||||
<view class="flow-header">
|
||||
<image
|
||||
class="flow-header__skin"
|
||||
src="/static/assets/foundation/opaque/root-header-cinnabar.jpg"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="flow-header__content">
|
||||
<view class="flow-header__back" @click="goBack">
|
||||
<image
|
||||
class="flow-header__back-icon"
|
||||
src="/static/assets/foundation/transparent/chevron-right.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>返回</text>
|
||||
</view>
|
||||
<text class="flow-header__title">{{ isAncestorStep ? "录入首代人物" : "创建家谱" }}</text>
|
||||
<view class="flow-header__side" />
|
||||
</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="flow-content">
|
||||
<view class="create-flow-panel">
|
||||
<image
|
||||
class="create-flow-panel__skin"
|
||||
src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
|
||||
<view class="bottom-action"><button class="primary-button" @click="submit">创建并录入首代</button></view>
|
||||
<view v-if="!isAncestorStep" class="create-flow-panel__content">
|
||||
<view class="flow-step-label"><text>第一步 · 立谱信息</text></view>
|
||||
<text class="flow-heading">为家族立一部可传承的谱</text>
|
||||
<text class="flow-note">家谱建立后可继续完善,名称与访问规则由创建者维护。</text>
|
||||
|
||||
<view class="flow-fields">
|
||||
<view class="field-row">
|
||||
<text>姓氏</text>
|
||||
<input v-model="createForm.surname" maxlength="4" placeholder="如:汤" placeholder-class="placeholder" />
|
||||
</view>
|
||||
<view class="field-row">
|
||||
<text>谱名</text>
|
||||
<input v-model="createForm.name" maxlength="24" placeholder="如:汤氏家谱" placeholder-class="placeholder" />
|
||||
</view>
|
||||
<view class="field-row">
|
||||
<text>堂号</text>
|
||||
<input v-model="createForm.hall" maxlength="12" placeholder="选填" placeholder-class="placeholder" />
|
||||
</view>
|
||||
<view class="field-row">
|
||||
<text>所在地</text>
|
||||
<input v-model="createForm.location" maxlength="30" placeholder="请选择或输入" placeholder-class="placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flow-rule">
|
||||
<text class="flow-rule__label">访问规则</text>
|
||||
<text class="flow-rule__value">默认仅成员可见</text>
|
||||
</view>
|
||||
<text class="flow-rule__note">保护族人资料;创建后可在家谱设置中开放搜索与申请加入。</text>
|
||||
|
||||
<view class="flow-primary-action" hover-class="action-hover" @click="submitCreate">
|
||||
<image
|
||||
class="flow-primary-action__skin"
|
||||
src="/static/assets/foundation/opaque/a01-primary-button.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text class="flow-primary-action__copy">创建并录入首代</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="create-flow-panel__content">
|
||||
<view class="flow-step-label"><text>第二步 · 首代人物</text></view>
|
||||
<text class="flow-heading">家谱从这一位开始</text>
|
||||
<text class="flow-note">可先录入姓名、世代和简要生平,其他资料后续随时补充。</text>
|
||||
|
||||
<view class="flow-fields">
|
||||
<view class="field-row">
|
||||
<text>姓名</text>
|
||||
<input v-model="ancestorForm.personName" maxlength="20" placeholder="请输入首代姓名" placeholder-class="placeholder" />
|
||||
</view>
|
||||
<view class="field-row">
|
||||
<text>世代</text>
|
||||
<text class="fixed-value">第一世</text>
|
||||
</view>
|
||||
<view class="field-row">
|
||||
<text>出生日期</text>
|
||||
<input v-model="ancestorForm.birthDate" placeholder="如:1940年" placeholder-class="placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="intro-field">
|
||||
<text>生平简述</text>
|
||||
<textarea v-model="ancestorForm.introduction" maxlength="200" placeholder="可选,记录家训、迁徙或重要经历" placeholder-class="placeholder" />
|
||||
</view>
|
||||
|
||||
<view class="flow-primary-action" hover-class="action-hover" @click="submitAncestor">
|
||||
<image
|
||||
class="flow-primary-action__skin"
|
||||
src="/static/assets/foundation/opaque/a01-primary-button.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text class="flow-primary-action__copy">保存并查看世系树</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</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'
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { appApi } from "@/utils/api.js";
|
||||
import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
|
||||
const form = reactive({ surname: '', name: '', hall: '', location: '', visibility: 'MEMBER_ONLY' })
|
||||
const currentStep = ref("create");
|
||||
const genealogyId = ref("");
|
||||
const isSubmitting = ref(false);
|
||||
const createForm = reactive({
|
||||
surname: "",
|
||||
name: "",
|
||||
hall: "",
|
||||
location: "",
|
||||
visibility: "MEMBER_ONLY",
|
||||
});
|
||||
const ancestorForm = reactive({
|
||||
personName: "",
|
||||
generationNo: 1,
|
||||
generationName: "一世",
|
||||
sex: "0",
|
||||
birthDate: "",
|
||||
introduction: "",
|
||||
});
|
||||
|
||||
const submit = async () => {
|
||||
if (!form.surname.trim() || !form.name.trim()) {
|
||||
uni.showToast({ title: '请填写姓氏和谱名', icon: 'none' })
|
||||
return
|
||||
const isAncestorStep = computed(() => currentStep.value === "ancestor");
|
||||
|
||||
const getFlowQuery = () => {
|
||||
if (typeof location !== "undefined") {
|
||||
return new URLSearchParams(location.hash.split("?")[1] || "");
|
||||
}
|
||||
const created = await appApi.createGenealogy({ ...form })
|
||||
genealogyContext.setCurrentGenealogyId(created.id)
|
||||
uni.redirectTo({ url: `/pages/genealogy/g04-first-ancestor?genealogyId=${created.id}` })
|
||||
}
|
||||
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
return currentPage?.options || {};
|
||||
};
|
||||
|
||||
const syncFlowFromRoute = () => {
|
||||
const query = getFlowQuery();
|
||||
const isAncestorRoute = query.get
|
||||
? query.get("step") === "ancestor"
|
||||
: query.step === "ancestor";
|
||||
const routeGenealogyId = query.get ? query.get("genealogyId") : query.genealogyId;
|
||||
currentStep.value = isAncestorRoute ? "ancestor" : "create";
|
||||
genealogyId.value = routeGenealogyId || (isAncestorStep.value ? genealogyContext.getCurrentGenealogyId() : "");
|
||||
|
||||
if (isAncestorStep.value && genealogyId.value) {
|
||||
genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
}
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
syncFlowFromRoute();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
syncFlowFromRoute();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("hashchange", syncFlowFromRoute);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.removeEventListener("hashchange", syncFlowFromRoute);
|
||||
}
|
||||
});
|
||||
|
||||
const goBack = () => {
|
||||
if (isAncestorStep.value) {
|
||||
uni.redirectTo({ url: "/pages/genealogy/g03-create-genealogy" });
|
||||
return;
|
||||
}
|
||||
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
const submitCreate = async () => {
|
||||
if (!createForm.surname.trim() || !createForm.name.trim()) {
|
||||
uni.showToast({ title: "请填写姓氏和谱名", icon: "none" });
|
||||
return;
|
||||
}
|
||||
|
||||
const created = await appApi.createGenealogy({ ...createForm });
|
||||
genealogyContext.setCurrentGenealogyId(created.id);
|
||||
uni.redirectTo({
|
||||
url: `/pages/genealogy/g03-create-genealogy?step=ancestor&genealogyId=${created.id}`,
|
||||
});
|
||||
};
|
||||
|
||||
const submitAncestor = async () => {
|
||||
if (isSubmitting.value) return;
|
||||
if (!genealogyId.value) {
|
||||
uni.showToast({ title: "未找到当前家谱", icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (!ancestorForm.personName.trim()) {
|
||||
uni.showToast({ title: "请填写首代姓名", icon: "none" });
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
const person = await appApi.createPerson(genealogyId.value, {
|
||||
...ancestorForm,
|
||||
personName: ancestorForm.personName.trim(),
|
||||
});
|
||||
uni.redirectTo({
|
||||
url: `/pages/tree/t01-tree-overview?genealogyId=${genealogyId.value}&selectedId=${person.id}`,
|
||||
});
|
||||
} catch (error) {
|
||||
uni.showToast({ title: error.message || "保存失败,请稍后重试", icon: "none" });
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
</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; }
|
||||
.flow-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
background: #f9f6ef;
|
||||
}
|
||||
|
||||
.page-paper-texture {
|
||||
position: absolute;
|
||||
top: 112rpx;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.74;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.page-footer-landscape {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 430rpx;
|
||||
opacity: 0.42;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.flow-header {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
height: 112rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.flow-header__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.flow-header__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 26rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.flow-header__back,
|
||||
.flow-header__side {
|
||||
display: flex;
|
||||
width: 130rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flow-header__back {
|
||||
color: #fff3d9;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 27rpx;
|
||||
}
|
||||
|
||||
.flow-header__back-icon {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
margin-right: 4rpx;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.flow-header__title {
|
||||
flex: 1;
|
||||
color: #ffe4a7;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 39rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flow-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 24rpx 32rpx 48rpx;
|
||||
}
|
||||
|
||||
.create-flow-panel {
|
||||
position: relative;
|
||||
min-height: 876rpx;
|
||||
}
|
||||
|
||||
.create-flow-panel__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.create-flow-panel__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
min-height: 876rpx;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
padding: 48rpx 50rpx 42rpx;
|
||||
}
|
||||
|
||||
.flow-step-label {
|
||||
color: #a33b2b;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 25rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.flow-heading {
|
||||
display: block;
|
||||
margin-top: 13rpx;
|
||||
color: #392719;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 39rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.flow-note,
|
||||
.flow-rule__note {
|
||||
display: block;
|
||||
color: #786654;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.flow-note {
|
||||
min-height: 56rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.flow-fields {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: flex;
|
||||
min-height: 86rpx;
|
||||
align-items: center;
|
||||
border-bottom: 1rpx solid rgba(181, 138, 75, 0.36);
|
||||
}
|
||||
|
||||
.field-row > text {
|
||||
width: 126rpx;
|
||||
flex: 0 0 auto;
|
||||
color: #786654;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 29rpx;
|
||||
}
|
||||
|
||||
.field-row input {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
color: #392719;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #b7aa97;
|
||||
}
|
||||
|
||||
.fixed-value {
|
||||
color: #a33b2b !important;
|
||||
}
|
||||
|
||||
.flow-rule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.flow-rule__label {
|
||||
color: #392719;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 29rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.flow-rule__value {
|
||||
color: #a33b2b;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
|
||||
.flow-rule__note {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.intro-field {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.intro-field > text {
|
||||
display: block;
|
||||
color: #392719;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 29rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.intro-field textarea {
|
||||
width: 100%;
|
||||
height: 142rpx;
|
||||
margin-top: 8rpx;
|
||||
color: #392719;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.flow-primary-action {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.flow-primary-action__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.flow-primary-action__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #fff9ec;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.action-hover {
|
||||
opacity: 0.82;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
<!-- 页面编号:G-04;用途:创建家谱后的始祖录入。 -->
|
||||
<template>
|
||||
<view class="page-shell first-person-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.personName" maxlength="20" placeholder="请输入首代姓名" placeholder-class="placeholder" /></view>
|
||||
<view class="field-row"><text>世代</text><text class="fixed-value">第一世</text></view>
|
||||
<view class="field-row"><text>出生日期</text><input v-model="form.birthDate" placeholder="如:1940年" placeholder-class="placeholder" /></view>
|
||||
<view class="intro-field"><text>生平简述</text><textarea v-model="form.introduction" maxlength="200" placeholder="可选,记录家训、迁徙或重要经历" placeholder-class="placeholder" /></view>
|
||||
</view>
|
||||
<view class="bottom-action"><button class="primary-button" :loading="isSubmitting" :disabled="isSubmitting" @click="submit">保存并查看世系树</button></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, 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 genealogyId = ref('')
|
||||
const isSubmitting = ref(false)
|
||||
const form = reactive({ personName: '', generationNo: 1, generationName: '一世', sex: '0', birthDate: '', introduction: '' })
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId()
|
||||
if (genealogyId.value) genealogyContext.setCurrentGenealogyId(genealogyId.value)
|
||||
})
|
||||
|
||||
const submit = async () => {
|
||||
if (!genealogyId.value) { uni.showToast({ title: '未找到当前家谱', icon: 'none' }); return }
|
||||
if (!form.personName.trim()) { uni.showToast({ title: '请填写首代姓名', icon: 'none' }); return }
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
const person = await appApi.createPerson(genealogyId.value, { ...form, personName: form.personName.trim() })
|
||||
uni.redirectTo({ url: `/pages/tree/t01-tree-overview?genealogyId=${genealogyId.value}&selectedId=${person.id}` })
|
||||
} catch (error) {
|
||||
uni.showToast({ title: error.message || '保存失败,请稍后重试', icon: 'none' })
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.first-person-page { padding-bottom: 136rpx; }
|
||||
.form-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 text { width: 150rpx; color: $ink-muted; font-size: 28rpx; }
|
||||
.field-row input { flex: 1; color: $ink; font-size: 29rpx; }
|
||||
.fixed-value { color: $brand-red !important; }
|
||||
.intro-field { padding-top: 30rpx; }
|
||||
.intro-field > text { display: block; color: $ink-muted; font-size: 28rpx; }
|
||||
.intro-field textarea { width: 100%; height: 180rpx; margin-top: 18rpx; color: $ink; font-size: 27rpx; line-height: 1.6; }
|
||||
.placeholder { color: #b7aa97; }
|
||||
.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>
|
||||
@@ -1,15 +1,48 @@
|
||||
<!-- 页面编号:G-06;用途:公开家谱搜索。 -->
|
||||
<!-- 页面编号:G-06;用途:公开家谱检索,统一承载初始、加载、结果与无结果状态。 -->
|
||||
<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 class="search-page">
|
||||
<!-- 底图只提供完整纸纹,不以 CSS 补色或绘制装饰。 -->
|
||||
<image class="search-page__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill" />
|
||||
<image class="search-page__footer" src="/static/assets/foundation/transparent/footer-mountain-bamboo.png" mode="widthFix" />
|
||||
<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="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="keyword" class="search-input" placeholder="输入姓氏、谱名或地区" placeholder-class="search-input__placeholder" @confirm="search" />
|
||||
</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>
|
||||
<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>正在整理可申请加入的家谱</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>
|
||||
<GenealogyCard v-for="item in results" :key="item.id" :genealogy="item" role="可申请" @select="openApply" />
|
||||
</view>
|
||||
<view v-else 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>
|
||||
</view>
|
||||
</template>
|
||||
@@ -23,25 +56,61 @@ import { appApi } from '@/utils/api.js'
|
||||
const keyword = ref('')
|
||||
const allResults = ref([])
|
||||
const results = ref([])
|
||||
const searched = ref(false)
|
||||
const searchState = ref('initial')
|
||||
|
||||
onMounted(async () => {
|
||||
searchState.value = 'loading'
|
||||
allResults.value = await appApi.getPublicGenealogies()
|
||||
searchState.value = 'initial'
|
||||
})
|
||||
|
||||
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
|
||||
results.value = value
|
||||
? allResults.value.filter((item) => `${item.name}${item.surname}${item.location}`.includes(value))
|
||||
: allResults.value
|
||||
searchState.value = results.value.length ? 'results' : 'empty'
|
||||
}
|
||||
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' }) } } })
|
||||
|
||||
const openApply = (genealogy) => uni.navigateTo({ url: `/pages/genealogy/g08-join-application?genealogyId=${genealogy.id}` })
|
||||
</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; }
|
||||
/* G06 的完整检索位图与叠放控件样式必须作为同一 SFC 样式模块刷新。 */
|
||||
.search-page { position: relative; min-height: 100vh; overflow: hidden; background: $paper; }
|
||||
.search-page__paper { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; opacity: .98; pointer-events: none; }
|
||||
.search-page__footer { position: absolute; right: 0; bottom: 0; left: 0; z-index: 1; width: 100%; opacity: .15; pointer-events: none; }
|
||||
.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; top: 0; right: 170rpx; bottom: 0; left: 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; }
|
||||
.search-controls { display: flex; align-items: center; gap: 20rpx; margin-top: 48rpx; }
|
||||
.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 30rpx; box-sizing: border-box; color: $ink; font-size: 26rpx; }
|
||||
.search-input__placeholder { color: #ab9c83; }
|
||||
.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: 7rpx; }
|
||||
.search-divider { display: block; width: 100%; height: 30rpx; margin: 40rpx 0 34rpx; }
|
||||
|
||||
.search-status { display: flex; min-height: 190rpx; flex-direction: column; align-items: center; justify-content: center; padding: 10rpx 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: 29rpx; font-weight: 700; letter-spacing: 2rpx; }
|
||||
.search-status__copy { margin-top: 12rpx; color: $ink-muted; font-size: 22rpx; }
|
||||
.search-status__hall { width: 330rpx; height: 132rpx; margin-top: 330rpx; opacity: .22; filter: grayscale(1); }
|
||||
.search-loading { min-height: 168rpx; color: $ink-muted; font-size: 24rpx; }
|
||||
.search-results { display: flex; flex-direction: column; gap: 18rpx; margin-top: 6rpx; }
|
||||
.search-results__head { display: flex; align-items: center; justify-content: space-between; padding: 0 16rpx; color: $ink-muted; font-size: 22rpx; }
|
||||
.search-results__head text:first-child { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 27rpx; font-weight: 700; }
|
||||
|
||||
@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; }
|
||||
.search-action { width: 200rpx; height: 88rpx; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<!-- 页面编号:G-07;用途:公开家谱搜索结果与简介。 -->
|
||||
<template><ModulePage page-id="g07" /></template>
|
||||
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
|
||||
Reference in New Issue
Block a user