Initial commit: ujcms-cp-v10.1.3

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
master
wanglu 2026-03-11 09:38:32 +08:00
commit 11cd939666
258 changed files with 45103 additions and 0 deletions

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
[*.{js,jsx,ts,tsx,vue,hbs}]
max_line_length = 180
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

13
.env Normal file
View File

@ -0,0 +1,13 @@
VITE_APP_TITLE=UJCMS后台管理
VITE_APP_NAME=UJCMS
VITE_PORT=5173
VITE_PUBLIC_PATH=./
VITE_PROXY_API=http://192.168.10.37:8080
VITE_PROXY_UPLOADS=http://192.168.10.37:8080
VITE_PROXY_TEMPLATES=http://192.168.10.37:8080
VITE_BASE_API=/api
VITE_BASE_UPLOADS=/uploads
VITE_BASE_TEMPLATES=/templates
VITE_I18N_LOCALE=zh-cn
VITE_I18N_FALLBACK_LOCALE=zh-cn
VITE_USE_MOCK=false

0
.env.development Normal file
View File

1
.env.production Normal file
View File

@ -0,0 +1 @@
VITE_BASE_API=../api

0
.env.staging Normal file
View File

4
.eslintignore Normal file
View File

@ -0,0 +1,4 @@
build/*.js
src/assets/*
public/*
dist/*

28
.eslintrc Normal file
View File

@ -0,0 +1,28 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true,
},
"extends": ["eslint:recommended", "plugin:vue/vue3-recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", "./.eslintrc-auto-import.json"],
"parser": "vue-eslint-parser",
"parserOptions": {
// "extraFileExtensions": [".vue"],
// "project": ["./tsconfig.json"],
"ecmaVersion": "latest",
"sourceType": "module",
"parser": "@typescript-eslint/parser",
},
"plugins": ["vue", "@typescript-eslint"],
"rules": {
// "no-param-reassign": ["error", { "props": true, "ignorePropertyModificationsFor": ["state"] }],
// "import/prefer-default-export": "off",
// "@typescript-eslint/explicit-module-boundary-types": "off",
// // 避免使用Q_作为查询参数时报错
// "@typescript-eslint/camelcase": "off",
// 允许使用 any 类型
"@typescript-eslint/no-explicit-any": "off",
},
}

View File

@ -0,0 +1,6 @@
{
"globals": {
"ElMessageBox": true,
"ElMessage": true
}
}

46
.gitattributes vendored Normal file
View File

@ -0,0 +1,46 @@
###############################
# Git Line Endings #
###############################
# Set default behaviour to automatically normalize line endings.
# * text=auto
# 文本文件全部使用lf换行eslint prettier等工具保持一致。
* text=auto eol=lf
# Force batch scripts to always use CRLF line endings so that if a repo is accessed
# in Windows via a file share from Linux, the scripts will work.
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf
# Force bash scripts to always use LF line endings so that if a repo is accessed
# in Unix via a file share from Windows, the scripts will work.
*.sh text eol=lf
###############################
# Git Large File System (LFS) #
###############################
# # Archives
# *.7z filter=lfs diff=lfs merge=lfs -text
# *.br filter=lfs diff=lfs merge=lfs -text
# *.gz filter=lfs diff=lfs merge=lfs -text
# *.tar filter=lfs diff=lfs merge=lfs -text
# *.zip filter=lfs diff=lfs merge=lfs -text
# # Documents
# *.pdf filter=lfs diff=lfs merge=lfs -text
# # Images
# *.gif filter=lfs diff=lfs merge=lfs -text
# *.ico filter=lfs diff=lfs merge=lfs -text
# *.jpg filter=lfs diff=lfs merge=lfs -text
# *.pdf filter=lfs diff=lfs merge=lfs -text
# *.png filter=lfs diff=lfs merge=lfs -text
# *.psd filter=lfs diff=lfs merge=lfs -text
# *.webp filter=lfs diff=lfs merge=lfs -text
# # Fonts
# *.woff2 filter=lfs diff=lfs merge=lfs -text
# # Other
# *.exe filter=lfs diff=lfs merge=lfs -text

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
node_modules
.DS_Store
.history
dist
dist-ssr
*.local
# VSCode project files
.vscode/*
!.vscode/extensions.json

0
.husky/pre-commit Normal file
View File

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
.idea/misc.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ujcms-cp-v10.1.3.iml" filepath="$PROJECT_DIR$/.idea/ujcms-cp-v10.1.3.iml" />
</modules>
</component>
</project>

6
.idea/prettier.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
</component>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"editorconfig.editorconfig",
]
}

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2021-2024 南昌蓝智科技有限公司
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

86
README.md Normal file
View File

@ -0,0 +1,86 @@
# UJCMS-CP
UJCMS-CP是UJCMS的后台前端项目。使用 Vue 3、Vite、TypeScript、ElementPlus、TailwindCSS、VueRouter、VueI18n 开发。
需要启动`UJCMS`主项目才可以使用,不可单独运行(无法访问后端接口)。
如不需要修改`UJCMS`的后台界面,则不必启动此项目。`UJCMS`的`/src/main/webapp/cp`目录已包含本项目编译后的代码,直接运行`UJCMS`主项目即可。
## 搭建步骤
* 使用 vscode 开发工具。
* 安装 node 环境。Node 20.12+ 版本。
* 安装 pnpm。执行npm install -g pnpm
* 使用淘宝 npm 镜像。执行pnpm set registry https://registry.npmmirror.com/
* 安装依赖。执行pnpm install
* 启动程序。执行pnpm run dev
* 访问http://127.0.0.1:5173
* 用户名admin密码password
## 修改后台标识
* 修改`.env`文件中的`VITE_APP_TITLE=UJCMS后台管理`配置,可改变浏览器页签上的标题。
* 修改`.env`文件中的`VITE_APP_NAME=UJCMS`配置,可改变登录页、后台左侧导航等处的`UJCMS`标识。
* 替换`/public/favicon.png`图片,可改变浏览器标签页上显示的图标。
* 修改`/src/layout/components/AppSidebar/SidebarLogo.vue`文件中的`svg`图标可改变后台左侧导航处LOGO图标。
## 编译及部署
* 执行pnpm run build
* 编译后的程序在`/dist`目录。
* 将`/dist`目录里的文件拷贝至主项目UJCMS的`/src/main/webapp/cp`目录下(先将原目录下的文件删除)。
## 常见错误
编译时出现 `Javascript Heap out of memory` 错误,代表内存溢出。可以设置 `NODE_OPTIONS` 环境变量为 `--max-old-space-size=8192`
## 前后端分开部署
通常前端和后端程序部署到同一个应用即将前端程序复制到主项目UJCMS的`/cp`目录。以演示站点为例,后端接口地址为`https://demo.ujcms.com/api`,前端访问地址则为`https://demo.ujcms.com/cp/`。这样可以避免跨域问题,是最简单的部署方式。
如果需要将前后端部署到不同域名或端口,如后端接口地址为`http://www.example.com/api`,前端地址为`http://www.frontend.com`。由于前后端域名不同,前端直接访问后端接口会出现跨域错误。这时需要在前端服务器部署反向代理,解决跨域问题。以`nginx`为例:
```
# 代理 api 接口
location /api {
proxy_pass http://www.example.com;
}
# 代理上传文件
location /uploads {
proxy_pass http://www.example.com;
}
```
开发模式启动时,情况也类似,后端接口地址为`http://localhost:8080/api`,前端地址为`http://localhost:9520`。前后端端口不同,也属于跨域。但前端开发在状态启动时,会自动开启代理,相关配置在`vite.config.ts`文件中。类似以下代码:
```
proxy: {
'/api': {
target: env.VITE_PROXY,
changeOrigin: true,
},
'/uploads': {
target: env.VITE_PROXY,
changeOrigin: true,
},
},
```
## 菜单和角色权限配置
如果进行二次开发,需新增功能,可在`/src/router/index.ts`文件中配置菜单。
并可在`/src/data.ts`文件中配置权限,配置好的权限会在`角色管理 - 权限设置`中的`功能权限`中显示。
配置内容:
```
export function getPermsTreeData(): any[] {
const {
global: { t },
} = i18n;
const perms = [
...
]
}
```

11
auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const ElMessage: typeof import('element-plus/es')['ElMessage']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
}

101
components.d.ts vendored Normal file
View File

@ -0,0 +1,101 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AssignmentProps: typeof import('./src/components/bpmnjs/properties-panel/properties/AssignmentProps.vue')['default']
BaseUpload: typeof import('./src/components/Upload/BaseUpload.vue')['default']
BreadCrumb: typeof import('./src/components/BreadCrumb/index.vue')['default']
ColumnList: typeof import('./src/components/TableList/ColumnList.vue')['default']
ColumnSetting: typeof import('./src/components/TableList/ColumnSetting.vue')['default']
ConditionProps: typeof import('./src/components/bpmnjs/properties-panel/properties/ConditionProps.vue')['default']
DialogForm: typeof import('./src/components/DialogForm.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAside: typeof import('element-plus/es')['ElAside']
ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxButton: typeof import('element-plus/es')['ElCheckboxButton']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSlider: typeof import('element-plus/es')['ElSlider']
ElSpace: typeof import('element-plus/es')['ElSpace']
ElStep: typeof import('element-plus/es')['ElStep']
ElSteps: typeof import('element-plus/es')['ElSteps']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree']
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
ElUpload: typeof import('element-plus/es')['ElUpload']
FileListUpload: typeof import('./src/components/Upload/FileListUpload.vue')['default']
FlowablePropertiesPannel: typeof import('./src/components/bpmnjs/properties-panel/FlowablePropertiesPannel.vue')['default']
FormProps: typeof import('./src/components/bpmnjs/properties-panel/properties/FormProps.vue')['default']
ImageCropper: typeof import('./src/components/Upload/ImageCropper.vue')['default']
ImageListUpload: typeof import('./src/components/Upload/ImageListUpload.vue')['default']
ImageUpload: typeof import('./src/components/Upload/ImageUpload.vue')['default']
LabelTip: typeof import('./src/components/LabelTip.vue')['default']
ListenerProps: typeof import('./src/components/bpmnjs/properties-panel/properties/ListenerProps.vue')['default']
ListMove: typeof import('./src/components/ListMove.vue')['default']
MultiInstanceProps: typeof import('./src/components/bpmnjs/properties-panel/properties/MultiInstanceProps.vue')['default']
NormalProps: typeof import('./src/components/bpmnjs/properties-panel/properties/NormalProps.vue')['default']
QueryForm: typeof import('./src/components/QueryForm/QueryForm.vue')['default']
QueryInput: typeof import('./src/components/QueryForm/QueryInput.vue')['default']
QueryItem: typeof import('./src/components/QueryForm/QueryItem.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
TimerProps: typeof import('./src/components/bpmnjs/properties-panel/properties/TimerProps.vue')['default']
Tinymce: typeof import('./src/components/Tinymce/Tinymce.vue')['default']
TuiEditor: typeof import('./src/components/TuiEditor/TuiEditor.vue')['default']
UserSelect: typeof import('./src/components/user/UserSelect.vue')['default']
UserSelectMulti: typeof import('./src/components/user/UserSelectMulti.vue')['default']
}
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UJCMS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

27
mock/sample/sample.ts Normal file
View File

@ -0,0 +1,27 @@
import { MockMethod } from 'vite-plugin-mock';
export default [
{
url: '/sample',
method: 'get',
// response: ({ query, body }: any) => {
response: () => {
return {
code: 0,
message: 'ok',
data: {
total: 2,
list: [
{
id: 100,
title: 'Mock测试数据100',
},
{
id: 101,
title: 'Mock测试数据101',
},
],
},
};
},
},
] as MockMethod[];

11269
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

131
package.json Normal file
View File

@ -0,0 +1,131 @@
{
"name": "ujcms-cp",
"version": "10.1.3",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint src/**/*.{vue,ts,tsx} --fix",
"prettier": "prettier --write src/**/*.{json,js,ts,tsx,css,scss,vue,html,md}",
"lint-staged": "lint-staged",
"prepare": "husky",
"plop": "plop"
},
"dependencies": {
"@codemirror/lang-html": "^6.4.9",
"@element-plus/icons-vue": "^2.3.1",
"@toast-ui/chart": "^4.6.1",
"@toast-ui/editor": "^3.2.2",
"@toast-ui/editor-plugin-chart": "^3.0.1",
"@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0",
"@toast-ui/editor-plugin-table-merged-cell": "^3.1.0",
"@toast-ui/editor-plugin-uml": "^3.0.1",
"@vueuse/components": "^10.11.1",
"@vueuse/core": "^10.11.1",
"axios": "^1.7.7",
"bpmn-js": "^17.11.1",
"codemirror": "^6.0.1",
"core-js": "^3.39.0",
"cropperjs": "^1.6.2",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"diagram-js": "^14.11.3",
"diagram-js-direct-editing": "^2.1.2",
"domutils": "^3.1.0",
"echarts": "^5.5.1",
"element-plus": "~2.8.8",
"entities": "^4.5.0",
"file-saver": "^2.0.5",
"htmlparser2": "^9.1.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"min-dash": "^4.2.2",
"nprogress": "^0.2.0",
"path-to-regexp": "^6.3.0",
"pinia": "^2.2.6",
"pinia-plugin-persistedstate": "^3.2.3",
"prismjs": "^1.29.0",
"sm-crypto": "^0.3.13",
"sortablejs": "1.14.0",
"tinymce": "~5.9.2",
"vue": "^3.5.13",
"vue-codemirror": "^6.1.1",
"vue-i18n": "^9.14.1",
"vue-router": "^4.4.5",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@types/crypto-js": "^4.2.2",
"@types/file-saver": "^2.0.7",
"@types/js-cookie": "^3.0.6",
"@types/lodash": "^4.17.13",
"@types/node": "^20.17.6",
"@types/nprogress": "^0.2.3",
"@types/prismjs": "^1.26.5",
"@types/sm-crypto": "^0.3.4",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-legacy": "^5.4.3",
"@vitejs/plugin-vue": "^5.2.0",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.31.0",
"husky": "^9.1.6",
"lint-staged": "^15.2.10",
"mockjs": "^1.1.0",
"plop": "^4.0.1",
"postcss": "^8.4.49",
"postcss-import": "^16.1.0",
"prettier": "^3.3.3",
"sass": "^1.81.0",
"tailwindcss": "^3.4.15",
"terser": "^5.36.0",
"typescript": "^5.6.3",
"unplugin-auto-import": "^0.19.0",
"unplugin-vue-components": "^0.27.5",
"vite": "^5.4.17",
"vite-plugin-mock": "^3.0.2",
"vue-tsc": "^2.1.10"
},
"prettier": {
"printWidth": 180,
"singleQuote": true,
"trailingComma": "all",
"arrowParens": "always"
},
"lint-staged": {
"*.{js,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.vue": [
"eslint --fix",
"prettier --write"
],
"{!(package)*.json,.!(browserslist)*rc}": [
"prettier --write--parser json"
],
"package.json": [
"prettier --write"
],
"*.{scss,html}": [
"prettier --write"
],
"*.md": [
"prettier --write"
]
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
],
"engines": {
"node": ">=20.12"
}
}

8
plop-templates/api.hbs Normal file
View File

@ -0,0 +1,8 @@
export const query{{pascalCase name}}{{pascalCase type}} = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/{{kebabCase sub}}/{{kebabCase name}}', { params })).data;
export const query{{pascalCase name}} = async (id: string): Promise<any> => (await axios.get(`/backend/{{kebabCase sub}}/{{kebabCase name}}/${id}`)).data;
export const create{{pascalCase name}} = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/{{kebabCase sub}}/{{kebabCase name}}', data)).data;
export const update{{pascalCase name}} = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/{{kebabCase sub}}/{{kebabCase name}}?_method=put', data)).data;
{{#if isList}}
export const update{{pascalCase name}}Order = async (fromId: string, toId: string): Promise<any> => (await axios.post('/backend/{{kebabCase sub}}/{{kebabCase name}}/update-order', { fromId, toId })).data;
{{/if}}
export const delete{{pascalCase name}} = async (data: string[]): Promise<any> => (await axios.post('/backend/{{kebabCase sub}}/{{kebabCase name}}?_method=delete', data)).data;

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import { ref } from 'vue';
import { query{{pascalCase name}}, create{{pascalCase name}}, update{{pascalCase name}}, delete{{pascalCase name}} } from '@/api/{{kebabCase path}}';
import DialogForm from '@/components/DialogForm.vue';
import LabelTip from '@/components/LabelTip.vue';
defineOptions({
name: '{{pascalCase name}}Form',
});
const visible = defineModel<boolean>({ default: false });
defineProps<{ beanId?: string; beanIds: string[] }>();
defineEmits({ finished: null });
const focus = ref<any>();
const values = ref<any>({});
</script>
<template>
<dialog-form
v-model="visible"
v-model:values="values"
:name="$t('menu.{{camelCase path}}.{{camelCase name}}')"
:query-bean="query{{pascalCase name}}"
:create-bean="create{{pascalCase name}}"
:update-bean="update{{pascalCase name}}"
:delete-bean="delete{{pascalCase name}}"
:bean-id="beanId"
:bean-ids="beanIds"
:focus="focus"
:init-values="() => ({})"
:to-values="(bean) => ({ ...bean })"
perms="{{camelCase name}}"
@finished="() => $emit('finished')"
>
<template #default="{}">
<el-form-item prop="name" :rules="{ required: true, message: () => $t('v.required') }">
<template #label><label-tip message="{{camelCase name}}.name" /></template>
<el-input ref="focus" v-model="values.name" maxlength="50"></el-input>
</el-form-item>
</template>
</dialog-form>
</template>

View File

@ -0,0 +1,147 @@
<script setup lang="ts">
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
import { Plus, Delete, Grid } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
import Sortable from 'sortablejs';
import { perm } from '@/stores/useCurrentUser';
import { toParams, resetParams } from '@/utils/common';
import { delete{{pascalCase name}}, query{{pascalCase name}}List, update{{pascalCase name}}Order } from '@/api/{{kebabCase path}}';
import { ColumnList, ColumnSetting } from '@/components/TableList';
import { QueryForm, QueryItem } from '@/components/QueryForm';
import {{pascalCase name}}Form from './{{pascalCase name}}Form.vue';
defineOptions({
name: '{{pascalCase name}}List',
});
const { t } = useI18n();
const params = ref<any>({});
const sort = ref<any>();
const table = ref<any>();
const data = ref<any[]>([]);
const selection = ref<any[]>([]);
const loading = ref<boolean>(false);
const formVisible = ref<boolean>(false);
const beanId = ref<string>();
const beanIds = computed(() => data.value.map((row) => row.id));
const isSorted = ref<boolean>(false);
const fetchData = async () => {
loading.value = true;
try {
data.value = await query{{pascalCase name}}List({ ...toParams(params.value), Q_OrderBy: sort.value });
isSorted.value = sort.value !== undefined;
} finally {
loading.value = false;
}
};
let sortable;
const initDragTable = () => {
const tbody = document.querySelector('#dataTable .el-table__body-wrapper tbody');
sortable = Sortable.create(tbody, {
handle: '.drag-handle',
onEnd: async function (event: any) {
const { oldIndex, newIndex } = event;
if (oldIndex !== newIndex) {
await update{{pascalCase name}}Order(data.value[oldIndex].id, data.value[newIndex].id);
data.value.splice(newIndex, 0, data.value.splice(oldIndex, 1)[0]);
ElMessage.success(t('success'));
}
},
});
};
onMounted(() => {
fetchData();
initDragTable();
});
onBeforeUnmount(() => {
if (sortable !== undefined) {
sortable.destroy();
}
});
const handleSort = ({ column, prop, order }: { column: any; prop: string; order: string }) => {
if (prop && order) {
sort.value = (column.sortBy ?? prop) + (order === 'descending' ? '_desc' : '');
} else {
sort.value = undefined;
}
fetchData();
};
const handleSearch = () => fetchData();
const handleReset = () => {
table.value.clearSort();
resetParams(params.value);
sort.value = undefined;
fetchData();
};
const handleAdd = () => {
beanId.value = undefined;
formVisible.value = true;
};
const handleEdit = (id: string) => {
beanId.value = id;
formVisible.value = true;
};
const handleDelete = async (ids: string[]) => {
await delete{{pascalCase name}}(ids);
fetchData();
ElMessage.success(t('success'));
};
</script>
<template>
<div>
<div class="mb-3">
<query-form :params="params" @search="handleSearch" @reset="() => handleReset()">
<query-item :label="$t('{{camelCase name}}.name')" name="Q_Contains_name"></query-item>
</query-form>
</div>
<div class="space-x-2">
<el-button type="primary" :disabled="perm('{{camelCase name}}:create')" :icon="Plus" @click="() => handleAdd()">\{{ $t('add') }}</el-button>
<el-popconfirm :title="$t('confirmDelete')" @confirm="() => handleDelete(selection.map((row) => row.id))">
<template #reference>
<el-button :disabled="selection.length <= 0 || perm('{{camelCase name}}:delete')" :icon="Delete">\{{ $t('delete') }}</el-button>
</template>
</el-popconfirm>
<column-setting name="{{camelCase name}}" />
</div>
<div class="mt-3 app-block">
<el-table
id="dataTable"
ref="table"
v-loading="loading"
row-key="id"
:data
@selection-change="(rows) => (selection = rows)"
@row-dblclick="(row) => handleEdit(row.id)"
@sort-change="handleSort"
>
<column-list name="{{camelCase name}}">
<el-table-column type="selection" width="45"></el-table-column>
<el-table-column width="42">
<el-icon
class="text-lg align-middle text-gray-secondary"
:class="isSorted || perm('{{camelCase name}}:update') ? ['cursor-not-allowed', 'text-gray-disabled'] : ['cursor-move', 'text-gray-regular', 'drag-handle']"
disalbed
>
<Grid />
</el-icon>
</el-table-column>
<el-table-column property="id" label="ID" width="180" sortable="custom"></el-table-column>
<el-table-column property="name" :label="$t('{{camelCase name}}.name')" sortable="custom" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('table.action')">
<template #default="{ row }">
<el-button type="primary" :disabled="perm('{{camelCase name}}:update')" size="small" link @click="() => handleEdit(row.id)">\{{ $t('edit') }}</el-button>
<el-popconfirm :title="$t('confirmDelete')" @confirm="() => handleDelete([row.id])">
<template #reference>
<el-button type="primary" :disabled="perm('{{camelCase name}}:delete')" size="small" link>\{{ $t('delete') }}</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</column-list>
</el-table>
</div>
<{{kebabCase name}}-form v-model="formVisible" :bean-id="beanId" :bean-ids="beanIds" @finished="() => fetchData()" />
</div>
</template>

View File

@ -0,0 +1,132 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { Plus, Delete } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
import { perm } from '@/stores/useCurrentUser';
import { pageSizes, pageLayout, toParams, resetParams } from '@/utils/common';
import { delete{{pascalCase name}}, query{{pascalCase name}}Page } from '@/api/{{kebabCase path}}';
import { ColumnList, ColumnSetting } from '@/components/TableList';
import { QueryForm, QueryItem } from '@/components/QueryForm';
import {{pascalCase name}}Form from './{{pascalCase name}}Form.vue';
defineOptions({
name: '{{pascalCase name}}List',
});
const { t } = useI18n();
const params = ref<any>({});
const sort = ref<any>();
const currentPage = ref<number>(1);
const pageSize = ref<number>(10);
const total = ref<number>(0);
const table = ref<any>();
const data = ref<any[]>([]);
const selection = ref<any[]>([]);
const loading = ref<boolean>(false);
const formVisible = ref<boolean>(false);
const beanId = ref<string>();
const beanIds = computed(() => data.value.map((row) => row.id));
const fetchData = async () => {
loading.value = true;
try {
const {
content,
page: { totalElements },
} = await query{{pascalCase name}}Page({ ...toParams(params.value), Q_OrderBy: sort.value, page: currentPage.value, pageSize: pageSize.value });
data.value = content;
total.value = Number(totalElements);
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchData();
});
const handleSort = ({ column, prop, order }: { column: any; prop: string; order: string }) => {
if (prop && order) {
sort.value = (column.sortBy ?? prop) + (order === 'descending' ? '_desc' : '');
} else {
sort.value = undefined;
}
fetchData();
};
const handleSearch = () => fetchData();
const handleReset = () => {
table.value.clearSort();
resetParams(params.value);
sort.value = undefined;
fetchData();
};
const handleAdd = () => {
beanId.value = undefined;
formVisible.value = true;
};
const handleEdit = (id: string) => {
beanId.value = id;
formVisible.value = true;
};
const handleDelete = async (ids: string[]) => {
await delete{{pascalCase name}}(ids);
fetchData();
ElMessage.success(t('success'));
};
</script>
<template>
<div>
<div class="mb-3">
<query-form :params="params" @search="handleSearch" @reset="() => handleReset()">
<query-item :label="$t('{{camelCase name}}.name')" name="Q_Contains_name"></query-item>
</query-form>
</div>
<div>
<el-button type="primary" :disabled="perm('{{camelCase name}}:create')" :icon="Plus" @click="() => handleAdd()">\{{ $t('add') }}</el-button>
<el-popconfirm :title="$t('confirmDelete')" @confirm="() => handleDelete(selection.map((row) => row.id))">
<template #reference>
<el-button :disabled="selection.length <= 0 || perm('{{camelCase name}}:delete')" :icon="Delete">\{{ $t('delete') }}</el-button>
</template>
</el-popconfirm>
<column-setting name="{{camelCase name}}" class="ml-2" />
</div>
<div class="mt-3 app-block">
<el-table
ref="table"
v-loading="loading"
:data="data"
@selection-change="(rows) => (selection = rows)"
@row-dblclick="(row) => handleEdit(row.id)"
@sort-change="handleSort"
>
<column-list name="{{camelCase name}}">
<el-table-column type="selection" width="45"></el-table-column>
<el-table-column property="id" label="ID" width="180" sortable="custom"></el-table-column>
<el-table-column property="name" :label="$t('{{camelCase name}}.name')" sortable="custom" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('table.action')">
<template #default="{row}">
<el-button type="primary" :disabled="perm('{{camelCase name}}:update')" size="small" link @click="() => handleEdit(row.id)">\{{ $t('edit') }}</el-button>
<el-popconfirm :title="$t('confirmDelete')" @confirm="() => handleDelete([row.id])">
<template #reference>
<el-button type="primary" :disabled="perm('{{camelCase name}}:delete')" size="small" link>\{{ $t('delete') }}</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</column-list>
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total
:page-sizes
:layout="pageLayout"
class="justify-end px-3 py-2"
size="small"
background
@size-change="() => fetchData()"
@current-change="() => fetchData()"
></el-pagination>
</div>
<{{kebabCase name}}-form v-model="formVisible" :bean-id="beanId" :bean-ids="beanIds" @finished="fetchData" />
</div>
</template>

51
plopfile.js Normal file
View File

@ -0,0 +1,51 @@
// pnpm run plop core user org page
// pnpm run plop <子系统> <分类> <模块> <page|list>
/* eslint-disable func-names */
export default function (plop) {
// controller generator
plop.setGenerator('view', {
description: 'application views',
prompts: [
{
type: 'input',
name: 'sub',
message: 'sub:',
},
{
type: 'input',
name: 'path',
message: 'path:',
},
{
type: 'input',
name: 'name',
message: 'name:',
},
{
type: 'input',
name: 'type',
message: 'type:',
},
],
actions: (data) => {
const actions = [];
actions.push({
type: 'add',
path: 'src/views/{{kebabCase path}}/{{pascalCase name}}Form.vue',
templateFile: 'plop-templates/view_form.hbs',
});
actions.push({
type: 'add',
path: 'src/views/{{kebabCase path}}/{{pascalCase name}}List.vue',
templateFile: `plop-templates/view_${data.type}.hbs`,
});
actions.push({
type: 'append',
path: 'src/api/{{kebabCase path}}.ts',
templateFile: 'plop-templates/api.hbs',
data: { isList: data.type === 'list' },
});
return actions;
},
});
}

7158
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

3
postcss.config.js Normal file
View File

@ -0,0 +1,3 @@
export default {
plugins: { 'postcss-import': {}, tailwindcss: {}, autoprefixer: {} },
};

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

View File

@ -0,0 +1,462 @@
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 character...": "\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",
"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",
"Alternative description": "\u66ff\u4ee3\u63cf\u8ff0",
"Accessibility": "\u8f85\u52a9\u529f\u80fd",
"Image is decorative": "\u56fe\u50cf\u662f\u88c5\u9970\u6027\u7684",
"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",
"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",
"Open link": "\u6253\u5f00\u94fe\u63a5",
"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",
"The URL you entered seems to be an external link. Do you want to add the required https:\/\/ prefix?": "\u60a8\u8f93\u5165\u7684 URL \u4f3c\u4e4e\u662f\u4e00\u4e2a\u5916\u90e8\u94fe\u63a5\u3002\u60a8\u60f3\u6dfb\u52a0\u6240\u9700\u7684 https:\/\/ \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\u548c\u66ff\u6362",
"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",
"Find in selection": "\u5728\u9009\u533a\u4e2d\u67e5\u627e",
"Spellcheck": "\u62fc\u5199\u68c0\u67e5",
"Spellcheck Language": "\u62fc\u5199\u68c0\u67e5\u8bed\u8a00",
"No misspellings found.": "\u6ca1\u6709\u53d1\u73b0\u62fc\u5199\u9519\u8bef",
"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",
"Caption": "\u6807\u9898",
"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",
"Count": "\u8ba1\u6570",
"Document": "\u6587\u6863",
"Selection": "\u9009\u62e9",
"Words": "\u5355\u8bcd",
"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)",
"{0} characters": "{0} \u4e2a\u5b57\u7b26",
"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",
"Light Green": "\u6d45\u7eff\u8272",
"Light Yellow": "\u6d45\u9ec4\u8272",
"Light Red": "\u6d45\u7ea2\u8272",
"Light Purple": "\u6d45\u7d2b\u8272",
"Light Blue": "\u6d45\u84dd\u8272",
"Dark Purple": "\u6df1\u7d2b\u8272",
"Dark Blue": "\u6df1\u84dd\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",
"Font": "\u5b57\u4f53",
"Size": "\u5b57\u53f7",
"More...": "\u66f4\u591a...",
"Select...": "\u9009\u62e9...",
"Preferences": "\u9996\u9009\u9879",
"Yes": "\u662f",
"No": "\u5426",
"Keyboard Navigation": "\u952e\u76d8\u6307\u5f15",
"Version": "\u7248\u672c",
"Code view": "\u4ee3\u7801\u89c6\u56fe",
"Open popup menu for split buttons": "\u6253\u5f00\u5f39\u51fa\u5f0f\u83dc\u5355\uff0c\u7528\u4e8e\u62c6\u5206\u6309\u94ae",
"List Properties": "\u5217\u8868\u5c5e\u6027",
"List properties...": "\u6807\u9898\u5b57\u4f53\u5c5e\u6027",
"Start list at number": "\u4ee5\u6570\u5b57\u5f00\u59cb\u5217\u8868",
"Line height": "\u884c\u9ad8",
"comments": "\u5907\u6ce8",
"Format Painter": "\u683c\u5f0f\u5237",
"Insert\/edit iframe": "\u63d2\u5165\/\u7f16\u8f91\u6846\u67b6",
"Capitalization": "\u5927\u5199",
"lowercase": "\u5c0f\u5199",
"UPPERCASE": "\u5927\u5199",
"Title Case": "\u9996\u5b57\u6bcd\u5927\u5199",
"permanent pen": "\u8bb0\u53f7\u7b14",
"Permanent Pen Properties": "\u6c38\u4e45\u7b14\u5c5e\u6027",
"Permanent pen properties...": "\u6c38\u4e45\u7b14\u5c5e\u6027...",
"case change": "\u6848\u4f8b\u66f4\u6539",
"page embed": "\u9875\u9762\u5d4c\u5165",
"Advanced sort...": "\u9ad8\u7ea7\u6392\u5e8f...",
"Advanced Sort": "\u9ad8\u7ea7\u6392\u5e8f",
"Sort table by column ascending": "\u6309\u5217\u5347\u5e8f\u8868",
"Sort table by column descending": "\u6309\u5217\u964d\u5e8f\u8868",
"Sort": "\u6392\u5e8f",
"Order": "\u6392\u5e8f",
"Sort by": "\u6392\u5e8f\u65b9\u5f0f",
"Ascending": "\u5347\u5e8f",
"Descending": "\u964d\u5e8f",
"Column {0}": "\u5217{0}",
"Row {0}": "\u884c{0}",
"Spellcheck...": "\u62fc\u5199\u68c0\u67e5...",
"Misspelled word": "\u62fc\u5199\u9519\u8bef\u7684\u5355\u8bcd",
"Suggestions": "\u5efa\u8bae",
"Change": "\u66f4\u6539",
"Finding word suggestions": "\u67e5\u627e\u5355\u8bcd\u5efa\u8bae",
"Success": "\u6210\u529f",
"Repair": "\u4fee\u590d",
"Issue {0} of {1}": "\u5171\u8ba1{1}\u95ee\u9898{0}",
"Images must be marked as decorative or have an alternative text description": "\u56fe\u50cf\u5fc5\u987b\u6807\u8bb0\u4e3a\u88c5\u9970\u6027\u6216\u5177\u6709\u66ff\u4ee3\u6587\u672c\u63cf\u8ff0",
"Images must have an alternative text description. Decorative images are not allowed.": "\u56fe\u50cf\u5fc5\u987b\u5177\u6709\u66ff\u4ee3\u6587\u672c\u63cf\u8ff0\u3002\u4e0d\u5141\u8bb8\u4f7f\u7528\u88c5\u9970\u56fe\u50cf\u3002",
"Or provide alternative text:": "\u6216\u63d0\u4f9b\u5907\u9009\u6587\u672c\uff1a",
"Make image decorative:": "\u4f7f\u56fe\u50cf\u88c5\u9970\uff1a",
"ID attribute must be unique": "ID \u5c5e\u6027\u5fc5\u987b\u662f\u552f\u4e00\u7684",
"Make ID unique": "\u4f7f ID \u72ec\u4e00\u65e0\u4e8c",
"Keep this ID and remove all others": "\u4fdd\u7559\u6b64 ID \u5e76\u5220\u9664\u6240\u6709\u5176\u4ed6",
"Remove this ID": "\u5220\u9664\u6b64 ID",
"Remove all IDs": "\u6e05\u9664\u5168\u90e8IDs",
"Checklist": "\u6e05\u5355",
"Anchor": "\u951a\u70b9",
"Special character": "\u7279\u6b8a\u7b26\u53f7",
"Code sample": "\u4ee3\u7801\u793a\u4f8b",
"Color": "\u989c\u8272",
"Document properties": "\u6587\u6863\u5c5e\u6027",
"Image description": "\u56fe\u7247\u63cf\u8ff0",
"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",
"Insert template": "\u63d2\u5165\u6a21\u677f"
});

