mirror of
https://github.com/crawlab-team/crawlab.git
synced 2026-01-30 18:00:56 +01:00
updated frontend
This commit is contained in:
4
frontend/src/utils/array.ts
Normal file
4
frontend/src/utils/array.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const isDuplicated = <T = any>(array?: T[]) => {
|
||||
if (!array) return false;
|
||||
return array.length > new Set(array).size;
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
const TokenKey = 'token'
|
||||
|
||||
export function getToken() {
|
||||
return window.localStorage.getItem(TokenKey)
|
||||
}
|
||||
|
||||
export function setToken(token) {
|
||||
return window.localStorage.setItem(TokenKey, token)
|
||||
}
|
||||
|
||||
export function removeToken() {
|
||||
return window.localStorage.removeItem(TokenKey)
|
||||
}
|
||||
9
frontend/src/utils/auth.ts
Normal file
9
frontend/src/utils/auth.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const TOKEN_KEY = 'token';
|
||||
|
||||
export const getToken = () => {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
};
|
||||
|
||||
export const setToken = (token: string) => {
|
||||
return localStorage.setItem(TOKEN_KEY, token);
|
||||
};
|
||||
11
frontend/src/utils/auto.ts
Normal file
11
frontend/src/utils/auto.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import {onBeforeUnmount, onMounted} from 'vue';
|
||||
|
||||
export const setupAutoUpdate = (fn: Function, interval?: number, handle?: number) => {
|
||||
if (!interval) interval = 5000;
|
||||
onMounted(() => {
|
||||
handle = setInterval(fn, interval);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(handle);
|
||||
});
|
||||
};
|
||||
309
frontend/src/utils/codemirror.ts
Normal file
309
frontend/src/utils/codemirror.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import CodeMirror, {Editor, EditorConfiguration} from 'codemirror';
|
||||
|
||||
// import addons
|
||||
import 'codemirror/addon/search/search.js';
|
||||
import 'codemirror/addon/search/searchcursor';
|
||||
import 'codemirror/addon/search/matchesonscrollbar.js';
|
||||
import 'codemirror/addon/search/matchesonscrollbar.css';
|
||||
import 'codemirror/addon/search/match-highlighter';
|
||||
import 'codemirror/addon/edit/matchtags';
|
||||
import 'codemirror/addon/edit/matchbrackets';
|
||||
import 'codemirror/addon/edit/closebrackets';
|
||||
import 'codemirror/addon/edit/closetag';
|
||||
import 'codemirror/addon/comment/comment';
|
||||
import 'codemirror/addon/hint/show-hint';
|
||||
|
||||
// import keymap
|
||||
import 'codemirror/keymap/emacs.js';
|
||||
import 'codemirror/keymap/sublime.js';
|
||||
import 'codemirror/keymap/vim.js';
|
||||
|
||||
const themes = [
|
||||
'3024-day',
|
||||
'3024-night',
|
||||
'abcdef',
|
||||
'ambiance',
|
||||
'ambiance-mobile',
|
||||
'ayu-dark',
|
||||
'ayu-mirage',
|
||||
'base16-dark',
|
||||
'base16-light',
|
||||
'bespin',
|
||||
'blackboard',
|
||||
'cobalt',
|
||||
'colorforth',
|
||||
'darcula',
|
||||
'dracula',
|
||||
'duotone-dark',
|
||||
'duotone-light',
|
||||
'eclipse',
|
||||
'elegant',
|
||||
'erlang-dark',
|
||||
'gruvbox-dark',
|
||||
'hopscotch',
|
||||
'icecoder',
|
||||
'idea',
|
||||
'isotope',
|
||||
'lesser-dark',
|
||||
'liquibyte',
|
||||
'lucario',
|
||||
'material',
|
||||
'material-darker',
|
||||
'material-ocean',
|
||||
'material-palenight',
|
||||
'mbo',
|
||||
'mdn-like',
|
||||
'midnight',
|
||||
'monokai',
|
||||
'moxer',
|
||||
'neat',
|
||||
'neo',
|
||||
'night',
|
||||
'nord',
|
||||
'oceanic-next',
|
||||
'panda-syntax',
|
||||
'paraiso-dark',
|
||||
'paraiso-light',
|
||||
'pastel-on-dark',
|
||||
'railscasts',
|
||||
'rubyblue',
|
||||
'seti',
|
||||
'shadowfox',
|
||||
'solarized',
|
||||
'ssms',
|
||||
'the-matrix',
|
||||
'tomorrow-night-bright',
|
||||
'tomorrow-night-eighties',
|
||||
'ttcn',
|
||||
'twilight',
|
||||
'vibrant-ink',
|
||||
'xq-dark',
|
||||
'xq-light',
|
||||
'yeti',
|
||||
'yonce',
|
||||
'zenburn',
|
||||
];
|
||||
|
||||
const template = `import os
|
||||
def func(a):
|
||||
pass
|
||||
class Class1:
|
||||
pass
|
||||
`;
|
||||
|
||||
const optionsDefinitions: FileEditorOptionDefinition[] = [
|
||||
{
|
||||
name: 'theme',
|
||||
title: 'Theme',
|
||||
description: 'The theme to style the editor with.',
|
||||
type: 'select',
|
||||
data: {
|
||||
options: themes,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'indentUnit',
|
||||
title: 'Indent Unit',
|
||||
description: 'How many spaces a block (whatever that means in the edited language) should be indented.',
|
||||
type: 'input-number',
|
||||
data: {
|
||||
min: 1,
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'smartIndent',
|
||||
title: 'Smart Indent',
|
||||
description: 'Whether to use the context-sensitive indentation that the mode provides (or just indent the same as the line before).',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
name: 'tabSize',
|
||||
title: 'Tab Size',
|
||||
description: 'The width of a tab character. Defaults to 4.',
|
||||
type: 'input-number',
|
||||
data: {
|
||||
min: 1,
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'indentWithTabs',
|
||||
title: 'Indent with Tabs',
|
||||
description: 'Whether, when indenting, the first N*tabSize spaces should be replaced by N tabs.',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
name: 'electricChars',
|
||||
title: 'Electric Chars',
|
||||
description: 'Configures whether the editor should re-indent the current line when a character is typed that might change its proper indentation (only works if the mode supports indentation).',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
name: 'keyMap',
|
||||
title: 'Keymap',
|
||||
description: 'Configures the keymap to use.',
|
||||
type: 'select',
|
||||
data: {
|
||||
options: [
|
||||
'default',
|
||||
'emacs',
|
||||
'sublime',
|
||||
'vim',
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'lineWrapping',
|
||||
title: 'Line Wrapping',
|
||||
description: 'Whether to scroll or wrap for long lines.',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
name: 'lineNumbers',
|
||||
title: 'Line Numbers',
|
||||
description: 'Whether to show line numbers to the left of the editor.',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
name: 'showCursorWhenSelecting',
|
||||
title: 'Show Cursor When Selecting',
|
||||
description: 'Whether the cursor should be drawn when a selection is active.',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
name: 'lineWiseCopyCut',
|
||||
title: 'Line-wise Copy-Cut',
|
||||
description: 'When enabled, doing copy or cut when there is no selection will copy or cut the whole lines that have cursors on them.',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
name: 'pasteLinesPerSelection',
|
||||
title: 'Paste Lines per Selection',
|
||||
description: 'When pasting something from an external source (not from the editor itself), if the number of lines matches the number of selection, the editor will by default insert one line per selection. You can set this to false to disable that behavior.',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
name: 'undoDepth',
|
||||
title: 'Paste Lines per Selection',
|
||||
description: 'The maximum number of undo levels that the editor stores.',
|
||||
type: 'input-number',
|
||||
data: {
|
||||
min: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'cursorBlinkRate',
|
||||
title: 'Cursor Blink Rate',
|
||||
description: 'Half-period in milliseconds used for cursor blinking.',
|
||||
type: 'input-number',
|
||||
data: {
|
||||
min: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'cursorScrollMargin',
|
||||
title: 'Cursor Scroll Margin',
|
||||
description: 'How much extra space to always keep above and below the cursor when approaching the top or bottom of the visible view in a scrollable document.',
|
||||
type: 'input-number',
|
||||
data: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'cursorHeight',
|
||||
title: 'Cursor Height',
|
||||
description: 'Determines the height of the cursor. Setting it to 1, means it spans the whole height of the line. For some fonts (and by some tastes) a smaller height (for example 0.85), which causes the cursor to not reach all the way to the bottom of the line, looks better',
|
||||
type: 'input-number',
|
||||
data: {
|
||||
min: 0,
|
||||
step: 0.01,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'maxHighlightLength',
|
||||
title: 'Max Highlight Length',
|
||||
description: 'When highlighting long lines, in order to stay responsive, the editor will give up and simply style the rest of the line as plain text when it reaches a certain position.',
|
||||
type: 'input-number',
|
||||
data: {
|
||||
min: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'spellcheck',
|
||||
title: 'Spell Check',
|
||||
description: 'Specifies whether or not spellcheck will be enabled on the input.',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
name: 'autocorrect',
|
||||
title: 'Auto Correct',
|
||||
description: 'Specifies whether or not auto-correct will be enabled on the input.',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
name: 'autocapitalize',
|
||||
title: 'Auto Capitalize',
|
||||
description: 'Specifies whether or not auto-capitalization will be enabled on the input.',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
name: 'highlightSelectionMatches',
|
||||
title: 'Highlight Selection Matches',
|
||||
description: 'Adds a highlightSelectionMatches option that can be enabled to highlight all instances of a currently selected word. When enabled, it causes the current word to be highlighted when nothing is selected.',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
name: 'matchBrackets',
|
||||
title: 'Match Brackets',
|
||||
description: 'When set to true or an options object, causes matching brackets to be highlighted whenever the cursor is next to them.',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
name: 'matchTags',
|
||||
title: 'Match Tags',
|
||||
description: 'When enabled will cause the tags around the cursor to be highlighted',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
name: 'autoCloseBrackets',
|
||||
title: 'Auto-Close Brackets',
|
||||
description: 'Will auto-close brackets and quotes when typed. It\'ll auto-close ()[]{}\'\'"".',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
name: 'autoCloseTags',
|
||||
title: 'Auto-Close Tags',
|
||||
description: 'Will auto-close XML tags when \'>\' or \'/\' is typed.',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
name: 'showHint',
|
||||
title: 'Show Hint',
|
||||
description: '',
|
||||
type: 'switch',
|
||||
},
|
||||
];
|
||||
|
||||
const themeCache = new Set<string>();
|
||||
|
||||
export const getCodemirrorEditor = (el: HTMLElement, options: EditorConfiguration): Editor => {
|
||||
return CodeMirror(el, options);
|
||||
};
|
||||
|
||||
export const initTheme = async (name?: string) => {
|
||||
if (!name) name = 'darcula';
|
||||
if (themeCache.has(name)) return;
|
||||
await import(`codemirror/theme/${name}.css`);
|
||||
themeCache.add(name);
|
||||
};
|
||||
|
||||
export const getThemes = () => {
|
||||
return themes;
|
||||
};
|
||||
|
||||
export const getCodeMirrorTemplate = () => {
|
||||
return template;
|
||||
};
|
||||
|
||||
export const getOptionDefinition = (name: string): FileEditorOptionDefinition | undefined => {
|
||||
return optionsDefinitions.find(d => d.name === name);
|
||||
};
|
||||
5
frontend/src/utils/color.ts
Normal file
5
frontend/src/utils/color.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import colors from '@/styles/color.scss';
|
||||
|
||||
export const getPredefinedColors = (): string[] => {
|
||||
return Object.values(colors);
|
||||
};
|
||||
28
frontend/src/utils/debounce.ts
Normal file
28
frontend/src/utils/debounce.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {plainClone} from '@/utils/object';
|
||||
|
||||
interface DebounceOptions {
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
const defaultDebounceOptions: DebounceOptions = {
|
||||
delay: 500,
|
||||
};
|
||||
|
||||
const getDefaultDebounceOptions = (): DebounceOptions => {
|
||||
return plainClone(defaultDebounceOptions);
|
||||
};
|
||||
|
||||
const normalizeDebounceOptions = (options?: DebounceOptions): DebounceOptions => {
|
||||
if (!options) options = getDefaultDebounceOptions();
|
||||
if (!options.delay) options.delay = defaultDebounceOptions.delay;
|
||||
return options;
|
||||
};
|
||||
|
||||
export const debounce = <T = any>(fn: Function, options?: DebounceOptions): Function => {
|
||||
let handle: number | null = null;
|
||||
return () => {
|
||||
if (handle) clearTimeout(handle);
|
||||
const {delay} = normalizeDebounceOptions(options);
|
||||
handle = setTimeout(fn, delay);
|
||||
};
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
export default {
|
||||
docs: [
|
||||
{
|
||||
path: '/projects',
|
||||
pattern: '^Project'
|
||||
},
|
||||
{
|
||||
path: '/spiders',
|
||||
pattern: '^Spider|^SDK|^Integration|^CI/Git'
|
||||
},
|
||||
{
|
||||
path: '/tasks',
|
||||
pattern: '^Task|^Architecture/Task'
|
||||
},
|
||||
{
|
||||
path: '/schedules',
|
||||
pattern: '^Schedule'
|
||||
},
|
||||
{
|
||||
path: '/nodes',
|
||||
pattern: '^Node|^Architecture/Node'
|
||||
},
|
||||
{
|
||||
path: '/setting',
|
||||
pattern: '^Notification'
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export default {
|
||||
UUID: () => {
|
||||
const s = []
|
||||
const hexDigits = '0123456789abcdef'
|
||||
for (let i = 0; i < 36; i++) {
|
||||
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1)
|
||||
}
|
||||
s[14] = '4' // bits 12-15 of the time_hi_and_version field to 0010
|
||||
s[8] = s[13] = s[18] = s[23] = '-'
|
||||
|
||||
return s.join('')
|
||||
}
|
||||
}
|
||||
10
frontend/src/utils/form.ts
Normal file
10
frontend/src/utils/form.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import {ref} from 'vue';
|
||||
|
||||
export const getDefaultFormComponentData = <T>(defaultFn: DefaultFormFunc<T>) => {
|
||||
return {
|
||||
form: ref<T>(defaultFn()),
|
||||
formRef: ref(),
|
||||
formList: ref<T[]>([]),
|
||||
formTableFieldRefsMap: ref<FormTableFieldRefsMap>(new Map()),
|
||||
} as FormComponentData<T>;
|
||||
};
|
||||
11
frontend/src/utils/func.ts
Normal file
11
frontend/src/utils/func.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const voidFunc = () => {
|
||||
// do nothing
|
||||
};
|
||||
|
||||
export const emptyObjectFunc = () => {
|
||||
return {};
|
||||
};
|
||||
|
||||
export const emptyArrayFunc = () => {
|
||||
return [];
|
||||
};
|
||||
5
frontend/src/utils/hash.ts
Normal file
5
frontend/src/utils/hash.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import md5 from 'md5';
|
||||
|
||||
export const getMd5 = (text: string): string => {
|
||||
return md5(text).toString();
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
export default {
|
||||
htmlEscape: text => {
|
||||
return text.replace(/[<>"&]/g, function(match, pos, originalText) {
|
||||
switch (match) {
|
||||
case '<':
|
||||
return '<'
|
||||
case '>':
|
||||
return '>'
|
||||
case '&':
|
||||
return '&'
|
||||
case '"':
|
||||
return '"'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// translate router.meta.title, be used in breadcrumb sidebar tagsview
|
||||
export function generateTitle(title) {
|
||||
const hasKey = this.$te('route.' + title)
|
||||
|
||||
if (hasKey) {
|
||||
// $t :this method from vue-i18n, inject in @/lang/index.js
|
||||
const translatedTitle = this.$t('route.' + title)
|
||||
|
||||
return translatedTitle
|
||||
}
|
||||
return title
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import stats from './stats'
|
||||
import encrypt from './encrypt'
|
||||
import tour from './tour'
|
||||
import log from './log'
|
||||
import scrapy from './scrapy'
|
||||
import doc from './doc'
|
||||
|
||||
export default {
|
||||
stats,
|
||||
encrypt,
|
||||
tour,
|
||||
log,
|
||||
scrapy,
|
||||
doc
|
||||
}
|
||||
28
frontend/src/utils/list.ts
Normal file
28
frontend/src/utils/list.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {onBeforeMount, Ref} from 'vue';
|
||||
import {Store} from 'vuex';
|
||||
import {setupAutoUpdate} from '@/utils/auto';
|
||||
|
||||
export const getDefaultUseListOptions = <T = any>(navActions: Ref<ListActionGroup[]>, tableColumns: Ref<TableColumns<T>>): UseListOptions<T> => {
|
||||
return {
|
||||
navActions,
|
||||
tableColumns,
|
||||
};
|
||||
};
|
||||
|
||||
export const setupGetAllList = (store: Store<RootStoreState>, allListNamespaces: ListStoreNamespace[]) => {
|
||||
onBeforeMount(async () => {
|
||||
await Promise.all(allListNamespaces?.map(ns => store.dispatch(`${ns}/getAllList`)) || []);
|
||||
});
|
||||
};
|
||||
|
||||
export const setupListComponent = (ns: ListStoreNamespace, store: Store<RootStoreState>, allListNamespaces?: ListStoreNamespace[]) => {
|
||||
if (!allListNamespaces) allListNamespaces = [];
|
||||
|
||||
// get all list
|
||||
setupGetAllList(store, allListNamespaces);
|
||||
|
||||
// auto update
|
||||
setupAutoUpdate(async () => {
|
||||
await store.dispatch(`${ns}/getList`);
|
||||
});
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
const regexToken = ' :,.'
|
||||
|
||||
export default {
|
||||
// errorRegex: new RegExp(`(?:[${regexToken}]|^)((?:error|exception|traceback)s?)(?:[${regexToken}]|$)`, 'gi')
|
||||
errorRegex: new RegExp(
|
||||
`(?:[${regexToken}]|^)((?:error|exception|traceback)s?)(?:[${regexToken}]|$)`,
|
||||
'gi'),
|
||||
errorWhitelist: [
|
||||
'log_count/ERROR'
|
||||
]
|
||||
}
|
||||
5
frontend/src/utils/mongo.ts
Normal file
5
frontend/src/utils/mongo.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const EMPTY_OBJECT_ID = '000000000000000000000000';
|
||||
|
||||
export const isZeroObjectId = (id: string): boolean => {
|
||||
return !id || id === EMPTY_OBJECT_ID;
|
||||
};
|
||||
8
frontend/src/utils/object.ts
Normal file
8
frontend/src/utils/object.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const plainClone = <T = any>(obj: T): T => {
|
||||
if (obj === undefined || obj === null) return obj;
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
};
|
||||
|
||||
export const cloneArray = <T = any>(arr: T[]): T[] => {
|
||||
return Array.from(arr);
|
||||
};
|
||||
6
frontend/src/utils/pagination.ts
Normal file
6
frontend/src/utils/pagination.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const getDefaultPagination = (): TablePagination => {
|
||||
return {
|
||||
page: 1,
|
||||
size: 10,
|
||||
};
|
||||
};
|
||||
8
frontend/src/utils/path.ts
Normal file
8
frontend/src/utils/path.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const getPrimaryPath = (path: string): string => {
|
||||
const arr = path.split('/');
|
||||
if (arr.length <= 1) {
|
||||
return path;
|
||||
} else {
|
||||
return `/${arr[1]}`;
|
||||
}
|
||||
};
|
||||
@@ -1,123 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import { MessageBox, Message } from 'element-ui'
|
||||
import store from '@/store'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import i18n from '@/i18n'
|
||||
import router from '@/router'
|
||||
|
||||
// const codeMessage = {
|
||||
// 200: '服务器成功返回请求的数据。',
|
||||
// 201: '新建或修改数据成功。',
|
||||
// 202: '一个请求已经进入后台排队(异步任务)。',
|
||||
// 204: '删除数据成功。',
|
||||
// 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
|
||||
// 401: '用户没有权限(令牌、用户名、密码错误)。',
|
||||
// 403: '用户得到授权,但是访问是被禁止的。',
|
||||
// 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
|
||||
// 406: '请求的格式不可得。',
|
||||
// 410: '请求的资源被永久删除,且不会再得到的。',
|
||||
// 422: '当创建一个对象时,发生一个验证错误。',
|
||||
// 500: '服务器发生错误,请检查服务器。',
|
||||
// 502: '网关错误。',
|
||||
// 503: '服务不可用,服务器暂时过载或维护。',
|
||||
// 504: '网关超时。'
|
||||
// }
|
||||
|
||||
/**
|
||||
* 异常处理程序
|
||||
*/
|
||||
const errorHandler = (error) => {
|
||||
const { response } = error
|
||||
const routePath = router.currentRoute.path
|
||||
if (response && response.status) {
|
||||
const errorText = response.data.error
|
||||
const { status } = response
|
||||
Message({
|
||||
message: i18n.t('Request Error') + ` ${status}: ${response.request.responseURL}. ${errorText}`,
|
||||
type: 'error',
|
||||
duration: 5 * 1000
|
||||
})
|
||||
switch (status) {
|
||||
case 401:
|
||||
if (routePath !== '/login' && routePath !== '/') {
|
||||
MessageBox.confirm(
|
||||
i18n.t('auth.login_expired_message'),
|
||||
i18n.t('auth.login_expired_title'), {
|
||||
confirmButtonText: i18n.t('auth.login_expired_confirm'),
|
||||
cancelButtonText: i18n.t('auth.login_expired_cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
store.dispatch('user/resetToken').then(() => {
|
||||
location.reload()
|
||||
})
|
||||
})
|
||||
}
|
||||
break
|
||||
default:
|
||||
}
|
||||
} else if (!response) {
|
||||
Message({
|
||||
message: `您的网络发生异常,无法连接服务器`,
|
||||
type: 'error',
|
||||
duration: 5 * 1000
|
||||
})
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// 根据 VUE_APP_BASE_URL 生成 baseUrl
|
||||
let baseUrl = process.env.VUE_APP_BASE_URL
|
||||
? process.env.VUE_APP_BASE_URL
|
||||
: 'http://localhost:8000'
|
||||
if (!baseUrl.match(/^https?/i)) {
|
||||
baseUrl = `${window.location.protocol}//${window.location.host}${process.env.VUE_APP_BASE_URL}`
|
||||
}
|
||||
|
||||
// 如果 Docker 中设置了 CRAWLAB_API_ADDRESS 这个环境变量,则会将 baseUrl 覆盖
|
||||
const CRAWLAB_API_ADDRESS = '###CRAWLAB_API_ADDRESS###'
|
||||
if (!CRAWLAB_API_ADDRESS.match('CRAWLAB_API_ADDRESS')) {
|
||||
baseUrl = CRAWLAB_API_ADDRESS
|
||||
}
|
||||
// create an axios instance
|
||||
const service = axios.create({
|
||||
baseURL: baseUrl, // url = base url + request url
|
||||
// withCredentials: true, // send cookies when cross-domain requests
|
||||
timeout: 15000 // request timeout
|
||||
})
|
||||
// request interceptor
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
// do something before request is sent
|
||||
|
||||
if (store.getters.token) {
|
||||
// let each request carry token
|
||||
// ['X-Token'] is a custom headers key
|
||||
// please modify it according to the actual situation
|
||||
config.headers['Authorization'] = getToken()
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
// do something with request error
|
||||
console.log(error) // for debug
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// response interceptor
|
||||
service.interceptors.response.use(
|
||||
/**
|
||||
* If you want to get http information such as headers or status
|
||||
* Please return response => response
|
||||
*/
|
||||
/**
|
||||
* Determine the request status by custom code
|
||||
* Here is just an example
|
||||
* You can also judge the status by HTTP Status Code
|
||||
*/
|
||||
response => {
|
||||
return response
|
||||
},
|
||||
errorHandler
|
||||
)
|
||||
export default service
|
||||
12
frontend/src/utils/result.ts
Normal file
12
frontend/src/utils/result.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const getFieldsFromData = (data: TableData<Result>) => {
|
||||
if (data.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const item = data[0];
|
||||
if (typeof item !== 'object') return [];
|
||||
return Object.keys(item).map(key => {
|
||||
return {
|
||||
key,
|
||||
};
|
||||
});
|
||||
};
|
||||
6
frontend/src/utils/route.ts
Normal file
6
frontend/src/utils/route.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const getRoutePathByDepth = (path: string, depth?: number) => {
|
||||
if (!depth) depth = 1;
|
||||
const arr = path.split('/');
|
||||
if (!arr[0]) depth += 1;
|
||||
return arr.slice(0, depth).join('/');
|
||||
};
|
||||
@@ -1,212 +0,0 @@
|
||||
export default {
|
||||
importantSettingParamNames: {
|
||||
BOT_NAME: String,
|
||||
SPIDER_MODULES: Array,
|
||||
NEWSPIDER_MODULE: String,
|
||||
USER_AGENT: String,
|
||||
ROBOTSTXT_OBEY: Boolean,
|
||||
CONCURRENT_REQUESTS: Number,
|
||||
DOWNLOAD_DELAY: Number,
|
||||
CONCURRENT_REQUESTS_PER_DOMAIN: Number,
|
||||
CONCURRENT_REQUESTS_PER_IP: Number,
|
||||
COOKIES_ENABLED: Boolean,
|
||||
TELNETCONSOLE_ENABLED: Boolean,
|
||||
DEFAULT_REQUEST_HEADERS: Object,
|
||||
SPIDER_MIDDLEWARES: Object,
|
||||
DOWNLOADER_MIDDLEWARES: Object,
|
||||
EXTENSIONS: Object,
|
||||
ITEM_PIPELINES: Object,
|
||||
AUTOTHROTTLE_ENABLED: Boolean,
|
||||
AUTOTHROTTLE_START_DELAY: Number,
|
||||
AUTOTHROTTLE_MAX_DELAY: Number,
|
||||
AUTOTHROTTLE_TARGET_CONCURRENCY: Number,
|
||||
AUTOTHROTTLE_DEBUG: Boolean,
|
||||
HTTPCACHE_ENABLED: Boolean,
|
||||
HTTPCACHE_EXPIRATION_SECS: Number,
|
||||
HTTPCACHE_DIR: String,
|
||||
HTTPCACHE_IGNORE_HTTP_CODES: Array,
|
||||
HTTPCACHE_STORAGE: String
|
||||
},
|
||||
settingParamNames: [
|
||||
'AWS_ACCESS_KEY_ID',
|
||||
'AWS_SECRET_ACCESS_KEY',
|
||||
'AWS_ENDPOINT_URL',
|
||||
'AWS_USE_SSL',
|
||||
'AWS_VERIFY',
|
||||
'AWS_REGION_NAME',
|
||||
'BOT_NAME',
|
||||
'CONCURRENT_ITEMS',
|
||||
'CONCURRENT_REQUESTS',
|
||||
'CONCURRENT_REQUESTS_PER_DOMAIN',
|
||||
'CONCURRENT_REQUESTS_PER_IP',
|
||||
'DEFAULT_ITEM_CLASS',
|
||||
'DEFAULT_REQUEST_HEADERS',
|
||||
'DEPTH_LIMIT',
|
||||
'DEPTH_PRIORITY',
|
||||
'DEPTH_STATS_VERBOSE',
|
||||
'DNSCACHE_ENABLED',
|
||||
'DNSCACHE_SIZE',
|
||||
'DNS_TIMEOUT',
|
||||
'DOWNLOADER',
|
||||
'DOWNLOADER_HTTPCLIENTFACTORY',
|
||||
'DOWNLOADER_CLIENTCONTEXTFACTORY',
|
||||
'DOWNLOADER_CLIENT_TLS_CIPHERS',
|
||||
'DOWNLOADER_CLIENT_TLS_METHOD',
|
||||
'DOWNLOADER_CLIENT_TLS_VERBOSE_LOGGING',
|
||||
'DOWNLOADER_MIDDLEWARES',
|
||||
'DOWNLOADER_MIDDLEWARES_BASE',
|
||||
'DOWNLOADER_STATS',
|
||||
'DOWNLOAD_DELAY',
|
||||
'DOWNLOAD_HANDLERS',
|
||||
'DOWNLOAD_HANDLERS_BASE',
|
||||
'DOWNLOAD_TIMEOUT',
|
||||
'DOWNLOAD_MAXSIZE',
|
||||
'DOWNLOAD_WARNSIZE',
|
||||
'DOWNLOAD_FAIL_ON_DATALOSS',
|
||||
'DUPEFILTER_CLASS',
|
||||
'DUPEFILTER_DEBUG',
|
||||
'EDITOR',
|
||||
'EXTENSIONS',
|
||||
'EXTENSIONS_BASE',
|
||||
'FEED_TEMPDIR',
|
||||
'FTP_PASSIVE_MODE',
|
||||
'FTP_PASSWORD',
|
||||
'FTP_USER',
|
||||
'ITEM_PIPELINES',
|
||||
'ITEM_PIPELINES_BASE',
|
||||
'LOG_ENABLED',
|
||||
'LOG_ENCODING',
|
||||
'LOG_FILE',
|
||||
'LOG_FORMAT',
|
||||
'LOG_DATEFORMAT',
|
||||
'LOG_FORMATTER',
|
||||
'LOG_LEVEL',
|
||||
'LOG_STDOUT',
|
||||
'LOG_SHORT_NAMES',
|
||||
'LOGSTATS_INTERVAL',
|
||||
'MEMDEBUG_ENABLED',
|
||||
'MEMDEBUG_NOTIFY',
|
||||
'MEMUSAGE_ENABLED',
|
||||
'MEMUSAGE_LIMIT_MB',
|
||||
'MEMUSAGE_CHECK_INTERVAL_SECONDS',
|
||||
'MEMUSAGE_NOTIFY_MAIL',
|
||||
'MEMUSAGE_WARNING_MB',
|
||||
'NEWSPIDER_MODULE',
|
||||
'RANDOMIZE_DOWNLOAD_DELAY',
|
||||
'REACTOR_THREADPOOL_MAXSIZE',
|
||||
'REDIRECT_MAX_TIMES',
|
||||
'REDIRECT_PRIORITY_ADJUST',
|
||||
'RETRY_PRIORITY_ADJUST',
|
||||
'ROBOTSTXT_OBEY',
|
||||
'ROBOTSTXT_PARSER',
|
||||
'SCHEDULER',
|
||||
'SCHEDULER_DEBUG',
|
||||
'SCHEDULER_DISK_QUEUE',
|
||||
'SCHEDULER_MEMORY_QUEUE',
|
||||
'SCHEDULER_PRIORITY_QUEUE',
|
||||
'SPIDER_CONTRACTS',
|
||||
'SPIDER_CONTRACTS_BASE',
|
||||
'SPIDER_LOADER_CLASS',
|
||||
'SPIDER_LOADER_WARN_ONLY',
|
||||
'SPIDER_MIDDLEWARES',
|
||||
'SPIDER_MIDDLEWARES_BASE',
|
||||
'SPIDER_MODULES',
|
||||
'STATS_CLASS',
|
||||
'STATS_DUMP',
|
||||
'STATSMAILER_RCPTS',
|
||||
'TELNETCONSOLE_ENABLED',
|
||||
'TELNETCONSOLE_PORT',
|
||||
'TEMPLATES_DIR',
|
||||
'URLLENGTH_LIMIT',
|
||||
'USER_AGENT',
|
||||
'AJAXCRAWL_ENABLED',
|
||||
'AUTOTHROTTLE_DEBUG',
|
||||
'AUTOTHROTTLE_ENABLED',
|
||||
'AUTOTHROTTLE_MAX_DELAY',
|
||||
'AUTOTHROTTLE_START_DELAY',
|
||||
'AUTOTHROTTLE_TARGET_CONCURRENCY',
|
||||
'CLOSESPIDER_ERRORCOUNT',
|
||||
'CLOSESPIDER_ITEMCOUNT',
|
||||
'CLOSESPIDER_PAGECOUNT',
|
||||
'CLOSESPIDER_TIMEOUT',
|
||||
'COMMANDS_MODULE',
|
||||
'COMPRESSION_ENABLED',
|
||||
'COOKIES_DEBUG',
|
||||
'COOKIES_ENABLED',
|
||||
'FEED_EXPORTERS',
|
||||
'FEED_EXPORTERS_BASE',
|
||||
'FEED_EXPORT_ENCODING',
|
||||
'FEED_EXPORT_FIELDS',
|
||||
'FEED_EXPORT_INDENT',
|
||||
'FEED_FORMAT',
|
||||
'FEED_STORAGES',
|
||||
'FEED_STORAGES_BASE',
|
||||
'FEED_STORAGE_FTP_ACTIVE',
|
||||
'FEED_STORAGE_S3_ACL',
|
||||
'FEED_STORE_EMPTY',
|
||||
'FEED_URI',
|
||||
'FILES_EXPIRES',
|
||||
'FILES_RESULT_FIELD',
|
||||
'FILES_STORE',
|
||||
'FILES_STORE_GCS_ACL',
|
||||
'FILES_STORE_S3_ACL',
|
||||
'FILES_URLS_FIELD',
|
||||
'GCS_PROJECT_ID',
|
||||
'HTTPCACHE_ALWAYS_STORE',
|
||||
'HTTPCACHE_DBM_MODULE',
|
||||
'HTTPCACHE_DIR',
|
||||
'HTTPCACHE_ENABLED',
|
||||
'HTTPCACHE_EXPIRATION_SECS',
|
||||
'HTTPCACHE_GZIP',
|
||||
'HTTPCACHE_IGNORE_HTTP_CODES',
|
||||
'HTTPCACHE_IGNORE_MISSING',
|
||||
'HTTPCACHE_IGNORE_RESPONSE_CACHE_CONTROLS',
|
||||
'HTTPCACHE_IGNORE_SCHEMES',
|
||||
'HTTPCACHE_POLICY',
|
||||
'HTTPCACHE_STORAGE',
|
||||
'HTTPERROR_ALLOWED_CODES',
|
||||
'HTTPERROR_ALLOW_ALL',
|
||||
'HTTPPROXY_AUTH_ENCODING',
|
||||
'HTTPPROXY_ENABLED',
|
||||
'IMAGES_EXPIRES',
|
||||
'IMAGES_MIN_HEIGHT',
|
||||
'IMAGES_MIN_WIDTH',
|
||||
'IMAGES_RESULT_FIELD',
|
||||
'IMAGES_STORE',
|
||||
'IMAGES_STORE_GCS_ACL',
|
||||
'IMAGES_STORE_S3_ACL',
|
||||
'IMAGES_THUMBS',
|
||||
'IMAGES_URLS_FIELD',
|
||||
'MAIL_FROM',
|
||||
'MAIL_HOST',
|
||||
'MAIL_PASS',
|
||||
'MAIL_PORT',
|
||||
'MAIL_SSL',
|
||||
'MAIL_TLS',
|
||||
'MAIL_USER',
|
||||
'MEDIA_ALLOW_REDIRECTS',
|
||||
'METAREFRESH_ENABLED',
|
||||
'METAREFRESH_IGNORE_TAGS',
|
||||
'METAREFRESH_MAXDELAY',
|
||||
'REDIRECT_ENABLED',
|
||||
'REDIRECT_MAX_TIMES',
|
||||
'REFERER_ENABLED',
|
||||
'REFERRER_POLICY',
|
||||
'RETRY_ENABLED',
|
||||
'RETRY_HTTP_CODES',
|
||||
'RETRY_TIMES',
|
||||
'TELNETCONSOLE_HOST',
|
||||
'TELNETCONSOLE_PASSWORD',
|
||||
'TELNETCONSOLE_PORT',
|
||||
'TELNETCONSOLE_USERNAME',
|
||||
'REDIS_ITEMS_KEY',
|
||||
'REDIS_ITEMS_SERIALIZER',
|
||||
'REDIS_HOST',
|
||||
'REDIS_PORT',
|
||||
'REDIS_URL',
|
||||
'REDIS_PARAMS',
|
||||
'REDIS_START_URLS_AS_SET',
|
||||
'REDIS_START_URLS_KEY',
|
||||
'REDIS_ENCODING'
|
||||
]
|
||||
}
|
||||
23
frontend/src/utils/service.ts
Normal file
23
frontend/src/utils/service.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {Store} from 'vuex';
|
||||
|
||||
export const getDefaultService = <T>(ns: string, store: Store<RootStoreState>): Services<T> => {
|
||||
const {dispatch} = store;
|
||||
|
||||
return {
|
||||
getById: (id: string) => dispatch(`${ns}/getById`, id),
|
||||
create: (form: T) => dispatch(`${ns}/create`, form),
|
||||
updateById: (id: string, form: T) => dispatch(`${ns}/updateById`, {id, form}),
|
||||
deleteById: (id: string) => dispatch(`${ns}/deleteById`, id),
|
||||
getList: (params?: ListRequestParams) => {
|
||||
if (params) {
|
||||
return dispatch(`${ns}/getListWithParams`, params);
|
||||
} else {
|
||||
return dispatch(`${ns}/getList`);
|
||||
}
|
||||
},
|
||||
getAll: () => dispatch(`${ns}/getAllList`),
|
||||
createList: (data: T[]) => dispatch(`${ns}/createList`, data),
|
||||
updateList: (ids: string[], data: T, fields: string[]) => dispatch(`${ns}/updateList`, {ids, data, fields}),
|
||||
deleteList: (ids: string[]) => dispatch(`${ns}/deleteList`, ids),
|
||||
};
|
||||
};
|
||||
3
frontend/src/utils/sleep.ts
Normal file
3
frontend/src/utils/sleep.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const sleep = (duration: number) => {
|
||||
return new Promise(resolve => setTimeout(resolve, duration));
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const sendEvCrawlab = async(eventCategory, eventAction, eventLabel) => {
|
||||
await axios.get(process.env.VUE_APP_CRAWLAB_BASE_URL + '/track', {
|
||||
params: {
|
||||
uid: localStorage.getItem('uid'),
|
||||
sid: sessionStorage.getItem('sid'),
|
||||
ec: eventCategory,
|
||||
ea: eventAction,
|
||||
el: eventLabel,
|
||||
v: sessionStorage.getItem('v')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
sendPv(page) {
|
||||
if (localStorage.getItem('useStats') !== '0') {
|
||||
window._hmt.push(['_trackPageview', page])
|
||||
sendEvCrawlab('访问页面', page, '')
|
||||
}
|
||||
},
|
||||
sendEv(category, eventName, optLabel, optValue) {
|
||||
if (localStorage.getItem('useStats') !== '0') {
|
||||
window._hmt.push(['_trackEvent', category, eventName, optLabel, optValue])
|
||||
sendEvCrawlab(category, eventName, optLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
28
frontend/src/utils/stats.ts
Normal file
28
frontend/src/utils/stats.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import dayjs, {Dayjs} from 'dayjs';
|
||||
|
||||
export const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD';
|
||||
|
||||
export const spanDateRange = (start: Dayjs | string, end: Dayjs | string, data: StatsResult[], dateKey?: string): StatsResult[] => {
|
||||
// date key
|
||||
const key = dateKey || 'date';
|
||||
|
||||
// format
|
||||
const format = DEFAULT_DATE_FORMAT;
|
||||
|
||||
// cache data
|
||||
const cache = new Map<string, StatsResult>();
|
||||
data.forEach(d => cache.set(d[key], d));
|
||||
|
||||
// results
|
||||
const results = [] as StatsResult[];
|
||||
|
||||
// iterate
|
||||
for (let date = dayjs(start, format); date.format(format) <= dayjs(end, format).format(format); date = date.add(1, 'day')) {
|
||||
let item = cache.get(date.format(format));
|
||||
if (!item) item = {};
|
||||
item[key] = date.format(format);
|
||||
results.push(item);
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
263
frontend/src/utils/store.ts
Normal file
263
frontend/src/utils/store.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import {getDefaultPagination} from '@/utils/pagination';
|
||||
import {useService} from '@/services';
|
||||
import router from '@/router';
|
||||
import {plainClone} from '@/utils/object';
|
||||
|
||||
export const getDefaultStoreState = <T = any>(ns: StoreNamespace): BaseStoreState<T> => {
|
||||
return {
|
||||
ns,
|
||||
dialogVisible: {
|
||||
createEdit: true,
|
||||
},
|
||||
activeDialogKey: undefined,
|
||||
createEditDialogTabName: 'single',
|
||||
form: {} as T,
|
||||
isSelectiveForm: false,
|
||||
selectedFormFields: [],
|
||||
readonlyFormFields: [],
|
||||
formList: [],
|
||||
confirmLoading: false,
|
||||
tableData: [],
|
||||
tableTotal: 0,
|
||||
tablePagination: getDefaultPagination(),
|
||||
tableListFilter: [],
|
||||
tableListSort: [],
|
||||
allList: [],
|
||||
sidebarCollapsed: false,
|
||||
actionsCollapsed: false,
|
||||
tabs: [{id: 'overview', title: 'Overview'}],
|
||||
afterSave: [],
|
||||
};
|
||||
};
|
||||
|
||||
export const getDefaultStoreGetters = <T = any>(opts?: GetDefaultStoreGettersOptions): BaseStoreGetters<BaseStoreState<T>> => {
|
||||
if (!opts) opts = {};
|
||||
if (!opts.selectOptionValueKey) opts.selectOptionValueKey = '_id';
|
||||
if (!opts.selectOptionLabelKey) opts.selectOptionLabelKey = 'name';
|
||||
|
||||
return {
|
||||
dialogVisible: (state: BaseStoreState<T>) => state.activeDialogKey !== undefined,
|
||||
// isBatchForm: (state: BaseStoreState<T>) => state.isSelectiveForm || state.createEditDialogTabName === 'batch',
|
||||
isBatchForm: (state: BaseStoreState<T>) => {
|
||||
return state.isSelectiveForm || state.createEditDialogTabName === 'batch';
|
||||
},
|
||||
formListIds: (state: BaseStoreState<BaseModel>) => state.formList.map(d => d._id as string),
|
||||
allListSelectOptions: (state: BaseStoreState<BaseModel>) => state.allList.map(d => {
|
||||
return {
|
||||
value: d[opts?.selectOptionValueKey as string],
|
||||
label: d[opts?.selectOptionLabelKey as string],
|
||||
};
|
||||
}),
|
||||
allDict: (state: BaseStoreState<BaseModel>) => {
|
||||
const dict = new Map<string, T>();
|
||||
state.allList.forEach(d => dict.set(d._id as string, d as any));
|
||||
return dict;
|
||||
},
|
||||
tabName: () => {
|
||||
const arr = router.currentRoute.value.path.split('/');
|
||||
if (arr.length < 3) return '';
|
||||
return arr[3];
|
||||
},
|
||||
allTags: (state: BaseStoreState<T>, getters, rootState) => {
|
||||
return rootState.tag.allList.filter(d => d.col === `${state.ns}s`);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getDefaultStoreMutations = <T = any>(): BaseStoreMutations<T> => {
|
||||
return {
|
||||
showDialog: (state: BaseStoreState<T>, key: DialogKey) => {
|
||||
state.activeDialogKey = key;
|
||||
},
|
||||
hideDialog: (state: BaseStoreState<T>) => {
|
||||
// reset all other state variables
|
||||
state.createEditDialogTabName = 'single';
|
||||
state.isSelectiveForm = false;
|
||||
state.selectedFormFields = [];
|
||||
state.formList = [];
|
||||
state.confirmLoading = false;
|
||||
|
||||
// set active dialog key as undefined
|
||||
state.activeDialogKey = undefined;
|
||||
},
|
||||
setCreateEditDialogTabName: (state: BaseStoreState<T>, tabName: CreateEditTabName) => {
|
||||
state.createEditDialogTabName = tabName;
|
||||
},
|
||||
resetCreateEditDialogTabName: (state: BaseStoreState<T>) => {
|
||||
state.createEditDialogTabName = 'single';
|
||||
},
|
||||
setForm: (state: BaseStoreState<T>, value: T) => {
|
||||
state.form = value;
|
||||
},
|
||||
resetForm: (state: BaseStoreState<T>) => {
|
||||
state.form = {} as T;
|
||||
},
|
||||
setIsSelectiveForm: (state: BaseStoreState<T>, value: boolean) => {
|
||||
state.isSelectiveForm = value;
|
||||
},
|
||||
setSelectedFormFields: (state: BaseStoreState<T>, value: string[]) => {
|
||||
state.selectedFormFields = value;
|
||||
},
|
||||
resetSelectedFormFields: (state: BaseStoreState<T>) => {
|
||||
state.selectedFormFields = [];
|
||||
},
|
||||
setReadonlyFormFields: (state: BaseStoreState<T>, value: string[]) => {
|
||||
state.readonlyFormFields = value;
|
||||
},
|
||||
resetReadonlyFormFields: (state: BaseStoreState<T>) => {
|
||||
state.readonlyFormFields = [];
|
||||
},
|
||||
setFormList: (state: BaseStoreState<T>, value: T[]) => {
|
||||
state.formList = value;
|
||||
},
|
||||
resetFormList: (state: BaseStoreState<T>) => {
|
||||
state.formList = [];
|
||||
},
|
||||
setConfirmLoading: (state: BaseStoreState<T>, value: boolean) => {
|
||||
state.confirmLoading = value;
|
||||
},
|
||||
resetTableData: (state: BaseStoreState<T>) => {
|
||||
state.tableData = [];
|
||||
},
|
||||
setTableData: (state: BaseStoreState<T>, payload: TableDataWithTotal<T>) => {
|
||||
const {data, total} = payload;
|
||||
state.tableData = data;
|
||||
state.tableTotal = total;
|
||||
},
|
||||
setTablePagination: (state: BaseStoreState<T>, pagination: TablePagination) => {
|
||||
state.tablePagination = pagination;
|
||||
},
|
||||
resetTablePagination: (state: BaseStoreState<T>) => {
|
||||
state.tablePagination = getDefaultPagination();
|
||||
},
|
||||
setTableListFilter: (state: BaseStoreState<T>, filter: FilterConditionData[]) => {
|
||||
state.tableListFilter = filter;
|
||||
},
|
||||
resetTableListFilter: (state: BaseStoreState<T>) => {
|
||||
state.tableListFilter = [];
|
||||
},
|
||||
setTableListFilterByKey: (state: BaseStoreState<T>, {key, conditions}) => {
|
||||
const filter = state.tableListFilter.filter(d => d.key !== key);
|
||||
conditions.forEach(d => {
|
||||
d.key = key;
|
||||
filter.push(d);
|
||||
});
|
||||
state.tableListFilter = filter;
|
||||
},
|
||||
resetTableListFilterByKey: (state: BaseStoreState<T>, key) => {
|
||||
state.tableListFilter = state.tableListFilter.filter(d => d.key !== key);
|
||||
},
|
||||
setTableListSort: (state: BaseStoreState<T>, sort: SortData[]) => {
|
||||
state.tableListSort = sort;
|
||||
},
|
||||
resetTableListSort: (state: BaseStoreState<T>) => {
|
||||
state.tableListSort = [];
|
||||
},
|
||||
setTableListSortByKey: (state: BaseStoreState<T>, {key, sort}) => {
|
||||
const idx = state.tableListSort.findIndex(d => d.key === key);
|
||||
if (idx === -1) {
|
||||
if (sort) {
|
||||
state.tableListSort.push(sort);
|
||||
}
|
||||
} else {
|
||||
if (sort) {
|
||||
state.tableListSort[idx] = plainClone(sort);
|
||||
} else {
|
||||
state.tableListSort.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
resetTableListSortByKey: (state: BaseStoreState<T>, key) => {
|
||||
state.tableListSort = state.tableListSort.filter(d => d.key !== key);
|
||||
},
|
||||
setAllList: (state: BaseStoreState<T>, value: T[]) => {
|
||||
state.allList = value;
|
||||
},
|
||||
resetAllList: (state: BaseStoreState<T>) => {
|
||||
state.allList = [];
|
||||
},
|
||||
expandSidebar: (state: BaseStoreState<T>) => {
|
||||
state.sidebarCollapsed = false;
|
||||
},
|
||||
collapseSidebar: (state: BaseStoreState<T>) => {
|
||||
state.sidebarCollapsed = true;
|
||||
},
|
||||
expandActions: (state: BaseStoreState<T>) => {
|
||||
state.actionsCollapsed = false;
|
||||
},
|
||||
collapseActions: (state: BaseStoreState<T>) => {
|
||||
state.actionsCollapsed = true;
|
||||
},
|
||||
setAfterSave: (state: BaseStoreState<T>, fnList) => {
|
||||
state.afterSave = fnList;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getDefaultStoreActions = <T = any>(endpoint: string): BaseStoreActions<T> => {
|
||||
const {
|
||||
getById,
|
||||
create,
|
||||
updateById,
|
||||
deleteById,
|
||||
getList,
|
||||
getAll,
|
||||
createList,
|
||||
updateList,
|
||||
deleteList,
|
||||
} = useService<T>(endpoint);
|
||||
|
||||
return {
|
||||
getById: async ({commit}: StoreActionContext<BaseStoreState<T>>, id: string) => {
|
||||
const res = await getById(id);
|
||||
commit('setForm', res.data);
|
||||
return res;
|
||||
},
|
||||
create: async ({commit}: StoreActionContext<BaseStoreState<T>>, form: T) => {
|
||||
const res = await create(form);
|
||||
return res;
|
||||
},
|
||||
updateById: async ({commit}: StoreActionContext<BaseStoreState<T>>, {id, form}: { id: string; form: T }) => {
|
||||
const res = await updateById(id, form);
|
||||
return res;
|
||||
},
|
||||
deleteById: async ({commit}: StoreActionContext<BaseStoreState<T>>, id: string) => {
|
||||
const res = await deleteById(id);
|
||||
return res;
|
||||
},
|
||||
getList: async ({state, commit}: StoreActionContext<BaseStoreState<T>>) => {
|
||||
const {page, size} = state.tablePagination;
|
||||
const res = await getList({
|
||||
page,
|
||||
size,
|
||||
conditions: JSON.stringify(state.tableListFilter),
|
||||
sort: JSON.stringify(state.tableListSort),
|
||||
} as ListRequestParams);
|
||||
commit('setTableData', {data: res.data || [], total: res.total});
|
||||
return res;
|
||||
},
|
||||
getListWithParams: async (_: StoreActionContext<BaseStoreState<T>>, params?: ListRequestParams) => {
|
||||
return await getList(params);
|
||||
},
|
||||
getAllList: async ({commit}: StoreActionContext<BaseStoreState<T>>) => {
|
||||
const res = await getAll();
|
||||
commit('setAllList', res.data || []);
|
||||
return res;
|
||||
},
|
||||
createList: async ({state, commit}: StoreActionContext<BaseStoreState<T>>, data: T[]) => {
|
||||
const res = await createList(data);
|
||||
return res;
|
||||
},
|
||||
updateList: async ({state, commit}: StoreActionContext<BaseStoreState<T>>, {
|
||||
ids,
|
||||
data,
|
||||
fields,
|
||||
}: BatchRequestPayloadWithData) => {
|
||||
const res = await updateList(ids, data, fields);
|
||||
return res;
|
||||
},
|
||||
deleteList: async ({commit}: StoreActionContext<BaseStoreState<T>>, ids: string[]) => {
|
||||
return await deleteList(ids);
|
||||
},
|
||||
};
|
||||
};
|
||||
6
frontend/src/utils/string.ts
Normal file
6
frontend/src/utils/string.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const capitalize = (str: string): string => {
|
||||
if (!str) return '';
|
||||
const arr = str.split('');
|
||||
arr[0] = arr[0].toUpperCase();
|
||||
return arr.join('');
|
||||
};
|
||||
129
frontend/src/utils/table.ts
Normal file
129
frontend/src/utils/table.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import {ElMessageBox} from 'element-plus';
|
||||
import {useStore} from 'vuex';
|
||||
import {useRouter} from 'vue-router';
|
||||
import {ACTION_CANCEL, ACTION_CLONE, ACTION_DELETE, ACTION_EDIT, ACTION_RUN, ACTION_VIEW,} from '@/constants/action';
|
||||
import {TABLE_COLUMN_NAME_ACTIONS} from '@/constants/table';
|
||||
|
||||
export const getDefaultTableDataWithTotal = (): TableDataWithTotal => {
|
||||
return {
|
||||
data: [],
|
||||
total: 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const getTableWidth = (): number | undefined => {
|
||||
const elTable = document.querySelector('.list-layout .table');
|
||||
if (!elTable) return;
|
||||
const style = getComputedStyle(elTable);
|
||||
const widthStr = style.width.replace('px', '');
|
||||
const width = Number(widthStr);
|
||||
if (isNaN(width)) return;
|
||||
return width;
|
||||
};
|
||||
|
||||
export const getColumnWidth = (column: TableColumn): number | undefined => {
|
||||
let width: number;
|
||||
if (typeof column.width === 'string') {
|
||||
width = Number(column.width.replace('px', ''));
|
||||
if (isNaN(width)) return;
|
||||
return width;
|
||||
}
|
||||
{
|
||||
return column.width;
|
||||
}
|
||||
};
|
||||
|
||||
export const getActionColumn = (endpoint: string, ns: ListStoreNamespace, actionNames: TableActionName[]): TableColumn => {
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
|
||||
const column = {
|
||||
key: TABLE_COLUMN_NAME_ACTIONS,
|
||||
label: 'Actions',
|
||||
fixed: 'right',
|
||||
width: '200',
|
||||
buttons: [],
|
||||
} as TableColumn;
|
||||
|
||||
const buttons = typeof column.buttons === 'function' ? column.buttons() : column.buttons as TableColumnButton[];
|
||||
|
||||
actionNames.forEach(name => {
|
||||
if (!buttons) return;
|
||||
switch (name) {
|
||||
case ACTION_VIEW:
|
||||
buttons.push({
|
||||
type: 'primary',
|
||||
icon: ['fa', 'search'],
|
||||
tooltip: 'View',
|
||||
onClick: (row: BaseModel) => {
|
||||
router.push(`${endpoint}/${row._id}`);
|
||||
},
|
||||
});
|
||||
break;
|
||||
case ACTION_EDIT:
|
||||
buttons.push({
|
||||
type: 'warning',
|
||||
icon: ['fa', 'edit'],
|
||||
tooltip: 'Edit',
|
||||
onClick: (row: BaseModel) => {
|
||||
store.commit(`${ns}/setForm`, row);
|
||||
store.commit(`${ns}/showDialog`, 'edit');
|
||||
},
|
||||
},);
|
||||
break;
|
||||
case ACTION_CLONE:
|
||||
buttons.push({
|
||||
type: 'info',
|
||||
size: 'mini',
|
||||
icon: ['fa', 'clone'],
|
||||
tooltip: 'Clone',
|
||||
onClick: (row: BaseModel) => {
|
||||
// TODO: implement
|
||||
console.log('clone', row);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case ACTION_DELETE:
|
||||
buttons.push({
|
||||
type: 'danger',
|
||||
size: 'mini',
|
||||
icon: ['fa', 'trash-alt'],
|
||||
tooltip: 'Delete',
|
||||
onClick: async (row: BaseModel) => {
|
||||
const res = await ElMessageBox.confirm('Are you sure to delete?', 'Delete');
|
||||
if (res) {
|
||||
await store.dispatch(`${ns}/deleteById`, row._id as string);
|
||||
}
|
||||
await store.dispatch(`${ns}/getList`);
|
||||
},
|
||||
});
|
||||
break;
|
||||
case ACTION_RUN:
|
||||
buttons.push({
|
||||
type: 'success',
|
||||
size: 'mini',
|
||||
icon: ['fa', 'play'],
|
||||
tooltip: 'Run',
|
||||
onClick: async (row: BaseModel) => {
|
||||
store.commit(`${ns}/setForm`, row);
|
||||
store.commit(`${ns}/showDialog`, 'run');
|
||||
},
|
||||
});
|
||||
break;
|
||||
case ACTION_CANCEL:
|
||||
buttons.push({
|
||||
type: 'info',
|
||||
size: 'mini',
|
||||
icon: ['fa', 'pause'],
|
||||
tooltip: 'Cancel',
|
||||
onClick: async (row: BaseModel) => {
|
||||
// TODO: implement
|
||||
console.log('cancel', row);
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return column;
|
||||
};
|
||||
49
frontend/src/utils/task.ts
Normal file
49
frontend/src/utils/task.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
TASK_MODE_ALL,
|
||||
TASK_MODE_RANDOM,
|
||||
TASK_MODE_SELECTED_NODE_TAGS,
|
||||
TASK_MODE_SELECTED_NODES,
|
||||
TASK_STATUS_PENDING,
|
||||
TASK_STATUS_RUNNING
|
||||
} from '@/constants/task';
|
||||
|
||||
export const getPriorityLabel = (priority: number): string => {
|
||||
if (priority <= 2) {
|
||||
return `High - ${priority}`;
|
||||
} else if (priority <= 4) {
|
||||
return `Higher - ${priority}`;
|
||||
} else if (priority <= 6) {
|
||||
return `Medium - ${priority}`;
|
||||
} else if (priority <= 8) {
|
||||
return `Lower - ${priority}`;
|
||||
} else {
|
||||
return `Low - ${priority}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const isCancellable = (status: TaskStatus): boolean => {
|
||||
switch (status) {
|
||||
case TASK_STATUS_PENDING:
|
||||
case TASK_STATUS_RUNNING:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getModeOptions = (): SelectOption[] => {
|
||||
return [
|
||||
{value: TASK_MODE_RANDOM, label: 'Random Node'},
|
||||
{value: TASK_MODE_ALL, label: 'All Nodes'},
|
||||
{value: TASK_MODE_SELECTED_NODES, label: 'Selected Nodes'},
|
||||
{value: TASK_MODE_SELECTED_NODE_TAGS, label: 'Selected Tags'},
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
export const getModeOptionsDict = (): Map<string, SelectOption> => {
|
||||
const modeOptions = getModeOptions();
|
||||
const dict = new Map<string, SelectOption>();
|
||||
modeOptions.forEach(op => dict.set(op.value, op));
|
||||
return dict;
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
import i18n from '../i18n'
|
||||
import store from '../store'
|
||||
import stats from './stats'
|
||||
|
||||
export default {
|
||||
isFinishedTour: (tourName) => {
|
||||
if (!localStorage.getItem('tour')) {
|
||||
localStorage.setItem('tour', JSON.stringify({}))
|
||||
return false
|
||||
}
|
||||
|
||||
let data
|
||||
try {
|
||||
data = JSON.parse(localStorage.getItem('tour'))
|
||||
} catch (e) {
|
||||
localStorage.setItem('tour', JSON.stringify({}))
|
||||
return false
|
||||
}
|
||||
return !!data[tourName]
|
||||
},
|
||||
startTour: (vm, tourName) => {
|
||||
if (localStorage.getItem('enableTutorial') === '0') return
|
||||
vm.$tours[tourName].start()
|
||||
vm.$st.sendEv('教程', '开始', tourName)
|
||||
},
|
||||
finishTour: (tourName) => {
|
||||
let data
|
||||
try {
|
||||
data = JSON.parse(localStorage.getItem('tour'))
|
||||
} catch (e) {
|
||||
localStorage.setItem('tour', JSON.stringify({}))
|
||||
data = {}
|
||||
}
|
||||
data[tourName] = 1
|
||||
localStorage.setItem('tour', JSON.stringify(data))
|
||||
|
||||
// 发送统计数据
|
||||
const finalStep = store.state.tour.tourFinishSteps[tourName]
|
||||
const currentStep = store.state.tour.tourSteps[tourName]
|
||||
if (currentStep === finalStep) {
|
||||
stats.sendEv('教程', '完成', tourName)
|
||||
} else {
|
||||
stats.sendEv('教程', '跳过', tourName)
|
||||
}
|
||||
},
|
||||
nextStep: (tourName, currentStep) => {
|
||||
store.commit('tour/SET_TOUR_STEP', {
|
||||
tourName,
|
||||
step: currentStep + 1
|
||||
})
|
||||
stats.sendEv('教程', '下一步', tourName)
|
||||
},
|
||||
prevStep: (tourName, currentStep) => {
|
||||
store.commit('tour/SET_TOUR_STEP', {
|
||||
tourName,
|
||||
step: currentStep - 1
|
||||
})
|
||||
stats.sendEv('教程', '上一步', tourName)
|
||||
},
|
||||
getOptions: (isShowHighlight) => {
|
||||
return {
|
||||
labels: {
|
||||
buttonSkip: i18n.t('Skip'),
|
||||
buttonPrevious: i18n.t('Previous'),
|
||||
buttonNext: i18n.t('Next'),
|
||||
buttonStop: i18n.t('Finish')
|
||||
},
|
||||
highlight: isShowHighlight
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* Created by jiachenpan on 16/11/18.
|
||||
*/
|
||||
|
||||
export function isValidUsername(str) {
|
||||
if (!str) return false
|
||||
if (str.length > 100) return false
|
||||
return true
|
||||
// const validMap = ['admin', 'editor']
|
||||
// return validMap.indexOf(str.trim()) >= 0
|
||||
}
|
||||
|
||||
export function isExternal(path) {
|
||||
return /^(https?:|mailto:|tel:)/.test(path)
|
||||
}
|
||||
11
frontend/src/utils/validate.ts
Normal file
11
frontend/src/utils/validate.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const isValidUsername = (str: string): boolean => {
|
||||
if (!str) return false;
|
||||
if (str.length > 100) return false;
|
||||
return true;
|
||||
// const validMap = ['admin', 'editor']
|
||||
// return validMap.indexOf(str.trim()) >= 0
|
||||
};
|
||||
|
||||
export const isExternal = (path: string) => {
|
||||
return /^(https?:|mailto:|tel:)/.test(path);
|
||||
};
|
||||
Reference in New Issue
Block a user