Files
mini-programs/src/user_pages/edit/index.tsx
2026-02-14 12:59:21 +08:00

942 lines
30 KiB
TypeScript
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.
import React, { useState, useEffect } from "react";
import { View, Text, Image, Button } from "@tarojs/components";
import { PopupPicker } from "@/components/Picker/index";
import Taro from "@tarojs/taro";
import "./index.scss";
import { UserService, PickerOption } from "@/services/userService";
import { clear_login_state } from "@/services/loginService";
import { convert_db_gender_to_display } from "@/utils/genderUtils";
import { EditModal, GeneralNavbar } from "@/components";
// import img from "@/config/images";
import CommonDialog from "@/components/CommonDialog";
import {
useUserActions,
useUserInfo,
useNicknameChangeStatus,
} from "@/store/userStore";
import { UserInfoType } from "@/services/userService";
import {
useCities,
useProfessions,
useNtrpLevels,
} from "@/store/pickerOptionsStore";
import { handleCustomerService } from "@/services/userService";
import evaluateService from "@/services/evaluateService";
const EditProfilePage: React.FC = () => {
const { updateUserInfo, updateNickname } = useUserActions();
// 直接从store获取用户信息确保响应式更新
const user_info = useUserInfo();
const nickname_change_status = useNicknameChangeStatus();
const ntrpLevels = useNtrpLevels();
// 表单状态基于store中的用户信息初始化
const getInitialFormData = () => {
const info = user_info as UserInfoType;
return {
nickname: info?.nickname ?? "",
personal_profile: info?.personal_profile ?? "",
occupation: info?.occupation ?? "",
ntrp_level: info?.ntrp_level ?? "",
phone: info?.phone ?? "",
gender: info?.gender ?? "",
birthday: info?.birthday ?? "",
country: info?.country ?? "",
province: info?.province ?? "",
city: info?.city ?? "",
district: info?.district ?? "",
};
};
const [form_data, setFormData] = useState(getInitialFormData());
// 加载状态
const [loading, setLoading] = useState(false);
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
// 编辑弹窗状态
const [edit_modal_visible, setEditModalVisible] = useState(false);
const [editing_field, setEditingField] = useState<string>("");
const [gender_picker_visible, setGenderPickerVisible] = useState(false);
const [birthday_picker_visible, setBirthdayPickerVisible] = useState(false);
const [location_picker_visible, setLocationPickerVisible] = useState(false);
const [ntrp_picker_visible, setNtrpPickerVisible] = useState(false);
const [occupation_picker_visible, setOccupationPickerVisible] =
useState(false);
// 职业数据
const professions = useProfessions();
// 城市数据
const cities = useCities();
const [ntrpTested, setNtrpTested] = useState<boolean>(false);
// 监听store中的用户信息变化同步到表单状态
useEffect(() => {
if (user_info && Object.keys(user_info).length > 0) {
const info = user_info as UserInfoType;
setFormData({
nickname: info?.nickname ?? "",
personal_profile: info?.personal_profile ?? "",
occupation: info?.occupation ?? "",
ntrp_level: info?.ntrp_level ?? "",
phone: info?.phone ?? "",
gender: info?.gender ?? "",
birthday: info?.birthday ?? "",
country: info?.country ?? "",
province: info?.province ?? "",
city: info?.city ?? "",
district: info?.district ?? "",
});
}
const getLastResult = async () => {
// 获取测试结果
const res = await evaluateService.getLastResult();
if (res.code === 0) {
setNtrpTested(res.data.has_test_in_last_month);
}
};
getLastResult();
}, [user_info]);
// 页面加载时初始化数据
// useEffect(() => {
// getCities();
// getProfessions();
// }, []);
// const getProfessions = async () => {
// try {
// const res = await UserService.getProfessions();
// setProfessions(res);
// } catch (e) {
// console.log("获取职业失败:", e);
// }
// };
// const getCities = async () => {
// try {
// const res = await UserService.getCities();
// setCities(res);
// } catch (e) {
// console.log("获取城市列表失败:", e);
// }
// };
// 加载用户信息
// const load_user_info = async () => {
// try {
// setLoading(true);
// const user_data = await UserService.get_user_info();
// setUserInfo(user_data);
// setFormData({
// nickname: user_data.nickname || "",
// personal_profile: user_data.personal_profile || "",
// occupation: user_data.occupation || "",
// ntrp_level: user_data.ntrp_level || "NTRP 4.0",
// phone: user_data.phone || "",
// gender: user_data.gender || "",
// birthday: user_data.birthday || "",
// country: user_data.country || "",
// province: user_data.province || "",
// city: user_data.city || "",
// });
// } catch (error) {
// console.warn("加载用户信息失败:", error);
// Taro.showToast({
// title: "加载用户信息失败",
// icon: "error",
// duration: 2000,
// });
// } finally {
// setLoading(false);
// }
// };
// 处理头像上传
const handle_avatar_upload = () => {
Taro.chooseImage({
count: 1,
sizeType: ["compressed"],
sourceType: ["album", "camera"],
success: async (res) => {
const tempFilePath = res.tempFilePaths[0];
try {
const avatar_url = await UserService.upload_avatar(tempFilePath);
await updateUserInfo({ avatar_url: avatar_url });
Taro.showToast({
title: "头像上传成功",
icon: "success",
});
} catch (error) {
console.warn("头像上传失败:", error);
Taro.showToast({
title: error.message,
icon: "none",
});
}
},
});
};
// 处理编辑弹窗
const handle_open_edit_modal = (field: string) => {
if (field === "gender") {
setGenderPickerVisible(true);
return;
}
if (field === "birthday") {
setBirthdayPickerVisible(true);
return;
}
if (field === "location") {
setLocationPickerVisible(true);
return;
}
if (field === "ntrp_level") {
setNtrpPickerVisible(true);
return;
}
if (field === "occupation") {
setOccupationPickerVisible(true);
return;
}
if (field === "nickname") {
if (!nickname_change_status.can_change) {
return Taro.showToast({
title: `30天内仅可修改4次昵称${nickname_change_status.next_period_start_date}后可修改`,
icon: "none",
duration: 2000,
});
}
// 手动输入
setEditingField(field);
setEditModalVisible(true);
} else {
setEditingField(field);
setEditModalVisible(true);
}
};
const handle_edit_modal_save = async (value: string) => {
try {
// 验证值不能是 undefined 或 null
if (value === undefined || value === null) {
Taro.showToast({
title: "数据不完整,请重新输入",
icon: "none",
});
return;
}
// 调用更新用户信息接口,只传递修改的字段
const update_data = { [editing_field]: value };
editing_field === "nickname"
? await updateNickname(value)
: await updateUserInfo(update_data);
// 更新表单状态store会自动更新
setFormData((prev) => ({ ...prev, [editing_field]: value }));
// 关闭弹窗
setEditModalVisible(false);
setEditingField("");
// 显示成功提示
Taro.showToast({
title: "保存成功",
icon: "success",
});
} catch (error) {
console.warn("保存失败:", error);
Taro.showToast({
title: "保存失败",
icon: "error",
});
}
};
const handle_edit_modal_cancel = () => {
setEditModalVisible(false);
setEditingField("");
};
// 处理字段编辑
const handle_field_edit = async (
field: string | { [key: string]: string },
value?: string
) => {
try {
if (
typeof field === "object" &&
field !== null &&
!Array.isArray(field)
) {
// 验证对象中的值不能是 undefined
const hasUndefined = Object.values(field).some(
(v) => v === undefined || v === null
);
if (hasUndefined) {
Taro.showToast({
title: "数据不完整,请重新选择",
icon: "none",
});
return;
}
await updateUserInfo({ ...field });
// 更新表单状态store会自动更新
setFormData((prev) => ({ ...prev, ...field }));
} else {
// 验证值不能是 undefined
if (value === undefined || value === null) {
Taro.showToast({
title: "数据不完整,请重新选择",
icon: "none",
});
return;
}
// 调用更新用户信息接口,只传递修改的字段
const update_data = { [field as string]: value };
await updateUserInfo(update_data);
// 更新表单状态store会自动更新
setFormData((prev) => ({ ...prev, [field as string]: value }));
}
// 显示成功提示
Taro.showToast({
title: "保存成功",
icon: "success",
});
} catch (error) {
console.warn("保存失败:", error);
Taro.showToast({
title: "保存失败",
icon: "error",
});
}
};
// 处理性别选择
const handle_gender_change = (e: any) => {
if (!Array.isArray(e) || e.length === 0 || e[0] === undefined) {
Taro.showToast({
title: "请选择性别",
icon: "none",
});
return;
}
const gender_value = e[0];
handle_field_edit("gender", String(gender_value));
};
// 处理生日选择
const handle_birthday_change = (e: any) => {
if (!Array.isArray(e) || e.length < 3 || e.some((v) => v === undefined)) {
Taro.showToast({
title: "请完整选择生日",
icon: "none",
});
return;
}
const [year, month, day] = e;
handle_field_edit(
"birthday",
`${year}-${String(month).padStart(2, "0")}-${String(day).padStart(
2,
"0"
)}`
);
};
// 处理地区选择
const handle_location_change = (e: any) => {
if (
!Array.isArray(e) ||
e.length < 3 ||
e.some((v) => v === undefined || v === null)
) {
Taro.showToast({
title: "请完整选择地区",
icon: "none",
});
return;
}
const [province, city, district] = e;
handle_field_edit({
province: String(province ?? ""),
city: String(city ?? ""),
district: String(district ?? ""),
});
};
// 处理NTRP水平选择
const handle_ntrp_level_change = (e: any) => {
// if (!Array.isArray(e) || e.length === 0 || e[0] === undefined) {
// Taro.showToast({
// title: "请选择NTRP水平",
// icon: "none",
// });
// return;
// }
const ntrp_level_value = e[0];
handle_field_edit("ntrp_level", ntrp_level_value);
};
// 处理职业选择
const handle_occupation_change = (e: any) => {
if (
!Array.isArray(e) ||
e.length === 0 ||
e.some((v) => v === undefined || v === null)
) {
Taro.showToast({
title: "请完整选择职业",
icon: "none",
});
return;
}
// 职业可能是多级联动,将所有选中的值用空格连接
const occupation_value = e
.map((v) => String(v ?? ""))
.filter(Boolean)
.join(" ");
handle_field_edit("occupation", occupation_value);
};
// 处理退出登录
const handle_logout = () => {
Taro.showModal({
title: "确认退出",
content: "确定要退出登录吗?",
success: (res) => {
if (res.confirm) {
// 清除用户数据
clear_login_state();
Taro.reLaunch({
url: "/login_pages/index/index",
});
}
},
});
};
// 注销账户
const handle_close_account = () => {
setShowLogoutDialog(true);
};
const onGetPhoneNumber = async (e) => {
if (!e.detail || !e.detail.code) {
Taro.showToast({
title: "获取手机号失败,请重试",
icon: "none",
duration: 2000,
});
return;
}
try {
const phone = await UserService.parse_phone(e.detail.code);
handle_field_edit("phone", phone);
} catch (e) {
console.warn("解析手机号失败:", e);
Taro.showToast({
title: "解析手机号失败,请重试",
icon: "none",
duration: 2000,
});
}
};
const handleJoinGroup = () => {
Taro.navigateTo({
url: "/user_pages/joinGroup/index",
});
};
const getDefaultOption = (options) => {
if (!Array.isArray(options) || options.length === 0) {
return [];
}
const defaultOptions: string[] = [];
let current = options[0];
while (current) {
defaultOptions.push(current.text);
current = current.children?.[0];
}
return defaultOptions;
};
return (
<View className="edit_profile_page">
{/* 导航栏 */}
{/* <View className="custom-navbar">
<View className="detail-navigator">
<View
className="detail-navigator-back"
onClick={() => {
Taro.navigateBack();
}}
>
<Image
className="detail-navigator-back-icon"
src={img.ICON_NAVIGATOR_BACK}
/>
</View>
</View>
</View> */}
{/* 顶部导航栏 */}
<GeneralNavbar
title=""
showBack={true}
showAvatar={false}
onBack={() => {
Taro.navigateBack();
}}
/>
{/* 主要内容 */}
<View className="edit_main_content">
{loading ? (
<View className="loading_container">
<Text className="loading_text">...</Text>
</View>
) : (
<>
{/* 头像编辑区域 */}
<View className="avatar_section">
<View className="avatar_container" onClick={handle_avatar_upload}>
<Image
className="avatar"
src={(user_info as UserInfoType)?.avatar_url || ""}
mode="aspectFill"
/>
<View className="avatar_overlay">
<Image
className="upload_icon"
src={require("@/static/userInfo/edit2.svg")}
/>
</View>
</View>
</View>
{/* 基本信息编辑 */}
<View className="form_section">
{/* 名字 */}
<View className="form_group">
<View
className="form_item"
onClick={() => handle_open_edit_modal("nickname")}
>
<View className="item_left">
<Image
className="item_icon"
src={require("@/static/userInfo/user.svg")}
/>
<Text className="item_label"></Text>
</View>
<View className="item_right">
<Text className="item_value">
{form_data.nickname || ""}
</Text>
<Image
className="arrow_icon"
src={require("@/static/list/icon-list-right-arrow.svg")}
/>
</View>
</View>
<View className="divider"></View>
</View>
{/* 性别 */}
<View className="form_group">
<View
className="form_item"
onClick={() => handle_open_edit_modal("gender")}
>
<View className="item_left">
<Image
className="item_icon"
src={require("@/static/userInfo/gender.svg")}
/>
<Text className="item_label"></Text>
</View>
<View className="item_right">
<Text
className={`item_value ${form_data.gender ? "" : "placeholder"
}`}
>
{convert_db_gender_to_display(form_data.gender)}
</Text>
<Image
className="arrow_icon"
src={require("@/static/list/icon-list-right-arrow.svg")}
/>
</View>
</View>
<View
className="divider"
style="transform: translate(0, 0);"
></View>
</View>
{/* 生日 */}
<View className="form_group">
<View
className="form_item"
onClick={() => handle_open_edit_modal("birthday")}
>
<View className="item_left">
<Image
className="item_icon"
src={require("@/static/userInfo/birthday.svg")}
/>
<Text className="item_label"></Text>
</View>
<View className="item_right">
<Text
className={`item_value ${form_data.birthday ? "" : "placeholder"
}`}
>
{form_data.birthday || "选择生日"}
</Text>
<Image
className="arrow_icon"
src={require("@/static/list/icon-list-right-arrow.svg")}
/>
</View>
</View>
</View>
</View>
{/* 简介编辑 */}
<View className="form_section">
<View className="form_group">
<View
className="form_item"
onClick={() => handle_open_edit_modal("personal_profile")}
>
<View className="item_left">
<Image
className="item_icon"
src={require("@/static/userInfo/introduce.svg")}
/>
<Text className="item_label"></Text>
</View>
<View className="item_right">
<Text
className={`item_value ${form_data.personal_profile ? "" : "placeholder"
}`}
>
{form_data.personal_profile.replace(/\n/g, " ") ||
"介绍一下自己"}
</Text>
<Image
className="arrow_icon"
src={require("@/static/list/icon-list-right-arrow.svg")}
/>
</View>
</View>
</View>
</View>
{/* 地区、NTRP水平、职业 */}
<View className="form_section">
<View className="form_group">
{/* 地区 */}
<View
className="form_item"
onClick={() => handle_open_edit_modal("location")}
>
<View className="item_left">
<Image
className="item_icon"
src={require("@/static/userInfo/gender.svg")}
/>
<Text className="item_label"></Text>
</View>
<View className="item_right">
<Text
className={`item_value ${form_data.province ||
form_data.city ||
form_data.district
? ""
: "placehoder"
}`}
>
{form_data.province ||
form_data.city ||
form_data.district
? `${form_data.province} ${form_data.city} ${form_data.district}`
: "选择所在地区"}
</Text>
<Image
className="arrow_icon"
src={require("@/static/list/icon-list-right-arrow.svg")}
/>
</View>
</View>
<View className="divider"></View>
{/* NTRP水平 */}
<View
className="form_item"
onClick={() => handle_open_edit_modal("ntrp_level")}
>
<View className="item_left">
<Image
className="item_icon"
src={require("@/static/userInfo/ball.svg")}
/>
<Text className="item_label">NTRP </Text>
</View>
<View className="item_right">
<Text
className={`item_value ${form_data.ntrp_level ? "" : "placeholder"
}`}
>
{form_data.ntrp_level || "测测你的 NTRP 水平"}
</Text>
<Image
className="arrow_icon"
src={require("@/static/list/icon-list-right-arrow.svg")}
/>
</View>
</View>
<View className="divider"></View>
{/* 职业 */}
<View
className="form_item"
onClick={() => handle_open_edit_modal("occupation")}
>
<View className="item_left">
<Image
className="item_icon"
src={require("@/static/userInfo/business.svg")}
/>
<Text className="item_label"></Text>
</View>
<View
className={`item_right ${form_data.occupation ? "" : "placeholder"
}`}
>
<Text
className={`item_value ${form_data.occupation ? "" : "placeholder"
}`}
>
{form_data.occupation || "填写你的职业"}
</Text>
<Image
className="arrow_icon"
src={require("@/static/list/icon-list-right-arrow.svg")}
/>
</View>
</View>
</View>
</View>
{/* 手机号 */}
<View className="form_section">
<View className="form_group">
<View className="form_item">
<View className="item_left">
<Image
className="item_icon"
src={require("@/static/userInfo/phone.svg")}
/>
<Text className="item_label"></Text>
</View>
<View className="item_right">
{/* <Input
className="item_input"
value={form_data.phone}
placeholder="请输入手机号"
type="number"
onInput={handle_phone_input}
onBlur={handle_phone_blur}
/> */}
<Button
className={form_data.phone ? "" : "placeholer"}
openType="getPhoneNumber"
onGetPhoneNumber={onGetPhoneNumber}
>
{form_data.phone
? form_data.phone.replace(
/(\d{3})(\d{4})(\d{4})/,
"$1 $2 $3"
)
: "未绑定"}
</Button>
<Image
className="arrow_icon"
src={require("@/static/list/icon-list-right-arrow.svg")}
/>
</View>
</View>
{/* <View className="divider"></View> */}
</View>
</View>
<View className="logout_section group">
<View className="logout_button" onClick={handleCustomerService}>
<Image
src={require("@/static/wallet/custom-service.svg")}
></Image>
<Text className="logout_text"></Text>
</View>
<View className="logout_button" onClick={handleJoinGroup}>
<Image src={require("@/static/userInfo/chat.svg")}></Image>
<Text className="logout_text"></Text>
</View>
</View>
{/* 退出登录 */}
<View className="logout_section">
<View className="logout_button" onClick={handle_logout}>
<Text className="logout_text">退</Text>
</View>
</View>
{/* 注销账号 */}
<View className="logout_section">
<View className="logout_button" onClick={handle_close_account}>
<Text className="logout_text close_account"></Text>
</View>
</View>
</>
)}
</View>
{/* 编辑弹窗 */}
<EditModal
visible={edit_modal_visible}
type={editing_field}
title={editing_field === "nickname" ? "编辑名字" : "编辑简介"}
placeholder={
editing_field === "nickname"
? "请输入您的名字"
: "介绍一下你的喜好,或者训练习惯"
}
initialValue={form_data[editing_field as keyof typeof form_data] || ""}
maxLength={editing_field === "nickname" ? 24 : 100}
invalidCharacters={
editing_field === "nickname"
? /^[\u4e00-\u9fa5a-zA-Z0-9_\-\.\(\)\s]*$/
: null
}
onSave={handle_edit_modal_save}
onCancel={handle_edit_modal_cancel}
validationMessage={
editing_field === "nickname"
? `请填写 2-24 个字符,不包括 @<>/等无效字符。30 天内可修改 4 次昵称,${nickname_change_status.next_period_start_date} 前还可修改 ${nickname_change_status.remaining_count} 次。`
: "请填写 2-100 个字符"
}
/>
{/* 性别选择弹窗 */}
{gender_picker_visible && (
<PopupPicker
showHeader={true}
title="选择性别"
confirmText="保存"
options={[
[
{ text: "男", value: "0" },
{ text: "女", value: "1" },
{ text: "保密", value: "2" },
],
]}
visible={gender_picker_visible}
setvisible={setGenderPickerVisible}
value={form_data.gender === "" ? ["0"] : [form_data.gender]}
onChange={handle_gender_change}
/>
)}
{/* 生日选择弹窗 */}
{birthday_picker_visible && (
<PopupPicker
minYear={1970}
maxYear={new Date().getFullYear()}
showHeader={true}
title="选择生日"
confirmText="保存"
visible={birthday_picker_visible}
setvisible={setBirthdayPickerVisible}
value={[
new Date(form_data.birthday || Date.now()).getFullYear(),
new Date(form_data.birthday || Date.now()).getMonth() + 1,
new Date(form_data.birthday || Date.now()).getDate(),
]}
type="day"
onChange={handle_birthday_change}
/>
)}
{/* 地区选择弹窗 */}
{location_picker_visible && (
<PopupPicker
showHeader={true}
title="选择地区"
confirmText="保存"
options={cities}
visible={location_picker_visible}
setvisible={setLocationPickerVisible}
value={
form_data.province
? [form_data.province, form_data.city, form_data.district]
: getDefaultOption(cities)
}
onChange={handle_location_change}
/>
)}
{/* NTRP水平选择弹窗 */}
{ntrp_picker_visible && (
<PopupPicker
showHeader={true}
title="选择 NTRP 自评水平"
confirmText="保存"
ntrpTested={ntrpTested}
options={ntrpLevels}
type="ntrp"
// img={(user_info as UserInfoType)?.avatar_url}
visible={ntrp_picker_visible}
setvisible={setNtrpPickerVisible}
value={!form_data.ntrp_level ? ["2.5"] : [form_data.ntrp_level]}
onChange={handle_ntrp_level_change}
/>
)}
{/* 职业选择弹窗 */}
{occupation_picker_visible && (
<PopupPicker
showHeader={true}
title="选择职业"
confirmText="保存"
options={professions}
visible={occupation_picker_visible}
setvisible={setOccupationPickerVisible}
value={
form_data.occupation
? [...form_data.occupation.split(" ")]
: getDefaultOption(professions)
}
onChange={handle_occupation_change}
/>
)}
{/* 取消关注确认弹窗 */}
<CommonDialog
visible={showLogoutDialog}
cancelText="确定注销"
confirmText="再想想"
onCancel={() => {
UserService.logout();
}}
onConfirm={() => setShowLogoutDialog(false)}
contentTitle="确定要注销账号吗?"
contentDesc="你的账号将会彻底删除,该操作不可恢复。"
/>
</View>
);
};
export default EditProfilePage;