21 |
22 |
appState.closeSidebar()" />
23 |
24 |
29 |
30 |
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 |
18 | $emit('finished')"
33 | >
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/components/TableList/ColumnSetting.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 | (visible = true)">
22 |
23 |
24 | {{ $t('reset') }}
25 |
26 | -
27 | {{ column.title }}
28 |
29 |
30 |
31 |
32 |
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 |
37 |
38 |
39 |
40 |
41 |
42 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/components/bpmnjs/properties-panel/properties/NormalProps.vue:
--------------------------------------------------------------------------------
1 |
35 |
36 |
37 |
38 |
39 | {{ $t('flowable.groups.normal') }}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
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 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | {{ $t('ok') }}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/views/config/MessageBoardTypeForm.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 | $emit('update:modelValue', event)"
32 | @finished="() => $emit('finished')"
33 | >
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
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 |
53 |
54 |
55 |
56 | fetchData()"
64 | @current-change="() => fetchData()"
65 | >
66 |
67 |
68 |
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 |
21 | $emit('update:modelValue', event)"
37 | @finished="() => $emit('finished')"
38 | >
39 |
40 |
41 |
42 |
43 |
44 |
45 | {{ $t(`model.scope.${n}`) }}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/layout/components/AppSidebar/MenuItem.vue:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {{ $t(title) }}
44 |
45 |
46 |
47 |
48 |
49 |
50 | {{ $t(title) }}
51 |
52 |
53 |
54 |
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 |
21 | $emit('update:modelValue', event)"
37 | @finished="() => $emit('finished')"
38 | >
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/views/content/components/ImageExtractor.vue:
--------------------------------------------------------------------------------
1 |
49 |
50 |
51 | $emit('update:modelValue', event)"
57 | >
58 |
59 |
60 | checkImg(image)">
61 |
62 |
![]()
63 |
64 |
65 |
66 |
67 | finish()">{{ $t('ok') }}
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/views/interaction/ExampleForm.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 | $emit('update:modelValue', event)"
32 | @finished="() => $emit('finished')"
33 | >
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/views/content/TagForm.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 | $emit('update:modelValue', event)"
33 | @finished="() => $emit('finished')"
34 | >
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
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 |
23 |
24 |
25 |
26 |
27 |
28 |
37 |
46 |
47 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/src/components/bpmnjs/properties-panel/FlowablePropertiesPannel.vue:
--------------------------------------------------------------------------------
1 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
66 |
--------------------------------------------------------------------------------
/src/views/content/DictForm.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 | $emit('update:modelValue', event)"
38 | @finished="() => $emit('finished')"
39 | >
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/views/personal/MachineLicense.vue:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 | $emit('update:modelValue', event)">
28 |
29 |
30 | {{ $t('license.activated.true') }}
31 | {{ $t('license.activated.false') }}
32 |
33 |
34 | {{ values.status != null ? $t('license.status.' + values.status) : undefined }}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
62 |
63 |
64 |
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 |
66 |
83 |
84 |
--------------------------------------------------------------------------------
/src/components/Upload/ImageCropper.vue:
--------------------------------------------------------------------------------
1 |
68 |
69 |
70 | destroyCropper()">
71 |
72 |
![]()
initCropper()" />
73 |
74 |
75 | handleSubmit()">{{ $t('save') }}
76 |
77 |
78 |
79 |
80 |
88 |
--------------------------------------------------------------------------------
/src/views/user/OrgForm.vue:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 | $emit('update:modelValue', event)"
55 | @finished="finished"
56 | @bean-change="() => fetchOrgList()"
57 | >
58 |
59 |
60 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/src/views/user/UserPermissionForm.vue:
--------------------------------------------------------------------------------
1 |
57 |
58 |
59 | $emit('update:modelValue', event)">
60 |
61 |
62 |
63 |
64 |
65 | {{ `${item.name}(${item.rank})` }}
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | {{ values?.username }}
78 |
79 |
80 | $emit('update:modelValue', false)">{{ $t('cancel') }}
81 | handleSubmit()">{{ $t('save') }}
82 |
83 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/src/views/user/RoleForm.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 | $emit('update:modelValue', event)"
36 | @finished="(event) => $emit('finished')"
37 | >
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
71 |
72 | {{ $t(`role.scope.${n}`) }}
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/src/views/config/DictTypeForm.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 | $emit('update:modelValue', event)"
34 | @finished="() => $emit('finished')"
35 | >
36 |
37 |
38 |
39 |
40 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | {{ $t(`dictType.dataType.${n}`) }}
65 |
66 |
67 |
68 |
69 | {{ $t(`dictType.scope.${n}`) }}
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/src/views/stat/EntryPage.vue:
--------------------------------------------------------------------------------
1 |
65 |
66 |
67 |
68 |
69 |
70 | fetchData(value)">
71 |
72 | {{ $t(`visit.${item}`) }}
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | fetchData(dateRange)"
93 | @current-change="() => fetchData(dateRange)"
94 | >
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/src/views/stat/VisitSource.vue:
--------------------------------------------------------------------------------
1 |
65 |
66 |
67 |
68 |
69 |
70 | fetchData(value)">
71 |
72 | {{ $t(`visit.${item}`) }}
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | fetchData(dateRange)"
93 | @current-change="() => fetchData(dateRange)"
94 | >
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/src/views/stat/VisitedPage.vue:
--------------------------------------------------------------------------------
1 |
65 |
66 |
67 |
68 |
69 |
70 | fetchData(value)">
71 |
72 | {{ $t(`visit.${item}`) }}
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | fetchData(dateRange)"
93 | @current-change="() => fetchData(dateRange)"
94 | >
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/src/views/content/ChannelPermissionForm.vue:
--------------------------------------------------------------------------------
1 |
61 |
62 |
63 | $emit('update:modelValue', event)">
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | {{ item.name }}
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | {{ values?.name }}
86 |
87 |
88 | $emit('update:modelValue', false)">{{ $t('cancel') }}
89 | handleSubmit()">{{ $t('save') }}
90 |
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/src/views/content/ChannelMergeForm.vue:
--------------------------------------------------------------------------------
1 |
81 |
82 |
83 |
84 |
85 |
86 | {{ $t('channel.batchMerge.srcChannel') }}
87 |
88 |
89 |
90 |
91 |
{{ $t('channel.batchMerge.mergeTo') }}
92 |
93 |
94 |
95 |
96 |
97 |
98 | handleSubmit()">{{ $t('submit') }}
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/src/views/stat/VisitTrend.vue:
--------------------------------------------------------------------------------
1 |
92 |
93 |
94 |
95 |
96 |
97 | initTrendChart(value)">
98 |
99 | {{ $t(`visit.${item}`) }}
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/src/views/file/WebFileBatch.vue:
--------------------------------------------------------------------------------
1 |
70 |
71 |
72 | $emit('update:modelValue', e)">
73 |
74 | changeParentId(baseId)">{{ baseId }}
75 | changeParentId('/' + parents.slice(0, index + 1).join('/'))">
76 | {{ item }}
77 |
78 |
79 |
80 |
81 |
82 | changeParentId(row.id)">
83 |
84 | {{ row.name }}
85 |
86 |
87 |
88 |
89 | handleSubmit()">{{ $t('submit') }}
90 |
91 |
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 |
19 | $emit('update:modelValue', event)"
36 | @finished="() => $emit('finished')"
37 | >
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | {{ $t(`task.status.${values.status}`) }}
80 | {{ $t(`task.status.${values.status}`) }}
81 | {{ $t(`task.status.${values.status}`) }}
82 | {{ $t(`task.status.${values.status}`) }}
83 | {{ $t(`task.status.${values.status}`) }}
84 |
85 |
86 |
87 |
88 | {{ values.errorInfo }}
89 |
90 |
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/src/views/user/UserPasswordForm.vue:
--------------------------------------------------------------------------------
1 |
49 |
50 |
51 | $emit('update:modelValue', event)"
55 | @opened="
56 | () => {
57 | focus?.focus();
58 | form.resetFields();
59 | }
60 | "
61 | >
62 |
63 |
64 |
65 |
66 |
79 |
80 |
81 |
97 |
98 |
99 |
100 | {{ $t('submit') }}
101 |
102 |
103 |
104 |
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 |
84 |
85 |
86 |
87 | {{ $t('channel.batchMove.srcChannel') }}
88 |
89 |
90 |
91 |
92 |
{{ $t('channel.batchMove.moveTo') }}
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | handleSubmit()">{{ $t('submit') }}
103 |
104 |
105 |
106 |
--------------------------------------------------------------------------------