├── .env.staging ├── .env.development ├── .husky └── pre-commit ├── .env.production ├── src ├── components │ ├── Tinymce │ │ ├── plugins │ │ │ ├── typesetting │ │ │ │ └── index.ts │ │ │ └── indent2em │ │ │ │ ├── api │ │ │ │ └── commands.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── index.ts │ │ │ │ ├── core │ │ │ │ └── actions.ts │ │ │ │ └── ui │ │ │ │ └── buttons.ts │ │ └── index.ts │ ├── TuiEditor │ │ ├── index.ts │ │ └── utils.ts │ ├── QueryForm │ │ ├── index.ts │ │ ├── QueryInput.vue │ │ ├── QueryItem.vue │ │ └── QueryForm.vue │ ├── TableList │ │ ├── index.ts │ │ ├── ColumnSetting.vue │ │ ├── useColumns.ts │ │ └── ColumnList.vue │ ├── bpmnjs │ │ ├── properties-panel │ │ │ ├── properties │ │ │ │ ├── FormProps.vue │ │ │ │ ├── ConditionProps.vue │ │ │ │ ├── ListenerProps.vue │ │ │ │ ├── TimerProps.vue │ │ │ │ ├── MultiInstanceProps.vue │ │ │ │ └── NormalProps.vue │ │ │ ├── utils.ts │ │ │ └── FlowablePropertiesPannel.vue │ │ ├── custom-translate │ │ │ ├── customTranslate.ts │ │ │ └── defaultBpmnXml.ts │ │ ├── palette │ │ │ └── index.js │ │ └── context-pad │ │ │ └── index.js │ ├── Upload │ │ ├── index.ts │ │ └── ImageCropper.vue │ ├── LabelTip.vue │ ├── ListMove.vue │ ├── BreadCrumb │ │ └── index.vue │ └── user │ │ └── UserSelect.vue ├── assets │ └── 404_images │ │ ├── 404.png │ │ └── 404_cloud.png ├── views │ ├── EnterpriseComponent.vue │ ├── user │ │ ├── OrgPermissionForm.vue │ │ ├── GroupForm.vue │ │ ├── OrgForm.vue │ │ ├── UserPermissionForm.vue │ │ ├── RoleForm.vue │ │ └── UserPasswordForm.vue │ ├── content │ │ ├── components │ │ │ ├── ReviewBack.vue │ │ │ ├── ReviewDelegate.vue │ │ │ ├── ReviewTransfer.vue │ │ │ ├── articleUtils.ts │ │ │ ├── ReviewFormProperties.vue │ │ │ └── ImageExtractor.vue │ │ ├── TagForm.vue │ │ ├── DictForm.vue │ │ ├── ChannelPermissionForm.vue │ │ ├── ChannelMergeForm.vue │ │ └── ChannelMoveForm.vue │ ├── interaction │ │ ├── CollectionSetup.vue │ │ └── ExampleForm.vue │ ├── system │ │ ├── ProcessDefinitionList.vue │ │ ├── ProcessModelDesign.vue │ │ └── TaskForm.vue │ ├── RefreshPage.vue │ ├── EnterprisePage.vue │ ├── 403.vue │ ├── file │ │ ├── WebFileHtmlList.vue │ │ ├── WebFileUploadList.vue │ │ ├── WebFileTemplateList.vue │ │ └── WebFileBatch.vue │ ├── personal │ │ ├── MachineCode.vue │ │ ├── GeneratedKey.vue │ │ └── MachineLicense.vue │ ├── config │ │ ├── MessageBoardTypeForm.vue │ │ ├── ModelForm.vue │ │ └── DictTypeForm.vue │ └── stat │ │ ├── EntryPage.vue │ │ ├── VisitSource.vue │ │ ├── VisitedPage.vue │ │ └── VisitTrend.vue ├── locales │ ├── en │ │ ├── index.ts │ │ ├── menu.json │ │ ├── common.json │ │ └── user.json │ └── zh-cn │ │ ├── index.ts │ │ ├── stat.json │ │ ├── file.json │ │ ├── homepage.json │ │ └── menu.json ├── layout │ ├── components │ │ ├── index.ts │ │ ├── AppMain.vue │ │ ├── AppSidebar │ │ │ ├── SidebarLogo.vue │ │ │ ├── index.vue │ │ │ └── MenuItem.vue │ │ └── useViewTabs.ts │ ├── composables │ │ └── useResizeHandler.ts │ └── index.vue ├── styles │ ├── tailwind.scss │ ├── index.scss │ ├── element-plus.scss │ └── transition.scss ├── stores │ ├── importDataStore.ts │ ├── currentSiteStore.ts │ ├── appStateStore.ts │ ├── columnSettingsStore.ts │ └── sysConfigStore.ts ├── App.vue ├── main.ts ├── vite-env.d.ts ├── api │ ├── personal.ts │ ├── login.ts │ ├── log.ts │ └── stat.ts ├── utils │ ├── echarts.ts │ ├── sm.ts │ ├── auth.ts │ └── request.ts ├── permission.ts └── i18n.ts ├── .eslintignore ├── public ├── favicon.png └── tinymce │ └── skins │ ├── ui │ ├── oxide │ │ ├── fonts │ │ │ └── tinymce-mobile.woff │ │ ├── content.mobile.min.css │ │ └── skin.shadowdom.min.css │ └── oxide-dark │ │ ├── fonts │ │ └── tinymce-mobile.woff │ │ ├── content.mobile.min.css │ │ └── skin.shadowdom.min.css │ └── content │ ├── default │ └── content.min.css │ ├── writer │ └── content.min.css │ ├── dark │ └── content.min.css │ └── document │ └── content.min.css ├── .eslintrc-auto-import.json ├── postcss.config.js ├── .gitignore ├── .vscode └── extensions.json ├── tsconfig.node.json ├── .editorconfig ├── auto-imports.d.ts ├── index.html ├── .env ├── mock └── sample │ └── sample.ts ├── .eslintrc ├── tsconfig.json ├── plop-templates ├── api.hbs └── view_form.hbs ├── plopfile.js ├── .gitattributes ├── vite.config.ts ├── README.md ├── tailwind.config.js └── package.json /.env.staging: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VITE_BASE_API=../api -------------------------------------------------------------------------------- /src/components/Tinymce/plugins/typesetting/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets/* 3 | public/* 4 | dist/* 5 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/ujcms-cp/HEAD/public/favicon.png -------------------------------------------------------------------------------- /src/components/TuiEditor/index.ts: -------------------------------------------------------------------------------- 1 | export { default as TuiEditor } from './TuiEditor.vue'; 2 | -------------------------------------------------------------------------------- /src/components/Tinymce/index.ts: -------------------------------------------------------------------------------- 1 | import Tinymce from './Tinymce.vue'; 2 | export default Tinymce; 3 | -------------------------------------------------------------------------------- /src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/ujcms-cp/HEAD/src/assets/404_images/404.png -------------------------------------------------------------------------------- /.eslintrc-auto-import.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "ElMessageBox": true, 4 | "ElMessage": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 'postcss-import': {}, tailwindcss: {}, autoprefixer: {} }, 3 | }; 4 | -------------------------------------------------------------------------------- /src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/ujcms-cp/HEAD/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /src/views/EnterpriseComponent.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/views/user/OrgPermissionForm.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/views/content/components/ReviewBack.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/views/interaction/CollectionSetup.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/views/system/ProcessDefinitionList.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/views/system/ProcessModelDesign.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/views/content/components/ReviewDelegate.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/views/content/components/ReviewTransfer.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/components/QueryForm/index.ts: -------------------------------------------------------------------------------- 1 | export { default as QueryForm } from './QueryForm.vue'; 2 | export { default as QueryItem } from './QueryItem.vue'; 3 | -------------------------------------------------------------------------------- /public/tinymce/skins/ui/oxide/fonts/tinymce-mobile.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/ujcms-cp/HEAD/public/tinymce/skins/ui/oxide/fonts/tinymce-mobile.woff -------------------------------------------------------------------------------- /src/components/TableList/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ColumnSetting } from './ColumnSetting.vue'; 2 | export { default as ColumnList } from './ColumnList.vue'; 3 | -------------------------------------------------------------------------------- /src/components/bpmnjs/properties-panel/properties/FormProps.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/components/bpmnjs/properties-panel/properties/ConditionProps.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/components/bpmnjs/properties-panel/properties/ListenerProps.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/components/bpmnjs/properties-panel/properties/TimerProps.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/tinymce/skins/ui/oxide-dark/fonts/tinymce-mobile.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/ujcms-cp/HEAD/public/tinymce/skins/ui/oxide-dark/fonts/tinymce-mobile.woff -------------------------------------------------------------------------------- /src/components/bpmnjs/properties-panel/properties/MultiInstanceProps.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .history 4 | dist 5 | dist-ssr 6 | *.local 7 | 8 | # VSCode project files 9 | .vscode/* 10 | !.vscode/extensions.json 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "bradlc.vscode-tailwindcss", 6 | "editorconfig.editorconfig", 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/locales/en/index.ts: -------------------------------------------------------------------------------- 1 | import common from './common.json'; 2 | import menu from './menu.json'; 3 | import user from './user.json'; 4 | 5 | export default { 6 | ...common, 7 | ...menu, 8 | ...user, 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/layout/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AppSidebar } from './AppSidebar/index.vue'; 2 | export { default as AppHeader } from './AppHeader.vue'; 3 | export { default as AppTab } from './AppTab.vue'; 4 | export { default as AppMain } from './AppMain.vue'; 5 | -------------------------------------------------------------------------------- /src/components/Upload/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ImageUpload } from './ImageUpload.vue'; 2 | export { default as ImageListUpload } from './ImageListUpload.vue'; 3 | export { default as FileListUpload } from './FileListUpload.vue'; 4 | export { default as BaseUpload } from './BaseUpload.vue'; 5 | -------------------------------------------------------------------------------- /src/components/bpmnjs/properties-panel/utils.ts: -------------------------------------------------------------------------------- 1 | export function createElement(type: string, properties: any, parent: any, bpmnFactory: any) { 2 | const element = bpmnFactory.create(type, properties); 3 | if (parent) { 4 | element.$parent = parent; 5 | } 6 | return element; 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/tailwind.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | div:focus, 7 | button:focus { 8 | outline: none; 9 | } 10 | } 11 | 12 | // @layer utilities { 13 | // .sidebar-bg { 14 | // @apply bg-gray-800; 15 | // } 16 | // } 17 | -------------------------------------------------------------------------------- /src/views/RefreshPage.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /src/components/Tinymce/plugins/indent2em/api/commands.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from 'tinymce'; 2 | import { doAction } from '../core/actions'; 3 | 4 | const register = (editor: Editor, defaultOptions: any): void => { 5 | editor.addCommand(defaultOptions.id, () => { 6 | doAction(editor, defaultOptions); 7 | }); 8 | }; 9 | 10 | export { register }; 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{js,jsx,ts,tsx,vue,hbs}] 12 | max_line_length = 180 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | [Makefile] 18 | indent_style = tab 19 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | // biome-ignore lint: disable 7 | export {} 8 | declare global { 9 | const ElMessage: typeof import('element-plus/es')['ElMessage'] 10 | const ElMessageBox: typeof import('element-plus/es')['ElMessageBox'] 11 | } 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | UJCMS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_APP_TITLE=UJCMS后台管理 2 | VITE_APP_NAME=UJCMS 3 | VITE_PORT=5173 4 | VITE_PUBLIC_PATH=./ 5 | VITE_PROXY_API=http://127.0.0.1:8080 6 | VITE_PROXY_UPLOADS=http://127.0.0.1:8080 7 | VITE_PROXY_TEMPLATES=http://127.0.0.1:8080 8 | VITE_BASE_API=/api 9 | VITE_BASE_UPLOADS=/uploads 10 | VITE_BASE_TEMPLATES=/templates 11 | VITE_I18N_LOCALE=zh-cn 12 | VITE_I18N_FALLBACK_LOCALE=zh-cn 13 | VITE_USE_MOCK=false 14 | -------------------------------------------------------------------------------- /src/views/EnterprisePage.vue: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /src/components/Tinymce/plugins/indent2em/plugin.ts: -------------------------------------------------------------------------------- 1 | import tinymce from 'tinymce'; 2 | import * as commands from './api/commands'; 3 | import * as buttons from './ui/buttons'; 4 | 5 | export default (defaultOptions): void => { 6 | tinymce.PluginManager.add('indent2em', function (editor) { 7 | commands.register(editor, defaultOptions); 8 | buttons.register(editor, defaultOptions); 9 | return {}; 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/QueryForm/QueryInput.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/stores/importDataStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | export const useImportDataStore = defineStore('ujcmsImportDataStore', { 4 | state: () => ({ 5 | datasource: {} as any, 6 | channel: {} as any, 7 | article: {} as any, 8 | }), 9 | persist: true, 10 | }); 11 | 12 | export const useImportDataPasswordStore = defineStore('ujcmsImportDataStore', { 13 | state: () => ({ 14 | password: '', 15 | }), 16 | persist: { 17 | storage: sessionStorage, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/Tinymce/plugins/indent2em/index.ts: -------------------------------------------------------------------------------- 1 | import Plugin from './plugin'; 2 | 3 | const defaultOptions = { 4 | id: 'indent2em', 5 | name: '首行缩进', 6 | tooltip: '首行缩进', 7 | icon: '', 8 | }; 9 | 10 | Plugin(defaultOptions); 11 | -------------------------------------------------------------------------------- /src/components/bpmnjs/custom-translate/customTranslate.ts: -------------------------------------------------------------------------------- 1 | import translations from './translations'; 2 | /** 3 | * https://github.com/bpmn-io/bpmn-js-examples/blob/main/i18n/src/customTranslate/customTranslate.js 4 | */ 5 | export default function customTranslate(template, replacements) { 6 | replacements = replacements || {}; 7 | 8 | // Translate 9 | template = translations[template] || template; 10 | 11 | // Replace 12 | return template.replace(/{([^}]+)}/g, function (_, key) { 13 | return replacements[key] || '{' + key + '}'; 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | -------------------------------------------------------------------------------- /src/stores/currentSiteStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | export const useCurrentSiteStore = defineStore('ujcmsCurrentSiteStore', { 4 | state: () => ({ 5 | currentSiteId: null as string | null, 6 | currentSite: null as any, 7 | }), 8 | actions: { 9 | getCurrentSiteId() { 10 | return this.currentSiteId; 11 | }, 12 | setCurrentSiteId(currentSiteId: string | null) { 13 | this.currentSiteId = currentSiteId; 14 | }, 15 | }, 16 | persist: { 17 | paths: ['currentSiteId'], 18 | storage: sessionStorage, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/views/content/components/articleUtils.ts: -------------------------------------------------------------------------------- 1 | import { getAccessToken } from '@/utils/auth'; 2 | 3 | export const openArticleLink = (status: number, url: string, dynamicUrl: string) => { 4 | if (status === 0 || status === 1) { 5 | window.open(url); 6 | } else { 7 | dynamicUrl = dynamicUrl + '?preview=true'; 8 | const index = dynamicUrl.lastIndexOf('/', dynamicUrl.lastIndexOf('/') - 1); 9 | const jwtLoginUrl = `${dynamicUrl.substring(0, index)}/auth/jwt/login?code=${getAccessToken()}&redirectUri=${encodeURIComponent(dynamicUrl)}`; 10 | window.open(jwtLoginUrl); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /public/tinymce/skins/ui/oxide/content.mobile.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | */ 7 | .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} 8 | -------------------------------------------------------------------------------- /public/tinymce/skins/ui/oxide-dark/content.mobile.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | */ 7 | .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} 8 | -------------------------------------------------------------------------------- /mock/sample/sample.ts: -------------------------------------------------------------------------------- 1 | import { MockMethod } from 'vite-plugin-mock'; 2 | export default [ 3 | { 4 | url: '/sample', 5 | method: 'get', 6 | // response: ({ query, body }: any) => { 7 | response: () => { 8 | return { 9 | code: 0, 10 | message: 'ok', 11 | data: { 12 | total: 2, 13 | list: [ 14 | { 15 | id: 100, 16 | title: 'Mock测试数据100', 17 | }, 18 | { 19 | id: 101, 20 | title: 'Mock测试数据101', 21 | }, 22 | ], 23 | }, 24 | }; 25 | }, 26 | }, 27 | ] as MockMethod[]; 28 | -------------------------------------------------------------------------------- /src/locales/zh-cn/index.ts: -------------------------------------------------------------------------------- 1 | import menu from './menu.json'; 2 | import common from './common.json'; 3 | import content from './content.json'; 4 | import file from './file.json'; 5 | import interaction from './interaction.json'; 6 | import homepage from './homepage.json'; 7 | import stat from './stat.json'; 8 | import user from './user.json'; 9 | import config from './config.json'; 10 | import log from './log.json'; 11 | import system from './system.json'; 12 | 13 | export default { 14 | ...menu, 15 | ...common, 16 | ...content, 17 | ...interaction, 18 | ...file, 19 | ...homepage, 20 | ...stat, 21 | ...user, 22 | ...config, 23 | ...log, 24 | ...system, 25 | }; 26 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @use './transition.scss'; 2 | 3 | html { 4 | height: 100%; 5 | } 6 | 7 | body { 8 | height: 100%; 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-font-smoothing: antialiased; 11 | text-rendering: optimizeLegibility; 12 | @apply bg-gray-100; 13 | } 14 | 15 | #app { 16 | height: 100%; 17 | } 18 | 19 | // global css 20 | .app-block { 21 | @apply shadow-sm; 22 | @apply bg-white; 23 | } 24 | 25 | .inline-form .el-form-item { 26 | margin-bottom: 0; 27 | } 28 | 29 | .bpmn-property-item { 30 | @apply py-1 px-2 text-xs rounded cursor-pointer text-primary; 31 | background-color: var(--el-color-primary-light-9); 32 | border: solid 1px var(--el-color-primary-light-8); 33 | } 34 | -------------------------------------------------------------------------------- /src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | -------------------------------------------------------------------------------- /src/styles/element-plus.scss: -------------------------------------------------------------------------------- 1 | @forward 'element-plus/theme-chalk/src/common/var.scss' with ( 2 | // 与 tailwindcss 的 xl 保持一致 3 | $xl: 1536px 4 | ); 5 | 6 | // 如果只是按需导入,则可以忽略以下内容。 7 | // 如果你想导入所有样式: 8 | @use 'element-plus/theme-chalk/src/index.scss' as *; 9 | 10 | // dialog body 与标题的padding太大,原30px改为10px。 11 | .el-dialog__body { 12 | padding-top: 10px; 13 | } 14 | // drawer header 下边距太大,再加上drawer body的padding,内容被挤的太靠下。原32px改为4px。 15 | .el-drawer__header { 16 | margin-bottom: 4px; 17 | } 18 | // Popconfirm 使用p的默认上下margin,但p的margin被tailwind默认样式设置为0。 19 | .el-popconfirm__main { 20 | margin-top: 14px; 21 | margin-bottom: 14px; 22 | } 23 | // Button 和 Button 之间的间距过大,原12px改为8px。 24 | .el-button + .el-button { 25 | margin-left: 8px; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/LabelTip.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /public/tinymce/skins/ui/oxide/skin.shadowdom.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | */ 7 | 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} 8 | -------------------------------------------------------------------------------- /public/tinymce/skins/ui/oxide-dark/skin.shadowdom.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | */ 7 | 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} 8 | -------------------------------------------------------------------------------- /src/components/ListMove.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /src/stores/appStateStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | export const useAppStateStore = defineStore('ujcmsAppStateStore', { 4 | state: () => ({ 5 | sidebar: true, 6 | messageBoxDisplay: false, 7 | loginBoxDisplay: false, 8 | }), 9 | actions: { 10 | setSidebar(sidebar: boolean) { 11 | this.sidebar = sidebar; 12 | }, 13 | closeSidebar() { 14 | this.sidebar = false; 15 | }, 16 | toggleSidebar() { 17 | this.sidebar = !this.sidebar; 18 | }, 19 | setMessageBoxDisplay(messageBoxDisplay: boolean) { 20 | this.messageBoxDisplay = messageBoxDisplay; 21 | }, 22 | setLoginBoxDisplay(loginBoxDisplay: boolean) { 23 | this.loginBoxDisplay = loginBoxDisplay; 24 | }, 25 | }, 26 | persist: { 27 | paths: ['sidebar'], 28 | storage: sessionStorage, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /src/locales/en/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu.homepage": "Home", 3 | "menu.personal": "Personal", 4 | "menu.personal.password": "Change Password", 5 | "menu.content": "Content", 6 | "menu.content.article": "Article", 7 | "menu.content.channel": "Channel", 8 | "menu.content.blockItem": "Block Item", 9 | "menu.content.attachment": "Attachment", 10 | "menu.content.generator": "Generator", 11 | "menu.user": "User", 12 | "menu.user.org": "Org", 13 | "menu.user.group": "Group", 14 | "menu.user.role": "Role", 15 | "menu.user.user": "User", 16 | "menu.config": "Config", 17 | "menu.config.globalSettings": "Global Settings", 18 | "menu.config.siteSettings": "Site Settings", 19 | "menu.config.block": "Block", 20 | "menu.config.model": "Model", 21 | "menu.config.dictType": "Dictionary Type", 22 | "menu.config.dict": "Dictionary", 23 | "menu.system": "System", 24 | "menu.system.site": "Site", 25 | 26 | "": "" 27 | } 28 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.25s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all 0.25s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all 0.25s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all 0.25s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /src/components/bpmnjs/palette/index.js: -------------------------------------------------------------------------------- 1 | import PaletteModule from 'diagram-js/lib/features/palette'; 2 | import CreateModule from 'diagram-js/lib/features/create'; 3 | import LassoToolModule from 'diagram-js/lib/features/lasso-tool'; 4 | import HandToolModule from 'diagram-js/lib/features/hand-tool'; 5 | import GlobalConnectModule from 'diagram-js/lib/features/global-connect'; 6 | import translate from 'diagram-js/lib/i18n/translate'; 7 | import SpaceToolModule from 'bpmn-js/lib/features/space-tool'; 8 | import FlowablePaletteProvider from './FlowablePaletteProvider'; 9 | 10 | /** 11 | * https://github.com/bpmn-io/bpmn-js/blob/develop/lib/features/palette/index.js 12 | */ 13 | export default { 14 | __depends__: [PaletteModule, CreateModule, SpaceToolModule, LassoToolModule, HandToolModule, GlobalConnectModule, translate], 15 | // 覆盖自带的 paletteProvider 全部自己定义 16 | __init__: ['paletteProvider'], 17 | paletteProvider: ['type', FlowablePaletteProvider], 18 | }; 19 | -------------------------------------------------------------------------------- /src/views/403.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 33 | -------------------------------------------------------------------------------- /src/components/bpmnjs/context-pad/index.js: -------------------------------------------------------------------------------- 1 | import DirectEditingModule from 'diagram-js-direct-editing'; 2 | import ContextPadModule from 'diagram-js/lib/features/context-pad'; 3 | import SelectionModule from 'diagram-js/lib/features/selection'; 4 | import ConnectModule from 'diagram-js/lib/features/connect'; 5 | import CreateModule from 'diagram-js/lib/features/create'; 6 | import AppendPreviewModule from 'bpmn-js/lib/features/append-preview'; 7 | import PopupMenuModule from 'bpmn-js/lib/features/popup-menu'; 8 | import FlowableContextPadProvider from './FlowableContextPadProvider'; 9 | 10 | /** 11 | * https://github.com/bpmn-io/bpmn-js/blob/develop/lib/features/context-pad/index.js 12 | */ 13 | export default { 14 | __depends__: [AppendPreviewModule, DirectEditingModule, ContextPadModule, SelectionModule, ConnectModule, CreateModule, PopupMenuModule], 15 | // 覆盖自带的 contextPadProvider 全部自己定义 16 | __init__: ['contextPadProvider'], 17 | contextPadProvider: ['type', FlowableContextPadProvider], 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/Tinymce/plugins/indent2em/core/actions.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from 'tinymce'; 2 | 3 | const doAction = (editor: Editor, defaultOptions: any): void => { 4 | editor.formatter.toggle(defaultOptions.id); 5 | editor.nodeChanged(); 6 | // const { dom, selection } = editor; 7 | // const blocks = selection.getSelectedBlocks(); 8 | // const styleName = 'text-indent'; 9 | // let textIndentExists: boolean; 10 | // tinymce.each(blocks, (block: Element) => { 11 | // const parents = dom.getParents(block, undefined, dom.getRoot()); 12 | // const parent = parents[parents.length - 1]; 13 | // if (!['p', 'div'].includes(parent.nodeName.toLowerCase())) { 14 | // return; 15 | // } 16 | // if (textIndentExists === undefined) { 17 | // // 使用 parseInt 可以将 0em 或 0px 转换成 0 18 | // textIndentExists = parseInt(dom.getStyle(parent, styleName)) > 0; 19 | // } 20 | // dom.setStyle(parent, styleName, textIndentExists ? '' : '2em'); 21 | // }); 22 | }; 23 | 24 | export { doAction }; 25 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true, 6 | }, 7 | "extends": ["eslint:recommended", "plugin:vue/vue3-recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", "./.eslintrc-auto-import.json"], 8 | "parser": "vue-eslint-parser", 9 | "parserOptions": { 10 | // "extraFileExtensions": [".vue"], 11 | // "project": ["./tsconfig.json"], 12 | "ecmaVersion": "latest", 13 | "sourceType": "module", 14 | "parser": "@typescript-eslint/parser", 15 | }, 16 | "plugins": ["vue", "@typescript-eslint"], 17 | "rules": { 18 | // "no-param-reassign": ["error", { "props": true, "ignorePropertyModificationsFor": ["state"] }], 19 | // "import/prefer-default-export": "off", 20 | 21 | // "@typescript-eslint/explicit-module-boundary-types": "off", 22 | // // 避免使用Q_作为查询参数时报错 23 | // "@typescript-eslint/camelcase": "off", 24 | 25 | // 允许使用 any 类型 26 | "@typescript-eslint/no-explicit-any": "off", 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { createPinia } from 'pinia'; 3 | import { ElRow } from 'element-plus'; 4 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; 5 | import { initRefreshInterval } from '@/stores/useCurrentUser'; 6 | import { useSysConfigStore } from '@/stores/sysConfigStore'; 7 | import App from './App.vue'; 8 | import router from './router'; 9 | import i18n from './i18n'; 10 | import '@/permission'; 11 | 12 | import '@/styles/tailwind.scss'; 13 | import '@/styles/index.scss'; 14 | 15 | // 初始化RefreshToken自动刷新 16 | initRefreshInterval(); 17 | 18 | const pinia = createPinia().use(piniaPluginPersistedstate); 19 | const app = createApp(App) 20 | .use(router) 21 | .use(pinia) 22 | // tinymce 对话框的层级太低,必须调低 ElementPlus 的 对话框层级(默认为2000) 23 | // .use(ElementPlus, { zIndex: 500 }) 24 | .use(i18n); 25 | // draggable 的 tag 属性使用了 ElRow,属于动态加载组件,在按需加载的情况下,必须全局注册才可使用 26 | app.component('ElRow', ElRow); 27 | app.mount('#app'); 28 | 29 | // 初始化系统配置 30 | useSysConfigStore().initConfig(); 31 | -------------------------------------------------------------------------------- /src/layout/components/AppSidebar/SidebarLogo.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 18 | -------------------------------------------------------------------------------- /src/components/bpmnjs/custom-translate/defaultBpmnXml.ts: -------------------------------------------------------------------------------- 1 | export default function defaultBpmnXml(key: string, name: string, category: string) { 2 | return ` 3 | 9 | 10 | 11 | 12 | 13 | 14 | `; 15 | } 16 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue'; 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | 10 | declare module 'bpmn-js/lib/Modeler'; 11 | declare module 'bpmn-js/lib/Viewer'; 12 | declare module 'bpmn-js/lib/util/ModelUtil'; 13 | declare module '@/components/bpmnjs/descriptors/flowable'; 14 | declare module '@/components/bpmnjs/palette'; 15 | declare module '@/components/bpmnjs/context-pad'; 16 | 17 | interface ImportMetaEnv { 18 | readonly VITE_APP_TITLE: string; 19 | readonly VITE_APP_NAME: string; 20 | readonly VITE_BASE_API: string; 21 | readonly VITE_PUBLIC_PATH: string; 22 | readonly VITE_I18N_LOCALE: string; 23 | readonly VITE_I18N_FALLBACK_LOCALE: string; 24 | readonly VITE_USE_MOCK: string; 25 | readonly MODE: string; 26 | } 27 | 28 | interface ImportMeta { 29 | readonly env: ImportMetaEnv; 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // "target": "ESNext", 4 | // "module": "ESNext", 5 | "target": "es2020", 6 | "module": "es2020", 7 | "useDefineForClassFields": true, 8 | "moduleResolution": "Node", 9 | "strict": true, 10 | "jsx": "preserve", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "esModuleInterop": true, 14 | "lib": ["ESNext", "DOM"], 15 | "skipLibCheck": true, 16 | "noEmit": true, 17 | 18 | "noImplicitAny": false, 19 | "sourceMap": true, 20 | "baseUrl": ".", 21 | "types": ["vite/client", "element-plus/global", "@intlify/unplugin-vue-i18n/messages"], 22 | "paths": { 23 | "@/*": ["src/*"] 24 | } 25 | }, 26 | // 使用按需导入之后,不能手动 import { ElMessageBox } from 'element-plus',否则会导致没有css样式 27 | // 应删除对 element-plus 的 import,但删除后会飘红报错,这是需要这里加上 auto-imports.d.ts 即可 28 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "mock/**/*.ts", "auto-imports.d.ts"], 29 | "references": [{ "path": "./tsconfig.node.json" }] 30 | } 31 | -------------------------------------------------------------------------------- /src/layout/components/useViewTabs.ts: -------------------------------------------------------------------------------- 1 | import { reactive, readonly } from 'vue'; 2 | 3 | export interface ViewTab { 4 | name: string | number; 5 | label: string; 6 | path: string; 7 | component: string; 8 | noCache?: boolean; 9 | } 10 | 11 | const viewTabsState: ViewTab[] = reactive([]); 12 | 13 | export const viewTabs = readonly(viewTabsState); 14 | 15 | export const addViewTab = (viewTab: ViewTab): void => { 16 | viewTabsState.push(viewTab); 17 | }; 18 | 19 | export const removeViewTab = (name: string | number): void => { 20 | const index = viewTabsState.findIndex((it) => it.name === name); 21 | viewTabsState.splice(index, 1); 22 | }; 23 | 24 | export const removeLeftViewTab = (name: string | number): void => { 25 | const index = viewTabsState.findIndex((it) => it.name === name); 26 | viewTabsState.splice(0, index); 27 | }; 28 | 29 | export const removeRightViewTab = (name: string | number): void => { 30 | const index = viewTabsState.findIndex((it) => it.name === name); 31 | viewTabsState.splice(index + 1, viewTabsState.length - index - 1); 32 | }; 33 | -------------------------------------------------------------------------------- /src/api/personal.ts: -------------------------------------------------------------------------------- 1 | import axios from '@/utils/request'; 2 | 3 | export const updatePersonalPassword = async (data: Record): Promise => (await axios.post('/backend/core/personal/password?_method=put', data)).data; 4 | 5 | export const queryMachineCode = async (): Promise => (await axios.get('/backend/core/machine/code')).data; 6 | export const queryMachineLicense = async (): Promise => (await axios.get('/backend/core/machine/license')).data; 7 | 8 | export const querySystemInfo = async (): Promise => (await axios.get('/backend/core/homepage/system-info')).data; 9 | export const querySystemMonitor = async (): Promise => (await axios.get('/backend/core/homepage/system-monitor')).data; 10 | export const querySystemLoad = async (): Promise => (await axios.get('/backend/core/homepage/system-load')).data; 11 | export const queryGeneratedKey = async (): Promise => (await axios.get('/backend/core/homepage/generated-key')).data; 12 | export const queryContentStat = async (): Promise => (await axios.get('/backend/core/homepage/content-stat')).data; 13 | -------------------------------------------------------------------------------- /plop-templates/api.hbs: -------------------------------------------------------------------------------- 1 | export const query{{pascalCase name}}{{pascalCase type}} = async (params?: Record): Promise => (await axios.get('/backend/{{kebabCase sub}}/{{kebabCase name}}', { params })).data; 2 | export const query{{pascalCase name}} = async (id: string): Promise => (await axios.get(`/backend/{{kebabCase sub}}/{{kebabCase name}}/${id}`)).data; 3 | export const create{{pascalCase name}} = async (data: Record): Promise => (await axios.post('/backend/{{kebabCase sub}}/{{kebabCase name}}', data)).data; 4 | export const update{{pascalCase name}} = async (data: Record): Promise => (await axios.post('/backend/{{kebabCase sub}}/{{kebabCase name}}?_method=put', data)).data; 5 | {{#if isList}} 6 | export const update{{pascalCase name}}Order = async (fromId: string, toId: string): Promise => (await axios.post('/backend/{{kebabCase sub}}/{{kebabCase name}}/update-order', { fromId, toId })).data; 7 | {{/if}} 8 | export const delete{{pascalCase name}} = async (data: string[]): Promise => (await axios.post('/backend/{{kebabCase sub}}/{{kebabCase name}}?_method=delete', data)).data; 9 | -------------------------------------------------------------------------------- /src/locales/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "list": "列表", 3 | "add": "新增", 4 | "edit": "编辑", 5 | "delete": "删除", 6 | "search": "查询", 7 | "back": "返回", 8 | "submit": "提交", 9 | "reset": "重置", 10 | "cancel": "取消", 11 | "save": "保存", 12 | "create": "创建", 13 | "update": "更新", 14 | "success": "成功", 15 | 16 | "confirmDelete": "您确定删除吗?", 17 | 18 | "form.prev": "上一条", 19 | "form.next": "下一条", 20 | "form.unsaved": "未保存", 21 | "form.continuous": "连续模式。保存后依然留在当前页面", 22 | "table.action": "操作", 23 | "table.selection": "选择框", 24 | "table.columnsReset": "列重置", 25 | "table.columnsSetting": "列设置", 26 | 27 | "v.required": "this field is required", 28 | "v.fix": "请修正此字段", 29 | "v.email": "请输入有效的电子邮箱", 30 | "v.url": "请输入有效的网址", 31 | "v.date": "请输入有效的日期 (YYYY-MM-DD)", 32 | "v.number": "请输入有效的数字", 33 | "v.integer": "请输入有效的整数", 34 | "v.equal": "两次输入不一致", 35 | "v.min": "请输入不小于 {min} 的数值", 36 | "v.max": "请输入不大于 {max} 的数值", 37 | "v.range": "请输入范围在 {min} 到 {max} 之间的数值", 38 | "v.minLength": "最少要输入 {minLength} 个字符", 39 | "v.maxLength": "最多可以输入 {maxLength} 个字符", 40 | "v.rangeLength": "请输入长度在 {min} 到 {max} 之间的字符串", 41 | 42 | "": "" 43 | } 44 | -------------------------------------------------------------------------------- /src/views/file/WebFileHtmlList.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 40 | -------------------------------------------------------------------------------- /src/views/personal/MachineCode.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 32 | -------------------------------------------------------------------------------- /src/layout/composables/useResizeHandler.ts: -------------------------------------------------------------------------------- 1 | import { watch, onBeforeMount, onBeforeUnmount, onMounted } from 'vue'; 2 | import { useRoute } from 'vue-router'; 3 | import { useAppStateStore } from '@/stores/appStateStore'; 4 | 5 | const { body } = document; 6 | const WIDTH = 992; // refer to Bootstrap's responsive design 7 | 8 | export default function useResizeHandler() { 9 | const route = useRoute(); 10 | const appState = useAppStateStore(); 11 | 12 | const isMobile = () => { 13 | const rect = body.getBoundingClientRect(); 14 | return rect.width - 1 < WIDTH; 15 | }; 16 | 17 | const resizeHandler = () => { 18 | if (!document.hidden && isMobile()) appState.closeSidebar(); 19 | }; 20 | 21 | watch( 22 | () => route.path, 23 | () => { 24 | if (isMobile() && appState.sidebar) appState.closeSidebar(); 25 | }, 26 | ); 27 | 28 | onBeforeMount(() => { 29 | window.addEventListener('resize', resizeHandler); 30 | }); 31 | 32 | onBeforeUnmount(() => { 33 | window.removeEventListener('resize', resizeHandler); 34 | }); 35 | 36 | onMounted(() => { 37 | if (isMobile()) { 38 | appState.closeSidebar(); 39 | } 40 | }); 41 | 42 | return { isMobile, resizeHandler }; 43 | } 44 | -------------------------------------------------------------------------------- /src/views/file/WebFileUploadList.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 40 | -------------------------------------------------------------------------------- /src/components/BreadCrumb/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 37 | -------------------------------------------------------------------------------- /src/views/personal/GeneratedKey.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 36 | -------------------------------------------------------------------------------- /src/views/file/WebFileTemplateList.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 40 | -------------------------------------------------------------------------------- /src/locales/en/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "org.name": "名称", 3 | "org.address": "地址", 4 | "org.phone": "电话", 5 | "org.contacts": "联系人", 6 | "org.order": "顺序", 7 | 8 | "group.name": "名称", 9 | "group.description": "描述", 10 | "group.order": "顺序", 11 | 12 | "role.name": "名称", 13 | "role.description": "描述", 14 | "role.permission": "功能权限", 15 | "role.order": "顺序", 16 | 17 | "user.username": "用户名", 18 | "user.group": "用户组", 19 | "user.org": "组织", 20 | "user.role": "角色", 21 | "user.password": "密码", 22 | "user.passwordAgain": "确认密码", 23 | "user.email": "电子邮箱", 24 | "user.mobile": "手机号", 25 | "user.avatar": "头像", 26 | "user.rank": "等级", 27 | "user.rank.tooltip": "数值越小等级越高。等级高的用户可以管理等级低的用户,等级低的用户不能管理等级高的用户", 28 | "user.status": "状态", 29 | 30 | "user.ext.realName": "真实姓名", 31 | "user.ext.created": "创建日期", 32 | "user.ext.gender": "性别", 33 | "user.ext.birthday": "出生日期", 34 | "user.ext.location": "居住地", 35 | "user.ext.bio": "自我介绍", 36 | "user.ext.loginDate": "最后登录日期", 37 | "user.ext.loginIp": "最后登录IP", 38 | "user.ext.loginCount": "登录次数", 39 | "user.ext.errorDate": "登录错误日期", 40 | "user.ext.errorCount": "登录错误次数", 41 | 42 | "user.error.usernameExist": "username exist", 43 | "user.error.emailExist": "email exist", 44 | "user.error.mobileExist": "mobile exist", 45 | 46 | "": "" 47 | } 48 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 31 | 32 | 46 | -------------------------------------------------------------------------------- /public/tinymce/skins/content/default/content.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | */ 7 | 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} 8 | -------------------------------------------------------------------------------- /src/utils/echarts.ts: -------------------------------------------------------------------------------- 1 | import * as echarts from 'echarts/core'; 2 | import { 3 | BarChart, 4 | // 系列类型的定义后缀都为 SeriesOption 5 | BarSeriesOption, 6 | PieChart, 7 | PieSeriesOption, 8 | LineChart, 9 | LineSeriesOption, 10 | } from 'echarts/charts'; 11 | import { 12 | TitleComponent, 13 | // 组件类型的定义后缀都为 ComponentOption 14 | TitleComponentOption, 15 | TooltipComponent, 16 | TooltipComponentOption, 17 | GridComponent, 18 | GridComponentOption, 19 | // 数据集组件 20 | DatasetComponent, 21 | DatasetComponentOption, 22 | // 内置数据转换器组件 (filter, sort) 23 | TransformComponent, 24 | LegendComponent, 25 | LegendComponentOption, 26 | } from 'echarts/components'; 27 | import { LabelLayout, UniversalTransition } from 'echarts/features'; 28 | import { CanvasRenderer } from 'echarts/renderers'; 29 | 30 | // 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型 31 | export type ECOption = echarts.ComposeOption< 32 | BarSeriesOption | PieSeriesOption | LineSeriesOption | TitleComponentOption | TooltipComponentOption | GridComponentOption | DatasetComponentOption | LegendComponentOption 33 | >; 34 | 35 | // 注册必须的组件 36 | echarts.use([ 37 | TitleComponent, 38 | TooltipComponent, 39 | GridComponent, 40 | DatasetComponent, 41 | TransformComponent, 42 | BarChart, 43 | PieChart, 44 | LineChart, 45 | LabelLayout, 46 | UniversalTransition, 47 | CanvasRenderer, 48 | LegendComponent, 49 | ]); 50 | 51 | export default echarts; 52 | -------------------------------------------------------------------------------- /public/tinymce/skins/content/writer/content.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | */ 7 | 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} 8 | -------------------------------------------------------------------------------- /plopfile.js: -------------------------------------------------------------------------------- 1 | // pnpm run plop core user org page 2 | // pnpm run plop <子系统> <分类> <模块> 3 | /* eslint-disable func-names */ 4 | export default function (plop) { 5 | // controller generator 6 | plop.setGenerator('view', { 7 | description: 'application views', 8 | prompts: [ 9 | { 10 | type: 'input', 11 | name: 'sub', 12 | message: 'sub:', 13 | }, 14 | { 15 | type: 'input', 16 | name: 'path', 17 | message: 'path:', 18 | }, 19 | { 20 | type: 'input', 21 | name: 'name', 22 | message: 'name:', 23 | }, 24 | { 25 | type: 'input', 26 | name: 'type', 27 | message: 'type:', 28 | }, 29 | ], 30 | actions: (data) => { 31 | const actions = []; 32 | actions.push({ 33 | type: 'add', 34 | path: 'src/views/{{kebabCase path}}/{{pascalCase name}}Form.vue', 35 | templateFile: 'plop-templates/view_form.hbs', 36 | }); 37 | actions.push({ 38 | type: 'add', 39 | path: 'src/views/{{kebabCase path}}/{{pascalCase name}}List.vue', 40 | templateFile: `plop-templates/view_${data.type}.hbs`, 41 | }); 42 | actions.push({ 43 | type: 'append', 44 | path: 'src/api/{{kebabCase path}}.ts', 45 | templateFile: 'plop-templates/api.hbs', 46 | data: { isList: data.type === 'list' }, 47 | }); 48 | return actions; 49 | }, 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /public/tinymce/skins/content/dark/content.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | */ 7 | 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} 8 | -------------------------------------------------------------------------------- /src/locales/zh-cn/stat.json: -------------------------------------------------------------------------------- 1 | { 2 | "visit.now": "现在", 3 | "visit.today": "今日", 4 | "visit.yesterday": "昨日", 5 | "visit.last7day": "最近7日", 6 | "visit.last30day": "最近30日", 7 | "visit.last3month": "最近3月", 8 | "visit.last6month": "最近6月", 9 | "visit.lastYear": "最近一年", 10 | "visit.all": "全部", 11 | "visit.pv": "浏览量(PV)", 12 | "visit.uv": "访客数(UV)", 13 | "visit.ip": "IP数", 14 | "visit.bounceRate": "跳出率", 15 | "visit.averageDuration": "平均访问时长", 16 | "visit.averagePv": "平均访问页数", 17 | "visit.url": "受访页面", 18 | "visit.entryUrl": "入口页面", 19 | 20 | "visitTrend.yesterdayPv": "昨日浏览量(PV)", 21 | "visitTrend.todayPv": "今日浏览量(PV)", 22 | 23 | "visitVisitor.newVisitor": "新访客", 24 | "visitVisitor.oldVisitor": "老访客", 25 | "visitVisitor.pv": "浏览量", 26 | "visitVisitor.uv": "访客数", 27 | 28 | "visitSource.name": "来源", 29 | "visitSource.type.DIRECT": "直接访问", 30 | "visitSource.type.INNER": "内部链接", 31 | "visitSource.type.OUTER": "外部链接", 32 | "visitSource.type.SEARCH": "搜索引擎", 33 | "visitDevice.name": "设备", 34 | "visitOs.name": "操作系统", 35 | "visitBrowser.name": "浏览器", 36 | 37 | "articleStat.user":"用户", 38 | "articleStat.org":"组织", 39 | "articleStat.channel":"栏目", 40 | "articleStat.total":"总录入数", 41 | "articleStat.published":"已发布数", 42 | "articleStat.unpublished":"未发布数", 43 | 44 | "performanceStat.user":"用户", 45 | "performanceStat.org":"组织", 46 | "performanceStat.score":"分", 47 | "performanceStat.totalCount":"总发布数", 48 | "performanceStat.totalScore":"总绩效分", 49 | 50 | "": "" 51 | } 52 | -------------------------------------------------------------------------------- /public/tinymce/skins/content/document/content.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | */ 7 | @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} 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################### 2 | # Git Line Endings # 3 | ############################### 4 | 5 | # Set default behaviour to automatically normalize line endings. 6 | # * text=auto 7 | # 文本文件全部使用lf换行,eslint prettier等工具保持一致。 8 | * text=auto eol=lf 9 | 10 | # Force batch scripts to always use CRLF line endings so that if a repo is accessed 11 | # in Windows via a file share from Linux, the scripts will work. 12 | *.{cmd,[cC][mM][dD]} text eol=crlf 13 | *.{bat,[bB][aA][tT]} text eol=crlf 14 | 15 | # Force bash scripts to always use LF line endings so that if a repo is accessed 16 | # in Unix via a file share from Windows, the scripts will work. 17 | *.sh text eol=lf 18 | 19 | ############################### 20 | # Git Large File System (LFS) # 21 | ############################### 22 | 23 | # # Archives 24 | # *.7z filter=lfs diff=lfs merge=lfs -text 25 | # *.br filter=lfs diff=lfs merge=lfs -text 26 | # *.gz filter=lfs diff=lfs merge=lfs -text 27 | # *.tar filter=lfs diff=lfs merge=lfs -text 28 | # *.zip filter=lfs diff=lfs merge=lfs -text 29 | 30 | # # Documents 31 | # *.pdf filter=lfs diff=lfs merge=lfs -text 32 | 33 | # # Images 34 | # *.gif filter=lfs diff=lfs merge=lfs -text 35 | # *.ico filter=lfs diff=lfs merge=lfs -text 36 | # *.jpg filter=lfs diff=lfs merge=lfs -text 37 | # *.pdf filter=lfs diff=lfs merge=lfs -text 38 | # *.png filter=lfs diff=lfs merge=lfs -text 39 | # *.psd filter=lfs diff=lfs merge=lfs -text 40 | # *.webp filter=lfs diff=lfs merge=lfs -text 41 | 42 | # # Fonts 43 | # *.woff2 filter=lfs diff=lfs merge=lfs -text 44 | 45 | # # Other 46 | # *.exe filter=lfs diff=lfs merge=lfs -text 47 | -------------------------------------------------------------------------------- /plop-templates/view_form.hbs: -------------------------------------------------------------------------------- 1 | 16 | 17 | 42 | -------------------------------------------------------------------------------- /src/components/TableList/ColumnSetting.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 33 | -------------------------------------------------------------------------------- /src/stores/columnSettingsStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | export interface ColumnState { 4 | title: string; 5 | display: boolean; 6 | } 7 | 8 | export const mergeColumns = (settings: ColumnState[], origins: ColumnState[]) => { 9 | // 去除不存在的列 10 | for (let i = 0, len = settings.length; i < len; ) { 11 | if (origins.findIndex((column) => column.title === settings[i].title) === -1) { 12 | settings.splice(i, 1); 13 | len -= 1; 14 | } else { 15 | i += 1; 16 | } 17 | } 18 | // 增加未记录的列 19 | origins.forEach((column, index) => { 20 | if (settings.findIndex((item) => item.title === column.title) === -1) { 21 | settings.splice(index, 0, { ...column }); 22 | } 23 | }); 24 | return settings; 25 | }; 26 | 27 | export const useColumnSettingsStore = defineStore('ujcmsColumnSettings', { 28 | state: () => ({ 29 | originSettings: {} as Record, 30 | crrrentSettings: {} as Record, 31 | }), 32 | actions: { 33 | getCurrentSettings(name: string) { 34 | if (!this.crrrentSettings[name]) this.crrrentSettings[name] = []; 35 | return this.crrrentSettings[name]; 36 | }, 37 | setCurrentSettings(name: string, origins: ColumnState[]) { 38 | this.crrrentSettings[name] = origins; 39 | }, 40 | getOriginSettings(name: string) { 41 | if (!this.originSettings[name]) this.originSettings[name] = []; 42 | return this.originSettings[name]; 43 | }, 44 | setOriginSettings(name: string, origins: ColumnState[]) { 45 | this.originSettings[name] = origins; 46 | }, 47 | }, 48 | persist: { 49 | paths: ['crrrentSettings'], 50 | }, 51 | }); 52 | -------------------------------------------------------------------------------- /src/utils/sm.ts: -------------------------------------------------------------------------------- 1 | import Base64 from 'crypto-js/enc-base64'; 2 | import Hex from 'crypto-js/enc-hex'; 3 | import { CipherMode, sm2 } from 'sm-crypto'; 4 | 5 | /** 6 | * SM2 加密。后台Java使用BC库解密,必须在JS库加密后的Hex值前加上'04',且转为Base64编码格式,减小数据传输量。 7 | * @param msg 待加密的信息 8 | * @param publicKey 公钥。QD值,Hex编码。 9 | * @param cipherMode 模式。1: C1C3C2, 0: C1C2C3。默认 1 10 | * @returns 加密后的字符串。Base64编码。 11 | */ 12 | export const sm2Encrypt = (msg: string, publicKey: string, cipherMode?: CipherMode): string => Base64.stringify(Hex.parse('04' + sm2.doEncrypt(msg, publicKey, cipherMode))); 13 | 14 | /** 15 | * SM2 解密。后台Java使用BC库加密,必须将Base64编码转为Hex编码,然后去掉前面'04'字符。 16 | * @param encryptData 待解密的字符串。Base64编码。 17 | * @param privateKey 私钥。QD值,Hex编码。 18 | * @param cipherMode 模式。1: C1C3C2, 0: C1C2C3。默认 1 19 | * @returns 解密后的字符串。 20 | */ 21 | export const sm2Decrypt = (encryptData: string, privateKey: string, cipherMode?: CipherMode): string => { 22 | let data = Hex.stringify(Base64.parse(encryptData)); 23 | // 去除前面两位'04'字符 24 | data = data.substring(2, data.length); 25 | return sm2.doDecrypt(data, privateKey, cipherMode); 26 | }; 27 | 28 | /** 29 | * SM2 签名。加上 { hash:true, der:true } 参数,与后台Java BC库默认签名一致。 30 | * @param msg 待签名信息 31 | * @param privateKey 私钥 32 | * @returns 签名。Hex编码 33 | */ 34 | export const sm2Signature = (msg: string, privateKey: string): string => sm2.doSignature(msg, privateKey, { hash: true, der: true }); 35 | 36 | /** 37 | * SM2 验签。加上 { hash:true, der:true } 参数,与后台Java BC库默认验签一致。 38 | * @param msg 待验证信息 39 | * @param signHex 签名。Hex编码。 40 | * @param publicKey 公钥 41 | * @returns 是否验签成功 42 | */ 43 | export const sm2VerifySignature = (msg: string, signHex: string, publicKey: string): boolean => sm2.doVerifySignature(msg, signHex, publicKey, { hash: true, der: true }); 44 | -------------------------------------------------------------------------------- /src/stores/sysConfigStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import _ from 'lodash'; 3 | import { queryConfig } from '@/api/login'; 4 | 5 | export const useSysConfigStore = defineStore('ujcmsSysConfigStore', { 6 | state: () => ({ 7 | base: { 8 | uploadUrlPrefix: '/uploads', 9 | filesExtensionBlacklist: 'exe,com,bat,jsp,jspx,asp,aspx,php', 10 | uploadsExtensionBlacklist: 'exe,com,bat,jsp,jspx,asp,aspx,php,html,htm,xhtml,xml,shtml,shtm', 11 | } as any, 12 | upload: { 13 | imageTypes: 'jpg,jpeg,png,gif', 14 | imageInputAccept: '.jpg,.jpeg,.png,.gif', 15 | videoInputAccept: '.mp4,.m3u8', 16 | audioInputAccept: '.mp3,.ogg,.wav', 17 | mediaInputAccept: '.mp4,.m3u8,.mp3,.ogg,.wav', 18 | libraryInputAccept: '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx', 19 | docInputAccept: '.doc,.docx,.xls,.xlsx', 20 | fileInputAccept: '.zip,.7z,.gz,.bz2,.iso,.rar,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.mp4,.m3u8,.mp3,.ogg', 21 | imageLimitByte: 0, 22 | videoLimitByte: 0, 23 | audioLimitByte: 0, 24 | mediaLimitByte: 0, 25 | libraryLimitByte: 0, 26 | docLimitByte: 0, 27 | fileLimitByte: 0, 28 | }, 29 | security: { 30 | passwordMinLength: 0, 31 | passwordMaxLength: 64, 32 | passwordStrength: 0, 33 | passwordPattern: '.*', 34 | ssrfList: [], 35 | }, 36 | register: { 37 | largeAvatarSize: 960, 38 | }, 39 | }), 40 | actions: { 41 | async initConfig() { 42 | const config = await queryConfig(); 43 | this.base = _.omit(config, ['upload', 'register', 'security']); 44 | this.upload = config.upload; 45 | this.register = config.register; 46 | this.security = config.security; 47 | }, 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /src/permission.ts: -------------------------------------------------------------------------------- 1 | import NProgress from 'nprogress'; 2 | import 'nprogress/nprogress.css'; 3 | import { RouteLocationNormalized } from 'vue-router'; 4 | import i18n from '@/i18n'; 5 | import { getAccessToken } from '@/utils/auth'; 6 | import { hasCurrentUser, fetchCurrentUser, hasPermission } from '@/stores/useCurrentUser'; 7 | import router from './router'; 8 | 9 | NProgress.configure({ showSpinner: false }); 10 | 11 | const LOGIN_PATH = '/login'; 12 | 13 | router.beforeEach(async (to: RouteLocationNormalized) => { 14 | const isLogin = getAccessToken() !== undefined; 15 | // 不需要权限 16 | if (!to.meta?.requiresPermission) { 17 | // 已登录状态访问登录页面,跳转到首页 18 | if (to.path === LOGIN_PATH && isLogin) return '/'; 19 | NProgress.start(); 20 | return true; 21 | } 22 | // 需要权限 23 | const toLogin = `${LOGIN_PATH}?redirect=${to.path}`; 24 | // 未登录,跳转到登录页面 25 | if (!isLogin) return toLogin; 26 | NProgress.start(); 27 | if (!hasCurrentUser()) { 28 | const user = await fetchCurrentUser(); 29 | // 没有获取到当前用户数据,代表accessToken已经失效,需要重新登录。 30 | if (!user) { 31 | NProgress.done(); 32 | return toLogin; 33 | } 34 | } 35 | // 没有权限 36 | if (!hasPermission(to.meta?.requiresPermission)) { 37 | NProgress.done(); 38 | if (to.path === '/') { 39 | return '/403'; 40 | } 41 | return '/'; 42 | } 43 | return true; 44 | }); 45 | 46 | router.afterEach((to: RouteLocationNormalized) => { 47 | document.title = getPageTitle(to.meta.title); 48 | NProgress.done(); 49 | }); 50 | 51 | const title = import.meta.env.VITE_APP_TITLE || 'UJCMS后台管理'; 52 | 53 | function getPageTitle(pageTitle?: string): string { 54 | if (pageTitle) { 55 | const { 56 | global: { t }, 57 | } = i18n; 58 | return `${t(pageTitle)} - ${title}`; 59 | } 60 | return `${title}`; 61 | } 62 | -------------------------------------------------------------------------------- /src/locales/zh-cn/file.json: -------------------------------------------------------------------------------- 1 | { 2 | "webFile.op.uploadZip": "ZIP上传", 3 | "webFile.op.upload": "上传", 4 | "webFile.op.downloadZip": "ZIP下载", 5 | "webFile.op.view": "浏览", 6 | "webFile.op.mkdir": "新建文件夹", 7 | "webFile.op.mkfile": "新建文件", 8 | "webFile.op.rename": "重命名", 9 | "webFile.op.copy": "复制", 10 | "webFile.op.move": "移动", 11 | "webFile.name": "名称", 12 | "webFile.newName": "新名称", 13 | "webFile.dir": "文件夹", 14 | "webFile.lastModified": "修改日期", 15 | "webFile.fileType": "类型", 16 | "webFile.fileType.DIRECTORY": "文件夹", 17 | "webFile.fileType.ZIP": "ZIP文件", 18 | "webFile.fileType.TEXT": "TXT文件", 19 | "webFile.fileType.IMAGE": "图片文件", 20 | "webFile.fileType.FILE": "文件", 21 | "webFile.size": "大小", 22 | "webFile.text": "正文", 23 | "webFile.image": "图片", 24 | "webFile.error.sameDir": "不能复制或移动到原文件夹,请选择其它文件夹", 25 | 26 | "backupDatabase.op.backup": "备份", 27 | "backupDatabase.op.restore": "恢复", 28 | "backupDatabase.confirm.backup": "您确定备份吗?", 29 | "backupDatabase.confirm.restore": "恢复操作将覆盖现有数据,且不可逆。您确定恢复吗?", 30 | "backupDatabase.error.unsupported": "目前不支持该数据库的备份操作", 31 | 32 | "backupTemplates.op.backup": "备份", 33 | "backupTemplates.op.restore": "恢复", 34 | "backupTemplates.confirm.backup": "您确定备份吗?", 35 | "backupTemplates.confirm.restore": "恢复操作将覆盖现有数据,且不可逆。您确定恢复吗?", 36 | 37 | "backupUploads.op.backup": "备份", 38 | "backupUploads.op.restore": "恢复", 39 | "backupUploads.confirm.backup": "您确定备份吗?", 40 | "backupUploads.confirm.restore": "恢复操作将覆盖现有数据,且不可逆。您确定恢复吗?", 41 | 42 | "incrementalUploads.op.backup": "备份", 43 | "incrementalUploads.op.merge": "合并", 44 | "incrementalUploads.op.restore": "恢复", 45 | "incrementalUploads.confirm.backup": "您确定备份吗?", 46 | "incrementalUploads.confirm.merge": "您确定合并吗?", 47 | "incrementalUploads.confirm.restore": "恢复操作将覆盖现有数据,且不可逆。您确定恢复吗?", 48 | 49 | "": "" 50 | } 51 | -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n'; 2 | import { Language } from 'element-plus/es/locale/index'; 3 | import ElZhCn from 'element-plus/es/locale/lang/zh-cn'; 4 | import ElEn from 'element-plus/es/locale/lang/en'; 5 | import { getCookieLocale } from '@/utils/common'; 6 | import en from './locales/en'; 7 | import zhCn from './locales/zh-cn'; 8 | 9 | const messages = { 10 | 'zh-cn': { 11 | ...zhCn, 12 | }, 13 | en: { 14 | ...en, 15 | }, 16 | }; 17 | 18 | const numberFormats: any = { 19 | 'zh-cn': { 20 | decimal: { 21 | style: 'decimal', 22 | minimumFractionDigits: 2, 23 | maximumFractionDigits: 2, 24 | }, 25 | percent: { 26 | style: 'percent', 27 | useGrouping: false, 28 | }, 29 | }, 30 | en: { 31 | decimal: { 32 | style: 'decimal', 33 | minimumFractionDigits: 2, 34 | maximumFractionDigits: 2, 35 | }, 36 | percent: { 37 | style: 'percent', 38 | useGrouping: false, 39 | }, 40 | }, 41 | }; 42 | 43 | const elMessages: Record = { 44 | 'zh-cn': ElZhCn, 45 | en: ElEn, 46 | }; 47 | 48 | export const languages: Record = { 'zh-cn': '中文', en: 'English' }; 49 | 50 | const i18nFallbackLocale = import.meta.env.VITE_I18N_FALLBACK_LOCALE || 'zh-cn'; 51 | 52 | export function getElementPlusLocale(lang: string): Language { 53 | return elMessages[lang] ?? elMessages[i18nFallbackLocale] ?? ElZhCn; 54 | } 55 | 56 | export function getLanguage(): string { 57 | const chooseLanguage = getCookieLocale(); 58 | if (chooseLanguage) return chooseLanguage; 59 | return import.meta.env.VITE_I18N_LOCALE || 'zh-cn'; 60 | } 61 | 62 | export default createI18n({ 63 | legacy: false, 64 | locale: getLanguage(), 65 | fallbackLocale: i18nFallbackLocale, 66 | globalInjection: true, 67 | numberFormats, 68 | messages, 69 | }); 70 | -------------------------------------------------------------------------------- /src/layout/components/AppSidebar/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 58 | -------------------------------------------------------------------------------- /src/components/bpmnjs/properties-panel/properties/NormalProps.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/components/TableList/useColumns.ts: -------------------------------------------------------------------------------- 1 | import { reactive, toRef } from 'vue'; 2 | 3 | export interface ColumnState { 4 | title: string; 5 | display: boolean; 6 | } 7 | 8 | const COLUMN_SETTINGS = 'ujcms_column_settings'; 9 | 10 | function fetchColumnSettings(): Record { 11 | const settings = localStorage.getItem(COLUMN_SETTINGS); 12 | return settings ? JSON.parse(settings) : {}; 13 | } 14 | 15 | const originStore: Record = reactive({}); 16 | const settingStore: Record = reactive(fetchColumnSettings()); 17 | 18 | export function storeColumnSettings() { 19 | localStorage.setItem(COLUMN_SETTINGS, JSON.stringify(settingStore)); 20 | } 21 | export const getColumnOrigins = (name: string) => { 22 | if (!originStore[name]) originStore[name] = []; 23 | return toRef(originStore, name); 24 | }; 25 | export const mergeColumns = (settings: ColumnState[], origins: ColumnState[]) => { 26 | // 去除不存在的列 27 | for (let i = 0, len = settings.length; i < len; ) { 28 | if (origins.findIndex((column) => column.title === settings[i].title) === -1) { 29 | settings.splice(i, 1); 30 | len -= 1; 31 | } else { 32 | i += 1; 33 | } 34 | } 35 | // 增加未记录的列 36 | origins.forEach((column) => { 37 | if (settings.findIndex((item) => item.title === column.title) === -1) { 38 | settings.push({ ...column }); 39 | } 40 | }); 41 | return settings; 42 | }; 43 | export const setColumnOrigins = (name: string, origins: ColumnState[]) => { 44 | originStore[name] = origins; 45 | if (!settingStore[name]) settingStore[name] = []; 46 | const settings = settingStore[name]; 47 | mergeColumns(settings, origins); 48 | }; 49 | export const getColumnSettings = (name: string) => { 50 | if (!settingStore[name]) settingStore[name] = []; 51 | return toRef(settingStore, name); 52 | }; 53 | // export const setColumnSettings = (name: string, settings: ColumnState[]) => { 54 | // settingStore[name] = settings; 55 | // }; 56 | -------------------------------------------------------------------------------- /src/views/content/components/ReviewFormProperties.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/views/config/MessageBoardTypeForm.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 46 | -------------------------------------------------------------------------------- /src/api/login.ts: -------------------------------------------------------------------------------- 1 | import axios from '@/utils/request'; 2 | 3 | export interface LoginParam { 4 | username: string; 5 | password: string; 6 | browser?: boolean; 7 | } 8 | 9 | export interface RefreshTokenParam { 10 | refreshToken: string; 11 | browser?: boolean; 12 | } 13 | 14 | export const accountLogin = async (data: LoginParam): Promise => (await axios.post('/auth/jwt/login', data)).data; 15 | export const accountLogout = async (refreshToken: string): Promise => (await axios.post('/auth/jwt/logout', { refreshToken })).data; 16 | export const accountRefreshToken = async (data: RefreshTokenParam): Promise => (await axios.post('/auth/jwt/refresh-token', data)).data; 17 | export const queryCurrentUser = async (): Promise => (await axios.get('/env/current-user')).data; 18 | export const queryCurrentSiteList = async (): Promise => (await axios.get('/env/current-site-list')).data; 19 | export const queryClientPublicKey = async (): Promise => (await axios.get('/env/client-public-key')).data; 20 | export const queryConfig = async (): Promise => (await axios.get('/env/config')).data; 21 | export const queryCaptcha = async (): Promise => (await axios.get('/captcha')).data; 22 | export const queryIsDisplayCaptcha = async (): Promise => (await axios.get('/captcha/is-display')).data; 23 | export const sendMobileMessage = async (captchaToken: string, captcha: string, mobile: string, usage: number): Promise => 24 | (await axios.post('/sms/mobile', { captchaToken, captcha, receiver: mobile, usage })).data; 25 | export const queryIsMfaLogin = async (): Promise => (await axios.get('/env/is-mfa-login')).data; 26 | export const tryCaptcha = async (token: string, captcha: string): Promise => (await axios.get('/captcha/try', { params: { token, captcha } })).data; 27 | export const mobileNotExist = async (mobile: string): Promise => (await axios.get('/user/mobile-not-exist', { params: { mobile } })).data; 28 | export const updatePassword = async (data: Record): Promise => (await axios.post('/update-password?_method=put', data)).data; 29 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv, ConfigEnv } from 'vite'; 2 | import { resolve } from 'path'; 3 | import vue from '@vitejs/plugin-vue'; 4 | import legacy from '@vitejs/plugin-legacy'; 5 | import vueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'; 6 | import { viteMockServe } from 'vite-plugin-mock'; 7 | import AutoImport from 'unplugin-auto-import/vite'; 8 | import Components from 'unplugin-vue-components/vite'; 9 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'; 10 | 11 | export default defineConfig(({ mode }: ConfigEnv) => { 12 | // 加载 .env 文件 13 | const env = loadEnv(mode, process.cwd()); 14 | return { 15 | base: env.VITE_PUBLIC_PATH, 16 | resolve: { 17 | alias: { 18 | '@/': `${resolve(__dirname, 'src')}/`, 19 | }, 20 | }, 21 | css: { 22 | preprocessorOptions: { 23 | scss: { 24 | api: 'modern-compiler', 25 | }, 26 | }, 27 | }, 28 | server: { 29 | host: '127.0.0.1', 30 | port: Number(env.VITE_PORT), 31 | proxy: { 32 | [env.VITE_BASE_API]: { 33 | target: env.VITE_PROXY_API, 34 | changeOrigin: true, 35 | }, 36 | [env.VITE_BASE_UPLOADS]: { 37 | target: env.VITE_PROXY_UPLOADS, 38 | changeOrigin: true, 39 | }, 40 | [env.VITE_BASE_TEMPLATES]: { 41 | target: env.VITE_PROXY_TEMPLATES, 42 | changeOrigin: true, 43 | }, 44 | }, 45 | }, 46 | build: { 47 | chunkSizeWarningLimit: 2000, 48 | }, 49 | plugins: [ 50 | vue(), 51 | legacy({ 52 | targets: ['defaults', 'not IE 11'], 53 | }), 54 | vueI18nPlugin({ 55 | include: [resolve(__dirname, './locales/**')], 56 | }), 57 | viteMockServe({ 58 | ignore: /^_/, 59 | mockPath: 'mock', 60 | enable: env.VITE_USE_MOCK === 'true', 61 | }), 62 | AutoImport({ 63 | resolvers: [ElementPlusResolver()], 64 | eslintrc: { enabled: true }, 65 | }), 66 | Components({ 67 | resolvers: [ElementPlusResolver()], 68 | }), 69 | ], 70 | }; 71 | }); 72 | -------------------------------------------------------------------------------- /src/components/user/UserSelect.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie'; 2 | 3 | const JWT_ACCESS_TOKEN = 'jwt-access-token'; 4 | const JWT_ACCESS_AT = 'jwt-access-at'; 5 | const JWT_REFRESH_TOKEN = 'jwt-refresh-token'; 6 | const JWT_REFRESH_AT = 'jwt-refresh-at'; 7 | const JWT_SESSION_TIMEOUT = 'jwt-session-timeout'; 8 | 9 | export const getAccessToken = (): string | undefined => Cookies.get(JWT_ACCESS_TOKEN); 10 | export const setAccessToken = (token: string): void => { 11 | Cookies.set(JWT_ACCESS_TOKEN, token); 12 | }; 13 | export const removeAccessToken = (): void => Cookies.remove(JWT_ACCESS_TOKEN); 14 | 15 | export const getRefreshToken = (): string | undefined => Cookies.get(JWT_REFRESH_TOKEN); 16 | export const setRefreshToken = (token: string): void => { 17 | Cookies.set(JWT_REFRESH_TOKEN, token); 18 | }; 19 | export const removeRefreshToken = (): void => { 20 | Cookies.remove(JWT_REFRESH_TOKEN); 21 | }; 22 | 23 | export const getRefreshAt = (): number => { 24 | const refreshAt = Cookies.get(JWT_REFRESH_AT); 25 | return refreshAt ? Number(refreshAt) : 0; 26 | }; 27 | export const setRefreshAt = (refreshAt: number): void => { 28 | Cookies.set(JWT_REFRESH_AT, String(refreshAt)); 29 | }; 30 | export const removeRefreshAt = (): void => { 31 | Cookies.remove(JWT_REFRESH_AT); 32 | }; 33 | 34 | export const getAccessAt = (): number => { 35 | const accessAt = Cookies.get(JWT_ACCESS_AT); 36 | return accessAt ? Number(accessAt) : 0; 37 | }; 38 | export const setAccessAt = (accessAt: number): void => { 39 | Cookies.set(JWT_ACCESS_AT, String(accessAt)); 40 | }; 41 | export const removeAccessAt = () => Cookies.remove(JWT_ACCESS_AT); 42 | 43 | export const getSessionTimeout = (): number => { 44 | const sessionTimeout = Cookies.get(JWT_SESSION_TIMEOUT); 45 | // 默认 30 分钟 46 | return sessionTimeout ? Number(sessionTimeout) : 30; 47 | }; 48 | export const setSessionTimeout = (sessionTimeout: number): void => { 49 | Cookies.set(JWT_SESSION_TIMEOUT, String(sessionTimeout)); 50 | }; 51 | export const removeSessionTimeout = (): void => { 52 | Cookies.remove(JWT_SESSION_TIMEOUT); 53 | }; 54 | 55 | export const getAuthHeaders = (): any => { 56 | const accessToken = getAccessToken(); 57 | return { Authorization: accessToken ? `Bearer ${accessToken}` : '' }; 58 | }; 59 | -------------------------------------------------------------------------------- /src/api/log.ts: -------------------------------------------------------------------------------- 1 | import axios from '@/utils/request'; 2 | 3 | export const queryShortMessagePage = async (params?: Record): Promise => (await axios.get('/backend/core/short-message', { params })).data; 4 | export const queryShortMessage = async (id: string): Promise => (await axios.get(`/backend/core/short-message/${id}`)).data; 5 | export const createShortMessage = async (data: Record): Promise => (await axios.post('/backend/core/short-message', data)).data; 6 | export const updateShortMessage = async (data: Record): Promise => (await axios.post('/backend/core/short-message?_method=put', data)).data; 7 | export const deleteShortMessage = async (data: string[]): Promise => (await axios.post('/backend/core/short-message?_method=delete', data)).data; 8 | 9 | export const queryLoginLogPage = async (params?: Record): Promise => (await axios.get('/backend/core/login-log', { params })).data; 10 | export const queryLoginLog = async (id: string): Promise => (await axios.get(`/backend/core/login-log/${id}`)).data; 11 | export const createLoginLog = async (data: Record): Promise => (await axios.post('/backend/core/login-log', data)).data; 12 | export const updateLoginLog = async (data: Record): Promise => (await axios.post('/backend/core/login-log?_method=put', data)).data; 13 | export const deleteLoginLog = async (data: string[]): Promise => (await axios.post('/backend/core/login-log?_method=delete', data)).data; 14 | 15 | export const queryOperationLogPage = async (params?: Record): Promise => (await axios.get('/backend/core/operation-log', { params })).data; 16 | export const queryOperationLog = async (id: string): Promise => (await axios.get(`/backend/core/operation-log/${id}`)).data; 17 | export const createOperationLog = async (data: Record): Promise => (await axios.post('/backend/core/operation-log', data)).data; 18 | export const updateOperationLog = async (data: Record): Promise => (await axios.post('/backend/core/operation-log?_method=put', data)).data; 19 | export const deleteOperationLog = async (data: string[]): Promise => (await axios.post('/backend/core/operation-log?_method=delete', data)).data; 20 | -------------------------------------------------------------------------------- /src/views/config/ModelForm.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 56 | -------------------------------------------------------------------------------- /src/layout/components/AppSidebar/MenuItem.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UJCMS-CP 2 | 3 | UJCMS-CP是UJCMS的后台前端项目。使用 Vue 3、Vite、TypeScript、ElementPlus、TailwindCSS、VueRouter、VueI18n 开发。 4 | 5 | 需要启动`UJCMS`主项目才可以使用,不可单独运行(无法访问后端接口)。 6 | 7 | 如不需要修改`UJCMS`的后台界面,则不必启动此项目。`UJCMS`的`/src/main/webapp/cp`目录已包含本项目编译后的代码,直接运行`UJCMS`主项目即可。 8 | 9 | ## 搭建步骤 10 | 11 | * 使用 vscode 开发工具。 12 | * 安装 node 环境。Node 20.12+ 版本。 13 | * 安装 pnpm。执行:npm install -g pnpm 14 | * 使用淘宝 npm 镜像。执行:pnpm set registry https://registry.npmmirror.com/ 15 | * 安装依赖。执行:pnpm install 16 | * 启动程序。执行:pnpm run dev 17 | * 访问:http://127.0.0.1:5173 18 | * 用户名:admin,密码:password 19 | 20 | ## 修改后台标识 21 | 22 | * 修改`.env`文件中的`VITE_APP_TITLE=UJCMS后台管理`配置,可改变浏览器页签上的标题。 23 | * 修改`.env`文件中的`VITE_APP_NAME=UJCMS`配置,可改变登录页、后台左侧导航等处的`UJCMS`标识。 24 | * 替换`/public/favicon.png`图片,可改变浏览器标签页上显示的图标。 25 | * 修改`/src/layout/components/AppSidebar/SidebarLogo.vue`文件中的`svg`图标,可改变后台左侧导航处LOGO图标。 26 | 27 | ## 编译及部署 28 | 29 | * 执行:pnpm run build 30 | * 编译后的程序在`/dist`目录。 31 | * 将`/dist`目录里的文件拷贝至主项目UJCMS的`/src/main/webapp/cp`目录下(先将原目录下的文件删除)。 32 | 33 | ## 常见错误 34 | 35 | 编译时出现 `Javascript Heap out of memory` 错误,代表内存溢出。可以设置 `NODE_OPTIONS` 环境变量为 `--max-old-space-size=8192`。 36 | 37 | ## 前后端分开部署 38 | 39 | 通常前端和后端程序部署到同一个应用,即将前端程序复制到主项目UJCMS的`/cp`目录。以演示站点为例,后端接口地址为`https://demo.ujcms.com/api`,前端访问地址则为`https://demo.ujcms.com/cp/`。这样可以避免跨域问题,是最简单的部署方式。 40 | 41 | 如果需要将前后端部署到不同域名或端口,如后端接口地址为`http://www.example.com/api`,前端地址为`http://www.frontend.com`。由于前后端域名不同,前端直接访问后端接口会出现跨域错误。这时需要在前端服务器部署反向代理,解决跨域问题。以`nginx`为例: 42 | 43 | ``` 44 | # 代理 api 接口 45 | location /api { 46 | proxy_pass http://www.example.com; 47 | } 48 | # 代理上传文件 49 | location /uploads { 50 | proxy_pass http://www.example.com; 51 | } 52 | ``` 53 | 54 | 开发模式启动时,情况也类似,后端接口地址为`http://localhost:8080/api`,前端地址为`http://localhost:9520`。前后端端口不同,也属于跨域。但前端开发在状态启动时,会自动开启代理,相关配置在`vite.config.ts`文件中。类似以下代码: 55 | 56 | ``` 57 | proxy: { 58 | '/api': { 59 | target: env.VITE_PROXY, 60 | changeOrigin: true, 61 | }, 62 | '/uploads': { 63 | target: env.VITE_PROXY, 64 | changeOrigin: true, 65 | }, 66 | }, 67 | ``` 68 | 69 | ## 菜单和角色权限配置 70 | 71 | 如果进行二次开发,需新增功能,可在`/src/router/index.ts`文件中配置菜单。 72 | 73 | 并可在`/src/data.ts`文件中配置权限,配置好的权限会在`角色管理 - 权限设置`中的`功能权限`中显示。 74 | 75 | 配置内容: 76 | 77 | ``` 78 | export function getPermsTreeData(): any[] { 79 | const { 80 | global: { t }, 81 | } = i18n; 82 | const perms = [ 83 | ... 84 | ] 85 | } 86 | ``` 87 | -------------------------------------------------------------------------------- /src/components/TableList/ColumnList.vue: -------------------------------------------------------------------------------- 1 | 53 | -------------------------------------------------------------------------------- /src/views/user/GroupForm.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 58 | -------------------------------------------------------------------------------- /src/views/content/components/ImageExtractor.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 71 | -------------------------------------------------------------------------------- /src/views/interaction/ExampleForm.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 58 | -------------------------------------------------------------------------------- /src/views/content/TagForm.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 59 | -------------------------------------------------------------------------------- /src/locales/zh-cn/homepage.json: -------------------------------------------------------------------------------- 1 | { 2 | "contentStat.article": "文章数量", 3 | "contentStat.channel": "栏目数量", 4 | "contentStat.user": "用户数量", 5 | "contentStat.attachment": "附件数量", 6 | "contentStat.last7day": "最近7天", 7 | 8 | "todo.pendingArticle": "待审核文章", 9 | "todo.rejectedArticle": "已退回文章", 10 | "todo.pendingForm": "待审核表单", 11 | "todo.rejectedForm": "已退回表单", 12 | "todo.unreviewedMessageBoard": "未审核留言", 13 | 14 | "systemInfo.version": "版本号", 15 | "systemInfo.os": "操作系统", 16 | "systemInfo.osName": "系统名称", 17 | "systemInfo.osArch": "系统架构", 18 | "systemInfo.osVersion": "系统版本", 19 | "systemInfo.java": "Java Runtime", 20 | "systemInfo.javaRuntimeName": "Java Runtime名称", 21 | "systemInfo.javaRuntimeVersion": "Java Runtime版本", 22 | "systemInfo.javaVersion": "Java版本", 23 | "systemInfo.javaVendor": "Java提供商", 24 | "systemInfo.javaVm": "Java虚拟机", 25 | "systemInfo.javaVmName": "虚拟机名称", 26 | "systemInfo.javaVmVersion": "虚拟机版本", 27 | "systemInfo.javaVmVendor": "虚拟机供应商", 28 | "systemInfo.userName": "系统用户", 29 | "systemInfo.userDir": "用户主目录", 30 | "systemInfo.javaIoTmpdir": "用户临时目录", 31 | "systemInfo.memory": "内存使用率", 32 | "systemInfo.maxMemory": "最大内存", 33 | "systemInfo.maxMemory.tooltip": "JVM可使用的最大内存", 34 | "systemInfo.totalMemory": "总内存", 35 | "systemInfo.totalMemory.tooltip": "JVM已申请的内存。总内存小于等于最大内存,JVM通常不会一次性把所有内存都申请下来", 36 | "systemInfo.usedMemory": "已用内存", 37 | "systemInfo.freeMemory": "空闲内存", 38 | "systemInfo.freeMemory.tooltip": "已申请但未使用的内存", 39 | "systemInfo.remainingMemory": "剩余内存", 40 | "systemInfo.remainingMemory.tooltip": "JVM未申请内存", 41 | "systemInfo.availableMemory": "可用内存", 42 | "systemInfo.availableMemory.tooltip": "未申请的内存加上空闲内存的总和", 43 | "systemInfo.upDays": "运行天数", 44 | "systemInfo.upDays.unit": "天", 45 | "systemInfo.upDays.tooltip": "JVM运行天数。即程序运行天数,非服务器运行天数", 46 | 47 | "systemMonitor.osUpDays": "运行天数", 48 | "systemMonitor.osName": "操作系统", 49 | "systemMonitor.cpuName": "CPU名称", 50 | "systemMonitor.cpuVendorFreq": "主频", 51 | "systemMonitor.cpuLoad": "CPU利用率", 52 | "systemMonitor.cpuCores": "物理核心", 53 | "systemMonitor.cpuLogicalCores": "逻辑核心", 54 | "systemMonitor.memory": "服务器内存", 55 | "systemMonitor.memoryTotal": "总内存", 56 | "systemMonitor.memoryUsed": "已用内存", 57 | "systemMonitor.memoryAvailable": "可用内存", 58 | "systemMonitor.fileStores": "文件系统", 59 | "systemMonitor.fileStore.mount": "挂载点", 60 | "systemMonitor.fileStore.type": "类型", 61 | "systemMonitor.fileStore.space": "空间", 62 | "systemMonitor.fileStore.total": "总空间", 63 | "systemMonitor.fileStore.used": "已用空间", 64 | "systemMonitor.fileStore.usable": "可用空间", 65 | 66 | "": "" 67 | } 68 | -------------------------------------------------------------------------------- /src/api/stat.ts: -------------------------------------------------------------------------------- 1 | import axios from '@/utils/request'; 2 | 3 | export const queryTrendStat = async (params?: Record): Promise => (await axios.get('/backend/ext/visit/trend-stat', { params })).data; 4 | export const queryVisitedPageStat = async (params?: Record): Promise => (await axios.get('/backend/ext/visit/visited-page-stat', { params })).data; 5 | export const queryEntryPageStat = async (params?: Record): Promise => (await axios.get('/backend/ext/visit/entry-page-stat', { params })).data; 6 | export const queryVisitorStat = async (params?: Record): Promise => (await axios.get('/backend/ext/visit/visitor-stat', { params })).data; 7 | export const querySourceStat = async (params?: Record): Promise => (await axios.get('/backend/ext/visit/source-stat', { params })).data; 8 | export const queryCountryStat = async (params?: Record): Promise => (await axios.get('/backend/ext/visit/country-stat', { params })).data; 9 | export const queryProvinceStat = async (params?: Record): Promise => (await axios.get('/backend/ext/visit/province-stat', { params })).data; 10 | export const queryDeviceStat = async (params?: Record): Promise => (await axios.get('/backend/ext/visit/device-stat', { params })).data; 11 | export const queryOsStat = async (params?: Record): Promise => (await axios.get('/backend/ext/visit/os-stat', { params })).data; 12 | export const queryBrowserStat = async (params?: Record): Promise => (await axios.get('/backend/ext/visit/browser-stat', { params })).data; 13 | export const querySourceTypeStat = async (params?: Record): Promise => (await axios.get('/backend/ext/visit/source-type-stat', { params })).data; 14 | 15 | export const queryArticleStatByUser = async (params?: Record): Promise => (await axios.get('/backend/ext/article-stat/by-user', { params })).data; 16 | export const queryArticleStatByOrg = async (params?: Record): Promise => (await axios.get('/backend/ext/article-stat/by-org', { params })).data; 17 | export const queryArticleStatByChannel = async (params?: Record): Promise => (await axios.get('/backend/ext/article-stat/by-channel', { params })).data; 18 | 19 | export const queryPerformanceStatByUser = async (params?: Record): Promise => (await axios.get('/backend/ext/performance-stat/by-user', { params })).data; 20 | export const queryPerformanceStatByOrg = async (params?: Record): Promise => (await axios.get('/backend/ext/performance-stat/by-org', { params })).data; 21 | -------------------------------------------------------------------------------- /src/components/TuiEditor/utils.ts: -------------------------------------------------------------------------------- 1 | import { imageUploadUrl } from '@/api/config'; 2 | import { getAuthHeaders } from '@/utils/auth'; 3 | import { getSiteHeaders } from '@/utils/common'; 4 | import Editor from '@toast-ui/editor'; 5 | 6 | /** 7 | * 在对话框中使用编辑器时,点击更多工具按钮后,再点击页面其它地方,弹出的工具不会消失。需要认为的抛出一个点击事件。 8 | */ 9 | export const clickOutside = (event: Event) => { 10 | if (event.bubbles || !event.cancelable || event.composed) { 11 | const myEvent = new Event('click', { bubbles: false, cancelable: true, composed: false }); 12 | document.dispatchEvent(myEvent); 13 | } 14 | }; 15 | 16 | export const toggleFullScreen = (editor: Editor, element: HTMLElement, height: string): void => { 17 | const style = element.style; 18 | if (style.height !== '100vh') { 19 | style.height = '100vh'; 20 | style.width = '100vw'; 21 | style.position = 'fixed'; 22 | style.zIndex = '10000000000'; 23 | style.top = '0px'; 24 | style.left = '0px'; 25 | style.backgroundColor = 'white'; 26 | editor.changePreviewStyle('vertical'); 27 | } else { 28 | style.height = height; 29 | style.width = ''; 30 | style.position = ''; 31 | style.zIndex = ''; 32 | style.top = ''; 33 | style.left = ''; 34 | style.backgroundColor = ''; 35 | editor.changePreviewStyle('tab'); 36 | } 37 | }; 38 | 39 | export const addImageBlobHook = (blob: Blob | File, callback: any): void => { 40 | const xhr = new XMLHttpRequest(); 41 | xhr.open('POST', imageUploadUrl); 42 | Object.entries(getSiteHeaders()).forEach(([key, value]: any) => xhr.setRequestHeader(key, value)); 43 | 44 | // xhr.upload.onprogress = (e) => { 45 | // (e.loaded / e.total) * 100; 46 | // }; 47 | 48 | xhr.onload = () => { 49 | if (xhr.status === 403) { 50 | ElMessageBox.alert(`HTTP Error: ${xhr.status}`, { type: 'warning' }); 51 | return; 52 | } 53 | 54 | if (xhr.status < 200 || xhr.status >= 300) { 55 | ElMessageBox.alert(`HTTP Error: ${xhr.status}`, { type: 'warning' }); 56 | return; 57 | } 58 | 59 | const json = JSON.parse(xhr.responseText); 60 | 61 | if (!json || typeof json.url !== 'string') { 62 | ElMessageBox.alert(`Invalid JSON: ${xhr.responseText}`, { type: 'warning' }); 63 | return; 64 | } 65 | callback(json.url); 66 | }; 67 | 68 | xhr.onerror = () => { 69 | ElMessageBox.alert(`Image upload failed due to a XHR Transport error. Code: ${xhr.status}`, { type: 'warning' }); 70 | }; 71 | 72 | const formData = new FormData(); 73 | formData.append('file', blob); 74 | 75 | Object.entries(getAuthHeaders()).forEach(([key, value]: any) => xhr.setRequestHeader(key, value)); 76 | xhr.send(formData); 77 | }; 78 | -------------------------------------------------------------------------------- /src/components/QueryForm/QueryItem.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 63 | -------------------------------------------------------------------------------- /src/components/bpmnjs/properties-panel/FlowablePropertiesPannel.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 60 | 61 | 66 | -------------------------------------------------------------------------------- /src/views/content/DictForm.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 65 | -------------------------------------------------------------------------------- /src/views/personal/MachineLicense.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 65 | -------------------------------------------------------------------------------- /src/components/Tinymce/plugins/indent2em/ui/buttons.ts: -------------------------------------------------------------------------------- 1 | import { Editor, Ui } from 'tinymce'; 2 | 3 | const register = (editor: Editor, defaultOptions: any): void => { 4 | const onAction = () => editor.execCommand(defaultOptions.id); 5 | 6 | // const onSetup = (buttonApi: Ui.Toolbar.ToolbarToggleButtonInstanceApi) => { 7 | // const indentSelector = '*[style*="text-indent"], *[data-mce-style*="text-indent"]'; 8 | // const containerSelector = 'p,div'; 9 | // const unbindActiveSelectorChange = editor.selection.selectorChangedWithUnbind(indentSelector, (active: boolean, args: { node: Node; parents: Element[] }) => { 10 | // const parent = editor.dom.getParent(args.node, containerSelector); 11 | // // 使用 parseInt 可以将 0em 或 0px 转换成 0 12 | // buttonApi.setActive(parent != null && parseInt(editor.dom.getStyle(parent, 'text-indent')) > 0 && active); 13 | // }).unbind; 14 | // const unbindDesabledSelectorChange = editor.selection.selectorChangedWithUnbind(containerSelector, (active: boolean) => { 15 | // buttonApi.setDisabled(!active); 16 | // }).unbind; 17 | // return () => { 18 | // unbindActiveSelectorChange(); 19 | // unbindDesabledSelectorChange(); 20 | // }; 21 | // }; 22 | 23 | // const onSetup = (api: Ui.Toolbar.ToolbarToggleButtonInstanceApi) => { 24 | // const { dom } = editor; 25 | // const nodeChangeHandler = (e: EditorEvent) => { 26 | // const { parents } = e; 27 | // const parent = parents[parents.length - 1]; 28 | // const enabled = ['p', 'div'].includes(parent?.nodeName.toLowerCase()); 29 | // api.setDisabled(!enabled); 30 | // // 使用 parseInt 可以将 0em 或 0px 转换成 0 31 | // api.setActive(enabled && parseInt(dom.getStyle(parent, 'text-indent')) > 0); 32 | // }; 33 | // editor.on('NodeChange', nodeChangeHandler); 34 | // return () => editor.off('NodeChange', nodeChangeHandler); 35 | // }; 36 | 37 | const onSetup = (api: Ui.Toolbar.ToolbarToggleButtonInstanceApi) => { 38 | const indent2em = [ 39 | { 40 | selector: 'p,div', 41 | styles: { 42 | textIndent: '2em', 43 | }, 44 | inherit: false, 45 | }, 46 | ]; 47 | editor.formatter.register(defaultOptions.id, indent2em); 48 | const nodeChangeHandler = () => { 49 | api.setActive(editor.formatter.match(defaultOptions.id)); 50 | }; 51 | editor.on('NodeChange', nodeChangeHandler); 52 | return () => editor.off('NodeChange', nodeChangeHandler); 53 | }; 54 | 55 | if (!editor.ui.registry.getAll().icons[defaultOptions.id]) { 56 | editor.ui.registry.addIcon(defaultOptions.id, defaultOptions.icon); 57 | } 58 | 59 | editor.ui.registry.addToggleButton(defaultOptions.id, { 60 | icon: defaultOptions.id, 61 | tooltip: defaultOptions.tooltip, 62 | onAction, 63 | onSetup, 64 | }); 65 | 66 | editor.ui.registry.addToggleMenuItem(defaultOptions.id, { 67 | icon: defaultOptions.id, 68 | text: defaultOptions.tooltip, 69 | onAction, 70 | }); 71 | }; 72 | 73 | export { register }; 74 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | import colors from 'tailwindcss/colors'; 3 | 4 | export default { 5 | important: true, 6 | content: ['./src/**/*.{vue,ts}'], 7 | theme: { 8 | fontFamily: { 9 | sans: [ 10 | 'ui-sans-serif', 11 | 'system-ui', 12 | '-apple-system', 13 | 'BlinkMacSystemFont', 14 | '"Segoe UI"', 15 | 'Roboto', 16 | '"Helvetica Neue"', 17 | 'Arial', 18 | '"Noto Sans"', 19 | '"PingFang SC"', 20 | '"Hiragino Sans GB"', 21 | '"Microsoft YaHei"', 22 | '"WenQuanYi Micro Hei"', 23 | 'sans-serif', 24 | '"Apple Color Emoji"', 25 | '"Segoe UI Emoji"', 26 | '"Segoe UI Symbol"', 27 | '"Noto Color Emoji"', 28 | ], 29 | }, 30 | screens: { 31 | // sm: '768px', 32 | md: '992px', 33 | // lg: '1200px', 34 | xl: '1536px', 35 | }, 36 | colors: { 37 | transparent: 'transparent', 38 | current: 'currentColor', 39 | black: colors.black, 40 | white: colors.white, 41 | gray: { 42 | ...colors.gray, 43 | primary: '#303133', 44 | regular: '#606266', 45 | secondary: '#909399', 46 | placeholder: '#A8ABB2', 47 | disabled: '#C0C4CC', 48 | }, 49 | primary: { 50 | DEFAULT: '#409eff', 51 | light: '#a0cfff', 52 | lighter: '#ecf5ff', 53 | }, 54 | success: { 55 | DEFAULT: '#67c23a', 56 | light: '#b3e19d', 57 | lighter: '#f0f9eb', 58 | }, 59 | warning: { 60 | DEFAULT: '#e6a23c', 61 | light: '#f3d19e', 62 | lighter: '#fdf6ec', 63 | }, 64 | danger: { 65 | DEFAULT: '#f56c6c', 66 | light: '#fab6b6', 67 | lighter: '#fef0f0', 68 | }, 69 | purple: { 70 | DEFAULT: colors.purple[500], 71 | light: colors.purple[300], 72 | lighter: colors.purple[100], 73 | }, 74 | secondary: { 75 | DEFAULT: '#909399', 76 | light: '#c8c9cc', 77 | lighter: '#f4f4f5', 78 | }, 79 | }, 80 | extend: { 81 | transitionDuration: { 82 | // vue 文档中提到,过度效果的时间一般在0.1s-0.4s之间,而0.25s会是一个比较好的值 83 | // https://v3.vuejs.org/guide/transitions-overview.html#timing 84 | 250: '250ms', 85 | 350: '350ms', 86 | }, 87 | transitionProperty: { 88 | width: 'width', 89 | margin: 'margin', 90 | }, 91 | // el-menu 展开时最小宽度是 200px 92 | // el-menu 折叠时宽度是 64px 93 | width: { 94 | sidebar: '200px', 95 | 'sidebar-collapse': '64px', 96 | }, 97 | margin: { 98 | sidebar: '200px', 99 | 'sidebar-collapse': '64px', 100 | }, 101 | }, 102 | }, 103 | variants: { 104 | // extend: { 105 | // borderStyle: ['hover'], 106 | // }, 107 | }, 108 | plugins: [], 109 | // corePlugins: { 110 | // preflight: false, 111 | // }, 112 | }; 113 | -------------------------------------------------------------------------------- /src/locales/zh-cn/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu.homepage": "首页", 3 | 4 | "menu.personal": "个人设置", 5 | "menu.personal.password": "修改密码", 6 | "menu.personal.machine.code": "许可请求码", 7 | "menu.personal.machine.license": "许可证信息", 8 | "menu.personal.homepage.systemInfo": "系统信息", 9 | "menu.personal.homepage.systemMonitor": "系统监控", 10 | "menu.personal.homepage.generatedKey": "密钥生成器", 11 | 12 | "menu.content": "内容", 13 | "menu.content.article": "文章管理", 14 | "menu.content.articleReview": "文章审核", 15 | "menu.content.channel": "栏目管理", 16 | "menu.content.blockItem": "区块管理", 17 | "menu.content.dict": "字典管理", 18 | "menu.content.tag": "TAG管理", 19 | "menu.content.form": "表单管理", 20 | "menu.content.formReview": "表单审核", 21 | "menu.content.attachment": "附件管理", 22 | "menu.content.generator": "生成管理", 23 | 24 | "menu.interaction": "互动", 25 | "menu.interaction.messageBoard": "留言管理", 26 | "menu.interaction.vote": "投票管理", 27 | "menu.interaction.survey": "调查问卷", 28 | "menu.interaction.collection": "采集管理", 29 | "menu.interaction.example": "示例管理", 30 | 31 | "menu.file": "文件", 32 | "menu.file.webFileTemplate": "模板文件", 33 | "menu.file.webFileUpload": "上传文件", 34 | "menu.file.webFileHtml": "HTML文件", 35 | "menu.file.backupTemplates": "模板备份", 36 | "menu.file.backupUploads": "上传备份", 37 | "menu.file.incrementalUploads": "上传增量备份", 38 | "menu.file.backupDatabase": "数据库备份", 39 | 40 | "menu.stat": "统计", 41 | "menu.stat.visit": "访问分析", 42 | "menu.stat.visitor": "访客分析", 43 | "menu.stat.visitTrend": "访问趋势", 44 | "menu.stat.visitedPage": "受访页面", 45 | "menu.stat.entryPage": "入口页面", 46 | "menu.stat.visitVisitor": "新老访客", 47 | "menu.stat.visitSource": "访问来源", 48 | "menu.stat.visitRegion": "地域分布", 49 | "menu.stat.visitCountry": "国家分布", 50 | "menu.stat.visitProvince": "省份分布", 51 | "menu.stat.visitEnv": "访客环境", 52 | "menu.stat.visitDevice": "访客设备", 53 | "menu.stat.visitOs": "访客操作系统", 54 | "menu.stat.visitBrowser": "访客浏览器", 55 | "menu.stat.articleStat": "文章统计", 56 | "menu.stat.articleStat.byUser": "按用户统计", 57 | "menu.stat.articleStat.byOrg": "按组织统计", 58 | "menu.stat.articleStat.byChannel": "按栏目统计", 59 | "menu.stat.performanceStat": "绩效统计", 60 | "menu.stat.performanceStat.byUser": "用户绩效", 61 | "menu.stat.performanceStat.byOrg": "组织绩效", 62 | 63 | "menu.user": "用户", 64 | "menu.user.org": "组织管理", 65 | "menu.user.group": "用户组管理", 66 | "menu.user.role": "角色管理", 67 | "menu.user.user": "用户管理", 68 | 69 | "menu.config": "配置", 70 | "menu.config.globalSettings": "全局设置", 71 | "menu.config.siteSettings": "站点设置", 72 | "menu.config.block": "区块设置", 73 | "menu.config.model": "模型管理", 74 | "menu.config.dictType": "字典设置", 75 | "menu.config.formType": "表单类型", 76 | "menu.config.performanceType": "绩效类型", 77 | "menu.config.messageBoardType": "留言类型", 78 | 79 | "menu.log": "日志", 80 | "menu.log.loginLog": "登录日志", 81 | "menu.log.operationLog": "操作日志", 82 | "menu.log.shortMessage": "短信日志", 83 | 84 | "menu.system": "系统", 85 | "menu.system.site": "站点管理", 86 | "menu.system.processModel": "流程模型", 87 | "menu.system.processInstance": "流程实例", 88 | "menu.system.processHistory": "历史流程", 89 | "menu.system.task": "任务管理", 90 | "menu.system.sensitiveWord": "敏感词管理", 91 | "menu.system.errorWord": "易错词管理", 92 | "menu.system.importData": "数据迁移", 93 | 94 | "": "" 95 | } 96 | -------------------------------------------------------------------------------- /src/components/QueryForm/QueryForm.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 84 | -------------------------------------------------------------------------------- /src/components/Upload/ImageCropper.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 79 | 80 | 88 | -------------------------------------------------------------------------------- /src/views/user/OrgForm.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 81 | -------------------------------------------------------------------------------- /src/views/user/UserPermissionForm.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 87 | -------------------------------------------------------------------------------- /src/views/user/RoleForm.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 78 | -------------------------------------------------------------------------------- /src/views/config/DictTypeForm.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 78 | -------------------------------------------------------------------------------- /src/views/stat/EntryPage.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 99 | -------------------------------------------------------------------------------- /src/views/stat/VisitSource.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 99 | -------------------------------------------------------------------------------- /src/views/stat/VisitedPage.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 99 | -------------------------------------------------------------------------------- /src/views/content/ChannelPermissionForm.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 95 | -------------------------------------------------------------------------------- /src/views/content/ChannelMergeForm.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 102 | -------------------------------------------------------------------------------- /src/views/stat/VisitTrend.vue: -------------------------------------------------------------------------------- 1 | 92 | 93 | 107 | -------------------------------------------------------------------------------- /src/views/file/WebFileBatch.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 92 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import { h } from 'vue'; 2 | import axios from 'axios'; 3 | import dayjs from 'dayjs'; 4 | import i18n from '@/i18n'; 5 | import { getAuthHeaders, removeAccessToken, removeRefreshToken, setAccessAt } from '@/utils/auth'; 6 | import { getSiteHeaders } from '@/utils/common'; 7 | import { useCurrentSiteStore } from '@/stores/currentSiteStore'; 8 | import { useAppStateStore } from '@/stores/appStateStore'; 9 | 10 | const { 11 | global: { t }, 12 | } = i18n; 13 | const showMessageBox = () => { 14 | const appState = useAppStateStore(); 15 | if (!appState.loginBoxDisplay && !appState.messageBoxDisplay) { 16 | window.location.reload(); 17 | // session超时会自动显示登录界面,应该不需要保留在原页面的提示框 18 | // setMessageBoxDisplay(true); 19 | // ElMessageBox.confirm(t('confirmLogin'), { 20 | // cancelButtonText: t('cancel'), 21 | // confirmButtonText: t('loginAgain'), 22 | // type: 'warning', 23 | // callback: (action: string) => { 24 | // if (action === 'cancel' || action === 'close') { 25 | // setMessageBoxDisplay(false); 26 | // return; 27 | // } 28 | // if (action === 'confirm') { 29 | // // 未登录。刷新页面以触发登录。无法直接使用router,会导致其它函数不可用的奇怪问题。 30 | // window.location.reload(); 31 | // } 32 | // }, 33 | // }); 34 | } 35 | }; 36 | 37 | const service = axios.create({ 38 | baseURL: import.meta.env.VITE_BASE_API, 39 | timeout: 30000, 40 | }); 41 | 42 | service.interceptors.request.use( 43 | (config) => { 44 | setAccessAt(Date.now()); 45 | // eslint-disable-next-line 46 | config.headers = { ...config.headers, ...getAuthHeaders(), ...getSiteHeaders() }; 47 | return config; 48 | }, 49 | (error) => Promise.reject(error), 50 | ); 51 | 52 | export interface ErrorInfo { 53 | message?: string; 54 | path?: string; 55 | error?: string; 56 | exception?: string; 57 | trace?: string; 58 | timestamp?: Date; 59 | status?: number; 60 | } 61 | 62 | export const handleError = ({ timestamp, message, path, error, exception, trace, status }: ErrorInfo): void => { 63 | if (exception === 'com.ujcms.cms.core.web.support.SiteForbiddenException') { 64 | //没有当前站点权限,清空站点信息,刷新页面以获取默认站点 65 | useCurrentSiteStore().setCurrentSiteId(null); 66 | window.location.reload(); 67 | } else if (exception === 'com.ujcms.commons.web.exception.LogicException') { 68 | ElMessageBox.alert(message, { type: 'warning' }); 69 | } else if (status === 401) { 70 | removeAccessToken(); 71 | removeRefreshToken(); 72 | showMessageBox(); 73 | } else if (status === 403) { 74 | ElMessageBox({ 75 | title: String(status), 76 | message: h('div', null, [h('p', { class: 'text-lg' }, t('error.forbidden')), h('p', { class: 'mt-2' }, path), h('p', { class: 'mt-2' }, message)]), 77 | }); 78 | } else { 79 | ElMessageBox({ 80 | title: t('error.title'), 81 | message: h('div', null, [ 82 | h('h', null, [h('span', { class: 'text-4xl' }, status), h('span', { class: ['ml-2', 'text-xl'] }, error)]), 83 | h('p', { class: 'mt-2' }, dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')), 84 | h('p', { class: 'mt-2' }, path), 85 | h('p', { class: 'mt-2' }, message), 86 | h('p', { class: 'mt-2' }, exception), 87 | h('pre', { class: 'mt-2' }, [h('code', { class: ['whitespace-pre-wrap'] }, trace)]), 88 | ]), 89 | customStyle: { maxWidth: '100%' }, 90 | }); 91 | } 92 | }; 93 | 94 | service.interceptors.response.use( 95 | (response) => response, 96 | (e) => { 97 | const { 98 | response: { data, status, statusText }, 99 | } = e; 100 | // spring boot 的响应 101 | if (data) { 102 | handleError(data); 103 | return Promise.reject(data.error); 104 | } 105 | // spring scurity BearerTokenAuthenticationEntryPoint 的响应 106 | handleError({ status }); 107 | return Promise.reject(statusText); 108 | }, 109 | ); 110 | 111 | export default service; 112 | -------------------------------------------------------------------------------- /src/views/system/TaskForm.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 95 | -------------------------------------------------------------------------------- /src/views/user/UserPasswordForm.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ujcms-cp", 3 | "version": "10.1.2", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint src/**/*.{vue,ts,tsx} --fix", 11 | "prettier": "prettier --write src/**/*.{json,js,ts,tsx,css,scss,vue,html,md}", 12 | "lint-staged": "lint-staged", 13 | "prepare": "husky", 14 | "plop": "plop" 15 | }, 16 | "dependencies": { 17 | "@codemirror/lang-html": "^6.4.9", 18 | "@element-plus/icons-vue": "^2.3.1", 19 | "@toast-ui/chart": "^4.6.1", 20 | "@toast-ui/editor": "^3.2.2", 21 | "@toast-ui/editor-plugin-chart": "^3.0.1", 22 | "@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0", 23 | "@toast-ui/editor-plugin-table-merged-cell": "^3.1.0", 24 | "@toast-ui/editor-plugin-uml": "^3.0.1", 25 | "@vueuse/components": "^10.11.1", 26 | "@vueuse/core": "^10.11.1", 27 | "axios": "^1.7.7", 28 | "bpmn-js": "^17.11.1", 29 | "codemirror": "^6.0.1", 30 | "core-js": "^3.39.0", 31 | "cropperjs": "^1.6.2", 32 | "crypto-js": "^4.2.0", 33 | "dayjs": "^1.11.13", 34 | "diagram-js": "^14.11.3", 35 | "diagram-js-direct-editing": "^2.1.2", 36 | "domutils": "^3.1.0", 37 | "echarts": "^5.5.1", 38 | "element-plus": "~2.8.8", 39 | "entities": "^4.5.0", 40 | "file-saver": "^2.0.5", 41 | "htmlparser2": "^9.1.0", 42 | "js-cookie": "^3.0.5", 43 | "lodash": "^4.17.21", 44 | "min-dash": "^4.2.2", 45 | "nprogress": "^0.2.0", 46 | "path-to-regexp": "^6.3.0", 47 | "pinia": "^2.2.6", 48 | "pinia-plugin-persistedstate": "^3.2.3", 49 | "prismjs": "^1.29.0", 50 | "sm-crypto": "^0.3.13", 51 | "sortablejs": "1.14.0", 52 | "tinymce": "~5.9.2", 53 | "vue": "^3.5.13", 54 | "vue-codemirror": "^6.1.1", 55 | "vue-i18n": "^9.14.1", 56 | "vue-router": "^4.4.5", 57 | "vuedraggable": "^4.1.0" 58 | }, 59 | "devDependencies": { 60 | "@intlify/unplugin-vue-i18n": "^4.0.0", 61 | "@types/crypto-js": "^4.2.2", 62 | "@types/file-saver": "^2.0.7", 63 | "@types/js-cookie": "^3.0.6", 64 | "@types/lodash": "^4.17.13", 65 | "@types/node": "^20.17.6", 66 | "@types/nprogress": "^0.2.3", 67 | "@types/prismjs": "^1.26.5", 68 | "@types/sm-crypto": "^0.3.4", 69 | "@typescript-eslint/eslint-plugin": "^6.21.0", 70 | "@typescript-eslint/parser": "^6.21.0", 71 | "@vitejs/plugin-legacy": "^5.4.3", 72 | "@vitejs/plugin-vue": "^5.2.0", 73 | "autoprefixer": "^10.4.20", 74 | "eslint": "^8.57.1", 75 | "eslint-config-prettier": "^9.1.0", 76 | "eslint-plugin-prettier": "^5.2.1", 77 | "eslint-plugin-vue": "^9.31.0", 78 | "husky": "^9.1.6", 79 | "lint-staged": "^15.2.10", 80 | "mockjs": "^1.1.0", 81 | "plop": "^4.0.1", 82 | "postcss": "^8.4.49", 83 | "postcss-import": "^16.1.0", 84 | "prettier": "^3.3.3", 85 | "sass": "^1.81.0", 86 | "tailwindcss": "^3.4.15", 87 | "terser": "^5.36.0", 88 | "typescript": "^5.6.3", 89 | "unplugin-auto-import": "^0.19.0", 90 | "unplugin-vue-components": "^0.27.5", 91 | "vite": "^5.4.17", 92 | "vite-plugin-mock": "^3.0.2", 93 | "vue-tsc": "^2.1.10" 94 | }, 95 | "prettier": { 96 | "printWidth": 180, 97 | "singleQuote": true, 98 | "trailingComma": "all", 99 | "arrowParens": "always" 100 | }, 101 | "lint-staged": { 102 | "*.{js,ts,tsx}": [ 103 | "eslint --fix", 104 | "prettier --write" 105 | ], 106 | "*.vue": [ 107 | "eslint --fix", 108 | "prettier --write" 109 | ], 110 | "{!(package)*.json,.!(browserslist)*rc}": [ 111 | "prettier --write--parser json" 112 | ], 113 | "package.json": [ 114 | "prettier --write" 115 | ], 116 | "*.{scss,html}": [ 117 | "prettier --write" 118 | ], 119 | "*.md": [ 120 | "prettier --write" 121 | ] 122 | }, 123 | "browserslist": [ 124 | "> 1%", 125 | "last 2 versions", 126 | "not dead" 127 | ], 128 | "engines": { 129 | "node": ">=20.12" 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/views/content/ChannelMoveForm.vue: -------------------------------------------------------------------------------- 1 | 82 | 83 | 106 | --------------------------------------------------------------------------------