Переглянути джерело

feat: 新增个人信息修改,补充多语言

lixuan 3 тижнів тому
батько
коміт
021c9cef31

+ 19 - 0
apps/web-velofex/src/api/core/user.ts

@@ -412,6 +412,18 @@ export namespace UserApi {
     isActive?: boolean;
   }
 
+  export interface UpdateUserInfoParams {
+    id: string;
+    gogs_email: string;
+    gender: string;
+    birthday: string;
+    avatarFileId: string;
+    langNameList: Array<{
+      name: string;
+      value: string;
+    }>;
+  }
+
   export interface UserResult {
     isSuccess: boolean;
     code: number;
@@ -865,3 +877,10 @@ export async function getMenuPermissionButtonsApi(
     data,
   );
 }
+
+/**
+ * 更新用户信息
+ */
+export async function updateUserInfoApi(data: UserApi.UpdateUserInfoParams) {
+  return requestClient.post<UserApi.UserResult>('/api/user/doUpdate', data);
+}

+ 1 - 1
apps/web-velofex/src/layouts/header/header.vue

@@ -49,7 +49,7 @@ function handleMenuClick({ key }: { key: string }) {
   if (key === 'Logout') {
     handleLogout();
   } else if (key === 'ChangeInformation') {
-    window.open('/Views/Home/userSet.html', '_blank');
+    router.push('/user-profile');
   }
 }
 

+ 25 - 0
apps/web-velofex/src/locales/langs/en-US/page.json

@@ -343,5 +343,30 @@
   "productManagement":{
     "breadcrumb":"Dashboard / Product List",
     "title":"Product List"
+  },
+  "userProfile": {
+    "breadcrumb": "Dashboard / Personal Information",
+    "title": "Personal Information",
+    "avatar": "Avatar",
+    "uploadAvatar": "Upload Avatar",
+    "clickToChangeAvatar": "Click to change avatar",
+    "username": "Username",
+    "gitAccount": "Git Account/Email",
+    "gender": "Gender",
+    "genderOptions": {
+      "secret": "Secret",
+      "male": "Male",
+      "female": "Female"
+    },
+    "birthday": "Birthday",
+    "cancel": "Cancel",
+    "save": "Save",
+    "messages": {
+      "userIdRequired": "User ID is required",
+      "updateSuccess": "Update successful",
+      "updateFailed": "Update failed",
+      "avatarUploadSuccess": "Avatar uploaded successfully",
+      "avatarUploadFailed": "Avatar upload failed"
+    }
   }
 }

+ 25 - 0
apps/web-velofex/src/locales/langs/zh-CN/page.json

@@ -343,5 +343,30 @@
   "productManagement":{
     "breadcrumb":"首页 / 产品管理",
     "title":"产品列表"
+  },
+  "userProfile": {
+    "breadcrumb": "首页 / 个人信息",
+    "title": "个人信息",
+    "avatar": "头像",
+    "uploadAvatar": "上传头像",
+    "clickToChangeAvatar": "点击更换头像",
+    "username": "用户名",
+    "gitAccount": "Git账户/邮箱",
+    "gender": "性别",
+    "genderOptions": {
+      "secret": "保密",
+      "male": "男",
+      "female": "女"
+    },
+    "birthday": "生日",
+    "cancel": "取消",
+    "save": "保存",
+    "messages": {
+      "userIdRequired": "用户ID不能为空",
+      "updateSuccess": "更新成功",
+      "updateFailed": "更新失败",
+      "avatarUploadSuccess": "头像上传成功",
+      "avatarUploadFailed": "头像上传失败"
+    }
   }
 }

+ 9 - 0
apps/web-velofex/src/router/routes/external/router-a.ts

@@ -72,6 +72,15 @@ const routes: RouteRecordRaw[] = [
           title: $t('homeModule.enterpriseCustomers'),
         },
       },
+      {
+        name: 'UserProfile',
+        path: 'user-profile',
+        component: () => import('#/views/dashboard/user-profile/index.vue'),
+        meta: {
+          icon: 'carbon:user',
+          title: '个人信息',
+        },
+      },
     ],
   },
 ];

+ 2 - 1
apps/web-velofex/src/views/dashboard/home/user-info.vue

@@ -5,6 +5,7 @@ import { useAccessStore, useUserStore } from '@vben/stores';
 
 import { $t } from '@/locales';
 
+import { router } from '#/router';
 import { useLoginModalStore } from '#/store';
 
 const userStore = useUserStore();
@@ -25,7 +26,7 @@ function openLogin() {
 }
 
 function handleEditProfile() {
-  window.open('/Views/Home/userSet.html', '_blank');
+  router.push('/user-profile');
 }
 </script>
 

+ 312 - 0
apps/web-velofex/src/views/dashboard/user-profile/index.vue