View File

@ -0,0 +1,419 @@
tinymce.addI18n('zh_TW',{
"Redo": "\u91cd\u505a",
"Undo": "\u64a4\u92b7",
"Cut": "\u526a\u4e0b",
"Copy": "\u8907\u88fd",
"Paste": "\u8cbc\u4e0a",
"Select all": "\u5168\u9078",
"New document": "\u65b0\u6587\u4ef6",
"Ok": "\u78ba\u5b9a",
"Cancel": "\u53d6\u6d88",
"Visual aids": "\u5c0f\u5e6b\u624b",
"Bold": "\u7c97\u9ad4",
"Italic": "\u659c\u9ad4",
"Underline": "\u4e0b\u5283\u7dda",
"Strikethrough": "\u522a\u9664\u7dda",
"Superscript": "\u4e0a\u6a19",
"Subscript": "\u4e0b\u6a19",
"Clear formatting": "\u6e05\u9664\u683c\u5f0f",
"Align left": "\u5de6\u908a\u5c0d\u9f4a",
"Align center": "\u4e2d\u9593\u5c0d\u9f4a",
"Align right": "\u53f3\u908a\u5c0d\u9f4a",
"Justify": "\u5de6\u53f3\u5c0d\u9f4a",
"Bullet list": "\u9805\u76ee\u6e05\u55ae",
"Numbered list": "\u6578\u5b57\u6e05\u55ae",
"Decrease indent": "\u6e1b\u5c11\u7e2e\u6392",
"Increase indent": "\u589e\u52a0\u7e2e\u6392",
"Close": "\u95dc\u9589",
"Formats": "\u683c\u5f0f",
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "\u60a8\u7684\u700f\u89bd\u5668\u4e0d\u652f\u63f4\u5b58\u53d6\u526a\u8cbc\u7c3f\uff0c\u53ef\u4ee5\u4f7f\u7528\u5feb\u901f\u9375 Ctrl + X\/C\/V \u4ee3\u66ff\u526a\u4e0b\u3001\u8907\u88fd\u8207\u8cbc\u4e0a\u3002",
"Headers": "\u6a19\u984c",
"Header 1": "\u6a19\u984c 1",
"Header 2": "\u6a19\u984c 2",
"Header 3": "\u6a19\u984c 3",
"Header 4": "\u6a19\u984c 4",
"Header 5": "\u6a19\u984c 5",
"Header 6": "\u6a19\u984c 6",
"Headings": "\u6a19\u984c",
"Heading 1": "\u6a19\u984c1",
"Heading 2": "\u6a19\u984c2",
"Heading 3": "\u6a19\u984c3",
"Heading 4": "\u6a19\u984c4",
"Heading 5": "\u6a19\u984c5",
"Heading 6": "\u6a19\u984c6",
"Preformatted": "\u9810\u5148\u683c\u5f0f\u5316\u7684",
"Div": "Div",
"Pre": "Pre",
"Code": "\u4ee3\u78bc",
"Paragraph": "\u6bb5\u843d",
"Blockquote": "\u5f15\u6587\u5340\u584a",
"Inline": "\u5167\u806f",
"Blocks": "\u57fa\u584a",
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\u76ee\u524d\u5c07\u4ee5\u7d14\u6587\u5b57\u7684\u6a21\u5f0f\u8cbc\u4e0a\uff0c\u60a8\u53ef\u4ee5\u518d\u9ede\u9078\u4e00\u6b21\u53d6\u6d88\u3002",
"Fonts": "\u5b57\u578b",
"Font Sizes": "\u5b57\u578b\u5927\u5c0f",
"Class": "\u985e\u578b",
"Browse for an image": "\u5f9e\u5716\u7247\u4e2d\u700f\u89bd",
"OR": "\u6216",
"Drop an image here": "\u62d6\u66f3\u5716\u7247\u81f3\u6b64",
"Upload": "\u4e0a\u50b3",
"Block": "\u5340\u584a",
"Align": "\u5c0d\u9f4a",
"Default": "\u9810\u8a2d",
"Circle": "\u7a7a\u5fc3\u5713",
"Disc": "\u5be6\u5fc3\u5713",
"Square": "\u6b63\u65b9\u5f62",
"Lower Alpha": "\u5c0f\u5beb\u82f1\u6587\u5b57\u6bcd",
"Lower Greek": "\u5e0c\u81d8\u5b57\u6bcd",
"Lower Roman": "\u5c0f\u5beb\u7f85\u99ac\u6578\u5b57",
"Upper Alpha": "\u5927\u5beb\u82f1\u6587\u5b57\u6bcd",
"Upper Roman": "\u5927\u5beb\u7f85\u99ac\u6578\u5b57",
"Anchor...": "\u9328\u9ede...",
"Name": "\u540d\u7a31",
"Id": "Id",
"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "Id\u61c9\u4ee5\u5b57\u6bcd\u958b\u982d\uff0c\u5f8c\u9762\u63a5\u8457\u5b57\u6bcd\uff0c\u6578\u5b57\uff0c\u7834\u6298\u865f\uff0c\u9ede\u6578\uff0c\u5192\u865f\u6216\u4e0b\u5283\u7dda\u3002",
"You have unsaved changes are you sure you want to navigate away?": "\u7de8\u8f2f\u5c1a\u672a\u88ab\u5132\u5b58\uff0c\u4f60\u78ba\u5b9a\u8981\u96e2\u958b\uff1f",
"Restore last draft": "\u8f09\u5165\u4e0a\u4e00\u6b21\u7de8\u8f2f\u7684\u8349\u7a3f",
"Special character...": "\u7279\u6b8a\u5b57\u5143......",
"Source code": "\u539f\u59cb\u78bc",
"Insert\/Edit code sample": "\u63d2\u5165\/\u7de8\u8f2f \u7a0b\u5f0f\u78bc\u7bc4\u4f8b",
"Language": "\u8a9e\u8a00",
"Code sample...": "\u7a0b\u5f0f\u78bc\u7bc4\u4f8b...",
"Color Picker": "\u9078\u8272\u5668",
"R": "\u7d05",
"G": "\u7da0",
"B": "\u85cd",
"Left to right": "\u5f9e\u5de6\u5230\u53f3",
"Right to left": "\u5f9e\u53f3\u5230\u5de6",
"Emoticons...": "\u8868\u60c5\u7b26\u865f\u2026",
"Metadata and Document Properties": "\u5f8c\u8a2d\u8cc7\u6599\u8207\u6587\u4ef6\u5c6c\u6027",
"Title": "\u6a19\u984c",
"Keywords": "\u95dc\u9375\u5b57",
"Description": "\u63cf\u8ff0",
"Robots": "\u6a5f\u5668\u4eba",
"Author": "\u4f5c\u8005",
"Encoding": "\u7de8\u78bc",
"Fullscreen": "\u5168\u87a2\u5e55",
"Action": "\u52d5\u4f5c",
"Shortcut": "\u5feb\u901f\u9375",
"Help": "\u5e6b\u52a9",
"Address": "\u5730\u5740",
"Focus to menubar": "\u8df3\u81f3\u9078\u55ae\u5217",
"Focus to toolbar": "\u8df3\u81f3\u5de5\u5177\u5217",
"Focus to element path": "\u8df3\u81f3HTML\u5143\u7d20\u5217",
"Focus to contextual toolbar": "\u8df3\u81f3\u5feb\u6377\u9078\u55ae",
"Insert link (if link plugin activated)": "\u65b0\u589e\u6377\u5f91 (\u6377\u5f91\u5916\u639b\u555f\u7528\u6642)",
"Save (if save plugin activated)": "\u5132\u5b58 (\u5132\u5b58\u5916\u639b\u555f\u7528\u6642)",
"Find (if searchreplace plugin activated)": "\u5c0b\u627e (\u5c0b\u627e\u53d6\u4ee3\u5916\u639b\u555f\u7528\u6642)",
"Plugins installed ({0}):": "({0}) \u500b\u5916\u639b\u5df2\u5b89\u88dd\uff1a",
"Premium plugins:": "\u52a0\u503c\u5916\u639b\uff1a",
"Learn more...": "\u4e86\u89e3\u66f4\u591a...",
"You are using {0}": "\u60a8\u6b63\u5728\u4f7f\u7528 {0}",
"Plugins": "\u5916\u639b",
"Handy Shortcuts": "\u5feb\u901f\u9375",
"Horizontal line": "\u6c34\u5e73\u7dda",
"Insert\/edit image": "\u63d2\u5165\/\u7de8\u8f2f \u5716\u7247",
"Image description": "\u5716\u7247\u63cf\u8ff0",
"Source": "\u5716\u7247\u7db2\u5740",
"Dimensions": "\u5c3a\u5bf8",
"Constrain proportions": "\u7b49\u6bd4\u4f8b\u7e2e\u653e",
"General": "\u4e00\u822c",
"Advanced": "\u9032\u968e",
"Style": "\u6a23\u5f0f",
"Vertical space": "\u9ad8\u5ea6",
"Horizontal space": "\u5bec\u5ea6",
"Border": "\u908a\u6846",
"Insert image": "\u63d2\u5165\u5716\u7247",
"Image...": "\u5716\u7247......",
"Image list": "\u5716\u7247\u6e05\u55ae",
"Rotate counterclockwise": "\u9006\u6642\u91dd\u65cb\u8f49",
"Rotate clockwise": "\u9806\u6642\u91dd\u65cb\u8f49",
"Flip vertically": "\u5782\u76f4\u7ffb\u8f49",
"Flip horizontally": "\u6c34\u5e73\u7ffb\u8f49",
"Edit image": "\u7de8\u8f2f\u5716\u7247",
"Image options": "\u5716\u7247\u9078\u9805",
"Zoom in": "\u653e\u5927",
"Zoom out": "\u7e2e\u5c0f",
"Crop": "\u88c1\u526a",
"Resize": "\u8abf\u6574\u5927\u5c0f",
"Orientation": "\u65b9\u5411",
"Brightness": "\u4eae\u5ea6",
"Sharpen": "\u92b3\u5316",
"Contrast": "\u5c0d\u6bd4",
"Color levels": "\u984f\u8272\u5c64\u6b21",
"Gamma": "\u4f3d\u99ac\u503c",
"Invert": "\u53cd\u8f49",
"Apply": "\u61c9\u7528",
"Back": "\u5f8c\u9000",
"Insert date\/time": "\u63d2\u5165 \u65e5\u671f\/\u6642\u9593",
"Date\/time": "\u65e5\u671f\/\u6642\u9593",
"Insert\/Edit Link": "\u63d2\u5165\/\u7de8\u8f2f\u9023\u7d50",
"Insert\/edit link": "\u63d2\u5165\/\u7de8\u8f2f\u9023\u7d50",
"Text to display": "\u986f\u793a\u6587\u5b57",
"Url": "\u7db2\u5740",
"Open link in...": "\u958b\u555f\u9023\u7d50\u65bc...",
"Current window": "\u76ee\u524d\u8996\u7a97",
"None": "\u7121",
"New window": "\u53e6\u958b\u8996\u7a97",
"Remove link": "\u79fb\u9664\u9023\u7d50",
"Anchors": "\u52a0\u5165\u9328\u9ede",
"Link...": "\u9023\u7d50...",
"Paste or type a link": "\u8cbc\u4e0a\u6216\u8f38\u5165\u9023\u7d50",
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\u4f60\u6240\u586b\u5beb\u7684URL\u70ba\u96fb\u5b50\u90f5\u4ef6\uff0c\u9700\u8981\u52a0\u4e0amailto:\u524d\u7db4\u55ce\uff1f",
"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "\u4f60\u6240\u586b\u5beb\u7684URL\u5c6c\u65bc\u5916\u90e8\u93c8\u63a5\uff0c\u9700\u8981\u52a0\u4e0ahttp:\/\/:\u524d\u7db4\u55ce\uff1f",
"Link list": "\u9023\u7d50\u6e05\u55ae",
"Insert video": "\u63d2\u5165\u5f71\u97f3",
"Insert\/edit video": "\u63d2\u4ef6\/\u7de8\u8f2f \u5f71\u97f3",
"Insert\/edit media": "\u63d2\u5165\/\u7de8\u8f2f \u5a92\u9ad4",
"Alternative source": "\u66ff\u4ee3\u5f71\u97f3",
"Alternative source URL": "\u66ff\u4ee3\u4f86\u6e90URL",
"Media poster (Image URL)": "\u5a92\u9ad4\u6d77\u5831\uff08\u5f71\u50cfImage URL\uff09",
"Paste your embed code below:": "\u8acb\u5c07\u60a8\u7684\u5d4c\u5165\u5f0f\u7a0b\u5f0f\u78bc\u8cbc\u5728\u4e0b\u9762:",
"Embed": "\u5d4c\u5165\u78bc",
"Media...": "\u5a92\u9ad4...",
"Nonbreaking space": "\u4e0d\u5206\u884c\u7684\u7a7a\u683c",
"Page break": "\u5206\u9801",
"Paste as text": "\u4ee5\u7d14\u6587\u5b57\u8cbc\u4e0a",
"Preview": "\u9810\u89bd",
"Print...": "\u5217\u5370...",
"Save": "\u5132\u5b58",
"Find": "\u641c\u5c0b",
"Replace with": "\u66f4\u63db",
"Replace": "\u66ff\u63db",
"Replace all": "\u66ff\u63db\u5168\u90e8",
"Previous": "\u4e0a\u4e00\u500b",
"Next": "\u4e0b\u4e00\u500b",
"Find and replace...": "\u5c0b\u627e\u53ca\u53d6\u4ee3...",
"Could not find the specified string.": "\u7121\u6cd5\u67e5\u8a62\u5230\u6b64\u7279\u5b9a\u5b57\u4e32",
"Match case": "\u76f8\u5339\u914d\u6848\u4ef6",
"Find whole words only": "\u50c5\u627e\u51fa\u5b8c\u6574\u5b57\u532f",
"Spell check": "\u62fc\u5beb\u6aa2\u67e5",
"Ignore": "\u5ffd\u7565",
"Ignore all": "\u5ffd\u7565\u6240\u6709",
"Finish": "\u5b8c\u6210",
"Add to Dictionary": "\u52a0\u5165\u5b57\u5178\u4e2d",
"Insert table": "\u63d2\u5165\u8868\u683c",
"Table properties": "\u8868\u683c\u5c6c\u6027",
"Delete table": "\u522a\u9664\u8868\u683c",
"Cell": "\u5132\u5b58\u683c",
"Row": "\u5217",
"Column": "\u884c",
"Cell properties": "\u5132\u5b58\u683c\u5c6c\u6027",
"Merge cells": "\u5408\u4f75\u5132\u5b58\u683c",
"Split cell": "\u5206\u5272\u5132\u5b58\u683c",
"Insert row before": "\u63d2\u5165\u5217\u5728...\u4e4b\u524d",
"Insert row after": "\u63d2\u5165\u5217\u5728...\u4e4b\u5f8c",
"Delete row": "\u522a\u9664\u5217",
"Row properties": "\u5217\u5c6c\u6027",
"Cut row": "\u526a\u4e0b\u5217",
"Copy row": "\u8907\u88fd\u5217",
"Paste row before": "\u8cbc\u4e0a\u5217\u5728...\u4e4b\u524d",
"Paste row after": "\u8cbc\u4e0a\u5217\u5728...\u4e4b\u5f8c",
"Insert column before": "\u63d2\u5165\u6b04\u4f4d\u5728...\u4e4b\u524d",
"Insert column after": "\u63d2\u5165\u6b04\u4f4d\u5728...\u4e4b\u5f8c",
"Delete column": "\u522a\u9664\u884c",
"Cols": "\u6b04\u4f4d\u6bb5",
"Rows": "\u5217",
"Width": "\u5bec\u5ea6",
"Height": "\u9ad8\u5ea6",
"Cell spacing": "\u5132\u5b58\u683c\u5f97\u9593\u8ddd",
"Cell padding": "\u5132\u5b58\u683c\u7684\u908a\u8ddd",
"Show caption": "\u986f\u793a\u6a19\u984c",
"Left": "\u5de6\u908a",
"Center": "\u4e2d\u9593",
"Right": "\u53f3\u908a",
"Cell type": "\u5132\u5b58\u683c\u7684\u985e\u578b",
"Scope": "\u7bc4\u570d",
"Alignment": "\u5c0d\u9f4a",
"H Align": "\u6c34\u5e73\u4f4d\u7f6e",
"V Align": "\u5782\u76f4\u4f4d\u7f6e",
"Top": "\u7f6e\u9802",
"Middle": "\u7f6e\u4e2d",
"Bottom": "\u7f6e\u5e95",
"Header cell": "\u6a19\u982d\u5132\u5b58\u683c",
"Row group": "\u5217\u7fa4\u7d44",
"Column group": "\u6b04\u4f4d\u7fa4\u7d44",
"Row type": "\u884c\u7684\u985e\u578b",
"Header": "\u6a19\u982d",
"Body": "\u4e3b\u9ad4",
"Footer": "\u9801\u5c3e",
"Border color": "\u908a\u6846\u984f\u8272",
"Insert template...": "\u63d2\u5165\u6a23\u7248...",
"Templates": "\u6a23\u7248",
"Template": "\u6a23\u677f",
"Text color": "\u6587\u5b57\u984f\u8272",
"Background color": "\u80cc\u666f\u984f\u8272",
"Custom...": "\u81ea\u8a02",
"Custom color": "\u81ea\u8a02\u984f\u8272",
"No color": "No color",
"Remove color": "\u79fb\u9664\u984f\u8272",
"Table of Contents": "\u76ee\u9304",
"Show blocks": "\u986f\u793a\u5340\u584a\u8cc7\u8a0a",
"Show invisible characters": "\u986f\u793a\u96b1\u85cf\u5b57\u5143",
"Word count": "\u8a08\u7b97\u5b57\u6578",
"Count": "\u8a08\u7b97",
"Document": "\u6587\u4ef6",
"Selection": "\u9078\u9805",
"Words": "\u5b57\u6578",
"Words: {0}": "\u5b57\u6578\uff1a{0}",
"{0} words": "{0} \u5b57\u5143",
"File": "\u6a94\u6848",
"Edit": "\u7de8\u8f2f",
"Insert": "\u63d2\u5165",
"View": "\u6aa2\u8996",
"Format": "\u683c\u5f0f",
"Table": "\u8868\u683c",
"Tools": "\u5de5\u5177",
"Powered by {0}": "\u7531 {0} \u63d0\u4f9b",
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u8c50\u5bcc\u7684\u6587\u672c\u5340\u57df\u3002\u6309ALT-F9\u524d\u5f80\u4e3b\u9078\u55ae\u3002\u6309ALT-F10\u547c\u53eb\u5de5\u5177\u6b04\u3002\u6309ALT-0\u5c0b\u6c42\u5e6b\u52a9",
"Image title": "\u5716\u7247\u6a19\u984c",
"Border width": "\u6846\u7dda\u5bec\u5ea6",
"Border style": "\u6846\u7dda\u6a23\u5f0f",
"Error": "\u932f\u8aa4",
"Warn": "\u8b66\u544a",
"Valid": "\u6709\u6548",
"To open the popup, press Shift+Enter": "\u8981\u958b\u555f\u5f48\u51fa\u8996\u7a97\uff0c\u8acb\u6309Shift+Enter",
"Rich Text Area. Press ALT-0 for help.": "\u5bcc\u6587\u672c\u5340\u57df\u3002\u8acb\u6309ALT-0\u5c0b\u6c42\u5354\u52a9\u3002",
"System Font": "\u7cfb\u7d71\u5b57\u578b",
"Failed to upload image: {0}": "\u7121\u6cd5\u4e0a\u50b3\u5f71\u50cf\uff1a{0}",
"Failed to load plugin: {0} from url {1}": "\u7121\u6cd5\u4e0a\u50b3\u63d2\u4ef6\uff1a{0}\u81eaurl{1}",
"Failed to load plugin url: {0}": "\u7121\u6cd5\u4e0a\u50b3\u63d2\u4ef6\uff1a{0}",
"Failed to initialize plugin: {0}": "\u7121\u6cd5\u555f\u52d5\u63d2\u4ef6\uff1a{0}",
"example": "\u7bc4\u4f8b",
"Search": "\u641c\u7d22",
"All": "\u5168\u90e8",
"Currency": "\u8ca8\u5e63",
"Text": "\u6587\u672c",
"Quotations": "\u5f15\u7528",
"Mathematical": "\u6578\u5b78",
"Extended Latin": "\u62c9\u4e01\u5b57\u6bcd\u64f4\u5145",
"Symbols": "\u7b26\u865f",
"Arrows": "\u7bad\u982d",
"User Defined": "\u4f7f\u7528\u8005\u5df2\u5b9a\u7fa9",
"dollar sign": "\u7f8e\u5143\u7b26\u865f",
"currency sign": "\u8ca8\u5e63\u7b26\u865f",
"euro-currency sign": "\u6b50\u5143\u7b26\u865f",
"colon sign": "\u79d1\u6717\u7b26\u865f",
"cruzeiro sign": "\u514b\u9b6f\u8cfd\u7f85\u7b26\u865f",
"french franc sign": "\u6cd5\u6717\u7b26\u865f",
"lira sign": "\u91cc\u62c9\u7b26\u865f",
"mill sign": "\u6587\u7b26\u865f",
"naira sign": "\u5948\u62c9\u7b26\u865f",
"peseta sign": "\u6bd4\u585e\u5854\u7b26\u865f",
"rupee sign": "\u76e7\u6bd4\u7b26\u865f",
"won sign": "\u97d3\u571c\u7b26\u865f",
"new sheqel sign": "\u65b0\u8b1d\u514b\u723e\u7b26\u865f",
"dong sign": "\u8d8a\u5357\u76fe\u7b26\u865f",
"kip sign": "\u8001\u64be\u5e63\u7b26\u865f",
"tugrik sign": "\u8499\u53e4\u5e63\u7b26\u865f",
"drachma sign": "\u5fb7\u514b\u62c9\u99ac\u7b26\u865f",
"german penny symbol": "\u5fb7\u570b\u5206\u7b26\u865f",
"peso sign": "\u62ab\u7d22\u7b26\u865f",
"guarani sign": "\u5df4\u62c9\u572d\u5e63\u7b26\u865f",
"austral sign": "\u963f\u6839\u5ef7\u5e63\u7b26\u865f",
"hryvnia sign": "\u70cf\u514b\u862d\u5e63\u7b26\u865f",
"cedi sign": "\u8fe6\u7d0d\u5e63\u7b26\u865f",
"livre tournois sign": "\u91cc\u5f17\u723e\u7b26\u865f",
"spesmilo sign": "\u570b\u969b\u5e63\u7b26\u865f",
"tenge sign": "\u54c8\u85a9\u514b\u5e63\u7b26\u865f",
"indian rupee sign": "\u5370\u5ea6\u76e7\u6bd4\u7b26\u865f",
"turkish lira sign": "\u571f\u8033\u5176\u91cc\u62c9\u7b26\u865f",
"nordic mark sign": "\u5317\u6b50\u99ac\u514b\u7b26\u865f",
"manat sign": "\u4e9e\u585e\u62dc\u7136\u5e63\u7b26\u865f",
"ruble sign": "\u76e7\u5e03\u7b26\u865f",
"yen character": "\u65e5\u5713\u7b26\u865f",
"yuan character": "\u4eba\u6c11\u5e63\u7b26\u865f",
"yuan character, in hong kong and taiwan": "\u6e2f\u5143\u8207\u53f0\u5e63\u7b26\u865f",
"yen\/yuan character variant one": "\u65e5\u5713\/\u4eba\u6c11\u5e63\u7b26\u865f\u8b8a\u5316\u578b",
"Loading emoticons...": "\u8f09\u5165\u8868\u60c5\u7b26\u865f\u2026",
"Could not load emoticons": "\u7121\u6cd5\u8f09\u5165\u8868\u60c5\u7b26\u865f",
"People": "\u4eba",
"Animals and Nature": "\u52d5\u7269\u8207\u81ea\u7136",
"Food and Drink": "\u98f2\u98df",
"Activity": "\u6d3b\u52d5",
"Travel and Places": "\u65c5\u884c\u8207\u5730\u9ede",
"Objects": "\u7269\u4ef6",
"Flags": "\u65d7\u6a19",
"Characters": "\u5b57\u5143",
"Characters (no spaces)": "\u5b57\u5143\uff08\u7121\u7a7a\u683c\uff09",
"{0} characters": "{0}\u5b57\u5143",
"Error: Form submit field collision.": "\u932f\u8aa4\uff1a\u8868\u683c\u905e\u4ea4\u6b04\u4f4d\u885d\u7a81\u3002",
"Error: No form element found.": "\u932f\u8aa4\uff1a\u627e\u4e0d\u5230\u8868\u683c\u5143\u7d20\u3002",
"Update": "\u66f4\u65b0",
"Color swatch": "\u8272\u5f69\u6a23\u672c",
"Turquoise": "\u571f\u8033\u5176\u85cd",
"Green": "\u7da0\u8272",
"Blue": "\u85cd\u8272",
"Purple": "\u7d2b\u8272",
"Navy Blue": "\u6df1\u85cd\u8272",
"Dark Turquoise": "\u6df1\u571f\u8033\u5176\u85cd",
"Dark Green": "\u6df1\u7da0\u8272",
"Medium Blue": "\u4e2d\u85cd\u8272",
"Medium Purple": "\u4e2d\u7d2b\u8272",
"Midnight Blue": "\u9ed1\u85cd\u8272",
"Yellow": "\u9ec3\u8272",
"Orange": "\u6a59\u8272",
"Red": "\u7d05\u8272",
"Light Gray": "\u6dfa\u7070\u8272",
"Gray": "\u7070\u8272",
"Dark Yellow": "\u6df1\u9ec3\u8272",
"Dark Orange": "\u6df1\u6a59\u8272",
"Dark Red": "\u6697\u7d05\u8272",
"Medium Gray": "\u4e2d\u7070\u8272",
"Dark Gray": "\u6df1\u7070\u8272",
"Light Green": "\u6de1\u7da0\u8272",
"Light Yellow": "\u6dfa\u9ec3\u8272",
"Light Red": "\u6dfa\u7d05\u8272",
"Light Purple": "\u6dfa\u7d2b\u8272",
"Light Blue": "\u6dfa\u85cd\u8272",
"Dark Purple": "\u6df1\u7d2b\u8272",
"Dark Blue": "\u6df1\u85cd\u8272",
"Black": "\u9ed1\u8272",
"White": "\u767d\u8272",
"Switch to or from fullscreen mode": "\u8f49\u63db\u81ea\/\u81f3\u5168\u87a2\u5e55\u6a21\u5f0f",
"Open help dialog": "\u958b\u555f\u5354\u52a9\u5c0d\u8a71",
"history": "\u6b77\u53f2",
"styles": "\u6a23\u5f0f",
"formatting": "\u683c\u5f0f",
"alignment": "\u5c0d\u9f4a",
"indentation": "\u7e2e\u6392",
"permanent pen": "\u6c38\u4e45\u6027\u7b46",
"comments": "\u8a3b\u89e3",
"Format Painter": "\u8907\u88fd\u683c\u5f0f",
"Insert\/edit iframe": "\u63d2\u5165\/\u7de8\u8f2fiframe",
"Capitalization": "\u5927\u5beb",
"lowercase": "\u5c0f\u5beb",
"UPPERCASE": "\u5927\u5beb",
"Title Case": "\u5b57\u9996\u5927\u5beb",
"Permanent Pen Properties": "\u6c38\u4e45\u6a19\u8a18\u5c6c\u6027",
"Permanent pen properties...": "\u6c38\u4e45\u6a19\u8a18\u5c6c\u6027......",
"Font": "\u5b57\u578b",
"Size": "\u5b57\u5f62\u5927\u5c0f",
"More...": "\u66f4\u591a\u8cc7\u8a0a......",
"Spellcheck Language": "\u62fc\u5beb\u8a9e\u8a00",
"Select...": "\u9078\u64c7......",
"Preferences": "\u9996\u9078\u9805",
"Yes": "\u662f",
"No": "\u5426",
"Keyboard Navigation": "\u9375\u76e4\u5c0e\u822a",
"Version": "\u7248\u672c",
"Anchor": "\u52a0\u5165\u9328\u9ede",
"Special character": "\u7279\u6b8a\u5b57\u5143",
"Code sample": "\u7a0b\u5f0f\u78bc\u7bc4\u4f8b",
"Color": "\u984f\u8272",
"Emoticons": "\u8868\u60c5",
"Document properties": "\u6587\u4ef6\u7684\u5c6c\u6027",
"Image": "\u5716\u7247",
"Insert link": "\u63d2\u5165\u9023\u7d50",
"Target": "\u958b\u555f\u65b9\u5f0f",
"Link": "\u9023\u7d50",
"Poster": "\u9810\u89bd\u5716\u7247",
"Media": "\u5a92\u9ad4",
"Print": "\u5217\u5370",
"Prev": "\u4e0a\u4e00\u500b",
"Find and replace": "\u5c0b\u627e\u53ca\u53d6\u4ee3",
"Whole words": "\u6574\u500b\u55ae\u5b57",
"Spellcheck": "\u62fc\u5b57\u6aa2\u67e5",
"Caption": "\u8868\u683c\u6a19\u984c",
"Insert template": "\u63d2\u5165\u6a23\u7248"
});

