diff --git a/apps/web-antd/src/locales/langs/en-US/demos.json b/apps/web-antd/src/locales/langs/en-US/demos.json deleted file mode 100644 index 07156434..00000000 --- a/apps/web-antd/src/locales/langs/en-US/demos.json +++ /dev/null @@ -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" - } -} diff --git a/apps/web-antd/src/locales/langs/zh-CN/demos.json b/apps/web-antd/src/locales/langs/zh-CN/demos.json deleted file mode 100644 index 93ee722f..00000000 --- a/apps/web-antd/src/locales/langs/zh-CN/demos.json +++ /dev/null @@ -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 版本" - } -} diff --git a/apps/web-naive/src/adapter/form.ts b/apps/web-naive/src/adapter/form.ts index 2f2ed2ab..cc3435f0 100644 --- a/apps/web-naive/src/adapter/form.ts +++ b/apps/web-naive/src/adapter/form.ts @@ -8,6 +8,9 @@ import type { ComponentType } from './component'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; import { $t } from '@vben/locales'; +/** 手机号正则表达式(中国) */ +const MOBILE_REGEX = /(?:0|86|\+86)?1[3-9]\d{9}/; + setupVbenForm({ config: { // naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效 @@ -32,6 +35,25 @@ setupVbenForm({ } 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; + }, }, }); diff --git a/apps/web-naive/src/adapter/vxe-table.ts b/apps/web-naive/src/adapter/vxe-table.ts index 081cfb29..1ab20c53 100644 --- a/apps/web-naive/src/adapter/vxe-table.ts +++ b/apps/web-naive/src/adapter/vxe-table.ts @@ -1,8 +1,16 @@ +import type { Recordable } from '@vben/types'; + import { h } from 'vue'; +import { IconifyIcon } from '@vben/icons'; +import { $te } from '@vben/locales'; 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'; @@ -20,16 +28,32 @@ setupVbenVxeTable({ // 全局禁用vxe-table的表单配置,使用formOptions enabled: false, }, + toolbarConfig: { + import: false, // 是否导入 + export: false, // 是否导出 + refresh: true, // 是否刷新 + print: false, // 是否打印 + zoom: true, // 是否缩放 + custom: true, // 是否自定义配置 + }, + customConfig: { + mode: 'modal', + }, proxyConfig: { autoLoad: true, response: { - result: 'items', + result: 'list', total: 'total', - list: 'items', }, showActiveMsg: true, showResponseMsg: false, }, + pagerConfig: { + enabled: true, + }, + sortConfig: { + multiple: true, + }, round: true, showOverflow: true, 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> = { + delete: { + danger: true, + text: $t('common.delete'), + }, + edit: { + text: $t('common.edit'), + }, + }; + const operations: Array> = ( + 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 = {}; + 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, 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) { + 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 的全局配置,比如自定义格式化 // 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, }); 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> = { + code: string; + row: T; +}; +export type OnActionClickFn> = ( + params: OnActionClickParams, +) => void; export type * from '@vben/plugins/vxe-table'; diff --git a/apps/web-naive/src/api/core/auth.ts b/apps/web-naive/src/api/core/auth.ts index 71d9f994..ccb6da34 100644 --- a/apps/web-naive/src/api/core/auth.ts +++ b/apps/web-naive/src/api/core/auth.ts @@ -1,3 +1,5 @@ +import type { AuthPermissionInfo } from '@vben/types'; + import { baseRequestClient, requestClient } from '#/api/request'; export namespace AuthApi { @@ -5,47 +7,151 @@ export namespace AuthApi { export interface LoginParams { password?: string; username?: string; + captchaVerification?: string; + // 绑定社交登录时,需要传递如下参数 + socialType?: number; + socialCode?: string; + socialState?: string; } /** 登录接口返回值 */ export interface LoginResult { accessToken: string; + refreshToken: string; + userId: number; + expiresTime: number; } - export interface RefreshTokenResult { - data: string; - status: number; + /** 租户信息返回值 */ + export interface TenantResult { + 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) { - return requestClient.post('/auth/login', data); + return requestClient.post('/system/auth/login', data); } -/** - * 刷新accessToken - */ -export async function refreshTokenApi() { - return baseRequestClient.post('/auth/refresh', { - withCredentials: true, +/** 刷新 accessToken */ +export async function refreshTokenApi(refreshToken: string) { + return baseRequestClient.post( + `/system/auth/refresh-token?refreshToken=${refreshToken}`, + ); +} + +/** 退出登录 */ +export async function logoutApi(accessToken: string) { + return baseRequestClient.post( + '/system/auth/logout', + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); +} + +/** 获取权限信息 */ +export async function getAuthPermissionInfoApi() { + return requestClient.get( + '/system/auth/get-permission-info', + ); +} + +/** 获取租户列表 */ +export async function getTenantSimpleList() { + return requestClient.get( + `/system/tenant/simple-list`, + ); +} + +/** 使用租户域名,获得租户信息 */ +export async function getTenantByWebsite(website: string) { + return requestClient.get( + `/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 logoutApi() { - return baseRequestClient.post('/auth/logout', { - withCredentials: true, - }); -} - -/** - * 获取用户权限码 - */ -export async function getAccessCodesApi() { - return requestClient.get('/auth/codes'); +/** 社交快捷登录 */ +export async function socialLogin(data: AuthApi.SocialLoginParams) { + return requestClient.post( + '/system/auth/social-login', + data, + ); } diff --git a/apps/web-naive/src/api/core/index.ts b/apps/web-naive/src/api/core/index.ts index 28a5aef4..269586ee 100644 --- a/apps/web-naive/src/api/core/index.ts +++ b/apps/web-naive/src/api/core/index.ts @@ -1,3 +1 @@ export * from './auth'; -export * from './menu'; -export * from './user'; diff --git a/apps/web-naive/src/api/core/menu.ts b/apps/web-naive/src/api/core/menu.ts deleted file mode 100644 index 9ef60b11..00000000 --- a/apps/web-naive/src/api/core/menu.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { RouteRecordStringComponent } from '@vben/types'; - -import { requestClient } from '#/api/request'; - -/** - * 获取用户所有菜单 - */ -export async function getAllMenusApi() { - return requestClient.get('/menu/all'); -} diff --git a/apps/web-naive/src/api/core/user.ts b/apps/web-naive/src/api/core/user.ts deleted file mode 100644 index 7e28ea84..00000000 --- a/apps/web-naive/src/api/core/user.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { UserInfo } from '@vben/types'; - -import { requestClient } from '#/api/request'; - -/** - * 获取用户信息 - */ -export async function getUserInfoApi() { - return requestClient.get('/user/info'); -} diff --git a/apps/web-naive/src/api/request.ts b/apps/web-naive/src/api/request.ts index f8fbacc0..737344bb 100644 --- a/apps/web-naive/src/api/request.ts +++ b/apps/web-naive/src/api/request.ts @@ -3,7 +3,7 @@ */ import type { RequestClientOptions } from '@vben/request'; -import { useAppConfig } from '@vben/hooks'; +import { isTenantEnable, useAppConfig } from '@vben/hooks'; import { preferences } from '@vben/preferences'; import { authenticateResponseInterceptor, @@ -19,6 +19,7 @@ import { useAuthStore } from '#/store'; import { refreshTokenApi } from './core'; const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); +const tenantEnable = isTenantEnable(); function createRequestClient(baseURL: string, options?: RequestClientOptions) { const client = new RequestClient({ @@ -49,8 +50,16 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { */ async function doRefreshToken() { const accessStore = useAccessStore(); - const resp = await refreshTokenApi(); - const newToken = resp.data; + const refreshToken = accessStore.refreshToken as string; + 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); return newToken; } @@ -66,6 +75,14 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { config.headers.Authorization = formatToken(accessStore.accessToken); 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; }, }); @@ -96,7 +113,12 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { // 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg // 当前mock接口返回的错误字段是 error 或者 message 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); }), @@ -110,3 +132,17 @@ export const requestClient = createRequestClient(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; + }, +}); diff --git a/apps/web-naive/src/layouts/basic.vue b/apps/web-naive/src/layouts/basic.vue index 69189384..2777f4a3 100644 --- a/apps/web-naive/src/layouts/basic.vue +++ b/apps/web-naive/src/layouts/basic.vue @@ -1,12 +1,17 @@