diff --git a/apps/vben5/.gitignore b/apps/vben5/.gitignore index c2a8a771f..8cb16e7e0 100644 --- a/apps/vben5/.gitignore +++ b/apps/vben5/.gitignore @@ -19,6 +19,7 @@ coverage dev-dist .stylelintcache yarn.lock +pnpm-lock.yaml package-lock.json .VSCodeCounter **/backend-mock/data @@ -49,3 +50,4 @@ vite.config.ts.* *.sln *.sw? .history +.cursor diff --git a/apps/vben5/.node-version b/apps/vben5/.node-version index ee5c24469..85e502778 100644 --- a/apps/vben5/.node-version +++ b/apps/vben5/.node-version @@ -1 +1 @@ -22.1.0 +22.22.0 diff --git a/apps/vben5/.npmrc b/apps/vben5/.npmrc index 21147aff2..aeac1ae91 100644 --- a/apps/vben5/.npmrc +++ b/apps/vben5/.npmrc @@ -1,4 +1,4 @@ -registry = "https://registry.npmmirror.com" +registry=https://registry.npmmirror.com public-hoist-pattern[]=lefthook public-hoist-pattern[]=eslint public-hoist-pattern[]=prettier diff --git a/apps/vben5/.prettierignore b/apps/vben5/.prettierignore index d0b0ca133..7c572fd66 100644 --- a/apps/vben5/.prettierignore +++ b/apps/vben5/.prettierignore @@ -16,3 +16,5 @@ CODEOWNERS public .npmrc *-lock.yaml + +packages/@abp/components diff --git a/apps/vben5/.stylelintignore b/apps/vben5/.stylelintignore index f4b2db2c1..339bcf6c2 100644 --- a/apps/vben5/.stylelintignore +++ b/apps/vben5/.stylelintignore @@ -2,3 +2,4 @@ dist public __tests__ coverage +packages/@abp/components diff --git a/apps/vben5/.vscode/settings.json b/apps/vben5/.vscode/settings.json index f38c42781..8da37dc96 100644 --- a/apps/vben5/.vscode/settings.json +++ b/apps/vben5/.vscode/settings.json @@ -180,7 +180,8 @@ "markdown", "json", "jsonc", - "json5" + "json5", + "yaml" ], "tailwindCSS.experimental.classRegex": [ @@ -226,16 +227,5 @@ "commentTranslate.multiLineMerge": true, "vue.server.hybridMode": true, "typescript.tsdk": "node_modules/typescript/lib", - "oxc.enable": false, - "cSpell.words": [ - "archiver", - "axios", - "dotenv", - "isequal", - "jspm", - "napi", - "nolebase", - "rollup", - "vitest" - ] + "oxc.enable": false } diff --git a/apps/vben5/README.ja-JP.md b/apps/vben5/README.ja-JP.md index f7847a1d9..4ce285a74 100644 --- a/apps/vben5/README.ja-JP.md +++ b/apps/vben5/README.ja-JP.md @@ -140,8 +140,12 @@ pnpm build ## 貢献者 + + Contribution Leaderboard + + - Contributors + Contributors ## Discord diff --git a/apps/vben5/README.md b/apps/vben5/README.md index e027949ab..ce8e89758 100644 --- a/apps/vben5/README.md +++ b/apps/vben5/README.md @@ -10,7 +10,7 @@

Vue Vben Admin

