Files
jiapuapp/pages/profile/home.vue
T

818 lines
21 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-metadata" aria-label="个人资料摘要">
<view class="profile-metadata__item">
<image
class="profile-metadata__icon"
src="/static/assets/modules/profile/transparent/icon-person.png"
mode="aspectFit"
aria-hidden="true"
/>
<view><text>性别</text><text class="app-single-line">{{ sexText }}</text></view>
</view>
<view class="profile-metadata__item">
<image
class="profile-metadata__icon"
src="/static/assets/modules/profile/transparent/icon-calendar.png"
mode="aspectFit"
aria-hidden="true"
/>
<view><text>生日</text><text class="app-single-line">{{ birthdayText }}</text></view>
</view>
<view class="profile-metadata__item profile-metadata__item--email">
<image
class="profile-metadata__icon"
src="/static/assets/modules/profile/transparent/icon-envelope.png"
mode="aspectFit"
aria-hidden="true"
/>
<view><text>邮箱</text><text class="app-long-value">{{ profile.email || "未设置" }}</text></view>
</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>
</template>
<AppPromotionStrip v-if="!loading && !profileHomeError" placement="profile_bottom" title="为你推荐" />
<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 AppPromotionStrip from "@/components/AppPromotionStrip.vue";
import AppTabbar from "@/components/AppTabbar.vue";
import {
PROFILE_SEX_OPTIONS
} from "@/services/api/profile-contract.js";
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 { openPage } from "@/utils/navigation/gateway.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 serviceGroups = [
{
title: "账户与服务",
items: [
{
routeKey: "M03",
label: "账号与安全",
iconVariant: "security",
icon: "/static/assets/modules/profile/transparent/icon-security.png",
},
{
routeKey: "N01",
label: "消息中心",
iconVariant: "notifications",
icon: "/static/assets/modules/profile/transparent/icon-message.png",
},
{
routeKey: "M11",
label: "活动邀请",
iconVariant: "invitations",
icon: "/static/assets/modules/profile/transparent/icon-message.png",
},
{
routeKey: "M09",
label: "VIP 服务",
iconVariant: "vip",
icon: "/static/assets/modules/profile/transparent/icon-vip.png",
},
{
routeKey: "M12",
label: "收益与提现",
iconVariant: "earnings",
icon: "/static/assets/modules/profile/transparent/icon-vip.png",
},
],
},
{
title: "支持与其他",
items: [
{
routeKey: "M06",
label: "帮助中心",
iconVariant: "help",
icon: "/static/assets/modules/profile/transparent/icon-help.png",
},
{
routeKey: "M07",
label: "意见反馈",
iconVariant: "feedback",
icon: "/static/assets/modules/profile/transparent/icon-feedback.png",
},
{
routeKey: "M08",
label: "应用推广",
iconVariant: "promotion",
icon: "/static/assets/modules/profile/transparent/icon-promotion.png",
},
{
routeKey: "M10",
label: "关于与设置",
iconVariant: "settings",
icon: "/static/assets/modules/profile/transparent/icon-settings.png",
},
],
},
];
const profileSexLabels = Object.freeze(
Object.fromEntries(PROFILE_SEX_OPTIONS.map(({ value, label }) => [value, label])),
);
const sexText = computed(
() => profileSexLabels[String(profile.sex)] || "未设置",
);
const birthdayText = computed(() =>
/^\d{4}-\d{2}-\d{2}/.test(profile.birthday)
? profile.birthday.slice(0, 10)
: "未设置",
);
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) => openPage(item.routeKey, {}, "M01");
const openEdit = () => openPage("M02", {}, "M01");
const openSettings = () => openPage("M10", {}, "M01");
onShow(() => {
void loadProfile();
void loadRecommendationPreference();
});
onPageScroll(({ scrollTop = 0 }) => {
compactHeader.value = scrollTop > 44;
});
onUnload(() => {
isPageActive = false;
compactHeader.value = false;
profileReadController.abort();
preferenceReadController.abort();
preferenceSaveController.abort();
});
</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: 52rpx;
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-metadata {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
z-index: 1;
margin: -36rpx 34rpx 0;
padding: 10rpx 12rpx 12rpx;
border: 1rpx solid rgba(117, 83, 52, 0.09);
border-radius: 18rpx;
background: rgba(239, 234, 224, 0.92);
}
.profile-metadata__item {
display: flex;
min-width: 0;
align-items: flex-start;
gap: 12rpx;
padding: 14rpx 12rpx;
border-right: 0;
}
.profile-metadata__item:first-child {
border-right: 1rpx solid rgba(117, 83, 52, 0.16);
}
.profile-metadata__item--email {
grid-column: 1 / -1;
border-top: 1rpx solid rgba(117, 83, 52, 0.16);
}
.profile-metadata__icon {
width: 56rpx;
height: 56rpx;
flex: 0 0 auto;
filter: sepia(0.7) saturate(0.8) hue-rotate(330deg) brightness(0.82);
}
.profile-metadata__item view {
min-width: 0;
}
.profile-metadata__item text {
display: block;
}
.profile-metadata__item text:first-child {
color: #857263;
font-size: clamp(13px, 20rpx, 16px);
}
.profile-metadata__item text:last-child {
margin-top: 4rpx;
color: #382c25;
font-size: clamp(15px, 24rpx, 18px);
line-height: 1.25;
overflow-wrap: anywhere;
}
.profile-metadata__item--email text:last-child {
font-size: clamp(13px, 20rpx, 16px);
line-height: 1.35;
}
.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-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;
}
</style>