@@ -0,0 +1,312 @@
+<script setup lang="ts">
+import { onMounted, reactive, ref } from 'vue';
+
+import { useAccessStore, useUserStore } from '@vben/stores';
+
+import { $t } from '@/locales';
+import {
+  Button,
+  DatePicker,
+  Input,
+  message,
+  Radio,
+  RadioGroup,
+  Upload,
+} from 'antdv-next';
+import dayjs from 'dayjs';
+import localeData from 'dayjs/plugin/localeData';
+import weekday from 'dayjs/plugin/weekday';
+
+import { getUserInfoApi, updateUserInfoApi } from '#/api';
+
+dayjs.extend(weekday);
+dayjs.extend(localeData);
+
+const userStore = useUserStore();
+const accessStore = useAccessStore();
+const loading = ref(false);
+
+const formData = reactive({
+  id: '',
+  gogs_email: '',
+  gender: '-1',
+  birthday: null as any,
+  avatarFileId: '',
+  fileList: [] as any[],
+  langNameList: [
+    {
+      name: 'zh-CN',
+      value: '',
+    },
+  ] as { name: string; value: string }[],
+});
+
+const genderOptions = [
+  { label: $t('userProfile.genderOptions.secret'), value: '-1' },
+  { label: $t('userProfile.genderOptions.male'), value: '1' },
+  { label: $t('userProfile.genderOptions.female'), value: '0' },
+];
+
+function handleAvatarUpload(info: any) {
+  if (info.file.status === 'done') {
+    if (info.file.response?.result?.[0]?.id) {
+      formData.avatarFileId = info.file.response.result[0].id;
+      message.success($t('userProfile.messages.avatarUploadSuccess'));
+    } else {
+      message.error($t('userProfile.messages.avatarUploadFailed'));
+    }
+  } else if (info.file.status === 'error') {
+    message.error($t('userProfile.messages.avatarUploadFailed'));
+  }
+}
+
+async function handleSubmit() {
+  if (!formData.id) {
+    message.error($t('userProfile.messages.userIdRequired'));
+    return;
+  }
+
+  loading.value = true;
+  try {
+    const submitData = {
+      ...formData,
+      birthday: formData.birthday ? formData.birthday.format('YYYY-MM-DD') : '',
+    };
+
+    const result = await updateUserInfoApi(submitData);
+    if (result && result.isSuccess) {
+      message.success($t('userProfile.messages.updateSuccess'));
+
+      const userInfo = await getUserInfoApi();
+      if (userInfo && userInfo.isSuccess) {
+        const updatedUserInfo = {
+          account:
+            userInfo.result?.account || userInfo.result?.englishName || '',
+          avatar: userInfo.result?.avatarFileId || '',
+          cellPhone: userInfo.result?.cellPhone || '',
+          realName: userInfo.result?.chineseName || userInfo.result?.name || '',
+          email: userInfo.result?.emailAddress || '',
+          roles: [],
+          userId: userInfo.result?.id || '',
+          username: userInfo.result?.name || '',
+        };
+        userStore.setUserInfo(updatedUserInfo);
+      }
+    } else {
+      message.error($t('userProfile.messages.updateFailed'));
+    }
+  } catch (error) {
+    console.error('更新失败:', error);
+    message.error($t('userProfile.messages.updateFailed'));
+  } finally {
+    loading.value = false;
+  }
+}
+
+function handleBack() {
+  window.history.back();
+}
+
+onMounted(async () => {
+  const localUserInfo = userStore.userInfo;
+  if (localUserInfo) {
+    formData.id = localUserInfo.userId || '';
+    formData.gogs_email = localUserInfo.email || '';
+    formData.avatarFileId = localUserInfo.avatar || '';
+    if (formData.langNameList && formData.langNameList[0]) {
+      formData.langNameList[0].value = localUserInfo.realName || '';
+    }
+  }
+
+  const userInfo = await getUserInfoApi();
+  if (userInfo && userInfo.isSuccess && userInfo.result) {
+    const info = userInfo.result;
+    formData.id = info.id || '';
+    formData.gogs_email = (info as any).gogs_email || '';
+    formData.gender = (info as any).gender?.toString() || '-1';
+    formData.birthday = (info as any).birthday
+      ? dayjs((info as any).birthday)
+      : null;
+    formData.avatarFileId = info.avatarFileId || '';
+    if (formData.langNameList && formData.langNameList.length > 0) {
+      formData.langNameList[0]!.value = info.chineseName || info.name || '';
+    }
+
+    if (info.avatarFileId) {
+      formData.fileList = [
+        {
+          uid: '-1',
+          name: 'avatar.png',
+          status: 'done',
+          url: `/File/Download?fileId=${info.avatarFileId}`,
+          response: {
+            result: [
+              {
+                id: info.avatarFileId,
+              },
+            ],
+          },
+        },
+      ];
+    }
+  }
+});
+</script>
+
+<template>
+  <div class="p-6">
+    <div class="mb-4 flex items-center justify-between">
+      <div class="text-sm text-[#462424] text-gray-500">
+        {{ $t('userProfile.breadcrumb') }}
+      </div>
+      <div
+        class="flex cursor-pointer items-center gap-[9px] text-[16px] font-bold text-[#462424]"
+        @click="handleBack"
+      >
+        <div
+          class="global-color flex h-[22px] w-[22px] items-center justify-center rounded-full"
+        >
+          <img
+            alt=""
+            class="h-[11px] w-[10px]"
+            src="@/assets/image/the-left.png"
+          />
+        </div>
+        {{ $t('btn.back') }}
+      </div>
+    </div>
+
+    <div class="mb-8 text-[26px] font-bold text-[#462424]">
+      {{ $t('userProfile.title') }}
+    </div>
+
+    <div class="mx-auto max-w-3xl rounded-lg bg-white p-8 shadow-md">
+      <div class="mb-10 flex flex-col items-center">
+        <Upload
+          v-model:file-list="formData.fileList"
+          :action="`/fileApi/File/UploadFiles?Authorization=${accessStore.accessToken}`"
+          :headers="{ Authorization: String(accessStore.accessToken) }"
+          :max-count="1"
+          :show-upload-list="false"
+          class="mb-4"
+          list-type="picture"
+          @change="handleAvatarUpload"
+        >
+          <div
+            class="flex h-[120px] w-[120px] cursor-pointer items-center justify-center overflow-hidden rounded-full border-2 border-dashed border-gray-300"
+          >
+            <img
+              v-if="formData.avatarFileId"
+              :alt="$t('userProfile.avatar')"
+              :src="`/File/Download?fileId=${formData.avatarFileId}`"
+              class="h-full w-full rounded-full object-cover"
+            />
+            <div
+              v-else
+              class="flex flex-col items-center text-sm text-gray-400"
+            >
+              {{ $t('userProfile.uploadAvatar') }}
+            </div>
+          </div>
+        </Upload>
+        <div class="text-sm text-gray-500">
+          {{ $t('userProfile.clickToChangeAvatar') }}
+        </div>
+      </div>
+
+      <div class="space-y-6">
+        <div class="grid grid-cols-1 gap-6 md:grid-cols-2">
+          <div class="flex flex-col gap-2">
+            <label class="text-sm font-medium text-gray-700">{{
+              $t('userProfile.username')
+            }}</label>
+            <Input
+              v-model:value="formData.langNameList[0]!.value"
+              class="h-[48px] rounded-[12px] border-[#e0e0e0]"
+            />
+          </div>
+
+          <div class="flex flex-col gap-2">
+            <label class="text-sm font-medium text-gray-700">{{
+              $t('userProfile.gitAccount')
+            }}</label>
+            <Input
+              v-model:value="formData.gogs_email"
+              class="h-[48px] rounded-[12px] border-[#e0e0e0]"
+            />
+          </div>
+        </div>
+
+        <div class="grid grid-cols-1 gap-6 md:grid-cols-2">
+          <div class="flex flex-col gap-2">
+            <label class="text-sm font-medium text-gray-700">{{
+              $t('userProfile.gender')
+            }}</label>
+            <RadioGroup v-model:value="formData.gender" class="mt-2">
+              <Radio
+                v-for="option in genderOptions"
+                :key="option.value"
+                :value="option.value"
+                class="mr-4"
+              >
+                {{ option.label }}
+              </Radio>
+            </RadioGroup>
+          </div>
+
+          <div class="flex flex-col gap-2">
+            <label class="text-sm font-medium text-gray-700">{{
+              $t('userProfile.birthday')
+            }}</label>
+            <DatePicker
+              v-model:value="formData.birthday"
+              class="h-[48px] rounded-[12px] border-[#e0e0e0]"
+              format="YYYY-MM-DD"
+            />
+          </div>
+        </div>
+      </div>
+
+      <div class="mt-12 flex justify-center gap-4">
+        <Button
+          class="h-[42px] rounded-[12px] border-[#e0e0e0] px-8"
+          @click="handleBack"
+        >
+          {{ $t('userProfile.cancel') }}
+        </Button>
+        <Button
+          :loading="loading"
+          class="h-[42px] rounded-[12px] bg-[#462424] px-8 text-white"
+          type="primary"
+          @click="handleSubmit"
+        >
+          {{ $t('userProfile.save') }}
+        </Button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+:deep(.ant-radio-group) {
+  display: flex;
+  gap: 24px;
+  margin-top: 8px;
+}
+
+:deep(.ant-radio-button-wrapper) {
+  margin-right: 12px !important;
+  border-radius: 8px !important;
+}
+
+:deep(.ant-radio-button-wrapper-checked) {
+  color: white !important;
+  background-color: #462424 !important;
+  border-color: #462424 !important;
+}
+
+:deep(.ant-radio-button-wrapper-checked:hover) {
+  background-color: #5a3434 !important;
+  border-color: #5a3434 !important;
+}
+</style>