View File

@ -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{background-color:#2f3742;color:#dfe0e4;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}a{color:#4099ff}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#6d737b}figure{display:table;margin:1rem auto}figure figcaption{color:#8a8f97;display:block;margin-top:.25rem;text-align:center}hr{border-color:#6d737b;border-style:solid;border-width:1px 0 0 0}code{background-color:#6d737b;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #6d737b;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #6d737b;margin-right:1.5rem;padding-right:1rem}

View File

@ -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{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}

View File

@ -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/
*/
@media screen{html{background:#f4f4f4;min-height:100%}}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif}@media screen{body{background-color:#fff;box-shadow:0 0 4px rgba(0,0,0,.15);box-sizing:border-box;margin:1rem auto 0;max-width:820px;min-height:calc(100vh - 1rem);padding:4rem 6rem 6rem 6rem}}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure figcaption{color:#999;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}

View File

@ -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{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem auto;max-width:900px}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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%;left:0;margin:0;overflow:hidden;-ms-scroll-chaining:none;overscroll-behavior:none;padding:0;position:fixed;top:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox.tox-tinymce.tox-fullscreen{background-color:transparent;z-index:1200}.tox-shadowhost.tox-fullscreen{z-index:1200}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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%;left:0;margin:0;overflow:hidden;-ms-scroll-chaining:none;overscroll-behavior:none;padding:0;position:fixed;top:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox.tox-tinymce.tox-fullscreen{background-color:transparent;z-index:1200}.tox-shadowhost.tox-fullscreen{z-index:1200}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}

16
src/App.vue Normal file
View File

@ -0,0 +1,16 @@
<script setup lang="ts">
import { computed } from 'vue';
import { ElConfigProvider } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { getElementPlusLocale } from '@/i18n';
const { locale } = useI18n({ useScope: 'global' });
const lang = computed(() => getElementPlusLocale(locale.value as string));
</script>
<template>
<!-- tinymce 对话框的层级太低必须调低 ElementPlus 对话框层级默认为2000 -->
<el-config-provider :locale="lang" :z-index="500">
<router-view />
</el-config-provider>
</template>

97
src/api/config.ts Normal file
View File

@ -0,0 +1,97 @@
import axios from '@/utils/request';
export const imageUploadUrl = `${import.meta.env.VITE_BASE_API}/backend/image-upload`;
export const avatarUploadUrl = `${import.meta.env.VITE_BASE_API}/backend/avatar-upload`;
export const videoUploadUrl = `${import.meta.env.VITE_BASE_API}/backend/video-upload`;
export const audioUploadUrl = `${import.meta.env.VITE_BASE_API}/backend/audio-upload`;
export const mediaUploadUrl = `${import.meta.env.VITE_BASE_API}/backend/media-upload`;
export const docUploadUrl = `${import.meta.env.VITE_BASE_API}/backend/doc-upload`;
export const fileUploadUrl = `${import.meta.env.VITE_BASE_API}/backend/file-upload`;
export const cropImage = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/image-crop', data)).data;
export const cropAvatar = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/avatar-crop', data)).data;
export const fetchImage = async (url: string): Promise<any> => (await axios.post('/backend/image-fetch', url, { headers: { 'Content-Type': 'text/plain' } })).data;
export const queryConfigModel = async (): Promise<any> => (await axios.get('/backend/core/config/model')).data;
export const queryConfig = async (): Promise<any> => (await axios.get('/backend/core/config')).data;
export const queryConfigGrey = async (): Promise<any> => (await axios.get('/backend/core/config/grey')).data;
export const queryConfigSms = async (): Promise<any> => (await axios.get('/backend/core/config/sms')).data;
export const queryConfigEmail = async (): Promise<any> => (await axios.get('/backend/core/config/email')).data;
export const queryUploadStorage = async (): Promise<any> => (await axios.get('/backend/core/config/upload-storage')).data;
export const queryHtmlStorage = async (): Promise<any> => (await axios.get('/backend/core/config/html-storage')).data;
export const queryTemplateStorage = async (): Promise<any> => (await axios.get('/backend/core/config/template-storage')).data;
export const updateConfigBase = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/config/base?_method=put', data)).data;
export const updateConfigCustoms = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/config/customs?_method=put', data)).data;
export const updateConfigUpload = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/config/upload?_method=put', data)).data;
export const updateConfigGrey = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/config/grey?_method=put', data)).data;
export const updateConfigRegister = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/config/register?_method=put', data)).data;
export const updateConfigSecurity = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/config/security?_method=put', data)).data;
export const updateConfigSms = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/config/sms?_method=put', data)).data;
export const sendTestSms = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/config/sms/send', data)).data;
export const updateConfigEmail = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/config/email?_method=put', data)).data;
export const sendTestEmail = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/config/email/send', data)).data;
export const updateUploadStorage = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/config/upload-storage?_method=put', data)).data;
export const updateHtmlStorage = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/config/html-storage?_method=put', data)).data;
export const updateTemplateStorage = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/config/template-storage?_method=put', data)).data;
export const storagePathAllowed = async (path: string): Promise<any> => (await axios.get('/backend/core/config/storage-path-allowed', { params: { path } })).data;
export const querySiteSettings = async (): Promise<any> => (await axios.get('/backend/core/site-settings')).data;
export const updateSiteBaseSettings = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/site-settings/base?_method=put', data)).data;
export const updateSiteCustomsSettings = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/site-settings/customs?_method=put', data)).data;
export const updateSiteWatermarkSettings = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/site-settings/watermark?_method=put', data)).data;
export const updateSiteEditorSettings = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/site-settings/editor?_method=put', data)).data;
export const updateSiteMessageBoardSettings = async (data: Record<string, any>): Promise<any> =>
(await axios.post('/backend/core/site-settings/message-board?_method=put', data)).data;
export const querySiteHtmlSettings = async (): Promise<any> => (await axios.get('/backend/core/site-settings/html')).data;
export const updateSiteHtmlSettings = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/site-settings/html?_method=put', data)).data;
export const queryCurrentSiteThemeList = async (): Promise<any> => (await axios.get('/backend/core/site/theme')).data;
export const queryModelList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/model', { params })).data;
export const queryModel = async (id: string): Promise<any> => (await axios.get(`/backend/core/model/${id}`)).data;
export const createModel = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/model', data)).data;
export const updateModel = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/model?_method=put', data)).data;
export const updateModelOrder = async (data: string[]): Promise<any> => (await axios.post('/backend/core/model/order?_method=put', data)).data;
export const deleteModel = async (data: string[]): Promise<any> => (await axios.post('/backend/core/model?_method=delete', data)).data;
export const queryDictTypeList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/dict-type', { params })).data;
export const queryDictType = async (id: string): Promise<any> => (await axios.get(`/backend/core/dict-type/${id}`)).data;
export const createDictType = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/dict-type', data)).data;
export const updateDictType = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/dict-type?_method=put', data)).data;
export const updateDictTypeOrder = async (data: string[]): Promise<any> => (await axios.post('/backend/core/dict-type/order?_method=put', data)).data;
export const deleteDictType = async (data: string[]): Promise<any> => (await axios.post('/backend/core/dict-type?_method=delete', data)).data;
export const dictTypeAliasExist = async (alias: string, scope: number): Promise<any> => (await axios.get('/backend/core/dict-type/alias-exist', { params: { alias, scope } })).data;
export const queryBlockList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/block', { params })).data;
export const queryBlock = async (id: string): Promise<any> => (await axios.get(`/backend/core/block/${id}`)).data;
export const createBlock = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/block', data)).data;
export const updateBlock = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/block?_method=put', data)).data;
export const updateBlockOrder = async (data: string[]): Promise<any> => (await axios.post('/backend/core/block/order?_method=put', data)).data;
export const deleteBlock = async (data: string[]): Promise<any> => (await axios.post('/backend/core/block?_method=delete', data)).data;
export const blockAliasExist = async (alias: string, scope: number): Promise<any> => (await axios.get('/backend/core/block/alias-exist', { params: { alias, scope } })).data;
export const blockScopeNotAllowed = async (scope: number, blockId: string): Promise<any> =>
(await axios.get('/backend/core/block/scope-not-allowed', { params: { scope, blockId } })).data;
export const queryMessageBoardTypeList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/message-board-type', { params })).data;
export const queryMessageBoardType = async (id: string): Promise<any> => (await axios.get(`/backend/ext/message-board-type/${id}`)).data;
export const createMessageBoardType = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/message-board-type', data)).data;
export const updateMessageBoardType = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/message-board-type?_method=put', data)).data;
export const updateMessageBoardTypeOrder = async (fromId: string, toId: string): Promise<any> =>
(await axios.post('/backend/ext/message-board-type/update-order', { fromId, toId })).data;
export const deleteMessageBoardType = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/message-board-type?_method=delete', data)).data;
export const queryPerformanceTypeList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/performance-type', { params })).data;
export const queryPerformanceType = async (id: string): Promise<any> => (await axios.get(`/backend/core/performance-type/${id}`)).data;
export const createPerformanceType = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/performance-type', data)).data;
export const updatePerformanceType = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/performance-type?_method=put', data)).data;
export const updatePerformanceTypeOrder = async (fromId: string, toId: string): Promise<any> =>
(await axios.post('/backend/core/performance-type/update-order', { fromId, toId })).data;
export const deletePerformanceType = async (data: string[]): Promise<any> => (await axios.post('/backend/core/performance-type?_method=delete', data)).data;
export const queryFormTypeList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/form-type', { params })).data;
export const queryFormType = async (id: string): Promise<any> => (await axios.get(`/backend/ext/form-type/${id}`)).data;
export const createFormType = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/form-type', data)).data;
export const updateFormType = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/form-type?_method=put', data)).data;
export const updateFormTypeOrder = async (fromId: string, toId: string): Promise<any> => (await axios.post('/backend/ext/form-type/update-order', { fromId, toId })).data;
export const deleteFormType = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/form-type?_method=delete', data)).data;
export const queryFormListTemplates = async (): Promise<any> => (await axios.get('/backend/ext/form-type/list-templates')).data;
export const queryFormItemTemplates = async (): Promise<any> => (await axios.get('/backend/ext/form-type/item-templates')).data;

112
src/api/content.ts Normal file
View File

@ -0,0 +1,112 @@
import axios from '@/utils/request';
export const jodConvertDocUrl = `${import.meta.env.VITE_BASE_API}/backend/core/jod-convert/doc`;
export const jodConvertLibraryUrl = `${import.meta.env.VITE_BASE_API}/backend/core/jod-convert/library`;
export const queryJodConvertEnabled = async (): Promise<boolean> => (await axios.get('/backend/core/jod-convert/enabled')).data;
export const queryChannelList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/channel', { params })).data;
export const queryChannel = async (id: string): Promise<any> => (await axios.get(`/backend/core/channel/${id}`)).data;
export const createChannel = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/channel', data)).data;
export const updateChannel = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/channel?_method=put', data)).data;
export const updateChannelNav = async (id: string, nav: boolean): Promise<any> => (await axios.post('/backend/core/channel/nav?_method=put', { id, nav })).data;
export const updateChannelReal = async (id: string, real: boolean): Promise<any> => (await axios.post('/backend/core/channel/real?_method=put', { id, real })).data;
export const moveChannel = async (fromId: string, toId: string, type: 'inner' | 'before' | 'after'): Promise<any> =>
(await axios.post('/backend/core/channel/move?_method=put', { fromId, toId, type })).data;
export const batchMoveChannel = async (fromIds: string[], toId: string, type: 'inner' | 'before' | 'after'): Promise<any> =>
(await axios.post('/backend/core/channel/batch-move?_method=put', { fromIds, toId, type })).data;
export const batchMergeChannel = async (fromIds: string[], toId: string): Promise<any> =>
(await axios.post('/backend/core/channel/batch-merge?_method=put', { fromIds, toId })).data;
export const tidyTreeChannel = async (): Promise<any> => (await axios.post('/backend/core/channel/tidy-tree?_method=put')).data;
export const deleteChannel = async (data: string[]): Promise<any> => (await axios.post('/backend/core/channel?_method=delete', data)).data;
export const queryChannelPermissions = async (): Promise<any> => (await axios.get('/backend/core/channel/channel-permissions')).data;
export const queryChannelTemplates = async (): Promise<any> => (await axios.get('/backend/core/channel/channel-templates')).data;
export const queryArticleTemplates = async (): Promise<any> => (await axios.get('/backend/core/channel/article-templates')).data;
export const channelAliasExist = async (alias?: string): Promise<any> => (await axios.get('/backend/core/channel/alias-exist', { params: { alias } })).data;
export const queryArticlePage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/article', { params })).data;
export const queryArticleRejectCount = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/article/reject-count', { params })).data;
export const queryArticle = async (id: string): Promise<any> => (await axios.get(`/backend/core/article/${id}`)).data;
export const queryArticleTitleSimilarity = async (similarity: number, title: string, excludeId?: string): Promise<any> =>
(await axios.get('/backend/core/article/title-similarity', { params: { similarity, title, excludeId } })).data;
export const createArticle = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/article', data)).data;
export const updateArticle = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/article?_method=put', data)).data;
export const updateArticleOrder = async (fromId: string, toId: string): Promise<any> => (await axios.post('/backend/core/article/update-order', { fromId, toId })).data;
export const internalPushArticle = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/article/internal-push', data)).data;
export const externalPushArticle = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/article/external-push', data)).data;
export const stickyArticle = async (ids: string[], sticky: number, stickyDate?: Date): Promise<any> =>
(await axios.post('/backend/core/article/sticky?_method=put', { ids, sticky, stickyDate })).data;
export const deleteArticle = async (data: string[]): Promise<any> => (await axios.post('/backend/core/article/delete?_method=put', data)).data;
export const submitArticle = async (data: string[]): Promise<any> => (await axios.post('/backend/core/article/submit?_method=put', data)).data;
export const archiveArticle = async (data: string[]): Promise<any> => (await axios.post('/backend/core/article/archive?_method=put', data)).data;
export const offlineArticle = async (data: string[]): Promise<any> => (await axios.post('/backend/core/article/offline?_method=put', data)).data;
export const completelyDeleteArticle = async (data: string[]): Promise<any> => (await axios.post('/backend/core/article?_method=delete', data)).data;
export const queryArticleReviewPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/article-review', { params })).data;
export const queryArticlePendingCount = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/article-review/pending-count', { params })).data;
export const queryArticleReview = async (id: string): Promise<any> => (await axios.get(`/backend/core/article-review/${id}`)).data;
export const passArticles = async (data: string[]): Promise<any> => (await axios.post('/backend/core/article-review/pass?_method=put', data)).data;
export const passArticle = async (taskId: string, properties: Record<string, string>, comment: string): Promise<any> =>
(await axios.post(`/backend/core/article-review/pass/${taskId}?_method=put`, { properties, comment })).data;
export const delegateArticle = async (taskId: string, toUserId: string, comment: string): Promise<any> =>
(await axios.post(`/backend/core/article-review/delegate?_method=put`, { taskId, toUserId, comment })).data;
export const transferArticle = async (taskId: string, toUserId: string, comment: string): Promise<any> =>
(await axios.post(`/backend/core/article-review/transfer?_method=put`, { taskId, toUserId, comment })).data;
export const backArticle = async (taskId: string, activityId: string, comment: string): Promise<any> =>
(await axios.post(`/backend/core/article-review/back?_method=put`, { taskId, activityId, comment })).data;
export const rejectArticle = async (taskIds: string[], reason: string): Promise<any> =>
(await axios.post('/backend/core/article-review/reject?_method=put', { taskIds, reason })).data;
export const queryBlockItemList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/block-item', { params })).data;
export const queryBlockItem = async (id: string): Promise<any> => (await axios.get(`/backend/core/block-item/${id}`)).data;
export const createBlockItem = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/block-item', data)).data;
export const updateBlockItem = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/block-item?_method=put', data)).data;
export const updateBlockItemOrder = async (fromId: string, toId: string): Promise<any> => (await axios.post('/backend/core/block-item/update-order', { fromId, toId })).data;
export const deleteBlockItem = async (data: string[]): Promise<any> => (await axios.post('/backend/core/block-item?_method=delete', data)).data;
export const queryDictList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/dict', { params })).data;
export const queryDictListByAlias = async (alias: string, name?: string): Promise<any> => (await axios.get('/backend/core/dict/list-by-alias', { params: { alias, name } })).data;
export const queryDict = async (id: string): Promise<any> => (await axios.get(`/backend/core/dict/${id}`)).data;
export const createDict = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/dict', data)).data;
export const updateDict = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/dict?_method=put', data)).data;
export const updateDictOrder = async (data: string[]): Promise<any> => (await axios.post('/backend/core/dict/order?_method=put', data)).data;
export const deleteDict = async (data: string[]): Promise<any> => (await axios.post('/backend/core/dict?_method=delete', data)).data;
export const fulltextReindexAll = async (): Promise<any> => (await axios.post('/backend/core/generator/fulltext-reindex-all')).data;
export const fulltextReindexSite = async (): Promise<any> => (await axios.post('/backend/core/generator/fulltext-reindex-site')).data;
export const htmlAll = async (): Promise<any> => (await axios.post('/backend/core/generator/html-all')).data;
export const htmlAllHome = async (): Promise<any> => (await axios.post('/backend/core/generator/html-all-home')).data;
export const htmlHome = async (): Promise<any> => (await axios.post('/backend/core/generator/html-home')).data;
export const htmlChannel = async (): Promise<any> => (await axios.post('/backend/core/generator/html-channel')).data;
export const htmlArticle = async (): Promise<any> => (await axios.post('/backend/core/generator/html-article')).data;
export const queryTagPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/tag', { params })).data;
export const queryTagList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/tag/list', { params })).data;
export const queryTag = async (id: string): Promise<any> => (await axios.get(`/backend/core/tag/${id}`)).data;
export const createTag = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/tag', data)).data;
export const updateTag = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/tag?_method=put', data)).data;
export const deleteTag = async (data: string[]): Promise<any> => (await axios.post('/backend/core/tag?_method=delete', data)).data;
export const queryFormPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/form', { params })).data;
export const queryForm = async (id: string): Promise<any> => (await axios.get(`/backend/ext/form/${id}`)).data;
export const queryFormRejectCount = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/form/reject-count', { params })).data;
export const createForm = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/form', data)).data;
export const updateForm = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/form?_method=put', data)).data;
export const updateFormOrder = async (fromId: string, toId: string): Promise<any> => (await axios.post('/backend/ext/form/update-order', { fromId, toId })).data;
export const deleteForm = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/form/delete?_method=put', data)).data;
export const submitForm = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/form/submit?_method=put', data)).data;
export const completelyDeleteForm = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/form?_method=delete', data)).data;
export const queryFormReviewPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/form-review', { params })).data;
export const queryFormReviewTypeList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/form-review/type-list', { params })).data;
export const queryFormPendingCount = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/form-review/pending-count', { params })).data;
export const queryFormReview = async (id: string): Promise<any> => (await axios.get(`/backend/ext/form-review/${id}`)).data;
export const passForms = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/form-review/pass?_method=put', data)).data;
export const passForm = async (taskId: string, properties: Record<string, string>, comment: string): Promise<any> =>
(await axios.post(`/backend/ext/form-review/pass/${taskId}?_method=put`, { properties, comment })).data;
export const delegateForm = async (taskId: string, toUserId: string, comment: string): Promise<any> =>
(await axios.post(`/backend/ext/form-review/delegate?_method=put`, { taskId, toUserId, comment })).data;
export const transferForm = async (taskId: string, toUserId: string, comment: string): Promise<any> =>
(await axios.post(`/backend/ext/form-review/transfer?_method=put`, { taskId, toUserId, comment })).data;
export const backForm = async (taskId: string, activityId: string, comment: string): Promise<any> =>
(await axios.post(`/backend/ext/form-review/back?_method=put`, { taskId, activityId, comment })).data;
export const rejectForm = async (taskIds: string[], reason: string): Promise<any> => (await axios.post('/backend/ext/form-review/reject?_method=put', { taskIds, reason })).data;

74
src/api/file.ts Normal file
View File

@ -0,0 +1,74 @@
import axios from '@/utils/request';
export const uploadWebFileTemplateUrl = `${import.meta.env.VITE_BASE_API}/backend/ext/web-file-template/upload`;
export const uploadZipWebFileTemplateUrl = `${import.meta.env.VITE_BASE_API}/backend/ext/web-file-template/upload-zip`;
export const queryWebFileTemplateList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/web-file-template', { params })).data;
export const queryWebFileTemplate = async (id: string): Promise<any> => (await axios.get('/backend/ext/web-file-template/show', { params: { id } })).data;
export const downloadZipWebFileTemplate = async (dir: string, names: string[]): Promise<any> =>
(await axios.post('/backend/ext/web-file-template/download-zip', { dir, names }, { responseType: 'blob' })).data;
export const createWebFileTemplate = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/web-file-template', data)).data;
export const mkdirWebFileTemplate = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/web-file-template/mkdir', data)).data;
export const updateWebFileTemplate = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/web-file-template?_method=put', data)).data;
export const renameWebFileTemplate = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/web-file-template/rename?_method=put', data)).data;
export const copyWebFileTemplate = async (dir: string, names: string[], destDir: string): Promise<any> =>
(await axios.post('/backend/ext/web-file-template/copy', { dir, names, destDir })).data;
export const moveWebFileTemplate = async (dir: string, names: string[], destDir: string): Promise<any> =>
(await axios.post('/backend/ext/web-file-template/move', { dir, names, destDir })).data;
export const deleteWebFileTemplate = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/web-file-template?_method=delete', data)).data;
export const uploadWebFileUploadUrl = `${import.meta.env.VITE_BASE_API}/backend/ext/web-file-upload/upload`;
export const uploadZipWebFileUploadUrl = `${import.meta.env.VITE_BASE_API}/backend/ext/web-file-upload/upload-zip`;
export const queryWebFileUploadList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/web-file-upload', { params })).data;
export const queryWebFileUpload = async (id: string): Promise<any> => (await axios.get('/backend/ext/web-file-upload/show', { params: { id } })).data;
export const downloadZipWebFileUpload = async (dir: string, names: string[]): Promise<any> =>
(await axios.post('/backend/ext/web-file-upload/download-zip', { dir, names }, { responseType: 'blob' })).data;
export const createWebFileUpload = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/web-file-upload', data)).data;
export const mkdirWebFileUpload = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/web-file-upload/mkdir', data)).data;
export const updateWebFileUpload = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/web-file-upload?_method=put', data)).data;
export const renameWebFileUpload = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/web-file-upload/rename?_method=put', data)).data;
export const copyWebFileUpload = async (dir: string, names: string[], destDir: string): Promise<any> =>
(await axios.post('/backend/ext/web-file-upload/copy', { dir, names, destDir })).data;
export const moveWebFileUpload = async (dir: string, names: string[], destDir: string): Promise<any> =>
(await axios.post('/backend/ext/web-file-upload/move', { dir, names, destDir })).data;
export const deleteWebFileUpload = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/web-file-upload?_method=delete', data)).data;
export const uploadWebFileHtmlUrl = `${import.meta.env.VITE_BASE_API}/backend/ext/web-file-html/upload`;
export const uploadZipWebFileHtmlUrl = `${import.meta.env.VITE_BASE_API}/backend/ext/web-file-html/upload-zip`;
export const queryWebFileHtmlList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/web-file-html', { params })).data;
export const queryWebFileHtml = async (id: string): Promise<any> => (await axios.get('/backend/ext/web-file-html/show', { params: { id } })).data;
export const downloadZipWebFileHtml = async (dir: string, names: string[]): Promise<any> =>
(await axios.post('/backend/ext/web-file-html/download-zip', { dir, names }, { responseType: 'blob' })).data;
export const createWebFileHtml = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/web-file-html', data)).data;
export const mkdirWebFileHtml = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/web-file-html/mkdir', data)).data;
export const updateWebFileHtml = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/web-file-html?_method=put', data)).data;
export const renameWebFileHtml = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/web-file-html/rename?_method=put', data)).data;
export const copyWebFileHtml = async (dir: string, names: string[], destDir: string): Promise<any> =>
(await axios.post('/backend/ext/web-file-html/copy', { dir, names, destDir })).data;
export const moveWebFileHtml = async (dir: string, names: string[], destDir: string): Promise<any> =>
(await axios.post('/backend/ext/web-file-html/move', { dir, names, destDir })).data;
export const deleteWebFileHtml = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/web-file-html?_method=delete', data)).data;
export const queryBackupDatabaseList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/backup-database', { params })).data;
export const queryBackupDatabase = async (id: string): Promise<any> => (await axios.get(`/backend/ext/backup-database/${id}`)).data;
export const backupBackupDatabase = async (): Promise<any> => (await axios.post('/backend/ext/backup-database')).data;
export const restoreBackupDatabase = async (name: string): Promise<any> => (await axios.post('/backend/ext/backup-database?_method=put', { name })).data;
export const deleteBackupDatabase = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/backup-database?_method=delete', data)).data;
export const queryBackupTemplatesList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/backup-templates', { params })).data;
export const queryBackupTemplates = async (id: string): Promise<any> => (await axios.get(`/backend/ext/backup-templates/${id}`)).data;
export const backupBackupTemplates = async (): Promise<any> => (await axios.post('/backend/ext/backup-templates')).data;
export const restoreBackupTemplates = async (name: string): Promise<any> => (await axios.post('/backend/ext/backup-templates?_method=put', { name })).data;
export const deleteBackupTemplates = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/backup-templates?_method=delete', data)).data;
export const queryBackupUploadsList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/backup-uploads', { params })).data;
export const queryBackupUploads = async (id: string): Promise<any> => (await axios.get(`/backend/ext/backup-uploads/${id}`)).data;
export const backupBackupUploads = async (): Promise<any> => (await axios.post('/backend/ext/backup-uploads')).data;
export const restoreBackupUploads = async (name: string): Promise<any> => (await axios.post('/backend/ext/backup-uploads?_method=put', { name })).data;
export const deleteBackupUploads = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/backup-uploads?_method=delete', data)).data;
export const queryIncrementalUploadsList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/incremental-uploads', { params })).data;
export const queryIncrementalUploads = async (id: string): Promise<any> => (await axios.get(`/backend/ext/incremental-uploads/${id}`)).data;
export const backupIncrementalUploads = async (): Promise<any> => (await axios.post('/backend/ext/incremental-uploads')).data;
export const restoreIncrementalUploads = async (name: string): Promise<any> => (await axios.post('/backend/ext/incremental-uploads?_method=put', { name })).data;
export const mergeIncrementalUploads = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/incremental-uploads/merge', data)).data;
export const deleteIncrementalUploads = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/incremental-uploads?_method=delete', data)).data;

68
src/api/interaction.ts Normal file
View File

@ -0,0 +1,68 @@
import axios from '@/utils/request';
export const queryMessageBoardPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/message-board', { params })).data;
export const queryMessageBoardUnreviewedCount = async (params?: Record<string, any>): Promise<any> =>
(await axios.get('/backend/ext/message-board/unreviewed-count', { params })).data;
export const queryMessageBoard = async (id: string): Promise<any> => (await axios.get(`/backend/ext/message-board/${id}`)).data;
export const createMessageBoard = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/message-board', data)).data;
export const updateMessageBoard = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/message-board?_method=put', data)).data;
export const updateMessageBoardStatus = async (ids: string[], status: number): Promise<any> =>
(await axios.post('/backend/ext/message-board/status?_method=put', { ids, status })).data;
export const deleteMessageBoard = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/message-board?_method=delete', data)).data;
export const queryVotePage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/vote', { params })).data;
export const queryVote = async (id: string): Promise<any> => (await axios.get(`/backend/ext/vote/${id}`)).data;
export const createVote = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/vote', data)).data;
export const updateVote = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/vote?_method=put', data)).data;
export const updateVoteOrder = async (fromId: string, toId: string): Promise<any> => (await axios.post('/backend/ext/vote/update-order', { fromId, toId })).data;
export const deleteVote = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/vote?_method=delete', data)).data;
export const querySurveyPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/survey', { params })).data;
export const querySurvey = async (id: string): Promise<any> => (await axios.get(`/backend/ext/survey/${id}`)).data;
export const createSurvey = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/survey', data)).data;
export const updateSurvey = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/survey?_method=put', data)).data;
export const updateSurveyOrder = async (fromId: string, toId: string): Promise<any> => (await axios.post('/backend/ext/survey/update-order', { fromId, toId })).data;
export const deleteSurvey = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/survey?_method=delete', data)).data;
export const querySurveyOptionFeedbackPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/survey/option-feedback', { params })).data;
export const querySurveyItemFeedbackPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/survey/item-feedback', { params })).data;
export const updateSurveyItemFeedback = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/survey/item-feedback?_method=put', data)).data;
export const queryExamplePage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/example', { params })).data;
export const queryExample = async (id: string): Promise<any> => (await axios.get(`/backend/ext/example/${id}`)).data;
export const createExample = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/example', data)).data;
export const updateExample = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/example?_method=put', data)).data;
export const deleteExample = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/example?_method=delete', data)).data;
export const queryCollectionPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/collection', { params })).data;
export const queryCollection = async (id: string): Promise<any> => (await axios.get(`/backend/ext/collection/${id}`)).data;
export const createCollection = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/collection', data)).data;
export const updateCollection = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/collection?_method=put', data)).data;
export const updateCollectionOrder = async (fromId: string, toId: string): Promise<any> => (await axios.post('/backend/ext/collection/update-order', { fromId, toId })).data;
export const deleteCollection = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/collection?_method=delete', data)).data;
export const startCollection = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/collection/start?_method=put', data)).data;
export const pauseCollection = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/collection/pause?_method=put', data)).data;
export const stopCollection = async (data: string[]): Promise<any> => (await axios.post('/backend/ext/collection/stop?_method=put', data)).data;
export const collectionSetupListUrls = async (listUrls: string, pageBegin: number, pageEnd: number, listDesc: boolean): Promise<any> =>
(await axios.get(`/backend/ext/collection/setup/list-urls`, { params: { listUrls, pageBegin, pageEnd, listDesc } })).data;
export const collectionSetupDetailUrls = async (
listUrls: string,
pageBegin: number,
pageEnd: number,
userAgent: string,
charset: string,
listAreaPattern: string,
itemUrlPattern: string,
itemUrlReg: boolean,
itemUrlJs: boolean,
): Promise<any> =>
(
await axios.get(`/backend/ext/collection/setup/detail-urls`, {
params: { listUrls, pageBegin, pageEnd, userAgent, charset, listAreaPattern, itemUrlPattern, itemUrlReg, itemUrlJs },
})
).data;
export const collectionSetupFetchContent = async (url: string, userAgent: string, charset: string): Promise<any> =>
(await axios.get(`/backend/ext/collection/setup/fetch-content`, { params: { url, userAgent, charset } })).data;
export const collectionSetupMatch = async (text: string, texts: string[] | undefined, pattern: string, multi?: boolean, reg?: boolean, js?: boolean): Promise<any> =>
(await axios.post(`/backend/ext/collection/setup/match?_method=put`, { text, texts, pattern, multi, reg, js })).data;
export const collectionSetupFilter = async (text: string, texts: string[] | undefined, filter: string, multi?: boolean): Promise<any> =>
(await axios.post(`/backend/ext/collection/setup/filter?_method=put`, { text, texts, filter, multi })).data;

19
src/api/log.ts Normal file
View File

@ -0,0 +1,19 @@
import axios from '@/utils/request';
export const queryShortMessagePage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/short-message', { params })).data;
export const queryShortMessage = async (id: string): Promise<any> => (await axios.get(`/backend/core/short-message/${id}`)).data;
export const createShortMessage = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/short-message', data)).data;
export const updateShortMessage = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/short-message?_method=put', data)).data;
export const deleteShortMessage = async (data: string[]): Promise<any> => (await axios.post('/backend/core/short-message?_method=delete', data)).data;
export const queryLoginLogPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/login-log', { params })).data;
export const queryLoginLog = async (id: string): Promise<any> => (await axios.get(`/backend/core/login-log/${id}`)).data;
export const createLoginLog = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/login-log', data)).data;
export const updateLoginLog = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/login-log?_method=put', data)).data;
export const deleteLoginLog = async (data: string[]): Promise<any> => (await axios.post('/backend/core/login-log?_method=delete', data)).data;
export const queryOperationLogPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/operation-log', { params })).data;
export const queryOperationLog = async (id: string): Promise<any> => (await axios.get(`/backend/core/operation-log/${id}`)).data;
export const createOperationLog = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/operation-log', data)).data;
export const updateOperationLog = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/operation-log?_method=put', data)).data;
export const deleteOperationLog = async (data: string[]): Promise<any> => (await axios.post('/backend/core/operation-log?_method=delete', data)).data;

28
src/api/login.ts Normal file
View File

@ -0,0 +1,28 @@
import axios from '@/utils/request';
export interface LoginParam {
username: string;
password: string;
browser?: boolean;
}
export interface RefreshTokenParam {
refreshToken: string;
browser?: boolean;
}
export const accountLogin = async (data: LoginParam): Promise<any> => (await axios.post('/auth/jwt/login', data)).data;
export const accountLogout = async (refreshToken: string): Promise<any> => (await axios.post('/auth/jwt/logout', { refreshToken })).data;
export const accountRefreshToken = async (data: RefreshTokenParam): Promise<any> => (await axios.post('/auth/jwt/refresh-token', data)).data;
export const queryCurrentUser = async (): Promise<any> => (await axios.get('/env/current-user')).data;
export const queryCurrentSiteList = async (): Promise<any> => (await axios.get('/env/current-site-list')).data;
export const queryClientPublicKey = async (): Promise<any> => (await axios.get('/env/client-public-key')).data;
export const queryConfig = async (): Promise<any> => (await axios.get('/env/config')).data;
export const queryCaptcha = async (): Promise<any> => (await axios.get('/captcha')).data;
export const queryIsDisplayCaptcha = async (): Promise<any> => (await axios.get('/captcha/is-display')).data;
export const sendMobileMessage = async (captchaToken: string, captcha: string, mobile: string, usage: number): Promise<any> =>
(await axios.post('/sms/mobile', { captchaToken, captcha, receiver: mobile, usage })).data;
export const queryIsMfaLogin = async (): Promise<any> => (await axios.get('/env/is-mfa-login')).data;
export const tryCaptcha = async (token: string, captcha: string): Promise<any> => (await axios.get('/captcha/try', { params: { token, captcha } })).data;
export const mobileNotExist = async (mobile: string): Promise<any> => (await axios.get('/user/mobile-not-exist', { params: { mobile } })).data;
export const updatePassword = async (data: Record<string, any>): Promise<any> => (await axios.post('/update-password?_method=put', data)).data;

12
src/api/personal.ts Normal file
View File

@ -0,0 +1,12 @@
import axios from '@/utils/request';
export const updatePersonalPassword = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/personal/password?_method=put', data)).data;
export const queryMachineCode = async (): Promise<any> => (await axios.get('/backend/core/machine/code')).data;
export const queryMachineLicense = async (): Promise<any> => (await axios.get('/backend/core/machine/license')).data;
export const querySystemInfo = async (): Promise<any> => (await axios.get('/backend/core/homepage/system-info')).data;
export const querySystemMonitor = async (): Promise<any> => (await axios.get('/backend/core/homepage/system-monitor')).data;
export const querySystemLoad = async (): Promise<any> => (await axios.get('/backend/core/homepage/system-load')).data;
export const queryGeneratedKey = async (): Promise<any> => (await axios.get('/backend/core/homepage/generated-key')).data;
export const queryContentStat = async (): Promise<any> => (await axios.get('/backend/core/homepage/content-stat')).data;

20
src/api/stat.ts Normal file
View File

@ -0,0 +1,20 @@
import axios from '@/utils/request';
export const queryTrendStat = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/visit/trend-stat', { params })).data;
export const queryVisitedPageStat = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/visit/visited-page-stat', { params })).data;
export const queryEntryPageStat = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/visit/entry-page-stat', { params })).data;
export const queryVisitorStat = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/visit/visitor-stat', { params })).data;
export const querySourceStat = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/visit/source-stat', { params })).data;
export const queryCountryStat = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/visit/country-stat', { params })).data;
export const queryProvinceStat = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/visit/province-stat', { params })).data;
export const queryDeviceStat = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/visit/device-stat', { params })).data;
export const queryOsStat = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/visit/os-stat', { params })).data;
export const queryBrowserStat = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/visit/browser-stat', { params })).data;
export const querySourceTypeStat = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/visit/source-type-stat', { params })).data;
export const queryArticleStatByUser = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/article-stat/by-user', { params })).data;
export const queryArticleStatByOrg = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/article-stat/by-org', { params })).data;
export const queryArticleStatByChannel = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/article-stat/by-channel', { params })).data;
export const queryPerformanceStatByUser = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/performance-stat/by-user', { params })).data;
export const queryPerformanceStatByOrg = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/ext/performance-stat/by-org', { params })).data;

74
src/api/system.ts Normal file
View File

@ -0,0 +1,74 @@
import axios from '@/utils/request';
export const queryCurrentSite = async (): Promise<any> => (await axios.get('/backend/core/site/current')).data;
export const querySiteList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/site', { params })).data;
export const querySite = async (id: string): Promise<any> => (await axios.get(`/backend/core/site/${id}`)).data;
export const createSite = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/site', data)).data;
export const updateSite = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/site?_method=put', data)).data;
export const openSite = async (data: string[]): Promise<any> => (await axios.post('/backend/core/site/open?_method=put', data)).data;
export const closeSite = async (data: string[]): Promise<any> => (await axios.post('/backend/core/site/close?_method=put', data)).data;
export const moveSite = async (fromId: string, toId: string, type: 'inner' | 'before' | 'after'): Promise<any> =>
(await axios.post('/backend/core/site/move?_method=put', { fromId, toId, type })).data;
export const tidyTreeSite = async (): Promise<any> => (await axios.post('/backend/core/site/tidy-tree?_method=put')).data;
export const deleteSite = async (data: string[]): Promise<any> => (await axios.post('/backend/core/site?_method=delete', data)).data;
export const querySiteThemeList = async (id: string): Promise<any> => (await axios.get(`/backend/core/site/${id}/theme`)).data;
export const queryAttachmentPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/attachment', { params })).data;
export const queryAttachment = async (id: string): Promise<any> => (await axios.get(`/backend/core/attachment/${id}`)).data;
export const createAttachment = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/attachment', data)).data;
export const updateAttachment = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/attachment?_method=put', data)).data;
export const deleteAttachment = async (data: string[]): Promise<any> => (await axios.post('/backend/core/attachment?_method=delete', data)).data;
export const queryTaskPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/task', { params })).data;
export const queryTask = async (id: string): Promise<any> => (await axios.get(`/backend/core/task/${id}`)).data;
export const createTask = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/task', data)).data;
export const updateTask = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/task?_method=put', data)).data;
export const deleteTask = async (data: string[]): Promise<any> => (await axios.post('/backend/core/task?_method=delete', data)).data;
export const queryProcessModelList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/process-model', { params })).data;
export const queryProcessModel = async (id: string): Promise<any> => (await axios.get(`/backend/core/process-model/${id}`)).data;
export const queryProcessModelXml = async (id: string): Promise<any> => (await axios.get(`/backend/core/process-model/xml/${id}`)).data;
export const createProcessModel = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/process-model', data)).data;
export const updateProcessModel = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/process-model?_method=put', data)).data;
export const updateProcessModelXml = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/process-model/xml?_method=put', data)).data;
export const validateProcessModelXml = async (xml: string): Promise<any> =>
(await axios.post('/backend/core/process-model/xml/validate', xml, { headers: { 'Content-Type': 'text/plain' } })).data;
export const deployProcessModel = async (id: string): Promise<any> => (await axios.post(`/backend/core/process-model/deploy/${id}`)).data;
export const deleteProcessModel = async (data: string[]): Promise<any> => (await axios.post('/backend/core/process-model?_method=delete', data)).data;
export const queryProcessDefinitionList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/process-definition', { params })).data;
export const queryProcessDefinitionXml = async (id: string): Promise<any> => (await axios.get(`/backend/core/process-definition/xml/${id}`)).data;
export const deleteProcessDefinition = async (data: string[]): Promise<any> => (await axios.post('/backend/core/process-definition?_method=delete', data)).data;
export const queryProcessInstanceList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/process-instance', { params })).data;
export const queryProcessTaskList = async (instanceId: string): Promise<any> => (await axios.get(`/backend/core/process-instance/task/${instanceId}`)).data;
export const queryProcessBackActivityList = async (taskId: string): Promise<any> => (await axios.get(`/backend/core/process-instance/back-activity/${taskId}`)).data;
export const deleteProcessInstance = async (data: string[]): Promise<any> => (await axios.post('/backend/core/process-instance?_method=delete', data)).data;
export const queryProcessHistoryList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/process-history', { params })).data;
export const deleteProcessHistory = async (data: string[]): Promise<any> => (await axios.post('/backend/core/process-history?_method=delete', data)).data;
export const queryTaskFormExists = async (taskIds: string[]): Promise<any> => (await axios.post('/backend/core/process-form/task-form-exists', taskIds)).data;
export const queryTaskFormProperties = async (taskId: string): Promise<any> => (await axios.get('/backend/core/process-form/task-form-properties', { params: { taskId } })).data;
export const queryStartFormProperties = async (processKey: string): Promise<any> =>
(await axios.get('/backend/core/process-form/start-form-properties', { params: { processKey } })).data;
export const querySensitiveWordPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/sensitive-word', { params })).data;
export const querySensitiveWord = async (id: string): Promise<any> => (await axios.get(`/backend/core/sensitive-word/${id}`)).data;
export const createSensitiveWord = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/sensitive-word', data)).data;
export const updateSensitiveWord = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/sensitive-word?_method=put', data)).data;
export const deleteSensitiveWord = async (data: string[]): Promise<any> => (await axios.post('/backend/core/sensitive-word?_method=delete', data)).data;
export const validateSensitiveWord = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/sensitive-word/validate-sensitive-word', data)).data;
export const queryErrorWordPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/error-word', { params })).data;
export const queryErrorWord = async (id: string): Promise<any> => (await axios.get(`/backend/core/error-word/${id}`)).data;
export const createErrorWord = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/error-word', data)).data;
export const updateErrorWord = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/error-word?_method=put', data)).data;
export const deleteErrorWord = async (data: string[]): Promise<any> => (await axios.post('/backend/core/error-word?_method=delete', data)).data;
export const validateErrorWord = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/error-word/validate-error-word', data)).data;
export const importDataTestConnection = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/import-data/test-connection', data)).data;
export const importDataChannel = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/import-data/import-channel', data)).data;
export const importDataArticle = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/ext/import-data/import-article', data)).data;
export const importDataDeleteCorrespond = async (): Promise<any> => (await axios.post('/backend/ext/import-data/delete-correspond?_method=delete')).data;

57
src/api/user.ts Normal file
View File

@ -0,0 +1,57 @@
import axios from '@/utils/request';
export const queryOrgList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/org', { params })).data;
export const queryOrg = async (id: string): Promise<any> => (await axios.get(`/backend/core/org/${id}`)).data;
export const createOrg = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/org', data)).data;
export const updateOrg = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/org?_method=put', data)).data;
export const updateOrgPermission = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/org/permission?_method=put', data)).data;
export const moveOrg = async (fromId: string, toId: string, type: 'inner' | 'before' | 'after'): Promise<any> =>
(await axios.post('/backend/core/org/move?_method=put', { fromId, toId, type })).data;
export const tidyTreeOrg = async (): Promise<any> => (await axios.post('/backend/core/org/tidy-tree?_method=put')).data;
export const deleteOrg = async (data: string[]): Promise<any> => (await axios.post('/backend/core/org?_method=delete', data)).data;
export const queryOrgPermissions = async (): Promise<any> => (await axios.get('/backend/core/org/permissions')).data;
export const queryOrgArticlePermissions = async (orgId: string, siteId?: string): Promise<any> =>
(await axios.get('/backend/core/org/article-permissions', { params: { orgId, siteId } })).data;
export const queryOrgChannelPermissions = async (orgId: string, siteId?: string): Promise<any> =>
(await axios.get('/backend/core/org/channel-permissions', { params: { orgId, siteId } })).data;
export const queryOrgPermPermissions = async (orgId: string, global: boolean): Promise<any> =>
(await axios.get('/backend/core/org/org-permissions', { params: { orgId, global } })).data;
export const queryRoleList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/role', { params })).data;
export const queryRole = async (id: string): Promise<any> => (await axios.get(`/backend/core/role/${id}`)).data;
export const createRole = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/role', data)).data;
export const updateRole = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/role?_method=put', data)).data;
export const updateRolePermission = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/role/permission?_method=put', data)).data;
export const updateRoleOrder = async (data: string[]): Promise<any> => (await axios.post('/backend/core/role/order?_method=put', data)).data;
export const deleteRole = async (data: string[]): Promise<any> => (await axios.post('/backend/core/role?_method=delete', data)).data;
export const queryRoleArticlePermissions = async (roleId: string, siteId?: string): Promise<any> =>
(await axios.get('/backend/core/role/article-permissions', { params: { roleId, siteId } })).data;
export const queryRoleChannelPermissions = async (roleId: string, siteId?: string): Promise<any> =>
(await axios.get('/backend/core/role/channel-permissions', { params: { roleId, siteId } })).data;
export const queryRoleOrgPermissions = async (roleId: string, siteId?: string): Promise<any> =>
(await axios.get('/backend/core/role/org-permissions', { params: { roleId, siteId } })).data;
export const roleScopeNotAllowed = async (scope: number, roleId: string): Promise<any> =>
(await axios.get('/backend/core/role/scope-not-allowed', { params: { scope, roleId } })).data;
export const queryGroupList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/group', { params })).data;
export const queryGroup = async (id: string): Promise<any> => (await axios.get(`/backend/core/group/${id}`)).data;
export const createGroup = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/group', data)).data;
export const updateGroup = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/group?_method=put', data)).data;
export const updateGroupPermission = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/group/permission?_method=put', data)).data;
export const updateGroupOrder = async (data: string[]): Promise<any> => (await axios.post('/backend/core/group/order?_method=put', data)).data;
export const deleteGroup = async (data: string[]): Promise<any> => (await axios.post('/backend/core/group?_method=delete', data)).data;
export const groupAccessPermissions = async (groupId: string, siteId?: string): Promise<any> =>
(await axios.get('/backend/core/group/access-permissions', { params: { groupId, siteId } })).data;
export const queryUserPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/user', { params })).data;
export const queryUserList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/user/list', { params })).data;
export const queryUser = async (id: string): Promise<any> => (await axios.get(`/backend/core/user/${id}`)).data;
export const createUser = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/user', data)).data;
export const updateUser = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/user?_method=put', data)).data;
export const updateUserPermission = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/user/permission?_method=put', data)).data;
export const updateUserPassword = async (id: string, password: string): Promise<any> => (await axios.post('/backend/core/user/password?_method=put', { id, password })).data;
export const updateUserStatus = async (ids: string[], status: number): Promise<any> => (await axios.post('/backend/core/user/status?_method=put', { ids, status })).data;
export const deleteUser = async (data: string[]): Promise<any> => (await axios.post('/backend/core/user?_method=delete', data)).data;
export const usernameExist = async (username?: string): Promise<any> => (await axios.get('/backend/core/user/username-exist', { params: { username } })).data;
export const emailExist = async (email?: string): Promise<any> => (await axios.get('/backend/core/user/email-exist', { params: { email } })).data;
export const mobileExist = async (mobile?: string): Promise<any> => (await axios.get('/backend/core/user/mobile-exist', { params: { mobile } })).data;

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -0,0 +1,36 @@
<script setup lang="ts">
import { ref, watchEffect } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { compile } from 'path-to-regexp';
defineOptions({
name: 'BreadCrumb',
});
const router = useRouter();
const route = useRoute();
const itemList = ref<any[]>([]);
const pathCompile = (path: string) => {
const { params } = route;
const toPath = compile(path);
return toPath(params);
};
const handleLink = (item: any) => {
const { redirect, path } = item;
router.push(redirect || pathCompile(path));
};
watchEffect(() => {
itemList.value = route.matched.filter((item) => item.meta?.title);
});
</script>
<template>
<el-breadcrumb separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in itemList" :key="item.path">
<span v-if="index === itemList.length - 1" class="text-gray-400">{{ $t(item.meta.title) }}</span>
<a v-else @click.prevent="() => handleLink(item)">{{ $t(item.meta.title) }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>

View File

@ -0,0 +1,287 @@
<script setup lang="ts">
import { computed, onMounted, PropType, ref, toRefs, watch } from 'vue';
import { Plus, Delete } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
import _ from 'lodash';
import { perm } from '@/stores/useCurrentUser';
const CONTINUOUS_SETTINGS = 'ujcms_continuous_settings';
function fetchContinuous(): Record<string, boolean> {
const settings = localStorage.getItem(CONTINUOUS_SETTINGS);
return settings ? JSON.parse(settings) : {};
}
function storeContinuous(settings: Record<string, boolean>) {
localStorage.setItem(CONTINUOUS_SETTINGS, JSON.stringify(settings));
}
function getContinuous(name: string) {
const settings = fetchContinuous();
return settings[name] ?? false;
}
function setContinuous(name: string, continuous: boolean) {
const settings = fetchContinuous();
settings[name] = continuous;
storeContinuous(settings);
}
const props = defineProps({
modelValue: { type: Boolean, required: true },
values: { type: Object, required: true },
name: { type: String, required: true },
beanId: { type: [Number, String], default: null },
beanIds: { type: Array as PropType<string[] | number[]>, required: true },
initValues: { type: Function as PropType<(bean?: any, isEditor?: boolean) => any>, required: true },
toValues: { type: Function as PropType<(bean: any) => any>, required: true },
queryBean: { type: Function as PropType<(id: any) => Promise<any>>, required: true },
createBean: { type: Function as PropType<(bean: any) => Promise<any>>, required: true },
updateBean: { type: Function as PropType<(bean: any) => Promise<any>>, required: true },
deleteBean: { type: Function as PropType<(ids: any[]) => Promise<any>>, required: true },
disableDelete: { type: Function as PropType<(bean: any) => boolean>, default: null },
disableEdit: { type: Function as PropType<(bean: any) => boolean>, default: null },
beforeValidate: { type: Function as PropType<(values: any) => Promise<boolean> | void>, default: null },
preventSubmit: { type: Function as PropType<(values: any) => Promise<boolean>>, default: null },
addable: { type: Boolean, default: true },
action: { type: String as PropType<'add' | 'copy' | 'edit'>, default: 'edit' },
showId: { type: Boolean, default: true },
perms: { type: String, default: null },
focus: { type: Object, default: null },
large: { type: Boolean, default: false },
labelPosition: { type: String as PropType<'top' | 'right' | 'left'>, default: 'right' },
labelWidth: { type: String, default: '150px' },
});
const emit = defineEmits({
'update:modelValue': null,
'update:values': null,
finished: null,
beanChange: null,
beforeSubmit: null,
});
const { name, beanId, beanIds, focus, values, action, modelValue: visible } = toRefs(props);
const { t } = useI18n();
const loading = ref<boolean>(false);
const buttonLoading = ref<boolean>(false);
const continuous = ref<boolean>(getContinuous(name.value));
const form = ref<any>();
const bean = ref<any>(props.initValues());
const origValues = ref<any>();
const id = ref<any>();
const ids = ref<Array<any>>([]);
const isEdit = computed(() => id.value != null && action.value === 'edit');
const unsaved = computed(() => {
//
// if (!_.isEqual(origValues.value, values.value)) {
// console.log(JSON.stringify(origValues.value));
// console.log(JSON.stringify(values.value));
// }
return !loading.value && !_.isEqual(origValues.value, values.value);
});
const disabled = computed(() => props.disableEdit?.(bean.value) ?? false);
const title = computed(() => `${name.value} - ${isEdit.value ? `${t(disabled.value ? 'detail' : 'edit')} (ID: ${id.value})` : `${t('add')}`}`);
const loadBean = async () => {
loading.value = true;
try {
bean.value = id.value != null ? await props.queryBean(id.value) : props.initValues(values.value, isEdit.value);
origValues.value = id.value != null ? props.toValues(bean.value) : bean.value;
emit('update:values', _.cloneDeep(origValues.value));
emit('beanChange', bean.value);
form.value?.resetFields();
} finally {
loading.value = false;
}
};
onMounted(() => emit('update:values', props.initValues()));
watch(visible, () => {
if (visible.value) {
ids.value = beanIds.value;
if (id.value !== beanId.value) {
id.value = beanId.value;
} else {
loadBean();
}
}
});
watch(id, () => {
loadBean();
});
watch(continuous, () => setContinuous(name.value, continuous.value));
const index = computed(() => ids.value.indexOf(id.value));
const hasPrev = computed(() => index.value > 0);
const hasNext = computed(() => index.value < ids.value.length - 1);
const handlePrev = () => {
if (hasPrev.value) {
id.value = ids.value[index.value - 1];
}
};
const handleNext = () => {
if (hasNext.value) {
id.value = ids.value[index.value + 1];
}
};
const handleAdd = () => {
focus.value?.focus?.();
id.value = undefined;
};
const handleCancel = () => {
emit('update:modelValue', false);
};
const handleDelete = async () => {
buttonLoading.value = true;
try {
await props.deleteBean([id.value]);
if (!continuous.value) emit('update:modelValue', false);
if (hasNext.value) {
handleNext();
ids.value.splice(index.value - 1, 1);
} else if (hasPrev.value) {
handlePrev();
ids.value.splice(index.value + 1, 1);
} else {
emit('update:modelValue', false);
}
ElMessage.success(t('success'));
emit('finished');
} finally {
buttonLoading.value = false;
}
};
const resetOrigValues = () => {
origValues.value = props.toValues(values.value);
};
const handleSubmit = async (stay = false) => {
await props.beforeValidate?.(values.value);
form.value.validate(async (valid: boolean) => {
if (!valid) return;
buttonLoading.value = true;
try {
if ((await props.preventSubmit?.(values.value)) ?? false) {
return;
}
emit('beforeSubmit', values.value);
if (isEdit.value) {
await props.updateBean(values.value);
resetOrigValues();
} else {
await props.createBean(values.value);
focus.value?.focus?.();
emit('update:values', props.initValues(values.value, isEdit.value));
form.value.resetFields();
}
ElMessage.success(t('success'));
if (!continuous.value && !stay) emit('update:modelValue', false);
emit('finished', bean.value);
} finally {
buttonLoading.value = false;
}
});
};
const submit = (
executor: (
values: any,
payload: { isEdit: boolean; continuous: boolean; form: any; props: any; focus: any; loadBean: () => Promise<any>; resetOrigValues: () => void; emit: any },
) => Promise<boolean | undefined>,
) => {
form.value.validate(async (valid: boolean) => {
if (!valid) return;
buttonLoading.value = true;
try {
if ((await props.preventSubmit?.(values.value)) ?? false) {
return;
}
emit('beforeSubmit', values.value);
const stay = await executor(values.value, {
isEdit: isEdit.value,
continuous: continuous.value,
form: form.value,
props,
focus: focus.value,
loadBean,
resetOrigValues,
emit,
});
if (!continuous.value && !stay) emit('update:modelValue', false);
emit('finished', bean.value);
} finally {
buttonLoading.value = false;
}
});
};
const remove = async (
executor: (values: any, payload: { isEdit: boolean; continuous: boolean; form: any; props: any; focus: any; loadBean: () => Promise<any>; emit: any }) => Promise<any>,
) => {
buttonLoading.value = true;
try {
await executor(values.value, { isEdit: isEdit.value, continuous: continuous.value, form: form.value, props, focus: focus.value, loadBean, emit });
if (!continuous.value) {
emit('update:modelValue', false);
}
if (hasNext.value) {
handleNext();
ids.value.splice(index.value - 1, 1);
} else if (hasPrev.value) {
handlePrev();
ids.value.splice(index.value + 1, 1);
} else {
emit('update:modelValue', false);
}
ElMessage.success(t('success'));
emit('finished');
} finally {
buttonLoading.value = false;
}
};
defineExpose({ form, submit, remove, defaultSubmit: handleSubmit });
</script>
<template>
<el-dialog
:title="title"
:close-on-click-modal="!unsaved"
:model-value="modelValue"
:close-on-press-escape="!unsaved"
:width="large ? '98%' : '768px'"
:top="large ? '16px' : '8vh'"
@update:model-value="(event) => $emit('update:modelValue', event)"
@opened="() => !isEdit && focus?.focus()"
>
<template #header>
{{ name }} -
<span v-if="isEdit">
{{ $t(disabled ? 'detail' : 'edit') }}
<span v-if="showId">(ID: {{ id }})</span>
</span>
<span v-else>{{ $t('add') }}</span>
</template>
<div v-loading="loading || buttonLoading" class="space-x-2">
<el-button v-if="isEdit && addable" :disabled="perm(`${perms}:create`)" type="primary" :icon="Plus" @click="handleAdd">{{ $t('add') }}</el-button>
<slot name="header-action" :bean="bean" :is-edit="isEdit" :disabled="disabled" :unsaved="unsaved" :disable-delete="disableDelete" :handle-delete="handleDelete">
<el-popconfirm v-if="isEdit" :title="$t('confirmDelete')" @confirm="() => handleDelete()">
<template #reference>
<el-button :disabled="disableDelete?.(bean) || perm(`${perms}:delete`)" :icon="Delete">{{ $t('delete') }}</el-button>
</template>
</el-popconfirm>
</slot>
<el-button-group v-if="isEdit">
<el-button :disabled="!hasPrev" @click="handlePrev">{{ $t('form.prev') }}</el-button>
<el-button :disabled="!hasNext" @click="handleNext">{{ $t('form.next') }}</el-button>
</el-button-group>
<el-button type="primary" @click="handleCancel">{{ $t('back') }}</el-button>
<el-tooltip :content="$t('form.continuous')" placement="top">
<el-switch v-model="continuous" size="small"></el-switch>
</el-tooltip>
<el-tag v-if="unsaved" type="danger">{{ $t('form.unsaved') }}</el-tag>
<slot name="header-status" :bean="bean" :is-edit="isEdit" :disabled="disabled"></slot>
</div>
<el-form ref="form" :class="['mt-5', 'pr-5']" :model="values" :disabled="disabled" :label-width="labelWidth" :label-position="labelPosition" scroll-to-error>
<slot :bean="bean" :is-edit="isEdit" :disabled="disabled"></slot>
<div v-if="!disabled" v-loading="buttonLoading">
<slot name="footer-action" :bean="bean" :is-edit="isEdit" :disabled="disabled" :handle-submit="handleSubmit">
<el-button :disabled="perm(isEdit ? `${perms}:update` : `${perms}:create`)" type="primary" native-type="submit" @click.prevent="() => handleSubmit()">
{{ $t('save') }}
</el-button>
</slot>
</div>
</el-form>
</el-dialog>
</template>

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
import { QuestionFilled } from '@element-plus/icons-vue';
defineProps({
label: { type: String, default: null },
tooltip: { type: String, default: null },
help: { type: Boolean, default: false },
message: { type: String, default: '' },
fixWidth: { type: Boolean, default: true },
});
</script>
<template>
<div class="inline-flex items-center">
<div class="overflow-hidden text-clip whitespace-nowrap" :title="label ?? $t(message)">{{ label ?? $t(message) }}</div>
<el-tooltip v-if="help" :content="tooltip ?? $t(message + '.tooltip')" placement="top">
<el-icon class="text-base align-text-top"><QuestionFilled /></el-icon>
</el-tooltip>
</div>
</template>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import { Top, Bottom, ArrowUp, ArrowDown } from '@element-plus/icons-vue';
defineProps({
disabled: { type: Boolean, required: true },
});
defineEmits({
move: null,
});
</script>
<template>
<el-button-group>
<el-button :disabled="disabled" :icon="Top" @click="() => $emit('move', 'top')">{{ $t('moveTop') }}</el-button>
<el-button :disabled="disabled" :icon="ArrowUp" @click="() => $emit('move', 'up')">{{ $t('moveUp') }}</el-button>
<el-button :disabled="disabled" :icon="ArrowDown" @click="() => $emit('move', 'down')">{{ $t('moveDown') }}</el-button>
<el-button :disabled="disabled" :icon="Bottom" @click="() => $emit('move', 'bottom')">{{ $t('moveBottom') }}</el-button>
</el-button-group>
</template>

View File

@ -0,0 +1,83 @@
<script setup lang="ts">
import { useSlots, watch, provide, computed, ref, toRefs, Slots } from 'vue';
import { Plus, Minus, Search, Refresh } from '@element-plus/icons-vue';
import QueryInput from './QueryInput.vue';
const props = defineProps({ params: { type: Object, required: true } });
const { params } = toRefs(props);
const slots: Readonly<Slots> = useSlots();
provide('params', params);
defineEmits({
search: null,
reset: null,
});
const inputs = computed<any[]>(
() =>
slots
.default?.()
.flatMap((item: any) => (item.children?.length > 0 ? item.children : item))
.flatMap((item: any) => (item.children?.length > 0 ? item.children : item))
.filter((item: any) => item.props?.name != null) ?? [],
);
const data = computed<any[]>(() => inputs.value.map((item) => ({ label: item.props?.label, name: item.props?.name })));
const names = ref<string[]>([]);
const remains = computed(() => data.value.filter((it) => !names.value.includes(it.name)));
const clearParams = () => {
Object.keys(params.value).forEach((key) => {
if (!names.value.includes(key) && names.value.findIndex((item) => item.split(',').includes(key)) === -1) {
delete params.value[key];
}
});
};
watch(
data,
() => {
const [first] = data.value;
if (names.value.length > 0) {
const sourceNames = data.value.map((item: any) => item.name);
names.value.filter((name: string) => sourceNames.includes(name));
Object.keys(params.value).forEach((key) => {
if (!sourceNames.includes(key) && sourceNames.findIndex((item) => item.split(',').includes(key)) === -1) {
delete params.value[key];
}
});
}
if (names.value.length === 0) {
names.value = [first.name];
}
},
{ deep: true, immediate: true },
);
const handelRow = (index: number) => {
if (index === 0) {
const [item] = remains.value;
names.value[names.value.length] = item.name;
} else {
names.value.splice(index, 1);
clearParams();
}
};
</script>
<template>
<form class="flex">
<div class="space-y-1">
<div v-for="(name, index) in names" :key="name" class="flex">
<el-button :icon="index == 0 ? Plus : Minus" :disabled="index <= 0 && remains.length <= 0" circle @click="() => handelRow(index)"></el-button>
<el-select v-model="names[index]" class="w-44" @change="() => clearParams()">
<el-option v-for="item in data.filter((it) => it.name === names[index] || remains.includes(it))" :key="item.name" :label="item.label" :value="item.name"></el-option>
</el-select>
<query-input :inputs="inputs" :name="names[index]"></query-input>
</div>
</div>
<div>
<el-button-group class="ml-2">
<el-button native-type="submit" :icon="Search" @click.prevent="() => $emit('search')">{{ $t('search') }}</el-button>
<el-button :icon="Refresh" @click="() => $emit('reset')">{{ $t('reset') }}</el-button>
</el-button-group>
</div>
</form>
</template>

View File

@ -0,0 +1,12 @@
<script lang="ts">
import { defineComponent, toRefs } from 'vue';
export default defineComponent({
name: 'QueryInput',
props: { inputs: { type: Array, required: true }, name: { type: String, required: true } },
setup(props) {
const { inputs, name } = toRefs(props);
return () => inputs.value.find((item: any) => item.props.name === name.value);
},
});
</script>

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import { inject, PropType, ref, toRefs } from 'vue';
defineOptions({
name: 'QueryItem',
});
const props = defineProps({
label: { type: String, required: true },
name: { type: String, required: true },
// 'string' | 'date' | 'datetime' | 'number'
type: { type: String, default: null },
options: { type: Object as PropType<Array<{ label: string; value: string | number }>>, default: null },
multiple: { type: Boolean, default: true },
});
const params = inject<any>('params');
const { name } = toRefs(props);
const [firstName, secondName] = name.value.split(',');
const first = ref<string>(firstName);
const second = ref<string>(secondName);
</script>
<template>
<slot>
<div v-if="type === 'number'" class="inline-block">
<el-input-number v-model="params[first]" :placeholder="$t('begin.number')" class="w-48"></el-input-number>
<el-input-number v-model="params[second]" :placeholder="$t('end.number')" class="w-48"></el-input-number>
</div>
<el-date-picker
v-else-if="type === 'date'"
v-model="params[name]"
type="daterange"
:start-placeholder="$t('begin.date')"
:end-placeholder="$t('end.date')"
:editable="false"
class="w-96"
></el-date-picker>
<el-date-picker
v-else-if="type === 'datetime'"
v-model="params[name]"
type="datetimerange"
:start-placeholder="$t('begin.date')"
:end-placeholder="$t('end.date')"
:editable="false"
class="w-96"
>
</el-date-picker>
<!--
<div v-else-if="type === 'date'" class="inline-block">
<el-date-picker v-model="params[first]" type="date" :placeholder="$t('begin.date')" class="w-48"></el-date-picker>
<el-date-picker v-model="params[second]" type="date" :placeholder="$t('end.date')" class="w-48"></el-date-picker>
</div>
<div v-else-if="type === 'datetime'" class="inline-block">
<el-date-picker v-model="params[first]" type="datetime" class="w-48"></el-date-picker>
<el-date-picker v-model="params[second]" type="datetime" class="w-48"></el-date-picker>
</div>
-->
<el-select v-else-if="options" v-model="params[name]" :multiple="multiple" class="w-96">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
<el-input v-else v-model="params[name]" class="w-96"></el-input>
</slot>
</template>

View File

@ -0,0 +1,2 @@
export { default as QueryForm } from './QueryForm.vue';
export { default as QueryItem } from './QueryItem.vue';

View File

@ -0,0 +1,52 @@
<script lang="ts">
import { computed, watch, defineComponent, toRefs } from 'vue';
import { useI18n } from 'vue-i18n';
import { useColumnSettingsStore, ColumnState } from '@/stores/columnSettingsStore';
export default defineComponent({
name: 'ColumnList',
props: { name: { type: String, required: true } },
setup(props, { slots }) {
const { name } = toRefs(props);
const { t } = useI18n();
const slotColumns = computed<any[]>(() => slots.default?.().flatMap((item: any) => (item.children?.length > 0 ? item.children : item)) ?? []);
//
const getColumnTitle = (columnProps: any) => {
// checkbox
if (columnProps?.type === 'selection') return t('table.selection');
return columnProps?.label;
};
const settingsStore = useColumnSettingsStore();
// el-table-column
const origins = computed<ColumnState[]>(() => slotColumns.value.map((column) => ({ title: getColumnTitle(column.props), display: column.props?.display !== 'none' })));
watch(
[name, origins],
() => {
settingsStore.setOriginSettings(name.value, origins.value);
},
{ deep: true, immediate: true },
);
const settings = computed<ColumnState[]>(() => settingsStore.getCurrentSettings(name.value));
const columns = computed(() =>
slotColumns.value
.filter((column) => {
const matched = settings.value.find((item) => getColumnTitle(column.props) === item.title);
return matched?.display ?? column.props?.display !== 'none';
})
.map((column) => ({ ...column, key: getColumnTitle(column.props) }))
.sort((a, b) => {
let indexA = settings.value.findIndex((item) => item.title === getColumnTitle(a));
if (indexA < 0) indexA = slotColumns.value.findIndex((item) => getColumnTitle(item) === getColumnTitle(a));
let indexB = settings.value.findIndex((item) => item.title === getColumnTitle(b));
if (indexB < 0) indexB = slotColumns.value.findIndex((item) => getColumnTitle(item) === getColumnTitle(b));
return indexA - indexB;
}),
);
return { columns };
},
render() {
return this.columns;
},
});
</script>

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import { toRefs, computed, ref } from 'vue';
import { Setting } from '@element-plus/icons-vue';
import { useColumnSettingsStore, mergeColumns, ColumnState } from '@/stores/columnSettingsStore';
const props = defineProps({ name: { type: String, required: true } });
const { name } = toRefs(props);
const settingsStore = useColumnSettingsStore();
const settings = computed<ColumnState[]>(() => settingsStore.getCurrentSettings(name.value));
const origins = computed<ColumnState[]>(() => settingsStore.getOriginSettings(name.value));
const merges = computed<ColumnState[]>(() => mergeColumns(settings.value, origins.value));
const visible = ref<boolean>(false);
const resetColumns = () => {
settingsStore.setCurrentSettings(name.value, mergeColumns([], origins.value));
};
</script>
<template>
<div class="inline-flex align-middle">
<el-tooltip :content="$t('table.columnsSetting')" placement="top">
<el-icon class="text-lg cursor-pointer text-gray-regular" @click="() => (visible = true)"><Setting /></el-icon>
</el-tooltip>
<el-drawer v-model="visible" :title="$t('table.columnsSetting')" :size="375">
<el-button class="mb-1" round @click="resetColumns">{{ $t('reset') }}</el-button>
<ul>
<li v-for="(column, index) in merges" :key="column.title" :divided="index === 0">
<el-checkbox v-model="column.display">{{ column.title }}</el-checkbox>
</li>
</ul>
</el-drawer>
</div>
</template>

View File

@ -0,0 +1,2 @@
export { default as ColumnSetting } from './ColumnSetting.vue';
export { default as ColumnList } from './ColumnList.vue';

View File

@ -0,0 +1,55 @@
import { reactive, toRef } from 'vue';
export interface ColumnState {
title: string;
display: boolean;
}
const COLUMN_SETTINGS = 'ujcms_column_settings';
function fetchColumnSettings(): Record<string, ColumnState[]> {
const settings = localStorage.getItem(COLUMN_SETTINGS);
return settings ? JSON.parse(settings) : {};
}
const originStore: Record<string, ColumnState[]> = reactive({});
const settingStore: Record<string, ColumnState[]> = reactive(fetchColumnSettings());
export function storeColumnSettings() {
localStorage.setItem(COLUMN_SETTINGS, JSON.stringify(settingStore));
}
export const getColumnOrigins = (name: string) => {
if (!originStore[name]) originStore[name] = [];
return toRef(originStore, name);
};
export const mergeColumns = (settings: ColumnState[], origins: ColumnState[]) => {
// 去除不存在的列
for (let i = 0, len = settings.length; i < len; ) {
if (origins.findIndex((column) => column.title === settings[i].title) === -1) {
settings.splice(i, 1);
len -= 1;
} else {
i += 1;
}
}
// 增加未记录的列
origins.forEach((column) => {
if (settings.findIndex((item) => item.title === column.title) === -1) {
settings.push({ ...column });
}
});
return settings;
};
export const setColumnOrigins = (name: string, origins: ColumnState[]) => {
originStore[name] = origins;
if (!settingStore[name]) settingStore[name] = [];
const settings = settingStore[name];
mergeColumns(settings, origins);
};
export const getColumnSettings = (name: string) => {
if (!settingStore[name]) settingStore[name] = [];
return toRef(settingStore, name);
};
// export const setColumnSettings = (name: string, settings: ColumnState[]) => {
// settingStore[name] = settings;
// };

View File

@ -0,0 +1,392 @@
<script lang="ts">
import { defineComponent, ref, toRefs, watch, onMounted, onBeforeUnmount, onActivated, onDeactivated, PropType } from 'vue';
import { useI18n } from 'vue-i18n';
import { useFormItem } from 'element-plus';
import { getAuthHeaders } from '@/utils/auth';
import { getSiteHeaders } from '@/utils/common';
import { currentUser } from '@/stores/useCurrentUser';
import { useSysConfigStore } from '@/stores/sysConfigStore';
import { imageUploadUrl, fileUploadUrl, mediaUploadUrl, fetchImage } from '@/api/config';
// https://www.tiny.cloud/docs/advanced/usage-with-module-loaders/webpack/webpack_es6_npm/
// https://github.com/tinymce/tinymce-vue/blob/main/src/main/ts/components/Editor.ts
// Import TinyMCE
import tinymce from 'tinymce';
// Default icons are required for TinyMCE 5.3 or above
import 'tinymce/icons/default';
// A theme is also required
import 'tinymce/themes/silver';
// Any plugins you want to use has to be imported
import 'tinymce/plugins/advlist';
// import 'tinymce/plugins/anchor';
// import 'tinymce/plugins/autolink';
import 'tinymce/plugins/autoresize';
import 'tinymce/plugins/autosave';
import 'tinymce/plugins/charmap';
import 'tinymce/plugins/code';
import 'tinymce/plugins/codesample';
import 'tinymce/plugins/directionality';
import 'tinymce/plugins/fullscreen';
import 'tinymce/plugins/hr';
// import 'tinymce/plugins/insertdatetime';
import 'tinymce/plugins/image';
import 'tinymce/plugins/imagetools';
import 'tinymce/plugins/link';
import 'tinymce/plugins/lists';
import 'tinymce/plugins/media';
// import 'tinymce/plugins/nonbreaking';
// import 'tinymce/plugins/noneditable';
import 'tinymce/plugins/pagebreak';
import 'tinymce/plugins/paste';
import 'tinymce/plugins/preview';
// import 'tinymce/plugins/print';
import 'tinymce/plugins/quickbars';
// import 'tinymce/plugins/save';
import 'tinymce/plugins/searchreplace';
// import 'tinymce/plugins/spellchecker';
// import 'tinymce/plugins/tabfocus';
import 'tinymce/plugins/table';
// import 'tinymce/plugins/template';
// import 'tinymce/plugins/textpattern';
// import 'tinymce/plugins/toc';
import 'tinymce/plugins/visualblocks';
import 'tinymce/plugins/visualchars';
// import 'tinymce/plugins/wordcount';
import './plugins/indent2em';
import './plugins/typesetting';
import { isTextarea, uuid, initEditor } from './utils';
export default defineComponent({
name: 'TinymceEditor',
props: {
id: { type: String, default: null },
modelValue: { type: String, default: '' },
disabled: { type: Boolean, default: false },
inline: { type: Boolean },
init: { type: Object, default: null },
modelEvents: { type: [String, Array], default: null },
plugins: { type: [String, Array] as PropType<string | string[]>, default: null },
toolbar: { type: [String, Array], default: null },
outputFormat: {
type: String as PropType<'html' | 'text'>,
default: 'html',
validator: (prop: string) => prop === 'html' || prop === 'text',
},
},
emits: ['update:modelValue', 'input', 'change', 'blur', 'keydown'],
setup(props, ctx) {
const { disabled, modelValue } = toRefs(props);
const { t } = useI18n();
const sysConfig = useSysConfigStore();
const element = ref<any>();
const vueEditor = ref<any>();
const elementId: string = props.id || uuid('tiny-vue');
const inlineEditor: boolean = (props.init && props.init.inline) || props.inline;
let mounting = true;
const { formItem } = useFormItem();
const initWrapper = (): void => {
let publicPath = import.meta.env.VITE_PUBLIC_PATH;
if (publicPath.endsWith('/')) {
publicPath = publicPath.substring(0, publicPath.length - 1);
}
const ep2 = currentUser.epRank >= 2;
const finalInit = {
base_url: '/tinymce',
language_url: `${publicPath}/tinymce/langs/zh_CN.js`,
language: 'zh_CN',
skin: 'oxide',
skin_url: `${publicPath}/tinymce/skins/ui/oxide`,
// '/tinymce/skins/content/default/content.min.css' fontselect
content_css: [`${publicPath}/tinymce/skins/ui/oxide/content.min.css`, `${publicPath}/tinymce/skins/content/default/content.min.css`],
//
content_style: 'body { font-size: 14px; }',
menubar: false,
// 'floating' '...' 'warp'
// toolbar_mode: 'wrap',
plugins:
'advlist autoresize autosave charmap code codesample directionality fullscreen hr image imagetools lists link media pagebreak paste preview quickbars ' +
`searchreplace table visualblocks visualchars indent2em ${ep2 ? 'typesetting' : ''}`,
toolbar:
`fullscreen code ${
ep2 ? '| typesetting' : ''
} | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | selectall removeformat pastetext | ` +
'quickimage media | blockquote codesample table | bullist numlist | indent2em outdent indent lineheight | forecolor backcolor | fontselect fontsizeselect formatselect | ' +
'superscript subscript charmap | hr | ltr rtl | visualblocks visualchars | restoredraft undo redo | preview searchreplace',
font_formats:
'宋体=SimSun; 微软雅黑=Microsoft YaHei; 楷体=SimKai,KaiTi; 黑体=SimHei; 隶书=SimLi,LiSu; Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;' +
'Arial Black=arial black,avant garde;Comic Sans MS=comic sans ms,sans-serif;Helvetica=helvetica;Impact=impact,chicago;Times New Roman=times new roman,times',
fontsize_formats: '8px 10px 12px 14px 16px 18px 24px 36px 48px 64px 72px 96px',
quickbars_selection_toolbar: 'bold italic | h2 h3 blockquote | link',
quickbars_insert_toolbar: false,
paste_data_images: true,
image_uploadtab: false,
image_advtab: true,
image_caption: true,
images_file_types: sysConfig.upload.imageTypes,
min_height: 300,
max_height: 500,
convert_urls: false,
autosave_ask_before_unload: false,
...props.init,
images_upload_handler(blobInfo: any, success: any, failure: any, progress: any) {
const fileSizeLimitByte = sysConfig.upload.imageLimitByte;
if (fileSizeLimitByte > 0 && blobInfo.blob().size > fileSizeLimitByte) {
failure(t('error.fileMaxSize', { size: `${fileSizeLimitByte / 1024 / 1024}MB` }), { remove: true });
return;
}
const xhr = new XMLHttpRequest();
xhr.open('POST', imageUploadUrl);
xhr.upload.onprogress = (e) => {
progress((e.loaded / e.total) * 100);
};
xhr.onload = () => {
if (xhr.status === 403) {
failure(`HTTP Error: ${xhr.status}`, { remove: true });
return;
}
if (xhr.status < 200 || xhr.status >= 300) {
failure(`HTTP Error: ${xhr.status}`, { remove: true });
return;
}
const json = JSON.parse(xhr.responseText);
if (!json || typeof json.url !== 'string') {
failure(`Invalid JSON: ${xhr.responseText}`, { remove: true });
return;
}
success(json.url);
};
xhr.onerror = () => {
failure(`Image upload failed due to a XHR Transport error. Code: ${xhr.status}`, { remove: true });
};
const formData = new FormData();
formData.append('file', blobInfo.blob(), blobInfo.filename());
//
formData.append('isWatermark', 'true');
Object.entries(getAuthHeaders()).forEach(([key, value]: any) => xhr.setRequestHeader(key, value));
Object.entries(getSiteHeaders()).forEach(([key, value]: any) => xhr.setRequestHeader(key, value));
xhr.send(formData);
},
file_picker_callback(callback: any, val: any, meta: any) {
const input = document.createElement('input');
input.setAttribute('type', 'file');
let fileSizeLimtByte = 0;
let uploadUrl: string;
if (meta.filetype === 'image') {
fileSizeLimtByte = sysConfig.upload.imageLimitByte;
input.setAttribute('accept', sysConfig.upload.imageInputAccept);
uploadUrl = imageUploadUrl;
// input.setAttribute('accept', 'image/*');
} else if (meta.filetype === 'media') {
fileSizeLimtByte = sysConfig.upload.mediaLimitByte;
input.setAttribute('accept', sysConfig.upload.mediaInputAccept);
uploadUrl = mediaUploadUrl;
// input.setAttribute('accept', 'video/*');
} else {
fileSizeLimtByte = sysConfig.upload.fileLimitByte;
input.setAttribute('accept', sysConfig.upload.fileInputAccept);
uploadUrl = fileUploadUrl;
}
/*
Note: In modern browsers input[type="file"] is functional without
even adding it to the DOM, but that might not be the case in some older
or quirky browsers like IE, so you might want to add it to the DOM
just in case, and visually hide it. And do not forget do remove it
once you do not need it anymore.
*/
input.onchange = (event: Event) => {
const { files } = event.target as HTMLInputElement;
const file = files?.item(0);
if (!file) return;
if (fileSizeLimtByte > 0 && file.size > fileSizeLimtByte) {
tinymce.activeEditor.windowManager.alert(t('error.fileMaxSize', { size: `${fileSizeLimtByte / 1024 / 1024}MB` }));
return;
}
const xhr = new XMLHttpRequest();
xhr.open('POST', uploadUrl);
// xhr.upload.onprogress = (e) => {
// progress((e.loaded / e.total) * 100);
// };
xhr.onload = () => {
if (xhr.status === 403) {
tinymce.activeEditor.windowManager.alert(`HTTP Error: ${xhr.status}`);
return;
}
if (xhr.status < 200 || xhr.status >= 300) {
tinymce.activeEditor.windowManager.alert(`HTTP Error: ${xhr.status}`);
return;
}
const json = JSON.parse(xhr.responseText);
if (!json || typeof json.url !== 'string') {
tinymce.activeEditor.windowManager.alert(`Invalid JSON: ${xhr.responseText}`);
return;
}
if (meta.filetype === 'image') {
callback(json.url, { alt: '' });
} else if (meta.filetype === 'media') {
callback(json.url);
// callback('movie.mp4', { source2: 'alt.ogg', poster: 'image.jpg' });
} else {
callback(json.url, { text: json.name });
}
};
xhr.onerror = () => {
tinymce.activeEditor.windowManager.alert(`Image upload failed due to a XHR Transport error. Code: ${xhr.status}`);
};
const formData = new FormData();
formData.append('file', file, file.name);
Object.entries(getAuthHeaders()).forEach(([key, value]: any) => xhr.setRequestHeader(key, value));
xhr.send(formData);
};
input.click();
},
readonly: props.disabled,
selector: `#${elementId}`,
// plugins: mergePlugins(props.init && props.init.plugins, props.plugins),
// toolbar: props.toolbar || (props.init && props.init.toolbar),
inline: inlineEditor,
setup: (editor: any) => {
vueEditor.value = editor;
editor.on('init', (event: Event) => initEditor(event, props, ctx, editor, modelValue, formItem));
if (props.init && typeof props.init.setup === 'function') {
props.init.setup(editor);
}
const replaceString = (content: string, search: string, replace: string): string => {
let index = 0;
do {
index = content.indexOf(search, index);
if (index !== -1) {
content = content.substring(0, index) + replace + content.substring(index + search.length);
index += replace.length - search.length + 1;
}
} while (index !== -1);
return content;
};
const transparentSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
const replaceImageUrl = (content: string, targetUrl: string, replacementUrl: string): string => {
const replacementString = `src="${replacementUrl}"${replacementUrl === transparentSrc ? ' data-mce-placeholder="1"' : ''}`;
content = replaceString(content, `src="${targetUrl}"`, replacementString);
content = replaceString(content, 'data-mce-src="' + targetUrl + '"', 'data-mce-src="' + replacementUrl + '"');
return content;
};
const replaceUrlInUndoStack = (targetUrl: string, replacementUrl: string) => {
editor.undoManager.data.forEach((level: any) => {
if (level.type === 'fragmented') {
level.fragments = level.fragments.map((fragment: any) => replaceImageUrl(fragment, targetUrl, replacementUrl));
} else {
level.content = replaceImageUrl(level.content, targetUrl, replacementUrl);
}
});
};
editor.on('SetContent', ({ content, format, paste }: { content: string; format?: string; paste?: boolean; selection?: boolean }) => {
if (format === 'html' && paste && content.includes('src="')) {
const images = Array.from(editor.getBody().getElementsByTagName('img')).filter((img: any) => {
const src = img.src;
if (src.startsWith(sysConfig.base.uploadUrlPrefix)) {
return false;
}
if (img.hasAttribute('data-mce-bogus')) {
return false;
}
if (img.hasAttribute('data-mce-placeholder')) {
return false;
}
if (!src || src === transparentSrc) {
return false;
}
if (src.indexOf('blob:') === 0) {
return false;
}
if (src.indexOf('data:') === 0) {
return false;
}
const host = new URL(src).host;
for (let domain of sysConfig.security.ssrfList) {
if (domain === '*' || host === domain || host.endsWith('.' + domain)) {
return true;
}
}
return false;
});
images.forEach(async (image: any) => {
const data = await fetchImage(image.src);
if (data.status === -1) {
console.warn(data.message);
return;
}
const resultUri = data.result.url;
const src = editor.convertURL(resultUri, 'src');
replaceUrlInUndoStack(image.src, resultUri);
editor.$(image).attr({
src: resultUri,
'data-mce-src': src,
});
});
}
});
},
branding: false,
};
if (isTextarea(element.value)) {
element.value.style.visibility = '';
}
tinymce.init({ toolbar_mode: 'sliding', ...finalInit });
mounting = false;
};
watch(disabled, () => {
if (vueEditor.value != null) {
vueEditor.value.setMode(disabled.value ? 'readonly' : 'design');
}
});
onMounted(async () => {
initWrapper();
});
onBeforeUnmount(() => {
tinymce.remove(vueEditor.value);
});
if (!inlineEditor) {
onActivated(() => {
if (!mounting) {
initWrapper();
}
});
onDeactivated(() => {
tinymce.remove(vueEditor.value);
});
}
return { element, elementId, editor: vueEditor };
},
});
</script>
<template>
<div>
<textarea :id="elementId" ref="element"></textarea>
</div>
</template>

View File

@ -0,0 +1,2 @@
import Tinymce from './Tinymce.vue';
export default Tinymce;

View File

@ -0,0 +1,10 @@
import { Editor } from 'tinymce';
import { doAction } from '../core/actions';
const register = (editor: Editor, defaultOptions: any): void => {
editor.addCommand(defaultOptions.id, () => {
doAction(editor, defaultOptions);
});
};
export { register };

View File

@ -0,0 +1,24 @@
import { Editor } from 'tinymce';
const doAction = (editor: Editor, defaultOptions: any): void => {
editor.formatter.toggle(defaultOptions.id);
editor.nodeChanged();
// const { dom, selection } = editor;
// const blocks = selection.getSelectedBlocks();
// const styleName = 'text-indent';
// let textIndentExists: boolean;
// tinymce.each(blocks, (block: Element) => {
// const parents = dom.getParents(block, undefined, dom.getRoot());
// const parent = parents[parents.length - 1];
// if (!['p', 'div'].includes(parent.nodeName.toLowerCase())) {
// return;
// }
// if (textIndentExists === undefined) {
// // 使用 parseInt 可以将 0em 或 0px 转换成 0
// textIndentExists = parseInt(dom.getStyle(parent, styleName)) > 0;
// }
// dom.setStyle(parent, styleName, textIndentExists ? '' : '2em');
// });
};
export { doAction };

View File

@ -0,0 +1,10 @@
import Plugin from './plugin';
const defaultOptions = {
id: 'indent2em',
name: '首行缩进',
tooltip: '首行缩进',
icon: '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M170.666667 563.2v-102.4H887.466667v102.4zM170.666667 836.266667v-102.4H887.466667v102.4zM512 290.133333v-102.4H887.466667v102.4zM238.933333 341.333333V136.533333l204.8 102.4z" p-id="5210"></path></svg>',
};
Plugin(defaultOptions);

View File

@ -0,0 +1,11 @@
import tinymce from 'tinymce';
import * as commands from './api/commands';
import * as buttons from './ui/buttons';
export default (defaultOptions): void => {
tinymce.PluginManager.add('indent2em', function (editor) {
commands.register(editor, defaultOptions);
buttons.register(editor, defaultOptions);
return {};
});
};

View File

@ -0,0 +1,73 @@
import { Editor, Ui } from 'tinymce';
const register = (editor: Editor, defaultOptions: any): void => {
const onAction = () => editor.execCommand(defaultOptions.id);
// const onSetup = (buttonApi: Ui.Toolbar.ToolbarToggleButtonInstanceApi) => {
// const indentSelector = '*[style*="text-indent"], *[data-mce-style*="text-indent"]';
// const containerSelector = 'p,div';
// const unbindActiveSelectorChange = editor.selection.selectorChangedWithUnbind(indentSelector, (active: boolean, args: { node: Node; parents: Element[] }) => {
// const parent = editor.dom.getParent(args.node, containerSelector);
// // 使用 parseInt 可以将 0em 或 0px 转换成 0
// buttonApi.setActive(parent != null && parseInt(editor.dom.getStyle(parent, 'text-indent')) > 0 && active);
// }).unbind;
// const unbindDesabledSelectorChange = editor.selection.selectorChangedWithUnbind(containerSelector, (active: boolean) => {
// buttonApi.setDisabled(!active);
// }).unbind;
// return () => {
// unbindActiveSelectorChange();
// unbindDesabledSelectorChange();
// };
// };
// const onSetup = (api: Ui.Toolbar.ToolbarToggleButtonInstanceApi) => {
// const { dom } = editor;
// const nodeChangeHandler = (e: EditorEvent<Events.NodeChangeEvent>) => {
// const { parents } = e;
// const parent = parents[parents.length - 1];
// const enabled = ['p', 'div'].includes(parent?.nodeName.toLowerCase());
// api.setDisabled(!enabled);
// // 使用 parseInt 可以将 0em 或 0px 转换成 0
// api.setActive(enabled && parseInt(dom.getStyle(parent, 'text-indent')) > 0);
// };
// editor.on('NodeChange', nodeChangeHandler);
// return () => editor.off('NodeChange', nodeChangeHandler);
// };
const onSetup = (api: Ui.Toolbar.ToolbarToggleButtonInstanceApi) => {
const indent2em = [
{
selector: 'p,div',
styles: {
textIndent: '2em',
},
inherit: false,
},
];
editor.formatter.register(defaultOptions.id, indent2em);
const nodeChangeHandler = () => {
api.setActive(editor.formatter.match(defaultOptions.id));
};
editor.on('NodeChange', nodeChangeHandler);
return () => editor.off('NodeChange', nodeChangeHandler);
};
if (!editor.ui.registry.getAll().icons[defaultOptions.id]) {
editor.ui.registry.addIcon(defaultOptions.id, defaultOptions.icon);
}
editor.ui.registry.addToggleButton(defaultOptions.id, {
icon: defaultOptions.id,
tooltip: defaultOptions.tooltip,
onAction,
onSetup,
});
editor.ui.registry.addToggleMenuItem(defaultOptions.id, {
icon: defaultOptions.id,
text: defaultOptions.tooltip,
onAction,
});
};
export { register };

View File

@ -0,0 +1,142 @@
import { Ref, watch, SetupContext } from 'vue';
const validEvents = [
'onActivate',
'onAddUndo',
'onBeforeAddUndo',
'onBeforeExecCommand',
'onBeforeGetContent',
'onBeforeRenderUI',
'onBeforeSetContent',
'onBeforePaste',
'onBlur',
'onChange',
'onClearUndos',
'onClick',
'onContextMenu',
'onCopy',
'onCut',
'onDblclick',
'onDeactivate',
'onDirty',
'onDrag',
'onDragDrop',
'onDragEnd',
'onDragGesture',
'onDragOver',
'onDrop',
'onExecCommand',
'onFocus',
'onFocusIn',
'onFocusOut',
'onGetContent',
'onHide',
'onInit',
'onKeyDown',
'onKeyPress',
'onKeyUp',
'onLoadContent',
'onMouseDown',
'onMouseEnter',
'onMouseLeave',
'onMouseMove',
'onMouseOut',
'onMouseOver',
'onMouseUp',
'onNodeChange',
'onObjectResizeStart',
'onObjectResized',
'onObjectSelected',
'onPaste',
'onPostProcess',
'onPostRender',
'onPreProcess',
'onProgressState',
'onRedo',
'onRemove',
'onReset',
'onSaveContent',
'onSelectionChange',
'onSetAttrib',
'onSetContent',
'onShow',
'onSubmit',
'onUndo',
'onVisualAid',
];
const isValidKey = (key: string): boolean => validEvents.map((event) => event.toLowerCase()).indexOf(key.toLowerCase()) !== -1;
const bindHandlers = (initEvent: Event, listeners: any, editor: any): void => {
Object.keys(listeners)
.filter(isValidKey)
.forEach((key: string) => {
const handler = listeners[key];
if (typeof handler === 'function') {
if (key === 'onInit') {
handler(initEvent, editor);
} else {
editor.on(key.substring(2), (e: any) => handler(e, editor));
}
}
});
};
const bindModelHandlers = (props: any, ctx: SetupContext, editor: any, modelValue: Ref<string>, formItem: any): void => {
const modelEvents = props.modelEvents ? props.modelEvents : null;
const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents;
watch(modelValue, (val: string, prevVal: string) => {
if (editor && typeof val === 'string' && val !== prevVal && val !== editor.getContent({ format: props.outputFormat })) {
editor.setContent(val);
}
});
// 要加上 paste 事件,否则首次粘贴时内容会为空。使用 'change input paste undo redo' 在快速剪切、粘贴的情况下,有可能还是会出现 '必填字段' 的错误提示。
editor.on(normalizedEvents ?? 'change keyup undo redo', () => {
const content = editor.getContent({ format: props.outputFormat });
ctx.emit('update:modelValue', content);
ctx.emit('input', content);
ctx.emit('change', content);
formItem?.validate?.('change').catch((err: any) => {
if (import.meta.env.MODE !== 'production') {
console.warn(err);
}
});
});
editor.on('blur', (event: any) => {
ctx.emit('blur', event);
});
editor.on('keydown', (event: any) => {
ctx.emit('keydown', event);
});
};
const initEditor = (initEvent: Event, props: any, ctx: any, editor: any, modelValue: Ref<string>, formItem: any): void => {
editor.setContent(modelValue.value ?? '');
bindModelHandlers(props, ctx, editor, modelValue, formItem);
bindHandlers(initEvent, ctx.attrs, editor);
};
let unique = 0;
const uuid = (prefix: string): string => {
const time = Date.now();
const random = Math.floor(Math.random() * 1000000000);
unique += 1;
return `${prefix}_${random + unique}${String(time)}`;
};
const isTextarea = (element: Element | null): element is HTMLTextAreaElement => element !== null && element.tagName.toLowerCase() === 'textarea';
const normalizePluginArray = (plugins?: string | string[]): string[] => {
if (typeof plugins === 'undefined' || plugins === '') {
return [];
}
return Array.isArray(plugins) ? plugins : plugins.split(' ');
};
const mergePlugins = (initPlugins: string | string[], inputPlugins?: string | string[]): string[] => normalizePluginArray(initPlugins).concat(normalizePluginArray(inputPlugins));
export { bindHandlers, bindModelHandlers, initEditor, isValidKey, uuid, isTextarea, mergePlugins };

View File

@ -0,0 +1,215 @@
<script lang="ts">
const editorEvents = ['load', 'change', 'caretChange', 'focus', 'blur', 'keydown', 'keyup', 'beforePreviewRender', 'beforeConvertWysiwygToMarkdown'];
</script>
<script setup lang="ts">
import { onMounted, ref, toRefs, watch, PropType, onUnmounted, nextTick } from 'vue';
import { useFormItem } from 'element-plus';
import { vOnClickOutside } from '@vueuse/components';
import { decodeHTML } from 'entities';
import Editor, { EditorType, PreviewStyle, EditorOptions } from '@toast-ui/editor';
import chart from '@toast-ui/editor-plugin-chart';
import codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight';
import tableMergedCell from '@toast-ui/editor-plugin-table-merged-cell';
import uml from '@toast-ui/editor-plugin-uml';
import Prism from 'prismjs';
import { addImageBlobHook, toggleFullScreen, clickOutside } from './utils';
import '@toast-ui/editor/dist/i18n/zh-cn';
import '@toast-ui/editor/dist/i18n/zh-tw';
import '@toast-ui/editor/dist/toastui-editor.css';
import '@toast-ui/chart/dist/toastui-chart.css';
import 'prismjs/themes/prism.css';
import 'prismjs/components/prism-clojure.js';
import '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css';
import '@toast-ui/editor-plugin-table-merged-cell/dist/toastui-editor-plugin-table-merged-cell.css';
defineOptions({
name: 'TuiEditor',
});
const props = defineProps({
modelValue: { type: String, default: '' },
html: { type: String, default: '' },
initialEditType: { type: String as PropType<EditorType>, default: 'markdown' },
height: { type: String, default: '300px' },
previewStyle: { type: String as PropType<PreviewStyle>, default: 'tab' },
language: { type: String, default: 'en' },
options: { type: Object, default: null },
});
const emit = defineEmits([...editorEvents, 'update:modelValue', 'update:html', 'different']);
const { modelValue, html, initialEditType, height, previewStyle, language, options } = toRefs(props);
const toastuiEditor = ref<any>();
let editor: Editor;
const { formItem } = useFormItem();
watch(previewStyle, () => {
editor.changePreviewStyle(previewStyle.value);
});
watch(height, () => {
editor.setHeight(height.value);
});
watch([modelValue, html], () => {
updateEditorValue();
});
const createFullscreenButton = () => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'toastui-editor-toolbar-icons text-xl';
button.style.backgroundImage = 'none';
button.style.margin = '0';
button.innerHTML = 'F';
button.addEventListener('click', () => {
toggleFullScreen(editor, toastuiEditor.value, height.value);
});
return button;
};
const updateEditorValue = () => {
if (modelValue.value && modelValue.value !== editor.getMarkdown()) {
editor.setMarkdown(modelValue.value);
} else if (!modelValue.value && html.value) {
// markdownhtmlhtml
editor.setHTML(html.value);
// markdown
nextTick().then(() => {
emit('update:modelValue', editor.getMarkdown());
});
return;
} else if (!modelValue.value && !html.value) {
// markdownhtml
editor.setMarkdown('');
}
// markdownHTMLHTMLHTML
const currHtml = getHTML();
if (modelValue.value && decodeHTML(html.value) !== currHtml) {
//
emit('different', html.value, currHtml);
emit('update:html', currHtml);
}
};
// HTML
const emptyHtml = '<p><br class="ProseMirror-trailingBreak"></p>';
const eventOptions: any = {};
editorEvents.forEach((eventName: string) => {
eventOptions[eventName] = (...args: any[]) => {
if (eventName === 'change') {
const newHtml = getHTML();
if (newHtml !== html.value) {
emit('update:html', newHtml !== emptyHtml ? newHtml : '');
}
const newMarkdown = editor.getMarkdown();
if (newMarkdown !== modelValue.value) {
emit('update:modelValue', newMarkdown);
}
formItem?.validate?.('change').catch((err: any) => {
if (import.meta.env.MODE !== 'production') {
console.warn(err);
}
});
} else if (eventName === 'keydown') {
// editorType
const event = args[1];
if (event.ctrlKey && !event.shiftKey && event.key.toLowerCase() === 'z') {
event.preventDefault();
event.stopPropagation();
editor.exec('undo');
} else if ((event.ctrlKey && event.key.toLowerCase() === 'y') || (event.ctrlKey && event.shiftKey && event.key.toLowerCase() === 'z')) {
event.preventDefault();
event.stopPropagation();
editor.exec('redo');
}
}
emit(eventName, ...args);
};
});
onMounted(() => {
const chartOptions = {
maxWidth: 800,
maxHeight: 400,
};
const computedOptions: EditorOptions = {
...options?.value,
initialValue: modelValue.value ?? '',
initialEditType: initialEditType.value,
height: height.value,
previewStyle: previewStyle.value,
language: language.value,
autofocus: false,
usageStatistics: false,
useCommandShortcut: false,
el: toastuiEditor.value,
events: eventOptions,
hooks: { addImageBlobHook },
plugins: [[chart, chartOptions], [codeSyntaxHighlight, { highlighter: Prism }], tableMergedCell, uml],
toolbarItems: [
[
{
name: 'fullscreen',
el: createFullscreenButton(),
tooltip: 'Fullscreen',
},
],
['heading', 'bold', 'italic', 'strike'],
['hr', 'quote'],
['ul', 'ol', 'task', 'indent', 'outdent'],
['table', 'image', 'link'],
['code', 'codeblock'],
['scrollSync'],
],
};
editor = new Editor(computedOptions);
updateEditorValue();
});
onUnmounted(() => {
editorEvents.forEach((event) => {
editor.off(event);
});
});
const getHTML = () => {
const html = editor.getHTML();
if (html != null) {
return html.replaceAll(/<p><br[ /]*><\/p>/gi, '');
}
return html;
};
const setHTML = (html: string): void => editor.setHTML(html);
const getMarkdown = () => editor.getMarkdown();
const setMarkdown = (markdown: string): void => editor.setMarkdown(markdown);
const getRootElement = () => toastuiEditor.value;
defineExpose({ getRootElement, getHTML, getMarkdown, setHTML, setMarkdown });
</script>
<template>
<!-- 在ElementPlus的对话框中更多工具条按钮点击后点击其它地方不会关闭工具条 -->
<div ref="toastuiEditor" v-on-click-outside="clickOutside"></div>
</template>
<style lang="scss" scoped>
:deep(.ProseMirror),
:deep(.toastui-editor-contents) {
font-family:
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Arial,
'Noto Sans',
'PingFang SC',
'Hiragino Sans GB',
'Microsoft YaHei',
'WenQuanYi Micro Hei',
sans-serif,
'Apple Color Emoji',
'Segoe UI Emoji',
'Segoe UI Symbol',
'Noto Color Emoji';
}
</style>

View File

@ -0,0 +1 @@
export { default as TuiEditor } from './TuiEditor.vue';

View File

@ -0,0 +1,77 @@
import { imageUploadUrl } from '@/api/config';
import { getAuthHeaders } from '@/utils/auth';
import { getSiteHeaders } from '@/utils/common';
import Editor from '@toast-ui/editor';
/**
* 使
*/
export const clickOutside = (event: Event) => {
if (event.bubbles || !event.cancelable || event.composed) {
const myEvent = new Event('click', { bubbles: false, cancelable: true, composed: false });
document.dispatchEvent(myEvent);
}
};
export const toggleFullScreen = (editor: Editor, element: HTMLElement, height: string): void => {
const style = element.style;
if (style.height !== '100vh') {
style.height = '100vh';
style.width = '100vw';
style.position = 'fixed';
style.zIndex = '10000000000';
style.top = '0px';
style.left = '0px';
style.backgroundColor = 'white';
editor.changePreviewStyle('vertical');
} else {
style.height = height;
style.width = '';
style.position = '';
style.zIndex = '';
style.top = '';
style.left = '';
style.backgroundColor = '';
editor.changePreviewStyle('tab');
}
};
export const addImageBlobHook = (blob: Blob | File, callback: any): void => {
const xhr = new XMLHttpRequest();
xhr.open('POST', imageUploadUrl);
Object.entries(getSiteHeaders()).forEach(([key, value]: any) => xhr.setRequestHeader(key, value));
// xhr.upload.onprogress = (e) => {
// (e.loaded / e.total) * 100;
// };
xhr.onload = () => {
if (xhr.status === 403) {
ElMessageBox.alert(`HTTP Error: ${xhr.status}`, { type: 'warning' });
return;
}
if (xhr.status < 200 || xhr.status >= 300) {
ElMessageBox.alert(`HTTP Error: ${xhr.status}`, { type: 'warning' });
return;
}
const json = JSON.parse(xhr.responseText);
if (!json || typeof json.url !== 'string') {
ElMessageBox.alert(`Invalid JSON: ${xhr.responseText}`, { type: 'warning' });
return;
}
callback(json.url);
};
xhr.onerror = () => {
ElMessageBox.alert(`Image upload failed due to a XHR Transport error. Code: ${xhr.status}`, { type: 'warning' });
};
const formData = new FormData();
formData.append('file', blob);
Object.entries(getAuthHeaders()).forEach(([key, value]: any) => xhr.setRequestHeader(key, value));
xhr.send(formData);
};

View File

@ -0,0 +1,142 @@
<script setup lang="ts">
import { ref, toRefs, computed, PropType } from 'vue';
import { UploadFile, UploadFiles } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { handleError } from '@/utils/request';
import { getAuthHeaders } from '@/utils/auth';
import { getSiteHeaders } from '@/utils/common';
import { useSysConfigStore } from '@/stores/sysConfigStore';
import { imageUploadUrl, videoUploadUrl, audioUploadUrl, docUploadUrl, fileUploadUrl } from '@/api/config';
const props = defineProps({
type: {
type: String as PropType<'image' | 'video' | 'audio' | 'library' | 'doc' | 'file' | 'any'>,
default: 'file',
validator: (value: string) => ['image', 'video', 'audio', 'library', 'doc', 'file', 'any'].includes(value),
},
button: { type: String, default: null },
data: { type: Object as PropType<Record<string, any>>, default: null },
uploadAction: { type: String, default: null },
fileAccept: { type: String, default: null },
fileMaxSize: { type: Number, default: null },
multiple: { type: Boolean },
disabled: { type: Boolean, default: false },
onSuccess: { type: Function as PropType<((response: any, uploadFile: UploadFile, uploadFiles: UploadFiles) => void) | undefined>, default: null },
});
const { type, uploadAction, fileAccept, fileMaxSize } = toRefs(props);
const { t } = useI18n();
const sysConfig = useSysConfigStore();
const progressFile = ref<any>({});
const action = computed(() => {
if (uploadAction?.value != null) {
return uploadAction.value;
}
switch (type.value) {
case 'image':
return imageUploadUrl;
case 'video':
return videoUploadUrl;
case 'audio':
return audioUploadUrl;
case 'library':
return docUploadUrl;
case 'doc':
return docUploadUrl;
case 'file':
return fileUploadUrl;
default:
throw new Error(`Type not support: ${type.value}`);
}
});
const accept = computed(() => {
if (fileAccept?.value != null) {
return fileAccept.value;
}
switch (type.value) {
case 'image':
return sysConfig.upload.imageInputAccept;
case 'video':
return sysConfig.upload.videoInputAccept;
case 'audio':
return sysConfig.upload.audioInputAccept;
case 'library':
return sysConfig.upload.libraryInputAccept;
case 'doc':
return sysConfig.upload.docInputAccept;
case 'file':
return sysConfig.upload.fileInputAccept;
case 'any':
return undefined;
default:
throw new Error(`Type not support: ${type.value}`);
}
});
const maxSize = computed(() => {
if (fileMaxSize?.value != null) {
return fileMaxSize.value;
}
switch (type.value) {
case 'image':
return sysConfig.upload.imageLimitByte;
case 'video':
return sysConfig.upload.videoLimitByte;
case 'audio':
return sysConfig.upload.audioLimitByte;
case 'library':
return sysConfig.upload.libraryLimitByte;
case 'doc':
return sysConfig.upload.docLimitByte;
case 'file':
return sysConfig.upload.fileLimitByte;
default:
return 0;
}
});
const beforeUpload = (file: any) => {
if (maxSize.value > 0 && file.size > maxSize.value) {
ElMessage.error(t('error.fileMaxSize', { size: `${maxSize.value / 1024 / 1024} MB` }));
return false;
}
return true;
};
const onError = (error: Error) => {
handleError(JSON.parse(error.message));
};
</script>
<template>
<div>
<el-upload
:action="action"
:headers="{ ...getAuthHeaders(), ...getSiteHeaders() }"
:data="data"
:accept="accept"
:before-upload="beforeUpload"
:on-success="onSuccess"
:on-progress="(event, file) => (progressFile = file)"
:on-error="onError"
:show-file-list="false"
:disabled="disabled"
:multiple="multiple"
drag
>
<!--
//
action="https://jsonplaceholder.typicode.com/posts/"
-->
<slot>
<span>{{ button ?? $t('clickOrDragToUpload') }}</span>
<!-- <el-button type="primary" :disabled="disabled">{{ button ?? $t('clickOrDragToUpload') }}</el-button> -->
</slot>
</el-upload>
<el-progress v-if="progressFile.status === 'uploading'" :percentage="parseInt(progressFile.percentage, 10)"></el-progress>
</div>
</template>
<style lang="scss" scoped>
:deep(.el-upload-dragger) {
padding: 0 20px;
@apply bg-primary-lighter text-primary;
}
</style>

View File

@ -0,0 +1,126 @@
<script setup lang="ts">
import { ref, toRefs, computed, watch } from 'vue';
import { useFormItem } from 'element-plus';
import { Close, Document, CircleCheck } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
import { handleError } from '@/utils/request';
import { getAuthHeaders } from '@/utils/auth';
import { getSiteHeaders } from '@/utils/common';
import { useSysConfigStore } from '@/stores/sysConfigStore';
import { fileUploadUrl } from '@/api/config';
const props = defineProps({
modelValue: { type: Array, default: () => [] },
fileAccept: { type: String, default: null },
fileMaxSize: { type: Number, default: null },
disabled: { type: Boolean, default: false },
});
const emit = defineEmits({ 'update:modelValue': null });
const { fileAccept, fileMaxSize, modelValue } = toRefs(props);
const { t } = useI18n();
const sysConfig = useSysConfigStore();
const progressFile = ref<any>({});
const fileList = computed({
get: (): any[] => modelValue.value,
set: (val) => emit('update:modelValue', val),
});
const { formItem } = useFormItem();
watch(
fileList,
() => {
formItem?.validate?.('change').catch((err: any) => {
if (import.meta.env.MODE !== 'production') {
console.warn(err);
}
});
},
{ deep: true },
);
const previewVisible = ref<boolean>(false);
const previewFile = ref<any>({});
const form = ref<any>();
const handlePreview = (file: any) => {
previewFile.value = file;
previewVisible.value = true;
};
const handleSubmit = () => {
form.value.validate(async (valid: boolean) => {
if (!valid) return;
previewVisible.value = false;
});
};
const accept = computed(() => fileAccept?.value ?? sysConfig.upload.fileInputAccept);
const maxSize = computed(() => fileMaxSize?.value ?? sysConfig.upload.fileLimitByte);
const beforeUpload = (file: any) => {
if (maxSize.value > 0 && file.size > maxSize.value) {
ElMessage.error(t('error.fileMaxSize', { size: `${maxSize.value / 1024 / 1024} MB` }));
return false;
}
return true;
};
const onError = (error: Error) => {
handleError(JSON.parse(error.message));
};
</script>
<template>
<div class="w-full">
<el-upload
:action="fileUploadUrl"
:headers="{ ...getAuthHeaders(), ...getSiteHeaders() }"
:accept="accept"
:before-upload="beforeUpload"
:on-success="(res: any) => fileList.push({ name: res.name, url: res.url, length: res.size })"
:on-progress="(event: any, file: any) => (progressFile = file)"
:on-error="onError"
:show-file-list="false"
:disabled="disabled"
multiple
drag
>
<!--
//
action="https://jsonplaceholder.typicode.com/posts/"
-->
{{ $t('clickOrDragToUpload') }}
<!-- <el-button type="primary">{{ $t('clickToUpload') }}</el-button> -->
</el-upload>
<el-progress v-if="progressFile.status === 'uploading'" :percentage="parseInt(progressFile.percentage, 10)"></el-progress>
<transition-group tag="ul" :class="['el-upload-list', 'el-upload-list--text', { 'is-disabled': disabled }]" name="el-list">
<li v-for="file in fileList" :key="file.url" class="el-upload-list__item is-success">
<a class="el-upload-list__item-name" @click="() => handlePreview(file)">
<el-icon class="el-icon--document"><Document /></el-icon>{{ file.name }}
</a>
<label class="el-upload-list__item-status-label">
<el-icon class="el-icon--upload-success el-icon--circle-check"><CircleCheck /></el-icon>
</label>
<el-icon v-if="!disabled" class="el-icon--close" @click="() => fileList.splice(fileList.indexOf(file), 1)"><Close /></el-icon>
</li>
</transition-group>
<el-dialog v-model="previewVisible" :title="$t('article.fileList.attribute')" top="5vh" :width="768" append-to-body>
<el-form ref="form" :model="previewFile" label-width="150px">
<el-form-item prop="name" :label="$t('name')" :rules="{ required: true, message: () => $t('v.required') }">
<el-input v-model="previewFile.name" maxlength="100"></el-input>
</el-form-item>
<el-form-item prop="length" :label="$t('size')" :rules="{ required: true, message: () => $t('v.required') }">
<el-input v-model="previewFile.length" maxlength="19">
<template #append>Byte</template>
</el-input>
</el-form-item>
<el-form-item prop="url" label="URL" :rules="{ required: true, message: () => $t('v.required') }">
<el-input v-model="previewFile.url" maxlength="255"></el-input>
</el-form-item>
<el-button type="primary" native-type="submit" @click.prevent="() => handleSubmit()">{{ $t('save') }}</el-button>
</el-form>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
:deep(.el-upload-dragger) {
padding: 0 20px;
@apply bg-primary-lighter text-primary;
}
</style>

View File

@ -0,0 +1,87 @@
<script setup lang="ts">
import { computed, ref, toRefs, PropType } from 'vue';
import Cropper from 'cropperjs';
import 'cropperjs/dist/cropper.css';
import { cropImage, cropAvatar } from '@/api/config';
const props = defineProps({
modelValue: { type: Boolean, required: true },
/**
* image: 图片上传, avatar: 头像上传
*/
type: { type: String as PropType<'image' | 'avatar'>, default: 'image' },
src: { type: String, default: null },
width: { type: Number, default: null },
height: { type: Number, default: null },
thumbnailWidth: { type: Number, default: null },
thumbnailHeight: { type: Number, default: null },
});
const emit = defineEmits({ 'update:modelValue': null, success: null });
const { modelValue, type, src, width, height, thumbnailWidth, thumbnailHeight } = toRefs(props);
const visible = computed({
get: () => modelValue.value,
set: (val) => emit('update:modelValue', val),
});
const imgRef = ref<any>();
const cropper = ref<any>();
const cropParam = ref<any>({});
const initCropper = () => {
if (imgRef.value) {
cropper.value = new Cropper(imgRef.value, {
aspectRatio: width?.value && height?.value ? width.value / height.value : NaN,
autoCropArea: width?.value && height?.value ? 1 : 0.8,
viewMode: 1,
minCropBoxWidth: width?.value ?? 16,
minCropBoxHeight: height?.value ?? 16,
zoomable: false,
crop(event) {
cropParam.value.url = src.value;
cropParam.value.x = Math.floor(event.detail.x);
cropParam.value.y = Math.floor(event.detail.y);
cropParam.value.width = Math.floor(event.detail.width);
cropParam.value.height = Math.floor(event.detail.height);
cropParam.value.maxWidth = width?.value;
cropParam.value.maxHeight = height?.value;
cropParam.value.thumbnailWidth = thumbnailWidth?.value;
cropParam.value.thumbnailHeight = thumbnailHeight?.value;
},
});
}
};
const destroyCropper = () => {
if (cropper.value) {
cropper.value.destroy();
}
};
const handleSubmit = async () => {
visible.value = false;
if (type.value === 'avatar') {
emit('success', (await cropAvatar(cropParam.value)).url);
} else {
emit('success', (await cropImage(cropParam.value)).url);
}
};
</script>
<template>
<el-dialog v-model="visible" :title="$t('imageCrop')" top="5vh" :width="768" destroy-on-close append-to-body @closed="() => destroyCropper()">
<div class="text-center">
<img ref="imgRef" :src="src" alt="" class="inline" style="max-height: 410px" @load="() => initCropper()" />
</div>
<div class="text-right">
<el-button type="primary" native-type="submit" class="mt-4" @click.prevent="() => handleSubmit()">{{ $t('save') }}</el-button>
</div>
</el-dialog>
</template>
<style lang="scss" scoped>
/* Ensure the size of the image fit the container perfectly */
:deep(img) {
display: block;
/* This rule is very important, please don't ignore this */
max-width: 100%;
}
</style>

View File

@ -0,0 +1,196 @@
<script setup lang="ts">
import { ref, toRefs, computed, watch, PropType } from 'vue';
import { useFormItem } from 'element-plus';
import { Plus, Crop, View, Delete } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
import draggable from 'vuedraggable';
import { handleError } from '@/utils/request';
import { getAuthHeaders } from '@/utils/auth';
import { getSiteHeaders } from '@/utils/common';
import { useSysConfigStore } from '@/stores/sysConfigStore';
import { imageUploadUrl } from '@/api/config';
import ImageCropper from './ImageCropper.vue';
const props = defineProps({
modelValue: { type: Array, default: () => [] },
fileAccept: { type: String, default: null },
fileMaxSize: { type: Number, default: null },
maxWidth: { type: Number, default: null },
maxHeight: { type: Number, default: null },
listType: { type: String as PropType<'pictureCard' | 'picture'>, default: 'pictureCard' },
disabled: { type: Boolean, default: false },
});
const emit = defineEmits({ 'update:modelValue': null });
const { modelValue, maxWidth, maxHeight, fileAccept, fileMaxSize } = toRefs(props);
const { t } = useI18n();
const sysConfig = useSysConfigStore();
const dragging = ref<boolean>(false);
const progressFile = ref<any>({});
const currentFile = ref<any>({});
const previewVisible = ref<boolean>(false);
const cropperVisible = ref<boolean>(false);
const previewFile = ref<any>({ src: 'data:;base64,=' });
const fileList = computed({
get: (): any[] => modelValue.value,
set: (val: any) => emit('update:modelValue', val),
});
const { formItem } = useFormItem();
watch(
fileList,
() => {
formItem?.validate?.('change').catch((err: any) => {
if (import.meta.env.MODE !== 'production') {
console.warn(err);
}
});
},
{ deep: true },
);
const handlePreview = (file: any) => {
previewFile.value = file;
previewVisible.value = true;
};
const thumbnailWidth = 300;
const thumbnailHeight = 300;
const getData = () => {
const data: any = { isWatermark: true, thumbnailWidth, thumbnailHeight };
if (maxWidth?.value != null) {
data.maxWidth = maxWidth.value;
}
if (maxHeight?.value != null) {
data.maxHeight = maxHeight.value;
}
return data;
};
const accept = computed(() => fileAccept?.value ?? sysConfig.upload.imageInputAccept);
const maxSize = computed(() => fileMaxSize?.value ?? sysConfig.upload.imageLimitByte);
const beforeUpload = (file: any) => {
if (maxSize.value > 0 && file.size > maxSize.value) {
ElMessage.error(t('error.fileMaxSize', { size: `${maxSize.value / 1024 / 1024} MB` }));
return false;
}
return true;
};
const onError = (error: Error) => {
handleError(JSON.parse(error.message));
};
</script>
<template>
<div>
<!-- <transition-group tag="ul" :class="['el-upload-list', 'el-upload-list--picture-card', { 'is-disabled': disabled }]" name="el-list"> -->
<draggable
:list="fileList"
tag="ul"
item-key="url"
:animation="250"
class="el-upload-list"
:class="[listType === 'picture' ? 'el-upload-list--picture' : 'el-upload-list--picture-card', { 'is-disabled': disabled }]"
@start="() => (dragging = true)"
@end="() => (dragging = false)"
>
<template #item="{ element: file }">
<li class="el-upload-list__item is-success">
<div :class="listType === 'picture' ? ['w-32', 'h-32'] : ['w-full', 'h-full']" class="relative flex items-center justify-center bg-gray-50">
<img class="block max-w-full max-h-full" :src="file.url" alt="" />
<div
class="absolute space-x-4 bg-black bg-opacity-50 rounded-md opacity-0 cursor-move full-flex-center"
:class="dragging ? undefined : 'hover:opacity-100'"
@click.stop
>
<el-icon class="image-action" :title="$t('cropImage')" @click="() => ((cropperVisible = true), (currentFile = file))"><Crop /></el-icon>
<el-icon class="image-action" :title="$t('previewImage')" @click="() => handlePreview(file)"><View /></el-icon>
<el-icon class="image-action" :title="$t('deleteImage')" @click="() => fileList.splice(fileList.indexOf(file), 1)"><Delete /></el-icon>
</div>
</div>
<div v-if="listType === 'picture'" class="ml-2">
<el-input v-model="file.url" placeholder="URL" maxlength="255">
<template #prepend>URL</template>
</el-input>
<el-input v-model="file.name" :placeholder="$t('article.imageList.name')" class="mt-1">
<template #prepend>{{ $t('article.imageList.name') }}</template>
</el-input>
<el-input v-model="file.description" type="textarea" :rows="2" :placeholder="$t('article.imageList.description')" class="mt-1"></el-input>
</div>
</li>
</template>
<template #footer>
<el-upload
:action="imageUploadUrl"
:headers="{ ...getAuthHeaders(), ...getSiteHeaders() }"
:data="getData()"
:accept="accept"
:before-upload="beforeUpload"
:on-success="(res: any, file: any) => fileList.push({ name: res.name, url: res.url })"
:on-progress="(event: any, file: any) => (progressFile = file)"
:on-error="onError"
:show-file-list="false"
:disabled="disabled"
multiple
drag
>
<el-progress v-if="progressFile.status === 'uploading'" type="circle" :percentage="parseInt(progressFile.percentage, 10)" />
<div v-else class="el-upload--picture-card">
<el-icon><Plus /></el-icon>
</div>
</el-upload>
</template>
</draggable>
<!-- </transition-group> -->
<div>
<el-dialog v-model="previewVisible" top="5vh" :width="768">
<el-input v-model="previewFile.url" maxlength="255">
<template #prepend>URL</template>
</el-input>
<el-input v-if="listType !== 'picture'" v-model="previewFile.name" :placeholder="$t('article.imageList.name')" class="mt-1">
<template #prepend>{{ $t('article.imageList.name') }}</template>
</el-input>
<el-input
v-if="listType !== 'picture'"
v-model="previewFile.description"
type="textarea"
:rows="2"
:placeholder="$t('article.imageList.description')"
class="mt-1"
></el-input>
<img :src="previewFile.url" alt="" class="mt-1 border border-gray-300" />
</el-dialog>
</div>
<image-cropper
v-model="cropperVisible"
:src="currentFile.url"
:thumbnail-width="thumbnailWidth"
:thumbnail-height="thumbnailHeight"
@success="(url) => (currentFile.url = url)"
></image-cropper>
</div>
</template>
<style lang="scss" scoped>
:deep(.el-dialog__headerbtn) {
top: 4px;
}
.full-flex-center {
@apply w-full h-full flex justify-center items-center;
}
.image-action {
@apply cursor-pointer text-xl text-white;
}
:deep(.el-upload-dragger) {
padding: 0;
}
:deep(.el-upload--picture-card) {
border: 0;
}
//
:deep(.el-upload-list--picture-card .el-progress) {
left: 0;
transform: none;
}
//
:deep(.el-upload-list__item) {
transition: none;
}
</style>

View File

@ -0,0 +1,149 @@
<script setup lang="ts">
import { computed, ref, toRefs, PropType } from 'vue';
import { useFormItem } from 'element-plus';
import { Plus, Crop, View, Delete } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
import { getAuthHeaders } from '@/utils/auth';
import { getSiteHeaders } from '@/utils/common';
import { handleError } from '@/utils/request';
import { useSysConfigStore } from '@/stores/sysConfigStore';
import { imageUploadUrl, avatarUploadUrl } from '@/api/config';
import ImageCropper from './ImageCropper.vue';
// 'image/jpg,image/jpeg,image/png,image/gif'
const props = defineProps({
modelValue: { type: String, default: null },
fileAccept: { type: String, default: null },
fileMaxSize: { type: Number, default: null },
width: { type: Number, default: null },
height: { type: Number, default: null },
/**
* none: 原图上传, cut: 自动裁剪, resize: 自动压缩, manual: 手动裁剪
*/
mode: { type: String as PropType<'none' | 'cut' | 'resize' | 'manual'>, default: 'none' },
/**
* image: 图片上传, avatar: 头像上传
*/
type: { type: String as PropType<'image' | 'avatar'>, default: 'image' },
disabled: { type: Boolean, default: false },
});
const emit = defineEmits({ 'update:modelValue': null, cropSuccess: null });
const { modelValue, type, width, height, mode, fileAccept, fileMaxSize } = toRefs(props);
const { t } = useI18n();
const sysConfig = useSysConfigStore();
const progressFile = ref<any>({});
const previewVisible = ref<boolean>(false);
const cropperVisible = ref<boolean>(false);
const { formItem } = useFormItem();
const src = computed({
get: (): string | undefined => modelValue.value,
set: (val?: string) => {
emit('update:modelValue', val);
formItem?.validate?.('change').catch((err: any) => {
if (import.meta.env.MODE !== 'production') {
console.warn(err);
}
});
},
});
const resizable = computed(() => ['cut', 'resize'].includes(mode.value));
const data = computed(() => {
const params: any = { resizeMode: mode.value === 'cut' ? 'cut' : 'normal' };
if (width.value != null) {
// 0
params.maxWidth = resizable.value ? width.value : 0;
}
if (height.value != null) {
// 0
params.maxHeight = resizable.value ? height.value : 0;
}
return params;
});
const accept = computed(() => fileAccept.value ?? sysConfig.upload.imageInputAccept);
const maxSize = computed(() => fileMaxSize.value ?? sysConfig.upload.imageLimitByte);
const beforeUpload = (file: any) => {
if (maxSize.value > 0 && file.size > maxSize.value) {
ElMessage.error(t('error.fileMaxSize', { size: `${maxSize.value / 1024 / 1024} MB` }));
return false;
}
return true;
};
const onError = (error: Error) => {
handleError(JSON.parse(error.message));
};
const onCropSuccess = (url: string) => {
src.value = url;
emit('cropSuccess', url);
};
</script>
<template>
<el-upload
:action="type === 'avatar' ? avatarUploadUrl : imageUploadUrl"
:headers="{ ...getAuthHeaders(), ...getSiteHeaders() }"
:accept="accept"
:before-upload="beforeUpload"
:data="data"
:show-file-list="false"
:on-success="(res: any) => ((src = res.url), (cropperVisible = mode === 'manual'))"
:on-error="onError"
:on-progress="(event: any, file: any) => (progressFile = file)"
:disabled="disabled"
:drag="!src"
>
<!--
//
action="https://jsonplaceholder.typicode.com/posts/"
-->
<div v-if="src" class="relative full-flex-center rounded-border hover:border-opacity-0">
<img :src="src" class="block max-w-full max-h-full" />
<div class="absolute space-x-4 bg-black bg-opacity-50 rounded-md opacity-0 cursor-default full-flex-center hover:opacity-100" @click.stop>
<el-icon class="image-action" :title="$t('cropImage')" @click="() => (cropperVisible = true)"><Crop /></el-icon>
<el-icon class="image-action" :title="$t('previewImage')" @click="() => (previewVisible = true)"><View /></el-icon>
<el-icon class="image-action" :title="$t('deleteImage')" @click="() => (src = undefined)"><Delete /></el-icon>
</div>
</div>
<el-progress v-else-if="progressFile.status === 'uploading'" type="circle" :percentage="parseInt(progressFile.percentage, 10)" />
<div v-else class="el-upload--picture-card">
<el-icon><Plus /></el-icon>
</div>
</el-upload>
<div>
<el-dialog v-model="previewVisible" top="5vh" :width="768" append-to-body destroy-on-close>
<el-input v-model="src">
<template #prepend>URL</template>
</el-input>
<img :src="src" alt="" class="mt-1 border border-gray-300" />
</el-dialog>
</div>
<image-cropper v-model="cropperVisible" :type="type" :src="src" :width="width" :height="height" @success="onCropSuccess" />
</template>
<style lang="scss" scoped>
:deep(.el-dialog__headerbtn) {
top: 4px;
}
:deep(.el-upload) {
width: 148px;
height: 148px;
}
.full-flex-center {
@apply w-full h-full flex justify-center items-center;
}
.rounded-border {
border: 1px solid #c0ccda;
@apply rounded-md bg-gray-50;
}
.image-action {
@apply cursor-pointer text-xl text-white;
}
:deep(.el-upload-dragger) {
padding: 0;
}
:deep(.el-upload--picture-card) {
border: 0;
}
</style>

View File

@ -0,0 +1,4 @@
export { default as ImageUpload } from './ImageUpload.vue';
export { default as ImageListUpload } from './ImageListUpload.vue';
export { default as FileListUpload } from './FileListUpload.vue';
export { default as BaseUpload } from './BaseUpload.vue';

View File

@ -0,0 +1,523 @@
import { assign, forEach, isArray, every } from 'min-dash';
import { is } from 'bpmn-js/lib/util/ModelUtil';
import { isExpanded, isHorizontal, isEventSubProcess } from 'bpmn-js/lib/util/DiUtil';
import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil';
import { getChildLanes } from 'bpmn-js/lib/features/modeling/util/LaneUtil';
import { hasPrimaryModifier } from 'diagram-js/lib/util/Mouse';
/**
* A provider for BPMN 2.0 elements context pad
* 将任务(Task)改为用户任务(UserTask)
* https://github.com/bpmn-io/bpmn-js/blob/develop/lib/features/context-pad/ContextPadProvider.js
*/
/**
* @typedef {import('didi').Injector} Injector
* @typedef {import('diagram-js/lib/core/EventBus').default} EventBus
* @typedef {import('diagram-js/lib/features/context-pad/ContextPad').default} ContextPad
* @typedef {import('../modeling/Modeling').default} Modeling
* @typedef {import('../modeling/ElementFactory').default} ElementFactory
* @typedef {import('diagram-js/lib/features/connect/Connect').default} Connect
* @typedef {import('diagram-js/lib/features/create/Create').default} Create
* @typedef {import('diagram-js/lib/features/popup-menu/PopupMenu').default} PopupMenu
* @typedef {import('diagram-js/lib/features/canvas/Canvas').default} Canvas
* @typedef {import('diagram-js/lib/features/rules/Rules').default} Rules
* @typedef {import('diagram-js/lib/i18n/translate/translate').default} Translate
*
* @typedef {import('../../model/Types').Element} Element
* @typedef {import('../../model/Types').ModdleElement} ModdleElement
*
* @typedef {import('diagram-js/lib/features/context-pad/ContextPadProvider').default<Element>} BaseContextPadProvider
* @typedef {import('diagram-js/lib/features/context-pad/ContextPadProvider').ContextPadEntries} ContextPadEntries
* @typedef {import('diagram-js/lib/features/context-pad/ContextPadProvider').ContextPadEntry} ContextPadEntry
*
* @typedef { { autoPlace?: boolean; } } ContextPadConfig
*/
/**
* BPMN-specific context pad provider.
*
* @implements {BaseContextPadProvider}
*
* @param {ContextPadConfig} config
* @param {Injector} injector
* @param {EventBus} eventBus
* @param {ContextPad} contextPad
* @param {Modeling} modeling
* @param {ElementFactory} elementFactory
* @param {Connect} connect
* @param {Create} create
* @param {PopupMenu} popupMenu
* @param {Canvas} canvas
* @param {Rules} rules
* @param {Translate} translate
*/
export default function FlowableContextPadProvider(
config,
injector,
eventBus,
contextPad,
modeling,
elementFactory,
connect,
create,
popupMenu,
canvas,
rules,
translate,
appendPreview,
) {
config = config || {};
contextPad.registerProvider(this);
this._contextPad = contextPad;
this._modeling = modeling;
this._elementFactory = elementFactory;
this._connect = connect;
this._create = create;
this._popupMenu = popupMenu;
this._canvas = canvas;
this._rules = rules;
this._translate = translate;
this._eventBus = eventBus;
this._appendPreview = appendPreview;
if (config.autoPlace !== false) {
this._autoPlace = injector.get('autoPlace', false);
}
eventBus.on('create.end', 250, function (event) {
var context = event.context,
shape = context.shape;
if (!hasPrimaryModifier(event) || !contextPad.isOpen(shape)) {
return;
}
var entries = contextPad.getEntries(shape);
if (entries.replace) {
entries.replace.action.click(event, shape);
}
});
}
FlowableContextPadProvider.$inject = [
'config.contextPad',
'injector',
'eventBus',
'contextPad',
'modeling',
'elementFactory',
'connect',
'create',
'popupMenu',
'canvas',
'rules',
'translate',
'appendPreview',
];
/**
* @param {Element[]} elements
*
* @return {ContextPadEntries}
*/
FlowableContextPadProvider.prototype.getMultiElementContextPadEntries = function (elements) {
var modeling = this._modeling;
var actions = {};
if (this._isDeleteAllowed(elements)) {
assign(actions, {
delete: {
group: 'edit',
className: 'bpmn-icon-trash',
title: this._translate('Delete'),
action: {
click: function (event, elements) {
modeling.removeElements(elements.slice());
},
},
},
});
}
return actions;
};
/**
* @param {Element[]} elements
*
* @return {boolean}
*/
FlowableContextPadProvider.prototype._isDeleteAllowed = function (elements) {
var baseAllowed = this._rules.allowed('elements.delete', {
elements: elements,
});
if (isArray(baseAllowed)) {
return every(baseAllowed, function (element) {
return includes(baseAllowed, element);
});
}
return baseAllowed;
};
/**
* @param {Element} element
*
* @return {ContextPadEntries}
*/
FlowableContextPadProvider.prototype.getContextPadEntries = function (element) {
var contextPad = this._contextPad,
modeling = this._modeling,
elementFactory = this._elementFactory,
connect = this._connect,
create = this._create,
popupMenu = this._popupMenu,
rules = this._rules,
autoPlace = this._autoPlace,
translate = this._translate,
appendPreview = this._appendPreview;
var actions = {};
if (element.type === 'label') {
return actions;
}
var businessObject = element.businessObject;
function startConnect(event, element) {
connect.start(event, element);
}
function removeElement(e, element) {
modeling.removeElements([element]);
}
function getReplaceMenuPosition(element) {
var Y_OFFSET = 5;
var pad = contextPad.getPad(element).html;
var padRect = pad.getBoundingClientRect();
var pos = {
x: padRect.left,
y: padRect.bottom + Y_OFFSET,
};
return pos;
}
/**
* Create an append action.
*
* @param {string} type
* @param {string} className
* @param {string} title
* @param {Object} [options]
*
* @return {ContextPadEntry}
*/
function appendAction(type, className, title, options) {
function appendStart(event, element) {
var shape = elementFactory.createShape(assign({ type: type }, options));
create.start(event, shape, {
source: element,
});
appendPreview.cleanUp();
}
var append = autoPlace
? function (_, element) {
var shape = elementFactory.createShape(assign({ type: type }, options));
autoPlace.append(element, shape);
appendPreview.cleanUp();
}
: appendStart;
var previewAppend = autoPlace
? function (_, element) {
// mouseover
appendPreview.create(element, type, options);
return () => {
// mouseout
appendPreview.cleanUp();
};
}
: null;
return {
group: 'model',
className: className,
title: title,
action: {
dragstart: appendStart,
click: append,
hover: previewAppend,
},
};
}
function splitLaneHandler(count) {
return function (_, element) {
// actual split
modeling.splitLane(element, count);
// refresh context pad after split to
// get rid of split icons
contextPad.open(element, true);
};
}
if (isAny(businessObject, ['bpmn:Lane', 'bpmn:Participant']) && isExpanded(element)) {
var childLanes = getChildLanes(element);
assign(actions, {
'lane-insert-above': {
group: 'lane-insert-above',
className: 'bpmn-icon-lane-insert-above',
title: translate('Add lane above'),
action: {
click: function (event, element) {
modeling.addLane(element, 'top');
},
},
},
});
if (childLanes.length < 2) {
if (isHorizontal(element) ? element.height >= 120 : element.width >= 120) {
assign(actions, {
'lane-divide-two': {
group: 'lane-divide',
className: 'bpmn-icon-lane-divide-two',
title: translate('Divide into two lanes'),
action: {
click: splitLaneHandler(2),
},
},
});
}
if (isHorizontal(element) ? element.height >= 180 : element.width >= 180) {
assign(actions, {
'lane-divide-three': {
group: 'lane-divide',
className: 'bpmn-icon-lane-divide-three',
title: translate('Divide into three lanes'),
action: {
click: splitLaneHandler(3),
},
},
});
}
}
assign(actions, {
'lane-insert-below': {
group: 'lane-insert-below',
className: 'bpmn-icon-lane-insert-below',
title: translate('Add lane below'),
action: {
click: function (event, element) {
modeling.addLane(element, 'bottom');
},
},
},
});
}
if (is(businessObject, 'bpmn:FlowNode')) {
if (is(businessObject, 'bpmn:EventBasedGateway')) {
assign(actions, {
'append.receive-task': appendAction('bpmn:ReceiveTask', 'bpmn-icon-receive-task', translate('Append receive task')),
'append.message-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-message',
translate('Append message intermediate catch event'),
{ eventDefinitionType: 'bpmn:MessageEventDefinition' },
),
'append.timer-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-timer',
translate('Append timer intermediate catch event'),
{ eventDefinitionType: 'bpmn:TimerEventDefinition' },
),
'append.condition-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-condition',
translate('Append conditional intermediate catch event'),
{ eventDefinitionType: 'bpmn:ConditionalEventDefinition' },
),
'append.signal-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-signal',
translate('Append signal intermediate catch event'),
{ eventDefinitionType: 'bpmn:SignalEventDefinition' },
),
});
} else if (isEventType(businessObject, 'bpmn:BoundaryEvent', 'bpmn:CompensateEventDefinition')) {
assign(actions, {
// 'append.compensation-activity': appendAction('bpmn:Task', 'bpmn-icon-task', translate('Append compensation activity'), {
'append.compensation-activity': appendAction('bpmn:UserTask', 'bpmn-icon-user-task', translate('Append compensation activity'), {
isForCompensation: true,
}),
});
} else if (
!is(businessObject, 'bpmn:EndEvent') &&
!businessObject.isForCompensation &&
!isEventType(businessObject, 'bpmn:IntermediateThrowEvent', 'bpmn:LinkEventDefinition') &&
!isEventSubProcess(businessObject)
) {
assign(actions, {
'append.end-event': appendAction('bpmn:EndEvent', 'bpmn-icon-end-event-none', translate('Append end event')),
'append.gateway': appendAction('bpmn:ExclusiveGateway', 'bpmn-icon-gateway-none', translate('Append gateway')),
// 'append.append-task': appendAction('bpmn:Task', 'bpmn-icon-task', translate('Append task')),
'append.append-user-task': appendAction('bpmn:UserTask', 'bpmn-icon-user-task', translate('Append user task')),
'append.intermediate-event': appendAction('bpmn:IntermediateThrowEvent', 'bpmn-icon-intermediate-event-none', translate('Append intermediate/boundary event')),
});
}
}
if (!popupMenu.isEmpty(element, 'bpmn-replace')) {
// Replace menu entry
assign(actions, {
replace: {
group: 'edit',
className: 'bpmn-icon-screw-wrench',
title: translate('Change element'),
action: {
click: function (event, element) {
var position = assign(getReplaceMenuPosition(element), {
cursor: { x: event.x, y: event.y },
});
popupMenu.open(element, 'bpmn-replace', position, {
title: translate('Change element'),
width: 300,
search: true,
});
},
},
},
});
}
if (is(businessObject, 'bpmn:SequenceFlow')) {
assign(actions, {
'append.text-annotation': appendAction('bpmn:TextAnnotation', 'bpmn-icon-text-annotation', translate('Add text annotation')),
});
}
if (isAny(businessObject, ['bpmn:FlowNode', 'bpmn:InteractionNode', 'bpmn:DataObjectReference', 'bpmn:DataStoreReference'])) {
assign(actions, {
'append.text-annotation': appendAction('bpmn:TextAnnotation', 'bpmn-icon-text-annotation', translate('Add text annotation')),
connect: {
group: 'connect',
className: 'bpmn-icon-connection-multi',
title: translate('Connect to other element'),
action: {
click: startConnect,
dragstart: startConnect,
},
},
});
}
if (is(businessObject, 'bpmn:TextAnnotation')) {
assign(actions, {
connect: {
group: 'connect',
className: 'bpmn-icon-connection-multi',
title: translate('Connect using association'),
action: {
click: startConnect,
dragstart: startConnect,
},
},
});
}
if (isAny(businessObject, ['bpmn:DataObjectReference', 'bpmn:DataStoreReference'])) {
assign(actions, {
connect: {
group: 'connect',
className: 'bpmn-icon-connection-multi',
title: translate('Connect using data input association'),
action: {
click: startConnect,
dragstart: startConnect,
},
},
});
}
if (is(businessObject, 'bpmn:Group')) {
assign(actions, {
'append.text-annotation': appendAction('bpmn:TextAnnotation', 'bpmn-icon-text-annotation', translate('Add text annotation')),
});
}
// delete element entry, only show if allowed by rules
var deleteAllowed = rules.allowed('elements.delete', { elements: [element] });
if (isArray(deleteAllowed)) {
// was the element returned as a deletion candidate?
deleteAllowed = deleteAllowed[0] === element;
}
if (deleteAllowed) {
assign(actions, {
delete: {
group: 'edit',
className: 'bpmn-icon-trash',
title: translate('Delete'),
action: {
click: removeElement,
},
},
});
}
return actions;
};
// helpers /////////
/**
* @param {ModdleElement} businessObject
* @param {string} type
* @param {string} eventDefinitionType
*
* @return {boolean}
*/
function isEventType(businessObject, type, eventDefinitionType) {
var isType = businessObject.$instanceOf(type);
var isDefinition = false;
var definitions = businessObject.eventDefinitions || [];
forEach(definitions, function (def) {
if (def.$type === eventDefinitionType) {
isDefinition = true;
}
});
return isType && isDefinition;
}
function includes(array, item) {
return array.indexOf(item) !== -1;
}

