diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index ca88bf1ac..2b60ef06e 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -43,6 +43,7 @@ "@vueuse/core": "catalog:", "ant-design-vue": "catalog:", "dayjs": "catalog:", + "highlight.js": "catalog:", "pinia": "catalog:", "vue": "catalog:", "vue-router": "catalog:" diff --git a/apps/web-antd/src/api/core/auth.ts b/apps/web-antd/src/api/core/auth.ts index b9e6fd86a..809f29cdc 100644 --- a/apps/web-antd/src/api/core/auth.ts +++ b/apps/web-antd/src/api/core/auth.ts @@ -7,6 +7,10 @@ export namespace AuthApi { password?: string; username?: string; captchaVerification?: string; + // 绑定社交登录时,需要传递如下参数 + socialType?: number; + socialCode?: string; + socialState?: string; } /** 登录接口返回值 */ @@ -37,11 +41,24 @@ export namespace AuthApi { /** 注册接口参数 */ export interface RegisterParams { - tenantName: string username: string password: string captchaVerification: string } + + /** 重置密码接口参数 */ + export interface ResetPasswordParams { + password: string; + mobile: string; + code: string; + } + + /** 社交快捷登录接口参数 */ + export interface SocialLoginParams { + type: number; + code: string; + state: string; + } } /** 登录 */ @@ -105,4 +122,24 @@ export const smsLogin = (data: AuthApi.SmsLoginParams) => { /** 注册 */ export const register = (data: AuthApi.RegisterParams) => { return requestClient.post('/system/auth/register', data) -} \ No newline at end of file +} + +/** 通过短信重置密码 */ +export const smsResetPassword = (data: AuthApi.ResetPasswordParams) => { + return requestClient.post('/system/auth/reset-password', data) +} + +/** 社交授权的跳转 */ +export const socialAuthRedirect = (type: number, redirectUri: string) => { + return requestClient.get('/system/auth/social-auth-redirect', { + params: { + type, + redirectUri, + }, + }); +} + +/** 社交快捷登录 */ +export const socialLogin = (data: AuthApi.SocialLoginParams) => { + return requestClient.post('/system/auth/social-login', data); +} diff --git a/apps/web-antd/src/api/infra/codegen/index.ts b/apps/web-antd/src/api/infra/codegen/index.ts new file mode 100644 index 000000000..e621a2460 --- /dev/null +++ b/apps/web-antd/src/api/infra/codegen/index.ts @@ -0,0 +1,145 @@ +import type { PageParam, PageResult } from '@vben/request'; + +import { requestClient } from '#/api/request'; + +export namespace InfraCodegenApi { + /** 代码生成表定义 */ + export interface CodegenTable { + id: number; + tableId: number; + isParentMenuIdValid: boolean; + dataSourceConfigId: number; + scene: number; + tableName: string; + tableComment: string; + remark: string; + moduleName: string; + businessName: string; + className: string; + classComment: string; + author: string; + createTime: Date; + updateTime: Date; + templateType: number; + parentMenuId: number; + } + + /** 代码生成字段定义 */ + export interface CodegenColumn { + id: number; + tableId: number; + columnName: string; + dataType: string; + columnComment: string; + nullable: number; + primaryKey: number; + ordinalPosition: number; + javaType: string; + javaField: string; + dictType: string; + example: string; + createOperation: number; + updateOperation: number; + listOperation: number; + listOperationCondition: string; + listOperationResult: number; + htmlType: string; + } + + /** 数据库表定义 */ + export interface DatabaseTable { + name: string; + comment: string; + } + + /** 代码生成详情 */ + export interface CodegenDetail { + table: CodegenTable; + columns: CodegenColumn[]; + } + + /** 代码预览 */ + export interface CodegenPreview { + filePath: string; + code: string; + } + + /** 更新代码生成请求 */ + export interface CodegenUpdateReq { + table: any | CodegenTable; + columns: CodegenColumn[]; + } + + /** 创建代码生成请求 */ + export interface CodegenCreateListReq { + dataSourceConfigId?: number; + tableNames: string[]; + } +} + +/** 查询列表代码生成表定义 */ +export function getCodegenTableList(dataSourceConfigId: number) { + return requestClient.get('/infra/codegen/table/list', { + params: { dataSourceConfigId }, + }); +} + +/** 查询列表代码生成表定义 */ +export function getCodegenTablePage(params: PageParam) { + return requestClient.get>('/infra/codegen/table/page', { params }); +} + +/** 查询详情代码生成表定义 */ +export function getCodegenTable(id: number) { + return requestClient.get('/infra/codegen/detail', { + params: { tableId: id }, + }); +} + +/** 新增代码生成表定义 */ +export function createCodegenTable(data: InfraCodegenApi.CodegenCreateListReq) { + return requestClient.post('/infra/codegen/create', data); +} + +/** 修改代码生成表定义 */ +export function updateCodegenTable(data: InfraCodegenApi.CodegenUpdateReq) { + return requestClient.put('/infra/codegen/update', data); +} + +/** 基于数据库的表结构,同步数据库的表和字段定义 */ +export function syncCodegenFromDB(id: number) { + return requestClient.put('/infra/codegen/sync-from-db', { + params: { tableId: id }, + }); +} + +/** 预览生成代码 */ +export function previewCodegen(id: number) { + return requestClient.get('/infra/codegen/preview', { + params: { tableId: id }, + }); +} + +/** 下载生成代码 */ +export function downloadCodegen(id: number) { + return requestClient.download('/infra/codegen/download', { + params: { tableId: id }, + }); +} + +/** 获得表定义 */ +export function getSchemaTableList(params: any) { + return requestClient.get('/infra/codegen/db/table/list', { params }); +} + +/** 基于数据库的表结构,创建代码生成器的表定义 */ +export function createCodegenList(data: InfraCodegenApi.CodegenCreateListReq) { + return requestClient.post('/infra/codegen/create-list', data); +} + +/** 删除代码生成表定义 */ +export function deleteCodegenTable(id: number) { + return requestClient.delete('/infra/codegen/delete', { + params: { tableId: id }, + }); +} diff --git a/apps/web-antd/src/api/system/mail/account/index.ts b/apps/web-antd/src/api/system/mail/account/index.ts index 5dcf23115..dc488c35c 100644 --- a/apps/web-antd/src/api/system/mail/account/index.ts +++ b/apps/web-antd/src/api/system/mail/account/index.ts @@ -18,36 +18,36 @@ export namespace SystemMailAccountApi { remark: string; } } -// TODO @puhui999:改成 function 风格;不用 await + /** 查询邮箱账号列表 */ -export const getMailAccountPage = async (params: PageParam) => { - return await requestClient.get>( +export function getMailAccountPage(params: PageParam) { + return requestClient.get>( '/system/mail-account/page', - { params }, + { params } ); -}; +} /** 查询邮箱账号详情 */ -export const getMailAccount = async (id: number) => { - return await requestClient.get(`/system/mail-account/get?id=${id}`); -}; +export function getMailAccount(id: number) { + return requestClient.get(`/system/mail-account/get?id=${id}`); +} /** 新增邮箱账号 */ -export const createMailAccount = async (data: SystemMailAccountApi.SystemMailAccount) => { - return await requestClient.post('/system/mail-account/create', data); -}; +export function createMailAccount(data: SystemMailAccountApi.SystemMailAccount) { + return requestClient.post('/system/mail-account/create', data); +} /** 修改邮箱账号 */ -export const updateMailAccount = async (data: SystemMailAccountApi.SystemMailAccount) => { - return await requestClient.put('/system/mail-account/update', data); -}; +export function updateMailAccount(data: SystemMailAccountApi.SystemMailAccount) { + return requestClient.put('/system/mail-account/update', data); +} /** 删除邮箱账号 */ -export const deleteMailAccount = async (id: number) => { - return await requestClient.delete(`/system/mail-account/delete?id=${id}`); -}; +export function deleteMailAccount(id: number) { + return requestClient.delete(`/system/mail-account/delete?id=${id}`); +} /** 获得邮箱账号精简列表 */ -export const getSimpleMailAccountList = async () => { - return await requestClient.get('/system/mail-account/simple-list'); -}; +export function getSimpleMailAccountList() { + return requestClient.get('/system/mail-account/simple-list'); +} diff --git a/apps/web-antd/src/api/system/mail/log/index.ts b/apps/web-antd/src/api/system/mail/log/index.ts index e321fede0..b328d2ce9 100644 --- a/apps/web-antd/src/api/system/mail/log/index.ts +++ b/apps/web-antd/src/api/system/mail/log/index.ts @@ -24,21 +24,21 @@ export namespace SystemMailLogApi { createTime: string; } } -// TODO @puhui999:改成 function 风格;不用 await + /** 查询邮件日志列表 */ -export const getMailLogPage = async (params: PageParam) => { - return await requestClient.get>( +export function getMailLogPage(params: PageParam) { + return requestClient.get>( '/system/mail-log/page', { params } ); -}; +} /** 查询邮件日志详情 */ -export const getMailLog = async (id: number) => { - return await requestClient.get(`/system/mail-log/get?${id}`); -}; +export function getMailLog(id: number) { + return requestClient.get(`/system/mail-log/get?id=${id}`); +} /** 重新发送邮件 */ -export const resendMail = async (id: number) => { - return await requestClient.put(`/system/mail-log/resend?id=${id}`); -}; +export function resendMail(id: number) { + return requestClient.put(`/system/mail-log/resend?id=${id}`); +} diff --git a/apps/web-antd/src/api/system/mail/template/index.ts b/apps/web-antd/src/api/system/mail/template/index.ts index 0bf98d2d0..6dc0d2fee 100644 --- a/apps/web-antd/src/api/system/mail/template/index.ts +++ b/apps/web-antd/src/api/system/mail/template/index.ts @@ -25,36 +25,36 @@ export namespace SystemMailTemplateApi { templateParams: Record; } } -// TODO @puhui999:改成 function 风格;不用 await + /** 查询邮件模版列表 */ -export const getMailTemplatePage = async (params: PageParam) => { - return await requestClient.get>( +export function getMailTemplatePage(params: PageParam) { + return requestClient.get>( '/system/mail-template/page', { params } ); -}; +} /** 查询邮件模版详情 */ -export const getMailTemplate = async (id: number) => { - return await requestClient.get(`/system/mail-template/get?id=${id}`); -}; +export function getMailTemplate(id: number) { + return requestClient.get(`/system/mail-template/get?id=${id}`); +} /** 新增邮件模版 */ -export const createMailTemplate = async (data: SystemMailTemplateApi.SystemMailTemplate) => { - return await requestClient.post('/system/mail-template/create', data); -}; +export function createMailTemplate(data: SystemMailTemplateApi.SystemMailTemplate) { + return requestClient.post('/system/mail-template/create', data); +} /** 修改邮件模版 */ -export const updateMailTemplate = async (data: SystemMailTemplateApi.SystemMailTemplate) => { - return await requestClient.put('/system/mail-template/update', data); -}; +export function updateMailTemplate(data: SystemMailTemplateApi.SystemMailTemplate) { + return requestClient.put('/system/mail-template/update', data); +} /** 删除邮件模版 */ -export const deleteMailTemplate = async (id: number) => { - return await requestClient.delete(`/system/mail-template/delete?id=${id}`); -}; +export function deleteMailTemplate(id: number) { + return requestClient.delete(`/system/mail-template/delete?id=${id}`); +} /** 发送邮件 */ -export const sendMail = async (data: SystemMailTemplateApi.MailSendReqVO) => { - return await requestClient.post('/system/mail-template/send-mail', data); -}; +export function sendMail(data: SystemMailTemplateApi.MailSendReqVO) { + return requestClient.post('/system/mail-template/send-mail', data); +} diff --git a/apps/web-antd/src/router/routes/core.ts b/apps/web-antd/src/router/routes/core.ts index 7218da228..9db6312d8 100644 --- a/apps/web-antd/src/router/routes/core.ts +++ b/apps/web-antd/src/router/routes/core.ts @@ -89,6 +89,14 @@ const coreRoutes: RouteRecordRaw[] = [ title: $t('page.auth.register'), }, }, + { + name: 'SocialLogin', + path: 'social-login', + component: () => import('#/views/_core/authentication/social-login.vue'), + meta: { + title: $t('page.auth.login'), + }, + }, ], }, ]; diff --git a/apps/web-antd/src/router/routes/modules/infra.ts b/apps/web-antd/src/router/routes/modules/infra.ts index 510bf8878..0f32b8db6 100644 --- a/apps/web-antd/src/router/routes/modules/infra.ts +++ b/apps/web-antd/src/router/routes/modules/infra.ts @@ -11,8 +11,30 @@ const routes: RouteRecordRaw[] = [ activePath: '/infra/job', keepAlive: false, hideInMenu: true, - } - } + }, + }, + { + path: '/codegen', + name: 'CodegenEdit', + meta: { + icon: 'ic:baseline-view-in-ar', + keepAlive: true, + order: 1000, + title: '代码生成', + hideInMenu: true, + }, + children: [ + { + path: '/codegen/edit', + name: 'InfraCodegenEdit', + component: () => import('#/views/infra/codegen/edit.vue'), + meta: { + title: '修改生成配置', + activeMenu: '/infra/codegen', + }, + }, + ], + }, ]; export default routes; diff --git a/apps/web-antd/src/store/auth.ts b/apps/web-antd/src/store/auth.ts index a66737a27..7f91d4639 100644 --- a/apps/web-antd/src/store/auth.ts +++ b/apps/web-antd/src/store/auth.ts @@ -9,7 +9,7 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; import { notification } from 'ant-design-vue'; import { defineStore } from 'pinia'; -import { type AuthApi, getAuthPermissionInfoApi, loginApi, logoutApi, smsLogin, register } from '#/api'; +import { type AuthApi, getAuthPermissionInfoApi, loginApi, logoutApi, smsLogin, register, socialLogin } from '#/api'; import { $t } from '#/locales'; export const useAuthStore = defineStore('auth', () => { @@ -27,7 +27,7 @@ export const useAuthStore = defineStore('auth', () => { * @param onSuccess 登录成功后的回调函数 */ async function authLogin( - type: 'mobile' | 'username' | 'register', + type: 'mobile' | 'username' | 'register' | 'social', params: Recordable, onSuccess?: () => Promise | void, ) { @@ -37,6 +37,7 @@ export const useAuthStore = defineStore('auth', () => { loginLoading.value = true; const { accessToken, refreshToken } = type === 'mobile' ? await smsLogin(params as AuthApi.SmsLoginParams) : type === 'register' ? await register(params as AuthApi.RegisterParams) + : type === 'social' ? await socialLogin(params as AuthApi.SocialLoginParams) : await loginApi(params); // 如果成功获取到 accessToken diff --git a/apps/web-antd/src/utils/constants.ts b/apps/web-antd/src/utils/constants.ts index 39803bb7d..b79660a95 100644 --- a/apps/web-antd/src/utils/constants.ts +++ b/apps/web-antd/src/utils/constants.ts @@ -72,7 +72,7 @@ export const SystemUserSocialTypeEnum = { export const InfraCodegenTemplateTypeEnum = { CRUD: 1, // 基础 CRUD TREE: 2, // 树形 CRUD - SUB: 3 // 主子表 CRUD + SUB: 15 // 主子表 CRUD } /** diff --git a/apps/web-antd/src/utils/date.ts b/apps/web-antd/src/utils/date.ts index c2704e1dd..d0230319c 100644 --- a/apps/web-antd/src/utils/date.ts +++ b/apps/web-antd/src/utils/date.ts @@ -2,9 +2,8 @@ import dayjs from 'dayjs'; // TODO @芋艿:后续整理下 -// TODO @puhui999:转成 function 方式哈 /** 时间段选择器拓展 */ -export const getRangePickerDefaultProps = () => { +export function getRangePickerDefaultProps() { return { showTime: { format: 'HH:mm:ss', @@ -16,23 +15,16 @@ export const getRangePickerDefaultProps = () => { valueFormat: 'YYYY-MM-DD HH:mm:ss', format: 'YYYY-MM-DD HH:mm:ss', placeholder: ['开始时间', '结束时间'], + // prettier-ignore ranges: { - '今天': [dayjs().startOf('day'), dayjs().endOf('day')], - '昨天': [ - dayjs().subtract(1, 'day').startOf('day'), - dayjs().subtract(1, 'day').endOf('day'), - ], - '本周': [dayjs().startOf('week'), dayjs().endOf('day')], - '本月': [dayjs().startOf('month'), dayjs().endOf('day')], - '最近 7 天': [ - dayjs().subtract(7, 'day').startOf('day'), - dayjs().endOf('day'), - ], - '最近 30 天': [ - dayjs().subtract(30, 'day').startOf('day'), - dayjs().endOf('day'), - ], - }, + '今天': [dayjs().startOf('day'), dayjs().endOf('day')], + '昨天': [dayjs().subtract(1, 'day').startOf('day'), + dayjs().subtract(1, 'day').endOf('day')], + '本周': [dayjs().startOf('week'), dayjs().endOf('day')], + '本月': [dayjs().startOf('month'), dayjs().endOf('day')], + '最近 7 天': [dayjs().subtract(7, 'day').startOf('day'), dayjs().endOf('day')], + '最近 30 天': [dayjs().subtract(30, 'day').startOf('day'), dayjs().endOf('day')], + }, transformDateFunc: (dates: any) => { if (dates && dates.length === 2) { return [dates.createTime[0], dates.createTime[1]].join(','); // 格式化为后台支持的时间格式 @@ -40,4 +32,4 @@ export const getRangePickerDefaultProps = () => { return {}; }, }; -}; +} diff --git a/apps/web-antd/src/views/_core/authentication/forget-password.vue b/apps/web-antd/src/views/_core/authentication/forget-password.vue index fef0d4279..d853a52a7 100644 --- a/apps/web-antd/src/views/_core/authentication/forget-password.vue +++ b/apps/web-antd/src/views/_core/authentication/forget-password.vue @@ -2,40 +2,212 @@ import type { VbenFormSchema } from '@vben/common-ui'; import type { Recordable } from '@vben/types'; -import { computed, ref } from 'vue'; +import { computed, ref, onMounted, h } from 'vue'; import { AuthenticationForgetPassword, z } from '@vben/common-ui'; import { $t } from '@vben/locales'; +import { type AuthApi, sendSmsCode, smsResetPassword } from '#/api'; +import { useAppConfig } from '@vben/hooks'; +import { message } from 'ant-design-vue'; +import { useRouter } from 'vue-router'; + +import { getTenantSimpleList, getTenantByWebsite } from '#/api/core/auth'; +import { useAccessStore } from '@vben/stores'; defineOptions({ name: 'ForgetPassword' }); +const { tenantEnable } = useAppConfig(import.meta.env, import.meta.env.PROD); +const accessStore = useAccessStore(); +const router = useRouter(); + const loading = ref(false); +const CODE_LENGTH = 4; +const forgetPasswordRef = ref(); + +/** 获取租户列表,并默认选中 */ +const tenantList = ref([]); // 租户列表 +const fetchTenantList = async () => { + if (!tenantEnable) { + return; + } + try { + // 获取租户列表、域名对应租户 + const websiteTenantPromise = getTenantByWebsite(window.location.hostname); + tenantList.value = await getTenantSimpleList(); + + // 选中租户:域名 > store 中的租户 > 首个租户 + let tenantId: number | null = 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); + } catch (error) { + console.error('获取租户列表失败:', error); + } +}; + +/** 组件挂载时获取租户信息 */ +onMounted(() => { + fetchTenantList(); +}); const formSchema = computed((): VbenFormSchema[] => { return [ + { + component: 'VbenSelect', + componentProps: { + options: tenantList.value.map((item) => ({ + label: item.name, + value: item.id, + })), + placeholder: $t('authentication.tenantTip'), + }, + fieldName: 'tenantId', + label: $t('authentication.tenant'), + rules: z + .number() + .nullable() + .refine((val) => val != null && val > 0, $t('authentication.tenantTip')) + .default(null), + dependencies: { + triggerFields: ['tenantId'], + if: tenantEnable, + trigger(values) { + if (values.tenantId) { + accessStore.setTenantId(values.tenantId); + } + }, + }, + }, { component: 'VbenInput', componentProps: { - placeholder: 'example@example.com', + placeholder: $t('authentication.mobile'), }, - fieldName: 'email', - label: $t('authentication.email'), + fieldName: 'mobile', + label: $t('authentication.mobile'), rules: z .string() - .min(1, { message: $t('authentication.emailTip') }) - .email($t('authentication.emailValidErrorTip')), + .min(1, { message: $t('authentication.mobileTip') }) + .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) { - // eslint-disable-next-line no-console - console.log('reset email:', value); +/** + * 处理重置密码操作 + * @param values 表单数据 + */ +async function handleSubmit(values: Recordable) { + 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; + } }