chore: naive init
This commit is contained in:
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Demos",
|
|
||||||
"antd": "Ant Design Vue",
|
|
||||||
"vben": {
|
|
||||||
"title": "Project",
|
|
||||||
"about": "About",
|
|
||||||
"document": "Document",
|
|
||||||
"antdv": "Ant Design Vue Version",
|
|
||||||
"naive-ui": "Naive UI Version",
|
|
||||||
"element-plus": "Element Plus Version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "演示",
|
|
||||||
"antd": "Ant Design Vue",
|
|
||||||
"vben": {
|
|
||||||
"title": "项目",
|
|
||||||
"about": "关于",
|
|
||||||
"document": "文档",
|
|
||||||
"antdv": "Ant Design Vue 版本",
|
|
||||||
"naive-ui": "Naive UI 版本",
|
|
||||||
"element-plus": "Element Plus 版本"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,9 @@ import type { ComponentType } from './component';
|
|||||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
/** 手机号正则表达式(中国) */
|
||||||
|
const MOBILE_REGEX = /(?:0|86|\+86)?1[3-9]\d{9}/;
|
||||||
|
|
||||||
setupVbenForm<ComponentType>({
|
setupVbenForm<ComponentType>({
|
||||||
config: {
|
config: {
|
||||||
// naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
|
// naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
|
||||||
@@ -32,6 +35,25 @@ setupVbenForm<ComponentType>({
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
// 手机号非必填
|
||||||
|
mobile: (value, _params, ctx) => {
|
||||||
|
if (value === undefined || value === null || value.length === 0) {
|
||||||
|
return true;
|
||||||
|
} else if (!MOBILE_REGEX.test(value)) {
|
||||||
|
return $t('ui.formRules.phone', [ctx.label]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
// 手机号必填
|
||||||
|
mobileRequired: (value, _params, ctx) => {
|
||||||
|
if (value === undefined || value === null || value.length === 0) {
|
||||||
|
return $t('ui.formRules.required', [ctx.label]);
|
||||||
|
}
|
||||||
|
if (!MOBILE_REGEX.test(value)) {
|
||||||
|
return $t('ui.formRules.phone', [ctx.label]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { $te } from '@vben/locales';
|
||||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||||
|
import { isFunction, isString } from '@vben/utils';
|
||||||
|
|
||||||
import { NButton, NImage } from 'naive-ui';
|
import { NButton, NImage, NPopconfirm, NSwitch } from 'naive-ui';
|
||||||
|
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useVbenForm } from './form';
|
import { useVbenForm } from './form';
|
||||||
|
|
||||||
@@ -20,16 +28,32 @@ setupVbenVxeTable({
|
|||||||
// 全局禁用vxe-table的表单配置,使用formOptions
|
// 全局禁用vxe-table的表单配置,使用formOptions
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
import: false, // 是否导入
|
||||||
|
export: false, // 是否导出
|
||||||
|
refresh: true, // 是否刷新
|
||||||
|
print: false, // 是否打印
|
||||||
|
zoom: true, // 是否缩放
|
||||||
|
custom: true, // 是否自定义配置
|
||||||
|
},
|
||||||
|
customConfig: {
|
||||||
|
mode: 'modal',
|
||||||
|
},
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
autoLoad: true,
|
autoLoad: true,
|
||||||
response: {
|
response: {
|
||||||
result: 'items',
|
result: 'list',
|
||||||
total: 'total',
|
total: 'total',
|
||||||
list: 'items',
|
|
||||||
},
|
},
|
||||||
showActiveMsg: true,
|
showActiveMsg: true,
|
||||||
showResponseMsg: false,
|
showResponseMsg: false,
|
||||||
},
|
},
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
sortConfig: {
|
||||||
|
multiple: true,
|
||||||
|
},
|
||||||
round: true,
|
round: true,
|
||||||
showOverflow: true,
|
showOverflow: true,
|
||||||
size: 'small',
|
size: 'small',
|
||||||
@@ -56,12 +80,208 @@ setupVbenVxeTable({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 表格配置项可以用 cellRender: { name: 'CellDict', props:{dictType: ''} },
|
||||||
|
vxeUI.renderer.add('CellDict', {
|
||||||
|
renderTableDefault(renderOpts, params) {
|
||||||
|
const { props } = renderOpts;
|
||||||
|
const { column, row } = params;
|
||||||
|
if (!props) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
// 使用 DictTag 组件替代原来的实现
|
||||||
|
return h(DictTag, {
|
||||||
|
type: props.type,
|
||||||
|
value: row[column.field]?.toString(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表格配置项可以用 cellRender: { name: 'CellSwitch', props: { beforeChange: () => {} } },
|
||||||
|
// add by 芋艿:from https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/adapter/vxe-table.ts#L97-L123
|
||||||
|
vxeUI.renderer.add('CellSwitch', {
|
||||||
|
renderTableDefault({ attrs, props }, { column, row }) {
|
||||||
|
const loadingKey = `__loading_${column.field}`;
|
||||||
|
const finallyProps = {
|
||||||
|
checkedChildren: $t('common.enabled'),
|
||||||
|
checkedValue: 1,
|
||||||
|
unCheckedChildren: $t('common.disabled'),
|
||||||
|
unCheckedValue: 0,
|
||||||
|
...props,
|
||||||
|
checked: row[column.field],
|
||||||
|
loading: row[loadingKey] ?? false,
|
||||||
|
'onUpdate:checked': onChange,
|
||||||
|
};
|
||||||
|
async function onChange(newVal: any) {
|
||||||
|
row[loadingKey] = true;
|
||||||
|
try {
|
||||||
|
const result = await attrs?.beforeChange?.(newVal, row);
|
||||||
|
if (result !== false) {
|
||||||
|
row[column.field] = newVal;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
row[loadingKey] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return h(NSwitch, finallyProps);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册表格的操作按钮渲染器 cellRender: { name: 'CellOperation', options: ['edit', 'delete'] }
|
||||||
|
// add by 芋艿:from https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/adapter/vxe-table.ts#L125-L255
|
||||||
|
vxeUI.renderer.add('CellOperation', {
|
||||||
|
renderTableDefault({ attrs, options, props }, { column, row }) {
|
||||||
|
const defaultProps = { size: 'small', type: 'text', ...props };
|
||||||
|
let align = 'end';
|
||||||
|
switch (column.align) {
|
||||||
|
case 'center': {
|
||||||
|
align = 'center';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'left': {
|
||||||
|
align = 'start';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
align = 'end';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const presets: Recordable<Recordable<any>> = {
|
||||||
|
delete: {
|
||||||
|
danger: true,
|
||||||
|
text: $t('common.delete'),
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
text: $t('common.edit'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const operations: Array<Recordable<any>> = (
|
||||||
|
options || ['edit', 'delete']
|
||||||
|
)
|
||||||
|
.map((opt) => {
|
||||||
|
if (isString(opt)) {
|
||||||
|
return presets[opt]
|
||||||
|
? { code: opt, ...presets[opt], ...defaultProps }
|
||||||
|
: {
|
||||||
|
code: opt,
|
||||||
|
text: $te(`common.${opt}`) ? $t(`common.${opt}`) : opt,
|
||||||
|
...defaultProps,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return { ...defaultProps, ...presets[opt.code], ...opt };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map((opt) => {
|
||||||
|
const optBtn: Recordable<any> = {};
|
||||||
|
Object.keys(opt).forEach((key) => {
|
||||||
|
optBtn[key] = isFunction(opt[key]) ? opt[key](row) : opt[key];
|
||||||
|
});
|
||||||
|
return optBtn;
|
||||||
|
})
|
||||||
|
.filter((opt) => opt.show !== false);
|
||||||
|
|
||||||
|
function renderBtn(opt: Recordable<any>, listen = true) {
|
||||||
|
return h(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
...props,
|
||||||
|
...opt,
|
||||||
|
icon: undefined,
|
||||||
|
onClick: listen
|
||||||
|
? () =>
|
||||||
|
attrs?.onClick?.({
|
||||||
|
code: opt.code,
|
||||||
|
row,
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => {
|
||||||
|
const content = [];
|
||||||
|
if (opt.icon) {
|
||||||
|
content.push(
|
||||||
|
h(IconifyIcon, { class: 'size-5', icon: opt.icon }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
content.push(opt.text);
|
||||||
|
return content;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderConfirm(opt: Recordable<any>) {
|
||||||
|
return h(
|
||||||
|
NPopconfirm,
|
||||||
|
{
|
||||||
|
title: $t('ui.actionTitle.delete', [attrs?.nameTitle || '']),
|
||||||
|
...props,
|
||||||
|
...opt,
|
||||||
|
icon: undefined,
|
||||||
|
onPositiveClick: () => {
|
||||||
|
attrs?.onClick?.({
|
||||||
|
code: opt.code,
|
||||||
|
row,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => renderBtn({ ...opt }, false),
|
||||||
|
description: () =>
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ class: 'truncate' },
|
||||||
|
$t('ui.actionMessage.deleteConfirm', [
|
||||||
|
row[attrs?.nameField || 'name'],
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const btns = operations.map((opt) =>
|
||||||
|
opt.code === 'delete' ? renderConfirm(opt) : renderBtn(opt),
|
||||||
|
);
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: 'flex table-operations',
|
||||||
|
style: { justifyContent: align },
|
||||||
|
},
|
||||||
|
btns,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
||||||
// vxeUI.formats.add
|
// vxeUI.formats.add
|
||||||
|
// add by 星语:数量格式化,例如说:金额
|
||||||
|
vxeUI.formats.add('formatAmount', {
|
||||||
|
cellFormatMethod({ cellValue }, digits = 2) {
|
||||||
|
if (cellValue === null || cellValue === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (isString(cellValue)) {
|
||||||
|
cellValue = Number.parseFloat(cellValue);
|
||||||
|
}
|
||||||
|
// 如果非 number,则直接返回空串
|
||||||
|
if (Number.isNaN(cellValue)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return cellValue.toFixed(digits);
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
useVbenForm,
|
useVbenForm,
|
||||||
});
|
});
|
||||||
|
|
||||||
export { useVbenVxeGrid };
|
export { useVbenVxeGrid };
|
||||||
|
// add by 芋艿:from https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/adapter/vxe-table.ts#L264-L270
|
||||||
|
export type OnActionClickParams<T = Recordable<any>> = {
|
||||||
|
code: string;
|
||||||
|
row: T;
|
||||||
|
};
|
||||||
|
export type OnActionClickFn<T = Recordable<any>> = (
|
||||||
|
params: OnActionClickParams<T>,
|
||||||
|
) => void;
|
||||||
export type * from '@vben/plugins/vxe-table';
|
export type * from '@vben/plugins/vxe-table';
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { AuthPermissionInfo } from '@vben/types';
|
||||||
|
|
||||||
import { baseRequestClient, requestClient } from '#/api/request';
|
import { baseRequestClient, requestClient } from '#/api/request';
|
||||||
|
|
||||||
export namespace AuthApi {
|
export namespace AuthApi {
|
||||||
@@ -5,47 +7,151 @@ export namespace AuthApi {
|
|||||||
export interface LoginParams {
|
export interface LoginParams {
|
||||||
password?: string;
|
password?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
captchaVerification?: string;
|
||||||
|
// 绑定社交登录时,需要传递如下参数
|
||||||
|
socialType?: number;
|
||||||
|
socialCode?: string;
|
||||||
|
socialState?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 登录接口返回值 */
|
/** 登录接口返回值 */
|
||||||
export interface LoginResult {
|
export interface LoginResult {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
userId: number;
|
||||||
|
expiresTime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RefreshTokenResult {
|
/** 租户信息返回值 */
|
||||||
data: string;
|
export interface TenantResult {
|
||||||
status: number;
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 手机验证码获取接口参数 */
|
||||||
|
export interface SmsCodeParams {
|
||||||
|
mobile: string;
|
||||||
|
scene: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 手机验证码登录接口参数 */
|
||||||
|
export interface SmsLoginParams {
|
||||||
|
mobile: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册接口参数 */
|
||||||
|
export interface RegisterParams {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
captchaVerification: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置密码接口参数 */
|
||||||
|
export interface ResetPasswordParams {
|
||||||
|
password: string;
|
||||||
|
mobile: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 社交快捷登录接口参数 */
|
||||||
|
export interface SocialLoginParams {
|
||||||
|
type: number;
|
||||||
|
code: string;
|
||||||
|
state: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 登录 */
|
||||||
* 登录
|
|
||||||
*/
|
|
||||||
export async function loginApi(data: AuthApi.LoginParams) {
|
export async function loginApi(data: AuthApi.LoginParams) {
|
||||||
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
|
return requestClient.post<AuthApi.LoginResult>('/system/auth/login', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 刷新 accessToken */
|
||||||
* 刷新accessToken
|
export async function refreshTokenApi(refreshToken: string) {
|
||||||
*/
|
return baseRequestClient.post(
|
||||||
export async function refreshTokenApi() {
|
`/system/auth/refresh-token?refreshToken=${refreshToken}`,
|
||||||
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
|
);
|
||||||
withCredentials: true,
|
}
|
||||||
|
|
||||||
|
/** 退出登录 */
|
||||||
|
export async function logoutApi(accessToken: string) {
|
||||||
|
return baseRequestClient.post(
|
||||||
|
'/system/auth/logout',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取权限信息 */
|
||||||
|
export async function getAuthPermissionInfoApi() {
|
||||||
|
return requestClient.get<AuthPermissionInfo>(
|
||||||
|
'/system/auth/get-permission-info',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取租户列表 */
|
||||||
|
export async function getTenantSimpleList() {
|
||||||
|
return requestClient.get<AuthApi.TenantResult[]>(
|
||||||
|
`/system/tenant/simple-list`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 使用租户域名,获得租户信息 */
|
||||||
|
export async function getTenantByWebsite(website: string) {
|
||||||
|
return requestClient.get<AuthApi.TenantResult>(
|
||||||
|
`/system/tenant/get-by-website?website=${website}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取验证码 */
|
||||||
|
export async function getCaptcha(data: any) {
|
||||||
|
return baseRequestClient.post('/system/captcha/get', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验验证码 */
|
||||||
|
export async function checkCaptcha(data: any) {
|
||||||
|
return baseRequestClient.post('/system/captcha/check', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取登录验证码 */
|
||||||
|
export async function sendSmsCode(data: AuthApi.SmsCodeParams) {
|
||||||
|
return requestClient.post('/system/auth/send-sms-code', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 短信验证码登录 */
|
||||||
|
export async function smsLogin(data: AuthApi.SmsLoginParams) {
|
||||||
|
return requestClient.post('/system/auth/sms-login', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册 */
|
||||||
|
export async function register(data: AuthApi.RegisterParams) {
|
||||||
|
return requestClient.post('/system/auth/register', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通过短信重置密码 */
|
||||||
|
export async function smsResetPassword(data: AuthApi.ResetPasswordParams) {
|
||||||
|
return requestClient.post('/system/auth/reset-password', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 社交授权的跳转 */
|
||||||
|
export async function socialAuthRedirect(type: number, redirectUri: string) {
|
||||||
|
return requestClient.get('/system/auth/social-auth-redirect', {
|
||||||
|
params: {
|
||||||
|
type,
|
||||||
|
redirectUri,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 社交快捷登录 */
|
||||||
* 退出登录
|
export async function socialLogin(data: AuthApi.SocialLoginParams) {
|
||||||
*/
|
return requestClient.post<AuthApi.LoginResult>(
|
||||||
export async function logoutApi() {
|
'/system/auth/social-login',
|
||||||
return baseRequestClient.post('/auth/logout', {
|
data,
|
||||||
withCredentials: true,
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户权限码
|
|
||||||
*/
|
|
||||||
export async function getAccessCodesApi() {
|
|
||||||
return requestClient.get<string[]>('/auth/codes');
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
export * from './auth';
|
export * from './auth';
|
||||||
export * from './menu';
|
|
||||||
export * from './user';
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import type { RouteRecordStringComponent } from '@vben/types';
|
|
||||||
|
|
||||||
import { requestClient } from '#/api/request';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户所有菜单
|
|
||||||
*/
|
|
||||||
export async function getAllMenusApi() {
|
|
||||||
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import type { UserInfo } from '@vben/types';
|
|
||||||
|
|
||||||
import { requestClient } from '#/api/request';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户信息
|
|
||||||
*/
|
|
||||||
export async function getUserInfoApi() {
|
|
||||||
return requestClient.get<UserInfo>('/user/info');
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import type { RequestClientOptions } from '@vben/request';
|
import type { RequestClientOptions } from '@vben/request';
|
||||||
|
|
||||||
import { useAppConfig } from '@vben/hooks';
|
import { isTenantEnable, useAppConfig } from '@vben/hooks';
|
||||||
import { preferences } from '@vben/preferences';
|
import { preferences } from '@vben/preferences';
|
||||||
import {
|
import {
|
||||||
authenticateResponseInterceptor,
|
authenticateResponseInterceptor,
|
||||||
@@ -19,6 +19,7 @@ import { useAuthStore } from '#/store';
|
|||||||
import { refreshTokenApi } from './core';
|
import { refreshTokenApi } from './core';
|
||||||
|
|
||||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||||
|
const tenantEnable = isTenantEnable();
|
||||||
|
|
||||||
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||||
const client = new RequestClient({
|
const client = new RequestClient({
|
||||||
@@ -49,8 +50,16 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
|||||||
*/
|
*/
|
||||||
async function doRefreshToken() {
|
async function doRefreshToken() {
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const resp = await refreshTokenApi();
|
const refreshToken = accessStore.refreshToken as string;
|
||||||
const newToken = resp.data;
|
if (!refreshToken) {
|
||||||
|
throw new Error('Refresh token is null!');
|
||||||
|
}
|
||||||
|
const resp = await refreshTokenApi(refreshToken);
|
||||||
|
const newToken = resp?.data?.data?.accessToken;
|
||||||
|
// add by 芋艿:这里一定要抛出 resp.data,从而触发 authenticateResponseInterceptor 中,刷新令牌失败!!!
|
||||||
|
if (!newToken) {
|
||||||
|
throw resp.data;
|
||||||
|
}
|
||||||
accessStore.setAccessToken(newToken);
|
accessStore.setAccessToken(newToken);
|
||||||
return newToken;
|
return newToken;
|
||||||
}
|
}
|
||||||
@@ -66,6 +75,14 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
|||||||
|
|
||||||
config.headers.Authorization = formatToken(accessStore.accessToken);
|
config.headers.Authorization = formatToken(accessStore.accessToken);
|
||||||
config.headers['Accept-Language'] = preferences.app.locale;
|
config.headers['Accept-Language'] = preferences.app.locale;
|
||||||
|
// 添加租户编号
|
||||||
|
config.headers['tenant-id'] = tenantEnable
|
||||||
|
? accessStore.tenantId
|
||||||
|
: undefined;
|
||||||
|
// 只有登录时,才设置 visit-tenant-id 访问租户
|
||||||
|
config.headers['visit-tenant-id'] = tenantEnable
|
||||||
|
? accessStore.visitTenantId
|
||||||
|
: undefined;
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -96,7 +113,12 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
|||||||
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
|
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
|
||||||
// 当前mock接口返回的错误字段是 error 或者 message
|
// 当前mock接口返回的错误字段是 error 或者 message
|
||||||
const responseData = error?.response?.data ?? {};
|
const responseData = error?.response?.data ?? {};
|
||||||
const errorMessage = responseData?.error ?? responseData?.message ?? '';
|
const errorMessage =
|
||||||
|
responseData?.error ?? responseData?.message ?? responseData.msg ?? '';
|
||||||
|
// add by 芋艿:特殊:避免 401 “账号未登录”,重复提示。因为,此时会跳转到登录界面,只需提示一次!!!
|
||||||
|
if (error?.data?.code === 401) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 如果没有错误信息,则会根据状态码进行提示
|
// 如果没有错误信息,则会根据状态码进行提示
|
||||||
message.error(errorMessage || msg);
|
message.error(errorMessage || msg);
|
||||||
}),
|
}),
|
||||||
@@ -110,3 +132,17 @@ export const requestClient = createRequestClient(apiURL, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
||||||
|
baseRequestClient.addRequestInterceptor({
|
||||||
|
fulfilled: (config) => {
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
// 添加租户编号
|
||||||
|
config.headers['tenant-id'] = tenantEnable
|
||||||
|
? accessStore.tenantId
|
||||||
|
: undefined;
|
||||||
|
// 只有登录时,才设置 visit-tenant-id 访问租户
|
||||||
|
config.headers['visit-tenant-id'] = tenantEnable
|
||||||
|
? accessStore.visitTenantId
|
||||||
|
: undefined;
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { NotificationItem } from '@vben/layouts';
|
import type { NotificationItem } from '@vben/layouts';
|
||||||
|
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
import { AuthenticationLoginExpiredModal, useVbenModal } from '@vben/common-ui';
|
||||||
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||||
import { useWatermark } from '@vben/hooks';
|
import { useWatermark } from '@vben/hooks';
|
||||||
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
|
import {
|
||||||
|
AntdProfileOutlined,
|
||||||
|
BookOpenText,
|
||||||
|
CircleHelp,
|
||||||
|
MdiGithub,
|
||||||
|
} from '@vben/icons';
|
||||||
import {
|
import {
|
||||||
BasicLayout,
|
BasicLayout,
|
||||||
LockScreen,
|
LockScreen,
|
||||||
@@ -15,52 +20,43 @@ import {
|
|||||||
} from '@vben/layouts';
|
} from '@vben/layouts';
|
||||||
import { preferences } from '@vben/preferences';
|
import { preferences } from '@vben/preferences';
|
||||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
import { openWindow } from '@vben/utils';
|
import { formatDateTime, openWindow } from '@vben/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getUnreadNotifyMessageCount,
|
||||||
|
getUnreadNotifyMessageList,
|
||||||
|
updateAllNotifyMessageRead,
|
||||||
|
updateNotifyMessageRead,
|
||||||
|
} from '#/api/system/notify/message';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
import { router } from '#/router';
|
||||||
import { useAuthStore } from '#/store';
|
import { useAuthStore } from '#/store';
|
||||||
import LoginForm from '#/views/_core/authentication/login.vue';
|
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||||
|
|
||||||
const notifications = ref<NotificationItem[]>([
|
import Help from './components/help.vue';
|
||||||
{
|
import TenantDropdown from './components/tenant-dropdown.vue';
|
||||||
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
|
||||||
date: '3小时前',
|
|
||||||
isRead: true,
|
|
||||||
message: '描述信息描述信息描述信息',
|
|
||||||
title: '收到了 14 份新周报',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'https://avatar.vercel.sh/1',
|
|
||||||
date: '刚刚',
|
|
||||||
isRead: false,
|
|
||||||
message: '描述信息描述信息描述信息',
|
|
||||||
title: '朱偏右 回复了你',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'https://avatar.vercel.sh/1',
|
|
||||||
date: '2024-01-01',
|
|
||||||
isRead: false,
|
|
||||||
message: '描述信息描述信息描述信息',
|
|
||||||
title: '曲丽丽 评论了你',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'https://avatar.vercel.sh/satori',
|
|
||||||
date: '1天前',
|
|
||||||
isRead: false,
|
|
||||||
message: '描述信息描述信息描述信息',
|
|
||||||
title: '代办提醒',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const { destroyWatermark, updateWatermark } = useWatermark();
|
const { destroyWatermark, updateWatermark } = useWatermark();
|
||||||
const showDot = computed(() =>
|
|
||||||
notifications.value.some((item) => !item.isRead),
|
const notifications = ref<NotificationItem[]>([]);
|
||||||
);
|
const unreadCount = ref(0);
|
||||||
|
const showDot = computed(() => unreadCount.value > 0);
|
||||||
|
|
||||||
|
const [HelpModal, helpModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Help,
|
||||||
|
});
|
||||||
|
|
||||||
const menus = computed(() => [
|
const menus = computed(() => [
|
||||||
|
{
|
||||||
|
handler: () => {
|
||||||
|
router.push({ name: 'Profile' });
|
||||||
|
},
|
||||||
|
icon: AntdProfileOutlined,
|
||||||
|
text: $t('ui.widgets.profile'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
handler: () => {
|
handler: () => {
|
||||||
openWindow(VBEN_DOC_URL, {
|
openWindow(VBEN_DOC_URL, {
|
||||||
@@ -81,9 +77,7 @@ const menus = computed(() => [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
handler: () => {
|
handler: () => {
|
||||||
openWindow(`${VBEN_GITHUB_URL}/issues`, {
|
helpModalApi.open();
|
||||||
target: '_blank',
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
icon: CircleHelp,
|
icon: CircleHelp,
|
||||||
text: $t('ui.widgets.qa'),
|
text: $t('ui.widgets.qa'),
|
||||||
@@ -98,20 +92,83 @@ async function handleLogout() {
|
|||||||
await authStore.logout(false);
|
await authStore.logout(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNoticeClear() {
|
/** 获得未读消息数 */
|
||||||
|
async function handleNotificationGetUnreadCount() {
|
||||||
|
unreadCount.value = await getUnreadNotifyMessageCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得消息列表 */
|
||||||
|
async function handleNotificationGetList() {
|
||||||
|
const list = await getUnreadNotifyMessageList();
|
||||||
|
notifications.value = list.map((item) => ({
|
||||||
|
avatar: preferences.app.defaultAvatar,
|
||||||
|
date: formatDateTime(item.createTime) as string,
|
||||||
|
isRead: false,
|
||||||
|
id: item.id,
|
||||||
|
message: item.templateContent,
|
||||||
|
title: item.templateNickname,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 跳转我的站内信 */
|
||||||
|
function handleNotificationViewAll() {
|
||||||
|
router.push({
|
||||||
|
name: 'MyNotifyMessage',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标记所有已读 */
|
||||||
|
async function handleNotificationMakeAll() {
|
||||||
|
await updateAllNotifyMessageRead();
|
||||||
|
unreadCount.value = 0;
|
||||||
notifications.value = [];
|
notifications.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMakeAll() {
|
/** 清空通知 */
|
||||||
notifications.value.forEach((item) => (item.isRead = true));
|
async function handleNotificationClear() {
|
||||||
|
handleNotificationMakeAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 标记单个已读 */
|
||||||
|
async function handleNotificationRead(item: NotificationItem) {
|
||||||
|
if (!item.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await updateNotifyMessageRead([item.id]);
|
||||||
|
await handleNotificationGetUnreadCount();
|
||||||
|
notifications.value = notifications.value.filter((n) => n.id !== item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理通知打开 */
|
||||||
|
function handleNotificationOpen(open: boolean) {
|
||||||
|
if (!open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleNotificationGetList();
|
||||||
|
handleNotificationGetUnreadCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 初始化 ==========
|
||||||
|
onMounted(() => {
|
||||||
|
// 首次加载未读数量
|
||||||
|
handleNotificationGetUnreadCount();
|
||||||
|
// 轮询刷新未读数量
|
||||||
|
setInterval(
|
||||||
|
() => {
|
||||||
|
if (userStore.userInfo) {
|
||||||
|
handleNotificationGetUnreadCount();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
1000 * 60 * 2,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => preferences.app.watermark,
|
() => preferences.app.watermark,
|
||||||
async (enable) => {
|
async (enable) => {
|
||||||
if (enable) {
|
if (enable) {
|
||||||
await updateWatermark({
|
await updateWatermark({
|
||||||
content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
|
content: `${userStore.userInfo?.id} - ${userStore.userInfo?.nickname}`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
destroyWatermark();
|
destroyWatermark();
|
||||||
@@ -129,9 +186,9 @@ watch(
|
|||||||
<UserDropdown
|
<UserDropdown
|
||||||
:avatar
|
:avatar
|
||||||
:menus
|
:menus
|
||||||
:text="userStore.userInfo?.realName"
|
:text="userStore.userInfo?.nickname"
|
||||||
description="ann.vben@gmail.com"
|
:description="userStore.userInfo?.email"
|
||||||
tag-text="Pro"
|
:tag-text="userStore.userInfo?.username"
|
||||||
@logout="handleLogout"
|
@logout="handleLogout"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -139,10 +196,16 @@ watch(
|
|||||||
<Notification
|
<Notification
|
||||||
:dot="showDot"
|
:dot="showDot"
|
||||||
:notifications="notifications"
|
:notifications="notifications"
|
||||||
@clear="handleNoticeClear"
|
@clear="handleNotificationClear"
|
||||||
@make-all="handleMakeAll"
|
@make-all="handleNotificationMakeAll"
|
||||||
|
@view-all="handleNotificationViewAll"
|
||||||
|
@open="handleNotificationOpen"
|
||||||
|
@read="handleNotificationRead"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
<template #header-right-1>
|
||||||
|
<TenantDropdown class="w-30 mr-2" />
|
||||||
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<AuthenticationLoginExpiredModal
|
<AuthenticationLoginExpiredModal
|
||||||
v-model:open="accessStore.loginExpired"
|
v-model:open="accessStore.loginExpired"
|
||||||
@@ -155,4 +218,5 @@ watch(
|
|||||||
<LockScreen :avatar @to-login="handleLogout" />
|
<LockScreen :avatar @to-login="handleLogout" />
|
||||||
</template>
|
</template>
|
||||||
</BasicLayout>
|
</BasicLayout>
|
||||||
|
<HelpModal />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Demos",
|
|
||||||
"naive": "Naive UI",
|
|
||||||
"table": "Table",
|
|
||||||
"form": "Form",
|
|
||||||
"vben": {
|
|
||||||
"title": "Project",
|
|
||||||
"about": "About",
|
|
||||||
"document": "Document",
|
|
||||||
"antdv": "Ant Design Vue Version",
|
|
||||||
"naive-ui": "Naive UI Version",
|
|
||||||
"element-plus": "Element Plus Version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,5 +10,23 @@
|
|||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
"analytics": "Analytics",
|
"analytics": "Analytics",
|
||||||
"workspace": "Workspace"
|
"workspace": "Workspace"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"action": "Action",
|
||||||
|
"add": "Add",
|
||||||
|
"edit": "Edit",
|
||||||
|
"delete": "Delete",
|
||||||
|
"save": "Save",
|
||||||
|
"import": "Import",
|
||||||
|
"export": "Export",
|
||||||
|
"submit": "Submit",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"reset": "Reset",
|
||||||
|
"search": "Search"
|
||||||
|
},
|
||||||
|
"tenant": {
|
||||||
|
"placeholder": "Please select tenant",
|
||||||
|
"success": "Switch tenant success"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "演示",
|
|
||||||
"naive": "Naive UI",
|
|
||||||
"table": "Table",
|
|
||||||
"form": "表单",
|
|
||||||
"vben": {
|
|
||||||
"title": "项目",
|
|
||||||
"about": "关于",
|
|
||||||
"document": "文档",
|
|
||||||
"antdv": "Ant Design Vue 版本",
|
|
||||||
"naive-ui": "Naive UI 版本",
|
|
||||||
"element-plus": "Element Plus 版本"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,5 +10,23 @@
|
|||||||
"title": "概览",
|
"title": "概览",
|
||||||
"analytics": "分析页",
|
"analytics": "分析页",
|
||||||
"workspace": "工作台"
|
"workspace": "工作台"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"action": "操作",
|
||||||
|
"add": "新增",
|
||||||
|
"edit": "编辑",
|
||||||
|
"delete": "删除",
|
||||||
|
"save": "保存",
|
||||||
|
"import": "导入",
|
||||||
|
"export": "导出",
|
||||||
|
"submit": "提交",
|
||||||
|
"cancel": "取消",
|
||||||
|
"confirm": "确认",
|
||||||
|
"reset": "重置",
|
||||||
|
"search": "搜索"
|
||||||
|
},
|
||||||
|
"tenant": {
|
||||||
|
"placeholder": "请选择租户",
|
||||||
|
"success": "切换租户成功"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,18 @@ import { defineOverridesPreferences } from '@vben/preferences';
|
|||||||
export const overridesPreferences = defineOverridesPreferences({
|
export const overridesPreferences = defineOverridesPreferences({
|
||||||
// overrides
|
// overrides
|
||||||
app: {
|
app: {
|
||||||
|
/** 后端路由模式 */
|
||||||
|
accessMode: 'backend',
|
||||||
name: import.meta.env.VITE_APP_TITLE,
|
name: import.meta.env.VITE_APP_TITLE,
|
||||||
|
enableRefreshToken: true,
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
/** 默认关闭 footer 页脚,因为有一定遮挡 */
|
||||||
|
enable: false,
|
||||||
|
fixed: false,
|
||||||
|
},
|
||||||
|
copyright: {
|
||||||
|
companyName: import.meta.env.VITE_APP_TITLE,
|
||||||
|
companySiteLink: 'https://gitee.com/yudaocode/yudao-ui-admin-vben',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import type {
|
import type {
|
||||||
|
AppRouteRecordRaw,
|
||||||
ComponentRecordType,
|
ComponentRecordType,
|
||||||
GenerateMenuAndRoutesOptions,
|
GenerateMenuAndRoutesOptions,
|
||||||
} from '@vben/types';
|
} from '@vben/types';
|
||||||
|
|
||||||
import { generateAccessible } from '@vben/access';
|
import { generateAccessible } from '@vben/access';
|
||||||
import { preferences } from '@vben/preferences';
|
import { preferences } from '@vben/preferences';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
import { convertServerMenuToRouteRecordStringComponent } from '@vben/utils';
|
||||||
|
|
||||||
import { message } from '#/adapter/naive';
|
|
||||||
import { getAllMenusApi } from '#/api';
|
|
||||||
import { BasicLayout, IFrameView } from '#/layouts';
|
import { BasicLayout, IFrameView } from '#/layouts';
|
||||||
import { $t } from '#/locales';
|
|
||||||
|
|
||||||
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
||||||
|
|
||||||
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||||
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
|
||||||
const layoutMap: ComponentRecordType = {
|
const layoutMap: ComponentRecordType = {
|
||||||
BasicLayout,
|
BasicLayout,
|
||||||
@@ -24,10 +25,10 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
|||||||
return await generateAccessible(preferences.app.accessMode, {
|
return await generateAccessible(preferences.app.accessMode, {
|
||||||
...options,
|
...options,
|
||||||
fetchMenuListAsync: async () => {
|
fetchMenuListAsync: async () => {
|
||||||
message.loading(`${$t('common.loadingMenu')}...`, {
|
// 由于 yudao 通过 accessStore 读取,所以不在进行 message.loading 提示
|
||||||
duration: 1.5,
|
// 补充说明:accessStore.accessMenus 一开始是 AppRouteRecordRaw 类型(后端加载),后面被赋值成 MenuRecordRaw 类型(前端转换)
|
||||||
});
|
const accessMenus = accessStore.accessMenus as AppRouteRecordRaw[];
|
||||||
return await getAllMenusApi();
|
return convertServerMenuToRouteRecordStringComponent(accessMenus);
|
||||||
},
|
},
|
||||||
// 可以指定没有权限跳转403页面
|
// 可以指定没有权限跳转403页面
|
||||||
forbiddenComponent,
|
forbiddenComponent,
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import type { Router } from 'vue-router';
|
import type { Router } from 'vue-router';
|
||||||
|
|
||||||
import { LOGIN_PATH } from '@vben/constants';
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
import { preferences } from '@vben/preferences';
|
import { preferences } from '@vben/preferences';
|
||||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
import { startProgress, stopProgress } from '@vben/utils';
|
import { startProgress, stopProgress } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { getSimpleDictDataList } from '#/api/system/dict/data';
|
||||||
import { accessRoutes, coreRouteNames } from '#/router/routes';
|
import { accessRoutes, coreRouteNames } from '#/router/routes';
|
||||||
import { useAuthStore } from '#/store';
|
import { useAuthStore, useDictStore } from '#/store';
|
||||||
|
|
||||||
import { generateAccess } from './access';
|
import { generateAccess } from './access';
|
||||||
|
|
||||||
@@ -49,6 +52,7 @@ function setupAccessGuard(router: Router) {
|
|||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
const dictStore = useDictStore();
|
||||||
|
|
||||||
// 基本路由,这些路由不需要进入权限拦截
|
// 基本路由,这些路由不需要进入权限拦截
|
||||||
if (coreRouteNames.includes(to.name as string)) {
|
if (coreRouteNames.includes(to.name as string)) {
|
||||||
@@ -89,10 +93,26 @@ function setupAccessGuard(router: Router) {
|
|||||||
if (accessStore.isAccessChecked) {
|
if (accessStore.isAccessChecked) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载字典数据(不阻塞加载)
|
||||||
|
dictStore.setDictCacheByApi(getSimpleDictDataList);
|
||||||
|
|
||||||
// 生成路由表
|
// 生成路由表
|
||||||
// 当前登录用户拥有的角色标识列表
|
// 当前登录用户拥有的角色标识列表
|
||||||
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
|
let userInfo = userStore.userInfo;
|
||||||
const userRoles = userInfo.roles ?? [];
|
if (!userInfo) {
|
||||||
|
// add by 芋艿:由于 yudao 是 fetchUserInfo 统一加载用户 + 权限信息,所以将 fetchMenuListAsync
|
||||||
|
const loading = message.loading(`${$t('common.loadingMenu')}...`);
|
||||||
|
try {
|
||||||
|
const authPermissionInfo = await authStore.fetchUserInfo();
|
||||||
|
if (authPermissionInfo) {
|
||||||
|
userInfo = authPermissionInfo.user;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const userRoles = userStore.userRoles ?? [];
|
||||||
|
|
||||||
// 生成菜单和路由
|
// 生成菜单和路由
|
||||||
const { accessibleMenus, accessibleRoutes } = await generateAccess({
|
const { accessibleMenus, accessibleRoutes } = await generateAccess({
|
||||||
@@ -106,9 +126,10 @@ function setupAccessGuard(router: Router) {
|
|||||||
accessStore.setAccessMenus(accessibleMenus);
|
accessStore.setAccessMenus(accessibleMenus);
|
||||||
accessStore.setAccessRoutes(accessibleRoutes);
|
accessStore.setAccessRoutes(accessibleRoutes);
|
||||||
accessStore.setIsAccessChecked(true);
|
accessStore.setIsAccessChecked(true);
|
||||||
|
userStore.setUserRoles(userRoles);
|
||||||
const redirectPath = (from.query.redirect ??
|
const redirectPath = (from.query.redirect ??
|
||||||
(to.path === preferences.app.defaultHomePath
|
(to.path === preferences.app.defaultHomePath
|
||||||
? userInfo.homePath || preferences.app.defaultHomePath
|
? userInfo?.homePath || preferences.app.defaultHomePath
|
||||||
: to.fullPath)) as string;
|
: to.fullPath)) as string;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { resetStaticRoutes } from '@vben/utils';
|
|||||||
|
|
||||||
import { createRouterGuard } from './guard';
|
import { createRouterGuard } from './guard';
|
||||||
import { routes } from './routes';
|
import { routes } from './routes';
|
||||||
|
import { setupBaiduTongJi } from './tongji';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 创建vue-router实例
|
* @zh_CN 创建vue-router实例
|
||||||
@@ -33,5 +34,7 @@ const resetRoutes = () => resetStaticRoutes(router, routes);
|
|||||||
|
|
||||||
// 创建路由守卫
|
// 创建路由守卫
|
||||||
createRouterGuard(router);
|
createRouterGuard(router);
|
||||||
|
// 设置百度统计
|
||||||
|
setupBaiduTongJi(router);
|
||||||
|
|
||||||
export { resetRoutes, router };
|
export { resetRoutes, router };
|
||||||
|
|||||||
@@ -90,6 +90,23 @@ const coreRoutes: RouteRecordRaw[] = [
|
|||||||
title: $t('page.auth.register'),
|
title: $t('page.auth.register'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'SocialLogin',
|
||||||
|
path: 'social-login',
|
||||||
|
component: () =>
|
||||||
|
import('#/views/_core/authentication/social-login.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.auth.login'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SSOLogin',
|
||||||
|
path: 'sso-login',
|
||||||
|
component: () => import('#/views/_core/authentication/sso-login.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.auth.login'),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -34,4 +34,14 @@ const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
|||||||
|
|
||||||
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||||
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||||
export { accessRoutes, coreRouteNames, routes };
|
|
||||||
|
// add by 芋艿:from https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/router/routes/index.ts#L38-L45
|
||||||
|
const componentKeys: string[] = Object.keys(
|
||||||
|
import.meta.glob('../../views/**/*.vue'),
|
||||||
|
)
|
||||||
|
.filter((item) => !item.includes('/modules/'))
|
||||||
|
.map((v) => {
|
||||||
|
const path = v.replace('../../views/', '/');
|
||||||
|
return path.endsWith('.vue') ? path.slice(0, -4) : path;
|
||||||
|
});
|
||||||
|
export { accessRoutes, componentKeys, coreRouteNames, routes };
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
name: 'Workspace',
|
||||||
|
path: '/workspace',
|
||||||
|
component: () => import('#/views/dashboard/workspace/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'carbon:workspace',
|
||||||
|
title: $t('page.dashboard.workspace'),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Analytics',
|
name: 'Analytics',
|
||||||
path: '/analytics',
|
path: '/analytics',
|
||||||
@@ -22,17 +31,18 @@ const routes: RouteRecordRaw[] = [
|
|||||||
title: $t('page.dashboard.analytics'),
|
title: $t('page.dashboard.analytics'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Workspace',
|
|
||||||
path: '/workspace',
|
|
||||||
component: () => import('#/views/dashboard/workspace/index.vue'),
|
|
||||||
meta: {
|
|
||||||
icon: 'carbon:workspace',
|
|
||||||
title: $t('page.dashboard.workspace'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Profile',
|
||||||
|
path: '/profile',
|
||||||
|
component: () => import('#/views/_core/profile/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'ant-design:profile-outlined',
|
||||||
|
title: $t('ui.widgets.profile'),
|
||||||
|
hideInMenu: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default routes;
|
export default routes;
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import type { RouteRecordRaw } from 'vue-router';
|
|
||||||
|
|
||||||
import { $t } from '#/locales';
|
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
|
||||||
{
|
|
||||||
meta: {
|
|
||||||
icon: 'ic:baseline-view-in-ar',
|
|
||||||
keepAlive: true,
|
|
||||||
order: 1000,
|
|
||||||
title: $t('demos.title'),
|
|
||||||
},
|
|
||||||
name: 'Demos',
|
|
||||||
path: '/demos',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
meta: {
|
|
||||||
title: $t('demos.naive'),
|
|
||||||
},
|
|
||||||
name: 'NaiveDemos',
|
|
||||||
path: '/demos/naive',
|
|
||||||
component: () => import('#/views/demos/naive/index.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
meta: {
|
|
||||||
title: $t('demos.table'),
|
|
||||||
},
|
|
||||||
name: 'Table',
|
|
||||||
path: '/demos/table',
|
|
||||||
component: () => import('#/views/demos/table/index.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
meta: {
|
|
||||||
title: $t('demos.form'),
|
|
||||||
},
|
|
||||||
name: 'Form',
|
|
||||||
path: '/demos/form',
|
|
||||||
component: () => import('#/views/demos/form/basic.vue'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default routes;
|
|
||||||
@@ -1,82 +1,81 @@
|
|||||||
import type { RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
import {
|
// import {
|
||||||
VBEN_ANT_PREVIEW_URL,
|
// VBEN_DOC_URL,
|
||||||
VBEN_DOC_URL,
|
// VBEN_ELE_PREVIEW_URL,
|
||||||
VBEN_ELE_PREVIEW_URL,
|
// VBEN_GITHUB_URL,
|
||||||
VBEN_GITHUB_URL,
|
// VBEN_LOGO_URL,
|
||||||
VBEN_LOGO_URL,
|
// VBEN_NAIVE_PREVIEW_URL,
|
||||||
} from '@vben/constants';
|
// } from '@vben/constants';
|
||||||
import { SvgAntdvLogoIcon } from '@vben/icons';
|
//
|
||||||
|
// import { IFrameView } from '#/layouts';
|
||||||
import { IFrameView } from '#/layouts';
|
// import { $t } from '#/locales';
|
||||||
import { $t } from '#/locales';
|
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
// {
|
||||||
meta: {
|
// meta: {
|
||||||
badgeType: 'dot',
|
// badgeType: 'dot',
|
||||||
icon: VBEN_LOGO_URL,
|
// icon: VBEN_LOGO_URL,
|
||||||
order: 9998,
|
// order: 9998,
|
||||||
title: $t('demos.vben.title'),
|
// title: $t('demos.vben.title'),
|
||||||
},
|
// },
|
||||||
name: 'VbenProject',
|
// name: 'VbenProject',
|
||||||
path: '/vben-admin',
|
// path: '/vben-admin',
|
||||||
children: [
|
// children: [
|
||||||
{
|
// {
|
||||||
name: 'VbenDocument',
|
// name: 'VbenDocument',
|
||||||
path: '/vben-admin/document',
|
// path: '/vben-admin/document',
|
||||||
component: IFrameView,
|
// component: IFrameView,
|
||||||
meta: {
|
// meta: {
|
||||||
icon: 'lucide:book-open-text',
|
// icon: 'lucide:book-open-text',
|
||||||
link: VBEN_DOC_URL,
|
// link: VBEN_DOC_URL,
|
||||||
title: $t('demos.vben.document'),
|
// title: $t('demos.vben.document'),
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: 'VbenGithub',
|
// name: 'VbenGithub',
|
||||||
path: '/vben-admin/github',
|
// path: '/vben-admin/github',
|
||||||
component: IFrameView,
|
// component: IFrameView,
|
||||||
meta: {
|
// meta: {
|
||||||
icon: 'mdi:github',
|
// icon: 'mdi:github',
|
||||||
link: VBEN_GITHUB_URL,
|
// link: VBEN_GITHUB_URL,
|
||||||
title: 'Github',
|
// title: 'Github',
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: 'VbenAntd',
|
// name: 'VbenNaive',
|
||||||
path: '/vben-admin/antd',
|
// path: '/vben-admin/naive',
|
||||||
component: IFrameView,
|
// component: IFrameView,
|
||||||
meta: {
|
// meta: {
|
||||||
badgeType: 'dot',
|
// badgeType: 'dot',
|
||||||
icon: SvgAntdvLogoIcon,
|
// icon: 'logos:naiveui',
|
||||||
link: VBEN_ANT_PREVIEW_URL,
|
// link: VBEN_NAIVE_PREVIEW_URL,
|
||||||
title: $t('demos.vben.antdv'),
|
// title: $t('demos.vben.naive-ui'),
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: 'VbenElementPlus',
|
// name: 'VbenElementPlus',
|
||||||
path: '/vben-admin/ele',
|
// path: '/vben-admin/ele',
|
||||||
component: IFrameView,
|
// component: IFrameView,
|
||||||
meta: {
|
// meta: {
|
||||||
badgeType: 'dot',
|
// badgeType: 'dot',
|
||||||
icon: 'logos:element',
|
// icon: 'logos:element',
|
||||||
link: VBEN_ELE_PREVIEW_URL,
|
// link: VBEN_ELE_PREVIEW_URL,
|
||||||
title: $t('demos.vben.element-plus'),
|
// title: $t('demos.vben.element-plus'),
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
],
|
// ],
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: 'VbenAbout',
|
// name: 'VbenAbout',
|
||||||
path: '/vben-admin/about',
|
// path: '/vben-admin/about',
|
||||||
component: () => import('#/views/_core/about/index.vue'),
|
// component: () => import('#/views/_core/about/index.vue'),
|
||||||
meta: {
|
// meta: {
|
||||||
icon: 'lucide:copyright',
|
// icon: 'lucide:copyright',
|
||||||
title: $t('demos.vben.about'),
|
// title: $t('demos.vben.about'),
|
||||||
order: 9999,
|
// order: 9999,
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default routes;
|
export default routes; // update by 芋艿:不展示
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { Recordable, UserInfo } from '@vben/types';
|
import type { AuthPermissionInfo, Recordable, UserInfo } from '@vben/types';
|
||||||
|
|
||||||
|
import type { AuthApi } from '#/api';
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
@@ -10,7 +12,14 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
import { notification } from '#/adapter/naive';
|
import { notification } from '#/adapter/naive';
|
||||||
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
|
import {
|
||||||
|
getAuthPermissionInfoApi,
|
||||||
|
loginApi,
|
||||||
|
logoutApi,
|
||||||
|
register,
|
||||||
|
smsLogin,
|
||||||
|
socialLogin,
|
||||||
|
} from '#/api';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
@@ -23,9 +32,12 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
/**
|
/**
|
||||||
* 异步处理登录操作
|
* 异步处理登录操作
|
||||||
* Asynchronously handle the login process
|
* Asynchronously handle the login process
|
||||||
|
* @param type 登录类型
|
||||||
* @param params 登录表单数据
|
* @param params 登录表单数据
|
||||||
|
* @param onSuccess 登录成功后的回调函数
|
||||||
*/
|
*/
|
||||||
async function authLogin(
|
async function authLogin(
|
||||||
|
type: 'mobile' | 'register' | 'social' | 'username',
|
||||||
params: Recordable<any>,
|
params: Recordable<any>,
|
||||||
onSuccess?: () => Promise<void> | void,
|
onSuccess?: () => Promise<void> | void,
|
||||||
) {
|
) {
|
||||||
@@ -33,23 +45,30 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
let userInfo: null | UserInfo = null;
|
let userInfo: null | UserInfo = null;
|
||||||
try {
|
try {
|
||||||
loginLoading.value = true;
|
loginLoading.value = true;
|
||||||
const { accessToken } = await loginApi(params);
|
const { accessToken, refreshToken } =
|
||||||
|
type === 'mobile'
|
||||||
|
? await smsLogin(params as AuthApi.SmsLoginParams)
|
||||||
|
: type === 'register'
|
||||||
|
? await register(params as AuthApi.RegisterParams)
|
||||||
|
: // eslint-disable-next-line unicorn/no-nested-ternary
|
||||||
|
type === 'social'
|
||||||
|
? await socialLogin(params as AuthApi.SocialLoginParams)
|
||||||
|
: await loginApi(params);
|
||||||
|
|
||||||
// 如果成功获取到 accessToken
|
// 如果成功获取到 accessToken
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
// 将 accessToken 存储到 accessStore 中
|
|
||||||
accessStore.setAccessToken(accessToken);
|
accessStore.setAccessToken(accessToken);
|
||||||
|
accessStore.setRefreshToken(refreshToken);
|
||||||
|
|
||||||
// 获取用户信息并存储到 accessStore 中
|
// 获取用户信息并存储到 userStore、accessStore 中
|
||||||
const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
// TODO @芋艿:清理掉 accessCodes 相关的逻辑
|
||||||
fetchUserInfo(),
|
// const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
||||||
getAccessCodesApi(),
|
// fetchUserInfo(),
|
||||||
]);
|
// // getAccessCodesApi(),
|
||||||
|
// ]);
|
||||||
|
const fetchUserInfoResult = await fetchUserInfo();
|
||||||
|
|
||||||
userInfo = fetchUserInfoResult;
|
userInfo = fetchUserInfoResult.user;
|
||||||
|
|
||||||
userStore.setUserInfo(userInfo);
|
|
||||||
accessStore.setAccessCodes(accessCodes);
|
|
||||||
|
|
||||||
if (accessStore.loginExpired) {
|
if (accessStore.loginExpired) {
|
||||||
accessStore.setLoginExpired(false);
|
accessStore.setLoginExpired(false);
|
||||||
@@ -61,10 +80,10 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userInfo?.realName) {
|
if (userInfo?.nickname) {
|
||||||
notification.success({
|
notification.success({
|
||||||
content: $t('authentication.loginSuccess'),
|
content: $t('authentication.loginSuccess'),
|
||||||
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
|
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.nickname}`,
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -80,7 +99,10 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
|
|
||||||
async function logout(redirect: boolean = true) {
|
async function logout(redirect: boolean = true) {
|
||||||
try {
|
try {
|
||||||
await logoutApi();
|
const accessToken = accessStore.accessToken as string;
|
||||||
|
if (accessToken) {
|
||||||
|
await logoutApi(accessToken);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 不做任何处理
|
// 不做任何处理
|
||||||
}
|
}
|
||||||
@@ -99,10 +121,16 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchUserInfo() {
|
async function fetchUserInfo() {
|
||||||
let userInfo: null | UserInfo = null;
|
// 加载
|
||||||
userInfo = await getUserInfoApi();
|
let authPermissionInfo: AuthPermissionInfo | null = null;
|
||||||
userStore.setUserInfo(userInfo);
|
authPermissionInfo = await getAuthPermissionInfoApi();
|
||||||
return userInfo;
|
// userStore
|
||||||
|
userStore.setUserInfo(authPermissionInfo.user);
|
||||||
|
userStore.setUserRoles(authPermissionInfo.roles);
|
||||||
|
// accessStore
|
||||||
|
accessStore.setAccessMenus(authPermissionInfo.menus);
|
||||||
|
accessStore.setAccessCodes(authPermissionInfo.permissions);
|
||||||
|
return authPermissionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
function $reset() {
|
function $reset() {
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './auth';
|
export * from './auth';
|
||||||
|
export * from './dict';
|
||||||
|
|||||||
@@ -2,24 +2,100 @@
|
|||||||
import type { VbenFormSchema } from '@vben/common-ui';
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
import type { Recordable } from '@vben/types';
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import type { AuthApi } from '#/api';
|
||||||
|
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||||
|
import { isTenantEnable } from '@vben/hooks';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { sendSmsCode } from '#/api';
|
||||||
|
import { getTenantByWebsite, getTenantSimpleList } from '#/api/core/auth';
|
||||||
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
defineOptions({ name: 'CodeLogin' });
|
defineOptions({ name: 'CodeLogin' });
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const tenantEnable = isTenantEnable();
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const CODE_LENGTH = 6;
|
const CODE_LENGTH = 4;
|
||||||
|
|
||||||
|
const loginRef = ref();
|
||||||
|
|
||||||
|
/** 获取租户列表,并默认选中 */
|
||||||
|
const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表
|
||||||
|
async function fetchTenantList() {
|
||||||
|
if (!tenantEnable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 获取租户列表、域名对应租户
|
||||||
|
const websiteTenantPromise = getTenantByWebsite(window.location.hostname);
|
||||||
|
tenantList.value = await getTenantSimpleList();
|
||||||
|
|
||||||
|
// 选中租户:域名 > store 中的租户 > 首个租户
|
||||||
|
let tenantId: null | number = null;
|
||||||
|
const websiteTenant = await websiteTenantPromise;
|
||||||
|
if (websiteTenant?.id) {
|
||||||
|
tenantId = websiteTenant.id;
|
||||||
|
}
|
||||||
|
// 如果没有从域名获取到租户,尝试从 store 中获取
|
||||||
|
if (!tenantId && accessStore.tenantId) {
|
||||||
|
tenantId = accessStore.tenantId;
|
||||||
|
}
|
||||||
|
// 如果还是没有租户,使用列表中的第一个
|
||||||
|
if (!tenantId && tenantList.value?.[0]?.id) {
|
||||||
|
tenantId = tenantList.value[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置选中的租户编号
|
||||||
|
accessStore.setTenantId(tenantId);
|
||||||
|
loginRef.value.getFormApi().setFieldValue('tenantId', tenantId?.toString());
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取租户列表失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 组件挂载时获取租户信息 */
|
||||||
|
onMounted(() => {
|
||||||
|
fetchTenantList();
|
||||||
|
});
|
||||||
|
|
||||||
const formSchema = computed((): VbenFormSchema[] => {
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
component: 'VbenSelect',
|
||||||
|
componentProps: {
|
||||||
|
options: tenantList.value.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id.toString(),
|
||||||
|
})),
|
||||||
|
placeholder: $t('authentication.tenantTip'),
|
||||||
|
},
|
||||||
|
fieldName: 'tenantId',
|
||||||
|
label: $t('authentication.tenant'),
|
||||||
|
rules: z.string().min(1, { message: $t('authentication.tenantTip') }),
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['tenantId'],
|
||||||
|
if: tenantEnable,
|
||||||
|
trigger(values) {
|
||||||
|
if (values.tenantId) {
|
||||||
|
accessStore.setTenantId(Number(values.tenantId));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: 'VbenInput',
|
component: 'VbenInput',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: $t('authentication.mobile'),
|
placeholder: $t('authentication.mobile'),
|
||||||
},
|
},
|
||||||
fieldName: 'phoneNumber',
|
fieldName: 'mobile',
|
||||||
label: $t('authentication.mobile'),
|
label: $t('authentication.mobile'),
|
||||||
rules: z
|
rules: z
|
||||||
.string()
|
.string()
|
||||||
@@ -40,6 +116,29 @@ const formSchema = computed((): VbenFormSchema[] => {
|
|||||||
return text;
|
return text;
|
||||||
},
|
},
|
||||||
placeholder: $t('authentication.code'),
|
placeholder: $t('authentication.code'),
|
||||||
|
handleSendCode: async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const formApi = loginRef.value?.getFormApi();
|
||||||
|
if (!formApi) {
|
||||||
|
throw new Error('表单未准备好');
|
||||||
|
}
|
||||||
|
// 验证手机号
|
||||||
|
await formApi.validateField('mobile');
|
||||||
|
const isMobileValid = await formApi.isFieldValid('mobile');
|
||||||
|
if (!isMobileValid) {
|
||||||
|
throw new Error('请输入有效的手机号码');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
const { mobile } = await formApi.getValues();
|
||||||
|
const scene = 21; // 场景:短信验证码登录
|
||||||
|
await sendSmsCode({ mobile, scene });
|
||||||
|
message.success('验证码发送成功');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fieldName: 'code',
|
fieldName: 'code',
|
||||||
label: $t('authentication.code'),
|
label: $t('authentication.code'),
|
||||||
@@ -55,13 +154,17 @@ const formSchema = computed((): VbenFormSchema[] => {
|
|||||||
* @param values 登录表单数据
|
* @param values 登录表单数据
|
||||||
*/
|
*/
|
||||||
async function handleLogin(values: Recordable<any>) {
|
async function handleLogin(values: Recordable<any>) {
|
||||||
// eslint-disable-next-line no-console
|
try {
|
||||||
console.log(values);
|
await authStore.authLogin('mobile', values);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in handleLogin:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationCodeLogin
|
<AuthenticationCodeLogin
|
||||||
|
ref="loginRef"
|
||||||
:form-schema="formSchema"
|
:form-schema="formSchema"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@submit="handleLogin"
|
@submit="handleLogin"
|
||||||
|
|||||||
@@ -2,40 +2,212 @@
|
|||||||
import type { VbenFormSchema } from '@vben/common-ui';
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
import type { Recordable } from '@vben/types';
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import type { AuthApi } from '#/api';
|
||||||
|
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
|
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
|
||||||
|
import { isTenantEnable } from '@vben/hooks';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { sendSmsCode, smsResetPassword } from '#/api';
|
||||||
|
import { getTenantByWebsite, getTenantSimpleList } from '#/api/core/auth';
|
||||||
|
|
||||||
defineOptions({ name: 'ForgetPassword' });
|
defineOptions({ name: 'ForgetPassword' });
|
||||||
|
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const tenantEnable = isTenantEnable();
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const CODE_LENGTH = 4;
|
||||||
|
const forgetPasswordRef = ref();
|
||||||
|
|
||||||
|
/** 获取租户列表,并默认选中 */
|
||||||
|
const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表
|
||||||
|
async function fetchTenantList() {
|
||||||
|
if (!tenantEnable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 获取租户列表、域名对应租户
|
||||||
|
const websiteTenantPromise = getTenantByWebsite(window.location.hostname);
|
||||||
|
tenantList.value = await getTenantSimpleList();
|
||||||
|
|
||||||
|
// 选中租户:域名 > store 中的租户 > 首个租户
|
||||||
|
let tenantId: null | number = null;
|
||||||
|
const websiteTenant = await websiteTenantPromise;
|
||||||
|
if (websiteTenant?.id) {
|
||||||
|
tenantId = websiteTenant.id;
|
||||||
|
}
|
||||||
|
// 如果没有从域名获取到租户,尝试从 store 中获取
|
||||||
|
if (!tenantId && accessStore.tenantId) {
|
||||||
|
tenantId = accessStore.tenantId;
|
||||||
|
}
|
||||||
|
// 如果还是没有租户,使用列表中的第一个
|
||||||
|
if (!tenantId && tenantList.value?.[0]?.id) {
|
||||||
|
tenantId = tenantList.value[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置选中的租户编号
|
||||||
|
accessStore.setTenantId(tenantId);
|
||||||
|
forgetPasswordRef.value
|
||||||
|
.getFormApi()
|
||||||
|
.setFieldValue('tenantId', tenantId?.toString());
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取租户列表失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 组件挂载时获取租户信息 */
|
||||||
|
onMounted(() => {
|
||||||
|
fetchTenantList();
|
||||||
|
});
|
||||||
|
|
||||||
const formSchema = computed((): VbenFormSchema[] => {
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
component: 'VbenSelect',
|
||||||
|
componentProps: {
|
||||||
|
options: tenantList.value.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id.toString(),
|
||||||
|
})),
|
||||||
|
placeholder: $t('authentication.tenantTip'),
|
||||||
|
},
|
||||||
|
fieldName: 'tenantId',
|
||||||
|
label: $t('authentication.tenant'),
|
||||||
|
rules: z.string().min(1, { message: $t('authentication.tenantTip') }),
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['tenantId'],
|
||||||
|
if: tenantEnable,
|
||||||
|
trigger(values) {
|
||||||
|
if (values.tenantId) {
|
||||||
|
accessStore.setTenantId(Number(values.tenantId));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: 'VbenInput',
|
component: 'VbenInput',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: 'example@example.com',
|
placeholder: $t('authentication.mobile'),
|
||||||
},
|
},
|
||||||
fieldName: 'email',
|
fieldName: 'mobile',
|
||||||
label: $t('authentication.email'),
|
label: $t('authentication.mobile'),
|
||||||
rules: z
|
rules: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: $t('authentication.emailTip') })
|
.min(1, { message: $t('authentication.mobileTip') })
|
||||||
.email($t('authentication.emailValidErrorTip')),
|
.refine((v) => /^\d{11}$/.test(v), {
|
||||||
|
message: $t('authentication.mobileErrortip'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenPinInput',
|
||||||
|
componentProps: {
|
||||||
|
codeLength: CODE_LENGTH,
|
||||||
|
createText: (countdown: number) => {
|
||||||
|
const text =
|
||||||
|
countdown > 0
|
||||||
|
? $t('authentication.sendText', [countdown])
|
||||||
|
: $t('authentication.sendCode');
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
placeholder: $t('authentication.code'),
|
||||||
|
handleSendCode: async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const formApi = forgetPasswordRef.value?.getFormApi();
|
||||||
|
if (!formApi) {
|
||||||
|
throw new Error('表单未准备好');
|
||||||
|
}
|
||||||
|
// 验证手机号
|
||||||
|
await formApi.validateField('mobile');
|
||||||
|
const isMobileValid = await formApi.isFieldValid('mobile');
|
||||||
|
if (!isMobileValid) {
|
||||||
|
throw new Error('请输入有效的手机号码');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
const { mobile } = await formApi.getValues();
|
||||||
|
const scene = 23; // 场景:重置密码
|
||||||
|
await sendSmsCode({ mobile, scene });
|
||||||
|
message.success('验证码发送成功');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fieldName: 'code',
|
||||||
|
label: $t('authentication.code'),
|
||||||
|
rules: z.string().length(CODE_LENGTH, {
|
||||||
|
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
passwordStrength: true,
|
||||||
|
placeholder: $t('authentication.password'),
|
||||||
|
},
|
||||||
|
fieldName: 'password',
|
||||||
|
label: $t('authentication.password'),
|
||||||
|
renderComponentContent() {
|
||||||
|
return {
|
||||||
|
strengthText: () => $t('authentication.passwordStrength'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('authentication.confirmPassword'),
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
rules(values) {
|
||||||
|
const { password } = values;
|
||||||
|
return z
|
||||||
|
.string({ required_error: $t('authentication.passwordTip') })
|
||||||
|
.min(1, { message: $t('authentication.passwordTip') })
|
||||||
|
.refine((value) => value === password, {
|
||||||
|
message: $t('authentication.confirmPasswordTip'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
triggerFields: ['password'],
|
||||||
|
},
|
||||||
|
fieldName: 'confirmPassword',
|
||||||
|
label: $t('authentication.confirmPassword'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleSubmit(value: Recordable<any>) {
|
/**
|
||||||
// eslint-disable-next-line no-console
|
* 处理重置密码操作
|
||||||
console.log('reset email:', value);
|
* @param values 表单数据
|
||||||
|
*/
|
||||||
|
async function handleSubmit(values: Recordable<any>) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const { mobile, code, password } = values;
|
||||||
|
await smsResetPassword({ mobile, code, password });
|
||||||
|
message.success($t('authentication.resetPasswordSuccess'));
|
||||||
|
// 重置成功后跳转到首页
|
||||||
|
router.push('/');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重置密码失败:', error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationForgetPassword
|
<AuthenticationForgetPassword
|
||||||
|
ref="forgetPasswordRef"
|
||||||
:form-schema="formSchema"
|
:form-schema="formSchema"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@submit="handleSubmit"
|
@submit="handleSubmit"
|
||||||
|
|||||||
@@ -1,98 +1,192 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { VbenFormSchema } from '@vben/common-ui';
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
import type { BasicOption } from '@vben/types';
|
|
||||||
|
|
||||||
import { computed, markRaw } from 'vue';
|
import type { AuthApi } from '#/api/core/auth';
|
||||||
|
|
||||||
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
import { AuthenticationLogin, Verification, z } from '@vben/common-ui';
|
||||||
|
import { isCaptchaEnable, isTenantEnable } from '@vben/hooks';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import {
|
||||||
|
checkCaptcha,
|
||||||
|
getCaptcha,
|
||||||
|
getTenantByWebsite,
|
||||||
|
getTenantSimpleList,
|
||||||
|
socialAuthRedirect,
|
||||||
|
} from '#/api/core/auth';
|
||||||
import { useAuthStore } from '#/store';
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
defineOptions({ name: 'Login' });
|
defineOptions({ name: 'Login' });
|
||||||
|
|
||||||
|
const { query } = useRoute();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const tenantEnable = isTenantEnable();
|
||||||
|
const captchaEnable = isCaptchaEnable();
|
||||||
|
|
||||||
const MOCK_USER_OPTIONS: BasicOption[] = [
|
const loginRef = ref();
|
||||||
{
|
const verifyRef = ref();
|
||||||
label: 'Super',
|
|
||||||
value: 'vben',
|
const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWord'
|
||||||
},
|
|
||||||
{
|
/** 获取租户列表,并默认选中 */
|
||||||
label: 'Admin',
|
const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表
|
||||||
value: 'admin',
|
async function fetchTenantList() {
|
||||||
},
|
if (!tenantEnable) {
|
||||||
{
|
return;
|
||||||
label: 'User',
|
}
|
||||||
value: 'jack',
|
try {
|
||||||
},
|
// 获取租户列表、域名对应租户
|
||||||
];
|
const websiteTenantPromise = getTenantByWebsite(window.location.hostname);
|
||||||
|
tenantList.value = await getTenantSimpleList();
|
||||||
|
|
||||||
|
// 选中租户:域名 > store 中的租户 > 首个租户
|
||||||
|
let tenantId: null | number = null;
|
||||||
|
const websiteTenant = await websiteTenantPromise;
|
||||||
|
if (websiteTenant?.id) {
|
||||||
|
tenantId = websiteTenant.id;
|
||||||
|
}
|
||||||
|
// 如果没有从域名获取到租户,尝试从 store 中获取
|
||||||
|
if (!tenantId && accessStore.tenantId) {
|
||||||
|
tenantId = accessStore.tenantId;
|
||||||
|
}
|
||||||
|
// 如果还是没有租户,使用列表中的第一个
|
||||||
|
if (!tenantId && tenantList.value?.[0]?.id) {
|
||||||
|
tenantId = tenantList.value[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置选中的租户编号
|
||||||
|
accessStore.setTenantId(tenantId);
|
||||||
|
loginRef.value.getFormApi().setFieldValue('tenantId', tenantId?.toString());
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取租户列表失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理登录 */
|
||||||
|
async function handleLogin(values: any) {
|
||||||
|
// 如果开启验证码,则先验证验证码
|
||||||
|
if (captchaEnable) {
|
||||||
|
verifyRef.value.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 无验证码,直接登录
|
||||||
|
await authStore.authLogin('username', values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 验证码通过,执行登录 */
|
||||||
|
async function handleVerifySuccess({ captchaVerification }: any) {
|
||||||
|
try {
|
||||||
|
await authStore.authLogin('username', {
|
||||||
|
...(await loginRef.value.getFormApi().getValues()),
|
||||||
|
captchaVerification,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in handleLogin:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理第三方登录 */
|
||||||
|
const redirect = query?.redirect;
|
||||||
|
async function handleThirdLogin(type: number) {
|
||||||
|
if (type <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 计算 redirectUri
|
||||||
|
// tricky: type、redirect 需要先 encode 一次,否则钉钉回调会丢失。配合 social-login.vue#getUrlValue() 使用
|
||||||
|
const redirectUri = `${
|
||||||
|
location.origin
|
||||||
|
}/auth/social-login?${encodeURIComponent(
|
||||||
|
`type=${type}&redirect=${redirect || '/'}`,
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
// 进行跳转
|
||||||
|
window.location.href = await socialAuthRedirect(type, redirectUri);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('第三方登录处理失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 组件挂载时获取租户信息 */
|
||||||
|
onMounted(() => {
|
||||||
|
fetchTenantList();
|
||||||
|
});
|
||||||
|
|
||||||
const formSchema = computed((): VbenFormSchema[] => {
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
component: 'VbenSelect',
|
component: 'VbenSelect',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
options: MOCK_USER_OPTIONS,
|
options: tenantList.value.map((item) => ({
|
||||||
placeholder: $t('authentication.selectAccount'),
|
label: item.name,
|
||||||
|
value: item.id.toString(),
|
||||||
|
})),
|
||||||
|
placeholder: $t('authentication.tenantTip'),
|
||||||
|
},
|
||||||
|
fieldName: 'tenantId',
|
||||||
|
label: $t('authentication.tenant'),
|
||||||
|
rules: z.string().min(1, { message: $t('authentication.tenantTip') }),
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['tenantId'],
|
||||||
|
if: tenantEnable,
|
||||||
|
trigger(values) {
|
||||||
|
if (values.tenantId) {
|
||||||
|
accessStore.setTenantId(Number(values.tenantId));
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fieldName: 'selectAccount',
|
|
||||||
label: $t('authentication.selectAccount'),
|
|
||||||
rules: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: $t('authentication.selectAccount') })
|
|
||||||
.optional()
|
|
||||||
.default('vben'),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'VbenInput',
|
component: 'VbenInput',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: $t('authentication.usernameTip'),
|
placeholder: $t('authentication.usernameTip'),
|
||||||
},
|
},
|
||||||
dependencies: {
|
|
||||||
trigger(values, form) {
|
|
||||||
if (values.selectAccount) {
|
|
||||||
const findUser = MOCK_USER_OPTIONS.find(
|
|
||||||
(item) => item.value === values.selectAccount,
|
|
||||||
);
|
|
||||||
if (findUser) {
|
|
||||||
form.setValues({
|
|
||||||
password: '123456',
|
|
||||||
username: findUser.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
triggerFields: ['selectAccount'],
|
|
||||||
},
|
|
||||||
fieldName: 'username',
|
fieldName: 'username',
|
||||||
label: $t('authentication.username'),
|
label: $t('authentication.username'),
|
||||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
rules: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: $t('authentication.usernameTip') })
|
||||||
|
.default(import.meta.env.VITE_APP_DEFAULT_USERNAME),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'VbenInputPassword',
|
component: 'VbenInputPassword',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: $t('authentication.password'),
|
placeholder: $t('authentication.passwordTip'),
|
||||||
},
|
},
|
||||||
fieldName: 'password',
|
fieldName: 'password',
|
||||||
label: $t('authentication.password'),
|
label: $t('authentication.password'),
|
||||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
rules: z
|
||||||
},
|
.string()
|
||||||
{
|
.min(1, { message: $t('authentication.passwordTip') })
|
||||||
component: markRaw(SliderCaptcha),
|
.default(import.meta.env.VITE_APP_DEFAULT_PASSWORD),
|
||||||
fieldName: 'captcha',
|
|
||||||
rules: z.boolean().refine((value) => value, {
|
|
||||||
message: $t('authentication.verifyRequiredTip'),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationLogin
|
<div>
|
||||||
:form-schema="formSchema"
|
<AuthenticationLogin
|
||||||
:loading="authStore.loginLoading"
|
ref="loginRef"
|
||||||
@submit="authStore.authLogin"
|
:form-schema="formSchema"
|
||||||
/>
|
:loading="authStore.loginLoading"
|
||||||
|
@submit="handleLogin"
|
||||||
|
@third-login="handleThirdLogin"
|
||||||
|
/>
|
||||||
|
<Verification
|
||||||
|
ref="verifyRef"
|
||||||
|
v-if="captchaEnable"
|
||||||
|
:captcha-type="captchaType"
|
||||||
|
:check-captcha-api="checkCaptcha"
|
||||||
|
:get-captcha-api="getCaptcha"
|
||||||
|
:img-size="{ width: '400px', height: '200px' }"
|
||||||
|
mode="pop"
|
||||||
|
@on-success="handleVerifySuccess"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,18 +1,126 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { VbenFormSchema } from '@vben/common-ui';
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
import type { Recordable } from '@vben/types';
|
|
||||||
|
|
||||||
import { computed, h, ref } from 'vue';
|
import type { AuthApi } from '#/api/core/auth';
|
||||||
|
|
||||||
import { AuthenticationRegister, z } from '@vben/common-ui';
|
import { computed, h, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { AuthenticationRegister, Verification, z } from '@vben/common-ui';
|
||||||
|
import { isCaptchaEnable, isTenantEnable } from '@vben/hooks';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import {
|
||||||
|
checkCaptcha,
|
||||||
|
getCaptcha,
|
||||||
|
getTenantByWebsite,
|
||||||
|
getTenantSimpleList,
|
||||||
|
} from '#/api/core/auth';
|
||||||
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
defineOptions({ name: 'Register' });
|
defineOptions({ name: 'Register' });
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const tenantEnable = isTenantEnable();
|
||||||
|
const captchaEnable = isCaptchaEnable();
|
||||||
|
|
||||||
|
const registerRef = ref();
|
||||||
|
const verifyRef = ref();
|
||||||
|
|
||||||
|
const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWord'
|
||||||
|
|
||||||
|
/** 获取租户列表,并默认选中 */
|
||||||
|
const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表
|
||||||
|
async function fetchTenantList() {
|
||||||
|
if (!tenantEnable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 获取租户列表、域名对应租户
|
||||||
|
const websiteTenantPromise = getTenantByWebsite(window.location.hostname);
|
||||||
|
tenantList.value = await getTenantSimpleList();
|
||||||
|
|
||||||
|
// 选中租户:域名 > store 中的租户 > 首个租户
|
||||||
|
let tenantId: null | number = null;
|
||||||
|
const websiteTenant = await websiteTenantPromise;
|
||||||
|
if (websiteTenant?.id) {
|
||||||
|
tenantId = websiteTenant.id;
|
||||||
|
}
|
||||||
|
// 如果没有从域名获取到租户,尝试从 store 中获取
|
||||||
|
if (!tenantId && accessStore.tenantId) {
|
||||||
|
tenantId = accessStore.tenantId;
|
||||||
|
}
|
||||||
|
// 如果还是没有租户,使用列表中的第一个
|
||||||
|
if (!tenantId && tenantList.value?.[0]?.id) {
|
||||||
|
tenantId = tenantList.value[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置选中的租户编号
|
||||||
|
accessStore.setTenantId(tenantId);
|
||||||
|
registerRef.value
|
||||||
|
.getFormApi()
|
||||||
|
.setFieldValue('tenantId', tenantId?.toString());
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取租户列表失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 执行注册 */
|
||||||
|
async function handleRegister(values: any) {
|
||||||
|
// 如果开启验证码,则先验证验证码
|
||||||
|
if (captchaEnable) {
|
||||||
|
verifyRef.value.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无验证码,直接登录
|
||||||
|
await authStore.authLogin('register', values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 验证码通过,执行注册 */
|
||||||
|
const handleVerifySuccess = async ({ captchaVerification }: any) => {
|
||||||
|
try {
|
||||||
|
await authStore.authLogin('register', {
|
||||||
|
...(await registerRef.value.getFormApi().getValues()),
|
||||||
|
captchaVerification,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in handleRegister:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 组件挂载时获取租户信息 */
|
||||||
|
onMounted(() => {
|
||||||
|
fetchTenantList();
|
||||||
|
});
|
||||||
|
|
||||||
const formSchema = computed((): VbenFormSchema[] => {
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
component: 'VbenSelect',
|
||||||
|
componentProps: {
|
||||||
|
options: tenantList.value.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id.toString(),
|
||||||
|
})),
|
||||||
|
placeholder: $t('authentication.tenantTip'),
|
||||||
|
},
|
||||||
|
fieldName: 'tenantId',
|
||||||
|
label: $t('authentication.tenant'),
|
||||||
|
rules: z.string().min(1, { message: $t('authentication.tenantTip') }),
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['tenantId'],
|
||||||
|
if: tenantEnable,
|
||||||
|
trigger(values) {
|
||||||
|
if (values.tenantId) {
|
||||||
|
accessStore.setTenantId(Number(values.tenantId));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: 'VbenInput',
|
component: 'VbenInput',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
@@ -22,6 +130,15 @@ const formSchema = computed((): VbenFormSchema[] => {
|
|||||||
label: $t('authentication.username'),
|
label: $t('authentication.username'),
|
||||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('authentication.nicknameTip'),
|
||||||
|
},
|
||||||
|
fieldName: 'nickname',
|
||||||
|
label: $t('authentication.nickname'),
|
||||||
|
rules: z.string().min(1, { message: $t('authentication.nicknameTip') }),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: 'VbenInputPassword',
|
component: 'VbenInputPassword',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
@@ -67,7 +184,7 @@ const formSchema = computed((): VbenFormSchema[] => {
|
|||||||
h(
|
h(
|
||||||
'a',
|
'a',
|
||||||
{
|
{
|
||||||
class: 'vben-link ml-1',
|
class: 'vben-link ml-1 ',
|
||||||
href: '',
|
href: '',
|
||||||
},
|
},
|
||||||
`${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`,
|
`${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`,
|
||||||
@@ -80,17 +197,25 @@ const formSchema = computed((): VbenFormSchema[] => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleSubmit(value: Recordable<any>) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('register submit:', value);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationRegister
|
<div>
|
||||||
:form-schema="formSchema"
|
<AuthenticationRegister
|
||||||
:loading="loading"
|
ref="registerRef"
|
||||||
@submit="handleSubmit"
|
:form-schema="formSchema"
|
||||||
/>
|
:loading="loading"
|
||||||
|
@submit="handleRegister"
|
||||||
|
/>
|
||||||
|
<Verification
|
||||||
|
ref="verifyRef"
|
||||||
|
v-if="captchaEnable"
|
||||||
|
:captcha-type="captchaType"
|
||||||
|
:check-captcha-api="checkCaptcha"
|
||||||
|
:get-captcha-api="getCaptcha"
|
||||||
|
:img-size="{ width: '400px', height: '200px' }"
|
||||||
|
mode="pop"
|
||||||
|
@on-success="handleVerifySuccess"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { Page } from '@vben/common-ui';
|
|
||||||
|
|
||||||
import { NButton, NCard, useMessage } from 'naive-ui';
|
|
||||||
|
|
||||||
import { useVbenForm } from '#/adapter/form';
|
|
||||||
import { getAllMenusApi } from '#/api';
|
|
||||||
|
|
||||||
const message = useMessage();
|
|
||||||
const [Form, formApi] = useVbenForm({
|
|
||||||
commonConfig: {
|
|
||||||
// 所有表单项
|
|
||||||
componentProps: {
|
|
||||||
class: 'w-full',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
layout: 'horizontal',
|
|
||||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
|
||||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
|
||||||
handleSubmit: (values) => {
|
|
||||||
message.success(`表单数据:${JSON.stringify(values)}`);
|
|
||||||
},
|
|
||||||
schema: [
|
|
||||||
{
|
|
||||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
|
||||||
component: 'ApiSelect',
|
|
||||||
// 对应组件的参数
|
|
||||||
componentProps: {
|
|
||||||
// 菜单接口转options格式
|
|
||||||
afterFetch: (data: { name: string; path: string }[]) => {
|
|
||||||
return data.map((item: any) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.path,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
// 菜单接口
|
|
||||||
api: getAllMenusApi,
|
|
||||||
},
|
|
||||||
// 字段名
|
|
||||||
fieldName: 'api',
|
|
||||||
// 界面显示的label
|
|
||||||
label: 'ApiSelect',
|
|
||||||
rules: 'required',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'ApiTreeSelect',
|
|
||||||
// 对应组件的参数
|
|
||||||
componentProps: {
|
|
||||||
// 菜单接口
|
|
||||||
api: getAllMenusApi,
|
|
||||||
childrenField: 'children',
|
|
||||||
// 菜单接口转options格式
|
|
||||||
labelField: 'name',
|
|
||||||
valueField: 'path',
|
|
||||||
},
|
|
||||||
// 字段名
|
|
||||||
fieldName: 'apiTree',
|
|
||||||
// 界面显示的label
|
|
||||||
label: 'ApiTreeSelect',
|
|
||||||
rules: 'required',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'Input',
|
|
||||||
fieldName: 'string',
|
|
||||||
label: 'String',
|
|
||||||
rules: 'required',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'InputNumber',
|
|
||||||
fieldName: 'number',
|
|
||||||
label: 'Number',
|
|
||||||
rules: 'required',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'RadioGroup',
|
|
||||||
fieldName: 'radio',
|
|
||||||
label: 'Radio',
|
|
||||||
componentProps: {
|
|
||||||
options: [
|
|
||||||
{ value: 'A', label: 'A' },
|
|
||||||
{ value: 'B', label: 'B' },
|
|
||||||
{ value: 'C', label: 'C' },
|
|
||||||
{ value: 'D', label: 'D' },
|
|
||||||
{ value: 'E', label: 'E' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
rules: 'selectRequired',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'RadioGroup',
|
|
||||||
fieldName: 'radioButton',
|
|
||||||
label: 'RadioButton',
|
|
||||||
componentProps: {
|
|
||||||
isButton: true,
|
|
||||||
class: 'flex flex-wrap', // 如果选项过多,可以添加class来自动折叠
|
|
||||||
options: [
|
|
||||||
{ value: 'A', label: '选项A' },
|
|
||||||
{ value: 'B', label: '选项B' },
|
|
||||||
{ value: 'C', label: '选项C' },
|
|
||||||
{ value: 'D', label: '选项D' },
|
|
||||||
{ value: 'E', label: '选项E' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
rules: 'selectRequired',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'CheckboxGroup',
|
|
||||||
fieldName: 'checkbox',
|
|
||||||
label: 'Checkbox',
|
|
||||||
componentProps: {
|
|
||||||
options: [
|
|
||||||
{ value: 'A', label: '选项A' },
|
|
||||||
{ value: 'B', label: '选项B' },
|
|
||||||
{ value: 'C', label: '选项C' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
rules: 'selectRequired',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'DatePicker',
|
|
||||||
fieldName: 'date',
|
|
||||||
label: 'Date',
|
|
||||||
rules: 'required',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'Input',
|
|
||||||
fieldName: 'textArea',
|
|
||||||
label: 'TextArea',
|
|
||||||
componentProps: {
|
|
||||||
type: 'textarea',
|
|
||||||
},
|
|
||||||
rules: 'required',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
function setFormValues() {
|
|
||||||
formApi.setValues({
|
|
||||||
string: 'string',
|
|
||||||
number: 123,
|
|
||||||
radio: 'B',
|
|
||||||
radioButton: 'C',
|
|
||||||
checkbox: ['A', 'C'],
|
|
||||||
date: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<Page
|
|
||||||
description="表单适配器重新包装了CheckboxGroup和RadioGroup,可以通过options属性传递选项数据(选项数据将作为子组件的属性)"
|
|
||||||
title="表单演示"
|
|
||||||
>
|
|
||||||
<NCard title="基础表单">
|
|
||||||
<template #header-extra>
|
|
||||||
<NButton type="primary" @click="setFormValues">设置表单值</NButton>
|
|
||||||
</template>
|
|
||||||
<Form />
|
|
||||||
</NCard>
|
|
||||||
</Page>
|
|
||||||
</template>
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { NotificationType } from 'naive-ui';
|
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
|
||||||
|
|
||||||
import { NButton, NCard, NSpace, useMessage, useNotification } from 'naive-ui';
|
|
||||||
|
|
||||||
const notification = useNotification();
|
|
||||||
|
|
||||||
const message = useMessage();
|
|
||||||
function error() {
|
|
||||||
message.error('Once upon a time you dressed so fine');
|
|
||||||
}
|
|
||||||
|
|
||||||
function warning() {
|
|
||||||
message.warning('How many roads must a man walk down');
|
|
||||||
}
|
|
||||||
function success() {
|
|
||||||
message.success('Cause you walked hand in hand With another man in my place');
|
|
||||||
}
|
|
||||||
function loading() {
|
|
||||||
message.loading(
|
|
||||||
'If I were you, I will realize that I love you more than any other guy',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function notify(type: NotificationType) {
|
|
||||||
notification[type]({
|
|
||||||
content: '说点啥呢',
|
|
||||||
duration: 2500,
|
|
||||||
keepAliveOnHover: true,
|
|
||||||
meta: '想不出来',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Page description="支持多语言,主题功能集成切换等" title="naive组件使用演示">
|
|
||||||
<NCard class="mb-5" title="按钮">
|
|
||||||
<NSpace>
|
|
||||||
<NButton>Default</NButton>
|
|
||||||
<NButton type="tertiary"> Tertiary </NButton>
|
|
||||||
<NButton type="primary"> Primary </NButton>
|
|
||||||
<NButton type="info"> Info </NButton>
|
|
||||||
<NButton type="success"> Success </NButton>
|
|
||||||
<NButton type="warning"> Warning </NButton>
|
|
||||||
<NButton type="error"> Error </NButton>
|
|
||||||
</NSpace>
|
|
||||||
</NCard>
|
|
||||||
|
|
||||||
<NCard class="mb-5" title="Message">
|
|
||||||
<NSpace>
|
|
||||||
<NButton type="error" @click="error"> 错误 </NButton>
|
|
||||||
<NButton type="warning" @click="warning"> 警告 </NButton>
|
|
||||||
<NButton type="success" @click="success"> 成功 </NButton>
|
|
||||||
<NButton type="primary" @click="loading"> 加载中 </NButton>
|
|
||||||
</NSpace>
|
|
||||||
</NCard>
|
|
||||||
|
|
||||||
<NCard class="mb-5" title="Notification">
|
|
||||||
<NSpace>
|
|
||||||
<NButton type="error" @click="notify('error')"> 错误 </NButton>
|
|
||||||
<NButton type="warning" @click="notify('warning')"> 警告 </NButton>
|
|
||||||
<NButton type="success" @click="notify('success')"> 成功 </NButton>
|
|
||||||
<NButton type="primary" @click="notify('info')"> 加载中 </NButton>
|
|
||||||
</NSpace>
|
|
||||||
</NCard>
|
|
||||||
</Page>
|
|
||||||
</template>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
|
||||||
|
|
||||||
import { NDataTable } from 'naive-ui';
|
|
||||||
|
|
||||||
const columns = ref([
|
|
||||||
{
|
|
||||||
key: 'no',
|
|
||||||
title: 'No',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'title',
|
|
||||||
title: 'Title',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'length',
|
|
||||||
title: 'Length',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const data = [
|
|
||||||
{ length: '4:18', no: 3, title: 'Wonderwall' },
|
|
||||||
{ length: '4:48', no: 4, title: "Don't Look Back in Anger" },
|
|
||||||
{ length: '7:27', no: 12, title: 'Champagne Supernova' },
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Page
|
|
||||||
description="表单页用于向用户收集或验证信息,基础表单常见于数据项较少的表单场景。"
|
|
||||||
title="NDataTable"
|
|
||||||
>
|
|
||||||
<NDataTable :columns="columns" :data="data" />
|
|
||||||
</Page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
Reference in New Issue
Block a user