Compare commits

...
88 Commits
Author SHA1 Message Date
whyour dcea231249 更新版本 v2.14.5 2022-09-24 21:00:47 +08:00
whyour f236119bd6 修改输入框自动填充样式 2022-09-24 14:40:13 +08:00
whyour cb870fad85 修改检查更新逻辑 2022-09-24 14:20:35 +08:00
whyour 44e2caedfc 修改更新日志清除评率 2022-09-24 13:59:56 +08:00
whyour c0b5192296 修改移动端脚本高亮 2022-09-24 11:27:16 +08:00
whyour ba43882c0b 修复脚本管理删除 2022-09-23 23:16:57 +08:00
whyour dfc706e16d 日志管理支持删除日志文件和目录 2022-09-23 20:09:10 +08:00
whyour 25b03d4345 脚本管理支持删除文件夹 2022-09-23 19:12:51 +08:00
whyour 4e8f36d9a4 修复登录错误提示 2022-09-23 17:44:48 +08:00
whyour 8f4d67ffa7 修改task命令ID获取 2022-09-23 14:37:56 +08:00
whyour ad47277149 修复登录验证 2022-09-23 12:15:15 +08:00
whyour 98c2a37ab6 修复初始化逻辑 2022-09-23 00:17:53 +08:00
whyour f2fea47336 重构前端错误提示 2022-09-22 23:59:23 +08:00
whyour e274d3e2f9 sentry增加版本号 2022-09-22 12:14:42 +08:00
whyour 36f4c3c02c 修改任务启动参数 2022-09-21 21:28:31 +08:00
whyour ae891f8e55 修复docker启动逻辑 2022-09-21 17:42:13 +08:00
whyour c874dc9705 更新readme 2022-09-21 16:03:53 +08:00
whyour 23bfbeb995 修改新建订阅提示 2022-09-21 14:54:45 +08:00
whyour a75df03243 更新版本 2.14.4 2022-09-20 19:59:51 +08:00
whyour e44a2b5995 修改tsconfig 2022-09-20 19:43:49 +08:00
whyour f23e7a9e49 修复自定义通知 2022-09-20 19:13:11 +08:00
whyour e08f0cd308 修复设置自定义通知 2022-09-20 18:00:36 +08:00
whyour 1ebcfd8001 修复自定义通知 2022-09-20 17:34:36 +08:00
whyour 5168b044a3 修复遍历key值 2022-09-20 01:23:33 +08:00
whyour 931638b687 增加自定义hook通知 2022-09-20 01:16:36 +08:00
whyour 4d9bbcfd3f 升级antd 2022-09-19 18:45:56 +08:00
whyour b0e3b26369 修改环境变量默认排序 2022-09-18 20:47:42 +08:00
whyour 1e55f4065d 升级umi4.0,修复新建脚本 2022-09-18 20:40:59 +08:00
whyour 518e8903a3 兼容Modal属性 2022-09-17 22:50:58 +08:00
whyour 6e56a55f6f 修改弹框表单逻辑 2022-09-16 00:28:04 +08:00
whyour 967071ad4e 脚本管理支持新增文件夹 2022-09-16 00:14:10 +08:00
whyour d23fcfaa5a 修复创建应用 2022-09-15 23:45:12 +08:00
whyour a2f2306430 增加批量重新安装依赖 2022-09-11 22:47:43 +08:00
whyour 144b543091 修改依赖管理默认排序 2022-09-11 22:26:33 +08:00
whyour e4ad8f512e 修改订阅默认排序 2022-09-11 22:10:17 +08:00
whyour 591a5006da 修复运行任务shell 2022-09-11 19:53:57 +08:00
whyour 281ed6cfba 修改任务执行日志目录规则 2022-09-11 19:43:37 +08:00
whyour 7354a61184 修改config配置说明 2022-09-10 14:23:16 +08:00
whyour 04037f29e5 更新版本 v2.14.3 2022-09-10 14:12:26 +08:00
whyour 6b124bad89 修复pnpm registry兼容 2022-09-09 00:21:10 +08:00
whyour 54d99a4954 更新版本 v2.14.2 2022-09-07 19:30:19 +08:00
whyour 9947594366 更新issue模板 2022-09-07 17:28:57 +08:00
whyour d7ff934336 增加github issue模版 2022-09-07 16:03:15 +08:00
whyour dc27856441 修复定时任务状态筛选 2022-09-07 11:30:11 +08:00
whyour bd28682769 排除认证失败错误上报 2022-09-06 23:57:21 +08:00
whyour 2060e62da4 更新sentry版本 2022-09-06 21:50:03 +08:00
whyour 70eac1e747 修改pnpm安装全局模块命令 2022-09-06 15:53:21 +08:00
whyour 6f5a5fe066 修改定时任务打开详情交互 2022-09-06 14:49:42 +08:00
景大侠andGitHub 8d24949202 修复#1614 正确解析多级目录的脚本路径 (#1616) 2022-09-06 13:25:12 +08:00
whyour f7b6237355 修复新建任务视图交互 2022-09-06 00:40:06 +08:00
景大侠andGitHub daf6f94c51 修复任务视图bug (#1612)
* 修复了视图无排序时无法再次修改的问题

* 修复在视图管理中编辑、新建视图点击确定后不能关闭页面的问题

* 修复#1611 避免查询条件被覆盖

* 修复视图筛选不能正确处理`不包含`
2022-09-06 00:25:05 +08:00
whyour 687d16e21b 增加npm镜像源配置 2022-09-06 00:21:53 +08:00
whyour 70571a9d34 更新版本 v2.14.1 2022-09-04 18:28:28 +08:00
whyour 4da8d8fe1e 修复node pushdeer通知 2022-09-04 18:25:40 +08:00
AppoipandGitHub 952cdc0e3f 添加群晖synology chat通知推送 (#1610) 2022-09-04 18:10:20 +08:00
whyour 2de189d189 修复视图切换页码初始值 2022-09-03 18:27:52 +08:00
whyour 3edc6e83d7 修复定时任务搜索 2022-09-03 18:23:35 +08:00
whyour 31a5386cfe 更新版本 v2.14.0 2022-09-03 15:26:04 +08:00
whyour 0198ebbbff 修复任务视图tab激活状态 2022-09-03 15:16:35 +08:00
whyour ce3df8704d 修复任务视图tab样式 2022-09-03 15:02:05 +08:00
whyour 1b4ca3a684 修复视图拖拽数据同步 2022-09-03 14:56:32 +08:00
whyour e31c2c0955 完善定时任务视图拖拽 2022-09-03 12:39:47 +08:00
whyour 4e389865b5 定时任务视图tab高亮 2022-09-03 11:58:21 +08:00
whyour e5d8adf955 修复定时任务视图排序 2022-09-03 02:15:32 +08:00
whyour 9a3181bc44 任务视图增加状态筛选 2022-09-03 01:56:47 +08:00
whyour 2f05c95422 完善定时任务视图 2022-09-02 17:52:06 +08:00
whyour bc5a3a2028 修复nginx配置文件 2022-09-02 11:28:08 +08:00
whyour 1fe91508f6 修复baseUrl 2022-09-02 10:54:44 +08:00
whyour b9253c8191 修复定时任务搜索api 2022-09-02 00:35:20 +08:00
whyour 7516acbc41 修复定时任务搜索 2022-08-31 00:02:35 +08:00
whyour 3b3d45da29 修复定时任务排序 2022-08-28 15:45:30 +08:00
whyour 918d68d140 修复布局样式 2022-08-28 00:44:17 +08:00
whyour a3f56be299 修改整体布局,减少无用间隔 2022-08-28 00:38:25 +08:00
whyour f8f63890e5 定时任务增加分组 2022-08-27 14:50:27 +08:00
whyour 9e997410ab 测试dockerfile 2022-08-26 18:42:25 +08:00
whyour 529880642f 修复typescript类型错误 2022-08-26 17:52:37 +08:00
景大侠andGitHub d2590edab3 支持非js任务随机延迟 (#1580)
* 修复非JS任务不能随机延迟的问题
2022-08-26 17:11:16 +08:00
whyour 32eec68278 更新版本 v2.13.9 2022-08-26 00:04:59 +08:00
whyour 05ed8c9f4b 修改定时任务列表布局 2022-08-25 23:58:20 +08:00
whyour bc281ee4d7 修改定时任务pageSize,增加全部展示为一页 2022-08-25 23:12:12 +08:00
whyour 3e88314d0a 修改二次验证交互 2022-08-23 22:38:03 +08:00
whyour 141defd845 修复定时任务默认pageSize 2022-08-20 23:21:31 +08:00
whyour b9e49b181a 修改定时任务排序 2022-08-20 20:27:19 +08:00
whyour 102e447f78 修改定时任务分页数据 2022-08-20 20:05:35 +08:00
whyour f3de8435f1 修改 favicon 2022-08-20 17:31:01 +08:00
whyour 06723407d2 更新版本 v2.13.8 2022-08-11 19:41:59 +08:00
whyour db8dfb32cf 修改生成系统token逻辑 2022-08-11 19:37:14 +08:00
whyour a7117e4442 修改内置token获取方式 2022-08-11 13:13:37 +08:00
93 changed files with 3142 additions and 1145 deletions
+42
View File
@@ -0,0 +1,42 @@
name: "\U0001F41E Bug report"
description: Create a report to help us improve
body:
- type: input
id: version
attributes:
label: Qinglong version
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce
description: |
What do we need to do after opening your repro in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code.
placeholder: Steps to reproduce
validations:
required: true
- type: textarea
id: expected
attributes:
label: What is expected?
validations:
required: true
- type: textarea
id: actually-happening
attributes:
label: What is actually happening?
validations:
required: true
- type: textarea
id: system-info
attributes:
label: System Info
description: Output of `npx envinfo --system --binaries --browsers`
render: shell
placeholder: System, Binaries, Browsers
- type: textarea
id: additional-comments
attributes:
label: Any additional comments?
description: e.g. some background/context of how you ran into this bug.
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Telegram Chat
url: https://t.me/jiao_long
about: Ask questions and discuss with other Qinglong users in real time.
- name: Questions & Discussions
url: https://github.com/whyour/qinglong/discussions/new?category=q-a
about: Use GitHub discussions for message-board style questions and discussions.
@@ -0,0 +1,40 @@
name: "\U0001F680 New feature proposal"
description: Suggest an idea for this project
labels: [":sparkles: feature request"]
body:
- type: markdown
attributes:
value: |
Thanks for your interest in the project and taking the time to fill out this feature report!
- type: textarea
id: feature-description
attributes:
label: Clear and concise description of the problem
description: "Explain your use case, context, and rationale behind this feature request. More importantly, what is the **end user experience** you are trying to build that led to the need for this feature?"
validations:
required: true
- type: textarea
id: suggested-solution
attributes:
label: Suggested solution
description: "In module [xy] we could provide following implementation..."
validations:
required: true
- type: textarea
id: alternative
attributes:
label: Alternative
description: Clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Any other context or screenshots about the feature request here.
- type: checkboxes
id: checkboxes
attributes:
label: Validations
description: Before submitting the issue, please make sure you do the following
options:
- label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
required: true
+31
View File
@@ -0,0 +1,31 @@
## PR Type
What kind of change does this PR introduce?
<!-- Please check the one that applies to this PR using "x". -->
- [ ] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Other... Please describe:
## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
Issue Number: N/A
## What is the new behavior?
## Does this PR introduce a breaking change?
- [ ] Yes
- [ ] No
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
## Other information
+7
View File
@@ -0,0 +1,7 @@
# Comment to be posted to on PRs from first time contributors in your repository
newPRWelcomeComment: |
💖 Thanks for opening this pull request! 💖
Please be patient and we will get back to you as soon as we can.
# Comment to be posted to on pull requests merged by a first time user
firstPRMergeComment: >
Congrats on merging your first pull request! 🎉🎉🎉
+11 -16
View File
@@ -1,20 +1,15 @@
import { defineConfig } from 'umi'; import { defineConfig } from '@umijs/max';
const CompressionPlugin = require('compression-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin');
export default defineConfig({ export default defineConfig({
hash: true, hash: true,
layout: false, antd: {},
outputPath: 'static/dist', outputPath: 'static/dist',
nodeModulesTransform: { fastRefresh: true,
type: 'none', favicons: ['/images/favicon.svg'],
mfsu: {
strategy: 'eager',
}, },
fastRefresh: {},
esbuild: {},
webpack5: {},
dynamicImport: {
loading: '@/components/pageLoading',
},
favicon: '/images/g5.ico',
proxy: { proxy: {
'/api/public': { '/api/public': {
target: 'http://127.0.0.1:5400/', target: 'http://127.0.0.1:5400/',
@@ -26,7 +21,7 @@ export default defineConfig({
ws: true, ws: true,
}, },
}, },
chainWebpack: (config) => { chainWebpack: ((config: any) => {
config.plugin('compression-webpack-plugin').use( config.plugin('compression-webpack-plugin').use(
new CompressionPlugin({ new CompressionPlugin({
algorithm: 'gzip', algorithm: 'gzip',
@@ -35,13 +30,13 @@ export default defineConfig({
minRatio: 0.6, minRatio: 0.6,
}), }),
); );
}, }) as any,
externals: { externals: {
react: 'window.React', react: 'window.React',
'react-dom': 'window.ReactDOM', 'react-dom': 'window.ReactDOM',
}, },
scripts: [ headScripts: [
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js', 'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js', 'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
], ],
}); });
+8 -4
View File
@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://github.com/whyour/qinglong"> <a href="https://github.com/whyour/qinglong">
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png"> <img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
</a> </a>
</p> </p>
@@ -57,7 +57,7 @@ https://podman.io/getting-started/installation
```bash ```bash
podman run -dit \ podman run -dit \
--network bridge \ --network bridge \
-v $PWD/ql:/ql/data \ -v $PWD/ql/data:/ql/data \
-p 5700:5700 \ -p 5700:5700 \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
@@ -96,7 +96,7 @@ systemctl restart docker
```bash ```bash
docker run -dit \ docker run -dit \
-v $PWD/ql:/ql/data \ -v $PWD/ql/data:/ql/data \
-p 5700:5700 \ -p 5700:5700 \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
@@ -124,6 +124,10 @@ docker-compose up -d
docker-compose down docker-compose down
``` ```
3. access
Open your browser and visit http://{ip}:5700
## Use ## Use
1. built-in commands 1. built-in commands
@@ -179,7 +183,7 @@ task <file_path> desi <env_name> <account_number>
- [crontab-ui](https://github.com/alseambusher/crontab-ui) - [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design) - [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/) - [Ant Design Pro](https://pro.ant.design/)
- [Umijs3.0](https://umijs.org) - [Umijs](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader) - [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server) - [admin-server](https://github.com/sunpu007/admin-server)
+8 -4
View File
@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://github.com/whyour/qinglong"> <a href="https://github.com/whyour/qinglong">
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png"> <img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
</a> </a>
</p> </p>
@@ -57,7 +57,7 @@ https://podman.io/getting-started/installation
```bash ```bash
podman run -dit \ podman run -dit \
--network bridge \ --network bridge \
-v $PWD/ql:/ql/data \ -v $PWD/ql/data:/ql/data \
-p 5700:5700 \ -p 5700:5700 \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
@@ -97,7 +97,7 @@ systemctl restart docker
```bash ```bash
docker run -dit \ docker run -dit \
-v $PWD/ql:/ql/data \ -v $PWD/ql/data:/ql/data \
-p 5700:5700 \ -p 5700:5700 \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
@@ -125,6 +125,10 @@ docker-compose up -d
docker-compose down docker-compose down
``` ```
3. 访问
打开你的浏览器,访问 http://{ip}:5700
## 使用 ## 使用
1. 内置命令 1. 内置命令
@@ -179,7 +183,7 @@ task <file_path> desi <env_name> <account_number>
- [crontab-ui](https://github.com/alseambusher/crontab-ui) - [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design) - [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/) - [Ant Design Pro](https://pro.ant.design/)
- [Umijs3.0](https://umijs.org) - [Umijs](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader) - [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server) - [admin-server](https://github.com/sunpu007/admin-server)
+126 -1
View File
@@ -2,6 +2,7 @@ import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import CronService from '../services/cron'; import CronService from '../services/cron';
import CronViewService from '../services/cronView';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
const route = Router(); const route = Router();
@@ -9,11 +10,135 @@ const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/crons', route); app.use('/crons', route);
route.get(
'/views',
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.list();
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.post(
'/views',
celebrate({
body: Joi.object({
name: Joi.string().required(),
sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.create(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views',
celebrate({
body: Joi.object({
name: Joi.string().required(),
id: Joi.number().required(),
sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.update(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.delete(
'/views',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.remove(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/move',
celebrate({
body: Joi.object({
fromIndex: Joi.number().required(),
toIndex: Joi.number().required(),
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.move(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/disable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.disabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/enable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.enabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.get('/', async (req: Request, res: Response, next: NextFunction) => { route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const data = await cronService.crontabs(req.query.searchValue as string); const data = await cronService.crontabs(req.query as any);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
+32 -1
View File
@@ -3,8 +3,9 @@ import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import * as fs from 'fs'; import * as fs from 'fs';
import config from '../config'; import config from '../config';
import { getFileContentByName, readDirs } from '../config/util'; import { emptyDir, getFileContentByName, readDirs } from '../config/util';
import { join } from 'path'; import { join } from 'path';
import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
const blacklist = ['.tmp']; const blacklist = ['.tmp'];
@@ -45,4 +46,34 @@ export default (app: Router) => {
} }
}, },
); );
route.delete(
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
type: Joi.string().optional()
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
let { filename, path, type } = req.body as {
filename: string;
path: string;
type: string;
};
const filePath = join(config.logPath, path, filename);
if (type === 'directory') {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
}; };
+21 -4
View File
@@ -4,6 +4,7 @@ import {
readDirs, readDirs,
getLastModifyFilePath, getLastModifyFilePath,
readDir, readDir,
emptyDir,
} from '../config/util'; } from '../config/util';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
@@ -77,11 +78,13 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let { filename, path, content, originFilename } = req.body as { let { filename, path, content, originFilename, directory } =
req.body as {
filename: string; filename: string;
path: string; path: string;
content: string; content: string;
originFilename: string; originFilename: string;
directory: string;
}; };
if (!path) { if (!path) {
@@ -105,11 +108,19 @@ export default (app: Router) => {
return res.send({ code: 200 }); return res.send({ code: 200 });
} }
if (directory) {
fs.mkdirSync(join(path, directory), { recursive: true });
return res.send({ code: 200 });
}
if (!originFilename) { if (!originFilename) {
originFilename = filename; originFilename = filename;
} }
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`; const originFilePath = join(
const filePath = `${path}${filename.replace(/\//g, '')}`; path,
`${originFilename.replace(/\//g, '')}`,
);
const filePath = join(path, `${filename.replace(/\//g, '')}`);
if (fs.existsSync(originFilePath)) { if (fs.existsSync(originFilePath)) {
fs.copyFileSync( fs.copyFileSync(
originFilePath, originFilePath,
@@ -159,17 +170,23 @@ export default (app: Router) => {
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().allow(''), path: Joi.string().allow(''),
type: Joi.string().optional()
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let { filename, path } = req.body as { let { filename, path, type } = req.body as {
filename: string; filename: string;
path: string; path: string;
type: string;
}; };
const filePath = join(config.scriptPath, path, filename); const filePath = join(config.scriptPath, path, filename);
if (type === 'directory') {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
}
res.send({ code: 200 }); res.send({ code: 200 });
} catch (e) { } catch (e) {
return next(e); return next(e);
+1 -1
View File
@@ -17,7 +17,7 @@ async function startServer() {
const server = app const server = app
.listen(config.port, () => { .listen(config.port, () => {
Logger.debug(`✌️ Back server launched on port ${config.port}`); Logger.debug(`✌️ 后端服务启动成功!`);
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
+1 -1
View File
@@ -11,7 +11,7 @@ if (!process.env.QL_DIR) {
if (qlHomePath.endsWith('/static/')) { if (qlHomePath.endsWith('/static/')) {
qlHomePath = path.join(qlHomePath, '../'); qlHomePath = path.join(qlHomePath, '../');
} }
process.env.QL_DIR = qlHomePath; process.env.QL_DIR = qlHomePath.replace(/\/$/g, '');
} }
const lastVersionFile = `http://qn.whyour.cn/version.ts?v=${Date.now()}`; const lastVersionFile = `http://qn.whyour.cn/version.ts?v=${Date.now()}`;
+74 -1
View File
@@ -3,6 +3,7 @@ import * as path from 'path';
import got from 'got'; import got from 'got';
import iconv from 'iconv-lite'; import iconv from 'iconv-lite';
import { exec } from 'child_process'; import { exec } from 'child_process';
import FormData from 'form-data';
export function getFileContentByName(fileName: string) { export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) { if (fs.existsSync(fileName)) {
@@ -301,7 +302,6 @@ export function readDirs(
title: file, title: file,
key, key,
type: 'directory', type: 'directory',
disabled: true,
parent: relativePath, parent: relativePath,
children: readDirs(subPath, baseDir).sort( children: readDirs(subPath, baseDir).sort(
(a: any, b: any) => (a: any, b: any) =>
@@ -344,6 +344,21 @@ export function readDir(
return result; return result;
} }
export function emptyDir(path: string) {
const files = fs.readdirSync(path);
files.forEach(file => {
const filePath = `${path}/${file}`;
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
});
fs.rmdirSync(path);
}
export function promiseExec(command: string): Promise<string> { export function promiseExec(command: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
exec( exec(
@@ -355,3 +370,61 @@ export function promiseExec(command: string): Promise<string> {
); );
}); });
} }
export function parseHeaders(headers: string) {
if (!headers) return {};
const parsed: any = {};
let key;
let val;
let i;
headers && headers.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key) {
return;
}
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
});
return parsed;
};
export function parseBody(body: string, contentType: 'application/json' | 'multipart/form-data' | 'application/x-www-form-urlencoded') {
if (!body) return '';
const parsed: any = {};
let key;
let val;
let i;
body && body.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key || parsed[key]) {
return;
}
parsed[key] = val;
});
switch (contentType) {
case 'multipart/form-data':
return Object.keys(parsed).reduce((p, c) => {
p.append(c, parsed[c])
return p;
}, new FormData());
case 'application/x-www-form-urlencoded':
return Object.keys(parsed).reduce((p, c) => {
return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`;
});
}
return parsed;
};
+47
View File
@@ -0,0 +1,47 @@
import { sequelize } from '.';
import { DataTypes, Model } from 'sequelize';
interface SortType {
type: 'ASC' | 'DESC';
value: string;
}
interface FilterType {
type: 'or' | 'and';
value: string;
}
export class CrontabView {
name?: string;
id?: number;
position?: number;
isDisabled?: 1 | 0;
filters?: FilterType[];
sorts?: SortType[];
constructor(options: CrontabView) {
this.name = options.name;
this.id = options.id;
this.position = options.position;
this.isDisabled = options.isDisabled || 0;
this.filters = options.filters;
this.sorts = options.sorts;
}
}
interface CronViewInstance
extends Model<CrontabView, CrontabView>,
CrontabView {}
export const CrontabViewModel = sequelize.define<CronViewInstance>(
'CrontabView',
{
name: {
unique: 'name',
type: DataTypes.STRING,
},
position: DataTypes.NUMBER,
isDisabled: DataTypes.NUMBER,
filters: DataTypes.JSON,
sorts: DataTypes.JSON,
},
);
+20 -1
View File
@@ -1,9 +1,12 @@
import { IncomingHttpHeaders } from 'http';
export enum NotificationMode { export enum NotificationMode {
'gotify' = 'gotify', 'gotify' = 'gotify',
'goCqHttpBot' = 'goCqHttpBot', 'goCqHttpBot' = 'goCqHttpBot',
'serverChan' = 'serverChan', 'serverChan' = 'serverChan',
'pushDeer' = 'pushDeer', 'pushDeer' = 'pushDeer',
'bark' = 'bark', 'bark' = 'bark',
'chat' = 'chat',
'telegramBot' = 'telegramBot', 'telegramBot' = 'telegramBot',
'dingtalkBot' = 'dingtalkBot', 'dingtalkBot' = 'dingtalkBot',
'weWorkBot' = 'weWorkBot', 'weWorkBot' = 'weWorkBot',
@@ -11,6 +14,7 @@ export enum NotificationMode {
'iGot' = 'iGot', 'iGot' = 'iGot',
'pushPlus' = 'pushPlus', 'pushPlus' = 'pushPlus',
'email' = 'email', 'email' = 'email',
'webhook' = 'webhook',
} }
abstract class NotificationBaseInfo { abstract class NotificationBaseInfo {
@@ -37,6 +41,11 @@ export class PushDeerNotification extends NotificationBaseInfo {
public pushDeerKey = ''; public pushDeerKey = '';
} }
export class ChatNotification extends NotificationBaseInfo {
public chatUrl = '';
public chatToken = '';
}
export class BarkNotification extends NotificationBaseInfo { export class BarkNotification extends NotificationBaseInfo {
public barkPush = ''; public barkPush = '';
public barkIcon = 'http://qn.whyour.cn/logo.png'; public barkIcon = 'http://qn.whyour.cn/logo.png';
@@ -81,11 +90,20 @@ export class EmailNotification extends NotificationBaseInfo {
public emailPass: string = ''; public emailPass: string = '';
} }
export class WebhookNotification extends NotificationBaseInfo {
public webhookHeaders: string = '';
public webhookBody: string = '';
public webhookUrl: string = '';
public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET';
public webhookContentType: 'application/json' | 'multipart/form-data' | 'application/x-www-form-urlencoded' = 'application/json';
}
export interface NotificationInfo export interface NotificationInfo
extends GoCqHttpBotNotification, extends GoCqHttpBotNotification,
GotifyNotification, GotifyNotification,
ServerChanNotification, ServerChanNotification,
PushDeerNotification, PushDeerNotification,
ChatNotification,
BarkNotification, BarkNotification,
TelegramBotNotification, TelegramBotNotification,
DingtalkBotNotification, DingtalkBotNotification,
@@ -93,4 +111,5 @@ export interface NotificationInfo
WeWorkAppNotification, WeWorkAppNotification,
IGotNotification, IGotNotification,
PushPlusNotification, PushPlusNotification,
EmailNotification {} EmailNotification,
WebhookNotification {}
+2 -1
View File
@@ -6,9 +6,9 @@ import { CrontabModel } from '../data/cron';
import { DependenceModel } from '../data/dependence'; import { DependenceModel } from '../data/dependence';
import { AppModel } from '../data/open'; import { AppModel } from '../data/open';
import { AuthModel } from '../data/auth'; import { AuthModel } from '../data/auth';
import { sequelize } from '../data';
import { fileExist } from '../config/util'; import { fileExist } from '../config/util';
import { SubscriptionModel } from '../data/subscription'; import { SubscriptionModel } from '../data/subscription';
import { CrontabViewModel } from '../data/cronView';
import config from '../config'; import config from '../config';
export default async () => { export default async () => {
@@ -19,6 +19,7 @@ export default async () => {
await AuthModel.sync(); await AuthModel.sync();
await EnvModel.sync(); await EnvModel.sync();
await SubscriptionModel.sync(); await SubscriptionModel.sync();
await CrontabViewModel.sync();
// try { // try {
// const queryInterface = sequelize.getQueryInterface(); // const queryInterface = sequelize.getQueryInterface();
+19 -7
View File
@@ -3,7 +3,7 @@ import bodyParser from 'body-parser';
import cors from 'cors'; import cors from 'cors';
import routes from '../api'; import routes from '../api';
import config from '../config'; import config from '../config';
import jwt from 'express-jwt'; import jwt, { UnauthorizedError } from 'express-jwt';
import fs from 'fs'; import fs from 'fs';
import { getPlatform, getToken } from '../config/util'; import { getPlatform, getToken } from '../config/util';
import Container from 'typedi'; import Container from 'typedi';
@@ -75,22 +75,24 @@ export default ({ app }: { app: Application }) => {
if ( if (
!headerToken && !headerToken &&
originPath && originPath &&
config.apiWhiteList.includes(originPath) && config.apiWhiteList.includes(originPath)
originPath !== '/api/crons/status'
) { ) {
return next(); return next();
} }
const data = fs.readFileSync(config.authConfigFile, 'utf8'); const data = fs.readFileSync(config.authConfigFile, 'utf8');
if (data) { if (data && headerToken) {
const { token = '', tokens = {} } = JSON.parse(data); const { token = '', tokens = {} } = JSON.parse(data);
if (headerToken === token || tokens[req.platform] === headerToken) { if (headerToken === token || tokens[req.platform] === headerToken) {
return next(); return next();
} }
} }
const err: any = new Error('UnauthorizedError'); const errorCode = headerToken ? 'invalid_token' : 'credentials_required';
err.status = 401; const errorMessage = headerToken
? 'jwt malformed'
: 'No authorization token was found';
const err = new UnauthorizedError(errorCode, { message: errorMessage });
next(err); next(err);
}); });
@@ -168,7 +170,17 @@ export default ({ app }: { app: Application }) => {
}, },
); );
app.use(Sentry.Handlers.errorHandler()); app.use(
Sentry.Handlers.errorHandler({
shouldHandleError(error) {
// 排除 SequelizeUniqueConstraintError / NotFound
return (
!['SequelizeUniqueConstraintError'].includes(error.name) ||
!['Not Found'].includes(error.message)
);
},
}),
);
app.use( app.use(
( (
+18 -2
View File
@@ -3,12 +3,29 @@ import _ from 'lodash';
import SystemService from '../services/system'; import SystemService from '../services/system';
import ScheduleService from '../services/schedule'; import ScheduleService from '../services/schedule';
import SubscriptionService from '../services/subscription'; import SubscriptionService from '../services/subscription';
import config from '../config';
import { fileExist } from '../config/util';
export default async () => { export default async () => {
const systemService = Container.get(SystemService); const systemService = Container.get(SystemService);
const scheduleService = Container.get(ScheduleService); const scheduleService = Container.get(ScheduleService);
const subscriptionService = Container.get(SubscriptionService); const subscriptionService = Container.get(SubscriptionService);
// 生成内置token
let tokenCommand = `ts-node-transpile-only ${config.rootPath}/back/token.ts`;
const tokenFile = `${config.rootPath}static/build/token.js`;
if (await fileExist(tokenFile)) {
tokenCommand = `node ${tokenFile}`;
}
const cron = {
id: NaN,
name: '生成token',
command: tokenCommand,
};
scheduleService.createIntervalTask(cron, {
days: 28,
});
// 运行删除日志任务 // 运行删除日志任务
const data = await systemService.getLogRemoveFrequency(); const data = await systemService.getLogRemoveFrequency();
if (data && data.info && data.info.frequency) { if (data && data.info && data.info.frequency) {
@@ -17,9 +34,8 @@ export default async () => {
name: '删除日志', name: '删除日志',
command: `ql rmlog ${data.info.frequency}`, command: `ql rmlog ${data.info.frequency}`,
}; };
await scheduleService.createIntervalTask(cron, { scheduleService.createIntervalTask(cron, {
days: data.info.frequency, days: data.info.frequency,
runImmediately: true,
}); });
} }
+9 -2
View File
@@ -2,16 +2,23 @@ import { Application } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing'; import * as Tracing from '@sentry/tracing';
import Logger from './logger'; import Logger from './logger';
import config from '../config';
import fs from 'fs';
export default ({ expressApp }: { expressApp: Application }) => { export default ({ expressApp }: { expressApp: Application }) => {
const versionRegx = /.*export const version = \'(.*)\'\;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
Sentry.init({ Sentry.init({
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819', dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
integrations: [ integrations: [
new Sentry.Integrations.Http({ tracing: true }), new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app: expressApp }), new Tracing.Integrations.Express({ app: expressApp }),
], ],
tracesSampleRate: 0.1,
tracesSampleRate: 1.0, release: currentVersion,
}); });
expressApp.use(Sentry.Handlers.requestHandler()); expressApp.use(Sentry.Handlers.requestHandler());
+1 -1
View File
@@ -22,7 +22,7 @@ app
await require('./loaders/sentry').default({ expressApp: app }); await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default(); await require('./loaders/db').default();
Logger.debug(`✌️ Back server launched on port ${config.publicPort}`); Logger.debug(`✌️ 公共服务启动成功!`);
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
+4 -7
View File
@@ -18,14 +18,15 @@ const run = async () => {
_schedule && _schedule &&
_schedule.length > 5 && _schedule.length > 5 &&
task.status !== CrontabStatus.disabled && task.status !== CrontabStatus.disabled &&
!task.isDisabled !task.isDisabled &&
task.schedule
) { ) {
schedule.scheduleJob(task.schedule, function () { schedule.scheduleJob(task.schedule, function () {
let command = task.command as string; let command = task.command as string;
if (!command.includes('task ') && !command.includes('ql ')) { if (!command.includes('task ') && !command.includes('ql ')) {
command = `task ${command}`; command = `task ${command}`;
} }
exec(command); exec(`ID=${task.id} ${command}`);
}); });
} }
} }
@@ -43,11 +44,7 @@ app
await require('./loaders/db').default(); await require('./loaders/db').default();
await run(); await run();
Logger.info(` Logger.debug('定时任务服务启动成功!');
################################################
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
################################################
`);
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
+147 -24
View File
@@ -113,9 +113,69 @@ export default class CronService {
} }
} }
public async crontabs(searchText?: string): Promise<Crontab[]> { private formatViewQuery(query: any, viewQuery: any) {
let query = {}; if (viewQuery.filters && viewQuery.filters.length > 0) {
if (!query[Op.and]) {
query[Op.and] = [];
}
for (const col of viewQuery.filters) {
const { property, value, operation } = col;
let q: any = {};
let operate2 = null;
let operate = null;
switch (operation) {
case 'Reg':
operate = Op.like;
operate2 = Op.or;
break;
case 'NotReg':
operate = Op.notLike;
operate2 = Op.and;
break;
case 'In':
q[Op.or] = [
{
[property]: value,
},
property === 'status' && value.includes(2)
? { isDisabled: 1 }
: {},
];
break;
case 'Nin':
q[Op.and] = [
{
[property]: {
[Op.notIn]: value,
},
},
property === 'status' && value.includes(2)
? { isDisabled: { [Op.ne]: 1 } }
: {},
];
break;
default:
break;
}
if (operate && operate2) {
q[property] = {
[operate2]: [
{ [operate]: `%${value}%` },
{ [operate]: `%${encodeURIComponent(value)}%` },
],
};
}
query[Op.and].push(q);
}
}
}
private formatSearchText(query: any, searchText: string | undefined) {
if (searchText) { if (searchText) {
if (!query[Op.and]) {
query[Op.and] = [];
}
let q: any = {};
const textArray = searchText.split(':'); const textArray = searchText.split(':');
switch (textArray[0]) { switch (textArray[0]) {
case 'name': case 'name':
@@ -123,13 +183,11 @@ export default class CronService {
case 'schedule': case 'schedule':
case 'label': case 'label':
const column = textArray[0] === 'label' ? 'labels' : textArray[0]; const column = textArray[0] === 'label' ? 'labels' : textArray[0];
query = { q[column] = {
[column]: {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${textArray[1]}%` }, { [Op.like]: `%${textArray[1]}%` },
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` }, { [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
], ],
},
}; };
break; break;
default: default:
@@ -139,8 +197,7 @@ export default class CronService {
{ [Op.like]: `%${encodeURIComponent(searchText)}%` }, { [Op.like]: `%${encodeURIComponent(searchText)}%` },
], ],
}; };
query = { q[Op.or] = [
[Op.or]: [
{ {
name: reg, name: reg,
}, },
@@ -153,17 +210,83 @@ export default class CronService {
{ {
labels: reg, labels: reg,
}, },
], ];
};
break; break;
} }
query[Op.and].push(q);
}
}
private formatFilterQuery(query: any, filterQuery: any) {
if (filterQuery) {
if (!query[Op.and]) {
query[Op.and] = [];
}
const filterKeys: any = Object.keys(filterQuery);
for (const key of filterKeys) {
let q: any = {};
if (filterKeys[key]) {
q[key] = filterKeys[key];
}
query[Op.and].push(q);
}
}
}
private formatViewSort(order: string[][], viewQuery: any) {
if (viewQuery.sorts && viewQuery.sorts.length > 0) {
for (const { property, type } of viewQuery.sorts) {
order.unshift([property, type]);
}
}
}
public async crontabs(params?: {
searchValue: string;
page: string;
size: string;
sorter: string;
filters: string;
queryString: string;
}): Promise<{ data: Crontab[]; total: number }> {
const searchText = params?.searchValue;
const page = Number(params?.page || '0');
const size = Number(params?.size || '0');
const viewQuery = JSON.parse(params?.queryString || '{}');
const filterQuery = JSON.parse(params?.filters || '{}');
const sorterQuery = JSON.parse(params?.sorter || '{}');
let query: any = {};
let order = [
['isPinned', 'DESC'],
['isDisabled', 'ASC'],
['status', 'ASC'],
['createdAt', 'DESC'],
];
this.formatViewQuery(query, viewQuery);
this.formatSearchText(query, searchText);
this.formatFilterQuery(query, filterQuery);
this.formatViewSort(order, viewQuery);
if (sorterQuery) {
const { field, type } = sorterQuery;
if (field && type) {
order.unshift([field, type]);
}
}
let condition: any = {
where: query,
order: order,
};
if (page && size) {
condition.offset = (page - 1) * size;
condition.limit = size;
} }
try { try {
const result = await CrontabModel.findAll({ const result = await CrontabModel.findAll(condition);
where: query, const count = await CrontabModel.count({ where: query });
order: [['createdAt', 'DESC']], return { data: result, total: count };
});
return result as any;
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -279,11 +402,17 @@ export default class CronService {
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) { if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
cmdStr = `task ${cmdStr}`; cmdStr = `task ${cmdStr}`;
} }
if (cmdStr.endsWith('.js')) { if (
cmdStr.endsWith('.js') ||
cmdStr.endsWith('.py') ||
cmdStr.endsWith('.pyc') ||
cmdStr.endsWith('.sh') ||
cmdStr.endsWith('.ts')
) {
cmdStr = `${cmdStr} now`; cmdStr = `${cmdStr} now`;
} }
const cp = spawn(cmdStr, { shell: '/bin/bash' }); const cp = spawn(`ID=${id} ${cmdStr}`, { shell: '/bin/bash' });
await CrontabModel.update( await CrontabModel.update(
{ status: CrontabStatus.running, pid: cp.pid }, { status: CrontabStatus.running, pid: cp.pid },
@@ -302,16 +431,10 @@ export default class CronService {
cp.on('exit', async (code, signal) => { cp.on('exit', async (code, signal) => {
this.logger.info( this.logger.info(
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`, `任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
); );
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ where: { id } },
);
resolve();
}); });
cp.on('close', async (code) => { cp.on('close', async (code) => {
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
await CrontabModel.update( await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined }, { status: CrontabStatus.idle, pid: undefined },
{ where: { id } }, { where: { id } },
@@ -441,7 +564,7 @@ export default class CronService {
private async set_crontab(needReloadSchedule: boolean = false) { private async set_crontab(needReloadSchedule: boolean = false) {
const tabs = await this.crontabs(); const tabs = await this.crontabs();
var crontab_string = ''; var crontab_string = '';
tabs.forEach((tab) => { tabs.data.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(/ +/); const _schedule = tab.schedule && tab.schedule.split(/ +/);
if (tab.isDisabled === 1 || _schedule!.length !== 5) { if (tab.isDisabled === 1 || _schedule!.length !== 5) {
crontab_string += '# '; crontab_string += '# ';
+92
View File
@@ -0,0 +1,92 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { CrontabView, CrontabViewModel } from '../data/cronView';
import { initEnvPosition } from '../data/env';
@Service()
export default class CronViewService {
constructor(@Inject('logger') private logger: winston.Logger) {}
public async create(payload: CrontabView): Promise<CrontabView> {
let position = initEnvPosition;
const views = await this.list();
if (views && views.length > 0 && views[views.length - 1].position) {
position = views[views.length - 1].position as number;
}
position = position / 2;
const tab = new CrontabView({ ...payload, position });
const doc = await this.insert(tab);
return doc;
}
public async insert(payload: CrontabView): Promise<CrontabView> {
return await CrontabViewModel.create(payload, { returning: true });
}
public async update(payload: CrontabView): Promise<CrontabView> {
const newDoc = await this.updateDb(payload);
return newDoc;
}
public async updateDb(payload: CrontabView): Promise<CrontabView> {
await CrontabViewModel.update(payload, { where: { id: payload.id } });
return await this.getDb({ id: payload.id });
}
public async remove(ids: number[]) {
await CrontabViewModel.destroy({ where: { id: ids } });
}
public async list(): Promise<CrontabView[]> {
try {
const result = await CrontabViewModel.findAll({
where: {},
order: [['position', 'DESC']],
});
return result;
} catch (error) {
throw error;
}
}
public async getDb(query: any): Promise<CrontabView> {
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as CrontabView);
}
public async disabled(ids: number[]) {
await CrontabViewModel.update({ isDisabled: 1 }, { where: { id: ids } });
}
public async enabled(ids: number[]) {
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
}
public async move({
id,
fromIndex,
toIndex,
}: {
fromIndex: number;
toIndex: number;
id: number;
}): Promise<CrontabView> {
let targetPosition: number;
const isUpward = fromIndex > toIndex;
const views = await this.list();
if (toIndex === 0 || toIndex === views.length - 1) {
targetPosition = isUpward
? views[0].position * 2
: views[toIndex].position / 2;
} else {
targetPosition = isUpward
? (views[toIndex].position + views[toIndex - 1].position) / 2
: (views[toIndex].position + views[toIndex + 1].position) / 2;
}
const newDoc = await this.update({
id,
position: targetPosition,
});
return newDoc;
}
}
+5 -2
View File
@@ -125,8 +125,11 @@ export default class DependenceService {
return docs; return docs;
} }
private async find(query: any, sort?: any): Promise<Dependence[]> { private async find(query: any, sort: any = []): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({ where: { ...query } }); const docs = await DependenceModel.findAll({
where: { ...query },
order: [...sort, ['createdAt', 'DESC']],
});
return docs; return docs;
} }
+5 -1
View File
@@ -112,7 +112,11 @@ export default class EnvService {
}; };
} }
try { try {
const result = await this.find(condition, [['position', 'DESC']]); const result = await this.find(condition, [
['status', 'ASC'],
['position', 'DESC'],
['createdAt', 'DESC'],
]);
return result as any; return result as any;
} catch (error) { } catch (error) {
throw error; throw error;
+67 -4
View File
@@ -6,6 +6,7 @@ import got from 'got';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import crypto from 'crypto'; import crypto from 'crypto';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import { parseBody, parseHeaders } from '../config/util';
@Service() @Service()
export default class NotificationService { export default class NotificationService {
@@ -17,6 +18,7 @@ export default class NotificationService {
['goCqHttpBot', this.goCqHttpBot], ['goCqHttpBot', this.goCqHttpBot],
['serverChan', this.serverChan], ['serverChan', this.serverChan],
['pushDeer', this.pushDeer], ['pushDeer', this.pushDeer],
['chat', this.chat],
['bark', this.bark], ['bark', this.bark],
['telegramBot', this.telegramBot], ['telegramBot', this.telegramBot],
['dingtalkBot', this.dingtalkBot], ['dingtalkBot', this.dingtalkBot],
@@ -25,6 +27,7 @@ export default class NotificationService {
['iGot', this.iGot], ['iGot', this.iGot],
['pushPlus', this.pushPlus], ['pushPlus', this.pushPlus],
['email', this.email], ['email', this.email],
['webhook', this.webhook],
]); ]);
private timeout = 10000; private timeout = 10000;
@@ -135,6 +138,20 @@ export default class NotificationService {
); );
} }
private async chat() {
const { chatUrl, chatToken } = this.params;
const url = `${chatUrl}${chatToken}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
body: `payload={"text":"${this.title}\n${this.content}"}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.success;
}
private async bark() { private async bark() {
let { barkPush, barkIcon, barkSound, barkGroup } = this.params; let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) { if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
@@ -165,8 +182,7 @@ export default class NotificationService {
telegramBotUserId, telegramBotUserId,
} = this.params; } = this.params;
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : ''; const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
const url = `https://${ const url = `https://${telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
}/bot${telegramBotToken}/sendMessage`; }/bot${telegramBotToken}/sendMessage`;
let agent; let agent;
if (telegramBotProxyHost && telegramBotProxyPort) { if (telegramBotProxyHost && telegramBotProxyPort) {
@@ -245,7 +261,7 @@ export default class NotificationService {
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] = const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
weWorkAppKey.split(','); weWorkAppKey.split(',');
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`; const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
const { access_token } = await got const tokenRes: any = await got
.post(url, { .post(url, {
timeout: this.timeout, timeout: this.timeout,
retry: 0, retry: 0,
@@ -296,7 +312,7 @@ export default class NotificationService {
const res: any = await got const res: any = await got
.post( .post(
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`, `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
{ {
timeout: this.timeout, timeout: this.timeout,
retry: 0, retry: 0,
@@ -368,4 +384,51 @@ export default class NotificationService {
return !!info.messageId; return !!info.messageId;
} }
private async webhook() {
const { webhookUrl, webhookBody, webhookHeaders, webhookMethod, webhookContentType } =
this.params;
const { formatBody, formatUrl } = this.formatNotifyContent(webhookUrl, webhookBody);
if (!formatUrl && !formatBody) {
return false;
}
const headers = parseHeaders(webhookHeaders);
const body = parseBody(formatBody, webhookContentType);
const bodyParam = this.formatBody(webhookContentType, body);
const options = {
method: webhookMethod,
headers,
timeout: this.timeout,
retry: 0,
allowGetBody: true,
...bodyParam
}
const res = await got(formatUrl, options);
return String(res.statusCode).startsWith('20');
}
private formatBody(contentType: string, body: any): object {
if (!body) return {};
switch (contentType) {
case 'application/json':
return { json: body };
case 'multipart/form-data':
return { form: body };
case 'application/x-www-form-urlencoded':
return { body };
}
return {};
}
private formatNotifyContent(url: string, body: string) {
if (!url.includes('$title') && !body.includes('$title')) {
return {};
}
return {
formatUrl: url.replaceAll('$title', encodeURIComponent(this.title)).replaceAll('$content', encodeURIComponent(this.content)),
formatBody: body.replaceAll('$title', this.title).replaceAll('$content', this.content),
}
}
} }
+6 -15
View File
@@ -145,7 +145,7 @@ export default class OpenService {
} }
} }
public async findSystemToken(): Promise<{ public async generateSystemToken(): Promise<{
value: string; value: string;
expiration: number; expiration: number;
}> { }> {
@@ -158,22 +158,13 @@ export default class OpenService {
scopes: ['crons', 'system'], scopes: ['crons', 'system'],
} as App); } as App);
} }
const nowTime = Math.round(Date.now() / 1000); const { data } = await this.authToken({
let token;
if (
!systemApp.tokens ||
!systemApp.tokens.length ||
nowTime > [...systemApp.tokens].pop()!.expiration
) {
const authToken = await this.authToken({
client_id: systemApp.client_id, client_id: systemApp.client_id,
client_secret: systemApp.client_secret, client_secret: systemApp.client_secret,
}); });
token = authToken.data; return {
token.value = token.token; ...data,
} else { value: data.token,
token = [...systemApp.tokens].pop(); };
}
return token;
} }
} }
+2 -3
View File
@@ -79,13 +79,12 @@ export default class ScheduleService {
cp.on('exit', async (code, signal) => { cp.on('exit', async (code, signal) => {
this.logger.info( this.logger.info(
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`, `任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
); );
}); });
cp.on('close', async (code) => { cp.on('close', async (code) => {
const endTime = dayjs(); const endTime = dayjs();
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
await callbacks.onEnd?.( await callbacks.onEnd?.(
cp, cp,
endTime, endTime,
@@ -169,7 +168,7 @@ export default class ScheduleService {
); );
const job = new LongIntervalJob( const job = new LongIntervalJob(
{ ...schedule, runImmediately: false }, { runImmediately: false, ...schedule },
task, task,
_id, _id,
); );
+4 -1
View File
@@ -62,7 +62,10 @@ export default class SubscriptionService {
try { try {
const result = await SubscriptionModel.findAll({ const result = await SubscriptionModel.findAll({
where: query, where: query,
order: [['createdAt', 'DESC']], order: [
['is_disabled', 'ASC'],
['createdAt', 'DESC'],
],
}); });
return result as any; return result as any;
} catch (error) { } catch (error) {
+1 -2
View File
@@ -70,9 +70,8 @@ export default class SystemService {
}; };
await this.scheduleService.cancelIntervalTask(cron); await this.scheduleService.cancelIntervalTask(cron);
if (frequency > 0) { if (frequency > 0) {
await this.scheduleService.createIntervalTask(cron, { this.scheduleService.createIntervalTask(cron, {
days: frequency, days: frequency,
runImmediately: true,
}); });
} }
return { code: 200, data: { ...cron } }; return { code: 200, data: { ...cron } };
+18 -13
View File
@@ -65,15 +65,13 @@ export default class UserService {
return this.initAuthInfo(); return this.initAuthInfo();
} }
if (retries > 2 && Date.now() - lastlogon < Math.pow(3, retries) * 1000) { const retriesTime = Math.pow(3, retries) * 1000;
if (retries > 2 && timestamp - lastlogon < retriesTime) {
const waitTime = Math.ceil((retriesTime - (timestamp - lastlogon)) / 1000);
return { return {
code: 410, code: 410,
message: `失败次数过多,请${Math.round( message: `失败次数过多,请${waitTime}秒后重试`,
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000, data: waitTime,
)}秒后重试`,
data: Math.round(
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
),
}; };
} }
@@ -85,12 +83,12 @@ export default class UserService {
}); });
return { return {
code: 420, code: 420,
message: '请输入两步验证token', message: '',
}; };
} }
const data = createRandomString(50, 100); const data = createRandomString(50, 100);
const expiration = twoFactorActivated ? 30 : 3; const expiration = twoFactorActivated ? 60 : 20;
let token = jwt.sign({ data }, config.secret as any, { let token = jwt.sign({ data }, config.secret as any, {
expiresIn: 60 * 60 * 24 * expiration, expiresIn: 60 * 60 * 24 * expiration,
algorithm: 'HS384', algorithm: 'HS384',
@@ -111,8 +109,7 @@ export default class UserService {
}); });
await this.notificationService.notify( await this.notificationService.notify(
'登录通知', '登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${ `你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${req.platform
req.platform
}端 登录成功,ip地址 ${ip}`, }端 登录成功,ip地址 ${ip}`,
); );
await this.getLoginLog(); await this.getLoginLog();
@@ -140,8 +137,7 @@ export default class UserService {
}); });
await this.notificationService.notify( await this.notificationService.notify(
'登录通知', '登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${ `你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${req.platform
req.platform
}端 登录失败,ip地址 ${ip}`, }端 登录失败,ip地址 ${ip}`,
); );
await this.getLoginLog(); await this.getLoginLog();
@@ -155,8 +151,17 @@ export default class UserService {
status: LoginStatus.fail, status: LoginStatus.fail,
}, },
}); });
if (retries > 2) {
const waitTime = Math.round(Math.pow(3, retries + 1));
return {
code: 410,
message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime,
};
} else {
return { code: 400, message: config.authError }; return { code: 400, message: config.authError };
} }
}
} else { } else {
return this.initAuthInfo(); return this.initAuthInfo();
} }
+1 -23
View File
@@ -10,41 +10,19 @@ const tokenFile = path.join(config.configPath, 'token.json');
async function getToken() { async function getToken() {
try { try {
const data = await readFile();
const nowTime = Math.round(Date.now() / 1000);
if (data.value && data.expiration > nowTime) {
console.log(data.value);
} else {
Container.set('logger', LoggerInstance); Container.set('logger', LoggerInstance);
const openService = Container.get(OpenService); const openService = Container.get(OpenService);
const appToken = await openService.findSystemToken(); const appToken = await openService.generateSystemToken();
console.log(appToken.value); console.log(appToken.value);
await writeFile({ await writeFile({
value: appToken.value, value: appToken.value,
expiration: appToken.expiration, expiration: appToken.expiration,
}); });
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
} }
async function readFile() {
return new Promise<any>((resolve, reject) => {
fs.readFile(
path.join(config.configPath, 'token.json'),
{ encoding: 'utf8' },
(err, data) => {
if (err) {
resolve({});
} else {
resolve(JSON.parse(data));
}
},
);
});
}
async function writeFile(data: any) { async function writeFile(data: any) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
fs.writeFile(tokenFile, JSON.stringify(data), { encoding: 'utf8' }, () => { fs.writeFile(tokenFile, JSON.stringify(data), { encoding: 'utf8' }, () => {
+4 -4
View File
@@ -1,9 +1,9 @@
FROM node:alpine FROM python:alpine
ARG QL_MAINTAINER="whyour" ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}" LABEL maintainer="${QL_MAINTAINER}"
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=master ARG QL_BRANCH=develop
ENV PNPM_HOME=/root/.local/share/pnpm \ ENV PNPM_HOME=/root/.local/share/pnpm \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
@@ -29,10 +29,10 @@ RUN set -x \
perl \ perl \
openssl \ openssl \
nginx \ nginx \
python3 \ nodejs \
jq \ jq \
openssh \ openssh \
py3-pip \ npm \
&& rm -rf /var/cache/apk/* \ && rm -rf /var/cache/apk/* \
&& apk update \ && apk update \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
+7 -2
View File
@@ -4,12 +4,17 @@ dir_shell=/ql/shell
. $dir_shell/share.sh . $dir_shell/share.sh
link_shell link_shell
export isFirstStartServer=true
echo -e "======================1. 检测配置文件========================\n" echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d make_dir /etc/nginx/conf.d
make_dir /run/nginx make_dir /run/nginx
cp -fv $nginx_conf /etc/nginx/nginx.conf cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
pm2 l &>/dev/null pm2 l &>/dev/null
patch_version &>/dev/null
echo echo
echo -e "======================2. 安装依赖========================\n" echo -e "======================2. 安装依赖========================\n"
@@ -37,13 +42,13 @@ echo -e "定时任务启动成功...\n"
if [[ $AutoStartBot == true ]]; then if [[ $AutoStartBot == true ]]; then
echo -e "======================7. 启动bot========================\n" echo -e "======================7. 启动bot========================\n"
nohup ql bot >>$dir_log/start.log 2>&1 & nohup ql bot >$dir_log/bot.log 2>&1 &
echo -e "bot后台启动中...\n" echo -e "bot后台启动中...\n"
fi fi
if [[ $EnableExtraShell == true ]]; then if [[ $EnableExtraShell == true ]]; then
echo -e "======================8. 执行自定义脚本========================\n" echo -e "======================8. 执行自定义脚本========================\n"
nohup ql extra >>$dir_log/start.log 2>&1 & nohup ql extra >$dir_log/extra.log 2>&1 &
echo -e "自定义脚本后台执行中...\n" echo -e "自定义脚本后台执行中...\n"
fi fi
+10 -10
View File
@@ -1,8 +1,8 @@
upstream api { upstream baseApi {
server 0.0.0.0:5600; server 0.0.0.0:5600;
} }
upstream public { upstream publicApi {
server 0.0.0.0:5400; server 0.0.0.0:5400;
} }
@@ -16,28 +16,28 @@ server {
root /ql/static/dist; root /ql/static/dist;
ssl_session_timeout 5m; ssl_session_timeout 5m;
location /api/public { location QL_BASE_URL/api/public/ {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://public; proxy_pass http://publicApi/api/public/;
} }
location /api { location QL_BASE_URL/api/ {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://api; proxy_pass http://baseApi/api/;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; proxy_set_header Connection $connection_upgrade;
} }
location /open { location QL_BASE_URL/open/ {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://api; proxy_pass http://baseApi/open/;
} }
gzip on; gzip on;
@@ -49,9 +49,9 @@ server {
gzip_buffers 16 8k; gzip_buffers 16 8k;
gzip_http_version 1.0; gzip_http_version 1.0;
location / { location QL_BASE_URL/ {
index index.html index.htm; index index.html index.htm;
try_files $uri $uri/ /index.html; try_files $uri $uri/ QL_BASE_URL/index.html;
} }
location ~ .*\.(html)$ { location ~ .*\.(html)$ {
+35 -18
View File
@@ -2,16 +2,16 @@
"private": true, "private": true,
"scripts": { "scripts": {
"start": "concurrently -n w: npm:start:*", "start": "concurrently -n w: npm:start:*",
"start:front": "umi dev", "start:front": "max dev",
"start:back": "nodemon", "start:back": "nodemon",
"start:public": "ts-node back/public.ts", "start:public": "ts-node back/public.ts",
"build:front": "umi build", "build:front": "max build",
"build:back": "tsc -p tsconfig.back.json", "build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.js", "panel": "npm run build:back && node static/build/app.js",
"schedule": "npm run build:back && node static/build/schedule.js", "schedule": "npm run build:back && node static/build/schedule.js",
"public": "npm run build:back && node static/build/public.js", "public": "npm run build:back && node static/build/public.js",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"postinstall": "umi generate tmp 2>/dev/null || true", "postinstall": "max setup 2>/dev/null || true",
"test": "umi-test", "test": "umi-test",
"test:coverage": "umi-test --coverage" "test:coverage": "umi-test --coverage"
}, },
@@ -28,16 +28,33 @@
}, },
"pnpm": { "pnpm": {
"peerDependencyRules": { "peerDependencyRules": {
"ignoreMissing": [
"react",
"react-dom",
"antd",
"dva",
"postcss",
"webpack",
"eslint",
"stylelint",
"redux",
"@babel/core",
"monaco-editor",
"rc-field-form",
"@types/lodash.merge",
"rollup"
],
"allowedVersions": { "allowedVersions": {
"react": "17", "react": "18",
"react-dom": "17" "react-dom": "18",
"dva-core": "2"
} }
} }
}, },
"dependencies": { "dependencies": {
"@otplib/preset-default": "^12.0.1", "@otplib/preset-default": "^12.0.1",
"@sentry/node": "^7.0.0", "@sentry/node": "^7.12.1",
"@sentry/tracing": "^7.0.0", "@sentry/tracing": "^7.12.1",
"body-parser": "^1.19.2", "body-parser": "^1.19.2",
"celebrate": "^15.0.1", "celebrate": "^15.0.1",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
@@ -48,6 +65,7 @@
"express": "^4.17.3", "express": "^4.17.3",
"express-jwt": "^6.1.1", "express-jwt": "^6.1.1",
"express-urlrewrite": "^1.4.0", "express-urlrewrite": "^1.4.0",
"form-data": "^4.0.0",
"got": "^11.8.2", "got": "^11.8.2",
"hpagent": "^0.1.2", "hpagent": "^0.1.2",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
@@ -72,8 +90,8 @@
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.7.0", "@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.33.1", "@ant-design/pro-layout": "^6.33.1",
"@monaco-editor/react": "^4.3.1", "@monaco-editor/react": "4.2.1",
"@sentry/react": "^7.0.0", "@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
@@ -86,18 +104,16 @@
"@types/node-schedule": "^1.3.2", "@types/node-schedule": "^1.3.2",
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
"@types/qrcode.react": "^1.0.2", "@types/qrcode.react": "^1.0.2",
"@types/react": "^17.0.39", "@types/react": "^18.0.20",
"@types/react-dom": "^17.0.13", "@types/react-dom": "^18.0.6",
"@types/serve-handler": "^6.1.1", "@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33", "@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@umijs/plugin-antd": "^0.15.0", "@umijs/max": "^4.0.21",
"@umijs/plugin-esbuild": "^1.4.1",
"@umijs/ssr-darkreader": "^4.9.45", "@umijs/ssr-darkreader": "^4.9.45",
"@umijs/test": "^3.5.21",
"ansi-to-react": "^6.1.6", "ansi-to-react": "^6.1.6",
"antd": "^4.20.5", "antd": "^4.23.0",
"antd-img-crop": "^4.2.3", "antd-img-crop": "^4.2.3",
"codemirror": "^5.65.2", "codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0", "compression-webpack-plugin": "9.2.0",
@@ -107,18 +123,19 @@
"prettier": "^2.5.1", "prettier": "^2.5.1",
"qiniu": "^7.4.0", "qiniu": "^7.4.0",
"qrcode.react": "^1.0.1", "qrcode.react": "^1.0.1",
"query-string": "^7.1.1",
"rc-tween-one": "^3.0.6", "rc-tween-one": "^3.0.6",
"react": "17.x", "react": "18.x",
"react-codemirror2": "^7.2.1", "react-codemirror2": "^7.2.1",
"react-diff-viewer": "^3.1.1", "react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2", "react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0", "react-dnd-html5-backend": "^14.0.0",
"react-dom": "17.x", "react-dom": "18.x",
"react-split-pane": "^0.1.92", "react-split-pane": "^0.1.92",
"sockjs-client": "^1.6.0", "sockjs-client": "^1.6.0",
"ts-node": "^10.6.0", "ts-node": "^10.6.0",
"tslib": "^2.4.0",
"typescript": "^4.6.2", "typescript": "^4.6.2",
"umi": "^3.5.21",
"umi-request": "^1.4.0", "umi-request": "^1.4.0",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"webpack": "^5.70.0", "webpack": "^5.70.0",
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

+18 -1
View File
@@ -29,6 +29,14 @@ MaxConcurrentNum="5"
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空 ## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
RandomDelay="300" RandomDelay="300"
## 需要随机延迟运行任务的文件后缀,直接写后缀名即可,多个后缀用空格分开,例如: js py ts
## 默认仅给javascript任务加随机延迟,其它任务按定时规则准点运行。全部任务随机延迟赋值为空
RandomDelayFileExtensions="js"
## 每小时的第几分钟准点运行任务,当在这些时间运行任务时将忽略 RandomDelay 配置,不会被随机延迟
## 默认是第0分钟和第30分钟,例如21:00或21:30分的任务将会准点运行。不需要准点运行赋值为空
RandomDelayIgnoredMinutes="0 30"
## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true ## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true
EnableExtraShell="true" EnableExtraShell="true"
@@ -38,9 +46,12 @@ AutoStartBot=""
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot ## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
BotRepoUrl="" BotRepoUrl=""
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空 ## 安装python依赖时指定pip源
PipMirror="https://pypi.doubanio.com/simple/" PipMirror="https://pypi.doubanio.com/simple/"
## 安装node依赖时指定npm源
NpmMirror="https://registry.npmmirror.com"
## 通知环境变量 ## 通知环境变量
## 1. Server酱 ## 1. Server酱
## https://sct.ftqq.com ## https://sct.ftqq.com
@@ -129,4 +140,10 @@ export GOTIFY_PRIORITY=0
## deer_key 填写PushDeer的key ## deer_key 填写PushDeer的key
export DEER_KEY="" export DEER_KEY=""
## 12. Chat
## chat_url 填写synology chat地址,http://IP:PORT/webapi/***token=
## chat_token 填写后面的token
export CHAT_URL=""
export CHAT_TOKEN=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可 ## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+59 -4
View File
@@ -39,6 +39,12 @@ let SCKEY = '';
//(环境变量名 DEER_KEY) //(环境变量名 DEER_KEY)
let PUSHDEER_KEY = ''; let PUSHDEER_KEY = '';
// =======================================Synology Chat通知设置区域===========================================
//此处填你申请的CHAT_URL与CHAT_TOKEN
//(环境变量名 CHAT_URL CHAT_TOKEN)
let CHAT_URL = '';
let CHAT_TOKEN = '';
// =======================================Bark App通知设置区域=========================================== // =======================================Bark App通知设置区域===========================================
//此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX) //此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
let BARK_PUSH = ''; let BARK_PUSH = '';
@@ -133,6 +139,14 @@ if (process.env.DEER_KEY) {
PUSHDEER_KEY = process.env.DEER_KEY; PUSHDEER_KEY = process.env.DEER_KEY;
} }
if (process.env.CHAT_URL) {
CHAT_URL = process.env.CHAT_URL;
}
if (process.env.CHAT_TOKEN) {
CHAT_TOKEN = process.env.CHAT_TOKEN;
}
if (process.env.QQ_SKEY) { if (process.env.QQ_SKEY) {
QQ_SKEY = process.env.QQ_SKEY; QQ_SKEY = process.env.QQ_SKEY;
} }
@@ -239,6 +253,8 @@ async function sendNotify(
iGotNotify(text, desp, params), //iGot iGotNotify(text, desp, params), //iGot
gobotNotify(text, desp), //go-cqhttp gobotNotify(text, desp), //go-cqhttp
gotifyNotify(text, desp), //gotify gotifyNotify(text, desp), //gotify
ChatNotify(text, desp), //synolog chat
PushDeerNotify(text, desp), //PushDeer
]); ]);
} }
@@ -369,7 +385,7 @@ function serverNotify(text, desp, time = 2100) {
}); });
} }
function PushDeerNotify(text, desp, time = 2100) { function PushDeerNotify(text, desp) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (PUSHDEER_KEY) { if (PUSHDEER_KEY) {
// PushDeer 建议对消息内容进行 urlencode // PushDeer 建议对消息内容进行 urlencode
@@ -382,8 +398,9 @@ function PushDeerNotify(text, desp, time = 2100) {
}, },
timeout, timeout,
}; };
setTimeout(() => { $.post(
$.post(options, (err, resp, data) => { options,
(err, resp, data) => {
try { try {
if (err) { if (err) {
console.log('发送通知调用API失败!!\n'); console.log('发送通知调用API失败!!\n');
@@ -407,8 +424,46 @@ function PushDeerNotify(text, desp, time = 2100) {
} finally { } finally {
resolve(data); resolve(data);
} }
},
time,
);
} else {
resolve();
}
});
}
function ChatNotify(text, desp) {
return new Promise((resolve) => {
if (CHAT_URL && CHAT_TOKEN) {
// 对消息内容进行 urlencode
desp = encodeURI(desp);
const options = {
url: `${CHAT_URL}${CHAT_TOKEN}`,
body: `payload={"text":"${text}\n${desp}"}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.success) {
console.log('Chat发送通知消息成功🎉\n');
} else {
console.log(`Chat发送通知消息异常\n${JSON.stringify(data)}`);
}
}
} catch (e) {
$.logErr(e);
} finally {
resolve(data);
}
}); });
}, time);
} else { } else {
resolve(); resolve();
} }
+24
View File
@@ -62,6 +62,9 @@ push_config = {
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY 'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
'CHAT_URL': '', # synology chat url
'CHAT_TOKEN': '', # synology chat token
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌 'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码 'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
@@ -281,6 +284,25 @@ def pushdeer(title: str, content: str) -> None:
print("PushDeer 推送失败!错误信息:", response) print("PushDeer 推送失败!错误信息:", response)
def chat(title: str, content: str) -> None:
"""
通过Chat 推送消息
"""
if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"):
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
return
print("chat 服务启动")
data = 'payload=' + json.dumps({'text': title + '\n' + content})
url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
response = requests.post(url, data=data)
if response.status_code == 200:
print("Chat 推送成功!")
else:
print("Chat 推送失败!错误信息:", response)
def pushplus_bot(title: str, content: str) -> None: def pushplus_bot(title: str, content: str) -> None:
""" """
通过 push+ 推送消息。 通过 push+ 推送消息。
@@ -527,6 +549,8 @@ if push_config.get("PUSH_KEY"):
notify_function.append(serverJ) notify_function.append(serverJ)
if push_config.get("DEER_KEY"): if push_config.get("DEER_KEY"):
notify_function.append(pushdeer) notify_function.append(pushdeer)
if push_config.get("CHAT_URL") and push_config.get("CHAT_TOKEN"):
notify_function.append(chat)
if push_config.get("PUSH_PLUS_TOKEN"): if push_config.get("PUSH_PLUS_TOKEN"):
notify_function.append(pushplus_bot) notify_function.append(pushplus_bot)
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"): if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
+1 -6
View File
@@ -1,12 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
get_token() { get_token() {
local tokenFile="$dir_static/build/token.js" token=$(cat $file_auth_token | jq -r .value)
if [[ ! -f "$tokenFile" ]]; then
token=$(ts-node-transpile-only "$dir_root/back/token.ts")
else
token=$(node "$tokenFile")
fi
} }
add_cron_api() { add_cron_api() {
+4
View File
@@ -31,6 +31,7 @@ copy_dep() {
echo -e "---> 2. 复制nginx配置文件\n" echo -e "---> 2. 复制nginx配置文件\n"
cp -fv $nginx_conf /etc/nginx/nginx.conf cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
echo -e "---> 配置文件复制完成\n" echo -e "---> 配置文件复制完成\n"
} }
@@ -89,6 +90,9 @@ check_pm2() {
main() { main() {
echo -e "=====> 开始检测" echo -e "=====> 开始检测"
npm i -g pnpm npm i -g pnpm
pnpm setup &>/dev/null
source ~/.bashrc
pnpm install -g &>/dev/null
pnpm add -g pm2 pnpm add -g pm2
copy_dep copy_dep
check_ql check_ql
+60 -7
View File
@@ -24,6 +24,7 @@ file_sharecode=$dir_config/sharecode.sh
file_config_user=$dir_config/config.sh file_config_user=$dir_config/config.sh
file_auth_sample=$dir_sample/auth.sample.json file_auth_sample=$dir_sample/auth.sample.json
file_auth_user=$dir_config/auth.json file_auth_user=$dir_config/auth.json
file_auth_token=$dir_config/token.json
file_extra_shell=$dir_config/extra.sh file_extra_shell=$dir_config/extra.sh
file_task_before=$dir_config/task_before.sh file_task_before=$dir_config/task_before.sh
file_task_after=$dir_config/task_after.sh file_task_after=$dir_config/task_after.sh
@@ -66,6 +67,7 @@ import_config() {
[[ -f $file_config_user ]] && . $file_config_user [[ -f $file_config_user ]] && . $file_config_user
[[ -f $file_env ]] && . $file_env [[ -f $file_env ]] && . $file_env
ql_base_url=${QlBaseUrl:-""}
command_timeout_time=${CommandTimeoutTime:-"1h"} command_timeout_time=${CommandTimeoutTime:-"1h"}
proxy_url=${ProxyUrl:-""} proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"} file_extensions=${RepoFileExtensions:-"js py"}
@@ -243,15 +245,13 @@ fix_config() {
} }
npm_install_sub() { npm_install_sub() {
set_proxy
if [ $is_termux -eq 1 ]; then if [ $is_termux -eq 1 ]; then
npm install --production --no-bin-links --registry=https://registry.npmmirror.com || npm install --production --no-bin-links npm install --production --no-bin-links
elif ! type pnpm &>/dev/null; then elif ! type pnpm &>/dev/null; then
npm install --production --registry=https://registry.npmmirror.com || npm install --production npm install --production
else else
pnpm install --loglevel error --production --registry=https://registry.npmmirror.com || pnpm install --production --loglevel error pnpm install --loglevel error --production
fi fi
unset_proxy
} }
npm_install_1() { npm_install_1() {
@@ -349,8 +349,8 @@ reset_branch() {
local branch="$1" local branch="$1"
if [[ $branch ]]; then if [[ $branch ]]; then
part_cmd="origin/${branch}" part_cmd="origin/${branch}"
git checkout -B "$branch" git checkout -B "$branch" &>/dev/null
git branch --set-upstream-to=$part_cmd $branch git branch --set-upstream-to=$part_cmd $branch &>/dev/null
fi fi
git reset --hard $part_cmd &>/dev/null git reset --hard $part_cmd &>/dev/null
} }
@@ -423,6 +423,59 @@ format_timestamp() {
fi fi
} }
patch_version() {
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror
fi
# 兼容pnpm@7
pnpm setup &>/dev/null
source ~/.bashrc
pnpm install -g &>/dev/null
if [[ -f "$dir_root/db/cookie.db" ]]; then
echo -e "检测到旧的db文件,拷贝为新db...\n"
mv $dir_root/db/cookie.db $dir_root/db/env.db
rm -rf $dir_root/db/cookie.db
echo
fi
if ! type ts-node &>/dev/null; then
pnpm add -g ts-node typescript tslib
fi
git config --global pull.rebase false
cp -f $dir_root/.env.example $dir_root/.env
if [[ -d "$dir_root/db" ]]; then
echo -e "检测到旧的db目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
if [[ -d "$dir_root/scripts" ]]; then
echo -e "检测到旧的scripts目录,拷贝到data目录...\n"
cp -rf $dir_root/scripts $dir_root/data
echo
fi
if [[ -d "$dir_root/log" ]]; then
echo -e "检测到旧的log目录,拷贝到data目录...\n"
cp -rf $dir_root/log $dir_root/data
echo
fi
if [[ -d "$dir_root/config" ]]; then
echo -e "检测到旧的config目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
}
init_env init_env
detect_termux detect_termux
detect_macos detect_macos
+51 -85
View File
@@ -24,13 +24,30 @@ define_program() {
random_delay() { random_delay() {
local random_delay_max=$RandomDelay local random_delay_max=$RandomDelay
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
local current_min=$(date "+%-M") local file_param=$1
if [[ $current_min -ne 0 ]] && [[ $current_min -ne 30 ]]; then local file_extensions=${RandomDelayFileExtensions-"js"}
delay_second=$(($(gen_random_num $random_delay_max) + 1)) local ignored_minutes=${RandomDelayIgnoredMinutes-"0 30"}
if [[ -n $file_extensions ]]; then
if ! echo "$file_param" | grep -qE "\.${file_extensions// /$|\\.}$"; then
# echo -e "\n当前文件需要准点运行, 放弃随机延迟\n"
return
fi
fi
local current_min
current_min=$(date "+%-M")
for minute in $ignored_minutes; do
if [[ $current_min -eq $minute ]]; then
# echo -e "\n当前时间需要准点运行, 放弃随机延迟\n"
return
fi
done
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\n" echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
sleep $delay_second sleep $delay_second
fi fi
fi
} }
## scripts目录下所有可运行脚本数组 ## scripts目录下所有可运行脚本数组
@@ -74,17 +91,14 @@ run_nohup() {
nohup node $file_name &>$log_path & nohup node $file_name &>$log_path &
} }
## 正常运行单个脚本,$1:传入参数 handle_log_path() {
run_normal() {
local file_param=$1
define_program "$file_param" define_program "$file_param"
if [[ $file_param == *.js ]]; then
if [[ $# -eq 1 ]]; then
random_delay
fi
fi
local time=$(date "+$time_format") local suffix=""
if [[ ! -z $ID ]]; then
suffix="_${ID}"
fi
time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time") log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}" log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then if [[ $file_param =~ "/" ]]; then
@@ -97,11 +111,21 @@ run_normal() {
log_dir_tmp_path="${log_dir_tmp_path%/*}" log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}" log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}" [[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}" log_dir="${log_dir_tmp%.*}${suffix}"
log_path="$log_dir/$log_time.log" log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1" cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd="" [[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir" make_dir "$dir_log/$log_dir"
}
## 正常运行单个脚本,$1:传入参数
run_normal() {
local file_param=$1
if [[ $# -eq 1 ]]; then
random_delay "$file_param"
fi
handle_log_path
local begin_time=$(format_time "$time_format" "$time") local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time") local begin_timestamp=$(format_timestamp "$time_format" "$time")
@@ -109,8 +133,7 @@ run_normal() {
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd [[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}') [[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd eval . $file_task_before "$@" $cmd
cd $dir_scripts cd $dir_scripts
@@ -126,7 +149,7 @@ run_normal() {
local end_time=$(date '+%Y-%m-%d %H:%M:%S') local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s") local end_timestamp=$(date "+%s")
local diff_time=$(expr $end_timestamp - $begin_timestamp) local diff_time=$(expr $end_timestamp - $begin_timestamp)
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" [[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
} }
@@ -155,25 +178,7 @@ run_concurrent() {
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g') local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr} [[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
define_program "$file_param" handle_log_path
local time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(format_time "$time_format" "$time") local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time") local begin_timestamp=$(format_timestamp "$time_format" "$time")
@@ -181,8 +186,7 @@ run_concurrent() {
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd [[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}') [[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd eval . $file_task_before "$@" $cmd
local envs=$(eval echo "\$${env_param}") local envs=$(eval echo "\$${env_param}")
@@ -212,7 +216,7 @@ run_concurrent() {
local end_time=$(date '+%Y-%m-%d %H:%M:%S') local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s") local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp )) local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" [[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
} }
@@ -225,25 +229,7 @@ run_designated() {
exit 1 exit 1
fi fi
define_program "$file_param" handle_log_path
local time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(format_time "$time_format" "$time") local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time") local begin_timestamp=$(format_timestamp "$time_format" "$time")
@@ -266,8 +252,7 @@ run_designated() {
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd [[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}') [[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd eval . $file_task_before "$@" $cmd
cd $dir_scripts cd $dir_scripts
@@ -282,41 +267,22 @@ run_designated() {
local end_time=$(date '+%Y-%m-%d %H:%M:%S') local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s") local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp )) local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" [[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
} }
## 运行其他命令 ## 运行其他命令
run_else() { run_else() {
local file_param="$1" local file_param="$1"
define_program "$file_param"
local time=$(date "+$time_format") handle_log_path
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(format_time "$time_format" "$time") local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time") local begin_timestamp=$(format_timestamp "$time_format" "$time")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd [[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
local id=$(cat $list_crontab_user | grep -E "$cmd_task $@" | perl -pe "s|.*ID=(.*) $cmd_task $@\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd eval . $file_task_before "$@" $cmd
cd $dir_scripts cd $dir_scripts
@@ -333,7 +299,7 @@ run_else() {
local end_time=$(date '+%Y-%m-%d %H:%M:%S') local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s") local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp )) local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" [[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
} }
@@ -356,7 +322,7 @@ main() {
fi fi
time_format="%Y-%m-%d %H:%M:%S" time_format="%Y-%m-%d %H:%M:%S"
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
case $# in case $# in
1) 1)
run_normal "$1" run_normal "$1"
+13 -53
View File
@@ -244,9 +244,10 @@ usage() {
## 更新qinglong ## 更新qinglong
update_qinglong() { update_qinglong() {
patch_version patch_version &>/dev/null
export isFirstStartServer=false
local no_restart="$1"
local all_branch=$(git branch -a) local all_branch=$(git branch -a)
local primary_branch="master" local primary_branch="master"
if [[ "${all_branch}" =~ "${current_branch}" ]]; then if [[ "${all_branch}" =~ "${current_branch}" ]]; then
@@ -264,10 +265,16 @@ update_qinglong() {
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json) [[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root [[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
else
echo -e "\n更新青龙源文件失败,请检查原因...\n"
fi
update_qinglong_static "$1" "$primary_branch"
else
echo -e "\n更新青龙源文件失败,请检查网络...\n"
fi
}
update_qinglong_static() {
local no_restart="$1"
local primary_branch="$2"
local url="https://github.com/whyour/qinglong-static.git" local url="https://github.com/whyour/qinglong-static.git"
if [[ -d ${ql_static_repo}/.git ]]; then if [[ -d ${ql_static_repo}/.git ]]; then
reset_romote_url ${ql_static_repo} ${url} ${primary_branch} reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
@@ -289,55 +296,8 @@ update_qinglong() {
reload_pm2 reload_pm2
fi fi
else else
echo -e "\n更新青龙静态资源失败,请检查原因...\n" echo -e "\n更新青龙静态资源失败,请检查网络...\n"
fi fi
}
patch_version() {
if [[ -f "$dir_root/db/cookie.db" ]]; then
echo -e "检测到旧的db文件,拷贝为新db...\n"
mv $dir_root/db/cookie.db $dir_root/db/env.db
rm -rf $dir_root/db/cookie.db
echo
fi
if ! type ts-node &>/dev/null; then
pnpm i -g ts-node typescript tslib
fi
# 兼容pnpm@7
pnpm setup
source ~/.bashrc
git config --global pull.rebase false
cp -f $dir_root/.env.example $dir_root/.env
if [[ -d "$dir_root/db" ]]; then
echo -e "检测到旧的db目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
if [[ -d "$dir_root/scripts" ]]; then
echo -e "检测到旧的scripts目录,拷贝到data目录...\n"
cp -rf $dir_root/scripts $dir_root/data
echo
fi
if [[ -d "$dir_root/log" ]]; then
echo -e "检测到旧的log目录,拷贝到data目录...\n"
cp -rf $dir_root/log $dir_root/data
echo
fi
if [[ -d "$dir_root/config" ]]; then
echo -e "检测到旧的config目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
} }
## 对比脚本 ## 对比脚本
+1
View File
@@ -50,6 +50,7 @@ const EditableTagGroup = ({
{tag} {tag}
</Tag> </Tag>
); );
return ( return (
<span key={tag} style={{ display: 'inline-block', marginBottom: 8 }}> <span key={tag} style={{ display: 'inline-block', marginBottom: 8 }}>
{tagElem} {tagElem}
+6 -6
View File
@@ -68,18 +68,18 @@ export default {
icon: <IconFont type="ql-icon-dependence" />, icon: <IconFont type="ql-icon-dependence" />,
component: '@/pages/dependence/index', component: '@/pages/dependence/index',
}, },
{
path: '/log',
name: '日志管理',
icon: <IconFont type="ql-icon-log" />,
component: '@/pages/log/index',
},
{ {
path: '/diff', path: '/diff',
name: '对比工具', name: '对比工具',
icon: <IconFont type="ql-icon-diff" />, icon: <IconFont type="ql-icon-diff" />,
component: '@/pages/diff/index', component: '@/pages/diff/index',
}, },
{
path: '/log',
name: '任务日志',
icon: <IconFont type="ql-icon-log" />,
component: '@/pages/log/index',
},
{ {
path: '/setting', path: '/setting',
name: '系统设置', name: '系统设置',
+24 -13
View File
@@ -56,7 +56,13 @@
overflow: auto; overflow: auto;
height: 100%; height: 100%;
background-color: @component-background; background-color: @component-background;
padding: 16px; padding: 12px;
}
}
.ql-container-wrapper-has-tab {
.ant-pro-grid-content.wide .ant-pro-page-container-children-content {
padding-top: 0;
} }
} }
@@ -136,7 +142,7 @@
flex: 1; flex: 1;
.ant-pro-grid-content-children { .ant-pro-grid-content-children {
height: calc(100% - 36px); height: 100%;
> div, > div,
.log-container, .log-container,
@@ -155,7 +161,7 @@
&.env-wrapper, &.env-wrapper,
&.config-wrapper { &.config-wrapper {
.CodeMirror { .CodeMirror {
width: calc(100vw - 80px); width: calc(100vw - 24px);
} }
} }
} }
@@ -237,7 +243,6 @@
font-size: 13px; font-size: 13px;
} }
[data-dark='true'] {
/* Change autocomplete styles in WebKit */ /* Change autocomplete styles in WebKit */
input:-webkit-autofill, input:-webkit-autofill,
input:-webkit-autofill:hover, input:-webkit-autofill:hover,
@@ -248,11 +253,10 @@
select:-webkit-autofill, select:-webkit-autofill,
select:-webkit-autofill:hover, select:-webkit-autofill:hover,
select:-webkit-autofill:focus { select:-webkit-autofill:focus {
border: 1px solid @border-color-base; border: none;
box-shadow: none; box-shadow: none;
transition: background-color 5000s ease-in-out 0s; transition: background-color 5000s ease-in-out 0s;
-webkit-text-fill-color: @text-color; -webkit-text-fill-color: @text-color;
caret-color: #e8e6f3 !important;
} }
::placeholder { ::placeholder {
@@ -262,18 +266,25 @@
.ant-select-selection-placeholder { .ant-select-selection-placeholder {
opacity: 0.5 !important; opacity: 0.5 !important;
} }
.ant-pro-basicLayout-content {
margin: 12px;
.ant-pro-page-container {
margin: -12px;
}
.ant-pro-page-container-warp .ant-page-header {
border-bottom: 1px solid #eee;
} }
.ant-pro-page-container-children-content { .ant-pro-page-container-children-content {
margin: 18px 18px 18px; margin: 0;
}
} }
.ant-pro-basicLayout-content { .ant-pro-global-header {
margin: 18px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03);
.ant-pro-page-container {
margin: -18px -18px -18px;
}
} }
.ant-menu-item.ant-pro-sider-collapsed-button { .ant-menu-item.ant-pro-sider-collapsed-button {
+48 -34
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef } from 'react';
import ProLayout, { PageLoading } from '@ant-design/pro-layout'; import ProLayout, { PageLoading } from '@ant-design/pro-layout';
import * as DarkReader from '@umijs/ssr-darkreader'; import * as DarkReader from '@umijs/ssr-darkreader';
import defaultProps from './defaultProps'; import defaultProps from './defaultProps';
import { Link, history } from 'umi'; import { Link, history, Outlet, useLocation } from '@umijs/max';
import { import {
LogoutOutlined, LogoutOutlined,
MenuFoldOutlined, MenuFoldOutlined,
@@ -20,8 +20,22 @@ import { message, Badge, Modal, Avatar, Dropdown, Menu, Image } from 'antd';
import SockJS from 'sockjs-client'; import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { init } from '../utils/init'; import { init } from '../utils/init';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/python/python';
import 'codemirror/mode/shell/shell';
export default function (props: any) { export interface SharedContext {
headerStyle: React.CSSProperties;
isPhone: boolean;
theme: 'vs' | 'vs-dark';
user: any;
reloadUser: (needLoading?: boolean) => void;
reloadTheme: () => void;
socketMessage: any;
}
export default function () {
const location = useLocation();
const ctx = useCtx(); const ctx = useCtx();
const { theme, reloadTheme } = useTheme(); const { theme, reloadTheme } = useTheme();
const [user, setUser] = useState<any>({}); const [user, setUser] = useState<any>({});
@@ -30,6 +44,7 @@ export default function (props: any) {
const ws = useRef<any>(null); const ws = useRef<any>(null);
const [socketMessage, setSocketMessage] = useState<any>(); const [socketMessage, setSocketMessage] = useState<any>();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [initLoading, setInitLoading] = useState<boolean>(true);
const { const {
enable: enableDarkMode, enable: enableDarkMode,
disable: disableDarkMode, disable: disableDarkMode,
@@ -53,17 +68,15 @@ export default function (props: any) {
setSystemInfo(data); setSystemInfo(data);
if (!data.isInitialized) { if (!data.isInitialized) {
history.push('/initialization'); history.push('/initialization');
setLoading(false);
} else { } else {
getUser(); getUser();
} }
} else {
message.error(data);
} }
}) })
.catch((error) => { .catch((error) => {
console.log(error); console.log(error);
}); })
.finally(() => setInitLoading(false));
}; };
const getUser = (needLoading = true) => { const getUser = (needLoading = true) => {
@@ -73,11 +86,9 @@ export default function (props: any) {
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200 && data.username) { if (code === 200 && data.username) {
setUser(data); setUser(data);
if (props.location.pathname === '/') { if (location.pathname === '/') {
history.push('/crontab'); history.push('/crontab');
} }
} else {
message.error(data);
} }
needLoading && setLoading(false); needLoading && setLoading(false);
}) })
@@ -94,7 +105,7 @@ export default function (props: any) {
if (systemInfo && systemInfo.isInitialized && !user) { if (systemInfo && systemInfo.isInitialized && !user) {
getUser(); getUser();
} }
}, [props.location.pathname]); }, [location.pathname]);
useEffect(() => { useEffect(() => {
if (!systemInfo) { if (!systemInfo) {
@@ -136,9 +147,11 @@ export default function (props: any) {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!user) return; if (!user || !user.username) return;
ws.current = new SockJS( ws.current = new SockJS(
`${location.origin}/api/ws?token=${localStorage.getItem(config.authKey)}`, `${window.location.origin}/api/ws?token=${localStorage.getItem(
config.authKey,
)}`,
); );
ws.current.onmessage = (e: any) => { ws.current.onmessage = (e: any) => {
@@ -146,9 +159,9 @@ export default function (props: any) {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
if (data.type === 'ping') { if (data.type === 'ping') {
if (data && data.message === 'hanhh') { if (data && data.message === 'hanhh') {
console.log('websocket连接成功', e); console.log('WS connection succeeded !!!');
} else { } else {
console.log('websocket连接失败', e); console.log('WS connection Failed !!!', e);
} }
} }
setSocketMessage(data); setSocketMessage(data);
@@ -183,30 +196,31 @@ export default function (props: any) {
}; };
}, []); }, []);
if ( if (initLoading) {
['/login', '/initialization', '/error'].includes(props.location.pathname) return <PageLoading />;
) { }
if (['/login', '/initialization', '/error'].includes(location.pathname)) {
document.title = `${ document.title = `${
(config.documentTitleMap as any)[props.location.pathname] (config.documentTitleMap as any)[location.pathname]
} - 控制面板`; } - 控制面板`;
if ( if (systemInfo?.isInitialized && location.pathname === '/initialization') {
systemInfo?.isInitialized &&
props.location.pathname === '/initialization'
) {
history.push('/crontab'); history.push('/crontab');
} }
if (systemInfo || props.location.pathname === '/error') { if (systemInfo || location.pathname === '/error') {
return React.Children.map(props.children, (child) => { return (
return React.cloneElement(child, { <Outlet
context={{
...ctx, ...ctx,
theme, theme,
user, user,
reloadUser, reloadUser,
reloadTheme, reloadTheme,
ws: ws.current, ws: ws.current,
}); }}
}); />
);
} }
} }
@@ -227,7 +241,7 @@ export default function (props: any) {
<PageLoading /> <PageLoading />
) : ( ) : (
<ProLayout <ProLayout
selectedKeys={[props.location.pathname]} selectedKeys={[location.pathname]}
loading={loading} loading={loading}
ErrorBoundary={Sentry.ErrorBoundary} ErrorBoundary={Sentry.ErrorBoundary}
logo={<Image preview={false} src="http://qn.whyour.cn/logo.png" />} logo={<Image preview={false} src="http://qn.whyour.cn/logo.png" />}
@@ -282,7 +296,7 @@ export default function (props: any) {
shape="square" shape="square"
size="small" size="small"
icon={<UserOutlined />} icon={<UserOutlined />}
src={`/api/static/${user.avatar}`} src={user.avatar ? `/api/static/${user.avatar}` : ''}
/> />
<span style={{ marginLeft: 5 }}>{user.username}</span> <span style={{ marginLeft: 5 }}>{user.username}</span>
</span> </span>
@@ -304,7 +318,7 @@ export default function (props: any) {
shape="square" shape="square"
size="small" size="small"
icon={<UserOutlined />} icon={<UserOutlined />}
src={`/api/static/${user.avatar}`} src={user.avatar ? `/api/static/${user.avatar}` : ''}
/> />
<span style={{ marginLeft: 5 }}>{user.username}</span> <span style={{ marginLeft: 5 }}>{user.username}</span>
</span> </span>
@@ -320,16 +334,16 @@ export default function (props: any) {
)} )}
{...defaultProps} {...defaultProps}
> >
{React.Children.map(props.children, (child) => { <Outlet
return React.cloneElement(child, { context={{
...ctx, ...ctx,
theme, theme,
user, user,
reloadUser, reloadUser,
reloadTheme, reloadTheme,
socketMessage, socketMessage,
}); }}
})} />
</ProLayout> </ProLayout>
); );
} }
+16 -7
View File
@@ -11,8 +11,11 @@ import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
import { Controlled as CodeMirror } from 'react-codemirror2'; import { Controlled as CodeMirror } from 'react-codemirror2';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const Config = ({ headerStyle, isPhone, theme }: any) => { const Config = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [title, setTitle] = useState('config.sh'); const [title, setTitle] = useState('config.sh');
@@ -22,8 +25,10 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
const [confirmLoading, setConfirmLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false);
const getConfig = (name: string) => { const getConfig = (name: string) => {
request.get(`${config.apiPrefix}configs/${name}`).then((data: any) => { request.get(`${config.apiPrefix}configs/${name}`).then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}); });
}; };
@@ -31,8 +36,10 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}configs/files`) .get(`${config.apiPrefix}configs/files`)
.then((data: any) => { .then(({ code, data }) => {
setData(data.data); if (code === 200) {
setData(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -47,8 +54,10 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
.post(`${config.apiPrefix}configs/save`, { .post(`${config.apiPrefix}configs/save`, {
data: { content, name: select }, data: { content, name: select },
}) })
.then((data: any) => { .then(({ code, data }) => {
message.success(data.message); if (code === 200) {
message.success('保存成功');
}
setConfirmLoading(false); setConfirmLoading(false);
}); });
}; };
+30 -34
View File
@@ -103,12 +103,10 @@ const CronDetailModal = ({
fontSize: 12, fontSize: 12,
lineNumbersMinChars: 3, lineNumbersMinChars: 3,
fontFamily: 'Source Code Pro', fontFamily: 'Source Code Pro',
folding: false,
glyphMargin: false, glyphMargin: false,
wordWrap: 'on', wordWrap: 'on',
}} }}
onMount={(editor, monaco) => { onMount={(editor, monaco) => {
console.log(monaco);
editorRef.current = editor; editorRef.current = editor;
}} }}
/> />
@@ -124,9 +122,11 @@ const CronDetailModal = ({
.get( .get(
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`, `${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
) )
.then((data) => { .then(({ code, data }) => {
setLog(data.data); if (code === 200) {
setLog(data);
setIsLogModalVisible(true); setIsLogModalVisible(true);
}
}); });
}; };
@@ -138,9 +138,9 @@ const CronDetailModal = ({
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}crons/${cron.id}/logs`) .get(`${config.apiPrefix}crons/${cron.id}/logs`)
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setLogs(data.data); setLogs(data);
} }
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -154,16 +154,22 @@ const CronDetailModal = ({
cmd[1] = cmd[1].replace('/ql/data/scripts/', ''); cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
} }
let [p, s] = cmd[1].split('/'); let p: string, s: string;
if (!s) { let index = cmd[1].lastIndexOf('/');
s = p; if (index >= 0) {
s = cmd[1].slice(index + 1);
p = cmd[1].slice(0, index);
} else {
s = cmd[1];
p = ''; p = '';
} }
setScriptInfo({ parent: p, filename: s }); setScriptInfo({ parent: p, filename: s });
request request
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`) .get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
.then((data) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}); });
} else { } else {
setValidTabs([validTabs[0]]); setValidTabs([validTabs[0]]);
@@ -195,12 +201,10 @@ const CronDetailModal = ({
content, content,
}, },
}) })
.then((_data: any) => { .then(({ code, data }) => {
if (_data.code === 200) { if (code === 200) {
setValue(content); setValue(content);
message.success(`保存成功`); message.success(`保存成功`);
} else {
message.error(_data);
} }
resolve(null); resolve(null);
}) })
@@ -228,14 +232,12 @@ const CronDetailModal = ({
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/run`, { data: [currentCron.id] }) .put(`${config.apiPrefix}crons/run`, { data: [currentCron.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.running }); setCurrentCron({ ...currentCron, status: CrontabStatus.running });
setTimeout(() => { setTimeout(() => {
getLogs(); getLogs();
}, 1000); }, 1000);
} else {
message.error(data);
} }
}); });
}, },
@@ -260,11 +262,9 @@ const CronDetailModal = ({
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/stop`, { data: [currentCron.id] }) .put(`${config.apiPrefix}crons/stop`, { data: [currentCron.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.idle }); setCurrentCron({ ...currentCron, status: CrontabStatus.idle });
} else {
message.error(data);
} }
}); });
}, },
@@ -297,14 +297,12 @@ const CronDetailModal = ({
data: [currentCron.id], data: [currentCron.id],
}, },
) )
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setCurrentCron({ setCurrentCron({
...currentCron, ...currentCron,
isDisabled: currentCron.isDisabled === 1 ? 0 : 1, isDisabled: currentCron.isDisabled === 1 ? 0 : 1,
}); });
} else {
message.error(data);
} }
}); });
}, },
@@ -337,14 +335,12 @@ const CronDetailModal = ({
data: [currentCron.id], data: [currentCron.id],
}, },
) )
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setCurrentCron({ setCurrentCron({
...currentCron, ...currentCron,
isPinned: currentCron.isPinned === 1 ? 0 : 1, isPinned: currentCron.isPinned === 1 ? 0 : 1,
}); });
} else {
message.error(data);
} }
}); });
}, },
@@ -379,7 +375,7 @@ const CronDetailModal = ({
{currentCron.labels?.length > 0 && {currentCron.labels?.length > 0 &&
currentCron.labels[0] !== '' && currentCron.labels[0] !== '' &&
currentCron.labels?.map((label: string, i: number) => ( currentCron.labels?.map((label: string, i: number) => (
<Tag color="blue" style={{ marginRight: 5 }}> <Tag key={label} color="blue" style={{ marginRight: 5 }}>
{label} {label}
</Tag> </Tag>
))} ))}
@@ -442,7 +438,7 @@ const CronDetailModal = ({
</div> </div>
} }
centered centered
visible={visible} open={visible}
forceRender forceRender
footer={false} footer={false}
onCancel={() => handleCancel()} onCancel={() => handleCancel()}
+81 -6
View File
@@ -2,12 +2,6 @@
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
.cron {
&:hover {
cursor: pointer;
}
}
.crontab-detail { .crontab-detail {
.card-wrapper { .card-wrapper {
height: 80vh; height: 80vh;
@@ -101,3 +95,84 @@
background: #fafafa; background: #fafafa;
} }
} }
.crontab-view {
.ant-tabs-nav-wrap {
flex: unset !important;
}
.ant-tabs-nav-operations {
position: absolute;
visibility: hidden;
pointer-events: none;
}
.view-more {
margin-left: 32px;
padding: 8px 0;
cursor: pointer;
.ant-tabs-ink-bar {
width: 0;
}
&:hover,
&:focus-visible {
color: #1890ff;
.ant-tabs-ink-bar {
width: 50px;
}
}
&.active {
color: #1890ff;
.ant-tabs-ink-bar {
width: 50px;
}
}
}
&.more-active {
.ant-tabs-nav-list {
.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
color: unset;
}
.ant-tabs-ink-bar {
width: 0 !important;
}
}
}
}
.view-create-modal-filters {
display: flex;
.ant-space-item:nth-child(3) {
flex: 1;
}
}
.view-create-modal-sorts {
display: flex;
.ant-space-item:nth-child(2) {
flex: 1;
}
}
tr.drop-over-downward td {
border-bottom: 2px dashed #1890ff;
}
tr.drop-over-upward td {
border-top: 2px dashed #1890ff;
}
.view-manage-modal {
.ant-modal-body {
padding-top: 10px;
}
}
+306 -113
View File
@@ -12,6 +12,8 @@ import {
Typography, Typography,
Input, Input,
Popover, Popover,
Tabs,
TablePaginationConfig,
} from 'antd'; } from 'antd';
import { import {
ClockCircleOutlined, ClockCircleOutlined,
@@ -27,6 +29,11 @@ import {
PauseCircleOutlined, PauseCircleOutlined,
FieldTimeOutlined, FieldTimeOutlined,
PushpinOutlined, PushpinOutlined,
DownOutlined,
SettingOutlined,
PlusOutlined,
UnorderedListOutlined,
CheckOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
@@ -37,8 +44,13 @@ import CronDetailModal from './detail';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { diffTime } from '@/utils/date'; import { diffTime } from '@/utils/date';
import { getTableScroll } from '@/utils/index'; import { getTableScroll } from '@/utils/index';
import { history } from 'umi'; import { history, useOutletContext } from '@umijs/max';
import './index.less'; import './index.less';
import ViewCreateModal from './viewCreateModal';
import ViewManageModal from './viewManageModal';
import pagination from 'antd/lib/pagination';
import { FilterValue, SorterResult } from 'antd/lib/table/interface';
import { SharedContext } from '@/layouts';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -70,7 +82,8 @@ enum OperationPath {
'unpin', 'unpin',
} }
const Crontab = ({ headerStyle, isPhone, theme }: any) => { const Crontab = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [ const columns: any = [
{ {
title: '名称', title: '名称',
@@ -82,10 +95,11 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
<> <>
<a <a
onClick={() => { onClick={() => {
goToScriptManager(record); setDetailCron(record);
setIsDetailModalVisible(true);
}} }}
> >
{record.labels?.length > 0 && record.labels[0] !== '' ? ( {record.labels?.length > 0 && record.labels[0] !== '' && false ? (
<Popover <Popover
placement="right" placement="right"
trigger={isPhone ? 'click' : 'hover'} trigger={isPhone ? 'click' : 'hover'}
@@ -94,10 +108,12 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
{record.labels?.map((label: string) => ( {record.labels?.map((label: string) => (
<Tag <Tag
color="blue" color="blue"
key={label}
style={{ cursor: 'point' }} style={{ cursor: 'point' }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setSearchValue(`label:${label}`); setSearchValue(`label:${label}`);
setSearchText(`label:${label}`);
}} }}
> >
<a>{label}</a> <a>{label}</a>
@@ -122,15 +138,14 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
</> </>
), ),
sorter: { sorter: {
compare: (a: any, b: any) => a.name.localeCompare(b.name), compare: (a: any, b: any) => a?.name?.localeCompare(b?.name),
multiple: 2,
}, },
}, },
{ {
title: '命令', title: '命令',
dataIndex: 'command', dataIndex: 'command',
key: 'command', key: 'command',
width: 250, width: 300,
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
@@ -141,14 +156,19 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
textAlign: 'left', textAlign: 'left',
}} }}
ellipsis={{ tooltip: text, rows: 2 }} ellipsis={{ tooltip: text, rows: 2 }}
>
<a
onClick={() => {
goToScriptManager(record);
}}
> >
{text} {text}
</a>
</Paragraph> </Paragraph>
); );
}, },
sorter: { sorter: {
compare: (a: any, b: any) => a.command.localeCompare(b.command), compare: (a: any, b: any) => a.command.localeCompare(b.command),
multiple: 3,
}, },
}, },
{ {
@@ -159,12 +179,13 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
align: 'center' as const, align: 'center' as const,
sorter: { sorter: {
compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule), compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule),
multiple: 1,
}, },
}, },
{ {
title: '最后运行时间', title: '最后运行时间',
align: 'center' as const, align: 'center' as const,
dataIndex: 'last_execution_time',
key: 'last_execution_time',
width: 150, width: 150,
sorter: { sorter: {
compare: (a: any, b: any) => { compare: (a: any, b: any) => {
@@ -194,6 +215,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
title: '最后运行时长', title: '最后运行时长',
align: 'center' as const, align: 'center' as const,
width: 120, width: 120,
dataIndex: 'last_running_time',
key: 'last_running_time',
sorter: { sorter: {
compare: (a: any, b: any) => { compare: (a: any, b: any) => {
return a.last_running_time - b.last_running_time; return a.last_running_time - b.last_running_time;
@@ -346,12 +369,25 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const [isLogModalVisible, setIsLogModalVisible] = useState(false); const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [logCron, setLogCron] = useState<any>(); const [logCron, setLogCron] = useState<any>();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]); const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [currentPage, setCurrentPage] = useState(1); const [pageConf, setPageConf] = useState<{
const [pageSize, setPageSize] = useState(20); page: number;
size: number;
sorter: any;
filters: any;
}>({} as any);
const [viewConf, setViewConf] = useState<any>();
const [tableScrollHeight, setTableScrollHeight] = useState<number>(); const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false); const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [detailCron, setDetailCron] = useState<any>(); const [detailCron, setDetailCron] = useState<any>();
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [total, setTotal] = useState<number>();
const [isCreateViewModalVisible, setIsCreateViewModalVisible] =
useState(false);
const [isViewManageModalVisible, setIsViewManageModalVisible] =
useState(false);
const [cronViews, setCronViews] = useState<any[]>([]);
const [enabledCronViews, setEnabledCronViews] = useState<any[]>([]);
const [moreMenuActive, setMoreMenuActive] = useState(false);
const goToScriptManager = (record: any) => { const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[]; const cmd = record.command.split(' ') as string[];
@@ -360,9 +396,13 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
cmd[1] = cmd[1].replace('/ql/data/scripts/', ''); cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
} }
let [p, s] = cmd[1].split('/'); let p: string, s: string;
if (!s) { let index = cmd[1].lastIndexOf('/');
s = p; if (index >= 0) {
s = cmd[1].slice(index + 1);
p = cmd[1].slice(0, index);
} else {
s = cmd[1];
p = ''; p = '';
} }
history.push(`/script?p=${p}&s=${s}`); history.push(`/script?p=${p}&s=${s}`);
@@ -373,27 +413,31 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const getCrons = () => { const getCrons = () => {
setLoading(true); setLoading(true);
const { page, size, sorter, filters } = pageConf;
let url = `${
config.apiPrefix
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
filters,
)}`;
if (sorter && sorter.field) {
url += `&sorter=${JSON.stringify({
field: sorter.field,
type: sorter.order === 'ascend' ? 'ASC' : 'DESC',
})}`;
}
if (viewConf) {
url += `&queryString=${JSON.stringify({
filters: viewConf.filters,
sorts: viewConf.sorts,
})}`;
}
request request
.get(`${config.apiPrefix}crons?searchValue=${searchText}`) .get(url)
.then((data: any) => { .then(({ code, data: _data }) => {
if (code === 200) {
const { data, total } = _data;
setValue( setValue(
data.data data.map((x) => {
.sort((a: any, b: any) => {
const sortA =
a.isPinned && a.status !== 0
? 5
: a.isDisabled && a.status !== 0
? 4
: a.status;
const sortB =
b.isPinned && b.status !== 0
? 5
: b.isDisabled && b.status !== 0
? 4
: b.status;
return CrontabSort[sortA] - CrontabSort[sortB];
})
.map((x) => {
return { return {
...x, ...x,
nextRunTime: cron_parser nextRunTime: cron_parser
@@ -403,7 +447,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
}; };
}), }),
); );
setCurrentPage(1); setTotal(total);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -433,8 +478,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}crons`, { data: [record.id] }) .delete(`${config.apiPrefix}crons`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('删除成功'); message.success('删除成功');
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
@@ -442,8 +487,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
result.splice(i, 1); result.splice(i, 1);
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -468,8 +511,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/run`, { data: [record.id] }) .put(`${config.apiPrefix}crons/run`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) { if (i !== -1) {
@@ -479,8 +522,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -505,8 +546,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/stop`, { data: [record.id] }) .put(`${config.apiPrefix}crons/stop`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) { if (i !== -1) {
@@ -517,8 +558,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -551,8 +590,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
data: [record.id], data: [record.id],
}, },
) )
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const newStatus = record.isDisabled === 1 ? 0 : 1; const newStatus = record.isDisabled === 1 ? 0 : 1;
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
@@ -563,8 +602,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -597,8 +634,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
data: [record.id], data: [record.id],
}, },
) )
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const newStatus = record.isPinned === 1 ? 0 : 1; const newStatus = record.isPinned === 1 ? 0 : 1;
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
@@ -609,8 +646,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -712,20 +747,22 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const getCronDetail = (cron: any) => { const getCronDetail = (cron: any) => {
request request
.get(`${config.apiPrefix}crons/${cron.id}`) .get(`${config.apiPrefix}crons/${cron.id}`)
.then((data: any) => { .then(({ code, data }) => {
if (code === 200) {
const index = value.findIndex((x) => x.id === cron.id); const index = value.findIndex((x) => x.id === cron.id);
const result = [...value]; const result = [...value];
data.data.nextRunTime = cron_parser data.nextRunTime = cron_parser
.parseExpression(data.data.schedule) .parseExpression(data.schedule)
.next() .next()
.toDate(); .toDate();
if (index !== -1) { if (index !== -1) {
result.splice(index, 1, { result.splice(index, 1, {
...cron, ...cron,
...data.data, ...data,
}); });
setValue(result); setValue(result);
} }
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -743,11 +780,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const rowSelection = { const rowSelection = {
selectedRowIds, selectedRowIds,
onChange: onSelectChange, onChange: onSelectChange,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE,
],
}; };
const delCrons = () => { const delCrons = () => {
@@ -757,13 +789,11 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}crons`, { data: selectedRowIds }) .delete(`${config.apiPrefix}crons`, { data: selectedRowIds })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('批量删除成功'); message.success('批量删除成功');
setSelectedRowIds([]); setSelectedRowIds([]);
getCrons(); getCrons();
} else {
message.error(data);
} }
}); });
}, },
@@ -782,11 +812,9 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
.put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, { .put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, {
data: selectedRowIds, data: selectedRowIds,
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
getCrons(); getCrons();
} else {
message.error(data);
} }
}); });
}, },
@@ -796,10 +824,19 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
}); });
}; };
const onPageChange = (page: number, pageSize: number | undefined) => { const onPageChange = (
setCurrentPage(page); pagination: TablePaginationConfig,
setPageSize(pageSize as number); filters: Record<string, FilterValue | null>,
localStorage.setItem('pageSize', pageSize + ''); sorter: SorterResult<any> | SorterResult<any>[],
) => {
const { current, pageSize } = pagination;
setPageConf({
page: current as number,
size: pageSize as number,
sorter,
filters,
});
localStorage.setItem('pageSize', String(pageSize));
}; };
const getRowClassName = (record: any, index: number) => { const getRowClassName = (record: any, index: number) => {
@@ -814,39 +851,37 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
}, [logCron]); }, [logCron]);
useEffect(() => { useEffect(() => {
getCrons(); setPageConf({ ...pageConf, page: 1 });
}, [searchText]); }, [searchText]);
useEffect(() => { useEffect(() => {
setPageSize(parseInt(localStorage.getItem('pageSize') || '20')); if (pageConf.page && pageConf.size) {
getCrons();
}
}, [pageConf, viewConf]);
useEffect(() => {
if (viewConf && enabledCronViews && enabledCronViews.length > 0) {
const view = enabledCronViews.slice(2).find((x) => x.id === viewConf.id);
setMoreMenuActive(!!view);
}
}, [viewConf, enabledCronViews]);
useEffect(() => {
setPageConf({
page: 1,
size: parseInt(localStorage.getItem('pageSize') || '20'),
sorter: {},
filters: {},
});
setTimeout(() => { setTimeout(() => {
setTableScrollHeight(getTableScroll()); setTableScrollHeight(getTableScroll());
}); });
getCronViews();
}, []); }, []);
return ( const panelContent = (
<PageContainer <>
className="ql-container-wrapper crontab-wrapper"
title="定时任务"
extra={[
<Search
placeholder="请输入名称或者关键词"
style={{ width: 'auto' }}
enterButton
allowClear
loading={loading}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onSearch={onSearch}
/>,
<Button key="2" type="primary" onClick={() => addCron()}>
</Button>,
]}
header={{
style: headerStyle,
}}
>
{selectedRowIds.length > 0 && ( {selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}> <div style={{ marginBottom: 16 }}>
<Button type="primary" style={{ marginBottom: 5 }} onClick={delCrons}> <Button type="primary" style={{ marginBottom: 5 }} onClick={delCrons}>
@@ -906,23 +941,16 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
<Table <Table
columns={columns} columns={columns}
pagination={{ pagination={{
current: currentPage, current: pageConf.page,
onChange: onPageChange, pageSize: pageConf.size,
pageSize: pageSize,
showSizeChanger: true, showSizeChanger: true,
simple: isPhone, simple: isPhone,
defaultPageSize: 20, total,
showTotal: (total: number, range: number[]) => showTotal: (total: number, range: number[]) =>
`${range[0]}-${range[1]} 条/总共 ${total}`, `${range[0]}-${range[1]} 条/总共 ${total}`,
pageSizeOptions: [20, 100, 500, 1000] as any, pageSizeOptions: [10, 20, 50, 100, 200, 500, total || 10000].sort(
}} (a, b) => a - b,
onRow={(record) => { ),
return {
onClick: (event) => {
setDetailCron(record);
setIsDetailModalVisible(true);
},
};
}} }}
dataSource={value} dataSource={value}
rowKey="id" rowKey="id"
@@ -931,7 +959,149 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
loading={loading} loading={loading}
rowSelection={rowSelection} rowSelection={rowSelection}
rowClassName={getRowClassName} rowClassName={getRowClassName}
onChange={onPageChange}
/> />
</>
);
const viewAction = (key: string) => {
switch (key) {
case 'new':
setIsCreateViewModalVisible(true);
break;
case 'manage':
setIsViewManageModalVisible(true);
break;
default:
tabClick(key);
break;
}
};
const menu = (
<Menu
onClick={({ key, domEvent }) => {
domEvent.stopPropagation();
viewAction(key);
}}
items={[
...[...enabledCronViews].slice(2).map((x) => ({
label: (
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{x.name}</span>
{viewConf?.id === x.id && (
<CheckOutlined style={{ color: '#1890ff' }} />
)}
</Space>
),
key: x.id,
icon: <UnorderedListOutlined />,
})),
{
type: 'divider',
},
{
label: '新建视图',
key: 'new',
icon: <PlusOutlined />,
},
{
label: '视图管理',
key: 'manage',
icon: <SettingOutlined />,
},
]}
/>
);
const getCronViews = () => {
setLoading(true);
request
.get(`${config.apiPrefix}crons/views`)
.then(({ code, data }) => {
if (code === 200) {
setCronViews(data);
setEnabledCronViews(data.filter((x) => !x.isDisabled));
}
})
.finally(() => {
setLoading(false);
});
};
const tabClick = (key: string) => {
const view = enabledCronViews.find((x) => x.id == key);
setPageConf({ ...pageConf, page: 1 });
setViewConf(view ? view : null);
};
return (
<PageContainer
className="ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab"
title="定时任务"
extra={[
<Search
placeholder="请输入名称或者关键词"
style={{ width: 'auto' }}
enterButton
allowClear
loading={loading}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onSearch={onSearch}
/>,
<Button key="2" type="primary" onClick={() => addCron()}>
</Button>,
]}
header={{
style: headerStyle,
}}
>
<Tabs
defaultActiveKey="all"
size="small"
tabPosition="top"
className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`}
tabBarExtraContent={
<Dropdown
overlay={menu}
trigger={['click']}
overlayStyle={{ minWidth: 200 }}
>
<div className={`view-more ${moreMenuActive ? 'active' : ''}`}>
<Space>
<DownOutlined />
</Space>
<div className="ant-tabs-ink-bar ant-tabs-ink-bar-animated"></div>
</div>
</Dropdown>
}
onTabClick={tabClick}
items={[
{
key: 'all',
label: '全部任务',
children: panelContent,
},
...[...enabledCronViews].slice(0, 2).map((x) => ({
key: x.id,
label: x.name,
children: panelContent,
})),
]}
>
<Tabs.TabPane tab="全部任务" key="all">
{panelContent}
</Tabs.TabPane>
{[...enabledCronViews].slice(0, 2).map((x) => (
<Tabs.TabPane tab={x.name} key={x.id}>
{panelContent}
</Tabs.TabPane>
))}
</Tabs>
<CronLogModal <CronLogModal
visible={isLogModalVisible} visible={isLogModalVisible}
handleCancel={() => { handleCancel={() => {
@@ -964,6 +1134,29 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
theme={theme} theme={theme}
isPhone={isPhone} isPhone={isPhone}
/> />
<ViewCreateModal
visible={isCreateViewModalVisible}
handleCancel={(data) => {
setIsCreateViewModalVisible(false);
getCronViews();
if (data && data.id === viewConf.id) {
setViewConf({ ...viewConf, ...data });
}
}}
/>
<ViewManageModal
cronViews={cronViews}
visible={isViewManageModalVisible}
handleCancel={() => {
setIsViewManageModalVisible(false);
}}
cronViewChange={(data) => {
getCronViews();
if (data && data.id === viewConf.id) {
setViewConf({ ...viewConf, ...data });
}
}}
/>
</PageContainer> </PageContainer>
); );
}; };
+7 -4
View File
@@ -41,9 +41,12 @@ const CronLogModal = ({
} }
request request
.get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`) .get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`)
.then((data: any) => { .then(({ code, data }) => {
if (localStorage.getItem('logCron') === String(cron.id)) { if (
const log = data.data as string; code === 200 &&
localStorage.getItem('logCron') === String(cron.id)
) {
const log = data as string;
setValue(log || '暂无日志'); setValue(log || '暂无日志');
setExecuting( setExecuting(
log && !log.includes('执行结束') && !log.includes('重启面板'), log && !log.includes('执行结束') && !log.includes('重启面板'),
@@ -119,7 +122,7 @@ const CronLogModal = ({
return ( return (
<Modal <Modal
title={titleElement()} title={titleElement()}
visible={visible} open={visible}
centered centered
className="log-modal" className="log-modal"
bodyStyle={{ bodyStyle={{
+10 -8
View File
@@ -28,13 +28,12 @@ const CronModal = ({
const { code, data } = await request[method](`${config.apiPrefix}crons`, { const { code, data } = await request[method](`${config.apiPrefix}crons`, {
data: payload, data: payload,
}); });
if (code === 200) { if (code === 200) {
message.success(cron ? '更新Cron成功' : '新建Cron成功'); message.success(cron ? '更新Cron成功' : '新建Cron成功');
} else { handleCancel(data);
message.error(data);
} }
setLoading(false); setLoading(false);
handleCancel(data);
} catch (error: any) { } catch (error: any) {
setLoading(false); setLoading(false);
} }
@@ -47,7 +46,7 @@ const CronModal = ({
return ( return (
<Modal <Modal
title={cron ? '编辑任务' : '新建任务'} title={cron ? '编辑任务' : '新建任务'}
visible={visible} open={visible}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
@@ -128,21 +127,24 @@ const CronLabelModal = ({
.then(async (values) => { .then(async (values) => {
setLoading(true); setLoading(true);
const payload = { ids, labels: values.labels }; const payload = { ids, labels: values.labels };
try {
const { code, data } = await request[action]( const { code, data } = await request[action](
`${config.apiPrefix}crons/labels`, `${config.apiPrefix}crons/labels`,
{ {
data: payload, data: payload,
}, },
); );
if (code === 200) { if (code === 200) {
message.success( message.success(
action === 'post' ? '添加Labels成功' : '删除Labels成功', action === 'post' ? '添加Labels成功' : '删除Labels成功',
); );
} else { handleCancel(true);
message.error(data);
} }
setLoading(false); setLoading(false);
handleCancel(true); } catch (error) {
setLoading(false);
}
}) })
.catch((info) => { .catch((info) => {
console.log('Validate Failed:', info); console.log('Validate Failed:', info);
@@ -166,7 +168,7 @@ const CronLabelModal = ({
return ( return (
<Modal <Modal
title="批量修改标签" title="批量修改标签"
visible={visible} open={visible}
footer={buttons} footer={buttons}
centered centered
maskClosable={false} maskClosable={false}
+262
View File
@@ -0,0 +1,262 @@
import React, { useEffect, useState } from 'react';
import {
Modal,
message,
Input,
Form,
Statistic,
Button,
Space,
Select,
} from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
const PROPERTIES = [
{ name: '命令', value: 'command' },
{ name: '名称', value: 'name' },
{ name: '定时规则', value: 'schedule' },
{ name: '状态', value: 'status' },
];
const OPERATIONS = [
{ name: '包含', value: 'Reg' },
{ name: '不包含', value: 'NotReg' },
{ name: '属于', value: 'In' },
{ name: '不属于', value: 'Nin' },
// { name: '等于', value: 'Eq' },
// { name: '不等于', value: 'Ne' },
// { name: '为空', value: 'IsNull' },
// { name: '不为空', value: 'NotNull' },
];
const SORTTYPES = [
{ name: '顺序', value: 'ASC' },
{ name: '倒序', value: 'DESC' },
];
const STATUS = [
{ name: '运行中', value: 0 },
{ name: '空闲中', value: 1 },
{ name: '已禁用', value: 2 },
];
const ViewCreateModal = ({
view,
handleCancel,
visible,
}: {
view?: any;
visible: boolean;
handleCancel: (param?: any) => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [operationMap, setOperationMap] = useState<any>();
const handleOk = async (values: any) => {
setLoading(true);
const method = view ? 'put' : 'post';
try {
const { code, data } = await request[method](
`${config.apiPrefix}crons/views`,
{
data: view ? { ...values, id: view.id } : values,
},
);
if (code === 200) {
handleCancel(data);
}
setLoading(false);
} catch (error: any) {
setLoading(false);
}
};
useEffect(() => {
if (!view) {
form.resetFields();
}
form.setFieldsValue(
view || {
filters: [{ property: 'command', operation: 'Reg' }],
},
);
}, [view, visible]);
const operationElement = (
<Select
style={{ width: 100 }}
onChange={() => {
setOperationMap({});
}}
>
{OPERATIONS.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
</Select.Option>
))}
</Select>
);
const propertyElement = (props: any, style: React.CSSProperties = {}) => {
return (
<Select style={style}>
{props.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
</Select.Option>
))}
</Select>
);
};
const typeElement = (
<Select>
{SORTTYPES.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
</Select.Option>
))}
</Select>
);
const statusElement = (
<Select mode="multiple" allowClear placeholder="请选择状态">
{STATUS.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
</Select.Option>
))}
</Select>
);
return (
<Modal
title={view ? '编辑视图' : '新建视图'}
open={visible}
forceRender
width={580}
centered
maskClosable={false}
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
confirmLoading={loading}
>
<Form form={form} layout="vertical" name="env_modal">
<Form.Item
name="name"
label="视图名称"
rules={[{ required: true, message: '请输入视图名称' }]}
>
<Input placeholder="请输入视图名称" />
</Form.Item>
<Form.List name="filters">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }, index) => (
<Form.Item
label={index === 0 ? '筛选条件' : ''}
key={key}
style={{ marginBottom: 0 }}
required
>
<Space className="view-create-modal-filters" align="baseline">
<Form.Item
{...restField}
name={[name, 'property']}
rules={[{ required: true }]}
>
{propertyElement(PROPERTIES, { width: 120 })}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'operation']}
rules={[{ required: true }]}
>
{operationElement}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'value']}
rules={[{ required: true, message: '请输入内容' }]}
>
{['In', 'Nin'].includes(
form.getFieldValue(['filters', index, 'operation']),
) ? (
statusElement
) : (
<Input placeholder="请输入内容" />
)}
</Form.Item>
{index !== 0 && (
<MinusCircleOutlined onClick={() => remove(name)} />
)}
</Space>
</Form.Item>
))}
<Form.Item>
<a
onClick={() => add({ property: 'command', operation: 'Reg' })}
>
<PlusOutlined />
</a>
</Form.Item>
</>
)}
</Form.List>
<Form.List name="sorts">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }, index) => (
<Form.Item
label={index === 0 ? '排序方式' : ''}
key={key}
style={{ marginBottom: 0 }}
>
<Space className="view-create-modal-sorts" align="baseline">
<Form.Item
{...restField}
name={[name, 'property']}
rules={[{ required: true }]}
>
{propertyElement(PROPERTIES, { width: 240 })}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'type']}
rules={[{ required: true }]}
>
{typeElement}
</Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} />
</Space>
</Form.Item>
))}
<Form.Item>
<a onClick={() => add({ property: 'command', type: 'ASC' })}>
<PlusOutlined />
</a>
</Form.Item>
</>
)}
</Form.List>
</Form>
</Modal>
);
};
export default ViewCreateModal;
+266
View File
@@ -0,0 +1,266 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
Modal,
message,
Space,
Table,
Tag,
Typography,
Button,
Switch,
} from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import ViewCreateModal from './viewCreateModal';
const { Text } = Typography;
const type = 'DragableBodyRow';
const DragableBodyRow = ({
index,
moveRow,
className,
style,
...restProps
}: any) => {
const ref = useRef();
const [{ isOver, dropClassName }, drop] = useDrop({
accept: type,
collect: (monitor) => {
const { index: dragIndex } = (monitor.getItem() as any) || {};
if (dragIndex === index) {
return {};
}
return {
isOver: monitor.isOver(),
dropClassName:
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
};
},
drop: (item: any) => {
moveRow(item.index, index);
},
});
const [, drag] = useDrag({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
drop(drag(ref));
return (
<tr
ref={ref}
className={`${className}${isOver ? dropClassName : ''}`}
style={{ cursor: 'move', ...style }}
{...restProps}
/>
);
};
const ViewManageModal = ({
cronViews,
handleCancel,
visible,
cronViewChange,
}: {
cronViews: any[];
visible: boolean;
handleCancel: () => void;
cronViewChange: (data?: any) => void;
}) => {
const columns: any = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
align: 'center' as const,
},
{
title: '显示',
key: 'isDisabled',
dataIndex: 'isDisabled',
align: 'center' as const,
width: 100,
render: (text: string, record: any, index: number) => {
return (
<Switch
checked={!record.isDisabled}
onChange={(checked) => onShowChange(checked, record, index)}
/>
);
},
},
{
title: '操作',
key: 'action',
width: 140,
align: 'center' as const,
render: (text: string, record: any, index: number) => {
return (
<Space size="middle">
<a onClick={() => editView(record, index)}>
<EditOutlined />
</a>
<a onClick={() => deleteView(record, index)}>
<DeleteOutlined />
</a>
</Space>
);
},
},
];
const [list, setList] = useState<any[]>([]);
const [isCreateViewModalVisible, setIsCreateViewModalVisible] =
useState<boolean>(false);
const [editedView, setEditedView] = useState<any>(null);
const editView = (record: any, index: number) => {
setEditedView(record);
setIsCreateViewModalVisible(true);
};
const deleteView = (record: any, index: number) => {
Modal.confirm({
title: '确认删除',
content: (
<>
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
</>
),
onOk() {
request
.delete(`${config.apiPrefix}crons/views`, { data: [record.id] })
.then(({ code, data }) => {
if (code === 200) {
message.success('删除成功');
cronViewChange();
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const onShowChange = (checked: boolean, record: any, index: number) => {
console.log(checked);
request
.put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, {
data: [record.id],
})
.then(({ code, data }) => {
if (code === 200) {
const _list = [...list];
_list.splice(index, 1, { ...list[index], isDisabled: !checked });
setList(_list);
cronViewChange();
}
});
};
const components = {
body: {
row: DragableBodyRow,
},
};
const moveRow = useCallback(
(dragIndex, hoverIndex) => {
if (dragIndex === hoverIndex) {
return;
}
const dragRow = list[dragIndex];
request
.put(`${config.apiPrefix}crons/views/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex, id: dragRow.id },
})
.then(({ code, data }) => {
if (code === 200) {
const newData = [...list];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data });
setList(newData);
cronViewChange();
}
});
},
[list],
);
useEffect(() => {
setList(cronViews);
}, [cronViews]);
return (
<Modal
title="视图管理"
open={visible}
centered
width={620}
onCancel={() => handleCancel()}
className="view-manage-modal"
forceRender
footer={false}
maskClosable={false}
>
<Space
style={{
display: 'flex',
justifyContent: 'flex-end',
marginBottom: 10,
}}
>
<Button
key="2"
type="primary"
onClick={() => {
setEditedView(null);
setIsCreateViewModalVisible(true);
}}
>
</Button>
</Space>
<DndProvider backend={HTML5Backend}>
<Table
bordered
columns={columns}
pagination={false}
dataSource={list}
rowKey="id"
size="middle"
style={{ marginBottom: 20 }}
components={components}
onRow={(record: any, index: number) => {
return {
index,
moveRow,
} as any;
}}
/>
</DndProvider>
<ViewCreateModal
view={editedView}
visible={isCreateViewModalVisible}
handleCancel={(data) => {
setIsCreateViewModalVisible(false);
cronViewChange(data);
}}
/>
</Modal>
);
};
export default ViewManageModal;
+68 -32
View File
@@ -29,6 +29,8 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less'; import './index.less';
import { getTableScroll } from '@/utils/index'; import { getTableScroll } from '@/utils/index';
import DependenceLogModal from './logModal'; import DependenceLogModal from './logModal';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const { Text } = Typography; const { Text } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -48,7 +50,9 @@ enum StatusColor {
'error', 'error',
} }
const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => { const Dependence = () => {
const { headerStyle, isPhone, socketMessage } =
useOutletContext<SharedContext>();
const columns: any = [ const columns: any = [
{ {
title: '序号', title: '序号',
@@ -171,8 +175,10 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
.get( .get(
`${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`, `${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`,
) )
.then((data: any) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -208,9 +214,8 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
.delete(`${config.apiPrefix}dependencies${force ? '/force' : ''}`, { .delete(`${config.apiPrefix}dependencies${force ? '/force' : ''}`, {
data: [record.id], data: [record.id],
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200 && force) {
if (force) {
const i = value.findIndex((x) => x.id === data.data[0].id); const i = value.findIndex((x) => x.id === data.data[0].id);
if (i !== -1) { if (i !== -1) {
const result = [...value]; const result = [...value];
@@ -218,9 +223,6 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
setValue(result); setValue(result);
} }
} }
} else {
message.error(data);
}
}); });
}, },
onCancel() { onCancel() {
@@ -246,11 +248,9 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
.put(`${config.apiPrefix}dependencies/reinstall`, { .put(`${config.apiPrefix}dependencies/reinstall`, {
data: [record.id], data: [record.id],
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
handleDependence(data.data[0]); handleDependence(data[0]);
} else {
message.error(data);
} }
}); });
}, },
@@ -305,12 +305,32 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
.delete(`${config.apiPrefix}dependencies${forceUrl}`, { .delete(`${config.apiPrefix}dependencies${forceUrl}`, {
data: selectedRowIds, data: selectedRowIds,
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setSelectedRowIds([]);
getDependencies();
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const handlereInstallDependencies = () => {
Modal.confirm({
title: '确认重新安装',
content: <></>,
onOk() {
request
.put(`${config.apiPrefix}dependencies/reinstall`, {
data: selectedRowIds,
})
.then(({ code, data }) => {
if (code === 200) {
setSelectedRowIds([]); setSelectedRowIds([]);
getDependencies(); getDependencies();
} else {
message.error(data);
} }
}); });
}, },
@@ -323,16 +343,18 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
const getDependenceDetail = (dependence: any) => { const getDependenceDetail = (dependence: any) => {
request request
.get(`${config.apiPrefix}dependencies/${dependence.id}`) .get(`${config.apiPrefix}dependencies/${dependence.id}`)
.then((data: any) => { .then(({ code, data }) => {
if (code === 200) {
const index = value.findIndex((x) => x.id === dependence.id); const index = value.findIndex((x) => x.id === dependence.id);
const result = [...value]; const result = [...value];
if (index !== -1) { if (index !== -1) {
result.splice(index, 1, { result.splice(index, 1, {
...dependence, ...dependence,
...data.data, ...data,
}); });
setValue(result); setValue(result);
} }
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -403,6 +425,13 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
<> <>
{selectedRowIds.length > 0 && ( {selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}> <div style={{ marginBottom: 16 }}>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => handlereInstallDependencies()}
>
</Button>
<Button <Button
type="primary" type="primary"
style={{ marginBottom: 5, marginLeft: 8 }} style={{ marginBottom: 5, marginLeft: 8 }}
@@ -444,7 +473,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper dependence-wrapper" className="ql-container-wrapper dependence-wrapper ql-container-wrapper-has-tab"
title="依赖管理" title="依赖管理"
extra={[ extra={[
<Search <Search
@@ -467,17 +496,24 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
size="small" size="small"
tabPosition="top" tabPosition="top"
onChange={onTabChange} onChange={onTabChange}
> items={[
<Tabs.TabPane tab="NodeJs" key="nodejs"> {
{panelContent()} key: 'nodejs',
</Tabs.TabPane> label: 'NodeJs',
<Tabs.TabPane tab="Python3" key="python3"> children: panelContent(),
{panelContent()} },
</Tabs.TabPane> {
<Tabs.TabPane tab="Linux" key="linux"> key: 'python3',
{panelContent()} label: 'Python3',
</Tabs.TabPane> children: panelContent(),
</Tabs> },
{
key: 'linux',
label: 'Linux',
children: panelContent(),
},
]}
/>
<DependenceModal <DependenceModal
visible={isModalVisible} visible={isModalVisible}
handleCancel={handleCancel} handleCancel={handleCancel}
+10 -5
View File
@@ -48,9 +48,12 @@ const DependenceLogModal = ({
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}dependencies/${dependence.id}`) .get(`${config.apiPrefix}dependencies/${dependence.id}`)
.then((data: any) => { .then(({ code, data }) => {
if (localStorage.getItem('logDependence') === String(dependence.id)) { if (
const log = (data.data.log || []).join('') as string; code === 200 &&
localStorage.getItem('logDependence') === String(dependence.id)
) {
const log = (data.log || []).join('') as string;
setValue(log); setValue(log);
setExecuting(!log.includes('结束时间')); setExecuting(!log.includes('结束时间'));
setIsRemoveFailed(log.includes('删除失败')); setIsRemoveFailed(log.includes('删除失败'));
@@ -67,8 +70,10 @@ const DependenceLogModal = ({
.delete(`${config.apiPrefix}dependencies/force`, { .delete(`${config.apiPrefix}dependencies/force`, {
data: [dependence.id], data: [dependence.id],
}) })
.then((data: any) => { .then(({ code, data }) => {
if (code === 200) {
cancel(true); cancel(true);
}
}) })
.finally(() => { .finally(() => {
setRemoveLoading(false); setRemoveLoading(false);
@@ -112,7 +117,7 @@ const DependenceLogModal = ({
return ( return (
<Modal <Modal
title={titleElement()} title={titleElement()}
visible={visible} open={visible}
centered centered
className="log-modal" className="log-modal"
bodyStyle={{ bodyStyle={{
+7 -5
View File
@@ -52,11 +52,11 @@ const DependenceModal = ({
data: payload, data: payload,
}, },
); );
if (code !== 200) {
message.error(data); if (code === 200) {
handleCancel(data);
} }
setLoading(false); setLoading(false);
handleCancel(data);
} catch (error) { } catch (error) {
setLoading(false); setLoading(false);
} }
@@ -69,7 +69,7 @@ const DependenceModal = ({
return ( return (
<Modal <Modal
title={dependence ? '编辑依赖' : '新建依赖'} title={dependence ? '编辑依赖' : '新建依赖'}
visible={visible} open={visible}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
@@ -99,7 +99,9 @@ const DependenceModal = ({
> >
<Select> <Select>
{config.dependenceTypes.map((x, i) => ( {config.dependenceTypes.map((x, i) => (
<Option value={i}>{x}</Option> <Option key={i} value={i}>
{x}
</Option>
))} ))}
</Select> </Select>
</Form.Item> </Form.Item>
+4
View File
@@ -17,4 +17,8 @@
.ant-form-item { .ant-form-item {
margin-bottom: 8px; margin-bottom: 8px;
} }
+ section {
height: calc(100% - 40px) !important;
}
} }
+30 -11
View File
@@ -6,10 +6,13 @@ import { request } from '@/utils/http';
import './index.less'; import './index.less';
import { DiffEditor } from '@monaco-editor/react'; import { DiffEditor } from '@monaco-editor/react';
import ReactDiffViewer from 'react-diff-viewer'; import ReactDiffViewer from 'react-diff-viewer';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const { Option } = Select; const { Option } = Select;
const Diff = ({ headerStyle, isPhone, theme }: any) => { const Diff = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [origin, setOrigin] = useState('config.sample.sh'); const [origin, setOrigin] = useState('config.sample.sh');
const [current, setCurrent] = useState('config.sh'); const [current, setCurrent] = useState('config.sh');
const [originValue, setOriginValue] = useState(''); const [originValue, setOriginValue] = useState('');
@@ -19,14 +22,22 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const getConfig = () => { const getConfig = () => {
request.get(`${config.apiPrefix}configs/${current}`).then((data) => { request
setCurrentValue(data.data); .get(`${config.apiPrefix}configs/${current}`)
.then(({ code, data }) => {
if (code === 200) {
setCurrentValue(data);
}
}); });
}; };
const getSample = () => { const getSample = () => {
request.get(`${config.apiPrefix}configs/${origin}`).then((data) => { request
setOriginValue(data.data); .get(`${config.apiPrefix}configs/${origin}`)
.then(({ code, data }) => {
if (code === 200) {
setOriginValue(data);
}
}); });
}; };
@@ -39,8 +50,10 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
.post(`${config.apiPrefix}configs/save`, { .post(`${config.apiPrefix}configs/save`, {
data: { content, name: current }, data: { content, name: current },
}) })
.then((data: any) => { .then(({ code, data }) => {
message.success(data.message); if (code === 200) {
message.success('保存成功');
}
}); });
}; };
@@ -48,8 +61,10 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}configs/files`) .get(`${config.apiPrefix}configs/files`)
.then((data: any) => { .then(({ code, data }) => {
setFiles(data.data); if (code === 200) {
setFiles(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -95,7 +110,9 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
<Form.Item label="源文件"> <Form.Item label="源文件">
<Select value={origin} onChange={originFileChange}> <Select value={origin} onChange={originFileChange}>
{files.map((x) => ( {files.map((x) => (
<Option value={x.value}>{x.title}</Option> <Option key={x.value} value={x.value}>
{x.title}
</Option>
))} ))}
</Select> </Select>
</Form.Item> </Form.Item>
@@ -104,7 +121,9 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
<Form.Item label="当前文件"> <Form.Item label="当前文件">
<Select value={current} onChange={currentFileChange}> <Select value={current} onChange={currentFileChange}>
{files.map((x) => ( {files.map((x) => (
<Option value={x.value}>{x.title}</Option> <Option key={x.value} value={x.value}>
{x.title}
</Option>
))} ))}
</Select> </Select>
</Form.Item> </Form.Item>
+7 -4
View File
@@ -17,19 +17,22 @@ const EditNameModal = ({
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
try {
const { code, data } = await request.put(`${config.apiPrefix}envs/name`, { const { code, data } = await request.put(`${config.apiPrefix}envs/name`, {
data: { data: {
ids, ids,
name: values.name, name: values.name,
}, },
}); });
if (code === 200) { if (code === 200) {
message.success('更新环境变量名称成功'); message.success('更新环境变量名称成功');
} else { handleCancel();
message.error(data);
} }
setLoading(false); setLoading(false);
handleCancel(); } catch (error) {
setLoading(false);
}
}; };
useEffect(() => { useEffect(() => {
@@ -39,7 +42,7 @@ const EditNameModal = ({
return ( return (
<Modal <Modal
title="修改环境变量名称" title="修改环境变量名称"
visible={visible} open={visible}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
+18 -23
View File
@@ -26,6 +26,8 @@ import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less'; import './index.less';
import { exportJson, getTableScroll } from '@/utils/index'; import { exportJson, getTableScroll } from '@/utils/index';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -96,7 +98,8 @@ const DragableBodyRow = ({
); );
}; };
const Env = ({ headerStyle, isPhone, theme }: any) => { const Env = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [ const columns: any = [
{ {
title: '序号', title: '序号',
@@ -252,8 +255,10 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}envs?searchValue=${searchText}`) .get(`${config.apiPrefix}envs?searchValue=${searchText}`)
.then((data: any) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -281,8 +286,8 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
data: [record.id], data: [record.id],
}, },
) )
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success( message.success(
`${record.status === Status. ? '启用' : '禁用'}成功`, `${record.status === Status. ? '启用' : '禁用'}成功`,
); );
@@ -294,8 +299,6 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
status: newStatus, status: newStatus,
}); });
setValue(result); setValue(result);
} else {
message.error(data);
} }
}); });
}, },
@@ -330,14 +333,12 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}envs`, { data: [record.id] }) .delete(`${config.apiPrefix}envs`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('删除成功'); message.success('删除成功');
const result = [...value]; const result = [...value];
result.splice(index, 1); result.splice(index, 1);
setValue(result); setValue(result);
} else {
message.error(data);
} }
}); });
}, },
@@ -387,14 +388,12 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
.put(`${config.apiPrefix}envs/${dragRow.id}/move`, { .put(`${config.apiPrefix}envs/${dragRow.id}/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex }, data: { fromIndex: dragIndex, toIndex: hoverIndex },
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const newData = [...value]; const newData = [...value];
newData.splice(dragIndex, 1); newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data }); newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
setValue([...newData]); setValue([...newData]);
} else {
message.error(data);
} }
}); });
}, },
@@ -423,13 +422,11 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}envs`, { data: selectedRowIds }) .delete(`${config.apiPrefix}envs`, { data: selectedRowIds })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('批量删除成功'); message.success('批量删除成功');
setSelectedRowIds([]); setSelectedRowIds([]);
getEnvs(); getEnvs();
} else {
message.error(data);
} }
}); });
}, },
@@ -448,11 +445,9 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
.put(`${config.apiPrefix}envs/${OperationPath[operationStatus]}`, { .put(`${config.apiPrefix}envs/${OperationPath[operationStatus]}`, {
data: selectedRowIds, data: selectedRowIds,
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
getEnvs(); getEnvs();
} else {
message.error(data);
} }
}); });
}, },
+3 -4
View File
@@ -40,13 +40,12 @@ const EnvModal = ({
const { code, data } = await request[method](`${config.apiPrefix}envs`, { const { code, data } = await request[method](`${config.apiPrefix}envs`, {
data: payload, data: payload,
}); });
if (code === 200) { if (code === 200) {
message.success(env ? '更新变量成功' : '新建变量成功'); message.success(env ? '更新变量成功' : '新建变量成功');
} else { handleCancel(data);
message.error(data);
} }
setLoading(false); setLoading(false);
handleCancel(data);
} catch (error: any) { } catch (error: any) {
setLoading(false); setLoading(false);
} }
@@ -59,7 +58,7 @@ const EnvModal = ({
return ( return (
<Modal <Modal
title={env ? '编辑变量' : '新建变量'} title={env ? '编辑变量' : '新建变量'}
visible={visible} open={visible}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
+8 -4
View File
@@ -3,11 +3,13 @@ import config from '@/utils/config';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import Terminal, { ColorMode, LineType } from '../../components/terminal'; import Terminal, { ColorMode, LineType } from '../../components/terminal';
import { PageLoading } from '@ant-design/pro-layout'; import { PageLoading } from '@ant-design/pro-layout';
import { history } from 'umi'; import { history, useOutletContext } from '@umijs/max';
import Ansi from 'ansi-to-react'; import Ansi from 'ansi-to-react';
import './index.less'; import './index.less';
import { SharedContext } from '@/layouts';
const Error = ({ user, theme }: any) => { const Error = () => {
const { user, theme } = useOutletContext<SharedContext>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState('暂无日志'); const [data, setData] = useState('暂无日志');
@@ -15,8 +17,10 @@ const Error = ({ user, theme }: any) => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}public/panel/log`) .get(`${config.apiPrefix}public/panel/log`)
.then((data: any) => { .then(({ code, data }) => {
setData(data.data); if (code === 200) {
setData(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
+9 -10
View File
@@ -10,7 +10,7 @@ import {
Select, Select,
} from 'antd'; } from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { history } from 'umi'; import { history } from '@umijs/max';
import styles from './index.less'; import styles from './index.less';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
@@ -41,11 +41,9 @@ const Initialization = () => {
password: values.password, password: values.password,
}, },
}) })
.then((data) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
next(); next();
} else {
message.error(data.message);
} }
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -59,11 +57,9 @@ const Initialization = () => {
...values, ...values,
}, },
}) })
.then((_data: any) => { .then(({ code, data }) => {
if (_data && _data.code === 200) { if (code === 200) {
next(); next();
} else {
message.error(_data.message);
} }
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -116,12 +112,15 @@ const Initialization = () => {
{config.notificationModes {config.notificationModes
.filter((x) => x.value !== 'closed') .filter((x) => x.value !== 'closed')
.map((x) => ( .map((x) => (
<Option value={x.value}>{x.label}</Option> <Option key={x.value} value={x.value}>
{x.label}
</Option>
))} ))}
</Select> </Select>
</Form.Item> </Form.Item>
{fields.map((x) => ( {fields.map((x) => (
<Form.Item <Form.Item
key={x.label}
label={x.label} label={x.label}
name={x.label} name={x.label}
extra={x.tip} extra={x.tip}
+1 -2
View File
@@ -6,8 +6,7 @@
overflow: hidden; overflow: hidden;
position: relative; position: relative;
background-color: @component-background; background-color: @component-background;
height: calc(100vh - 128px); height: 100%;
height: calc(100vh - var(--vh-offset, 0px) - 128px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
+126 -14
View File
@@ -1,5 +1,15 @@
import { useState, useEffect, useCallback, Key, useRef } from 'react'; import { useState, useEffect, useCallback, Key, useRef } from 'react';
import { TreeSelect, Tree, Input, Empty } from 'antd'; import {
TreeSelect,
Tree,
Input,
Empty,
Button,
message,
Modal,
Tooltip,
Typography,
} from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
@@ -7,6 +17,13 @@ import { request } from '@/utils/http';
import styles from './index.module.less'; import styles from './index.module.less';
import { Controlled as CodeMirror } from 'react-codemirror2'; import { Controlled as CodeMirror } from 'react-codemirror2';
import SplitPane from 'react-split-pane'; import SplitPane from 'react-split-pane';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import { DeleteOutlined } from '@ant-design/icons';
import { depthFirstSearch } from '@/utils';
import { debounce } from 'lodash';
const { Text } = Typography;
function getFilterData(keyword: string, data: any) { function getFilterData(keyword: string, data: any) {
const expandedKeys: string[] = []; const expandedKeys: string[] = [];
@@ -36,24 +53,28 @@ function getFilterData(keyword: string, data: any) {
return { tree: data, expandedKeys }; return { tree: data, expandedKeys };
} }
const Log = ({ headerStyle, isPhone, theme }: any) => { const Log = () => {
const [title, setTitle] = useState('请选择日志文件'); const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [value, setValue] = useState('请选择日志文件'); const [value, setValue] = useState('请选择日志文件');
const [select, setSelect] = useState<any>(); const [select, setSelect] = useState<string>('');
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const [filterData, setFilterData] = useState<any[]>([]); const [filterData, setFilterData] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [height, setHeight] = useState<number>(); const [height, setHeight] = useState<number>();
const treeDom = useRef<any>(); const treeDom = useRef<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]); const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const [currentNode, setCurrentNode] = useState<any>();
const [searchValue, setSearchValue] = useState('');
const getLogs = () => { const getLogs = () => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}logs`) .get(`${config.apiPrefix}logs`)
.then((data) => { .then(({ code, data }) => {
setData(data.data); if (code === 200) {
setFilterData(data.data); setData(data);
setFilterData(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -61,18 +82,27 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
const getLog = (node: any) => { const getLog = (node: any) => {
request request
.get(`${config.apiPrefix}logs/${node.title}?path=${node.parent || ''}`) .get(`${config.apiPrefix}logs/${node.title}?path=${node.parent || ''}`)
.then((data) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}); });
}; };
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
setCurrentNode(node);
setSelect(value);
if (node.key === select || !value) { if (node.key === select || !value) {
return; return;
} }
if (node.type === 'directory') {
setValue('请选择日志文件');
return;
}
setValue('加载中...'); setValue('加载中...');
setSelect(value);
setTitle(node.key);
getLog(node); getLog(node);
}; };
@@ -83,16 +113,86 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
const onSearch = useCallback( const onSearch = useCallback(
(e) => { (e) => {
const keyword = e.target.value; const keyword = e.target.value;
debounceSearch(keyword);
},
[data, setFilterData],
);
const debounceSearch = useCallback(
debounce((keyword) => {
setSearchValue(keyword);
const { tree, expandedKeys } = getFilterData( const { tree, expandedKeys } = getFilterData(
keyword.toLocaleLowerCase(), keyword.toLocaleLowerCase(),
data, data,
); );
setFilterData(tree); setFilterData(tree);
setExpandedKeys(expandedKeys); setExpandedKeys(expandedKeys);
}, }, 300),
[data, setFilterData], [data, setFilterData],
); );
const deleteFile = () => {
Modal.confirm({
title: `确认删除`,
content: (
<>
<Text style={{ wordBreak: 'break-all' }} type="warning">
{select}
</Text>
{currentNode.type === 'directory' ? '夹下所以日志' : ''}
</>
),
onOk() {
request
.delete(`${config.apiPrefix}logs`, {
data: {
filename: currentNode.title,
path: currentNode.parent || '',
type: currentNode.type,
},
})
.then(({ code }) => {
if (code === 200) {
message.success(`删除成功`);
let newData = [...data];
if (currentNode.parent) {
newData = depthFirstSearch(
newData,
(c) => c.key === currentNode.key,
);
} else {
const index = newData.findIndex(
(x) => x.key === currentNode.key,
);
if (index !== -1) {
newData.splice(index, 1);
}
}
setData(newData);
initState();
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const initState = () => {
setSelect('');
setCurrentNode(null);
setValue('请选择脚本文件');
};
useEffect(() => {
const word = searchValue || '';
const { tree } = getFilterData(word.toLocaleLowerCase(), data);
setFilterData(tree);
}, [data]);
useEffect(() => { useEffect(() => {
getLogs(); getLogs();
if (treeDom && treeDom.current) { if (treeDom && treeDom.current) {
@@ -103,10 +203,11 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper log-wrapper" className="ql-container-wrapper log-wrapper"
title={title} title={select}
loading={loading} loading={loading}
extra={ extra={
isPhone && [ isPhone
? [
<TreeSelect <TreeSelect
className="log-select" className="log-select"
value={select} value={select}
@@ -118,6 +219,16 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
onSelect={onSelect} onSelect={onSelect}
/>, />,
] ]
: [
<Tooltip title="删除">
<Button
type="primary"
disabled={!select}
onClick={deleteFile}
icon={<DeleteOutlined />}
/>
</Tooltip>,
]
} }
header={{ header={{
style: headerStyle, style: headerStyle,
@@ -137,6 +248,7 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
></Input.Search> ></Input.Search>
<div className={styles['left-tree-scroller']} ref={treeDom}> <div className={styles['left-tree-scroller']} ref={treeDom}>
<Tree <Tree
expandAction="click"
className={styles['left-tree']} className={styles['left-tree']}
treeData={filterData} treeData={filterData}
showIcon={true} showIcon={true}
+20 -25
View File
@@ -9,16 +9,18 @@ import {
Statistic, Statistic,
} from 'antd'; } from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { history, Link } from 'umi'; import { history, useOutletContext } from '@umijs/max';
import styles from './index.less'; import styles from './index.less';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import { useTheme } from '@/utils/hooks'; import { useTheme } from '@/utils/hooks';
import { MobileOutlined } from '@ant-design/icons'; import { MobileOutlined } from '@ant-design/icons';
import { SharedContext } from '@/layouts';
const FormItem = Form.Item; const FormItem = Form.Item;
const { Countdown } = Statistic; const { Countdown } = Statistic;
const Login = ({ reloadUser }: any) => { const Login = () => {
const { reloadUser } = useOutletContext<SharedContext>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [waitTime, setWaitTime] = useState<any>(); const [waitTime, setWaitTime] = useState<any>();
const { theme } = useTheme(); const { theme } = useTheme();
@@ -38,15 +40,7 @@ const Login = ({ reloadUser }: any) => {
}, },
}) })
.then((data) => { .then((data) => {
if (data.code === 420) { checkResponse(data, values);
setLoginInfo({
username: values.username,
password: values.password,
});
setTwoFactor(true);
} else {
checkResponse(data);
}
setLoading(false); setLoading(false);
}) })
.catch(function (error) { .catch(function (error) {
@@ -62,11 +56,7 @@ const Login = ({ reloadUser }: any) => {
data: { ...loginInfo, code: values.code }, data: { ...loginInfo, code: values.code },
}) })
.then((data: any) => { .then((data: any) => {
if (data.code === 430) {
message.error(data.message);
} else {
checkResponse(data); checkResponse(data);
}
setVerifying(false); setVerifying(false);
}) })
.catch((error: any) => { .catch((error: any) => {
@@ -75,8 +65,11 @@ const Login = ({ reloadUser }: any) => {
}); });
}; };
const checkResponse = (data: any) => { const checkResponse = (
if (data.code === 200) { { code, data, message: _message }: any,
values?: any,
) => {
if (code === 200) {
const { const {
token, token,
lastip, lastip,
@@ -84,7 +77,7 @@ const Login = ({ reloadUser }: any) => {
lastlogon, lastlogon,
retries = 0, retries = 0,
platform, platform,
} = data.data; } = data;
localStorage.setItem(config.authKey, token); localStorage.setItem(config.authKey, token);
notification.success({ notification.success({
message: '登录成功!', message: '登录成功!',
@@ -103,13 +96,14 @@ const Login = ({ reloadUser }: any) => {
}); });
reloadUser(true); reloadUser(true);
history.push('/crontab'); history.push('/crontab');
} else if (data.code === 100) { } else if (code === 410) {
message.warn(data.message); setWaitTime(data);
} else if (data.code === 410) { } else if (code === 420) {
message.error(data.message); setLoginInfo({
setWaitTime(data.data); username: values.username,
} else { password: values.password,
message.error(data.message); });
setTwoFactor(true);
} }
}; };
@@ -154,6 +148,7 @@ const Login = ({ reloadUser }: any) => {
message: '验证码为6位数字', message: '验证码为6位数字',
}, },
]} ]}
validateTrigger="onBlur"
> >
<Input <Input
placeholder="6位数字" placeholder="6位数字"
+11 -5
View File
@@ -67,8 +67,10 @@ const EditModal = ({
const getDetail = (node: any) => { const getDetail = (node: any) => {
request request
.get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`) .get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`)
.then((data) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}); });
}; };
@@ -83,8 +85,10 @@ const EditModal = ({
content, content,
}, },
}) })
.then((data) => { .then(({ code, data }) => {
if (code === 200) {
setIsRunning(true); setIsRunning(true);
}
}); });
}; };
@@ -101,8 +105,10 @@ const EditModal = ({
content, content,
}, },
}) })
.then((data) => { .then(({ code, data }) => {
if (code === 200) {
setIsRunning(false); setIsRunning(false);
}
}); });
}; };
@@ -214,7 +220,7 @@ const EditModal = ({
width={'100%'} width={'100%'}
headerStyle={{ padding: '11px 24px' }} headerStyle={{ padding: '11px 24px' }}
onClose={cancel} onClose={cancel}
visible={visible} open={visible}
> >
<SplitPane <SplitPane
split="vertical" split="vertical"
+31 -14
View File
@@ -32,32 +32,31 @@ const EditScriptNameModal = ({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [dirs, setDirs] = useState<any[]>([]); const [dirs, setDirs] = useState<any[]>([]);
const [file, setFile] = useState<File>(); const [file, setFile] = useState<File>();
const [type, setType] = useState<'blank' | 'upload'>('blank'); const [type, setType] = useState<'blank' | 'upload' | 'directory'>('blank');
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
values.path = values.path || ''; const { path = '', filename: inputFilename, directory = '' } = values;
const formData = new FormData(); const formData = new FormData();
formData.append('file', file as any); formData.append('file', file as any);
formData.append('filename', values.filename); formData.append('filename', inputFilename);
formData.append('path', values.path); formData.append('path', path);
formData.append('content', ''); formData.append('content', '');
formData.append('directory', directory);
request request
.post(`${config.apiPrefix}scripts`, { .post(`${config.apiPrefix}scripts`, {
data: formData, data: formData,
}) })
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
message.success('新建文件成功'); message.success(directory ? '新建文件夹成功' : '新建文件成功');
const key = values.path ? `${values.path}/` : ''; const key = path ? `${path}/` : '';
const filename = file ? file.name : values.filename; const filename = file ? file.name : inputFilename;
handleCancel({ handleCancel({
filename, filename,
path: values.path, path,
key: `${key}${filename}`, key: `${key}${filename}`,
}); });
} else {
message.error(data);
} }
setLoading(false); setLoading(false);
}) })
@@ -99,8 +98,8 @@ const EditScriptNameModal = ({
return ( return (
<Modal <Modal
title="新建脚本" title="新建"
visible={visible} open={visible}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
@@ -126,18 +125,36 @@ const EditScriptNameModal = ({
> >
<Radio.Group onChange={typeChange}> <Radio.Group onChange={typeChange}>
<Radio value="blank"></Radio> <Radio value="blank"></Radio>
<Radio value="upload"></Radio> <Radio value="upload"></Radio>
<Radio value="directory"></Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
{type === 'blank' && ( {type === 'blank' && (
<Form.Item <Form.Item
name="filename" name="filename"
label="文件名" label="文件名"
rules={[{ required: true, message: '请输入文件名' }]} rules={[
{ required: true, message: '请输入文件名' },
{
validator: (_, value) =>
value.includes('/')
? Promise.reject(new Error('文件名不能包含斜杠'))
: Promise.resolve(),
},
]}
> >
<Input placeholder="请输入文件名" /> <Input placeholder="请输入文件名" />
</Form.Item> </Form.Item>
)} )}
{type === 'directory' && (
<Form.Item
name="directory"
label="文件夹名"
rules={[{ required: true, message: '请输入文件夹名' }]}
>
<Input placeholder="请输入文件夹名" />
</Form.Item>
)}
<Form.Item label="父目录" name="path"> <Form.Item label="父目录" name="path">
<TreeSelect <TreeSelect
allowClear allowClear
+1 -2
View File
@@ -6,8 +6,7 @@
overflow: hidden; overflow: hidden;
position: relative; position: relative;
background-color: @component-background; background-color: @component-background;
height: calc(100vh - 128px); height: 100%;
height: calc(100vh - var(--vh-offset, 0px) - 128px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
+62 -54
View File
@@ -33,7 +33,10 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import EditScriptNameModal from './editNameModal'; import EditScriptNameModal from './editNameModal';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { history } from 'umi'; import { history, useOutletContext, useLocation } from '@umijs/max';
import { parse } from 'query-string';
import { depthFirstSearch } from '@/utils';
import { SharedContext } from '@/layouts';
const { Text } = Typography; const { Text } = Typography;
@@ -72,10 +75,11 @@ const LangMap: any = {
'.ts': 'typescript', '.ts': 'typescript',
}; };
const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => { const Script = () => {
const [title, setTitle] = useState('请选择脚本文件'); const { headerStyle, isPhone, theme, socketMessage } =
useOutletContext<SharedContext>();
const [value, setValue] = useState('请选择脚本文件'); const [value, setValue] = useState('请选择脚本文件');
const [select, setSelect] = useState<any>(); const [select, setSelect] = useState<string>('');
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const [filterData, setFilterData] = useState<any[]>([]); const [filterData, setFilterData] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -94,10 +98,12 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}scripts`) .get(`${config.apiPrefix}scripts`)
.then((data) => { .then(({ code, data }) => {
setData(data.data); if (code === 200) {
setFilterData(data.data); setData(data);
setFilterData(data);
initGetScript(); initGetScript();
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -105,13 +111,15 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
const getDetail = (node: any) => { const getDetail = (node: any) => {
request request
.get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`) .get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`)
.then((data) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}); });
}; };
const initGetScript = () => { const initGetScript = () => {
const { p, s } = history.location.query as any; const { p, s } = parse(history.location.search);
if (s) { if (s) {
const vkey = `${p}/${s}`; const vkey = `${p}/${s}`;
const obj = { const obj = {
@@ -127,15 +135,21 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
}; };
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
setSelect(node.key);
setCurrentNode(node);
if (node.key === select || !value) { if (node.key === select || !value) {
return; return;
} }
setValue('加载中...');
if (node.type === 'directory') {
setValue('请选择脚本文件');
return;
}
const newMode = value ? LangMap[value.slice(-3)] : ''; const newMode = value ? LangMap[value.slice(-3)] : '';
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode); setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
setSelect(node.key); setValue('加载中...');
setTitle(node.key);
setCurrentNode(node);
getDetail(node); getDetail(node);
}; };
@@ -226,13 +240,11 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
content, content,
}, },
}) })
.then((_data: any) => { .then(({ code, data }) => {
if (_data.code === 200) { if (code === 200) {
message.success(`保存成功`); message.success(`保存成功`);
setValue(content); setValue(content);
setIsEditing(false); setIsEditing(false);
} else {
message.error(_data);
} }
resolve(null); resolve(null);
}) })
@@ -250,10 +262,11 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
title: `确认删除`, title: `确认删除`,
content: ( content: (
<> <>
<Text style={{ wordBreak: 'break-all' }} type="warning"> <Text style={{ wordBreak: 'break-all' }} type="warning">
{select} {select}
</Text>{' '} </Text>
{currentNode.type === 'directory' ? '夹及其子文件' : ''}
</> </>
), ),
@@ -263,24 +276,18 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
data: { data: {
filename: currentNode.title, filename: currentNode.title,
path: currentNode.parent || '', path: currentNode.parent || '',
type: currentNode.type,
}, },
}) })
.then((_data: any) => { .then(({ code }) => {
if (_data.code === 200) { if (code === 200) {
message.success(`删除成功`); message.success(`删除成功`);
let newData = [...data]; let newData = [...data];
if (currentNode.parent) { if (currentNode.parent) {
const parentNodeIndex = newData.findIndex( newData = depthFirstSearch(
(x) => x.key === currentNode.parent, newData,
(c) => c.key === currentNode.key,
); );
const parentNode = newData[parentNodeIndex];
const index = parentNode.children.findIndex(
(y) => y.key === currentNode.key,
);
if (index !== -1 && parentNodeIndex !== -1) {
parentNode.children.splice(index, 1);
newData.splice(parentNodeIndex, 1, { ...parentNode });
}
} else { } else {
const index = newData.findIndex( const index = newData.findIndex(
(x) => x.key === currentNode.key, (x) => x.key === currentNode.key,
@@ -290,8 +297,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
} }
} }
setData(newData); setData(newData);
} else { initState();
message.error(_data);
} }
}); });
}, },
@@ -313,20 +319,17 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
}, },
) => { ) => {
if (filename) { if (filename) {
const newData = [...data]; let newData = [...data];
const _file = { title: filename, key, parent: path }; const _file = { title: filename, key, parent: path };
if (path) { if (path) {
// TODO: 更新左侧树数据 newData = depthFirstSearch(newData, (c) => c.key === path, _file);
const parentNodeIndex = newData.findIndex((x) => x.key === path); const keys = path.split('/');
if (parentNodeIndex !== -1) { const sKeys: string[] = [];
const parentNode = newData[parentNodeIndex]; keys.reduce((p, c) => {
if (parentNode.children && parentNode.children.length > 0) { sKeys.push(p);
parentNode.children.unshift(_file); return `${p}/${c}`;
} else { });
parentNode.children = [_file]; setExpandedKeys([...expandedKeys, ...sKeys, path]);
}
newData.splice(parentNodeIndex, 1, { ...parentNode });
}
} else { } else {
newData.unshift(_file); newData.unshift(_file);
} }
@@ -344,8 +347,9 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
filename: currentNode.title, filename: currentNode.title,
}, },
}) })
.then((_data: any) => { .then(({ code, data }) => {
const blob = new Blob([_data], { type: 'application/json' }); if (code === 200) {
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
@@ -353,17 +357,20 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
document.documentElement.appendChild(a); document.documentElement.appendChild(a);
a.click(); a.click();
document.documentElement.removeChild(a); document.documentElement.removeChild(a);
}
}); });
}; };
const initState = () => {
setSelect('');
setCurrentNode(null);
setValue('请选择脚本文件');
};
useEffect(() => { useEffect(() => {
const word = searchValue || ''; const word = searchValue || '';
const { tree } = getFilterData(word.toLocaleLowerCase(), data); const { tree } = getFilterData(word.toLocaleLowerCase(), data);
setFilterData(tree); setFilterData(tree);
setSelect('');
setCurrentNode(null);
setTitle('请选择脚本文件');
setValue('请选择脚本文件');
}, [data]); }, [data]);
useEffect(() => { useEffect(() => {
@@ -440,12 +447,13 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper log-wrapper" className="ql-container-wrapper log-wrapper"
title={title} title={select}
loading={loading} loading={loading}
extra={ extra={
isPhone isPhone
? [ ? [
<TreeSelect <TreeSelect
treeExpandAction="click"
className="log-select" className="log-select"
value={select} value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
@@ -520,6 +528,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
></Input.Search> ></Input.Search>
<div className={styles['left-tree-scroller']} ref={treeDom}> <div className={styles['left-tree-scroller']} ref={treeDom}>
<Tree <Tree
expandAction="click"
className={styles['left-tree']} className={styles['left-tree']}
treeData={filterData} treeData={filterData}
showIcon={true} showIcon={true}
@@ -556,7 +565,6 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
readOnly: !isEditing, readOnly: !isEditing,
fontSize: 12, fontSize: 12,
lineNumbersMinChars: 3, lineNumbersMinChars: 3,
folding: false,
glyphMargin: false, glyphMargin: false,
}} }}
onMount={(editor) => { onMount={(editor) => {
+1 -3
View File
@@ -26,8 +26,6 @@ const SaveModal = ({
if (code === 200) { if (code === 200) {
message.success('保存文件成功'); message.success('保存文件成功');
handleCancel(data); handleCancel(data);
} else {
message.error(data);
} }
setLoading(false); setLoading(false);
}); });
@@ -41,7 +39,7 @@ const SaveModal = ({
return ( return (
<Modal <Modal
title="保存文件" title="保存文件"
visible={visible} open={visible}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
+1 -3
View File
@@ -26,8 +26,6 @@ const SettingModal = ({
if (code === 200) { if (code === 200) {
message.success('保存文件成功'); message.success('保存文件成功');
handleCancel(data); handleCancel(data);
} else {
message.error(data);
} }
setLoading(false); setLoading(false);
}); });
@@ -41,7 +39,7 @@ const SettingModal = ({
return ( return (
<Modal <Modal
title="运行设置" title="运行设置"
visible={visible} open={visible}
forceRender forceRender
centered centered
onCancel={() => handleCancel()} onCancel={() => handleCancel()}
+24 -6
View File
@@ -22,16 +22,19 @@ const AppModal = ({
if (app) { if (app) {
payload.id = app.id; payload.id = app.id;
} }
try {
const { code, data } = await request[method](`${config.apiPrefix}apps`, { const { code, data } = await request[method](`${config.apiPrefix}apps`, {
data: payload, data: payload,
}); });
if (code === 200) { if (code === 200) {
message.success(app ? '更新应用成功' : '新建应用成功'); message.success(app ? '更新应用成功' : '新建应用成功');
} else { handleCancel(data);
message.error(data);
} }
setLoading(false); setLoading(false);
handleCancel(data); } catch (error) {
setLoading(false);
}
}; };
useEffect(() => { useEffect(() => {
@@ -41,7 +44,7 @@ const AppModal = ({
return ( return (
<Modal <Modal
title={app ? '编辑应用' : '新建应用'} title={app ? '编辑应用' : '新建应用'}
visible={visible} open={visible}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
@@ -64,7 +67,18 @@ const AppModal = ({
name="form_app_modal" name="form_app_modal"
initialValues={app} initialValues={app}
> >
<Form.Item name="name" label="名称"> <Form.Item
name="name"
label="名称"
rules={[
{
validator: (_, value) =>
['system'].includes(value)
? Promise.reject(new Error('名称不能为保留关键字'))
: Promise.resolve(),
},
]}
>
<Input placeholder="请输入应用名称" /> <Input placeholder="请输入应用名称" />
</Form.Item> </Form.Item>
<Form.Item name="scopes" label="权限" rules={[{ required: true }]}> <Form.Item name="scopes" label="权限" rules={[{ required: true }]}>
@@ -75,7 +89,11 @@ const AppModal = ({
style={{ width: '100%' }} style={{ width: '100%' }}
> >
{config.scopes.map((x) => { {config.scopes.map((x) => {
return <Select.Option value={x.value}>{x.name}</Select.Option>; return (
<Select.Option key={x.value} value={x.value}>
{x.name}
</Select.Option>
);
})} })}
</Select> </Select>
</Form.Item> </Form.Item>
+10 -4
View File
@@ -17,17 +17,14 @@ const CheckUpdate = ({ socketMessage }: any) => {
message.loading('检查更新中...', 0); message.loading('检查更新中...', 0);
request request
.put(`${config.apiPrefix}system/update-check`) .put(`${config.apiPrefix}system/update-check`)
.then((_data: any) => { .then(({ code, data }) => {
message.destroy(); message.destroy();
const { code, data } = _data;
if (code === 200) { if (code === 200) {
if (data.hasNewVersion) { if (data.hasNewVersion) {
showConfirmUpdateModal(data); showConfirmUpdateModal(data);
} else { } else {
showForceUpdateModal(); showForceUpdateModal();
} }
} else {
message.error(data);
} }
}) })
.catch((error: any) => { .catch((error: any) => {
@@ -140,7 +137,12 @@ const CheckUpdate = ({ socketMessage }: any) => {
} }
const newMessage = `${value}${_message}`; const newMessage = `${value}${_message}`;
const updateFailed = newMessage.includes('失败,请检查');
modalRef.current.update({ modalRef.current.update({
maskClosable: updateFailed,
closable: updateFailed,
okButtonProps: { disabled: !updateFailed },
content: ( content: (
<div style={{ height: '60vh', overflowY: 'auto' }}> <div style={{ height: '60vh', overflowY: 'auto' }}>
<pre <pre
@@ -159,6 +161,10 @@ const CheckUpdate = ({ socketMessage }: any) => {
}); });
setValue(newMessage); setValue(newMessage);
if (updateFailed) {
message.error('更新失败,请检查网络及日志或稍后再试');
}
document.getElementById('log-identifier') && document.getElementById('log-identifier') &&
document document
.getElementById('log-identifier')! .getElementById('log-identifier')!
+64 -46
View File
@@ -29,6 +29,8 @@ import LoginLog from './loginLog';
import NotificationSetting from './notification'; import NotificationSetting from './notification';
import CheckUpdate from './checkUpdate'; import CheckUpdate from './checkUpdate';
import About from './about'; import About from './about';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const { Text } = Typography; const { Text } = Typography;
const optionsWithDisabled = [ const optionsWithDisabled = [
@@ -37,14 +39,9 @@ const optionsWithDisabled = [
{ label: '跟随系统', value: 'auto' }, { label: '跟随系统', value: 'auto' },
]; ];
const Setting = ({ const Setting = () => {
headerStyle, const { headerStyle, isPhone, user, reloadUser, reloadTheme, socketMessage } =
isPhone, useOutletContext<SharedContext>();
user,
reloadUser,
reloadTheme,
socketMessage,
}: any) => {
const columns = [ const columns = [
{ {
title: '名称', title: '名称',
@@ -147,8 +144,10 @@ const Setting = ({
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}apps`) .get(`${config.apiPrefix}apps`)
.then((data: any) => { .then(({ code, data }) => {
setDataSource(data.data); if (code === 200) {
setDataSource(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -178,14 +177,12 @@ const Setting = ({
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}apps`, { data: [record.id] }) .delete(`${config.apiPrefix}apps`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('删除成功'); message.success('删除成功');
const result = [...dataSource]; const result = [...dataSource];
result.splice(index, 1); result.splice(index, 1);
setDataSource(result); setDataSource(result);
} else {
message.error(data);
} }
}); });
}, },
@@ -212,12 +209,10 @@ const Setting = ({
onOk() { onOk() {
request request
.put(`${config.apiPrefix}apps/${record.id}/reset-secret`) .put(`${config.apiPrefix}apps/${record.id}/reset-secret`)
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('重置成功'); message.success('重置成功');
handleApp(data.data); handleApp(data);
} else {
message.error(data);
} }
}); });
}, },
@@ -250,8 +245,10 @@ const Setting = ({
const getLoginLog = () => { const getLoginLog = () => {
request request
.get(`${config.apiPrefix}user/login-log`) .get(`${config.apiPrefix}user/login-log`)
.then((data: any) => { .then(({ code, data }) => {
setLoginLogData(data.data); if (code === 200) {
setLoginLogData(data);
}
}) })
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
@@ -274,8 +271,10 @@ const Setting = ({
const getNotification = () => { const getNotification = () => {
request request
.get(`${config.apiPrefix}user/notification`) .get(`${config.apiPrefix}user/notification`)
.then((data: any) => { .then(({ code, data }) => {
setNotificationInfo(data.data); if (code === 200) {
setNotificationInfo(data);
}
}) })
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
@@ -285,9 +284,9 @@ const Setting = ({
const getLogRemoveFrequency = () => { const getLogRemoveFrequency = () => {
request request
.get(`${config.apiPrefix}system/log/remove`) .get(`${config.apiPrefix}system/log/remove`)
.then((data: any) => { .then(({ code, data }) => {
if (data.data.info) { if (code === 200 && data.info) {
const { frequency } = data.data.info; const { frequency } = data.info;
setLogRemoveFrequency(frequency); setLogRemoveFrequency(frequency);
} }
}) })
@@ -302,8 +301,10 @@ const Setting = ({
.put(`${config.apiPrefix}system/log/remove`, { .put(`${config.apiPrefix}system/log/remove`, {
data: { frequency: logRemoveFrequency }, data: { frequency: logRemoveFrequency },
}) })
.then((data: any) => { .then(({ code, data }) => {
if (code === 200) {
message.success('更新成功'); message.success('更新成功');
}
}) })
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
@@ -313,7 +314,7 @@ const Setting = ({
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper" className="ql-container-wrapper ql-container-wrapper-has-tab"
title="系统设置" title="系统设置"
header={{ header={{
style: headerStyle, style: headerStyle,
@@ -333,11 +334,16 @@ const Setting = ({
size="small" size="small"
tabPosition="top" tabPosition="top"
onChange={tabChange} onChange={tabChange}
> items={[
<Tabs.TabPane tab="安全设置" key="security"> {
<SecuritySettings user={user} userChange={reloadUser} /> key: 'security',
</Tabs.TabPane> label: '安全设置',
<Tabs.TabPane tab="应用设置" key="app"> children: <SecuritySettings user={user} userChange={reloadUser} />,
},
{
key: 'app',
label: '应用设置',
children: (
<Table <Table
columns={columns} columns={columns}
pagination={false} pagination={false}
@@ -347,14 +353,22 @@ const Setting = ({
scroll={{ x: 768 }} scroll={{ x: 768 }}
loading={loading} loading={loading}
/> />
</Tabs.TabPane> ),
<Tabs.TabPane tab="通知设置" key="notification"> },
<NotificationSetting data={notificationInfo} /> {
</Tabs.TabPane> key: 'notification',
<Tabs.TabPane tab="登录日志" key="login"> label: '通知设置',
<LoginLog data={loginLogData} /> children: <NotificationSetting data={notificationInfo} />,
</Tabs.TabPane> },
<Tabs.TabPane tab="其他设置" key="other"> {
key: 'login',
label: '登录日志',
children: <LoginLog data={loginLogData} />,
},
{
key: 'other',
label: '其他设置',
children: (
<Form layout="vertical" form={form}> <Form layout="vertical" form={form}>
<Form.Item <Form.Item
label="主题设置" label="主题设置"
@@ -392,11 +406,15 @@ const Setting = ({
<CheckUpdate socketMessage={socketMessage} /> <CheckUpdate socketMessage={socketMessage} />
</Form.Item> </Form.Item>
</Form> </Form>
</Tabs.TabPane> ),
<Tabs.TabPane tab="关于" key="about"> },
<About /> {
</Tabs.TabPane> key: 'about',
</Tabs> label: '关于',
children: <About />,
},
]}
></Tabs>
<AppModal <AppModal
visible={isModalVisible} visible={isModalVisible}
handleCancel={handleCancel} handleCancel={handleCancel}
+23 -7
View File
@@ -12,20 +12,20 @@ const NotificationSetting = ({ data }: any) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const handleOk = (values: any) => { const handleOk = (values: any) => {
if (values.type == 'closed') { const { type } = values;
if (type == 'closed') {
values.type = ''; values.type = '';
} }
request request
.put(`${config.apiPrefix}user/notification`, { .put(`${config.apiPrefix}user/notification`, {
data: { data: {
...values, ...values,
}, },
}) })
.then((_data: any) => { .then(({ code, data }) => {
if (_data && _data.code === 200) { if (code === 200) {
message.success(values.type ? '通知发送成功' : '通知关闭成功'); message.success(values.type ? '通知发送成功' : '通知关闭成功');
} else {
message.error(_data.data);
} }
}) })
.catch((error: any) => { .catch((error: any) => {
@@ -58,19 +58,35 @@ const NotificationSetting = ({ data }: any) => {
> >
<Select onChange={notificationModeChange}> <Select onChange={notificationModeChange}>
{config.notificationModes.map((x) => ( {config.notificationModes.map((x) => (
<Option value={x.value}>{x.label}</Option> <Option key={x.value} value={x.value}>
{x.label}
</Option>
))} ))}
</Select> </Select>
</Form.Item> </Form.Item>
{fields.map((x) => ( {fields.map((x) => (
<Form.Item <Form.Item
key={x.label}
label={x.label} label={x.label}
name={x.label} name={x.label}
extra={x.tip} extra={x.tip}
rules={[{ required: x.required }]} rules={[{ required: x.required }]}
style={{ maxWidth: 400 }} style={{ maxWidth: 400 }}
> >
<Input.TextArea autoSize={true} placeholder={`请输入${x.label}`} /> {x.items ? (
<Select placeholder={x.placeholder || `请选择${x.label}`}>
{x.items.map((y) => (
<Option key={y.value} value={y.value}>
{y.label || y.value}
</Option>
))}
</Select>
) : (
<Input.TextArea
autoSize={true}
placeholder={x.placeholder || `请输入${x.label}`}
/>
)}
</Form.Item> </Form.Item>
))} ))}
<Button type="primary" htmlType="submit"> <Button type="primary" htmlType="submit">
+14 -8
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { Typography, Input, Form, Button, message, Avatar, Upload } from 'antd'; import { Typography, Input, Form, Button, message, Avatar, Upload } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import { history } from 'umi'; import { history } from '@umijs/max';
import QRCode from 'qrcode.react'; import QRCode from 'qrcode.react';
import { PageLoading } from '@ant-design/pro-layout'; import { PageLoading } from '@ant-design/pro-layout';
import { UploadOutlined, UserOutlined } from '@ant-design/icons'; import { UploadOutlined, UserOutlined } from '@ant-design/icons';
@@ -27,9 +27,11 @@ const SecuritySettings = ({ user, userChange }: any) => {
password: values.password, password: values.password,
}, },
}) })
.then((data: any) => { .then(({ code, data }) => {
if (code === 200) {
localStorage.removeItem(config.authKey); localStorage.removeItem(config.authKey);
history.push('/login'); history.push('/login');
}
}) })
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
@@ -48,8 +50,8 @@ const SecuritySettings = ({ user, userChange }: any) => {
const deactiveTowFactor = () => { const deactiveTowFactor = () => {
request request
.put(`${config.apiPrefix}user/two-factor/deactive`) .put(`${config.apiPrefix}user/two-factor/deactive`)
.then((data: any) => { .then(({ code, data }) => {
if (data.data) { if (code === 200 && data) {
setTwoFactorActivated(false); setTwoFactorActivated(false);
userChange(); userChange();
} }
@@ -63,8 +65,9 @@ const SecuritySettings = ({ user, userChange }: any) => {
setLoading(true); setLoading(true);
request request
.put(`${config.apiPrefix}user/two-factor/active`, { data: { code } }) .put(`${config.apiPrefix}user/two-factor/active`, { data: { code } })
.then((data: any) => { .then(({ code, data }) => {
if (data.data) { if (code === 200) {
if (data) {
message.success('激活成功'); message.success('激活成功');
setTwoFactoring(false); setTwoFactoring(false);
setTwoFactorActivated(true); setTwoFactorActivated(true);
@@ -72,6 +75,7 @@ const SecuritySettings = ({ user, userChange }: any) => {
} else { } else {
message.success('验证失败'); message.success('验证失败');
} }
}
}) })
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
@@ -82,8 +86,10 @@ const SecuritySettings = ({ user, userChange }: any) => {
const getTwoFactorInfo = () => { const getTwoFactorInfo = () => {
request request
.get(`${config.apiPrefix}user/two-factor/init`) .get(`${config.apiPrefix}user/two-factor/init`)
.then((data: any) => { .then(({ code, data }) => {
setTwoFactorInfo(data.data); if (code === 200) {
setTwoFactorInfo(data);
}
}) })
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
+18 -20
View File
@@ -30,9 +30,10 @@ import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import SubscriptionModal from './modal'; import SubscriptionModal from './modal';
import { getTableScroll } from '@/utils/index'; import { getTableScroll } from '@/utils/index';
import { history } from 'umi'; import { history, useOutletContext } from '@umijs/max';
import './index.less'; import './index.less';
import SubscriptionLogModal from './logModal'; import SubscriptionLogModal from './logModal';
import { SharedContext } from '@/layouts';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -57,7 +58,10 @@ export enum SubscriptionType {
'file' = '单文件', 'file' = '单文件',
} }
const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => { const Subscription = () => {
const { headerStyle, isPhone, socketMessage } =
useOutletContext<SharedContext>();
const columns: any = [ const columns: any = [
{ {
title: '名称', title: '名称',
@@ -259,8 +263,8 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
onOk() { onOk() {
request request
.put(`${config.apiPrefix}subscriptions/run`, { data: [record.id] }) .put(`${config.apiPrefix}subscriptions/run`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) { if (i !== -1) {
@@ -270,8 +274,6 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -296,8 +298,8 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
onOk() { onOk() {
request request
.put(`${config.apiPrefix}subscriptions/stop`, { data: [record.id] }) .put(`${config.apiPrefix}subscriptions/stop`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) { if (i !== -1) {
@@ -308,8 +310,6 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -323,9 +323,11 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}subscriptions?searchValue=${searchText}`) .get(`${config.apiPrefix}subscriptions?searchValue=${searchText}`)
.then((data: any) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
setCurrentPage(1); setCurrentPage(1);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -355,8 +357,8 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}subscriptions`, { data: [record.id] }) .delete(`${config.apiPrefix}subscriptions`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('删除成功'); message.success('删除成功');
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
@@ -364,8 +366,6 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
result.splice(i, 1); result.splice(i, 1);
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -398,8 +398,8 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
data: [record.id], data: [record.id],
}, },
) )
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const newStatus = record.is_disabled === 1 ? 0 : 1; const newStatus = record.is_disabled === 1 ? 0 : 1;
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
@@ -410,8 +410,6 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
+4 -3
View File
@@ -36,11 +36,12 @@ const SubscriptionLogModal = ({
? logUrl ? logUrl
: `${config.apiPrefix}subscriptions/${subscription.id}/log`, : `${config.apiPrefix}subscriptions/${subscription.id}/log`,
) )
.then((data: any) => { .then(({ code, data }) => {
if ( if (
code === 200 &&
localStorage.getItem('logSubscription') === String(subscription.id) localStorage.getItem('logSubscription') === String(subscription.id)
) { ) {
const log = data.data as string; const log = data as string;
setValue(log || '暂无日志'); setValue(log || '暂无日志');
setExecuting(log && !log.includes('执行结束')); setExecuting(log && !log.includes('执行结束'));
if (log && !log.includes('执行结束')) { if (log && !log.includes('执行结束')) {
@@ -93,7 +94,7 @@ const SubscriptionLogModal = ({
return ( return (
<Modal <Modal
title={titleElement()} title={titleElement()}
visible={visible} open={visible}
centered centered
className="log-modal" className="log-modal"
bodyStyle={{ bodyStyle={{
+16 -34
View File
@@ -39,11 +39,9 @@ const SubscriptionModal = ({
); );
if (code === 200) { if (code === 200) {
message.success(subscription ? '更新订阅成功' : '新建订阅成功'); message.success(subscription ? '更新订阅成功' : '新建订阅成功');
} else { handleCancel(data);
message.error(data);
} }
setLoading(false); setLoading(false);
handleCancel(data);
} catch (error: any) { } catch (error: any) {
setLoading(false); setLoading(false);
} }
@@ -190,8 +188,8 @@ const SubscriptionModal = ({
}; };
const onPaste = useCallback((e: any) => { const onPaste = useCallback((e: any) => {
const text = e.clipboardData.getData('text'); const text = e.clipboardData.getData('text') as string;
if (!subscription && text.includes('ql ')) { if (text.startsWith('ql ')) {
const [ const [
, ,
type, type,
@@ -225,6 +223,13 @@ const SubscriptionModal = ({
} }
}, []); }, []);
const onNamePaste = useCallback((e) => {
const text = e.clipboardData.getData('text') as string;
if (text.startsWith('ql ')) {
e.preventDefault();
}
}, []);
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
window.addEventListener('paste', onPaste); window.addEventListener('paste', onPaste);
@@ -243,36 +248,10 @@ const SubscriptionModal = ({
} }
}, [subscription, visible]); }, [subscription, visible]);
const isFirefox = navigator.userAgent.includes('Firefox');
const isSafari =
navigator.userAgent.includes('Safari') &&
!navigator.userAgent.includes('Chrome');
const isQQBrowser = navigator.userAgent.includes('QQBrowser');
return ( return (
<Modal <Modal
title={ title={subscription ? '编辑订阅' : '新建订阅'}
subscription ? ( open={visible}
'编辑订阅'
) : (
<span>
<small
style={{
color: '#999',
fontWeight: 400,
fontSize: isFirefox ? 9 : 12,
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}}
>
repo/raw命令
</small>
</span>
)
}
visible={visible}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
@@ -291,7 +270,10 @@ const SubscriptionModal = ({
> >
<Form form={form} name="form_in_modal" layout="vertical"> <Form form={form} name="form_in_modal" layout="vertical">
<Form.Item name="name" label="名称"> <Form.Item name="name" label="名称">
<Input placeholder="请输入订阅名" /> <Input
placeholder="支持拷贝ql repo/raw命令,粘贴导入"
onPaste={onNamePaste}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="type" name="type"
+44 -4
View File
@@ -56,7 +56,7 @@ export default {
value: 'scripts', value: 'scripts',
}, },
{ {
name: '任务日志', name: '日志管理',
value: 'logs', value: 'logs',
}, },
{ {
@@ -74,7 +74,7 @@ export default {
subscriptions: '订阅管理', subscriptions: '订阅管理',
configs: '配置文件', configs: '配置文件',
scripts: '脚本管理', scripts: '脚本管理',
logs: '任务日志', logs: '日志管理',
dependencies: '依赖管理', dependencies: '依赖管理',
system: '系统信息', system: '系统信息',
}, },
@@ -90,7 +90,9 @@ export default {
{ value: 'weWorkApp', label: '企业微信应用' }, { value: 'weWorkApp', label: '企业微信应用' },
{ value: 'iGot', label: 'IGot' }, { value: 'iGot', label: 'IGot' },
{ value: 'pushPlus', label: 'PushPlus' }, { value: 'pushPlus', label: 'PushPlus' },
{ value: 'chat', label: '群辉chat' },
{ value: 'email', label: '邮箱' }, { value: 'email', label: '邮箱' },
{ value: 'webhook', label: '自定义通知' },
{ value: 'closed', label: '已关闭' }, { value: 'closed', label: '已关闭' },
], ],
notificationModeMap: { notificationModeMap: {
@@ -103,6 +105,14 @@ export default {
{ label: 'gotifyToken', tip: 'gotify的消息应用token码', required: true }, { label: 'gotifyToken', tip: 'gotify的消息应用token码', required: true },
{ label: 'gotifyPriority', tip: '推送消息的优先级' }, { label: 'gotifyPriority', tip: '推送消息的优先级' },
], ],
chat: [
{
label: 'chatUrl',
tip: 'chat的url地址',
required: true,
},
{ label: 'chatToken', tip: 'chat的token码', required: true },
],
goCqHttpBot: [ goCqHttpBot: [
{ {
label: 'goCqHttpBotUrl', label: 'goCqHttpBotUrl',
@@ -196,7 +206,7 @@ export default {
pushPlus: [ pushPlus: [
{ {
label: 'pushPlusToken', label: 'pushPlusToken',
tip: '微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送', tip: '微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/',
required: true, required: true,
}, },
{ {
@@ -213,6 +223,36 @@ export default {
{ label: 'emailUser', tip: '邮箱地址', required: true }, { label: 'emailUser', tip: '邮箱地址', required: true },
{ label: 'emailPass', tip: '邮箱SMTP授权码', required: true }, { label: 'emailPass', tip: '邮箱SMTP授权码', required: true },
], ],
webhook: [
{
label: 'webhookMethod',
tip: '请求方法',
required: true,
items: [{ value: 'GET' }, { value: 'POST' }, { value: 'PUT' }],
},
{
label: 'webhookContentType',
tip: '请求头Content-Type',
required: true,
items: [{ value: 'application/json' }, { value: 'multipart/form-data' }, { value: 'application/x-www-form-urlencoded' }],
},
{
label: 'webhookUrl',
tip: '请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置',
required: true,
placeholder: 'https://xxx.cn/api?content=$title\n',
},
{
label: 'webhookHeaders',
tip: '请求头格式Custom-Header1: Header1,多个换行分割',
placeholder: 'Custom-Header1: Header1\nCustom-Header2: Header2',
},
{
label: 'webhookBody',
tip: '请求体格式key1: value1,多个换行分割。url或者body中必须包含$title$content可选,对应api内容的位置',
placeholder: 'key1: $title\nkey2: $content',
},
],
}, },
documentTitleMap: { documentTitleMap: {
'/login': '登录', '/login': '登录',
@@ -223,7 +263,7 @@ export default {
'/config': '配置文件', '/config': '配置文件',
'/script': '脚本管理', '/script': '脚本管理',
'/diff': '对比工具', '/diff': '对比工具',
'/log': '任务日志', '/log': '日志管理',
'/setting': '系统设置', '/setting': '系统设置',
'/error': '错误日志', '/error': '错误日志',
}, },
+18 -2
View File
@@ -1,7 +1,7 @@
import { extend } from 'umi-request'; import { extend } from 'umi-request';
import { message } from 'antd'; import { message } from 'antd';
import config from './config'; import config from './config';
import { history } from 'umi'; import { history } from '@umijs/max';
message.config({ message.config({
duration: 1.5, duration: 1.5,
@@ -54,7 +54,23 @@ _request.interceptors.request.use((url, options) => {
}); });
_request.interceptors.response.use(async (response) => { _request.interceptors.response.use(async (response) => {
const res = await response.clone(); const responseStatus = response.status;
if ([502, 504].includes(responseStatus)) {
message.error('服务异常,请稍后刷新!');
history.push('/error');
} else if (responseStatus === 401) {
if (history.location.pathname !== '/login') {
localStorage.removeItem(config.authKey);
history.push('/login');
}
} else {
const res = await response.clone().json();
if (res.code !== 200) {
const msg = res.message || res.data;
msg && message.error(msg);
}
return res;
}
return response; return response;
}); });
+34 -2
View File
@@ -181,8 +181,8 @@ export function getTableScroll({
id, id,
}: { extraHeight?: number; id?: string } = {}) { }: { extraHeight?: number; id?: string } = {}) {
if (typeof extraHeight == 'undefined') { if (typeof extraHeight == 'undefined') {
// 47 + 40 + 10 + 24 // 47 + 40 + 12
extraHeight = 121; extraHeight = 99;
} }
let tHeader = null; let tHeader = null;
if (id) { if (id) {
@@ -241,3 +241,35 @@ export function exportJson(name: string, data: string) {
createA.download = name; createA.download = name;
automaticClick(createA); automaticClick(createA);
} }
export function depthFirstSearch<
T extends Record<string, any> & { children?: T[] },
>(children: T[], condition: (column: T) => boolean, item?: T) {
const c = [...children];
const keys = [];
(function find(cls: T[] | undefined) {
if (!cls) return;
for (let i = 0; i < cls?.length; i++) {
if (condition(cls[i])) {
if (!item) {
cls.splice(i, 1);
return;
}
if (cls[i].children) {
cls[i].children!.unshift(item);
} else {
cls[i].children = [item];
}
return;
}
if (cls[i].children) {
keys.push(cls[i].key);
find(cls[i].children);
}
}
})(c);
return c;
}
+8 -7
View File
@@ -1,8 +1,9 @@
export const version = '2.13.7'; export const version = '2.14.5';
export const changeLogLink = 'https://t.me/jiao_long/322'; export const changeLogLink = 'https://t.me/jiao_long/333';
export const changeLog = `2.13.7 版本说明 export const changeLog = `2.14.5 版本说明
1. typescript脚本引用sendNotify文件 charmingYouYou 1.
2. 2.
3. \n转义 3.
4. repo命令后缀参数改为竖线分割 4.
5. Node依赖提示ERR_PNPM_REGISTRIES_MISMATCH
`; `;
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2017", "target": "es2017",
"lib": ["es2017", "esnext.asynciterable", "DOM"], "lib": ["es2021", "esnext.asynciterable", "DOM"],
"typeRoots": [ "typeRoots": [
"./node_modules/@types", "./node_modules/@types",
"./src/types", "./src/types",