Browse Source

Merge 02433a87b3 into 692dc0fa17

pull/1529/merge
yx lin 1 day 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 package-lock.json
.VSCodeCounter .VSCodeCounter
**/backend-mock/data **/backend-mock/data
.omx
.pnpm-store
# local env files # local env files
.env.local .env.local
.env.*.local .env.*.local
@ -51,3 +52,12 @@ vite.config.ts.*
*.sw? *.sw?
.history .history
.cursor .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 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 public
__tests__ __tests__
coverage coverage
packages/@abp/components .codex
.claude
.agent
.agents

10
apps/vben5/.vscode/extensions.json

@ -2,14 +2,18 @@
"recommendations": [ "recommendations": [
// Vue 3 // Vue 3
"Vue.volar", "Vue.volar",
// ESLint JavaScript VS Code // eslint VS Code
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
// oxlint VS Code
"oxc.oxc-vscode",
// Visual Studio Code Stylelint // Visual Studio Code Stylelint
"stylelint.vscode-stylelint", "stylelint.vscode-stylelint",
// 使 Prettier // 使 oxfmt
"esbenp.prettier-vscode", "oxc.oxc-vscode",
// dotenv // dotenv
"mikestead.dotenv", "mikestead.dotenv",
// YAML ESLint pnpm-workspace.yaml
"redhat.vscode-yaml",
// //
"streetsidesoftware.code-spell-checker", "streetsidesoftware.code-spell-checker",
// Tailwind CSS VS Code // 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
"workbench.list.smoothScrolling": true, "workbench.list.smoothScrolling": true,
"workbench.startupEditor": "newUntitledFile", "workbench.startupEditor": "newUntitledFile",
@ -31,39 +32,51 @@
"editor.autoClosingOvertype": "always", "editor.autoClosingOvertype": "always",
"editor.autoClosingQuotes": "beforeWhitespace", "editor.autoClosingQuotes": "beforeWhitespace",
"editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?", "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": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit", "source.fixAll.eslint": "explicit",
"source.fixAll.oxc": "explicit",
"source.fixAll.stylelint": "explicit", "source.fixAll.stylelint": "explicit",
"source.organizeImports": "never" "source.organizeImports": "never"
}, },
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "oxc.oxc-vscode",
"[html]": { "[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
"[css]": { "[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
"[scss]": { "[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
"[json]": { "[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
"[markdown]": { "[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
"[jsonc]": { "[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
// extensions // extensions
"extensions.ignoreRecommendations": true, "extensions.ignoreRecommendations": true,
@ -79,6 +92,7 @@
"files.insertFinalNewline": true, "files.insertFinalNewline": true,
"files.simpleDialog.enable": true, "files.simpleDialog.enable": true,
"files.associations": { "files.associations": {
"*.css": "tailwindcss",
"*.ejs": "html", "*.ejs": "html",
"*.art": "html", "*.art": "html",
"**/tsconfig.json": "jsonc", "**/tsconfig.json": "jsonc",
@ -118,7 +132,7 @@
// search // search
"search.searchEditor.singleClickBehaviour": "peekDefinition", "search.searchEditor.singleClickBehaviour": "peekDefinition",
"search.followSymlinks": false, "search.followSymlinks": false,
// 使/ // 使
"search.exclude": { "search.exclude": {
"**/node_modules": true, "**/node_modules": true,
"**/*.log": true, "**/*.log": true,
@ -158,7 +172,7 @@
"emmet.triggerExpansionOnTab": false, "emmet.triggerExpansionOnTab": false,
"errorLens.enabledDiagnosticLevels": ["warning", "error"], "errorLens.enabledDiagnosticLevels": ["warning", "error"],
"errorLens.excludeBySource": ["cSpell", "Grammarly", "eslint"], "errorLens.excludeBySource": ["cSpell", "Grammarly"],
"stylelint.enable": true, "stylelint.enable": true,
"stylelint.packageManager": "pnpm", "stylelint.packageManager": "pnpm",
@ -166,9 +180,10 @@
"stylelint.customSyntax": "postcss-html", "stylelint.customSyntax": "postcss-html",
"stylelint.snippet": ["css", "less", "postcss", "scss", "vue"], "stylelint.snippet": ["css", "less", "postcss", "scss", "vue"],
"typescript.inlayHints.enumMemberValues.enabled": true, "js/ts.tsdk.path": "node_modules/typescript/lib",
"typescript.preferences.preferTypeOnlyAutoImports": true, "js/ts.inlayHints.enumMemberValues.enabled": true,
"typescript.preferences.includePackageJsonAutoImports": "on", "js/ts.preferences.preferTypeOnlyAutoImports": true,
"js/ts.preferences.includePackageJsonAutoImports": "on",
"eslint.validate": [ "eslint.validate": [
"javascript", "javascript",
@ -195,7 +210,7 @@
"yaml": false "yaml": false
}, },
"cssVariables.lookupFiles": ["packages/core/base/design/src/**/*.css"], "cssVariables.lookupFiles": ["packages/@core/base/design/src/**/*.css"],
"i18n-ally.localesPaths": [ "i18n-ally.localesPaths": [
"packages/locales/src/langs", "packages/locales/src/langs",
@ -220,12 +235,9 @@
"*.env": "$(capture).env.*", "*.env": "$(capture).env.*",
"README.md": "README*,CHANGELOG*,LICENSE,CNAME", "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", "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", "oxlint.config.ts": ".eslintignore,.stylelintignore,.commitlintrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml,oxfmt.config.*,eslint.config.*"
"tailwind.config.mjs": "postcss.*"
}, },
"commentTranslate.hover.enabled": false, "commentTranslate.hover.enabled": false,
"commentTranslate.multiLineMerge": true, "commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": true, "vue.server.hybridMode": true
"typescript.tsdk": "node_modules/typescript/lib",
"oxc.enable": false
} }

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はサポートしません モダンブラウザをサポートし、IEはサポートしません

2
apps/vben5/README.md

@ -114,7 +114,7 @@ Reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.
## Browser Support ## 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 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 支持现代浏览器,不支持 IE

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

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

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

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

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

@ -9,8 +9,7 @@
<meta name="author" content="Vben" /> <meta name="author" content="Vben" />
<meta <meta
name="viewport" 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 文件内配置 --> <!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
<title><%= VITE_APP_TITLE %></title> <title><%= VITE_APP_TITLE %></title>
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />

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

@ -50,6 +50,7 @@
"@abp/ui": "workspace:*", "@abp/ui": "workspace:*",
"@abp/webhooks": "workspace:*", "@abp/webhooks": "workspace:*",
"@abp/wechat": "workspace:*", "@abp/wechat": "workspace:*",
"@tanstack/vue-query": "catalog:",
"@vben-core/shadcn-ui": "workspace:*", "@vben-core/shadcn-ui": "workspace:*",
"@vben/access": "workspace:*", "@vben/access": "workspace:*",
"@vben/common-ui": "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', Italic: 'Cursiva',
Underline: 'Subrayado', Underline: 'Subrayado',
Strikethrough: 'Tachado', Strikethrough: 'Tachado',
Superscript: 'Super\u00edndice', Superscript: 'Super\u00EDndice',
Subscript: 'Sub\u00edndice', Subscript: 'Sub\u00EDndice',
'Clear formatting': 'Limpiar formato', 'Clear formatting': 'Limpiar formato',
'Align left': 'Alinear a la izquierda', 'Align left': 'Alinear a la izquierda',
'Align center': 'Alinear al centro', 'Align center': 'Alinear al centro',
'Align right': 'Alinear a la derecha', 'Align right': 'Alinear a la derecha',
Justify: 'Justificar', Justify: 'Justificar',
'Bullet list': 'Lista de vi\u00f1etas', 'Bullet list': 'Lista de vi\u00F1etas',
'Numbered list': 'Lista numerada', 'Numbered list': 'Lista numerada',
'Decrease indent': 'Disminuir sangr\u00eda', 'Decrease indent': 'Disminuir sangr\u00EDa',
'Increase indent': 'Incrementar sangr\u00eda', 'Increase indent': 'Incrementar sangr\u00EDa',
Close: 'Cerrar', Close: 'Cerrar',
Formats: 'Formatos', 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', Headers: 'Encabezados',
'Header 1': 'Encabezado 1', 'Header 1': 'Encabezado 1',
'Header 2': 'Encabezado 2', 'Header 2': 'Encabezado 2',
@ -44,23 +44,23 @@ tinymce.addI18n('es', {
Preformatted: 'Con formato previo', Preformatted: 'Con formato previo',
Div: 'Div', Div: 'Div',
Pre: 'Pre', Pre: 'Pre',
Code: 'C\u00f3digo', Code: 'C\u00F3digo',
Paragraph: 'P\u00e1rrafo', Paragraph: 'P\u00E1rrafo',
Blockquote: 'Blockquote', Blockquote: 'Blockquote',
Inline: 'Alineado', Inline: 'Alineado',
Blocks: 'Bloques', 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', Fonts: 'Fuentes',
'Font Sizes': 'Tama\u00f1os de fuente', 'Font Sizes': 'Tama\u00F1os de fuente',
Class: 'Clase', Class: 'Clase',
'Browse for an image': 'Buscar una imagen', 'Browse for an image': 'Buscar una imagen',
OR: 'OR', OR: 'OR',
'Drop an image here': 'Arrastre una imagen aqu\u00ed', 'Drop an image here': 'Arrastre una imagen aqu\u00ED',
Upload: 'Cargar', Upload: 'Cargar',
Block: 'Bloque', Block: 'Bloque',
Align: 'Alinear', Align: 'Alinear',
Default: 'Por defecto', Default: 'Por defecto',
Circle: 'C\u00edrculo', Circle: 'C\u00EDrculo',
Disc: 'Disco', Disc: 'Disco',
Square: 'Cuadrado', Square: 'Cuadrado',
'Lower Alpha': 'Inferior Alfa', 'Lower Alpha': 'Inferior Alfa',
@ -71,14 +71,14 @@ tinymce.addI18n('es', {
'Anchor...': 'Anclaje...', 'Anchor...': 'Anclaje...',
Name: 'Nombre', Name: 'Nombre',
Id: 'Id', 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.', '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?', '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', 'Restore last draft': 'Restaurar el \u00FAltimo borrador',
'Special character...': 'Car\u00e1cter especial...', 'Special character...': 'Car\u00E1cter especial...',
'Source code': 'C\u00f3digo fuente', 'Source code': 'C\u00F3digo fuente',
'Insert\/Edit code sample': 'Insertar\/editar c\u00f3digo de prueba', 'Insert/Edit code sample': 'Insertar/editar c\u00F3digo de prueba',
Language: 'Idioma', Language: 'Idioma',
'Code sample...': 'Ejemplo de c\u00f3digo...', 'Code sample...': 'Ejemplo de c\u00F3digo...',
'Color Picker': 'Selector de colores', 'Color Picker': 'Selector de colores',
R: 'R', R: 'R',
G: 'V', G: 'V',
@ -87,33 +87,33 @@ tinymce.addI18n('es', {
'Right to left': 'De derecha a izquierda', 'Right to left': 'De derecha a izquierda',
'Emoticons...': 'Emoticones...', 'Emoticons...': 'Emoticones...',
'Metadata and Document Properties': 'Metadatos y propiedades del documento', 'Metadata and Document Properties': 'Metadatos y propiedades del documento',
Title: 'T\u00edtulo', Title: 'T\u00EDtulo',
Keywords: 'Palabras clave', Keywords: 'Palabras clave',
Description: 'Descripci\u00f3n', Description: 'Descripci\u00F3n',
Robots: 'Robots', Robots: 'Robots',
Author: 'Autor', Author: 'Autor',
Encoding: 'Codificaci\u00f3n', Encoding: 'Codificaci\u00F3n',
Fullscreen: 'Pantalla completa', Fullscreen: 'Pantalla completa',
Action: 'Acci\u00f3n', Action: 'Acci\u00F3n',
Shortcut: 'Atajo', Shortcut: 'Atajo',
Help: 'Ayuda', Help: 'Ayuda',
Address: 'Direcci\u00f3n', Address: 'Direcci\u00F3n',
'Focus to menubar': 'Enfocar la barra del men\u00fa', 'Focus to menubar': 'Enfocar la barra del men\u00FA',
'Focus to toolbar': 'Enfocar la barra de herramientas', 'Focus to toolbar': 'Enfocar la barra de herramientas',
'Focus to element path': 'Enfocar la ruta del elemento', 'Focus to element path': 'Enfocar la ruta del elemento',
'Focus to contextual toolbar': 'Enfocar la barra de herramientas contextual', '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)', '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)', '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)', 'Find (if searchreplace plugin activated)': 'Buscar (si el complemento buscar-remplazar est\u00E1 activado)',
'Plugins installed ({0}):': 'Plugins instalados ({0}):', 'Plugins installed ({0}):': 'Plugins instalados ({0}):',
'Premium plugins:': 'Complementos premium:', 'Premium plugins:': 'Complementos premium:',
'Learn more...': 'Aprende m\u00e1s...', 'Learn more...': 'Aprende m\u00E1s...',
'You are using {0}': 'Estas usando {0}', 'You are using {0}': 'Estas usando {0}',
Plugins: 'Complementos', Plugins: 'Complementos',
'Handy Shortcuts': 'Accesos directos', 'Handy Shortcuts': 'Accesos directos',
'Horizontal line': 'L\u00ednea horizontal', 'Horizontal line': 'L\u00EDnea horizontal',
'Insert\/edit image': 'Insertar\/editar imagen', 'Insert/edit image': 'Insertar/editar imagen',
'Image description': 'Descripci\u00f3n de la imagen', 'Image description': 'Descripci\u00F3n de la imagen',
Source: 'Enlace', Source: 'Enlace',
Dimensions: 'Dimensiones', Dimensions: 'Dimensiones',
'Constrain proportions': 'Restringir proporciones', 'Constrain proportions': 'Restringir proporciones',
@ -125,7 +125,7 @@ tinymce.addI18n('es', {
Border: 'Borde', Border: 'Borde',
'Insert image': 'Insertar imagen', 'Insert image': 'Insertar imagen',
'Image...': 'Imagen...', 'Image...': 'Imagen...',
'Image list': 'Lista de im\u00e1genes', 'Image list': 'Lista de im\u00E1genes',
'Rotate counterclockwise': 'Girar a la izquierda', 'Rotate counterclockwise': 'Girar a la izquierda',
'Rotate clockwise': 'Girar a la derecha', 'Rotate clockwise': 'Girar a la derecha',
'Flip vertically': 'Invertir verticalmente', 'Flip vertically': 'Invertir verticalmente',
@ -136,7 +136,7 @@ tinymce.addI18n('es', {
'Zoom out': 'Alejar', 'Zoom out': 'Alejar',
Crop: 'Recortar', Crop: 'Recortar',
Resize: 'Redimensionar', Resize: 'Redimensionar',
Orientation: 'Orientaci\u00f3n', Orientation: 'Orientaci\u00F3n',
Brightness: 'Brillo', Brightness: 'Brillo',
Sharpen: 'Forma', Sharpen: 'Forma',
Contrast: 'Contraste', Contrast: 'Contraste',
@ -144,11 +144,11 @@ tinymce.addI18n('es', {
Gamma: 'Gamma', Gamma: 'Gamma',
Invert: 'Invertir', Invert: 'Invertir',
Apply: 'Aplicar', Apply: 'Aplicar',
Back: 'Atr\u00e1s', Back: 'Atr\u00E1s',
'Insert date\/time': 'Insertar fecha\/hora', 'Insert date/time': 'Insertar fecha/hora',
'Date\/time': 'Fecha\/hora', 'Date/time': 'Fecha/hora',
'Insert\/Edit Link': 'Insertar\/editar enlace', 'Insert/Edit Link': 'Insertar/editar enlace',
'Insert\/edit link': 'Insertar\/editar enlace', 'Insert/edit link': 'Insertar/editar enlace',
'Text to display': 'Texto para mostrar', 'Text to display': 'Texto para mostrar',
Url: 'URL', Url: 'URL',
'Open link in...': 'Abrir enlace en...', 'Open link in...': 'Abrir enlace en...',
@ -159,20 +159,20 @@ tinymce.addI18n('es', {
Anchors: 'Anclas', Anchors: 'Anclas',
'Link...': 'Enlace...', 'Link...': 'Enlace...',
'Paste or type a link': 'Pega o introduce un 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 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 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', 'Link list': 'Lista de enlaces',
'Insert video': 'Insertar video', 'Insert video': 'Insertar video',
'Insert\/edit video': 'Insertar\/editar video', 'Insert/edit video': 'Insertar/editar video',
'Insert\/edit media': 'Insertar\/editar medio', 'Insert/edit media': 'Insertar/editar medio',
'Alternative source': 'Enlace alternativo', 'Alternative source': 'Enlace alternativo',
'Alternative source URL': 'Origen de URL alternativo', 'Alternative source URL': 'Origen de URL alternativo',
'Media poster (Image URL)': 'P\u00f3ster de medio (URL de imagen)', 'Media poster (Image URL)': 'P\u00F3ster de medio (URL de imagen)',
'Paste your embed code below:': 'Pega tu c\u00f3digo embebido debajo', 'Paste your embed code below:': 'Pega tu c\u00F3digo embebido debajo',
Embed: 'Incrustado', Embed: 'Incrustado',
'Media...': 'Medios...', 'Media...': 'Medios...',
'Nonbreaking space': 'Espacio fijo', 'Nonbreaking space': 'Espacio fijo',
'Page break': 'Salto de p\u00e1gina', 'Page break': 'Salto de p\u00E1gina',
'Paste as text': 'Pegar como texto', 'Paste as text': 'Pegar como texto',
Preview: 'Previsualizar', Preview: 'Previsualizar',
'Print...': 'Imprimir...', 'Print...': 'Imprimir...',
@ -187,11 +187,11 @@ tinymce.addI18n('es', {
'Could not find the specified string.': 'No se encuentra la cadena de texto especificada', 'Could not find the specified string.': 'No se encuentra la cadena de texto especificada',
'Match case': 'Coincidencia exacta', 'Match case': 'Coincidencia exacta',
'Find whole words only': 'Solo palabras completas', 'Find whole words only': 'Solo palabras completas',
'Spell check': 'Revisar ortograf\u00eda', 'Spell check': 'Revisar ortograf\u00EDa',
Ignore: 'Ignorar', Ignore: 'Ignorar',
'Ignore all': 'Ignorar todos', 'Ignore all': 'Ignorar todos',
Finish: 'Finalizar', Finish: 'Finalizar',
'Add to Dictionary': 'A\u00f1adir al Diccionario', 'Add to Dictionary': 'A\u00F1adir al Diccionario',
'Insert table': 'Insertar tabla', 'Insert table': 'Insertar tabla',
'Table properties': 'Propiedades de la tabla', 'Table properties': 'Propiedades de la tabla',
'Delete table': 'Eliminar tabla', 'Delete table': 'Eliminar tabla',
@ -202,15 +202,15 @@ tinymce.addI18n('es', {
'Merge cells': 'Combinar celdas', 'Merge cells': 'Combinar celdas',
'Split cell': 'Dividir celdas', 'Split cell': 'Dividir celdas',
'Insert row before': 'Insertar fila antes', 'Insert row before': 'Insertar fila antes',
'Insert row after': 'Insertar fila despu\u00e9s ', 'Insert row after': 'Insertar fila despu\u00E9s ',
'Delete row': 'Eliminar fila', 'Delete row': 'Eliminar fila',
'Row properties': 'Propiedades de la fila', 'Row properties': 'Propiedades de la fila',
'Cut row': 'Cortar fila', 'Cut row': 'Cortar fila',
'Copy row': 'Copiar fila', 'Copy row': 'Copiar fila',
'Paste row before': 'Pegar la fila antes', '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 before': 'Insertar columna antes',
'Insert column after': 'Insertar columna despu\u00e9s', 'Insert column after': 'Insertar columna despu\u00E9s',
'Delete column': 'Eliminar columna', 'Delete column': 'Eliminar columna',
Cols: 'Columnas', Cols: 'Columnas',
Rows: 'Filas', Rows: 'Filas',
@ -218,13 +218,13 @@ tinymce.addI18n('es', {
Height: 'Alto', Height: 'Alto',
'Cell spacing': 'Espacio entre celdas', 'Cell spacing': 'Espacio entre celdas',
'Cell padding': 'Relleno de celda', 'Cell padding': 'Relleno de celda',
'Show caption': 'Mostrar t\u00edtulo', 'Show caption': 'Mostrar t\u00EDtulo',
Left: 'Izquierda', Left: 'Izquierda',
Center: 'Centrado', Center: 'Centrado',
Right: 'Derecha', Right: 'Derecha',
'Cell type': 'Tipo de celda', 'Cell type': 'Tipo de celda',
Scope: '\u00c1mbito', Scope: '\u00C1mbito',
Alignment: 'Alineaci\u00f3n', Alignment: 'Alineaci\u00F3n',
'H Align': 'Alineamiento Horizontal', 'H Align': 'Alineamiento Horizontal',
'V Align': 'Alineamiento Vertical', 'V Align': 'Alineamiento Vertical',
Top: 'Arriba', Top: 'Arriba',
@ -236,7 +236,7 @@ tinymce.addI18n('es', {
'Row type': 'Tipo de fila', 'Row type': 'Tipo de fila',
Header: 'Cabecera', Header: 'Cabecera',
Body: 'Cuerpo', Body: 'Cuerpo',
Footer: 'Pie de p\u00e1gina', Footer: 'Pie de p\u00E1gina',
'Border color': 'Color del borde', 'Border color': 'Color del borde',
'Insert template...': 'Insertar plantilla...', 'Insert template...': 'Insertar plantilla...',
Templates: 'Plantillas', Templates: 'Plantillas',
@ -253,7 +253,7 @@ tinymce.addI18n('es', {
'Word count': 'Contar palabras', 'Word count': 'Contar palabras',
Count: 'Recuento', Count: 'Recuento',
Document: 'Documento', Document: 'Documento',
Selection: 'Selecci\u00f3n', Selection: 'Selecci\u00F3n',
Words: 'Palabras', Words: 'Palabras',
'Words: {0}': 'Palabras: {0}', 'Words: {0}': 'Palabras: {0}',
'{0} words': '{0} palabras', '{0} words': '{0} palabras',
@ -265,15 +265,15 @@ tinymce.addI18n('es', {
Table: 'Tabla', Table: 'Tabla',
Tools: 'Herramientas', Tools: 'Herramientas',
'Powered by {0}': 'Desarrollado por {0}', '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', 'Image title': 'Titulo de imagen',
'Border width': 'Ancho de borde', 'Border width': 'Ancho de borde',
'Border style': 'Estilo de borde', 'Border style': 'Estilo de borde',
Error: 'Error', Error: 'Error',
Warn: 'Advertencia', Warn: 'Advertencia',
Valid: 'V\u00e1lido', Valid: 'V\u00E1lido',
'To open the popup, press Shift+Enter': 'Para abrir el elemento emergente, pulse May\u00fas+Intro', '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.', 'Rich Text Area. Press ALT-0 for help.': '\u00C1rea de texto enriquecido. Pulse ALT-0 para abrir la ayuda.',
'System Font': 'Fuente de sistema', 'System Font': 'Fuente de sistema',
'Failed to upload image: {0}': 'Fallo al cargar imagen: {0}', '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}', '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', Currency: 'Divisa',
Text: 'Texto', Text: 'Texto',
Quotations: 'Comillas', Quotations: 'Comillas',
Mathematical: 'S\u00edmbolo matem\u00e1tico', Mathematical: 'S\u00EDmbolo matem\u00E1tico',
'Extended Latin': 'Latino extendido A', 'Extended Latin': 'Latino extendido A',
Symbols: 'S\u00edmbolos', Symbols: 'S\u00EDmbolos',
Arrows: 'Flechas', Arrows: 'Flechas',
'User Defined': 'Definido por el usuario', 'User Defined': 'Definido por el usuario',
'dollar sign': 'signo de d\u00f3lar', 'dollar sign': 'signo de d\u00F3lar',
'currency sign': 'signo de divisa', 'currency sign': 'signo de divisa',
'euro-currency sign': 'signo de euro', 'euro-currency sign': 'signo de euro',
'colon sign': 'signo de dos puntos', 'colon sign': 'signo de dos puntos',
'cruzeiro sign': 'signo de cruceiro', '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', 'lira sign': 'signo de lira',
'mill sign': 'signo de mill', 'mill sign': 'signo de mill',
'naira sign': 'signo de naira', 'naira sign': 'signo de naira',
'peseta sign': 'signo de peseta', 'peseta sign': 'signo de peseta',
'rupee sign': 'signo de rupia', 'rupee sign': 'signo de rupia',
'won sign': 'signo de won', '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', 'dong sign': 'signo de dong',
'kip sign': 'signo de kip', 'kip sign': 'signo de kip',
'tugrik sign': 'signo de tugrik', 'tugrik sign': 'signo de tugrik',
'drachma sign': 'signo de dracma', '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', 'peso sign': 'signo de peso',
'guarani sign': 'signo de guaran\u00ed', 'guarani sign': 'signo de guaran\u00ED',
'austral sign': 'signo de austral', 'austral sign': 'signo de austral',
'hryvnia sign': 'signo de grivna', 'hryvnia sign': 'signo de grivna',
'cedi sign': 'signo de cedi', 'cedi sign': 'signo de cedi',
@ -318,13 +318,13 @@ tinymce.addI18n('es', {
'tenge sign': 'signo de tenge', 'tenge sign': 'signo de tenge',
'indian rupee sign': 'signo de rupia india', 'indian rupee sign': 'signo de rupia india',
'turkish lira sign': 'signo de lira turca', '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', 'manat sign': 'signo de manat',
'ruble sign': 'signo de rublo', 'ruble sign': 'signo de rublo',
'yen character': 'car\u00e1cter de yen', 'yen character': 'car\u00E1cter de yen',
'yuan character': 'car\u00e1cter de yuan', 'yuan character': 'car\u00E1cter de yuan',
'yuan character, in hong kong and taiwan': 'car\u00e1cter de yuan en Hong Kong y Taiw\u00e1n', '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/yuan character variant one': 'Variante uno de car\u00E1cter de yen/yuan',
'Loading emoticons...': 'Cargando emoticonos...', 'Loading emoticons...': 'Cargando emoticonos...',
'Could not load emoticons': 'No se han podido cargar los emoticonos', 'Could not load emoticons': 'No se han podido cargar los emoticonos',
People: 'Personas', People: 'Personas',
@ -337,19 +337,19 @@ tinymce.addI18n('es', {
Characters: 'Caracteres', Characters: 'Caracteres',
'Characters (no spaces)': 'Caracteres (sin espacios)', 'Characters (no spaces)': 'Caracteres (sin espacios)',
'{0} characters': '{0} caracteres', '{0} characters': '{0} caracteres',
'Error: Form submit field collision.': 'Error: Colisi\u00f3n de campo al enviar 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.', 'Error: No form element found.': 'Error: No se encuentra ning\u00FAn elemento de formulario.',
Update: 'Actualizar', Update: 'Actualizar',
'Color swatch': 'Muestrario de colores', 'Color swatch': 'Muestrario de colores',
Turquoise: 'Turquesa', Turquoise: 'Turquesa',
Green: 'Verde', Green: 'Verde',
Blue: 'Azul', Blue: 'Azul',
Purple: 'P\u00farpura', Purple: 'P\u00FArpura',
'Navy Blue': 'Azul marino', 'Navy Blue': 'Azul marino',
'Dark Turquoise': 'Turquesa oscuro', 'Dark Turquoise': 'Turquesa oscuro',
'Dark Green': 'Verde oscuro', 'Dark Green': 'Verde oscuro',
'Medium Blue': 'Azul medio', 'Medium Blue': 'Azul medio',
'Medium Purple': 'P\u00farpura medio', 'Medium Purple': 'P\u00FArpura medio',
'Midnight Blue': 'Azul medio', 'Midnight Blue': 'Azul medio',
Yellow: 'Amarillo', Yellow: 'Amarillo',
Orange: 'Naranja', Orange: 'Naranja',
@ -371,35 +371,35 @@ tinymce.addI18n('es', {
Black: 'Negro', Black: 'Negro',
White: 'Blanco', White: 'Blanco',
'Switch to or from fullscreen mode': 'Activar o desactivar modo pantalla completa', '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', history: 'historial',
styles: 'estilos', styles: 'estilos',
formatting: 'formato', formatting: 'formato',
alignment: 'alineaci\u00f3n', alignment: 'alineaci\u00F3n',
indentation: 'sangr\u00eda', indentation: 'sangr\u00EDa',
'permanent pen': 'bol\u00edgrafo permanente', 'permanent pen': 'bol\u00EDgrafo permanente',
comments: 'comentarios', comments: 'comentarios',
'Format Painter': 'Copiar formato', 'Format Painter': 'Copiar formato',
'Insert\/edit iframe': 'Insertar\/editar iframe', 'Insert/edit iframe': 'Insertar/editar iframe',
Capitalization: 'Uso de may\u00fasculas', Capitalization: 'Uso de may\u00FAsculas',
lowercase: 'min\u00fasculas', lowercase: 'min\u00FAsculas',
UPPERCASE: 'MAY\u00daSCULAS', UPPERCASE: 'MAY\u00DASCULAS',
'Title Case': 'Tipo T\u00edtulo', 'Title Case': 'Tipo T\u00EDtulo',
'Permanent Pen Properties': 'Propiedades del bol\u00edgrafo permanente', 'Permanent Pen Properties': 'Propiedades del bol\u00EDgrafo permanente',
'Permanent pen properties...': 'Propiedades del bol\u00edgrafo permanente...', 'Permanent pen properties...': 'Propiedades del bol\u00EDgrafo permanente...',
Font: 'Fuente', Font: 'Fuente',
Size: 'Tama\u00f1o', Size: 'Tama\u00F1o',
'More...': 'M\u00e1s...', 'More...': 'M\u00E1s...',
'Spellcheck Language': 'Corrector', 'Spellcheck Language': 'Corrector',
'Select...': 'Seleccionar...', 'Select...': 'Seleccionar...',
Preferences: 'Preferencias', Preferences: 'Preferencias',
Yes: 'S\u00ed', Yes: 'S\u00ED',
No: 'No', No: 'No',
'Keyboard Navigation': 'Navegaci\u00f3n con el teclado', 'Keyboard Navigation': 'Navegaci\u00F3n con el teclado',
Version: 'Versi\u00f3n', Version: 'Versi\u00F3n',
Anchor: 'Ancla', Anchor: 'Ancla',
'Special character': 'Car\u00e1cter especial', 'Special character': 'Car\u00E1cter especial',
'Code sample': 'Ejemplo de c\u00f3digo', 'Code sample': 'Ejemplo de c\u00F3digo',
Color: 'Color', Color: 'Color',
Emoticons: 'Emoticonos', Emoticons: 'Emoticonos',
'Document properties': 'Propiedades del documento', 'Document properties': 'Propiedades del documento',
@ -413,7 +413,7 @@ tinymce.addI18n('es', {
Prev: 'Anterior', Prev: 'Anterior',
'Find and replace': 'Buscar y reemplazar', 'Find and replace': 'Buscar y reemplazar',
'Whole words': 'Palabras completas', 'Whole words': 'Palabras completas',
Spellcheck: 'Corrector ortogr\u00e1fico', Spellcheck: 'Corrector ortogr\u00E1fico',
Caption: 'Subt\u00edtulo', Caption: 'Subt\u00EDtulo',
'Insert template': 'Insertar plantilla' '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',{ tinymce.addI18n('zh_CN',{
"Redo": "\u91cd\u505a", "Redo": "\u91CD\u505A",
"Undo": "\u64a4\u9500", "Undo": "\u64A4\u9500",
"Cut": "\u526a\u5207", "Cut": "\u526A\u5207",
"Copy": "\u590d\u5236", "Copy": "\u590D\u5236",
"Paste": "\u7c98\u8d34", "Paste": "\u7C98\u8D34",
"Select all": "\u5168\u9009", "Select all": "\u5168\u9009",
"New document": "\u65b0\u6587\u4ef6", "New document": "\u65B0\u6587\u4EF6",
"Ok": "\u786e\u5b9a", "Ok": "\u786E\u5B9A",
"Cancel": "\u53d6\u6d88", "Cancel": "\u53D6\u6D88",
"Visual aids": "\u7f51\u683c\u7ebf", "Visual aids": "\u7F51\u683C\u7EBF",
"Bold": "\u7c97\u4f53", "Bold": "\u7C97\u4F53",
"Italic": "\u659c\u4f53", "Italic": "\u659C\u4F53",
"Underline": "\u4e0b\u5212\u7ebf", "Underline": "\u4E0B\u5212\u7EBF",
"Strikethrough": "\u5220\u9664\u7ebf", "Strikethrough": "\u5220\u9664\u7EBF",
"Superscript": "\u4e0a\u6807", "Superscript": "\u4E0A\u6807",
"Subscript": "\u4e0b\u6807", "Subscript": "\u4E0B\u6807",
"Clear formatting": "\u6e05\u9664\u683c\u5f0f", "Clear formatting": "\u6E05\u9664\u683C\u5F0F",
"Align left": "\u5de6\u8fb9\u5bf9\u9f50", "Align left": "\u5DE6\u8FB9\u5BF9\u9F50",
"Align center": "\u4e2d\u95f4\u5bf9\u9f50", "Align center": "\u4E2D\u95F4\u5BF9\u9F50",
"Align right": "\u53f3\u8fb9\u5bf9\u9f50", "Align right": "\u53F3\u8FB9\u5BF9\u9F50",
"Justify": "\u4e24\u7aef\u5bf9\u9f50", "Justify": "\u4E24\u7AEF\u5BF9\u9F50",
"Bullet list": "\u9879\u76ee\u7b26\u53f7", "Bullet list": "\u9879\u76EE\u7B26\u53F7",
"Numbered list": "\u7f16\u53f7\u5217\u8868", "Numbered list": "\u7F16\u53F7\u5217\u8868",
"Decrease indent": "\u51cf\u5c11\u7f29\u8fdb", "Decrease indent": "\u51CF\u5C11\u7F29\u8FDB",
"Increase indent": "\u589e\u52a0\u7f29\u8fdb", "Increase indent": "\u589E\u52A0\u7F29\u8FDB",
"Close": "\u5173\u95ed", "Close": "\u5173\u95ED",
"Formats": "\u683c\u5f0f", "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", "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", "Headers": "\u6807\u9898",
"Header 1": "\u6807\u98981", "Header 1": "\u6807\u98981",
"Header 2": "\u6807\u98982", "Header 2": "\u6807\u98982",
@ -41,349 +41,349 @@ tinymce.addI18n('zh_CN',{
"Heading 4": "\u6807\u98984", "Heading 4": "\u6807\u98984",
"Heading 5": "\u6807\u98985", "Heading 5": "\u6807\u98985",
"Heading 6": "\u6807\u98986", "Heading 6": "\u6807\u98986",
"Preformatted": "\u9884\u5148\u683c\u5f0f\u5316\u7684", "Preformatted": "\u9884\u5148\u683C\u5F0F\u5316\u7684",
"Div": "Div", "Div": "Div",
"Pre": "Pre", "Pre": "Pre",
"Code": "\u4ee3\u7801", "Code": "\u4EE3\u7801",
"Paragraph": "\u6bb5\u843d", "Paragraph": "\u6BB5\u843D",
"Blockquote": "\u5f15\u6587\u533a\u5757", "Blockquote": "\u5F15\u6587\u533A\u5757",
"Inline": "\u6587\u672c", "Inline": "\u6587\u672C",
"Blocks": "\u57fa\u5757", "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", "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", "Fonts": "\u5B57\u4F53",
"Font Sizes": "\u5b57\u53f7", "Font Sizes": "\u5B57\u53F7",
"Class": "\u7c7b\u578b", "Class": "\u7C7B\u578B",
"Browse for an image": "\u6d4f\u89c8\u56fe\u50cf", "Browse for an image": "\u6D4F\u89C8\u56FE\u50CF",
"OR": "\u6216", "OR": "\u6216",
"Drop an image here": "\u62d6\u653e\u4e00\u5f20\u56fe\u50cf\u81f3\u6b64", "Drop an image here": "\u62D6\u653E\u4E00\u5F20\u56FE\u50CF\u81F3\u6B64",
"Upload": "\u4e0a\u4f20", "Upload": "\u4E0A\u4F20",
"Block": "\u5757", "Block": "\u5757",
"Align": "\u5bf9\u9f50", "Align": "\u5BF9\u9F50",
"Default": "\u9ed8\u8ba4", "Default": "\u9ED8\u8BA4",
"Circle": "\u7a7a\u5fc3\u5706", "Circle": "\u7A7A\u5FC3\u5706",
"Disc": "\u5b9e\u5fc3\u5706", "Disc": "\u5B9E\u5FC3\u5706",
"Square": "\u65b9\u5757", "Square": "\u65B9\u5757",
"Lower Alpha": "\u5c0f\u5199\u82f1\u6587\u5b57\u6bcd", "Lower Alpha": "\u5C0F\u5199\u82F1\u6587\u5B57\u6BCD",
"Lower Greek": "\u5c0f\u5199\u5e0c\u814a\u5b57\u6bcd", "Lower Greek": "\u5C0F\u5199\u5E0C\u814A\u5B57\u6BCD",
"Lower Roman": "\u5c0f\u5199\u7f57\u9a6c\u5b57\u6bcd", "Lower Roman": "\u5C0F\u5199\u7F57\u9A6C\u5B57\u6BCD",
"Upper Alpha": "\u5927\u5199\u82f1\u6587\u5b57\u6bcd", "Upper Alpha": "\u5927\u5199\u82F1\u6587\u5B57\u6BCD",
"Upper Roman": "\u5927\u5199\u7f57\u9a6c\u5b57\u6bcd", "Upper Roman": "\u5927\u5199\u7F57\u9A6C\u5B57\u6BCD",
"Anchor...": "\u951a\u70b9...", "Anchor...": "\u951A\u70B9...",
"Name": "\u540d\u79f0", "Name": "\u540D\u79F0",
"Id": "\u6807\u8bc6\u7b26", "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", "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", "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", "Restore last draft": "\u6062\u590D\u4E0A\u6B21\u7684\u8349\u7A3F",
"Special characters...": "\u7279\u6b8a\u5b57\u7b26...", "Special characters...": "\u7279\u6B8A\u5B57\u7B26...",
"Source code": "\u6e90\u4ee3\u7801", "Source code": "\u6E90\u4EE3\u7801",
"Insert\/Edit code sample": "\u63d2\u5165\/\u7f16\u8f91\u4ee3\u7801\u793a\u4f8b", "Insert/Edit code sample": "\u63D2\u5165/\u7F16\u8F91\u4EE3\u7801\u793A\u4F8B",
"Language": "\u8bed\u8a00", "Language": "\u8BED\u8A00",
"Code sample...": "\u793a\u4f8b\u4ee3\u7801...", "Code sample...": "\u793A\u4F8B\u4EE3\u7801...",
"Color Picker": "\u9009\u8272\u5668", "Color Picker": "\u9009\u8272\u5668",
"R": "R", "R": "R",
"G": "G", "G": "G",
"B": "B", "B": "B",
"Left to right": "\u4ece\u5de6\u5230\u53f3", "Left to right": "\u4ECE\u5DE6\u5230\u53F3",
"Right to left": "\u4ece\u53f3\u5230\u5de6", "Right to left": "\u4ECE\u53F3\u5230\u5DE6",
"Emoticons...": "\u8868\u60c5\u7b26\u53f7...", "Emoticons...": "\u8868\u60C5\u7B26\u53F7...",
"Metadata and Document Properties": "\u5143\u6570\u636e\u548c\u6587\u6863\u5c5e\u6027", "Metadata and Document Properties": "\u5143\u6570\u636E\u548C\u6587\u6863\u5C5E\u6027",
"Title": "\u6807\u9898", "Title": "\u6807\u9898",
"Keywords": "\u5173\u952e\u8bcd", "Keywords": "\u5173\u952E\u8BCD",
"Description": "\u63cf\u8ff0", "Description": "\u63CF\u8FF0",
"Robots": "\u673a\u5668\u4eba", "Robots": "\u673A\u5668\u4EBA",
"Author": "\u4f5c\u8005", "Author": "\u4F5C\u8005",
"Encoding": "\u7f16\u7801", "Encoding": "\u7F16\u7801",
"Fullscreen": "\u5168\u5c4f", "Fullscreen": "\u5168\u5C4F",
"Action": "\u64cd\u4f5c", "Action": "\u64CD\u4F5C",
"Shortcut": "\u5feb\u6377\u952e", "Shortcut": "\u5FEB\u6377\u952E",
"Help": "\u5e2e\u52a9", "Help": "\u5E2E\u52A9",
"Address": "\u5730\u5740", "Address": "\u5730\u5740",
"Focus to menubar": "\u79fb\u52a8\u7126\u70b9\u5230\u83dc\u5355\u680f", "Focus to menubar": "\u79FB\u52A8\u7126\u70B9\u5230\u83DC\u5355\u680F",
"Focus to toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u5de5\u5177\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 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", "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)", "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)", "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)", "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}):", "Plugins installed ({0}):": "\u5DF2\u5B89\u88C5\u63D2\u4EF6 ({0}):",
"Premium plugins:": "\u4f18\u79c0\u63d2\u4ef6\uff1a", "Premium plugins:": "\u4F18\u79C0\u63D2\u4EF6\uFF1A",
"Learn more...": "\u4e86\u89e3\u66f4\u591a...", "Learn more...": "\u4E86\u89E3\u66F4\u591A...",
"You are using {0}": "\u4f60\u6b63\u5728\u4f7f\u7528 {0}", "You are using {0}": "\u4F60\u6B63\u5728\u4F7F\u7528 {0}",
"Plugins": "\u63d2\u4ef6", "Plugins": "\u63D2\u4EF6",
"Handy Shortcuts": "\u5feb\u6377\u952e", "Handy Shortcuts": "\u5FEB\u6377\u952E",
"Horizontal line": "\u6c34\u5e73\u5206\u5272\u7ebf", "Horizontal line": "\u6C34\u5E73\u5206\u5272\u7EBF",
"Insert\/edit image": "\u63d2\u5165\/\u7f16\u8f91\u56fe\u7247", "Insert/edit image": "\u63D2\u5165/\u7F16\u8F91\u56FE\u7247",
"Image description": "\u56fe\u7247\u63cf\u8ff0", "Image description": "\u56FE\u7247\u63CF\u8FF0",
"Source": "\u5730\u5740", "Source": "\u5730\u5740",
"Dimensions": "\u5927\u5c0f", "Dimensions": "\u5927\u5C0F",
"Constrain proportions": "\u4fdd\u6301\u7eb5\u6a2a\u6bd4", "Constrain proportions": "\u4FDD\u6301\u7EB5\u6A2A\u6BD4",
"General": "\u666e\u901a", "General": "\u666E\u901A",
"Advanced": "\u9ad8\u7ea7", "Advanced": "\u9AD8\u7EA7",
"Style": "\u6837\u5f0f", "Style": "\u6837\u5F0F",
"Vertical space": "\u5782\u76f4\u8fb9\u8ddd", "Vertical space": "\u5782\u76F4\u8FB9\u8DDD",
"Horizontal space": "\u6c34\u5e73\u8fb9\u8ddd", "Horizontal space": "\u6C34\u5E73\u8FB9\u8DDD",
"Border": "\u8fb9\u6846", "Border": "\u8FB9\u6846",
"Insert image": "\u63d2\u5165\u56fe\u7247", "Insert image": "\u63D2\u5165\u56FE\u7247",
"Image...": "\u56fe\u7247...", "Image...": "\u56FE\u7247...",
"Image list": "\u56fe\u7247\u5217\u8868", "Image list": "\u56FE\u7247\u5217\u8868",
"Rotate counterclockwise": "\u9006\u65f6\u9488\u65cb\u8f6c", "Rotate counterclockwise": "\u9006\u65F6\u9488\u65CB\u8F6C",
"Rotate clockwise": "\u987a\u65f6\u9488\u65cb\u8f6c", "Rotate clockwise": "\u987A\u65F6\u9488\u65CB\u8F6C",
"Flip vertically": "\u5782\u76f4\u7ffb\u8f6c", "Flip vertically": "\u5782\u76F4\u7FFB\u8F6C",
"Flip horizontally": "\u6c34\u5e73\u7ffb\u8f6c", "Flip horizontally": "\u6C34\u5E73\u7FFB\u8F6C",
"Edit image": "\u7f16\u8f91\u56fe\u7247", "Edit image": "\u7F16\u8F91\u56FE\u7247",
"Image options": "\u56fe\u7247\u9009\u9879", "Image options": "\u56FE\u7247\u9009\u9879",
"Zoom in": "\u653e\u5927", "Zoom in": "\u653E\u5927",
"Zoom out": "\u7f29\u5c0f", "Zoom out": "\u7F29\u5C0F",
"Crop": "\u88c1\u526a", "Crop": "\u88C1\u526A",
"Resize": "\u8c03\u6574\u5927\u5c0f", "Resize": "\u8C03\u6574\u5927\u5C0F",
"Orientation": "\u65b9\u5411", "Orientation": "\u65B9\u5411",
"Brightness": "\u4eae\u5ea6", "Brightness": "\u4EAE\u5EA6",
"Sharpen": "\u9510\u5316", "Sharpen": "\u9510\u5316",
"Contrast": "\u5bf9\u6bd4\u5ea6", "Contrast": "\u5BF9\u6BD4\u5EA6",
"Color levels": "\u989c\u8272\u5c42\u6b21", "Color levels": "\u989C\u8272\u5C42\u6B21",
"Gamma": "\u4f3d\u9a6c\u503c", "Gamma": "\u4F3D\u9A6C\u503C",
"Invert": "\u53cd\u8f6c", "Invert": "\u53CD\u8F6C",
"Apply": "\u5e94\u7528", "Apply": "\u5E94\u7528",
"Back": "\u540e\u9000", "Back": "\u540E\u9000",
"Insert date\/time": "\u63d2\u5165\u65e5\u671f\/\u65f6\u95f4", "Insert date/time": "\u63D2\u5165\u65E5\u671F/\u65F6\u95F4",
"Date\/time": "\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",
"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", "Text to display": "\u663E\u793A\u6587\u5B57",
"Url": "\u5730\u5740", "Url": "\u5730\u5740",
"Open link in...": "\u94fe\u63a5\u6253\u5f00\u4f4d\u7f6e...", "Open link in...": "\u94FE\u63A5\u6253\u5F00\u4F4D\u7F6E...",
"Current window": "\u5f53\u524d\u7a97\u53e3", "Current window": "\u5F53\u524D\u7A97\u53E3",
"None": "\u65e0", "None": "\u65E0",
"New window": "\u5728\u65b0\u7a97\u53e3\u6253\u5f00", "New window": "\u5728\u65B0\u7A97\u53E3\u6253\u5F00",
"Remove link": "\u5220\u9664\u94fe\u63a5", "Remove link": "\u5220\u9664\u94FE\u63A5",
"Anchors": "\u951a\u70b9", "Anchors": "\u951A\u70B9",
"Link...": "\u94fe\u63a5...", "Link...": "\u94FE\u63A5...",
"Paste or type a link": "\u7c98\u8d34\u6216\u8f93\u5165\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 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", "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", "Link list": "\u94FE\u63A5\u5217\u8868",
"Insert video": "\u63d2\u5165\u89c6\u9891", "Insert video": "\u63D2\u5165\u89C6\u9891",
"Insert\/edit video": "\u63d2\u5165\/\u7f16\u8f91\u89c6\u9891", "Insert/edit video": "\u63D2\u5165/\u7F16\u8F91\u89C6\u9891",
"Insert\/edit media": "\u63d2\u5165\/\u7f16\u8f91\u5a92\u4f53", "Insert/edit media": "\u63D2\u5165/\u7F16\u8F91\u5A92\u4F53",
"Alternative source": "\u955c\u50cf", "Alternative source": "\u955C\u50CF",
"Alternative source URL": "\u66ff\u4ee3\u6765\u6e90\u7f51\u5740", "Alternative source URL": "\u66FF\u4EE3\u6765\u6E90\u7F51\u5740",
"Media poster (Image URL)": "\u5c01\u9762(\u56fe\u7247\u5730\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:", "Paste your embed code below:": "\u5C06\u5185\u5D4C\u4EE3\u7801\u7C98\u8D34\u5728\u4E0B\u9762:",
"Embed": "\u5185\u5d4c", "Embed": "\u5185\u5D4C",
"Media...": "\u591a\u5a92\u4f53...", "Media...": "\u591A\u5A92\u4F53...",
"Nonbreaking space": "\u4e0d\u95f4\u65ad\u7a7a\u683c", "Nonbreaking space": "\u4E0D\u95F4\u65AD\u7A7A\u683C",
"Page break": "\u5206\u9875\u7b26", "Page break": "\u5206\u9875\u7B26",
"Paste as text": "\u7c98\u8d34\u4e3a\u6587\u672c", "Paste as text": "\u7C98\u8D34\u4E3A\u6587\u672C",
"Preview": "\u9884\u89c8", "Preview": "\u9884\u89C8",
"Print...": "\u6253\u5370...", "Print...": "\u6253\u5370...",
"Save": "\u4fdd\u5b58", "Save": "\u4FDD\u5B58",
"Find": "\u67e5\u627e", "Find": "\u67E5\u627E",
"Replace with": "\u66ff\u6362\u4e3a", "Replace with": "\u66FF\u6362\u4E3A",
"Replace": "\u66ff\u6362", "Replace": "\u66FF\u6362",
"Replace all": "\u5168\u90e8\u66ff\u6362", "Replace all": "\u5168\u90E8\u66FF\u6362",
"Previous": "\u4e0a\u4e00\u4e2a", "Previous": "\u4E0A\u4E00\u4E2A",
"Next": "\u4e0b\u4e00\u4e2a", "Next": "\u4E0B\u4E00\u4E2A",
"Find and replace...": "\u67e5\u627e\u5e76\u66ff\u6362...", "Find and replace...": "\u67E5\u627E\u5E76\u66FF\u6362...",
"Could not find the specified string.": "\u672a\u627e\u5230\u641c\u7d22\u5185\u5bb9.", "Could not find the specified string.": "\u672A\u627E\u5230\u641C\u7D22\u5185\u5BB9.",
"Match case": "\u533a\u5206\u5927\u5c0f\u5199", "Match case": "\u533A\u5206\u5927\u5C0F\u5199",
"Find whole words only": "\u5168\u5b57\u5339\u914d", "Find whole words only": "\u5168\u5B57\u5339\u914D",
"Spell check": "\u62fc\u5199\u68c0\u67e5", "Spell check": "\u62FC\u5199\u68C0\u67E5",
"Ignore": "\u5ffd\u7565", "Ignore": "\u5FFD\u7565",
"Ignore all": "\u5168\u90e8\u5ffd\u7565", "Ignore all": "\u5168\u90E8\u5FFD\u7565",
"Finish": "\u5b8c\u6210", "Finish": "\u5B8C\u6210",
"Add to Dictionary": "\u6dfb\u52a0\u5230\u5b57\u5178", "Add to Dictionary": "\u6DFB\u52A0\u5230\u5B57\u5178",
"Insert table": "\u63d2\u5165\u8868\u683c", "Insert table": "\u63D2\u5165\u8868\u683C",
"Table properties": "\u8868\u683c\u5c5e\u6027", "Table properties": "\u8868\u683C\u5C5E\u6027",
"Delete table": "\u5220\u9664\u8868\u683c", "Delete table": "\u5220\u9664\u8868\u683C",
"Cell": "\u5355\u5143\u683c", "Cell": "\u5355\u5143\u683C",
"Row": "\u884c", "Row": "\u884C",
"Column": "\u5217", "Column": "\u5217",
"Cell properties": "\u5355\u5143\u683c\u5c5e\u6027", "Cell properties": "\u5355\u5143\u683C\u5C5E\u6027",
"Merge cells": "\u5408\u5e76\u5355\u5143\u683c", "Merge cells": "\u5408\u5E76\u5355\u5143\u683C",
"Split cell": "\u62c6\u5206\u5355\u5143\u683c", "Split cell": "\u62C6\u5206\u5355\u5143\u683C",
"Insert row before": "\u5728\u4e0a\u65b9\u63d2\u5165", "Insert row before": "\u5728\u4E0A\u65B9\u63D2\u5165",
"Insert row after": "\u5728\u4e0b\u65b9\u63d2\u5165", "Insert row after": "\u5728\u4E0B\u65B9\u63D2\u5165",
"Delete row": "\u5220\u9664\u884c", "Delete row": "\u5220\u9664\u884C",
"Row properties": "\u884c\u5c5e\u6027", "Row properties": "\u884C\u5C5E\u6027",
"Cut row": "\u526a\u5207\u884c", "Cut row": "\u526A\u5207\u884C",
"Copy row": "\u590d\u5236\u884c", "Copy row": "\u590D\u5236\u884C",
"Paste row before": "\u7c98\u8d34\u5230\u4e0a\u65b9", "Paste row before": "\u7C98\u8D34\u5230\u4E0A\u65B9",
"Paste row after": "\u7c98\u8d34\u5230\u4e0b\u65b9", "Paste row after": "\u7C98\u8D34\u5230\u4E0B\u65B9",
"Insert column before": "\u5728\u5de6\u4fa7\u63d2\u5165", "Insert column before": "\u5728\u5DE6\u4FA7\u63D2\u5165",
"Insert column after": "\u5728\u53f3\u4fa7\u63d2\u5165", "Insert column after": "\u5728\u53F3\u4FA7\u63D2\u5165",
"Delete column": "\u5220\u9664\u5217", "Delete column": "\u5220\u9664\u5217",
"Cols": "\u5217", "Cols": "\u5217",
"Rows": "\u884c", "Rows": "\u884C",
"Width": "\u5bbd", "Width": "\u5BBD",
"Height": "\u9ad8", "Height": "\u9AD8",
"Cell spacing": "\u5355\u5143\u683c\u5916\u95f4\u8ddd", "Cell spacing": "\u5355\u5143\u683C\u5916\u95F4\u8DDD",
"Cell padding": "\u5355\u5143\u683c\u5185\u8fb9\u8ddd", "Cell padding": "\u5355\u5143\u683C\u5185\u8FB9\u8DDD",
"Show caption": "\u663e\u793a\u6807\u9898", "Show caption": "\u663E\u793A\u6807\u9898",
"Left": "\u5de6\u5bf9\u9f50", "Left": "\u5DE6\u5BF9\u9F50",
"Center": "\u5c45\u4e2d", "Center": "\u5C45\u4E2D",
"Right": "\u53f3\u5bf9\u9f50", "Right": "\u53F3\u5BF9\u9F50",
"Cell type": "\u5355\u5143\u683c\u7c7b\u578b", "Cell type": "\u5355\u5143\u683C\u7C7B\u578B",
"Scope": "\u8303\u56f4", "Scope": "\u8303\u56F4",
"Alignment": "\u5bf9\u9f50\u65b9\u5f0f", "Alignment": "\u5BF9\u9F50\u65B9\u5F0F",
"H Align": "\u6c34\u5e73\u5bf9\u9f50", "H Align": "\u6C34\u5E73\u5BF9\u9F50",
"V Align": "\u5782\u76f4\u5bf9\u9f50", "V Align": "\u5782\u76F4\u5BF9\u9F50",
"Top": "\u9876\u90e8\u5bf9\u9f50", "Top": "\u9876\u90E8\u5BF9\u9F50",
"Middle": "\u5782\u76f4\u5c45\u4e2d", "Middle": "\u5782\u76F4\u5C45\u4E2D",
"Bottom": "\u5e95\u90e8\u5bf9\u9f50", "Bottom": "\u5E95\u90E8\u5BF9\u9F50",
"Header cell": "\u8868\u5934\u5355\u5143\u683c", "Header cell": "\u8868\u5934\u5355\u5143\u683C",
"Row group": "\u884c\u7ec4", "Row group": "\u884C\u7EC4",
"Column group": "\u5217\u7ec4", "Column group": "\u5217\u7EC4",
"Row type": "\u884c\u7c7b\u578b", "Row type": "\u884C\u7C7B\u578B",
"Header": "\u8868\u5934", "Header": "\u8868\u5934",
"Body": "\u8868\u4f53", "Body": "\u8868\u4F53",
"Footer": "\u8868\u5c3e", "Footer": "\u8868\u5C3E",
"Border color": "\u8fb9\u6846\u989c\u8272", "Border color": "\u8FB9\u6846\u989C\u8272",
"Insert template...": "\u63d2\u5165\u6a21\u677f...", "Insert template...": "\u63D2\u5165\u6A21\u677F...",
"Templates": "\u6a21\u677f", "Templates": "\u6A21\u677F",
"Template": "\u6a21\u677f", "Template": "\u6A21\u677F",
"Text color": "\u6587\u5b57\u989c\u8272", "Text color": "\u6587\u5B57\u989C\u8272",
"Background color": "\u80cc\u666f\u8272", "Background color": "\u80CC\u666F\u8272",
"Custom...": "\u81ea\u5b9a\u4e49...", "Custom...": "\u81EA\u5B9A\u4E49...",
"Custom color": "\u81ea\u5b9a\u4e49\u989c\u8272", "Custom color": "\u81EA\u5B9A\u4E49\u989C\u8272",
"No color": "\u65e0", "No color": "\u65E0",
"Remove color": "\u79fb\u9664\u989c\u8272", "Remove color": "\u79FB\u9664\u989C\u8272",
"Table of Contents": "\u5185\u5bb9\u5217\u8868", "Table of Contents": "\u5185\u5BB9\u5217\u8868",
"Show blocks": "\u663e\u793a\u533a\u5757\u8fb9\u6846", "Show blocks": "\u663E\u793A\u533A\u5757\u8FB9\u6846",
"Show invisible characters": "\u663e\u793a\u4e0d\u53ef\u89c1\u5b57\u7b26", "Show invisible characters": "\u663E\u793A\u4E0D\u53EF\u89C1\u5B57\u7B26",
"Word count": "\u5b57\u6570", "Word count": "\u5B57\u6570",
"Words: {0}": "\u5b57\u6570\uff1a{0}", "Words: {0}": "\u5B57\u6570\uFF1A{0}",
"{0} words": "{0} \u5b57", "{0} words": "{0} \u5B57",
"File": "\u6587\u4ef6", "File": "\u6587\u4EF6",
"Edit": "\u7f16\u8f91", "Edit": "\u7F16\u8F91",
"Insert": "\u63d2\u5165", "Insert": "\u63D2\u5165",
"View": "\u89c6\u56fe", "View": "\u89C6\u56FE",
"Format": "\u683c\u5f0f", "Format": "\u683C\u5F0F",
"Table": "\u8868\u683c", "Table": "\u8868\u683C",
"Tools": "\u5de5\u5177", "Tools": "\u5DE5\u5177",
"Powered by {0}": "\u7531{0}\u9a71\u52a8", "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", "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", "Image title": "\u56FE\u7247\u6807\u9898",
"Border width": "\u8fb9\u6846\u5bbd\u5ea6", "Border width": "\u8FB9\u6846\u5BBD\u5EA6",
"Border style": "\u8fb9\u6846\u6837\u5f0f", "Border style": "\u8FB9\u6846\u6837\u5F0F",
"Error": "\u9519\u8bef", "Error": "\u9519\u8BEF",
"Warn": "\u8b66\u544a", "Warn": "\u8B66\u544A",
"Valid": "\u6709\u6548", "Valid": "\u6709\u6548",
"To open the popup, press Shift+Enter": "\u6309Shitf+Enter\u952e\u6253\u5f00\u5bf9\u8bdd\u6846", "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", "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", "System Font": "\u7CFB\u7EDF\u5B57\u4F53",
"Failed to upload image: {0}": "\u56fe\u7247\u4e0a\u4f20\u5931\u8d25: {0}", "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: {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 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}", "Failed to initialize plugin: {0}": "\u63D2\u4EF6\u521D\u59CB\u5316\u5931\u8D25: {0}",
"example": "\u793a\u4f8b", "example": "\u793A\u4F8B",
"Search": "\u641c\u7d22", "Search": "\u641C\u7D22",
"All": "\u5168\u90e8", "All": "\u5168\u90E8",
"Currency": "\u8d27\u5e01", "Currency": "\u8D27\u5E01",
"Text": "\u6587\u5b57", "Text": "\u6587\u5B57",
"Quotations": "\u5f15\u7528", "Quotations": "\u5F15\u7528",
"Mathematical": "\u6570\u5b66", "Mathematical": "\u6570\u5B66",
"Extended Latin": "\u62c9\u4e01\u8bed\u6269\u5145", "Extended Latin": "\u62C9\u4E01\u8BED\u6269\u5145",
"Symbols": "\u7b26\u53f7", "Symbols": "\u7B26\u53F7",
"Arrows": "\u7bad\u5934", "Arrows": "\u7BAD\u5934",
"User Defined": "\u81ea\u5b9a\u4e49", "User Defined": "\u81EA\u5B9A\u4E49",
"dollar sign": "\u7f8e\u5143\u7b26\u53f7", "dollar sign": "\u7F8E\u5143\u7B26\u53F7",
"currency sign": "\u8d27\u5e01\u7b26\u53f7", "currency sign": "\u8D27\u5E01\u7B26\u53F7",
"euro-currency sign": "\u6b27\u5143\u7b26\u53f7", "euro-currency sign": "\u6B27\u5143\u7B26\u53F7",
"colon sign": "\u5192\u53f7", "colon sign": "\u5192\u53F7",
"cruzeiro sign": "\u514b\u9c81\u8d5b\u7f57\u5e01\u7b26\u53f7", "cruzeiro sign": "\u514B\u9C81\u8D5B\u7F57\u5E01\u7B26\u53F7",
"french franc sign": "\u6cd5\u90ce\u7b26\u53f7", "french franc sign": "\u6CD5\u90CE\u7B26\u53F7",
"lira sign": "\u91cc\u62c9\u7b26\u53f7", "lira sign": "\u91CC\u62C9\u7B26\u53F7",
"mill sign": "\u5bc6\u5c14\u7b26\u53f7", "mill sign": "\u5BC6\u5C14\u7B26\u53F7",
"naira sign": "\u5948\u62c9\u7b26\u53f7", "naira sign": "\u5948\u62C9\u7B26\u53F7",
"peseta sign": "\u6bd4\u585e\u5854\u7b26\u53f7", "peseta sign": "\u6BD4\u585E\u5854\u7B26\u53F7",
"rupee sign": "\u5362\u6bd4\u7b26\u53f7", "rupee sign": "\u5362\u6BD4\u7B26\u53F7",
"won sign": "\u97e9\u5143\u7b26\u53f7", "won sign": "\u97E9\u5143\u7B26\u53F7",
"new sheqel sign": "\u65b0\u8c22\u514b\u5c14\u7b26\u53f7", "new sheqel sign": "\u65B0\u8C22\u514B\u5C14\u7B26\u53F7",
"dong sign": "\u8d8a\u5357\u76fe\u7b26\u53f7", "dong sign": "\u8D8A\u5357\u76FE\u7B26\u53F7",
"kip sign": "\u8001\u631d\u57fa\u666e\u7b26\u53f7", "kip sign": "\u8001\u631D\u57FA\u666E\u7B26\u53F7",
"tugrik sign": "\u56fe\u683c\u91cc\u514b\u7b26\u53f7", "tugrik sign": "\u56FE\u683C\u91CC\u514B\u7B26\u53F7",
"drachma sign": "\u5fb7\u62c9\u514b\u9a6c\u7b26\u53f7", "drachma sign": "\u5FB7\u62C9\u514B\u9A6C\u7B26\u53F7",
"german penny symbol": "\u5fb7\u56fd\u4fbf\u58eb\u7b26\u53f7", "german penny symbol": "\u5FB7\u56FD\u4FBF\u58EB\u7B26\u53F7",
"peso sign": "\u6bd4\u7d22\u7b26\u53f7", "peso sign": "\u6BD4\u7D22\u7B26\u53F7",
"guarani sign": "\u74dc\u62c9\u5c3c\u7b26\u53f7", "guarani sign": "\u74DC\u62C9\u5C3C\u7B26\u53F7",
"austral sign": "\u6fb3\u5143\u7b26\u53f7", "austral sign": "\u6FB3\u5143\u7B26\u53F7",
"hryvnia sign": "\u683c\u91cc\u592b\u5c3c\u4e9a\u7b26\u53f7", "hryvnia sign": "\u683C\u91CC\u592B\u5C3C\u4E9A\u7B26\u53F7",
"cedi sign": "\u585e\u5730\u7b26\u53f7", "cedi sign": "\u585E\u5730\u7B26\u53F7",
"livre tournois sign": "\u91cc\u5f17\u5f17\u5c14\u7b26\u53f7", "livre tournois sign": "\u91CC\u5F17\u5F17\u5C14\u7B26\u53F7",
"spesmilo sign": "spesmilo\u7b26\u53f7", "spesmilo sign": "spesmilo\u7B26\u53F7",
"tenge sign": "\u575a\u6208\u7b26\u53f7", "tenge sign": "\u575A\u6208\u7B26\u53F7",
"indian rupee sign": "\u5370\u5ea6\u5362\u6bd4", "indian rupee sign": "\u5370\u5EA6\u5362\u6BD4",
"turkish lira sign": "\u571f\u8033\u5176\u91cc\u62c9", "turkish lira sign": "\u571F\u8033\u5176\u91CC\u62C9",
"nordic mark sign": "\u5317\u6b27\u9a6c\u514b", "nordic mark sign": "\u5317\u6B27\u9A6C\u514B",
"manat sign": "\u9a6c\u7eb3\u7279\u7b26\u53f7", "manat sign": "\u9A6C\u7EB3\u7279\u7B26\u53F7",
"ruble sign": "\u5362\u5e03\u7b26\u53f7", "ruble sign": "\u5362\u5E03\u7B26\u53F7",
"yen character": "\u65e5\u5143\u5b57\u6837", "yen character": "\u65E5\u5143\u5B57\u6837",
"yuan character": "\u4eba\u6c11\u5e01\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", "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", "yen/yuan character variant one": "\u5143\u5B57\u6837\uFF08\u5927\u5199\uFF09",
"Loading emoticons...": "\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7...", "Loading emoticons...": "\u52A0\u8F7D\u8868\u60C5\u7B26\u53F7...",
"Could not load emoticons": "\u4e0d\u80fd\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7", "Could not load emoticons": "\u4E0D\u80FD\u52A0\u8F7D\u8868\u60C5\u7B26\u53F7",
"People": "\u4eba\u7c7b", "People": "\u4EBA\u7C7B",
"Animals and Nature": "\u52a8\u7269\u548c\u81ea\u7136", "Animals and Nature": "\u52A8\u7269\u548C\u81EA\u7136",
"Food and Drink": "\u98df\u7269\u548c\u996e\u54c1", "Food and Drink": "\u98DF\u7269\u548C\u996E\u54C1",
"Activity": "\u6d3b\u52a8", "Activity": "\u6D3B\u52A8",
"Travel and Places": "\u65c5\u6e38\u548c\u5730\u70b9", "Travel and Places": "\u65C5\u6E38\u548C\u5730\u70B9",
"Objects": "\u7269\u4ef6", "Objects": "\u7269\u4EF6",
"Flags": "\u65d7\u5e1c", "Flags": "\u65D7\u5E1C",
"Characters": "\u5b57\u7b26", "Characters": "\u5B57\u7B26",
"Characters (no spaces)": "\u5b57\u7b26(\u65e0\u7a7a\u683c)", "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: 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", "Error: No form element found.": "\u9519\u8BEF: \u6CA1\u6709\u8868\u5355\u63A7\u4EF6\u3002",
"Update": "\u66f4\u65b0", "Update": "\u66F4\u65B0",
"Color swatch": "\u989c\u8272\u6837\u672c", "Color swatch": "\u989C\u8272\u6837\u672C",
"Turquoise": "\u9752\u7eff\u8272", "Turquoise": "\u9752\u7EFF\u8272",
"Green": "\u7eff\u8272", "Green": "\u7EFF\u8272",
"Blue": "\u84dd\u8272", "Blue": "\u84DD\u8272",
"Purple": "\u7d2b\u8272", "Purple": "\u7D2B\u8272",
"Navy Blue": "\u6d77\u519b\u84dd", "Navy Blue": "\u6D77\u519B\u84DD",
"Dark Turquoise": "\u6df1\u84dd\u7eff\u8272", "Dark Turquoise": "\u6DF1\u84DD\u7EFF\u8272",
"Dark Green": "\u6df1\u7eff\u8272", "Dark Green": "\u6DF1\u7EFF\u8272",
"Medium Blue": "\u4e2d\u84dd\u8272", "Medium Blue": "\u4E2D\u84DD\u8272",
"Medium Purple": "\u4e2d\u7d2b\u8272", "Medium Purple": "\u4E2D\u7D2B\u8272",
"Midnight Blue": "\u6df1\u84dd\u8272", "Midnight Blue": "\u6DF1\u84DD\u8272",
"Yellow": "\u9ec4\u8272", "Yellow": "\u9EC4\u8272",
"Orange": "\u6a59\u8272", "Orange": "\u6A59\u8272",
"Red": "\u7ea2\u8272", "Red": "\u7EA2\u8272",
"Light Gray": "\u6d45\u7070\u8272", "Light Gray": "\u6D45\u7070\u8272",
"Gray": "\u7070\u8272", "Gray": "\u7070\u8272",
"Dark Yellow": "\u6697\u9ec4\u8272", "Dark Yellow": "\u6697\u9EC4\u8272",
"Dark Orange": "\u6df1\u6a59\u8272", "Dark Orange": "\u6DF1\u6A59\u8272",
"Dark Red": "\u6df1\u7ea2\u8272", "Dark Red": "\u6DF1\u7EA2\u8272",
"Medium Gray": "\u4e2d\u7070\u8272", "Medium Gray": "\u4E2D\u7070\u8272",
"Dark Gray": "\u6df1\u7070\u8272", "Dark Gray": "\u6DF1\u7070\u8272",
"Black": "\u9ed1\u8272", "Black": "\u9ED1\u8272",
"White": "\u767d\u8272", "White": "\u767D\u8272",
"Switch to or from fullscreen mode": "\u5207\u6362\u5168\u5c4f\u6a21\u5f0f", "Switch to or from fullscreen mode": "\u5207\u6362\u5168\u5C4F\u6A21\u5F0F",
"Open help dialog": "\u6253\u5f00\u5e2e\u52a9\u5bf9\u8bdd\u6846", "Open help dialog": "\u6253\u5F00\u5E2E\u52A9\u5BF9\u8BDD\u6846",
"history": "\u5386\u53f2", "history": "\u5386\u53F2",
"styles": "\u6837\u5f0f", "styles": "\u6837\u5F0F",
"formatting": "\u683c\u5f0f\u5316", "formatting": "\u683C\u5F0F\u5316",
"alignment": "\u5bf9\u9f50", "alignment": "\u5BF9\u9F50",
"indentation": "\u7f29\u8fdb", "indentation": "\u7F29\u8FDB",
"permanent pen": "\u8bb0\u53f7\u7b14", "permanent pen": "\u8BB0\u53F7\u7B14",
"comments": "\u5907\u6ce8", "comments": "\u5907\u6CE8",
"Anchor": "\u951a\u70b9", "Anchor": "\u951A\u70B9",
"Special character": "\u7279\u6b8a\u7b26\u53f7", "Special character": "\u7279\u6B8A\u7B26\u53F7",
"Code sample": "\u4ee3\u7801\u793a\u4f8b", "Code sample": "\u4EE3\u7801\u793A\u4F8B",
"Color": "\u989c\u8272", "Color": "\u989C\u8272",
"Emoticons": "\u8868\u60c5", "Emoticons": "\u8868\u60C5",
"Document properties": "\u6587\u6863\u5c5e\u6027", "Document properties": "\u6587\u6863\u5C5E\u6027",
"Image": "\u56fe\u7247", "Image": "\u56FE\u7247",
"Insert link": "\u63d2\u5165\u94fe\u63a5", "Insert link": "\u63D2\u5165\u94FE\u63A5",
"Target": "\u6253\u5f00\u65b9\u5f0f", "Target": "\u6253\u5F00\u65B9\u5F0F",
"Link": "\u94fe\u63a5", "Link": "\u94FE\u63A5",
"Poster": "\u5c01\u9762", "Poster": "\u5C01\u9762",
"Media": "\u5a92\u4f53", "Media": "\u5A92\u4F53",
"Print": "\u6253\u5370", "Print": "\u6253\u5370",
"Prev": "\u4e0a\u4e00\u4e2a", "Prev": "\u4E0A\u4E00\u4E2A",
"Find and replace": "\u67e5\u627e\u548c\u66ff\u6362", "Find and replace": "\u67E5\u627E\u548C\u66FF\u6362",
"Whole words": "\u5168\u5b57\u5339\u914d", "Whole words": "\u5168\u5B57\u5339\u914D",
"Spellcheck": "\u62fc\u5199\u68c0\u67e5", "Spellcheck": "\u62FC\u5199\u68C0\u67E5",
"Caption": "\u6807\u9898", "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 */ /* eslint-disable vue/one-component-per-file */
import type { import type {
AutoCompleteProps,
ButtonProps,
CascaderProps,
CheckboxGroupProps,
CheckboxProps,
DatePickerProps,
DividerProps,
InputNumberProps,
InputProps,
MentionsProps,
RadioGroupProps,
RadioProps,
RateProps,
SelectProps,
SpaceProps,
SwitchProps,
TextAreaProps,
TimePickerProps,
TreeSelectProps,
UploadChangeParam, UploadChangeParam,
UploadFile, UploadFile,
UploadProps, UploadProps,
} from 'ant-design-vue'; } from 'ant-design-vue';
import type { RangePickerProps } from 'ant-design-vue/es/date-picker';
import type { Component, Ref } from 'vue'; 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 type { Recordable } from '@vben/types';
import { import {
@ -21,6 +46,9 @@ import {
defineAsyncComponent, defineAsyncComponent,
defineComponent, defineComponent,
h, h,
nextTick,
onMounted,
onUnmounted,
ref, ref,
render, render,
unref, unref,
@ -33,6 +61,7 @@ import {
IconPicker, IconPicker,
VCropper, VCropper,
} from '@vben/common-ui'; } from '@vben/common-ui';
import { useSortable } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
@ -40,7 +69,14 @@ import { isEmpty } from '@vben/utils';
import { FeatureStateCheck, GlobalFeatureStateCheck } from '@abp/features'; import { FeatureStateCheck, GlobalFeatureStateCheck } from '@abp/features';
import { PermissionStateCheck } from '@abp/permissions'; import { PermissionStateCheck } from '@abp/permissions';
import { TenantSelect } from '@abp/saas'; 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(() => const ColorPicker = defineAsyncComponent(() =>
import('vue3-colorpicker').then((res) => { 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( const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'), () => import('ant-design-vue/es/auto-complete'),
); );
@ -63,7 +109,6 @@ const DatePicker = defineAsyncComponent(
() => import('ant-design-vue/es/date-picker'), () => import('ant-design-vue/es/date-picker'),
); );
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider')); 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 Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
const InputNumber = defineAsyncComponent( const InputNumber = defineAsyncComponent(
() => import('ant-design-vue/es/input-number'), () => import('ant-design-vue/es/input-number'),
@ -71,9 +116,6 @@ const InputNumber = defineAsyncComponent(
const InputPassword = defineAsyncComponent(() => const InputPassword = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.InputPassword), 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( const Mentions = defineAsyncComponent(
() => import('ant-design-vue/es/mentions'), () => import('ant-design-vue/es/mentions'),
); );
@ -94,7 +136,6 @@ const Textarea = defineAsyncComponent(() =>
const TimePicker = defineAsyncComponent( const TimePicker = defineAsyncComponent(
() => import('ant-design-vue/es/time-picker'), () => import('ant-design-vue/es/time-picker'),
); );
const Tree = defineAsyncComponent(() => import('ant-design-vue/es/tree'));
const TreeSelect = defineAsyncComponent( const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'), () => import('ant-design-vue/es/tree-select'),
); );
@ -107,8 +148,8 @@ const PreviewGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/image').then((res) => res.ImagePreviewGroup), import('ant-design-vue/es/image').then((res) => res.ImagePreviewGroup),
); );
const withDefaultPlaceholder = <T extends Component>( const withDefaultPlaceholder = (
component: T, component: Component,
type: 'input' | 'select', type: 'input' | 'select',
componentProps: Recordable<any> = {}, componentProps: Recordable<any> = {},
) => { ) => {
@ -141,260 +182,261 @@ const withDefaultPlaceholder = <T extends Component>(
}); });
}; };
const withPreviewUpload = () => { const IMAGE_EXTENSIONS = new Set([
// 检查是否为图片文件的辅助函数 'bmp',
const isImageFile = (file: UploadFile): boolean => { 'gif',
const imageExtensions = new Set([ 'jpeg',
'bmp', 'jpg',
'gif', 'png',
'jpeg', 'svg',
'jpg', 'webp',
'png', ]);
'svg',
'webp', /**
]); *
if (file.url) { */
try { function isImageFile(file: UploadFile): boolean {
const pathname = new URL(file.url, 'http://localhost').pathname; if (file.url) {
const ext = pathname.split('.').pop()?.toLowerCase(); try {
return ext ? imageExtensions.has(ext) : false; const pathname = new URL(file.url, 'http://localhost').pathname;
} catch { const ext = pathname.split('.').pop()?.toLowerCase();
const ext = file.url?.split('.').pop()?.toLowerCase(); return ext ? IMAGE_EXTENSIONS.has(ext) : false;
return ext ? imageExtensions.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 ? imageExtensions.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, * Base64
) => { */
switch (listType) { function getBase64(file: File): Promise<string> {
case 'picture-card': { return new Promise((resolve, reject) => {
return { const reader = new FileReader();
default: () => placeholder, reader.readAsDataURL(file);
}; reader.addEventListener('load', () => resolve(reader.result as string));
} reader.addEventListener('error', reject);
default: { });
return { }
default: () =>
h( /**
Button, *
{ */
icon: h(IconifyIcon, { async function previewImage(
icon: 'ant-design:upload-outlined', file: UploadFile,
class: 'mb-1 size-4', 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;
}
// 对于图片文件,继续使用预览组 render(h(PreviewWrapper), container);
const [ImageComponent, PreviewGroupComponent] = await Promise.all([ }
Image,
PreviewGroup, /**
]); *
*/
const getBase64 = (file: File) => { function cropImage(file: File, aspectRatio: string | undefined) {
return new Promise((resolve, reject) => { return new Promise<Blob | string | undefined>((resolve, reject) => {
const reader = new FileReader(); const container = document.createElement('div');
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');
document.body.append(container); document.body.append(container);
// 用于追踪组件是否已卸载
let isUnmounted = false; 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() { setup() {
return () => { return () => {
if (isUnmounted) return null; if (isUnmounted) return null;
if (!objectUrl) {
objectUrl = URL.createObjectURL(file);
}
return h( return h(
PreviewGroupComponent, Modal,
{ {
class: 'hidden', open: open.value,
preview: { title: h('div', {}, [
visible: visible.value, $t('ui.crop.title'),
// 设置初始显示的图片索引 h(
current: imageFiles.findIndex((f) => f.uid === file.uid), 'span',
onVisibleChange: (value: boolean) => { {
visible.value = value; class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`,
if (!value) { },
// 延迟清理,确保动画完成 $t('ui.crop.titleTip', [aspectRatio]),
setTimeout(() => { ),
if (!isUnmounted && container) { ]),
isUnmounted = true; centered: true,
render(null, container); width: 548,
container.remove(); keyboard: false,
} maskClosable: false,
}, 300); 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, {
imageFiles.map((imgFile) => ref: (ref: any) => (cropperRef.value = ref),
h(ImageComponent, { img: objectUrl as string,
key: imgFile.uid, aspectRatio,
src: imgFile.url || imgFile.preview, }),
}),
),
); );
}; };
}, },
}; };
render(h(PreviewWrapper), container); render(h(CropperWrapper), 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);
});
};
/**
*
*/
const withPreviewUpload = () => {
return defineComponent({ return defineComponent({
name: Upload.name, name: Upload.name,
emits: ['update:modelValue'], emits: ['update:modelValue'],
setup: ( setup(
props: any, props: any,
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any }, { attrs, slots, emit }: { attrs: any; emit: any; slots: any },
) => { ) {
const previewVisible = ref<boolean>(false); 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 listType = attrs?.listType || attrs?.['list-type'] || 'text';
const fileList = ref<UploadProps['fileList']>( const fileList = ref<UploadProps['fileList']>(
attrs?.fileList || attrs?.['file-list'] || [], attrs?.fileList || attrs?.['file-list'] || [],
); );
@ -408,12 +450,14 @@ const withPreviewUpload = () => {
file: UploadFile, file: UploadFile,
originFileList: Array<File>, originFileList: Array<File>,
) => { ) => {
// 文件大小限制
if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) { if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) {
message.error($t('ui.formRules.sizeLimit', [maxSize.value])); message.error($t('ui.formRules.sizeLimit', [maxSize.value]));
file.status = 'removed'; file.status = 'removed';
return false; return false;
} }
// 多选或者非图片不唤起裁剪框
// 图片裁剪处理
if ( if (
attrs.crop && attrs.crop &&
!attrs.multiple && !attrs.multiple &&
@ -421,14 +465,11 @@ const withPreviewUpload = () => {
isImageFile(file) isImageFile(file)
) { ) {
file.status = 'removed'; file.status = 'removed';
// antd Upload组件问题 file参数获取的是UploadFile类型对象无法取到File类型 所以通过originFileList[0]获取
const blob = await cropImage(originFileList[0], aspectRatio.value); const blob = await cropImage(originFileList[0], aspectRatio.value);
return new Promise((resolve, reject) => { if (!blob) {
if (!blob) { throw new Error($t('ui.crop.errorTip'));
return reject(new Error($t('ui.crop.errorTip'))); }
} return blob;
resolve(blob);
});
} }
return attrs.beforeUpload?.(file) ?? true; return attrs.beforeUpload?.(file) ?? true;
@ -436,12 +477,9 @@ const withPreviewUpload = () => {
const handleChange = (event: UploadChangeParam) => { const handleChange = (event: UploadChangeParam) => {
try { try {
// 行内写法 handleChange: (event) => {}
attrs.handleChange?.(event); attrs.handleChange?.(event);
// template写法 @handle-change="(event) => {}"
attrs.onHandleChange?.(event); attrs.onHandleChange?.(event);
} catch (error) { } catch (error) {
// Avoid breaking internal v-model sync on user handler errors
console.error(error); console.error(error);
} }
fileList.value = event.fileList.filter( fileList.value = event.fileList.filter(
@ -458,21 +496,88 @@ const withPreviewUpload = () => {
await previewImage(file, previewVisible, fileList); await previewImage(file, previewVisible, fileList);
}; };
const renderUploadButton = (): any => { const renderUploadButton = () => {
const isDisabled = attrs.disabled; if (attrs.disabled) return null;
// 如果禁用,不渲染上传按钮
if (isDisabled) {
return null;
}
// 否则渲染默认上传按钮
return isEmpty(slots) return isEmpty(slots)
? createDefaultSlotsWithUpload(listType, placeholder) ? createDefaultUploadSlots(listType, placeholder)
: slots; : 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( watch(
() => attrs.modelValue, () => attrs.modelValue,
(res) => { (res) => {
@ -480,18 +585,28 @@ const withPreviewUpload = () => {
}, },
); );
onMounted(initSortable);
onUnmounted(() => {
sortableInstance.value?.destroy();
removeDragStyle();
});
return () => return () =>
h( h(
Upload, 'div',
{ { 'data-upload-id': uploadId, class: 'w-full' },
...props, h(
...attrs, Upload,
fileList: fileList.value, {
beforeUpload: handleBeforeUpload, ...props,
onChange: handleChange, ...attrs,
onPreview: handlePreview, fileList: fileList.value,
}, beforeUpload: handleBeforeUpload,
renderUploadButton(), onChange: handleChange,
onPreview: handlePreview,
},
renderUploadButton() as any,
),
); );
}, },
}); });
@ -536,6 +651,39 @@ export type ComponentType =
| 'Upload' | 'Upload'
| BaseFormComponentType; | 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() { async function initComponentAdapter() {
const components: Partial<Record<ComponentType, Component>> = { 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(); const timezoneStore = useTimezoneStore();
if (accessStore.accessToken) { if (accessStore.accessToken) {
config.headers.Authorization = `${accessStore.accessToken}`; config.headers.Authorization = accessStore.accessToken;
} }
config.headers['Accept-Language'] = preferences.app.locale; config.headers['Accept-Language'] = preferences.app.locale;
config.headers['X-Request-From'] = 'vben'; config.headers['X-Request-From'] = 'vben';
@ -76,7 +76,7 @@ export function initRequestClient() {
}); });
// response数据解构 // response数据解构
requestClient.addResponseInterceptor<any>({ requestClient.addResponseInterceptor({
fulfilled: (response) => { fulfilled: (response) => {
const { data, status } = response; const { data, status } = response;
const { hasWrapResult, getData } = useWrapperResult(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 { registerAccessDirective } from '@vben/access';
import { registerLoadingDirective } from '@vben/common-ui'; import { registerLoadingDirective } from '@vben/common-ui';
import { providePluginsOptions } from '@vben/plugins';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores'; import { initStores } from '@vben/stores';
import '@vben/styles'; import '@vben/styles';
@ -12,7 +13,7 @@ import { useTitle } from '@vueuse/core';
import { $t, setupI18n } from '#/locales'; import { $t, setupI18n } from '#/locales';
import { initComponentAdapter } from './adapter/component'; import { initComponentAdapter } from './adapter/component';
import { initSetupVbenForm } from './adapter/form'; import { initSetupVbenForm, useVbenForm } from './adapter/form';
import { initRequestClient } from './adapter/request'; import { initRequestClient } from './adapter/request';
import App from './app.vue'; import App from './app.vue';
import { router } from './router'; import { router } from './router';
@ -25,6 +26,11 @@ async function bootstrap(namespace: string) {
// 初始化表单组件 // 初始化表单组件
await initSetupVbenForm(); await initSetupVbenForm();
// 注入插件全局配置
providePluginsOptions({
form: { useVbenForm },
});
// 初始化axios // 初始化axios
initRequestClient(); initRequestClient();
@ -64,6 +70,10 @@ async function bootstrap(namespace: string) {
// 配置路由及路由守卫 // 配置路由及路由守卫
app.use(router); app.use(router);
// 配置@tanstack/vue-query
const { VueQueryPlugin } = await import('@tanstack/vue-query');
app.use(VueQueryPlugin);
// 配置Motion插件 // 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion'); const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin); 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" :app-name="appName"
:logo="logo" :logo="logo"
:page-description="$t('authentication.pageDesc')" :page-description="$t('authentication.pageDesc')"
:page-title="$t('authentication.pageTitle')" :page-title="$t('authentication.pageTitle')">
>
<!-- 自定义工具栏 --> <!-- 自定义工具栏 -->
<!-- <template #toolbar></template> --> <!-- <template #toolbar></template> -->
</AuthPageLayout> </AuthPageLayout>

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

@ -108,13 +108,17 @@ function handleMakeAll() {
} }
async function handleLinkUser(token: string) { async function handleLinkUser(token: string) {
const { currentUser, currentTenant } = abpStore.application!;
const extraQueryParams: Record<string, string> = { const extraQueryParams: Record<string, string> = {
LinkUserId: currentUser.id!,
LinkToken: token, LinkToken: token,
}; };
if (currentTenant.id) { if (abpStore.application) {
extraQueryParams.LinkTenantId = currentTenant.id; const { currentUser, currentTenant } = abpStore.application;
if (currentUser.id) {
extraQueryParams.LinkUserId = currentUser.id;
}
if (currentTenant.id) {
extraQueryParams.LinkTenantId = currentTenant.id;
}
} }
// , // ,
await authStore.oidcLogin({ await authStore.oidcLogin({
@ -173,8 +177,7 @@ watch(
:menus :menus
:tag-text="userInfo?.email" :tag-text="userInfo?.email"
:text="userInfo?.realName" :text="userInfo?.realName"
@logout="handleLogout" @logout="handleLogout" />
/>
<span class="mb-1 flex items-center text-sm font-medium"> <span class="mb-1 flex items-center text-sm font-medium">
{{ description }} {{ description }}
</span> </span>
@ -186,14 +189,12 @@ watch(
:dot="showDot" :dot="showDot"
:notifications="notifications" :notifications="notifications"
@clear="handleNoticeClear" @clear="handleNoticeClear"
@make-all="handleMakeAll" @make-all="handleMakeAll" />
/>
</template> </template>
<template #extra> <template #extra>
<AuthenticationLoginExpiredModal <AuthenticationLoginExpiredModal
v-model:open="accessStore.loginExpired" v-model:open="accessStore.loginExpired"
:avatar :avatar>
>
<LoginForm /> <LoginForm />
</AuthenticationLoginExpiredModal> </AuthenticationLoginExpiredModal>
</template> </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 * @description
@ -13,6 +16,7 @@ export const overridesPreferences = defineOverridesPreferences({
enableRefreshToken: true, enableRefreshToken: true,
name: import.meta.env.VITE_APP_TITLE, name: import.meta.env.VITE_APP_TITLE,
}, },
copyright: appCopyrightPreferences,
logo: { logo: {
source: '/resource/img/logo.png', 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, accessToken: user.access_token,
tokenType: user.token_type, tokenType: user.token_type,
refreshToken: user.refresh_token ?? '', refreshToken: user.refresh_token ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expiresIn: user.expires_in!, expiresIn: user.expires_in!,
}); });
} catch (error: any) { } catch (error: any) {
@ -81,7 +80,6 @@ export const useAuthStore = defineStore('auth', () => {
accessToken: user.access_token, accessToken: user.access_token,
tokenType: user.token_type, tokenType: user.token_type,
refreshToken: user.refresh_token ?? '', refreshToken: user.refresh_token ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expiresIn: user.expires_in!, expiresIn: user.expires_in!,
}, },
onSuccess, onSuccess,
@ -104,7 +102,6 @@ export const useAuthStore = defineStore('auth', () => {
accessToken: user.access_token, accessToken: user.access_token,
tokenType: user.token_type, tokenType: user.token_type,
refreshToken: user.refresh_token ?? '', refreshToken: user.refresh_token ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expiresIn: user.expires_in!, expiresIn: user.expires_in!,
}, },
onSuccess, onSuccess,
@ -132,7 +129,6 @@ export const useAuthStore = defineStore('auth', () => {
accessToken: user.access_token, accessToken: user.access_token,
tokenType: user.token_type, tokenType: user.token_type,
refreshToken: user.refresh_token ?? '', refreshToken: user.refresh_token ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expiresIn: user.expires_in!, expiresIn: user.expires_in!,
}, },
onSuccess, onSuccess,
@ -163,7 +159,6 @@ export const useAuthStore = defineStore('auth', () => {
accessToken: user.access_token, accessToken: user.access_token,
tokenType: user.token_type, tokenType: user.token_type,
refreshToken: user.refresh_token ?? '', refreshToken: user.refresh_token ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expiresIn: user.expires_in!, expiresIn: user.expires_in!,
}, },
onSuccess, onSuccess,
@ -190,7 +185,6 @@ export const useAuthStore = defineStore('auth', () => {
accessToken: user.access_token, accessToken: user.access_token,
tokenType: user.token_type, tokenType: user.token_type,
refreshToken: user.refresh_token ?? '', refreshToken: user.refresh_token ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expiresIn: user.expires_in!, expiresIn: user.expires_in!,
}, },
onSuccess, onSuccess,
@ -228,7 +222,6 @@ export const useAuthStore = defineStore('auth', () => {
} }
async function fetchUserInfo() { async function fetchUserInfo() {
let userInfo: null | (UserInfo & { [key: string]: any }) = null;
let userInfoRes: { [key: string]: any } = {}; let userInfoRes: { [key: string]: any } = {};
const user = await oAuthService.getUser(); const user = await oAuthService.getUser();
if (user) { if (user) {
@ -237,7 +230,7 @@ export const useAuthStore = defineStore('auth', () => {
const abpConfig = await getConfigApi({ const abpConfig = await getConfigApi({
includeLocalizationResources: false, includeLocalizationResources: false,
}); });
userInfo = { const userInfo = {
userId: userInfoRes.sub ?? abpConfig.currentUser.id, userId: userInfoRes.sub ?? abpConfig.currentUser.id,
username: userInfoRes.uniqueName ?? abpConfig.currentUser.userName, username: userInfoRes.uniqueName ?? abpConfig.currentUser.userName,
realName: 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" ref="codeLogin"
:form-schema="formSchema" :form-schema="formSchema"
:loading="loading" :loading="loading"
@submit="handleLogin" @submit="handleLogin" />
/>
</template> </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" :form-schema="formSchema"
:submit-button-text="$t('AbpAccount.ResetPassword')" :submit-button-text="$t('AbpAccount.ResetPassword')"
:loading="loading" :loading="loading"
@submit="handleSubmit" @submit="handleSubmit" />
/>
</template> </template>

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

@ -162,8 +162,7 @@ onMounted(onInit);
:form-schema="formSchema" :form-schema="formSchema"
:loading="authStore.loginLoading" :loading="authStore.loginLoading"
:show-register="isTrue('Abp.Account.IsSelfRegistrationEnabled')" :show-register="isTrue('Abp.Account.IsSelfRegistrationEnabled')"
@submit="onLogin" @submit="onLogin">
>
<!-- 第三方登录 --> <!-- 第三方登录 -->
<template #third-party-login> <template #third-party-login>
<ThirdPartyLogin /> <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 <QrCodeLogin
:login-path="LOGIN_PATH" :login-path="LOGIN_PATH"
@confirm="onConfirm" @confirm="onConfirm"
:default-avatar="preferences.app.defaultAvatar" :default-avatar="preferences.app.defaultAvatar" />
/>
</template> </template>

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

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

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

@ -105,8 +105,7 @@ async function onInit() {
<MySetting <MySetting
:bind-items="externalLogins" :bind-items="externalLogins"
@on-bind-init="onInit" @on-bind-init="onInit"
@on-confirm="onBindComfirm" @on-confirm="onBindComfirm" />
/>
<WechatWorkUserBindModal @on-login="onBindWorkWeixin" /> <WechatWorkUserBindModal @on-login="onBindWorkWeixin" />
</Page> </Page>
</template> </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: 310 },
{ name: '技术支持', value: 274 }, { name: '技术支持', value: 274 },
{ name: '远程', value: 400 }, { name: '远程', value: 400 },
].sort((a, b) => { ].toSorted((a, b) => {
return a.value - b.value; return a.value - b.value;
}), }),
name: '商业占比', name: '商业占比',

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

@ -35,8 +35,7 @@ function notify(type: NotificationType) {
<template> <template>
<Page <Page
description="支持多语言,主题功能集成切换等" description="支持多语言,主题功能集成切换等"
title="Ant Design Vue组件使用演示" title="Ant Design Vue组件使用演示">
>
<Card class="mb-5" title="按钮"> <Card class="mb-5" title="按钮">
<Space> <Space>
<Button>Default</Button> <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", "$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web-app.json", "extends": "@vben/tsconfig/web-app.json",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"paths": { "paths": {
"#/*": ["./src/*"] "#/*": ["./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 { 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) => { export default eventHandler((event) => {
const userinfo = verifyAccessToken(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 { import {
clearRefreshTokenCookie, clearRefreshTokenCookie,
setRefreshTokenCookie, setRefreshTokenCookie,
} from '~/utils/cookie-utils'; } from '~/utils/cookie-utils';
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-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) => { export default defineEventHandler(async (event) => {
const { password, username } = await readBody(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 { import {
clearRefreshTokenCookie, clearRefreshTokenCookie,
getRefreshTokenFromCookie, getRefreshTokenFromCookie,
} from '~/utils/cookie-utils'; } from '~/utils/cookie-utils';
import { useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(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 { import {
clearRefreshTokenCookie, clearRefreshTokenCookie,
getRefreshTokenFromCookie, getRefreshTokenFromCookie,
setRefreshTokenCookie, setRefreshTokenCookie,
} from '~/utils/cookie-utils'; } 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'; import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => { 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) => { export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event); const userinfo = verifyAccessToken(event);
if (!userinfo) { 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 { 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) => { export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(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) => { export default eventHandler((event) => {
const { status } = getQuery(event); const { status } = getQuery(event);
setResponseStatus(event, Number(status)); 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 { verifyAccessToken } from '~/utils/jwt-utils';
import { import {
sleep, 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 { verifyAccessToken } from '~/utils/jwt-utils';
import { import {
sleep, 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 { verifyAccessToken } from '~/utils/jwt-utils';
import { import {
sleep, sleep,

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

@ -1,4 +1,5 @@
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response'; 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 { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data'; import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response'; 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 { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data'; import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse } from '~/utils/response'; import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const namesMap: Record<string, any> = {}; 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 { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data'; 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 }; 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 { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; import { verifyAccessToken } from '~/utils/jwt-utils';
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data'; import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response'; 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 { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response'; import {
sleep,
unAuthorizedResponse,
usePageResponseSuccess,
} from '~/utils/response';
function generateMockDataList(count: number) { function generateMockDataList(count: number) {
const dataList = []; const dataList = [];
@ -44,30 +49,69 @@ export default eventHandler(async (event) => {
await sleep(600); await sleep(600);
const { page, pageSize, sortBy, sortOrder } = getQuery(event); 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); 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) => { listData.sort((a, b) => {
if (sortOrder === 'asc') { const aValue = a[sortKey] as unknown;
if (sortBy === 'price') { const bValue = b[sortKey] as unknown;
return (
Number.parseFloat(a[sortBy as string]) - let result: number;
Number.parseFloat(b[sortBy as string])
); 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 { } else {
return a[sortBy as string] > b[sortBy as string] ? 1 : -1; result = aValue ? 1 : -1;
} }
} else { } else {
if (sortBy === 'price') { const aStr = String(aValue);
return ( const bStr = String(bValue);
Number.parseFloat(b[sortBy as string]) - const aNum = Number(aStr);
Number.parseFloat(a[sortBy as string]) const bNum = Number(bStr);
); result =
} else { Number.isFinite(aNum) && Number.isFinite(bNum)
return a[sortBy as string] < b[sortBy as string] ? 1 : -1; ? 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'); 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'); 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 { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response'; import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => { export default eventHandler((event) => {
const userinfo = verifyAccessToken(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 { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response'; import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => { export default eventHandler((event) => {
const userinfo = verifyAccessToken(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'; import { forbiddenResponse, sleep } from '~/utils/response';
export default defineEventHandler(async (event) => { 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'; import errorHandler from './error';
process.env.COMPATIBILITY_DATE = new Date().toISOString(); process.env.COMPATIBILITY_DATE = new Date().toISOString();

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

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

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

@ -1,3 +1,5 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => { export default defineEventHandler(() => {
return ` return `
<h1>Hello Vben Admin</h1> <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 type { EventHandlerRequest, H3Event } from 'h3';
import { deleteCookie, getCookie, setCookie } from 'h3';
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) { export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
deleteCookie(event, 'jwt', { deleteCookie(event, 'jwt', {
httpOnly: true, httpOnly: true,

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

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

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

@ -7,6 +7,11 @@ export interface UserInfo {
homePath?: string; homePath?: string;
} }
export interface TimezoneOption {
offset: number;
timezone: string;
}
export const MOCK_USERS: UserInfo[] = [ export const MOCK_USERS: UserInfo[] = [
{ {
id: 0, id: 0,
@ -276,7 +281,7 @@ export const MOCK_MENU_LIST = [
children: [ children: [
{ {
id: 20_401, id: 20_401,
pid: 201, pid: 202,
name: 'SystemDeptCreate', name: 'SystemDeptCreate',
status: 1, status: 1,
type: 'button', type: 'button',
@ -285,7 +290,7 @@ export const MOCK_MENU_LIST = [
}, },
{ {
id: 20_402, id: 20_402,
pid: 201, pid: 202,
name: 'SystemDeptEdit', name: 'SystemDeptEdit',
status: 1, status: 1,
type: 'button', type: 'button',
@ -294,7 +299,7 @@ export const MOCK_MENU_LIST = [
}, },
{ {
id: 20_403, id: 20_403,
pid: 201, pid: 202,
name: 'SystemDeptDelete', name: 'SystemDeptDelete',
status: 1, status: 1,
type: 'button', type: 'button',
@ -388,3 +393,29 @@ export function getMenuIds(menus: any[]) {
}); });
return ids; 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 type { EventHandlerRequest, H3Event } from 'h3';
import { setResponseStatus } from 'h3';
export function useResponseSuccess<T = any>(data: T) { export function useResponseSuccess<T = any>(data: T) {
return { return {
code: 0, 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="author" content="Vben" />
<meta <meta
name="viewport" 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 文件内配置 --> <!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
<title><%= VITE_APP_TITLE %></title> <title>%VITE_APP_TITLE%</title>
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<script> <script>
// 生产环境下注入百度统计 // 生产环境下注入百度统计

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

@ -1,6 +1,6 @@
{ {
"name": "@vben/web-antd", "name": "@vben/web-antd",
"version": "5.5.7", "version": "5.7.0",
"homepage": "https://vben.pro", "homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": { "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 使, * 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 type { Recordable } from '@vben/types';
import { import {
computed,
defineAsyncComponent, defineAsyncComponent,
defineComponent, defineComponent,
getCurrentInstance,
h, h,
nextTick,
onMounted,
onUnmounted,
ref, ref,
render,
unref,
watch,
} from 'vue'; } 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 { $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( const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'), () => import('ant-design-vue/es/auto-complete'),
@ -65,10 +122,17 @@ const TimePicker = defineAsyncComponent(
const TreeSelect = defineAsyncComponent( const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'), () => 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 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>( const withDefaultPlaceholder = (
component: T, component: Component,
type: 'input' | 'select', type: 'input' | 'select',
componentProps: Recordable<any> = {}, componentProps: Recordable<any> = {},
) => { ) => {
@ -82,16 +146,15 @@ const withDefaultPlaceholder = <T extends Component>(
$t(`ui.placeholder.${type}`); $t(`ui.placeholder.${type}`);
// 透传组件暴露的方法 // 透传组件暴露的方法
const innerRef = ref(); const innerRef = ref();
const publicApi: Recordable<any> = {}; expose(
expose(publicApi); new Proxy(
const instance = getCurrentInstance(); {},
instance?.proxy?.$nextTick(() => { {
for (const key in innerRef.value) { get: (_target, key) => innerRef.value?.[key],
if (typeof innerRef.value[key] === 'function') { has: (_target, key) => key in (innerRef.value || {}),
publicApi[key] = innerRef.value[key]; },
} ),
} );
});
return () => return () =>
h( h(
component, 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 = export type ComponentType =
| 'ApiCascader'
| 'ApiSelect' | 'ApiSelect'
| 'ApiTreeSelect' | 'ApiTreeSelect'
| 'AutoComplete' | 'AutoComplete'
| 'Cascader'
| 'Checkbox' | 'Checkbox'
| 'CheckboxGroup' | 'CheckboxGroup'
| 'DatePicker' | 'DatePicker'
@ -131,40 +626,68 @@ export type ComponentType =
| 'Upload' | 'Upload'
| BaseFormComponentType; | 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() { async function initComponentAdapter() {
const components: Partial<Record<ComponentType, Component>> = { const components: Partial<Record<ComponentType, Component>> = {
// 如果你的组件体积比较大,可以使用异步加载 // 如果你的组件体积比较大,可以使用异步加载
// Button: () => // Button: () =>
// import('xxx').then((res) => res.Button), // import('xxx').then((res) => res.Button),
ApiSelect: withDefaultPlaceholder(
{ ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', {
...ApiComponent, component: Cascader,
name: 'ApiSelect', fieldNames: { label: 'label', value: 'value', children: 'children' },
}, loadingSlot: 'suffixIcon',
'select', modelPropName: 'value',
{ visibleEvent: 'onVisibleChange',
component: Select, }),
loadingSlot: 'suffixIcon', ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
visibleEvent: 'onDropdownVisibleChange', component: Select,
modelPropName: 'value', loadingSlot: 'suffixIcon',
}, modelPropName: 'value',
), visibleEvent: 'onVisibleChange',
ApiTreeSelect: withDefaultPlaceholder( }),
{ ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
...ApiComponent, component: TreeSelect,
name: 'ApiTreeSelect', fieldNames: { label: 'label', value: 'value', children: 'children' },
}, loadingSlot: 'suffixIcon',
'select', modelPropName: 'value',
{ optionsPropName: 'treeData',
component: TreeSelect, visibleEvent: 'onVisibleChange',
fieldNames: { label: 'label', value: 'value', children: 'children' }, }),
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
},
),
AutoComplete, AutoComplete,
Cascader,
Checkbox, Checkbox,
CheckboxGroup, CheckboxGroup,
DatePicker, DatePicker,
@ -179,7 +702,9 @@ async function initComponentAdapter() {
modelValueProp: 'value', modelValueProp: 'value',
}), }),
Input: withDefaultPlaceholder(Input, 'input'), Input: withDefaultPlaceholder(Input, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input'), InputNumber: withDefaultPlaceholder(InputNumber, 'input', {
style: { width: '100%' },
}),
InputPassword: withDefaultPlaceholder(InputPassword, 'input'), InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
Mentions: withDefaultPlaceholder(Mentions, 'input'), Mentions: withDefaultPlaceholder(Mentions, 'input'),
// 自定义主要按钮 // 自定义主要按钮
@ -196,7 +721,7 @@ async function initComponentAdapter() {
Textarea: withDefaultPlaceholder(Textarea, 'input'), Textarea: withDefaultPlaceholder(Textarea, 'input'),
TimePicker, TimePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'), TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload, Upload: withPreviewUpload(),
}; };
// 将组件注册到全局共享状态中 // 将组件注册到全局共享状态中

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

@ -1,9 +1,9 @@
import type { import type {
VbenFormProps as FormProps,
VbenFormSchema as FormSchema, VbenFormSchema as FormSchema,
VbenFormProps,
} from '@vben/common-ui'; } 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 { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales'; 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 { initSetupVbenForm, useVbenForm, z };
export type VbenFormSchema = FormSchema<ComponentType>; export type VbenFormSchema = FormSchema<ComponentType, ComponentPropsMap>;
export type { VbenFormProps }; 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 { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { ComponentPropsMap, ComponentType } from './component';
import { h } from 'vue'; 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'; import { Button, Image } from 'ant-design-vue';
@ -40,9 +45,10 @@ setupVbenVxeTable({
// 表格配置项可以用 cellRender: { name: 'CellImage' }, // 表格配置项可以用 cellRender: { name: 'CellImage' },
vxeUI.renderer.add('CellImage', { vxeUI.renderer.add('CellImage', {
renderTableDefault(_renderOpts, params) { renderTableDefault(renderOpts, params) {
const { props } = renderOpts;
const { column, row } = params; 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, 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'; 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 appName = computed(() => preferences.app.name);
const logo = computed(() => preferences.logo.source); const logo = computed(() => preferences.logo.source);
const logoDark = computed(() => preferences.logo.sourceDark);
</script> </script>
<template> <template>
<AuthPageLayout <AuthPageLayout
:app-name="appName" :app-name="appName"
:logo="logo" :logo="logo"
:logo-dark="logoDark"
:page-description="$t('authentication.pageDesc')" :page-description="$t('authentication.pageDesc')"
:page-title="$t('authentication.pageTitle')" :page-title="$t('authentication.pageTitle')">
>
<!-- 自定义工具栏 --> <!-- 自定义工具栏 -->
<!-- <template #toolbar></template> --> <!-- <template #toolbar></template> -->
</AuthPageLayout> </AuthPageLayout>

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

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

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

@ -6,7 +6,9 @@
"about": "About", "about": "About",
"document": "Document", "document": "Document",
"antdv": "Ant Design Vue Version", "antdv": "Ant Design Vue Version",
"antdv-next": "Antdv Next Version",
"naive-ui": "Naive UI 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", "register": "Register",
"codeLogin": "Code Login", "codeLogin": "Code Login",
"qrcodeLogin": "Qr Code Login", "qrcodeLogin": "Qr Code Login",
"forgetPassword": "Forget Password" "forgetPassword": "Forget Password",
"profile": "Profile"
}, },
"dashboard": { "dashboard": {
"title": "Dashboard", "title": "Dashboard",

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

@ -6,7 +6,9 @@
"about": "关于", "about": "关于",
"document": "文档", "document": "文档",
"antdv": "Ant Design Vue 版本", "antdv": "Ant Design Vue 版本",
"antdv-next": "Antdv Next 版本",
"naive-ui": "Naive UI 版本", "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": "注册", "register": "注册",
"codeLogin": "验证码登录", "codeLogin": "验证码登录",
"qrcodeLogin": "二维码登录", "qrcodeLogin": "二维码登录",
"forgetPassword": "忘记密码" "forgetPassword": "忘记密码",
"profile": "个人中心"
}, },
"dashboard": { "dashboard": {
"title": "概览", "title": "概览",

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

@ -1,7 +1,7 @@
import { initPreferences } from '@vben/preferences'; import { initPreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils'; import { unmountGlobalLoading } from '@vben/utils';
import { overridesPreferences } from './preferences'; import { overridesPreferences, preferencesExtension } from './preferences';
/** /**
* *
@ -15,6 +15,7 @@ async function initApplication() {
// app偏好设置初始化 // app偏好设置初始化
await initPreferences({ await initPreferences({
extension: preferencesExtension,
namespace, namespace,
overrides: overridesPreferences, 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 * @description
@ -10,4 +21,54 @@ export const overridesPreferences = defineOverridesPreferences({
app: { app: {
name: import.meta.env.VITE_APP_TITLE, 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 type { RouteRecordRaw } from 'vue-router';
import { import {
VBEN_ANTDV_NEXT_PREVIEW_URL,
VBEN_DOC_URL, VBEN_DOC_URL,
VBEN_ELE_PREVIEW_URL, VBEN_ELE_PREVIEW_URL,
VBEN_GITHUB_URL, VBEN_GITHUB_URL,
VBEN_LOGO_URL, VBEN_LOGO_URL,
VBEN_NAIVE_PREVIEW_URL, VBEN_NAIVE_PREVIEW_URL,
VBEN_TD_PREVIEW_URL,
} from '@vben/constants'; } from '@vben/constants';
import { SvgAntdvNextLogoIcon, SvgTDesignIcon } from '@vben/icons';
import { IFrameView } from '#/layouts'; import { IFrameView } from '#/layouts';
import { $t } from '#/locales'; import { $t } from '#/locales';
@ -42,6 +45,17 @@ const routes: RouteRecordRaw[] = [
title: 'Github', 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', name: 'VbenNaive',
path: '/vben-admin/naive', path: '/vben-admin/naive',
@ -53,6 +67,17 @@ const routes: RouteRecordRaw[] = [
title: $t('demos.vben.naive-ui'), 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', name: 'VbenElementPlus',
path: '/vben-admin/ele', path: '/vben-admin/ele',
@ -76,6 +101,16 @@ const routes: RouteRecordRaw[] = [
order: 9999, 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; 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() { async function fetchUserInfo() {
let userInfo: null | UserInfo = null; const userInfo = await getUserInfoApi();
userInfo = await getUserInfoApi();
userStore.setUserInfo(userInfo); userStore.setUserInfo(userInfo);
return 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 登录表单数据 * @param values 登录表单数据
*/ */
async function handleLogin(values: Recordable<any>) { async function handleLogin(values: Recordable<any>) {
// eslint-disable-next-line no-console void values;
console.log(values);
} }
</script> </script>
@ -64,6 +63,5 @@ async function handleLogin(values: Recordable<any>) {
<AuthenticationCodeLogin <AuthenticationCodeLogin
:form-schema="formSchema" :form-schema="formSchema"
:loading="loading" :loading="loading"
@submit="handleLogin" @submit="handleLogin" />
/>
</template> </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>) { function handleSubmit(value: Recordable<any>) {
// eslint-disable-next-line no-console void value;
console.log('reset email:', value);
} }
</script> </script>
@ -38,6 +37,5 @@ function handleSubmit(value: Recordable<any>) {
<AuthenticationForgetPassword <AuthenticationForgetPassword
:form-schema="formSchema" :form-schema="formSchema"
:loading="loading" :loading="loading"
@submit="handleSubmit" @submit="handleSubmit" />
/>
</template> </template>

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

@ -93,6 +93,5 @@ const formSchema = computed((): VbenFormSchema[] => {
<AuthenticationLogin <AuthenticationLogin
:form-schema="formSchema" :form-schema="formSchema"
:loading="authStore.loginLoading" :loading="authStore.loginLoading"
@submit="authStore.authLogin" @submit="authStore.authLogin" />
/>
</template> </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>) { function handleSubmit(value: Recordable<any>) {
// eslint-disable-next-line no-console void value;
console.log('register submit:', value);
} }
</script> </script>
@ -91,6 +90,5 @@ function handleSubmit(value: Recordable<any>) {
<AuthenticationRegister <AuthenticationRegister
:form-schema="formSchema" :form-schema="formSchema"
:loading="loading" :loading="loading"
@submit="handleSubmit" @submit="handleSubmit" />
/>
</template> </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: 310 },
{ name: '技术支持', value: 274 }, { name: '技术支持', value: 274 },
{ name: '远程', value: 400 }, { name: '远程', value: 400 },
].sort((a, b) => { ].toSorted((a, b) => {
return a.value - b.value; return a.value - b.value;
}), }),
name: '商业占比', name: '商业占比',

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

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

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

Loading…
Cancel
Save