hookehuyr

feat(profile): 添加个人信息完善页面及导航检查

添加个人信息完善页面组件,包含头像上传、昵称、出生年月和轮椅出行需求表单
在Welcome页面添加导航前检查个人信息是否完善的逻辑
更新app.config.js添加新页面路由
1 /* 1 /*
2 * @Date: 2025-06-28 10:33:00 2 * @Date: 2025-06-28 10:33:00
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-08-28 14:52:14 4 + * @LastEditTime: 2025-08-28 21:48:50
5 * @FilePath: /lls_program/src/app.config.js 5 * @FilePath: /lls_program/src/app.config.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
...@@ -18,6 +18,7 @@ export default { ...@@ -18,6 +18,7 @@ export default {
18 'pages/Rewards/index', 18 'pages/Rewards/index',
19 'pages/MyRewards/index', 19 'pages/MyRewards/index',
20 'pages/Profile/index', 20 'pages/Profile/index',
21 + 'pages/AddProfile/index',
21 'pages/Feedback/index', 22 'pages/Feedback/index',
22 'pages/PointsDetail/index', 23 'pages/PointsDetail/index',
23 'pages/RewardDetail/index', 24 'pages/RewardDetail/index',
......
1 +/*
2 + * @Date: 2025-08-28 11:22:13
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-08-28 22:02:32
5 + * @FilePath: /lls_program/src/pages/AddProfile/index.config.js
6 + * @Description: 文件描述
7 + */
8 +export default {
9 + navigationBarTitleText: '完善个人信息'
10 +}
1 +<template>
2 + <view class="min-h-screen bg-gray-50 p-5 pb-24">
3 + <!-- Avatar -->
4 + <view class="flex flex-col items-center py-8" @click="changeAvatar">
5 + <view class="w-24 h-24 rounded-full bg-gray-100 flex items-center justify-center mb-2 overflow-hidden">
6 + <image v-if="formData.avatar_url" :src="formData.avatar_url" class="w-full h-full" mode="aspectFill" />
7 + <My size="40" color="#888" v-else />
8 + </view>
9 + <text class="text-gray-500 text-sm">上传头像</text>
10 + </view>
11 +
12 + <!-- Form -->
13 + <view class="space-y-6">
14 + <!-- Nickname -->
15 + <view class="mb-6">
16 + <label class="block text-sm font-medium text-gray-700 mb-2">昵称</label>
17 + <view class="bg-white rounded-xl p-2 border border-gray-200">
18 + <nut-input
19 + v-model="formData.nickname"
20 + placeholder="请输入昵称"
21 + :border="false"
22 + class="!bg-transparent !border-none !p-0 text-base"
23 + />
24 + </view>
25 + </view>
26 +
27 + <!-- Birthday -->
28 + <view class="mb-6">
29 + <label class="block text-sm font-medium text-gray-700 mb-2">出生年月</label>
30 + <view class="bg-white rounded-xl p-4 border border-gray-200" @click="showDatePicker = true">
31 + <view class="flex justify-between items-center">
32 + <text :class="{'text-gray-400': !formData.birthday, 'text-gray-900': formData.birthday}" class="text-base">
33 + {{ formData.birthday || '-/-/-/' }}
34 + </text>
35 + <DateIcon size="20" color="#888" />
36 + </view>
37 + </view>
38 + </view>
39 +
40 + <!-- Wheelchair -->
41 + <view class="mb-6">
42 + <label class="block text-sm font-medium text-gray-700 mb-2">是否需要轮椅出行</label>
43 + <view class="bg-white rounded-xl p-4 border border-gray-200" @click="showWheelchairPicker = true">
44 + <view class="flex justify-between items-center">
45 + <text :class="{'text-gray-400': !formData.wheelchair_text, 'text-gray-900': formData.wheelchair_text}" class="text-base">
46 + {{ formData.wheelchair_text || '请选择' }}
47 + </text>
48 + <Right size="16" color="#888" />
49 + </view>
50 + </view>
51 + </view>
52 + </view>
53 +
54 + <!-- Save Button -->
55 + <view class="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-gray-100">
56 + <nut-button type="primary" size="large" :color="isFormValid ? '#4A90E2' : '#D8D8D8'" block @click="handleSave" :disabled="!isFormValid">保存</nut-button>
57 + </view>
58 +
59 + <!-- Popups -->
60 + <nut-popup v-model:visible="showDatePicker" position="bottom">
61 + <nut-date-picker
62 + v-model="currentDate"
63 + @confirm="onDateConfirm"
64 + @cancel="showDatePicker = false"
65 + :min-date="minDate"
66 + :max-date="maxDate"
67 + title="选择出生年月"
68 + ></nut-date-picker>
69 + </nut-popup>
70 +
71 + <nut-popup v-model:visible="showWheelchairPicker" position="bottom">
72 + <nut-picker
73 + v-model="wheelchairValue"
74 + :columns="wheelchairColumns"
75 + @confirm="onWheelchairConfirm"
76 + @cancel="showWheelchairPicker = false"
77 + title="是否需要轮椅出行"
78 + ></nut-picker>
79 + </nut-popup>
80 +
81 + <nut-image-preview v-model:show="previewVisible" :images="previewImages" />
82 + </view>
83 +</template>
84 +
85 +<script setup>
86 +import { ref, reactive, onMounted, computed } from 'vue';
87 +import Taro from '@tarojs/taro';
88 +import { My, Date as DateIcon, Right } from '@nutui/icons-vue-taro';
89 +import BASE_URL from '@/utils/config';
90 +
91 +/**
92 + * @description 表单数据
93 + */
94 +const formData = reactive({
95 + avatar_url: '',
96 + nickname: '',
97 + birthday: '',
98 + wheelchair: null, // 0 for No, 1 for Yes
99 + wheelchair_text: '',
100 +});
101 +
102 +/**
103 + * @description 检查表单是否有效
104 + */
105 +const isFormValid = computed(() => {
106 + return formData.nickname && formData.birthday && formData.wheelchair !== null;
107 +});
108 +
109 +// --- Date Picker ---
110 +/**
111 + * @description 控制日期选择器显示
112 + */
113 +const showDatePicker = ref(false);
114 +/**
115 + * @description 最小可选日期
116 + */
117 +const minDate = new Date(1920, 0, 1);
118 +/**
119 + * @description 最大可选日期
120 + */
121 +const maxDate = new Date();
122 +/**
123 + * @description 当前选中的日期
124 + */
125 +const currentDate = ref(new Date());
126 +
127 +/**
128 + * @description 确认选择日期
129 + * @param {object} param0 - 包含 selectedValue 的对象
130 + */
131 +const onDateConfirm = ({ selectedValue }) => {
132 + formData.birthday = selectedValue.join('-');
133 + showDatePicker.value = false;
134 +};
135 +
136 +// --- Wheelchair Picker ---
137 +/**
138 + * @description 控制轮椅选择器显示
139 + */
140 +const showWheelchairPicker = ref(false);
141 +/**
142 + * @description 当前选中的轮椅选项
143 + */
144 +const wheelchairValue = ref([]);
145 +/**
146 + * @description 轮椅选择器选项
147 + */
148 +const wheelchairColumns = ref([
149 + { text: '是', value: 1 },
150 + { text: '否', value: 0 },
151 +]);
152 +
153 +/**
154 + * @description 确认选择轮椅选项
155 + * @param {object} param0 - 包含 selectedValue 和 selectedOptions 的对象
156 + */
157 +const onWheelchairConfirm = ({ selectedValue, selectedOptions }) => {
158 + formData.wheelchair = selectedValue[0];
159 + formData.wheelchair_text = selectedOptions.map((option) => option.text).join('');
160 + showWheelchairPicker.value = false;
161 +};
162 +
163 +// --- Avatar ---
164 +/**
165 + * @description 控制图片预览显示
166 + */
167 +const previewVisible = ref(false);
168 +/**
169 + * @description 预览的图片列表
170 + */
171 +const previewImages = ref([]);
172 +
173 +/**
174 + * @description 更换头像
175 + */
176 +const changeAvatar = () => {
177 + Taro.chooseImage({
178 + count: 1,
179 + sizeType: ['compressed'],
180 + sourceType: ['album', 'camera'],
181 + success: (res) => {
182 + const tempFile = res.tempFiles[0];
183 + if (tempFile.size > 10 * 1024 * 1024) {
184 + Taro.showToast({
185 + title: '图片大小不能超过10MB',
186 + icon: 'none',
187 + });
188 + return;
189 + }
190 +
191 + Taro.showLoading({ title: '上传中...' });
192 +
193 + Taro.uploadFile({
194 + url: BASE_URL + '/admin/?m=srv&a=upload',
195 + filePath: tempFile.path,
196 + name: 'file',
197 + success: (uploadRes) => {
198 + Taro.hideLoading();
199 + const data = JSON.parse(uploadRes.data);
200 + if (data.code === 0) {
201 + formData.avatar_url = data.data.url;
202 + Taro.showToast({ title: '上传成功', icon: 'success' });
203 + } else {
204 + Taro.showToast({ title: data.msg || '上传失败', icon: 'none' });
205 + }
206 + },
207 + fail: () => {
208 + Taro.hideLoading();
209 + Taro.showToast({ title: '上传失败,请稍后重试', icon: 'none' });
210 + },
211 + });
212 + },
213 + fail: () => {
214 + Taro.showToast({ title: '选择图片失败', icon: 'none' });
215 + },
216 + });
217 +};
218 +
219 +// --- Save ---
220 +/**
221 + * @description 保存用户信息
222 + */
223 +const handleSave = () => {
224 + if (!isFormValid.value) {
225 + Taro.showToast({
226 + title: '请填写所有必填项',
227 + icon: 'none',
228 + });
229 + return;
230 + }
231 + console.log('Saving data:', formData);
232 + Taro.showLoading({ title: '保存中...' });
233 + // Mock save process
234 + setTimeout(() => {
235 + Taro.hideLoading();
236 + Taro.showToast({
237 + title: '保存成功',
238 + icon: 'success',
239 + duration: 1500,
240 + complete: () => {
241 + Taro.navigateBack();
242 + },
243 + });
244 + }, 1000);
245 +};
246 +
247 +/**
248 + * @description 页面加载时获取初始数据
249 + */
250 +onMounted(() => {
251 + // Mock fetching user data, in a real app, you would call an API
252 + // const mockUserData = {
253 + // avatar_url: 'https://img.yzcdn.cn/vant/cat.jpeg',
254 + // nickname: '张三',
255 + // birthday: '1990-05-15',
256 + // wheelchair: 0,
257 + // wheelchair_text: '否',
258 + // };
259 + // Object.assign(formData, mockUserData);
260 +
261 + // Initialize pickers with current data
262 + // currentDate.value = new Date(mockUserData.birthday);
263 + // wheelchairValue.value = [mockUserData.wheelchair];
264 +});
265 +</script>
266 +
267 +<style>
268 +.nut-input.mt-2 {
269 + padding: 12px 0 !important;
270 + border-bottom: 1px solid #e5e7eb;
271 +}
272 +</style>
1 <!-- 1 <!--
2 * @Date: 2025-08-27 17:43:45 2 * @Date: 2025-08-27 17:43:45
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-08-28 21:45:22 4 + * @LastEditTime: 2025-08-28 22:08:17
5 * @FilePath: /lls_program/src/pages/Welcome/index.vue 5 * @FilePath: /lls_program/src/pages/Welcome/index.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
...@@ -61,10 +61,10 @@ ...@@ -61,10 +61,10 @@
61 </view> 61 </view>
62 <!-- Action Buttons --> 62 <!-- Action Buttons -->
63 <view class="space-y-4 mt-auto"> 63 <view class="space-y-4 mt-auto">
64 - <view @click="navigateTo('/pages/CreateFamily/index')" class="w-full py-3.5 bg-blue-500 text-white text-lg font-medium rounded-full text-center"> 64 + <view @tap="handleNavigate('/pages/CreateFamily/index')" class="w-full py-3.5 bg-blue-500 text-white text-lg font-medium rounded-full text-center">
65 创建家庭 65 创建家庭
66 </view> 66 </view>
67 - <view @click="navigateTo('/pages/JoinFamily/index')" class="w-full py-3.5 bg-white text-gray-800 text-lg font-medium rounded-full border border-gray-300 text-center" style="margin-bottom: 1rem;"> 67 + <view @tap="handleNavigate('/pages/JoinFamily/index')" class="w-full py-3.5 bg-white text-gray-800 text-lg font-medium rounded-full border border-gray-300 text-center" style="margin-bottom: 1rem;">
68 加入家庭 68 加入家庭
69 </view> 69 </view>
70 </view> 70 </view>
...@@ -82,6 +82,27 @@ import welcomeHomeImg from '../../assets/images/welcome_home.png'; ...@@ -82,6 +82,27 @@ import welcomeHomeImg from '../../assets/images/welcome_home.png';
82 const navigateTo = (url) => { 82 const navigateTo = (url) => {
83 Taro.navigateTo({ url }); 83 Taro.navigateTo({ url });
84 }; 84 };
85 +
86 +const handleNavigate = (url) => {
87 + // TODO: 模拟检查个人信息是否完善
88 + const hasProfile = true; // 假设未完善
89 +
90 + if (!hasProfile) {
91 + Taro.showModal({
92 + title: '提示',
93 + content: '参加活动需要完善个人信息',
94 + cancelText: '关闭',
95 + confirmText: '完善信息',
96 + success: (res) => {
97 + if (res.confirm) {
98 + Taro.navigateTo({ url: '/pages/AddProfile/index' });
99 + }
100 + },
101 + });
102 + } else {
103 + navigateTo(url);
104 + }
105 +};
85 </script> 106 </script>
86 107
87 <style lang="less"> 108 <style lang="less">
......