feat: 个人中心(未完成)
This commit is contained in:
parent
450a598b30
commit
71f137eda3
@ -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);
|
||||
}
|
||||
|
20
apps/web-antd/src/api/system/social/index.ts
Normal file
20
apps/web-antd/src/api/system/social/index.ts
Normal 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}`);
|
||||
}
|
26
apps/web-antd/src/api/system/social/model.d.ts
vendored
Normal file
26
apps/web-antd/src/api/system/social/model.d.ts
vendored
Normal 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;
|
||||
}
|
@ -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>
|
@ -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>
|
@ -0,0 +1,8 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
在线设备
|
||||
<div>先不做</div>
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,5 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div>安全设置</div>
|
||||
</template>
|
@ -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>
|
||||
|
53
apps/web-antd/src/views/_core/profile/profile-panel.vue
Normal file
53
apps/web-antd/src/views/_core/profile/profile-panel.vue
Normal 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>
|
59
apps/web-antd/src/views/_core/profile/setting-panel.vue
Normal file
59
apps/web-antd/src/views/_core/profile/setting-panel.vue
Normal 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>
|
@ -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';
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user