Browse Source

Merge 02433a87b3 into 692dc0fa17

pull/1529/merge
yx lin 7 hours ago
committed by GitHub
parent
commit
fe2efaf952
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      apps/vben5/.changeset/.prettierrc.json
  2. 7
      apps/vben5/.changeset/fancy-ears-walk.md
  3. 12
      apps/vben5/.gitignore
  4. 2
      apps/vben5/.node-version
  5. 12
      apps/vben5/.npmrc
  6. 20
      apps/vben5/.prettierignore
  7. 1
      apps/vben5/.prettierrc.mjs
  8. 5
      apps/vben5/.stylelintignore
  9. 10
      apps/vben5/.vscode/extensions.json
  10. 56
      apps/vben5/.vscode/settings.json
  11. 2
      apps/vben5/README.ja-JP.md
  12. 2
      apps/vben5/README.md
  13. 2
      apps/vben5/README.zh-CN.md
  14. 1
      apps/vben5/apps/app-antd/.env.development
  15. 2
      apps/vben5/apps/app-antd/.vite/deps/_metadata.json
  16. 3
      apps/vben5/apps/app-antd/index.html
  17. 1
      apps/vben5/apps/app-antd/package.json
  18. 1
      apps/vben5/apps/app-antd/postcss.config.mjs
  19. 192
      apps/vben5/apps/app-antd/public/resource/tinymce/langs/en.js
  20. 700
      apps/vben5/apps/app-antd/public/resource/tinymce/langs/zh-CN.js
  21. 674
      apps/vben5/apps/app-antd/src/adapter/component/index.ts
  22. 4
      apps/vben5/apps/app-antd/src/adapter/request/index.ts
  23. 12
      apps/vben5/apps/app-antd/src/bootstrap.ts
  24. 3
      apps/vben5/apps/app-antd/src/layouts/auth.vue
  25. 21
      apps/vben5/apps/app-antd/src/layouts/basic.vue
  26. 6
      apps/vben5/apps/app-antd/src/preferences.ts
  27. 9
      apps/vben5/apps/app-antd/src/store/auth.ts
  28. 3
      apps/vben5/apps/app-antd/src/views/_core/authentication/code-login.vue
  29. 3
      apps/vben5/apps/app-antd/src/views/_core/authentication/forget-password.vue
  30. 3
      apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue
  31. 3
      apps/vben5/apps/app-antd/src/views/_core/authentication/qrcode-login.vue
  32. 3
      apps/vben5/apps/app-antd/src/views/_core/authentication/register.vue
  33. 24
      apps/vben5/apps/app-antd/src/views/_core/authentication/two-factor-login.vue
  34. 3
      apps/vben5/apps/app-antd/src/views/account/my-settings/index.vue
  35. 2
      apps/vben5/apps/app-antd/src/views/dashboard/analytics/analytics-visits-sales.vue
  36. 3
      apps/vben5/apps/app-antd/src/views/demos/antd/index.vue
  37. 1
      apps/vben5/apps/app-antd/tailwind.config.mjs
  38. 1
      apps/vben5/apps/app-antd/tsconfig.json
  39. 4
      apps/vben5/apps/backend-mock/api/auth/codes.ts
  40. 8
      apps/vben5/apps/backend-mock/api/auth/login.post.ts
  41. 2
      apps/vben5/apps/backend-mock/api/auth/logout.post.ts
  42. 4
      apps/vben5/apps/backend-mock/api/auth/refresh.post.ts
  43. 4
      apps/vben5/apps/backend-mock/api/demo/bigint.ts
  44. 4
      apps/vben5/apps/backend-mock/api/menu/all.ts
  45. 3
      apps/vben5/apps/backend-mock/api/status.ts
  46. 1
      apps/vben5/apps/backend-mock/api/system/dept/.post.ts
  47. 1
      apps/vben5/apps/backend-mock/api/system/dept/[id].delete.ts
  48. 1
      apps/vben5/apps/backend-mock/api/system/dept/[id].put.ts
  49. 1
      apps/vben5/apps/backend-mock/api/system/dept/list.ts
  50. 1
      apps/vben5/apps/backend-mock/api/system/menu/list.ts
  51. 3
      apps/vben5/apps/backend-mock/api/system/menu/name-exists.ts
  52. 3
      apps/vben5/apps/backend-mock/api/system/menu/path-exists.ts
  53. 1
      apps/vben5/apps/backend-mock/api/system/role/list.ts
  54. 85
      apps/vben5/apps/backend-mock/api/system/user/list.ts
  55. 80
      apps/vben5/apps/backend-mock/api/table/list.ts
  56. 2
      apps/vben5/apps/backend-mock/api/test.get.ts
  57. 2
      apps/vben5/apps/backend-mock/api/test.post.ts
  58. 12
      apps/vben5/apps/backend-mock/api/timezone/getTimezone.ts
  59. 11
      apps/vben5/apps/backend-mock/api/timezone/getTimezoneOptions.ts
  60. 26
      apps/vben5/apps/backend-mock/api/timezone/setTimezone.ts
  61. 3
      apps/vben5/apps/backend-mock/api/upload.ts
  62. 3
      apps/vben5/apps/backend-mock/api/user/info.ts
  63. 1
      apps/vben5/apps/backend-mock/middleware/1.api.ts
  64. 2
      apps/vben5/apps/backend-mock/nitro.config.ts
  65. 2
      apps/vben5/apps/backend-mock/package.json
  66. 2
      apps/vben5/apps/backend-mock/routes/[...].ts
  67. 10
      apps/vben5/apps/backend-mock/tsconfig.json
  68. 2
      apps/vben5/apps/backend-mock/utils/cookie-utils.ts
  69. 26
      apps/vben5/apps/backend-mock/utils/jwt-utils.ts
  70. 37
      apps/vben5/apps/backend-mock/utils/mock-data.ts
  71. 2
      apps/vben5/apps/backend-mock/utils/response.ts
  72. 9
      apps/vben5/apps/backend-mock/utils/timezone-utils.ts
  73. 5
      apps/vben5/apps/web-antd/index.html
  74. 2
      apps/vben5/apps/web-antd/package.json
  75. 1
      apps/vben5/apps/web-antd/postcss.config.mjs
  76. 619
      apps/vben5/apps/web-antd/src/adapter/component/index.ts
  77. 10
      apps/vben5/apps/web-antd/src/adapter/form.ts
  78. 16
      apps/vben5/apps/web-antd/src/adapter/vxe-table.ts
  79. 5
      apps/vben5/apps/web-antd/src/layouts/auth.vue
  80. 116
      apps/vben5/apps/web-antd/src/layouts/basic.vue
  81. 4
      apps/vben5/apps/web-antd/src/locales/langs/en-US/demos.json
  82. 3
      apps/vben5/apps/web-antd/src/locales/langs/en-US/page.json
  83. 4
      apps/vben5/apps/web-antd/src/locales/langs/zh-CN/demos.json
  84. 3
      apps/vben5/apps/web-antd/src/locales/langs/zh-CN/page.json
  85. 3
      apps/vben5/apps/web-antd/src/main.ts
  86. 63
      apps/vben5/apps/web-antd/src/preferences.ts
  87. 35
      apps/vben5/apps/web-antd/src/router/routes/modules/vben.ts
  88. 3
      apps/vben5/apps/web-antd/src/store/auth.ts
  89. 6
      apps/vben5/apps/web-antd/src/views/_core/authentication/code-login.vue
  90. 6
      apps/vben5/apps/web-antd/src/views/_core/authentication/forget-password.vue
  91. 3
      apps/vben5/apps/web-antd/src/views/_core/authentication/login.vue
  92. 6
      apps/vben5/apps/web-antd/src/views/_core/authentication/register.vue
  93. 65
      apps/vben5/apps/web-antd/src/views/_core/profile/base-setting.vue
  94. 48
      apps/vben5/apps/web-antd/src/views/_core/profile/index.vue
  95. 31
      apps/vben5/apps/web-antd/src/views/_core/profile/notification-setting.vue
  96. 62
      apps/vben5/apps/web-antd/src/views/_core/profile/password-setting.vue
  97. 43
      apps/vben5/apps/web-antd/src/views/_core/profile/security-setting.vue
  98. 2
      apps/vben5/apps/web-antd/src/views/dashboard/analytics/analytics-visits-sales.vue
  99. 4
      apps/vben5/apps/web-antd/src/views/dashboard/analytics/index.vue
  100. 10
      apps/vben5/apps/web-antd/src/views/dashboard/workspace/index.vue

3
apps/vben5/.changeset/.prettierrc.json

@ -0,0 +1,3 @@
{
"singleQuote": true
}

7
apps/vben5/.changeset/fancy-ears-walk.md

@ -0,0 +1,7 @@
---
'@vben/styles': patch
'@vben-core/form-ui': patch
'@vben/web-naive': patch
---
feat(@core/form-ui): 新增 useVbenForm 数组编辑器 VbenFormFieldArray

12
apps/vben5/.gitignore

@ -23,7 +23,8 @@ pnpm-lock.yaml
package-lock.json
.VSCodeCounter
**/backend-mock/data
.omx
.pnpm-store
# local env files
.env.local
.env.*.local
@ -51,3 +52,12 @@ vite.config.ts.*
*.sw?
.history
.cursor
# AI
.agent
.agents
.claude
.codex
skills-lock.json
.atomcode
datalog

2
apps/vben5/.node-version

@ -1 +1 @@
22.22.0
24.16.0

12
apps/vben5/.npmrc

@ -1,13 +1 @@
registry=https://registry.npmmirror.com
public-hoist-pattern[]=lefthook
public-hoist-pattern[]=eslint
public-hoist-pattern[]=prettier
public-hoist-pattern[]=prettier-plugin-tailwindcss
public-hoist-pattern[]=stylelint
public-hoist-pattern[]=*postcss*
public-hoist-pattern[]=@commitlint/*
public-hoist-pattern[]=czg
strict-peer-dependencies=false
auto-install-peers=true
dedupe-peer-dependents=true

20
apps/vben5/.prettierignore

@ -1,20 +0,0 @@
dist
dev-dist
.local
.output.js
node_modules
.nvmrc
coverage
CODEOWNERS
.nitro
.output
**/*.svg
**/*.sh
public
.npmrc
*-lock.yaml
packages/@abp/components

1
apps/vben5/.prettierrc.mjs

@ -1 +0,0 @@
export { default } from '@vben/prettier-config';

5
apps/vben5/.stylelintignore

@ -2,4 +2,7 @@ dist
public
__tests__
coverage
packages/@abp/components
.codex
.claude
.agent
.agents

10
apps/vben5/.vscode/extensions.json

@ -2,14 +2,18 @@
"recommendations": [
// Vue 3
"Vue.volar",
// ESLint JavaScript VS Code
// eslint VS Code
"dbaeumer.vscode-eslint",
// oxlint VS Code
"oxc.oxc-vscode",
// Visual Studio Code Stylelint
"stylelint.vscode-stylelint",
// 使 Prettier
"esbenp.prettier-vscode",
// 使 oxfmt
"oxc.oxc-vscode",
// dotenv
"mikestead.dotenv",
// YAML ESLint pnpm-workspace.yaml
"redhat.vscode-yaml",
//
"streetsidesoftware.code-spell-checker",
// Tailwind CSS VS Code

56
apps/vben5/.vscode/settings.json

@ -1,5 +1,6 @@
{
"tailwindCSS.experimental.configFile": "internal/tailwind-config/src/index.ts",
"tailwindCSS.experimental.configFile": "internal/tailwind-config/src/theme.css",
"tailwindCSS.lint.suggestCanonicalClasses": "ignore",
// workbench
"workbench.list.smoothScrolling": true,
"workbench.startupEditor": "newUntitledFile",
@ -31,39 +32,51 @@
"editor.autoClosingOvertype": "always",
"editor.autoClosingQuotes": "beforeWhitespace",
"editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?",
"editor.quickSuggestions": {
"strings": "on"
},
// lint && format
"oxc.enable": true,
"oxc.typeAware": true,
"oxc.configPath": "oxlint.config.ts",
"oxc.fmt.configPath": "oxfmt.config.ts",
"eslint.useFlatConfig": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.oxc": "explicit",
"source.fixAll.stylelint": "explicit",
"source.organizeImports": "never"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.defaultFormatter": "oxc.oxc-vscode",
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
// extensions
"extensions.ignoreRecommendations": true,
@ -79,6 +92,7 @@
"files.insertFinalNewline": true,
"files.simpleDialog.enable": true,
"files.associations": {
"*.css": "tailwindcss",
"*.ejs": "html",
"*.art": "html",
"**/tsconfig.json": "jsonc",
@ -118,7 +132,7 @@
// search
"search.searchEditor.singleClickBehaviour": "peekDefinition",
"search.followSymlinks": false,
// 使/
// 使
"search.exclude": {
"**/node_modules": true,
"**/*.log": true,
@ -158,7 +172,7 @@
"emmet.triggerExpansionOnTab": false,
"errorLens.enabledDiagnosticLevels": ["warning", "error"],
"errorLens.excludeBySource": ["cSpell", "Grammarly", "eslint"],
"errorLens.excludeBySource": ["cSpell", "Grammarly"],
"stylelint.enable": true,
"stylelint.packageManager": "pnpm",
@ -166,9 +180,10 @@
"stylelint.customSyntax": "postcss-html",
"stylelint.snippet": ["css", "less", "postcss", "scss", "vue"],
"typescript.inlayHints.enumMemberValues.enabled": true,
"typescript.preferences.preferTypeOnlyAutoImports": true,
"typescript.preferences.includePackageJsonAutoImports": "on",
"js/ts.tsdk.path": "node_modules/typescript/lib",
"js/ts.inlayHints.enumMemberValues.enabled": true,
"js/ts.preferences.preferTypeOnlyAutoImports": true,
"js/ts.preferences.includePackageJsonAutoImports": "on",
"eslint.validate": [
"javascript",
@ -195,7 +210,7 @@
"yaml": false
},
"cssVariables.lookupFiles": ["packages/core/base/design/src/**/*.css"],
"cssVariables.lookupFiles": ["packages/@core/base/design/src/**/*.css"],
"i18n-ally.localesPaths": [
"packages/locales/src/langs",
@ -220,12 +235,9 @@
"*.env": "$(capture).env.*",
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml",
"tailwind.config.mjs": "postcss.*"
"oxlint.config.ts": ".eslintignore,.stylelintignore,.commitlintrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml,oxfmt.config.*,eslint.config.*"
},
"commentTranslate.hover.enabled": false,
"commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": true,
"typescript.tsdk": "node_modules/typescript/lib",
"oxc.enable": false
"vue.server.hybridMode": true
}

2
apps/vben5/README.ja-JP.md

@ -114,7 +114,7 @@ pnpm build
## ブラウザサポート
ローカル開発には `Chrome 80+` ブラウザを推奨します
Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
モダンブラウザをサポートし、IEはサポートしません

2
apps/vben5/README.md

@ -114,7 +114,7 @@ Reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.
## Browser Support
The `Chrome 80+` browser is recommended for local development
Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
Support modern browsers, not IE

2
apps/vben5/README.zh-CN.md

@ -114,7 +114,7 @@ pnpm build
## 浏览器支持
本地开发推荐使用 `Chrome 80+` 浏览器
Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
支持现代浏览器,不支持 IE

1
apps/vben5/apps/app-antd/.env.development

@ -17,6 +17,7 @@ VITE_INJECT_APP_LOADING=true
# 是否仅允许OIDC登录
VITE_GLOB_AUTH_ONLY_OIDC=true
VITE_GLOB_AUTH_DISABLE_PKCE=true
# 认证服务器
VITE_GLOB_AUTH_AUTHORITY="http://localhost:30000"
# 授权范围

2
apps/vben5/apps/app-antd/.vite/deps/_metadata.json

@ -5,4 +5,4 @@
"browserHash": "cfcea6c4",
"optimized": {},
"chunks": {}
}
}

3
apps/vben5/apps/app-antd/index.html

@ -9,8 +9,7 @@
<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"
/>
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" />

1
apps/vben5/apps/app-antd/package.json

@ -50,6 +50,7 @@
"@abp/ui": "workspace:*",
"@abp/webhooks": "workspace:*",
"@abp/wechat": "workspace:*",
"@tanstack/vue-query": "catalog:",
"@vben-core/shadcn-ui": "workspace:*",
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",

1
apps/vben5/apps/app-antd/postcss.config.mjs

@ -1 +0,0 @@
export { default } from '@vben/tailwind-config/postcss';

192
apps/vben5/apps/app-antd/public/resource/tinymce/langs/en.js