-[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) ![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg) ![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg) ![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg) ![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) [![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml) [![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml) [![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml) [![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml) **English** | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md) @@ -140,8 +140,12 @@ If you think this project is helpful to you, you can help the author buy a cup o ## Contributors + + Contribution Leaderboard + + - Contributors + Contributors ## Discord diff --git a/apps/vben5/README.zh-CN.md b/apps/vben5/README.zh-CN.md index 5a6b191b8..d3193ef65 100644 --- a/apps/vben5/README.zh-CN.md +++ b/apps/vben5/README.zh-CN.md @@ -140,8 +140,12 @@ pnpm build ## 贡献者 + + Contribution Leaderboard + + - Contributors + Contributors ## Discord diff --git a/apps/vben5/apps/app-antd/.env.development b/apps/vben5/apps/app-antd/.env.development index b9e43314c..b638cd47c 100644 --- a/apps/vben5/apps/app-antd/.env.development +++ b/apps/vben5/apps/app-antd/.env.development @@ -16,12 +16,12 @@ VITE_DEVTOOLS=false VITE_INJECT_APP_LOADING=true # 是否仅允许OIDC登录 -VITE_GLOB_ONLY_OIDC=false +VITE_GLOB_AUTH_ONLY_OIDC=false # 认证服务器 -VITE_GLOB_AUTHORITY="http://localhost:30000" +VITE_GLOB_AUTH_AUTHORITY="http://localhost:44385" # 授权范围 -VITE_GLOB_AUDIENCE="openid email address phone profile offline_access lingyun-abp-application" +VITE_GLOB_AUTH_AUDIENCE="openid email address phone profile offline_access lingyun-abp-application" # 客户端Id -VITE_GLOB_CLIENT_ID=vue-admin-client +VITE_GLOB_AUTH_CLIENT_ID=vue-admin-client # 客户端密钥【生产环境请勿设置此值,建议启用仅允许OIDC登录,将使用授权码类型登录】 -VITE_GLOB_CLIENT_SECRET=1q2w3e* +VITE_GLOB_AUTH_CLIENT_SECRET=1q2w3e* diff --git a/apps/vben5/apps/app-antd/.env.production b/apps/vben5/apps/app-antd/.env.production index 40c957808..169103a71 100644 --- a/apps/vben5/apps/app-antd/.env.production +++ b/apps/vben5/apps/app-antd/.env.production @@ -20,13 +20,13 @@ VITE_INJECT_APP_LOADING=true VITE_ARCHIVER=true # 是否仅允许OIDC登录 -VITE_GLOB_ONLY_OIDC=false +VITE_GLOB_AUTH_ONLY_OIDC=false # 认证服务器 -VITE_GLOB_AUTHORITY="http://127.0.0.1:30001" +VITE_GLOB_AUTH_AUTHORITY="http://127.0.0.1:30001" # 授权范围 -VITE_GLOB_AUDIENCE="openid email address phone profile offline_access lingyun-abp-application" +VITE_GLOB_AUTH_AUDIENCE="openid email address phone profile offline_access lingyun-abp-application" # 客户端Id -VITE_GLOB_CLIENT_ID=vue-oauth-client +VITE_GLOB_AUTH_CLIENT_ID=vue-oauth-client diff --git a/apps/vben5/apps/app-antd/index.html b/apps/vben5/apps/app-antd/index.html index 480eb84de..33d34a9e2 100644 --- a/apps/vben5/apps/app-antd/index.html +++ b/apps/vben5/apps/app-antd/index.html @@ -14,19 +14,6 @@ <%= VITE_APP_TITLE %> -
diff --git a/apps/vben5/apps/app-antd/package.json b/apps/vben5/apps/app-antd/package.json index 6b1852596..3fb801bb5 100644 --- a/apps/vben5/apps/app-antd/package.json +++ b/apps/vben5/apps/app-antd/package.json @@ -1,6 +1,6 @@ { "name": "@abp/app-antd", - "version": "9.2.0", + "version": "10.0.2", "homepage": "https://github.com/colinin/abp-next-admin", "bugs": "https://github.com/colinin/abp-next-admin/issues", "repository": { @@ -27,6 +27,7 @@ }, "dependencies": { "@abp/account": "workspace:*", + "@abp/ai-management": "workspace:*", "@abp/auditing": "workspace:*", "@abp/components": "workspace:*", "@abp/core": "workspace:*", diff --git a/apps/vben5/apps/app-antd/public/resource/img/logo.png b/apps/vben5/apps/app-antd/public/resource/img/logo.png new file mode 100644 index 000000000..f2dbd8ae5 Binary files /dev/null and b/apps/vben5/apps/app-antd/public/resource/img/logo.png differ diff --git a/apps/vben5/apps/app-antd/src/adapter/component/index.ts b/apps/vben5/apps/app-antd/src/adapter/component/index.ts index e98ba7f61..4ac7e46f3 100644 --- a/apps/vben5/apps/app-antd/src/adapter/component/index.ts +++ b/apps/vben5/apps/app-antd/src/adapter/component/index.ts @@ -3,26 +3,51 @@ * 可用于 vben-form、vben-modal、vben-drawer 等组件使用, */ -import type { Component } from 'vue'; +/* eslint-disable vue/one-component-per-file */ + +import type { + UploadChangeParam, + UploadFile, + UploadProps, +} from 'ant-design-vue'; + +import type { Component, Ref } from 'vue'; import type { BaseFormComponentType } from '@vben/common-ui'; import type { Recordable } from '@vben/types'; import { + computed, defineAsyncComponent, defineComponent, - getCurrentInstance, h, ref, + render, + unref, + watch, } from 'vue'; -import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; +import { + ApiComponent, + globalShareState, + IconPicker, + VCropper, +} from '@vben/common-ui'; +import { IconifyIcon } from '@vben/icons'; import { $t } from '@vben/locales'; +import { isEmpty } from '@vben/utils'; import { FeatureStateCheck, GlobalFeatureStateCheck } from '@abp/features'; import { PermissionStateCheck } from '@abp/permissions'; import { TenantSelect } from '@abp/saas'; -import { notification } from 'ant-design-vue'; +import { message, Modal, notification } from 'ant-design-vue'; + +const ColorPicker = defineAsyncComponent(() => + import('vue3-colorpicker').then((res) => { + import('vue3-colorpicker/style.css'); + return res.ColorPicker; + }), +); const AutoComplete = defineAsyncComponent( () => import('ant-design-vue/es/auto-complete'), @@ -31,12 +56,6 @@ const Button = defineAsyncComponent(() => import('ant-design-vue/es/button')); const Checkbox = defineAsyncComponent( () => import('ant-design-vue/es/checkbox'), ); -const ColorPicker = defineAsyncComponent(() => - import('vue3-colorpicker').then((res) => { - import('vue3-colorpicker/style.css'); - return res.ColorPicker; - }), -); const CheckboxGroup = defineAsyncComponent(() => import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup), ); @@ -49,12 +68,12 @@ const Input = defineAsyncComponent(() => import('ant-design-vue/es/input')); const InputNumber = defineAsyncComponent( () => import('ant-design-vue/es/input-number'), ); -const InputSearch = defineAsyncComponent(() => - import('ant-design-vue/es/input').then((res) => res.InputSearch), -); const InputPassword = defineAsyncComponent(() => import('ant-design-vue/es/input').then((res) => res.InputPassword), ); +const InputSearch = defineAsyncComponent(() => + import('ant-design-vue/es/input').then((res) => res.InputSearch), +); const Mentions = defineAsyncComponent( () => import('ant-design-vue/es/mentions'), ); @@ -79,7 +98,14 @@ const Tree = defineAsyncComponent(() => import('ant-design-vue/es/tree')); const TreeSelect = defineAsyncComponent( () => import('ant-design-vue/es/tree-select'), ); +const Cascader = defineAsyncComponent( + () => import('ant-design-vue/es/cascader'), +); const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload')); +const Image = defineAsyncComponent(() => import('ant-design-vue/es/image')); +const PreviewGroup = defineAsyncComponent(() => + import('ant-design-vue/es/image').then((res) => res.ImagePreviewGroup), +); const withDefaultPlaceholder = ( component: T, @@ -96,16 +122,15 @@ const withDefaultPlaceholder = ( $t(`ui.placeholder.${type}`); // 透传组件暴露的方法 const innerRef = ref(); - const publicApi: Recordable = {}; - expose(publicApi); - const instance = getCurrentInstance(); - instance?.proxy?.$nextTick(() => { - for (const key in innerRef.value) { - if (typeof innerRef.value[key] === 'function') { - publicApi[key] = innerRef.value[key]; - } - } - }); + expose( + new Proxy( + {}, + { + get: (_target, key) => innerRef.value?.[key], + has: (_target, key) => key in (innerRef.value || {}), + }, + ), + ); return () => h( component, @@ -116,11 +141,369 @@ const withDefaultPlaceholder = ( }); }; +const withPreviewUpload = () => { + // 检查是否为图片文件的辅助函数 + const isImageFile = (file: UploadFile): boolean => { + const imageExtensions = new Set([ + 'bmp', + 'gif', + 'jpeg', + 'jpg', + 'png', + 'svg', + 'webp', + ]); + if (file.url) { + try { + const pathname = new URL(file.url, 'http://localhost').pathname; + const ext = pathname.split('.').pop()?.toLowerCase(); + return ext ? imageExtensions.has(ext) : false; + } catch { + const ext = file.url?.split('.').pop()?.toLowerCase(); + return ext ? imageExtensions.has(ext) : false; + } + } + if (!file.type) { + const ext = file.name?.split('.').pop()?.toLowerCase(); + return ext ? imageExtensions.has(ext) : false; + } + return file.type.startsWith('image/'); + }; + // 创建默认的上传按钮插槽 + const createDefaultSlotsWithUpload = ( + listType: string, + placeholder: string, + ) => { + switch (listType) { + case 'picture-card': { + return { + default: () => placeholder, + }; + } + default: { + return { + default: () => + h( + Button, + { + icon: h(IconifyIcon, { + icon: 'ant-design:upload-outlined', + class: 'mb-1 size-4', + }), + }, + () => placeholder, + ), + }; + } + } + }; + // 构建预览图片组 + const previewImage = async ( + file: UploadFile, + visible: Ref, + fileList: Ref, + ) => { + // 如果当前文件不是图片,直接打开 + if (!isImageFile(file)) { + if (file.url) { + window.open(file.url, '_blank'); + } else if (file.preview) { + window.open(file.preview, '_blank'); + } else { + message.error($t('ui.formRules.previewWarning')); + } + return; + } + + // 对于图片文件,继续使用预览组 + const [ImageComponent, PreviewGroupComponent] = await Promise.all([ + Image, + PreviewGroup, + ]); + + const getBase64 = (file: File) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.addEventListener('load', () => resolve(reader.result)); + reader.addEventListener('error', (error) => reject(error)); + }); + }; + // 从fileList中过滤出所有图片文件 + const imageFiles = (unref(fileList) || []).filter((element) => + isImageFile(element), + ); + + // 为所有没有预览地址的图片生成预览 + for (const imgFile of imageFiles) { + if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) { + imgFile.preview = (await getBase64(imgFile.originFileObj)) as string; + } + } + const container: HTMLElement | null = document.createElement('div'); + document.body.append(container); + + // 用于追踪组件是否已卸载 + let isUnmounted = false; + + const PreviewWrapper = { + setup() { + return () => { + if (isUnmounted) return null; + return h( + PreviewGroupComponent, + { + class: 'hidden', + preview: { + visible: visible.value, + // 设置初始显示的图片索引 + current: imageFiles.findIndex((f) => f.uid === file.uid), + onVisibleChange: (value: boolean) => { + visible.value = value; + if (!value) { + // 延迟清理,确保动画完成 + setTimeout(() => { + if (!isUnmounted && container) { + isUnmounted = true; + render(null, container); + container.remove(); + } + }, 300); + } + }, + }, + }, + () => + // 渲染所有图片文件 + imageFiles.map((imgFile) => + h(ImageComponent, { + key: imgFile.uid, + src: imgFile.url || imgFile.preview, + }), + ), + ); + }; + }, + }; + + render(h(PreviewWrapper), container); + }; + + // 图片裁剪操作 + const cropImage = (file: File, aspectRatio: string | undefined) => { + return new Promise((resolve, reject) => { + const container: HTMLElement | null = document.createElement('div'); + document.body.append(container); + + // 用于追踪组件是否已卸载 + let isUnmounted = false; + let objectUrl: null | string = null; + + const open = ref(true); + const cropperRef = ref | null>(null); + + const closeModal = () => { + open.value = false; + // 延迟清理,确保动画完成 + setTimeout(() => { + if (!isUnmounted && container) { + if (objectUrl) { + URL.revokeObjectURL(objectUrl); + } + isUnmounted = true; + render(null, container); + container.remove(); + } + }, 300); + }; + + const CropperWrapper = { + setup() { + return () => { + if (isUnmounted) return null; + if (!objectUrl) { + objectUrl = URL.createObjectURL(file); + } + return h( + Modal, + { + open: open.value, + title: h('div', {}, [ + $t('ui.crop.title'), + h( + 'span', + { + class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`, + }, + $t('ui.crop.titleTip', [aspectRatio]), + ), + ]), + centered: true, + width: 548, + keyboard: false, + maskClosable: false, + closable: false, + cancelText: $t('common.cancel'), + okText: $t('ui.crop.confirm'), + destroyOnClose: true, + onOk: async () => { + const cropper = cropperRef.value; + if (!cropper) { + reject(new Error('Cropper not found')); + closeModal(); + return; + } + try { + const dataUrl = await cropper.getCropImage(); + resolve(dataUrl); + } catch { + reject(new Error($t('ui.crop.errorTip'))); + } finally { + closeModal(); + } + }, + onCancel() { + resolve(''); + closeModal(); + }, + }, + () => + h(VCropper, { + ref: (ref: any) => (cropperRef.value = ref), + img: objectUrl as string, + aspectRatio, + }), + ); + }; + }, + }; + + render(h(CropperWrapper), container); + }); + }; + + return defineComponent({ + name: Upload.name, + emits: ['update:modelValue'], + setup: ( + props: any, + { attrs, slots, emit }: { attrs: any; emit: any; slots: any }, + ) => { + const previewVisible = ref(false); + + const placeholder = attrs?.placeholder || $t(`ui.placeholder.upload`); + + const listType = attrs?.listType || attrs?.['list-type'] || 'text'; + + const fileList = ref( + attrs?.fileList || attrs?.['file-list'] || [], + ); + + const maxSize = computed(() => attrs?.maxSize ?? attrs?.['max-size']); + const aspectRatio = computed( + () => attrs?.aspectRatio ?? attrs?.['aspect-ratio'], + ); + + const handleBeforeUpload = async ( + file: UploadFile, + originFileList: Array, + ) => { + if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) { + message.error($t('ui.formRules.sizeLimit', [maxSize.value])); + file.status = 'removed'; + return false; + } + // 多选或者非图片不唤起裁剪框 + if ( + attrs.crop && + !attrs.multiple && + originFileList[0] && + isImageFile(file) + ) { + file.status = 'removed'; + // antd Upload组件问题 file参数获取的是UploadFile类型对象无法取到File类型 所以通过originFileList[0]获取 + const blob = await cropImage(originFileList[0], aspectRatio.value); + return new Promise((resolve, reject) => { + if (!blob) { + return reject(new Error($t('ui.crop.errorTip'))); + } + resolve(blob); + }); + } + + return attrs.beforeUpload?.(file) ?? true; + }; + + const handleChange = (event: UploadChangeParam) => { + try { + // 行内写法 handleChange: (event) => {} + attrs.handleChange?.(event); + // template写法 @handle-change="(event) => {}" + attrs.onHandleChange?.(event); + } catch (error) { + // Avoid breaking internal v-model sync on user handler errors + console.error(error); + } + fileList.value = event.fileList.filter( + (file) => file.status !== 'removed', + ); + emit( + 'update:modelValue', + event.fileList?.length ? fileList.value : undefined, + ); + }; + + const handlePreview = async (file: UploadFile) => { + previewVisible.value = true; + await previewImage(file, previewVisible, fileList); + }; + + const renderUploadButton = (): any => { + const isDisabled = attrs.disabled; + + // 如果禁用,不渲染上传按钮 + if (isDisabled) { + return null; + } + + // 否则渲染默认上传按钮 + return isEmpty(slots) + ? createDefaultSlotsWithUpload(listType, placeholder) + : slots; + }; + + // 可以监听到表单API设置的值 + watch( + () => attrs.modelValue, + (res) => { + fileList.value = res; + }, + ); + + return () => + h( + Upload, + { + ...props, + ...attrs, + fileList: fileList.value, + beforeUpload: handleBeforeUpload, + onChange: handleChange, + onPreview: handlePreview, + }, + renderUploadButton(), + ); + }, + }); +}; + // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 export type ComponentType = + | 'ApiCascader' | 'ApiSelect' | 'ApiTreeSelect' | 'AutoComplete' + | 'Cascader' | 'Checkbox' | 'CheckboxGroup' | 'ColorPicker' @@ -158,35 +541,30 @@ async function initComponentAdapter() { // 如果你的组件体积比较大,可以使用异步加载 // Button: () => // import('xxx').then((res) => res.Button), - ApiSelect: withDefaultPlaceholder( - { - ...ApiComponent, - name: 'ApiSelect', - }, - 'select', - { - component: Select, - loadingSlot: 'suffixIcon', - visibleEvent: 'onDropdownVisibleChange', - modelPropName: 'value', - }, - ), - ApiTreeSelect: withDefaultPlaceholder( - { - ...ApiComponent, - name: 'ApiTreeSelect', - }, - 'select', - { - component: TreeSelect, - fieldNames: { label: 'label', value: 'value', children: 'children' }, - loadingSlot: 'suffixIcon', - modelPropName: 'value', - optionsPropName: 'treeData', - visibleEvent: 'onVisibleChange', - }, - ), + + ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', { + component: Cascader, + fieldNames: { label: 'label', value: 'value', children: 'children' }, + loadingSlot: 'suffixIcon', + modelPropName: 'value', + visibleEvent: 'onVisibleChange', + }), + ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', { + component: Select, + loadingSlot: 'suffixIcon', + modelPropName: 'value', + visibleEvent: 'onVisibleChange', + }), + ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', { + component: TreeSelect, + fieldNames: { label: 'label', value: 'value', children: 'children' }, + loadingSlot: 'suffixIcon', + modelPropName: 'value', + optionsPropName: 'treeData', + visibleEvent: 'onVisibleChange', + }), AutoComplete, + Cascader, Checkbox, CheckboxGroup, ColorPicker, @@ -222,7 +600,7 @@ async function initComponentAdapter() { TimePicker, Tree, TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'), - Upload, + Upload: withPreviewUpload(), FeatureStateCheck, GlobalFeatureStateCheck, PermissionStateCheck, diff --git a/apps/vben5/apps/app-antd/src/adapter/request/index.ts b/apps/vben5/apps/app-antd/src/adapter/request/index.ts index 1fe3242a1..072e67d12 100644 --- a/apps/vben5/apps/app-antd/src/adapter/request/index.ts +++ b/apps/vben5/apps/app-antd/src/adapter/request/index.ts @@ -3,7 +3,7 @@ import { authenticateResponseInterceptor, errorMessageResponseInterceptor, } from '@vben/request'; -import { useAccessStore } from '@vben/stores'; +import { useAccessStore, useTimezoneStore } from '@vben/stores'; import { useOAuthError } from '@abp/account'; import { useAbpStore } from '@abp/core'; @@ -53,17 +53,24 @@ export function initRequestClient() { fulfilled: async (config) => { const abpStore = useAbpStore(); const accessStore = useAccessStore(); + const timezoneStore = useTimezoneStore(); + if (accessStore.accessToken) { config.headers.Authorization = `${accessStore.accessToken}`; } config.headers['Accept-Language'] = preferences.app.locale; config.headers['X-Request-From'] = 'vben'; if (abpStore.tenantId) { + // see: https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantResolverConsts.cs config.headers.__tenant = abpStore.tenantId; } if (abpStore.xsrfToken) { config.headers.RequestVerificationToken = abpStore.xsrfToken; } + if (timezoneStore.timezone) { + // see: https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimeZoneConsts.cs + config.headers.__timezone = timezoneStore.timezone; + } return config; }, }); diff --git a/apps/vben5/apps/app-antd/src/bootstrap.ts b/apps/vben5/apps/app-antd/src/bootstrap.ts index d34a6f46b..f75585871 100644 --- a/apps/vben5/apps/app-antd/src/bootstrap.ts +++ b/apps/vben5/apps/app-antd/src/bootstrap.ts @@ -16,6 +16,7 @@ import { initSetupVbenForm } from './adapter/form'; import { initRequestClient } from './adapter/request'; import App from './app.vue'; import { router } from './router'; +import { initTimezone } from './timezone-init'; async function bootstrap(namespace: string) { // 初始化组件适配器 @@ -50,6 +51,9 @@ async function bootstrap(namespace: string) { // 国际化 i18n 配置 await setupI18n(app); + // 初始化时区HANDLER + initTimezone(); + // 安装权限指令 registerAccessDirective(app); diff --git a/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json b/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json index 84408903b..bf17c63ce 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json @@ -52,7 +52,8 @@ "title": "Notifications", "myNotifilers": "My Notifilers", "groups": "Groups", - "definitions": "Definitions" + "definitions": "Definitions", + "sendRecords": "Send Records" }, "localization": { "title": "Localization", @@ -98,7 +99,8 @@ "authenticatorSettings": "Authenticator Settings", "changeAvatar": "Change Avatar", "sessionSettings": "Session Settings", - "personalDataSettings": "Personal Data Settings" + "personalDataSettings": "Personal Data Settings", + "systemSettings": "System Settings" }, "profile": "My Profile" }, @@ -147,5 +149,10 @@ "wechat": { "title": "WeChat", "settings": "Settings" + }, + "ai": { + "title": "Artificial Intelligence", + "workspaces": "Workspaces", + "conversations": "Conversations" } } diff --git a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json index bdf07a393..3e5fe5888 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json @@ -52,7 +52,8 @@ "title": "通知管理", "myNotifilers": "我的通知", "groups": "通知分组", - "definitions": "通知定义" + "definitions": "通知定义", + "sendRecords": "发送记录" }, "localization": { "title": "本地化管理", @@ -98,7 +99,8 @@ "authenticatorSettings": "身份验证程序", "changeAvatar": "更改头像", "sessionSettings": "会话管理", - "personalDataSettings": "个人信息管理" + "personalDataSettings": "个人信息管理", + "systemSettings": "系统设置" }, "profile": "个人中心" }, @@ -147,5 +149,10 @@ "wechat": { "title": "微信集成", "settings": "微信设置" + }, + "ai": { + "title": "人工智能", + "workspaces": "工作区管理", + "conversations": "会话管理" } } diff --git a/apps/vben5/apps/app-antd/src/preferences.ts b/apps/vben5/apps/app-antd/src/preferences.ts index 94d2a3ca8..1591bd1d8 100644 --- a/apps/vben5/apps/app-antd/src/preferences.ts +++ b/apps/vben5/apps/app-antd/src/preferences.ts @@ -13,6 +13,9 @@ export const overridesPreferences = defineOverridesPreferences({ enableRefreshToken: true, name: import.meta.env.VITE_APP_TITLE, }, + logo: { + source: '/resource/img/logo.png', + }, theme: { mode: 'auto', radius: '0.25', diff --git a/apps/vben5/apps/app-antd/src/timezone-init.ts b/apps/vben5/apps/app-antd/src/timezone-init.ts new file mode 100644 index 000000000..704ec022b --- /dev/null +++ b/apps/vben5/apps/app-antd/src/timezone-init.ts @@ -0,0 +1,28 @@ +import { setTimezoneHandler } from '@vben/stores'; + +import { useTimeZoneSettingsApi } from '@abp/settings'; + +/** + * 初始化时区处理,通过API保存时区设置 + */ +export function initTimezone() { + const { getMyTimezoneApi, getTimezonesApi, updateMyTimezoneApi } = + useTimeZoneSettingsApi(); + setTimezoneHandler({ + getTimezone() { + return getMyTimezoneApi(); + }, + setTimezone(timezone: string) { + return updateMyTimezoneApi(timezone); + }, + async getTimezoneOptions() { + const timezones = await getTimezonesApi(); + return timezones.map((timezone) => { + return { + label: timezone.name, + value: timezone.value, + }; + }); + }, + }); +} diff --git a/apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue b/apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue index 157db329a..32a11cfc0 100644 --- a/apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue +++ b/apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue @@ -26,7 +26,7 @@ interface LoginInstance { defineOptions({ name: 'Login' }); -const { onlyOidc } = useAppConfig(import.meta.env, import.meta.env.PROD); +const { auth } = useAppConfig(import.meta.env, import.meta.env.PROD); const abpStore = useAbpStore(); const authStore = useAuthStore(); @@ -38,7 +38,7 @@ const { getConfigApi } = useAbpConfigApi(); const login = useTemplateRef('login'); const formSchema = computed((): VbenFormSchema[] => { - if (onlyOidc) { + if (auth.onlyOidc) { return []; } let schemas: VbenFormSchema[] = [ @@ -82,22 +82,26 @@ const [ShouldChangePasswordModal, changePasswordModalApi] = useVbenModal({ connectedComponent: ShouldChangePassword, }); async function onInit() { - if (onlyOidc === true) { - setTimeout(() => { - Modal.confirm({ - centered: true, - title: $t('page.auth.oidcLogin'), - content: $t('page.auth.oidcLoginMessage'), - maskClosable: false, - closable: false, - cancelButtonProps: { - disabled: true, - }, - async onOk() { - await authStore.oidcLogin(); - }, - }); - }, 300); + if (auth.onlyOidc === true) { + if (auth.onlyOidcHint) { + setTimeout(() => { + Modal.confirm({ + centered: true, + title: $t('page.auth.oidcLogin'), + content: $t('page.auth.oidcLoginMessage'), + maskClosable: false, + closable: false, + cancelButtonProps: { + disabled: true, + }, + async onOk() { + await authStore.oidcLogin(); + }, + }); + }, 300); + } else { + await authStore.oidcLogin(); + } return; } const abpConfig = await getConfigApi(); @@ -108,7 +112,7 @@ async function onInit() { }); } async function onLogin(params: Recordable) { - if (onlyOidc === true) { + if (auth.onlyOidc === true) { await authStore.oidcLogin(); return; } @@ -144,7 +148,7 @@ onMounted(onInit);