Compare commits

..

22 Commits

Author SHA1 Message Date
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
59 changed files with 753 additions and 384 deletions
+11 -16
View File
@@ -1,20 +1,15 @@
import { defineConfig } from 'umi';
import { defineConfig } from '@umijs/max';
const CompressionPlugin = require('compression-webpack-plugin');
export default defineConfig({
hash: true,
layout: false,
antd: {},
outputPath: 'static/dist',
nodeModulesTransform: {
type: 'none',
fastRefresh: true,
favicons: ['/images/favicon.svg'],
mfsu: {
strategy: 'eager',
},
fastRefresh: {},
esbuild: {},
webpack5: {},
dynamicImport: {
loading: '@/components/pageLoading',
},
favicon: '/images/favicon.svg',
proxy: {
'/api/public': {
target: 'http://127.0.0.1:5400/',
@@ -26,7 +21,7 @@ export default defineConfig({
ws: true,
},
},
chainWebpack: (config) => {
chainWebpack: ((config: any) => {
config.plugin('compression-webpack-plugin').use(
new CompressionPlugin({
algorithm: 'gzip',
@@ -35,13 +30,13 @@ export default defineConfig({
minRatio: 0.6,
}),
);
},
}) as any,
externals: {
react: 'window.React',
'react-dom': 'window.ReactDOM',
},
scripts: [
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
headScripts: [
'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
],
});
+18 -8
View File
@@ -77,12 +77,14 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path, content, originFilename } = req.body as {
filename: string;
path: string;
content: string;
originFilename: string;
};
let { filename, path, content, originFilename, directory } =
req.body as {
filename: string;
path: string;
content: string;
originFilename: string;
directory: string;
};
if (!path) {
path = config.scriptPath;
@@ -105,11 +107,19 @@ export default (app: Router) => {
return res.send({ code: 200 });
}
if (directory) {
fs.mkdirSync(join(path, directory), { recursive: true });
return res.send({ code: 200 });
}
if (!originFilename) {
originFilename = filename;
}
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
const filePath = `${path}${filename.replace(/\//g, '')}`;
const originFilePath = join(
path,
`${originFilename.replace(/\//g, '')}`,
);
const filePath = join(path, `${filename.replace(/\//g, '')}`);
if (fs.existsSync(originFilePath)) {
fs.copyFileSync(
originFilePath,
+59
View File
@@ -3,6 +3,7 @@ import * as path from 'path';
import got from 'got';
import iconv from 'iconv-lite';
import { exec } from 'child_process';
import FormData from 'form-data';
export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) {
@@ -355,3 +356,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;
};
+13 -1
View File
@@ -1,3 +1,5 @@
import { IncomingHttpHeaders } from 'http';
export enum NotificationMode {
'gotify' = 'gotify',
'goCqHttpBot' = 'goCqHttpBot',
@@ -12,6 +14,7 @@ export enum NotificationMode {
'iGot' = 'iGot',
'pushPlus' = 'pushPlus',
'email' = 'email',
'webhook' = 'webhook',
}
abstract class NotificationBaseInfo {
@@ -87,6 +90,14 @@ export class EmailNotification extends NotificationBaseInfo {
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
extends GoCqHttpBotNotification,
GotifyNotification,
@@ -100,4 +111,5 @@ export interface NotificationInfo
WeWorkAppNotification,
IGotNotification,
PushPlusNotification,
EmailNotification {}
EmailNotification,
WebhookNotification {}
+1 -1
View File
@@ -13,7 +13,7 @@ export default async () => {
// 生成内置token
let tokenCommand = `ts-node-transpile-only ${config.rootPath}/back/token.ts`;
const tokenFile = `${config.rootPath}/static/build/token.js`;
const tokenFile = `${config.rootPath}static/build/token.js`;
if (await fileExist(tokenFile)) {
tokenCommand = `node ${tokenFile}`;
}
+3 -2
View File
@@ -18,14 +18,15 @@ const run = async () => {
_schedule &&
_schedule.length > 5 &&
task.status !== CrontabStatus.disabled &&
!task.isDisabled
!task.isDisabled &&
task.schedule
) {
schedule.scheduleJob(task.schedule, function () {
let command = task.command as string;
if (!command.includes('task ') && !command.includes('ql ')) {
command = `task ${command}`;
}
exec(command);
exec(`ID=${task.id} ${command}`);
});
}
}
+1 -1
View File
@@ -412,7 +412,7 @@ export default class CronService {
cmdStr = `${cmdStr} now`;
}
const cp = spawn(cmdStr, { shell: '/bin/bash' });
const cp = spawn(`ID=${id} ${cmdStr}`, { shell: '/bin/bash' });
await CrontabModel.update(
{ status: CrontabStatus.running, pid: cp.pid },
+5 -2
View File
@@ -125,8 +125,11 @@ export default class DependenceService {
return docs;
}
private async find(query: any, sort?: any): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({ where: { ...query } });
private async find(query: any, sort: any = []): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({
where: { ...query },
order: [...sort, ['createdAt', 'DESC']],
});
return docs;
}
+5 -1
View File
@@ -112,7 +112,11 @@ export default class EnvService {
};
}
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;
} catch (error) {
throw error;
+52 -4
View File
@@ -6,6 +6,7 @@ import got from 'got';
import nodemailer from 'nodemailer';
import crypto from 'crypto';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import { parseBody, parseHeaders } from '../config/util';
@Service()
export default class NotificationService {
@@ -26,6 +27,7 @@ export default class NotificationService {
['iGot', this.iGot],
['pushPlus', this.pushPlus],
['email', this.email],
['webhook', this.webhook],
]);
private timeout = 10000;
@@ -33,7 +35,7 @@ export default class NotificationService {
private content = '';
private params!: Omit<NotificationInfo, 'type'>;
constructor(@Inject('logger') private logger: winston.Logger) {}
constructor(@Inject('logger') private logger: winston.Logger) { }
public async notify(
title: string,
@@ -180,9 +182,8 @@ export default class NotificationService {
telegramBotUserId,
} = this.params;
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
const url = `https://${
telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
}/bot${telegramBotToken}/sendMessage`;
const url = `https://${telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
}/bot${telegramBotToken}/sendMessage`;
let agent;
if (telegramBotProxyHost && telegramBotProxyPort) {
const options: any = {
@@ -383,4 +384,51 @@ export default class NotificationService {
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),
}
}
}
+4 -1
View File
@@ -62,7 +62,10 @@ export default class SubscriptionService {
try {
const result = await SubscriptionModel.findAll({
where: query,
order: [['createdAt', 'DESC']],
order: [
['is_disabled', 'ASC'],
['createdAt', 'DESC'],
],
});
return result as any;
} catch (error) {
+2
View File
@@ -4,6 +4,8 @@ dir_shell=/ql/shell
. $dir_shell/share.sh
link_shell
export isFirstStartServer=true
echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d
make_dir /run/nginx
+31 -15
View File
@@ -2,16 +2,16 @@
"private": true,
"scripts": {
"start": "concurrently -n w: npm:start:*",
"start:front": "umi dev",
"start:front": "max dev",
"start:back": "nodemon",
"start:public": "ts-node back/public.ts",
"build:front": "umi build",
"build:front": "max build",
"build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.js",
"schedule": "npm run build:back && node static/build/schedule.js",
"public": "npm run build:back && node static/build/public.js",
"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:coverage": "umi-test --coverage"
},
@@ -28,9 +28,26 @@
},
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"react",
"react-dom",
"antd",
"dva",
"postcss",
"webpack",
"eslint",
"stylelint",
"redux",
"@babel/core",
"monaco-editor",
"rc-field-form",
"@types/lodash.merge",
"rollup"
],
"allowedVersions": {
"react": "17",
"react-dom": "17"
"react": "18",
"react-dom": "18",
"dva-core": "2"
}
}
},
@@ -48,6 +65,7 @@
"express": "^4.17.3",
"express-jwt": "^6.1.1",
"express-urlrewrite": "^1.4.0",
"form-data": "^4.0.0",
"got": "^11.8.2",
"hpagent": "^0.1.2",
"iconv-lite": "^0.6.3",
@@ -72,7 +90,7 @@
"devDependencies": {
"@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.33.1",
"@monaco-editor/react": "^4.3.1",
"@monaco-editor/react": "4.2.1",
"@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12",
@@ -86,18 +104,16 @@
"@types/node-schedule": "^1.3.2",
"@types/nodemailer": "^6.4.4",
"@types/qrcode.react": "^1.0.2",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.13",
"@types/react": "^18.0.20",
"@types/react-dom": "^18.0.6",
"@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1",
"@types/uuid": "^8.3.4",
"@umijs/plugin-antd": "^0.15.0",
"@umijs/plugin-esbuild": "^1.4.1",
"@umijs/max": "^4.0.21",
"@umijs/ssr-darkreader": "^4.9.45",
"@umijs/test": "^3.5.21",
"ansi-to-react": "^6.1.6",
"antd": "^4.20.5",
"antd": "^4.23.0",
"antd-img-crop": "^4.2.3",
"codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0",
@@ -107,19 +123,19 @@
"prettier": "^2.5.1",
"qiniu": "^7.4.0",
"qrcode.react": "^1.0.1",
"query-string": "^7.1.1",
"rc-tween-one": "^3.0.6",
"react": "17.x",
"react": "18.x",
"react-codemirror2": "^7.2.1",
"react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0",
"react-dom": "17.x",
"react-dom": "18.x",
"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",
"vh-check": "^2.0.5",
"webpack": "^5.70.0",
+2 -2
View File
@@ -46,10 +46,10 @@ AutoStartBot=""
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
BotRepoUrl=""
## 安装python依赖时指定pip源,默认使用清华源
## 安装python依赖时指定pip源
PipMirror="https://pypi.doubanio.com/simple/"
## 安装node依赖时指定pip源,默认使用清华
## 安装node依赖时指定npm
NpmMirror="https://registry.npmmirror.com"
## 通知环境变量
+3
View File
@@ -90,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
+21 -65
View File
@@ -91,15 +91,14 @@ run_nohup() {
nohup node $file_name &>$log_path &
}
## 正常运行单个脚本,$1:传入参数
run_normal() {
local file_param=$1
handle_log_path() {
define_program "$file_param"
if [[ $# -eq 1 ]]; then
random_delay "$file_param"
local suffix=""
if [[ ! -z $ID ]]; then
suffix="_${ID}"
fi
local time=$(date "+$time_format")
time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
@@ -112,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="${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"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
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_timestamp=$(format_timestamp "$time_format" "$time")
@@ -170,25 +179,7 @@ run_concurrent() {
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
define_program "$file_param"
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"
handle_log_path
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
@@ -240,25 +231,7 @@ run_designated() {
exit 1
fi
define_program "$file_param"
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"
handle_log_path
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
@@ -304,25 +277,8 @@ run_designated() {
## 运行其他命令
run_else() {
local file_param="$1"
define_program "$file_param"
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"
handle_log_path
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
+7 -4
View File
@@ -246,6 +246,8 @@ usage() {
update_qinglong() {
patch_version
export isFirstStartServer=false
local no_restart="$1"
local all_branch=$(git branch -a)
local primary_branch="master"
@@ -295,6 +297,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
@@ -306,10 +313,6 @@ patch_version() {
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
+1
View File
@@ -50,6 +50,7 @@ const EditableTagGroup = ({
{tag}
</Tag>
);
return (
<span key={tag} style={{ display: 'inline-block', marginBottom: 8 }}>
{tagElem}
-4
View File
@@ -326,10 +326,6 @@
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;
+44 -33
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef } from 'react';
import ProLayout, { PageLoading } from '@ant-design/pro-layout';
import * as DarkReader from '@umijs/ssr-darkreader';
import defaultProps from './defaultProps';
import { Link, history } from 'umi';
import { Link, history, Outlet, useLocation } from '@umijs/max';
import {
LogoutOutlined,
MenuFoldOutlined,
@@ -21,7 +21,18 @@ import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react';
import { init } from '../utils/init';
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 { theme, reloadTheme } = useTheme();
const [user, setUser] = useState<any>({});
@@ -73,7 +84,7 @@ export default function (props: any) {
.then(({ code, data }) => {
if (code === 200 && data.username) {
setUser(data);
if (props.location.pathname === '/') {
if (location.pathname === '/') {
history.push('/crontab');
}
} else {
@@ -94,7 +105,7 @@ export default function (props: any) {
if (systemInfo && systemInfo.isInitialized && !user) {
getUser();
}
}, [props.location.pathname]);
}, [location.pathname]);
useEffect(() => {
if (!systemInfo) {
@@ -136,9 +147,11 @@ export default function (props: any) {
}, []);
useEffect(() => {
if (!user) return;
if (!user || !user.username) return;
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) => {
@@ -146,9 +159,9 @@ export default function (props: any) {
const data = JSON.parse(e.data);
if (data.type === 'ping') {
if (data && data.message === 'hanhh') {
console.log('websocket连接成功', e);
console.log('WS connection succeeded !!!');
} else {
console.log('websocket连接失败', e);
console.log('WS connection Failed !!!', e);
}
}
setSocketMessage(data);
@@ -183,29 +196,27 @@ export default function (props: any) {
};
}, []);
if (
['/login', '/initialization', '/error'].includes(props.location.pathname)
) {
document.title = `${(config.documentTitleMap as any)[props.location.pathname]
} - 控制面板`;
if (
systemInfo?.isInitialized &&
props.location.pathname === '/initialization'
) {
if (['/login', '/initialization', '/error'].includes(location.pathname)) {
document.title = `${
(config.documentTitleMap as any)[location.pathname]
} - 控制面板`;
if (systemInfo?.isInitialized && location.pathname === '/initialization') {
history.push('/crontab');
}
if (systemInfo || props.location.pathname === '/error') {
return React.Children.map(props.children, (child) => {
return React.cloneElement(child, {
...ctx,
theme,
user,
reloadUser,
reloadTheme,
ws: ws.current,
});
});
if (systemInfo || location.pathname === '/error') {
return (
<Outlet
context={{
...ctx,
theme,
user,
reloadUser,
reloadTheme,
ws: ws.current,
}}
/>
);
}
}
@@ -226,7 +237,7 @@ export default function (props: any) {
<PageLoading />
) : (
<ProLayout
selectedKeys={[props.location.pathname]}
selectedKeys={[location.pathname]}
loading={loading}
ErrorBoundary={Sentry.ErrorBoundary}
logo={<Image preview={false} src="http://qn.whyour.cn/logo.png" />}
@@ -319,16 +330,16 @@ export default function (props: any) {
)}
{...defaultProps}
>
{React.Children.map(props.children, (child) => {
return React.cloneElement(child, {
<Outlet
context={{
...ctx,
theme,
user,
reloadUser,
reloadTheme,
socketMessage,
});
})}
}}
/>
</ProLayout>
);
}
+4 -1
View File
@@ -11,8 +11,11 @@ import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http';
import Editor from '@monaco-editor/react';
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 [loading, setLoading] = useState(true);
const [title, setTitle] = useState('config.sh');
+2 -2
View File
@@ -382,7 +382,7 @@ const CronDetailModal = ({
{currentCron.labels?.length > 0 &&
currentCron.labels[0] !== '' &&
currentCron.labels?.map((label: string, i: number) => (
<Tag color="blue" style={{ marginRight: 5 }}>
<Tag key={label} color="blue" style={{ marginRight: 5 }}>
{label}
</Tag>
))}
@@ -445,7 +445,7 @@ const CronDetailModal = ({
</div>
}
centered
visible={visible}
open={visible}
forceRender
footer={false}
onCancel={() => handleCancel()}
+41 -12
View File
@@ -44,12 +44,13 @@ import CronDetailModal from './detail';
import cron_parser from 'cron-parser';
import { diffTime } from '@/utils/date';
import { getTableScroll } from '@/utils/index';
import { history } from 'umi';
import { history, useOutletContext } from '@umijs/max';
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 { Search } = Input;
@@ -81,7 +82,8 @@ enum OperationPath {
'unpin',
}
const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const Crontab = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [
{
title: '名称',
@@ -106,6 +108,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
{record.labels?.map((label: string) => (
<Tag
color="blue"
key={label}
style={{ cursor: 'point' }}
onClick={(e) => {
e.stopPropagation();
@@ -199,10 +202,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>
);
@@ -411,9 +414,16 @@ 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)}`;
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' })}`;
url += `&sorter=${JSON.stringify({
field: sorter.field,
type: sorter.order === 'ascend' ? 'ASC' : 'DESC',
})}`;
}
if (viewConf) {
url += `&queryString=${JSON.stringify({
@@ -577,7 +587,8 @@ 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],
@@ -622,7 +633,8 @@ 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],
@@ -828,7 +840,12 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
sorter: SorterResult<any> | SorterResult<any>[],
) => {
const { current, pageSize } = pagination;
setPageConf({ page: current as number, size: pageSize as number, sorter, filters });
setPageConf({
page: current as number,
size: pageSize as number,
sorter,
filters,
});
localStorage.setItem('pageSize', String(pageSize));
};
@@ -865,7 +882,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
page: 1,
size: parseInt(localStorage.getItem('pageSize') || '20'),
sorter: {},
filters: {}
filters: {},
});
setTimeout(() => {
setTableScrollHeight(getTableScroll());
@@ -1071,6 +1088,18 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
</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}
+1 -1
View File
@@ -119,7 +119,7 @@ const CronLogModal = ({
return (
<Modal
title={titleElement()}
visible={visible}
open={visible}
centered
className="log-modal"
bodyStyle={{
+22 -16
View File
@@ -28,13 +28,14 @@ const CronModal = ({
const { code, data } = await request[method](`${config.apiPrefix}crons`, {
data: payload,
});
if (code === 200) {
message.success(cron ? '更新Cron成功' : '新建Cron成功');
handleCancel(data);
} else {
message.error(data);
}
setLoading(false);
handleCancel(data);
} catch (error: any) {
setLoading(false);
}
@@ -47,7 +48,7 @@ const CronModal = ({
return (
<Modal
title={cron ? '编辑任务' : '新建任务'}
visible={visible}
open={visible}
forceRender
centered
maskClosable={false}
@@ -128,21 +129,26 @@ const CronLabelModal = ({
.then(async (values) => {
setLoading(true);
const payload = { ids, labels: values.labels };
const { code, data } = await request[action](
`${config.apiPrefix}crons/labels`,
{
data: payload,
},
);
if (code === 200) {
message.success(
action === 'post' ? '添加Labels成功' : '删除Labels成功',
try {
const { code, data } = await request[action](
`${config.apiPrefix}crons/labels`,
{
data: payload,
},
);
} else {
message.error(data);
if (code === 200) {
message.success(
action === 'post' ? '添加Labels成功' : '删除Labels成功',
);
handleCancel(true);
} else {
message.error(data);
}
setLoading(false);
} catch (error) {
setLoading(false);
}
setLoading(false);
handleCancel(true);
})
.catch((info) => {
console.log('Validate Failed:', info);
@@ -166,7 +172,7 @@ const CronLabelModal = ({
return (
<Modal
title="批量修改标签"
visible={visible}
open={visible}
footer={buttons}
centered
maskClosable={false}
+4 -2
View File
@@ -65,11 +65,13 @@ const ViewCreateModal = ({
data: view ? { ...values, id: view.id } : values,
},
);
if (code !== 200) {
message.error(data);
} else {
handleCancel(data);
}
setLoading(false);
handleCancel(data);
} catch (error: any) {
setLoading(false);
}
@@ -136,7 +138,7 @@ const ViewCreateModal = ({
return (
<Modal
title={view ? '编辑视图' : '新建视图'}
visible={visible}
open={visible}
forceRender
width={580}
centered
+1 -1
View File
@@ -212,7 +212,7 @@ const ViewManageModal = ({
return (
<Modal
title="视图管理"
visible={visible}
open={visible}
centered
width={620}
onCancel={() => handleCancel()}
+54 -12
View File
@@ -29,6 +29,8 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less';
import { getTableScroll } from '@/utils/index';
import DependenceLogModal from './logModal';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const { Text } = Typography;
const { Search } = Input;
@@ -48,7 +50,9 @@ enum StatusColor {
'error',
}
const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
const Dependence = () => {
const { headerStyle, isPhone, socketMessage } =
useOutletContext<SharedContext>();
const columns: any = [
{
title: '序号',
@@ -320,6 +324,30 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
});
};
const handlereInstallDependencies = () => {
Modal.confirm({
title: '确认重新安装',
content: <></>,
onOk() {
request
.put(`${config.apiPrefix}dependencies/reinstall`, {
data: selectedRowIds,
})
.then((data: any) => {
if (data.code === 200) {
setSelectedRowIds([]);
getDependencies();
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const getDependenceDetail = (dependence: any) => {
request
.get(`${config.apiPrefix}dependencies/${dependence.id}`)
@@ -403,6 +431,13 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
<>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => handlereInstallDependencies()}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
@@ -467,17 +502,24 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
size="small"
tabPosition="top"
onChange={onTabChange}
>
<Tabs.TabPane tab="NodeJs" key="nodejs">
{panelContent()}
</Tabs.TabPane>
<Tabs.TabPane tab="Python3" key="python3">
{panelContent()}
</Tabs.TabPane>
<Tabs.TabPane tab="Linux" key="linux">
{panelContent()}
</Tabs.TabPane>
</Tabs>
items={[
{
key: 'nodejs',
label: 'NodeJs',
children: panelContent(),
},
{
key: 'python3',
label: 'Python3',
children: panelContent(),
},
{
key: 'linux',
label: 'Linux',
children: panelContent(),
},
]}
/>
<DependenceModal
visible={isModalVisible}
handleCancel={handleCancel}
+1 -1
View File
@@ -112,7 +112,7 @@ const DependenceLogModal = ({
return (
<Modal
title={titleElement()}
visible={visible}
open={visible}
centered
className="log-modal"
bodyStyle={{
+7 -3
View File
@@ -52,11 +52,13 @@ const DependenceModal = ({
data: payload,
},
);
if (code !== 200) {
message.error(data);
} else {
handleCancel(data);
}
setLoading(false);
handleCancel(data);
} catch (error) {
setLoading(false);
}
@@ -69,7 +71,7 @@ const DependenceModal = ({
return (
<Modal
title={dependence ? '编辑依赖' : '新建依赖'}
visible={visible}
open={visible}
forceRender
centered
maskClosable={false}
@@ -99,7 +101,9 @@ const DependenceModal = ({
>
<Select>
{config.dependenceTypes.map((x, i) => (
<Option value={i}>{x}</Option>
<Option key={i} value={i}>
{x}
</Option>
))}
</Select>
</Form.Item>
+4
View File
@@ -17,4 +17,8 @@
.ant-form-item {
margin-bottom: 8px;
}
+ section {
height: calc(100% - 40px) !important;
}
}
+10 -3
View File
@@ -6,10 +6,13 @@ import { request } from '@/utils/http';
import './index.less';
import { DiffEditor } from '@monaco-editor/react';
import ReactDiffViewer from 'react-diff-viewer';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
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 [current, setCurrent] = useState('config.sh');
const [originValue, setOriginValue] = useState('');
@@ -95,7 +98,9 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
<Form.Item label="源文件">
<Select value={origin} onChange={originFileChange}>
{files.map((x) => (
<Option value={x.value}>{x.title}</Option>
<Option key={x.value} value={x.value}>
{x.title}
</Option>
))}
</Select>
</Form.Item>
@@ -104,7 +109,9 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
<Form.Item label="当前文件">
<Select value={current} onChange={currentFileChange}>
{files.map((x) => (
<Option value={x.value}>{x.title}</Option>
<Option key={x.value} value={x.value}>
{x.title}
</Option>
))}
</Select>
</Form.Item>
+18 -13
View File
@@ -17,19 +17,24 @@ const EditNameModal = ({
const handleOk = async (values: any) => {
setLoading(true);
const { code, data } = await request.put(`${config.apiPrefix}envs/name`, {
data: {
ids,
name: values.name,
},
});
if (code === 200) {
message.success('更新环境变量名称成功');
} else {
message.error(data);
try {
const { code, data } = await request.put(`${config.apiPrefix}envs/name`, {
data: {
ids,
name: values.name,
},
});
if (code === 200) {
message.success('更新环境变量名称成功');
handleCancel();
} else {
message.error(data);
}
setLoading(false);
} catch (error) {
setLoading(false);
}
setLoading(false);
handleCancel();
};
useEffect(() => {
@@ -39,7 +44,7 @@ const EditNameModal = ({
return (
<Modal
title="修改环境变量名称"
visible={visible}
open={visible}
forceRender
centered
maskClosable={false}
+4 -1
View File
@@ -26,6 +26,8 @@ import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less';
import { exportJson, getTableScroll } from '@/utils/index';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const { Text, Paragraph } = Typography;
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 = [
{
title: '序号',
+3 -2
View File
@@ -40,13 +40,14 @@ const EnvModal = ({
const { code, data } = await request[method](`${config.apiPrefix}envs`, {
data: payload,
});
if (code === 200) {
message.success(env ? '更新变量成功' : '新建变量成功');
handleCancel(data);
} else {
message.error(data);
}
setLoading(false);
handleCancel(data);
} catch (error: any) {
setLoading(false);
}
@@ -59,7 +60,7 @@ const EnvModal = ({
return (
<Modal
title={env ? '编辑变量' : '新建变量'}
visible={visible}
open={visible}
forceRender
centered
maskClosable={false}
+4 -2
View File
@@ -3,11 +3,13 @@ import config from '@/utils/config';
import { request } from '@/utils/http';
import Terminal, { ColorMode, LineType } from '../../components/terminal';
import { PageLoading } from '@ant-design/pro-layout';
import { history } from 'umi';
import { history, useOutletContext } from '@umijs/max';
import Ansi from 'ansi-to-react';
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 [data, setData] = useState('暂无日志');
+5 -2
View File
@@ -10,7 +10,7 @@ import {
Select,
} from 'antd';
import config from '@/utils/config';
import { history } from 'umi';
import { history } from '@umijs/max';
import styles from './index.less';
import { request } from '@/utils/http';
@@ -116,12 +116,15 @@ const Initialization = () => {
{config.notificationModes
.filter((x) => x.value !== 'closed')
.map((x) => (
<Option value={x.value}>{x.label}</Option>
<Option key={x.value} value={x.value}>
{x.label}
</Option>
))}
</Select>
</Form.Item>
{fields.map((x) => (
<Form.Item
key={x.label}
label={x.label}
name={x.label}
extra={x.tip}
+1 -2
View File
@@ -6,8 +6,7 @@
overflow: hidden;
position: relative;
background-color: @component-background;
height: calc(100vh - 128px);
height: calc(100vh - var(--vh-offset, 0px) - 128px);
height: 100%;
display: flex;
flex-direction: column;
}
+4 -1
View File
@@ -7,6 +7,8 @@ import { request } from '@/utils/http';
import styles from './index.module.less';
import { Controlled as CodeMirror } from 'react-codemirror2';
import SplitPane from 'react-split-pane';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
function getFilterData(keyword: string, data: any) {
const expandedKeys: string[] = [];
@@ -36,7 +38,8 @@ function getFilterData(keyword: string, data: any) {
return { tree: data, expandedKeys };
}
const Log = ({ headerStyle, isPhone, theme }: any) => {
const Log = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [title, setTitle] = useState('请选择日志文件');
const [value, setValue] = useState('请选择日志文件');
const [select, setSelect] = useState<any>();
+4 -2
View File
@@ -9,16 +9,18 @@ import {
Statistic,
} from 'antd';
import config from '@/utils/config';
import { history, Link } from 'umi';
import { history, useOutletContext } from '@umijs/max';
import styles from './index.less';
import { request } from '@/utils/http';
import { useTheme } from '@/utils/hooks';
import { MobileOutlined } from '@ant-design/icons';
import { SharedContext } from '@/layouts';
const FormItem = Form.Item;
const { Countdown } = Statistic;
const Login = ({ reloadUser }: any) => {
const Login = () => {
const { reloadUser } = useOutletContext<SharedContext>();
const [loading, setLoading] = useState(false);
const [waitTime, setWaitTime] = useState<any>();
const { theme } = useTheme();
+1 -1
View File
@@ -214,7 +214,7 @@ const EditModal = ({
width={'100%'}
headerStyle={{ padding: '11px 24px' }}
onClose={cancel}
visible={visible}
open={visible}
>
<SplitPane
split="vertical"
+31 -12
View File
@@ -32,28 +32,29 @@ const EditScriptNameModal = ({
const [loading, setLoading] = useState(false);
const [dirs, setDirs] = useState<any[]>([]);
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) => {
setLoading(true);
values.path = values.path || '';
const { path = '', filename: inputFilename, directory = '' } = values;
const formData = new FormData();
formData.append('file', file as any);
formData.append('filename', values.filename);
formData.append('path', values.path);
formData.append('filename', inputFilename);
formData.append('path', path);
formData.append('content', '');
formData.append('directory', directory);
request
.post(`${config.apiPrefix}scripts`, {
data: formData,
})
.then(({ code, data }) => {
if (code === 200) {
message.success('新建文件成功');
const key = values.path ? `${values.path}/` : '';
const filename = file ? file.name : values.filename;
message.success(directory ? '新建文件夹成功' : '新建文件成功');
const key = path ? `${path}/` : '';
const filename = file ? file.name : inputFilename;
handleCancel({
filename,
path: values.path,
path,
key: `${key}${filename}`,
});
} else {
@@ -99,8 +100,8 @@ const EditScriptNameModal = ({
return (
<Modal
title="新建脚本"
visible={visible}
title="新建"
open={visible}
forceRender
centered
maskClosable={false}
@@ -126,18 +127,36 @@ const EditScriptNameModal = ({
>
<Radio.Group onChange={typeChange}>
<Radio value="blank"></Radio>
<Radio value="upload"></Radio>
<Radio value="upload"></Radio>
<Radio value="directory"></Radio>
</Radio.Group>
</Form.Item>
{type === 'blank' && (
<Form.Item
name="filename"
label="文件名"
rules={[{ required: true, message: '请输入文件名' }]}
rules={[
{ required: true, message: '请输入文件名' },
{
validator: (_, value) =>
value.includes('/')
? Promise.reject(new Error('文件名不能包含斜杠'))
: Promise.resolve(),
},
]}
>
<Input placeholder="请输入文件名" />
</Form.Item>
)}
{type === 'directory' && (
<Form.Item
name="directory"
label="文件夹名"
rules={[{ required: true, message: '请输入文件夹名' }]}
>
<Input placeholder="请输入文件夹名" />
</Form.Item>
)}
<Form.Item label="父目录" name="path">
<TreeSelect
allowClear
+1 -2
View File
@@ -6,8 +6,7 @@
overflow: hidden;
position: relative;
background-color: @component-background;
height: calc(100vh - 128px);
height: calc(100vh - var(--vh-offset, 0px) - 128px);
height: 100%;
display: flex;
flex-direction: column;
}
+17 -19
View File
@@ -33,7 +33,10 @@ import {
} from '@ant-design/icons';
import EditScriptNameModal from './editNameModal';
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;
@@ -72,7 +75,9 @@ const LangMap: any = {
'.ts': 'typescript',
};
const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
const Script = () => {
const { headerStyle, isPhone, theme, socketMessage } =
useOutletContext<SharedContext>();
const [title, setTitle] = useState('请选择脚本文件');
const [value, setValue] = useState('请选择脚本文件');
const [select, setSelect] = useState<any>();
@@ -111,7 +116,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
};
const initGetScript = () => {
const { p, s } = history.location.query as any;
const { p, s } = parse(history.location.search);
if (s) {
const vkey = `${p}/${s}`;
const obj = {
@@ -313,20 +318,17 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
},
) => {
if (filename) {
const newData = [...data];
let newData = [...data];
const _file = { title: filename, key, parent: path };
if (path) {
// TODO: 更新左侧树数据
const parentNodeIndex = newData.findIndex((x) => x.key === path);
if (parentNodeIndex !== -1) {
const parentNode = newData[parentNodeIndex];
if (parentNode.children && parentNode.children.length > 0) {
parentNode.children.unshift(_file);
} else {
parentNode.children = [_file];
}
newData.splice(parentNodeIndex, 1, { ...parentNode });
}
newData = depthFirstSearch(newData, (c) => c.key === path, _file);
const keys = path.split('/');
const sKeys: string[] = [];
keys.reduce((p, c) => {
sKeys.push(p);
return `${p}/${c}`;
});
setExpandedKeys([...expandedKeys, ...sKeys, path]);
} else {
newData.unshift(_file);
}
@@ -360,10 +362,6 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
const word = searchValue || '';
const { tree } = getFilterData(word.toLocaleLowerCase(), data);
setFilterData(tree);
setSelect('');
setCurrentNode(null);
setTitle('请选择脚本文件');
setValue('请选择脚本文件');
}, [data]);
useEffect(() => {
+1 -1
View File
@@ -41,7 +41,7 @@ const SaveModal = ({
return (
<Modal
title="保存文件"
visible={visible}
open={visible}
forceRender
centered
maskClosable={false}
+1 -1
View File
@@ -41,7 +41,7 @@ const SettingModal = ({
return (
<Modal
title="运行设置"
visible={visible}
open={visible}
forceRender
centered
onCancel={() => handleCancel()}
+32 -12
View File
@@ -22,16 +22,21 @@ const AppModal = ({
if (app) {
payload.id = app.id;
}
const { code, data } = await request[method](`${config.apiPrefix}apps`, {
data: payload,
});
if (code === 200) {
message.success(app ? '更新应用成功' : '新建应用成功');
} else {
message.error(data);
try {
const { code, data } = await request[method](`${config.apiPrefix}apps`, {
data: payload,
});
if (code === 200) {
message.success(app ? '更新应用成功' : '新建应用成功');
handleCancel(data);
} else {
message.error(data);
}
setLoading(false);
} catch (error) {
setLoading(false);
}
setLoading(false);
handleCancel(data);
};
useEffect(() => {
@@ -41,7 +46,7 @@ const AppModal = ({
return (
<Modal
title={app ? '编辑应用' : '新建应用'}
visible={visible}
open={visible}
forceRender
centered
maskClosable={false}
@@ -64,7 +69,18 @@ const AppModal = ({
name="form_app_modal"
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="请输入应用名称" />
</Form.Item>
<Form.Item name="scopes" label="权限" rules={[{ required: true }]}>
@@ -75,7 +91,11 @@ const AppModal = ({
style={{ width: '100%' }}
>
{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>
</Form.Item>
+85 -71
View File
@@ -29,6 +29,8 @@ import LoginLog from './loginLog';
import NotificationSetting from './notification';
import CheckUpdate from './checkUpdate';
import About from './about';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const { Text } = Typography;
const optionsWithDisabled = [
@@ -37,14 +39,9 @@ const optionsWithDisabled = [
{ label: '跟随系统', value: 'auto' },
];
const Setting = ({
headerStyle,
isPhone,
user,
reloadUser,
reloadTheme,
socketMessage,
}: any) => {
const Setting = () => {
const { headerStyle, isPhone, user, reloadUser, reloadTheme, socketMessage } =
useOutletContext<SharedContext>();
const columns = [
{
title: '名称',
@@ -333,70 +330,87 @@ const Setting = ({
size="small"
tabPosition="top"
onChange={tabChange}
>
<Tabs.TabPane tab="安全设置" key="security">
<SecuritySettings user={user} userChange={reloadUser} />
</Tabs.TabPane>
<Tabs.TabPane tab="应用设置" key="app">
<Table
columns={columns}
pagination={false}
dataSource={dataSource}
rowKey="id"
size="middle"
scroll={{ x: 768 }}
loading={loading}
/>
</Tabs.TabPane>
<Tabs.TabPane tab="通知设置" key="notification">
<NotificationSetting data={notificationInfo} />
</Tabs.TabPane>
<Tabs.TabPane tab="登录日志" key="login">
<LoginLog data={loginLogData} />
</Tabs.TabPane>
<Tabs.TabPane tab="其他设置" key="other">
<Form layout="vertical" form={form}>
<Form.Item
label="主题设置"
name="theme"
initialValue={defaultTheme}
>
<Radio.Group
options={optionsWithDisabled}
onChange={themeChange}
value={defaultTheme}
optionType="button"
buttonStyle="solid"
items={[
{
key: 'security',
label: '安全设置',
children: <SecuritySettings user={user} userChange={reloadUser} />,
},
{
key: 'app',
label: '应用设置',
children: (
<Table
columns={columns}
pagination={false}
dataSource={dataSource}
rowKey="id"
size="middle"
scroll={{ x: 768 }}
loading={loading}
/>
</Form.Item>
<Form.Item
label="日志删除频率"
name="frequency"
tooltip="每x天自动删除x天以前的日志"
>
<Input.Group compact>
<InputNumber
addonBefore="每"
addonAfter="天"
style={{ width: 150 }}
min={0}
value={logRemoveFrequency}
onChange={(value) => setLogRemoveFrequency(value)}
/>
<Button type="primary" onClick={updateRemoveLogFrequency}>
</Button>
</Input.Group>
</Form.Item>
<Form.Item label="检查更新" name="update">
<CheckUpdate socketMessage={socketMessage} />
</Form.Item>
</Form>
</Tabs.TabPane>
<Tabs.TabPane tab="关于" key="about">
<About />
</Tabs.TabPane>
</Tabs>
),
},
{
key: 'notification',
label: '通知设置',
children: <NotificationSetting data={notificationInfo} />,
},
{
key: 'login',
label: '登录日志',
children: <LoginLog data={loginLogData} />,
},
{
key: 'other',
label: '其他设置',
children: (
<Form layout="vertical" form={form}>
<Form.Item
label="主题设置"
name="theme"
initialValue={defaultTheme}
>
<Radio.Group
options={optionsWithDisabled}
onChange={themeChange}
value={defaultTheme}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
<Form.Item
label="日志删除频率"
name="frequency"
tooltip="每x天自动删除x天以前的日志"
>
<Input.Group compact>
<InputNumber
addonBefore="每"
addonAfter="天"
style={{ width: 150 }}
min={0}
value={logRemoveFrequency}
onChange={(value) => setLogRemoveFrequency(value)}
/>
<Button type="primary" onClick={updateRemoveLogFrequency}>
</Button>
</Input.Group>
</Form.Item>
<Form.Item label="检查更新" name="update">
<CheckUpdate socketMessage={socketMessage} />
</Form.Item>
</Form>
),
},
{
key: 'about',
label: '关于',
children: <About />,
},
]}
></Tabs>
<AppModal
visible={isModalVisible}
handleCancel={handleCancel}
+24 -4
View File
@@ -12,9 +12,11 @@ const NotificationSetting = ({ data }: any) => {
const [form] = Form.useForm();
const handleOk = (values: any) => {
if (values.type == 'closed') {
const { type } = values;
if (type == 'closed') {
values.type = '';
}
request
.put(`${config.apiPrefix}user/notification`, {
data: {
@@ -25,7 +27,7 @@ const NotificationSetting = ({ data }: any) => {
if (_data && _data.code === 200) {
message.success(values.type ? '通知发送成功' : '通知关闭成功');
} else {
message.error(_data.data);
message.error(_data.message);
}
})
.catch((error: any) => {
@@ -58,19 +60,37 @@ const NotificationSetting = ({ data }: any) => {
>
<Select onChange={notificationModeChange}>
{config.notificationModes.map((x) => (
<Option value={x.value}>{x.label}</Option>
<Option key={x.value} value={x.value}>
{x.label}
</Option>
))}
</Select>
</Form.Item>
{fields.map((x) => (
<Form.Item
key={x.label}
label={x.label}
name={x.label}
extra={x.tip}
rules={[{ required: x.required }]}
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>
))}
<Button type="primary" htmlType="submit">
+1 -1
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { Typography, Input, Form, Button, message, Avatar, Upload } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import { history } from 'umi';
import { history } from '@umijs/max';
import QRCode from 'qrcode.react';
import { PageLoading } from '@ant-design/pro-layout';
import { UploadOutlined, UserOutlined } from '@ant-design/icons';
+6 -2
View File
@@ -30,9 +30,10 @@ import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http';
import SubscriptionModal from './modal';
import { getTableScroll } from '@/utils/index';
import { history } from 'umi';
import { history, useOutletContext } from '@umijs/max';
import './index.less';
import SubscriptionLogModal from './logModal';
import { SharedContext } from '@/layouts';
const { Text, Paragraph } = Typography;
const { Search } = Input;
@@ -57,7 +58,10 @@ export enum SubscriptionType {
'file' = '单文件',
}
const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
const Subscription = () => {
const { headerStyle, isPhone, socketMessage } =
useOutletContext<SharedContext>();
const columns: any = [
{
title: '名称',
+1 -1
View File
@@ -93,7 +93,7 @@ const SubscriptionLogModal = ({
return (
<Modal
title={titleElement()}
visible={visible}
open={visible}
centered
className="log-modal"
bodyStyle={{
+2 -2
View File
@@ -39,11 +39,11 @@ const SubscriptionModal = ({
);
if (code === 200) {
message.success(subscription ? '更新订阅成功' : '新建订阅成功');
handleCancel(data);
} else {
message.error(data);
}
setLoading(false);
handleCancel(data);
} catch (error: any) {
setLoading(false);
}
@@ -272,7 +272,7 @@ const SubscriptionModal = ({
</span>
)
}
visible={visible}
open={visible}
forceRender
centered
maskClosable={false}
+32 -1
View File
@@ -92,6 +92,7 @@ export default {
{ value: 'pushPlus', label: 'PushPlus' },
{ value: 'chat', label: '群辉chat' },
{ value: 'email', label: '邮箱' },
{ value: 'webhook', label: '自定义通知' },
{ value: 'closed', label: '已关闭' },
],
notificationModeMap: {
@@ -205,7 +206,7 @@ export default {
pushPlus: [
{
label: 'pushPlusToken',
tip: '微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送',
tip: '微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/',
required: true,
},
{
@@ -222,6 +223,36 @@ export default {
{ label: 'emailUser', tip: '邮箱地址', 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: {
'/login': '登录',
+1 -1
View File
@@ -1,7 +1,7 @@
import { extend } from 'umi-request';
import { message } from 'antd';
import config from './config';
import { history } from 'umi';
import { history } from '@umijs/max';
message.config({
duration: 1.5,
+32 -5
View File
@@ -150,9 +150,9 @@ export default function browserType() {
shell === 'none'
? {}
: {
shell, // wechat qq uc 360 2345 sougou liebao maxthon
shellVs,
},
shell, // wechat qq uc 360 2345 sougou liebao maxthon
shellVs,
},
);
console.log(
@@ -188,8 +188,8 @@ export function getTableScroll({
if (id) {
tHeader = document.getElementById(id)
? document
.getElementById(id)!
.getElementsByClassName('ant-table-thead')[0]
.getElementById(id)!
.getElementsByClassName('ant-table-thead')[0]
: null;
} else {
tHeader = document.querySelector('.ant-table-wrapper');
@@ -241,3 +241,30 @@ export function exportJson(name: string, data: string) {
createA.download = name;
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 (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;
}
+7 -8
View File
@@ -1,9 +1,8 @@
export const version = '2.14.2';
export const changeLogLink = 'https://t.me/jiao_long/327';
export const changeLog = `2.14.2 版本说明
1. 增加npm镜像源配置 NpmMirror="https://registry.npmmirror.com"
2. 修复任务视图筛选条件和编辑视图,感谢 https://github.com/cddjr
3. 修复获取多级脚本路径错误,感谢 https://github.com/cddjr
4. 修改定时任务详情交互,点击任务名称打开任务详情,点击任务命令跳转脚本
5. 其他bug修复
export const version = '2.14.4';
export const changeLogLink = 'https://t.me/jiao_long/331';
export const changeLog = `2.14.4 版本说明
1. 通知设置新增自定义通知
2. 脚本管理支持新建文件夹
3. 依赖管理增加批量重新安装
4. 修复创建应用错误
`;
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "es2017",
"lib": ["es2017", "esnext.asynciterable", "DOM"],
"lib": ["es2021", "esnext.asynciterable", "DOM"],
"typeRoots": [
"./node_modules/@types",
"./src/types",