mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-06 00:34:33 +08:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 04037f29e5 | |||
| 6b124bad89 | |||
| 54d99a4954 | |||
| 9947594366 | |||
| d7ff934336 | |||
| dc27856441 | |||
| bd28682769 | |||
| 2060e62da4 | |||
| 70eac1e747 | |||
| 6f5a5fe066 | |||
| 8d24949202 | |||
| f7b6237355 | |||
| daf6f94c51 | |||
| 687d16e21b | |||
| 70571a9d34 | |||
| 4da8d8fe1e | |||
| 952cdc0e3f | |||
| 2de189d189 | |||
| 3edc6e83d7 | |||
| 31a5386cfe | |||
| 0198ebbbff | |||
| ce3df8704d | |||
| 1b4ca3a684 | |||
| e31c2c0955 | |||
| 4e389865b5 | |||
| e5d8adf955 | |||
| 9a3181bc44 | |||
| 2f05c95422 | |||
| bc5a3a2028 | |||
| 1fe91508f6 | |||
| b9253c8191 | |||
| 7516acbc41 | |||
| 3b3d45da29 | |||
| 918d68d140 | |||
| a3f56be299 | |||
| f8f63890e5 | |||
| 9e997410ab | |||
| 529880642f | |||
| d2590edab3 | |||
| 32eec68278 | |||
| 05ed8c9f4b | |||
| bc281ee4d7 | |||
| 3e88314d0a | |||
| 141defd845 | |||
| b9e49b181a | |||
| 102e447f78 | |||
| f3de8435f1 |
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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! 🎉🎉🎉
|
||||
@@ -14,7 +14,7 @@ export default defineConfig({
|
||||
dynamicImport: {
|
||||
loading: '@/components/pageLoading',
|
||||
},
|
||||
favicon: '/images/g5.ico',
|
||||
favicon: '/images/favicon.svg',
|
||||
proxy: {
|
||||
'/api/public': {
|
||||
target: 'http://127.0.0.1:5400/',
|
||||
|
||||
+6
-2
@@ -57,7 +57,7 @@ https://podman.io/getting-started/installation
|
||||
```bash
|
||||
podman run -dit \
|
||||
--network bridge \
|
||||
-v $PWD/ql:/ql/data \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
-p 5700:5700 \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
@@ -96,7 +96,7 @@ systemctl restart docker
|
||||
|
||||
```bash
|
||||
docker run -dit \
|
||||
-v $PWD/ql:/ql/data \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
-p 5700:5700 \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
@@ -124,6 +124,10 @@ docker-compose up -d
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
3. access
|
||||
|
||||
Open your browser and visit http://{ip}:5700
|
||||
|
||||
## Use
|
||||
|
||||
1. built-in commands
|
||||
|
||||
@@ -57,7 +57,7 @@ https://podman.io/getting-started/installation
|
||||
```bash
|
||||
podman run -dit \
|
||||
--network bridge \
|
||||
-v $PWD/ql:/ql/data \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
-p 5700:5700 \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
@@ -97,7 +97,7 @@ systemctl restart docker
|
||||
|
||||
```bash
|
||||
docker run -dit \
|
||||
-v $PWD/ql:/ql/data \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
-p 5700:5700 \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
@@ -125,6 +125,10 @@ docker-compose up -d
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
3. 访问
|
||||
|
||||
打开你的浏览器,访问 http://{ip}:5700
|
||||
|
||||
## 使用
|
||||
|
||||
1. 内置命令
|
||||
|
||||
+126
-1
@@ -2,6 +2,7 @@ import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
import CronService from '../services/cron';
|
||||
import CronViewService from '../services/cronView';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import cron_parser from 'cron-parser';
|
||||
const route = Router();
|
||||
@@ -9,11 +10,135 @@ const route = Router();
|
||||
export default (app: Router) => {
|
||||
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) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
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 });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
@@ -4,6 +4,7 @@ export enum NotificationMode {
|
||||
'serverChan' = 'serverChan',
|
||||
'pushDeer' = 'pushDeer',
|
||||
'bark' = 'bark',
|
||||
'chat' = 'chat',
|
||||
'telegramBot' = 'telegramBot',
|
||||
'dingtalkBot' = 'dingtalkBot',
|
||||
'weWorkBot' = 'weWorkBot',
|
||||
@@ -37,6 +38,11 @@ export class PushDeerNotification extends NotificationBaseInfo {
|
||||
public pushDeerKey = '';
|
||||
}
|
||||
|
||||
export class ChatNotification extends NotificationBaseInfo {
|
||||
public chatUrl = '';
|
||||
public chatToken = '';
|
||||
}
|
||||
|
||||
export class BarkNotification extends NotificationBaseInfo {
|
||||
public barkPush = '';
|
||||
public barkIcon = 'http://qn.whyour.cn/logo.png';
|
||||
@@ -86,6 +92,7 @@ export interface NotificationInfo
|
||||
GotifyNotification,
|
||||
ServerChanNotification,
|
||||
PushDeerNotification,
|
||||
ChatNotification,
|
||||
BarkNotification,
|
||||
TelegramBotNotification,
|
||||
DingtalkBotNotification,
|
||||
|
||||
+2
-1
@@ -6,9 +6,9 @@ import { CrontabModel } from '../data/cron';
|
||||
import { DependenceModel } from '../data/dependence';
|
||||
import { AppModel } from '../data/open';
|
||||
import { AuthModel } from '../data/auth';
|
||||
import { sequelize } from '../data';
|
||||
import { fileExist } from '../config/util';
|
||||
import { SubscriptionModel } from '../data/subscription';
|
||||
import { CrontabViewModel } from '../data/cronView';
|
||||
import config from '../config';
|
||||
|
||||
export default async () => {
|
||||
@@ -19,6 +19,7 @@ export default async () => {
|
||||
await AuthModel.sync();
|
||||
await EnvModel.sync();
|
||||
await SubscriptionModel.sync();
|
||||
await CrontabViewModel.sync();
|
||||
|
||||
// try {
|
||||
// const queryInterface = sequelize.getQueryInterface();
|
||||
|
||||
+19
-7
@@ -3,7 +3,7 @@ import bodyParser from 'body-parser';
|
||||
import cors from 'cors';
|
||||
import routes from '../api';
|
||||
import config from '../config';
|
||||
import jwt from 'express-jwt';
|
||||
import jwt, { UnauthorizedError } from 'express-jwt';
|
||||
import fs from 'fs';
|
||||
import { getPlatform, getToken } from '../config/util';
|
||||
import Container from 'typedi';
|
||||
@@ -75,22 +75,24 @@ export default ({ app }: { app: Application }) => {
|
||||
if (
|
||||
!headerToken &&
|
||||
originPath &&
|
||||
config.apiWhiteList.includes(originPath) &&
|
||||
originPath !== '/api/crons/status'
|
||||
config.apiWhiteList.includes(originPath)
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||
if (data) {
|
||||
if (data && headerToken) {
|
||||
const { token = '', tokens = {} } = JSON.parse(data);
|
||||
if (headerToken === token || tokens[req.platform] === headerToken) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
const err: any = new Error('UnauthorizedError');
|
||||
err.status = 401;
|
||||
const errorCode = headerToken ? 'invalid_token' : 'credentials_required';
|
||||
const errorMessage = headerToken
|
||||
? 'jwt malformed'
|
||||
: 'No authorization token was found';
|
||||
const err = new UnauthorizedError(errorCode, { message: errorMessage });
|
||||
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(
|
||||
(
|
||||
|
||||
+162
-39
@@ -113,9 +113,69 @@ export default class CronService {
|
||||
}
|
||||
}
|
||||
|
||||
public async crontabs(searchText?: string): Promise<Crontab[]> {
|
||||
let query = {};
|
||||
private formatViewQuery(query: any, viewQuery: any) {
|
||||
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 (!query[Op.and]) {
|
||||
query[Op.and] = [];
|
||||
}
|
||||
let q: any = {};
|
||||
const textArray = searchText.split(':');
|
||||
switch (textArray[0]) {
|
||||
case 'name':
|
||||
@@ -123,13 +183,11 @@ export default class CronService {
|
||||
case 'schedule':
|
||||
case 'label':
|
||||
const column = textArray[0] === 'label' ? 'labels' : textArray[0];
|
||||
query = {
|
||||
[column]: {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${textArray[1]}%` },
|
||||
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
|
||||
],
|
||||
},
|
||||
q[column] = {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${textArray[1]}%` },
|
||||
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
|
||||
],
|
||||
};
|
||||
break;
|
||||
default:
|
||||
@@ -139,31 +197,96 @@ export default class CronService {
|
||||
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
|
||||
],
|
||||
};
|
||||
query = {
|
||||
[Op.or]: [
|
||||
{
|
||||
name: reg,
|
||||
},
|
||||
{
|
||||
command: reg,
|
||||
},
|
||||
{
|
||||
schedule: reg,
|
||||
},
|
||||
{
|
||||
labels: reg,
|
||||
},
|
||||
],
|
||||
};
|
||||
q[Op.or] = [
|
||||
{
|
||||
name: reg,
|
||||
},
|
||||
{
|
||||
command: reg,
|
||||
},
|
||||
{
|
||||
schedule: reg,
|
||||
},
|
||||
{
|
||||
labels: reg,
|
||||
},
|
||||
];
|
||||
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 {
|
||||
const result = await CrontabModel.findAll({
|
||||
where: query,
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
return result as any;
|
||||
const result = await CrontabModel.findAll(condition);
|
||||
const count = await CrontabModel.count({ where: query });
|
||||
return { data: result, total: count };
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -279,7 +402,13 @@ export default class CronService {
|
||||
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
|
||||
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`;
|
||||
}
|
||||
|
||||
@@ -302,16 +431,10 @@ export default class CronService {
|
||||
|
||||
cp.on('exit', async (code, signal) => {
|
||||
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) => {
|
||||
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle, pid: undefined },
|
||||
{ where: { id } },
|
||||
@@ -441,7 +564,7 @@ export default class CronService {
|
||||
private async set_crontab(needReloadSchedule: boolean = false) {
|
||||
const tabs = await this.crontabs();
|
||||
var crontab_string = '';
|
||||
tabs.forEach((tab) => {
|
||||
tabs.data.forEach((tab) => {
|
||||
const _schedule = tab.schedule && tab.schedule.split(/ +/);
|
||||
if (tab.isDisabled === 1 || _schedule!.length !== 5) {
|
||||
crontab_string += '# ';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
-2
@@ -17,6 +17,7 @@ export default class NotificationService {
|
||||
['goCqHttpBot', this.goCqHttpBot],
|
||||
['serverChan', this.serverChan],
|
||||
['pushDeer', this.pushDeer],
|
||||
['chat', this.chat],
|
||||
['bark', this.bark],
|
||||
['telegramBot', this.telegramBot],
|
||||
['dingtalkBot', this.dingtalkBot],
|
||||
@@ -135,6 +136,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() {
|
||||
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
|
||||
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
|
||||
@@ -245,7 +260,7 @@ export default class NotificationService {
|
||||
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
|
||||
weWorkAppKey.split(',');
|
||||
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
|
||||
const { access_token } = await got
|
||||
const tokenRes: any = await got
|
||||
.post(url, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
@@ -296,7 +311,7 @@ export default class NotificationService {
|
||||
|
||||
const res: any = await got
|
||||
.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,
|
||||
retry: 0,
|
||||
|
||||
@@ -79,13 +79,12 @@ export default class ScheduleService {
|
||||
|
||||
cp.on('exit', async (code, signal) => {
|
||||
this.logger.info(
|
||||
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
|
||||
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
|
||||
);
|
||||
});
|
||||
|
||||
cp.on('close', async (code) => {
|
||||
const endTime = dayjs();
|
||||
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
|
||||
await callbacks.onEnd?.(
|
||||
cp,
|
||||
endTime,
|
||||
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
FROM node:alpine
|
||||
FROM python:alpine
|
||||
|
||||
ARG QL_MAINTAINER="whyour"
|
||||
LABEL maintainer="${QL_MAINTAINER}"
|
||||
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 \
|
||||
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 \
|
||||
openssl \
|
||||
nginx \
|
||||
python3 \
|
||||
nodejs \
|
||||
jq \
|
||||
openssh \
|
||||
py3-pip \
|
||||
npm \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& apk update \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
|
||||
@@ -9,7 +9,15 @@ make_dir /etc/nginx/conf.d
|
||||
make_dir /run/nginx
|
||||
cp -fv $nginx_conf /etc/nginx/nginx.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
|
||||
|
||||
if [[ $PipMirror ]]; then
|
||||
pip3 config set global.index-url $PipMirror
|
||||
fi
|
||||
if [[ $NpmMirror ]]; then
|
||||
npm config set registry $NpmMirror
|
||||
fi
|
||||
echo
|
||||
|
||||
echo -e "======================2. 安装依赖========================\n"
|
||||
|
||||
+15
-15
@@ -1,12 +1,12 @@
|
||||
upstream api {
|
||||
upstream baseApi {
|
||||
server 0.0.0.0:5600;
|
||||
}
|
||||
|
||||
upstream public {
|
||||
upstream publicApi {
|
||||
server 0.0.0.0:5400;
|
||||
}
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default keep-alive;
|
||||
'websocket' upgrade;
|
||||
}
|
||||
@@ -16,42 +16,42 @@ server {
|
||||
root /ql/static/dist;
|
||||
ssl_session_timeout 5m;
|
||||
|
||||
location /api/public {
|
||||
location QL_BASE_URL/api/public/ {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
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 X-Real-IP $remote_addr;
|
||||
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 Connection $connection_upgrade;
|
||||
}
|
||||
|
||||
location /open {
|
||||
location QL_BASE_URL/open/ {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://api;
|
||||
proxy_pass http://baseApi/open/;
|
||||
}
|
||||
|
||||
gzip on;
|
||||
gzip_static on;
|
||||
gzip_static on;
|
||||
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
|
||||
gzip_proxied any;
|
||||
gzip_proxied any;
|
||||
gzip_vary on;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.0;
|
||||
gzip_http_version 1.0;
|
||||
|
||||
location / {
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
location QL_BASE_URL/ {
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ QL_BASE_URL/index.html;
|
||||
}
|
||||
|
||||
location ~ .*\.(html)$ {
|
||||
|
||||
+19
-19
@@ -5,41 +5,41 @@ error_log /var/log/nginx/error.log warn;
|
||||
include /etc/nginx/modules/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
server_tokens off;
|
||||
server_tokens off;
|
||||
|
||||
client_max_body_size 20m;
|
||||
client_body_buffer_size 20m;
|
||||
client_max_body_size 20m;
|
||||
client_body_buffer_size 20m;
|
||||
|
||||
keepalive_timeout 65;
|
||||
keepalive_timeout 65;
|
||||
|
||||
sendfile on;
|
||||
sendfile on;
|
||||
|
||||
tcp_nodelay on;
|
||||
tcp_nodelay on;
|
||||
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
ssl_session_cache shared:SSL:2m;
|
||||
ssl_session_cache shared:SSL:2m;
|
||||
|
||||
gzip on;
|
||||
gzip_static on;
|
||||
gzip on;
|
||||
gzip_static on;
|
||||
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
|
||||
gzip_proxied any;
|
||||
gzip_proxied any;
|
||||
gzip_vary on;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.0;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
access_log /var/log/nginx/access.log main;
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
|
||||
+4
-3
@@ -36,8 +36,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@otplib/preset-default": "^12.0.1",
|
||||
"@sentry/node": "^7.0.0",
|
||||
"@sentry/tracing": "^7.0.0",
|
||||
"@sentry/node": "^7.12.1",
|
||||
"@sentry/tracing": "^7.12.1",
|
||||
"body-parser": "^1.19.2",
|
||||
"celebrate": "^15.0.1",
|
||||
"chokidar": "^3.5.3",
|
||||
@@ -73,7 +73,7 @@
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
"@ant-design/pro-layout": "^6.33.1",
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@sentry/react": "^7.0.0",
|
||||
"@sentry/react": "^7.12.1",
|
||||
"@types/body-parser": "^1.19.2",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.13",
|
||||
@@ -117,6 +117,7 @@
|
||||
"react-split-pane": "^0.1.92",
|
||||
"sockjs-client": "^1.6.0",
|
||||
"ts-node": "^10.6.0",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.6.2",
|
||||
"umi": "^3.5.21",
|
||||
"umi-request": "^1.4.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
@@ -29,6 +29,14 @@ MaxConcurrentNum="5"
|
||||
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-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
|
||||
EnableExtraShell="true"
|
||||
|
||||
@@ -38,9 +46,12 @@ AutoStartBot=""
|
||||
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
|
||||
BotRepoUrl=""
|
||||
|
||||
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
|
||||
## 安装python依赖时指定pip源,默认使用清华源
|
||||
PipMirror="https://pypi.doubanio.com/simple/"
|
||||
|
||||
## 安装node依赖时指定pip源,默认使用清华源
|
||||
NpmMirror="https://registry.npmmirror.com"
|
||||
|
||||
## 通知环境变量
|
||||
## 1. Server酱
|
||||
## https://sct.ftqq.com
|
||||
@@ -129,4 +140,10 @@ export GOTIFY_PRIORITY=0
|
||||
## deer_key 填写PushDeer的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 变量名= 声明即可
|
||||
|
||||
+60
-5
@@ -39,6 +39,12 @@ let SCKEY = '';
|
||||
//(环境变量名 DEER_KEY)
|
||||
let PUSHDEER_KEY = '';
|
||||
|
||||
// =======================================Synology Chat通知设置区域===========================================
|
||||
//此处填你申请的CHAT_URL与CHAT_TOKEN
|
||||
//(环境变量名 CHAT_URL CHAT_TOKEN)
|
||||
let CHAT_URL = '';
|
||||
let CHAT_TOKEN = '';
|
||||
|
||||
// =======================================Bark App通知设置区域===========================================
|
||||
//此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
|
||||
let BARK_PUSH = '';
|
||||
@@ -133,6 +139,14 @@ if (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) {
|
||||
QQ_SKEY = process.env.QQ_SKEY;
|
||||
}
|
||||
@@ -239,6 +253,8 @@ async function sendNotify(
|
||||
iGotNotify(text, desp, params), //iGot
|
||||
gobotNotify(text, desp), //go-cqhttp
|
||||
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) => {
|
||||
if (PUSHDEER_KEY) {
|
||||
// PushDeer 建议对消息内容进行 urlencode
|
||||
@@ -382,8 +398,9 @@ function PushDeerNotify(text, desp, time = 2100) {
|
||||
},
|
||||
timeout,
|
||||
};
|
||||
setTimeout(() => {
|
||||
$.post(options, (err, resp, data) => {
|
||||
$.post(
|
||||
options,
|
||||
(err, resp, data) => {
|
||||
try {
|
||||
if (err) {
|
||||
console.log('发送通知调用API失败!!\n');
|
||||
@@ -407,8 +424,46 @@ function PushDeerNotify(text, desp, time = 2100) {
|
||||
} finally {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
}, time);
|
||||
},
|
||||
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);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
|
||||
+26
-2
@@ -61,7 +61,10 @@ push_config = {
|
||||
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
|
||||
|
||||
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
|
||||
|
||||
|
||||
'CHAT_URL': '', # synology chat url
|
||||
'CHAT_TOKEN': '', # synology chat token
|
||||
|
||||
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
|
||||
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
|
||||
|
||||
@@ -279,8 +282,27 @@ def pushdeer(title: str, content: str) -> None:
|
||||
print("PushDeer 推送成功!")
|
||||
else:
|
||||
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:
|
||||
"""
|
||||
通过 push+ 推送消息。
|
||||
@@ -527,6 +549,8 @@ if push_config.get("PUSH_KEY"):
|
||||
notify_function.append(serverJ)
|
||||
if push_config.get("DEER_KEY"):
|
||||
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"):
|
||||
notify_function.append(pushplus_bot)
|
||||
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
|
||||
|
||||
@@ -31,6 +31,7 @@ copy_dep() {
|
||||
echo -e "---> 2. 复制nginx配置文件\n"
|
||||
cp -fv $nginx_conf /etc/nginx/nginx.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"
|
||||
}
|
||||
|
||||
@@ -89,6 +90,9 @@ check_pm2() {
|
||||
main() {
|
||||
echo -e "=====> 开始检测"
|
||||
npm i -g pnpm
|
||||
pnpm setup &>/dev/null
|
||||
source ~/.bashrc
|
||||
pnpm install -g &>/dev/null
|
||||
pnpm add -g pm2
|
||||
copy_dep
|
||||
check_ql
|
||||
|
||||
+4
-5
@@ -67,6 +67,7 @@ import_config() {
|
||||
[[ -f $file_config_user ]] && . $file_config_user
|
||||
[[ -f $file_env ]] && . $file_env
|
||||
|
||||
ql_base_url=${QlBaseUrl:-""}
|
||||
command_timeout_time=${CommandTimeoutTime:-"1h"}
|
||||
proxy_url=${ProxyUrl:-""}
|
||||
file_extensions=${RepoFileExtensions:-"js py"}
|
||||
@@ -244,15 +245,13 @@ fix_config() {
|
||||
}
|
||||
|
||||
npm_install_sub() {
|
||||
set_proxy
|
||||
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
|
||||
npm install --production --registry=https://registry.npmmirror.com || npm install --production
|
||||
npm install --production
|
||||
else
|
||||
pnpm install --loglevel error --production --registry=https://registry.npmmirror.com || pnpm install --production --loglevel error
|
||||
pnpm install --loglevel error --production
|
||||
fi
|
||||
unset_proxy
|
||||
}
|
||||
|
||||
npm_install_1() {
|
||||
|
||||
+25
-10
@@ -24,12 +24,29 @@ define_program() {
|
||||
random_delay() {
|
||||
local random_delay_max=$RandomDelay
|
||||
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
|
||||
local current_min=$(date "+%-M")
|
||||
if [[ $current_min -ne 0 ]] && [[ $current_min -ne 30 ]]; then
|
||||
delay_second=$(($(gen_random_num $random_delay_max) + 1))
|
||||
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
|
||||
sleep $delay_second
|
||||
local file_param=$1
|
||||
local file_extensions=${RandomDelayFileExtensions-"js"}
|
||||
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"
|
||||
sleep $delay_second
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -78,10 +95,8 @@ run_nohup() {
|
||||
run_normal() {
|
||||
local file_param=$1
|
||||
define_program "$file_param"
|
||||
if [[ $file_param == *.js ]]; then
|
||||
if [[ $# -eq 1 ]]; then
|
||||
random_delay
|
||||
fi
|
||||
if [[ $# -eq 1 ]]; then
|
||||
random_delay "$file_param"
|
||||
fi
|
||||
|
||||
local time=$(date "+$time_format")
|
||||
@@ -356,7 +371,7 @@ main() {
|
||||
fi
|
||||
|
||||
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
|
||||
1)
|
||||
run_normal "$1"
|
||||
|
||||
+13
-5
@@ -295,6 +295,11 @@ update_qinglong() {
|
||||
}
|
||||
|
||||
patch_version() {
|
||||
# 兼容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
|
||||
@@ -303,13 +308,9 @@ patch_version() {
|
||||
fi
|
||||
|
||||
if ! type ts-node &>/dev/null; then
|
||||
pnpm i -g ts-node typescript tslib
|
||||
pnpm add -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
|
||||
@@ -338,6 +339,13 @@ patch_version() {
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ $PipMirror ]]; then
|
||||
pip3 config set global.index-url $PipMirror
|
||||
fi
|
||||
if [[ $NpmMirror ]]; then
|
||||
npm config set registry $NpmMirror
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
## 对比脚本
|
||||
|
||||
+26
-8
@@ -56,7 +56,13 @@
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
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;
|
||||
|
||||
.ant-pro-grid-content-children {
|
||||
height: calc(100% - 36px);
|
||||
height: 100%;
|
||||
|
||||
> div,
|
||||
.log-container,
|
||||
@@ -264,16 +270,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pro-page-container-children-content {
|
||||
margin: 18px 18px 18px;
|
||||
}
|
||||
|
||||
.ant-pro-basicLayout-content {
|
||||
margin: 18px;
|
||||
margin: 12px;
|
||||
|
||||
.ant-pro-page-container {
|
||||
margin: -18px -18px -18px;
|
||||
margin: -12px;
|
||||
}
|
||||
|
||||
.ant-pro-page-container-warp .ant-page-header {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.ant-pro-page-container-children-content {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pro-global-header {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.ant-menu-item.ant-pro-sider-collapsed-button {
|
||||
@@ -312,6 +326,10 @@
|
||||
background-color: #373739;
|
||||
}
|
||||
|
||||
.ant-layout-sider {
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.06) !important;
|
||||
}
|
||||
|
||||
.ant-pro-sider-logo {
|
||||
padding: 16px 8px !important;
|
||||
|
||||
|
||||
@@ -186,9 +186,8 @@ export default function (props: any) {
|
||||
if (
|
||||
['/login', '/initialization', '/error'].includes(props.location.pathname)
|
||||
) {
|
||||
document.title = `${
|
||||
(config.documentTitleMap as any)[props.location.pathname]
|
||||
} - 控制面板`;
|
||||
document.title = `${(config.documentTitleMap as any)[props.location.pathname]
|
||||
} - 控制面板`;
|
||||
if (
|
||||
systemInfo?.isInitialized &&
|
||||
props.location.pathname === '/initialization'
|
||||
@@ -282,7 +281,7 @@ export default function (props: any) {
|
||||
shape="square"
|
||||
size="small"
|
||||
icon={<UserOutlined />}
|
||||
src={`/api/static/${user.avatar}`}
|
||||
src={user.avatar ? `/api/static/${user.avatar}` : ''}
|
||||
/>
|
||||
<span style={{ marginLeft: 5 }}>{user.username}</span>
|
||||
</span>
|
||||
@@ -304,7 +303,7 @@ export default function (props: any) {
|
||||
shape="square"
|
||||
size="small"
|
||||
icon={<UserOutlined />}
|
||||
src={`/api/static/${user.avatar}`}
|
||||
src={user.avatar ? `/api/static/${user.avatar}` : ''}
|
||||
/>
|
||||
<span style={{ marginLeft: 5 }}>{user.username}</span>
|
||||
</span>
|
||||
|
||||
@@ -108,7 +108,6 @@ const CronDetailModal = ({
|
||||
wordWrap: 'on',
|
||||
}}
|
||||
onMount={(editor, monaco) => {
|
||||
console.log(monaco);
|
||||
editorRef.current = editor;
|
||||
}}
|
||||
/>
|
||||
@@ -154,9 +153,13 @@ const CronDetailModal = ({
|
||||
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
|
||||
}
|
||||
|
||||
let [p, s] = cmd[1].split('/');
|
||||
if (!s) {
|
||||
s = p;
|
||||
let p: string, s: string;
|
||||
let index = cmd[1].lastIndexOf('/');
|
||||
if (index >= 0) {
|
||||
s = cmd[1].slice(index + 1);
|
||||
p = cmd[1].slice(0, index);
|
||||
} else {
|
||||
s = cmd[1];
|
||||
p = '';
|
||||
}
|
||||
setScriptInfo({ parent: p, filename: s });
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.cron {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.crontab-detail {
|
||||
.card-wrapper {
|
||||
height: 80vh;
|
||||
@@ -101,3 +95,84 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+268
-96
@@ -12,6 +12,8 @@ import {
|
||||
Typography,
|
||||
Input,
|
||||
Popover,
|
||||
Tabs,
|
||||
TablePaginationConfig,
|
||||
} from 'antd';
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
@@ -27,6 +29,11 @@ import {
|
||||
PauseCircleOutlined,
|
||||
FieldTimeOutlined,
|
||||
PushpinOutlined,
|
||||
DownOutlined,
|
||||
SettingOutlined,
|
||||
PlusOutlined,
|
||||
UnorderedListOutlined,
|
||||
CheckOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import config from '@/utils/config';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
@@ -39,6 +46,10 @@ import { diffTime } from '@/utils/date';
|
||||
import { getTableScroll } from '@/utils/index';
|
||||
import { history } from 'umi';
|
||||
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';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
const { Search } = Input;
|
||||
@@ -82,10 +93,11 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
<>
|
||||
<a
|
||||
onClick={() => {
|
||||
goToScriptManager(record);
|
||||
setDetailCron(record);
|
||||
setIsDetailModalVisible(true);
|
||||
}}
|
||||
>
|
||||
{record.labels?.length > 0 && record.labels[0] !== '' ? (
|
||||
{record.labels?.length > 0 && record.labels[0] !== '' && false ? (
|
||||
<Popover
|
||||
placement="right"
|
||||
trigger={isPhone ? 'click' : 'hover'}
|
||||
@@ -98,6 +110,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSearchValue(`label:${label}`);
|
||||
setSearchText(`label:${label}`);
|
||||
}}
|
||||
>
|
||||
<a>{label}</a>
|
||||
@@ -122,15 +135,14 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
</>
|
||||
),
|
||||
sorter: {
|
||||
compare: (a: any, b: any) => a.name.localeCompare(b.name),
|
||||
multiple: 2,
|
||||
compare: (a: any, b: any) => a?.name?.localeCompare(b?.name),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '命令',
|
||||
dataIndex: 'command',
|
||||
key: 'command',
|
||||
width: 250,
|
||||
width: 300,
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any) => {
|
||||
return (
|
||||
@@ -142,13 +154,18 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
}}
|
||||
ellipsis={{ tooltip: text, rows: 2 }}
|
||||
>
|
||||
{text}
|
||||
<a
|
||||
onClick={() => {
|
||||
goToScriptManager(record);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
</Paragraph>
|
||||
);
|
||||
},
|
||||
sorter: {
|
||||
compare: (a: any, b: any) => a.command.localeCompare(b.command),
|
||||
multiple: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -159,12 +176,13 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
align: 'center' as const,
|
||||
sorter: {
|
||||
compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule),
|
||||
multiple: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '最后运行时间',
|
||||
align: 'center' as const,
|
||||
dataIndex: 'last_execution_time',
|
||||
key: 'last_execution_time',
|
||||
width: 150,
|
||||
sorter: {
|
||||
compare: (a: any, b: any) => {
|
||||
@@ -181,10 +199,10 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
>
|
||||
{record.last_execution_time
|
||||
? new Date(record.last_execution_time * 1000)
|
||||
.toLocaleString(language, {
|
||||
hour12: false,
|
||||
})
|
||||
.replace(' 24:', ' 00:')
|
||||
.toLocaleString(language, {
|
||||
hour12: false,
|
||||
})
|
||||
.replace(' 24:', ' 00:')
|
||||
: '-'}
|
||||
</span>
|
||||
);
|
||||
@@ -194,6 +212,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
title: '最后运行时长',
|
||||
align: 'center' as const,
|
||||
width: 120,
|
||||
dataIndex: 'last_running_time',
|
||||
key: 'last_running_time',
|
||||
sorter: {
|
||||
compare: (a: any, b: any) => {
|
||||
return a.last_running_time - b.last_running_time;
|
||||
@@ -346,12 +366,25 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
||||
const [logCron, setLogCron] = useState<any>();
|
||||
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageConf, setPageConf] = useState<{
|
||||
page: number;
|
||||
size: number;
|
||||
sorter: any;
|
||||
filters: any;
|
||||
}>({} as any);
|
||||
const [viewConf, setViewConf] = useState<any>();
|
||||
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
|
||||
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
|
||||
const [detailCron, setDetailCron] = useState<any>();
|
||||
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 cmd = record.command.split(' ') as string[];
|
||||
@@ -360,9 +393,13 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
|
||||
}
|
||||
|
||||
let [p, s] = cmd[1].split('/');
|
||||
if (!s) {
|
||||
s = p;
|
||||
let p: string, s: string;
|
||||
let index = cmd[1].lastIndexOf('/');
|
||||
if (index >= 0) {
|
||||
s = cmd[1].slice(index + 1);
|
||||
p = cmd[1].slice(0, index);
|
||||
} else {
|
||||
s = cmd[1];
|
||||
p = '';
|
||||
}
|
||||
history.push(`/script?p=${p}&s=${s}`);
|
||||
@@ -373,37 +410,33 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
|
||||
const getCrons = () => {
|
||||
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
|
||||
.get(`${config.apiPrefix}crons?searchValue=${searchText}`)
|
||||
.then((data: any) => {
|
||||
.get(url)
|
||||
.then((_data: any) => {
|
||||
const { data, total } = _data.data;
|
||||
setValue(
|
||||
data.data
|
||||
.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 {
|
||||
...x,
|
||||
nextRunTime: cron_parser
|
||||
.parseExpression(x.schedule)
|
||||
.next()
|
||||
.toDate(),
|
||||
};
|
||||
}),
|
||||
data.map((x) => {
|
||||
return {
|
||||
...x,
|
||||
nextRunTime: cron_parser
|
||||
.parseExpression(x.schedule)
|
||||
.next()
|
||||
.toDate(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
setCurrentPage(1);
|
||||
setTotal(total);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
@@ -544,8 +577,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
onOk() {
|
||||
request
|
||||
.put(
|
||||
`${config.apiPrefix}crons/${
|
||||
record.isDisabled === 1 ? 'enable' : 'disable'
|
||||
`${config.apiPrefix}crons/${record.isDisabled === 1 ? 'enable' : 'disable'
|
||||
}`,
|
||||
{
|
||||
data: [record.id],
|
||||
@@ -590,8 +622,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
onOk() {
|
||||
request
|
||||
.put(
|
||||
`${config.apiPrefix}crons/${
|
||||
record.isPinned === 1 ? 'unpin' : 'pin'
|
||||
`${config.apiPrefix}crons/${record.isPinned === 1 ? 'unpin' : 'pin'
|
||||
}`,
|
||||
{
|
||||
data: [record.id],
|
||||
@@ -743,11 +774,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
const rowSelection = {
|
||||
selectedRowIds,
|
||||
onChange: onSelectChange,
|
||||
selections: [
|
||||
Table.SELECTION_ALL,
|
||||
Table.SELECTION_INVERT,
|
||||
Table.SELECTION_NONE,
|
||||
],
|
||||
};
|
||||
|
||||
const delCrons = () => {
|
||||
@@ -796,10 +822,14 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
});
|
||||
};
|
||||
|
||||
const onPageChange = (page: number, pageSize: number | undefined) => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize as number);
|
||||
localStorage.setItem('pageSize', pageSize + '');
|
||||
const onPageChange = (
|
||||
pagination: TablePaginationConfig,
|
||||
filters: Record<string, FilterValue | null>,
|
||||
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) => {
|
||||
@@ -814,39 +844,37 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
}, [logCron]);
|
||||
|
||||
useEffect(() => {
|
||||
getCrons();
|
||||
setPageConf({ ...pageConf, page: 1 });
|
||||
}, [searchText]);
|
||||
|
||||
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(() => {
|
||||
setTableScrollHeight(getTableScroll());
|
||||
});
|
||||
getCronViews();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<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,
|
||||
}}
|
||||
>
|
||||
const panelContent = (
|
||||
<>
|
||||
{selectedRowIds.length > 0 && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Button type="primary" style={{ marginBottom: 5 }} onClick={delCrons}>
|
||||
@@ -906,23 +934,16 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
<Table
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
onChange: onPageChange,
|
||||
pageSize: pageSize,
|
||||
current: pageConf.page,
|
||||
pageSize: pageConf.size,
|
||||
showSizeChanger: true,
|
||||
simple: isPhone,
|
||||
defaultPageSize: 20,
|
||||
total,
|
||||
showTotal: (total: number, range: number[]) =>
|
||||
`第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,
|
||||
pageSizeOptions: [20, 100, 500, 1000] as any,
|
||||
}}
|
||||
onRow={(record) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
setDetailCron(record);
|
||||
setIsDetailModalVisible(true);
|
||||
},
|
||||
};
|
||||
pageSizeOptions: [10, 20, 50, 100, 200, 500, total || 10000].sort(
|
||||
(a, b) => a - b,
|
||||
),
|
||||
}}
|
||||
dataSource={value}
|
||||
rowKey="id"
|
||||
@@ -931,7 +952,135 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
loading={loading}
|
||||
rowSelection={rowSelection}
|
||||
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((data: any) => {
|
||||
setCronViews(data.data);
|
||||
setEnabledCronViews(data.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}
|
||||
>
|
||||
<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
|
||||
visible={isLogModalVisible}
|
||||
handleCancel={() => {
|
||||
@@ -964,6 +1113,29 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
theme={theme}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
message.error(data);
|
||||
}
|
||||
setLoading(false);
|
||||
handleCancel(data);
|
||||
} 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 ? '编辑视图' : '新建视图'}
|
||||
visible={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;
|
||||
@@ -0,0 +1,272 @@
|
||||
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((data: any) => {
|
||||
if (data.code === 200) {
|
||||
message.success('删除成功');
|
||||
cronViewChange();
|
||||
} else {
|
||||
message.error(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
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((data: any) => {
|
||||
if (data.code === 200) {
|
||||
const _list = [...list];
|
||||
_list.splice(index, 1, { ...list[index], isDisabled: !checked });
|
||||
setList(_list);
|
||||
cronViewChange();
|
||||
} else {
|
||||
message.error(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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((data: any) => {
|
||||
if (data.code === 200) {
|
||||
const newData = [...list];
|
||||
newData.splice(dragIndex, 1);
|
||||
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
|
||||
setList(newData);
|
||||
cronViewChange();
|
||||
} else {
|
||||
message.error(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
[list],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setList(cronViews);
|
||||
}, [cronViews]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="视图管理"
|
||||
visible={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;
|
||||
@@ -444,7 +444,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
className="ql-container-wrapper dependence-wrapper"
|
||||
className="ql-container-wrapper dependence-wrapper ql-container-wrapper-has-tab"
|
||||
title="依赖管理"
|
||||
extra={[
|
||||
<Search
|
||||
|
||||
@@ -154,6 +154,7 @@ const Login = ({ reloadUser }: any) => {
|
||||
message: '验证码为6位数字',
|
||||
},
|
||||
]}
|
||||
validateTrigger="onBlur"
|
||||
>
|
||||
<Input
|
||||
placeholder="6位数字"
|
||||
|
||||
@@ -313,7 +313,7 @@ const Setting = ({
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
className="ql-container-wrapper"
|
||||
className="ql-container-wrapper ql-container-wrapper-has-tab"
|
||||
title="系统设置"
|
||||
header={{
|
||||
style: headerStyle,
|
||||
|
||||
@@ -90,6 +90,7 @@ export default {
|
||||
{ value: 'weWorkApp', label: '企业微信应用' },
|
||||
{ value: 'iGot', label: 'IGot' },
|
||||
{ value: 'pushPlus', label: 'PushPlus' },
|
||||
{ value: 'chat', label: '群辉chat' },
|
||||
{ value: 'email', label: '邮箱' },
|
||||
{ value: 'closed', label: '已关闭' },
|
||||
],
|
||||
@@ -103,6 +104,14 @@ export default {
|
||||
{ label: 'gotifyToken', tip: 'gotify的消息应用token码', required: true },
|
||||
{ label: 'gotifyPriority', tip: '推送消息的优先级' },
|
||||
],
|
||||
chat: [
|
||||
{
|
||||
label: 'chatUrl',
|
||||
tip: 'chat的url地址',
|
||||
required: true,
|
||||
},
|
||||
{ label: 'chatToken', tip: 'chat的token码', required: true },
|
||||
],
|
||||
goCqHttpBot: [
|
||||
{
|
||||
label: 'goCqHttpBotUrl',
|
||||
|
||||
+2
-2
@@ -181,8 +181,8 @@ export function getTableScroll({
|
||||
id,
|
||||
}: { extraHeight?: number; id?: string } = {}) {
|
||||
if (typeof extraHeight == 'undefined') {
|
||||
// 47 + 40 + 10 + 24
|
||||
extraHeight = 121;
|
||||
// 47 + 40 + 12
|
||||
extraHeight = 99;
|
||||
}
|
||||
let tHeader = null;
|
||||
if (id) {
|
||||
|
||||
+5
-4
@@ -1,5 +1,6 @@
|
||||
export const version = '2.13.8';
|
||||
export const changeLogLink = 'https://t.me/jiao_long/323';
|
||||
export const changeLog = `2.13.8 版本说明
|
||||
1. 修改系统token访问逻辑,加快任务启动速度
|
||||
export const version = '2.14.3';
|
||||
export const changeLogLink = 'https://t.me/jiao_long/330';
|
||||
export const changeLog = `2.14.8 版本说明
|
||||
1. 修复安装依赖可能出现的 ERR_PNPM_REGISTRIES_MISMATCH
|
||||
2. 今人不见古时月,今月曾经照古人 -> 中秋快乐
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user