Files
youlai-app/src/pages/mine/profile/complete-profile.vue
2026-05-24 11:59:45 +08:00

409 lines
9.7 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="page">
<!-- 头部 -->
<view class="complete-profile__hero">
<view class="complete-profile__hero-title">完善个人信息</view>
<view class="complete-profile__hero-subtitle">为了给您提供更好的服务请完善以下信息</view>
</view>
<!-- 表单 -->
<view class="profile-form">
<wd-form ref="profileFormRef" :model="profileForm">
<!-- 头像 -->
<view class="profile-form__section">
<view class="profile-form__label">头像</view>
<view class="avatar-picker">
<!-- #ifdef MP-WEIXIN -->
<button class="avatar-picker__trigger" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="avatar-picker__trigger" @click="chooseAvatar">
<!-- #endif -->
<image
v-if="profileForm.avatar"
:src="profileForm.avatar"
class="avatar-picker__img"
mode="aspectFill"
/>
<view v-else class="avatar-picker__placeholder">
<wd-icon name="camera" size="40" color="var(--color-text-placeholder)" />
<text class="avatar-picker__hint">选择头像</text>
</view>
<!-- #ifdef MP-WEIXIN -->
</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
</view>
<!-- #endif -->
</view>
</view>
<!-- 昵称 -->
<view class="profile-form__section">
<view class="profile-form__label">
昵称
<text class="profile-form__required">*</text>
</view>
<!-- #ifdef MP-WEIXIN -->
<input
v-model="profileForm.nickname"
type="nickname"
class="profile-form__nickname-input"
placeholder="请输入昵称"
:maxlength="20"
/>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<wd-input
v-model="profileForm.nickname"
placeholder="请输入昵称"
prop="nickname"
:rules="rules.nickname"
custom-class="profile-form__nickname-input"
/>
<!-- #endif -->
</view>
<!-- 性别 -->
<view class="profile-form__section">
<view class="profile-form__label">性别</view>
<wd-radio-group v-model="profileForm.gender" type="button" class="gender-group">
<wd-radio :value="1" class="gender-radio"></wd-radio>
<wd-radio :value="2" class="gender-radio"></wd-radio>
</wd-radio-group>
</view>
<!-- 手机号 -->
<view class="profile-form__section">
<view class="profile-form__label">
手机号
<text class="profile-form__required">*</text>
</view>
<wd-input
v-model="profileForm.mobile"
placeholder="请输入手机号"
prop="mobile"
custom-class="profile-form__nickname-input"
/>
</view>
</wd-form>
</view>
<!-- 底部 -->
<view class="profile-footer">
<wd-button
class="profile-footer__submit"
type="primary"
size="large"
block
:disabled="!canComplete || isLoading"
:loading="isLoading"
@click="handleComplete"
>
完成
</wd-button>
<view class="profile-footer__skip" @click="handleSkip">
<text>暂时跳过</text>
</view>
</view>
<!-- 头像裁剪 -->
<wd-img-cropper
v-model="cropperVisible"
:img-src="originalImageSrc"
@confirm="handleAvatarConfirm"
/>
<wd-toast />
</view>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { useUserStore } from "@/store";
import UserAPI, { type UserProfileForm } from "@/api/user";
import FileAPI, { type FileInfo } from "@/api/file";
const toast = useToast();
const { confirm } = useDialog();
const userStore = useUserStore();
const redirect = ref("/pages/index/index");
const profileForm = reactive<UserProfileForm & { mobile?: string }>({
nickname: "",
avatar: "",
gender: 1,
mobile: "",
});
const rules = {
nickname: [
{ required: true, message: "请输入昵称" },
{ required: true, min: 2, max: 20, message: "昵称长度为2-20个字符" },
],
};
const isLoading = ref(false);
const cropperVisible = ref(false);
const originalImageSrc = ref("");
const profileFormRef = ref();
const canComplete = computed(() => {
return profileForm.nickname && profileForm.mobile;
});
onLoad((options: any) => {
if (options?.redirect) {
redirect.value = decodeURIComponent(options.redirect);
}
const userInfo = userStore.userInfo;
if (userInfo) {
profileForm.nickname = userInfo.nickname || "";
profileForm.avatar = userInfo.avatar || "";
profileForm.gender = (userInfo as any).gender || 1;
}
});
const onChooseAvatar = async (e: any) => {
try {
const { avatarUrl } = e.detail;
toast.loading("上传中...");
const fileInfo: FileInfo = await FileAPI.upload(avatarUrl);
profileForm.avatar = fileInfo.url;
toast.success("头像上传成功");
} catch {
toast.error("头像上传失败");
}
};
const chooseAvatar = () => {
// #ifdef MP-WEIXIN
uni.chooseMedia({
count: 1,
mediaType: ["image"],
sourceType: ["album", "camera"],
success: (res) => {
originalImageSrc.value = res.tempFiles[0].tempFilePath;
cropperVisible.value = true;
},
fail: () => {
toast.error("选择图片失败");
},
});
// #endif
// #ifndef MP-WEIXIN
uni.chooseImage({
count: 1,
sourceType: ["album", "camera"],
success: (res) => {
originalImageSrc.value = res.tempFilePaths[0];
cropperVisible.value = true;
},
fail: () => {
toast.error("选择图片失败");
},
});
// #endif
};
const handleAvatarConfirm = async (event: any) => {
try {
const { tempFilePath } = event;
toast.loading("上传中...");
const fileInfo: FileInfo = await FileAPI.upload(tempFilePath);
profileForm.avatar = fileInfo.url;
toast.success("头像上传成功");
} catch {
toast.error("头像上传失败");
}
};
const handleComplete = async () => {
try {
const { valid } = await profileFormRef.value.validate();
if (!valid) return;
if (!profileForm.mobile) {
toast.error("请输入手机号");
return;
}
isLoading.value = true;
await UserAPI.updateProfile({
nickname: profileForm.nickname,
avatar: profileForm.avatar,
gender: profileForm.gender,
});
if (profileForm.mobile) {
await UserAPI.bindMobile({ mobile: profileForm.mobile });
}
await userStore.getInfo();
toast.success("信息完善成功");
setTimeout(() => {
uni.reLaunch({ url: redirect.value });
}, 1000);
} catch (error: any) {
toast.error(error?.message || "完善信息失败");
} finally {
isLoading.value = false;
}
};
const handleSkip = async () => {
try {
await confirm({ title: "提示", msg: "跳过信息完善可能会影响部分功能使用,确定要跳过吗?", headerImage: "warning" });
uni.reLaunch({ url: redirect.value });
} catch {
// 用户取消
}
};
</script>
<style lang="scss" scoped>
.complete-profile__hero {
margin-bottom: 60rpx;
text-align: center;
}
.complete-profile__hero-title {
margin-bottom: 20rpx;
font-size: 48rpx;
font-weight: bold;
color: var(--color-text-inverse);
}
.complete-profile__hero-subtitle {
font-size: 28rpx;
line-height: 1.5;
color: rgba(255, 255, 255, 0.8);
}
.profile-form {
padding: 40rpx;
margin-bottom: 40rpx;
background: var(--color-bg);
border-radius: 24rpx;
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.1);
&__section {
margin-bottom: 40rpx;
&:last-child {
margin-bottom: 0;
}
}
&__label {
margin-bottom: 20rpx;
font-size: 32rpx;
font-weight: 600;
color: var(--color-text);
}
&__required {
color: var(--color-danger);
}
&__nickname-input {
:deep(.wd-input__inner) {
padding: 24rpx 20rpx;
background: var(--color-bg-tertiary);
border: 1rpx solid var(--color-border-light);
border-radius: 12rpx;
}
}
}
.avatar-picker {
display: flex;
justify-content: center;
&__trigger {
display: flex;
align-items: center;
justify-content: center;
width: 160rpx;
height: 160rpx;
padding: 0;
margin: 0 auto;
overflow: hidden;
background: transparent;
border: none;
border-radius: 50%;
&::after {
border: none;
}
}
&__img {
width: 100%;
height: 100%;
border: 4rpx solid var(--color-border-light);
border-radius: 50%;
}
&__placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 160rpx;
height: 160rpx;
background: var(--color-bg-tertiary);
border: 2rpx dashed var(--color-border);
border-radius: 50%;
}
&__hint {
margin-top: 10rpx;
font-size: 24rpx;
color: var(--color-text-placeholder);
}
}
.gender-group {
display: flex;
gap: 20rpx;
.gender-radio {
flex: 1;
:deep(.wd-radio) {
justify-content: center;
width: 100%;
}
}
}
.profile-footer {
&__submit {
margin-bottom: 30rpx;
:deep(.wd-button) {
height: 88rpx;
font-size: 32rpx;
font-weight: 600;
border-radius: 44rpx;
}
}
&__skip {
text-align: center;
text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
text-decoration: underline;
}
}
}
</style>