diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2207ace28..4916d417a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -30,7 +30,7 @@ jobs: run: pnpm build:play - name: Sync Playground files - uses: SamKirkland/FTP-Deploy-Action@v4.3.6 + uses: SamKirkland/FTP-Deploy-Action@v4.4.0 with: server: ${{ secrets.PRO_FTP_HOST }} username: ${{ secrets.WEB_PLAYGROUND_FTP_ACCOUNT }} @@ -54,7 +54,7 @@ jobs: run: pnpm build:docs - name: Sync Docs files - uses: SamKirkland/FTP-Deploy-Action@v4.3.6 + uses: SamKirkland/FTP-Deploy-Action@v4.4.0 with: server: ${{ secrets.PRO_FTP_HOST }} username: ${{ secrets.WEBSITE_FTP_ACCOUNT }} @@ -85,7 +85,7 @@ jobs: run: pnpm run build:antd - name: Sync files - uses: SamKirkland/FTP-Deploy-Action@v4.3.6 + uses: SamKirkland/FTP-Deploy-Action@v4.4.0 with: server: ${{ secrets.PRO_FTP_HOST }} username: ${{ secrets.WEB_ANTD_FTP_ACCOUNT }} @@ -116,7 +116,7 @@ jobs: run: pnpm run build:ele - name: Sync files - uses: SamKirkland/FTP-Deploy-Action@v4.3.6 + uses: SamKirkland/FTP-Deploy-Action@v4.4.0 with: server: ${{ secrets.PRO_FTP_HOST }} username: ${{ secrets.WEB_ELE_FTP_ACCOUNT }} @@ -147,7 +147,7 @@ jobs: run: pnpm run build:naive - name: Sync files - uses: SamKirkland/FTP-Deploy-Action@v4.3.6 + uses: SamKirkland/FTP-Deploy-Action@v4.4.0 with: server: ${{ secrets.PRO_FTP_HOST }} username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }} diff --git a/.gitignore b/.gitignore index 86d1f1f8a..5e08fa4e2 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,5 @@ vite.config.ts.* .claude .codex skills-lock.json +.atomcode +.datalog diff --git a/.npmrc b/.npmrc index 6d28fabf4..7549542d7 100644 --- a/.npmrc +++ b/.npmrc @@ -1,13 +1 @@ registry=https://registry.npmmirror.com -public-hoist-pattern[]=lefthook -public-hoist-pattern[]=eslint -public-hoist-pattern[]=oxfmt -public-hoist-pattern[]=oxlint -public-hoist-pattern[]=stylelint -public-hoist-pattern[]=*postcss* -public-hoist-pattern[]=@commitlint/* -public-hoist-pattern[]=czg - -strict-peer-dependencies=false -auto-install-peers=true -dedupe-peer-dependents=true diff --git a/.vscode/settings.json b/.vscode/settings.json index 993b52039..ff13cfa9c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -38,7 +38,7 @@ // lint && format "oxc.enable": true, - "oxc.typeAware": true, + "oxc.typeAware": false, "oxc.configPath": "oxlint.config.ts", "oxc.fmt.configPath": "oxfmt.config.ts", "eslint.useFlatConfig": true, diff --git a/apps/backend-mock/api/system/user/list.ts b/apps/backend-mock/api/system/user/list.ts new file mode 100644 index 000000000..21d44c8ba --- /dev/null +++ b/apps/backend-mock/api/system/user/list.ts @@ -0,0 +1,85 @@ +import { faker } from '@faker-js/faker'; +import { eventHandler, getQuery } from 'h3'; +import { verifyAccessToken } from '~/utils/jwt-utils'; +import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response'; + +const formatterCN = new Intl.DateTimeFormat('zh-CN', { + timeZone: 'Asia/Shanghai', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', +}); + +function generateMockDataList(count: number) { + const dataList = []; + + for (let i = 0; i < count; i++) { + const dataItem: Record = { + id: faker.string.uuid(), + name: faker.commerce.product(), + status: faker.helpers.arrayElement([0, 1]), + createTime: formatterCN.format( + faker.date.between({ from: '2022-01-01', to: '2025-01-01' }), + ), + deptId: faker.string.uuid(), + remark: faker.lorem.sentence(), + }; + + dataList.push(dataItem); + } + + return dataList; +} + +const mockData = generateMockDataList(100); + +export default eventHandler(async (event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + + const { + page = 1, + pageSize = 20, + name, + id, + remark, + startTime, + endTime, + deptId, + status, + } = getQuery(event); + let listData = structuredClone(mockData); + if (name) { + listData = listData.filter((item) => + item.name.toLowerCase().includes(String(name).toLowerCase()), + ); + } + if (id) { + listData = listData.filter((item) => + item.id.toLowerCase().includes(String(id).toLowerCase()), + ); + } + if (remark) { + listData = listData.filter((item) => + item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()), + ); + } + if (startTime) { + listData = listData.filter((item) => item.createTime >= startTime); + } + if (endTime) { + listData = listData.filter((item) => item.createTime <= endTime); + } + if (['0', '1'].includes(status as string)) { + listData = listData.filter((item) => item.status === Number(status)); + } + if (deptId) { + listData = listData.filter((item) => item.deptId === deptId); + } + return usePageResponseSuccess(page as string, pageSize as string, listData); +}); diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index bf34ba767..81b6e8fa3 100644 --- a/apps/web-antd/src/adapter/component/index.ts +++ b/apps/web-antd/src/adapter/component/index.ts @@ -131,8 +131,8 @@ const PreviewGroup = defineAsyncComponent(() => import('ant-design-vue/es/image').then((res) => res.ImagePreviewGroup), ); -const withDefaultPlaceholder = ( - component: T, +const withDefaultPlaceholder = ( + component: Component, type: 'input' | 'select', componentProps: Recordable = {}, ) => { @@ -702,7 +702,9 @@ async function initComponentAdapter() { modelValueProp: 'value', }), Input: withDefaultPlaceholder(Input, 'input'), - InputNumber: withDefaultPlaceholder(InputNumber, 'input'), + InputNumber: withDefaultPlaceholder(InputNumber, 'input', { + style: { width: '100%' }, + }), InputPassword: withDefaultPlaceholder(InputPassword, 'input'), Mentions: withDefaultPlaceholder(Mentions, 'input'), // 自定义主要按钮 diff --git a/apps/web-antd/src/layouts/basic.vue b/apps/web-antd/src/layouts/basic.vue index 211ee7be3..1bb07ec8f 100644 --- a/apps/web-antd/src/layouts/basic.vue +++ b/apps/web-antd/src/layouts/basic.vue @@ -14,7 +14,7 @@ import { Notification, UserDropdown, } from '@vben/layouts'; -import { preferences } from '@vben/preferences'; +import { preferences, usePreferences } from '@vben/preferences'; import { useAccessStore, useUserStore } from '@vben/stores'; import { openWindow } from '@vben/utils'; @@ -80,6 +80,7 @@ const userStore = useUserStore(); const authStore = useAuthStore(); const accessStore = useAccessStore(); const { destroyWatermark, updateWatermark } = useWatermark(); +const { isDark } = usePreferences(); const showDot = computed(() => notifications.value.some((item) => !item.isRead), ); @@ -179,10 +180,28 @@ watch( () => ({ enable: preferences.app.watermark, content: preferences.app.watermarkContent, + isDark: isDark.value, }), - async ({ enable, content }) => { + async ({ enable, content, isDark: isDarkValue }) => { if (enable) { + const watermarkColor = isDarkValue + ? 'rgba(255, 255, 255, 0.12)' + : 'rgba(0, 0, 0, 0.12)'; + await updateWatermark({ + advancedStyle: { + colorStops: [ + { + color: watermarkColor, + offset: 0, + }, + { + color: watermarkColor, + offset: 1, + }, + ], + type: 'linear', + }, content: content || `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`, diff --git a/apps/web-antdv-next/src/adapter/component/index.ts b/apps/web-antdv-next/src/adapter/component/index.ts index 1099d67a9..e87411d16 100644 --- a/apps/web-antdv-next/src/adapter/component/index.ts +++ b/apps/web-antdv-next/src/adapter/component/index.ts @@ -136,8 +136,8 @@ const PreviewGroup = defineAsyncComponent(() => import('antdv-next/dist/image/index').then((res) => res.ImagePreviewGroup), ); -const withDefaultPlaceholder = ( - component: T, +const withDefaultPlaceholder = ( + component: Component, type: 'input' | 'select', componentProps: Recordable = {}, ) => { @@ -633,7 +633,9 @@ async function initComponentAdapter() { modelValueProp: 'value', }), Input: withDefaultPlaceholder(Input, 'input'), - InputNumber: withDefaultPlaceholder(InputNumber, 'input'), + InputNumber: withDefaultPlaceholder(InputNumber, 'input', { + style: { width: '100%' }, + }), InputPassword: withDefaultPlaceholder(InputPassword, 'input'), Mentions: withDefaultPlaceholder(Mentions, 'input'), // 自定义主要按钮 diff --git a/apps/web-antdv-next/src/layouts/basic.vue b/apps/web-antdv-next/src/layouts/basic.vue index 211ee7be3..1bb07ec8f 100644 --- a/apps/web-antdv-next/src/layouts/basic.vue +++ b/apps/web-antdv-next/src/layouts/basic.vue @@ -14,7 +14,7 @@ import { Notification, UserDropdown, } from '@vben/layouts'; -import { preferences } from '@vben/preferences'; +import { preferences, usePreferences } from '@vben/preferences'; import { useAccessStore, useUserStore } from '@vben/stores'; import { openWindow } from '@vben/utils'; @@ -80,6 +80,7 @@ const userStore = useUserStore(); const authStore = useAuthStore(); const accessStore = useAccessStore(); const { destroyWatermark, updateWatermark } = useWatermark(); +const { isDark } = usePreferences(); const showDot = computed(() => notifications.value.some((item) => !item.isRead), ); @@ -179,10 +180,28 @@ watch( () => ({ enable: preferences.app.watermark, content: preferences.app.watermarkContent, + isDark: isDark.value, }), - async ({ enable, content }) => { + async ({ enable, content, isDark: isDarkValue }) => { if (enable) { + const watermarkColor = isDarkValue + ? 'rgba(255, 255, 255, 0.12)' + : 'rgba(0, 0, 0, 0.12)'; + await updateWatermark({ + advancedStyle: { + colorStops: [ + { + color: watermarkColor, + offset: 0, + }, + { + color: watermarkColor, + offset: 1, + }, + ], + type: 'linear', + }, content: content || `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`, diff --git a/apps/web-ele/src/adapter/component/index.ts b/apps/web-ele/src/adapter/component/index.ts index b27ca9afa..28d87c568 100644 --- a/apps/web-ele/src/adapter/component/index.ts +++ b/apps/web-ele/src/adapter/component/index.ts @@ -141,8 +141,8 @@ const ElUpload = defineAsyncComponent(() => ]).then(([res]) => res.ElUpload), ); -const withDefaultPlaceholder = ( - component: T, +const withDefaultPlaceholder = ( + component: Component, type: 'input' | 'select', componentProps: Recordable = {}, ) => { @@ -284,7 +284,9 @@ async function initComponentAdapter() { inputComponent: ElInput, }), Input: withDefaultPlaceholder(ElInput, 'input'), - InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'), + InputNumber: withDefaultPlaceholder(ElInputNumber, 'input', { + style: { width: '100%' }, + }), RadioGroup: (props, { attrs, slots }) => { let defaultSlot; if (Reflect.has(slots, 'default')) { diff --git a/apps/web-ele/src/layouts/basic.vue b/apps/web-ele/src/layouts/basic.vue index 211ee7be3..1bb07ec8f 100644 --- a/apps/web-ele/src/layouts/basic.vue +++ b/apps/web-ele/src/layouts/basic.vue @@ -14,7 +14,7 @@ import { Notification, UserDropdown, } from '@vben/layouts'; -import { preferences } from '@vben/preferences'; +import { preferences, usePreferences } from '@vben/preferences'; import { useAccessStore, useUserStore } from '@vben/stores'; import { openWindow } from '@vben/utils'; @@ -80,6 +80,7 @@ const userStore = useUserStore(); const authStore = useAuthStore(); const accessStore = useAccessStore(); const { destroyWatermark, updateWatermark } = useWatermark(); +const { isDark } = usePreferences(); const showDot = computed(() => notifications.value.some((item) => !item.isRead), ); @@ -179,10 +180,28 @@ watch( () => ({ enable: preferences.app.watermark, content: preferences.app.watermarkContent, + isDark: isDark.value, }), - async ({ enable, content }) => { + async ({ enable, content, isDark: isDarkValue }) => { if (enable) { + const watermarkColor = isDarkValue + ? 'rgba(255, 255, 255, 0.12)' + : 'rgba(0, 0, 0, 0.12)'; + await updateWatermark({ + advancedStyle: { + colorStops: [ + { + color: watermarkColor, + offset: 0, + }, + { + color: watermarkColor, + offset: 1, + }, + ], + type: 'linear', + }, content: content || `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`, diff --git a/apps/web-naive/src/adapter/component/index.ts b/apps/web-naive/src/adapter/component/index.ts index 81fc97027..9ee677f80 100644 --- a/apps/web-naive/src/adapter/component/index.ts +++ b/apps/web-naive/src/adapter/component/index.ts @@ -84,8 +84,8 @@ const NUpload = defineAsyncComponent(() => import('naive-ui/es/upload').then((res) => res.NUpload), ); -const withDefaultPlaceholder = ( - component: T, +const withDefaultPlaceholder = ( + component: Component, type: 'input' | 'select', componentProps: Recordable = {}, ) => { @@ -225,7 +225,9 @@ async function initComponentAdapter() { inputComponent: NInput, }), Input: withDefaultPlaceholder(NInput, 'input'), - InputNumber: withDefaultPlaceholder(NInputNumber, 'input'), + InputNumber: withDefaultPlaceholder(NInputNumber, 'input', { + style: { width: '100%' }, + }), RadioGroup: (props, { attrs, slots }) => { let defaultSlot; if (Reflect.has(slots, 'default')) { diff --git a/apps/web-naive/src/layouts/basic.vue b/apps/web-naive/src/layouts/basic.vue index 211ee7be3..1bb07ec8f 100644 --- a/apps/web-naive/src/layouts/basic.vue +++ b/apps/web-naive/src/layouts/basic.vue @@ -14,7 +14,7 @@ import { Notification, UserDropdown, } from '@vben/layouts'; -import { preferences } from '@vben/preferences'; +import { preferences, usePreferences } from '@vben/preferences'; import { useAccessStore, useUserStore } from '@vben/stores'; import { openWindow } from '@vben/utils'; @@ -80,6 +80,7 @@ const userStore = useUserStore(); const authStore = useAuthStore(); const accessStore = useAccessStore(); const { destroyWatermark, updateWatermark } = useWatermark(); +const { isDark } = usePreferences(); const showDot = computed(() => notifications.value.some((item) => !item.isRead), ); @@ -179,10 +180,28 @@ watch( () => ({ enable: preferences.app.watermark, content: preferences.app.watermarkContent, + isDark: isDark.value, }), - async ({ enable, content }) => { + async ({ enable, content, isDark: isDarkValue }) => { if (enable) { + const watermarkColor = isDarkValue + ? 'rgba(255, 255, 255, 0.12)' + : 'rgba(0, 0, 0, 0.12)'; + await updateWatermark({ + advancedStyle: { + colorStops: [ + { + color: watermarkColor, + offset: 0, + }, + { + color: watermarkColor, + offset: 1, + }, + ], + type: 'linear', + }, content: content || `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`, diff --git a/apps/web-tdesign/src/adapter/component/index.ts b/apps/web-tdesign/src/adapter/component/index.ts index 54617f84c..37a3055e7 100644 --- a/apps/web-tdesign/src/adapter/component/index.ts +++ b/apps/web-tdesign/src/adapter/component/index.ts @@ -89,8 +89,8 @@ const TreeSelect = defineAsyncComponent( ); const Upload = defineAsyncComponent(() => import('tdesign-vue-next/es/upload')); -const withDefaultPlaceholder = ( - component: T, +const withDefaultPlaceholder = ( + component: Component, type: 'input' | 'select', componentProps: Recordable = {}, ) => { @@ -239,7 +239,9 @@ async function initComponentAdapter() { modelValueProp: 'value', }), Input: withDefaultPlaceholder(Input, 'input'), - InputNumber: withDefaultPlaceholder(InputNumber, 'input'), + InputNumber: withDefaultPlaceholder(InputNumber, 'input', { + style: { width: '100%' }, + }), // InputPassword: withDefaultPlaceholder(InputPassword, 'input'), // Mentions: withDefaultPlaceholder(Mentions, 'input'), // 自定义主要按钮 diff --git a/apps/web-tdesign/src/layouts/basic.vue b/apps/web-tdesign/src/layouts/basic.vue index 211ee7be3..1bb07ec8f 100644 --- a/apps/web-tdesign/src/layouts/basic.vue +++ b/apps/web-tdesign/src/layouts/basic.vue @@ -14,7 +14,7 @@ import { Notification, UserDropdown, } from '@vben/layouts'; -import { preferences } from '@vben/preferences'; +import { preferences, usePreferences } from '@vben/preferences'; import { useAccessStore, useUserStore } from '@vben/stores'; import { openWindow } from '@vben/utils'; @@ -80,6 +80,7 @@ const userStore = useUserStore(); const authStore = useAuthStore(); const accessStore = useAccessStore(); const { destroyWatermark, updateWatermark } = useWatermark(); +const { isDark } = usePreferences(); const showDot = computed(() => notifications.value.some((item) => !item.isRead), ); @@ -179,10 +180,28 @@ watch( () => ({ enable: preferences.app.watermark, content: preferences.app.watermarkContent, + isDark: isDark.value, }), - async ({ enable, content }) => { + async ({ enable, content, isDark: isDarkValue }) => { if (enable) { + const watermarkColor = isDarkValue + ? 'rgba(255, 255, 255, 0.12)' + : 'rgba(0, 0, 0, 0.12)'; + await updateWatermark({ + advancedStyle: { + colorStops: [ + { + color: watermarkColor, + offset: 0, + }, + { + color: watermarkColor, + offset: 1, + }, + ], + type: 'linear', + }, content: content || `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`, diff --git a/docs/.vitepress/build/dayjs-loader.mjs b/docs/.vitepress/build/dayjs-loader.mjs new file mode 100644 index 000000000..623c394ff --- /dev/null +++ b/docs/.vitepress/build/dayjs-loader.mjs @@ -0,0 +1,2 @@ +import { register } from 'node:module'; +register('./dayjs-resolve-hook.mjs', import.meta.url); diff --git a/docs/.vitepress/build/dayjs-resolve-hook.mjs b/docs/.vitepress/build/dayjs-resolve-hook.mjs new file mode 100644 index 000000000..f8989044a --- /dev/null +++ b/docs/.vitepress/build/dayjs-resolve-hook.mjs @@ -0,0 +1,10 @@ +const DAYJS_SUBPATH_RE = /^dayjs\/(plugin|locale)\/([^./]+)$/; + +/** @type {import('node:module').ResolveHook} */ +export async function resolve(specifier, context, nextResolve) { + const match = specifier.match(DAYJS_SUBPATH_RE); + if (match) { + return nextResolve(`${specifier}.js`, context); + } + return nextResolve(specifier, context); +} diff --git a/docs/.vitepress/config/en.mts b/docs/.vitepress/config/en.mts index 8bac04ceb..fcbe50edf 100644 --- a/docs/.vitepress/config/en.mts +++ b/docs/.vitepress/config/en.mts @@ -198,6 +198,14 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] { link: 'common-ui/vben-ellipsis-text', text: 'EllipsisText', }, + { + link: 'common-ui/vben-cropper', + text: 'Cropper', + }, + { + link: 'common-ui/vben-tiptap', + text: 'Tiptap RichTextEditor', + }, ], }, ]; diff --git a/docs/.vitepress/config/index.mts b/docs/.vitepress/config/index.mts index 6b8cb81d8..e966a8c1a 100644 --- a/docs/.vitepress/config/index.mts +++ b/docs/.vitepress/config/index.mts @@ -1,12 +1,12 @@ import { withPwa } from '@vite-pwa/vitepress'; -import { defineConfigWithTheme } from 'vitepress'; +import { defineConfig } from 'vitepress'; import { en } from './en.mts'; import { shared } from './shared.mts'; import { zh } from './zh.mts'; export default withPwa( - defineConfigWithTheme({ + defineConfig({ ...shared, locales: { en: { diff --git a/docs/.vitepress/config/shared.mts b/docs/.vitepress/config/shared.mts index cdfb0b779..30b99d691 100644 --- a/docs/.vitepress/config/shared.mts +++ b/docs/.vitepress/config/shared.mts @@ -5,6 +5,7 @@ import { resolve } from 'node:path'; import { viteArchiverPlugin, + viteDayjsPlugin, viteVxeTableImportsPlugin, } from '@vben/vite-config'; @@ -61,7 +62,7 @@ export const shared = defineConfig({ }, preprocessorOptions: { scss: { - api: 'modern', + // api: 'modern', }, }, }, @@ -69,6 +70,7 @@ export const shared = defineConfig({ stringify: true, }, plugins: [ + viteDayjsPlugin(), tailwindcss(), GitChangelog({ mapAuthors: [ @@ -85,9 +87,23 @@ export const shared = defineConfig({ name: 'Li Kui', username: 'likui628', }, + { + mapByNameAliases: ['Jin Mao', 'jinmao'], + name: 'Jin Mao', + username: 'jinmao88', + }, + { + name: 'Netfan', + username: 'mynetfan', + }, + { + mapByNameAliases: ['xingyu4j', 'xingyu'], + name: 'xingyu4j', + username: 'xingyu4j', + }, ], repoURL: () => 'https://github.com/vbenjs/vue-vben-admin', - }), + }) as any, GitChangelogMarkdownSection(), viteArchiverPlugin({ outputDir: '.vitepress' }), groupIconVitePlugin(), @@ -103,6 +119,7 @@ export const shared = defineConfig({ ssr: { external: ['@vue/repl'], + noExternal: ['@v-c/picker'], }, }, }); @@ -113,7 +130,7 @@ function head(): HeadConfig[] { [ 'meta', { - content: 'vben, vitejs, vite, shacdn-ui, vue', + content: 'vben, vitejs, vite, shadcn-ui, vue', name: 'keywords', }, ], diff --git a/docs/.vitepress/config/zh.mts b/docs/.vitepress/config/zh.mts index 2c3753deb..c90418b8d 100644 --- a/docs/.vitepress/config/zh.mts +++ b/docs/.vitepress/config/zh.mts @@ -196,6 +196,14 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] { link: 'common-ui/vben-ellipsis-text', text: 'EllipsisText 省略文本', }, + { + link: 'common-ui/vben-cropper', + text: 'Cropper 图片裁剪', + }, + { + link: 'common-ui/vben-tiptap', + text: 'Tiptap 富文本编辑器', + }, ], }, ]; @@ -329,8 +337,8 @@ export const search: DefaultTheme.AlgoliaSearchOptions['locales'] = { footer: { closeText: '关闭', navigateText: '切换', - searchByText: '搜索提供者', selectText: '选择', + poweredByText: '搜索提供者', }, noResultsScreen: { noResultsText: '无法找到相关结果', @@ -339,10 +347,10 @@ export const search: DefaultTheme.AlgoliaSearchOptions['locales'] = { suggestedQueryText: '你可以尝试查询', }, searchBox: { - cancelButtonAriaLabel: '取消', - cancelButtonText: '取消', - resetButtonAriaLabel: '清除查询条件', - resetButtonTitle: '清除查询条件', + closeButtonAriaLabel: '取消', + closeButtonText: '取消', + clearButtonAriaLabel: '清除查询条件', + clearButtonTitle: '清除查询条件', }, startScreen: { favoriteSearchesTitle: '收藏', diff --git a/docs/.vitepress/theme/components/site-layout.vue b/docs/.vitepress/theme/components/site-layout.vue index 96427d055..8b40ed3f7 100644 --- a/docs/.vitepress/theme/components/site-layout.vue +++ b/docs/.vitepress/theme/components/site-layout.vue @@ -10,7 +10,7 @@ import { // import { useAntdDesignTokens } from '@vben/hooks'; // import { initPreferences } from '@vben/preferences'; -import { ConfigProvider, theme } from 'ant-design-vue'; +import { ConfigProvider, theme } from 'antdv-next'; import mediumZoom from 'medium-zoom'; import { useRoute } from 'vitepress'; import DefaultTheme from 'vitepress/theme'; diff --git a/docs/package.json b/docs/package.json index c418d797f..400fb2a6b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -2,8 +2,9 @@ "name": "@vben/docs", "version": "5.7.0", "private": true, + "type": "module", "scripts": { - "build": "vitepress build", + "build": "cross-env NODE_OPTIONS=\"--import ./.vitepress/build/dayjs-loader.mjs\" vitepress build", "dev": "vitepress dev", "docs:preview": "vitepress preview" }, @@ -19,7 +20,7 @@ "@vben/locales": "workspace:*", "@vben/plugins": "workspace:*", "@vben/styles": "workspace:*", - "ant-design-vue": "catalog:", + "antdv-next": "catalog:", "lucide-vue-next": "catalog:", "medium-zoom": "catalog:", "reka-ui": "catalog:", diff --git a/docs/src/_env/adapter/component.ts b/docs/src/_env/adapter/component.ts index ec3d0196c..0fa10f3ff 100644 --- a/docs/src/_env/adapter/component.ts +++ b/docs/src/_env/adapter/component.ts @@ -26,16 +26,15 @@ import { notification, Radio, RadioGroup, - RangePicker, Rate, Select, Space, Switch, - Textarea, + TextArea, TimePicker, TreeSelect, Upload, -} from 'ant-design-vue'; +} from 'antdv-next'; const withDefaultPlaceholder = ( component: T, @@ -98,12 +97,12 @@ async function initComponentAdapter() { }, Radio, RadioGroup, - RangePicker, + RangePicker: withDefaultPlaceholder(DatePicker.RangePicker, 'select'), Rate, Select: withDefaultPlaceholder(Select, 'select'), Space, Switch, - Textarea: withDefaultPlaceholder(Textarea, 'input'), + Textarea: withDefaultPlaceholder(TextArea, 'input'), TimePicker, TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'), Upload, @@ -118,7 +117,7 @@ async function initComponentAdapter() { copyPreferencesSuccess: (title, content) => { notification.success({ description: content, - message: title, + title, placement: 'bottomRight', }); }, diff --git a/docs/src/_env/adapter/vxe-table.ts b/docs/src/_env/adapter/vxe-table.ts index 7bc27056e..6d9ed6449 100644 --- a/docs/src/_env/adapter/vxe-table.ts +++ b/docs/src/_env/adapter/vxe-table.ts @@ -2,7 +2,7 @@ import { h } from 'vue'; import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; -import { Button, Image } from 'ant-design-vue'; +import { Button, Image } from 'antdv-next'; import { useVbenForm } from './form'; diff --git a/docs/src/components/common-ui/vben-api-component.md b/docs/src/components/common-ui/vben-api-component.md index 5465f1516..b14e07258 100644 --- a/docs/src/components/common-ui/vben-api-component.md +++ b/docs/src/components/common-ui/vben-api-component.md @@ -22,7 +22,7 @@ outline: deep +``` + +#### getCropImage + +裁剪并获取图片。 + +```ts +interface GetCropImageOptions { + /** 输出图片格式 */ + format?: 'image/jpeg' | 'image/png'; + /** 压缩质量(0-1),仅对 jpeg 格式有效 */ + quality?: number; + /** 输出类型 */ + outputType?: 'base64' | 'blob'; + /** 目标宽度(可选,不传则为原始裁剪宽度) */ + targetWidth?: number; + /** 目标高度(可选,不传则为原始裁剪高度) */ + targetHeight?: number; +} + +getCropImage( + format?: 'image/jpeg' | 'image/png', + quality?: number, + outputType?: 'base64' | 'blob', + targetWidth?: number, + targetHeight?: number, +): Promise +``` + +**参数说明:** + +| 参数 | 类型 | 默认值 | 描述 | +| --- | --- | --- | --- | +| `format` | `'image/jpeg' \| 'image/png'` | `'image/png'` | 输出图片格式 | +| `quality` | `number` | `0.92` | 压缩质量(0-1),仅 jpeg 有效 | +| `outputType` | `'base64' \| 'blob'` | `'blob'` | 输出类型,base64 字符串或 Blob 对象 | +| `targetWidth` | `number` | - | 目标宽度,不传则使用原始裁剪宽度 | +| `targetHeight` | `number` | - | 目标高度,不传则使用原始裁剪高度 | + +## 功能特性 + +### 裁剪操作 + +- **拖拽移动** - 拖拽裁剪框中心区域移动裁剪位置 +- **边角调整** - 拖拽四角调整裁剪框大小 +- **边缘调整** - 拖拽四边中点调整单边 + +### 比例控制 + +- **自由比例** - 不设置 `aspectRatio` 时,可自由调整任意比例 +- **固定比例** - 设置 `aspectRatio` 后,裁剪框始终保持设定比例 + +### 高清屏适配 + +组件自动适配 Retina 等高清屏幕,保证输出图片清晰无模糊。 + +### 图片适配 + +- 图片自动等比缩放以完整显示在容器内 +- 支持本地图片和网络图片 +- 网络图片需目标服务端支持 CORS 才能导出裁剪结果 + +## 使用示例 + +```vue + + + +``` diff --git a/docs/src/components/common-ui/vben-form.md b/docs/src/components/common-ui/vben-form.md index 608511377..f99c543c0 100644 --- a/docs/src/components/common-ui/vben-form.md +++ b/docs/src/components/common-ui/vben-form.md @@ -117,7 +117,7 @@ import { TimePicker, TreeSelect, Upload, -} from 'ant-design-vue'; +} from 'antdv-next'; const withDefaultPlaceholder = ( component: T, diff --git a/docs/src/components/common-ui/vben-tiptap.md b/docs/src/components/common-ui/vben-tiptap.md new file mode 100644 index 000000000..84b84ad1d --- /dev/null +++ b/docs/src/components/common-ui/vben-tiptap.md @@ -0,0 +1,199 @@ +--- +outline: deep +--- + +# Vben Tiptap 富文本编辑器 + +基于 [Tiptap](https://tiptap.dev/) 构建的富文本编辑器组件,支持丰富的文本格式化、图片插入、图片上传等功能。 + +> 如果文档内没有参数说明,可以尝试在在线示例内寻找 + +::: info 写在前面 + +如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。 + +::: + +## 基础用法 + + + +## 组件列表 + +### VbenTiptap + +富文本编辑器主组件。 + +### VbenTiptapPreview + +富文本内容预览组件,用于只读展示编辑器内容。 + +## API + +### Props + +| 属性名 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| `modelValue` (v-model) | 编辑器内容(HTML字符串) | `string` | `''` | +| `editable` | 是否可编辑 | `boolean` | `true` | +| `toolbar` | 是否显示工具栏 | `boolean` | `true` | +| `previewable` | 是否显示预览按钮 | `boolean` | `true` | +| `placeholder` | 占位提示文本 | `string` | - | +| `minHeight` | 最小高度 | `number \| string` | `240` | +| `maxHeight` | 最大高度 | `number \| string` | `400` | +| `extensions` | 自定义 Tiptap 扩展配置 | `Extensions` | - | +| `imageUpload` | 图片上传配置 | `ImageUploadOptions` | - | + +### Events + +| 事件名 | 说明 | 参数类型 | +| -------- | -------------- | ----------------------- | +| `change` | 内容变化时触发 | `VbenTiptapChangeEvent` | + +#### VbenTiptapChangeEvent + +```ts +interface VbenTiptapChangeEvent { + html: string; // HTML 内容 + json: JSONContent; // JSON 结构内容 + text: string; // 纯文本内容 +} +``` + +### ImageUploadOptions + +图片上传配置项: + +```ts +interface ImageUploadOptions { + /** 允许的文件类型,默认 'image/*' */ + accept?: string; + /** 最大文件大小(字节),默认 5MB */ + maxSize?: number; + /** 上传失败回调,未提供时使用 alert 弹窗提示 */ + onUploadError?: (error: unknown) => void; + /** 上传函数,返回图片 URL */ + upload: ( + file: File, + onProgress?: (percent: number) => void, + ) => Promise; +} +``` + +### VbenTiptapPreview Props + +| 属性名 | 说明 | 类型 | 默认值 | +| ----------- | ------------------ | ------------------ | ------ | +| `content` | 要预览的 HTML 内容 | `string` | `''` | +| `minHeight` | 最小高度 | `number \| string` | `160` | +| `class` | 自定义类名 | `any` | - | + +## 工具栏功能 + +编辑器工具栏提供以下功能: + +### 格式化 + +- **撤销/重做** - 撤销或重做编辑操作 +- **清除格式** - 清除选中文本的所有格式 +- **粗体** - 加粗文本 +- **斜体** - 斜体文本 +- **下划线** - 下划线文本 +- **删除线** - 删除线文本 +- **行内代码** - 行内代码标记 + +### 结构 + +- **标题** - 段落、H1-H4 标题切换 +- **有序列表** - 有序编号列表 +- **无序列表** - 无序符号列表 +- **引用块** - 引用块样式 +- **代码块** - 多行代码块 + +### 链接与图片 + +- **插入链接** - 插入或编辑超链接 +- **移除链接** - 移除选中文本的链接 +- **插入图片** - 通过 URL 插入图片 + +### 样式 + +- **文字颜色** - 设置文字颜色(预设色板) +- **背景颜色** - 设置文字背景高亮颜色 + +### 对齐 + +- **左对齐** - 文本左对齐 +- **居中对齐** - 文本居中对齐 +- **右对齐** - 文本右对齐 + +### 其他 + +- **预览** - 在弹窗中预览编辑内容 + +## 图片上传 + + + +当配置 `imageUpload` 时,工具栏的图片按钮会变为下拉菜单,包含「本地上传」和「URL 插入」两个选项。 + +### 上传方式 + +支持三种图片上传方式: + +1. **文件选择** - 点击工具栏本地上传按钮 +2. **拖拽上传** - 直接拖拽图片到编辑器区域 +3. **粘贴上传** - 粘贴图片到编辑器 + +### 上传进度显示 + +上传过程中会显示: + +- **加载指示器** - 旋转动画指示上传进行中 +- **进度条** - 当上传函数提供 `onProgress` 回调时,显示进度条 + +### 文件校验 + +- `accept` - 指定允许的文件类型(MIME类型) +- `maxSize` - 最大文件大小限制(字节) +- 校验失败时会触发 `onUploadError` 回调或默认 alert 提示 + +::: warning 注意事项 + +- 仅支持单张图片上传,多图拖拽/粘贴时会提示并仅处理第一张 +- 上传中不要保存编辑器内容(`getHTML()`),因为此时图片 URL 为临时 blob URL +- 自定义 `extensions` 时,图片上传功能将不显示(因为可能缺少 uploadImage 命令) + +::: + +## 自定义扩展 + +通过 `extensions` 属性可以传入自定义的 Tiptap 扩展配置: + +```vue + + + +``` + +::: warning 自定义扩展注意事项 + +使用自定义 `extensions` 时: + +- 默认扩展配置将不会生效 +- 图片上传功能不可用(工具栏不显示上传选项) +- 需自行配置所需的编辑器功能 + +::: diff --git a/docs/src/components/common-ui/vben-vxe-table.md b/docs/src/components/common-ui/vben-vxe-table.md index 8e3088a05..b9148f8c1 100644 --- a/docs/src/components/common-ui/vben-vxe-table.md +++ b/docs/src/components/common-ui/vben-vxe-table.md @@ -25,7 +25,7 @@ import { h } from 'vue'; import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; -import { Button, Image } from 'ant-design-vue'; +import { Button, Image } from 'antdv-next'; import { useVbenForm } from './form'; diff --git a/docs/src/demos/vben-alert/alert/index.vue b/docs/src/demos/vben-alert/alert/index.vue index 9ba18a4da..89ae16087 100644 --- a/docs/src/demos/vben-alert/alert/index.vue +++ b/docs/src/demos/vben-alert/alert/index.vue @@ -3,7 +3,7 @@ import { h } from 'vue'; import { alert, VbenButton } from '@vben/common-ui'; -import { Result } from 'ant-design-vue'; +import { Result } from 'antdv-next'; function showAlert() { alert('This is an alert message'); diff --git a/docs/src/demos/vben-alert/confirm/index.vue b/docs/src/demos/vben-alert/confirm/index.vue index 723445d96..4264f9dc9 100644 --- a/docs/src/demos/vben-alert/confirm/index.vue +++ b/docs/src/demos/vben-alert/confirm/index.vue @@ -3,7 +3,7 @@ import { h, ref } from 'vue'; import { alert, confirm, VbenButton } from '@vben/common-ui'; -import { Checkbox, message } from 'ant-design-vue'; +import { Checkbox, message } from 'antdv-next'; function showConfirm() { confirm('This is an alert message') diff --git a/docs/src/demos/vben-alert/prompt/index.vue b/docs/src/demos/vben-alert/prompt/index.vue index a19221347..5109c4791 100644 --- a/docs/src/demos/vben-alert/prompt/index.vue +++ b/docs/src/demos/vben-alert/prompt/index.vue @@ -3,7 +3,7 @@ import { h } from 'vue'; import { alert, prompt, useAlertContext, VbenButton } from '@vben/common-ui'; -import { Input, RadioGroup, Select } from 'ant-design-vue'; +import { Input, RadioGroup, Select } from 'antdv-next'; import { BadgeJapaneseYen } from 'lucide-vue-next'; function showPrompt() { diff --git a/docs/src/demos/vben-api-component/cascader/index.vue b/docs/src/demos/vben-api-component/cascader/index.vue index 957964cd3..e7e856816 100644 --- a/docs/src/demos/vben-api-component/cascader/index.vue +++ b/docs/src/demos/vben-api-component/cascader/index.vue @@ -1,7 +1,7 @@ + + diff --git a/docs/src/demos/vben-cropper/basic/index.vue b/docs/src/demos/vben-cropper/basic/index.vue new file mode 100644 index 000000000..332a1c358 --- /dev/null +++ b/docs/src/demos/vben-cropper/basic/index.vue @@ -0,0 +1,70 @@ + + + diff --git a/docs/src/demos/vben-form/api/index.vue b/docs/src/demos/vben-form/api/index.vue index 786dc89e5..543f20dbf 100644 --- a/docs/src/demos/vben-form/api/index.vue +++ b/docs/src/demos/vben-form/api/index.vue @@ -1,5 +1,5 @@ + + diff --git a/docs/src/demos/vben-tiptap/image-upload/index.vue b/docs/src/demos/vben-tiptap/image-upload/index.vue new file mode 100644 index 000000000..db2bdf13d --- /dev/null +++ b/docs/src/demos/vben-tiptap/image-upload/index.vue @@ -0,0 +1,45 @@ + + + diff --git a/docs/src/demos/vben-vxe-table/basic/index.vue b/docs/src/demos/vben-vxe-table/basic/index.vue index 4b6b5a632..5480f9945 100644 --- a/docs/src/demos/vben-vxe-table/basic/index.vue +++ b/docs/src/demos/vben-vxe-table/basic/index.vue @@ -1,7 +1,7 @@ +``` + +#### getCropImage + +Crop and retrieve the image. + +```ts +getCropImage( + format?: 'image/jpeg' | 'image/png', + quality?: number, + outputType?: 'base64' | 'blob', + targetWidth?: number, + targetHeight?: number, +): Promise +``` + +**Parameters:** + +| Parameter | Type | Default | Description | +| --- | --- | --- | --- | +| `format` | `'image/jpeg' \| 'image/png'` | `'image/png'` | Output image format | +| `quality` | `number` | `0.92` | Compression quality (0-1), only effective for jpeg | +| `outputType` | `'base64' \| 'blob'` | `'blob'` | Output type, base64 string or Blob object | +| `targetWidth` | `number` | - | Target width, defaults to original crop width if omitted | +| `targetHeight` | `number` | - | Target height, defaults to original crop height if omitted | + +## Features + +### Cropping Operations + +- **Drag to Move** - Drag the center area of the crop box to move its position +- **Corner Resize** - Drag the four corners to resize the crop box +- **Edge Resize** - Drag the midpoints of edges to adjust a single side + +### Aspect Ratio Control + +- **Free Ratio** - Without `aspectRatio`, adjust the crop box to any ratio +- **Fixed Ratio** - With `aspectRatio` set, the crop box maintains the specified ratio + +### HiDPI Support + +The component automatically adapts to Retina and other high-DPI screens, ensuring crisp output images. + +### Image Fitting + +- Images are automatically scaled to fit within the container +- Supports both local and remote images +- Remote images require CORS support from the server to export cropped results + +## Usage Example + +```vue + + + +``` diff --git a/docs/src/en/components/common-ui/vben-form.md b/docs/src/en/components/common-ui/vben-form.md index 58cd7a4d4..aa8d0589c 100644 --- a/docs/src/en/components/common-ui/vben-form.md +++ b/docs/src/en/components/common-ui/vben-form.md @@ -103,7 +103,7 @@ import { TimePicker, TreeSelect, Upload, -} from 'ant-design-vue'; +} from 'antdv-next'; const withDefaultPlaceholder = ( component: T, diff --git a/docs/src/en/components/common-ui/vben-tiptap.md b/docs/src/en/components/common-ui/vben-tiptap.md new file mode 100644 index 000000000..e9dfe8b88 --- /dev/null +++ b/docs/src/en/components/common-ui/vben-tiptap.md @@ -0,0 +1,199 @@ +--- +outline: deep +--- + +# Vben Tiptap Rich Text Editor + +A rich text editor component built on [Tiptap](https://tiptap.dev/), supporting rich text formatting, image insertion, and image upload features. + +> If some details are not obvious from the docs, check the live demos as well. + +::: info Note + +If you feel the current component implementation doesn't meet your needs, you can use native components directly or create your own component. The components provided by the framework are not constraints - use them at your discretion. + +::: + +## Basic Usage + + + +## Component List + +### VbenTiptap + +Main rich text editor component. + +### VbenTiptapPreview + +Read-only preview component for displaying editor content. + +## API + +### Props + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| `modelValue` (v-model) | Editor content (HTML string) | `string` | `''` | +| `editable` | Whether the editor is editable | `boolean` | `true` | +| `toolbar` | Whether to show the toolbar | `boolean` | `true` | +| `previewable` | Whether to show the preview button | `boolean` | `true` | +| `placeholder` | Placeholder text | `string` | - | +| `minHeight` | Minimum height | `number \| string` | `240` | +| `maxHeight` | Maximum height | `number \| string` | `400` | +| `extensions` | Custom Tiptap extensions | `Extensions` | - | +| `imageUpload` | Image upload configuration | `ImageUploadOptions` | - | + +### Events + +| Event | Description | Parameters | +| -------- | ------------------------------ | ----------------------- | +| `change` | Triggered when content changes | `VbenTiptapChangeEvent` | + +#### VbenTiptapChangeEvent + +```ts +interface VbenTiptapChangeEvent { + html: string; // HTML content + json: JSONContent; // JSON structure + text: string; // Plain text content +} +``` + +### ImageUploadOptions + +Image upload configuration: + +```ts +interface ImageUploadOptions { + /** Allowed file types, default 'image/*' */ + accept?: string; + /** Max file size in bytes, default 5MB */ + maxSize?: number; + /** Upload error callback, uses alert if not provided */ + onUploadError?: (error: unknown) => void; + /** Upload function, returns image URL */ + upload: ( + file: File, + onProgress?: (percent: number) => void, + ) => Promise; +} +``` + +### VbenTiptapPreview Props + +| Property | Description | Type | Default | +| ----------- | ----------------------- | ------------------ | ------- | +| `content` | HTML content to preview | `string` | `''` | +| `minHeight` | Minimum height | `number \| string` | `160` | +| `class` | Custom class name | `any` | - | + +## Toolbar Features + +The editor toolbar provides the following features: + +### Formatting + +- **Undo/Redo** - Undo or redo editing operations +- **Clear Formatting** - Remove all formatting from selected text +- **Bold** - Bold text +- **Italic** - Italic text +- **Underline** - Underline text +- **Strikethrough** - Strikethrough text +- **Inline Code** - Inline code mark + +### Structure + +- **Headings** - Paragraph, H1-H4 heading switching +- **Ordered List** - Numbered list +- **Bullet List** - Bulleted list +- **Blockquote** - Quote block style +- **Code Block** - Multi-line code block + +### Links & Images + +- **Insert Link** - Insert or edit hyperlinks +- **Remove Link** - Remove link from selected text +- **Insert Image** - Insert image via URL + +### Style + +- **Text Color** - Set text color (preset palette) +- **Highlight Color** - Set text background highlight color + +### Alignment + +- **Align Left** - Left align text +- **Align Center** - Center align text +- **Align Right** - Right align text + +### Other + +- **Preview** - Preview content in a modal + +## Image Upload + + + +When `imageUpload` is configured, the toolbar image button becomes a dropdown menu with "Upload" and "URL" options. + +### Upload Methods + +Three image upload methods are supported: + +1. **File Selection** - Click the upload button in toolbar +2. **Drag & Drop** - Drag images directly into the editor +3. **Paste** - Paste images into the editor + +### Upload Progress Display + +During upload: + +- **Loading Indicator** - Spinner animation indicating upload in progress +- **Progress Bar** - Shows progress bar when upload function provides `onProgress` callback + +### File Validation + +- `accept` - Specify allowed file types (MIME types) +- `maxSize` - Maximum file size limit (bytes) +- Validation failure triggers `onUploadError` callback or default alert + +::: warning Important Notes + +- Only single image upload is supported; multi-image drag/paste will show a prompt and process only the first image +- Do not save editor content (`getHTML()`) during upload as image URLs are temporary blob URLs +- When using custom `extensions`, the image upload feature will not be available (toolbar won't show upload option) + +::: + +## Custom Extensions + +Pass custom Tiptap extension configurations via the `extensions` property: + +```vue + + + +``` + +::: warning Custom Extensions Note + +When using custom `extensions`: + +- Default extension configuration will not take effect +- Image upload feature is not available (toolbar won't show upload option) +- You need to configure all required editor features yourself + +::: diff --git a/docs/src/en/guide/essentials/external-module.md b/docs/src/en/guide/essentials/external-module.md index f0a6d6e3f..0b1fc22b7 100644 --- a/docs/src/en/guide/essentials/external-module.md +++ b/docs/src/en/guide/essentials/external-module.md @@ -1,6 +1,6 @@ # External Modules -In addition to the external modules that are included by default in the project, sometimes we need to import other external modules. Let's take [ant-design-vue](https://antdv.com/components/overview) as an example: +In addition to the external modules that are included by default in the project, sometimes we need to import other external modules. Let's take [antdv-next](https://antdv.com/components/overview) as an example: ## Installing Dependencies @@ -13,7 +13,7 @@ In addition to the external modules that are included by default in the project, ```bash # cd /path/to/your/package -pnpm add ant-design-vue +pnpm add antdv-next ``` ## Usage @@ -22,9 +22,9 @@ pnpm add ant-design-vue ```ts import { createApp } from 'vue'; -import Antd from 'ant-design-vue'; +import Antd from 'antdv-next'; import App from './App'; -import 'ant-design-vue/dist/reset.css'; +import 'antdv-next/dist/reset.css'; const app = createApp(App); @@ -43,7 +43,7 @@ app.use(Antd).mount('#app'); ```vue