From 2264eaae18a3b19684e973157612fde8b94f4b55 Mon Sep 17 00:00:00 2001 From: Jin Mao Date: Tue, 21 Oct 2025 10:47:39 +0800 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E9=9B=86?= =?UTF-8?q?=E6=88=90tdesign=E7=BB=84=E4=BB=B6=E7=9A=84apps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-tdesign/.env | 8 + apps/web-tdesign/.env.analyze | 7 + apps/web-tdesign/.env.development | 16 ++ apps/web-tdesign/.env.production | 19 ++ apps/web-tdesign/index.html | 35 +++ apps/web-tdesign/package.json | 50 ++++ apps/web-tdesign/postcss.config.mjs | 1 + apps/web-tdesign/public/favicon.ico | Bin 0 -> 5430 bytes .../src/adapter/component/index.ts | 212 ++++++++++++++ apps/web-tdesign/src/adapter/form.ts | 49 ++++ apps/web-tdesign/src/adapter/vxe-table.ts | 69 +++++ apps/web-tdesign/src/api/core/auth.ts | 51 ++++ apps/web-tdesign/src/api/core/index.ts | 3 + apps/web-tdesign/src/api/core/menu.ts | 10 + apps/web-tdesign/src/api/core/user.ts | 10 + apps/web-tdesign/src/api/index.ts | 1 + apps/web-tdesign/src/api/request.ts | 113 ++++++++ apps/web-tdesign/src/app.vue | 39 +++ apps/web-tdesign/src/bootstrap.ts | 78 +++++ apps/web-tdesign/src/layouts/auth.vue | 23 ++ apps/web-tdesign/src/layouts/basic.vue | 162 +++++++++++ apps/web-tdesign/src/layouts/index.ts | 6 + apps/web-tdesign/src/locales/README.md | 3 + apps/web-tdesign/src/locales/index.ts | 102 +++++++ .../src/locales/langs/en-US/demos.json | 12 + .../src/locales/langs/en-US/page.json | 14 + .../src/locales/langs/zh-CN/demos.json | 12 + .../src/locales/langs/zh-CN/page.json | 14 + apps/web-tdesign/src/main.ts | 31 ++ apps/web-tdesign/src/preferences.ts | 13 + apps/web-tdesign/src/router/access.ts | 42 +++ apps/web-tdesign/src/router/guard.ts | 133 +++++++++ apps/web-tdesign/src/router/index.ts | 37 +++ apps/web-tdesign/src/router/routes/core.ts | 97 +++++++ apps/web-tdesign/src/router/routes/index.ts | 37 +++ .../src/router/routes/modules/dashboard.ts | 38 +++ .../src/router/routes/modules/demos.ts | 28 ++ .../src/router/routes/modules/vben.ts | 81 ++++++ apps/web-tdesign/src/store/auth.ts | 117 ++++++++ apps/web-tdesign/src/store/index.ts | 1 + apps/web-tdesign/src/views/_core/README.md | 3 + .../src/views/_core/about/index.vue | 9 + .../views/_core/authentication/code-login.vue | 69 +++++ .../_core/authentication/forget-password.vue | 43 +++ .../src/views/_core/authentication/login.vue | 99 +++++++ .../_core/authentication/qrcode-login.vue | 10 + .../views/_core/authentication/register.vue | 96 +++++++ .../src/views/_core/fallback/coming-soon.vue | 7 + .../src/views/_core/fallback/forbidden.vue | 9 + .../views/_core/fallback/internal-error.vue | 9 + .../src/views/_core/fallback/not-found.vue | 9 + .../src/views/_core/fallback/offline.vue | 9 + .../dashboard/analytics/analytics-trends.vue | 98 +++++++ .../analytics/analytics-visits-data.vue | 82 ++++++ .../analytics/analytics-visits-sales.vue | 46 +++ .../analytics/analytics-visits-source.vue | 65 +++++ .../dashboard/analytics/analytics-visits.vue | 55 ++++ .../src/views/dashboard/analytics/index.vue | 90 ++++++ .../src/views/dashboard/workspace/index.vue | 266 ++++++++++++++++++ .../src/views/demos/antd/index.vue | 66 +++++ apps/web-tdesign/tailwind.config.mjs | 1 + apps/web-tdesign/tsconfig.json | 12 + apps/web-tdesign/tsconfig.node.json | 10 + apps/web-tdesign/vite.config.mts | 20 ++ 64 files changed, 2957 insertions(+) create mode 100644 apps/web-tdesign/.env create mode 100644 apps/web-tdesign/.env.analyze create mode 100644 apps/web-tdesign/.env.development create mode 100644 apps/web-tdesign/.env.production create mode 100644 apps/web-tdesign/index.html create mode 100644 apps/web-tdesign/package.json create mode 100644 apps/web-tdesign/postcss.config.mjs create mode 100644 apps/web-tdesign/public/favicon.ico create mode 100644 apps/web-tdesign/src/adapter/component/index.ts create mode 100644 apps/web-tdesign/src/adapter/form.ts create mode 100644 apps/web-tdesign/src/adapter/vxe-table.ts create mode 100644 apps/web-tdesign/src/api/core/auth.ts create mode 100644 apps/web-tdesign/src/api/core/index.ts create mode 100644 apps/web-tdesign/src/api/core/menu.ts create mode 100644 apps/web-tdesign/src/api/core/user.ts create mode 100644 apps/web-tdesign/src/api/index.ts create mode 100644 apps/web-tdesign/src/api/request.ts create mode 100644 apps/web-tdesign/src/app.vue create mode 100644 apps/web-tdesign/src/bootstrap.ts create mode 100644 apps/web-tdesign/src/layouts/auth.vue create mode 100644 apps/web-tdesign/src/layouts/basic.vue create mode 100644 apps/web-tdesign/src/layouts/index.ts create mode 100644 apps/web-tdesign/src/locales/README.md create mode 100644 apps/web-tdesign/src/locales/index.ts create mode 100644 apps/web-tdesign/src/locales/langs/en-US/demos.json create mode 100644 apps/web-tdesign/src/locales/langs/en-US/page.json create mode 100644 apps/web-tdesign/src/locales/langs/zh-CN/demos.json create mode 100644 apps/web-tdesign/src/locales/langs/zh-CN/page.json create mode 100644 apps/web-tdesign/src/main.ts create mode 100644 apps/web-tdesign/src/preferences.ts create mode 100644 apps/web-tdesign/src/router/access.ts create mode 100644 apps/web-tdesign/src/router/guard.ts create mode 100644 apps/web-tdesign/src/router/index.ts create mode 100644 apps/web-tdesign/src/router/routes/core.ts create mode 100644 apps/web-tdesign/src/router/routes/index.ts create mode 100644 apps/web-tdesign/src/router/routes/modules/dashboard.ts create mode 100644 apps/web-tdesign/src/router/routes/modules/demos.ts create mode 100644 apps/web-tdesign/src/router/routes/modules/vben.ts create mode 100644 apps/web-tdesign/src/store/auth.ts create mode 100644 apps/web-tdesign/src/store/index.ts create mode 100644 apps/web-tdesign/src/views/_core/README.md create mode 100644 apps/web-tdesign/src/views/_core/about/index.vue create mode 100644 apps/web-tdesign/src/views/_core/authentication/code-login.vue create mode 100644 apps/web-tdesign/src/views/_core/authentication/forget-password.vue create mode 100644 apps/web-tdesign/src/views/_core/authentication/login.vue create mode 100644 apps/web-tdesign/src/views/_core/authentication/qrcode-login.vue create mode 100644 apps/web-tdesign/src/views/_core/authentication/register.vue create mode 100644 apps/web-tdesign/src/views/_core/fallback/coming-soon.vue create mode 100644 apps/web-tdesign/src/views/_core/fallback/forbidden.vue create mode 100644 apps/web-tdesign/src/views/_core/fallback/internal-error.vue create mode 100644 apps/web-tdesign/src/views/_core/fallback/not-found.vue create mode 100644 apps/web-tdesign/src/views/_core/fallback/offline.vue create mode 100644 apps/web-tdesign/src/views/dashboard/analytics/analytics-trends.vue create mode 100644 apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-data.vue create mode 100644 apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-sales.vue create mode 100644 apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-source.vue create mode 100644 apps/web-tdesign/src/views/dashboard/analytics/analytics-visits.vue create mode 100644 apps/web-tdesign/src/views/dashboard/analytics/index.vue create mode 100644 apps/web-tdesign/src/views/dashboard/workspace/index.vue create mode 100644 apps/web-tdesign/src/views/demos/antd/index.vue create mode 100644 apps/web-tdesign/tailwind.config.mjs create mode 100644 apps/web-tdesign/tsconfig.json create mode 100644 apps/web-tdesign/tsconfig.node.json create mode 100644 apps/web-tdesign/vite.config.mts diff --git a/apps/web-tdesign/.env b/apps/web-tdesign/.env new file mode 100644 index 00000000000..19735f36fda --- /dev/null +++ b/apps/web-tdesign/.env @@ -0,0 +1,8 @@ +# 应用标题 +VITE_APP_TITLE=Vben Admin Antd + +# 应用命名空间,用于缓存、store等功能的前缀,确保隔离 +VITE_APP_NAMESPACE=vben-web-antd + +# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密 +VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key diff --git a/apps/web-tdesign/.env.analyze b/apps/web-tdesign/.env.analyze new file mode 100644 index 00000000000..ffafa8dd529 --- /dev/null +++ b/apps/web-tdesign/.env.analyze @@ -0,0 +1,7 @@ +# public path +VITE_BASE=/ + +# Basic interface address SPA +VITE_GLOB_API_URL=/api + +VITE_VISUALIZER=true diff --git a/apps/web-tdesign/.env.development b/apps/web-tdesign/.env.development new file mode 100644 index 00000000000..1eae9e67d48 --- /dev/null +++ b/apps/web-tdesign/.env.development @@ -0,0 +1,16 @@ +# 端口号 +VITE_PORT=5666 + +VITE_BASE=/ + +# 接口地址 +VITE_GLOB_API_URL=/api + +# 是否开启 Nitro Mock服务,true 为开启,false 为关闭 +VITE_NITRO_MOCK=false + +# 是否打开 devtools,true 为打开,false 为关闭 +VITE_DEVTOOLS=false + +# 是否注入全局loading +VITE_INJECT_APP_LOADING=true diff --git a/apps/web-tdesign/.env.production b/apps/web-tdesign/.env.production new file mode 100644 index 00000000000..5375847a6ca --- /dev/null +++ b/apps/web-tdesign/.env.production @@ -0,0 +1,19 @@ +VITE_BASE=/ + +# 接口地址 +VITE_GLOB_API_URL=https://mock-napi.vben.pro/api + +# 是否开启压缩,可以设置为 none, brotli, gzip +VITE_COMPRESS=none + +# 是否开启 PWA +VITE_PWA=false + +# vue-router 的模式 +VITE_ROUTER_HISTORY=hash + +# 是否注入全局loading +VITE_INJECT_APP_LOADING=true + +# 打包后是否生成dist.zip +VITE_ARCHIVER=true diff --git a/apps/web-tdesign/index.html b/apps/web-tdesign/index.html new file mode 100644 index 00000000000..480eb84de6a --- /dev/null +++ b/apps/web-tdesign/index.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + <%= VITE_APP_TITLE %> + + + + +
+ + + diff --git a/apps/web-tdesign/package.json b/apps/web-tdesign/package.json new file mode 100644 index 00000000000..533d136196e --- /dev/null +++ b/apps/web-tdesign/package.json @@ -0,0 +1,50 @@ +{ + "name": "@vben/web-tdesign", + "version": "5.5.9", + "homepage": "https://vben.pro", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "apps/web-naive" + }, + "license": "MIT", + "author": { + "name": "vben", + "email": "ann.vben@gmail.com", + "url": "https://github.com/anncwb" + }, + "type": "module", + "scripts": { + "build": "pnpm vite build --mode production", + "build:analyze": "pnpm vite build --mode analyze", + "dev": "pnpm vite --mode development", + "preview": "vite preview", + "typecheck": "vue-tsc --noEmit --skipLibCheck" + }, + "imports": { + "#/*": "./src/*" + }, + "dependencies": { + "@vben/access": "workspace:*", + "@vben/common-ui": "workspace:*", + "@vben/constants": "workspace:*", + "@vben/hooks": "workspace:*", + "@vben/icons": "workspace:*", + "@vben/layouts": "workspace:*", + "@vben/locales": "workspace:*", + "@vben/plugins": "workspace:*", + "@vben/preferences": "workspace:*", + "@vben/request": "workspace:*", + "@vben/stores": "workspace:*", + "@vben/styles": "workspace:*", + "@vben/types": "workspace:*", + "@vben/utils": "workspace:*", + "@vueuse/core": "catalog:", + "dayjs": "catalog:", + "pinia": "catalog:", + "tdesign-vue-next": "^1.17.1", + "vue": "catalog:", + "vue-router": "catalog:" + }, +} diff --git a/apps/web-tdesign/postcss.config.mjs b/apps/web-tdesign/postcss.config.mjs new file mode 100644 index 00000000000..3d807045561 --- /dev/null +++ b/apps/web-tdesign/postcss.config.mjs @@ -0,0 +1 @@ +export { default } from '@vben/tailwind-config/postcss'; diff --git a/apps/web-tdesign/public/favicon.ico b/apps/web-tdesign/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fcf9818e2cf855039b272bdbfbb202d3ff3fa159 GIT binary patch literal 5430 zcmbtY33L=y7JY$12T+8gBk14=GU5gfn~Es=nm`C4AtVq2WF`B)l8}u>kcfdG`xZh7 z1QH@k!WNbQA}Y8rB4pn?Rn^_qRo&T0_nY^tI}C`PBiiTO^QwMT{dfQSKkwZa04wMM zy?X=M0kG_E0D}QwzyR}o4vl|CV{g(JUD6xoaWVij{_9_Z+hnMBoj6eCWqqZ|bDG3? zP1V)7jjOBM^TxNWH(wK5z8fU#uMgCSB?DD--$H2RPrcYwmDwZk?iT=9oB(QH+axi3 z9?DS*P#;M)Y%a>#V-YKdAXdDsz*NrcJ2dtV8t=Z2nsdGH*5&qiyUE>Vm@dqrLZt6> z(Th+Yvk>*s^HEHlg>AP+S>_mmri!6xDj#NlJ7oxWQ{b1Xzn%o3F2 zJy4EYjGB~r*z(;Z#Hz7qs`{9|A<#8ejX|uNj3&YMhCXf82Xb<7t(@Sce5a-F#rS2Y zPw=KW-BEkM4vp1Q(NsN!FqN*!*3f;EooLj|vHO`eL|&WtiJTISa&ibti29;|D&^HFD6dLJF=aUl zQK_g7S%X4w3TiekkUA=zPMK5-kDF8rPMA1{9Ua2-!AA8AkaE6;=1?EdZ_7=RH)f)o zz6YhW?I?uqK+bnR@_w194oGPgLT0)mW_M}f{ky5|&9E|QX9CL8LEd)~4EqY9IqS?o z{kEgO$QcJ|@3tc!kcFIoHX)1V*p0kb)a5#dX?;}TF3?iv0EkJTD|G@qe-~Pce+GHo zUNGz}jFvZLqdGDZxqw5+`{xi25@;Up%^i&{5ngJKR4B7`edHK7AGr+82hKvv;WMv@ zYYyG!LyjO9l#6`eVdBv_&jI9pw_i|sXY^1l+y2P+cXkv&)3*J<2c8E_(&=zM;!EU0 z@{kKYf_xCoLGuVSpE^80voR>Un<^;hk?+}!ZdX2t>o0=%S;@=7@}lqg@B-vQ^J$Kw z$kRMRNM0isbo4zwI1l*X$GyH9LO9vCUO3gaUR~I)UQ^VszB=pq zJ72ARzCd00QaRnLwP*Ti2{J*f&gxTfBdPm^TdR65(H6Yc<=&PT;fM6WaKmm2a4u7z zY0)I8b)GPl_qG;vVa_N8*`pjZ+l*jlZISnwfl zwR4>1*Y=-YMWLuCHstj@R+ZJSN6o<(K)=KqG#)n4uxOg27&xazjC4dT`IKUoTJp9N z>Z6DU%ij@>sEM{gL&=Bc{L6Cp!%%!u97-9Vcg-A>{BGewG17na4{SQ#y`>@VSqJV! zcc^fj1ovi-e@^h8bzUMLBKgvWJD@glA>~?k%K0vo`<)5%mHaPJ4dFIT#QUX`ua$K_ zv`!@kR1+xvrt;y$@z-$}g2rF|jbd>xY&y~NLfzpPo|paXq0w!|VqJ(syG(v`(M}c) z^C(YyQq5S5hFBLgC>)$ACQisl+Msy;Bg*%~&Gm=YYGUiHG=tS7V<~&aI8v>n*qgzw zhhqDW|3clxUTvDQeS)f;roW`|oBNea>y*ilMC(*IP%N}m_vCnQ;y|^Jp0PN{>^bdE zL)sKg8Kh+_=+ri~?4IeHdE|Qz-@)6CF{)<) zjs-BRjtBk54es*F==&1+kV)$>F7gULf-mibH*%4rpOQ!mtq3B}^A+qlAw8#G}T>Y-?wcHN#jlo%oSHYH%Rm4ro+cZ_?b3=+aWV5Y&5fK;E$%$f_)8BCDP)cJNdD z5dR5M*#fwd?XFZ~Zg(zdGj@V{^PXPPhOKAhPts6cvkv9eYc2Rhv0`cpHiRc5w`?T^8l1Q&Crn*oxsRVg_ar{VE#)>+#dL67hLvPFyvkW!mt=0{?cCmWIH@mr+o(*k>E2{86CkHbRwjbs_ z;eGcaw{!*8Il9N8dkTDGI}Ok+=`mlwH&Byx8Vtv-fuZ&9*o3w*^g`-N}!37fsOYgx4us+7ZgITsO%AU}9@c0~6uE~i8yCA6W+4Kb70q*X~ z1AYEAXs;5x${9Jk)G_2cgnE{cP7c&Z5fA?1UP|ew%;$pjW$KWFCzc8j?yl(;c%DQUTT-YW=pQN6P`YqPGu^v$A11)>exgV^rWIbW%QR)c`>QsTb zBe+27tpc;3*gyL~x0Us6C9H27sZKoG$VZs_K92Q$ojlmNVC%O)pq^6AVikBK30GD_ zMZkWz8Mfn3`Trp!2gH5Xq1C9d64zZy7vc&@^BhNW56d_Z=v?p=3wAyk1d0(Kly|Gn zJG*~_Zw}N39{f|jJ3rFxybe;vRgkt^8=^@)U&}|GR5-Byw=)jB5)0x%RtPO import('tdesign-vue-next/es/auto-complete'), +); +const Button = defineAsyncComponent(() => import('tdesign-vue-next/es/button')); +const Checkbox = defineAsyncComponent( + () => import('tdesign-vue-next/es/checkbox'), +); +const CheckboxGroup = defineAsyncComponent(() => + import('tdesign-vue-next/es/checkbox').then((res) => res.CheckboxGroup), +); +const DatePicker = defineAsyncComponent( + () => import('tdesign-vue-next/es/date-picker'), +); +const Divider = defineAsyncComponent( + () => import('tdesign-vue-next/es/divider'), +); +const Input = defineAsyncComponent(() => import('tdesign-vue-next/es/input')); +const InputNumber = defineAsyncComponent( + () => import('tdesign-vue-next/es/input-number'), +); +// const InputPassword = defineAsyncComponent(() => +// import('tdesign-vue-next/es/input').then((res) => res.InputPassword), +// ); +// const Mentions = defineAsyncComponent( +// () => import('tdesign-vue-next/es/mentions'), +// ); +const Radio = defineAsyncComponent(() => import('tdesign-vue-next/es/radio')); +const RadioGroup = defineAsyncComponent(() => + import('tdesign-vue-next/es/radio').then((res) => res.RadioGroup), +); +const RangePicker = defineAsyncComponent(() => + import('tdesign-vue-next/es/date-picker').then((res) => res.DateRangePicker), +); +const Rate = defineAsyncComponent(() => import('tdesign-vue-next/es/rate')); +const Select = defineAsyncComponent(() => import('tdesign-vue-next/es/select')); +const Space = defineAsyncComponent(() => import('tdesign-vue-next/es/space')); +const Switch = defineAsyncComponent(() => import('tdesign-vue-next/es/switch')); +const Textarea = defineAsyncComponent( + () => import('tdesign-vue-next/es/textarea'), +); +const TimePicker = defineAsyncComponent( + () => import('tdesign-vue-next/es/time-picker'), +); +const TreeSelect = defineAsyncComponent( + () => import('tdesign-vue-next/es/tree-select'), +); +const Upload = defineAsyncComponent(() => import('tdesign-vue-next/es/upload')); + +const withDefaultPlaceholder = ( + component: T, + type: 'input' | 'select', + componentProps: Recordable = {}, +) => { + return defineComponent({ + name: component.name, + inheritAttrs: false, + setup: (props: any, { attrs, expose, slots }) => { + const placeholder = + props?.placeholder || + attrs?.placeholder || + $t(`ui.placeholder.${type}`); + // 透传组件暴露的方法 + const innerRef = ref(); + expose( + new Proxy( + {}, + { + get: (_target, key) => innerRef.value?.[key], + has: (_target, key) => key in (innerRef.value || {}), + }, + ), + ); + return () => + h( + component, + { ...componentProps, placeholder, ...props, ...attrs, ref: innerRef }, + slots, + ); + }, + }); +}; + +// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 +export type ComponentType = + | 'ApiSelect' + | 'ApiTreeSelect' + | 'AutoComplete' + | 'Checkbox' + | 'CheckboxGroup' + | 'DatePicker' + | 'DefaultButton' + | 'Divider' + | 'IconPicker' + | 'Input' + | 'InputNumber' + // | 'InputPassword' + // | 'Mentions' + | 'PrimaryButton' + | 'Radio' + | 'RadioGroup' + | 'RangePicker' + | 'Rate' + | 'Select' + | 'Space' + | 'Switch' + | 'Textarea' + | 'TimePicker' + | 'TreeSelect' + | 'Upload' + | BaseFormComponentType; + +async function initComponentAdapter() { + const components: Partial> = { + // 如果你的组件体积比较大,可以使用异步加载 + // 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', + }, + ), + AutoComplete, + Checkbox, + CheckboxGroup, + DatePicker, + // 自定义默认按钮 + DefaultButton: (props, { attrs, slots }) => { + return h(Button, { ...props, attrs, theme: 'default' }, slots); + }, + Divider, + IconPicker: withDefaultPlaceholder(IconPicker, 'select', { + iconSlot: 'addonAfter', + inputComponent: Input, + modelValueProp: 'value', + }), + Input: withDefaultPlaceholder(Input, 'input'), + InputNumber: withDefaultPlaceholder(InputNumber, 'input'), + // InputPassword: withDefaultPlaceholder(InputPassword, 'input'), + // Mentions: withDefaultPlaceholder(Mentions, 'input'), + // 自定义主要按钮 + PrimaryButton: (props, { attrs, slots }) => { + return h(Button, { ...props, attrs, theme: 'primary' }, slots); + }, + Radio, + RadioGroup, + RangePicker, + Rate, + Select: withDefaultPlaceholder(Select, 'select'), + Space, + Switch, + Textarea: withDefaultPlaceholder(Textarea, 'input'), + TimePicker, + TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'), + Upload, + }; + + // 将组件注册到全局共享状态中 + globalShareState.setComponents(components); + + // 定义全局共享状态中的消息提示 + globalShareState.defineMessage({ + // 复制成功消息提示 + copyPreferencesSuccess: (title, content) => { + notification.success({ + description: content, + message: title, + placement: 'bottomRight', + }); + }, + }); +} + +export { initComponentAdapter }; diff --git a/apps/web-tdesign/src/adapter/form.ts b/apps/web-tdesign/src/adapter/form.ts new file mode 100644 index 00000000000..983a7f51698 --- /dev/null +++ b/apps/web-tdesign/src/adapter/form.ts @@ -0,0 +1,49 @@ +import type { + VbenFormSchema as FormSchema, + VbenFormProps, +} from '@vben/common-ui'; + +import type { ComponentType } from './component'; + +import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; +import { $t } from '@vben/locales'; + +async function initSetupVbenForm() { + setupVbenForm({ + config: { + // ant design vue组件库默认都是 v-model:value + baseModelPropName: 'value', + + // 一些组件是 v-model:checked 或者 v-model:fileList + modelPropNameMap: { + Checkbox: 'checked', + Radio: 'checked', + Switch: 'checked', + Upload: 'fileList', + }, + }, + defineRules: { + // 输入项目必填国际化适配 + required: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return $t('ui.formRules.required', [ctx.label]); + } + return true; + }, + // 选择项目必填国际化适配 + selectRequired: (value, _params, ctx) => { + if (value === undefined || value === null) { + return $t('ui.formRules.selectRequired', [ctx.label]); + } + return true; + }, + }, + }); +} + +const useVbenForm = useForm; + +export { initSetupVbenForm, useVbenForm, z }; + +export type VbenFormSchema = FormSchema; +export type { VbenFormProps }; diff --git a/apps/web-tdesign/src/adapter/vxe-table.ts b/apps/web-tdesign/src/adapter/vxe-table.ts new file mode 100644 index 00000000000..7de2859de8b --- /dev/null +++ b/apps/web-tdesign/src/adapter/vxe-table.ts @@ -0,0 +1,69 @@ +import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; + +import { h } from 'vue'; + +import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; + +import { Button, Image } from 'ant-design-vue'; + +import { useVbenForm } from './form'; + +setupVbenVxeTable({ + configVxeTable: (vxeUI) => { + vxeUI.setConfig({ + grid: { + align: 'center', + border: false, + columnConfig: { + resizable: true, + }, + minHeight: 180, + formConfig: { + // 全局禁用vxe-table的表单配置,使用formOptions + enabled: false, + }, + proxyConfig: { + autoLoad: true, + response: { + result: 'items', + total: 'total', + list: 'items', + }, + showActiveMsg: true, + showResponseMsg: false, + }, + round: true, + showOverflow: true, + size: 'small', + } as VxeTableGridOptions, + }); + + // 表格配置项可以用 cellRender: { name: 'CellImage' }, + vxeUI.renderer.add('CellImage', { + renderTableDefault(_renderOpts, params) { + const { column, row } = params; + return h(Image, { src: row[column.field] }); + }, + }); + + // 表格配置项可以用 cellRender: { name: 'CellLink' }, + vxeUI.renderer.add('CellLink', { + renderTableDefault(renderOpts) { + const { props } = renderOpts; + return h( + Button, + { size: 'small', type: 'link' }, + { default: () => props?.text }, + ); + }, + }); + + // 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化 + // vxeUI.formats.add + }, + useVbenForm, +}); + +export { useVbenVxeGrid }; + +export type * from '@vben/plugins/vxe-table'; diff --git a/apps/web-tdesign/src/api/core/auth.ts b/apps/web-tdesign/src/api/core/auth.ts new file mode 100644 index 00000000000..71d9f994396 --- /dev/null +++ b/apps/web-tdesign/src/api/core/auth.ts @@ -0,0 +1,51 @@ +import { baseRequestClient, requestClient } from '#/api/request'; + +export namespace AuthApi { + /** 登录接口参数 */ + export interface LoginParams { + password?: string; + username?: string; + } + + /** 登录接口返回值 */ + export interface LoginResult { + accessToken: string; + } + + export interface RefreshTokenResult { + data: string; + status: number; + } +} + +/** + * 登录 + */ +export async function loginApi(data: AuthApi.LoginParams) { + return requestClient.post('/auth/login', data); +} + +/** + * 刷新accessToken + */ +export async function refreshTokenApi() { + return baseRequestClient.post('/auth/refresh', { + withCredentials: true, + }); +} + +/** + * 退出登录 + */ +export async function logoutApi() { + return baseRequestClient.post('/auth/logout', { + withCredentials: true, + }); +} + +/** + * 获取用户权限码 + */ +export async function getAccessCodesApi() { + return requestClient.get('/auth/codes'); +} diff --git a/apps/web-tdesign/src/api/core/index.ts b/apps/web-tdesign/src/api/core/index.ts new file mode 100644 index 00000000000..28a5aef47ef --- /dev/null +++ b/apps/web-tdesign/src/api/core/index.ts @@ -0,0 +1,3 @@ +export * from './auth'; +export * from './menu'; +export * from './user'; diff --git a/apps/web-tdesign/src/api/core/menu.ts b/apps/web-tdesign/src/api/core/menu.ts new file mode 100644 index 00000000000..9ef60b11cd2 --- /dev/null +++ b/apps/web-tdesign/src/api/core/menu.ts @@ -0,0 +1,10 @@ +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-tdesign/src/api/core/user.ts b/apps/web-tdesign/src/api/core/user.ts new file mode 100644 index 00000000000..7e28ea8489a --- /dev/null +++ b/apps/web-tdesign/src/api/core/user.ts @@ -0,0 +1,10 @@ +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-tdesign/src/api/index.ts b/apps/web-tdesign/src/api/index.ts new file mode 100644 index 00000000000..4b0e0413762 --- /dev/null +++ b/apps/web-tdesign/src/api/index.ts @@ -0,0 +1 @@ +export * from './core'; diff --git a/apps/web-tdesign/src/api/request.ts b/apps/web-tdesign/src/api/request.ts new file mode 100644 index 00000000000..288dddd09db --- /dev/null +++ b/apps/web-tdesign/src/api/request.ts @@ -0,0 +1,113 @@ +/** + * 该文件可自行根据业务逻辑进行调整 + */ +import type { RequestClientOptions } from '@vben/request'; + +import { useAppConfig } from '@vben/hooks'; +import { preferences } from '@vben/preferences'; +import { + authenticateResponseInterceptor, + defaultResponseInterceptor, + errorMessageResponseInterceptor, + RequestClient, +} from '@vben/request'; +import { useAccessStore } from '@vben/stores'; + +import { message } from 'ant-design-vue'; + +import { useAuthStore } from '#/store'; + +import { refreshTokenApi } from './core'; + +const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); + +function createRequestClient(baseURL: string, options?: RequestClientOptions) { + const client = new RequestClient({ + ...options, + baseURL, + }); + + /** + * 重新认证逻辑 + */ + async function doReAuthenticate() { + console.warn('Access token or refresh token is invalid or expired. '); + const accessStore = useAccessStore(); + const authStore = useAuthStore(); + accessStore.setAccessToken(null); + if ( + preferences.app.loginExpiredMode === 'modal' && + accessStore.isAccessChecked + ) { + accessStore.setLoginExpired(true); + } else { + await authStore.logout(); + } + } + + /** + * 刷新token逻辑 + */ + async function doRefreshToken() { + const accessStore = useAccessStore(); + const resp = await refreshTokenApi(); + const newToken = resp.data; + accessStore.setAccessToken(newToken); + return newToken; + } + + function formatToken(token: null | string) { + return token ? `Bearer ${token}` : null; + } + + // 请求头处理 + client.addRequestInterceptor({ + fulfilled: async (config) => { + const accessStore = useAccessStore(); + + config.headers.Authorization = formatToken(accessStore.accessToken); + config.headers['Accept-Language'] = preferences.app.locale; + return config; + }, + }); + + // 处理返回的响应数据格式 + client.addResponseInterceptor( + defaultResponseInterceptor({ + codeField: 'code', + dataField: 'data', + successCode: 0, + }), + ); + + // token过期的处理 + client.addResponseInterceptor( + authenticateResponseInterceptor({ + client, + doReAuthenticate, + doRefreshToken, + enableRefreshToken: preferences.app.enableRefreshToken, + formatToken, + }), + ); + + // 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里 + client.addResponseInterceptor( + errorMessageResponseInterceptor((msg: string, error) => { + // 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg + // 当前mock接口返回的错误字段是 error 或者 message + const responseData = error?.response?.data ?? {}; + const errorMessage = responseData?.error ?? responseData?.message ?? ''; + // 如果没有错误信息,则会根据状态码进行提示 + message.error(errorMessage || msg); + }), + ); + + return client; +} + +export const requestClient = createRequestClient(apiURL, { + responseReturn: 'data', +}); + +export const baseRequestClient = new RequestClient({ baseURL: apiURL }); diff --git a/apps/web-tdesign/src/app.vue b/apps/web-tdesign/src/app.vue new file mode 100644 index 00000000000..bbaccce13bc --- /dev/null +++ b/apps/web-tdesign/src/app.vue @@ -0,0 +1,39 @@ + + + diff --git a/apps/web-tdesign/src/bootstrap.ts b/apps/web-tdesign/src/bootstrap.ts new file mode 100644 index 00000000000..edcccea4ce6 --- /dev/null +++ b/apps/web-tdesign/src/bootstrap.ts @@ -0,0 +1,78 @@ +import { createApp, watchEffect } from 'vue'; + +import { registerAccessDirective } from '@vben/access'; +import { registerLoadingDirective } from '@vben/common-ui/es/loading'; +import { preferences } from '@vben/preferences'; +import { initStores } from '@vben/stores'; +import '@vben/styles'; +import '@vben/styles/antd'; + +import { useTitle } from '@vueuse/core'; + +import { $t, setupI18n } from '#/locales'; + +import { initComponentAdapter } from './adapter/component'; +import { initSetupVbenForm } from './adapter/form'; +import App from './app.vue'; +import { router } from './router'; + +// 引入组件库的少量全局样式变量 + +async function bootstrap(namespace: string) { + // 初始化组件适配器 + await initComponentAdapter(); + + // 初始化表单组件 + await initSetupVbenForm(); + + // // 设置弹窗的默认配置 + // setDefaultModalProps({ + // fullscreenButton: false, + // }); + // // 设置抽屉的默认配置 + // setDefaultDrawerProps({ + // zIndex: 1020, + // }); + + const app = createApp(App); + + // 注册v-loading指令 + registerLoadingDirective(app, { + loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令 + spinning: 'spinning', + }); + + // 国际化 i18n 配置 + await setupI18n(app); + + // 配置 pinia-tore + await initStores(app, { namespace }); + + // 安装权限指令 + registerAccessDirective(app); + + // 初始化 tippy + const { initTippy } = await import('@vben/common-ui/es/tippy'); + initTippy(app); + + // 配置路由及路由守卫 + app.use(router); + + // 配置Motion插件 + const { MotionPlugin } = await import('@vben/plugins/motion'); + app.use(MotionPlugin); + + // 动态更新标题 + watchEffect(() => { + if (preferences.app.dynamicTitle) { + const routeTitle = router.currentRoute.value.meta?.title; + const pageTitle = + (routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name; + useTitle(pageTitle); + } + }); + + app.mount('#app'); +} + +export { bootstrap }; diff --git a/apps/web-tdesign/src/layouts/auth.vue b/apps/web-tdesign/src/layouts/auth.vue new file mode 100644 index 00000000000..18d415bc7fb --- /dev/null +++ b/apps/web-tdesign/src/layouts/auth.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/web-tdesign/src/layouts/basic.vue b/apps/web-tdesign/src/layouts/basic.vue new file mode 100644 index 00000000000..805b8a73d2e --- /dev/null +++ b/apps/web-tdesign/src/layouts/basic.vue @@ -0,0 +1,162 @@ + + + diff --git a/apps/web-tdesign/src/layouts/index.ts b/apps/web-tdesign/src/layouts/index.ts new file mode 100644 index 00000000000..a4320780540 --- /dev/null +++ b/apps/web-tdesign/src/layouts/index.ts @@ -0,0 +1,6 @@ +const BasicLayout = () => import('./basic.vue'); +const AuthPageLayout = () => import('./auth.vue'); + +const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView); + +export { AuthPageLayout, BasicLayout, IFrameView }; diff --git a/apps/web-tdesign/src/locales/README.md b/apps/web-tdesign/src/locales/README.md new file mode 100644 index 00000000000..7b451032e6c --- /dev/null +++ b/apps/web-tdesign/src/locales/README.md @@ -0,0 +1,3 @@ +# locale + +每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。 diff --git a/apps/web-tdesign/src/locales/index.ts b/apps/web-tdesign/src/locales/index.ts new file mode 100644 index 00000000000..7f32bd18ef3 --- /dev/null +++ b/apps/web-tdesign/src/locales/index.ts @@ -0,0 +1,102 @@ +import type { Locale } from 'ant-design-vue/es/locale'; + +import type { App } from 'vue'; + +import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales'; + +import { ref } from 'vue'; + +import { + $t, + setupI18n as coreSetup, + loadLocalesMapFromDir, +} from '@vben/locales'; +import { preferences } from '@vben/preferences'; + +import antdEnLocale from 'ant-design-vue/es/locale/en_US'; +import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN'; +import dayjs from 'dayjs'; + +const antdLocale = ref(antdDefaultLocale); + +const modules = import.meta.glob('./langs/**/*.json'); + +const localesMap = loadLocalesMapFromDir( + /\.\/langs\/([^/]+)\/(.*)\.json$/, + modules, +); +/** + * 加载应用特有的语言包 + * 这里也可以改造为从服务端获取翻译数据 + * @param lang + */ +async function loadMessages(lang: SupportedLanguagesType) { + const [appLocaleMessages] = await Promise.all([ + localesMap[lang]?.(), + loadThirdPartyMessage(lang), + ]); + return appLocaleMessages?.default; +} + +/** + * 加载第三方组件库的语言包 + * @param lang + */ +async function loadThirdPartyMessage(lang: SupportedLanguagesType) { + await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]); +} + +/** + * 加载dayjs的语言包 + * @param lang + */ +async function loadDayjsLocale(lang: SupportedLanguagesType) { + let locale; + switch (lang) { + case 'en-US': { + locale = await import('dayjs/locale/en'); + break; + } + case 'zh-CN': { + locale = await import('dayjs/locale/zh-cn'); + break; + } + // 默认使用英语 + default: { + locale = await import('dayjs/locale/en'); + } + } + if (locale) { + dayjs.locale(locale); + } else { + console.error(`Failed to load dayjs locale for ${lang}`); + } +} + +/** + * 加载antd的语言包 + * @param lang + */ +async function loadAntdLocale(lang: SupportedLanguagesType) { + switch (lang) { + case 'en-US': { + antdLocale.value = antdEnLocale; + break; + } + case 'zh-CN': { + antdLocale.value = antdDefaultLocale; + break; + } + } +} + +async function setupI18n(app: App, options: LocaleSetupOptions = {}) { + await coreSetup(app, { + defaultLocale: preferences.app.locale, + loadMessages, + missingWarn: !import.meta.env.PROD, + ...options, + }); +} + +export { $t, antdLocale, setupI18n }; diff --git a/apps/web-tdesign/src/locales/langs/en-US/demos.json b/apps/web-tdesign/src/locales/langs/en-US/demos.json new file mode 100644 index 00000000000..071564349b7 --- /dev/null +++ b/apps/web-tdesign/src/locales/langs/en-US/demos.json @@ -0,0 +1,12 @@ +{ + "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-tdesign/src/locales/langs/en-US/page.json b/apps/web-tdesign/src/locales/langs/en-US/page.json new file mode 100644 index 00000000000..618a258c0a6 --- /dev/null +++ b/apps/web-tdesign/src/locales/langs/en-US/page.json @@ -0,0 +1,14 @@ +{ + "auth": { + "login": "Login", + "register": "Register", + "codeLogin": "Code Login", + "qrcodeLogin": "Qr Code Login", + "forgetPassword": "Forget Password" + }, + "dashboard": { + "title": "Dashboard", + "analytics": "Analytics", + "workspace": "Workspace" + } +} diff --git a/apps/web-tdesign/src/locales/langs/zh-CN/demos.json b/apps/web-tdesign/src/locales/langs/zh-CN/demos.json new file mode 100644 index 00000000000..93ee722f59f --- /dev/null +++ b/apps/web-tdesign/src/locales/langs/zh-CN/demos.json @@ -0,0 +1,12 @@ +{ + "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-tdesign/src/locales/langs/zh-CN/page.json b/apps/web-tdesign/src/locales/langs/zh-CN/page.json new file mode 100644 index 00000000000..4cb67081cbf --- /dev/null +++ b/apps/web-tdesign/src/locales/langs/zh-CN/page.json @@ -0,0 +1,14 @@ +{ + "auth": { + "login": "登录", + "register": "注册", + "codeLogin": "验证码登录", + "qrcodeLogin": "二维码登录", + "forgetPassword": "忘记密码" + }, + "dashboard": { + "title": "概览", + "analytics": "分析页", + "workspace": "工作台" + } +} diff --git a/apps/web-tdesign/src/main.ts b/apps/web-tdesign/src/main.ts new file mode 100644 index 00000000000..5d728a02acb --- /dev/null +++ b/apps/web-tdesign/src/main.ts @@ -0,0 +1,31 @@ +import { initPreferences } from '@vben/preferences'; +import { unmountGlobalLoading } from '@vben/utils'; + +import { overridesPreferences } from './preferences'; + +/** + * 应用初始化完成之后再进行页面加载渲染 + */ +async function initApplication() { + // name用于指定项目唯一标识 + // 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据 + const env = import.meta.env.PROD ? 'prod' : 'dev'; + const appVersion = import.meta.env.VITE_APP_VERSION; + const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`; + + // app偏好设置初始化 + await initPreferences({ + namespace, + overrides: overridesPreferences, + }); + + // 启动应用并挂载 + // vue应用主要逻辑及视图 + const { bootstrap } = await import('./bootstrap'); + await bootstrap(namespace); + + // 移除并销毁loading + unmountGlobalLoading(); +} + +initApplication(); diff --git a/apps/web-tdesign/src/preferences.ts b/apps/web-tdesign/src/preferences.ts new file mode 100644 index 00000000000..b2e9ace43c8 --- /dev/null +++ b/apps/web-tdesign/src/preferences.ts @@ -0,0 +1,13 @@ +import { defineOverridesPreferences } from '@vben/preferences'; + +/** + * @description 项目配置文件 + * 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置 + * !!! 更改配置后请清空缓存,否则可能不生效 + */ +export const overridesPreferences = defineOverridesPreferences({ + // overrides + app: { + name: import.meta.env.VITE_APP_TITLE, + }, +}); diff --git a/apps/web-tdesign/src/router/access.ts b/apps/web-tdesign/src/router/access.ts new file mode 100644 index 00000000000..3a48be2378c --- /dev/null +++ b/apps/web-tdesign/src/router/access.ts @@ -0,0 +1,42 @@ +import type { + ComponentRecordType, + GenerateMenuAndRoutesOptions, +} from '@vben/types'; + +import { generateAccessible } from '@vben/access'; +import { preferences } from '@vben/preferences'; + +import { message } from 'ant-design-vue'; + +import { getAllMenusApi } from '#/api'; +import { BasicLayout, IFrameView } from '#/layouts'; +import { $t } from '#/locales'; + +const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue'); + +async function generateAccess(options: GenerateMenuAndRoutesOptions) { + const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue'); + + const layoutMap: ComponentRecordType = { + BasicLayout, + IFrameView, + }; + + return await generateAccessible(preferences.app.accessMode, { + ...options, + fetchMenuListAsync: async () => { + message.loading({ + content: `${$t('common.loadingMenu')}...`, + duration: 1.5, + }); + return await getAllMenusApi(); + }, + // 可以指定没有权限跳转403页面 + forbiddenComponent, + // 如果 route.meta.menuVisibleWithForbidden = true + layoutMap, + pageMap, + }); +} + +export { generateAccess }; diff --git a/apps/web-tdesign/src/router/guard.ts b/apps/web-tdesign/src/router/guard.ts new file mode 100644 index 00000000000..a1ad6d88cff --- /dev/null +++ b/apps/web-tdesign/src/router/guard.ts @@ -0,0 +1,133 @@ +import type { Router } from 'vue-router'; + +import { LOGIN_PATH } from '@vben/constants'; +import { preferences } from '@vben/preferences'; +import { useAccessStore, useUserStore } from '@vben/stores'; +import { startProgress, stopProgress } from '@vben/utils'; + +import { accessRoutes, coreRouteNames } from '#/router/routes'; +import { useAuthStore } from '#/store'; + +import { generateAccess } from './access'; + +/** + * 通用守卫配置 + * @param router + */ +function setupCommonGuard(router: Router) { + // 记录已经加载的页面 + const loadedPaths = new Set(); + + router.beforeEach((to) => { + to.meta.loaded = loadedPaths.has(to.path); + + // 页面加载进度条 + if (!to.meta.loaded && preferences.transition.progress) { + startProgress(); + } + return true; + }); + + router.afterEach((to) => { + // 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行 + + loadedPaths.add(to.path); + + // 关闭页面加载进度条 + if (preferences.transition.progress) { + stopProgress(); + } + }); +} + +/** + * 权限访问守卫配置 + * @param router + */ +function setupAccessGuard(router: Router) { + router.beforeEach(async (to, from) => { + const accessStore = useAccessStore(); + const userStore = useUserStore(); + const authStore = useAuthStore(); + + // 基本路由,这些路由不需要进入权限拦截 + if (coreRouteNames.includes(to.name as string)) { + if (to.path === LOGIN_PATH && accessStore.accessToken) { + return decodeURIComponent( + (to.query?.redirect as string) || + userStore.userInfo?.homePath || + preferences.app.defaultHomePath, + ); + } + return true; + } + + // accessToken 检查 + if (!accessStore.accessToken) { + // 明确声明忽略权限访问权限,则可以访问 + if (to.meta.ignoreAccess) { + return true; + } + + // 没有访问权限,跳转登录页面 + if (to.fullPath !== LOGIN_PATH) { + return { + path: LOGIN_PATH, + // 如不需要,直接删除 query + query: + to.fullPath === preferences.app.defaultHomePath + ? {} + : { redirect: encodeURIComponent(to.fullPath) }, + // 携带当前跳转的页面,登录后重新跳转该页面 + replace: true, + }; + } + return to; + } + + // 是否已经生成过动态路由 + if (accessStore.isAccessChecked) { + return true; + } + + // 生成路由表 + // 当前登录用户拥有的角色标识列表 + const userInfo = userStore.userInfo || (await authStore.fetchUserInfo()); + const userRoles = userInfo.roles ?? []; + + // 生成菜单和路由 + const { accessibleMenus, accessibleRoutes } = await generateAccess({ + roles: userRoles, + router, + // 则会在菜单中显示,但是访问会被重定向到403 + routes: accessRoutes, + }); + + // 保存菜单信息和路由信息 + accessStore.setAccessMenus(accessibleMenus); + accessStore.setAccessRoutes(accessibleRoutes); + accessStore.setIsAccessChecked(true); + const redirectPath = (from.query.redirect ?? + (to.path === preferences.app.defaultHomePath + ? userInfo.homePath || preferences.app.defaultHomePath + : to.fullPath)) as string; + + return { + ...router.resolve(decodeURIComponent(redirectPath)), + replace: true, + }; + }); +} + +/** + * 项目守卫配置 + * @param router + */ +function createRouterGuard(router: Router) { + /** 通用 */ + setupCommonGuard(router); + /** 权限访问 */ + setupAccessGuard(router); +} + +export { createRouterGuard }; diff --git a/apps/web-tdesign/src/router/index.ts b/apps/web-tdesign/src/router/index.ts new file mode 100644 index 00000000000..48402303425 --- /dev/null +++ b/apps/web-tdesign/src/router/index.ts @@ -0,0 +1,37 @@ +import { + createRouter, + createWebHashHistory, + createWebHistory, +} from 'vue-router'; + +import { resetStaticRoutes } from '@vben/utils'; + +import { createRouterGuard } from './guard'; +import { routes } from './routes'; + +/** + * @zh_CN 创建vue-router实例 + */ +const router = createRouter({ + history: + import.meta.env.VITE_ROUTER_HISTORY === 'hash' + ? createWebHashHistory(import.meta.env.VITE_BASE) + : createWebHistory(import.meta.env.VITE_BASE), + // 应该添加到路由的初始路由列表。 + routes, + scrollBehavior: (to, _from, savedPosition) => { + if (savedPosition) { + return savedPosition; + } + return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 }; + }, + // 是否应该禁止尾部斜杠。 + // strict: true, +}); + +const resetRoutes = () => resetStaticRoutes(router, routes); + +// 创建路由守卫 +createRouterGuard(router); + +export { resetRoutes, router }; diff --git a/apps/web-tdesign/src/router/routes/core.ts b/apps/web-tdesign/src/router/routes/core.ts new file mode 100644 index 00000000000..949b0b65acf --- /dev/null +++ b/apps/web-tdesign/src/router/routes/core.ts @@ -0,0 +1,97 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { LOGIN_PATH } from '@vben/constants'; +import { preferences } from '@vben/preferences'; + +import { $t } from '#/locales'; + +const BasicLayout = () => import('#/layouts/basic.vue'); +const AuthPageLayout = () => import('#/layouts/auth.vue'); +/** 全局404页面 */ +const fallbackNotFoundRoute: RouteRecordRaw = { + component: () => import('#/views/_core/fallback/not-found.vue'), + meta: { + hideInBreadcrumb: true, + hideInMenu: true, + hideInTab: true, + title: '404', + }, + name: 'FallbackNotFound', + path: '/:path(.*)*', +}; + +/** 基本路由,这些路由是必须存在的 */ +const coreRoutes: RouteRecordRaw[] = [ + /** + * 根路由 + * 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。 + * 此路由必须存在,且不应修改 + */ + { + component: BasicLayout, + meta: { + hideInBreadcrumb: true, + title: 'Root', + }, + name: 'Root', + path: '/', + redirect: preferences.app.defaultHomePath, + children: [], + }, + { + component: AuthPageLayout, + meta: { + hideInTab: true, + title: 'Authentication', + }, + name: 'Authentication', + path: '/auth', + redirect: LOGIN_PATH, + children: [ + { + name: 'Login', + path: 'login', + component: () => import('#/views/_core/authentication/login.vue'), + meta: { + title: $t('page.auth.login'), + }, + }, + { + name: 'CodeLogin', + path: 'code-login', + component: () => import('#/views/_core/authentication/code-login.vue'), + meta: { + title: $t('page.auth.codeLogin'), + }, + }, + { + name: 'QrCodeLogin', + path: 'qrcode-login', + component: () => + import('#/views/_core/authentication/qrcode-login.vue'), + meta: { + title: $t('page.auth.qrcodeLogin'), + }, + }, + { + name: 'ForgetPassword', + path: 'forget-password', + component: () => + import('#/views/_core/authentication/forget-password.vue'), + meta: { + title: $t('page.auth.forgetPassword'), + }, + }, + { + name: 'Register', + path: 'register', + component: () => import('#/views/_core/authentication/register.vue'), + meta: { + title: $t('page.auth.register'), + }, + }, + ], + }, +]; + +export { coreRoutes, fallbackNotFoundRoute }; diff --git a/apps/web-tdesign/src/router/routes/index.ts b/apps/web-tdesign/src/router/routes/index.ts new file mode 100644 index 00000000000..e6fb1440204 --- /dev/null +++ b/apps/web-tdesign/src/router/routes/index.ts @@ -0,0 +1,37 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { mergeRouteModules, traverseTreeValues } from '@vben/utils'; + +import { coreRoutes, fallbackNotFoundRoute } from './core'; + +const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', { + eager: true, +}); + +// 有需要可以自行打开注释,并创建文件夹 +// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true }); +// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true }); + +/** 动态路由 */ +const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles); + +/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */ +// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles); +// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles); +const staticRoutes: RouteRecordRaw[] = []; +const externalRoutes: RouteRecordRaw[] = []; + +/** 路由列表,由基本路由、外部路由和404兜底路由组成 + * 无需走权限验证(会一直显示在菜单中) */ +const routes: RouteRecordRaw[] = [ + ...coreRoutes, + ...externalRoutes, + fallbackNotFoundRoute, +]; + +/** 基本路由列表,这些路由不需要进入权限拦截 */ +const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name); + +/** 有权限校验的路由列表,包含动态路由和静态路由 */ +const accessRoutes = [...dynamicRoutes, ...staticRoutes]; +export { accessRoutes, coreRouteNames, routes }; diff --git a/apps/web-tdesign/src/router/routes/modules/dashboard.ts b/apps/web-tdesign/src/router/routes/modules/dashboard.ts new file mode 100644 index 00000000000..5254dc65de4 --- /dev/null +++ b/apps/web-tdesign/src/router/routes/modules/dashboard.ts @@ -0,0 +1,38 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { $t } from '#/locales'; + +const routes: RouteRecordRaw[] = [ + { + meta: { + icon: 'lucide:layout-dashboard', + order: -1, + title: $t('page.dashboard.title'), + }, + name: 'Dashboard', + path: '/dashboard', + children: [ + { + name: 'Analytics', + path: '/analytics', + component: () => import('#/views/dashboard/analytics/index.vue'), + meta: { + affixTab: true, + icon: 'lucide:area-chart', + title: $t('page.dashboard.analytics'), + }, + }, + { + name: 'Workspace', + path: '/workspace', + component: () => import('#/views/dashboard/workspace/index.vue'), + meta: { + icon: 'carbon:workspace', + title: $t('page.dashboard.workspace'), + }, + }, + ], + }, +]; + +export default routes; diff --git a/apps/web-tdesign/src/router/routes/modules/demos.ts b/apps/web-tdesign/src/router/routes/modules/demos.ts new file mode 100644 index 00000000000..55ade09c954 --- /dev/null +++ b/apps/web-tdesign/src/router/routes/modules/demos.ts @@ -0,0 +1,28 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { $t } from '#/locales'; + +const routes: RouteRecordRaw[] = [ + { + meta: { + icon: 'ic:baseline-view-in-ar', + keepAlive: true, + order: 1000, + title: $t('demos.title'), + }, + name: 'Demos', + path: '/demos', + children: [ + { + meta: { + title: $t('demos.antd'), + }, + name: 'AntDesignDemos', + path: '/demos/ant-design', + component: () => import('#/views/demos/antd/index.vue'), + }, + ], + }, +]; + +export default routes; diff --git a/apps/web-tdesign/src/router/routes/modules/vben.ts b/apps/web-tdesign/src/router/routes/modules/vben.ts new file mode 100644 index 00000000000..98acf582118 --- /dev/null +++ b/apps/web-tdesign/src/router/routes/modules/vben.ts @@ -0,0 +1,81 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { + VBEN_DOC_URL, + VBEN_ELE_PREVIEW_URL, + VBEN_GITHUB_URL, + VBEN_LOGO_URL, + VBEN_NAIVE_PREVIEW_URL, +} from '@vben/constants'; + +import { IFrameView } from '#/layouts'; +import { $t } from '#/locales'; + +const routes: RouteRecordRaw[] = [ + { + meta: { + badgeType: 'dot', + icon: VBEN_LOGO_URL, + order: 9998, + title: $t('demos.vben.title'), + }, + name: 'VbenProject', + path: '/vben-admin', + children: [ + { + name: 'VbenDocument', + path: '/vben-admin/document', + component: IFrameView, + meta: { + icon: 'lucide:book-open-text', + link: VBEN_DOC_URL, + title: $t('demos.vben.document'), + }, + }, + { + name: 'VbenGithub', + path: '/vben-admin/github', + component: IFrameView, + meta: { + icon: 'mdi:github', + link: VBEN_GITHUB_URL, + title: 'Github', + }, + }, + { + name: 'VbenNaive', + path: '/vben-admin/naive', + component: IFrameView, + meta: { + badgeType: 'dot', + icon: 'logos:naiveui', + link: VBEN_NAIVE_PREVIEW_URL, + title: $t('demos.vben.naive-ui'), + }, + }, + { + name: 'VbenElementPlus', + path: '/vben-admin/ele', + component: IFrameView, + meta: { + badgeType: 'dot', + icon: 'logos:element', + link: VBEN_ELE_PREVIEW_URL, + title: $t('demos.vben.element-plus'), + }, + }, + ], + }, + { + name: 'VbenAbout', + path: '/vben-admin/about', + component: () => import('#/views/_core/about/index.vue'), + meta: { + icon: 'lucide:copyright', + title: $t('demos.vben.about'), + order: 9999, + }, + }, +]; + +export default routes; diff --git a/apps/web-tdesign/src/store/auth.ts b/apps/web-tdesign/src/store/auth.ts new file mode 100644 index 00000000000..312a74da247 --- /dev/null +++ b/apps/web-tdesign/src/store/auth.ts @@ -0,0 +1,117 @@ +import type { Recordable, UserInfo } from '@vben/types'; + +import { ref } from 'vue'; +import { useRouter } from 'vue-router'; + +import { LOGIN_PATH } from '@vben/constants'; +import { preferences } from '@vben/preferences'; +import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; + +import { notification } from 'ant-design-vue'; +import { defineStore } from 'pinia'; + +import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api'; +import { $t } from '#/locales'; + +export const useAuthStore = defineStore('auth', () => { + const accessStore = useAccessStore(); + const userStore = useUserStore(); + const router = useRouter(); + + const loginLoading = ref(false); + + /** + * 异步处理登录操作 + * Asynchronously handle the login process + * @param params 登录表单数据 + */ + async function authLogin( + params: Recordable, + onSuccess?: () => Promise | void, + ) { + // 异步处理用户登录操作并获取 accessToken + const userInfo: null | UserInfo = null; + try { + loginLoading.value = true; + const { accessToken } = await loginApi(params); + + // 如果成功获取到 accessToken + if (accessToken) { + accessStore.setAccessToken(accessToken); + // 获取用户信息并存储到 accessStore 中 + const [fetchUserInfoResult, accessCodes] = await Promise.all([ + fetchUserInfo(), + getAccessCodesApi(), + ]); + + userInfo = fetchUserInfoResult; + + userStore.setUserInfo(userInfo); + accessStore.setAccessCodes(accessCodes); + + if (accessStore.loginExpired) { + accessStore.setLoginExpired(false); + } else { + onSuccess + ? await onSuccess?.() + : await router.push( + userInfo.homePath || preferences.app.defaultHomePath, + ); + } + + if (userInfo?.realName) { + notification.success({ + description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`, + duration: 3, + message: $t('authentication.loginSuccess'), + }); + } + } + } finally { + loginLoading.value = false; + } + + return { + userInfo, + }; + } + + async function logout(redirect: boolean = true) { + try { + await logoutApi(); + } catch { + // 不做任何处理 + } + resetAllStores(); + accessStore.setLoginExpired(false); + + // 回登录页带上当前路由地址 + await router.replace({ + path: LOGIN_PATH, + query: redirect + ? { + redirect: encodeURIComponent(router.currentRoute.value.fullPath), + } + : {}, + }); + } + + async function fetchUserInfo() { + let userInfo: null | UserInfo = null; + userInfo = await getUserInfoApi(); + userStore.setUserInfo(userInfo); + return userInfo; + } + + function $reset() { + loginLoading.value = false; + } + + return { + $reset, + authLogin, + fetchUserInfo, + loginLoading, + logout, + }; +}); diff --git a/apps/web-tdesign/src/store/index.ts b/apps/web-tdesign/src/store/index.ts new file mode 100644 index 00000000000..269586ee8b8 --- /dev/null +++ b/apps/web-tdesign/src/store/index.ts @@ -0,0 +1 @@ +export * from './auth'; diff --git a/apps/web-tdesign/src/views/_core/README.md b/apps/web-tdesign/src/views/_core/README.md new file mode 100644 index 00000000000..8248afe6c10 --- /dev/null +++ b/apps/web-tdesign/src/views/_core/README.md @@ -0,0 +1,3 @@ +# \_core + +此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。 diff --git a/apps/web-tdesign/src/views/_core/about/index.vue b/apps/web-tdesign/src/views/_core/about/index.vue new file mode 100644 index 00000000000..0ee524335c2 --- /dev/null +++ b/apps/web-tdesign/src/views/_core/about/index.vue @@ -0,0 +1,9 @@ + + + diff --git a/apps/web-tdesign/src/views/_core/authentication/code-login.vue b/apps/web-tdesign/src/views/_core/authentication/code-login.vue new file mode 100644 index 00000000000..acfd1fd7881 --- /dev/null +++ b/apps/web-tdesign/src/views/_core/authentication/code-login.vue @@ -0,0 +1,69 @@ + + + diff --git a/apps/web-tdesign/src/views/_core/authentication/forget-password.vue b/apps/web-tdesign/src/views/_core/authentication/forget-password.vue new file mode 100644 index 00000000000..fef0d427945 --- /dev/null +++ b/apps/web-tdesign/src/views/_core/authentication/forget-password.vue @@ -0,0 +1,43 @@ + + + diff --git a/apps/web-tdesign/src/views/_core/authentication/login.vue b/apps/web-tdesign/src/views/_core/authentication/login.vue new file mode 100644 index 00000000000..89af0c2703f --- /dev/null +++ b/apps/web-tdesign/src/views/_core/authentication/login.vue @@ -0,0 +1,99 @@ + + + diff --git a/apps/web-tdesign/src/views/_core/authentication/qrcode-login.vue b/apps/web-tdesign/src/views/_core/authentication/qrcode-login.vue new file mode 100644 index 00000000000..23f5f2dad58 --- /dev/null +++ b/apps/web-tdesign/src/views/_core/authentication/qrcode-login.vue @@ -0,0 +1,10 @@ + + + diff --git a/apps/web-tdesign/src/views/_core/authentication/register.vue b/apps/web-tdesign/src/views/_core/authentication/register.vue new file mode 100644 index 00000000000..1a80ff51af0 --- /dev/null +++ b/apps/web-tdesign/src/views/_core/authentication/register.vue @@ -0,0 +1,96 @@ + + + diff --git a/apps/web-tdesign/src/views/_core/fallback/coming-soon.vue b/apps/web-tdesign/src/views/_core/fallback/coming-soon.vue new file mode 100644 index 00000000000..f394930f21a --- /dev/null +++ b/apps/web-tdesign/src/views/_core/fallback/coming-soon.vue @@ -0,0 +1,7 @@ + + + diff --git a/apps/web-tdesign/src/views/_core/fallback/forbidden.vue b/apps/web-tdesign/src/views/_core/fallback/forbidden.vue new file mode 100644 index 00000000000..8ea65fedb52 --- /dev/null +++ b/apps/web-tdesign/src/views/_core/fallback/forbidden.vue @@ -0,0 +1,9 @@ + + + diff --git a/apps/web-tdesign/src/views/_core/fallback/internal-error.vue b/apps/web-tdesign/src/views/_core/fallback/internal-error.vue new file mode 100644 index 00000000000..819a47d5ea8 --- /dev/null +++ b/apps/web-tdesign/src/views/_core/fallback/internal-error.vue @@ -0,0 +1,9 @@ + + + diff --git a/apps/web-tdesign/src/views/_core/fallback/not-found.vue b/apps/web-tdesign/src/views/_core/fallback/not-found.vue new file mode 100644 index 00000000000..4d178e9cbd4 --- /dev/null +++ b/apps/web-tdesign/src/views/_core/fallback/not-found.vue @@ -0,0 +1,9 @@ + + + diff --git a/apps/web-tdesign/src/views/_core/fallback/offline.vue b/apps/web-tdesign/src/views/_core/fallback/offline.vue new file mode 100644 index 00000000000..5de4a88de4e --- /dev/null +++ b/apps/web-tdesign/src/views/_core/fallback/offline.vue @@ -0,0 +1,9 @@ + + + diff --git a/apps/web-tdesign/src/views/dashboard/analytics/analytics-trends.vue b/apps/web-tdesign/src/views/dashboard/analytics/analytics-trends.vue new file mode 100644 index 00000000000..f1f0b232a62 --- /dev/null +++ b/apps/web-tdesign/src/views/dashboard/analytics/analytics-trends.vue @@ -0,0 +1,98 @@ + + + diff --git a/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-data.vue b/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-data.vue new file mode 100644 index 00000000000..190fb41f47d --- /dev/null +++ b/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-data.vue @@ -0,0 +1,82 @@ + + + diff --git a/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-sales.vue b/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-sales.vue new file mode 100644 index 00000000000..02f5091231b --- /dev/null +++ b/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-sales.vue @@ -0,0 +1,46 @@ + + + diff --git a/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-source.vue b/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-source.vue new file mode 100644 index 00000000000..0915c7af7dd --- /dev/null +++ b/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-source.vue @@ -0,0 +1,65 @@ + + + diff --git a/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits.vue b/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits.vue new file mode 100644 index 00000000000..7e0f10133d8 --- /dev/null +++ b/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits.vue @@ -0,0 +1,55 @@ + + + diff --git a/apps/web-tdesign/src/views/dashboard/analytics/index.vue b/apps/web-tdesign/src/views/dashboard/analytics/index.vue new file mode 100644 index 00000000000..5e3d6d285ae --- /dev/null +++ b/apps/web-tdesign/src/views/dashboard/analytics/index.vue @@ -0,0 +1,90 @@ + + + diff --git a/apps/web-tdesign/src/views/dashboard/workspace/index.vue b/apps/web-tdesign/src/views/dashboard/workspace/index.vue new file mode 100644 index 00000000000..b95d6138166 --- /dev/null +++ b/apps/web-tdesign/src/views/dashboard/workspace/index.vue @@ -0,0 +1,266 @@ + + + diff --git a/apps/web-tdesign/src/views/demos/antd/index.vue b/apps/web-tdesign/src/views/demos/antd/index.vue new file mode 100644 index 00000000000..b3b05cc153f --- /dev/null +++ b/apps/web-tdesign/src/views/demos/antd/index.vue @@ -0,0 +1,66 @@ + + + diff --git a/apps/web-tdesign/tailwind.config.mjs b/apps/web-tdesign/tailwind.config.mjs new file mode 100644 index 00000000000..f17f556fa9f --- /dev/null +++ b/apps/web-tdesign/tailwind.config.mjs @@ -0,0 +1 @@ +export { default } from '@vben/tailwind-config'; diff --git a/apps/web-tdesign/tsconfig.json b/apps/web-tdesign/tsconfig.json new file mode 100644 index 00000000000..02c287fe642 --- /dev/null +++ b/apps/web-tdesign/tsconfig.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web-app.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "#/*": ["./src/*"] + } + }, + "references": [{ "path": "./tsconfig.node.json" }], + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/apps/web-tdesign/tsconfig.node.json b/apps/web-tdesign/tsconfig.node.json new file mode 100644 index 00000000000..c2f0d86cc78 --- /dev/null +++ b/apps/web-tdesign/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/node.json", + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "noEmit": false + }, + "include": ["vite.config.mts"] +} diff --git a/apps/web-tdesign/vite.config.mts b/apps/web-tdesign/vite.config.mts new file mode 100644 index 00000000000..4517fc09657 --- /dev/null +++ b/apps/web-tdesign/vite.config.mts @@ -0,0 +1,20 @@ +import { defineConfig } from "@vben/vite-config"; + +export default defineConfig(async () => { + return { + application: {}, + vite: { + server: { + proxy: { + '/api': { + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ''), + // mock代理目标地址 + target: 'http://localhost:5320/api', + ws: true, + }, + }, + }, + }, + }; +}); From 33b7a605c037507eef6031b20371c9916b843b24 Mon Sep 17 00:00:00 2001 From: Jin Mao Date: Wed, 22 Oct 2025 18:14:42 +0800 Subject: [PATCH 02/12] =?UTF-8?q?chore:=20=E4=B8=80=E4=BA=9B=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E5=92=8C=E5=85=BC=E5=AE=B9=E6=80=A7=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-tdesign/.env | 4 +- .../src/adapter/component/index.ts | 21 +++++++++- apps/web-tdesign/src/adapter/form.ts | 14 +++---- apps/web-tdesign/src/app.vue | 41 ++++++++----------- 4 files changed, 43 insertions(+), 37 deletions(-) diff --git a/apps/web-tdesign/.env b/apps/web-tdesign/.env index 19735f36fda..dae0149d4ae 100644 --- a/apps/web-tdesign/.env +++ b/apps/web-tdesign/.env @@ -1,8 +1,8 @@ # 应用标题 -VITE_APP_TITLE=Vben Admin Antd +VITE_APP_TITLE=Vben Admin Tdesign # 应用命名空间,用于缓存、store等功能的前缀,确保隔离 -VITE_APP_NAMESPACE=vben-web-antd +VITE_APP_NAMESPACE=vben-web-tdesign # 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密 VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key diff --git a/apps/web-tdesign/src/adapter/component/index.ts b/apps/web-tdesign/src/adapter/component/index.ts index 1ba19baf9b8..0d1423f1f9c 100644 --- a/apps/web-tdesign/src/adapter/component/index.ts +++ b/apps/web-tdesign/src/adapter/component/index.ts @@ -9,6 +9,7 @@ import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import { $t } from '@vben/locales'; import { notification } from 'ant-design-vue'; + /** * 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用 * 可用于 vben-form、vben-modal、vben-drawer 等组件使用, @@ -178,11 +179,27 @@ async function initComponentAdapter() { // Mentions: withDefaultPlaceholder(Mentions, 'input'), // 自定义主要按钮 PrimaryButton: (props, { attrs, slots }) => { - return h(Button, { ...props, attrs, theme: 'primary' }, slots); + let ghost = false; + let variant = props.variant; + if (props.variant === 'ghost') { + ghost = true; + variant = 'base'; + } + return h( + Button, + { ...props, ghost, variant, attrs, theme: 'default' }, + slots, + ); }, Radio, RadioGroup, - RangePicker, + RangePicker: (props, { attrs, slots }) => { + return h( + RangePicker, + { ...props, modelValue: props.modelValue ?? [], attrs }, + slots, + ); + }, Rate, Select: withDefaultPlaceholder(Select, 'select'), Space, diff --git a/apps/web-tdesign/src/adapter/form.ts b/apps/web-tdesign/src/adapter/form.ts index 983a7f51698..a270a960d52 100644 --- a/apps/web-tdesign/src/adapter/form.ts +++ b/apps/web-tdesign/src/adapter/form.ts @@ -1,17 +1,13 @@ -import type { - VbenFormSchema as FormSchema, - VbenFormProps, -} from '@vben/common-ui'; +import type { VbenFormProps, VbenFormSchema as FormSchema } from "@vben/common-ui"; +import { setupVbenForm, useVbenForm as useForm, z } from "@vben/common-ui"; -import type { ComponentType } from './component'; - -import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; -import { $t } from '@vben/locales'; +import type { ComponentType } from "./component"; +import { $t } from "@vben/locales"; async function initSetupVbenForm() { setupVbenForm({ config: { - // ant design vue组件库默认都是 v-model:value + // tdesign组件库默认都是 v-model:value baseModelPropName: 'value', // 一些组件是 v-model:checked 或者 v-model:fileList diff --git a/apps/web-tdesign/src/app.vue b/apps/web-tdesign/src/app.vue index bbaccce13bc..1e6ef8260d1 100644 --- a/apps/web-tdesign/src/app.vue +++ b/apps/web-tdesign/src/app.vue @@ -1,39 +1,32 @@ From 04321b16c199117788645a405012fc73c36b38a6 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 14:35:50 +0800 Subject: [PATCH 03/12] fix: replace ant-design-vue with tdesign components in web-tdesign app (#6880) * Initial plan * fix: replace ant-design-vue with tdesign components Co-authored-by: likui628 <90845831+likui628@users.noreply.github.com> * fix: remove trailing comma in package.json Co-authored-by: likui628 <90845831+likui628@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: likui628 <90845831+likui628@users.noreply.github.com> --- apps/web-tdesign/package.json | 4 +-- .../src/adapter/component/index.ts | 8 +++--- apps/web-tdesign/src/adapter/tdesign.ts | 9 +++++++ apps/web-tdesign/src/adapter/vxe-table.ts | 4 +-- apps/web-tdesign/src/api/request.ts | 2 +- apps/web-tdesign/src/locales/index.ts | 27 ++----------------- apps/web-tdesign/src/router/access.ts | 4 +-- apps/web-tdesign/src/store/auth.ts | 8 +++--- .../src/views/demos/antd/index.vue | 19 ++++++------- 9 files changed, 36 insertions(+), 49 deletions(-) create mode 100644 apps/web-tdesign/src/adapter/tdesign.ts diff --git a/apps/web-tdesign/package.json b/apps/web-tdesign/package.json index 533d136196e..46918cd6146 100644 --- a/apps/web-tdesign/package.json +++ b/apps/web-tdesign/package.json @@ -6,7 +6,7 @@ "repository": { "type": "git", "url": "git+https://github.com/vbenjs/vue-vben-admin.git", - "directory": "apps/web-naive" + "directory": "apps/web-tdesign" }, "license": "MIT", "author": { @@ -46,5 +46,5 @@ "tdesign-vue-next": "^1.17.1", "vue": "catalog:", "vue-router": "catalog:" - }, + } } diff --git a/apps/web-tdesign/src/adapter/component/index.ts b/apps/web-tdesign/src/adapter/component/index.ts index 0d1423f1f9c..b4562d3a5c7 100644 --- a/apps/web-tdesign/src/adapter/component/index.ts +++ b/apps/web-tdesign/src/adapter/component/index.ts @@ -8,7 +8,7 @@ import { defineAsyncComponent, defineComponent, h, ref } from 'vue'; import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import { $t } from '@vben/locales'; -import { notification } from 'ant-design-vue'; +import { notification } from '#/adapter/tdesign'; /** * 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用 @@ -218,9 +218,9 @@ async function initComponentAdapter() { // 复制成功消息提示 copyPreferencesSuccess: (title, content) => { notification.success({ - description: content, - message: title, - placement: 'bottomRight', + title, + content, + placement: 'bottom-right', }); }, }); diff --git a/apps/web-tdesign/src/adapter/tdesign.ts b/apps/web-tdesign/src/adapter/tdesign.ts new file mode 100644 index 00000000000..aa169bb58c4 --- /dev/null +++ b/apps/web-tdesign/src/adapter/tdesign.ts @@ -0,0 +1,9 @@ +import { + DialogPlugin, + MessagePlugin, + NotificationPlugin, +} from 'tdesign-vue-next'; + +export const message = MessagePlugin; +export const notification = NotificationPlugin; +export const dialog = DialogPlugin; diff --git a/apps/web-tdesign/src/adapter/vxe-table.ts b/apps/web-tdesign/src/adapter/vxe-table.ts index 7de2859de8b..43bd68afd31 100644 --- a/apps/web-tdesign/src/adapter/vxe-table.ts +++ b/apps/web-tdesign/src/adapter/vxe-table.ts @@ -4,7 +4,7 @@ import { h } from 'vue'; import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; -import { Button, Image } from 'ant-design-vue'; +import { Button, Image } from 'tdesign-vue-next'; import { useVbenForm } from './form'; @@ -52,7 +52,7 @@ setupVbenVxeTable({ const { props } = renderOpts; return h( Button, - { size: 'small', type: 'link' }, + { size: 'small', theme: 'primary', variant: 'text' }, { default: () => props?.text }, ); }, diff --git a/apps/web-tdesign/src/api/request.ts b/apps/web-tdesign/src/api/request.ts index 288dddd09db..eb822148457 100644 --- a/apps/web-tdesign/src/api/request.ts +++ b/apps/web-tdesign/src/api/request.ts @@ -13,7 +13,7 @@ import { } from '@vben/request'; import { useAccessStore } from '@vben/stores'; -import { message } from 'ant-design-vue'; +import { message } from '#/adapter/tdesign'; import { useAuthStore } from '#/store'; diff --git a/apps/web-tdesign/src/locales/index.ts b/apps/web-tdesign/src/locales/index.ts index 7f32bd18ef3..ece11efd151 100644 --- a/apps/web-tdesign/src/locales/index.ts +++ b/apps/web-tdesign/src/locales/index.ts @@ -1,5 +1,3 @@ -import type { Locale } from 'ant-design-vue/es/locale'; - import type { App } from 'vue'; import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales'; @@ -13,12 +11,8 @@ import { } from '@vben/locales'; import { preferences } from '@vben/preferences'; -import antdEnLocale from 'ant-design-vue/es/locale/en_US'; -import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN'; import dayjs from 'dayjs'; -const antdLocale = ref(antdDefaultLocale); - const modules = import.meta.glob('./langs/**/*.json'); const localesMap = loadLocalesMapFromDir( @@ -43,7 +37,7 @@ async function loadMessages(lang: SupportedLanguagesType) { * @param lang */ async function loadThirdPartyMessage(lang: SupportedLanguagesType) { - await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]); + await loadDayjsLocale(lang); } /** @@ -73,23 +67,6 @@ async function loadDayjsLocale(lang: SupportedLanguagesType) { } } -/** - * 加载antd的语言包 - * @param lang - */ -async function loadAntdLocale(lang: SupportedLanguagesType) { - switch (lang) { - case 'en-US': { - antdLocale.value = antdEnLocale; - break; - } - case 'zh-CN': { - antdLocale.value = antdDefaultLocale; - break; - } - } -} - async function setupI18n(app: App, options: LocaleSetupOptions = {}) { await coreSetup(app, { defaultLocale: preferences.app.locale, @@ -99,4 +76,4 @@ async function setupI18n(app: App, options: LocaleSetupOptions = {}) { }); } -export { $t, antdLocale, setupI18n }; +export { $t, setupI18n }; diff --git a/apps/web-tdesign/src/router/access.ts b/apps/web-tdesign/src/router/access.ts index 3a48be2378c..617549fb39b 100644 --- a/apps/web-tdesign/src/router/access.ts +++ b/apps/web-tdesign/src/router/access.ts @@ -6,7 +6,7 @@ import type { import { generateAccessible } from '@vben/access'; import { preferences } from '@vben/preferences'; -import { message } from 'ant-design-vue'; +import { message } from '#/adapter/tdesign'; import { getAllMenusApi } from '#/api'; import { BasicLayout, IFrameView } from '#/layouts'; @@ -27,7 +27,7 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) { fetchMenuListAsync: async () => { message.loading({ content: `${$t('common.loadingMenu')}...`, - duration: 1.5, + duration: 1500, }); return await getAllMenusApi(); }, diff --git a/apps/web-tdesign/src/store/auth.ts b/apps/web-tdesign/src/store/auth.ts index 312a74da247..38c74e32320 100644 --- a/apps/web-tdesign/src/store/auth.ts +++ b/apps/web-tdesign/src/store/auth.ts @@ -7,7 +7,7 @@ import { LOGIN_PATH } from '@vben/constants'; import { preferences } from '@vben/preferences'; import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; -import { notification } from 'ant-design-vue'; +import { notification } from '#/adapter/tdesign'; import { defineStore } from 'pinia'; import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api'; @@ -61,9 +61,9 @@ export const useAuthStore = defineStore('auth', () => { if (userInfo?.realName) { notification.success({ - description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`, - duration: 3, - message: $t('authentication.loginSuccess'), + title: $t('authentication.loginSuccess'), + content: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`, + duration: 3000, }); } } diff --git a/apps/web-tdesign/src/views/demos/antd/index.vue b/apps/web-tdesign/src/views/demos/antd/index.vue index b3b05cc153f..f44f96e504f 100644 --- a/apps/web-tdesign/src/views/demos/antd/index.vue +++ b/apps/web-tdesign/src/views/demos/antd/index.vue @@ -1,7 +1,9 @@ @@ -35,20 +36,20 @@ function notify(type: NotificationType) {