commit ec7da12e7b55b8801ad12fb65222f33981f88a40
Author: shichao <17568097882@163.com>
Date: Sat Jan 13 10:56:48 2024 +0800
first commit
diff --git a/.browserslistrc b/.browserslistrc
new file mode 100644
index 0000000..dc3bc09
--- /dev/null
+++ b/.browserslistrc
@@ -0,0 +1,4 @@
+> 1%
+last 2 versions
+not dead
+not ie 11
diff --git a/.commitlintrc.cjs b/.commitlintrc.cjs
new file mode 100644
index 0000000..151ead3
--- /dev/null
+++ b/.commitlintrc.cjs
@@ -0,0 +1,107 @@
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const scopes = fs
+ .readdirSync(path.resolve(__dirname, 'src'), { withFileTypes: true })
+ .filter((dirent) => dirent.isDirectory())
+ .map((dirent) => dirent.name.replace(/s$/, ''));
+
+// precomputed scope
+const scopeComplete = execSync('git status --porcelain || true')
+ .toString()
+ .trim()
+ .split('\n')
+ .find((r) => ~r.indexOf('M src'))
+ ?.replace(/(\/)/g, '%%')
+ ?.match(/src%%((\w|-)*)/)?.[1]
+ ?.replace(/s$/, '');
+
+/** @type {import('cz-git').UserConfig} */
+module.exports = {
+ ignores: [(commit) => commit.includes('init')],
+ extends: ['@commitlint/config-conventional'],
+ rules: {
+ 'body-leading-blank': [2, 'always'],
+ 'footer-leading-blank': [1, 'always'],
+ 'header-max-length': [2, 'always', 108],
+ 'subject-empty': [2, 'never'],
+ 'type-empty': [2, 'never'],
+ 'subject-case': [0],
+ 'type-enum': [
+ 2,
+ 'always',
+ [
+ 'feat',
+ 'fix',
+ 'perf',
+ 'style',
+ 'docs',
+ 'test',
+ 'refactor',
+ 'build',
+ 'ci',
+ 'chore',
+ 'revert',
+ 'wip',
+ 'workflow',
+ 'types',
+ 'release',
+ ],
+ ],
+ },
+ prompt: {
+ /** @use `yarn commit :f` */
+ alias: {
+ f: 'docs: fix typos',
+ r: 'docs: update README',
+ s: 'style: update code format',
+ b: 'build: bump dependencies',
+ c: 'chore: update config',
+ },
+ customScopesAlign: !scopeComplete ? 'top' : 'bottom',
+ defaultScope: scopeComplete,
+ scopes: [...scopes, 'mock'],
+ allowEmptyIssuePrefixs: false,
+ allowCustomIssuePrefixs: false,
+
+ // English
+ typesAppend: [
+ { value: 'wip', name: 'wip: work in process' },
+ { value: 'workflow', name: 'workflow: workflow improvements' },
+ { value: 'types', name: 'types: type definition file changes' },
+ ],
+
+ // 中英文对照版
+ // messages: {
+ // type: '选择你要提交的类型 :',
+ // scope: '选择一个提交范围 (可选):',
+ // customScope: '请输入自定义的提交范围 :',
+ // subject: '填写简短精炼的变更描述 :\n',
+ // body: '填写更加详细的变更描述 (可选)。使用 "|" 换行 :\n',
+ // breaking: '列举非兼容性重大的变更 (可选)。使用 "|" 换行 :\n',
+ // footerPrefixsSelect: '选择关联issue前缀 (可选):',
+ // customFooterPrefixs: '输入自定义issue前缀 :',
+ // footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
+ // confirmCommit: '是否提交或修改commit ?',
+ // },
+ // types: [
+ // { value: 'feat', name: 'feat: 新增功能' },
+ // { value: 'fix', name: 'fix: 修复缺陷' },
+ // { value: 'docs', name: 'docs: 文档变更' },
+ // { value: 'style', name: 'style: 代码格式' },
+ // { value: 'refactor', name: 'refactor: 代码重构' },
+ // { value: 'perf', name: 'perf: 性能优化' },
+ // { value: 'test', name: 'test: 添加疏漏测试或已有测试改动' },
+ // { value: 'build', name: 'build: 构建流程、外部依赖变更 (如升级 npm 包、修改打包配置等)' },
+ // { value: 'ci', name: 'ci: 修改 CI 配置、脚本' },
+ // { value: 'revert', name: 'revert: 回滚 commit' },
+ // { value: 'chore', name: 'chore: 对构建过程或辅助工具和库的更改 (不影响源文件、测试用例)' },
+ // { value: 'wip', name: 'wip: 正在开发中' },
+ // { value: 'workflow', name: 'workflow: 工作流程改进' },
+ // { value: 'types', name: 'types: 类型定义文件修改' },
+ // ],
+ // emptyScopesAlias: 'empty: 不填写',
+ // customScopesAlias: 'custom: 自定义',
+ },
+};
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..8617652
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,3 @@
+node_modules/
+dist/
+.vscode/
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..dccf841
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,19 @@
+root = true
+
+[*]
+charset=utf-8
+end_of_line=lf
+insert_final_newline=true
+indent_style=space
+indent_size=2
+max_line_length = 100
+
+[*.{yml,yaml,json}]
+indent_style = space
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
+
+[Makefile]
+indent_style = tab
diff --git a/.env b/.env
new file mode 100644
index 0000000..c6c030e
--- /dev/null
+++ b/.env
@@ -0,0 +1,2 @@
+# 标题
+VITE_GLOB_APP_TITLE = 费县智慧林业防灭火平台
diff --git a/.env.analyze b/.env.analyze
new file mode 100644
index 0000000..1ff51cf
--- /dev/null
+++ b/.env.analyze
@@ -0,0 +1,23 @@
+# Whether to open mock
+VITE_USE_MOCK = true
+
+# public path
+VITE_PUBLIC_PATH = /
+
+# Whether to enable gzip or brotli compression
+# Optional: gzip | brotli | none
+# If you need multiple forms, you can use `,` to separate
+VITE_BUILD_COMPRESS = 'none'
+
+
+# Basic interface address SPA
+VITE_GLOB_API_URL=/basic-api
+
+# File upload address, optional
+# It can be forwarded by nginx or write the actual address directly
+VITE_GLOB_UPLOAD_URL=/upload
+
+# Interface prefix
+VITE_GLOB_API_URL_PREFIX=
+
+VITE_ENABLE_ANALYZE = true
diff --git a/.env.development b/.env.development
new file mode 100644
index 0000000..ed7a6b2
--- /dev/null
+++ b/.env.development
@@ -0,0 +1,14 @@
+# Whether to open mock
+VITE_USE_MOCK = true
+
+# public path
+VITE_PUBLIC_PATH = /
+
+# Basic interface address SPA
+VITE_GLOB_API_URL=/basic-api
+
+# File upload address, optional
+VITE_GLOB_UPLOAD_URL=/upload
+
+# Interface prefix
+VITE_GLOB_API_URL_PREFIX=
diff --git a/.env.production b/.env.production
new file mode 100644
index 0000000..c672de6
--- /dev/null
+++ b/.env.production
@@ -0,0 +1,21 @@
+# Whether to open mock
+VITE_USE_MOCK = true
+
+# public path
+VITE_PUBLIC_PATH = /
+
+# Whether to enable gzip or brotli compression
+# Optional: gzip | brotli | none
+# If you need multiple forms, you can use `,` to separate
+VITE_BUILD_COMPRESS = 'none'
+
+
+# Basic interface address SPA
+VITE_GLOB_API_URL=/basic-api
+
+# File upload address, optional
+# It can be forwarded by nginx or write the actual address directly
+VITE_GLOB_UPLOAD_URL=/upload
+
+# Interface prefix
+VITE_GLOB_API_URL_PREFIX=
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..67ac532
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,16 @@
+
+*.sh
+node_modules
+*.md
+*.woff
+*.ttf
+.vscode
+.idea
+dist
+/public
+/docs
+.husky
+.local
+/bin
+Dockerfile
+package.json
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000..98fc3ef
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,7 @@
+module.exports = {
+ root: true,
+ extends: ['@vben'],
+ rules: {
+ 'no-undef': 'off',
+ },
+};
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..d4e5bd3
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,11 @@
+# https://docs.github.com/cn/get-started/getting-started-with-git/configuring-git-to-handle-line-endings
+
+# Automatically normalize line endings (to LF) for all text-based files.
+* text=auto eol=lf
+
+# Declare files that will always have CRLF line endings on checkout.
+*.{cmd,[cC][mM][dD]} text eol=crlf
+*.{bat,[bB][aA][tT]} text eol=crlf
+
+# Denote all files that are truly binary and should not be modified.
+*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa15905
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,34 @@
+node_modules
+.DS_Store
+dist
+.cache
+.turbo
+
+tests/server/static
+tests/server/static/upload
+
+.local
+# local env files
+.env.local
+.env.*.local
+.eslintcache
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+# .vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+package-lock.json
+pnpm-lock.yaml
+
+.history
diff --git a/.gitpod.yml b/.gitpod.yml
new file mode 100644
index 0000000..866381f
--- /dev/null
+++ b/.gitpod.yml
@@ -0,0 +1,6 @@
+ports:
+ - port: 3344
+ onOpen: open-preview
+tasks:
+ - init: pnpm install
+ command: pnpm run dev
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..29f68cf
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,7 @@
+public-hoist-pattern[]=husky
+public-hoist-pattern[]=*eslint*
+public-hoist-pattern[]=*prettier*
+public-hoist-pattern[]=lint-staged
+public-hoist-pattern[]=*stylelint*
+public-hoist-pattern[]=@commitlint/cli
+public-hoist-pattern[]=@vben/eslint-config
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..24531e6
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,12 @@
+dist
+.local
+.output.js
+node_modules
+
+**/*.svg
+**/*.sh
+
+public
+.npmrc
+
+*-lock.yaml
diff --git a/.prettierrc.cjs b/.prettierrc.cjs
new file mode 100644
index 0000000..4a24e88
--- /dev/null
+++ b/.prettierrc.cjs
@@ -0,0 +1,19 @@
+module.exports = {
+ printWidth: 100,
+ semi: true,
+ vueIndentScriptAndStyle: true,
+ singleQuote: true,
+ trailingComma: 'all',
+ proseWrap: 'never',
+ htmlWhitespaceSensitivity: 'strict',
+ endOfLine: 'auto',
+ plugins: ['prettier-plugin-packagejson'],
+ overrides: [
+ {
+ files: '.*rc',
+ options: {
+ parser: 'json',
+ },
+ },
+ ],
+};
diff --git a/.stylelintignore b/.stylelintignore
new file mode 100644
index 0000000..6cd69e0
--- /dev/null
+++ b/.stylelintignore
@@ -0,0 +1,2 @@
+dist
+public
diff --git a/.stylelintrc.cjs b/.stylelintrc.cjs
new file mode 100644
index 0000000..65320e7
--- /dev/null
+++ b/.stylelintrc.cjs
@@ -0,0 +1,4 @@
+module.exports = {
+ root: true,
+ extends: ['@vben/stylelint-config'],
+};
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..3f7ad41
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,14 @@
+{
+ "recommendations": [
+ "vue.volar",
+ "dbaeumer.vscode-eslint",
+ "stylelint.vscode-stylelint",
+ "esbenp.prettier-vscode",
+ "mrmlnc.vscode-less",
+ "lokalise.i18n-ally",
+ "antfu.iconify",
+ "antfu.unocss",
+ "mikestead.dotenv",
+ "vue.vscode-typescript-vue-plugin"
+ ]
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..720a6df
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,13 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "chrome",
+ "request": "launch",
+ "name": "Launch Chrome",
+ "url": "http://localhost:3100",
+ "webRoot": "${workspaceFolder}/src",
+ "sourceMaps": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..fcbab54
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,188 @@
+{
+ "typescript.tsdk": "./node_modules/typescript/lib",
+ "volar.tsPlugin": true,
+ "volar.tsPluginStatus": false,
+ "npm.packageManager": "pnpm",
+ "editor.tabSize": 2,
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "files.eol": "\n",
+ "search.exclude": {
+ "**/node_modules": true,
+ "**/*.log": true,
+ "**/*.log*": true,
+ "**/bower_components": true,
+ "**/dist": true,
+ "**/elehukouben": true,
+ "**/.git": true,
+ "**/.gitignore": true,
+ "**/.svn": true,
+ "**/.DS_Store": true,
+ "**/.idea": true,
+ "**/.vscode": false,
+ "**/yarn.lock": true,
+ "**/tmp": true,
+ "out": true,
+ "dist": true,
+ "node_modules": true,
+ "CHANGELOG.md": true,
+ "examples": true,
+ "res": true,
+ "screenshots": true,
+ "yarn-error.log": true,
+ "**/.yarn": true
+ },
+ "files.exclude": {
+ "**/.cache": true,
+ "**/.editorconfig": true,
+ "**/.eslintcache": true,
+ "**/bower_components": true,
+ "**/.idea": true,
+ "**/tmp": true,
+ "**/.git": true,
+ "**/.svn": true,
+ "**/.hg": true,
+ "**/CVS": true,
+ "**/.DS_Store": true
+ },
+ "files.watcherExclude": {
+ "**/.git/objects/**": true,
+ "**/.git/subtree-cache/**": true,
+ "**/.vscode/**": true,
+ "**/node_modules/**": true,
+ "**/tmp/**": true,
+ "**/bower_components/**": true,
+ "**/dist/**": true,
+ "**/yarn.lock": true
+ },
+ "stylelint.enable": true,
+ "stylelint.validate": [
+ "css",
+ "less",
+ "postcss",
+ "scss",
+ "vue",
+ "sass"
+ ],
+ "path-intellisense.mappings": {
+ "@/": "${workspaceRoot}/src"
+ },
+ "[javascriptreact]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[typescript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[typescriptreact]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[html]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[css]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[less]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[scss]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[markdown]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "editor.codeActionsOnSave": {
+ "source.fixAll.eslint": "explicit",
+ "source.fixAll.stylelint": "explicit"
+ },
+ "[vue]": {
+ "editor.codeActionsOnSave": {
+ "source.fixAll.eslint": "explicit",
+ "source.fixAll.stylelint": "explicit"
+ },
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "i18n-ally.localesPaths": [
+ "src/locales/lang"
+ ],
+ "i18n-ally.keystyle": "nested",
+ "i18n-ally.sortKeys": true,
+ "i18n-ally.namespace": true,
+ "i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}",
+ "i18n-ally.enabledParsers": [
+ "json"
+ ],
+ "i18n-ally.sourceLanguage": "en",
+ "i18n-ally.displayLanguage": "zh-CN",
+ "i18n-ally.enabledFrameworks": [
+ "vue",
+ "react"
+ ],
+ "cSpell.words": [
+ "antd",
+ "antv",
+ "brotli",
+ "browserslist",
+ "codemirror",
+ "commitlint",
+ "cropperjs",
+ "echarts",
+ "esnext",
+ "esno",
+ "iconify",
+ "INTLIFY",
+ "lintstagedrc",
+ "logicflow",
+ "mockjs",
+ "nprogress",
+ "pinia",
+ "pnpm",
+ "qrcode",
+ "sider",
+ "sortablejs",
+ "stylelint",
+ "tailwindcss",
+ "tinymce",
+ "unocss",
+ "unref",
+ "vben",
+ "vditor",
+ "Vite",
+ "vitejs",
+ "vueuse",
+ "zxcvbn"
+ ],
+ "vetur.format.scriptInitialIndent": true,
+ "vetur.format.styleInitialIndent": true,
+ "vetur.validation.script": false,
+ "MicroPython.executeButton": [
+ {
+ "text": "▶",
+ "tooltip": "运行",
+ "alignment": "left",
+ "command": "extension.executeFile",
+ "priority": 3.5
+ }
+ ],
+ "MicroPython.syncButton": [
+ {
+ "text": "$(sync)",
+ "tooltip": "同步",
+ "alignment": "left",
+ "command": "extension.execute",
+ "priority": 4
+ }
+ ],
+ // 控制相关文件嵌套展示
+ "explorer.fileNesting.enabled": true,
+ "explorer.fileNesting.expand": false,
+ "explorer.fileNesting.patterns": {
+ "*.ts": "$(capture).test.ts, $(capture).test.tsx",
+ "*.tsx": "$(capture).test.ts, $(capture).test.tsx",
+ "*.env": "$(capture).env.*",
+ "CHANGELOG.md": "CHANGELOG*",
+ "package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitattributes,.gitignore,.gitpod.yml,CNAME,README*,.npmrc,.browserslistrc",
+ ".eslintrc.cjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,.stylelintrc.*"
+ },
+ "terminal.integrated.scrollback": 10000,
+ "nuxt.isNuxtApp": false
+}
diff --git a/CNAME b/CNAME
new file mode 100644
index 0000000..3436928
--- /dev/null
+++ b/CNAME
@@ -0,0 +1 @@
+vben.vvbin.cn
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..16722a6
--- /dev/null
+++ b/index.html
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+ <%= VITE_GLOB_APP_TITLE %>
+
+
+
+
+
+
+
+

+
+
+
+
<%= VITE_GLOB_APP_TITLE %>
+
+
+
+
+
+
diff --git a/internal/eslint-config/.eslintignore b/internal/eslint-config/.eslintignore
new file mode 100644
index 0000000..cef44b3
--- /dev/null
+++ b/internal/eslint-config/.eslintignore
@@ -0,0 +1,9 @@
+
+*.sh
+node_modules
+*.md
+*.woff
+*.ttf
+.turbo
+dist
+package.json
diff --git a/internal/eslint-config/.eslintrc.cjs b/internal/eslint-config/.eslintrc.cjs
new file mode 100644
index 0000000..cd27a19
--- /dev/null
+++ b/internal/eslint-config/.eslintrc.cjs
@@ -0,0 +1,4 @@
+module.exports = {
+ root: true,
+ extends: ['@vben/eslint-config/strict'],
+};
diff --git a/internal/eslint-config/build.config.ts b/internal/eslint-config/build.config.ts
new file mode 100644
index 0000000..08301e5
--- /dev/null
+++ b/internal/eslint-config/build.config.ts
@@ -0,0 +1,10 @@
+import { defineBuildConfig } from 'unbuild';
+
+export default defineBuildConfig({
+ clean: true,
+ entries: ['src/index', 'src/strict'],
+ declaration: true,
+ rollup: {
+ emitCJS: true,
+ },
+});
diff --git a/internal/eslint-config/package.json b/internal/eslint-config/package.json
new file mode 100644
index 0000000..1288394
--- /dev/null
+++ b/internal/eslint-config/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "@vben/eslint-config",
+ "version": "1.0.0",
+ "private": true,
+ "homepage": "https://github.com/vbenjs/vue-vben-admin",
+ "bugs": {
+ "url": "https://github.com/vbenjs/vue-vben-admin/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+ "directory": "internal/eslint-config"
+ },
+ "license": "MIT",
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.cjs"
+ },
+ "./strict": {
+ "types": "./dist/strict.d.ts",
+ "import": "./dist/strict.mjs",
+ "require": "./dist/strict.cjs"
+ }
+ },
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.mjs",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "clean": "pnpm rimraf .turbo node_modules dist",
+ "lint": "pnpm eslint .",
+ "stub": "pnpm unbuild --stub"
+ },
+ "devDependencies": {
+ "@typescript-eslint/eslint-plugin": "^6.17.0",
+ "@typescript-eslint/parser": "^6.17.0",
+ "eslint": "^8.56.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-import": "^2.29.1",
+ "eslint-plugin-prettier": "^5.1.2",
+ "eslint-plugin-simple-import-sort": "^10.0.0",
+ "eslint-plugin-vue": "^9.19.2",
+ "vue-eslint-parser": "^9.3.2"
+ }
+}
diff --git a/internal/eslint-config/src/index.ts b/internal/eslint-config/src/index.ts
new file mode 100644
index 0000000..1138bb3
--- /dev/null
+++ b/internal/eslint-config/src/index.ts
@@ -0,0 +1,91 @@
+export default {
+ env: {
+ browser: true,
+ node: true,
+ es6: true,
+ },
+ parser: 'vue-eslint-parser',
+ parserOptions: {
+ parser: '@typescript-eslint/parser',
+ ecmaVersion: 2020,
+ sourceType: 'module',
+ jsxPragma: 'React',
+ ecmaFeatures: {
+ jsx: true,
+ },
+ project: './tsconfig.*?.json',
+ createDefaultProgram: false,
+ extraFileExtensions: ['.vue'],
+ },
+ plugins: ['vue', '@typescript-eslint', 'import'],
+ extends: [
+ 'eslint:recommended',
+ 'plugin:vue/vue3-recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:prettier/recommended',
+ ],
+ rules: {
+ 'no-unused-vars': 'off',
+ 'no-case-declarations': 'off',
+ 'no-use-before-define': 'off',
+ 'space-before-function-paren': 'off',
+
+ 'import/first': 'error',
+ 'import/newline-after-import': 'error',
+ 'import/no-duplicates': 'error',
+
+ '@typescript-eslint/no-unused-vars': [
+ 'error',
+ {
+ argsIgnorePattern: '^_',
+ varsIgnorePattern: '^_',
+ },
+ ],
+ '@typescript-eslint/ban-ts-ignore': 'off',
+ '@typescript-eslint/ban-ts-comment': 'off',
+ '@typescript-eslint/ban-types': 'off',
+ '@typescript-eslint/explicit-function-return-type': 'off',
+ '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/no-var-requires': 'off',
+ '@typescript-eslint/no-empty-function': 'off',
+ '@typescript-eslint/no-use-before-define': 'off',
+ '@typescript-eslint/no-non-null-assertion': 'off',
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
+ 'vue/script-setup-uses-vars': 'error',
+ 'vue/no-reserved-component-names': 'off',
+ 'vue/custom-event-name-casing': 'off',
+ 'vue/attributes-order': 'off',
+ 'vue/one-component-per-file': 'off',
+ 'vue/html-closing-bracket-newline': 'off',
+ 'vue/max-attributes-per-line': 'off',
+ 'vue/multiline-html-element-content-newline': 'off',
+ 'vue/singleline-html-element-content-newline': 'off',
+ 'vue/attribute-hyphenation': 'off',
+ 'vue/require-default-prop': 'off',
+ 'vue/require-explicit-emits': 'off',
+ 'vue/html-self-closing': [
+ 'error',
+ {
+ html: {
+ void: 'always',
+ normal: 'never',
+ component: 'always',
+ },
+ svg: 'always',
+ math: 'always',
+ },
+ ],
+ 'vue/multi-word-component-names': 'off',
+ // 'sort-imports': [
+ // 'error',
+ // {
+ // ignoreCase: true,
+ // ignoreDeclarationSort: false,
+ // ignoreMemberSort: false,
+ // memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
+ // allowSeparatedGroups: false,
+ // },
+ // ],
+ },
+ globals: { defineOptions: 'readonly' },
+};
diff --git a/internal/eslint-config/src/strict.ts b/internal/eslint-config/src/strict.ts
new file mode 100644
index 0000000..5dbf5b7
--- /dev/null
+++ b/internal/eslint-config/src/strict.ts
@@ -0,0 +1,57 @@
+export default {
+ extends: ['@vben'],
+ plugins: ['simple-import-sort'],
+ rules: {
+ 'simple-import-sort/imports': 'error',
+ 'simple-import-sort/exports': 'error',
+
+ '@typescript-eslint/ban-ts-comment': [
+ 'error',
+ {
+ 'ts-expect-error': 'allow-with-description',
+ 'ts-ignore': 'allow-with-description',
+ 'ts-nocheck': 'allow-with-description',
+ 'ts-check': false,
+ },
+ ],
+
+ /**
+ * 【强制】关键字前后有一个空格
+ * @link https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/keyword-spacing.md
+ */
+ 'keyword-spacing': 'off',
+ '@typescript-eslint/keyword-spacing': [
+ 'error',
+ {
+ before: true,
+ after: true,
+ overrides: {
+ return: { after: true },
+ throw: { after: true },
+ case: { after: true },
+ },
+ },
+ ],
+
+ /**
+ * 禁止出现空函数,普通函数(非 async/await/generator)、箭头函数、类上的方法除外
+ * @link https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-empty-function.md
+ */
+ 'no-empty-function': 'off',
+ '@typescript-eslint/no-empty-function': [
+ 'error',
+ {
+ allow: ['arrowFunctions', 'functions', 'methods'],
+ },
+ ],
+
+ /**
+ * 优先使用 interface 而不是 type 定义对象类型
+ * @link https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/consistent-type-definitions.md
+ */
+ '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
+
+ 'vue/attributes-order': 'error',
+ 'vue/require-default-prop': 'error',
+ },
+};
diff --git a/internal/eslint-config/tsconfig.json b/internal/eslint-config/tsconfig.json
new file mode 100644
index 0000000..cd27063
--- /dev/null
+++ b/internal/eslint-config/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/ts-config/node.json",
+ "include": ["src"]
+}
diff --git a/internal/stylelint-config/.eslintignore b/internal/stylelint-config/.eslintignore
new file mode 100644
index 0000000..cef44b3
--- /dev/null
+++ b/internal/stylelint-config/.eslintignore
@@ -0,0 +1,9 @@
+
+*.sh
+node_modules
+*.md
+*.woff
+*.ttf
+.turbo
+dist
+package.json
diff --git a/internal/stylelint-config/.eslintrc.cjs b/internal/stylelint-config/.eslintrc.cjs
new file mode 100644
index 0000000..cd27a19
--- /dev/null
+++ b/internal/stylelint-config/.eslintrc.cjs
@@ -0,0 +1,4 @@
+module.exports = {
+ root: true,
+ extends: ['@vben/eslint-config/strict'],
+};
diff --git a/internal/stylelint-config/build.config.ts b/internal/stylelint-config/build.config.ts
new file mode 100644
index 0000000..20c8b54
--- /dev/null
+++ b/internal/stylelint-config/build.config.ts
@@ -0,0 +1,10 @@
+import { defineBuildConfig } from 'unbuild';
+
+export default defineBuildConfig({
+ clean: true,
+ entries: ['src/index'],
+ declaration: true,
+ rollup: {
+ emitCJS: true,
+ },
+});
diff --git a/internal/stylelint-config/package.json b/internal/stylelint-config/package.json
new file mode 100644
index 0000000..6a3ba22
--- /dev/null
+++ b/internal/stylelint-config/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "@vben/stylelint-config",
+ "version": "1.0.0",
+ "private": true,
+ "homepage": "https://github.com/vbenjs/vue-vben-admin",
+ "bugs": {
+ "url": "https://github.com/vbenjs/vue-vben-admin/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+ "directory": "internal/stylelint-config"
+ },
+ "license": "MIT",
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.cjs"
+ }
+ },
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.mjs",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "clean": "pnpm rimraf .turbo node_modules dist",
+ "lint": "pnpm eslint .",
+ "stub": "pnpm unbuild --stub"
+ },
+ "devDependencies": {
+ "postcss": "^8.4.33",
+ "postcss-html": "^1.5.0",
+ "postcss-less": "^6.0.0",
+ "postcss-scss": "^4.0.9",
+ "prettier": "^3.1.1",
+ "stylelint": "^16.1.0",
+ "stylelint-config-property-sort-order-smacss": "^10.0.0",
+ "stylelint-config-recommended-scss": "^14.0.0",
+ "stylelint-config-recommended-vue": "^1.5.0",
+ "stylelint-config-standard": "^36.0.0",
+ "stylelint-config-standard-scss": "^13.0.0",
+ "stylelint-order": "^6.0.4",
+ "stylelint-prettier": "^5.0.0"
+ }
+}
diff --git a/internal/stylelint-config/src/index.ts b/internal/stylelint-config/src/index.ts
new file mode 100644
index 0000000..8b15456
--- /dev/null
+++ b/internal/stylelint-config/src/index.ts
@@ -0,0 +1,92 @@
+export default {
+ extends: ['stylelint-config-standard', 'stylelint-config-property-sort-order-smacss'],
+ plugins: ['stylelint-order', 'stylelint-prettier'],
+ // customSyntax: 'postcss-html',
+ overrides: [
+ {
+ files: ['**/*.(css|html|vue)'],
+ customSyntax: 'postcss-html',
+ },
+ {
+ files: ['*.less', '**/*.less'],
+ customSyntax: 'postcss-less',
+ extends: ['stylelint-config-standard', 'stylelint-config-recommended-vue'],
+ },
+ {
+ files: ['*.scss', '**/*.scss'],
+ customSyntax: 'postcss-scss',
+ extends: ['stylelint-config-standard-scss', 'stylelint-config-recommended-vue/scss'],
+ rule: {
+ 'scss/percent-placeholder-pattern': null,
+ },
+ },
+ ],
+ rules: {
+ 'prettier/prettier': true,
+ 'media-feature-range-notation': null,
+ 'selector-not-notation': null,
+ 'import-notation': null,
+ 'function-no-unknown': null,
+ 'selector-class-pattern': null,
+ 'selector-pseudo-class-no-unknown': [
+ true,
+ {
+ ignorePseudoClasses: ['global', 'deep'],
+ },
+ ],
+ 'selector-pseudo-element-no-unknown': [
+ true,
+ {
+ ignorePseudoElements: ['v-deep'],
+ },
+ ],
+ 'at-rule-no-unknown': [
+ true,
+ {
+ ignoreAtRules: [
+ 'tailwind',
+ 'apply',
+ 'variants',
+ 'responsive',
+ 'screen',
+ 'function',
+ 'if',
+ 'each',
+ 'include',
+ 'mixin',
+ 'extend',
+ ],
+ },
+ ],
+ 'no-empty-source': null,
+ 'named-grid-areas-no-invalid': null,
+ 'no-descending-specificity': null,
+ 'font-family-no-missing-generic-family-keyword': null,
+ 'rule-empty-line-before': [
+ 'always',
+ {
+ ignore: ['after-comment', 'first-nested'],
+ },
+ ],
+ 'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }],
+ 'order/order': [
+ [
+ 'dollar-variables',
+ 'custom-properties',
+ 'at-rules',
+ 'declarations',
+ {
+ type: 'at-rule',
+ name: 'supports',
+ },
+ {
+ type: 'at-rule',
+ name: 'media',
+ },
+ 'rules',
+ ],
+ { severity: 'error' },
+ ],
+ },
+ ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts'],
+};
diff --git a/internal/stylelint-config/tsconfig.json b/internal/stylelint-config/tsconfig.json
new file mode 100644
index 0000000..cd27063
--- /dev/null
+++ b/internal/stylelint-config/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/ts-config/node.json",
+ "include": ["src"]
+}
diff --git a/internal/ts-config/base.json b/internal/ts-config/base.json
new file mode 100644
index 0000000..8b90054
--- /dev/null
+++ b/internal/ts-config/base.json
@@ -0,0 +1,27 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "display": "Base",
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "declaration": true,
+ "noImplicitOverride": true,
+ "noUnusedLocals": true,
+ "esModuleInterop": true,
+ "useUnknownInCatchVariables": false,
+ "composite": false,
+ "declarationMap": false,
+ "forceConsistentCasingInFileNames": true,
+ "inlineSources": false,
+ "isolatedModules": true,
+ "skipLibCheck": true,
+ "noUnusedParameters": false,
+ "preserveWatchOutput": true,
+ "experimentalDecorators": true,
+ "resolveJsonModule": true,
+ "removeComments": true
+ },
+ "exclude": ["**/node_modules/**", "**/dist/**"]
+}
diff --git a/internal/ts-config/node-server.json b/internal/ts-config/node-server.json
new file mode 100644
index 0000000..e27374a
--- /dev/null
+++ b/internal/ts-config/node-server.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "display": "Node Server Config",
+ "extends": "./base.json",
+ "compilerOptions": {
+ "module": "commonjs",
+ "declaration": false,
+ "removeComments": true,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "target": "es6",
+ "sourceMap": false,
+ "esModuleInterop": true,
+ "outDir": "./dist",
+ "baseUrl": "./"
+ },
+ "exclude": ["node_modules"]
+}
diff --git a/internal/ts-config/node.json b/internal/ts-config/node.json
new file mode 100644
index 0000000..cdd365f
--- /dev/null
+++ b/internal/ts-config/node.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "display": "Node Config",
+ "extends": "./base.json",
+ "compilerOptions": {
+ "lib": ["ESNext"],
+ "noImplicitAny": true,
+ "sourceMap": true,
+ "noEmit": true,
+ "baseUrl": "./"
+ }
+}
diff --git a/internal/ts-config/package.json b/internal/ts-config/package.json
new file mode 100644
index 0000000..9a41a22
--- /dev/null
+++ b/internal/ts-config/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@vben/ts-config",
+ "version": "1.0.0",
+ "private": true,
+ "homepage": "https://github.com/vbenjs/vue-vben-admin",
+ "bugs": {
+ "url": "https://github.com/vbenjs/vue-vben-admin/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+ "directory": "internal/ts-config"
+ },
+ "license": "MIT",
+ "type": "module",
+ "files": [
+ "base.json",
+ "node.json",
+ "vue-app.json",
+ "node-server.json"
+ ],
+ "dependencies": {
+ "@types/node": "^20.10.6",
+ "vite": "^5.0.10"
+ }
+}
diff --git a/internal/ts-config/vue-app.json b/internal/ts-config/vue-app.json
new file mode 100644
index 0000000..02f3fd2
--- /dev/null
+++ b/internal/ts-config/vue-app.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "display": "Vue Application",
+ "extends": "./base.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "lib": ["ESNext", "DOM"],
+ "noImplicitAny": false
+ }
+}
diff --git a/internal/vite-config/.eslintignore b/internal/vite-config/.eslintignore
new file mode 100644
index 0000000..cef44b3
--- /dev/null
+++ b/internal/vite-config/.eslintignore
@@ -0,0 +1,9 @@
+
+*.sh
+node_modules
+*.md
+*.woff
+*.ttf
+.turbo
+dist
+package.json
diff --git a/internal/vite-config/.eslintrc.cjs b/internal/vite-config/.eslintrc.cjs
new file mode 100644
index 0000000..cd27a19
--- /dev/null
+++ b/internal/vite-config/.eslintrc.cjs
@@ -0,0 +1,4 @@
+module.exports = {
+ root: true,
+ extends: ['@vben/eslint-config/strict'],
+};
diff --git a/internal/vite-config/build.config.ts b/internal/vite-config/build.config.ts
new file mode 100644
index 0000000..20c8b54
--- /dev/null
+++ b/internal/vite-config/build.config.ts
@@ -0,0 +1,10 @@
+import { defineBuildConfig } from 'unbuild';
+
+export default defineBuildConfig({
+ clean: true,
+ entries: ['src/index'],
+ declaration: true,
+ rollup: {
+ emitCJS: true,
+ },
+});
diff --git a/internal/vite-config/package.json b/internal/vite-config/package.json
new file mode 100644
index 0000000..35ae44b
--- /dev/null
+++ b/internal/vite-config/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "@vben/vite-config",
+ "version": "1.0.0",
+ "private": true,
+ "homepage": "https://github.com/vbenjs/vue-vben-admin",
+ "bugs": {
+ "url": "https://github.com/vbenjs/vue-vben-admin/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+ "directory": "internal/vite-config"
+ },
+ "license": "MIT",
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.cjs"
+ }
+ },
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.mjs",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "clean": "pnpm rimraf .turbo node_modules dist",
+ "lint": "pnpm eslint .",
+ "stub": "pnpm unbuild --stub"
+ },
+ "dependencies": {
+ "@ant-design/colors": "^7.0.2",
+ "vite": "^5.0.10"
+ },
+ "devDependencies": {
+ "@types/fs-extra": "^11.0.4",
+ "@vitejs/plugin-vue": "^5.0.2",
+ "@vitejs/plugin-vue-jsx": "^3.1.0",
+ "ant-design-vue": "^4.0.8",
+ "dayjs": "^1.11.10",
+ "dotenv": "^16.3.1",
+ "fs-extra": "^11.2.0",
+ "less": "^4.2.0",
+ "picocolors": "^1.0.0",
+ "pkg-types": "^1.0.3",
+ "rollup-plugin-visualizer": "^5.12.0",
+ "sass": "^1.69.7",
+ "unocss": "0.58.3",
+ "vite-plugin-compression": "^0.5.1",
+ "vite-plugin-dts": "^3.7.0",
+ "vite-plugin-html": "^3.2.1",
+ "vite-plugin-mock": "^2.9.6",
+ "vite-plugin-purge-icons": "^0.10.0",
+ "vite-plugin-svg-icons": "^2.0.1"
+ }
+}
diff --git a/internal/vite-config/src/config/application.ts b/internal/vite-config/src/config/application.ts
new file mode 100644
index 0000000..bd6d6ca
--- /dev/null
+++ b/internal/vite-config/src/config/application.ts
@@ -0,0 +1,109 @@
+import { resolve } from 'node:path';
+
+import dayjs from 'dayjs';
+import { readPackageJSON } from 'pkg-types';
+import { defineConfig, loadEnv, mergeConfig, type UserConfig } from 'vite';
+
+import { createPlugins } from '../plugins';
+import { generateModifyVars } from '../utils/modifyVars';
+import { commonConfig } from './common';
+
+interface DefineOptions {
+ overrides?: UserConfig;
+ options?: {
+ //
+ };
+}
+
+function defineApplicationConfig(defineOptions: DefineOptions = {}) {
+ const { overrides = {} } = defineOptions;
+
+ return defineConfig(async ({ command, mode }) => {
+ const root = process.cwd();
+ const isBuild = command === 'build';
+ const { VITE_PUBLIC_PATH, VITE_USE_MOCK, VITE_BUILD_COMPRESS, VITE_ENABLE_ANALYZE } = loadEnv(
+ mode,
+ root,
+ );
+
+ const defineData = await createDefineData(root);
+ const plugins = await createPlugins({
+ isBuild,
+ root,
+ enableAnalyze: VITE_ENABLE_ANALYZE === 'true',
+ enableMock: VITE_USE_MOCK === 'true',
+ compress: VITE_BUILD_COMPRESS,
+ });
+
+ const pathResolve = (pathname: string) => resolve(root, '.', pathname);
+ const timestamp = new Date().getTime();
+ const applicationConfig: UserConfig = {
+ base: VITE_PUBLIC_PATH,
+ resolve: {
+ alias: [
+ {
+ find: 'vue-i18n',
+ replacement: 'vue-i18n/dist/vue-i18n.cjs.js',
+ },
+ // @/xxxx => src/xxxx
+ {
+ find: /@\//,
+ replacement: pathResolve('src') + '/',
+ },
+ // #/xxxx => types/xxxx
+ {
+ find: /#\//,
+ replacement: pathResolve('types') + '/',
+ },
+ ],
+ },
+ define: defineData,
+ build: {
+ target: 'es2015',
+ cssTarget: 'chrome80',
+ rollupOptions: {
+ output: {
+ // 入口文件名
+ entryFileNames: `assets/entry/[name]-[hash]-${timestamp}.js`,
+ manualChunks: {
+ vue: ['vue', 'pinia', 'vue-router'],
+ antd: ['ant-design-vue', '@ant-design/icons-vue'],
+ },
+ },
+ },
+ },
+ css: {
+ preprocessorOptions: {
+ less: {
+ modifyVars: generateModifyVars(),
+ javascriptEnabled: true,
+ },
+ },
+ },
+ plugins,
+ };
+
+ const mergedConfig = mergeConfig(commonConfig(mode), applicationConfig);
+
+ return mergeConfig(mergedConfig, overrides);
+ });
+}
+
+async function createDefineData(root: string) {
+ try {
+ const pkgJson = await readPackageJSON(root);
+ const { dependencies, devDependencies, name, version } = pkgJson;
+
+ const __APP_INFO__ = {
+ pkg: { dependencies, devDependencies, name, version },
+ lastBuildTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+ };
+ return {
+ __APP_INFO__: JSON.stringify(__APP_INFO__),
+ };
+ } catch (error) {
+ return {};
+ }
+}
+
+export { defineApplicationConfig };
diff --git a/internal/vite-config/src/config/common.ts b/internal/vite-config/src/config/common.ts
new file mode 100644
index 0000000..559896e
--- /dev/null
+++ b/internal/vite-config/src/config/common.ts
@@ -0,0 +1,22 @@
+import UnoCSS from 'unocss/vite';
+import { type UserConfig } from 'vite';
+
+const commonConfig: (mode: string) => UserConfig = (mode) => ({
+ server: {
+ host: true,
+ },
+ esbuild: {
+ drop: mode === 'production' ? ['console', 'debugger'] : [],
+ },
+ build: {
+ reportCompressedSize: false,
+ chunkSizeWarningLimit: 1500,
+ rollupOptions: {
+ // TODO: Prevent memory overflow
+ maxParallelFileOps: 3,
+ },
+ },
+ plugins: [UnoCSS()],
+});
+
+export { commonConfig };
diff --git a/internal/vite-config/src/config/package.ts b/internal/vite-config/src/config/package.ts
new file mode 100644
index 0000000..ab83852
--- /dev/null
+++ b/internal/vite-config/src/config/package.ts
@@ -0,0 +1,42 @@
+import { readPackageJSON } from 'pkg-types';
+import { defineConfig, mergeConfig, type UserConfig } from 'vite';
+import dts from 'vite-plugin-dts';
+
+import { commonConfig } from './common';
+
+interface DefineOptions {
+ overrides?: UserConfig;
+ options?: {
+ //
+ };
+}
+
+function definePackageConfig(defineOptions: DefineOptions = {}) {
+ const { overrides = {} } = defineOptions;
+ const root = process.cwd();
+ return defineConfig(async ({ mode }) => {
+ const { dependencies = {}, peerDependencies = {} } = await readPackageJSON(root);
+ const packageConfig: UserConfig = {
+ build: {
+ lib: {
+ entry: 'src/index.ts',
+ formats: ['es'],
+ fileName: () => 'index.mjs',
+ },
+ rollupOptions: {
+ external: [...Object.keys(dependencies), ...Object.keys(peerDependencies)],
+ },
+ },
+ plugins: [
+ dts({
+ logLevel: 'error',
+ }),
+ ],
+ };
+ const mergedConfig = mergeConfig(commonConfig(mode), packageConfig);
+
+ return mergeConfig(mergedConfig, overrides);
+ });
+}
+
+export { definePackageConfig };
diff --git a/internal/vite-config/src/index.ts b/internal/vite-config/src/index.ts
new file mode 100644
index 0000000..9ef1e80
--- /dev/null
+++ b/internal/vite-config/src/index.ts
@@ -0,0 +1,2 @@
+export * from './config/application';
+export * from './config/package';
diff --git a/internal/vite-config/src/plugins/appConfig.ts b/internal/vite-config/src/plugins/appConfig.ts
new file mode 100644
index 0000000..7d50662
--- /dev/null
+++ b/internal/vite-config/src/plugins/appConfig.ts
@@ -0,0 +1,104 @@
+import colors from 'picocolors';
+import { readPackageJSON } from 'pkg-types';
+import { type PluginOption } from 'vite';
+
+import { getEnvConfig } from '../utils/env';
+import { createContentHash } from '../utils/hash';
+
+const GLOBAL_CONFIG_FILE_NAME = '_app.config.js';
+const PLUGIN_NAME = 'app-config';
+
+async function createAppConfigPlugin({
+ root,
+ isBuild,
+}: {
+ root: string;
+ isBuild: boolean;
+}): Promise {
+ let publicPath: string;
+ let source: string;
+ if (!isBuild) {
+ return {
+ name: PLUGIN_NAME,
+ };
+ }
+ const { version = '' } = await readPackageJSON(root);
+
+ return {
+ name: PLUGIN_NAME,
+ async configResolved(_config) {
+ const appTitle = _config?.env?.VITE_GLOB_APP_TITLE ?? '';
+ // appTitle = appTitle.replace(/\s/g, '_').replace(/-/g, '_');
+ publicPath = _config.base;
+ source = await getConfigSource(appTitle);
+ },
+ async transformIndexHtml(html) {
+ publicPath = publicPath.endsWith('/') ? publicPath : `${publicPath}/`;
+
+ const appConfigSrc = `${
+ publicPath || '/'
+ }${GLOBAL_CONFIG_FILE_NAME}?v=${version}-${createContentHash(source)}`;
+
+ return {
+ html,
+ tags: [
+ {
+ tag: 'script',
+ attrs: {
+ src: appConfigSrc,
+ },
+ },
+ ],
+ };
+ },
+ async generateBundle() {
+ try {
+ this.emitFile({
+ type: 'asset',
+ fileName: GLOBAL_CONFIG_FILE_NAME,
+ source,
+ });
+
+ console.log(colors.cyan(`✨configuration file is build successfully!`));
+ } catch (error) {
+ console.log(
+ colors.red('configuration file configuration file failed to package:\n' + error),
+ );
+ }
+ },
+ };
+}
+
+/**
+ * Get the configuration file variable name
+ * @param env
+ */
+const getVariableName = (title: string) => {
+ function strToHex(str: string) {
+ const result: string[] = [];
+ for (let i = 0; i < str.length; ++i) {
+ const hex = str.charCodeAt(i).toString(16);
+ result.push(('000' + hex).slice(-4));
+ }
+ return result.join('').toUpperCase();
+ }
+ return `__PRODUCTION__${strToHex(title) || '__APP'}__CONF__`.toUpperCase().replace(/\s/g, '');
+};
+
+async function getConfigSource(appTitle: string) {
+ const config = await getEnvConfig();
+ const variableName = getVariableName(appTitle);
+ const windowVariable = `window.${variableName}`;
+ // Ensure that the variable will not be modified
+ let source = `${windowVariable}=${JSON.stringify(config)};`;
+ source += `
+ Object.freeze(${windowVariable});
+ Object.defineProperty(window, "${variableName}", {
+ configurable: false,
+ writable: false,
+ });
+ `.replace(/\s/g, '');
+ return source;
+}
+
+export { createAppConfigPlugin };
diff --git a/internal/vite-config/src/plugins/compress.ts b/internal/vite-config/src/plugins/compress.ts
new file mode 100644
index 0000000..8fc1397
--- /dev/null
+++ b/internal/vite-config/src/plugins/compress.ts
@@ -0,0 +1,38 @@
+/**
+ * Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated
+ * https://github.com/anncwb/vite-plugin-compression
+ */
+import type { PluginOption } from 'vite';
+import compressPlugin from 'vite-plugin-compression';
+
+export function configCompressPlugin({
+ compress,
+ deleteOriginFile = false,
+}: {
+ compress: string;
+ deleteOriginFile?: boolean;
+}): PluginOption[] {
+ const compressList = compress.split(',');
+
+ const plugins: PluginOption[] = [];
+
+ if (compressList.includes('gzip')) {
+ plugins.push(
+ compressPlugin({
+ ext: '.gz',
+ deleteOriginFile,
+ }),
+ );
+ }
+
+ if (compressList.includes('brotli')) {
+ plugins.push(
+ compressPlugin({
+ ext: '.br',
+ algorithm: 'brotliCompress',
+ deleteOriginFile,
+ }),
+ );
+ }
+ return plugins;
+}
diff --git a/internal/vite-config/src/plugins/html.ts b/internal/vite-config/src/plugins/html.ts
new file mode 100644
index 0000000..2623a28
--- /dev/null
+++ b/internal/vite-config/src/plugins/html.ts
@@ -0,0 +1,14 @@
+/**
+ * Plugin to minimize and use ejs template syntax in index.html.
+ * https://github.com/anncwb/vite-plugin-html
+ */
+import type { PluginOption } from 'vite';
+import { createHtmlPlugin } from 'vite-plugin-html';
+
+export function configHtmlPlugin({ isBuild }: { isBuild: boolean }) {
+ const htmlPlugin: PluginOption[] = createHtmlPlugin({
+ minify: isBuild,
+ viteNext: true,
+ });
+ return htmlPlugin;
+}
diff --git a/internal/vite-config/src/plugins/index.ts b/internal/vite-config/src/plugins/index.ts
new file mode 100644
index 0000000..b13fb39
--- /dev/null
+++ b/internal/vite-config/src/plugins/index.ts
@@ -0,0 +1,59 @@
+import vue from '@vitejs/plugin-vue';
+import vueJsx from '@vitejs/plugin-vue-jsx';
+import { type PluginOption } from 'vite';
+import purgeIcons from 'vite-plugin-purge-icons';
+
+import { createAppConfigPlugin } from './appConfig';
+import { configCompressPlugin } from './compress';
+import { configHtmlPlugin } from './html';
+import { configMockPlugin } from './mock';
+import { configSvgIconsPlugin } from './svgSprite';
+import { configVisualizerConfig } from './visualizer';
+
+interface Options {
+ isBuild: boolean;
+ root: string;
+ compress: string;
+ enableMock?: boolean;
+ enableAnalyze?: boolean;
+}
+
+async function createPlugins({ isBuild, root, enableMock, compress, enableAnalyze }: Options) {
+ const vitePlugins: (PluginOption | PluginOption[])[] = [vue(), vueJsx()];
+
+ const appConfigPlugin = await createAppConfigPlugin({ root, isBuild });
+ vitePlugins.push(appConfigPlugin);
+
+ // vite-plugin-html
+ vitePlugins.push(configHtmlPlugin({ isBuild }));
+
+ // vite-plugin-svg-icons
+ vitePlugins.push(configSvgIconsPlugin({ isBuild }));
+
+ // vite-plugin-purge-icons
+ vitePlugins.push(purgeIcons());
+
+ // The following plugins only work in the production environment
+ if (isBuild) {
+ // rollup-plugin-gzip
+ vitePlugins.push(
+ configCompressPlugin({
+ compress,
+ }),
+ );
+ }
+
+ // rollup-plugin-visualizer
+ if (enableAnalyze) {
+ vitePlugins.push(configVisualizerConfig());
+ }
+
+ // vite-plugin-mock
+ if (enableMock) {
+ vitePlugins.push(configMockPlugin({ isBuild }));
+ }
+
+ return vitePlugins;
+}
+
+export { createPlugins };
diff --git a/internal/vite-config/src/plugins/mock.ts b/internal/vite-config/src/plugins/mock.ts
new file mode 100644
index 0000000..b47899c
--- /dev/null
+++ b/internal/vite-config/src/plugins/mock.ts
@@ -0,0 +1,19 @@
+/**
+ * Mock plugin for development and production.
+ * https://github.com/anncwb/vite-plugin-mock
+ */
+import { viteMockServe } from 'vite-plugin-mock';
+
+export function configMockPlugin({ isBuild }: { isBuild: boolean }) {
+ return viteMockServe({
+ ignore: /^_/,
+ mockPath: 'mock',
+ localEnabled: !isBuild,
+ prodEnabled: isBuild,
+ injectCode: `
+ import { setupProdMockServer } from '../mock/_createProductionServer';
+
+ setupProdMockServer();
+ `,
+ });
+}
diff --git a/internal/vite-config/src/plugins/svgSprite.ts b/internal/vite-config/src/plugins/svgSprite.ts
new file mode 100644
index 0000000..659e5af
--- /dev/null
+++ b/internal/vite-config/src/plugins/svgSprite.ts
@@ -0,0 +1,17 @@
+/**
+ * Vite Plugin for fast creating SVG sprites.
+ * https://github.com/anncwb/vite-plugin-svg-icons
+ */
+
+import { resolve } from 'node:path';
+
+import type { PluginOption } from 'vite';
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
+
+export function configSvgIconsPlugin({ isBuild }: { isBuild: boolean }) {
+ const svgIconsPlugin = createSvgIconsPlugin({
+ iconDirs: [resolve(process.cwd(), 'src/assets/icons')],
+ svgoOptions: isBuild,
+ });
+ return svgIconsPlugin as PluginOption;
+}
diff --git a/internal/vite-config/src/plugins/visualizer.ts b/internal/vite-config/src/plugins/visualizer.ts
new file mode 100644
index 0000000..0b6ba62
--- /dev/null
+++ b/internal/vite-config/src/plugins/visualizer.ts
@@ -0,0 +1,14 @@
+/**
+ * Package file volume analysis
+ */
+import visualizer from 'rollup-plugin-visualizer';
+import { type PluginOption } from 'vite';
+
+export function configVisualizerConfig() {
+ return visualizer({
+ filename: './node_modules/.cache/visualizer/stats.html',
+ open: true,
+ gzipSize: true,
+ brotliSize: true,
+ }) as PluginOption;
+}
diff --git a/internal/vite-config/src/utils/env.ts b/internal/vite-config/src/utils/env.ts
new file mode 100644
index 0000000..c84ea94
--- /dev/null
+++ b/internal/vite-config/src/utils/env.ts
@@ -0,0 +1,49 @@
+import { join } from 'node:path';
+
+import dotenv from 'dotenv';
+import { readFile } from 'fs-extra';
+
+/**
+ * 获取当前环境下生效的配置文件名
+ */
+function getConfFiles() {
+ const script = process.env.npm_lifecycle_script as string;
+ const reg = new RegExp('--mode ([a-z_\\d]+)');
+ const result = reg.exec(script);
+ if (result) {
+ const mode = result[1];
+ return ['.env', `.env.${mode}`];
+ }
+ return ['.env', '.env.production'];
+}
+
+/**
+ * Get the environment variables starting with the specified prefix
+ * @param match prefix
+ * @param confFiles ext
+ */
+export async function getEnvConfig(
+ match = 'VITE_GLOB_',
+ confFiles = getConfFiles(),
+): Promise<{
+ [key: string]: string;
+}> {
+ let envConfig = {};
+
+ for (const confFile of confFiles) {
+ try {
+ const envPath = await readFile(join(process.cwd(), confFile), { encoding: 'utf8' });
+ const env = dotenv.parse(envPath);
+ envConfig = { ...envConfig, ...env };
+ } catch (e) {
+ console.error(`Error in parsing ${confFile}`, e);
+ }
+ }
+ const reg = new RegExp(`^(${match})`);
+ Object.keys(envConfig).forEach((key) => {
+ if (!reg.test(key)) {
+ Reflect.deleteProperty(envConfig, key);
+ }
+ });
+ return envConfig;
+}
diff --git a/internal/vite-config/src/utils/hash.ts b/internal/vite-config/src/utils/hash.ts
new file mode 100644
index 0000000..0b5a7c9
--- /dev/null
+++ b/internal/vite-config/src/utils/hash.ts
@@ -0,0 +1,16 @@
+import { createHash } from 'node:crypto';
+
+function createContentHash(content: string, hashLSize = 12) {
+ const hash = createHash('sha256').update(content);
+ return hash.digest('hex').slice(0, hashLSize);
+}
+function strToHex(str: string) {
+ const result: string[] = [];
+ for (let i = 0; i < str.length; ++i) {
+ const hex = str.charCodeAt(i).toString(16);
+ result.push(('000' + hex).slice(-4));
+ }
+ return result.join('').toUpperCase();
+}
+
+export { createContentHash, strToHex };
diff --git a/internal/vite-config/src/utils/modifyVars.ts b/internal/vite-config/src/utils/modifyVars.ts
new file mode 100644
index 0000000..0554343
--- /dev/null
+++ b/internal/vite-config/src/utils/modifyVars.ts
@@ -0,0 +1,47 @@
+import { resolve } from 'node:path';
+
+import { generate } from '@ant-design/colors';
+// @ts-ignore: typo
+/* import { getThemeVariables } from 'ant-design-vue/dist/theme'; */
+import { theme } from 'ant-design-vue/lib';
+import convertLegacyToken from 'ant-design-vue/lib/theme/convertLegacyToken';
+
+const { defaultAlgorithm, defaultSeed } = theme;
+const primaryColor = '#0960bd';
+
+function generateAntColors(color: string, theme: 'default' | 'dark' = 'default') {
+ return generate(color, {
+ theme,
+ });
+}
+
+/**
+ * less global variable
+ */
+export function generateModifyVars() {
+ const palettes = generateAntColors(primaryColor);
+ const primary = palettes[5];
+ const primaryColorObj: Record = {};
+
+ for (let index = 0; index < 10; index++) {
+ primaryColorObj[`primary-${index + 1}`] = palettes[index];
+ }
+ // const modifyVars = getThemeVariables();
+ const mapToken = defaultAlgorithm(defaultSeed);
+ const v3Token = convertLegacyToken(mapToken);
+ return {
+ ...v3Token,
+ // reference: Avoid repeated references
+ hack: `true; @import (reference) "${resolve('src/design/config.less')}";`,
+ 'primary-color': primary,
+ ...primaryColorObj,
+ 'info-color': primary,
+ 'processing-color': primary,
+ 'success-color': '#55D187', // Success color
+ 'error-color': '#ED6F6F', // False color
+ 'warning-color': '#EFBD47', // Warning color
+ 'font-size-base': '14px', // Main font size
+ 'border-radius-base': '2px', // Component/float fillet
+ 'link-color': primary, // Link color
+ };
+}
diff --git a/internal/vite-config/tsconfig.json b/internal/vite-config/tsconfig.json
new file mode 100644
index 0000000..cd27063
--- /dev/null
+++ b/internal/vite-config/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/ts-config/node.json",
+ "include": ["src"]
+}
diff --git a/mock/_createProductionServer.ts b/mock/_createProductionServer.ts
new file mode 100644
index 0000000..57c2755
--- /dev/null
+++ b/mock/_createProductionServer.ts
@@ -0,0 +1,34 @@
+import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';
+
+// 问题描述
+// 1. `import.meta.globEager` 已被弃用, 需要升级vite版本,有兼容问题
+// 2. `vite-plugin-mock` 插件问题 https://github.com/vbenjs/vite-plugin-mock/issues/56
+
+// const modules: Record = import.meta.glob("./**/*.ts", {
+// import: "default",
+// eager: true,
+// });
+
+// const mockModules = Object.keys(modules).reduce((pre, key) => {
+// if (!key.includes("/_")) {
+// pre.push(...modules[key]);
+// }
+// return pre;
+// }, [] as any[]);
+
+const modules = import.meta.glob('./**/*.ts', { eager: true });
+
+const mockModules: any[] = [];
+Object.keys(modules).forEach((key) => {
+ if (key.includes('/_')) {
+ return;
+ }
+ mockModules.push(...(modules as Recordable)[key].default);
+});
+
+/**
+ * Used in a production environment. Need to manually import all modules
+ */
+export function setupProdMockServer() {
+ createProdMockServer(mockModules);
+}
diff --git a/mock/_util.ts b/mock/_util.ts
new file mode 100644
index 0000000..380c3a3
--- /dev/null
+++ b/mock/_util.ts
@@ -0,0 +1,62 @@
+// Interface data format used to return a unified format
+import { ResultEnum } from '@/enums/httpEnum';
+
+export function resultSuccess(result: T, { message = 'ok' } = {}) {
+ return {
+ code: ResultEnum.SUCCESS,
+ result,
+ message,
+ type: 'success',
+ };
+}
+
+export function resultPageSuccess(
+ page: number,
+ pageSize: number,
+ list: T[],
+ { message = 'ok' } = {},
+) {
+ const pageData = pagination(page, pageSize, list);
+
+ return {
+ ...resultSuccess({
+ items: pageData,
+ total: list.length,
+ }),
+ message,
+ };
+}
+
+export function resultError(
+ message = 'Request failed',
+ { code = ResultEnum.ERROR, result = null } = {},
+) {
+ return {
+ code,
+ result,
+ message,
+ type: 'error',
+ };
+}
+
+export function pagination(pageNo: number, pageSize: number, array: T[]): T[] {
+ const offset = (pageNo - 1) * Number(pageSize);
+ return offset + Number(pageSize) >= array.length
+ ? array.slice(offset, array.length)
+ : array.slice(offset, offset + Number(pageSize));
+}
+
+export interface requestParams {
+ method: string;
+ body: any;
+ headers?: { authorization?: string };
+ query: any;
+}
+
+/**
+ * @description 本函数用于从request数据中获取token,请根据项目的实际情况修改
+ *
+ */
+export function getRequestToken({ headers }: requestParams): string | undefined {
+ return headers?.authorization;
+}
diff --git a/mock/demo/account.ts b/mock/demo/account.ts
new file mode 100644
index 0000000..a392493
--- /dev/null
+++ b/mock/demo/account.ts
@@ -0,0 +1,71 @@
+import { MockMethod } from 'vite-plugin-mock';
+import { resultSuccess, resultError } from '../_util';
+import { ResultEnum } from '../../src/enums/httpEnum';
+
+const userInfo = {
+ name: 'Vben',
+ userid: '00000001',
+ email: 'test@gmail.com',
+ signature: '海纳百川,有容乃大',
+ introduction: '微笑着,努力着,欣赏着',
+ title: '交互专家',
+ group: '某某某事业群-某某平台部-某某技术部-UED',
+ tags: [
+ {
+ key: '0',
+ label: '很有想法的',
+ },
+ {
+ key: '1',
+ label: '专注设计',
+ },
+ {
+ key: '2',
+ label: '辣~',
+ },
+ {
+ key: '3',
+ label: '大长腿',
+ },
+ {
+ key: '4',
+ label: '川妹子',
+ },
+ {
+ key: '5',
+ label: '海纳百川',
+ },
+ ],
+ notifyCount: 12,
+ unreadCount: 11,
+ country: 'China',
+ address: 'Xiamen City 77',
+ phone: '0592-268888888',
+};
+
+export default [
+ {
+ url: '/basic-api/account/getAccountInfo',
+ timeout: 1000,
+ method: 'get',
+ response: () => {
+ return resultSuccess(userInfo);
+ },
+ },
+ {
+ url: '/basic-api/user/sessionTimeout',
+ method: 'post',
+ statusCode: 401,
+ response: () => {
+ return resultError();
+ },
+ },
+ {
+ url: '/basic-api/user/tokenExpired',
+ method: 'post',
+ statusCode: 200,
+ response: () => {
+ return resultError('Token Expired!', { code: ResultEnum.TIMEOUT as number });
+ },
+ },
+] as MockMethod[];
diff --git a/mock/demo/api-cascader.ts b/mock/demo/api-cascader.ts
new file mode 100644
index 0000000..6334ef5
--- /dev/null
+++ b/mock/demo/api-cascader.ts
@@ -0,0 +1,325 @@
+import { MockMethod } from 'vite-plugin-mock';
+import { resultSuccess } from '../_util';
+
+const areaList: any[] = [
+ {
+ id: '530825900854620160',
+ code: '430000',
+ parentCode: '100000',
+ levelType: 1,
+ name: '湖南省',
+ province: '湖南省',
+ city: null,
+ district: null,
+ town: null,
+ village: null,
+ parentPath: '430000',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2020-11-30 16:33:42',
+ customized: false,
+ usable: true,
+ },
+ {
+ id: '530825900883980288',
+ code: '430100',
+ parentCode: '430000',
+ levelType: 2,
+ name: '长沙市',
+ province: '湖南省',
+ city: '长沙市',
+ district: null,
+ town: null,
+ village: null,
+ parentPath: '430000,430100',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2020-11-30 16:33:42',
+ customized: false,
+ usable: true,
+ },
+ {
+ id: '530825900951089152',
+ code: '430102',
+ parentCode: '430100',
+ levelType: 3,
+ name: '芙蓉区',
+ province: '湖南省',
+ city: '长沙市',
+ district: '芙蓉区',
+ town: null,
+ village: null,
+ parentPath: '430000,430100,430102',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2020-11-30 16:33:42',
+ customized: false,
+ usable: true,
+ },
+ {
+ id: '530825901014003712',
+ code: '430104',
+ parentCode: '430100',
+ levelType: 3,
+ name: '岳麓区',
+ province: '湖南省',
+ city: '长沙市',
+ district: '岳麓区',
+ town: null,
+ village: null,
+ parentPath: '430000,430100,430104',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2020-11-30 16:33:42',
+ customized: false,
+ usable: true,
+ },
+ {
+ id: '530825900988837888',
+ code: '430103',
+ parentCode: '430100',
+ levelType: 3,
+ name: '天心区',
+ province: '湖南省',
+ city: '长沙市',
+ district: '天心区',
+ town: null,
+ village: null,
+ parentPath: '430000,430100,430103',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2020-11-30 16:33:42',
+ customized: false,
+ usable: true,
+ },
+ {
+ id: '530826672489115648',
+ code: '430103002',
+ parentCode: '430103',
+ levelType: 4,
+ name: '坡子街街道',
+ province: '湖南省',
+ city: '长沙市',
+ district: '天心区',
+ town: '坡子街街道',
+ village: null,
+ parentPath: '430000,430100,430103,430103002',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2020-12-14 15:26:43',
+ customized: false,
+ usable: true,
+ },
+ {
+ id: '530840241171607552',
+ code: '430103002001',
+ parentCode: '430103002',
+ levelType: 5,
+ name: '八角亭社区',
+ province: '湖南省',
+ city: '长沙市',
+ district: '天心区',
+ town: '坡子街街道',
+ village: '八角亭社区',
+ parentPath: '430000,430100,430103,430103002,430103002001',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2021-01-20 14:07:23',
+ customized: false,
+ usable: true,
+ },
+ {
+ id: '530840241200967680',
+ code: '430103002002',
+ parentCode: '430103002',
+ levelType: 5,
+ name: '西牌楼社区',
+ province: '湖南省',
+ city: '长沙市',
+ district: '天心区',
+ town: '坡子街街道',
+ village: '西牌楼社区',
+ parentPath: '430000,430100,430103,430103002,430103002002',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2020-11-30 17:30:41',
+ customized: false,
+ usable: true,
+ },
+ {
+ id: '530840241230327808',
+ code: '430103002003',
+ parentCode: '430103002',
+ levelType: 5,
+ name: '太平街社区',
+ province: '湖南省',
+ city: '长沙市',
+ district: '天心区',
+ town: '坡子街街道',
+ village: '太平街社区',
+ parentPath: '430000,430100,430103,430103002,430103002003',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2020-11-30 17:30:41',
+ customized: false,
+ usable: true,
+ },
+ {
+ id: '530840241259687936',
+ code: '430103002005',
+ parentCode: '430103002',
+ levelType: 5,
+ name: '坡子街社区',
+ province: '湖南省',
+ city: '长沙市',
+ district: '天心区',
+ town: '坡子街街道',
+ village: '坡子街社区',
+ parentPath: '430000,430100,430103,430103002,430103002005',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2020-11-30 17:30:41',
+ customized: false,
+ usable: true,
+ },
+ {
+ id: '530840241284853760',
+ code: '430103002006',
+ parentCode: '430103002',
+ levelType: 5,
+ name: '青山祠社区',
+ province: '湖南省',
+ city: '长沙市',
+ district: '天心区',
+ town: '坡子街街道',
+ village: '青山祠社区',
+ parentPath: '430000,430100,430103,430103002,430103002006',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2020-11-30 17:30:41',
+ customized: false,
+ usable: true,
+ },
+ {
+ id: '530840241310019584',
+ code: '430103002007',
+ parentCode: '430103002',
+ levelType: 5,
+ name: '沙河社区',
+ province: '湖南省',
+ city: '长沙市',
+ district: '天心区',
+ town: '坡子街街道',
+ village: '沙河社区',
+ parentPath: '430000,430100,430103,430103002,430103002007',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2020-11-30 17:30:41',
+ customized: false,
+ usable: true,
+ },
+ {
+ id: '530840241381322752',
+ code: '430103002008',
+ parentCode: '430103002',
+ levelType: 5,
+ name: '碧湘社区',
+ province: '湖南省',
+ city: '长沙市',
+ district: '天心区',
+ town: '坡子街街道',
+ village: '碧湘社区',
+ parentPath: '430000,430100,430103,430103002,430103002008',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2020-11-30 17:30:41',
+ customized: false,
+ usable: true,
+ },
+ {
+ id: '530840241410682880',
+ code: '430103002009',
+ parentCode: '430103002',
+ levelType: 5,
+ name: '创远社区',
+ province: '湖南省',
+ city: '长沙市',
+ district: '天心区',
+ town: '坡子街街道',
+ village: '创远社区',
+ parentPath: '430000,430100,430103,430103002,430103002009',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2020-11-30 17:30:41',
+ customized: false,
+ usable: true,
+ },
+ {
+ id: '530840241431654400',
+ code: '430103002010',
+ parentCode: '430103002',
+ levelType: 5,
+ name: '楚湘社区',
+ province: '湖南省',
+ city: '长沙市',
+ district: '天心区',
+ town: '坡子街街道',
+ village: '楚湘社区',
+ parentPath: '430000,430100,430103,430103002,430103002010',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2020-11-30 17:30:41',
+ customized: false,
+ usable: true,
+ },
+ {
+ id: '530840241465208832',
+ code: '430103002011',
+ parentCode: '430103002',
+ levelType: 5,
+ name: '西湖社区',
+ province: '湖南省',
+ city: '长沙市',
+ district: '天心区',
+ town: '坡子街街道',
+ village: '西湖社区',
+ parentPath: '430000,430100,430103,430103002,430103002011',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2020-11-30 17:30:41',
+ customized: false,
+ usable: true,
+ },
+ {
+ id: '530840241502957568',
+ code: '430103002012',
+ parentCode: '430103002',
+ levelType: 5,
+ name: '登仁桥社区',
+ province: '湖南省',
+ city: '长沙市',
+ district: '天心区',
+ town: '坡子街街道',
+ village: '登仁桥社区',
+ parentPath: '430000,430100,430103,430103002,430103002012',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2020-11-30 17:30:41',
+ customized: false,
+ usable: true,
+ },
+ {
+ id: '530840241553289216',
+ code: '430103002013',
+ parentCode: '430103002',
+ levelType: 5,
+ name: '文庙坪社区',
+ province: '湖南省',
+ city: '长沙市',
+ district: '天心区',
+ town: '坡子街街道',
+ village: '文庙坪社区',
+ parentPath: '430000,430100,430103,430103002,430103002013',
+ createTime: '2020-11-30 15:47:31',
+ updateTime: '2020-11-30 17:30:41',
+ customized: false,
+ usable: true,
+ },
+];
+export default [
+ {
+ url: '/basic-api/cascader/getAreaRecord',
+ timeout: 1000,
+ method: 'post',
+ response: ({ body }) => {
+ const { parentCode } = body || {};
+ if (!parentCode) {
+ return resultSuccess(areaList.filter((it) => it.code === '430000'));
+ }
+ return resultSuccess(areaList.filter((it) => it.parentCode === parentCode));
+ },
+ },
+] as MockMethod[];
diff --git a/mock/demo/select-demo.ts b/mock/demo/select-demo.ts
new file mode 100644
index 0000000..631c6bb
--- /dev/null
+++ b/mock/demo/select-demo.ts
@@ -0,0 +1,28 @@
+import { MockMethod } from 'vite-plugin-mock';
+import { resultSuccess } from '../_util';
+
+const demoList = (keyword, count = 20) => {
+ const result = {
+ list: [] as any[],
+ };
+ for (let index = 0; index < count; index++) {
+ result.list.push({
+ name: `${keyword ?? ''}选项${index}`,
+ id: `${index}`,
+ });
+ }
+ return result;
+};
+
+export default [
+ {
+ url: '/basic-api/select/getDemoOptions',
+ timeout: 1000,
+ method: 'get',
+ response: ({ query }) => {
+ const { keyword, count } = query;
+ console.log(keyword);
+ return resultSuccess(demoList(keyword, count));
+ },
+ },
+] as MockMethod[];
diff --git a/mock/demo/system.ts b/mock/demo/system.ts
new file mode 100644
index 0000000..4c5d8de
--- /dev/null
+++ b/mock/demo/system.ts
@@ -0,0 +1,203 @@
+import { MockMethod } from 'vite-plugin-mock';
+import { resultError, resultPageSuccess, resultSuccess } from '../_util';
+
+const accountList = (() => {
+ const result: any[] = [];
+ for (let index = 0; index < 20; index++) {
+ result.push({
+ id: `${index}`,
+ account: '@first',
+ email: '@email',
+ nickname: '@cname()',
+ role: '@first',
+ createTime: '@datetime',
+ remark: '@cword(10,20)',
+ 'dept|0-2': 1,
+ 'status|1': ['0', '1'],
+ });
+ }
+ return result;
+})();
+
+const roleList = (() => {
+ const result: any[] = [];
+ for (let index = 0; index < 4; index++) {
+ result.push({
+ id: index + 1,
+ orderNo: `${index + 1}`,
+ roleName: ['超级管理员', '管理员', '文章管理员', '普通用户'][index],
+ roleValue: '@first',
+ createTime: '@datetime',
+ remark: '@cword(10,20)',
+ menu: [['0', '1', '2'], ['0', '1'], ['0', '2'], ['2']][index],
+ 'status|1': ['0', '1'],
+ });
+ }
+ return result;
+})();
+
+const deptList = (() => {
+ const result: any[] = [];
+ for (let index = 0; index < 3; index++) {
+ result.push({
+ id: `${index}`,
+ deptName: ['华东分部', '华南分部', '西北分部'][index],
+ orderNo: index + 1,
+ createTime: '@datetime',
+ remark: '@cword(10,20)',
+ 'status|1': ['0', '0', '1'],
+ children: (() => {
+ const children: any[] = [];
+ for (let j = 0; j < 4; j++) {
+ children.push({
+ id: `${index}-${j}`,
+ deptName: ['研发部', '市场部', '商务部', '财务部'][j],
+ orderNo: j + 1,
+ createTime: '@datetime',
+ remark: '@cword(10,20)',
+ 'status|1': ['0', '1'],
+ parentDept: `${index}`,
+ children: undefined,
+ });
+ }
+ return children;
+ })(),
+ });
+ }
+ return result;
+})();
+
+const menuList = (() => {
+ const result: any[] = [];
+ for (let index = 0; index < 3; index++) {
+ result.push({
+ id: `${index}`,
+ icon: ['ion:layers-outline', 'ion:git-compare-outline', 'ion:tv-outline'][index],
+ component: 'LAYOUT',
+ type: '0',
+ menuName: ['Dashboard', '权限管理', '功能'][index],
+ permission: '',
+ orderNo: index + 1,
+ createTime: '@datetime',
+ 'status|1': ['0', '0', '1'],
+ children: (() => {
+ const children: any[] = [];
+ for (let j = 0; j < 4; j++) {
+ children.push({
+ id: `${index}-${j}`,
+ type: '1',
+ menuName: ['菜单1', '菜单2', '菜单3', '菜单4'][j],
+ icon: 'ion:document',
+ permission: ['menu1:view', 'menu2:add', 'menu3:update', 'menu4:del'][index],
+ component: [
+ '/dashboard/welcome/index',
+ '/dashboard/analysis/index',
+ '/dashboard/workbench/index',
+ '/dashboard/test/index',
+ ][j],
+ orderNo: j + 1,
+ createTime: '@datetime',
+ 'status|1': ['0', '1'],
+ parentMenu: `${index}`,
+ children: (() => {
+ const children: any[] = [];
+ for (let k = 0; k < 4; k++) {
+ children.push({
+ id: `${index}-${j}-${k}`,
+ type: '2',
+ menuName: '按钮' + (j + 1) + '-' + (k + 1),
+ icon: '',
+ permission:
+ ['menu1:view', 'menu2:add', 'menu3:update', 'menu4:del'][index] +
+ ':btn' +
+ (k + 1),
+ component: [
+ '/dashboard/welcome/index',
+ '/dashboard/analysis/index',
+ '/dashboard/workbench/index',
+ '/dashboard/test/index',
+ ][j],
+ orderNo: j + 1,
+ createTime: '@datetime',
+ 'status|1': ['0', '1'],
+ parentMenu: `${index}-${j}`,
+ children: undefined,
+ });
+ }
+ return children;
+ })(),
+ });
+ }
+ return children;
+ })(),
+ });
+ }
+ return result;
+})();
+
+export default [
+ {
+ url: '/basic-api/system/getAccountList',
+ timeout: 100,
+ method: 'get',
+ response: ({ query }) => {
+ const { page = 1, pageSize = 20 } = query;
+ return resultPageSuccess(page, pageSize, accountList);
+ },
+ },
+ {
+ url: '/basic-api/system/getRoleListByPage',
+ timeout: 100,
+ method: 'get',
+ response: ({ query }) => {
+ const { page = 1, pageSize = 20 } = query;
+ return resultPageSuccess(page, pageSize, roleList);
+ },
+ },
+ {
+ url: '/basic-api/system/setRoleStatus',
+ timeout: 500,
+ method: 'post',
+ response: ({ query }) => {
+ const { id, status } = query;
+ return resultSuccess({ id, status });
+ },
+ },
+ {
+ url: '/basic-api/system/getAllRoleList',
+ timeout: 100,
+ method: 'get',
+ response: () => {
+ return resultSuccess(roleList);
+ },
+ },
+ {
+ url: '/basic-api/system/getDeptList',
+ timeout: 100,
+ method: 'get',
+ response: () => {
+ return resultSuccess(deptList);
+ },
+ },
+ {
+ url: '/basic-api/system/getMenuList',
+ timeout: 100,
+ method: 'get',
+ response: () => {
+ return resultSuccess(menuList);
+ },
+ },
+ {
+ url: '/basic-api/system/accountExist',
+ timeout: 500,
+ method: 'post',
+ response: ({ body }) => {
+ const { account } = body || {};
+ if (account && account.indexOf('admin') !== -1) {
+ return resultError('该字段不能包含admin');
+ } else {
+ return resultSuccess(`${account} can use`);
+ }
+ },
+ },
+] as MockMethod[];
diff --git a/mock/demo/table-demo.ts b/mock/demo/table-demo.ts
new file mode 100644
index 0000000..b450e3e
--- /dev/null
+++ b/mock/demo/table-demo.ts
@@ -0,0 +1,55 @@
+import { MockMethod } from 'vite-plugin-mock';
+import { Random } from 'mockjs';
+import { resultPageSuccess } from '../_util';
+
+function getRandomPics(count = 10): string[] {
+ const arr: string[] = [];
+ for (let i = 0; i < count; i++) {
+ arr.push(Random.image('800x600', Random.color(), Random.color(), Random.title()));
+ }
+ return arr;
+}
+
+const demoList = (() => {
+ const result: any[] = [];
+ for (let index = 0; index < 200; index++) {
+ result.push({
+ id: `${index}`,
+ beginTime: '@datetime',
+ endTime: '@datetime',
+ address: '@city()',
+ name: '@cname()',
+ name1: '@cname()',
+ name2: '@cname()',
+ name3: '@cname()',
+ name4: '@cname()',
+ name5: '@cname()',
+ name6: '@cname()',
+ name7: '@cname()',
+ name8: '@cname()',
+ radio1: `选项${index + 1}`,
+ radio2: `选项${index + 1}`,
+ radio3: `选项${index + 1}`,
+ avatar: Random.image('400x400', Random.color(), Random.color(), Random.first()),
+ imgArr: getRandomPics(Math.ceil(Math.random() * 3) + 1),
+ imgs: getRandomPics(Math.ceil(Math.random() * 3) + 1),
+ date: `@date('yyyy-MM-dd')`,
+ time: `@time('HH:mm')`,
+ 'no|100000-10000000': 100000,
+ 'status|1': ['normal', 'enable', 'disable'],
+ });
+ }
+ return result;
+})();
+
+export default [
+ {
+ url: '/basic-api/table/getDemoList',
+ timeout: 100,
+ method: 'get',
+ response: ({ query }) => {
+ const { page = 1, pageSize = 20 } = query;
+ return resultPageSuccess(page, pageSize, demoList);
+ },
+ },
+] as MockMethod[];
diff --git a/mock/demo/tree-demo.ts b/mock/demo/tree-demo.ts
new file mode 100644
index 0000000..6fdcb85
--- /dev/null
+++ b/mock/demo/tree-demo.ts
@@ -0,0 +1,38 @@
+import { MockMethod } from 'vite-plugin-mock';
+import { resultSuccess } from '../_util';
+
+const demoTreeList = (keyword) => {
+ const result = {
+ list: [] as Recordable[],
+ };
+ for (let index = 0; index < 5; index++) {
+ const children: Recordable[] = [];
+ for (let j = 0; j < 3; j++) {
+ children.push({
+ title: `${keyword ?? ''}选项${index}-${j}`,
+ value: `${index}-${j}`,
+ key: `${index}-${j}`,
+ });
+ }
+ result.list.push({
+ title: `${keyword ?? ''}选项${index}`,
+ value: `${index}`,
+ key: `${index}`,
+ children,
+ });
+ }
+ return result;
+};
+
+export default [
+ {
+ url: '/basic-api/tree/getDemoOptions',
+ timeout: 1000,
+ method: 'get',
+ response: ({ query }) => {
+ const { keyword } = query;
+ console.log(keyword);
+ return resultSuccess(demoTreeList(keyword));
+ },
+ },
+] as MockMethod[];
diff --git a/mock/sys/menu.ts b/mock/sys/menu.ts
new file mode 100644
index 0000000..e85af63
--- /dev/null
+++ b/mock/sys/menu.ts
@@ -0,0 +1,270 @@
+import { resultSuccess, resultError, getRequestToken, requestParams } from '../_util';
+import { MockMethod } from 'vite-plugin-mock';
+import { createFakeUserList } from './user';
+
+// single
+const dashboardRoute = {
+ path: '/dashboard',
+ name: 'Dashboard',
+ component: 'LAYOUT',
+ redirect: '/dashboard/analysis',
+ meta: {
+ title: 'routes.dashboard.dashboard',
+ hideChildrenInMenu: true,
+ icon: 'bx:bx-home',
+ },
+ children: [
+ {
+ path: 'analysis',
+ name: 'Analysis',
+ component: '/dashboard/analysis/index',
+ meta: {
+ hideMenu: true,
+ hideBreadcrumb: true,
+ title: 'routes.dashboard.analysis',
+ currentActiveMenu: '/dashboard',
+ icon: 'bx:bx-home',
+ },
+ },
+ {
+ path: 'workbench',
+ name: 'Workbench',
+ component: '/dashboard/workbench/index',
+ meta: {
+ hideMenu: true,
+ hideBreadcrumb: true,
+ title: 'routes.dashboard.workbench',
+ currentActiveMenu: '/dashboard',
+ icon: 'bx:bx-home',
+ },
+ },
+ ],
+};
+
+const backRoute = {
+ path: 'back',
+ name: 'PermissionBackDemo',
+ meta: {
+ title: 'routes.demo.permission.back',
+ },
+
+ children: [
+ {
+ path: 'page',
+ name: 'BackAuthPage',
+ component: '/demo/permission/back/index',
+ meta: {
+ title: 'routes.demo.permission.backPage',
+ },
+ },
+ {
+ path: 'btn',
+ name: 'BackAuthBtn',
+ component: '/demo/permission/back/Btn',
+ meta: {
+ title: 'routes.demo.permission.backBtn',
+ },
+ },
+ ],
+};
+
+const authRoute = {
+ path: '/permission',
+ name: 'Permission',
+ component: 'LAYOUT',
+ redirect: '/permission/front/page',
+ meta: {
+ icon: 'carbon:user-role',
+ title: 'routes.demo.permission.permission',
+ },
+ children: [backRoute],
+};
+
+const levelRoute = {
+ path: '/level',
+ name: 'Level',
+ component: 'LAYOUT',
+ redirect: '/level/menu1/menu1-1',
+ meta: {
+ icon: 'carbon:user-role',
+ title: 'routes.demo.level.level',
+ },
+
+ children: [
+ {
+ path: 'menu1',
+ name: 'Menu1Demo',
+ meta: {
+ title: 'Menu1',
+ },
+ children: [
+ {
+ path: 'menu1-1',
+ name: 'Menu11Demo',
+ meta: {
+ title: 'Menu1-1',
+ },
+ children: [
+ {
+ path: 'menu1-1-1',
+ name: 'Menu111Demo',
+ component: '/demo/level/Menu111',
+ meta: {
+ title: 'Menu111',
+ },
+ },
+ ],
+ },
+ {
+ path: 'menu1-2',
+ name: 'Menu12Demo',
+ component: '/demo/level/Menu12',
+ meta: {
+ title: 'Menu1-2',
+ },
+ },
+ ],
+ },
+ {
+ path: 'menu2',
+ name: 'Menu2Demo',
+ component: '/demo/level/Menu2',
+ meta: {
+ title: 'Menu2',
+ },
+ },
+ ],
+};
+
+const sysRoute = {
+ path: '/system',
+ name: 'System',
+ component: 'LAYOUT',
+ redirect: '/system/account',
+ meta: {
+ icon: 'ion:settings-outline',
+ title: 'routes.demo.system.moduleName',
+ },
+ children: [
+ {
+ path: 'account',
+ name: 'AccountManagement',
+ meta: {
+ title: 'routes.demo.system.account',
+ ignoreKeepAlive: true,
+ },
+ component: '/demo/system/account/index',
+ },
+ {
+ path: 'account_detail/:id',
+ name: 'AccountDetail',
+ meta: {
+ hideMenu: true,
+ title: 'routes.demo.system.account_detail',
+ ignoreKeepAlive: true,
+ showMenu: false,
+ currentActiveMenu: '/system/account',
+ },
+ component: '/demo/system/account/AccountDetail',
+ },
+ {
+ path: 'role',
+ name: 'RoleManagement',
+ meta: {
+ title: 'routes.demo.system.role',
+ ignoreKeepAlive: true,
+ },
+ component: '/demo/system/role/index',
+ },
+
+ {
+ path: 'menu',
+ name: 'MenuManagement',
+ meta: {
+ title: 'routes.demo.system.menu',
+ ignoreKeepAlive: true,
+ },
+ component: '/demo/system/menu/index',
+ },
+ {
+ path: 'dept',
+ name: 'DeptManagement',
+ meta: {
+ title: 'routes.demo.system.dept',
+ ignoreKeepAlive: true,
+ },
+ component: '/demo/system/dept/index',
+ },
+ {
+ path: 'changePassword',
+ name: 'ChangePassword',
+ meta: {
+ title: 'routes.demo.system.password',
+ ignoreKeepAlive: true,
+ },
+ component: '/demo/system/password/index',
+ },
+ ],
+};
+
+const linkRoute = {
+ path: '/link',
+ name: 'Link',
+ component: 'LAYOUT',
+ meta: {
+ icon: 'ion:tv-outline',
+ title: 'routes.demo.iframe.frame',
+ },
+ children: [
+ {
+ path: 'doc',
+ name: 'Doc',
+ meta: {
+ title: 'routes.demo.iframe.doc',
+ frameSrc: 'https://doc.vvbin.cn/',
+ },
+ },
+ {
+ path: 'https://doc.vvbin.cn/',
+ name: 'DocExternal',
+ component: 'LAYOUT',
+ meta: {
+ title: 'routes.demo.iframe.docExternal',
+ },
+ },
+ ],
+};
+
+export default [
+ {
+ url: '/basic-api/getMenuList',
+ timeout: 1000,
+ method: 'get',
+ response: (request: requestParams) => {
+ const token = getRequestToken(request);
+ if (!token) {
+ return resultError('Invalid token!');
+ }
+ const checkUser = createFakeUserList().find((item) => item.token === token);
+ if (!checkUser) {
+ return resultError('Invalid user token!');
+ }
+ const id = checkUser.userId;
+ let menu: Object[];
+ switch (id) {
+ case '1':
+ dashboardRoute.redirect = dashboardRoute.path + '/' + dashboardRoute.children[0].path;
+ menu = [dashboardRoute, authRoute, levelRoute, sysRoute, linkRoute];
+ break;
+ case '2':
+ dashboardRoute.redirect = dashboardRoute.path + '/' + dashboardRoute.children[1].path;
+ menu = [dashboardRoute, authRoute, levelRoute, linkRoute];
+ break;
+ default:
+ menu = [];
+ }
+
+ return resultSuccess(menu);
+ },
+ },
+] as unknown as MockMethod[];
diff --git a/mock/sys/user.ts b/mock/sys/user.ts
new file mode 100644
index 0000000..79e46b9
--- /dev/null
+++ b/mock/sys/user.ts
@@ -0,0 +1,122 @@
+import { MockMethod } from 'vite-plugin-mock';
+import { resultError, resultSuccess, getRequestToken, requestParams } from '../_util';
+
+export function createFakeUserList() {
+ return [
+ {
+ userId: '1',
+ username: 'vben',
+ realName: 'Vben Admin',
+ avatar: '',
+ desc: 'manager',
+ password: '123456',
+ token: 'fakeToken1',
+ homePath: '/dashboard/analysis',
+ roles: [
+ {
+ roleName: 'Super Admin',
+ value: 'super',
+ },
+ ],
+ },
+ {
+ userId: '2',
+ username: 'test',
+ password: '123456',
+ realName: 'test user',
+ avatar: '',
+ desc: 'tester',
+ token: 'fakeToken2',
+ homePath: '/dashboard/workbench',
+ roles: [
+ {
+ roleName: 'Tester',
+ value: 'test',
+ },
+ ],
+ },
+ ];
+}
+
+const fakeCodeList: any = {
+ '1': ['1000', '3000', '5000'],
+
+ '2': ['2000', '4000', '6000'],
+};
+export default [
+ // mock user login
+ {
+ url: '/basic-api/login',
+ timeout: 200,
+ method: 'post',
+ response: ({ body }) => {
+ const { username, password } = body;
+ const checkUser = createFakeUserList().find(
+ (item) => item.username === username && password === item.password,
+ );
+ if (!checkUser) {
+ return resultError('Incorrect account or password!');
+ }
+ const { userId, username: _username, token, realName, desc, roles } = checkUser;
+ return resultSuccess({
+ roles,
+ userId,
+ username: _username,
+ token,
+ realName,
+ desc,
+ });
+ },
+ },
+ {
+ url: '/basic-api/getUserInfo',
+ method: 'get',
+ response: (request: requestParams) => {
+ const token = getRequestToken(request);
+ if (!token) return resultError('Invalid token');
+ const checkUser = createFakeUserList().find((item) => item.token === token);
+ if (!checkUser) {
+ return resultError('The corresponding user information was not obtained!');
+ }
+ return resultSuccess(checkUser);
+ },
+ },
+ {
+ url: '/basic-api/getPermCode',
+ timeout: 200,
+ method: 'get',
+ response: (request: requestParams) => {
+ const token = getRequestToken(request);
+ if (!token) return resultError('Invalid token');
+ const checkUser = createFakeUserList().find((item) => item.token === token);
+ if (!checkUser) {
+ return resultError('Invalid token!');
+ }
+ const codeList = fakeCodeList[checkUser.userId];
+
+ return resultSuccess(codeList);
+ },
+ },
+ {
+ url: '/basic-api/logout',
+ timeout: 200,
+ method: 'get',
+ response: (request: requestParams) => {
+ const token = getRequestToken(request);
+ if (!token) return resultError('Invalid token');
+ const checkUser = createFakeUserList().find((item) => item.token === token);
+ if (!checkUser) {
+ return resultError('Invalid token!');
+ }
+ return resultSuccess(undefined, { message: 'Token has been destroyed' });
+ },
+ },
+ {
+ url: '/basic-api/testRetry',
+ statusCode: 405,
+ method: 'get',
+ response: () => {
+ return resultError('Error!');
+ },
+ },
+] as MockMethod[];
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..789bc84
--- /dev/null
+++ b/package.json
@@ -0,0 +1,155 @@
+{
+ "name": "vben-admin",
+ "version": "2.10.1",
+ "homepage": "https://github.com/vbenjs/vue-vben-admin",
+ "bugs": {
+ "url": "https://github.com/vbenjs/vue-vben-admin/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vbenjs/vue-vben-admin.git"
+ },
+ "license": "MIT",
+ "author": {
+ "name": "vben",
+ "email": "anncwb@126.com",
+ "url": "https://github.com/anncwb"
+ },
+ "type": "module",
+ "scripts": {
+ "bootstrap": "pnpm install",
+ "build": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 pnpm vite build",
+ "build:analyze": "cross-env NODE_OPTIONS=--max-old-space-size=8192 pnpm vite build --mode analyze",
+ "build:docker": "vite build --mode docker",
+ "build:no-cache": "pnpm store prune && npm run build",
+ "build:test": "cross-env NODE_OPTIONS=--max-old-space-size=8192 pnpm vite build --mode test",
+ "commit": "czg",
+ "dev": "pnpm vite",
+ "preinstall": "npx only-allow pnpm",
+ "postinstall": "turbo run stub",
+ "lint": "turbo run lint",
+ "lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
+ "lint:prettier": "prettier --write .",
+ "lint:stylelint": "stylelint \"**/*.{vue,css,less,scss}\" --fix --cache --cache-location node_modules/.cache/stylelint/",
+ "prepare": "husky install",
+ "preview": "npm run build && vite preview",
+ "reinstall": "rimraf pnpm-lock.yaml && rimraf package.lock.json && rimraf node_modules && npm run bootstrap",
+ "serve": "npm run dev",
+ "test:gzip": "npx http-server dist --cors --gzip -c-1",
+ "type:check": "vue-tsc --noEmit --skipLibCheck"
+ },
+ "lint-staged": {
+ "*.{js,jsx,ts,tsx}": [
+ "prettier --write",
+ "eslint --fix"
+ ],
+ "{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": [
+ "prettier --write--parser json"
+ ],
+ "package.json": [
+ "prettier --write"
+ ],
+ "*.vue": [
+ "prettier --write",
+ "eslint --fix",
+ "stylelint --fix"
+ ],
+ "*.{scss,less,styl,html}": [
+ "prettier --write",
+ "stylelint --fix"
+ ],
+ "*.md": [
+ "prettier --write"
+ ]
+ },
+ "config": {
+ "commitizen": {
+ "path": "node_modules/cz-git"
+ }
+ },
+ "dependencies": {
+ "@ant-design/icons-vue": "^7.0.1",
+ "@iconify/iconify": "^3.1.1",
+ "@logicflow/core": "^1.2.18",
+ "@logicflow/extension": "^1.2.19",
+ "@vben/hooks": "workspace:*",
+ "@vue/shared": "^3.4.5",
+ "@vueuse/core": "^10.7.1",
+ "@zxcvbn-ts/core": "^3.0.4",
+ "ant-design-vue": "^4.0.8",
+ "axios": "^1.6.4",
+ "codemirror": "^5.65.16",
+ "cropperjs": "^1.6.1",
+ "crypto-js": "^4.2.0",
+ "dayjs": "^1.11.10",
+ "driver.js": "^1.3.1",
+ "echarts": "^5.4.3",
+ "exceljs": "^4.4.0",
+ "lodash-es": "^4.17.21",
+ "mockjs": "^1.1.0",
+ "nprogress": "^0.2.0",
+ "path-to-regexp": "^6.2.1",
+ "pinia": "2.1.7",
+ "pinia-plugin-persistedstate": "^3.2.1",
+ "print-js": "^1.6.0",
+ "qrcode": "^1.5.3",
+ "qs": "^6.11.2",
+ "resize-observer-polyfill": "^1.5.1",
+ "showdown": "^2.1.0",
+ "sortablejs": "^1.15.1",
+ "tinymce": "^5.10.9",
+ "unocss": "0.58.3",
+ "vditor": "^3.9.8",
+ "vue": "3.3.4",
+ "vue-i18n": "^9.8.0",
+ "vue-json-pretty": "^2.3.0",
+ "vue-router": "^4.2.5",
+ "vue-types": "^5.1.1",
+ "vuedraggable": "^4.1.0",
+ "vxe-table": "^4.5.17",
+ "vxe-table-plugin-export-xlsx": "^3.1.0",
+ "xe-utils": "^3.5.14",
+ "xlsx": "^0.18.5"
+ },
+ "devDependencies": {
+ "@commitlint/cli": "^18.4.4",
+ "@commitlint/config-conventional": "^18.4.4",
+ "@iconify/json": "^2.2.164",
+ "@purge-icons/generated": "^0.10.0",
+ "@types/codemirror": "^5.60.15",
+ "@types/crypto-js": "^4.2.1",
+ "@types/lodash-es": "^4.17.12",
+ "@types/mockjs": "^1.0.10",
+ "@types/nprogress": "^0.2.3",
+ "@types/qrcode": "^1.5.5",
+ "@types/qs": "^6.9.11",
+ "@types/showdown": "^2.0.6",
+ "@types/sortablejs": "^1.15.7",
+ "@vben/eslint-config": "workspace:*",
+ "@vben/stylelint-config": "workspace:*",
+ "@vben/ts-config": "workspace:*",
+ "@vben/types": "workspace:*",
+ "@vben/vite-config": "workspace:*",
+ "@vue/compiler-sfc": "^3.4.5",
+ "@vue/test-utils": "^2.4.3",
+ "cross-env": "^7.0.3",
+ "cz-git": "^1.8.0",
+ "czg": "^1.8.0",
+ "husky": "^8.0.3",
+ "lint-staged": "15.2.0",
+ "prettier": "^3.1.1",
+ "prettier-plugin-packagejson": "^2.4.8",
+ "rimraf": "^5.0.5",
+ "turbo": "^1.11.3",
+ "typescript": "^5.3.3",
+ "unbuild": "^2.0.0",
+ "vite": "^5.0.10",
+ "vite-plugin-mock": "^2.9.6",
+ "vue-tsc": "^1.8.27"
+ },
+ "packageManager": "pnpm@8.10.0",
+ "engines": {
+ "node": ">=18.12.0",
+ "pnpm": ">=8.10.0"
+ }
+}
diff --git a/packages/.gitkeep b/packages/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/packages/hooks/.eslintrc.cjs b/packages/hooks/.eslintrc.cjs
new file mode 100644
index 0000000..cd27a19
--- /dev/null
+++ b/packages/hooks/.eslintrc.cjs
@@ -0,0 +1,4 @@
+module.exports = {
+ root: true,
+ extends: ['@vben/eslint-config/strict'],
+};
diff --git a/packages/hooks/build.config.ts b/packages/hooks/build.config.ts
new file mode 100644
index 0000000..20c8b54
--- /dev/null
+++ b/packages/hooks/build.config.ts
@@ -0,0 +1,10 @@
+import { defineBuildConfig } from 'unbuild';
+
+export default defineBuildConfig({
+ clean: true,
+ entries: ['src/index'],
+ declaration: true,
+ rollup: {
+ emitCJS: true,
+ },
+});
diff --git a/packages/hooks/package.json b/packages/hooks/package.json
new file mode 100644
index 0000000..0156169
--- /dev/null
+++ b/packages/hooks/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@vben/hooks",
+ "version": "1.0.0",
+ "homepage": "https://github.com/vbenjs/vue-vben-admin",
+ "bugs": {
+ "url": "https://github.com/vbenjs/vue-vben-admin/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+ "directory": "packages/hooks"
+ },
+ "license": "MIT",
+ "sideEffects": false,
+ "type": "module",
+ "exports": {
+ ".": {
+ "default": "./src/index.ts"
+ }
+ },
+ "main": "./src/index.ts",
+ "module": "./src/index.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "//build": "pnpm unbuild",
+ "//stub": "pnpm unbuild --stub",
+ "clean": "pnpm rimraf .turbo node_modules dist",
+ "lint": "pnpm eslint ."
+ },
+ "dependencies": {
+ "@vueuse/core": "^10.7.1",
+ "lodash-es": "^4.17.21",
+ "vue": "3.3.4"
+ },
+ "devDependencies": {
+ "@vben/types": "workspace:*"
+ }
+}
diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts
new file mode 100644
index 0000000..557a974
--- /dev/null
+++ b/packages/hooks/src/index.ts
@@ -0,0 +1,7 @@
+export * from './onMountedOrActivated';
+export * from './useAttrs';
+export * from './useRefs';
+export * from './useRequest';
+export * from './useScrollTo';
+export * from './useWindowSizeFn';
+export { useTimeoutFn } from '@vueuse/core';
diff --git a/packages/hooks/src/onMountedOrActivated.ts b/packages/hooks/src/onMountedOrActivated.ts
new file mode 100644
index 0000000..53a3c5e
--- /dev/null
+++ b/packages/hooks/src/onMountedOrActivated.ts
@@ -0,0 +1,25 @@
+import { type AnyFunction } from '@vben/types';
+import { nextTick, onActivated, onMounted } from 'vue';
+
+/**
+ * 在 OnMounted 或者 OnActivated 时触发
+ * @param hook 任何函数(包括异步函数)
+ */
+function onMountedOrActivated(hook: AnyFunction) {
+ let mounted: boolean;
+
+ onMounted(() => {
+ hook();
+ nextTick(() => {
+ mounted = true;
+ });
+ });
+
+ onActivated(() => {
+ if (mounted) {
+ hook();
+ }
+ });
+}
+
+export { onMountedOrActivated };
diff --git a/packages/hooks/src/useAttrs.ts b/packages/hooks/src/useAttrs.ts
new file mode 100644
index 0000000..df2118d
--- /dev/null
+++ b/packages/hooks/src/useAttrs.ts
@@ -0,0 +1,43 @@
+import { type Recordable } from '@vben/types';
+import { getCurrentInstance, reactive, shallowRef, watchEffect } from 'vue';
+
+interface UseAttrsOptions {
+ excludeListeners?: boolean;
+ excludeKeys?: string[];
+ excludeDefaultKeys?: boolean;
+}
+
+const DEFAULT_EXCLUDE_KEYS = ['class', 'style'];
+const LISTENER_PREFIX = /^on[A-Z]/;
+
+function entries(obj: Recordable): [string, T][] {
+ return Object.keys(obj).map((key: string) => [key, obj[key]]);
+}
+
+function useAttrs(options: UseAttrsOptions = {}): Recordable {
+ const instance = getCurrentInstance();
+ if (!instance) return {};
+
+ const { excludeListeners = false, excludeKeys = [], excludeDefaultKeys = true } = options;
+ const attrs = shallowRef({});
+ const allExcludeKeys = excludeKeys.concat(excludeDefaultKeys ? DEFAULT_EXCLUDE_KEYS : []);
+
+ // Since attrs are not reactive, make it reactive instead of doing in `onUpdated` hook for better performance
+ instance.attrs = reactive(instance.attrs);
+
+ watchEffect(() => {
+ const res = entries(instance.attrs).reduce((acm, [key, val]) => {
+ if (!allExcludeKeys.includes(key) && !(excludeListeners && LISTENER_PREFIX.test(key))) {
+ acm[key] = val;
+ }
+
+ return acm;
+ }, {} as Recordable);
+
+ attrs.value = res;
+ });
+
+ return attrs;
+}
+
+export { useAttrs, type UseAttrsOptions };
diff --git a/packages/hooks/src/useRefs.ts b/packages/hooks/src/useRefs.ts
new file mode 100644
index 0000000..efbaabd
--- /dev/null
+++ b/packages/hooks/src/useRefs.ts
@@ -0,0 +1,24 @@
+import type { ComponentPublicInstance, Ref } from 'vue';
+import { onBeforeUpdate, shallowRef } from 'vue';
+
+function useRefs(): {
+ refs: Ref;
+ setRefs: (index: number) => (el: Element | ComponentPublicInstance | null) => void;
+} {
+ const refs = shallowRef([]) as Ref;
+
+ onBeforeUpdate(() => {
+ refs.value = [];
+ });
+
+ const setRefs = (index: number) => (el: Element | ComponentPublicInstance | null) => {
+ refs.value[index] = el as T;
+ };
+
+ return {
+ refs,
+ setRefs,
+ };
+}
+
+export { useRefs };
diff --git a/packages/hooks/src/useRequest/Fetch.ts b/packages/hooks/src/useRequest/Fetch.ts
new file mode 100644
index 0000000..5e5b40a
--- /dev/null
+++ b/packages/hooks/src/useRequest/Fetch.ts
@@ -0,0 +1,147 @@
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+import { reactive } from 'vue';
+
+import type { FetchState, PluginReturn, Service, Subscribe, UseRequestOptions } from './types';
+import { isFunction } from './utils/isFunction';
+
+export default class Fetch {
+ pluginImpls: PluginReturn[] = [];
+
+ count: number = 0;
+
+ state: FetchState = reactive({
+ loading: false,
+ params: undefined,
+ data: undefined,
+ error: undefined,
+ });
+
+ constructor(
+ public serviceRef: Service,
+ public options: UseRequestOptions,
+ public subscribe: Subscribe,
+ public initState: Partial> = {},
+ ) {
+ this.setState({ loading: !options.manual, ...initState });
+ }
+
+ setState(s: Partial> = {}) {
+ Object.assign(this.state, s);
+ this.subscribe();
+ }
+
+ runPluginHandler(event: keyof PluginReturn, ...rest: any[]) {
+ // @ts-ignore
+ const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
+ return Object.assign({}, ...r);
+ }
+
+ async runAsync(...params: TParams): Promise {
+ this.count += 1;
+ const currentCount = this.count;
+
+ const {
+ stopNow = false,
+ returnNow = false,
+ ...state
+ } = this.runPluginHandler('onBefore', params);
+
+ // stop request
+ if (stopNow) {
+ return new Promise(() => {});
+ }
+
+ this.setState({
+ loading: true,
+ params,
+ ...state,
+ });
+
+ // return now
+ if (returnNow) {
+ return Promise.resolve(state.data);
+ }
+
+ this.options.onBefore?.(params);
+
+ try {
+ // replace service
+ let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef, params);
+
+ if (!servicePromise) {
+ servicePromise = this.serviceRef(...params);
+ }
+
+ const res = await servicePromise;
+
+ if (currentCount !== this.count) {
+ // prevent run.then when request is canceled
+ return new Promise(() => {});
+ }
+
+ // const formattedResult = this.options.formatResultRef.current ? this.options.formatResultRef.current(res) : res;
+
+ this.setState({ data: res, error: undefined, loading: false });
+
+ this.options.onSuccess?.(res, params);
+ this.runPluginHandler('onSuccess', res, params);
+
+ this.options.onFinally?.(params, res, undefined);
+
+ if (currentCount === this.count) {
+ this.runPluginHandler('onFinally', params, res, undefined);
+ }
+
+ return res;
+ } catch (error) {
+ if (currentCount !== this.count) {
+ // prevent run.then when request is canceled
+ return new Promise(() => {});
+ }
+
+ this.setState({ error, loading: false });
+
+ this.options.onError?.(error, params);
+ this.runPluginHandler('onError', error, params);
+
+ this.options.onFinally?.(params, undefined, error);
+
+ if (currentCount === this.count) {
+ this.runPluginHandler('onFinally', params, undefined, error);
+ }
+
+ throw error;
+ }
+ }
+
+ run(...params: TParams) {
+ this.runAsync(...params).catch((error) => {
+ if (!this.options.onError) {
+ console.error(error);
+ }
+ });
+ }
+
+ cancel() {
+ this.count += 1;
+ this.setState({ loading: false });
+
+ this.runPluginHandler('onCancel');
+ }
+
+ refresh() {
+ // @ts-ignore
+ this.run(...(this.state.params || []));
+ }
+
+ refreshAsync() {
+ // @ts-ignore
+ return this.runAsync(...(this.state.params || []));
+ }
+
+ mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
+ const targetData = isFunction(data) ? data(this.state.data) : data;
+ this.runPluginHandler('onMutate', targetData);
+ this.setState({ data: targetData });
+ }
+}
diff --git a/packages/hooks/src/useRequest/index.ts b/packages/hooks/src/useRequest/index.ts
new file mode 100644
index 0000000..8b8c887
--- /dev/null
+++ b/packages/hooks/src/useRequest/index.ts
@@ -0,0 +1,30 @@
+import useAutoRunPlugin from './plugins/useAutoRunPlugin';
+import useCachePlugin from './plugins/useCachePlugin';
+import useDebouncePlugin from './plugins/useDebouncePlugin';
+import useLoadingDelayPlugin from './plugins/useLoadingDelayPlugin';
+import usePollingPlugin from './plugins/usePollingPlugin';
+import useRefreshOnWindowFocusPlugin from './plugins/useRefreshOnWindowFocusPlugin';
+import useRetryPlugin from './plugins/useRetryPlugin';
+import useThrottlePlugin from './plugins/useThrottlePlugin';
+import type { Service, UseRequestOptions, UseRequestPlugin } from './types';
+import { useRequestImplement } from './useRequestImplement';
+
+export { clearCache } from './utils/cache';
+
+export function useRequest(
+ service: Service,
+ options?: UseRequestOptions,
+ plugins?: UseRequestPlugin[],
+) {
+ return useRequestImplement(service, options, [
+ ...(plugins || []),
+ useDebouncePlugin,
+ useLoadingDelayPlugin,
+ usePollingPlugin,
+ useRefreshOnWindowFocusPlugin,
+ useThrottlePlugin,
+ useAutoRunPlugin,
+ useCachePlugin,
+ useRetryPlugin,
+ ] as UseRequestPlugin[]);
+}
diff --git a/packages/hooks/src/useRequest/plugins/useAutoRunPlugin.ts b/packages/hooks/src/useRequest/plugins/useAutoRunPlugin.ts
new file mode 100644
index 0000000..0023b9b
--- /dev/null
+++ b/packages/hooks/src/useRequest/plugins/useAutoRunPlugin.ts
@@ -0,0 +1,52 @@
+import { ref, unref, watch } from 'vue';
+
+import type { UseRequestPlugin } from '../types';
+
+// support refreshDeps & ready
+const useAutoRunPlugin: UseRequestPlugin = (
+ fetchInstance,
+ { manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction },
+) => {
+ const hasAutoRun = ref(false);
+
+ watch(
+ () => unref(ready),
+ (readyVal) => {
+ if (!unref(manual) && readyVal) {
+ hasAutoRun.value = true;
+ fetchInstance.run(...defaultParams);
+ }
+ },
+ );
+
+ if (refreshDeps.length) {
+ watch(refreshDeps, () => {
+ if (hasAutoRun.value) {
+ return;
+ }
+ if (!manual) {
+ if (refreshDepsAction) {
+ refreshDepsAction();
+ } else {
+ fetchInstance.refresh();
+ }
+ }
+ });
+ }
+
+ return {
+ onBefore: () => {
+ if (!unref(ready)) {
+ return { stopNow: true };
+ }
+ },
+ };
+};
+
+useAutoRunPlugin.onInit = ({ ready = true, manual }) => {
+ return {
+ loading: !unref(manual) && unref(ready),
+ };
+};
+
+export default useAutoRunPlugin;
diff --git a/packages/hooks/src/useRequest/plugins/useCachePlugin.ts b/packages/hooks/src/useRequest/plugins/useCachePlugin.ts
new file mode 100644
index 0000000..b449b66
--- /dev/null
+++ b/packages/hooks/src/useRequest/plugins/useCachePlugin.ts
@@ -0,0 +1,127 @@
+import { onUnmounted, ref, watchEffect } from 'vue';
+
+import type { UseRequestPlugin } from '../types';
+import type { CachedData } from '../utils/cache';
+import { getCache, setCache } from '../utils/cache';
+import { getCachePromise, setCachePromise } from '../utils/cachePromise';
+import { subscribe, trigger } from '../utils/cacheSubscribe';
+
+const useCachePlugin: UseRequestPlugin = (
+ fetchInstance,
+ {
+ cacheKey,
+ cacheTime = 5 * 60 * 1000,
+ staleTime = 0,
+ setCache: customSetCache,
+ getCache: customGetCache,
+ },
+) => {
+ const unSubscribeRef = ref<() => void>();
+ const currentPromiseRef = ref>();
+
+ const _setCache = (key: string, cachedData: CachedData) => {
+ customSetCache ? customSetCache(cachedData) : setCache(key, cacheTime, cachedData);
+ trigger(key, cachedData.data);
+ };
+
+ const _getCache = (key: string, params: any[] = []) => {
+ return customGetCache ? customGetCache(params) : getCache(key);
+ };
+
+ watchEffect(() => {
+ if (!cacheKey) return;
+
+ // get data from cache when init
+ const cacheData = _getCache(cacheKey);
+ if (cacheData && Object.hasOwnProperty.call(cacheData, 'data')) {
+ fetchInstance.state.data = cacheData.data;
+ fetchInstance.state.params = cacheData.params;
+
+ if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
+ fetchInstance.state.loading = false;
+ }
+ }
+
+ // subscribe same cachekey update, trigger update
+ unSubscribeRef.value = subscribe(cacheKey, (data) => {
+ fetchInstance.setState({ data });
+ });
+ });
+
+ onUnmounted(() => {
+ unSubscribeRef.value?.();
+ });
+
+ if (!cacheKey) {
+ return {};
+ }
+
+ return {
+ onBefore: (params) => {
+ const cacheData = _getCache(cacheKey, params);
+
+ if (!cacheData || !Object.hasOwnProperty.call(cacheData, 'data')) {
+ return {};
+ }
+
+ // If the data is fresh, stop request
+ if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
+ return {
+ loading: false,
+ data: cacheData?.data,
+ error: undefined,
+ returnNow: true,
+ };
+ } else {
+ // If the data is stale, return data, and request continue
+ return { data: cacheData?.data, error: undefined };
+ }
+ },
+ onRequest: (service, args) => {
+ let servicePromise = getCachePromise(cacheKey);
+
+ // If has servicePromise, and is not trigger by self, then use it
+ if (servicePromise && servicePromise !== currentPromiseRef.value) {
+ return { servicePromise };
+ }
+
+ servicePromise = service(...args);
+ currentPromiseRef.value = servicePromise;
+ setCachePromise(cacheKey, servicePromise);
+
+ return { servicePromise };
+ },
+ onSuccess: (data, params) => {
+ if (cacheKey) {
+ // cancel subscribe, avoid trgger self
+ unSubscribeRef.value?.();
+
+ _setCache(cacheKey, { data, params, time: new Date().getTime() });
+
+ // resubscribe
+ unSubscribeRef.value = subscribe(cacheKey, (d) => {
+ fetchInstance.setState({ data: d });
+ });
+ }
+ },
+ onMutate: (data) => {
+ if (cacheKey) {
+ // cancel subscribe, avoid trigger self
+ unSubscribeRef.value?.();
+
+ _setCache(cacheKey, {
+ data,
+ params: fetchInstance.state.params,
+ time: new Date().getTime(),
+ });
+
+ // resubscribe
+ unSubscribeRef.value = subscribe(cacheKey, (d) => {
+ fetchInstance.setState({ data: d });
+ });
+ }
+ },
+ };
+};
+
+export default useCachePlugin;
diff --git a/packages/hooks/src/useRequest/plugins/useDebouncePlugin.ts b/packages/hooks/src/useRequest/plugins/useDebouncePlugin.ts
new file mode 100644
index 0000000..6a91ad1
--- /dev/null
+++ b/packages/hooks/src/useRequest/plugins/useDebouncePlugin.ts
@@ -0,0 +1,71 @@
+import type { DebouncedFunc, DebounceSettings } from 'lodash-es';
+import { debounce } from 'lodash-es';
+import { computed, ref, watchEffect } from 'vue';
+
+import type { UseRequestPlugin } from '../types';
+
+const useDebouncePlugin: UseRequestPlugin = (
+ fetchInstance,
+ { debounceWait, debounceLeading, debounceTrailing, debounceMaxWait },
+) => {
+ const debouncedRef = ref>();
+
+ const options = computed(() => {
+ const ret: DebounceSettings = {};
+
+ if (debounceLeading !== undefined) {
+ ret.leading = debounceLeading;
+ }
+ if (debounceTrailing !== undefined) {
+ ret.trailing = debounceTrailing;
+ }
+ if (debounceMaxWait !== undefined) {
+ ret.maxWait = debounceMaxWait;
+ }
+
+ return ret;
+ });
+
+ watchEffect(() => {
+ if (debounceWait) {
+ const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
+
+ debouncedRef.value = debounce(
+ (callback) => {
+ callback();
+ },
+ debounceWait,
+ options.value,
+ );
+
+ // debounce runAsync should be promise
+ // https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
+ fetchInstance.runAsync = (...args) => {
+ return new Promise((resolve, reject) => {
+ debouncedRef.value?.(() => {
+ _originRunAsync(...args)
+ .then(resolve)
+ .catch(reject);
+ });
+ });
+ };
+
+ return () => {
+ debouncedRef.value?.cancel();
+ fetchInstance.runAsync = _originRunAsync;
+ };
+ }
+ });
+
+ if (!debounceWait) {
+ return {};
+ }
+
+ return {
+ onCancel: () => {
+ debouncedRef.value?.cancel();
+ },
+ };
+};
+
+export default useDebouncePlugin;
diff --git a/packages/hooks/src/useRequest/plugins/useLoadingDelayPlugin.ts b/packages/hooks/src/useRequest/plugins/useLoadingDelayPlugin.ts
new file mode 100644
index 0000000..89c77f8
--- /dev/null
+++ b/packages/hooks/src/useRequest/plugins/useLoadingDelayPlugin.ts
@@ -0,0 +1,45 @@
+import { ref, unref } from 'vue';
+
+import type { UseRequestPlugin, UseRequestTimeout } from '../types';
+
+const useLoadingDelayPlugin: UseRequestPlugin = (
+ fetchInstance,
+ { loadingDelay, ready },
+) => {
+ const timerRef = ref();
+
+ if (!loadingDelay) {
+ return {};
+ }
+
+ const cancelTimeout = () => {
+ if (timerRef.value) {
+ clearTimeout(timerRef.value);
+ }
+ };
+
+ return {
+ onBefore: () => {
+ cancelTimeout();
+
+ // Two cases:
+ // 1. ready === undefined
+ // 2. ready === true
+ if (unref(ready) !== false) {
+ timerRef.value = setTimeout(() => {
+ fetchInstance.setState({ loading: true });
+ }, loadingDelay);
+ }
+
+ return { loading: false };
+ },
+ onFinally: () => {
+ cancelTimeout();
+ },
+ onCancel: () => {
+ cancelTimeout();
+ },
+ };
+};
+
+export default useLoadingDelayPlugin;
diff --git a/packages/hooks/src/useRequest/plugins/usePollingPlugin.ts b/packages/hooks/src/useRequest/plugins/usePollingPlugin.ts
new file mode 100644
index 0000000..7d076b3
--- /dev/null
+++ b/packages/hooks/src/useRequest/plugins/usePollingPlugin.ts
@@ -0,0 +1,71 @@
+import { ref, watch } from 'vue';
+
+import type { UseRequestPlugin, UseRequestTimeout } from '../types';
+import { isDocumentVisible } from '../utils/isDocumentVisible';
+import subscribeReVisible from '../utils/subscribeReVisible';
+
+const usePollingPlugin: UseRequestPlugin = (
+ fetchInstance,
+ { pollingInterval, pollingWhenHidden = true, pollingErrorRetryCount = -1 },
+) => {
+ const timerRef = ref();
+ const unsubscribeRef = ref<() => void>();
+ const countRef = ref(0);
+
+ const stopPolling = () => {
+ if (timerRef.value) {
+ clearTimeout(timerRef.value);
+ }
+ unsubscribeRef.value?.();
+ };
+
+ watch(
+ () => pollingInterval,
+ () => {
+ if (!pollingInterval) {
+ stopPolling();
+ }
+ },
+ );
+
+ if (!pollingInterval) {
+ return {};
+ }
+
+ return {
+ onBefore: () => {
+ stopPolling();
+ },
+ onError: () => {
+ countRef.value += 1;
+ },
+ onSuccess: () => {
+ countRef.value = 0;
+ },
+ onFinally: () => {
+ if (
+ pollingErrorRetryCount === -1 ||
+ // When an error occurs, the request is not repeated after pollingErrorRetryCount retries
+ (pollingErrorRetryCount !== -1 && countRef.value <= pollingErrorRetryCount)
+ ) {
+ timerRef.value = setTimeout(() => {
+ // if pollingWhenHidden = false && document is hidden, then stop polling and subscribe revisible
+ if (!pollingWhenHidden && !isDocumentVisible()) {
+ unsubscribeRef.value = subscribeReVisible(() => {
+ fetchInstance.refresh();
+ });
+ } else {
+ fetchInstance.refresh();
+ }
+ }, pollingInterval);
+ } else {
+ countRef.value = 0;
+ }
+ },
+ onCancel: () => {
+ stopPolling();
+ },
+ };
+};
+
+export default usePollingPlugin;
diff --git a/packages/hooks/src/useRequest/plugins/useRefreshOnWindowFocusPlugin.ts b/packages/hooks/src/useRequest/plugins/useRefreshOnWindowFocusPlugin.ts
new file mode 100644
index 0000000..e1f7b3b
--- /dev/null
+++ b/packages/hooks/src/useRequest/plugins/useRefreshOnWindowFocusPlugin.ts
@@ -0,0 +1,37 @@
+import { onUnmounted, ref, watchEffect } from 'vue';
+
+import type { UseRequestPlugin } from '../types';
+import { limit } from '../utils/limit';
+import subscribeFocus from '../utils/subscribeFocus';
+
+const useRefreshOnWindowFocusPlugin: UseRequestPlugin = (
+ fetchInstance,
+ { refreshOnWindowFocus, focusTimespan = 5000 },
+) => {
+ const unsubscribeRef = ref<() => void>();
+
+ const stopSubscribe = () => {
+ unsubscribeRef.value?.();
+ };
+
+ watchEffect(() => {
+ if (refreshOnWindowFocus) {
+ const limitRefresh = limit(fetchInstance.refresh.bind(fetchInstance), focusTimespan);
+ unsubscribeRef.value = subscribeFocus(() => {
+ limitRefresh();
+ });
+ }
+
+ return () => {
+ stopSubscribe();
+ };
+ });
+
+ onUnmounted(() => {
+ stopSubscribe();
+ });
+
+ return {};
+};
+
+export default useRefreshOnWindowFocusPlugin;
diff --git a/packages/hooks/src/useRequest/plugins/useRetryPlugin.ts b/packages/hooks/src/useRequest/plugins/useRetryPlugin.ts
new file mode 100644
index 0000000..b400db3
--- /dev/null
+++ b/packages/hooks/src/useRequest/plugins/useRetryPlugin.ts
@@ -0,0 +1,54 @@
+import { ref } from 'vue';
+
+import type { UseRequestPlugin, UseRequestTimeout } from '../types';
+
+const useRetryPlugin: UseRequestPlugin = (
+ fetchInstance,
+ { retryInterval, retryCount },
+) => {
+ const timerRef = ref();
+ const countRef = ref(0);
+
+ const triggerByRetry = ref(false);
+
+ if (!retryCount) {
+ return {};
+ }
+
+ return {
+ onBefore: () => {
+ if (!triggerByRetry.value) {
+ countRef.value = 0;
+ }
+ triggerByRetry.value = false;
+
+ if (timerRef.value) {
+ clearTimeout(timerRef.value);
+ }
+ },
+ onSuccess: () => {
+ countRef.value = 0;
+ },
+ onError: () => {
+ countRef.value += 1;
+ if (retryCount === -1 || countRef.value <= retryCount) {
+ // Exponential backoff
+ const timeout = retryInterval ?? Math.min(1000 * 2 ** countRef.value, 30000);
+ timerRef.value = setTimeout(() => {
+ triggerByRetry.value = true;
+ fetchInstance.refresh();
+ }, timeout);
+ } else {
+ countRef.value = 0;
+ }
+ },
+ onCancel: () => {
+ countRef.value = 0;
+ if (timerRef.value) {
+ clearTimeout(timerRef.value);
+ }
+ },
+ };
+};
+
+export default useRetryPlugin;
diff --git a/packages/hooks/src/useRequest/plugins/useThrottlePlugin.ts b/packages/hooks/src/useRequest/plugins/useThrottlePlugin.ts
new file mode 100644
index 0000000..adeed05
--- /dev/null
+++ b/packages/hooks/src/useRequest/plugins/useThrottlePlugin.ts
@@ -0,0 +1,63 @@
+import type { DebouncedFunc, ThrottleSettings } from 'lodash-es';
+import { throttle } from 'lodash-es';
+import { ref, watchEffect } from 'vue';
+
+import type { UseRequestPlugin } from '../types';
+
+const useThrottlePlugin: UseRequestPlugin = (
+ fetchInstance,
+ { throttleWait, throttleLeading, throttleTrailing },
+) => {
+ const throttledRef = ref>();
+
+ const options: ThrottleSettings = {};
+ if (throttleLeading !== undefined) {
+ options.leading = throttleLeading;
+ }
+ if (throttleTrailing !== undefined) {
+ options.trailing = throttleTrailing;
+ }
+
+ watchEffect(() => {
+ if (throttleWait) {
+ const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
+
+ throttledRef.value = throttle(
+ (callback) => {
+ callback();
+ },
+ throttleWait,
+ options,
+ );
+
+ // throttle runAsync should be promise
+ // https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
+ fetchInstance.runAsync = (...args) => {
+ return new Promise((resolve, reject) => {
+ throttledRef.value?.(() => {
+ _originRunAsync(...args)
+ .then(resolve)
+ .catch(reject);
+ });
+ });
+ };
+
+ return () => {
+ fetchInstance.runAsync = _originRunAsync;
+ throttledRef.value?.cancel();
+ };
+ }
+ });
+
+ if (!throttleWait) {
+ return {};
+ }
+
+ return {
+ onCancel: () => {
+ throttledRef.value?.cancel();
+ },
+ };
+};
+
+export default useThrottlePlugin;
diff --git a/packages/hooks/src/useRequest/types.ts b/packages/hooks/src/useRequest/types.ts
new file mode 100644
index 0000000..60cc8e9
--- /dev/null
+++ b/packages/hooks/src/useRequest/types.ts
@@ -0,0 +1,124 @@
+import type { MaybeRef, Ref, WatchSource } from 'vue';
+
+import type Fetch from './Fetch';
+import type { CachedData } from './utils/cache';
+
+export type Service = (...args: TParams) => Promise;
+export type Subscribe = () => void;
+
+// for Fetch
+export interface FetchState {
+ loading: boolean;
+ params?: TParams;
+ data?: TData;
+ error?: Error;
+}
+
+export interface PluginReturn {
+ onBefore?: (params: TParams) =>
+ | ({
+ stopNow?: boolean;
+ returnNow?: boolean;
+ } & Partial>)
+ | void;
+
+ onRequest?: (
+ service: Service,
+ params: TParams,
+ ) => {
+ servicePromise?: Promise;
+ };
+
+ onSuccess?: (data: TData, params: TParams) => void;
+ onError?: (e: Error, params: TParams) => void;
+ onFinally?: (params: TParams, data?: TData, e?: Error) => void;
+ onCancel?: () => void;
+ onMutate?: (data: TData) => void;
+}
+
+// for useRequestImplement
+export interface UseRequestOptions {
+ manual?: MaybeRef;
+
+ onBefore?: (params: TParams) => void;
+ onSuccess?: (data: TData, params: TParams) => void;
+ onError?: (e: Error, params: TParams) => void;
+ // formatResult?: (res: any) => TData;
+ onFinally?: (params: TParams, data?: TData, e?: Error) => void;
+
+ defaultParams?: TParams;
+
+ // refreshDeps
+ refreshDeps?: WatchSource[];
+ refreshDepsAction?: () => void;
+
+ // loading delay
+ loadingDelay?: number;
+
+ // polling
+ pollingInterval?: number;
+ pollingWhenHidden?: boolean;
+ pollingErrorRetryCount?: number;
+
+ // refresh on window focus
+ refreshOnWindowFocus?: boolean;
+ focusTimespan?: number;
+
+ // debounce
+ debounceWait?: number;
+ debounceLeading?: boolean;
+ debounceTrailing?: boolean;
+ debounceMaxWait?: number;
+
+ // throttle
+ throttleWait?: number;
+ throttleLeading?: boolean;
+ throttleTrailing?: boolean;
+
+ // cache
+ cacheKey?: string;
+ cacheTime?: number;
+ staleTime?: number;
+ setCache?: (data: CachedData) => void;
+ getCache?: (params: TParams) => CachedData | undefined;
+
+ // retry
+ retryCount?: number;
+ retryInterval?: number;
+
+ // ready
+ ready?: MaybeRef;
+
+ // [key: string]: any;
+}
+
+export interface UseRequestPlugin {
+ // eslint-disable-next-line prettier/prettier
+ (
+ fetchInstance: Fetch,
+ options: UseRequestOptions,
+ ): PluginReturn;
+ onInit?: (options: UseRequestOptions) => Partial>;
+}
+
+// for index
+// export type OptionsWithoutFormat = Omit, 'formatResult'>;
+
+// export interface OptionsWithFormat extends Omit, 'formatResult'> {
+// formatResult: (res: TData) => TFormated;
+// };
+
+export interface UseRequestResult {
+ loading: Ref;
+ data: Ref;
+ error: Ref;
+ params: Ref;
+ cancel: Fetch['cancel'];
+ refresh: Fetch['refresh'];
+ refreshAsync: Fetch['refreshAsync'];
+ run: Fetch['run'];
+ runAsync: Fetch['runAsync'];
+ mutate: Fetch['mutate'];
+}
+
+export type UseRequestTimeout = ReturnType;
diff --git a/packages/hooks/src/useRequest/useRequestImplement.ts b/packages/hooks/src/useRequest/useRequestImplement.ts
new file mode 100644
index 0000000..54cf153
--- /dev/null
+++ b/packages/hooks/src/useRequest/useRequestImplement.ts
@@ -0,0 +1,49 @@
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+import { onMounted, onUnmounted, toRefs } from 'vue';
+
+import Fetch from './Fetch';
+import type { Service, UseRequestOptions, UseRequestPlugin, UseRequestResult } from './types';
+
+export function useRequestImplement(
+ service: Service,
+ options: UseRequestOptions = {},
+ plugins: UseRequestPlugin[] = [],
+) {
+ const { manual = false, ...rest } = options;
+ const fetchOptions = { manual, ...rest };
+
+ const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
+
+ const fetchInstance = new Fetch(
+ service,
+ fetchOptions,
+ () => {},
+ Object.assign({}, ...initState),
+ );
+
+ fetchInstance.options = fetchOptions;
+ // run all plugins hooks
+ fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
+
+ onMounted(() => {
+ if (!manual) {
+ const params = fetchInstance.state.params || options.defaultParams || [];
+ // @ts-ignore
+ fetchInstance.run(...params);
+ }
+ });
+
+ onUnmounted(() => {
+ fetchInstance.cancel();
+ });
+
+ return {
+ ...toRefs(fetchInstance.state),
+ cancel: fetchInstance.cancel.bind(fetchInstance),
+ mutate: fetchInstance.mutate.bind(fetchInstance),
+ refresh: fetchInstance.refresh.bind(fetchInstance),
+ refreshAsync: fetchInstance.refreshAsync.bind(fetchInstance),
+ run: fetchInstance.run.bind(fetchInstance),
+ runAsync: fetchInstance.runAsync.bind(fetchInstance),
+ } as UseRequestResult;
+}
diff --git a/packages/hooks/src/useRequest/utils/cache.ts b/packages/hooks/src/useRequest/utils/cache.ts
new file mode 100644
index 0000000..f89e0a1
--- /dev/null
+++ b/packages/hooks/src/useRequest/utils/cache.ts
@@ -0,0 +1,48 @@
+type Timer = ReturnType;
+type CachedKey = string | number;
+
+export interface CachedData {
+ data: TData;
+ params: TParams;
+ time: number;
+}
+
+interface RecordData extends CachedData {
+ timer: Timer | undefined;
+}
+
+const cache = new Map();
+
+export const setCache = (key: CachedKey, cacheTime: number, cachedData: CachedData) => {
+ const currentCache = cache.get(key);
+ if (currentCache?.timer) {
+ clearTimeout(currentCache.timer);
+ }
+
+ let timer: Timer | undefined = undefined;
+
+ if (cacheTime > -1) {
+ // if cache out, clear it
+ timer = setTimeout(() => {
+ cache.delete(key);
+ }, cacheTime);
+ }
+
+ cache.set(key, {
+ ...cachedData,
+ timer,
+ });
+};
+
+export const getCache = (key: CachedKey) => {
+ return cache.get(key);
+};
+
+export const clearCache = (key?: string | string[]) => {
+ if (key) {
+ const cacheKeys = Array.isArray(key) ? key : [key];
+ cacheKeys.forEach((cacheKey) => cache.delete(cacheKey));
+ } else {
+ cache.clear();
+ }
+};
diff --git a/packages/hooks/src/useRequest/utils/cachePromise.ts b/packages/hooks/src/useRequest/utils/cachePromise.ts
new file mode 100644
index 0000000..602a5c2
--- /dev/null
+++ b/packages/hooks/src/useRequest/utils/cachePromise.ts
@@ -0,0 +1,23 @@
+type CachedKey = string | number;
+
+const cachePromise = new Map>();
+
+export const getCachePromise = (cacheKey: CachedKey) => {
+ return cachePromise.get(cacheKey);
+};
+
+export const setCachePromise = (cacheKey: CachedKey, promise: Promise) => {
+ // Should cache the same promise, cannot be promise.finally
+ // Because the promise.finally will change the reference of the promise
+ cachePromise.set(cacheKey, promise);
+
+ // no use promise.finally for compatibility
+ promise
+ .then((res) => {
+ cachePromise.delete(cacheKey);
+ return res;
+ })
+ .catch(() => {
+ cachePromise.delete(cacheKey);
+ });
+};
diff --git a/packages/hooks/src/useRequest/utils/cacheSubscribe.ts b/packages/hooks/src/useRequest/utils/cacheSubscribe.ts
new file mode 100644
index 0000000..c66dc0c
--- /dev/null
+++ b/packages/hooks/src/useRequest/utils/cacheSubscribe.ts
@@ -0,0 +1,22 @@
+type Listener = (data: any) => void;
+
+const listeners: Record = {};
+
+export const trigger = (key: string, data: any) => {
+ if (listeners[key]) {
+ listeners[key].forEach((item) => item(data));
+ }
+};
+
+export const subscribe = (key: string, listener: Listener) => {
+ if (!listeners[key]) {
+ listeners[key] = [];
+ }
+
+ listeners[key].push(listener);
+
+ return function unsubscribe() {
+ const index = listeners[key].indexOf(listener);
+ listeners[key].splice(index, 1);
+ };
+};
diff --git a/packages/hooks/src/useRequest/utils/isBrowser.ts b/packages/hooks/src/useRequest/utils/isBrowser.ts
new file mode 100644
index 0000000..4a1b91e
--- /dev/null
+++ b/packages/hooks/src/useRequest/utils/isBrowser.ts
@@ -0,0 +1,5 @@
+export const isBrowser = !!(
+ typeof window !== 'undefined' &&
+ window.document &&
+ window.document.createElement
+);
diff --git a/packages/hooks/src/useRequest/utils/isDocumentVisible.ts b/packages/hooks/src/useRequest/utils/isDocumentVisible.ts
new file mode 100644
index 0000000..e9d1275
--- /dev/null
+++ b/packages/hooks/src/useRequest/utils/isDocumentVisible.ts
@@ -0,0 +1,8 @@
+import { isBrowser } from './isBrowser';
+
+export function isDocumentVisible(): boolean {
+ if (isBrowser) {
+ return document.visibilityState !== 'hidden';
+ }
+ return true;
+}
diff --git a/packages/hooks/src/useRequest/utils/isFunction.ts b/packages/hooks/src/useRequest/utils/isFunction.ts
new file mode 100644
index 0000000..782f37c
--- /dev/null
+++ b/packages/hooks/src/useRequest/utils/isFunction.ts
@@ -0,0 +1,2 @@
+export const isFunction = (value: unknown): value is (...args: any) => any =>
+ typeof value === 'function';
diff --git a/packages/hooks/src/useRequest/utils/isOnline.ts b/packages/hooks/src/useRequest/utils/isOnline.ts
new file mode 100644
index 0000000..900f9a3
--- /dev/null
+++ b/packages/hooks/src/useRequest/utils/isOnline.ts
@@ -0,0 +1,8 @@
+import { isBrowser } from './isBrowser';
+
+export function isOnline(): boolean {
+ if (isBrowser && typeof navigator.onLine !== 'undefined') {
+ return navigator.onLine;
+ }
+ return true;
+}
diff --git a/packages/hooks/src/useRequest/utils/limit.ts b/packages/hooks/src/useRequest/utils/limit.ts
new file mode 100644
index 0000000..c540e87
--- /dev/null
+++ b/packages/hooks/src/useRequest/utils/limit.ts
@@ -0,0 +1,12 @@
+export function limit(fn: any, timespan: number) {
+ let pending = false;
+
+ return (...args: any[]) => {
+ if (pending) return;
+ pending = true;
+ fn(...args);
+ setTimeout(() => {
+ pending = false;
+ }, timespan);
+ };
+}
diff --git a/packages/hooks/src/useRequest/utils/subscribeFocus.ts b/packages/hooks/src/useRequest/utils/subscribeFocus.ts
new file mode 100644
index 0000000..751650f
--- /dev/null
+++ b/packages/hooks/src/useRequest/utils/subscribeFocus.ts
@@ -0,0 +1,30 @@
+import { isBrowser } from './isBrowser';
+import { isDocumentVisible } from './isDocumentVisible';
+import { isOnline } from './isOnline';
+
+type Listener = () => void;
+
+const listeners: Listener[] = [];
+
+if (isBrowser) {
+ const revalidate = () => {
+ if (!isDocumentVisible() || !isOnline()) return;
+ for (let i = 0; i < listeners.length; i++) {
+ const listener = listeners[i];
+ listener();
+ }
+ };
+ window.addEventListener('visibilitychange', revalidate, false);
+ window.addEventListener('focus', revalidate, false);
+}
+
+export default function subscribe(listener: Listener) {
+ listeners.push(listener);
+
+ return function unsubscribe() {
+ const index = listeners.indexOf(listener);
+ if (index > -1) {
+ listeners.splice(index, 1);
+ }
+ };
+}
diff --git a/packages/hooks/src/useRequest/utils/subscribeReVisible.ts b/packages/hooks/src/useRequest/utils/subscribeReVisible.ts
new file mode 100644
index 0000000..cf961e9
--- /dev/null
+++ b/packages/hooks/src/useRequest/utils/subscribeReVisible.ts
@@ -0,0 +1,25 @@
+import { isBrowser } from './isBrowser';
+import { isDocumentVisible } from './isDocumentVisible';
+
+type Listener = () => void;
+
+const listeners: Listener[] = [];
+
+if (isBrowser) {
+ const revalidate = () => {
+ if (!isDocumentVisible()) return;
+ for (let i = 0; i < listeners.length; i++) {
+ const listener = listeners[i];
+ listener();
+ }
+ };
+ window.addEventListener('visibilitychange', revalidate, false);
+}
+
+export default function subscribe(listener: Listener) {
+ listeners.push(listener);
+ return function unsubscribe() {
+ const index = listeners.indexOf(listener);
+ listeners.splice(index, 1);
+ };
+}
diff --git a/packages/hooks/src/useScrollTo.ts b/packages/hooks/src/useScrollTo.ts
new file mode 100644
index 0000000..f6a95f4
--- /dev/null
+++ b/packages/hooks/src/useScrollTo.ts
@@ -0,0 +1,60 @@
+import { shallowRef, unref } from 'vue';
+
+interface UseScrollToOptions {
+ el: any;
+ to: number;
+ duration?: number;
+ callback?: () => any;
+}
+
+function easeInOutQuad(t: number, b: number, c: number, d: number) {
+ t /= d / 2;
+ if (t < 1) {
+ return (c / 2) * t * t + b;
+ }
+ t--;
+ return (-c / 2) * (t * (t - 2) - 1) + b;
+}
+
+function move(el: HTMLElement, amount: number) {
+ el.scrollTop = amount;
+}
+
+const position = (el: HTMLElement) => {
+ return el.scrollTop;
+};
+function useScrollTo({ el, to, duration = 500, callback }: UseScrollToOptions) {
+ const isActiveRef = shallowRef(false);
+ const start = position(el);
+ const change = to - start;
+ const increment = 20;
+ let currentTime = 0;
+
+ const animateScroll = function () {
+ if (!unref(isActiveRef)) {
+ return;
+ }
+ currentTime += increment;
+ const val = easeInOutQuad(currentTime, start, change, duration);
+ move(el, val);
+ if (currentTime < duration && unref(isActiveRef)) {
+ requestAnimationFrame(animateScroll);
+ } else {
+ if (callback && typeof callback === 'function') {
+ callback();
+ }
+ }
+ };
+ const run = () => {
+ isActiveRef.value = true;
+ animateScroll();
+ };
+
+ const stop = () => {
+ isActiveRef.value = false;
+ };
+
+ return { start: run, stop };
+}
+
+export { useScrollTo, type UseScrollToOptions };
diff --git a/packages/hooks/src/useWindowSizeFn.ts b/packages/hooks/src/useWindowSizeFn.ts
new file mode 100644
index 0000000..d8e7710
--- /dev/null
+++ b/packages/hooks/src/useWindowSizeFn.ts
@@ -0,0 +1,40 @@
+import { type AnyFunction } from '@vben/types';
+import { tryOnMounted, tryOnUnmounted, useDebounceFn } from '@vueuse/core';
+
+interface UseWindowSizeOptions {
+ wait?: number;
+ once?: boolean;
+ immediate?: boolean;
+ listenerOptions?: AddEventListenerOptions | boolean;
+}
+
+function useWindowSizeFn(fn: AnyFunction, options: UseWindowSizeOptions = {}) {
+ const { wait = 150, immediate } = options;
+ let handler = () => {
+ fn();
+ };
+ const handleSize = useDebounceFn(handler, wait);
+ handler = handleSize;
+
+ const start = () => {
+ if (immediate) {
+ handler();
+ }
+ window.addEventListener('resize', handler);
+ };
+
+ const stop = () => {
+ window.removeEventListener('resize', handler);
+ };
+
+ tryOnMounted(() => {
+ start();
+ });
+
+ tryOnUnmounted(() => {
+ stop();
+ });
+ return { start, stop };
+}
+
+export { useWindowSizeFn, type UseWindowSizeOptions };
diff --git a/packages/hooks/tsconfig.json b/packages/hooks/tsconfig.json
new file mode 100644
index 0000000..8508d50
--- /dev/null
+++ b/packages/hooks/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/ts-config/vue-app.json",
+ "include": ["src"]
+}
diff --git a/packages/types/.eslintrc.cjs b/packages/types/.eslintrc.cjs
new file mode 100644
index 0000000..cd27a19
--- /dev/null
+++ b/packages/types/.eslintrc.cjs
@@ -0,0 +1,4 @@
+module.exports = {
+ root: true,
+ extends: ['@vben/eslint-config/strict'],
+};
diff --git a/packages/types/build.config.ts b/packages/types/build.config.ts
new file mode 100644
index 0000000..20c8b54
--- /dev/null
+++ b/packages/types/build.config.ts
@@ -0,0 +1,10 @@
+import { defineBuildConfig } from 'unbuild';
+
+export default defineBuildConfig({
+ clean: true,
+ entries: ['src/index'],
+ declaration: true,
+ rollup: {
+ emitCJS: true,
+ },
+});
diff --git a/packages/types/package.json b/packages/types/package.json
new file mode 100644
index 0000000..38c15a9
--- /dev/null
+++ b/packages/types/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@vben/types",
+ "version": "1.0.0",
+ "homepage": "https://github.com/vbenjs/vue-vben-admin",
+ "bugs": {
+ "url": "https://github.com/vbenjs/vue-vben-admin/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+ "directory": "packages/types"
+ },
+ "license": "MIT",
+ "sideEffects": false,
+ "type": "module",
+ "exports": {
+ ".": {
+ "default": "./src/index.ts"
+ }
+ },
+ "main": "./src/index.ts",
+ "module": "./src/index.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "//build": "pnpm unbuild",
+ "//stub": "pnpm unbuild --stub",
+ "clean": "pnpm rimraf .turbo node_modules dist",
+ "lint": "pnpm eslint ."
+ }
+}
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
new file mode 100644
index 0000000..04bca77
--- /dev/null
+++ b/packages/types/src/index.ts
@@ -0,0 +1 @@
+export * from './utils';
diff --git a/packages/types/src/utils.ts b/packages/types/src/utils.ts
new file mode 100644
index 0000000..b8ed226
--- /dev/null
+++ b/packages/types/src/utils.ts
@@ -0,0 +1,58 @@
+/**
+ * 任意类型的异步函数
+ */
+type AnyPromiseFunction = (...arg: any[]) => PromiseLike;
+
+/**
+ * 任意类型的普通函数
+ */
+type AnyNormalFunction = (...arg: any[]) => any;
+
+/**
+ * 任意类型的函数
+ */
+type AnyFunction = AnyNormalFunction | AnyPromiseFunction;
+
+/**
+ * T | null 包装
+ */
+type Nullable = T | null;
+
+/**
+ * T | Not null 包装
+ */
+type NonNullable = T extends null | undefined ? never : T;
+
+/**
+ * 字符串类型对象
+ */
+type Recordable = Record;
+
+/**
+ * 字符串类型对象(只读)
+ */
+interface ReadonlyRecordable {
+ readonly [key: string]: T;
+}
+
+/**
+ * setTimeout 返回值类型
+ */
+type TimeoutHandle = ReturnType;
+
+/**
+ * setInterval 返回值类型
+ */
+type IntervalHandle = ReturnType;
+
+export {
+ type AnyFunction,
+ type AnyNormalFunction,
+ type AnyPromiseFunction,
+ type IntervalHandle,
+ type NonNullable,
+ type Nullable,
+ type ReadonlyRecordable,
+ type Recordable,
+ type TimeoutHandle,
+};
diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json
new file mode 100644
index 0000000..8508d50
--- /dev/null
+++ b/packages/types/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/ts-config/vue-app.json",
+ "include": ["src"]
+}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
new file mode 100644
index 0000000..103ba07
--- /dev/null
+++ b/pnpm-workspace.yaml
@@ -0,0 +1,4 @@
+packages:
+ - 'internal/*'
+ - 'packages/*'
+ - 'apps/*'
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..d92e0b8
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/logo.png b/public/logo.png
new file mode 100644
index 0000000..cd4c33d
Binary files /dev/null and b/public/logo.png differ
diff --git a/public/resource/tinymce/langs/en.js b/public/resource/tinymce/langs/en.js
new file mode 100644
index 0000000..27337c3
--- /dev/null
+++ b/public/resource/tinymce/langs/en.js
@@ -0,0 +1,419 @@
+tinymce.addI18n('es', {
+ Redo: 'Rehacer',
+ Undo: 'Deshacer',
+ Cut: 'Cortar',
+ Copy: 'Copiar',
+ Paste: 'Pegar',
+ 'Select all': 'Seleccionar todo',
+ 'New document': 'Nuevo documento',
+ Ok: 'Ok',
+ Cancel: 'Cancelar',
+ 'Visual aids': 'Ayudas visuales',
+ Bold: 'Negrita',
+ Italic: 'Cursiva',
+ Underline: 'Subrayado',
+ Strikethrough: 'Tachado',
+ Superscript: 'Super\u00edndice',
+ Subscript: 'Sub\u00edndice',
+ 'Clear formatting': 'Limpiar formato',
+ 'Align left': 'Alinear a la izquierda',
+ 'Align center': 'Alinear al centro',
+ 'Align right': 'Alinear a la derecha',
+ Justify: 'Justificar',
+ 'Bullet list': 'Lista de vi\u00f1etas',
+ 'Numbered list': 'Lista numerada',
+ 'Decrease indent': 'Disminuir sangr\u00eda',
+ 'Increase indent': 'Incrementar sangr\u00eda',
+ Close: 'Cerrar',
+ Formats: 'Formatos',
+ "Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": 'Su navegador no es compatible con el acceso directo al portapapeles. Use las teclas Crtl+X\/C\/V de su teclado.',
+ Headers: 'Encabezados',
+ 'Header 1': 'Encabezado 1',
+ 'Header 2': 'Encabezado 2',
+ 'Header 3': 'Encabezado 3',
+ 'Header 4': 'Encabezado 4',
+ 'Header 5': 'Encabezado 5',
+ 'Header 6': 'Encabezado 6',
+ Headings: 'Encabezados',
+ 'Heading 1': 'Encabezado 1',
+ 'Heading 2': 'Encabezado 2',
+ 'Heading 3': 'Encabezado 3',
+ 'Heading 4': 'Encabezado 4',
+ 'Heading 5': 'Encabezado 5',
+ 'Heading 6': 'Encabezado 6',
+ Preformatted: 'Con formato previo',
+ Div: 'Div',
+ Pre: 'Pre',
+ Code: 'C\u00f3digo',
+ Paragraph: 'P\u00e1rrafo',
+ Blockquote: 'Blockquote',
+ Inline: 'Alineado',
+ Blocks: 'Bloques',
+ 'Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.': 'Pegar est\u00e1 ahora en modo de texto plano. El contenido se pegar\u00e1 como texto plano hasta que desactive esta opci\u00f3n.',
+ Fonts: 'Fuentes',
+ 'Font Sizes': 'Tama\u00f1os de fuente',
+ Class: 'Clase',
+ 'Browse for an image': 'Buscar una imagen',
+ OR: 'OR',
+ 'Drop an image here': 'Arrastre una imagen aqu\u00ed',
+ Upload: 'Cargar',
+ Block: 'Bloque',
+ Align: 'Alinear',
+ Default: 'Por defecto',
+ Circle: 'C\u00edrculo',
+ Disc: 'Disco',
+ Square: 'Cuadrado',
+ 'Lower Alpha': 'Inferior Alfa',
+ 'Lower Greek': 'Inferior Griega',
+ 'Lower Roman': 'Inferior Romana',
+ 'Upper Alpha': 'Superior Alfa',
+ 'Upper Roman': 'Superior Romana',
+ 'Anchor...': 'Anclaje...',
+ Name: 'Nombre',
+ Id: 'Id',
+ 'Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.': 'Deber\u00eda comenzar por una letra, seguida solo de letras, n\u00fameros, guiones, puntos, dos puntos o guiones bajos.',
+ 'You have unsaved changes are you sure you want to navigate away?': 'Tiene cambios sin guardar. \u00bfEst\u00e1 seguro de que quiere salir?',
+ 'Restore last draft': 'Restaurar el \u00faltimo borrador',
+ 'Special character...': 'Car\u00e1cter especial...',
+ 'Source code': 'C\u00f3digo fuente',
+ 'Insert\/Edit code sample': 'Insertar\/editar c\u00f3digo de prueba',
+ Language: 'Idioma',
+ 'Code sample...': 'Ejemplo de c\u00f3digo...',
+ 'Color Picker': 'Selector de colores',
+ R: 'R',
+ G: 'V',
+ B: 'A',
+ 'Left to right': 'De izquierda a derecha',
+ 'Right to left': 'De derecha a izquierda',
+ 'Emoticons...': 'Emoticones...',
+ 'Metadata and Document Properties': 'Metadatos y propiedades del documento',
+ Title: 'T\u00edtulo',
+ Keywords: 'Palabras clave',
+ Description: 'Descripci\u00f3n',
+ Robots: 'Robots',
+ Author: 'Autor',
+ Encoding: 'Codificaci\u00f3n',
+ Fullscreen: 'Pantalla completa',
+ Action: 'Acci\u00f3n',
+ Shortcut: 'Atajo',
+ Help: 'Ayuda',
+ Address: 'Direcci\u00f3n',
+ 'Focus to menubar': 'Enfocar la barra del men\u00fa',
+ 'Focus to toolbar': 'Enfocar la barra de herramientas',
+ 'Focus to element path': 'Enfocar la ruta del elemento',
+ 'Focus to contextual toolbar': 'Enfocar la barra de herramientas contextual',
+ 'Insert link (if link plugin activated)': 'Insertar enlace (si el complemento de enlace est\u00e1 activado)',
+ 'Save (if save plugin activated)': 'Guardar (si el componente de salvar est\u00e1 activado)',
+ 'Find (if searchreplace plugin activated)': 'Buscar (si el complemento buscar-remplazar est\u00e1 activado)',
+ 'Plugins installed ({0}):': 'Plugins instalados ({0}):',
+ 'Premium plugins:': 'Complementos premium:',
+ 'Learn more...': 'Aprende m\u00e1s...',
+ 'You are using {0}': 'Estas usando {0}',
+ Plugins: 'Complementos',
+ 'Handy Shortcuts': 'Accesos directos',
+ 'Horizontal line': 'L\u00ednea horizontal',
+ 'Insert\/edit image': 'Insertar\/editar imagen',
+ 'Image description': 'Descripci\u00f3n de la imagen',
+ Source: 'Enlace',
+ Dimensions: 'Dimensiones',
+ 'Constrain proportions': 'Restringir proporciones',
+ General: 'General',
+ Advanced: 'Avanzado',
+ Style: 'Estilo',
+ 'Vertical space': 'Espacio vertical',
+ 'Horizontal space': 'Espacio horizontal',
+ Border: 'Borde',
+ 'Insert image': 'Insertar imagen',
+ 'Image...': 'Imagen...',
+ 'Image list': 'Lista de im\u00e1genes',
+ 'Rotate counterclockwise': 'Girar a la izquierda',
+ 'Rotate clockwise': 'Girar a la derecha',
+ 'Flip vertically': 'Invertir verticalmente',
+ 'Flip horizontally': 'Invertir horizontalmente',
+ 'Edit image': 'Editar imagen',
+ 'Image options': 'Opciones de imagen',
+ 'Zoom in': 'Acercar',
+ 'Zoom out': 'Alejar',
+ Crop: 'Recortar',
+ Resize: 'Redimensionar',
+ Orientation: 'Orientaci\u00f3n',
+ Brightness: 'Brillo',
+ Sharpen: 'Forma',
+ Contrast: 'Contraste',
+ 'Color levels': 'Niveles de color',
+ Gamma: 'Gamma',
+ Invert: 'Invertir',
+ Apply: 'Aplicar',
+ Back: 'Atr\u00e1s',
+ 'Insert date\/time': 'Insertar fecha\/hora',
+ 'Date\/time': 'Fecha\/hora',
+ 'Insert\/Edit Link': 'Insertar\/editar enlace',
+ 'Insert\/edit link': 'Insertar\/editar enlace',
+ 'Text to display': 'Texto para mostrar',
+ Url: 'URL',
+ 'Open link in...': 'Abrir enlace en...',
+ 'Current window': 'Ventana actual',
+ None: 'Ninguno',
+ 'New window': 'Nueva ventana',
+ 'Remove link': 'Quitar enlace',
+ Anchors: 'Anclas',
+ 'Link...': 'Enlace...',
+ 'Paste or type a link': 'Pega o introduce un enlace',
+ 'The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?': 'El enlace que has introducido no parece ser una direcci\u00f3n de correo electr\u00f3nico. Quieres a\u00f1adir el prefijo necesario mailto: ?',
+ 'The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?': 'El enlace que has introducido no parece ser una enlace externo. Quieres a\u00f1adir el prefijo necesario http:\/\/ ?',
+ 'Link list': 'Lista de enlaces',
+ 'Insert video': 'Insertar video',
+ 'Insert\/edit video': 'Insertar\/editar video',
+ 'Insert\/edit media': 'Insertar\/editar medio',
+ 'Alternative source': 'Enlace alternativo',
+ 'Alternative source URL': 'Origen de URL alternativo',
+ 'Media poster (Image URL)': 'P\u00f3ster de medio (URL de imagen)',
+ 'Paste your embed code below:': 'Pega tu c\u00f3digo embebido debajo',
+ Embed: 'Incrustado',
+ 'Media...': 'Medios...',
+ 'Nonbreaking space': 'Espacio fijo',
+ 'Page break': 'Salto de p\u00e1gina',
+ 'Paste as text': 'Pegar como texto',
+ Preview: 'Previsualizar',
+ 'Print...': 'Imprimir...',
+ Save: 'Guardar',
+ Find: 'Buscar',
+ 'Replace with': 'Reemplazar con',
+ Replace: 'Reemplazar',
+ 'Replace all': 'Reemplazar todo',
+ Previous: 'Anterior',
+ Next: 'Siguiente',
+ 'Find and replace...': 'Buscar y reemplazar...',
+ 'Could not find the specified string.': 'No se encuentra la cadena de texto especificada',
+ 'Match case': 'Coincidencia exacta',
+ 'Find whole words only': 'Solo palabras completas',
+ 'Spell check': 'Revisar ortograf\u00eda',
+ Ignore: 'Ignorar',
+ 'Ignore all': 'Ignorar todos',
+ Finish: 'Finalizar',
+ 'Add to Dictionary': 'A\u00f1adir al Diccionario',
+ 'Insert table': 'Insertar tabla',
+ 'Table properties': 'Propiedades de la tabla',
+ 'Delete table': 'Eliminar tabla',
+ Cell: 'Celda',
+ Row: 'Fila',
+ Column: 'Columna',
+ 'Cell properties': 'Propiedades de la celda',
+ 'Merge cells': 'Combinar celdas',
+ 'Split cell': 'Dividir celdas',
+ 'Insert row before': 'Insertar fila antes',
+ 'Insert row after': 'Insertar fila despu\u00e9s ',
+ 'Delete row': 'Eliminar fila',
+ 'Row properties': 'Propiedades de la fila',
+ 'Cut row': 'Cortar fila',
+ 'Copy row': 'Copiar fila',
+ 'Paste row before': 'Pegar la fila antes',
+ 'Paste row after': 'Pegar la fila despu\u00e9s',
+ 'Insert column before': 'Insertar columna antes',
+ 'Insert column after': 'Insertar columna despu\u00e9s',
+ 'Delete column': 'Eliminar columna',
+ Cols: 'Columnas',
+ Rows: 'Filas',
+ Width: 'Ancho',
+ Height: 'Alto',
+ 'Cell spacing': 'Espacio entre celdas',
+ 'Cell padding': 'Relleno de celda',
+ 'Show caption': 'Mostrar t\u00edtulo',
+ Left: 'Izquierda',
+ Center: 'Centrado',
+ Right: 'Derecha',
+ 'Cell type': 'Tipo de celda',
+ Scope: '\u00c1mbito',
+ Alignment: 'Alineaci\u00f3n',
+ 'H Align': 'Alineamiento Horizontal',
+ 'V Align': 'Alineamiento Vertical',
+ Top: 'Arriba',
+ Middle: 'Centro',
+ Bottom: 'Abajo',
+ 'Header cell': 'Celda de la cebecera',
+ 'Row group': 'Grupo de filas',
+ 'Column group': 'Grupo de columnas',
+ 'Row type': 'Tipo de fila',
+ Header: 'Cabecera',
+ Body: 'Cuerpo',
+ Footer: 'Pie de p\u00e1gina',
+ 'Border color': 'Color del borde',
+ 'Insert template...': 'Insertar plantilla...',
+ Templates: 'Plantillas',
+ Template: 'Plantilla',
+ 'Text color': 'Color del texto',
+ 'Background color': 'Color de fondo',
+ 'Custom...': 'Personalizar...',
+ 'Custom color': 'Color personalizado',
+ 'No color': 'Sin color',
+ 'Remove color': 'Quitar color',
+ 'Table of Contents': 'Tabla de contenidos',
+ 'Show blocks': 'Mostrar bloques',
+ 'Show invisible characters': 'Mostrar caracteres invisibles',
+ 'Word count': 'Contar palabras',
+ Count: 'Recuento',
+ Document: 'Documento',
+ Selection: 'Selecci\u00f3n',
+ Words: 'Palabras',
+ 'Words: {0}': 'Palabras: {0}',
+ '{0} words': '{0} palabras',
+ File: 'Archivo',
+ Edit: 'Editar',
+ Insert: 'Insertar',
+ View: 'Ver',
+ Format: 'Formato',
+ Table: 'Tabla',
+ Tools: 'Herramientas',
+ 'Powered by {0}': 'Desarrollado por {0}',
+ 'Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help': '\u00c1rea de texto enriquecido. Pulse ALT-F9 para el menu. Pulse ALT-F10 para la barra de herramientas. Pulse ALT-0 para ayuda',
+ 'Image title': 'Titulo de imagen',
+ 'Border width': 'Ancho de borde',
+ 'Border style': 'Estilo de borde',
+ Error: 'Error',
+ Warn: 'Advertencia',
+ Valid: 'V\u00e1lido',
+ 'To open the popup, press Shift+Enter': 'Para abrir el elemento emergente, pulse May\u00fas+Intro',
+ 'Rich Text Area. Press ALT-0 for help.': '\u00c1rea de texto enriquecido. Pulse ALT-0 para abrir la ayuda.',
+ 'System Font': 'Fuente de sistema',
+ 'Failed to upload image: {0}': 'Fallo al cargar imagen: {0}',
+ 'Failed to load plugin: {0} from url {1}': 'Fallo al cargar complemento: {0} desde URL {1}',
+ 'Failed to load plugin url: {0}': 'Fallo al cargar URL del complemento: {0}',
+ 'Failed to initialize plugin: {0}': 'Fallo al iniciar el complemento: {0}',
+ example: 'ejemplo',
+ Search: 'Buscar',
+ All: 'Todo',
+ Currency: 'Divisa',
+ Text: 'Texto',
+ Quotations: 'Comillas',
+ Mathematical: 'S\u00edmbolo matem\u00e1tico',
+ 'Extended Latin': 'Latino extendido A',
+ Symbols: 'S\u00edmbolos',
+ Arrows: 'Flechas',
+ 'User Defined': 'Definido por el usuario',
+ 'dollar sign': 'signo de d\u00f3lar',
+ 'currency sign': 'signo de divisa',
+ 'euro-currency sign': 'signo de euro',
+ 'colon sign': 'signo de dos puntos',
+ 'cruzeiro sign': 'signo de cruceiro',
+ 'french franc sign': 'signo de franco franc\u00e9s',
+ 'lira sign': 'signo de lira',
+ 'mill sign': 'signo de mill',
+ 'naira sign': 'signo de naira',
+ 'peseta sign': 'signo de peseta',
+ 'rupee sign': 'signo de rupia',
+ 'won sign': 'signo de won',
+ 'new sheqel sign': 'signo de nuevo s\u00e9quel',
+ 'dong sign': 'signo de dong',
+ 'kip sign': 'signo de kip',
+ 'tugrik sign': 'signo de tugrik',
+ 'drachma sign': 'signo de dracma',
+ 'german penny symbol': 'signo de penique alem\u00e1n',
+ 'peso sign': 'signo de peso',
+ 'guarani sign': 'signo de guaran\u00ed',
+ 'austral sign': 'signo de austral',
+ 'hryvnia sign': 'signo de grivna',
+ 'cedi sign': 'signo de cedi',
+ 'livre tournois sign': 'signo de libra tornesa',
+ 'spesmilo sign': 'signo de spesmilo',
+ 'tenge sign': 'signo de tenge',
+ 'indian rupee sign': 'signo de rupia india',
+ 'turkish lira sign': 'signo de lira turca',
+ 'nordic mark sign': 'signo de marco n\u00f3rdico',
+ 'manat sign': 'signo de manat',
+ 'ruble sign': 'signo de rublo',
+ 'yen character': 'car\u00e1cter de yen',
+ 'yuan character': 'car\u00e1cter de yuan',
+ 'yuan character, in hong kong and taiwan': 'car\u00e1cter de yuan en Hong Kong y Taiw\u00e1n',
+ 'yen\/yuan character variant one': 'Variante uno de car\u00e1cter de yen\/yuan',
+ 'Loading emoticons...': 'Cargando emoticonos...',
+ 'Could not load emoticons': 'No se han podido cargar los emoticonos',
+ People: 'Personas',
+ 'Animals and Nature': 'Animales y naturaleza',
+ 'Food and Drink': 'Comida y bebida',
+ Activity: 'Actividad',
+ 'Travel and Places': 'Viajes y lugares',
+ Objects: 'Objetos',
+ Flags: 'Banderas',
+ Characters: 'Caracteres',
+ 'Characters (no spaces)': 'Caracteres (sin espacios)',
+ '{0} characters': '{0} caracteres',
+ 'Error: Form submit field collision.': 'Error: Colisi\u00f3n de campo al enviar formulario.',
+ 'Error: No form element found.': 'Error: No se encuentra ning\u00fan elemento de formulario.',
+ Update: 'Actualizar',
+ 'Color swatch': 'Muestrario de colores',
+ Turquoise: 'Turquesa',
+ Green: 'Verde',
+ Blue: 'Azul',
+ Purple: 'P\u00farpura',
+ 'Navy Blue': 'Azul marino',
+ 'Dark Turquoise': 'Turquesa oscuro',
+ 'Dark Green': 'Verde oscuro',
+ 'Medium Blue': 'Azul medio',
+ 'Medium Purple': 'P\u00farpura medio',
+ 'Midnight Blue': 'Azul medio',
+ Yellow: 'Amarillo',
+ Orange: 'Naranja',
+ Red: 'Rojo',
+ 'Light Gray': 'Gris claro',
+ Gray: 'Gris',
+ 'Dark Yellow': 'Amarillo oscuro',
+ 'Dark Orange': 'Naranja oscuro',
+ 'Dark Red': 'Rojo oscuro',
+ 'Medium Gray': 'Gris medio',
+ 'Dark Gray': 'Gris oscuro',
+ 'Light Green': 'Verde claro',
+ 'Light Yellow': 'Amarillo claro',
+ 'Light Red': 'Rojo claro',
+ 'Light Purple': 'Morado claro',
+ 'Light Blue': 'Azul claro',
+ 'Dark Purple': 'Morado oscuro',
+ 'Dark Blue': 'Azul oscuro',
+ Black: 'Negro',
+ White: 'Blanco',
+ 'Switch to or from fullscreen mode': 'Activar o desactivar modo pantalla completa',
+ 'Open help dialog': 'Abrir di\u00e1logo de ayuda',
+ history: 'historial',
+ styles: 'estilos',
+ formatting: 'formato',
+ alignment: 'alineaci\u00f3n',
+ indentation: 'sangr\u00eda',
+ 'permanent pen': 'bol\u00edgrafo permanente',
+ comments: 'comentarios',
+ 'Format Painter': 'Copiar formato',
+ 'Insert\/edit iframe': 'Insertar\/editar iframe',
+ Capitalization: 'Uso de may\u00fasculas',
+ lowercase: 'min\u00fasculas',
+ UPPERCASE: 'MAY\u00daSCULAS',
+ 'Title Case': 'Tipo T\u00edtulo',
+ 'Permanent Pen Properties': 'Propiedades del bol\u00edgrafo permanente',
+ 'Permanent pen properties...': 'Propiedades del bol\u00edgrafo permanente...',
+ Font: 'Fuente',
+ Size: 'Tama\u00f1o',
+ 'More...': 'M\u00e1s...',
+ 'Spellcheck Language': 'Corrector',
+ 'Select...': 'Seleccionar...',
+ Preferences: 'Preferencias',
+ Yes: 'S\u00ed',
+ No: 'No',
+ 'Keyboard Navigation': 'Navegaci\u00f3n con el teclado',
+ Version: 'Versi\u00f3n',
+ Anchor: 'Ancla',
+ 'Special character': 'Car\u00e1cter especial',
+ 'Code sample': 'Ejemplo de c\u00f3digo',
+ Color: 'Color',
+ Emoticons: 'Emoticonos',
+ 'Document properties': 'Propiedades del documento',
+ Image: 'Imagen',
+ 'Insert link': 'Insertar enlace',
+ Target: 'Destino',
+ Link: 'Enlace',
+ Poster: 'Miniatura',
+ Media: 'Media',
+ Print: 'Imprimir',
+ Prev: 'Anterior',
+ 'Find and replace': 'Buscar y reemplazar',
+ 'Whole words': 'Palabras completas',
+ Spellcheck: 'Corrector ortogr\u00e1fico',
+ Caption: 'Subt\u00edtulo',
+ 'Insert template': 'Insertar plantilla'
+})
diff --git a/public/resource/tinymce/langs/zh_CN.js b/public/resource/tinymce/langs/zh_CN.js
new file mode 100644
index 0000000..f9d8b5c
--- /dev/null
+++ b/public/resource/tinymce/langs/zh_CN.js
@@ -0,0 +1,389 @@
+tinymce.addI18n('zh_CN',{
+"Redo": "\u91cd\u505a",
+"Undo": "\u64a4\u9500",
+"Cut": "\u526a\u5207",
+"Copy": "\u590d\u5236",
+"Paste": "\u7c98\u8d34",
+"Select all": "\u5168\u9009",
+"New document": "\u65b0\u6587\u4ef6",
+"Ok": "\u786e\u5b9a",
+"Cancel": "\u53d6\u6d88",
+"Visual aids": "\u7f51\u683c\u7ebf",
+"Bold": "\u7c97\u4f53",
+"Italic": "\u659c\u4f53",
+"Underline": "\u4e0b\u5212\u7ebf",
+"Strikethrough": "\u5220\u9664\u7ebf",
+"Superscript": "\u4e0a\u6807",
+"Subscript": "\u4e0b\u6807",
+"Clear formatting": "\u6e05\u9664\u683c\u5f0f",
+"Align left": "\u5de6\u8fb9\u5bf9\u9f50",
+"Align center": "\u4e2d\u95f4\u5bf9\u9f50",
+"Align right": "\u53f3\u8fb9\u5bf9\u9f50",
+"Justify": "\u4e24\u7aef\u5bf9\u9f50",
+"Bullet list": "\u9879\u76ee\u7b26\u53f7",
+"Numbered list": "\u7f16\u53f7\u5217\u8868",
+"Decrease indent": "\u51cf\u5c11\u7f29\u8fdb",
+"Increase indent": "\u589e\u52a0\u7f29\u8fdb",
+"Close": "\u5173\u95ed",
+"Formats": "\u683c\u5f0f",
+"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "\u4f60\u7684\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u6253\u5f00\u526a\u8d34\u677f\uff0c\u8bf7\u4f7f\u7528Ctrl+X\/C\/V\u7b49\u5feb\u6377\u952e\u3002",
+"Headers": "\u6807\u9898",
+"Header 1": "\u6807\u98981",
+"Header 2": "\u6807\u98982",
+"Header 3": "\u6807\u98983",
+"Header 4": "\u6807\u98984",
+"Header 5": "\u6807\u98985",
+"Header 6": "\u6807\u98986",
+"Headings": "\u6807\u9898",
+"Heading 1": "\u6807\u98981",
+"Heading 2": "\u6807\u98982",
+"Heading 3": "\u6807\u98983",
+"Heading 4": "\u6807\u98984",
+"Heading 5": "\u6807\u98985",
+"Heading 6": "\u6807\u98986",
+"Preformatted": "\u9884\u5148\u683c\u5f0f\u5316\u7684",
+"Div": "Div",
+"Pre": "Pre",
+"Code": "\u4ee3\u7801",
+"Paragraph": "\u6bb5\u843d",
+"Blockquote": "\u5f15\u6587\u533a\u5757",
+"Inline": "\u6587\u672c",
+"Blocks": "\u57fa\u5757",
+"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\u5f53\u524d\u4e3a\u7eaf\u6587\u672c\u7c98\u8d34\u6a21\u5f0f\uff0c\u518d\u6b21\u70b9\u51fb\u53ef\u4ee5\u56de\u5230\u666e\u901a\u7c98\u8d34\u6a21\u5f0f\u3002",
+"Fonts": "\u5b57\u4f53",
+"Font Sizes": "\u5b57\u53f7",
+"Class": "\u7c7b\u578b",
+"Browse for an image": "\u6d4f\u89c8\u56fe\u50cf",
+"OR": "\u6216",
+"Drop an image here": "\u62d6\u653e\u4e00\u5f20\u56fe\u50cf\u81f3\u6b64",
+"Upload": "\u4e0a\u4f20",
+"Block": "\u5757",
+"Align": "\u5bf9\u9f50",
+"Default": "\u9ed8\u8ba4",
+"Circle": "\u7a7a\u5fc3\u5706",
+"Disc": "\u5b9e\u5fc3\u5706",
+"Square": "\u65b9\u5757",
+"Lower Alpha": "\u5c0f\u5199\u82f1\u6587\u5b57\u6bcd",
+"Lower Greek": "\u5c0f\u5199\u5e0c\u814a\u5b57\u6bcd",
+"Lower Roman": "\u5c0f\u5199\u7f57\u9a6c\u5b57\u6bcd",
+"Upper Alpha": "\u5927\u5199\u82f1\u6587\u5b57\u6bcd",
+"Upper Roman": "\u5927\u5199\u7f57\u9a6c\u5b57\u6bcd",
+"Anchor...": "\u951a\u70b9...",
+"Name": "\u540d\u79f0",
+"Id": "\u6807\u8bc6\u7b26",
+"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "\u6807\u8bc6\u7b26\u5e94\u8be5\u4ee5\u5b57\u6bcd\u5f00\u5934\uff0c\u540e\u8ddf\u5b57\u6bcd\u3001\u6570\u5b57\u3001\u7834\u6298\u53f7\u3001\u70b9\u3001\u5192\u53f7\u6216\u4e0b\u5212\u7ebf\u3002",
+"You have unsaved changes are you sure you want to navigate away?": "\u4f60\u8fd8\u6709\u6587\u6863\u5c1a\u672a\u4fdd\u5b58\uff0c\u786e\u5b9a\u8981\u79bb\u5f00\uff1f",
+"Restore last draft": "\u6062\u590d\u4e0a\u6b21\u7684\u8349\u7a3f",
+"Special characters...": "\u7279\u6b8a\u5b57\u7b26...",
+"Source code": "\u6e90\u4ee3\u7801",
+"Insert\/Edit code sample": "\u63d2\u5165\/\u7f16\u8f91\u4ee3\u7801\u793a\u4f8b",
+"Language": "\u8bed\u8a00",
+"Code sample...": "\u793a\u4f8b\u4ee3\u7801...",
+"Color Picker": "\u9009\u8272\u5668",
+"R": "R",
+"G": "G",
+"B": "B",
+"Left to right": "\u4ece\u5de6\u5230\u53f3",
+"Right to left": "\u4ece\u53f3\u5230\u5de6",
+"Emoticons...": "\u8868\u60c5\u7b26\u53f7...",
+"Metadata and Document Properties": "\u5143\u6570\u636e\u548c\u6587\u6863\u5c5e\u6027",
+"Title": "\u6807\u9898",
+"Keywords": "\u5173\u952e\u8bcd",
+"Description": "\u63cf\u8ff0",
+"Robots": "\u673a\u5668\u4eba",
+"Author": "\u4f5c\u8005",
+"Encoding": "\u7f16\u7801",
+"Fullscreen": "\u5168\u5c4f",
+"Action": "\u64cd\u4f5c",
+"Shortcut": "\u5feb\u6377\u952e",
+"Help": "\u5e2e\u52a9",
+"Address": "\u5730\u5740",
+"Focus to menubar": "\u79fb\u52a8\u7126\u70b9\u5230\u83dc\u5355\u680f",
+"Focus to toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u5de5\u5177\u680f",
+"Focus to element path": "\u79fb\u52a8\u7126\u70b9\u5230\u5143\u7d20\u8def\u5f84",
+"Focus to contextual toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u4e0a\u4e0b\u6587\u83dc\u5355",
+"Insert link (if link plugin activated)": "\u63d2\u5165\u94fe\u63a5 (\u5982\u679c\u94fe\u63a5\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
+"Save (if save plugin activated)": "\u4fdd\u5b58(\u5982\u679c\u4fdd\u5b58\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
+"Find (if searchreplace plugin activated)": "\u67e5\u627e(\u5982\u679c\u67e5\u627e\u66ff\u6362\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
+"Plugins installed ({0}):": "\u5df2\u5b89\u88c5\u63d2\u4ef6 ({0}):",
+"Premium plugins:": "\u4f18\u79c0\u63d2\u4ef6\uff1a",
+"Learn more...": "\u4e86\u89e3\u66f4\u591a...",
+"You are using {0}": "\u4f60\u6b63\u5728\u4f7f\u7528 {0}",
+"Plugins": "\u63d2\u4ef6",
+"Handy Shortcuts": "\u5feb\u6377\u952e",
+"Horizontal line": "\u6c34\u5e73\u5206\u5272\u7ebf",
+"Insert\/edit image": "\u63d2\u5165\/\u7f16\u8f91\u56fe\u7247",
+"Image description": "\u56fe\u7247\u63cf\u8ff0",
+"Source": "\u5730\u5740",
+"Dimensions": "\u5927\u5c0f",
+"Constrain proportions": "\u4fdd\u6301\u7eb5\u6a2a\u6bd4",
+"General": "\u666e\u901a",
+"Advanced": "\u9ad8\u7ea7",
+"Style": "\u6837\u5f0f",
+"Vertical space": "\u5782\u76f4\u8fb9\u8ddd",
+"Horizontal space": "\u6c34\u5e73\u8fb9\u8ddd",
+"Border": "\u8fb9\u6846",
+"Insert image": "\u63d2\u5165\u56fe\u7247",
+"Image...": "\u56fe\u7247...",
+"Image list": "\u56fe\u7247\u5217\u8868",
+"Rotate counterclockwise": "\u9006\u65f6\u9488\u65cb\u8f6c",
+"Rotate clockwise": "\u987a\u65f6\u9488\u65cb\u8f6c",
+"Flip vertically": "\u5782\u76f4\u7ffb\u8f6c",
+"Flip horizontally": "\u6c34\u5e73\u7ffb\u8f6c",
+"Edit image": "\u7f16\u8f91\u56fe\u7247",
+"Image options": "\u56fe\u7247\u9009\u9879",
+"Zoom in": "\u653e\u5927",
+"Zoom out": "\u7f29\u5c0f",
+"Crop": "\u88c1\u526a",
+"Resize": "\u8c03\u6574\u5927\u5c0f",
+"Orientation": "\u65b9\u5411",
+"Brightness": "\u4eae\u5ea6",
+"Sharpen": "\u9510\u5316",
+"Contrast": "\u5bf9\u6bd4\u5ea6",
+"Color levels": "\u989c\u8272\u5c42\u6b21",
+"Gamma": "\u4f3d\u9a6c\u503c",
+"Invert": "\u53cd\u8f6c",
+"Apply": "\u5e94\u7528",
+"Back": "\u540e\u9000",
+"Insert date\/time": "\u63d2\u5165\u65e5\u671f\/\u65f6\u95f4",
+"Date\/time": "\u65e5\u671f\/\u65f6\u95f4",
+"Insert\/Edit Link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5",
+"Insert\/edit link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5",
+"Text to display": "\u663e\u793a\u6587\u5b57",
+"Url": "\u5730\u5740",
+"Open link in...": "\u94fe\u63a5\u6253\u5f00\u4f4d\u7f6e...",
+"Current window": "\u5f53\u524d\u7a97\u53e3",
+"None": "\u65e0",
+"New window": "\u5728\u65b0\u7a97\u53e3\u6253\u5f00",
+"Remove link": "\u5220\u9664\u94fe\u63a5",
+"Anchors": "\u951a\u70b9",
+"Link...": "\u94fe\u63a5...",
+"Paste or type a link": "\u7c98\u8d34\u6216\u8f93\u5165\u94fe\u63a5",
+"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u4e3a\u90ae\u4ef6\u5730\u5740\uff0c\u9700\u8981\u52a0\u4e0amailto:\u524d\u7f00\u5417\uff1f",
+"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u5c5e\u4e8e\u5916\u90e8\u94fe\u63a5\uff0c\u9700\u8981\u52a0\u4e0ahttp:\/\/:\u524d\u7f00\u5417\uff1f",
+"Link list": "\u94fe\u63a5\u5217\u8868",
+"Insert video": "\u63d2\u5165\u89c6\u9891",
+"Insert\/edit video": "\u63d2\u5165\/\u7f16\u8f91\u89c6\u9891",
+"Insert\/edit media": "\u63d2\u5165\/\u7f16\u8f91\u5a92\u4f53",
+"Alternative source": "\u955c\u50cf",
+"Alternative source URL": "\u66ff\u4ee3\u6765\u6e90\u7f51\u5740",
+"Media poster (Image URL)": "\u5c01\u9762(\u56fe\u7247\u5730\u5740)",
+"Paste your embed code below:": "\u5c06\u5185\u5d4c\u4ee3\u7801\u7c98\u8d34\u5728\u4e0b\u9762:",
+"Embed": "\u5185\u5d4c",
+"Media...": "\u591a\u5a92\u4f53...",
+"Nonbreaking space": "\u4e0d\u95f4\u65ad\u7a7a\u683c",
+"Page break": "\u5206\u9875\u7b26",
+"Paste as text": "\u7c98\u8d34\u4e3a\u6587\u672c",
+"Preview": "\u9884\u89c8",
+"Print...": "\u6253\u5370...",
+"Save": "\u4fdd\u5b58",
+"Find": "\u67e5\u627e",
+"Replace with": "\u66ff\u6362\u4e3a",
+"Replace": "\u66ff\u6362",
+"Replace all": "\u5168\u90e8\u66ff\u6362",
+"Previous": "\u4e0a\u4e00\u4e2a",
+"Next": "\u4e0b\u4e00\u4e2a",
+"Find and replace...": "\u67e5\u627e\u5e76\u66ff\u6362...",
+"Could not find the specified string.": "\u672a\u627e\u5230\u641c\u7d22\u5185\u5bb9.",
+"Match case": "\u533a\u5206\u5927\u5c0f\u5199",
+"Find whole words only": "\u5168\u5b57\u5339\u914d",
+"Spell check": "\u62fc\u5199\u68c0\u67e5",
+"Ignore": "\u5ffd\u7565",
+"Ignore all": "\u5168\u90e8\u5ffd\u7565",
+"Finish": "\u5b8c\u6210",
+"Add to Dictionary": "\u6dfb\u52a0\u5230\u5b57\u5178",
+"Insert table": "\u63d2\u5165\u8868\u683c",
+"Table properties": "\u8868\u683c\u5c5e\u6027",
+"Delete table": "\u5220\u9664\u8868\u683c",
+"Cell": "\u5355\u5143\u683c",
+"Row": "\u884c",
+"Column": "\u5217",
+"Cell properties": "\u5355\u5143\u683c\u5c5e\u6027",
+"Merge cells": "\u5408\u5e76\u5355\u5143\u683c",
+"Split cell": "\u62c6\u5206\u5355\u5143\u683c",
+"Insert row before": "\u5728\u4e0a\u65b9\u63d2\u5165",
+"Insert row after": "\u5728\u4e0b\u65b9\u63d2\u5165",
+"Delete row": "\u5220\u9664\u884c",
+"Row properties": "\u884c\u5c5e\u6027",
+"Cut row": "\u526a\u5207\u884c",
+"Copy row": "\u590d\u5236\u884c",
+"Paste row before": "\u7c98\u8d34\u5230\u4e0a\u65b9",
+"Paste row after": "\u7c98\u8d34\u5230\u4e0b\u65b9",
+"Insert column before": "\u5728\u5de6\u4fa7\u63d2\u5165",
+"Insert column after": "\u5728\u53f3\u4fa7\u63d2\u5165",
+"Delete column": "\u5220\u9664\u5217",
+"Cols": "\u5217",
+"Rows": "\u884c",
+"Width": "\u5bbd",
+"Height": "\u9ad8",
+"Cell spacing": "\u5355\u5143\u683c\u5916\u95f4\u8ddd",
+"Cell padding": "\u5355\u5143\u683c\u5185\u8fb9\u8ddd",
+"Show caption": "\u663e\u793a\u6807\u9898",
+"Left": "\u5de6\u5bf9\u9f50",
+"Center": "\u5c45\u4e2d",
+"Right": "\u53f3\u5bf9\u9f50",
+"Cell type": "\u5355\u5143\u683c\u7c7b\u578b",
+"Scope": "\u8303\u56f4",
+"Alignment": "\u5bf9\u9f50\u65b9\u5f0f",
+"H Align": "\u6c34\u5e73\u5bf9\u9f50",
+"V Align": "\u5782\u76f4\u5bf9\u9f50",
+"Top": "\u9876\u90e8\u5bf9\u9f50",
+"Middle": "\u5782\u76f4\u5c45\u4e2d",
+"Bottom": "\u5e95\u90e8\u5bf9\u9f50",
+"Header cell": "\u8868\u5934\u5355\u5143\u683c",
+"Row group": "\u884c\u7ec4",
+"Column group": "\u5217\u7ec4",
+"Row type": "\u884c\u7c7b\u578b",
+"Header": "\u8868\u5934",
+"Body": "\u8868\u4f53",
+"Footer": "\u8868\u5c3e",
+"Border color": "\u8fb9\u6846\u989c\u8272",
+"Insert template...": "\u63d2\u5165\u6a21\u677f...",
+"Templates": "\u6a21\u677f",
+"Template": "\u6a21\u677f",
+"Text color": "\u6587\u5b57\u989c\u8272",
+"Background color": "\u80cc\u666f\u8272",
+"Custom...": "\u81ea\u5b9a\u4e49...",
+"Custom color": "\u81ea\u5b9a\u4e49\u989c\u8272",
+"No color": "\u65e0",
+"Remove color": "\u79fb\u9664\u989c\u8272",
+"Table of Contents": "\u5185\u5bb9\u5217\u8868",
+"Show blocks": "\u663e\u793a\u533a\u5757\u8fb9\u6846",
+"Show invisible characters": "\u663e\u793a\u4e0d\u53ef\u89c1\u5b57\u7b26",
+"Word count": "\u5b57\u6570",
+"Words: {0}": "\u5b57\u6570\uff1a{0}",
+"{0} words": "{0} \u5b57",
+"File": "\u6587\u4ef6",
+"Edit": "\u7f16\u8f91",
+"Insert": "\u63d2\u5165",
+"View": "\u89c6\u56fe",
+"Format": "\u683c\u5f0f",
+"Table": "\u8868\u683c",
+"Tools": "\u5de5\u5177",
+"Powered by {0}": "\u7531{0}\u9a71\u52a8",
+"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u5728\u7f16\u8f91\u533a\u6309ALT-F9\u6253\u5f00\u83dc\u5355\uff0c\u6309ALT-F10\u6253\u5f00\u5de5\u5177\u680f\uff0c\u6309ALT-0\u67e5\u770b\u5e2e\u52a9",
+"Image title": "\u56fe\u7247\u6807\u9898",
+"Border width": "\u8fb9\u6846\u5bbd\u5ea6",
+"Border style": "\u8fb9\u6846\u6837\u5f0f",
+"Error": "\u9519\u8bef",
+"Warn": "\u8b66\u544a",
+"Valid": "\u6709\u6548",
+"To open the popup, press Shift+Enter": "\u6309Shitf+Enter\u952e\u6253\u5f00\u5bf9\u8bdd\u6846",
+"Rich Text Area. Press ALT-0 for help.": "\u7f16\u8f91\u533a\u3002\u6309Alt+0\u952e\u6253\u5f00\u5e2e\u52a9\u3002",
+"System Font": "\u7cfb\u7edf\u5b57\u4f53",
+"Failed to upload image: {0}": "\u56fe\u7247\u4e0a\u4f20\u5931\u8d25: {0}",
+"Failed to load plugin: {0} from url {1}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25: {0} \u6765\u81ea\u94fe\u63a5 {1}",
+"Failed to load plugin url: {0}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25 \u94fe\u63a5: {0}",
+"Failed to initialize plugin: {0}": "\u63d2\u4ef6\u521d\u59cb\u5316\u5931\u8d25: {0}",
+"example": "\u793a\u4f8b",
+"Search": "\u641c\u7d22",
+"All": "\u5168\u90e8",
+"Currency": "\u8d27\u5e01",
+"Text": "\u6587\u5b57",
+"Quotations": "\u5f15\u7528",
+"Mathematical": "\u6570\u5b66",
+"Extended Latin": "\u62c9\u4e01\u8bed\u6269\u5145",
+"Symbols": "\u7b26\u53f7",
+"Arrows": "\u7bad\u5934",
+"User Defined": "\u81ea\u5b9a\u4e49",
+"dollar sign": "\u7f8e\u5143\u7b26\u53f7",
+"currency sign": "\u8d27\u5e01\u7b26\u53f7",
+"euro-currency sign": "\u6b27\u5143\u7b26\u53f7",
+"colon sign": "\u5192\u53f7",
+"cruzeiro sign": "\u514b\u9c81\u8d5b\u7f57\u5e01\u7b26\u53f7",
+"french franc sign": "\u6cd5\u90ce\u7b26\u53f7",
+"lira sign": "\u91cc\u62c9\u7b26\u53f7",
+"mill sign": "\u5bc6\u5c14\u7b26\u53f7",
+"naira sign": "\u5948\u62c9\u7b26\u53f7",
+"peseta sign": "\u6bd4\u585e\u5854\u7b26\u53f7",
+"rupee sign": "\u5362\u6bd4\u7b26\u53f7",
+"won sign": "\u97e9\u5143\u7b26\u53f7",
+"new sheqel sign": "\u65b0\u8c22\u514b\u5c14\u7b26\u53f7",
+"dong sign": "\u8d8a\u5357\u76fe\u7b26\u53f7",
+"kip sign": "\u8001\u631d\u57fa\u666e\u7b26\u53f7",
+"tugrik sign": "\u56fe\u683c\u91cc\u514b\u7b26\u53f7",
+"drachma sign": "\u5fb7\u62c9\u514b\u9a6c\u7b26\u53f7",
+"german penny symbol": "\u5fb7\u56fd\u4fbf\u58eb\u7b26\u53f7",
+"peso sign": "\u6bd4\u7d22\u7b26\u53f7",
+"guarani sign": "\u74dc\u62c9\u5c3c\u7b26\u53f7",
+"austral sign": "\u6fb3\u5143\u7b26\u53f7",
+"hryvnia sign": "\u683c\u91cc\u592b\u5c3c\u4e9a\u7b26\u53f7",
+"cedi sign": "\u585e\u5730\u7b26\u53f7",
+"livre tournois sign": "\u91cc\u5f17\u5f17\u5c14\u7b26\u53f7",
+"spesmilo sign": "spesmilo\u7b26\u53f7",
+"tenge sign": "\u575a\u6208\u7b26\u53f7",
+"indian rupee sign": "\u5370\u5ea6\u5362\u6bd4",
+"turkish lira sign": "\u571f\u8033\u5176\u91cc\u62c9",
+"nordic mark sign": "\u5317\u6b27\u9a6c\u514b",
+"manat sign": "\u9a6c\u7eb3\u7279\u7b26\u53f7",
+"ruble sign": "\u5362\u5e03\u7b26\u53f7",
+"yen character": "\u65e5\u5143\u5b57\u6837",
+"yuan character": "\u4eba\u6c11\u5e01\u5143\u5b57\u6837",
+"yuan character, in hong kong and taiwan": "\u5143\u5b57\u6837\uff08\u6e2f\u53f0\u5730\u533a\uff09",
+"yen\/yuan character variant one": "\u5143\u5b57\u6837\uff08\u5927\u5199\uff09",
+"Loading emoticons...": "\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7...",
+"Could not load emoticons": "\u4e0d\u80fd\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7",
+"People": "\u4eba\u7c7b",
+"Animals and Nature": "\u52a8\u7269\u548c\u81ea\u7136",
+"Food and Drink": "\u98df\u7269\u548c\u996e\u54c1",
+"Activity": "\u6d3b\u52a8",
+"Travel and Places": "\u65c5\u6e38\u548c\u5730\u70b9",
+"Objects": "\u7269\u4ef6",
+"Flags": "\u65d7\u5e1c",
+"Characters": "\u5b57\u7b26",
+"Characters (no spaces)": "\u5b57\u7b26(\u65e0\u7a7a\u683c)",
+"Error: Form submit field collision.": "\u9519\u8bef: \u8868\u5355\u63d0\u4ea4\u5b57\u6bb5\u51b2\u7a81\u3002",
+"Error: No form element found.": "\u9519\u8bef: \u6ca1\u6709\u8868\u5355\u63a7\u4ef6\u3002",
+"Update": "\u66f4\u65b0",
+"Color swatch": "\u989c\u8272\u6837\u672c",
+"Turquoise": "\u9752\u7eff\u8272",
+"Green": "\u7eff\u8272",
+"Blue": "\u84dd\u8272",
+"Purple": "\u7d2b\u8272",
+"Navy Blue": "\u6d77\u519b\u84dd",
+"Dark Turquoise": "\u6df1\u84dd\u7eff\u8272",
+"Dark Green": "\u6df1\u7eff\u8272",
+"Medium Blue": "\u4e2d\u84dd\u8272",
+"Medium Purple": "\u4e2d\u7d2b\u8272",
+"Midnight Blue": "\u6df1\u84dd\u8272",
+"Yellow": "\u9ec4\u8272",
+"Orange": "\u6a59\u8272",
+"Red": "\u7ea2\u8272",
+"Light Gray": "\u6d45\u7070\u8272",
+"Gray": "\u7070\u8272",
+"Dark Yellow": "\u6697\u9ec4\u8272",
+"Dark Orange": "\u6df1\u6a59\u8272",
+"Dark Red": "\u6df1\u7ea2\u8272",
+"Medium Gray": "\u4e2d\u7070\u8272",
+"Dark Gray": "\u6df1\u7070\u8272",
+"Black": "\u9ed1\u8272",
+"White": "\u767d\u8272",
+"Switch to or from fullscreen mode": "\u5207\u6362\u5168\u5c4f\u6a21\u5f0f",
+"Open help dialog": "\u6253\u5f00\u5e2e\u52a9\u5bf9\u8bdd\u6846",
+"history": "\u5386\u53f2",
+"styles": "\u6837\u5f0f",
+"formatting": "\u683c\u5f0f\u5316",
+"alignment": "\u5bf9\u9f50",
+"indentation": "\u7f29\u8fdb",
+"permanent pen": "\u8bb0\u53f7\u7b14",
+"comments": "\u5907\u6ce8",
+"Anchor": "\u951a\u70b9",
+"Special character": "\u7279\u6b8a\u7b26\u53f7",
+"Code sample": "\u4ee3\u7801\u793a\u4f8b",
+"Color": "\u989c\u8272",
+"Emoticons": "\u8868\u60c5",
+"Document properties": "\u6587\u6863\u5c5e\u6027",
+"Image": "\u56fe\u7247",
+"Insert link": "\u63d2\u5165\u94fe\u63a5",
+"Target": "\u6253\u5f00\u65b9\u5f0f",
+"Link": "\u94fe\u63a5",
+"Poster": "\u5c01\u9762",
+"Media": "\u5a92\u4f53",
+"Print": "\u6253\u5370",
+"Prev": "\u4e0a\u4e00\u4e2a",
+"Find and replace": "\u67e5\u627e\u548c\u66ff\u6362",
+"Whole words": "\u5168\u5b57\u5339\u914d",
+"Spellcheck": "\u62fc\u5199\u68c0\u67e5",
+"Caption": "\u6807\u9898",
+"Insert template": "\u63d2\u5165\u6a21\u677f"
+});
\ No newline at end of file
diff --git a/public/resource/tinymce/skins/ui/oxide-dark/content.inline.min.css b/public/resource/tinymce/skins/ui/oxide-dark/content.inline.min.css
new file mode 100644
index 0000000..b4ab9a3
--- /dev/null
+++ b/public/resource/tinymce/skins/ui/oxide-dark/content.inline.min.css
@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+.mce-content-body .mce-item-anchor{background:transparent url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'8'%20height%3D'12'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20d%3D'M0%200L8%200%208%2012%204.09117821%209%200%2012z'%2F%3E%3C%2Fsvg%3E%0A") no-repeat center;cursor:default;display:inline-block;height:12px!important;padding:0 2px;-webkit-user-modify:read-only;-moz-user-modify:read-only;-webkit-user-select:all;-moz-user-select:all;-ms-user-select:all;user-select:all;width:8px!important}.mce-content-body .mce-item-anchor[data-mce-selected]{outline-offset:1px}.tox-comments-visible .tox-comment{background-color:#fff0b7}.tox-comments-visible .tox-comment--active{background-color:#ffe168}.tox-checklist>li:not(.tox-checklist--hidden){list-style:none;margin:.25em 0}.tox-checklist>li:not(.tox-checklist--hidden)::before{content:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-unchecked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2215%22%20height%3D%2215%22%20x%3D%22.5%22%20y%3D%22.5%22%20fill-rule%3D%22nonzero%22%20stroke%3D%22%234C4C4C%22%20rx%3D%222%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A");cursor:pointer;height:1em;margin-left:-1.5em;margin-top:.125em;position:absolute;width:1em}.tox-checklist li:not(.tox-checklist--hidden).tox-checklist--checked::before{content:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-checked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%234099FF%22%20fill-rule%3D%22nonzero%22%20rx%3D%222%22%2F%3E%3Cpath%20id%3D%22Path%22%20fill%3D%22%23FFF%22%20fill-rule%3D%22nonzero%22%20d%3D%22M11.5703186%2C3.14417309%20C11.8516238%2C2.73724603%2012.4164781%2C2.62829933%2012.83558%2C2.89774797%20C13.260121%2C3.17069355%2013.3759736%2C3.72932262%2013.0909105%2C4.14168582%20L7.7580587%2C11.8560195%20C7.43776896%2C12.3193404%206.76483983%2C12.3852142%206.35607322%2C11.9948725%20L3.02491697%2C8.8138662%20C2.66090143%2C8.46625845%202.65798871%2C7.89594698%203.01850234%2C7.54483354%20C3.373942%2C7.19866177%203.94940006%2C7.19592841%204.30829608%2C7.5386474%20L6.85276923%2C9.9684299%20L11.5703186%2C3.14417309%20Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A")}[dir=rtl] .tox-checklist>li:not(.tox-checklist--hidden)::before{margin-left:0;margin-right:-1.5em}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;tab-size:4;-webkit-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.mce-content-body{overflow-wrap:break-word;word-wrap:break-word}.mce-content-body .mce-visual-caret{background-color:#000;background-color:currentColor;position:absolute}.mce-content-body .mce-visual-caret-hidden{display:none}.mce-content-body [data-mce-caret]{left:-1000px;margin:0;padding:0;position:absolute;right:auto;top:0}.mce-content-body .mce-offscreen-selection{left:-2000000px;max-width:1000000px;position:absolute}.mce-content-body [contentEditable=false]{cursor:default}.mce-content-body [contentEditable=true]{cursor:text}.tox-cursor-format-painter{cursor:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M15%2C6%20C15%2C5.45%2014.55%2C5%2014%2C5%20L6%2C5%20C5.45%2C5%205%2C5.45%205%2C6%20L5%2C10%20C5%2C10.55%205.45%2C11%206%2C11%20L14%2C11%20C14.55%2C11%2015%2C10.55%2015%2C10%20L15%2C9%20L16%2C9%20L16%2C12%20L9%2C12%20L9%2C19%20C9%2C19.55%209.45%2C20%2010%2C20%20L11%2C20%20C11.55%2C20%2012%2C19.55%2012%2C19%20L12%2C14%20L18%2C14%20L18%2C7%20L15%2C7%20L15%2C6%20Z%22%2F%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M1%2C1%20L8.25%2C1%20C8.66421356%2C1%209%2C1.33578644%209%2C1.75%20L9%2C1.75%20C9%2C2.16421356%208.66421356%2C2.5%208.25%2C2.5%20L2.5%2C2.5%20L2.5%2C8.25%20C2.5%2C8.66421356%202.16421356%2C9%201.75%2C9%20L1.75%2C9%20C1.33578644%2C9%201%2C8.66421356%201%2C8.25%20L1%2C1%20Z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A"),default}.mce-content-body figure.align-left{float:left}.mce-content-body figure.align-right{float:right}.mce-content-body figure.image.align-center{display:table;margin-left:auto;margin-right:auto}.mce-preview-object{border:1px solid gray;display:inline-block;line-height:0;margin:0 2px 0 2px;position:relative}.mce-preview-object .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.mce-preview-object[data-mce-selected="2"] .mce-shim{display:none}.mce-object{background:transparent url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%203h16a1%201%200%200%201%201%201v16a1%201%200%200%201-1%201H4a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1zm1%202v14h14V5H5zm4.79%202.565l5.64%204.028a.5.5%200%200%201%200%20.814l-5.64%204.028a.5.5%200%200%201-.79-.407V7.972a.5.5%200%200%201%20.79-.407z%22%2F%3E%3C%2Fsvg%3E%0A") no-repeat center;border:1px dashed #aaa}.mce-pagebreak{border:1px dashed #aaa;cursor:default;display:block;height:5px;margin-top:15px;page-break-before:always;width:100%}@media print{.mce-pagebreak{border:0}}.tiny-pageembed .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.tiny-pageembed[data-mce-selected="2"] .mce-shim{display:none}.tiny-pageembed{display:inline-block;position:relative}.tiny-pageembed--16by9,.tiny-pageembed--1by1,.tiny-pageembed--21by9,.tiny-pageembed--4by3{display:block;overflow:hidden;padding:0;position:relative;width:100%}.tiny-pageembed--21by9{padding-top:42.857143%}.tiny-pageembed--16by9{padding-top:56.25%}.tiny-pageembed--4by3{padding-top:75%}.tiny-pageembed--1by1{padding-top:100%}.tiny-pageembed--16by9 iframe,.tiny-pageembed--1by1 iframe,.tiny-pageembed--21by9 iframe,.tiny-pageembed--4by3 iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.mce-content-body[data-mce-placeholder]{position:relative}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:rgba(34,47,62,.7);content:attr(data-mce-placeholder);position:absolute}.mce-content-body:not([dir=rtl])[data-mce-placeholder]:not(.mce-visualblocks)::before{left:1px}.mce-content-body[dir=rtl][data-mce-placeholder]:not(.mce-visualblocks)::before{right:1px}.mce-content-body div.mce-resizehandle{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;height:10px;position:absolute;width:10px;z-index:1298}.mce-content-body div.mce-resizehandle:hover{background-color:#4099ff}.mce-content-body div.mce-resizehandle:nth-of-type(1){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(2){cursor:nesw-resize}.mce-content-body div.mce-resizehandle:nth-of-type(3){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(4){cursor:nesw-resize}.mce-content-body .mce-resize-backdrop{z-index:10000}.mce-content-body .mce-clonedresizable{cursor:default;opacity:.5;outline:1px dashed #000;position:absolute;z-index:10001}.mce-content-body .mce-clonedresizable.mce-resizetable-columns td,.mce-content-body .mce-clonedresizable.mce-resizetable-columns th{border:0}.mce-content-body .mce-resize-helper{background:#555;background:rgba(0,0,0,.75);border:1px;border-radius:3px;color:#fff;display:none;font-family:sans-serif;font-size:12px;line-height:14px;margin:5px 10px;padding:5px;position:absolute;white-space:nowrap;z-index:10002}.tox-rtc-user-selection{position:relative}.tox-rtc-user-cursor{bottom:0;cursor:default;position:absolute;top:0;width:2px}.tox-rtc-user-cursor::before{background-color:inherit;border-radius:50%;content:'';display:block;height:8px;position:absolute;right:-3px;top:-3px;width:8px}.tox-rtc-user-cursor:hover::after{background-color:inherit;border-radius:100px;box-sizing:border-box;color:#fff;content:attr(data-user);display:block;font-size:12px;font-weight:700;left:-5px;min-height:8px;min-width:8px;padding:0 12px;position:absolute;top:-11px;white-space:nowrap;z-index:1000}.tox-rtc-user-selection--1 .tox-rtc-user-cursor{background-color:#2dc26b}.tox-rtc-user-selection--2 .tox-rtc-user-cursor{background-color:#e03e2d}.tox-rtc-user-selection--3 .tox-rtc-user-cursor{background-color:#f1c40f}.tox-rtc-user-selection--4 .tox-rtc-user-cursor{background-color:#3598db}.tox-rtc-user-selection--5 .tox-rtc-user-cursor{background-color:#b96ad9}.tox-rtc-user-selection--6 .tox-rtc-user-cursor{background-color:#e67e23}.tox-rtc-user-selection--7 .tox-rtc-user-cursor{background-color:#aaa69d}.tox-rtc-user-selection--8 .tox-rtc-user-cursor{background-color:#f368e0}.tox-rtc-remote-image{background:#eaeaea url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2236%22%20height%3D%2212%22%20viewBox%3D%220%200%2036%2012%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Ccircle%20cx%3D%226%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2218%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.33s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2230%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.66s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A") no-repeat center center;border:1px solid #ccc;min-height:240px;min-width:320px}.mce-match-marker{background:#aaa;color:#fff}.mce-match-marker-selected{background:#39f;color:#fff}.mce-match-marker-selected::-moz-selection{background:#39f;color:#fff}.mce-match-marker-selected::selection{background:#39f;color:#fff}.mce-content-body audio[data-mce-selected],.mce-content-body embed[data-mce-selected],.mce-content-body img[data-mce-selected],.mce-content-body object[data-mce-selected],.mce-content-body table[data-mce-selected],.mce-content-body video[data-mce-selected]{outline:3px solid #b4d7ff}.mce-content-body hr[data-mce-selected]{outline:3px solid #b4d7ff;outline-offset:1px}.mce-content-body [contentEditable=false] [contentEditable=true]:focus{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false] [contentEditable=true]:hover{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false][data-mce-selected]{cursor:not-allowed;outline:3px solid #b4d7ff}.mce-content-body.mce-content-readonly [contentEditable=true]:focus,.mce-content-body.mce-content-readonly [contentEditable=true]:hover{outline:0}.mce-content-body [data-mce-selected=inline-boundary]{background-color:#b4d7ff}.mce-content-body .mce-edit-focus{outline:3px solid #b4d7ff}.mce-content-body td[data-mce-selected],.mce-content-body th[data-mce-selected]{position:relative}.mce-content-body td[data-mce-selected]::-moz-selection,.mce-content-body th[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body td[data-mce-selected]::selection,.mce-content-body th[data-mce-selected]::selection{background:0 0}.mce-content-body td[data-mce-selected] *,.mce-content-body th[data-mce-selected] *{outline:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{background-color:rgba(180,215,255,.7);border:1px solid rgba(180,215,255,.7);bottom:-1px;content:'';left:-1px;mix-blend-mode:multiply;position:absolute;right:-1px;top:-1px}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{border-color:rgba(0,84,180,.7)}}.mce-content-body img::-moz-selection{background:0 0}.mce-content-body img::selection{background:0 0}.ephox-snooker-resizer-bar{background-color:#b4d7ff;opacity:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:1}.mce-spellchecker-word{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%23ff0000'%20fill%3D'none'%20stroke-linecap%3D'round'%20stroke-opacity%3D'.75'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default;height:2rem}.mce-spellchecker-grammar{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%2300A835'%20fill%3D'none'%20stroke-linecap%3D'round'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc li{list-style-type:none}.mce-item-table:not([border]),.mce-item-table:not([border]) caption,.mce-item-table:not([border]) td,.mce-item-table:not([border]) th,.mce-item-table[border="0"],.mce-item-table[border="0"] caption,.mce-item-table[border="0"] td,.mce-item-table[border="0"] th,table[style*="border-width: 0px"],table[style*="border-width: 0px"] caption,table[style*="border-width: 0px"] td,table[style*="border-width: 0px"] th{border:1px dashed #bbb}.mce-visualblocks address,.mce-visualblocks article,.mce-visualblocks aside,.mce-visualblocks blockquote,.mce-visualblocks div:not([data-mce-bogus]),.mce-visualblocks dl,.mce-visualblocks figcaption,.mce-visualblocks figure,.mce-visualblocks h1,.mce-visualblocks h2,.mce-visualblocks h3,.mce-visualblocks h4,.mce-visualblocks h5,.mce-visualblocks h6,.mce-visualblocks hgroup,.mce-visualblocks ol,.mce-visualblocks p,.mce-visualblocks pre,.mce-visualblocks section,.mce-visualblocks ul{background-repeat:no-repeat;border:1px dashed #bbb;margin-left:3px;padding-top:10px}.mce-visualblocks p{background-image:url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7)}.mce-visualblocks h1{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==)}.mce-visualblocks h2{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==)}.mce-visualblocks h3{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7)}.mce-visualblocks h4{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==)}.mce-visualblocks h5{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==)}.mce-visualblocks h6{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==)}.mce-visualblocks div:not([data-mce-bogus]){background-image:url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7)}.mce-visualblocks section{background-image:url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=)}.mce-visualblocks article{background-image:url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7)}.mce-visualblocks blockquote{background-image:url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7)}.mce-visualblocks address{background-image:url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=)}.mce-visualblocks pre{background-image:url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==)}.mce-visualblocks figure{background-image:url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7)}.mce-visualblocks figcaption{border:1px dashed #bbb}.mce-visualblocks hgroup{background-image:url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7)}.mce-visualblocks aside{background-image:url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=)}.mce-visualblocks ul{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==)}.mce-visualblocks ol{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==)}.mce-visualblocks dl{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==)}.mce-visualblocks:not([dir=rtl]) address,.mce-visualblocks:not([dir=rtl]) article,.mce-visualblocks:not([dir=rtl]) aside,.mce-visualblocks:not([dir=rtl]) blockquote,.mce-visualblocks:not([dir=rtl]) div:not([data-mce-bogus]),.mce-visualblocks:not([dir=rtl]) dl,.mce-visualblocks:not([dir=rtl]) figcaption,.mce-visualblocks:not([dir=rtl]) figure,.mce-visualblocks:not([dir=rtl]) h1,.mce-visualblocks:not([dir=rtl]) h2,.mce-visualblocks:not([dir=rtl]) h3,.mce-visualblocks:not([dir=rtl]) h4,.mce-visualblocks:not([dir=rtl]) h5,.mce-visualblocks:not([dir=rtl]) h6,.mce-visualblocks:not([dir=rtl]) hgroup,.mce-visualblocks:not([dir=rtl]) ol,.mce-visualblocks:not([dir=rtl]) p,.mce-visualblocks:not([dir=rtl]) pre,.mce-visualblocks:not([dir=rtl]) section,.mce-visualblocks:not([dir=rtl]) ul{margin-left:3px}.mce-visualblocks[dir=rtl] address,.mce-visualblocks[dir=rtl] article,.mce-visualblocks[dir=rtl] aside,.mce-visualblocks[dir=rtl] blockquote,.mce-visualblocks[dir=rtl] div:not([data-mce-bogus]),.mce-visualblocks[dir=rtl] dl,.mce-visualblocks[dir=rtl] figcaption,.mce-visualblocks[dir=rtl] figure,.mce-visualblocks[dir=rtl] h1,.mce-visualblocks[dir=rtl] h2,.mce-visualblocks[dir=rtl] h3,.mce-visualblocks[dir=rtl] h4,.mce-visualblocks[dir=rtl] h5,.mce-visualblocks[dir=rtl] h6,.mce-visualblocks[dir=rtl] hgroup,.mce-visualblocks[dir=rtl] ol,.mce-visualblocks[dir=rtl] p,.mce-visualblocks[dir=rtl] pre,.mce-visualblocks[dir=rtl] section,.mce-visualblocks[dir=rtl] ul{background-position-x:right;margin-right:3px}.mce-nbsp,.mce-shy{background:#aaa}.mce-shy::after{content:'-'}
diff --git a/public/resource/tinymce/skins/ui/oxide-dark/content.min.css b/public/resource/tinymce/skins/ui/oxide-dark/content.min.css
new file mode 100644
index 0000000..e27b8a0
--- /dev/null
+++ b/public/resource/tinymce/skins/ui/oxide-dark/content.min.css
@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+.mce-content-body .mce-item-anchor{background:transparent url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'8'%20height%3D'12'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20d%3D'M0%200L8%200%208%2012%204.09117821%209%200%2012z'%20fill%3D%22%23cccccc%22%2F%3E%3C%2Fsvg%3E%0A") no-repeat center;cursor:default;display:inline-block;height:12px!important;padding:0 2px;-webkit-user-modify:read-only;-moz-user-modify:read-only;-webkit-user-select:all;-moz-user-select:all;-ms-user-select:all;user-select:all;width:8px!important}.mce-content-body .mce-item-anchor[data-mce-selected]{outline-offset:1px}.tox-comments-visible .tox-comment{background-color:#fff0b7}.tox-comments-visible .tox-comment--active{background-color:#ffe168}.tox-checklist>li:not(.tox-checklist--hidden){list-style:none;margin:.25em 0}.tox-checklist>li:not(.tox-checklist--hidden)::before{content:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-unchecked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2215%22%20height%3D%2215%22%20x%3D%22.5%22%20y%3D%22.5%22%20fill-rule%3D%22nonzero%22%20stroke%3D%22%236d737b%22%20rx%3D%222%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A");cursor:pointer;height:1em;margin-left:-1.5em;margin-top:.125em;position:absolute;width:1em}.tox-checklist li:not(.tox-checklist--hidden).tox-checklist--checked::before{content:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-checked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%234099FF%22%20fill-rule%3D%22nonzero%22%20rx%3D%222%22%2F%3E%3Cpath%20id%3D%22Path%22%20fill%3D%22%23FFF%22%20fill-rule%3D%22nonzero%22%20d%3D%22M11.5703186%2C3.14417309%20C11.8516238%2C2.73724603%2012.4164781%2C2.62829933%2012.83558%2C2.89774797%20C13.260121%2C3.17069355%2013.3759736%2C3.72932262%2013.0909105%2C4.14168582%20L7.7580587%2C11.8560195%20C7.43776896%2C12.3193404%206.76483983%2C12.3852142%206.35607322%2C11.9948725%20L3.02491697%2C8.8138662%20C2.66090143%2C8.46625845%202.65798871%2C7.89594698%203.01850234%2C7.54483354%20C3.373942%2C7.19866177%203.94940006%2C7.19592841%204.30829608%2C7.5386474%20L6.85276923%2C9.9684299%20L11.5703186%2C3.14417309%20Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A")}[dir=rtl] .tox-checklist>li:not(.tox-checklist--hidden)::before{margin-left:0;margin-right:-1.5em}code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;tab-size:4;-webkit-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#282a36}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#6272a4}.token.punctuation{color:#f8f8f2}.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#ff79c6}.token.boolean,.token.number{color:#bd93f9}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#50fa7b}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#f1fa8c}.token.keyword{color:#8be9fd}.token.important,.token.regex{color:#ffb86c}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.mce-content-body{overflow-wrap:break-word;word-wrap:break-word}.mce-content-body .mce-visual-caret{background-color:#000;background-color:currentColor;position:absolute}.mce-content-body .mce-visual-caret-hidden{display:none}.mce-content-body [data-mce-caret]{left:-1000px;margin:0;padding:0;position:absolute;right:auto;top:0}.mce-content-body .mce-offscreen-selection{left:-2000000px;max-width:1000000px;position:absolute}.mce-content-body [contentEditable=false]{cursor:default}.mce-content-body [contentEditable=true]{cursor:text}.tox-cursor-format-painter{cursor:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M15%2C6%20C15%2C5.45%2014.55%2C5%2014%2C5%20L6%2C5%20C5.45%2C5%205%2C5.45%205%2C6%20L5%2C10%20C5%2C10.55%205.45%2C11%206%2C11%20L14%2C11%20C14.55%2C11%2015%2C10.55%2015%2C10%20L15%2C9%20L16%2C9%20L16%2C12%20L9%2C12%20L9%2C19%20C9%2C19.55%209.45%2C20%2010%2C20%20L11%2C20%20C11.55%2C20%2012%2C19.55%2012%2C19%20L12%2C14%20L18%2C14%20L18%2C7%20L15%2C7%20L15%2C6%20Z%22%2F%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M1%2C1%20L8.25%2C1%20C8.66421356%2C1%209%2C1.33578644%209%2C1.75%20L9%2C1.75%20C9%2C2.16421356%208.66421356%2C2.5%208.25%2C2.5%20L2.5%2C2.5%20L2.5%2C8.25%20C2.5%2C8.66421356%202.16421356%2C9%201.75%2C9%20L1.75%2C9%20C1.33578644%2C9%201%2C8.66421356%201%2C8.25%20L1%2C1%20Z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A"),default}.mce-content-body figure.align-left{float:left}.mce-content-body figure.align-right{float:right}.mce-content-body figure.image.align-center{display:table;margin-left:auto;margin-right:auto}.mce-preview-object{border:1px solid gray;display:inline-block;line-height:0;margin:0 2px 0 2px;position:relative}.mce-preview-object .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.mce-preview-object[data-mce-selected="2"] .mce-shim{display:none}.mce-object{background:transparent url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%203h16a1%201%200%200%201%201%201v16a1%201%200%200%201-1%201H4a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1zm1%202v14h14V5H5zm4.79%202.565l5.64%204.028a.5.5%200%200%201%200%20.814l-5.64%204.028a.5.5%200%200%201-.79-.407V7.972a.5.5%200%200%201%20.79-.407z%22%20fill%3D%22%23cccccc%22%2F%3E%3C%2Fsvg%3E%0A") no-repeat center;border:1px dashed #aaa}.mce-pagebreak{border:1px dashed #aaa;cursor:default;display:block;height:5px;margin-top:15px;page-break-before:always;width:100%}@media print{.mce-pagebreak{border:0}}.tiny-pageembed .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.tiny-pageembed[data-mce-selected="2"] .mce-shim{display:none}.tiny-pageembed{display:inline-block;position:relative}.tiny-pageembed--16by9,.tiny-pageembed--1by1,.tiny-pageembed--21by9,.tiny-pageembed--4by3{display:block;overflow:hidden;padding:0;position:relative;width:100%}.tiny-pageembed--21by9{padding-top:42.857143%}.tiny-pageembed--16by9{padding-top:56.25%}.tiny-pageembed--4by3{padding-top:75%}.tiny-pageembed--1by1{padding-top:100%}.tiny-pageembed--16by9 iframe,.tiny-pageembed--1by1 iframe,.tiny-pageembed--21by9 iframe,.tiny-pageembed--4by3 iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.mce-content-body[data-mce-placeholder]{position:relative}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:rgba(34,47,62,.7);content:attr(data-mce-placeholder);position:absolute}.mce-content-body:not([dir=rtl])[data-mce-placeholder]:not(.mce-visualblocks)::before{left:1px}.mce-content-body[dir=rtl][data-mce-placeholder]:not(.mce-visualblocks)::before{right:1px}.mce-content-body div.mce-resizehandle{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;height:10px;position:absolute;width:10px;z-index:1298}.mce-content-body div.mce-resizehandle:hover{background-color:#4099ff}.mce-content-body div.mce-resizehandle:nth-of-type(1){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(2){cursor:nesw-resize}.mce-content-body div.mce-resizehandle:nth-of-type(3){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(4){cursor:nesw-resize}.mce-content-body .mce-resize-backdrop{z-index:10000}.mce-content-body .mce-clonedresizable{cursor:default;opacity:.5;outline:1px dashed #000;position:absolute;z-index:10001}.mce-content-body .mce-clonedresizable.mce-resizetable-columns td,.mce-content-body .mce-clonedresizable.mce-resizetable-columns th{border:0}.mce-content-body .mce-resize-helper{background:#555;background:rgba(0,0,0,.75);border:1px;border-radius:3px;color:#fff;display:none;font-family:sans-serif;font-size:12px;line-height:14px;margin:5px 10px;padding:5px;position:absolute;white-space:nowrap;z-index:10002}.tox-rtc-user-selection{position:relative}.tox-rtc-user-cursor{bottom:0;cursor:default;position:absolute;top:0;width:2px}.tox-rtc-user-cursor::before{background-color:inherit;border-radius:50%;content:'';display:block;height:8px;position:absolute;right:-3px;top:-3px;width:8px}.tox-rtc-user-cursor:hover::after{background-color:inherit;border-radius:100px;box-sizing:border-box;color:#fff;content:attr(data-user);display:block;font-size:12px;font-weight:700;left:-5px;min-height:8px;min-width:8px;padding:0 12px;position:absolute;top:-11px;white-space:nowrap;z-index:1000}.tox-rtc-user-selection--1 .tox-rtc-user-cursor{background-color:#2dc26b}.tox-rtc-user-selection--2 .tox-rtc-user-cursor{background-color:#e03e2d}.tox-rtc-user-selection--3 .tox-rtc-user-cursor{background-color:#f1c40f}.tox-rtc-user-selection--4 .tox-rtc-user-cursor{background-color:#3598db}.tox-rtc-user-selection--5 .tox-rtc-user-cursor{background-color:#b96ad9}.tox-rtc-user-selection--6 .tox-rtc-user-cursor{background-color:#e67e23}.tox-rtc-user-selection--7 .tox-rtc-user-cursor{background-color:#aaa69d}.tox-rtc-user-selection--8 .tox-rtc-user-cursor{background-color:#f368e0}.tox-rtc-remote-image{background:#eaeaea url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2236%22%20height%3D%2212%22%20viewBox%3D%220%200%2036%2012%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Ccircle%20cx%3D%226%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2218%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.33s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2230%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.66s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A") no-repeat center center;border:1px solid #ccc;min-height:240px;min-width:320px}.mce-match-marker{background:#aaa;color:#fff}.mce-match-marker-selected{background:#39f;color:#fff}.mce-match-marker-selected::-moz-selection{background:#39f;color:#fff}.mce-match-marker-selected::selection{background:#39f;color:#fff}.mce-content-body audio[data-mce-selected],.mce-content-body embed[data-mce-selected],.mce-content-body img[data-mce-selected],.mce-content-body object[data-mce-selected],.mce-content-body table[data-mce-selected],.mce-content-body video[data-mce-selected]{outline:3px solid #4099ff}.mce-content-body hr[data-mce-selected]{outline:3px solid #4099ff;outline-offset:1px}.mce-content-body [contentEditable=false] [contentEditable=true]:focus{outline:3px solid #4099ff}.mce-content-body [contentEditable=false] [contentEditable=true]:hover{outline:3px solid #4099ff}.mce-content-body [contentEditable=false][data-mce-selected]{cursor:not-allowed;outline:3px solid #4099ff}.mce-content-body.mce-content-readonly [contentEditable=true]:focus,.mce-content-body.mce-content-readonly [contentEditable=true]:hover{outline:0}.mce-content-body [data-mce-selected=inline-boundary]{background-color:#4099ff}.mce-content-body .mce-edit-focus{outline:3px solid #4099ff}.mce-content-body td[data-mce-selected],.mce-content-body th[data-mce-selected]{position:relative}.mce-content-body td[data-mce-selected]::-moz-selection,.mce-content-body th[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body td[data-mce-selected]::selection,.mce-content-body th[data-mce-selected]::selection{background:0 0}.mce-content-body td[data-mce-selected] *,.mce-content-body th[data-mce-selected] *{outline:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{background-color:rgba(180,215,255,.7);border:1px solid transparent;bottom:-1px;content:'';left:-1px;mix-blend-mode:lighten;position:absolute;right:-1px;top:-1px}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{border-color:rgba(0,84,180,.7)}}.mce-content-body img::-moz-selection{background:0 0}.mce-content-body img::selection{background:0 0}.ephox-snooker-resizer-bar{background-color:#4099ff;opacity:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:1}.mce-spellchecker-word{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%23ff0000'%20fill%3D'none'%20stroke-linecap%3D'round'%20stroke-opacity%3D'.75'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default;height:2rem}.mce-spellchecker-grammar{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%2300A835'%20fill%3D'none'%20stroke-linecap%3D'round'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc li{list-style-type:none}.mce-item-table:not([border]),.mce-item-table:not([border]) caption,.mce-item-table:not([border]) td,.mce-item-table:not([border]) th,.mce-item-table[border="0"],.mce-item-table[border="0"] caption,.mce-item-table[border="0"] td,.mce-item-table[border="0"] th,table[style*="border-width: 0px"],table[style*="border-width: 0px"] caption,table[style*="border-width: 0px"] td,table[style*="border-width: 0px"] th{border:1px dashed #bbb}.mce-visualblocks address,.mce-visualblocks article,.mce-visualblocks aside,.mce-visualblocks blockquote,.mce-visualblocks div:not([data-mce-bogus]),.mce-visualblocks dl,.mce-visualblocks figcaption,.mce-visualblocks figure,.mce-visualblocks h1,.mce-visualblocks h2,.mce-visualblocks h3,.mce-visualblocks h4,.mce-visualblocks h5,.mce-visualblocks h6,.mce-visualblocks hgroup,.mce-visualblocks ol,.mce-visualblocks p,.mce-visualblocks pre,.mce-visualblocks section,.mce-visualblocks ul{background-repeat:no-repeat;border:1px dashed #bbb;margin-left:3px;padding-top:10px}.mce-visualblocks p{background-image:url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7)}.mce-visualblocks h1{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==)}.mce-visualblocks h2{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==)}.mce-visualblocks h3{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7)}.mce-visualblocks h4{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==)}.mce-visualblocks h5{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==)}.mce-visualblocks h6{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==)}.mce-visualblocks div:not([data-mce-bogus]){background-image:url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7)}.mce-visualblocks section{background-image:url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=)}.mce-visualblocks article{background-image:url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7)}.mce-visualblocks blockquote{background-image:url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7)}.mce-visualblocks address{background-image:url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=)}.mce-visualblocks pre{background-image:url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==)}.mce-visualblocks figure{background-image:url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7)}.mce-visualblocks figcaption{border:1px dashed #bbb}.mce-visualblocks hgroup{background-image:url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7)}.mce-visualblocks aside{background-image:url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=)}.mce-visualblocks ul{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==)}.mce-visualblocks ol{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==)}.mce-visualblocks dl{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==)}.mce-visualblocks:not([dir=rtl]) address,.mce-visualblocks:not([dir=rtl]) article,.mce-visualblocks:not([dir=rtl]) aside,.mce-visualblocks:not([dir=rtl]) blockquote,.mce-visualblocks:not([dir=rtl]) div:not([data-mce-bogus]),.mce-visualblocks:not([dir=rtl]) dl,.mce-visualblocks:not([dir=rtl]) figcaption,.mce-visualblocks:not([dir=rtl]) figure,.mce-visualblocks:not([dir=rtl]) h1,.mce-visualblocks:not([dir=rtl]) h2,.mce-visualblocks:not([dir=rtl]) h3,.mce-visualblocks:not([dir=rtl]) h4,.mce-visualblocks:not([dir=rtl]) h5,.mce-visualblocks:not([dir=rtl]) h6,.mce-visualblocks:not([dir=rtl]) hgroup,.mce-visualblocks:not([dir=rtl]) ol,.mce-visualblocks:not([dir=rtl]) p,.mce-visualblocks:not([dir=rtl]) pre,.mce-visualblocks:not([dir=rtl]) section,.mce-visualblocks:not([dir=rtl]) ul{margin-left:3px}.mce-visualblocks[dir=rtl] address,.mce-visualblocks[dir=rtl] article,.mce-visualblocks[dir=rtl] aside,.mce-visualblocks[dir=rtl] blockquote,.mce-visualblocks[dir=rtl] div:not([data-mce-bogus]),.mce-visualblocks[dir=rtl] dl,.mce-visualblocks[dir=rtl] figcaption,.mce-visualblocks[dir=rtl] figure,.mce-visualblocks[dir=rtl] h1,.mce-visualblocks[dir=rtl] h2,.mce-visualblocks[dir=rtl] h3,.mce-visualblocks[dir=rtl] h4,.mce-visualblocks[dir=rtl] h5,.mce-visualblocks[dir=rtl] h6,.mce-visualblocks[dir=rtl] hgroup,.mce-visualblocks[dir=rtl] ol,.mce-visualblocks[dir=rtl] p,.mce-visualblocks[dir=rtl] pre,.mce-visualblocks[dir=rtl] section,.mce-visualblocks[dir=rtl] ul{background-position-x:right;margin-right:3px}.mce-nbsp,.mce-shy{background:#aaa}.mce-shy::after{content:'-'}body{font-family:sans-serif}table{border-collapse:collapse}
diff --git a/public/resource/tinymce/skins/ui/oxide-dark/content.mobile.min.css b/public/resource/tinymce/skins/ui/oxide-dark/content.mobile.min.css
new file mode 100644
index 0000000..35f7dc0
--- /dev/null
+++ b/public/resource/tinymce/skins/ui/oxide-dark/content.mobile.min.css
@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+.tinymce-mobile-unfocused-selections .tinymce-mobile-unfocused-selection{background-color:green;display:inline-block;opacity:.5;position:absolute}body{-webkit-text-size-adjust:none}body img{max-width:96vw}body table img{max-width:95%}body{font-family:sans-serif}table{border-collapse:collapse}
diff --git a/public/resource/tinymce/skins/ui/oxide-dark/skin.min.css b/public/resource/tinymce/skins/ui/oxide-dark/skin.min.css
new file mode 100644
index 0000000..e71f6f0
--- /dev/null
+++ b/public/resource/tinymce/skins/ui/oxide-dark/skin.min.css
@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+.tox{box-shadow:none;box-sizing:content-box;color:#2a3746;cursor:auto;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:16px;font-style:normal;font-weight:400;line-height:normal;-webkit-tap-highlight-color:transparent;text-decoration:none;text-shadow:none;text-transform:none;vertical-align:initial;white-space:normal}.tox :not(svg):not(rect){box-sizing:inherit;color:inherit;cursor:inherit;direction:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;line-height:inherit;-webkit-tap-highlight-color:inherit;text-align:inherit;text-decoration:inherit;text-shadow:inherit;text-transform:inherit;vertical-align:inherit;white-space:inherit}.tox :not(svg):not(rect){background:0 0;border:0;box-shadow:none;float:none;height:auto;margin:0;max-width:none;outline:0;padding:0;position:static;width:auto}.tox:not([dir=rtl]){direction:ltr;text-align:left}.tox[dir=rtl]{direction:rtl;text-align:right}.tox-tinymce{border:1px solid #000;border-radius:0;box-shadow:none;box-sizing:border-box;display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;overflow:hidden;position:relative;visibility:inherit!important}.tox-tinymce-inline{border:none;box-shadow:none}.tox-tinymce-inline .tox-editor-header{background-color:transparent;border:1px solid #000;border-radius:0;box-shadow:none}.tox-tinymce-aux{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;z-index:1300}.tox-tinymce :focus,.tox-tinymce-aux :focus{outline:0}button::-moz-focus-inner{border:0}.tox[dir=rtl] .tox-icon--flip svg{transform:rotateY(180deg)}.tox .accessibility-issue__header{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description{align-items:stretch;border:1px solid #000;border-radius:3px;display:flex;justify-content:space-between}.tox .accessibility-issue__description>div{padding-bottom:4px}.tox .accessibility-issue__description>div>div{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description>:last-child:not(:only-child){border-color:#000;border-style:solid}.tox .accessibility-issue__repair{margin-top:16px}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description{background-color:rgba(32,122,183,.5);border-color:#207ab7;color:#fff}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description>:last-child{border-color:#207ab7}.tox .tox-dialog__body-content .accessibility-issue--info .tox-form__group h2{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--info .tox-icon svg{fill:#fff}.tox .tox-dialog__body-content .accessibility-issue--info a .tox-icon{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description{background-color:rgba(255,165,0,.5);border-color:rgba(255,165,0,.8);color:#fff}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description>:last-child{border-color:rgba(255,165,0,.8)}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-form__group h2{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-icon svg{fill:#fff}.tox .tox-dialog__body-content .accessibility-issue--warn a .tox-icon{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description{background-color:rgba(204,0,0,.5);border-color:rgba(204,0,0,.8);color:#fff}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description>:last-child{border-color:rgba(204,0,0,.8)}.tox .tox-dialog__body-content .accessibility-issue--error .tox-form__group h2{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--error .tox-icon svg{fill:#fff}.tox .tox-dialog__body-content .accessibility-issue--error a .tox-icon{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--success .accessibility-issue__description{background-color:rgba(120,171,70,.5);border-color:rgba(120,171,70,.8);color:#fff}.tox .tox-dialog__body-content .accessibility-issue--success .accessibility-issue__description>:last-child{border-color:rgba(120,171,70,.8)}.tox .tox-dialog__body-content .accessibility-issue--success .tox-form__group h2{color:#fff}.tox .tox-dialog__body-content .accessibility-issue--success .tox-icon svg{fill:#fff}.tox .tox-dialog__body-content .accessibility-issue--success a .tox-icon{color:#fff}.tox .tox-dialog__body-content .accessibility-issue__header h1,.tox .tox-dialog__body-content .tox-form__group .accessibility-issue__description h2{margin-top:0}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__header .tox-button{margin-left:4px}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__header>:nth-last-child(2){margin-left:auto}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__description{padding:4px 4px 4px 8px}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__description>:last-child{border-left-width:1px;padding-left:4px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__header .tox-button{margin-right:4px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__header>:nth-last-child(2){margin-right:auto}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__description{padding:4px 8px 4px 4px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__description>:last-child{border-right-width:1px;padding-right:4px}.tox .tox-anchorbar{display:flex;flex:0 0 auto}.tox .tox-bar{display:flex;flex:0 0 auto}.tox .tox-button{background-color:#207ab7;background-image:none;background-position:0 0;background-repeat:repeat;border-color:#207ab7;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#fff;cursor:pointer;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:14px;font-style:normal;font-weight:700;letter-spacing:normal;line-height:24px;margin:0;outline:0;padding:4px 16px;text-align:center;text-decoration:none;text-transform:none;white-space:nowrap}.tox .tox-button[disabled]{background-color:#207ab7;background-image:none;border-color:#207ab7;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-button:focus:not(:disabled){background-color:#1c6ca1;background-image:none;border-color:#1c6ca1;box-shadow:none;color:#fff}.tox .tox-button:hover:not(:disabled){background-color:#1c6ca1;background-image:none;border-color:#1c6ca1;box-shadow:none;color:#fff}.tox .tox-button:active:not(:disabled){background-color:#185d8c;background-image:none;border-color:#185d8c;box-shadow:none;color:#fff}.tox .tox-button--secondary{background-color:#3d546f;background-image:none;background-position:0 0;background-repeat:repeat;border-color:#3d546f;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;color:#fff;font-size:14px;font-style:normal;font-weight:700;letter-spacing:normal;outline:0;padding:4px 16px;text-decoration:none;text-transform:none}.tox .tox-button--secondary[disabled]{background-color:#3d546f;background-image:none;border-color:#3d546f;box-shadow:none;color:rgba(255,255,255,.5)}.tox .tox-button--secondary:focus:not(:disabled){background-color:#34485f;background-image:none;border-color:#34485f;box-shadow:none;color:#fff}.tox .tox-button--secondary:hover:not(:disabled){background-color:#34485f;background-image:none;border-color:#34485f;box-shadow:none;color:#fff}.tox .tox-button--secondary:active:not(:disabled){background-color:#2b3b4e;background-image:none;border-color:#2b3b4e;box-shadow:none;color:#fff}.tox .tox-button--icon,.tox .tox-button.tox-button--icon,.tox .tox-button.tox-button--secondary.tox-button--icon{padding:4px}.tox .tox-button--icon .tox-icon svg,.tox .tox-button.tox-button--icon .tox-icon svg,.tox .tox-button.tox-button--secondary.tox-button--icon .tox-icon svg{display:block;fill:currentColor}.tox .tox-button-link{background:0;border:none;box-sizing:border-box;cursor:pointer;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:16px;font-weight:400;line-height:1.3;margin:0;padding:0;white-space:nowrap}.tox .tox-button-link--sm{font-size:14px}.tox .tox-button--naked{background-color:transparent;border-color:transparent;box-shadow:unset;color:#fff}.tox .tox-button--naked[disabled]{background-color:#3d546f;border-color:#3d546f;box-shadow:none;color:rgba(255,255,255,.5)}.tox .tox-button--naked:hover:not(:disabled){background-color:#34485f;border-color:#34485f;box-shadow:none;color:#fff}.tox .tox-button--naked:focus:not(:disabled){background-color:#34485f;border-color:#34485f;box-shadow:none;color:#fff}.tox .tox-button--naked:active:not(:disabled){background-color:#2b3b4e;border-color:#2b3b4e;box-shadow:none;color:#fff}.tox .tox-button--naked .tox-icon svg{fill:currentColor}.tox .tox-button--naked.tox-button--icon:hover:not(:disabled){color:#fff}.tox .tox-checkbox{align-items:center;border-radius:3px;cursor:pointer;display:flex;height:36px;min-width:36px}.tox .tox-checkbox__input{height:1px;overflow:hidden;position:absolute;top:auto;width:1px}.tox .tox-checkbox__icons{align-items:center;border-radius:3px;box-shadow:0 0 0 2px transparent;box-sizing:content-box;display:flex;height:24px;justify-content:center;padding:calc(4px - 1px);width:24px}.tox .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:block;fill:rgba(255,255,255,.2)}.tox .tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{display:none;fill:#207ab7}.tox .tox-checkbox__icons .tox-checkbox-icon__checked svg{display:none;fill:#207ab7}.tox .tox-checkbox--disabled{color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__checked svg{fill:rgba(255,255,255,.5)}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{fill:rgba(255,255,255,.5)}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{fill:rgba(255,255,255,.5)}.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:none}.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__checked svg{display:block}.tox input.tox-checkbox__input:indeterminate+.tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:none}.tox input.tox-checkbox__input:indeterminate+.tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{display:block}.tox input.tox-checkbox__input:focus+.tox-checkbox__icons{border-radius:3px;box-shadow:inset 0 0 0 1px #207ab7;padding:calc(4px - 1px)}.tox:not([dir=rtl]) .tox-checkbox__label{margin-left:4px}.tox:not([dir=rtl]) .tox-checkbox__input{left:-10000px}.tox:not([dir=rtl]) .tox-bar .tox-checkbox{margin-left:4px}.tox[dir=rtl] .tox-checkbox__label{margin-right:4px}.tox[dir=rtl] .tox-checkbox__input{right:-10000px}.tox[dir=rtl] .tox-bar .tox-checkbox{margin-right:4px}.tox .tox-collection--toolbar .tox-collection__group{display:flex;padding:0}.tox .tox-collection--grid .tox-collection__group{display:flex;flex-wrap:wrap;max-height:208px;overflow-x:hidden;overflow-y:auto;padding:0}.tox .tox-collection--list .tox-collection__group{border-bottom-width:0;border-color:#1a1a1a;border-left-width:0;border-right-width:0;border-style:solid;border-top-width:1px;padding:4px 0}.tox .tox-collection--list .tox-collection__group:first-child{border-top-width:0}.tox .tox-collection__group-heading{background-color:#333;color:#fff;cursor:default;font-size:12px;font-style:normal;font-weight:400;margin-bottom:4px;margin-top:-4px;padding:4px 8px;text-transform:none;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.tox .tox-collection__item{align-items:center;color:#fff;cursor:pointer;display:flex;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.tox .tox-collection--list .tox-collection__item{padding:4px 8px}.tox .tox-collection--toolbar .tox-collection__item{border-radius:3px;padding:4px}.tox .tox-collection--grid .tox-collection__item{border-radius:3px;padding:4px}.tox .tox-collection--list .tox-collection__item--enabled{background-color:#2b3b4e;color:#fff}.tox .tox-collection--list .tox-collection__item--active{background-color:#4a5562}.tox .tox-collection--toolbar .tox-collection__item--enabled{background-color:#757d87;color:#fff}.tox .tox-collection--toolbar .tox-collection__item--active{background-color:#4a5562}.tox .tox-collection--grid .tox-collection__item--enabled{background-color:#757d87;color:#fff}.tox .tox-collection--grid .tox-collection__item--active:not(.tox-collection__item--state-disabled){background-color:#4a5562;color:#fff}.tox .tox-collection--list .tox-collection__item--active:not(.tox-collection__item--state-disabled){color:#fff}.tox .tox-collection--toolbar .tox-collection__item--active:not(.tox-collection__item--state-disabled){color:#fff}.tox .tox-collection__item-checkmark,.tox .tox-collection__item-icon{align-items:center;display:flex;height:24px;justify-content:center;width:24px}.tox .tox-collection__item-checkmark svg,.tox .tox-collection__item-icon svg{fill:currentColor}.tox .tox-collection--toolbar-lg .tox-collection__item-icon{height:48px;width:48px}.tox .tox-collection__item-label{color:currentColor;display:inline-block;flex:1;-ms-flex-preferred-size:auto;font-size:14px;font-style:normal;font-weight:400;line-height:24px;text-transform:none;word-break:break-all}.tox .tox-collection__item-accessory{color:rgba(255,255,255,.5);display:inline-block;font-size:14px;height:24px;line-height:24px;text-transform:none}.tox .tox-collection__item-caret{align-items:center;display:flex;min-height:24px}.tox .tox-collection__item-caret::after{content:'';font-size:0;min-height:inherit}.tox .tox-collection__item-caret svg{fill:#fff}.tox .tox-collection__item--state-disabled{background-color:transparent;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-collection__item--state-disabled .tox-collection__item-caret svg{fill:rgba(255,255,255,.5)}.tox .tox-collection--list .tox-collection__item:not(.tox-collection__item--enabled) .tox-collection__item-checkmark svg{display:none}.tox .tox-collection--list .tox-collection__item:not(.tox-collection__item--enabled) .tox-collection__item-accessory+.tox-collection__item-checkmark{display:none}.tox .tox-collection--horizontal{background-color:#2b3b4e;border:1px solid #1a1a1a;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,.15);display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:nowrap;margin-bottom:0;overflow-x:auto;padding:0}.tox .tox-collection--horizontal .tox-collection__group{align-items:center;display:flex;flex-wrap:nowrap;margin:0;padding:0 4px}.tox .tox-collection--horizontal .tox-collection__item{height:34px;margin:2px 0 3px 0;padding:0 4px}.tox .tox-collection--horizontal .tox-collection__item-label{white-space:nowrap}.tox .tox-collection--horizontal .tox-collection__item-caret{margin-left:4px}.tox .tox-collection__item-container{display:flex}.tox .tox-collection__item-container--row{align-items:center;flex:1 1 auto;flex-direction:row}.tox .tox-collection__item-container--row.tox-collection__item-container--align-left{margin-right:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--align-right{justify-content:flex-end;margin-left:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-top{align-items:flex-start;margin-bottom:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-middle{align-items:center}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-bottom{align-items:flex-end;margin-top:auto}.tox .tox-collection__item-container--column{-ms-grid-row-align:center;align-self:center;flex:1 1 auto;flex-direction:column}.tox .tox-collection__item-container--column.tox-collection__item-container--align-left{align-items:flex-start}.tox .tox-collection__item-container--column.tox-collection__item-container--align-right{align-items:flex-end}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-top{align-self:flex-start}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-middle{-ms-grid-row-align:center;align-self:center}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-bottom{align-self:flex-end}.tox:not([dir=rtl]) .tox-collection--horizontal .tox-collection__group:not(:last-of-type){border-right:1px solid #000}.tox:not([dir=rtl]) .tox-collection--list .tox-collection__item>:not(:first-child){margin-left:8px}.tox:not([dir=rtl]) .tox-collection--list .tox-collection__item>.tox-collection__item-label:first-child{margin-left:4px}.tox:not([dir=rtl]) .tox-collection__item-accessory{margin-left:16px;text-align:right}.tox:not([dir=rtl]) .tox-collection .tox-collection__item-caret{margin-left:16px}.tox[dir=rtl] .tox-collection--horizontal .tox-collection__group:not(:last-of-type){border-left:1px solid #000}.tox[dir=rtl] .tox-collection--list .tox-collection__item>:not(:first-child){margin-right:8px}.tox[dir=rtl] .tox-collection--list .tox-collection__item>.tox-collection__item-label:first-child{margin-right:4px}.tox[dir=rtl] .tox-collection__item-accessory{margin-right:16px;text-align:left}.tox[dir=rtl] .tox-collection .tox-collection__item-caret{margin-right:16px;transform:rotateY(180deg)}.tox[dir=rtl] .tox-collection--horizontal .tox-collection__item-caret{margin-right:4px}.tox .tox-color-picker-container{display:flex;flex-direction:row;height:225px;margin:0}.tox .tox-sv-palette{box-sizing:border-box;display:flex;height:100%}.tox .tox-sv-palette-spectrum{height:100%}.tox .tox-sv-palette,.tox .tox-sv-palette-spectrum{width:225px}.tox .tox-sv-palette-thumb{background:0 0;border:1px solid #000;border-radius:50%;box-sizing:content-box;height:12px;position:absolute;width:12px}.tox .tox-sv-palette-inner-thumb{border:1px solid #fff;border-radius:50%;height:10px;position:absolute;width:10px}.tox .tox-hue-slider{box-sizing:border-box;height:100%;width:25px}.tox .tox-hue-slider-spectrum{background:linear-gradient(to bottom,red,#ff0080,#f0f,#8000ff,#00f,#0080ff,#0ff,#00ff80,#0f0,#80ff00,#ff0,#ff8000,red);height:100%;width:100%}.tox .tox-hue-slider,.tox .tox-hue-slider-spectrum{width:20px}.tox .tox-hue-slider-thumb{background:#fff;border:1px solid #000;box-sizing:content-box;height:4px;width:100%}.tox .tox-rgb-form{display:flex;flex-direction:column;justify-content:space-between}.tox .tox-rgb-form div{align-items:center;display:flex;justify-content:space-between;margin-bottom:5px;width:inherit}.tox .tox-rgb-form input{width:6em}.tox .tox-rgb-form input.tox-invalid{border:1px solid red!important}.tox .tox-rgb-form .tox-rgba-preview{border:1px solid #000;flex-grow:2;margin-bottom:0}.tox:not([dir=rtl]) .tox-sv-palette{margin-right:15px}.tox:not([dir=rtl]) .tox-hue-slider{margin-right:15px}.tox:not([dir=rtl]) .tox-hue-slider-thumb{margin-left:-1px}.tox:not([dir=rtl]) .tox-rgb-form label{margin-right:.5em}.tox[dir=rtl] .tox-sv-palette{margin-left:15px}.tox[dir=rtl] .tox-hue-slider{margin-left:15px}.tox[dir=rtl] .tox-hue-slider-thumb{margin-right:-1px}.tox[dir=rtl] .tox-rgb-form label{margin-left:.5em}.tox .tox-toolbar .tox-swatches,.tox .tox-toolbar__overflow .tox-swatches,.tox .tox-toolbar__primary .tox-swatches{margin:2px 0 3px 4px}.tox .tox-collection--list .tox-collection__group .tox-swatches-menu{border:0;margin:-4px 0}.tox .tox-swatches__row{display:flex}.tox .tox-swatch{height:30px;transition:transform .15s,box-shadow .15s;width:30px}.tox .tox-swatch:focus,.tox .tox-swatch:hover{box-shadow:0 0 0 1px rgba(127,127,127,.3) inset;transform:scale(.8)}.tox .tox-swatch--remove{align-items:center;display:flex;justify-content:center}.tox .tox-swatch--remove svg path{stroke:#e74c3c}.tox .tox-swatches__picker-btn{align-items:center;background-color:transparent;border:0;cursor:pointer;display:flex;height:30px;justify-content:center;outline:0;padding:0;width:30px}.tox .tox-swatches__picker-btn svg{height:24px;width:24px}.tox .tox-swatches__picker-btn:hover{background:#4a5562}.tox:not([dir=rtl]) .tox-swatches__picker-btn{margin-left:auto}.tox[dir=rtl] .tox-swatches__picker-btn{margin-right:auto}.tox .tox-comment-thread{background:#2b3b4e;position:relative}.tox .tox-comment-thread>:not(:first-child){margin-top:8px}.tox .tox-comment{background:#2b3b4e;border:1px solid #000;border-radius:3px;box-shadow:0 4px 8px 0 rgba(42,55,70,.1);padding:8px 8px 16px 8px;position:relative}.tox .tox-comment__header{align-items:center;color:#fff;display:flex;justify-content:space-between}.tox .tox-comment__date{color:rgba(255,255,255,.5);font-size:12px}.tox .tox-comment__body{color:#fff;font-size:14px;font-style:normal;font-weight:400;line-height:1.3;margin-top:8px;position:relative;text-transform:initial}.tox .tox-comment__body textarea{resize:none;white-space:normal;width:100%}.tox .tox-comment__expander{padding-top:8px}.tox .tox-comment__expander p{color:rgba(255,255,255,.5);font-size:14px;font-style:normal}.tox .tox-comment__body p{margin:0}.tox .tox-comment__buttonspacing{padding-top:16px;text-align:center}.tox .tox-comment-thread__overlay::after{background:#2b3b4e;bottom:0;content:"";display:flex;left:0;opacity:.9;position:absolute;right:0;top:0;z-index:5}.tox .tox-comment__reply{display:flex;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end;margin-top:8px}.tox .tox-comment__reply>:first-child{margin-bottom:8px;width:100%}.tox .tox-comment__edit{display:flex;flex-wrap:wrap;justify-content:flex-end;margin-top:16px}.tox .tox-comment__gradient::after{background:linear-gradient(rgba(43,59,78,0),#2b3b4e);bottom:0;content:"";display:block;height:5em;margin-top:-40px;position:absolute;width:100%}.tox .tox-comment__overlay{background:#2b3b4e;bottom:0;display:flex;flex-direction:column;flex-grow:1;left:0;opacity:.9;position:absolute;right:0;text-align:center;top:0;z-index:5}.tox .tox-comment__loading-text{align-items:center;color:#fff;display:flex;flex-direction:column;position:relative}.tox .tox-comment__loading-text>div{padding-bottom:16px}.tox .tox-comment__overlaytext{bottom:0;flex-direction:column;font-size:14px;left:0;padding:1em;position:absolute;right:0;top:0;z-index:10}.tox .tox-comment__overlaytext p{background-color:#2b3b4e;box-shadow:0 0 8px 8px #2b3b4e;color:#fff;text-align:center}.tox .tox-comment__overlaytext div:nth-of-type(2){font-size:.8em}.tox .tox-comment__busy-spinner{align-items:center;background-color:#2b3b4e;bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0;z-index:20}.tox .tox-comment__scroll{display:flex;flex-direction:column;flex-shrink:1;overflow:auto}.tox .tox-conversations{margin:8px}.tox:not([dir=rtl]) .tox-comment__edit{margin-left:8px}.tox:not([dir=rtl]) .tox-comment__buttonspacing>:last-child,.tox:not([dir=rtl]) .tox-comment__edit>:last-child,.tox:not([dir=rtl]) .tox-comment__reply>:last-child{margin-left:8px}.tox[dir=rtl] .tox-comment__edit{margin-right:8px}.tox[dir=rtl] .tox-comment__buttonspacing>:last-child,.tox[dir=rtl] .tox-comment__edit>:last-child,.tox[dir=rtl] .tox-comment__reply>:last-child{margin-right:8px}.tox .tox-user{align-items:center;display:flex}.tox .tox-user__avatar svg{fill:rgba(255,255,255,.5)}.tox .tox-user__name{color:rgba(255,255,255,.5);font-size:12px;font-style:normal;font-weight:700;text-transform:uppercase}.tox:not([dir=rtl]) .tox-user__avatar svg{margin-right:8px}.tox:not([dir=rtl]) .tox-user__avatar+.tox-user__name{margin-left:8px}.tox[dir=rtl] .tox-user__avatar svg{margin-left:8px}.tox[dir=rtl] .tox-user__avatar+.tox-user__name{margin-right:8px}.tox .tox-dialog-wrap{align-items:center;bottom:0;display:flex;justify-content:center;left:0;position:fixed;right:0;top:0;z-index:1100}.tox .tox-dialog-wrap__backdrop{background-color:rgba(34,47,62,.75);bottom:0;left:0;position:absolute;right:0;top:0;z-index:1}.tox .tox-dialog-wrap__backdrop--opaque{background-color:#222f3e}.tox .tox-dialog{background-color:#2b3b4e;border-color:#000;border-radius:3px;border-style:solid;border-width:1px;box-shadow:0 16px 16px -10px rgba(42,55,70,.15),0 0 40px 1px rgba(42,55,70,.15);display:flex;flex-direction:column;max-height:100%;max-width:480px;overflow:hidden;position:relative;width:95vw;z-index:2}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog{align-self:flex-start;margin:8px auto;width:calc(100vw - 16px)}}.tox .tox-dialog-inline{z-index:1100}.tox .tox-dialog__header{align-items:center;background-color:#2b3b4e;border-bottom:none;color:#fff;display:flex;font-size:16px;justify-content:space-between;padding:8px 16px 0 16px;position:relative}.tox .tox-dialog__header .tox-button{z-index:1}.tox .tox-dialog__draghandle{cursor:grab;height:100%;left:0;position:absolute;top:0;width:100%}.tox .tox-dialog__draghandle:active{cursor:grabbing}.tox .tox-dialog__dismiss{margin-left:auto}.tox .tox-dialog__title{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:20px;font-style:normal;font-weight:400;line-height:1.3;margin:0;text-transform:none}.tox .tox-dialog__body{color:#fff;display:flex;flex:1;-ms-flex-preferred-size:auto;font-size:16px;font-style:normal;font-weight:400;line-height:1.3;min-width:0;text-align:left;text-transform:none}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog__body{flex-direction:column}}.tox .tox-dialog__body-nav{align-items:flex-start;display:flex;flex-direction:column;padding:16px 16px}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog__body-nav{flex-direction:row;-webkit-overflow-scrolling:touch;overflow-x:auto;padding-bottom:0}}.tox .tox-dialog__body-nav-item{border-bottom:2px solid transparent;color:rgba(255,255,255,.5);display:inline-block;font-size:14px;line-height:1.3;margin-bottom:8px;text-decoration:none;white-space:nowrap}.tox .tox-dialog__body-nav-item:focus{background-color:rgba(32,122,183,.1)}.tox .tox-dialog__body-nav-item--active{border-bottom:2px solid #207ab7;color:#207ab7}.tox .tox-dialog__body-content{box-sizing:border-box;display:flex;flex:1;flex-direction:column;-ms-flex-preferred-size:auto;max-height:650px;overflow:auto;-webkit-overflow-scrolling:touch;padding:16px 16px}.tox .tox-dialog__body-content>*{margin-bottom:0;margin-top:16px}.tox .tox-dialog__body-content>:first-child{margin-top:0}.tox .tox-dialog__body-content>:last-child{margin-bottom:0}.tox .tox-dialog__body-content>:only-child{margin-bottom:0;margin-top:0}.tox .tox-dialog__body-content a{color:#207ab7;cursor:pointer;text-decoration:none}.tox .tox-dialog__body-content a:focus,.tox .tox-dialog__body-content a:hover{color:#185d8c;text-decoration:none}.tox .tox-dialog__body-content a:active{color:#185d8c;text-decoration:none}.tox .tox-dialog__body-content svg{fill:#fff}.tox .tox-dialog__body-content ul{display:block;list-style-type:disc;margin-bottom:16px;-webkit-margin-end:0;margin-inline-end:0;-webkit-margin-start:0;margin-inline-start:0;-webkit-padding-start:2.5rem;padding-inline-start:2.5rem}.tox .tox-dialog__body-content .tox-form__group h1{color:#fff;font-size:20px;font-style:normal;font-weight:700;letter-spacing:normal;margin-bottom:16px;margin-top:2rem;text-transform:none}.tox .tox-dialog__body-content .tox-form__group h2{color:#fff;font-size:16px;font-style:normal;font-weight:700;letter-spacing:normal;margin-bottom:16px;margin-top:2rem;text-transform:none}.tox .tox-dialog__body-content .tox-form__group p{margin-bottom:16px}.tox .tox-dialog__body-content .tox-form__group h1:first-child,.tox .tox-dialog__body-content .tox-form__group h2:first-child,.tox .tox-dialog__body-content .tox-form__group p:first-child{margin-top:0}.tox .tox-dialog__body-content .tox-form__group h1:last-child,.tox .tox-dialog__body-content .tox-form__group h2:last-child,.tox .tox-dialog__body-content .tox-form__group p:last-child{margin-bottom:0}.tox .tox-dialog__body-content .tox-form__group h1:only-child,.tox .tox-dialog__body-content .tox-form__group h2:only-child,.tox .tox-dialog__body-content .tox-form__group p:only-child{margin-bottom:0;margin-top:0}.tox .tox-dialog--width-lg{height:650px;max-width:1200px}.tox .tox-dialog--width-md{max-width:800px}.tox .tox-dialog--width-md .tox-dialog__body-content{overflow:auto}.tox .tox-dialog__body-content--centered{text-align:center}.tox .tox-dialog__footer{align-items:center;background-color:#2b3b4e;border-top:1px solid #000;display:flex;justify-content:space-between;padding:8px 16px}.tox .tox-dialog__footer-end,.tox .tox-dialog__footer-start{display:flex}.tox .tox-dialog__busy-spinner{align-items:center;background-color:rgba(34,47,62,.75);bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0;z-index:3}.tox .tox-dialog__table{border-collapse:collapse;width:100%}.tox .tox-dialog__table thead th{font-weight:700;padding-bottom:8px}.tox .tox-dialog__table tbody tr{border-bottom:1px solid #000}.tox .tox-dialog__table tbody tr:last-child{border-bottom:none}.tox .tox-dialog__table td{padding-bottom:8px;padding-top:8px}.tox .tox-dialog__popups{position:absolute;width:100%;z-index:1100}.tox .tox-dialog__body-iframe{display:flex;flex:1;flex-direction:column;-ms-flex-preferred-size:auto}.tox .tox-dialog__body-iframe .tox-navobj{display:flex;flex:1;-ms-flex-preferred-size:auto}.tox .tox-dialog__body-iframe .tox-navobj :nth-child(2){flex:1;-ms-flex-preferred-size:auto;height:100%}.tox .tox-dialog-dock-fadeout{opacity:0;visibility:hidden}.tox .tox-dialog-dock-fadein{opacity:1;visibility:visible}.tox .tox-dialog-dock-transition{transition:visibility 0s linear .3s,opacity .3s ease}.tox .tox-dialog-dock-transition.tox-dialog-dock-fadein{transition-delay:0s}.tox.tox-platform-ie .tox-dialog-wrap{position:-ms-device-fixed}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox:not([dir=rtl]) .tox-dialog__body-nav{margin-right:0}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox:not([dir=rtl]) .tox-dialog__body-nav-item:not(:first-child){margin-left:8px}}.tox:not([dir=rtl]) .tox-dialog__footer .tox-dialog__footer-end>*,.tox:not([dir=rtl]) .tox-dialog__footer .tox-dialog__footer-start>*{margin-left:8px}.tox[dir=rtl] .tox-dialog__body{text-align:right}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox[dir=rtl] .tox-dialog__body-nav{margin-left:0}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox[dir=rtl] .tox-dialog__body-nav-item:not(:first-child){margin-right:8px}}.tox[dir=rtl] .tox-dialog__footer .tox-dialog__footer-end>*,.tox[dir=rtl] .tox-dialog__footer .tox-dialog__footer-start>*{margin-right:8px}body.tox-dialog__disable-scroll{overflow:hidden}.tox .tox-dropzone-container{display:flex;flex:1;-ms-flex-preferred-size:auto}.tox .tox-dropzone{align-items:center;background:#fff;border:2px dashed #000;box-sizing:border-box;display:flex;flex-direction:column;flex-grow:1;justify-content:center;min-height:100px;padding:10px}.tox .tox-dropzone p{color:rgba(255,255,255,.5);margin:0 0 16px 0}.tox .tox-edit-area{display:flex;flex:1;-ms-flex-preferred-size:auto;overflow:hidden;position:relative}.tox .tox-edit-area__iframe{background-color:#fff;border:0;box-sizing:border-box;flex:1;-ms-flex-preferred-size:auto;height:100%;position:absolute;width:100%}.tox.tox-inline-edit-area{border:1px dotted #000}.tox .tox-editor-container{display:flex;flex:1 1 auto;flex-direction:column;overflow:hidden}.tox .tox-editor-header{z-index:1}.tox:not(.tox-tinymce-inline) .tox-editor-header{box-shadow:none;transition:box-shadow .5s}.tox.tox-tinymce--toolbar-bottom .tox-editor-header,.tox.tox-tinymce-inline .tox-editor-header{margin-bottom:-1px}.tox.tox-tinymce--toolbar-sticky-on .tox-editor-header{background-color:transparent;box-shadow:0 4px 4px -3px rgba(0,0,0,.25)}.tox-editor-dock-fadeout{opacity:0;visibility:hidden}.tox-editor-dock-fadein{opacity:1;visibility:visible}.tox-editor-dock-transition{transition:visibility 0s linear .25s,opacity .25s ease}.tox-editor-dock-transition.tox-editor-dock-fadein{transition-delay:0s}.tox .tox-control-wrap{flex:1;position:relative}.tox .tox-control-wrap:not(.tox-control-wrap--status-invalid) .tox-control-wrap__status-icon-invalid,.tox .tox-control-wrap:not(.tox-control-wrap--status-unknown) .tox-control-wrap__status-icon-unknown,.tox .tox-control-wrap:not(.tox-control-wrap--status-valid) .tox-control-wrap__status-icon-valid{display:none}.tox .tox-control-wrap svg{display:block}.tox .tox-control-wrap__status-icon-wrap{position:absolute;top:50%;transform:translateY(-50%)}.tox .tox-control-wrap__status-icon-invalid svg{fill:#c00}.tox .tox-control-wrap__status-icon-unknown svg{fill:orange}.tox .tox-control-wrap__status-icon-valid svg{fill:green}.tox:not([dir=rtl]) .tox-control-wrap--status-invalid .tox-textfield,.tox:not([dir=rtl]) .tox-control-wrap--status-unknown .tox-textfield,.tox:not([dir=rtl]) .tox-control-wrap--status-valid .tox-textfield{padding-right:32px}.tox:not([dir=rtl]) .tox-control-wrap__status-icon-wrap{right:4px}.tox[dir=rtl] .tox-control-wrap--status-invalid .tox-textfield,.tox[dir=rtl] .tox-control-wrap--status-unknown .tox-textfield,.tox[dir=rtl] .tox-control-wrap--status-valid .tox-textfield{padding-left:32px}.tox[dir=rtl] .tox-control-wrap__status-icon-wrap{left:4px}.tox .tox-autocompleter{max-width:25em}.tox .tox-autocompleter .tox-menu{max-width:25em}.tox .tox-autocompleter .tox-autocompleter-highlight{font-weight:700}.tox .tox-color-input{display:flex;position:relative;z-index:1}.tox .tox-color-input .tox-textfield{z-index:-1}.tox .tox-color-input span{border-color:rgba(42,55,70,.2);border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;height:24px;position:absolute;top:6px;width:24px}.tox .tox-color-input span:focus:not([aria-disabled=true]),.tox .tox-color-input span:hover:not([aria-disabled=true]){border-color:#207ab7;cursor:pointer}.tox .tox-color-input span::before{background-image:linear-gradient(45deg,rgba(255,255,255,.25) 25%,transparent 25%),linear-gradient(-45deg,rgba(255,255,255,.25) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(255,255,255,.25) 75%),linear-gradient(-45deg,transparent 75%,rgba(255,255,255,.25) 75%);background-position:0 0,0 6px,6px -6px,-6px 0;background-size:12px 12px;border:1px solid #2b3b4e;border-radius:3px;box-sizing:border-box;content:'';height:24px;left:-1px;position:absolute;top:-1px;width:24px;z-index:-1}.tox .tox-color-input span[aria-disabled=true]{cursor:not-allowed}.tox:not([dir=rtl]) .tox-color-input .tox-textfield{padding-left:36px}.tox:not([dir=rtl]) .tox-color-input span{left:6px}.tox[dir=rtl] .tox-color-input .tox-textfield{padding-right:36px}.tox[dir=rtl] .tox-color-input span{right:6px}.tox .tox-label,.tox .tox-toolbar-label{color:rgba(255,255,255,.5);display:block;font-size:14px;font-style:normal;font-weight:400;line-height:1.3;padding:0 8px 0 0;text-transform:none;white-space:nowrap}.tox .tox-toolbar-label{padding:0 8px}.tox[dir=rtl] .tox-label{padding:0 0 0 8px}.tox .tox-form{display:flex;flex:1;flex-direction:column;-ms-flex-preferred-size:auto}.tox .tox-form__group{box-sizing:border-box;margin-bottom:4px}.tox .tox-form-group--maximize{flex:1}.tox .tox-form__group--error{color:#c00}.tox .tox-form__group--collection{display:flex}.tox .tox-form__grid{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between}.tox .tox-form__grid--2col>.tox-form__group{width:calc(50% - (8px / 2))}.tox .tox-form__grid--3col>.tox-form__group{width:calc(100% / 3 - (8px / 2))}.tox .tox-form__grid--4col>.tox-form__group{width:calc(25% - (8px / 2))}.tox .tox-form__controls-h-stack{align-items:center;display:flex}.tox .tox-form__group--inline{align-items:center;display:flex}.tox .tox-form__group--stretched{display:flex;flex:1;flex-direction:column;-ms-flex-preferred-size:auto}.tox .tox-form__group--stretched .tox-textarea{flex:1;-ms-flex-preferred-size:auto}.tox .tox-form__group--stretched .tox-navobj{display:flex;flex:1;-ms-flex-preferred-size:auto}.tox .tox-form__group--stretched .tox-navobj :nth-child(2){flex:1;-ms-flex-preferred-size:auto;height:100%}.tox:not([dir=rtl]) .tox-form__controls-h-stack>:not(:first-child){margin-left:4px}.tox[dir=rtl] .tox-form__controls-h-stack>:not(:first-child){margin-right:4px}.tox .tox-lock.tox-locked .tox-lock-icon__unlock,.tox .tox-lock:not(.tox-locked) .tox-lock-icon__lock{display:none}.tox .tox-listboxfield .tox-listbox--select,.tox .tox-textarea,.tox .tox-textfield,.tox .tox-toolbar-textfield{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#2b3b4e;border-color:#000;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#fff;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:16px;line-height:24px;margin:0;min-height:34px;outline:0;padding:5px 4.75px;resize:none;width:100%}.tox .tox-textarea[disabled],.tox .tox-textfield[disabled]{background-color:#222f3e;color:rgba(255,255,255,.85);cursor:not-allowed}.tox .tox-listboxfield .tox-listbox--select:focus,.tox .tox-textarea:focus,.tox .tox-textfield:focus{background-color:#2b3b4e;border-color:#207ab7;box-shadow:none;outline:0}.tox .tox-toolbar-textfield{border-width:0;margin-bottom:3px;margin-top:2px;max-width:250px}.tox .tox-naked-btn{background-color:transparent;border:0;border-color:transparent;box-shadow:unset;color:#207ab7;cursor:pointer;display:block;margin:0;padding:0}.tox .tox-naked-btn svg{display:block;fill:#fff}.tox:not([dir=rtl]) .tox-toolbar-textfield+*{margin-left:4px}.tox[dir=rtl] .tox-toolbar-textfield+*{margin-right:4px}.tox .tox-listboxfield{cursor:pointer;position:relative}.tox .tox-listboxfield .tox-listbox--select[disabled]{background-color:#19232e;color:rgba(255,255,255,.85);cursor:not-allowed}.tox .tox-listbox__select-label{cursor:default;flex:1;margin:0 4px}.tox .tox-listbox__select-chevron{align-items:center;display:flex;justify-content:center;width:16px}.tox .tox-listbox__select-chevron svg{fill:#fff}.tox .tox-listboxfield .tox-listbox--select{align-items:center;display:flex}.tox:not([dir=rtl]) .tox-listboxfield svg{right:8px}.tox[dir=rtl] .tox-listboxfield svg{left:8px}.tox .tox-selectfield{cursor:pointer;position:relative}.tox .tox-selectfield select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#2b3b4e;border-color:#000;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#fff;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:16px;line-height:24px;margin:0;min-height:34px;outline:0;padding:5px 4.75px;resize:none;width:100%}.tox .tox-selectfield select[disabled]{background-color:#19232e;color:rgba(255,255,255,.85);cursor:not-allowed}.tox .tox-selectfield select::-ms-expand{display:none}.tox .tox-selectfield select:focus{background-color:#2b3b4e;border-color:#207ab7;box-shadow:none;outline:0}.tox .tox-selectfield svg{pointer-events:none;position:absolute;top:50%;transform:translateY(-50%)}.tox:not([dir=rtl]) .tox-selectfield select[size="0"],.tox:not([dir=rtl]) .tox-selectfield select[size="1"]{padding-right:24px}.tox:not([dir=rtl]) .tox-selectfield svg{right:8px}.tox[dir=rtl] .tox-selectfield select[size="0"],.tox[dir=rtl] .tox-selectfield select[size="1"]{padding-left:24px}.tox[dir=rtl] .tox-selectfield svg{left:8px}.tox .tox-textarea{-webkit-appearance:textarea;-moz-appearance:textarea;appearance:textarea;white-space:pre-wrap}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;-ms-scroll-chaining:none;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}.tox .tox-help__more-link{list-style:none;margin-top:1em}.tox .tox-image-tools{width:100%}.tox .tox-image-tools__toolbar{align-items:center;display:flex;justify-content:center}.tox .tox-image-tools__image{background-color:#666;height:380px;overflow:auto;position:relative;width:100%}.tox .tox-image-tools__image,.tox .tox-image-tools__image+.tox-image-tools__toolbar{margin-top:8px}.tox .tox-image-tools__image-bg{background:url(data:image/gif;base64,R0lGODdhDAAMAIABAMzMzP///ywAAAAADAAMAAACFoQfqYeabNyDMkBQb81Uat85nxguUAEAOw==)}.tox .tox-image-tools__toolbar>.tox-spacer{flex:1;-ms-flex-preferred-size:auto}.tox .tox-croprect-block{background:#000;opacity:.5;position:absolute;zoom:1}.tox .tox-croprect-handle{border:2px solid #fff;height:20px;left:0;position:absolute;top:0;width:20px}.tox .tox-croprect-handle-move{border:0;cursor:move;position:absolute}.tox .tox-croprect-handle-nw{border-width:2px 0 0 2px;cursor:nw-resize;left:100px;margin:-2px 0 0 -2px;top:100px}.tox .tox-croprect-handle-ne{border-width:2px 2px 0 0;cursor:ne-resize;left:200px;margin:-2px 0 0 -20px;top:100px}.tox .tox-croprect-handle-sw{border-width:0 0 2px 2px;cursor:sw-resize;left:100px;margin:-20px 2px 0 -2px;top:200px}.tox .tox-croprect-handle-se{border-width:0 2px 2px 0;cursor:se-resize;left:200px;margin:-20px 0 0 -20px;top:200px}.tox:not([dir=rtl]) .tox-image-tools__toolbar>.tox-slider:not(:first-of-type){margin-left:8px}.tox:not([dir=rtl]) .tox-image-tools__toolbar>.tox-button+.tox-slider{margin-left:32px}.tox:not([dir=rtl]) .tox-image-tools__toolbar>.tox-slider+.tox-button{margin-left:32px}.tox[dir=rtl] .tox-image-tools__toolbar>.tox-slider:not(:first-of-type){margin-right:8px}.tox[dir=rtl] .tox-image-tools__toolbar>.tox-button+.tox-slider{margin-right:32px}.tox[dir=rtl] .tox-image-tools__toolbar>.tox-slider+.tox-button{margin-right:32px}.tox .tox-insert-table-picker{display:flex;flex-wrap:wrap;width:170px}.tox .tox-insert-table-picker>div{border-color:#000;border-style:solid;border-width:0 1px 1px 0;box-sizing:border-box;height:17px;width:17px}.tox .tox-collection--list .tox-collection__group .tox-insert-table-picker{margin:-4px 0}.tox .tox-insert-table-picker .tox-insert-table-picker__selected{background-color:rgba(32,122,183,.5);border-color:rgba(32,122,183,.5)}.tox .tox-insert-table-picker__label{color:#fff;display:block;font-size:14px;padding:4px;text-align:center;width:100%}.tox:not([dir=rtl]) .tox-insert-table-picker>div:nth-child(10n){border-right:0}.tox[dir=rtl] .tox-insert-table-picker>div:nth-child(10n+1){border-right:0}.tox .tox-menu{background-color:#2b3b4e;border:1px solid #000;border-radius:3px;box-shadow:0 4px 8px 0 rgba(42,55,70,.1);display:inline-block;overflow:hidden;vertical-align:top;z-index:1150}.tox .tox-menu.tox-collection.tox-collection--list{padding:0}.tox .tox-menu.tox-collection.tox-collection--toolbar{padding:4px}.tox .tox-menu.tox-collection.tox-collection--grid{padding:4px}.tox .tox-menu__label blockquote,.tox .tox-menu__label code,.tox .tox-menu__label h1,.tox .tox-menu__label h2,.tox .tox-menu__label h3,.tox .tox-menu__label h4,.tox .tox-menu__label h5,.tox .tox-menu__label h6,.tox .tox-menu__label p{margin:0}.tox .tox-menubar{background:url("data:image/svg+xml;charset=utf8,%3Csvg height='39px' viewBox='0 0 40 39px' width='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='38px' width='100' height='1' fill='%23000000'/%3E%3C/svg%3E") left 0 top 0 #222f3e;background-color:#222f3e;display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:wrap;padding:0 4px 0 4px}.tox.tox-tinymce:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-menubar{border-top:1px solid #000}.tox .tox-mbtn{align-items:center;background:0 0;border:0;border-radius:3px;box-shadow:none;color:#fff;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:34px;justify-content:center;margin:2px 0 3px 0;outline:0;overflow:hidden;padding:0 4px;text-transform:none;width:auto}.tox .tox-mbtn[disabled]{background-color:transparent;border:0;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-mbtn:focus:not(:disabled){background:#4a5562;border:0;box-shadow:none;color:#fff}.tox .tox-mbtn--active{background:#757d87;border:0;box-shadow:none;color:#fff}.tox .tox-mbtn:hover:not(:disabled):not(.tox-mbtn--active){background:#4a5562;border:0;box-shadow:none;color:#fff}.tox .tox-mbtn__select-label{cursor:default;font-weight:400;margin:0 4px}.tox .tox-mbtn[disabled] .tox-mbtn__select-label{cursor:not-allowed}.tox .tox-mbtn__select-chevron{align-items:center;display:flex;justify-content:center;width:16px;display:none}.tox .tox-notification{border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;display:-ms-grid;display:grid;font-size:14px;font-weight:400;-ms-grid-columns:minmax(40px,1fr) auto minmax(40px,1fr);grid-template-columns:minmax(40px,1fr) auto minmax(40px,1fr);margin-top:4px;opacity:0;padding:4px;transition:transform .1s ease-in,opacity 150ms ease-in}.tox .tox-notification p{font-size:14px;font-weight:400}.tox .tox-notification a{cursor:pointer;text-decoration:underline}.tox .tox-notification--in{opacity:1}.tox .tox-notification--success{background-color:#e4eeda;border-color:#d7e6c8;color:#fff}.tox .tox-notification--success p{color:#fff}.tox .tox-notification--success a{color:#547831}.tox .tox-notification--success svg{fill:#fff}.tox .tox-notification--error{background-color:#f8dede;border-color:#f2bfbf;color:#fff}.tox .tox-notification--error p{color:#fff}.tox .tox-notification--error a{color:#c00}.tox .tox-notification--error svg{fill:#fff}.tox .tox-notification--warn,.tox .tox-notification--warning{background-color:#fffaea;border-color:#ffe89d;color:#fff}.tox .tox-notification--warn p,.tox .tox-notification--warning p{color:#fff}.tox .tox-notification--warn a,.tox .tox-notification--warning a{color:#fff}.tox .tox-notification--warn svg,.tox .tox-notification--warning svg{fill:#fff}.tox .tox-notification--info{background-color:#d9edf7;border-color:#779ecb;color:#fff}.tox .tox-notification--info p{color:#fff}.tox .tox-notification--info a{color:#fff}.tox .tox-notification--info svg{fill:#fff}.tox .tox-notification__body{-ms-grid-row-align:center;align-self:center;color:#fff;font-size:14px;-ms-grid-column-span:1;grid-column-end:3;-ms-grid-column:2;grid-column-start:2;-ms-grid-row-span:1;grid-row-end:2;-ms-grid-row:1;grid-row-start:1;text-align:center;white-space:normal;word-break:break-all;word-break:break-word}.tox .tox-notification__body>*{margin:0}.tox .tox-notification__body>*+*{margin-top:1rem}.tox .tox-notification__icon{-ms-grid-row-align:center;align-self:center;-ms-grid-column-span:1;grid-column-end:2;-ms-grid-column:1;grid-column-start:1;-ms-grid-row-span:1;grid-row-end:2;-ms-grid-row:1;grid-row-start:1;-ms-grid-column-align:end;justify-self:end}.tox .tox-notification__icon svg{display:block}.tox .tox-notification__dismiss{-ms-grid-row-align:start;align-self:start;-ms-grid-column-span:1;grid-column-end:4;-ms-grid-column:3;grid-column-start:3;-ms-grid-row-span:1;grid-row-end:2;-ms-grid-row:1;grid-row-start:1;-ms-grid-column-align:end;justify-self:end}.tox .tox-notification .tox-progress-bar{-ms-grid-column-span:3;grid-column-end:4;-ms-grid-column:1;grid-column-start:1;-ms-grid-row-span:1;grid-row-end:3;-ms-grid-row:2;grid-row-start:2;-ms-grid-column-align:center;justify-self:center}.tox .tox-pop{display:inline-block;position:relative}.tox .tox-pop--resizing{transition:width .1s ease}.tox .tox-pop--resizing .tox-toolbar,.tox .tox-pop--resizing .tox-toolbar__group{flex-wrap:nowrap}.tox .tox-pop--transition{transition:.15s ease;transition-property:left,right,top,bottom}.tox .tox-pop--transition::after,.tox .tox-pop--transition::before{transition:all .15s,visibility 0s,opacity 75ms ease 75ms}.tox .tox-pop__dialog{background-color:#222f3e;border:1px solid #000;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,.15);min-width:0;overflow:hidden}.tox .tox-pop__dialog>:not(.tox-toolbar){margin:4px 4px 4px 8px}.tox .tox-pop__dialog .tox-toolbar{background-color:transparent;margin-bottom:-1px}.tox .tox-pop::after,.tox .tox-pop::before{border-style:solid;content:'';display:block;height:0;opacity:1;position:absolute;width:0}.tox .tox-pop.tox-pop--inset::after,.tox .tox-pop.tox-pop--inset::before{opacity:0;transition:all 0s .15s,visibility 0s,opacity 75ms ease}.tox .tox-pop.tox-pop--bottom::after,.tox .tox-pop.tox-pop--bottom::before{left:50%;top:100%}.tox .tox-pop.tox-pop--bottom::after{border-color:#222f3e transparent transparent transparent;border-width:8px;margin-left:-8px;margin-top:-1px}.tox .tox-pop.tox-pop--bottom::before{border-color:#000 transparent transparent transparent;border-width:9px;margin-left:-9px}.tox .tox-pop.tox-pop--top::after,.tox .tox-pop.tox-pop--top::before{left:50%;top:0;transform:translateY(-100%)}.tox .tox-pop.tox-pop--top::after{border-color:transparent transparent #222f3e transparent;border-width:8px;margin-left:-8px;margin-top:1px}.tox .tox-pop.tox-pop--top::before{border-color:transparent transparent #000 transparent;border-width:9px;margin-left:-9px}.tox .tox-pop.tox-pop--left::after,.tox .tox-pop.tox-pop--left::before{left:0;top:calc(50% - 1px);transform:translateY(-50%)}.tox .tox-pop.tox-pop--left::after{border-color:transparent #222f3e transparent transparent;border-width:8px;margin-left:-15px}.tox .tox-pop.tox-pop--left::before{border-color:transparent #000 transparent transparent;border-width:10px;margin-left:-19px}.tox .tox-pop.tox-pop--right::after,.tox .tox-pop.tox-pop--right::before{left:100%;top:calc(50% + 1px);transform:translateY(-50%)}.tox .tox-pop.tox-pop--right::after{border-color:transparent transparent transparent #222f3e;border-width:8px;margin-left:-1px}.tox .tox-pop.tox-pop--right::before{border-color:transparent transparent transparent #000;border-width:10px;margin-left:-1px}.tox .tox-pop.tox-pop--align-left::after,.tox .tox-pop.tox-pop--align-left::before{left:20px}.tox .tox-pop.tox-pop--align-right::after,.tox .tox-pop.tox-pop--align-right::before{left:calc(100% - 20px)}.tox .tox-sidebar-wrap{display:flex;flex-direction:row;flex-grow:1;-ms-flex-preferred-size:0;min-height:0}.tox .tox-sidebar{background-color:#222f3e;display:flex;flex-direction:row;justify-content:flex-end}.tox .tox-sidebar__slider{display:flex;overflow:hidden}.tox .tox-sidebar__pane-container{display:flex}.tox .tox-sidebar__pane{display:flex}.tox .tox-sidebar--sliding-closed{opacity:0}.tox .tox-sidebar--sliding-open{opacity:1}.tox .tox-sidebar--sliding-growing,.tox .tox-sidebar--sliding-shrinking{transition:width .5s ease,opacity .5s ease}.tox .tox-selector{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;display:inline-block;height:10px;position:absolute;width:10px}.tox.tox-platform-touch .tox-selector{height:12px;width:12px}.tox .tox-slider{align-items:center;display:flex;flex:1;-ms-flex-preferred-size:auto;height:24px;justify-content:center;position:relative}.tox .tox-slider__rail{background-color:transparent;border:1px solid #000;border-radius:3px;height:10px;min-width:120px;width:100%}.tox .tox-slider__handle{background-color:#207ab7;border:2px solid #185d8c;border-radius:3px;box-shadow:none;height:24px;left:50%;position:absolute;top:50%;transform:translateX(-50%) translateY(-50%);width:14px}.tox .tox-source-code{overflow:auto}.tox .tox-spinner{display:flex}.tox .tox-spinner>div{animation:tam-bouncing-dots 1.5s ease-in-out 0s infinite both;background-color:rgba(255,255,255,.5);border-radius:100%;height:8px;width:8px}.tox .tox-spinner>div:nth-child(1){animation-delay:-.32s}.tox .tox-spinner>div:nth-child(2){animation-delay:-.16s}@keyframes tam-bouncing-dots{0%,100%,80%{transform:scale(0)}40%{transform:scale(1)}}.tox:not([dir=rtl]) .tox-spinner>div:not(:first-child){margin-left:4px}.tox[dir=rtl] .tox-spinner>div:not(:first-child){margin-right:4px}.tox .tox-statusbar{align-items:center;background-color:#222f3e;border-top:1px solid #000;color:#fff;display:flex;flex:0 0 auto;font-size:12px;font-weight:400;height:18px;overflow:hidden;padding:0 8px;position:relative;text-transform:uppercase}.tox .tox-statusbar__text-container{display:flex;flex:1 1 auto;justify-content:flex-end;overflow:hidden}.tox .tox-statusbar__path{display:flex;flex:1 1 auto;margin-right:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tox .tox-statusbar__path>*{display:inline;white-space:nowrap}.tox .tox-statusbar__wordcount{flex:0 0 auto;margin-left:1ch}.tox .tox-statusbar a,.tox .tox-statusbar__path-item,.tox .tox-statusbar__wordcount{color:#fff;text-decoration:none}.tox .tox-statusbar a:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar a:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:hover:not(:disabled):not([aria-disabled=true]){cursor:pointer;text-decoration:underline}.tox .tox-statusbar__resize-handle{align-items:flex-end;align-self:stretch;cursor:nwse-resize;display:flex;flex:0 0 auto;justify-content:flex-end;margin-left:auto;margin-right:-8px;padding-left:1ch}.tox .tox-statusbar__resize-handle svg{display:block;fill:#fff}.tox .tox-statusbar__resize-handle:focus svg{background-color:#4a5562;border-radius:1px;box-shadow:0 0 0 2px #4a5562}.tox:not([dir=rtl]) .tox-statusbar__path>*{margin-right:4px}.tox:not([dir=rtl]) .tox-statusbar__branding{margin-left:1ch}.tox[dir=rtl] .tox-statusbar{flex-direction:row-reverse}.tox[dir=rtl] .tox-statusbar__path>*{margin-left:4px}.tox .tox-throbber{z-index:1299}.tox .tox-throbber__busy-spinner{align-items:center;background-color:rgba(34,47,62,.6);bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0}.tox .tox-tbtn{align-items:center;background:0 0;border:0;border-radius:3px;box-shadow:none;color:#fff;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:34px;justify-content:center;margin:2px 0 3px 0;outline:0;overflow:hidden;padding:0;text-transform:none;width:34px}.tox .tox-tbtn svg{display:block;fill:#fff}.tox .tox-tbtn.tox-tbtn-more{padding-left:5px;padding-right:5px;width:inherit}.tox .tox-tbtn:focus{background:#4a5562;border:0;box-shadow:none}.tox .tox-tbtn:hover{background:#4a5562;border:0;box-shadow:none;color:#fff}.tox .tox-tbtn:hover svg{fill:#fff}.tox .tox-tbtn:active{background:#757d87;border:0;box-shadow:none;color:#fff}.tox .tox-tbtn:active svg{fill:#fff}.tox .tox-tbtn--disabled,.tox .tox-tbtn--disabled:hover,.tox .tox-tbtn:disabled,.tox .tox-tbtn:disabled:hover{background:0 0;border:0;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-tbtn--disabled svg,.tox .tox-tbtn--disabled:hover svg,.tox .tox-tbtn:disabled svg,.tox .tox-tbtn:disabled:hover svg{fill:rgba(255,255,255,.5)}.tox .tox-tbtn--enabled,.tox .tox-tbtn--enabled:hover{background:#757d87;border:0;box-shadow:none;color:#fff}.tox .tox-tbtn--enabled:hover>*,.tox .tox-tbtn--enabled>*{transform:none}.tox .tox-tbtn--enabled svg,.tox .tox-tbtn--enabled:hover svg{fill:#fff}.tox .tox-tbtn:focus:not(.tox-tbtn--disabled){color:#fff}.tox .tox-tbtn:focus:not(.tox-tbtn--disabled) svg{fill:#fff}.tox .tox-tbtn:active>*{transform:none}.tox .tox-tbtn--md{height:51px;width:51px}.tox .tox-tbtn--lg{flex-direction:column;height:68px;width:68px}.tox .tox-tbtn--return{-ms-grid-row-align:stretch;align-self:stretch;height:unset;width:16px}.tox .tox-tbtn--labeled{padding:0 4px;width:unset}.tox .tox-tbtn__vlabel{display:block;font-size:10px;font-weight:400;letter-spacing:-.025em;margin-bottom:4px;white-space:nowrap}.tox .tox-tbtn--select{margin:2px 0 3px 0;padding:0 4px;width:auto}.tox .tox-tbtn__select-label{cursor:default;font-weight:400;margin:0 4px}.tox .tox-tbtn__select-chevron{align-items:center;display:flex;justify-content:center;width:16px}.tox .tox-tbtn__select-chevron svg{fill:rgba(255,255,255,.5)}.tox .tox-tbtn--bespoke .tox-tbtn__select-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:7em}.tox .tox-split-button{border:0;border-radius:3px;box-sizing:border-box;display:flex;margin:2px 0 3px 0;overflow:hidden}.tox .tox-split-button:hover{box-shadow:0 0 0 1px #4a5562 inset}.tox .tox-split-button:focus{background:#4a5562;box-shadow:none;color:#fff}.tox .tox-split-button>*{border-radius:0}.tox .tox-split-button__chevron{width:16px}.tox .tox-split-button__chevron svg{fill:rgba(255,255,255,.5)}.tox .tox-split-button .tox-tbtn{margin:0}.tox.tox-platform-touch .tox-split-button .tox-tbtn:first-child{width:30px}.tox.tox-platform-touch .tox-split-button__chevron{width:20px}.tox .tox-split-button.tox-tbtn--disabled .tox-tbtn:focus,.tox .tox-split-button.tox-tbtn--disabled .tox-tbtn:hover,.tox .tox-split-button.tox-tbtn--disabled:focus,.tox .tox-split-button.tox-tbtn--disabled:hover{background:0 0;box-shadow:none;color:rgba(255,255,255,.5)}.tox .tox-toolbar-overlord{background-color:#222f3e}.tox .tox-toolbar,.tox .tox-toolbar__overflow,.tox .tox-toolbar__primary{background:url("data:image/svg+xml;charset=utf8,%3Csvg height='39px' viewBox='0 0 40 39px' width='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='38px' width='100' height='1' fill='%23000000'/%3E%3C/svg%3E") left 0 top 0 #222f3e;background-color:#222f3e;display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:wrap;padding:0 0}.tox .tox-toolbar__overflow.tox-toolbar__overflow--closed{height:0;opacity:0;padding-bottom:0;padding-top:0;visibility:hidden}.tox .tox-toolbar__overflow--growing{transition:height .3s ease,opacity .2s linear .1s}.tox .tox-toolbar__overflow--shrinking{transition:opacity .3s ease,height .2s linear .1s,visibility 0s linear .3s}.tox .tox-menubar+.tox-toolbar,.tox .tox-menubar+.tox-toolbar-overlord .tox-toolbar__primary{border-top:1px solid #000;margin-top:-1px}.tox .tox-toolbar--scrolling{flex-wrap:nowrap;overflow-x:auto}.tox .tox-pop .tox-toolbar{border-width:0}.tox .tox-toolbar--no-divider{background-image:none}.tox-tinymce:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-toolbar-overlord:first-child .tox-toolbar__primary,.tox-tinymce:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-toolbar:first-child{border-top:1px solid #000}.tox.tox-tinymce-aux .tox-toolbar__overflow{background-color:#222f3e;border:1px solid #000;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,.15)}.tox .tox-toolbar__group{align-items:center;display:flex;flex-wrap:wrap;margin:0 0;padding:0 4px 0 4px}.tox .tox-toolbar__group--pull-right{margin-left:auto}.tox .tox-toolbar--scrolling .tox-toolbar__group{flex-shrink:0;flex-wrap:nowrap}.tox:not([dir=rtl]) .tox-toolbar__group:not(:last-of-type){border-right:1px solid #000}.tox[dir=rtl] .tox-toolbar__group:not(:last-of-type){border-left:1px solid #000}.tox .tox-tooltip{display:inline-block;padding:8px;position:relative}.tox .tox-tooltip__body{background-color:#3d546f;border-radius:3px;box-shadow:0 2px 4px rgba(42,55,70,.3);color:rgba(255,255,255,.75);font-size:14px;font-style:normal;font-weight:400;padding:4px 8px;text-transform:none}.tox .tox-tooltip__arrow{position:absolute}.tox .tox-tooltip--down .tox-tooltip__arrow{border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid #3d546f;bottom:0;left:50%;position:absolute;transform:translateX(-50%)}.tox .tox-tooltip--up .tox-tooltip__arrow{border-bottom:8px solid #3d546f;border-left:8px solid transparent;border-right:8px solid transparent;left:50%;position:absolute;top:0;transform:translateX(-50%)}.tox .tox-tooltip--right .tox-tooltip__arrow{border-bottom:8px solid transparent;border-left:8px solid #3d546f;border-top:8px solid transparent;position:absolute;right:0;top:50%;transform:translateY(-50%)}.tox .tox-tooltip--left .tox-tooltip__arrow{border-bottom:8px solid transparent;border-right:8px solid #3d546f;border-top:8px solid transparent;left:0;position:absolute;top:50%;transform:translateY(-50%)}.tox .tox-well{border:1px solid #000;border-radius:3px;padding:8px;width:100%}.tox .tox-well>:first-child{margin-top:0}.tox .tox-well>:last-child{margin-bottom:0}.tox .tox-well>:only-child{margin:0}.tox .tox-custom-editor{border:1px solid #000;border-radius:3px;display:flex;flex:1;position:relative}.tox .tox-dialog-loading::before{background-color:rgba(0,0,0,.5);content:"";height:100%;position:absolute;width:100%;z-index:1000}.tox .tox-tab{cursor:pointer}.tox .tox-dialog__content-js{display:flex;flex:1;-ms-flex-preferred-size:auto}.tox .tox-dialog__body-content .tox-collection{display:flex;flex:1;-ms-flex-preferred-size:auto}.tox .tox-image-tools-edit-panel{height:60px}.tox .tox-image-tools__sidebar{height:60px}
diff --git a/public/resource/tinymce/skins/ui/oxide-dark/skin.mobile.min.css b/public/resource/tinymce/skins/ui/oxide-dark/skin.mobile.min.css
new file mode 100644
index 0000000..3a45cac
--- /dev/null
+++ b/public/resource/tinymce/skins/ui/oxide-dark/skin.mobile.min.css
@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+.tinymce-mobile-outer-container{all:initial;display:block}.tinymce-mobile-outer-container *{border:0;box-sizing:initial;cursor:inherit;float:none;line-height:1;margin:0;outline:0;padding:0;-webkit-tap-highlight-color:transparent;text-shadow:none;white-space:nowrap}.tinymce-mobile-icon-arrow-back::before{content:"\e5cd"}.tinymce-mobile-icon-image::before{content:"\e412"}.tinymce-mobile-icon-cancel-circle::before{content:"\e5c9"}.tinymce-mobile-icon-full-dot::before{content:"\e061"}.tinymce-mobile-icon-align-center::before{content:"\e234"}.tinymce-mobile-icon-align-left::before{content:"\e236"}.tinymce-mobile-icon-align-right::before{content:"\e237"}.tinymce-mobile-icon-bold::before{content:"\e238"}.tinymce-mobile-icon-italic::before{content:"\e23f"}.tinymce-mobile-icon-unordered-list::before{content:"\e241"}.tinymce-mobile-icon-ordered-list::before{content:"\e242"}.tinymce-mobile-icon-font-size::before{content:"\e245"}.tinymce-mobile-icon-underline::before{content:"\e249"}.tinymce-mobile-icon-link::before{content:"\e157"}.tinymce-mobile-icon-unlink::before{content:"\eca2"}.tinymce-mobile-icon-color::before{content:"\e891"}.tinymce-mobile-icon-previous::before{content:"\e314"}.tinymce-mobile-icon-next::before{content:"\e315"}.tinymce-mobile-icon-large-font::before,.tinymce-mobile-icon-style-formats::before{content:"\e264"}.tinymce-mobile-icon-undo::before{content:"\e166"}.tinymce-mobile-icon-redo::before{content:"\e15a"}.tinymce-mobile-icon-removeformat::before{content:"\e239"}.tinymce-mobile-icon-small-font::before{content:"\e906"}.tinymce-mobile-format-matches::after,.tinymce-mobile-icon-readonly-back::before{content:"\e5ca"}.tinymce-mobile-icon-small-heading::before{content:"small"}.tinymce-mobile-icon-large-heading::before{content:"large"}.tinymce-mobile-icon-large-heading::before,.tinymce-mobile-icon-small-heading::before{font-family:sans-serif;font-size:80%}.tinymce-mobile-mask-edit-icon::before{content:"\e254"}.tinymce-mobile-icon-back::before{content:"\e5c4"}.tinymce-mobile-icon-heading::before{content:"Headings";font-family:sans-serif;font-size:80%;font-weight:700}.tinymce-mobile-icon-h1::before{content:"H1";font-weight:700}.tinymce-mobile-icon-h2::before{content:"H2";font-weight:700}.tinymce-mobile-icon-h3::before{content:"H3";font-weight:700}.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask{align-items:center;display:flex;justify-content:center;background:rgba(51,51,51,.5);height:100%;position:absolute;top:0;width:100%}.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container{align-items:center;border-radius:50%;display:flex;flex-direction:column;font-family:sans-serif;font-size:1em;justify-content:space-between}.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .mixin-menu-item{align-items:center;display:flex;justify-content:center;border-radius:50%;height:2.1em;width:2.1em}.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section{align-items:center;display:flex;justify-content:center;flex-direction:column;font-size:1em}@media only screen and (min-device-width:700px){.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section{font-size:1.2em}}.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section .tinymce-mobile-mask-tap-icon{align-items:center;display:flex;justify-content:center;border-radius:50%;height:2.1em;width:2.1em;background-color:#fff;color:#207ab7}.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section .tinymce-mobile-mask-tap-icon::before{content:"\e900";font-family:tinymce-mobile,sans-serif}.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section:not(.tinymce-mobile-mask-tap-icon-selected) .tinymce-mobile-mask-tap-icon{z-index:2}.tinymce-mobile-android-container.tinymce-mobile-android-maximized{background:#fff;border:none;bottom:0;display:flex;flex-direction:column;left:0;position:fixed;right:0;top:0}.tinymce-mobile-android-container:not(.tinymce-mobile-android-maximized){position:relative}.tinymce-mobile-android-container .tinymce-mobile-editor-socket{display:flex;flex-grow:1}.tinymce-mobile-android-container .tinymce-mobile-editor-socket iframe{display:flex!important;flex-grow:1;height:auto!important}.tinymce-mobile-android-scroll-reload{overflow:hidden}:not(.tinymce-mobile-readonly-mode)>.tinymce-mobile-android-selection-context-toolbar{margin-top:23px}.tinymce-mobile-toolstrip{background:#fff;display:flex;flex:0 0 auto;z-index:1}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar{align-items:center;background-color:#fff;border-bottom:1px solid #ccc;display:flex;flex:1;height:2.5em;width:100%}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group{align-items:center;display:flex;height:100%;flex-shrink:1}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group>div{align-items:center;display:flex;height:100%;flex:1}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group.tinymce-mobile-exit-container{background:#f44336}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group.tinymce-mobile-toolbar-scrollable-group{flex-grow:1}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item{padding-left:.5em;padding-right:.5em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item.tinymce-mobile-toolbar-button{align-items:center;display:flex;height:80%;margin-left:2px;margin-right:2px}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item.tinymce-mobile-toolbar-button.tinymce-mobile-toolbar-button-selected{background:#c8cbcf;color:#ccc}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group:first-of-type,.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group:last-of-type{background:#207ab7;color:#eceff1}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group{align-items:center;display:flex;height:100%;flex:1;padding-bottom:.4em;padding-top:.4em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog{display:flex;min-height:1.5em;overflow:hidden;padding-left:0;padding-right:0;position:relative;width:100%}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain{display:flex;height:100%;transition:left cubic-bezier(.4,0,1,1) .15s;width:100%}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen{display:flex;flex:0 0 auto;justify-content:space-between;width:100%}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen input{font-family:Sans-serif}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-input-container{display:flex;flex-grow:1;position:relative}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-input-container .tinymce-mobile-input-container-x{-ms-grid-row-align:center;align-self:center;background:inherit;border:none;border-radius:50%;color:#888;font-size:.6em;font-weight:700;height:100%;padding-right:2px;position:absolute;right:0}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-input-container.tinymce-mobile-input-container-empty .tinymce-mobile-input-container-x{display:none}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-next,.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-previous{align-items:center;display:flex}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-next::before,.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-previous::before{align-items:center;display:flex;font-weight:700;height:100%;padding-left:.5em;padding-right:.5em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-next.tinymce-mobile-toolbar-navigation-disabled::before,.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-previous.tinymce-mobile-toolbar-navigation-disabled::before{visibility:hidden}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-dot-item{color:#ccc;font-size:10px;line-height:10px;margin:0 2px;padding-top:3px}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-dot-item.tinymce-mobile-dot-active{color:#c8cbcf}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-icon-large-font::before,.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-icon-large-heading::before{margin-left:.5em;margin-right:.9em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-icon-small-font::before,.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-icon-small-heading::before{margin-left:.9em;margin-right:.5em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider{display:flex;flex:1;margin-left:0;margin-right:0;padding:.28em 0;position:relative}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider .tinymce-mobile-slider-size-container{align-items:center;display:flex;flex-grow:1;height:100%}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider .tinymce-mobile-slider-size-container .tinymce-mobile-slider-size-line{background:#ccc;display:flex;flex:1;height:.2em;margin-bottom:.3em;margin-top:.3em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container{padding-left:2em;padding-right:2em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container .tinymce-mobile-slider-gradient-container{align-items:center;display:flex;flex-grow:1;height:100%}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container .tinymce-mobile-slider-gradient-container .tinymce-mobile-slider-gradient{background:linear-gradient(to right,red 0,#feff00 17%,#0f0 33%,#00feff 50%,#00f 67%,#ff00fe 83%,red 100%);display:flex;flex:1;height:.2em;margin-bottom:.3em;margin-top:.3em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container .tinymce-mobile-hue-slider-black{background:#000;height:.2em;margin-bottom:.3em;margin-top:.3em;width:1.2em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container .tinymce-mobile-hue-slider-white{background:#fff;height:.2em;margin-bottom:.3em;margin-top:.3em;width:1.2em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider .tinymce-mobile-slider-thumb{align-items:center;background-clip:padding-box;background-color:#455a64;border:.5em solid rgba(136,136,136,0);border-radius:3em;bottom:0;color:#fff;display:flex;height:.5em;justify-content:center;left:-10px;margin:auto;position:absolute;top:0;transition:border 120ms cubic-bezier(.39,.58,.57,1);width:.5em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider .tinymce-mobile-slider-thumb.tinymce-mobile-thumb-active{border:.5em solid rgba(136,136,136,.39)}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serializer-wrapper,.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group>div{align-items:center;display:flex;height:100%;flex:1}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serializer-wrapper{flex-direction:column;justify-content:center}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item{align-items:center;display:flex}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item:not(.tinymce-mobile-serialised-dialog){height:100%}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-dot-container{display:flex}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group input{background:#fff;border:none;border-radius:0;color:#455a64;flex-grow:1;font-size:.85em;padding-bottom:.1em;padding-left:5px;padding-top:.1em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group input::-webkit-input-placeholder{color:#888}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group input::placeholder{color:#888}.tinymce-mobile-dropup{background:#fff;display:flex;overflow:hidden;width:100%}.tinymce-mobile-dropup.tinymce-mobile-dropup-shrinking{transition:height .3s ease-out}.tinymce-mobile-dropup.tinymce-mobile-dropup-growing{transition:height .3s ease-in}.tinymce-mobile-dropup.tinymce-mobile-dropup-closed{flex-grow:0}.tinymce-mobile-dropup.tinymce-mobile-dropup-open:not(.tinymce-mobile-dropup-growing){flex-grow:1}.tinymce-mobile-ios-container .tinymce-mobile-dropup:not(.tinymce-mobile-dropup-closed){min-height:200px}@media only screen and (orientation:landscape){.tinymce-mobile-dropup:not(.tinymce-mobile-dropup-closed){min-height:200px}}@media only screen and (min-device-width :320px) and (max-device-width :568px) and (orientation :landscape){.tinymce-mobile-ios-container .tinymce-mobile-dropup:not(.tinymce-mobile-dropup-closed){min-height:150px}}.tinymce-mobile-styles-menu{font-family:sans-serif;outline:4px solid #000;overflow:hidden;position:relative;width:100%}.tinymce-mobile-styles-menu [role=menu]{display:flex;flex-direction:column;height:100%;position:absolute;width:100%}.tinymce-mobile-styles-menu [role=menu].transitioning{transition:transform .5s ease-in-out}.tinymce-mobile-styles-menu .tinymce-mobile-styles-item{border-bottom:1px solid #ddd;color:#455a64;cursor:pointer;display:flex;padding:1em 1em;position:relative}.tinymce-mobile-styles-menu .tinymce-mobile-styles-collapser .tinymce-mobile-styles-collapse-icon::before{color:#455a64;content:"\e314";font-family:tinymce-mobile,sans-serif}.tinymce-mobile-styles-menu .tinymce-mobile-styles-item.tinymce-mobile-styles-item-is-menu::after{color:#455a64;content:"\e315";font-family:tinymce-mobile,sans-serif;padding-left:1em;padding-right:1em;position:absolute;right:0}.tinymce-mobile-styles-menu .tinymce-mobile-styles-item.tinymce-mobile-format-matches::after{font-family:tinymce-mobile,sans-serif;padding-left:1em;padding-right:1em;position:absolute;right:0}.tinymce-mobile-styles-menu .tinymce-mobile-styles-collapser,.tinymce-mobile-styles-menu .tinymce-mobile-styles-separator{align-items:center;background:#fff;border-top:#455a64;color:#455a64;display:flex;min-height:2.5em;padding-left:1em;padding-right:1em}.tinymce-mobile-styles-menu [data-transitioning-destination=before][data-transitioning-state],.tinymce-mobile-styles-menu [data-transitioning-state=before]{transform:translate(-100%)}.tinymce-mobile-styles-menu [data-transitioning-destination=current][data-transitioning-state],.tinymce-mobile-styles-menu [data-transitioning-state=current]{transform:translate(0)}.tinymce-mobile-styles-menu [data-transitioning-destination=after][data-transitioning-state],.tinymce-mobile-styles-menu [data-transitioning-state=after]{transform:translate(100%)}@font-face{font-family:tinymce-mobile;font-style:normal;font-weight:400;src:url(fonts/tinymce-mobile.woff?8x92w3) format('woff')}@media (min-device-width:700px){.tinymce-mobile-outer-container,.tinymce-mobile-outer-container input{font-size:25px}}@media (max-device-width:700px){.tinymce-mobile-outer-container,.tinymce-mobile-outer-container input{font-size:18px}}.tinymce-mobile-icon{font-family:tinymce-mobile,sans-serif}.mixin-flex-and-centre{align-items:center;display:flex;justify-content:center}.mixin-flex-bar{align-items:center;display:flex;height:100%}.tinymce-mobile-outer-container .tinymce-mobile-editor-socket iframe{background-color:#fff;width:100%}.tinymce-mobile-editor-socket .tinymce-mobile-mask-edit-icon{background-color:#207ab7;border-radius:50%;bottom:1em;color:#fff;font-size:1em;height:2.1em;position:fixed;right:2em;width:2.1em;align-items:center;display:flex;justify-content:center}@media only screen and (min-device-width:700px){.tinymce-mobile-editor-socket .tinymce-mobile-mask-edit-icon{font-size:1.2em}}.tinymce-mobile-outer-container:not(.tinymce-mobile-fullscreen-maximized) .tinymce-mobile-editor-socket{height:300px;overflow:hidden}.tinymce-mobile-outer-container:not(.tinymce-mobile-fullscreen-maximized) .tinymce-mobile-editor-socket iframe{height:100%}.tinymce-mobile-outer-container:not(.tinymce-mobile-fullscreen-maximized) .tinymce-mobile-toolstrip{display:none}input[type=file]::-webkit-file-upload-button{display:none}@media only screen and (min-device-width :320px) and (max-device-width :568px) and (orientation :landscape){.tinymce-mobile-ios-container .tinymce-mobile-editor-socket .tinymce-mobile-mask-edit-icon{bottom:50%}}
diff --git a/public/resource/tinymce/skins/ui/oxide-dark/skin.shadowdom.min.css b/public/resource/tinymce/skins/ui/oxide-dark/skin.shadowdom.min.css
new file mode 100644
index 0000000..a0893b9
--- /dev/null
+++ b/public/resource/tinymce/skins/ui/oxide-dark/skin.shadowdom.min.css
@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+body.tox-dialog__disable-scroll{overflow:hidden}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;-ms-scroll-chaining:none;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}
diff --git a/public/resource/tinymce/skins/ui/oxide/content.inline.min.css b/public/resource/tinymce/skins/ui/oxide/content.inline.min.css
new file mode 100644
index 0000000..b4ab9a3
--- /dev/null
+++ b/public/resource/tinymce/skins/ui/oxide/content.inline.min.css
@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+.mce-content-body .mce-item-anchor{background:transparent url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'8'%20height%3D'12'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20d%3D'M0%200L8%200%208%2012%204.09117821%209%200%2012z'%2F%3E%3C%2Fsvg%3E%0A") no-repeat center;cursor:default;display:inline-block;height:12px!important;padding:0 2px;-webkit-user-modify:read-only;-moz-user-modify:read-only;-webkit-user-select:all;-moz-user-select:all;-ms-user-select:all;user-select:all;width:8px!important}.mce-content-body .mce-item-anchor[data-mce-selected]{outline-offset:1px}.tox-comments-visible .tox-comment{background-color:#fff0b7}.tox-comments-visible .tox-comment--active{background-color:#ffe168}.tox-checklist>li:not(.tox-checklist--hidden){list-style:none;margin:.25em 0}.tox-checklist>li:not(.tox-checklist--hidden)::before{content:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-unchecked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2215%22%20height%3D%2215%22%20x%3D%22.5%22%20y%3D%22.5%22%20fill-rule%3D%22nonzero%22%20stroke%3D%22%234C4C4C%22%20rx%3D%222%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A");cursor:pointer;height:1em;margin-left:-1.5em;margin-top:.125em;position:absolute;width:1em}.tox-checklist li:not(.tox-checklist--hidden).tox-checklist--checked::before{content:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-checked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%234099FF%22%20fill-rule%3D%22nonzero%22%20rx%3D%222%22%2F%3E%3Cpath%20id%3D%22Path%22%20fill%3D%22%23FFF%22%20fill-rule%3D%22nonzero%22%20d%3D%22M11.5703186%2C3.14417309%20C11.8516238%2C2.73724603%2012.4164781%2C2.62829933%2012.83558%2C2.89774797%20C13.260121%2C3.17069355%2013.3759736%2C3.72932262%2013.0909105%2C4.14168582%20L7.7580587%2C11.8560195%20C7.43776896%2C12.3193404%206.76483983%2C12.3852142%206.35607322%2C11.9948725%20L3.02491697%2C8.8138662%20C2.66090143%2C8.46625845%202.65798871%2C7.89594698%203.01850234%2C7.54483354%20C3.373942%2C7.19866177%203.94940006%2C7.19592841%204.30829608%2C7.5386474%20L6.85276923%2C9.9684299%20L11.5703186%2C3.14417309%20Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A")}[dir=rtl] .tox-checklist>li:not(.tox-checklist--hidden)::before{margin-left:0;margin-right:-1.5em}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;tab-size:4;-webkit-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.mce-content-body{overflow-wrap:break-word;word-wrap:break-word}.mce-content-body .mce-visual-caret{background-color:#000;background-color:currentColor;position:absolute}.mce-content-body .mce-visual-caret-hidden{display:none}.mce-content-body [data-mce-caret]{left:-1000px;margin:0;padding:0;position:absolute;right:auto;top:0}.mce-content-body .mce-offscreen-selection{left:-2000000px;max-width:1000000px;position:absolute}.mce-content-body [contentEditable=false]{cursor:default}.mce-content-body [contentEditable=true]{cursor:text}.tox-cursor-format-painter{cursor:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M15%2C6%20C15%2C5.45%2014.55%2C5%2014%2C5%20L6%2C5%20C5.45%2C5%205%2C5.45%205%2C6%20L5%2C10%20C5%2C10.55%205.45%2C11%206%2C11%20L14%2C11%20C14.55%2C11%2015%2C10.55%2015%2C10%20L15%2C9%20L16%2C9%20L16%2C12%20L9%2C12%20L9%2C19%20C9%2C19.55%209.45%2C20%2010%2C20%20L11%2C20%20C11.55%2C20%2012%2C19.55%2012%2C19%20L12%2C14%20L18%2C14%20L18%2C7%20L15%2C7%20L15%2C6%20Z%22%2F%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M1%2C1%20L8.25%2C1%20C8.66421356%2C1%209%2C1.33578644%209%2C1.75%20L9%2C1.75%20C9%2C2.16421356%208.66421356%2C2.5%208.25%2C2.5%20L2.5%2C2.5%20L2.5%2C8.25%20C2.5%2C8.66421356%202.16421356%2C9%201.75%2C9%20L1.75%2C9%20C1.33578644%2C9%201%2C8.66421356%201%2C8.25%20L1%2C1%20Z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A"),default}.mce-content-body figure.align-left{float:left}.mce-content-body figure.align-right{float:right}.mce-content-body figure.image.align-center{display:table;margin-left:auto;margin-right:auto}.mce-preview-object{border:1px solid gray;display:inline-block;line-height:0;margin:0 2px 0 2px;position:relative}.mce-preview-object .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.mce-preview-object[data-mce-selected="2"] .mce-shim{display:none}.mce-object{background:transparent url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%203h16a1%201%200%200%201%201%201v16a1%201%200%200%201-1%201H4a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1zm1%202v14h14V5H5zm4.79%202.565l5.64%204.028a.5.5%200%200%201%200%20.814l-5.64%204.028a.5.5%200%200%201-.79-.407V7.972a.5.5%200%200%201%20.79-.407z%22%2F%3E%3C%2Fsvg%3E%0A") no-repeat center;border:1px dashed #aaa}.mce-pagebreak{border:1px dashed #aaa;cursor:default;display:block;height:5px;margin-top:15px;page-break-before:always;width:100%}@media print{.mce-pagebreak{border:0}}.tiny-pageembed .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.tiny-pageembed[data-mce-selected="2"] .mce-shim{display:none}.tiny-pageembed{display:inline-block;position:relative}.tiny-pageembed--16by9,.tiny-pageembed--1by1,.tiny-pageembed--21by9,.tiny-pageembed--4by3{display:block;overflow:hidden;padding:0;position:relative;width:100%}.tiny-pageembed--21by9{padding-top:42.857143%}.tiny-pageembed--16by9{padding-top:56.25%}.tiny-pageembed--4by3{padding-top:75%}.tiny-pageembed--1by1{padding-top:100%}.tiny-pageembed--16by9 iframe,.tiny-pageembed--1by1 iframe,.tiny-pageembed--21by9 iframe,.tiny-pageembed--4by3 iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.mce-content-body[data-mce-placeholder]{position:relative}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:rgba(34,47,62,.7);content:attr(data-mce-placeholder);position:absolute}.mce-content-body:not([dir=rtl])[data-mce-placeholder]:not(.mce-visualblocks)::before{left:1px}.mce-content-body[dir=rtl][data-mce-placeholder]:not(.mce-visualblocks)::before{right:1px}.mce-content-body div.mce-resizehandle{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;height:10px;position:absolute;width:10px;z-index:1298}.mce-content-body div.mce-resizehandle:hover{background-color:#4099ff}.mce-content-body div.mce-resizehandle:nth-of-type(1){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(2){cursor:nesw-resize}.mce-content-body div.mce-resizehandle:nth-of-type(3){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(4){cursor:nesw-resize}.mce-content-body .mce-resize-backdrop{z-index:10000}.mce-content-body .mce-clonedresizable{cursor:default;opacity:.5;outline:1px dashed #000;position:absolute;z-index:10001}.mce-content-body .mce-clonedresizable.mce-resizetable-columns td,.mce-content-body .mce-clonedresizable.mce-resizetable-columns th{border:0}.mce-content-body .mce-resize-helper{background:#555;background:rgba(0,0,0,.75);border:1px;border-radius:3px;color:#fff;display:none;font-family:sans-serif;font-size:12px;line-height:14px;margin:5px 10px;padding:5px;position:absolute;white-space:nowrap;z-index:10002}.tox-rtc-user-selection{position:relative}.tox-rtc-user-cursor{bottom:0;cursor:default;position:absolute;top:0;width:2px}.tox-rtc-user-cursor::before{background-color:inherit;border-radius:50%;content:'';display:block;height:8px;position:absolute;right:-3px;top:-3px;width:8px}.tox-rtc-user-cursor:hover::after{background-color:inherit;border-radius:100px;box-sizing:border-box;color:#fff;content:attr(data-user);display:block;font-size:12px;font-weight:700;left:-5px;min-height:8px;min-width:8px;padding:0 12px;position:absolute;top:-11px;white-space:nowrap;z-index:1000}.tox-rtc-user-selection--1 .tox-rtc-user-cursor{background-color:#2dc26b}.tox-rtc-user-selection--2 .tox-rtc-user-cursor{background-color:#e03e2d}.tox-rtc-user-selection--3 .tox-rtc-user-cursor{background-color:#f1c40f}.tox-rtc-user-selection--4 .tox-rtc-user-cursor{background-color:#3598db}.tox-rtc-user-selection--5 .tox-rtc-user-cursor{background-color:#b96ad9}.tox-rtc-user-selection--6 .tox-rtc-user-cursor{background-color:#e67e23}.tox-rtc-user-selection--7 .tox-rtc-user-cursor{background-color:#aaa69d}.tox-rtc-user-selection--8 .tox-rtc-user-cursor{background-color:#f368e0}.tox-rtc-remote-image{background:#eaeaea url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2236%22%20height%3D%2212%22%20viewBox%3D%220%200%2036%2012%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Ccircle%20cx%3D%226%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2218%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.33s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2230%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.66s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A") no-repeat center center;border:1px solid #ccc;min-height:240px;min-width:320px}.mce-match-marker{background:#aaa;color:#fff}.mce-match-marker-selected{background:#39f;color:#fff}.mce-match-marker-selected::-moz-selection{background:#39f;color:#fff}.mce-match-marker-selected::selection{background:#39f;color:#fff}.mce-content-body audio[data-mce-selected],.mce-content-body embed[data-mce-selected],.mce-content-body img[data-mce-selected],.mce-content-body object[data-mce-selected],.mce-content-body table[data-mce-selected],.mce-content-body video[data-mce-selected]{outline:3px solid #b4d7ff}.mce-content-body hr[data-mce-selected]{outline:3px solid #b4d7ff;outline-offset:1px}.mce-content-body [contentEditable=false] [contentEditable=true]:focus{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false] [contentEditable=true]:hover{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false][data-mce-selected]{cursor:not-allowed;outline:3px solid #b4d7ff}.mce-content-body.mce-content-readonly [contentEditable=true]:focus,.mce-content-body.mce-content-readonly [contentEditable=true]:hover{outline:0}.mce-content-body [data-mce-selected=inline-boundary]{background-color:#b4d7ff}.mce-content-body .mce-edit-focus{outline:3px solid #b4d7ff}.mce-content-body td[data-mce-selected],.mce-content-body th[data-mce-selected]{position:relative}.mce-content-body td[data-mce-selected]::-moz-selection,.mce-content-body th[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body td[data-mce-selected]::selection,.mce-content-body th[data-mce-selected]::selection{background:0 0}.mce-content-body td[data-mce-selected] *,.mce-content-body th[data-mce-selected] *{outline:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{background-color:rgba(180,215,255,.7);border:1px solid rgba(180,215,255,.7);bottom:-1px;content:'';left:-1px;mix-blend-mode:multiply;position:absolute;right:-1px;top:-1px}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{border-color:rgba(0,84,180,.7)}}.mce-content-body img::-moz-selection{background:0 0}.mce-content-body img::selection{background:0 0}.ephox-snooker-resizer-bar{background-color:#b4d7ff;opacity:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:1}.mce-spellchecker-word{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%23ff0000'%20fill%3D'none'%20stroke-linecap%3D'round'%20stroke-opacity%3D'.75'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default;height:2rem}.mce-spellchecker-grammar{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%2300A835'%20fill%3D'none'%20stroke-linecap%3D'round'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc li{list-style-type:none}.mce-item-table:not([border]),.mce-item-table:not([border]) caption,.mce-item-table:not([border]) td,.mce-item-table:not([border]) th,.mce-item-table[border="0"],.mce-item-table[border="0"] caption,.mce-item-table[border="0"] td,.mce-item-table[border="0"] th,table[style*="border-width: 0px"],table[style*="border-width: 0px"] caption,table[style*="border-width: 0px"] td,table[style*="border-width: 0px"] th{border:1px dashed #bbb}.mce-visualblocks address,.mce-visualblocks article,.mce-visualblocks aside,.mce-visualblocks blockquote,.mce-visualblocks div:not([data-mce-bogus]),.mce-visualblocks dl,.mce-visualblocks figcaption,.mce-visualblocks figure,.mce-visualblocks h1,.mce-visualblocks h2,.mce-visualblocks h3,.mce-visualblocks h4,.mce-visualblocks h5,.mce-visualblocks h6,.mce-visualblocks hgroup,.mce-visualblocks ol,.mce-visualblocks p,.mce-visualblocks pre,.mce-visualblocks section,.mce-visualblocks ul{background-repeat:no-repeat;border:1px dashed #bbb;margin-left:3px;padding-top:10px}.mce-visualblocks p{background-image:url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7)}.mce-visualblocks h1{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==)}.mce-visualblocks h2{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==)}.mce-visualblocks h3{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7)}.mce-visualblocks h4{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==)}.mce-visualblocks h5{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==)}.mce-visualblocks h6{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==)}.mce-visualblocks div:not([data-mce-bogus]){background-image:url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7)}.mce-visualblocks section{background-image:url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=)}.mce-visualblocks article{background-image:url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7)}.mce-visualblocks blockquote{background-image:url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7)}.mce-visualblocks address{background-image:url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=)}.mce-visualblocks pre{background-image:url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==)}.mce-visualblocks figure{background-image:url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7)}.mce-visualblocks figcaption{border:1px dashed #bbb}.mce-visualblocks hgroup{background-image:url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7)}.mce-visualblocks aside{background-image:url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=)}.mce-visualblocks ul{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==)}.mce-visualblocks ol{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==)}.mce-visualblocks dl{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==)}.mce-visualblocks:not([dir=rtl]) address,.mce-visualblocks:not([dir=rtl]) article,.mce-visualblocks:not([dir=rtl]) aside,.mce-visualblocks:not([dir=rtl]) blockquote,.mce-visualblocks:not([dir=rtl]) div:not([data-mce-bogus]),.mce-visualblocks:not([dir=rtl]) dl,.mce-visualblocks:not([dir=rtl]) figcaption,.mce-visualblocks:not([dir=rtl]) figure,.mce-visualblocks:not([dir=rtl]) h1,.mce-visualblocks:not([dir=rtl]) h2,.mce-visualblocks:not([dir=rtl]) h3,.mce-visualblocks:not([dir=rtl]) h4,.mce-visualblocks:not([dir=rtl]) h5,.mce-visualblocks:not([dir=rtl]) h6,.mce-visualblocks:not([dir=rtl]) hgroup,.mce-visualblocks:not([dir=rtl]) ol,.mce-visualblocks:not([dir=rtl]) p,.mce-visualblocks:not([dir=rtl]) pre,.mce-visualblocks:not([dir=rtl]) section,.mce-visualblocks:not([dir=rtl]) ul{margin-left:3px}.mce-visualblocks[dir=rtl] address,.mce-visualblocks[dir=rtl] article,.mce-visualblocks[dir=rtl] aside,.mce-visualblocks[dir=rtl] blockquote,.mce-visualblocks[dir=rtl] div:not([data-mce-bogus]),.mce-visualblocks[dir=rtl] dl,.mce-visualblocks[dir=rtl] figcaption,.mce-visualblocks[dir=rtl] figure,.mce-visualblocks[dir=rtl] h1,.mce-visualblocks[dir=rtl] h2,.mce-visualblocks[dir=rtl] h3,.mce-visualblocks[dir=rtl] h4,.mce-visualblocks[dir=rtl] h5,.mce-visualblocks[dir=rtl] h6,.mce-visualblocks[dir=rtl] hgroup,.mce-visualblocks[dir=rtl] ol,.mce-visualblocks[dir=rtl] p,.mce-visualblocks[dir=rtl] pre,.mce-visualblocks[dir=rtl] section,.mce-visualblocks[dir=rtl] ul{background-position-x:right;margin-right:3px}.mce-nbsp,.mce-shy{background:#aaa}.mce-shy::after{content:'-'}
diff --git a/public/resource/tinymce/skins/ui/oxide/content.min.css b/public/resource/tinymce/skins/ui/oxide/content.min.css
new file mode 100644
index 0000000..844858d
--- /dev/null
+++ b/public/resource/tinymce/skins/ui/oxide/content.min.css
@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+.mce-content-body .mce-item-anchor{background:transparent url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'8'%20height%3D'12'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20d%3D'M0%200L8%200%208%2012%204.09117821%209%200%2012z'%2F%3E%3C%2Fsvg%3E%0A") no-repeat center;cursor:default;display:inline-block;height:12px!important;padding:0 2px;-webkit-user-modify:read-only;-moz-user-modify:read-only;-webkit-user-select:all;-moz-user-select:all;-ms-user-select:all;user-select:all;width:8px!important}.mce-content-body .mce-item-anchor[data-mce-selected]{outline-offset:1px}.tox-comments-visible .tox-comment{background-color:#fff0b7}.tox-comments-visible .tox-comment--active{background-color:#ffe168}.tox-checklist>li:not(.tox-checklist--hidden){list-style:none;margin:.25em 0}.tox-checklist>li:not(.tox-checklist--hidden)::before{content:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-unchecked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2215%22%20height%3D%2215%22%20x%3D%22.5%22%20y%3D%22.5%22%20fill-rule%3D%22nonzero%22%20stroke%3D%22%234C4C4C%22%20rx%3D%222%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A");cursor:pointer;height:1em;margin-left:-1.5em;margin-top:.125em;position:absolute;width:1em}.tox-checklist li:not(.tox-checklist--hidden).tox-checklist--checked::before{content:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cg%20id%3D%22checklist-checked%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20id%3D%22Rectangle%22%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%234099FF%22%20fill-rule%3D%22nonzero%22%20rx%3D%222%22%2F%3E%3Cpath%20id%3D%22Path%22%20fill%3D%22%23FFF%22%20fill-rule%3D%22nonzero%22%20d%3D%22M11.5703186%2C3.14417309%20C11.8516238%2C2.73724603%2012.4164781%2C2.62829933%2012.83558%2C2.89774797%20C13.260121%2C3.17069355%2013.3759736%2C3.72932262%2013.0909105%2C4.14168582%20L7.7580587%2C11.8560195%20C7.43776896%2C12.3193404%206.76483983%2C12.3852142%206.35607322%2C11.9948725%20L3.02491697%2C8.8138662%20C2.66090143%2C8.46625845%202.65798871%2C7.89594698%203.01850234%2C7.54483354%20C3.373942%2C7.19866177%203.94940006%2C7.19592841%204.30829608%2C7.5386474%20L6.85276923%2C9.9684299%20L11.5703186%2C3.14417309%20Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E%0A")}[dir=rtl] .tox-checklist>li:not(.tox-checklist--hidden)::before{margin-left:0;margin-right:-1.5em}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;tab-size:4;-webkit-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.mce-content-body{overflow-wrap:break-word;word-wrap:break-word}.mce-content-body .mce-visual-caret{background-color:#000;background-color:currentColor;position:absolute}.mce-content-body .mce-visual-caret-hidden{display:none}.mce-content-body [data-mce-caret]{left:-1000px;margin:0;padding:0;position:absolute;right:auto;top:0}.mce-content-body .mce-offscreen-selection{left:-2000000px;max-width:1000000px;position:absolute}.mce-content-body [contentEditable=false]{cursor:default}.mce-content-body [contentEditable=true]{cursor:text}.tox-cursor-format-painter{cursor:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M15%2C6%20C15%2C5.45%2014.55%2C5%2014%2C5%20L6%2C5%20C5.45%2C5%205%2C5.45%205%2C6%20L5%2C10%20C5%2C10.55%205.45%2C11%206%2C11%20L14%2C11%20C14.55%2C11%2015%2C10.55%2015%2C10%20L15%2C9%20L16%2C9%20L16%2C12%20L9%2C12%20L9%2C19%20C9%2C19.55%209.45%2C20%2010%2C20%20L11%2C20%20C11.55%2C20%2012%2C19.55%2012%2C19%20L12%2C14%20L18%2C14%20L18%2C7%20L15%2C7%20L15%2C6%20Z%22%2F%3E%0A%20%20%20%20%3Cpath%20fill%3D%22%23000%22%20fill-rule%3D%22nonzero%22%20d%3D%22M1%2C1%20L8.25%2C1%20C8.66421356%2C1%209%2C1.33578644%209%2C1.75%20L9%2C1.75%20C9%2C2.16421356%208.66421356%2C2.5%208.25%2C2.5%20L2.5%2C2.5%20L2.5%2C8.25%20C2.5%2C8.66421356%202.16421356%2C9%201.75%2C9%20L1.75%2C9%20C1.33578644%2C9%201%2C8.66421356%201%2C8.25%20L1%2C1%20Z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A"),default}.mce-content-body figure.align-left{float:left}.mce-content-body figure.align-right{float:right}.mce-content-body figure.image.align-center{display:table;margin-left:auto;margin-right:auto}.mce-preview-object{border:1px solid gray;display:inline-block;line-height:0;margin:0 2px 0 2px;position:relative}.mce-preview-object .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.mce-preview-object[data-mce-selected="2"] .mce-shim{display:none}.mce-object{background:transparent url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%203h16a1%201%200%200%201%201%201v16a1%201%200%200%201-1%201H4a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1zm1%202v14h14V5H5zm4.79%202.565l5.64%204.028a.5.5%200%200%201%200%20.814l-5.64%204.028a.5.5%200%200%201-.79-.407V7.972a.5.5%200%200%201%20.79-.407z%22%2F%3E%3C%2Fsvg%3E%0A") no-repeat center;border:1px dashed #aaa}.mce-pagebreak{border:1px dashed #aaa;cursor:default;display:block;height:5px;margin-top:15px;page-break-before:always;width:100%}@media print{.mce-pagebreak{border:0}}.tiny-pageembed .mce-shim{background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);height:100%;left:0;position:absolute;top:0;width:100%}.tiny-pageembed[data-mce-selected="2"] .mce-shim{display:none}.tiny-pageembed{display:inline-block;position:relative}.tiny-pageembed--16by9,.tiny-pageembed--1by1,.tiny-pageembed--21by9,.tiny-pageembed--4by3{display:block;overflow:hidden;padding:0;position:relative;width:100%}.tiny-pageembed--21by9{padding-top:42.857143%}.tiny-pageembed--16by9{padding-top:56.25%}.tiny-pageembed--4by3{padding-top:75%}.tiny-pageembed--1by1{padding-top:100%}.tiny-pageembed--16by9 iframe,.tiny-pageembed--1by1 iframe,.tiny-pageembed--21by9 iframe,.tiny-pageembed--4by3 iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.mce-content-body[data-mce-placeholder]{position:relative}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before{color:rgba(34,47,62,.7);content:attr(data-mce-placeholder);position:absolute}.mce-content-body:not([dir=rtl])[data-mce-placeholder]:not(.mce-visualblocks)::before{left:1px}.mce-content-body[dir=rtl][data-mce-placeholder]:not(.mce-visualblocks)::before{right:1px}.mce-content-body div.mce-resizehandle{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;height:10px;position:absolute;width:10px;z-index:1298}.mce-content-body div.mce-resizehandle:hover{background-color:#4099ff}.mce-content-body div.mce-resizehandle:nth-of-type(1){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(2){cursor:nesw-resize}.mce-content-body div.mce-resizehandle:nth-of-type(3){cursor:nwse-resize}.mce-content-body div.mce-resizehandle:nth-of-type(4){cursor:nesw-resize}.mce-content-body .mce-resize-backdrop{z-index:10000}.mce-content-body .mce-clonedresizable{cursor:default;opacity:.5;outline:1px dashed #000;position:absolute;z-index:10001}.mce-content-body .mce-clonedresizable.mce-resizetable-columns td,.mce-content-body .mce-clonedresizable.mce-resizetable-columns th{border:0}.mce-content-body .mce-resize-helper{background:#555;background:rgba(0,0,0,.75);border:1px;border-radius:3px;color:#fff;display:none;font-family:sans-serif;font-size:12px;line-height:14px;margin:5px 10px;padding:5px;position:absolute;white-space:nowrap;z-index:10002}.tox-rtc-user-selection{position:relative}.tox-rtc-user-cursor{bottom:0;cursor:default;position:absolute;top:0;width:2px}.tox-rtc-user-cursor::before{background-color:inherit;border-radius:50%;content:'';display:block;height:8px;position:absolute;right:-3px;top:-3px;width:8px}.tox-rtc-user-cursor:hover::after{background-color:inherit;border-radius:100px;box-sizing:border-box;color:#fff;content:attr(data-user);display:block;font-size:12px;font-weight:700;left:-5px;min-height:8px;min-width:8px;padding:0 12px;position:absolute;top:-11px;white-space:nowrap;z-index:1000}.tox-rtc-user-selection--1 .tox-rtc-user-cursor{background-color:#2dc26b}.tox-rtc-user-selection--2 .tox-rtc-user-cursor{background-color:#e03e2d}.tox-rtc-user-selection--3 .tox-rtc-user-cursor{background-color:#f1c40f}.tox-rtc-user-selection--4 .tox-rtc-user-cursor{background-color:#3598db}.tox-rtc-user-selection--5 .tox-rtc-user-cursor{background-color:#b96ad9}.tox-rtc-user-selection--6 .tox-rtc-user-cursor{background-color:#e67e23}.tox-rtc-user-selection--7 .tox-rtc-user-cursor{background-color:#aaa69d}.tox-rtc-user-selection--8 .tox-rtc-user-cursor{background-color:#f368e0}.tox-rtc-remote-image{background:#eaeaea url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2236%22%20height%3D%2212%22%20viewBox%3D%220%200%2036%2012%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Ccircle%20cx%3D%226%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2218%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.33s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%20%20%3Ccircle%20cx%3D%2230%22%20cy%3D%226%22%20r%3D%223%22%20fill%3D%22rgba(0%2C%200%2C%200%2C%20.2)%22%3E%0A%20%20%20%20%3Canimate%20attributeName%3D%22r%22%20values%3D%223%3B5%3B3%22%20calcMode%3D%22linear%22%20begin%3D%22.66s%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%20%2F%3E%0A%20%20%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A") no-repeat center center;border:1px solid #ccc;min-height:240px;min-width:320px}.mce-match-marker{background:#aaa;color:#fff}.mce-match-marker-selected{background:#39f;color:#fff}.mce-match-marker-selected::-moz-selection{background:#39f;color:#fff}.mce-match-marker-selected::selection{background:#39f;color:#fff}.mce-content-body audio[data-mce-selected],.mce-content-body embed[data-mce-selected],.mce-content-body img[data-mce-selected],.mce-content-body object[data-mce-selected],.mce-content-body table[data-mce-selected],.mce-content-body video[data-mce-selected]{outline:3px solid #b4d7ff}.mce-content-body hr[data-mce-selected]{outline:3px solid #b4d7ff;outline-offset:1px}.mce-content-body [contentEditable=false] [contentEditable=true]:focus{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false] [contentEditable=true]:hover{outline:3px solid #b4d7ff}.mce-content-body [contentEditable=false][data-mce-selected]{cursor:not-allowed;outline:3px solid #b4d7ff}.mce-content-body.mce-content-readonly [contentEditable=true]:focus,.mce-content-body.mce-content-readonly [contentEditable=true]:hover{outline:0}.mce-content-body [data-mce-selected=inline-boundary]{background-color:#b4d7ff}.mce-content-body .mce-edit-focus{outline:3px solid #b4d7ff}.mce-content-body td[data-mce-selected],.mce-content-body th[data-mce-selected]{position:relative}.mce-content-body td[data-mce-selected]::-moz-selection,.mce-content-body th[data-mce-selected]::-moz-selection{background:0 0}.mce-content-body td[data-mce-selected]::selection,.mce-content-body th[data-mce-selected]::selection{background:0 0}.mce-content-body td[data-mce-selected] *,.mce-content-body th[data-mce-selected] *{outline:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{background-color:rgba(180,215,255,.7);border:1px solid rgba(180,215,255,.7);bottom:-1px;content:'';left:-1px;mix-blend-mode:multiply;position:absolute;right:-1px;top:-1px}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){.mce-content-body td[data-mce-selected]::after,.mce-content-body th[data-mce-selected]::after{border-color:rgba(0,84,180,.7)}}.mce-content-body img::-moz-selection{background:0 0}.mce-content-body img::selection{background:0 0}.ephox-snooker-resizer-bar{background-color:#b4d7ff;opacity:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:1}.mce-spellchecker-word{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%23ff0000'%20fill%3D'none'%20stroke-linecap%3D'round'%20stroke-opacity%3D'.75'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default;height:2rem}.mce-spellchecker-grammar{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'4'%20height%3D'4'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20stroke%3D'%2300A835'%20fill%3D'none'%20stroke-linecap%3D'round'%20d%3D'M0%203L2%201%204%203'%2F%3E%3C%2Fsvg%3E%0A");background-position:0 calc(100% + 1px);background-repeat:repeat-x;background-size:auto 6px;cursor:default}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc li{list-style-type:none}.mce-item-table:not([border]),.mce-item-table:not([border]) caption,.mce-item-table:not([border]) td,.mce-item-table:not([border]) th,.mce-item-table[border="0"],.mce-item-table[border="0"] caption,.mce-item-table[border="0"] td,.mce-item-table[border="0"] th,table[style*="border-width: 0px"],table[style*="border-width: 0px"] caption,table[style*="border-width: 0px"] td,table[style*="border-width: 0px"] th{border:1px dashed #bbb}.mce-visualblocks address,.mce-visualblocks article,.mce-visualblocks aside,.mce-visualblocks blockquote,.mce-visualblocks div:not([data-mce-bogus]),.mce-visualblocks dl,.mce-visualblocks figcaption,.mce-visualblocks figure,.mce-visualblocks h1,.mce-visualblocks h2,.mce-visualblocks h3,.mce-visualblocks h4,.mce-visualblocks h5,.mce-visualblocks h6,.mce-visualblocks hgroup,.mce-visualblocks ol,.mce-visualblocks p,.mce-visualblocks pre,.mce-visualblocks section,.mce-visualblocks ul{background-repeat:no-repeat;border:1px dashed #bbb;margin-left:3px;padding-top:10px}.mce-visualblocks p{background-image:url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7)}.mce-visualblocks h1{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==)}.mce-visualblocks h2{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==)}.mce-visualblocks h3{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7)}.mce-visualblocks h4{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==)}.mce-visualblocks h5{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==)}.mce-visualblocks h6{background-image:url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==)}.mce-visualblocks div:not([data-mce-bogus]){background-image:url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7)}.mce-visualblocks section{background-image:url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=)}.mce-visualblocks article{background-image:url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7)}.mce-visualblocks blockquote{background-image:url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7)}.mce-visualblocks address{background-image:url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=)}.mce-visualblocks pre{background-image:url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==)}.mce-visualblocks figure{background-image:url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7)}.mce-visualblocks figcaption{border:1px dashed #bbb}.mce-visualblocks hgroup{background-image:url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7)}.mce-visualblocks aside{background-image:url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=)}.mce-visualblocks ul{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==)}.mce-visualblocks ol{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==)}.mce-visualblocks dl{background-image:url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==)}.mce-visualblocks:not([dir=rtl]) address,.mce-visualblocks:not([dir=rtl]) article,.mce-visualblocks:not([dir=rtl]) aside,.mce-visualblocks:not([dir=rtl]) blockquote,.mce-visualblocks:not([dir=rtl]) div:not([data-mce-bogus]),.mce-visualblocks:not([dir=rtl]) dl,.mce-visualblocks:not([dir=rtl]) figcaption,.mce-visualblocks:not([dir=rtl]) figure,.mce-visualblocks:not([dir=rtl]) h1,.mce-visualblocks:not([dir=rtl]) h2,.mce-visualblocks:not([dir=rtl]) h3,.mce-visualblocks:not([dir=rtl]) h4,.mce-visualblocks:not([dir=rtl]) h5,.mce-visualblocks:not([dir=rtl]) h6,.mce-visualblocks:not([dir=rtl]) hgroup,.mce-visualblocks:not([dir=rtl]) ol,.mce-visualblocks:not([dir=rtl]) p,.mce-visualblocks:not([dir=rtl]) pre,.mce-visualblocks:not([dir=rtl]) section,.mce-visualblocks:not([dir=rtl]) ul{margin-left:3px}.mce-visualblocks[dir=rtl] address,.mce-visualblocks[dir=rtl] article,.mce-visualblocks[dir=rtl] aside,.mce-visualblocks[dir=rtl] blockquote,.mce-visualblocks[dir=rtl] div:not([data-mce-bogus]),.mce-visualblocks[dir=rtl] dl,.mce-visualblocks[dir=rtl] figcaption,.mce-visualblocks[dir=rtl] figure,.mce-visualblocks[dir=rtl] h1,.mce-visualblocks[dir=rtl] h2,.mce-visualblocks[dir=rtl] h3,.mce-visualblocks[dir=rtl] h4,.mce-visualblocks[dir=rtl] h5,.mce-visualblocks[dir=rtl] h6,.mce-visualblocks[dir=rtl] hgroup,.mce-visualblocks[dir=rtl] ol,.mce-visualblocks[dir=rtl] p,.mce-visualblocks[dir=rtl] pre,.mce-visualblocks[dir=rtl] section,.mce-visualblocks[dir=rtl] ul{background-position-x:right;margin-right:3px}.mce-nbsp,.mce-shy{background:#aaa}.mce-shy::after{content:'-'}body{font-family:sans-serif}table{border-collapse:collapse}
diff --git a/public/resource/tinymce/skins/ui/oxide/content.mobile.min.css b/public/resource/tinymce/skins/ui/oxide/content.mobile.min.css
new file mode 100644
index 0000000..35f7dc0
--- /dev/null
+++ b/public/resource/tinymce/skins/ui/oxide/content.mobile.min.css
@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+.tinymce-mobile-unfocused-selections .tinymce-mobile-unfocused-selection{background-color:green;display:inline-block;opacity:.5;position:absolute}body{-webkit-text-size-adjust:none}body img{max-width:96vw}body table img{max-width:95%}body{font-family:sans-serif}table{border-collapse:collapse}
diff --git a/public/resource/tinymce/skins/ui/oxide/fonts/tinymce-mobile.woff b/public/resource/tinymce/skins/ui/oxide/fonts/tinymce-mobile.woff
new file mode 100644
index 0000000..1e3be03
Binary files /dev/null and b/public/resource/tinymce/skins/ui/oxide/fonts/tinymce-mobile.woff differ
diff --git a/public/resource/tinymce/skins/ui/oxide/skin.min.css b/public/resource/tinymce/skins/ui/oxide/skin.min.css
new file mode 100644
index 0000000..f570b8e
--- /dev/null
+++ b/public/resource/tinymce/skins/ui/oxide/skin.min.css
@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+.tox{box-shadow:none;box-sizing:content-box;color:#222f3e;cursor:auto;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:16px;font-style:normal;font-weight:400;line-height:normal;-webkit-tap-highlight-color:transparent;text-decoration:none;text-shadow:none;text-transform:none;vertical-align:initial;white-space:normal}.tox :not(svg):not(rect){box-sizing:inherit;color:inherit;cursor:inherit;direction:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;line-height:inherit;-webkit-tap-highlight-color:inherit;text-align:inherit;text-decoration:inherit;text-shadow:inherit;text-transform:inherit;vertical-align:inherit;white-space:inherit}.tox :not(svg):not(rect){background:0 0;border:0;box-shadow:none;float:none;height:auto;margin:0;max-width:none;outline:0;padding:0;position:static;width:auto}.tox:not([dir=rtl]){direction:ltr;text-align:left}.tox[dir=rtl]{direction:rtl;text-align:right}.tox-tinymce{border:1px solid #ccc;border-radius:0;box-shadow:none;box-sizing:border-box;display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;overflow:hidden;position:relative;visibility:inherit!important}.tox-tinymce-inline{border:none;box-shadow:none}.tox-tinymce-inline .tox-editor-header{background-color:transparent;border:1px solid #ccc;border-radius:0;box-shadow:none}.tox-tinymce-aux{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;z-index:1300}.tox-tinymce :focus,.tox-tinymce-aux :focus{outline:0}button::-moz-focus-inner{border:0}.tox[dir=rtl] .tox-icon--flip svg{transform:rotateY(180deg)}.tox .accessibility-issue__header{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description{align-items:stretch;border:1px solid #ccc;border-radius:3px;display:flex;justify-content:space-between}.tox .accessibility-issue__description>div{padding-bottom:4px}.tox .accessibility-issue__description>div>div{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description>:last-child:not(:only-child){border-color:#ccc;border-style:solid}.tox .accessibility-issue__repair{margin-top:16px}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description{background-color:rgba(32,122,183,.1);border-color:rgba(32,122,183,.4);color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description>:last-child{border-color:rgba(32,122,183,.4)}.tox .tox-dialog__body-content .accessibility-issue--info .tox-form__group h2{color:#207ab7}.tox .tox-dialog__body-content .accessibility-issue--info .tox-icon svg{fill:#207ab7}.tox .tox-dialog__body-content .accessibility-issue--info a .tox-icon{color:#207ab7}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description{background-color:rgba(255,165,0,.1);border-color:rgba(255,165,0,.5);color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description>:last-child{border-color:rgba(255,165,0,.5)}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-form__group h2{color:#cc8500}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-icon svg{fill:#cc8500}.tox .tox-dialog__body-content .accessibility-issue--warn a .tox-icon{color:#cc8500}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description{background-color:rgba(204,0,0,.1);border-color:rgba(204,0,0,.4);color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description>:last-child{border-color:rgba(204,0,0,.4)}.tox .tox-dialog__body-content .accessibility-issue--error .tox-form__group h2{color:#c00}.tox .tox-dialog__body-content .accessibility-issue--error .tox-icon svg{fill:#c00}.tox .tox-dialog__body-content .accessibility-issue--error a .tox-icon{color:#c00}.tox .tox-dialog__body-content .accessibility-issue--success .accessibility-issue__description{background-color:rgba(120,171,70,.1);border-color:rgba(120,171,70,.4);color:#222f3e}.tox .tox-dialog__body-content .accessibility-issue--success .accessibility-issue__description>:last-child{border-color:rgba(120,171,70,.4)}.tox .tox-dialog__body-content .accessibility-issue--success .tox-form__group h2{color:#78ab46}.tox .tox-dialog__body-content .accessibility-issue--success .tox-icon svg{fill:#78ab46}.tox .tox-dialog__body-content .accessibility-issue--success a .tox-icon{color:#78ab46}.tox .tox-dialog__body-content .accessibility-issue__header h1,.tox .tox-dialog__body-content .tox-form__group .accessibility-issue__description h2{margin-top:0}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__header .tox-button{margin-left:4px}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__header>:nth-last-child(2){margin-left:auto}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__description{padding:4px 4px 4px 8px}.tox:not([dir=rtl]) .tox-dialog__body-content .accessibility-issue__description>:last-child{border-left-width:1px;padding-left:4px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__header .tox-button{margin-right:4px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__header>:nth-last-child(2){margin-right:auto}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__description{padding:4px 8px 4px 4px}.tox[dir=rtl] .tox-dialog__body-content .accessibility-issue__description>:last-child{border-right-width:1px;padding-right:4px}.tox .tox-anchorbar{display:flex;flex:0 0 auto}.tox .tox-bar{display:flex;flex:0 0 auto}.tox .tox-button{background-color:#207ab7;background-image:none;background-position:0 0;background-repeat:repeat;border-color:#207ab7;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#fff;cursor:pointer;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:14px;font-style:normal;font-weight:700;letter-spacing:normal;line-height:24px;margin:0;outline:0;padding:4px 16px;text-align:center;text-decoration:none;text-transform:none;white-space:nowrap}.tox .tox-button[disabled]{background-color:#207ab7;background-image:none;border-color:#207ab7;box-shadow:none;color:rgba(255,255,255,.5);cursor:not-allowed}.tox .tox-button:focus:not(:disabled){background-color:#1c6ca1;background-image:none;border-color:#1c6ca1;box-shadow:none;color:#fff}.tox .tox-button:hover:not(:disabled){background-color:#1c6ca1;background-image:none;border-color:#1c6ca1;box-shadow:none;color:#fff}.tox .tox-button:active:not(:disabled){background-color:#185d8c;background-image:none;border-color:#185d8c;box-shadow:none;color:#fff}.tox .tox-button--secondary{background-color:#f0f0f0;background-image:none;background-position:0 0;background-repeat:repeat;border-color:#f0f0f0;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;color:#222f3e;font-size:14px;font-style:normal;font-weight:700;letter-spacing:normal;outline:0;padding:4px 16px;text-decoration:none;text-transform:none}.tox .tox-button--secondary[disabled]{background-color:#f0f0f0;background-image:none;border-color:#f0f0f0;box-shadow:none;color:rgba(34,47,62,.5)}.tox .tox-button--secondary:focus:not(:disabled){background-color:#e3e3e3;background-image:none;border-color:#e3e3e3;box-shadow:none;color:#222f3e}.tox .tox-button--secondary:hover:not(:disabled){background-color:#e3e3e3;background-image:none;border-color:#e3e3e3;box-shadow:none;color:#222f3e}.tox .tox-button--secondary:active:not(:disabled){background-color:#d6d6d6;background-image:none;border-color:#d6d6d6;box-shadow:none;color:#222f3e}.tox .tox-button--icon,.tox .tox-button.tox-button--icon,.tox .tox-button.tox-button--secondary.tox-button--icon{padding:4px}.tox .tox-button--icon .tox-icon svg,.tox .tox-button.tox-button--icon .tox-icon svg,.tox .tox-button.tox-button--secondary.tox-button--icon .tox-icon svg{display:block;fill:currentColor}.tox .tox-button-link{background:0;border:none;box-sizing:border-box;cursor:pointer;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:16px;font-weight:400;line-height:1.3;margin:0;padding:0;white-space:nowrap}.tox .tox-button-link--sm{font-size:14px}.tox .tox-button--naked{background-color:transparent;border-color:transparent;box-shadow:unset;color:#222f3e}.tox .tox-button--naked[disabled]{background-color:#f0f0f0;border-color:#f0f0f0;box-shadow:none;color:rgba(34,47,62,.5)}.tox .tox-button--naked:hover:not(:disabled){background-color:#e3e3e3;border-color:#e3e3e3;box-shadow:none;color:#222f3e}.tox .tox-button--naked:focus:not(:disabled){background-color:#e3e3e3;border-color:#e3e3e3;box-shadow:none;color:#222f3e}.tox .tox-button--naked:active:not(:disabled){background-color:#d6d6d6;border-color:#d6d6d6;box-shadow:none;color:#222f3e}.tox .tox-button--naked .tox-icon svg{fill:currentColor}.tox .tox-button--naked.tox-button--icon:hover:not(:disabled){color:#222f3e}.tox .tox-checkbox{align-items:center;border-radius:3px;cursor:pointer;display:flex;height:36px;min-width:36px}.tox .tox-checkbox__input{height:1px;overflow:hidden;position:absolute;top:auto;width:1px}.tox .tox-checkbox__icons{align-items:center;border-radius:3px;box-shadow:0 0 0 2px transparent;box-sizing:content-box;display:flex;height:24px;justify-content:center;padding:calc(4px - 1px);width:24px}.tox .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:block;fill:rgba(34,47,62,.3)}.tox .tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{display:none;fill:#207ab7}.tox .tox-checkbox__icons .tox-checkbox-icon__checked svg{display:none;fill:#207ab7}.tox .tox-checkbox--disabled{color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__checked svg{fill:rgba(34,47,62,.5)}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__unchecked svg{fill:rgba(34,47,62,.5)}.tox .tox-checkbox--disabled .tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{fill:rgba(34,47,62,.5)}.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:none}.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__checked svg{display:block}.tox input.tox-checkbox__input:indeterminate+.tox-checkbox__icons .tox-checkbox-icon__unchecked svg{display:none}.tox input.tox-checkbox__input:indeterminate+.tox-checkbox__icons .tox-checkbox-icon__indeterminate svg{display:block}.tox input.tox-checkbox__input:focus+.tox-checkbox__icons{border-radius:3px;box-shadow:inset 0 0 0 1px #207ab7;padding:calc(4px - 1px)}.tox:not([dir=rtl]) .tox-checkbox__label{margin-left:4px}.tox:not([dir=rtl]) .tox-checkbox__input{left:-10000px}.tox:not([dir=rtl]) .tox-bar .tox-checkbox{margin-left:4px}.tox[dir=rtl] .tox-checkbox__label{margin-right:4px}.tox[dir=rtl] .tox-checkbox__input{right:-10000px}.tox[dir=rtl] .tox-bar .tox-checkbox{margin-right:4px}.tox .tox-collection--toolbar .tox-collection__group{display:flex;padding:0}.tox .tox-collection--grid .tox-collection__group{display:flex;flex-wrap:wrap;max-height:208px;overflow-x:hidden;overflow-y:auto;padding:0}.tox .tox-collection--list .tox-collection__group{border-bottom-width:0;border-color:#ccc;border-left-width:0;border-right-width:0;border-style:solid;border-top-width:1px;padding:4px 0}.tox .tox-collection--list .tox-collection__group:first-child{border-top-width:0}.tox .tox-collection__group-heading{background-color:#e6e6e6;color:rgba(34,47,62,.7);cursor:default;font-size:12px;font-style:normal;font-weight:400;margin-bottom:4px;margin-top:-4px;padding:4px 8px;text-transform:none;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.tox .tox-collection__item{align-items:center;color:#222f3e;cursor:pointer;display:flex;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.tox .tox-collection--list .tox-collection__item{padding:4px 8px}.tox .tox-collection--toolbar .tox-collection__item{border-radius:3px;padding:4px}.tox .tox-collection--grid .tox-collection__item{border-radius:3px;padding:4px}.tox .tox-collection--list .tox-collection__item--enabled{background-color:#fff;color:#222f3e}.tox .tox-collection--list .tox-collection__item--active{background-color:#dee0e2}.tox .tox-collection--toolbar .tox-collection__item--enabled{background-color:#c8cbcf;color:#222f3e}.tox .tox-collection--toolbar .tox-collection__item--active{background-color:#dee0e2}.tox .tox-collection--grid .tox-collection__item--enabled{background-color:#c8cbcf;color:#222f3e}.tox .tox-collection--grid .tox-collection__item--active:not(.tox-collection__item--state-disabled){background-color:#dee0e2;color:#222f3e}.tox .tox-collection--list .tox-collection__item--active:not(.tox-collection__item--state-disabled){color:#222f3e}.tox .tox-collection--toolbar .tox-collection__item--active:not(.tox-collection__item--state-disabled){color:#222f3e}.tox .tox-collection__item-checkmark,.tox .tox-collection__item-icon{align-items:center;display:flex;height:24px;justify-content:center;width:24px}.tox .tox-collection__item-checkmark svg,.tox .tox-collection__item-icon svg{fill:currentColor}.tox .tox-collection--toolbar-lg .tox-collection__item-icon{height:48px;width:48px}.tox .tox-collection__item-label{color:currentColor;display:inline-block;flex:1;-ms-flex-preferred-size:auto;font-size:14px;font-style:normal;font-weight:400;line-height:24px;text-transform:none;word-break:break-all}.tox .tox-collection__item-accessory{color:rgba(34,47,62,.7);display:inline-block;font-size:14px;height:24px;line-height:24px;text-transform:none}.tox .tox-collection__item-caret{align-items:center;display:flex;min-height:24px}.tox .tox-collection__item-caret::after{content:'';font-size:0;min-height:inherit}.tox .tox-collection__item-caret svg{fill:#222f3e}.tox .tox-collection__item--state-disabled{background-color:transparent;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-collection__item--state-disabled .tox-collection__item-caret svg{fill:rgba(34,47,62,.5)}.tox .tox-collection--list .tox-collection__item:not(.tox-collection__item--enabled) .tox-collection__item-checkmark svg{display:none}.tox .tox-collection--list .tox-collection__item:not(.tox-collection__item--enabled) .tox-collection__item-accessory+.tox-collection__item-checkmark{display:none}.tox .tox-collection--horizontal{background-color:#fff;border:1px solid #ccc;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,.15);display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:nowrap;margin-bottom:0;overflow-x:auto;padding:0}.tox .tox-collection--horizontal .tox-collection__group{align-items:center;display:flex;flex-wrap:nowrap;margin:0;padding:0 4px}.tox .tox-collection--horizontal .tox-collection__item{height:34px;margin:2px 0 3px 0;padding:0 4px}.tox .tox-collection--horizontal .tox-collection__item-label{white-space:nowrap}.tox .tox-collection--horizontal .tox-collection__item-caret{margin-left:4px}.tox .tox-collection__item-container{display:flex}.tox .tox-collection__item-container--row{align-items:center;flex:1 1 auto;flex-direction:row}.tox .tox-collection__item-container--row.tox-collection__item-container--align-left{margin-right:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--align-right{justify-content:flex-end;margin-left:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-top{align-items:flex-start;margin-bottom:auto}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-middle{align-items:center}.tox .tox-collection__item-container--row.tox-collection__item-container--valign-bottom{align-items:flex-end;margin-top:auto}.tox .tox-collection__item-container--column{-ms-grid-row-align:center;align-self:center;flex:1 1 auto;flex-direction:column}.tox .tox-collection__item-container--column.tox-collection__item-container--align-left{align-items:flex-start}.tox .tox-collection__item-container--column.tox-collection__item-container--align-right{align-items:flex-end}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-top{align-self:flex-start}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-middle{-ms-grid-row-align:center;align-self:center}.tox .tox-collection__item-container--column.tox-collection__item-container--valign-bottom{align-self:flex-end}.tox:not([dir=rtl]) .tox-collection--horizontal .tox-collection__group:not(:last-of-type){border-right:1px solid #ccc}.tox:not([dir=rtl]) .tox-collection--list .tox-collection__item>:not(:first-child){margin-left:8px}.tox:not([dir=rtl]) .tox-collection--list .tox-collection__item>.tox-collection__item-label:first-child{margin-left:4px}.tox:not([dir=rtl]) .tox-collection__item-accessory{margin-left:16px;text-align:right}.tox:not([dir=rtl]) .tox-collection .tox-collection__item-caret{margin-left:16px}.tox[dir=rtl] .tox-collection--horizontal .tox-collection__group:not(:last-of-type){border-left:1px solid #ccc}.tox[dir=rtl] .tox-collection--list .tox-collection__item>:not(:first-child){margin-right:8px}.tox[dir=rtl] .tox-collection--list .tox-collection__item>.tox-collection__item-label:first-child{margin-right:4px}.tox[dir=rtl] .tox-collection__item-accessory{margin-right:16px;text-align:left}.tox[dir=rtl] .tox-collection .tox-collection__item-caret{margin-right:16px;transform:rotateY(180deg)}.tox[dir=rtl] .tox-collection--horizontal .tox-collection__item-caret{margin-right:4px}.tox .tox-color-picker-container{display:flex;flex-direction:row;height:225px;margin:0}.tox .tox-sv-palette{box-sizing:border-box;display:flex;height:100%}.tox .tox-sv-palette-spectrum{height:100%}.tox .tox-sv-palette,.tox .tox-sv-palette-spectrum{width:225px}.tox .tox-sv-palette-thumb{background:0 0;border:1px solid #000;border-radius:50%;box-sizing:content-box;height:12px;position:absolute;width:12px}.tox .tox-sv-palette-inner-thumb{border:1px solid #fff;border-radius:50%;height:10px;position:absolute;width:10px}.tox .tox-hue-slider{box-sizing:border-box;height:100%;width:25px}.tox .tox-hue-slider-spectrum{background:linear-gradient(to bottom,red,#ff0080,#f0f,#8000ff,#00f,#0080ff,#0ff,#00ff80,#0f0,#80ff00,#ff0,#ff8000,red);height:100%;width:100%}.tox .tox-hue-slider,.tox .tox-hue-slider-spectrum{width:20px}.tox .tox-hue-slider-thumb{background:#fff;border:1px solid #000;box-sizing:content-box;height:4px;width:100%}.tox .tox-rgb-form{display:flex;flex-direction:column;justify-content:space-between}.tox .tox-rgb-form div{align-items:center;display:flex;justify-content:space-between;margin-bottom:5px;width:inherit}.tox .tox-rgb-form input{width:6em}.tox .tox-rgb-form input.tox-invalid{border:1px solid red!important}.tox .tox-rgb-form .tox-rgba-preview{border:1px solid #000;flex-grow:2;margin-bottom:0}.tox:not([dir=rtl]) .tox-sv-palette{margin-right:15px}.tox:not([dir=rtl]) .tox-hue-slider{margin-right:15px}.tox:not([dir=rtl]) .tox-hue-slider-thumb{margin-left:-1px}.tox:not([dir=rtl]) .tox-rgb-form label{margin-right:.5em}.tox[dir=rtl] .tox-sv-palette{margin-left:15px}.tox[dir=rtl] .tox-hue-slider{margin-left:15px}.tox[dir=rtl] .tox-hue-slider-thumb{margin-right:-1px}.tox[dir=rtl] .tox-rgb-form label{margin-left:.5em}.tox .tox-toolbar .tox-swatches,.tox .tox-toolbar__overflow .tox-swatches,.tox .tox-toolbar__primary .tox-swatches{margin:2px 0 3px 4px}.tox .tox-collection--list .tox-collection__group .tox-swatches-menu{border:0;margin:-4px 0}.tox .tox-swatches__row{display:flex}.tox .tox-swatch{height:30px;transition:transform .15s,box-shadow .15s;width:30px}.tox .tox-swatch:focus,.tox .tox-swatch:hover{box-shadow:0 0 0 1px rgba(127,127,127,.3) inset;transform:scale(.8)}.tox .tox-swatch--remove{align-items:center;display:flex;justify-content:center}.tox .tox-swatch--remove svg path{stroke:#e74c3c}.tox .tox-swatches__picker-btn{align-items:center;background-color:transparent;border:0;cursor:pointer;display:flex;height:30px;justify-content:center;outline:0;padding:0;width:30px}.tox .tox-swatches__picker-btn svg{height:24px;width:24px}.tox .tox-swatches__picker-btn:hover{background:#dee0e2}.tox:not([dir=rtl]) .tox-swatches__picker-btn{margin-left:auto}.tox[dir=rtl] .tox-swatches__picker-btn{margin-right:auto}.tox .tox-comment-thread{background:#fff;position:relative}.tox .tox-comment-thread>:not(:first-child){margin-top:8px}.tox .tox-comment{background:#fff;border:1px solid #ccc;border-radius:3px;box-shadow:0 4px 8px 0 rgba(34,47,62,.1);padding:8px 8px 16px 8px;position:relative}.tox .tox-comment__header{align-items:center;color:#222f3e;display:flex;justify-content:space-between}.tox .tox-comment__date{color:rgba(34,47,62,.7);font-size:12px}.tox .tox-comment__body{color:#222f3e;font-size:14px;font-style:normal;font-weight:400;line-height:1.3;margin-top:8px;position:relative;text-transform:initial}.tox .tox-comment__body textarea{resize:none;white-space:normal;width:100%}.tox .tox-comment__expander{padding-top:8px}.tox .tox-comment__expander p{color:rgba(34,47,62,.7);font-size:14px;font-style:normal}.tox .tox-comment__body p{margin:0}.tox .tox-comment__buttonspacing{padding-top:16px;text-align:center}.tox .tox-comment-thread__overlay::after{background:#fff;bottom:0;content:"";display:flex;left:0;opacity:.9;position:absolute;right:0;top:0;z-index:5}.tox .tox-comment__reply{display:flex;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end;margin-top:8px}.tox .tox-comment__reply>:first-child{margin-bottom:8px;width:100%}.tox .tox-comment__edit{display:flex;flex-wrap:wrap;justify-content:flex-end;margin-top:16px}.tox .tox-comment__gradient::after{background:linear-gradient(rgba(255,255,255,0),#fff);bottom:0;content:"";display:block;height:5em;margin-top:-40px;position:absolute;width:100%}.tox .tox-comment__overlay{background:#fff;bottom:0;display:flex;flex-direction:column;flex-grow:1;left:0;opacity:.9;position:absolute;right:0;text-align:center;top:0;z-index:5}.tox .tox-comment__loading-text{align-items:center;color:#222f3e;display:flex;flex-direction:column;position:relative}.tox .tox-comment__loading-text>div{padding-bottom:16px}.tox .tox-comment__overlaytext{bottom:0;flex-direction:column;font-size:14px;left:0;padding:1em;position:absolute;right:0;top:0;z-index:10}.tox .tox-comment__overlaytext p{background-color:#fff;box-shadow:0 0 8px 8px #fff;color:#222f3e;text-align:center}.tox .tox-comment__overlaytext div:nth-of-type(2){font-size:.8em}.tox .tox-comment__busy-spinner{align-items:center;background-color:#fff;bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0;z-index:20}.tox .tox-comment__scroll{display:flex;flex-direction:column;flex-shrink:1;overflow:auto}.tox .tox-conversations{margin:8px}.tox:not([dir=rtl]) .tox-comment__edit{margin-left:8px}.tox:not([dir=rtl]) .tox-comment__buttonspacing>:last-child,.tox:not([dir=rtl]) .tox-comment__edit>:last-child,.tox:not([dir=rtl]) .tox-comment__reply>:last-child{margin-left:8px}.tox[dir=rtl] .tox-comment__edit{margin-right:8px}.tox[dir=rtl] .tox-comment__buttonspacing>:last-child,.tox[dir=rtl] .tox-comment__edit>:last-child,.tox[dir=rtl] .tox-comment__reply>:last-child{margin-right:8px}.tox .tox-user{align-items:center;display:flex}.tox .tox-user__avatar svg{fill:rgba(34,47,62,.7)}.tox .tox-user__name{color:rgba(34,47,62,.7);font-size:12px;font-style:normal;font-weight:700;text-transform:uppercase}.tox:not([dir=rtl]) .tox-user__avatar svg{margin-right:8px}.tox:not([dir=rtl]) .tox-user__avatar+.tox-user__name{margin-left:8px}.tox[dir=rtl] .tox-user__avatar svg{margin-left:8px}.tox[dir=rtl] .tox-user__avatar+.tox-user__name{margin-right:8px}.tox .tox-dialog-wrap{align-items:center;bottom:0;display:flex;justify-content:center;left:0;position:fixed;right:0;top:0;z-index:1100}.tox .tox-dialog-wrap__backdrop{background-color:rgba(255,255,255,.75);bottom:0;left:0;position:absolute;right:0;top:0;z-index:1}.tox .tox-dialog-wrap__backdrop--opaque{background-color:#fff}.tox .tox-dialog{background-color:#fff;border-color:#ccc;border-radius:3px;border-style:solid;border-width:1px;box-shadow:0 16px 16px -10px rgba(34,47,62,.15),0 0 40px 1px rgba(34,47,62,.15);display:flex;flex-direction:column;max-height:100%;max-width:480px;overflow:hidden;position:relative;width:95vw;z-index:2}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog{align-self:flex-start;margin:8px auto;width:calc(100vw - 16px)}}.tox .tox-dialog-inline{z-index:1100}.tox .tox-dialog__header{align-items:center;background-color:#fff;border-bottom:none;color:#222f3e;display:flex;font-size:16px;justify-content:space-between;padding:8px 16px 0 16px;position:relative}.tox .tox-dialog__header .tox-button{z-index:1}.tox .tox-dialog__draghandle{cursor:grab;height:100%;left:0;position:absolute;top:0;width:100%}.tox .tox-dialog__draghandle:active{cursor:grabbing}.tox .tox-dialog__dismiss{margin-left:auto}.tox .tox-dialog__title{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:20px;font-style:normal;font-weight:400;line-height:1.3;margin:0;text-transform:none}.tox .tox-dialog__body{color:#222f3e;display:flex;flex:1;-ms-flex-preferred-size:auto;font-size:16px;font-style:normal;font-weight:400;line-height:1.3;min-width:0;text-align:left;text-transform:none}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog__body{flex-direction:column}}.tox .tox-dialog__body-nav{align-items:flex-start;display:flex;flex-direction:column;padding:16px 16px}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox .tox-dialog__body-nav{flex-direction:row;-webkit-overflow-scrolling:touch;overflow-x:auto;padding-bottom:0}}.tox .tox-dialog__body-nav-item{border-bottom:2px solid transparent;color:rgba(34,47,62,.7);display:inline-block;font-size:14px;line-height:1.3;margin-bottom:8px;text-decoration:none;white-space:nowrap}.tox .tox-dialog__body-nav-item:focus{background-color:rgba(32,122,183,.1)}.tox .tox-dialog__body-nav-item--active{border-bottom:2px solid #207ab7;color:#207ab7}.tox .tox-dialog__body-content{box-sizing:border-box;display:flex;flex:1;flex-direction:column;-ms-flex-preferred-size:auto;max-height:650px;overflow:auto;-webkit-overflow-scrolling:touch;padding:16px 16px}.tox .tox-dialog__body-content>*{margin-bottom:0;margin-top:16px}.tox .tox-dialog__body-content>:first-child{margin-top:0}.tox .tox-dialog__body-content>:last-child{margin-bottom:0}.tox .tox-dialog__body-content>:only-child{margin-bottom:0;margin-top:0}.tox .tox-dialog__body-content a{color:#207ab7;cursor:pointer;text-decoration:none}.tox .tox-dialog__body-content a:focus,.tox .tox-dialog__body-content a:hover{color:#185d8c;text-decoration:none}.tox .tox-dialog__body-content a:active{color:#185d8c;text-decoration:none}.tox .tox-dialog__body-content svg{fill:#222f3e}.tox .tox-dialog__body-content ul{display:block;list-style-type:disc;margin-bottom:16px;-webkit-margin-end:0;margin-inline-end:0;-webkit-margin-start:0;margin-inline-start:0;-webkit-padding-start:2.5rem;padding-inline-start:2.5rem}.tox .tox-dialog__body-content .tox-form__group h1{color:#222f3e;font-size:20px;font-style:normal;font-weight:700;letter-spacing:normal;margin-bottom:16px;margin-top:2rem;text-transform:none}.tox .tox-dialog__body-content .tox-form__group h2{color:#222f3e;font-size:16px;font-style:normal;font-weight:700;letter-spacing:normal;margin-bottom:16px;margin-top:2rem;text-transform:none}.tox .tox-dialog__body-content .tox-form__group p{margin-bottom:16px}.tox .tox-dialog__body-content .tox-form__group h1:first-child,.tox .tox-dialog__body-content .tox-form__group h2:first-child,.tox .tox-dialog__body-content .tox-form__group p:first-child{margin-top:0}.tox .tox-dialog__body-content .tox-form__group h1:last-child,.tox .tox-dialog__body-content .tox-form__group h2:last-child,.tox .tox-dialog__body-content .tox-form__group p:last-child{margin-bottom:0}.tox .tox-dialog__body-content .tox-form__group h1:only-child,.tox .tox-dialog__body-content .tox-form__group h2:only-child,.tox .tox-dialog__body-content .tox-form__group p:only-child{margin-bottom:0;margin-top:0}.tox .tox-dialog--width-lg{height:650px;max-width:1200px}.tox .tox-dialog--width-md{max-width:800px}.tox .tox-dialog--width-md .tox-dialog__body-content{overflow:auto}.tox .tox-dialog__body-content--centered{text-align:center}.tox .tox-dialog__footer{align-items:center;background-color:#fff;border-top:1px solid #ccc;display:flex;justify-content:space-between;padding:8px 16px}.tox .tox-dialog__footer-end,.tox .tox-dialog__footer-start{display:flex}.tox .tox-dialog__busy-spinner{align-items:center;background-color:rgba(255,255,255,.75);bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0;z-index:3}.tox .tox-dialog__table{border-collapse:collapse;width:100%}.tox .tox-dialog__table thead th{font-weight:700;padding-bottom:8px}.tox .tox-dialog__table tbody tr{border-bottom:1px solid #ccc}.tox .tox-dialog__table tbody tr:last-child{border-bottom:none}.tox .tox-dialog__table td{padding-bottom:8px;padding-top:8px}.tox .tox-dialog__popups{position:absolute;width:100%;z-index:1100}.tox .tox-dialog__body-iframe{display:flex;flex:1;flex-direction:column;-ms-flex-preferred-size:auto}.tox .tox-dialog__body-iframe .tox-navobj{display:flex;flex:1;-ms-flex-preferred-size:auto}.tox .tox-dialog__body-iframe .tox-navobj :nth-child(2){flex:1;-ms-flex-preferred-size:auto;height:100%}.tox .tox-dialog-dock-fadeout{opacity:0;visibility:hidden}.tox .tox-dialog-dock-fadein{opacity:1;visibility:visible}.tox .tox-dialog-dock-transition{transition:visibility 0s linear .3s,opacity .3s ease}.tox .tox-dialog-dock-transition.tox-dialog-dock-fadein{transition-delay:0s}.tox.tox-platform-ie .tox-dialog-wrap{position:-ms-device-fixed}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox:not([dir=rtl]) .tox-dialog__body-nav{margin-right:0}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox:not([dir=rtl]) .tox-dialog__body-nav-item:not(:first-child){margin-left:8px}}.tox:not([dir=rtl]) .tox-dialog__footer .tox-dialog__footer-end>*,.tox:not([dir=rtl]) .tox-dialog__footer .tox-dialog__footer-start>*{margin-left:8px}.tox[dir=rtl] .tox-dialog__body{text-align:right}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox[dir=rtl] .tox-dialog__body-nav{margin-left:0}}@media only screen and (max-width:767px){body:not(.tox-force-desktop) .tox[dir=rtl] .tox-dialog__body-nav-item:not(:first-child){margin-right:8px}}.tox[dir=rtl] .tox-dialog__footer .tox-dialog__footer-end>*,.tox[dir=rtl] .tox-dialog__footer .tox-dialog__footer-start>*{margin-right:8px}body.tox-dialog__disable-scroll{overflow:hidden}.tox .tox-dropzone-container{display:flex;flex:1;-ms-flex-preferred-size:auto}.tox .tox-dropzone{align-items:center;background:#fff;border:2px dashed #ccc;box-sizing:border-box;display:flex;flex-direction:column;flex-grow:1;justify-content:center;min-height:100px;padding:10px}.tox .tox-dropzone p{color:rgba(34,47,62,.7);margin:0 0 16px 0}.tox .tox-edit-area{display:flex;flex:1;-ms-flex-preferred-size:auto;overflow:hidden;position:relative}.tox .tox-edit-area__iframe{background-color:#fff;border:0;box-sizing:border-box;flex:1;-ms-flex-preferred-size:auto;height:100%;position:absolute;width:100%}.tox.tox-inline-edit-area{border:1px dotted #ccc}.tox .tox-editor-container{display:flex;flex:1 1 auto;flex-direction:column;overflow:hidden}.tox .tox-editor-header{z-index:1}.tox:not(.tox-tinymce-inline) .tox-editor-header{box-shadow:none;transition:box-shadow .5s}.tox.tox-tinymce--toolbar-bottom .tox-editor-header,.tox.tox-tinymce-inline .tox-editor-header{margin-bottom:-1px}.tox.tox-tinymce--toolbar-sticky-on .tox-editor-header{background-color:transparent;box-shadow:0 4px 4px -3px rgba(0,0,0,.25)}.tox-editor-dock-fadeout{opacity:0;visibility:hidden}.tox-editor-dock-fadein{opacity:1;visibility:visible}.tox-editor-dock-transition{transition:visibility 0s linear .25s,opacity .25s ease}.tox-editor-dock-transition.tox-editor-dock-fadein{transition-delay:0s}.tox .tox-control-wrap{flex:1;position:relative}.tox .tox-control-wrap:not(.tox-control-wrap--status-invalid) .tox-control-wrap__status-icon-invalid,.tox .tox-control-wrap:not(.tox-control-wrap--status-unknown) .tox-control-wrap__status-icon-unknown,.tox .tox-control-wrap:not(.tox-control-wrap--status-valid) .tox-control-wrap__status-icon-valid{display:none}.tox .tox-control-wrap svg{display:block}.tox .tox-control-wrap__status-icon-wrap{position:absolute;top:50%;transform:translateY(-50%)}.tox .tox-control-wrap__status-icon-invalid svg{fill:#c00}.tox .tox-control-wrap__status-icon-unknown svg{fill:orange}.tox .tox-control-wrap__status-icon-valid svg{fill:green}.tox:not([dir=rtl]) .tox-control-wrap--status-invalid .tox-textfield,.tox:not([dir=rtl]) .tox-control-wrap--status-unknown .tox-textfield,.tox:not([dir=rtl]) .tox-control-wrap--status-valid .tox-textfield{padding-right:32px}.tox:not([dir=rtl]) .tox-control-wrap__status-icon-wrap{right:4px}.tox[dir=rtl] .tox-control-wrap--status-invalid .tox-textfield,.tox[dir=rtl] .tox-control-wrap--status-unknown .tox-textfield,.tox[dir=rtl] .tox-control-wrap--status-valid .tox-textfield{padding-left:32px}.tox[dir=rtl] .tox-control-wrap__status-icon-wrap{left:4px}.tox .tox-autocompleter{max-width:25em}.tox .tox-autocompleter .tox-menu{max-width:25em}.tox .tox-autocompleter .tox-autocompleter-highlight{font-weight:700}.tox .tox-color-input{display:flex;position:relative;z-index:1}.tox .tox-color-input .tox-textfield{z-index:-1}.tox .tox-color-input span{border-color:rgba(34,47,62,.2);border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;height:24px;position:absolute;top:6px;width:24px}.tox .tox-color-input span:focus:not([aria-disabled=true]),.tox .tox-color-input span:hover:not([aria-disabled=true]){border-color:#207ab7;cursor:pointer}.tox .tox-color-input span::before{background-image:linear-gradient(45deg,rgba(0,0,0,.25) 25%,transparent 25%),linear-gradient(-45deg,rgba(0,0,0,.25) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(0,0,0,.25) 75%),linear-gradient(-45deg,transparent 75%,rgba(0,0,0,.25) 75%);background-position:0 0,0 6px,6px -6px,-6px 0;background-size:12px 12px;border:1px solid #fff;border-radius:3px;box-sizing:border-box;content:'';height:24px;left:-1px;position:absolute;top:-1px;width:24px;z-index:-1}.tox .tox-color-input span[aria-disabled=true]{cursor:not-allowed}.tox:not([dir=rtl]) .tox-color-input .tox-textfield{padding-left:36px}.tox:not([dir=rtl]) .tox-color-input span{left:6px}.tox[dir=rtl] .tox-color-input .tox-textfield{padding-right:36px}.tox[dir=rtl] .tox-color-input span{right:6px}.tox .tox-label,.tox .tox-toolbar-label{color:rgba(34,47,62,.7);display:block;font-size:14px;font-style:normal;font-weight:400;line-height:1.3;padding:0 8px 0 0;text-transform:none;white-space:nowrap}.tox .tox-toolbar-label{padding:0 8px}.tox[dir=rtl] .tox-label{padding:0 0 0 8px}.tox .tox-form{display:flex;flex:1;flex-direction:column;-ms-flex-preferred-size:auto}.tox .tox-form__group{box-sizing:border-box;margin-bottom:4px}.tox .tox-form-group--maximize{flex:1}.tox .tox-form__group--error{color:#c00}.tox .tox-form__group--collection{display:flex}.tox .tox-form__grid{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between}.tox .tox-form__grid--2col>.tox-form__group{width:calc(50% - (8px / 2))}.tox .tox-form__grid--3col>.tox-form__group{width:calc(100% / 3 - (8px / 2))}.tox .tox-form__grid--4col>.tox-form__group{width:calc(25% - (8px / 2))}.tox .tox-form__controls-h-stack{align-items:center;display:flex}.tox .tox-form__group--inline{align-items:center;display:flex}.tox .tox-form__group--stretched{display:flex;flex:1;flex-direction:column;-ms-flex-preferred-size:auto}.tox .tox-form__group--stretched .tox-textarea{flex:1;-ms-flex-preferred-size:auto}.tox .tox-form__group--stretched .tox-navobj{display:flex;flex:1;-ms-flex-preferred-size:auto}.tox .tox-form__group--stretched .tox-navobj :nth-child(2){flex:1;-ms-flex-preferred-size:auto;height:100%}.tox:not([dir=rtl]) .tox-form__controls-h-stack>:not(:first-child){margin-left:4px}.tox[dir=rtl] .tox-form__controls-h-stack>:not(:first-child){margin-right:4px}.tox .tox-lock.tox-locked .tox-lock-icon__unlock,.tox .tox-lock:not(.tox-locked) .tox-lock-icon__lock{display:none}.tox .tox-listboxfield .tox-listbox--select,.tox .tox-textarea,.tox .tox-textfield,.tox .tox-toolbar-textfield{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#ccc;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#222f3e;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:16px;line-height:24px;margin:0;min-height:34px;outline:0;padding:5px 4.75px;resize:none;width:100%}.tox .tox-textarea[disabled],.tox .tox-textfield[disabled]{background-color:#f2f2f2;color:rgba(34,47,62,.85);cursor:not-allowed}.tox .tox-listboxfield .tox-listbox--select:focus,.tox .tox-textarea:focus,.tox .tox-textfield:focus{background-color:#fff;border-color:#207ab7;box-shadow:none;outline:0}.tox .tox-toolbar-textfield{border-width:0;margin-bottom:3px;margin-top:2px;max-width:250px}.tox .tox-naked-btn{background-color:transparent;border:0;border-color:transparent;box-shadow:unset;color:#207ab7;cursor:pointer;display:block;margin:0;padding:0}.tox .tox-naked-btn svg{display:block;fill:#222f3e}.tox:not([dir=rtl]) .tox-toolbar-textfield+*{margin-left:4px}.tox[dir=rtl] .tox-toolbar-textfield+*{margin-right:4px}.tox .tox-listboxfield{cursor:pointer;position:relative}.tox .tox-listboxfield .tox-listbox--select[disabled]{background-color:#f2f2f2;color:rgba(34,47,62,.85);cursor:not-allowed}.tox .tox-listbox__select-label{cursor:default;flex:1;margin:0 4px}.tox .tox-listbox__select-chevron{align-items:center;display:flex;justify-content:center;width:16px}.tox .tox-listbox__select-chevron svg{fill:#222f3e}.tox .tox-listboxfield .tox-listbox--select{align-items:center;display:flex}.tox:not([dir=rtl]) .tox-listboxfield svg{right:8px}.tox[dir=rtl] .tox-listboxfield svg{left:8px}.tox .tox-selectfield{cursor:pointer;position:relative}.tox .tox-selectfield select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#ccc;border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;color:#222f3e;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:16px;line-height:24px;margin:0;min-height:34px;outline:0;padding:5px 4.75px;resize:none;width:100%}.tox .tox-selectfield select[disabled]{background-color:#f2f2f2;color:rgba(34,47,62,.85);cursor:not-allowed}.tox .tox-selectfield select::-ms-expand{display:none}.tox .tox-selectfield select:focus{background-color:#fff;border-color:#207ab7;box-shadow:none;outline:0}.tox .tox-selectfield svg{pointer-events:none;position:absolute;top:50%;transform:translateY(-50%)}.tox:not([dir=rtl]) .tox-selectfield select[size="0"],.tox:not([dir=rtl]) .tox-selectfield select[size="1"]{padding-right:24px}.tox:not([dir=rtl]) .tox-selectfield svg{right:8px}.tox[dir=rtl] .tox-selectfield select[size="0"],.tox[dir=rtl] .tox-selectfield select[size="1"]{padding-left:24px}.tox[dir=rtl] .tox-selectfield svg{left:8px}.tox .tox-textarea{-webkit-appearance:textarea;-moz-appearance:textarea;appearance:textarea;white-space:pre-wrap}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;-ms-scroll-chaining:none;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}.tox .tox-help__more-link{list-style:none;margin-top:1em}.tox .tox-image-tools{width:100%}.tox .tox-image-tools__toolbar{align-items:center;display:flex;justify-content:center}.tox .tox-image-tools__image{background-color:#666;height:380px;overflow:auto;position:relative;width:100%}.tox .tox-image-tools__image,.tox .tox-image-tools__image+.tox-image-tools__toolbar{margin-top:8px}.tox .tox-image-tools__image-bg{background:url(data:image/gif;base64,R0lGODdhDAAMAIABAMzMzP///ywAAAAADAAMAAACFoQfqYeabNyDMkBQb81Uat85nxguUAEAOw==)}.tox .tox-image-tools__toolbar>.tox-spacer{flex:1;-ms-flex-preferred-size:auto}.tox .tox-croprect-block{background:#000;opacity:.5;position:absolute;zoom:1}.tox .tox-croprect-handle{border:2px solid #fff;height:20px;left:0;position:absolute;top:0;width:20px}.tox .tox-croprect-handle-move{border:0;cursor:move;position:absolute}.tox .tox-croprect-handle-nw{border-width:2px 0 0 2px;cursor:nw-resize;left:100px;margin:-2px 0 0 -2px;top:100px}.tox .tox-croprect-handle-ne{border-width:2px 2px 0 0;cursor:ne-resize;left:200px;margin:-2px 0 0 -20px;top:100px}.tox .tox-croprect-handle-sw{border-width:0 0 2px 2px;cursor:sw-resize;left:100px;margin:-20px 2px 0 -2px;top:200px}.tox .tox-croprect-handle-se{border-width:0 2px 2px 0;cursor:se-resize;left:200px;margin:-20px 0 0 -20px;top:200px}.tox:not([dir=rtl]) .tox-image-tools__toolbar>.tox-slider:not(:first-of-type){margin-left:8px}.tox:not([dir=rtl]) .tox-image-tools__toolbar>.tox-button+.tox-slider{margin-left:32px}.tox:not([dir=rtl]) .tox-image-tools__toolbar>.tox-slider+.tox-button{margin-left:32px}.tox[dir=rtl] .tox-image-tools__toolbar>.tox-slider:not(:first-of-type){margin-right:8px}.tox[dir=rtl] .tox-image-tools__toolbar>.tox-button+.tox-slider{margin-right:32px}.tox[dir=rtl] .tox-image-tools__toolbar>.tox-slider+.tox-button{margin-right:32px}.tox .tox-insert-table-picker{display:flex;flex-wrap:wrap;width:170px}.tox .tox-insert-table-picker>div{border-color:#ccc;border-style:solid;border-width:0 1px 1px 0;box-sizing:border-box;height:17px;width:17px}.tox .tox-collection--list .tox-collection__group .tox-insert-table-picker{margin:-4px 0}.tox .tox-insert-table-picker .tox-insert-table-picker__selected{background-color:rgba(32,122,183,.5);border-color:rgba(32,122,183,.5)}.tox .tox-insert-table-picker__label{color:rgba(34,47,62,.7);display:block;font-size:14px;padding:4px;text-align:center;width:100%}.tox:not([dir=rtl]) .tox-insert-table-picker>div:nth-child(10n){border-right:0}.tox[dir=rtl] .tox-insert-table-picker>div:nth-child(10n+1){border-right:0}.tox .tox-menu{background-color:#fff;border:1px solid #ccc;border-radius:3px;box-shadow:0 4px 8px 0 rgba(34,47,62,.1);display:inline-block;overflow:hidden;vertical-align:top;z-index:1150}.tox .tox-menu.tox-collection.tox-collection--list{padding:0}.tox .tox-menu.tox-collection.tox-collection--toolbar{padding:4px}.tox .tox-menu.tox-collection.tox-collection--grid{padding:4px}.tox .tox-menu__label blockquote,.tox .tox-menu__label code,.tox .tox-menu__label h1,.tox .tox-menu__label h2,.tox .tox-menu__label h3,.tox .tox-menu__label h4,.tox .tox-menu__label h5,.tox .tox-menu__label h6,.tox .tox-menu__label p{margin:0}.tox .tox-menubar{background:url("data:image/svg+xml;charset=utf8,%3Csvg height='39px' viewBox='0 0 40 39px' width='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='38px' width='100' height='1' fill='%23cccccc'/%3E%3C/svg%3E") left 0 top 0 #fff;background-color:#fff;display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:wrap;padding:0 4px 0 4px}.tox.tox-tinymce:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-menubar{border-top:1px solid #ccc}.tox .tox-mbtn{align-items:center;background:0 0;border:0;border-radius:3px;box-shadow:none;color:#222f3e;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:34px;justify-content:center;margin:2px 0 3px 0;outline:0;overflow:hidden;padding:0 4px;text-transform:none;width:auto}.tox .tox-mbtn[disabled]{background-color:transparent;border:0;box-shadow:none;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-mbtn:focus:not(:disabled){background:#dee0e2;border:0;box-shadow:none;color:#222f3e}.tox .tox-mbtn--active{background:#c8cbcf;border:0;box-shadow:none;color:#222f3e}.tox .tox-mbtn:hover:not(:disabled):not(.tox-mbtn--active){background:#dee0e2;border:0;box-shadow:none;color:#222f3e}.tox .tox-mbtn__select-label{cursor:default;font-weight:400;margin:0 4px}.tox .tox-mbtn[disabled] .tox-mbtn__select-label{cursor:not-allowed}.tox .tox-mbtn__select-chevron{align-items:center;display:flex;justify-content:center;width:16px;display:none}.tox .tox-notification{border-radius:3px;border-style:solid;border-width:1px;box-shadow:none;box-sizing:border-box;display:-ms-grid;display:grid;font-size:14px;font-weight:400;-ms-grid-columns:minmax(40px,1fr) auto minmax(40px,1fr);grid-template-columns:minmax(40px,1fr) auto minmax(40px,1fr);margin-top:4px;opacity:0;padding:4px;transition:transform .1s ease-in,opacity 150ms ease-in}.tox .tox-notification p{font-size:14px;font-weight:400}.tox .tox-notification a{cursor:pointer;text-decoration:underline}.tox .tox-notification--in{opacity:1}.tox .tox-notification--success{background-color:#e4eeda;border-color:#d7e6c8;color:#222f3e}.tox .tox-notification--success p{color:#222f3e}.tox .tox-notification--success a{color:#547831}.tox .tox-notification--success svg{fill:#222f3e}.tox .tox-notification--error{background-color:#f8dede;border-color:#f2bfbf;color:#222f3e}.tox .tox-notification--error p{color:#222f3e}.tox .tox-notification--error a{color:#c00}.tox .tox-notification--error svg{fill:#222f3e}.tox .tox-notification--warn,.tox .tox-notification--warning{background-color:#fffaea;border-color:#ffe89d;color:#222f3e}.tox .tox-notification--warn p,.tox .tox-notification--warning p{color:#222f3e}.tox .tox-notification--warn a,.tox .tox-notification--warning a{color:#222f3e}.tox .tox-notification--warn svg,.tox .tox-notification--warning svg{fill:#222f3e}.tox .tox-notification--info{background-color:#d9edf7;border-color:#779ecb;color:#222f3e}.tox .tox-notification--info p{color:#222f3e}.tox .tox-notification--info a{color:#222f3e}.tox .tox-notification--info svg{fill:#222f3e}.tox .tox-notification__body{-ms-grid-row-align:center;align-self:center;color:#222f3e;font-size:14px;-ms-grid-column-span:1;grid-column-end:3;-ms-grid-column:2;grid-column-start:2;-ms-grid-row-span:1;grid-row-end:2;-ms-grid-row:1;grid-row-start:1;text-align:center;white-space:normal;word-break:break-all;word-break:break-word}.tox .tox-notification__body>*{margin:0}.tox .tox-notification__body>*+*{margin-top:1rem}.tox .tox-notification__icon{-ms-grid-row-align:center;align-self:center;-ms-grid-column-span:1;grid-column-end:2;-ms-grid-column:1;grid-column-start:1;-ms-grid-row-span:1;grid-row-end:2;-ms-grid-row:1;grid-row-start:1;-ms-grid-column-align:end;justify-self:end}.tox .tox-notification__icon svg{display:block}.tox .tox-notification__dismiss{-ms-grid-row-align:start;align-self:start;-ms-grid-column-span:1;grid-column-end:4;-ms-grid-column:3;grid-column-start:3;-ms-grid-row-span:1;grid-row-end:2;-ms-grid-row:1;grid-row-start:1;-ms-grid-column-align:end;justify-self:end}.tox .tox-notification .tox-progress-bar{-ms-grid-column-span:3;grid-column-end:4;-ms-grid-column:1;grid-column-start:1;-ms-grid-row-span:1;grid-row-end:3;-ms-grid-row:2;grid-row-start:2;-ms-grid-column-align:center;justify-self:center}.tox .tox-pop{display:inline-block;position:relative}.tox .tox-pop--resizing{transition:width .1s ease}.tox .tox-pop--resizing .tox-toolbar,.tox .tox-pop--resizing .tox-toolbar__group{flex-wrap:nowrap}.tox .tox-pop--transition{transition:.15s ease;transition-property:left,right,top,bottom}.tox .tox-pop--transition::after,.tox .tox-pop--transition::before{transition:all .15s,visibility 0s,opacity 75ms ease 75ms}.tox .tox-pop__dialog{background-color:#fff;border:1px solid #ccc;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,.15);min-width:0;overflow:hidden}.tox .tox-pop__dialog>:not(.tox-toolbar){margin:4px 4px 4px 8px}.tox .tox-pop__dialog .tox-toolbar{background-color:transparent;margin-bottom:-1px}.tox .tox-pop::after,.tox .tox-pop::before{border-style:solid;content:'';display:block;height:0;opacity:1;position:absolute;width:0}.tox .tox-pop.tox-pop--inset::after,.tox .tox-pop.tox-pop--inset::before{opacity:0;transition:all 0s .15s,visibility 0s,opacity 75ms ease}.tox .tox-pop.tox-pop--bottom::after,.tox .tox-pop.tox-pop--bottom::before{left:50%;top:100%}.tox .tox-pop.tox-pop--bottom::after{border-color:#fff transparent transparent transparent;border-width:8px;margin-left:-8px;margin-top:-1px}.tox .tox-pop.tox-pop--bottom::before{border-color:#ccc transparent transparent transparent;border-width:9px;margin-left:-9px}.tox .tox-pop.tox-pop--top::after,.tox .tox-pop.tox-pop--top::before{left:50%;top:0;transform:translateY(-100%)}.tox .tox-pop.tox-pop--top::after{border-color:transparent transparent #fff transparent;border-width:8px;margin-left:-8px;margin-top:1px}.tox .tox-pop.tox-pop--top::before{border-color:transparent transparent #ccc transparent;border-width:9px;margin-left:-9px}.tox .tox-pop.tox-pop--left::after,.tox .tox-pop.tox-pop--left::before{left:0;top:calc(50% - 1px);transform:translateY(-50%)}.tox .tox-pop.tox-pop--left::after{border-color:transparent #fff transparent transparent;border-width:8px;margin-left:-15px}.tox .tox-pop.tox-pop--left::before{border-color:transparent #ccc transparent transparent;border-width:10px;margin-left:-19px}.tox .tox-pop.tox-pop--right::after,.tox .tox-pop.tox-pop--right::before{left:100%;top:calc(50% + 1px);transform:translateY(-50%)}.tox .tox-pop.tox-pop--right::after{border-color:transparent transparent transparent #fff;border-width:8px;margin-left:-1px}.tox .tox-pop.tox-pop--right::before{border-color:transparent transparent transparent #ccc;border-width:10px;margin-left:-1px}.tox .tox-pop.tox-pop--align-left::after,.tox .tox-pop.tox-pop--align-left::before{left:20px}.tox .tox-pop.tox-pop--align-right::after,.tox .tox-pop.tox-pop--align-right::before{left:calc(100% - 20px)}.tox .tox-sidebar-wrap{display:flex;flex-direction:row;flex-grow:1;-ms-flex-preferred-size:0;min-height:0}.tox .tox-sidebar{background-color:#fff;display:flex;flex-direction:row;justify-content:flex-end}.tox .tox-sidebar__slider{display:flex;overflow:hidden}.tox .tox-sidebar__pane-container{display:flex}.tox .tox-sidebar__pane{display:flex}.tox .tox-sidebar--sliding-closed{opacity:0}.tox .tox-sidebar--sliding-open{opacity:1}.tox .tox-sidebar--sliding-growing,.tox .tox-sidebar--sliding-shrinking{transition:width .5s ease,opacity .5s ease}.tox .tox-selector{background-color:#4099ff;border-color:#4099ff;border-style:solid;border-width:1px;box-sizing:border-box;display:inline-block;height:10px;position:absolute;width:10px}.tox.tox-platform-touch .tox-selector{height:12px;width:12px}.tox .tox-slider{align-items:center;display:flex;flex:1;-ms-flex-preferred-size:auto;height:24px;justify-content:center;position:relative}.tox .tox-slider__rail{background-color:transparent;border:1px solid #ccc;border-radius:3px;height:10px;min-width:120px;width:100%}.tox .tox-slider__handle{background-color:#207ab7;border:2px solid #185d8c;border-radius:3px;box-shadow:none;height:24px;left:50%;position:absolute;top:50%;transform:translateX(-50%) translateY(-50%);width:14px}.tox .tox-source-code{overflow:auto}.tox .tox-spinner{display:flex}.tox .tox-spinner>div{animation:tam-bouncing-dots 1.5s ease-in-out 0s infinite both;background-color:rgba(34,47,62,.7);border-radius:100%;height:8px;width:8px}.tox .tox-spinner>div:nth-child(1){animation-delay:-.32s}.tox .tox-spinner>div:nth-child(2){animation-delay:-.16s}@keyframes tam-bouncing-dots{0%,100%,80%{transform:scale(0)}40%{transform:scale(1)}}.tox:not([dir=rtl]) .tox-spinner>div:not(:first-child){margin-left:4px}.tox[dir=rtl] .tox-spinner>div:not(:first-child){margin-right:4px}.tox .tox-statusbar{align-items:center;background-color:#fff;border-top:1px solid #ccc;color:rgba(34,47,62,.7);display:flex;flex:0 0 auto;font-size:12px;font-weight:400;height:18px;overflow:hidden;padding:0 8px;position:relative;text-transform:uppercase}.tox .tox-statusbar__text-container{display:flex;flex:1 1 auto;justify-content:flex-end;overflow:hidden}.tox .tox-statusbar__path{display:flex;flex:1 1 auto;margin-right:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tox .tox-statusbar__path>*{display:inline;white-space:nowrap}.tox .tox-statusbar__wordcount{flex:0 0 auto;margin-left:1ch}.tox .tox-statusbar a,.tox .tox-statusbar__path-item,.tox .tox-statusbar__wordcount{color:rgba(34,47,62,.7);text-decoration:none}.tox .tox-statusbar a:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar a:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__path-item:hover:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:focus:not(:disabled):not([aria-disabled=true]),.tox .tox-statusbar__wordcount:hover:not(:disabled):not([aria-disabled=true]){cursor:pointer;text-decoration:underline}.tox .tox-statusbar__resize-handle{align-items:flex-end;align-self:stretch;cursor:nwse-resize;display:flex;flex:0 0 auto;justify-content:flex-end;margin-left:auto;margin-right:-8px;padding-left:1ch}.tox .tox-statusbar__resize-handle svg{display:block;fill:rgba(34,47,62,.7)}.tox .tox-statusbar__resize-handle:focus svg{background-color:#dee0e2;border-radius:1px;box-shadow:0 0 0 2px #dee0e2}.tox:not([dir=rtl]) .tox-statusbar__path>*{margin-right:4px}.tox:not([dir=rtl]) .tox-statusbar__branding{margin-left:1ch}.tox[dir=rtl] .tox-statusbar{flex-direction:row-reverse}.tox[dir=rtl] .tox-statusbar__path>*{margin-left:4px}.tox .tox-throbber{z-index:1299}.tox .tox-throbber__busy-spinner{align-items:center;background-color:rgba(255,255,255,.6);bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0}.tox .tox-tbtn{align-items:center;background:0 0;border:0;border-radius:3px;box-shadow:none;color:#222f3e;display:flex;flex:0 0 auto;font-size:14px;font-style:normal;font-weight:400;height:34px;justify-content:center;margin:2px 0 3px 0;outline:0;overflow:hidden;padding:0;text-transform:none;width:34px}.tox .tox-tbtn svg{display:block;fill:#222f3e}.tox .tox-tbtn.tox-tbtn-more{padding-left:5px;padding-right:5px;width:inherit}.tox .tox-tbtn:focus{background:#dee0e2;border:0;box-shadow:none}.tox .tox-tbtn:hover{background:#dee0e2;border:0;box-shadow:none;color:#222f3e}.tox .tox-tbtn:hover svg{fill:#222f3e}.tox .tox-tbtn:active{background:#c8cbcf;border:0;box-shadow:none;color:#222f3e}.tox .tox-tbtn:active svg{fill:#222f3e}.tox .tox-tbtn--disabled,.tox .tox-tbtn--disabled:hover,.tox .tox-tbtn:disabled,.tox .tox-tbtn:disabled:hover{background:0 0;border:0;box-shadow:none;color:rgba(34,47,62,.5);cursor:not-allowed}.tox .tox-tbtn--disabled svg,.tox .tox-tbtn--disabled:hover svg,.tox .tox-tbtn:disabled svg,.tox .tox-tbtn:disabled:hover svg{fill:rgba(34,47,62,.5)}.tox .tox-tbtn--enabled,.tox .tox-tbtn--enabled:hover{background:#c8cbcf;border:0;box-shadow:none;color:#222f3e}.tox .tox-tbtn--enabled:hover>*,.tox .tox-tbtn--enabled>*{transform:none}.tox .tox-tbtn--enabled svg,.tox .tox-tbtn--enabled:hover svg{fill:#222f3e}.tox .tox-tbtn:focus:not(.tox-tbtn--disabled){color:#222f3e}.tox .tox-tbtn:focus:not(.tox-tbtn--disabled) svg{fill:#222f3e}.tox .tox-tbtn:active>*{transform:none}.tox .tox-tbtn--md{height:51px;width:51px}.tox .tox-tbtn--lg{flex-direction:column;height:68px;width:68px}.tox .tox-tbtn--return{-ms-grid-row-align:stretch;align-self:stretch;height:unset;width:16px}.tox .tox-tbtn--labeled{padding:0 4px;width:unset}.tox .tox-tbtn__vlabel{display:block;font-size:10px;font-weight:400;letter-spacing:-.025em;margin-bottom:4px;white-space:nowrap}.tox .tox-tbtn--select{margin:2px 0 3px 0;padding:0 4px;width:auto}.tox .tox-tbtn__select-label{cursor:default;font-weight:400;margin:0 4px}.tox .tox-tbtn__select-chevron{align-items:center;display:flex;justify-content:center;width:16px}.tox .tox-tbtn__select-chevron svg{fill:rgba(34,47,62,.5)}.tox .tox-tbtn--bespoke .tox-tbtn__select-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:7em}.tox .tox-split-button{border:0;border-radius:3px;box-sizing:border-box;display:flex;margin:2px 0 3px 0;overflow:hidden}.tox .tox-split-button:hover{box-shadow:0 0 0 1px #dee0e2 inset}.tox .tox-split-button:focus{background:#dee0e2;box-shadow:none;color:#222f3e}.tox .tox-split-button>*{border-radius:0}.tox .tox-split-button__chevron{width:16px}.tox .tox-split-button__chevron svg{fill:rgba(34,47,62,.5)}.tox .tox-split-button .tox-tbtn{margin:0}.tox.tox-platform-touch .tox-split-button .tox-tbtn:first-child{width:30px}.tox.tox-platform-touch .tox-split-button__chevron{width:20px}.tox .tox-split-button.tox-tbtn--disabled .tox-tbtn:focus,.tox .tox-split-button.tox-tbtn--disabled .tox-tbtn:hover,.tox .tox-split-button.tox-tbtn--disabled:focus,.tox .tox-split-button.tox-tbtn--disabled:hover{background:0 0;box-shadow:none;color:rgba(34,47,62,.5)}.tox .tox-toolbar-overlord{background-color:#fff}.tox .tox-toolbar,.tox .tox-toolbar__overflow,.tox .tox-toolbar__primary{background:url("data:image/svg+xml;charset=utf8,%3Csvg height='39px' viewBox='0 0 40 39px' width='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='0' y='38px' width='100' height='1' fill='%23cccccc'/%3E%3C/svg%3E") left 0 top 0 #fff;background-color:#fff;display:flex;flex:0 0 auto;flex-shrink:0;flex-wrap:wrap;padding:0 0}.tox .tox-toolbar__overflow.tox-toolbar__overflow--closed{height:0;opacity:0;padding-bottom:0;padding-top:0;visibility:hidden}.tox .tox-toolbar__overflow--growing{transition:height .3s ease,opacity .2s linear .1s}.tox .tox-toolbar__overflow--shrinking{transition:opacity .3s ease,height .2s linear .1s,visibility 0s linear .3s}.tox .tox-menubar+.tox-toolbar,.tox .tox-menubar+.tox-toolbar-overlord .tox-toolbar__primary{border-top:1px solid #ccc;margin-top:-1px}.tox .tox-toolbar--scrolling{flex-wrap:nowrap;overflow-x:auto}.tox .tox-pop .tox-toolbar{border-width:0}.tox .tox-toolbar--no-divider{background-image:none}.tox-tinymce:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-toolbar-overlord:first-child .tox-toolbar__primary,.tox-tinymce:not(.tox-tinymce-inline) .tox-editor-header:not(:first-child) .tox-toolbar:first-child{border-top:1px solid #ccc}.tox.tox-tinymce-aux .tox-toolbar__overflow{background-color:#fff;border:1px solid #ccc;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,.15)}.tox .tox-toolbar__group{align-items:center;display:flex;flex-wrap:wrap;margin:0 0;padding:0 4px 0 4px}.tox .tox-toolbar__group--pull-right{margin-left:auto}.tox .tox-toolbar--scrolling .tox-toolbar__group{flex-shrink:0;flex-wrap:nowrap}.tox:not([dir=rtl]) .tox-toolbar__group:not(:last-of-type){border-right:1px solid #ccc}.tox[dir=rtl] .tox-toolbar__group:not(:last-of-type){border-left:1px solid #ccc}.tox .tox-tooltip{display:inline-block;padding:8px;position:relative}.tox .tox-tooltip__body{background-color:#222f3e;border-radius:3px;box-shadow:0 2px 4px rgba(34,47,62,.3);color:rgba(255,255,255,.75);font-size:14px;font-style:normal;font-weight:400;padding:4px 8px;text-transform:none}.tox .tox-tooltip__arrow{position:absolute}.tox .tox-tooltip--down .tox-tooltip__arrow{border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid #222f3e;bottom:0;left:50%;position:absolute;transform:translateX(-50%)}.tox .tox-tooltip--up .tox-tooltip__arrow{border-bottom:8px solid #222f3e;border-left:8px solid transparent;border-right:8px solid transparent;left:50%;position:absolute;top:0;transform:translateX(-50%)}.tox .tox-tooltip--right .tox-tooltip__arrow{border-bottom:8px solid transparent;border-left:8px solid #222f3e;border-top:8px solid transparent;position:absolute;right:0;top:50%;transform:translateY(-50%)}.tox .tox-tooltip--left .tox-tooltip__arrow{border-bottom:8px solid transparent;border-right:8px solid #222f3e;border-top:8px solid transparent;left:0;position:absolute;top:50%;transform:translateY(-50%)}.tox .tox-well{border:1px solid #ccc;border-radius:3px;padding:8px;width:100%}.tox .tox-well>:first-child{margin-top:0}.tox .tox-well>:last-child{margin-bottom:0}.tox .tox-well>:only-child{margin:0}.tox .tox-custom-editor{border:1px solid #ccc;border-radius:3px;display:flex;flex:1;position:relative}.tox .tox-dialog-loading::before{background-color:rgba(0,0,0,.5);content:"";height:100%;position:absolute;width:100%;z-index:1000}.tox .tox-tab{cursor:pointer}.tox .tox-dialog__content-js{display:flex;flex:1;-ms-flex-preferred-size:auto}.tox .tox-dialog__body-content .tox-collection{display:flex;flex:1;-ms-flex-preferred-size:auto}.tox .tox-image-tools-edit-panel{height:60px}.tox .tox-image-tools__sidebar{height:60px}
diff --git a/public/resource/tinymce/skins/ui/oxide/skin.mobile.min.css b/public/resource/tinymce/skins/ui/oxide/skin.mobile.min.css
new file mode 100644
index 0000000..3a45cac
--- /dev/null
+++ b/public/resource/tinymce/skins/ui/oxide/skin.mobile.min.css
@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+.tinymce-mobile-outer-container{all:initial;display:block}.tinymce-mobile-outer-container *{border:0;box-sizing:initial;cursor:inherit;float:none;line-height:1;margin:0;outline:0;padding:0;-webkit-tap-highlight-color:transparent;text-shadow:none;white-space:nowrap}.tinymce-mobile-icon-arrow-back::before{content:"\e5cd"}.tinymce-mobile-icon-image::before{content:"\e412"}.tinymce-mobile-icon-cancel-circle::before{content:"\e5c9"}.tinymce-mobile-icon-full-dot::before{content:"\e061"}.tinymce-mobile-icon-align-center::before{content:"\e234"}.tinymce-mobile-icon-align-left::before{content:"\e236"}.tinymce-mobile-icon-align-right::before{content:"\e237"}.tinymce-mobile-icon-bold::before{content:"\e238"}.tinymce-mobile-icon-italic::before{content:"\e23f"}.tinymce-mobile-icon-unordered-list::before{content:"\e241"}.tinymce-mobile-icon-ordered-list::before{content:"\e242"}.tinymce-mobile-icon-font-size::before{content:"\e245"}.tinymce-mobile-icon-underline::before{content:"\e249"}.tinymce-mobile-icon-link::before{content:"\e157"}.tinymce-mobile-icon-unlink::before{content:"\eca2"}.tinymce-mobile-icon-color::before{content:"\e891"}.tinymce-mobile-icon-previous::before{content:"\e314"}.tinymce-mobile-icon-next::before{content:"\e315"}.tinymce-mobile-icon-large-font::before,.tinymce-mobile-icon-style-formats::before{content:"\e264"}.tinymce-mobile-icon-undo::before{content:"\e166"}.tinymce-mobile-icon-redo::before{content:"\e15a"}.tinymce-mobile-icon-removeformat::before{content:"\e239"}.tinymce-mobile-icon-small-font::before{content:"\e906"}.tinymce-mobile-format-matches::after,.tinymce-mobile-icon-readonly-back::before{content:"\e5ca"}.tinymce-mobile-icon-small-heading::before{content:"small"}.tinymce-mobile-icon-large-heading::before{content:"large"}.tinymce-mobile-icon-large-heading::before,.tinymce-mobile-icon-small-heading::before{font-family:sans-serif;font-size:80%}.tinymce-mobile-mask-edit-icon::before{content:"\e254"}.tinymce-mobile-icon-back::before{content:"\e5c4"}.tinymce-mobile-icon-heading::before{content:"Headings";font-family:sans-serif;font-size:80%;font-weight:700}.tinymce-mobile-icon-h1::before{content:"H1";font-weight:700}.tinymce-mobile-icon-h2::before{content:"H2";font-weight:700}.tinymce-mobile-icon-h3::before{content:"H3";font-weight:700}.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask{align-items:center;display:flex;justify-content:center;background:rgba(51,51,51,.5);height:100%;position:absolute;top:0;width:100%}.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container{align-items:center;border-radius:50%;display:flex;flex-direction:column;font-family:sans-serif;font-size:1em;justify-content:space-between}.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .mixin-menu-item{align-items:center;display:flex;justify-content:center;border-radius:50%;height:2.1em;width:2.1em}.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section{align-items:center;display:flex;justify-content:center;flex-direction:column;font-size:1em}@media only screen and (min-device-width:700px){.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section{font-size:1.2em}}.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section .tinymce-mobile-mask-tap-icon{align-items:center;display:flex;justify-content:center;border-radius:50%;height:2.1em;width:2.1em;background-color:#fff;color:#207ab7}.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section .tinymce-mobile-mask-tap-icon::before{content:"\e900";font-family:tinymce-mobile,sans-serif}.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section:not(.tinymce-mobile-mask-tap-icon-selected) .tinymce-mobile-mask-tap-icon{z-index:2}.tinymce-mobile-android-container.tinymce-mobile-android-maximized{background:#fff;border:none;bottom:0;display:flex;flex-direction:column;left:0;position:fixed;right:0;top:0}.tinymce-mobile-android-container:not(.tinymce-mobile-android-maximized){position:relative}.tinymce-mobile-android-container .tinymce-mobile-editor-socket{display:flex;flex-grow:1}.tinymce-mobile-android-container .tinymce-mobile-editor-socket iframe{display:flex!important;flex-grow:1;height:auto!important}.tinymce-mobile-android-scroll-reload{overflow:hidden}:not(.tinymce-mobile-readonly-mode)>.tinymce-mobile-android-selection-context-toolbar{margin-top:23px}.tinymce-mobile-toolstrip{background:#fff;display:flex;flex:0 0 auto;z-index:1}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar{align-items:center;background-color:#fff;border-bottom:1px solid #ccc;display:flex;flex:1;height:2.5em;width:100%}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group{align-items:center;display:flex;height:100%;flex-shrink:1}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group>div{align-items:center;display:flex;height:100%;flex:1}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group.tinymce-mobile-exit-container{background:#f44336}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group.tinymce-mobile-toolbar-scrollable-group{flex-grow:1}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item{padding-left:.5em;padding-right:.5em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item.tinymce-mobile-toolbar-button{align-items:center;display:flex;height:80%;margin-left:2px;margin-right:2px}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item.tinymce-mobile-toolbar-button.tinymce-mobile-toolbar-button-selected{background:#c8cbcf;color:#ccc}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group:first-of-type,.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group:last-of-type{background:#207ab7;color:#eceff1}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group{align-items:center;display:flex;height:100%;flex:1;padding-bottom:.4em;padding-top:.4em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog{display:flex;min-height:1.5em;overflow:hidden;padding-left:0;padding-right:0;position:relative;width:100%}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain{display:flex;height:100%;transition:left cubic-bezier(.4,0,1,1) .15s;width:100%}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen{display:flex;flex:0 0 auto;justify-content:space-between;width:100%}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen input{font-family:Sans-serif}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-input-container{display:flex;flex-grow:1;position:relative}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-input-container .tinymce-mobile-input-container-x{-ms-grid-row-align:center;align-self:center;background:inherit;border:none;border-radius:50%;color:#888;font-size:.6em;font-weight:700;height:100%;padding-right:2px;position:absolute;right:0}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-input-container.tinymce-mobile-input-container-empty .tinymce-mobile-input-container-x{display:none}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-next,.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-previous{align-items:center;display:flex}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-next::before,.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-previous::before{align-items:center;display:flex;font-weight:700;height:100%;padding-left:.5em;padding-right:.5em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-next.tinymce-mobile-toolbar-navigation-disabled::before,.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-previous.tinymce-mobile-toolbar-navigation-disabled::before{visibility:hidden}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-dot-item{color:#ccc;font-size:10px;line-height:10px;margin:0 2px;padding-top:3px}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-dot-item.tinymce-mobile-dot-active{color:#c8cbcf}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-icon-large-font::before,.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-icon-large-heading::before{margin-left:.5em;margin-right:.9em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-icon-small-font::before,.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-icon-small-heading::before{margin-left:.9em;margin-right:.5em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider{display:flex;flex:1;margin-left:0;margin-right:0;padding:.28em 0;position:relative}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider .tinymce-mobile-slider-size-container{align-items:center;display:flex;flex-grow:1;height:100%}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider .tinymce-mobile-slider-size-container .tinymce-mobile-slider-size-line{background:#ccc;display:flex;flex:1;height:.2em;margin-bottom:.3em;margin-top:.3em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container{padding-left:2em;padding-right:2em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container .tinymce-mobile-slider-gradient-container{align-items:center;display:flex;flex-grow:1;height:100%}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container .tinymce-mobile-slider-gradient-container .tinymce-mobile-slider-gradient{background:linear-gradient(to right,red 0,#feff00 17%,#0f0 33%,#00feff 50%,#00f 67%,#ff00fe 83%,red 100%);display:flex;flex:1;height:.2em;margin-bottom:.3em;margin-top:.3em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container .tinymce-mobile-hue-slider-black{background:#000;height:.2em;margin-bottom:.3em;margin-top:.3em;width:1.2em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container .tinymce-mobile-hue-slider-white{background:#fff;height:.2em;margin-bottom:.3em;margin-top:.3em;width:1.2em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider .tinymce-mobile-slider-thumb{align-items:center;background-clip:padding-box;background-color:#455a64;border:.5em solid rgba(136,136,136,0);border-radius:3em;bottom:0;color:#fff;display:flex;height:.5em;justify-content:center;left:-10px;margin:auto;position:absolute;top:0;transition:border 120ms cubic-bezier(.39,.58,.57,1);width:.5em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider .tinymce-mobile-slider-thumb.tinymce-mobile-thumb-active{border:.5em solid rgba(136,136,136,.39)}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serializer-wrapper,.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group>div{align-items:center;display:flex;height:100%;flex:1}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serializer-wrapper{flex-direction:column;justify-content:center}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item{align-items:center;display:flex}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item:not(.tinymce-mobile-serialised-dialog){height:100%}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-dot-container{display:flex}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group input{background:#fff;border:none;border-radius:0;color:#455a64;flex-grow:1;font-size:.85em;padding-bottom:.1em;padding-left:5px;padding-top:.1em}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group input::-webkit-input-placeholder{color:#888}.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group input::placeholder{color:#888}.tinymce-mobile-dropup{background:#fff;display:flex;overflow:hidden;width:100%}.tinymce-mobile-dropup.tinymce-mobile-dropup-shrinking{transition:height .3s ease-out}.tinymce-mobile-dropup.tinymce-mobile-dropup-growing{transition:height .3s ease-in}.tinymce-mobile-dropup.tinymce-mobile-dropup-closed{flex-grow:0}.tinymce-mobile-dropup.tinymce-mobile-dropup-open:not(.tinymce-mobile-dropup-growing){flex-grow:1}.tinymce-mobile-ios-container .tinymce-mobile-dropup:not(.tinymce-mobile-dropup-closed){min-height:200px}@media only screen and (orientation:landscape){.tinymce-mobile-dropup:not(.tinymce-mobile-dropup-closed){min-height:200px}}@media only screen and (min-device-width :320px) and (max-device-width :568px) and (orientation :landscape){.tinymce-mobile-ios-container .tinymce-mobile-dropup:not(.tinymce-mobile-dropup-closed){min-height:150px}}.tinymce-mobile-styles-menu{font-family:sans-serif;outline:4px solid #000;overflow:hidden;position:relative;width:100%}.tinymce-mobile-styles-menu [role=menu]{display:flex;flex-direction:column;height:100%;position:absolute;width:100%}.tinymce-mobile-styles-menu [role=menu].transitioning{transition:transform .5s ease-in-out}.tinymce-mobile-styles-menu .tinymce-mobile-styles-item{border-bottom:1px solid #ddd;color:#455a64;cursor:pointer;display:flex;padding:1em 1em;position:relative}.tinymce-mobile-styles-menu .tinymce-mobile-styles-collapser .tinymce-mobile-styles-collapse-icon::before{color:#455a64;content:"\e314";font-family:tinymce-mobile,sans-serif}.tinymce-mobile-styles-menu .tinymce-mobile-styles-item.tinymce-mobile-styles-item-is-menu::after{color:#455a64;content:"\e315";font-family:tinymce-mobile,sans-serif;padding-left:1em;padding-right:1em;position:absolute;right:0}.tinymce-mobile-styles-menu .tinymce-mobile-styles-item.tinymce-mobile-format-matches::after{font-family:tinymce-mobile,sans-serif;padding-left:1em;padding-right:1em;position:absolute;right:0}.tinymce-mobile-styles-menu .tinymce-mobile-styles-collapser,.tinymce-mobile-styles-menu .tinymce-mobile-styles-separator{align-items:center;background:#fff;border-top:#455a64;color:#455a64;display:flex;min-height:2.5em;padding-left:1em;padding-right:1em}.tinymce-mobile-styles-menu [data-transitioning-destination=before][data-transitioning-state],.tinymce-mobile-styles-menu [data-transitioning-state=before]{transform:translate(-100%)}.tinymce-mobile-styles-menu [data-transitioning-destination=current][data-transitioning-state],.tinymce-mobile-styles-menu [data-transitioning-state=current]{transform:translate(0)}.tinymce-mobile-styles-menu [data-transitioning-destination=after][data-transitioning-state],.tinymce-mobile-styles-menu [data-transitioning-state=after]{transform:translate(100%)}@font-face{font-family:tinymce-mobile;font-style:normal;font-weight:400;src:url(fonts/tinymce-mobile.woff?8x92w3) format('woff')}@media (min-device-width:700px){.tinymce-mobile-outer-container,.tinymce-mobile-outer-container input{font-size:25px}}@media (max-device-width:700px){.tinymce-mobile-outer-container,.tinymce-mobile-outer-container input{font-size:18px}}.tinymce-mobile-icon{font-family:tinymce-mobile,sans-serif}.mixin-flex-and-centre{align-items:center;display:flex;justify-content:center}.mixin-flex-bar{align-items:center;display:flex;height:100%}.tinymce-mobile-outer-container .tinymce-mobile-editor-socket iframe{background-color:#fff;width:100%}.tinymce-mobile-editor-socket .tinymce-mobile-mask-edit-icon{background-color:#207ab7;border-radius:50%;bottom:1em;color:#fff;font-size:1em;height:2.1em;position:fixed;right:2em;width:2.1em;align-items:center;display:flex;justify-content:center}@media only screen and (min-device-width:700px){.tinymce-mobile-editor-socket .tinymce-mobile-mask-edit-icon{font-size:1.2em}}.tinymce-mobile-outer-container:not(.tinymce-mobile-fullscreen-maximized) .tinymce-mobile-editor-socket{height:300px;overflow:hidden}.tinymce-mobile-outer-container:not(.tinymce-mobile-fullscreen-maximized) .tinymce-mobile-editor-socket iframe{height:100%}.tinymce-mobile-outer-container:not(.tinymce-mobile-fullscreen-maximized) .tinymce-mobile-toolstrip{display:none}input[type=file]::-webkit-file-upload-button{display:none}@media only screen and (min-device-width :320px) and (max-device-width :568px) and (orientation :landscape){.tinymce-mobile-ios-container .tinymce-mobile-editor-socket .tinymce-mobile-mask-edit-icon{bottom:50%}}
diff --git a/public/resource/tinymce/skins/ui/oxide/skin.shadowdom.min.css b/public/resource/tinymce/skins/ui/oxide/skin.shadowdom.min.css
new file mode 100644
index 0000000..a0893b9
--- /dev/null
+++ b/public/resource/tinymce/skins/ui/oxide/skin.shadowdom.min.css
@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+body.tox-dialog__disable-scroll{overflow:hidden}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;-ms-scroll-chaining:none;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..18bcdbd
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/api/demo/account.ts b/src/api/demo/account.ts
new file mode 100644
index 0000000..d4d9c27
--- /dev/null
+++ b/src/api/demo/account.ts
@@ -0,0 +1,16 @@
+import { defHttp } from '@/utils/http/axios';
+import { GetAccountInfoModel } from './model/accountModel';
+
+enum Api {
+ ACCOUNT_INFO = '/account/getAccountInfo',
+ SESSION_TIMEOUT = '/user/sessionTimeout',
+ TOKEN_EXPIRED = '/user/tokenExpired',
+}
+
+// Get personal center-basic settings
+
+export const accountInfoApi = () => defHttp.get({ url: Api.ACCOUNT_INFO });
+
+export const sessionTimeoutApi = () => defHttp.post({ url: Api.SESSION_TIMEOUT });
+
+export const tokenExpiredApi = () => defHttp.post({ url: Api.TOKEN_EXPIRED });
diff --git a/src/api/demo/cascader.ts b/src/api/demo/cascader.ts
new file mode 100644
index 0000000..198853d
--- /dev/null
+++ b/src/api/demo/cascader.ts
@@ -0,0 +1,9 @@
+import { defHttp } from '@/utils/http/axios';
+import { AreaModel, AreaParams } from '@/api/demo/model/areaModel';
+
+enum Api {
+ AREA_RECORD = '/cascader/getAreaRecord',
+}
+
+export const areaRecord = (data: AreaParams) =>
+ defHttp.post({ url: Api.AREA_RECORD, data });
diff --git a/src/api/demo/error.ts b/src/api/demo/error.ts
new file mode 100644
index 0000000..0a7f662
--- /dev/null
+++ b/src/api/demo/error.ts
@@ -0,0 +1,12 @@
+import { defHttp } from '@/utils/http/axios';
+
+enum Api {
+ // The address does not exist
+ Error = '/error',
+}
+
+/**
+ * @description: Trigger ajax error
+ */
+
+export const fireErrorApi = () => defHttp.get({ url: Api.Error });
diff --git a/src/api/demo/model/accountModel.ts b/src/api/demo/model/accountModel.ts
new file mode 100644
index 0000000..4594393
--- /dev/null
+++ b/src/api/demo/model/accountModel.ts
@@ -0,0 +1,7 @@
+export interface GetAccountInfoModel {
+ email: string;
+ name: string;
+ introduction: string;
+ phone: string;
+ address: string;
+}
diff --git a/src/api/demo/model/areaModel.ts b/src/api/demo/model/areaModel.ts
new file mode 100644
index 0000000..dfaa481
--- /dev/null
+++ b/src/api/demo/model/areaModel.ts
@@ -0,0 +1,12 @@
+export interface AreaModel {
+ id: string;
+ code: string;
+ parentCode: string;
+ name: string;
+ levelType: number;
+ [key: string]: string | number;
+}
+
+export interface AreaParams {
+ parentCode: string;
+}
diff --git a/src/api/demo/model/optionsModel.ts b/src/api/demo/model/optionsModel.ts
new file mode 100644
index 0000000..871ae9f
--- /dev/null
+++ b/src/api/demo/model/optionsModel.ts
@@ -0,0 +1,15 @@
+import { BasicFetchResult } from '@/api/model/baseModel';
+
+export interface DemoOptionsItem {
+ name: string;
+ id: string;
+}
+
+export interface selectParams {
+ id: number | string;
+}
+
+/**
+ * @description: Request list return value
+ */
+export type DemoOptionsGetResultModel = BasicFetchResult;
diff --git a/src/api/demo/model/systemModel.ts b/src/api/demo/model/systemModel.ts
new file mode 100644
index 0000000..0e5f8fa
--- /dev/null
+++ b/src/api/demo/model/systemModel.ts
@@ -0,0 +1,75 @@
+import { BasicPageParams, BasicFetchResult } from '@/api/model/baseModel';
+
+export type AccountParams = BasicPageParams & {
+ account?: string;
+ nickname?: string;
+ [key: string]: any;
+};
+
+export type RoleParams = {
+ roleName?: string;
+ status?: string;
+};
+
+export type RolePageParams = BasicPageParams & RoleParams;
+
+export type DeptParams = {
+ deptName?: string;
+ status?: string;
+};
+
+export type MenuParams = {
+ menuName?: string;
+ status?: string;
+};
+
+export interface AccountListItem {
+ id: string;
+ account: string;
+ email: string;
+ nickname: string;
+ role: number;
+ createTime: string;
+ remark: string;
+ status: number;
+}
+
+export interface DeptListItem {
+ id: string;
+ orderNo: string;
+ createTime: string;
+ remark: string;
+ status: number;
+}
+
+export interface MenuListItem {
+ id: string;
+ orderNo: string;
+ createTime: string;
+ status: number;
+ icon: string;
+ component: string;
+ permission: string;
+}
+
+export interface RoleListItem {
+ id: string;
+ roleName: string;
+ roleValue: string;
+ status: number;
+ orderNo: string;
+ createTime: string;
+}
+
+/**
+ * @description: Request list return value
+ */
+export type AccountListGetResultModel = BasicFetchResult;
+
+export type DeptListGetResultModel = BasicFetchResult;
+
+export type MenuListGetResultModel = BasicFetchResult;
+
+export type RolePageListGetResultModel = BasicFetchResult;
+
+export type RoleListGetResultModel = RoleListItem[];
diff --git a/src/api/demo/model/tableModel.ts b/src/api/demo/model/tableModel.ts
new file mode 100644
index 0000000..8f1eea5
--- /dev/null
+++ b/src/api/demo/model/tableModel.ts
@@ -0,0 +1,20 @@
+import { BasicPageParams, BasicFetchResult } from '@/api/model/baseModel';
+/**
+ * @description: Request list interface parameters
+ */
+export type DemoParams = Partial;
+
+export interface DemoListItem {
+ id: string;
+ beginTime: string;
+ endTime: string;
+ address: string;
+ name: string;
+ no: number;
+ status: number;
+}
+
+/**
+ * @description: Request list return value
+ */
+export type DemoListGetResultModel = BasicFetchResult;
diff --git a/src/api/demo/select.ts b/src/api/demo/select.ts
new file mode 100644
index 0000000..f5f5cf9
--- /dev/null
+++ b/src/api/demo/select.ts
@@ -0,0 +1,12 @@
+import { defHttp } from '@/utils/http/axios';
+import { DemoOptionsItem, selectParams } from './model/optionsModel';
+
+enum Api {
+ OPTIONS_LIST = '/select/getDemoOptions',
+}
+
+/**
+ * @description: Get sample options value
+ */
+export const optionsListApi = (params?: selectParams) =>
+ defHttp.get({ url: Api.OPTIONS_LIST, params });
diff --git a/src/api/demo/system.ts b/src/api/demo/system.ts
new file mode 100644
index 0000000..980e58c
--- /dev/null
+++ b/src/api/demo/system.ts
@@ -0,0 +1,44 @@
+import {
+ AccountParams,
+ DeptListItem,
+ MenuParams,
+ RoleParams,
+ RolePageParams,
+ MenuListGetResultModel,
+ DeptListGetResultModel,
+ AccountListGetResultModel,
+ RolePageListGetResultModel,
+ RoleListGetResultModel,
+} from './model/systemModel';
+import { defHttp } from '@/utils/http/axios';
+
+enum Api {
+ AccountList = '/system/getAccountList',
+ IsAccountExist = '/system/accountExist',
+ DeptList = '/system/getDeptList',
+ setRoleStatus = '/system/setRoleStatus',
+ MenuList = '/system/getMenuList',
+ RolePageList = '/system/getRoleListByPage',
+ GetAllRoleList = '/system/getAllRoleList',
+}
+
+export const getAccountList = (params: AccountParams) =>
+ defHttp.get({ url: Api.AccountList, params });
+
+export const getDeptList = (params?: DeptListItem) =>
+ defHttp.get({ url: Api.DeptList, params });
+
+export const getMenuList = (params?: MenuParams) =>
+ defHttp.get({ url: Api.MenuList, params });
+
+export const getRoleListByPage = (params?: RolePageParams) =>
+ defHttp.get({ url: Api.RolePageList, params });
+
+export const getAllRoleList = (params?: RoleParams) =>
+ defHttp.get({ url: Api.GetAllRoleList, params });
+
+export const setRoleStatus = (id: number, status: string) =>
+ defHttp.post({ url: Api.setRoleStatus, params: { id, status } });
+
+export const isAccountExist = (account: string) =>
+ defHttp.post({ url: Api.IsAccountExist, params: { account } }, { errorMessageMode: 'none' });
diff --git a/src/api/model/baseModel.ts b/src/api/model/baseModel.ts
new file mode 100644
index 0000000..1a36511
--- /dev/null
+++ b/src/api/model/baseModel.ts
@@ -0,0 +1,9 @@
+export interface BasicPageParams {
+ page: number;
+ pageSize: number;
+}
+
+export interface BasicFetchResult {
+ items: T[];
+ total: number;
+}
diff --git a/src/api/sys/menu.ts b/src/api/sys/menu.ts
new file mode 100644
index 0000000..27fb826
--- /dev/null
+++ b/src/api/sys/menu.ts
@@ -0,0 +1,14 @@
+import { defHttp } from '@/utils/http/axios';
+import { getMenuListResultModel } from './model/menuModel';
+
+enum Api {
+ GetMenuList = '/getMenuList',
+}
+
+/**
+ * @description: Get user menu based on id
+ */
+
+export const getMenuList = () => {
+ return defHttp.get({ url: Api.GetMenuList });
+};
diff --git a/src/api/sys/model/menuModel.ts b/src/api/sys/model/menuModel.ts
new file mode 100644
index 0000000..c2064a1
--- /dev/null
+++ b/src/api/sys/model/menuModel.ts
@@ -0,0 +1,17 @@
+import type { RouteMeta } from 'vue-router';
+
+export interface RouteItem {
+ path: string;
+ component: any;
+ meta: RouteMeta;
+ name?: string;
+ alias?: string | string[];
+ redirect?: string;
+ caseSensitive?: boolean;
+ children?: RouteItem[];
+}
+
+/**
+ * @description: Get menu return value
+ */
+export type getMenuListResultModel = RouteItem[];
diff --git a/src/api/sys/model/uploadModel.ts b/src/api/sys/model/uploadModel.ts
new file mode 100644
index 0000000..d770c64
--- /dev/null
+++ b/src/api/sys/model/uploadModel.ts
@@ -0,0 +1,5 @@
+export interface UploadApiResult {
+ message: string;
+ code: number;
+ url: string;
+}
diff --git a/src/api/sys/model/userModel.ts b/src/api/sys/model/userModel.ts
new file mode 100644
index 0000000..3869219
--- /dev/null
+++ b/src/api/sys/model/userModel.ts
@@ -0,0 +1,38 @@
+/**
+ * @description: Login interface parameters
+ */
+export interface LoginParams {
+ username: string;
+ password: string;
+}
+
+export interface RoleInfo {
+ roleName: string;
+ value: string;
+}
+
+/**
+ * @description: Login interface return value
+ */
+export interface LoginResultModel {
+ userId: string | number;
+ token: string;
+ roles: RoleInfo[];
+}
+
+/**
+ * @description: Get user information return value
+ */
+export interface GetUserInfoModel {
+ roles: RoleInfo[];
+ // 用户id
+ userId: string | number;
+ // 用户名
+ username: string;
+ // 真实名字
+ realName: string;
+ // 头像
+ avatar: string;
+ // 介绍
+ desc?: string;
+}
diff --git a/src/api/sys/upload.ts b/src/api/sys/upload.ts
new file mode 100644
index 0000000..2c4e191
--- /dev/null
+++ b/src/api/sys/upload.ts
@@ -0,0 +1,23 @@
+import { UploadApiResult } from './model/uploadModel';
+import { defHttp } from '@/utils/http/axios';
+import { UploadFileParams } from '#/axios';
+import { useGlobSetting } from '@/hooks/setting';
+import { AxiosProgressEvent } from 'axios';
+
+const { uploadUrl = '' } = useGlobSetting();
+
+/**
+ * @description: Upload interface
+ */
+export function uploadApi(
+ params: UploadFileParams,
+ onUploadProgress: (progressEvent: AxiosProgressEvent) => void,
+) {
+ return defHttp.uploadFile(
+ {
+ url: uploadUrl,
+ onUploadProgress,
+ },
+ params,
+ );
+}
diff --git a/src/api/sys/user.ts b/src/api/sys/user.ts
new file mode 100644
index 0000000..511fca2
--- /dev/null
+++ b/src/api/sys/user.ts
@@ -0,0 +1,55 @@
+import { defHttp } from '@/utils/http/axios';
+import { LoginParams, LoginResultModel, GetUserInfoModel } from './model/userModel';
+
+import { ErrorMessageMode } from '#/axios';
+
+enum Api {
+ Login = '/login',
+ Logout = '/logout',
+ GetUserInfo = '/getUserInfo',
+ GetPermCode = '/getPermCode',
+ TestRetry = '/testRetry',
+}
+
+/**
+ * @description: user login api
+ */
+export function loginApi(params: LoginParams, mode: ErrorMessageMode = 'modal') {
+ return defHttp.post(
+ {
+ url: Api.Login,
+ params,
+ },
+ {
+ errorMessageMode: mode,
+ },
+ );
+}
+
+/**
+ * @description: getUserInfo
+ */
+export function getUserInfo() {
+ return defHttp.get({ url: Api.GetUserInfo }, { errorMessageMode: 'none' });
+}
+
+export function getPermCode() {
+ return defHttp.get({ url: Api.GetPermCode });
+}
+
+export function doLogout() {
+ return defHttp.get({ url: Api.Logout });
+}
+
+export function testRetry() {
+ return defHttp.get(
+ { url: Api.TestRetry },
+ {
+ retryRequest: {
+ isOpenRetry: true,
+ count: 5,
+ waitTime: 1000,
+ },
+ },
+ );
+}
diff --git a/src/assets/icons/download-count.svg b/src/assets/icons/download-count.svg
new file mode 100644
index 0000000..1c95195
--- /dev/null
+++ b/src/assets/icons/download-count.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/dynamic-avatar-1.svg b/src/assets/icons/dynamic-avatar-1.svg
new file mode 100644
index 0000000..e1553e5
--- /dev/null
+++ b/src/assets/icons/dynamic-avatar-1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/dynamic-avatar-2.svg b/src/assets/icons/dynamic-avatar-2.svg
new file mode 100644
index 0000000..c4c1722
--- /dev/null
+++ b/src/assets/icons/dynamic-avatar-2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/dynamic-avatar-3.svg b/src/assets/icons/dynamic-avatar-3.svg
new file mode 100644
index 0000000..81145f9
--- /dev/null
+++ b/src/assets/icons/dynamic-avatar-3.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/dynamic-avatar-4.svg b/src/assets/icons/dynamic-avatar-4.svg
new file mode 100644
index 0000000..e586ed4
--- /dev/null
+++ b/src/assets/icons/dynamic-avatar-4.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/dynamic-avatar-5.svg b/src/assets/icons/dynamic-avatar-5.svg
new file mode 100644
index 0000000..746e4b8
--- /dev/null
+++ b/src/assets/icons/dynamic-avatar-5.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/dynamic-avatar-6.svg b/src/assets/icons/dynamic-avatar-6.svg
new file mode 100644
index 0000000..b2432f2
--- /dev/null
+++ b/src/assets/icons/dynamic-avatar-6.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/moon.svg b/src/assets/icons/moon.svg
new file mode 100644
index 0000000..e6667f0
--- /dev/null
+++ b/src/assets/icons/moon.svg
@@ -0,0 +1,16 @@
+
+
diff --git a/src/assets/icons/sun.svg b/src/assets/icons/sun.svg
new file mode 100644
index 0000000..a3997cb
--- /dev/null
+++ b/src/assets/icons/sun.svg
@@ -0,0 +1,42 @@
+
+
diff --git a/src/assets/icons/test.svg b/src/assets/icons/test.svg
new file mode 100644
index 0000000..244252d
--- /dev/null
+++ b/src/assets/icons/test.svg
@@ -0,0 +1,21 @@
+
+
\ No newline at end of file
diff --git a/src/assets/icons/total-sales.svg b/src/assets/icons/total-sales.svg
new file mode 100644
index 0000000..eff7964
--- /dev/null
+++ b/src/assets/icons/total-sales.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/transaction.svg b/src/assets/icons/transaction.svg
new file mode 100644
index 0000000..7ba9e2f
--- /dev/null
+++ b/src/assets/icons/transaction.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/visit-count.svg b/src/assets/icons/visit-count.svg
new file mode 100644
index 0000000..ba2a306
--- /dev/null
+++ b/src/assets/icons/visit-count.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/images/demo.png b/src/assets/images/demo.png
new file mode 100644
index 0000000..1a45c98
Binary files /dev/null and b/src/assets/images/demo.png differ
diff --git a/src/assets/images/header.jpg b/src/assets/images/header.jpg
new file mode 100644
index 0000000..977584b
Binary files /dev/null and b/src/assets/images/header.jpg differ
diff --git a/src/assets/images/logo.png b/src/assets/images/logo.png
new file mode 100644
index 0000000..16f2e7a
Binary files /dev/null and b/src/assets/images/logo.png differ
diff --git a/src/assets/svg/illustration.svg b/src/assets/svg/illustration.svg
new file mode 100644
index 0000000..b45215b
--- /dev/null
+++ b/src/assets/svg/illustration.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/svg/login-bg-dark.svg b/src/assets/svg/login-bg-dark.svg
new file mode 100644
index 0000000..888da7a
--- /dev/null
+++ b/src/assets/svg/login-bg-dark.svg
@@ -0,0 +1,19 @@
+
diff --git a/src/assets/svg/login-bg.svg b/src/assets/svg/login-bg.svg
new file mode 100644
index 0000000..7b66baf
--- /dev/null
+++ b/src/assets/svg/login-bg.svg
@@ -0,0 +1,17 @@
+
diff --git a/src/assets/svg/login-box-bg.svg b/src/assets/svg/login-box-bg.svg
new file mode 100644
index 0000000..ee7dbdc
--- /dev/null
+++ b/src/assets/svg/login-box-bg.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/svg/net-error.svg b/src/assets/svg/net-error.svg
new file mode 100644
index 0000000..81f2004
--- /dev/null
+++ b/src/assets/svg/net-error.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/svg/no-data.svg b/src/assets/svg/no-data.svg
new file mode 100644
index 0000000..2b9f257
--- /dev/null
+++ b/src/assets/svg/no-data.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/svg/preview/p-rotate.svg b/src/assets/svg/preview/p-rotate.svg
new file mode 100644
index 0000000..5153a81
--- /dev/null
+++ b/src/assets/svg/preview/p-rotate.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/svg/preview/resume.svg b/src/assets/svg/preview/resume.svg
new file mode 100644
index 0000000..0e86c5f
--- /dev/null
+++ b/src/assets/svg/preview/resume.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/svg/preview/scale.svg b/src/assets/svg/preview/scale.svg
new file mode 100644
index 0000000..1f7adae
--- /dev/null
+++ b/src/assets/svg/preview/scale.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/svg/preview/unrotate.svg b/src/assets/svg/preview/unrotate.svg
new file mode 100644
index 0000000..e4708be
--- /dev/null
+++ b/src/assets/svg/preview/unrotate.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/svg/preview/unscale.svg b/src/assets/svg/preview/unscale.svg
new file mode 100644
index 0000000..1359b34
--- /dev/null
+++ b/src/assets/svg/preview/unscale.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/Application/index.ts b/src/components/Application/index.ts
new file mode 100644
index 0000000..e97d33e
--- /dev/null
+++ b/src/components/Application/index.ts
@@ -0,0 +1,15 @@
+import { withInstall } from '@/utils';
+
+import appLogo from './src/AppLogo.vue';
+import appProvider from './src/AppProvider.vue';
+import appSearch from './src/search/AppSearch.vue';
+import appLocalePicker from './src/AppLocalePicker.vue';
+import appDarkModeToggle from './src/AppDarkModeToggle.vue';
+
+export { useAppProviderContext } from './src/useAppContext';
+
+export const AppLogo = withInstall(appLogo);
+export const AppProvider = withInstall(appProvider);
+export const AppSearch = withInstall(appSearch);
+export const AppLocalePicker = withInstall(appLocalePicker);
+export const AppDarkModeToggle = withInstall(appDarkModeToggle);
diff --git a/src/components/Application/src/AppDarkModeToggle.vue b/src/components/Application/src/AppDarkModeToggle.vue
new file mode 100644
index 0000000..6b912f1
--- /dev/null
+++ b/src/components/Application/src/AppDarkModeToggle.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
diff --git a/src/components/Application/src/AppLocalePicker.vue b/src/components/Application/src/AppLocalePicker.vue
new file mode 100644
index 0000000..f44a2f7
--- /dev/null
+++ b/src/components/Application/src/AppLocalePicker.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+ {{ getLocaleText }}
+
+
+
+
+
+
diff --git a/src/components/Application/src/AppLogo.vue b/src/components/Application/src/AppLogo.vue
new file mode 100644
index 0000000..73e48ac
--- /dev/null
+++ b/src/components/Application/src/AppLogo.vue
@@ -0,0 +1,93 @@
+
+
+
+

+
+ {{ title }}
+
+
+
+
+
diff --git a/src/components/Application/src/AppProvider.vue b/src/components/Application/src/AppProvider.vue
new file mode 100644
index 0000000..5481802
--- /dev/null
+++ b/src/components/Application/src/AppProvider.vue
@@ -0,0 +1,82 @@
+
diff --git a/src/components/Application/src/search/AppSearch.vue b/src/components/Application/src/search/AppSearch.vue
new file mode 100644
index 0000000..e017f42
--- /dev/null
+++ b/src/components/Application/src/search/AppSearch.vue
@@ -0,0 +1,33 @@
+
diff --git a/src/components/Application/src/search/AppSearchFooter.vue b/src/components/Application/src/search/AppSearchFooter.vue
new file mode 100644
index 0000000..7c92c7b
--- /dev/null
+++ b/src/components/Application/src/search/AppSearchFooter.vue
@@ -0,0 +1,59 @@
+
+
+
+
{{ t('component.app.toSearch') }}
+
+
+
{{ t('component.app.toNavigate') }}
+
+
{{ t('common.closeText') }}
+
+
+
+
+
diff --git a/src/components/Application/src/search/AppSearchKeyItem.vue b/src/components/Application/src/search/AppSearchKeyItem.vue
new file mode 100644
index 0000000..08e3dbd
--- /dev/null
+++ b/src/components/Application/src/search/AppSearchKeyItem.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/src/components/Application/src/search/AppSearchModal.vue b/src/components/Application/src/search/AppSearchModal.vue
new file mode 100644
index 0000000..76cc0c3
--- /dev/null
+++ b/src/components/Application/src/search/AppSearchModal.vue
@@ -0,0 +1,266 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('common.cancelText') }}
+
+
+
+
+ {{ t('component.app.searchNotData') }}
+
+
+
+ -
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Application/src/search/useMenuSearch.ts b/src/components/Application/src/search/useMenuSearch.ts
new file mode 100644
index 0000000..5f29c2b
--- /dev/null
+++ b/src/components/Application/src/search/useMenuSearch.ts
@@ -0,0 +1,167 @@
+import { type Menu } from '@/router/types';
+import { type AnyFunction } from '@vben/types';
+import { ref, onBeforeMount, unref, Ref, nextTick } from 'vue';
+import { getMenus } from '@/router/menus';
+import { cloneDeep } from 'lodash-es';
+import { filter, forEach } from '@/utils/helper/treeHelper';
+import { useGo } from '@/hooks/web/usePage';
+import { useScrollTo } from '@vben/hooks';
+import { onKeyStroke, useDebounceFn } from '@vueuse/core';
+import { useI18n } from '@/hooks/web/useI18n';
+
+export interface SearchResult {
+ name: string;
+ path: string;
+ icon?: string;
+}
+
+// Translate special characters
+function transform(c: string) {
+ const code: string[] = ['$', '(', ')', '*', '+', '.', '[', ']', '?', '\\', '^', '{', '}', '|'];
+ return code.includes(c) ? `\\${c}` : c;
+}
+
+function createSearchReg(key: string) {
+ const keys = [...key].map((item) => transform(item));
+ const str = ['', ...keys, ''].join('.*');
+ return new RegExp(str);
+}
+
+export function useMenuSearch(refs: Ref, scrollWrap: Ref, emit: AnyFunction) {
+ const searchResult = ref([]);
+ const keyword = ref('');
+ const activeIndex = ref(-1);
+
+ let menuList: Menu[] = [];
+
+ const { t } = useI18n();
+ const go = useGo();
+ const handleSearch = useDebounceFn(search, 200);
+
+ onBeforeMount(async () => {
+ const list = await getMenus();
+ menuList = cloneDeep(list);
+ forEach(menuList, (item) => {
+ item.name = t(item.name);
+ });
+ });
+
+ function search(e: ChangeEvent) {
+ e?.stopPropagation();
+ const key = e.target.value;
+ keyword.value = key.trim();
+ if (!key) {
+ searchResult.value = [];
+ return;
+ }
+ const reg = createSearchReg(unref(keyword));
+ const filterMenu = filter(menuList, (item) => {
+ return reg.test(item.name) && !item.hideMenu;
+ });
+ searchResult.value = handlerSearchResult(filterMenu, reg);
+ activeIndex.value = 0;
+ }
+
+ function handlerSearchResult(filterMenu: Menu[], reg: RegExp, parent?: Menu) {
+ const ret: SearchResult[] = [];
+ filterMenu.forEach((item) => {
+ const { name, path, icon, children, hideMenu, meta } = item;
+ if (!hideMenu && reg.test(name) && (!children?.length || meta?.hideChildrenInMenu)) {
+ ret.push({
+ name: parent?.name ? `${parent.name} > ${name}` : name,
+ path,
+ icon,
+ });
+ }
+ if (!meta?.hideChildrenInMenu && Array.isArray(children) && children.length) {
+ ret.push(...handlerSearchResult(children, reg, item));
+ }
+ });
+ return ret;
+ }
+
+ // Activate when the mouse moves to a certain line
+ function handleMouseenter(e: any) {
+ const index = e.target.dataset.index;
+ activeIndex.value = Number(index);
+ }
+
+ // Arrow key up
+ function handleUp() {
+ if (!searchResult.value.length) return;
+ activeIndex.value--;
+ if (activeIndex.value < 0) {
+ activeIndex.value = searchResult.value.length - 1;
+ }
+ handleScroll();
+ }
+
+ // Arrow key down
+ function handleDown() {
+ if (!searchResult.value.length) return;
+ activeIndex.value++;
+ if (activeIndex.value > searchResult.value.length - 1) {
+ activeIndex.value = 0;
+ }
+ handleScroll();
+ }
+
+ // When the keyboard up and down keys move to an invisible place
+ // the scroll bar needs to scroll automatically
+ function handleScroll() {
+ const refList = unref(refs);
+ if (!refList || !Array.isArray(refList) || refList.length === 0 || !unref(scrollWrap)) {
+ return;
+ }
+
+ const index = unref(activeIndex);
+ const currentRef = refList[index];
+ if (!currentRef) {
+ return;
+ }
+ const wrapEl = unref(scrollWrap);
+ if (!wrapEl) {
+ return;
+ }
+ const scrollHeight = currentRef.offsetTop + currentRef.offsetHeight;
+ const wrapHeight = wrapEl.offsetHeight;
+ const { start } = useScrollTo({
+ el: wrapEl,
+ duration: 100,
+ to: scrollHeight - wrapHeight,
+ });
+ start();
+ }
+
+ // enter keyboard event
+ async function handleEnter() {
+ if (!searchResult.value.length) {
+ return;
+ }
+ const result = unref(searchResult);
+ const index = unref(activeIndex);
+ if (result.length === 0 || index < 0) {
+ return;
+ }
+ const to = result[index];
+ handleClose();
+ await nextTick();
+ go(to.path);
+ }
+
+ // close search modal
+ function handleClose() {
+ searchResult.value = [];
+ emit('close');
+ }
+
+ // enter search
+ onKeyStroke('Enter', handleEnter);
+ // Monitor keyboard arrow keys
+ onKeyStroke('ArrowUp', handleUp);
+ onKeyStroke('ArrowDown', handleDown);
+ // esc close
+ onKeyStroke('Escape', handleClose);
+
+ return { handleSearch, searchResult, keyword, activeIndex, handleMouseenter, handleEnter };
+}
diff --git a/src/components/Application/src/useAppContext.ts b/src/components/Application/src/useAppContext.ts
new file mode 100644
index 0000000..5356cdb
--- /dev/null
+++ b/src/components/Application/src/useAppContext.ts
@@ -0,0 +1,17 @@
+import { InjectionKey, Ref } from 'vue';
+import { createContext, useContext } from '@/hooks/core/useContext';
+
+export interface AppProviderContextProps {
+ prefixCls: Ref;
+ isMobile: Ref;
+}
+
+const key: InjectionKey = Symbol();
+
+export function createAppProviderContext(context: AppProviderContextProps) {
+ return createContext(context, key);
+}
+
+export function useAppProviderContext() {
+ return useContext(key);
+}
diff --git a/src/components/Authority/index.ts b/src/components/Authority/index.ts
new file mode 100644
index 0000000..7a86235
--- /dev/null
+++ b/src/components/Authority/index.ts
@@ -0,0 +1,4 @@
+import { withInstall } from '@/utils';
+import authority from './src/Authority.vue';
+
+export const Authority = withInstall(authority);
diff --git a/src/components/Authority/src/Authority.vue b/src/components/Authority/src/Authority.vue
new file mode 100644
index 0000000..2cfe657
--- /dev/null
+++ b/src/components/Authority/src/Authority.vue
@@ -0,0 +1,45 @@
+
+
diff --git a/src/components/Basic/index.ts b/src/components/Basic/index.ts
new file mode 100644
index 0000000..3fad74c
--- /dev/null
+++ b/src/components/Basic/index.ts
@@ -0,0 +1,8 @@
+import { withInstall } from '@/utils';
+import basicArrow from './src/BasicArrow.vue';
+import basicTitle from './src/BasicTitle.vue';
+import basicHelp from './src/BasicHelp.vue';
+
+export const BasicArrow = withInstall(basicArrow);
+export const BasicTitle = withInstall(basicTitle);
+export const BasicHelp = withInstall(basicHelp);
diff --git a/src/components/Basic/src/BasicArrow.vue b/src/components/Basic/src/BasicArrow.vue
new file mode 100644
index 0000000..400e3da
--- /dev/null
+++ b/src/components/Basic/src/BasicArrow.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
diff --git a/src/components/Basic/src/BasicHelp.vue b/src/components/Basic/src/BasicHelp.vue
new file mode 100644
index 0000000..67c5c05
--- /dev/null
+++ b/src/components/Basic/src/BasicHelp.vue
@@ -0,0 +1,116 @@
+
+
diff --git a/src/components/Basic/src/BasicTitle.vue b/src/components/Basic/src/BasicTitle.vue
new file mode 100644
index 0000000..06ca7ef
--- /dev/null
+++ b/src/components/Basic/src/BasicTitle.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
diff --git a/src/components/Button/index.ts b/src/components/Button/index.ts
new file mode 100644
index 0000000..49ad67a
--- /dev/null
+++ b/src/components/Button/index.ts
@@ -0,0 +1,9 @@
+import { withInstall } from '@/utils';
+import type { ExtractPropTypes } from 'vue';
+import button from './src/BasicButton.vue';
+import popConfirmButton from './src/PopConfirmButton.vue';
+import { buttonProps } from './src/props';
+
+export const Button = withInstall(button);
+export const PopConfirmButton = withInstall(popConfirmButton);
+export declare type ButtonProps = Partial>;
diff --git a/src/components/Button/src/BasicButton.vue b/src/components/Button/src/BasicButton.vue
new file mode 100644
index 0000000..b8cfcaa
--- /dev/null
+++ b/src/components/Button/src/BasicButton.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
diff --git a/src/components/Button/src/PopConfirmButton.vue b/src/components/Button/src/PopConfirmButton.vue
new file mode 100644
index 0000000..1e33889
--- /dev/null
+++ b/src/components/Button/src/PopConfirmButton.vue
@@ -0,0 +1,54 @@
+
diff --git a/src/components/Button/src/props.ts b/src/components/Button/src/props.ts
new file mode 100644
index 0000000..a61c6c3
--- /dev/null
+++ b/src/components/Button/src/props.ts
@@ -0,0 +1,27 @@
+const validColors = ['primary', 'error', 'warning', 'success', ''] as const;
+type ButtonColorType = (typeof validColors)[number];
+
+export const buttonProps = {
+ color: {
+ type: String as PropType,
+ validator: (v) => validColors.includes(v),
+ default: '',
+ },
+ loading: { type: Boolean },
+ disabled: { type: Boolean },
+ /**
+ * Text before icon.
+ */
+ preIcon: { type: String },
+ /**
+ * Text after icon.
+ */
+ postIcon: { type: String },
+ /**
+ * preIcon and postIcon icon size.
+ * @default: 14
+ */
+ iconSize: { type: Number, default: 14 },
+ onClick: { type: [Function, Array] as PropType<(() => any) | (() => any)[]>, default: null },
+ text: { type: String },
+};
diff --git a/src/components/CardList/index.ts b/src/components/CardList/index.ts
new file mode 100644
index 0000000..e4c6a21
--- /dev/null
+++ b/src/components/CardList/index.ts
@@ -0,0 +1,4 @@
+import { withInstall } from '@/utils';
+import cardList from './src/CardList.vue';
+
+export const CardList = withInstall(cardList);
diff --git a/src/components/CardList/src/CardList.vue b/src/components/CardList/src/CardList.vue
new file mode 100644
index 0000000..f2cbe4e
--- /dev/null
+++ b/src/components/CardList/src/CardList.vue
@@ -0,0 +1,177 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 每行显示数量
+
+
+
+
+
+ 刷新
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.time }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/CardList/src/data.ts b/src/components/CardList/src/data.ts
new file mode 100644
index 0000000..b7c4be7
--- /dev/null
+++ b/src/components/CardList/src/data.ts
@@ -0,0 +1,25 @@
+import { ref } from 'vue';
+// 每行个数
+export const grid = ref(12);
+// slider属性
+export const useSlider = (min = 6, max = 12) => {
+ // 每行显示个数滑动条
+ const getMarks = () => {
+ const l = {};
+ for (let i = min; i < max + 1; i++) {
+ l[i] = {
+ style: {
+ color: '#fff',
+ },
+ label: i,
+ };
+ }
+ return l;
+ };
+ return {
+ min,
+ max,
+ marks: getMarks(),
+ step: 1,
+ };
+};
diff --git a/src/components/ClickOutSide/index.ts b/src/components/ClickOutSide/index.ts
new file mode 100644
index 0000000..19c9033
--- /dev/null
+++ b/src/components/ClickOutSide/index.ts
@@ -0,0 +1,4 @@
+import { withInstall } from '@/utils';
+import clickOutSide from './src/ClickOutSide.vue';
+
+export const ClickOutSide = withInstall(clickOutSide);
diff --git a/src/components/ClickOutSide/src/ClickOutSide.vue b/src/components/ClickOutSide/src/ClickOutSide.vue
new file mode 100644
index 0000000..901a508
--- /dev/null
+++ b/src/components/ClickOutSide/src/ClickOutSide.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/src/components/CodeEditor/index.ts b/src/components/CodeEditor/index.ts
new file mode 100644
index 0000000..996eae1
--- /dev/null
+++ b/src/components/CodeEditor/index.ts
@@ -0,0 +1,8 @@
+import { withInstall } from '@/utils';
+import codeEditor from './src/CodeEditor.vue';
+import jsonPreview from './src/json-preview/JsonPreview.vue';
+
+export const CodeEditor = withInstall(codeEditor);
+export const JsonPreview = withInstall(jsonPreview);
+
+export * from './src/typing';
diff --git a/src/components/CodeEditor/src/CodeEditor.vue b/src/components/CodeEditor/src/CodeEditor.vue
new file mode 100644
index 0000000..6416663
--- /dev/null
+++ b/src/components/CodeEditor/src/CodeEditor.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
diff --git a/src/components/CodeEditor/src/codemirror/CodeMirror.vue b/src/components/CodeEditor/src/codemirror/CodeMirror.vue
new file mode 100644
index 0000000..796bdc4
--- /dev/null
+++ b/src/components/CodeEditor/src/codemirror/CodeMirror.vue
@@ -0,0 +1,128 @@
+
+
+
+
+
diff --git a/src/components/CodeEditor/src/codemirror/codeMirror.ts b/src/components/CodeEditor/src/codemirror/codeMirror.ts
new file mode 100644
index 0000000..e04f51b
--- /dev/null
+++ b/src/components/CodeEditor/src/codemirror/codeMirror.ts
@@ -0,0 +1,21 @@
+import CodeMirror from 'codemirror';
+import './codemirror.css';
+import 'codemirror/theme/idea.css';
+import 'codemirror/theme/material-palenight.css';
+// import 'codemirror/addon/lint/lint.css';
+
+// modes
+import 'codemirror/mode/javascript/javascript';
+import 'codemirror/mode/css/css';
+import 'codemirror/mode/htmlmixed/htmlmixed';
+// addons
+// import 'codemirror/addon/edit/closebrackets';
+// import 'codemirror/addon/edit/closetag';
+// import 'codemirror/addon/comment/comment';
+// import 'codemirror/addon/fold/foldcode';
+// import 'codemirror/addon/fold/foldgutter';
+// import 'codemirror/addon/fold/brace-fold';
+// import 'codemirror/addon/fold/indent-fold';
+// import 'codemirror/addon/lint/json-lint';
+// import 'codemirror/addon/fold/comment-fold';
+export { CodeMirror };
diff --git a/src/components/CodeEditor/src/codemirror/codemirror.css b/src/components/CodeEditor/src/codemirror/codemirror.css
new file mode 100644
index 0000000..d0f703e
--- /dev/null
+++ b/src/components/CodeEditor/src/codemirror/codemirror.css
@@ -0,0 +1,529 @@
+/* BASICS */
+
+.CodeMirror {
+ --base: #545281;
+ --comment: hsl(210deg 25% 60%);
+ --keyword: #af4ab1;
+ --variable: #0055d1;
+ --function: #c25205;
+ --string: #2ba46d;
+ --number: #c25205;
+ --tags: #d00;
+ --qualifier: #ff6032;
+ --important: var(--string);
+
+ position: relative;
+ height: auto;
+ height: 100%;
+ overflow: hidden;
+ font-family: var(--font-code);
+ background: white;
+ direction: ltr;
+}
+
+/* PADDING */
+
+.CodeMirror-lines {
+ min-height: 1px; /* prevents collapsing before first draw */
+ padding: 4px 0; /* Vertical padding around content */
+ cursor: text;
+}
+
+.CodeMirror-scrollbar-filler,
+.CodeMirror-gutter-filler {
+ background-color: white; /* The little square between H and V scrollbars */
+}
+
+/* GUTTER */
+
+.CodeMirror-gutters {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 3;
+ min-height: 100%;
+ white-space: nowrap;
+ background-color: transparent;
+ border-right: 1px solid #ddd;
+}
+
+.CodeMirror-linenumber {
+ min-width: 20px;
+ padding: 0 3px 0 5px;
+ color: var(--comment);
+ text-align: right;
+ white-space: nowrap;
+ opacity: 0.6;
+}
+
+.CodeMirror-guttermarker {
+ color: black;
+}
+
+.CodeMirror-guttermarker-subtle {
+ color: #999;
+}
+
+/* FOLD GUTTER */
+
+.CodeMirror-foldmarker {
+ font-family: arial;
+ line-height: 0.3;
+ color: #414141;
+ text-shadow:
+ #f96 1px 1px 2px,
+ #f96 -1px -1px 2px,
+ #f96 1px -1px 2px,
+ #f96 -1px 1px 2px;
+ cursor: pointer;
+}
+
+.CodeMirror-foldgutter {
+ width: 0.7em;
+}
+
+.CodeMirror-foldgutter-open,
+.CodeMirror-foldgutter-folded {
+ cursor: pointer;
+}
+
+.CodeMirror-foldgutter-open::after,
+.CodeMirror-foldgutter-folded::after {
+ position: relative;
+ top: -0.1em;
+ display: inline-block;
+ font-size: 0.8em;
+ content: '>';
+ opacity: 0.8;
+ transform: rotate(90deg);
+ transition: transform 0.2s;
+}
+
+.CodeMirror-foldgutter-folded::after {
+ transform: none;
+}
+
+/* CURSOR */
+
+.CodeMirror-cursor {
+ position: absolute;
+ width: 0;
+ pointer-events: none;
+ border-right: none;
+ border-left: 1px solid black;
+}
+
+/* Shown when moving in bi-directional text */
+.CodeMirror div.CodeMirror-secondarycursor {
+ border-left: 1px solid silver;
+}
+
+.cm-fat-cursor .CodeMirror-cursor {
+ width: auto;
+ background: #7e7;
+ border: 0 !important;
+}
+
+.cm-fat-cursor div.CodeMirror-cursors {
+ z-index: 1;
+}
+
+.cm-fat-cursor-mark {
+ background-color: rgb(20 255 20 / 50%);
+ animation: blink 1.06s steps(1) infinite;
+}
+
+.cm-animate-fat-cursor {
+ width: auto;
+ background-color: #7e7;
+ border: 0;
+ animation: blink 1.06s steps(1) infinite;
+}
+@keyframes blink {
+ 50% {
+ background-color: transparent;
+ }
+}
+@keyframes blink {
+ 50% {
+ background-color: transparent;
+ }
+}
+@keyframes blink {
+ 50% {
+ background-color: transparent;
+ }
+}
+
+.cm-tab {
+ display: inline-block;
+ text-decoration: inherit;
+}
+
+.CodeMirror-rulers {
+ position: absolute;
+ top: -50px;
+ right: 0;
+ bottom: -20px;
+ left: 0;
+ overflow: hidden;
+}
+
+.CodeMirror-ruler {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ border-left: 1px solid #ccc;
+}
+
+/* DEFAULT THEME */
+.cm-s-default.CodeMirror {
+ background-color: transparent;
+}
+
+.cm-s-default .cm-header {
+ color: blue;
+}
+
+.cm-s-default .cm-quote {
+ color: #090;
+}
+
+.cm-negative {
+ color: #d44;
+}
+
+.cm-positive {
+ color: #292;
+}
+
+.cm-header,
+.cm-strong {
+ font-weight: bold;
+}
+
+.cm-em {
+ font-style: italic;
+}
+
+.cm-link {
+ text-decoration: underline;
+}
+
+.cm-strikethrough {
+ text-decoration: line-through;
+}
+
+.cm-s-default .cm-atom,
+.cm-s-default .cm-def,
+.cm-s-default .cm-property,
+.cm-s-default .cm-variable-2,
+.cm-s-default .cm-variable-3,
+.cm-s-default .cm-punctuation {
+ color: var(--base);
+}
+
+.cm-s-default .cm-hr,
+.cm-s-default .cm-comment {
+ color: var(--comment);
+}
+
+.cm-s-default .cm-attribute,
+.cm-s-default .cm-keyword {
+ color: var(--keyword);
+}
+
+.cm-s-default .cm-variable {
+ color: var(--variable);
+}
+
+.cm-s-default .cm-bracket,
+.cm-s-default .cm-tag {
+ color: var(--tags);
+}
+
+.cm-s-default .cm-number {
+ color: var(--number);
+}
+
+.cm-s-default .cm-string,
+.cm-s-default .cm-string-2 {
+ color: var(--string);
+}
+
+.cm-s-default .cm-type {
+ color: #085;
+}
+
+.cm-s-default .cm-meta {
+ color: #555;
+}
+
+.cm-s-default .cm-qualifier {
+ color: var(--qualifier);
+}
+
+.cm-s-default .cm-builtin {
+ color: #7539ff;
+}
+
+.cm-s-default .cm-link {
+ color: var(--flash);
+}
+
+.cm-s-default .cm-error {
+ color: #ff008c;
+}
+
+.cm-invalidchar {
+ color: #ff008c;
+}
+
+.CodeMirror-composing {
+ border-bottom: 2px solid;
+}
+
+/* Default styles for common addons */
+
+div.CodeMirror span.CodeMirror-matchingbracket {
+ color: #0b0;
+}
+
+div.CodeMirror span.CodeMirror-nonmatchingbracket {
+ color: #a22;
+}
+
+.CodeMirror-matchingtag {
+ background: rgb(255 150 0 / 30%);
+}
+
+.CodeMirror-activeline-background {
+ background: #e8f2ff;
+}
+
+/* STOP */
+
+/* The rest of this file contains styles related to the mechanics of
+ the editor. You probably shouldn't touch them. */
+
+.CodeMirror-scroll {
+ position: relative;
+ height: 100%;
+ padding-bottom: 30px;
+ margin-right: -30px;
+
+ /* 30px is the magic margin used to hide the element's real scrollbars */
+
+ /* See overflow: hidden in .CodeMirror */
+ margin-bottom: -30px;
+ overflow: scroll !important; /* Things will break if this is overridden */
+ outline: none; /* Prevent dragging from highlighting the element */
+}
+
+.CodeMirror-sizer {
+ position: relative;
+ margin-bottom: 20px !important;
+ border-right: 30px solid transparent;
+}
+
+/* The fake, visible scrollbars. Used to force redraw during scrolling
+ before actual scrolling happens, thus preventing shaking and
+ flickering artifacts. */
+.CodeMirror-vscrollbar,
+.CodeMirror-hscrollbar,
+.CodeMirror-scrollbar-filler,
+.CodeMirror-gutter-filler {
+ position: absolute;
+ z-index: 6;
+ display: none;
+}
+
+.CodeMirror-vscrollbar {
+ top: 0;
+ right: 0;
+ overflow-x: hidden;
+ overflow-y: scroll;
+}
+
+.CodeMirror-hscrollbar {
+ bottom: 0;
+ left: 0;
+ overflow-x: scroll;
+ overflow-y: hidden;
+}
+
+.CodeMirror-scrollbar-filler {
+ right: 0;
+ bottom: 0;
+}
+
+.CodeMirror-gutter-filler {
+ bottom: 0;
+ left: 0;
+}
+
+.CodeMirror-gutter {
+ display: inline-block;
+ height: 100%;
+ margin-bottom: -30px;
+ white-space: normal;
+ vertical-align: top;
+}
+
+.CodeMirror-gutter-wrapper {
+ position: absolute;
+ z-index: 4;
+ background: none !important;
+ border: none !important;
+}
+
+.CodeMirror-gutter-background {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ z-index: 4;
+}
+
+.CodeMirror-gutter-elt {
+ position: absolute;
+ z-index: 4;
+ cursor: default;
+}
+
+.CodeMirror-gutter-wrapper ::selection {
+ background-color: transparent;
+}
+
+.CodeMirrorwrapper ::selection {
+ background-color: transparent;
+}
+
+.CodeMirror pre {
+ position: relative;
+ z-index: 2;
+ padding: 0 4px; /* Horizontal padding of content */
+ margin: 0;
+ overflow: visible;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+ color: inherit;
+ word-wrap: normal;
+ white-space: pre;
+ background: transparent;
+ border-width: 0;
+
+ /* Reset some styles that the rest of the page might have set */
+ border-radius: 0;
+ -webkit-tap-highlight-color: transparent;
+ font-variant-ligatures: contextual;
+}
+
+.CodeMirror-wrap pre {
+ word-break: normal;
+ word-wrap: break-word;
+ white-space: pre-wrap;
+}
+
+.CodeMirror-linebackground {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 0;
+}
+
+.CodeMirror-linewidget {
+ position: relative;
+ z-index: 2;
+ padding: 0.1px; /* Force widget margins to stay inside of the container */
+}
+
+.CodeMirror-rtl pre {
+ direction: rtl;
+}
+
+.CodeMirror-code {
+ outline: none;
+}
+
+/* Force content-box sizing for the elements where we expect it */
+.CodeMirror-scroll,
+.CodeMirror-sizer,
+.CodeMirror-gutter,
+.CodeMirror-gutters,
+.CodeMirror-linenumber {
+ box-sizing: content-box;
+}
+
+.CodeMirror-measure {
+ position: absolute;
+ width: 100%;
+ height: 0;
+ overflow: hidden;
+ visibility: hidden;
+}
+
+.CodeMirror-measure pre {
+ position: static;
+}
+
+div.CodeMirror-cursors {
+ position: relative;
+ z-index: 3;
+ visibility: hidden;
+}
+
+div.CodeMirror-dragcursors {
+ visibility: visible;
+}
+
+.CodeMirror-focused div.CodeMirror-cursors {
+ visibility: visible;
+}
+
+.CodeMirror-selected {
+ background: #d9d9d9;
+}
+
+.CodeMirror-focused .CodeMirror-selected {
+ background: #d7d4f0;
+}
+
+.CodeMirror-crosshair {
+ cursor: crosshair;
+}
+
+.CodeMirror-line::selection,
+.CodeMirror-line > span::selection,
+.CodeMirror-line > span > span::selection {
+ background: #d7d4f0;
+}
+
+.cm-searching {
+ background-color: #ffa;
+ background-color: rgb(255 255 0 / 40%);
+}
+
+/* Used to force a border model for a node */
+.cm-force-border {
+ padding-right: 0.1px;
+}
+
+@media print {
+ /* Hide the cursor when printing */
+ .CodeMirror div.CodeMirror-cursors {
+ visibility: hidden;
+ }
+}
+
+/* See issue #2901 */
+.cm-tab-wrap-hack::after {
+ content: '';
+}
+
+/* Help users use markselection to safely style text background */
+span.CodeMirror-selectedtext {
+ background: none;
+}
diff --git a/src/components/CodeEditor/src/json-preview/JsonPreview.vue b/src/components/CodeEditor/src/json-preview/JsonPreview.vue
new file mode 100644
index 0000000..75890c3
--- /dev/null
+++ b/src/components/CodeEditor/src/json-preview/JsonPreview.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/src/components/CodeEditor/src/typing.ts b/src/components/CodeEditor/src/typing.ts
new file mode 100644
index 0000000..34b5ed1
--- /dev/null
+++ b/src/components/CodeEditor/src/typing.ts
@@ -0,0 +1,5 @@
+export enum MODE {
+ JSON = 'application/json',
+ HTML = 'htmlmixed',
+ JS = 'javascript',
+}
diff --git a/src/components/Container/index.ts b/src/components/Container/index.ts
new file mode 100644
index 0000000..4a90c3d
--- /dev/null
+++ b/src/components/Container/index.ts
@@ -0,0 +1,8 @@
+import { withInstall } from '@/utils';
+import collapseContainer from './src/collapse/CollapseContainer.vue';
+import scrollContainer from './src/ScrollContainer.vue';
+
+export const CollapseContainer = withInstall(collapseContainer);
+export const ScrollContainer = withInstall(scrollContainer);
+
+export * from './src/typing';
diff --git a/src/components/Container/src/ScrollContainer.vue b/src/components/Container/src/ScrollContainer.vue
new file mode 100644
index 0000000..489b673
--- /dev/null
+++ b/src/components/Container/src/ScrollContainer.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
diff --git a/src/components/Container/src/collapse/CollapseContainer.vue b/src/components/Container/src/collapse/CollapseContainer.vue
new file mode 100644
index 0000000..c3a43c8
--- /dev/null
+++ b/src/components/Container/src/collapse/CollapseContainer.vue
@@ -0,0 +1,118 @@
+
+
+
diff --git a/src/components/Container/src/collapse/CollapseHeader.vue b/src/components/Container/src/collapse/CollapseHeader.vue
new file mode 100644
index 0000000..b49bb2a
--- /dev/null
+++ b/src/components/Container/src/collapse/CollapseHeader.vue
@@ -0,0 +1,44 @@
+
diff --git a/src/components/Container/src/typing.ts b/src/components/Container/src/typing.ts
new file mode 100644
index 0000000..86c03be
--- /dev/null
+++ b/src/components/Container/src/typing.ts
@@ -0,0 +1,17 @@
+export type ScrollType = 'default' | 'main';
+
+export interface CollapseContainerOptions {
+ canExpand?: boolean;
+ title?: string;
+ helpMessage?: Array | string;
+}
+export interface ScrollContainerOptions {
+ enableScroll?: boolean;
+ type?: ScrollType;
+}
+
+export type ScrollActionType = RefType<{
+ scrollBottom: () => void;
+ getScrollWrap: () => Nullable;
+ scrollTo: (top: number) => void;
+}>;
diff --git a/src/components/ContextMenu/index.ts b/src/components/ContextMenu/index.ts
new file mode 100644
index 0000000..ed294d7
--- /dev/null
+++ b/src/components/ContextMenu/index.ts
@@ -0,0 +1,3 @@
+export { createContextMenu, destroyContextMenu } from './src/createContextMenu';
+
+export * from './src/typing';
diff --git a/src/components/ContextMenu/src/ContextMenu.vue b/src/components/ContextMenu/src/ContextMenu.vue
new file mode 100644
index 0000000..fcf6fcb
--- /dev/null
+++ b/src/components/ContextMenu/src/ContextMenu.vue
@@ -0,0 +1,217 @@
+
+
diff --git a/src/components/ContextMenu/src/createContextMenu.ts b/src/components/ContextMenu/src/createContextMenu.ts
new file mode 100644
index 0000000..a014903
--- /dev/null
+++ b/src/components/ContextMenu/src/createContextMenu.ts
@@ -0,0 +1,77 @@
+import contextMenuVue from './ContextMenu.vue';
+import { isClient } from '@/utils/is';
+import { CreateContextOptions, ContextMenuProps } from './typing';
+import { createVNode, render } from 'vue';
+
+const menuManager: {
+ domList: Element[];
+ resolve: Fn;
+} = {
+ domList: [],
+ resolve: () => {},
+};
+
+export const createContextMenu = function (options: CreateContextOptions) {
+ const { event } = options || {};
+
+ event && event?.preventDefault();
+
+ if (!isClient) {
+ return;
+ }
+ return new Promise((resolve) => {
+ const body = document.body;
+
+ const container = document.createElement('div');
+ const propsData: Partial = {};
+ if (options.styles) {
+ propsData.styles = options.styles;
+ }
+
+ if (options.items) {
+ propsData.items = options.items;
+ }
+
+ if (options.event) {
+ propsData.customEvent = event;
+ propsData.axis = { x: event.clientX, y: event.clientY + body.scrollTop }; // y坐标需加上body往上滚动的Y
+ }
+
+ const vm = createVNode(contextMenuVue, propsData);
+ render(vm, container);
+
+ const handleClick = function () {
+ menuManager.resolve('');
+ };
+
+ menuManager.domList.push(container);
+
+ const remove = function () {
+ menuManager.domList.forEach((dom: Element) => {
+ try {
+ dom && body.removeChild(dom);
+ } catch (error) {
+ //
+ }
+ });
+ body.removeEventListener('click', handleClick);
+ body.removeEventListener('scroll', handleClick);
+ };
+
+ menuManager.resolve = function (arg) {
+ remove();
+ resolve(arg);
+ };
+ remove();
+ body.appendChild(container);
+ body.addEventListener('click', handleClick);
+ body.addEventListener('scroll', handleClick);
+ });
+};
+
+export const destroyContextMenu = function () {
+ if (menuManager) {
+ menuManager.resolve('');
+ menuManager.domList = [];
+ }
+};
diff --git a/src/components/ContextMenu/src/typing.ts b/src/components/ContextMenu/src/typing.ts
new file mode 100644
index 0000000..63d3d37
--- /dev/null
+++ b/src/components/ContextMenu/src/typing.ts
@@ -0,0 +1,36 @@
+export interface Axis {
+ x: number;
+ y: number;
+}
+
+export interface ContextMenuItem {
+ label: string;
+ icon?: string;
+ hidden?: boolean;
+ disabled?: boolean;
+ handler?: Fn;
+ divider?: boolean;
+ children?: ContextMenuItem[];
+}
+export interface CreateContextOptions {
+ event: MouseEvent;
+ icon?: string;
+ styles?: any;
+ items?: ContextMenuItem[];
+}
+
+export interface ContextMenuProps {
+ event?: MouseEvent;
+ styles?: any;
+ items: ContextMenuItem[];
+ customEvent?: MouseEvent;
+ axis?: Axis;
+ width?: number;
+ showIcon?: boolean;
+}
+
+export interface ItemContentProps {
+ showIcon: boolean | undefined;
+ item: ContextMenuItem;
+ handler: Fn;
+}
diff --git a/src/components/CountDown/index.ts b/src/components/CountDown/index.ts
new file mode 100644
index 0000000..0da9d31
--- /dev/null
+++ b/src/components/CountDown/index.ts
@@ -0,0 +1,6 @@
+import { withInstall } from '@/utils';
+import countButton from './src/CountButton.vue';
+import countdownInput from './src/CountdownInput.vue';
+
+export const CountdownInput = withInstall(countdownInput);
+export const CountButton = withInstall(countButton);
diff --git a/src/components/CountDown/src/CountButton.vue b/src/components/CountDown/src/CountButton.vue
new file mode 100644
index 0000000..74e45cc
--- /dev/null
+++ b/src/components/CountDown/src/CountButton.vue
@@ -0,0 +1,55 @@
+
+
+
+
diff --git a/src/components/CountDown/src/CountdownInput.vue b/src/components/CountDown/src/CountdownInput.vue
new file mode 100644
index 0000000..8c3821f
--- /dev/null
+++ b/src/components/CountDown/src/CountdownInput.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/CountDown/src/useCountdown.ts b/src/components/CountDown/src/useCountdown.ts
new file mode 100644
index 0000000..316d69a
--- /dev/null
+++ b/src/components/CountDown/src/useCountdown.ts
@@ -0,0 +1,51 @@
+import { ref, unref } from 'vue';
+import { tryOnUnmounted } from '@vueuse/core';
+
+export function useCountdown(count: number) {
+ const currentCount = ref(count);
+
+ const isStart = ref(false);
+
+ let timerId: ReturnType | null;
+
+ function clear() {
+ timerId && window.clearInterval(timerId);
+ }
+
+ function stop() {
+ isStart.value = false;
+ clear();
+ timerId = null;
+ }
+
+ function start() {
+ if (unref(isStart) || !!timerId) {
+ return;
+ }
+ isStart.value = true;
+ timerId = setInterval(() => {
+ if (unref(currentCount) === 1) {
+ stop();
+ currentCount.value = count;
+ } else {
+ currentCount.value -= 1;
+ }
+ }, 1000);
+ }
+
+ function reset() {
+ currentCount.value = count;
+ stop();
+ }
+
+ function restart() {
+ reset();
+ start();
+ }
+
+ tryOnUnmounted(() => {
+ reset();
+ });
+
+ return { start, reset, restart, clear, stop, currentCount, isStart };
+}
diff --git a/src/components/CountTo/index.ts b/src/components/CountTo/index.ts
new file mode 100644
index 0000000..0c41941
--- /dev/null
+++ b/src/components/CountTo/index.ts
@@ -0,0 +1,4 @@
+import { withInstall } from '@/utils';
+import countTo from './src/CountTo.vue';
+
+export const CountTo = withInstall(countTo);
diff --git a/src/components/CountTo/src/CountTo.vue b/src/components/CountTo/src/CountTo.vue
new file mode 100644
index 0000000..98684a4
--- /dev/null
+++ b/src/components/CountTo/src/CountTo.vue
@@ -0,0 +1,107 @@
+
+
+ {{ value }}
+
+
+
diff --git a/src/components/Cropper/index.ts b/src/components/Cropper/index.ts
new file mode 100644
index 0000000..df20be0
--- /dev/null
+++ b/src/components/Cropper/index.ts
@@ -0,0 +1,7 @@
+import { withInstall } from '@/utils';
+import cropperImage from './src/Cropper.vue';
+import avatarCropper from './src/CropperAvatar.vue';
+
+export * from './src/typing';
+export const CropperImage = withInstall(cropperImage);
+export const CropperAvatar = withInstall(avatarCropper);
diff --git a/src/components/Cropper/src/Cropper.vue b/src/components/Cropper/src/Cropper.vue
new file mode 100644
index 0000000..0aaade4
--- /dev/null
+++ b/src/components/Cropper/src/Cropper.vue
@@ -0,0 +1,186 @@
+
+
+
![]()
+
+
+
+
diff --git a/src/components/Cropper/src/CropperAvatar.vue b/src/components/Cropper/src/CropperAvatar.vue
new file mode 100644
index 0000000..b5bdb23
--- /dev/null
+++ b/src/components/Cropper/src/CropperAvatar.vue
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
![avatar]()
+
+
+ {{ btnText ? btnText : t('component.cropper.selectImage') }}
+
+
+
+
+
+
+
+
diff --git a/src/components/Cropper/src/CropperModal.vue b/src/components/Cropper/src/CropperModal.vue
new file mode 100644
index 0000000..cf0f565
--- /dev/null
+++ b/src/components/Cropper/src/CropperModal.vue
@@ -0,0 +1,274 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Cropper/src/typing.ts b/src/components/Cropper/src/typing.ts
new file mode 100644
index 0000000..e76cc6f
--- /dev/null
+++ b/src/components/Cropper/src/typing.ts
@@ -0,0 +1,8 @@
+import type Cropper from 'cropperjs';
+
+export interface CropendResult {
+ imgBase64: string;
+ imgInfo: Cropper.Data;
+}
+
+export type { Cropper };
diff --git a/src/components/Description/index.ts b/src/components/Description/index.ts
new file mode 100644
index 0000000..c75c4db
--- /dev/null
+++ b/src/components/Description/index.ts
@@ -0,0 +1,6 @@
+import { withInstall } from '@/utils';
+import description from './src/Description.vue';
+
+export * from './src/typing';
+export { useDescription } from './src/useDescription';
+export const Description = withInstall(description);
diff --git a/src/components/Description/src/Description.vue b/src/components/Description/src/Description.vue
new file mode 100644
index 0000000..bcca799
--- /dev/null
+++ b/src/components/Description/src/Description.vue
@@ -0,0 +1,195 @@
+
diff --git a/src/components/Description/src/typing.ts b/src/components/Description/src/typing.ts
new file mode 100644
index 0000000..32278cd
--- /dev/null
+++ b/src/components/Description/src/typing.ts
@@ -0,0 +1,50 @@
+import type { VNode, CSSProperties } from 'vue';
+import type { CollapseContainerOptions } from '@/components/Container';
+import type { DescriptionsProps } from 'ant-design-vue/es/descriptions';
+
+export interface DescItem {
+ labelMinWidth?: number;
+ contentMinWidth?: number;
+ labelStyle?: CSSProperties;
+ field: string;
+ label: string | VNode | JSX.Element;
+ // Merge column
+ span?: number;
+ show?: (...arg: any) => boolean;
+ // render
+ render?: (
+ val: any,
+ data: Recordable,
+ ) => VNode | undefined | JSX.Element | Element | string | number;
+}
+
+export interface DescriptionProps extends DescriptionsProps {
+ // Whether to include the collapse component
+ useCollapse?: boolean;
+ /**
+ * item configuration
+ * @type DescItem
+ */
+ schema: DescItem[];
+ /**
+ * 数据
+ * @type object
+ */
+ data: Recordable;
+ /**
+ * Built-in CollapseContainer component configuration
+ * @type CollapseContainerOptions
+ */
+ collapseOptions?: CollapseContainerOptions;
+}
+
+export interface DescInstance {
+ setDescProps(descProps: Partial): void;
+}
+
+export type Register = (descInstance: DescInstance) => void;
+
+/**
+ * @description:
+ */
+export type UseDescReturnType = [Register, DescInstance];
diff --git a/src/components/Description/src/useDescription.ts b/src/components/Description/src/useDescription.ts
new file mode 100644
index 0000000..062f53c
--- /dev/null
+++ b/src/components/Description/src/useDescription.ts
@@ -0,0 +1,28 @@
+import type { DescriptionProps, DescInstance, UseDescReturnType } from './typing';
+import { ref, getCurrentInstance, unref } from 'vue';
+import { isProdMode } from '@/utils/env';
+
+export function useDescription(props?: Partial): UseDescReturnType {
+ if (!getCurrentInstance()) {
+ throw new Error('useDescription() can only be used inside setup() or functional components!');
+ }
+ const desc = ref>(null);
+ const loaded = ref(false);
+
+ function register(instance: DescInstance) {
+ if (unref(loaded) && isProdMode()) {
+ return;
+ }
+ desc.value = instance;
+ props && instance.setDescProps(props);
+ loaded.value = true;
+ }
+
+ const methods: DescInstance = {
+ setDescProps: (descProps: Partial): void => {
+ unref(desc)?.setDescProps(descProps);
+ },
+ };
+
+ return [register, methods];
+}
diff --git a/src/components/Drawer/index.ts b/src/components/Drawer/index.ts
new file mode 100644
index 0000000..0eb1a16
--- /dev/null
+++ b/src/components/Drawer/index.ts
@@ -0,0 +1,6 @@
+import { withInstall } from '@/utils';
+import basicDrawer from './src/BasicDrawer.vue';
+
+export const BasicDrawer = withInstall(basicDrawer);
+export * from './src/typing';
+export { useDrawer, useDrawerInner } from './src/useDrawer';
diff --git a/src/components/Drawer/src/BasicDrawer.vue b/src/components/Drawer/src/BasicDrawer.vue
new file mode 100644
index 0000000..ba1d5e5
--- /dev/null
+++ b/src/components/Drawer/src/BasicDrawer.vue
@@ -0,0 +1,231 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Drawer/src/components/DrawerFooter.vue b/src/components/Drawer/src/components/DrawerFooter.vue
new file mode 100644
index 0000000..6093fcf
--- /dev/null
+++ b/src/components/Drawer/src/components/DrawerFooter.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+ {{ cancelText }}
+
+
+
+ {{ okText }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Drawer/src/components/DrawerHeader.vue b/src/components/Drawer/src/components/DrawerHeader.vue
new file mode 100644
index 0000000..c7cac4a
--- /dev/null
+++ b/src/components/Drawer/src/components/DrawerHeader.vue
@@ -0,0 +1,68 @@
+
+
+
+ {{ !$slots.title ? title : '' }}
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Drawer/src/props.ts b/src/components/Drawer/src/props.ts
new file mode 100644
index 0000000..30a624d
--- /dev/null
+++ b/src/components/Drawer/src/props.ts
@@ -0,0 +1,45 @@
+import type { PropType } from 'vue';
+
+import { useI18n } from '@/hooks/web/useI18n';
+
+const { t } = useI18n();
+
+export const footerProps = {
+ confirmLoading: { type: Boolean },
+ /**
+ * @description: Show close button
+ */
+ showCancelBtn: { type: Boolean, default: true },
+ cancelButtonProps: Object as PropType,
+ cancelText: { type: String, default: t('common.cancelText') },
+ /**
+ * @description: Show confirmation button
+ */
+ showOkBtn: { type: Boolean, default: true },
+ okButtonProps: Object as PropType,
+ okText: { type: String, default: t('common.okText') },
+ okType: { type: String, default: 'primary' },
+ showFooter: { type: Boolean },
+ footerHeight: {
+ type: [String, Number] as PropType,
+ default: 60,
+ },
+};
+export const basicProps = {
+ isDetail: { type: Boolean },
+ title: { type: String, default: '' },
+ loadingText: { type: String },
+ showDetailBack: { type: Boolean, default: true },
+ open: { type: Boolean },
+ loading: { type: Boolean },
+ maskClosable: { type: Boolean, default: true },
+ getContainer: {
+ type: [Object, String] as PropType,
+ },
+ closeFunc: {
+ type: [Function, Object] as PropType,
+ default: null,
+ },
+ destroyOnClose: { type: Boolean },
+ ...footerProps,
+};
diff --git a/src/components/Drawer/src/typing.ts b/src/components/Drawer/src/typing.ts
new file mode 100644
index 0000000..9789a50
--- /dev/null
+++ b/src/components/Drawer/src/typing.ts
@@ -0,0 +1,194 @@
+import type { ButtonProps } from 'ant-design-vue/lib/button/buttonTypes';
+import type { CSSProperties, VNodeChild, ComputedRef } from 'vue';
+import type { ScrollContainerOptions } from '@/components/Container';
+
+export interface DrawerInstance {
+ setDrawerProps: (props: Partial) => void;
+ emitOpen?: (open: boolean, uid: number) => void;
+}
+
+export interface ReturnMethods extends DrawerInstance {
+ openDrawer: (open?: boolean, data?: T, openOnSet?: boolean) => void;
+ closeDrawer: () => void;
+ getOpen?: ComputedRef;
+}
+
+export type RegisterFn = (drawerInstance: DrawerInstance, uuid: number) => void;
+
+export interface ReturnInnerMethods extends DrawerInstance {
+ closeDrawer: () => void;
+ changeLoading: (loading: boolean) => void;
+ changeOkLoading: (loading: boolean) => void;
+ getOpen?: ComputedRef;
+}
+
+export type UseDrawerReturnType = [RegisterFn, ReturnMethods];
+
+export type UseDrawerInnerReturnType = [RegisterFn, ReturnInnerMethods];
+
+export interface DrawerFooterProps {
+ showOkBtn: boolean;
+ showCancelBtn: boolean;
+ /**
+ * Text of the Cancel button
+ * @default 'cancel'
+ * @type string
+ */
+ cancelText: string;
+ /**
+ * Text of the OK button
+ * @default 'OK'
+ * @type string
+ */
+ okText: string;
+
+ /**
+ * Button type of the OK button
+ * @default 'primary'
+ * @type string
+ */
+ okType: 'primary' | 'danger' | 'dashed' | 'ghost' | 'default';
+ /**
+ * The ok button props, follow jsx rules
+ * @type object
+ */
+ okButtonProps: { props: ButtonProps; on: {} };
+
+ /**
+ * The cancel button props, follow jsx rules
+ * @type object
+ */
+ cancelButtonProps: { props: ButtonProps; on: {} };
+ /**
+ * Whether to apply loading visual effect for OK button or not
+ * @default false
+ * @type boolean
+ */
+ confirmLoading: boolean;
+
+ showFooter: boolean;
+ footerHeight: string | number;
+}
+export interface DrawerProps extends DrawerFooterProps {
+ isDetail?: boolean;
+ loading?: boolean;
+ showDetailBack?: boolean;
+ open?: boolean;
+ /**
+ * Built-in ScrollContainer component configuration
+ * @type ScrollContainerOptions
+ */
+ scrollOptions?: ScrollContainerOptions;
+ closeFunc?: () => Promise;
+ triggerWindowResize?: boolean;
+ /**
+ * Whether a close (x) button is visible on top right of the Drawer dialog or not.
+ * @default true
+ * @type boolean
+ */
+ closable?: boolean;
+
+ /**
+ * Whether to unmount child components on closing drawer or not.
+ * @default false
+ * @type boolean
+ */
+ destroyOnClose?: boolean;
+
+ /**
+ * Return the mounted node for Drawer.
+ * @default 'body'
+ * @type any ( HTMLElement| () => HTMLElement | string)
+ */
+ getContainer?: string | false | HTMLElement | (() => HTMLElement);
+
+ /**
+ * Whether to show mask or not.
+ * @default true
+ * @type boolean
+ */
+ mask?: boolean;
+
+ /**
+ * Clicking on the mask (area outside the Drawer) to close the Drawer or not.
+ * @default true
+ * @type boolean
+ */
+ maskClosable?: boolean;
+
+ /**
+ * Style for Drawer's mask element.
+ * @default {}
+ * @type object
+ */
+ maskStyle?: CSSProperties;
+
+ /**
+ * The title for Drawer.
+ * @type any (string | slot)
+ */
+ title?: VNodeChild | JSX.Element;
+ /**
+ * The class name of the container of the Drawer dialog.
+ * @type string
+ */
+ wrapClassName?: string;
+ class?: string;
+ rootClassName?: string;
+ /**
+ * Style of wrapper element which **contains mask** compare to `drawerStyle`
+ * @type object
+ */
+ wrapStyle?: CSSProperties;
+
+ /**
+ * Style of the popup layer element
+ * @type object
+ */
+ drawerStyle?: CSSProperties;
+
+ /**
+ * Style of floating layer, typically used for adjusting its position.
+ * @type object
+ */
+ bodyStyle?: CSSProperties;
+ headerStyle?: CSSProperties;
+
+ /**
+ * Width of the Drawer dialog.
+ * @default 256
+ * @type string | number
+ */
+ width?: string | number;
+
+ /**
+ * placement is top or bottom, height of the Drawer dialog.
+ * @type string | number
+ */
+ height?: string | number;
+
+ /**
+ * The z-index of the Drawer.
+ * @default 1000
+ * @type number
+ */
+ zIndex?: number;
+
+ /**
+ * The placement of the Drawer.
+ * @default 'right'
+ * @type string
+ */
+ placement?: 'top' | 'right' | 'bottom' | 'left';
+ afterOpenChange?: (open?: boolean) => void;
+ keyboard?: boolean;
+ /**
+ * Specify a callback that will be called when a user clicks mask, close button or Cancel button.
+ */
+ onClose?: (e?: Event) => void;
+}
+export interface DrawerActionType {
+ scrollBottom: () => void;
+ scrollTo: (to: number) => void;
+ getScrollWrap: () => Element | null;
+}
diff --git a/src/components/Drawer/src/useDrawer.ts b/src/components/Drawer/src/useDrawer.ts
new file mode 100644
index 0000000..22db768
--- /dev/null
+++ b/src/components/Drawer/src/useDrawer.ts
@@ -0,0 +1,161 @@
+import type {
+ UseDrawerReturnType,
+ DrawerInstance,
+ ReturnMethods,
+ DrawerProps,
+ UseDrawerInnerReturnType,
+} from './typing';
+import {
+ ref,
+ getCurrentInstance,
+ unref,
+ reactive,
+ watchEffect,
+ nextTick,
+ toRaw,
+ computed,
+} from 'vue';
+import { isProdMode } from '@/utils/env';
+import { isFunction } from '@/utils/is';
+import { tryOnUnmounted } from '@vueuse/core';
+import { isEqual } from 'lodash-es';
+import { error } from '@/utils/log';
+
+const dataTransferRef = reactive({});
+
+const openData = reactive<{ [key: number]: boolean }>({});
+
+/**
+ * @description: Applicable to separate drawer and call outside
+ */
+export function useDrawer(): UseDrawerReturnType {
+ if (!getCurrentInstance()) {
+ throw new Error('useDrawer() can only be used inside setup() or functional components!');
+ }
+ const drawer = ref(null);
+ const loaded = ref>(false);
+ const uid = ref(0);
+
+ function register(drawerInstance: DrawerInstance, uuid: number) {
+ isProdMode() &&
+ tryOnUnmounted(() => {
+ drawer.value = null;
+ loaded.value = null;
+ dataTransferRef[unref(uid)] = null;
+ });
+
+ if (unref(loaded) && isProdMode() && drawerInstance === unref(drawer)) {
+ return;
+ }
+ uid.value = uuid;
+ drawer.value = drawerInstance;
+ loaded.value = true;
+
+ drawerInstance.emitOpen = (open: boolean, uid: number) => {
+ openData[uid] = open;
+ };
+ }
+
+ const getInstance = () => {
+ const instance = unref(drawer);
+ if (!instance) {
+ error('useDrawer instance is undefined!');
+ }
+ return instance;
+ };
+
+ const methods: ReturnMethods = {
+ setDrawerProps: (props: Partial): void => {
+ getInstance()?.setDrawerProps(props);
+ },
+
+ getOpen: computed((): boolean => {
+ return openData[~~unref(uid)];
+ }),
+
+ openDrawer: (open = true, data?: T, openOnSet = true): void => {
+ getInstance()?.setDrawerProps({
+ open,
+ });
+ if (!data) return;
+
+ if (openOnSet) {
+ dataTransferRef[unref(uid)] = null;
+ dataTransferRef[unref(uid)] = toRaw(data);
+ return;
+ }
+ const equal = isEqual(toRaw(dataTransferRef[unref(uid)]), toRaw(data));
+ if (!equal) {
+ dataTransferRef[unref(uid)] = toRaw(data);
+ }
+ },
+ closeDrawer: () => {
+ getInstance()?.setDrawerProps({ open: false });
+ },
+ };
+
+ return [register, methods];
+}
+
+export const useDrawerInner = (callbackFn?: Fn): UseDrawerInnerReturnType => {
+ const drawerInstanceRef = ref>(null);
+ const currentInstance = getCurrentInstance();
+ const uidRef = ref(0);
+
+ if (!getCurrentInstance()) {
+ throw new Error('useDrawerInner() can only be used inside setup() or functional components!');
+ }
+
+ const getInstance = () => {
+ const instance = unref(drawerInstanceRef);
+ if (!instance) {
+ error('useDrawerInner instance is undefined!');
+ return;
+ }
+ return instance;
+ };
+
+ const register = (modalInstance: DrawerInstance, uuid: number) => {
+ isProdMode() &&
+ tryOnUnmounted(() => {
+ drawerInstanceRef.value = null;
+ });
+
+ uidRef.value = uuid;
+ drawerInstanceRef.value = modalInstance;
+ currentInstance?.emit('register', modalInstance, uuid);
+ };
+
+ watchEffect(() => {
+ const data = dataTransferRef[unref(uidRef)];
+ if (!data) return;
+ if (!callbackFn || !isFunction(callbackFn)) return;
+ nextTick(() => {
+ callbackFn(data);
+ });
+ });
+
+ return [
+ register,
+ {
+ changeLoading: (loading = true) => {
+ getInstance()?.setDrawerProps({ loading });
+ },
+
+ changeOkLoading: (loading = true) => {
+ getInstance()?.setDrawerProps({ confirmLoading: loading });
+ },
+ getOpen: computed((): boolean => {
+ return openData[~~unref(uidRef)];
+ }),
+
+ closeDrawer: () => {
+ getInstance()?.setDrawerProps({ open: false });
+ },
+
+ setDrawerProps: (props: Partial) => {
+ getInstance()?.setDrawerProps(props);
+ },
+ },
+ ];
+};
diff --git a/src/components/Dropdown/index.ts b/src/components/Dropdown/index.ts
new file mode 100644
index 0000000..98b3041
--- /dev/null
+++ b/src/components/Dropdown/index.ts
@@ -0,0 +1,5 @@
+import { withInstall } from '@/utils';
+import dropdown from './src/Dropdown.vue';
+
+export * from './src/typing';
+export const Dropdown = withInstall(dropdown);
diff --git a/src/components/Dropdown/src/Dropdown.vue b/src/components/Dropdown/src/Dropdown.vue
new file mode 100644
index 0000000..5deea99
--- /dev/null
+++ b/src/components/Dropdown/src/Dropdown.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.text }}
+
+
+
+
+ {{ item.text }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Dropdown/src/typing.ts b/src/components/Dropdown/src/typing.ts
new file mode 100644
index 0000000..29de8cb
--- /dev/null
+++ b/src/components/Dropdown/src/typing.ts
@@ -0,0 +1,9 @@
+export interface DropMenu {
+ onClick?: Fn;
+ to?: string;
+ icon?: string;
+ event: string | number;
+ text: string;
+ disabled?: boolean;
+ divider?: boolean;
+}
diff --git a/src/components/EllipsisText/index.ts b/src/components/EllipsisText/index.ts
new file mode 100644
index 0000000..35f9fd1
--- /dev/null
+++ b/src/components/EllipsisText/index.ts
@@ -0,0 +1,4 @@
+import { withInstall } from '@/utils';
+import ellipsisText from './src/EllipsisText.vue';
+
+export const EllipsisText = withInstall(ellipsisText);
diff --git a/src/components/EllipsisText/src/EllipsisText.vue b/src/components/EllipsisText/src/EllipsisText.vue
new file mode 100644
index 0000000..9cd5de5
--- /dev/null
+++ b/src/components/EllipsisText/src/EllipsisText.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+ false"
+ v-bind="$attrs"
+ >
+
+
+
+ false"
+ v-bind="$attrs"
+ >
+
+
+
+
+
diff --git a/src/components/EllipsisText/src/Tooltip.vue b/src/components/EllipsisText/src/Tooltip.vue
new file mode 100644
index 0000000..01d8ad5
--- /dev/null
+++ b/src/components/EllipsisText/src/Tooltip.vue
@@ -0,0 +1,158 @@
+
+
+
+
+
+
diff --git a/src/components/EllipsisText/src/_utils.ts b/src/components/EllipsisText/src/_utils.ts
new file mode 100644
index 0000000..8e6a1e4
--- /dev/null
+++ b/src/components/EllipsisText/src/_utils.ts
@@ -0,0 +1,40 @@
+// cancelAnimationFrame
+export const cancelAnimationFrame = window.cancelAnimationFrame;
+// 使用 requestAnimationFrame 模拟 setTimeout 和 setInterval
+export function rafTimeout(fn: Function, delay = 0, interval = false): object {
+ const requestAnimationFrame =
+ typeof window !== 'undefined' ? window.requestAnimationFrame : () => {};
+ let start: any = null;
+ function timeElapse(timestamp: number) {
+ /*
+ timestamp参数:与performance.now()的返回值相同,它表示requestAnimationFrame() 开始去执行回调函数的时刻
+ */
+ if (!start) {
+ start = timestamp;
+ }
+ const elapsed = timestamp - start;
+ if (elapsed >= delay) {
+ fn(); // 执行目标函数func
+ if (interval) {
+ // 使用间歇调用
+ start = null;
+ raf.id = requestAnimationFrame(timeElapse);
+ }
+ } else {
+ raf.id = requestAnimationFrame(timeElapse);
+ }
+ }
+ const raf = {
+ // 引用类型保存,方便获取 requestAnimationFrame()方法返回的 ID.
+ id: requestAnimationFrame(timeElapse),
+ };
+ return raf;
+}
+// 用于取消 rafTimeout 函数
+export function cancelRaf(raf: { id: number }): void {
+ const cancelAnimationFrame =
+ typeof window !== 'undefined' ? window.cancelAnimationFrame : () => {};
+ if (raf && raf.id) {
+ cancelAnimationFrame(raf.id);
+ }
+}
diff --git a/src/components/Excel/index.ts b/src/components/Excel/index.ts
new file mode 100644
index 0000000..73228ba
--- /dev/null
+++ b/src/components/Excel/index.ts
@@ -0,0 +1,8 @@
+import { withInstall } from '@/utils';
+import impExcel from './src/ImportExcel.vue';
+import expExcelModal from './src/ExportExcelModal.vue';
+
+export const ImpExcel = withInstall(impExcel);
+export const ExpExcelModal = withInstall(expExcelModal);
+export * from './src/typing';
+export { jsonToSheetXlsx, aoaToSheetXlsx } from './src/Export2Excel';
diff --git a/src/components/Excel/src/Export2Excel.ts b/src/components/Excel/src/Export2Excel.ts
new file mode 100644
index 0000000..3c78f76
--- /dev/null
+++ b/src/components/Excel/src/Export2Excel.ts
@@ -0,0 +1,148 @@
+import * as xlsx from 'xlsx';
+import type { WorkBook } from 'xlsx';
+import type { JsonToSheet, AoAToSheet } from './typing';
+import { AoaToMultipleSheet, JsonToMultipleSheet } from './typing';
+
+const { utils, writeFile } = xlsx;
+
+const DEF_FILE_NAME = 'excel-list.xlsx';
+const DEF_SHEET_NAME = 'sheet';
+
+/**
+ * @param data source data
+ * @param worksheet worksheet object
+ * @param min min width
+ */
+function setColumnWidth(data, worksheet, min = 3) {
+ const obj = {};
+ worksheet['!cols'] = [];
+ data.forEach((item) => {
+ Object.keys(item).forEach((key) => {
+ const cur = item[key];
+ const length = cur?.length ?? min;
+ obj[key] = Math.max(length, obj[key] ?? min);
+ });
+ });
+ Object.keys(obj).forEach((key) => {
+ worksheet['!cols'].push({
+ wch: obj[key],
+ });
+ });
+}
+
+export function jsonToSheetXlsx({
+ data,
+ header,
+ filename = DEF_FILE_NAME,
+ sheetName = DEF_SHEET_NAME,
+ json2sheetOpts = {},
+ write2excelOpts = { bookType: 'xlsx' },
+}: JsonToSheet) {
+ const arrData = [...data];
+ if (header) {
+ arrData.unshift(header);
+ json2sheetOpts.skipHeader = true;
+ }
+
+ const worksheet = utils.json_to_sheet(arrData, json2sheetOpts);
+ setColumnWidth(arrData, worksheet);
+ /* add worksheet to workbook */
+ const workbook: WorkBook = {
+ SheetNames: [sheetName],
+ Sheets: {
+ [sheetName]: worksheet,
+ },
+ };
+ /* output format determined by filename */
+ writeFile(workbook, filename, write2excelOpts);
+ /* at this point, out.xlsb will have been downloaded */
+}
+
+export function aoaToSheetXlsx({
+ data,
+ header,
+ filename = DEF_FILE_NAME,
+ write2excelOpts = { bookType: 'xlsx' },
+}: AoAToSheet) {
+ const arrData = [...data];
+ if (header) {
+ arrData.unshift(header);
+ }
+
+ const worksheet = utils.aoa_to_sheet(arrData);
+
+ /* add worksheet to workbook */
+ const workbook: WorkBook = {
+ SheetNames: [filename],
+ Sheets: {
+ [filename]: worksheet,
+ },
+ };
+ /* output format determined by filename */
+ writeFile(workbook, filename, write2excelOpts);
+ /* at this point, out.xlsb will have been downloaded */
+}
+
+/**
+ * json导出多Sheet的Xlsx
+ * @param sheetList 多sheet配置
+ * @param filename 文件名(包含后缀)
+ * @param write2excelOpts 文件配置
+ */
+export function jsonToMultipleSheetXlsx({
+ sheetList,
+ filename = DEF_FILE_NAME,
+ write2excelOpts = { bookType: 'xlsx' },
+}: JsonToMultipleSheet) {
+ const workbook: WorkBook = {
+ SheetNames: [],
+ Sheets: {},
+ };
+ sheetList.forEach((p, index) => {
+ const arrData = [...p.data];
+ if (p.header) {
+ arrData.unshift(p.header);
+ p.json2sheetOpts = p.json2sheetOpts || {};
+ p.json2sheetOpts.skipHeader = true;
+ }
+
+ const worksheet = utils.json_to_sheet(arrData, p.json2sheetOpts);
+ setColumnWidth(arrData, worksheet);
+
+ p.sheetName = p.sheetName || `${DEF_SHEET_NAME}${index}`;
+ workbook.SheetNames.push(p.sheetName);
+ workbook.Sheets[p.sheetName] = worksheet;
+ });
+ writeFile(workbook, filename, write2excelOpts);
+}
+
+/**
+ * aoa导出多Sheet的Xlsx
+ * @param sheetList 多sheet配置
+ * @param filename 文件名(包含后缀)
+ * @param write2excelOpts 文件配置
+ */
+export function aoaToMultipleSheetXlsx({
+ sheetList,
+ filename = DEF_FILE_NAME,
+ write2excelOpts = { bookType: 'xlsx' },
+}: AoaToMultipleSheet) {
+ const workbook: WorkBook = {
+ SheetNames: [],
+ Sheets: {},
+ };
+ sheetList.forEach((p, index) => {
+ const arrData = [...p.data];
+ if (p.header) {
+ arrData.unshift(p.header);
+ }
+ const worksheet = utils.aoa_to_sheet(arrData);
+
+ p.sheetName = p.sheetName || `${DEF_SHEET_NAME}${index}`;
+ workbook.SheetNames.push(p.sheetName);
+ workbook.Sheets[p.sheetName] = worksheet;
+ });
+ /* output format determined by filename */
+ writeFile(workbook, filename, write2excelOpts);
+ /* at this point, out.xlsb will have been downloaded */
+}
diff --git a/src/components/Excel/src/ExportExcelModal.vue b/src/components/Excel/src/ExportExcelModal.vue
new file mode 100644
index 0000000..cc8b7ae
--- /dev/null
+++ b/src/components/Excel/src/ExportExcelModal.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
diff --git a/src/components/Excel/src/ImportExcel.vue b/src/components/Excel/src/ImportExcel.vue
new file mode 100644
index 0000000..c8a7894
--- /dev/null
+++ b/src/components/Excel/src/ImportExcel.vue
@@ -0,0 +1,221 @@
+
+
+
+
diff --git a/src/components/Excel/src/typing.ts b/src/components/Excel/src/typing.ts
new file mode 100644
index 0000000..b5808f7
--- /dev/null
+++ b/src/components/Excel/src/typing.ts
@@ -0,0 +1,41 @@
+import type { JSON2SheetOpts, WritingOptions, BookType } from 'xlsx';
+
+export interface ExcelData {
+ header: string[];
+ results: T[];
+ meta: { sheetName: string };
+}
+
+export interface JsonToSheet {
+ data: T[];
+ header?: T;
+ filename?: string;
+ sheetName?: string;
+ json2sheetOpts?: JSON2SheetOpts;
+ write2excelOpts?: WritingOptions;
+}
+
+export interface AoAToSheet {
+ data: T[][];
+ header?: T[];
+ filename?: string;
+ sheetName?: string;
+ write2excelOpts?: WritingOptions;
+}
+
+export interface ExportModalResult {
+ filename: string;
+ bookType: BookType;
+}
+
+export interface JsonToMultipleSheet {
+ sheetList: JsonToSheet[];
+ filename?: string;
+ write2excelOpts?: WritingOptions;
+}
+
+export interface AoaToMultipleSheet {
+ sheetList: AoAToSheet[];
+ filename?: string;
+ write2excelOpts?: WritingOptions;
+}
diff --git a/src/components/FlowChart/index.ts b/src/components/FlowChart/index.ts
new file mode 100644
index 0000000..bad85db
--- /dev/null
+++ b/src/components/FlowChart/index.ts
@@ -0,0 +1,4 @@
+import { withInstall } from '@/utils';
+import flowChart from './src/FlowChart.vue';
+
+export const FlowChart = withInstall(flowChart);
diff --git a/src/components/FlowChart/src/FlowChart.vue b/src/components/FlowChart/src/FlowChart.vue
new file mode 100644
index 0000000..cf7097a
--- /dev/null
+++ b/src/components/FlowChart/src/FlowChart.vue
@@ -0,0 +1,147 @@
+
+
+
+
diff --git a/src/components/FlowChart/src/FlowChartToolbar.vue b/src/components/FlowChart/src/FlowChartToolbar.vue
new file mode 100644
index 0000000..b0f9227
--- /dev/null
+++ b/src/components/FlowChart/src/FlowChartToolbar.vue
@@ -0,0 +1,159 @@
+
+
+
+
+ {{ item.tooltip }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/FlowChart/src/adpterForTurbo.ts b/src/components/FlowChart/src/adpterForTurbo.ts
new file mode 100644
index 0000000..b68b276
--- /dev/null
+++ b/src/components/FlowChart/src/adpterForTurbo.ts
@@ -0,0 +1,75 @@
+const TurboType = {
+ SEQUENCE_FLOW: 1,
+ START_EVENT: 2,
+ END_EVENT: 3,
+ USER_TASK: 4,
+ SERVICE_TASK: 5,
+ EXCLUSIVE_GATEWAY: 6,
+};
+
+function convertFlowElementToEdge(element) {
+ const { incoming, outgoing, properties, key } = element;
+ const { text, startPoint, endPoint, pointsList, logicFlowType } = properties;
+ const edge = {
+ id: key,
+ type: logicFlowType,
+ sourceNodeId: incoming[0],
+ targetNodeId: outgoing[0],
+ text,
+ startPoint,
+ endPoint,
+ pointsList,
+ properties: {},
+ };
+ const excludeProperties = ['startPoint', 'endPoint', 'pointsList', 'text', 'logicFlowType'];
+ Object.keys(element.properties).forEach((property) => {
+ if (excludeProperties.indexOf(property) === -1) {
+ edge.properties[property] = element.properties[property];
+ }
+ });
+ return edge;
+}
+
+function convertFlowElementToNode(element) {
+ const { properties, key } = element;
+ const { x, y, text, logicFlowType } = properties;
+ const node = {
+ id: key,
+ type: logicFlowType,
+ x,
+ y,
+ text,
+ properties: {},
+ };
+ const excludeProperties = ['x', 'y', 'text', 'logicFlowType'];
+ Object.keys(element.properties).forEach((property) => {
+ if (excludeProperties.indexOf(property) === -1) {
+ node.properties[property] = element.properties[property];
+ }
+ });
+ return node;
+}
+
+export function toLogicFlowData(data) {
+ const lfData: {
+ // TODO type
+ nodes: any[];
+ edges: any[];
+ } = {
+ nodes: [],
+ edges: [],
+ };
+ const list = data.flowElementList;
+ list &&
+ list.length > 0 &&
+ list.forEach((element) => {
+ if (element.type === TurboType.SEQUENCE_FLOW) {
+ const edge = convertFlowElementToEdge(element);
+ lfData.edges.push(edge);
+ } else {
+ const node = convertFlowElementToNode(element);
+ lfData.nodes.push(node);
+ }
+ });
+ return lfData;
+}
diff --git a/src/components/FlowChart/src/config.ts b/src/components/FlowChart/src/config.ts
new file mode 100644
index 0000000..4f10d2c
--- /dev/null
+++ b/src/components/FlowChart/src/config.ts
@@ -0,0 +1,96 @@
+export const nodeList = [
+ {
+ text: '开始',
+ type: 'start',
+ class: 'node-start',
+ },
+ {
+ text: '矩形',
+ type: 'rect',
+ class: 'node-rect',
+ },
+ {
+ type: 'user',
+ text: '用户',
+ class: 'node-user',
+ },
+ {
+ type: 'push',
+ text: '推送',
+ class: 'node-push',
+ },
+ {
+ type: 'download',
+ text: '位置',
+ class: 'node-download',
+ },
+ {
+ type: 'end',
+ text: '结束',
+ class: 'node-end',
+ },
+];
+
+export const BpmnNode = [
+ {
+ type: 'bpmn:startEvent',
+ text: '开始',
+ class: 'bpmn-start',
+ },
+ {
+ type: 'bpmn:endEvent',
+ text: '结束',
+ class: 'bpmn-end',
+ },
+ {
+ type: 'bpmn:exclusiveGateway',
+ text: '网关',
+ class: 'bpmn-exclusiveGateway',
+ },
+ {
+ type: 'bpmn:userTask',
+ text: '用户',
+ class: 'bpmn-user',
+ },
+];
+
+export function configDefaultDndPanel(lf) {
+ return [
+ {
+ text: '选区',
+ icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAAOVJREFUOBGtVMENwzAIjKP++2026ETdpv10iy7WFbqFyyW6GBywLCv5gI+Dw2Bluj1znuSjhb99Gkn6QILDY2imo60p8nsnc9bEo3+QJ+AKHfMdZHnl78wyTnyHZD53Zzx73MRSgYvnqgCUHj6gwdck7Zsp1VOrz0Uz8NbKunzAW+Gu4fYW28bUYutYlzSa7B84Fh7d1kjLwhcSdYAYrdkMQVpsBr5XgDGuXwQfQr0y9zwLda+DUYXLaGKdd2ZTtvbolaO87pdo24hP7ov16N0zArH1ur3iwJpXxm+v7oAJNR4JEP8DoAuSFEkYH7cAAAAASUVORK5CYII=',
+ callback: () => {
+ lf.updateEditConfig({
+ stopMoveGraph: true,
+ });
+ },
+ },
+ {
+ type: 'circle',
+ text: '开始',
+ icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAAnBJREFUOBGdVL1rU1EcPfdGBddmaZLiEhdx1MHZQXApraCzQ7GKLgoRBxMfcRELuihWKcXFRcEWF8HBf0DdDCKYRZpnl7p0svLe9Zzbd29eQhTbC8nv+9zf130AT63jvooOGS8Vf9Nt5zxba7sXQwODfkWpkbjTQfCGUd9gIp3uuPP8bZ946g56dYQvnBg+b1HB8VIQmMFrazKcKSvFW2dQTxJnJdQ77urmXWOMBCmXM2Rke4S7UAW+/8ywwFoewmBps2tu7mbTdp8VMOkIRAkKfrVawalJTtIliclFbaOBqa0M2xImHeVIfd/nKAfVq/LGnPss5Kh00VEdSzfwnBXPUpmykNss4lUI9C1ga+8PNrBD5YeqRY2Zz8PhjooIbfJXjowvQJBqkmEkVnktWhwu2SM7SMx7Cj0N9IC0oQXRo8xwAGzQms+xrB/nNSUWVveI48ayrFGyC2+E2C+aWrZHXvOuz+CiV6iycWe1Rd1Q6+QUG07nb5SbPrL4426d+9E1axKjY3AoRrlEeSQo2Eu0T6BWAAr6COhTcWjRaYfKG5csnvytvUr/WY4rrPMB53Uo7jZRjXaG6/CFfNMaXEu75nG47X+oepU7PKJvvzGDY1YLSKHJrK7vFUwXKkaxwhCW3u+sDFMVrIju54RYYbFKpALZAo7sB6wcKyyrd+aBMryMT2gPyD6GsQoRFkGHr14TthZni9ck0z+Pnmee460mHXbRAypKNy3nuMdrWgVKj8YVV8E7PSzp1BZ9SJnJAsXdryw/h5ctboUVi4AFiCd+lQaYMw5z3LGTBKjLQOeUF35k89f58Vv/tGh+l+PE/wG0rgfIUbZK5AAAAABJRU5ErkJggg==',
+ },
+ {
+ type: 'rect',
+ text: '用户任务',
+ icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAEFVwZaAAAABGdBTUEAALGPC/xhBQAAAqlJREFUOBF9VM9rE0EUfrMJNUKLihGbpLGtaCOIR8VjQMGDePCgCCIiCNqzCAp2MyYUCXhUtF5E0D+g1t48qAd7CCLqQUQKEWkStcEfVGlLdp/fm3aW2QQdyLzf33zz5m2IsAZ9XhDpyaaIZkTS4ASzK41TFao88GuJ3hsr2pAbipHxuSYyKRugagICGANkfFnNh3HeE2N0b3nN2cgnpcictw5veJIzxmDamSlxxQZicq/mflxhbaH8BLRbuRwNtZp0JAhoplVRUdzmCe/vO27wFuuA3S5qXruGdboy5/PRGFsbFGKo/haRtQHIrM83bVeTrOgNhZReWaYGnE4aUQgTJNvijJFF4jQ8BxJE5xfKatZWmZcTQ+BVgh7s8SgPlCkcec4mGTmieTP4xd7PcpIEg1TX6gdeLW8rTVMVLVvb7ctXoH0Cydl2QOPJBG21STE5OsnbweVYzAnD3A7PVILuY0yiiyDwSm2g441r6rMSgp6iK42yqroI2QoXeJVeA+YeZSa47gZdXaZWQKTrG93rukk/l2Al6Kzh5AZEl7dDQy+JjgFahQjRopSxPbrbvK7GRe9ePWBo1wcU7sYrFZtavXALwGw/7Dnc50urrHJuTPSoO2IMV3gUQGNg87IbSOIY9BpiT9HV7FCZ94nPXb3MSnwHn/FFFE1vG6DTby+r31KAkUktB3Qf6ikUPWxW1BkXSPQeMHHiW0+HAd2GelJsZz1OJegCxqzl+CLVHa/IibuHeJ1HAKzhuDR+ymNaRFM+4jU6UWKXorRmbyqkq/D76FffevwdCp+jN3UAN/C9JRVTDuOxC/oh+EdMnqIOrlYteKSfadVRGLJFJPSB/ti/6K8f0CNymg/iH2gO/f0DwE0yjAFO6l8JaR5j0VPwPwfaYHqOqrCI319WzwhwzNW/aQAAAABJRU5ErkJggg==',
+ cls: 'important-node',
+ },
+ {
+ type: 'rect',
+ text: '系统任务',
+ icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAEFVwZaAAAABGdBTUEAALGPC/xhBQAAAqlJREFUOBF9VM9rE0EUfrMJNUKLihGbpLGtaCOIR8VjQMGDePCgCCIiCNqzCAp2MyYUCXhUtF5E0D+g1t48qAd7CCLqQUQKEWkStcEfVGlLdp/fm3aW2QQdyLzf33zz5m2IsAZ9XhDpyaaIZkTS4ASzK41TFao88GuJ3hsr2pAbipHxuSYyKRugagICGANkfFnNh3HeE2N0b3nN2cgnpcictw5veJIzxmDamSlxxQZicq/mflxhbaH8BLRbuRwNtZp0JAhoplVRUdzmCe/vO27wFuuA3S5qXruGdboy5/PRGFsbFGKo/haRtQHIrM83bVeTrOgNhZReWaYGnE4aUQgTJNvijJFF4jQ8BxJE5xfKatZWmZcTQ+BVgh7s8SgPlCkcec4mGTmieTP4xd7PcpIEg1TX6gdeLW8rTVMVLVvb7ctXoH0Cydl2QOPJBG21STE5OsnbweVYzAnD3A7PVILuY0yiiyDwSm2g441r6rMSgp6iK42yqroI2QoXeJVeA+YeZSa47gZdXaZWQKTrG93rukk/l2Al6Kzh5AZEl7dDQy+JjgFahQjRopSxPbrbvK7GRe9ePWBo1wcU7sYrFZtavXALwGw/7Dnc50urrHJuTPSoO2IMV3gUQGNg87IbSOIY9BpiT9HV7FCZ94nPXb3MSnwHn/FFFE1vG6DTby+r31KAkUktB3Qf6ikUPWxW1BkXSPQeMHHiW0+HAd2GelJsZz1OJegCxqzl+CLVHa/IibuHeJ1HAKzhuDR+ymNaRFM+4jU6UWKXorRmbyqkq/D76FffevwdCp+jN3UAN/C9JRVTDuOxC/oh+EdMnqIOrlYteKSfadVRGLJFJPSB/ti/6K8f0CNymg/iH2gO/f0DwE0yjAFO6l8JaR5j0VPwPwfaYHqOqrCI319WzwhwzNW/aQAAAABJRU5ErkJggg==',
+ cls: 'import_icon',
+ },
+ {
+ type: 'diamond',
+ text: '条件判断',
+ icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVCAYAAAHeEJUAAAAABGdBTUEAALGPC/xhBQAAAvVJREFUOBGNVEFrE0EU/mY3bQoiFlOkaUJrQUQoWMGePLX24EH0IIoHKQiCV0G8iE1covgLiqA/QTzVm1JPogc9tIJYFaQtlhQxqYjSpunu+L7JvmUTU3AgmTfvffPNN++9WSA1DO182f6xwILzD5btfAoQmwL5KJEwiQyVbSVZ0IgRyV6PTpIJ81E5ZvqfHQR0HUOBHW4L5Et2kQ6Zf7iAOhTFAA8s0pEP7AXO1uAA52SbqGk6h/6J45LaLhO64ByfcUzM39V7ZiAdS2yCePPEIQYvTUHqM/n7dgQNfBKWPjpF4ISk8q3J4nB11qw6X8l+FsF3EhlkEMfrjIer3wJTLwS2aCNcj4DbGxXTw00JmAuO+Ni6bBxVUCvS5d9aa04+so4pHW5jLTywuXAL7jJ+D06sl82Sgl2JuVBQn498zkc2bGKxULHjCnSMadBKYDYYHAtsby1EQ5lNGrQd4Y3v4Zo0XdGEmDno46yCM9Tk+RiJmUYHS/aXHPNTcjxcbTFna000PFJHIVZ5lFRqRpJWk9/+QtlOUYJj9HG5pVFEU7zqIYDVsw2s+AJaD8wTd2umgSCCyUxgGsS1Y6TBwXQQTFuZaHcd8gAGioE90hlsY+wMcs30RduYtxanjMGal8H5dMW67dmT1JFtYUEe8LiQLRsPZ6IIc7A4J5tqco3T0pnv/4u0kyzrYUq7gASuEyI8VXKvB9Odytv6jS/PNaZBln0nioJG/AVQRZvApOdhjj3Jt8QC8Im09SafwdBdvIpztpxWxpeKCC+EsFdS8DCyuCn2munFpL7ctHKp+Xc5cMybeIyMAN33SPL3ZR9QV1XVwLyzHm6Iv0/yeUuUb7PPlZC4D4HZkeu6dpF4v9j9MreGtMbxMMRLIcjJic9yHi7WQ3yVKzZVWUr5UrViJvn1FfUlwe/KYVfYyWRLSGNu16hR01U9IacajXPei0wx/5BqgInvJN+MMNtNme7ReU9SBbgntovn0kKHpFg7UogZvaZiOue/q1SBo9ktHzQAAAAASUVORK5CYII=',
+ },
+ {
+ type: 'circle',
+ text: '结束',
+ icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAA1BJREFUOBFtVE1IVUEYPXOf+tq40Y3vPcmFIdSjIorWoRG0ERWUgnb5FwVhYQSl72oUoZAboxKNFtWiwKRN0M+jpfSzqJAQclHo001tKkjl3emc8V69igP3znzfnO/M9zcDcKT67azmjYWTwl9Vn7Vumeqzj1DVb6cleQY4oAVnIOPb+mKAGxQmKI5CWNJ2aLPatxWa3aB9K7/fB+/Z0jUF6TmMlFLQqrkECWQzOZxYGjTlOl8eeKaIY5yHnFn486xBustDjWT6dG7pmjHOJd+33t0iitTPkK6tEvjxq4h2MozQ6WFSX/LkDUGfFwfhEZj1Auz/U4pyAi5Sznd7uKzznXeVHlI/Aywmk6j7fsUsEuCGADrWARXXwjxWQsUbIupDHJI7kF5dRktg0eN81IbiZXiTESic50iwS+t1oJgL83jAiBupLDCQqwziaWSoAFSeIR3P5Xv5az00wyIn35QRYTwdSYbz8pH8fxUUAtxnFvYmEmgI0wYXUXcCCSpeEVpXlsRhBnCEATxWylL9+EKCAYhe1NGstUa6356kS9NVvt3DU2fd+Wtbm/+lSbylJqsqkSm9CRhvoJVlvKPvF1RKY/FcPn5j4UfIMLn8D4UYb54BNsilTDXKnF4CfTobA0FpoW/LSp306wkXM+XaOJhZaFkcNM82ASNAWMrhrUbRfmyeI1FvRBTpN06WKxa9BK0o2E4Pd3zfBBEwPsv9sQBnmLVbLEIZ/Xe9LYwJu/Er17W6HYVBc7vmuk0xUQ+pqxdom5Fnp55SiytXLPYoMXNM4u4SNSCFWnrVIzKG3EGyMXo6n/BQOe+bX3FClY4PwydVhthOZ9NnS+ntiLh0fxtlUJHAuGaFoVmttpVMeum0p3WEXbcll94l1wM/gZ0Ccczop77VvN2I7TlsZCsuXf1WHvWEhjO8DPtyOVg2/mvK9QqboEth+7pD6NUQC1HN/TwvydGBARi9MZSzLE4b8Ru3XhX2PBxf8E1er2A6516o0w4sIA+lwURhAON82Kwe2iDAC1Watq4XHaGQ7skLcFOtI5lDxuM2gZe6WFIotPAhbaeYlU4to5cuarF1QrcZ/lwrLaCJl66JBocYZnrNlvm2+MBCTmUymPrYZVbjdlr/BxlMjmNmNI3SAAAAAElFTkSuQmCC',
+ },
+ ];
+}
diff --git a/src/components/FlowChart/src/enum.ts b/src/components/FlowChart/src/enum.ts
new file mode 100644
index 0000000..8ea134c
--- /dev/null
+++ b/src/components/FlowChart/src/enum.ts
@@ -0,0 +1,11 @@
+export enum ToolbarTypeEnum {
+ ZOOM_IN = 'zoomIn',
+ ZOOM_OUT = 'zoomOut',
+ RESET_ZOOM = 'resetZoom',
+
+ UNDO = 'undo',
+ REDO = 'redo',
+
+ SNAPSHOT = 'snapshot',
+ VIEW_DATA = 'viewData',
+}
diff --git a/src/components/FlowChart/src/types.ts b/src/components/FlowChart/src/types.ts
new file mode 100644
index 0000000..94992ba
--- /dev/null
+++ b/src/components/FlowChart/src/types.ts
@@ -0,0 +1,14 @@
+import { NodeConfig } from '@logicflow/core';
+import { ToolbarTypeEnum } from './enum';
+
+export interface NodeItem extends NodeConfig {
+ icon: string;
+}
+
+export interface ToolbarConfig {
+ type?: string | ToolbarTypeEnum;
+ tooltip?: string | boolean;
+ icon?: string;
+ disabled?: boolean;
+ separate?: boolean;
+}
diff --git a/src/components/FlowChart/src/useFlowContext.ts b/src/components/FlowChart/src/useFlowContext.ts
new file mode 100644
index 0000000..7d15f3b
--- /dev/null
+++ b/src/components/FlowChart/src/useFlowContext.ts
@@ -0,0 +1,17 @@
+import type LogicFlow from '@logicflow/core';
+
+import { provide, inject } from 'vue';
+
+const key = Symbol('flow-chart');
+
+type Instance = {
+ logicFlow: LogicFlow;
+};
+
+export function createFlowChartContext(instance: Instance) {
+ provide(key, instance);
+}
+
+export function useFlowChartContext(): Instance {
+ return inject(key) as Instance;
+}
diff --git a/src/components/Form/index.ts b/src/components/Form/index.ts
new file mode 100644
index 0000000..d85b3c5
--- /dev/null
+++ b/src/components/Form/index.ts
@@ -0,0 +1,17 @@
+import BasicForm from './src/BasicForm.vue';
+
+export * from './src/types/form';
+export * from './src/types/formItem';
+
+export { useComponentRegister } from './src/hooks/useComponentRegister';
+export { useForm } from './src/hooks/useForm';
+
+export { default as ApiSelect } from './src/components/ApiSelect.vue';
+export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue';
+export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue';
+export { default as ApiTree } from './src/components/ApiTree.vue';
+export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
+export { default as ApiCascader } from './src/components/ApiCascader.vue';
+export { default as ApiTransfer } from './src/components/ApiTransfer.vue';
+
+export { BasicForm };
diff --git a/src/components/Form/src/BasicForm.vue b/src/components/Form/src/BasicForm.vue
new file mode 100644
index 0000000..e509b6b
--- /dev/null
+++ b/src/components/Form/src/BasicForm.vue
@@ -0,0 +1,365 @@
+
+
+
+
+
diff --git a/src/components/Form/src/componentMap.ts b/src/components/Form/src/componentMap.ts
new file mode 100644
index 0000000..687db26
--- /dev/null
+++ b/src/components/Form/src/componentMap.ts
@@ -0,0 +1,93 @@
+import type { Component } from 'vue';
+import type { ComponentType } from './types';
+
+/**
+ * Component list, register here to setting it in the form
+ */
+import {
+ AutoComplete,
+ Cascader,
+ Checkbox,
+ DatePicker,
+ Divider,
+ Input,
+ InputNumber,
+ Radio,
+ Rate,
+ Select,
+ Slider,
+ Switch,
+ TimePicker,
+ TreeSelect,
+ Transfer,
+} from 'ant-design-vue';
+import ApiRadioGroup from './components/ApiRadioGroup.vue';
+import RadioButtonGroup from './components/RadioButtonGroup.vue';
+import ApiSelect from './components/ApiSelect.vue';
+import ApiTree from './components/ApiTree.vue';
+import ApiTreeSelect from './components/ApiTreeSelect.vue';
+import ApiCascader from './components/ApiCascader.vue';
+import ApiTransfer from './components/ApiTransfer.vue';
+import { BasicUpload, ImageUpload } from '@/components/Upload';
+import { StrengthMeter } from '@/components/StrengthMeter';
+import { IconPicker } from '@/components/Icon';
+import { CountdownInput } from '@/components/CountDown';
+import { BasicTitle } from '@/components/Basic';
+import { CropperAvatar } from '@/components/Cropper';
+
+const componentMap = new Map();
+
+componentMap.set('Input', Input);
+componentMap.set('InputGroup', Input.Group);
+componentMap.set('InputPassword', Input.Password);
+componentMap.set('InputSearch', Input.Search);
+componentMap.set('InputTextArea', Input.TextArea);
+componentMap.set('InputNumber', InputNumber);
+componentMap.set('AutoComplete', AutoComplete);
+componentMap.set('ImageUpload', ImageUpload);
+componentMap.set('Select', Select);
+componentMap.set('ApiSelect', ApiSelect);
+componentMap.set('ApiTree', ApiTree);
+componentMap.set('TreeSelect', TreeSelect);
+componentMap.set('ApiTreeSelect', ApiTreeSelect);
+componentMap.set('ApiRadioGroup', ApiRadioGroup);
+componentMap.set('Switch', Switch);
+componentMap.set('RadioButtonGroup', RadioButtonGroup);
+componentMap.set('RadioGroup', Radio.Group);
+componentMap.set('Checkbox', Checkbox);
+componentMap.set('CheckboxGroup', Checkbox.Group);
+componentMap.set('ApiCascader', ApiCascader);
+componentMap.set('Cascader', Cascader);
+componentMap.set('Slider', Slider);
+componentMap.set('Rate', Rate);
+componentMap.set('Transfer', Transfer);
+componentMap.set('ApiTransfer', ApiTransfer);
+
+componentMap.set('DatePicker', DatePicker);
+componentMap.set('MonthPicker', DatePicker.MonthPicker);
+componentMap.set('RangePicker', DatePicker.RangePicker);
+componentMap.set('WeekPicker', DatePicker.WeekPicker);
+componentMap.set('TimePicker', TimePicker);
+componentMap.set('TimeRangePicker', TimePicker.TimeRangePicker);
+componentMap.set('StrengthMeter', StrengthMeter);
+componentMap.set('IconPicker', IconPicker);
+componentMap.set('InputCountDown', CountdownInput);
+
+componentMap.set('Upload', BasicUpload);
+componentMap.set('Divider', Divider);
+componentMap.set('CropperAvatar', CropperAvatar);
+
+componentMap.set('BasicTitle', BasicTitle);
+
+export function add(
+ compName: ComponentType | T,
+ component: R,
+) {
+ componentMap.set(compName, component);
+}
+
+export function del(compName: ComponentType | T) {
+ componentMap.delete(compName);
+}
+
+export { componentMap };
diff --git a/src/components/Form/src/components/ApiCascader.vue b/src/components/Form/src/components/ApiCascader.vue
new file mode 100644
index 0000000..04b423c
--- /dev/null
+++ b/src/components/Form/src/components/ApiCascader.vue
@@ -0,0 +1,188 @@
+
+
+
+
+
+
+
+
+ {{ t('component.form.apiSelectNotFound') }}
+
+
+
+
+
diff --git a/src/components/Form/src/components/ApiRadioGroup.vue b/src/components/Form/src/components/ApiRadioGroup.vue
new file mode 100644
index 0000000..3cdbb8c
--- /dev/null
+++ b/src/components/Form/src/components/ApiRadioGroup.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
+ {{ item.label }}
+
+
+ {{ item.label }}
+
+
+
+
+
diff --git a/src/components/Form/src/components/ApiSelect.vue b/src/components/Form/src/components/ApiSelect.vue
new file mode 100644
index 0000000..f1c74e7
--- /dev/null
+++ b/src/components/Form/src/components/ApiSelect.vue
@@ -0,0 +1,148 @@
+
+
+
+
diff --git a/src/components/Form/src/components/ApiTransfer.vue b/src/components/Form/src/components/ApiTransfer.vue
new file mode 100644
index 0000000..1db2e07
--- /dev/null
+++ b/src/components/Form/src/components/ApiTransfer.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
diff --git a/src/components/Form/src/components/ApiTree.vue b/src/components/Form/src/components/ApiTree.vue
new file mode 100644
index 0000000..d2b9597
--- /dev/null
+++ b/src/components/Form/src/components/ApiTree.vue
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Form/src/components/ApiTreeSelect.vue b/src/components/Form/src/components/ApiTreeSelect.vue
new file mode 100644
index 0000000..c426455
--- /dev/null
+++ b/src/components/Form/src/components/ApiTreeSelect.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Form/src/components/FormAction.vue b/src/components/Form/src/components/FormAction.vue
new file mode 100644
index 0000000..64c6e19
--- /dev/null
+++ b/src/components/Form/src/components/FormAction.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Form/src/components/FormItem.vue b/src/components/Form/src/components/FormItem.vue
new file mode 100644
index 0000000..d4a6073
--- /dev/null
+++ b/src/components/Form/src/components/FormItem.vue
@@ -0,0 +1,454 @@
+
diff --git a/src/components/Form/src/components/RadioButtonGroup.vue b/src/components/Form/src/components/RadioButtonGroup.vue
new file mode 100644
index 0000000..fe9a12c
--- /dev/null
+++ b/src/components/Form/src/components/RadioButtonGroup.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
diff --git a/src/components/Form/src/helper.ts b/src/components/Form/src/helper.ts
new file mode 100644
index 0000000..6312931
--- /dev/null
+++ b/src/components/Form/src/helper.ts
@@ -0,0 +1,103 @@
+import type { Rule as ValidationRule } from 'ant-design-vue/lib/form/interface';
+import type { ComponentType } from './types';
+import { useI18n } from '@/hooks/web/useI18n';
+import { dateUtil } from '@/utils/dateUtil';
+import { isNumber, isObject } from '@/utils/is';
+
+const { t } = useI18n();
+
+/**
+ * @description: 生成placeholder
+ */
+export function createPlaceholderMessage(component: ComponentType) {
+ if (component.includes('Input') || component.includes('Complete')) {
+ return t('common.inputText');
+ }
+ if (component.includes('Picker')) {
+ return t('common.chooseText');
+ }
+ if (
+ component.includes('Select') ||
+ component.includes('Cascader') ||
+ component.includes('Checkbox') ||
+ component.includes('Radio') ||
+ component.includes('Switch')
+ ) {
+ // return `请选择${label}`;
+ return t('common.chooseText');
+ }
+ return '';
+}
+
+const DATE_TYPE = ['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker'];
+
+function genType() {
+ return [...DATE_TYPE, 'RangePicker'];
+}
+
+export function setComponentRuleType(
+ rule: ValidationRule,
+ component: ComponentType,
+ valueFormat: string,
+) {
+ if (Reflect.has(rule, 'type')) {
+ return;
+ }
+ if (['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker'].includes(component)) {
+ rule.type = valueFormat ? 'string' : 'object';
+ } else if (['RangePicker', 'Upload', 'CheckboxGroup', 'TimePicker'].includes(component)) {
+ rule.type = 'array';
+ } else if (['InputNumber'].includes(component)) {
+ rule.type = 'number';
+ }
+}
+
+export function processDateValue(attr: Recordable, component: string) {
+ const { valueFormat, value } = attr;
+ if (valueFormat) {
+ attr.value = isObject(value) ? dateUtil(value as unknown as Date).format(valueFormat) : value;
+ } else if (DATE_TYPE.includes(component) && value) {
+ attr.value = dateUtil(attr.value);
+ }
+}
+
+export const defaultValueComponents = [
+ 'Input',
+ 'InputPassword',
+ 'InputNumber',
+ 'InputSearch',
+ 'InputTextArea',
+];
+
+export function handleInputNumberValue(component?: ComponentType, val?: any) {
+ if (!component) return val;
+ if (defaultValueComponents.includes(component)) {
+ return val && isNumber(val) ? `${val}` : val;
+ }
+ return val;
+}
+
+/**
+ * 时间字段
+ */
+export const dateItemType = genType();
+
+// TODO 自定义组件封装会出现验证问题,因此这里目前改成手动触发验证
+export const NO_AUTO_LINK_COMPONENTS: ComponentType[] = [
+ 'Upload',
+ 'ApiTransfer',
+ 'ApiTree',
+ 'ApiTreeSelect',
+ 'ApiRadioGroup',
+ 'ApiCascader',
+ 'AutoComplete',
+ 'RadioButtonGroup',
+ 'ImageUpload',
+ 'ApiSelect',
+];
+
+export const simpleComponents = ['Divider', 'BasicTitle'];
+
+export function isIncludeSimpleComponents(component?: ComponentType) {
+ return simpleComponents.includes(component || '');
+}
diff --git a/src/components/Form/src/hooks/useAdvanced.ts b/src/components/Form/src/hooks/useAdvanced.ts
new file mode 100644
index 0000000..5f54dd4
--- /dev/null
+++ b/src/components/Form/src/hooks/useAdvanced.ts
@@ -0,0 +1,171 @@
+import type { ColEx } from '../types';
+import type { AdvanceState } from '../types/hooks';
+import { ComputedRef, getCurrentInstance, Ref, shallowReactive, computed, unref, watch } from 'vue';
+import type { FormProps, FormSchemaInner as FormSchema } from '../types/form';
+import { isBoolean, isFunction, isNumber, isObject } from '@/utils/is';
+import { useBreakpoint } from '@/hooks/event/useBreakpoint';
+import { useDebounceFn } from '@vueuse/core';
+
+const BASIC_COL_LEN = 24;
+
+interface UseAdvancedContext {
+ advanceState: AdvanceState;
+ emit: EmitType;
+ getProps: ComputedRef;
+ getSchema: ComputedRef;
+ formModel: Recordable;
+ defaultValueRef: Ref;
+}
+
+export default function ({
+ advanceState,
+ emit,
+ getProps,
+ getSchema,
+ formModel,
+ defaultValueRef,
+}: UseAdvancedContext) {
+ const vm = getCurrentInstance();
+
+ const { realWidthRef, screenEnum, screenRef } = useBreakpoint();
+
+ const getEmptySpan = computed((): number => {
+ if (!advanceState.isAdvanced) {
+ return 0;
+ }
+ // For some special cases, you need to manually specify additional blank lines
+ const emptySpan = unref(getProps).emptySpan || 0;
+
+ if (isNumber(emptySpan)) {
+ return emptySpan;
+ }
+ if (isObject(emptySpan)) {
+ const { span = 0 } = emptySpan;
+ const screen = unref(screenRef) as string;
+
+ const screenSpan = (emptySpan as any)[screen.toLowerCase()];
+ return screenSpan || span || 0;
+ }
+ return 0;
+ });
+
+ const debounceUpdateAdvanced = useDebounceFn(updateAdvanced, 30);
+
+ watch(
+ [() => unref(getSchema), () => advanceState.isAdvanced, () => unref(realWidthRef)],
+ () => {
+ const { showAdvancedButton } = unref(getProps);
+ if (showAdvancedButton) {
+ debounceUpdateAdvanced();
+ }
+ },
+ { immediate: true },
+ );
+
+ function getAdvanced(itemCol: Partial, itemColSum = 0, isLastAction = false) {
+ const width = unref(realWidthRef);
+
+ const mdWidth =
+ parseInt(itemCol.md as string) ||
+ parseInt(itemCol.xs as string) ||
+ parseInt(itemCol.sm as string) ||
+ (itemCol.span as number) ||
+ BASIC_COL_LEN;
+
+ const lgWidth = parseInt(itemCol.lg as string) || mdWidth;
+ const xlWidth = parseInt(itemCol.xl as string) || lgWidth;
+ const xxlWidth = parseInt(itemCol.xxl as string) || xlWidth;
+ if (width <= screenEnum.LG) {
+ itemColSum += mdWidth;
+ } else if (width < screenEnum.XL) {
+ itemColSum += lgWidth;
+ } else if (width < screenEnum.XXL) {
+ itemColSum += xlWidth;
+ } else {
+ itemColSum += xxlWidth;
+ }
+
+ if (isLastAction) {
+ advanceState.hideAdvanceBtn = false;
+ if (itemColSum <= BASIC_COL_LEN * 2) {
+ // When less than or equal to 2 lines, the collapse and expand buttons are not displayed
+ advanceState.hideAdvanceBtn = true;
+ advanceState.isAdvanced = true;
+ } else if (
+ itemColSum > BASIC_COL_LEN * 2 &&
+ itemColSum <= BASIC_COL_LEN * (unref(getProps).autoAdvancedLine || 3)
+ ) {
+ advanceState.hideAdvanceBtn = false;
+
+ // More than 3 lines collapsed by default
+ } else if (!advanceState.isLoad) {
+ advanceState.isLoad = true;
+ advanceState.isAdvanced = !advanceState.isAdvanced;
+ }
+ return { isAdvanced: advanceState.isAdvanced, itemColSum };
+ }
+ if (itemColSum > BASIC_COL_LEN * (unref(getProps).alwaysShowLines || 1)) {
+ return { isAdvanced: advanceState.isAdvanced, itemColSum };
+ } else {
+ // The first line is always displayed
+ return { isAdvanced: true, itemColSum };
+ }
+ }
+
+ const fieldsIsAdvancedMap = shallowReactive({});
+
+ function updateAdvanced() {
+ let itemColSum = 0;
+ let realItemColSum = 0;
+ const { baseColProps = {} } = unref(getProps);
+
+ for (const schema of unref(getSchema)) {
+ const { show, colProps } = schema;
+ let isShow = true;
+
+ if (isBoolean(show)) {
+ isShow = show;
+ }
+
+ if (isFunction(show)) {
+ isShow = show({
+ schema: schema,
+ model: formModel,
+ field: schema.field,
+ values: {
+ ...unref(defaultValueRef),
+ ...formModel,
+ },
+ });
+ }
+
+ if (isShow && (colProps || baseColProps)) {
+ const { itemColSum: sum, isAdvanced } = getAdvanced(
+ { ...baseColProps, ...colProps },
+ itemColSum,
+ );
+
+ itemColSum = sum || 0;
+ if (isAdvanced) {
+ realItemColSum = itemColSum;
+ }
+ fieldsIsAdvancedMap[schema.field] = isAdvanced;
+ }
+ }
+
+ // 确保页面发送更新
+ vm?.proxy?.$forceUpdate();
+
+ advanceState.actionSpan = (realItemColSum % BASIC_COL_LEN) + unref(getEmptySpan);
+
+ getAdvanced(unref(getProps).actionColOptions || { span: BASIC_COL_LEN }, itemColSum, true);
+
+ emit('advanced-change');
+ }
+
+ function handleToggleAdvanced() {
+ advanceState.isAdvanced = !advanceState.isAdvanced;
+ }
+
+ return { handleToggleAdvanced, fieldsIsAdvancedMap };
+}
diff --git a/src/components/Form/src/hooks/useAutoFocus.ts b/src/components/Form/src/hooks/useAutoFocus.ts
new file mode 100644
index 0000000..a88a4c3
--- /dev/null
+++ b/src/components/Form/src/hooks/useAutoFocus.ts
@@ -0,0 +1,44 @@
+import type { ComputedRef, Ref } from 'vue';
+import {
+ type FormSchemaInner as FormSchema,
+ type FormActionType,
+ type FormProps,
+} from '../types/form';
+
+import { unref, nextTick, watchEffect } from 'vue';
+
+interface UseAutoFocusContext {
+ getSchema: ComputedRef;
+ getProps: ComputedRef;
+ isInitedDefault: Ref;
+ formElRef: Ref;
+}
+export async function useAutoFocus({
+ getSchema,
+ getProps,
+ formElRef,
+ isInitedDefault,
+}: UseAutoFocusContext) {
+ watchEffect(async () => {
+ if (unref(isInitedDefault) || !unref(getProps).autoFocusFirstItem) {
+ return;
+ }
+ await nextTick();
+ const schemas = unref(getSchema);
+ const formEl = unref(formElRef);
+ const el = (formEl as any)?.$el as HTMLElement;
+ if (!formEl || !el || !schemas || schemas.length === 0) {
+ return;
+ }
+
+ const firstItem = schemas[0];
+ // Only open when the first form item is input type
+ if (!firstItem.component || !firstItem.component.includes('Input')) {
+ return;
+ }
+
+ const inputEl = el.querySelector('.ant-row:first-child input') as Nullable;
+ if (!inputEl) return;
+ inputEl?.focus();
+ });
+}
diff --git a/src/components/Form/src/hooks/useComponentRegister.ts b/src/components/Form/src/hooks/useComponentRegister.ts
new file mode 100644
index 0000000..25f23ef
--- /dev/null
+++ b/src/components/Form/src/hooks/useComponentRegister.ts
@@ -0,0 +1,19 @@
+import type { ComponentType } from '../types';
+import { tryOnUnmounted } from '@vueuse/core';
+import { add, del } from '../componentMap';
+import type { Component } from 'vue';
+import { isPascalCase } from '@/utils/is';
+
+export function useComponentRegister(
+ compName: ComponentType | T,
+ comp: R,
+) {
+ if (!isPascalCase(compName)) {
+ throw new Error('compName must be in PascalCase');
+ }
+
+ add(compName, comp);
+ tryOnUnmounted(() => {
+ del(compName);
+ });
+}
diff --git a/src/components/Form/src/hooks/useForm.ts b/src/components/Form/src/hooks/useForm.ts
new file mode 100644
index 0000000..eccabe7
--- /dev/null
+++ b/src/components/Form/src/hooks/useForm.ts
@@ -0,0 +1,127 @@
+import type {
+ FormProps,
+ FormActionType,
+ UseFormReturnType,
+ FormSchemaInner as FormSchema,
+} from '../types/form';
+import type { NamePath } from 'ant-design-vue/lib/form/interface';
+import type { DynamicProps } from '#/utils';
+import { ref, onUnmounted, unref, nextTick, watch } from 'vue';
+import { isProdMode } from '@/utils/env';
+import { error } from '@/utils/log';
+import { getDynamicProps } from '@/utils';
+
+export declare type ValidateFields = (nameList?: NamePath[]) => Promise;
+
+type Props = Partial>;
+
+export function useForm(props?: Props): UseFormReturnType {
+ const formRef = ref>(null);
+ const loadedRef = ref>(false);
+
+ async function getForm() {
+ const form = unref(formRef);
+ if (!form) {
+ error(
+ 'The form instance has not been obtained, please make sure that the form has been rendered when performing the form operation!',
+ );
+ }
+ await nextTick();
+ return form as FormActionType;
+ }
+
+ function register(instance: FormActionType) {
+ isProdMode() &&
+ onUnmounted(() => {
+ formRef.value = null;
+ loadedRef.value = null;
+ });
+ if (unref(loadedRef) && isProdMode() && instance === unref(formRef)) return;
+
+ formRef.value = instance;
+ loadedRef.value = true;
+
+ watch(
+ () => props,
+ () => {
+ props && instance.setProps(getDynamicProps(props));
+ },
+ {
+ immediate: true,
+ deep: true,
+ },
+ );
+ }
+
+ const methods: FormActionType = {
+ scrollToField: async (name: NamePath, options?: ScrollOptions | undefined) => {
+ const form = await getForm();
+ form.scrollToField(name, options);
+ },
+ setProps: async (formProps: Partial) => {
+ const form = await getForm();
+ form.setProps(formProps);
+ },
+
+ updateSchema: async (data: Partial | Partial[]) => {
+ const form = await getForm();
+ form.updateSchema(data);
+ },
+
+ resetSchema: async (data: Partial | Partial[]) => {
+ const form = await getForm();
+ form.resetSchema(data);
+ },
+
+ clearValidate: async (name?: string | string[]) => {
+ const form = await getForm();
+ form.clearValidate(name);
+ },
+
+ resetFields: async () => {
+ getForm().then(async (form) => {
+ await form.resetFields();
+ });
+ },
+
+ removeSchemaByField: async (field: string | string[]) => {
+ unref(formRef)?.removeSchemaByField(field);
+ },
+
+ // TODO promisify
+ getFieldsValue: () => {
+ return unref(formRef)?.getFieldsValue() as T;
+ },
+
+ setFieldsValue: async >(values: T) => {
+ const form = await getForm();
+ form.setFieldsValue(values);
+ },
+
+ appendSchemaByField: async (
+ schema: FormSchema | FormSchema[],
+ prefixField: string | undefined,
+ first?: boolean,
+ ) => {
+ const form = await getForm();
+ form.appendSchemaByField(schema, prefixField, first);
+ },
+
+ submit: async (): Promise => {
+ const form = await getForm();
+ return form.submit();
+ },
+
+ validate: async (nameList?: NamePath[] | false): Promise => {
+ const form = await getForm();
+ return form.validate(nameList);
+ },
+
+ validateFields: async (nameList?: NamePath[]): Promise => {
+ const form = await getForm();
+ return form.validateFields(nameList);
+ },
+ };
+
+ return [register, methods];
+}
diff --git a/src/components/Form/src/hooks/useFormContext.ts b/src/components/Form/src/hooks/useFormContext.ts
new file mode 100644
index 0000000..0996132
--- /dev/null
+++ b/src/components/Form/src/hooks/useFormContext.ts
@@ -0,0 +1,17 @@
+import type { InjectionKey } from 'vue';
+import { createContext, useContext } from '@/hooks/core/useContext';
+
+export interface FormContextProps {
+ resetAction: () => Promise;
+ submitAction: () => Promise;
+}
+
+const key: InjectionKey = Symbol();
+
+export function createFormContext(context: FormContextProps) {
+ return createContext(context, key);
+}
+
+export function useFormContext() {
+ return useContext(key);
+}
diff --git a/src/components/Form/src/hooks/useFormEvents.ts b/src/components/Form/src/hooks/useFormEvents.ts
new file mode 100644
index 0000000..edb7085
--- /dev/null
+++ b/src/components/Form/src/hooks/useFormEvents.ts
@@ -0,0 +1,454 @@
+import type { ComputedRef, Ref } from 'vue';
+import type { FormProps, FormSchemaInner as FormSchema, FormActionType } from '../types/form';
+import type { NamePath } from 'ant-design-vue/lib/form/interface';
+import { unref, toRaw, nextTick } from 'vue';
+import { isArray, isFunction, isObject, isString, isDef, isNil } from '@/utils/is';
+import { deepMerge } from '@/utils';
+import {
+ dateItemType,
+ handleInputNumberValue,
+ defaultValueComponents,
+ isIncludeSimpleComponents,
+} from '../helper';
+import { dateUtil } from '@/utils/dateUtil';
+import { cloneDeep, set, uniqBy, get } from 'lodash-es';
+import { error } from '@/utils/log';
+
+interface UseFormActionContext {
+ emit: EmitType;
+ getProps: ComputedRef;
+ getSchema: ComputedRef;
+ formModel: Recordable;
+ defaultValueRef: Ref;
+ formElRef: Ref;
+ schemaRef: Ref;
+ handleFormValues: Fn;
+}
+
+function tryConstructArray(field: string, values: Recordable = {}): any[] | undefined {
+ const pattern = /^\[(.+)\]$/;
+ if (pattern.test(field)) {
+ const match = field.match(pattern);
+ if (match && match[1]) {
+ const keys = match[1].split(',');
+ if (!keys.length) {
+ return undefined;
+ }
+
+ const result = [];
+ keys.forEach((k, index) => {
+ set(result, index, values[k.trim()]);
+ });
+
+ return result.filter(Boolean).length ? result : undefined;
+ }
+ }
+}
+
+function tryConstructObject(field: string, values: Recordable = {}): Recordable | undefined {
+ const pattern = /^\{(.+)\}$/;
+ if (pattern.test(field)) {
+ const match = field.match(pattern);
+ if (match && match[1]) {
+ const keys = match[1].split(',');
+ if (!keys.length) {
+ return;
+ }
+
+ const result = {};
+ keys.forEach((k) => {
+ set(result, k.trim(), values[k.trim()]);
+ });
+
+ return Object.values(result).filter(Boolean).length ? result : undefined;
+ }
+ }
+}
+
+export function useFormEvents({
+ emit,
+ getProps,
+ formModel,
+ getSchema,
+ defaultValueRef,
+ formElRef,
+ schemaRef,
+ handleFormValues,
+}: UseFormActionContext) {
+ async function resetFields(): Promise {
+ const { resetFunc, submitOnReset } = unref(getProps);
+ resetFunc && isFunction(resetFunc) && (await resetFunc());
+
+ const formEl = unref(formElRef);
+ if (!formEl) return;
+
+ Object.keys(formModel).forEach((key) => {
+ const schema = unref(getSchema).find((item) => item.field === key);
+ const defaultValueObj = schema?.defaultValueObj;
+ const fieldKeys = Object.keys(defaultValueObj || {});
+ if (fieldKeys.length) {
+ fieldKeys.map((field) => {
+ formModel[field] = defaultValueObj![field];
+ });
+ }
+ formModel[key] = getDefaultValue(schema, defaultValueRef, key);
+ });
+ nextTick(() => clearValidate());
+
+ emit('reset', toRaw(formModel));
+ submitOnReset && handleSubmit();
+ }
+ // 获取表单fields
+ const getAllFields = () =>
+ unref(getSchema)
+ .map((item) => [...(item.fields || []), item.field])
+ .flat(1)
+ .filter(Boolean);
+ /**
+ * @description: Set form value
+ */
+ async function setFieldsValue(values: Recordable): Promise {
+ if (Object.keys(values).length === 0) {
+ return;
+ }
+
+ const fields = getAllFields();
+
+ // key 支持 a.b.c 的嵌套写法
+ const delimiter = '.';
+ const nestKeyArray = fields.filter((item) => String(item).indexOf(delimiter) >= 0);
+
+ const validKeys: string[] = [];
+ fields.forEach((key) => {
+ const schema = unref(getSchema).find((item) => item.field === key);
+ let value = get(values, key);
+ const hasKey = Reflect.has(values, key);
+
+ value = handleInputNumberValue(schema?.component, value);
+ const { componentProps } = schema || {};
+ let _props = componentProps as any;
+ if (typeof componentProps === 'function') {
+ _props = _props({
+ formModel: unref(formModel),
+ formActionType,
+ });
+ }
+
+ const constructValue = tryConstructArray(key, values) || tryConstructObject(key, values);
+
+ // 0| '' is allow
+ if (hasKey || !!constructValue) {
+ const fieldValue = constructValue || value;
+ // time type
+ if (itemIsDateType(key)) {
+ if (Array.isArray(fieldValue)) {
+ const arr: any[] = [];
+ for (const ele of fieldValue) {
+ arr.push(ele ? dateUtil(ele) : null);
+ }
+ unref(formModel)[key] = arr;
+ } else {
+ unref(formModel)[key] = fieldValue
+ ? _props?.valueFormat
+ ? fieldValue
+ : dateUtil(fieldValue)
+ : null;
+ }
+ } else {
+ unref(formModel)[key] = fieldValue;
+ }
+ if (_props?.onChange) {
+ _props?.onChange(fieldValue);
+ }
+ validKeys.push(key);
+ } else {
+ nestKeyArray.forEach((nestKey: string) => {
+ try {
+ const value = nestKey.split('.').reduce((out, item) => out[item], values);
+ if (isDef(value)) {
+ unref(formModel)[nestKey] = unref(value);
+ validKeys.push(nestKey);
+ }
+ } catch (e) {
+ // key not exist
+ if (isDef(defaultValueRef.value[nestKey])) {
+ unref(formModel)[nestKey] = cloneDeep(unref(defaultValueRef.value[nestKey]));
+ }
+ }
+ });
+ }
+ });
+ validateFields(validKeys).catch((_) => {});
+ }
+
+ /**
+ * @description: Delete based on field name
+ */
+ async function removeSchemaByField(fields: string | string[]): Promise {
+ const schemaList: FormSchema[] = cloneDeep(unref(getSchema));
+ if (!fields) {
+ return;
+ }
+
+ let fieldList: string[] = isString(fields) ? [fields] : fields;
+ if (isString(fields)) {
+ fieldList = [fields];
+ }
+ for (const field of fieldList) {
+ _removeSchemaByFeild(field, schemaList);
+ }
+ schemaRef.value = schemaList;
+ }
+
+ /**
+ * @description: Delete based on field name
+ */
+ function _removeSchemaByFeild(field: string, schemaList: FormSchema[]): void {
+ if (isString(field)) {
+ const index = schemaList.findIndex((schema) => schema.field === field);
+ if (index !== -1) {
+ delete formModel[field];
+ schemaList.splice(index, 1);
+ }
+ }
+ }
+
+ /**
+ * @description: Insert after a certain field, if not insert the last
+ */
+ async function appendSchemaByField(
+ schema: FormSchema | FormSchema[],
+ prefixField?: string,
+ first = false,
+ ) {
+ const schemaList: FormSchema[] = cloneDeep(unref(getSchema));
+ const addSchemaIds: string[] = Array.isArray(schema)
+ ? schema.map((item) => item.field)
+ : [schema.field];
+ if (schemaList.find((item) => addSchemaIds.includes(item.field))) {
+ error('There are schemas that have already been added');
+ return;
+ }
+ const index = schemaList.findIndex((schema) => schema.field === prefixField);
+ const _schemaList = isObject(schema) ? [schema as FormSchema] : (schema as FormSchema[]);
+ if (!prefixField || index === -1 || first) {
+ first ? schemaList.unshift(..._schemaList) : schemaList.push(..._schemaList);
+ } else if (index !== -1) {
+ schemaList.splice(index + 1, 0, ..._schemaList);
+ }
+ schemaRef.value = schemaList;
+ _setDefaultValue(schema);
+ }
+
+ async function resetSchema(data: Partial | Partial[]) {
+ let updateData: Partial[] = [];
+ if (isObject(data)) {
+ updateData.push(data as FormSchema);
+ }
+ if (isArray(data)) {
+ updateData = [...data];
+ }
+
+ const hasField = updateData.every(
+ (item) =>
+ isIncludeSimpleComponents(item.component) || (Reflect.has(item, 'field') && item.field),
+ );
+
+ if (!hasField) {
+ error(
+ 'All children of the form Schema array that need to be updated must contain the `field` field',
+ );
+ return;
+ }
+ schemaRef.value = updateData as FormSchema[];
+ }
+
+ async function updateSchema(data: Partial | Partial[]) {
+ let updateData: Partial[] = [];
+ if (isObject(data)) {
+ updateData.push(data as FormSchema);
+ }
+ if (isArray(data)) {
+ updateData = [...data];
+ }
+
+ const hasField = updateData.every(
+ (item) =>
+ isIncludeSimpleComponents(item.component) || (Reflect.has(item, 'field') && item.field),
+ );
+
+ if (!hasField) {
+ error(
+ 'All children of the form Schema array that need to be updated must contain the `field` field',
+ );
+ return;
+ }
+ const schema: FormSchema[] = [];
+ const updatedSchema: FormSchema[] = [];
+ unref(getSchema).forEach((val) => {
+ const updatedItem = updateData.find((item) => val.field === item.field);
+
+ if (updatedItem) {
+ const newSchema = deepMerge(val, updatedItem);
+ updatedSchema.push(newSchema as FormSchema);
+ schema.push(newSchema as FormSchema);
+ } else {
+ schema.push(val);
+ }
+ });
+ _setDefaultValue(updatedSchema);
+
+ schemaRef.value = uniqBy(schema, 'field');
+ }
+
+ function _setDefaultValue(data: FormSchema | FormSchema[]) {
+ let schemas: FormSchema[] = [];
+ if (isObject(data)) {
+ schemas.push(data as FormSchema);
+ }
+ if (isArray(data)) {
+ schemas = [...data];
+ }
+
+ const obj: Recordable = {};
+ const currentFieldsValue = getFieldsValue();
+ schemas.forEach((item) => {
+ if (
+ !isIncludeSimpleComponents(item.component) &&
+ Reflect.has(item, 'field') &&
+ item.field &&
+ !isNil(item.defaultValue) &&
+ (!(item.field in currentFieldsValue) || isNil(currentFieldsValue[item.field]))
+ ) {
+ obj[item.field] = item.defaultValue;
+ }
+ });
+ setFieldsValue(obj);
+ }
+
+ function getFieldsValue(): Recordable {
+ const formEl = unref(formElRef);
+ if (!formEl) return {};
+ return handleFormValues(toRaw(unref(formModel)));
+ }
+
+ /**
+ * @description: Is it time
+ */
+ function itemIsDateType(key: string) {
+ return unref(getSchema).some((item) => {
+ return item.field === key && item.component ? dateItemType.includes(item.component) : false;
+ });
+ }
+
+ async function validateFields(nameList?: NamePath[] | undefined) {
+ const values = await unref(formElRef)?.validateFields(nameList);
+ return handleFormValues(values);
+ }
+
+ async function setProps(formProps: Partial): Promise {
+ await unref(formElRef)?.setProps(formProps);
+ }
+
+ async function validate(nameList?: NamePath[] | false | undefined) {
+ let _nameList: any;
+ if (nameList === undefined) {
+ _nameList = getAllFields();
+ } else {
+ _nameList = nameList === Array.isArray(nameList) ? nameList : undefined;
+ }
+ const values = await unref(formElRef)?.validate(_nameList);
+ return handleFormValues(values);
+ }
+
+ async function clearValidate(name?: string | string[]) {
+ await unref(formElRef)?.clearValidate(name);
+ }
+
+ async function scrollToField(name: NamePath, options?: ScrollOptions | undefined) {
+ await unref(formElRef)?.scrollToField(name, options);
+ }
+
+ /**
+ * @description: Form submission
+ */
+ async function handleSubmit(e?: Event): Promise {
+ e && e.preventDefault();
+ const { submitFunc } = unref(getProps);
+ if (submitFunc && isFunction(submitFunc)) {
+ await submitFunc();
+ return;
+ }
+ const formEl = unref(formElRef);
+ if (!formEl) return;
+ try {
+ const values = await validate();
+ emit('submit', values);
+ } catch (error: any) {
+ if (error?.outOfDate === false && error?.errorFields) {
+ return;
+ }
+ throw new Error(error);
+ }
+ }
+
+ const formActionType: Partial = {
+ getFieldsValue,
+ setFieldsValue,
+ resetFields,
+ updateSchema,
+ resetSchema,
+ setProps,
+ removeSchemaByField,
+ appendSchemaByField,
+ clearValidate,
+ validateFields,
+ validate,
+ submit: handleSubmit,
+ scrollToField: scrollToField,
+ };
+
+ return {
+ handleSubmit,
+ clearValidate,
+ validate,
+ validateFields,
+ getFieldsValue,
+ updateSchema,
+ resetSchema,
+ appendSchemaByField,
+ removeSchemaByField,
+ resetFields,
+ setFieldsValue,
+ scrollToField,
+ };
+}
+
+function getDefaultValue(
+ schema: FormSchema | undefined,
+ defaultValueRef: UseFormActionContext['defaultValueRef'],
+ key: string,
+) {
+ let defaultValue = cloneDeep(defaultValueRef.value[key]);
+ const isInput = checkIsInput(schema);
+ if (isInput) {
+ return defaultValue || undefined;
+ }
+ if (!defaultValue && schema && checkIsRangeSlider(schema)) {
+ defaultValue = [0, 0];
+ }
+ if (!defaultValue && schema && schema.component === 'ApiTree') {
+ defaultValue = [];
+ }
+ return defaultValue;
+}
+
+function checkIsRangeSlider(schema: FormSchema) {
+ if (schema.component === 'Slider' && schema.componentProps && 'range' in schema.componentProps) {
+ return true;
+ }
+}
+
+function checkIsInput(schema?: FormSchema) {
+ return schema?.component && defaultValueComponents.includes(schema.component);
+}
diff --git a/src/components/Form/src/hooks/useFormValues.ts b/src/components/Form/src/hooks/useFormValues.ts
new file mode 100644
index 0000000..2e06152
--- /dev/null
+++ b/src/components/Form/src/hooks/useFormValues.ts
@@ -0,0 +1,160 @@
+import { isArray, isFunction, isEmpty, isObject, isString, isNil } from '@/utils/is';
+import { dateUtil } from '@/utils/dateUtil';
+import { unref } from 'vue';
+import type { Ref, ComputedRef } from 'vue';
+import type { FormProps, FormSchemaInner as FormSchema } from '../types/form';
+import { cloneDeep, get, set, unset } from 'lodash-es';
+
+interface UseFormValuesContext {
+ defaultValueRef: Ref;
+ getSchema: ComputedRef;
+ getProps: ComputedRef;
+ formModel: Recordable;
+}
+
+/**
+ * @desription deconstruct array-link key. This method will mutate the target.
+ */
+function tryDeconstructArray(key: string, value: any, target: Recordable) {
+ const pattern = /^\[(.+)\]$/;
+ if (pattern.test(key)) {
+ const match = key.match(pattern);
+ if (match && match[1]) {
+ const keys = match[1].split(',');
+ value = Array.isArray(value) ? value : [value];
+ keys.forEach((k, index) => {
+ set(target, k.trim(), value[index]);
+ });
+ return true;
+ }
+ }
+}
+
+/**
+ * @desription deconstruct object-link key. This method will mutate the target.
+ */
+function tryDeconstructObject(key: string, value: any, target: Recordable) {
+ const pattern = /^\{(.+)\}$/;
+ if (pattern.test(key)) {
+ const match = key.match(pattern);
+ if (match && match[1]) {
+ const keys = match[1].split(',');
+ value = isObject(value) ? value : {};
+ keys.forEach((k) => {
+ set(target, k.trim(), value[k.trim()]);
+ });
+ return true;
+ }
+ }
+}
+
+export function useFormValues({
+ defaultValueRef,
+ getSchema,
+ formModel,
+ getProps,
+}: UseFormValuesContext) {
+ // Processing form values
+ function handleFormValues(values: Recordable) {
+ if (!isObject(values)) {
+ return {};
+ }
+ const res: Recordable = {};
+ for (const item of Object.entries(values)) {
+ let [, value] = item;
+ const [key] = item;
+ if (!key || (isArray(value) && value.length === 0) || isFunction(value)) {
+ continue;
+ }
+ const transformDateFunc = unref(getProps).transformDateFunc;
+ if (isObject(value)) {
+ value = transformDateFunc?.(value);
+ }
+
+ if (isArray(value) && value[0]?.format && value[1]?.format) {
+ value = value.map((item) => transformDateFunc?.(item));
+ }
+ // Remove spaces
+ if (isString(value)) {
+ value = value.trim();
+ }
+ if (!tryDeconstructArray(key, value, res) && !tryDeconstructObject(key, value, res)) {
+ // 没有解构成功的,按原样赋值
+ set(res, key, value);
+ }
+ }
+ return handleRangeTimeValue(res);
+ }
+
+ /**
+ * @description: Processing time interval parameters
+ */
+ function handleRangeTimeValue(values: Recordable) {
+ const fieldMapToTime = unref(getProps).fieldMapToTime;
+
+ if (!fieldMapToTime || !Array.isArray(fieldMapToTime)) {
+ return values;
+ }
+
+ for (const [field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD'] of fieldMapToTime) {
+ if (!field || !startTimeKey || !endTimeKey) {
+ continue;
+ }
+ // If the value to be converted is empty, remove the field
+ if (!get(values, field)) {
+ unset(values, field);
+ continue;
+ }
+
+ const [startTime, endTime]: string[] = get(values, field);
+
+ const [startTimeFormat, endTimeFormat] = Array.isArray(format) ? format : [format, format];
+
+ if (!isNil(startTime) && !isEmpty(startTime)) {
+ set(values, startTimeKey, formatTime(startTime, startTimeFormat));
+ }
+ if (!isNil(endTime) && !isEmpty(endTime)) {
+ set(values, endTimeKey, formatTime(endTime, endTimeFormat));
+ }
+ unset(values, field);
+ }
+
+ return values;
+ }
+
+ function formatTime(time: string, format: string) {
+ if (format === 'timestamp') {
+ return dateUtil(time).unix();
+ } else if (format === 'timestampStartDay') {
+ return dateUtil(time).startOf('day').unix();
+ }
+ return dateUtil(time).format(format);
+ }
+
+ function initDefault() {
+ const schemas = unref(getSchema);
+ const obj: Recordable = {};
+ schemas.forEach((item) => {
+ const { defaultValue, defaultValueObj } = item;
+ const fieldKeys = Object.keys(defaultValueObj || {});
+ if (fieldKeys.length) {
+ fieldKeys.map((field) => {
+ obj[field] = defaultValueObj![field];
+ if (formModel[field] === undefined) {
+ formModel[field] = defaultValueObj![field];
+ }
+ });
+ }
+ if (!isNil(defaultValue)) {
+ obj[item.field] = defaultValue;
+
+ if (formModel[item.field] === undefined) {
+ formModel[item.field] = defaultValue;
+ }
+ }
+ });
+ defaultValueRef.value = cloneDeep(obj);
+ }
+
+ return { handleFormValues, initDefault };
+}
diff --git a/src/components/Form/src/hooks/useLabelWidth.ts b/src/components/Form/src/hooks/useLabelWidth.ts
new file mode 100644
index 0000000..511ba42
--- /dev/null
+++ b/src/components/Form/src/hooks/useLabelWidth.ts
@@ -0,0 +1,42 @@
+import type { Ref } from 'vue';
+import { computed, unref } from 'vue';
+import type { FormProps, FormSchemaInner as FormSchema } from '../types/form';
+import { isNumber } from '@/utils/is';
+
+export function useItemLabelWidth(schemaItemRef: Ref, propsRef: Ref) {
+ return computed(() => {
+ const schemaItem = unref(schemaItemRef);
+ const { labelCol = {}, wrapperCol = {} } = schemaItem.itemProps || {};
+ const { labelWidth, disabledLabelWidth } = schemaItem;
+
+ const {
+ labelWidth: globalLabelWidth,
+ labelCol: globalLabelCol,
+ wrapperCol: globWrapperCol,
+ layout,
+ } = unref(propsRef);
+
+ // If labelWidth is set globally, all items setting
+ if ((!globalLabelWidth && !labelWidth && !globalLabelCol) || disabledLabelWidth) {
+ labelCol.style = {
+ textAlign: 'left',
+ };
+ return { labelCol, wrapperCol };
+ }
+ let width = labelWidth || globalLabelWidth;
+ const col = { ...globalLabelCol, ...labelCol };
+ const wrapCol = { ...globWrapperCol, ...wrapperCol };
+
+ if (width) {
+ width = isNumber(width) ? `${width}px` : width;
+ }
+
+ return {
+ labelCol: { style: { width }, ...col },
+ wrapperCol: {
+ style: { width: layout === 'vertical' ? '100%' : `calc(100% - ${width})` },
+ ...wrapCol,
+ },
+ };
+ });
+}
diff --git a/src/components/Form/src/props.ts b/src/components/Form/src/props.ts
new file mode 100644
index 0000000..91c69f9
--- /dev/null
+++ b/src/components/Form/src/props.ts
@@ -0,0 +1,104 @@
+import type { FieldMapToTime, FormSchema } from './types/form';
+import type { CSSProperties, PropType } from 'vue';
+import type { ColEx } from './types';
+import type { TableActionType } from '@/components/Table';
+import type { RowProps } from 'ant-design-vue/lib/grid/Row';
+import { propTypes } from '@/utils/propTypes';
+
+import { ButtonProps } from '@/components/Button';
+
+export const basicProps = {
+ model: {
+ type: Object as PropType,
+ default: () => ({}),
+ },
+ // 标签宽度 固定宽度
+ labelWidth: {
+ type: [Number, String] as PropType,
+ default: 0,
+ },
+ fieldMapToTime: {
+ type: Array as PropType,
+ default: () => [],
+ },
+ compact: propTypes.bool,
+ // 表单配置规则
+ schemas: {
+ type: Array as PropType,
+ default: () => [],
+ },
+ mergeDynamicData: {
+ type: Object as PropType,
+ default: null,
+ },
+ baseRowStyle: {
+ type: Object as PropType,
+ },
+ baseColProps: {
+ type: Object as PropType>,
+ },
+ autoSetPlaceHolder: propTypes.bool.def(true),
+ // 在INPUT组件上单击回车时,是否自动提交
+ autoSubmitOnEnter: propTypes.bool.def(false),
+ submitOnReset: propTypes.bool,
+ submitOnChange: propTypes.bool,
+ size: propTypes.oneOf(['default', 'small', 'large']).def('default'),
+ // 禁用表单
+ disabled: propTypes.bool,
+ emptySpan: {
+ type: [Number, Object] as PropType,
+ default: 0,
+ },
+ // 是否显示收起展开按钮
+ showAdvancedButton: propTypes.bool,
+ // 转化时间
+ transformDateFunc: {
+ type: Function as PropType,
+ default: (date: any) => {
+ return date?.format?.('YYYY-MM-DD HH:mm:ss') ?? date;
+ },
+ },
+ rulesMessageJoinLabel: propTypes.bool.def(true),
+ // 超过3行自动折叠
+ autoAdvancedLine: propTypes.number.def(3),
+ // 不受折叠影响的行数
+ alwaysShowLines: propTypes.number.def(1),
+
+ // 是否显示操作按钮
+ showActionButtonGroup: propTypes.bool.def(true),
+ // 操作列Col配置
+ actionColOptions: Object as PropType>,
+ // 显示重置按钮
+ showResetButton: propTypes.bool.def(true),
+ // 是否聚焦第一个输入框,只在第一个表单项为input的时候作用
+ autoFocusFirstItem: propTypes.bool,
+ // 重置按钮配置
+ resetButtonOptions: Object as PropType>,
+
+ // 显示确认按钮
+ showSubmitButton: propTypes.bool.def(true),
+ // 确认按钮配置
+ submitButtonOptions: Object as PropType>,
+
+ // 自定义重置函数
+ resetFunc: Function as PropType<() => Promise>,
+ submitFunc: Function as PropType<() => Promise>,
+
+ // 以下为默认props
+ hideRequiredMark: propTypes.bool,
+
+ labelCol: Object as PropType>,
+
+ layout: propTypes.oneOf(['horizontal', 'vertical', 'inline']).def('horizontal'),
+ tableAction: {
+ type: Object as PropType,
+ },
+
+ wrapperCol: Object as PropType>,
+
+ colon: propTypes.bool,
+
+ labelAlign: propTypes.string,
+
+ rowProps: Object as PropType,
+};
diff --git a/src/components/Form/src/types/form.ts b/src/components/Form/src/types/form.ts
new file mode 100644
index 0000000..56f75e2
--- /dev/null
+++ b/src/components/Form/src/types/form.ts
@@ -0,0 +1,268 @@
+import type { NamePath, RuleObject } from 'ant-design-vue/lib/form/interface';
+import type { VNode, CSSProperties } from 'vue';
+import type { ButtonProps as AntdButtonProps } from '@/components/Button';
+import type { FormItem } from './formItem';
+import type { ColEx, ComponentType, ComponentProps } from './';
+import type { TableActionType } from '@/components/Table/src/types/table';
+import type { RowProps } from 'ant-design-vue/lib/grid/Row';
+
+export type FieldMapToTime = [string, [string, string], (string | [string, string])?][];
+
+export type Rule = RuleObject & {
+ trigger?: 'blur' | 'change' | ['change', 'blur'];
+};
+
+export interface RenderCallbackParams {
+ schema: FormSchemaInner;
+ values: Recordable;
+ model: Recordable;
+ field: string;
+}
+
+export interface ButtonProps extends AntdButtonProps {
+ text?: string;
+}
+
+export interface FormActionType {
+ submit: () => Promise;
+ setFieldsValue: (values: Recordable) => Promise;
+ resetFields: () => Promise;
+ getFieldsValue: () => Recordable;
+ clearValidate: (name?: string | string[]) => Promise;
+ updateSchema: (data: Partial | Partial[]) => Promise;
+ resetSchema: (data: Partial