修改部分样式
This commit is contained in:
@@ -4,7 +4,9 @@
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-header"><PageHeader root title="我的" /></view>
|
||||
<view class="page-content">
|
||||
<view v-if="loading" class="state-card"><text>正在读取个人资料…</text></view>
|
||||
<view v-if="loading" class="state-card"
|
||||
><text>正在读取个人资料…</text></view
|
||||
>
|
||||
<view v-else-if="loadError" class="state-card">
|
||||
<text>个人资料暂时无法读取</text>
|
||||
<text>{{ loadError }}</text>
|
||||
@@ -12,19 +14,33 @@
|
||||
</view>
|
||||
<template v-else>
|
||||
<view class="profile-summary">
|
||||
<view class="profile-summary__avatar"><image src="/static/assets/foundation/transparent/auth-login-outline.png" mode="aspectFit" /></view>
|
||||
<view class="profile-summary__avatar"
|
||||
><image
|
||||
src="/static/assets/foundation/transparent/auth-login-outline.png"
|
||||
mode="aspectFit"
|
||||
/></view>
|
||||
<view class="profile-summary__identity">
|
||||
<text>{{ profile.nickName || "未设置昵称" }}</text>
|
||||
<text>{{ profile.realName || "未设置真实姓名" }}</text>
|
||||
<text>{{ profile.phone || "未提供手机号" }}</text>
|
||||
</view>
|
||||
<AppButton class="profile-summary__edit" label="编辑资料" @click="openEdit" />
|
||||
<AppButton
|
||||
class="profile-summary__edit"
|
||||
label="编辑资料"
|
||||
@click="openEdit"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="profile-fields">
|
||||
<view class="profile-field"><text>性别</text><text>{{ sexText }}</text></view>
|
||||
<view class="profile-field"><text>生日</text><text>{{ birthdayText }}</text></view>
|
||||
<view class="profile-field"><text>邮箱</text><text>{{ profile.email || "未设置" }}</text></view>
|
||||
<view class="profile-field"
|
||||
><text>性别</text><text>{{ sexText }}</text></view
|
||||
>
|
||||
<view class="profile-field"
|
||||
><text>生日</text><text>{{ birthdayText }}</text></view
|
||||
>
|
||||
<view class="profile-field"
|
||||
><text>邮箱</text><text>{{ profile.email || "未设置" }}</text></view
|
||||
>
|
||||
</view>
|
||||
|
||||
<view class="profile-menu">
|
||||
@@ -49,10 +65,22 @@ 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 { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
|
||||
import {
|
||||
appApi,
|
||||
createRequestController,
|
||||
isRequestCancelled,
|
||||
} from "@/utils/api.js";
|
||||
import { openPage } from "@/utils/navigation.js";
|
||||
|
||||
const profile = reactive({ avatar: null, nickName: "", realName: "", phone: "", sex: "", birthday: "", email: "" });
|
||||
const profile = reactive({
|
||||
avatar: null,
|
||||
nickName: "",
|
||||
realName: "",
|
||||
phone: "",
|
||||
sex: "",
|
||||
birthday: "",
|
||||
email: "",
|
||||
});
|
||||
const loading = ref(false);
|
||||
const loadError = ref("");
|
||||
const requestController = createRequestController();
|
||||
@@ -60,7 +88,11 @@ let pageActive = true;
|
||||
|
||||
const sexText = computed(() => (profile.sex ? "待确认" : "未设置"));
|
||||
|
||||
const birthdayText = computed(() => /^\d{4}-\d{2}-\d{2}/.test(profile.birthday) ? profile.birthday.slice(0, 10) : "未设置");
|
||||
const birthdayText = computed(() =>
|
||||
/^\d{4}-\d{2}-\d{2}/.test(profile.birthday)
|
||||
? profile.birthday.slice(0, 10)
|
||||
: "未设置",
|
||||
);
|
||||
|
||||
const loadProfile = async () => {
|
||||
if (loading.value) return;
|
||||
@@ -91,28 +123,131 @@ onUnload(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
.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, .profile-summary, .profile-fields { @include adaptive.adaptive-family-content; }
|
||||
.state-card { box-sizing: border-box; min-height: 340rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
|
||||
.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; }
|
||||
.profile-summary { display: grid; grid-template-columns: 96rpx minmax(0, 1fr); align-items: center; gap: 18rpx; padding: 30rpx; }
|
||||
.profile-summary__avatar { display: flex; width: 96rpx; height: 96rpx; align-items: center; justify-content: center; border: 1rpx solid rgba(159, 23, 15, .32); border-radius: 50%; background: rgba(159, 23, 15, .06); }
|
||||
.profile-summary__avatar image { width: 64rpx; height: 64rpx; opacity: .8; }
|
||||
.profile-summary__identity { min-width: 0; }
|
||||
.profile-summary__identity text { display: block; overflow-wrap: anywhere; }
|
||||
.profile-summary__identity text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 32rpx; font-weight: 700; }
|
||||
.profile-summary__identity text:nth-child(2), .profile-summary__identity text:last-child { margin-top: 5rpx; color: $ink-muted; font-size: 21rpx; }
|
||||
.profile-summary__edit { grid-column: 1 / -1; margin-top: 6rpx; }
|
||||
.profile-fields { display: flex; flex-direction: column; margin-top: 18rpx; padding: 8rpx 30rpx; }
|
||||
.profile-field { display: flex; min-height: 82rpx; align-items: center; justify-content: space-between; gap: 20rpx; border-bottom: 1rpx solid rgba(181, 137, 63, .3); }
|
||||
.profile-field:last-child { border-bottom: 0; }
|
||||
.profile-field text:first-child { flex: 0 0 auto; color: $ink; font-size: 24rpx; font-weight: 700; }
|
||||
.profile-field text:last-child { min-width: 0; color: $ink-muted; font-size: 22rpx; overflow-wrap: anywhere; text-align: right; }
|
||||
.profile-menu { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14rpx; margin-top: 24rpx; }
|
||||
.profile-menu .app-button { margin: 0; }
|
||||
.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,
|
||||
.profile-summary,
|
||||
.profile-fields {
|
||||
@include adaptive.adaptive-family-content;
|
||||
}
|
||||
.state-card {
|
||||
box-sizing: border-box;
|
||||
min-height: 340rpx;
|
||||
padding: 78rpx 52rpx 50rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.profile-summary {
|
||||
display: grid;
|
||||
grid-template-columns: 96rpx minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
.profile-summary__avatar {
|
||||
display: flex;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1rpx solid rgba(159, 23, 15, 0.32);
|
||||
border-radius: 50%;
|
||||
background: rgba(159, 23, 15, 0.06);
|
||||
}
|
||||
.profile-summary__avatar image {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.profile-summary__identity {
|
||||
min-width: 0;
|
||||
}
|
||||
.profile-summary__identity text {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.profile-summary__identity text:first-child {
|
||||
color: $ink;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.profile-summary__identity text:nth-child(2),
|
||||
.profile-summary__identity text:last-child {
|
||||
margin-top: 5rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 21rpx;
|
||||
}
|
||||
.profile-summary__edit {
|
||||
grid-column: 1 / -1;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
.profile-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 18rpx;
|
||||
padding: 8rpx 30rpx;
|
||||
}
|
||||
.profile-field {
|
||||
display: flex;
|
||||
min-height: 82rpx;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
border-bottom: 1rpx solid rgba(181, 137, 63, 0.3);
|
||||
}
|
||||
.profile-field:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.profile-field text:first-child {
|
||||
flex: 0 0 auto;
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.profile-field text:last-child {
|
||||
min-width: 0;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
overflow-wrap: anywhere;
|
||||
text-align: right;
|
||||
}
|
||||
.profile-menu {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
.profile-menu .app-button {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
<template>
|
||||
<view class="profile-edit-page">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-header"><PageHeader title="编辑资料" custom-back @back="backToProfile" /></view>
|
||||
<view class="page-header"
|
||||
><PageHeader title="编辑资料" custom-back @back="backToProfile"
|
||||
/></view>
|
||||
<view class="page-content">
|
||||
<view v-if="loading" class="state-card"><text>正在读取个人资料…</text></view>
|
||||
<view v-if="loading" class="state-card"
|
||||
><text>正在读取个人资料…</text></view
|
||||
>
|
||||
<view v-else-if="loadError" class="state-card">
|
||||
<text>个人资料暂时无法读取</text>
|
||||
<text>{{ loadError }}</text>
|
||||
@@ -13,23 +17,78 @@
|
||||
<template v-else>
|
||||
<view class="form-panel">
|
||||
<view class="avatar-row">
|
||||
<view><text>头像</text><text>{{ avatarMessage }}</text></view>
|
||||
<button class="upload-button" :disabled="uploading || saving" @click="uploadAvatar">{{ uploading ? "上传中…" : "选择图片" }}</button>
|
||||
<view
|
||||
><text>头像</text><text>{{ avatarMessage }}</text></view
|
||||
>
|
||||
<button
|
||||
class="upload-button"
|
||||
:disabled="uploading || saving"
|
||||
@click="uploadAvatar"
|
||||
>
|
||||
{{ uploading ? "上传中…" : "选择图片" }}
|
||||
</button>
|
||||
</view>
|
||||
<text v-if="avatarError" class="field-error">{{ avatarError }}</text>
|
||||
|
||||
<view class="form-row"><text>昵称</text><input v-model.trim="form.nickName" maxlength="30" placeholder="请输入昵称" placeholder-class="placeholder" :disabled="saving || uploading" /></view>
|
||||
<view class="form-row"><text>真实姓名</text><input v-model.trim="form.realName" maxlength="30" placeholder="请输入真实姓名" placeholder-class="placeholder" :disabled="saving || uploading" /></view>
|
||||
<view class="form-row form-row--readonly"><text>性别</text><view class="readonly-value"><text>{{ form.sex ? "待确认" : "未设置" }}</text><text>选项待提供</text></view></view>
|
||||
<view class="form-row"
|
||||
><text>昵称</text
|
||||
><input
|
||||
v-model.trim="form.nickName"
|
||||
maxlength="30"
|
||||
placeholder="请输入昵称"
|
||||
placeholder-class="placeholder"
|
||||
:disabled="saving || uploading"
|
||||
/></view>
|
||||
<view class="form-row"
|
||||
><text>真实姓名</text
|
||||
><input
|
||||
v-model.trim="form.realName"
|
||||
maxlength="30"
|
||||
placeholder="请输入真实姓名"
|
||||
placeholder-class="placeholder"
|
||||
:disabled="saving || uploading"
|
||||
/></view>
|
||||
<view class="form-row form-row--readonly"
|
||||
><text>性别</text
|
||||
><view class="readonly-value"
|
||||
><text>{{ form.sex ? "待确认" : "未设置" }}</text
|
||||
><text>选项待提供</text></view
|
||||
></view
|
||||
>
|
||||
<view class="form-row">
|
||||
<text>生日</text>
|
||||
<picker mode="date" :value="form.birthday" :disabled="saving || uploading" @change="changeBirthday"><view class="picker-value" :class="{ 'picker-value--placeholder': !form.birthday }">{{ form.birthday || "请选择生日" }}</view></picker>
|
||||
<picker
|
||||
mode="date"
|
||||
:value="form.birthday"
|
||||
:disabled="saving || uploading"
|
||||
@change="changeBirthday"
|
||||
><view
|
||||
class="picker-value"
|
||||
:class="{ 'picker-value--placeholder': !form.birthday }"
|
||||
>{{ form.birthday || "请选择生日" }}</view
|
||||
></picker
|
||||
>
|
||||
</view>
|
||||
<view class="form-row"><text>邮箱</text><input v-model.trim="form.email" maxlength="100" placeholder="请输入邮箱" placeholder-class="placeholder" :disabled="saving || uploading" /></view>
|
||||
<view class="form-row"
|
||||
><text>邮箱</text
|
||||
><input
|
||||
v-model.trim="form.email"
|
||||
maxlength="100"
|
||||
placeholder="请输入邮箱"
|
||||
placeholder-class="placeholder"
|
||||
:disabled="saving || uploading"
|
||||
/></view>
|
||||
</view>
|
||||
<text class="form-note">资料字段均可按需填写;留空不会提交为修改。</text>
|
||||
<text class="form-note"
|
||||
>资料字段均可按需填写;留空不会提交为修改。</text
|
||||
>
|
||||
<text v-if="saveError" class="save-error">{{ saveError }}</text>
|
||||
<AppButton block :disabled="saving || uploading" :label="saving ? '保存中…' : '保存资料'" @click="saveProfile" />
|
||||
<AppButton
|
||||
block
|
||||
:disabled="saving || uploading"
|
||||
:label="saving ? '保存中…' : '保存资料'"
|
||||
@click="saveProfile"
|
||||
/>
|
||||
</template>
|
||||
</view>
|
||||
<AppToast :visible="feedbackVisible" :message="feedbackMessage" />
|
||||
@@ -43,12 +102,33 @@ 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 { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
|
||||
import { isImagePickCancelled, pickAndUploadImage, toConsumerOssId } from "@/utils/resumable-image-upload.js";
|
||||
import {
|
||||
appApi,
|
||||
createRequestController,
|
||||
isRequestCancelled,
|
||||
} from "@/utils/api.js";
|
||||
import {
|
||||
isImagePickCancelled,
|
||||
pickAndUploadImage,
|
||||
toConsumerOssId,
|
||||
} from "@/utils/resumable-image-upload.js";
|
||||
import { finishPage, returnTo } from "@/utils/navigation.js";
|
||||
|
||||
const form = reactive({ nickName: "", realName: "", sex: "", birthday: "", email: "" });
|
||||
const original = reactive({ nickName: "", realName: "", sex: "", birthday: "", email: "", avatar: null });
|
||||
const form = reactive({
|
||||
nickName: "",
|
||||
realName: "",
|
||||
sex: "",
|
||||
birthday: "",
|
||||
email: "",
|
||||
});
|
||||
const original = reactive({
|
||||
nickName: "",
|
||||
realName: "",
|
||||
sex: "",
|
||||
birthday: "",
|
||||
email: "",
|
||||
avatar: null,
|
||||
});
|
||||
const avatarId = ref(null);
|
||||
const avatarFileName = ref("");
|
||||
const loading = ref(true);
|
||||
@@ -78,7 +158,8 @@ const showFeedback = (message) => {
|
||||
}, 2200);
|
||||
};
|
||||
|
||||
const toBirthdayDate = (value) => /^\d{4}-\d{2}-\d{2}/.test(value) ? value.slice(0, 10) : "";
|
||||
const toBirthdayDate = (value) =>
|
||||
/^\d{4}-\d{2}-\d{2}/.test(value) ? value.slice(0, 10) : "";
|
||||
|
||||
const loadProfile = async () => {
|
||||
if (loading.value === false && (uploading.value || saving.value)) return;
|
||||
@@ -98,7 +179,8 @@ const loadProfile = async () => {
|
||||
avatarId.value = profile.avatar;
|
||||
avatarFileName.value = "";
|
||||
} catch (error) {
|
||||
if (pageActive && !isRequestCancelled(error)) loadError.value = error?.message || "请稍后重试";
|
||||
if (pageActive && !isRequestCancelled(error))
|
||||
loadError.value = error?.message || "请稍后重试";
|
||||
} finally {
|
||||
if (pageActive) loading.value = false;
|
||||
}
|
||||
@@ -118,7 +200,11 @@ const uploadAvatar = async () => {
|
||||
avatarId.value = toConsumerOssId(receipt.ossId);
|
||||
avatarFileName.value = receipt.fileName || "新头像";
|
||||
} catch (error) {
|
||||
if (pageActive && !isImagePickCancelled(error) && !isRequestCancelled(error)) {
|
||||
if (
|
||||
pageActive &&
|
||||
!isImagePickCancelled(error) &&
|
||||
!isRequestCancelled(error)
|
||||
) {
|
||||
avatarError.value = error?.message || "头像上传失败,请稍后重试";
|
||||
}
|
||||
} finally {
|
||||
@@ -129,10 +215,13 @@ const uploadAvatar = async () => {
|
||||
const buildUpdatePayload = () => {
|
||||
const data = {};
|
||||
for (const field of ["nickName", "realName", "sex", "email"]) {
|
||||
if (form[field] && form[field] !== original[field]) data[field] = form[field];
|
||||
if (form[field] && form[field] !== original[field])
|
||||
data[field] = form[field];
|
||||
}
|
||||
if (form.birthday && form.birthday !== original.birthday) data.birthday = form.birthday;
|
||||
if (avatarId.value && avatarId.value !== original.avatar) data.avatar = avatarId.value;
|
||||
if (form.birthday && form.birthday !== original.birthday)
|
||||
data.birthday = form.birthday;
|
||||
if (avatarId.value && avatarId.value !== original.avatar)
|
||||
data.avatar = avatarId.value;
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -148,9 +237,14 @@ const saveProfile = async () => {
|
||||
try {
|
||||
await appApi.updateProfile(payload, { requestController });
|
||||
if (!pageActive) return;
|
||||
await finishPage("M01", {}, { operation: "profile-updated", refresh: true });
|
||||
await finishPage(
|
||||
"M01",
|
||||
{},
|
||||
{ operation: "profile-updated", refresh: true },
|
||||
);
|
||||
} catch (error) {
|
||||
if (pageActive && !isRequestCancelled(error)) saveError.value = error?.message || "保存失败,请稍后重试";
|
||||
if (pageActive && !isRequestCancelled(error))
|
||||
saveError.value = error?.message || "保存失败,请稍后重试";
|
||||
} finally {
|
||||
if (pageActive) saving.value = false;
|
||||
}
|
||||
@@ -171,32 +265,158 @@ onUnload(() => {
|
||||
|
||||
<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-header, .page-content { z-index: 1; }
|
||||
.page-content { flex: 1; padding: 24rpx 30rpx 72rpx; }
|
||||
.state-card, .form-panel { @include adaptive.adaptive-profile-content; }
|
||||
.state-card { box-sizing: border-box; min-height: 340rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
|
||||
.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; }
|
||||
.form-panel { padding: 16rpx 30rpx 22rpx; }
|
||||
.avatar-row, .form-row { display: grid; grid-template-columns: 142rpx minmax(0, 1fr); min-height: 92rpx; align-items: center; gap: 16rpx; border-bottom: 1rpx solid rgba(181, 137, 63, .42); }
|
||||
.avatar-row > view text { display: block; }
|
||||
.avatar-row > view text:first-child, .form-row > text { color: $ink; font-size: 23rpx; font-weight: 700; }
|
||||
.avatar-row > view text:last-child { margin-top: 4rpx; color: $ink-muted; font-size: 19rpx; overflow-wrap: anywhere; }
|
||||
.upload-button { display: flex; min-height: 88rpx; align-items: center; justify-content: center; border: 0; background: transparent; color: $brand-red; font-size: 22rpx; text-align: right; }
|
||||
.form-row input, .picker-value { width: auto; min-width: 0; min-height: 68rpx; color: $ink; font-size: 23rpx; text-align: right; }
|
||||
.picker-value { display: flex; align-items: center; justify-content: flex-end; }
|
||||
.readonly-value { display:flex; min-height:68rpx; align-items:center; justify-content:flex-end; gap:12rpx; color:$ink-muted; }
|
||||
.readonly-value text:first-child { color:$ink; font-size:23rpx; }
|
||||
.readonly-value text:last-child { color:$ink-muted; font-size:19rpx; }
|
||||
.picker-value--placeholder, .placeholder { color: #ab9a86; }
|
||||
.field-error, .save-error, .form-note { display: block; color: $ink-muted; font-size: 20rpx; line-height: 1.5; }
|
||||
.field-error, .save-error { color: #b42318; }
|
||||
.field-error { margin-top: 8rpx; text-align: right; }
|
||||
.form-note { margin: 16rpx 4rpx 0; }
|
||||
.save-error { margin: 12rpx 4rpx 0; }
|
||||
.page-content > .app-button { margin-top: 24rpx; }
|
||||
@media (max-width: 340px) { .page-content { padding-right: 22rpx; padding-left: 22rpx; } .form-panel { padding-right: 24rpx; padding-left: 24rpx; } .avatar-row, .form-row { grid-template-columns: 116rpx minmax(0, 1fr); gap: 12rpx; } }
|
||||
.profile-edit-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.page-header,
|
||||
.page-content {
|
||||
z-index: 1;
|
||||
}
|
||||
.page-content {
|
||||
flex: 1;
|
||||
padding: 24rpx 30rpx 72rpx;
|
||||
}
|
||||
.state-card,
|
||||
.form-panel {
|
||||
@include adaptive.adaptive-profile-content;
|
||||
}
|
||||
.state-card {
|
||||
box-sizing: border-box;
|
||||
min-height: 340rpx;
|
||||
padding: 78rpx 52rpx 50rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.form-panel {
|
||||
padding: 16rpx 30rpx 22rpx;
|
||||
}
|
||||
.avatar-row,
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 142rpx minmax(0, 1fr);
|
||||
min-height: 92rpx;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
border-bottom: 1rpx solid rgba(181, 137, 63, 0.42);
|
||||
}
|
||||
.avatar-row > view text {
|
||||
display: block;
|
||||
}
|
||||
.avatar-row > view text:first-child,
|
||||
.form-row > text {
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.avatar-row > view text:last-child {
|
||||
margin-top: 4rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 19rpx;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.upload-button {
|
||||
display: flex;
|
||||
min-height: 88rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: $brand-red;
|
||||
font-size: 22rpx;
|
||||
text-align: right;
|
||||
}
|
||||
.form-row input,
|
||||
.picker-value {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
min-height: 68rpx;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
text-align: right;
|
||||
}
|
||||
.picker-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.readonly-value {
|
||||
display: flex;
|
||||
min-height: 68rpx;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12rpx;
|
||||
color: $ink-muted;
|
||||
}
|
||||
.readonly-value text:first-child {
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.readonly-value text:last-child {
|
||||
color: $ink-muted;
|
||||
font-size: 19rpx;
|
||||
}
|
||||
.picker-value--placeholder,
|
||||
.placeholder {
|
||||
color: #ab9a86;
|
||||
}
|
||||
.field-error,
|
||||
.save-error,
|
||||
.form-note {
|
||||
display: block;
|
||||
color: $ink-muted;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.field-error,
|
||||
.save-error {
|
||||
color: #b42318;
|
||||
}
|
||||
.field-error {
|
||||
margin-top: 8rpx;
|
||||
text-align: right;
|
||||
}
|
||||
.form-note {
|
||||
margin: 16rpx 4rpx 0;
|
||||
}
|
||||
.save-error {
|
||||
margin: 12rpx 4rpx 0;
|
||||
}
|
||||
.page-content > .app-button {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
@media (max-width: 340px) {
|
||||
.page-content {
|
||||
padding-right: 22rpx;
|
||||
padding-left: 22rpx;
|
||||
}
|
||||
.form-panel {
|
||||
padding-right: 24rpx;
|
||||
padding-left: 24rpx;
|
||||
}
|
||||
.avatar-row,
|
||||
.form-row {
|
||||
grid-template-columns: 116rpx minmax(0, 1fr);
|
||||
gap: 12rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,21 +2,40 @@
|
||||
<template>
|
||||
<view class="security-page">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-header"><PageHeader title="账号与安全" custom-back @back="backToProfile" /></view>
|
||||
<view class="page-header"
|
||||
><PageHeader title="账号与安全" custom-back @back="backToProfile"
|
||||
/></view>
|
||||
<view class="page-content">
|
||||
<view v-if="loading" class="state-card"><AppLoading text="正在读取账号安全资料" /></view>
|
||||
<view v-if="loading" class="state-card"
|
||||
><AppLoading text="正在读取账号安全资料"
|
||||
/></view>
|
||||
<view v-else-if="loadError" class="state-card">
|
||||
<text>账号安全资料暂时无法读取</text><text>{{ loadError }}</text>
|
||||
<AppButton block label="重新读取" @click="loadProfile" />
|
||||
</view>
|
||||
<view v-else class="security-card">
|
||||
<text class="security-card__title">当前账号安全概览</text>
|
||||
<view class="security-field"><text>绑定手机号</text><text>{{ maskedPhone }}</text></view>
|
||||
<view class="security-field"><text>账号编号</text><text>{{ profile.userNo || "未提供" }}</text></view>
|
||||
<view class="security-field"><text>账号状态</text><text>{{ profile.status || "未提供" }}</text></view>
|
||||
<text class="security-note">当前仅展示可确认的账户信息,其他安全记录暂不可查看。</text>
|
||||
<view class="security-field"
|
||||
><text>绑定手机号</text><text>{{ maskedPhone }}</text></view
|
||||
>
|
||||
<view class="security-field"
|
||||
><text>账号编号</text
|
||||
><text>{{ profile.userNo || "未提供" }}</text></view
|
||||
>
|
||||
<view class="security-field"
|
||||
><text>账号状态</text
|
||||
><text>{{ profile.status || "未提供" }}</text></view
|
||||
>
|
||||
<text class="security-note"
|
||||
>当前仅展示可确认的账户信息,其他安全记录暂不可查看。</text
|
||||
>
|
||||
<AppButton block label="修改密码" @click="openPassword" />
|
||||
<AppButton type="secondary" block label="换绑手机号" @click="openPhone" />
|
||||
<AppButton
|
||||
type="secondary"
|
||||
block
|
||||
label="换绑手机号"
|
||||
@click="openPhone"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -29,7 +48,11 @@ import AppButton from "@/components/AppButton.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
|
||||
import {
|
||||
appApi,
|
||||
createRequestController,
|
||||
isRequestCancelled,
|
||||
} from "@/utils/api.js";
|
||||
import { openPage, returnTo } from "@/utils/navigation.js";
|
||||
|
||||
const profile = reactive({ phone: "", userNo: "", status: "" });
|
||||
@@ -37,9 +60,11 @@ const loading = ref(false);
|
||||
const loadError = ref("");
|
||||
const controller = createRequestController();
|
||||
let pageActive = true;
|
||||
const maskedPhone = computed(() => /^\d{7,}$/.test(profile.phone)
|
||||
? `${profile.phone.slice(0, 3)}****${profile.phone.slice(-4)}`
|
||||
: "未提供");
|
||||
const maskedPhone = computed(() =>
|
||||
/^\d{7,}$/.test(profile.phone)
|
||||
? `${profile.phone.slice(0, 3)}****${profile.phone.slice(-4)}`
|
||||
: "未提供",
|
||||
);
|
||||
|
||||
const loadProfile = async () => {
|
||||
if (loading.value) return;
|
||||
@@ -49,7 +74,8 @@ const loadProfile = async () => {
|
||||
const data = await appApi.getProfile({ requestController: controller });
|
||||
if (pageActive) Object.assign(profile, data);
|
||||
} catch (error) {
|
||||
if (pageActive && !isRequestCancelled(error)) loadError.value = error?.message || "请稍后重试";
|
||||
if (pageActive && !isRequestCancelled(error))
|
||||
loadError.value = error?.message || "请稍后重试";
|
||||
} finally {
|
||||
if (pageActive) loading.value = false;
|
||||
}
|
||||
@@ -58,18 +84,86 @@ const backToProfile = () => returnTo("M01");
|
||||
const openPassword = () => openPage("M04", {}, "M03");
|
||||
const openPhone = () => openPage("M05", {}, "M03");
|
||||
onShow(loadProfile);
|
||||
onUnload(() => { pageActive = false; controller.abort(); });
|
||||
onUnload(() => {
|
||||
pageActive = false;
|
||||
controller.abort();
|
||||
});
|
||||
</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-header,.page-content { z-index:1; }.page-content { padding:18rpx 24rpx 72rpx; }
|
||||
.state-card,.security-card { box-sizing:border-box; min-height:340rpx; padding:54rpx 42rpx 44rpx; @include adaptive.adaptive-family-content; }
|
||||
.state-card { text-align:center; }.state-card text,.security-card text { display:block; }
|
||||
.state-card text:first-child,.security-card__title { 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; }
|
||||
.security-field { display:flex; min-height:82rpx; align-items:center; justify-content:space-between; gap:20rpx; border-bottom:1rpx solid rgba(181,137,63,.3); }
|
||||
.security-field:first-of-type { margin-top:24rpx; }.security-field text:first-child { color:$ink; font-size:24rpx; font-weight:700; }.security-field text:last-child { color:$ink-muted; font-size:22rpx; text-align:right; overflow-wrap:anywhere; }
|
||||
.security-note { margin-top:22rpx; color:$ink-muted; font-size:22rpx; line-height:1.6; }.security-card .app-button { margin-top:20rpx; }
|
||||
.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,
|
||||
.security-card {
|
||||
box-sizing: border-box;
|
||||
min-height: 340rpx;
|
||||
padding: 54rpx 42rpx 44rpx;
|
||||
@include adaptive.adaptive-family-content;
|
||||
}
|
||||
.state-card {
|
||||
text-align: center;
|
||||
}
|
||||
.state-card text,
|
||||
.security-card text {
|
||||
display: block;
|
||||
}
|
||||
.state-card text:first-child,
|
||||
.security-card__title {
|
||||
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;
|
||||
}
|
||||
.security-field {
|
||||
display: flex;
|
||||
min-height: 82rpx;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
border-bottom: 1rpx solid rgba(181, 137, 63, 0.3);
|
||||
}
|
||||
.security-field:first-of-type {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
.security-field text:first-child {
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.security-field text:last-child {
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
text-align: right;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.security-note {
|
||||
margin-top: 22rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.security-card .app-button {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,21 +1,56 @@
|
||||
<!-- 页面编号:M-04;用途:修改登录密码。 -->
|
||||
<template>
|
||||
<view class="password-page" :class="{ 'password-state--ready': passwordState === 'ready', 'password-state--saving': passwordState === 'saving' }">
|
||||
<view
|
||||
class="password-page"
|
||||
:class="{
|
||||
'password-state--ready': passwordState === 'ready',
|
||||
'password-state--saving': passwordState === 'saving',
|
||||
}"
|
||||
>
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="修改密码" custom-back @back="requestBack" /></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="security-tip"
|
||||
><text>设置安全密码</text
|
||||
><text>建议使用 8–32 位字母与数字组合,不要与其他应用共用。</text></view
|
||||
>
|
||||
<view class="form-panel">
|
||||
<view v-for="field in passwordFields" :key="field.key" class="field-block">
|
||||
<view
|
||||
v-for="field in passwordFields"
|
||||
:key="field.key"
|
||||
class="field-block"
|
||||
>
|
||||
<view class="form-row">
|
||||
<text>{{ field.label }}</text>
|
||||
<input v-model="passwordForm[field.key]" :password="!passwordVisible[field.key]" maxlength="32" :aria-label="field.label" :placeholder="field.placeholder" @input="errors[field.key] = ''" />
|
||||
<view class="password-toggle" role="button" :aria-label="`${passwordVisible[field.key] ? '隐藏' : '显示'}${field.label}`" @click="togglePassword(field.key)">{{ passwordVisible[field.key] ? "隐藏" : "显示" }}</view>
|
||||
<input
|
||||
v-model="passwordForm[field.key]"
|
||||
:password="!passwordVisible[field.key]"
|
||||
maxlength="32"
|
||||
:aria-label="field.label"
|
||||
:placeholder="field.placeholder"
|
||||
@input="errors[field.key] = ''"
|
||||
/>
|
||||
<view
|
||||
class="password-toggle"
|
||||
role="button"
|
||||
:aria-label="`${passwordVisible[field.key] ? '隐藏' : '显示'}${field.label}`"
|
||||
@click="togglePassword(field.key)"
|
||||
>{{ passwordVisible[field.key] ? "隐藏" : "显示" }}</view
|
||||
>
|
||||
</view>
|
||||
<text v-if="errors[field.key]" class="field-error">{{ errors[field.key] }}</text>
|
||||
<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 +76,11 @@ 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 {
|
||||
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";
|
||||
@@ -51,7 +90,11 @@ import {
|
||||
} from "@/utils/validation.js";
|
||||
|
||||
const passwordForm = reactive({ current: "", next: "", confirm: "" });
|
||||
const passwordVisible = reactive({ current: false, next: false, confirm: false });
|
||||
const passwordVisible = reactive({
|
||||
current: false,
|
||||
next: false,
|
||||
confirm: false,
|
||||
});
|
||||
const errors = reactive({ current: "", next: "", confirm: "" });
|
||||
const passwordState = ref("ready");
|
||||
const discardVisible = ref(false);
|
||||
@@ -60,7 +103,9 @@ const passwordFields = [
|
||||
{ key: "next", label: "新密码", placeholder: "8–32 位字母与数字" },
|
||||
{ key: "confirm", label: "确认新密码", placeholder: "请再次输入新密码" },
|
||||
];
|
||||
const toastVisible = ref(false); const toastMessage = ref(""); let timer = null;
|
||||
const toastVisible = ref(false);
|
||||
const toastMessage = ref("");
|
||||
let timer = null;
|
||||
const passwordRequestController = createRequestController();
|
||||
let pageActive = true;
|
||||
const formSnapshot = computed(() => JSON.stringify(passwordForm));
|
||||
@@ -72,25 +117,39 @@ const discardConfirmation = createDiscardConfirmation((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 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 = () => {
|
||||
errors.current = passwordForm.current ? "" : "请输入当前密码";
|
||||
const nextPassword = validatePassword(passwordForm.next);
|
||||
errors.next = nextPassword.valid ? "" : PASSWORD_POLICY_MESSAGE;
|
||||
if (!errors.next && passwordForm.next === passwordForm.current)
|
||||
errors.next = "新密码不能与当前密码相同";
|
||||
errors.confirm = !passwordForm.confirm ? "请再次输入新密码" : passwordForm.confirm !== passwordForm.next ? "两次输入的新密码不一致" : "";
|
||||
errors.confirm = !passwordForm.confirm
|
||||
? "请再次输入新密码"
|
||||
: passwordForm.confirm !== passwordForm.next
|
||||
? "两次输入的新密码不一致"
|
||||
: "";
|
||||
return !Object.values(errors).some(Boolean);
|
||||
};
|
||||
const savePassword = async () => {
|
||||
if (!validateForm() || passwordState.value === "saving") return;
|
||||
passwordState.value = "saving";
|
||||
try {
|
||||
await appApi.changePassword({
|
||||
oldPasswordHash: calcMD5(passwordForm.current),
|
||||
newPasswordHash: calcMD5(passwordForm.next),
|
||||
}, { requestController: passwordRequestController });
|
||||
await appApi.changePassword(
|
||||
{
|
||||
oldPasswordHash: calcMD5(passwordForm.current),
|
||||
newPasswordHash: calcMD5(passwordForm.next),
|
||||
},
|
||||
{ requestController: passwordRequestController },
|
||||
);
|
||||
if (!pageActive) return;
|
||||
passwordForm.current = "";
|
||||
passwordForm.next = "";
|
||||
@@ -125,5 +184,100 @@ onUnload(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
.password-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.security-tip,.form-panel{@include adaptive.adaptive-profile-content}.security-tip{min-height:170rpx;padding:38rpx 44rpx;text-align:center}.security-tip text{display:block}.security-tip text:first-child{color:$ink;font-size:32rpx;font-weight:700}.security-tip text:last-child{margin-top:10rpx;color:$ink-muted;font-size:22rpx;line-height:1.55}.form-panel{margin-top:20rpx;padding:22rpx 34rpx 30rpx}.field-block+.field-block{margin-top:6rpx}.form-row{display:grid;grid-template-columns:142rpx minmax(0,1fr) 74rpx;min-height:92rpx;align-items:center;gap:12rpx;border-bottom:1px solid rgba(181,137,63,.42)}.form-row>text{color:$ink;font-size:23rpx;font-weight:700}.form-row input{width:auto;min-width:0;min-height:68rpx;color:$ink;font-size:23rpx}.password-toggle{display:flex;min-height:68rpx;align-items:center;justify-content:flex-end;color:$brand-red;font-size:21rpx}.field-error{display:block;padding-top:7rpx;color:#b42318;font-size:20rpx;line-height:1.4;text-align:right}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.form-panel{padding-right:26rpx;padding-left:26rpx}.form-row{grid-template-columns:120rpx minmax(0,1fr) 64rpx;gap:8rpx}}
|
||||
.password-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.page-layer {
|
||||
z-index: 1;
|
||||
}
|
||||
.page-content {
|
||||
flex: 1;
|
||||
padding: 28rpx 30rpx 72rpx;
|
||||
}
|
||||
.security-tip,
|
||||
.form-panel {
|
||||
@include adaptive.adaptive-profile-content;
|
||||
}
|
||||
.security-tip {
|
||||
min-height: 170rpx;
|
||||
padding: 38rpx 44rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.security-tip text {
|
||||
display: block;
|
||||
}
|
||||
.security-tip text:first-child {
|
||||
color: $ink;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.security-tip text:last-child {
|
||||
margin-top: 10rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.form-panel {
|
||||
margin-top: 20rpx;
|
||||
padding: 22rpx 34rpx 30rpx;
|
||||
}
|
||||
.field-block + .field-block {
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 142rpx minmax(0, 1fr) 74rpx;
|
||||
min-height: 92rpx;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
border-bottom: 1px solid rgba(181, 137, 63, 0.42);
|
||||
}
|
||||
.form-row > text {
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.form-row input {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
min-height: 68rpx;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.password-toggle {
|
||||
display: flex;
|
||||
min-height: 68rpx;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
color: $brand-red;
|
||||
font-size: 21rpx;
|
||||
}
|
||||
.field-error {
|
||||
display: block;
|
||||
padding-top: 7rpx;
|
||||
color: #b42318;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.4;
|
||||
text-align: right;
|
||||
}
|
||||
.page-content > .app-button {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
@media (max-width: 340px) {
|
||||
.page-content {
|
||||
padding-right: 22rpx;
|
||||
padding-left: 22rpx;
|
||||
}
|
||||
.form-panel {
|
||||
padding-right: 26rpx;
|
||||
padding-left: 26rpx;
|
||||
}
|
||||
.form-row {
|
||||
grid-template-columns: 120rpx minmax(0, 1fr) 64rpx;
|
||||
gap: 8rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,66 @@
|
||||
<!-- 页面编号:M-05;用途:换绑手机号。缺专用发码/资料 DTO 且需人工 TAC/短信时保持关闭。 -->
|
||||
<template>
|
||||
<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>为保护账户安全,请联系管理员协助办理手机号换绑。</text><AppButton block label="返回账号与安全" @click="backToSecurity" /></view></view></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>为保护账户安全,请联系管理员协助办理手机号换绑。</text
|
||||
><AppButton
|
||||
block
|
||||
label="返回账号与安全"
|
||||
@click="backToSecurity" /></view></view
|
||||
></view>
|
||||
</template>
|
||||
<script setup>
|
||||
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");
|
||||
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-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}
|
||||
.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>
|
||||
|
||||
@@ -5,16 +5,57 @@
|
||||
<view class="page-layer"><PageHeader title="帮助中心" /></view>
|
||||
<view class="page-content page-layer">
|
||||
<text class="help-source-note">{{ sourceNote }}</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.value" class="category-chip" :class="{ active: activeCategory === category.value }" role="button" @click="activeCategory = category.value">{{ category.label }}</view></view></scroll-view>
|
||||
<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.value"
|
||||
class="category-chip"
|
||||
:class="{ active: activeCategory === category.value }"
|
||||
role="button"
|
||||
@click="activeCategory = category.value"
|
||||
>{{ category.label }}</view
|
||||
></view
|
||||
></scroll-view
|
||||
>
|
||||
<view v-if="filteredQuestions.length" class="question-list">
|
||||
<view v-for="item in filteredQuestions" :key="item.id" class="question-card">
|
||||
<view class="question-heading" role="button" :aria-label="item.question" @click="toggleQuestion(item.id)"><text>{{ item.question }}</text><text>{{ expandedIds.includes(item.id) ? "收起" : "查看" }}</text></view>
|
||||
<text v-if="expandedIds.includes(item.id)" class="answer">{{ item.answer }}</text>
|
||||
<view
|
||||
v-for="item in filteredQuestions"
|
||||
:key="item.id"
|
||||
class="question-card"
|
||||
>
|
||||
<view
|
||||
class="question-heading"
|
||||
role="button"
|
||||
:aria-label="item.question"
|
||||
@click="toggleQuestion(item.id)"
|
||||
><text>{{ item.question }}</text
|
||||
><text>{{
|
||||
expandedIds.includes(item.id) ? "收起" : "查看"
|
||||
}}</text></view
|
||||
>
|
||||
<text v-if="expandedIds.includes(item.id)" class="answer">{{
|
||||
item.answer
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-card"><text>没有找到相关问题</text><text>换个关键词试试,或直接提交意见反馈。</text></view>
|
||||
<AppButton block type="secondary" label="联系家谱助手" @click="contactSupport" />
|
||||
<view v-else class="empty-card"
|
||||
><text>没有找到相关问题</text
|
||||
><text>换个关键词试试,或直接提交意见反馈。</text></view
|
||||
>
|
||||
<AppButton
|
||||
block
|
||||
type="secondary"
|
||||
label="联系家谱助手"
|
||||
@click="contactSupport"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -25,39 +66,71 @@ import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
|
||||
import {
|
||||
appApi,
|
||||
createRequestController,
|
||||
isRequestCancelled,
|
||||
} from "@/utils/api.js";
|
||||
import { openPage } from "@/utils/navigation.js";
|
||||
|
||||
const questions = ref([]);
|
||||
const helpState = ref("loading");
|
||||
const controller = createRequestController();
|
||||
let active = true;
|
||||
const categoryLabels = Object.freeze({ common: "常见问题", member: "成员管理", lineage: "世系关系" });
|
||||
const categoryLabels = Object.freeze({
|
||||
common: "常见问题",
|
||||
member: "成员管理",
|
||||
lineage: "世系关系",
|
||||
});
|
||||
const helpCategories = computed(() => [
|
||||
{ value: "", label: "全部" },
|
||||
...[...new Set(questions.value.map((item) => item.category).filter(Boolean))].map((value) => ({ value, label: categoryLabels[value.toLowerCase()] || value })),
|
||||
...[
|
||||
...new Set(questions.value.map((item) => item.category).filter(Boolean)),
|
||||
].map((value) => ({
|
||||
value,
|
||||
label: categoryLabels[value.toLowerCase()] || value,
|
||||
})),
|
||||
]);
|
||||
const activeCategory = ref("");
|
||||
const keyword = ref("");
|
||||
const expandedIds = ref([]);
|
||||
const sourceNote = computed(() => ({ loading: "正在读取帮助文章…", error: "帮助文章暂时无法读取,请稍后重试。", empty: "暂未收到可展示的帮助文章。", ready: "帮助内容来自服务端,可按分类或关键词查找。" })[helpState.value]);
|
||||
const sourceNote = computed(
|
||||
() =>
|
||||
({
|
||||
loading: "正在读取帮助文章…",
|
||||
error: "帮助文章暂时无法读取,请稍后重试。",
|
||||
empty: "暂未收到可展示的帮助文章。",
|
||||
ready: "帮助内容来自服务端,可按分类或关键词查找。",
|
||||
})[helpState.value],
|
||||
);
|
||||
const filteredQuestions = computed(() => {
|
||||
const query = keyword.value.toLowerCase();
|
||||
return questions.value.filter((item) => (!activeCategory.value || item.category === activeCategory.value) && (!query || `${item.question}${item.answer}`.toLowerCase().includes(query)));
|
||||
return questions.value.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 toggleQuestion = (id) => {
|
||||
expandedIds.value = expandedIds.value.includes(id)
|
||||
? expandedIds.value.filter((item) => item !== id)
|
||||
: [...expandedIds.value, id];
|
||||
};
|
||||
const contactSupport = () => openPage("M07", {}, "M06");
|
||||
const loadHelpArticles = async () => {
|
||||
controller.abort();
|
||||
helpState.value = "loading";
|
||||
try {
|
||||
const rows = await appApi.getHelpArticles({ requestController: controller });
|
||||
const rows = await appApi.getHelpArticles({
|
||||
requestController: controller,
|
||||
});
|
||||
if (!active) return;
|
||||
questions.value = rows.map((item) => ({
|
||||
id: String(item.helpId),
|
||||
category: String(item.helpCategory || "其他"),
|
||||
question: String(item.helpTitle || "未命名帮助文章"),
|
||||
answer: String(item.helpContent || "暂无正文")
|
||||
answer: String(item.helpContent || "暂无正文"),
|
||||
}));
|
||||
helpState.value = questions.value.length ? "ready" : "empty";
|
||||
} catch (error) {
|
||||
@@ -66,11 +139,149 @@ const loadHelpArticles = async () => {
|
||||
}
|
||||
};
|
||||
onLoad(loadHelpArticles);
|
||||
onShow(() => { if (helpState.value !== "loading") loadHelpArticles(); });
|
||||
onUnload(() => { active = false; controller.abort(); });
|
||||
onShow(() => {
|
||||
if (helpState.value !== "loading") loadHelpArticles();
|
||||
});
|
||||
onUnload(() => {
|
||||
active = false;
|
||||
controller.abort();
|
||||
});
|
||||
</script>
|
||||
|
||||
<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}.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}}
|
||||
.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, 0.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>
|
||||
|
||||
+239
-26
@@ -11,11 +11,20 @@
|
||||
}"
|
||||
>
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="意见反馈" custom-back @back="requestBack" /></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-field" role="group" aria-label="反馈类型(选填)" aria-describedby="feedback-type-note">
|
||||
<text id="feedback-type-note" class="field-note">反馈类型为选填,可再次点击取消选择。</text>
|
||||
<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"
|
||||
@@ -25,7 +34,9 @@
|
||||
:aria-pressed="feedbackForm.feedbackType === type"
|
||||
:disabled="feedbackState === 'submitting'"
|
||||
@click="selectFeedbackType(type)"
|
||||
>{{ type }}</button>
|
||||
>
|
||||
{{ type }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-panel">
|
||||
@@ -35,7 +46,11 @@
|
||||
maxlength="500"
|
||||
aria-label="问题描述"
|
||||
aria-required="true"
|
||||
:aria-describedby="errors.feedbackContent ? 'feedback-content-help feedback-content-error' : 'feedback-content-help'"
|
||||
:aria-describedby="
|
||||
errors.feedbackContent
|
||||
? 'feedback-content-help feedback-content-error'
|
||||
: 'feedback-content-help'
|
||||
"
|
||||
:aria-invalid="Boolean(errors.feedbackContent)"
|
||||
:disabled="feedbackState === 'submitting'"
|
||||
:focus="feedbackContentFocused"
|
||||
@@ -43,9 +58,18 @@
|
||||
@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>
|
||||
<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
|
||||
@@ -68,8 +92,14 @@
|
||||
}"
|
||||
:role="feedbackResultTone === 'error' ? 'alert' : 'status'"
|
||||
:aria-live="feedbackResultTone === 'error' ? 'assertive' : 'polite'"
|
||||
>{{ feedbackResult }}</view>
|
||||
<AppButton block :disabled="submitDisabled" :label="submitLabel" @click="submitFeedback" />
|
||||
>{{ feedbackResult }}</view
|
||||
>
|
||||
<AppButton
|
||||
block
|
||||
:disabled="submitDisabled"
|
||||
:label="submitLabel"
|
||||
@click="submitFeedback"
|
||||
/>
|
||||
</view>
|
||||
<AppDialog
|
||||
:visible="discardVisible"
|
||||
@@ -93,12 +123,20 @@ 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 { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
|
||||
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({ feedbackType: "", feedbackContent: "", contactInfo: "" });
|
||||
const feedbackForm = reactive({
|
||||
feedbackType: "",
|
||||
feedbackContent: "",
|
||||
contactInfo: "",
|
||||
});
|
||||
const errors = reactive({ feedbackContent: "" });
|
||||
const feedbackState = ref("ready");
|
||||
const feedbackResult = ref("");
|
||||
@@ -117,10 +155,13 @@ const normalizedForm = computed(() => ({
|
||||
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 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 "正在提交";
|
||||
@@ -136,7 +177,9 @@ const requestDiscardConfirmation = discardConfirmation.request;
|
||||
const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
const validateFeedback = () => {
|
||||
errors.feedbackContent = feedbackForm.feedbackContent.trim() ? "" : "请填写问题描述";
|
||||
errors.feedbackContent = feedbackForm.feedbackContent.trim()
|
||||
? ""
|
||||
: "请填写问题描述";
|
||||
if (errors.feedbackContent) {
|
||||
feedbackContentFocused.value = false;
|
||||
nextTick(() => {
|
||||
@@ -156,14 +199,23 @@ const feedbackErrorMessage = (error) => {
|
||||
if (error?.httpStatus === 401) {
|
||||
return "登录状态已失效,服务器未确认提交成功。请重新登录后再处理。";
|
||||
}
|
||||
return error?.message ? `服务器未确认提交成功:${error.message}` : "服务器未确认提交成功,请稍后再试。";
|
||||
return error?.message
|
||||
? `服务器未确认提交成功:${error.message}`
|
||||
: "服务器未确认提交成功,请稍后再试。";
|
||||
};
|
||||
const submittedFeedbackMessage = "反馈已提交。以下内容为本次提交记录;修改任一项后可提交新反馈。";
|
||||
const submittedFeedbackMessage =
|
||||
"反馈已提交。以下内容为本次提交记录;修改任一项后可提交新反馈。";
|
||||
const submittedWithPendingEditsMessage = "上一份反馈已提交,当前修改尚未提交。";
|
||||
const uncertainFeedbackMessage = "提交结果可能未知。为避免重复记录,当前内容不能直接重提;请稍后确认或修改后提交新反馈。";
|
||||
const uncertainWithPendingEditsMessage = "上一份反馈的提交结果仍待确认,当前修改尚未提交。";
|
||||
const uncertainFeedbackMessage =
|
||||
"提交结果可能未知。为避免重复记录,当前内容不能直接重提;请稍后确认或修改后提交新反馈。";
|
||||
const uncertainWithPendingEditsMessage =
|
||||
"上一份反馈的提交结果仍待确认,当前修改尚未提交。";
|
||||
const isFeedbackResultUncertain = (error) => {
|
||||
if (["REQUEST_TIMEOUT", "NETWORK_ERROR", "RESPONSE_INVALID"].includes(error?.code)) {
|
||||
if (
|
||||
["REQUEST_TIMEOUT", "NETWORK_ERROR", "RESPONSE_INVALID"].includes(
|
||||
error?.code,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (error?.code !== "HTTP_ERROR") return false;
|
||||
@@ -173,9 +225,12 @@ const isFeedbackResultUncertain = (error) => {
|
||||
const submitFeedback = async () => {
|
||||
if (!validateFeedback() || feedbackState.value === "submitting") return;
|
||||
if (
|
||||
(lastSubmittedSnapshot.value && formSnapshot.value === lastSubmittedSnapshot.value) ||
|
||||
(lastUncertainSnapshot.value && formSnapshot.value === lastUncertainSnapshot.value)
|
||||
) return;
|
||||
(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";
|
||||
@@ -259,5 +314,163 @@ onUnload(() => {
|
||||
|
||||
<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-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}}
|
||||
.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: 0.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: 0.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, 0.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, 0.24);
|
||||
border-radius: 12rpx;
|
||||
font-size: 21rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.feedback-result--success {
|
||||
border-color: rgba(41, 112, 66, 0.34);
|
||||
background: rgba(234, 246, 237, 0.88);
|
||||
color: #245f39;
|
||||
}
|
||||
.feedback-result--error {
|
||||
border-color: rgba(180, 35, 24, 0.3);
|
||||
background: rgba(255, 241, 239, 0.9);
|
||||
color: #8f241c;
|
||||
}
|
||||
.feedback-result--uncertain {
|
||||
border-color: rgba(155, 101, 22, 0.34);
|
||||
background: rgba(255, 248, 229, 0.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>
|
||||
|
||||
+155
-14
@@ -1,23 +1,42 @@
|
||||
<template>
|
||||
<view class="promotion-page">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="推广中心" custom-back @back="requestBack" /></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" />
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/brand-seal.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>家谱服务推荐</text>
|
||||
<text>以下推荐内容由服务端配置。</text>
|
||||
</view>
|
||||
<view v-if="state === 'loading'" class="state-card"><AppLoading text="正在读取推广内容" /></view>
|
||||
<view v-if="state === 'loading'" class="state-card"
|
||||
><AppLoading text="正在读取推广内容"
|
||||
/></view>
|
||||
<view v-else-if="state === 'error'" class="state-card">
|
||||
<text>暂时无法读取推广内容</text><AppButton block type="secondary" label="重新加载" @click="loadPromotions" />
|
||||
<text>暂时无法读取推广内容</text
|
||||
><AppButton
|
||||
block
|
||||
type="secondary"
|
||||
label="重新加载"
|
||||
@click="loadPromotions"
|
||||
/>
|
||||
</view>
|
||||
<view v-else-if="state === 'empty'" class="state-card"><text>当前没有可展示的推广内容</text></view>
|
||||
<view v-else-if="state === 'empty'" class="state-card"
|
||||
><text>当前没有可展示的推广内容</text></view
|
||||
>
|
||||
<view v-else class="promotion-list">
|
||||
<view v-for="item in promotions" :key="item.id" class="promotion-card">
|
||||
<text class="promotion-title">{{ item.title }}</text>
|
||||
<text v-if="item.platform" class="promotion-platform">{{ item.platform }}</text>
|
||||
<text v-if="item.description" class="promotion-copy">{{ item.description }}</text>
|
||||
<text v-if="item.platform" class="promotion-platform">{{
|
||||
item.platform
|
||||
}}</text>
|
||||
<text v-if="item.description" class="promotion-copy">{{
|
||||
item.description
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -31,15 +50,28 @@ import AppButton from "@/components/AppButton.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
|
||||
import {
|
||||
appApi,
|
||||
createRequestController,
|
||||
isRequestCancelled,
|
||||
} from "@/utils/api.js";
|
||||
import { handleBackPress, runBackGuard } from "@/utils/navigation.js";
|
||||
|
||||
const promotions = ref([]);
|
||||
const state = ref("loading");
|
||||
const controller = createRequestController();
|
||||
let active = true;
|
||||
const platformLabels = Object.freeze({ app: "APP 应用", pc: "管理后台", wechat: "微信小程序" });
|
||||
const formatPlatform = (value) => platformLabels[String(value || "").trim().toLowerCase()] || String(value || "").trim();
|
||||
const platformLabels = Object.freeze({
|
||||
app: "APP 应用",
|
||||
pc: "管理后台",
|
||||
wechat: "微信小程序",
|
||||
});
|
||||
const formatPlatform = (value) =>
|
||||
platformLabels[
|
||||
String(value || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
] || String(value || "").trim();
|
||||
const loadPromotions = async () => {
|
||||
controller.abort();
|
||||
state.value = "loading";
|
||||
@@ -50,7 +82,7 @@ const loadPromotions = async () => {
|
||||
id: String(item.promotionId),
|
||||
title: String(item.promotionTitle || "未命名推广"),
|
||||
description: String(item.promotionDesc || ""),
|
||||
platform: formatPlatform(item.platform)
|
||||
platform: formatPlatform(item.platform),
|
||||
}));
|
||||
state.value = promotions.value.length ? "ready" : "empty";
|
||||
} catch (error) {
|
||||
@@ -60,12 +92,121 @@ const loadPromotions = async () => {
|
||||
};
|
||||
const requestBack = () => runBackGuard({});
|
||||
onLoad(loadPromotions);
|
||||
onShow(() => { if (state.value !== "loading") loadPromotions(); });
|
||||
onUnload(() => { active = false; controller.abort(); });
|
||||
onShow(() => {
|
||||
if (state.value !== "loading") loadPromotions();
|
||||
});
|
||||
onUnload(() => {
|
||||
active = false;
|
||||
controller.abort();
|
||||
});
|
||||
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,.state-card,.promotion-card{@include adaptive.adaptive-profile-content}.invite-hero{display:flex;min-height:250rpx;flex-direction:column;align-items:center;justify-content:center;padding:38rpx 48rpx;text-align:center}.invite-hero image{width:90rpx;height:102rpx}.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}.state-card{display:flex;min-height:210rpx;flex-direction:column;align-items:center;justify-content:center;margin-top:20rpx;padding:38rpx;text-align:center;color:$ink;font-size:26rpx}.state-card .app-button{width:100%;margin-top:22rpx}.promotion-list{display:flex;flex-direction:column;gap:16rpx;margin-top:20rpx}.promotion-card{position:relative;display:flex;min-height:142rpx;flex-direction:column;padding:30rpx 34rpx}.promotion-title{color:$ink;font-size:28rpx;font-weight:700;overflow-wrap:anywhere}.promotion-platform{align-self:flex-start;margin-top:10rpx;padding:3rpx 12rpx;border:1rpx solid rgba(181,46,34,.26);border-radius:999rpx;color:$brand-red;font-size:20rpx;line-height:1.3}.promotion-copy{margin-top:12rpx;color:$ink-muted;font-size:22rpx;line-height:1.55;overflow-wrap:anywhere}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}}
|
||||
.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,
|
||||
.state-card,
|
||||
.promotion-card {
|
||||
@include adaptive.adaptive-profile-content;
|
||||
}
|
||||
.invite-hero {
|
||||
display: flex;
|
||||
min-height: 250rpx;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 38rpx 48rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.invite-hero image {
|
||||
width: 90rpx;
|
||||
height: 102rpx;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.state-card {
|
||||
display: flex;
|
||||
min-height: 210rpx;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 20rpx;
|
||||
padding: 38rpx;
|
||||
text-align: center;
|
||||
color: $ink;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.state-card .app-button {
|
||||
width: 100%;
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
.promotion-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.promotion-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 142rpx;
|
||||
flex-direction: column;
|
||||
padding: 30rpx 34rpx;
|
||||
}
|
||||
.promotion-title {
|
||||
color: $ink;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.promotion-platform {
|
||||
align-self: flex-start;
|
||||
margin-top: 10rpx;
|
||||
padding: 3rpx 12rpx;
|
||||
border: 1rpx solid rgba(181, 46, 34, 0.26);
|
||||
border-radius: 999rpx;
|
||||
color: $brand-red;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.promotion-copy {
|
||||
margin-top: 12rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.55;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
@media (max-width: 340px) {
|
||||
.page-content {
|
||||
padding-right: 22rpx;
|
||||
padding-left: 22rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,43 @@
|
||||
<template>
|
||||
<view class="orders-page">
|
||||
<ModulePageBackground module="profile" />
|
||||
<view class="page-layer"><PageHeader title="VIP 与订单" custom-back @back="requestBack" /></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></view>
|
||||
<view v-if="state === 'loading'" class="state-card"><AppLoading text="正在读取服务套餐" /></view>
|
||||
<view v-else-if="state === 'error'" class="state-card"><text>暂时无法读取套餐或订单</text><AppButton block type="secondary" label="重新加载" @click="loadVip" /></view>
|
||||
<view class="service-card"
|
||||
><text>服务套餐</text
|
||||
><text>套餐和订单均从服务端读取;本页不发起支付或创建订单。</text></view
|
||||
>
|
||||
<view v-if="state === 'loading'" class="state-card"
|
||||
><AppLoading text="正在读取服务套餐"
|
||||
/></view>
|
||||
<view v-else-if="state === 'error'" class="state-card"
|
||||
><text>暂时无法读取套餐或订单</text
|
||||
><AppButton block type="secondary" label="重新加载" @click="loadVip"
|
||||
/></view>
|
||||
<template v-else>
|
||||
<view v-if="packages.length" class="package-list"><view v-for="item in packages" :key="item.id" class="package-card"><view class="package-heading"><text>{{ item.name }}</text><text v-if="item.price">¥{{ item.price }}</text></view><text v-if="item.description">{{ item.description }}</text><text v-if="item.duration">{{ item.duration }}</text></view></view>
|
||||
<view v-else class="state-card"><text>当前没有可展示的服务套餐</text></view>
|
||||
<view class="section-heading"><text>订单记录</text><text>{{ orders.length }} 条</text></view>
|
||||
<view v-if="orders.length" class="order-list"><view v-for="item in orders" :key="item.id" class="order-card"><text>{{ item.title }}</text><text>{{ item.status }}</text></view></view>
|
||||
<view v-if="packages.length" class="package-list"
|
||||
><view v-for="item in packages" :key="item.id" class="package-card"
|
||||
><view class="package-heading"
|
||||
><text>{{ item.name }}</text
|
||||
><text v-if="item.price">¥{{ item.price }}</text></view
|
||||
><text v-if="item.description">{{ item.description }}</text
|
||||
><text v-if="item.duration">{{ item.duration }}</text></view
|
||||
></view
|
||||
>
|
||||
<view v-else class="state-card"
|
||||
><text>当前没有可展示的服务套餐</text></view
|
||||
>
|
||||
<view class="section-heading"
|
||||
><text>订单记录</text><text>{{ orders.length }} 条</text></view
|
||||
>
|
||||
<view v-if="orders.length" class="order-list"
|
||||
><view v-for="item in orders" :key="item.id" class="order-card"
|
||||
><text>{{ item.title }}</text
|
||||
><text>{{ item.status }}</text></view
|
||||
></view
|
||||
>
|
||||
<view v-else class="order-empty"><text>暂无订单记录</text></view>
|
||||
</template>
|
||||
</view>
|
||||
@@ -24,7 +51,11 @@ import AppButton from "@/components/AppButton.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
|
||||
import {
|
||||
appApi,
|
||||
createRequestController,
|
||||
isRequestCancelled,
|
||||
} from "@/utils/api.js";
|
||||
import { handleBackPress, runBackGuard } from "@/utils/navigation.js";
|
||||
|
||||
const packages = ref([]);
|
||||
@@ -32,28 +63,44 @@ const orders = ref([]);
|
||||
const state = ref("loading");
|
||||
const controller = createRequestController();
|
||||
let active = true;
|
||||
const formatPrice = (value) => Number.isFinite(Number(value)) ? Number(value).toFixed(2) : "";
|
||||
const formatPrice = (value) =>
|
||||
Number.isFinite(Number(value)) ? Number(value).toFixed(2) : "";
|
||||
const durationUnits = Object.freeze({ year: "年", month: "个月", day: "天" });
|
||||
const formatDuration = (value, unit) => {
|
||||
if (value === undefined || value === null || value === "") return "";
|
||||
if (Number.isFinite(Number(value)) && Number(value) <= 0) return "永久有效";
|
||||
const normalizedUnit = String(unit || "").trim().toLowerCase();
|
||||
return durationUnits[normalizedUnit] ? `${value} ${durationUnits[normalizedUnit]}` : String(value);
|
||||
const normalizedUnit = String(unit || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return durationUnits[normalizedUnit]
|
||||
? `${value} ${durationUnits[normalizedUnit]}`
|
||||
: String(value);
|
||||
};
|
||||
const loadVip = async () => {
|
||||
controller.abort();
|
||||
state.value = "loading";
|
||||
try {
|
||||
const packageRows = await appApi.getVipPackages({ requestController: controller });
|
||||
const orderRows = await appApi.getVipOrders({ requestController: controller });
|
||||
const packageRows = await appApi.getVipPackages({
|
||||
requestController: controller,
|
||||
});
|
||||
const orderRows = await appApi.getVipOrders({
|
||||
requestController: controller,
|
||||
});
|
||||
if (!active) return;
|
||||
packages.value = packageRows.map((item) => ({
|
||||
id: String(item.packageId), name: String(item.packageName || "未命名套餐"), description: String(item.packageDesc || ""),
|
||||
price: formatPrice(item.price), duration: formatDuration(item.durationValue, item.durationUnit)
|
||||
id: String(item.packageId),
|
||||
name: String(item.packageName || "未命名套餐"),
|
||||
description: String(item.packageDesc || ""),
|
||||
price: formatPrice(item.price),
|
||||
duration: formatDuration(item.durationValue, item.durationUnit),
|
||||
}));
|
||||
orders.value = orderRows.map((item) => ({
|
||||
id: String(item.orderId || item.id || ""), title: String(item.packageName || item.orderNo || "VIP 订单"), status: String(item.orderStatus || item.status || "" )
|
||||
})).filter((item) => item.id);
|
||||
orders.value = orderRows
|
||||
.map((item) => ({
|
||||
id: String(item.orderId || item.id || ""),
|
||||
title: String(item.packageName || item.orderNo || "VIP 订单"),
|
||||
status: String(item.orderStatus || item.status || ""),
|
||||
}))
|
||||
.filter((item) => item.id);
|
||||
state.value = "ready";
|
||||
} catch (error) {
|
||||
if (!active || isRequestCancelled(error)) return;
|
||||
@@ -62,12 +109,138 @@ const loadVip = async () => {
|
||||
};
|
||||
const requestBack = () => runBackGuard({});
|
||||
onLoad(loadVip);
|
||||
onShow(() => { if (state.value !== "loading") loadVip(); });
|
||||
onUnload(() => { active = false; controller.abort(); });
|
||||
onShow(() => {
|
||||
if (state.value !== "loading") loadVip();
|
||||
});
|
||||
onUnload(() => {
|
||||
active = false;
|
||||
controller.abort();
|
||||
});
|
||||
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,.state-card,.package-card,.order-card,.order-empty{@include adaptive.adaptive-profile-content}.service-card{min-height:170rpx;padding:38rpx 44rpx;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:last-child{margin-top:12rpx;color:$ink-muted;font-size:21rpx;line-height:1.55}.state-card{display:flex;min-height:190rpx;flex-direction:column;align-items:center;justify-content:center;margin-top:18rpx;padding:36rpx;text-align:center;color:$ink;font-size:26rpx}.state-card .app-button{width:100%;margin-top:22rpx}.package-list,.order-list{display:flex;flex-direction:column;gap:14rpx;margin-top:18rpx}.package-card{padding:28rpx 32rpx}.package-heading{display:flex;align-items:center;justify-content:space-between;gap:16rpx;color:$ink;font-size:27rpx;font-weight:700}.package-heading text:last-child{flex:0 0 auto;color:$brand-red}.package-card>text{display:block;margin-top:10rpx;color:$ink-muted;font-size:21rpx;line-height:1.5}.section-heading{display:flex;justify-content:space-between;margin:28rpx 6rpx 12rpx;color:$ink;font-size:27rpx;font-weight:700}.section-heading text:last-child{color:$ink-muted;font-size:21rpx;font-weight:400}.order-card{display:flex;justify-content:space-between;gap:18rpx;padding:26rpx 32rpx;color:$ink;font-size:24rpx}.order-card text:last-child{color:$ink-muted}.order-empty{padding:38rpx;text-align:center;color:$ink-muted;font-size:23rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}}
|
||||
.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,
|
||||
.state-card,
|
||||
.package-card,
|
||||
.order-card,
|
||||
.order-empty {
|
||||
@include adaptive.adaptive-profile-content;
|
||||
}
|
||||
.service-card {
|
||||
min-height: 170rpx;
|
||||
padding: 38rpx 44rpx;
|
||||
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:last-child {
|
||||
margin-top: 12rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 21rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.state-card {
|
||||
display: flex;
|
||||
min-height: 190rpx;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 18rpx;
|
||||
padding: 36rpx;
|
||||
text-align: center;
|
||||
color: $ink;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.state-card .app-button {
|
||||
width: 100%;
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
.package-list,
|
||||
.order-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.package-card {
|
||||
padding: 28rpx 32rpx;
|
||||
}
|
||||
.package-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
color: $ink;
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.package-heading text:last-child {
|
||||
flex: 0 0 auto;
|
||||
color: $brand-red;
|
||||
}
|
||||
.package-card > text {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 21rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.section-heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 28rpx 6rpx 12rpx;
|
||||
color: $ink;
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.section-heading text:last-child {
|
||||
color: $ink-muted;
|
||||
font-size: 21rpx;
|
||||
font-weight: 400;
|
||||
}
|
||||
.order-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
padding: 26rpx 32rpx;
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.order-card text:last-child {
|
||||
color: $ink-muted;
|
||||
}
|
||||
.order-empty {
|
||||
padding: 38rpx;
|
||||
text-align: center;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
@media (max-width: 340px) {
|
||||
.page-content {
|
||||
padding-right: 22rpx;
|
||||
padding-left: 22rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,17 +1,67 @@
|
||||
<!-- 页面编号:M-10;用途:协议、隐私、版本与退出登录。 -->
|
||||
<template>
|
||||
<view class="about-page" :class="{ 'about-state--logging-out': logoutSubmitting }">
|
||||
<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-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="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
|
||||
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="退出登录" @click="logoutVisible = true" />
|
||||
<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="logoutSubmitting ? '正在退出' : '确认退出'" cancel-text="取消" show-cancel :close-on-mask="false" @confirm="confirmLogout" @cancel="logoutVisible = false" @close="logoutVisible = 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="logoutSubmitting ? '正在退出' : '确认退出'"
|
||||
cancel-text="取消"
|
||||
show-cancel
|
||||
:close-on-mask="false"
|
||||
@confirm="confirmLogout"
|
||||
@cancel="logoutVisible = false"
|
||||
@close="logoutVisible = false"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -29,16 +79,35 @@ import { session } from "@/utils/session.js";
|
||||
|
||||
const appVersion = manifest.versionName;
|
||||
const agreementItems = [
|
||||
{ key: "terms", label: "用户协议", note: "了解账号与服务使用规则", copy: "用户协议正文将在正式协议服务接入后展示。当前页面不代表最终法律文本。" },
|
||||
{ key: "privacy", label: "隐私政策", note: "了解个人资料如何使用与保护", copy: "隐私政策正文将在正式协议服务接入后展示。敏感资料默认按权限与脱敏规则展示。" },
|
||||
{ key: "version", label: "版本说明", note: `当前版本 ${appVersion}`, copy: `当前安装版本为 ${appVersion}。基础版本采用浅色国风主题。` },
|
||||
{
|
||||
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 logoutSubmitting = ref(false);
|
||||
const logoutRequestController = createRequestController();
|
||||
const openAgreement = (item) => { activeAgreement.label = item.label; activeAgreement.copy = item.copy; agreementVisible.value = true; };
|
||||
const openAgreement = (item) => {
|
||||
activeAgreement.label = item.label;
|
||||
activeAgreement.copy = item.copy;
|
||||
agreementVisible.value = true;
|
||||
};
|
||||
const confirmLogout = async () => {
|
||||
if (logoutSubmitting.value) return false;
|
||||
logoutSubmitting.value = true;
|
||||
@@ -68,5 +137,98 @@ onUnload(() => logoutRequestController.abort());
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
.about-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.brand-card{@include adaptive.adaptive-profile-summary;display:flex;min-height:300rpx;flex-direction:column;align-items:center;justify-content:center;padding:36rpx 44rpx;text-align:center}.brand-card image{width:92rpx;height:106rpx}.brand-card text{display:block}.brand-card text:nth-child(2){margin-top:6rpx;color:$ink;font-family:STKaiti,KaiTi,serif;font-size:40rpx;font-weight:700;letter-spacing:4rpx}.brand-card text:nth-child(3){margin-top:8rpx;color:$ink-muted;font-size:22rpx;line-height:1.5}.brand-card text:last-child{margin-top:10rpx;color:$ink-muted;font-size:20rpx}.settings-list{display:flex;flex-direction:column;gap:12rpx;margin-top:22rpx}.settings-row{@include adaptive.adaptive-profile-field;display:grid;grid-template-columns:minmax(0,1fr) 32rpx;min-height:104rpx;align-items:center;gap:18rpx;padding:18rpx 30rpx}.settings-row view{min-width:0}.settings-row text{display:block;overflow-wrap:anywhere}.settings-row text:first-child{color:$ink;font-size:24rpx;font-weight:700}.settings-row text:last-child{margin-top:6rpx;color:$ink-muted;font-size:20rpx}.settings-row image{width:30rpx;height:30rpx}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}}
|
||||
.about-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.page-layer {
|
||||
z-index: 1;
|
||||
}
|
||||
.page-content {
|
||||
flex: 1;
|
||||
padding: 28rpx 30rpx 72rpx;
|
||||
}
|
||||
.brand-card {
|
||||
@include adaptive.adaptive-profile-summary;
|
||||
display: flex;
|
||||
min-height: 300rpx;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 36rpx 44rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.brand-card image {
|
||||
width: 92rpx;
|
||||
height: 106rpx;
|
||||
}
|
||||
.brand-card text {
|
||||
display: block;
|
||||
}
|
||||
.brand-card text:nth-child(2) {
|
||||
margin-top: 6rpx;
|
||||
color: $ink;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
.brand-card text:nth-child(3) {
|
||||
margin-top: 8rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.brand-card text:last-child {
|
||||
margin-top: 10rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
.settings-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
.settings-row {
|
||||
@include adaptive.adaptive-profile-field;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 32rpx;
|
||||
min-height: 104rpx;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
padding: 18rpx 30rpx;
|
||||
}
|
||||
.settings-row view {
|
||||
min-width: 0;
|
||||
}
|
||||
.settings-row text {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.settings-row text:first-child {
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.settings-row text:last-child {
|
||||
margin-top: 6rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
.settings-row image {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
}
|
||||
.page-content > .app-button {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
@media (max-width: 340px) {
|
||||
.page-content {
|
||||
padding-right: 22rpx;
|
||||
padding-left: 22rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user