From 2264eaae18a3b19684e973157612fde8b94f4b55 Mon Sep 17 00:00:00 2001 From: Jin Mao Date: Tue, 21 Oct 2025 10:47:39 +0800 Subject: [PATCH 01/76] =?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 000000000..19735f36f --- /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 000000000..ffafa8dd5 --- /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 000000000..1eae9e67d --- /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 000000000..5375847a6 --- /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 000000000..480eb84de --- /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 000000000..533d13619 --- /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 000000000..3d8070455 --- /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 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 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 000000000..983a7f516 --- /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 000000000..7de2859de --- /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 000000000..71d9f9943 --- /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 000000000..28a5aef47 --- /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 000000000..9ef60b11c --- /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 000000000..7e28ea848 --- /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 000000000..4b0e04137 --- /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 000000000..288dddd09 --- /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 000000000..bbaccce13 --- /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 000000000..edcccea4c --- /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 000000000..18d415bc7 --- /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 000000000..805b8a73d --- /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 000000000..a43207805 --- /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 000000000..7b451032e --- /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 000000000..7f32bd18e --- /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 000000000..071564349 --- /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 000000000..618a258c0 --- /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 000000000..93ee722f5 --- /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 000000000..4cb67081c --- /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 000000000..5d728a02a --- /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 000000000..b2e9ace43 --- /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 000000000..3a48be237 --- /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 000000000..a1ad6d88c --- /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 000000000..484023034 --- /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 000000000..949b0b65a --- /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 000000000..e6fb14402 --- /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 000000000..5254dc65d --- /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 000000000..55ade09c9 --- /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 000000000..98acf5821 --- /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 000000000..312a74da2 --- /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 000000000..269586ee8 --- /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 000000000..8248afe6c --- /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 000000000..0ee524335 --- /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 000000000..acfd1fd78 --- /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 000000000..fef0d4279 --- /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 000000000..89af0c270 --- /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 000000000..23f5f2dad --- /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 000000000..1a80ff51a --- /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 000000000..f394930f2 --- /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 000000000..8ea65fedb --- /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 000000000..819a47d5e --- /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 000000000..4d178e9cb --- /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 000000000..5de4a88de --- /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 000000000..f1f0b232a --- /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 000000000..190fb41f4 --- /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 000000000..02f509123 --- /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 000000000..0915c7af7 --- /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 000000000..7e0f10133 --- /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 000000000..5e3d6d285 --- /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 000000000..b95d61381 --- /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 000000000..b3b05cc15 --- /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 000000000..f17f556fa --- /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 000000000..02c287fe6 --- /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 000000000..c2f0d86cc --- /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 000000000..4517fc096 --- /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/76] =?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 19735f36f..dae0149d4 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 1ba19baf9..0d1423f1f 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 983a7f516..a270a960d 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 bbaccce13..1e6ef8260 100644 --- a/apps/web-tdesign/src/app.vue +++ b/apps/web-tdesign/src/app.vue @@ -1,39 +1,32 @@ From 0f3f6c0c2077c5b9057f6018eb79ca8cb76b8c97 Mon Sep 17 00:00:00 2001 From: hw Date: Thu, 30 Oct 2025 16:33:33 +0800 Subject: [PATCH 03/76] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E4=BB=B6=E5=92=8C=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-ele/.env.development | 2 +- playground/package.json | 3 ++- pnpm-lock.yaml | 6 ++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/web-ele/.env.development b/apps/web-ele/.env.development index 77c13d398..cf25c6dd5 100644 --- a/apps/web-ele/.env.development +++ b/apps/web-ele/.env.development @@ -6,7 +6,7 @@ VITE_BASE=/ # 请求路径 VITE_BASE_URL=http://127.0.0.1:48080 # 接口地址 -VITE_GLOB_API_URL=/admin-api +VITE_GLOB_API_URL=http://47.103.66.220:48080/admin-api # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 VITE_UPLOAD_TYPE=server # 是否打开 devtools,true 为打开,false 为关闭 diff --git a/playground/package.json b/playground/package.json index c8651563a..6c3045e62 100644 --- a/playground/package.json +++ b/playground/package.json @@ -54,6 +54,7 @@ "vue-router": "catalog:" }, "devDependencies": { - "@types/json-bigint": "catalog:" + "@types/json-bigint": "catalog:", + "vite": "catalog:" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d74811fa4..83cc6bd39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -854,6 +854,9 @@ importers: vue3-signature: specifier: 'catalog:' version: 0.2.4(vue@3.5.22(typescript@5.9.3)) + vuedraggable: + specifier: 'catalog:' + version: 4.1.0(vue@3.5.22(typescript@5.9.3)) apps/web-ele: dependencies: @@ -2113,6 +2116,9 @@ importers: '@types/json-bigint': specifier: 'catalog:' version: 1.0.4 + vite: + specifier: 'catalog:' + version: 7.1.11(@types/node@24.8.1)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) scripts/turbo-run: dependencies: From 20f838adde8e391bcca48468ced2b488e5a3862b Mon Sep 17 00:00:00 2001 From: hw Date: Thu, 30 Oct 2025 16:33:33 +0800 Subject: [PATCH 04/76] fix: eslint errors --- apps/web-ele/.env.development | 2 +- apps/web-ele/package.json | 1 + apps/web-ele/src/store/index.ts | 11 + apps/web-ele/src/store/tagsView.ts | 202 ++++++++ apps/web-ele/src/utils/index.ts | 28 ++ apps/web-ele/src/utils/routerHelper.ts | 22 + apps/web-ele/src/utils/tree.ts | 442 ++++++++++++++++++ .../mp/components/wx-account-select/index.ts | 1 + .../mp/components/wx-account-select/main.vue | 83 ++++ .../views/mp/components/wx-location/index.ts | 1 + .../views/mp/components/wx-location/main.vue | 61 +++ .../mp/components/wx-material-select/index.ts | 3 + .../mp/components/wx-material-select/main.vue | 295 ++++++++++++ .../mp/components/wx-material-select/types.ts | 11 + .../src/views/mp/components/wx-msg/card.scss | 116 +++++ .../views/mp/components/wx-msg/comment.scss | 109 +++++ .../mp/components/wx-msg/components/Msg.vue | 85 ++++ .../components/wx-msg/components/MsgEvent.vue | 54 +++ .../components/wx-msg/components/MsgList.vue | 77 +++ .../src/views/mp/components/wx-msg/index.ts | 3 + .../src/views/mp/components/wx-msg/main.vue | 210 +++++++++ .../src/views/mp/components/wx-msg/types.ts | 17 + .../src/views/mp/components/wx-music/index.ts | 1 + .../src/views/mp/components/wx-music/main.vue | 70 +++ .../src/views/mp/components/wx-news/index.ts | 1 + .../src/views/mp/components/wx-news/main.vue | 123 +++++ .../wx-reply/components/TabImage.vue | 186 ++++++++ .../wx-reply/components/TabMusic.vue | 137 ++++++ .../wx-reply/components/TabNews.vue | 92 ++++ .../wx-reply/components/TabText.vue | 29 ++ .../wx-reply/components/TabVideo.vue | 148 ++++++ .../wx-reply/components/TabVoice.vue | 174 +++++++ .../components/wx-reply/components/types.ts | 58 +++ .../src/views/mp/components/wx-reply/index.ts | 8 + .../src/views/mp/components/wx-reply/main.vue | 215 +++++++++ .../mp/components/wx-video-play/index.ts | 1 + .../mp/components/wx-video-play/main.vue | 76 +++ .../mp/components/wx-voice-play/index.ts | 1 + .../mp/components/wx-voice-play/main.vue | 108 +++++ apps/web-ele/src/views/mp/hooks/useUpload.ts | 67 +++ .../views/mp/menu/assets/iphone_backImg.png | Bin 0 -> 34272 bytes .../src/views/mp/menu/assets/menu_foot.png | Bin 0 -> 1348 bytes .../src/views/mp/menu/assets/menu_head.png | Bin 0 -> 12673 bytes .../views/mp/menu/components/menuOptions.ts | 42 ++ .../src/views/mp/menu/components/types.ts | 73 +++ apps/web-ele/src/views/mp/menu/data.ts | 9 + apps/web-ele/src/views/mp/menu/index.vue | 411 ++++++++++++++++ .../src/views/mp/menu/modules/menu-editor.vue | 278 +++++++++++ .../views/mp/menu/modules/menu-previewer.vue | 250 ++++++++++ playground/package.json | 3 +- pnpm-lock.yaml | 59 ++- 51 files changed, 4433 insertions(+), 21 deletions(-) create mode 100644 apps/web-ele/src/store/tagsView.ts create mode 100644 apps/web-ele/src/utils/tree.ts create mode 100644 apps/web-ele/src/views/mp/components/wx-account-select/index.ts create mode 100644 apps/web-ele/src/views/mp/components/wx-account-select/main.vue create mode 100644 apps/web-ele/src/views/mp/components/wx-location/index.ts create mode 100644 apps/web-ele/src/views/mp/components/wx-location/main.vue create mode 100644 apps/web-ele/src/views/mp/components/wx-material-select/index.ts create mode 100644 apps/web-ele/src/views/mp/components/wx-material-select/main.vue create mode 100644 apps/web-ele/src/views/mp/components/wx-material-select/types.ts create mode 100644 apps/web-ele/src/views/mp/components/wx-msg/card.scss create mode 100644 apps/web-ele/src/views/mp/components/wx-msg/comment.scss create mode 100644 apps/web-ele/src/views/mp/components/wx-msg/components/Msg.vue create mode 100644 apps/web-ele/src/views/mp/components/wx-msg/components/MsgEvent.vue create mode 100644 apps/web-ele/src/views/mp/components/wx-msg/components/MsgList.vue create mode 100644 apps/web-ele/src/views/mp/components/wx-msg/index.ts create mode 100644 apps/web-ele/src/views/mp/components/wx-msg/main.vue create mode 100644 apps/web-ele/src/views/mp/components/wx-msg/types.ts create mode 100644 apps/web-ele/src/views/mp/components/wx-music/index.ts create mode 100644 apps/web-ele/src/views/mp/components/wx-music/main.vue create mode 100644 apps/web-ele/src/views/mp/components/wx-news/index.ts create mode 100644 apps/web-ele/src/views/mp/components/wx-news/main.vue create mode 100644 apps/web-ele/src/views/mp/components/wx-reply/components/TabImage.vue create mode 100644 apps/web-ele/src/views/mp/components/wx-reply/components/TabMusic.vue create mode 100644 apps/web-ele/src/views/mp/components/wx-reply/components/TabNews.vue create mode 100644 apps/web-ele/src/views/mp/components/wx-reply/components/TabText.vue create mode 100644 apps/web-ele/src/views/mp/components/wx-reply/components/TabVideo.vue create mode 100644 apps/web-ele/src/views/mp/components/wx-reply/components/TabVoice.vue create mode 100644 apps/web-ele/src/views/mp/components/wx-reply/components/types.ts create mode 100644 apps/web-ele/src/views/mp/components/wx-reply/index.ts create mode 100644 apps/web-ele/src/views/mp/components/wx-reply/main.vue create mode 100644 apps/web-ele/src/views/mp/components/wx-video-play/index.ts create mode 100644 apps/web-ele/src/views/mp/components/wx-video-play/main.vue create mode 100644 apps/web-ele/src/views/mp/components/wx-voice-play/index.ts create mode 100644 apps/web-ele/src/views/mp/components/wx-voice-play/main.vue create mode 100644 apps/web-ele/src/views/mp/hooks/useUpload.ts create mode 100644 apps/web-ele/src/views/mp/menu/assets/iphone_backImg.png create mode 100644 apps/web-ele/src/views/mp/menu/assets/menu_foot.png create mode 100644 apps/web-ele/src/views/mp/menu/assets/menu_head.png create mode 100644 apps/web-ele/src/views/mp/menu/components/menuOptions.ts create mode 100644 apps/web-ele/src/views/mp/menu/components/types.ts create mode 100644 apps/web-ele/src/views/mp/menu/data.ts create mode 100644 apps/web-ele/src/views/mp/menu/index.vue create mode 100644 apps/web-ele/src/views/mp/menu/modules/menu-editor.vue create mode 100644 apps/web-ele/src/views/mp/menu/modules/menu-previewer.vue diff --git a/apps/web-ele/.env.development b/apps/web-ele/.env.development index 77c13d398..cf25c6dd5 100644 --- a/apps/web-ele/.env.development +++ b/apps/web-ele/.env.development @@ -6,7 +6,7 @@ VITE_BASE=/ # 请求路径 VITE_BASE_URL=http://127.0.0.1:48080 # 接口地址 -VITE_GLOB_API_URL=/admin-api +VITE_GLOB_API_URL=http://47.103.66.220:48080/admin-api # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 VITE_UPLOAD_TYPE=server # 是否打开 devtools,true 为打开,false 为关闭 diff --git a/apps/web-ele/package.json b/apps/web-ele/package.json index aa96e8564..9ded8eee4 100644 --- a/apps/web-ele/package.json +++ b/apps/web-ele/package.json @@ -45,6 +45,7 @@ "@vben/utils": "workspace:*", "@vueuse/core": "catalog:", "@vueuse/integrations": "catalog:", + "benz-amr-recorder": "^1.1.5", "cropperjs": "catalog:", "dayjs": "catalog:", "element-plus": "catalog:", diff --git a/apps/web-ele/src/store/index.ts b/apps/web-ele/src/store/index.ts index 269586ee8..af0c4ab6f 100644 --- a/apps/web-ele/src/store/index.ts +++ b/apps/web-ele/src/store/index.ts @@ -1 +1,12 @@ +import type { App } from 'vue'; + +import { createPinia } from 'pinia'; + export * from './auth'; +const store = createPinia(); + +export const setupStore = (app: App) => { + app.use(store); +}; + +export { store }; diff --git a/apps/web-ele/src/store/tagsView.ts b/apps/web-ele/src/store/tagsView.ts new file mode 100644 index 000000000..396f581e8 --- /dev/null +++ b/apps/web-ele/src/store/tagsView.ts @@ -0,0 +1,202 @@ +import type { RouteLocationNormalizedLoaded } from 'vue-router'; + +import { useUserStore } from '@vben/stores'; + +import { defineStore } from 'pinia'; + +import { router } from '#/router'; +import { findIndex } from '#/utils'; +import { getRawRoute } from '#/utils/routerHelper'; + +import { store } from './index'; + +const userStore = useUserStore(); + +export interface TagsViewState { + visitedViews: RouteLocationNormalizedLoaded[]; + cachedViews: Set; + selectedTag?: RouteLocationNormalizedLoaded; +} + +export const useTagsViewStore = defineStore('tagsView', { + state: (): TagsViewState => ({ + visitedViews: [], + cachedViews: new Set(), + selectedTag: undefined, + }), + getters: { + getVisitedViews(): RouteLocationNormalizedLoaded[] { + return this.visitedViews; + }, + getCachedViews(): string[] { + return [...this.cachedViews]; + }, + getSelectedTag(): RouteLocationNormalizedLoaded | undefined { + return this.selectedTag; + }, + }, + actions: { + // 新增缓存和tag + addView(view: RouteLocationNormalizedLoaded): void { + this.addVisitedView(view); + this.addCachedView(); + }, + // 新增tag + addVisitedView(view: RouteLocationNormalizedLoaded) { + if (this.visitedViews.some((v) => v.fullPath === view.fullPath)) return; + if (view.meta?.noTagsView) return; + const visitedView = Object.assign({}, view, { + title: view.meta?.title || 'no-name', + }); + + if (visitedView.meta) { + const titleSuffixList: string[] = []; + this.visitedViews.forEach((v) => { + if ( + v.path === visitedView.path && + v.meta?.title === visitedView.meta?.title + ) { + titleSuffixList.push((v.meta?.titleSuffix as string) || '1'); + } + }); + if (titleSuffixList.length > 0) { + let titleSuffix = 1; + while (titleSuffixList.includes(`${titleSuffix}`)) { + titleSuffix += 1; + } + visitedView.meta.titleSuffix = + titleSuffix === 1 ? undefined : `${titleSuffix}`; + } + } + + this.visitedViews.push(visitedView); + }, + // 新增缓存 + addCachedView() { + const cacheMap: Set = new Set(); + for (const v of this.visitedViews) { + const item = getRawRoute(v); + const needCache = !item.meta?.noCache; + if (!needCache) { + continue; + } + const name = item.name as string; + cacheMap.add(name); + } + if ( + [...this.cachedViews].sort().toString() === + [...cacheMap].sort().toString() + ) + return; + this.cachedViews = cacheMap; + }, + // 删除某个 + delView(view: RouteLocationNormalizedLoaded) { + this.delVisitedView(view); + this.delCachedView(); + }, + // 删除tag + delVisitedView(view: RouteLocationNormalizedLoaded) { + for (const [i, v] of this.visitedViews.entries()) { + if (v.fullPath === view.fullPath) { + this.visitedViews.splice(i, 1); + break; + } + } + }, + // 删除缓存 + delCachedView() { + const route = router.currentRoute.value; + const index = findIndex( + this.getCachedViews, + (v) => v === route.name, + ); + for (const v of this.visitedViews) { + if (v.name === route.name) { + return; + } + } + if (index > -1) { + this.cachedViews.delete( + this.getCachedViews[index] as unknown as string, + ); + } + }, + // 删除所有缓存和tag + delAllViews() { + this.delAllVisitedViews(); + this.delCachedView(); + }, + // 删除所有tag + delAllVisitedViews() { + // const userStore = useUserStoreWithOut(); + + // const affixTags = this.visitedViews.filter((tag) => tag.meta.affix) + this.visitedViews = userStore.userInfo + ? this.visitedViews.filter((tag) => tag?.meta?.affix) + : []; + }, + // 删除其他 + delOthersViews(view: RouteLocationNormalizedLoaded) { + this.delOthersVisitedViews(view); + this.addCachedView(); + }, + // 删除其他tag + delOthersVisitedViews(view: RouteLocationNormalizedLoaded) { + this.visitedViews = this.visitedViews.filter((v) => { + return v?.meta?.affix || v.fullPath === view.fullPath; + }); + }, + // 删除左侧 + delLeftViews(view: RouteLocationNormalizedLoaded) { + const index = findIndex( + this.visitedViews, + (v) => v.fullPath === view.fullPath, + ); + if (index > -1) { + this.visitedViews = this.visitedViews.filter((v, i) => { + return v?.meta?.affix || v.fullPath === view.fullPath || i > index; + }); + this.addCachedView(); + } + }, + // 删除右侧 + delRightViews(view: RouteLocationNormalizedLoaded) { + const index = findIndex( + this.visitedViews, + (v) => v.fullPath === view.fullPath, + ); + if (index > -1) { + this.visitedViews = this.visitedViews.filter((v, i) => { + return v?.meta?.affix || v.fullPath === view.fullPath || i < index; + }); + this.addCachedView(); + } + }, + updateVisitedView(view: RouteLocationNormalizedLoaded) { + for (let v of this.visitedViews) { + if (v.fullPath === view.fullPath) { + v = Object.assign(v, view); + break; + } + } + }, + // 设置当前选中的 tag + setSelectedTag(tag: RouteLocationNormalizedLoaded) { + this.selectedTag = tag; + }, + setTitle(title: string, path?: string) { + for (const v of this.visitedViews) { + if (v.path === (path ?? this.selectedTag?.path)) { + v.meta.title = title; + break; + } + } + }, + }, + persist: false, +}); + +export const useTagsViewStoreWithOut = () => { + return useTagsViewStore(store); +}; diff --git a/apps/web-ele/src/utils/index.ts b/apps/web-ele/src/utils/index.ts index 3a066fd4a..e64084b6c 100644 --- a/apps/web-ele/src/utils/index.ts +++ b/apps/web-ele/src/utils/index.ts @@ -1,2 +1,30 @@ +import type { Recordable } from '@vben/types'; + export * from './rangePickerProps'; export * from './routerHelper'; + +/** + * 查找数组对象的某个下标 + * @param {Array} ary 查找的数组 + * @param {Function} fn 判断的方法 + */ +type Fn = (item: T, index: number, array: Array) => boolean; + +export const findIndex = >( + ary: Array, + fn: Fn, +): number => { + if (ary.findIndex) { + return ary.findIndex((item, index, array) => fn(item, index, array)); + } + let index = -1; + ary.some((item: T, i: number, ary: Array) => { + const ret: boolean = fn(item, i, ary); + if (ret) { + index = i; + return true; + } + return false; + }); + return index; +}; diff --git a/apps/web-ele/src/utils/routerHelper.ts b/apps/web-ele/src/utils/routerHelper.ts index 477a640ce..31164d7ba 100644 --- a/apps/web-ele/src/utils/routerHelper.ts +++ b/apps/web-ele/src/utils/routerHelper.ts @@ -1,3 +1,8 @@ +import type { + RouteLocationNormalized, + RouteRecordNormalized, +} from 'vue-router'; + import { defineAsyncComponent } from 'vue'; const modules = import.meta.glob('../views/**/*.{vue,tsx}'); @@ -13,3 +18,20 @@ export function registerComponent(componentPath: string) { } } } + +export const getRawRoute = ( + route: RouteLocationNormalized, +): RouteLocationNormalized => { + if (!route) return route; + const { matched, ...opt } = route; + return { + ...opt, + matched: (matched + ? matched.map((item) => ({ + meta: item.meta, + name: item.name, + path: item.path, + })) + : undefined) as RouteRecordNormalized[], + }; +}; diff --git a/apps/web-ele/src/utils/tree.ts b/apps/web-ele/src/utils/tree.ts new file mode 100644 index 000000000..172ed2ba1 --- /dev/null +++ b/apps/web-ele/src/utils/tree.ts @@ -0,0 +1,442 @@ +interface TreeHelperConfig { + id: string; + children: string; + pid: string; +} + +const DEFAULT_CONFIG: TreeHelperConfig = { + id: 'id', + children: 'children', + pid: 'pid', +}; +export const defaultProps = { + children: 'children', + label: 'name', + value: 'id', + isLeaf: 'leaf', + emitPath: false, // 用于 cascader 组件:在选中节点改变时,是否返回由该节点所在的各级菜单的值所组成的数组,若设置 false,则只返回该节点的值 +}; + +const getConfig = (config: Partial) => + Object.assign({}, DEFAULT_CONFIG, config); + +// tree from list +export const listToTree = ( + list: any[], + config: Partial = {}, +): T[] => { + const conf = getConfig(config) as TreeHelperConfig; + const nodeMap = new Map(); + const result: T[] = []; + const { id, children, pid } = conf; + + for (const node of list) { + node[children] = node[children] || []; + nodeMap.set(node[id], node); + } + for (const node of list) { + const parent = nodeMap.get(node[pid]); + (parent ? parent.children : result).push(node); + } + return result; +}; + +export const treeToList = ( + tree: any, + config: Partial = {}, +): T => { + config = getConfig(config); + const { children } = config; + const result: any = [...tree]; + for (let i = 0; i < result.length; i++) { + const childNodes = result[i][children]; + if (!childNodes) continue; + result.splice(i + 1, 0, ...childNodes); + } + return result; +}; + +export const findNode = ( + tree: any, + func: Fn, + config: Partial = {}, +): null | T => { + config = getConfig(config); + const { children } = config; + const list = [...tree]; + for (const node of list) { + if (func(node)) return node; + const childNodes = node[children]; + if (childNodes) { + list.push(...childNodes); + } + } + return null; +}; + +export const findNodeAll = ( + tree: any, + func: Fn, + config: Partial = {}, +): T[] => { + config = getConfig(config); + const { children } = config; + const list = [...tree]; + const result: T[] = []; + for (const node of list) { + func(node) && result.push(node); + const childNodes = node[children]; + if (childNodes) { + list.push(...childNodes); + } + } + return result; +}; + +export const findPath = ( + tree: any, + func: Fn, + config: Partial = {}, +): null | T | T[] => { + config = getConfig(config); + const path: T[] = []; + const list = [...tree]; + const visitedSet = new Set(); + const { children } = config; + while (list.length > 0) { + const node = list[0]; + if (visitedSet.has(node)) { + path.pop(); + list.shift(); + } else { + visitedSet.add(node); + const childNodes = node[children]; + if (childNodes) { + list.unshift(...childNodes); + } + path.push(node); + if (func(node)) { + return path; + } + } + } + return null; +}; + +export const findPathAll = ( + tree: any, + func: Fn, + config: Partial = {}, +) => { + config = getConfig(config); + const path: any[] = []; + const list = [...tree]; + const result: any[] = []; + const visitedSet = new Set(); + const { children } = config; + while (list.length > 0) { + const node = list[0]; + if (visitedSet.has(node)) { + path.pop(); + list.shift(); + } else { + visitedSet.add(node); + const childNodes = node[children]; + if (childNodes) { + list.unshift(...childNodes); + } + path.push(node); + func(node) && result.push([...path]); + } + } + return result; +}; + +export const filter = ( + tree: T[], + func: (n: T) => boolean, + config: Partial = {}, +): T[] => { + config = getConfig(config); + const children = config.children as string; + + function listFilter(list: T[]) { + return list + .map((node: any) => ({ ...node })) + .filter((node) => { + node[children] = node[children] && listFilter(node[children]); + return func(node) || node[children]?.length > 0; + }); + } + + return listFilter(tree); +}; + +export const forEach = ( + tree: T[], + func: (n: T) => any, + config: Partial = {}, +): void => { + config = getConfig(config); + const list: any[] = [...tree]; + const { children } = config; + for (let i = 0; i < list.length; i++) { + // func 返回true就终止遍历,避免大量节点场景下无意义循环,引起浏览器卡顿 + if (func(list[i])) { + return; + } + children && + list[i][children] && + list.splice(i + 1, 0, ...list[i][children]); + } +}; + +/** + * @description: Extract tree specified structure + */ +export const treeMap = ( + treeData: T[], + opt: { children?: string; conversion: Fn }, +): T[] => { + return treeData.map((item) => treeMapEach(item, opt)); +}; + +/** + * @description: Extract tree specified structure + */ +export const treeMapEach = ( + data: any, + { children = 'children', conversion }: { children?: string; conversion: Fn }, +) => { + const haveChildren = + Array.isArray(data[children]) && data[children].length > 0; + const conversionData = conversion(data) || {}; + return haveChildren + ? { + ...conversionData, + [children]: data[children].map((i: number) => + treeMapEach(i, { + children, + conversion, + }), + ), + } + : { + ...conversionData, + }; +}; + +/** + * 递归遍历树结构 + * @param treeDatas 树 + * @param callBack 回调 + * @param parentNode 父节点 + */ +export const eachTree = (treeDatas: any[], callBack: Fn, parentNode = {}) => { + treeDatas.forEach((element) => { + const newNode = callBack(element, parentNode) || element; + if (element.children) { + eachTree(element.children, callBack, newNode); + } + }); +}; + +/** + * 构造树型结构数据 + * @param {*} data 数据源 + * @param {*} id id字段 默认 'id' + * @param {*} parentId 父节点字段 默认 'parentId' + * @param {*} children 孩子节点字段 默认 'children' + */ +export const handleTree = ( + data: any[], + id?: string, + parentId?: string, + children?: string, +) => { + if (!Array.isArray(data)) { + console.warn('data must be an array'); + return []; + } + const config = { + id: id || 'id', + parentId: parentId || 'parentId', + childrenList: children || 'children', + }; + + const childrenListMap = {}; + const nodeIds = {}; + const tree: any[] = []; + + for (const d of data) { + const parentId = d[config.parentId]; + if ( + childrenListMap[parentId] === null || + childrenListMap[parentId] === undefined + ) { + childrenListMap[parentId] = []; + } + nodeIds[d[config.id]] = d; + childrenListMap[parentId].push(d); + } + + for (const d of data) { + const parentId = d[config.parentId]; + if (nodeIds[parentId] === null || nodeIds[parentId] === undefined) { + tree.push(d); + } + } + + for (const t of tree) { + adaptToChildrenList(t); + } + + function adaptToChildrenList(o) { + if (childrenListMap[o[config.id]] !== null) { + o[config.childrenList] = childrenListMap[o[config.id]]; + } + if (o[config.childrenList]) { + for (const c of o[config.childrenList]) { + adaptToChildrenList(c); + } + } + } + + return tree; +}; + +/** + * 构造树型结构数据 + * @param {*} data 数据源 + * @param {*} id id字段 默认 'id' + * @param {*} parentId 父节点字段 默认 'parentId' + * @param {*} children 孩子节点字段 默认 'children' + * @param {*} rootId 根Id 默认 0 + */ +// @ts-ignore: 遗留函数,保持原有逻辑不变 +export const handleTree2 = (data, id, parentId, children, rootId) => { + id = id || 'id'; + parentId = parentId || 'parentId'; + // children = children || 'children' + rootId = + rootId || + Math.min( + ...data.map((item) => { + return item[parentId]; + }), + ) || + 0; + // 对源数据深度克隆 + const cloneData = structuredClone(data); + // 循环所有项 + const treeData = cloneData.filter((father) => { + const branchArr = cloneData.filter((child) => { + // 返回每一项的子级数组 + return father[id] === child[parentId]; + }); + branchArr.length > 0 ? (father.children = branchArr) : ''; + // 返回第一层 + return father[parentId] === rootId; + }); + return treeData === '' ? data : treeData; +}; + +/** + * 校验选中的节点,是否为指定 level + * + * @param tree 要操作的树结构数据 + * @param nodeId 需要判断在什么层级的数据 + * @param level 检查的级别, 默认检查到二级 + * @return true 是;false 否 + */ +export const checkSelectedNode = ( + tree: any[], + nodeId: any, + level = 2, +): boolean => { + if (tree === undefined || !Array.isArray(tree) || tree.length === 0) { + console.warn('tree must be an array'); + return false; + } + + // 校验是否是一级节点 + if (tree.some((item) => item.id === nodeId)) { + return false; + } + + // 递归计数 + let count = 1; + + // 深层次校验 + function performAThoroughValidation(arr: any[]): boolean { + count += 1; + for (const item of arr) { + if (item.id === nodeId) { + return true; + } else if ( + item.children !== undefined && + item.children.length > 0 && + performAThoroughValidation(item.children) + ) { + return true; + } + } + return false; + } + + for (const item of tree) { + count = 1; + if ( + performAThoroughValidation(item.children) && // 找到后对比是否是期望的层级 + count >= level + ) { + return true; + } + } + + return false; +}; + +/** + * 获取节点的完整结构 + * @param tree 树数据 + * @param nodeId 节点 id + */ +export const treeToString = (tree: any[], nodeId) => { + if (tree === undefined || !Array.isArray(tree) || tree.length === 0) { + console.warn('tree must be an array'); + return ''; + } + // 校验是否是一级节点 + const node = tree.find((item) => item.id === nodeId); + if (node !== undefined) { + return node.name; + } + let str = ''; + + function performAThoroughValidation(arr) { + if (arr === undefined || !Array.isArray(arr) || arr.length === 0) { + return false; + } + for (const item of arr) { + if (item.id === nodeId) { + str += ` / ${item.name}`; + return true; + } else if (item.children !== undefined && item.children.length > 0) { + str += ` / ${item.name}`; + if (performAThoroughValidation(item.children)) { + return true; + } + } + } + return false; + } + + for (const item of tree) { + str = `${item.name}`; + if (performAThoroughValidation(item.children)) { + break; + } + } + return str; +}; diff --git a/apps/web-ele/src/views/mp/components/wx-account-select/index.ts b/apps/web-ele/src/views/mp/components/wx-account-select/index.ts new file mode 100644 index 000000000..374d993fe --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-account-select/index.ts @@ -0,0 +1 @@ +export { default } from './main.vue'; diff --git a/apps/web-ele/src/views/mp/components/wx-account-select/main.vue b/apps/web-ele/src/views/mp/components/wx-account-select/main.vue new file mode 100644 index 000000000..e2c1b552e --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-account-select/main.vue @@ -0,0 +1,83 @@ + + + + diff --git a/apps/web-ele/src/views/mp/components/wx-location/index.ts b/apps/web-ele/src/views/mp/components/wx-location/index.ts new file mode 100644 index 000000000..374d993fe --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-location/index.ts @@ -0,0 +1 @@ +export { default } from './main.vue'; diff --git a/apps/web-ele/src/views/mp/components/wx-location/main.vue b/apps/web-ele/src/views/mp/components/wx-location/main.vue new file mode 100644 index 000000000..784b45553 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-location/main.vue @@ -0,0 +1,61 @@ + + + + diff --git a/apps/web-ele/src/views/mp/components/wx-material-select/index.ts b/apps/web-ele/src/views/mp/components/wx-material-select/index.ts new file mode 100644 index 000000000..f4fcea0ba --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-material-select/index.ts @@ -0,0 +1,3 @@ +export { default } from './main.vue'; + +export { MaterialType, NewsType } from './types'; diff --git a/apps/web-ele/src/views/mp/components/wx-material-select/main.vue b/apps/web-ele/src/views/mp/components/wx-material-select/main.vue new file mode 100644 index 000000000..467636110 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-material-select/main.vue @@ -0,0 +1,295 @@ + + + + + diff --git a/apps/web-ele/src/views/mp/components/wx-material-select/types.ts b/apps/web-ele/src/views/mp/components/wx-material-select/types.ts new file mode 100644 index 000000000..3d44c5db7 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-material-select/types.ts @@ -0,0 +1,11 @@ +export enum NewsType { + Draft = '2', + Published = '1', +} + +export enum MaterialType { + Image = 'image', + News = 'news', + Video = 'video', + Voice = 'voice', +} diff --git a/apps/web-ele/src/views/mp/components/wx-msg/card.scss b/apps/web-ele/src/views/mp/components/wx-msg/card.scss new file mode 100644 index 000000000..58fde79fd --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-msg/card.scss @@ -0,0 +1,116 @@ +.avue-card { + &__item { + box-sizing: border-box; + height: 200px; + margin-bottom: 16px; + font-size: 14px; + font-feature-settings: 'tnum'; + font-variant: tabular-nums; + line-height: 1.5; + color: rgb(0 0 0 / 65%); + cursor: pointer; + list-style: none; + background-color: #fff; + border: 1px solid #e8e8e8; + + &:hover { + border-color: rgb(0 0 0 / 9%); + box-shadow: 0 2px 8px rgb(0 0 0 / 9%); + } + + &--add { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + font-size: 16px; + color: rgb(0 0 0 / 45%); + background-color: #fff; + border: 1px dashed #000; + border-color: #d9d9d9; + border-radius: 2px; + + i { + margin-right: 10px; + } + + &:hover { + color: #40a9ff; + background-color: #fff; + border-color: #40a9ff; + } + } + } + + &__body { + display: flex; + padding: 24px; + } + + &__detail { + flex: 1; + } + + &__avatar { + width: 48px; + height: 48px; + margin-right: 12px; + overflow: hidden; + border-radius: 48px; + + img { + width: 100%; + height: 100%; + } + } + + &__title { + margin-bottom: 12px; + font-size: 16px; + color: rgb(0 0 0 / 85%); + + &:hover { + color: #1890ff; + } + } + + &__info { + display: -webkit-box; + height: 64px; + overflow: hidden; + -webkit-line-clamp: 3; + color: rgb(0 0 0 / 45%); + -webkit-box-orient: vertical; + } + + &__menu { + display: flex; + justify-content: space-around; + height: 50px; + line-height: 50px; + color: rgb(0 0 0 / 45%); + text-align: center; + background: #f7f9fa; + + &:hover { + color: #1890ff; + } + } +} + +/** joolun 额外加的 */ +.avue-comment__main { + flex: unset !important; + margin: 0 8px !important; + border-radius: 5px !important; +} + +.avue-comment__header { + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +.avue-comment__body { + border-bottom-right-radius: 5px; + border-bottom-left-radius: 5px; +} diff --git a/apps/web-ele/src/views/mp/components/wx-msg/comment.scss b/apps/web-ele/src/views/mp/components/wx-msg/comment.scss new file mode 100644 index 000000000..219e2e5d4 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-msg/comment.scss @@ -0,0 +1,109 @@ +/* 来自 https://github.com/nmxiaowei/avue/blob/master/styles/src/element-ui/comment.scss */ +.avue-comment { + display: flex; + align-items: flex-start; + margin-bottom: 30px; + + &--reverse { + flex-direction: row-reverse; + + .avue-comment__main { + &::before, + &::after { + right: -8px; + left: auto; + border-width: 8px 0 8px 8px; + } + + &::before { + border-left-color: #dedede; + } + + &::after { + margin-right: 1px; + margin-left: auto; + border-left-color: #f8f8f8; + } + } + } + + &__avatar { + box-sizing: border-box; + width: 48px; + height: 48px; + vertical-align: middle; + border: 1px solid transparent; + border-radius: 50%; + } + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 5px 15px; + background: #f8f8f8; + border-bottom: 1px solid #eee; + } + + &__author { + font-size: 14px; + font-weight: 700; + color: #999; + } + + &__main { + position: relative; + flex: 1; + margin: 0 20px; + border: 1px solid #dedede; + border-radius: 2px; + + &::before, + &::after { + position: absolute; + top: 10px; + right: 100%; + left: -8px; + display: block; + width: 0; + height: 0; + pointer-events: none; + content: ' '; + border-color: transparent; + border-style: solid solid outset; + border-width: 8px 8px 8px 0; + } + + &::before { + z-index: 1; + border-right-color: #dedede; + } + + &::after { + z-index: 2; + margin-left: 1px; + border-right-color: #f8f8f8; + } + } + + &__body { + padding: 15px; + overflow: hidden; + font-family: + 'Segoe UI', 'Lucida Grande', Helvetica, Arial, 'Microsoft YaHei', + FreeSans, Arimo, 'Droid Sans', 'wenquanyi micro hei', 'Hiragino Sans GB', + 'Hiragino Sans GB W3', FontAwesome, sans-serif; + font-size: 14px; + color: #333; + background: #fff; + } + + blockquote { + padding: 1px 0 1px 15px; + margin: 0; + font-family: + Georgia, 'Times New Roman', Times, Kai, 'Kaiti SC', KaiTi, BiauKai, + FontAwesome, serif; + border-left: 4px solid #ddd; + } +} diff --git a/apps/web-ele/src/views/mp/components/wx-msg/components/Msg.vue b/apps/web-ele/src/views/mp/components/wx-msg/components/Msg.vue new file mode 100644 index 000000000..8daa0fd51 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-msg/components/Msg.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/apps/web-ele/src/views/mp/components/wx-msg/components/MsgEvent.vue b/apps/web-ele/src/views/mp/components/wx-msg/components/MsgEvent.vue new file mode 100644 index 000000000..70c193082 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-msg/components/MsgEvent.vue @@ -0,0 +1,54 @@ + + + diff --git a/apps/web-ele/src/views/mp/components/wx-msg/components/MsgList.vue b/apps/web-ele/src/views/mp/components/wx-msg/components/MsgList.vue new file mode 100644 index 000000000..36b574d83 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-msg/components/MsgList.vue @@ -0,0 +1,77 @@ + + + + diff --git a/apps/web-ele/src/views/mp/components/wx-msg/index.ts b/apps/web-ele/src/views/mp/components/wx-msg/index.ts new file mode 100644 index 000000000..048afc95f --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-msg/index.ts @@ -0,0 +1,3 @@ +export { default } from './main.vue'; + +export { MsgType } from './types'; diff --git a/apps/web-ele/src/views/mp/components/wx-msg/main.vue b/apps/web-ele/src/views/mp/components/wx-msg/main.vue new file mode 100644 index 000000000..4e2bb6469 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-msg/main.vue @@ -0,0 +1,210 @@ + + + + + + diff --git a/apps/web-ele/src/views/mp/components/wx-msg/types.ts b/apps/web-ele/src/views/mp/components/wx-msg/types.ts new file mode 100644 index 000000000..e988954ad --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-msg/types.ts @@ -0,0 +1,17 @@ +export enum MsgType { + Event = 'event', + Image = 'image', + Link = 'link', + Location = 'location', + Music = 'music', + News = 'news', + Text = 'text', + Video = 'video', + Voice = 'voice', +} + +export interface User { + nickname: string; + avatar: string; + accountId: number; +} diff --git a/apps/web-ele/src/views/mp/components/wx-music/index.ts b/apps/web-ele/src/views/mp/components/wx-music/index.ts new file mode 100644 index 000000000..374d993fe --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-music/index.ts @@ -0,0 +1 @@ +export { default } from './main.vue'; diff --git a/apps/web-ele/src/views/mp/components/wx-music/main.vue b/apps/web-ele/src/views/mp/components/wx-music/main.vue new file mode 100644 index 000000000..e097e1dfc --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-music/main.vue @@ -0,0 +1,70 @@ + + + + + + diff --git a/apps/web-ele/src/views/mp/components/wx-news/index.ts b/apps/web-ele/src/views/mp/components/wx-news/index.ts new file mode 100644 index 000000000..374d993fe --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-news/index.ts @@ -0,0 +1 @@ +export { default } from './main.vue'; diff --git a/apps/web-ele/src/views/mp/components/wx-news/main.vue b/apps/web-ele/src/views/mp/components/wx-news/main.vue new file mode 100644 index 000000000..f93893c92 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-news/main.vue @@ -0,0 +1,123 @@ + + + + + + diff --git a/apps/web-ele/src/views/mp/components/wx-reply/components/TabImage.vue b/apps/web-ele/src/views/mp/components/wx-reply/components/TabImage.vue new file mode 100644 index 000000000..073934088 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-reply/components/TabImage.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/apps/web-ele/src/views/mp/components/wx-reply/components/TabMusic.vue b/apps/web-ele/src/views/mp/components/wx-reply/components/TabMusic.vue new file mode 100644 index 000000000..fe74ea779 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-reply/components/TabMusic.vue @@ -0,0 +1,137 @@ + + + diff --git a/apps/web-ele/src/views/mp/components/wx-reply/components/TabNews.vue b/apps/web-ele/src/views/mp/components/wx-reply/components/TabNews.vue new file mode 100644 index 000000000..85ede8813 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-reply/components/TabNews.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/apps/web-ele/src/views/mp/components/wx-reply/components/TabText.vue b/apps/web-ele/src/views/mp/components/wx-reply/components/TabText.vue new file mode 100644 index 000000000..a7e389db0 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-reply/components/TabText.vue @@ -0,0 +1,29 @@ + + + diff --git a/apps/web-ele/src/views/mp/components/wx-reply/components/TabVideo.vue b/apps/web-ele/src/views/mp/components/wx-reply/components/TabVideo.vue new file mode 100644 index 000000000..3ddcb2cd9 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-reply/components/TabVideo.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/apps/web-ele/src/views/mp/components/wx-reply/components/TabVoice.vue b/apps/web-ele/src/views/mp/components/wx-reply/components/TabVoice.vue new file mode 100644 index 000000000..9467120c6 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-reply/components/TabVoice.vue @@ -0,0 +1,174 @@ + + + + diff --git a/apps/web-ele/src/views/mp/components/wx-reply/components/types.ts b/apps/web-ele/src/views/mp/components/wx-reply/components/types.ts new file mode 100644 index 000000000..67571ef59 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-reply/components/types.ts @@ -0,0 +1,58 @@ +import type { Ref } from 'vue'; + +import { unref } from 'vue'; + +enum ReplyType { + Image = 'image', + Music = 'music', + News = 'news', + Text = 'text', + Video = 'video', + Voice = 'voice', +} + +interface _Reply { + accountId: number; + type: ReplyType; + name?: null | string; + content?: null | string; + mediaId?: null | string; + url?: null | string; + title?: null | string; + description?: null | string; + thumbMediaId?: null | string; + thumbMediaUrl?: null | string; + musicUrl?: null | string; + hqMusicUrl?: null | string; + introduction?: null | string; + articles?: any[]; +} + +type Reply = _Reply; // Partial<_Reply> + +enum NewsType { + Draft = '2', + Published = '1', +} + +/** 利用旧的reply[accountId, type]初始化新的Reply */ +const createEmptyReply = (old: Ref | Reply): Reply => { + return { + accountId: unref(old).accountId, + type: unref(old).type, + name: null, + content: null, + mediaId: null, + url: null, + title: null, + description: null, + thumbMediaId: null, + thumbMediaUrl: null, + musicUrl: null, + hqMusicUrl: null, + introduction: null, + articles: [], + }; +}; + +export { createEmptyReply, NewsType, type Reply, ReplyType }; diff --git a/apps/web-ele/src/views/mp/components/wx-reply/index.ts b/apps/web-ele/src/views/mp/components/wx-reply/index.ts new file mode 100644 index 000000000..a3f675bb1 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-reply/index.ts @@ -0,0 +1,8 @@ +export { + createEmptyReply, + NewsType, + type Reply, + ReplyType, +} from './components/types'; + +export { default } from './main.vue'; diff --git a/apps/web-ele/src/views/mp/components/wx-reply/main.vue b/apps/web-ele/src/views/mp/components/wx-reply/main.vue new file mode 100644 index 000000000..d3be2557a --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-reply/main.vue @@ -0,0 +1,215 @@ + + + + + + diff --git a/apps/web-ele/src/views/mp/components/wx-video-play/index.ts b/apps/web-ele/src/views/mp/components/wx-video-play/index.ts new file mode 100644 index 000000000..374d993fe --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-video-play/index.ts @@ -0,0 +1 @@ +export { default } from './main.vue'; diff --git a/apps/web-ele/src/views/mp/components/wx-video-play/main.vue b/apps/web-ele/src/views/mp/components/wx-video-play/main.vue new file mode 100644 index 000000000..364b05890 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-video-play/main.vue @@ -0,0 +1,76 @@ + + + + diff --git a/apps/web-ele/src/views/mp/components/wx-voice-play/index.ts b/apps/web-ele/src/views/mp/components/wx-voice-play/index.ts new file mode 100644 index 000000000..374d993fe --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-voice-play/index.ts @@ -0,0 +1 @@ +export { default } from './main.vue'; diff --git a/apps/web-ele/src/views/mp/components/wx-voice-play/main.vue b/apps/web-ele/src/views/mp/components/wx-voice-play/main.vue new file mode 100644 index 000000000..23b721380 --- /dev/null +++ b/apps/web-ele/src/views/mp/components/wx-voice-play/main.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/apps/web-ele/src/views/mp/hooks/useUpload.ts b/apps/web-ele/src/views/mp/hooks/useUpload.ts new file mode 100644 index 000000000..05da55f58 --- /dev/null +++ b/apps/web-ele/src/views/mp/hooks/useUpload.ts @@ -0,0 +1,67 @@ +import type { UploadRawFile } from 'element-plus'; + +import { ElMessage } from 'element-plus'; + +const message = ElMessage; // 消息 + +enum UploadType { + Image = 'image', + Video = 'video', + Voice = 'voice', +} + +const useBeforeUpload = (type: UploadType, maxSizeMB: number) => { + const fn = (rawFile: UploadRawFile): boolean => { + let allowTypes: string[] = []; + let name = ''; + + switch (type) { + case UploadType.Image: { + allowTypes = [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/bmp', + 'image/jpg', + ]; + maxSizeMB = 2; + name = '图片'; + break; + } + case UploadType.Video: { + allowTypes = ['video/mp4']; + maxSizeMB = 10; + name = '视频'; + break; + } + case UploadType.Voice: { + allowTypes = [ + 'audio/mp3', + 'audio/mpeg', + 'audio/wma', + 'audio/wav', + 'audio/amr', + ]; + maxSizeMB = 2; + name = '语音'; + break; + } + } + // 格式不正确 + if (!allowTypes.includes(rawFile.type)) { + message.error(`上传${name}格式不对!`); + return false; + } + // 大小不正确 + if (rawFile.size / 1024 / 1024 > maxSizeMB) { + message.error(`上传${name}大小不能超过${maxSizeMB}M!`); + return false; + } + + return true; + }; + + return fn; +}; + +export { UploadType, useBeforeUpload }; diff --git a/apps/web-ele/src/views/mp/menu/assets/iphone_backImg.png b/apps/web-ele/src/views/mp/menu/assets/iphone_backImg.png new file mode 100644 index 0000000000000000000000000000000000000000..bb09591a7912730aab07287e0bb2bc4e3383abc5 GIT binary patch literal 34272 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 n1Y$yl2Y%)kp^j~lTlnWvqUJ7=Y>Ul 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 X!lw4y|2_Uwf${mHs$8XvQQ-do;aw2N literal 0 HcmV?d00001 diff --git a/apps/web-ele/src/views/mp/menu/components/menuOptions.ts b/apps/web-ele/src/views/mp/menu/components/menuOptions.ts new file mode 100644 index 000000000..9ba818fc4 --- /dev/null +++ b/apps/web-ele/src/views/mp/menu/components/menuOptions.ts @@ -0,0 +1,42 @@ +export default [ + { + value: 'view', + label: '跳转网页', + }, + { + value: 'miniprogram', + label: '跳转小程序', + }, + { + value: 'click', + label: '点击回复', + }, + { + value: 'article_view_limited', + label: '跳转图文消息', + }, + { + value: 'scancode_push', + label: '扫码直接返回结果', + }, + { + value: 'scancode_waitmsg', + label: '扫码回复', + }, + { + value: 'pic_sysphoto', + label: '系统拍照发图', + }, + { + value: 'pic_photo_or_album', + label: '拍照或者相册', + }, + { + value: 'pic_weixin', + label: '微信相册', + }, + { + value: 'location_select', + label: '选择地理位置', + }, +]; diff --git a/apps/web-ele/src/views/mp/menu/components/types.ts b/apps/web-ele/src/views/mp/menu/components/types.ts new file mode 100644 index 000000000..928534ea9 --- /dev/null +++ b/apps/web-ele/src/views/mp/menu/components/types.ts @@ -0,0 +1,73 @@ +export interface Replay { + title: string; + description: string; + picUrl: string; + url: string; +} + +export type MenuType = + | '' + | 'article_view_limited' + | 'click' + | 'location_select' + | 'pic_photo_or_album' + | 'pic_sysphoto' + | 'pic_weixin' + | 'scancode_push' + | 'scancode_waitmsg' + | 'view'; + +interface _RawMenu { + // db + id: number; + parentId: number; + accountId: number; + appId: string; + createTime: number; + + // mp-native + name: string; + menuKey: string; + type: MenuType; + url: string; + miniProgramAppId: string; + miniProgramPagePath: string; + articleId: string; + replyMessageType: string; + replyContent: string; + replyMediaId: string; + replyMediaUrl: string; + replyThumbMediaId: string; + replyThumbMediaUrl: string; + replyTitle: string; + replyDescription: string; + replyArticles: Replay; + replyMusicUrl: string; + replyHqMusicUrl: string; +} + +export type RawMenu = Partial<_RawMenu>; + +interface _Reply { + type: string; + accountId: number; + content: string; + mediaId: string; + url: string; + thumbMediaId: string; + thumbMediaUrl: string; + title: string; + description: string; + articles: null | Replay[]; + musicUrl: string; + hqMusicUrl: string; +} + +export type Reply = Partial<_Reply>; + +interface _Menu extends RawMenu { + children: _Menu[]; + reply: Reply; +} + +export type Menu = Partial<_Menu>; diff --git a/apps/web-ele/src/views/mp/menu/data.ts b/apps/web-ele/src/views/mp/menu/data.ts new file mode 100644 index 000000000..2a22837ba --- /dev/null +++ b/apps/web-ele/src/views/mp/menu/data.ts @@ -0,0 +1,9 @@ +/** 菜单未选中标识 */ +export const MENU_NOT_SELECTED = '__MENU_NOT_SELECTED__'; + +/** 菜单级别枚举 */ +export enum Level { + Child = '2', + Parent = '1', + Undefined = '0', +} diff --git a/apps/web-ele/src/views/mp/menu/index.vue b/apps/web-ele/src/views/mp/menu/index.vue new file mode 100644 index 000000000..36425357f --- /dev/null +++ b/apps/web-ele/src/views/mp/menu/index.vue @@ -0,0 +1,411 @@ + + + + + diff --git a/apps/web-ele/src/views/mp/menu/modules/menu-editor.vue b/apps/web-ele/src/views/mp/menu/modules/menu-editor.vue new file mode 100644 index 000000000..45f259a68 --- /dev/null +++ b/apps/web-ele/src/views/mp/menu/modules/menu-editor.vue @@ -0,0 +1,278 @@ + + + + + diff --git a/apps/web-ele/src/views/mp/menu/modules/menu-previewer.vue b/apps/web-ele/src/views/mp/menu/modules/menu-previewer.vue new file mode 100644 index 000000000..44ae9fc1d --- /dev/null +++ b/apps/web-ele/src/views/mp/menu/modules/menu-previewer.vue @@ -0,0 +1,250 @@ + + + + + diff --git a/playground/package.json b/playground/package.json index c8651563a..6c3045e62 100644 --- a/playground/package.json +++ b/playground/package.json @@ -54,6 +54,7 @@ "vue-router": "catalog:" }, "devDependencies": { - "@types/json-bigint": "catalog:" + "@types/json-bigint": "catalog:", + "vite": "catalog:" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d74811fa4..484162529 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -669,10 +669,10 @@ importers: version: link:scripts/vsh '@vitejs/plugin-vue': specifier: 'catalog:' - version: 6.0.1(vite@7.1.11(@types/node@22.18.11)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) + version: 6.0.1(vite@7.1.11(@types/node@22.18.11)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) '@vitejs/plugin-vue-jsx': specifier: 'catalog:' - version: 5.1.1(vite@7.1.11(@types/node@22.18.11)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) + version: 5.1.1(vite@7.1.11(@types/node@22.18.11)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) '@vue/test-utils': specifier: 'catalog:' version: 2.4.6 @@ -714,10 +714,10 @@ importers: version: 3.6.1(sass@1.93.2)(typescript@5.9.3)(vue-tsc@2.2.10(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) vite: specifier: 'catalog:' - version: 7.1.11(@types/node@22.18.11)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) + version: 7.1.11(@types/node@22.18.11)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) vitest: specifier: 'catalog:' - version: 3.2.4(@types/node@22.18.11)(happy-dom@17.6.3)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) + version: 3.2.4(@types/node@22.18.11)(happy-dom@17.6.3)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) vue: specifier: ^3.5.17 version: 3.5.22(typescript@5.9.3) @@ -854,6 +854,9 @@ importers: vue3-signature: specifier: 'catalog:' version: 0.2.4(vue@3.5.22(typescript@5.9.3)) + vuedraggable: + specifier: 'catalog:' + version: 4.1.0(vue@3.5.22(typescript@5.9.3)) apps/web-ele: dependencies: @@ -914,6 +917,9 @@ importers: '@vueuse/integrations': specifier: 'catalog:' version: 13.9.0(async-validator@4.2.5)(axios@1.12.2)(focus-trap@7.6.5)(nprogress@0.2.0)(qrcode@1.5.4)(sortablejs@1.15.6)(vue@3.5.22(typescript@5.9.3)) + benz-amr-recorder: + specifier: ^1.1.5 + version: 1.1.5 cropperjs: specifier: 'catalog:' version: 1.6.2 @@ -2113,6 +2119,9 @@ importers: '@types/json-bigint': specifier: 'catalog:' version: 1.0.4 + vite: + specifier: 'catalog:' + version: 7.1.11(@types/node@24.8.1)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) scripts/turbo-run: dependencies: @@ -5721,6 +5730,12 @@ packages: resolution: {integrity: sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==} hasBin: true + benz-amr-recorder@1.1.5: + resolution: {integrity: sha512-NepctcNTsZHK8NxBb5uKO5p8S+xkbm+vD6GLSkCYdJeEsriexvgumLHpDkanX4QJBcLRMVtg16buWMs+gUPB3g==} + + benz-recorderjs@1.0.5: + resolution: {integrity: sha512-EwedOQo9KLti7HxDi/eZY51PSRbAXnOdEZmLvJ6ro3QQSoF9Y3AXBt57MIllGvVz5vtFYMeikG+GD7qTm3+p9w==} + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -15108,14 +15123,14 @@ snapshots: dependencies: vite-plugin-pwa: 1.1.0(vite@5.4.21(@types/node@24.8.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0))(workbox-build@7.3.0)(workbox-window@7.3.0) - '@vitejs/plugin-vue-jsx@5.1.1(vite@7.1.11(@types/node@22.18.11)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': + '@vitejs/plugin-vue-jsx@5.1.1(vite@7.1.11(@types/node@22.18.11)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.4) '@rolldown/pluginutils': 1.0.0-beta.44 '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.4) - vite: 7.1.11(@types/node@22.18.11)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.11(@types/node@22.18.11)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) vue: 3.5.22(typescript@5.9.3) transitivePeerDependencies: - supports-color @@ -15137,10 +15152,10 @@ snapshots: vite: 5.4.21(@types/node@24.8.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0) vue: 3.5.22(typescript@5.9.3) - '@vitejs/plugin-vue@6.0.1(vite@7.1.11(@types/node@22.18.11)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.1(vite@7.1.11(@types/node@22.18.11)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.29 - vite: 7.1.11(@types/node@22.18.11)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.11(@types/node@22.18.11)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) vue: 3.5.22(typescript@5.9.3) '@vitejs/plugin-vue@6.0.1(vite@7.1.11(@types/node@24.8.1)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': @@ -15157,13 +15172,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.11(@types/node@22.18.11)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(vite@7.1.11(@types/node@22.18.11)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 7.1.11(@types/node@22.18.11)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.11(@types/node@22.18.11)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -15782,6 +15797,12 @@ snapshots: baseline-browser-mapping@2.8.18: {} + benz-amr-recorder@1.1.5: + dependencies: + benz-recorderjs: 1.0.5 + + benz-recorderjs@1.0.5: {} + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 @@ -21583,13 +21604,13 @@ snapshots: dependencies: vite: 7.1.11(@types/node@24.8.1)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) - vite-node@3.2.4(@types/node@22.18.11)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1): + vite-node@3.2.4(@types/node@22.18.11)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.11(@types/node@22.18.11)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.11(@types/node@22.18.11)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -21758,7 +21779,7 @@ snapshots: sass: 1.93.2 terser: 5.44.0 - vite@7.1.11(@types/node@22.18.11)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1): + vite@7.1.11(@types/node@22.18.11)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1): dependencies: esbuild: 0.25.3 fdir: 6.5.0(picomatch@4.0.3) @@ -21769,7 +21790,7 @@ snapshots: optionalDependencies: '@types/node': 22.18.11 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 1.21.7 less: 4.4.2 sass: 1.93.2 terser: 5.44.0 @@ -21851,11 +21872,11 @@ snapshots: - typescript - universal-cookie - vitest@3.2.4(@types/node@22.18.11)(happy-dom@17.6.3)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1): + vitest@3.2.4(@types/node@22.18.11)(happy-dom@17.6.3)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.11(@types/node@22.18.11)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.1.11(@types/node@22.18.11)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -21873,8 +21894,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.11(@types/node@22.18.11)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@22.18.11)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.11(@types/node@22.18.11)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@22.18.11)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.18.11 @@ -21897,7 +21918,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.11(@types/node@22.18.11)(jiti@2.6.1)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.1.11(@types/node@22.18.11)(jiti@1.21.7)(less@4.4.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 From 81f85c200e3d673900869569d3141d7428c35b09 Mon Sep 17 00:00:00 2001 From: hw Date: Mon, 3 Nov 2025 11:18:45 +0800 Subject: [PATCH 05/76] =?UTF-8?q?style:=20=E4=BB=A3=E7=A0=81=E9=A3=8E?= =?UTF-8?q?=E6=A0=BC=E7=BB=9F=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-ele/.env.development | 2 +- apps/web-ele/src/views/mp/menu/index.vue | 68 +++++++-------- .../src/views/mp/menu/modules/menu-editor.vue | 18 ++-- .../views/mp/menu/modules/menu-previewer.vue | 34 ++++---- .../{components => modules}/menuOptions.ts | 0 .../mp/menu/{components => modules}/types.ts | 0 .../wx-account-select/index.ts | 0 .../wx-account-select/main.vue | 10 ++- .../wx-location/index.ts | 0 .../wx-location/main.vue | 0 .../wx-material-select/index.ts | 0 .../wx-material-select/main.vue | 31 ++++--- .../wx-material-select/types.ts | 0 .../{components => modules}/wx-msg/card.scss | 0 .../wx-msg/comment.scss | 0 .../wx-msg/components/Msg.vue | 10 +-- .../wx-msg/components/MsgEvent.vue | 0 .../wx-msg/components/MsgList.vue | 0 .../{components => modules}/wx-msg/index.ts | 0 .../{components => modules}/wx-msg/main.vue | 29 ++++--- .../{components => modules}/wx-msg/types.ts | 0 .../{components => modules}/wx-music/index.ts | 0 .../{components => modules}/wx-music/main.vue | 0 .../{components => modules}/wx-news/index.ts | 0 .../{components => modules}/wx-news/main.vue | 0 .../wx-reply/components/TabImage.vue | 74 ++++++++++------- .../wx-reply/components/TabMusic.vue | 83 +++++++++++-------- .../wx-reply/components/TabNews.vue | 58 +++++++------ .../wx-reply/components/TabText.vue | 4 +- .../wx-reply/components/TabVideo.vue | 78 +++++++++-------- .../wx-reply/components/TabVoice.vue | 77 ++++++++++------- .../wx-reply/components/types.ts | 0 .../{components => modules}/wx-reply/index.ts | 0 .../{components => modules}/wx-reply/main.vue | 52 ++++++------ .../wx-video-play/index.ts | 0 .../wx-video-play/main.vue | 0 .../wx-voice-play/index.ts | 0 .../wx-voice-play/main.vue | 0 38 files changed, 353 insertions(+), 275 deletions(-) rename apps/web-ele/src/views/mp/menu/{components => modules}/menuOptions.ts (100%) rename apps/web-ele/src/views/mp/menu/{components => modules}/types.ts (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-account-select/index.ts (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-account-select/main.vue (94%) rename apps/web-ele/src/views/mp/{components => modules}/wx-location/index.ts (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-location/main.vue (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-material-select/index.ts (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-material-select/main.vue (93%) rename apps/web-ele/src/views/mp/{components => modules}/wx-material-select/types.ts (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-msg/card.scss (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-msg/comment.scss (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-msg/components/Msg.vue (87%) rename apps/web-ele/src/views/mp/{components => modules}/wx-msg/components/MsgEvent.vue (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-msg/components/MsgList.vue (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-msg/index.ts (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-msg/main.vue (93%) rename apps/web-ele/src/views/mp/{components => modules}/wx-msg/types.ts (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-music/index.ts (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-music/main.vue (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-news/index.ts (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-news/main.vue (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-reply/components/TabImage.vue (74%) rename apps/web-ele/src/views/mp/{components => modules}/wx-reply/components/TabMusic.vue (64%) rename apps/web-ele/src/views/mp/{components => modules}/wx-reply/components/TabNews.vue (59%) rename apps/web-ele/src/views/mp/{components => modules}/wx-reply/components/TabText.vue (91%) rename apps/web-ele/src/views/mp/{components => modules}/wx-reply/components/TabVideo.vue (69%) rename apps/web-ele/src/views/mp/{components => modules}/wx-reply/components/TabVoice.vue (71%) rename apps/web-ele/src/views/mp/{components => modules}/wx-reply/components/types.ts (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-reply/index.ts (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-reply/main.vue (80%) rename apps/web-ele/src/views/mp/{components => modules}/wx-video-play/index.ts (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-video-play/main.vue (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-voice-play/index.ts (100%) rename apps/web-ele/src/views/mp/{components => modules}/wx-voice-play/main.vue (100%) diff --git a/apps/web-ele/.env.development b/apps/web-ele/.env.development index cf25c6dd5..cb83c77f0 100644 --- a/apps/web-ele/.env.development +++ b/apps/web-ele/.env.development @@ -4,7 +4,7 @@ VITE_PORT=5777 VITE_BASE=/ # 请求路径 -VITE_BASE_URL=http://127.0.0.1:48080 +VITE_BASE_URL=http://47.103.66.220:48080 # 接口地址 VITE_GLOB_API_URL=http://47.103.66.220:48080/admin-api # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 diff --git a/apps/web-ele/src/views/mp/menu/index.vue b/apps/web-ele/src/views/mp/menu/index.vue index 36425357f..d95d1268f 100644 --- a/apps/web-ele/src/views/mp/menu/index.vue +++ b/apps/web-ele/src/views/mp/menu/index.vue @@ -1,5 +1,5 @@