Merge branch 'codex/profile-page-merge'

This commit is contained in:
czz
2026-05-09 15:57:35 +08:00
7 changed files with 401 additions and 24 deletions

14
package-lock.json generated
View File

@@ -24,6 +24,7 @@
"@tarojs/webpack5-runner": "^4.0.0",
"@types/react": "^18.2.66",
"babel-preset-taro": "^4.0.0",
"miniprogram-api-typings": "^5.2.0",
"typescript": "^5.4.5"
}
},
@@ -10249,6 +10250,13 @@
"miniprogram-exparser": "latest"
}
},
"node_modules/j-component/node_modules/miniprogram-api-typings": {
"version": "3.12.3",
"resolved": "https://registry.npmmirror.com/miniprogram-api-typings/-/miniprogram-api-typings-3.12.3.tgz",
"integrity": "sha512-o7bOfrU28MEMCBWo83nXv0ROQSBFxJcfCl4f2wTYqah64ipC5RGqLJfvWJTWhlQt2ECVwspSzM8LgvnfMo7TEQ==",
"dev": true,
"license": "MIT"
},
"node_modules/jackspeak": {
"version": "2.3.6",
"resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-2.3.6.tgz",
@@ -11395,9 +11403,9 @@
}
},
"node_modules/miniprogram-api-typings": {
"version": "3.12.3",
"resolved": "https://registry.npmmirror.com/miniprogram-api-typings/-/miniprogram-api-typings-3.12.3.tgz",
"integrity": "sha512-o7bOfrU28MEMCBWo83nXv0ROQSBFxJcfCl4f2wTYqah64ipC5RGqLJfvWJTWhlQt2ECVwspSzM8LgvnfMo7TEQ==",
"version": "5.2.0",
"resolved": "https://registry.npmmirror.com/miniprogram-api-typings/-/miniprogram-api-typings-5.2.0.tgz",
"integrity": "sha512-dkel1zG/eAfApabCtZnr9Y69+5z89GtWVPb6aCTvTJ0gu9mk+A0wCwdxlKWReFfXhcvhuonFrfYDwfSnSEkxsA==",
"dev": true,
"license": "MIT"
},

View File

@@ -26,6 +26,7 @@
"@tarojs/webpack5-runner": "^4.0.0",
"@types/react": "^18.2.66",
"babel-preset-taro": "^4.0.0",
"miniprogram-api-typings": "^5.2.0",
"typescript": "^5.4.5"
}
}

View File

