修改完成
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user