Files
jiapuapp/pages/profile/m01-profile-home.vue
T
2026-07-29 18:16:08 +08:00

606 lines
14 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.
<!-- 页面编号M-01用途个人中心显示当前用户资料与服务入口 -->
<template>
<view
class="profile-page"
:class="{
'profile-state--ready': !loading && !loadError,
'profile-state--error': Boolean(loadError),
}"
>
<view v-if="compactHeader" class="profile-compact-header" aria-hidden="true">
<text>我的</text>
</view>
<view v-if="loading" class="profile-state-card">
<text>正在读取个人资料</text>
</view>
<view v-else-if="loadError" class="profile-state-card">
<text>个人资料暂时无法读取</text>
<text>{{ loadError }}</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/m01-profile-archive-hero-v2.png"
mode="scaleToFill"
aria-hidden="true"
/>
<view class="profile-hero__content">
<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/m01-icon-settings.png"
mode="aspectFit"
aria-hidden="true"
/></view>
</view>
<view class="profile-hero__identity">
<image
class="profile-hero__emblem"
src="/static/assets/modules/profile/transparent/m01-profile-tree-medallion.png"
mode="aspectFit"
aria-hidden="true"
/>
<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/m01-icon-person.png"
mode="aspectFit"
aria-hidden="true"
/>
<view><text>性别</text><text>{{ sexText }}</text></view>
</view>
<view class="profile-metadata__item">
<image
class="profile-metadata__icon"
src="/static/assets/modules/profile/transparent/m01-icon-calendar.png"
mode="aspectFit"
aria-hidden="true"
/>
<view><text>生日</text><text>{{ birthdayText }}</text></view>
</view>
<view class="profile-metadata__item profile-metadata__item--email">
<image
class="profile-metadata__icon"
src="/static/assets/modules/profile/transparent/m01-icon-envelope.png"
mode="aspectFit"
aria-hidden="true"
/>
<view><text>邮箱</text><text>{{ profile.email || "未设置" }}</text></view>
</view>
</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.routeKey.toLowerCase()}`,
]"
: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>
<AppTabbar active="profile" />
</view>
</template>
<script setup>
import { computed, reactive, ref } from "vue";
import { onPageScroll, onShow, onUnload } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppTabbar from "@/components/AppTabbar.vue";
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 loading = ref(false);
const loadError = ref("");
const compactHeader = ref(false);
const requestController = createRequestController();
let pageActive = true;
const serviceGroups = [
{
title: "账户与服务",
items: [
{
routeKey: "M03",
label: "账号与安全",
icon: "/static/assets/modules/profile/transparent/m01-icon-security.png",
},
{
routeKey: "N01",
label: "消息中心",
icon: "/static/assets/modules/profile/transparent/m01-icon-message.png",
},
{
routeKey: "M09",
label: "VIP 服务",
icon: "/static/assets/modules/profile/transparent/m01-icon-vip.png",
},
],
},
{
title: "支持与其他",
items: [
{
routeKey: "M06",
label: "帮助中心",
icon: "/static/assets/modules/profile/transparent/m01-icon-help.png",
},
{
routeKey: "M07",
label: "意见反馈",
icon: "/static/assets/modules/profile/transparent/m01-icon-feedback.png",
},
{
routeKey: "M08",
label: "应用推广",
icon: "/static/assets/modules/profile/transparent/m01-icon-promotion.png",
},
{
routeKey: "M10",
label: "关于与设置",
icon: "/static/assets/modules/profile/transparent/m01-icon-settings.png",
},
],
},
];
const sexText = computed(() => (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;
loadError.value = "";
try {
const data = await appApi.getProfile({ requestController });
if (!pageActive) return;
Object.assign(profile, data);
} catch (error) {
if (pageActive && !isRequestCancelled(error)) {
loadError.value = error?.message || "请稍后重试";
}
} finally {
if (pageActive) loading.value = false;
}
};
const openService = (item) => openPage(item.routeKey, {}, "M01");
const openEdit = () => openPage("M02", {}, "M01");
const openSettings = () => openPage("M10", {}, "M01");
onShow(loadProfile);
onPageScroll(({ scrollTop = 0 }) => {
compactHeader.value = scrollTop > 44;
});
onUnload(() => {
pageActive = false;
compactHeader.value = false;
requestController.abort();
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.profile-page {
width: 100%;
min-height: 100vh;
padding-bottom: 166rpx;
box-sizing: border-box;
overflow-x: hidden;
background: #f3eee5 url("/static/assets/modules/profile/opaque/m01-profile-paper-mountains-v1.png") center 360rpx / 100% auto no-repeat;
}
.profile-compact-header {
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 30;
height: 88rpx;
box-sizing: border-box;
padding: 38rpx 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;
font-size: 28rpx;
font-weight: 700;
letter-spacing: 3rpx;
line-height: 1;
pointer-events: none;
}
.profile-state-card {
@include adaptive.adaptive-profile-summary;
margin: 48rpx 30rpx;
padding: 64rpx 44rpx;
color: #382c25;
text-align: center;
}
.profile-state-card text {
display: block;
font-size: 26rpx;
line-height: 1.6;
}
.profile-state-card text + text {
margin-top: 12rpx;
color: #806e5d;
font-size: 22rpx;
}
.profile-state-card .app-button {
@include adaptive.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 {
display: flex;
flex-direction: column;
z-index: 1;
padding: 100rpx 56rpx 50rpx 54rpx;
box-sizing: border-box;
}
.profile-hero__label {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
color: #f0d6a4;
font-family: STKaiti, KaiTi, serif;
font-size: 54rpx;
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: -30rpx;
}
.profile-hero__emblem {
width: 220rpx;
height: 220rpx;
flex: 0 0 auto;
}
.profile-hero__copy {
min-width: 0;
flex: 1;
margin: 46rpx 0 0 -5rpx;
}
.profile-hero__copy text {
display: block;
overflow-wrap: anywhere;
}
.profile-hero__copy text:first-child {
color: #fff2da;
font-family: STSong, SimSun, serif;
font-size: 32rpx;
font-weight: 700;
letter-spacing: 1rpx;
line-height: 1.25;
white-space: nowrap;
}
.profile-hero__copy text:nth-child(2) {
margin-top: 14rpx;
color: #ecd2a4;
font-size: 27rpx;
line-height: 1.35;
}
.profile-hero__copy text:last-child {
margin-top: 18rpx;
color: #e8bd78;
font-size: 28rpx;
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/m01-profile-edit-button-v2.png") center / 100% 100% no-repeat;
box-shadow: none;
color: #fff6e5;
font-size: 22rpx;
font-weight: 700;
letter-spacing: 1rpx;
}
.profile-hero__edit::after {
border: 0;
}
.profile-metadata {
display: flex;
min-height: 146rpx;
z-index: 1;
margin: -36rpx 34rpx 0;
padding: 5rpx 0;
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: center;
flex: 1;
gap: 10rpx;
padding: 0 10rpx;
border-right: 1rpx solid rgba(117, 83, 52, 0.16);
}
.profile-metadata__item:last-child {
border-right: 0;
}
.profile-metadata__item--email {
flex: 2.3;
}
.profile-metadata__icon {
width: 42rpx;
height: 42rpx;
filter: sepia(0.7) saturate(0.8) hue-rotate(330deg) brightness(0.82);
transform: scale(2.1);
}
.profile-metadata__item view {
min-width: 0;
transform: translateY(14rpx);
}
.profile-metadata__item text {
display: block;
}
.profile-metadata__item text:first-child {
color: #857263;
font-size: 20rpx;
}
.profile-metadata__item text:last-child {
margin-top: 0;
color: #382c25;
font-size: 24rpx;
line-height: 1.25;
overflow-wrap: anywhere;
}
.profile-metadata__item--email text:last-child {
font-size: 13rpx;
white-space: nowrap;
}
.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: 34rpx;
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--m03 {
transform: translate(9rpx, -9rpx) scale(1.8);
}
.profile-service-row__icon--n01 {
transform: translate(10rpx, -10rpx) scale(1.8);
}
.profile-service-row__icon--m09 {
transform: translate(-5rpx, 5rpx) scale(1.8);
}
.profile-service-row__icon--m06 {
transform: translate(-3rpx, 7rpx) scale(1.8);
}
.profile-service-row__icon--m07 {
transform: translate(4rpx, 8rpx) scale(1.8);
}
.profile-service-row__icon--m08 {
transform: translate(11rpx, 7rpx) scale(1.8);
}
.profile-service-row__icon--m10 {
transform: translate(9rpx, 7rpx) scale(1.8);
}
.profile-service-row text {
font-size: 32rpx;
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>