@@ -1,6 +1,7 @@
import "./app.scss";
import type { ReactNode } from "react";
function App(props) {
function App(props: { children?: ReactNode }) {
const { children } = props;
return children ?? null;
}

View File

@@ -1,3 +1,4 @@
export default definePageConfig({
navigationStyle: "custom",
navigationBarTitleText: "个人信息"
});

View File

@@ -1,3 +1,214 @@
.profile-page {
display: block;
min-height: 100vh;
box-sizing: border-box;
padding: calc(var(--profile-top-safe-height, 0px) + 22rpx) 0 72rpx;
background:
linear-gradient(180deg, rgba(39, 44, 60, 0.98) 0 150rpx, transparent 150rpx),
linear-gradient(180deg, #181d2a 0%, #171c29 100%);
color: var(--color-text-primary);
}
.profile-page__nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 26rpx;
}
.profile-page__back {
display: flex;
align-items: center;
justify-content: flex-start;
width: 112rpx;
height: 72rpx;
}
.profile-page__back-icon {
width: 24rpx;
height: 24rpx;
border-left: 4rpx solid var(--color-text-white);
border-bottom: 4rpx solid var(--color-text-white);
transform: rotate(45deg);
}
.profile-page__title {
color: var(--color-text-white);
font-size: 52rpx;
font-weight: 600;
letter-spacing: 2rpx;
}
.profile-page__save {
display: flex;
align-items: center;
justify-content: center;
min-width: 126rpx;
height: 60rpx;
padding: 0 24rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, var(--color-brand-start), var(--color-brand-end));
box-shadow: 0 12rpx 24rpx var(--color-brand-shadow-soft);
}
.profile-page__save-text {
color: var(--color-text-white);
font-size: 28rpx;
}
.profile-page__content {
padding: 70rpx 30rpx 0;
}
.profile-page__avatar-block {
display: flex;
flex-direction: column;
align-items: center;
}
.profile-page__avatar {
display: flex;
align-items: center;
justify-content: center;
width: 154rpx;
height: 154rpx;
border-radius: 50%;
background:
radial-gradient(circle at 50% 34%, #f8fafc 0 20rpx, transparent 20rpx),
linear-gradient(180deg, #b8c1cf 0%, #7f8ba0 100%);
box-shadow: 0 16rpx 40rpx rgba(7, 10, 20, 0.26);
overflow: hidden;
}
.profile-page__avatar-image {
width: 100%;
height: 100%;
border-radius: 50%;
background: linear-gradient(180deg, #dbe2ed 0%, #aeb8c7 100%);
}
.profile-page__avatar-placeholder {
position: relative;
width: 100%;
height: 100%;
border-radius: 50%;
background:
radial-gradient(circle at 50% 34%, #f6f8fb 0 24rpx, transparent 25rpx),
linear-gradient(180deg, #d9dfea 0 58%, #b7c1d1 58% 100%);
}
.profile-page__avatar-head {
position: absolute;
top: 36rpx;
left: 50%;
width: 42rpx;
height: 42rpx;
margin-left: -21rpx;
border-radius: 50%;
background: #2d3650;
}
.profile-page__avatar-body {
position: absolute;
left: 50%;
bottom: 24rpx;
width: 90rpx;
height: 54rpx;
margin-left: -45rpx;
border-radius: 50rpx 50rpx 28rpx 28rpx;
background: #2d3650;
}
.profile-page__avatar-tip {
margin-top: 42rpx;
color: #08e0da;
font-size: 30rpx;
}
.profile-page__nickname-block {
margin: 110rpx 30rpx 0;
padding: 0 12rpx 18rpx;
border-top: 2rpx solid rgba(255, 255, 255, 0.28);
border-bottom: 2rpx solid rgba(255, 255, 255, 0.28);
}
.profile-page__nickname-input {
width: 100%;
height: 108rpx;
color: rgba(255, 255, 255, 0.64);
font-size: 34rpx;
line-height: 108rpx;
text-align: center;
}
.profile-page__nickname-placeholder {
color: rgba(255, 255, 255, 0.28);
}
.profile-page__info-card {
margin-top: 92rpx;
padding: 18rpx 36rpx;
border-radius: 30rpx;
background: rgba(41, 46, 61, 0.96);
box-shadow:
inset 0 0 0 2rpx rgba(255, 255, 255, 0.03),
0 16rpx 34rpx rgba(5, 9, 18, 0.18);
}
.profile-page__info-row {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 154rpx;
gap: 20rpx;
border-bottom: 2rpx solid rgba(255, 255, 255, 0.04);
}
.profile-page__info-row--last {
border-bottom: 0;
}
.profile-page__info-main {
display: flex;
align-items: baseline;
gap: 16rpx;
min-width: 0;
}
.profile-page__info-label {
color: var(--color-text-white);
font-size: 34rpx;
}
.profile-page__info-value {
color: rgba(255, 255, 255, 0.42);
font-size: 28rpx;
}
.profile-page__info-value--muted {
color: rgba(255, 255, 255, 0.26);
}
.profile-page__info-action {
display: flex;
align-items: center;
gap: 18rpx;
flex-shrink: 0;
}
.profile-page__info-action-text {
color: #08e0da;
font-size: 28rpx;
}
.profile-page__info-action-text--muted {
color: rgba(255, 255, 255, 0.42);
}
.profile-page__info-action-text--bound {
color: #08e0da;
}
.profile-page__info-arrow {
color: rgba(255, 255, 255, 0.72);
font-size: 28rpx;
}

View File

@@ -1,26 +1,181 @@
import SecondaryPage, { type SecondaryPageSection } from "../../components/secondary-page";
import { Input, Text, View } from "@tarojs/components";
import Taro from "@tarojs/taro";
import type { CSSProperties } from "react";
import { useEffect, useState } from "react";
import "./index.scss";
const sections: SecondaryPageSection[] = [
{
title: "资料编辑",
items: [
{ label: "头像", value: "支持后续替换" },
{ label: "昵称", value: "张天爱" },
{ label: "手机号", value: "135****2598" },
{ label: "修改密码", value: "待接入" }
]
type ProfileState = {
avatar: string;
nickname: string;
phone: string;
email: string;
wechatBound: boolean;
};
type InfoAction = "replacePhone" | "replaceEmail" | "bindWechat";
const defaultProfile: ProfileState = {
avatar: "",
nickname: "玛利亚",
phone: "139****0753",
email: "",
wechatBound: false
};
function getActionToast(action: InfoAction) {
switch (action) {
case "replacePhone":
return "手机号更换功能待接入";
case "replaceEmail":
return "邮箱更换功能待接入";
case "bindWechat":
return "微信绑定功能待接入";
default:
return "功能待接入";
}
}
];
export default function ProfilePage() {
const [profile, setProfile] = useState(defaultProfile);
const [draftNickname, setDraftNickname] = useState(defaultProfile.nickname);
const [topSafeHeight, setTopSafeHeight] = useState(0);
useEffect(() => {
const windowInfo = typeof Taro.getWindowInfo === "function" ? Taro.getWindowInfo() : Taro.getSystemInfoSync();
const menuButtonRect =
typeof Taro.getMenuButtonBoundingClientRect === "function" ? Taro.getMenuButtonBoundingClientRect() : null;
const safeTop = menuButtonRect?.bottom || windowInfo.statusBarHeight || 0;
setTopSafeHeight(safeTop);
}, []);
const pageStyle = {
"--profile-top-safe-height": `${topSafeHeight}px`
} as CSSProperties;
const showToast = (title: string, icon: "none" | "success" = "none") => {
Taro.showToast({
title,
icon
});
};
const handleBack = () => {
const pages = Taro.getCurrentPages();
if (pages.length > 1) {
Taro.navigateBack({ delta: 1 });
return;
}
Taro.redirectTo({ url: "/pages/mine/index" });
};
const handleSave = () => {
const nickname = draftNickname.trim() || defaultProfile.nickname;
setProfile((current) => ({
...current,
nickname
}));
setDraftNickname(nickname);
showToast("保存成功", "success");
};
const handleAvatarClick = () => {
showToast("头像更换功能待接入");
};
const handleInfoAction = (action: InfoAction) => {
showToast(getActionToast(action));
};
return (
<SecondaryPage
eyebrow="PROFILE"
title="个人信息"
description="这里先保留资料编辑骨架,后续接入真实账号体系时可以直接补上头像上传、昵称编辑和密码修改。"
sections={sections}
footerTip="当前页面以结构预留为主,暂不提交真实修改。"
<View className="profile-page" style={pageStyle}>
<View className="profile-page__nav">
<View className="profile-page__back" onClick={handleBack}>
<View className="profile-page__back-icon" />
</View>
<Text className="profile-page__title"></Text>
<View className="profile-page__save" onClick={handleSave}>
<Text className="profile-page__save-text"></Text>
</View>
</View>
<View className="profile-page__content">
<View className="profile-page__avatar-block" onClick={handleAvatarClick}>
<View className="profile-page__avatar">
{profile.avatar ? (
<View className="profile-page__avatar-image" />
) : (
<View className="profile-page__avatar-placeholder">
<View className="profile-page__avatar-head" />
<View className="profile-page__avatar-body" />
</View>
)}
</View>
<Text className="profile-page__avatar-tip"></Text>
</View>
<View className="profile-page__nickname-block">
<Input
className="profile-page__nickname-input"
value={draftNickname}
maxlength={20}
placeholder="请输入昵称"
placeholderClass="profile-page__nickname-placeholder"
onInput={(event) => setDraftNickname(event.detail.value)}
/>
</View>
<View className="profile-page__info-card">
<View className="profile-page__info-row" onClick={() => handleInfoAction("replacePhone")}>
<View className="profile-page__info-main">
<Text className="profile-page__info-label"></Text>
<Text className="profile-page__info-value">({profile.phone})</Text>
</View>
<View className="profile-page__info-action">
<Text className="profile-page__info-action-text"></Text>
<Text className="profile-page__info-arrow">&gt;</Text>
</View>
</View>
<View className="profile-page__info-row" onClick={() => handleInfoAction("replaceEmail")}>
<View className="profile-page__info-main">
<Text className="profile-page__info-label"></Text>
<Text className="profile-page__info-value profile-page__info-value--muted">
{profile.email || "暂未填写"}
</Text>
</View>
<View className="profile-page__info-action">
<Text className="profile-page__info-action-text"></Text>
<Text className="profile-page__info-arrow">&gt;</Text>
</View>
</View>
<View className="profile-page__info-row profile-page__info-row--last" onClick={() => handleInfoAction("bindWechat")}>
<View className="profile-page__info-main">
<Text className="profile-page__info-label"></Text>
</View>
<View className="profile-page__info-action">
<Text
className={`profile-page__info-action-text ${
profile.wechatBound ? "profile-page__info-action-text--bound" : "profile-page__info-action-text--muted"
}`}
>
{profile.wechatBound ? "已绑定" : "去绑定"}
</Text>
<Text className="profile-page__info-arrow">&gt;</Text>
</View>
</View>
</View>
</View>
</View>
);
}

View File

@@ -18,7 +18,7 @@
},
"types": [
"@tarojs/taro",
"wechat-miniprogram"
"miniprogram-api-typings"
]
},
"include": [