feat: 个人中心(未完成)

This commit is contained in:
dap 2024-09-03 16:46:42 +08:00
parent 450a598b30
commit 71f137eda3
12 changed files with 463 additions and 93 deletions

View File

@ -1,3 +1,5 @@
import type { GrantType } from '@vben/common-ui';
import { useAppConfig } from '@vben/hooks';
import { requestClient } from '#/api/request';
@ -5,16 +7,56 @@ import { requestClient } from '#/api/request';
const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);
export namespace AuthApi {
/** 登录接口参数 */
export interface LoginParams {
code?: string;
grantType: string;
password: string;
/**
* @description:
* @param clientId ID loginApi内部处理了
* @param grantType /
* @param tenantId id
*/
export interface BaseLoginParams {
clientId?: string;
grantType: GrantType;
tenantId: string;
username: string;
uuid?: string;
}
/**
* @description: oauth登录需要用到的参数
* @param socialCode
* @param socialState
* @param source justauth.type.xxx的回调地址的source对应
*/
export interface OAuthLoginParams extends BaseLoginParams {
socialCode: string;
socialState: string;
source: string;
}
/**
* @description:
* @param code ()
* @param uuid ID ()
* @param username
* @param password
*/
export interface SimpleLoginParams extends BaseLoginParams {
code?: string;
uuid?: string;
username: string;
password: string;
}
export type LoginParams = OAuthLoginParams | SimpleLoginParams;
// /** 登录接口参数 */
// export interface LoginParams {
// code?: string;
// grantType: string;
// password: string;
// tenantId: string;
// username: string;
// uuid?: string;
// }
/** 登录接口返回值 */
export interface LoginResult {
access_token: string;
@ -76,6 +118,41 @@ export function tenantList() {
return requestClient.get<TenantResp>('/auth/tenant/list');
}
/**
* vben的
* @returns string[]
*/
export async function getAccessCodesApi() {
return requestClient.get<string[]>('/auth/codes');
}
/**
*
* @param source
* @returns url
*/
export function authBinding(source: string, tenantId: string) {
return requestClient.get<string>(`/auth/binding/${source}`, {
params: {
domain: window.location.host,
tenantId,
},
});
}
/**
*
* @param id id
*/
export function authUnbinding(id: string) {
return requestClient.deleteWithMsg<void>(`/auth/unlock/${id}`);
}
/**
* oauth授权回调
* @param data oauth授权
* @returns void
*/
export function authCallback(data: AuthApi.OAuthLoginParams) {
return requestClient.post<void>('/auth/social/callback', data);
}

View File

@ -0,0 +1,20 @@
import type { SocialInfo } from './model';
import { requestClient } from '#/api/request';
enum Api {
root = '/system/social',
socialList = '/system/social/list',
}
/**
*
* @returns info
*/
export function socialList() {
return requestClient.get<SocialInfo[]>(Api.socialList);
}
export function socialInfo(id: number | string) {
return requestClient.get(`${Api.root}/${id}`);
}

View File

@ -0,0 +1,26 @@
export interface SocialInfo {
id: string;
userId: number;
tenantId: string;
authId: string;
source: string;
accessToken: string;
expireIn: number;
refreshToken: string;
openId: string;
userName: string;
nickName: string;
email: string;
avatar: string;
accessCode?: any;
unionId?: any;
scope: string;
tokenType: string;
idToken?: any;
macAlgorithm?: any;
macKey?: any;
code?: any;
oauthToken?: any;
oauthTokenSecret?: any;
createTime: string;
}

View File

@ -0,0 +1,79 @@
<script setup lang="tsx">
import type { ColumnsType } from 'ant-design-vue/es/table';
import type { SocialInfo } from '#/api/system/social/model';
import { onMounted, ref } from 'vue';
import { Avatar, Modal, Table } from 'ant-design-vue';
import { authUnbinding } from '#/api';
import { socialList } from '#/api/system/social';
const columns: ColumnsType = [
{
align: 'center',
dataIndex: 'source',
title: '绑定平台',
},
{
align: 'center',
customRender: ({ value }) => {
return <Avatar src={value} />;
},
dataIndex: 'avatar',
title: '头像',
},
{
align: 'center',
dataIndex: 'userName',
title: '账号',
},
{
align: 'center',
dataIndex: 'action',
title: '操作',
},
];
const tableData = ref<SocialInfo[]>([]);
async function reload() {
tableData.value = await socialList();
}
onMounted(reload);
/**
* 解绑账号
*/
function handleUnbind(record: Record<string, any>) {
Modal.confirm({
content: `确定解绑[${record.source}]平台的[${record.userName}]账号吗?`,
async onOk() {
await authUnbinding(record.id);
await reload();
},
title: '提示',
type: 'warning',
});
}
</script>
<template>
<div>
<Table
:columns="columns"
:data-source="tableData"
:pagination="false"
size="middle"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button type="link" @click="handleUnbind(record)">解绑</a-button>
</template>
</template>
</Table>
<div>todo: 绑定功能</div>
</div>
</template>

View File

@ -0,0 +1,104 @@
<script setup lang="ts">
import type { UserProfile } from '#/api/system/profile/model';
import { ref } from 'vue';
import { useUserStore } from '@vben/stores';
import { Form, FormItem, Input, RadioGroup } from 'ant-design-vue';
import { userProfileUpdate } from '#/api/system/profile';
import { useAuthStore } from '#/store';
import { getDictOptions } from '#/utils/dict';
const props = defineProps<{ profile: UserProfile }>();
const emit = defineEmits<{ reload: [] }>();
/**
* 要重构
*/
const form = ref({
email: props.profile.user.email,
nickName: props.profile.user.nickName,
phonenumber: props.profile.user.phonenumber,
sex: props.profile.user.sex,
userId: props.profile.user.userId,
});
const sexOptions = getDictOptions('sys_user_sex');
const userStore = useUserStore();
const authStore = useAuthStore();
const loading = ref(false);
async function handleSubmit() {
try {
loading.value = true;
await userProfileUpdate(form.value);
// store
const userInfo = await authStore.fetchUserInfo();
userStore.setUserInfo(userInfo);
// reload
emit('reload');
} catch (error) {
console.error(error);
} finally {
loading.value = false;
}
}
</script>
<template>
<div class="mt-[16px] md:w-full lg:w-1/2 xl:w-1/3">
<Form :label-col="{ span: 4 }" :model="form" @finish="handleSubmit">
<FormItem
:rules="[{ required: true, message: '请输入昵称' }]"
label="昵称"
name="nickName"
>
<Input v-model:value="form.nickName" placeholder="请输入" />
</FormItem>
<FormItem
:rules="[
{
required: true,
message: '请输入正确的邮箱',
pattern:
/^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/,
},
]"
label="邮箱"
name="email"
>
<Input v-model:value="form.email" placeholder="请输入" />
</FormItem>
<FormItem label="性别" name="sex">
<RadioGroup
v-model:value="form.sex"
:options="sexOptions"
button-style="solid"
option-type="button"
/>
</FormItem>
<FormItem
:rules="[
{
required: true,
message: '请输入正确的电话',
pattern: /^1[3-9]\d{9}$/,
},
]"
label="电话"
name="phonenumber"
>
<Input v-model:value="form.phonenumber" placeholder="请输入" />
</FormItem>
<FormItem :wrapper-col="{ span: 4, offset: 4 }">
<a-button :loading="loading" html-type="submit" type="primary">
更新信息
</a-button>
</FormItem>
</Form>
</div>
</template>

View File

@ -0,0 +1,8 @@
<script setup lang="ts"></script>
<template>
<div>
在线设备
<div>先不做</div>
</div>
</template>

View File

@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<div>安全设置</div>
</template>

View File

@ -4,101 +4,28 @@ import type { UserProfile } from '#/api/system/profile/model';
import { onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import {
Avatar,
Card,
Descriptions,
DescriptionsItem,
Tag,
} from 'ant-design-vue';
import { userProfile } from '#/api/system/profile';
const currentActiveKey = ref('tab1');
import ProfilePanel from './profile-panel.vue';
import SettingPanel from './setting-panel.vue';
const tabList = [
{
key: 'tab1',
tab: '基本设置',
},
{
key: 'tab2',
tab: '安全设置',
},
{
key: 'tab3',
tab: '账号绑定',
},
];
const userStore = useUserStore();
const profile = ref<UserProfile>();
onMounted(async () => {
console.log(userStore.userInfo);
profile.value = await userProfile();
});
async function loadProfile() {
const resp = await userProfile();
profile.value = resp;
}
onMounted(loadProfile);
</script>
<template>
<Page>
<div class="flex flex-col gap-[16px] lg:flex-row">
<Card :loading="!profile" class="h-full lg:w-1/3">
<div v-if="profile" class="flex flex-col items-center gap-[24px]">
<div class="flex flex-col items-center gap-[20px]">
<Avatar :size="96" :src="profile.user.avatar" />
<div class="flex flex-col items-center gap-[8px]">
<span class="text-foreground text-xl font-bold">
{{ profile.user.nickName ?? '未知' }}
</span>
<span> 海纳百川有容乃大 </span>
</div>
</div>
<div class="px-[24px]">
<Descriptions :column="1">
<DescriptionsItem label="账号">
{{ profile.user.userName }}
</DescriptionsItem>
<DescriptionsItem label="手机号码">
{{ profile.user.phonenumber ?? '未绑定手机号' }}
</DescriptionsItem>
<DescriptionsItem label="邮箱">
{{ profile.user.email ?? '未绑定邮箱' }}
</DescriptionsItem>
<DescriptionsItem label="部门">
<Tag color="processing">
{{ profile.user.deptName ?? '未分配部门' }}
</Tag>
<Tag v-if="profile.postGroup" color="processing">
{{ profile.postGroup }}
</Tag>
</DescriptionsItem>
<DescriptionsItem label="上次登录">
{{ profile.user.loginDate }}
</DescriptionsItem>
</Descriptions>
</div>
</div>
</Card>
<Card
:active-tab-key="currentActiveKey"
:tab-list="tabList"
class="lg:flex-1"
@tab-change="
(key) => {
currentActiveKey = key;
}
"
>
<div
class="flex h-[550px] items-center justify-center rounded-xl bg-[hsl(var(--primary))]"
>
<span class="text-lg font-bold text-white dark:text-black">
基本设置
</span>
</div>
</Card>
<!-- 左侧 -->
<ProfilePanel :profile="profile" />
<!-- 右侧 -->
<SettingPanel v-if="profile" :profile="profile" @reload="loadProfile" />
</div>
</Page>
</template>

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import type { UserProfile } from '#/api/system/profile/model';
import {
Avatar,
Card,
Descriptions,
DescriptionsItem,
Tag,
} from 'ant-design-vue';
defineProps<{ profile?: UserProfile }>();
</script>
<template>
<Card :loading="!profile" class="h-full lg:w-1/3">
<div v-if="profile" class="flex flex-col items-center gap-[24px]">
<div class="flex flex-col items-center gap-[20px]">
<Avatar :size="96" :src="profile.user.avatar" />
<div class="flex flex-col items-center gap-[8px]">
<span class="text-foreground text-xl font-bold">
{{ profile.user.nickName ?? '未知' }}
</span>
<span> 海纳百川有容乃大 </span>
</div>
</div>
<div class="px-[24px]">
<Descriptions :column="1">
<DescriptionsItem label="账号">
{{ profile.user.userName }}
</DescriptionsItem>
<DescriptionsItem label="手机号码">
{{ profile.user.phonenumber ?? '未绑定手机号' }}
</DescriptionsItem>
<DescriptionsItem label="邮箱">
{{ profile.user.email ?? '未绑定邮箱' }}
</DescriptionsItem>
<DescriptionsItem label="部门">
<Tag color="processing">
{{ profile.user.deptName ?? '未分配部门' }}
</Tag>
<Tag v-if="profile.postGroup" color="processing">
{{ profile.postGroup }}
</Tag>
</DescriptionsItem>
<DescriptionsItem label="上次登录">
{{ profile.user.loginDate }}
</DescriptionsItem>
</Descriptions>
</div>
</div>
</Card>
</template>

View File

@ -0,0 +1,59 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { TabPane, Tabs } from 'ant-design-vue';
import AccountBind from './components/account-bind.vue';
import BaseSetting from './components/base-setting.vue';
import OnlineDevice from './components/online-device.vue';
import SecureSetting from './components/secure-setting.vue';
export default defineComponent({
components: {
AccountBind,
BaseSetting,
OnlineDevice,
SecureSetting,
TabPane,
Tabs,
},
setup() {
const settingList = [
{
component: 'BaseSetting',
key: '1',
name: '基本设置',
},
{
component: 'SecureSetting',
key: '2',
name: '安全设置',
},
{
component: 'AccountBind',
key: '3',
name: '账号绑定',
},
{
component: 'OnlineDevice',
key: '4',
name: '在线设备',
},
];
return {
settingList,
};
},
});
</script>
<template>
<Tabs class="bg-background rounded-[var(--radius)] px-[16px] lg:flex-1">
<template v-for="item in settingList" :key="item.key">
<TabPane :tab="item.name">
<component :is="item.component" v-bind="$attrs" />
</TabPane>
</template>
</Tabs>
</template>

View File

@ -6,6 +6,7 @@ export { default as AuthenticationQrCodeLogin } from './qrcode-login.vue';
export { default as AuthenticationRegister } from './register.vue';
export type {
AuthenticationProps,
GrantType,
LoginAndRegisterParams,
LoginCodeParams,
} from './types';

View File

@ -74,9 +74,19 @@ interface AuthenticationProps {
usernamePlaceholder?: string;
}
/**
*
* password
* sms
* social oauth
* email
* xcx
*/
type GrantType = 'email' | 'password' | 'sms' | 'social' | 'xcx';
interface LoginAndRegisterParams {
code?: string;
grantType: string;
grantType: GrantType;
password: string;
tenantId: string;
username: string;
@ -102,6 +112,7 @@ interface RegisterEmits {
export type {
AuthenticationProps,
GrantType,
LoginAndRegisterParams,
LoginCodeEmits,
LoginCodeParams,