-
Notifications
You must be signed in to change notification settings - Fork 8.3k
feat: add app with Tdesign #6836
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
2264eaa
33b7a60
04321b1
b472fbb
57620dc
bd8ff73
a38d081
dc77721
be84330
3e8e869
1af1124
99d663a
b7bfd69
83ea27a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # 应用标题 | ||
| VITE_APP_TITLE=Vben Admin Tdesign | ||
|
|
||
| # 应用命名空间,用于缓存、store等功能的前缀,确保隔离 | ||
| VITE_APP_NAMESPACE=vben-web-tdesign | ||
|
|
||
| # 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密 | ||
| VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # public path | ||
| VITE_BASE=/ | ||
|
|
||
| # Basic interface address SPA | ||
| VITE_GLOB_API_URL=/api | ||
|
|
||
| VITE_VISUALIZER=true |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| <!doctype html> | ||
| <html lang="zh"> | ||
likui628 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> | ||
| <meta name="renderer" content="webkit" /> | ||
| <meta name="description" content="A Modern Back-end Management System" /> | ||
| <meta name="keywords" content="Vben Admin Vue3 Vite" /> | ||
| <meta name="author" content="Vben" /> | ||
| <meta | ||
| name="viewport" | ||
| content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0" | ||
| /> | ||
| <!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 --> | ||
| <title><%= VITE_APP_TITLE %></title> | ||
| <link rel="icon" href="/favicon.ico" /> | ||
| <script> | ||
| // 生产环境下注入百度统计 | ||
| if (window._VBEN_ADMIN_PRO_APP_CONF_) { | ||
| var _hmt = _hmt || []; | ||
| (function () { | ||
| var hm = document.createElement('script'); | ||
| hm.src = | ||
| 'https://hm.baidu.com/hm.js?b38e689f40558f20a9a686d7f6f33edf'; | ||
| var s = document.getElementsByTagName('script')[0]; | ||
| s.parentNode.insertBefore(hm, s); | ||
| })(); | ||
| } | ||
| </script> | ||
| </head> | ||
| <body> | ||
| <div id="app"></div> | ||
| <script type="module" src="/src/main.ts"></script> | ||
| </body> | ||
| </html> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }, | ||
| "license": "MIT", | ||
| "author": { | ||
| "name": "vben", | ||
| "email": "[email protected]", | ||
| "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:" | ||
| }, | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { default } from '@vben/tailwind-config/postcss'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,229 @@ | ||
| import type { Component } from 'vue'; | ||
|
|
||
| import type { BaseFormComponentType } from '@vben/common-ui'; | ||
| import type { Recordable } from '@vben/types'; | ||
|
|
||
| import { defineAsyncComponent, defineComponent, h, ref } from 'vue'; | ||
|
|
||
| import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; | ||
| import { $t } from '@vben/locales'; | ||
|
|
||
| import { notification } from 'ant-design-vue'; | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用 | ||
| * 可用于 vben-form、vben-modal、vben-drawer 等组件使用, | ||
| */ | ||
|
|
||
| const AutoComplete = defineAsyncComponent( | ||
| () => 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 = <T extends Component>( | ||
| component: T, | ||
| type: 'input' | 'select', | ||
| componentProps: Recordable<any> = {}, | ||
| ) => { | ||
| 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<Record<ComponentType, Component>> = { | ||
| // 如果你的组件体积比较大,可以使用异步加载 | ||
| // Button: () => | ||
| // import('xxx').then((res) => res.Button), | ||
| ApiSelect: withDefaultPlaceholder( | ||
| { | ||
| ...ApiComponent, | ||
| name: 'ApiSelect', | ||
| }, | ||
| 'select', | ||
| { | ||
| component: Select, | ||
| loadingSlot: 'suffixIcon', | ||
| visibleEvent: 'onDropdownVisibleChange', | ||
| modelPropName: 'value', | ||
| }, | ||
| ), | ||
|
Comment on lines
+134
to
+146
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainVerify TDesign Select API compatibility. The 🏁 Script executed: #!/bin/bash
# Search for TDesign Select usage patterns and event handlers in the codebase
rg -n 'onDropdownVisibleChange|onVisibleChange' --type=ts --type=vue -C2
# Search for suffixIcon usage with TDesign Select
rg -n 'suffixIcon' --type=ts --type=vue -C2Length of output: 55 🏁 Script executed: #!/bin/bash
# Search for event handlers without type flag (works for .ts files)
rg -n 'onDropdownVisibleChange|onVisibleChange' --type=ts -C2
# Search for suffixIcon in TypeScript files
rg -n 'suffixIcon' --type=ts -C2
# Search in Vue files using glob pattern
rg -n 'onDropdownVisibleChange|onVisibleChange' -g "*.vue" -C2
# Search for suffixIcon in Vue files
rg -n 'suffixIcon' -g "*.vue" -C2
# Search for tdesign Select imports and configurations
rg -n 'from.*tdesign.*Select|import.*Select' --type=ts -C2Length of output: 8090 🏁 Script executed: #!/bin/bash
# Search for tdesign Select imports and component definitions
rg -n 'from.*tdesign|import.*Select' --type=ts -C3
# Look for ApiSelect or component adapter configurations
rg -n 'ApiSelect|visibleEvent|loadingSlot' --type=ts -C2
# Check TDesign Select event names in codebase usage
rg -n 'Select.*event|onVisible|onDropdown' -g "*.vue" -C2Length of output: 15061 💡 Result: Short answer — Select's visibility callback receives (visible, context). API: onVisibleChange(visible: boolean, context: PopupVisibleChangeContext)
Sources: 💡 Result: Short answer: TDesign Vue Select supports a suffixIcon slot named "suffixIcon" (and also a "suffix" slot for custom content). Use it like: Example: <template #suffix> References: TDesign changelog noting Select added suffix/suffixIcon and slot fixes [1]; TDesign Vue repo/docs overview [2]. Sources: Fix incorrect TDesign Select event name at line 143. The 🤖 Prompt for AI Agents |
||
| 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', | ||
| }), | ||
|
Comment on lines
+171
to
+175
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainVerify TDesign Input slot compatibility for IconPicker. The 🏁 Script executed: #!/bin/bash
# Search for TDesign Input slot usage patterns in the codebase
rg -n 'addonAfter|suffix|suffixIcon' --type=ts --type=vue -C3 -g '**/tdesign**' -g '**Input**'
# Also check IconPicker usage across the codebase
rg -n 'IconPicker' --type=ts --type=vue -C3Length of output: 55 Length of output: 23848 🏁 Script executed: #!/bin/bash
# Search for IconPicker configuration usage
rg -n 'IconPicker' -C5
# Search for slot patterns in the tdesign adapter directory
rg -n 'addonAfter|suffix|suffixIcon' apps/web-tdesign/src/
# Search for Input component usage with slots in the adapter
rg -n 'Input.*:' apps/web-tdesign/src/adapter/ -C3Length of output: 23140 🌐 Web query: 💡 Result: TDesign Vue Input supports front/back adornments via prepend/append props (and an InputAdornment subcomponent) and also standard prefix/suffix slots (named slots). Use the named slots: prefix, suffix (inside the input), and prepend, append (outside as input-group). See the component props/types for prepend/append and the input-adornment types. [1][2][3] Example (vue template): Sources: tdesign input-adornment props/types and input-adornment declaration. [1][2]; example slot names documented in input component guides. [3] Change TDesign Input slot from TDesign Vue Input supports named slots: Change line 172 from: to: (Align with web-naive's 🤖 Prompt for AI Agents |
||
| Input: withDefaultPlaceholder(Input, 'input'), | ||
| InputNumber: withDefaultPlaceholder(InputNumber, 'input'), | ||
| // InputPassword: withDefaultPlaceholder(InputPassword, 'input'), | ||
| // Mentions: withDefaultPlaceholder(Mentions, 'input'), | ||
| // 自定义主要按钮 | ||
| PrimaryButton: (props, { attrs, 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, | ||
| ); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }, | ||
| Radio, | ||
| RadioGroup, | ||
| RangePicker: (props, { attrs, slots }) => { | ||
| return h( | ||
| RangePicker, | ||
| { ...props, modelValue: props.modelValue ?? [], attrs }, | ||
| slots, | ||
| ); | ||
| }, | ||
| 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 }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Critical security issue: Replace the placeholder encryption key before deployment.
The placeholder value
please-replace-me-with-your-own-keyremains unresolved. Per prior analysis, this key is actively used for AES encryption of localStorage data. Deploying with this placeholder would compromise the security of encrypted store data.Before any production deployment, generate and inject a strong, randomly generated encryption key via GitHub Actions secrets or
.env.localoverride to replace this placeholder.I'll verify the security concern in the review comment by examining the codebase.
🏁 Script executed:
Length of output: 809
I need to correct my search syntax and locate where this key is actually used.
🏁 Script executed:
Length of output: 5136
🏁 Script executed:
Length of output: 5119
I need to check for deployment safeguards and build-time configurations.
🏁 Script executed:
Length of output: 776
I need to verify if this is a template project or actual production deployment, and check for environment-specific configurations.
🏁 Script executed:
Length of output: 1273
Let me check the environment-specific configuration files to see if production has a proper value or also uses the placeholder.
🏁 Script executed:
Length of output: 1437
Address the placeholder encryption key before production deployment.
The review comment is verified as accurate. The placeholder
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-keyinapps/web-tdesign/.envwill be used for AES encryption of localStorage data (configured inpackages/stores/src/setup.ts:26) unless explicitly overridden. Since.env.productioncontains no override for this key and no build-time validation exists, production builds will be compiled with the placeholder unless you:.env.localwith a strong encryption key (development only; not suitable for production builds)VITE_APP_STORE_SECURE_KEYas an environment variable before runningpnpm vite build --mode production.env.productionwith a generated encryption keyWithout one of these steps, encrypted store data in production remains vulnerable.
🤖 Prompt for AI Agents