Răsfoiți Sursa

feat: 个人资料支持修改地区/年龄/性别,修复任务列表重复加载

- 个人资料页(profile)新增地区、年龄、性别展示与编辑功能
- UserInfo 类型和 API 接口新增 region/age/gender 字段
- 性别编辑采用点选交互,年龄为数字输入,地区为文本输入
- 活动页 Address 按钮改为跳转到地区编辑页
- 修复福利任务列表连续点击导致数据重复加载的问题
urbanu 4 săptămâni în urmă
părinte
comite
a18a975285

+ 3 - 0
src/api/user.ts

@@ -12,6 +12,9 @@ export function requestGetUserInfo(): Promise<CommonResponse<UserInfo>> {
 export function requestUpdateUserInfo(data: {
   avatar?: string;
   nickname?: string;
+  region?: string;
+  age?: number;
+  gender?: number;
 }): Promise<CommonResponse> {
   return http.request({
     url: "/api/v1/dt/user/update",

+ 3 - 0
src/typings/global.ts

@@ -30,6 +30,9 @@ interface UserInfo {
   balance: string;
   status: number;
   createdAt: number;
+  region?: string;
+  age?: number;
+  gender?: number; // 0-未设置 1-男 2-女
 }
 
 // 任务分类

+ 1 - 1
src/views/game-show/index.vue

@@ -98,7 +98,7 @@ const goTaskDetail = (id: number) => router.push(`/task/detail/${id}`);
 const goTaskList = () => router.push('/task/list');
 const goMyTask = () => router.push('/my-task');
 const goInvite = () => router.push('/invite');
-const goProfile = () => router.push('/profile/edit');
+const goProfile = () => router.push('/profile/edit?type=region');
 
 onMounted(() => {
   getHotTasks();

+ 96 - 0
src/views/profile/edit.vue

@@ -77,6 +77,47 @@
       </div>
     </div>
 
+    <!-- 地区编辑 -->
+    <div class="edit-section" v-if="type === 'region'">
+      <van-field
+        v-model="region"
+        :placeholder="$t('user.region')"
+        maxlength="50"
+        class="edit-input"
+      />
+    </div>
+
+    <!-- 年龄编辑 -->
+    <div class="edit-section" v-if="type === 'age'">
+      <van-field
+        v-model="age"
+        type="digit"
+        :placeholder="$t('user.age')"
+        maxlength="3"
+        class="edit-input"
+      />
+    </div>
+
+    <!-- 性别编辑 -->
+    <div class="edit-section" v-if="type === 'gender'">
+      <div class="gender-options">
+        <div
+          class="gender-item"
+          :class="{ active: gender === 1 }"
+          @click="gender = 1"
+        >
+          {{ $t('user.male') }}
+        </div>
+        <div
+          class="gender-item"
+          :class="{ active: gender === 2 }"
+          @click="gender = 2"
+        >
+          {{ $t('user.female') }}
+        </div>
+      </div>
+    </div>
+
     <!-- 实名认证 -->
     <div class="edit-section" v-if="type === 'realName'">
       <van-field
@@ -120,6 +161,9 @@ const nickname = ref(userInfo.value?.nickname || '');
 const phone = ref('');
 const email = ref('');
 const code = ref('');
+const region = ref(userInfo.value?.region || '');
+const age = ref(userInfo.value?.age ? String(userInfo.value.age) : '');
+const gender = ref(userInfo.value?.gender || 0);
 const realName = ref('');
 const idCard = ref('');
 const countdown = ref(0);
@@ -130,6 +174,9 @@ const getTitle = () => {
     nickname: t('user.nickname'),
     phone: t('user.bindPhone'),
     email: t('user.bindEmail'),
+    region: t('user.region'),
+    age: t('user.age'),
+    gender: t('user.gender'),
     realName: t('user.realName')
   };
   return map[type.value] || '';
@@ -198,6 +245,30 @@ const handleSave = async () => {
         res = await requestBindEmail({ email: email.value, code: code.value });
         break;
 
+      case 'region':
+        if (!region.value.trim()) {
+          showToast(t('user.region'));
+          return;
+        }
+        res = await requestUpdateUserInfo({ region: region.value });
+        break;
+
+      case 'age':
+        if (!age.value || Number(age.value) <= 0) {
+          showToast(t('user.age'));
+          return;
+        }
+        res = await requestUpdateUserInfo({ age: Number(age.value) });
+        break;
+
+      case 'gender':
+        if (!gender.value) {
+          showToast(t('user.gender'));
+          return;
+        }
+        res = await requestUpdateUserInfo({ gender: gender.value });
+        break;
+
       case 'realName':
         if (!realName.value || !idCard.value) {
           showToast('Please fill in all fields');
@@ -227,6 +298,9 @@ const goBack = () => {
 onMounted(() => {
   if (userInfo.value) {
     nickname.value = userInfo.value.nickname || '';
+    region.value = userInfo.value.region || '';
+    age.value = userInfo.value.age ? String(userInfo.value.age) : '';
+    gender.value = userInfo.value.gender || 0;
   }
 });
 </script>
@@ -302,6 +376,28 @@ onMounted(() => {
     }
   }
 
+  .gender-options {
+    display: flex;
+    gap: 12px;
+
+    .gender-item {
+      flex: 1;
+      padding: 16px;
+      text-align: center;
+      background: rgba(255, 255, 255, 0.05);
+      border-radius: 12px;
+      font-size: 14px;
+      color: rgba(255, 255, 255, 0.7);
+      border: 2px solid transparent;
+
+      &.active {
+        border-color: #ffc300;
+        color: #ffc300;
+        background: rgba(255, 195, 0, 0.1);
+      }
+    }
+  }
+
   .tips {
     margin-top: 16px;
     padding: 12px;

+ 34 - 0
src/views/profile/index.vue

@@ -45,6 +45,33 @@
         </div>
       </div>
 
+      <!-- 地区 -->
+      <div class="info-item" @click="goEdit('region')">
+        <span class="label">{{ $t('user.region') }}</span>
+        <div class="value">
+          <span>{{ userInfo?.region || '-' }}</span>
+          <van-icon name="arrow" />
+        </div>
+      </div>
+
+      <!-- 年龄 -->
+      <div class="info-item" @click="goEdit('age')">
+        <span class="label">{{ $t('user.age') }}</span>
+        <div class="value">
+          <span>{{ userInfo?.age || '-' }}</span>
+          <van-icon name="arrow" />
+        </div>
+      </div>
+
+      <!-- 性别 -->
+      <div class="info-item" @click="goEdit('gender')">
+        <span class="label">{{ $t('user.gender') }}</span>
+        <div class="value">
+          <span>{{ getGenderText() }}</span>
+          <van-icon name="arrow" />
+        </div>
+      </div>
+
       <!-- 实名认证 -->
       <div class="info-item" @click="goEdit('realName')">
         <span class="label">{{ $t('user.realName') }}</span>
@@ -109,6 +136,13 @@ const formatPhone = (phone: string | undefined) => {
   return phone;
 };
 
+const getGenderText = () => {
+  const gender = userInfo.value?.gender;
+  if (gender === 1) return t('user.male');
+  if (gender === 2) return t('user.female');
+  return '-';
+};
+
 const getRealNameStatus = () => {
   const status = userInfo.value?.realNameStatus;
   if (status === 1) return t('task.completed');

+ 16 - 0
src/views/task/list.vue

@@ -148,11 +148,15 @@ const getCategories = async () => {
 };
 
 const getTaskList = async (isRefresh = false) => {
+  if (loading.value && !isRefresh) return;
+
   if (isRefresh) {
     page.value = 1;
     finished.value = false;
   }
 
+  loading.value = true;
+
   try {
     const res = await requestTaskList({
       page: page.value,
@@ -220,6 +224,18 @@ watch(categoryId, () => {
   getTaskList(true);
 });
 
+// 监听路由变化,重新进入同一页面时重置数据
+watch(
+  () => route.fullPath,
+  () => {
+    taskList.value = [];
+    page.value = 1;
+    finished.value = false;
+    getCategories();
+    getTaskList(true);
+  }
+);
+
 onMounted(() => {
   getCategories();
   getTaskList(true);