hookehuyr

feat(user-profile): 新增参数构建工具并完善用户资料提交逻辑

- 新建src/utils/userProfile.js工具文件,封装标准化的更新用户资料请求参数构建逻辑
- 重构AddProfile和EditProfile页面的保存逻辑,统一使用工具处理表单数据
- 为AddProfile新增路由参数解析,支持带入reg_source和reg_stage_id请求参数
- 完善EditProfile表单验证,补充性别和轮椅需求的校验逻辑
- 统一项目代码格式,修正多余分号和导入语句风格
/*
* @Date: 2024-01-01 00:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-02 20:33:00
* @LastEditTime: 2026-05-20 11:23:02
* @FilePath: /lls_program/src/api/user.js
* @Description: 用户相关接口
*/
import { fn, fetch } from './fn';
import { fn, fetch } from './fn'
const Api = {
GET_PROFILE: '/srv/?a=user&t=get_profile',
UPDATE_PROFILE: '/srv/?a=user&t=update_profile',
GET_PROFILE: '/srv/?a=user&t=get_profile',
UPDATE_PROFILE: '/srv/?a=user&t=update_profile',
}
/**
......@@ -28,7 +28,7 @@ const Api = {
* @returns {boolean} response.data.user.wheelchair_needed - 是否需要轮椅
* @returns {number} response.data.user.gender - 用户性别 (0=女, 1=男)
*/
export const getUserProfileAPI = (params) => fn(fetch.get(Api.GET_PROFILE, params));
export const getUserProfileAPI = params => fn(fetch.get(Api.GET_PROFILE, params))
/**
* @description: 更新个人信息
......@@ -38,5 +38,7 @@ export const getUserProfileAPI = (params) => fn(fetch.get(Api.GET_PROFILE, param
* @param {string} params.avatar_url - 用户头像URL
* @param {boolean} params.wheelchair_needed - 是否需要轮椅
* @param {number} params.gender - 用户性别 (0=女, 1=男)
* @param {string} [params.reg_source] - 注册来源
* @param {number} [params.reg_stage_id] - 注册关卡ID
*/
export const updateUserProfileAPI = (params) => fn(fetch.post(Api.UPDATE_PROFILE, params));
export const updateUserProfileAPI = params => fn(fetch.post(Api.UPDATE_PROFILE, params))
......
<template>
<view class="min-h-screen bg-gray-50 p-5 pb-24">
<!-- Avatar -->
<view class="flex flex-col items-center py-8" @click="changeAvatar">
<view class="w-24 h-24 rounded-full bg-gray-100 flex items-center justify-center mb-2 overflow-hidden">
<image :src="formData.avatar_url || defaultAvatar" class="w-full h-full" mode="aspectFill" />
</view>
<view class="flex items-center justify-center">
<text class="text-gray-500 text-sm mr-1">上传头像</text>
<Ask size="15" color="#888" @tap.stop="showAvatarTipDialog = true" />
</view>
</view>
<!-- Form -->
<view class="space-y-6">
<!-- Nickname -->
<view class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">昵称</label>
<view class="bg-white rounded-xl p-2 border border-gray-200">
<nut-input
v-model="formData.nickname"
placeholder="请输入昵称"
:border="false"
class="!bg-transparent !border-none !p-0 text-base"
/>
</view>
</view>
<view class="min-h-screen bg-gray-50 p-5 pb-24">
<!-- Avatar -->
<view class="flex flex-col items-center py-8" @click="changeAvatar">
<view
class="w-24 h-24 rounded-full bg-gray-100 flex items-center justify-center mb-2 overflow-hidden"
>
<image
:src="formData.avatar_url || defaultAvatar"
class="w-full h-full"
mode="aspectFill"
/>
</view>
<view class="flex items-center justify-center">
<text class="text-gray-500 text-sm mr-1">上传头像</text>
<Ask size="15" color="#888" @tap.stop="showAvatarTipDialog = true" />
</view>
</view>
<!-- birth_date -->
<view class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">出生年月</label>
<view class="bg-white rounded-xl p-4 border border-gray-200" @click="showDatePicker = true">
<view class="flex justify-between items-center">
<text :class="{'text-gray-400': !formData.birth_date, 'text-gray-900': formData.birth_date}" class="text-base">
{{ formData.birth_date || '-/-/-/' }}
</text>
<DateIcon size="20" color="#888" />
</view>
</view>
<text class="text-red-500 text-xs mt-1 block">注意:出生年月填写后不可修改,请仔细确认</text>
</view>
<!-- Form -->
<view class="space-y-6">
<!-- Nickname -->
<view class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">昵称</label>
<view class="bg-white rounded-xl p-2 border border-gray-200">
<nut-input
v-model="formData.nickname"
placeholder="请输入昵称"
:border="false"
class="!bg-transparent !border-none !p-0 text-base"
/>
</view>
</view>
<!-- gender -->
<view class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">性别</label>
<view class="bg-white rounded-xl p-4 border border-gray-200" @click="showGenderPicker = true">
<view class="flex justify-between items-center">
<text :class="{'text-gray-400': !formData.gender_text, 'text-gray-900': formData.gender_text}" class="text-base">
{{ formData.gender_text || '请选择' }}
</text>
<Right size="16" color="#888" />
</view>
</view>
</view>
<!-- birth_date -->
<view class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">出生年月</label>
<view class="bg-white rounded-xl p-4 border border-gray-200" @click="showDatePicker = true">
<view class="flex justify-between items-center">
<text
:class="{
'text-gray-400': !formData.birth_date,
'text-gray-900': formData.birth_date,
}"
class="text-base"
>
{{ formData.birth_date || '-/-/-/' }}
</text>
<DateIcon size="20" color="#888" />
</view>
</view>
<text class="text-red-500 text-xs mt-1 block"
>注意:出生年月填写后不可修改,请仔细确认</text
>
</view>
<!-- wheelchair_needed -->
<view class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">是否需要轮椅出行</label>
<view class="bg-white rounded-xl p-4 border border-gray-200" @click="showWheelchairPicker = true">
<view class="flex justify-between items-center">
<text :class="{'text-gray-400': !formData.wheelchair_text, 'text-gray-900': formData.wheelchair_text}" class="text-base">
{{ formData.wheelchair_text || '请选择' }}
</text>
<Right size="16" color="#888" />
</view>
</view>
</view>
<!-- gender -->
<view class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">性别</label>
<view
class="bg-white rounded-xl p-4 border border-gray-200"
@click="showGenderPicker = true"
>
<view class="flex justify-between items-center">
<text
:class="{
'text-gray-400': !formData.gender_text,
'text-gray-900': formData.gender_text,
}"
class="text-base"
>
{{ formData.gender_text || '请选择' }}
</text>
<Right size="16" color="#888" />
</view>
</view>
</view>
<!-- Save Button -->
<view class="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-gray-100">
<nut-button type="primary" size="large" :color="isFormValid ? THEME_COLORS.PRIMARY : '#D8D8D8'" block @click="handleSave" :disabled="!isFormValid">保存</nut-button>
<!-- wheelchair_needed -->
<view class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">是否需要轮椅出行</label>
<view
class="bg-white rounded-xl p-4 border border-gray-200"
@click="showWheelchairPicker = true"
>
<view class="flex justify-between items-center">
<text
:class="{
'text-gray-400': !formData.wheelchair_text,
'text-gray-900': formData.wheelchair_text,
}"
class="text-base"
>
{{ formData.wheelchair_text || '请选择' }}
</text>
<Right size="16" color="#888" />
</view>
</view>
</view>
</view>
<!-- Popups -->
<nut-popup v-model:visible="showDatePicker" position="bottom">
<nut-date-picker
v-model="currentDate"
@confirm="onDateConfirm"
@cancel="showDatePicker = false"
:min-date="minDate"
:max-date="maxDate"
title="选择出生年月"
></nut-date-picker>
</nut-popup>
<!-- Save Button -->
<view class="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-gray-100">
<nut-button
type="primary"
size="large"
:color="isFormValid ? THEME_COLORS.PRIMARY : '#D8D8D8'"
block
@click="handleSave"
:disabled="!isFormValid"
>保存</nut-button
>
</view>
<!-- Popups -->
<nut-popup v-model:visible="showDatePicker" position="bottom">
<nut-date-picker
v-model="currentDate"
@confirm="onDateConfirm"
@cancel="showDatePicker = false"
:min-date="minDate"
:max-date="maxDate"
title="选择出生年月"
></nut-date-picker>
</nut-popup>
<nut-popup v-model:visible="showWheelchairPicker" position="bottom">
<nut-picker
v-model="wheelchairValue"
:columns="wheelchairColumns"
@confirm="onWheelchairConfirm"
@cancel="showWheelchairPicker = false"
title="是否需要轮椅出行"
></nut-picker>
</nut-popup>
<nut-popup v-model:visible="showWheelchairPicker" position="bottom">
<nut-picker
v-model="wheelchairValue"
:columns="wheelchairColumns"
@confirm="onWheelchairConfirm"
@cancel="showWheelchairPicker = false"
title="是否需要轮椅出行"
></nut-picker>
</nut-popup>
<nut-popup v-model:visible="showGenderPicker" position="bottom">
<nut-picker
v-model="genderValue"
:columns="genderColumns"
@confirm="onGenderConfirm"
@cancel="showGenderPicker = false"
title="选择性别"
></nut-picker>
</nut-popup>
<nut-popup v-model:visible="showGenderPicker" position="bottom">
<nut-picker
v-model="genderValue"
:columns="genderColumns"
@confirm="onGenderConfirm"
@cancel="showGenderPicker = false"
title="选择性别"
></nut-picker>
</nut-popup>
<nut-image-preview v-model:show="previewVisible" :images="previewImages" />
<nut-image-preview v-model:show="previewVisible" :images="previewImages" />
<!-- 头像上传须知对话框 -->
<nut-dialog
v-model:visible="showAvatarTipDialog"
title="头像上传须知"
<!-- 头像上传须知对话框 -->
<nut-dialog v-model:visible="showAvatarTipDialog" title="头像上传须知">
<template #default>
<view class="text-gray-700 leading-loose text-sm text-left break-words">
<view class="mb-2">1. 头像大小不能超过5MB</view>
<view class="mb-2">2. 头像格式为jpg、jpeg、png</view>
<view>3. 头像未通过审核,可能原因:色情 / 暴恐内容、敏感人物、违规广告</view>
</view>
</template>
<template #footer>
<nut-button
@click="showAvatarTipDialog = false"
type="primary"
size="normal"
:color="THEME_COLORS.PRIMARY"
block
>
<template #default>
<view class="text-gray-700 leading-loose text-sm text-left break-words">
<view class="mb-2">1. 头像大小不能超过5MB</view>
<view class="mb-2">2. 头像格式为jpg、jpeg、png</view>
<view>3. 头像未通过审核,可能原因:色情 / 暴恐内容、敏感人物、违规广告</view>
</view>
</template>
<template #footer>
<nut-button
@click="showAvatarTipDialog = false"
type="primary"
size="normal"
:color="THEME_COLORS.PRIMARY"
block
>
我知道了
</nut-button>
</template>
</nut-dialog>
</view>
我知道了
</nut-button>
</template>
</nut-dialog>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import Taro from '@tarojs/taro';
import { Date as DateIcon, Right, Ask } from '@nutui/icons-vue-taro';
import BASE_URL from '@/utils/config';
import { ref, reactive, computed } from 'vue'
import Taro, { useLoad } from '@tarojs/taro'
import { Date as DateIcon, Right, Ask } from '@nutui/icons-vue-taro'
import BASE_URL from '@/utils/config'
// 默认头像
const defaultAvatar = 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
// 获取接口信息
import { updateUserProfileAPI } from '@/api/user';
import { updateUserProfileAPI } from '@/api/user'
import { buildUpdateUserProfilePayload } from '@/utils/userProfile'
// 导入主题颜色
import { THEME_COLORS } from '@/utils/config';
import { THEME_COLORS } from '@/utils/config'
/**
* @description 表单数据
......@@ -152,116 +190,129 @@ const formData = reactive({
avatar_url: '',
nickname: '',
birth_date: '',
wheelchair_needed: null, // 0 for No, 1 for Yes
wheelchair_needed: null,
wheelchair_text: '',
gender: null, // 0 for 女, 1 for 男
gender: null,
gender_text: '',
qiniu_audit: '', // 图片审核信息
});
})
/**
* @description 注册来源补充参数
*/
const registerSourceParams = reactive({
reg_source: '',
reg_stage_id: '',
})
/**
* @description 检查表单是否有效
*/
const isFormValid = computed(() => {
return formData.nickname && formData.birth_date && formData.wheelchair_needed !== null && formData.gender !== null;
});
return (
formData.nickname &&
formData.birth_date &&
formData.wheelchair_needed !== null &&
formData.gender !== null
)
})
// --- Avatar Tip Dialog ---
/**
* @description 控制头像上传须知对话框显示
*/
const showAvatarTipDialog = ref(false);
const showAvatarTipDialog = ref(false)
// --- Date Picker ---
/**
* @description 控制日期选择器显示
*/
const showDatePicker = ref(false);
const showDatePicker = ref(false)
/**
* @description 最小可选日期
*/
const minDate = new Date(1920, 0, 1);
const minDate = new Date(1920, 0, 1)
/**
* @description 最大可选日期
*/
const maxDate = new Date();
const maxDate = new Date()
/**
* @description 当前选中的日期
*/
const currentDate = ref(new Date());
const currentDate = ref(new Date())
/**
* @description 确认选择日期
* @param {object} param0 - 包含 selectedValue 的对象
*/
const onDateConfirm = ({ selectedValue }) => {
formData.birth_date = selectedValue.join('-');
showDatePicker.value = false;
};
formData.birth_date = selectedValue.join('-')
showDatePicker.value = false
}
// --- wheelchair_needed Picker ---
/**
* @description 控制轮椅选择器显示
*/
const showWheelchairPicker = ref(false);
const showWheelchairPicker = ref(false)
/**
* @description 当前选中的轮椅选项
*/
const wheelchairValue = ref([]);
const wheelchairValue = ref([])
/**
* @description 轮椅选择器选项
*/
const wheelchairColumns = ref([
{ text: '是', value: 1 },
{ text: '否', value: 0 },
]);
])
// --- Gender Picker ---
/**
* @description 控制性别选择器显示
*/
const showGenderPicker = ref(false);
const showGenderPicker = ref(false)
/**
* @description 当前选中的性别选项
*/
const genderValue = ref([]);
const genderValue = ref([])
/**
* @description 性别选择器选项
*/
const genderColumns = ref([
{ text: '女', value: 0 },
{ text: '男', value: 1 },
]);
])
/**
* @description 确认选择性别选项
* @param {object} param0 - 包含 selectedValue 和 selectedOptions 的对象
*/
const onGenderConfirm = ({ selectedValue, selectedOptions }) => {
formData.gender = selectedValue[0];
formData.gender_text = selectedOptions.map((option) => option.text).join('');
showGenderPicker.value = false;
};
formData.gender = selectedValue[0]
formData.gender_text = selectedOptions.map(option => option.text).join('')
showGenderPicker.value = false
}
/**
* @description 确认选择轮椅选项
* @param {object} param0 - 包含 selectedValue 和 selectedOptions 的对象
*/
const onWheelchairConfirm = ({ selectedValue, selectedOptions }) => {
formData.wheelchair_needed = selectedValue[0];
formData.wheelchair_text = selectedOptions.map((option) => option.text).join('');
showWheelchairPicker.value = false;
};
formData.wheelchair_needed = selectedValue[0]
formData.wheelchair_text = selectedOptions.map(option => option.text).join('')
showWheelchairPicker.value = false
}
// --- Avatar ---
/**
* @description 控制图片预览显示
*/
const previewVisible = ref(false);
const previewVisible = ref(false)
/**
* @description 预览的图片列表
*/
const previewImages = ref([]);
const previewImages = ref([])
/**
* @description 更换头像
......@@ -271,57 +322,57 @@ const changeAvatar = () => {
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFile = res.tempFiles[0];
success: res => {
const tempFile = res.tempFiles[0]
if (tempFile.size > 5 * 1024 * 1024) {
Taro.showToast({
title: '图片大小不能超过5MB',
icon: 'none',
});
return;
})
return
}
Taro.showLoading({ title: '上传中...' });
Taro.showLoading({ title: '上传中...' })
Taro.uploadFile({
url: BASE_URL + '/admin/?m=srv&a=upload&image_audit=1',
filePath: tempFile.path,
name: 'file',
success: (uploadRes) => {
Taro.hideLoading();
const data = JSON.parse(uploadRes.data);
if (data.code == 0) {
success: uploadRes => {
Taro.hideLoading()
const data = JSON.parse(uploadRes.data)
if (data.code === 0) {
// 检查是否为审核不通过
if (data.data.audit_code == -1) {
if (data.data.audit_code === -1) {
Taro.showModal({
title: '温馨提示',
content: data.msg,
showCancel: false
showCancel: false,
}).then(res => {
if (res.confirm) {
// 点击了确认按钮
}
});
})
} else {
formData.avatar_url = data.data.src;
formData.qiniu_audit = data.data.audit_result;
Taro.showToast({ title: '上传成功', icon: 'success' });
formData.avatar_url = data.data.src
formData.qiniu_audit = data.data.audit_result
Taro.showToast({ title: '上传成功', icon: 'success' })
}
} else {
Taro.showToast({ title: data.msg || '上传失败', icon: 'none' });
Taro.showToast({ title: data.msg || '上传失败', icon: 'none' })
}
},
fail: () => {
Taro.hideLoading();
Taro.showToast({ title: '上传失败,请稍后重试', icon: 'none' });
Taro.hideLoading()
Taro.showToast({ title: '上传失败,请稍后重试', icon: 'none' })
},
});
})
},
fail: () => {
// Taro.showToast({ title: '选择图片失败', icon: 'none' });
},
});
};
})
}
// --- Save ---
/**
......@@ -332,30 +383,42 @@ const handleSave = async () => {
Taro.showToast({
title: '请填写所有必填项',
icon: 'none',
});
return;
})
return
}
if (!formData.avatar_url) {
formData.avatar_url = defaultAvatar
}
// 保存用户信息
const { code, data } = await updateUserProfileAPI(formData);
if (code) {
const payload = buildUpdateUserProfilePayload(formData, registerSourceParams)
const result = await updateUserProfileAPI(payload)
if (result?.code === 1) {
Taro.showToast({
title: '保存成功',
icon: 'success',
});
})
setTimeout(() => {
Taro.navigateBack();
}, 1500);
Taro.navigateBack()
}, 1500)
return
}
};
onMounted(() => {
});
Taro.showToast({
title: result?.msg || '保存失败,请稍后重试',
icon: 'none',
})
}
useLoad(options => {
registerSourceParams.reg_source = options?.reg_source || ''
registerSourceParams.reg_stage_id = options?.reg_stage_id || ''
})
</script>
<style>
.nut-input.mt-2 {
padding: 12px 0 !important;
border-bottom: 1px solid #e5e7eb;
padding: 12px 0 !important;
border-bottom: 1px solid #e5e7eb;
}
</style>
......
<template>
<view class="min-h-screen bg-gray-50 p-5 pb-24">
<!-- Avatar -->
<view class="flex flex-col items-center py-8" @tap="changeAvatar">
<view class="w-24 h-24 rounded-full bg-gray-100 flex items-center justify-center mb-2 overflow-hidden">
<image :src="formData.avatar_url || defaultAvatar" class="w-full h-full" mode="aspectFill" />
</view>
<view class="flex items-center justify-center">
<text class="text-gray-500 text-sm mr-1">上传头像</text>
<Ask size="15" color="#888" @tap.stop="showAvatarTipDialog = true" />
</view>
</view>
<view class="min-h-screen bg-gray-50 p-5 pb-24">
<!-- Avatar -->
<view class="flex flex-col items-center py-8" @tap="changeAvatar">
<view
class="w-24 h-24 rounded-full bg-gray-100 flex items-center justify-center mb-2 overflow-hidden"
>
<image
:src="formData.avatar_url || defaultAvatar"
class="w-full h-full"
mode="aspectFill"
/>
</view>
<view class="flex items-center justify-center">
<text class="text-gray-500 text-sm mr-1">上传头像</text>
<Ask size="15" color="#888" @tap.stop="showAvatarTipDialog = true" />
</view>
</view>
<!-- Form -->
<view class="space-y-6">
<!-- Nickname -->
<view class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">昵称</label>
<view class="bg-white rounded-xl p-2 border border-gray-200">
<nut-input
v-model="formData.nickname"
placeholder="请输入昵称"
:border="false"
class="!bg-transparent !border-none !p-0 text-base"
/>
</view>
</view>
<!-- birth_date -->
<view class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">出生年月</label>
<view class="bg-gray-100 rounded-xl p-4 border border-gray-200">
<view class="flex justify-between items-center">
<text class="text-gray-500 text-base">
{{ formData.birth_date || '-/-/-/' }}
</text>
<DateIcon size="20" color="#ccc" />
</view>
</view>
<text class="text-gray-400 text-xs mt-1 block">出生年月不可修改</text>
</view>
<!-- gender -->
<view class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">性别</label>
<view class="bg-white rounded-xl p-4 border border-gray-200" @click="showGenderPicker = true">
<view class="flex justify-between items-center">
<text :class="{'text-gray-400': !formData.gender_text, 'text-gray-900': formData.gender_text}" class="text-base">
{{ formData.gender_text || '请选择' }}
</text>
<Right size="16" color="#888" />
</view>
</view>
</view>
<!-- wheelchair_needed -->
<view class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">是否需要轮椅出行</label>
<view class="bg-white rounded-xl p-4 border border-gray-200" @click="showWheelchairPicker = true">
<view class="flex justify-between items-center">
<text :class="{'text-gray-400': !formData.wheelchair_text, 'text-gray-900': formData.wheelchair_text}" class="text-base">
{{ formData.wheelchair_text || '请选择' }}
</text>
<Right size="16" color="#888" />
</view>
</view>
</view>
<!-- Form -->
<view class="space-y-6">
<!-- Nickname -->
<view class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">昵称</label>
<view class="bg-white rounded-xl p-2 border border-gray-200">
<nut-input
v-model="formData.nickname"
placeholder="请输入昵称"
:border="false"
class="!bg-transparent !border-none !p-0 text-base"
/>
</view>
<!-- Save Button -->
<view class="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-gray-100">
<nut-button type="primary" size="large" :color="THEME_COLORS.PRIMARY" block @click="handleSave">保存</nut-button>
</view>
<!-- birth_date -->
<view class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">出生年月</label>
<view class="bg-gray-100 rounded-xl p-4 border border-gray-200">
<view class="flex justify-between items-center">
<text class="text-gray-500 text-base">
{{ formData.birth_date || '-/-/-/' }}
</text>
<DateIcon size="20" color="#ccc" />
</view>
</view>
<!-- Popups -->
<nut-popup v-model:visible="showDatePicker" position="bottom">
<nut-date-picker
v-model="currentDate"
@confirm="onDateConfirm"
@cancel="showDatePicker = false"
:min-date="minDate"
:max-date="maxDate"
title="选择出生年月"
></nut-date-picker>
</nut-popup>
<nut-popup v-model:visible="showWheelchairPicker" position="bottom">
<nut-picker
v-model="wheelchairValue"
:columns="wheelchairColumns"
@confirm="onWheelchairConfirm"
@cancel="showWheelchairPicker = false"
title="是否需要轮椅出行"
></nut-picker>
</nut-popup>
<nut-popup v-model:visible="showGenderPicker" position="bottom">
<nut-picker
v-model="genderValue"
:columns="genderColumns"
@confirm="onGenderConfirm"
@cancel="showGenderPicker = false"
title="选择性别"
></nut-picker>
</nut-popup>
<nut-image-preview v-model:show="previewVisible" :images="previewImages" />
<!-- 头像上传须知对话框 -->
<nut-dialog
v-model:visible="showAvatarTipDialog"
title="头像上传须知"
<text class="text-gray-400 text-xs mt-1 block">出生年月不可修改</text>
</view>
<!-- gender -->
<view class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">性别</label>
<view
class="bg-white rounded-xl p-4 border border-gray-200"
@click="showGenderPicker = true"
>
<view class="flex justify-between items-center">
<text
:class="{
'text-gray-400': !formData.gender_text,
'text-gray-900': formData.gender_text,
}"
class="text-base"
>
{{ formData.gender_text || '请选择' }}
</text>
<Right size="16" color="#888" />
</view>
</view>
</view>
<!-- wheelchair_needed -->
<view class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">是否需要轮椅出行</label>
<view
class="bg-white rounded-xl p-4 border border-gray-200"
@click="showWheelchairPicker = true"
>
<template #default>
<view class="text-gray-700 leading-loose text-sm text-left break-words">
<view class="mb-2">1. 头像大小不能超过5MB</view>
<view class="mb-2">2. 头像格式为jpg、jpeg、png</view>
<view>3. 头像未通过审核,可能原因:色情 / 暴恐内容、敏感人物、违规广告</view>
</view>
</template>
<template #footer>
<nut-button
@click="showAvatarTipDialog = false"
type="primary"
size="normal"
:color="THEME_COLORS.PRIMARY"
block
>
我知道了
</nut-button>
</template>
</nut-dialog>
<view class="flex justify-between items-center">
<text
:class="{
'text-gray-400': !formData.wheelchair_text,
'text-gray-900': formData.wheelchair_text,
}"
class="text-base"
>
{{ formData.wheelchair_text || '请选择' }}
</text>
<Right size="16" color="#888" />
</view>
</view>
</view>
</view>
<!-- Save Button -->
<view class="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-gray-100">
<nut-button
type="primary"
size="large"
:color="THEME_COLORS.PRIMARY"
block
@click="handleSave"
>保存</nut-button
>
</view>
<!-- Popups -->
<nut-popup v-model:visible="showDatePicker" position="bottom">
<nut-date-picker
v-model="currentDate"
@confirm="onDateConfirm"
@cancel="showDatePicker = false"
:min-date="minDate"
:max-date="maxDate"
title="选择出生年月"
></nut-date-picker>
</nut-popup>
<nut-popup v-model:visible="showWheelchairPicker" position="bottom">
<nut-picker
v-model="wheelchairValue"
:columns="wheelchairColumns"
@confirm="onWheelchairConfirm"
@cancel="showWheelchairPicker = false"
title="是否需要轮椅出行"
></nut-picker>
</nut-popup>
<nut-popup v-model:visible="showGenderPicker" position="bottom">
<nut-picker
v-model="genderValue"
:columns="genderColumns"
@confirm="onGenderConfirm"
@cancel="showGenderPicker = false"
title="选择性别"
></nut-picker>
</nut-popup>
<nut-image-preview v-model:show="previewVisible" :images="previewImages" />
<!-- 头像上传须知对话框 -->
<nut-dialog v-model:visible="showAvatarTipDialog" title="头像上传须知">
<template #default>
<view class="text-gray-700 leading-loose text-sm text-left break-words">
<view class="mb-2">1. 头像大小不能超过5MB</view>
<view class="mb-2">2. 头像格式为jpg、jpeg、png</view>
<view>3. 头像未通过审核,可能原因:色情 / 暴恐内容、敏感人物、违规广告</view>
</view>
</template>
<template #footer>
<nut-button
@click="showAvatarTipDialog = false"
type="primary"
size="normal"
:color="THEME_COLORS.PRIMARY"
block
>
我知道了
</nut-button>
</template>
</nut-dialog>
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import Taro from '@tarojs/taro';
import { My, Date as DateIcon, Right, Ask } from '@nutui/icons-vue-taro';
import BASE_URL from '@/utils/config';
import { ref, reactive } from 'vue'
import Taro, { useLoad } from '@tarojs/taro'
import { My, Date as DateIcon, Right, Ask } from '@nutui/icons-vue-taro'
import BASE_URL from '@/utils/config'
// 默认头像
const defaultAvatar = 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
// 获取接口信息
import { getUserProfileAPI, updateUserProfileAPI } from '@/api/user';
import { getUserProfileAPI, updateUserProfileAPI } from '@/api/user'
import { buildUpdateUserProfilePayload } from '@/utils/userProfile'
// 导入主题颜色
import { THEME_COLORS } from '@/utils/config';
import { THEME_COLORS } from '@/utils/config'
/**
* @description 表单数据
......@@ -152,109 +181,109 @@ const formData = reactive({
avatar_url: '',
nickname: '',
birth_date: '',
wheelchair_needed: null, // 0 for No, 1 for Yes
wheelchair_needed: null,
wheelchair_text: '',
gender: null, // 0 for 女, 1 for 男
gender: null,
gender_text: '',
qiniu_audit: '', // 图片审核信息
});
})
// --- Avatar Tip Dialog ---
/**
* @description 控制头像上传须知对话框显示
*/
const showAvatarTipDialog = ref(false);
const showAvatarTipDialog = ref(false)
// --- Date Picker ---
/**
* @description 控制日期选择器显示
*/
const showDatePicker = ref(false);
const showDatePicker = ref(false)
/**
* @description 最小可选日期
*/
const minDate = new Date(1920, 0, 1);
const minDate = new Date(1920, 0, 1)
/**
* @description 最大可选日期
*/
const maxDate = new Date();
const maxDate = new Date()
/**
* @description 当前选中的日期
*/
const currentDate = ref(new Date());
const currentDate = ref(new Date())
/**
* @description 确认选择日期
* @param {object} param0 - 包含 selectedValue 的对象
*/
const onDateConfirm = ({ selectedValue }) => {
formData.birth_date = selectedValue.join('-');
showDatePicker.value = false;
};
formData.birth_date = selectedValue.join('-')
showDatePicker.value = false
}
// --- wheelchair_needed Picker ---
/**
* @description 控制轮椅选择器显示
*/
const showWheelchairPicker = ref(false);
const showWheelchairPicker = ref(false)
/**
* @description 当前选中的轮椅选项
*/
const wheelchairValue = ref([]);
const wheelchairValue = ref([])
/**
* @description 轮椅选择器选项
*/
const wheelchairColumns = ref([
{ text: '是', value: true },
{ text: '否', value: false },
]);
])
// --- Gender Picker ---
/**
* @description 控制性别选择器显示
*/
const showGenderPicker = ref(false);
const showGenderPicker = ref(false)
/**
* @description 当前选中的性别选项
*/
const genderValue = ref([]);
const genderValue = ref([])
/**
* @description 性别选择器选项
*/
const genderColumns = ref([
{ text: '女', value: 0 },
{ text: '男', value: 1 },
]);
])
/**
* @description 确认选择性别选项
* @param {object} param0 - 包含 selectedValue 和 selectedOptions 的对象
*/
const onGenderConfirm = ({ selectedValue, selectedOptions }) => {
formData.gender = selectedValue[0];
formData.gender_text = selectedOptions.map((option) => option.text).join('');
showGenderPicker.value = false;
};
formData.gender = selectedValue[0]
formData.gender_text = selectedOptions.map(option => option.text).join('')
showGenderPicker.value = false
}
/**
* @description 确认选择轮椅选项
* @param {object} param0 - 包含 selectedValue 和 selectedOptions 的对象
*/
const onWheelchairConfirm = ({ selectedValue, selectedOptions }) => {
formData.wheelchair_needed = selectedValue[0];
formData.wheelchair_text = selectedOptions.map((option) => option.text).join('');
showWheelchairPicker.value = false;
};
formData.wheelchair_needed = selectedValue[0]
formData.wheelchair_text = selectedOptions.map(option => option.text).join('')
showWheelchairPicker.value = false
}
// --- Avatar ---
/**
* @description 控制图片预览显示
*/
const previewVisible = ref(false);
const previewVisible = ref(false)
/**
* @description 预览的图片列表
*/
const previewImages = ref([]);
const previewImages = ref([])
/**
* @description 更换头像
......@@ -264,57 +293,57 @@ const changeAvatar = () => {
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFile = res.tempFiles[0];
success: res => {
const tempFile = res.tempFiles[0]
if (tempFile.size > 5 * 1024 * 1024) {
Taro.showToast({
title: '图片大小不能超过5MB',
icon: 'none',
});
return;
})
return
}
Taro.showLoading({ title: '上传中...' });
Taro.showLoading({ title: '上传中...' })
Taro.uploadFile({
url: BASE_URL + '/admin/?m=srv&a=upload&image_audit=1',
filePath: tempFile.path,
name: 'file',
success: (uploadRes) => {
Taro.hideLoading();
const data = JSON.parse(uploadRes.data);
success: uploadRes => {
Taro.hideLoading()
const data = JSON.parse(uploadRes.data)
if (data.code === 0) {
// 检查是否为审核不通过
if (data.data.audit_code == -1) {
if (data.data.audit_code === -1) {
Taro.showModal({
title: '温馨提示',
content: data.msg,
showCancel: false
showCancel: false,
}).then(res => {
if (res.confirm) {
// 点击了确认按钮
}
});
})
} else {
formData.avatar_url = data.data.src;
formData.qiniu_audit = data.data.audit_result;
Taro.showToast({ title: '上传成功', icon: 'success' });
formData.avatar_url = data.data.src
formData.qiniu_audit = data.data.audit_result
Taro.showToast({ title: '上传成功', icon: 'success' })
}
} else {
Taro.showToast({ title: data.msg || '上传失败', icon: 'none' });
Taro.showToast({ title: data.msg || '上传失败', icon: 'none' })
}
},
fail: () => {
Taro.hideLoading();
Taro.showToast({ title: '上传失败,请稍后重试', icon: 'none' });
Taro.hideLoading()
Taro.showToast({ title: '上传失败,请稍后重试', icon: 'none' })
},
});
})
},
fail: () => {
// Taro.showToast({ title: '选择图片失败', icon: 'none' });
},
});
};
})
}
// --- Save ---
/**
......@@ -325,29 +354,37 @@ const validateForm = () => {
if (!formData.nickname.trim()) {
Taro.showToast({
title: '请输入昵称',
icon: 'none'
});
return false;
icon: 'none',
})
return false
}
if (!formData.birth_date) {
Taro.showToast({
title: '请选择出生年月',
icon: 'none'
});
return false;
icon: 'none',
})
return false
}
if (formData.gender === null || formData.gender === undefined) {
Taro.showToast({
title: '请选择性别',
icon: 'none',
})
return false
}
if (formData.wheelchair_needed === null || formData.wheelchair_needed === undefined) {
Taro.showToast({
title: '请选择是否需要轮椅出行',
icon: 'none'
});
return false;
icon: 'none',
})
return false
}
return true;
};
return true
}
/**
* @description 保存用户信息
......@@ -355,56 +392,62 @@ const validateForm = () => {
const handleSave = async () => {
// 验证表单
if (!validateForm()) {
return;
return
}
// 如果头像为空,使用默认头像
if (!formData.avatar_url) {
formData.avatar_url = defaultAvatar;
formData.avatar_url = defaultAvatar
}
// 保存数据
const { code, data } = await updateUserProfileAPI(formData);
if (code) {
const payload = buildUpdateUserProfilePayload(formData)
const result = await updateUserProfileAPI(payload)
if (result?.code === 1) {
Taro.showToast({
title: '保存成功',
icon: 'success',
});
})
setTimeout(() => {
Taro.navigateBack();
}, 1500);
Taro.navigateBack()
}, 1500)
return
}
};
Taro.showToast({
title: result?.msg || '保存失败,请稍后重试',
icon: 'none',
})
}
/**
* @description 页面加载时获取初始数据
*/
onMounted(async () => {
useLoad(async () => {
// 获取用户信息
const { code, data } = await getUserProfileAPI();
if (code && data?.user) {
Object.assign(formData, data.user);
const result = await getUserProfileAPI()
if (result?.code === 1 && result?.data?.user) {
Object.assign(formData, result.data.user)
// 初始化勾选数据
if (formData.birth_date) {
currentDate.value = new Date(formData.birth_date);
currentDate.value = new Date(formData.birth_date)
}
// 设置轮椅选择器的值和显示文本
if (formData.wheelchair_needed !== null && formData.wheelchair_needed !== undefined) {
wheelchairValue.value = [formData.wheelchair_needed];
formData.wheelchair_text = formData.wheelchair_needed ? '是' : '否';
wheelchairValue.value = [formData.wheelchair_needed]
formData.wheelchair_text = formData.wheelchair_needed ? '是' : '否'
}
// 设置性别选择器的值和显示文本
if (formData.gender !== null && formData.gender !== undefined) {
genderValue.value = [formData.gender];
formData.gender_text = formData.gender === 0 ? '女' : '男';
genderValue.value = [formData.gender]
formData.gender_text = formData.gender === 0 ? '女' : '男'
}
}
});
})
</script>
<style>
.nut-input.mt-2 {
padding: 12px 0 !important;
border-bottom: 1px solid #e5e7eb;
padding: 12px 0 !important;
border-bottom: 1px solid #e5e7eb;
}
</style>
......
/*
* @Date: 2026-05-20 11:40:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-05-20 11:40:00
* @FilePath: /lls_program/src/utils/userProfile.js
* @Description: 用户资料相关工具函数
*/
/**
* @description 构建更新个人资料请求参数
* @param {Object} formData - 页面表单数据
* @param {Object} extraParams - 额外补充参数
* @returns {Object} 仅包含接口需要的字段
*/
export const buildUpdateUserProfilePayload = (formData, extraParams = {}) => {
const payload = {
nickname: (formData?.nickname || '').trim(),
avatar_url: formData?.avatar_url || '',
birth_date: formData?.birth_date || '',
wheelchair_needed: Boolean(formData?.wheelchair_needed),
gender: formData?.gender,
}
const optionalKeys = ['reg_source', 'reg_stage_id']
optionalKeys.forEach(key => {
const value = extraParams[key]
if (value !== '' && value !== undefined && value !== null) {
payload[key] = key === 'reg_stage_id' ? Number(value) : value
}
})
return payload
}