完成70%
This commit is contained in:
@@ -1,429 +1,11 @@
|
||||
<!-- 页面编号:M-01;用途:个人中心首页、提醒与服务导航。 -->
|
||||
<!-- 页面编号:M-01;用途:个人中心。资料 DTO 未声明时不展示 mock 用户。 -->
|
||||
<template>
|
||||
<view
|
||||
class="profile-page"
|
||||
:class="{
|
||||
'profile-state--ready': profileState === 'ready',
|
||||
'profile-state--error': profileState === 'error',
|
||||
}"
|
||||
>
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="profile-page__header">
|
||||
<PageHeader
|
||||
root
|
||||
title="我的"
|
||||
:action="profileState === 'ready' ? '资料' : ''"
|
||||
@action="toProfile"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="profile-content">
|
||||
<template v-if="profileState === 'ready'">
|
||||
<view class="profile-hero">
|
||||
<image
|
||||
class="profile-hero__hall"
|
||||
src="/static/assets/foundation/transparent/root-header-hall.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<image
|
||||
class="profile-hero__cloud"
|
||||
src="/static/assets/foundation/transparent/auth-title-cloud.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<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">{{ currentUser.name }}</text>
|
||||
<text class="profile-identity__role">{{ currentUser.role }}</text>
|
||||
<text class="profile-identity__phone">{{ currentUser.phone }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="profile-scroll-notice"
|
||||
role="button"
|
||||
aria-label="查看待处理消息"
|
||||
@click="toNotifications"
|
||||
>
|
||||
<view class="profile-scroll-notice__copy">
|
||||
<text>待你处理</text>
|
||||
<text>{{ unreadCount }} 条家谱提醒与审核通知</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="profile-services">
|
||||
<view class="profile-section-heading">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/auth-divider-knot.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>服务与设置</text>
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/auth-divider-knot.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<view class="profile-menu-list">
|
||||
<view
|
||||
v-for="item in menuItems"
|
||||
:key="item.label"
|
||||
class="profile-menu"
|
||||
role="button"
|
||||
:aria-label="`查看${item.label}`"
|
||||
@click="openItem(item)"
|
||||
>
|
||||
<image
|
||||
class="profile-menu__icon"
|
||||
:src="item.icon"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="profile-menu__copy">
|
||||
<text class="profile-menu__label">{{ item.label }}</text>
|
||||
<text class="profile-menu__note">{{ item.note }}</text>
|
||||
</view>
|
||||
<image
|
||||
class="profile-menu__chevron"
|
||||
src="/static/assets/foundation/transparent/chevron-right.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-else class="profile-error">
|
||||
<view class="profile-error__copy">
|
||||
<text class="profile-error__title">个人资料暂不可用</text>
|
||||
<text class="profile-error__description"
|
||||
>请稍后重新进入,账号和家谱资料不会受到影响。</text
|
||||
>
|
||||
</view>
|
||||
<AppButton
|
||||
block
|
||||
type="secondary"
|
||||
label="重新查看"
|
||||
@click="restoreProfile"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AppTabbar active="profile" />
|
||||
</view>
|
||||
<view class="profile-page"><ModulePageBackground module="profile" /><view class="page-header"><PageHeader root title="我的" /></view><view class="page-content"><view class="state-card"><text>个人资料待后端字段合同</text><text>当前资料读取接口只返回通用对象,未声明昵称、角色、手机号或脱敏规则;页面已停止展示 mock 用户与 fixture 未读数。</text><view class="profile-menu"><AppButton block label="编辑资料" @click="open('M02')" /><AppButton block label="账号与安全" @click="open('M03')" /><AppButton block label="消息中心" @click="open('N01')" /><AppButton block label="帮助中心" @click="open('M06')" /><AppButton block label="意见反馈" @click="open('M07')" /><AppButton block label="应用推广" @click="open('M08')" /><AppButton block label="VIP 服务" @click="open('M09')" /><AppButton block label="关于与设置" @click="open('M10')" /></view></view></view><AppTabbar active="profile" /></view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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: "密码与手机号",
|
||||
icon: "/static/assets/modules/auth/transparent/a01-icon-lock-v1.png",
|
||||
routeKey: "M03",
|
||||
},
|
||||
{
|
||||
label: "帮助与反馈",
|
||||
note: "使用说明与问题反馈",
|
||||
icon: "/static/assets/foundation/transparent/notice.png",
|
||||
routeKey: "M06",
|
||||
},
|
||||
{
|
||||
label: "关于家谱",
|
||||
note: "协议、隐私与版本",
|
||||
icon: "/static/assets/foundation/transparent/brand-seal.png",
|
||||
routeKey: "M10",
|
||||
},
|
||||
{
|
||||
label: "邀请家人",
|
||||
note: "邀请规则与接入状态",
|
||||
icon: "/static/assets/foundation/transparent/brand-seal.png",
|
||||
routeKey: "M08",
|
||||
},
|
||||
{
|
||||
label: "服务与订单",
|
||||
note: "权益说明与订单记录",
|
||||
icon: "/static/assets/foundation/transparent/notice.png",
|
||||
routeKey: "M09",
|
||||
},
|
||||
];
|
||||
|
||||
onLoad((query) => {
|
||||
profileState.value = query.state === "error" ? "error" : "ready";
|
||||
});
|
||||
|
||||
const restoreProfile = () => {
|
||||
profileState.value = "ready";
|
||||
};
|
||||
const toProfile = () => openPage("M02", {}, "M01");
|
||||
const toNotifications = () => openPage("N01", {}, "M01");
|
||||
const openItem = (item) =>
|
||||
item.routeKey ? openPage(item.routeKey, {}, "M01") : Promise.resolve(false);
|
||||
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 { openPage } from "@/utils/navigation.js"; const open=(route)=>openPage(route,{},"M01");
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
|
||||
.profile-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.profile-page__header,
|
||||
.profile-content {
|
||||
z-index: 1;
|
||||
}
|
||||
.profile-content {
|
||||
flex: 1;
|
||||
padding: 26rpx 30rpx 190rpx;
|
||||
}
|
||||
.profile-hero {
|
||||
@include adaptive.adaptive-profile-summary;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-height: 252rpx;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.profile-hero__hall {
|
||||
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 {
|
||||
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 {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
width: 42rpx;
|
||||
min-height: 152rpx;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2rpx solid #c9964c;
|
||||
border-radius: 4rpx;
|
||||
background: linear-gradient(180deg, #9f241f, #bd3427);
|
||||
box-shadow: inset 0 0 0 3rpx rgba(255, 222, 147, 0.24);
|
||||
color: #ffe6a9;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 21rpx;
|
||||
line-height: 1.12;
|
||||
}
|
||||
.profile-identity {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
min-height: 252rpx;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.profile-identity__seal {
|
||||
width: 92rpx;
|
||||
height: 106rpx;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.profile-identity__name,
|
||||
.profile-identity__role,
|
||||
.profile-identity__phone {
|
||||
display: block;
|
||||
}
|
||||
.profile-identity__name {
|
||||
color: $ink;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 38rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
.profile-identity__role {
|
||||
margin-top: 10rpx;
|
||||
color: #6f5942;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.profile-identity__phone {
|
||||
margin-top: 7rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 21rpx;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.profile-scroll-notice {
|
||||
@include adaptive.adaptive-scroll-button(primary);
|
||||
width: 88%;
|
||||
min-height: 112rpx;
|
||||
margin: 22rpx auto 0;
|
||||
}
|
||||
.profile-scroll-notice__copy {
|
||||
display: flex;
|
||||
min-height: 112rpx;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 13rpx 70rpx;
|
||||
color: #fff4d3;
|
||||
text-align: center;
|
||||
}
|
||||
.profile-scroll-notice__copy text:first-child {
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
.profile-scroll-notice__copy text:last-child {
|
||||
margin-top: 4rpx;
|
||||
color: #ffe4a8;
|
||||
font-size: 18rpx;
|
||||
}
|
||||
.profile-services {
|
||||
margin-top: 26rpx;
|
||||
padding: 0 12rpx;
|
||||
}
|
||||
.profile-section-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20rpx;
|
||||
padding-bottom: 12rpx;
|
||||
}
|
||||
.profile-section-heading image {
|
||||
width: 88rpx;
|
||||
height: 28rpx;
|
||||
opacity: 0.78;
|
||||
}
|
||||
.profile-section-heading image:last-child {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
.profile-section-heading text {
|
||||
color: $ink;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
.profile-menu-list {
|
||||
border-top: 1px solid rgba(181, 137, 63, 0.55);
|
||||
}
|
||||
.profile-menu {
|
||||
display: flex;
|
||||
min-height: 116rpx;
|
||||
align-items: center;
|
||||
padding: 0 8rpx;
|
||||
border-bottom: 1px solid rgba(181, 137, 63, 0.4);
|
||||
}
|
||||
.profile-menu__icon {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
margin-right: 22rpx;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.profile-menu__copy {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.profile-menu__label,
|
||||
.profile-menu__note {
|
||||
display: block;
|
||||
}
|
||||
.profile-menu__label {
|
||||
color: $ink;
|
||||
font-size: 25rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.profile-menu__note {
|
||||
margin-top: 5rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 21rpx;
|
||||
}
|
||||
.profile-menu__chevron {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
margin-left: 18rpx;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.profile-error {
|
||||
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__copy {
|
||||
display: flex;
|
||||
min-height: 118px;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 54rpx;
|
||||
}
|
||||
.profile-error__title,
|
||||
.profile-error__description {
|
||||
display: block;
|
||||
}
|
||||
.profile-error__title {
|
||||
color: $ink;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.profile-error__description {
|
||||
margin-top: 13rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.profile-error .app-button {
|
||||
margin: 20rpx auto 0;
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.profile-content {
|
||||
padding-right: 38rpx;
|
||||
padding-left: 38rpx;
|
||||
}
|
||||
}
|
||||
/* M01 medium-ornate profile folio styles remain page-scoped and extensible. */
|
||||
.profile-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-header,.page-content{z-index:1}.page-content{flex:1;padding:18rpx 24rpx 190rpx}.state-card{box-sizing:border-box;min-height:340rpx;padding:52rpx 40rpx 50rpx;text-align:center;@include adaptive.adaptive-family-content}.state-card text{display:block}.state-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:35rpx;font-weight:700}.state-card text:nth-child(2){margin-top:18rpx;color:$ink-muted;font-size:24rpx;line-height:1.65}.profile-menu{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14rpx;margin-top:28rpx}.profile-menu .app-button{margin:0}
|
||||
</style>
|
||||
|
||||
@@ -1,124 +1,11 @@
|
||||
<!-- 页面编号:M-02;用途:编辑个人资料。 -->
|
||||
<!-- 页面编号:M-02;用途:编辑资料。读取 DTO 与现有字段不一致时保持关闭。 -->
|
||||
<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="个人资料" 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="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="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" />
|
||||
</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>
|
||||
<view class="profile-edit-page"><ModulePageBackground module="profile" /><view class="page-header"><PageHeader title="编辑资料" custom-back @back="backToProfile" /></view><view class="page-content"><view class="state-card"><text>个人资料编辑待字段合同</text><text>读取接口没有资料 DTO;更新接口允许昵称、头像、性别、生日、地区和地址,但当前页面原有真实姓名、邮箱等字段不属于该合同。页面不再用 mock 预填或提交未知字段。</text><AppButton block label="返回个人中心" @click="backToProfile" /></view></view></view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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: 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.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);
|
||||
};
|
||||
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();
|
||||
});
|
||||
import AppButton from "@/components/AppButton.vue"; import ModulePageBackground from "@/components/ModulePageBackground.vue"; import PageHeader from "@/components/PageHeader.vue"; import { returnTo } from "@/utils/navigation.js"; const backToProfile=()=>returnTo("M01");
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
|
||||
.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 { @include adaptive.adaptive-profile-field; }
|
||||
.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 { 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; }
|
||||
.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)}}
|
||||
.profile-edit-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-header,.page-content{z-index:1}.page-content{padding:18rpx 24rpx 72rpx}.state-card{box-sizing:border-box;min-height:340rpx;padding:78rpx 52rpx 50rpx;text-align:center;@include adaptive.adaptive-family-content}.state-card text{display:block}.state-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:35rpx;font-weight:700}.state-card text:nth-child(2){margin-top:18rpx;color:$ink-muted;font-size:24rpx;line-height:1.65}.state-card .app-button{margin-top:28rpx}
|
||||
</style>
|
||||
|
||||
@@ -1,50 +1,11 @@
|
||||
<!-- 页面编号:M-03;用途:账号安全总览与安全功能入口。 -->
|
||||
<!-- 页面编号:M-03;用途:账号与安全入口。安全概览 DTO 未声明。 -->
|
||||
<template>
|
||||
<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>
|
||||
</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>
|
||||
<view class="security-page"><ModulePageBackground module="profile" /><view class="page-header"><PageHeader title="账号与安全" custom-back @back="backToProfile" /></view><view class="page-content"><view class="state-card"><text>安全资料待后端字段合同</text><text>没有独立安全概览、设备或登录记录读取 owner;页面不再显示 mock 账号摘要。可进入已独立核对的修改密码页面,换绑手机号仍需人工 TAC/短信窗口。</text><AppButton block label="修改密码" @click="openPassword" /><AppButton type="secondary" block label="换绑手机号" @click="openPhone" /></view></view></view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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";
|
||||
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", 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) => openPage(item.routeKey, {}, "M03");
|
||||
const checkSecurity = () => showToast("账号安全接口将在后续独立批次接入");
|
||||
onUnmounted(() => clearTimeout(timer));
|
||||
import AppButton from "@/components/AppButton.vue"; import ModulePageBackground from "@/components/ModulePageBackground.vue"; import PageHeader from "@/components/PageHeader.vue"; import { openPage,returnTo } from "@/utils/navigation.js"; const backToProfile=()=>returnTo("M01");const openPassword=()=>openPage("M04",{},"M03");const openPhone=()=>openPage("M05",{},"M03");
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
|
||||
.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{@include adaptive.adaptive-profile-summary;min-height:190rpx;padding:42rpx 48rpx;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{@include adaptive.adaptive-profile-field;display:grid;grid-template-columns:56rpx minmax(0,1fr) 34rpx;min-height:110rpx;align-items:center;gap:18rpx;padding:16rpx 26rpx}.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}}
|
||||
.security-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-header,.page-content{z-index:1}.page-content{padding:18rpx 24rpx 72rpx}.state-card{box-sizing:border-box;min-height:340rpx;padding:78rpx 52rpx 50rpx;text-align:center;@include adaptive.adaptive-family-content}.state-card text{display:block}.state-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:35rpx;font-weight:700}.state-card text:nth-child(2){margin-top:18rpx;color:$ink-muted;font-size:24rpx;line-height:1.65}.state-card .app-button{margin-top:20rpx}
|
||||
</style>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<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
|
||||
@@ -41,7 +41,9 @@ 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 { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import { calcMD5 } from "@/utils/md5.js";
|
||||
import { handleBackPress, runBackGuard } from "@/utils/navigation.js";
|
||||
import {
|
||||
PASSWORD_POLICY_MESSAGE,
|
||||
@@ -59,6 +61,8 @@ const passwordFields = [
|
||||
{ key: "confirm", label: "确认新密码", placeholder: "请再次输入新密码" },
|
||||
];
|
||||
const toastVisible = ref(false); const toastMessage = ref(""); let timer = null;
|
||||
const passwordRequestController = createRequestController();
|
||||
let pageActive = true;
|
||||
const formSnapshot = computed(() => JSON.stringify(passwordForm));
|
||||
const baseline = ref(formSnapshot.value);
|
||||
const isDirty = computed(() => formSnapshot.value !== baseline.value);
|
||||
@@ -79,13 +83,27 @@ const validateForm = () => {
|
||||
errors.confirm = !passwordForm.confirm ? "请再次输入新密码" : passwordForm.confirm !== passwordForm.next ? "两次输入的新密码不一致" : "";
|
||||
return !Object.values(errors).some(Boolean);
|
||||
};
|
||||
const savePassword = () => {
|
||||
const savePassword = async () => {
|
||||
if (!validateForm() || passwordState.value === "saving") return;
|
||||
passwordState.value = "saving";
|
||||
timer = setTimeout(() => {
|
||||
passwordState.value = "ready";
|
||||
showToast("本地校验通过,尚未提交服务器");
|
||||
}, 500);
|
||||
try {
|
||||
await appApi.changePassword({
|
||||
oldPasswordHash: calcMD5(passwordForm.current),
|
||||
newPasswordHash: calcMD5(passwordForm.next),
|
||||
}, { requestController: passwordRequestController });
|
||||
if (!pageActive) return;
|
||||
passwordForm.current = "";
|
||||
passwordForm.next = "";
|
||||
passwordForm.confirm = "";
|
||||
baseline.value = formSnapshot.value;
|
||||
showToast("密码修改成功");
|
||||
} catch (error) {
|
||||
if (pageActive && !isRequestCancelled(error)) {
|
||||
showToast(error?.message || "密码修改失败,请稍后重试");
|
||||
}
|
||||
} finally {
|
||||
if (pageActive) passwordState.value = "ready";
|
||||
}
|
||||
};
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
@@ -98,6 +116,8 @@ const requestBack = () =>
|
||||
});
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
onUnload(() => {
|
||||
pageActive = false;
|
||||
passwordRequestController.abort();
|
||||
clearTimeout(timer);
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
|
||||
@@ -1,98 +1,11 @@
|
||||
<!-- 页面编号:M-05;用途:验证并更换绑定手机号。 -->
|
||||
<!-- 页面编号:M-05;用途:换绑手机号。缺专用发码/资料 DTO 且需人工 TAC/短信时保持关闭。 -->
|
||||
<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="修改手机号" 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="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" />
|
||||
</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>
|
||||
<view class="phone-page"><ModulePageBackground module="profile" /><view class="page-header"><PageHeader title="换绑手机号" custom-back @back="backToSecurity" /></view><view class="page-content"><view class="state-card"><text>换绑手机号待人工验证</text><text>换绑接口要求新手机号、四位短信码和 `clientId`,但没有可安全消费的当前手机号 DTO,也没有已核实的专用受保护发码链。页面不显示 mock 手机号、不发送验证码、不在无人值守时换绑。</text><AppButton block label="返回账号与安全" @click="backToSecurity" /></view></view></view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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: currentUser.phone, newPhone: "", code: "" });
|
||||
const errors = reactive({ newPhone: "", code: "" });
|
||||
const phoneState = ref("ready");
|
||||
const discardVisible = ref(false);
|
||||
const toastVisible = ref(false); const toastMessage = ref("");
|
||||
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 (!/^1\d{10}$/.test(phoneForm.newPhone)) { errors.newPhone = "请输入正确的新手机号"; return; }
|
||||
errors.newPhone = "";
|
||||
showToast("需先完成滑动行为验证;当前未发送验证码");
|
||||
};
|
||||
const validatePhone = () => {
|
||||
errors.newPhone = /^1\d{10}$/.test(phoneForm.newPhone) ? "" : "请输入正确的新手机号";
|
||||
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";
|
||||
showToast("本地校验通过,尚未提交服务器");
|
||||
}, 500);
|
||||
};
|
||||
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();
|
||||
});
|
||||
import AppButton from "@/components/AppButton.vue"; import ModulePageBackground from "@/components/ModulePageBackground.vue"; import PageHeader from "@/components/PageHeader.vue"; import { returnTo } from "@/utils/navigation.js"; const backToSecurity=()=>returnTo("M03");
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
.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{@include adaptive.adaptive-profile-content}.current-phone{min-height:210rpx;padding:40rpx 48rpx;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}.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}}
|
||||
.phone-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-header,.page-content{z-index:1}.page-content{padding:18rpx 24rpx 72rpx}.state-card{box-sizing:border-box;min-height:340rpx;padding:78rpx 52rpx 50rpx;text-align:center;@include adaptive.adaptive-family-content}.state-card text{display:block}.state-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:35rpx;font-weight:700}.state-card text:nth-child(2){margin-top:18rpx;color:$ink-muted;font-size:24rpx;line-height:1.65}.state-card .app-button{margin-top:28rpx}
|
||||
</style>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="帮助中心" /></view>
|
||||
<view class="page-content page-layer">
|
||||
<text class="help-source-note">当前展示的是本地使用说明,不代表已读取服务端帮助文章;需要最新支持请提交意见反馈。</text>
|
||||
<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">
|
||||
@@ -46,5 +47,5 @@ const contactSupport = () => openPage("M07", {}, "M06");
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
.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{@include adaptive.adaptive-profile-field;display:grid;grid-template-columns:minmax(0,1fr) auto;min-height:86rpx;align-items:center;gap:16rpx;padding:0 30rpx}.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{@include adaptive.adaptive-scroll-button(secondary);color:$brand-red;font-weight:700}.question-list{display:flex;flex-direction:column;gap:14rpx;margin-top:18rpx}.question-card,.empty-card{@include adaptive.adaptive-profile-content}.question-card{min-height:104rpx;padding:24rpx 34rpx}.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;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}}
|
||||
.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}.help-source-note{display:block;margin:0 4rpx 16rpx;color:$ink-muted;font-size:21rpx;line-height:1.55}.search-box{@include adaptive.adaptive-profile-field;display:grid;grid-template-columns:minmax(0,1fr) auto;min-height:86rpx;align-items:center;gap:16rpx;padding:0 30rpx}.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{@include adaptive.adaptive-scroll-button(secondary);color:$brand-red;font-weight:700}.question-list{display:flex;flex-direction:column;gap:14rpx;margin-top:18rpx}.question-card,.empty-card{@include adaptive.adaptive-profile-content}.question-card{min-height:104rpx;padding:24rpx 34rpx}.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;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,6 +1,6 @@
|
||||
<!-- 页面编号:M-10;用途:协议、隐私、版本与退出登录。 -->
|
||||
<template>
|
||||
<view class="about-page">
|
||||
<view class="about-page" :class="{ 'about-state--logging-out': logoutSubmitting }">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="关于家谱" custom-back @back="requestBack" /></view>
|
||||
<view class="page-content page-layer">
|
||||
@@ -11,18 +11,19 @@
|
||||
<AppButton block type="secondary" label="退出登录" @click="logoutVisible = true" />
|
||||
</view>
|
||||
<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" />
|
||||
<AppDialog :visible="logoutVisible" eyebrow="账号操作" title="确认退出登录?" message="退出后需要重新验证账号;本机保存的密码不会被保留。" :confirm-text="logoutSubmitting ? '正在退出' : '确认退出'" cancel-text="取消" show-cancel :close-on-mask="false" @confirm="confirmLogout" @cancel="logoutVisible = false" @close="logoutVisible = false" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import { onBackPress } from "@dcloudio/uni-app";
|
||||
import { onBackPress, onUnload } 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 manifest from "@/manifest.json";
|
||||
import { appApi, createRequestController } from "@/utils/api.js";
|
||||
import { goRoot, handleBackPress, runBackGuard } from "@/utils/navigation.js";
|
||||
import { session } from "@/utils/session.js";
|
||||
|
||||
@@ -35,10 +36,21 @@ const agreementItems = [
|
||||
const activeAgreement = reactive({ label: "", copy: "" });
|
||||
const agreementVisible = ref(false);
|
||||
const logoutVisible = ref(false);
|
||||
const logoutSubmitting = ref(false);
|
||||
const logoutRequestController = createRequestController();
|
||||
const openAgreement = (item) => { activeAgreement.label = item.label; activeAgreement.copy = item.copy; agreementVisible.value = true; };
|
||||
const confirmLogout = () => {
|
||||
session.clear();
|
||||
logoutVisible.value = false;
|
||||
const confirmLogout = async () => {
|
||||
if (logoutSubmitting.value) return false;
|
||||
logoutSubmitting.value = true;
|
||||
try {
|
||||
await appApi.logout({ requestController: logoutRequestController });
|
||||
} catch {
|
||||
// 退出请求的结果未知时仍必须撤销本机会话,不能保留旧授权或自动重试。
|
||||
} finally {
|
||||
session.clear();
|
||||
logoutVisible.value = false;
|
||||
logoutSubmitting.value = false;
|
||||
}
|
||||
return goRoot("A01");
|
||||
};
|
||||
const closeActiveDialog = () => {
|
||||
@@ -51,6 +63,7 @@ const requestBack = () =>
|
||||
"close-transient": closeActiveDialog,
|
||||
});
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
onUnload(() => logoutRequestController.abort());
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
Reference in New Issue
Block a user