View File

@ -0,0 +1,18 @@
import DirectEditingModule from 'diagram-js-direct-editing';
import ContextPadModule from 'diagram-js/lib/features/context-pad';
import SelectionModule from 'diagram-js/lib/features/selection';
import ConnectModule from 'diagram-js/lib/features/connect';
import CreateModule from 'diagram-js/lib/features/create';
import AppendPreviewModule from 'bpmn-js/lib/features/append-preview';
import PopupMenuModule from 'bpmn-js/lib/features/popup-menu';
import FlowableContextPadProvider from './FlowableContextPadProvider';
/**
* https://github.com/bpmn-io/bpmn-js/blob/develop/lib/features/context-pad/index.js
*/
export default {
__depends__: [AppendPreviewModule, DirectEditingModule, ContextPadModule, SelectionModule, ConnectModule, CreateModule, PopupMenuModule],
// 覆盖自带的 contextPadProvider 全部自己定义
__init__: ['contextPadProvider'],
contextPadProvider: ['type', FlowableContextPadProvider],
};

View File

@ -0,0 +1,15 @@
import translations from './translations';
/**
* https://github.com/bpmn-io/bpmn-js-examples/blob/main/i18n/src/customTranslate/customTranslate.js
*/
export default function customTranslate(template, replacements) {
replacements = replacements || {};
// Translate
template = translations[template] || template;
// Replace
return template.replace(/{([^}]+)}/g, function (_, key) {
return replacements[key] || '{' + key + '}';
});
}

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