Files
jiapuapp/pages/profile/home.vue
T
2026-09-13 17:45:52 +08:00

723 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view
class="profile-page"
:class="{
'profile-state--ready': !loading && !profileHomeError,
'profile-state--error': Boolean(profileHomeError),
}"
>
<view v-if="compactHeader" class="profile-compact-header" aria-hidden="true">
<view class="profile-compact-header__brand-lockup">
<image
src="/static/assets/foundation/transparent/brand-seal.png"
mode="aspectFit"
/>
<text>代代相传</text>
</view>
<text class="profile-compact-header__title">我的</text>
</view>
<view v-if="loading" class="profile-state-card">
<text>正在读取个人资料</text>
</view>
<view v-else-if="profileHomeError" class="profile-state-card">
<text>个人资料暂时无法读取</text>
<text>{{ profileHomeError }}</text>
<AppButton block label="重新读取" @click="loadProfile" />
</view>
<template v-else>
<view class="profile-hero">
<image
class="profile-hero__art"
src="/static/assets/modules/profile/opaque/archive-hero.png"
mode="scaleToFill"
aria-hidden="true"
/>
<view class="profile-hero__content">
<view class="profile-hero__brand-lockup">
<image
src="/static/assets/foundation/transparent/brand-seal.png"
mode="aspectFit"
aria-hidden="true"
/>
<text>代代相传</text>
</view>
<view class="profile-hero__label">
<text>我的</text>
<view
class="profile-hero__setting"
role="button"
aria-label="关于与设置"
@click="openSettings"
><image
class="profile-hero__setting-icon"
src="/static/assets/modules/profile/transparent/icon-settings.png"
mode="aspectFit"
aria-hidden="true"
/></view>
</view>
<view class="profile-hero__identity">
<view class="profile-hero__emblem">
<AppAvatar
:sex="profile.sex"
:src="profile.avatarFile?.accessUrl || ''"
/>
</view>
<view class="profile-hero__copy">
<text>{{ profile.nickName || "未设置昵称" }}</text>
<text>{{ profile.realName || "未设置真实姓名" }}</text>
<text>{{ profile.phone || "未提供手机号" }}</text>
</view>
</view>
<button
class="profile-hero__edit"
@click="openEdit"
>编辑资料</button>
</view>
</view>
<view class="profile-preference" aria-label="个性化推荐设置">
<view>
<text>个性化推荐</text>
<text v-if="preferenceLoading">正在读取设置</text>
<text v-else-if="preferenceError">设置暂不可用</text>
<text v-else>{{ recommendationPreference.enabled ? "已开启将优先展示与家谱互动相关的内容" : "已关闭按常规时间顺序展示内容" }}</text>
</view>
<switch
:checked="recommendationPreference.enabled"
:disabled="preferenceLoading || preferenceSaving || Boolean(preferenceError)"
color="#9f170f"
@change="changeRecommendationPreference"
/>
</view>
<view class="profile-services">
<view
v-for="group in serviceGroups"
:key="group.title"
class="profile-service-group"
>
<view class="profile-section-title">
<view class="profile-section-title__mark" aria-hidden="true"></view>
<text>{{ group.title }}</text>
</view>
<view class="profile-service-list">
<view
v-for="item in group.items"
:key="item.routeKey"
class="profile-service-row"
role="button"
:aria-label="item.label"
@click="openService(item)"
>
<view class="profile-service-row__leading">
<image
:class="[
'profile-service-row__icon',
`profile-service-row__icon--${item.iconVariant}`,
]"
:src="item.icon"
mode="aspectFit"
aria-hidden="true"
/>
<text>{{ item.label }}</text>
</view>
<image
class="profile-service-row__chevron"
src="/static/assets/foundation/transparent/chevron-right.png"
mode="aspectFit"
aria-hidden="true"
/>
</view>
</view>
</view>
</view>
<HomeAdvertisementPanel
class="profile-home-advertisements"
@open="openContentHome"
/>
</template>
<AppTabbar active="profile" />
</view>
</template>
<script setup>
import { computed, reactive, ref } from "vue";
import { onPageScroll, onShow, onUnload } from "@dcloudio/uni-app";
import AppAvatar from "@/components/AppAvatar.vue";
import AppButton from "@/components/AppButton.vue";
import AppTabbar from "@/components/AppTabbar.vue";
import HomeAdvertisementPanel from "@/components/genealogy/HomeAdvertisementPanel.vue";
import {
createRequestController,
isRequestCancelled
} from "@/services/api/request-controller.js";
import { profileApi } from "@/services/api/profile-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import { goRoot, openPage } from "@/utils/navigation/gateway.js";
import { genealogyContext } from "@/utils/genealogy/context.js";
const profile = reactive({
avatarFile: null,
nickName: "",
realName: "",
phone: "",
sex: "",
birthday: "",
email: "",
});
const loading = ref(false);
const profileHomeError = ref("");
const compactHeader = ref(false);
const profileReadController = createRequestController();
const preferenceReadController = createRequestController();
const preferenceSaveController = createRequestController();
const recommendationPreference = reactive({ enabled: false, version: 0, updatedAt: "" });
const preferenceLoading = ref(false);
const preferenceSaving = ref(false);
const preferenceError = ref("");
let isPageActive = true;
const profileIcons = Object.freeze({
security: "/static/assets/modules/profile/transparent/icon-security.png",
notifications: "/static/assets/modules/profile/transparent/icon-message.png",
vip: "/static/assets/modules/profile/transparent/icon-vip.png",
help: "/static/assets/modules/profile/transparent/icon-help.png",
feedback: "/static/assets/modules/profile/transparent/icon-feedback.png",
settings: "/static/assets/modules/profile/transparent/icon-settings.png",
});
const serviceItem = (routeKey, label, iconVariant, params = null) => ({
routeKey,
label,
iconVariant,
icon: profileIcons[iconVariant],
...(params ? { params } : {}),
});
const serviceGroups = Object.freeze([
{
title: "支持与其他",
items: [
serviceItem("M06", "帮助中心", "help"),
serviceItem("M07", "意见反馈", "feedback"),
serviceItem("M03", "账号与安全", "security"),
serviceItem("M10", "关于与设置", "settings"),
serviceItem("F13", "网站公告", "notifications", {
title: "网站公告",
articleType: "notice",
}),
serviceItem("F13", "家谱新闻列表(图文)", "feedback", {
title: "家谱新闻",
articleType: "news",
}),
{
...serviceItem("F04", "家谱系列文章(图文)", "feedback"),
requiresGenealogy: true,
},
],
},
]);
const loadProfile = async () => {
if (loading.value) return;
loading.value = true;
profileHomeError.value = "";
try {
const profileResponse = await profileApi.getProfile({ requestController: profileReadController });
if (!isPageActive) return;
Object.assign(profile, profileResponse);
} catch (error) {
if (isPageActive && !isRequestCancelled(error)) {
profileHomeError.value = getRequestErrorMessage(error, "请稍后重试");
}
} finally {
if (isPageActive) loading.value = false;
}
};
const loadRecommendationPreference = async () => {
if (preferenceLoading.value || preferenceSaving.value) return;
preferenceLoading.value = true;
preferenceError.value = "";
try {
const preferenceResponse = await profileApi.getRecommendationPreference({
requestController: preferenceReadController,
});
if (isPageActive) Object.assign(recommendationPreference, preferenceResponse);
} catch (error) {
if (isPageActive && !isRequestCancelled(error)) {
preferenceError.value = getRequestErrorMessage(error, "请稍后重试");
}
} finally {
if (isPageActive) preferenceLoading.value = false;
}
};
const changeRecommendationPreference = async (event) => {
if (preferenceLoading.value || preferenceSaving.value || preferenceError.value) return;
const enabled = Boolean(event?.detail?.value);
preferenceSaving.value = true;
try {
const savedPreference = await profileApi.updateRecommendationPreference(enabled, {
requestController: preferenceSaveController,
});
if (isPageActive) Object.assign(recommendationPreference, savedPreference);
} catch (error) {
if (isPageActive && !isRequestCancelled(error)) {
preferenceError.value = getRequestErrorMessage(error, "设置未保存,请稍后重试");
}
} finally {
if (isPageActive) preferenceSaving.value = false;
}
};
const openService = (item) => {
const params = { ...(item.params || {}) };
if (item.requiresGenealogy) {
const genealogyId = genealogyContext.getCurrentGenealogyId();
if (!genealogyId) {
uni.showToast({ title: "请先在家谱页选择家谱", icon: "none" });
return Promise.resolve(false);
}
params.genealogyId = genealogyId;
}
return openPage(item.routeKey, params, "M01");
};
const openEdit = () => openPage("M02", {}, "M01");
const openSettings = () => openPage("M10", {}, "M01");
const openContentHome = () => goRoot("F01");
onShow(() => {
void loadProfile();
void loadRecommendationPreference();
});
onPageScroll(({ scrollTop = 0 }) => {
compactHeader.value = scrollTop > 44;
});
const disposeProfilePortal = () => {
isPageActive = false;
compactHeader.value = false;
profileReadController.abort();
preferenceReadController.abort();
preferenceSaveController.abort();
};
onUnload(disposeProfilePortal);
</script>
<style scoped lang="scss">
@import "../../styles/adaptive-frame-profiles.scss";
.profile-page {
position: relative;
width: 100%;
min-height: 100vh;
padding-bottom: 166rpx;
box-sizing: border-box;
overflow-x: hidden;
background: #f3eee5 url("/static/assets/modules/profile/opaque/paper-mountains.png") center 360rpx / 100% auto no-repeat;
}
.profile-compact-header {
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 30;
height: calc(104rpx + var(--status-bar-height, 0px));
box-sizing: border-box;
padding: calc(var(--status-bar-height, 0px) + 46rpx) 48rpx 0;
background: linear-gradient(90deg, #6a1d17, #4e120f);
box-shadow: 0 2rpx 12rpx rgba(58, 16, 12, 0.22);
color: #f2d9a8;
font-family: STKaiti, KaiTi, serif;
pointer-events: none;
}
.profile-compact-header__brand-lockup {
position: absolute;
top: calc(var(--status-bar-height, 0px) + 8rpx);
left: 50%;
display: flex;
align-items: center;
gap: 7rpx;
transform: translateX(-50%);
white-space: nowrap;
}
.profile-compact-header__brand-lockup image {
width: 32rpx;
height: 32rpx;
}
.profile-compact-header__brand-lockup text {
color: #f2d9a8;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(11px, 19rpx, 14px);
font-weight: 700;
letter-spacing: 3rpx;
line-height: 1;
}
.profile-compact-header__title {
display: block;
color: #f2d9a8;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(16px, 28rpx, 20px);
font-weight: 700;
letter-spacing: 3rpx;
line-height: 1;
}
.profile-state-card {
@include adaptive-profile-summary;
margin: 48rpx 30rpx;
padding: 64rpx 44rpx;
color: #382c25;
text-align: center;
}
.profile-state-card text {
display: block;
font-size: clamp(16px, 26rpx, 20px);
line-height: 1.6;
}
.profile-state-card text + text {
margin-top: 12rpx;
color: #806e5d;
font-size: clamp(14px, 22rpx, 17px);
}
.profile-state-card .app-button {
@include adaptive-scroll-button(primary);
margin-top: 28rpx;
}
.profile-hero {
display: grid;
width: 100%;
height: 503rpx;
}
.profile-hero__art,
.profile-hero__content {
grid-area: 1 / 1;
}
.profile-hero__art {
width: 100%;
height: 480rpx;
pointer-events: none;
}
.profile-hero__content {
position: relative;
display: flex;
flex-direction: column;
z-index: 1;
padding: 100rpx 56rpx 50rpx 54rpx;
box-sizing: border-box;
}
.profile-hero__brand-lockup {
position: absolute;
top: calc(var(--status-bar-height, 0px) + 4rpx);
left: 50%;
display: flex;
align-items: center;
gap: 7rpx;
transform: translateX(-50%);
white-space: nowrap;
}
.profile-hero__brand-lockup image {
width: 32rpx;
height: 32rpx;
}
.profile-hero__brand-lockup text {
color: #f2d9a8;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(11px, 19rpx, 14px);
font-weight: 700;
letter-spacing: 3rpx;
line-height: 1;
}
.profile-hero__label {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
color: #f0d6a4;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(27px, 54rpx, 34px);
font-weight: 700;
letter-spacing: 5rpx;
line-height: 1;
}
.profile-hero__setting {
display: flex;
width: 48rpx;
height: 48rpx;
align-items: center;
justify-content: center;
}
.profile-hero__setting-icon {
width: 42rpx;
height: 42rpx;
filter: grayscale(1) sepia(0.9) saturate(1.6) hue-rotate(348deg) brightness(1.35);
transform: scale(1.25);
}
.profile-hero__identity {
display: flex;
min-width: 0;
align-items: flex-start;
position: relative;
margin-top: 43rpx;
margin-left: -22rpx;
column-gap: 20rpx;
}
.profile-hero__emblem {
display: block;
width: 196rpx;
height: 196rpx;
flex: 0 0 auto;
box-sizing: border-box;
border: 4rpx solid #d8ad5d;
border-radius: 50%;
background: #fff7e6;
box-shadow: 0 8rpx 18rpx rgba(44, 15, 12, 0.32);
overflow: hidden;
}
.profile-hero__copy {
min-width: 0;
flex: 1;
margin: 40rpx 0 0;
}
.profile-hero__copy text {
display: block;
overflow-wrap: anywhere;
}
.profile-hero__copy text:first-child {
color: #fff2da;
font-family: STSong, SimSun, serif;
font-size: clamp(17px, 32rpx, 22px);
font-weight: 700;
letter-spacing: 1rpx;
line-height: 1.25;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.profile-hero__copy text:nth-child(2) {
margin-top: 14rpx;
color: #ecd2a4;
font-size: clamp(16px, 27rpx, 20px);
line-height: 1.35;
}
.profile-hero__copy text:last-child {
margin-top: 18rpx;
color: #e8bd78;
font-size: clamp(16px, 28rpx, 20px);
letter-spacing: 1rpx;
}
.profile-hero__edit {
position: absolute;
top: 337rpx;
right: 61rpx;
width: 154rpx;
min-height: 72rpx;
margin: 0;
padding: 0;
border: 0;
border-radius: 0;
background: transparent url("/static/assets/modules/profile/transparent/edit-button.png") center / 100% 100% no-repeat;
box-shadow: none;
color: #fff6e5;
font-size: clamp(14px, 22rpx, 17px);
font-weight: 700;
letter-spacing: 1rpx;
}
.profile-hero__edit::after {
border: 0;
}
.profile-preference {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
margin: 18rpx 34rpx 0;
padding: 22rpx 24rpx;
border: 1rpx solid rgba(117, 83, 52, 0.12);
border-radius: 18rpx;
background: rgba(239, 234, 224, 0.82);
}
.profile-preference > view {
min-width: 0;
flex: 1;
}
.profile-preference text {
display: block;
}
.profile-preference text:first-child {
color: #382c25;
font-size: clamp(15px, 26rpx, 19px);
font-weight: 700;
}
.profile-preference text:last-child {
margin-top: 6rpx;
color: #857263;
font-size: clamp(12px, 20rpx, 15px);
line-height: 1.4;
}
.profile-preference switch {
flex: 0 0 auto;
transform: scale(0.88);
transform-origin: right center;
}
.profile-services {
margin: 49rpx 58rpx 0 48rpx;
}
.profile-home-advertisements {
margin: 28rpx 24rpx 0;
}
.profile-service-group + .profile-service-group {
margin-top: 51rpx;
}
.profile-section-title {
display: flex;
align-items: center;
color: #382c25;
font-family: STSong, SimSun, serif;
font-size: clamp(19px, 34rpx, 24px);
font-weight: 700;
letter-spacing: 2rpx;
}
.profile-section-title__mark {
width: 6rpx;
height: 24rpx;
border-radius: 6rpx;
background: #8d2722;
pointer-events: none;
flex: 0 0 auto;
margin-right: 24rpx;
}
.profile-service-list {
margin-top: 2rpx;
}
.profile-service-row {
display: flex;
min-height: 96rpx;
align-items: center;
justify-content: space-between;
gap: 24rpx;
border-bottom: 1rpx solid rgba(112, 85, 61, 0.2);
color: #45362d;
}
.profile-service-row__leading {
display: flex;
min-width: 0;
align-items: center;
gap: 30rpx;
padding-left: 7rpx;
}
.profile-service-row__icon {
width: 44rpx;
height: 44rpx;
flex: 0 0 auto;
transform: translate(7rpx, 0) scale(1.8);
}
.profile-service-row__icon--security {
transform: translate(9rpx, -9rpx) scale(1.8);
}
.profile-service-row__icon--notifications {
transform: translate(10rpx, -10rpx) scale(1.8);
}
.profile-service-row__icon--vip {
transform: translate(-5rpx, 5rpx) scale(1.8);
}
.profile-service-row__icon--help {
transform: translate(-3rpx, 7rpx) scale(1.8);
}
.profile-service-row__icon--feedback {
transform: translate(4rpx, 8rpx) scale(1.8);
}
.profile-service-row__icon--promotion {
transform: translate(11rpx, 7rpx) scale(1.8);
}
.profile-service-row__icon--settings {
transform: translate(9rpx, 7rpx) scale(1.8);
}
.profile-service-row text {
font-size: clamp(17px, 32rpx, 22px);
line-height: 1.3;
}
.profile-service-row__chevron {
width: 24rpx;
height: 24rpx;
flex: 0 0 auto;
opacity: 0.86;
}
.profile-service-row:active {
opacity: 0.68;
}
@media (max-width: 340px) {
.profile-hero__content {
padding-right: 42rpx;
padding-left: 42rpx;
}
.profile-hero__emblem {
width: 176rpx;
height: 176rpx;
}
.profile-services {
margin-right: 38rpx;
margin-left: 38rpx;
}
}
</style>