修改完成
This commit is contained in:
+32
-50
@@ -374,19 +374,17 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
|
||||
<style scoped lang="scss">
|
||||
.auth-page {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
min-height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background: #f7f0e5;
|
||||
color: #493323;
|
||||
}
|
||||
|
||||
.page-canvas {
|
||||
position: relative;
|
||||
display: grid;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: calc(1665rpx + env(safe-area-inset-bottom));
|
||||
@@ -394,28 +392,28 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
}
|
||||
|
||||
.page-backdrop {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
inset: 0 0 auto;
|
||||
grid-area: 1 / 1;
|
||||
align-self: start;
|
||||
width: 100%;
|
||||
height: 1665rpx;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.brand-seal {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 64rpx;
|
||||
left: 50%;
|
||||
grid-area: 1 / 1;
|
||||
align-self: start;
|
||||
justify-self: center;
|
||||
width: 184rpx;
|
||||
height: 221rpx;
|
||||
transform: translateX(-50%);
|
||||
margin-top: 64rpx;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.login-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
grid-area: 1 / 1;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 480rpx 64rpx 34rpx;
|
||||
@@ -426,7 +424,7 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 142rpx;
|
||||
min-height: 142rpx;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
@@ -452,7 +450,6 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
}
|
||||
|
||||
.login-tab {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
@@ -464,30 +461,21 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
}
|
||||
|
||||
.login-tab:first-child::before {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
width: 1rpx;
|
||||
height: 36rpx;
|
||||
background: rgba(176, 120, 52, 0.58);
|
||||
content: "";
|
||||
transform: translateY(-50%);
|
||||
content: none;
|
||||
}
|
||||
.login-tab:first-child {
|
||||
border-right: 1rpx solid rgba(176, 120, 52, 0.58);
|
||||
}
|
||||
|
||||
.login-tab.active {
|
||||
background: linear-gradient(#ad160d, #ad160d) center bottom /
|
||||
calc(100% - 116rpx) 5rpx no-repeat;
|
||||
color: #a9160d;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.login-tab.active::after {
|
||||
position: absolute;
|
||||
right: 58rpx;
|
||||
bottom: 0;
|
||||
left: 58rpx;
|
||||
height: 5rpx;
|
||||
border-radius: 5rpx;
|
||||
background: #ad160d;
|
||||
content: "";
|
||||
content: none;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
@@ -518,7 +506,7 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
.auth-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 104rpx;
|
||||
min-height: 104rpx;
|
||||
color: #493323;
|
||||
font-size: 40rpx;
|
||||
}
|
||||
@@ -550,7 +538,6 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
min-height: 88rpx;
|
||||
color: #a9160d;
|
||||
font-size: 32rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.form-secondary-row {
|
||||
@@ -570,28 +557,24 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
|
||||
.login-submit,
|
||||
.wechat-login {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.login-submit {
|
||||
height: 92rpx;
|
||||
min-height: 92rpx;
|
||||
}
|
||||
|
||||
.button-skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.login-submit__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
color: #fffaf1;
|
||||
font-size: 52rpx;
|
||||
letter-spacing: 8rpx;
|
||||
@@ -600,7 +583,7 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
.other-login-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100rpx;
|
||||
min-height: 100rpx;
|
||||
color: #9f6a27;
|
||||
font-size: 32rpx;
|
||||
white-space: nowrap;
|
||||
@@ -625,16 +608,16 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
}
|
||||
|
||||
.wechat-login {
|
||||
height: 100rpx;
|
||||
min-height: 100rpx;
|
||||
color: #493323;
|
||||
font-size: 40rpx;
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
|
||||
.wechat-login__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -689,7 +672,6 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
flex: 0 1 auto;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.agreement-link {
|
||||
@@ -750,7 +732,6 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
}
|
||||
|
||||
.feedback-toast__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 16rpx 36rpx;
|
||||
color: #5c4330;
|
||||
@@ -770,26 +751,27 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
}
|
||||
|
||||
.verification-dialog {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 620rpx;
|
||||
height: 520rpx;
|
||||
min-height: 520rpx;
|
||||
max-height: calc(100vh - 80rpx);
|
||||
}
|
||||
|
||||
.verification-dialog__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.verification-dialog__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 90rpx 64rpx 54rpx;
|
||||
}
|
||||
|
||||
|
||||
+25
-29
@@ -229,18 +229,16 @@ const submitRegister = () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.auth-page {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
min-height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background: #f7f0e4;
|
||||
color: #493323;
|
||||
}
|
||||
|
||||
.page-canvas {
|
||||
position: relative;
|
||||
display: grid;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: calc(1665rpx + env(safe-area-inset-bottom));
|
||||
@@ -248,48 +246,49 @@ const submitRegister = () => {
|
||||
}
|
||||
|
||||
.page-backdrop {
|
||||
position: absolute;
|
||||
inset: 0 0 auto;
|
||||
grid-area: 1 / 1;
|
||||
align-self: start;
|
||||
width: 100%;
|
||||
height: 1665rpx;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.brand-seal {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 64rpx;
|
||||
left: 50%;
|
||||
grid-area: 1 / 1;
|
||||
align-self: start;
|
||||
justify-self: center;
|
||||
width: 184rpx;
|
||||
height: 221rpx;
|
||||
transform: translateX(-50%);
|
||||
margin-top: 64rpx;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.register-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
grid-area: 1 / 1;
|
||||
box-sizing: border-box;
|
||||
padding: 430rpx 64rpx 46rpx;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto;
|
||||
justify-items: center;
|
||||
min-height: 154rpx;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
position: absolute;
|
||||
top: 10rpx;
|
||||
left: 0;
|
||||
display: flex;
|
||||
grid-row: 1 / -1;
|
||||
grid-column: 1;
|
||||
align-self: start;
|
||||
justify-self: start;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.back-button__icon {
|
||||
@@ -299,6 +298,7 @@ const submitRegister = () => {
|
||||
}
|
||||
|
||||
.page-title {
|
||||
grid-row: 1;
|
||||
color: #9f170f;
|
||||
font-size: 66rpx;
|
||||
font-weight: 700;
|
||||
@@ -306,6 +306,7 @@ const submitRegister = () => {
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
grid-row: 2;
|
||||
margin-top: 12rpx;
|
||||
color: #806c58;
|
||||
font-size: 25rpx;
|
||||
@@ -313,6 +314,7 @@ const submitRegister = () => {
|
||||
}
|
||||
|
||||
.register-divider {
|
||||
grid-row: 3;
|
||||
width: 300rpx;
|
||||
height: 50rpx;
|
||||
filter: brightness(0.68) saturate(1.5) contrast(1.2);
|
||||
@@ -342,7 +344,7 @@ const submitRegister = () => {
|
||||
.auth-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 96rpx;
|
||||
min-height: 96rpx;
|
||||
color: #3f2c1d;
|
||||
font-size: 29rpx;
|
||||
}
|
||||
@@ -368,25 +370,21 @@ const submitRegister = () => {
|
||||
}
|
||||
|
||||
.register-submit {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 104rpx;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 104rpx;
|
||||
margin-top: 36rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.register-submit__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.register-submit__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
color: #fffaf0;
|
||||
font-size: 39rpx;
|
||||
letter-spacing: 6rpx;
|
||||
@@ -417,7 +415,6 @@ const submitRegister = () => {
|
||||
.agreement-copy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.agreement-link {
|
||||
@@ -490,7 +487,6 @@ const submitRegister = () => {
|
||||
}
|
||||
|
||||
.feedback-toast__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 16rpx 36rpx;
|
||||
color: #5c4330;
|
||||
|
||||
@@ -157,7 +157,11 @@
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="success-dialog__content">
|
||||
<text class="success-mark">✓</text>
|
||||
<image
|
||||
class="success-mark"
|
||||
src="/static/assets/foundation/transparent/brand-seal.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="success-title">密码已重设</text>
|
||||
<text class="success-copy">请返回登录页,使用新密码登录</text>
|
||||
<view
|
||||
@@ -259,18 +263,16 @@ const submitReset = () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.auth-page {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
min-height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background: #f7f0e4;
|
||||
color: #493323;
|
||||
}
|
||||
|
||||
.page-canvas {
|
||||
position: relative;
|
||||
display: grid;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: calc(1665rpx + env(safe-area-inset-bottom));
|
||||
@@ -278,48 +280,49 @@ const submitReset = () => {
|
||||
}
|
||||
|
||||
.page-backdrop {
|
||||
position: absolute;
|
||||
inset: 0 0 auto;
|
||||
grid-area: 1 / 1;
|
||||
align-self: start;
|
||||
width: 100%;
|
||||
height: 1665rpx;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.brand-seal {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 64rpx;
|
||||
left: 50%;
|
||||
grid-area: 1 / 1;
|
||||
align-self: start;
|
||||
justify-self: center;
|
||||
width: 184rpx;
|
||||
height: 221rpx;
|
||||
transform: translateX(-50%);
|
||||
margin-top: 64rpx;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.reset-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
grid-area: 1 / 1;
|
||||
box-sizing: border-box;
|
||||
padding: 430rpx 64rpx 46rpx;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto;
|
||||
justify-items: center;
|
||||
min-height: 154rpx;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
position: absolute;
|
||||
top: 10rpx;
|
||||
left: 0;
|
||||
display: flex;
|
||||
grid-row: 1 / -1;
|
||||
grid-column: 1;
|
||||
align-self: start;
|
||||
justify-self: start;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.back-button__icon {
|
||||
@@ -328,18 +331,21 @@ const submitReset = () => {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
.page-title {
|
||||
grid-row: 1;
|
||||
color: #9f170f;
|
||||
font-size: 66rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 7rpx;
|
||||
}
|
||||
.page-subtitle {
|
||||
grid-row: 2;
|
||||
margin-top: 12rpx;
|
||||
color: #806c58;
|
||||
font-size: 25rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
.reset-divider {
|
||||
grid-row: 3;
|
||||
width: 300rpx;
|
||||
height: 50rpx;
|
||||
margin-top: 2rpx;
|
||||
@@ -367,7 +373,7 @@ const submitReset = () => {
|
||||
.auth-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 82rpx;
|
||||
min-height: 82rpx;
|
||||
color: #3f2c1d;
|
||||
font-size: 27rpx;
|
||||
}
|
||||
@@ -394,25 +400,21 @@ const submitReset = () => {
|
||||
}
|
||||
|
||||
.reset-submit {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 104rpx;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 104rpx;
|
||||
margin-top: 26rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.reset-submit__skin,
|
||||
.success-action__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.reset-submit__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
color: #fffaf0;
|
||||
font-size: 39rpx;
|
||||
letter-spacing: 6rpx;
|
||||
@@ -453,24 +455,25 @@ const submitReset = () => {
|
||||
}
|
||||
|
||||
.success-dialog {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 620rpx;
|
||||
height: 520rpx;
|
||||
min-height: 520rpx;
|
||||
max-height: calc(100vh - 80rpx);
|
||||
}
|
||||
.success-dialog__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.success-dialog__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 64rpx 64rpx 42rpx;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -487,30 +490,20 @@ const submitReset = () => {
|
||||
line-height: 40rpx;
|
||||
}
|
||||
.success-mark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 82rpx;
|
||||
height: 82rpx;
|
||||
margin-bottom: 20rpx;
|
||||
border: 2rpx solid #a7160c;
|
||||
border-radius: 50%;
|
||||
color: #a7160c;
|
||||
font-size: 48rpx;
|
||||
}
|
||||
.success-action {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 100%;
|
||||
height: 82rpx;
|
||||
min-height: 82rpx;
|
||||
margin-top: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.success-action__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
color: #fffaf0;
|
||||
font-size: 30rpx;
|
||||
letter-spacing: 4rpx;
|
||||
@@ -537,7 +530,6 @@ const submitReset = () => {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.feedback-toast__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 16rpx 36rpx;
|
||||
color: #5c4330;
|
||||
|
||||
@@ -204,62 +204,61 @@ const goBack = () => uni.navigateBack();
|
||||
|
||||
<style scoped lang="scss">
|
||||
.auth-page {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
min-height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background: #f7f0e4;
|
||||
color: #493323;
|
||||
}
|
||||
|
||||
.page-canvas {
|
||||
position: relative;
|
||||
display: grid;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: calc(1665rpx + env(safe-area-inset-bottom));
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
.page-backdrop {
|
||||
position: absolute;
|
||||
inset: 0 0 auto;
|
||||
grid-area: 1 / 1;
|
||||
align-self: start;
|
||||
width: 100%;
|
||||
height: 1665rpx;
|
||||
pointer-events: none;
|
||||
}
|
||||
.brand-seal {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 64rpx;
|
||||
left: 50%;
|
||||
grid-area: 1 / 1;
|
||||
align-self: start;
|
||||
justify-self: center;
|
||||
width: 184rpx;
|
||||
height: 221rpx;
|
||||
transform: translateX(-50%);
|
||||
margin-top: 64rpx;
|
||||
pointer-events: none;
|
||||
}
|
||||
.status-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
grid-area: 1 / 1;
|
||||
box-sizing: border-box;
|
||||
padding: 430rpx 64rpx 46rpx;
|
||||
}
|
||||
.page-heading {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto;
|
||||
justify-items: center;
|
||||
min-height: 154rpx;
|
||||
}
|
||||
.back-button {
|
||||
position: absolute;
|
||||
top: 10rpx;
|
||||
left: 0;
|
||||
display: flex;
|
||||
grid-row: 1 / -1;
|
||||
grid-column: 1;
|
||||
align-self: start;
|
||||
justify-self: start;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
.back-button__icon {
|
||||
width: 42rpx;
|
||||
@@ -267,18 +266,21 @@ const goBack = () => uni.navigateBack();
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
.page-title {
|
||||
grid-row: 1;
|
||||
color: #9f170f;
|
||||
font-size: 66rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 7rpx;
|
||||
}
|
||||
.page-subtitle {
|
||||
grid-row: 2;
|
||||
margin-top: 12rpx;
|
||||
color: #806c58;
|
||||
font-size: 25rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
.status-divider {
|
||||
grid-row: 3;
|
||||
width: 300rpx;
|
||||
height: 50rpx;
|
||||
margin-top: 2rpx;
|
||||
@@ -344,24 +346,20 @@ const goBack = () => uni.navigateBack();
|
||||
}
|
||||
|
||||
.status-primary {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 104rpx;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 104rpx;
|
||||
margin-top: 28rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.status-primary__skin,
|
||||
.recovery-action__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.status-primary__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
color: #fffaf0;
|
||||
font-size: 38rpx;
|
||||
letter-spacing: 5rpx;
|
||||
@@ -387,24 +385,25 @@ const goBack = () => uni.navigateBack();
|
||||
background: rgba(35, 18, 10, 0.62);
|
||||
}
|
||||
.recovery-dialog {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 620rpx;
|
||||
height: 520rpx;
|
||||
min-height: 520rpx;
|
||||
max-height: calc(100vh - 80rpx);
|
||||
}
|
||||
.recovery-dialog__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.recovery-dialog__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 58rpx 58rpx 36rpx;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -427,18 +426,15 @@ const goBack = () => uni.navigateBack();
|
||||
line-height: 31rpx;
|
||||
}
|
||||
.recovery-action {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
min-height: 80rpx;
|
||||
margin-top: 22rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.recovery-action__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
color: #fffaf0;
|
||||
font-size: 29rpx;
|
||||
letter-spacing: 4rpx;
|
||||
|
||||
@@ -22,11 +22,6 @@
|
||||
><text>汤氏家族圈</text><text>家宴、通知与共同记忆</text></view
|
||||
>
|
||||
<view v-if="feedState !== 'loading'" class="feed-shortcuts">
|
||||
<image
|
||||
class="feed-shortcuts__skin"
|
||||
src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view
|
||||
v-for="item in shortcuts"
|
||||
:key="item.key"
|
||||
@@ -42,11 +37,6 @@
|
||||
class="feed-card"
|
||||
@click="openDetail(item)"
|
||||
>
|
||||
<image
|
||||
class="feed-card__skin"
|
||||
src="/static/assets/modules/family/transparent/f01-family-letter-card.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="feed-card__copy">
|
||||
<text class="feed-card__meta"
|
||||
>{{ item.tag }} · {{ item.time }}</text
|
||||
@@ -58,11 +48,7 @@
|
||||
</view>
|
||||
</template>
|
||||
<view v-else-if="feedState !== 'loading'" class="feed-state-card">
|
||||
<image
|
||||
src="/static/assets/modules/family/transparent/f01-family-letter-card.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view
|
||||
<view class="feed-state-card__copy"
|
||||
><text>{{
|
||||
feedState === "empty" ? "还没有家族动态" : "家族动态暂不可用"
|
||||
}}</text
|
||||
@@ -77,10 +63,7 @@
|
||||
v-if="feedState !== 'loading'"
|
||||
class="feed-action"
|
||||
@click="feedState === 'error' ? (feedState = 'list') : toPublish()"
|
||||
><image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/><text>{{
|
||||
><text>{{
|
||||
feedState === "error" ? "重新查看" : "发布家族动态"
|
||||
}}</text></view
|
||||
>
|
||||
@@ -121,6 +104,10 @@ const shortcuts = [
|
||||
{ key: "albums", label: "相册" },
|
||||
{ key: "rituals", label: "礼仪" },
|
||||
{ key: "memos", label: "备忘" },
|
||||
{ key: "people", label: "人物录" },
|
||||
{ key: "gifts", label: "贺礼簿" },
|
||||
{ key: "merits", label: "功德录" },
|
||||
{ key: "videos", label: "家族视频" },
|
||||
];
|
||||
onLoad((query) => {
|
||||
genealogyId.value =
|
||||
@@ -146,23 +133,29 @@ const openSection = (key) => {
|
||||
albums: "/pages/family/f07-album-list",
|
||||
rituals: "/pages/records/r05-ritual-list",
|
||||
memos: "/pages/records/r10-memo-list",
|
||||
people: "/pages/records/r01-people-list",
|
||||
gifts: "/pages/records/r03-gift-list",
|
||||
merits: "/pages/records/r11-merit-records",
|
||||
videos: "/pages/family/f10-video-list",
|
||||
};
|
||||
uni.navigateTo({ url: routes[key] });
|
||||
uni.navigateTo({
|
||||
url: `${routes[key]}?genealogyId=${genealogyId.value}`,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.family-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.family-page__header,
|
||||
.feed-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
}
|
||||
.feed-content {
|
||||
flex: 1;
|
||||
padding: 24rpx 24rpx 190rpx;
|
||||
}
|
||||
.feed-heading text {
|
||||
@@ -180,23 +173,14 @@ const openSection = (key) => {
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.feed-shortcuts {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
min-height: 48px;
|
||||
margin-top: 15rpx;
|
||||
}
|
||||
.feed-shortcuts__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.feed-shortcut {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
min-height: 44px;
|
||||
@@ -212,28 +196,17 @@ const openSection = (key) => {
|
||||
}
|
||||
.feed-card,
|
||||
.feed-state-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 230rpx;
|
||||
min-height: 230rpx;
|
||||
min-height: 98px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-top: 17rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.feed-card__skin,
|
||||
.feed-state-card > image,
|
||||
.feed-action image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: url("/static/assets/modules/family/transparent/f01-family-letter-card.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.feed-card__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 15% 9%;
|
||||
}
|
||||
.feed-card text {
|
||||
@@ -265,9 +238,7 @@ const openSection = (key) => {
|
||||
.feed-state-card {
|
||||
margin-top: 70rpx;
|
||||
}
|
||||
.feed-state-card > view {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
.feed-state-card__copy {
|
||||
padding: 23% 10%;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
@@ -288,14 +259,13 @@ const openSection = (key) => {
|
||||
line-height: 1.45;
|
||||
}
|
||||
.feed-action {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 76rpx;
|
||||
margin-top: 19rpx;
|
||||
width: 514rpx;
|
||||
max-width: 100%;
|
||||
min-height: 76rpx;
|
||||
margin: 19rpx auto 0;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.feed-action text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -10,19 +10,13 @@
|
||||
><ModulePageBackground module="family" /><view class="publish-page__header"
|
||||
><PageHeader title="发布动态" /></view
|
||||
><view class="publish-panel"
|
||||
><image
|
||||
class="publish-panel__skin"
|
||||
src="/static/assets/modules/family/transparent/module-content-frame.png"
|
||||
mode="scaleToFill" /><view
|
||||
><view
|
||||
v-if="publishState === 'form'"
|
||||
class="publish-form"
|
||||
><text>记录此刻</text
|
||||
><text>分享通知、活动、家族故事或一段共同记忆。</text
|
||||
><view class="publish-field"
|
||||
><image
|
||||
src="/static/assets/modules/family/transparent/module-field-frame.png"
|
||||
mode="scaleToFill"
|
||||
/><textarea
|
||||
><textarea
|
||||
v-model="content"
|
||||
maxlength="300"
|
||||
placeholder="写下想对家人说的话"
|
||||
@@ -87,15 +81,14 @@ onUnmounted(() => {
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.publish-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.publish-page__header,
|
||||
.publish-panel {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
}
|
||||
.publish-panel {
|
||||
width: calc(100% - 32rpx);
|
||||
@@ -103,18 +96,7 @@ onUnmounted(() => {
|
||||
margin: 18rpx auto 0;
|
||||
padding: 9%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.publish-panel__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.publish-form,
|
||||
.publish-result {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: url("/static/assets/modules/family/transparent/module-content-frame.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.publish-form > text,
|
||||
.publish-result > text {
|
||||
@@ -135,25 +117,16 @@ onUnmounted(() => {
|
||||
line-height: 1.6;
|
||||
}
|
||||
.publish-field {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 250rpx;
|
||||
margin-top: 24rpx;
|
||||
padding: 25rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.publish-field image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.publish-field textarea {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
min-height: 200rpx;
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.7;
|
||||
|
||||
@@ -1,5 +1,153 @@
|
||||
<!-- 页面编号:F-03;用途:动态详情、评论、点赞与删除确认。 -->
|
||||
<template><ModulePage page-id="f03" /></template>
|
||||
<!-- 页面编号:F-03;用途:动态详情、评论与内容失效状态。 -->
|
||||
<template>
|
||||
<view class="feed-detail-page" :class="{ 'feed-state--expired': feedState === 'expired', 'feed-state--error': feedState === 'error', 'feed-state--ready': feedState === 'ready' }">
|
||||
<ModulePageBackground module="family" />
|
||||
<view class="feed-detail-header"><PageHeader title="动态详情" /></view>
|
||||
|
||||
<view v-if="feedState === 'loading'" class="feed-detail-loading">
|
||||
<AppLoading text="正在读取家族动态" description="请稍候,正在整理正文与家人评论。" />
|
||||
</view>
|
||||
|
||||
<view v-else class="feed-detail-content">
|
||||
<template v-if="feedState === 'ready'">
|
||||
<view class="feed-article-card">
|
||||
<text class="feed-article-card__meta">{{ currentFeed.tag }} · {{ currentFeed.time }}</text>
|
||||
<text class="feed-article-card__title">{{ currentFeed.title }}</text>
|
||||
<text class="feed-article-card__body">{{ currentFeed.content }}</text>
|
||||
<text class="feed-article-card__author">发布人:{{ currentFeed.author }}</text>
|
||||
</view>
|
||||
|
||||
<view class="feed-comments-panel">
|
||||
<view class="feed-comments-heading">
|
||||
<text>家人评论</text><text>{{ feedComments.length }} 条</text>
|
||||
</view>
|
||||
<view v-for="comment in feedComments" :key="comment.id" class="feed-comment-card">
|
||||
<view><text>{{ comment.author }}</text><text>{{ comment.time }}</text></view>
|
||||
<text>{{ comment.content }}</text>
|
||||
</view>
|
||||
<view v-if="!feedComments.length" class="feed-comments-empty">
|
||||
<text>还没有评论</text><text>写下第一句祝福或共同记忆。</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="feed-comment-form" :class="{ 'comment-state--saving': commentState === 'saving', 'comment-state--error': commentState === 'error' }">
|
||||
<text>写下评论</text>
|
||||
<textarea v-model="commentDraft" auto-height maxlength="240" placeholder="对家人说点什么" />
|
||||
<text v-if="commentError" class="feed-comment-error">{{ commentError }}</text>
|
||||
<AppButton block :disabled="commentState === 'saving'" :label="commentState === 'saving' ? '正在发送' : '发送评论'" @click="submitComment" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-else class="feed-state-card">
|
||||
<text>{{ feedState === 'expired' ? '动态已失效' : '动态暂不可用' }}</text>
|
||||
<text>{{ feedState === 'expired' ? '这条动态可能已被发布人删除,请返回家族圈查看其他内容。' : '请稍后重新查看,已有家族记录不会受到影响。' }}</text>
|
||||
<AppButton :type="feedState === 'error' ? 'secondary' : 'primary'" block :label="feedState === 'error' ? '重新查看' : '返回家族圈'" @click="feedState === 'error' ? restoreFeed() : backToFamily()" />
|
||||
</view>
|
||||
</view>
|
||||
<AppToast :visible="toastVisible" message="评论已发送" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { onUnmounted, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const feedRecords = [
|
||||
{ id: "1", tag: "团圆记忆", time: "今天 10:24", title: "端午家宴", content: "今年端午全家相聚,长辈讲起祖居旧事,孩子们也为大家拍下了新的全家福。饭后我们把照片和口述片段整理进家族档案,让这份热闹成为往后仍能翻看的共同记忆。", author: "汤正国" },
|
||||
{ id: "2", tag: "家族通知", time: "昨天 18:02", title: "修谱资料征集", content: "请家人补充老照片中的人物姓名、拍摄时间和地点。无法确认的信息也可以先写下线索,由熟悉往事的长辈共同核对。", author: "谱主" },
|
||||
];
|
||||
const feedId = ref("1");
|
||||
const currentFeed = ref(feedRecords[0]);
|
||||
const feedState = ref("loading");
|
||||
const commentState = ref("idle");
|
||||
const commentDraft = ref("");
|
||||
const commentError = ref("");
|
||||
const toastVisible = ref(false);
|
||||
const forceCommentFailure = ref(false);
|
||||
const feedComments = ref([
|
||||
{ id: 1, author: "汤淑华", time: "今天 10:42", content: "一家人能常常相聚,就是最珍贵的福气。" },
|
||||
{ id: 2, author: "汤文清", time: "今天 11:08", content: "照片已经整理好了,晚些时候放进春节团圆相册。" },
|
||||
]);
|
||||
let submitTimer = null;
|
||||
let toastTimer = null;
|
||||
|
||||
onLoad((query) => {
|
||||
feedId.value = String(query.feedId || "1");
|
||||
const selected = feedRecords.find((item) => item.id === feedId.value);
|
||||
currentFeed.value = selected || feedRecords[0];
|
||||
forceCommentFailure.value = query.commentResult === "error";
|
||||
feedState.value = ["loading", "error", "expired"].includes(query.state)
|
||||
? query.state
|
||||
: selected
|
||||
? "ready"
|
||||
: "expired";
|
||||
});
|
||||
|
||||
const submitComment = () => {
|
||||
if (commentState.value === "saving") return;
|
||||
const content = commentDraft.value.trim();
|
||||
if (!content) {
|
||||
commentError.value = "请先写下评论内容";
|
||||
return;
|
||||
}
|
||||
commentError.value = "";
|
||||
commentState.value = "saving";
|
||||
submitTimer = setTimeout(() => {
|
||||
if (forceCommentFailure.value) {
|
||||
commentState.value = "error";
|
||||
commentError.value = "评论发送失败,请保留文字后重试";
|
||||
forceCommentFailure.value = false;
|
||||
return;
|
||||
}
|
||||
feedComments.value.push({ id: Date.now(), author: "我", time: "刚刚", content });
|
||||
commentDraft.value = "";
|
||||
commentState.value = "idle";
|
||||
toastVisible.value = true;
|
||||
toastTimer = setTimeout(() => { toastVisible.value = false; }, 1800);
|
||||
}, 320);
|
||||
};
|
||||
const restoreFeed = () => { feedState.value = "ready"; };
|
||||
const backToFamily = () => uni.redirectTo({ url: "/pages/family/f01-family-feed" });
|
||||
|
||||
onUnmounted(() => {
|
||||
if (submitTimer) clearTimeout(submitTimer);
|
||||
if (toastTimer) clearTimeout(toastTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.feed-detail-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.feed-detail-header, .feed-detail-loading, .feed-detail-content { z-index: 1; }
|
||||
.feed-detail-loading { min-height: calc(100vh - 100rpx); }
|
||||
.feed-detail-content { padding: 18rpx 24rpx 72rpx; }
|
||||
.feed-article-card, .feed-comments-panel, .feed-comment-form, .feed-state-card { width: 100%; box-sizing: border-box; background: url("/static/assets/modules/family/transparent/module-content-frame.png") center / 100% 100% no-repeat; }
|
||||
.feed-article-card { padding: 46rpx 48rpx 42rpx; }
|
||||
.feed-article-card text, .feed-state-card > text { display: block; }
|
||||
.feed-article-card__meta, .feed-article-card__author { color: $ink-muted; font-size: 22rpx; }
|
||||
.feed-article-card__title { margin-top: 12rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 36rpx; font-weight: 700; }
|
||||
.feed-article-card__body { margin-top: 18rpx; color: $ink; font-size: 25rpx; line-height: 1.75; }
|
||||
.feed-article-card__author { margin-top: 22rpx; }
|
||||
.feed-comments-panel { margin-top: 18rpx; padding: 38rpx 38rpx 34rpx; }
|
||||
.feed-comments-heading { display: flex; align-items: center; justify-content: space-between; color: $brand-red; font-size: 24rpx; font-weight: 700; }
|
||||
.feed-comments-heading text:last-child { color: $ink-muted; font-size: 21rpx; font-weight: 400; }
|
||||
.feed-comment-card { margin-top: 16rpx; padding: 20rpx 22rpx; background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat; }
|
||||
.feed-comment-card > view { display: flex; justify-content: space-between; gap: 18rpx; color: $ink-muted; font-size: 20rpx; }
|
||||
.feed-comment-card > text { display: block; margin-top: 9rpx; color: $ink; font-size: 24rpx; line-height: 1.55; }
|
||||
.feed-comments-empty { padding: 34rpx 10rpx 20rpx; text-align: center; }
|
||||
.feed-comments-empty text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.6; }
|
||||
.feed-comments-empty text:first-child { color: $ink; font-size: 28rpx; font-weight: 700; }
|
||||
.feed-comment-form { margin-top: 18rpx; padding: 38rpx 40rpx 42rpx; }
|
||||
.feed-comment-form > text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 30rpx; font-weight: 700; }
|
||||
.feed-comment-form textarea { width: auto; min-width: 0; min-height: 126rpx; margin-top: 16rpx; padding: 20rpx 22rpx; box-sizing: border-box; color: $ink; font-size: 24rpx; line-height: 1.55; background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat; }
|
||||
.feed-comment-error { display: block; margin-top: 10rpx; color: $brand-red; font-size: 22rpx; }
|
||||
.feed-comment-form .app-button { margin-top: 22rpx; }
|
||||
.feed-state-card { min-height: 340rpx; margin-top: 36rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
|
||||
.feed-state-card > text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 35rpx; font-weight: 700; }
|
||||
.feed-state-card > text:nth-child(2) { margin-top: 18rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.65; }
|
||||
.feed-state-card .app-button { margin-top: 30rpx; }
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,117 @@
|
||||
<!-- 页面编号:F-04;用途:谱文分类与文章列表。 -->
|
||||
<template><ModulePage page-id="f04" /></template>
|
||||
<!-- 页面编号:F-04;用途:谱文分类、搜索、列表与新建入口。 -->
|
||||
<template>
|
||||
<view class="article-list-page" :class="{ 'article-list-state--loading': listState === 'loading', 'article-list-state--empty': listState === 'empty', 'article-list-state--error': listState === 'error' }">
|
||||
<ModulePageBackground module="family" />
|
||||
<view class="article-list-header"><PageHeader title="谱文" action="新建" @action="createArticle" /></view>
|
||||
|
||||
<view v-if="listState === 'loading'" class="article-list-loading">
|
||||
<AppLoading text="正在整理家族谱文" description="请稍候,正在读取家训、往事与序言。" />
|
||||
</view>
|
||||
|
||||
<view v-else class="article-list-content">
|
||||
<template v-if="listState === 'ready'">
|
||||
<view class="article-search">
|
||||
<input v-model="keyword" placeholder="搜索标题、作者或正文" confirm-type="search" />
|
||||
</view>
|
||||
<view class="article-categories">
|
||||
<view class="article-categories__row">
|
||||
<view v-for="category in articleCategories" :key="category" class="article-category" :class="{ 'article-category--active': activeCategory === category }" @click="activeCategory = category"><text>{{ category }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="filteredArticles.length" class="article-list">
|
||||
<view v-for="article in filteredArticles" :key="article.id" class="article-card" role="button" :aria-label="`查看谱文${article.title}`" @click="openArticle(article)">
|
||||
<text class="article-card__category">{{ article.category }}</text>
|
||||
<text class="article-card__title">{{ article.title }}</text>
|
||||
<text class="article-card__summary">{{ article.summary }}</text>
|
||||
<view class="article-card__meta"><text>{{ article.author }}</text><text>{{ article.updatedAt }}</text></view>
|
||||
</view>
|
||||
<AppButton block label="新建谱文" @click="createArticle" />
|
||||
</view>
|
||||
|
||||
<view v-else class="article-list-state-card">
|
||||
<text>{{ keyword || activeCategory !== '全部' ? '没有找到相关谱文' : '还没有谱文' }}</text>
|
||||
<text>{{ keyword || activeCategory !== '全部' ? '换一个关键词或分类继续查找。' : '从第一篇家训、序言或家族往事开始记录。' }}</text>
|
||||
<AppButton block :label="keyword || activeCategory !== '全部' ? '清空筛选' : '新建谱文'" @click="keyword || activeCategory !== '全部' ? resetFilters() : createArticle()" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-else class="article-list-state-card">
|
||||
<text>{{ listState === 'empty' ? '还没有谱文' : '谱文列表暂不可用' }}</text>
|
||||
<text>{{ listState === 'empty' ? '记录第一篇家风家训或家族往事。' : '请稍后重新查看,已有谱文不会受到影响。' }}</text>
|
||||
<AppButton :type="listState === 'error' ? 'secondary' : 'primary'" block :label="listState === 'error' ? '重新查看' : '新建谱文'" @click="listState === 'error' ? restoreArticles() : createArticle()" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const articleCategories = ["全部", "家风家训", "家族往事", "族谱序言"];
|
||||
const baseArticles = [
|
||||
{ id: "101", category: "家风家训", title: "孝友传家的日常", summary: "从敬老、睦亲与守信的小事里,看见家风如何代代相传。", author: "汤文正", updatedAt: "今天更新" },
|
||||
{ id: "102", category: "家族往事", title: "祖居门前的那棵桂花树", summary: "长辈口述的旧居记忆,以及每年中秋一家人相聚的故事。", author: "汤淑华", updatedAt: "昨天更新" },
|
||||
{ id: "103", category: "族谱序言", title: "续修族谱序", summary: "说明本次续修的缘起、资料来源与共同参与的家人。", author: "谱主", updatedAt: "5 月 12 日" },
|
||||
];
|
||||
const articles = ref([...baseArticles]);
|
||||
const activeCategory = ref("全部");
|
||||
const keyword = ref("");
|
||||
const listState = ref("loading");
|
||||
const filteredArticles = computed(() => {
|
||||
const term = keyword.value.trim().toLowerCase();
|
||||
return articles.value.filter((article) => {
|
||||
const categoryMatched = activeCategory.value === "全部" || article.category === activeCategory.value;
|
||||
const keywordMatched = !term || `${article.title} ${article.summary} ${article.author}`.toLowerCase().includes(term);
|
||||
return categoryMatched && keywordMatched;
|
||||
});
|
||||
});
|
||||
|
||||
onLoad((query) => {
|
||||
const count = Math.max(1, Math.min(Number(query.count) || baseArticles.length, 50));
|
||||
articles.value = Array.from({ length: count }, (_, index) => ({
|
||||
...baseArticles[index % baseArticles.length],
|
||||
id: String(101 + index),
|
||||
title: count > baseArticles.length ? `${baseArticles[index % baseArticles.length].title}(第 ${index + 1} 篇)` : baseArticles[index].title,
|
||||
}));
|
||||
listState.value = ["loading", "empty", "error"].includes(query.state) ? query.state : "ready";
|
||||
});
|
||||
|
||||
const openArticle = (article) => uni.navigateTo({ url: `/pages/family/f05-article-detail?articleId=${article.id}` });
|
||||
const createArticle = () => uni.navigateTo({ url: "/pages/family/f06-article-editor?mode=create" });
|
||||
const restoreArticles = () => { listState.value = "ready"; };
|
||||
const resetFilters = () => { keyword.value = ""; activeCategory.value = "全部"; };
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.article-list-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.article-list-header, .article-list-loading, .article-list-content { z-index: 1; }
|
||||
.article-list-loading { min-height: calc(100vh - 100rpx); }
|
||||
.article-list-content { padding: 18rpx 24rpx 72rpx; }
|
||||
.article-search { min-height: 82rpx; padding: 12rpx 26rpx; box-sizing: border-box; background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat; }
|
||||
.article-search input { width: 100%; min-height: 58rpx; color: $ink; font-size: 24rpx; }
|
||||
.article-categories { width: 100%; margin-top: 16rpx; }
|
||||
.article-categories__row { display: flex; flex-wrap: wrap; gap: 12rpx; padding: 2rpx 4rpx 8rpx; }
|
||||
.article-category { min-height: 58rpx; padding: 0 28rpx; color: $ink-muted; font-size: 23rpx; background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat; }
|
||||
.article-category text { display: flex; min-height: 58rpx; align-items: center; }
|
||||
.article-category--active { color: $brand-red; font-weight: 700; }
|
||||
.article-list { display: flex; flex-direction: column; gap: 16rpx; margin-top: 10rpx; }
|
||||
.article-card, .article-list-state-card { width: 100%; box-sizing: border-box; background: url("/static/assets/modules/family/transparent/module-content-frame.png") center / 100% 100% no-repeat; }
|
||||
.article-card { min-height: 196rpx; padding: 34rpx 46rpx 30rpx; }
|
||||
.article-card > text { display: block; }
|
||||
.article-card__category { color: $brand-red; font-size: 21rpx; font-weight: 700; letter-spacing: 2rpx; }
|
||||
.article-card__title { margin-top: 7rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 31rpx; font-weight: 700; }
|
||||
.article-card__summary { margin-top: 10rpx; color: $ink-muted; font-size: 23rpx; line-height: 1.55; }
|
||||
.article-card__meta { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 6rpx 18rpx; margin-top: 12rpx; color: $ink-muted; font-size: 20rpx; }
|
||||
.article-list > .app-button { margin-top: 8rpx; }
|
||||
.article-list-state-card { min-height: 340rpx; margin-top: 30rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
|
||||
.article-list-state-card > text { display: block; }
|
||||
.article-list-state-card > text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 35rpx; font-weight: 700; }
|
||||
.article-list-state-card > text:nth-child(2) { margin-top: 18rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.65; }
|
||||
.article-list-state-card .app-button { margin-top: 28rpx; }
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,113 @@
|
||||
<!-- 页面编号:F-05;用途:谱文详情。 -->
|
||||
<template><ModulePage page-id="f05" /></template>
|
||||
<!-- 页面编号:F-05;用途:谱文正文、收藏、编辑与受控状态。 -->
|
||||
<template>
|
||||
<view class="article-detail-page" :class="articleStateClasses">
|
||||
<ModulePageBackground module="family" />
|
||||
<view class="article-detail-header"><PageHeader title="谱文详情" /></view>
|
||||
|
||||
<view v-if="articleState === 'loading'" class="article-detail-loading">
|
||||
<AppLoading text="正在读取谱文" description="请稍候,正在整理正文与收录信息。" />
|
||||
</view>
|
||||
|
||||
<view v-else class="article-detail-content">
|
||||
<template v-if="articleState === 'ready'">
|
||||
<view class="article-paper">
|
||||
<text class="article-paper__category">{{ article.category }}</text>
|
||||
<text class="article-paper__title">{{ article.title }}</text>
|
||||
<view class="article-paper__meta"><text>{{ article.author }}</text><text>{{ article.updatedAt }}</text></view>
|
||||
<view class="article-paper__body">
|
||||
<text v-for="(paragraph, index) in articleParagraphs" :key="index">{{ paragraph }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="article-actions">
|
||||
<AppButton block :label="favorite ? '已收藏谱文' : '收藏谱文'" @click="toggleFavorite" />
|
||||
<AppButton type="secondary" block label="编辑谱文" @click="editArticle" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-else class="article-state-card">
|
||||
<text>{{ stateCopy.title }}</text>
|
||||
<text>{{ stateCopy.copy }}</text>
|
||||
<AppButton :type="articleState === 'error' ? 'secondary' : 'primary'" block :label="stateCopy.action" @click="handleStateAction" />
|
||||
</view>
|
||||
</view>
|
||||
<AppToast :visible="toastVisible" :message="favorite ? '已收藏谱文' : '已取消收藏'" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { computed, onUnmounted, reactive, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const articleRecords = [
|
||||
{ id: "101", category: "家风家训", title: "孝友传家的日常", author: "汤文正", updatedAt: "2024 年 5 月 12 日", paragraphs: ["孝友传家,不只在族谱序言里,也在一家人每日的言行中。长辈以宽厚待晚辈,晚辈以耐心照料长辈,亲友之间守信互助,便是最朴素也最长久的家风。", "勤俭并非一味节省,而是珍惜所得、量入为出,也愿意在家人需要时伸出援手。家中每一代人都可以用自己的方式,把这份分寸与担当继续传下去。", "敬祖睦宗,最终是为了让今天的家人彼此认识、彼此关心。记录姓名与世代之外,也应留下真实的生活、共同经历和温暖记忆。"] },
|
||||
{ id: "102", category: "家族往事", title: "祖居门前的那棵桂花树", author: "汤淑华", updatedAt: "2024 年 5 月 10 日", paragraphs: ["祖居门前曾有一棵桂花树。每到中秋,院里都是清甜的香气,远道回来的家人也总能循着那股味道找到家门。", "后来房屋几经修缮,桂花树仍被大家小心保留下来。它见过孩子长大,也见过长辈把往事一遍遍讲给后来人。"] },
|
||||
{ id: "103", category: "族谱序言", title: "续修族谱序", author: "谱主", updatedAt: "2024 年 5 月 8 日", paragraphs: ["本次续修以旧谱、碑记、户籍资料和长辈口述为基础,由家人共同核对补充。凡暂不能确认之处,均保留来源和疑问,留待后续查证。", "愿这份记录不仅理清世系,也能保存家风、人物与共同记忆。"] },
|
||||
];
|
||||
const articleId = ref("101");
|
||||
const article = reactive({ ...articleRecords[0] });
|
||||
const articleState = ref("loading");
|
||||
const favorite = ref(false);
|
||||
const toastVisible = ref(false);
|
||||
let toastTimer = null;
|
||||
const articleParagraphs = computed(() => article.paragraphs || []);
|
||||
const articleStateClasses = computed(() => ({
|
||||
[`article-state--${articleState.value}`]: true,
|
||||
"article-state--expired": articleState.value === "expired",
|
||||
"article-state--privacy": articleState.value === "privacy",
|
||||
"article-state--error": articleState.value === "error",
|
||||
}));
|
||||
const stateCopy = computed(() => ({
|
||||
expired: { title: "这篇谱文已无法查看", copy: "内容可能已被作者删除或取消公开,请返回谱文列表查看其他内容。", action: "返回谱文列表" },
|
||||
privacy: { title: "这篇谱文暂未公开", copy: "作者仅向有权限的家人开放正文,请返回谱文列表查看其他内容。", action: "返回谱文列表" },
|
||||
error: { title: "谱文暂不可用", copy: "请稍后重新查看,已有谱文不会受到影响。", action: "重新查看" },
|
||||
}[articleState.value] || {}));
|
||||
|
||||
onLoad((query) => {
|
||||
articleId.value = String(query.articleId || "101");
|
||||
const selected = articleRecords.find((item) => item.id === articleId.value);
|
||||
if (selected) Object.assign(article, selected);
|
||||
articleState.value = ["loading", "error", "expired", "privacy"].includes(query.state)
|
||||
? query.state
|
||||
: selected
|
||||
? "ready"
|
||||
: "expired";
|
||||
});
|
||||
|
||||
const toggleFavorite = () => {
|
||||
favorite.value = !favorite.value;
|
||||
toastVisible.value = true;
|
||||
if (toastTimer) clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => { toastVisible.value = false; }, 1800);
|
||||
};
|
||||
const editArticle = () => uni.navigateTo({ url: `/pages/family/f06-article-editor?mode=edit&articleId=${articleId.value}` });
|
||||
const backToArticles = () => uni.redirectTo({ url: "/pages/family/f04-article-list" });
|
||||
const handleStateAction = () => { if (articleState.value === "error") articleState.value = "ready"; else backToArticles(); };
|
||||
onUnmounted(() => { if (toastTimer) clearTimeout(toastTimer); });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.article-detail-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.article-detail-header, .article-detail-loading, .article-detail-content { z-index: 1; }
|
||||
.article-detail-loading { min-height: calc(100vh - 100rpx); }
|
||||
.article-detail-content { padding: 18rpx 24rpx 72rpx; }
|
||||
.article-paper, .article-state-card { width: 100%; box-sizing: border-box; background: url("/static/assets/modules/family/transparent/module-content-frame.png") center / 100% 100% no-repeat; }
|
||||
.article-paper { padding: 50rpx 48rpx 54rpx; }
|
||||
.article-paper > text { display: block; }
|
||||
.article-paper__category { color: $brand-red; font-size: 22rpx; font-weight: 700; letter-spacing: 3rpx; }
|
||||
.article-paper__title { margin-top: 12rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 39rpx; font-weight: 700; line-height: 1.35; }
|
||||
.article-paper__meta { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 6rpx 18rpx; margin-top: 16rpx; color: $ink-muted; font-size: 21rpx; }
|
||||
.article-paper__body { margin-top: 28rpx; }
|
||||
.article-paper__body text { display: block; margin-top: 18rpx; color: $ink; font-size: 25rpx; line-height: 1.85; text-align: justify; }
|
||||
.article-actions { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14rpx; margin-top: 18rpx; }
|
||||
.article-state-card { min-height: 350rpx; margin-top: 36rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
|
||||
.article-state-card > text { display: block; }
|
||||
.article-state-card > text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 35rpx; font-weight: 700; }
|
||||
.article-state-card > text:nth-child(2) { margin-top: 18rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.65; }
|
||||
.article-state-card .app-button { margin-top: 30rpx; }
|
||||
@media (max-width: 340px) { .article-actions { grid-template-columns: minmax(0, 1fr); } }
|
||||
</style>
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
class="editor-control"
|
||||
:class="{ 'editor-control--error': fieldErrors.title }"
|
||||
>
|
||||
<image :src="fieldFrame" mode="scaleToFill" />
|
||||
<input
|
||||
v-model="form.title"
|
||||
maxlength="40"
|
||||
@@ -67,7 +66,6 @@
|
||||
class="editor-control"
|
||||
:class="{ 'editor-control--error': fieldErrors.category }"
|
||||
>
|
||||
<image :src="fieldFrame" mode="scaleToFill" />
|
||||
<input
|
||||
v-model="form.category"
|
||||
maxlength="20"
|
||||
@@ -87,7 +85,6 @@
|
||||
class="editor-control editor-control--textarea"
|
||||
:class="{ 'editor-control--error': fieldErrors.content }"
|
||||
>
|
||||
<image :src="fieldFrame" mode="scaleToFill" />
|
||||
<textarea
|
||||
v-model="form.content"
|
||||
maxlength="1200"
|
||||
@@ -127,8 +124,6 @@ import AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const fieldFrame =
|
||||
"/static/assets/modules/family/transparent/module-field-frame.png";
|
||||
const draftFixture = {
|
||||
title: "汤氏家训辑录",
|
||||
category: "家风家训",
|
||||
@@ -136,6 +131,9 @@ const draftFixture = {
|
||||
"孝友传家,勤俭立业;敬祖睦宗,诚实待人。愿后人常怀感恩,彼此扶持。",
|
||||
};
|
||||
const editorState = ref("form");
|
||||
const articleId = ref("");
|
||||
const editorMode = ref("create");
|
||||
const simulateSaveFailure = ref(false);
|
||||
const allowedStates = new Set([
|
||||
"draft",
|
||||
"loading",
|
||||
@@ -156,8 +154,8 @@ const actionLabel = computed(() =>
|
||||
: "保存谱文",
|
||||
);
|
||||
|
||||
const fillDraft = (failed = false) => {
|
||||
Object.assign(form, draftFixture, failed ? { title: "保存失败" } : {});
|
||||
const fillDraft = () => {
|
||||
Object.assign(form, draftFixture);
|
||||
};
|
||||
const showAllFieldErrors = () => {
|
||||
fieldErrors.title = "请填写文章标题";
|
||||
@@ -166,9 +164,15 @@ const showAllFieldErrors = () => {
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
articleId.value = query.articleId || "";
|
||||
editorMode.value = articleId.value || query.mode === "edit" ? "edit" : "create";
|
||||
const requestedState = allowedStates.has(query.state) ? query.state : "form";
|
||||
if (["draft", "saving", "error"].includes(requestedState)) {
|
||||
fillDraft(requestedState === "error");
|
||||
simulateSaveFailure.value = requestedState === "error";
|
||||
if (
|
||||
editorMode.value === "edit" ||
|
||||
["draft", "saving", "error"].includes(requestedState)
|
||||
) {
|
||||
fillDraft();
|
||||
}
|
||||
if (requestedState === "validation") showAllFieldErrors();
|
||||
editorState.value = requestedState;
|
||||
@@ -197,12 +201,18 @@ const submit = () => {
|
||||
}
|
||||
editorState.value = "saving";
|
||||
saveTimer = setTimeout(() => {
|
||||
editorState.value =
|
||||
form.title.trim() === "保存失败" ? "error" : "success";
|
||||
editorState.value = simulateSaveFailure.value ? "error" : "success";
|
||||
simulateSaveFailure.value = false;
|
||||
saveTimer = null;
|
||||
}, 320);
|
||||
};
|
||||
const returnToList = () => uni.navigateBack();
|
||||
const returnToList = () =>
|
||||
uni.redirectTo({
|
||||
url:
|
||||
editorMode.value === "edit" && articleId.value
|
||||
? `/pages/family/f05-article-detail?articleId=${articleId.value}`
|
||||
: "/pages/family/f04-article-list",
|
||||
});
|
||||
|
||||
onUnload(() => {
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
@@ -211,16 +221,15 @@ onUnload(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.article-editor-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.article-editor-page__header,
|
||||
.article-editor-loading,
|
||||
.article-editor-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
}
|
||||
.article-editor-loading {
|
||||
padding-top: 150rpx;
|
||||
@@ -230,7 +239,6 @@ onUnload(() => {
|
||||
}
|
||||
.editor-panel,
|
||||
.editor-result-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border: 20rpx solid transparent;
|
||||
border-image-source: url("/static/assets/modules/family/transparent/module-content-frame.png");
|
||||
@@ -240,8 +248,6 @@ onUnload(() => {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.editor-panel__body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 38rpx 34rpx 42rpx;
|
||||
}
|
||||
.editor-eyebrow {
|
||||
@@ -279,22 +285,13 @@ onUnload(() => {
|
||||
font-weight: 700;
|
||||
}
|
||||
.editor-control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 82rpx;
|
||||
min-height: 82rpx;
|
||||
align-items: center;
|
||||
}
|
||||
.editor-control > image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.editor-control input,
|
||||
.editor-control textarea {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
color: $ink;
|
||||
@@ -302,17 +299,17 @@ onUnload(() => {
|
||||
}
|
||||
.editor-control input {
|
||||
width: 100%;
|
||||
height: 82rpx;
|
||||
min-height: 82rpx;
|
||||
padding: 0 28rpx;
|
||||
}
|
||||
.editor-control--textarea {
|
||||
height: 220rpx;
|
||||
min-height: 220rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.editor-control textarea {
|
||||
width: 100%;
|
||||
height: 180rpx;
|
||||
min-height: 180rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.editor-control--error {
|
||||
@@ -350,8 +347,6 @@ onUnload(() => {
|
||||
min-height: 420rpx;
|
||||
}
|
||||
.editor-result-card__body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 112rpx 52rpx 68rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,131 @@
|
||||
<!-- 页面编号:F-07;用途:家族相册列表。 -->
|
||||
<template><ModulePage page-id="f07" /></template>
|
||||
<!-- 页面编号:F-07;用途:家族相册列表、创建与受控状态。 -->
|
||||
<template>
|
||||
<view class="album-list-page" :class="albumStateClasses">
|
||||
<ModulePageBackground module="family" />
|
||||
<view class="album-list-header"><PageHeader title="家族相册" action="新建" @action="createAlbum" /></view>
|
||||
|
||||
<view v-if="albumState === 'loading'" class="album-list-loading">
|
||||
<AppLoading text="正在整理家族相册" description="请稍候,正在读取照片与更新时间。" />
|
||||
</view>
|
||||
|
||||
<view v-else class="album-list-content">
|
||||
<template v-if="albumState === 'ready'">
|
||||
<view class="album-list-lead"><text>让每一张照片都回到家人身边</text><text>共 {{ albums.length }} 本相册</text></view>
|
||||
<view v-if="albums.length" class="album-list">
|
||||
<view v-for="album in albums" :key="album.id" class="album-card" role="button" :aria-label="`打开相册${album.name}`" @click="openAlbum(album)">
|
||||
<view class="album-card__media"><image class="album-card__cover" :src="album.cover" mode="aspectFill" :alt="album.name" /></view>
|
||||
<view class="album-card__copy">
|
||||
<text>{{ album.name }}</text>
|
||||
<text>{{ album.photoCount }} 张照片 · {{ album.updatedAt }}</text>
|
||||
<text>{{ album.description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<AppButton block label="新建相册" @click="createAlbum" />
|
||||
</view>
|
||||
<view v-else class="album-state-card">
|
||||
<text>还没有相册</text><text>创建一本相册,把团圆、成长与祖居旧影整理在一起。</text>
|
||||
<AppButton block label="新建相册" @click="createAlbum" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-else class="album-state-card">
|
||||
<text>{{ albumState === 'empty' ? '还没有相册' : '相册暂不可用' }}</text>
|
||||
<text>{{ albumState === 'empty' ? '从第一本团圆相册开始收集家族影像。' : '请稍后重新查看,已有照片不会受到影响。' }}</text>
|
||||
<AppButton :type="albumState === 'error' ? 'secondary' : 'primary'" block :label="albumState === 'error' ? '重新查看' : '新建相册'" @click="albumState === 'error' ? restoreAlbums() : createAlbum()" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AppDialog :visible="dialogVisible" eyebrow="新建相册" title="为家人整理一段影像" message="相册创建后可继续添加照片和说明。" confirm-text="创建相册" cancel-text="取消" show-cancel @confirm="confirmCreateAlbum" @cancel="closeCreateDialog">
|
||||
<view class="album-dialog-field">
|
||||
<text>相册名称</text>
|
||||
<input v-model="albumNameDraft" maxlength="30" placeholder="例如:春节团圆" />
|
||||
<text v-if="albumNameError">{{ albumNameError }}</text>
|
||||
</view>
|
||||
</AppDialog>
|
||||
<AppToast :visible="toastVisible" message="相册已创建" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { computed, onUnmounted, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const baseAlbums = [
|
||||
{ id: "201", name: "2024 春节团圆", photoCount: 18, updatedAt: "今天更新", description: "三代家人的团圆饭与院前合影", cover: "/static/assets/modules/family/f08/f08-reunion-hero.png" },
|
||||
{ id: "202", name: "祖居旧影", photoCount: 32, updatedAt: "5 月 10 日更新", description: "祖居、旧物与长辈珍藏的老照片", cover: "/static/assets/modules/family/f08/f08-ancestral-home.png" },
|
||||
{ id: "203", name: "儿童成长", photoCount: 46, updatedAt: "持续更新", description: "记录孩子们每一个值得珍藏的瞬间", cover: "/static/assets/modules/family/f08/f08-family-portrait.png" },
|
||||
];
|
||||
const albums = ref([...baseAlbums]);
|
||||
const albumState = ref("loading");
|
||||
const dialogVisible = ref(false);
|
||||
const albumNameDraft = ref("");
|
||||
const albumNameError = ref("");
|
||||
const toastVisible = ref(false);
|
||||
let toastTimer = null;
|
||||
const albumStateClasses = computed(() => ({
|
||||
[`album-list-state--${albumState.value}`]: true,
|
||||
"album-state--empty": albumState.value === "empty",
|
||||
"album-list-state--loading": albumState.value === "loading",
|
||||
"album-list-state--error": albumState.value === "error",
|
||||
}));
|
||||
|
||||
onLoad((query) => {
|
||||
const count = Math.max(1, Math.min(Number(query.count) || baseAlbums.length, 30));
|
||||
albums.value = Array.from({ length: count }, (_, index) => ({
|
||||
...baseAlbums[index % baseAlbums.length],
|
||||
id: String(201 + index),
|
||||
name: count > baseAlbums.length ? `${baseAlbums[index % baseAlbums.length].name}(${index + 1})` : baseAlbums[index].name,
|
||||
}));
|
||||
albumState.value = ["loading", "empty", "error"].includes(query.state) ? query.state : "ready";
|
||||
});
|
||||
|
||||
const openAlbum = (album) => uni.navigateTo({ url: `/pages/family/f08-album-detail?albumId=${album.id}` });
|
||||
const createAlbum = () => { albumNameDraft.value = ""; albumNameError.value = ""; dialogVisible.value = true; };
|
||||
const closeCreateDialog = () => { dialogVisible.value = false; };
|
||||
const confirmCreateAlbum = () => {
|
||||
const name = albumNameDraft.value.trim();
|
||||
if (!name) { albumNameError.value = "请填写相册名称"; return; }
|
||||
albums.value.unshift({ id: String(Date.now()), name, photoCount: 0, updatedAt: "刚刚创建", description: "等待添加第一张照片", cover: "/static/assets/modules/family/f08/f08-reunion-hero.png" });
|
||||
albumState.value = "ready";
|
||||
dialogVisible.value = false;
|
||||
toastVisible.value = true;
|
||||
toastTimer = setTimeout(() => { toastVisible.value = false; }, 1800);
|
||||
};
|
||||
const restoreAlbums = () => { albumState.value = "ready"; };
|
||||
onUnmounted(() => { if (toastTimer) clearTimeout(toastTimer); });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.album-list-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.album-list-header, .album-list-loading, .album-list-content { z-index: 1; }
|
||||
.album-list-loading { min-height: calc(100vh - 100rpx); }
|
||||
.album-list-content { padding: 18rpx 24rpx 72rpx; }
|
||||
.album-list-lead { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8rpx 18rpx; min-height: 62rpx; padding: 0 20rpx; color: $ink-muted; font-size: 22rpx; background: url("/static/assets/modules/genealogy/transparent/section-divider.png") center / 100% 100% no-repeat; }
|
||||
.album-list { display: flex; flex-direction: column; gap: 16rpx; margin-top: 16rpx; }
|
||||
.album-card, .album-state-card { width: 100%; box-sizing: border-box; background: url("/static/assets/modules/family/transparent/module-content-frame.png") center / 100% 100% no-repeat; }
|
||||
.album-card { display: grid; grid-template-columns: minmax(150rpx, 0.7fr) minmax(0, 1.3fr); min-height: 210rpx; gap: 22rpx; padding: 30rpx 38rpx; }
|
||||
.album-card__media { width: 100%; aspect-ratio: 4 / 3; align-self: center; }
|
||||
.album-card__cover { display: block; width: 100%; height: 100%; }
|
||||
.album-card__copy { align-self: center; min-width: 0; }
|
||||
.album-card__copy text { display: block; overflow-wrap: anywhere; }
|
||||
.album-card__copy text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 30rpx; font-weight: 700; }
|
||||
.album-card__copy text:nth-child(2) { margin-top: 9rpx; color: $brand-red; font-size: 21rpx; }
|
||||
.album-card__copy text:last-child { margin-top: 8rpx; color: $ink-muted; font-size: 22rpx; line-height: 1.5; }
|
||||
.album-list > .app-button { margin-top: 8rpx; }
|
||||
.album-state-card { min-height: 340rpx; margin-top: 30rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
|
||||
.album-state-card > text { display: block; }
|
||||
.album-state-card > text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 35rpx; font-weight: 700; }
|
||||
.album-state-card > text:nth-child(2) { margin-top: 18rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.65; }
|
||||
.album-state-card .app-button { margin-top: 28rpx; }
|
||||
.album-dialog-field { width: 100%; margin: 24rpx 0; text-align: left; }
|
||||
.album-dialog-field > text:first-child { display: block; color: $ink; font-size: 23rpx; font-weight: 700; }
|
||||
.album-dialog-field input { width: 100%; min-height: 76rpx; margin-top: 10rpx; padding: 0 22rpx; box-sizing: border-box; color: $ink; font-size: 24rpx; background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat; }
|
||||
.album-dialog-field > text:last-child { display: block; margin-top: 8rpx; color: $brand-red; font-size: 21rpx; }
|
||||
@media (max-width: 340px) { .album-card { grid-template-columns: 130rpx minmax(0, 1fr); gap: 16rpx; padding-right: 30rpx; padding-left: 30rpx; } }
|
||||
</style>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="album-upload-action" @click="showUploadNotice">
|
||||
<view class="album-upload-action" @click="toUpload">
|
||||
<AppButton block label="添加照片" />
|
||||
</view>
|
||||
</template>
|
||||
@@ -93,24 +93,20 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AppToast :visible="uploadNoticeVisible" :message="uploadNoticeMessage" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import { onBackPress, onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const albumState = ref("normal");
|
||||
const albumId = ref("");
|
||||
const previewVisible = ref(false);
|
||||
const previewIndex = ref(0);
|
||||
const uploadNoticeVisible = ref(false);
|
||||
const uploadNoticeMessage = "上传照片将在入口接通后开放";
|
||||
let uploadNoticeTimer;
|
||||
|
||||
const photos = [
|
||||
{ src: "/static/assets/modules/family/f08/f08-reunion-hero.png", alt: "春节团圆时三代家人的合影", caption: "除夕团圆 · 2024" },
|
||||
@@ -131,19 +127,17 @@ const closePreview = () => {
|
||||
albumState.value = "normal";
|
||||
};
|
||||
|
||||
const showUploadNotice = () => {
|
||||
uploadNoticeVisible.value = true;
|
||||
clearTimeout(uploadNoticeTimer);
|
||||
uploadNoticeTimer = setTimeout(() => {
|
||||
uploadNoticeVisible.value = false;
|
||||
}, 1800);
|
||||
};
|
||||
const toUpload = () =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/family/f09-media-upload?albumId=${albumId.value}`,
|
||||
});
|
||||
|
||||
const returnToAlbums = () => {
|
||||
uni.redirectTo({ url: "/pages/family/f07-album-list" });
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
albumId.value = query.albumId || "reunion";
|
||||
albumState.value =
|
||||
query.state === "empty"
|
||||
? "empty"
|
||||
@@ -161,22 +155,18 @@ onBackPress(() => {
|
||||
return true;
|
||||
});
|
||||
|
||||
onUnload(() => {
|
||||
clearTimeout(uploadNoticeTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.album-detail-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.album-detail-header,
|
||||
.album-detail-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
}
|
||||
.album-detail-content {
|
||||
padding: 22rpx 24rpx calc(48rpx + env(safe-area-inset-bottom));
|
||||
@@ -265,7 +255,6 @@ onUnload(() => {
|
||||
}
|
||||
.album-empty-state,
|
||||
.album-expired-state {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 430rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -237,6 +237,7 @@ const mockLibrary = [
|
||||
];
|
||||
|
||||
const selectedPhotos = ref([]);
|
||||
const albumId = ref("");
|
||||
const activePhoto = computed(
|
||||
() => selectedPhotos.value[activePhotoIndex.value] || null,
|
||||
);
|
||||
@@ -322,10 +323,13 @@ const retryFailed = () => {
|
||||
};
|
||||
|
||||
const returnToAlbum = () => {
|
||||
uni.redirectTo({ url: "/pages/family/f08-album-detail" });
|
||||
uni.redirectTo({
|
||||
url: `/pages/family/f08-album-detail?albumId=${albumId.value}`,
|
||||
});
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
albumId.value = query.albumId || "reunion";
|
||||
uploadState.value = ["permission", "selected", "uploading", "error", "success"].includes(query.state)
|
||||
? query.state
|
||||
: "initial";
|
||||
@@ -339,21 +343,19 @@ onLoad((query) => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.media-upload-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.media-upload-header,
|
||||
.media-upload-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
}
|
||||
.media-upload-content {
|
||||
padding: 20rpx 24rpx calc(48rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.media-album-card {
|
||||
position: relative;
|
||||
display: grid;
|
||||
min-height: 154rpx;
|
||||
box-sizing: border-box;
|
||||
@@ -420,7 +422,6 @@ onLoad((query) => {
|
||||
}
|
||||
.media-photo-tile,
|
||||
.media-add-tile {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
min-height: 44px;
|
||||
overflow: hidden;
|
||||
@@ -429,6 +430,9 @@ onLoad((query) => {
|
||||
border: 2rpx solid rgba(179, 133, 63, 0.62);
|
||||
background: rgba(247, 241, 231, 0.88);
|
||||
}
|
||||
.media-photo-tile {
|
||||
position: relative;
|
||||
}
|
||||
.media-photo-tile--active {
|
||||
outline: 4rpx solid rgba(159, 44, 35, 0.78);
|
||||
outline-offset: -4rpx;
|
||||
@@ -570,7 +574,6 @@ onLoad((query) => {
|
||||
.media-batch-field textarea,
|
||||
.media-photo-field textarea {
|
||||
width: 100%;
|
||||
height: 92rpx;
|
||||
min-height: 92rpx;
|
||||
margin-top: 12rpx;
|
||||
box-sizing: border-box;
|
||||
@@ -601,7 +604,6 @@ onLoad((query) => {
|
||||
}
|
||||
.media-permission-card,
|
||||
.media-success-card {
|
||||
position: relative;
|
||||
min-height: 410rpx;
|
||||
margin-top: 24rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -46,56 +46,47 @@ const returnToFamily = () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.video-status-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
background: $paper;
|
||||
}
|
||||
.video-status-header,
|
||||
.video-status-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.video-status-content {
|
||||
padding: 16rpx 26rpx 56rpx;
|
||||
}
|
||||
.video-status-lead {
|
||||
position: relative;
|
||||
height: 56rpx;
|
||||
display: grid;
|
||||
min-height: 56rpx;
|
||||
margin: 0 12rpx 18rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.video-status-lead image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.video-status-lead text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
.video-status-card {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-height: 330rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.video-status-card__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.video-status-card__body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
padding: 70rpx 60rpx 48rpx;
|
||||
}
|
||||
.video-status-card__eyebrow,
|
||||
|
||||
@@ -21,11 +21,6 @@
|
||||
</view>
|
||||
|
||||
<view v-else-if="hasError" class="state-panel state-panel--error">
|
||||
<image
|
||||
class="error-panel__frame"
|
||||
src="/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="error-panel__content">
|
||||
<image
|
||||
class="error-panel__seal"
|
||||
@@ -44,11 +39,6 @@
|
||||
hover-class="action-hover"
|
||||
@click="retryLoad"
|
||||
>
|
||||
<image
|
||||
class="state-retry__skin"
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="state-retry__copy">重新加载</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -57,18 +47,8 @@
|
||||
<template v-else-if="hasGenealogies">
|
||||
<view class="genealogy-fixed-zone">
|
||||
<view class="current-slip" @click="openSwitcher">
|
||||
<image
|
||||
class="current-frame"
|
||||
src="/static/assets/modules/genealogy/transparent/current-slip-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="current-summary">
|
||||
<view class="current-seal">
|
||||
<image
|
||||
class="current-seal-frame"
|
||||
src="/static/assets/modules/genealogy/transparent/current-seal-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text class="current-seal-title">家谱</text>
|
||||
</view>
|
||||
<text class="current-name">{{ currentGenealogy.name }}</text>
|
||||
@@ -165,11 +145,6 @@
|
||||
hover-class="action-hover"
|
||||
@click="openApplication(item)"
|
||||
>
|
||||
<image
|
||||
class="application-record__skin"
|
||||
src="/static/assets/modules/genealogy/transparent/list-slip-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="application-record__main">
|
||||
<view class="application-record__title-row">
|
||||
<text class="application-record__name">{{
|
||||
@@ -217,18 +192,8 @@
|
||||
|
||||
<view v-else class="genealogy-empty-state">
|
||||
<view class="empty-panel">
|
||||
<image
|
||||
class="empty-panel__frame"
|
||||
src="/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<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>
|
||||
@@ -245,11 +210,6 @@
|
||||
hover-class="action-hover"
|
||||
@click="applyToJoin"
|
||||
>
|
||||
<image
|
||||
class="empty-search-action__skin"
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text
|
||||
class="empty-search-action__copy empty-search-action__copy--primary"
|
||||
>搜索家谱</text
|
||||
@@ -260,11 +220,6 @@
|
||||
hover-class="action-hover"
|
||||
@click="joinByInvite"
|
||||
>
|
||||
<image
|
||||
class="empty-invite-action__skin"
|
||||
src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="empty-invite-action__copy">邀请码加入</text>
|
||||
</view>
|
||||
<view
|
||||
@@ -393,6 +348,7 @@ import GenealogyCard from "@/components/GenealogyCard.vue";
|
||||
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { genealogies, notifications } from "@/data/mock.js";
|
||||
import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
|
||||
const isLoading = ref(false);
|
||||
const hasError = ref(false);
|
||||
@@ -400,7 +356,12 @@ const list = ref(genealogies);
|
||||
const forceEmptyState = ref(false);
|
||||
const addDialogVisible = ref(false);
|
||||
const switcherVisible = ref(false);
|
||||
const selectedGenealogyId = ref(genealogies[0]?.id || null);
|
||||
const storedGenealogyId = Number(genealogyContext.getCurrentGenealogyId());
|
||||
const selectedGenealogyId = ref(
|
||||
genealogies.some((item) => item.id === storedGenealogyId)
|
||||
? storedGenealogyId
|
||||
: genealogies[0]?.id || null,
|
||||
);
|
||||
const listScrollCommand = ref(0);
|
||||
const currentListScrollTop = ref(0);
|
||||
|
||||
@@ -422,7 +383,14 @@ const syncEmptyStateFromRoute = () => {
|
||||
hasError.value = presentationState === "error";
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
onLoad((query) => {
|
||||
const requestedId = Number(query?.genealogyId);
|
||||
if (genealogies.some((item) => item.id === requestedId)) {
|
||||
selectedGenealogyId.value = requestedId;
|
||||
genealogyContext.setCurrentGenealogyId(requestedId);
|
||||
} else if (selectedGenealogyId.value) {
|
||||
genealogyContext.setCurrentGenealogyId(selectedGenealogyId.value);
|
||||
}
|
||||
syncEmptyStateFromRoute();
|
||||
});
|
||||
|
||||
@@ -440,10 +408,10 @@ const isListLayout = computed(
|
||||
() => !isLoading.value && !hasError.value && hasGenealogies.value,
|
||||
);
|
||||
const createdGenealogies = computed(() =>
|
||||
list.value.filter((item, index) => index === 0),
|
||||
list.value.filter((item) => item.membership === "created"),
|
||||
);
|
||||
const joinedGenealogies = computed(() =>
|
||||
list.value.filter((item, index) => index > 0),
|
||||
list.value.filter((item) => item.membership === "joined"),
|
||||
);
|
||||
const availableGenealogies = computed(() => list.value);
|
||||
const currentGenealogy = computed(
|
||||
@@ -501,10 +469,12 @@ const shortcuts = [
|
||||
},
|
||||
];
|
||||
|
||||
const openGenealogy = (genealogy) =>
|
||||
const openGenealogy = (genealogy) => {
|
||||
genealogyContext.setCurrentGenealogyId(genealogy.id);
|
||||
uni.navigateTo({
|
||||
url: `/pages/genealogy/g05-genealogy-overview?genealogyId=${genealogy.id}`,
|
||||
});
|
||||
};
|
||||
const createGenealogy = () =>
|
||||
uni.navigateTo({ url: "/pages/genealogy/g03-create-genealogy" });
|
||||
const applyToJoin = () =>
|
||||
@@ -550,6 +520,7 @@ const resetListScroll = async () => {
|
||||
};
|
||||
const selectGenealogy = async (genealogy) => {
|
||||
selectedGenealogyId.value = genealogy.id;
|
||||
genealogyContext.setCurrentGenealogyId(genealogy.id);
|
||||
closeSwitcher();
|
||||
await resetListScroll();
|
||||
};
|
||||
@@ -565,11 +536,14 @@ const retryLoad = () => {
|
||||
};
|
||||
|
||||
const openShortcut = (key) => {
|
||||
if (!currentGenealogy.value) return;
|
||||
const genealogyId = currentGenealogy.value.id;
|
||||
genealogyContext.setCurrentGenealogyId(genealogyId);
|
||||
const paths = {
|
||||
tree: "/pages/tree/t01-tree-overview",
|
||||
tree: `/pages/tree/t01-tree-overview?genealogyId=${currentGenealogy.value.id}`,
|
||||
members: `/pages/genealogy/g05-genealogy-overview?genealogyId=${currentGenealogy.value.id}`,
|
||||
poem: "/pages/genealogy/g12-generation-poems",
|
||||
applications: "/pages/genealogy/g10-application-review",
|
||||
poem: `/pages/genealogy/g12-generation-poems?genealogyId=${currentGenealogy.value.id}`,
|
||||
applications: `/pages/genealogy/g10-application-review?genealogyId=${currentGenealogy.value.id}`,
|
||||
};
|
||||
uni.navigateTo({ url: paths[key] });
|
||||
};
|
||||
@@ -577,15 +551,15 @@ const openShortcut = (key) => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.genealogy-index {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #f9f6ef;
|
||||
}
|
||||
|
||||
.genealogy-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
padding: 24rpx 32rpx 176rpx;
|
||||
}
|
||||
|
||||
@@ -618,29 +592,14 @@ const openShortcut = (key) => {
|
||||
}
|
||||
|
||||
.current-slip {
|
||||
position: relative;
|
||||
display: block;
|
||||
min-height: 268rpx;
|
||||
padding: 30rpx 34rpx 28rpx;
|
||||
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;
|
||||
background: url("/static/assets/modules/genealogy/transparent/current-slip-frame.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
|
||||
.current-seal {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 80rpx;
|
||||
height: 128rpx;
|
||||
@@ -648,22 +607,10 @@ const openShortcut = (key) => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png") center / 100% 100% no-repeat;
|
||||
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;
|
||||
@@ -672,20 +619,16 @@ const openShortcut = (key) => {
|
||||
writing-mode: vertical-rl;
|
||||
}
|
||||
.current-summary {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
}
|
||||
.current-name {
|
||||
overflow: hidden;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 50rpx;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.current-switch-copy {
|
||||
flex: 0 0 auto;
|
||||
@@ -694,8 +637,6 @@ const openShortcut = (key) => {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.current-info-divider {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 1rpx;
|
||||
margin: 14rpx 0 16rpx;
|
||||
@@ -703,8 +644,6 @@ const openShortcut = (key) => {
|
||||
}
|
||||
|
||||
.current-meta {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
@@ -719,10 +658,10 @@ const openShortcut = (key) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.current-meta-item-text {
|
||||
white-space: nowrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
.current-meta-icon {
|
||||
@@ -752,7 +691,6 @@ const openShortcut = (key) => {
|
||||
height: 82rpx;
|
||||
}
|
||||
.shortcut-label {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
margin-top: 12rpx;
|
||||
color: $ink;
|
||||
@@ -760,8 +698,7 @@ const openShortcut = (key) => {
|
||||
font-size: 29rpx;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
@@ -803,25 +740,17 @@ const openShortcut = (key) => {
|
||||
}
|
||||
|
||||
.application-record {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 138rpx;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 22rpx 26rpx;
|
||||
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.application-record + .application-record {
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
.application-record__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.application-record__main {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
@@ -831,13 +760,11 @@ const openShortcut = (key) => {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.application-record__name {
|
||||
overflow: hidden;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.application-record__status {
|
||||
flex: 0 0 auto;
|
||||
@@ -861,8 +788,6 @@ const openShortcut = (key) => {
|
||||
line-height: 1.4;
|
||||
}
|
||||
.application-record__chevron {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-left: 14rpx;
|
||||
@@ -907,20 +832,18 @@ const openShortcut = (key) => {
|
||||
}
|
||||
.create-action > text {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
.create-cloud--right {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.state-panel {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 540rpx;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
padding: 42rpx;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
@@ -941,25 +864,15 @@ const openShortcut = (key) => {
|
||||
}
|
||||
|
||||
.state-panel--error {
|
||||
height: 1120rpx;
|
||||
min-height: 1120rpx;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.error-panel__frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
|
||||
.error-panel__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 1120rpx;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -1000,7 +913,7 @@ const openShortcut = (key) => {
|
||||
|
||||
.state-panel--error .state-retry {
|
||||
width: 560rpx;
|
||||
height: 124rpx;
|
||||
min-height: 124rpx;
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
@@ -1009,23 +922,13 @@ const openShortcut = (key) => {
|
||||
}
|
||||
|
||||
.empty-panel {
|
||||
position: relative;
|
||||
height: 1120rpx;
|
||||
}
|
||||
|
||||
.empty-panel__frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
min-height: 1120rpx;
|
||||
background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
|
||||
.empty-panel__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 1120rpx;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -1035,24 +938,15 @@ const openShortcut = (key) => {
|
||||
}
|
||||
|
||||
.empty-seal {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 120rpx;
|
||||
height: 184rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.empty-seal__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
|
||||
.empty-seal__text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #fff7e7;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 32rpx;
|
||||
@@ -1089,35 +983,25 @@ const openShortcut = (key) => {
|
||||
|
||||
.empty-search-action,
|
||||
.empty-invite-action {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 560rpx;
|
||||
height: 124rpx;
|
||||
min-height: 124rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.empty-search-action {
|
||||
margin-top: 26rpx;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / contain no-repeat;
|
||||
}
|
||||
|
||||
.empty-invite-action {
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
.empty-search-action__skin,
|
||||
.empty-invite-action__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png") center / contain no-repeat;
|
||||
}
|
||||
|
||||
.empty-search-action__copy,
|
||||
.empty-invite-action__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #7b4e24;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 34rpx;
|
||||
@@ -1147,24 +1031,15 @@ const openShortcut = (key) => {
|
||||
}
|
||||
|
||||
.state-retry {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 360rpx;
|
||||
height: 96rpx;
|
||||
min-height: 96rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 30rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.state-retry__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / contain no-repeat;
|
||||
}
|
||||
.state-retry__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #fff9ec;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 29rpx;
|
||||
@@ -1181,7 +1056,6 @@ const openShortcut = (key) => {
|
||||
background: rgba(34, 20, 12, 0.68);
|
||||
}
|
||||
.add-dialog {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 780rpx;
|
||||
max-height: calc(100vh - 80rpx);
|
||||
@@ -1193,8 +1067,6 @@ const openShortcut = (key) => {
|
||||
border-image-repeat: stretch;
|
||||
}
|
||||
.add-dialog__content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
min-height: 780rpx;
|
||||
max-height: calc(100vh - 80rpx);
|
||||
@@ -1208,24 +1080,26 @@ const openShortcut = (key) => {
|
||||
margin: auto 0;
|
||||
}
|
||||
.add-dialog__heading {
|
||||
position: relative;
|
||||
padding-right: 96rpx;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 96rpx;
|
||||
}
|
||||
.add-dialog .dialog-title,
|
||||
.add-dialog .dialog-copy {
|
||||
display: block;
|
||||
grid-column: 1;
|
||||
text-align: left;
|
||||
}
|
||||
.add-dialog__close {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 0;
|
||||
right: -22rpx;
|
||||
display: flex;
|
||||
grid-column: 2;
|
||||
grid-row: 1 / span 2;
|
||||
align-self: start;
|
||||
justify-self: end;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: -22rpx;
|
||||
}
|
||||
.add-dialog__close-icon {
|
||||
width: 80rpx;
|
||||
@@ -1258,7 +1132,6 @@ const openShortcut = (key) => {
|
||||
background: rgba(34, 20, 12, 0.58);
|
||||
}
|
||||
.genealogy-switcher {
|
||||
position: relative;
|
||||
width: 670rpx;
|
||||
max-width: 100%;
|
||||
min-height: 600rpx;
|
||||
@@ -1271,32 +1144,35 @@ const openShortcut = (key) => {
|
||||
border-image-repeat: stretch;
|
||||
}
|
||||
.genealogy-switcher__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
display: grid;
|
||||
min-height: 600rpx;
|
||||
max-height: calc(100vh - 120rpx);
|
||||
flex-direction: column;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 120rpx 58rpx 140rpx;
|
||||
}
|
||||
.genealogy-switcher__content > .dialog-title {
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
.genealogy-switcher__close {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 78rpx;
|
||||
right: 34rpx;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
align-self: start;
|
||||
justify-self: end;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: -42rpx;
|
||||
margin-right: -24rpx;
|
||||
}
|
||||
.genealogy-switcher__close-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
}
|
||||
.genealogy-switcher__list {
|
||||
grid-area: 2 / 1;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
max-height: calc(100vh - 456rpx);
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
<GenealogyPageBackground />
|
||||
|
||||
<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
|
||||
@@ -26,12 +21,6 @@
|
||||
|
||||
<view class="flow-content">
|
||||
<view class="create-flow-panel">
|
||||
<image
|
||||
class="create-flow-panel__skin"
|
||||
src="/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
|
||||
<view v-if="!isAncestorStep" class="create-flow-panel__content">
|
||||
<view class="flow-step-label"><text>第一步 · 立谱信息</text></view>
|
||||
<text class="flow-heading">为家族立一部可传承的谱</text>
|
||||
@@ -125,11 +114,6 @@
|
||||
hover-class="action-hover"
|
||||
@click="submitCreate"
|
||||
>
|
||||
<image
|
||||
class="flow-primary-action__skin"
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="flow-primary-action__copy">{{
|
||||
createState === "submitting" ? "正在创建…" : "创建并录入首代"
|
||||
}}</text>
|
||||
@@ -161,13 +145,30 @@
|
||||
<text>世代</text>
|
||||
<text class="fixed-value">第一世</text>
|
||||
</view>
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="sexOptions"
|
||||
range-key="label"
|
||||
@change="changeAncestorSex"
|
||||
>
|
||||
<view class="field-row">
|
||||
<text>性别</text>
|
||||
<text class="fixed-value">{{ sexLabel }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<view class="field-row">
|
||||
<text>出生日期</text>
|
||||
<input
|
||||
v-model="ancestorForm.birthDate"
|
||||
placeholder="如:1940年"
|
||||
placeholder-class="placeholder"
|
||||
/>
|
||||
<picker
|
||||
mode="date"
|
||||
:value="ancestorForm.birthDate"
|
||||
start="1800-01-01"
|
||||
:end="new Date().toISOString().slice(0, 10)"
|
||||
@change="changeAncestorBirthDate"
|
||||
>
|
||||
<text class="fixed-value">{{
|
||||
ancestorForm.birthDate || "请选择日期"
|
||||
}}</text>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="ancestorState === 'error'" class="flow-error"
|
||||
@@ -178,6 +179,7 @@
|
||||
<text>生平简述</text>
|
||||
<textarea
|
||||
v-model="ancestorForm.introduction"
|
||||
auto-height
|
||||
maxlength="200"
|
||||
placeholder="可选,记录家训、迁徙或重要经历"
|
||||
placeholder-class="placeholder"
|
||||
@@ -189,11 +191,6 @@
|
||||
hover-class="action-hover"
|
||||
@click="submitAncestor"
|
||||
>
|
||||
<image
|
||||
class="flow-primary-action__skin"
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="flow-primary-action__copy">{{
|
||||
ancestorState === "submitting" ? "正在保存…" : "保存并进入家谱"
|
||||
}}</text>
|
||||
@@ -208,11 +205,6 @@
|
||||
@click="closeDuplicateReminder"
|
||||
>
|
||||
<view class="duplicate-reminder" @click.stop>
|
||||
<image
|
||||
class="duplicate-reminder__skin"
|
||||
src="/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="duplicate-reminder__content">
|
||||
<text class="duplicate-reminder__title">先确认是否已有家谱</text>
|
||||
<text class="duplicate-reminder__copy"
|
||||
@@ -222,17 +214,9 @@
|
||||
class="duplicate-reminder__search"
|
||||
@click="searchExistingGenealogy"
|
||||
>
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>先搜索已有家谱</text>
|
||||
</view>
|
||||
<view class="duplicate-reminder__confirm" @click="confirmCreate">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>确认没有,继续创建</text>
|
||||
</view>
|
||||
<view
|
||||
@@ -246,21 +230,12 @@
|
||||
|
||||
<view v-if="ancestorState === 'success'" class="flow-success-layer">
|
||||
<view class="flow-success-dialog">
|
||||
<image
|
||||
class="flow-success-dialog__skin"
|
||||
src="/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="flow-success-dialog__content">
|
||||
<text class="flow-success-dialog__title">家谱创建完成</text>
|
||||
<text class="flow-success-dialog__copy"
|
||||
>首代人物已保存,接下来进入家谱总览继续完善资料。</text
|
||||
>
|
||||
<view class="flow-success-dialog__action" @click="enterOverview">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>进入家谱总览</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -302,6 +277,22 @@ const ancestorForm = reactive({
|
||||
birthDate: "",
|
||||
introduction: "",
|
||||
});
|
||||
const sexOptions = [
|
||||
{ value: "0", label: "男" },
|
||||
{ value: "1", label: "女" },
|
||||
{ value: "2", label: "暂不填写" },
|
||||
];
|
||||
const sexLabel = computed(
|
||||
() =>
|
||||
sexOptions.find((option) => option.value === ancestorForm.sex)?.label ||
|
||||
"请选择",
|
||||
);
|
||||
const changeAncestorSex = (event) => {
|
||||
ancestorForm.sex = sexOptions[Number(event.detail.value)]?.value || "2";
|
||||
};
|
||||
const changeAncestorBirthDate = (event) => {
|
||||
ancestorForm.birthDate = event.detail.value;
|
||||
};
|
||||
|
||||
const isAncestorStep = computed(() => currentStep.value === "ancestor");
|
||||
|
||||
@@ -426,29 +417,21 @@ const enterOverview = () =>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.flow-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
background: #f9f6ef;
|
||||
}
|
||||
|
||||
.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;
|
||||
background: url("/static/assets/foundation/opaque/root-header-cinnabar.jpg")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
|
||||
.flow-header__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
@@ -482,26 +465,17 @@ const enterOverview = () =>
|
||||
}
|
||||
|
||||
.flow-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 24rpx 32rpx 48rpx;
|
||||
}
|
||||
|
||||
.create-flow-panel {
|
||||
position: relative;
|
||||
min-height: 1000rpx;
|
||||
}
|
||||
|
||||
.create-flow-panel__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
|
||||
.create-flow-panel__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
min-height: 1000rpx;
|
||||
@@ -641,7 +615,7 @@ const enterOverview = () =>
|
||||
|
||||
.intro-field textarea {
|
||||
width: 100%;
|
||||
height: 142rpx;
|
||||
min-height: 142rpx;
|
||||
margin-top: 8rpx;
|
||||
color: #392719;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
@@ -650,25 +624,17 @@ const enterOverview = () =>
|
||||
}
|
||||
|
||||
.flow-primary-action {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
min-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%;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
|
||||
.flow-primary-action__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #fff9ec;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
@@ -694,21 +660,14 @@ const enterOverview = () =>
|
||||
}
|
||||
.duplicate-reminder,
|
||||
.flow-success-dialog {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 670rpx;
|
||||
min-height: 650rpx;
|
||||
}
|
||||
.duplicate-reminder__skin,
|
||||
.flow-success-dialog__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.duplicate-reminder__content,
|
||||
.flow-success-dialog__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
min-height: 650rpx;
|
||||
@@ -735,32 +694,25 @@ const enterOverview = () =>
|
||||
.duplicate-reminder__search,
|
||||
.duplicate-reminder__confirm,
|
||||
.flow-success-dialog__action {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 92rpx;
|
||||
min-height: 92rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.duplicate-reminder__search {
|
||||
margin-top: 38rpx;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.duplicate-reminder__confirm {
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
.duplicate-reminder__search image,
|
||||
.duplicate-reminder__confirm image,
|
||||
.flow-success-dialog__action image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.duplicate-reminder__search text,
|
||||
.duplicate-reminder__confirm text,
|
||||
.flow-success-dialog__action text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #fff9ec;
|
||||
font-size: 25rpx;
|
||||
@@ -779,5 +731,7 @@ const enterOverview = () =>
|
||||
}
|
||||
.flow-success-dialog__action {
|
||||
margin-top: 44rpx;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,12 +20,6 @@
|
||||
class="overview-surface"
|
||||
:class="{ 'overview-surface--state': overviewState !== 'ready' }"
|
||||
>
|
||||
<image
|
||||
class="overview-surface__skin"
|
||||
src="/static/assets/modules/genealogy/opaque/g05-overview-surface.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
|
||||
<template
|
||||
v-if="overviewState === 'ready' && genealogy && viewMode === 'member'"
|
||||
>
|
||||
@@ -44,6 +38,10 @@
|
||||
<text>已激活 {{ genealogy.activeCount || 0 }} 人</text>
|
||||
<text>{{ genealogy.visibility || "仅成员可见" }}</text>
|
||||
</view>
|
||||
<view class="overview-hero__stats">
|
||||
<text>始祖 {{ genealogy.ancestorName }}</text>
|
||||
<text>更新于 {{ genealogy.updatedAt }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
@@ -62,10 +60,6 @@
|
||||
<text class="overview-action__title">录入族人</text
|
||||
><text>从首代开始完善</text>
|
||||
</view>
|
||||
<view v-else class="overview-action-lock">
|
||||
<text class="overview-action__title">录入族人</text
|
||||
><text>仅管理员可用</text>
|
||||
</view>
|
||||
<view
|
||||
class="overview-action overview-action--poem"
|
||||
@click="toGenerationPoems"
|
||||
@@ -81,10 +75,6 @@
|
||||
<text class="overview-action__title">入谱审核</text
|
||||
><text>处理加入申请</text>
|
||||
</view>
|
||||
<view v-else class="overview-action-lock">
|
||||
<text class="overview-action__title">入谱审核</text
|
||||
><text>仅管理员可用</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="overview-family" @click="toFamily">
|
||||
@@ -130,10 +120,6 @@
|
||||
<text>{{ genealogy.publicDescription }}</text>
|
||||
</view>
|
||||
<view class="overview-public__action" @click="applyToJoin">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>申请加入这部家谱</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -142,11 +128,6 @@
|
||||
v-else-if="overviewState === 'loading'"
|
||||
class="overview-state overview-state--loading"
|
||||
>
|
||||
<image
|
||||
class="overview-state__frame"
|
||||
src="/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<AppLoading
|
||||
text="正在展开家谱"
|
||||
description="请稍候,正在读取家谱概览。"
|
||||
@@ -162,11 +143,6 @@
|
||||
'overview-state--no-permission': overviewState === 'no-permission',
|
||||
}"
|
||||
>
|
||||
<image
|
||||
class="overview-state__frame"
|
||||
src="/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="overview-state__content">
|
||||
<image
|
||||
class="overview-state__seal"
|
||||
@@ -181,10 +157,6 @@
|
||||
overviewState === 'error' ? reloadOverview() : toGenealogies()
|
||||
"
|
||||
>
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>{{
|
||||
overviewState === "error" ? "重新查看" : "返回我的家谱"
|
||||
}}</text>
|
||||
@@ -223,6 +195,28 @@ const overviewFixture = {
|
||||
memberCount: 428,
|
||||
activeCount: 316,
|
||||
visibility: "仅成员可见",
|
||||
ancestorName: "汤文远",
|
||||
updatedAt: "2026-07-12",
|
||||
};
|
||||
const overviewFixtures = {
|
||||
1001: {
|
||||
...overviewFixture,
|
||||
id: "1001",
|
||||
name: "汤氏家谱",
|
||||
location: "河南·洛阳",
|
||||
memberCount: 158,
|
||||
activeCount: 108,
|
||||
},
|
||||
1002: {
|
||||
...overviewFixture,
|
||||
id: "1002",
|
||||
name: "汤氏宗谱",
|
||||
hall: "承志堂",
|
||||
location: "山东·济宁",
|
||||
memberCount: 286,
|
||||
activeCount: 215,
|
||||
},
|
||||
2001: overviewFixture,
|
||||
};
|
||||
|
||||
const stateTitle = computed(
|
||||
@@ -268,7 +262,13 @@ const loadGenealogy = (query = {}) => {
|
||||
}
|
||||
if (query.state === "loading") return;
|
||||
|
||||
genealogy.value = { ...overviewFixture, id: genealogyId.value };
|
||||
genealogy.value = {
|
||||
...(overviewFixtures[genealogyId.value] || overviewFixture),
|
||||
id: genealogyId.value,
|
||||
};
|
||||
if (query.genealogyName) {
|
||||
genealogy.value.name = decodeURIComponent(query.genealogyName);
|
||||
}
|
||||
overviewState.value = "ready";
|
||||
};
|
||||
|
||||
@@ -305,43 +305,40 @@ const applyToJoin = () =>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.overview-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.overview-page__header {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
.overview-surface {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: min(483px, calc(100vw * 1.3422));
|
||||
}
|
||||
.overview-surface__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: min(483px, calc(100vw * 1.3422));
|
||||
flex: 0 0 auto;
|
||||
background: url("/static/assets/modules/genealogy/opaque/g05-overview-surface.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.overview-surface--state {
|
||||
display: flex;
|
||||
height: calc(100vh - 112rpx);
|
||||
min-height: 1040rpx;
|
||||
}
|
||||
.overview-surface--state .overview-surface__skin {
|
||||
display: none;
|
||||
padding: 28rpx;
|
||||
box-sizing: border-box;
|
||||
background: none;
|
||||
}
|
||||
.overview-ready {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
min-height: min(483px, calc(100vw * 1.3422));
|
||||
flex-direction: column;
|
||||
gap: 34rpx;
|
||||
padding: 58rpx 0 42rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.overview-hero {
|
||||
position: absolute;
|
||||
top: 6.5%;
|
||||
right: 8%;
|
||||
left: 8%;
|
||||
margin: 0 8%;
|
||||
color: #fff8ec;
|
||||
}
|
||||
.overview-hero__kicker {
|
||||
@@ -372,19 +369,15 @@ const applyToJoin = () =>
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.overview-actions {
|
||||
position: absolute;
|
||||
top: 29.2%;
|
||||
right: 7.2%;
|
||||
left: 7.2%;
|
||||
display: grid;
|
||||
min-height: 386rpx;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
gap: 3.4% 3%;
|
||||
height: 36.2%;
|
||||
margin: 0 7.2%;
|
||||
}
|
||||
.overview-actions--member {
|
||||
grid-template-rows: 1fr 1fr;
|
||||
height: 36.2%;
|
||||
min-height: 386rpx;
|
||||
}
|
||||
.overview-action,
|
||||
.overview-action-lock {
|
||||
@@ -392,7 +385,7 @@ const applyToJoin = () =>
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
padding: 0 22rpx;
|
||||
padding: 24rpx 22rpx;
|
||||
box-sizing: border-box;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
@@ -408,14 +401,12 @@ const applyToJoin = () =>
|
||||
font-weight: 700;
|
||||
}
|
||||
.overview-family {
|
||||
position: absolute;
|
||||
top: 70.2%;
|
||||
right: 7.5%;
|
||||
left: 7.5%;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
align-self: start;
|
||||
gap: 16rpx;
|
||||
margin: 0 7.5%;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
@@ -429,17 +420,14 @@ const applyToJoin = () =>
|
||||
color: $brand-red;
|
||||
}
|
||||
.overview-note {
|
||||
position: absolute;
|
||||
top: 80.5%;
|
||||
right: 8.5%;
|
||||
left: 8.5%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 15%;
|
||||
margin: 0 8.5%;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.6;
|
||||
margin-top: 100rpx;
|
||||
}
|
||||
.overview-note__title {
|
||||
margin-bottom: 8rpx;
|
||||
@@ -449,25 +437,16 @@ const applyToJoin = () =>
|
||||
font-weight: 700;
|
||||
}
|
||||
.overview-state {
|
||||
position: absolute;
|
||||
top: 28rpx;
|
||||
right: 28rpx;
|
||||
bottom: 28rpx;
|
||||
left: 28rpx;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
text-align: center;
|
||||
}
|
||||
.overview-state__frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.overview-state__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
@@ -501,37 +480,31 @@ const applyToJoin = () =>
|
||||
line-height: 1.7;
|
||||
}
|
||||
.overview-state-panel__action {
|
||||
position: relative;
|
||||
width: 560rpx;
|
||||
height: 124rpx;
|
||||
margin-top: 34rpx;
|
||||
}
|
||||
.overview-state-panel__action image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.overview-state-panel__action text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 560rpx;
|
||||
min-height: 124rpx;
|
||||
margin-top: 34rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.overview-state-panel__action text {
|
||||
z-index: 1;
|
||||
color: #fff9ed;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.overview-public {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
min-height: min(483px, calc(100vw * 1.3422));
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
padding: 58rpx 0 28rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.overview-public__hero {
|
||||
position: absolute;
|
||||
top: 6.5%;
|
||||
right: 8%;
|
||||
left: 8%;
|
||||
margin: 0 8%;
|
||||
color: #fff8ec;
|
||||
}
|
||||
.overview-public__eyebrow {
|
||||
@@ -555,24 +528,19 @@ const applyToJoin = () =>
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.overview-public__details {
|
||||
position: absolute;
|
||||
top: 30%;
|
||||
right: 9%;
|
||||
left: 9%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 18rpx 28rpx;
|
||||
min-height: 240rpx;
|
||||
margin: 126rpx 9% 0;
|
||||
}
|
||||
.overview-public__details > view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.overview-public__details > view:nth-child(5) {
|
||||
position: absolute;
|
||||
top: 250rpx;
|
||||
right: 0;
|
||||
left: 0;
|
||||
grid-column: 1 / -1;
|
||||
align-self: start;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -596,11 +564,8 @@ const applyToJoin = () =>
|
||||
font-size: 27rpx;
|
||||
}
|
||||
.overview-public__notice {
|
||||
position: absolute;
|
||||
top: 78%;
|
||||
right: 9%;
|
||||
left: 9%;
|
||||
display: flex;
|
||||
margin: 54rpx 9% 0;
|
||||
color: $ink-muted;
|
||||
font-size: 19rpx;
|
||||
line-height: 26rpx;
|
||||
@@ -612,24 +577,15 @@ const applyToJoin = () =>
|
||||
font-weight: 700;
|
||||
}
|
||||
.overview-public__action {
|
||||
position: absolute;
|
||||
right: 9%;
|
||||
bottom: 2%;
|
||||
left: 9%;
|
||||
display: flex;
|
||||
height: 82rpx;
|
||||
min-height: 82rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.overview-public__action image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 204rpx 9% 0;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.overview-public__action text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #fff9ed;
|
||||
font-size: 27rpx;
|
||||
@@ -637,9 +593,6 @@ const applyToJoin = () =>
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.overview-hero {
|
||||
top: 7%;
|
||||
}
|
||||
.overview-action {
|
||||
padding: 0 28rpx;
|
||||
}
|
||||
|
||||
@@ -6,11 +6,6 @@
|
||||
|
||||
<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"
|
||||
@@ -37,11 +32,6 @@
|
||||
<template v-if="mode === 'search'">
|
||||
<view class="search-controls">
|
||||
<view class="search-field">
|
||||
<image
|
||||
class="search-field__skin"
|
||||
src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<input
|
||||
v-model.trim="keyword"
|
||||
class="search-input"
|
||||
@@ -54,11 +44,6 @@
|
||||
>
|
||||
</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>
|
||||
@@ -110,11 +95,6 @@
|
||||
class="result-card genealogy-card"
|
||||
@click="openPreview(item)"
|
||||
>
|
||||
<image
|
||||
class="result-card__skin"
|
||||
src="/static/assets/modules/genealogy/transparent/list-slip-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="result-card__content">
|
||||
<view class="result-card__head">
|
||||
<view>
|
||||
@@ -169,10 +149,6 @@
|
||||
>当前仅展示失败样式,请稍后重新搜索。</text
|
||||
>
|
||||
<view class="status-retry" @click="search">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>重新搜索</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -186,11 +162,6 @@
|
||||
>
|
||||
<view class="invite-controls">
|
||||
<view class="invite-field">
|
||||
<image
|
||||
class="search-field__skin"
|
||||
src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<input
|
||||
v-model.trim="inviteCode"
|
||||
class="invite-input"
|
||||
@@ -200,11 +171,6 @@
|
||||
/>
|
||||
</view>
|
||||
<view class="search-action" @click="verifyInvite">
|
||||
<image
|
||||
class="search-action__skin"
|
||||
src="/static/assets/modules/genealogy/opaque/g06-search-button.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text class="search-action__label">验证</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -237,11 +203,6 @@
|
||||
<view v-else class="invite-result">
|
||||
<text class="invite-result__label">已定位目标家谱</text>
|
||||
<view class="result-card genealogy-card">
|
||||
<image
|
||||
class="result-card__skin"
|
||||
src="/static/assets/modules/genealogy/transparent/list-slip-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="result-card__content">
|
||||
<text class="result-card__name">{{ inviteTarget.name }}</text>
|
||||
<view class="result-card__facts">
|
||||
@@ -444,7 +405,7 @@ const clearSearch = () => {
|
||||
const openPreview = (item) => {
|
||||
if (item.relation === "available") {
|
||||
uni.navigateTo({
|
||||
url: `/pages/genealogy/g05-genealogy-overview?mode=public&genealogyId=${item.id}`,
|
||||
url: `/pages/genealogy/g05-genealogy-overview?mode=public&genealogyId=${item.id}&genealogyName=${encodeURIComponent(item.name)}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -452,7 +413,7 @@ const openPreview = (item) => {
|
||||
const handleResultAction = (item) => {
|
||||
if (item.relation === "available")
|
||||
return uni.navigateTo({
|
||||
url: `/pages/genealogy/g08-join-application?source=search&genealogyId=${item.id}`,
|
||||
url: `/pages/genealogy/g08-join-application?source=search&genealogyId=${item.id}&genealogyName=${encodeURIComponent(item.name)}`,
|
||||
});
|
||||
if (item.relation === "pending")
|
||||
return uni.navigateTo({
|
||||
@@ -460,7 +421,7 @@ const handleResultAction = (item) => {
|
||||
});
|
||||
if (item.relation === "rejected" || item.relation === "removed")
|
||||
return uni.navigateTo({
|
||||
url: `/pages/genealogy/g08-join-application?source=search&previous=${item.relation}&genealogyId=${item.id}`,
|
||||
url: `/pages/genealogy/g08-join-application?source=search&previous=${item.relation}&genealogyId=${item.id}&genealogyName=${encodeURIComponent(item.name)}`,
|
||||
});
|
||||
return uni.reLaunch({
|
||||
url: `/pages/genealogy/g01-my-genealogies?genealogyId=${item.id}`,
|
||||
@@ -473,51 +434,42 @@ const verifyInvite = () => {
|
||||
};
|
||||
const confirmInvite = () =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/genealogy/g08-join-application?source=invite&genealogyId=${inviteTarget.value.id}`,
|
||||
url: `/pages/genealogy/g08-join-application?source=invite&genealogyId=${inviteTarget.value.id}&genealogyName=${encodeURIComponent(inviteTarget.value.name)}`,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.search-page__header {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.search-page__content {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
min-height: calc(100vh - 104rpx);
|
||||
padding: 30rpx 24rpx calc(58rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.search-title-strip {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 138rpx;
|
||||
}
|
||||
.search-title-strip__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
grid-template-columns: 156rpx 1fr 170rpx;
|
||||
align-items: center;
|
||||
background: url("/static/assets/modules/genealogy/opaque/g06-search-title-strip.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.search-title-strip__seal {
|
||||
position: absolute;
|
||||
top: 27rpx;
|
||||
left: 52rpx;
|
||||
z-index: 1;
|
||||
width: 72rpx;
|
||||
height: 84rpx;
|
||||
margin-left: 52rpx;
|
||||
}
|
||||
.search-title-strip__text {
|
||||
position: absolute;
|
||||
inset: 0 170rpx 0 156rpx;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -535,7 +487,6 @@ const confirmInvite = () =>
|
||||
border-bottom: 1rpx solid rgba(181, 138, 75, 0.45);
|
||||
}
|
||||
.mode-tab {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
@@ -546,18 +497,12 @@ const confirmInvite = () =>
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.mode-tab--active {
|
||||
margin-bottom: -2rpx;
|
||||
background: linear-gradient($brand-red, $brand-red) center bottom /
|
||||
calc(100% - 84rpx) 4rpx no-repeat;
|
||||
color: $brand-red;
|
||||
font-weight: 700;
|
||||
}
|
||||
.mode-tab--active::after {
|
||||
position: absolute;
|
||||
right: 42rpx;
|
||||
bottom: -2rpx;
|
||||
left: 42rpx;
|
||||
height: 4rpx;
|
||||
background: $brand-red;
|
||||
content: "";
|
||||
}
|
||||
.search-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -571,21 +516,15 @@ const confirmInvite = () =>
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
.search-field {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 430rpx;
|
||||
height: 95rpx;
|
||||
}
|
||||
.search-field__skin,
|
||||
.search-action__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
min-height: 95rpx;
|
||||
background: url("/static/assets/modules/genealogy/opaque/g06-search-input-wide.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.search-input {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 82rpx 0 30rpx;
|
||||
@@ -597,26 +536,26 @@ const confirmInvite = () =>
|
||||
color: #ab9c83;
|
||||
}
|
||||
.search-clear {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
right: 20rpx;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
justify-self: end;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
margin-right: 20rpx;
|
||||
color: $brand-red;
|
||||
font-size: 21rpx;
|
||||
}
|
||||
.search-action {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 200rpx;
|
||||
height: 88rpx;
|
||||
min-height: 88rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: url("/static/assets/modules/genealogy/opaque/g06-search-button.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.search-action__label {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #fff4dc;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
@@ -690,23 +629,16 @@ const confirmInvite = () =>
|
||||
filter: grayscale(1);
|
||||
}
|
||||
.status-retry {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 300rpx;
|
||||
height: 76rpx;
|
||||
min-height: 76rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.status-retry image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.status-retry text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #fff9ed;
|
||||
font-size: 25rpx;
|
||||
@@ -733,20 +665,13 @@ const confirmInvite = () =>
|
||||
font-weight: 700;
|
||||
}
|
||||
.result-card {
|
||||
position: relative;
|
||||
min-height: 314rpx;
|
||||
box-sizing: border-box;
|
||||
padding: 28rpx 30rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.result-card__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.result-card__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.result-card__head {
|
||||
@@ -823,13 +748,15 @@ const confirmInvite = () =>
|
||||
line-height: 35rpx;
|
||||
}
|
||||
.invite-field {
|
||||
position: relative;
|
||||
display: grid;
|
||||
flex: 1;
|
||||
height: 95rpx;
|
||||
min-height: 95rpx;
|
||||
background: url("/static/assets/modules/genealogy/opaque/g06-search-input-wide.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.invite-input {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 30rpx;
|
||||
@@ -866,11 +793,12 @@ const confirmInvite = () =>
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
.search-title-strip__seal {
|
||||
left: 44rpx;
|
||||
margin-left: 44rpx;
|
||||
}
|
||||
.search-title-strip {
|
||||
grid-template-columns: 138rpx 1fr 148rpx;
|
||||
}
|
||||
.search-title-strip__text {
|
||||
right: 148rpx;
|
||||
left: 138rpx;
|
||||
font-size: 37rpx;
|
||||
}
|
||||
.search-field {
|
||||
|
||||
@@ -14,24 +14,17 @@
|
||||
'join-state--error': joinState === 'error',
|
||||
}"
|
||||
>
|
||||
<image
|
||||
class="join-panel__skin"
|
||||
src="/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
|
||||
<view v-if="joinState === 'form'" class="join-form">
|
||||
<text class="join-form__eyebrow">{{ sourceContract.eyebrow }}</text>
|
||||
<text class="join-form__title"
|
||||
>{{ sourceContract.formTitle }} {{ genealogyName }}</text
|
||||
>
|
||||
<text class="join-form__copy">{{ sourceContract.formCopy }}</text>
|
||||
<text v-if="previousNotice" class="join-form__previous">{{
|
||||
previousNotice
|
||||
}}</text>
|
||||
|
||||
<view class="join-field">
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/g-form-field-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text>真实姓名</text
|
||||
><input
|
||||
v-model="form.realName"
|
||||
@@ -44,10 +37,6 @@
|
||||
fieldErrors.realName
|
||||
}}</text>
|
||||
<view class="join-field">
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/g-form-field-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text>与家谱关系</text
|
||||
><input
|
||||
v-model="form.relation"
|
||||
@@ -63,13 +52,10 @@
|
||||
>请以家谱中一位已知长辈为参照,例如:汤正华堂侄</text
|
||||
>
|
||||
<view class="join-field join-field--message">
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/g-form-field-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text>{{ sourceContract.thirdFieldLabel }}</text
|
||||
><textarea
|
||||
v-model="form.message"
|
||||
auto-height
|
||||
maxlength="80"
|
||||
:placeholder="sourceContract.thirdFieldPlaceholder"
|
||||
placeholder-class="join-placeholder"
|
||||
@@ -78,10 +64,6 @@
|
||||
|
||||
<text class="join-form__note">{{ sourceContract.note }}</text>
|
||||
<view class="join-action" @click="submitJoin">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>{{
|
||||
isSubmitting
|
||||
? sourceContract.submittingLabel
|
||||
@@ -106,10 +88,6 @@
|
||||
class="join-action"
|
||||
@click="joinState === 'success' ? completeFlow() : retryForm()"
|
||||
>
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>{{
|
||||
joinState === "success" ? sourceContract.nextLabel : "重新填写"
|
||||
}}</text>
|
||||
@@ -127,6 +105,7 @@ import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const source = ref("search");
|
||||
const previousRelation = ref("");
|
||||
const genealogyName = ref("这部家谱");
|
||||
const genealogyPreview = {
|
||||
1001: "汤氏家谱",
|
||||
@@ -137,6 +116,15 @@ const isSubmitting = ref(false);
|
||||
const errorMessage = ref("");
|
||||
const form = reactive({ realName: "", relation: "", message: "" });
|
||||
const fieldErrors = reactive({ realName: "", relation: "" });
|
||||
const previousNotice = computed(() =>
|
||||
previousRelation.value === "rejected"
|
||||
? "上次申请未通过,请补充更准确的长辈姓名、祖居地或支系信息。"
|
||||
: previousRelation.value === "removed"
|
||||
? "你曾退出或被移出这部家谱,请重新确认身份关系后申请。"
|
||||
: previousRelation.value === "withdrawn"
|
||||
? "上次申请已撤回;如仍希望加入,请重新确认关系并提交。"
|
||||
: "",
|
||||
);
|
||||
const sourceContract = computed(() =>
|
||||
source.value === "invite"
|
||||
? {
|
||||
@@ -185,6 +173,9 @@ const resultCopy = computed(() =>
|
||||
onLoad((query) => {
|
||||
genealogyId.value = query.genealogyId || "";
|
||||
source.value = query.source === "invite" ? "invite" : "search";
|
||||
previousRelation.value = ["rejected", "removed", "withdrawn"].includes(query.previous)
|
||||
? query.previous
|
||||
: "";
|
||||
if (query.state === "success") {
|
||||
joinState.value = "success";
|
||||
return;
|
||||
@@ -194,7 +185,9 @@ onLoad((query) => {
|
||||
joinState.value = "error";
|
||||
return;
|
||||
}
|
||||
genealogyName.value = genealogyPreview[genealogyId.value] || "这部家谱";
|
||||
genealogyName.value = query.genealogyName
|
||||
? decodeURIComponent(query.genealogyName)
|
||||
: genealogyPreview[genealogyId.value] || "这部家谱";
|
||||
});
|
||||
|
||||
const submitJoin = () => {
|
||||
@@ -232,31 +225,26 @@ const completeFlow = () =>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.join-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.join-page__header {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
.join-panel {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: calc(100% - 32rpx);
|
||||
height: min(590px, calc((100vw - 16px) * 1.337));
|
||||
min-height: min(590px, calc((100vw - 16px) * 1.337));
|
||||
margin: 22rpx auto 0;
|
||||
}
|
||||
.join-panel__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 70rpx 8%;
|
||||
box-sizing: border-box;
|
||||
background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.join-form {
|
||||
position: absolute;
|
||||
inset: 8.5% 8%;
|
||||
min-width: 0;
|
||||
}
|
||||
.join-form__eyebrow,
|
||||
.join-result__eyebrow {
|
||||
@@ -283,34 +271,37 @@ const completeFlow = () =>
|
||||
line-height: 1.6;
|
||||
}
|
||||
.join-field {
|
||||
position: relative;
|
||||
height: 92rpx;
|
||||
display: grid;
|
||||
min-height: 92rpx;
|
||||
margin-top: 18rpx;
|
||||
background: url("/static/assets/modules/genealogy/transparent/g-form-field-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.join-field > image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.join-form__previous {
|
||||
display: block;
|
||||
margin-top: 14rpx;
|
||||
color: $brand-red;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.join-field > text {
|
||||
position: absolute;
|
||||
top: 31rpx;
|
||||
left: 24rpx;
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
align-self: center;
|
||||
margin-left: 24rpx;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.join-field input,
|
||||
.join-field textarea {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 20rpx;
|
||||
bottom: 0;
|
||||
left: 170rpx;
|
||||
z-index: 1;
|
||||
height: 92rpx;
|
||||
grid-area: 1 / 1;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
min-height: 92rpx;
|
||||
margin-right: 20rpx;
|
||||
margin-left: 170rpx;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
line-height: 92rpx;
|
||||
@@ -346,36 +337,29 @@ const completeFlow = () =>
|
||||
text-align: center;
|
||||
}
|
||||
.join-action {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 82rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.join-action image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.join-action text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 82rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
margin-top: 20rpx;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.join-action text {
|
||||
z-index: 1;
|
||||
color: #fff9ed;
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
.join-result {
|
||||
position: absolute;
|
||||
top: 28%;
|
||||
right: 12%;
|
||||
left: 12%;
|
||||
text-align: center;
|
||||
}
|
||||
.join-state--success,
|
||||
.join-state--error {
|
||||
padding: 170rpx 12% 70rpx;
|
||||
}
|
||||
.join-result__eyebrow {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -391,9 +375,5 @@ const completeFlow = () =>
|
||||
.join-panel {
|
||||
width: calc(100% - 48rpx);
|
||||
}
|
||||
.join-form {
|
||||
right: 9%;
|
||||
left: 9%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,11 +23,6 @@
|
||||
:key="item.id"
|
||||
class="application-card"
|
||||
>
|
||||
<image
|
||||
class="application-card__skin"
|
||||
src="/static/assets/modules/genealogy/transparent/list-slip-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="application-card__body">
|
||||
<text class="application-card__name">{{ item.genealogyName }}</text>
|
||||
<text class="application-card__time">{{ item.appliedAt }}</text>
|
||||
@@ -56,10 +51,6 @@
|
||||
description="请稍候,正在同步审核状态。"
|
||||
/>
|
||||
<view v-else class="application-state-card">
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/list-slip-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="application-state-card__copy">
|
||||
<text>{{ stateTitle }}</text>
|
||||
<text>{{ stateCopy }}</text>
|
||||
@@ -72,10 +63,6 @@
|
||||
applicationState === 'error' ? loadApplications({}) : toSearch()
|
||||
"
|
||||
>
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>{{
|
||||
applicationState === "error" ? "重新查看" : "查找公开家谱"
|
||||
}}</text>
|
||||
@@ -156,7 +143,7 @@ const statusHint = (status) =>
|
||||
})[status] || "";
|
||||
|
||||
const actionFor = (item) =>
|
||||
({ PENDING: "撤回申请", APPROVED: "进入家谱", REJECTED: "修改后重新提交" })[
|
||||
({ PENDING: "撤回申请", APPROVED: "进入家谱", REJECTED: "修改后重新提交", WITHDRAWN: "重新申请" })[
|
||||
item.status
|
||||
] || "";
|
||||
const stateTitle = computed(() =>
|
||||
@@ -190,7 +177,14 @@ const loadApplications = (query = {}) => {
|
||||
return;
|
||||
}
|
||||
applications.value = applicationSamples.map((item) => ({ ...item }));
|
||||
applicationState.value = "list";
|
||||
if (query.status) {
|
||||
const requestedStatus = String(query.status).toUpperCase();
|
||||
applications.value = applications.value.filter(
|
||||
(item) => item.status === requestedStatus,
|
||||
);
|
||||
if (!applications.value.length) applicationState.value = "empty";
|
||||
}
|
||||
if (applicationState.value !== "empty") applicationState.value = "list";
|
||||
};
|
||||
|
||||
onLoad(loadApplications);
|
||||
@@ -201,7 +195,11 @@ const handleApplicationAction = (item) => {
|
||||
});
|
||||
if (item.status === "REJECTED")
|
||||
return uni.navigateTo({
|
||||
url: `/pages/genealogy/g08-join-application?source=search&previous=rejected&genealogyId=${item.genealogyId}`,
|
||||
url: `/pages/genealogy/g08-join-application?source=search&previous=rejected&genealogyId=${item.genealogyId}&genealogyName=${encodeURIComponent(item.genealogyName)}`,
|
||||
});
|
||||
if (item.status === "WITHDRAWN")
|
||||
return uni.navigateTo({
|
||||
url: `/pages/genealogy/g08-join-application?source=search&previous=withdrawn&genealogyId=${item.genealogyId}&genealogyName=${encodeURIComponent(item.genealogyName)}`,
|
||||
});
|
||||
if (item.status === "PENDING") withdrawTarget.value = item;
|
||||
};
|
||||
@@ -221,17 +219,15 @@ const toSearch = () =>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.application-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.application-page__header {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
.application-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 28rpx 24rpx 60rpx;
|
||||
}
|
||||
@@ -252,47 +248,57 @@ const toSearch = () =>
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.application-card {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: calc((100vw - 24px) * 0.34286);
|
||||
min-height: 228rpx;
|
||||
max-height: 282rpx;
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
.application-card__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.application-card__body {
|
||||
position: absolute;
|
||||
inset: 18% 7% 14% 8.5%;
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
min-height: 228rpx;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 12rpx 18rpx;
|
||||
padding: 42rpx 52rpx 34rpx 64rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.application-card__name {
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
align-self: start;
|
||||
justify-self: start;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.application-card__time {
|
||||
position: absolute;
|
||||
top: 3rpx;
|
||||
right: 0;
|
||||
grid-row: 1;
|
||||
grid-column: 2;
|
||||
align-self: start;
|
||||
justify-self: end;
|
||||
margin-top: 3rpx;
|
||||
color: #998873;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.application-card__relation {
|
||||
display: block;
|
||||
max-width: 70%;
|
||||
margin-top: 14rpx;
|
||||
grid-row: 2;
|
||||
grid-column: 1 / -1;
|
||||
align-self: start;
|
||||
justify-self: start;
|
||||
margin-top: 0;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.application-card__status {
|
||||
position: absolute;
|
||||
right: 3%;
|
||||
bottom: 38%;
|
||||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
align-self: center;
|
||||
justify-self: end;
|
||||
margin-right: 3%;
|
||||
color: $brand-red;
|
||||
font-size: 25rpx;
|
||||
font-weight: 700;
|
||||
@@ -304,41 +310,42 @@ const toSearch = () =>
|
||||
color: #7e6f62;
|
||||
}
|
||||
.application-card__hint {
|
||||
position: absolute;
|
||||
bottom: 5%;
|
||||
left: 0;
|
||||
max-width: 62%;
|
||||
grid-row: 3;
|
||||
grid-column: 1;
|
||||
align-self: center;
|
||||
justify-self: start;
|
||||
margin-bottom: 4rpx;
|
||||
color: #766653;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.application-card__action {
|
||||
position: absolute;
|
||||
right: 2%;
|
||||
bottom: 2%;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
grid-row: 3;
|
||||
grid-column: 2;
|
||||
align-self: center;
|
||||
justify-self: end;
|
||||
min-height: 54rpx;
|
||||
align-items: center;
|
||||
margin-right: 2%;
|
||||
margin-bottom: 0;
|
||||
color: $brand-red;
|
||||
font-size: 23rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.application-state-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: calc((100vw - 24px) * 0.34286);
|
||||
min-height: 228rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 80rpx;
|
||||
}
|
||||
.application-state-card > image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 48rpx 12%;
|
||||
box-sizing: border-box;
|
||||
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.application-state-card__copy {
|
||||
position: absolute;
|
||||
inset: 22% 12%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@@ -357,24 +364,17 @@ const toSearch = () =>
|
||||
line-height: 1.6;
|
||||
}
|
||||
.application-page__action {
|
||||
position: relative;
|
||||
width: 420rpx;
|
||||
height: 82rpx;
|
||||
margin: 34rpx auto 0;
|
||||
}
|
||||
.application-page__action image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.application-page__action text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 420rpx;
|
||||
min-height: 82rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
margin: 34rpx auto 0;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.application-page__action text {
|
||||
z-index: 1;
|
||||
color: #fff9ed;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -18,10 +18,6 @@
|
||||
<view v-if="reviewState !== 'loading'" class="review-intro">
|
||||
<text>核实亲属关系后再决定</text>
|
||||
<text>审核结果会通过消息告知申请人</text>
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/section-divider.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<template v-if="reviewState === 'list'">
|
||||
@@ -30,11 +26,6 @@
|
||||
:key="item.id"
|
||||
class="application-card"
|
||||
>
|
||||
<image
|
||||
class="application-card__skin"
|
||||
src="/static/assets/modules/genealogy/transparent/list-slip-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="application-card__body">
|
||||
<text class="application-card__name">{{ item.name }}</text>
|
||||
<text class="application-card__phone">{{ item.phone }}</text>
|
||||
@@ -85,10 +76,6 @@
|
||||
description="请稍候,正在读取待审核记录。"
|
||||
/>
|
||||
<view v-else class="review-state-card">
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/list-slip-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="review-state-card__copy">
|
||||
<text>{{ stateTitle }}</text>
|
||||
<text>{{ stateCopy }}</text>
|
||||
@@ -133,13 +120,23 @@
|
||||
@confirm="helpVisible ? closeDialog() : applyAudit()"
|
||||
@cancel="closeDialog"
|
||||
@close="closeDialog"
|
||||
/>
|
||||
>
|
||||
<view v-if="confirmation && !confirmation.approved" class="rejection-field">
|
||||
<text>拒绝原因</text>
|
||||
<textarea
|
||||
v-model="rejectionReason"
|
||||
auto-height
|
||||
maxlength="120"
|
||||
placeholder="请说明需要补充或核实的信息"
|
||||
@input="rejectionError = ''"
|
||||
/>
|
||||
<text v-if="rejectionError" class="rejection-field__error">{{
|
||||
rejectionError
|
||||
}}</text>
|
||||
</view>
|
||||
</AppDialog>
|
||||
|
||||
<view v-if="feedbackVisible" class="review-feedback">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-toast-v3.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text>{{ feedbackMessage }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -177,6 +174,8 @@ const genealogyId = ref("");
|
||||
const reviewState = ref("loading");
|
||||
const errorMessage = ref("");
|
||||
const confirmation = ref(null);
|
||||
const rejectionReason = ref("");
|
||||
const rejectionError = ref("");
|
||||
const helpVisible = ref(false);
|
||||
const feedbackVisible = ref(false);
|
||||
const feedbackMessage = ref("");
|
||||
@@ -235,10 +234,14 @@ const loadApplications = (query = {}) => {
|
||||
|
||||
onLoad(loadApplications);
|
||||
const confirmAudit = (item, approved) => {
|
||||
rejectionReason.value = "";
|
||||
rejectionError.value = "";
|
||||
confirmation.value = { item, approved };
|
||||
};
|
||||
const closeDialog = () => {
|
||||
confirmation.value = null;
|
||||
rejectionReason.value = "";
|
||||
rejectionError.value = "";
|
||||
helpVisible.value = false;
|
||||
};
|
||||
const showFeedback = (message) => {
|
||||
@@ -253,7 +256,12 @@ const showFeedback = (message) => {
|
||||
const applyAudit = () => {
|
||||
const current = confirmation.value;
|
||||
if (!current) return;
|
||||
if (!current.approved && !rejectionReason.value.trim()) {
|
||||
rejectionError.value = "请填写拒绝原因,方便申请人补充资料";
|
||||
return;
|
||||
}
|
||||
current.item.status = current.approved ? "APPROVED" : "REJECTED";
|
||||
if (!current.approved) current.item.rejectionReason = rejectionReason.value.trim();
|
||||
closeDialog();
|
||||
showFeedback(current.approved ? "已通过申请" : "已拒绝申请");
|
||||
};
|
||||
@@ -264,24 +272,26 @@ const showHelp = () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.review-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.review-page__header {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
z-index: 1;
|
||||
}
|
||||
.rejection-field { width: 100%; margin: 20rpx 0; text-align: left; }
|
||||
.rejection-field > text:first-child { display: block; color: $ink; font-size: 23rpx; font-weight: 700; }
|
||||
.rejection-field textarea { width: 100%; min-height: 110rpx; margin-top: 10rpx; padding: 18rpx; box-sizing: border-box; color: $ink; font-size: 23rpx; line-height: 1.55; background: url("/static/assets/modules/genealogy/transparent/g-form-field-frame.png") center / 100% 100% no-repeat; }
|
||||
.rejection-field__error { display: block; margin-top: 8rpx; color: $brand-red; font-size: 21rpx; }
|
||||
.review-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
padding: 24rpx 24rpx 60rpx;
|
||||
}
|
||||
.review-intro {
|
||||
position: relative;
|
||||
margin: 0 8rpx 24rpx;
|
||||
padding-bottom: 22rpx;
|
||||
background: url("/static/assets/modules/genealogy/transparent/section-divider.png") bottom center / 100% 14rpx no-repeat;
|
||||
}
|
||||
.review-intro text {
|
||||
display: block;
|
||||
@@ -297,80 +307,72 @@ const showHelp = () => {
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.review-intro image {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 14rpx;
|
||||
}
|
||||
.application-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc((100vw - 24px) * 0.34286);
|
||||
min-height: 228rpx;
|
||||
max-height: 282rpx;
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
.application-card__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.application-card__body {
|
||||
position: absolute;
|
||||
inset: 18% 7% 12% 8.5%;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-height: 228rpx;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
grid-template-rows: auto auto auto;
|
||||
gap: 12rpx 0;
|
||||
padding: 44rpx 52rpx 29rpx 64rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.application-card__name {
|
||||
grid-column: 1;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.application-card__phone {
|
||||
grid-column: 2;
|
||||
margin-left: 14rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.application-card__time {
|
||||
position: absolute;
|
||||
top: 3rpx;
|
||||
right: 0;
|
||||
grid-column: 3;
|
||||
justify-self: end;
|
||||
margin-top: 3rpx;
|
||||
color: #998873;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.application-card__relation {
|
||||
display: block;
|
||||
max-width: 52%;
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 2;
|
||||
margin-top: 15rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.review-actions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
grid-column: 2 / 4;
|
||||
grid-row: 3;
|
||||
justify-self: end;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.review-action {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 148rpx;
|
||||
height: 68rpx;
|
||||
min-height: 68rpx;
|
||||
}
|
||||
.review-action image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.review-action text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
@@ -384,22 +386,22 @@ const showHelp = () => {
|
||||
color: #fff9ed;
|
||||
}
|
||||
.review-result {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: grid;
|
||||
grid-column: 2 / 4;
|
||||
grid-row: 3;
|
||||
justify-self: end;
|
||||
width: 308rpx;
|
||||
height: 68rpx;
|
||||
min-height: 68rpx;
|
||||
}
|
||||
.review-result image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.application-card__status {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
@@ -414,24 +416,18 @@ const showHelp = () => {
|
||||
color: $ink;
|
||||
}
|
||||
.review-state-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc((100vw - 24px) * 0.34286);
|
||||
min-height: 228rpx;
|
||||
margin-top: 70rpx;
|
||||
}
|
||||
.review-state-card > image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.review-state-card__copy {
|
||||
position: absolute;
|
||||
inset: 22% 12%;
|
||||
display: flex;
|
||||
min-height: 228rpx;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 48rpx 12%;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
.review-state-card__copy text:first-child {
|
||||
@@ -447,20 +443,12 @@ const showHelp = () => {
|
||||
line-height: 1.6;
|
||||
}
|
||||
.review-page__retry {
|
||||
position: relative;
|
||||
width: 420rpx;
|
||||
height: 82rpx;
|
||||
min-height: 82rpx;
|
||||
margin: 32rpx auto 0;
|
||||
}
|
||||
.review-page__retry image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / contain no-repeat;
|
||||
}
|
||||
.review-page__retry text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -480,17 +468,10 @@ const showHelp = () => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 36rpx;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-toast-v3.png") center / 100% 100% no-repeat;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.review-feedback image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.review-feedback text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
|
||||
@@ -13,12 +13,6 @@
|
||||
'settings-state--no-permission': settingsState === 'no-permission',
|
||||
}"
|
||||
>
|
||||
<image
|
||||
class="settings-panel__skin"
|
||||
src="/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
|
||||
<AppLoading
|
||||
v-if="settingsState === 'loading'"
|
||||
text="正在读取家谱设置"
|
||||
@@ -32,10 +26,6 @@
|
||||
>
|
||||
|
||||
<view class="settings-field">
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/g-form-field-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text>家谱名称</text>
|
||||
<input
|
||||
v-model="genealogyDraft.name"
|
||||
@@ -79,13 +69,10 @@
|
||||
</view>
|
||||
|
||||
<view class="settings-field settings-field--note">
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/g-form-field-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text>访问说明</text>
|
||||
<textarea
|
||||
v-model="genealogyDraft.accessNote"
|
||||
auto-height
|
||||
maxlength="80"
|
||||
placeholder="向访问者说明家谱用途"
|
||||
placeholder-class="settings-placeholder"
|
||||
@@ -93,10 +80,6 @@
|
||||
</view>
|
||||
|
||||
<view class="settings-action" @click="saveSettings">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>保存设置</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -124,10 +107,6 @@
|
||||
: "请从家谱总览重新进入,当前修改不会被保留。"
|
||||
}}</text>
|
||||
<view class="settings-action" @click="settingsState = 'form'">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>{{
|
||||
settingsState === "success" ? "继续调整" : "重新查看"
|
||||
}}</text>
|
||||
@@ -135,10 +114,6 @@
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="feedbackVisible" class="settings-feedback">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>设置已保存</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -156,11 +131,27 @@ const settingsState = ref("loading");
|
||||
const nameError = ref("");
|
||||
const feedbackVisible = ref(false);
|
||||
let feedbackTimer = null;
|
||||
const settingsFixtures = {
|
||||
1001: {
|
||||
name: "汤氏家谱",
|
||||
visibility: "MEMBER_ONLY",
|
||||
accessNote: "家族资料,请妥善保存",
|
||||
},
|
||||
1002: {
|
||||
name: "汤氏宗谱",
|
||||
visibility: "PUBLIC_APPLY",
|
||||
accessNote: "公开家谱身份,成员资料需审核后查看",
|
||||
},
|
||||
};
|
||||
const genealogyDraft = reactive({
|
||||
name: "汤氏家谱",
|
||||
visibility: "MEMBER_ONLY",
|
||||
accessNote: "家族资料,请妥善保存",
|
||||
});
|
||||
const originalDraft = ref("");
|
||||
const isDirty = computed(
|
||||
() => JSON.stringify(genealogyDraft) !== originalDraft.value,
|
||||
);
|
||||
const visibilityOptions = [
|
||||
{ value: "MEMBER_ONLY", label: "仅成员可见" },
|
||||
{ value: "PUBLIC_APPLY", label: "公开可申请" },
|
||||
@@ -178,6 +169,11 @@ const visibilityHint = computed(() =>
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = query.genealogyId || "";
|
||||
Object.assign(
|
||||
genealogyDraft,
|
||||
settingsFixtures[genealogyId.value] || settingsFixtures[1001],
|
||||
);
|
||||
originalDraft.value = JSON.stringify(genealogyDraft);
|
||||
settingsState.value =
|
||||
query.state === "loading"
|
||||
? "loading"
|
||||
@@ -198,6 +194,11 @@ const saveSettings = () => {
|
||||
nameError.value = "请填写家谱名称";
|
||||
return;
|
||||
}
|
||||
if (genealogyDraft.name.trim().length > 30) {
|
||||
nameError.value = "家谱名称不能超过 30 个字";
|
||||
return;
|
||||
}
|
||||
originalDraft.value = JSON.stringify(genealogyDraft);
|
||||
settingsState.value = "success";
|
||||
feedbackVisible.value = true;
|
||||
feedbackTimer = setTimeout(() => {
|
||||
@@ -209,31 +210,29 @@ const saveSettings = () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.settings-page__header {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
.settings-panel {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: calc(100% - 32rpx);
|
||||
height: min(620px, calc((100vw - 16px) * 1.43));
|
||||
min-height: min(620px, calc((100vw - 16px) * 1.43));
|
||||
margin: 18rpx auto 0;
|
||||
padding: 70rpx 8%;
|
||||
box-sizing: border-box;
|
||||
background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.settings-panel__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.settings-panel > .app-loading {
|
||||
grid-area: 1 / 1 / -1 / -1;
|
||||
}
|
||||
.settings-form {
|
||||
position: absolute;
|
||||
inset: 7.5% 8%;
|
||||
min-width: 0;
|
||||
}
|
||||
.settings-form__eyebrow,
|
||||
.settings-result__eyebrow {
|
||||
@@ -260,34 +259,30 @@ const saveSettings = () => {
|
||||
line-height: 1.55;
|
||||
}
|
||||
.settings-field {
|
||||
position: relative;
|
||||
height: 92rpx;
|
||||
display: grid;
|
||||
min-height: 92rpx;
|
||||
margin-top: 17rpx;
|
||||
}
|
||||
.settings-field > image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("/static/assets/modules/genealogy/transparent/g-form-field-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.settings-field > text {
|
||||
position: absolute;
|
||||
top: 31rpx;
|
||||
left: 24rpx;
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
align-self: center;
|
||||
margin-left: 24rpx;
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.settings-field input,
|
||||
.settings-field textarea {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 20rpx;
|
||||
bottom: 0;
|
||||
left: 164rpx;
|
||||
z-index: 1;
|
||||
height: 92rpx;
|
||||
grid-area: 1 / 1;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
min-height: 92rpx;
|
||||
margin-right: 20rpx;
|
||||
margin-left: 164rpx;
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
line-height: 92rpx;
|
||||
@@ -323,20 +318,19 @@ const saveSettings = () => {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
.visibility-option {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: calc(50% - 7rpx);
|
||||
height: 64rpx;
|
||||
min-height: 64rpx;
|
||||
}
|
||||
.visibility-option image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.visibility-option text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
@@ -358,36 +352,30 @@ const saveSettings = () => {
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
.settings-action {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 78rpx;
|
||||
margin-top: 19rpx;
|
||||
}
|
||||
.settings-action image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.settings-action text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 78rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
margin-top: 19rpx;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.settings-action text {
|
||||
z-index: 1;
|
||||
color: #fff9ed;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
.settings-result {
|
||||
position: absolute;
|
||||
top: 29%;
|
||||
right: 12%;
|
||||
left: 12%;
|
||||
text-align: center;
|
||||
}
|
||||
.settings-state--success,
|
||||
.settings-state--error,
|
||||
.settings-state--no-permission {
|
||||
padding: 180rpx 12% 70rpx;
|
||||
}
|
||||
.settings-result__eyebrow {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -411,15 +399,10 @@ const saveSettings = () => {
|
||||
justify-content: center;
|
||||
padding: 0 34rpx;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.settings-feedback image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.settings-feedback text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: $ink;
|
||||
font-size: 22rpx;
|
||||
@@ -428,9 +411,5 @@ const saveSettings = () => {
|
||||
.settings-panel {
|
||||
width: calc(100% - 48rpx);
|
||||
}
|
||||
.settings-form {
|
||||
right: 9%;
|
||||
left: 9%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,12 +19,6 @@
|
||||
'poem-state--no-permission': poemState === 'no-permission',
|
||||
}"
|
||||
>
|
||||
<image
|
||||
class="poem-panel__skin"
|
||||
src="/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
|
||||
<AppLoading
|
||||
v-if="poemState === 'loading'"
|
||||
text="正在整理字辈诗"
|
||||
@@ -43,10 +37,6 @@
|
||||
class="poem-row"
|
||||
:class="{ 'poem-row--current': item.current }"
|
||||
>
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/g-form-field-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text class="poem-row__number">第 {{ item.generationNo }} 世</text>
|
||||
<text class="poem-row__character">{{ item.character }}</text>
|
||||
<text class="poem-row__status">{{
|
||||
@@ -69,14 +59,11 @@
|
||||
>按照接口支持的连续文本录入,每个汉字对应一代;保存前可预览新字序。</text
|
||||
>
|
||||
<view class="poem-field">
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/g-form-field-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text>字辈内容</text>
|
||||
<textarea
|
||||
v-model="poemDraft"
|
||||
maxlength="24"
|
||||
auto-height
|
||||
maxlength="50"
|
||||
placeholder="例如:启宗敦本继世传芳"
|
||||
placeholder-class="poem-placeholder"
|
||||
@input="poemError = ''"
|
||||
@@ -158,10 +145,6 @@
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="feedbackVisible" class="poem-feedback">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>字辈预览已更新</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -181,6 +164,8 @@ const feedbackVisible = ref(false);
|
||||
let feedbackTimer = null;
|
||||
const poemDraft = ref("启宗敦本继世传芳");
|
||||
const stopMissingOldGeneration = ref(true);
|
||||
const startGeneration = ref(12);
|
||||
const currentGeneration = ref(14);
|
||||
const poemRows = ref([
|
||||
{ generationNo: 12, character: "启", current: false },
|
||||
{ generationNo: 13, character: "宗", current: false },
|
||||
@@ -193,6 +178,11 @@ const previewCharacters = computed(() =>
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = query.genealogyId || "";
|
||||
startGeneration.value = Math.max(1, Number(query.startGeneration) || 12);
|
||||
currentGeneration.value = Math.max(
|
||||
startGeneration.value,
|
||||
Number(query.currentGeneration) || 14,
|
||||
);
|
||||
poemState.value =
|
||||
query.state === "loading"
|
||||
? "loading"
|
||||
@@ -223,13 +213,20 @@ const savePoems = () => {
|
||||
poemError.value = "字辈保存失败,请稍后重试";
|
||||
return;
|
||||
}
|
||||
poemRows.value = characters
|
||||
.slice(0, 4)
|
||||
.map((character, index) => ({
|
||||
generationNo: 12 + index,
|
||||
const preservedRows = stopMissingOldGeneration.value
|
||||
? []
|
||||
: poemRows.value.filter(
|
||||
(item) => item.generationNo < startGeneration.value,
|
||||
);
|
||||
const nextRows = characters.map((character, index) => {
|
||||
const generationNo = startGeneration.value + index;
|
||||
return {
|
||||
generationNo,
|
||||
character,
|
||||
current: index === 2,
|
||||
}));
|
||||
current: generationNo === currentGeneration.value,
|
||||
};
|
||||
});
|
||||
poemRows.value = [...preservedRows, ...nextRows];
|
||||
poemState.value = "list";
|
||||
feedbackVisible.value = true;
|
||||
feedbackTimer = setTimeout(() => {
|
||||
@@ -241,33 +238,31 @@ const savePoems = () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.poem-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.poem-page__header {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
.poem-panel {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: calc(100% - 32rpx);
|
||||
height: min(650px, calc((100vw - 16px) * 1.5));
|
||||
min-height: min(650px, calc((100vw - 16px) * 1.5));
|
||||
margin: 18rpx auto 0;
|
||||
padding: 76rpx 8%;
|
||||
box-sizing: border-box;
|
||||
background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.poem-panel__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.poem-panel > .app-loading {
|
||||
grid-area: 1 / 1 / -1 / -1;
|
||||
}
|
||||
.poem-list,
|
||||
.poem-editor,
|
||||
.poem-state-card {
|
||||
position: absolute;
|
||||
inset: 7.5% 8%;
|
||||
min-width: 0;
|
||||
}
|
||||
.poem-list__eyebrow {
|
||||
display: block;
|
||||
@@ -294,39 +289,33 @@ const savePoems = () => {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.poem-row {
|
||||
position: relative;
|
||||
height: 72rpx;
|
||||
display: grid;
|
||||
min-height: 72rpx;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
.poem-row image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
grid-template-columns: 48% auto 1fr auto;
|
||||
align-items: center;
|
||||
background: url("/static/assets/modules/genealogy/transparent/g-form-field-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.poem-row__number {
|
||||
position: absolute;
|
||||
top: 24rpx;
|
||||
left: 24rpx;
|
||||
z-index: 1;
|
||||
grid-column: 1;
|
||||
margin-left: 24rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.poem-row__character {
|
||||
position: absolute;
|
||||
top: 14rpx;
|
||||
left: 48%;
|
||||
z-index: 1;
|
||||
grid-column: 2;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.poem-row__status {
|
||||
position: absolute;
|
||||
top: 25rpx;
|
||||
right: 22rpx;
|
||||
z-index: 1;
|
||||
grid-column: 4;
|
||||
margin-right: 22rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
@@ -335,21 +324,20 @@ const savePoems = () => {
|
||||
color: $brand-red;
|
||||
}
|
||||
.poem-action {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 76rpx;
|
||||
min-height: 76rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.poem-action image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.poem-action text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
@@ -359,34 +347,30 @@ const savePoems = () => {
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.poem-field {
|
||||
position: relative;
|
||||
height: 118rpx;
|
||||
display: grid;
|
||||
min-height: 118rpx;
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
.poem-field image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("/static/assets/modules/genealogy/transparent/g-form-field-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.poem-field > text {
|
||||
position: absolute;
|
||||
top: 47rpx;
|
||||
left: 24rpx;
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
align-self: center;
|
||||
margin-left: 24rpx;
|
||||
color: $ink;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.poem-field textarea {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 20rpx;
|
||||
bottom: 0;
|
||||
left: 164rpx;
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
box-sizing: border-box;
|
||||
height: 118rpx;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
min-height: 118rpx;
|
||||
margin-right: 20rpx;
|
||||
margin-left: 164rpx;
|
||||
padding-top: 34rpx;
|
||||
color: $ink;
|
||||
font-size: 22rpx;
|
||||
@@ -415,20 +399,19 @@ const savePoems = () => {
|
||||
font-weight: 700;
|
||||
}
|
||||
.poem-policy__option {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 248rpx;
|
||||
height: 62rpx;
|
||||
min-height: 62rpx;
|
||||
}
|
||||
.poem-policy__option image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.poem-policy__option text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
@@ -459,9 +442,13 @@ const savePoems = () => {
|
||||
color: $ink;
|
||||
}
|
||||
.poem-state-card {
|
||||
top: 29%;
|
||||
text-align: center;
|
||||
}
|
||||
.poem-state--empty,
|
||||
.poem-state--error,
|
||||
.poem-state--no-permission {
|
||||
padding: 180rpx 4% 70rpx;
|
||||
}
|
||||
.poem-state-card .poem-list__eyebrow {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -485,15 +472,10 @@ const savePoems = () => {
|
||||
justify-content: center;
|
||||
padding: 0 34rpx;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.poem-feedback image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.poem-feedback text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: $ink;
|
||||
font-size: 22rpx;
|
||||
@@ -502,11 +484,5 @@ const savePoems = () => {
|
||||
.poem-panel {
|
||||
width: calc(100% - 48rpx);
|
||||
}
|
||||
.poem-list,
|
||||
.poem-editor,
|
||||
.poem-state-card {
|
||||
right: 9%;
|
||||
left: 9%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<view class="notice-page__header">
|
||||
<PageHeader
|
||||
title="消息中心"
|
||||
:action="noticeState === 'list' ? '全部已读' : ''"
|
||||
:action="unreadCount > 0 && noticeState === 'list' ? '全部已读' : ''"
|
||||
@action="markAllRead"
|
||||
/>
|
||||
</view>
|
||||
@@ -33,13 +33,8 @@
|
||||
class="notice-card"
|
||||
role="button"
|
||||
:aria-label="`${item.unread ? '未读' : '已读'}消息:${item.title}`"
|
||||
@click="readNotice(item)"
|
||||
@click="openNotice(item)"
|
||||
>
|
||||
<image
|
||||
class="notice-card__skin"
|
||||
src="/static/assets/modules/notification/transparent/n01-notice-card.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="notice-card__copy">
|
||||
<text
|
||||
class="notice-card__status"
|
||||
@@ -61,11 +56,6 @@
|
||||
</template>
|
||||
|
||||
<view v-else class="notice-state-card">
|
||||
<image
|
||||
class="notice-state-card__skin"
|
||||
src="/static/assets/modules/notification/transparent/n01-notice-card.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="notice-state-card__copy">
|
||||
<text class="notice-state-card__title">{{
|
||||
noticeState === "empty" ? "暂时没有新消息" : "消息中心暂不可用"
|
||||
@@ -90,7 +80,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onUnmounted, ref } from "vue";
|
||||
import { computed, onUnmounted, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
@@ -112,6 +102,7 @@ const notices = ref([
|
||||
content: "汤志成申请加入汤氏家谱,请核实亲属关系。",
|
||||
time: "今天 10:28",
|
||||
unread: true,
|
||||
detailId: "review-1",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -119,6 +110,7 @@ const notices = ref([
|
||||
content: "你申请加入汝南汤氏家谱的请求已通过。",
|
||||
time: "昨天 18:10",
|
||||
unread: false,
|
||||
detailId: "approved",
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -135,8 +127,14 @@ onLoad((query) => {
|
||||
: "list";
|
||||
});
|
||||
|
||||
const readNotice = (item) => {
|
||||
const unreadCount = computed(
|
||||
() => notices.value.filter((item) => item.unread).length,
|
||||
);
|
||||
const openNotice = (item) => {
|
||||
item.unread = false;
|
||||
uni.navigateTo({
|
||||
url: `/pages/notification/n02-message-detail?id=${item.detailId}&genealogyId=${genealogyId.value}`,
|
||||
});
|
||||
};
|
||||
const restoreList = () => {
|
||||
noticeState.value = "list";
|
||||
@@ -168,17 +166,17 @@ onUnmounted(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.notice-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.notice-page__header,
|
||||
.notice-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
}
|
||||
.notice-content {
|
||||
flex: 1;
|
||||
padding: 24rpx 28rpx 100rpx;
|
||||
}
|
||||
.notice-list {
|
||||
@@ -187,19 +185,11 @@ onUnmounted(() => {
|
||||
gap: 18rpx;
|
||||
}
|
||||
.notice-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 220rpx;
|
||||
}
|
||||
.notice-card__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("/static/assets/modules/notification/transparent/n01-notice-card.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.notice-card__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
min-height: 220rpx;
|
||||
box-sizing: border-box;
|
||||
@@ -238,24 +228,12 @@ onUnmounted(() => {
|
||||
margin: 30rpx auto 0;
|
||||
}
|
||||
.notice-state-card {
|
||||
position: relative;
|
||||
margin-top: 38rpx;
|
||||
background: url("/static/assets/modules/notification/transparent/n01-notice-card.png") top center / 100% 220rpx no-repeat;
|
||||
text-align: center;
|
||||
}
|
||||
.notice-state-card__skin {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 220rpx;
|
||||
min-height: 118px;
|
||||
}
|
||||
.notice-state-card__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
height: 220rpx;
|
||||
min-height: 118px;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,5 +1,111 @@
|
||||
<!-- 页面编号:N-02;用途:消息详情、已读与空状态。 -->
|
||||
<template><ModulePage page-id="n02" /></template>
|
||||
<!-- 页面编号:N-02;用途:消息详情、已读状态与上下文操作。 -->
|
||||
<template>
|
||||
<view class="notice-detail-page" :class="{ 'notice-state--ready': noticeState === 'ready', 'notice-state--loading': noticeState === 'loading', 'notice-state--expired': noticeState === 'expired' }">
|
||||
<ModulePageBackground module="notification" />
|
||||
<view class="page-layer"><PageHeader title="消息详情" /></view>
|
||||
|
||||
<view class="notice-content page-layer">
|
||||
<AppLoading v-if="noticeState === 'loading'" text="正在读取消息" description="请稍候,正在整理消息详情。" />
|
||||
|
||||
<view v-else-if="noticeState === 'expired'" class="paper-panel state-card">
|
||||
<text class="state-title">这条消息已失效</text>
|
||||
<text class="state-copy">消息可能已撤回或超过保留期限,请返回消息中心查看其他内容。</text>
|
||||
<AppButton block label="返回消息中心" @click="backToMessages" />
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<view class="paper-panel notice-card">
|
||||
<view class="notice-meta">
|
||||
<text :class="{ 'is-unread': !noticeDetail.read }">{{ noticeDetail.read ? "已读" : "未读提醒" }}</text>
|
||||
<text>{{ noticeDetail.time }}</text>
|
||||
</view>
|
||||
<text class="notice-title">{{ noticeDetail.title }}</text>
|
||||
<text class="notice-body">{{ noticeDetail.body }}</text>
|
||||
<text class="notice-source">来自:{{ noticeDetail.source }}</text>
|
||||
</view>
|
||||
<view class="action-stack">
|
||||
<AppButton v-if="!noticeDetail.read" block label="标记已读" @click="markAsRead" />
|
||||
<AppButton v-if="noticeDetail.target" block type="secondary" :label="noticeDetail.targetLabel" @click="openNoticeTarget" />
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
<AppToast :visible="toastVisible" :message="toastMessage" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { onUnmounted, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const noticeState = ref("loading");
|
||||
const toastVisible = ref(false);
|
||||
const toastMessage = ref("");
|
||||
let toastTimer = null;
|
||||
const noticeDetail = ref({
|
||||
id: "review-1",
|
||||
title: "申请待审核",
|
||||
body: "汤志成申请加入汤氏家谱,请核实申请人的亲属关系与世代信息后完成审核。",
|
||||
time: "今天 10:28",
|
||||
source: "汝南汤氏家谱",
|
||||
read: false,
|
||||
target: "/pages/genealogy/g10-application-review",
|
||||
targetLabel: "前往入谱审核",
|
||||
});
|
||||
|
||||
onLoad((query) => {
|
||||
if (query.state === "expired") {
|
||||
noticeState.value = "expired";
|
||||
return;
|
||||
}
|
||||
if (query.id === "approved") {
|
||||
noticeDetail.value = {
|
||||
id: "approved",
|
||||
title: "入谱申请已通过",
|
||||
body: "你申请加入汝南汤氏家谱的请求已通过,现在可以查看家谱与家族动态。",
|
||||
time: "昨天 18:10",
|
||||
source: "汝南汤氏家谱",
|
||||
read: true,
|
||||
target: "/pages/genealogy/g01-my-genealogies",
|
||||
targetLabel: "查看我的家谱",
|
||||
};
|
||||
}
|
||||
noticeState.value = "ready";
|
||||
});
|
||||
|
||||
const showToast = (message) => {
|
||||
toastMessage.value = message;
|
||||
toastVisible.value = true;
|
||||
if (toastTimer) clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => (toastVisible.value = false), 1800);
|
||||
};
|
||||
const markAsRead = () => {
|
||||
noticeDetail.value.read = true;
|
||||
showToast("已标记为已读");
|
||||
};
|
||||
const openNoticeTarget = () => uni.navigateTo({ url: noticeDetail.value.target });
|
||||
const backToMessages = () => uni.navigateBack();
|
||||
onUnmounted(() => toastTimer && clearTimeout(toastTimer));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.notice-detail-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.page-layer { z-index: 1; }
|
||||
.notice-content { flex: 1; padding: 28rpx 30rpx 72rpx; }
|
||||
.paper-panel { background: url("/static/assets/modules/notification/transparent/n01-notice-card.png") center / 100% 100% no-repeat; }
|
||||
.notice-card { min-height: 430rpx; padding: 54rpx 52rpx; box-sizing: border-box; }
|
||||
.notice-meta { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8rpx 20rpx; color: $ink-muted; font-size: 22rpx; }
|
||||
.notice-meta .is-unread { color: $brand-red; font-weight: 700; }
|
||||
.notice-title { display: block; margin-top: 24rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 38rpx; font-weight: 700; overflow-wrap: anywhere; }
|
||||
.notice-body { display: block; margin-top: 22rpx; color: #5f4a38; font-size: 25rpx; line-height: 1.75; overflow-wrap: anywhere; }
|
||||
.notice-source { display: block; margin-top: 28rpx; color: $ink-muted; font-size: 22rpx; overflow-wrap: anywhere; }
|
||||
.action-stack { display: flex; flex-direction: column; gap: 18rpx; margin-top: 28rpx; }
|
||||
.state-card { min-height: 340rpx; padding: 72rpx 50rpx 48rpx; box-sizing: border-box; text-align: center; }
|
||||
.state-title { display: block; color: $ink; font-size: 34rpx; font-weight: 700; }
|
||||
.state-copy { display: block; margin: 18rpx 0 28rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.65; }
|
||||
@media (max-width: 340px) { .notice-content { padding-right: 22rpx; padding-left: 22rpx; } .notice-card { padding-right: 40rpx; padding-left: 40rpx; } }
|
||||
</style>
|
||||
|
||||
@@ -19,11 +19,6 @@
|
||||
<view class="profile-content">
|
||||
<template v-if="profileState === 'ready'">
|
||||
<view class="profile-hero">
|
||||
<image
|
||||
class="profile-hero__frame"
|
||||
src="/static/assets/modules/profile/transparent/m01-profile-summary-card.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<image
|
||||
class="profile-hero__hall"
|
||||
src="/static/assets/foundation/transparent/root-header-hall.png"
|
||||
@@ -34,19 +29,21 @@
|
||||
src="/static/assets/foundation/transparent/auth-title-cloud.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="profile-hero__label"
|
||||
><text>个</text><text>人</text><text>谱</text><text>牒</text></view
|
||||
>
|
||||
<view class="profile-identity">
|
||||
<image
|
||||
class="profile-identity__seal"
|
||||
src="/static/assets/foundation/transparent/brand-seal.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="profile-identity__copy">
|
||||
<text class="profile-identity__name">汤文清</text>
|
||||
<text class="profile-identity__role">汤氏家谱 · 家谱成员</text>
|
||||
<text class="profile-identity__phone">139****6421</text>
|
||||
<view class="profile-hero__content">
|
||||
<view class="profile-hero__label"
|
||||
><text>个</text><text>人</text><text>谱</text><text>牒</text></view
|
||||
>
|
||||
<view class="profile-identity">
|
||||
<image
|
||||
class="profile-identity__seal"
|
||||
src="/static/assets/foundation/transparent/brand-seal.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="profile-identity__copy">
|
||||
<text class="profile-identity__name">汤文清</text>
|
||||
<text class="profile-identity__role">汤氏家谱 · 家谱成员</text>
|
||||
<text class="profile-identity__phone">139****6421</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -57,11 +54,6 @@
|
||||
aria-label="查看待处理消息"
|
||||
@click="toNotifications"
|
||||
>
|
||||
<image
|
||||
class="profile-scroll-notice__skin"
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="profile-scroll-notice__copy">
|
||||
<text>待你处理</text>
|
||||
<text>2 条家谱提醒与审核通知</text>
|
||||
@@ -109,11 +101,6 @@
|
||||
</template>
|
||||
|
||||
<view v-else class="profile-error">
|
||||
<image
|
||||
class="profile-error__skin"
|
||||
src="/static/assets/modules/profile/transparent/m01-profile-summary-card.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="profile-error__copy">
|
||||
<text class="profile-error__title">个人资料暂不可用</text>
|
||||
<text class="profile-error__description"
|
||||
@@ -161,6 +148,18 @@ const menuItems = [
|
||||
icon: "/static/assets/foundation/transparent/brand-seal.png",
|
||||
url: "/pages/profile/m10-about-settings",
|
||||
},
|
||||
{
|
||||
label: "邀请家人",
|
||||
note: "邀请码与家谱推广",
|
||||
icon: "/static/assets/foundation/transparent/brand-seal.png",
|
||||
url: "/pages/profile/m08-promotion",
|
||||
},
|
||||
{
|
||||
label: "服务与订单",
|
||||
note: "权益说明与订单记录",
|
||||
icon: "/static/assets/foundation/transparent/notice.png",
|
||||
url: "/pages/profile/m09-vip-orders",
|
||||
},
|
||||
];
|
||||
|
||||
onLoad((query) => {
|
||||
@@ -179,58 +178,58 @@ const openItem = (item) => uni.navigateTo({ url: item.url });
|
||||
|
||||
<style scoped lang="scss">
|
||||
.profile-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.profile-page__header,
|
||||
.profile-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
}
|
||||
.profile-content {
|
||||
flex: 1;
|
||||
padding: 26rpx 30rpx 190rpx;
|
||||
}
|
||||
.profile-hero {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 42rpx minmax(0, 1fr);
|
||||
width: 100%;
|
||||
min-height: 252rpx;
|
||||
box-sizing: border-box;
|
||||
background: url("/static/assets/modules/profile/transparent/m01-profile-summary-card.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.profile-hero__content {
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-area: 1 / 1;
|
||||
grid-template-columns: 42rpx minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 30rpx;
|
||||
padding: 34rpx 52rpx 32rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
.profile-hero__frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.profile-hero__hall {
|
||||
position: absolute;
|
||||
right: -18rpx;
|
||||
bottom: -30rpx;
|
||||
align-self: end;
|
||||
justify-self: end;
|
||||
grid-area: 1 / 1;
|
||||
width: 330rpx;
|
||||
height: 122rpx;
|
||||
margin-right: -18rpx;
|
||||
margin-bottom: -30rpx;
|
||||
opacity: 0.12;
|
||||
pointer-events: none;
|
||||
}
|
||||
.profile-hero__cloud {
|
||||
position: absolute;
|
||||
top: 22rpx;
|
||||
right: 30rpx;
|
||||
align-self: start;
|
||||
justify-self: end;
|
||||
grid-area: 1 / 1;
|
||||
width: 96rpx;
|
||||
height: 52rpx;
|
||||
margin-top: 22rpx;
|
||||
margin-right: 30rpx;
|
||||
opacity: 0.32;
|
||||
pointer-events: none;
|
||||
}
|
||||
.profile-hero__label {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
width: 42rpx;
|
||||
@@ -249,7 +248,6 @@ const openItem = (item) => uni.navigateTo({ url: item.url });
|
||||
line-height: 1.12;
|
||||
}
|
||||
.profile-identity {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
min-height: 252rpx;
|
||||
@@ -286,21 +284,12 @@ const openItem = (item) => uni.navigateTo({ url: item.url });
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.profile-scroll-notice {
|
||||
position: relative;
|
||||
width: 88%;
|
||||
min-height: 112rpx;
|
||||
margin: 22rpx auto 0;
|
||||
}
|
||||
.profile-scroll-notice__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.profile-scroll-notice__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
min-height: 112rpx;
|
||||
box-sizing: border-box;
|
||||
@@ -389,25 +378,12 @@ const openItem = (item) => uni.navigateTo({ url: item.url });
|
||||
opacity: 0.75;
|
||||
}
|
||||
.profile-error {
|
||||
position: relative;
|
||||
margin-top: 38rpx;
|
||||
background: url("/static/assets/modules/profile/transparent/m01-profile-summary-card.png") top center / 100% 220rpx no-repeat;
|
||||
text-align: center;
|
||||
}
|
||||
.profile-error__skin {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 220rpx;
|
||||
min-height: 118px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.profile-error__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
height: 220rpx;
|
||||
min-height: 118px;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,5 +1,79 @@
|
||||
<!-- 页面编号:M-02;用途:编辑个人资料。 -->
|
||||
<template><ModulePage page-id="m02" /></template>
|
||||
<template>
|
||||
<view class="profile-edit-page" :class="{ 'profile-state--ready': profileState === 'ready', 'profile-state--saving': profileState === 'saving' }">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="个人资料" /></view>
|
||||
<view class="page-content page-layer">
|
||||
<view class="profile-avatar-card">
|
||||
<image class="avatar-seal" src="/static/assets/foundation/transparent/brand-seal.png" mode="aspectFit" />
|
||||
<view class="avatar-copy"><text>{{ profileForm.nickname || "未填写昵称" }}</text><text>头像将在相册权限接入后支持上传</text></view>
|
||||
<view class="text-action" role="button" aria-label="选择头像" @click="chooseAvatar">选择头像</view>
|
||||
</view>
|
||||
|
||||
<view class="form-panel">
|
||||
<view class="form-row"><text>昵称</text><input v-model.trim="profileForm.nickname" maxlength="20" aria-label="昵称" placeholder="请输入昵称" /></view>
|
||||
<text v-if="errors.nickname" class="field-error">{{ errors.nickname }}</text>
|
||||
<view class="form-row"><text>真实姓名</text><input v-model.trim="profileForm.realName" maxlength="20" aria-label="真实姓名" placeholder="请输入真实姓名" /></view>
|
||||
<view class="form-row"><text>常住地区</text><input v-model.trim="profileForm.region" maxlength="40" aria-label="常住地区" placeholder="省 / 市 / 区县" /></view>
|
||||
<view class="textarea-row"><text>个人简介</text><textarea v-model.trim="profileForm.bio" auto-height maxlength="300" aria-label="个人简介" placeholder="介绍你的家族身份或经历" /></view>
|
||||
<text class="counter">{{ profileForm.bio.length }}/300</text>
|
||||
<text v-if="errors.bio" class="field-error">{{ errors.bio }}</text>
|
||||
</view>
|
||||
<AppButton block :disabled="profileState === 'saving'" :label="profileState === 'saving' ? '正在保存' : '保存资料'" @click="saveProfile" />
|
||||
</view>
|
||||
<AppToast :visible="toastVisible" :message="toastMessage" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { onUnmounted, reactive, ref } from "vue";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const profileForm = reactive({ nickname: "汤文清", realName: "汤文清", region: "河南省 周口市", bio: "热心参与家谱资料整理。" });
|
||||
const errors = reactive({ nickname: "", bio: "" });
|
||||
const profileState = ref("ready");
|
||||
const toastVisible = ref(false);
|
||||
const toastMessage = ref("");
|
||||
let timer = null;
|
||||
const showToast = (message) => { toastMessage.value = message; toastVisible.value = true; clearTimeout(timer); timer = setTimeout(() => (toastVisible.value = false), 1800); };
|
||||
const chooseAvatar = () => showToast("头像选择将在相册权限接入后开放");
|
||||
const validateProfile = () => {
|
||||
errors.nickname = profileForm.nickname ? "" : "请填写昵称";
|
||||
errors.bio = profileForm.bio.length > 300 ? "个人简介不能超过 300 字" : "";
|
||||
return !errors.nickname && !errors.bio;
|
||||
};
|
||||
const saveProfile = () => {
|
||||
if (!validateProfile() || profileState.value === "saving") return;
|
||||
profileState.value = "saving";
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => { profileState.value = "ready"; showToast("个人资料已保存"); }, 500);
|
||||
};
|
||||
onUnmounted(() => clearTimeout(timer));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.profile-edit-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.page-layer { z-index: 1; }
|
||||
.page-content { flex: 1; padding: 26rpx 30rpx 70rpx; }
|
||||
.profile-avatar-card, .form-panel { background: var(--module-field-asset, url("/static/assets/modules/profile/transparent/module-field-frame.png")) center / 100% 100% no-repeat; }
|
||||
.profile-avatar-card { display: grid; grid-template-columns: 92rpx minmax(0,1fr) auto; align-items: center; gap: 20rpx; min-height: 150rpx; padding: 24rpx 34rpx; box-sizing: border-box; }
|
||||
.avatar-seal { width: 82rpx; height: auto; max-height: 92rpx; aspect-ratio: 82 / 92; }
|
||||
.avatar-copy { min-width: 0; }
|
||||
.avatar-copy text { display: block; overflow-wrap: anywhere; }
|
||||
.avatar-copy text:first-child { color: $ink; font-size: 29rpx; font-weight: 700; }
|
||||
.avatar-copy text:last-child { margin-top: 6rpx; color: $ink-muted; font-size: 20rpx; line-height: 1.45; }
|
||||
.text-action { min-height: 72rpx; display: flex; align-items: center; color: $brand-red; font-size: 22rpx; }
|
||||
.form-panel { margin-top: 22rpx; padding: 20rpx 34rpx 30rpx; box-sizing: border-box; }
|
||||
.form-row { display: grid; grid-template-columns: 150rpx minmax(0,1fr); min-height: 92rpx; align-items: center; border-bottom: 1px solid rgba(181,137,63,.42); gap: 18rpx; }
|
||||
.form-row > text, .textarea-row > text { color: $ink; font-size: 24rpx; font-weight: 700; }
|
||||
.form-row input { width: auto; min-width: 0; min-height: 70rpx; color: $ink; font-size: 24rpx; text-align: right; }
|
||||
.textarea-row { padding-top: 24rpx; }
|
||||
.textarea-row textarea { display: block; width: auto; min-width: 0; min-height: 150rpx; margin-top: 14rpx; color: $ink; font-size: 24rpx; line-height: 1.6; }
|
||||
.counter { display: block; color: $ink-muted; font-size: 20rpx; text-align: right; }
|
||||
.field-error { display: block; padding-top: 7rpx; color: #b42318; font-size: 21rpx; text-align: right; }
|
||||
.page-content > .app-button { margin-top: 26rpx; }
|
||||
@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.profile-avatar-card{grid-template-columns:72rpx minmax(0,1fr);padding-right:26rpx;padding-left:26rpx}.text-action{grid-column:2}.avatar-seal{width:68rpx;max-height:78rpx}.form-row{grid-template-columns:126rpx minmax(0,1fr)}}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,47 @@
|
||||
<!-- 页面编号:M-03;用途:账号与安全设置。 -->
|
||||
<template><ModulePage page-id="m03" /></template>
|
||||
<!-- 页面编号:M-03;用途:账号安全总览与安全功能入口。 -->
|
||||
<template>
|
||||
<view class="security-page device-state--safe">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="账号与安全" /></view>
|
||||
<view class="page-content page-layer">
|
||||
<view class="security-summary">
|
||||
<text>账号状态安全</text>
|
||||
<text>手机号已绑定,最近登录设备未发现异常。</text>
|
||||
</view>
|
||||
<view class="security-list">
|
||||
<view v-for="item in securityItems" :key="item.key" class="security-row" role="button" :aria-label="item.label" @click="openSecurityItem(item)">
|
||||
<image :src="item.icon" mode="aspectFit" />
|
||||
<view><text>{{ item.label }}</text><text>{{ item.note }}</text></view>
|
||||
<image class="chevron" src="/static/assets/foundation/transparent/chevron-right.png" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
<AppButton block type="secondary" label="检查账号安全" @click="checkSecurity" />
|
||||
</view>
|
||||
<AppToast :visible="toastVisible" :message="toastMessage" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { onUnmounted, ref } from "vue";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const securityItems = [
|
||||
{ key: "password", label: "登录密码", note: "建议定期更新密码", icon: "/static/assets/modules/auth/transparent/a01-icon-lock-v1.png", url: "/pages/profile/m04-change-password" },
|
||||
{ key: "phone", label: "绑定手机号", note: "139****6421", icon: "/static/assets/modules/auth/transparent/a01-icon-phone-v1.png", url: "/pages/profile/m05-change-phone" },
|
||||
{ key: "device", label: "登录设备", note: "当前设备 · 今天登录", icon: "/static/assets/foundation/transparent/auth-login-outline.png" },
|
||||
];
|
||||
const toastVisible = ref(false);
|
||||
const toastMessage = ref("");
|
||||
let timer = null;
|
||||
const showToast = (message) => { toastMessage.value = message; toastVisible.value = true; clearTimeout(timer); timer = setTimeout(() => (toastVisible.value = false), 1800); };
|
||||
const openSecurityItem = (item) => item.url ? uni.navigateTo({ url: item.url }) : showToast("当前设备登录正常,未发现异常记录");
|
||||
const checkSecurity = () => showToast("安全检查完成,当前账号状态正常");
|
||||
onUnmounted(() => clearTimeout(timer));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.security-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.security-summary{min-height:190rpx;padding:42rpx 48rpx;box-sizing:border-box;background:url("/static/assets/modules/profile/transparent/m01-profile-summary-card.png") center/100% 100% no-repeat;text-align:center}.security-summary text{display:block;overflow-wrap:anywhere}.security-summary text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:34rpx;font-weight:700}.security-summary text:last-child{margin-top:12rpx;color:$ink-muted;font-size:23rpx;line-height:1.55}.security-list{margin-top:22rpx}.security-row{display:grid;grid-template-columns:56rpx minmax(0,1fr) 34rpx;min-height:110rpx;align-items:center;gap:18rpx;padding:16rpx 26rpx;box-sizing:border-box;background:url("/static/assets/modules/profile/transparent/module-field-frame.png") center/100% 100% no-repeat}.security-row+ .security-row{margin-top:12rpx}.security-row>image{width:50rpx;height:50rpx}.security-row .chevron{width:30rpx;height:30rpx}.security-row view{min-width:0}.security-row text{display:block;overflow-wrap:anywhere}.security-row text:first-child{color:$ink;font-size:25rpx;font-weight:700}.security-row text:last-child{margin-top:6rpx;color:$ink-muted;font-size:21rpx}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,59 @@
|
||||
<!-- 页面编号:M-04;用途:修改密码。 -->
|
||||
<template><ModulePage page-id="m04" /></template>
|
||||
<!-- 页面编号:M-04;用途:修改登录密码。 -->
|
||||
<template>
|
||||
<view class="password-page" :class="{ 'password-state--ready': passwordState === 'ready', 'password-state--saving': passwordState === 'saving' }">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="修改密码" /></view>
|
||||
<view class="page-content page-layer">
|
||||
<view class="security-tip"><text>设置安全密码</text><text>建议使用 8–32 位字母与数字组合,不要与其他应用共用。</text></view>
|
||||
<view class="form-panel">
|
||||
<view v-for="field in passwordFields" :key="field.key" class="field-block">
|
||||
<view class="form-row">
|
||||
<text>{{ field.label }}</text>
|
||||
<input v-model="passwordForm[field.key]" :password="!passwordVisible[field.key]" maxlength="32" :aria-label="field.label" :placeholder="field.placeholder" @input="errors[field.key] = ''" />
|
||||
<view class="password-toggle" role="button" :aria-label="`${passwordVisible[field.key] ? '隐藏' : '显示'}${field.label}`" @click="togglePassword(field.key)">{{ passwordVisible[field.key] ? "隐藏" : "显示" }}</view>
|
||||
</view>
|
||||
<text v-if="errors[field.key]" class="field-error">{{ errors[field.key] }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<AppButton block :disabled="passwordState === 'saving'" :label="passwordState === 'saving' ? '正在修改' : '确认修改'" @click="savePassword" />
|
||||
</view>
|
||||
<AppToast :visible="toastVisible" :message="toastMessage" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { onUnmounted, reactive, ref } from "vue";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const passwordForm = reactive({ current: "", next: "", confirm: "" });
|
||||
const passwordVisible = reactive({ current: false, next: false, confirm: false });
|
||||
const errors = reactive({ current: "", next: "", confirm: "" });
|
||||
const passwordState = ref("ready");
|
||||
const passwordFields = [
|
||||
{ key: "current", label: "当前密码", placeholder: "请输入当前密码" },
|
||||
{ key: "next", label: "新密码", placeholder: "8–32 位字母与数字" },
|
||||
{ key: "confirm", label: "确认新密码", placeholder: "请再次输入新密码" },
|
||||
];
|
||||
const toastVisible = ref(false); const toastMessage = ref(""); let timer = null;
|
||||
const showToast = (message) => { toastMessage.value = message; toastVisible.value = true; clearTimeout(timer); timer = setTimeout(() => (toastVisible.value = false), 1800); };
|
||||
const togglePassword = (key) => { passwordVisible[key] = !passwordVisible[key]; };
|
||||
const validatePassword = () => {
|
||||
errors.current = passwordForm.current ? "" : "请输入当前密码";
|
||||
errors.next = /^(?=.*[A-Za-z])(?=.*\d).{8,32}$/.test(passwordForm.next) ? "" : "新密码需为 8–32 位,并同时包含字母和数字";
|
||||
errors.confirm = !passwordForm.confirm ? "请再次输入新密码" : passwordForm.confirm !== passwordForm.next ? "两次输入的新密码不一致" : "";
|
||||
return !Object.values(errors).some(Boolean);
|
||||
};
|
||||
const savePassword = () => {
|
||||
if (!validatePassword() || passwordState.value === "saving") return;
|
||||
passwordState.value = "saving";
|
||||
timer = setTimeout(() => { passwordState.value = "ready"; passwordForm.current = ""; passwordForm.next = ""; passwordForm.confirm = ""; showToast("密码已修改,请妥善保管新密码"); }, 500);
|
||||
};
|
||||
onUnmounted(() => clearTimeout(timer));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.password-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.security-tip,.form-panel{background:url("/static/assets/modules/profile/transparent/module-content-frame.png") center/100% 100% no-repeat}.security-tip{min-height:170rpx;padding:38rpx 44rpx;box-sizing:border-box;text-align:center}.security-tip text{display:block}.security-tip text:first-child{color:$ink;font-size:32rpx;font-weight:700}.security-tip text:last-child{margin-top:10rpx;color:$ink-muted;font-size:22rpx;line-height:1.55}.form-panel{margin-top:20rpx;padding:22rpx 34rpx 30rpx;box-sizing:border-box}.field-block+.field-block{margin-top:6rpx}.form-row{display:grid;grid-template-columns:142rpx minmax(0,1fr) 74rpx;min-height:92rpx;align-items:center;gap:12rpx;border-bottom:1px solid rgba(181,137,63,.42)}.form-row>text{color:$ink;font-size:23rpx;font-weight:700}.form-row input{width:auto;min-width:0;min-height:68rpx;color:$ink;font-size:23rpx}.password-toggle{display:flex;min-height:68rpx;align-items:center;justify-content:flex-end;color:$brand-red;font-size:21rpx}.field-error{display:block;padding-top:7rpx;color:#b42318;font-size:20rpx;line-height:1.4;text-align:right}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.form-panel{padding-right:26rpx;padding-left:26rpx}.form-row{grid-template-columns:120rpx minmax(0,1fr) 64rpx;gap:8rpx}}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,55 @@
|
||||
<!-- 页面编号:M-05;用途:修改手机号。 -->
|
||||
<template><ModulePage page-id="m05" /></template>
|
||||
<!-- 页面编号:M-05;用途:验证并更换绑定手机号。 -->
|
||||
<template>
|
||||
<view class="phone-page" :class="{ 'phone-state--ready': phoneState === 'ready', 'phone-state--saving': phoneState === 'saving' }">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="修改手机号" /></view>
|
||||
<view class="page-content page-layer">
|
||||
<view class="current-phone"><text>当前绑定手机号</text><text>{{ phoneForm.currentPhone }}</text><text>更换后,新手机号将用于登录与安全验证。</text></view>
|
||||
<view class="form-panel">
|
||||
<view class="form-row"><text>新手机号</text><input v-model.trim="phoneForm.newPhone" type="number" maxlength="11" aria-label="新手机号" placeholder="请输入新手机号" @input="errors.newPhone = ''" /></view>
|
||||
<text v-if="errors.newPhone" class="field-error">{{ errors.newPhone }}</text>
|
||||
<view class="form-row code-row"><text>验证码</text><input v-model.trim="phoneForm.code" type="number" maxlength="6" aria-label="短信验证码" placeholder="6 位验证码" @input="errors.code = ''" /><view class="code-action" role="button" :aria-label="codeCountdown ? `${codeCountdown}秒后可重新发送` : '发送验证码'" @click="sendCode">{{ codeCountdown ? `${codeCountdown}s` : "发送验证码" }}</view></view>
|
||||
<text v-if="errors.code" class="field-error">{{ errors.code }}</text>
|
||||
</view>
|
||||
<AppButton block :disabled="phoneState === 'saving'" :label="phoneState === 'saving' ? '正在更换' : '确认更换'" @click="savePhone" />
|
||||
</view>
|
||||
<AppToast :visible="toastVisible" :message="toastMessage" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { onUnmounted, reactive, ref } from "vue";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const phoneForm = reactive({ currentPhone: "139****6421", newPhone: "", code: "" });
|
||||
const errors = reactive({ newPhone: "", code: "" });
|
||||
const phoneState = ref("ready");
|
||||
const codeCountdown = ref(0);
|
||||
const toastVisible = ref(false); const toastMessage = ref("");
|
||||
let countdownTimer = null; let stateTimer = null; let toastTimer = null;
|
||||
const showToast = (message) => { toastMessage.value = message; toastVisible.value = true; clearTimeout(toastTimer); toastTimer = setTimeout(() => (toastVisible.value = false), 1800); };
|
||||
const sendCode = () => {
|
||||
if (codeCountdown.value) return;
|
||||
if (!/^1\d{10}$/.test(phoneForm.newPhone)) { errors.newPhone = "请输入正确的新手机号"; return; }
|
||||
errors.newPhone = ""; codeCountdown.value = 60; showToast("演示验证码已发送,请输入任意 6 位数字");
|
||||
countdownTimer = setInterval(() => { codeCountdown.value -= 1; if (!codeCountdown.value) { clearInterval(countdownTimer); countdownTimer = null; } }, 1000);
|
||||
};
|
||||
const validatePhone = () => {
|
||||
errors.newPhone = /^1\d{10}$/.test(phoneForm.newPhone) ? "" : "请输入正确的新手机号";
|
||||
errors.code = /^\d{6}$/.test(phoneForm.code) ? "" : "请输入 6 位验证码";
|
||||
return !errors.newPhone && !errors.code;
|
||||
};
|
||||
const savePhone = () => {
|
||||
if (!validatePhone() || phoneState.value === "saving") return;
|
||||
phoneState.value = "saving";
|
||||
stateTimer = setTimeout(() => { phoneState.value = "ready"; phoneForm.currentPhone = `${phoneForm.newPhone.slice(0,3)}****${phoneForm.newPhone.slice(-4)}`; phoneForm.newPhone = ""; phoneForm.code = ""; showToast("绑定手机号已更新"); }, 500);
|
||||
};
|
||||
onUnmounted(() => { clearInterval(countdownTimer); clearTimeout(stateTimer); clearTimeout(toastTimer); });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.phone-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.current-phone,.form-panel{background:url("/static/assets/modules/profile/transparent/module-content-frame.png") center/100% 100% no-repeat}.current-phone{min-height:210rpx;padding:40rpx 48rpx;box-sizing:border-box;text-align:center}.current-phone text{display:block}.current-phone text:first-child{color:$ink-muted;font-size:22rpx}.current-phone text:nth-child(2){margin-top:8rpx;color:$ink;font-size:38rpx;font-weight:700;letter-spacing:3rpx}.current-phone text:last-child{margin-top:12rpx;color:$ink-muted;font-size:21rpx;line-height:1.5}.form-panel{margin-top:22rpx;padding:24rpx 34rpx 32rpx;box-sizing:border-box}.form-row{display:grid;grid-template-columns:130rpx minmax(0,1fr);min-height:94rpx;align-items:center;gap:16rpx;border-bottom:1px solid rgba(181,137,63,.42)}.code-row{grid-template-columns:130rpx minmax(0,1fr) auto}.form-row>text{color:$ink;font-size:23rpx;font-weight:700}.form-row input{width:auto;min-width:0;min-height:70rpx;color:$ink;font-size:23rpx}.code-action{display:flex;min-width:126rpx;min-height:70rpx;align-items:center;justify-content:flex-end;color:$brand-red;font-size:21rpx}.field-error{display:block;padding-top:7rpx;color:#b42318;font-size:20rpx;text-align:right}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.form-panel{padding-right:26rpx;padding-left:26rpx}.form-row{grid-template-columns:112rpx minmax(0,1fr)}.code-row{grid-template-columns:112rpx minmax(0,1fr);}.code-action{grid-column:2;justify-content:flex-start}}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,48 @@
|
||||
<!-- 页面编号:M-06;用途:帮助中心。 -->
|
||||
<template><ModulePage page-id="m06" /></template>
|
||||
<!-- 页面编号:M-06;用途:帮助搜索、分类与常见问题。 -->
|
||||
<template>
|
||||
<view class="help-page">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="帮助中心" /></view>
|
||||
<view class="page-content page-layer">
|
||||
<view class="search-box"><input v-model.trim="keyword" aria-label="搜索帮助" placeholder="搜索问题关键词" /><text>{{ filteredQuestions.length }} 条</text></view>
|
||||
<scroll-view scroll-x class="category-scroll" :show-scrollbar="false"><view class="category-row"><view v-for="category in helpCategories" :key="category" class="category-chip" :class="{ active: activeCategory === category }" role="button" @click="activeCategory = category">{{ category }}</view></view></scroll-view>
|
||||
<view v-if="filteredQuestions.length" class="question-list">
|
||||
<view v-for="item in filteredQuestions" :key="item.id" class="question-card">
|
||||
<view class="question-heading" role="button" :aria-label="item.question" @click="toggleQuestion(item.id)"><text>{{ item.question }}</text><text>{{ expandedIds.includes(item.id) ? "收起" : "查看" }}</text></view>
|
||||
<text v-if="expandedIds.includes(item.id)" class="answer">{{ item.answer }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-card"><text>没有找到相关问题</text><text>换个关键词试试,或直接提交意见反馈。</text></view>
|
||||
<AppButton block type="secondary" label="联系家谱助手" @click="contactSupport" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { computed, ref } from "vue";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const helpCategories = ["全部", "家谱", "成员", "隐私", "账号"];
|
||||
const activeCategory = ref("全部");
|
||||
const keyword = ref("");
|
||||
const expandedIds = ref([]);
|
||||
const questions = [
|
||||
{ id: 1, category: "家谱", question: "如何创建一部新家谱?", answer: "进入“我的家谱”,选择新建家谱,按步骤填写姓氏、堂号和地区等基础资料。" },
|
||||
{ id: 2, category: "成员", question: "如何邀请家人共同完善家谱?", answer: "家人可搜索家谱后提交加入申请,管理员核实亲属关系后完成审核。" },
|
||||
{ id: 3, category: "隐私", question: "哪些个人资料会展示给其他成员?", answer: "资料按家谱角色与权限展示;敏感联系方式默认脱敏,后续可在家谱设置中管理权限。" },
|
||||
{ id: 4, category: "账号", question: "忘记密码后怎样恢复账号?", answer: "在登录页选择“忘记密码”,通过绑定手机号验证后设置新密码。" },
|
||||
{ id: 5, category: "家谱", question: "家谱资料填写错了怎么办?", answer: "有编辑权限的成员可以进入对应资料页修改;关键世系关系建议核对后再保存。" },
|
||||
];
|
||||
const filteredQuestions = computed(() => {
|
||||
const query = keyword.value.toLowerCase();
|
||||
return questions.filter((item) => (activeCategory.value === "全部" || item.category === activeCategory.value) && (!query || `${item.question}${item.answer}`.toLowerCase().includes(query)));
|
||||
});
|
||||
const toggleQuestion = (id) => { expandedIds.value = expandedIds.value.includes(id) ? expandedIds.value.filter((item) => item !== id) : [...expandedIds.value, id]; };
|
||||
const contactSupport = () => uni.navigateTo({ url: "/pages/profile/m07-feedback" });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.help-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:24rpx 28rpx 72rpx}.search-box{display:grid;grid-template-columns:minmax(0,1fr) auto;min-height:86rpx;align-items:center;gap:16rpx;padding:0 30rpx;background:url("/static/assets/modules/profile/transparent/module-field-frame.png") center/100% 100% no-repeat}.search-box input{width:auto;min-width:0;min-height:68rpx;color:$ink;font-size:23rpx}.search-box text{color:$ink-muted;font-size:20rpx}.category-scroll{width:100%;margin-top:18rpx}.category-row{display:flex;width:max-content;gap:12rpx;padding:2rpx}.category-chip{display:flex;min-width:100rpx;min-height:64rpx;align-items:center;justify-content:center;padding:0 22rpx;box-sizing:border-box;color:$ink-muted;font-size:22rpx}.category-chip.active{background:url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png") center/100% 100% no-repeat;color:$brand-red;font-weight:700}.question-list{display:flex;flex-direction:column;gap:14rpx;margin-top:18rpx}.question-card,.empty-card{background:url("/static/assets/modules/profile/transparent/module-content-frame.png") center/100% 100% no-repeat}.question-card{min-height:104rpx;padding:24rpx 34rpx;box-sizing:border-box}.question-heading{display:grid;grid-template-columns:minmax(0,1fr) auto;min-height:58rpx;align-items:center;gap:18rpx}.question-heading text:first-child{color:$ink;font-size:24rpx;font-weight:700;overflow-wrap:anywhere}.question-heading text:last-child{color:$brand-red;font-size:20rpx}.answer{display:block;padding:14rpx 4rpx 6rpx;border-top:1px solid rgba(181,137,63,.32);color:$ink-muted;font-size:22rpx;line-height:1.65;overflow-wrap:anywhere}.empty-card{min-height:220rpx;padding:58rpx 44rpx;box-sizing:border-box;text-align:center}.empty-card text{display:block}.empty-card text:first-child{color:$ink;font-size:29rpx;font-weight:700}.empty-card text:last-child{margin-top:12rpx;color:$ink-muted;font-size:22rpx;line-height:1.5}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:20rpx;padding-left:20rpx}}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,52 @@
|
||||
<!-- 页面编号:M-07;用途:意见反馈。 -->
|
||||
<template><ModulePage page-id="m07" /></template>
|
||||
<!-- 页面编号:M-07;用途:提交意见反馈。 -->
|
||||
<template>
|
||||
<view class="feedback-page" :class="{ 'feedback-state--ready': feedbackState === 'ready', 'feedback-state--submitting': feedbackState === 'submitting' }">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="意见反馈" /></view>
|
||||
<view class="page-content page-layer">
|
||||
<text class="lead">你的建议会帮助我们把家谱做得更好</text>
|
||||
<view class="type-grid">
|
||||
<view v-for="type in feedbackTypes" :key="type" class="type-chip" :class="{ active: feedbackForm.type === type }" role="button" @click="feedbackForm.type = type; errors.type = ''">{{ type }}</view>
|
||||
</view>
|
||||
<text v-if="errors.type" class="field-error">{{ errors.type }}</text>
|
||||
<view class="form-panel">
|
||||
<textarea v-model.trim="feedbackForm.description" auto-height maxlength="500" aria-label="问题描述" placeholder="请说明遇到的问题、操作步骤或建议" @input="errors.description = ''" />
|
||||
<text class="counter">{{ feedbackForm.description.length }}/500</text>
|
||||
<text v-if="errors.description" class="field-error">{{ errors.description }}</text>
|
||||
<view class="contact-row"><text>联系方式</text><input v-model.trim="feedbackForm.contact" maxlength="50" aria-label="联系方式" placeholder="手机号或邮箱(选填)" /></view>
|
||||
</view>
|
||||
<text class="privacy-note">仅用于跟进本次反馈,不会在家谱中公开。</text>
|
||||
<AppButton block :disabled="feedbackState === 'submitting'" :label="feedbackState === 'submitting' ? '正在提交' : '提交反馈'" @click="submitFeedback" />
|
||||
</view>
|
||||
<AppToast :visible="toastVisible" :message="toastMessage" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { onUnmounted, reactive, ref } from "vue";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const feedbackTypes = ["功能问题", "使用建议", "内容纠错", "其他"];
|
||||
const feedbackForm = reactive({ type: "", description: "", contact: "" });
|
||||
const errors = reactive({ type: "", description: "" });
|
||||
const feedbackState = ref("ready");
|
||||
const toastVisible = ref(false); const toastMessage = ref(""); let timer = null;
|
||||
const validateFeedback = () => {
|
||||
errors.type = feedbackForm.type ? "" : "请选择反馈类型";
|
||||
errors.description = feedbackForm.description.length < 5 ? "请至少填写 5 个字的问题描述" : "";
|
||||
return !errors.type && !errors.description;
|
||||
};
|
||||
const submitFeedback = () => {
|
||||
if (!validateFeedback() || feedbackState.value === "submitting") return;
|
||||
feedbackState.value = "submitting";
|
||||
timer = setTimeout(() => { feedbackState.value = "ready"; feedbackForm.type = ""; feedbackForm.description = ""; feedbackForm.contact = ""; toastMessage.value = "反馈已保存,服务接入后将提交给家谱助手"; toastVisible.value = true; timer = setTimeout(() => (toastVisible.value = false), 2200); }, 500);
|
||||
};
|
||||
onUnmounted(() => clearTimeout(timer));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.feedback-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:26rpx 30rpx 72rpx}.lead{display:block;color:$ink-muted;font-size:23rpx;line-height:1.5;text-align:center}.type-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14rpx;margin-top:20rpx}.type-chip{display:flex;min-height:76rpx;align-items:center;justify-content:center;padding:10rpx 16rpx;box-sizing:border-box;background:url("/static/assets/modules/profile/transparent/module-field-frame.png") center/100% 100% no-repeat;color:$ink-muted;font-size:22rpx;text-align:center}.type-chip.active{color:$brand-red;font-weight:700;filter:saturate(1.2)}.form-panel{margin-top:20rpx;padding:30rpx 34rpx;background:url("/static/assets/modules/profile/transparent/module-content-frame.png") center/100% 100% no-repeat}.form-panel textarea{display:block;width:auto;min-width:0;min-height:210rpx;color:$ink;font-size:24rpx;line-height:1.65}.counter{display:block;color:$ink-muted;font-size:20rpx;text-align:right}.contact-row{display:grid;grid-template-columns:130rpx minmax(0,1fr);min-height:88rpx;align-items:center;gap:16rpx;margin-top:16rpx;border-top:1px solid rgba(181,137,63,.38)}.contact-row text{color:$ink;font-size:23rpx;font-weight:700}.contact-row input{width:auto;min-width:0;min-height:68rpx;color:$ink;font-size:22rpx;text-align:right}.field-error{display:block;margin-top:7rpx;color:#b42318;font-size:20rpx;line-height:1.4;text-align:right}.privacy-note{display:block;margin-top:15rpx;color:$ink-muted;font-size:20rpx;line-height:1.5;text-align:center}.page-content>.app-button{margin-top:24rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.form-panel{padding-right:26rpx;padding-left:26rpx}}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,46 @@
|
||||
<!-- 页面编号:M-08;用途:应用推广。 -->
|
||||
<template><ModulePage page-id="m08" /></template>
|
||||
<!-- 页面编号:M-08;用途:邀请家人共建家谱。 -->
|
||||
<template>
|
||||
<view class="promotion-page share-state--ready">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="邀请家人" /></view>
|
||||
<view class="page-content page-layer">
|
||||
<view class="invite-hero">
|
||||
<image src="/static/assets/foundation/transparent/brand-seal.png" mode="aspectFit" />
|
||||
<text>邀请亲友共建家族记忆</text>
|
||||
<text>家人搜索家谱或使用邀请码后,仍需管理员审核才能加入。</text>
|
||||
</view>
|
||||
<view class="invite-code-card">
|
||||
<text>汝南汤氏家谱邀请码</text>
|
||||
<text class="invite-code">{{ inviteCode }}</text>
|
||||
<view class="copy-action" role="button" aria-label="复制邀请码" @click="copyInviteCode">复制邀请码</view>
|
||||
</view>
|
||||
<view class="steps-card"><text>邀请步骤</text><text>1. 生成邀请信息</text><text>2. 发送给家人</text><text>3. 家人提交加入申请</text><text>4. 管理员核实并审核</text></view>
|
||||
<AppButton block label="生成邀请海报" @click="generatePoster" />
|
||||
</view>
|
||||
<AppDialog :visible="posterVisible" eyebrow="家谱邀请" title="汝南汤氏家谱" :message="`邀请码 ${inviteCode}。长按或复制邀请码发送给家人。`" confirm-text="完成" @confirm="posterVisible = false" @close="posterVisible = false">
|
||||
<image class="poster-seal" src="/static/assets/foundation/transparent/brand-seal.png" mode="aspectFit" />
|
||||
</AppDialog>
|
||||
<AppToast :visible="toastVisible" :message="toastMessage" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { onUnmounted, ref } from "vue";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const inviteCode = ref("TN20260720");
|
||||
const posterVisible = ref(false);
|
||||
const toastVisible = ref(false); const toastMessage = ref(""); let timer = null;
|
||||
const showToast = (message) => { toastMessage.value = message; toastVisible.value = true; clearTimeout(timer); timer = setTimeout(() => (toastVisible.value = false), 1800); };
|
||||
const copyInviteCode = () => uni.setClipboardData({ data: inviteCode.value, showToast: false, success: () => showToast("邀请码已复制"), fail: () => showToast("复制失败,请长按邀请码复制") });
|
||||
const generatePoster = () => { posterVisible.value = true; };
|
||||
onUnmounted(() => clearTimeout(timer));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.promotion-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.invite-hero,.invite-code-card,.steps-card{background:url("/static/assets/modules/profile/transparent/module-content-frame.png") center/100% 100% no-repeat}.invite-hero{display:flex;min-height:300rpx;flex-direction:column;align-items:center;justify-content:center;padding:38rpx 48rpx;box-sizing:border-box;text-align:center}.invite-hero image{width:90rpx;height:auto;max-height:102rpx;aspect-ratio:90/102}.invite-hero text{display:block}.invite-hero text:nth-child(2){margin-top:12rpx;color:$ink;font-family:STKaiti,KaiTi,serif;font-size:34rpx;font-weight:700}.invite-hero text:last-child{margin-top:12rpx;color:$ink-muted;font-size:22rpx;line-height:1.55}.invite-code-card{min-height:220rpx;margin-top:20rpx;padding:38rpx 44rpx;box-sizing:border-box;text-align:center}.invite-code-card>text:first-child{display:block;color:$ink-muted;font-size:21rpx}.invite-code{display:block;margin-top:10rpx;color:$brand-red;font-size:42rpx;font-weight:700;letter-spacing:5rpx;overflow-wrap:anywhere}.copy-action{display:flex;min-height:62rpx;align-items:center;justify-content:center;color:$brand-red;font-size:22rpx}.steps-card{display:flex;min-height:260rpx;flex-direction:column;gap:10rpx;margin-top:20rpx;padding:34rpx 44rpx;box-sizing:border-box;color:$ink-muted;font-size:22rpx}.steps-card text:first-child{margin-bottom:4rpx;color:$ink;font-size:27rpx;font-weight:700}.page-content>.app-button{margin-top:26rpx}.poster-seal{width:96rpx;height:auto;max-height:110rpx;aspect-ratio:96/110;margin:18rpx auto 24rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.invite-code{font-size:35rpx;letter-spacing:3rpx}}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,39 @@
|
||||
<!-- 页面编号:M-09;用途:VIP 服务与订单。 -->
|
||||
<template><ModulePage page-id="m09" /></template>
|
||||
<!-- 页面编号:M-09;用途:服务权益与订单记录。 -->
|
||||
<template>
|
||||
<view class="orders-page" :class="orders.length ? 'order-state--ready' : 'order-state--empty'">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="VIP 与订单" /></view>
|
||||
<view class="page-content page-layer">
|
||||
<view class="service-card"><text>家谱基础服务</text><text>当前未开通付费服务</text><text>基础家谱浏览、成员资料与家族记录可正常使用。</text></view>
|
||||
<view class="benefit-grid"><view v-for="benefit in serviceBenefits" :key="benefit.title" class="benefit-card"><text>{{ benefit.title }}</text><text>{{ benefit.copy }}</text></view></view>
|
||||
<view class="section-heading"><text>订单记录</text><text>{{ orders.length }} 笔</text></view>
|
||||
<view v-if="orders.length" class="order-list"><view v-for="order in orders" :key="order.id" class="order-card"><view><text>{{ order.name }}</text><text>{{ order.createdAt }}</text></view><view><text>{{ order.amount }}</text><text>{{ order.status }}</text></view></view></view>
|
||||
<view v-else class="empty-card"><text>暂无订单记录</text><text>服务开放并完成购买后,订单状态会在这里展示。</text></view>
|
||||
<AppButton block type="secondary" label="查看服务说明" @click="openServiceNotice" />
|
||||
</view>
|
||||
<AppDialog :visible="serviceNoticeVisible" eyebrow="服务说明" title="付费服务尚未开放" message="当前版本不会产生扣费或订单。未来开放前会明确展示价格、权益、续费与退款规则。" confirm-text="我知道了" @confirm="serviceNoticeVisible = false" @close="serviceNoticeVisible = false" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const serviceBenefits = [
|
||||
{ title: "更大存储", copy: "为家族影像和资料提供更多空间" },
|
||||
{ title: "资料导出", copy: "按规则整理并导出家谱资料" },
|
||||
{ title: "专属服务", copy: "获得家谱整理与使用支持" },
|
||||
];
|
||||
const orders = ref([]);
|
||||
const serviceNoticeVisible = ref(false);
|
||||
onLoad((query) => { if (query.state === "ready") orders.value = [{ id: "O20260720001", name: "家谱服务演示订单", createdAt: "2026-07-20 08:30", amount: "¥0.00", status: "演示记录" }]; });
|
||||
const openServiceNotice = () => { serviceNoticeVisible.value = true; };
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.orders-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.service-card,.benefit-card,.order-card,.empty-card{background:url("/static/assets/modules/profile/transparent/module-content-frame.png") center/100% 100% no-repeat}.service-card{min-height:220rpx;padding:42rpx 48rpx;box-sizing:border-box;text-align:center}.service-card text{display:block}.service-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:34rpx;font-weight:700}.service-card text:nth-child(2){margin-top:10rpx;color:$brand-red;font-size:23rpx;font-weight:700}.service-card text:last-child{margin-top:12rpx;color:$ink-muted;font-size:21rpx;line-height:1.55}.benefit-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12rpx;margin-top:18rpx}.benefit-card{min-height:150rpx;padding:28rpx 18rpx;box-sizing:border-box;text-align:center}.benefit-card text{display:block;overflow-wrap:anywhere}.benefit-card text:first-child{color:$ink;font-size:23rpx;font-weight:700}.benefit-card text:last-child{margin-top:8rpx;color:$ink-muted;font-size:19rpx;line-height:1.45}.section-heading{display:flex;flex-wrap:wrap;justify-content:space-between;gap:8rpx 20rpx;margin:28rpx 6rpx 14rpx}.section-heading text:first-child{color:$ink;font-size:27rpx;font-weight:700}.section-heading text:last-child{color:$ink-muted;font-size:21rpx}.order-list{display:flex;flex-direction:column;gap:14rpx}.order-card{display:grid;grid-template-columns:minmax(0,1fr) auto;min-height:130rpx;align-items:center;gap:20rpx;padding:26rpx 34rpx;box-sizing:border-box}.order-card text{display:block;overflow-wrap:anywhere}.order-card view:first-child text:first-child{color:$ink;font-size:23rpx;font-weight:700}.order-card text:last-child{margin-top:6rpx;color:$ink-muted;font-size:20rpx}.order-card view:last-child{text-align:right}.order-card view:last-child text:first-child{color:$brand-red;font-size:23rpx;font-weight:700}.empty-card{min-height:190rpx;padding:48rpx 42rpx;box-sizing:border-box;text-align:center}.empty-card text{display:block}.empty-card text:first-child{color:$ink;font-size:28rpx;font-weight:700}.empty-card text:last-child{margin-top:12rpx;color:$ink-muted;font-size:21rpx;line-height:1.55}.page-content>.app-button{margin-top:26rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.benefit-grid{grid-template-columns:1fr}.benefit-card{min-height:100rpx}.order-card{grid-template-columns:1fr}.order-card view:last-child{text-align:left}}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
<!-- 页面编号:M-10;用途:关于、协议、隐私与退出确认。 -->
|
||||
<template><ModulePage page-id="m10" /></template>
|
||||
<!-- 页面编号:M-10;用途:协议、隐私、版本与退出登录。 -->
|
||||
<template>
|
||||
<view class="about-page">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="关于家谱" /></view>
|
||||
<view class="page-content page-layer">
|
||||
<view class="brand-card"><image src="/static/assets/foundation/transparent/brand-seal.png" mode="aspectFit" /><text>家谱</text><text>传承每一段值得珍藏的家族记忆</text><text>版本 {{ appVersion }}</text></view>
|
||||
<view class="settings-list">
|
||||
<view v-for="item in agreementItems" :key="item.key" class="settings-row" role="button" :aria-label="item.label" @click="openAgreement(item)"><view><text>{{ item.label }}</text><text>{{ item.note }}</text></view><image src="/static/assets/foundation/transparent/chevron-right.png" mode="aspectFit" /></view>
|
||||
</view>
|
||||
<AppButton block type="secondary" :label="loggedOut ? '已退出登录' : '退出登录'" :disabled="loggedOut" @click="logoutVisible = true" />
|
||||
</view>
|
||||
<AppDialog :visible="agreementVisible" eyebrow="协议与说明" :title="activeAgreement.label" :message="activeAgreement.copy" confirm-text="关闭" @confirm="agreementVisible = false" @close="agreementVisible = false" />
|
||||
<AppDialog :visible="logoutVisible" eyebrow="账号操作" title="确认退出登录?" message="退出后需要重新验证账号;本机保存的密码不会被保留。" confirm-text="确认退出" cancel-text="取消" show-cancel :close-on-mask="false" @confirm="confirmLogout" @cancel="logoutVisible = false" @close="logoutVisible = false" />
|
||||
<AppToast :visible="toastVisible" message="已退出当前账号" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { onUnmounted, reactive, ref } from "vue";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const appVersion = "1.0.0";
|
||||
const agreementItems = [
|
||||
{ key: "terms", label: "用户协议", note: "了解账号与服务使用规则", copy: "用户协议正文将在正式协议服务接入后展示。当前页面不代表最终法律文本。" },
|
||||
{ key: "privacy", label: "隐私政策", note: "了解个人资料如何使用与保护", copy: "隐私政策正文将在正式协议服务接入后展示。敏感资料默认按权限与脱敏规则展示。" },
|
||||
{ key: "version", label: "版本说明", note: `当前版本 ${appVersion}`, copy: `当前安装版本为 ${appVersion}。基础版本采用浅色国风主题。` },
|
||||
];
|
||||
const activeAgreement = reactive({ label: "", copy: "" });
|
||||
const agreementVisible = ref(false); const logoutVisible = ref(false); const toastVisible = ref(false); const loggedOut = ref(false); let timer = null;
|
||||
const openAgreement = (item) => { activeAgreement.label = item.label; activeAgreement.copy = item.copy; agreementVisible.value = true; };
|
||||
const confirmLogout = () => { logoutVisible.value = false; loggedOut.value = true; toastVisible.value = true; timer = setTimeout(() => (toastVisible.value = false), 1800); };
|
||||
onUnmounted(() => clearTimeout(timer));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.about-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.brand-card{display:flex;min-height:300rpx;flex-direction:column;align-items:center;justify-content:center;padding:36rpx 44rpx;box-sizing:border-box;background:url("/static/assets/modules/profile/transparent/m01-profile-summary-card.png") center/100% 100% no-repeat;text-align:center}.brand-card image{width:92rpx;height:106rpx}.brand-card text{display:block}.brand-card text:nth-child(2){margin-top:6rpx;color:$ink;font-family:STKaiti,KaiTi,serif;font-size:40rpx;font-weight:700;letter-spacing:4rpx}.brand-card text:nth-child(3){margin-top:8rpx;color:$ink-muted;font-size:22rpx;line-height:1.5}.brand-card text:last-child{margin-top:10rpx;color:$ink-muted;font-size:20rpx}.settings-list{display:flex;flex-direction:column;gap:12rpx;margin-top:22rpx}.settings-row{display:grid;grid-template-columns:minmax(0,1fr) 32rpx;min-height:104rpx;align-items:center;gap:18rpx;padding:18rpx 30rpx;box-sizing:border-box;background:url("/static/assets/modules/profile/transparent/module-field-frame.png") center/100% 100% no-repeat}.settings-row view{min-width:0}.settings-row text{display:block;overflow-wrap:anywhere}.settings-row text:first-child{color:$ink;font-size:24rpx;font-weight:700}.settings-row text:last-child{margin-top:6rpx;color:$ink-muted;font-size:20rpx}.settings-row image{width:30rpx;height:30rpx}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}}
|
||||
</style>
|
||||
|
||||
@@ -14,10 +14,6 @@
|
||||
<view class="people-content">
|
||||
<template v-if="peopleState === 'ready'">
|
||||
<view class="people-search">
|
||||
<image
|
||||
src="/static/assets/modules/records/transparent/r01-search-input-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<input
|
||||
v-model="keywordInput"
|
||||
confirm-type="search"
|
||||
@@ -41,11 +37,6 @@
|
||||
class="person-card"
|
||||
@click="openPerson(person)"
|
||||
>
|
||||
<image
|
||||
class="person-card__skin"
|
||||
src="/static/assets/modules/records/transparent/r01-person-name-card.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="person-card__copy">
|
||||
<text class="person-card__name">{{ person.name }}</text>
|
||||
<text class="person-card__meta"
|
||||
@@ -63,11 +54,7 @@
|
||||
</view>
|
||||
|
||||
<view v-else class="people-result-empty">
|
||||
<image
|
||||
src="/static/assets/modules/records/transparent/r01-person-name-card.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view
|
||||
<view class="people-state-card__copy"
|
||||
><text>没有找到相关人物</text
|
||||
><text>请更换姓名、身份或世代关键词后再试。</text></view
|
||||
>
|
||||
@@ -81,11 +68,7 @@
|
||||
</template>
|
||||
|
||||
<view v-else class="people-state-card">
|
||||
<image
|
||||
src="/static/assets/modules/records/transparent/r01-person-name-card.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view>
|
||||
<view class="people-state-card__copy">
|
||||
<text>{{
|
||||
peopleState === "empty" ? "还没有人物记录" : "人物录暂不可用"
|
||||
}}</text>
|
||||
@@ -171,12 +154,7 @@ const openPerson = (person) =>
|
||||
url: `/pages/records/r02-person-detail?personId=${person.id}`,
|
||||
});
|
||||
const showCreateNotice = () => {
|
||||
toastVisible.value = true;
|
||||
if (toastTimer) clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => {
|
||||
toastVisible.value = false;
|
||||
toastTimer = null;
|
||||
}, 1800);
|
||||
uni.navigateTo({ url: "/pages/records/r02-person-detail?mode=create" });
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -186,37 +164,28 @@ onUnmounted(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.people-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.people-page__header,
|
||||
.people-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
}
|
||||
.people-content {
|
||||
flex: 1;
|
||||
padding: 22rpx 24rpx 100rpx;
|
||||
}
|
||||
.people-search {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 126rpx;
|
||||
width: 100%;
|
||||
height: 82rpx;
|
||||
min-height: 44px;
|
||||
align-items: center;
|
||||
}
|
||||
.people-search > image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: url("/static/assets/modules/records/transparent/r01-search-input-frame.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.people-search input {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
@@ -230,7 +199,6 @@ onUnmounted(() => {
|
||||
color: #8a7965;
|
||||
}
|
||||
.people-search__action {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
width: 126rpx;
|
||||
@@ -246,7 +214,6 @@ onUnmounted(() => {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.person-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: clamp(92px, 190rpx, 108px);
|
||||
@@ -254,20 +221,12 @@ onUnmounted(() => {
|
||||
margin-top: 12px;
|
||||
padding: 16% 10%;
|
||||
box-sizing: border-box;
|
||||
background: url("/static/assets/modules/records/transparent/r01-person-name-card.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.person-card:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.person-card__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.person-card__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
@@ -294,27 +253,12 @@ onUnmounted(() => {
|
||||
}
|
||||
.people-result-empty,
|
||||
.people-state-card {
|
||||
position: relative;
|
||||
margin-top: 38rpx;
|
||||
background: url("/static/assets/modules/records/transparent/r01-person-name-card.png") top center / 100% 220rpx no-repeat;
|
||||
text-align: center;
|
||||
}
|
||||
.people-result-empty > image,
|
||||
.people-state-card > image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 220rpx;
|
||||
min-height: 118px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.people-result-empty > view,
|
||||
.people-state-card > view {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
.people-state-card__copy {
|
||||
display: flex;
|
||||
height: 220rpx;
|
||||
min-height: 118px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<template>
|
||||
<view class="person-detail-page" :class="`person-detail-state--${personState}`">
|
||||
<ModulePageBackground module="records" />
|
||||
<view class="person-detail-header"><PageHeader :title="personState === 'edit' ? '编辑人物' : '人物详情'" /></view>
|
||||
<view class="person-detail-header"><PageHeader :title="personState === 'edit' ? (isCreateMode ? '新建人物' : '编辑人物') : '人物详情'" /></view>
|
||||
|
||||
<view v-if="personState === 'loading'" class="person-detail-loading">
|
||||
<AppLoading text="正在读取人物档案" description="请稍候,正在整理人物资料。" />
|
||||
@@ -11,7 +11,6 @@
|
||||
<view v-else class="person-detail-content">
|
||||
<template v-if="['detail', 'edit', 'privacy'].includes(personState)">
|
||||
<view class="person-identity-card">
|
||||
<image class="person-card-skin" src="/static/assets/modules/records/transparent/r01-person-name-card.png" mode="scaleToFill" />
|
||||
<view class="person-identity-card__copy">
|
||||
<text class="person-identity-card__name">{{ person.name }}</text>
|
||||
<text class="person-identity-card__meta">{{ person.role }} · 第 {{ person.generation }} 世</text>
|
||||
@@ -22,23 +21,24 @@
|
||||
|
||||
<template v-if="personState === 'detail'">
|
||||
<view v-for="item in detailSections" :key="item.title" class="person-archive-card">
|
||||
<image class="person-card-skin" src="/static/assets/modules/records/transparent/module-content-frame.png" mode="scaleToFill" />
|
||||
<view><text>{{ item.title }}</text><text>{{ item.copy }}</text></view>
|
||||
</view>
|
||||
<view class="person-related-actions">
|
||||
<AppButton type="secondary" block label="成长日志" @click="toGrowthJournal" />
|
||||
<AppButton type="secondary" block label="人生大事" @click="toLifeEvents" />
|
||||
</view>
|
||||
<view class="person-edit-action" @click="enterEdit"><AppButton block label="编辑人物" /></view>
|
||||
</template>
|
||||
|
||||
<template v-else-if="personState === 'edit'">
|
||||
<view v-for="field in shortFields" :key="field.key" class="person-field">
|
||||
<image class="person-card-skin" src="/static/assets/modules/records/transparent/module-field-frame.png" mode="scaleToFill" />
|
||||
<text class="person-field__label">{{ field.label }}</text>
|
||||
<input v-model="draft[field.key]" :type="field.key === 'generation' ? 'number' : 'text'" :placeholder="`请输入${field.label}`" />
|
||||
<text v-if="errors[field.key]" class="person-field-error">{{ errors[field.key] }}</text>
|
||||
</view>
|
||||
<view v-for="field in longFields" :key="field.key" class="person-long-field">
|
||||
<image class="person-card-skin" src="/static/assets/modules/records/transparent/module-content-frame.png" mode="scaleToFill" />
|
||||
<text class="person-long-field__label">{{ field.label }}</text>
|
||||
<textarea v-model="draft[field.key]" :placeholder="`请输入${field.label}`" />
|
||||
<textarea v-model="draft[field.key]" auto-height :placeholder="`请输入${field.label}`" />
|
||||
</view>
|
||||
<view class="person-edit-actions">
|
||||
<view class="person-save-action" @click="savePerson"><AppButton block label="保存人物" /></view>
|
||||
@@ -47,13 +47,11 @@
|
||||
</template>
|
||||
|
||||
<view v-else-if="personState === 'privacy'" class="person-state-card">
|
||||
<image class="person-card-skin" src="/static/assets/modules/records/transparent/module-content-frame.png" mode="scaleToFill" />
|
||||
<view><text>部分资料未公开</text><text>人物小传与家族印记受隐私设置保护,当前只展示公开身份。</text></view>
|
||||
<view class="person-state-action" @click="returnToPeople"><AppButton block label="返回人物录" /></view>
|
||||
</view>
|
||||
|
||||
<view v-else class="person-state-card">
|
||||
<image class="person-card-skin" src="/static/assets/modules/records/transparent/module-content-frame.png" mode="scaleToFill" />
|
||||
<view>
|
||||
<text>{{ personState === 'expired' ? '人物档案已失效' : '人物档案暂不可用' }}</text>
|
||||
<text>{{ personState === 'expired' ? '这份人物资料已无法查看,请返回人物录选择其他档案。' : '请稍后重新查看,已有资料不会受到影响。' }}</text>
|
||||
@@ -85,6 +83,7 @@ const person = reactive({ ...people[0] });
|
||||
const draft = reactive({ name: "", role: "", generation: "", biography: "", legacy: "" });
|
||||
const errors = reactive({ name: "", role: "", generation: "" });
|
||||
const personState = ref("loading");
|
||||
const isCreateMode = ref(false);
|
||||
const toastVisible = ref(false);
|
||||
let toastTimer = null;
|
||||
const shortFields = [{ key: "name", label: "姓名" }, { key: "role", label: "身份" }, { key: "generation", label: "世代" }];
|
||||
@@ -100,7 +99,8 @@ const savePerson = () => {
|
||||
if (!String(draft.role).trim()) errors.role = "请填写身份";
|
||||
if (!String(draft.generation).trim()) errors.generation = "请填写世代";
|
||||
if (errors.name || errors.role || errors.generation) return;
|
||||
Object.assign(person, { ...draft, name: draft.name.trim(), role: draft.role.trim(), generation: String(draft.generation).trim() });
|
||||
Object.assign(person, { id: person.id || "new", ...draft, name: draft.name.trim(), role: draft.role.trim(), generation: String(draft.generation).trim() });
|
||||
isCreateMode.value = false;
|
||||
personState.value = "detail";
|
||||
toastVisible.value = true;
|
||||
if (toastTimer) clearTimeout(toastTimer);
|
||||
@@ -108,7 +108,29 @@ const savePerson = () => {
|
||||
};
|
||||
const returnToPeople = () => uni.reLaunch({ url: "/pages/records/r01-people-list" });
|
||||
const restoreDetail = () => { personState.value = "detail"; };
|
||||
const toGrowthJournal = () =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/records/r08-growth-journal?personId=${person.id}`,
|
||||
});
|
||||
const toLifeEvents = () =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/records/r09-life-events?personId=${person.id}`,
|
||||
});
|
||||
onLoad((query) => {
|
||||
isCreateMode.value = query.mode === "create";
|
||||
if (isCreateMode.value) {
|
||||
Object.assign(person, {
|
||||
id: "",
|
||||
name: "",
|
||||
role: "",
|
||||
generation: "",
|
||||
biography: "",
|
||||
legacy: "",
|
||||
});
|
||||
copyToDraft();
|
||||
personState.value = "edit";
|
||||
return;
|
||||
}
|
||||
const selected = people.find((item) => item.id === String(query.personId || ""));
|
||||
if (selected) Object.assign(person, selected);
|
||||
copyToDraft();
|
||||
@@ -119,33 +141,31 @@ onUnmounted(() => { if (toastTimer) clearTimeout(toastTimer); });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.person-detail-page { min-height: 100vh; overflow-x: hidden; background: $paper; }
|
||||
.person-detail-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.person-detail-header,.person-detail-loading,.person-detail-content { z-index: 1; }
|
||||
.person-detail-loading { min-height: calc(100vh - 100rpx); }
|
||||
.person-detail-content { padding: 18rpx 24rpx 72rpx; }
|
||||
.person-identity-card,.person-archive-card,.person-field,.person-long-field,.person-state-card { position: relative; }
|
||||
.person-card-skin { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; pointer-events: none; }
|
||||
.person-identity-card { display: flex; min-height: 190rpx; padding: 30rpx 10%; box-sizing: border-box; }
|
||||
.person-identity-card__copy { position: relative; z-index: 1; display: flex; flex: 1; flex-direction: column; justify-content: center; }
|
||||
.person-identity-card { display: flex; min-height: 190rpx; padding: 30rpx 10%; box-sizing: border-box; background: url("/static/assets/modules/records/transparent/r01-person-name-card.png") center / 100% 100% no-repeat; }
|
||||
.person-identity-card__copy { display: flex; flex: 1; flex-direction: column; justify-content: center; }
|
||||
.person-identity-card__copy text { display: block; }
|
||||
.person-identity-card__name { color: $ink; font-family: STKaiti,KaiTi,serif; font-size: 38rpx; font-weight: 700; }
|
||||
.person-identity-card__meta { margin-top: 8rpx; color: $ink-muted; font-size: 25rpx; font-weight: 600; }
|
||||
.person-identity-card__hint { margin-top: 8rpx; color: #806a51; font-size: 21rpx; }
|
||||
.person-archive-card { min-height: 154rpx; margin-top: 14rpx; padding: 32rpx 42rpx; box-sizing: border-box; }
|
||||
.person-archive-card > view { position: relative; z-index: 1; }
|
||||
.person-archive-card,.person-long-field,.person-state-card { background: url("/static/assets/modules/records/transparent/module-content-frame.png") center / 100% 100% no-repeat; }
|
||||
.person-archive-card text { display: block; }
|
||||
.person-archive-card text:first-child,.person-long-field__label { color: $brand-red; font-size: 24rpx; font-weight: 700; }
|
||||
.person-archive-card text:last-child { margin-top: 11rpx; color: $ink; font-size: 24rpx; line-height: 1.55; }
|
||||
.person-edit-action { margin-top: 20rpx; }
|
||||
.person-field { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 8rpx 24rpx; min-height: 92rpx; margin-top: 12rpx; padding: 18rpx 28rpx; box-sizing: border-box; }
|
||||
.person-field__label { position: relative; z-index: 1; color: $ink; font-size: 23rpx; font-weight: 700; }
|
||||
.person-field input { position: relative; z-index: 1; width: 100%; min-width: 0; height: 56rpx; color: $ink; font-size: 23rpx; text-align: right; }
|
||||
.person-field-error { position: relative; z-index: 1; grid-column: 1 / -1; display: block; color: $brand-red; font-size: 21rpx; text-align: right; }
|
||||
.person-related-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14rpx; margin-top: 18rpx; }
|
||||
.person-field { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 8rpx 24rpx; min-height: 92rpx; margin-top: 12rpx; padding: 18rpx 28rpx; box-sizing: border-box; background: url("/static/assets/modules/records/transparent/module-field-frame.png") center / 100% 100% no-repeat; }
|
||||
.person-field__label { color: $ink; font-size: 23rpx; font-weight: 700; }
|
||||
.person-field input { width: 100%; min-width: 0; min-height: 56rpx; color: $ink; font-size: 23rpx; text-align: right; }
|
||||
.person-field-error { grid-column: 1 / -1; display: block; color: $brand-red; font-size: 21rpx; text-align: right; }
|
||||
.person-long-field { display: flex; flex-direction: column; min-height: 210rpx; margin-top: 14rpx; padding: 28rpx 38rpx; box-sizing: border-box; }
|
||||
.person-long-field__label,.person-long-field textarea { position: relative; z-index: 1; }
|
||||
.person-long-field textarea { width: 100%; height: 112rpx; min-height: 112rpx; margin-top: 14rpx; color: $ink; font-size: 23rpx; line-height: 1.5; }
|
||||
.person-long-field textarea { width: 100%; min-height: 112rpx; margin-top: 14rpx; color: $ink; font-size: 23rpx; line-height: 1.5; }
|
||||
.person-edit-actions { display: flex; flex-direction: column; gap: 14rpx; margin-top: 20rpx; }
|
||||
.person-state-card { display: flex; flex-direction: column; min-height: 300rpx; margin-top: 26rpx; padding: 72rpx 54rpx 42rpx; box-sizing: border-box; text-align: center; }
|
||||
.person-state-card > view { position: relative; z-index: 1; }
|
||||
.person-state-card text { display: block; }
|
||||
.person-state-card text:first-child { color: $ink; font-family: STKaiti,KaiTi,serif; font-size: 34rpx; font-weight: 700; }
|
||||
.person-state-card text:last-child { margin-top: 16rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.6; }
|
||||
|
||||
@@ -1,5 +1,193 @@
|
||||
<!-- 页面编号:R-03;用途:贺礼列表。 -->
|
||||
<template><ModulePage page-id="r03" /></template>
|
||||
<!-- 页面编号:R-03;用途:贺礼簿列表、空态、失败与新增入口。 -->
|
||||
<template>
|
||||
<view class="gift-page" :class="stateClasses">
|
||||
<ModulePageBackground module="records" />
|
||||
<view class="page-header">
|
||||
<PageHeader title="贺礼簿" action="新增" @action="createGift" />
|
||||
</view>
|
||||
<view v-if="giftState === 'loading'" class="page-loading">
|
||||
<AppLoading
|
||||
text="正在整理贺礼簿"
|
||||
description="请稍候,正在读取家人的礼仪往来。"
|
||||
/>
|
||||
</view>
|
||||
<view v-else class="page-content">
|
||||
<template v-if="giftState === 'ready' && giftBooks.length">
|
||||
<view
|
||||
v-for="gift in giftBooks"
|
||||
:key="gift.id"
|
||||
class="record-card"
|
||||
role="button"
|
||||
:aria-label="`查看${gift.title}`"
|
||||
@click="openGiftBook(gift)"
|
||||
>
|
||||
<text class="record-card__tag">{{ gift.occasion }}</text>
|
||||
<text class="record-card__title">{{ gift.title }}</text>
|
||||
<text class="record-card__copy">
|
||||
{{ gift.from }} · {{ gift.date }}
|
||||
</text>
|
||||
<text class="record-card__hint">查看并编辑贺礼</text>
|
||||
</view>
|
||||
<AppButton block label="新增贺礼" @click="createGift" />
|
||||
</template>
|
||||
<view v-else class="state-card">
|
||||
<text>
|
||||
{{ giftState === "error" ? "贺礼簿暂不可用" : "还没有贺礼记录" }}
|
||||
</text>
|
||||
<text>
|
||||
{{
|
||||
giftState === "error"
|
||||
? "请稍后重新查看,已有记录不会受到影响。"
|
||||
: "从第一份家人之间的心意开始记录。"
|
||||
}}
|
||||
</text>
|
||||
<AppButton
|
||||
:type="giftState === 'error' ? 'secondary' : 'primary'"
|
||||
block
|
||||
:label="giftState === 'error' ? '重新查看' : '新增贺礼'"
|
||||
@click="giftState === 'error' ? restoreGifts() : createGift()"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
const baseGifts = [
|
||||
{
|
||||
id: "301",
|
||||
title: "新春贺礼",
|
||||
occasion: "春节",
|
||||
from: "汤文正一家",
|
||||
date: "2024 年 2 月 10 日",
|
||||
},
|
||||
{
|
||||
id: "302",
|
||||
title: "寿宴礼单",
|
||||
occasion: "寿辰",
|
||||
from: "汤淑华",
|
||||
date: "2024 年 4 月 18 日",
|
||||
},
|
||||
{
|
||||
id: "303",
|
||||
title: "添丁祝福",
|
||||
occasion: "新生",
|
||||
from: "汤文清一家",
|
||||
date: "2024 年 6 月 2 日",
|
||||
},
|
||||
];
|
||||
const giftBooks = ref([...baseGifts]);
|
||||
const giftState = ref("loading");
|
||||
const stateClasses = computed(() => ({
|
||||
"gift-state--loading": giftState.value === "loading",
|
||||
"gift-state--empty": giftState.value === "empty",
|
||||
"gift-state--error": giftState.value === "error",
|
||||
}));
|
||||
onLoad((query) => {
|
||||
const count = Math.max(
|
||||
1,
|
||||
Math.min(Number(query.count) || baseGifts.length, 50),
|
||||
);
|
||||
giftBooks.value = Array.from({ length: count }, (_, i) => ({
|
||||
...baseGifts[i % baseGifts.length],
|
||||
id: String(301 + i),
|
||||
title:
|
||||
count > 3 ? `${baseGifts[i % 3].title}(${i + 1})` : baseGifts[i].title,
|
||||
}));
|
||||
giftState.value = ["loading", "empty", "error"].includes(query.state)
|
||||
? query.state
|
||||
: "ready";
|
||||
});
|
||||
const openGiftBook = (gift) =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/records/r04-gift-editor?mode=view&giftId=${gift.id}`,
|
||||
});
|
||||
const createGift = () =>
|
||||
uni.navigateTo({ url: "/pages/records/r04-gift-editor?mode=create" });
|
||||
const restoreGifts = () => {
|
||||
giftState.value = "ready";
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.gift-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.page-header,
|
||||
.page-loading,
|
||||
.page-content {
|
||||
z-index: 1;
|
||||
}
|
||||
.page-loading {
|
||||
min-height: calc(100vh - 100rpx);
|
||||
}
|
||||
.page-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
padding: 18rpx 24rpx 72rpx;
|
||||
}
|
||||
.record-card,
|
||||
.state-card {
|
||||
box-sizing: border-box;
|
||||
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
|
||||
center/100% 100% no-repeat;
|
||||
}
|
||||
.record-card {
|
||||
min-height: 190rpx;
|
||||
padding: 34rpx 46rpx;
|
||||
}
|
||||
.record-card > text,
|
||||
.state-card > text {
|
||||
display: block;
|
||||
}
|
||||
.record-card__tag {
|
||||
color: $brand-red;
|
||||
font-size: 21rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.record-card__title {
|
||||
margin-top: 6rpx;
|
||||
color: $ink;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.record-card__copy {
|
||||
margin-top: 9rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.record-card__hint {
|
||||
margin-top: 10rpx;
|
||||
color: $brand-red;
|
||||
font-size: 21rpx;
|
||||
}
|
||||
.state-card {
|
||||
min-height: 340rpx;
|
||||
padding: 78rpx 52rpx 50rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.state-card > text:first-child {
|
||||
color: $ink;
|
||||
font-size: 35rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.state-card > text:nth-child(2) {
|
||||
margin-top: 18rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.state-card .app-button {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,260 @@
|
||||
<!-- 页面编号:R-04;用途:贺礼新增、详情与删除确认。 -->
|
||||
<template><ModulePage page-id="r04" /></template>
|
||||
<!-- 页面编号:R-04;用途:贺礼查看、新增、编辑、保存与删除确认。 -->
|
||||
<template>
|
||||
<view class="gift-editor-page" :class="editorClasses">
|
||||
<ModulePageBackground module="records" />
|
||||
<view class="page-header">
|
||||
<PageHeader
|
||||
:title="
|
||||
mode === 'create'
|
||||
? '新增贺礼'
|
||||
: mode === 'view'
|
||||
? '贺礼详情'
|
||||
: '编辑贺礼'
|
||||
"
|
||||
:action="mode === 'view' ? '编辑' : ''"
|
||||
@action="mode = 'edit'"
|
||||
/>
|
||||
</view>
|
||||
<view v-if="editorState === 'loading'" class="page-loading">
|
||||
<AppLoading
|
||||
text="正在读取贺礼"
|
||||
description="请稍候,正在整理这份礼仪记录。"
|
||||
/>
|
||||
</view>
|
||||
<view v-else class="page-content">
|
||||
<view v-if="editorState === 'success'" class="state-card">
|
||||
<text>贺礼已保存</text>
|
||||
<text>这份心意已加入家族贺礼簿。</text>
|
||||
<AppButton block label="返回贺礼簿" @click="backToGifts" />
|
||||
</view>
|
||||
<view v-else-if="mode === 'view'" class="detail-card">
|
||||
<text>{{ giftForm.title }}</text>
|
||||
<view v-for="item in detailRows" :key="item.label">
|
||||
<text>{{ item.label }}</text>
|
||||
<text>{{ item.value }}</text>
|
||||
</view>
|
||||
<AppButton block label="编辑贺礼" @click="mode = 'edit'" />
|
||||
<AppButton
|
||||
type="secondary"
|
||||
block
|
||||
label="删除记录"
|
||||
@click="confirmDelete"
|
||||
/>
|
||||
</view>
|
||||
<view v-else class="form-card">
|
||||
<text>
|
||||
{{ mode === "create" ? "记录一份家人心意" : "修改贺礼信息" }}
|
||||
</text>
|
||||
<view v-for="field in fields" :key="field.key" class="field-row">
|
||||
<text>{{ field.label }}</text>
|
||||
<input
|
||||
v-model="giftForm[field.key]"
|
||||
:placeholder="`请输入${field.label}`"
|
||||
/>
|
||||
<text v-if="giftErrors[field.key]">{{ giftErrors[field.key] }}</text>
|
||||
</view>
|
||||
<text v-if="editorState === 'error'" class="save-error">
|
||||
保存失败,请检查内容后重试。
|
||||
</text>
|
||||
<AppButton
|
||||
block
|
||||
:disabled="editorState === 'saving'"
|
||||
:label="editorState === 'saving' ? '正在保存' : '保存贺礼'"
|
||||
@click="saveGift"
|
||||
/>
|
||||
<AppButton
|
||||
v-if="mode === 'edit'"
|
||||
type="secondary"
|
||||
block
|
||||
label="删除记录"
|
||||
@click="confirmDelete"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<AppDialog
|
||||
:visible="deleteVisible"
|
||||
eyebrow="删除确认"
|
||||
title="删除这份贺礼?"
|
||||
message="删除后将返回贺礼簿,本地演示记录不会继续显示。"
|
||||
confirm-text="确认删除"
|
||||
cancel-text="保留记录"
|
||||
show-cancel
|
||||
@confirm="deleteGift"
|
||||
@cancel="deleteVisible = false"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { computed, onUnmounted, reactive, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
const records = [
|
||||
{
|
||||
id: "301",
|
||||
title: "新春贺礼",
|
||||
from: "汤文正一家",
|
||||
date: "2024-02-10",
|
||||
note: "新春团拜时赠予长辈的心意",
|
||||
},
|
||||
{
|
||||
id: "302",
|
||||
title: "寿宴礼单",
|
||||
from: "汤淑华",
|
||||
date: "2024-04-18",
|
||||
note: "汤老先生八十寿辰",
|
||||
},
|
||||
];
|
||||
const giftId = ref("");
|
||||
const mode = ref("create");
|
||||
const editorState = ref("ready");
|
||||
const deleteVisible = ref(false);
|
||||
const forceSaveFailure = ref(false);
|
||||
const giftForm = reactive({ title: "", from: "", date: "", note: "" });
|
||||
const giftErrors = reactive({ title: "", from: "", date: "" });
|
||||
const fields = [
|
||||
{ key: "title", label: "贺礼名称" },
|
||||
{ key: "from", label: "赠送人" },
|
||||
{ key: "date", label: "日期" },
|
||||
{ key: "note", label: "备注" },
|
||||
];
|
||||
let saveTimer = null;
|
||||
const editorClasses = computed(() => ({
|
||||
"gift-editor-state--saving": editorState.value === "saving",
|
||||
"gift-editor-state--error": editorState.value === "error",
|
||||
}));
|
||||
const detailRows = computed(() =>
|
||||
fields
|
||||
.slice(1)
|
||||
.map((f) => ({ label: f.label, value: giftForm[f.key] || "未填写" })),
|
||||
);
|
||||
onLoad((query) => {
|
||||
giftId.value = String(query.giftId || "");
|
||||
mode.value = ["view", "edit"].includes(query.mode) ? query.mode : "create";
|
||||
forceSaveFailure.value = query.saveResult === "error";
|
||||
const selected = records.find((x) => x.id === giftId.value);
|
||||
if (selected) Object.assign(giftForm, selected);
|
||||
else if (mode.value !== "create") editorState.value = "error";
|
||||
if (query.state === "loading") editorState.value = "loading";
|
||||
});
|
||||
const validateGift = () => {
|
||||
giftErrors.title = giftForm.title.trim() ? "" : "请填写贺礼名称";
|
||||
giftErrors.from = giftForm.from.trim() ? "" : "请填写赠送人";
|
||||
giftErrors.date = giftForm.date.trim() ? "" : "请填写日期";
|
||||
return !giftErrors.title && !giftErrors.from && !giftErrors.date;
|
||||
};
|
||||
const saveGift = () => {
|
||||
if (editorState.value === "saving" || !validateGift()) return;
|
||||
editorState.value = "saving";
|
||||
saveTimer = setTimeout(() => {
|
||||
editorState.value = forceSaveFailure.value ? "error" : "success";
|
||||
forceSaveFailure.value = false;
|
||||
}, 320);
|
||||
};
|
||||
const confirmDelete = () => {
|
||||
deleteVisible.value = true;
|
||||
};
|
||||
const deleteGift = () => {
|
||||
deleteVisible.value = false;
|
||||
backToGifts();
|
||||
};
|
||||
const backToGifts = () =>
|
||||
uni.redirectTo({ url: "/pages/records/r03-gift-list" });
|
||||
onUnmounted(() => {
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.gift-editor-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.page-header,
|
||||
.page-loading,
|
||||
.page-content {
|
||||
z-index: 1;
|
||||
}
|
||||
.page-loading {
|
||||
min-height: calc(100vh - 100rpx);
|
||||
}
|
||||
.page-content {
|
||||
padding: 18rpx 24rpx 72rpx;
|
||||
}
|
||||
.form-card,
|
||||
.detail-card,
|
||||
.state-card {
|
||||
box-sizing: border-box;
|
||||
padding: 46rpx;
|
||||
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
|
||||
center/100% 100% no-repeat;
|
||||
}
|
||||
.form-card > text:first-child,
|
||||
.detail-card > text:first-child,
|
||||
.state-card > text:first-child {
|
||||
display: block;
|
||||
color: $ink;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.field-row,
|
||||
.detail-card > view {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 12rpx 20rpx;
|
||||
min-height: 82rpx;
|
||||
margin-top: 14rpx;
|
||||
padding: 14rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
background: url("/static/assets/modules/records/transparent/module-field-frame.png")
|
||||
center/100% 100% no-repeat;
|
||||
}
|
||||
.field-row > text:first-child,
|
||||
.detail-card > view > text:first-child {
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.field-row input,
|
||||
.detail-card > view > text:last-child {
|
||||
min-width: 0;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
text-align: right;
|
||||
}
|
||||
.field-row > text:last-child {
|
||||
grid-column: 1/-1;
|
||||
color: $brand-red;
|
||||
font-size: 20rpx;
|
||||
text-align: right;
|
||||
}
|
||||
.form-card .app-button,
|
||||
.detail-card .app-button {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.save-error {
|
||||
display: block;
|
||||
margin-top: 14rpx;
|
||||
color: $brand-red;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.state-card {
|
||||
min-height: 340rpx;
|
||||
padding-top: 80rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.state-card > text:nth-child(2) {
|
||||
display: block;
|
||||
margin-top: 18rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.state-card .app-button {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,181 @@
|
||||
<!-- 页面编号:R-05;用途:礼仪活动列表。 -->
|
||||
<template><ModulePage page-id="r05" /></template>
|
||||
<!-- 页面编号:R-05;用途:礼仪活动列表、状态与创建入口。 -->
|
||||
<template>
|
||||
<view class="ritual-page" :class="stateClasses">
|
||||
<ModulePageBackground module="records" />
|
||||
<view class="page-header">
|
||||
<PageHeader title="礼仪活动" action="新建" @action="createRitual" />
|
||||
</view>
|
||||
<view v-if="ritualState === 'loading'" class="page-loading">
|
||||
<AppLoading
|
||||
text="正在整理礼仪活动"
|
||||
description="请稍候,正在读取时间与地点。"
|
||||
/>
|
||||
</view>
|
||||
<view v-else class="page-content">
|
||||
<template v-if="ritualState === 'ready' && rituals.length">
|
||||
<view
|
||||
v-for="ritual in rituals"
|
||||
:key="ritual.id"
|
||||
class="record-card"
|
||||
@click="openRitual(ritual)"
|
||||
>
|
||||
<text>{{ ritual.status }}</text>
|
||||
<text>{{ ritual.name }}</text>
|
||||
<text>{{ ritual.date }} · {{ ritual.place }}</text>
|
||||
<text>查看活动详情</text>
|
||||
</view>
|
||||
<AppButton block label="新建礼仪" @click="createRitual" />
|
||||
</template>
|
||||
<view v-else class="state-card">
|
||||
<text>
|
||||
{{ ritualState === "error" ? "礼仪活动暂不可用" : "还没有礼仪活动" }}
|
||||
</text>
|
||||
<text>
|
||||
{{
|
||||
ritualState === "error"
|
||||
? "请稍后重新查看,已有活动不会受到影响。"
|
||||
: "从一次祭祖、家宴或团拜开始安排。"
|
||||
}}
|
||||
</text>
|
||||
<AppButton
|
||||
:type="ritualState === 'error' ? 'secondary' : 'primary'"
|
||||
block
|
||||
:label="ritualState === 'error' ? '重新查看' : '新建礼仪'"
|
||||
@click="ritualState === 'error' ? restoreRituals() : createRitual()"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
const base = [
|
||||
{
|
||||
id: "501",
|
||||
name: "清明祭祖",
|
||||
status: "报名中",
|
||||
date: "2025 年 4 月 4 日",
|
||||
place: "汤氏宗祠",
|
||||
},
|
||||
{
|
||||
id: "502",
|
||||
name: "中秋家宴",
|
||||
status: "筹备中",
|
||||
date: "2025 年 9 月 17 日",
|
||||
place: "祖居院落",
|
||||
},
|
||||
{
|
||||
id: "503",
|
||||
name: "新春团拜",
|
||||
status: "已结束",
|
||||
date: "2025 年 1 月 29 日",
|
||||
place: "家族礼堂",
|
||||
},
|
||||
];
|
||||
const rituals = ref([...base]);
|
||||
const ritualState = ref("loading");
|
||||
const stateClasses = computed(() => ({
|
||||
"ritual-state--loading": ritualState.value === "loading",
|
||||
"ritual-state--empty": ritualState.value === "empty",
|
||||
"ritual-state--error": ritualState.value === "error",
|
||||
}));
|
||||
onLoad((q) => {
|
||||
const n = Math.max(1, Math.min(Number(q.count) || base.length, 50));
|
||||
rituals.value = Array.from({ length: n }, (_, i) => ({
|
||||
...base[i % 3],
|
||||
id: String(501 + i),
|
||||
name: n > 3 ? `${base[i % 3].name}(${i + 1})` : base[i].name,
|
||||
}));
|
||||
ritualState.value = ["loading", "empty", "error"].includes(q.state)
|
||||
? q.state
|
||||
: "ready";
|
||||
});
|
||||
const openRitual = (r) =>
|
||||
uni.navigateTo({ url: `/pages/records/r06-ritual-detail?ritualId=${r.id}` });
|
||||
const createRitual = () =>
|
||||
uni.navigateTo({ url: "/pages/records/r07-ritual-editor?mode=create" });
|
||||
const restoreRituals = () => {
|
||||
ritualState.value = "ready";
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.ritual-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.page-header,
|
||||
.page-loading,
|
||||
.page-content {
|
||||
z-index: 1;
|
||||
}
|
||||
.page-loading {
|
||||
min-height: calc(100vh - 100rpx);
|
||||
}
|
||||
.page-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
padding: 18rpx 24rpx 72rpx;
|
||||
}
|
||||
.record-card,
|
||||
.state-card {
|
||||
box-sizing: border-box;
|
||||
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
|
||||
center/100% 100% no-repeat;
|
||||
}
|
||||
.record-card {
|
||||
min-height: 190rpx;
|
||||
padding: 32rpx 46rpx;
|
||||
}
|
||||
.record-card > text,
|
||||
.state-card > text {
|
||||
display: block;
|
||||
}
|
||||
.record-card > text:first-child {
|
||||
color: $brand-red;
|
||||
font-size: 21rpx;
|
||||
}
|
||||
.record-card > text:nth-child(2) {
|
||||
margin-top: 6rpx;
|
||||
color: $ink;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.record-card > text:nth-child(3) {
|
||||
margin-top: 9rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.record-card > text:last-child {
|
||||
margin-top: 9rpx;
|
||||
color: $brand-red;
|
||||
font-size: 21rpx;
|
||||
}
|
||||
.state-card {
|
||||
min-height: 340rpx;
|
||||
padding: 78rpx 52rpx 50rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.state-card > text:first-child {
|
||||
color: $ink;
|
||||
font-size: 35rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.state-card > text:nth-child(2) {
|
||||
margin-top: 18rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.state-card .app-button {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,222 @@
|
||||
<!-- 页面编号:R-06;用途:礼仪详情。 -->
|
||||
<template><ModulePage page-id="r06" /></template>
|
||||
<!-- 页面编号:R-06;用途:礼仪详情、参与者与受控状态。 -->
|
||||
<template>
|
||||
<view class="ritual-detail-page" :class="stateClasses">
|
||||
<ModulePageBackground module="records" />
|
||||
<view class="page-header">
|
||||
<PageHeader
|
||||
title="礼仪详情"
|
||||
:action="ritualState === 'ready' ? '编辑' : ''"
|
||||
@action="editRitual"
|
||||
/>
|
||||
</view>
|
||||
<view v-if="ritualState === 'loading'" class="page-loading">
|
||||
<AppLoading
|
||||
text="正在读取礼仪详情"
|
||||
description="请稍候,正在整理活动与参与信息。"
|
||||
/>
|
||||
</view>
|
||||
<view v-else class="page-content">
|
||||
<template v-if="ritualState === 'ready'">
|
||||
<view class="detail-card">
|
||||
<text>{{ ritualDetail.status }}</text>
|
||||
<text>{{ ritualDetail.name }}</text>
|
||||
<text>{{ ritualDetail.date }} · {{ ritualDetail.place }}</text>
|
||||
<text>{{ ritualDetail.description }}</text>
|
||||
</view>
|
||||
<view class="participant-card">
|
||||
<view>
|
||||
<text>参与家人</text>
|
||||
<text>{{ participants.length }} 人</text>
|
||||
</view>
|
||||
<view v-for="person in participants" :key="person.id">
|
||||
<text>{{ person.name }}</text>
|
||||
<text>{{ person.role }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<AppButton block label="编辑活动" @click="editRitual" />
|
||||
</template>
|
||||
<view v-else class="state-card">
|
||||
<text>{{ stateCopy.title }}</text>
|
||||
<text>{{ stateCopy.copy }}</text>
|
||||
<AppButton
|
||||
:type="ritualState === 'error' ? 'secondary' : 'primary'"
|
||||
block
|
||||
:label="ritualState === 'error' ? '重新查看' : '返回礼仪列表'"
|
||||
@click="ritualState === 'error' ? restoreRitual() : backToRituals()"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
const records = [
|
||||
{
|
||||
id: "501",
|
||||
name: "清明祭祖",
|
||||
status: "报名中",
|
||||
date: "2025 年 4 月 4 日",
|
||||
place: "汤氏宗祠",
|
||||
description: "缅怀先祖,整理祭扫礼序,并由长辈讲述家族往事。",
|
||||
},
|
||||
{
|
||||
id: "502",
|
||||
name: "中秋家宴",
|
||||
status: "筹备中",
|
||||
date: "2025 年 9 月 17 日",
|
||||
place: "祖居院落",
|
||||
description: "家人团聚,共叙近况并整理年度家族影像。",
|
||||
},
|
||||
];
|
||||
const ritualDetail = reactive({ ...records[0] });
|
||||
const ritualId = ref("501");
|
||||
const ritualState = ref("loading");
|
||||
const participants = ref([
|
||||
{ id: 1, name: "汤文正", role: "主理人" },
|
||||
{ id: 2, name: "汤淑华", role: "家族长辈" },
|
||||
{ id: 3, name: "汤文清", role: "影像记录" },
|
||||
]);
|
||||
const stateClasses = computed(() => ({
|
||||
"ritual-state--expired": ritualState.value === "expired",
|
||||
"ritual-state--privacy": ritualState.value === "privacy",
|
||||
"ritual-state--error": ritualState.value === "error",
|
||||
}));
|
||||
const stateCopy = computed(
|
||||
() =>
|
||||
({
|
||||
expired: {
|
||||
title: "活动已失效",
|
||||
copy: "这项礼仪活动已取消或结束归档,请返回列表查看其他活动。",
|
||||
},
|
||||
privacy: {
|
||||
title: "活动信息未公开",
|
||||
copy: "当前活动只向受邀家人展示,请返回礼仪列表。",
|
||||
},
|
||||
error: {
|
||||
title: "礼仪详情暂不可用",
|
||||
copy: "请稍后重新查看,已有活动不会受到影响。",
|
||||
},
|
||||
})[ritualState.value] || {},
|
||||
);
|
||||
onLoad((q) => {
|
||||
ritualId.value = String(q.ritualId || "501");
|
||||
const selected = records.find((x) => x.id === ritualId.value);
|
||||
if (selected) Object.assign(ritualDetail, selected);
|
||||
ritualState.value = ["loading", "expired", "privacy", "error"].includes(
|
||||
q.state,
|
||||
)
|
||||
? q.state
|
||||
: selected
|
||||
? "ready"
|
||||
: "expired";
|
||||
});
|
||||
const editRitual = () =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/records/r07-ritual-editor?mode=edit&ritualId=${ritualId.value}`,
|
||||
});
|
||||
const restoreRitual = () => {
|
||||
ritualState.value = "ready";
|
||||
};
|
||||
const backToRituals = () =>
|
||||
uni.redirectTo({ url: "/pages/records/r05-ritual-list" });
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.ritual-detail-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.page-header,
|
||||
.page-loading,
|
||||
.page-content {
|
||||
z-index: 1;
|
||||
}
|
||||
.page-loading {
|
||||
min-height: calc(100vh - 100rpx);
|
||||
}
|
||||
.page-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
padding: 18rpx 24rpx 72rpx;
|
||||
}
|
||||
.detail-card,
|
||||
.participant-card,
|
||||
.state-card {
|
||||
box-sizing: border-box;
|
||||
padding: 42rpx 46rpx;
|
||||
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
|
||||
center/100% 100% no-repeat;
|
||||
}
|
||||
.detail-card > text {
|
||||
display: block;
|
||||
}
|
||||
.detail-card > text:first-child {
|
||||
color: $brand-red;
|
||||
font-size: 21rpx;
|
||||
}
|
||||
.detail-card > text:nth-child(2) {
|
||||
margin-top: 8rpx;
|
||||
color: $ink;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.detail-card > text:nth-child(3) {
|
||||
margin-top: 10rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.detail-card > text:last-child {
|
||||
margin-top: 20rpx;
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.participant-card > view {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 8rpx 18rpx;
|
||||
min-height: 52rpx;
|
||||
align-items: center;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.participant-card > view:first-child {
|
||||
color: $brand-red;
|
||||
font-weight: 700;
|
||||
}
|
||||
.participant-card > view + view {
|
||||
margin-top: 9rpx;
|
||||
padding-top: 9rpx;
|
||||
border-top: 1px solid rgba(136, 84, 42, 0.18);
|
||||
}
|
||||
.state-card {
|
||||
min-height: 340rpx;
|
||||
padding-top: 78rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.state-card > text {
|
||||
display: block;
|
||||
}
|
||||
.state-card > text:first-child {
|
||||
color: $ink;
|
||||
font-size: 35rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.state-card > text:nth-child(2) {
|
||||
margin-top: 18rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.state-card .app-button {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,227 @@
|
||||
<!-- 页面编号:R-07;用途:新建与编辑礼仪。 -->
|
||||
<template><ModulePage page-id="r07" /></template>
|
||||
<!-- 页面编号:R-07;用途:礼仪创建、编辑、校验、保存与删除确认。 -->
|
||||
<template>
|
||||
<view class="ritual-editor-page" :class="editorClasses">
|
||||
<ModulePageBackground module="records" />
|
||||
<view class="page-header">
|
||||
<PageHeader :title="mode === 'create' ? '新建礼仪' : '编辑礼仪'" />
|
||||
</view>
|
||||
<view class="page-content">
|
||||
<view v-if="editorState === 'success'" class="state-card">
|
||||
<text>礼仪活动已保存</text>
|
||||
<text>时间、地点与活动说明已整理完成。</text>
|
||||
<AppButton block label="返回礼仪列表" @click="backToRituals" />
|
||||
</view>
|
||||
<view v-else class="form-card">
|
||||
<text>
|
||||
{{ mode === "create" ? "安排一次家族礼仪" : "修改活动信息" }}
|
||||
</text>
|
||||
<view v-for="field in fields" :key="field.key" class="field-row">
|
||||
<text>{{ field.label }}</text>
|
||||
<textarea
|
||||
v-if="field.long"
|
||||
v-model="ritualForm[field.key]"
|
||||
auto-height
|
||||
:placeholder="`请输入${field.label}`"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
v-model="ritualForm[field.key]"
|
||||
:placeholder="`请输入${field.label}`"
|
||||
/>
|
||||
<text v-if="ritualErrors[field.key]">
|
||||
{{ ritualErrors[field.key] }}
|
||||
</text>
|
||||
</view>
|
||||
<text v-if="editorState === 'error'" class="save-error">
|
||||
保存失败,请保留内容后重试。
|
||||
</text>
|
||||
<AppButton
|
||||
block
|
||||
:disabled="editorState === 'saving'"
|
||||
:label="editorState === 'saving' ? '正在保存' : '保存活动'"
|
||||
@click="saveRitual"
|
||||
/>
|
||||
<AppButton
|
||||
v-if="mode === 'edit'"
|
||||
type="secondary"
|
||||
block
|
||||
label="删除活动"
|
||||
@click="confirmDelete"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<AppDialog
|
||||
:visible="deleteVisible"
|
||||
eyebrow="删除确认"
|
||||
title="删除这项礼仪活动?"
|
||||
message="删除后将返回礼仪列表。"
|
||||
confirm-text="确认删除"
|
||||
cancel-text="保留活动"
|
||||
show-cancel
|
||||
@confirm="deleteRitual"
|
||||
@cancel="deleteVisible = false"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { computed, onUnmounted, reactive, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
const records = [
|
||||
{
|
||||
id: "501",
|
||||
name: "清明祭祖",
|
||||
date: "2025-04-04",
|
||||
place: "汤氏宗祠",
|
||||
description: "缅怀先祖,凝聚家人,共叙家风传承。",
|
||||
},
|
||||
];
|
||||
const ritualId = ref("");
|
||||
const mode = ref("create");
|
||||
const editorState = ref("ready");
|
||||
const deleteVisible = ref(false);
|
||||
const forceSaveFailure = ref(false);
|
||||
const ritualForm = reactive({ name: "", date: "", place: "", description: "" });
|
||||
const ritualErrors = reactive({
|
||||
name: "",
|
||||
date: "",
|
||||
place: "",
|
||||
description: "",
|
||||
});
|
||||
const fields = [
|
||||
{ key: "name", label: "活动名称" },
|
||||
{ key: "date", label: "活动日期" },
|
||||
{ key: "place", label: "举办地点" },
|
||||
{ key: "description", label: "活动说明", long: true },
|
||||
];
|
||||
let saveTimer = null;
|
||||
const editorClasses = computed(() => ({
|
||||
"ritual-editor-state--saving": editorState.value === "saving",
|
||||
"ritual-editor-state--error": editorState.value === "error",
|
||||
}));
|
||||
onLoad((q) => {
|
||||
ritualId.value = String(q.ritualId || "");
|
||||
mode.value = q.mode === "edit" ? "edit" : "create";
|
||||
forceSaveFailure.value = q.saveResult === "error";
|
||||
const selected = records.find((x) => x.id === ritualId.value);
|
||||
if (selected) Object.assign(ritualForm, selected);
|
||||
});
|
||||
const validateRitual = () => {
|
||||
for (const f of fields)
|
||||
ritualErrors[f.key] = String(ritualForm[f.key]).trim()
|
||||
? ""
|
||||
: `请填写${f.label}`;
|
||||
return fields.every((f) => !ritualErrors[f.key]);
|
||||
};
|
||||
const saveRitual = () => {
|
||||
if (editorState.value === "saving" || !validateRitual()) return;
|
||||
editorState.value = "saving";
|
||||
saveTimer = setTimeout(() => {
|
||||
editorState.value = forceSaveFailure.value ? "error" : "success";
|
||||
forceSaveFailure.value = false;
|
||||
}, 320);
|
||||
};
|
||||
const confirmDelete = () => {
|
||||
deleteVisible.value = true;
|
||||
};
|
||||
const deleteRitual = () => {
|
||||
deleteVisible.value = false;
|
||||
backToRituals();
|
||||
};
|
||||
const backToRituals = () =>
|
||||
uni.redirectTo({ url: "/pages/records/r05-ritual-list" });
|
||||
onUnmounted(() => {
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.ritual-editor-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.page-header,
|
||||
.page-content {
|
||||
z-index: 1;
|
||||
}
|
||||
.page-content {
|
||||
padding: 18rpx 24rpx 72rpx;
|
||||
}
|
||||
.form-card,
|
||||
.state-card {
|
||||
box-sizing: border-box;
|
||||
padding: 46rpx;
|
||||
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
|
||||
center/100% 100% no-repeat;
|
||||
}
|
||||
.form-card > text:first-child,
|
||||
.state-card > text:first-child {
|
||||
display: block;
|
||||
color: $ink;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.field-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 10rpx 18rpx;
|
||||
min-height: 82rpx;
|
||||
margin-top: 14rpx;
|
||||
padding: 14rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
background: url("/static/assets/modules/records/transparent/module-field-frame.png")
|
||||
center/100% 100% no-repeat;
|
||||
}
|
||||
.field-row > text:first-child {
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.field-row input,
|
||||
.field-row textarea {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
text-align: right;
|
||||
}
|
||||
.field-row textarea {
|
||||
min-height: 76rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.field-row > text:last-child {
|
||||
grid-column: 1/-1;
|
||||
color: $brand-red;
|
||||
font-size: 20rpx;
|
||||
text-align: right;
|
||||
}
|
||||
.form-card .app-button {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.save-error {
|
||||
display: block;
|
||||
margin-top: 14rpx;
|
||||
color: $brand-red;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.state-card {
|
||||
min-height: 340rpx;
|
||||
padding-top: 80rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.state-card > text:nth-child(2) {
|
||||
display: block;
|
||||
margin-top: 18rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.state-card .app-button {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,253 @@
|
||||
<!-- 页面编号:R-08;用途:成长日志。 -->
|
||||
<template><ModulePage page-id="r08" /></template>
|
||||
<!-- 页面编号:R-08;用途:人物成长日志、时间轴与同页新增。 -->
|
||||
<template>
|
||||
<view class="timeline-page" :class="stateClasses">
|
||||
<ModulePageBackground module="records" />
|
||||
<view class="page-header">
|
||||
<PageHeader title="成长日志" action="记录" @action="recordGrowth" />
|
||||
</view>
|
||||
<view v-if="timelineState === 'loading'" class="page-loading">
|
||||
<AppLoading
|
||||
text="正在读取成长日志"
|
||||
:description="`请稍候,正在整理${personName}的成长记录。`"
|
||||
/>
|
||||
</view>
|
||||
<view v-else class="page-content">
|
||||
<view class="person-lead">
|
||||
<text>{{ personName }}</text>
|
||||
<text>成长中的每一个瞬间</text>
|
||||
</view>
|
||||
<template v-if="timelineState === 'ready' && growthRecords.length">
|
||||
<view
|
||||
v-for="(record, index) in growthRecords"
|
||||
:key="record.id"
|
||||
class="timeline-card"
|
||||
>
|
||||
<text>第 {{ growthRecords.length - index }} 则</text>
|
||||
<text>{{ record.title }}</text>
|
||||
<text>{{ record.date }}</text>
|
||||
<text>{{ record.description }}</text>
|
||||
</view>
|
||||
<AppButton block label="记录成长" @click="recordGrowth" />
|
||||
</template>
|
||||
<view v-else class="state-card">
|
||||
<text>
|
||||
{{
|
||||
timelineState === "error" ? "成长日志暂不可用" : "还没有成长记录"
|
||||
}}
|
||||
</text>
|
||||
<text>
|
||||
{{
|
||||
timelineState === "error"
|
||||
? "请稍后重新查看,已有记录不会受到影响。"
|
||||
: "从第一次微笑、入园或毕业开始记录。"
|
||||
}}
|
||||
</text>
|
||||
<AppButton
|
||||
:type="timelineState === 'error' ? 'secondary' : 'primary'"
|
||||
block
|
||||
:label="timelineState === 'error' ? '重新查看' : '记录成长'"
|
||||
@click="
|
||||
timelineState === 'error'
|
||||
? (timelineState = 'ready')
|
||||
: recordGrowth()
|
||||
"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<AppDialog
|
||||
:visible="dialogVisible"
|
||||
eyebrow="成长日志"
|
||||
title="记录一个成长瞬间"
|
||||
confirm-text="保存记录"
|
||||
cancel-text="取消"
|
||||
show-cancel
|
||||
@confirm="saveGrowth"
|
||||
@cancel="dialogVisible = false"
|
||||
>
|
||||
<view class="dialog-form">
|
||||
<input v-model="growthForm.title" placeholder="事件名称" />
|
||||
<input v-model="growthForm.date" placeholder="日期" />
|
||||
<textarea
|
||||
v-model="growthForm.description"
|
||||
auto-height
|
||||
placeholder="写下当时的故事"
|
||||
/>
|
||||
<text v-if="formError">{{ formError }}</text>
|
||||
</view>
|
||||
</AppDialog>
|
||||
<AppToast :visible="toastVisible" message="成长记录已保存" />
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { computed, onUnmounted, reactive, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
const personName = ref("汤小满");
|
||||
const growthRecords = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: "第一次叫爸爸",
|
||||
date: "2024 年 3 月",
|
||||
description: "家人共同听见了这声清晰的呼唤。",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "入园第一天",
|
||||
date: "2024 年 9 月",
|
||||
description: "背着小书包,勇敢地向家人挥手。",
|
||||
},
|
||||
]);
|
||||
const timelineState = ref("loading");
|
||||
const dialogVisible = ref(false);
|
||||
const toastVisible = ref(false);
|
||||
const formError = ref("");
|
||||
const growthForm = reactive({ title: "", date: "", description: "" });
|
||||
let timer = null;
|
||||
const stateClasses = computed(() => ({
|
||||
"timeline-state--loading": timelineState.value === "loading",
|
||||
"timeline-state--empty": timelineState.value === "empty",
|
||||
"timeline-state--error": timelineState.value === "error",
|
||||
}));
|
||||
onLoad((q) => {
|
||||
personName.value = String(q.personName || "汤小满");
|
||||
timelineState.value = ["loading", "empty", "error"].includes(q.state)
|
||||
? q.state
|
||||
: "ready";
|
||||
});
|
||||
const recordGrowth = () => {
|
||||
Object.assign(growthForm, { title: "", date: "", description: "" });
|
||||
formError.value = "";
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
const saveGrowth = () => {
|
||||
if (!growthForm.title.trim() || !growthForm.date.trim()) {
|
||||
formError.value = "请填写事件名称和日期";
|
||||
return;
|
||||
}
|
||||
growthRecords.value.unshift({ id: Date.now(), ...growthForm });
|
||||
timelineState.value = "ready";
|
||||
dialogVisible.value = false;
|
||||
toastVisible.value = true;
|
||||
timer = setTimeout(() => (toastVisible.value = false), 1800);
|
||||
};
|
||||
onUnmounted(() => {
|
||||
if (timer) clearTimeout(timer);
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.timeline-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.page-header,
|
||||
.page-loading,
|
||||
.page-content {
|
||||
z-index: 1;
|
||||
}
|
||||
.page-loading {
|
||||
min-height: calc(100vh - 100rpx);
|
||||
}
|
||||
.page-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
padding: 18rpx 24rpx 72rpx;
|
||||
}
|
||||
.person-lead {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 8rpx 18rpx;
|
||||
min-height: 62rpx;
|
||||
align-items: center;
|
||||
padding: 0 20rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
background: url("/static/assets/modules/genealogy/transparent/section-divider.png")
|
||||
center/100% 100% no-repeat;
|
||||
}
|
||||
.person-lead text:first-child {
|
||||
color: $brand-red;
|
||||
font-weight: 700;
|
||||
}
|
||||
.timeline-card,
|
||||
.state-card {
|
||||
box-sizing: border-box;
|
||||
padding: 34rpx 46rpx;
|
||||
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
|
||||
center/100% 100% no-repeat;
|
||||
}
|
||||
.timeline-card > text,
|
||||
.state-card > text {
|
||||
display: block;
|
||||
}
|
||||
.timeline-card > text:first-child {
|
||||
color: $brand-red;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
.timeline-card > text:nth-child(2) {
|
||||
margin-top: 6rpx;
|
||||
color: $ink;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.timeline-card > text:nth-child(3) {
|
||||
margin-top: 8rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 21rpx;
|
||||
}
|
||||
.timeline-card > text:last-child {
|
||||
margin-top: 10rpx;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.state-card {
|
||||
min-height: 340rpx;
|
||||
padding-top: 78rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.state-card > text:first-child {
|
||||
color: $ink;
|
||||
font-size: 35rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.state-card > text:nth-child(2) {
|
||||
margin-top: 18rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.state-card .app-button {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
.dialog-form {
|
||||
width: 100%;
|
||||
margin: 18rpx 0;
|
||||
}
|
||||
.dialog-form input,
|
||||
.dialog-form textarea {
|
||||
width: 100%;
|
||||
min-height: 70rpx;
|
||||
margin-top: 10rpx;
|
||||
padding: 14rpx 20rpx;
|
||||
box-sizing: border-box;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
background: url("/static/assets/modules/records/transparent/module-field-frame.png")
|
||||
center/100% 100% no-repeat;
|
||||
}
|
||||
.dialog-form text {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
color: $brand-red;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,260 @@
|
||||
<!-- 页面编号:R-09;用途:人生事。 -->
|
||||
<template><ModulePage page-id="r09" /></template>
|
||||
<!-- 页面编号:R-09;用途:人物人生事时间轴与同页新增。 -->
|
||||
<template>
|
||||
<view class="timeline-page" :class="stateClasses">
|
||||
<ModulePageBackground module="records" />
|
||||
<view class="page-header">
|
||||
<PageHeader title="人生事" action="新增" @action="createLifeEvent" />
|
||||
</view>
|
||||
<view v-if="timelineState === 'loading'" class="page-loading">
|
||||
<AppLoading
|
||||
text="正在读取人生事"
|
||||
:description="`请稍候,正在整理${personName}的重要节点。`"
|
||||
/>
|
||||
</view>
|
||||
<view v-else class="page-content">
|
||||
<view class="person-lead">
|
||||
<text>{{ personName }}</text>
|
||||
<text>值得回望的人生节点</text>
|
||||
</view>
|
||||
<template v-if="timelineState === 'ready' && lifeEvents.length">
|
||||
<view v-for="event in lifeEvents" :key="event.id" class="timeline-card">
|
||||
<text>{{ event.year }}</text>
|
||||
<text>{{ event.title }}</text>
|
||||
<text>{{ event.place }}</text>
|
||||
<text>{{ event.description }}</text>
|
||||
</view>
|
||||
<AppButton block label="新增人生事" @click="createLifeEvent" />
|
||||
</template>
|
||||
<view v-else class="state-card">
|
||||
<text>
|
||||
{{ timelineState === "error" ? "人生事暂不可用" : "还没有人生事" }}
|
||||
</text>
|
||||
<text>
|
||||
{{
|
||||
timelineState === "error"
|
||||
? "请稍后重新查看,已有记录不会受到影响。"
|
||||
: "从毕业、成家或重要迁居开始记录。"
|
||||
}}
|
||||
</text>
|
||||
<AppButton
|
||||
:type="timelineState === 'error' ? 'secondary' : 'primary'"
|
||||
block
|
||||
:label="timelineState === 'error' ? '重新查看' : '新增人生事'"
|
||||
@click="
|
||||
timelineState === 'error'
|
||||
? (timelineState = 'ready')
|
||||
: createLifeEvent()
|
||||
"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<AppDialog
|
||||
:visible="dialogVisible"
|
||||
eyebrow="人生事"
|
||||
title="记录一个人生节点"
|
||||
confirm-text="保存记录"
|
||||
cancel-text="取消"
|
||||
show-cancel
|
||||
@confirm="saveLifeEvent"
|
||||
@cancel="dialogVisible = false"
|
||||
>
|
||||
<view class="dialog-form">
|
||||
<input v-model="lifeEventForm.title" placeholder="事件名称" />
|
||||
<input v-model="lifeEventForm.year" placeholder="年份或日期" />
|
||||
<input v-model="lifeEventForm.place" placeholder="地点(选填)" />
|
||||
<textarea
|
||||
v-model="lifeEventForm.description"
|
||||
auto-height
|
||||
placeholder="写下这段经历"
|
||||
/>
|
||||
<text v-if="formError">{{ formError }}</text>
|
||||
</view>
|
||||
</AppDialog>
|
||||
<AppToast :visible="toastVisible" message="人生事已保存" />
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { computed, onUnmounted, reactive, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
const personName = ref("汤文清");
|
||||
const lifeEvents = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: "大学毕业",
|
||||
year: "2018 年 6 月",
|
||||
place: "杭州",
|
||||
description: "完成学业,带着家人的祝福走向新的生活。",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "结为连理",
|
||||
year: "2022 年 10 月",
|
||||
place: "汤氏祖居",
|
||||
description: "在家人见证下组成新的家庭。",
|
||||
},
|
||||
]);
|
||||
const timelineState = ref("loading");
|
||||
const dialogVisible = ref(false);
|
||||
const toastVisible = ref(false);
|
||||
const formError = ref("");
|
||||
const lifeEventForm = reactive({
|
||||
title: "",
|
||||
year: "",
|
||||
place: "",
|
||||
description: "",
|
||||
});
|
||||
let timer = null;
|
||||
const stateClasses = computed(() => ({
|
||||
"timeline-state--loading": timelineState.value === "loading",
|
||||
"timeline-state--empty": timelineState.value === "empty",
|
||||
"timeline-state--error": timelineState.value === "error",
|
||||
}));
|
||||
onLoad((q) => {
|
||||
personName.value = String(q.personName || "汤文清");
|
||||
timelineState.value = ["loading", "empty", "error"].includes(q.state)
|
||||
? q.state
|
||||
: "ready";
|
||||
});
|
||||
const createLifeEvent = () => {
|
||||
Object.assign(lifeEventForm, {
|
||||
title: "",
|
||||
year: "",
|
||||
place: "",
|
||||
description: "",
|
||||
});
|
||||
formError.value = "";
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
const saveLifeEvent = () => {
|
||||
if (!lifeEventForm.title.trim() || !lifeEventForm.year.trim()) {
|
||||
formError.value = "请填写事件名称和年份";
|
||||
return;
|
||||
}
|
||||
lifeEvents.value.unshift({ id: Date.now(), ...lifeEventForm });
|
||||
timelineState.value = "ready";
|
||||
dialogVisible.value = false;
|
||||
toastVisible.value = true;
|
||||
timer = setTimeout(() => (toastVisible.value = false), 1800);
|
||||
};
|
||||
onUnmounted(() => {
|
||||
if (timer) clearTimeout(timer);
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.timeline-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.page-header,
|
||||
.page-loading,
|
||||
.page-content {
|
||||
z-index: 1;
|
||||
}
|
||||
.page-loading {
|
||||
min-height: calc(100vh - 100rpx);
|
||||
}
|
||||
.page-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
padding: 18rpx 24rpx 72rpx;
|
||||
}
|
||||
.person-lead {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 8rpx 18rpx;
|
||||
min-height: 62rpx;
|
||||
align-items: center;
|
||||
padding: 0 20rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
background: url("/static/assets/modules/genealogy/transparent/section-divider.png")
|
||||
center/100% 100% no-repeat;
|
||||
}
|
||||
.person-lead text:first-child {
|
||||
color: $brand-red;
|
||||
font-weight: 700;
|
||||
}
|
||||
.timeline-card,
|
||||
.state-card {
|
||||
box-sizing: border-box;
|
||||
padding: 34rpx 46rpx;
|
||||
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
|
||||
center/100% 100% no-repeat;
|
||||
}
|
||||
.timeline-card > text,
|
||||
.state-card > text {
|
||||
display: block;
|
||||
}
|
||||
.timeline-card > text:first-child {
|
||||
color: $brand-red;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
.timeline-card > text:nth-child(2) {
|
||||
margin-top: 6rpx;
|
||||
color: $ink;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.timeline-card > text:nth-child(3) {
|
||||
margin-top: 8rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 21rpx;
|
||||
}
|
||||
.timeline-card > text:last-child {
|
||||
margin-top: 10rpx;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.state-card {
|
||||
min-height: 340rpx;
|
||||
padding-top: 78rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.state-card > text:first-child {
|
||||
color: $ink;
|
||||
font-size: 35rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.state-card > text:nth-child(2) {
|
||||
margin-top: 18rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.state-card .app-button {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
.dialog-form {
|
||||
width: 100%;
|
||||
margin: 18rpx 0;
|
||||
}
|
||||
.dialog-form input,
|
||||
.dialog-form textarea {
|
||||
width: 100%;
|
||||
min-height: 66rpx;
|
||||
margin-top: 8rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
box-sizing: border-box;
|
||||
color: $ink;
|
||||
font-size: 22rpx;
|
||||
background: url("/static/assets/modules/records/transparent/module-field-frame.png")
|
||||
center/100% 100% no-repeat;
|
||||
}
|
||||
.dialog-form text {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
color: $brand-red;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,246 @@
|
||||
<!-- 页面编号:R-10;用途:家族备忘录。 -->
|
||||
<template><ModulePage page-id="r10" /></template>
|
||||
<!-- 页面编号:R-10;用途:家族备忘列表、完成状态与同页新增。 -->
|
||||
<template>
|
||||
<view class="memo-page" :class="stateClasses">
|
||||
<ModulePageBackground module="records" />
|
||||
<view class="page-header">
|
||||
<PageHeader title="家族备忘" action="新增" @action="createMemo" />
|
||||
</view>
|
||||
<view v-if="memoState === 'loading'" class="page-loading">
|
||||
<AppLoading
|
||||
text="正在读取家族备忘"
|
||||
description="请稍候,正在整理待办事项。"
|
||||
/>
|
||||
</view>
|
||||
<view v-else class="page-content">
|
||||
<template v-if="memoState === 'ready' && memos.length">
|
||||
<view
|
||||
v-for="memo in memos"
|
||||
:key="memo.id"
|
||||
class="memo-card"
|
||||
:class="{ 'memo-card--done': memo.done }"
|
||||
@click="toggleMemo(memo)"
|
||||
>
|
||||
<view>
|
||||
<text>{{ memo.done ? "已完成" : "待办理" }}</text>
|
||||
<text>{{ memo.due }}</text>
|
||||
</view>
|
||||
<text>{{ memo.title }}</text>
|
||||
<text>{{ memo.description }}</text>
|
||||
<text>{{ memo.done ? "点击恢复待办" : "点击标记完成" }}</text>
|
||||
</view>
|
||||
<AppButton block label="新增备忘" @click="createMemo" />
|
||||
</template>
|
||||
<view v-else class="state-card">
|
||||
<text>
|
||||
{{ memoState === "error" ? "家族备忘暂不可用" : "还没有备忘" }}
|
||||
</text>
|
||||
<text>
|
||||
{{
|
||||
memoState === "error"
|
||||
? "请稍后重新查看,已有备忘不会受到影响。"
|
||||
: "把需要家人共同记住的事情写在这里。"
|
||||
}}
|
||||
</text>
|
||||
<AppButton
|
||||
:type="memoState === 'error' ? 'secondary' : 'primary'"
|
||||
block
|
||||
:label="memoState === 'error' ? '重新查看' : '新增备忘'"
|
||||
@click="memoState === 'error' ? (memoState = 'ready') : createMemo()"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<AppDialog
|
||||
:visible="dialogVisible"
|
||||
eyebrow="家族备忘"
|
||||
title="新增一项备忘"
|
||||
confirm-text="保存备忘"
|
||||
cancel-text="取消"
|
||||
show-cancel
|
||||
@confirm="saveMemo"
|
||||
@cancel="dialogVisible = false"
|
||||
>
|
||||
<view class="dialog-form">
|
||||
<input v-model="memoForm.title" placeholder="备忘标题" />
|
||||
<input v-model="memoForm.due" placeholder="截止日期或时间" />
|
||||
<textarea
|
||||
v-model="memoForm.description"
|
||||
auto-height
|
||||
placeholder="补充具体事项"
|
||||
/>
|
||||
<text v-if="formError">{{ formError }}</text>
|
||||
</view>
|
||||
</AppDialog>
|
||||
<AppToast :visible="toastVisible" message="备忘已更新" />
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { computed, onUnmounted, reactive, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
const memos = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: "修谱资料整理",
|
||||
due: "本月底前",
|
||||
description: "补充老照片中的人物姓名和拍摄时间。",
|
||||
done: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "重阳敬老活动",
|
||||
due: "10 月 11 日上午",
|
||||
description: "在祠堂集合,并确认接送长辈的车辆。",
|
||||
done: true,
|
||||
},
|
||||
]);
|
||||
const memoState = ref("loading");
|
||||
const dialogVisible = ref(false);
|
||||
const toastVisible = ref(false);
|
||||
const formError = ref("");
|
||||
const memoForm = reactive({ title: "", due: "", description: "" });
|
||||
let timer = null;
|
||||
const stateClasses = computed(() => ({
|
||||
"memo-state--loading": memoState.value === "loading",
|
||||
"memo-state--empty": memoState.value === "empty",
|
||||
"memo-state--error": memoState.value === "error",
|
||||
}));
|
||||
onLoad((q) => {
|
||||
memoState.value = ["loading", "empty", "error"].includes(q.state)
|
||||
? q.state
|
||||
: "ready";
|
||||
});
|
||||
const showToast = () => {
|
||||
toastVisible.value = true;
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => (toastVisible.value = false), 1800);
|
||||
};
|
||||
const toggleMemo = (memo) => {
|
||||
memo.done = !memo.done;
|
||||
showToast();
|
||||
};
|
||||
const createMemo = () => {
|
||||
Object.assign(memoForm, { title: "", due: "", description: "" });
|
||||
formError.value = "";
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
const saveMemo = () => {
|
||||
if (!memoForm.title.trim()) {
|
||||
formError.value = "请填写备忘标题";
|
||||
return;
|
||||
}
|
||||
memos.value.unshift({ id: Date.now(), ...memoForm, done: false });
|
||||
memoState.value = "ready";
|
||||
dialogVisible.value = false;
|
||||
showToast();
|
||||
};
|
||||
onUnmounted(() => {
|
||||
if (timer) clearTimeout(timer);
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.memo-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.page-header,
|
||||
.page-loading,
|
||||
.page-content {
|
||||
z-index: 1;
|
||||
}
|
||||
.page-loading {
|
||||
min-height: calc(100vh - 100rpx);
|
||||
}
|
||||
.page-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
padding: 18rpx 24rpx 72rpx;
|
||||
}
|
||||
.memo-card,
|
||||
.state-card {
|
||||
box-sizing: border-box;
|
||||
padding: 34rpx 46rpx;
|
||||
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
|
||||
center/100% 100% no-repeat;
|
||||
}
|
||||
.memo-card > view {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 6rpx 18rpx;
|
||||
color: $brand-red;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
.memo-card > text,
|
||||
.state-card > text {
|
||||
display: block;
|
||||
}
|
||||
.memo-card > text:nth-child(2) {
|
||||
margin-top: 8rpx;
|
||||
color: $ink;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.memo-card > text:nth-child(3) {
|
||||
margin-top: 9rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.memo-card > text:last-child {
|
||||
margin-top: 10rpx;
|
||||
color: $brand-red;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
.memo-card--done {
|
||||
opacity: 0.68;
|
||||
}
|
||||
.state-card {
|
||||
min-height: 340rpx;
|
||||
padding-top: 78rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.state-card > text:first-child {
|
||||
color: $ink;
|
||||
font-size: 35rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.state-card > text:nth-child(2) {
|
||||
margin-top: 18rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.state-card .app-button {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
.dialog-form {
|
||||
width: 100%;
|
||||
margin: 18rpx 0;
|
||||
}
|
||||
.dialog-form input,
|
||||
.dialog-form textarea {
|
||||
width: 100%;
|
||||
min-height: 68rpx;
|
||||
margin-top: 9rpx;
|
||||
padding: 13rpx 20rpx;
|
||||
box-sizing: border-box;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
background: url("/static/assets/modules/records/transparent/module-field-frame.png")
|
||||
center/100% 100% no-repeat;
|
||||
}
|
||||
.dialog-form text {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
color: $brand-red;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,266 @@
|
||||
<!-- 页面编号:R-11;用途:功德记录。 -->
|
||||
<template><ModulePage page-id="r11" /></template>
|
||||
<!-- 页面编号:R-11;用途:功德记录、贡献汇总与同页新增。 -->
|
||||
<template>
|
||||
<view class="merit-page" :class="stateClasses">
|
||||
<ModulePageBackground module="records" />
|
||||
<view class="page-header">
|
||||
<PageHeader title="功德记录" action="新增" @action="createMerit" />
|
||||
</view>
|
||||
<view v-if="meritState === 'loading'" class="page-loading">
|
||||
<AppLoading
|
||||
text="正在整理功德记录"
|
||||
description="请稍候,正在读取家人对家族事务的支持。"
|
||||
/>
|
||||
</view>
|
||||
<view v-else class="page-content">
|
||||
<view v-if="meritState === 'ready'" class="merit-summary">
|
||||
<text>共同贡献</text>
|
||||
<text>{{ totalContribution }} 次</text>
|
||||
<text>每一次时间、物资与心力的付出都值得被记住</text>
|
||||
</view>
|
||||
<template v-if="meritState === 'ready' && meritRecords.length">
|
||||
<view v-for="merit in meritRecords" :key="merit.id" class="merit-card">
|
||||
<text>{{ merit.category }}</text>
|
||||
<text>{{ merit.title }}</text>
|
||||
<text>{{ merit.contributor }} · {{ merit.date }}</text>
|
||||
<text>{{ merit.description }}</text>
|
||||
</view>
|
||||
<AppButton block label="新增功德记录" @click="createMerit" />
|
||||
</template>
|
||||
<view v-else class="state-card">
|
||||
<text>
|
||||
{{ meritState === "error" ? "功德记录暂不可用" : "还没有功德记录" }}
|
||||
</text>
|
||||
<text>
|
||||
{{
|
||||
meritState === "error"
|
||||
? "请稍后重新查看,已有记录不会受到影响。"
|
||||
: "记录第一份对家族事务的时间、物资或心力支持。"
|
||||
}}
|
||||
</text>
|
||||
<AppButton
|
||||
:type="meritState === 'error' ? 'secondary' : 'primary'"
|
||||
block
|
||||
:label="meritState === 'error' ? '重新查看' : '新增记录'"
|
||||
@click="
|
||||
meritState === 'error' ? (meritState = 'ready') : createMerit()
|
||||
"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<AppDialog
|
||||
:visible="dialogVisible"
|
||||
eyebrow="功德记录"
|
||||
title="记下一份家族贡献"
|
||||
confirm-text="保存记录"
|
||||
cancel-text="取消"
|
||||
show-cancel
|
||||
@confirm="saveMerit"
|
||||
@cancel="dialogVisible = false"
|
||||
>
|
||||
<view class="dialog-form">
|
||||
<input v-model="meritForm.title" placeholder="贡献事项" />
|
||||
<input v-model="meritForm.contributor" placeholder="贡献人" />
|
||||
<input v-model="meritForm.date" placeholder="日期" />
|
||||
<textarea
|
||||
v-model="meritForm.description"
|
||||
auto-height
|
||||
placeholder="说明时间、物资或具体帮助"
|
||||
/>
|
||||
<text v-if="formError">{{ formError }}</text>
|
||||
</view>
|
||||
</AppDialog>
|
||||
<AppToast :visible="toastVisible" message="功德记录已保存" />
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { computed, onUnmounted, reactive, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
const meritRecords = ref([
|
||||
{
|
||||
id: 1,
|
||||
category: "共同修缮",
|
||||
title: "修缮祠堂",
|
||||
contributor: "汤氏家人共同参与",
|
||||
date: "2024 年春",
|
||||
description: "协助整理院落、修补门窗并登记旧物。",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category: "奖学助学",
|
||||
title: "支持后辈勤学",
|
||||
contributor: "家族教育小组",
|
||||
date: "2024 年夏",
|
||||
description: "为家族中努力求学的孩子提供书籍与经验分享。",
|
||||
},
|
||||
]);
|
||||
const totalContribution = computed(() => meritRecords.value.length);
|
||||
const meritState = ref("loading");
|
||||
const dialogVisible = ref(false);
|
||||
const toastVisible = ref(false);
|
||||
const formError = ref("");
|
||||
const meritForm = reactive({
|
||||
title: "",
|
||||
contributor: "",
|
||||
date: "",
|
||||
description: "",
|
||||
category: "家族贡献",
|
||||
});
|
||||
let timer = null;
|
||||
const stateClasses = computed(() => ({
|
||||
"merit-state--loading": meritState.value === "loading",
|
||||
"merit-state--empty": meritState.value === "empty",
|
||||
"merit-state--error": meritState.value === "error",
|
||||
}));
|
||||
onLoad((q) => {
|
||||
meritState.value = ["loading", "empty", "error"].includes(q.state)
|
||||
? q.state
|
||||
: "ready";
|
||||
});
|
||||
const createMerit = () => {
|
||||
Object.assign(meritForm, {
|
||||
title: "",
|
||||
contributor: "",
|
||||
date: "",
|
||||
description: "",
|
||||
category: "家族贡献",
|
||||
});
|
||||
formError.value = "";
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
const saveMerit = () => {
|
||||
if (!meritForm.title.trim() || !meritForm.contributor.trim()) {
|
||||
formError.value = "请填写贡献事项和贡献人";
|
||||
return;
|
||||
}
|
||||
meritRecords.value.unshift({ id: Date.now(), ...meritForm });
|
||||
meritState.value = "ready";
|
||||
dialogVisible.value = false;
|
||||
toastVisible.value = true;
|
||||
timer = setTimeout(() => (toastVisible.value = false), 1800);
|
||||
};
|
||||
onUnmounted(() => {
|
||||
if (timer) clearTimeout(timer);
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.merit-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.page-header,
|
||||
.page-loading,
|
||||
.page-content {
|
||||
z-index: 1;
|
||||
}
|
||||
.page-loading {
|
||||
min-height: calc(100vh - 100rpx);
|
||||
}
|
||||
.page-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
padding: 18rpx 24rpx 72rpx;
|
||||
}
|
||||
.merit-summary,
|
||||
.merit-card,
|
||||
.state-card {
|
||||
box-sizing: border-box;
|
||||
padding: 34rpx 46rpx;
|
||||
background: url("/static/assets/modules/records/transparent/module-content-frame.png")
|
||||
center/100% 100% no-repeat;
|
||||
}
|
||||
.merit-summary {
|
||||
text-align: center;
|
||||
}
|
||||
.merit-summary > text,
|
||||
.merit-card > text,
|
||||
.state-card > text {
|
||||
display: block;
|
||||
}
|
||||
.merit-summary > text:first-child {
|
||||
color: $brand-red;
|
||||
font-size: 21rpx;
|
||||
}
|
||||
.merit-summary > text:nth-child(2) {
|
||||
margin-top: 5rpx;
|
||||
color: $ink;
|
||||
font-size: 38rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.merit-summary > text:last-child {
|
||||
margin-top: 9rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.merit-card > text:first-child {
|
||||
color: $brand-red;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
.merit-card > text:nth-child(2) {
|
||||
margin-top: 6rpx;
|
||||
color: $ink;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.merit-card > text:nth-child(3) {
|
||||
margin-top: 8rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 21rpx;
|
||||
}
|
||||
.merit-card > text:last-child {
|
||||
margin-top: 9rpx;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.state-card {
|
||||
min-height: 340rpx;
|
||||
padding-top: 78rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.state-card > text:first-child {
|
||||
color: $ink;
|
||||
font-size: 35rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.state-card > text:nth-child(2) {
|
||||
margin-top: 18rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.state-card .app-button {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
.dialog-form {
|
||||
width: 100%;
|
||||
margin: 14rpx 0;
|
||||
}
|
||||
.dialog-form input,
|
||||
.dialog-form textarea {
|
||||
width: 100%;
|
||||
min-height: 62rpx;
|
||||
margin-top: 7rpx;
|
||||
padding: 11rpx 18rpx;
|
||||
box-sizing: border-box;
|
||||
color: $ink;
|
||||
font-size: 22rpx;
|
||||
background: url("/static/assets/modules/records/transparent/module-field-frame.png")
|
||||
center/100% 100% no-repeat;
|
||||
}
|
||||
.dialog-form text {
|
||||
display: block;
|
||||
margin-top: 7rpx;
|
||||
color: $brand-red;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
+220
-157
@@ -25,13 +25,20 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tree-stage">
|
||||
<view v-if="treeState === 'tree'" class="generation-rail">
|
||||
<view
|
||||
class="tree-stage"
|
||||
:class="{ 'tree-stage--lineage': treeState === 'tree' }"
|
||||
>
|
||||
<view
|
||||
v-if="treeState === 'tree'"
|
||||
class="generation-rail"
|
||||
:style="generationRailStyle"
|
||||
>
|
||||
<view
|
||||
v-for="row in generationRows"
|
||||
:key="row.generation"
|
||||
class="generation-band"
|
||||
:style="generationRowStyle(row)"
|
||||
:style="generationBandStyle(row)"
|
||||
>
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/section-divider.png"
|
||||
@@ -50,10 +57,12 @@
|
||||
scroll-x
|
||||
:scroll-left="initialScrollLeft"
|
||||
:show-scrollbar="false"
|
||||
:style="treeScrollStyle"
|
||||
>
|
||||
<view
|
||||
class="tree-canvas"
|
||||
:class="{ 'tree-canvas--state': treeState !== 'tree' }"
|
||||
:style="treeMetricsStyle"
|
||||
>
|
||||
<AppLoading
|
||||
v-if="treeState === 'loading'"
|
||||
@@ -66,24 +75,19 @@
|
||||
<text>同代分支可左右查看</text>
|
||||
</view>
|
||||
|
||||
<view class="lineage-connector lineage-connector--root" />
|
||||
<view class="lineage-connector lineage-connector--root-branch" />
|
||||
<view class="lineage-connector lineage-connector--gen13-left" />
|
||||
<view class="lineage-connector lineage-connector--gen13-right" />
|
||||
<view class="lineage-connector lineage-connector--left-trunk" />
|
||||
<view class="lineage-connector lineage-connector--left-branch" />
|
||||
<view class="lineage-connector lineage-connector--gen14-left" />
|
||||
<view class="lineage-connector lineage-connector--gen14-middle" />
|
||||
<view class="lineage-connector lineage-connector--right-trunk" />
|
||||
<view class="lineage-connector lineage-connector--right-branch" />
|
||||
<view class="lineage-connector lineage-connector--gen14-right" />
|
||||
<view
|
||||
v-for="connector in lineageConnectors"
|
||||
:key="connector.id"
|
||||
class="lineage-connector"
|
||||
:style="connector.style"
|
||||
/>
|
||||
|
||||
<view
|
||||
v-for="member in members"
|
||||
v-for="member in layoutMembers"
|
||||
:key="member.id"
|
||||
class="member-node"
|
||||
:class="{ 'member-node--selected': selected?.id === member.id }"
|
||||
:style="nodeStyle(member)"
|
||||
:style="nodeGridStyle(member)"
|
||||
@click="selected = member"
|
||||
>
|
||||
<image
|
||||
@@ -95,11 +99,13 @@
|
||||
"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text class="node-name">{{ member.name }}</text>
|
||||
<text class="node-relation"
|
||||
>{{ member.relation }} · {{ member.branch }}</text
|
||||
>
|
||||
<text class="node-years">{{ member.years }}</text>
|
||||
<view class="member-node__copy">
|
||||
<text class="node-name">{{ member.name }}</text>
|
||||
<text class="node-relation"
|
||||
>{{ member.relation }} · {{ member.branch }}</text
|
||||
>
|
||||
<text class="node-years">{{ member.years }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -171,11 +177,15 @@ const genealogyId = ref("");
|
||||
const treeState = ref("loading");
|
||||
const selected = ref(null);
|
||||
const initialScrollLeft = ref(90);
|
||||
const generationRows = [
|
||||
{ generation: 12, label: "第十二世", summary: "始祖", y: 135 },
|
||||
{ generation: 13, label: "第十三世", summary: "两房", y: 350 },
|
||||
{ generation: 14, label: "第十四世", summary: "三支", y: 650 },
|
||||
];
|
||||
const GRID_UNIT = 5;
|
||||
const NODE_HALF_HEIGHT = 47;
|
||||
const MEMBER_GAP = 250;
|
||||
const GENERATION_GAP = 220;
|
||||
const generationMeta = {
|
||||
12: { label: "第十二世", summary: "始祖" },
|
||||
13: { label: "第十三世", summary: "两房" },
|
||||
14: { label: "第十四世", summary: "三支" },
|
||||
};
|
||||
const members = ref([
|
||||
{
|
||||
id: 101,
|
||||
@@ -184,8 +194,6 @@ const members = ref([
|
||||
years: "1940—2012",
|
||||
generation: 12,
|
||||
branch: "主支",
|
||||
x: 450,
|
||||
y: 135,
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
@@ -193,9 +201,8 @@ const members = ref([
|
||||
relation: "长子",
|
||||
years: "1965—",
|
||||
generation: 13,
|
||||
parentId: 101,
|
||||
branch: "长房",
|
||||
x: 275,
|
||||
y: 350,
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
@@ -203,9 +210,8 @@ const members = ref([
|
||||
relation: "次子",
|
||||
years: "1968—",
|
||||
generation: 13,
|
||||
parentId: 101,
|
||||
branch: "二房",
|
||||
x: 625,
|
||||
y: 350,
|
||||
},
|
||||
{
|
||||
id: 104,
|
||||
@@ -213,9 +219,8 @@ const members = ref([
|
||||
relation: "长孙",
|
||||
years: "1992—",
|
||||
generation: 14,
|
||||
parentId: 102,
|
||||
branch: "长房",
|
||||
x: 150,
|
||||
y: 650,
|
||||
},
|
||||
{
|
||||
id: 105,
|
||||
@@ -223,9 +228,8 @@ const members = ref([
|
||||
relation: "长孙女",
|
||||
years: "1995—",
|
||||
generation: 14,
|
||||
parentId: 102,
|
||||
branch: "长房",
|
||||
x: 400,
|
||||
y: 650,
|
||||
},
|
||||
{
|
||||
id: 106,
|
||||
@@ -233,12 +237,132 @@ const members = ref([
|
||||
relation: "次孙",
|
||||
years: "1998—",
|
||||
generation: 14,
|
||||
parentId: 103,
|
||||
branch: "二房",
|
||||
x: 690,
|
||||
y: 650,
|
||||
},
|
||||
]);
|
||||
|
||||
const snapToGrid = (value) => Math.ceil(value / GRID_UNIT) * GRID_UNIT;
|
||||
const layoutMembers = computed(() => {
|
||||
const generations = Array.from(
|
||||
new Set(members.value.map((member) => Number(member.generation))),
|
||||
).sort((left, right) => left - right);
|
||||
const groups = new Map(
|
||||
generations.map((generation) => [
|
||||
generation,
|
||||
members.value
|
||||
.filter((member) => Number(member.generation) === generation)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
Number(left.parentId || 0) - Number(right.parentId || 0) ||
|
||||
Number(left.id) - Number(right.id),
|
||||
),
|
||||
]),
|
||||
);
|
||||
const maxCount = Math.max(1, ...Array.from(groups.values()).map((group) => group.length));
|
||||
const canvasWidth = Math.max(720, maxCount * MEMBER_GAP + 100);
|
||||
return generations.flatMap((generation, generationIndex) => {
|
||||
const group = groups.get(generation) || [];
|
||||
const occupiedWidth = Math.max(0, (group.length - 1) * MEMBER_GAP);
|
||||
const startX = (canvasWidth - occupiedWidth) / 2;
|
||||
return group.map((member, memberIndex) => ({
|
||||
...member,
|
||||
x: snapToGrid(startX + memberIndex * MEMBER_GAP),
|
||||
y: snapToGrid(135 + generationIndex * GENERATION_GAP),
|
||||
}));
|
||||
});
|
||||
});
|
||||
const treeMetrics = computed(() => {
|
||||
const memberList = layoutMembers.value;
|
||||
const maxX = Math.max(0, ...memberList.map((member) => member.x));
|
||||
const maxY = Math.max(0, ...memberList.map((member) => member.y));
|
||||
const width = Math.max(720, snapToGrid(maxX + 210));
|
||||
const height = Math.max(640, snapToGrid(maxY + 250));
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
columns: width / GRID_UNIT,
|
||||
rows: height / GRID_UNIT,
|
||||
};
|
||||
});
|
||||
const treeMetricsStyle = computed(() => ({
|
||||
width: `${treeMetrics.value.width}rpx`,
|
||||
height: `${treeMetrics.value.height}rpx`,
|
||||
gridTemplateColumns: `repeat(${treeMetrics.value.columns}, ${GRID_UNIT}rpx)`,
|
||||
gridTemplateRows: `repeat(${treeMetrics.value.rows}, ${GRID_UNIT}rpx)`,
|
||||
}));
|
||||
const generationRailStyle = computed(() => ({
|
||||
height: `${treeMetrics.value.height}rpx`,
|
||||
gridTemplateRows: `repeat(${treeMetrics.value.rows}, ${GRID_UNIT}rpx)`,
|
||||
}));
|
||||
const treeScrollStyle = computed(() => ({
|
||||
height: `${treeMetrics.value.height}rpx`,
|
||||
}));
|
||||
const generationRows = computed(() => {
|
||||
const groups = new Map();
|
||||
layoutMembers.value.forEach((member) => {
|
||||
const group = groups.get(member.generation) || [];
|
||||
group.push(member);
|
||||
groups.set(member.generation, group);
|
||||
});
|
||||
return Array.from(groups.entries())
|
||||
.sort(([left], [right]) => left - right)
|
||||
.map(([generation, group]) => {
|
||||
const y = Math.min(...group.map((member) => member.y));
|
||||
return {
|
||||
generation,
|
||||
label: generationMeta[generation]?.label || `第 ${generation} 世`,
|
||||
summary: generationMeta[generation]?.summary || `${group.length} 位成员`,
|
||||
y,
|
||||
};
|
||||
});
|
||||
});
|
||||
const generationBandStyle = (row) => ({
|
||||
gridRow: `${Math.max(1, Math.round((row.y - 88) / GRID_UNIT) + 1)} / span 12`,
|
||||
});
|
||||
const lineageConnectors = computed(() => {
|
||||
const memberById = new Map(
|
||||
layoutMembers.value.map((member) => [member.id, member]),
|
||||
);
|
||||
const childrenByParent = new Map();
|
||||
layoutMembers.value.forEach((member) => {
|
||||
if (!member.parentId || !memberById.has(member.parentId)) return;
|
||||
const children = childrenByParent.get(member.parentId) || [];
|
||||
children.push(member);
|
||||
childrenByParent.set(member.parentId, children);
|
||||
});
|
||||
|
||||
const connectors = [];
|
||||
childrenByParent.forEach((children, parentId) => {
|
||||
const parent = memberById.get(parentId);
|
||||
const childTop = Math.min(...children.map((child) => child.y - NODE_HALF_HEIGHT));
|
||||
const parentBottom = parent.y + NODE_HALF_HEIGHT;
|
||||
const branchY = snapToGrid((parentBottom + childTop) / 2);
|
||||
const minX = Math.min(...children.map((child) => child.x));
|
||||
const maxX = Math.max(...children.map((child) => child.x));
|
||||
const verticalStyle = (x, top, bottom) => ({
|
||||
gridColumn: `${Math.round(x / GRID_UNIT) + 1} / span 1`,
|
||||
gridRow: `${Math.round(top / GRID_UNIT) + 1} / ${Math.round(bottom / GRID_UNIT) + 1}`,
|
||||
});
|
||||
connectors.push({
|
||||
id: `${parentId}-trunk`,
|
||||
style: verticalStyle(parent.x, parentBottom, branchY),
|
||||
});
|
||||
connectors.push({
|
||||
id: `${parentId}-branch`,
|
||||
style: {
|
||||
gridColumn: `${Math.round(minX / GRID_UNIT) + 1} / ${Math.round(maxX / GRID_UNIT) + 2}`,
|
||||
gridRow: `${Math.round(branchY / GRID_UNIT) + 1} / span 1`,
|
||||
},
|
||||
});
|
||||
children.forEach((child) => connectors.push({
|
||||
id: `${parentId}-${child.id}`,
|
||||
style: verticalStyle(child.x, branchY, child.y - NODE_HALF_HEIGHT),
|
||||
}));
|
||||
});
|
||||
return connectors;
|
||||
});
|
||||
|
||||
const stateCopy = computed(
|
||||
() =>
|
||||
({
|
||||
@@ -279,18 +403,15 @@ onLoad((query) => {
|
||||
}
|
||||
genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
selected.value =
|
||||
members.value.find(
|
||||
layoutMembers.value.find(
|
||||
(item) => String(item.id) === String(query.selectedId),
|
||||
) || members.value[0];
|
||||
) || layoutMembers.value[0];
|
||||
treeState.value = "tree";
|
||||
});
|
||||
|
||||
const nodeStyle = (member) => ({
|
||||
left: `${member.x}rpx`,
|
||||
top: `${member.y}rpx`,
|
||||
});
|
||||
const generationRowStyle = (row) => ({
|
||||
top: `${row.y - 88}rpx`,
|
||||
const nodeGridStyle = (member) => ({
|
||||
gridColumn: `${member.x / 5 + 1}`,
|
||||
gridRow: `${member.y / 5 + 1}`,
|
||||
});
|
||||
const handleStateAction = () => {
|
||||
if (treeState.value === "empty") {
|
||||
@@ -299,7 +420,7 @@ const handleStateAction = () => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
selected.value = members.value[0];
|
||||
selected.value = layoutMembers.value[0];
|
||||
treeState.value = "tree";
|
||||
};
|
||||
const toMember = () =>
|
||||
@@ -322,17 +443,16 @@ const toAddRelative = () =>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tree-page {
|
||||
position: relative;
|
||||
display: grid;
|
||||
height: 100vh;
|
||||
grid-template-rows: auto auto minmax(0, 1fr);
|
||||
overflow: hidden;
|
||||
background: $paper;
|
||||
}
|
||||
.tree-page__header,
|
||||
.tree-toolbar,
|
||||
.tree-stage,
|
||||
.member-sheet {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
.tree-stage {
|
||||
z-index: 1;
|
||||
}
|
||||
.tree-toolbar {
|
||||
display: flex;
|
||||
@@ -361,8 +481,13 @@ const toAddRelative = () =>
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.tree-stage {
|
||||
position: relative;
|
||||
height: calc(100vh - 272rpx);
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.tree-stage--lineage {
|
||||
display: grid;
|
||||
grid-template-columns: 190rpx minmax(0, 1fr);
|
||||
align-items: start;
|
||||
}
|
||||
.tree-scroll {
|
||||
width: 100%;
|
||||
@@ -370,40 +495,37 @@ const toAddRelative = () =>
|
||||
white-space: nowrap;
|
||||
}
|
||||
.generation-rail {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: grid;
|
||||
z-index: 4;
|
||||
width: 190rpx;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
border-right: 1rpx solid rgba(143, 108, 63, 0.2);
|
||||
}
|
||||
.tree-scroll--lineage {
|
||||
width: calc(100% - 190rpx);
|
||||
margin-left: 190rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
.tree-canvas {
|
||||
position: relative;
|
||||
width: 900rpx;
|
||||
height: 900rpx;
|
||||
display: grid;
|
||||
margin: 0 16rpx;
|
||||
}
|
||||
.tree-canvas--state {
|
||||
display: block;
|
||||
width: calc(100vw - 32rpx);
|
||||
}
|
||||
.lineage-pan-cue {
|
||||
position: absolute;
|
||||
top: 8rpx;
|
||||
right: 22rpx;
|
||||
grid-area: 1 / 1 / -1 / -1;
|
||||
align-self: start;
|
||||
justify-self: end;
|
||||
z-index: 3;
|
||||
margin: 8rpx 22rpx 0 0;
|
||||
color: #9a7748;
|
||||
font-size: 20rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
.generation-band {
|
||||
position: absolute;
|
||||
left: 12rpx;
|
||||
display: grid;
|
||||
justify-self: center;
|
||||
z-index: 3;
|
||||
width: 166rpx;
|
||||
height: 58rpx;
|
||||
@@ -411,14 +533,15 @@ const toAddRelative = () =>
|
||||
font-size: 22rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.generation-band image,
|
||||
.generation-band__copy {
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
.generation-band image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.generation-band__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -431,78 +554,15 @@ const toAddRelative = () =>
|
||||
font-size: 19rpx;
|
||||
}
|
||||
.lineage-connector {
|
||||
position: absolute;
|
||||
align-self: stretch;
|
||||
justify-self: stretch;
|
||||
z-index: 1;
|
||||
background: #b78a42;
|
||||
}
|
||||
.lineage-connector--root {
|
||||
left: 449rpx;
|
||||
top: 177rpx;
|
||||
width: 2rpx;
|
||||
height: 88rpx;
|
||||
}
|
||||
.lineage-connector--root-branch {
|
||||
left: 275rpx;
|
||||
top: 264rpx;
|
||||
width: 350rpx;
|
||||
height: 2rpx;
|
||||
}
|
||||
.lineage-connector--gen13-left {
|
||||
left: 274rpx;
|
||||
top: 264rpx;
|
||||
width: 2rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
.lineage-connector--gen13-right {
|
||||
left: 624rpx;
|
||||
top: 264rpx;
|
||||
width: 2rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
.lineage-connector--left-trunk {
|
||||
left: 274rpx;
|
||||
top: 392rpx;
|
||||
width: 2rpx;
|
||||
height: 149rpx;
|
||||
}
|
||||
.lineage-connector--left-branch {
|
||||
left: 150rpx;
|
||||
top: 540rpx;
|
||||
width: 250rpx;
|
||||
height: 2rpx;
|
||||
}
|
||||
.lineage-connector--gen14-left {
|
||||
left: 149rpx;
|
||||
top: 540rpx;
|
||||
width: 2rpx;
|
||||
height: 68rpx;
|
||||
}
|
||||
.lineage-connector--gen14-middle {
|
||||
left: 399rpx;
|
||||
top: 540rpx;
|
||||
width: 2rpx;
|
||||
height: 68rpx;
|
||||
}
|
||||
.lineage-connector--right-trunk {
|
||||
left: 624rpx;
|
||||
top: 392rpx;
|
||||
width: 2rpx;
|
||||
height: 149rpx;
|
||||
}
|
||||
.lineage-connector--right-branch {
|
||||
left: 625rpx;
|
||||
top: 540rpx;
|
||||
width: 65rpx;
|
||||
height: 2rpx;
|
||||
}
|
||||
.lineage-connector--gen14-right {
|
||||
left: 689rpx;
|
||||
top: 540rpx;
|
||||
width: 2rpx;
|
||||
height: 68rpx;
|
||||
}
|
||||
.member-node {
|
||||
position: absolute;
|
||||
display: grid;
|
||||
align-self: start;
|
||||
justify-self: start;
|
||||
z-index: 2;
|
||||
width: 224rpx;
|
||||
height: 86rpx;
|
||||
@@ -510,16 +570,19 @@ const toAddRelative = () =>
|
||||
text-align: center;
|
||||
}
|
||||
.member-node__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.member-node__skin,
|
||||
.member-node__copy {
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
.member-node__copy {
|
||||
z-index: 1;
|
||||
}
|
||||
.node-name,
|
||||
.node-relation,
|
||||
.node-years {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
}
|
||||
.node-name {
|
||||
@@ -547,22 +610,23 @@ const toAddRelative = () =>
|
||||
color: $brand-red;
|
||||
}
|
||||
.tree-state-card {
|
||||
position: relative;
|
||||
display: grid;
|
||||
z-index: 2;
|
||||
width: calc(100% - 48rpx);
|
||||
height: 360rpx;
|
||||
min-height: 360rpx;
|
||||
margin: 150rpx 24rpx 0;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
}
|
||||
.tree-state-card__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.tree-state-card__skin,
|
||||
.tree-state-card__content {
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
.tree-state-card__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 62rpx 58rpx 42rpx;
|
||||
}
|
||||
@@ -588,19 +652,20 @@ const toAddRelative = () =>
|
||||
line-height: 1.65;
|
||||
}
|
||||
.tree-state-card__action {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 360rpx;
|
||||
height: 70rpx;
|
||||
min-height: 70rpx;
|
||||
margin: 22rpx auto 0;
|
||||
}
|
||||
.tree-state-card__action image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.tree-state-card__action image,
|
||||
.tree-state-card__action text {
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
.tree-state-card__action text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -616,7 +681,7 @@ const toAddRelative = () =>
|
||||
bottom: calc(14rpx + env(safe-area-inset-bottom));
|
||||
left: 18rpx;
|
||||
display: flex;
|
||||
height: 240rpx;
|
||||
min-height: 240rpx;
|
||||
flex-direction: column;
|
||||
padding: 38rpx 44rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
@@ -629,7 +694,6 @@ const toAddRelative = () =>
|
||||
pointer-events: none;
|
||||
}
|
||||
.member-sheet__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.sheet-name {
|
||||
@@ -646,7 +710,6 @@ const toAddRelative = () =>
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.sheet-actions {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -667,7 +730,7 @@ const toAddRelative = () =>
|
||||
gap: 14rpx;
|
||||
}
|
||||
.member-sheet {
|
||||
height: 240rpx;
|
||||
min-height: 240rpx;
|
||||
padding-top: 32rpx;
|
||||
padding-right: 36rpx;
|
||||
padding-left: 36rpx;
|
||||
|
||||
+123
-239
@@ -1,4 +1,4 @@
|
||||
<!-- 页面编号:T-03;用途:成员档案详情与失败状态(页面设计阶段使用本地模拟数据)。 -->
|
||||
<!-- 页面编号:T-03;用途:按 personId 展示成员档案并进入其真实成员状态。 -->
|
||||
<template>
|
||||
<view
|
||||
class="member-page"
|
||||
@@ -8,73 +8,57 @@
|
||||
}"
|
||||
>
|
||||
<ModulePageBackground module="tree" />
|
||||
<view class="member-page__header"
|
||||
><PageHeader
|
||||
title="成员档案"
|
||||
:action="memberState === 'detail' ? '编辑' : ''"
|
||||
@action="toEdit"
|
||||
/></view>
|
||||
<view class="member-page__header">
|
||||
<PageHeader title="成员档案" :action="memberState === 'detail' && canEdit ? '编辑' : ''" @action="toEdit" />
|
||||
</view>
|
||||
<view class="member-context">
|
||||
<text>{{ genealogyName }}</text>
|
||||
<text>{{ memberState === "detail" ? "成员身份与亲属关系" : "请重新选择成员" }}</text>
|
||||
</view>
|
||||
|
||||
<view class="member-panel">
|
||||
<image
|
||||
class="member-panel__skin"
|
||||
src="/static/assets/modules/tree/transparent/t01-state-panel.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<AppLoading
|
||||
v-if="memberState === 'loading'"
|
||||
text="正在读取成员档案"
|
||||
description="请稍候,正在整理成员资料。"
|
||||
/>
|
||||
<view v-else-if="memberState === 'detail'" class="member-detail">
|
||||
<AppLoading v-if="memberState === 'loading'" text="正在读取成员档案" description="请稍候,正在整理成员资料。" />
|
||||
|
||||
<view v-else-if="memberState === 'detail' && member" class="member-detail">
|
||||
<view class="member-heading">
|
||||
<view class="member-heading__seal"
|
||||
><image
|
||||
src="/static/assets/modules/genealogy/transparent/current-seal-frame.png"
|
||||
mode="scaleToFill"
|
||||
/><text>{{ member.name.slice(0, 1) }}</text></view
|
||||
>
|
||||
<view
|
||||
><text>{{ member.name }}</text
|
||||
><text
|
||||
>第 {{ member.generation }} 世 · {{ member.relation }} ·
|
||||
{{ member.branch }}</text
|
||||
></view
|
||||
>
|
||||
<view class="member-heading__seal"><text>{{ member.name.slice(0, 1) }}</text></view>
|
||||
<view>
|
||||
<text>{{ member.name }}</text>
|
||||
<text>第 {{ member.generation }} 世 · {{ member.relation }} · {{ member.branch }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="member-status-entry" @click="openMemberState(member.status)">
|
||||
<text>{{ statusLabel }}</text>
|
||||
<text>{{ statusDescription }}</text>
|
||||
<text>查看</text>
|
||||
</view>
|
||||
|
||||
<text class="member-section-title">基本资料</text>
|
||||
<view v-for="item in details" :key="item.label" class="member-info-row">
|
||||
<image
|
||||
src="/static/assets/modules/tree/transparent/t07-search-input-frame.png"
|
||||
mode="scaleToFill"
|
||||
/><text>{{ item.label }}</text
|
||||
><text>{{ item.value }}</text>
|
||||
<text>{{ item.label }}</text><text>{{ item.value || "未填写" }}</text>
|
||||
</view>
|
||||
<text class="member-section-title member-section-title--relation"
|
||||
>亲属关系</text
|
||||
>
|
||||
|
||||
<text class="member-section-title member-section-title--relation">亲属关系</text>
|
||||
<view class="member-relatives">
|
||||
<text>长子 · 汤正国</text><text>次子 · 汤正华</text>
|
||||
</view>
|
||||
<view class="member-profile-action" @click="toEdit">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/><text>完善成员档案</text>
|
||||
<view v-for="relative in member.relatives" :key="relative.id" @click="openRelative(relative)">
|
||||
<text>{{ relative.relation }}</text><text>{{ relative.name }}</text>
|
||||
</view>
|
||||
<text v-if="!member.relatives.length">尚未记录可查看的亲属</text>
|
||||
</view>
|
||||
|
||||
<view v-if="canEdit" class="member-profile-action" @click="toEdit"><text>完善成员档案</text></view>
|
||||
</view>
|
||||
|
||||
<view v-else class="member-error">
|
||||
<text>成员档案暂不可用</text
|
||||
><text>请从世系树重新选择成员,当前没有可展示的个人资料。</text>
|
||||
<view class="member-profile-action" @click="toTree"
|
||||
><image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/><text>返回世系树</text></view
|
||||
>
|
||||
<text>成员档案暂不可用</text>
|
||||
<text>{{ errorMessage }}</text>
|
||||
<view class="member-profile-action" @click="toTree"><text>返回世系树</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
@@ -82,203 +66,103 @@ import AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const personId = ref("");
|
||||
const memberState = ref("loading");
|
||||
const member = ref({
|
||||
id: 101,
|
||||
name: "汤文远",
|
||||
generation: 12,
|
||||
relation: "始祖",
|
||||
branch: "主支",
|
||||
generationName: "文字辈",
|
||||
birthDate: "1940年3月",
|
||||
years: "1940—2012",
|
||||
});
|
||||
const details = computed(() => [
|
||||
const member = ref(null);
|
||||
const errorMessage = ref("");
|
||||
const genealogyName = ref("汤氏家谱");
|
||||
|
||||
const memberFixtures = {
|
||||
101: {
|
||||
id: 101, name: "汤文远", generation: 12, relation: "始祖", branch: "主支", generationName: "文字辈",
|
||||
birthDate: "1940年3月", years: "1940—2012", birthplace: "河南南阳", status: "deceased", canEdit: true,
|
||||
relatives: [{ id: 102, name: "汤正国", relation: "长子" }, { id: 103, name: "汤正华", relation: "次子" }],
|
||||
},
|
||||
102: {
|
||||
id: 102, name: "汤正国", generation: 13, relation: "长子", branch: "长房", generationName: "正字辈",
|
||||
birthDate: "1965年5月", years: "1965—", birthplace: "河南洛阳", status: "privacy", canEdit: true,
|
||||
relatives: [{ id: 101, name: "汤文远", relation: "父亲" }, { id: 104, name: "汤凯", relation: "长子" }],
|
||||
},
|
||||
103: {
|
||||
id: 103, name: "汤正华", generation: 13, relation: "次子", branch: "二房", generationName: "正字辈",
|
||||
birthDate: "", years: "资料受限", birthplace: "", status: "forbidden", canEdit: false,
|
||||
relatives: [{ id: 101, name: "汤文远", relation: "父亲" }],
|
||||
},
|
||||
};
|
||||
const details = computed(() => member.value ? [
|
||||
{ label: "字辈", value: member.value.generationName },
|
||||
{ label: "出生日期", value: member.value.birthDate },
|
||||
{ label: "出生日期", value: member.value.status === "forbidden" ? "按权限隐藏" : member.value.birthDate },
|
||||
{ label: "生卒信息", value: member.value.years },
|
||||
{ label: "祖居地", value: member.value.status === "forbidden" ? "按权限隐藏" : member.value.birthplace },
|
||||
{ label: "所属支系", value: member.value.branch },
|
||||
]);
|
||||
] : []);
|
||||
const canEdit = computed(() => Boolean(member.value?.canEdit));
|
||||
const statusLabel = computed(() => ({ privacy: "隐私资料", deceased: "离世纪念", forbidden: "访问受限" })[member.value?.status] || "成员状态");
|
||||
const statusDescription = computed(() => ({
|
||||
privacy: "部分资料仅向授权成员展示",
|
||||
deceased: "查看生平保留与纪念资料说明",
|
||||
forbidden: "当前账号只能查看有限身份信息",
|
||||
})[member.value?.status] || "查看成员状态说明");
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value =
|
||||
query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
|
||||
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
|
||||
personId.value = query.personId || "";
|
||||
if (genealogyId.value)
|
||||
genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
memberState.value =
|
||||
query.state === "loading"
|
||||
? "loading"
|
||||
: query.state === "error" || !personId.value
|
||||
? "error"
|
||||
: "detail";
|
||||
if (genealogyId.value) genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
if (query.state === "loading") { memberState.value = "loading"; return; }
|
||||
if (query.state === "error" || !personId.value || !memberFixtures[personId.value]) {
|
||||
memberState.value = "error";
|
||||
errorMessage.value = !personId.value ? "没有指定成员,请从世系树重新选择。" : "这位成员不存在或已不属于当前家谱。";
|
||||
return;
|
||||
}
|
||||
member.value = { ...memberFixtures[personId.value] };
|
||||
memberState.value = "detail";
|
||||
});
|
||||
const toEdit = () =>
|
||||
|
||||
const toEdit = () => {
|
||||
if (!canEdit.value) { openMemberState("forbidden"); return; }
|
||||
uni.navigateTo({ url: `/pages/tree/t05-edit-member?genealogyId=${genealogyId.value}&personId=${personId.value}` });
|
||||
};
|
||||
const openMemberState = (state) =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/tree/t05-edit-member?genealogyId=${genealogyId.value}&personId=${personId.value}`,
|
||||
url: `/pages/tree/t08-member-states?genealogyId=${genealogyId.value}&personId=${personId.value}&state=${state}`,
|
||||
});
|
||||
const openRelative = (relative) =>
|
||||
uni.navigateTo({ url: `/pages/tree/t03-member-profile?genealogyId=${genealogyId.value}&personId=${relative.id}` });
|
||||
const toTree = () => uni.navigateBack();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.member-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
background: $paper;
|
||||
}
|
||||
.member-page__header {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
.member-panel {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: calc(100% - 32rpx);
|
||||
height: min(650px, calc((100vw - 16px) * 1.5));
|
||||
margin: 18rpx auto 0;
|
||||
}
|
||||
.member-panel__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.member-detail {
|
||||
position: absolute;
|
||||
inset: 7% 8%;
|
||||
}
|
||||
.member-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.member-heading__seal {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 84rpx;
|
||||
height: 108rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.member-heading__seal image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.member-heading__seal text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #fff8ec;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 33rpx;
|
||||
}
|
||||
.member-heading > view:last-child text {
|
||||
display: block;
|
||||
}
|
||||
.member-heading > view:last-child text:first-child {
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 38rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.member-heading > view:last-child text:last-child {
|
||||
margin-top: 8rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.member-section-title {
|
||||
display: block;
|
||||
margin: 20rpx 0 4rpx;
|
||||
color: $brand-red;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.member-info-row {
|
||||
position: relative;
|
||||
height: 62rpx;
|
||||
margin-top: 7rpx;
|
||||
}
|
||||
.member-info-row image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.member-info-row text {
|
||||
position: absolute;
|
||||
top: 21rpx;
|
||||
z-index: 1;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.member-info-row text:nth-child(2) {
|
||||
left: 22rpx;
|
||||
color: $ink-muted;
|
||||
}
|
||||
.member-info-row text:last-child {
|
||||
right: 22rpx;
|
||||
color: $ink;
|
||||
font-weight: 700;
|
||||
}
|
||||
.member-section-title--relation {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.member-relatives {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.member-profile-action {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.member-profile-action image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.member-profile-action text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #fff9ed;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.member-error {
|
||||
position: absolute;
|
||||
top: 31%;
|
||||
right: 12%;
|
||||
left: 12%;
|
||||
text-align: center;
|
||||
}
|
||||
.member-error > text {
|
||||
display: block;
|
||||
}
|
||||
.member-error > text:first-child {
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 35rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.member-error > text:nth-child(2) {
|
||||
margin-top: 18rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.member-panel {
|
||||
width: calc(100% - 48rpx);
|
||||
}
|
||||
}
|
||||
.member-page { display: flex; min-height: 100vh; flex-direction: column; padding-bottom: 28rpx; box-sizing: border-box; background: $paper; }
|
||||
.member-page__header, .member-context, .member-panel { z-index: 2; }
|
||||
.member-context { width: calc(100% - 32rpx); margin: 18rpx auto 0; padding: 16rpx 24rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
|
||||
.member-context text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
|
||||
.member-context text:first-child { color: $ink; font-size: 26rpx; font-weight: 700; }
|
||||
.member-panel { width: calc(100% - 32rpx); min-height: min(620px, calc((100vw - 16px) * 1.42)); margin: 16rpx auto 0; padding: 9% 8%; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t01-state-panel.png") center / 100% 100% no-repeat; }
|
||||
.member-panel > .app-loading { margin-top: 30%; }
|
||||
.member-heading { display: flex; align-items: center; gap: 20rpx; }
|
||||
.member-heading__seal { display: flex; width: 74rpx; height: 74rpx; flex: 0 0 74rpx; align-items: center; justify-content: center; background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png") center / contain no-repeat; }
|
||||
.member-heading__seal text { color: #fff7e7; font-size: 28rpx; }
|
||||
.member-heading > view:last-child text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
|
||||
.member-heading > view:last-child text:last-child { font-size: 23rpx; }
|
||||
.member-heading > view:last-child text:first-child { color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 36rpx; font-weight: 700; }
|
||||
.member-status-entry { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 14rpx; margin-top: 18rpx; padding: 18rpx 22rpx; background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png") center / 100% 100% no-repeat; }
|
||||
.member-status-entry text { color: $ink-muted; font-size: 22rpx; line-height: 1.4; }
|
||||
.member-status-entry text:first-child, .member-status-entry text:last-child { color: $brand-red; font-weight: 700; }
|
||||
.member-section-title { display: block; margin-top: 24rpx; color: $brand-red; font-size: 24rpx; font-weight: 700; }
|
||||
.member-info-row { display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 64rpx; align-items: center; gap: 18rpx; margin-top: 8rpx; padding: 8rpx 18rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
|
||||
.member-info-row text { color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
|
||||
.member-info-row text:last-child { color: $ink; text-align: right; }
|
||||
.member-relatives { margin-top: 8rpx; font-size: 23rpx; }
|
||||
.member-relatives > view { display: flex; justify-content: space-between; gap: 20rpx; padding: 12rpx 18rpx; color: $ink; font-size: 23rpx; }
|
||||
.member-relatives > view text:last-child { color: $brand-red; }
|
||||
.member-relatives > text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.5; }
|
||||
.member-profile-action { display: flex; min-height: 76rpx; align-items: center; justify-content: center; margin-top: 22rpx; background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / 100% 100% no-repeat; }
|
||||
.member-profile-action text { color: #fff9ed; font-size: 24rpx; font-weight: 700; }
|
||||
.member-error { margin-top: 30%; text-align: center; }
|
||||
.member-error > text { display: block; color: $ink-muted; font-size: 24rpx; line-height: 1.55; }
|
||||
.member-error > text:nth-child(2) { font-size: 24rpx; }
|
||||
.member-error > text:first-child { color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; }
|
||||
@media (min-width: 400px) { .member-context, .member-panel { width: calc(100% - 48rpx); } }
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,244 @@
|
||||
<!-- 页面编号:T-04;用途:新增直系亲属与保存结果。 -->
|
||||
<template><TreeMemberForm kind="add" /></template>
|
||||
<!-- 页面编号:T-04;用途:录入首位成员或为指定成员新增亲属。 -->
|
||||
<template>
|
||||
<view
|
||||
class="add-relative-page"
|
||||
:class="{
|
||||
'add-state--form': addState === 'form',
|
||||
'add-state--success': addState === 'success',
|
||||
'add-state--error': addState === 'error',
|
||||
}"
|
||||
>
|
||||
<ModulePageBackground module="tree" />
|
||||
<view class="add-relative-page__header">
|
||||
<PageHeader :title="isFirstMember ? '录入首位成员' : '新增亲属'" />
|
||||
</view>
|
||||
|
||||
<view class="add-relative-panel">
|
||||
<view v-if="addState === 'form'" class="add-relative-form">
|
||||
<text class="form-eyebrow">{{ isFirstMember ? "建立世系起点" : "补全家族关系" }}</text>
|
||||
<text class="form-title">{{ formTitle }}</text>
|
||||
<text class="form-copy">{{ formCopy }}</text>
|
||||
|
||||
<view v-if="!isFirstMember" class="member-context">
|
||||
<text>当前成员</text><text>{{ currentMember.name }} · 第 {{ currentMember.generation }} 世</text>
|
||||
</view>
|
||||
|
||||
<view class="form-field">
|
||||
<text>姓名</text>
|
||||
<input
|
||||
v-model="addForm.name"
|
||||
maxlength="20"
|
||||
placeholder="请输入真实姓名"
|
||||
placeholder-class="form-placeholder"
|
||||
@input="clearError('name')"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="fieldErrors.name" class="field-error">{{ fieldErrors.name }}</text>
|
||||
|
||||
<picker
|
||||
v-if="!isFirstMember"
|
||||
:range="relationOptions"
|
||||
:value="relationIndex"
|
||||
@change="selectRelation"
|
||||
>
|
||||
<view class="form-field form-field--picker">
|
||||
<text>与本人关系</text>
|
||||
<text>{{ addForm.relation || "请选择亲属关系" }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<text v-if="fieldErrors.relation" class="field-error">{{ fieldErrors.relation }}</text>
|
||||
|
||||
<picker :range="genderOptions" :value="genderIndex" @change="selectGender">
|
||||
<view class="form-field form-field--picker">
|
||||
<text>性别</text><text>{{ addForm.gender || "请选择" }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<text v-if="fieldErrors.gender" class="field-error">{{ fieldErrors.gender }}</text>
|
||||
|
||||
<picker mode="date" :value="addForm.birthDate" @change="selectBirthDate">
|
||||
<view class="form-field form-field--picker">
|
||||
<text>出生日期</text><text>{{ addForm.birthDate || "选填" }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<view class="form-field form-field--summary">
|
||||
<text>简要说明</text>
|
||||
<textarea
|
||||
v-model="addForm.summary"
|
||||
auto-height
|
||||
maxlength="200"
|
||||
placeholder="选填:字辈、祖居地或身份说明"
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<text class="form-note">{{ formNote }}</text>
|
||||
<view class="form-action" @click="submitAdd">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
|
||||
<text>{{ isSubmitting ? "正在保存…" : isFirstMember ? "保存首位成员" : "保存亲属" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="add-result">
|
||||
<text class="form-eyebrow">{{ addState === "success" ? "世系资料已更新" : "成员未保存" }}</text>
|
||||
<text class="form-title">{{ addState === "success" ? successTitle : "暂时无法保存成员" }}</text>
|
||||
<text class="form-copy">{{ addState === "success" ? successCopy : "当前填写内容仍保留,可返回修改后重试。" }}</text>
|
||||
<view class="form-action" @click="addState === 'success' ? returnToTree() : retryForm()">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
|
||||
<text>{{ addState === "success" ? "返回世系树" : "返回修改" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AppDialog
|
||||
:visible="discardDialogVisible"
|
||||
eyebrow="尚未保存"
|
||||
title="要放弃本次填写吗"
|
||||
message="返回后,本次新增成员的内容不会保留。"
|
||||
cancel-text="继续填写"
|
||||
confirm-text="放弃并返回"
|
||||
show-cancel
|
||||
@close="discardDialogVisible = false"
|
||||
@cancel="discardDialogVisible = false"
|
||||
@confirm="discardAndBack"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TreeMemberForm from "@/components/tree/TreeMemberForm.vue";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
|
||||
const addState = ref("form");
|
||||
const genealogyId = ref("");
|
||||
const personId = ref("");
|
||||
const mode = ref("relative");
|
||||
const isSubmitting = ref(false);
|
||||
const discardDialogVisible = ref(false);
|
||||
let submitTimer = null;
|
||||
|
||||
const memberFixtures = {
|
||||
101: { id: 101, name: "汤文远", generation: 12 },
|
||||
102: { id: 102, name: "汤正国", generation: 13 },
|
||||
103: { id: 103, name: "汤正华", generation: 13 },
|
||||
};
|
||||
const relationOptions = ["长子", "次子", "女儿", "配偶", "兄弟", "姐妹"];
|
||||
const genderOptions = ["男", "女", "未说明"];
|
||||
const addForm = reactive({ name: "", relation: "", gender: "", birthDate: "", summary: "" });
|
||||
const fieldErrors = reactive({ name: "", relation: "", gender: "" });
|
||||
|
||||
const isFirstMember = computed(
|
||||
() => mode.value === "first" || (!personId.value && mode.value !== "relative"),
|
||||
);
|
||||
const currentMember = computed(
|
||||
() => memberFixtures[personId.value] || { id: personId.value, name: "当前成员", generation: "待确认" },
|
||||
);
|
||||
const relationIndex = computed(() => Math.max(0, relationOptions.indexOf(addForm.relation)));
|
||||
const genderIndex = computed(() => Math.max(0, genderOptions.indexOf(addForm.gender)));
|
||||
const formTitle = computed(() =>
|
||||
isFirstMember.value ? "录入家谱中的第一位成员" : `为${currentMember.value.name}添加一位亲属`,
|
||||
);
|
||||
const formCopy = computed(() =>
|
||||
isFirstMember.value
|
||||
? "首位成员将成为世系起点,后续可从此人继续补充配偶、子女和后代。"
|
||||
: "先确认新成员与当前成员的关系,再填写可核实的身份信息。",
|
||||
);
|
||||
const formNote = computed(() =>
|
||||
isFirstMember.value
|
||||
? "保存后进入世系树,并可继续为首位成员添加亲属。"
|
||||
: "保存后成员会出现在相应世代;详细生平可在成员档案中继续完善。",
|
||||
);
|
||||
const successTitle = computed(() =>
|
||||
isFirstMember.value ? `${addForm.name}已成为世系起点` : `${addForm.name}已加入世系`,
|
||||
);
|
||||
const successCopy = computed(() =>
|
||||
isFirstMember.value
|
||||
? "首位成员已保存,世系树现在可以继续向下补充。"
|
||||
: `${addForm.relation}关系已记录,返回后可查看新的成员节点。`,
|
||||
);
|
||||
const hasDraft = computed(() =>
|
||||
Object.values(addForm).some((value) => String(value).trim()),
|
||||
);
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
|
||||
personId.value = query.personId || "";
|
||||
mode.value = query.mode === "first" ? "first" : "relative";
|
||||
if (genealogyId.value) genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
addState.value = query.state === "success" ? "success" : query.state === "error" ? "error" : "form";
|
||||
});
|
||||
onUnload(() => {
|
||||
if (submitTimer) clearTimeout(submitTimer);
|
||||
});
|
||||
onBackPress(() => {
|
||||
if (discardDialogVisible.value) {
|
||||
discardDialogVisible.value = false;
|
||||
return true;
|
||||
}
|
||||
if (addState.value === "form" && hasDraft.value) {
|
||||
discardDialogVisible.value = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const clearError = (field) => { fieldErrors[field] = ""; };
|
||||
const selectRelation = (event) => {
|
||||
addForm.relation = relationOptions[Number(event.detail.value)] || "";
|
||||
clearError("relation");
|
||||
};
|
||||
const selectGender = (event) => {
|
||||
addForm.gender = genderOptions[Number(event.detail.value)] || "";
|
||||
clearError("gender");
|
||||
};
|
||||
const selectBirthDate = (event) => { addForm.birthDate = event.detail.value || ""; };
|
||||
const validateAddForm = () => {
|
||||
fieldErrors.name = addForm.name.trim() ? "" : "请填写成员姓名";
|
||||
fieldErrors.relation = isFirstMember.value || addForm.relation ? "" : "请选择与当前成员的关系";
|
||||
fieldErrors.gender = addForm.gender ? "" : "请选择性别或未说明";
|
||||
return !fieldErrors.name && !fieldErrors.relation && !fieldErrors.gender;
|
||||
};
|
||||
const submitAdd = () => {
|
||||
if (isSubmitting.value || !validateAddForm()) return;
|
||||
isSubmitting.value = true;
|
||||
submitTimer = setTimeout(() => {
|
||||
addState.value = addForm.name.trim() === "失败" ? "error" : "success";
|
||||
isSubmitting.value = false;
|
||||
submitTimer = null;
|
||||
}, 280);
|
||||
};
|
||||
const retryForm = () => { addState.value = "form"; };
|
||||
const returnToTree = () => uni.navigateBack();
|
||||
const discardAndBack = () => {
|
||||
discardDialogVisible.value = false;
|
||||
uni.navigateBack();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.add-relative-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.add-relative-page__header { z-index: 3; }
|
||||
.add-relative-panel { z-index: 2; width: calc(100% - 32rpx); min-height: min(680px, calc((100vw - 16px) * 1.5)); margin: 18rpx auto 28rpx; padding: 7.5% 8%; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t01-state-panel.png") center / 100% 100% no-repeat; }
|
||||
.form-eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
|
||||
.form-title { display: block; margin-top: 10rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; line-height: 1.35; }
|
||||
.form-copy, .form-note { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.5; }
|
||||
.member-context, .form-field { display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 78rpx; align-items: center; gap: 20rpx; margin-top: 14rpx; padding: 12rpx 22rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
|
||||
.member-context text:first-child, .form-field > text:first-child { color: $ink; font-size: 24rpx; font-weight: 700; }
|
||||
.member-context text:last-child, .form-field > text:last-child, .form-field input, .form-field textarea { min-width: 0; color: $ink; font-size: 24rpx; line-height: 1.45; text-align: right; }
|
||||
.form-field textarea { width: auto; min-height: 54rpx; text-align: left; }
|
||||
.form-field--summary { align-items: start; }
|
||||
.form-placeholder { color: #a79884; }
|
||||
.field-error { display: block; margin: 5rpx 18rpx 0; color: $brand-red; font-size: 22rpx; line-height: 32rpx; }
|
||||
.form-note { text-align: center; }
|
||||
.form-action { display: grid; width: 100%; min-height: 76rpx; margin-top: 18rpx; }
|
||||
.form-action image, .form-action text { grid-area: 1 / 1; width: 100%; height: 100%; }
|
||||
.form-action text { z-index: 1; display: flex; align-items: center; justify-content: center; color: #fff9ed; font-size: 24rpx; font-weight: 700; }
|
||||
.add-result { margin-top: 30%; text-align: center; }
|
||||
.add-result .form-eyebrow, .add-result .form-copy { text-align: center; }
|
||||
.add-result .form-action { width: 420rpx; max-width: 100%; margin-right: auto; margin-left: auto; }
|
||||
@media (min-width: 400px) { .add-relative-panel { width: calc(100% - 48rpx); } }
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,193 @@
|
||||
<!-- 页面编号:T-05;用途:编辑成员资料与保存结果。 -->
|
||||
<template><TreeMemberForm kind="edit" /></template>
|
||||
<!-- 页面编号:T-05;用途:维护指定成员的身份与生平资料。 -->
|
||||
<template>
|
||||
<view
|
||||
class="edit-member-page"
|
||||
:class="{
|
||||
'edit-state--form': editState === 'form',
|
||||
'edit-state--success': editState === 'success',
|
||||
'edit-state--error': editState === 'error',
|
||||
'edit-state--no-permission': editState === 'no-permission',
|
||||
}"
|
||||
>
|
||||
<ModulePageBackground module="tree" />
|
||||
<view class="edit-member-page__header"><PageHeader title="编辑成员" /></view>
|
||||
<view class="edit-member-panel">
|
||||
<view v-if="editState === 'form'" class="edit-member-form">
|
||||
<text class="form-eyebrow">成员档案维护</text>
|
||||
<text class="form-title">完善{{ originalMember.name }}的生命记录</text>
|
||||
<text class="form-copy">基础身份用于世系展示,生平说明会显示在有权限查看的成员档案中。</text>
|
||||
|
||||
<view class="member-context">
|
||||
<text>成员身份</text><text>第 {{ originalMember.generation }} 世 · {{ originalMember.branch }}</text>
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<text>姓名</text>
|
||||
<input v-model="editForm.name" maxlength="20" placeholder="请输入姓名" placeholder-class="form-placeholder" @input="clearError('name')" />
|
||||
</view>
|
||||
<text v-if="fieldErrors.name" class="field-error">{{ fieldErrors.name }}</text>
|
||||
|
||||
<view class="form-field">
|
||||
<text>字辈</text>
|
||||
<input v-model="editForm.generationName" maxlength="12" placeholder="例如:文字辈" placeholder-class="form-placeholder" />
|
||||
</view>
|
||||
<picker mode="date" :value="editForm.birthDate" @change="selectDate('birthDate', $event)">
|
||||
<view class="form-field form-field--picker"><text>出生日期</text><text>{{ editForm.birthDate || "未填写" }}</text></view>
|
||||
</picker>
|
||||
<picker mode="date" :value="editForm.deathDate" @change="selectDate('deathDate', $event)">
|
||||
<view class="form-field form-field--picker"><text>离世日期</text><text>{{ editForm.deathDate || "在世或未填写" }}</text></view>
|
||||
</picker>
|
||||
<view class="form-field form-field--summary">
|
||||
<text>人物简介</text>
|
||||
<textarea v-model="editForm.summary" auto-height maxlength="500" placeholder="记录生平、迁徙或重要经历" placeholder-class="form-placeholder" />
|
||||
</view>
|
||||
<text v-if="fieldErrors.dates" class="field-error">{{ fieldErrors.dates }}</text>
|
||||
|
||||
<text class="form-note">隐私字段只向本人和具备维护权限的家谱管理员展示。</text>
|
||||
<view class="form-action" @click="saveMember">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
|
||||
<text>{{ isSubmitting ? "正在保存…" : "保存资料" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="edit-result">
|
||||
<text class="form-eyebrow">{{ resultCopy.eyebrow }}</text>
|
||||
<text class="form-title">{{ resultCopy.title }}</text>
|
||||
<text class="form-copy">{{ resultCopy.copy }}</text>
|
||||
<view class="form-action" @click="handleResultAction">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
|
||||
<text>{{ resultCopy.action }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AppDialog
|
||||
:visible="discardDialogVisible"
|
||||
eyebrow="资料尚未保存"
|
||||
title="要放弃本次修改吗"
|
||||
message="返回后,本次对成员档案的修改不会保留。"
|
||||
cancel-text="继续编辑"
|
||||
confirm-text="放弃修改"
|
||||
show-cancel
|
||||
@close="discardDialogVisible = false"
|
||||
@cancel="discardDialogVisible = false"
|
||||
@confirm="discardAndBack"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TreeMemberForm from "@/components/tree/TreeMemberForm.vue";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
|
||||
const editState = ref("form");
|
||||
const genealogyId = ref("");
|
||||
const personId = ref("");
|
||||
const isSubmitting = ref(false);
|
||||
const discardDialogVisible = ref(false);
|
||||
let submitTimer = null;
|
||||
|
||||
const memberFixtures = {
|
||||
101: { id: 101, name: "汤文远", generation: 12, generationName: "文字辈", branch: "主支", birthDate: "1940-03-01", deathDate: "2012-08-16", summary: "一生敦亲睦族,参与整理家族旧谱。" },
|
||||
102: { id: 102, name: "汤正国", generation: 13, generationName: "正字辈", branch: "长房", birthDate: "1965-05-12", deathDate: "", summary: "负责长房资料核对。" },
|
||||
103: { id: 103, name: "汤正华", generation: 13, generationName: "正字辈", branch: "二房", birthDate: "1968-09-03", deathDate: "", summary: "资料仍在补充。" },
|
||||
};
|
||||
const fallbackMember = { id: "", name: "当前成员", generation: "待确认", generationName: "", branch: "待确认", birthDate: "", deathDate: "", summary: "" };
|
||||
const originalMember = ref({ ...fallbackMember });
|
||||
const baseline = ref("");
|
||||
const editForm = reactive({ name: "", generationName: "", birthDate: "", deathDate: "", summary: "" });
|
||||
const fieldErrors = reactive({ name: "", dates: "" });
|
||||
|
||||
const formSnapshot = computed(() => JSON.stringify(editForm));
|
||||
const isDirty = computed(
|
||||
() => editState.value === "form" && baseline.value && formSnapshot.value !== baseline.value,
|
||||
);
|
||||
const resultCopy = computed(() => ({
|
||||
success: { eyebrow: "成员资料已更新", title: `${editForm.name}的档案已保存`, copy: "返回成员档案后可以查看本次修改。", action: "返回成员档案" },
|
||||
error: { eyebrow: "资料未保存", title: "暂时无法保存成员档案", copy: "当前修改仍保留,可返回表单后重试。", action: "返回修改" },
|
||||
"no-permission": { eyebrow: "权限不足", title: "当前账号不能编辑这位成员", copy: "本人或具备成员维护权限的家谱管理员才能修改档案。", action: "返回成员档案" },
|
||||
}[editState.value] || {}));
|
||||
|
||||
const loadMember = (id) => {
|
||||
const member = memberFixtures[id] || { ...fallbackMember, id, name: "待核实成员" };
|
||||
originalMember.value = { ...member };
|
||||
Object.assign(editForm, {
|
||||
name: member.name,
|
||||
generationName: member.generationName,
|
||||
birthDate: member.birthDate,
|
||||
deathDate: member.deathDate,
|
||||
summary: member.summary,
|
||||
});
|
||||
baseline.value = formSnapshot.value;
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
|
||||
personId.value = query.personId || "";
|
||||
if (genealogyId.value) genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
loadMember(personId.value);
|
||||
editState.value = query.state === "success" ? "success" : query.state === "error" ? "error" : query.state === "no-permission" ? "no-permission" : !personId.value ? "error" : "form";
|
||||
});
|
||||
onUnload(() => { if (submitTimer) clearTimeout(submitTimer); });
|
||||
onBackPress(() => {
|
||||
if (discardDialogVisible.value) { discardDialogVisible.value = false; return true; }
|
||||
if (isDirty.value) { discardDialogVisible.value = true; return true; }
|
||||
return false;
|
||||
});
|
||||
|
||||
const clearError = (field) => { fieldErrors[field] = ""; };
|
||||
const selectDate = (field, event) => {
|
||||
editForm[field] = event.detail.value || "";
|
||||
fieldErrors.dates = "";
|
||||
};
|
||||
const validateEditForm = () => {
|
||||
fieldErrors.name = editForm.name.trim() ? "" : "请填写成员姓名";
|
||||
fieldErrors.dates = editForm.birthDate && editForm.deathDate && editForm.deathDate < editForm.birthDate ? "离世日期不能早于出生日期" : "";
|
||||
return !fieldErrors.name && !fieldErrors.dates;
|
||||
};
|
||||
const saveMember = () => {
|
||||
if (isSubmitting.value || !validateEditForm()) return;
|
||||
isSubmitting.value = true;
|
||||
submitTimer = setTimeout(() => {
|
||||
editState.value = editForm.name.trim() === "失败" ? "error" : "success";
|
||||
if (editState.value === "success") baseline.value = formSnapshot.value;
|
||||
isSubmitting.value = false;
|
||||
submitTimer = null;
|
||||
}, 280);
|
||||
};
|
||||
const handleResultAction = () => {
|
||||
if (editState.value === "error") { editState.value = "form"; return; }
|
||||
uni.navigateBack();
|
||||
};
|
||||
const discardAndBack = () => {
|
||||
discardDialogVisible.value = false;
|
||||
uni.navigateBack();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.edit-member-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.edit-member-page__header { z-index: 3; }
|
||||
.edit-member-panel { z-index: 2; width: calc(100% - 32rpx); min-height: min(690px, calc((100vw - 16px) * 1.52)); margin: 18rpx auto 28rpx; padding: 7.5% 8%; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t01-state-panel.png") center / 100% 100% no-repeat; }
|
||||
.form-eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
|
||||
.form-title { display: block; margin-top: 10rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; line-height: 1.35; }
|
||||
.form-copy, .form-note { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.5; }
|
||||
.member-context, .form-field { display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 78rpx; align-items: center; gap: 20rpx; margin-top: 14rpx; padding: 12rpx 22rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
|
||||
.member-context text:first-child, .form-field > text:first-child { color: $ink; font-size: 24rpx; font-weight: 700; }
|
||||
.member-context text:last-child, .form-field > text:last-child, .form-field input, .form-field textarea { min-width: 0; color: $ink; font-size: 24rpx; line-height: 1.45; text-align: right; }
|
||||
.form-field textarea { width: auto; min-height: 54rpx; text-align: left; }
|
||||
.form-field--summary { align-items: start; }
|
||||
.form-placeholder { color: #a79884; }
|
||||
.field-error { display: block; margin: 5rpx 18rpx 0; color: $brand-red; font-size: 22rpx; line-height: 32rpx; }
|
||||
.form-note { text-align: center; }
|
||||
.form-action { display: grid; width: 100%; min-height: 76rpx; margin-top: 18rpx; }
|
||||
.form-action image, .form-action text { grid-area: 1 / 1; width: 100%; height: 100%; }
|
||||
.form-action text { z-index: 1; display: flex; align-items: center; justify-content: center; color: #fff9ed; font-size: 24rpx; font-weight: 700; }
|
||||
.edit-result { margin-top: 30%; text-align: center; }
|
||||
.edit-result .form-eyebrow, .edit-result .form-copy { text-align: center; }
|
||||
.edit-result .form-action { width: 420rpx; max-width: 100%; margin-right: auto; margin-left: auto; }
|
||||
@media (min-width: 400px) { .edit-member-panel { width: calc(100% - 48rpx); } }
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,215 @@
|
||||
<!-- 页面编号:T-06;用途:编辑亲属关系、冲突提示与处理。 -->
|
||||
<template><TreeMemberForm kind="relation" /></template>
|
||||
<!-- 页面编号:T-06;用途:选择两位现有成员并校正其家族关系。 -->
|
||||
<template>
|
||||
<view
|
||||
class="relationship-page"
|
||||
:class="{
|
||||
'relationship-state--form': relationshipState === 'form',
|
||||
'relationship-state--success': relationshipState === 'success',
|
||||
'relationship-state--conflict': relationshipState === 'conflict',
|
||||
'relationship-state--error': relationshipState === 'error',
|
||||
}"
|
||||
>
|
||||
<ModulePageBackground module="tree" />
|
||||
<view class="relationship-page__header"><PageHeader title="关系维护" /></view>
|
||||
<view class="relationship-panel">
|
||||
<view v-if="relationshipState === 'form'" class="relationship-form">
|
||||
<text class="form-eyebrow">亲属关系校正</text>
|
||||
<text class="form-title">确认两位成员的家族关系</text>
|
||||
<text class="form-copy">选择成员和关系类型后,先查看对世系的影响,再决定是否保存。</text>
|
||||
|
||||
<picker :range="memberLabels" :value="sourceIndex" @change="selectMember('sourceId', $event)">
|
||||
<view class="form-field form-field--picker"><text>当前成员</text><text>{{ memberName(relationshipForm.sourceId) || "请选择成员" }}</text></view>
|
||||
</picker>
|
||||
<text v-if="fieldErrors.sourceId" class="field-error">{{ fieldErrors.sourceId }}</text>
|
||||
|
||||
<picker :range="memberLabels" :value="targetIndex" @change="selectMember('targetId', $event)">
|
||||
<view class="form-field form-field--picker"><text>关联成员</text><text>{{ memberName(relationshipForm.targetId) || "请选择另一位成员" }}</text></view>
|
||||
</picker>
|
||||
<text v-if="fieldErrors.targetId" class="field-error">{{ fieldErrors.targetId }}</text>
|
||||
|
||||
<picker :range="relationshipOptions" :value="relationshipIndex" @change="selectRelationship">
|
||||
<view class="form-field form-field--picker"><text>关系类型</text><text>{{ relationshipForm.relationship || "请选择关系" }}</text></view>
|
||||
</picker>
|
||||
<text v-if="fieldErrors.relationship" class="field-error">{{ fieldErrors.relationship }}</text>
|
||||
|
||||
<view class="relationship-preview">
|
||||
<text>关系影响预览</text>
|
||||
<text>{{ relationshipPreview }}</text>
|
||||
</view>
|
||||
|
||||
<text class="form-note">父母子女关系会改变世系位置;配偶和兄弟姐妹关系不会自动改写现有父母。</text>
|
||||
<view class="form-action" @click="saveRelationship">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
|
||||
<text>{{ isSubmitting ? "正在校验…" : "校验并保存关系" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="relationship-result">
|
||||
<text class="form-eyebrow">{{ resultCopy.eyebrow }}</text>
|
||||
<text class="form-title">{{ resultCopy.title }}</text>
|
||||
<text class="form-copy">{{ resultCopy.copy }}</text>
|
||||
<view v-if="relationshipState === 'conflict'" class="result-actions">
|
||||
<view class="form-action form-action--secondary" @click="relationshipState = 'form'">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png" mode="scaleToFill" /><text>返回核对</text>
|
||||
</view>
|
||||
<view class="form-action" @click="conflictDialogVisible = true">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" /><text>查看规则</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="form-action" @click="handleResultAction">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
|
||||
<text>{{ relationshipState === "success" ? "返回世系树" : "重新选择" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AppDialog
|
||||
:visible="conflictDialogVisible"
|
||||
eyebrow="关系校验规则"
|
||||
title="为什么不能保存这段关系"
|
||||
:message="conflictReason || '同一成员不能成为自己的亲属,也不能形成上下代循环或重复父母关系。'"
|
||||
@confirm="conflictDialogVisible = false"
|
||||
@close="conflictDialogVisible = false"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TreeMemberForm from "@/components/tree/TreeMemberForm.vue";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
|
||||
const relationshipState = ref("form");
|
||||
const genealogyId = ref("");
|
||||
const personId = ref("");
|
||||
const isSubmitting = ref(false);
|
||||
const conflictDialogVisible = ref(false);
|
||||
const conflictReason = ref("");
|
||||
let submitTimer = null;
|
||||
|
||||
const memberOptions = [
|
||||
{ id: "101", name: "汤文远", generation: 12, parentId: "" },
|
||||
{ id: "102", name: "汤正国", generation: 13, parentId: "101" },
|
||||
{ id: "103", name: "汤正华", generation: 13, parentId: "101" },
|
||||
{ id: "104", name: "汤凯", generation: 14, parentId: "102" },
|
||||
{ id: "105", name: "汤悦", generation: 14, parentId: "102" },
|
||||
];
|
||||
const relationshipOptions = ["父子(当前成员为父)", "父女(当前成员为父)", "母子(当前成员为母)", "母女(当前成员为母)", "配偶", "兄弟姐妹"];
|
||||
const relationshipForm = reactive({ sourceId: "", targetId: "", relationship: "" });
|
||||
const fieldErrors = reactive({ sourceId: "", targetId: "", relationship: "" });
|
||||
const memberLabels = computed(() => memberOptions.map((item) => `${item.name} · 第 ${item.generation} 世`));
|
||||
const memberById = computed(() => new Map(memberOptions.map((item) => [item.id, item])));
|
||||
const sourceIndex = computed(() => Math.max(0, memberOptions.findIndex((item) => item.id === relationshipForm.sourceId)));
|
||||
const targetIndex = computed(() => Math.max(0, memberOptions.findIndex((item) => item.id === relationshipForm.targetId)));
|
||||
const relationshipIndex = computed(() => Math.max(0, relationshipOptions.indexOf(relationshipForm.relationship)));
|
||||
const memberName = (id) => memberById.value.get(String(id))?.name || "";
|
||||
const relationshipPreview = computed(
|
||||
() => relationshipForm.sourceId && relationshipForm.targetId && relationshipForm.relationship
|
||||
? `${memberName(relationshipForm.sourceId)}将以“${relationshipForm.relationship}”关联${memberName(relationshipForm.targetId)}。`
|
||||
: "完成三项选择后,这里会说明世系位置将如何变化。",
|
||||
);
|
||||
const resultCopy = computed(() => ({
|
||||
success: { eyebrow: "关系已保存", title: "世系关系已经更新", copy: relationshipPreview.value },
|
||||
conflict: { eyebrow: "发现关系冲突", title: "这段关系会造成世系矛盾", copy: conflictReason.value },
|
||||
error: { eyebrow: "关系未保存", title: "暂时无法完成关系调整", copy: "当前选择仍然保留,可返回后重新校验。" },
|
||||
}[relationshipState.value] || {}));
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
|
||||
personId.value = query.personId || "";
|
||||
relationshipForm.sourceId = personId.value && memberById.value.has(String(personId.value)) ? String(personId.value) : memberOptions[0].id;
|
||||
if (genealogyId.value) genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
relationshipState.value = query.state === "success" ? "success" : query.state === "conflict" ? "conflict" : query.state === "error" ? "error" : "form";
|
||||
if (relationshipState.value === "conflict") conflictReason.value = "目标成员已经存在父级关系,请先核对原关系。";
|
||||
});
|
||||
onUnload(() => { if (submitTimer) clearTimeout(submitTimer); });
|
||||
|
||||
const clearFieldError = (field) => { fieldErrors[field] = ""; };
|
||||
const selectMember = (field, event) => {
|
||||
relationshipForm[field] = memberOptions[Number(event.detail.value)]?.id || "";
|
||||
clearFieldError(field);
|
||||
};
|
||||
const selectRelationship = (event) => {
|
||||
relationshipForm.relationship = relationshipOptions[Number(event.detail.value)] || "";
|
||||
clearFieldError("relationship");
|
||||
};
|
||||
const isAncestor = (possibleAncestorId, memberId) => {
|
||||
let current = memberById.value.get(String(memberId));
|
||||
const visited = new Set();
|
||||
while (current?.parentId && !visited.has(current.id)) {
|
||||
if (current.parentId === String(possibleAncestorId)) return true;
|
||||
visited.add(current.id);
|
||||
current = memberById.value.get(current.parentId);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const validateRelationship = () => {
|
||||
fieldErrors.sourceId = relationshipForm.sourceId ? "" : "请选择当前成员";
|
||||
fieldErrors.targetId = relationshipForm.targetId ? "" : "请选择关联成员";
|
||||
fieldErrors.relationship = relationshipForm.relationship ? "" : "请选择关系类型";
|
||||
if (fieldErrors.sourceId || fieldErrors.targetId || fieldErrors.relationship) return false;
|
||||
if (relationshipForm.sourceId === relationshipForm.targetId) {
|
||||
conflictReason.value = "同一成员不能与自己建立亲属关系。";
|
||||
return false;
|
||||
}
|
||||
const isParentRelationship = relationshipForm.relationship.includes("当前成员为");
|
||||
const target = memberById.value.get(relationshipForm.targetId);
|
||||
if (isParentRelationship && isAncestor(relationshipForm.targetId, relationshipForm.sourceId)) {
|
||||
conflictReason.value = "保存后会形成上下代循环,请重新选择成员方向。";
|
||||
return false;
|
||||
}
|
||||
if (isParentRelationship && target?.parentId && target.parentId !== relationshipForm.sourceId) {
|
||||
conflictReason.value = `${target.name}已经存在父级成员,不能直接重复建立父母关系。`;
|
||||
return false;
|
||||
}
|
||||
conflictReason.value = "";
|
||||
return true;
|
||||
};
|
||||
const saveRelationship = () => {
|
||||
if (isSubmitting.value) return;
|
||||
if (!validateRelationship()) {
|
||||
if (conflictReason.value) relationshipState.value = "conflict";
|
||||
return;
|
||||
}
|
||||
isSubmitting.value = true;
|
||||
submitTimer = setTimeout(() => {
|
||||
relationshipState.value = relationshipForm.relationship === "失败" ? "error" : "success";
|
||||
isSubmitting.value = false;
|
||||
submitTimer = null;
|
||||
}, 280);
|
||||
};
|
||||
const handleResultAction = () => {
|
||||
if (relationshipState.value === "error") { relationshipState.value = "form"; return; }
|
||||
uni.navigateBack();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.relationship-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.relationship-page__header { z-index: 3; }
|
||||
.relationship-panel { z-index: 2; width: calc(100% - 32rpx); min-height: min(650px, calc((100vw - 16px) * 1.46)); margin: 18rpx auto 28rpx; padding: 7.5% 8%; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t01-state-panel.png") center / 100% 100% no-repeat; }
|
||||
.form-eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
|
||||
.form-title { display: block; margin-top: 10rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; line-height: 1.35; }
|
||||
.form-copy, .form-note { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.5; }
|
||||
.form-field { display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 78rpx; align-items: center; gap: 20rpx; margin-top: 14rpx; padding: 12rpx 22rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
|
||||
.form-field text:first-child { color: $ink; font-size: 24rpx; font-weight: 700; }
|
||||
.form-field text:last-child { min-width: 0; color: $ink; font-size: 24rpx; line-height: 1.45; text-align: right; }
|
||||
.field-error { display: block; margin: 5rpx 18rpx 0; color: $brand-red; font-size: 22rpx; line-height: 32rpx; }
|
||||
.relationship-preview { margin-top: 18rpx; padding: 20rpx 22rpx; background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png") center / 100% 100% no-repeat; }
|
||||
.relationship-preview text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.5; }
|
||||
.relationship-preview text:first-child { color: $brand-red; font-weight: 700; }
|
||||
.form-note { text-align: center; }
|
||||
.form-action { display: grid; width: 100%; min-height: 76rpx; margin-top: 18rpx; }
|
||||
.form-action image, .form-action text { grid-area: 1 / 1; width: 100%; height: 100%; }
|
||||
.form-action text { z-index: 1; display: flex; align-items: center; justify-content: center; color: #fff9ed; font-size: 24rpx; font-weight: 700; }
|
||||
.form-action--secondary text { color: $ink; }
|
||||
.relationship-result { margin-top: 28%; text-align: center; }
|
||||
.relationship-result .form-eyebrow, .relationship-result .form-copy { text-align: center; }
|
||||
.relationship-result > .form-action { width: 420rpx; max-width: 100%; margin-right: auto; margin-left: auto; }
|
||||
.result-actions { display: flex; gap: 14rpx; margin-top: 24rpx; }
|
||||
.result-actions .form-action { width: calc(50% - 7rpx); margin-top: 0; }
|
||||
@media (min-width: 400px) { .relationship-panel { width: calc(100% - 48rpx); } }
|
||||
</style>
|
||||
|
||||
@@ -21,11 +21,6 @@
|
||||
v-if="directoryState === 'list' || directoryState === 'empty'"
|
||||
class="directory-search"
|
||||
>
|
||||
<image
|
||||
class="directory-search__frame"
|
||||
src="/static/assets/modules/tree/transparent/t07-search-input-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<input
|
||||
v-model="keyword"
|
||||
aria-label="成员搜索关键词"
|
||||
@@ -59,11 +54,6 @@
|
||||
class="directory-card"
|
||||
@click="openMember(item)"
|
||||
>
|
||||
<image
|
||||
class="directory-card__frame"
|
||||
src="/static/assets/modules/genealogy/transparent/list-slip-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="directory-card__copy">
|
||||
<text class="directory-card__name">{{ item.name }}</text>
|
||||
<text class="directory-card__meta"
|
||||
@@ -75,10 +65,6 @@
|
||||
</view>
|
||||
</template>
|
||||
<view v-else class="directory-state-card">
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/list-slip-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view
|
||||
><text>{{
|
||||
directoryState === "empty" ? "没有找到相关成员" : "成员目录暂不可用"
|
||||
@@ -167,9 +153,9 @@ const openMember = (item) =>
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.directory-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
background: $paper;
|
||||
}
|
||||
@@ -177,7 +163,6 @@ const openMember = (item) =>
|
||||
.directory-context,
|
||||
.directory-search,
|
||||
.directory-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.directory-context {
|
||||
@@ -199,39 +184,33 @@ const openMember = (item) =>
|
||||
font-weight: 500;
|
||||
}
|
||||
.directory-search {
|
||||
display: grid;
|
||||
width: calc(100% - 48rpx);
|
||||
height: 44px;
|
||||
min-height: 44px;
|
||||
margin: 20rpx auto 0;
|
||||
}
|
||||
.directory-search__frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.directory-search input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 110rpx;
|
||||
bottom: 0;
|
||||
left: 26rpx;
|
||||
z-index: 1;
|
||||
height: 44px;
|
||||
grid-area: 1 / 1;
|
||||
min-height: 44px;
|
||||
margin-right: 110rpx;
|
||||
margin-left: 26rpx;
|
||||
color: $ink;
|
||||
font-size: 26rpx;
|
||||
line-height: 44px;
|
||||
}
|
||||
.directory-search__action {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 8rpx;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
justify-self: end;
|
||||
min-width: 88rpx;
|
||||
min-height: 44px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 8rpx;
|
||||
color: $brand-red;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
@@ -258,25 +237,17 @@ const openMember = (item) =>
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.directory-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 196rpx;
|
||||
min-height: 196rpx;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 16rpx;
|
||||
padding: 18rpx 28rpx;
|
||||
}
|
||||
.directory-card__frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.directory-card__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
@@ -285,21 +256,19 @@ const openMember = (item) =>
|
||||
justify-content: center;
|
||||
}
|
||||
.directory-card__name {
|
||||
overflow: hidden;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.3;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.directory-card__meta {
|
||||
overflow: hidden;
|
||||
margin-top: 7rpx;
|
||||
color: #62584c;
|
||||
font-size: 24rpx;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.directory-card__status {
|
||||
align-self: flex-start;
|
||||
@@ -311,23 +280,21 @@ const openMember = (item) =>
|
||||
line-height: 30rpx;
|
||||
}
|
||||
.directory-state-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 220rpx;
|
||||
min-height: 220rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.directory-state-card > image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 48rpx 12%;
|
||||
box-sizing: border-box;
|
||||
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.directory-state-card {
|
||||
margin-top: 70rpx;
|
||||
}
|
||||
.directory-state-card > view {
|
||||
position: absolute;
|
||||
inset: 24% 12%;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -351,7 +318,7 @@ const openMember = (item) =>
|
||||
}
|
||||
@media screen and (max-width: 340px) {
|
||||
.directory-card {
|
||||
height: 192rpx;
|
||||
min-height: 192rpx;
|
||||
padding-right: 22rpx;
|
||||
padding-left: 22rpx;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 页面编号:T-08;用途:成员隐私、离世纪念与无权限状态说明。 -->
|
||||
<!-- 页面编号:T-08;用途:展示指定成员的隐私、纪念或无权限状态。 -->
|
||||
<template>
|
||||
<view
|
||||
class="member-status-page"
|
||||
@@ -6,220 +6,129 @@
|
||||
'member-status--privacy': statusState === 'privacy',
|
||||
'member-status--deceased': statusState === 'deceased',
|
||||
'member-status--forbidden': statusState === 'forbidden',
|
||||
'member-status--error': statusState === 'error',
|
||||
}"
|
||||
>
|
||||
<ModulePageBackground module="tree" />
|
||||
<view class="member-status-page__header"
|
||||
><PageHeader title="成员状态"
|
||||
/></view>
|
||||
<view class="status-tabs">
|
||||
<view
|
||||
v-for="item in tabs"
|
||||
:key="item.value"
|
||||
class="status-tab"
|
||||
@click="statusState = item.value"
|
||||
>
|
||||
<image
|
||||
:src="
|
||||
statusState === item.value
|
||||
? '/static/assets/foundation/transparent/a01-scroll-primary-v3.png'
|
||||
: '/static/assets/foundation/transparent/a01-scroll-secondary-v3.png'
|
||||
"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text
|
||||
:class="{ 'status-tab__text--active': statusState === item.value }"
|
||||
>{{ item.label }}</text
|
||||
>
|
||||
</view>
|
||||
<view class="member-status-page__header"><PageHeader :title="pageTitle" /></view>
|
||||
|
||||
<view class="member-status-context">
|
||||
<text>{{ genealogyName }}</text>
|
||||
<text v-if="member">{{ member.name }} · 第 {{ member.generation }} 世 · {{ member.branch }}</text>
|
||||
<text v-else>未找到成员身份</text>
|
||||
</view>
|
||||
|
||||
<view class="status-card">
|
||||
<image
|
||||
src="/static/assets/modules/tree/transparent/t01-state-panel.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="status-card__copy">
|
||||
<text>{{ activeStatus.eyebrow }}</text
|
||||
><text>{{ activeStatus.title }}</text
|
||||
><text>{{ activeStatus.copy }}</text>
|
||||
<text>{{ activeStatus.eyebrow }}</text>
|
||||
<text>{{ activeStatus.title }}</text>
|
||||
<text>{{ activeStatus.copy }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="status-guidance">
|
||||
<image
|
||||
src="/static/assets/modules/tree/transparent/t01-state-panel.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view>
|
||||
<text>{{ activeStatus.guideTitle }}</text>
|
||||
<text v-for="line in activeStatus.guides" :key="line">{{ line }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="status-action" @click="handleAction">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
|
||||
<text>{{ activeStatus.action }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
const statusState = ref("privacy");
|
||||
const tabs = [
|
||||
{ value: "privacy", label: "隐私" },
|
||||
{ value: "deceased", label: "纪念" },
|
||||
{ value: "forbidden", label: "无权限" },
|
||||
];
|
||||
import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const personId = ref("");
|
||||
const statusState = ref("loading");
|
||||
const genealogyName = ref("汤氏家谱");
|
||||
const member = ref(null);
|
||||
|
||||
const memberFixtures = {
|
||||
101: { id: 101, name: "汤文远", generation: 12, branch: "主支", allowedStates: ["deceased"] },
|
||||
102: { id: 102, name: "汤正国", generation: 13, branch: "长房", allowedStates: ["privacy"] },
|
||||
103: { id: 103, name: "汤正华", generation: 13, branch: "二房", allowedStates: ["forbidden"] },
|
||||
};
|
||||
const states = {
|
||||
privacy: {
|
||||
eyebrow: "隐私成员",
|
||||
title: "敏感资料只向授权成员展示",
|
||||
copy: "姓名可保留在世系位置,出生信息、联系方式和生平内容根据权限隐藏。",
|
||||
guideTitle: "隐私展示原则",
|
||||
guides: [
|
||||
"本人可以查看和维护自己的完整档案",
|
||||
"谱主可协助处理身份与世系关系",
|
||||
"普通成员只看到被允许公开的内容",
|
||||
],
|
||||
copy: "该成员姓名和世系位置仍然保留,出生信息、联系方式和生平内容按权限隐藏。",
|
||||
guideTitle: "当前可见范围",
|
||||
guides: ["姓名、世代与所属支系可见", "联系方式和详细生平已隐藏", "本人或谱主可维护授权范围"],
|
||||
action: "返回成员档案",
|
||||
},
|
||||
deceased: {
|
||||
eyebrow: "离世成员",
|
||||
title: "在世系中保留温和的纪念状态",
|
||||
copy: "离世不会删除成员关系;档案可继续记录生平、影像和家人的追思内容。",
|
||||
eyebrow: "离世纪念",
|
||||
title: "这位家人的生命记录被温和保留",
|
||||
copy: "离世状态不会删除成员关系;有权限的家人仍可共同维护生卒年月、生平和追思资料。",
|
||||
guideTitle: "纪念资料范围",
|
||||
guides: [
|
||||
"生卒年月与世系关系继续保留",
|
||||
"生平内容由有权限家人共同维护",
|
||||
"敏感资料仍遵守原有隐私设置",
|
||||
],
|
||||
guides: ["生卒年月与世系关系继续保留", "生平内容由有权限家人维护", "敏感资料继续遵守原有隐私设置"],
|
||||
action: "返回成员档案",
|
||||
},
|
||||
forbidden: {
|
||||
eyebrow: "访问受限",
|
||||
title: "当前账号没有查看该档案的权限",
|
||||
copy: "为保护家人隐私,页面不会展示被隐藏的字段,也不会提供绕过权限的入口。",
|
||||
copy: "页面不会展示被隐藏字段,也不会提供绕过家谱权限的入口。",
|
||||
guideTitle: "如何申请查看",
|
||||
guides: [
|
||||
"先确认已经加入对应家谱",
|
||||
"联系谱主说明亲属关系和用途",
|
||||
"权限变更后重新进入成员档案",
|
||||
],
|
||||
guides: ["先确认已经加入对应家谱", "联系谱主说明亲属关系和用途", "权限变更后重新进入成员档案"],
|
||||
action: "返回我的家谱",
|
||||
},
|
||||
error: {
|
||||
eyebrow: "成员状态不可用",
|
||||
title: "没有找到要查看的成员",
|
||||
copy: "请从成员档案或世系树重新选择成员。",
|
||||
guideTitle: "重新进入方式",
|
||||
guides: ["从世系树选择成员节点", "从成员目录打开成员档案", "确认当前家谱仍然有效"],
|
||||
action: "返回上一页",
|
||||
},
|
||||
};
|
||||
const activeStatus = computed(() => states[statusState.value]);
|
||||
const activeStatus = computed(() => states[statusState.value] || states.error);
|
||||
const pageTitle = computed(() => statusState.value === "deceased" ? "成员纪念" : statusState.value === "privacy" ? "隐私资料" : "成员状态");
|
||||
|
||||
onLoad((query) => {
|
||||
statusState.value = states[query.state] ? query.state : "privacy";
|
||||
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
|
||||
personId.value = query.personId || "";
|
||||
if (genealogyId.value) genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
member.value = memberFixtures[personId.value] || null;
|
||||
const requestedState = ["privacy", "deceased", "forbidden"].includes(query.state) ? query.state : "";
|
||||
statusState.value = member.value?.allowedStates.includes(requestedState) ? requestedState : "error";
|
||||
});
|
||||
|
||||
const handleAction = () => {
|
||||
if (statusState.value === "forbidden") {
|
||||
uni.reLaunch({ url: "/pages/genealogy/g01-my-genealogies" });
|
||||
return;
|
||||
}
|
||||
uni.navigateBack();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.member-status-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
background: $paper;
|
||||
}
|
||||
.member-status-page__header,
|
||||
.status-tabs,
|
||||
.status-card,
|
||||
.status-guidance {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.status-tabs {
|
||||
display: flex;
|
||||
gap: 10rpx;
|
||||
padding: 22rpx 24rpx 0;
|
||||
}
|
||||
.status-tab {
|
||||
position: relative;
|
||||
width: calc(33.333% - 7rpx);
|
||||
height: 62rpx;
|
||||
}
|
||||
.status-tab image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.status-tab text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.status-tab .status-tab__text--active {
|
||||
color: #fff9ed;
|
||||
}
|
||||
.status-card {
|
||||
width: calc(100% - 48rpx);
|
||||
height: calc((100vw - 24px) * 0.34286);
|
||||
min-height: 224rpx;
|
||||
margin: 24rpx auto 0;
|
||||
}
|
||||
.status-card > image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.status-card__copy {
|
||||
position: absolute;
|
||||
inset: 18% 10%;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.status-card__copy text {
|
||||
display: block;
|
||||
}
|
||||
.status-card__copy text:first-child {
|
||||
color: $brand-red;
|
||||
font-size: 22rpx;
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
.status-card__copy text:nth-child(2) {
|
||||
margin-top: 7rpx;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.status-card__copy text:last-child {
|
||||
margin-top: 10rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.status-guidance {
|
||||
width: calc(100% - 48rpx);
|
||||
height: min(400px, calc((100vw - 24px) * 0.9));
|
||||
margin: 18rpx auto 0;
|
||||
}
|
||||
.status-guidance > image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.status-guidance > view {
|
||||
position: absolute;
|
||||
inset: 12% 11%;
|
||||
z-index: 1;
|
||||
}
|
||||
.status-guidance text {
|
||||
display: block;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.status-guidance text:first-child {
|
||||
margin-bottom: 18rpx;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.status-guidance text:not(:first-child) {
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
.member-status-page { display: flex; min-height: 100vh; flex-direction: column; padding-bottom: 34rpx; box-sizing: border-box; background: $paper; }
|
||||
.member-status-page__header, .member-status-context, .status-card, .status-guidance, .status-action { z-index: 2; }
|
||||
.member-status-context { width: calc(100% - 32rpx); margin: 18rpx auto 0; padding: 16rpx 24rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
|
||||
.member-status-context text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
|
||||
.member-status-context text:first-child { color: $ink; font-size: 26rpx; font-weight: 700; }
|
||||
.status-card { width: calc(100% - 32rpx); min-height: 330rpx; margin: 18rpx auto 0; padding: 12% 10%; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t01-state-panel.png") center / 100% 100% no-repeat; }
|
||||
.status-card__copy text { display: block; color: $ink-muted; font-size: 24rpx; line-height: 1.55; text-align: center; }
|
||||
.status-card__copy text:first-child { color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
|
||||
.status-card__copy text:nth-child(2) { margin-top: 12rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 32rpx; font-weight: 700; }
|
||||
.status-card__copy text:last-child { margin-top: 14rpx; font-size: 24rpx; }
|
||||
.status-guidance { width: calc(100% - 48rpx); margin: 16rpx auto 0; padding: 24rpx 28rpx; box-sizing: border-box; background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png") center / 100% 100% no-repeat; }
|
||||
.status-guidance text { display: block; color: $ink-muted; font-size: 24rpx; line-height: 1.6; }
|
||||
.status-guidance text:first-child { margin-bottom: 8rpx; color: $ink; font-size: 26rpx; font-weight: 700; }
|
||||
.status-action { display: grid; width: calc(100% - 72rpx); min-height: 76rpx; margin: 22rpx auto 0; }
|
||||
.status-action image, .status-action text { grid-area: 1 / 1; width: 100%; height: 100%; }
|
||||
.status-action text { z-index: 1; display: flex; align-items: center; justify-content: center; color: #fff9ed; font-size: 24rpx; font-weight: 700; }
|
||||
@media (min-width: 400px) { .member-status-context, .status-card { width: calc(100% - 48rpx); } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user