新增:新增上传头像 API,采用本地存储方式存储头像

This commit is contained in:
2023-01-05 22:32:23 +08:00
parent e77c77419b
commit 5252c54c48
54 changed files with 931 additions and 937 deletions

View File

@@ -0,0 +1,18 @@
import axios from 'axios';
export interface BasicInfoModel {
username: string;
nickname: string;
gender: number;
}
export interface AvatarRes {
avatar: string;
}
export function uploadAvatar(data: FormData) {
return axios.post<AvatarRes>('/system/user/center/avatar', data);
}
export function saveUserInfo() {
return axios.get('/api/user/save-info');
}

View File

@@ -1,57 +0,0 @@
import axios from 'axios';
export interface MyProjectRecord {
id: number;
name: string;
description: string;
peopleNumber: number;
contributors: {
name: string;
email: string;
avatar: string;
}[];
}
export function queryMyProjectList() {
return axios.get('/api/user/my-project/list');
}
export interface MyTeamRecord {
id: number;
avatar: string;
name: string;
peopleNumber: number;
}
export function queryMyTeamList() {
return axios.get('/api/user/my-team/list');
}
export interface LatestActivity {
id: number;
title: string;
description: string;
avatar: string;
}
export function queryLatestActivity() {
return axios.get<LatestActivity[]>('/api/user/latest-activity');
}
export function saveUserInfo() {
return axios.get('/api/user/save-info');
}
export interface BasicInfoModel {
username: string;
nickname: string;
gender: number;
}
export function userUploadApi(
data: FormData,
config: {
controller: AbortController;
onUploadProgress?: (progressEvent: any) => void;
}
) {
// const controller = new AbortController();
return axios.post('/api/user/upload', data, config);
}

View File

@@ -8,6 +8,5 @@ export default {
'messageBox.noContent': 'No Content',
'messageBox.switchRoles': 'Switch Roles',
'messageBox.userCenter': 'User Center',
'messageBox.userSettings': 'User Settings',
'messageBox.logout': 'Logout',
};

View File

@@ -7,7 +7,6 @@ export default {
'messageBox.viewMore': '查看更多',
'messageBox.noContent': '暂无内容',
'messageBox.switchRoles': '切换角色',
'messageBox.userCenter': '用户中心',
'messageBox.userSettings': '用户设置',
'messageBox.userCenter': '个人中心',
'messageBox.logout': '退出登录',
};

View File

@@ -144,7 +144,7 @@
:size="32"
:style="{ marginRight: '8px', cursor: 'pointer' }"
>
<img alt="avatar" :src="loginStore.avatar ?? getAvatar(loginStore.gender)" />
<img alt="avatar" :src="getAvatar(loginStore)" />
</a-avatar>
<template #content>
<a-doption>
@@ -156,18 +156,10 @@
</a-space>
</a-doption>
<a-doption>
<a-space @click="$router.push({ name: 'Info' })">
<icon-user />
<span>
{{ $t('messageBox.userCenter') }}
</span>
</a-space>
</a-doption>
<a-doption>
<a-space @click="$router.push({ name: 'Setting' })">
<a-space @click="$router.push({ name: 'UserCenter' })">
<icon-settings />
<span>
{{ $t('messageBox.userSettings') }}
{{ $t('messageBox.userCenter') }}
</span>
</a-space>
</a-doption>

12
continew-admin-ui/src/hooks/axios.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
import axios, { Axios, AxiosResponse, AxiosRequestConfig } from "axios";
declare module "axios" {
interface AxiosResponse<T = any> {
success: boolean; // 是否成功
code: number; // 状态码
msg: string; // 状态信息
timestamp: string; // 时间戳
data: T; // 返回数据
}
export function create(config?: AxiosRequestConfig): AxiosInstance;
}

View File

@@ -23,8 +23,7 @@ import locale403 from '@/views/exception/403/locale/en-US';
import locale404 from '@/views/exception/404/locale/en-US';
import locale500 from '@/views/exception/500/locale/en-US';
import localeUserInfo from '@/views/user/info/locale/en-US';
import localeUserSetting from '@/views/user/setting/locale/en-US';
import localeUserCenter from '@/views/system/user/center/locale/en-US';
import localeSettings from './en-US/settings';
@@ -62,6 +61,5 @@ export default {
...locale403,
...locale404,
...locale500,
...localeUserInfo,
...localeUserSetting,
...localeUserCenter,
};

View File

@@ -23,8 +23,7 @@ import locale403 from '@/views/exception/403/locale/zh-CN';
import locale404 from '@/views/exception/404/locale/zh-CN';
import locale500 from '@/views/exception/500/locale/zh-CN';
import localeUserInfo from '@/views/user/info/locale/zh-CN';
import localeUserSetting from '@/views/user/setting/locale/zh-CN';
import localeUserCenter from '@/views/system/user/center/locale/zh-CN';
import localeSettings from './zh-CN/settings';
@@ -62,6 +61,5 @@ export default {
...locale403,
...locale404,
...locale500,
...localeUserInfo,
...localeUserSetting,
...localeUserCenter,
};

View File

