完成50%
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="profile-page__header">
|
||||
<PageHeader
|
||||
root
|
||||
title="我的"
|
||||
:action="profileState === 'ready' ? '资料' : ''"
|
||||
@action="toProfile"
|
||||
@@ -40,9 +41,9 @@
|
||||
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>
|
||||
<text class="profile-identity__name">{{ currentUser.name }}</text>
|
||||
<text class="profile-identity__role">{{ currentUser.role }}</text>
|
||||
<text class="profile-identity__phone">{{ currentUser.phone }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -56,7 +57,7 @@
|
||||
>
|
||||
<view class="profile-scroll-notice__copy">
|
||||
<text>待你处理</text>
|
||||
<text>2 条家谱提醒与审核通知</text>
|
||||
<text>{{ unreadCount }} 条家谱提醒与审核通知</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -121,44 +122,52 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppTabbar from "@/components/AppTabbar.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import {
|
||||
currentUser,
|
||||
listNotificationFixtures,
|
||||
} from "@/data/mock.js";
|
||||
import { openPage } from "@/utils/navigation.js";
|
||||
|
||||
const profileState = ref("ready");
|
||||
const unreadCount = computed(
|
||||
() => listNotificationFixtures().filter((notice) => notice.unread).length,
|
||||
);
|
||||
const menuItems = [
|
||||
{
|
||||
label: "账号与安全",
|
||||
note: "密码、手机与设备",
|
||||
note: "密码与手机号",
|
||||
icon: "/static/assets/modules/auth/transparent/a01-icon-lock-v1.png",
|
||||
url: "/pages/profile/m03-security-settings",
|
||||
routeKey: "M03",
|
||||
},
|
||||
{
|
||||
label: "帮助与反馈",
|
||||
note: "使用说明与问题反馈",
|
||||
icon: "/static/assets/foundation/transparent/notice.png",
|
||||
url: "/pages/profile/m06-help-center",
|
||||
routeKey: "M06",
|
||||
},
|
||||
{
|
||||
label: "关于家谱",
|
||||
note: "协议、隐私与版本",
|
||||
icon: "/static/assets/foundation/transparent/brand-seal.png",
|
||||
url: "/pages/profile/m10-about-settings",
|
||||
routeKey: "M10",
|
||||
},
|
||||
{
|
||||
label: "邀请家人",
|
||||
note: "邀请码与家谱推广",
|
||||
note: "邀请规则与接入状态",
|
||||
icon: "/static/assets/foundation/transparent/brand-seal.png",
|
||||
url: "/pages/profile/m08-promotion",
|
||||
routeKey: "M08",
|
||||
},
|
||||
{
|
||||
label: "服务与订单",
|
||||
note: "权益说明与订单记录",
|
||||
icon: "/static/assets/foundation/transparent/notice.png",
|
||||
url: "/pages/profile/m09-vip-orders",
|
||||
routeKey: "M09",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -169,11 +178,10 @@ onLoad((query) => {
|
||||
const restoreProfile = () => {
|
||||
profileState.value = "ready";
|
||||
};
|
||||
const toProfile = () =>
|
||||
uni.navigateTo({ url: "/pages/profile/m02-edit-profile" });
|
||||
const toNotifications = () =>
|
||||
uni.navigateTo({ url: "/pages/notification/n01-message-center" });
|
||||
const openItem = (item) => uni.navigateTo({ url: item.url });
|
||||
const toProfile = () => openPage("M02", {}, "M01");
|
||||
const toNotifications = () => openPage("N01", {}, "M01");
|
||||
const openItem = (item) =>
|
||||
item.routeKey ? openPage(item.routeKey, {}, "M01") : Promise.resolve(false);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -2,56 +2,102 @@
|
||||
<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-layer"><PageHeader title="个人资料" custom-back @back="requestBack" /></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="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 class="form-row"><text>昵称</text><input v-model.trim="profileForm.nickName" maxlength="30" aria-label="昵称" placeholder="请输入昵称" @input="errors.nickName = ''" /></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="30" aria-label="真实姓名" placeholder="请输入真实姓名" /></view>
|
||||
<view class="form-row"><text>邮箱</text><input v-model.trim="profileForm.email" maxlength="100" aria-label="邮箱" placeholder="选填,用于接收通知" @input="errors.email = ''" /></view>
|
||||
<text v-if="errors.email" class="field-error">{{ errors.email }}</text>
|
||||
</view>
|
||||
<AppButton block :disabled="profileState === 'saving'" :label="profileState === 'saving' ? '正在保存' : '保存资料'" @click="saveProfile" />
|
||||
<AppButton block :disabled="profileState === 'saving'" :label="profileState === 'saving' ? '正在校验' : '生成本地校验预览'" @click="saveProfile" />
|
||||
</view>
|
||||
<AppToast :visible="toastVisible" :message="toastMessage" />
|
||||
<AppDialog
|
||||
:visible="discardVisible"
|
||||
:close-on-mask="false"
|
||||
eyebrow="放弃确认"
|
||||
title="放弃当前填写?"
|
||||
message="尚未提交服务器的资料将从当前页面清除。"
|
||||
confirm-text="确认放弃"
|
||||
cancel-text="继续填写"
|
||||
show-cancel
|
||||
@confirm="confirmDiscard"
|
||||
@cancel="cancelDiscard"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onUnmounted, reactive, ref } from "vue";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onBackPress, onUnload } from "@dcloudio/uni-app";
|
||||
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";
|
||||
import { currentUser } from "@/data/mock.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import {
|
||||
handleBackPress,
|
||||
runBackGuard,
|
||||
} from "@/utils/navigation.js";
|
||||
|
||||
const profileForm = reactive({ nickname: "汤文清", realName: "汤文清", region: "河南省 周口市", bio: "热心参与家谱资料整理。" });
|
||||
const errors = reactive({ nickname: "", bio: "" });
|
||||
const profileForm = reactive({ nickName: currentUser.name, realName: currentUser.name, email: "" });
|
||||
const errors = reactive({ nickName: "", email: "" });
|
||||
const profileState = ref("ready");
|
||||
const toastVisible = ref(false);
|
||||
const toastMessage = ref("");
|
||||
const discardVisible = ref(false);
|
||||
let timer = null;
|
||||
const formSnapshot = computed(() => JSON.stringify(profileForm));
|
||||
const baseline = ref(formSnapshot.value);
|
||||
const isDirty = computed(() => formSnapshot.value !== baseline.value);
|
||||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||||
discardVisible.value = visible;
|
||||
});
|
||||
const requestDiscardConfirmation = discardConfirmation.request;
|
||||
const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
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;
|
||||
errors.nickName = profileForm.nickName ? "" : "请填写昵称";
|
||||
errors.email = !profileForm.email || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(profileForm.email)
|
||||
? ""
|
||||
: "请输入正确的邮箱地址";
|
||||
return !errors.nickName && !errors.email;
|
||||
};
|
||||
const saveProfile = () => {
|
||||
if (!validateProfile() || profileState.value === "saving") return;
|
||||
profileState.value = "saving";
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => { profileState.value = "ready"; showToast("个人资料已保存"); }, 500);
|
||||
timer = setTimeout(() => {
|
||||
profileState.value = "ready";
|
||||
showToast("本地校验通过,尚未提交服务器");
|
||||
}, 500);
|
||||
};
|
||||
onUnmounted(() => clearTimeout(timer));
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: discardVisible.value,
|
||||
dirty: isDirty.value,
|
||||
submitting: profileState.value === "saving",
|
||||
"close-transient": cancelDiscard,
|
||||
"block-submitting": () => true,
|
||||
"confirm-discard": requestDiscardConfirmation,
|
||||
});
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
onUnload(() => {
|
||||
clearTimeout(timer);
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -70,11 +116,8 @@ onUnmounted(() => clearTimeout(timer));
|
||||
.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 > 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)}}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<!-- 页面编号:M-03;用途:账号安全总览与安全功能入口。 -->
|
||||
<template>
|
||||
<view class="security-page device-state--safe">
|
||||
<view class="security-page device-state--limited">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="账号与安全" /></view>
|
||||
<view class="page-content page-layer">
|
||||
<view class="security-summary">
|
||||
<text>账号状态安全</text>
|
||||
<text>手机号已绑定,最近登录设备未发现异常。</text>
|
||||
<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)">
|
||||
@@ -15,7 +15,7 @@
|
||||
<image class="chevron" src="/static/assets/foundation/transparent/chevron-right.png" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
<AppButton block type="secondary" label="检查账号安全" @click="checkSecurity" />
|
||||
<AppButton block type="secondary" label="查看接入状态" @click="checkSecurity" />
|
||||
</view>
|
||||
<AppToast :visible="toastVisible" :message="toastMessage" />
|
||||
</view>
|
||||
@@ -27,18 +27,19 @@ import AppButton from "@/components/AppButton.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { currentUser } from "@/data/mock.js";
|
||||
import { openPage } from "@/utils/navigation.js";
|
||||
|
||||
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" },
|
||||
{ key: "password", label: "登录密码", note: "建议定期更新密码", icon: "/static/assets/modules/auth/transparent/a01-icon-lock-v1.png", routeKey: "M04" },
|
||||
{ key: "phone", label: "绑定手机号", note: currentUser.phone, icon: "/static/assets/modules/auth/transparent/a01-icon-phone-v1.png", routeKey: "M05" },
|
||||
];
|
||||
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("安全检查完成,当前账号状态正常");
|
||||
const openSecurityItem = (item) => openPage(item.routeKey, {}, "M03");
|
||||
const checkSecurity = () => showToast("账号安全接口将在后续独立批次接入");
|
||||
onUnmounted(() => clearTimeout(timer));
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<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-layer"><PageHeader title="修改密码" custom-back @back="requestBack" /></view>
|
||||
<view class="page-content page-layer">
|
||||
<view class="security-tip"><text>设置安全密码</text><text>建议使用 8–32 位字母与数字组合,不要与其他应用共用。</text></view>
|
||||
<view class="form-panel">
|
||||
@@ -15,18 +15,34 @@
|
||||
<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" />
|
||||
<AppButton block :disabled="passwordState === 'saving'" :label="passwordState === 'saving' ? '正在校验' : '校验新密码(不提交)'" @click="savePassword" />
|
||||
</view>
|
||||
<AppToast :visible="toastVisible" :message="toastMessage" />
|
||||
<AppDialog
|
||||
:visible="discardVisible"
|
||||
:close-on-mask="false"
|
||||
eyebrow="放弃确认"
|
||||
title="放弃密码填写?"
|
||||
message="当前密码和新密码尚未提交服务器。"
|
||||
confirm-text="确认放弃"
|
||||
cancel-text="继续填写"
|
||||
show-cancel
|
||||
@confirm="confirmDiscard"
|
||||
@cancel="cancelDiscard"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onUnmounted, reactive, ref } from "vue";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onBackPress, onUnload } from "@dcloudio/uni-app";
|
||||
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";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import { handleBackPress, runBackGuard } from "@/utils/navigation.js";
|
||||
import {
|
||||
PASSWORD_POLICY_MESSAGE,
|
||||
validatePassword,
|
||||
@@ -36,12 +52,22 @@ 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 discardVisible = ref(false);
|
||||
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 formSnapshot = computed(() => JSON.stringify(passwordForm));
|
||||
const baseline = ref(formSnapshot.value);
|
||||
const isDirty = computed(() => formSnapshot.value !== baseline.value);
|
||||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||||
discardVisible.value = visible;
|
||||
});
|
||||
const requestDiscardConfirmation = discardConfirmation.request;
|
||||
const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
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 validateForm = () => {
|
||||
@@ -56,9 +82,25 @@ const validateForm = () => {
|
||||
const savePassword = () => {
|
||||
if (!validateForm() || passwordState.value === "saving") return;
|
||||
passwordState.value = "saving";
|
||||
timer = setTimeout(() => { passwordState.value = "ready"; passwordForm.current = ""; passwordForm.next = ""; passwordForm.confirm = ""; showToast("密码已修改,请妥善保管新密码"); }, 500);
|
||||
timer = setTimeout(() => {
|
||||
passwordState.value = "ready";
|
||||
showToast("本地校验通过,尚未提交服务器");
|
||||
}, 500);
|
||||
};
|
||||
onUnmounted(() => clearTimeout(timer));
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: discardVisible.value,
|
||||
dirty: isDirty.value,
|
||||
submitting: passwordState.value === "saving",
|
||||
"close-transient": cancelDiscard,
|
||||
"block-submitting": () => true,
|
||||
"confirm-discard": requestDiscardConfirmation,
|
||||
});
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
onUnload(() => {
|
||||
clearTimeout(timer);
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -2,52 +2,94 @@
|
||||
<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-layer"><PageHeader title="修改手机号" custom-back @back="requestBack" /></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>
|
||||
<view class="form-row code-row"><text>验证码</text><input v-model.trim="phoneForm.code" type="number" maxlength="4" aria-label="短信验证码" placeholder="4 位验证码" @input="errors.code = ''" /><view class="code-action" role="button" aria-label="检查验证码发送条件" @click="sendCode">发送条件</view></view>
|
||||
<text v-if="errors.code" class="field-error">{{ errors.code }}</text>
|
||||
</view>
|
||||
<AppButton block :disabled="phoneState === 'saving'" :label="phoneState === 'saving' ? '正在更换' : '确认更换'" @click="savePhone" />
|
||||
<AppButton block :disabled="phoneState === 'saving'" :label="phoneState === 'saving' ? '正在校验' : '校验换绑信息(不提交)'" @click="savePhone" />
|
||||
</view>
|
||||
<AppToast :visible="toastVisible" :message="toastMessage" />
|
||||
<AppDialog
|
||||
:visible="discardVisible"
|
||||
:close-on-mask="false"
|
||||
eyebrow="放弃确认"
|
||||
title="放弃换绑填写?"
|
||||
message="新手机号和验证码尚未提交服务器。"
|
||||
confirm-text="确认放弃"
|
||||
cancel-text="继续填写"
|
||||
show-cancel
|
||||
@confirm="confirmDiscard"
|
||||
@cancel="cancelDiscard"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onUnmounted, reactive, ref } from "vue";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onBackPress, onUnload } from "@dcloudio/uni-app";
|
||||
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";
|
||||
import { currentUser } from "@/data/mock.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import { handleBackPress, runBackGuard } from "@/utils/navigation.js";
|
||||
|
||||
const phoneForm = reactive({ currentPhone: "139****6421", newPhone: "", code: "" });
|
||||
const phoneForm = reactive({ currentPhone: currentUser.phone, newPhone: "", code: "" });
|
||||
const errors = reactive({ newPhone: "", code: "" });
|
||||
const phoneState = ref("ready");
|
||||
const codeCountdown = ref(0);
|
||||
const discardVisible = ref(false);
|
||||
const toastVisible = ref(false); const toastMessage = ref("");
|
||||
let countdownTimer = null; let stateTimer = null; let toastTimer = null;
|
||||
let stateTimer = null; let toastTimer = null;
|
||||
const formSnapshot = computed(() => JSON.stringify({ newPhone: phoneForm.newPhone, code: phoneForm.code }));
|
||||
const baseline = ref(formSnapshot.value);
|
||||
const isDirty = computed(() => formSnapshot.value !== baseline.value);
|
||||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||||
discardVisible.value = visible;
|
||||
});
|
||||
const requestDiscardConfirmation = discardConfirmation.request;
|
||||
const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
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);
|
||||
errors.newPhone = "";
|
||||
showToast("需先完成滑动行为验证;当前未发送验证码");
|
||||
};
|
||||
const validatePhone = () => {
|
||||
errors.newPhone = /^1\d{10}$/.test(phoneForm.newPhone) ? "" : "请输入正确的新手机号";
|
||||
errors.code = /^\d{6}$/.test(phoneForm.code) ? "" : "请输入 6 位验证码";
|
||||
errors.code = /^\d{4}$/.test(phoneForm.code) ? "" : "请输入 4 位验证码";
|
||||
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);
|
||||
stateTimer = setTimeout(() => {
|
||||
phoneState.value = "ready";
|
||||
showToast("本地校验通过,尚未提交服务器");
|
||||
}, 500);
|
||||
};
|
||||
onUnmounted(() => { clearInterval(countdownTimer); clearTimeout(stateTimer); clearTimeout(toastTimer); });
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: discardVisible.value,
|
||||
dirty: isDirty.value,
|
||||
submitting: phoneState.value === "saving",
|
||||
"close-transient": cancelDiscard,
|
||||
"block-submitting": () => true,
|
||||
"confirm-discard": requestDiscardConfirmation,
|
||||
});
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
onUnload(() => {
|
||||
clearTimeout(stateTimer);
|
||||
clearTimeout(toastTimer);
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -23,6 +23,7 @@ import { computed, ref } from "vue";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { openPage } from "@/utils/navigation.js";
|
||||
|
||||
const helpCategories = ["全部", "家谱", "成员", "隐私", "账号"];
|
||||
const activeCategory = ref("全部");
|
||||
@@ -30,7 +31,7 @@ const keyword = ref("");
|
||||
const expandedIds = ref([]);
|
||||
const questions = [
|
||||
{ id: 1, category: "家谱", question: "如何创建一部新家谱?", answer: "进入“我的家谱”,选择新建家谱,按步骤填写姓氏、堂号和地区等基础资料。" },
|
||||
{ id: 2, category: "成员", question: "如何邀请家人共同完善家谱?", answer: "家人可搜索家谱后提交加入申请,管理员核实亲属关系后完成审核。" },
|
||||
{ id: 2, category: "成员", question: "如何邀请家人共同完善家谱?", answer: "家人可搜索家谱后提交加入申请,由管理员核实关系;邀请码尚未接入,未来校验成功后将直接加入,不产生审核记录。" },
|
||||
{ id: 3, category: "隐私", question: "哪些个人资料会展示给其他成员?", answer: "资料按家谱角色与权限展示;敏感联系方式默认脱敏,后续可在家谱设置中管理权限。" },
|
||||
{ id: 4, category: "账号", question: "忘记密码后怎样恢复账号?", answer: "在登录页选择“忘记密码”,通过绑定手机号验证后设置新密码。" },
|
||||
{ id: 5, category: "家谱", question: "家谱资料填写错了怎么办?", answer: "有编辑权限的成员可以进入对应资料页修改;关键世系关系建议核对后再保存。" },
|
||||
@@ -40,7 +41,7 @@ const filteredQuestions = computed(() => {
|
||||
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" });
|
||||
const contactSupport = () => openPage("M07", {}, "M06");
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
+234
-24
@@ -1,53 +1,263 @@
|
||||
<!-- 页面编号:M-07;用途:提交意见反馈。 -->
|
||||
<template>
|
||||
<view class="feedback-page" :class="{ 'feedback-state--ready': feedbackState === 'ready', 'feedback-state--submitting': feedbackState === 'submitting' }">
|
||||
<view
|
||||
class="feedback-page"
|
||||
:class="{
|
||||
'feedback-state--ready': feedbackState === 'ready',
|
||||
'feedback-state--submitting': feedbackState === 'submitting',
|
||||
'feedback-state--success': feedbackState === 'success',
|
||||
'feedback-state--error': feedbackState === 'error',
|
||||
'feedback-state--uncertain': feedbackState === 'uncertain',
|
||||
}"
|
||||
>
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="意见反馈" /></view>
|
||||
<view class="page-layer"><PageHeader title="意见反馈" custom-back @back="requestBack" /></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 class="type-field" role="group" aria-label="反馈类型(选填)" aria-describedby="feedback-type-note">
|
||||
<text id="feedback-type-note" class="field-note">反馈类型为选填,可再次点击取消选择。</text>
|
||||
<view class="type-grid">
|
||||
<button
|
||||
v-for="type in feedbackTypes"
|
||||
:key="type"
|
||||
class="type-chip"
|
||||
:class="{ active: feedbackForm.feedbackType === type }"
|
||||
:aria-pressed="feedbackForm.feedbackType === type"
|
||||
:disabled="feedbackState === 'submitting'"
|
||||
@click="selectFeedbackType(type)"
|
||||
>{{ type }}</button>
|
||||
</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>
|
||||
<textarea
|
||||
v-model.trim="feedbackForm.feedbackContent"
|
||||
auto-height
|
||||
maxlength="500"
|
||||
aria-label="问题描述"
|
||||
aria-required="true"
|
||||
:aria-describedby="errors.feedbackContent ? 'feedback-content-help feedback-content-error' : 'feedback-content-help'"
|
||||
:aria-invalid="Boolean(errors.feedbackContent)"
|
||||
:disabled="feedbackState === 'submitting'"
|
||||
:focus="feedbackContentFocused"
|
||||
placeholder="请说明遇到的问题、操作步骤或建议"
|
||||
@input="errors.feedbackContent = ''"
|
||||
@blur="feedbackContentFocused = false"
|
||||
/>
|
||||
<text class="counter">{{ feedbackForm.feedbackContent.length }}/500</text>
|
||||
<text id="feedback-content-help" class="field-note">请勿填写密码、验证码等敏感信息。</text>
|
||||
<text v-if="errors.feedbackContent" id="feedback-content-error" class="field-error">{{ errors.feedbackContent }}</text>
|
||||
<view class="contact-row">
|
||||
<text>联系方式</text>
|
||||
<input
|
||||
v-model.trim="feedbackForm.contactInfo"
|
||||
maxlength="50"
|
||||
aria-label="联系方式(选填)"
|
||||
:disabled="feedbackState === 'submitting'"
|
||||
placeholder="手机号或邮箱(选填)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<text class="privacy-note">仅用于跟进本次反馈,不会在家谱中公开。</text>
|
||||
<AppButton block :disabled="feedbackState === 'submitting'" :label="feedbackState === 'submitting' ? '正在提交' : '提交反馈'" @click="submitFeedback" />
|
||||
<text class="privacy-note">联系方式选填,便于需要时核实本次反馈。</text>
|
||||
<view
|
||||
v-if="feedbackResult"
|
||||
class="feedback-result"
|
||||
:class="{
|
||||
'feedback-result--success': feedbackResultTone === 'success',
|
||||
'feedback-result--error': feedbackResultTone === 'error',
|
||||
'feedback-result--uncertain': feedbackResultTone === 'uncertain',
|
||||
}"
|
||||
:role="feedbackResultTone === 'error' ? 'alert' : 'status'"
|
||||
:aria-live="feedbackResultTone === 'error' ? 'assertive' : 'polite'"
|
||||
>{{ feedbackResult }}</view>
|
||||
<AppButton block :disabled="submitDisabled" :label="submitLabel" @click="submitFeedback" />
|
||||
</view>
|
||||
<AppToast :visible="toastVisible" :message="toastMessage" />
|
||||
<AppDialog
|
||||
:visible="discardVisible"
|
||||
:close-on-mask="false"
|
||||
eyebrow="放弃确认"
|
||||
title="放弃当前反馈?"
|
||||
message="当前修改尚未提交,离开后将清除这些修改。"
|
||||
confirm-text="确认放弃"
|
||||
cancel-text="继续填写"
|
||||
show-cancel
|
||||
@confirm="confirmDiscard"
|
||||
@cancel="cancelDiscard"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onUnmounted, reactive, ref } from "vue";
|
||||
import { computed, nextTick, reactive, ref, watch } from "vue";
|
||||
import { onBackPress, onUnload } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import { handleBackPress, runBackGuard } from "@/utils/navigation.js";
|
||||
|
||||
const feedbackTypes = ["功能问题", "使用建议", "内容纠错", "其他"];
|
||||
const feedbackForm = reactive({ type: "", description: "", contact: "" });
|
||||
const errors = reactive({ type: "", description: "" });
|
||||
const feedbackForm = reactive({ feedbackType: "", feedbackContent: "", contactInfo: "" });
|
||||
const errors = reactive({ feedbackContent: "" });
|
||||
const feedbackState = ref("ready");
|
||||
const toastVisible = ref(false); const toastMessage = ref(""); let timer = null;
|
||||
const feedbackResult = ref("");
|
||||
const feedbackResultTone = ref("");
|
||||
const feedbackContentFocused = ref(false);
|
||||
const discardVisible = ref(false);
|
||||
const lastSubmittedSnapshot = ref("");
|
||||
const lastUncertainSnapshot = ref("");
|
||||
let pageActive = true;
|
||||
const requestController = createRequestController();
|
||||
const normalizedForm = computed(() => ({
|
||||
feedbackType: feedbackForm.feedbackType.trim(),
|
||||
feedbackContent: feedbackForm.feedbackContent.trim(),
|
||||
contactInfo: feedbackForm.contactInfo.trim(),
|
||||
}));
|
||||
const formSnapshot = computed(() => JSON.stringify(normalizedForm.value));
|
||||
const baseline = ref(formSnapshot.value);
|
||||
const isDirty = computed(() => formSnapshot.value !== baseline.value);
|
||||
const submitDisabled = computed(() =>
|
||||
feedbackState.value === "submitting" ||
|
||||
(Boolean(lastSubmittedSnapshot.value) && formSnapshot.value === lastSubmittedSnapshot.value) ||
|
||||
(Boolean(lastUncertainSnapshot.value) && formSnapshot.value === lastUncertainSnapshot.value),
|
||||
);
|
||||
const submitLabel = computed(() => {
|
||||
if (feedbackState.value === "submitting") return "正在提交";
|
||||
if (feedbackState.value === "success") return "反馈已提交";
|
||||
if (feedbackState.value === "uncertain") return "提交结果待确认";
|
||||
if (feedbackState.value === "error") return "重新提交";
|
||||
return "提交反馈";
|
||||
});
|
||||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||||
discardVisible.value = visible;
|
||||
});
|
||||
const requestDiscardConfirmation = discardConfirmation.request;
|
||||
const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
const validateFeedback = () => {
|
||||
errors.type = feedbackForm.type ? "" : "请选择反馈类型";
|
||||
errors.description = feedbackForm.description.length < 5 ? "请至少填写 5 个字的问题描述" : "";
|
||||
return !errors.type && !errors.description;
|
||||
errors.feedbackContent = feedbackForm.feedbackContent.trim() ? "" : "请填写问题描述";
|
||||
if (errors.feedbackContent) {
|
||||
feedbackContentFocused.value = false;
|
||||
nextTick(() => {
|
||||
if (pageActive) feedbackContentFocused.value = true;
|
||||
});
|
||||
}
|
||||
return !errors.feedbackContent;
|
||||
};
|
||||
const submitFeedback = () => {
|
||||
const selectFeedbackType = (type) => {
|
||||
if (feedbackState.value === "submitting") return;
|
||||
feedbackForm.feedbackType = feedbackForm.feedbackType === type ? "" : type;
|
||||
};
|
||||
const feedbackErrorMessage = (error) => {
|
||||
if (error?.code === "WRITE_UNAVAILABLE") {
|
||||
return "当前为本地预览模式,反馈未提交服务器。";
|
||||
}
|
||||
if (error?.httpStatus === 401) {
|
||||
return "登录状态已失效,服务器未确认提交成功。请重新登录后再处理。";
|
||||
}
|
||||
return error?.message ? `服务器未确认提交成功:${error.message}` : "服务器未确认提交成功,请稍后再试。";
|
||||
};
|
||||
const submittedFeedbackMessage = "反馈已提交。以下内容为本次提交记录;修改任一项后可提交新反馈。";
|
||||
const submittedWithPendingEditsMessage = "上一份反馈已提交,当前修改尚未提交。";
|
||||
const uncertainFeedbackMessage = "提交结果可能未知。为避免重复记录,当前内容不能直接重提;请稍后确认或修改后提交新反馈。";
|
||||
const uncertainWithPendingEditsMessage = "上一份反馈的提交结果仍待确认,当前修改尚未提交。";
|
||||
const isFeedbackResultUncertain = (error) => {
|
||||
if (["REQUEST_TIMEOUT", "NETWORK_ERROR", "RESPONSE_INVALID"].includes(error?.code)) {
|
||||
return true;
|
||||
}
|
||||
if (error?.code !== "HTTP_ERROR") return false;
|
||||
const status = error.httpStatus;
|
||||
return (status >= 200 && status < 400) || status === 408 || status >= 500;
|
||||
};
|
||||
const submitFeedback = async () => {
|
||||
if (!validateFeedback() || feedbackState.value === "submitting") return;
|
||||
if (
|
||||
(lastSubmittedSnapshot.value && formSnapshot.value === lastSubmittedSnapshot.value) ||
|
||||
(lastUncertainSnapshot.value && formSnapshot.value === lastUncertainSnapshot.value)
|
||||
) return;
|
||||
const submittedSnapshot = formSnapshot.value;
|
||||
const submittedPayload = JSON.parse(submittedSnapshot);
|
||||
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);
|
||||
feedbackResult.value = "";
|
||||
feedbackResultTone.value = "";
|
||||
try {
|
||||
await appApi.submitFeedback(submittedPayload, { requestController });
|
||||
if (!pageActive) return;
|
||||
baseline.value = submittedSnapshot;
|
||||
lastSubmittedSnapshot.value = submittedSnapshot;
|
||||
feedbackResultTone.value = "success";
|
||||
if (formSnapshot.value === submittedSnapshot) {
|
||||
feedbackState.value = "success";
|
||||
feedbackResult.value = submittedFeedbackMessage;
|
||||
} else {
|
||||
feedbackState.value = "ready";
|
||||
feedbackResult.value = submittedWithPendingEditsMessage;
|
||||
}
|
||||
} catch (error) {
|
||||
if (!pageActive || isRequestCancelled(error)) return;
|
||||
if (isFeedbackResultUncertain(error)) {
|
||||
lastUncertainSnapshot.value = submittedSnapshot;
|
||||
feedbackResultTone.value = "uncertain";
|
||||
if (formSnapshot.value === submittedSnapshot) {
|
||||
feedbackState.value = "uncertain";
|
||||
feedbackResult.value = uncertainFeedbackMessage;
|
||||
} else {
|
||||
feedbackState.value = "ready";
|
||||
feedbackResult.value = uncertainWithPendingEditsMessage;
|
||||
}
|
||||
return;
|
||||
}
|
||||
feedbackState.value = "error";
|
||||
feedbackResult.value = feedbackErrorMessage(error);
|
||||
feedbackResultTone.value = "error";
|
||||
}
|
||||
};
|
||||
onUnmounted(() => clearTimeout(timer));
|
||||
watch(formSnapshot, (snapshot) => {
|
||||
if (feedbackState.value === "submitting") return;
|
||||
errors.feedbackContent = "";
|
||||
if (lastSubmittedSnapshot.value && snapshot === lastSubmittedSnapshot.value) {
|
||||
feedbackState.value = "success";
|
||||
feedbackResult.value = submittedFeedbackMessage;
|
||||
feedbackResultTone.value = "success";
|
||||
return;
|
||||
}
|
||||
if (lastUncertainSnapshot.value && snapshot === lastUncertainSnapshot.value) {
|
||||
feedbackState.value = "uncertain";
|
||||
feedbackResult.value = uncertainFeedbackMessage;
|
||||
feedbackResultTone.value = "uncertain";
|
||||
return;
|
||||
}
|
||||
feedbackState.value = "ready";
|
||||
if (lastUncertainSnapshot.value) {
|
||||
feedbackResult.value = uncertainWithPendingEditsMessage;
|
||||
feedbackResultTone.value = "uncertain";
|
||||
} else if (lastSubmittedSnapshot.value) {
|
||||
feedbackResult.value = submittedWithPendingEditsMessage;
|
||||
feedbackResultTone.value = "success";
|
||||
} else {
|
||||
feedbackResult.value = "";
|
||||
feedbackResultTone.value = "";
|
||||
}
|
||||
});
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: discardVisible.value,
|
||||
dirty: isDirty.value,
|
||||
submitting: feedbackState.value === "submitting",
|
||||
"close-transient": cancelDiscard,
|
||||
"block-submitting": () => true,
|
||||
"confirm-discard": requestDiscardConfirmation,
|
||||
});
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
onUnload(() => {
|
||||
pageActive = false;
|
||||
requestController.abort();
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
.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{@include adaptive.adaptive-profile-field;display:flex;min-height:76rpx;align-items:center;justify-content:center;padding:10rpx 16rpx;color:$ink-muted;font-size:22rpx;text-align:center}.type-chip.active{color:$brand-red;font-weight:700;filter:saturate(1.2)}.form-panel{@include adaptive.adaptive-profile-content;margin-top:20rpx;padding:30rpx 34rpx}.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}}
|
||||
.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-field{margin-top:20rpx}.field-note{display:block;color:$ink-muted;font-size:20rpx;line-height:1.5}.type-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14rpx;margin-top:10rpx}.type-chip{margin:0;padding:0;border:0;background:transparent;line-height:normal;@include adaptive.adaptive-profile-field;display:flex;min-height:88rpx;align-items:center;justify-content:center;color:$ink-muted;font-size:22rpx;text-align:center}.type-chip::after{border:0}.type-chip.active{color:$brand-red;font-weight:700;filter:saturate(1.2)}.type-chip[disabled]{opacity:.55}.form-panel{@include adaptive.adaptive-profile-content;margin-top:20rpx;padding:30rpx 34rpx}.form-panel textarea{display:block;width:auto;min-width:0;min-height:210rpx;color:$ink;font-size:24rpx;line-height:1.65}.form-panel textarea[disabled],.contact-row input[disabled]{opacity:.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}.feedback-result{margin-top:18rpx;padding:18rpx 20rpx;border:1px solid rgba(122,91,46,.24);border-radius:12rpx;font-size:21rpx;line-height:1.55}.feedback-result--success{border-color:rgba(41,112,66,.34);background:rgba(234,246,237,.88);color:#245f39}.feedback-result--error{border-color:rgba(180,35,24,.3);background:rgba(255,241,239,.9);color:#8f241c}.feedback-result--uncertain{border-color:rgba(155,101,22,.34);background:rgba(255,248,229,.92);color:#76500f}.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,47 +1,46 @@
|
||||
<!-- 页面编号:M-08;用途:邀请家人共建家谱。 -->
|
||||
<template>
|
||||
<view class="promotion-page share-state--ready">
|
||||
<view class="promotion-page share-state--unavailable">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="邀请家人" /></view>
|
||||
<view class="page-layer"><PageHeader title="邀请家人" custom-back @back="requestBack" /></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>
|
||||
<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 v-if="inviteState === 'unavailable'" class="unavailable-card">
|
||||
<text>当前没有可用邀请码</text>
|
||||
<text>当前尚未接入邀请码校验与签发接口,页面不会生成、复制或展示虚假邀请码。</text>
|
||||
</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 class="steps-card"><text>开放条件</text><text>1. 后端提供邀请码签发接口</text><text>2. 明确有效期、使用次数和失效规则</text><text>3. 校验家谱权限与直接加入结果</text></view>
|
||||
<AppButton block type="secondary" label="查看邀请说明" @click="openInviteExplanation" />
|
||||
</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" />
|
||||
<AppDialog :visible="explanationVisible" :close-on-mask="false" eyebrow="邀请说明" title="功能尚未接入" message="当前后端接口文档没有邀请码签发与校验合同。完成接口接入前,本页保持不可用,避免家人拿到无法验证的邀请码。" confirm-text="我知道了" @confirm="explanationVisible = false" @close="explanationVisible = false" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onUnmounted, ref } from "vue";
|
||||
import { ref } from "vue";
|
||||
import { onBackPress } from "@dcloudio/uni-app";
|
||||
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";
|
||||
import { handleBackPress, runBackGuard } from "@/utils/navigation.js";
|
||||
|
||||
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));
|
||||
const inviteState = ref("unavailable");
|
||||
const explanationVisible = ref(false);
|
||||
const openInviteExplanation = () => { explanationVisible.value = true; };
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: explanationVisible.value,
|
||||
"close-transient": () => { explanationVisible.value = false; },
|
||||
});
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
.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{@include adaptive.adaptive-profile-content}.invite-hero{display:flex;min-height:300rpx;flex-direction:column;align-items:center;justify-content:center;padding:38rpx 48rpx;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;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;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}}
|
||||
.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,.unavailable-card,.steps-card{@include adaptive.adaptive-profile-content}.invite-hero{display:flex;min-height:300rpx;flex-direction:column;align-items:center;justify-content:center;padding:38rpx 48rpx;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}.unavailable-card{min-height:190rpx;margin-top:20rpx;padding:40rpx 44rpx;text-align:center}.unavailable-card text{display:block}.unavailable-card text:first-child{color:$brand-red;font-size:28rpx;font-weight:700}.unavailable-card text:last-child{margin-top:12rpx;color:$ink-muted;font-size:21rpx;line-height:1.55}.steps-card{display:flex;min-height:220rpx;flex-direction:column;gap:10rpx;margin-top:20rpx;padding:34rpx 44rpx;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}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}}
|
||||
</style>
|
||||
|
||||
@@ -1,40 +1,45 @@
|
||||
<!-- 页面编号:M-09;用途:服务权益与订单记录。 -->
|
||||
<template>
|
||||
<view class="orders-page" :class="orders.length ? 'order-state--ready' : 'order-state--empty'">
|
||||
<view class="orders-page order-state--unavailable">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="VIP 与订单" /></view>
|
||||
<view class="page-layer"><PageHeader title="VIP 与订单" custom-back @back="requestBack" /></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>
|
||||
<view class="section-heading"><text>订单记录</text><text>暂不可用</text></view>
|
||||
<view v-if="orderState === 'unavailable'" 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" />
|
||||
<AppDialog :visible="serviceNoticeVisible" :close-on-mask="false" eyebrow="服务说明" title="付费服务尚未开放" message="当前版本不会产生扣费或订单。未来开放前会明确展示价格、权益、续费与退款规则。" confirm-text="我知道了" @confirm="serviceNoticeVisible = false" @close="serviceNoticeVisible = false" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onBackPress } 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";
|
||||
import { handleBackPress, runBackGuard } from "@/utils/navigation.js";
|
||||
|
||||
const serviceBenefits = [
|
||||
{ title: "更大存储", copy: "为家族影像和资料提供更多空间" },
|
||||
{ title: "资料导出", copy: "按规则整理并导出家谱资料" },
|
||||
{ title: "专属服务", copy: "获得家谱整理与使用支持" },
|
||||
];
|
||||
const orders = ref([]);
|
||||
const orderState = ref("unavailable");
|
||||
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; };
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: serviceNoticeVisible.value,
|
||||
"close-transient": () => { serviceNoticeVisible.value = false; },
|
||||
});
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
.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{@include adaptive.adaptive-profile-content}.service-card{min-height:220rpx;padding:42rpx 48rpx;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;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;margin-top:18rpx}.order-card{display:grid;grid-template-columns:minmax(0,1fr) auto;min-height:130rpx;align-items:center;gap:20rpx;padding:26rpx 34rpx}.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;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}}
|
||||
.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,.empty-card{@include adaptive.adaptive-profile-content}.service-card{min-height:220rpx;padding:42rpx 48rpx;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;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}.empty-card{min-height:190rpx;padding:48rpx 42rpx;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}}
|
||||
</style>
|
||||
|
||||
@@ -2,39 +2,55 @@
|
||||
<template>
|
||||
<view class="about-page">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="关于家谱" /></view>
|
||||
<view class="page-layer"><PageHeader title="关于家谱" custom-back @back="requestBack" /></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" />
|
||||
<AppButton block type="secondary" label="退出登录" @click="logoutVisible = true" />
|
||||
</view>
|
||||
<AppDialog :visible="agreementVisible" eyebrow="协议与说明" :title="activeAgreement.label" :message="activeAgreement.copy" confirm-text="关闭" @confirm="agreementVisible = false" @close="agreementVisible = false" />
|
||||
<AppDialog :visible="agreementVisible" :close-on-mask="false" 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 { onUnmounted, reactive, ref } from "vue";
|
||||
import { reactive, ref } from "vue";
|
||||
import { onBackPress } from "@dcloudio/uni-app";
|
||||
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";
|
||||
import manifest from "@/manifest.json";
|
||||
import { goRoot, handleBackPress, runBackGuard } from "@/utils/navigation.js";
|
||||
import { session } from "@/utils/session.js";
|
||||
|
||||
const appVersion = "1.0.0";
|
||||
const appVersion = manifest.versionName;
|
||||
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 agreementVisible = ref(false);
|
||||
const logoutVisible = ref(false);
|
||||
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));
|
||||
const confirmLogout = () => {
|
||||
session.clear();
|
||||
logoutVisible.value = false;
|
||||
return goRoot("A01");
|
||||
};
|
||||
const closeActiveDialog = () => {
|
||||
if (logoutVisible.value) logoutVisible.value = false;
|
||||
else agreementVisible.value = false;
|
||||
};
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: agreementVisible.value || logoutVisible.value,
|
||||
"close-transient": closeActiveDialog,
|
||||
});
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
Reference in New Issue
Block a user