@ -13,20 +13,20 @@ tinymce.addI18n('es', {
Italic: 'Cursiva',
Underline: 'Subrayado',
Strikethrough: 'Tachado',
Superscript: 'Super\u00edndice',
Subscript: 'Sub\u00edndice',
Superscript: 'Super\u00EDndice',
Subscript: 'Sub\u00EDndice',
'Clear formatting': 'Limpiar formato',
'Align left': 'Alinear a la izquierda',
'Align center': 'Alinear al centro',
'Align right': 'Alinear a la derecha',
Justify: 'Justificar',
'Bullet list': 'Lista de vi\u00f1etas',
'Bullet list': 'Lista de vi\u00F1etas',
'Numbered list': 'Lista numerada',
'Decrease indent': 'Disminuir sangr\u00eda',
'Increase indent': 'Incrementar sangr\u00eda',
'Decrease indent': 'Disminuir sangr\u00EDa',
'Increase indent': 'Incrementar sangr\u00EDa',
Close: 'Cerrar',
Formats: 'Formatos',
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": 'Su navegador no es compatible con el acceso directo al portapapeles. Use las teclas Crtl+X\/C\/V de su teclado.',
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X/C/V keyboard shortcuts instead.": 'Su navegador no es compatible con el acceso directo al portapapeles. Use las teclas Crtl+X/C/V de su teclado.',
Headers: 'Encabezados',
'Header 1': 'Encabezado 1',
'Header 2': 'Encabezado 2',
@ -44,23 +44,23 @@ tinymce.addI18n('es', {
Preformatted: 'Con formato previo',
Div: 'Div',
Pre: 'Pre',
Code: 'C\u00f3digo',
Paragraph: 'P\u00e1rrafo',
Code: 'C\u00F3digo',
Paragraph: 'P\u00E1rrafo',
Blockquote: 'Blockquote',
Inline: 'Alineado',
Blocks: 'Bloques',
'Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.': 'Pegar est\u00e1 ahora en modo de texto plano. El contenido se pegar\u00e1 como texto plano hasta que desactive esta opci\u00f3n.',
'Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.': 'Pegar est\u00E1 ahora en modo de texto plano. El contenido se pegar\u00E1 como texto plano hasta que desactive esta opci\u00F3n.',
Fonts: 'Fuentes',
'Font Sizes': 'Tama\u00f1os de fuente',
'Font Sizes': 'Tama\u00F1os de fuente',
Class: 'Clase',
'Browse for an image': 'Buscar una imagen',
OR: 'OR',
'Drop an image here': 'Arrastre una imagen aqu\u00ed',
'Drop an image here': 'Arrastre una imagen aqu\u00ED',
Upload: 'Cargar',
Block: 'Bloque',
Align: 'Alinear',
Default: 'Por defecto',
Circle: 'C\u00edrculo',
Circle: 'C\u00EDrculo',
Disc: 'Disco',
Square: 'Cuadrado',
'Lower Alpha': 'Inferior Alfa',
@ -71,14 +71,14 @@ tinymce.addI18n('es', {
'Anchor...': 'Anclaje...',
Name: 'Nombre',
Id: 'Id',
'Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.': 'Deber\u00eda comenzar por una letra, seguida solo de letras, n\u00fameros, guiones, puntos, dos puntos o guiones bajos.',
'You have unsaved changes are you sure you want to navigate away?': 'Tiene cambios sin guardar. \u00bfEst\u00e1 seguro de que quiere salir?',
'Restore last draft': 'Restaurar el \u00faltimo borrador',
'Special character...': 'Car\u00e1cter especial...',
'Source code': 'C\u00f3digo fuente',
'Insert\/Edit code sample': 'Insertar\/editar c\u00f3digo de prueba',
'Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.': 'Deber\u00EDa comenzar por una letra, seguida solo de letras, n\u00FAmeros, guiones, puntos, dos puntos o guiones bajos.',
'You have unsaved changes are you sure you want to navigate away?': 'Tiene cambios sin guardar. \u00BFEst\u00E1 seguro de que quiere salir?',
'Restore last draft': 'Restaurar el \u00FAltimo borrador',
'Special character...': 'Car\u00E1cter especial...',
'Source code': 'C\u00F3digo fuente',
'Insert/Edit code sample': 'Insertar/editar c\u00F3digo de prueba',
Language: 'Idioma',
'Code sample...': 'Ejemplo de c\u00f3digo...',
'Code sample...': 'Ejemplo de c\u00F3digo...',
'Color Picker': 'Selector de colores',
R: 'R',
G: 'V',
@ -87,33 +87,33 @@ tinymce.addI18n('es', {
'Right to left': 'De derecha a izquierda',
'Emoticons...': 'Emoticones...',
'Metadata and Document Properties': 'Metadatos y propiedades del documento',
Title: 'T\u00edtulo',
Title: 'T\u00EDtulo',
Keywords: 'Palabras clave',
Description: 'Descripci\u00f3n',
Description: 'Descripci\u00F3n',
Robots: 'Robots',
Author: 'Autor',
Encoding: 'Codificaci\u00f3n',
Encoding: 'Codificaci\u00F3n',
Fullscreen: 'Pantalla completa',
Action: 'Acci\u00f3n',
Action: 'Acci\u00F3n',
Shortcut: 'Atajo',
Help: 'Ayuda',
Address: 'Direcci\u00f3n',
'Focus to menubar': 'Enfocar la barra del men\u00fa',
Address: 'Direcci\u00F3n',
'Focus to menubar': 'Enfocar la barra del men\u00FA',
'Focus to toolbar': 'Enfocar la barra de herramientas',
'Focus to element path': 'Enfocar la ruta del elemento',
'Focus to contextual toolbar': 'Enfocar la barra de herramientas contextual',
'Insert link (if link plugin activated)': 'Insertar enlace (si el complemento de enlace est\u00e1 activado)',
'Save (if save plugin activated)': 'Guardar (si el componente de salvar est\u00e1 activado)',
'Find (if searchreplace plugin activated)': 'Buscar (si el complemento buscar-remplazar est\u00e1 activado)',
'Insert link (if link plugin activated)': 'Insertar enlace (si el complemento de enlace est\u00E1 activado)',
'Save (if save plugin activated)': 'Guardar (si el componente de salvar est\u00E1 activado)',
'Find (if searchreplace plugin activated)': 'Buscar (si el complemento buscar-remplazar est\u00E1 activado)',
'Plugins installed ({0}):': 'Plugins instalados ({0}):',
'Premium plugins:': 'Complementos premium:',
'Learn more...': 'Aprende m\u00e1s...',
'Learn more...': 'Aprende m\u00E1s...',
'You are using {0}': 'Estas usando {0}',
Plugins: 'Complementos',
'Handy Shortcuts': 'Accesos directos',
'Horizontal line': 'L\u00ednea horizontal',
'Insert\/edit image': 'Insertar\/editar imagen',
'Image description': 'Descripci\u00f3n de la imagen',
'Horizontal line': 'L\u00EDnea horizontal',
'Insert/edit image': 'Insertar/editar imagen',
'Image description': 'Descripci\u00F3n de la imagen',
Source: 'Enlace',
Dimensions: 'Dimensiones',
'Constrain proportions': 'Restringir proporciones',
@ -125,7 +125,7 @@ tinymce.addI18n('es', {
Border: 'Borde',
'Insert image': 'Insertar imagen',
'Image...': 'Imagen...',
'Image list': 'Lista de im\u00e1genes',
'Image list': 'Lista de im\u00E1genes',
'Rotate counterclockwise': 'Girar a la izquierda',
'Rotate clockwise': 'Girar a la derecha',
'Flip vertically': 'Invertir verticalmente',
@ -136,7 +136,7 @@ tinymce.addI18n('es', {
'Zoom out': 'Alejar',
Crop: 'Recortar',
Resize: 'Redimensionar',
Orientation: 'Orientaci\u00f3n',
Orientation: 'Orientaci\u00F3n',
Brightness: 'Brillo',
Sharpen: 'Forma',
Contrast: 'Contraste',
@ -144,11 +144,11 @@ tinymce.addI18n('es', {
Gamma: 'Gamma',
Invert: 'Invertir',
Apply: 'Aplicar',
Back: 'Atr\u00e1s',
'Insert date\/time': 'Insertar fecha\/hora',
'Date\/time': 'Fecha\/hora',
'Insert\/Edit Link': 'Insertar\/editar enlace',
'Insert\/edit link': 'Insertar\/editar enlace',
Back: 'Atr\u00E1s',
'Insert date/time': 'Insertar fecha/hora',
'Date/time': 'Fecha/hora',
'Insert/Edit Link': 'Insertar/editar enlace',
'Insert/edit link': 'Insertar/editar enlace',
'Text to display': 'Texto para mostrar',
Url: 'URL',
'Open link in...': 'Abrir enlace en...',
@ -159,20 +159,20 @@ tinymce.addI18n('es', {
Anchors: 'Anclas',
'Link...': 'Enlace...',
'Paste or type a link': 'Pega o introduce un enlace',
'The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?': 'El enlace que has introducido no parece ser una direcci\u00f3n de correo electr\u00f3nico. Quieres a\u00f1adir el prefijo necesario mailto: ?',
'The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?': 'El enlace que has introducido no parece ser una enlace externo. Quieres a\u00f1adir el prefijo necesario http:\/\/ ?',
'The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?': 'El enlace que has introducido no parece ser una direcci\u00F3n de correo electr\u00F3nico. Quieres a\u00F1adir el prefijo necesario mailto: ?',
'The URL you entered seems to be an external link. Do you want to add the required http:// prefix?': 'El enlace que has introducido no parece ser una enlace externo. Quieres a\u00F1adir el prefijo necesario http:// ?',
'Link list': 'Lista de enlaces',
'Insert video': 'Insertar video',
'Insert\/edit video': 'Insertar\/editar video',
'Insert\/edit media': 'Insertar\/editar medio',
'Insert/edit video': 'Insertar/editar video',
'Insert/edit media': 'Insertar/editar medio',
'Alternative source': 'Enlace alternativo',
'Alternative source URL': 'Origen de URL alternativo',
'Media poster (Image URL)': 'P\u00f3ster de medio (URL de imagen)',
'Paste your embed code below:': 'Pega tu c\u00f3digo embebido debajo',
'Media poster (Image URL)': 'P\u00F3ster de medio (URL de imagen)',
'Paste your embed code below:': 'Pega tu c\u00F3digo embebido debajo',
Embed: 'Incrustado',
'Media...': 'Medios...',
'Nonbreaking space': 'Espacio fijo',
'Page break': 'Salto de p\u00e1gina',
'Page break': 'Salto de p\u00E1gina',
'Paste as text': 'Pegar como texto',
Preview: 'Previsualizar',
'Print...': 'Imprimir...',
@ -187,11 +187,11 @@ tinymce.addI18n('es', {
'Could not find the specified string.': 'No se encuentra la cadena de texto especificada',
'Match case': 'Coincidencia exacta',
'Find whole words only': 'Solo palabras completas',
'Spell check': 'Revisar ortograf\u00eda',
'Spell check': 'Revisar ortograf\u00EDa',
Ignore: 'Ignorar',
'Ignore all': 'Ignorar todos',
Finish: 'Finalizar',
'Add to Dictionary': 'A\u00f1adir al Diccionario',
'Add to Dictionary': 'A\u00F1adir al Diccionario',
'Insert table': 'Insertar tabla',
'Table properties': 'Propiedades de la tabla',
'Delete table': 'Eliminar tabla',
@ -202,15 +202,15 @@ tinymce.addI18n('es', {
'Merge cells': 'Combinar celdas',
'Split cell': 'Dividir celdas',
'Insert row before': 'Insertar fila antes',
'Insert row after': 'Insertar fila despu\u00e9s ',
'Insert row after': 'Insertar fila despu\u00E9s ',
'Delete row': 'Eliminar fila',
'Row properties': 'Propiedades de la fila',
'Cut row': 'Cortar fila',
'Copy row': 'Copiar fila',
'Paste row before': 'Pegar la fila antes',
'Paste row after': 'Pegar la fila despu\u00e9s',
'Paste row after': 'Pegar la fila despu\u00E9s',
'Insert column before': 'Insertar columna antes',
'Insert column after': 'Insertar columna despu\u00e9s',
'Insert column after': 'Insertar columna despu\u00E9s',
'Delete column': 'Eliminar columna',
Cols: 'Columnas',
Rows: 'Filas',
@ -218,13 +218,13 @@ tinymce.addI18n('es', {
Height: 'Alto',
'Cell spacing': 'Espacio entre celdas',
'Cell padding': 'Relleno de celda',
'Show caption': 'Mostrar t\u00edtulo',
'Show caption': 'Mostrar t\u00EDtulo',
Left: 'Izquierda',
Center: 'Centrado',
Right: 'Derecha',
'Cell type': 'Tipo de celda',
Scope: '\u00c1mbito',
Alignment: 'Alineaci\u00f3n',
Scope: '\u00C1mbito',
Alignment: 'Alineaci\u00F3n',
'H Align': 'Alineamiento Horizontal',
'V Align': 'Alineamiento Vertical',
Top: 'Arriba',
@ -236,7 +236,7 @@ tinymce.addI18n('es', {
'Row type': 'Tipo de fila',
Header: 'Cabecera',
Body: 'Cuerpo',
Footer: 'Pie de p\u00e1gina',
Footer: 'Pie de p\u00E1gina',
'Border color': 'Color del borde',
'Insert template...': 'Insertar plantilla...',
Templates: 'Plantillas',
@ -253,7 +253,7 @@ tinymce.addI18n('es', {
'Word count': 'Contar palabras',
Count: 'Recuento',
Document: 'Documento',
Selection: 'Selecci\u00f3n',
Selection: 'Selecci\u00F3n',
Words: 'Palabras',
'Words: {0}': 'Palabras: {0}',
'{0} words': '{0} palabras',
@ -265,15 +265,15 @@ tinymce.addI18n('es', {
Table: 'Tabla',
Tools: 'Herramientas',
'Powered by {0}': 'Desarrollado por {0}',
'Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help': '\u00c1rea de texto enriquecido. Pulse ALT-F9 para el menu. Pulse ALT-F10 para la barra de herramientas. Pulse ALT-0 para ayuda',
'Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help': '\u00C1rea de texto enriquecido. Pulse ALT-F9 para el menu. Pulse ALT-F10 para la barra de herramientas. Pulse ALT-0 para ayuda',
'Image title': 'Titulo de imagen',
'Border width': 'Ancho de borde',
'Border style': 'Estilo de borde',
Error: 'Error',
Warn: 'Advertencia',
Valid: 'V\u00e1lido',
'To open the popup, press Shift+Enter': 'Para abrir el elemento emergente, pulse May\u00fas+Intro',
'Rich Text Area. Press ALT-0 for help.': '\u00c1rea de texto enriquecido. Pulse ALT-0 para abrir la ayuda.',
Valid: 'V\u00E1lido',
'To open the popup, press Shift+Enter': 'Para abrir el elemento emergente, pulse May\u00FAs+Intro',
'Rich Text Area. Press ALT-0 for help.': '\u00C1rea de texto enriquecido. Pulse ALT-0 para abrir la ayuda.',
'System Font': 'Fuente de sistema',
'Failed to upload image: {0}': 'Fallo al cargar imagen: {0}',
'Failed to load plugin: {0} from url {1}': 'Fallo al cargar complemento: {0} desde URL {1}',
@ -285,31 +285,31 @@ tinymce.addI18n('es', {
Currency: 'Divisa',
Text: 'Texto',
Quotations: 'Comillas',
Mathematical: 'S\u00edmbolo matem\u00e1tico',
Mathematical: 'S\u00EDmbolo matem\u00E1tico',
'Extended Latin': 'Latino extendido A',
Symbols: 'S\u00edmbolos',
Symbols: 'S\u00EDmbolos',
Arrows: 'Flechas',
'User Defined': 'Definido por el usuario',
'dollar sign': 'signo de d\u00f3lar',
'dollar sign': 'signo de d\u00F3lar',
'currency sign': 'signo de divisa',
'euro-currency sign': 'signo de euro',
'colon sign': 'signo de dos puntos',
'cruzeiro sign': 'signo de cruceiro',
'french franc sign': 'signo de franco franc\u00e9s',
'french franc sign': 'signo de franco franc\u00E9s',
'lira sign': 'signo de lira',
'mill sign': 'signo de mill',
'naira sign': 'signo de naira',
'peseta sign': 'signo de peseta',
'rupee sign': 'signo de rupia',
'won sign': 'signo de won',
'new sheqel sign': 'signo de nuevo s\u00e9quel',
'new sheqel sign': 'signo de nuevo s\u00E9quel',
'dong sign': 'signo de dong',
'kip sign': 'signo de kip',
'tugrik sign': 'signo de tugrik',
'drachma sign': 'signo de dracma',
'german penny symbol': 'signo de penique alem\u00e1n',
'german penny symbol': 'signo de penique alem\u00E1n',
'peso sign': 'signo de peso',
'guarani sign': 'signo de guaran\u00ed',
'guarani sign': 'signo de guaran\u00ED',
'austral sign': 'signo de austral',
'hryvnia sign': 'signo de grivna',
'cedi sign': 'signo de cedi',
@ -318,13 +318,13 @@ tinymce.addI18n('es', {
'tenge sign': 'signo de tenge',
'indian rupee sign': 'signo de rupia india',
'turkish lira sign': 'signo de lira turca',
'nordic mark sign': 'signo de marco n\u00f3rdico',
'nordic mark sign': 'signo de marco n\u00F3rdico',
'manat sign': 'signo de manat',
'ruble sign': 'signo de rublo',
'yen character': 'car\u00e1cter de yen',
'yuan character': 'car\u00e1cter de yuan',
'yuan character, in hong kong and taiwan': 'car\u00e1cter de yuan en Hong Kong y Taiw\u00e1n',
'yen\/yuan character variant one': 'Variante uno de car\u00e1cter de yen\/yuan',
'yen character': 'car\u00E1cter de yen',
'yuan character': 'car\u00E1cter de yuan',
'yuan character, in hong kong and taiwan': 'car\u00E1cter de yuan en Hong Kong y Taiw\u00E1n',
'yen/yuan character variant one': 'Variante uno de car\u00E1cter de yen/yuan',
'Loading emoticons...': 'Cargando emoticonos...',
'Could not load emoticons': 'No se han podido cargar los emoticonos',
People: 'Personas',
@ -337,19 +337,19 @@ tinymce.addI18n('es', {
Characters: 'Caracteres',
'Characters (no spaces)': 'Caracteres (sin espacios)',
'{0} characters': '{0} caracteres',
'Error: Form submit field collision.': 'Error: Colisi\u00f3n de campo al enviar formulario.',
'Error: No form element found.': 'Error: No se encuentra ning\u00fan elemento de formulario.',
'Error: Form submit field collision.': 'Error: Colisi\u00F3n de campo al enviar formulario.',
'Error: No form element found.': 'Error: No se encuentra ning\u00FAn elemento de formulario.',
Update: 'Actualizar',
'Color swatch': 'Muestrario de colores',
Turquoise: 'Turquesa',
Green: 'Verde',
Blue: 'Azul',
Purple: 'P\u00farpura',
Purple: 'P\u00FArpura',
'Navy Blue': 'Azul marino',
'Dark Turquoise': 'Turquesa oscuro',
'Dark Green': 'Verde oscuro',
'Medium Blue': 'Azul medio',
'Medium Purple': 'P\u00farpura medio',
'Medium Purple': 'P\u00FArpura medio',
'Midnight Blue': 'Azul medio',
Yellow: 'Amarillo',
Orange: 'Naranja',
@ -371,35 +371,35 @@ tinymce.addI18n('es', {
Black: 'Negro',
White: 'Blanco',
'Switch to or from fullscreen mode': 'Activar o desactivar modo pantalla completa',
'Open help dialog': 'Abrir di\u00e1logo de ayuda',
'Open help dialog': 'Abrir di\u00E1logo de ayuda',
history: 'historial',
styles: 'estilos',
formatting: 'formato',
alignment: 'alineaci\u00f3n',
indentation: 'sangr\u00eda',
'permanent pen': 'bol\u00edgrafo permanente',
alignment: 'alineaci\u00F3n',
indentation: 'sangr\u00EDa',
'permanent pen': 'bol\u00EDgrafo permanente',
comments: 'comentarios',
'Format Painter': 'Copiar formato',
'Insert\/edit iframe': 'Insertar\/editar iframe',
Capitalization: 'Uso de may\u00fasculas',
lowercase: 'min\u00fasculas',
UPPERCASE: 'MAY\u00daSCULAS',
'Title Case': 'Tipo T\u00edtulo',
'Permanent Pen Properties': 'Propiedades del bol\u00edgrafo permanente',
'Permanent pen properties...': 'Propiedades del bol\u00edgrafo permanente...',
'Insert/edit iframe': 'Insertar/editar iframe',
Capitalization: 'Uso de may\u00FAsculas',
lowercase: 'min\u00FAsculas',
UPPERCASE: 'MAY\u00DASCULAS',
'Title Case': 'Tipo T\u00EDtulo',
'Permanent Pen Properties': 'Propiedades del bol\u00EDgrafo permanente',
'Permanent pen properties...': 'Propiedades del bol\u00EDgrafo permanente...',
Font: 'Fuente',
Size: 'Tama\u00f1o',
'More...': 'M\u00e1s...',
Size: 'Tama\u00F1o',
'More...': 'M\u00E1s...',
'Spellcheck Language': 'Corrector',
'Select...': 'Seleccionar...',
Preferences: 'Preferencias',
Yes: 'S\u00ed',
Yes: 'S\u00ED',
No: 'No',
'Keyboard Navigation': 'Navegaci\u00f3n con el teclado',
Version: 'Versi\u00f3n',
'Keyboard Navigation': 'Navegaci\u00F3n con el teclado',
Version: 'Versi\u00F3n',
Anchor: 'Ancla',
'Special character': 'Car\u00e1cter especial',
'Code sample': 'Ejemplo de c\u00f3digo',
'Special character': 'Car\u00E1cter especial',
'Code sample': 'Ejemplo de c\u00F3digo',
Color: 'Color',
Emoticons: 'Emoticonos',
'Document properties': 'Propiedades del documento',
@ -413,7 +413,7 @@ tinymce.addI18n('es', {
Prev: 'Anterior',
'Find and replace': 'Buscar y reemplazar',
'Whole words': 'Palabras completas',
Spellcheck: 'Corrector ortogr\u00e1fico',
Caption: 'Subt\u00edtulo',
Spellcheck: 'Corrector ortogr\u00E1fico',
Caption: 'Subt\u00EDtulo',
'Insert template': 'Insertar plantilla'
})

700
apps/vben5/apps/app-antd/public/resource/tinymce/langs/zh-CN.js

@ -1,32 +1,32 @@
tinymce.addI18n('zh_CN',{
"Redo": "\u91cd\u505a",
"Undo": "\u64a4\u9500",
"Cut": "\u526a\u5207",
"Copy": "\u590d\u5236",
"Paste": "\u7c98\u8d34",
"Redo": "\u91CD\u505A",
"Undo": "\u64A4\u9500",
"Cut": "\u526A\u5207",
"Copy": "\u590D\u5236",
"Paste": "\u7C98\u8D34",
"Select all": "\u5168\u9009",
"New document": "\u65b0\u6587\u4ef6",
"Ok": "\u786e\u5b9a",
"Cancel": "\u53d6\u6d88",
"Visual aids": "\u7f51\u683c\u7ebf",
"Bold": "\u7c97\u4f53",
"Italic": "\u659c\u4f53",
"Underline": "\u4e0b\u5212\u7ebf",
"Strikethrough": "\u5220\u9664\u7ebf",
"Superscript": "\u4e0a\u6807",
"Subscript": "\u4e0b\u6807",
"Clear formatting": "\u6e05\u9664\u683c\u5f0f",
"Align left": "\u5de6\u8fb9\u5bf9\u9f50",
"Align center": "\u4e2d\u95f4\u5bf9\u9f50",
"Align right": "\u53f3\u8fb9\u5bf9\u9f50",
"Justify": "\u4e24\u7aef\u5bf9\u9f50",
"Bullet list": "\u9879\u76ee\u7b26\u53f7",
"Numbered list": "\u7f16\u53f7\u5217\u8868",
"Decrease indent": "\u51cf\u5c11\u7f29\u8fdb",
"Increase indent": "\u589e\u52a0\u7f29\u8fdb",
"Close": "\u5173\u95ed",
"Formats": "\u683c\u5f0f",
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "\u4f60\u7684\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u6253\u5f00\u526a\u8d34\u677f\uff0c\u8bf7\u4f7f\u7528Ctrl+X\/C\/V\u7b49\u5feb\u6377\u952e\u3002",
"New document": "\u65B0\u6587\u4EF6",
"Ok": "\u786E\u5B9A",
"Cancel": "\u53D6\u6D88",
"Visual aids": "\u7F51\u683C\u7EBF",
"Bold": "\u7C97\u4F53",
"Italic": "\u659C\u4F53",
"Underline": "\u4E0B\u5212\u7EBF",
"Strikethrough": "\u5220\u9664\u7EBF",
"Superscript": "\u4E0A\u6807",
"Subscript": "\u4E0B\u6807",
"Clear formatting": "\u6E05\u9664\u683C\u5F0F",
"Align left": "\u5DE6\u8FB9\u5BF9\u9F50",
"Align center": "\u4E2D\u95F4\u5BF9\u9F50",
"Align right": "\u53F3\u8FB9\u5BF9\u9F50",
"Justify": "\u4E24\u7AEF\u5BF9\u9F50",
"Bullet list": "\u9879\u76EE\u7B26\u53F7",
"Numbered list": "\u7F16\u53F7\u5217\u8868",
"Decrease indent": "\u51CF\u5C11\u7F29\u8FDB",
"Increase indent": "\u589E\u52A0\u7F29\u8FDB",
"Close": "\u5173\u95ED",
"Formats": "\u683C\u5F0F",
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X/C/V keyboard shortcuts instead.": "\u4F60\u7684\u6D4F\u89C8\u5668\u4E0D\u652F\u6301\u6253\u5F00\u526A\u8D34\u677F\uFF0C\u8BF7\u4F7F\u7528Ctrl+X/C/V\u7B49\u5FEB\u6377\u952E\u3002",
"Headers": "\u6807\u9898",
"Header 1": "\u6807\u98981",
"Header 2": "\u6807\u98982",
@ -41,349 +41,349 @@ tinymce.addI18n('zh_CN',{
"Heading 4": "\u6807\u98984",
"Heading 5": "\u6807\u98985",
"Heading 6": "\u6807\u98986",
"Preformatted": "\u9884\u5148\u683c\u5f0f\u5316\u7684",
"Preformatted": "\u9884\u5148\u683C\u5F0F\u5316\u7684",
"Div": "Div",
"Pre": "Pre",
"Code": "\u4ee3\u7801",
"Paragraph": "\u6bb5\u843d",
"Blockquote": "\u5f15\u6587\u533a\u5757",
"Inline": "\u6587\u672c",
"Blocks": "\u57fa\u5757",
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\u5f53\u524d\u4e3a\u7eaf\u6587\u672c\u7c98\u8d34\u6a21\u5f0f\uff0c\u518d\u6b21\u70b9\u51fb\u53ef\u4ee5\u56de\u5230\u666e\u901a\u7c98\u8d34\u6a21\u5f0f\u3002",
"Fonts": "\u5b57\u4f53",
"Font Sizes": "\u5b57\u53f7",
"Class": "\u7c7b\u578b",
"Browse for an image": "\u6d4f\u89c8\u56fe\u50cf",
"Code": "\u4EE3\u7801",
"Paragraph": "\u6BB5\u843D",
"Blockquote": "\u5F15\u6587\u533A\u5757",
"Inline": "\u6587\u672C",
"Blocks": "\u57FA\u5757",
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\u5F53\u524D\u4E3A\u7EAF\u6587\u672C\u7C98\u8D34\u6A21\u5F0F\uFF0C\u518D\u6B21\u70B9\u51FB\u53EF\u4EE5\u56DE\u5230\u666E\u901A\u7C98\u8D34\u6A21\u5F0F\u3002",
"Fonts": "\u5B57\u4F53",
"Font Sizes": "\u5B57\u53F7",
"Class": "\u7C7B\u578B",
"Browse for an image": "\u6D4F\u89C8\u56FE\u50CF",
"OR": "\u6216",
"Drop an image here": "\u62d6\u653e\u4e00\u5f20\u56fe\u50cf\u81f3\u6b64",
"Upload": "\u4e0a\u4f20",
"Drop an image here": "\u62D6\u653E\u4E00\u5F20\u56FE\u50CF\u81F3\u6B64",
"Upload": "\u4E0A\u4F20",
"Block": "\u5757",
"Align": "\u5bf9\u9f50",
"Default": "\u9ed8\u8ba4",
"Circle": "\u7a7a\u5fc3\u5706",
"Disc": "\u5b9e\u5fc3\u5706",
"Square": "\u65b9\u5757",
"Lower Alpha": "\u5c0f\u5199\u82f1\u6587\u5b57\u6bcd",
"Lower Greek": "\u5c0f\u5199\u5e0c\u814a\u5b57\u6bcd",
"Lower Roman": "\u5c0f\u5199\u7f57\u9a6c\u5b57\u6bcd",
"Upper Alpha": "\u5927\u5199\u82f1\u6587\u5b57\u6bcd",
"Upper Roman": "\u5927\u5199\u7f57\u9a6c\u5b57\u6bcd",
"Anchor...": "\u951a\u70b9...",
"Name": "\u540d\u79f0",
"Id": "\u6807\u8bc6\u7b26",
"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "\u6807\u8bc6\u7b26\u5e94\u8be5\u4ee5\u5b57\u6bcd\u5f00\u5934\uff0c\u540e\u8ddf\u5b57\u6bcd\u3001\u6570\u5b57\u3001\u7834\u6298\u53f7\u3001\u70b9\u3001\u5192\u53f7\u6216\u4e0b\u5212\u7ebf\u3002",
"You have unsaved changes are you sure you want to navigate away?": "\u4f60\u8fd8\u6709\u6587\u6863\u5c1a\u672a\u4fdd\u5b58\uff0c\u786e\u5b9a\u8981\u79bb\u5f00\uff1f",
"Restore last draft": "\u6062\u590d\u4e0a\u6b21\u7684\u8349\u7a3f",
"Special characters...": "\u7279\u6b8a\u5b57\u7b26...",
"Source code": "\u6e90\u4ee3\u7801",
"Insert\/Edit code sample": "\u63d2\u5165\/\u7f16\u8f91\u4ee3\u7801\u793a\u4f8b",
"Language": "\u8bed\u8a00",
"Code sample...": "\u793a\u4f8b\u4ee3\u7801...",
"Align": "\u5BF9\u9F50",
"Default": "\u9ED8\u8BA4",
"Circle": "\u7A7A\u5FC3\u5706",
"Disc": "\u5B9E\u5FC3\u5706",
"Square": "\u65B9\u5757",
"Lower Alpha": "\u5C0F\u5199\u82F1\u6587\u5B57\u6BCD",
"Lower Greek": "\u5C0F\u5199\u5E0C\u814A\u5B57\u6BCD",
"Lower Roman": "\u5C0F\u5199\u7F57\u9A6C\u5B57\u6BCD",
"Upper Alpha": "\u5927\u5199\u82F1\u6587\u5B57\u6BCD",
"Upper Roman": "\u5927\u5199\u7F57\u9A6C\u5B57\u6BCD",
"Anchor...": "\u951A\u70B9...",
"Name": "\u540D\u79F0",
"Id": "\u6807\u8BC6\u7B26",
"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "\u6807\u8BC6\u7B26\u5E94\u8BE5\u4EE5\u5B57\u6BCD\u5F00\u5934\uFF0C\u540E\u8DDF\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u7834\u6298\u53F7\u3001\u70B9\u3001\u5192\u53F7\u6216\u4E0B\u5212\u7EBF\u3002",
"You have unsaved changes are you sure you want to navigate away?": "\u4F60\u8FD8\u6709\u6587\u6863\u5C1A\u672A\u4FDD\u5B58\uFF0C\u786E\u5B9A\u8981\u79BB\u5F00\uFF1F",
"Restore last draft": "\u6062\u590D\u4E0A\u6B21\u7684\u8349\u7A3F",
"Special characters...": "\u7279\u6B8A\u5B57\u7B26...",
"Source code": "\u6E90\u4EE3\u7801",
"Insert/Edit code sample": "\u63D2\u5165/\u7F16\u8F91\u4EE3\u7801\u793A\u4F8B",
"Language": "\u8BED\u8A00",
"Code sample...": "\u793A\u4F8B\u4EE3\u7801...",
"Color Picker": "\u9009\u8272\u5668",
"R": "R",
"G": "G",
"B": "B",
"Left to right": "\u4ece\u5de6\u5230\u53f3",
"Right to left": "\u4ece\u53f3\u5230\u5de6",
"Emoticons...": "\u8868\u60c5\u7b26\u53f7...",
"Metadata and Document Properties": "\u5143\u6570\u636e\u548c\u6587\u6863\u5c5e\u6027",
"Left to right": "\u4ECE\u5DE6\u5230\u53F3",
"Right to left": "\u4ECE\u53F3\u5230\u5DE6",
"Emoticons...": "\u8868\u60C5\u7B26\u53F7...",
"Metadata and Document Properties": "\u5143\u6570\u636E\u548C\u6587\u6863\u5C5E\u6027",
"Title": "\u6807\u9898",
"Keywords": "\u5173\u952e\u8bcd",
"Description": "\u63cf\u8ff0",
"Robots": "\u673a\u5668\u4eba",
"Author": "\u4f5c\u8005",
"Encoding": "\u7f16\u7801",
"Fullscreen": "\u5168\u5c4f",
"Action": "\u64cd\u4f5c",
"Shortcut": "\u5feb\u6377\u952e",
"Help": "\u5e2e\u52a9",
"Keywords": "\u5173\u952E\u8BCD",
"Description": "\u63CF\u8FF0",
"Robots": "\u673A\u5668\u4EBA",
"Author": "\u4F5C\u8005",
"Encoding": "\u7F16\u7801",
"Fullscreen": "\u5168\u5C4F",
"Action": "\u64CD\u4F5C",
"Shortcut": "\u5FEB\u6377\u952E",
"Help": "\u5E2E\u52A9",
"Address": "\u5730\u5740",
"Focus to menubar": "\u79fb\u52a8\u7126\u70b9\u5230\u83dc\u5355\u680f",
"Focus to toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u5de5\u5177\u680f",
"Focus to element path": "\u79fb\u52a8\u7126\u70b9\u5230\u5143\u7d20\u8def\u5f84",
"Focus to contextual toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u4e0a\u4e0b\u6587\u83dc\u5355",
"Insert link (if link plugin activated)": "\u63d2\u5165\u94fe\u63a5 (\u5982\u679c\u94fe\u63a5\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
"Save (if save plugin activated)": "\u4fdd\u5b58(\u5982\u679c\u4fdd\u5b58\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
"Find (if searchreplace plugin activated)": "\u67e5\u627e(\u5982\u679c\u67e5\u627e\u66ff\u6362\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
"Plugins installed ({0}):": "\u5df2\u5b89\u88c5\u63d2\u4ef6 ({0}):",
"Premium plugins:": "\u4f18\u79c0\u63d2\u4ef6\uff1a",
"Learn more...": "\u4e86\u89e3\u66f4\u591a...",
"You are using {0}": "\u4f60\u6b63\u5728\u4f7f\u7528 {0}",
"Plugins": "\u63d2\u4ef6",
"Handy Shortcuts": "\u5feb\u6377\u952e",
"Horizontal line": "\u6c34\u5e73\u5206\u5272\u7ebf",
"Insert\/edit image": "\u63d2\u5165\/\u7f16\u8f91\u56fe\u7247",
"Image description": "\u56fe\u7247\u63cf\u8ff0",
"Focus to menubar": "\u79FB\u52A8\u7126\u70B9\u5230\u83DC\u5355\u680F",
"Focus to toolbar": "\u79FB\u52A8\u7126\u70B9\u5230\u5DE5\u5177\u680F",
"Focus to element path": "\u79FB\u52A8\u7126\u70B9\u5230\u5143\u7D20\u8DEF\u5F84",
"Focus to contextual toolbar": "\u79FB\u52A8\u7126\u70B9\u5230\u4E0A\u4E0B\u6587\u83DC\u5355",
"Insert link (if link plugin activated)": "\u63D2\u5165\u94FE\u63A5 (\u5982\u679C\u94FE\u63A5\u63D2\u4EF6\u5DF2\u6FC0\u6D3B)",
"Save (if save plugin activated)": "\u4FDD\u5B58(\u5982\u679C\u4FDD\u5B58\u63D2\u4EF6\u5DF2\u6FC0\u6D3B)",
"Find (if searchreplace plugin activated)": "\u67E5\u627E(\u5982\u679C\u67E5\u627E\u66FF\u6362\u63D2\u4EF6\u5DF2\u6FC0\u6D3B)",
"Plugins installed ({0}):": "\u5DF2\u5B89\u88C5\u63D2\u4EF6 ({0}):",
"Premium plugins:": "\u4F18\u79C0\u63D2\u4EF6\uFF1A",
"Learn more...": "\u4E86\u89E3\u66F4\u591A...",
"You are using {0}": "\u4F60\u6B63\u5728\u4F7F\u7528 {0}",
"Plugins": "\u63D2\u4EF6",
"Handy Shortcuts": "\u5FEB\u6377\u952E",
"Horizontal line": "\u6C34\u5E73\u5206\u5272\u7EBF",
"Insert/edit image": "\u63D2\u5165/\u7F16\u8F91\u56FE\u7247",
"Image description": "\u56FE\u7247\u63CF\u8FF0",
"Source": "\u5730\u5740",
"Dimensions": "\u5927\u5c0f",
"Constrain proportions": "\u4fdd\u6301\u7eb5\u6a2a\u6bd4",
"General": "\u666e\u901a",
"Advanced": "\u9ad8\u7ea7",
"Style": "\u6837\u5f0f",
"Vertical space": "\u5782\u76f4\u8fb9\u8ddd",
"Horizontal space": "\u6c34\u5e73\u8fb9\u8ddd",
"Border": "\u8fb9\u6846",
"Insert image": "\u63d2\u5165\u56fe\u7247",
"Image...": "\u56fe\u7247...",
"Image list": "\u56fe\u7247\u5217\u8868",
"Rotate counterclockwise": "\u9006\u65f6\u9488\u65cb\u8f6c",
"Rotate clockwise": "\u987a\u65f6\u9488\u65cb\u8f6c",
"Flip vertically": "\u5782\u76f4\u7ffb\u8f6c",
"Flip horizontally": "\u6c34\u5e73\u7ffb\u8f6c",
"Edit image": "\u7f16\u8f91\u56fe\u7247",
"Image options": "\u56fe\u7247\u9009\u9879",
"Zoom in": "\u653e\u5927",
"Zoom out": "\u7f29\u5c0f",
"Crop": "\u88c1\u526a",
"Resize": "\u8c03\u6574\u5927\u5c0f",
"Orientation": "\u65b9\u5411",
"Brightness": "\u4eae\u5ea6",
"Dimensions": "\u5927\u5C0F",
"Constrain proportions": "\u4FDD\u6301\u7EB5\u6A2A\u6BD4",
"General": "\u666E\u901A",
"Advanced": "\u9AD8\u7EA7",
"Style": "\u6837\u5F0F",
"Vertical space": "\u5782\u76F4\u8FB9\u8DDD",
"Horizontal space": "\u6C34\u5E73\u8FB9\u8DDD",
"Border": "\u8FB9\u6846",
"Insert image": "\u63D2\u5165\u56FE\u7247",
"Image...": "\u56FE\u7247...",
"Image list": "\u56FE\u7247\u5217\u8868",
"Rotate counterclockwise": "\u9006\u65F6\u9488\u65CB\u8F6C",
"Rotate clockwise": "\u987A\u65F6\u9488\u65CB\u8F6C",
"Flip vertically": "\u5782\u76F4\u7FFB\u8F6C",
"Flip horizontally": "\u6C34\u5E73\u7FFB\u8F6C",
"Edit image": "\u7F16\u8F91\u56FE\u7247",
"Image options": "\u56FE\u7247\u9009\u9879",
"Zoom in": "\u653E\u5927",
"Zoom out": "\u7F29\u5C0F",
"Crop": "\u88C1\u526A",
"Resize": "\u8C03\u6574\u5927\u5C0F",
"Orientation": "\u65B9\u5411",
"Brightness": "\u4EAE\u5EA6",
"Sharpen": "\u9510\u5316",
"Contrast": "\u5bf9\u6bd4\u5ea6",
"Color levels": "\u989c\u8272\u5c42\u6b21",
"Gamma": "\u4f3d\u9a6c\u503c",
"Invert": "\u53cd\u8f6c",
"Apply": "\u5e94\u7528",
"Back": "\u540e\u9000",
"Insert date\/time": "\u63d2\u5165\u65e5\u671f\/\u65f6\u95f4",
"Date\/time": "\u65e5\u671f\/\u65f6\u95f4",
"Insert\/Edit Link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5",
"Insert\/edit link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5",
"Text to display": "\u663e\u793a\u6587\u5b57",
"Contrast": "\u5BF9\u6BD4\u5EA6",
"Color levels": "\u989C\u8272\u5C42\u6B21",
"Gamma": "\u4F3D\u9A6C\u503C",
"Invert": "\u53CD\u8F6C",
"Apply": "\u5E94\u7528",
"Back": "\u540E\u9000",
"Insert date/time": "\u63D2\u5165\u65E5\u671F/\u65F6\u95F4",
"Date/time": "\u65E5\u671F/\u65F6\u95F4",
"Insert/Edit Link": "\u63D2\u5165/\u7F16\u8F91\u94FE\u63A5",
"Insert/edit link": "\u63D2\u5165/\u7F16\u8F91\u94FE\u63A5",
"Text to display": "\u663E\u793A\u6587\u5B57",
"Url": "\u5730\u5740",
"Open link in...": "\u94fe\u63a5\u6253\u5f00\u4f4d\u7f6e...",
"Current window": "\u5f53\u524d\u7a97\u53e3",
"None": "\u65e0",
"New window": "\u5728\u65b0\u7a97\u53e3\u6253\u5f00",
"Remove link": "\u5220\u9664\u94fe\u63a5",
"Anchors": "\u951a\u70b9",
"Link...": "\u94fe\u63a5...",
"Paste or type a link": "\u7c98\u8d34\u6216\u8f93\u5165\u94fe\u63a5",
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u4e3a\u90ae\u4ef6\u5730\u5740\uff0c\u9700\u8981\u52a0\u4e0amailto:\u524d\u7f00\u5417\uff1f",
"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u5c5e\u4e8e\u5916\u90e8\u94fe\u63a5\uff0c\u9700\u8981\u52a0\u4e0ahttp:\/\/:\u524d\u7f00\u5417\uff1f",
"Link list": "\u94fe\u63a5\u5217\u8868",
"Insert video": "\u63d2\u5165\u89c6\u9891",
"Insert\/edit video": "\u63d2\u5165\/\u7f16\u8f91\u89c6\u9891",
"Insert\/edit media": "\u63d2\u5165\/\u7f16\u8f91\u5a92\u4f53",
"Alternative source": "\u955c\u50cf",
"Alternative source URL": "\u66ff\u4ee3\u6765\u6e90\u7f51\u5740",
"Media poster (Image URL)": "\u5c01\u9762(\u56fe\u7247\u5730\u5740)",
"Paste your embed code below:": "\u5c06\u5185\u5d4c\u4ee3\u7801\u7c98\u8d34\u5728\u4e0b\u9762:",
"Embed": "\u5185\u5d4c",
"Media...": "\u591a\u5a92\u4f53...",
"Nonbreaking space": "\u4e0d\u95f4\u65ad\u7a7a\u683c",
"Page break": "\u5206\u9875\u7b26",
"Paste as text": "\u7c98\u8d34\u4e3a\u6587\u672c",
"Preview": "\u9884\u89c8",
"Open link in...": "\u94FE\u63A5\u6253\u5F00\u4F4D\u7F6E...",
"Current window": "\u5F53\u524D\u7A97\u53E3",
"None": "\u65E0",
"New window": "\u5728\u65B0\u7A97\u53E3\u6253\u5F00",
"Remove link": "\u5220\u9664\u94FE\u63A5",
"Anchors": "\u951A\u70B9",
"Link...": "\u94FE\u63A5...",
"Paste or type a link": "\u7C98\u8D34\u6216\u8F93\u5165\u94FE\u63A5",
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\u4F60\u6240\u586B\u5199\u7684URL\u5730\u5740\u4E3A\u90AE\u4EF6\u5730\u5740\uFF0C\u9700\u8981\u52A0\u4E0Amailto:\u524D\u7F00\u5417\uFF1F",
"The URL you entered seems to be an external link. Do you want to add the required http:// prefix?": "\u4F60\u6240\u586B\u5199\u7684URL\u5730\u5740\u5C5E\u4E8E\u5916\u90E8\u94FE\u63A5\uFF0C\u9700\u8981\u52A0\u4E0Ahttp://:\u524D\u7F00\u5417\uFF1F",
"Link list": "\u94FE\u63A5\u5217\u8868",
"Insert video": "\u63D2\u5165\u89C6\u9891",
"Insert/edit video": "\u63D2\u5165/\u7F16\u8F91\u89C6\u9891",
"Insert/edit media": "\u63D2\u5165/\u7F16\u8F91\u5A92\u4F53",
"Alternative source": "\u955C\u50CF",
"Alternative source URL": "\u66FF\u4EE3\u6765\u6E90\u7F51\u5740",
"Media poster (Image URL)": "\u5C01\u9762(\u56FE\u7247\u5730\u5740)",
"Paste your embed code below:": "\u5C06\u5185\u5D4C\u4EE3\u7801\u7C98\u8D34\u5728\u4E0B\u9762:",
"Embed": "\u5185\u5D4C",
"Media...": "\u591A\u5A92\u4F53...",
"Nonbreaking space": "\u4E0D\u95F4\u65AD\u7A7A\u683C",
"Page break": "\u5206\u9875\u7B26",
"Paste as text": "\u7C98\u8D34\u4E3A\u6587\u672C",
"Preview": "\u9884\u89C8",
"Print...": "\u6253\u5370...",
"Save": "\u4fdd\u5b58",
"Find": "\u67e5\u627e",
"Replace with": "\u66ff\u6362\u4e3a",
"Replace": "\u66ff\u6362",
"Replace all": "\u5168\u90e8\u66ff\u6362",
"Previous": "\u4e0a\u4e00\u4e2a",
"Next": "\u4e0b\u4e00\u4e2a",
"Find and replace...": "\u67e5\u627e\u5e76\u66ff\u6362...",
"Could not find the specified string.": "\u672a\u627e\u5230\u641c\u7d22\u5185\u5bb9.",
"Match case": "\u533a\u5206\u5927\u5c0f\u5199",
"Find whole words only": "\u5168\u5b57\u5339\u914d",
"Spell check": "\u62fc\u5199\u68c0\u67e5",
"Ignore": "\u5ffd\u7565",
"Ignore all": "\u5168\u90e8\u5ffd\u7565",
"Finish": "\u5b8c\u6210",
"Add to Dictionary": "\u6dfb\u52a0\u5230\u5b57\u5178",
"Insert table": "\u63d2\u5165\u8868\u683c",
"Table properties": "\u8868\u683c\u5c5e\u6027",
"Delete table": "\u5220\u9664\u8868\u683c",
"Cell": "\u5355\u5143\u683c",
"Row": "\u884c",
"Save": "\u4FDD\u5B58",
"Find": "\u67E5\u627E",
"Replace with": "\u66FF\u6362\u4E3A",
"Replace": "\u66FF\u6362",
"Replace all": "\u5168\u90E8\u66FF\u6362",
"Previous": "\u4E0A\u4E00\u4E2A",
"Next": "\u4E0B\u4E00\u4E2A",
"Find and replace...": "\u67E5\u627E\u5E76\u66FF\u6362...",
"Could not find the specified string.": "\u672A\u627E\u5230\u641C\u7D22\u5185\u5BB9.",
"Match case": "\u533A\u5206\u5927\u5C0F\u5199",
"Find whole words only": "\u5168\u5B57\u5339\u914D",
"Spell check": "\u62FC\u5199\u68C0\u67E5",
"Ignore": "\u5FFD\u7565",
"Ignore all": "\u5168\u90E8\u5FFD\u7565",
"Finish": "\u5B8C\u6210",
"Add to Dictionary": "\u6DFB\u52A0\u5230\u5B57\u5178",
"Insert table": "\u63D2\u5165\u8868\u683C",
"Table properties": "\u8868\u683C\u5C5E\u6027",
"Delete table": "\u5220\u9664\u8868\u683C",
"Cell": "\u5355\u5143\u683C",
"Row": "\u884C",
"Column": "\u5217",
"Cell properties": "\u5355\u5143\u683c\u5c5e\u6027",
"Merge cells": "\u5408\u5e76\u5355\u5143\u683c",
"Split cell": "\u62c6\u5206\u5355\u5143\u683c",
"Insert row before": "\u5728\u4e0a\u65b9\u63d2\u5165",
"Insert row after": "\u5728\u4e0b\u65b9\u63d2\u5165",
"Delete row": "\u5220\u9664\u884c",
"Row properties": "\u884c\u5c5e\u6027",
"Cut row": "\u526a\u5207\u884c",
"Copy row": "\u590d\u5236\u884c",
"Paste row before": "\u7c98\u8d34\u5230\u4e0a\u65b9",
"Paste row after": "\u7c98\u8d34\u5230\u4e0b\u65b9",
"Insert column before": "\u5728\u5de6\u4fa7\u63d2\u5165",
"Insert column after": "\u5728\u53f3\u4fa7\u63d2\u5165",
"Cell properties": "\u5355\u5143\u683C\u5C5E\u6027",
"Merge cells": "\u5408\u5E76\u5355\u5143\u683C",
"Split cell": "\u62C6\u5206\u5355\u5143\u683C",
"Insert row before": "\u5728\u4E0A\u65B9\u63D2\u5165",
"Insert row after": "\u5728\u4E0B\u65B9\u63D2\u5165",
"Delete row": "\u5220\u9664\u884C",
"Row properties": "\u884C\u5C5E\u6027",
"Cut row": "\u526A\u5207\u884C",
"Copy row": "\u590D\u5236\u884C",
"Paste row before": "\u7C98\u8D34\u5230\u4E0A\u65B9",
"Paste row after": "\u7C98\u8D34\u5230\u4E0B\u65B9",
"Insert column before": "\u5728\u5DE6\u4FA7\u63D2\u5165",
"Insert column after": "\u5728\u53F3\u4FA7\u63D2\u5165",
"Delete column": "\u5220\u9664\u5217",
"Cols": "\u5217",
"Rows": "\u884c",
"Width": "\u5bbd",
"Height": "\u9ad8",
"Cell spacing": "\u5355\u5143\u683c\u5916\u95f4\u8ddd",
"Cell padding": "\u5355\u5143\u683c\u5185\u8fb9\u8ddd",
"Show caption": "\u663e\u793a\u6807\u9898",
"Left": "\u5de6\u5bf9\u9f50",
"Center": "\u5c45\u4e2d",
"Right": "\u53f3\u5bf9\u9f50",
"Cell type": "\u5355\u5143\u683c\u7c7b\u578b",
"Scope": "\u8303\u56f4",
"Alignment": "\u5bf9\u9f50\u65b9\u5f0f",
"H Align": "\u6c34\u5e73\u5bf9\u9f50",
"V Align": "\u5782\u76f4\u5bf9\u9f50",
"Top": "\u9876\u90e8\u5bf9\u9f50",
"Middle": "\u5782\u76f4\u5c45\u4e2d",
"Bottom": "\u5e95\u90e8\u5bf9\u9f50",
"Header cell": "\u8868\u5934\u5355\u5143\u683c",
"Row group": "\u884c\u7ec4",
"Column group": "\u5217\u7ec4",
"Row type": "\u884c\u7c7b\u578b",
"Rows": "\u884C",
"Width": "\u5BBD",
"Height": "\u9AD8",
"Cell spacing": "\u5355\u5143\u683C\u5916\u95F4\u8DDD",
"Cell padding": "\u5355\u5143\u683C\u5185\u8FB9\u8DDD",
"Show caption": "\u663E\u793A\u6807\u9898",
"Left": "\u5DE6\u5BF9\u9F50",
"Center": "\u5C45\u4E2D",
"Right": "\u53F3\u5BF9\u9F50",
"Cell type": "\u5355\u5143\u683C\u7C7B\u578B",
"Scope": "\u8303\u56F4",
"Alignment": "\u5BF9\u9F50\u65B9\u5F0F",
"H Align": "\u6C34\u5E73\u5BF9\u9F50",
"V Align": "\u5782\u76F4\u5BF9\u9F50",
"Top": "\u9876\u90E8\u5BF9\u9F50",
"Middle": "\u5782\u76F4\u5C45\u4E2D",
"Bottom": "\u5E95\u90E8\u5BF9\u9F50",
"Header cell": "\u8868\u5934\u5355\u5143\u683C",
"Row group": "\u884C\u7EC4",
"Column group": "\u5217\u7EC4",
"Row type": "\u884C\u7C7B\u578B",
"Header": "\u8868\u5934",
"Body": "\u8868\u4f53",
"Footer": "\u8868\u5c3e",
"Border color": "\u8fb9\u6846\u989c\u8272",
"Insert template...": "\u63d2\u5165\u6a21\u677f...",
"Templates": "\u6a21\u677f",
"Template": "\u6a21\u677f",
"Text color": "\u6587\u5b57\u989c\u8272",
"Background color": "\u80cc\u666f\u8272",
"Custom...": "\u81ea\u5b9a\u4e49...",
"Custom color": "\u81ea\u5b9a\u4e49\u989c\u8272",
"No color": "\u65e0",
"Remove color": "\u79fb\u9664\u989c\u8272",
"Table of Contents": "\u5185\u5bb9\u5217\u8868",
"Show blocks": "\u663e\u793a\u533a\u5757\u8fb9\u6846",
"Show invisible characters": "\u663e\u793a\u4e0d\u53ef\u89c1\u5b57\u7b26",
"Word count": "\u5b57\u6570",
"Words: {0}": "\u5b57\u6570\uff1a{0}",
"{0} words": "{0} \u5b57",
"File": "\u6587\u4ef6",
"Edit": "\u7f16\u8f91",
"Insert": "\u63d2\u5165",
"View": "\u89c6\u56fe",
"Format": "\u683c\u5f0f",
"Table": "\u8868\u683c",
"Tools": "\u5de5\u5177",
"Powered by {0}": "\u7531{0}\u9a71\u52a8",
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u5728\u7f16\u8f91\u533a\u6309ALT-F9\u6253\u5f00\u83dc\u5355\uff0c\u6309ALT-F10\u6253\u5f00\u5de5\u5177\u680f\uff0c\u6309ALT-0\u67e5\u770b\u5e2e\u52a9",
"Image title": "\u56fe\u7247\u6807\u9898",
"Border width": "\u8fb9\u6846\u5bbd\u5ea6",
"Border style": "\u8fb9\u6846\u6837\u5f0f",
"Error": "\u9519\u8bef",
"Warn": "\u8b66\u544a",
"Body": "\u8868\u4F53",
"Footer": "\u8868\u5C3E",
"Border color": "\u8FB9\u6846\u989C\u8272",
"Insert template...": "\u63D2\u5165\u6A21\u677F...",
"Templates": "\u6A21\u677F",
"Template": "\u6A21\u677F",
"Text color": "\u6587\u5B57\u989C\u8272",
"Background color": "\u80CC\u666F\u8272",
"Custom...": "\u81EA\u5B9A\u4E49...",
"Custom color": "\u81EA\u5B9A\u4E49\u989C\u8272",
"No color": "\u65E0",
"Remove color": "\u79FB\u9664\u989C\u8272",
"Table of Contents": "\u5185\u5BB9\u5217\u8868",
"Show blocks": "\u663E\u793A\u533A\u5757\u8FB9\u6846",
"Show invisible characters": "\u663E\u793A\u4E0D\u53EF\u89C1\u5B57\u7B26",
"Word count": "\u5B57\u6570",
"Words: {0}": "\u5B57\u6570\uFF1A{0}",
"{0} words": "{0} \u5B57",
"File": "\u6587\u4EF6",
"Edit": "\u7F16\u8F91",
"Insert": "\u63D2\u5165",
"View": "\u89C6\u56FE",
"Format": "\u683C\u5F0F",
"Table": "\u8868\u683C",
"Tools": "\u5DE5\u5177",
"Powered by {0}": "\u7531{0}\u9A71\u52A8",
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u5728\u7F16\u8F91\u533A\u6309ALT-F9\u6253\u5F00\u83DC\u5355\uFF0C\u6309ALT-F10\u6253\u5F00\u5DE5\u5177\u680F\uFF0C\u6309ALT-0\u67E5\u770B\u5E2E\u52A9",
"Image title": "\u56FE\u7247\u6807\u9898",
"Border width": "\u8FB9\u6846\u5BBD\u5EA6",
"Border style": "\u8FB9\u6846\u6837\u5F0F",
"Error": "\u9519\u8BEF",
"Warn": "\u8B66\u544A",
"Valid": "\u6709\u6548",
"To open the popup, press Shift+Enter": "\u6309Shitf+Enter\u952e\u6253\u5f00\u5bf9\u8bdd\u6846",
"Rich Text Area. Press ALT-0 for help.": "\u7f16\u8f91\u533a\u3002\u6309Alt+0\u952e\u6253\u5f00\u5e2e\u52a9\u3002",
"System Font": "\u7cfb\u7edf\u5b57\u4f53",
"Failed to upload image: {0}": "\u56fe\u7247\u4e0a\u4f20\u5931\u8d25: {0}",
"Failed to load plugin: {0} from url {1}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25: {0} \u6765\u81ea\u94fe\u63a5 {1}",
"Failed to load plugin url: {0}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25 \u94fe\u63a5: {0}",
"Failed to initialize plugin: {0}": "\u63d2\u4ef6\u521d\u59cb\u5316\u5931\u8d25: {0}",
"example": "\u793a\u4f8b",
"Search": "\u641c\u7d22",
"All": "\u5168\u90e8",
"Currency": "\u8d27\u5e01",
"Text": "\u6587\u5b57",
"Quotations": "\u5f15\u7528",
"Mathematical": "\u6570\u5b66",
"Extended Latin": "\u62c9\u4e01\u8bed\u6269\u5145",
"Symbols": "\u7b26\u53f7",
"Arrows": "\u7bad\u5934",
"User Defined": "\u81ea\u5b9a\u4e49",
"dollar sign": "\u7f8e\u5143\u7b26\u53f7",
"currency sign": "\u8d27\u5e01\u7b26\u53f7",
"euro-currency sign": "\u6b27\u5143\u7b26\u53f7",
"colon sign": "\u5192\u53f7",
"cruzeiro sign": "\u514b\u9c81\u8d5b\u7f57\u5e01\u7b26\u53f7",
"french franc sign": "\u6cd5\u90ce\u7b26\u53f7",
"lira sign": "\u91cc\u62c9\u7b26\u53f7",
"mill sign": "\u5bc6\u5c14\u7b26\u53f7",
"naira sign": "\u5948\u62c9\u7b26\u53f7",
"peseta sign": "\u6bd4\u585e\u5854\u7b26\u53f7",
"rupee sign": "\u5362\u6bd4\u7b26\u53f7",
"won sign": "\u97e9\u5143\u7b26\u53f7",
"new sheqel sign": "\u65b0\u8c22\u514b\u5c14\u7b26\u53f7",
"dong sign": "\u8d8a\u5357\u76fe\u7b26\u53f7",
"kip sign": "\u8001\u631d\u57fa\u666e\u7b26\u53f7",
"tugrik sign": "\u56fe\u683c\u91cc\u514b\u7b26\u53f7",
"drachma sign": "\u5fb7\u62c9\u514b\u9a6c\u7b26\u53f7",
"german penny symbol": "\u5fb7\u56fd\u4fbf\u58eb\u7b26\u53f7",
"peso sign": "\u6bd4\u7d22\u7b26\u53f7",
"guarani sign": "\u74dc\u62c9\u5c3c\u7b26\u53f7",
"austral sign": "\u6fb3\u5143\u7b26\u53f7",
"hryvnia sign": "\u683c\u91cc\u592b\u5c3c\u4e9a\u7b26\u53f7",
"cedi sign": "\u585e\u5730\u7b26\u53f7",
"livre tournois sign": "\u91cc\u5f17\u5f17\u5c14\u7b26\u53f7",
"spesmilo sign": "spesmilo\u7b26\u53f7",
"tenge sign": "\u575a\u6208\u7b26\u53f7",
"indian rupee sign": "\u5370\u5ea6\u5362\u6bd4",
"turkish lira sign": "\u571f\u8033\u5176\u91cc\u62c9",
"nordic mark sign": "\u5317\u6b27\u9a6c\u514b",
"manat sign": "\u9a6c\u7eb3\u7279\u7b26\u53f7",
"ruble sign": "\u5362\u5e03\u7b26\u53f7",
"yen character": "\u65e5\u5143\u5b57\u6837",
"yuan character": "\u4eba\u6c11\u5e01\u5143\u5b57\u6837",
"yuan character, in hong kong and taiwan": "\u5143\u5b57\u6837\uff08\u6e2f\u53f0\u5730\u533a\uff09",
"yen\/yuan character variant one": "\u5143\u5b57\u6837\uff08\u5927\u5199\uff09",
"Loading emoticons...": "\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7...",
"Could not load emoticons": "\u4e0d\u80fd\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7",
"People": "\u4eba\u7c7b",
"Animals and Nature": "\u52a8\u7269\u548c\u81ea\u7136",
"Food and Drink": "\u98df\u7269\u548c\u996e\u54c1",
"Activity": "\u6d3b\u52a8",
"Travel and Places": "\u65c5\u6e38\u548c\u5730\u70b9",
"Objects": "\u7269\u4ef6",
"Flags": "\u65d7\u5e1c",
"Characters": "\u5b57\u7b26",
"Characters (no spaces)": "\u5b57\u7b26(\u65e0\u7a7a\u683c)",
"Error: Form submit field collision.": "\u9519\u8bef: \u8868\u5355\u63d0\u4ea4\u5b57\u6bb5\u51b2\u7a81\u3002",
"Error: No form element found.": "\u9519\u8bef: \u6ca1\u6709\u8868\u5355\u63a7\u4ef6\u3002",
"Update": "\u66f4\u65b0",
"Color swatch": "\u989c\u8272\u6837\u672c",
"Turquoise": "\u9752\u7eff\u8272",
"Green": "\u7eff\u8272",
"Blue": "\u84dd\u8272",
"Purple": "\u7d2b\u8272",
"Navy Blue": "\u6d77\u519b\u84dd",
"Dark Turquoise": "\u6df1\u84dd\u7eff\u8272",
"Dark Green": "\u6df1\u7eff\u8272",
"Medium Blue": "\u4e2d\u84dd\u8272",
"Medium Purple": "\u4e2d\u7d2b\u8272",
"Midnight Blue": "\u6df1\u84dd\u8272",
"Yellow": "\u9ec4\u8272",
"Orange": "\u6a59\u8272",
"Red": "\u7ea2\u8272",
"Light Gray": "\u6d45\u7070\u8272",
"To open the popup, press Shift+Enter": "\u6309Shitf+Enter\u952E\u6253\u5F00\u5BF9\u8BDD\u6846",
"Rich Text Area. Press ALT-0 for help.": "\u7F16\u8F91\u533A\u3002\u6309Alt+0\u952E\u6253\u5F00\u5E2E\u52A9\u3002",
"System Font": "\u7CFB\u7EDF\u5B57\u4F53",
"Failed to upload image: {0}": "\u56FE\u7247\u4E0A\u4F20\u5931\u8D25: {0}",
"Failed to load plugin: {0} from url {1}": "\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25: {0} \u6765\u81EA\u94FE\u63A5 {1}",
"Failed to load plugin url: {0}": "\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25 \u94FE\u63A5: {0}",
"Failed to initialize plugin: {0}": "\u63D2\u4EF6\u521D\u59CB\u5316\u5931\u8D25: {0}",
"example": "\u793A\u4F8B",
"Search": "\u641C\u7D22",
"All": "\u5168\u90E8",
"Currency": "\u8D27\u5E01",
"Text": "\u6587\u5B57",
"Quotations": "\u5F15\u7528",
"Mathematical": "\u6570\u5B66",
"Extended Latin": "\u62C9\u4E01\u8BED\u6269\u5145",
"Symbols": "\u7B26\u53F7",
"Arrows": "\u7BAD\u5934",
"User Defined": "\u81EA\u5B9A\u4E49",
"dollar sign": "\u7F8E\u5143\u7B26\u53F7",
"currency sign": "\u8D27\u5E01\u7B26\u53F7",
"euro-currency sign": "\u6B27\u5143\u7B26\u53F7",
"colon sign": "\u5192\u53F7",
"cruzeiro sign": "\u514B\u9C81\u8D5B\u7F57\u5E01\u7B26\u53F7",
"french franc sign": "\u6CD5\u90CE\u7B26\u53F7",
"lira sign": "\u91CC\u62C9\u7B26\u53F7",
"mill sign": "\u5BC6\u5C14\u7B26\u53F7",
"naira sign": "\u5948\u62C9\u7B26\u53F7",
"peseta sign": "\u6BD4\u585E\u5854\u7B26\u53F7",
"rupee sign": "\u5362\u6BD4\u7B26\u53F7",
"won sign": "\u97E9\u5143\u7B26\u53F7",
"new sheqel sign": "\u65B0\u8C22\u514B\u5C14\u7B26\u53F7",
"dong sign": "\u8D8A\u5357\u76FE\u7B26\u53F7",
"kip sign": "\u8001\u631D\u57FA\u666E\u7B26\u53F7",
"tugrik sign": "\u56FE\u683C\u91CC\u514B\u7B26\u53F7",
"drachma sign": "\u5FB7\u62C9\u514B\u9A6C\u7B26\u53F7",
"german penny symbol": "\u5FB7\u56FD\u4FBF\u58EB\u7B26\u53F7",
"peso sign": "\u6BD4\u7D22\u7B26\u53F7",
"guarani sign": "\u74DC\u62C9\u5C3C\u7B26\u53F7",
"austral sign": "\u6FB3\u5143\u7B26\u53F7",
"hryvnia sign": "\u683C\u91CC\u592B\u5C3C\u4E9A\u7B26\u53F7",
"cedi sign": "\u585E\u5730\u7B26\u53F7",
"livre tournois sign": "\u91CC\u5F17\u5F17\u5C14\u7B26\u53F7",
"spesmilo sign": "spesmilo\u7B26\u53F7",
"tenge sign": "\u575A\u6208\u7B26\u53F7",
"indian rupee sign": "\u5370\u5EA6\u5362\u6BD4",
"turkish lira sign": "\u571F\u8033\u5176\u91CC\u62C9",
"nordic mark sign": "\u5317\u6B27\u9A6C\u514B",
"manat sign": "\u9A6C\u7EB3\u7279\u7B26\u53F7",
"ruble sign": "\u5362\u5E03\u7B26\u53F7",
"yen character": "\u65E5\u5143\u5B57\u6837",
"yuan character": "\u4EBA\u6C11\u5E01\u5143\u5B57\u6837",
"yuan character, in hong kong and taiwan": "\u5143\u5B57\u6837\uFF08\u6E2F\u53F0\u5730\u533A\uFF09",
"yen/yuan character variant one": "\u5143\u5B57\u6837\uFF08\u5927\u5199\uFF09",
"Loading emoticons...": "\u52A0\u8F7D\u8868\u60C5\u7B26\u53F7...",
"Could not load emoticons": "\u4E0D\u80FD\u52A0\u8F7D\u8868\u60C5\u7B26\u53F7",
"People": "\u4EBA\u7C7B",
"Animals and Nature": "\u52A8\u7269\u548C\u81EA\u7136",
"Food and Drink": "\u98DF\u7269\u548C\u996E\u54C1",
"Activity": "\u6D3B\u52A8",
"Travel and Places": "\u65C5\u6E38\u548C\u5730\u70B9",
"Objects": "\u7269\u4EF6",
"Flags": "\u65D7\u5E1C",
"Characters": "\u5B57\u7B26",
"Characters (no spaces)": "\u5B57\u7B26(\u65E0\u7A7A\u683C)",
"Error: Form submit field collision.": "\u9519\u8BEF: \u8868\u5355\u63D0\u4EA4\u5B57\u6BB5\u51B2\u7A81\u3002",
"Error: No form element found.": "\u9519\u8BEF: \u6CA1\u6709\u8868\u5355\u63A7\u4EF6\u3002",
"Update": "\u66F4\u65B0",
"Color swatch": "\u989C\u8272\u6837\u672C",
"Turquoise": "\u9752\u7EFF\u8272",
"Green": "\u7EFF\u8272",
"Blue": "\u84DD\u8272",
"Purple": "\u7D2B\u8272",
"Navy Blue": "\u6D77\u519B\u84DD",
"Dark Turquoise": "\u6DF1\u84DD\u7EFF\u8272",
"Dark Green": "\u6DF1\u7EFF\u8272",
"Medium Blue": "\u4E2D\u84DD\u8272",
"Medium Purple": "\u4E2D\u7D2B\u8272",
"Midnight Blue": "\u6DF1\u84DD\u8272",
"Yellow": "\u9EC4\u8272",
"Orange": "\u6A59\u8272",
"Red": "\u7EA2\u8272",
"Light Gray": "\u6D45\u7070\u8272",
"Gray": "\u7070\u8272",
"Dark Yellow": "\u6697\u9ec4\u8272",
"Dark Orange": "\u6df1\u6a59\u8272",
"Dark Red": "\u6df1\u7ea2\u8272",
"Medium Gray": "\u4e2d\u7070\u8272",
"Dark Gray": "\u6df1\u7070\u8272",
"Black": "\u9ed1\u8272",
"White": "\u767d\u8272",
"Switch to or from fullscreen mode": "\u5207\u6362\u5168\u5c4f\u6a21\u5f0f",
"Open help dialog": "\u6253\u5f00\u5e2e\u52a9\u5bf9\u8bdd\u6846",
"history": "\u5386\u53f2",
"styles": "\u6837\u5f0f",
"formatting": "\u683c\u5f0f\u5316",
"alignment": "\u5bf9\u9f50",
"indentation": "\u7f29\u8fdb",
"permanent pen": "\u8bb0\u53f7\u7b14",
"comments": "\u5907\u6ce8",
"Anchor": "\u951a\u70b9",
"Special character": "\u7279\u6b8a\u7b26\u53f7",
"Code sample": "\u4ee3\u7801\u793a\u4f8b",
"Color": "\u989c\u8272",
"Emoticons": "\u8868\u60c5",
"Document properties": "\u6587\u6863\u5c5e\u6027",
"Image": "\u56fe\u7247",
"Insert link": "\u63d2\u5165\u94fe\u63a5",
"Target": "\u6253\u5f00\u65b9\u5f0f",
"Link": "\u94fe\u63a5",
"Poster": "\u5c01\u9762",
"Media": "\u5a92\u4f53",
"Dark Yellow": "\u6697\u9EC4\u8272",
"Dark Orange": "\u6DF1\u6A59\u8272",
"Dark Red": "\u6DF1\u7EA2\u8272",
"Medium Gray": "\u4E2D\u7070\u8272",
"Dark Gray": "\u6DF1\u7070\u8272",
"Black": "\u9ED1\u8272",
"White": "\u767D\u8272",
"Switch to or from fullscreen mode": "\u5207\u6362\u5168\u5C4F\u6A21\u5F0F",
"Open help dialog": "\u6253\u5F00\u5E2E\u52A9\u5BF9\u8BDD\u6846",
"history": "\u5386\u53F2",
"styles": "\u6837\u5F0F",
"formatting": "\u683C\u5F0F\u5316",
"alignment": "\u5BF9\u9F50",
"indentation": "\u7F29\u8FDB",
"permanent pen": "\u8BB0\u53F7\u7B14",
"comments": "\u5907\u6CE8",
"Anchor": "\u951A\u70B9",
"Special character": "\u7279\u6B8A\u7B26\u53F7",
"Code sample": "\u4EE3\u7801\u793A\u4F8B",
"Color": "\u989C\u8272",
"Emoticons": "\u8868\u60C5",
"Document properties": "\u6587\u6863\u5C5E\u6027",
"Image": "\u56FE\u7247",
"Insert link": "\u63D2\u5165\u94FE\u63A5",
"Target": "\u6253\u5F00\u65B9\u5F0F",
"Link": "\u94FE\u63A5",
"Poster": "\u5C01\u9762",
"Media": "\u5A92\u4F53",
"Print": "\u6253\u5370",
"Prev": "\u4e0a\u4e00\u4e2a",
"Find and replace": "\u67e5\u627e\u548c\u66ff\u6362",
"Whole words": "\u5168\u5b57\u5339\u914d",
"Spellcheck": "\u62fc\u5199\u68c0\u67e5",
"Prev": "\u4E0A\u4E00\u4E2A",
"Find and replace": "\u67E5\u627E\u548C\u66FF\u6362",
"Whole words": "\u5168\u5B57\u5339\u914D",
"Spellcheck": "\u62FC\u5199\u68C0\u67E5",
"Caption": "\u6807\u9898",
"Insert template": "\u63d2\u5165\u6a21\u677f"
"Insert template": "\u63D2\u5165\u6A21\u677F"
});

674
apps/vben5/apps/app-antd/src/adapter/component/index.ts

@ -6,14 +6,39 @@
/* eslint-disable vue/one-component-per-file */
import type {
AutoCompleteProps,
ButtonProps,
CascaderProps,
CheckboxGroupProps,
CheckboxProps,
DatePickerProps,
DividerProps,
InputNumberProps,
InputProps,
MentionsProps,
RadioGroupProps,
RadioProps,
RateProps,
SelectProps,
SpaceProps,
SwitchProps,
TextAreaProps,
TimePickerProps,
TreeSelectProps,
UploadChangeParam,
UploadFile,
UploadProps,
} from 'ant-design-vue';
import type { RangePickerProps } from 'ant-design-vue/es/date-picker';
import type { Component, Ref } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type {
ApiComponentSharedProps,
BaseFormComponentType,
IconPickerProps,
} from '@vben/common-ui';
import type { Sortable } from '@vben/hooks';
import type { Recordable } from '@vben/types';
import {
@ -21,6 +46,9 @@ import {
defineAsyncComponent,
defineComponent,
h,
nextTick,
onMounted,
onUnmounted,
ref,
render,
unref,
@ -33,6 +61,7 @@ import {
IconPicker,
VCropper,
} from '@vben/common-ui';
import { useSortable } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { isEmpty } from '@vben/utils';
@ -40,7 +69,14 @@ import { isEmpty } from '@vben/utils';
import { FeatureStateCheck, GlobalFeatureStateCheck } from '@abp/features';
import { PermissionStateCheck } from '@abp/permissions';
import { TenantSelect } from '@abp/saas';
import { message, Modal, notification } from 'ant-design-vue';
import {
Empty,
InputSearch,
message,
Modal,
notification,
Tree,
} from 'ant-design-vue';
const ColorPicker = defineAsyncComponent(() =>
import('vue3-colorpicker').then((res) => {
@ -49,6 +85,16 @@ const ColorPicker = defineAsyncComponent(() =>
}),
);
type AdapterUploadProps = UploadProps & {
aspectRatio?: string;
crop?: boolean;
draggable?: boolean;
handleChange?: (event: UploadChangeParam) => void;
maxSize?: number;
onDragSort?: (oldIndex: number, newIndex: number) => void;
onHandleChange?: (event: UploadChangeParam) => void;
};
const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
);
@ -63,7 +109,6 @@ const DatePicker = defineAsyncComponent(
() => import('ant-design-vue/es/date-picker'),
);
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
const Empty = defineAsyncComponent(() => import('ant-design-vue/es/empty'));
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
const InputNumber = defineAsyncComponent(
() => import('ant-design-vue/es/input-number'),
@ -71,9 +116,6 @@ const InputNumber = defineAsyncComponent(
const InputPassword = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.InputPassword),
);
const InputSearch = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.InputSearch),
);
const Mentions = defineAsyncComponent(
() => import('ant-design-vue/es/mentions'),
);
@ -94,7 +136,6 @@ const Textarea = defineAsyncComponent(() =>
const TimePicker = defineAsyncComponent(
() => import('ant-design-vue/es/time-picker'),
);
const Tree = defineAsyncComponent(() => import('ant-design-vue/es/tree'));
const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'),
);
@ -107,8 +148,8 @@ const PreviewGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/image').then((res) => res.ImagePreviewGroup),
);
const withDefaultPlaceholder = <T extends Component>(
component: T,
const withDefaultPlaceholder = (
component: Component,
type: 'input' | 'select',
componentProps: Recordable<any> = {},
) => {
@ -141,260 +182,261 @@ const withDefaultPlaceholder = <T extends Component>(
});
};
const withPreviewUpload = () => {
// 检查是否为图片文件的辅助函数
const isImageFile = (file: UploadFile): boolean => {
const imageExtensions = new Set([
'bmp',
'gif',
'jpeg',
'jpg',
'png',
'svg',
'webp',
]);
if (file.url) {
try {
const pathname = new URL(file.url, 'http://localhost').pathname;
const ext = pathname.split('.').pop()?.toLowerCase();
return ext ? imageExtensions.has(ext) : false;
} catch {
const ext = file.url?.split('.').pop()?.toLowerCase();
return ext ? imageExtensions.has(ext) : false;
}
}
if (!file.type) {
const ext = file.name?.split('.').pop()?.toLowerCase();
return ext ? imageExtensions.has(ext) : false;
const IMAGE_EXTENSIONS = new Set([
'bmp',
'gif',
'jpeg',
'jpg',
'png',
'svg',
'webp',
]);
/**
*
*/
function isImageFile(file: UploadFile): boolean {
if (file.url) {
try {
const pathname = new URL(file.url, 'http://localhost').pathname;
const ext = pathname.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
} catch {
const ext = file.url?.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
}
return file.type.startsWith('image/');
}
if (!file.type) {
const ext = file.name?.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
}
return file.type.startsWith('image/');
}
/**
*
*/
function createDefaultUploadSlots(listType: string, placeholder: string) {
if (listType === 'picture-card') {
return { default: () => placeholder };
}
return {
default: () =>
h(
Button,
{
icon: h(IconifyIcon, {
icon: 'ant-design:upload-outlined',
class: 'mb-1 size-4',
}),
},
() => placeholder,
),
};
// 创建默认的上传按钮插槽
const createDefaultSlotsWithUpload = (
listType: string,
placeholder: string,
) => {
switch (listType) {
case 'picture-card': {
return {
default: () => placeholder,
};
}
default: {
return {
default: () =>
h(
Button,
{
icon: h(IconifyIcon, {
icon: 'ant-design:upload-outlined',
class: 'mb-1 size-4',
}),
}
/**
* Base64
*/
function getBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => resolve(reader.result as string));
reader.addEventListener('error', reject);
});
}
/**
*
*/
async function previewImage(
file: UploadFile,
visible: Ref<boolean>,
fileList: Ref<UploadProps['fileList']>,
) {
// 非图片文件直接打开链接
if (!isImageFile(file)) {
const url = file.url || file.preview;
if (url) {
window.open(url, '_blank');
} else {
message.error($t('ui.formRules.previewWarning'));
}
return;
}
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
Image,
PreviewGroup,
]);
// 过滤图片文件并生成预览
const imageFiles = (unref(fileList) || []).filter((f) => isImageFile(f));
for (const imgFile of imageFiles) {
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
imgFile.preview = await getBase64(imgFile.originFileObj);
}
}
const container = document.createElement('div');
document.body.append(container);
let isUnmounted = false;
const currentIndex = imageFiles.findIndex((f) => f.uid === file.uid);
const PreviewWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
return h(
PreviewGroupComponent,
{
class: 'hidden',
preview: {
visible: visible.value,
current: currentIndex,
onVisibleChange: (value: boolean) => {
visible.value = value;
if (!value) {
setTimeout(() => {
if (!isUnmounted && container) {
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
}
},
() => placeholder,
},
},
() =>
imageFiles.map((imgFile) =>
h(ImageComponent, {
key: imgFile.uid,
src: imgFile.url || imgFile.preview,
}),
),
};
}
}
);
};
},
};
// 构建预览图片组
const previewImage = async (
file: UploadFile,
visible: Ref<boolean>,
fileList: Ref<UploadProps['fileList']>,
) => {
// 如果当前文件不是图片,直接打开
if (!isImageFile(file)) {
if (file.url) {
window.open(file.url, '_blank');
} else if (file.preview) {
window.open(file.preview, '_blank');
} else {
message.error($t('ui.formRules.previewWarning'));
}
return;
}
// 对于图片文件,继续使用预览组
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
Image,
PreviewGroup,
]);
const getBase64 = (file: File) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => resolve(reader.result));
reader.addEventListener('error', (error) => reject(error));
});
};
// 从fileList中过滤出所有图片文件
const imageFiles = (unref(fileList) || []).filter((element) =>
isImageFile(element),
);
// 为所有没有预览地址的图片生成预览
for (const imgFile of imageFiles) {
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
imgFile.preview = (await getBase64(imgFile.originFileObj)) as string;
}
}
const container: HTMLElement | null = document.createElement('div');
render(h(PreviewWrapper), container);
}
/**
*
*/
function cropImage(file: File, aspectRatio: string | undefined) {
return new Promise<Blob | string | undefined>((resolve, reject) => {
const container = document.createElement('div');
document.body.append(container);
// 用于追踪组件是否已卸载
let isUnmounted = false;
let objectUrl: null | string = null;
const PreviewWrapper = {
const open = ref<boolean>(true);
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
const closeModal = () => {
open.value = false;
setTimeout(() => {
if (!isUnmounted && container) {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
};
const CropperWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
if (!objectUrl) {
objectUrl = URL.createObjectURL(file);
}
return h(
PreviewGroupComponent,
Modal,
{
class: 'hidden',
preview: {
visible: visible.value,
// 设置初始显示的图片索引
current: imageFiles.findIndex((f) => f.uid === file.uid),
onVisibleChange: (value: boolean) => {
visible.value = value;
if (!value) {
// 延迟清理,确保动画完成
setTimeout(() => {
if (!isUnmounted && container) {
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
open: open.value,
title: h('div', {}, [
$t('ui.crop.title'),
h(
'span',
{
class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`,
},
$t('ui.crop.titleTip', [aspectRatio]),
),
]),
centered: true,
width: 548,
keyboard: false,
maskClosable: false,
closable: false,
cancelText: $t('common.cancel'),
okText: $t('ui.crop.confirm'),
destroyOnClose: true,
onOk: async () => {
const cropper = cropperRef.value;
if (!cropper) {
reject(new Error('Cropper not found'));
closeModal();
return;
}
try {
const dataUrl = await cropper.getCropImage();
if (dataUrl) {
resolve(dataUrl);
} else {
reject(new Error($t('ui.crop.errorTip')));
}
},
} catch {
reject(new Error($t('ui.crop.errorTip')));
} finally {
closeModal();
}
},
onCancel() {
resolve('');
closeModal();
},
},
() =>
// 渲染所有图片文件
imageFiles.map((imgFile) =>
h(ImageComponent, {
key: imgFile.uid,
src: imgFile.url || imgFile.preview,
}),
),
h(VCropper, {
ref: (ref: any) => (cropperRef.value = ref),
img: objectUrl as string,
aspectRatio,
}),
);
};
},
};
render(h(PreviewWrapper), container);
};
// 图片裁剪操作
const cropImage = (file: File, aspectRatio: string | undefined) => {
return new Promise((resolve, reject) => {
const container: HTMLElement | null = document.createElement('div');
document.body.append(container);
// 用于追踪组件是否已卸载
let isUnmounted = false;
let objectUrl: null | string = null;
const open = ref<boolean>(true);
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
const closeModal = () => {
open.value = false;
// 延迟清理,确保动画完成
setTimeout(() => {
if (!isUnmounted && container) {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
};
const CropperWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
if (!objectUrl) {
objectUrl = URL.createObjectURL(file);
}
return h(
Modal,
{
open: open.value,
title: h('div', {}, [
$t('ui.crop.title'),
h(
'span',
{
class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`,
},
$t('ui.crop.titleTip', [aspectRatio]),
),
]),
centered: true,
width: 548,
keyboard: false,
maskClosable: false,
closable: false,
cancelText: $t('common.cancel'),
okText: $t('ui.crop.confirm'),
destroyOnClose: true,
onOk: async () => {
const cropper = cropperRef.value;
if (!cropper) {
reject(new Error('Cropper not found'));
closeModal();
return;
}
try {
const dataUrl = await cropper.getCropImage();
resolve(dataUrl);
} catch {
reject(new Error($t('ui.crop.errorTip')));
} finally {
closeModal();
}
},
onCancel() {
resolve('');
closeModal();
},
},
() =>
h(VCropper, {
ref: (ref: any) => (cropperRef.value = ref),
img: objectUrl as string,
aspectRatio,
}),
);
};
},
};
render(h(CropperWrapper), container);
});
};
render(h(CropperWrapper), container);
});
}
/**
*
*/
const withPreviewUpload = () => {
return defineComponent({
name: Upload.name,
emits: ['update:modelValue'],
setup: (
setup(
props: any,
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
) => {
) {
const previewVisible = ref<boolean>(false);
const placeholder = attrs?.placeholder || $t(`ui.placeholder.upload`);
const placeholder = attrs?.placeholder || $t('ui.placeholder.upload');
const listType = attrs?.listType || attrs?.['list-type'] || 'text';
const fileList = ref<UploadProps['fileList']>(
attrs?.fileList || attrs?.['file-list'] || [],
);
@ -408,12 +450,14 @@ const withPreviewUpload = () => {
file: UploadFile,
originFileList: Array<File>,
) => {
// 文件大小限制
if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) {
message.error($t('ui.formRules.sizeLimit', [maxSize.value]));
file.status = 'removed';
return false;
}
// 多选或者非图片不唤起裁剪框
// 图片裁剪处理
if (
attrs.crop &&
!attrs.multiple &&
@ -421,14 +465,11 @@ const withPreviewUpload = () => {
isImageFile(file)
) {
file.status = 'removed';
// antd Upload组件问题 file参数获取的是UploadFile类型对象无法取到File类型 所以通过originFileList[0]获取
const blob = await cropImage(originFileList[0], aspectRatio.value);
return new Promise((resolve, reject) => {
if (!blob) {
return reject(new Error($t('ui.crop.errorTip')));
}
resolve(blob);
});
if (!blob) {
throw new Error($t('ui.crop.errorTip'));
}
return blob;
}
return attrs.beforeUpload?.(file) ?? true;
@ -436,12 +477,9 @@ const withPreviewUpload = () => {
const handleChange = (event: UploadChangeParam) => {
try {
// 行内写法 handleChange: (event) => {}
attrs.handleChange?.(event);
// template写法 @handle-change="(event) => {}"
attrs.onHandleChange?.(event);
} catch (error) {
// Avoid breaking internal v-model sync on user handler errors
console.error(error);
}
fileList.value = event.fileList.filter(
@ -458,21 +496,88 @@ const withPreviewUpload = () => {
await previewImage(file, previewVisible, fileList);
};
const renderUploadButton = (): any => {
const isDisabled = attrs.disabled;
// 如果禁用,不渲染上传按钮
if (isDisabled) {
return null;
}
// 否则渲染默认上传按钮
const renderUploadButton = () => {
if (attrs.disabled) return null;
return isEmpty(slots)
? createDefaultSlotsWithUpload(listType, placeholder)
? createDefaultUploadSlots(listType, placeholder)
: slots;
};
// 可以监听到表单API设置的值
// 拖拽排序
const draggable = computed(
() => (attrs.draggable ?? false) && !attrs.disabled,
);
const uploadId = `upload-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
const sortableInstance = ref<null | Sortable>(null);
const styleId = `upload-drag-style-${uploadId}`;
function injectDragStyle() {
if (!document.querySelector(`[id="${styleId}"]`)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
[data-upload-id="${uploadId}"] .ant-upload-list-item { cursor: move; }
[data-upload-id="${uploadId}"] .ant-upload-list-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
`;
document.head.append(style);
}
}
function removeDragStyle() {
document.querySelector(`[id="${styleId}"]`)?.remove();
}
async function initSortable(retryCount = 0) {
if (!draggable.value) return;
injectDragStyle();
await nextTick();
await new Promise((resolve) => setTimeout(resolve, 100));
const container = document.querySelector(
`[data-upload-id="${uploadId}"] .ant-upload-list`,
) as HTMLElement;
if (!container) {
if (retryCount < 5) {
setTimeout(() => initSortable(retryCount + 1), 200);
}
return;
}
const { initializeSortable } = useSortable(container, {
animation: 300,
delay: 400,
delayOnTouchOnly: true,
filter:
'.ant-upload-select, .ant-upload-list-item-error, .ant-upload-list-item-uploading',
onEnd: (evt) => {
const { oldIndex, newIndex } = evt;
if (
oldIndex === undefined ||
newIndex === undefined ||
oldIndex === newIndex
) {
return;
}
const list = [...(fileList.value || [])];
const [movedItem] = list.splice(oldIndex, 1);
if (movedItem) {
list.splice(newIndex, 0, movedItem);
fileList.value = list;
}
attrs.onDragSort?.(oldIndex, newIndex);
emit('update:modelValue', fileList.value);
},
});
sortableInstance.value = await initializeSortable();
}
// 监听表单值变化
watch(
() => attrs.modelValue,
(res) => {
@ -480,18 +585,28 @@ const withPreviewUpload = () => {
},
);
onMounted(initSortable);
onUnmounted(() => {
sortableInstance.value?.destroy();
removeDragStyle();
});
return () =>
h(
Upload,
{
...props,
...attrs,
fileList: fileList.value,
beforeUpload: handleBeforeUpload,
onChange: handleChange,
onPreview: handlePreview,
},
renderUploadButton(),
'div',
{ 'data-upload-id': uploadId, class: 'w-full' },
h(
Upload,
{
...props,
...attrs,
fileList: fileList.value,
beforeUpload: handleBeforeUpload,
onChange: handleChange,
onPreview: handlePreview,
},
renderUploadButton() as any,
),
);
},
});
@ -536,6 +651,39 @@ export type ComponentType =
| 'Upload'
| BaseFormComponentType;
/**
* {@link ComponentType} 便 Schema `component` + `componentProps`
*/
export interface ComponentPropsMap {
ApiCascader: ApiComponentSharedProps & CascaderProps;
ApiSelect: ApiComponentSharedProps & SelectProps;
ApiTreeSelect: ApiComponentSharedProps & TreeSelectProps;
AutoComplete: AutoCompleteProps;
Cascader: CascaderProps;
Checkbox: CheckboxProps;
CheckboxGroup: CheckboxGroupProps;
DatePicker: DatePickerProps;
DefaultButton: ButtonProps;
Divider: DividerProps;
IconPicker: IconPickerProps;
Input: InputProps;
InputNumber: InputNumberProps;
InputPassword: InputProps;
Mentions: MentionsProps;
PrimaryButton: ButtonProps;
Radio: RadioProps;
RadioGroup: RadioGroupProps;
RangePicker: RangePickerProps;
Rate: RateProps;
Select: SelectProps;
Space: SpaceProps;
Switch: SwitchProps;
Textarea: TextAreaProps;
TimePicker: TimePickerProps;
TreeSelect: TreeSelectProps;
Upload: AdapterUploadProps;
}
async function initComponentAdapter() {
const components: Partial<Record<ComponentType, Component>> = {
// 如果你的组件体积比较大,可以使用异步加载

4
apps/vben5/apps/app-antd/src/adapter/request/index.ts

@ -56,7 +56,7 @@ export function initRequestClient() {
const timezoneStore = useTimezoneStore();
if (accessStore.accessToken) {
config.headers.Authorization = `${accessStore.accessToken}`;
config.headers.Authorization = accessStore.accessToken;
}
config.headers['Accept-Language'] = preferences.app.locale;
config.headers['X-Request-From'] = 'vben';
@ -76,7 +76,7 @@ export function initRequestClient() {
});
// response数据解构
requestClient.addResponseInterceptor<any>({
requestClient.addResponseInterceptor({
fulfilled: (response) => {
const { data, status } = response;
const { hasWrapResult, getData } = useWrapperResult(response);

12
apps/vben5/apps/app-antd/src/bootstrap.ts

@ -2,6 +2,7 @@ import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { registerLoadingDirective } from '@vben/common-ui';
import { providePluginsOptions } from '@vben/plugins';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
@ -12,7 +13,7 @@ import { useTitle } from '@vueuse/core';
import { $t, setupI18n } from '#/locales';
import { initComponentAdapter } from './adapter/component';
import { initSetupVbenForm } from './adapter/form';
import { initSetupVbenForm, useVbenForm } from './adapter/form';
import { initRequestClient } from './adapter/request';
import App from './app.vue';
import { router } from './router';
@ -25,6 +26,11 @@ async function bootstrap(namespace: string) {
// 初始化表单组件
await initSetupVbenForm();
// 注入插件全局配置
providePluginsOptions({
form: { useVbenForm },
});
// 初始化axios
initRequestClient();
@ -64,6 +70,10 @@ async function bootstrap(namespace: string) {
// 配置路由及路由守卫
app.use(router);
// 配置@tanstack/vue-query
const { VueQueryPlugin } = await import('@tanstack/vue-query');
app.use(VueQueryPlugin);
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);

3
apps/vben5/apps/app-antd/src/layouts/auth.vue

@ -15,8 +15,7 @@ const logo = computed(() => preferences.logo.source);
:app-name="appName"
:logo="logo"
:page-description="$t('authentication.pageDesc')"
:page-title="$t('authentication.pageTitle')"
>
:page-title="$t('authentication.pageTitle')">
<!-- 自定义工具栏 -->
<!-- <template #toolbar></template> -->
</AuthPageLayout>

21
apps/vben5/apps/app-antd/src/layouts/basic.vue

@ -108,13 +108,17 @@ function handleMakeAll() {
}
async function handleLinkUser(token: string) {
const { currentUser, currentTenant } = abpStore.application!;
const extraQueryParams: Record<string, string> = {
LinkUserId: currentUser.id!,
LinkToken: token,
};
if (currentTenant.id) {
extraQueryParams.LinkTenantId = currentTenant.id;
if (abpStore.application) {
const { currentUser, currentTenant } = abpStore.application;
if (currentUser.id) {
extraQueryParams.LinkUserId = currentUser.id;
}
if (currentTenant.id) {
extraQueryParams.LinkTenantId = currentTenant.id;
}
}
// ,
await authStore.oidcLogin({
@ -173,8 +177,7 @@ watch(
:menus
:tag-text="userInfo?.email"
:text="userInfo?.realName"
@logout="handleLogout"
/>
@logout="handleLogout" />
<span class="mb-1 flex items-center text-sm font-medium">
{{ description }}
</span>
@ -186,14 +189,12 @@ watch(
:dot="showDot"
:notifications="notifications"
@clear="handleNoticeClear"
@make-all="handleMakeAll"
/>
@make-all="handleMakeAll" />
</template>
<template #extra>
<AuthenticationLoginExpiredModal
v-model:open="accessStore.loginExpired"
:avatar
>
:avatar>
<LoginForm />
</AuthenticationLoginExpiredModal>
</template>

6
apps/vben5/apps/app-antd/src/preferences.ts

@ -1,4 +1,7 @@
import { defineOverridesPreferences } from '@vben/preferences';
import {
appCopyrightPreferences,
defineOverridesPreferences,
} from '@vben/preferences';
/**
* @description
@ -13,6 +16,7 @@ export const overridesPreferences = defineOverridesPreferences({
enableRefreshToken: true,
name: import.meta.env.VITE_APP_TITLE,
},
copyright: appCopyrightPreferences,
logo: {
source: '/resource/img/logo.png',
},

9
apps/vben5/apps/app-antd/src/store/auth.ts

@ -58,7 +58,6 @@ export const useAuthStore = defineStore('auth', () => {
accessToken: user.access_token,
tokenType: user.token_type,
refreshToken: user.refresh_token ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expiresIn: user.expires_in!,
});
} catch (error: any) {
@ -81,7 +80,6 @@ export const useAuthStore = defineStore('auth', () => {
accessToken: user.access_token,
tokenType: user.token_type,
refreshToken: user.refresh_token ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expiresIn: user.expires_in!,
},
onSuccess,
@ -104,7 +102,6 @@ export const useAuthStore = defineStore('auth', () => {
accessToken: user.access_token,
tokenType: user.token_type,
refreshToken: user.refresh_token ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expiresIn: user.expires_in!,
},
onSuccess,
@ -132,7 +129,6 @@ export const useAuthStore = defineStore('auth', () => {
accessToken: user.access_token,
tokenType: user.token_type,
refreshToken: user.refresh_token ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expiresIn: user.expires_in!,
},
onSuccess,
@ -163,7 +159,6 @@ export const useAuthStore = defineStore('auth', () => {
accessToken: user.access_token,
tokenType: user.token_type,
refreshToken: user.refresh_token ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expiresIn: user.expires_in!,
},
onSuccess,
@ -190,7 +185,6 @@ export const useAuthStore = defineStore('auth', () => {
accessToken: user.access_token,
tokenType: user.token_type,
refreshToken: user.refresh_token ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expiresIn: user.expires_in!,
},
onSuccess,
@ -228,7 +222,6 @@ export const useAuthStore = defineStore('auth', () => {
}
async function fetchUserInfo() {
let userInfo: null | (UserInfo & { [key: string]: any }) = null;
let userInfoRes: { [key: string]: any } = {};
const user = await oAuthService.getUser();
if (user) {
@ -237,7 +230,7 @@ export const useAuthStore = defineStore('auth', () => {
const abpConfig = await getConfigApi({
includeLocalizationResources: false,
});
userInfo = {
const userInfo = {
userId: userInfoRes.sub ?? abpConfig.currentUser.id,
username: userInfoRes.uniqueName ?? abpConfig.currentUser.userName,
realName:

3
apps/vben5/apps/app-antd/src/views/_core/authentication/code-login.vue

@ -86,6 +86,5 @@ async function handleLogin(values: Recordable<any>) {
ref="codeLogin"
:form-schema="formSchema"
:loading="loading"
@submit="handleLogin"
/>
@submit="handleLogin" />
</template>

3
apps/vben5/apps/app-antd/src/views/_core/authentication/forget-password.vue

@ -160,6 +160,5 @@ async function handleSubmit(values: Recordable<any>) {
:form-schema="formSchema"
:submit-button-text="$t('AbpAccount.ResetPassword')"
:loading="loading"
@submit="handleSubmit"
/>
@submit="handleSubmit" />
</template>

3
apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue

@ -162,8 +162,7 @@ onMounted(onInit);
:form-schema="formSchema"
:loading="authStore.loginLoading"
:show-register="isTrue('Abp.Account.IsSelfRegistrationEnabled')"
@submit="onLogin"
>
@submit="onLogin">
<!-- 第三方登录 -->
<template #third-party-login>
<ThirdPartyLogin />

3
apps/vben5/apps/app-antd/src/views/_core/authentication/qrcode-login.vue

@ -19,6 +19,5 @@ async function onConfirm(key: string, tenantId?: string) {
<QrCodeLogin
:login-path="LOGIN_PATH"
@confirm="onConfirm"
:default-avatar="preferences.app.defaultAvatar"
/>
:default-avatar="preferences.app.defaultAvatar" />
</template>

3
apps/vben5/apps/app-antd/src/views/_core/authentication/register.vue

@ -91,6 +91,5 @@ function handleSubmit(value: Recordable<any>) {
<AuthenticationRegister
:form-schema="formSchema"
:loading="loading"
@submit="handleSubmit"
/>
@submit="handleSubmit" />
</template>

24
apps/vben5/apps/app-antd/src/views/_core/authentication/two-factor-login.vue

@ -130,33 +130,28 @@ async function onSendCode(api: () => Promise<void>) {
ref="formRef"
:label-col="{ span: 6 }"
:model="formModel"
:wrapper-col="{ span: 18 }"
>
:wrapper-col="{ span: 18 }">
<!-- 选择验证器 -->
<FormItem
:label="$t('abp.oauth.twoFactor.authenticator')"
name="twoFactorProvider"
required
>
required>
<Select
v-model:value="formModel.twoFactorProvider"
:field-names="{ label: 'name', value: 'value' }"
:options="twoFactorProviders"
allow-clear
/>
allow-clear />
</FormItem>
<!-- 邮件验证 -->
<template v-if="formModel.twoFactorProvider === 'Email'">
<FormItem
:label="$t('abp.oauth.twoFactor.emailAddress')"
name="email"
required
>
required>
<Input
v-model:value="formModel.email"
autocomplete="off"
type="email"
/>
type="email" />
</FormItem>
<FormItem :label="$t('abp.oauth.twoFactor.code')" name="code" required>
<div class="flex flex-row gap-4">
@ -164,8 +159,7 @@ async function onSendCode(api: () => Promise<void>) {
<Button
:disabled="getSendValidCodeLoading"
:loading="sendValidCodeLoading"
@click="onSendEmail"
>
@click="onSendEmail">
{{ getSendValidCodeTitle }}
</Button>
</div>
@ -176,8 +170,7 @@ async function onSendCode(api: () => Promise<void>) {
<FormItem
:label="$t('abp.oauth.twoFactor.phoneNumber')"
name="phoneNumber"
required
>
required>
<Input v-model:value="formModel.phoneNumber" autocomplete="off" />
</FormItem>
<FormItem :label="$t('abp.oauth.twoFactor.code')" name="code" required>
@ -186,8 +179,7 @@ async function onSendCode(api: () => Promise<void>) {
<Button
:disabled="getSendValidCodeLoading"
:loading="sendValidCodeLoading"
@click="onSendSms"
>
@click="onSendSms">
{{ getSendValidCodeTitle }}
</Button>
</div>

3
apps/vben5/apps/app-antd/src/views/account/my-settings/index.vue

@ -105,8 +105,7 @@ async function onInit() {
<MySetting
:bind-items="externalLogins"
@on-bind-init="onInit"
@on-confirm="onBindComfirm"
/>
@on-confirm="onBindComfirm" />
<WechatWorkUserBindModal @on-login="onBindWorkWeixin" />
</Page>
</template>

2
apps/vben5/apps/app-antd/src/views/dashboard/analytics/analytics-visits-sales.vue

@ -24,7 +24,7 @@ onMounted(() => {
{ name: '定制', value: 310 },
{ name: '技术支持', value: 274 },
{ name: '远程', value: 400 },
].sort((a, b) => {
].toSorted((a, b) => {
return a.value - b.value;
}),
name: '商业占比',

3
apps/vben5/apps/app-antd/src/views/demos/antd/index.vue

@ -35,8 +35,7 @@ function notify(type: NotificationType) {
<template>
<Page
description="支持多语言,主题功能集成切换等"
title="Ant Design Vue组件使用演示"
>
title="Ant Design Vue组件使用演示">
<Card class="mb-5" title="按钮">
<Space>
<Button>Default</Button>

1
apps/vben5/apps/app-antd/tailwind.config.mjs

@ -1 +0,0 @@
export { default } from '@vben/tailwind-config';

1
apps/vben5/apps/app-antd/tsconfig.json

@ -2,7 +2,6 @@
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web-app.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"#/*": ["./src/*"]
}

4
apps/vben5/apps/backend-mock/api/auth/codes.ts

@ -1,5 +1,7 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
import { MOCK_CODES } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);

8
apps/vben5/apps/backend-mock/api/auth/login.post.ts

@ -1,9 +1,15 @@
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
import {
clearRefreshTokenCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
import { forbiddenResponse } from '~/utils/response';
import { MOCK_USERS } from '~/utils/mock-data';
import {
forbiddenResponse,
useResponseError,
useResponseSuccess,
} from '~/utils/response';
export default defineEventHandler(async (event) => {
const { password, username } = await readBody(event);

2
apps/vben5/apps/backend-mock/api/auth/logout.post.ts

@ -1,7 +1,9 @@
import { defineEventHandler } from 'h3';
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
} from '~/utils/cookie-utils';
import { useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);

4
apps/vben5/apps/backend-mock/api/auth/refresh.post.ts

@ -1,9 +1,11 @@
import { defineEventHandler } from 'h3';
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { verifyRefreshToken } from '~/utils/jwt-utils';
import { generateAccessToken, verifyRefreshToken } from '~/utils/jwt-utils';
import { MOCK_USERS } from '~/utils/mock-data';
import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => {

4
apps/vben5/apps/backend-mock/api/demo/bigint.ts

@ -1,3 +1,7 @@
import { eventHandler, setHeader } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {

4
apps/vben5/apps/backend-mock/api/menu/all.ts

@ -1,5 +1,7 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
import { MOCK_MENUS } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);

3
apps/vben5/apps/backend-mock/api/status.ts

@ -1,3 +1,6 @@
import { eventHandler, getQuery, setResponseStatus } from 'h3';
import { useResponseError } from '~/utils/response';
export default eventHandler((event) => {
const { status } = getQuery(event);
setResponseStatus(event, Number(status));

1
apps/vben5/apps/backend-mock/api/system/dept/.post.ts

@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,

1
apps/vben5/apps/backend-mock/api/system/dept/[id].delete.ts

@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,

1
apps/vben5/apps/backend-mock/api/system/dept/[id].put.ts

@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,

1
apps/vben5/apps/backend-mock/api/system/dept/list.ts

@ -1,4 +1,5 @@
import { faker } from '@faker-js/faker';
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';

1
apps/vben5/apps/backend-mock/api/system/menu/list.ts

@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';

3
apps/vben5/apps/backend-mock/api/system/menu/name-exists.ts

@ -1,6 +1,7 @@
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse } from '~/utils/response';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const namesMap: Record<string, any> = {};

3
apps/vben5/apps/backend-mock/api/system/menu/path-exists.ts

@ -1,6 +1,7 @@
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse } from '~/utils/response';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const pathMap: Record<string, any> = { '/': 0 };

1
apps/vben5/apps/backend-mock/api/system/role/list.ts

@ -1,4 +1,5 @@
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';

85
apps/vben5/apps/backend-mock/api/system/user/list.ts

@ -0,0 +1,85 @@
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem: Record<string, any> = {
id: faker.string.uuid(),
name: faker.commerce.product(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2022-01-01', to: '2025-01-01' }),
),
deptId: faker.string.uuid(),
remark: faker.lorem.sentence(),
};
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(100);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const {
page = 1,
pageSize = 20,
name,
id,
remark,
startTime,
endTime,
deptId,
status,
} = getQuery(event);
let listData = structuredClone(mockData);
if (name) {
listData = listData.filter((item) =>
item.name.toLowerCase().includes(String(name).toLowerCase()),
);
}
if (id) {
listData = listData.filter((item) =>
item.id.toLowerCase().includes(String(id).toLowerCase()),
);
}
if (remark) {
listData = listData.filter((item) =>
item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()),
);
}
if (startTime) {
listData = listData.filter((item) => item.createTime >= startTime);
}
if (endTime) {
listData = listData.filter((item) => item.createTime <= endTime);
}
if (['0', '1'].includes(status as string)) {
listData = listData.filter((item) => item.status === Number(status));
}
if (deptId) {
listData = listData.filter((item) => item.deptId === deptId);
}
return usePageResponseSuccess(page as string, pageSize as string, listData);
});

80
apps/vben5/apps/backend-mock/api/table/list.ts

@ -1,6 +1,11 @@
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
import {
sleep,
unAuthorizedResponse,
usePageResponseSuccess,
} from '~/utils/response';
function generateMockDataList(count: number) {
const dataList = [];
@ -44,30 +49,69 @@ export default eventHandler(async (event) => {
await sleep(600);
const { page, pageSize, sortBy, sortOrder } = getQuery(event);
// 规范化分页参数,处理 string[]
const pageRaw = Array.isArray(page) ? page[0] : page;
const pageSizeRaw = Array.isArray(pageSize) ? pageSize[0] : pageSize;
const pageNumber = Math.max(
1,
Number.parseInt(String(pageRaw ?? '1'), 10) || 1,
);
const pageSizeNumber = Math.min(
100,
Math.max(1, Number.parseInt(String(pageSizeRaw ?? '10'), 10) || 10),
);
const listData = structuredClone(mockData);
if (sortBy && Reflect.has(listData[0], sortBy as string)) {
// 规范化 query 入参,兼容 string[]
const sortKeyRaw = Array.isArray(sortBy) ? sortBy[0] : sortBy;
const sortOrderRaw = Array.isArray(sortOrder) ? sortOrder[0] : sortOrder;
// 检查 sortBy 是否是 listData 元素的合法属性键
if (
typeof sortKeyRaw === 'string' &&
listData[0] &&
Object.prototype.hasOwnProperty.call(listData[0], sortKeyRaw)
) {
// 定义数组元素的类型
type ItemType = (typeof listData)[0];
const sortKey = sortKeyRaw as keyof ItemType; // 将 sortBy 断言为合法键
const isDesc = sortOrderRaw === 'desc';
listData.sort((a, b) => {
if (sortOrder === 'asc') {
if (sortBy === 'price') {
return (
Number.parseFloat(a[sortBy as string]) -
Number.parseFloat(b[sortBy as string])
);
const aValue = a[sortKey] as unknown;
const bValue = b[sortKey] as unknown;
let result: number;
if (typeof aValue === 'number' && typeof bValue === 'number') {
result = aValue - bValue;
} else if (aValue instanceof Date && bValue instanceof Date) {
result = aValue.getTime() - bValue.getTime();
} else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') {
if (aValue === bValue) {
result = 0;
} else {
return a[sortBy as string] > b[sortBy as string] ? 1 : -1;
result = aValue ? 1 : -1;
}
} else {
if (sortBy === 'price') {
return (
Number.parseFloat(b[sortBy as string]) -
Number.parseFloat(a[sortBy as string])
);
} else {
return a[sortBy as string] < b[sortBy as string] ? 1 : -1;
}
const aStr = String(aValue);
const bStr = String(bValue);
const aNum = Number(aStr);
const bNum = Number(bStr);
result =
Number.isFinite(aNum) && Number.isFinite(bNum)
? aNum - bNum
: aStr.localeCompare(bStr, undefined, {
numeric: true,
sensitivity: 'base',
});
}
return isDesc ? -result : result;
});
}
return usePageResponseSuccess(page as string, pageSize as string, listData);
return usePageResponseSuccess(
String(pageNumber),
String(pageSizeNumber),
listData,
);
});

2
apps/vben5/apps/backend-mock/api/test.get.ts

@ -1 +1,3 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => 'Test get handler');

2
apps/vben5/apps/backend-mock/api/test.post.ts

@ -1 +1,3 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => 'Test post handler');

12
apps/vben5/apps/backend-mock/api/timezone/getTimezone.ts

@ -0,0 +1,12 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
import { getTimezone } from '~/utils/timezone-utils';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess(getTimezone());
});

11
apps/vben5/apps/backend-mock/api/timezone/getTimezoneOptions.ts

@ -0,0 +1,11 @@
import { eventHandler } from 'h3';
import { TIME_ZONE_OPTIONS } from '~/utils/mock-data';
import { useResponseSuccess } from '~/utils/response';
export default eventHandler(() => {
const data = TIME_ZONE_OPTIONS.map((o) => ({
label: `${o.timezone} (GMT${o.offset >= 0 ? `+${o.offset}` : o.offset})`,
value: o.timezone,
}));
return useResponseSuccess(data);
});

26
apps/vben5/apps/backend-mock/api/timezone/setTimezone.ts

@ -0,0 +1,26 @@
import { eventHandler, readBody, setResponseStatus } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { TIME_ZONE_OPTIONS } from '~/utils/mock-data';
import {
unAuthorizedResponse,
useResponseError,
useResponseSuccess,
} from '~/utils/response';
import { setTimezone } from '~/utils/timezone-utils';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const body = await readBody<{ timezone?: unknown }>(event);
const timezone =
typeof body?.timezone === 'string' ? body.timezone : undefined;
const allowed = TIME_ZONE_OPTIONS.some((o) => o.timezone === timezone);
if (!timezone || !allowed) {
setResponseStatus(event, 400);
return useResponseError('Bad Request', 'Invalid timezone');
}
setTimezone(timezone);
return useResponseSuccess({});
});

3
apps/vben5/apps/backend-mock/api/upload.ts

@ -1,5 +1,6 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);

3
apps/vben5/apps/backend-mock/api/user/info.ts

@ -1,5 +1,6 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);

1
apps/vben5/apps/backend-mock/middleware/1.api.ts

@ -1,3 +1,4 @@
import { defineEventHandler } from 'h3';
import { forbiddenResponse, sleep } from '~/utils/response';
export default defineEventHandler(async (event) => {

2
apps/vben5/apps/backend-mock/nitro.config.ts

@ -1,3 +1,5 @@
import { defineNitroConfig } from 'nitropack/config';
import errorHandler from './error';
process.env.COMPATIBILITY_DATE = new Date().toISOString();

2
apps/vben5/apps/backend-mock/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/backend-mock",
"version": "0.0.1",
"version": "5.7.0",
"description": "",
"private": true,
"license": "MIT",

2
apps/vben5/apps/backend-mock/routes/[...].ts

@ -1,3 +1,5 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => {
return `
<h1>Hello Vben Admin</h1>

10
apps/vben5/apps/backend-mock/tsconfig.json

@ -1,3 +1,11 @@
{
"extends": "./.nitro/types/tsconfig.json"
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/node.json",
"compilerOptions": {
"paths": {
"~/*": ["./*"]
}
},
"include": ["**/*.ts", "**/.*.ts"],
"exclude": ["node_modules", "dist", ".nitro"]
}

2
apps/vben5/apps/backend-mock/utils/cookie-utils.ts

@ -1,5 +1,7 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import { deleteCookie, getCookie, setCookie } from 'h3';
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
deleteCookie(event, 'jwt', {
httpOnly: true,

26
apps/vben5/apps/backend-mock/utils/jwt-utils.ts

@ -1,8 +1,11 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import type { UserInfo } from './mock-data';
import { getHeader } from 'h3';
import jwt from 'jsonwebtoken';
import { UserInfo } from './mock-data';
import { MOCK_USERS } from './mock-data';
// TODO: Replace with your own secret key
const ACCESS_TOKEN_SECRET = 'access_token_secret';
@ -31,12 +34,22 @@ export function verifyAccessToken(
return null;
}
const token = authHeader.split(' ')[1];
const tokenParts = authHeader.split(' ');
if (tokenParts.length !== 2) {
return null;
}
const token = tokenParts[1] as string;
try {
const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET) as UserPayload;
const decoded = jwt.verify(
token,
ACCESS_TOKEN_SECRET,
) as unknown as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
if (!user) {
return null;
}
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
@ -50,7 +63,12 @@ export function verifyRefreshToken(
try {
const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
const user = MOCK_USERS.find(
(item) => item.username === username,
) as UserInfo;
if (!user) {
return null;
}
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {

37
apps/vben5/apps/backend-mock/utils/mock-data.ts

@ -7,6 +7,11 @@ export interface UserInfo {
homePath?: string;
}
export interface TimezoneOption {
offset: number;
timezone: string;
}
export const MOCK_USERS: UserInfo[] = [
{
id: 0,
@ -276,7 +281,7 @@ export const MOCK_MENU_LIST = [
children: [
{
id: 20_401,
pid: 201,
pid: 202,
name: 'SystemDeptCreate',
status: 1,
type: 'button',
@ -285,7 +290,7 @@ export const MOCK_MENU_LIST = [
},
{
id: 20_402,
pid: 201,
pid: 202,
name: 'SystemDeptEdit',
status: 1,
type: 'button',
@ -294,7 +299,7 @@ export const MOCK_MENU_LIST = [
},
{
id: 20_403,
pid: 201,
pid: 202,
name: 'SystemDeptDelete',
status: 1,
type: 'button',
@ -388,3 +393,29 @@ export function getMenuIds(menus: any[]) {
});
return ids;
}
/**
*
*/
export const TIME_ZONE_OPTIONS: TimezoneOption[] = [
{
offset: -5,
timezone: 'America/New_York',
},
{
offset: 0,
timezone: 'Europe/London',
},
{
offset: 8,
timezone: 'Asia/Shanghai',
},
{
offset: 9,
timezone: 'Asia/Tokyo',
},
{
offset: 9,
timezone: 'Asia/Seoul',
},
];

2
apps/vben5/apps/backend-mock/utils/response.ts

@ -1,5 +1,7 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import { setResponseStatus } from 'h3';
export function useResponseSuccess<T = any>(data: T) {
return {
code: 0,

9
apps/vben5/apps/backend-mock/utils/timezone-utils.ts

@ -0,0 +1,9 @@
let mockTimeZone: null | string = null;
export const setTimezone = (timeZone: string) => {
mockTimeZone = timeZone;
};
export const getTimezone = () => {
return mockTimeZone;
};

5
apps/vben5/apps/web-antd/index.html

@ -9,10 +9,9 @@
<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"
/>
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>
<title>%VITE_APP_TITLE%</title>
<link rel="icon" href="/favicon.ico" />
<script>
// 生产环境下注入百度统计

2
apps/vben5/apps/web-antd/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/web-antd",
"version": "5.5.7",
"version": "5.7.0",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

1
apps/vben5/apps/web-antd/postcss.config.mjs

@ -1 +0,0 @@
export { default } from '@vben/tailwind-config/postcss';

619
apps/vben5/apps/web-antd/src/adapter/component/index.ts

@ -3,23 +3,80 @@
* vben-formvben-modalvben-drawer 使,
*/
import type { Component } from 'vue';
/* eslint-disable vue/one-component-per-file */
import type { BaseFormComponentType } from '@vben/common-ui';
import type {
AutoCompleteProps,
ButtonProps,
CascaderProps,
CheckboxGroupProps,
CheckboxProps,
DatePickerProps,
DividerProps,
InputNumberProps,
InputProps,
MentionsProps,
RadioGroupProps,
RadioProps,
RateProps,
SelectProps,
SpaceProps,
SwitchProps,
TextAreaProps,
TimePickerProps,
TreeSelectProps,
UploadChangeParam,
UploadFile,
UploadProps,
} from 'ant-design-vue';
import type { RangePickerProps } from 'ant-design-vue/es/date-picker';
import type { Component, Ref } from 'vue';
import type {
ApiComponentSharedProps,
BaseFormComponentType,
IconPickerProps,
} from '@vben/common-ui';
import type { Sortable } from '@vben/hooks';
import type { Recordable } from '@vben/types';
import {
computed,
defineAsyncComponent,
defineComponent,
getCurrentInstance,
h,
nextTick,
onMounted,
onUnmounted,
ref,
render,
unref,
watch,
} from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import {
ApiComponent,
globalShareState,
IconPicker,
VCropper,
} from '@vben/common-ui';
import { useSortable } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { isEmpty } from '@vben/utils';
import { notification } from 'ant-design-vue';
import { message, Modal, notification } from 'ant-design-vue';
type AdapterUploadProps = UploadProps & {
aspectRatio?: string;
crop?: boolean;
draggable?: boolean;
handleChange?: (event: UploadChangeParam) => void;
maxSize?: number;
onDragSort?: (oldIndex: number, newIndex: number) => void;
onHandleChange?: (event: UploadChangeParam) => void;
};
const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
@ -65,10 +122,17 @@ const TimePicker = defineAsyncComponent(
const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'),
);
const Cascader = defineAsyncComponent(
() => import('ant-design-vue/es/cascader'),
);
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
const Image = defineAsyncComponent(() => import('ant-design-vue/es/image'));
const PreviewGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/image').then((res) => res.ImagePreviewGroup),
);
const withDefaultPlaceholder = <T extends Component>(
component: T,
const withDefaultPlaceholder = (
component: Component,
type: 'input' | 'select',
componentProps: Recordable<any> = {},
) => {
@ -82,16 +146,15 @@ const withDefaultPlaceholder = <T extends Component>(
$t(`ui.placeholder.${type}`);
// 透传组件暴露的方法
const innerRef = ref();
const publicApi: Recordable<any> = {};
expose(publicApi);
const instance = getCurrentInstance();
instance?.proxy?.$nextTick(() => {
for (const key in innerRef.value) {
if (typeof innerRef.value[key] === 'function') {
publicApi[key] = innerRef.value[key];
}
}
});
expose(
new Proxy(
{},
{
get: (_target, key) => innerRef.value?.[key],
has: (_target, key) => key in (innerRef.value || {}),
},
),
);
return () =>
h(
component,
@ -102,11 +165,443 @@ const withDefaultPlaceholder = <T extends Component>(
});
};
const IMAGE_EXTENSIONS = new Set([
'bmp',
'gif',
'jpeg',
'jpg',
'png',
'svg',
'webp',
]);
/**
*
*/
function isImageFile(file: UploadFile): boolean {
if (file.url) {
try {
const pathname = new URL(file.url, 'http://localhost').pathname;
const ext = pathname.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
} catch {
const ext = file.url?.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
}
}
if (!file.type) {
const ext = file.name?.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
}
return file.type.startsWith('image/');
}
/**
*
*/
function createDefaultUploadSlots(listType: string, placeholder: string) {
if (listType === 'picture-card') {
return { default: () => placeholder };
}
return {
default: () =>
h(
Button,
{
icon: h(IconifyIcon, {
icon: 'ant-design:upload-outlined',
class: 'mb-1 size-4',
}),
},
() => placeholder,
),
};
}
/**
* Base64
*/
function getBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => resolve(reader.result as string));
reader.addEventListener('error', reject);
});
}
/**
*
*/
async function previewImage(
file: UploadFile,
visible: Ref<boolean>,
fileList: Ref<UploadProps['fileList']>,
) {
// 非图片文件直接打开链接
if (!isImageFile(file)) {
const url = file.url || file.preview;
if (url) {
window.open(url, '_blank');
} else {
message.error($t('ui.formRules.previewWarning'));
}
return;
}
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
Image,
PreviewGroup,
]);
// 过滤图片文件并生成预览
const imageFiles = (unref(fileList) || []).filter((f) => isImageFile(f));
for (const imgFile of imageFiles) {
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
imgFile.preview = await getBase64(imgFile.originFileObj);
}
}
const container = document.createElement('div');
document.body.append(container);
let isUnmounted = false;
const currentIndex = imageFiles.findIndex((f) => f.uid === file.uid);
const PreviewWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
return h(
PreviewGroupComponent,
{
class: 'hidden',
preview: {
visible: visible.value,
current: currentIndex,
onVisibleChange: (value: boolean) => {
visible.value = value;
if (!value) {
setTimeout(() => {
if (!isUnmounted && container) {
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
}
},
},
},
() =>
imageFiles.map((imgFile) =>
h(ImageComponent, {
key: imgFile.uid,
src: imgFile.url || imgFile.preview,
}),
),
);
};
},
};
render(h(PreviewWrapper), container);
}
/**
*
*/
function cropImage(file: File, aspectRatio: string | undefined) {
return new Promise<Blob | string | undefined>((resolve, reject) => {
const container = document.createElement('div');
document.body.append(container);
let isUnmounted = false;
let objectUrl: null | string = null;
const open = ref<boolean>(true);
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
const closeModal = () => {
open.value = false;
setTimeout(() => {
if (!isUnmounted && container) {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
};
const CropperWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
if (!objectUrl) {
objectUrl = URL.createObjectURL(file);
}
return h(
Modal,
{
open: open.value,
title: h('div', {}, [
$t('ui.crop.title'),
h(
'span',
{
class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`,
},
$t('ui.crop.titleTip', [aspectRatio]),
),
]),
centered: true,
width: 548,
keyboard: false,
maskClosable: false,
closable: false,
cancelText: $t('common.cancel'),
okText: $t('ui.crop.confirm'),
destroyOnClose: true,
onOk: async () => {
const cropper = cropperRef.value;
if (!cropper) {
reject(new Error('Cropper not found'));
closeModal();
return;
}
try {
const dataUrl = await cropper.getCropImage();
if (dataUrl) {
resolve(dataUrl);
} else {
reject(new Error($t('ui.crop.errorTip')));
}
} catch {
reject(new Error($t('ui.crop.errorTip')));
} finally {
closeModal();
}
},
onCancel() {
resolve('');
closeModal();
},
},
() =>
h(VCropper, {
ref: (ref: any) => (cropperRef.value = ref),
img: objectUrl as string,
aspectRatio,
}),
);
};
},
};
render(h(CropperWrapper), container);
});
}
/**
*
*/
const withPreviewUpload = () => {
return defineComponent({
name: Upload.name,
emits: ['update:modelValue'],
setup(
props: any,
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
) {
const previewVisible = ref<boolean>(false);
const placeholder = attrs?.placeholder || $t('ui.placeholder.upload');
const listType = attrs?.listType || attrs?.['list-type'] || 'text';
const fileList = ref<UploadProps['fileList']>(
attrs?.fileList || attrs?.['file-list'] || [],
);
const maxSize = computed(() => attrs?.maxSize ?? attrs?.['max-size']);
const aspectRatio = computed(
() => attrs?.aspectRatio ?? attrs?.['aspect-ratio'],
);
const handleBeforeUpload = async (
file: UploadFile,
originFileList: Array<File>,
) => {
// 文件大小限制
if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) {
message.error($t('ui.formRules.sizeLimit', [maxSize.value]));
file.status = 'removed';
return false;
}
// 图片裁剪处理
if (
attrs.crop &&
!attrs.multiple &&
originFileList[0] &&
isImageFile(file)
) {
file.status = 'removed';
const blob = await cropImage(originFileList[0], aspectRatio.value);
if (!blob) {
throw new Error($t('ui.crop.errorTip'));
}
return blob;
}
return attrs.beforeUpload?.(file) ?? true;
};
const handleChange = (event: UploadChangeParam) => {
try {
attrs.handleChange?.(event);
attrs.onHandleChange?.(event);
} catch (error) {
console.error(error);
}
fileList.value = event.fileList.filter(
(file) => file.status !== 'removed',
);
emit(
'update:modelValue',
event.fileList?.length ? fileList.value : undefined,
);
};
const handlePreview = async (file: UploadFile) => {
previewVisible.value = true;
await previewImage(file, previewVisible, fileList);
};
const renderUploadButton = () => {
if (attrs.disabled) return null;
return isEmpty(slots)
? createDefaultUploadSlots(listType, placeholder)
: slots;
};
// 拖拽排序
const draggable = computed(
() => (attrs.draggable ?? false) && !attrs.disabled,
);
const uploadId = `upload-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
const sortableInstance = ref<null | Sortable>(null);
const styleId = `upload-drag-style-${uploadId}`;
function injectDragStyle() {
if (!document.querySelector(`[id="${styleId}"]`)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
[data-upload-id="${uploadId}"] .ant-upload-list-item { cursor: move; }
[data-upload-id="${uploadId}"] .ant-upload-list-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
`;
document.head.append(style);
}
}
function removeDragStyle() {
document.querySelector(`[id="${styleId}"]`)?.remove();
}
async function initSortable(retryCount = 0) {
if (!draggable.value) return;
injectDragStyle();
await nextTick();
await new Promise((resolve) => setTimeout(resolve, 100));
const container = document.querySelector(
`[data-upload-id="${uploadId}"] .ant-upload-list`,
) as HTMLElement;
if (!container) {
if (retryCount < 5) {
setTimeout(() => initSortable(retryCount + 1), 200);
}
return;
}
const { initializeSortable } = useSortable(container, {
animation: 300,
delay: 400,
delayOnTouchOnly: true,
filter:
'.ant-upload-select, .ant-upload-list-item-error, .ant-upload-list-item-uploading',
onEnd: (evt) => {
const { oldIndex, newIndex } = evt;
if (
oldIndex === undefined ||
newIndex === undefined ||
oldIndex === newIndex
) {
return;
}
const list = [...(fileList.value || [])];
const [movedItem] = list.splice(oldIndex, 1);
if (movedItem) {
list.splice(newIndex, 0, movedItem);
fileList.value = list;
}
attrs.onDragSort?.(oldIndex, newIndex);
emit('update:modelValue', fileList.value);
},
});
sortableInstance.value = await initializeSortable();
}
// 监听表单值变化
watch(
() => attrs.modelValue,
(res) => {
fileList.value = res;
},
);
onMounted(initSortable);
onUnmounted(() => {
sortableInstance.value?.destroy();
removeDragStyle();
});
return () =>
h(
'div',
{ 'data-upload-id': uploadId, class: 'w-full' },
h(
Upload,
{
...props,
...attrs,
fileList: fileList.value,
beforeUpload: handleBeforeUpload,
onChange: handleChange,
onPreview: handlePreview,
},
renderUploadButton() as any,
),
);
},
});
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiCascader'
| 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete'
| 'Cascader'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
@ -131,40 +626,68 @@ export type ComponentType =
| 'Upload'
| BaseFormComponentType;
/**
* {@link ComponentType} 便 Schema `component` + `componentProps`
*/
export interface ComponentPropsMap {
ApiCascader: ApiComponentSharedProps & CascaderProps;
ApiSelect: ApiComponentSharedProps & SelectProps;
ApiTreeSelect: ApiComponentSharedProps & TreeSelectProps;
AutoComplete: AutoCompleteProps;
Cascader: CascaderProps;
Checkbox: CheckboxProps;
CheckboxGroup: CheckboxGroupProps;
DatePicker: DatePickerProps;
DefaultButton: ButtonProps;
Divider: DividerProps;
IconPicker: IconPickerProps;
Input: InputProps;
InputNumber: InputNumberProps;
InputPassword: InputProps;
Mentions: MentionsProps;
PrimaryButton: ButtonProps;
Radio: RadioProps;
RadioGroup: RadioGroupProps;
RangePicker: RangePickerProps;
Rate: RateProps;
Select: SelectProps;
Space: SpaceProps;
Switch: SwitchProps;
Textarea: TextAreaProps;
TimePicker: TimePickerProps;
TreeSelect: TreeSelectProps;
Upload: AdapterUploadProps;
}
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',
},
),
ApiTreeSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiTreeSelect',
},
'select',
{
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
},
),
ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', {
component: Cascader,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
visibleEvent: 'onVisibleChange',
}),
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
component: Select,
loadingSlot: 'suffixIcon',
modelPropName: 'value',
visibleEvent: 'onVisibleChange',
}),
ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
}),
AutoComplete,
Cascader,
Checkbox,
CheckboxGroup,
DatePicker,
@ -179,7 +702,9 @@ async function initComponentAdapter() {
modelValueProp: 'value',
}),
Input: withDefaultPlaceholder(Input, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input', {
style: { width: '100%' },
}),
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
Mentions: withDefaultPlaceholder(Mentions, 'input'),
// 自定义主要按钮
@ -196,7 +721,7 @@ async function initComponentAdapter() {
Textarea: withDefaultPlaceholder(Textarea, 'input'),
TimePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload,
Upload: withPreviewUpload(),
};
// 将组件注册到全局共享状态中

10
apps/vben5/apps/web-antd/src/adapter/form.ts

@ -1,9 +1,9 @@
import type {
VbenFormProps as FormProps,
VbenFormSchema as FormSchema,
VbenFormProps,
} from '@vben/common-ui';
import type { ComponentType } from './component';
import type { ComponentPropsMap, ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
@ -41,9 +41,9 @@ async function initSetupVbenForm() {
});
}
const useVbenForm = useForm<ComponentType>;
const useVbenForm = useForm<ComponentType, ComponentPropsMap>;
export { initSetupVbenForm, useVbenForm, z };
export type VbenFormSchema = FormSchema<ComponentType>;
export type { VbenFormProps };
export type VbenFormSchema = FormSchema<ComponentType, ComponentPropsMap>;
export type VbenFormProps = FormProps<ComponentType, ComponentPropsMap>;

16
apps/vben5/apps/web-antd/src/adapter/vxe-table.ts

@ -1,8 +1,13 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { ComponentPropsMap, ComponentType } from './component';
import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
import {
setupVbenVxeTable,
useVbenVxeGrid as useGrid,
} from '@vben/plugins/vxe-table';
import { Button, Image } from 'ant-design-vue';
@ -40,9 +45,10 @@ setupVbenVxeTable({
// 表格配置项可以用 cellRender: { name: 'CellImage' },
vxeUI.renderer.add('CellImage', {
renderTableDefault(_renderOpts, params) {
renderTableDefault(renderOpts, params) {
const { props } = renderOpts;
const { column, row } = params;
return h(Image, { src: row[column.field] });
return h(Image, { src: row[column.field], ...props });
},
});
@ -64,6 +70,8 @@ setupVbenVxeTable({
useVbenForm,
});
export { useVbenVxeGrid };
export const useVbenVxeGrid = <T extends Record<string, any>>(
...rest: Parameters<typeof useGrid<T, ComponentType, ComponentPropsMap>>
) => useGrid<T, ComponentType, ComponentPropsMap>(...rest);
export type * from '@vben/plugins/vxe-table';

5
apps/vben5/apps/web-antd/src/layouts/auth.vue

@ -8,15 +8,16 @@ import { $t } from '#/locales';
const appName = computed(() => preferences.app.name);
const logo = computed(() => preferences.logo.source);
const logoDark = computed(() => preferences.logo.sourceDark);
</script>
<template>
<AuthPageLayout
:app-name="appName"
:logo="logo"
:logo-dark="logoDark"
:page-description="$t('authentication.pageDesc')"
:page-title="$t('authentication.pageTitle')"
>
:page-title="$t('authentication.pageTitle')">
<!-- 自定义工具栏 -->
<!-- <template #toolbar></template> -->
</AuthPageLayout>

116
apps/vben5/apps/web-antd/src/layouts/basic.vue

@ -2,18 +2,19 @@
import type { NotificationItem } from '@vben/layouts';
import { computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { useWatermark } from '@vben/hooks';
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons';
import {
BasicLayout,
LockScreen,
Notification,
UserDropdown,
} from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { preferences, usePreferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { openWindow } from '@vben/utils';
@ -23,6 +24,7 @@ import LoginForm from '#/views/_core/authentication/login.vue';
const notifications = ref<NotificationItem[]>([
{
id: 1,
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
date: '3小时前',
isRead: true,
@ -30,6 +32,7 @@ const notifications = ref<NotificationItem[]>([
title: '收到了 14 份新周报',
},
{
id: 2,
avatar: 'https://avatar.vercel.sh/1',
date: '刚刚',
isRead: false,
@ -37,6 +40,7 @@ const notifications = ref<NotificationItem[]>([
title: '朱偏右 回复了你',
},
{
id: 3,
avatar: 'https://avatar.vercel.sh/1',
date: '2024-01-01',
isRead: false,
@ -44,23 +48,51 @@ const notifications = ref<NotificationItem[]>([
title: '曲丽丽 评论了你',
},
{
id: 4,
avatar: 'https://avatar.vercel.sh/satori',
date: '1天前',
isRead: false,
message: '描述信息描述信息描述信息',
title: '代办提醒',
},
{
id: 5,
avatar: 'https://avatar.vercel.sh/satori',
date: '1天前',
isRead: false,
message: '描述信息描述信息描述信息',
title: '跳转Workspace示例',
link: '/workspace',
},
{
id: 6,
avatar: 'https://avatar.vercel.sh/satori',
date: '1天前',
isRead: false,
message: '描述信息描述信息描述信息',
title: '跳转外部链接示例',
link: 'https://doc.vben.pro',
},
]);
const router = useRouter();
const userStore = useUserStore();
const authStore = useAuthStore();
const accessStore = useAccessStore();
const { destroyWatermark, updateWatermark } = useWatermark();
const { isDark } = usePreferences();
const showDot = computed(() =>
notifications.value.some((item) => !item.isRead),
);
const menus = computed(() => [
{
handler: () => {
router.push({ name: 'Profile' });
},
icon: 'lucide:user',
text: $t('page.auth.profile'),
},
{
handler: () => {
openWindow(VBEN_DOC_URL, {
@ -76,7 +108,7 @@ const menus = computed(() => [
target: '_blank',
});
},
icon: MdiGithub,
icon: SvgGithubIcon,
text: 'GitHub',
},
{
@ -102,15 +134,77 @@ function handleNoticeClear() {
notifications.value = [];
}
function markRead(id: number | string) {
const item = notifications.value.find((item) => item.id === id);
if (item) {
item.isRead = true;
}
}
function remove(id: number | string) {
notifications.value = notifications.value.filter((item) => item.id !== id);
}
function handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true));
}
const viewAll = () => {};
const handleClick = (item: NotificationItem) => {
//
if (item.link) {
navigateTo(item.link, item.query, item.state);
}
};
function navigateTo(
link: string,
query?: Record<string, any>,
state?: Record<string, any>,
) {
if (link.startsWith('http://') || link.startsWith('https://')) {
//
window.open(link, '_blank');
} else {
// query state
router.push({
path: link,
query: query || {},
state,
});
}
}
watch(
() => preferences.app.watermark,
async (enable) => {
() => ({
enable: preferences.app.watermark,
content: preferences.app.watermarkContent,
isDark: isDark.value,
}),
async ({ enable, content, isDark: isDarkValue }) => {
if (enable) {
const watermarkColor = isDarkValue
? 'rgba(255, 255, 255, 0.12)'
: 'rgba(0, 0, 0, 0.12)';
await updateWatermark({
content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
advancedStyle: {
colorStops: [
{
color: watermarkColor,
offset: 0,
},
{
color: watermarkColor,
offset: 1,
},
],
type: 'linear',
},
content:
content ||
`${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
});
} else {
destroyWatermark();
@ -132,21 +226,23 @@ watch(
description="ann.vben@gmail.com"
tag-text="Pro"
@logout="handleLogout"
/>
@clear-preferences-and-logout="handleLogout" />
</template>
<template #notification>
<Notification
:dot="showDot"
:notifications="notifications"
@clear="handleNoticeClear"
@read="(item) => item.id && markRead(item.id)"
@remove="(item) => item.id && remove(item.id)"
@make-all="handleMakeAll"
/>
@on-click="handleClick"
@view-all="viewAll" />
</template>
<template #extra>
<AuthenticationLoginExpiredModal
v-model:open="accessStore.loginExpired"
:avatar
>
:avatar>
<LoginForm />
</AuthenticationLoginExpiredModal>
</template>

4
apps/vben5/apps/web-antd/src/locales/langs/en-US/demos.json

@ -6,7 +6,9 @@
"about": "About",
"document": "Document",
"antdv": "Ant Design Vue Version",
"antdv-next": "Antdv Next Version",
"naive-ui": "Naive UI Version",
"element-plus": "Element Plus Version"
"element-plus": "Element Plus Version",
"tdesign": "TDesign Vue Version"
}
}

3
apps/vben5/apps/web-antd/src/locales/langs/en-US/page.json

@ -4,7 +4,8 @@
"register": "Register",
"codeLogin": "Code Login",
"qrcodeLogin": "Qr Code Login",
"forgetPassword": "Forget Password"
"forgetPassword": "Forget Password",
"profile": "Profile"
},
"dashboard": {
"title": "Dashboard",

4
apps/vben5/apps/web-antd/src/locales/langs/zh-CN/demos.json

@ -6,7 +6,9 @@
"about": "关于",
"document": "文档",
"antdv": "Ant Design Vue 版本",
"antdv-next": "Antdv Next 版本",
"naive-ui": "Naive UI 版本",
"element-plus": "Element Plus 版本"
"element-plus": "Element Plus 版本",
"tdesign": "TDesign Vue 版本"
}
}

3
apps/vben5/apps/web-antd/src/locales/langs/zh-CN/page.json

@ -4,7 +4,8 @@
"register": "注册",
"codeLogin": "验证码登录",
"qrcodeLogin": "二维码登录",
"forgetPassword": "忘记密码"
"forgetPassword": "忘记密码",
"profile": "个人中心"
},
"dashboard": {
"title": "概览",

3
apps/vben5/apps/web-antd/src/main.ts

@ -1,7 +1,7 @@
import { initPreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils';
import { overridesPreferences } from './preferences';
import { overridesPreferences, preferencesExtension } from './preferences';
/**
*
@ -15,6 +15,7 @@ async function initApplication() {
// app偏好设置初始化
await initPreferences({
extension: preferencesExtension,
namespace,
overrides: overridesPreferences,
});

63
apps/vben5/apps/web-antd/src/preferences.ts

@ -1,4 +1,15 @@
import { defineOverridesPreferences } from '@vben/preferences';
import {
appCopyrightPreferences,
defineOverridesPreferences,
definePreferencesExtension,
} from '@vben/preferences';
interface WebAntdPreferencesExtension {
defaultTableSize: number;
enableFormFullscreen: boolean;
reportTitle: string;
tenantMode: 'multi' | 'single';
}
/**
* @description
@ -10,4 +21,54 @@ export const overridesPreferences = defineOverridesPreferences({
app: {
name: import.meta.env.VITE_APP_TITLE,
},
copyright: appCopyrightPreferences,
});
export const preferencesExtension =
definePreferencesExtension<WebAntdPreferencesExtension>({
tabLabel: 'preferences.antd.tabLabel',
title: 'preferences.antd.title',
fields: [
{
component: 'switch',
defaultValue: true,
key: 'enableFormFullscreen',
label: 'preferences.antd.fields.enableFormFullscreen.label',
tip: 'preferences.antd.fields.enableFormFullscreen.tip',
},
{
component: 'select',
defaultValue: 'single',
key: 'tenantMode',
label: 'preferences.antd.fields.tenantMode.label',
options: [
{
label: 'preferences.antd.fields.tenantMode.options.single.label',
value: 'single',
},
{
label: 'preferences.antd.fields.tenantMode.options.multi.label',
value: 'multi',
},
],
},
{
component: 'number',
componentProps: {
max: 200,
min: 10,
step: 10,
},
defaultValue: 20,
key: 'defaultTableSize',
label: 'preferences.antd.fields.defaultTableSize.label',
},
{
component: 'input',
defaultValue: '',
key: 'reportTitle',
label: 'preferences.antd.fields.reportTitle.label',
placeholder: 'preferences.antd.fields.reportTitle.placeholder',
},
],
});

35
apps/vben5/apps/web-antd/src/router/routes/modules/vben.ts

@ -1,12 +1,15 @@
import type { RouteRecordRaw } from 'vue-router';
import {
VBEN_ANTDV_NEXT_PREVIEW_URL,
VBEN_DOC_URL,
VBEN_ELE_PREVIEW_URL,
VBEN_GITHUB_URL,
VBEN_LOGO_URL,
VBEN_NAIVE_PREVIEW_URL,
VBEN_TD_PREVIEW_URL,
} from '@vben/constants';
import { SvgAntdvNextLogoIcon, SvgTDesignIcon } from '@vben/icons';
import { IFrameView } from '#/layouts';
import { $t } from '#/locales';
@ -42,6 +45,17 @@ const routes: RouteRecordRaw[] = [
title: 'Github',
},
},
{
name: 'VbenAntdVNext',
path: '/vben-admin/antdv-next',
component: IFrameView,
meta: {
badgeType: 'dot',
icon: SvgAntdvNextLogoIcon,
link: VBEN_ANTDV_NEXT_PREVIEW_URL,
title: $t('demos.vben.antdv-next'),
},
},
{
name: 'VbenNaive',
path: '/vben-admin/naive',
@ -53,6 +67,17 @@ const routes: RouteRecordRaw[] = [
title: $t('demos.vben.naive-ui'),
},
},
{
name: 'VbenTDesign',
path: '/vben-admin/tdesign',
component: IFrameView,
meta: {
badgeType: 'dot',
icon: SvgTDesignIcon,
link: VBEN_TD_PREVIEW_URL,
title: $t('demos.vben.tdesign'),
},
},
{
name: 'VbenElementPlus',
path: '/vben-admin/ele',
@ -76,6 +101,16 @@ const routes: RouteRecordRaw[] = [
order: 9999,
},
},
{
name: 'Profile',
path: '/profile',
component: () => import('#/views/_core/profile/index.vue'),
meta: {
icon: 'lucide:user',
hideInMenu: true,
title: $t('page.auth.profile'),
},
},
];
export default routes;

3
apps/vben5/apps/web-antd/src/store/auth.ts

@ -98,8 +98,7 @@ export const useAuthStore = defineStore('auth', () => {
}
async function fetchUserInfo() {
let userInfo: null | UserInfo = null;
userInfo = await getUserInfoApi();
const userInfo = await getUserInfoApi();
userStore.setUserInfo(userInfo);
return userInfo;
}

6
apps/vben5/apps/web-antd/src/views/_core/authentication/code-login.vue

@ -55,8 +55,7 @@ const formSchema = computed((): VbenFormSchema[] => {
* @param values 登录表单数据
*/
async function handleLogin(values: Recordable<any>) {
// eslint-disable-next-line no-console
console.log(values);
void values;
}
</script>
@ -64,6 +63,5 @@ async function handleLogin(values: Recordable<any>) {
<AuthenticationCodeLogin
:form-schema="formSchema"
:loading="loading"
@submit="handleLogin"
/>
@submit="handleLogin" />
</template>

6
apps/vben5/apps/web-antd/src/views/_core/authentication/forget-password.vue

@ -29,8 +29,7 @@ const formSchema = computed((): VbenFormSchema[] => {
});
function handleSubmit(value: Recordable<any>) {
// eslint-disable-next-line no-console
console.log('reset email:', value);
void value;
}
</script>
@ -38,6 +37,5 @@ function handleSubmit(value: Recordable<any>) {
<AuthenticationForgetPassword
:form-schema="formSchema"
:loading="loading"
@submit="handleSubmit"
/>
@submit="handleSubmit" />
</template>

3
apps/vben5/apps/web-antd/src/views/_core/authentication/login.vue

@ -93,6 +93,5 @@ const formSchema = computed((): VbenFormSchema[] => {
<AuthenticationLogin
:form-schema="formSchema"
:loading="authStore.loginLoading"
@submit="authStore.authLogin"
/>
@submit="authStore.authLogin" />
</template>

6
apps/vben5/apps/web-antd/src/views/_core/authentication/register.vue

@ -82,8 +82,7 @@ const formSchema = computed((): VbenFormSchema[] => {
});
function handleSubmit(value: Recordable<any>) {
// eslint-disable-next-line no-console
console.log('register submit:', value);
void value;
}
</script>
@ -91,6 +90,5 @@ function handleSubmit(value: Recordable<any>) {
<AuthenticationRegister
:form-schema="formSchema"
:loading="loading"
@submit="handleSubmit"
/>
@submit="handleSubmit" />
</template>

65
apps/vben5/apps/web-antd/src/views/_core/profile/base-setting.vue

@ -0,0 +1,65 @@
<script setup lang="ts">
import type { BasicOption } from '@vben/types';
import type { VbenFormSchema } from '#/adapter/form';
import { computed, onMounted, ref } from 'vue';
import { ProfileBaseSetting } from '@vben/common-ui';
import { getUserInfoApi } from '#/api';
const profileBaseSettingRef = ref();
const MOCK_ROLES_OPTIONS: BasicOption[] = [
{
label: '管理员',
value: 'super',
},
{
label: '用户',
value: 'user',
},
{
label: '测试',
value: 'test',
},
];
const formSchema = computed((): VbenFormSchema[] => {
return [
{
fieldName: 'realName',
component: 'Input',
label: '姓名',
},
{
fieldName: 'username',
component: 'Input',
label: '用户名',
},
{
fieldName: 'roles',
component: 'Select',
componentProps: {
mode: 'tags',
options: MOCK_ROLES_OPTIONS,
},
label: '角色',
},
{
fieldName: 'introduction',
component: 'Textarea',
label: '个人简介',
},
];
});
onMounted(async () => {
const data = await getUserInfoApi();
profileBaseSettingRef.value.getFormApi().setValues(data);
});
</script>
<template>
<ProfileBaseSetting ref="profileBaseSettingRef" :form-schema="formSchema" />
</template>

48
apps/vben5/apps/web-antd/src/views/_core/profile/index.vue

@ -0,0 +1,48 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Profile } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import ProfileBase from './base-setting.vue';
import ProfileNotificationSetting from './notification-setting.vue';
import ProfilePasswordSetting from './password-setting.vue';
import ProfileSecuritySetting from './security-setting.vue';
const userStore = useUserStore();
const tabsValue = ref<string>('basic');
const tabs = ref([
{
label: '基本设置',
value: 'basic',
},
{
label: '安全设置',
value: 'security',
},
{
label: '修改密码',
value: 'password',
},
{
label: '新消息提醒',
value: 'notice',
},
]);
</script>
<template>
<Profile
v-model:model-value="tabsValue"
title="个人中心"
:user-info="userStore.userInfo"
:tabs="tabs">
<template #content>
<ProfileBase v-if="tabsValue === 'basic'" />
<ProfileSecuritySetting v-if="tabsValue === 'security'" />
<ProfilePasswordSetting v-if="tabsValue === 'password'" />
<ProfileNotificationSetting v-if="tabsValue === 'notice'" />
</template>
</Profile>
</template>

31
apps/vben5/apps/web-antd/src/views/_core/profile/notification-setting.vue

@ -0,0 +1,31 @@
<script setup lang="ts">
import { computed } from 'vue';
import { ProfileNotificationSetting } from '@vben/common-ui';
const formSchema = computed(() => {
return [
{
value: true,
fieldName: 'accountPassword',
label: '账户密码',
description: '其他用户的消息将以站内信的形式通知',
},
{
value: true,
fieldName: 'systemMessage',
label: '系统消息',
description: '系统消息将以站内信的形式通知',
},
{
value: true,
fieldName: 'todoTask',
label: '待办任务',
description: '待办任务将以站内信的形式通知',
},
];
});
</script>
<template>
<ProfileNotificationSetting :form-schema="formSchema" />
</template>

62
apps/vben5/apps/web-antd/src/views/_core/profile/password-setting.vue

@ -0,0 +1,62 @@
<script setup lang="ts">
import type { VbenFormSchema } from '#/adapter/form';
import { computed } from 'vue';
import { ProfilePasswordSetting, z } from '@vben/common-ui';
import { message } from 'ant-design-vue';
const formSchema = computed((): VbenFormSchema[] => {
return [
{
fieldName: 'oldPassword',
label: '旧密码',
component: 'VbenInputPassword',
componentProps: {
placeholder: '请输入旧密码',
},
},
{
fieldName: 'newPassword',
label: '新密码',
component: 'VbenInputPassword',
componentProps: {
passwordStrength: true,
placeholder: '请输入新密码',
},
},
{
fieldName: 'confirmPassword',
label: '确认密码',
component: 'VbenInputPassword',
componentProps: {
passwordStrength: true,
placeholder: '请再次输入新密码',
},
dependencies: {
rules(values) {
const { newPassword } = values;
return z
.string({ required_error: '请再次输入新密码' })
.min(1, { message: '请再次输入新密码' })
.refine((value) => value === newPassword, {
message: '两次输入的密码不一致',
});
},
triggerFields: ['newPassword'],
},
},
];
});
function handleSubmit() {
message.success('密码修改成功');
}
</script>
<template>
<ProfilePasswordSetting
class="w-1/3"
:form-schema="formSchema"
@submit="handleSubmit" />
</template>

43
apps/vben5/apps/web-antd/src/views/_core/profile/security-setting.vue

@ -0,0 +1,43 @@
<script setup lang="ts">
import { computed } from 'vue';
import { ProfileSecuritySetting } from '@vben/common-ui';
const formSchema = computed(() => {
return [
{
value: true,
fieldName: 'accountPassword',
label: '账户密码',
description: '当前密码强度:强',
},
{
value: true,
fieldName: 'securityPhone',
label: '密保手机',
description: '已绑定手机:138****8293',
},
{
value: true,
fieldName: 'securityQuestion',
label: '密保问题',
description: '未设置密保问题,密保问题可有效保护账户安全',
},
{
value: true,
fieldName: 'securityEmail',
label: '备用邮箱',
description: '已绑定邮箱:ant***sign.com',
},
{
value: false,
fieldName: 'securityMfa',
label: 'MFA 设备',
description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
},
];
});
</script>
<template>
<ProfileSecuritySetting :form-schema="formSchema" />
</template>

2
apps/vben5/apps/web-antd/src/views/dashboard/analytics/analytics-visits-sales.vue

@ -24,7 +24,7 @@ onMounted(() => {
{ name: '定制', value: 310 },
{ name: '技术支持', value: 274 },
{ name: '远程', value: 400 },
].sort((a, b) => {
].toSorted((a, b) => {
return a.value - b.value;
}),
name: '商业占比',

4
apps/vben5/apps/web-antd/src/views/dashboard/analytics/index.vue

@ -76,10 +76,10 @@ const chartTabs: TabOption[] = [
</AnalysisChartsTabs>
<div class="mt-5 w-full md:flex">
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
<AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问数量">
<AnalyticsVisitsData />
</AnalysisChartCard>
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
<AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问来源">
<AnalyticsVisitsSource />
</AnalysisChartCard>
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">

10
apps/vben5/apps/web-antd/src/views/dashboard/workspace/index.vue

@ -236,15 +236,14 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
<template>
<div class="p-5">
<WorkbenchHeader
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
>
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar">
<template #title>
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧
</template>
<template #description> 今日晴20 - 32 </template>
</WorkbenchHeader>
<div class="mt-5 flex flex-col lg:flex-row">
<div class="flex flex-col lg:flex-row">
<div class="mr-4 w-full lg:w-3/5">
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
@ -252,10 +251,9 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
<div class="w-full lg:w-2/5">
<WorkbenchQuickNav
:items="quickNavItems"
class="mt-5 lg:mt-0"
class="lg:mt-0"
title="快捷导航"
@click="navTo"
/>
@click="navTo" />
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
<AnalysisChartCard class="mt-5" title="访问来源">
<AnalyticsVisitsSource />

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save