@@ -17,8 +17,7 @@ import '@/views/profile/basic/mock';
import '@/views/visualization/data-analysis/mock';
import '@/views/visualization/multi-dimension-data-analysis/mock';
import '@/views/user/info/mock';
import '@/views/user/setting/mock';
import '@/views/system/user/center/mock';
Mock.setup({
timeout: '600-1000',

View File

@@ -13,7 +13,7 @@ export default function setupUserLoginInfoGuard(router: Router) {
next();
} else {
try {
await loginStore.info();
await loginStore.getInfo();
next();
} catch (error) {
await loginStore.logout();

View File

@@ -2,7 +2,7 @@ import { DEFAULT_LAYOUT } from '../base';
import { AppRouteRecordRaw } from '../types';
const USER: AppRouteRecordRaw = {
path: '/user',
path: '/system/user',
name: 'user',
component: DEFAULT_LAYOUT,
meta: {
@@ -13,21 +13,11 @@ const USER: AppRouteRecordRaw = {
},
children: [
{
path: 'info',
name: 'Info',
component: () => import('@/views/user/info/index.vue'),
path: 'center',
name: 'UserCenter',
component: () => import('@/views/system/user/center/index.vue'),
meta: {
locale: 'menu.user.info',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'setting',
name: 'Setting',
component: () => import('@/views/user/setting/index.vue'),
meta: {
locale: 'menu.user.setting',
locale: 'menu.user.center',
requiresAuth: true,
roles: ['*'],
},

View File

@@ -75,7 +75,7 @@ const useLoginStore = defineStore('user', {
},
// 获取用户信息
async info() {
async getInfo() {
const res = await getUserInfo();
this.setInfo(res.data);
},

View File

@@ -1,12 +1,20 @@
import { UserState } from '@/store/modules/login/types';
import Unknown from '../assets/images/avatar/unknown.png';
import Male from '../assets/images/avatar/male.png';
import Female from '../assets/images/avatar/female.png';
export default function getAvatar(gender: number | undefined) {
if (gender === 1) {
export default function getAvatar(loginStore: UserState) {
const userAvatar = loginStore.avatar;
if (userAvatar) {
const baseUrl = import.meta.env.VITE_API_BASE_URL;
return `${baseUrl}/avatar/${userAvatar}`;
}
const userGender = loginStore.gender;
if (userGender === 1) {
return Male;
}
if (gender === 2) {
if (userGender === 2) {
return Female;
}
return Unknown;

View File

@@ -12,7 +12,7 @@
<div v-if="userInfo">
<a-space :size="12">
<a-avatar :size="24">
<img :src="userInfo.avatar ?? getAvatar(userInfo.gender)" />
<img :src="getAvatar(userInfo)" />
</a-avatar>
<a-typography-text>
{{ userInfo.nickname }} {{ $t('monitor.studioPreview.studio') }}

View File

@@ -56,7 +56,7 @@
<script lang="ts">
export default {
name: 'Dashboard', // If you want the include property of keep-alive to take effect, you must name the component
name: 'Workplace', // If you want the include property of keep-alive to take effect, you must name the component
};
</script>

View File

@@ -7,44 +7,38 @@
:wrapper-col-props="{ span: 16 }"
>
<a-form-item
:label="$t('userSetting.basicInfo.form.label.username')"
:label="$t('userCenter.basicInfo.form.label.username')"
:rules="[
{
required: true,
message: $t('userSetting.form.error.username.required'),
message: $t('userCenter.form.error.username.required'),
},
]"
disabled
>
<a-input
v-model="formData.username"
:placeholder="$t('userSetting.basicInfo.placeholder.username')"
:placeholder="$t('userCenter.basicInfo.placeholder.username')"
/>
</a-form-item>
<a-form-item
field="nickname"
:label="$t('userSetting.basicInfo.form.label.nickname')"
:label="$t('userCenter.basicInfo.form.label.nickname')"
:rules="[
{
required: true,
message: $t('userSetting.form.error.nickname.required'),
message: $t('userCenter.form.error.nickname.required'),
},
]"
>
<a-input
v-model="formData.nickname"
:placeholder="$t('userSetting.basicInfo.placeholder.nickname')"
:placeholder="$t('userCenter.basicInfo.placeholder.nickname')"
/>
</a-form-item>
<a-form-item
field="gender"
:label="$t('userSetting.basicInfo.form.label.gender')"
:rules="[
{
required: true,
message: $t('userSetting.form.error.gender.required'),
},
]"
:label="$t('userCenter.basicInfo.form.label.gender')"
>
<a-radio-group v-model="formData.gender">
<a-radio :value="1"></a-radio>
@@ -55,10 +49,10 @@
<a-form-item>
<a-space>
<a-button type="primary" @click="validate">
{{ $t('userSetting.save') }}
{{ $t('userCenter.save') }}
</a-button>
<a-button type="secondary" @click="reset">
{{ $t('userSetting.reset') }}
{{ $t('userCenter.reset') }}
</a-button>
</a-space>
</a-form-item>
@@ -69,7 +63,7 @@
import { ref } from 'vue';
import { useLoginStore } from '@/store';
import { FormInstance } from '@arco-design/web-vue/es/form';
import { BasicInfoModel } from '@/api/user-center';
import { BasicInfoModel } from '@/api/system/user-center';
const loginStore = useLoginStore();
const formRef = ref<FormInstance>();

View File

@@ -4,7 +4,7 @@
<a-list-item-meta>
<template #avatar>
<a-typography-paragraph>
{{ $t('userSetting.SecuritySettings.form.label.password') }}
{{ $t('userCenter.SecuritySettings.form.label.password') }}
</a-typography-paragraph>
</template>
<template #description>
@@ -13,12 +13,12 @@
已设置
</a-typography-paragraph>
<a-typography-paragraph v-else class="tip">
{{ $t('userSetting.SecuritySettings.placeholder.password') }}
{{ $t('userCenter.SecuritySettings.placeholder.password') }}
</a-typography-paragraph>
</div>
<div class="operation">
<a-link>
{{ $t('userSetting.SecuritySettings.button.update') }}
{{ $t('userCenter.SecuritySettings.button.update') }}
</a-link>
</div>
</template>
@@ -28,7 +28,7 @@
<a-list-item-meta>
<template #avatar>
<a-typography-paragraph>
{{ $t('userSetting.SecuritySettings.form.label.phone') }}
{{ $t('userCenter.SecuritySettings.form.label.phone') }}
</a-typography-paragraph>
</template>
<template #description>
@@ -37,12 +37,12 @@
已绑定{{ loginStore.phone }}
</a-typography-paragraph>
<a-typography-paragraph v-else class="tip">
{{ $t('userSetting.SecuritySettings.placeholder.phone') }}
{{ $t('userCenter.SecuritySettings.placeholder.phone') }}
</a-typography-paragraph>
</div>
<div class="operation">
<a-link>
{{ $t('userSetting.SecuritySettings.button.update') }}
{{ $t('userCenter.SecuritySettings.button.update') }}
</a-link>
</div>
</template>
@@ -52,7 +52,7 @@
<a-list-item-meta>
<template #avatar>
<a-typography-paragraph>
{{ $t('userSetting.SecuritySettings.form.label.email') }}
{{ $t('userCenter.SecuritySettings.form.label.email') }}
</a-typography-paragraph>
</template>
<template #description>
@@ -61,12 +61,12 @@
已绑定{{ loginStore.email }}
</a-typography-paragraph>
<a-typography-paragraph v-else class="tip">
{{ $t('userSetting.SecuritySettings.placeholder.email') }}
{{ $t('userCenter.SecuritySettings.placeholder.email') }}
</a-typography-paragraph>
</div>
<div class="operation">
<a-link>
{{ $t('userSetting.SecuritySettings.button.update') }}
{{ $t('userCenter.SecuritySettings.button.update') }}
</a-link>
</div>
</template>

View File

@@ -2,19 +2,19 @@
<a-card :bordered="false">
<a-space :size="54">
<a-upload
:custom-request="customRequest"
:custom-request="handleUpload"
list-type="picture-card"
:file-list="fileList"
:file-list="avatarList"
:show-upload-button="true"
:show-file-list="false"
@change="uploadChange"
@change="changeAvatar"
>
<template #upload-button>
<a-avatar :size="100" class="info-avatar">
<template #trigger-icon>
<icon-camera />
</template>
<img v-if="fileList.length" :src="fileList[0].url" />
<img v-if="avatarList.length" :src="avatarList[0].url" />
</a-avatar>
</template>
</a-upload>
@@ -36,7 +36,7 @@
>
<template #label="{ label }">{{ $t(label) }} :</template>
<template #value="{ value, data }">
<div v-if="data.label === 'userSetting.label.gender'">
<div v-if="data.label === 'userCenter.label.gender'">
<div v-if="loginStore.gender === 1">
<icon-man style="color: #19BBF1" />
@@ -61,74 +61,69 @@
RequestOption,
} from '@arco-design/web-vue/es/upload/interfaces';
import { useLoginStore } from '@/store';
import { userUploadApi } from '@/api/user-center';
import { uploadAvatar } from '@/api/system/user-center';
import type { DescData } from '@arco-design/web-vue/es/descriptions/interface';
import getAvatar from "@/utils/avatar";
import { Message } from "@arco-design/web-vue";
const loginStore = useLoginStore();
const file = {
const avatar = {
uid: '-2',
name: 'avatar.png',
url: loginStore.avatar ?? getAvatar(loginStore.gender),
url: getAvatar(loginStore),
};
const renderData = [
{
label: 'userSetting.label.nickname',
label: 'userCenter.label.nickname',
value: loginStore.nickname,
},
{
label: 'userSetting.label.gender',
label: 'userCenter.label.gender',
value: loginStore.gender,
},
{
label: 'userSetting.label.phone',
label: 'userCenter.label.phone',
value: loginStore.phone,
},
{
label: 'userSetting.label.email',
label: 'userCenter.label.email',
value: loginStore.email,
},
{
label: 'userSetting.label.registrationDate',
label: 'userCenter.label.registrationDate',
value: loginStore.registrationDate,
},
] as DescData[];
const fileList = ref<FileItem[]>([file]);
const uploadChange = (fileItemList: FileItem[], fileItem: FileItem) => {
fileList.value = [fileItem];
};
const customRequest = (options: RequestOption) => {
// docs: https://axios-http.com/docs/cancellation
const controller = new AbortController();
const avatarList = ref<FileItem[]>([avatar]);
//
const changeAvatar = (fileItemList: FileItem[], currentFile: FileItem) => {
avatarList.value = [currentFile];
};
//
const handleUpload = (options: RequestOption) => {
const controller = new AbortController();
(async function requestWrap() {
const {
onProgress,
onError,
onSuccess,
fileItem,
name = 'file',
name = 'avatarFile',
} = options;
onProgress(20);
const formData = new FormData();
formData.append(name as string, fileItem.file as Blob);
const onUploadProgress = (event: ProgressEvent) => {
let percent;
if (event.total > 0) {
percent = (event.loaded / event.total) * 100;
}
onProgress(parseInt(String(percent), 10), event);
};
try {
// https://github.com/axios/axios/issues/1630
// https://github.com/nuysoft/Mock/issues/127
const res = await userUploadApi(formData, {
controller,
onUploadProgress,
});
const res = await uploadAvatar(formData);
onSuccess(res);
Message.success({
content: res.msg || '网络错误',
duration: 3 * 1000,
});
//
loginStore.avatar = res.data.avatar;
} catch (error) {
onError(error);
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="container">
<Breadcrumb :items="['menu.user', 'menu.user.setting']" />
<Breadcrumb :items="['menu.user.center']" />
<a-row style="margin-bottom: 16px">
<a-col :span="24">
<UserPanel />
@@ -9,10 +9,10 @@
<a-row class="wrapper">
<a-col :span="24">
<a-tabs default-active-key="1" type="rounded">
<a-tab-pane key="1" :title="$t('userSetting.tab.basicInformation')">
<a-tab-pane key="1" :title="$t('userCenter.tab.basicInformation')">
<BasicInformation />
</a-tab-pane>
<a-tab-pane key="2" :title="$t('userSetting.tab.securitySettings')">
<a-tab-pane key="2" :title="$t('userCenter.tab.securitySettings')">
<SecuritySettings />
</a-tab-pane>
</a-tabs>
@@ -29,7 +29,7 @@
<script lang="ts">
export default {
name: 'Setting',
name: 'UserCenter',
};
</script>

View File

@@ -0,0 +1,31 @@
export default {
'menu.user.center': 'User Center',
'userCenter.label.nickname': 'Nick Name',
'userCenter.label.gender': 'Gender',
'userCenter.label.phone': 'Phone',
'userCenter.label.email': 'Email',
'userCenter.label.registrationDate': 'Registration Date',
'userCenter.tab.basicInformation': 'Basic Information',
'userCenter.basicInfo.form.label.username': 'Username',
'userCenter.basicInfo.placeholder.username': 'Please enter username',
'userCenter.form.error.username.required': 'Please enter username',
'userCenter.basicInfo.form.label.nickname': 'Nickname',
'userCenter.basicInfo.placeholder.nickname': 'Please enter nickname',
'userCenter.form.error.nickname.required': 'Please enter nickname',
'userCenter.basicInfo.form.label.gender': 'Gender',
'userCenter.save': 'Save',
'userCenter.reset': 'Reset',
'userCenter.tab.securitySettings': 'Security Settings',
'userCenter.SecuritySettings.form.label.password': 'Login Password',
'userCenter.SecuritySettings.placeholder.password':
'You have not set a password yet. The password must contain at least six letters, digits, and special characters except Spaces.',
'userCenter.SecuritySettings.form.label.phone': 'Phone',
'userCenter.SecuritySettings.placeholder.phone':
'You have not set a phone yet. The phone binding can be used to retrieve passwords and receive notifications and SMS login.',
'userCenter.SecuritySettings.form.label.email': 'Email',
'userCenter.SecuritySettings.placeholder.email':
'You have not set a mailbox yet. The mailbox binding can be used to retrieve passwords and receive notifications.',
'userCenter.SecuritySettings.button.update': 'Update',
};

View File

@@ -0,0 +1,31 @@
export default {
'menu.user.center': '个人中心',
'userCenter.label.nickname': '昵称',
'userCenter.label.gender': '性别',
'userCenter.label.phone': '手机号码',
'userCenter.label.email': '邮箱',
'userCenter.label.registrationDate': '注册日期',
'userCenter.tab.basicInformation': '基础信息',
'userCenter.basicInfo.form.label.username': '用户名',
'userCenter.basicInfo.placeholder.username': '请输入您的用户名',
'userCenter.form.error.username.required': '请输入用户名',
'userCenter.basicInfo.form.label.nickname': '昵称',
'userCenter.basicInfo.placeholder.nickname': '请输入您的昵称',
'userCenter.form.error.nickname.required': '请输入昵称',
'userCenter.basicInfo.form.label.gender': '性别',
'userCenter.save': '保存',
'userCenter.reset': '重置',
'userCenter.tab.securitySettings': '安全设置',
'userCenter.SecuritySettings.form.label.password': '登录密码',
'userCenter.SecuritySettings.placeholder.password':
'您暂未设置密码密码至少6位字符支持数字、字母和除空格外的特殊字符。',
'userCenter.SecuritySettings.form.label.phone': '安全手机',
'userCenter.SecuritySettings.placeholder.phone':
'您暂未设置手机号,绑定手机号可以用来找回密码、接收通知、短信登录等。',
'userCenter.SecuritySettings.form.label.email': '安全邮箱',
'userCenter.SecuritySettings.placeholder.email':
'您暂未设置邮箱,绑定邮箱可以用来找回密码、接收通知等。',
'userCenter.SecuritySettings.button.update': '修改',
};

View File

@@ -1,88 +0,0 @@
<template>
<a-card class="general-card" :title="$t('userInfo.title.latestActivity')">
<template #extra>
<a-link>{{ $t('userInfo.viewAll') }}</a-link>
</template>
<a-list :bordered="false">
<a-list-item
v-for="activity in activityList"
:key="activity.id"
action-layout="horizontal"
>
<a-skeleton
v-if="loading"
:loading="loading"
:animation="true"
class="skeleton-item"
>
<a-row :gutter="6">
<a-col :span="2">
<a-skeleton-shape shape="circle" />
</a-col>
<a-col :span="22">
<a-skeleton-line :widths="['40%', '100%']" :rows="2" />
</a-col>
</a-row>
</a-skeleton>
<a-list-item-meta
v-else
:title="activity.title"
:description="activity.description"
>
<template #avatar>
<a-avatar>
<img :src="activity.avatar" />
</a-avatar>
</template>
</a-list-item-meta>
</a-list-item>
</a-list>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { queryLatestActivity, LatestActivity } from '@/api/user-center';
import useLoading from '@/hooks/loading';
const { loading, setLoading } = useLoading(true);
const activityList = ref<LatestActivity[]>(new Array(7).fill({}));
const fetchData = async () => {
try {
const { data } = await queryLatestActivity();
activityList.value = data;
} catch (err) {
// you can report use errorHandler or other
} finally {
setLoading(false);
}
};
fetchData();
</script>
<style scoped lang="less">
.latest-activity {
&-header {
display: flex;
align-items: center;
justify-content: space-between;
}
}
.general-card :deep(.arco-list-item) {
padding-left: 0;
border-bottom: none;
.arco-list-item-meta-content {
flex: 1;
padding-bottom: 27px;
border-bottom: 1px solid var(--color-neutral-3);
}
.arco-list-item-meta-avatar {
padding-bottom: 27px;
}
.skeleton-item {
margin-top: 10px;
padding-bottom: 20px;
border-bottom: 1px solid var(--color-neutral-3);
}
}
</style>

View File

@@ -1,27 +0,0 @@
<template>
<a-card class="general-card" :title="$t('userInfo.title.latestNotification')">
<a-skeleton v-if="loading" :animation="true">
<a-skeleton-line :rows="3" />
</a-skeleton>
<a-result v-else status="404">
<template #subtitle>
{{ $t('userInfo.nodata') }}
</template>
</a-result>
</a-card>
</template>
<script lang="ts" setup>
import useLoading from '@/hooks/loading';
const { loading, setLoading } = useLoading(true);
setTimeout(() => {
setLoading(false);
}, 500);
</script>
<style lang="less" scoped>
:deep(.arco-result) {
padding: 40px 32px 108px;
}
</style>

View File

@@ -1,91 +0,0 @@
<template>
<a-card class="general-card" :title="$t('userInfo.title.myProject')">
<template #extra>
<a-link>{{ $t('userInfo.showMore') }}</a-link>
</template>
<a-row :gutter="16">
<a-col
v-for="(project, index) in projectList"
:key="index"
:xs="12"
:sm="12"
:md="12"
:lg="12"
:xl="8"
:xxl="8"
class="my-project-item"
>
<a-card>
<a-skeleton v-if="loading" :loading="loading" :animation="true">
<a-skeleton-line :rows="3" />
</a-skeleton>
<a-space v-else direction="vertical">
<a-typography-text bold>{{ project.name }}</a-typography-text>
<a-typography-text type="secondary">
{{ project.description }}
</a-typography-text>
<a-space>
<a-avatar-group :size="24">
{{ project.contributors }}
<a-avatar
v-for="(contributor, idx) in project.contributors"
:key="idx"
:size="32"
>
<img alt="avatar" :src="contributor.avatar" />
</a-avatar>
</a-avatar-group>
<a-typography-text type="secondary">
{{ project.peopleNumber }}
</a-typography-text>
</a-space>
</a-space>
</a-card>
</a-col>
</a-row>
</a-card>
</template>
<script lang="ts" setup>
import { queryMyProjectList, MyProjectRecord } from '@/api/user-center';
import useRequest from '@/hooks/request';
const defaultValue = Array(6).fill({} as MyProjectRecord);
const { loading, response: projectList } = useRequest<MyProjectRecord[]>(
queryMyProjectList,
defaultValue
);
</script>
<style scoped lang="less">
:deep(.arco-card-body) {
min-height: 128px;
padding-bottom: 0;
}
.my-project {
&-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
&-title {
margin-top: 0 !important;
margin-bottom: 18px !important;
}
&-list {
display: flex;
justify-content: space-between;
}
&-item {
// padding-right: 16px;
margin-bottom: 16px;
&:last-child {
padding-right: 0;
}
}
}
</style>

View File

@@ -1,64 +0,0 @@
<template>
<a-card
class="general-card"
:title="$t('userInfo.tab.title.team')"
:header-style="{ paddingBottom: '18px' }"
:body-style="{ paddingBottom: '12px' }"
>
<a-list :bordered="false">
<a-list-item
v-for="team in teamList"
:key="team.id"
action-layout="horizontal"
>
<a-skeleton v-if="loading" :loading="loading" :animation="true">
<a-row :gutter="6">
<a-col :span="6">
<a-skeleton-shape shape="circle" />
</a-col>
<a-col :span="16">
<a-skeleton-line :widths="['100%', '40%']" :rows="2" />
</a-col>
</a-row>
</a-skeleton>
<a-list-item-meta v-else :title="team.name">
<template #avatar>
<a-avatar>
<img :src="team.avatar" />
</a-avatar>
</template>
<template #description> {{ team.peopleNumber }} </template>
</a-list-item-meta>
</a-list-item>
</a-list>
</a-card>
</template>
<script lang="ts" setup>
import { queryMyTeamList, MyTeamRecord } from '@/api/user-center';
import useRequest from '@/hooks/request';
const defaultValue: MyTeamRecord[] = new Array(4).fill({});
const { loading, response: teamList } = useRequest<MyTeamRecord[]>(
queryMyTeamList,
defaultValue
);
</script>
<style scoped lang="less">
.general-card {
height: 356px;
.arco-list-item {
height: 72px;
padding-left: 0;
padding-bottom: 12px;
border-bottom: 1px solid var(--color-neutral-3);
&:last-child {
border-bottom: none;
}
.arco-list-item-meta {
padding: 0;
}
}
}
</style>

View File

@@ -1,70 +0,0 @@
<template>
<div class="header">
<a-space :size="12" direction="vertical" align="center">
<a-avatar :size="64">
<template #trigger-icon>
<icon-camera />
</template>
<img :src="userInfo.avatar ?? getAvatar(userInfo.gender)" />
</a-avatar>
<a-typography-title :heading="6" style="margin: 0">
{{ userInfo.nickname }}
</a-typography-title>
<div class="user-msg">
<a-space :size="18">
<div>
<icon-user />
<a-typography-text>{{ userInfo.jobName }}</a-typography-text>
</div>
<div>
<icon-home />
<a-typography-text>
{{ userInfo.organizationName }}
</a-typography-text>
</div>
<div>
<icon-location />
<a-typography-text>{{ userInfo.locationName }}</a-typography-text>
</div>
</a-space>
</div>
</a-space>
</div>
</template>
<script lang="ts" setup>
import { useLoginStore } from '@/store';
import getAvatar from "@/utils/avatar";
const userInfo = useLoginStore();
</script>
<style scoped lang="less">
.header {
display: flex;
align-items: center;
justify-content: center;
height: 204px;
color: var(--gray-10);
background: url(//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/41c6b125cc2e27021bf7fcc9a9b1897c.svg~tplv-49unhts6dw-image.image)
no-repeat;
background-size: cover;
border-radius: 4px;
:deep(.arco-avatar-trigger-icon-button) {
color: rgb(var(--arcoblue-6));
:deep(.arco-icon) {
vertical-align: -1px;
}
}
.user-msg {
.arco-icon {
color: rgb(var(--gray-10));
}
.arco-typography {
margin-left: 6px;
}
}
}
</style>

View File

@@ -1,87 +0,0 @@
<template>
<div class="container">
<Breadcrumb :items="['menu.user', 'menu.user.info']" />
<UserInfoHeader />
<div class="content">
<div class="content-left">
<a-grid :cols="24" :col-gap="16" :row-gap="16">
<a-grid-item :span="24">
<MyProject />
</a-grid-item>
<a-grid-item :span="24">
<LatestActivity />
</a-grid-item>
</a-grid>
</div>
<div class="content-right">
<a-grid :cols="24" :row-gap="16">
<a-grid-item :span="24">
<MyTeam />
</a-grid-item>
<a-grid-item class="panel" :span="24">
<LatestNotification />
</a-grid-item>
</a-grid>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import UserInfoHeader from './components/user-info-header.vue';
import LatestNotification from './components/latest-notification.vue';
import MyProject from './components/my-project.vue';
import LatestActivity from './components/latest-activity.vue';
import MyTeam from './components/my-team.vue';
</script>
<script lang="ts">
export default {
name: 'Info',
};
</script>
<style scoped lang="less">
.container {
padding: 0 20px 20px 20px;
}
.content {
display: flex;
margin-top: 12px;
&-left {
flex: 1;
margin-right: 16px;
overflow: hidden;
// background-color: var(--color-bg-2);
:deep(.arco-tabs-nav-tab) {
margin-left: 16px;
}
}
&-right {
width: 332px;
}
.tab-pane-wrapper {
padding: 0 16px 16px 16px;
}
}
</style>
<style lang="less" scoped>
.mobile {
.content {
display: block;
&-left {
margin-right: 0;
margin-bottom: 16px;
}
&-right {
width: 100%;
}
}
}
</style>

View File

@@ -1,15 +0,0 @@
export default {
'menu.user.info': 'User Info',
'userInfo.editUserInfo': 'Edit Info',
'userInfo.tab.title.overview': 'Overview',
'userInfo.tab.title.project': 'Project',
'userInfo.tab.title.team': 'My Team',
'userInfo.title.latestActivity': 'Latest Activity',
'userInfo.title.latestNotification': 'In-site Notification',
'userInfo.title.myProject': 'My Project',
'userInfo.showMore': 'Show More',
'userInfo.viewAll': 'View All',
'userInfo.nodata': 'No Data',
'userInfo.visits.unit': 'times',
'userInfo.visits.lastMonth': 'Last Month',
};

View File

@@ -1,15 +0,0 @@
export default {
'menu.user.info': '用户信息',
'userInfo.editUserInfo': '编辑信息',
'userInfo.tab.title.overview': '总览',
'userInfo.tab.title.project': '项目',
'userInfo.tab.title.team': '我的团队',
'userInfo.title.latestActivity': '最新动态',
'userInfo.title.latestNotification': '站内通知',
'userInfo.title.myProject': '我的项目',
'userInfo.showMore': '查看更多',
'userInfo.viewAll': '查看全部',
'userInfo.nodata': '暂无数据',
'userInfo.visits.unit': '人次',
'userInfo.visits.lastMonth': '较上月',
};

View File

@@ -1,162 +0,0 @@
import Mock from 'mockjs';
import setupMock, { successResponseWrap } from '@/utils/setup-mock';
setupMock({
setup() {
// 最新项目
Mock.mock(new RegExp('/api/user/my-project/list'), () => {
const contributors = [
{
name: '秦臻宇',
email: 'qingzhenyu@arco.design',
avatar:
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/a8c8cdb109cb051163646151a4a5083b.png~tplv-uwbnlip3yd-webp.webp',
},
{
name: '于涛',
email: 'yuebao@arco.design',
avatar:
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/a8c8cdb109cb051163646151a4a5083b.png~tplv-uwbnlip3yd-webp.webp',
},
{
name: '宁波',
email: 'ningbo@arco.design',
avatar:
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
},
{
name: '郑曦月',
email: 'zhengxiyue@arco.design',
avatar:
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/8361eeb82904210b4f55fab888fe8416.png~tplv-uwbnlip3yd-webp.webp',
},
{
name: '宁波',
email: 'ningbo@arco.design',
avatar:
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
},
];
const units = [
{
name: '企业级产品设计系统',
description: 'Arco Design System',
},
{
name: '火山引擎智能应用',
description: 'The Volcano Engine',
},
{
name: 'OCR文本识别',
description: 'OCR text recognition',
},
{
name: '内容资源管理',
description: 'Content resource management ',
},
{
name: '今日头条内容管理',
description: 'Toutiao content management',
},
{
name: '智能机器人',
description: 'Intelligent Robot Project',
},
];
return successResponseWrap(
new Array(6).fill(null).map((_item, index) => ({
id: index,
name: units[index].name,
description: units[index].description,
peopleNumber: Mock.Random.natural(10, 1000),
contributors,
}))
);
});
// 最新动态
Mock.mock(new RegExp('/api/user/latest-activity'), () => {
return successResponseWrap(
new Array(7).fill(null).map((_item, index) => ({
id: index,
title: '发布了项目 Arco Design System',
description: '企业级产品设计系统',
avatar:
'//lf1-xgcdn-tos.pstatp.com/obj/vcloud/vadmin/start.8e0e4855ee346a46ccff8ff3e24db27b.png',
}))
);
});
// 访问量
Mock.mock(new RegExp('/api/user/visits'), () => {
return successResponseWrap([
{
name: '主页访问量',
visits: 5670,
growth: 206.32,
},
{
name: '项目访问量',
visits: 5670,
growth: 206.32,
},
]);
});
// 项目和团队列表
Mock.mock(new RegExp('/api/user/project-and-team/list'), () => {
return successResponseWrap([
{
id: 1,
content: '他创建的项目',
},
{
id: 2,
content: '他参与的项目',
},
{
id: 3,
content: '他创建的团队',
},
{
id: 4,
content: '他加入的团队',
},
]);
});
// 团队列表
Mock.mock(new RegExp('/api/user/my-team/list'), () => {
return successResponseWrap([
{
id: 1,
avatar:
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/a8c8cdb109cb051163646151a4a5083b.png~tplv-uwbnlip3yd-webp.webp',
name: '火山引擎智能应用团队',
peopleNumber: Mock.Random.natural(10, 100),
},
{
id: 2,
avatar:
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
name: '企业级产品设计团队',
peopleNumber: Mock.Random.natural(5000, 6000),
},
{
id: 3,
avatar:
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
name: '前端/UE小分队',
peopleNumber: Mock.Random.natural(10, 5000),
},
{
id: 4,
avatar:
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/8361eeb82904210b4f55fab888fe8416.png~tplv-uwbnlip3yd-webp.webp',
name: '内容识别插件小分队',
peopleNumber: Mock.Random.natural(10, 100),
},
]);
});
},
});

View File

@@ -1,47 +0,0 @@
export default {
'menu.user.setting': 'User Setting',
'userSetting.menu.title.info': 'Personal Information',
'userSetting.menu.title.account': 'Account Setting',
'userSetting.menu.title.password': 'Password',
'userSetting.menu.title.message': 'Message Notification',
'userSetting.menu.title.result': 'Result',
'userSetting.menu.title.data': 'Export Data',
'userSetting.saveSuccess': 'Save Success',
'userSetting.title.basicInfo': 'Basic Information',
'userSetting.title.socialInfo': 'Social Information',
'userSetting.label.avatar': 'Avatar',
'userSetting.label.nickname': 'Nick Name',
'userSetting.label.location': 'Office Location',
'userSetting.label.introduction': 'Introduction',
'userSetting.label.personalWebsite': 'Website',
'userSetting.save': 'Save',
'userSetting.cancel': 'Cancel',
'userSetting.reset': 'Reset',
// new
'userSetting.label.phone': 'Phone',
'userSetting.label.email': 'Email',
'userSetting.label.gender': 'Gender',
'userSetting.label.registrationDate': 'Registration Date',
'userSetting.tab.basicInformation': 'Basic Information',
'userSetting.tab.securitySettings': 'Security Settings',
'userSetting.tab.certification': 'Certification',
'userSetting.basicInfo.form.label.username': 'Username',
'userSetting.basicInfo.placeholder.username': 'Please enter username',
'userSetting.form.error.username.required': 'Please enter username',
'userSetting.basicInfo.form.label.nickname': 'Nickname',
'userSetting.basicInfo.placeholder.nickname': 'Please enter nickname',
'userSetting.form.error.nickname.required': 'Please enter nickname',
'userSetting.basicInfo.form.label.gender': 'Gender',
'userSetting.form.error.gender.required': 'Please select gender',
'userSetting.SecuritySettings.form.label.password': 'Login Password',
'userSetting.SecuritySettings.placeholder.password':
'You have not set a password yet. The password must contain at least six letters, digits, and special characters except Spaces.',
'userSetting.SecuritySettings.form.label.phone': 'Phone',
'userSetting.SecuritySettings.placeholder.phone':
'You have not set a phone yet. The phone binding can be used to retrieve passwords and receive notifications and SMS login.',
'userSetting.SecuritySettings.form.label.email': 'Email',
'userSetting.SecuritySettings.placeholder.email':
'You have not set a mailbox yet. The mailbox binding can be used to retrieve passwords and receive notifications.',
'userSetting.SecuritySettings.button.settings': 'Settings',
'userSetting.SecuritySettings.button.update': 'Update',
};

View File

@@ -1,47 +0,0 @@
export default {
'menu.user.setting': '用户设置',
'userSetting.menu.title.info': '个人信息',
'userSetting.menu.title.account': '账号设置',
'userSetting.menu.title.password': '密码',
'userSetting.menu.title.message': '消息通知',
'userSetting.menu.title.result': '结果页',
'userSetting.menu.title.data': '导出数据',
'userSetting.saveSuccess': '保存成功',
'userSetting.title.basicInfo': '基本信息',
'userSetting.title.socialInfo': '社交信息',
'userSetting.label.avatar': '头像',
'userSetting.label.nickname': '昵称',
'userSetting.label.location': '办公地点',
'userSetting.label.introduction': '个人简介',
'userSetting.label.personalWebsite': '个人网站',
'userSetting.save': '保存',
'userSetting.cancel': '取消',
'userSetting.reset': '重置',
// new
'userSetting.label.phone': '手机号码',
'userSetting.label.email': '邮箱',
'userSetting.label.gender': '性别',
'userSetting.label.registrationDate': '注册日期',
'userSetting.tab.basicInformation': '基础信息',
'userSetting.tab.securitySettings': '安全设置',
'userSetting.tab.certification': '实名认证',
'userSetting.basicInfo.form.label.username': '用户名',
'userSetting.basicInfo.placeholder.username': '请输入您的用户名',
'userSetting.form.error.username.required': '请输入用户名',
'userSetting.basicInfo.form.label.nickname': '昵称',
'userSetting.basicInfo.placeholder.nickname': '请输入您的昵称',
'userSetting.form.error.nickname.required': '请输入昵称',
'userSetting.basicInfo.form.label.gender': '性别',
'userSetting.form.error.gender.required': '请选择性别',
'userSetting.SecuritySettings.form.label.password': '登录密码',
'userSetting.SecuritySettings.placeholder.password':
'您暂未设置密码密码至少6位字符支持数字、字母和除空格外的特殊字符。',
'userSetting.SecuritySettings.form.label.phone': '安全手机',
'userSetting.SecuritySettings.placeholder.phone':
'您暂未设置手机号,绑定手机号可以用来找回密码、接收通知、短信登录等。',
'userSetting.SecuritySettings.form.label.email': '安全邮箱',
'userSetting.SecuritySettings.placeholder.email':
'您暂未设置邮箱,绑定邮箱可以用来找回密码、接收通知等。',
'userSetting.SecuritySettings.button.settings': '设置',
'userSetting.SecuritySettings.button.update': '修改',
};