Import x-panel source
This commit is contained in:
BIN
web/assets/Vazirmatn-UI-NL-Regular.woff2
Normal file
BIN
web/assets/Vazirmatn-UI-NL-Regular.woff2
Normal file
Binary file not shown.
8
web/assets/ant-design-vue/antd.min.css
vendored
Normal file
8
web/assets/ant-design-vue/antd.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
web/assets/ant-design-vue/antd.min.js
vendored
Normal file
3
web/assets/ant-design-vue/antd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
web/assets/axios/axios.min.js
vendored
Normal file
3
web/assets/axios/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/assets/codemirror/codemirror.min.css
vendored
Normal file
1
web/assets/codemirror/codemirror.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/assets/codemirror/codemirror.min.js
vendored
Normal file
1
web/assets/codemirror/codemirror.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
119
web/assets/codemirror/fold/brace-fold.js
Normal file
119
web/assets/codemirror/fold/brace-fold.js
Normal file
@@ -0,0 +1,119 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
function bracketFolding(pairs) {
|
||||
return function(cm, start) {
|
||||
var line = start.line, lineText = cm.getLine(line);
|
||||
|
||||
function findOpening(pair) {
|
||||
var tokenType;
|
||||
for (var at = start.ch, pass = 0;;) {
|
||||
var found = at <= 0 ? -1 : lineText.lastIndexOf(pair[0], at - 1);
|
||||
if (found == -1) {
|
||||
if (pass == 1) break;
|
||||
pass = 1;
|
||||
at = lineText.length;
|
||||
continue;
|
||||
}
|
||||
if (pass == 1 && found < start.ch) break;
|
||||
tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1));
|
||||
if (!/^(comment|string)/.test(tokenType)) return {ch: found + 1, tokenType: tokenType, pair: pair};
|
||||
at = found - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function findRange(found) {
|
||||
var count = 1, lastLine = cm.lastLine(), end, startCh = found.ch, endCh
|
||||
outer: for (var i = line; i <= lastLine; ++i) {
|
||||
var text = cm.getLine(i), pos = i == line ? startCh : 0;
|
||||
for (;;) {
|
||||
var nextOpen = text.indexOf(found.pair[0], pos), nextClose = text.indexOf(found.pair[1], pos);
|
||||
if (nextOpen < 0) nextOpen = text.length;
|
||||
if (nextClose < 0) nextClose = text.length;
|
||||
pos = Math.min(nextOpen, nextClose);
|
||||
if (pos == text.length) break;
|
||||
if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == found.tokenType) {
|
||||
if (pos == nextOpen) ++count;
|
||||
else if (!--count) { end = i; endCh = pos; break outer; }
|
||||
}
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
if (end == null || line == end) return null
|
||||
return {from: CodeMirror.Pos(line, startCh),
|
||||
to: CodeMirror.Pos(end, endCh)};
|
||||
}
|
||||
|
||||
var found = []
|
||||
for (var i = 0; i < pairs.length; i++) {
|
||||
var open = findOpening(pairs[i])
|
||||
if (open) found.push(open)
|
||||
}
|
||||
found.sort(function(a, b) { return a.ch - b.ch })
|
||||
for (var i = 0; i < found.length; i++) {
|
||||
var range = findRange(found[i])
|
||||
if (range) return range
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
CodeMirror.registerHelper("fold", "brace", bracketFolding([["{", "}"], ["[", "]"]]));
|
||||
|
||||
CodeMirror.registerHelper("fold", "brace-paren", bracketFolding([["{", "}"], ["[", "]"], ["(", ")"]]));
|
||||
|
||||
CodeMirror.registerHelper("fold", "import", function(cm, start) {
|
||||
function hasImport(line) {
|
||||
if (line < cm.firstLine() || line > cm.lastLine()) return null;
|
||||
var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
|
||||
if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
|
||||
if (start.type != "keyword" || start.string != "import") return null;
|
||||
// Now find closing semicolon, return its position
|
||||
for (var i = line, e = Math.min(cm.lastLine(), line + 10); i <= e; ++i) {
|
||||
var text = cm.getLine(i), semi = text.indexOf(";");
|
||||
if (semi != -1) return {startCh: start.end, end: CodeMirror.Pos(i, semi)};
|
||||
}
|
||||
}
|
||||
|
||||
var startLine = start.line, has = hasImport(startLine), prev;
|
||||
if (!has || hasImport(startLine - 1) || ((prev = hasImport(startLine - 2)) && prev.end.line == startLine - 1))
|
||||
return null;
|
||||
for (var end = has.end;;) {
|
||||
var next = hasImport(end.line + 1);
|
||||
if (next == null) break;
|
||||
end = next.end;
|
||||
}
|
||||
return {from: cm.clipPos(CodeMirror.Pos(startLine, has.startCh + 1)), to: end};
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper("fold", "include", function(cm, start) {
|
||||
function hasInclude(line) {
|
||||
if (line < cm.firstLine() || line > cm.lastLine()) return null;
|
||||
var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
|
||||
if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
|
||||
if (start.type == "meta" && start.string.slice(0, 8) == "#include") return start.start + 8;
|
||||
}
|
||||
|
||||
var startLine = start.line, has = hasInclude(startLine);
|
||||
if (has == null || hasInclude(startLine - 1) != null) return null;
|
||||
for (var end = startLine;;) {
|
||||
var next = hasInclude(end + 1);
|
||||
if (next == null) break;
|
||||
++end;
|
||||
}
|
||||
return {from: CodeMirror.Pos(startLine, has + 1),
|
||||
to: cm.clipPos(CodeMirror.Pos(end))};
|
||||
});
|
||||
|
||||
});
|
||||
159
web/assets/codemirror/fold/foldcode.js
Normal file
159
web/assets/codemirror/fold/foldcode.js
Normal file
@@ -0,0 +1,159 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
function doFold(cm, pos, options, force) {
|
||||
if (options && options.call) {
|
||||
var finder = options;
|
||||
options = null;
|
||||
} else {
|
||||
var finder = getOption(cm, options, "rangeFinder");
|
||||
}
|
||||
if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0);
|
||||
var minSize = getOption(cm, options, "minFoldSize");
|
||||
|
||||
function getRange(allowFolded) {
|
||||
var range = finder(cm, pos);
|
||||
if (!range || range.to.line - range.from.line < minSize) return null;
|
||||
if (force === "fold") return range;
|
||||
|
||||
var marks = cm.findMarksAt(range.from);
|
||||
for (var i = 0; i < marks.length; ++i) {
|
||||
if (marks[i].__isFold) {
|
||||
if (!allowFolded) return null;
|
||||
range.cleared = true;
|
||||
marks[i].clear();
|
||||
}
|
||||
}
|
||||
return range;
|
||||
}
|
||||
|
||||
var range = getRange(true);
|
||||
if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) {
|
||||
pos = CodeMirror.Pos(pos.line - 1, 0);
|
||||
range = getRange(false);
|
||||
}
|
||||
if (!range || range.cleared || force === "unfold") return;
|
||||
|
||||
var myWidget = makeWidget(cm, options, range);
|
||||
CodeMirror.on(myWidget, "mousedown", function(e) {
|
||||
myRange.clear();
|
||||
CodeMirror.e_preventDefault(e);
|
||||
});
|
||||
var myRange = cm.markText(range.from, range.to, {
|
||||
replacedWith: myWidget,
|
||||
clearOnEnter: getOption(cm, options, "clearOnEnter"),
|
||||
__isFold: true
|
||||
});
|
||||
myRange.on("clear", function(from, to) {
|
||||
CodeMirror.signal(cm, "unfold", cm, from, to);
|
||||
});
|
||||
CodeMirror.signal(cm, "fold", cm, range.from, range.to);
|
||||
}
|
||||
|
||||
function makeWidget(cm, options, range) {
|
||||
var widget = getOption(cm, options, "widget");
|
||||
|
||||
if (typeof widget == "function") {
|
||||
widget = widget(range.from, range.to);
|
||||
}
|
||||
|
||||
if (typeof widget == "string") {
|
||||
var text = document.createTextNode(widget);
|
||||
widget = document.createElement("span");
|
||||
widget.appendChild(text);
|
||||
widget.className = "CodeMirror-foldmarker";
|
||||
} else if (widget) {
|
||||
widget = widget.cloneNode(true)
|
||||
}
|
||||
return widget;
|
||||
}
|
||||
|
||||
// Clumsy backwards-compatible interface
|
||||
CodeMirror.newFoldFunction = function(rangeFinder, widget) {
|
||||
return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); };
|
||||
};
|
||||
|
||||
// New-style interface
|
||||
CodeMirror.defineExtension("foldCode", function(pos, options, force) {
|
||||
doFold(this, pos, options, force);
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("isFolded", function(pos) {
|
||||
var marks = this.findMarksAt(pos);
|
||||
for (var i = 0; i < marks.length; ++i)
|
||||
if (marks[i].__isFold) return true;
|
||||
});
|
||||
|
||||
CodeMirror.commands.toggleFold = function(cm) {
|
||||
cm.foldCode(cm.getCursor());
|
||||
};
|
||||
CodeMirror.commands.fold = function(cm) {
|
||||
cm.foldCode(cm.getCursor(), null, "fold");
|
||||
};
|
||||
CodeMirror.commands.unfold = function(cm) {
|
||||
cm.foldCode(cm.getCursor(), { scanUp: false }, "unfold");
|
||||
};
|
||||
CodeMirror.commands.foldAll = function(cm) {
|
||||
cm.operation(function() {
|
||||
for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
|
||||
cm.foldCode(CodeMirror.Pos(i, 0), { scanUp: false }, "fold");
|
||||
});
|
||||
};
|
||||
CodeMirror.commands.unfoldAll = function(cm) {
|
||||
cm.operation(function() {
|
||||
for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
|
||||
cm.foldCode(CodeMirror.Pos(i, 0), { scanUp: false }, "unfold");
|
||||
});
|
||||
};
|
||||
|
||||
CodeMirror.registerHelper("fold", "combine", function() {
|
||||
var funcs = Array.prototype.slice.call(arguments, 0);
|
||||
return function(cm, start) {
|
||||
for (var i = 0; i < funcs.length; ++i) {
|
||||
var found = funcs[i](cm, start);
|
||||
if (found) return found;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper("fold", "auto", function(cm, start) {
|
||||
var helpers = cm.getHelpers(start, "fold");
|
||||
for (var i = 0; i < helpers.length; i++) {
|
||||
var cur = helpers[i](cm, start);
|
||||
if (cur) return cur;
|
||||
}
|
||||
});
|
||||
|
||||
var defaultOptions = {
|
||||
rangeFinder: CodeMirror.fold.auto,
|
||||
widget: "\u2194",
|
||||
minFoldSize: 0,
|
||||
scanUp: false,
|
||||
clearOnEnter: true
|
||||
};
|
||||
|
||||
CodeMirror.defineOption("foldOptions", null);
|
||||
|
||||
function getOption(cm, options, name) {
|
||||
if (options && options[name] !== undefined)
|
||||
return options[name];
|
||||
var editorOptions = cm.options.foldOptions;
|
||||
if (editorOptions && editorOptions[name] !== undefined)
|
||||
return editorOptions[name];
|
||||
return defaultOptions[name];
|
||||
}
|
||||
|
||||
CodeMirror.defineExtension("foldOption", function(options, name) {
|
||||
return getOption(this, options, name);
|
||||
});
|
||||
});
|
||||
20
web/assets/codemirror/fold/foldgutter.css
Normal file
20
web/assets/codemirror/fold/foldgutter.css
Normal file
@@ -0,0 +1,20 @@
|
||||
.CodeMirror-foldmarker {
|
||||
color: blue;
|
||||
text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px;
|
||||
font-family: arial;
|
||||
line-height: .3;
|
||||
cursor: pointer;
|
||||
}
|
||||
.CodeMirror-foldgutter {
|
||||
width: .7em;
|
||||
}
|
||||
.CodeMirror-foldgutter-open,
|
||||
.CodeMirror-foldgutter-folded {
|
||||
cursor: pointer;
|
||||
}
|
||||
.CodeMirror-foldgutter-open:after {
|
||||
content: "\25BE";
|
||||
}
|
||||
.CodeMirror-foldgutter-folded:after {
|
||||
content: "\25B8";
|
||||
}
|
||||
169
web/assets/codemirror/fold/foldgutter.js
Normal file
169
web/assets/codemirror/fold/foldgutter.js
Normal file
@@ -0,0 +1,169 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"), require("./foldcode"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror", "./foldcode"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
CodeMirror.defineOption("foldGutter", false, function(cm, val, old) {
|
||||
if (old && old != CodeMirror.Init) {
|
||||
cm.clearGutter(cm.state.foldGutter.options.gutter);
|
||||
cm.state.foldGutter = null;
|
||||
cm.off("gutterClick", onGutterClick);
|
||||
cm.off("changes", onChange);
|
||||
cm.off("viewportChange", onViewportChange);
|
||||
cm.off("fold", onFold);
|
||||
cm.off("unfold", onFold);
|
||||
cm.off("swapDoc", onChange);
|
||||
cm.off("optionChange", optionChange);
|
||||
}
|
||||
if (val) {
|
||||
cm.state.foldGutter = new State(parseOptions(val));
|
||||
updateInViewport(cm);
|
||||
cm.on("gutterClick", onGutterClick);
|
||||
cm.on("changes", onChange);
|
||||
cm.on("viewportChange", onViewportChange);
|
||||
cm.on("fold", onFold);
|
||||
cm.on("unfold", onFold);
|
||||
cm.on("swapDoc", onChange);
|
||||
cm.on("optionChange", optionChange);
|
||||
}
|
||||
});
|
||||
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
function State(options) {
|
||||
this.options = options;
|
||||
this.from = this.to = 0;
|
||||
}
|
||||
|
||||
function parseOptions(opts) {
|
||||
if (opts === true) opts = {};
|
||||
if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter";
|
||||
if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open";
|
||||
if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded";
|
||||
return opts;
|
||||
}
|
||||
|
||||
function isFolded(cm, line) {
|
||||
var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0));
|
||||
for (var i = 0; i < marks.length; ++i) {
|
||||
if (marks[i].__isFold) {
|
||||
var fromPos = marks[i].find(-1);
|
||||
if (fromPos && fromPos.line === line)
|
||||
return marks[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function marker(spec) {
|
||||
if (typeof spec == "string") {
|
||||
var elt = document.createElement("div");
|
||||
elt.className = spec + " CodeMirror-guttermarker-subtle";
|
||||
return elt;
|
||||
} else {
|
||||
return spec.cloneNode(true);
|
||||
}
|
||||
}
|
||||
|
||||
function updateFoldInfo(cm, from, to) {
|
||||
var opts = cm.state.foldGutter.options, cur = from - 1;
|
||||
var minSize = cm.foldOption(opts, "minFoldSize");
|
||||
var func = cm.foldOption(opts, "rangeFinder");
|
||||
// we can reuse the built-in indicator element if its className matches the new state
|
||||
var clsFolded = typeof opts.indicatorFolded == "string" && classTest(opts.indicatorFolded);
|
||||
var clsOpen = typeof opts.indicatorOpen == "string" && classTest(opts.indicatorOpen);
|
||||
cm.eachLine(from, to, function(line) {
|
||||
++cur;
|
||||
var mark = null;
|
||||
var old = line.gutterMarkers;
|
||||
if (old) old = old[opts.gutter];
|
||||
if (isFolded(cm, cur)) {
|
||||
if (clsFolded && old && clsFolded.test(old.className)) return;
|
||||
mark = marker(opts.indicatorFolded);
|
||||
} else {
|
||||
var pos = Pos(cur, 0);
|
||||
var range = func && func(cm, pos);
|
||||
if (range && range.to.line - range.from.line >= minSize) {
|
||||
if (clsOpen && old && clsOpen.test(old.className)) return;
|
||||
mark = marker(opts.indicatorOpen);
|
||||
}
|
||||
}
|
||||
if (!mark && !old) return;
|
||||
cm.setGutterMarker(line, opts.gutter, mark);
|
||||
});
|
||||
}
|
||||
|
||||
// copied from CodeMirror/src/util/dom.js
|
||||
function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") }
|
||||
|
||||
function updateInViewport(cm) {
|
||||
var vp = cm.getViewport(), state = cm.state.foldGutter;
|
||||
if (!state) return;
|
||||
cm.operation(function() {
|
||||
updateFoldInfo(cm, vp.from, vp.to);
|
||||
});
|
||||
state.from = vp.from; state.to = vp.to;
|
||||
}
|
||||
|
||||
function onGutterClick(cm, line, gutter) {
|
||||
var state = cm.state.foldGutter;
|
||||
if (!state) return;
|
||||
var opts = state.options;
|
||||
if (gutter != opts.gutter) return;
|
||||
var folded = isFolded(cm, line);
|
||||
if (folded) folded.clear();
|
||||
else cm.foldCode(Pos(line, 0), opts);
|
||||
}
|
||||
|
||||
function optionChange(cm, option) {
|
||||
if (option == "mode") onChange(cm)
|
||||
}
|
||||
|
||||
function onChange(cm) {
|
||||
var state = cm.state.foldGutter;
|
||||
if (!state) return;
|
||||
var opts = state.options;
|
||||
state.from = state.to = 0;
|
||||
clearTimeout(state.changeUpdate);
|
||||
state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600);
|
||||
}
|
||||
|
||||
function onViewportChange(cm) {
|
||||
var state = cm.state.foldGutter;
|
||||
if (!state) return;
|
||||
var opts = state.options;
|
||||
clearTimeout(state.changeUpdate);
|
||||
state.changeUpdate = setTimeout(function() {
|
||||
var vp = cm.getViewport();
|
||||
if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) {
|
||||
updateInViewport(cm);
|
||||
} else {
|
||||
cm.operation(function() {
|
||||
if (vp.from < state.from) {
|
||||
updateFoldInfo(cm, vp.from, state.from);
|
||||
state.from = vp.from;
|
||||
}
|
||||
if (vp.to > state.to) {
|
||||
updateFoldInfo(cm, state.to, vp.to);
|
||||
state.to = vp.to;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, opts.updateViewportTimeSpan || 400);
|
||||
}
|
||||
|
||||
function onFold(cm, from) {
|
||||
var state = cm.state.foldGutter;
|
||||
if (!state) return;
|
||||
var line = from.line;
|
||||
if (line >= state.from && line < state.to)
|
||||
updateFoldInfo(cm, line, line + 1);
|
||||
}
|
||||
});
|
||||
162
web/assets/codemirror/hint/javascript-hint.js
Normal file
162
web/assets/codemirror/hint/javascript-hint.js
Normal file
@@ -0,0 +1,162 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
function forEach(arr, f) {
|
||||
for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]);
|
||||
}
|
||||
|
||||
function arrayContains(arr, item) {
|
||||
if (!Array.prototype.indexOf) {
|
||||
var i = arr.length;
|
||||
while (i--) {
|
||||
if (arr[i] === item) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return arr.indexOf(item) != -1;
|
||||
}
|
||||
|
||||
function scriptHint(editor, keywords, getToken, options) {
|
||||
// Find the token at the cursor
|
||||
var cur = editor.getCursor(), token = getToken(editor, cur);
|
||||
if (/\b(?:string|comment)\b/.test(token.type)) return;
|
||||
var innerMode = CodeMirror.innerMode(editor.getMode(), token.state);
|
||||
if (innerMode.mode.helperType === "json") return;
|
||||
token.state = innerMode.state;
|
||||
|
||||
// If it's not a 'word-style' token, ignore the token.
|
||||
if (!/^[\w$_]*$/.test(token.string)) {
|
||||
token = {start: cur.ch, end: cur.ch, string: "", state: token.state,
|
||||
type: token.string == "." ? "property" : null};
|
||||
} else if (token.end > cur.ch) {
|
||||
token.end = cur.ch;
|
||||
token.string = token.string.slice(0, cur.ch - token.start);
|
||||
}
|
||||
|
||||
var tprop = token;
|
||||
// If it is a property, find out what it is a property of.
|
||||
while (tprop.type == "property") {
|
||||
tprop = getToken(editor, Pos(cur.line, tprop.start));
|
||||
if (tprop.string != ".") return;
|
||||
tprop = getToken(editor, Pos(cur.line, tprop.start));
|
||||
if (!context) var context = [];
|
||||
context.push(tprop);
|
||||
}
|
||||
return {list: getCompletions(token, context, keywords, options),
|
||||
from: Pos(cur.line, token.start),
|
||||
to: Pos(cur.line, token.end)};
|
||||
}
|
||||
|
||||
function javascriptHint(editor, options) {
|
||||
return scriptHint(editor, javascriptKeywords,
|
||||
function (e, cur) {return e.getTokenAt(cur);},
|
||||
options);
|
||||
}
|
||||
CodeMirror.registerHelper("hint", "javascript", javascriptHint);
|
||||
|
||||
function getCoffeeScriptToken(editor, cur) {
|
||||
// This getToken, it is for coffeescript, imitates the behavior of
|
||||
// getTokenAt method in javascript.js, that is, returning "property"
|
||||
// type and treat "." as independent token.
|
||||
var token = editor.getTokenAt(cur);
|
||||
if (cur.ch == token.start + 1 && token.string.charAt(0) == '.') {
|
||||
token.end = token.start;
|
||||
token.string = '.';
|
||||
token.type = "property";
|
||||
}
|
||||
else if (/^\.[\w$_]*$/.test(token.string)) {
|
||||
token.type = "property";
|
||||
token.start++;
|
||||
token.string = token.string.replace(/\./, '');
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
function coffeescriptHint(editor, options) {
|
||||
return scriptHint(editor, coffeescriptKeywords, getCoffeeScriptToken, options);
|
||||
}
|
||||
CodeMirror.registerHelper("hint", "coffeescript", coffeescriptHint);
|
||||
|
||||
var stringProps = ("charAt charCodeAt indexOf lastIndexOf substring substr slice trim trimLeft trimRight " +
|
||||
"toUpperCase toLowerCase split concat match replace search").split(" ");
|
||||
var arrayProps = ("length concat join splice push pop shift unshift slice reverse sort indexOf " +
|
||||
"lastIndexOf every some filter forEach map reduce reduceRight ").split(" ");
|
||||
var funcProps = "prototype apply call bind".split(" ");
|
||||
var javascriptKeywords = ("break case catch class const continue debugger default delete do else export extends false finally for function " +
|
||||
"if in import instanceof new null return super switch this throw true try typeof var void while with yield").split(" ");
|
||||
var coffeescriptKeywords = ("and break catch class continue delete do else extends false finally for " +
|
||||
"if in instanceof isnt new no not null of off on or return switch then throw true try typeof until void while with yes").split(" ");
|
||||
|
||||
function forAllProps(obj, callback) {
|
||||
if (!Object.getOwnPropertyNames || !Object.getPrototypeOf) {
|
||||
for (var name in obj) callback(name)
|
||||
} else {
|
||||
for (var o = obj; o; o = Object.getPrototypeOf(o))
|
||||
Object.getOwnPropertyNames(o).forEach(callback)
|
||||
}
|
||||
}
|
||||
|
||||
function getCompletions(token, context, keywords, options) {
|
||||
var found = [], start = token.string, global = options && options.globalScope || window;
|
||||
function maybeAdd(str) {
|
||||
if (str.lastIndexOf(start, 0) == 0 && !arrayContains(found, str)) found.push(str);
|
||||
}
|
||||
function gatherCompletions(obj) {
|
||||
if (typeof obj == "string") forEach(stringProps, maybeAdd);
|
||||
else if (obj instanceof Array) forEach(arrayProps, maybeAdd);
|
||||
else if (obj instanceof Function) forEach(funcProps, maybeAdd);
|
||||
forAllProps(obj, maybeAdd)
|
||||
}
|
||||
|
||||
if (context && context.length) {
|
||||
// If this is a property, see if it belongs to some object we can
|
||||
// find in the current environment.
|
||||
var obj = context.pop(), base;
|
||||
if (obj.type && obj.type.indexOf("variable") === 0) {
|
||||
if (options && options.additionalContext)
|
||||
base = options.additionalContext[obj.string];
|
||||
if (!options || options.useGlobalScope !== false)
|
||||
base = base || global[obj.string];
|
||||
} else if (obj.type == "string") {
|
||||
base = "";
|
||||
} else if (obj.type == "atom") {
|
||||
base = 1;
|
||||
} else if (obj.type == "function") {
|
||||
if (global.jQuery != null && (obj.string == '$' || obj.string == 'jQuery') &&
|
||||
(typeof global.jQuery == 'function'))
|
||||
base = global.jQuery();
|
||||
else if (global._ != null && (obj.string == '_') && (typeof global._ == 'function'))
|
||||
base = global._();
|
||||
}
|
||||
while (base != null && context.length)
|
||||
base = base[context.pop().string];
|
||||
if (base != null) gatherCompletions(base);
|
||||
} else {
|
||||
// If not, just look in the global object, any local scope, and optional additional-context
|
||||
// (reading into JS mode internals to get at the local and global variables)
|
||||
for (var v = token.state.localVars; v; v = v.next) maybeAdd(v.name);
|
||||
for (var c = token.state.context; c; c = c.prev)
|
||||
for (var v = c.vars; v; v = v.next) maybeAdd(v.name)
|
||||
for (var v = token.state.globalVars; v; v = v.next) maybeAdd(v.name);
|
||||
if (options && options.additionalContext != null)
|
||||
for (var key in options.additionalContext)
|
||||
maybeAdd(key);
|
||||
if (!options || options.useGlobalScope !== false)
|
||||
gatherCompletions(global);
|
||||
forEach(keywords, maybeAdd);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
});
|
||||
960
web/assets/codemirror/javascript.js
Normal file
960
web/assets/codemirror/javascript.js
Normal file
@@ -0,0 +1,960 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
var indentUnit = config.indentUnit;
|
||||
var statementIndent = parserConfig.statementIndent;
|
||||
var jsonldMode = parserConfig.jsonld;
|
||||
var jsonMode = parserConfig.json || jsonldMode;
|
||||
var trackScope = parserConfig.trackScope !== false
|
||||
var isTS = parserConfig.typescript;
|
||||
var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/;
|
||||
|
||||
// Tokenizer
|
||||
|
||||
var keywords = function(){
|
||||
function kw(type) {return {type: type, style: "keyword"};}
|
||||
var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d");
|
||||
var operator = kw("operator"), atom = {type: "atom", style: "atom"};
|
||||
|
||||
return {
|
||||
"if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
|
||||
"return": D, "break": D, "continue": D, "new": kw("new"), "delete": C, "void": C, "throw": C,
|
||||
"debugger": kw("debugger"), "var": kw("var"), "const": kw("var"), "let": kw("var"),
|
||||
"function": kw("function"), "catch": kw("catch"),
|
||||
"for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"),
|
||||
"in": operator, "typeof": operator, "instanceof": operator,
|
||||
"true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom,
|
||||
"this": kw("this"), "class": kw("class"), "super": kw("atom"),
|
||||
"yield": C, "export": kw("export"), "import": kw("import"), "extends": C,
|
||||
"await": C
|
||||
};
|
||||
}();
|
||||
|
||||
var isOperatorChar = /[+\-*&%=<>!?|~^@]/;
|
||||
var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/;
|
||||
|
||||
function readRegexp(stream) {
|
||||
var escaped = false, next, inSet = false;
|
||||
while ((next = stream.next()) != null) {
|
||||
if (!escaped) {
|
||||
if (next == "/" && !inSet) return;
|
||||
if (next == "[") inSet = true;
|
||||
else if (inSet && next == "]") inSet = false;
|
||||
}
|
||||
escaped = !escaped && next == "\\";
|
||||
}
|
||||
}
|
||||
|
||||
// Used as scratch variables to communicate multiple values without
|
||||
// consing up tons of objects.
|
||||
var type, content;
|
||||
function ret(tp, style, cont) {
|
||||
type = tp; content = cont;
|
||||
return style;
|
||||
}
|
||||
function tokenBase(stream, state) {
|
||||
var ch = stream.next();
|
||||
if (ch == '"' || ch == "'") {
|
||||
state.tokenize = tokenString(ch);
|
||||
return state.tokenize(stream, state);
|
||||
} else if (ch == "." && stream.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/)) {
|
||||
return ret("number", "number");
|
||||
} else if (ch == "." && stream.match("..")) {
|
||||
return ret("spread", "meta");
|
||||
} else if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
|
||||
return ret(ch);
|
||||
} else if (ch == "=" && stream.eat(">")) {
|
||||
return ret("=>", "operator");
|
||||
} else if (ch == "0" && stream.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) {
|
||||
return ret("number", "number");
|
||||
} else if (/\d/.test(ch)) {
|
||||
stream.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/);
|
||||
return ret("number", "number");
|
||||
} else if (ch == "/") {
|
||||
if (stream.eat("*")) {
|
||||
state.tokenize = tokenComment;
|
||||
return tokenComment(stream, state);
|
||||
} else if (stream.eat("/")) {
|
||||
stream.skipToEnd();
|
||||
return ret("comment", "comment");
|
||||
} else if (expressionAllowed(stream, state, 1)) {
|
||||
readRegexp(stream);
|
||||
stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/);
|
||||
return ret("regexp", "string-2");
|
||||
} else {
|
||||
stream.eat("=");
|
||||
return ret("operator", "operator", stream.current());
|
||||
}
|
||||
} else if (ch == "`") {
|
||||
state.tokenize = tokenQuasi;
|
||||
return tokenQuasi(stream, state);
|
||||
} else if (ch == "#" && stream.peek() == "!") {
|
||||
stream.skipToEnd();
|
||||
return ret("meta", "meta");
|
||||
} else if (ch == "#" && stream.eatWhile(wordRE)) {
|
||||
return ret("variable", "property")
|
||||
} else if (ch == "<" && stream.match("!--") ||
|
||||
(ch == "-" && stream.match("->") && !/\S/.test(stream.string.slice(0, stream.start)))) {
|
||||
stream.skipToEnd()
|
||||
return ret("comment", "comment")
|
||||
} else if (isOperatorChar.test(ch)) {
|
||||
if (ch != ">" || !state.lexical || state.lexical.type != ">") {
|
||||
if (stream.eat("=")) {
|
||||
if (ch == "!" || ch == "=") stream.eat("=")
|
||||
} else if (/[<>*+\-|&?]/.test(ch)) {
|
||||
stream.eat(ch)
|
||||
if (ch == ">") stream.eat(ch)
|
||||
}
|
||||
}
|
||||
if (ch == "?" && stream.eat(".")) return ret(".")
|
||||
return ret("operator", "operator", stream.current());
|
||||
} else if (wordRE.test(ch)) {
|
||||
stream.eatWhile(wordRE);
|
||||
var word = stream.current()
|
||||
if (state.lastType != ".") {
|
||||
if (keywords.propertyIsEnumerable(word)) {
|
||||
var kw = keywords[word]
|
||||
return ret(kw.type, kw.style, word)
|
||||
}
|
||||
if (word == "async" && stream.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/, false))
|
||||
return ret("async", "keyword", word)
|
||||
}
|
||||
return ret("variable", "variable", word)
|
||||
}
|
||||
}
|
||||
|
||||
function tokenString(quote) {
|
||||
return function(stream, state) {
|
||||
var escaped = false, next;
|
||||
if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){
|
||||
state.tokenize = tokenBase;
|
||||
return ret("jsonld-keyword", "meta");
|
||||
}
|
||||
while ((next = stream.next()) != null) {
|
||||
if (next == quote && !escaped) break;
|
||||
escaped = !escaped && next == "\\";
|
||||
}
|
||||
if (!escaped) state.tokenize = tokenBase;
|
||||
return ret("string", "string");
|
||||
};
|
||||
}
|
||||
|
||||
function tokenComment(stream, state) {
|
||||
var maybeEnd = false, ch;
|
||||
while (ch = stream.next()) {
|
||||
if (ch == "/" && maybeEnd) {
|
||||
state.tokenize = tokenBase;
|
||||
break;
|
||||
}
|
||||
maybeEnd = (ch == "*");
|
||||
}
|
||||
return ret("comment", "comment");
|
||||
}
|
||||
|
||||
function tokenQuasi(stream, state) {
|
||||
var escaped = false, next;
|
||||
while ((next = stream.next()) != null) {
|
||||
if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) {
|
||||
state.tokenize = tokenBase;
|
||||
break;
|
||||
}
|
||||
escaped = !escaped && next == "\\";
|
||||
}
|
||||
return ret("quasi", "string-2", stream.current());
|
||||
}
|
||||
|
||||
var brackets = "([{}])";
|
||||
// This is a crude lookahead trick to try and notice that we're
|
||||
// parsing the argument patterns for a fat-arrow function before we
|
||||
// actually hit the arrow token. It only works if the arrow is on
|
||||
// the same line as the arguments and there's no strange noise
|
||||
// (comments) in between. Fallback is to only notice when we hit the
|
||||
// arrow, and not declare the arguments as locals for the arrow
|
||||
// body.
|
||||
function findFatArrow(stream, state) {
|
||||
if (state.fatArrowAt) state.fatArrowAt = null;
|
||||
var arrow = stream.string.indexOf("=>", stream.start);
|
||||
if (arrow < 0) return;
|
||||
|
||||
if (isTS) { // Try to skip TypeScript return type declarations after the arguments
|
||||
var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow))
|
||||
if (m) arrow = m.index
|
||||
}
|
||||
|
||||
var depth = 0, sawSomething = false;
|
||||
for (var pos = arrow - 1; pos >= 0; --pos) {
|
||||
var ch = stream.string.charAt(pos);
|
||||
var bracket = brackets.indexOf(ch);
|
||||
if (bracket >= 0 && bracket < 3) {
|
||||
if (!depth) { ++pos; break; }
|
||||
if (--depth == 0) { if (ch == "(") sawSomething = true; break; }
|
||||
} else if (bracket >= 3 && bracket < 6) {
|
||||
++depth;
|
||||
} else if (wordRE.test(ch)) {
|
||||
sawSomething = true;
|
||||
} else if (/["'\/`]/.test(ch)) {
|
||||
for (;; --pos) {
|
||||
if (pos == 0) return
|
||||
var next = stream.string.charAt(pos - 1)
|
||||
if (next == ch && stream.string.charAt(pos - 2) != "\\") { pos--; break }
|
||||
}
|
||||
} else if (sawSomething && !depth) {
|
||||
++pos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sawSomething && !depth) state.fatArrowAt = pos;
|
||||
}
|
||||
|
||||
// Parser
|
||||
|
||||
var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true,
|
||||
"regexp": true, "this": true, "import": true, "jsonld-keyword": true};
|
||||
|
||||
function JSLexical(indented, column, type, align, prev, info) {
|
||||
this.indented = indented;
|
||||
this.column = column;
|
||||
this.type = type;
|
||||
this.prev = prev;
|
||||
this.info = info;
|
||||
if (align != null) this.align = align;
|
||||
}
|
||||
|
||||
function inScope(state, varname) {
|
||||
if (!trackScope) return false
|
||||
for (var v = state.localVars; v; v = v.next)
|
||||
if (v.name == varname) return true;
|
||||
for (var cx = state.context; cx; cx = cx.prev) {
|
||||
for (var v = cx.vars; v; v = v.next)
|
||||
if (v.name == varname) return true;
|
||||
}
|
||||
}
|
||||
|
||||
function parseJS(state, style, type, content, stream) {
|
||||
var cc = state.cc;
|
||||
// Communicate our context to the combinators.
|
||||
// (Less wasteful than consing up a hundred closures on every call.)
|
||||
cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style;
|
||||
|
||||
if (!state.lexical.hasOwnProperty("align"))
|
||||
state.lexical.align = true;
|
||||
|
||||
while(true) {
|
||||
var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement;
|
||||
if (combinator(type, content)) {
|
||||
while(cc.length && cc[cc.length - 1].lex)
|
||||
cc.pop()();
|
||||
if (cx.marked) return cx.marked;
|
||||
if (type == "variable" && inScope(state, content)) return "variable-2";
|
||||
return style;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Combinator utils
|
||||
|
||||
var cx = {state: null, column: null, marked: null, cc: null};
|
||||
function pass() {
|
||||
for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]);
|
||||
}
|
||||
function cont() {
|
||||
pass.apply(null, arguments);
|
||||
return true;
|
||||
}
|
||||
function inList(name, list) {
|
||||
for (var v = list; v; v = v.next) if (v.name == name) return true
|
||||
return false;
|
||||
}
|
||||
function register(varname) {
|
||||
var state = cx.state;
|
||||
cx.marked = "def";
|
||||
if (!trackScope) return
|
||||
if (state.context) {
|
||||
if (state.lexical.info == "var" && state.context && state.context.block) {
|
||||
// FIXME function decls are also not block scoped
|
||||
var newContext = registerVarScoped(varname, state.context)
|
||||
if (newContext != null) {
|
||||
state.context = newContext
|
||||
return
|
||||
}
|
||||
} else if (!inList(varname, state.localVars)) {
|
||||
state.localVars = new Var(varname, state.localVars)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Fall through means this is global
|
||||
if (parserConfig.globalVars && !inList(varname, state.globalVars))
|
||||
state.globalVars = new Var(varname, state.globalVars)
|
||||
}
|
||||
function registerVarScoped(varname, context) {
|
||||
if (!context) {
|
||||
return null
|
||||
} else if (context.block) {
|
||||
var inner = registerVarScoped(varname, context.prev)
|
||||
if (!inner) return null
|
||||
if (inner == context.prev) return context
|
||||
return new Context(inner, context.vars, true)
|
||||
} else if (inList(varname, context.vars)) {
|
||||
return context
|
||||
} else {
|
||||
return new Context(context.prev, new Var(varname, context.vars), false)
|
||||
}
|
||||
}
|
||||
|
||||
function isModifier(name) {
|
||||
return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly"
|
||||
}
|
||||
|
||||
// Combinators
|
||||
|
||||
function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block }
|
||||
function Var(name, next) { this.name = name; this.next = next }
|
||||
|
||||
var defaultVars = new Var("this", new Var("arguments", null))
|
||||
function pushcontext() {
|
||||
cx.state.context = new Context(cx.state.context, cx.state.localVars, false)
|
||||
cx.state.localVars = defaultVars
|
||||
}
|
||||
function pushblockcontext() {
|
||||
cx.state.context = new Context(cx.state.context, cx.state.localVars, true)
|
||||
cx.state.localVars = null
|
||||
}
|
||||
pushcontext.lex = pushblockcontext.lex = true
|
||||
function popcontext() {
|
||||
cx.state.localVars = cx.state.context.vars
|
||||
cx.state.context = cx.state.context.prev
|
||||
}
|
||||
popcontext.lex = true
|
||||
function pushlex(type, info) {
|
||||
var result = function() {
|
||||
var state = cx.state, indent = state.indented;
|
||||
if (state.lexical.type == "stat") indent = state.lexical.indented;
|
||||
else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev)
|
||||
indent = outer.indented;
|
||||
state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info);
|
||||
};
|
||||
result.lex = true;
|
||||
return result;
|
||||
}
|
||||
function poplex() {
|
||||
var state = cx.state;
|
||||
if (state.lexical.prev) {
|
||||
if (state.lexical.type == ")")
|
||||
state.indented = state.lexical.indented;
|
||||
state.lexical = state.lexical.prev;
|
||||
}
|
||||
}
|
||||
poplex.lex = true;
|
||||
|
||||
function expect(wanted) {
|
||||
function exp(type) {
|
||||
if (type == wanted) return cont();
|
||||
else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass();
|
||||
else return cont(exp);
|
||||
}
|
||||
return exp;
|
||||
}
|
||||
|
||||
function statement(type, value) {
|
||||
if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex);
|
||||
if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex);
|
||||
if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
|
||||
if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex);
|
||||
if (type == "debugger") return cont(expect(";"));
|
||||
if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext);
|
||||
if (type == ";") return cont();
|
||||
if (type == "if") {
|
||||
if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex)
|
||||
cx.state.cc.pop()();
|
||||
return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse);
|
||||
}
|
||||
if (type == "function") return cont(functiondef);
|
||||
if (type == "for") return cont(pushlex("form"), pushblockcontext, forspec, statement, popcontext, poplex);
|
||||
if (type == "class" || (isTS && value == "interface")) {
|
||||
cx.marked = "keyword"
|
||||
return cont(pushlex("form", type == "class" ? type : value), className, poplex)
|
||||
}
|
||||
if (type == "variable") {
|
||||
if (isTS && value == "declare") {
|
||||
cx.marked = "keyword"
|
||||
return cont(statement)
|
||||
} else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) {
|
||||
cx.marked = "keyword"
|
||||
if (value == "enum") return cont(enumdef);
|
||||
else if (value == "type") return cont(typename, expect("operator"), typeexpr, expect(";"));
|
||||
else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex)
|
||||
} else if (isTS && value == "namespace") {
|
||||
cx.marked = "keyword"
|
||||
return cont(pushlex("form"), expression, statement, poplex)
|
||||
} else if (isTS && value == "abstract") {
|
||||
cx.marked = "keyword"
|
||||
return cont(statement)
|
||||
} else {
|
||||
return cont(pushlex("stat"), maybelabel);
|
||||
}
|
||||
}
|
||||
if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext,
|
||||
block, poplex, poplex, popcontext);
|
||||
if (type == "case") return cont(expression, expect(":"));
|
||||
if (type == "default") return cont(expect(":"));
|
||||
if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext);
|
||||
if (type == "export") return cont(pushlex("stat"), afterExport, poplex);
|
||||
if (type == "import") return cont(pushlex("stat"), afterImport, poplex);
|
||||
if (type == "async") return cont(statement)
|
||||
if (value == "@") return cont(expression, statement)
|
||||
return pass(pushlex("stat"), expression, expect(";"), poplex);
|
||||
}
|
||||
function maybeCatchBinding(type) {
|
||||
if (type == "(") return cont(funarg, expect(")"))
|
||||
}
|
||||
function expression(type, value) {
|
||||
return expressionInner(type, value, false);
|
||||
}
|
||||
function expressionNoComma(type, value) {
|
||||
return expressionInner(type, value, true);
|
||||
}
|
||||
function parenExpr(type) {
|
||||
if (type != "(") return pass()
|
||||
return cont(pushlex(")"), maybeexpression, expect(")"), poplex)
|
||||
}
|
||||
function expressionInner(type, value, noComma) {
|
||||
if (cx.state.fatArrowAt == cx.stream.start) {
|
||||
var body = noComma ? arrowBodyNoComma : arrowBody;
|
||||
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext);
|
||||
else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext);
|
||||
}
|
||||
|
||||
var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma;
|
||||
if (atomicTypes.hasOwnProperty(type)) return cont(maybeop);
|
||||
if (type == "function") return cont(functiondef, maybeop);
|
||||
if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), classExpression, poplex); }
|
||||
if (type == "keyword c" || type == "async") return cont(noComma ? expressionNoComma : expression);
|
||||
if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop);
|
||||
if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression);
|
||||
if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop);
|
||||
if (type == "{") return contCommasep(objprop, "}", null, maybeop);
|
||||
if (type == "quasi") return pass(quasi, maybeop);
|
||||
if (type == "new") return cont(maybeTarget(noComma));
|
||||
return cont();
|
||||
}
|
||||
function maybeexpression(type) {
|
||||
if (type.match(/[;\}\)\],]/)) return pass();
|
||||
return pass(expression);
|
||||
}
|
||||
|
||||
function maybeoperatorComma(type, value) {
|
||||
if (type == ",") return cont(maybeexpression);
|
||||
return maybeoperatorNoComma(type, value, false);
|
||||
}
|
||||
function maybeoperatorNoComma(type, value, noComma) {
|
||||
var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma;
|
||||
var expr = noComma == false ? expression : expressionNoComma;
|
||||
if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext);
|
||||
if (type == "operator") {
|
||||
if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me);
|
||||
if (isTS && value == "<" && cx.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/, false))
|
||||
return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me);
|
||||
if (value == "?") return cont(expression, expect(":"), expr);
|
||||
return cont(expr);
|
||||
}
|
||||
if (type == "quasi") { return pass(quasi, me); }
|
||||
if (type == ";") return;
|
||||
if (type == "(") return contCommasep(expressionNoComma, ")", "call", me);
|
||||
if (type == ".") return cont(property, me);
|
||||
if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me);
|
||||
if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) }
|
||||
if (type == "regexp") {
|
||||
cx.state.lastType = cx.marked = "operator"
|
||||
cx.stream.backUp(cx.stream.pos - cx.stream.start - 1)
|
||||
return cont(expr)
|
||||
}
|
||||
}
|
||||
function quasi(type, value) {
|
||||
if (type != "quasi") return pass();
|
||||
if (value.slice(value.length - 2) != "${") return cont(quasi);
|
||||
return cont(maybeexpression, continueQuasi);
|
||||
}
|
||||
function continueQuasi(type) {
|
||||
if (type == "}") {
|
||||
cx.marked = "string-2";
|
||||
cx.state.tokenize = tokenQuasi;
|
||||
return cont(quasi);
|
||||
}
|
||||
}
|
||||
function arrowBody(type) {
|
||||
findFatArrow(cx.stream, cx.state);
|
||||
return pass(type == "{" ? statement : expression);
|
||||
}
|
||||
function arrowBodyNoComma(type) {
|
||||
findFatArrow(cx.stream, cx.state);
|
||||
return pass(type == "{" ? statement : expressionNoComma);
|
||||
}
|
||||
function maybeTarget(noComma) {
|
||||
return function(type) {
|
||||
if (type == ".") return cont(noComma ? targetNoComma : target);
|
||||
else if (type == "variable" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma)
|
||||
else return pass(noComma ? expressionNoComma : expression);
|
||||
};
|
||||
}
|
||||
function target(_, value) {
|
||||
if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); }
|
||||
}
|
||||
function targetNoComma(_, value) {
|
||||
if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); }
|
||||
}
|
||||
function maybelabel(type) {
|
||||
if (type == ":") return cont(poplex, statement);
|
||||
return pass(maybeoperatorComma, expect(";"), poplex);
|
||||
}
|
||||
function property(type) {
|
||||
if (type == "variable") {cx.marked = "property"; return cont();}
|
||||
}
|
||||
function objprop(type, value) {
|
||||
if (type == "async") {
|
||||
cx.marked = "property";
|
||||
return cont(objprop);
|
||||
} else if (type == "variable" || cx.style == "keyword") {
|
||||
cx.marked = "property";
|
||||
if (value == "get" || value == "set") return cont(getterSetter);
|
||||
var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params
|
||||
if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false)))
|
||||
cx.state.fatArrowAt = cx.stream.pos + m[0].length
|
||||
return cont(afterprop);
|
||||
} else if (type == "number" || type == "string") {
|
||||
cx.marked = jsonldMode ? "property" : (cx.style + " property");
|
||||
return cont(afterprop);
|
||||
} else if (type == "jsonld-keyword") {
|
||||
return cont(afterprop);
|
||||
} else if (isTS && isModifier(value)) {
|
||||
cx.marked = "keyword"
|
||||
return cont(objprop)
|
||||
} else if (type == "[") {
|
||||
return cont(expression, maybetype, expect("]"), afterprop);
|
||||
} else if (type == "spread") {
|
||||
return cont(expressionNoComma, afterprop);
|
||||
} else if (value == "*") {
|
||||
cx.marked = "keyword";
|
||||
return cont(objprop);
|
||||
} else if (type == ":") {
|
||||
return pass(afterprop)
|
||||
}
|
||||
}
|
||||
function getterSetter(type) {
|
||||
if (type != "variable") return pass(afterprop);
|
||||
cx.marked = "property";
|
||||
return cont(functiondef);
|
||||
}
|
||||
function afterprop(type) {
|
||||
if (type == ":") return cont(expressionNoComma);
|
||||
if (type == "(") return pass(functiondef);
|
||||
}
|
||||
function commasep(what, end, sep) {
|
||||
function proceed(type, value) {
|
||||
if (sep ? sep.indexOf(type) > -1 : type == ",") {
|
||||
var lex = cx.state.lexical;
|
||||
if (lex.info == "call") lex.pos = (lex.pos || 0) + 1;
|
||||
return cont(function(type, value) {
|
||||
if (type == end || value == end) return pass()
|
||||
return pass(what)
|
||||
}, proceed);
|
||||
}
|
||||
if (type == end || value == end) return cont();
|
||||
if (sep && sep.indexOf(";") > -1) return pass(what)
|
||||
return cont(expect(end));
|
||||
}
|
||||
return function(type, value) {
|
||||
if (type == end || value == end) return cont();
|
||||
return pass(what, proceed);
|
||||
};
|
||||
}
|
||||
function contCommasep(what, end, info) {
|
||||
for (var i = 3; i < arguments.length; i++)
|
||||
cx.cc.push(arguments[i]);
|
||||
return cont(pushlex(end, info), commasep(what, end), poplex);
|
||||
}
|
||||
function block(type) {
|
||||
if (type == "}") return cont();
|
||||
return pass(statement, block);
|
||||
}
|
||||
function maybetype(type, value) {
|
||||
if (isTS) {
|
||||
if (type == ":") return cont(typeexpr);
|
||||
if (value == "?") return cont(maybetype);
|
||||
}
|
||||
}
|
||||
function maybetypeOrIn(type, value) {
|
||||
if (isTS && (type == ":" || value == "in")) return cont(typeexpr)
|
||||
}
|
||||
function mayberettype(type) {
|
||||
if (isTS && type == ":") {
|
||||
if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr)
|
||||
else return cont(typeexpr)
|
||||
}
|
||||
}
|
||||
function isKW(_, value) {
|
||||
if (value == "is") {
|
||||
cx.marked = "keyword"
|
||||
return cont()
|
||||
}
|
||||
}
|
||||
function typeexpr(type, value) {
|
||||
if (value == "keyof" || value == "typeof" || value == "infer" || value == "readonly") {
|
||||
cx.marked = "keyword"
|
||||
return cont(value == "typeof" ? expressionNoComma : typeexpr)
|
||||
}
|
||||
if (type == "variable" || value == "void") {
|
||||
cx.marked = "type"
|
||||
return cont(afterType)
|
||||
}
|
||||
if (value == "|" || value == "&") return cont(typeexpr)
|
||||
if (type == "string" || type == "number" || type == "atom") return cont(afterType);
|
||||
if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType)
|
||||
if (type == "{") return cont(pushlex("}"), typeprops, poplex, afterType)
|
||||
if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType, afterType)
|
||||
if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr)
|
||||
if (type == "quasi") { return pass(quasiType, afterType); }
|
||||
}
|
||||
function maybeReturnType(type) {
|
||||
if (type == "=>") return cont(typeexpr)
|
||||
}
|
||||
function typeprops(type) {
|
||||
if (type.match(/[\}\)\]]/)) return cont()
|
||||
if (type == "," || type == ";") return cont(typeprops)
|
||||
return pass(typeprop, typeprops)
|
||||
}
|
||||
function typeprop(type, value) {
|
||||
if (type == "variable" || cx.style == "keyword") {
|
||||
cx.marked = "property"
|
||||
return cont(typeprop)
|
||||
} else if (value == "?" || type == "number" || type == "string") {
|
||||
return cont(typeprop)
|
||||
} else if (type == ":") {
|
||||
return cont(typeexpr)
|
||||
} else if (type == "[") {
|
||||
return cont(expect("variable"), maybetypeOrIn, expect("]"), typeprop)
|
||||
} else if (type == "(") {
|
||||
return pass(functiondecl, typeprop)
|
||||
} else if (!type.match(/[;\}\)\],]/)) {
|
||||
return cont()
|
||||
}
|
||||
}
|
||||
function quasiType(type, value) {
|
||||
if (type != "quasi") return pass();
|
||||
if (value.slice(value.length - 2) != "${") return cont(quasiType);
|
||||
return cont(typeexpr, continueQuasiType);
|
||||
}
|
||||
function continueQuasiType(type) {
|
||||
if (type == "}") {
|
||||
cx.marked = "string-2";
|
||||
cx.state.tokenize = tokenQuasi;
|
||||
return cont(quasiType);
|
||||
}
|
||||
}
|
||||
function typearg(type, value) {
|
||||
if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg)
|
||||
if (type == ":") return cont(typeexpr)
|
||||
if (type == "spread") return cont(typearg)
|
||||
return pass(typeexpr)
|
||||
}
|
||||
function afterType(type, value) {
|
||||
if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
|
||||
if (value == "|" || type == "." || value == "&") return cont(typeexpr)
|
||||
if (type == "[") return cont(typeexpr, expect("]"), afterType)
|
||||
if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) }
|
||||
if (value == "?") return cont(typeexpr, expect(":"), typeexpr)
|
||||
}
|
||||
function maybeTypeArgs(_, value) {
|
||||
if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
|
||||
}
|
||||
function typeparam() {
|
||||
return pass(typeexpr, maybeTypeDefault)
|
||||
}
|
||||
function maybeTypeDefault(_, value) {
|
||||
if (value == "=") return cont(typeexpr)
|
||||
}
|
||||
function vardef(_, value) {
|
||||
if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)}
|
||||
return pass(pattern, maybetype, maybeAssign, vardefCont);
|
||||
}
|
||||
function pattern(type, value) {
|
||||
if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(pattern) }
|
||||
if (type == "variable") { register(value); return cont(); }
|
||||
if (type == "spread") return cont(pattern);
|
||||
if (type == "[") return contCommasep(eltpattern, "]");
|
||||
if (type == "{") return contCommasep(proppattern, "}");
|
||||
}
|
||||
function proppattern(type, value) {
|
||||
if (type == "variable" && !cx.stream.match(/^\s*:/, false)) {
|
||||
register(value);
|
||||
return cont(maybeAssign);
|
||||
}
|
||||
if (type == "variable") cx.marked = "property";
|
||||
if (type == "spread") return cont(pattern);
|
||||
if (type == "}") return pass();
|
||||
if (type == "[") return cont(expression, expect(']'), expect(':'), proppattern);
|
||||
return cont(expect(":"), pattern, maybeAssign);
|
||||
}
|
||||
function eltpattern() {
|
||||
return pass(pattern, maybeAssign)
|
||||
}
|
||||
function maybeAssign(_type, value) {
|
||||
if (value == "=") return cont(expressionNoComma);
|
||||
}
|
||||
function vardefCont(type) {
|
||||
if (type == ",") return cont(vardef);
|
||||
}
|
||||
function maybeelse(type, value) {
|
||||
if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex);
|
||||
}
|
||||
function forspec(type, value) {
|
||||
if (value == "await") return cont(forspec);
|
||||
if (type == "(") return cont(pushlex(")"), forspec1, poplex);
|
||||
}
|
||||
function forspec1(type) {
|
||||
if (type == "var") return cont(vardef, forspec2);
|
||||
if (type == "variable") return cont(forspec2);
|
||||
return pass(forspec2)
|
||||
}
|
||||
function forspec2(type, value) {
|
||||
if (type == ")") return cont()
|
||||
if (type == ";") return cont(forspec2)
|
||||
if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression, forspec2) }
|
||||
return pass(expression, forspec2)
|
||||
}
|
||||
function functiondef(type, value) {
|
||||
if (value == "*") {cx.marked = "keyword"; return cont(functiondef);}
|
||||
if (type == "variable") {register(value); return cont(functiondef);}
|
||||
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext);
|
||||
if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef)
|
||||
}
|
||||
function functiondecl(type, value) {
|
||||
if (value == "*") {cx.marked = "keyword"; return cont(functiondecl);}
|
||||
if (type == "variable") {register(value); return cont(functiondecl);}
|
||||
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, popcontext);
|
||||
if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondecl)
|
||||
}
|
||||
function typename(type, value) {
|
||||
if (type == "keyword" || type == "variable") {
|
||||
cx.marked = "type"
|
||||
return cont(typename)
|
||||
} else if (value == "<") {
|
||||
return cont(pushlex(">"), commasep(typeparam, ">"), poplex)
|
||||
}
|
||||
}
|
||||
function funarg(type, value) {
|
||||
if (value == "@") cont(expression, funarg)
|
||||
if (type == "spread") return cont(funarg);
|
||||
if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(funarg); }
|
||||
if (isTS && type == "this") return cont(maybetype, maybeAssign)
|
||||
return pass(pattern, maybetype, maybeAssign);
|
||||
}
|
||||
function classExpression(type, value) {
|
||||
// Class expressions may have an optional name.
|
||||
if (type == "variable") return className(type, value);
|
||||
return classNameAfter(type, value);
|
||||
}
|
||||
function className(type, value) {
|
||||
if (type == "variable") {register(value); return cont(classNameAfter);}
|
||||
}
|
||||
function classNameAfter(type, value) {
|
||||
if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter)
|
||||
if (value == "extends" || value == "implements" || (isTS && type == ",")) {
|
||||
if (value == "implements") cx.marked = "keyword";
|
||||
return cont(isTS ? typeexpr : expression, classNameAfter);
|
||||
}
|
||||
if (type == "{") return cont(pushlex("}"), classBody, poplex);
|
||||
}
|
||||
function classBody(type, value) {
|
||||
if (type == "async" ||
|
||||
(type == "variable" &&
|
||||
(value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) &&
|
||||
cx.stream.match(/^\s+#?[\w$\xa1-\uffff]/, false))) {
|
||||
cx.marked = "keyword";
|
||||
return cont(classBody);
|
||||
}
|
||||
if (type == "variable" || cx.style == "keyword") {
|
||||
cx.marked = "property";
|
||||
return cont(classfield, classBody);
|
||||
}
|
||||
if (type == "number" || type == "string") return cont(classfield, classBody);
|
||||
if (type == "[")
|
||||
return cont(expression, maybetype, expect("]"), classfield, classBody)
|
||||
if (value == "*") {
|
||||
cx.marked = "keyword";
|
||||
return cont(classBody);
|
||||
}
|
||||
if (isTS && type == "(") return pass(functiondecl, classBody)
|
||||
if (type == ";" || type == ",") return cont(classBody);
|
||||
if (type == "}") return cont();
|
||||
if (value == "@") return cont(expression, classBody)
|
||||
}
|
||||
function classfield(type, value) {
|
||||
if (value == "!") return cont(classfield)
|
||||
if (value == "?") return cont(classfield)
|
||||
if (type == ":") return cont(typeexpr, maybeAssign)
|
||||
if (value == "=") return cont(expressionNoComma)
|
||||
var context = cx.state.lexical.prev, isInterface = context && context.info == "interface"
|
||||
return pass(isInterface ? functiondecl : functiondef)
|
||||
}
|
||||
function afterExport(type, value) {
|
||||
if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); }
|
||||
if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); }
|
||||
if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";"));
|
||||
return pass(statement);
|
||||
}
|
||||
function exportField(type, value) {
|
||||
if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); }
|
||||
if (type == "variable") return pass(expressionNoComma, exportField);
|
||||
}
|
||||
function afterImport(type) {
|
||||
if (type == "string") return cont();
|
||||
if (type == "(") return pass(expression);
|
||||
if (type == ".") return pass(maybeoperatorComma);
|
||||
return pass(importSpec, maybeMoreImports, maybeFrom);
|
||||
}
|
||||
function importSpec(type, value) {
|
||||
if (type == "{") return contCommasep(importSpec, "}");
|
||||
if (type == "variable") register(value);
|
||||
if (value == "*") cx.marked = "keyword";
|
||||
return cont(maybeAs);
|
||||
}
|
||||
function maybeMoreImports(type) {
|
||||
if (type == ",") return cont(importSpec, maybeMoreImports)
|
||||
}
|
||||
function maybeAs(_type, value) {
|
||||
if (value == "as") { cx.marked = "keyword"; return cont(importSpec); }
|
||||
}
|
||||
function maybeFrom(_type, value) {
|
||||
if (value == "from") { cx.marked = "keyword"; return cont(expression); }
|
||||
}
|
||||
function arrayLiteral(type) {
|
||||
if (type == "]") return cont();
|
||||
return pass(commasep(expressionNoComma, "]"));
|
||||
}
|
||||
function enumdef() {
|
||||
return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex)
|
||||
}
|
||||
function enummember() {
|
||||
return pass(pattern, maybeAssign);
|
||||
}
|
||||
|
||||
function isContinuedStatement(state, textAfter) {
|
||||
return state.lastType == "operator" || state.lastType == "," ||
|
||||
isOperatorChar.test(textAfter.charAt(0)) ||
|
||||
/[,.]/.test(textAfter.charAt(0));
|
||||
}
|
||||
|
||||
function expressionAllowed(stream, state, backUp) {
|
||||
return state.tokenize == tokenBase &&
|
||||
/^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) ||
|
||||
(state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0))))
|
||||
}
|
||||
|
||||
// Interface
|
||||
|
||||
return {
|
||||
startState: function(basecolumn) {
|
||||
var state = {
|
||||
tokenize: tokenBase,
|
||||
lastType: "sof",
|
||||
cc: [],
|
||||
lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
|
||||
localVars: parserConfig.localVars,
|
||||
context: parserConfig.localVars && new Context(null, null, false),
|
||||
indented: basecolumn || 0
|
||||
};
|
||||
if (parserConfig.globalVars && typeof parserConfig.globalVars == "object")
|
||||
state.globalVars = parserConfig.globalVars;
|
||||
return state;
|
||||
},
|
||||
|
||||
token: function(stream, state) {
|
||||
if (stream.sol()) {
|
||||
if (!state.lexical.hasOwnProperty("align"))
|
||||
state.lexical.align = false;
|
||||
state.indented = stream.indentation();
|
||||
findFatArrow(stream, state);
|
||||
}
|
||||
if (state.tokenize != tokenComment && stream.eatSpace()) return null;
|
||||
var style = state.tokenize(stream, state);
|
||||
if (type == "comment") return style;
|
||||
state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type;
|
||||
return parseJS(state, style, type, content, stream);
|
||||
},
|
||||
|
||||
indent: function(state, textAfter) {
|
||||
if (state.tokenize == tokenComment || state.tokenize == tokenQuasi) return CodeMirror.Pass;
|
||||
if (state.tokenize != tokenBase) return 0;
|
||||
var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top
|
||||
// Kludge to prevent 'maybelse' from blocking lexical scope pops
|
||||
if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) {
|
||||
var c = state.cc[i];
|
||||
if (c == poplex) lexical = lexical.prev;
|
||||
else if (c != maybeelse && c != popcontext) break;
|
||||
}
|
||||
while ((lexical.type == "stat" || lexical.type == "form") &&
|
||||
(firstChar == "}" || ((top = state.cc[state.cc.length - 1]) &&
|
||||
(top == maybeoperatorComma || top == maybeoperatorNoComma) &&
|
||||
!/^[,\.=+\-*:?[\(]/.test(textAfter))))
|
||||
lexical = lexical.prev;
|
||||
if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat")
|
||||
lexical = lexical.prev;
|
||||
var type = lexical.type, closing = firstChar == type;
|
||||
|
||||
if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0);
|
||||
else if (type == "form" && firstChar == "{") return lexical.indented;
|
||||
else if (type == "form") return lexical.indented + indentUnit;
|
||||
else if (type == "stat")
|
||||
return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0);
|
||||
else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false)
|
||||
return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit);
|
||||
else if (lexical.align) return lexical.column + (closing ? 0 : 1);
|
||||
else return lexical.indented + (closing ? 0 : indentUnit);
|
||||
},
|
||||
|
||||
electricInput: /^\s*(?:case .*?:|default:|\{|\})$/,
|
||||
blockCommentStart: jsonMode ? null : "/*",
|
||||
blockCommentEnd: jsonMode ? null : "*/",
|
||||
blockCommentContinue: jsonMode ? null : " * ",
|
||||
lineComment: jsonMode ? null : "//",
|
||||
fold: "brace",
|
||||
closeBrackets: "()[]{}''\"\"``",
|
||||
|
||||
helperType: jsonMode ? "json" : "javascript",
|
||||
jsonldMode: jsonldMode,
|
||||
jsonMode: jsonMode,
|
||||
|
||||
expressionAllowed: expressionAllowed,
|
||||
|
||||
skipExpression: function(state) {
|
||||
parseJS(state, "atom", "atom", "true", new CodeMirror.StringStream("", 2, null))
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/);
|
||||
|
||||
CodeMirror.defineMIME("text/javascript", "javascript");
|
||||
CodeMirror.defineMIME("text/ecmascript", "javascript");
|
||||
CodeMirror.defineMIME("application/javascript", "javascript");
|
||||
CodeMirror.defineMIME("application/x-javascript", "javascript");
|
||||
CodeMirror.defineMIME("application/ecmascript", "javascript");
|
||||
CodeMirror.defineMIME("application/json", { name: "javascript", json: true });
|
||||
CodeMirror.defineMIME("application/x-json", { name: "javascript", json: true });
|
||||
CodeMirror.defineMIME("application/manifest+json", { name: "javascript", json: true })
|
||||
CodeMirror.defineMIME("application/ld+json", { name: "javascript", jsonld: true });
|
||||
CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true });
|
||||
CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true });
|
||||
|
||||
});
|
||||
32076
web/assets/codemirror/jshint.js
Normal file
32076
web/assets/codemirror/jshint.js
Normal file
File diff suppressed because one or more lines are too long
1
web/assets/codemirror/jsonlint.js
Normal file
1
web/assets/codemirror/jsonlint.js
Normal file
File diff suppressed because one or more lines are too long
65
web/assets/codemirror/lint/javascript-lint.js
Normal file
65
web/assets/codemirror/lint/javascript-lint.js
Normal file
@@ -0,0 +1,65 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
// Depends on jshint.js from https://github.com/jshint/jshint
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
// declare global: JSHINT
|
||||
|
||||
function validator(text, options) {
|
||||
if (!window.JSHINT) {
|
||||
if (window.console) {
|
||||
window.console.error("Error: window.JSHINT not defined, CodeMirror JavaScript linting cannot run.");
|
||||
}
|
||||
return [];
|
||||
}
|
||||
if (!options.indent) // JSHint error.character actually is a column index, this fixes underlining on lines using tabs for indentation
|
||||
options.indent = 1; // JSHint default value is 4
|
||||
JSHINT(text, options, options.globals);
|
||||
var errors = JSHINT.data().errors, result = [];
|
||||
if (errors) parseErrors(errors, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
CodeMirror.registerHelper("lint", "javascript", validator);
|
||||
|
||||
function parseErrors(errors, output) {
|
||||
for ( var i = 0; i < errors.length; i++) {
|
||||
var error = errors[i];
|
||||
if (error) {
|
||||
if (error.line <= 0) {
|
||||
if (window.console) {
|
||||
window.console.warn("Cannot display JSHint error (invalid line " + error.line + ")", error);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var start = error.character - 1, end = start + 1;
|
||||
if (error.evidence) {
|
||||
var index = error.evidence.substring(start).search(/.\b/);
|
||||
if (index > -1) {
|
||||
end += index;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to format expected by validation service
|
||||
var hint = {
|
||||
message: error.reason,
|
||||
severity: error.code ? (error.code.startsWith('W') ? "warning" : "error") : "error",
|
||||
from: CodeMirror.Pos(error.line - 1, start),
|
||||
to: CodeMirror.Pos(error.line - 1, end)
|
||||
};
|
||||
|
||||
output.push(hint);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
79
web/assets/codemirror/lint/lint.css
Normal file
79
web/assets/codemirror/lint/lint.css
Normal file
@@ -0,0 +1,79 @@
|
||||
/* The lint marker gutter */
|
||||
.CodeMirror-lint-markers {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-tooltip {
|
||||
background-color: #ffd;
|
||||
border: 1px solid black;
|
||||
border-radius: 4px;
|
||||
color: black;
|
||||
font-family: monospace;
|
||||
font-size: 10pt;
|
||||
overflow: hidden;
|
||||
padding: 2px 5px;
|
||||
position: fixed;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
z-index: 100;
|
||||
max-width: 600px;
|
||||
opacity: 0;
|
||||
transition: opacity .4s;
|
||||
-moz-transition: opacity .4s;
|
||||
-webkit-transition: opacity .4s;
|
||||
-o-transition: opacity .4s;
|
||||
-ms-transition: opacity .4s;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark {
|
||||
background-position: left bottom;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-warning {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-error {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker {
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-message {
|
||||
padding-left: 18px;
|
||||
background-position: top left;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-multiple {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right bottom;
|
||||
width: 100%; height: 100%;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-line-error {
|
||||
background-color: rgba(183, 76, 81, 0.08);
|
||||
}
|
||||
|
||||
.CodeMirror-lint-line-warning {
|
||||
background-color: rgba(255, 211, 0, 0.1);
|
||||
}
|
||||
288
web/assets/codemirror/lint/lint.js
Normal file
288
web/assets/codemirror/lint/lint.js
Normal file
@@ -0,0 +1,288 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
var GUTTER_ID = "CodeMirror-lint-markers";
|
||||
var LINT_LINE_ID = "CodeMirror-lint-line-";
|
||||
|
||||
function showTooltip(cm, e, content) {
|
||||
var tt = document.createElement("div");
|
||||
tt.className = "CodeMirror-lint-tooltip cm-s-" + cm.options.theme;
|
||||
tt.appendChild(content.cloneNode(true));
|
||||
if (cm.state.lint.options.selfContain)
|
||||
cm.getWrapperElement().appendChild(tt);
|
||||
else
|
||||
document.body.appendChild(tt);
|
||||
|
||||
function position(e) {
|
||||
if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position);
|
||||
var top = Math.max(0, e.clientY - tt.offsetHeight - 5);
|
||||
var left = Math.max(0, Math.min(e.clientX + 5, tt.ownerDocument.defaultView.innerWidth - tt.offsetWidth));
|
||||
tt.style.top = top + "px"
|
||||
tt.style.left = left + "px";
|
||||
}
|
||||
CodeMirror.on(document, "mousemove", position);
|
||||
position(e);
|
||||
if (tt.style.opacity != null) tt.style.opacity = 1;
|
||||
return tt;
|
||||
}
|
||||
function rm(elt) {
|
||||
if (elt.parentNode) elt.parentNode.removeChild(elt);
|
||||
}
|
||||
function hideTooltip(tt) {
|
||||
if (!tt.parentNode) return;
|
||||
if (tt.style.opacity == null) rm(tt);
|
||||
tt.style.opacity = 0;
|
||||
setTimeout(function() { rm(tt); }, 600);
|
||||
}
|
||||
|
||||
function showTooltipFor(cm, e, content, node) {
|
||||
var tooltip = showTooltip(cm, e, content);
|
||||
function hide() {
|
||||
CodeMirror.off(node, "mouseout", hide);
|
||||
if (tooltip) { hideTooltip(tooltip); tooltip = null; }
|
||||
}
|
||||
var poll = setInterval(function() {
|
||||
if (tooltip) for (var n = node;; n = n.parentNode) {
|
||||
if (n && n.nodeType == 11) n = n.host;
|
||||
if (n == document.body) return;
|
||||
if (!n) { hide(); break; }
|
||||
}
|
||||
if (!tooltip) return clearInterval(poll);
|
||||
}, 400);
|
||||
CodeMirror.on(node, "mouseout", hide);
|
||||
}
|
||||
|
||||
function LintState(cm, conf, hasGutter) {
|
||||
this.marked = [];
|
||||
if (conf instanceof Function) conf = {getAnnotations: conf};
|
||||
if (!conf || conf === true) conf = {};
|
||||
this.options = {};
|
||||
this.linterOptions = conf.options || {};
|
||||
for (var prop in defaults) this.options[prop] = defaults[prop];
|
||||
for (var prop in conf) {
|
||||
if (defaults.hasOwnProperty(prop)) {
|
||||
if (conf[prop] != null) this.options[prop] = conf[prop];
|
||||
} else if (!conf.options) {
|
||||
this.linterOptions[prop] = conf[prop];
|
||||
}
|
||||
}
|
||||
this.timeout = null;
|
||||
this.hasGutter = hasGutter;
|
||||
this.onMouseOver = function(e) { onMouseOver(cm, e); };
|
||||
this.waitingFor = 0
|
||||
}
|
||||
|
||||
var defaults = {
|
||||
highlightLines: false,
|
||||
tooltips: true,
|
||||
delay: 500,
|
||||
lintOnChange: true,
|
||||
getAnnotations: null,
|
||||
async: false,
|
||||
selfContain: null,
|
||||
formatAnnotation: null,
|
||||
onUpdateLinting: null
|
||||
}
|
||||
|
||||
function clearMarks(cm) {
|
||||
var state = cm.state.lint;
|
||||
if (state.hasGutter) cm.clearGutter(GUTTER_ID);
|
||||
if (state.options.highlightLines) clearErrorLines(cm);
|
||||
for (var i = 0; i < state.marked.length; ++i)
|
||||
state.marked[i].clear();
|
||||
state.marked.length = 0;
|
||||
}
|
||||
|
||||
function clearErrorLines(cm) {
|
||||
cm.eachLine(function(line) {
|
||||
var has = line.wrapClass && /\bCodeMirror-lint-line-\w+\b/.exec(line.wrapClass);
|
||||
if (has) cm.removeLineClass(line, "wrap", has[0]);
|
||||
})
|
||||
}
|
||||
|
||||
function makeMarker(cm, labels, severity, multiple, tooltips) {
|
||||
var marker = document.createElement("div"), inner = marker;
|
||||
marker.className = "CodeMirror-lint-marker CodeMirror-lint-marker-" + severity;
|
||||
if (multiple) {
|
||||
inner = marker.appendChild(document.createElement("div"));
|
||||
inner.className = "CodeMirror-lint-marker CodeMirror-lint-marker-multiple";
|
||||
}
|
||||
|
||||
if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) {
|
||||
showTooltipFor(cm, e, labels, inner);
|
||||
});
|
||||
|
||||
return marker;
|
||||
}
|
||||
|
||||
function getMaxSeverity(a, b) {
|
||||
if (a == "error") return a;
|
||||
else return b;
|
||||
}
|
||||
|
||||
function groupByLine(annotations) {
|
||||
var lines = [];
|
||||
for (var i = 0; i < annotations.length; ++i) {
|
||||
var ann = annotations[i], line = ann.from.line;
|
||||
(lines[line] || (lines[line] = [])).push(ann);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
function annotationTooltip(ann) {
|
||||
var severity = ann.severity;
|
||||
if (!severity) severity = "error";
|
||||
var tip = document.createElement("div");
|
||||
tip.className = "CodeMirror-lint-message CodeMirror-lint-message-" + severity;
|
||||
if (typeof ann.messageHTML != 'undefined') {
|
||||
tip.innerHTML = ann.messageHTML;
|
||||
} else {
|
||||
tip.appendChild(document.createTextNode(ann.message));
|
||||
}
|
||||
return tip;
|
||||
}
|
||||
|
||||
function lintAsync(cm, getAnnotations) {
|
||||
var state = cm.state.lint
|
||||
var id = ++state.waitingFor
|
||||
function abort() {
|
||||
id = -1
|
||||
cm.off("change", abort)
|
||||
}
|
||||
cm.on("change", abort)
|
||||
getAnnotations(cm.getValue(), function(annotations, arg2) {
|
||||
cm.off("change", abort)
|
||||
if (state.waitingFor != id) return
|
||||
if (arg2 && annotations instanceof CodeMirror) annotations = arg2
|
||||
cm.operation(function() {updateLinting(cm, annotations)})
|
||||
}, state.linterOptions, cm);
|
||||
}
|
||||
|
||||
function startLinting(cm) {
|
||||
var state = cm.state.lint;
|
||||
if (!state) return;
|
||||
var options = state.options;
|
||||
/*
|
||||
* Passing rules in `options` property prevents JSHint (and other linters) from complaining
|
||||
* about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc.
|
||||
*/
|
||||
var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), "lint");
|
||||
if (!getAnnotations) return;
|
||||
if (options.async || getAnnotations.async) {
|
||||
lintAsync(cm, getAnnotations)
|
||||
} else {
|
||||
var annotations = getAnnotations(cm.getValue(), state.linterOptions, cm);
|
||||
if (!annotations) return;
|
||||
if (annotations.then) annotations.then(function(issues) {
|
||||
cm.operation(function() {updateLinting(cm, issues)})
|
||||
});
|
||||
else cm.operation(function() {updateLinting(cm, annotations)})
|
||||
}
|
||||
}
|
||||
|
||||
function updateLinting(cm, annotationsNotSorted) {
|
||||
var state = cm.state.lint;
|
||||
if (!state) return;
|
||||
var options = state.options;
|
||||
clearMarks(cm);
|
||||
|
||||
var annotations = groupByLine(annotationsNotSorted);
|
||||
|
||||
for (var line = 0; line < annotations.length; ++line) {
|
||||
var anns = annotations[line];
|
||||
if (!anns) continue;
|
||||
|
||||
var maxSeverity = null;
|
||||
var tipLabel = state.hasGutter && document.createDocumentFragment();
|
||||
|
||||
for (var i = 0; i < anns.length; ++i) {
|
||||
var ann = anns[i];
|
||||
var severity = ann.severity;
|
||||
if (!severity) severity = "error";
|
||||
maxSeverity = getMaxSeverity(maxSeverity, severity);
|
||||
|
||||
if (options.formatAnnotation) ann = options.formatAnnotation(ann);
|
||||
if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann));
|
||||
|
||||
if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, {
|
||||
className: "CodeMirror-lint-mark CodeMirror-lint-mark-" + severity,
|
||||
__annotation: ann
|
||||
}));
|
||||
}
|
||||
if (state.hasGutter)
|
||||
cm.setGutterMarker(line, GUTTER_ID, makeMarker(cm, tipLabel, maxSeverity, anns.length > 1,
|
||||
options.tooltips));
|
||||
|
||||
if (options.highlightLines)
|
||||
cm.addLineClass(line, "wrap", LINT_LINE_ID + maxSeverity);
|
||||
}
|
||||
if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm);
|
||||
}
|
||||
|
||||
function onChange(cm) {
|
||||
var state = cm.state.lint;
|
||||
if (!state) return;
|
||||
clearTimeout(state.timeout);
|
||||
state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay);
|
||||
}
|
||||
|
||||
function popupTooltips(cm, annotations, e) {
|
||||
var target = e.target || e.srcElement;
|
||||
var tooltip = document.createDocumentFragment();
|
||||
for (var i = 0; i < annotations.length; i++) {
|
||||
var ann = annotations[i];
|
||||
tooltip.appendChild(annotationTooltip(ann));
|
||||
}
|
||||
showTooltipFor(cm, e, tooltip, target);
|
||||
}
|
||||
|
||||
function onMouseOver(cm, e) {
|
||||
var target = e.target || e.srcElement;
|
||||
if (!/\bCodeMirror-lint-mark-/.test(target.className)) return;
|
||||
var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2;
|
||||
var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client"));
|
||||
|
||||
var annotations = [];
|
||||
for (var i = 0; i < spans.length; ++i) {
|
||||
var ann = spans[i].__annotation;
|
||||
if (ann) annotations.push(ann);
|
||||
}
|
||||
if (annotations.length) popupTooltips(cm, annotations, e);
|
||||
}
|
||||
|
||||
CodeMirror.defineOption("lint", false, function(cm, val, old) {
|
||||
if (old && old != CodeMirror.Init) {
|
||||
clearMarks(cm);
|
||||
if (cm.state.lint.options.lintOnChange !== false)
|
||||
cm.off("change", onChange);
|
||||
CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver);
|
||||
clearTimeout(cm.state.lint.timeout);
|
||||
delete cm.state.lint;
|
||||
}
|
||||
|
||||
if (val) {
|
||||
var gutters = cm.getOption("gutters"), hasLintGutter = false;
|
||||
for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true;
|
||||
var state = cm.state.lint = new LintState(cm, val, hasLintGutter);
|
||||
if (state.options.lintOnChange)
|
||||
cm.on("change", onChange);
|
||||
if (state.options.tooltips != false && state.options.tooltips != "gutter")
|
||||
CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver);
|
||||
|
||||
startLinting(cm);
|
||||
}
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("performLint", function() {
|
||||
startLinting(this);
|
||||
});
|
||||
});
|
||||
1
web/assets/codemirror/xq.min.css
vendored
Normal file
1
web/assets/codemirror/xq.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.CodeMirror{background-color:#f6fbfa;border:1px solid #d9d9d9}.cm-s-xq.CodeMirror{border-radius:1.5rem;height:auto;transition:background-color .3s,border-color 0.3s}.cm-s-xq.CodeMirror:hover{background-color:#e8f4f2;border-color:#18947b}.cm-s-xq div.CodeMirror-selected{background:rgb(0 102 85 / .1)}.cm-s-xq .CodeMirror-line::selection,.cm-s-xq .CodeMirror-line>span::selection,.cm-s-xq .CodeMirror-line>span>span::selection{background:rgb(0 102 85 / .1)}.cm-s-xq .CodeMirror-line::-moz-selection,.cm-s-xq .CodeMirror-line>span::-moz-selection,.cm-s-xq .CodeMirror-line>span>span::-moz-selection{background:rgb(0 102 85 / .1)}.cm-s-xq .CodeMirror-gutters{border-right:1px solid #ddd;background-color:rgb(221 221 221 / 20%);white-space:nowrap}.cm-s-xq span.cm-keyword{line-height:1em;font-weight:700;color:#5A5CAD}.cm-s-xq span.cm-atom{color:#7A316F;font-weight:700}.cm-s-xq span.cm-number{color:#e36209}.cm-s-xq span.cm-def{text-decoration:underline}.cm-s-xq span.cm-variable{color:#000}.cm-s-xq span.cm-variable-2{color:#000}.cm-s-xq span.cm-variable-3,.cm-s-xq span.cm-type{color:#000}.cm-s-xq span.cm-property{color:#008771}.cm-s-xq span.cm-comment{color:#bbb;font-style:italic}.cm-s-xq span.cm-meta{color:#ff0}.cm-s-xq span.cm-qualifier{color:grey}.cm-s-xq span.cm-builtin{color:#7EA656}.cm-s-xq span.cm-bracket{color:#cc7}.cm-s-xq span.cm-tag{color:#3F7F7F}.cm-s-xq span.cm-attribute{color:#7F007F}.cm-s-xq span.cm-error{color:#e04141}.cm-s-xq .CodeMirror-activeline-background{background:#e8f2ff}.cm-s-xq .CodeMirror-matchingbracket{outline:1px solid grey;color:black!important;background:#ff0}.dark .CodeMirror{background-color:var(--dark-color-surface-200);border-color:var(--dark-color-surface-300)}.dark .cm-s-xq.CodeMirror{background-color:var(--dark-color-surface-200);border-color:var(--dark-color-surface-300);color:rgb(255 255 255 / 65%)}.dark .cm-s-xq.CodeMirror:hover{background-color:rgb(0 50 42 / 30%);border-color:#008771}.dark .cm-s-xq div.CodeMirror-selected{background:var(--dark-color-codemirror-line-selection)}.dark .cm-s-xq .CodeMirror-line::selection,.dark .cm-s-xq .CodeMirror-line>span::selection,.dark .cm-s-xq .CodeMirror-line>span>span::selection{background:var(--dark-color-codemirror-line-selection)}.dark .cm-s-xq .CodeMirror-line::-moz-selection,.dark .cm-s-xq .CodeMirror-line>span::-moz-selection,.dark .cm-s-xq .CodeMirror-line>span>span::-moz-selection{background:var(--dark-color-codemirror-line-selection)}.dark .cm-s-xq .CodeMirror-gutters{background:rgb(0 0 0 / 30%);border-right:1px solid var(--dark-color-surface-300)}.dark .cm-s-xq .CodeMirror-guttermarker{color:#FFBD40}.dark .cm-s-xq .CodeMirror-guttermarker-subtle{color:rgb(255 255 255 / 70%)}.dark .cm-s-xq .CodeMirror-linenumber{color:rgb(255 255 255 / 50%)}.dark .cm-s-xq .CodeMirror-cursor{border-left:1px solid #fff}.dark .cm-s-xq span.cm-keyword{color:#FFBD40}.dark .cm-s-xq span.cm-atom{color:#c099ff}.dark .cm-s-xq span.cm-number{color:#9ccfd8}.dark .cm-s-xq span.cm-def{color:#FFF;text-decoration:underline}.dark .cm-s-xq span.cm-variable{color:#FFF}.dark .cm-s-xq span.cm-variable-2{color:#EEE}.dark .cm-s-xq span.cm-variable-3,.dark .cm-s-xq span.cm-type{color:#DDD}.dark .cm-s-xq span.cm-property{color:#f6c177}.dark .cm-s-xq span.cm-comment{color:gray}.dark .cm-s-xq span.cm-meta{color:#ff0}.dark .cm-s-xq span.cm-qualifier{color:#FFF700}.dark .cm-s-xq span.cm-builtin{color:#30a}.dark .cm-s-xq span.cm-bracket{color:#cc7}.dark .cm-s-xq span.cm-tag{color:#FFBD40}.dark .cm-s-xq span.cm-attribute{color:#FFF700}.dark .cm-s-xq span.cm-error{color:#e04141}.dark .cm-s-xq .CodeMirror-activeline-background{background:#27282E}.dark .cm-s-xq .CodeMirror-matchingbracket{outline:1px solid grey;color:white!important}.Line-Hover{transition:all .2s}.CodeMirror pre.CodeMirror-line:hover,.CodeMirror pre.CodeMirror-line-like:hover{background-color:rgb(0 102 85 / .05)}.dark .CodeMirror pre.CodeMirror-line:hover,.CodeMirror pre.CodeMirror-line-like:hover{background-color:var(--dark-color-codemirror-line-hover)}.CodeMirror-foldmarker{color:#fc8800;text-shadow:#ffd8aa 1px 1px 2px,#ffd8aa -1px -1px 2px,#ffd8aa 1px -1px 2px,#ffd8aa -1px 1px 2px;font-family:arial;line-height:.3;cursor:pointer}.dark .CodeMirror-foldmarker{color:#fff;text-shadow:#bbb 1px 1px 2px,#bbb -1px -1px 2px,#bbb 1px -1px 2px,#bbb -1px 1px 2px;font-family:arial;line-height:.3;cursor:pointer}
|
||||
1
web/assets/css/custom.min.css
vendored
Normal file
1
web/assets/css/custom.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
30
web/assets/js/axios-init.js
Normal file
30
web/assets/js/axios-init.js
Normal file
@@ -0,0 +1,30 @@
|
||||
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
|
||||
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
|
||||
axios.interceptors.request.use(
|
||||
(config) => {
|
||||
if (config.data instanceof FormData) {
|
||||
config.headers['Content-Type'] = 'multipart/form-data';
|
||||
} else {
|
||||
config.data = Qs.stringify(config.data, {
|
||||
arrayFormat: 'repeat',
|
||||
});
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error),
|
||||
);
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response) {
|
||||
const statusCode = error.response.status;
|
||||
// Check the status code
|
||||
if (statusCode === 401) { // Unauthorized
|
||||
return window.location.reload();
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
152
web/assets/js/model/dbinbound.js
Normal file
152
web/assets/js/model/dbinbound.js
Normal file
@@ -0,0 +1,152 @@
|
||||
class DBInbound {
|
||||
|
||||
constructor(data) {
|
||||
this.id = 0;
|
||||
this.userId = 0;
|
||||
this.up = 0;
|
||||
this.down = 0;
|
||||
this.total = 0;
|
||||
this.allTime = 0;
|
||||
this.remark = "";
|
||||
this.enable = true;
|
||||
this.expiryTime = 0;
|
||||
|
||||
// 新增:入站级设备限制(0 表示不限制)
|
||||
this.deviceLimit = 0;
|
||||
|
||||
this.listen = "";
|
||||
this.port = 0;
|
||||
this.protocol = "";
|
||||
this.settings = "";
|
||||
this.streamSettings = "";
|
||||
this.tag = "";
|
||||
this.sniffing = "";
|
||||
this.clientStats = ""
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
ObjectUtil.cloneProps(this, data);
|
||||
}
|
||||
|
||||
get totalGB() {
|
||||
return NumberFormatter.toFixed(this.total / SizeFormatter.ONE_GB, 2);
|
||||
}
|
||||
|
||||
set totalGB(gb) {
|
||||
this.total = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
|
||||
}
|
||||
|
||||
get isVMess() {
|
||||
return this.protocol === Protocols.VMESS;
|
||||
}
|
||||
|
||||
get isVLess() {
|
||||
return this.protocol === Protocols.VLESS;
|
||||
}
|
||||
|
||||
get isTrojan() {
|
||||
return this.protocol === Protocols.TROJAN;
|
||||
}
|
||||
|
||||
get isSS() {
|
||||
return this.protocol === Protocols.SHADOWSOCKS;
|
||||
}
|
||||
|
||||
get isSocks() {
|
||||
return this.protocol === Protocols.SOCKS;
|
||||
}
|
||||
|
||||
get isHTTP() {
|
||||
return this.protocol === Protocols.HTTP;
|
||||
}
|
||||
|
||||
get isWireguard() {
|
||||
return this.protocol === Protocols.WIREGUARD;
|
||||
}
|
||||
|
||||
get address() {
|
||||
let address = location.hostname;
|
||||
if (!ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0") {
|
||||
address = this.listen;
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
get _expiryTime() {
|
||||
if (this.expiryTime === 0) {
|
||||
return null;
|
||||
}
|
||||
return moment(this.expiryTime);
|
||||
}
|
||||
|
||||
set _expiryTime(t) {
|
||||
if (t == null) {
|
||||
this.expiryTime = 0;
|
||||
} else {
|
||||
this.expiryTime = t.valueOf();
|
||||
}
|
||||
}
|
||||
|
||||
get isExpiry() {
|
||||
return this.expiryTime < new Date().getTime();
|
||||
}
|
||||
|
||||
toInbound() {
|
||||
let settings = {};
|
||||
if (!ObjectUtil.isEmpty(this.settings)) {
|
||||
settings = JSON.parse(this.settings);
|
||||
}
|
||||
|
||||
let streamSettings = {};
|
||||
if (!ObjectUtil.isEmpty(this.streamSettings)) {
|
||||
streamSettings = JSON.parse(this.streamSettings);
|
||||
}
|
||||
|
||||
let sniffing = {};
|
||||
if (!ObjectUtil.isEmpty(this.sniffing)) {
|
||||
sniffing = JSON.parse(this.sniffing);
|
||||
}
|
||||
|
||||
const config = {
|
||||
port: this.port,
|
||||
listen: this.listen,
|
||||
protocol: this.protocol,
|
||||
settings: settings,
|
||||
streamSettings: streamSettings,
|
||||
tag: this.tag,
|
||||
sniffing: sniffing,
|
||||
clientStats: this.clientStats,
|
||||
};
|
||||
return Inbound.fromJson(config);
|
||||
}
|
||||
|
||||
isMultiUser() {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VMESS:
|
||||
case Protocols.VLESS:
|
||||
case Protocols.TROJAN:
|
||||
return true;
|
||||
case Protocols.SHADOWSOCKS:
|
||||
return this.toInbound().isSSMultiUser;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
hasLink() {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VMESS:
|
||||
case Protocols.VLESS:
|
||||
case Protocols.TROJAN:
|
||||
case Protocols.SHADOWSOCKS:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
genInboundLinks(remarkModel) {
|
||||
const inbound = this.toInbound();
|
||||
return inbound.genInboundLinks(this.remark, remarkModel);
|
||||
}
|
||||
}
|
||||
2690
web/assets/js/model/inbound.js
Normal file
2690
web/assets/js/model/inbound.js
Normal file
File diff suppressed because it is too large
Load Diff
1598
web/assets/js/model/outbound.js
Normal file
1598
web/assets/js/model/outbound.js
Normal file
File diff suppressed because it is too large
Load Diff
61
web/assets/js/model/setting.js
Normal file
61
web/assets/js/model/setting.js
Normal file
@@ -0,0 +1,61 @@
|
||||
class AllSetting {
|
||||
|
||||
constructor(data) {
|
||||
this.webListen = "";
|
||||
this.webDomain = "";
|
||||
this.webPort = 13688;
|
||||
this.webCertFile = "";
|
||||
this.webKeyFile = "";
|
||||
this.webBasePath = "/";
|
||||
this.sessionMaxAge = 360;
|
||||
this.pageSize = 50;
|
||||
this.expireDiff = 0;
|
||||
this.trafficDiff = 0;
|
||||
this.remarkModel = "-ieo";
|
||||
this.datepicker = "gregorian";
|
||||
this.tgBotEnable = false;
|
||||
this.tgBotToken = "";
|
||||
this.tgBotProxy = "";
|
||||
this.tgBotAPIServer = "";
|
||||
this.tgBotChatId = "";
|
||||
this.tgRunTime = "@daily";
|
||||
this.tgBotBackup = false;
|
||||
this.tgBotLoginNotify = true;
|
||||
this.tgCpu = 80;
|
||||
this.tgLang = "zh-CN";
|
||||
this.twoFactorEnable = false;
|
||||
this.twoFactorToken = "";
|
||||
this.xrayTemplateConfig = "";
|
||||
this.subEnable = false;
|
||||
this.subTitle = "";
|
||||
this.subListen = "";
|
||||
this.subPort = 13788;
|
||||
this.subPath = "/sub/";
|
||||
this.subJsonPath = "/json/";
|
||||
this.subDomain = "";
|
||||
this.externalTrafficInformEnable = false;
|
||||
this.externalTrafficInformURI = "";
|
||||
this.subCertFile = "";
|
||||
this.subKeyFile = "";
|
||||
this.subUpdates = 12;
|
||||
this.subEncrypt = true;
|
||||
this.subShowInfo = true;
|
||||
this.subURI = "";
|
||||
this.subJsonURI = "";
|
||||
this.subJsonFragment = "";
|
||||
this.subJsonNoises = "";
|
||||
this.subJsonMux = "";
|
||||
this.subJsonRules = "";
|
||||
|
||||
this.timeLocation = "Local";
|
||||
|
||||
if (data == null) {
|
||||
return
|
||||
}
|
||||
ObjectUtil.cloneProps(this, data);
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
return ObjectUtil.equals(this, other);
|
||||
}
|
||||
}
|
||||
151
web/assets/js/util/date-util.js
Normal file
151
web/assets/js/util/date-util.js
Normal file
@@ -0,0 +1,151 @@
|
||||
const oneMinute = 1000 * 60; // MilliseConds in a Minute
|
||||
const oneHour = oneMinute * 60; // The milliseconds of one hour
|
||||
const oneDay = oneHour * 24; // The Number of MilliseConds A Day
|
||||
const oneWeek = oneDay * 7; // The milliseconds per week
|
||||
const oneMonth = oneDay * 30; // The milliseconds of a month
|
||||
|
||||
/**
|
||||
* Decrease according to the number of days
|
||||
*
|
||||
* @param days to reduce the number of days to be reduced
|
||||
*/
|
||||
Date.prototype.minusDays = function (days) {
|
||||
return this.minusMillis(oneDay * days);
|
||||
};
|
||||
|
||||
/**
|
||||
* Increase according to the number of days
|
||||
*
|
||||
* @param days The number of days to be increased
|
||||
*/
|
||||
Date.prototype.plusDays = function (days) {
|
||||
return this.plusMillis(oneDay * days);
|
||||
};
|
||||
|
||||
/**
|
||||
* A few
|
||||
*
|
||||
* @param hours to be reduced
|
||||
*/
|
||||
Date.prototype.minusHours = function (hours) {
|
||||
return this.minusMillis(oneHour * hours);
|
||||
};
|
||||
|
||||
/**
|
||||
* Increase hourly
|
||||
*
|
||||
* @param hours to increase the number of hours
|
||||
*/
|
||||
Date.prototype.plusHours = function (hours) {
|
||||
return this.plusMillis(oneHour * hours);
|
||||
};
|
||||
|
||||
/**
|
||||
* Make reduction in minutes
|
||||
*
|
||||
* @param minutes to reduce the number of minutes
|
||||
*/
|
||||
Date.prototype.minusMinutes = function (minutes) {
|
||||
return this.minusMillis(oneMinute * minutes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add in minutes
|
||||
*
|
||||
* @param minutes to increase the number of minutes
|
||||
*/
|
||||
Date.prototype.plusMinutes = function (minutes) {
|
||||
return this.plusMillis(oneMinute * minutes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Decrease in milliseconds
|
||||
*
|
||||
* @param millis to reduce the milliseconds
|
||||
*/
|
||||
Date.prototype.minusMillis = function(millis) {
|
||||
let time = this.getTime() - millis;
|
||||
let newDate = new Date();
|
||||
newDate.setTime(time);
|
||||
return newDate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add in milliseconds to increase
|
||||
*
|
||||
* @param millis to increase the milliseconds to increase
|
||||
*/
|
||||
Date.prototype.plusMillis = function(millis) {
|
||||
let time = this.getTime() + millis;
|
||||
let newDate = new Date();
|
||||
newDate.setTime(time);
|
||||
return newDate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Setting time is 00: 00: 00.000 on the day
|
||||
*/
|
||||
Date.prototype.setMinTime = function () {
|
||||
this.setHours(0);
|
||||
this.setMinutes(0);
|
||||
this.setSeconds(0);
|
||||
this.setMilliseconds(0);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Setting time is 23: 59: 59.999 on the same day
|
||||
*/
|
||||
Date.prototype.setMaxTime = function () {
|
||||
this.setHours(23);
|
||||
this.setMinutes(59);
|
||||
this.setSeconds(59);
|
||||
this.setMilliseconds(999);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatting date
|
||||
*/
|
||||
Date.prototype.formatDate = function () {
|
||||
return this.getFullYear() + "-" + NumberFormatter.addZero(this.getMonth() + 1) + "-" + NumberFormatter.addZero(this.getDate());
|
||||
};
|
||||
|
||||
/**
|
||||
* Format time
|
||||
*/
|
||||
Date.prototype.formatTime = function () {
|
||||
return NumberFormatter.addZero(this.getHours()) + ":" + NumberFormatter.addZero(this.getMinutes()) + ":" + NumberFormatter.addZero(this.getSeconds());
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatting date plus time
|
||||
*
|
||||
* @param split Date and time separation symbols, default is a space
|
||||
*/
|
||||
Date.prototype.formatDateTime = function (split = ' ') {
|
||||
return this.formatDate() + split + this.formatTime();
|
||||
};
|
||||
|
||||
class DateUtil {
|
||||
// String to date object
|
||||
static parseDate(str) {
|
||||
return new Date(str.replace(/-/g, '/'));
|
||||
}
|
||||
|
||||
static formatMillis(millis) {
|
||||
return moment(millis).format('YYYY-M-D HH:mm:ss');
|
||||
}
|
||||
|
||||
static firstDayOfMonth() {
|
||||
const date = new Date();
|
||||
date.setDate(1);
|
||||
date.setMinTime();
|
||||
return date;
|
||||
}
|
||||
|
||||
static convertToJalalian(date) {
|
||||
return date && moment.isMoment(date) ? date.format('jYYYY/jMM/jDD HH:mm:ss') : null;
|
||||
}
|
||||
|
||||
}
|
||||
911
web/assets/js/util/index.js
Normal file
911
web/assets/js/util/index.js
Normal file
@@ -0,0 +1,911 @@
|
||||
class Msg {
|
||||
constructor(success = false, msg = "", obj = null) {
|
||||
this.success = success;
|
||||
this.msg = msg;
|
||||
this.obj = obj;
|
||||
}
|
||||
}
|
||||
|
||||
class HttpUtil {
|
||||
static _handleMsg(msg) {
|
||||
if (!(msg instanceof Msg) || msg.msg === "") {
|
||||
return;
|
||||
}
|
||||
const messageType = msg.success ? 'success' : 'error';
|
||||
Vue.prototype.$message[messageType](msg.msg);
|
||||
}
|
||||
|
||||
static _respToMsg(resp) {
|
||||
if (!resp || !resp.data) {
|
||||
return new Msg(false, 'No response data');
|
||||
}
|
||||
const { data } = resp;
|
||||
if (data == null) {
|
||||
return new Msg(true);
|
||||
}
|
||||
if (typeof data === 'object' && 'success' in data) {
|
||||
return new Msg(data.success, data.msg, data.obj);
|
||||
}
|
||||
return typeof data === 'object' ? data : new Msg(false, 'unknown data:', data);
|
||||
}
|
||||
|
||||
static async postForm(url, data) {
|
||||
const formData = new URLSearchParams();
|
||||
for (const key in data) {
|
||||
formData.append(key, data[key]);
|
||||
}
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: formData.toString()
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
|
||||
static async get(url, params, options = {}) {
|
||||
try {
|
||||
const resp = await axios.get(url, { params, ...options });
|
||||
const msg = this._respToMsg(resp);
|
||||
this._handleMsg(msg);
|
||||
return msg;
|
||||
} catch (error) {
|
||||
console.error('GET request failed:', error);
|
||||
const errorMsg = new Msg(false, error.response?.data?.message || error.message || 'Request failed');
|
||||
this._handleMsg(errorMsg);
|
||||
return errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
static async post(url, data, options = {}) {
|
||||
try {
|
||||
const resp = await axios.post(url, data, options);
|
||||
const msg = this._respToMsg(resp);
|
||||
this._handleMsg(msg);
|
||||
return msg;
|
||||
} catch (error) {
|
||||
console.error('POST request failed:', error);
|
||||
const errorMsg = new Msg(false, error.response?.data?.message || error.message || 'Request failed');
|
||||
this._handleMsg(errorMsg);
|
||||
return errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
static async postWithModal(url, data, modal) {
|
||||
if (modal) {
|
||||
modal.loading(true);
|
||||
}
|
||||
const msg = await this.post(url, data);
|
||||
if (modal) {
|
||||
modal.loading(false);
|
||||
if (msg instanceof Msg && msg.success) {
|
||||
modal.close();
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
class PromiseUtil {
|
||||
static async sleep(timeout) {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, timeout)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class RandomUtil {
|
||||
static getSeq({ type = "default", hasNumbers = true, hasLowercase = true, hasUppercase = true } = {}) {
|
||||
let seq = '';
|
||||
|
||||
switch (type) {
|
||||
case "hex":
|
||||
seq += "0123456789abcdef";
|
||||
break;
|
||||
default:
|
||||
if (hasNumbers) seq += "0123456789";
|
||||
if (hasLowercase) seq += "abcdefghijklmnopqrstuvwxyz";
|
||||
if (hasUppercase) seq += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
break;
|
||||
}
|
||||
|
||||
return seq;
|
||||
}
|
||||
|
||||
static randomInteger(min, max) {
|
||||
const range = max - min + 1;
|
||||
const randomBuffer = new Uint32Array(1);
|
||||
window.crypto.getRandomValues(randomBuffer);
|
||||
return Math.floor((randomBuffer[0] / (0xFFFFFFFF + 1)) * range) + min;
|
||||
}
|
||||
|
||||
static randomSeq(count, options = {}) {
|
||||
const seq = this.getSeq(options);
|
||||
const seqLength = seq.length;
|
||||
const randomValues = new Uint32Array(count);
|
||||
window.crypto.getRandomValues(randomValues);
|
||||
return Array.from(randomValues, v => seq[v % seqLength]).join('');
|
||||
}
|
||||
|
||||
static randomShortIds() {
|
||||
const lengths = [2, 4, 6, 8, 10, 12, 14, 16].sort(() => Math.random() - 0.5);
|
||||
|
||||
return lengths.map(len => this.randomSeq(len, { type: "hex" })).join(',');
|
||||
}
|
||||
|
||||
static randomLowerAndNum(len) {
|
||||
return this.randomSeq(len, { hasUppercase: false });
|
||||
}
|
||||
|
||||
static randomUUID() {
|
||||
if (window.location.protocol === "https:") {
|
||||
return window.crypto.randomUUID();
|
||||
} else {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
|
||||
.replace(/[xy]/g, function (c) {
|
||||
const randomValues = new Uint8Array(1);
|
||||
window.crypto.getRandomValues(randomValues);
|
||||
let randomValue = randomValues[0] % 16;
|
||||
let calculatedValue = (c === 'x') ? randomValue : (randomValue & 0x3 | 0x8);
|
||||
return calculatedValue.toString(16);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static randomShadowsocksPassword(method = SSMethods.BLAKE3_AES_256_GCM) {
|
||||
let length = 32;
|
||||
|
||||
if ([SSMethods.BLAKE3_AES_128_GCM].includes(method)) {
|
||||
length = 16;
|
||||
}
|
||||
|
||||
const array = new Uint8Array(length);
|
||||
|
||||
window.crypto.getRandomValues(array);
|
||||
|
||||
return Base64.alternativeEncode(String.fromCharCode(...array));
|
||||
}
|
||||
|
||||
static randomBase32String(length = 16) {
|
||||
const array = new Uint8Array(length);
|
||||
|
||||
window.crypto.getRandomValues(array);
|
||||
|
||||
const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
let result = '';
|
||||
let bits = 0;
|
||||
let buffer = 0;
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
buffer = (buffer << 8) | array[i];
|
||||
bits += 8;
|
||||
|
||||
while (bits >= 5) {
|
||||
bits -= 5;
|
||||
result += base32Chars[(buffer >>> bits) & 0x1F];
|
||||
}
|
||||
}
|
||||
|
||||
if (bits > 0) {
|
||||
result += base32Chars[(buffer << (5 - bits)) & 0x1F];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class ObjectUtil {
|
||||
static getPropIgnoreCase(obj, prop) {
|
||||
for (const name in obj) {
|
||||
if (!obj.hasOwnProperty(name)) {
|
||||
continue;
|
||||
}
|
||||
if (name.toLowerCase() === prop.toLowerCase()) {
|
||||
return obj[name];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static deepSearch(obj, key) {
|
||||
if (obj instanceof Array) {
|
||||
for (let i = 0; i < obj.length; ++i) {
|
||||
if (this.deepSearch(obj[i], key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (obj instanceof Object) {
|
||||
for (let name in obj) {
|
||||
if (!obj.hasOwnProperty(name)) {
|
||||
continue;
|
||||
}
|
||||
if (this.deepSearch(obj[name], key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return this.isEmpty(obj) ? false : obj.toString().toLowerCase().indexOf(key.toLowerCase()) >= 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static isEmpty(obj) {
|
||||
return obj === null || obj === undefined || obj === '';
|
||||
}
|
||||
|
||||
static isArrEmpty(arr) {
|
||||
return !this.isEmpty(arr) && arr.length === 0;
|
||||
}
|
||||
|
||||
static copyArr(dest, src) {
|
||||
dest.splice(0);
|
||||
for (const item of src) {
|
||||
dest.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
static clone(obj) {
|
||||
let newObj;
|
||||
if (obj instanceof Array) {
|
||||
newObj = [];
|
||||
this.copyArr(newObj, obj);
|
||||
} else if (obj instanceof Object) {
|
||||
newObj = {};
|
||||
for (const key of Object.keys(obj)) {
|
||||
newObj[key] = obj[key];
|
||||
}
|
||||
} else {
|
||||
newObj = obj;
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
|
||||
static deepClone(obj) {
|
||||
let newObj;
|
||||
if (obj instanceof Array) {
|
||||
newObj = [];
|
||||
for (const item of obj) {
|
||||
newObj.push(this.deepClone(item));
|
||||
}
|
||||
} else if (obj instanceof Object) {
|
||||
newObj = {};
|
||||
for (const key of Object.keys(obj)) {
|
||||
newObj[key] = this.deepClone(obj[key]);
|
||||
}
|
||||
} else {
|
||||
newObj = obj;
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
|
||||
static cloneProps(dest, src, ...ignoreProps) {
|
||||
if (dest == null || src == null) {
|
||||
return;
|
||||
}
|
||||
const ignoreEmpty = this.isArrEmpty(ignoreProps);
|
||||
for (const key of Object.keys(src)) {
|
||||
if (!src.hasOwnProperty(key)) {
|
||||
continue;
|
||||
} else if (!dest.hasOwnProperty(key)) {
|
||||
continue;
|
||||
} else if (src[key] === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (ignoreEmpty) {
|
||||
dest[key] = src[key];
|
||||
} else {
|
||||
let ignore = false;
|
||||
for (let i = 0; i < ignoreProps.length; ++i) {
|
||||
if (key === ignoreProps[i]) {
|
||||
ignore = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ignore) {
|
||||
dest[key] = src[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static delProps(obj, ...props) {
|
||||
for (const prop of props) {
|
||||
if (prop in obj) {
|
||||
delete obj[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static execute(func, ...args) {
|
||||
if (!this.isEmpty(func) && typeof func === 'function') {
|
||||
func(...args);
|
||||
}
|
||||
}
|
||||
|
||||
static orDefault(obj, defaultValue) {
|
||||
if (obj == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
static equals(a, b) {
|
||||
for (const key in a) {
|
||||
if (!a.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
if (!b.hasOwnProperty(key)) {
|
||||
return false;
|
||||
} else if (a[key] !== b[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const key in b) {
|
||||
if (!b.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
if (!a.hasOwnProperty(key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class Wireguard {
|
||||
static gf(init) {
|
||||
var r = new Float64Array(16);
|
||||
if (init) {
|
||||
for (var i = 0; i < init.length; ++i)
|
||||
r[i] = init[i];
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
static pack(o, n) {
|
||||
var b, m = this.gf(), t = this.gf();
|
||||
for (var i = 0; i < 16; ++i)
|
||||
t[i] = n[i];
|
||||
this.carry(t);
|
||||
this.carry(t);
|
||||
this.carry(t);
|
||||
for (var j = 0; j < 2; ++j) {
|
||||
m[0] = t[0] - 0xffed;
|
||||
for (var i = 1; i < 15; ++i) {
|
||||
m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
|
||||
m[i - 1] &= 0xffff;
|
||||
}
|
||||
m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
|
||||
b = (m[15] >> 16) & 1;
|
||||
m[14] &= 0xffff;
|
||||
this.cswap(t, m, 1 - b);
|
||||
}
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
o[2 * i] = t[i] & 0xff;
|
||||
o[2 * i + 1] = t[i] >> 8;
|
||||
}
|
||||
}
|
||||
|
||||
static carry(o) {
|
||||
var c;
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
o[(i + 1) % 16] += (i < 15 ? 1 : 38) * Math.floor(o[i] / 65536);
|
||||
o[i] &= 0xffff;
|
||||
}
|
||||
}
|
||||
|
||||
static cswap(p, q, b) {
|
||||
var t, c = ~(b - 1);
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
t = c & (p[i] ^ q[i]);
|
||||
p[i] ^= t;
|
||||
q[i] ^= t;
|
||||
}
|
||||
}
|
||||
|
||||
static add(o, a, b) {
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = (a[i] + b[i]) | 0;
|
||||
}
|
||||
|
||||
static subtract(o, a, b) {
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = (a[i] - b[i]) | 0;
|
||||
}
|
||||
|
||||
static multmod(o, a, b) {
|
||||
var t = new Float64Array(31);
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
for (var j = 0; j < 16; ++j)
|
||||
t[i + j] += a[i] * b[j];
|
||||
}
|
||||
for (var i = 0; i < 15; ++i)
|
||||
t[i] += 38 * t[i + 16];
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = t[i];
|
||||
this.carry(o);
|
||||
this.carry(o);
|
||||
}
|
||||
|
||||
static invert(o, i) {
|
||||
var c = this.gf();
|
||||
for (var a = 0; a < 16; ++a)
|
||||
c[a] = i[a];
|
||||
for (var a = 253; a >= 0; --a) {
|
||||
this.multmod(c, c, c);
|
||||
if (a !== 2 && a !== 4)
|
||||
this.multmod(c, c, i);
|
||||
}
|
||||
for (var a = 0; a < 16; ++a)
|
||||
o[a] = c[a];
|
||||
}
|
||||
|
||||
static clamp(z) {
|
||||
z[31] = (z[31] & 127) | 64;
|
||||
z[0] &= 248;
|
||||
}
|
||||
|
||||
static generatePublicKey(privateKey) {
|
||||
var r, z = new Uint8Array(32);
|
||||
var a = this.gf([1]),
|
||||
b = this.gf([9]),
|
||||
c = this.gf(),
|
||||
d = this.gf([1]),
|
||||
e = this.gf(),
|
||||
f = this.gf(),
|
||||
_121665 = this.gf([0xdb41, 1]),
|
||||
_9 = this.gf([9]);
|
||||
for (var i = 0; i < 32; ++i)
|
||||
z[i] = privateKey[i];
|
||||
this.clamp(z);
|
||||
for (var i = 254; i >= 0; --i) {
|
||||
r = (z[i >>> 3] >>> (i & 7)) & 1;
|
||||
this.cswap(a, b, r);
|
||||
this.cswap(c, d, r);
|
||||
this.add(e, a, c);
|
||||
this.subtract(a, a, c);
|
||||
this.add(c, b, d);
|
||||
this.subtract(b, b, d);
|
||||
this.multmod(d, e, e);
|
||||
this.multmod(f, a, a);
|
||||
this.multmod(a, c, a);
|
||||
this.multmod(c, b, e);
|
||||
this.add(e, a, c);
|
||||
this.subtract(a, a, c);
|
||||
this.multmod(b, a, a);
|
||||
this.subtract(c, d, f);
|
||||
this.multmod(a, c, _121665);
|
||||
this.add(a, a, d);
|
||||
this.multmod(c, c, a);
|
||||
this.multmod(a, d, f);
|
||||
this.multmod(d, b, _9);
|
||||
this.multmod(b, e, e);
|
||||
this.cswap(a, b, r);
|
||||
this.cswap(c, d, r);
|
||||
}
|
||||
this.invert(c, c);
|
||||
this.multmod(a, a, c);
|
||||
this.pack(z, a);
|
||||
return z;
|
||||
}
|
||||
|
||||
static generatePresharedKey() {
|
||||
var privateKey = new Uint8Array(32);
|
||||
window.crypto.getRandomValues(privateKey);
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
static generatePrivateKey() {
|
||||
var privateKey = this.generatePresharedKey();
|
||||
this.clamp(privateKey);
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
static encodeBase64(dest, src) {
|
||||
var input = Uint8Array.from([(src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63]);
|
||||
for (var i = 0; i < 4; ++i)
|
||||
dest[i] = input[i] + 65 +
|
||||
(((25 - input[i]) >> 8) & 6) -
|
||||
(((51 - input[i]) >> 8) & 75) -
|
||||
(((61 - input[i]) >> 8) & 15) +
|
||||
(((62 - input[i]) >> 8) & 3);
|
||||
}
|
||||
|
||||
static keyToBase64(key) {
|
||||
var i, base64 = new Uint8Array(44);
|
||||
for (i = 0; i < 32 / 3; ++i)
|
||||
this.encodeBase64(base64.subarray(i * 4), key.subarray(i * 3));
|
||||
this.encodeBase64(base64.subarray(i * 4), Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0]));
|
||||
base64[43] = 61;
|
||||
return String.fromCharCode.apply(null, base64);
|
||||
}
|
||||
|
||||
static keyFromBase64(encoded) {
|
||||
const binaryStr = atob(encoded);
|
||||
const bytes = new Uint8Array(binaryStr.length);
|
||||
for (let i = 0; i < binaryStr.length; i++) {
|
||||
bytes[i] = binaryStr.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static generateKeypair(secretKey = '') {
|
||||
var privateKey = secretKey.length > 0 ? this.keyFromBase64(secretKey) : this.generatePrivateKey();
|
||||
var publicKey = this.generatePublicKey(privateKey);
|
||||
return {
|
||||
publicKey: this.keyToBase64(publicKey),
|
||||
privateKey: secretKey.length > 0 ? secretKey : this.keyToBase64(privateKey)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ClipboardManager {
|
||||
static copyText(content = "") {
|
||||
// !! here old way of copying is used because not everyone can afford https connection
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const textarea = window.document.createElement('textarea');
|
||||
|
||||
textarea.style.fontSize = '12pt';
|
||||
textarea.style.border = '0';
|
||||
textarea.style.padding = '0';
|
||||
textarea.style.margin = '0';
|
||||
textarea.style.position = 'absolute';
|
||||
textarea.style.left = '-9999px';
|
||||
textarea.style.top = `${window.pageYOffset || document.documentElement.scrollTop}px`;
|
||||
textarea.setAttribute('readonly', '');
|
||||
textarea.value = content;
|
||||
|
||||
window.document.body.appendChild(textarea);
|
||||
|
||||
textarea.select();
|
||||
window.document.execCommand("copy");
|
||||
|
||||
window.document.body.removeChild(textarea);
|
||||
|
||||
resolve(true)
|
||||
} catch {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class Base64 {
|
||||
static encode(content = "", safe = false) {
|
||||
if (safe) {
|
||||
return Base64.encode(content)
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/=/g, '')
|
||||
.replace(/\//g, '_')
|
||||
}
|
||||
|
||||
return window.btoa(
|
||||
String.fromCharCode(...new TextEncoder().encode(content))
|
||||
)
|
||||
}
|
||||
|
||||
static alternativeEncode(content) {
|
||||
return window.btoa(
|
||||
content
|
||||
)
|
||||
}
|
||||
|
||||
static decode(content = "") {
|
||||
return new TextDecoder()
|
||||
.decode(
|
||||
Uint8Array.from(window.atob(content), c => c.charCodeAt(0))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SizeFormatter {
|
||||
static ONE_KB = 1024;
|
||||
static ONE_MB = this.ONE_KB * 1024;
|
||||
static ONE_GB = this.ONE_MB * 1024;
|
||||
static ONE_TB = this.ONE_GB * 1024;
|
||||
static ONE_PB = this.ONE_TB * 1024;
|
||||
|
||||
static sizeFormat(size) {
|
||||
if (size <= 0) return "0 B";
|
||||
if (size < this.ONE_KB) return size.toFixed(0) + " B";
|
||||
if (size < this.ONE_MB) return (size / this.ONE_KB).toFixed(2) + " KB";
|
||||
if (size < this.ONE_GB) return (size / this.ONE_MB).toFixed(2) + " MB";
|
||||
if (size < this.ONE_TB) return (size / this.ONE_GB).toFixed(2) + " GB";
|
||||
if (size < this.ONE_PB) return (size / this.ONE_TB).toFixed(2) + " TB";
|
||||
return (size / this.ONE_PB).toFixed(2) + " PB";
|
||||
}
|
||||
}
|
||||
|
||||
class CPUFormatter {
|
||||
static cpuSpeedFormat(speed) {
|
||||
return speed > 1000 ? (speed / 1000).toFixed(2) + " GHz" : speed.toFixed(2) + " MHz";
|
||||
}
|
||||
|
||||
static cpuCoreFormat(cores) {
|
||||
return cores === 1 ? "1 Core" : cores + " Cores";
|
||||
}
|
||||
}
|
||||
|
||||
class TimeFormatter {
|
||||
static formatSecond(second) {
|
||||
if (second < 60) return second.toFixed(0) + 's';
|
||||
if (second < 3600) return (second / 60).toFixed(0) + 'm';
|
||||
if (second < 3600 * 24) return (second / 3600).toFixed(0) + 'h';
|
||||
let day = Math.floor(second / 3600 / 24);
|
||||
let remain = ((second / 3600) - (day * 24)).toFixed(0);
|
||||
return day + 'd' + (remain > 0 ? ' ' + remain + 'h' : '');
|
||||
}
|
||||
}
|
||||
|
||||
class NumberFormatter {
|
||||
static addZero(num) {
|
||||
return num < 10 ? "0" + num : num;
|
||||
}
|
||||
|
||||
static toFixed(num, n) {
|
||||
n = Math.pow(10, n);
|
||||
return Math.floor(num * n) / n;
|
||||
}
|
||||
}
|
||||
|
||||
class Utils {
|
||||
static debounce(fn, delay) {
|
||||
let timeoutID = null;
|
||||
return function () {
|
||||
clearTimeout(timeoutID);
|
||||
let args = arguments;
|
||||
let that = this;
|
||||
timeoutID = setTimeout(() => fn.apply(that, args), delay);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CookieManager {
|
||||
static getCookie(cname) {
|
||||
let name = cname + '=';
|
||||
let ca = document.cookie.split(';');
|
||||
for (let c of ca) {
|
||||
c = c.trim();
|
||||
if (c.indexOf(name) === 0) {
|
||||
return decodeURIComponent(c.substring(name.length, c.length));
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
static setCookie(cname, cvalue, exdays) {
|
||||
const d = new Date();
|
||||
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
|
||||
let expires = 'expires=' + d.toUTCString();
|
||||
document.cookie = cname + '=' + encodeURIComponent(cvalue) + ';' + expires + ';path=/';
|
||||
}
|
||||
}
|
||||
|
||||
class ColorUtils {
|
||||
static usageColor(data, threshold, total) {
|
||||
switch (true) {
|
||||
case data === null: return "purple";
|
||||
case total < 0: return "green";
|
||||
case total == 0: return "purple";
|
||||
case data < total - threshold: return "green";
|
||||
case data < total: return "orange";
|
||||
default: return "red";
|
||||
}
|
||||
}
|
||||
|
||||
static clientUsageColor(clientStats, trafficDiff) {
|
||||
switch (true) {
|
||||
case !clientStats || clientStats.total == 0: return "#7a316f";
|
||||
case clientStats.up + clientStats.down < clientStats.total - trafficDiff: return "#008771";
|
||||
case clientStats.up + clientStats.down < clientStats.total: return "#f37b24";
|
||||
default: return "#cf3c3c";
|
||||
}
|
||||
}
|
||||
|
||||
static userExpiryColor(threshold, client, isDark = false) {
|
||||
if (!client.enable) return isDark ? '#2c3950' : '#bcbcbc';
|
||||
let now = new Date().getTime(), expiry = client.expiryTime;
|
||||
switch (true) {
|
||||
case expiry === null: return "#7a316f";
|
||||
case expiry < 0: return "#008771";
|
||||
case expiry == 0: return "#7a316f";
|
||||
case now < expiry - threshold: return "#008771";
|
||||
case now < expiry: return "#f37b24";
|
||||
default: return "#cf3c3c";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ArrayUtils {
|
||||
static doAllItemsExist(array1, array2) {
|
||||
return array1.every(item => array2.includes(item));
|
||||
}
|
||||
}
|
||||
|
||||
class URLBuilder {
|
||||
static buildURL({ host, port, isTLS, base, path }) {
|
||||
if (!host || host.length === 0) host = window.location.hostname;
|
||||
if (!port || port.length === 0) port = window.location.port;
|
||||
if (isTLS === undefined) isTLS = window.location.protocol === "https:";
|
||||
|
||||
const protocol = isTLS ? "https:" : "http:";
|
||||
port = String(port);
|
||||
if (port === "" || (isTLS && port === "443") || (!isTLS && port === "80")) {
|
||||
port = "";
|
||||
} else {
|
||||
port = `:${port}`;
|
||||
}
|
||||
|
||||
return `${protocol}//${host}${port}${base}${path}`;
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageManager {
|
||||
static supportedLanguages = [
|
||||
{
|
||||
name: "العربية",
|
||||
value: "ar-EG",
|
||||
icon: "🇪🇬",
|
||||
},
|
||||
{
|
||||
name: "English",
|
||||
value: "en-US",
|
||||
icon: "🇺🇸",
|
||||
},
|
||||
{
|
||||
name: "فارسی",
|
||||
value: "fa-IR",
|
||||
icon: "🇮🇷",
|
||||
},
|
||||
{
|
||||
name: "简体中文",
|
||||
value: "zh-CN",
|
||||
icon: "🇨🇳",
|
||||
},
|
||||
{
|
||||
name: "繁體中文",
|
||||
value: "zh-TW",
|
||||
icon: "🇹🇼",
|
||||
},
|
||||
{
|
||||
name: "日本語",
|
||||
value: "ja-JP",
|
||||
icon: "🇯🇵",
|
||||
},
|
||||
{
|
||||
name: "Русский",
|
||||
value: "ru-RU",
|
||||
icon: "🇷🇺",
|
||||
},
|
||||
{
|
||||
name: "Tiếng Việt",
|
||||
value: "vi-VN",
|
||||
icon: "🇻🇳",
|
||||
},
|
||||
{
|
||||
name: "Español",
|
||||
value: "es-ES",
|
||||
icon: "🇪🇸",
|
||||
},
|
||||
{
|
||||
name: "Indonesian",
|
||||
value: "id-ID",
|
||||
icon: "🇮🇩",
|
||||
},
|
||||
{
|
||||
name: "Український",
|
||||
value: "uk-UA",
|
||||
icon: "🇺🇦",
|
||||
},
|
||||
{
|
||||
name: "Türkçe",
|
||||
value: "tr-TR",
|
||||
icon: "🇹🇷",
|
||||
},
|
||||
{
|
||||
name: "Português",
|
||||
value: "pt-BR",
|
||||
icon: "🇧🇷",
|
||||
}
|
||||
]
|
||||
|
||||
static getLanguage() {
|
||||
let lang = CookieManager.getCookie("lang");
|
||||
|
||||
if (!lang) {
|
||||
if (window.navigator) {
|
||||
lang = window.navigator.language || window.navigator.userLanguage;
|
||||
|
||||
const simularLangs = [
|
||||
["ar", this.supportedLanguages[0].value],
|
||||
["fa", this.supportedLanguages[2].value],
|
||||
["ja", this.supportedLanguages[5].value],
|
||||
["ru", this.supportedLanguages[6].value],
|
||||
["vi", this.supportedLanguages[7].value],
|
||||
["es", this.supportedLanguages[8].value],
|
||||
["id", this.supportedLanguages[9].value],
|
||||
["uk", this.supportedLanguages[10].value],
|
||||
["tr", this.supportedLanguages[11].value],
|
||||
["pt", this.supportedLanguages[12].value],
|
||||
]
|
||||
|
||||
simularLangs.forEach((pair) => {
|
||||
if (lang === pair[0]) {
|
||||
lang = pair[1];
|
||||
}
|
||||
});
|
||||
|
||||
if (LanguageManager.isSupportLanguage(lang)) {
|
||||
CookieManager.setCookie("lang", lang, 150);
|
||||
} else {
|
||||
CookieManager.setCookie("lang", "zh-CN", 150);
|
||||
window.location.reload();
|
||||
}
|
||||
} else {
|
||||
CookieManager.setCookie("lang", "zh-CN", 150);
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
return lang;
|
||||
}
|
||||
|
||||
static setLanguage(language) {
|
||||
if (!LanguageManager.isSupportLanguage(language)) {
|
||||
language = "zh-CN";
|
||||
}
|
||||
|
||||
CookieManager.setCookie("lang", language, 150);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
static isSupportLanguage(language) {
|
||||
const languageFilter = LanguageManager.supportedLanguages.filter((lang) => {
|
||||
return lang.value === language
|
||||
})
|
||||
|
||||
return languageFilter.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
const MediaQueryMixin = {
|
||||
data() {
|
||||
return {
|
||||
isMobile: window.innerWidth <= 768,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
updateDeviceType() {
|
||||
this.isMobile = window.innerWidth <= 768;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.updateDeviceType);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.updateDeviceType);
|
||||
},
|
||||
}
|
||||
|
||||
class FileManager {
|
||||
static downloadTextFile(content, filename = 'file.txt', options = { type: "text/plain" }) {
|
||||
let link = window.document.createElement('a');
|
||||
|
||||
link.download = filename;
|
||||
link.style.border = '0';
|
||||
link.style.padding = '0';
|
||||
link.style.margin = '0';
|
||||
link.style.position = 'absolute';
|
||||
link.style.left = '-9999px';
|
||||
link.style.top = `${window.pageYOffset || window.document.documentElement.scrollTop}px`;
|
||||
link.href = URL.createObjectURL(new Blob([content], options));
|
||||
link.click();
|
||||
|
||||
URL.revokeObjectURL(link.href);
|
||||
|
||||
link.remove();
|
||||
}
|
||||
}
|
||||
1
web/assets/moment/moment-jalali.min.js
vendored
Normal file
1
web/assets/moment/moment-jalali.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
web/assets/moment/moment.min.js
vendored
Normal file
2
web/assets/moment/moment.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
19
web/assets/otpauth/otpauth.umd.min.js
vendored
Normal file
19
web/assets/otpauth/otpauth.umd.min.js
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
//! otpauth 9.4.0 | (c) Héctor Molinero Fernández | MIT | https://github.com/hectorm/otpauth
|
||||
//! noble-hashes 1.7.1 | (c) Paul Miller | MIT | https://github.com/paulmillr/noble-hashes
|
||||
/// <reference types="./otpauth.d.ts" />
|
||||
// @ts-nocheck
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).OTPAuth={})}(this,(function(t){"use strict";function e(t){if(!Number.isSafeInteger(t)||t<0)throw new Error("positive integer expected, got "+t)}function s(t,...e){if(!((s=t)instanceof Uint8Array||ArrayBuffer.isView(s)&&"Uint8Array"===s.constructor.name))throw new Error("Uint8Array expected");var s;if(e.length>0&&!e.includes(t.length))throw new Error("Uint8Array expected of length "+e+", got length="+t.length)}function i(t,e=!0){if(t.destroyed)throw new Error("Hash instance has been destroyed");if(e&&t.finished)throw new Error("Hash#digest() has already been called")}function r(t,e){s(t);const i=e.outputLen;if(t.length<i)throw new Error("digestInto() expects output buffer of length at least "+i)}function n(t){return new DataView(t.buffer,t.byteOffset,t.byteLength)}function o(t,e){return t<<32-e|t>>>e}function h(t,e){return t<<e|t>>>32-e>>>0}const a=(()=>68===new Uint8Array(new Uint32Array([287454020]).buffer)[0])();function l(t){for(let s=0;s<t.length;s++)t[s]=(e=t[s])<<24&4278190080|e<<8&16711680|e>>>8&65280|e>>>24&255;var e}function c(t){return"string"==typeof t&&(t=function(t){if("string"!=typeof t)throw new Error("utf8ToBytes expected string, got "+typeof t);return new Uint8Array((new TextEncoder).encode(t))}(t)),s(t),t}class u{clone(){return this._cloneInto()}}function d(t){const e=e=>t().update(c(e)).digest(),s=t();return e.outputLen=s.outputLen,e.blockLen=s.blockLen,e.create=()=>t(),e}class f extends u{update(t){return i(this),this.iHash.update(t),this}digestInto(t){i(this),s(t,this.outputLen),this.finished=!0,this.iHash.digestInto(t),this.oHash.update(t),this.oHash.digestInto(t),this.destroy()}digest(){const t=new Uint8Array(this.oHash.outputLen);return this.digestInto(t),t}_cloneInto(t){t||(t=Object.create(Object.getPrototypeOf(this),{}));const{oHash:e,iHash:s,finished:i,destroyed:r,blockLen:n,outputLen:o}=this
|
||||
;return t.finished=i,t.destroyed=r,t.blockLen=n,t.outputLen=o,t.oHash=e._cloneInto(t.oHash),t.iHash=s._cloneInto(t.iHash),t}destroy(){this.destroyed=!0,this.oHash.destroy(),this.iHash.destroy()}constructor(t,s){super(),this.finished=!1,this.destroyed=!1,function(t){if("function"!=typeof t||"function"!=typeof t.create)throw new Error("Hash should be wrapped by utils.wrapConstructor");e(t.outputLen),e(t.blockLen)}(t);const i=c(s);if(this.iHash=t.create(),"function"!=typeof this.iHash.update)throw new Error("Expected instance of class which extends utils.Hash");this.blockLen=this.iHash.blockLen,this.outputLen=this.iHash.outputLen;const r=this.blockLen,n=new Uint8Array(r);n.set(i.length>r?t.create().update(i).digest():i);for(let t=0;t<n.length;t++)n[t]^=54;this.iHash.update(n),this.oHash=t.create();for(let t=0;t<n.length;t++)n[t]^=106;this.oHash.update(n),n.fill(0)}}const b=(t,e,s)=>new f(t,e).update(s).digest();function g(t,e,s){return t&e^~t&s}function p(t,e,s){return t&e^t&s^e&s}b.create=(t,e)=>new f(t,e);class w extends u{update(t){i(this);const{view:e,buffer:s,blockLen:r}=this,o=(t=c(t)).length;for(let i=0;i<o;){const h=Math.min(r-this.pos,o-i);if(h!==r)s.set(t.subarray(i,i+h),this.pos),this.pos+=h,i+=h,this.pos===r&&(this.process(e,0),this.pos=0);else{const e=n(t);for(;r<=o-i;i+=r)this.process(e,i)}}return this.length+=t.length,this.roundClean(),this}digestInto(t){i(this),r(t,this),this.finished=!0;const{buffer:e,view:s,blockLen:o,isLE:h}=this;let{pos:a}=this;e[a++]=128,this.buffer.subarray(a).fill(0),this.padOffset>o-a&&(this.process(s,0),a=0);for(let t=a;t<o;t++)e[t]=0;!function(t,e,s,i){if("function"==typeof t.setBigUint64)return t.setBigUint64(e,s,i);const r=BigInt(32),n=BigInt(4294967295),o=Number(s>>r&n),h=Number(s&n),a=i?4:0,l=i?0:4;t.setUint32(e+a,o,i),t.setUint32(e+l,h,i)}(s,o-8,BigInt(8*this.length),h),this.process(s,0);const l=n(t),c=this.outputLen;if(c%4)throw new Error("_sha2: outputLen should be aligned to 32bit");const u=c/4,d=this.get()
|
||||
;if(u>d.length)throw new Error("_sha2: outputLen bigger than state");for(let t=0;t<u;t++)l.setUint32(4*t,d[t],h)}digest(){const{buffer:t,outputLen:e}=this;this.digestInto(t);const s=t.slice(0,e);return this.destroy(),s}_cloneInto(t){t||(t=new this.constructor),t.set(...this.get());const{blockLen:e,buffer:s,length:i,finished:r,destroyed:n,pos:o}=this;return t.length=i,t.pos=o,t.finished=r,t.destroyed=n,i%e&&t.buffer.set(s),t}constructor(t,e,s,i){super(),this.blockLen=t,this.outputLen=e,this.padOffset=s,this.isLE=i,this.finished=!1,this.length=0,this.pos=0,this.destroyed=!1,this.buffer=new Uint8Array(t),this.view=n(this.buffer)}}const y=new Uint32Array([1732584193,4023233417,2562383102,271733878,3285377520]),x=new Uint32Array(80);class A extends w{get(){const{A:t,B:e,C:s,D:i,E:r}=this;return[t,e,s,i,r]}set(t,e,s,i,r){this.A=0|t,this.B=0|e,this.C=0|s,this.D=0|i,this.E=0|r}process(t,e){for(let s=0;s<16;s++,e+=4)x[s]=t.getUint32(e,!1);for(let t=16;t<80;t++)x[t]=h(x[t-3]^x[t-8]^x[t-14]^x[t-16],1);let{A:s,B:i,C:r,D:n,E:o}=this;for(let t=0;t<80;t++){let e,a;t<20?(e=g(i,r,n),a=1518500249):t<40?(e=i^r^n,a=1859775393):t<60?(e=p(i,r,n),a=2400959708):(e=i^r^n,a=3395469782);const l=h(s,5)+e+o+a+x[t]|0;o=n,n=r,r=h(i,30),i=s,s=l}s=s+this.A|0,i=i+this.B|0,r=r+this.C|0,n=n+this.D|0,o=o+this.E|0,this.set(s,i,r,n,o)}roundClean(){x.fill(0)}destroy(){this.set(0,0,0,0,0),this.buffer.fill(0)}constructor(){super(64,20,8,!1),this.A=0|y[0],this.B=0|y[1],this.C=0|y[2],this.D=0|y[3],this.E=0|y[4]}}
|
||||
const m=d((()=>new A)),H=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),L=new Uint32Array([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),I=new Uint32Array(64);class S extends w{get(){const{A:t,B:e,C:s,D:i,E:r,F:n,G:o,H:h}=this;return[t,e,s,i,r,n,o,h]}set(t,e,s,i,r,n,o,h){this.A=0|t,this.B=0|e,this.C=0|s,this.D=0|i,this.E=0|r,this.F=0|n,this.G=0|o,this.H=0|h}process(t,e){for(let s=0;s<16;s++,e+=4)I[s]=t.getUint32(e,!1);for(let t=16;t<64;t++){const e=I[t-15],s=I[t-2],i=o(e,7)^o(e,18)^e>>>3,r=o(s,17)^o(s,19)^s>>>10;I[t]=r+I[t-7]+i+I[t-16]|0}let{A:s,B:i,C:r,D:n,E:h,F:a,G:l,H:c}=this;for(let t=0;t<64;t++){const e=c+(o(h,6)^o(h,11)^o(h,25))+g(h,a,l)+H[t]+I[t]|0,u=(o(s,2)^o(s,13)^o(s,22))+p(s,i,r)|0;c=l,l=a,a=h,h=n+e|0,n=r,r=i,i=s,s=e+u|0}s=s+this.A|0,i=i+this.B|0,r=r+this.C|0,n=n+this.D|0,h=h+this.E|0,a=a+this.F|0,l=l+this.G|0,c=c+this.H|0,this.set(s,i,r,n,h,a,l,c)}roundClean(){I.fill(0)}destroy(){this.set(0,0,0,0,0,0,0,0),this.buffer.fill(0)}constructor(){super(64,32,8,!1),this.A=0|L[0],this.B=0|L[1],this.C=0|L[2],this.D=0|L[3],this.E=0|L[4],this.F=0|L[5],this.G=0|L[6],this.H=0|L[7]}}class B extends S{constructor(){super(),this.A=-1056596264,this.B=914150663,this.C=812702999,this.D=-150054599,this.E=-4191439,this.F=1750603025,this.G=1694076839,this.H=-1090891868,this.outputLen=28}}
|
||||
const E=d((()=>new S)),U=d((()=>new B)),C=BigInt(2**32-1),O=BigInt(32);function v(t,e=!1){return e?{h:Number(t&C),l:Number(t>>O&C)}:{h:0|Number(t>>O&C),l:0|Number(t&C)}}function k(t,e=!1){let s=new Uint32Array(t.length),i=new Uint32Array(t.length);for(let r=0;r<t.length;r++){const{h:n,l:o}=v(t[r],e);[s[r],i[r]]=[n,o]}return[s,i]}const T=(t,e,s)=>t<<s|e>>>32-s,$=(t,e,s)=>e<<s|t>>>32-s,D=(t,e,s)=>e<<s-32|t>>>64-s,_=(t,e,s)=>t<<s-32|e>>>64-s,F={fromBig:v,split:k,toBig:(t,e)=>BigInt(t>>>0)<<O|BigInt(e>>>0),shrSH:(t,e,s)=>t>>>s,shrSL:(t,e,s)=>t<<32-s|e>>>s,rotrSH:(t,e,s)=>t>>>s|e<<32-s,rotrSL:(t,e,s)=>t<<32-s|e>>>s,rotrBH:(t,e,s)=>t<<64-s|e>>>s-32,rotrBL:(t,e,s)=>t>>>s-32|e<<64-s,rotr32H:(t,e)=>e,rotr32L:(t,e)=>t,rotlSH:T,rotlSL:$,rotlBH:D,rotlBL:_,add:function(t,e,s,i){const r=(e>>>0)+(i>>>0);return{h:t+s+(r/2**32|0)|0,l:0|r}},add3L:(t,e,s)=>(t>>>0)+(e>>>0)+(s>>>0),add3H:(t,e,s,i)=>e+s+i+(t/2**32|0)|0,add4L:(t,e,s,i)=>(t>>>0)+(e>>>0)+(s>>>0)+(i>>>0),add4H:(t,e,s,i,r)=>e+s+i+r+(t/2**32|0)|0,add5H:(t,e,s,i,r,n)=>e+s+i+r+n+(t/2**32|0)|0,add5L:(t,e,s,i,r)=>(t>>>0)+(e>>>0)+(s>>>0)+(i>>>0)+(r>>>0)
|
||||
},[G,P]=(()=>F.split(["0x428a2f98d728ae22","0x7137449123ef65cd","0xb5c0fbcfec4d3b2f","0xe9b5dba58189dbbc","0x3956c25bf348b538","0x59f111f1b605d019","0x923f82a4af194f9b","0xab1c5ed5da6d8118","0xd807aa98a3030242","0x12835b0145706fbe","0x243185be4ee4b28c","0x550c7dc3d5ffb4e2","0x72be5d74f27b896f","0x80deb1fe3b1696b1","0x9bdc06a725c71235","0xc19bf174cf692694","0xe49b69c19ef14ad2","0xefbe4786384f25e3","0x0fc19dc68b8cd5b5","0x240ca1cc77ac9c65","0x2de92c6f592b0275","0x4a7484aa6ea6e483","0x5cb0a9dcbd41fbd4","0x76f988da831153b5","0x983e5152ee66dfab","0xa831c66d2db43210","0xb00327c898fb213f","0xbf597fc7beef0ee4","0xc6e00bf33da88fc2","0xd5a79147930aa725","0x06ca6351e003826f","0x142929670a0e6e70","0x27b70a8546d22ffc","0x2e1b21385c26c926","0x4d2c6dfc5ac42aed","0x53380d139d95b3df","0x650a73548baf63de","0x766a0abb3c77b2a8","0x81c2c92e47edaee6","0x92722c851482353b","0xa2bfe8a14cf10364","0xa81a664bbc423001","0xc24b8b70d0f89791","0xc76c51a30654be30","0xd192e819d6ef5218","0xd69906245565a910","0xf40e35855771202a","0x106aa07032bbd1b8","0x19a4c116b8d2d0c8","0x1e376c085141ab53","0x2748774cdf8eeb99","0x34b0bcb5e19b48a8","0x391c0cb3c5c95a63","0x4ed8aa4ae3418acb","0x5b9cca4f7763e373","0x682e6ff3d6b2b8a3","0x748f82ee5defb2fc","0x78a5636f43172f60","0x84c87814a1f0ab72","0x8cc702081a6439ec","0x90befffa23631e28","0xa4506cebde82bde9","0xbef9a3f7b2c67915","0xc67178f2e372532b","0xca273eceea26619c","0xd186b8c721c0c207","0xeada7dd6cde0eb1e","0xf57d4f7fee6ed178","0x06f067aa72176fba","0x0a637dc5a2c898a6","0x113f9804bef90dae","0x1b710b35131c471b","0x28db77f523047d84","0x32caab7b40c72493","0x3c9ebe0a15c9bebc","0x431d67c49c100d4c","0x4cc5d4becb3e42b6","0x597f299cfc657e2a","0x5fcb6fab3ad6faec","0x6c44198c4a475817"].map((t=>BigInt(t)))))(),j=new Uint32Array(80),M=new Uint32Array(80);class R extends w{get(){const{Ah:t,Al:e,Bh:s,Bl:i,Ch:r,Cl:n,Dh:o,Dl:h,Eh:a,El:l,Fh:c,Fl:u,Gh:d,Gl:f,Hh:b,Hl:g}=this;return[t,e,s,i,r,n,o,h,a,l,c,u,d,f,b,g]}set(t,e,s,i,r,n,o,h,a,l,c,u,d,f,b,g){this.Ah=0|t,this.Al=0|e,this.Bh=0|s,this.Bl=0|i,this.Ch=0|r,this.Cl=0|n,this.Dh=0|o,
|
||||
this.Dl=0|h,this.Eh=0|a,this.El=0|l,this.Fh=0|c,this.Fl=0|u,this.Gh=0|d,this.Gl=0|f,this.Hh=0|b,this.Hl=0|g}process(t,e){for(let s=0;s<16;s++,e+=4)j[s]=t.getUint32(e),M[s]=t.getUint32(e+=4);for(let t=16;t<80;t++){const e=0|j[t-15],s=0|M[t-15],i=F.rotrSH(e,s,1)^F.rotrSH(e,s,8)^F.shrSH(e,s,7),r=F.rotrSL(e,s,1)^F.rotrSL(e,s,8)^F.shrSL(e,s,7),n=0|j[t-2],o=0|M[t-2],h=F.rotrSH(n,o,19)^F.rotrBH(n,o,61)^F.shrSH(n,o,6),a=F.rotrSL(n,o,19)^F.rotrBL(n,o,61)^F.shrSL(n,o,6),l=F.add4L(r,a,M[t-7],M[t-16]),c=F.add4H(l,i,h,j[t-7],j[t-16]);j[t]=0|c,M[t]=0|l}let{Ah:s,Al:i,Bh:r,Bl:n,Ch:o,Cl:h,Dh:a,Dl:l,Eh:c,El:u,Fh:d,Fl:f,Gh:b,Gl:g,Hh:p,Hl:w}=this;for(let t=0;t<80;t++){const e=F.rotrSH(c,u,14)^F.rotrSH(c,u,18)^F.rotrBH(c,u,41),y=F.rotrSL(c,u,14)^F.rotrSL(c,u,18)^F.rotrBL(c,u,41),x=c&d^~c&b,A=u&f^~u&g,m=F.add5L(w,y,A,P[t],M[t]),H=F.add5H(m,p,e,x,G[t],j[t]),L=0|m,I=F.rotrSH(s,i,28)^F.rotrBH(s,i,34)^F.rotrBH(s,i,39),S=F.rotrSL(s,i,28)^F.rotrBL(s,i,34)^F.rotrBL(s,i,39),B=s&r^s&o^r&o,E=i&n^i&h^n&h;p=0|b,w=0|g,b=0|d,g=0|f,d=0|c,f=0|u,({h:c,l:u}=F.add(0|a,0|l,0|H,0|L)),a=0|o,l=0|h,o=0|r,h=0|n,r=0|s,n=0|i;const U=F.add3L(L,S,E);s=F.add3H(U,H,I,B),i=0|U}({h:s,l:i}=F.add(0|this.Ah,0|this.Al,0|s,0|i)),({h:r,l:n}=F.add(0|this.Bh,0|this.Bl,0|r,0|n)),({h:o,l:h}=F.add(0|this.Ch,0|this.Cl,0|o,0|h)),({h:a,l}=F.add(0|this.Dh,0|this.Dl,0|a,0|l)),({h:c,l:u}=F.add(0|this.Eh,0|this.El,0|c,0|u)),({h:d,l:f}=F.add(0|this.Fh,0|this.Fl,0|d,0|f)),({h:b,l:g}=F.add(0|this.Gh,0|this.Gl,0|b,0|g)),({h:p,l:w}=F.add(0|this.Hh,0|this.Hl,0|p,0|w)),this.set(s,i,r,n,o,h,a,l,c,u,d,f,b,g,p,w)}roundClean(){j.fill(0),M.fill(0)}destroy(){this.buffer.fill(0),this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)}constructor(){super(128,64,16,!1),this.Ah=1779033703,this.Al=-205731576,this.Bh=-1150833019,this.Bl=-2067093701,this.Ch=1013904242,this.Cl=-23791573,this.Dh=-1521486534,this.Dl=1595750129,this.Eh=1359893119,this.El=-1377402159,this.Fh=-1694144372,this.Fl=725511199,this.Gh=528734635,this.Gl=-79577749,this.Hh=1541459225,this.Hl=327033209}}class N extends R{constructor(){super(),
|
||||
this.Ah=-876896931,this.Al=-1056596264,this.Bh=1654270250,this.Bl=914150663,this.Ch=-1856437926,this.Cl=812702999,this.Dh=355462360,this.Dl=-150054599,this.Eh=1731405415,this.El=-4191439,this.Fh=-1900787065,this.Fl=1750603025,this.Gh=-619958771,this.Gl=1694076839,this.Hh=1203062813,this.Hl=-1090891868,this.outputLen=48}}const X=d((()=>new R)),V=d((()=>new N)),Z=[],z=[],J=[],K=BigInt(0),Q=BigInt(1),W=BigInt(2),Y=BigInt(7),q=BigInt(256),tt=BigInt(113);for(let t=0,e=Q,s=1,i=0;t<24;t++){[s,i]=[i,(2*s+3*i)%5],Z.push(2*(5*i+s)),z.push((t+1)*(t+2)/2%64);let r=K;for(let t=0;t<7;t++)e=(e<<Q^(e>>Y)*tt)%q,e&W&&(r^=Q<<(Q<<BigInt(t))-Q);J.push(r)}const[et,st]=k(J,!0),it=(t,e,s)=>s>32?D(t,e,s):T(t,e,s),rt=(t,e,s)=>s>32?_(t,e,s):$(t,e,s);class nt extends u{keccak(){a||l(this.state32),function(t,e=24){const s=new Uint32Array(10);for(let i=24-e;i<24;i++){for(let e=0;e<10;e++)s[e]=t[e]^t[e+10]^t[e+20]^t[e+30]^t[e+40];for(let e=0;e<10;e+=2){const i=(e+8)%10,r=(e+2)%10,n=s[r],o=s[r+1],h=it(n,o,1)^s[i],a=rt(n,o,1)^s[i+1];for(let s=0;s<50;s+=10)t[e+s]^=h,t[e+s+1]^=a}let e=t[2],r=t[3];for(let s=0;s<24;s++){const i=z[s],n=it(e,r,i),o=rt(e,r,i),h=Z[s];e=t[h],r=t[h+1],t[h]=n,t[h+1]=o}for(let e=0;e<50;e+=10){for(let i=0;i<10;i++)s[i]=t[e+i];for(let i=0;i<10;i++)t[e+i]^=~s[(i+2)%10]&s[(i+4)%10]}t[0]^=et[i],t[1]^=st[i]}s.fill(0)}(this.state32,this.rounds),a||l(this.state32),this.posOut=0,this.pos=0}update(t){i(this);const{blockLen:e,state:s}=this,r=(t=c(t)).length;for(let i=0;i<r;){const n=Math.min(e-this.pos,r-i);for(let e=0;e<n;e++)s[this.pos++]^=t[i++];this.pos===e&&this.keccak()}return this}finish(){if(this.finished)return;this.finished=!0;const{state:t,suffix:e,pos:s,blockLen:i}=this;t[s]^=e,128&e&&s===i-1&&this.keccak(),t[i-1]^=128,this.keccak()}writeInto(t){i(this,!1),s(t),this.finish();const e=this.state,{blockLen:r}=this;for(let s=0,i=t.length;s<i;){this.posOut>=r&&this.keccak();const n=Math.min(r-this.posOut,i-s);t.set(e.subarray(this.posOut,this.posOut+n),s),this.posOut+=n,s+=n}return t}xofInto(t){
|
||||
if(!this.enableXOF)throw new Error("XOF is not possible for this instance");return this.writeInto(t)}xof(t){return e(t),this.xofInto(new Uint8Array(t))}digestInto(t){if(r(t,this),this.finished)throw new Error("digest() was already called");return this.writeInto(t),this.destroy(),t}digest(){return this.digestInto(new Uint8Array(this.outputLen))}destroy(){this.destroyed=!0,this.state.fill(0)}_cloneInto(t){const{blockLen:e,suffix:s,outputLen:i,rounds:r,enableXOF:n}=this;return t||(t=new nt(e,s,i,n,r)),t.state32.set(this.state32),t.pos=this.pos,t.posOut=this.posOut,t.finished=this.finished,t.rounds=r,t.suffix=s,t.outputLen=i,t.enableXOF=n,t.destroyed=this.destroyed,t}constructor(t,s,i,r=!1,n=24){if(super(),this.blockLen=t,this.suffix=s,this.outputLen=i,this.enableXOF=r,this.rounds=n,this.pos=0,this.posOut=0,this.finished=!1,this.destroyed=!1,e(i),0>=this.blockLen||this.blockLen>=200)throw new Error("Sha3 supports only keccak-f1600 function");var o;this.state=new Uint8Array(200),this.state32=(o=this.state,new Uint32Array(o.buffer,o.byteOffset,Math.floor(o.byteLength/4)))}}const ot=(t,e,s)=>d((()=>new nt(e,t,s))),ht=ot(6,144,28),at=ot(6,136,32),lt=ot(6,104,48),ct=ot(6,72,64),ut=(()=>{if("object"==typeof globalThis)return globalThis;Object.defineProperty(Object.prototype,"__GLOBALTHIS__",{get(){return this},configurable:!0});try{if("undefined"!=typeof __GLOBALTHIS__)return __GLOBALTHIS__}finally{delete Object.prototype.__GLOBALTHIS__}return"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:void 0})(),dt={SHA1:m,SHA224:U,SHA256:E,SHA384:V,SHA512:X,"SHA3-224":ht,"SHA3-256":at,"SHA3-384":lt,"SHA3-512":ct},ft=t=>{switch(!0){case/^(?:SHA-?1|SSL3-SHA1)$/i.test(t):return"SHA1";case/^SHA(?:2?-)?224$/i.test(t):return"SHA224";case/^SHA(?:2?-)?256$/i.test(t):return"SHA256";case/^SHA(?:2?-)?384$/i.test(t):return"SHA384";case/^SHA(?:2?-)?512$/i.test(t):return"SHA512";case/^SHA3-224$/i.test(t):return"SHA3-224";case/^SHA3-256$/i.test(t):return"SHA3-256";case/^SHA3-384$/i.test(t):
|
||||
return"SHA3-384";case/^SHA3-512$/i.test(t):return"SHA3-512";default:throw new TypeError(`Unknown hash algorithm: ${t}`)}},bt="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",gt=t=>{let e=(t=t.replace(/ /g,"")).length;for(;"="===t[e-1];)--e;t=(e<t.length?t.substring(0,e):t).toUpperCase();const s=new ArrayBuffer(5*t.length/8|0),i=new Uint8Array(s);let r=0,n=0,o=0;for(let e=0;e<t.length;e++){const s=bt.indexOf(t[e]);if(-1===s)throw new TypeError(`Invalid character found: ${t[e]}`);n=n<<5|s,r+=5,r>=8&&(r-=8,i[o++]=n>>>r)}return i},pt=t=>{let e=0,s=0,i="";for(let r=0;r<t.length;r++)for(s=s<<8|t[r],e+=8;e>=5;)i+=bt[s>>>e-5&31],e-=5;return e>0&&(i+=bt[s<<5-e&31]),i},wt=t=>{t=t.replace(/ /g,"");const e=new ArrayBuffer(t.length/2),s=new Uint8Array(e);for(let e=0;e<t.length;e+=2)s[e/2]=parseInt(t.substring(e,e+2),16);return s},yt=t=>{let e="";for(let s=0;s<t.length;s++){const i=t[s].toString(16);1===i.length&&(e+="0"),e+=i}return e.toUpperCase()},xt=t=>{const e=new ArrayBuffer(t.length),s=new Uint8Array(e);for(let e=0;e<t.length;e++)s[e]=255&t.charCodeAt(e);return s},At=t=>{let e="";for(let s=0;s<t.length;s++)e+=String.fromCharCode(t[s]);return e},mt=ut.TextEncoder?new ut.TextEncoder:null,Ht=ut.TextDecoder?new ut.TextDecoder:null,Lt=t=>{if(!mt)throw new Error("Encoding API not available");return mt.encode(t)},It=t=>{if(!Ht)throw new Error("Encoding API not available");return Ht.decode(t)};class St{static fromLatin1(t){return new St({buffer:xt(t).buffer})}static fromUTF8(t){return new St({buffer:Lt(t).buffer})}static fromBase32(t){return new St({buffer:gt(t).buffer})}static fromHex(t){return new St({buffer:wt(t).buffer})}get buffer(){return this.bytes.buffer}get latin1(){return Object.defineProperty(this,"latin1",{enumerable:!0,writable:!1,configurable:!1,value:At(this.bytes)}),this.latin1}get utf8(){return Object.defineProperty(this,"utf8",{enumerable:!0,writable:!1,configurable:!1,value:It(this.bytes)}),this.utf8}get base32(){return Object.defineProperty(this,"base32",{enumerable:!0,writable:!1,configurable:!1,value:pt(this.bytes)}),
|
||||
this.base32}get hex(){return Object.defineProperty(this,"hex",{enumerable:!0,writable:!1,configurable:!1,value:yt(this.bytes)}),this.hex}constructor({buffer:t,size:e=20}={}){this.bytes=void 0===t?(t=>{if(ut.crypto?.getRandomValues)return ut.crypto.getRandomValues(new Uint8Array(t));throw new Error("Cryptography API not available")})(e):new Uint8Array(t),Object.defineProperty(this,"bytes",{enumerable:!0,writable:!1,configurable:!1,value:this.bytes})}}class Bt{static get defaults(){return{issuer:"",label:"OTPAuth",issuerInLabel:!0,algorithm:"SHA1",digits:6,counter:0,window:1}}static generate({secret:t,algorithm:e=Bt.defaults.algorithm,digits:s=Bt.defaults.digits,counter:i=Bt.defaults.counter}){const r=((t,e,s)=>{if(b){const i=dt[t]??dt[ft(t)];return b(i,e,s)}throw new Error("Missing HMAC function")})(e,t.bytes,(t=>{const e=new ArrayBuffer(8),s=new Uint8Array(e);let i=t;for(let t=7;t>=0&&0!==i;t--)s[t]=255&i,i-=s[t],i/=256;return s})(i)),n=15&r[r.byteLength-1];return(((127&r[n])<<24|(255&r[n+1])<<16|(255&r[n+2])<<8|255&r[n+3])%10**s).toString().padStart(s,"0")}generate({counter:t=this.counter++}={}){return Bt.generate({secret:this.secret,algorithm:this.algorithm,digits:this.digits,counter:t})}static validate({token:t,secret:e,algorithm:s,digits:i=Bt.defaults.digits,counter:r=Bt.defaults.counter,window:n=Bt.defaults.window}){if(t.length!==i)return null;let o=null;const h=n=>{const h=Bt.generate({secret:e,algorithm:s,digits:i,counter:n});((t,e)=>{{if(t.length!==e.length)throw new TypeError("Input strings must have the same length");let s=-1,i=0;for(;++s<t.length;)i|=t.charCodeAt(s)^e.charCodeAt(s);return 0===i}})(t,h)&&(o=n-r)};h(r);for(let t=1;t<=n&&null===o&&(h(r-t),null===o)&&(h(r+t),null===o);++t);return o}validate({token:t,counter:e=this.counter,window:s}){return Bt.validate({token:t,secret:this.secret,algorithm:this.algorithm,digits:this.digits,counter:e,window:s})}toString(){const t=encodeURIComponent
|
||||
;return"otpauth://hotp/"+(this.issuer.length>0?this.issuerInLabel?`${t(this.issuer)}:${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?`)+`secret=${t(this.secret.base32)}&`+`algorithm=${t(this.algorithm)}&`+`digits=${t(this.digits)}&`+`counter=${t(this.counter)}`}constructor({issuer:t=Bt.defaults.issuer,label:e=Bt.defaults.label,issuerInLabel:s=Bt.defaults.issuerInLabel,secret:i=new St,algorithm:r=Bt.defaults.algorithm,digits:n=Bt.defaults.digits,counter:o=Bt.defaults.counter}={}){this.issuer=t,this.label=e,this.issuerInLabel=s,this.secret="string"==typeof i?St.fromBase32(i):i,this.algorithm=ft(r),this.digits=n,this.counter=o}}class Et{static get defaults(){return{issuer:"",label:"OTPAuth",issuerInLabel:!0,algorithm:"SHA1",digits:6,period:30,window:1}}static counter({period:t=Et.defaults.period,timestamp:e=Date.now()}={}){return Math.floor(e/1e3/t)}counter({timestamp:t=Date.now()}={}){return Et.counter({period:this.period,timestamp:t})}static remaining({period:t=Et.defaults.period,timestamp:e=Date.now()}={}){return 1e3*t-e%(1e3*t)}remaining({timestamp:t=Date.now()}={}){return Et.remaining({period:this.period,timestamp:t})}static generate({secret:t,algorithm:e,digits:s,period:i=Et.defaults.period,timestamp:r=Date.now()}){return Bt.generate({secret:t,algorithm:e,digits:s,counter:Et.counter({period:i,timestamp:r})})}generate({timestamp:t=Date.now()}={}){return Et.generate({secret:this.secret,algorithm:this.algorithm,digits:this.digits,period:this.period,timestamp:t})}static validate({token:t,secret:e,algorithm:s,digits:i,period:r=Et.defaults.period,timestamp:n=Date.now(),window:o}){return Bt.validate({token:t,secret:e,algorithm:s,digits:i,counter:Et.counter({period:r,timestamp:n}),window:o})}validate({token:t,timestamp:e,window:s}){return Et.validate({token:t,secret:this.secret,algorithm:this.algorithm,digits:this.digits,period:this.period,timestamp:e,window:s})}toString(){const t=encodeURIComponent
|
||||
;return"otpauth://totp/"+(this.issuer.length>0?this.issuerInLabel?`${t(this.issuer)}:${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?`)+`secret=${t(this.secret.base32)}&`+`algorithm=${t(this.algorithm)}&`+`digits=${t(this.digits)}&`+`period=${t(this.period)}`}constructor({issuer:t=Et.defaults.issuer,label:e=Et.defaults.label,issuerInLabel:s=Et.defaults.issuerInLabel,secret:i=new St,algorithm:r=Et.defaults.algorithm,digits:n=Et.defaults.digits,period:o=Et.defaults.period}={}){this.issuer=t,this.label=e,this.issuerInLabel=s,this.secret="string"==typeof i?St.fromBase32(i):i,this.algorithm=ft(r),this.digits=n,this.period=o}}const Ut=/^otpauth:\/\/([ht]otp)\/(.+)\?([A-Z0-9.~_-]+=[^?&]*(?:&[A-Z0-9.~_-]+=[^?&]*)*)$/i,Ct=/^[2-7A-Z]+=*$/i,Ot=/^SHA(?:1|224|256|384|512|3-224|3-256|3-384|3-512)$/i,vt=/^[+-]?\d+$/,kt=/^\+?[1-9]\d*$/;t.HOTP=Bt,t.Secret=St,t.TOTP=Et,t.URI=class{static parse(t){let e;try{e=t.match(Ut)}catch(t){}if(!Array.isArray(e))throw new URIError("Invalid URI format");const s=e[1].toLowerCase(),i=e[2].split(/(?::|%3A) *(.+)/i,2).map(decodeURIComponent),r=e[3].split("&").reduce(((t,e)=>{const s=e.split(/=(.*)/,2).map(decodeURIComponent),i=s[0].toLowerCase(),r=s[1],n=t;return n[i]=r,n}),{});let n;const o={};if("hotp"===s){if(n=Bt,void 0===r.counter||!vt.test(r.counter))throw new TypeError("Missing or invalid 'counter' parameter");o.counter=parseInt(r.counter,10)}else{if("totp"!==s)throw new TypeError("Unknown OTP type");if(n=Et,void 0!==r.period){if(!kt.test(r.period))throw new TypeError("Invalid 'period' parameter");o.period=parseInt(r.period,10)}}if(void 0!==r.issuer&&(o.issuer=r.issuer),2===i.length?(o.label=i[1],void 0===o.issuer||""===o.issuer?o.issuer=i[0]:""===i[0]&&(o.issuerInLabel=!1)):(o.label=i[0],void 0!==o.issuer&&""!==o.issuer&&(o.issuerInLabel=!1)),void 0===r.secret||!Ct.test(r.secret))throw new TypeError("Missing or invalid 'secret' parameter");if(o.secret=r.secret,void 0!==r.algorithm){
|
||||
if(!Ot.test(r.algorithm))throw new TypeError("Invalid 'algorithm' parameter");o.algorithm=r.algorithm}if(void 0!==r.digits){if(!kt.test(r.digits))throw new TypeError("Invalid 'digits' parameter");o.digits=parseInt(r.digits,10)}return new n(o)}static stringify(t){if(t instanceof Bt||t instanceof Et)return t.toString();throw new TypeError("Invalid 'HOTP/TOTP' object")}},t.version="9.4.0"}));
|
||||
//# sourceMappingURL=otpauth.umd.min.js.map
|
||||
1
web/assets/persian-datepicker/persian-datepicker.min.css
vendored
Normal file
1
web/assets/persian-datepicker/persian-datepicker.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/assets/persian-datepicker/persian-datepicker.min.js
vendored
Normal file
1
web/assets/persian-datepicker/persian-datepicker.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
web/assets/qrcode/qrious2.min.js
vendored
Normal file
5
web/assets/qrcode/qrious2.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/assets/qs/qs.min.js
vendored
Normal file
1
web/assets/qs/qs.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
95
web/assets/uri/URI.min.js
vendored
Normal file
95
web/assets/uri/URI.min.js
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
/*! URI.js v1.19.11 http://medialize.github.io/URI.js/ */
|
||||
/* build contains: IPv6.js, punycode.js, SecondLevelDomains.js, URI.js, URITemplate.js */
|
||||
'use strict';(function(r,x){"object"===typeof module&&module.exports?module.exports=x():"function"===typeof define&&define.amd?define(x):r.IPv6=x(r)})(this,function(r){var x=r&&r.IPv6;return{best:function(k){k=k.toLowerCase().split(":");var m=k.length,d=8;""===k[0]&&""===k[1]&&""===k[2]?(k.shift(),k.shift()):""===k[0]&&""===k[1]?k.shift():""===k[m-1]&&""===k[m-2]&&k.pop();m=k.length;-1!==k[m-1].indexOf(".")&&(d=7);var q;for(q=0;q<m&&""!==k[q];q++);if(q<d)for(k.splice(q,1,"0000");k.length<d;)k.splice(q,
|
||||
0,"0000");for(q=0;q<d;q++){m=k[q].split("");for(var B=0;3>B;B++)if("0"===m[0]&&1<m.length)m.splice(0,1);else break;k[q]=m.join("")}m=-1;var z=B=0,h=-1,p=!1;for(q=0;q<d;q++)p?"0"===k[q]?z+=1:(p=!1,z>B&&(m=h,B=z)):"0"===k[q]&&(p=!0,h=q,z=1);z>B&&(m=h,B=z);1<B&&k.splice(m,B,"");m=k.length;d="";""===k[0]&&(d=":");for(q=0;q<m;q++){d+=k[q];if(q===m-1)break;d+=":"}""===k[m-1]&&(d+=":");return d},noConflict:function(){r.IPv6===this&&(r.IPv6=x);return this}}});
|
||||
(function(r){function x(l){throw new RangeError(G[l]);}function k(l,t){for(var H=l.length,y=[];H--;)y[H]=t(l[H]);return y}function m(l,t){var H=l.split("@"),y="";1<H.length&&(y=H[0]+"@",l=H[1]);l=l.replace(w,".");l=l.split(".");t=k(l,t).join(".");return y+t}function d(l){for(var t=[],H=0,y=l.length,I,M;H<y;)I=l.charCodeAt(H++),55296<=I&&56319>=I&&H<y?(M=l.charCodeAt(H++),56320==(M&64512)?t.push(((I&1023)<<10)+(M&1023)+65536):(t.push(I),H--)):t.push(I);return t}function q(l){return k(l,function(t){var H=
|
||||
"";65535<t&&(t-=65536,H+=g(t>>>10&1023|55296),t=56320|t&1023);return H+=g(t)}).join("")}function B(l,t,H){var y=0;l=H?v(l/700):l>>1;for(l+=v(l/t);455<l;y+=36)l=v(l/35);return v(y+36*l/(l+38))}function z(l){var t=[],H=l.length,y=0,I=128,M=72,a,b;var c=l.lastIndexOf("-");0>c&&(c=0);for(a=0;a<c;++a)128<=l.charCodeAt(a)&&x("not-basic"),t.push(l.charCodeAt(a));for(c=0<c?c+1:0;c<H;){a=y;var e=1;for(b=36;;b+=36){c>=H&&x("invalid-input");var f=l.charCodeAt(c++);f=10>f-48?f-22:26>f-65?f-65:26>f-97?f-97:36;
|
||||
(36<=f||f>v((2147483647-y)/e))&&x("overflow");y+=f*e;var n=b<=M?1:b>=M+26?26:b-M;if(f<n)break;f=36-n;e>v(2147483647/f)&&x("overflow");e*=f}e=t.length+1;M=B(y-a,e,0==a);v(y/e)>2147483647-I&&x("overflow");I+=v(y/e);y%=e;t.splice(y++,0,I)}return q(t)}function h(l){var t,H,y,I=[];l=d(l);var M=l.length;var a=128;var b=0;var c=72;for(y=0;y<M;++y){var e=l[y];128>e&&I.push(g(e))}for((t=H=I.length)&&I.push("-");t<M;){var f=2147483647;for(y=0;y<M;++y)e=l[y],e>=a&&e<f&&(f=e);var n=t+1;f-a>v((2147483647-b)/n)&&
|
||||
x("overflow");b+=(f-a)*n;a=f;for(y=0;y<M;++y)if(e=l[y],e<a&&2147483647<++b&&x("overflow"),e==a){var E=b;for(f=36;;f+=36){e=f<=c?1:f>=c+26?26:f-c;if(E<e)break;var K=E-e;E=36-e;var L=I;e+=K%E;L.push.call(L,g(e+22+75*(26>e)-0));E=v(K/E)}I.push(g(E+22+75*(26>E)-0));c=B(b,n,t==H);b=0;++t}++b;++a}return I.join("")}var p="object"==typeof exports&&exports&&!exports.nodeType&&exports,F="object"==typeof module&&module&&!module.nodeType&&module,u="object"==typeof global&&global;if(u.global===u||u.window===u||
|
||||
u.self===u)r=u;var J=/^xn--/,C=/[^\x20-\x7E]/,w=/[\x2E\u3002\uFF0E\uFF61]/g,G={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},v=Math.floor,g=String.fromCharCode,A;var D={version:"1.3.2",ucs2:{decode:d,encode:q},decode:z,encode:h,toASCII:function(l){return m(l,function(t){return C.test(t)?"xn--"+h(t):t})},toUnicode:function(l){return m(l,function(t){return J.test(t)?z(t.slice(4).toLowerCase()):
|
||||
t})}};if("function"==typeof define&&"object"==typeof define.amd&&define.amd)define("punycode",function(){return D});else if(p&&F)if(module.exports==p)F.exports=D;else for(A in D)D.hasOwnProperty(A)&&(p[A]=D[A]);else r.punycode=D})(this);
|
||||
(function(r,x){"object"===typeof module&&module.exports?module.exports=x():"function"===typeof define&&define.amd?define(x):r.SecondLevelDomains=x(r)})(this,function(r){var x=r&&r.SecondLevelDomains,k={list:{ac:" com gov mil net org ",ae:" ac co gov mil name net org pro sch ",af:" com edu gov net org ",al:" com edu gov mil net org ",ao:" co ed gv it og pb ",ar:" com edu gob gov int mil net org tur ",at:" ac co gv or ",au:" asn com csiro edu gov id net org ",ba:" co com edu gov mil net org rs unbi unmo unsa untz unze ",
|
||||
bb:" biz co com edu gov info net org store tv ",bh:" biz cc com edu gov info net org ",bn:" com edu gov net org ",bo:" com edu gob gov int mil net org tv ",br:" adm adv agr am arq art ato b bio blog bmd cim cng cnt com coop ecn edu eng esp etc eti far flog fm fnd fot fst g12 ggf gov imb ind inf jor jus lel mat med mil mus net nom not ntr odo org ppg pro psc psi qsl rec slg srv tmp trd tur tv vet vlog wiki zlg ",bs:" com edu gov net org ",bz:" du et om ov rg ",ca:" ab bc mb nb nf nl ns nt nu on pe qc sk yk ",
|
||||
ck:" biz co edu gen gov info net org ",cn:" ac ah bj com cq edu fj gd gov gs gx gz ha hb he hi hl hn jl js jx ln mil net nm nx org qh sc sd sh sn sx tj tw xj xz yn zj ",co:" com edu gov mil net nom org ",cr:" ac c co ed fi go or sa ",cy:" ac biz com ekloges gov ltd name net org parliament press pro tm ","do":" art com edu gob gov mil net org sld web ",dz:" art asso com edu gov net org pol ",ec:" com edu fin gov info med mil net org pro ",eg:" com edu eun gov mil name net org sci ",er:" com edu gov ind mil net org rochest w ",
|
||||
es:" com edu gob nom org ",et:" biz com edu gov info name net org ",fj:" ac biz com info mil name net org pro ",fk:" ac co gov net nom org ",fr:" asso com f gouv nom prd presse tm ",gg:" co net org ",gh:" com edu gov mil org ",gn:" ac com gov net org ",gr:" com edu gov mil net org ",gt:" com edu gob ind mil net org ",gu:" com edu gov net org ",hk:" com edu gov idv net org ",hu:" 2000 agrar bolt casino city co erotica erotika film forum games hotel info ingatlan jogasz konyvelo lakas media news org priv reklam sex shop sport suli szex tm tozsde utazas video ",
|
||||
id:" ac co go mil net or sch web ",il:" ac co gov idf k12 muni net org ","in":" ac co edu ernet firm gen gov i ind mil net nic org res ",iq:" com edu gov i mil net org ",ir:" ac co dnssec gov i id net org sch ",it:" edu gov ",je:" co net org ",jo:" com edu gov mil name net org sch ",jp:" ac ad co ed go gr lg ne or ",ke:" ac co go info me mobi ne or sc ",kh:" com edu gov mil net org per ",ki:" biz com de edu gov info mob net org tel ",km:" asso com coop edu gouv k medecin mil nom notaires pharmaciens presse tm veterinaire ",
|
||||
kn:" edu gov net org ",kr:" ac busan chungbuk chungnam co daegu daejeon es gangwon go gwangju gyeongbuk gyeonggi gyeongnam hs incheon jeju jeonbuk jeonnam k kg mil ms ne or pe re sc seoul ulsan ",kw:" com edu gov net org ",ky:" com edu gov net org ",kz:" com edu gov mil net org ",lb:" com edu gov net org ",lk:" assn com edu gov grp hotel int ltd net ngo org sch soc web ",lr:" com edu gov net org ",lv:" asn com conf edu gov id mil net org ",ly:" com edu gov id med net org plc sch ",ma:" ac co gov m net org press ",
|
||||
mc:" asso tm ",me:" ac co edu gov its net org priv ",mg:" com edu gov mil nom org prd tm ",mk:" com edu gov inf name net org pro ",ml:" com edu gov net org presse ",mn:" edu gov org ",mo:" com edu gov net org ",mt:" com edu gov net org ",mv:" aero biz com coop edu gov info int mil museum name net org pro ",mw:" ac co com coop edu gov int museum net org ",mx:" com edu gob net org ",my:" com edu gov mil name net org sch ",nf:" arts com firm info net other per rec store web ",ng:" biz com edu gov mil mobi name net org sch ",
|
||||
ni:" ac co com edu gob mil net nom org ",np:" com edu gov mil net org ",nr:" biz com edu gov info net org ",om:" ac biz co com edu gov med mil museum net org pro sch ",pe:" com edu gob mil net nom org sld ",ph:" com edu gov i mil net ngo org ",pk:" biz com edu fam gob gok gon gop gos gov net org web ",pl:" art bialystok biz com edu gda gdansk gorzow gov info katowice krakow lodz lublin mil net ngo olsztyn org poznan pwr radom slupsk szczecin torun warszawa waw wroc wroclaw zgora ",pr:" ac biz com edu est gov info isla name net org pro prof ",
|
||||
ps:" com edu gov net org plo sec ",pw:" belau co ed go ne or ",ro:" arts com firm info nom nt org rec store tm www ",rs:" ac co edu gov in org ",sb:" com edu gov net org ",sc:" com edu gov net org ",sh:" co com edu gov net nom org ",sl:" com edu gov net org ",st:" co com consulado edu embaixada gov mil net org principe saotome store ",sv:" com edu gob org red ",sz:" ac co org ",tr:" av bbs bel biz com dr edu gen gov info k12 name net org pol tel tsk tv web ",tt:" aero biz cat co com coop edu gov info int jobs mil mobi museum name net org pro tel travel ",
|
||||
tw:" club com ebiz edu game gov idv mil net org ",mu:" ac co com gov net or org ",mz:" ac co edu gov org ",na:" co com ",nz:" ac co cri geek gen govt health iwi maori mil net org parliament school ",pa:" abo ac com edu gob ing med net nom org sld ",pt:" com edu gov int net nome org publ ",py:" com edu gov mil net org ",qa:" com edu gov mil net org ",re:" asso com nom ",ru:" ac adygeya altai amur arkhangelsk astrakhan bashkiria belgorod bir bryansk buryatia cbg chel chelyabinsk chita chukotka chuvashia com dagestan e-burg edu gov grozny int irkutsk ivanovo izhevsk jar joshkar-ola kalmykia kaluga kamchatka karelia kazan kchr kemerovo khabarovsk khakassia khv kirov koenig komi kostroma kranoyarsk kuban kurgan kursk lipetsk magadan mari mari-el marine mil mordovia mosreg msk murmansk nalchik net nnov nov novosibirsk nsk omsk orenburg org oryol penza perm pp pskov ptz rnd ryazan sakhalin samara saratov simbirsk smolensk spb stavropol stv surgut tambov tatarstan tom tomsk tsaritsyn tsk tula tuva tver tyumen udm udmurtia ulan-ude vladikavkaz vladimir vladivostok volgograd vologda voronezh vrn vyatka yakutia yamal yekaterinburg yuzhno-sakhalinsk ",
|
||||
rw:" ac co com edu gouv gov int mil net ",sa:" com edu gov med net org pub sch ",sd:" com edu gov info med net org tv ",se:" a ac b bd c d e f g h i k l m n o org p parti pp press r s t tm u w x y z ",sg:" com edu gov idn net org per ",sn:" art com edu gouv org perso univ ",sy:" com edu gov mil net news org ",th:" ac co go in mi net or ",tj:" ac biz co com edu go gov info int mil name net nic org test web ",tn:" agrinet com defense edunet ens fin gov ind info intl mincom nat net org perso rnrt rns rnu tourism ",
|
||||
tz:" ac co go ne or ",ua:" biz cherkassy chernigov chernovtsy ck cn co com crimea cv dn dnepropetrovsk donetsk dp edu gov if in ivano-frankivsk kh kharkov kherson khmelnitskiy kiev kirovograd km kr ks kv lg lugansk lutsk lviv me mk net nikolaev od odessa org pl poltava pp rovno rv sebastopol sumy te ternopil uzhgorod vinnica vn zaporizhzhe zhitomir zp zt ",ug:" ac co go ne or org sc ",uk:" ac bl british-library co cym gov govt icnet jet lea ltd me mil mod national-library-scotland nel net nhs nic nls org orgn parliament plc police sch scot soc ",
|
||||
us:" dni fed isa kids nsn ",uy:" com edu gub mil net org ",ve:" co com edu gob info mil net org web ",vi:" co com k12 net org ",vn:" ac biz com edu gov health info int name net org pro ",ye:" co com gov ltd me net org plc ",yu:" ac co edu gov org ",za:" ac agric alt bourse city co cybernet db edu gov grondar iaccess imt inca landesign law mil net ngo nis nom olivetti org pix school tm web ",zm:" ac co com edu gov net org sch ",com:"ar br cn de eu gb gr hu jpn kr no qc ru sa se uk us uy za ",net:"gb jp se uk ",
|
||||
org:"ae",de:"com "},has:function(m){var d=m.lastIndexOf(".");if(0>=d||d>=m.length-1)return!1;var q=m.lastIndexOf(".",d-1);if(0>=q||q>=d-1)return!1;var B=k.list[m.slice(d+1)];return B?0<=B.indexOf(" "+m.slice(q+1,d)+" "):!1},is:function(m){var d=m.lastIndexOf(".");if(0>=d||d>=m.length-1||0<=m.lastIndexOf(".",d-1))return!1;var q=k.list[m.slice(d+1)];return q?0<=q.indexOf(" "+m.slice(0,d)+" "):!1},get:function(m){var d=m.lastIndexOf(".");if(0>=d||d>=m.length-1)return null;var q=m.lastIndexOf(".",d-1);
|
||||
if(0>=q||q>=d-1)return null;var B=k.list[m.slice(d+1)];return!B||0>B.indexOf(" "+m.slice(q+1,d)+" ")?null:m.slice(q+1)},noConflict:function(){r.SecondLevelDomains===this&&(r.SecondLevelDomains=x);return this}};return k});
|
||||
(function(r,x){"object"===typeof module&&module.exports?module.exports=x(require("./punycode"),require("./IPv6"),require("./SecondLevelDomains")):"function"===typeof define&&define.amd?define(["./punycode","./IPv6","./SecondLevelDomains"],x):r.URI=x(r.punycode,r.IPv6,r.SecondLevelDomains,r)})(this,function(r,x,k,m){function d(a,b){var c=1<=arguments.length,e=2<=arguments.length;if(!(this instanceof d))return c?e?new d(a,b):new d(a):new d;if(void 0===a){if(c)throw new TypeError("undefined is not a valid argument for URI");
|
||||
a="undefined"!==typeof location?location.href+"":""}if(null===a&&c)throw new TypeError("null is not a valid argument for URI");this.href(a);return void 0!==b?this.absoluteTo(b):this}function q(a){return a.replace(/([.*+?^=!:${}()|[\]\/\\])/g,"\\$1")}function B(a){return void 0===a?"Undefined":String(Object.prototype.toString.call(a)).slice(8,-1)}function z(a){return"Array"===B(a)}function h(a,b){var c={},e;if("RegExp"===B(b))c=null;else if(z(b)){var f=0;for(e=b.length;f<e;f++)c[b[f]]=!0}else c[b]=
|
||||
!0;f=0;for(e=a.length;f<e;f++)if(c&&void 0!==c[a[f]]||!c&&b.test(a[f]))a.splice(f,1),e--,f--;return a}function p(a,b){var c;if(z(b)){var e=0;for(c=b.length;e<c;e++)if(!p(a,b[e]))return!1;return!0}var f=B(b);e=0;for(c=a.length;e<c;e++)if("RegExp"===f){if("string"===typeof a[e]&&a[e].match(b))return!0}else if(a[e]===b)return!0;return!1}function F(a,b){if(!z(a)||!z(b)||a.length!==b.length)return!1;a.sort();b.sort();for(var c=0,e=a.length;c<e;c++)if(a[c]!==b[c])return!1;return!0}function u(a){return a.replace(/^\/+|\/+$/g,
|
||||
"")}function J(a){return escape(a)}function C(a){return encodeURIComponent(a).replace(/[!'()*]/g,J).replace(/\*/g,"%2A")}function w(a){return function(b,c){if(void 0===b)return this._parts[a]||"";this._parts[a]=b||null;this.build(!c);return this}}function G(a,b){return function(c,e){if(void 0===c)return this._parts[a]||"";null!==c&&(c+="",c.charAt(0)===b&&(c=c.substring(1)));this._parts[a]=c;this.build(!e);return this}}var v=m&&m.URI;d.version="1.19.11";var g=d.prototype,A=Object.prototype.hasOwnProperty;
|
||||
d._parts=function(){return{protocol:null,username:null,password:null,hostname:null,urn:null,port:null,path:null,query:null,fragment:null,preventInvalidHostname:d.preventInvalidHostname,duplicateQueryParameters:d.duplicateQueryParameters,escapeQuerySpace:d.escapeQuerySpace}};d.preventInvalidHostname=!1;d.duplicateQueryParameters=!1;d.escapeQuerySpace=!0;d.protocol_expression=/^[a-z][a-z0-9.+-]*$/i;d.idn_expression=/[^a-z0-9\._-]/i;d.punycode_expression=/(xn--)/i;d.ip4_expression=/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
|
||||
d.ip6_expression=/^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/;
|
||||
d.find_uri_expression=/\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?\u00ab\u00bb\u201c\u201d\u2018\u2019]))/ig;d.findUri={start:/\b(?:([a-z][a-z0-9.+-]*:\/\/)|www\.)/gi,end:/[\s\r\n]|$/,trim:/[`!()\[\]{};:'".,<>?\u00ab\u00bb\u201c\u201d\u201e\u2018\u2019]+$/,parens:/(\([^\)]*\)|\[[^\]]*\]|\{[^}]*\}|<[^>]*>)/g};d.leading_whitespace_expression=/^[\x00-\x20\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/;
|
||||
d.ascii_tab_whitespace=/[\u0009\u000A\u000D]+/g;d.defaultPorts={http:"80",https:"443",ftp:"21",gopher:"70",ws:"80",wss:"443"};d.hostProtocols=["http","https"];d.invalid_hostname_characters=/[^a-zA-Z0-9\.\-:_]/;d.domAttributes={a:"href",blockquote:"cite",link:"href",base:"href",script:"src",form:"action",img:"src",area:"href",iframe:"src",embed:"src",source:"src",track:"src",input:"src",audio:"src",video:"src"};d.getDomAttribute=function(a){if(a&&a.nodeName){var b=a.nodeName.toLowerCase();if("input"!==
|
||||
b||"image"===a.type)return d.domAttributes[b]}};d.encode=C;d.decode=decodeURIComponent;d.iso8859=function(){d.encode=escape;d.decode=unescape};d.unicode=function(){d.encode=C;d.decode=decodeURIComponent};d.characters={pathname:{encode:{expression:/%(24|26|2B|2C|3B|3D|3A|40)/ig,map:{"%24":"$","%26":"&","%2B":"+","%2C":",","%3B":";","%3D":"=","%3A":":","%40":"@"}},decode:{expression:/[\/\?#]/g,map:{"/":"%2F","?":"%3F","#":"%23"}}},reserved:{encode:{expression:/%(21|23|24|26|27|28|29|2A|2B|2C|2F|3A|3B|3D|3F|40|5B|5D)/ig,
|
||||
map:{"%3A":":","%2F":"/","%3F":"?","%23":"#","%5B":"[","%5D":"]","%40":"@","%21":"!","%24":"$","%26":"&","%27":"'","%28":"(","%29":")","%2A":"*","%2B":"+","%2C":",","%3B":";","%3D":"="}}},urnpath:{encode:{expression:/%(21|24|27|28|29|2A|2B|2C|3B|3D|40)/ig,map:{"%21":"!","%24":"$","%27":"'","%28":"(","%29":")","%2A":"*","%2B":"+","%2C":",","%3B":";","%3D":"=","%40":"@"}},decode:{expression:/[\/\?#:]/g,map:{"/":"%2F","?":"%3F","#":"%23",":":"%3A"}}}};d.encodeQuery=function(a,b){a=d.encode(a+"");void 0===
|
||||
b&&(b=d.escapeQuerySpace);return b?a.replace(/%20/g,"+"):a};d.decodeQuery=function(a,b){a+="";void 0===b&&(b=d.escapeQuerySpace);try{return d.decode(b?a.replace(/\+/g,"%20"):a)}catch(c){return a}};var D={encode:"encode",decode:"decode"},l,t=function(a,b){return function(c){try{return d[b](c+"").replace(d.characters[a][b].expression,function(e){return d.characters[a][b].map[e]})}catch(e){return c}}};for(l in D)d[l+"PathSegment"]=t("pathname",D[l]),d[l+"UrnPathSegment"]=t("urnpath",D[l]);D=function(a,
|
||||
b,c){return function(e){var f=c?function(K){return d[b](d[c](K))}:d[b];e=(e+"").split(a);for(var n=0,E=e.length;n<E;n++)e[n]=f(e[n]);return e.join(a)}};d.decodePath=D("/","decodePathSegment");d.decodeUrnPath=D(":","decodeUrnPathSegment");d.recodePath=D("/","encodePathSegment","decode");d.recodeUrnPath=D(":","encodeUrnPathSegment","decode");d.encodeReserved=t("reserved","encode");d.parse=function(a,b){b||={preventInvalidHostname:d.preventInvalidHostname};a=a.replace(d.leading_whitespace_expression,
|
||||
"");a=a.replace(d.ascii_tab_whitespace,"");var c=a.indexOf("#");-1<c&&(b.fragment=a.substring(c+1)||null,a=a.substring(0,c));c=a.indexOf("?");-1<c&&(b.query=a.substring(c+1)||null,a=a.substring(0,c));a=a.replace(/^(https?|ftp|wss?)?:+[/\\]*/i,"$1://");a=a.replace(/^[/\\]{2,}/i,"//");"//"===a.substring(0,2)?(b.protocol=null,a=a.substring(2),a=d.parseAuthority(a,b)):(c=a.indexOf(":"),-1<c&&(b.protocol=a.substring(0,c)||null,b.protocol&&!b.protocol.match(d.protocol_expression)?b.protocol=void 0:"//"===
|
||||
a.substring(c+1,c+3).replace(/\\/g,"/")?(a=a.substring(c+3),a=d.parseAuthority(a,b)):(a=a.substring(c+1),b.urn=!0)));b.path=a;return b};d.parseHost=function(a,b){a||="";a=a.replace(/\\/g,"/");var c=a.indexOf("/");-1===c&&(c=a.length);if("["===a.charAt(0)){var e=a.indexOf("]");b.hostname=a.substring(1,e)||null;b.port=a.substring(e+2,c)||null;"/"===b.port&&(b.port=null)}else{var f=a.indexOf(":");e=a.indexOf("/");f=a.indexOf(":",f+1);-1!==f&&(-1===e||f<e)?(b.hostname=a.substring(0,c)||null,b.port=null):
|
||||
(e=a.substring(0,c).split(":"),b.hostname=e[0]||null,b.port=e[1]||null)}b.hostname&&"/"!==a.substring(c).charAt(0)&&(c++,a="/"+a);b.preventInvalidHostname&&d.ensureValidHostname(b.hostname,b.protocol);b.port&&d.ensureValidPort(b.port);return a.substring(c)||"/"};d.parseAuthority=function(a,b){a=d.parseUserinfo(a,b);return d.parseHost(a,b)};d.parseUserinfo=function(a,b){var c=a;-1!==a.indexOf("\\")&&(a=a.replace(/\\/g,"/"));var e=a.indexOf("/"),f=a.lastIndexOf("@",-1<e?e:a.length-1);-1<f&&(-1===e||
|
||||
f<e)?(a=a.substring(0,f).split(":"),b.username=a[0]?d.decode(a[0]):null,a.shift(),b.password=a[0]?d.decode(a.join(":")):null,a=c.substring(f+1)):(b.username=null,b.password=null);return a};d.parseQuery=function(a,b){if(!a)return{};a=a.replace(/&+/g,"&").replace(/^\?*&*|&+$/g,"");if(!a)return{};var c={};a=a.split("&");for(var e=a.length,f,n,E=0;E<e;E++)if(f=a[E].split("="),n=d.decodeQuery(f.shift(),b),f=f.length?d.decodeQuery(f.join("="),b):null,"__proto__"!==n)if(A.call(c,n)){if("string"===typeof c[n]||
|
||||
null===c[n])c[n]=[c[n]];c[n].push(f)}else c[n]=f;return c};d.build=function(a){var b="",c=!1;a.protocol&&(b+=a.protocol+":");a.urn||!b&&!a.hostname||(b+="//",c=!0);b+=d.buildAuthority(a)||"";"string"===typeof a.path&&("/"!==a.path.charAt(0)&&c&&(b+="/"),b+=a.path);"string"===typeof a.query&&a.query&&(b+="?"+a.query);"string"===typeof a.fragment&&a.fragment&&(b+="#"+a.fragment);return b};d.buildHost=function(a){var b="";if(a.hostname)b=d.ip6_expression.test(a.hostname)?b+("["+a.hostname+"]"):b+a.hostname;
|
||||
else return"";a.port&&(b+=":"+a.port);return b};d.buildAuthority=function(a){return d.buildUserinfo(a)+d.buildHost(a)};d.buildUserinfo=function(a){var b="";a.username&&(b+=d.encode(a.username));a.password&&(b+=":"+d.encode(a.password));b&&(b+="@");return b};d.buildQuery=function(a,b,c){var e="",f,n;for(f in a)if("__proto__"!==f&&A.call(a,f))if(z(a[f])){var E={};var K=0;for(n=a[f].length;K<n;K++)void 0!==a[f][K]&&void 0===E[a[f][K]+""]&&(e+="&"+d.buildQueryParameter(f,a[f][K],c),!0!==b&&(E[a[f][K]+
|
||||
""]=!0))}else void 0!==a[f]&&(e+="&"+d.buildQueryParameter(f,a[f],c));return e.substring(1)};d.buildQueryParameter=function(a,b,c){return d.encodeQuery(a,c)+(null!==b?"="+d.encodeQuery(b,c):"")};d.addQuery=function(a,b,c){if("object"===typeof b)for(var e in b)A.call(b,e)&&d.addQuery(a,e,b[e]);else if("string"===typeof b)void 0===a[b]?a[b]=c:("string"===typeof a[b]&&(a[b]=[a[b]]),z(c)||(c=[c]),a[b]=(a[b]||[]).concat(c));else throw new TypeError("URI.addQuery() accepts an object, string as the name parameter");
|
||||
};d.setQuery=function(a,b,c){if("object"===typeof b)for(var e in b)A.call(b,e)&&d.setQuery(a,e,b[e]);else if("string"===typeof b)a[b]=void 0===c?null:c;else throw new TypeError("URI.setQuery() accepts an object, string as the name parameter");};d.removeQuery=function(a,b,c){var e;if(z(b))for(c=0,e=b.length;c<e;c++)a[b[c]]=void 0;else if("RegExp"===B(b))for(e in a)b.test(e)&&(a[e]=void 0);else if("object"===typeof b)for(e in b)A.call(b,e)&&d.removeQuery(a,e,b[e]);else if("string"===typeof b)void 0!==
|
||||
c?"RegExp"===B(c)?!z(a[b])&&c.test(a[b])?a[b]=void 0:a[b]=h(a[b],c):a[b]!==String(c)||z(c)&&1!==c.length?z(a[b])&&(a[b]=h(a[b],c)):a[b]=void 0:a[b]=void 0;else throw new TypeError("URI.removeQuery() accepts an object, string, RegExp as the first parameter");};d.hasQuery=function(a,b,c,e){switch(B(b)){case "String":break;case "RegExp":for(var f in a)if(A.call(a,f)&&b.test(f)&&(void 0===c||d.hasQuery(a,f,c)))return!0;return!1;case "Object":for(var n in b)if(A.call(b,n)&&!d.hasQuery(a,n,b[n]))return!1;
|
||||
return!0;default:throw new TypeError("URI.hasQuery() accepts a string, regular expression or object as the name parameter");}switch(B(c)){case "Undefined":return b in a;case "Boolean":return a=!(z(a[b])?!a[b].length:!a[b]),c===a;case "Function":return!!c(a[b],b,a);case "Array":return z(a[b])?(e?p:F)(a[b],c):!1;case "RegExp":return z(a[b])?e?p(a[b],c):!1:!(!a[b]||!a[b].match(c));case "Number":c=String(c);case "String":return z(a[b])?e?p(a[b],c):!1:a[b]===c;default:throw new TypeError("URI.hasQuery() accepts undefined, boolean, string, number, RegExp, Function as the value parameter");
|
||||
}};d.joinPaths=function(){for(var a=[],b=[],c=0,e=0;e<arguments.length;e++){var f=new d(arguments[e]);a.push(f);f=f.segment();for(var n=0;n<f.length;n++)"string"===typeof f[n]&&b.push(f[n]),f[n]&&c++}if(!b.length||!c)return new d("");b=(new d("")).segment(b);""!==a[0].path()&&"/"!==a[0].path().slice(0,1)||b.path("/"+b.path());return b.normalize()};d.commonPath=function(a,b){var c=Math.min(a.length,b.length),e;for(e=0;e<c;e++)if(a.charAt(e)!==b.charAt(e)){e--;break}if(1>e)return a.charAt(0)===b.charAt(0)&&
|
||||
"/"===a.charAt(0)?"/":"";if("/"!==a.charAt(e)||"/"!==b.charAt(e))e=a.substring(0,e).lastIndexOf("/");return a.substring(0,e+1)};d.withinString=function(a,b,c){c||={};var e=c.start||d.findUri.start,f=c.end||d.findUri.end,n=c.trim||d.findUri.trim,E=c.parens||d.findUri.parens,K=/[a-z0-9-]=["']?$/i;for(e.lastIndex=0;;){var L=e.exec(a);if(!L)break;var P=L.index;if(c.ignoreHtml){var N=a.slice(Math.max(P-3,0),P);if(N&&K.test(N))continue}var O=P+a.slice(P).search(f);N=a.slice(P,O);for(O=-1;;){var Q=E.exec(N);
|
||||
if(!Q)break;O=Math.max(O,Q.index+Q[0].length)}N=-1<O?N.slice(0,O)+N.slice(O).replace(n,""):N.replace(n,"");N.length<=L[0].length||c.ignore&&c.ignore.test(N)||(O=P+N.length,L=b(N,P,O,a),void 0===L?e.lastIndex=O:(L=String(L),a=a.slice(0,P)+L+a.slice(O),e.lastIndex=P+L.length))}e.lastIndex=0;return a};d.ensureValidHostname=function(a,b){var c=!!a,e=!1;b&&(e=p(d.hostProtocols,b));if(e&&!c)throw new TypeError("Hostname cannot be empty, if protocol is "+b);if(a&&a.match(d.invalid_hostname_characters)){if(!r)throw new TypeError('Hostname "'+
|
||||
a+'" contains characters other than [A-Z0-9.-:_] and Punycode.js is not available');if(r.toASCII(a).match(d.invalid_hostname_characters))throw new TypeError('Hostname "'+a+'" contains characters other than [A-Z0-9.-:_]');}};d.ensureValidPort=function(a){if(a){var b=Number(a);if(!(/^[0-9]+$/.test(b)&&0<b&&65536>b))throw new TypeError('Port "'+a+'" is not a valid port');}};d.noConflict=function(a){if(a)return a={URI:this.noConflict()},m.URITemplate&&"function"===typeof m.URITemplate.noConflict&&(a.URITemplate=
|
||||
m.URITemplate.noConflict()),m.IPv6&&"function"===typeof m.IPv6.noConflict&&(a.IPv6=m.IPv6.noConflict()),m.SecondLevelDomains&&"function"===typeof m.SecondLevelDomains.noConflict&&(a.SecondLevelDomains=m.SecondLevelDomains.noConflict()),a;m.URI===this&&(m.URI=v);return this};g.build=function(a){if(!0===a)this._deferred_build=!0;else if(void 0===a||this._deferred_build)this._string=d.build(this._parts),this._deferred_build=!1;return this};g.clone=function(){return new d(this)};g.valueOf=g.toString=
|
||||
function(){return this.build(!1)._string};g.protocol=w("protocol");g.username=w("username");g.password=w("password");g.hostname=w("hostname");g.port=w("port");g.query=G("query","?");g.fragment=G("fragment","#");g.search=function(a,b){a=this.query(a,b);return"string"===typeof a&&a.length?"?"+a:a};g.hash=function(a,b){a=this.fragment(a,b);return"string"===typeof a&&a.length?"#"+a:a};g.pathname=function(a,b){if(void 0===a||!0===a)return b=this._parts.path||(this._parts.hostname?"/":""),a?(this._parts.urn?
|
||||
d.decodeUrnPath:d.decodePath)(b):b;this._parts.path=this._parts.urn?a?d.recodeUrnPath(a):"":a?d.recodePath(a):"/";this.build(!b);return this};g.path=g.pathname;g.href=function(a,b){var c;if(void 0===a)return this.toString();this._string="";this._parts=d._parts();var e=a instanceof d,f="object"===typeof a&&(a.hostname||a.path||a.pathname);a.nodeName&&(f=d.getDomAttribute(a),a=a[f]||"",f=!1);!e&&f&&void 0!==a.pathname&&(a=a.toString());if("string"===typeof a||a instanceof String)this._parts=d.parse(String(a),
|
||||
this._parts);else if(e||f){a=e?a._parts:a;for(c in a)"query"!==c&&A.call(this._parts,c)&&(this._parts[c]=a[c]);a.query&&this.query(a.query,!1)}else throw new TypeError("invalid input");this.build(!b);return this};g.is=function(a){var b=!1,c=!1,e=!1,f=!1,n=!1,E=!1,K=!1,L=!this._parts.urn;this._parts.hostname&&(L=!1,c=d.ip4_expression.test(this._parts.hostname),e=d.ip6_expression.test(this._parts.hostname),b=c||e,n=(f=!b)&&k&&k.has(this._parts.hostname),E=f&&d.idn_expression.test(this._parts.hostname),
|
||||
K=f&&d.punycode_expression.test(this._parts.hostname));switch(a.toLowerCase()){case "relative":return L;case "absolute":return!L;case "domain":case "name":return f;case "sld":return n;case "ip":return b;case "ip4":case "ipv4":case "inet4":return c;case "ip6":case "ipv6":case "inet6":return e;case "idn":return E;case "url":return!this._parts.urn;case "urn":return!!this._parts.urn;case "punycode":return K}return null};var H=g.protocol,y=g.port,I=g.hostname;g.protocol=function(a,b){if(a&&(a=a.replace(/:(\/\/)?$/,
|
||||
""),!a.match(d.protocol_expression)))throw new TypeError('Protocol "'+a+"\" contains characters other than [A-Z0-9.+-] or doesn't start with [A-Z]");return H.call(this,a,b)};g.scheme=g.protocol;g.port=function(a,b){if(this._parts.urn)return void 0===a?"":this;void 0!==a&&(0===a&&(a=null),a&&(a+="",":"===a.charAt(0)&&(a=a.substring(1)),d.ensureValidPort(a)));return y.call(this,a,b)};g.hostname=function(a,b){if(this._parts.urn)return void 0===a?"":this;if(void 0!==a){var c={preventInvalidHostname:this._parts.preventInvalidHostname};
|
||||
if("/"!==d.parseHost(a,c))throw new TypeError('Hostname "'+a+'" contains characters other than [A-Z0-9.-]');a=c.hostname;this._parts.preventInvalidHostname&&d.ensureValidHostname(a,this._parts.protocol)}return I.call(this,a,b)};g.origin=function(a,b){if(this._parts.urn)return void 0===a?"":this;if(void 0===a)return b=this.protocol(),this.authority()?(b?b+"://":"")+this.authority():"";a=d(a);this.protocol(a.protocol()).authority(a.authority()).build(!b);return this};g.host=function(a,b){if(this._parts.urn)return void 0===
|
||||
a?"":this;if(void 0===a)return this._parts.hostname?d.buildHost(this._parts):"";if("/"!==d.parseHost(a,this._parts))throw new TypeError('Hostname "'+a+'" contains characters other than [A-Z0-9.-]');this.build(!b);return this};g.authority=function(a,b){if(this._parts.urn)return void 0===a?"":this;if(void 0===a)return this._parts.hostname?d.buildAuthority(this._parts):"";if("/"!==d.parseAuthority(a,this._parts))throw new TypeError('Hostname "'+a+'" contains characters other than [A-Z0-9.-]');this.build(!b);
|
||||
return this};g.userinfo=function(a,b){if(this._parts.urn)return void 0===a?"":this;if(void 0===a)return(a=d.buildUserinfo(this._parts))?a.substring(0,a.length-1):a;"@"!==a[a.length-1]&&(a+="@");d.parseUserinfo(a,this._parts);this.build(!b);return this};g.resource=function(a,b){if(void 0===a)return this.path()+this.search()+this.hash();a=d.parse(a);this._parts.path=a.path;this._parts.query=a.query;this._parts.fragment=a.fragment;this.build(!b);return this};g.subdomain=function(a,b){if(this._parts.urn)return void 0===
|
||||
a?"":this;if(void 0===a){if(!this._parts.hostname||this.is("IP"))return"";a=this._parts.hostname.length-this.domain().length-1;return this._parts.hostname.substring(0,a)||""}var c=this._parts.hostname.length-this.domain().length;c=this._parts.hostname.substring(0,c);c=new RegExp("^"+q(c));a&&"."!==a.charAt(a.length-1)&&(a+=".");if(-1!==a.indexOf(":"))throw new TypeError("Domains cannot contain colons");a&&d.ensureValidHostname(a,this._parts.protocol);this._parts.hostname=this._parts.hostname.replace(c,
|
||||
a);this.build(!b);return this};g.domain=function(a,b){if(this._parts.urn)return void 0===a?"":this;"boolean"===typeof a&&(b=a,a=void 0);if(void 0===a){if(!this._parts.hostname||this.is("IP"))return"";if((a=this._parts.hostname.match(/\./g))&&2>a.length)return this._parts.hostname;b=this._parts.hostname.length-this.tld(b).length-1;b=this._parts.hostname.lastIndexOf(".",b-1)+1;return this._parts.hostname.substring(b)||""}if(!a)throw new TypeError("cannot set domain empty");if(-1!==a.indexOf(":"))throw new TypeError("Domains cannot contain colons");
|
||||
d.ensureValidHostname(a,this._parts.protocol);if(!this._parts.hostname||this.is("IP"))this._parts.hostname=a;else{var c=new RegExp(q(this.domain())+"$");this._parts.hostname=this._parts.hostname.replace(c,a)}this.build(!b);return this};g.tld=function(a,b){if(this._parts.urn)return void 0===a?"":this;"boolean"===typeof a&&(b=a,a=void 0);if(void 0===a){if(!this._parts.hostname||this.is("IP"))return"";a=this._parts.hostname.lastIndexOf(".");a=this._parts.hostname.substring(a+1);return!0!==b&&k&&k.list[a.toLowerCase()]?
|
||||
k.get(this._parts.hostname)||a:a}if(a)if(a.match(/[^a-zA-Z0-9-]/))if(k&&k.is(a)){var c=new RegExp(q(this.tld())+"$");this._parts.hostname=this._parts.hostname.replace(c,a)}else throw new TypeError('TLD "'+a+'" contains characters other than [A-Z0-9]');else{if(!this._parts.hostname||this.is("IP"))throw new ReferenceError("cannot set TLD on non-domain host");c=new RegExp(q(this.tld())+"$");this._parts.hostname=this._parts.hostname.replace(c,a)}else throw new TypeError("cannot set TLD empty");this.build(!b);
|
||||
return this};g.directory=function(a,b){if(this._parts.urn)return void 0===a?"":this;if(void 0===a||!0===a){if(!this._parts.path&&!this._parts.hostname)return"";if("/"===this._parts.path)return"/";b=this._parts.path.length-this.filename().length-1;b=this._parts.path.substring(0,b)||(this._parts.hostname?"/":"");return a?d.decodePath(b):b}var c=this._parts.path.length-this.filename().length;c=this._parts.path.substring(0,c);c=new RegExp("^"+q(c));this.is("relative")||(a||="/","/"!==a.charAt(0)&&(a=
|
||||
"/"+a));a&&"/"!==a.charAt(a.length-1)&&(a+="/");a=d.recodePath(a);this._parts.path=this._parts.path.replace(c,a);this.build(!b);return this};g.filename=function(a,b){if(this._parts.urn)return void 0===a?"":this;if("string"!==typeof a){if(!this._parts.path||"/"===this._parts.path)return"";b=this._parts.path.lastIndexOf("/");b=this._parts.path.substring(b+1);return a?d.decodePathSegment(b):b}var c=!1;"/"===a.charAt(0)&&(a=a.substring(1));a.match(/\.?\//)&&(c=!0);var e=new RegExp(q(this.filename())+
|
||||
"$");a=d.recodePath(a);this._parts.path=this._parts.path.replace(e,a);c?this.normalizePath(b):this.build(!b);return this};g.suffix=function(a,b){if(this._parts.urn)return void 0===a?"":this;if(void 0===a||!0===a){if(!this._parts.path||"/"===this._parts.path)return"";b=this.filename();var c=b.lastIndexOf(".");if(-1===c)return"";b=b.substring(c+1);b=/^[a-z0-9%]+$/i.test(b)?b:"";return a?d.decodePathSegment(b):b}"."===a.charAt(0)&&(a=a.substring(1));c=this.suffix();if(c)var e=a?new RegExp(q(c)+"$"):
|
||||
new RegExp(q("."+c)+"$");else{if(!a)return this;this._parts.path+="."+d.recodePath(a)}e&&(a=d.recodePath(a),this._parts.path=this._parts.path.replace(e,a));this.build(!b);return this};g.segment=function(a,b,c){var e=this._parts.urn?":":"/",f=this.path(),n="/"===f.substring(0,1);f=f.split(e);void 0!==a&&"number"!==typeof a&&(c=b,b=a,a=void 0);if(void 0!==a&&"number"!==typeof a)throw Error('Bad segment "'+a+'", must be 0-based integer');n&&f.shift();0>a&&(a=Math.max(f.length+a,0));if(void 0===b)return void 0===
|
||||
a?f:f[a];if(null===a||void 0===f[a])if(z(b)){f=[];a=0;for(var E=b.length;a<E;a++)if(b[a].length||f.length&&f[f.length-1].length)f.length&&!f[f.length-1].length&&f.pop(),f.push(u(b[a]))}else{if(b||"string"===typeof b)b=u(b),""===f[f.length-1]?f[f.length-1]=b:f.push(b)}else b?f[a]=u(b):f.splice(a,1);n&&f.unshift("");return this.path(f.join(e),c)};g.segmentCoded=function(a,b,c){var e;"number"!==typeof a&&(c=b,b=a,a=void 0);if(void 0===b){a=this.segment(a,b,c);if(z(a)){var f=0;for(e=a.length;f<e;f++)a[f]=
|
||||
d.decode(a[f])}else a=void 0!==a?d.decode(a):void 0;return a}if(z(b))for(f=0,e=b.length;f<e;f++)b[f]=d.encode(b[f]);else b="string"===typeof b||b instanceof String?d.encode(b):b;return this.segment(a,b,c)};var M=g.query;g.query=function(a,b){if(!0===a)return d.parseQuery(this._parts.query,this._parts.escapeQuerySpace);if("function"===typeof a){var c=d.parseQuery(this._parts.query,this._parts.escapeQuerySpace);a=a.call(this,c);this._parts.query=d.buildQuery(a||c,this._parts.duplicateQueryParameters,
|
||||
this._parts.escapeQuerySpace);this.build(!b);return this}return void 0!==a&&"string"!==typeof a?(this._parts.query=d.buildQuery(a,this._parts.duplicateQueryParameters,this._parts.escapeQuerySpace),this.build(!b),this):M.call(this,a,b)};g.setQuery=function(a,b,c){var e=d.parseQuery(this._parts.query,this._parts.escapeQuerySpace);if("string"===typeof a||a instanceof String)e[a]=void 0!==b?b:null;else if("object"===typeof a)for(var f in a)A.call(a,f)&&(e[f]=a[f]);else throw new TypeError("URI.addQuery() accepts an object, string as the name parameter");
|
||||
this._parts.query=d.buildQuery(e,this._parts.duplicateQueryParameters,this._parts.escapeQuerySpace);"string"!==typeof a&&(c=b);this.build(!c);return this};g.addQuery=function(a,b,c){var e=d.parseQuery(this._parts.query,this._parts.escapeQuerySpace);d.addQuery(e,a,void 0===b?null:b);this._parts.query=d.buildQuery(e,this._parts.duplicateQueryParameters,this._parts.escapeQuerySpace);"string"!==typeof a&&(c=b);this.build(!c);return this};g.removeQuery=function(a,b,c){var e=d.parseQuery(this._parts.query,
|
||||
this._parts.escapeQuerySpace);d.removeQuery(e,a,b);this._parts.query=d.buildQuery(e,this._parts.duplicateQueryParameters,this._parts.escapeQuerySpace);"string"!==typeof a&&(c=b);this.build(!c);return this};g.hasQuery=function(a,b,c){var e=d.parseQuery(this._parts.query,this._parts.escapeQuerySpace);return d.hasQuery(e,a,b,c)};g.setSearch=g.setQuery;g.addSearch=g.addQuery;g.removeSearch=g.removeQuery;g.hasSearch=g.hasQuery;g.normalize=function(){return this._parts.urn?this.normalizeProtocol(!1).normalizePath(!1).normalizeQuery(!1).normalizeFragment(!1).build():
|
||||
this.normalizeProtocol(!1).normalizeHostname(!1).normalizePort(!1).normalizePath(!1).normalizeQuery(!1).normalizeFragment(!1).build()};g.normalizeProtocol=function(a){"string"===typeof this._parts.protocol&&(this._parts.protocol=this._parts.protocol.toLowerCase(),this.build(!a));return this};g.normalizeHostname=function(a){this._parts.hostname&&(this.is("IDN")&&r?this._parts.hostname=r.toASCII(this._parts.hostname):this.is("IPv6")&&x&&(this._parts.hostname=x.best(this._parts.hostname)),this._parts.hostname=
|
||||
this._parts.hostname.toLowerCase(),this.build(!a));return this};g.normalizePort=function(a){"string"===typeof this._parts.protocol&&this._parts.port===d.defaultPorts[this._parts.protocol]&&(this._parts.port=null,this.build(!a));return this};g.normalizePath=function(a){var b=this._parts.path;if(!b)return this;if(this._parts.urn)return this._parts.path=d.recodeUrnPath(this._parts.path),this.build(!a),this;if("/"===this._parts.path)return this;b=d.recodePath(b);var c="";if("/"!==b.charAt(0)){var e=!0;
|
||||
b="/"+b}if("/.."===b.slice(-3)||"/."===b.slice(-2))b+="/";b=b.replace(/(\/(\.\/)+)|(\/\.$)/g,"/").replace(/\/{2,}/g,"/");e&&(c=b.substring(1).match(/^(\.\.\/)+/)||"")&&(c=c[0]);for(;;){var f=b.search(/\/\.\.(\/|$)/);if(-1===f)break;else if(0===f){b=b.substring(3);continue}var n=b.substring(0,f).lastIndexOf("/");-1===n&&(n=f);b=b.substring(0,n)+b.substring(f+3)}e&&this.is("relative")&&(b=c+b.substring(1));this._parts.path=b;this.build(!a);return this};g.normalizePathname=g.normalizePath;g.normalizeQuery=
|
||||
function(a){"string"===typeof this._parts.query&&(this._parts.query.length?this.query(d.parseQuery(this._parts.query,this._parts.escapeQuerySpace)):this._parts.query=null,this.build(!a));return this};g.normalizeFragment=function(a){this._parts.fragment||(this._parts.fragment=null,this.build(!a));return this};g.normalizeSearch=g.normalizeQuery;g.normalizeHash=g.normalizeFragment;g.iso8859=function(){var a=d.encode,b=d.decode;d.encode=escape;d.decode=decodeURIComponent;try{this.normalize()}finally{d.encode=
|
||||
a,d.decode=b}return this};g.unicode=function(){var a=d.encode,b=d.decode;d.encode=C;d.decode=unescape;try{this.normalize()}finally{d.encode=a,d.decode=b}return this};g.readable=function(){var a=this.clone();a.username("").password("").normalize();var b="";a._parts.protocol&&(b+=a._parts.protocol+"://");a._parts.hostname&&(a.is("punycode")&&r?(b+=r.toUnicode(a._parts.hostname),a._parts.port&&(b+=":"+a._parts.port)):b+=a.host());a._parts.hostname&&a._parts.path&&"/"!==a._parts.path.charAt(0)&&(b+="/");
|
||||
b+=a.path(!0);if(a._parts.query){for(var c="",e=0,f=a._parts.query.split("&"),n=f.length;e<n;e++){var E=(f[e]||"").split("=");c+="&"+d.decodeQuery(E[0],this._parts.escapeQuerySpace).replace(/&/g,"%26");void 0!==E[1]&&(c+="="+d.decodeQuery(E[1],this._parts.escapeQuerySpace).replace(/&/g,"%26"))}b+="?"+c.substring(1)}return b+=d.decodeQuery(a.hash(),!0)};g.absoluteTo=function(a){var b=this.clone(),c=["protocol","username","password","hostname","port"],e,f;if(this._parts.urn)throw Error("URNs do not have any generally defined hierarchical components");
|
||||
a instanceof d||(a=new d(a));if(b._parts.protocol)return b;b._parts.protocol=a._parts.protocol;if(this._parts.hostname)return b;for(e=0;f=c[e];e++)b._parts[f]=a._parts[f];b._parts.path?(".."===b._parts.path.substring(-2)&&(b._parts.path+="/"),"/"!==b.path().charAt(0)&&(c=(c=a.directory())?c:0===a.path().indexOf("/")?"/":"",b._parts.path=(c?c+"/":"")+b._parts.path,b.normalizePath())):(b._parts.path=a._parts.path,b._parts.query||(b._parts.query=a._parts.query));b.build();return b};g.relativeTo=function(a){var b=
|
||||
this.clone().normalize();if(b._parts.urn)throw Error("URNs do not have any generally defined hierarchical components");a=(new d(a)).normalize();var c=b._parts;var e=a._parts;var f=b.path();a=a.path();if("/"!==f.charAt(0))throw Error("URI is already relative");if("/"!==a.charAt(0))throw Error("Cannot calculate a URI relative to another relative URI");c.protocol===e.protocol&&(c.protocol=null);if(c.username===e.username&&c.password===e.password&&null===c.protocol&&null===c.username&&null===c.password&&
|
||||
c.hostname===e.hostname&&c.port===e.port)c.hostname=null,c.port=null;else return b.build();if(f===a)return c.path="",b.build();f=d.commonPath(f,a);if(!f)return b.build();e=e.path.substring(f.length).replace(/[^\/]*$/,"").replace(/.*?\//g,"../");c.path=e+c.path.substring(f.length)||"./";return b.build()};g.equals=function(a){var b=this.clone(),c=new d(a);a={};var e;b.normalize();c.normalize();if(b.toString()===c.toString())return!0;var f=b.query();var n=c.query();b.query("");c.query("");if(b.toString()!==
|
||||
c.toString()||f.length!==n.length)return!1;b=d.parseQuery(f,this._parts.escapeQuerySpace);n=d.parseQuery(n,this._parts.escapeQuerySpace);for(e in b)if(A.call(b,e)){if(!z(b[e])){if(b[e]!==n[e])return!1}else if(!F(b[e],n[e]))return!1;a[e]=!0}for(e in n)if(A.call(n,e)&&!a[e])return!1;return!0};g.preventInvalidHostname=function(a){this._parts.preventInvalidHostname=!!a;return this};g.duplicateQueryParameters=function(a){this._parts.duplicateQueryParameters=!!a;return this};g.escapeQuerySpace=function(a){this._parts.escapeQuerySpace=
|
||||
!!a;return this};return d});
|
||||
(function(r,x){"object"===typeof module&&module.exports?module.exports=x(require("./URI")):"function"===typeof define&&define.amd?define(["./URI"],x):r.URITemplate=x(r.URI,r)})(this,function(r,x){function k(h){if(k._cache[h])return k._cache[h];if(!(this instanceof k))return new k(h);this.expression=h;k._cache[h]=this;return this}function m(h){this.data=h;this.cache={}}var d=x&&x.URITemplate,q=Object.prototype.hasOwnProperty,B=k.prototype,z={"":{prefix:"",separator:",",named:!1,empty_name_separator:!1,
|
||||
encode:"encode"},"+":{prefix:"",separator:",",named:!1,empty_name_separator:!1,encode:"encodeReserved"},"#":{prefix:"#",separator:",",named:!1,empty_name_separator:!1,encode:"encodeReserved"},".":{prefix:".",separator:".",named:!1,empty_name_separator:!1,encode:"encode"},"/":{prefix:"/",separator:"/",named:!1,empty_name_separator:!1,encode:"encode"},";":{prefix:";",separator:";",named:!0,empty_name_separator:!1,encode:"encode"},"?":{prefix:"?",separator:"&",named:!0,empty_name_separator:!0,encode:"encode"},
|
||||
"&":{prefix:"&",separator:"&",named:!0,empty_name_separator:!0,encode:"encode"}};k._cache={};k.EXPRESSION_PATTERN=/\{([^a-zA-Z0-9%_]?)([^\}]+)(\}|$)/g;k.VARIABLE_PATTERN=/^([^*:.](?:\.?[^*:.])*)((\*)|:(\d+))?$/;k.VARIABLE_NAME_PATTERN=/[^a-zA-Z0-9%_.]/;k.LITERAL_PATTERN=/[<>{}"`^| \\]/;k.expand=function(h,p,F){var u=z[h.operator],J=u.named?"Named":"Unnamed";h=h.variables;var C=[],w,G;for(G=0;w=h[G];G++){var v=p.get(w.name);if(0===v.type&&F&&F.strict)throw Error('Missing expansion value for variable "'+
|
||||
w.name+'"');if(v.val.length){if(1<v.type&&w.maxlength)throw Error('Invalid expression: Prefix modifier not applicable to variable "'+w.name+'"');C.push(k["expand"+J](v,u,w.explode,w.explode&&u.separator||",",w.maxlength,w.name))}else v.type&&C.push("")}return C.length?u.prefix+C.join(u.separator):""};k.expandNamed=function(h,p,F,u,J,C){var w="",G=p.encode;p=p.empty_name_separator;var v=!h[G].length,g=2===h.type?"":r[G](C),A;var D=0;for(A=h.val.length;D<A;D++){if(J){var l=r[G](h.val[D][1].substring(0,
|
||||
J));2===h.type&&(g=r[G](h.val[D][0].substring(0,J)))}else v?(l=r[G](h.val[D][1]),2===h.type?(g=r[G](h.val[D][0]),h[G].push([g,l])):h[G].push([void 0,l])):(l=h[G][D][1],2===h.type&&(g=h[G][D][0]));w&&(w+=u);F?w+=g+(p||l?"=":"")+l:(D||(w+=r[G](C)+(p||l?"=":"")),2===h.type&&(w+=g+","),w+=l)}return w};k.expandUnnamed=function(h,p,F,u,J){var C="",w=p.encode;p=p.empty_name_separator;var G=!h[w].length,v;var g=0;for(v=h.val.length;g<v;g++){if(J)var A=r[w](h.val[g][1].substring(0,J));else G?(A=r[w](h.val[g][1]),
|
||||
h[w].push([2===h.type?r[w](h.val[g][0]):void 0,A])):A=h[w][g][1];C&&(C+=u);if(2===h.type){var D=J?r[w](h.val[g][0].substring(0,J)):h[w][g][0];C+=D;C=F?C+(p||A?"=":""):C+","}C+=A}return C};k.noConflict=function(){x.URITemplate===k&&(x.URITemplate=d);return k};B.expand=function(h,p){var F="";this.parts&&this.parts.length||this.parse();h instanceof m||(h=new m(h));for(var u=0,J=this.parts.length;u<J;u++)F+="string"===typeof this.parts[u]?this.parts[u]:k.expand(this.parts[u],h,p);return F};B.parse=function(){var h=
|
||||
this.expression,p=k.EXPRESSION_PATTERN,F=k.VARIABLE_PATTERN,u=k.VARIABLE_NAME_PATTERN,J=k.LITERAL_PATTERN,C=[],w=0,G=function(t){if(t.match(J))throw Error('Invalid Literal "'+t+'"');return t};for(p.lastIndex=0;;){var v=p.exec(h);if(null===v){C.push(G(h.substring(w)));break}else C.push(G(h.substring(w,v.index))),w=v.index+v[0].length;if(!z[v[1]])throw Error('Unknown Operator "'+v[1]+'" in "'+v[0]+'"');if(!v[3])throw Error('Unclosed Expression "'+v[0]+'"');var g=v[2].split(",");for(var A=0,D=g.length;A<
|
||||
D;A++){var l=g[A].match(F);if(null===l)throw Error('Invalid Variable "'+g[A]+'" in "'+v[0]+'"');if(l[1].match(u))throw Error('Invalid Variable Name "'+l[1]+'" in "'+v[0]+'"');g[A]={name:l[1],explode:!!l[3],maxlength:l[4]&&parseInt(l[4],10)}}if(!g.length)throw Error('Expression Missing Variable(s) "'+v[0]+'"');C.push({expression:v[0],operator:v[1],variables:g})}C.length||C.push(G(h));this.parts=C;return this};m.prototype.get=function(h){var p=this.data,F={type:0,val:[],encode:[],encodeReserved:[]};
|
||||
if(void 0!==this.cache[h])return this.cache[h];this.cache[h]=F;p="[object Function]"===String(Object.prototype.toString.call(p))?p(h):"[object Function]"===String(Object.prototype.toString.call(p[h]))?p[h](h):p[h];if(void 0!==p&&null!==p)if("[object Array]"===String(Object.prototype.toString.call(p))){var u=0;for(h=p.length;u<h;u++)void 0!==p[u]&&null!==p[u]&&F.val.push([void 0,String(p[u])]);F.val.length&&(F.type=3)}else if("[object Object]"===String(Object.prototype.toString.call(p))){for(u in p)q.call(p,
|
||||
u)&&void 0!==p[u]&&null!==p[u]&&F.val.push([u,String(p[u])]);F.val.length&&(F.type=2)}else F.type=1,F.val.push([void 0,String(p)]);return F};r.expand=function(h,p){h=(new k(h)).expand(p);return new r(h)};return k});
|
||||
|
||||
6
web/assets/vue/vue.min.js
vendored
Normal file
6
web/assets/vue/vue.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
42
web/controller/api.go
Normal file
42
web/controller/api.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type APIController struct {
|
||||
BaseController
|
||||
inboundController *InboundController
|
||||
serverController *ServerController
|
||||
Tgbot service.Tgbot
|
||||
serverService service.ServerService
|
||||
}
|
||||
|
||||
func NewAPIController(g *gin.RouterGroup) *APIController {
|
||||
a := &APIController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||
// Main API group
|
||||
api := g.Group("/panel/api")
|
||||
api.Use(a.checkLogin)
|
||||
|
||||
// Inbounds API
|
||||
inbounds := api.Group("/inbounds")
|
||||
a.inboundController = NewInboundController(inbounds)
|
||||
|
||||
// Server API
|
||||
server := api.Group("/server")
|
||||
a.serverController = NewServerController(server, a.serverService)
|
||||
|
||||
// Extra routes
|
||||
api.GET("/backuptotgbot", a.BackuptoTgbot)
|
||||
}
|
||||
|
||||
func (a *APIController) BackuptoTgbot(c *gin.Context) {
|
||||
a.Tgbot.SendBackupToAdmins()
|
||||
}
|
||||
37
web/controller/base.go
Normal file
37
web/controller/base.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"x-ui/logger"
|
||||
"x-ui/web/locale"
|
||||
"x-ui/web/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type BaseController struct{}
|
||||
|
||||
func (a *BaseController) checkLogin(c *gin.Context) {
|
||||
if !session.IsLogin(c) {
|
||||
if isAjax(c) {
|
||||
pureJsonMsg(c, http.StatusUnauthorized, false, I18nWeb(c, "pages.login.loginAgain"))
|
||||
} else {
|
||||
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
|
||||
}
|
||||
c.Abort()
|
||||
} else {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func I18nWeb(c *gin.Context, name string, params ...string) string {
|
||||
anyfunc, funcExists := c.Get("I18n")
|
||||
if !funcExists {
|
||||
logger.Warning("I18n function not exists in gin context!")
|
||||
return ""
|
||||
}
|
||||
i18nFunc, _ := anyfunc.(func(i18nType locale.I18nType, key string, keyParams ...string) string)
|
||||
msg := i18nFunc(locale.Web, name, params...)
|
||||
return msg
|
||||
}
|
||||
375
web/controller/inbound.go
Normal file
375
web/controller/inbound.go
Normal file
@@ -0,0 +1,375 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"x-ui/database/model"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type InboundController struct {
|
||||
inboundService service.InboundService
|
||||
xrayService service.XrayService
|
||||
}
|
||||
|
||||
func NewInboundController(g *gin.RouterGroup) *InboundController {
|
||||
a := &InboundController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||
g.GET("/list", a.getInbounds)
|
||||
g.GET("/get/:id", a.getInbound)
|
||||
g.GET("/getClientTraffics/:email", a.getClientTraffics)
|
||||
g.GET("/getClientTrafficsById/:id", a.getClientTrafficsById)
|
||||
|
||||
g.POST("/add", a.addInbound)
|
||||
g.POST("/del/:id", a.delInbound)
|
||||
g.POST("/update/:id", a.updateInbound)
|
||||
g.POST("/clientIps/:email", a.getClientIps)
|
||||
g.POST("/clearClientIps/:email", a.clearClientIps)
|
||||
g.POST("/addClient", a.addInboundClient)
|
||||
g.POST("/:id/delClient/:clientId", a.delInboundClient)
|
||||
g.POST("/updateClient/:clientId", a.updateInboundClient)
|
||||
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
||||
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
||||
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
||||
g.POST("/import", a.importInbound)
|
||||
g.POST("/onlines", a.onlines)
|
||||
g.POST("/lastOnline", a.lastOnline)
|
||||
g.POST("/updateClientTraffic/:email", a.updateClientTraffic)
|
||||
}
|
||||
|
||||
func (a *InboundController) getInbounds(c *gin.Context) {
|
||||
user := session.GetLoginUser(c)
|
||||
inbounds, err := a.inboundService.GetInbounds(user.Id)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, inbounds, nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) getInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "get"), err)
|
||||
return
|
||||
}
|
||||
inbound, err := a.inboundService.GetInbound(id)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, inbound, nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) getClientTraffics(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.trafficGetError"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, clientTraffics, nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) getClientTrafficsById(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
clientTraffics, err := a.inboundService.GetClientTrafficByID(id)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.trafficGetError"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, clientTraffics, nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) addInbound(c *gin.Context) {
|
||||
inbound := &model.Inbound{}
|
||||
err := c.ShouldBind(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), err)
|
||||
return
|
||||
}
|
||||
user := session.GetLoginUser(c)
|
||||
inbound.UserId = user.Id
|
||||
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
||||
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
||||
} else {
|
||||
inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port)
|
||||
}
|
||||
|
||||
needRestart := false
|
||||
inbound, needRestart, err = a.inboundService.AddInbound(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, nil)
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) delInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), err)
|
||||
return
|
||||
}
|
||||
needRestart := true
|
||||
needRestart, err = a.inboundService.DelInbound(id)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, nil)
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) updateInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
||||
return
|
||||
}
|
||||
inbound := &model.Inbound{
|
||||
Id: id,
|
||||
}
|
||||
err = c.ShouldBind(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
||||
return
|
||||
}
|
||||
needRestart := true
|
||||
inbound, needRestart, err = a.inboundService.UpdateInbound(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, nil)
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) getClientIps(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
|
||||
ips, err := a.inboundService.GetInboundClientIps(email)
|
||||
if err != nil || ips == "" {
|
||||
jsonObj(c, "No IP Record", nil)
|
||||
return
|
||||
}
|
||||
|
||||
jsonObj(c, ips, nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) clearClientIps(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
|
||||
err := a.inboundService.ClearClientIps(email)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.updateSuccess"), err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.logCleanSuccess"), nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||
data := &model.Inbound{}
|
||||
err := c.ShouldBind(data)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
||||
return
|
||||
}
|
||||
|
||||
needRestart := true
|
||||
|
||||
needRestart, err = a.inboundService.AddInboundClient(data)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientAddSuccess"), nil)
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
||||
return
|
||||
}
|
||||
clientId := c.Param("clientId")
|
||||
|
||||
needRestart := true
|
||||
|
||||
needRestart, err = a.inboundService.DelInboundClient(id, clientId)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientDeleteSuccess"), nil)
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||
clientId := c.Param("clientId")
|
||||
|
||||
inbound := &model.Inbound{}
|
||||
err := c.ShouldBind(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
||||
return
|
||||
}
|
||||
|
||||
needRestart := true
|
||||
|
||||
needRestart, err = a.inboundService.UpdateInboundClient(inbound, clientId)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
||||
return
|
||||
}
|
||||
email := c.Param("email")
|
||||
|
||||
needRestart, err := a.inboundService.ResetClientTraffic(id, email)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetInboundClientTrafficSuccess"), nil)
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) resetAllTraffics(c *gin.Context) {
|
||||
err := a.inboundService.ResetAllTraffics()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
} else {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllTrafficSuccess"), nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
||||
return
|
||||
}
|
||||
|
||||
err = a.inboundService.ResetAllClientTraffics(id)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
} else {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllClientTrafficSuccess"), nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) importInbound(c *gin.Context) {
|
||||
inbound := &model.Inbound{}
|
||||
err := json.Unmarshal([]byte(c.PostForm("data")), inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
user := session.GetLoginUser(c)
|
||||
inbound.Id = 0
|
||||
inbound.UserId = user.Id
|
||||
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
||||
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
||||
} else {
|
||||
inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port)
|
||||
}
|
||||
|
||||
for index := range inbound.ClientStats {
|
||||
inbound.ClientStats[index].Id = 0
|
||||
inbound.ClientStats[index].Enable = true
|
||||
}
|
||||
|
||||
needRestart := false
|
||||
inbound, needRestart, err = a.inboundService.AddInbound(inbound)
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, err)
|
||||
if err == nil && needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) delDepletedClients(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
||||
return
|
||||
}
|
||||
err = a.inboundService.DelDepletedClients(id)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.delDepletedClientsSuccess"), nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) onlines(c *gin.Context) {
|
||||
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) lastOnline(c *gin.Context) {
|
||||
data, err := a.inboundService.GetClientsLastOnline()
|
||||
jsonObj(c, data, err)
|
||||
}
|
||||
|
||||
func (a *InboundController) updateClientTraffic(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
|
||||
// Define the request structure for traffic update
|
||||
type TrafficUpdateRequest struct {
|
||||
Upload int64 `json:"upload"`
|
||||
Download int64 `json:"download"`
|
||||
}
|
||||
|
||||
var request TrafficUpdateRequest
|
||||
err := c.ShouldBindJSON(&request)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
||||
return
|
||||
}
|
||||
|
||||
err = a.inboundService.UpdateClientTrafficByEmail(email, request.Upload, request.Download)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
|
||||
}
|
||||
115
web/controller/index.go
Normal file
115
web/controller/index.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"x-ui/logger"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type LoginForm struct {
|
||||
Username string `json:"username" form:"username"`
|
||||
Password string `json:"password" form:"password"`
|
||||
TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
|
||||
}
|
||||
|
||||
type IndexController struct {
|
||||
BaseController
|
||||
|
||||
settingService service.SettingService
|
||||
userService service.UserService
|
||||
tgbot service.Tgbot
|
||||
}
|
||||
|
||||
func NewIndexController(g *gin.RouterGroup) *IndexController {
|
||||
a := &IndexController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *IndexController) initRouter(g *gin.RouterGroup) {
|
||||
g.GET("/", a.index)
|
||||
g.POST("/login", a.login)
|
||||
g.GET("/logout", a.logout)
|
||||
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
|
||||
}
|
||||
|
||||
func (a *IndexController) index(c *gin.Context) {
|
||||
if session.IsLogin(c) {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "panel/")
|
||||
return
|
||||
}
|
||||
html(c, "login.html", "pages.login.title", nil)
|
||||
}
|
||||
|
||||
func (a *IndexController) login(c *gin.Context) {
|
||||
var form LoginForm
|
||||
|
||||
if err := c.ShouldBind(&form); err != nil {
|
||||
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.invalidFormData"))
|
||||
return
|
||||
}
|
||||
if form.Username == "" {
|
||||
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.emptyUsername"))
|
||||
return
|
||||
}
|
||||
if form.Password == "" {
|
||||
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.emptyPassword"))
|
||||
return
|
||||
}
|
||||
|
||||
user := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode)
|
||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||
safeUser := template.HTMLEscapeString(form.Username)
|
||||
safePass := template.HTMLEscapeString(form.Password)
|
||||
|
||||
if user == nil {
|
||||
logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c))
|
||||
a.tgbot.UserLoginNotify(safeUser, safePass, getRemoteIp(c), timeStr, 0)
|
||||
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("%s logged in successfully, Ip Address: %s\n", safeUser, getRemoteIp(c))
|
||||
a.tgbot.UserLoginNotify(safeUser, ``, getRemoteIp(c), timeStr, 1)
|
||||
|
||||
sessionMaxAge, err := a.settingService.GetSessionMaxAge()
|
||||
if err != nil {
|
||||
logger.Warning("Unable to get session's max age from DB")
|
||||
}
|
||||
|
||||
session.SetMaxAge(c, sessionMaxAge*60)
|
||||
session.SetLoginUser(c, user)
|
||||
if err := sessions.Default(c).Save(); err != nil {
|
||||
logger.Warning("Unable to save session: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("%s logged in successfully", safeUser)
|
||||
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), nil)
|
||||
}
|
||||
|
||||
func (a *IndexController) logout(c *gin.Context) {
|
||||
user := session.GetLoginUser(c)
|
||||
if user != nil {
|
||||
logger.Infof("%s logged out successfully", user.Username)
|
||||
}
|
||||
session.ClearSession(c)
|
||||
if err := sessions.Default(c).Save(); err != nil {
|
||||
logger.Warning("Unable to save session after clearing:", err)
|
||||
}
|
||||
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
|
||||
}
|
||||
|
||||
func (a *IndexController) getTwoFactorEnable(c *gin.Context) {
|
||||
status, err := a.settingService.GetTwoFactorEnable()
|
||||
if err == nil {
|
||||
jsonObj(c, status, nil)
|
||||
}
|
||||
}
|
||||
376
web/controller/server.go
Normal file
376
web/controller/server.go
Normal file
@@ -0,0 +1,376 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`)
|
||||
|
||||
type ServerController struct {
|
||||
BaseController
|
||||
|
||||
serverService service.ServerService
|
||||
settingService service.SettingService
|
||||
|
||||
lastStatus *service.Status
|
||||
lastGetStatusTime time.Time
|
||||
|
||||
lastVersions []string
|
||||
lastGetVersionsTime time.Time
|
||||
}
|
||||
|
||||
// 〔中文注释〕: 1. 在函数参数中,增加 serverService service.ServerService,让它可以接收一个服务实例。
|
||||
func NewServerController(g *gin.RouterGroup, serverService service.ServerService) *ServerController {
|
||||
a := &ServerController{
|
||||
lastGetStatusTime: time.Now(),
|
||||
// 〔中文注释〕: 2. 将传入的 serverService 赋值给 a.serverService。
|
||||
// 这样一来,这个 Controller 内部使用的就是我们在 main.go 中创建的那个功能完整的服务了。
|
||||
serverService: serverService,
|
||||
}
|
||||
a.initRouter(g)
|
||||
a.startTask()
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||
g.GET("/status", a.status)
|
||||
g.GET("/getXrayVersion", a.getXrayVersion)
|
||||
g.GET("/getConfigJson", a.getConfigJson)
|
||||
g.GET("/getDb", a.getDb)
|
||||
g.GET("/getNewUUID", a.getNewUUID)
|
||||
g.GET("/getNewX25519Cert", a.getNewX25519Cert)
|
||||
g.GET("/getNewmldsa65", a.getNewmldsa65)
|
||||
g.GET("/getNewmlkem768", a.getNewmlkem768)
|
||||
g.GET("/getNewVlessEnc", a.getNewVlessEnc)
|
||||
|
||||
g.POST("/stopXrayService", a.stopXrayService)
|
||||
g.POST("/restartXrayService", a.restartXrayService)
|
||||
g.POST("/installXray/:version", a.installXray)
|
||||
g.POST("/updateGeofile", a.updateGeofile)
|
||||
g.POST("/updateGeofile/:fileName", a.updateGeofile)
|
||||
g.POST("/logs/:count", a.getLogs)
|
||||
g.POST("/xraylogs/:count", a.getXrayLogs)
|
||||
g.POST("/importDB", a.importDB)
|
||||
g.POST("/getNewEchCert", a.getNewEchCert)
|
||||
g.POST("/history/save", a.saveHistory)
|
||||
g.GET("/history/load", a.loadHistory)
|
||||
g.POST("/install/subconverter", a.installSubconverter)
|
||||
g.POST("/openPort", a.openPort)
|
||||
}
|
||||
|
||||
func (a *ServerController) refreshStatus() {
|
||||
a.lastStatus = a.serverService.GetStatus(a.lastStatus)
|
||||
}
|
||||
|
||||
func (a *ServerController) startTask() {
|
||||
webServer := global.GetWebServer()
|
||||
c := webServer.GetCron()
|
||||
c.AddFunc("@every 2s", func() {
|
||||
now := time.Now()
|
||||
if now.Sub(a.lastGetStatusTime) > time.Minute*3 {
|
||||
return
|
||||
}
|
||||
a.refreshStatus()
|
||||
})
|
||||
}
|
||||
|
||||
func (a *ServerController) status(c *gin.Context) {
|
||||
a.lastGetStatusTime = time.Now()
|
||||
|
||||
jsonObj(c, a.lastStatus, nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||
now := time.Now()
|
||||
if now.Sub(a.lastGetVersionsTime) <= time.Minute {
|
||||
jsonObj(c, a.lastVersions, nil)
|
||||
return
|
||||
}
|
||||
|
||||
versions, err := a.serverService.GetXrayVersions()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "getVersion"), err)
|
||||
return
|
||||
}
|
||||
|
||||
a.lastVersions = versions
|
||||
a.lastGetVersionsTime = time.Now()
|
||||
|
||||
jsonObj(c, versions, nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) installXray(c *gin.Context) {
|
||||
version := c.Param("version")
|
||||
err := a.serverService.UpdateXray(version)
|
||||
jsonMsg(c, I18nWeb(c, "pages.index.xraySwitchVersionPopover"), err)
|
||||
}
|
||||
|
||||
func (a *ServerController) updateGeofile(c *gin.Context) {
|
||||
fileName := c.Param("fileName")
|
||||
err := a.serverService.UpdateGeofile(fileName)
|
||||
jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), err)
|
||||
}
|
||||
|
||||
func (a *ServerController) stopXrayService(c *gin.Context) {
|
||||
a.lastGetStatusTime = time.Now()
|
||||
err := a.serverService.StopXrayService()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.xray.stopError"), err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, I18nWeb(c, "pages.xray.stopSuccess"), err)
|
||||
}
|
||||
|
||||
func (a *ServerController) restartXrayService(c *gin.Context) {
|
||||
err := a.serverService.RestartXrayService()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.xray.restartError"), err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, I18nWeb(c, "pages.xray.restartSuccess"), err)
|
||||
}
|
||||
|
||||
func (a *ServerController) getLogs(c *gin.Context) {
|
||||
count := c.Param("count")
|
||||
level := c.PostForm("level")
|
||||
syslog := c.PostForm("syslog")
|
||||
logs := a.serverService.GetLogs(count, level, syslog)
|
||||
jsonObj(c, logs, nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getXrayLogs(c *gin.Context) {
|
||||
count := c.Param("count")
|
||||
filter := c.PostForm("filter")
|
||||
showDirect := c.PostForm("showDirect")
|
||||
showBlocked := c.PostForm("showBlocked")
|
||||
showProxy := c.PostForm("showProxy")
|
||||
|
||||
var freedoms []string
|
||||
var blackholes []string
|
||||
|
||||
//getting tags for freedom and blackhole outbounds
|
||||
config, err := a.settingService.GetDefaultXrayConfig()
|
||||
if err == nil && config != nil {
|
||||
if cfgMap, ok := config.(map[string]interface{}); ok {
|
||||
if outbounds, ok := cfgMap["outbounds"].([]interface{}); ok {
|
||||
for _, outbound := range outbounds {
|
||||
if obMap, ok := outbound.(map[string]interface{}); ok {
|
||||
switch obMap["protocol"] {
|
||||
case "freedom":
|
||||
if tag, ok := obMap["tag"].(string); ok {
|
||||
freedoms = append(freedoms, tag)
|
||||
}
|
||||
case "blackhole":
|
||||
if tag, ok := obMap["tag"].(string); ok {
|
||||
blackholes = append(blackholes, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(freedoms) == 0 {
|
||||
freedoms = []string{"direct"}
|
||||
}
|
||||
if len(blackholes) == 0 {
|
||||
blackholes = []string{"blocked"}
|
||||
}
|
||||
|
||||
logs := a.serverService.GetXrayLogs(count, filter, showDirect, showBlocked, showProxy, freedoms, blackholes)
|
||||
jsonObj(c, logs, nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getConfigJson(c *gin.Context) {
|
||||
configJson, err := a.serverService.GetConfigJson()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.index.getConfigError"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, configJson, nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getDb(c *gin.Context) {
|
||||
db, err := a.serverService.GetDb()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.index.getDatabaseError"), err)
|
||||
return
|
||||
}
|
||||
|
||||
filename := "x-ui.db"
|
||||
|
||||
if !isValidFilename(filename) {
|
||||
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename"))
|
||||
return
|
||||
}
|
||||
|
||||
// Set the headers for the response
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Header("Content-Disposition", "attachment; filename="+filename)
|
||||
|
||||
// Write the file contents to the response
|
||||
c.Writer.Write(db)
|
||||
}
|
||||
|
||||
func isValidFilename(filename string) bool {
|
||||
// Validate that the filename only contains allowed characters
|
||||
return filenameRegex.MatchString(filename)
|
||||
}
|
||||
|
||||
func (a *ServerController) importDB(c *gin.Context) {
|
||||
// Get the file from the request body
|
||||
file, _, err := c.Request.FormFile("db")
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.index.readDatabaseError"), err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
// Always restart Xray before return
|
||||
defer a.serverService.RestartXrayService()
|
||||
defer func() {
|
||||
a.lastGetStatusTime = time.Now()
|
||||
}()
|
||||
// Import it
|
||||
err = a.serverService.ImportDB(file)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.index.importDatabaseError"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, I18nWeb(c, "pages.index.importDatabaseSuccess"), nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getNewX25519Cert(c *gin.Context) {
|
||||
cert, err := a.serverService.GetNewX25519Cert()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.getNewX25519CertError"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, cert, nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getNewmldsa65(c *gin.Context) {
|
||||
cert, err := a.serverService.GetNewmldsa65()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.getNewmldsa65Error"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, cert, nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getNewEchCert(c *gin.Context) {
|
||||
sni := c.PostForm("sni")
|
||||
cert, err := a.serverService.GetNewEchCert(sni)
|
||||
if err != nil {
|
||||
jsonMsg(c, "get ech certificate", err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, cert, nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getNewVlessEnc(c *gin.Context) {
|
||||
out, err := a.serverService.GetNewVlessEnc()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.getNewVlessEncError"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, out, nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getNewUUID(c *gin.Context) {
|
||||
uuidResp, err := a.serverService.GetNewUUID()
|
||||
if err != nil {
|
||||
jsonMsg(c, "Failed to generate UUID", err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonObj(c, uuidResp, nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getNewmlkem768(c *gin.Context) {
|
||||
out, err := a.serverService.GetNewmlkem768()
|
||||
if err != nil {
|
||||
jsonMsg(c, "Failed to generate mlkem768 keys", err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, out, nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) saveHistory(c *gin.Context) {
|
||||
/* 【中文注释】: 旧的错误代码,因为它期望一个 JSON 请求体,但前端发送的是表单数据
|
||||
var req struct {
|
||||
Type string `json:"type"`
|
||||
Link string `json:"link"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
jsonMsg(c, "Invalid request", err)
|
||||
return
|
||||
}
|
||||
err := a.serverService.SaveLinkHistory(req.Type, req.Link)
|
||||
*/
|
||||
|
||||
// 【中文注释】: 修改后的新代码,直接从 POST 表单中获取 'type' 和 'link' 参数
|
||||
// 【中文注释】: 这与其他 POST 方法(如 getLogs, getXrayLogs)的处理方式保持一致,解决了数据格式不匹配的问题。
|
||||
historyType := c.PostForm("type")
|
||||
link := c.PostForm("link")
|
||||
|
||||
// 【中文注释】: 调用服务层方法来保存历史记录
|
||||
err := a.serverService.SaveLinkHistory(historyType, link)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Failed to save history", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "History saved successfully", nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) loadHistory(c *gin.Context) {
|
||||
history, err := a.serverService.LoadLinkHistory()
|
||||
if err != nil {
|
||||
jsonMsg(c, "Failed to load history", err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, history, nil)
|
||||
}
|
||||
|
||||
|
||||
// 〔新增接口〕: 安装 Subconverter
|
||||
// 〔中文注释〕: 这个函数是暴露给前端的 API 接口,用于处理安装 Subconverter 的请求。
|
||||
func (a *ServerController) installSubconverter(c *gin.Context) {
|
||||
// 〔中文注释〕: 调用服务层中我们刚刚创建的 InstallSubconverter 方法。
|
||||
err := a.serverService.InstallSubconverter()
|
||||
if err != nil {
|
||||
// 〔中文注释〕: 如果 service 层返回了错误,则向前台返回失败的 JSON 消息。
|
||||
jsonMsg(c, "Subconverter 安装指令执行失败", err)
|
||||
return
|
||||
}
|
||||
// 〔中文注释〕: 如果没有错误,则向前台返回成功的 JSON 消息。
|
||||
jsonMsg(c, "Subconverter 安装指令已成功发送", nil)
|
||||
}
|
||||
|
||||
// 【新增接口实现】: 前端放行端口
|
||||
func (a *ServerController) openPort(c *gin.Context) {
|
||||
|
||||
// 直接使用 c.PostForm("port") 获取表单数据
|
||||
port := c.PostForm("port")
|
||||
|
||||
// 1. 手动进行参数校验
|
||||
if port == "" {
|
||||
jsonMsg(c, "请求端口参数失败", fmt.Errorf("无效的请求参数,请确保端口号存在"))
|
||||
return
|
||||
}
|
||||
|
||||
// 【中文注释】: 2. 调用服务层方法,该方法会立即返回,并在后台启动一个协程执行任务。
|
||||
a.serverService.OpenPort(port)
|
||||
|
||||
// 【中文注释】: 3. 因为服务层方法是异步的,不再检查它的 error 返回值。
|
||||
// 直接向前端返回一个成功的消息,告知用户指令已发送。
|
||||
jsonMsg(c, "端口放行指令已成功发送,正在后台执行...", nil)
|
||||
}
|
||||
111
web/controller/setting.go
Normal file
111
web/controller/setting.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"x-ui/util/crypto"
|
||||
"x-ui/web/entity"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type updateUserForm struct {
|
||||
OldUsername string `json:"oldUsername" form:"oldUsername"`
|
||||
OldPassword string `json:"oldPassword" form:"oldPassword"`
|
||||
NewUsername string `json:"newUsername" form:"newUsername"`
|
||||
NewPassword string `json:"newPassword" form:"newPassword"`
|
||||
}
|
||||
|
||||
type SettingController struct {
|
||||
settingService service.SettingService
|
||||
userService service.UserService
|
||||
panelService service.PanelService
|
||||
}
|
||||
|
||||
func NewSettingController(g *gin.RouterGroup) *SettingController {
|
||||
a := &SettingController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/setting")
|
||||
|
||||
g.POST("/all", a.getAllSetting)
|
||||
g.POST("/defaultSettings", a.getDefaultSettings)
|
||||
g.POST("/update", a.updateSetting)
|
||||
g.POST("/updateUser", a.updateUser)
|
||||
g.POST("/restartPanel", a.restartPanel)
|
||||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||
}
|
||||
|
||||
func (a *SettingController) getAllSetting(c *gin.Context) {
|
||||
allSetting, err := a.settingService.GetAllSetting()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, allSetting, nil)
|
||||
}
|
||||
|
||||
func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
||||
result, err := a.settingService.GetDefaultSettings(c.Request.Host)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, result, nil)
|
||||
}
|
||||
|
||||
func (a *SettingController) updateSetting(c *gin.Context) {
|
||||
allSetting := &entity.AllSetting{}
|
||||
err := c.ShouldBind(allSetting)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
return
|
||||
}
|
||||
err = a.settingService.UpdateAllSetting(allSetting)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
}
|
||||
|
||||
func (a *SettingController) updateUser(c *gin.Context) {
|
||||
form := &updateUserForm{}
|
||||
err := c.ShouldBind(form)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
return
|
||||
}
|
||||
user := session.GetLoginUser(c)
|
||||
if user.Username != form.OldUsername || !crypto.CheckPasswordHash(user.Password, form.OldPassword) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUserError"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect")))
|
||||
return
|
||||
}
|
||||
if form.NewUsername == "" || form.NewPassword == "" {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUserError"), errors.New(I18nWeb(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
|
||||
return
|
||||
}
|
||||
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
|
||||
if err == nil {
|
||||
user.Username = form.NewUsername
|
||||
user.Password, _ = crypto.HashPasswordAsBcrypt(form.NewPassword)
|
||||
session.SetLoginUser(c, user)
|
||||
}
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
|
||||
}
|
||||
|
||||
func (a *SettingController) restartPanel(c *gin.Context) {
|
||||
err := a.panelService.RestartPanel(time.Second * 3)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.restartPanelSuccess"), err)
|
||||
}
|
||||
|
||||
func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
|
||||
defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, defaultJsonConfig, nil)
|
||||
}
|
||||
96
web/controller/util.go
Normal file
96
web/controller/util.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/entity"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func getRemoteIp(c *gin.Context) string {
|
||||
value := c.GetHeader("X-Real-IP")
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
value = c.GetHeader("X-Forwarded-For")
|
||||
if value != "" {
|
||||
ips := strings.Split(value, ",")
|
||||
return ips[0]
|
||||
}
|
||||
addr := c.Request.RemoteAddr
|
||||
ip, _, _ := net.SplitHostPort(addr)
|
||||
return ip
|
||||
}
|
||||
|
||||
func jsonMsg(c *gin.Context, msg string, err error) {
|
||||
jsonMsgObj(c, msg, nil, err)
|
||||
}
|
||||
|
||||
func jsonObj(c *gin.Context, obj any, err error) {
|
||||
jsonMsgObj(c, "", obj, err)
|
||||
}
|
||||
|
||||
func jsonMsgObj(c *gin.Context, msg string, obj any, err error) {
|
||||
m := entity.Msg{
|
||||
Obj: obj,
|
||||
}
|
||||
if err == nil {
|
||||
m.Success = true
|
||||
if msg != "" {
|
||||
m.Msg = msg
|
||||
}
|
||||
} else {
|
||||
m.Success = false
|
||||
m.Msg = msg + " (" + err.Error() + ")"
|
||||
logger.Warning(msg+" "+I18nWeb(c, "fail")+": ", err)
|
||||
}
|
||||
c.JSON(http.StatusOK, m)
|
||||
}
|
||||
|
||||
func pureJsonMsg(c *gin.Context, statusCode int, success bool, msg string) {
|
||||
c.JSON(statusCode, entity.Msg{
|
||||
Success: success,
|
||||
Msg: msg,
|
||||
})
|
||||
}
|
||||
|
||||
func html(c *gin.Context, name string, title string, data gin.H) {
|
||||
if data == nil {
|
||||
data = gin.H{}
|
||||
}
|
||||
data["title"] = title
|
||||
host := c.GetHeader("X-Forwarded-Host")
|
||||
if host == "" {
|
||||
host = c.GetHeader("X-Real-IP")
|
||||
}
|
||||
if host == "" {
|
||||
var err error
|
||||
host, _, err = net.SplitHostPort(c.Request.Host)
|
||||
if err != nil {
|
||||
host = c.Request.Host
|
||||
}
|
||||
}
|
||||
data["host"] = host
|
||||
data["request_uri"] = c.Request.RequestURI
|
||||
data["base_path"] = c.GetString("base_path")
|
||||
c.HTML(http.StatusOK, name, getContext(data))
|
||||
}
|
||||
|
||||
func getContext(h gin.H) gin.H {
|
||||
a := gin.H{
|
||||
"cur_ver": config.GetVersion(),
|
||||
}
|
||||
for key, value := range h {
|
||||
a[key] = value
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func isAjax(c *gin.Context) bool {
|
||||
return c.GetHeader("X-Requested-With") == "XMLHttpRequest"
|
||||
}
|
||||
110
web/controller/xray_setting.go
Normal file
110
web/controller/xray_setting.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type XraySettingController struct {
|
||||
XraySettingService service.XraySettingService
|
||||
SettingService service.SettingService
|
||||
InboundService service.InboundService
|
||||
OutboundService service.OutboundService
|
||||
XrayService service.XrayService
|
||||
WarpService service.WarpService
|
||||
}
|
||||
|
||||
func NewXraySettingController(g *gin.RouterGroup) *XraySettingController {
|
||||
a := &XraySettingController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/xray")
|
||||
|
||||
g.POST("/", a.getXraySetting)
|
||||
g.POST("/update", a.updateSetting)
|
||||
g.GET("/getXrayResult", a.getXrayResult)
|
||||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||
g.POST("/warp/:action", a.warp)
|
||||
g.GET("/getOutboundsTraffic", a.getOutboundsTraffic)
|
||||
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
||||
}
|
||||
|
||||
func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
||||
xraySetting, err := a.SettingService.GetXrayConfigTemplate()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
inboundTags, err := a.InboundService.GetInboundTags()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
xrayResponse := "{ \"xraySetting\": " + xraySetting + ", \"inboundTags\": " + inboundTags + " }"
|
||||
jsonObj(c, xrayResponse, nil)
|
||||
}
|
||||
|
||||
func (a *XraySettingController) updateSetting(c *gin.Context) {
|
||||
xraySetting := c.PostForm("xraySetting")
|
||||
err := a.XraySettingService.SaveXraySetting(xraySetting)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
}
|
||||
|
||||
func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
|
||||
defaultJsonConfig, err := a.SettingService.GetDefaultXrayConfig()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, defaultJsonConfig, nil)
|
||||
}
|
||||
|
||||
func (a *XraySettingController) getXrayResult(c *gin.Context) {
|
||||
jsonObj(c, a.XrayService.GetXrayResult(), nil)
|
||||
}
|
||||
|
||||
func (a *XraySettingController) warp(c *gin.Context) {
|
||||
action := c.Param("action")
|
||||
var resp string
|
||||
var err error
|
||||
switch action {
|
||||
case "data":
|
||||
resp, err = a.WarpService.GetWarpData()
|
||||
case "del":
|
||||
err = a.WarpService.DelWarpData()
|
||||
case "config":
|
||||
resp, err = a.WarpService.GetWarpConfig()
|
||||
case "reg":
|
||||
skey := c.PostForm("privateKey")
|
||||
pkey := c.PostForm("publicKey")
|
||||
resp, err = a.WarpService.RegWarp(skey, pkey)
|
||||
case "license":
|
||||
license := c.PostForm("license")
|
||||
resp, err = a.WarpService.SetWarpLicense(license)
|
||||
}
|
||||
|
||||
jsonObj(c, resp, err)
|
||||
}
|
||||
|
||||
func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
|
||||
outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getOutboundTrafficError"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, outboundsTraffic, nil)
|
||||
}
|
||||
|
||||
func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {
|
||||
tag := c.PostForm("tag")
|
||||
err := a.OutboundService.ResetOutboundTraffic(tag)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.resetOutboundTrafficError"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, "", nil)
|
||||
}
|
||||
67
web/controller/xui.go
Normal file
67
web/controller/xui.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type XUIController struct {
|
||||
BaseController
|
||||
|
||||
inboundController *InboundController
|
||||
serverController *ServerController
|
||||
settingController *SettingController
|
||||
xraySettingController *XraySettingController
|
||||
serverService service.ServerService
|
||||
}
|
||||
|
||||
func NewXUIController(g *gin.RouterGroup) *XUIController {
|
||||
a := &XUIController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/panel")
|
||||
g.Use(a.checkLogin)
|
||||
|
||||
g.GET("/", a.index)
|
||||
g.GET("/inbounds", a.inbounds)
|
||||
g.GET("/settings", a.settings)
|
||||
g.GET("/xray", a.xraySettings)
|
||||
g.GET("/navigation", a.navigation)
|
||||
|
||||
// 【新增 2】注册页面路由
|
||||
g.GET("/servers", a.serversPage)
|
||||
|
||||
a.inboundController = NewInboundController(g)
|
||||
a.serverController = NewServerController(g, a.serverService)
|
||||
a.settingController = NewSettingController(g)
|
||||
a.xraySettingController = NewXraySettingController(g)
|
||||
}
|
||||
|
||||
func (a *XUIController) index(c *gin.Context) {
|
||||
html(c, "index.html", "pages.index.title", nil)
|
||||
}
|
||||
|
||||
func (a *XUIController) inbounds(c *gin.Context) {
|
||||
html(c, "inbounds.html", "pages.inbounds.title", nil)
|
||||
}
|
||||
|
||||
func (a *XUIController) settings(c *gin.Context) {
|
||||
html(c, "settings.html", "pages.settings.title", nil)
|
||||
}
|
||||
|
||||
func (a *XUIController) xraySettings(c *gin.Context) {
|
||||
html(c, "xray.html", "pages.xray.title", nil)
|
||||
}
|
||||
|
||||
func (a *XUIController) navigation(c *gin.Context) {
|
||||
html(c, "navigation.html", "pages.navigation.title", nil)
|
||||
}
|
||||
|
||||
// 【新增 4】添加页面渲染方法
|
||||
func (a *XUIController) serversPage(c *gin.Context) {
|
||||
html(c, "servers.html", "pages.controlledmanagement.title", nil)
|
||||
}
|
||||
134
web/entity/entity.go
Normal file
134
web/entity/entity.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"math"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"x-ui/util/common"
|
||||
)
|
||||
|
||||
type Msg struct {
|
||||
Success bool `json:"success"`
|
||||
Msg string `json:"msg"`
|
||||
Obj any `json:"obj"`
|
||||
}
|
||||
|
||||
type AllSetting struct {
|
||||
WebListen string `json:"webListen" form:"webListen"`
|
||||
WebDomain string `json:"webDomain" form:"webDomain"`
|
||||
WebPort int `json:"webPort" form:"webPort"`
|
||||
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
||||
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
||||
WebBasePath string `json:"webBasePath" form:"webBasePath"`
|
||||
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
|
||||
PageSize int `json:"pageSize" form:"pageSize"`
|
||||
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
|
||||
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
|
||||
RemarkModel string `json:"remarkModel" form:"remarkModel"`
|
||||
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
|
||||
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
||||
TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"`
|
||||
TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"`
|
||||
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
|
||||
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
||||
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
|
||||
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"`
|
||||
TgCpu int `json:"tgCpu" form:"tgCpu"`
|
||||
TgLang string `json:"tgLang" form:"tgLang"`
|
||||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
||||
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
|
||||
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
|
||||
SubEnable bool `json:"subEnable" form:"subEnable"`
|
||||
SubTitle string `json:"subTitle" form:"subTitle"`
|
||||
SubListen string `json:"subListen" form:"subListen"`
|
||||
SubPort int `json:"subPort" form:"subPort"`
|
||||
SubPath string `json:"subPath" form:"subPath"`
|
||||
SubDomain string `json:"subDomain" form:"subDomain"`
|
||||
SubCertFile string `json:"subCertFile" form:"subCertFile"`
|
||||
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
|
||||
SubUpdates int `json:"subUpdates" form:"subUpdates"`
|
||||
ExternalTrafficInformEnable bool `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"`
|
||||
ExternalTrafficInformURI string `json:"externalTrafficInformURI" form:"externalTrafficInformURI"`
|
||||
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
|
||||
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
|
||||
SubURI string `json:"subURI" form:"subURI"`
|
||||
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
|
||||
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
|
||||
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"`
|
||||
SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"`
|
||||
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
|
||||
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
|
||||
Datepicker string `json:"datepicker" form:"datepicker"`
|
||||
}
|
||||
|
||||
func (s *AllSetting) CheckValid() error {
|
||||
if s.WebListen != "" {
|
||||
ip := net.ParseIP(s.WebListen)
|
||||
if ip == nil {
|
||||
return common.NewError("web listen is not valid ip:", s.WebListen)
|
||||
}
|
||||
}
|
||||
|
||||
if s.SubListen != "" {
|
||||
ip := net.ParseIP(s.SubListen)
|
||||
if ip == nil {
|
||||
return common.NewError("Sub listen is not valid ip:", s.SubListen)
|
||||
}
|
||||
}
|
||||
|
||||
if s.WebPort <= 0 || s.WebPort > math.MaxUint16 {
|
||||
return common.NewError("web port is not a valid port:", s.WebPort)
|
||||
}
|
||||
|
||||
if s.SubPort <= 0 || s.SubPort > math.MaxUint16 {
|
||||
return common.NewError("Sub port is not a valid port:", s.SubPort)
|
||||
}
|
||||
|
||||
if (s.SubPort == s.WebPort) && (s.WebListen == s.SubListen) {
|
||||
return common.NewError("Sub and Web could not use same ip:port, ", s.SubListen, ":", s.SubPort, " & ", s.WebListen, ":", s.WebPort)
|
||||
}
|
||||
|
||||
if s.WebCertFile != "" || s.WebKeyFile != "" {
|
||||
_, err := tls.LoadX509KeyPair(s.WebCertFile, s.WebKeyFile)
|
||||
if err != nil {
|
||||
return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.WebCertFile, s.WebKeyFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.SubCertFile != "" || s.SubKeyFile != "" {
|
||||
_, err := tls.LoadX509KeyPair(s.SubCertFile, s.SubKeyFile)
|
||||
if err != nil {
|
||||
return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.SubCertFile, s.SubKeyFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(s.WebBasePath, "/") {
|
||||
s.WebBasePath = "/" + s.WebBasePath
|
||||
}
|
||||
if !strings.HasSuffix(s.WebBasePath, "/") {
|
||||
s.WebBasePath += "/"
|
||||
}
|
||||
if !strings.HasPrefix(s.SubPath, "/") {
|
||||
s.SubPath = "/" + s.SubPath
|
||||
}
|
||||
if !strings.HasSuffix(s.SubPath, "/") {
|
||||
s.SubPath += "/"
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(s.SubJsonPath, "/") {
|
||||
s.SubJsonPath = "/" + s.SubJsonPath
|
||||
}
|
||||
if !strings.HasSuffix(s.SubJsonPath, "/") {
|
||||
s.SubJsonPath += "/"
|
||||
}
|
||||
|
||||
_, err := time.LoadLocation(s.TimeLocation)
|
||||
if err != nil {
|
||||
return common.NewError("time location not exist:", s.TimeLocation)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
38
web/global/global.go
Normal file
38
web/global/global.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
webServer WebServer
|
||||
subServer SubServer
|
||||
)
|
||||
|
||||
type WebServer interface {
|
||||
GetCron() *cron.Cron
|
||||
GetCtx() context.Context
|
||||
}
|
||||
|
||||
type SubServer interface {
|
||||
GetCtx() context.Context
|
||||
}
|
||||
|
||||
func SetWebServer(s WebServer) {
|
||||
webServer = s
|
||||
}
|
||||
|
||||
func GetWebServer() WebServer {
|
||||
return webServer
|
||||
}
|
||||
|
||||
func SetSubServer(s SubServer) {
|
||||
subServer = s
|
||||
}
|
||||
|
||||
func GetSubServer() SubServer {
|
||||
return subServer
|
||||
}
|
||||
80
web/global/hashStorage.go
Normal file
80
web/global/hashStorage.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type HashEntry struct {
|
||||
Hash string
|
||||
Value string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
type HashStorage struct {
|
||||
sync.RWMutex
|
||||
Data map[string]HashEntry
|
||||
Expiration time.Duration
|
||||
}
|
||||
|
||||
func NewHashStorage(expiration time.Duration) *HashStorage {
|
||||
return &HashStorage{
|
||||
Data: make(map[string]HashEntry),
|
||||
Expiration: expiration,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HashStorage) SaveHash(query string) string {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
md5Hash := md5.Sum([]byte(query))
|
||||
md5HashString := hex.EncodeToString(md5Hash[:])
|
||||
|
||||
entry := HashEntry{
|
||||
Hash: md5HashString,
|
||||
Value: query,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
h.Data[md5HashString] = entry
|
||||
|
||||
return md5HashString
|
||||
}
|
||||
|
||||
func (h *HashStorage) GetValue(hash string) (string, bool) {
|
||||
h.RLock()
|
||||
defer h.RUnlock()
|
||||
|
||||
entry, exists := h.Data[hash]
|
||||
|
||||
return entry.Value, exists
|
||||
}
|
||||
|
||||
func (h *HashStorage) IsMD5(hash string) bool {
|
||||
match, _ := regexp.MatchString("^[a-f0-9]{32}$", hash)
|
||||
return match
|
||||
}
|
||||
|
||||
func (h *HashStorage) RemoveExpiredHashes() {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
for hash, entry := range h.Data {
|
||||
if now.Sub(entry.Timestamp) > h.Expiration {
|
||||
delete(h.Data, hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HashStorage) Reset() {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
h.Data = make(map[string]HashEntry)
|
||||
}
|
||||
58
web/html/common/page.html
Normal file
58
web/html/common/page.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{{ define "page/head_start" }}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue/antd.min.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.min.css?{{ .cur_ver }}">
|
||||
<style>
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
/* vazirmatn-regular - arabic_latin_latin-ext */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Vazirmatn';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('{{ .base_path }}assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2');
|
||||
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039;
|
||||
}
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
</style>
|
||||
<title>{{ .host }} – {{ i18n .title}}</title>
|
||||
{{ end }}
|
||||
|
||||
{{ define "page/head_end" }}
|
||||
</head>
|
||||
{{ end }}
|
||||
|
||||
{{ define "page/body_start" }}
|
||||
<body>
|
||||
<div id="message"></div>
|
||||
{{ end }}
|
||||
|
||||
{{ define "page/body_scripts" }}
|
||||
<script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/moment/moment.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/axios/axios.min.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/qs/qs.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
|
||||
<script>
|
||||
const basePath = '{{ .base_path }}';
|
||||
axios.defaults.baseURL = basePath;
|
||||
</script>
|
||||
{{ end }}
|
||||
|
||||
{{ define "page/body_end" }}
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
316
web/html/component/aClientTable.html
Normal file
316
web/html/component/aClientTable.html
Normal file
@@ -0,0 +1,316 @@
|
||||
{{define "component/aClientTable"}}
|
||||
<template slot="actions" slot-scope="text, client, index">
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "qrCode" }}</template>
|
||||
<a-icon :style="{ fontSize: '24px' }" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.client.edit" }}</template>
|
||||
<a-icon :style="{ fontSize: '24px' }" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "info" }}</template>
|
||||
<a-icon :style="{ fontSize: '24px' }" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
||||
<a-popconfirm @confirm="resetClientTraffic(client,record.id,false)" title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o" :style="{ color: 'var(--color-primary-100)'}"></a-icon>
|
||||
<a-icon :style="{ fontSize: '24px', cursor: 'pointer' }" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span :style="{ color: '#FF4D4F' }"> {{ i18n "delete"}}</span>
|
||||
</template>
|
||||
<a-popconfirm @confirm="delClient(record.id,client,false)" title='{{ i18n "pages.inbounds.deleteClientContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "delete"}}' ok-type="danger" cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o" :style="{ color: '#e04141' }"></a-icon>
|
||||
<a-icon :style="{ fontSize: '24px', cursor: 'pointer' }" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template slot="enable" slot-scope="text, client, index">
|
||||
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
|
||||
</template>
|
||||
<template slot="online" slot-scope="text, client, index">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content" >
|
||||
{{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
|
||||
</template>
|
||||
<template v-if="client.enable && isClientOnline(client.email)">
|
||||
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>{{ i18n "offline" }}</a-tag>
|
||||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="client" slot-scope="text, client">
|
||||
<a-space direction="horizontal" :size="2">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template>
|
||||
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
|
||||
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
|
||||
</template>
|
||||
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge>
|
||||
</a-tooltip>
|
||||
<a-space direction="vertical" :size="2">
|
||||
<span class="client-email">[[ client.email ]]</span>
|
||||
<template v-if="client.comment && client.comment.trim()">
|
||||
<a-tooltip v-if="client.comment.length > 50" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="title">
|
||||
[[ client.comment ]]
|
||||
</template>
|
||||
<span class="client-comment">[[ client.comment.substring(0, 47) + '...' ]]</span>
|
||||
</a-tooltip>
|
||||
<span v-else class="client-comment">[[ client.comment ]]</span>
|
||||
</template>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</template>
|
||||
<template slot="traffic" slot-scope="text, client">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content" v-if="client.email">
|
||||
<table cellpadding="2" width="100%">
|
||||
<tr>
|
||||
<td>↑[[ SizeFormatter.sizeFormat(getUpStats(record, client.email)) ]]</td>
|
||||
<td>↓[[ SizeFormatter.sizeFormat(getDownStats(record, client.email)) ]]</td>
|
||||
</tr>
|
||||
<tr v-if="client.totalGB > 0">
|
||||
<td>{{ i18n "remained" }}</td>
|
||||
<td>[[ SizeFormatter.sizeFormat(getRemStats(record, client.email)) ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<table>
|
||||
<tr class="tr-table-box">
|
||||
<td class="tr-table-rt"> [[ SizeFormatter.sizeFormat(getSumStats(record, client.email)) ]] </td>
|
||||
<td class="tr-table-bar" v-if="!client.enable">
|
||||
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
|
||||
</td>
|
||||
<td class="tr-table-bar" v-else-if="client.totalGB > 0">
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
||||
</td>
|
||||
<td v-else class="infinite-bar tr-table-bar">
|
||||
<a-progress :show-info="false" :percent="100"></a-progress>
|
||||
</td>
|
||||
<td class="tr-table-lt">
|
||||
<template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
|
||||
<span v-else class="tr-infinity-ch">∞</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<template slot="allTime" slot-scope="text, client">
|
||||
<a-tag>[[ SizeFormatter.sizeFormat(getAllTimeClient(record, client.email)) ]]</a-tag>
|
||||
</template>
|
||||
<template slot="expiryTime" slot-scope="text, client, index">
|
||||
<template v-if="client.expiryTime !=0 && client.reset >0">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<table>
|
||||
<tr class="tr-table-box">
|
||||
<td class="tr-table-rt"> [[ remainedDays(client.expiryTime) ]] </td>
|
||||
<td class="infinite-bar tr-table-bar">
|
||||
<a-progress :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||
</td>
|
||||
<td class="tr-table-lt">[[ client.reset + "d" ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ remainedDays(client.expiryTime) ]] </a-tag>
|
||||
</a-popover>
|
||||
<a-tag v-else :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" :style="{ border: 'none' }" class="infinite-tag">
|
||||
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
|
||||
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template slot="actionMenu" slot-scope="text, client, index">
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-icon @click="e => e.preventDefault()" type="ellipsis" :style="{ fontSize: '20px' }"></a-icon>
|
||||
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item v-if="record.hasLink()" @click="showQrcode(record.id,client);">
|
||||
<a-icon :style="{ fontSize: '14px' }" type="qrcode"></a-icon>
|
||||
{{ i18n "qrCode" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="openEditClient(record.id,client);">
|
||||
<a-icon :style="{ fontSize: '14px' }" type="edit"></a-icon>
|
||||
{{ i18n "pages.client.edit" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="showInfo(record.id,client);">
|
||||
<a-icon :style="{ fontSize: '14px' }" type="info-circle"></a-icon>
|
||||
{{ i18n "info" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0">
|
||||
<a-icon :style="{ fontSize: '14px' }" type="retweet"></a-icon>
|
||||
{{ i18n "pages.inbounds.resetTraffic" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="isRemovable(record.id)" @click="delClient(record.id,client)">
|
||||
<a-icon :style="{ fontSize: '14px' }" type="delete"></a-icon>
|
||||
<span :style="{ color: '#FF4D4F' }"> {{ i18n "delete"}}</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item>
|
||||
<a-switch v-model="client.enable" size="small" @change="switchEnableClient(record.id,client)"></a-switch>
|
||||
{{ i18n "enable"}}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<template slot="info" slot-scope="text, client, index">
|
||||
<a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
|
||||
<template slot="content">
|
||||
<table>
|
||||
<tr>
|
||||
<td colspan="3" :style="{ textAlign: 'center' }">{{ i18n "pages.inbounds.traffic" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[ SizeFormatter.sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] </td>
|
||||
<td width="120px" v-if="!client.enable">
|
||||
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
|
||||
</td>
|
||||
<td width="120px" v-else-if="client.totalGB > 0">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content" v-if="client.email">
|
||||
<table cellpadding="2" width="100%">
|
||||
<tr>
|
||||
<td>↑[[ SizeFormatter.sizeFormat(getUpStats(record, client.email)) ]]</td>
|
||||
<td>↓[[ SizeFormatter.sizeFormat(getDownStats(record, client.email)) ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "remained" }}</td>
|
||||
<td>[[ SizeFormatter.sizeFormat(getRemStats(record, client.email)) ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
||||
</a-popover>
|
||||
</td>
|
||||
<td width="120px" v-else class="infinite-bar">
|
||||
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? '#2c1e32':'#F2EAF1'" :show-info="false" :percent="100"></a-progress>
|
||||
</td>
|
||||
<td width="80px">
|
||||
<template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
|
||||
<span v-else class="tr-infinity-ch">∞</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3" :style="{ textAlign: 'center' }">
|
||||
<a-divider :style="{ margin: '0', borderCollapse: 'separate' }"></a-divider>
|
||||
{{ i18n "pages.inbounds.expireDate" }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<template v-if="client.expiryTime !=0 && client.reset >0">
|
||||
<td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[ remainedDays(client.expiryTime) ]] </td>
|
||||
<td width="120px" class="infinite-bar">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<a-progress :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||
</a-popover>
|
||||
</td>
|
||||
<td width="60px">[[ client.reset + "d" ]]</td>
|
||||
</template>
|
||||
<template v-else>
|
||||
<td colspan="3" :style="{ textAlign: 'center' }">
|
||||
<a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ remainedDays(client.expiryTime) ]] </a-tag>
|
||||
</a-popover>
|
||||
<a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'" class="infinite-tag">
|
||||
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
|
||||
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</a-tag>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-badge>
|
||||
<a-icon v-if="!client.enable" slot="count" type="pause-circle" theme="filled" :style="{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }"></a-icon>
|
||||
<a-button shape="round" size="small" :style="{ fontSize: '14px', padding: '0 10px' }">
|
||||
<a-icon type="solution"></a-icon>
|
||||
</a-button>
|
||||
</a-badge>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="createdAt" slot-scope="text, client, index">
|
||||
<template v-if="client.created_at">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client.created_at) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client.created_at)) ]]
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
</template>
|
||||
</template>
|
||||
<template slot="updatedAt" slot-scope="text, client, index">
|
||||
<template v-if="client.updated_at">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client.updated_at) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client.updated_at)) ]]
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
</template>
|
||||
</template>
|
||||
{{end}}
|
||||
42
web/html/component/aCustomStatistic.html
Normal file
42
web/html/component/aCustomStatistic.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{{define "component/customStatistic"}}
|
||||
<template>
|
||||
<a-statistic :title="title" :value="value">
|
||||
<template #prefix>
|
||||
<slot name="prefix"></slot>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<slot name="suffix"></slot>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
{{define "component/aCustomStatistic"}}
|
||||
<style>
|
||||
.dark .ant-statistic-content {
|
||||
color: var(--dark-color-text-primary)
|
||||
}
|
||||
.dark .ant-statistic-title {
|
||||
color: rgba(255, 255, 255, 0.55)
|
||||
}
|
||||
.ant-statistic-content {
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
Vue.component('a-custom-statistic', {
|
||||
props: {
|
||||
'title': {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
'value': {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
template: `{{template "component/customStatistic"}}`,
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
72
web/html/component/aPersianDatepicker.html
Normal file
72
web/html/component/aPersianDatepicker.html
Normal file
@@ -0,0 +1,72 @@
|
||||
{{define "component/persianDatepickerTemplate"}}
|
||||
<template>
|
||||
<div>
|
||||
<a-input :value="value" type="text" v-model="date" data-jdp class="persian-datepicker"
|
||||
@input="$emit('input', convertToGregorian($event.target.value)); jalaliDatepicker.hide();"
|
||||
:placeholder="placeholder">
|
||||
<template #addonAfter>
|
||||
<a-icon type="calendar" :style="{ fontSize: '14px', opacity: '0.5' }" />
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
{{define "component/aPersianDatepicker"}}
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/persian-datepicker/persian-datepicker.min.css?{{ .cur_ver }}" />
|
||||
<script src="{{ .base_path }}assets/moment/moment-jalali.min.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/persian-datepicker/persian-datepicker.min.js?{{ .cur_ver }}"></script>
|
||||
<script>
|
||||
const persianDatepicker = {};
|
||||
|
||||
Vue.component('a-persian-datepicker', {
|
||||
props: {
|
||||
'format': {
|
||||
type: undefined,
|
||||
required: false,
|
||||
},
|
||||
'value': {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
'placeholder': {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
template: `{{template "component/persianDatepickerTemplate"}}`,
|
||||
data() {
|
||||
return {
|
||||
date: '',
|
||||
persianDatepicker,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value: function (date) {
|
||||
this.date = this.convertToJalalian(date)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.date = this.convertToJalalian(this.value)
|
||||
this.listenToDatepicker()
|
||||
},
|
||||
methods: {
|
||||
convertToGregorian(date) {
|
||||
return date ? moment(moment(date, 'jYYYY/jMM/jDD HH:mm:ss').format('YYYY-MM-DD HH:mm:ss')) : null
|
||||
},
|
||||
convertToJalalian(date) {
|
||||
return date && moment.isMoment(date) ? date.format('jYYYY/jMM/jDD HH:mm:ss') : null
|
||||
},
|
||||
listenToDatepicker() {
|
||||
jalaliDatepicker.startWatch({
|
||||
time: true,
|
||||
zIndex: '9999',
|
||||
hideAfterChange: true,
|
||||
useDropDownYears: false,
|
||||
changeMonthRotateYear: true,
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
49
web/html/component/aSettingListItem.html
Normal file
49
web/html/component/aSettingListItem.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{{define "component/settingListItem"}}
|
||||
<a-list-item :style="{ padding: padding }">
|
||||
<a-row :gutter="[8,16]">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<slot name="title"></slot>
|
||||
</template>
|
||||
<template #description>
|
||||
<slot name="description"></slot>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<slot name="control"></slot>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
{{end}}
|
||||
|
||||
{{define "component/aSettingListItem"}}
|
||||
<script>
|
||||
Vue.component('a-setting-list-item', {
|
||||
props: {
|
||||
'paddings': {
|
||||
type: String,
|
||||
required: false,
|
||||
defaultValue: "default",
|
||||
validator: function (value) {
|
||||
return ['small', 'default'].includes(value)
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `{{ template "component/settingListItem" }}`,
|
||||
computed: {
|
||||
padding() {
|
||||
switch (this.paddings) {
|
||||
case "small":
|
||||
return "10px 20px !important"
|
||||
break;
|
||||
case "default":
|
||||
return "20px !important"
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{{end}}
|
||||
218
web/html/component/aSidebar.html
Normal file
218
web/html/component/aSidebar.html
Normal file
@@ -0,0 +1,218 @@
|
||||
{{define "component/sidebar/content"}}
|
||||
<template>
|
||||
<div class="ant-sidebar">
|
||||
<a-layout-sider :theme="themeSwitcher.currentTheme" collapsible :collapsed="collapsed"
|
||||
@collapse="(isCollapsed, type) => collapseHandle(isCollapsed, type)" breakpoint="md" width="200">
|
||||
|
||||
<div class="sider-flex-wrapper">
|
||||
|
||||
<div class="sider-top">
|
||||
<a-theme-switch></a-theme-switch>
|
||||
</div>
|
||||
|
||||
<div class="sider-menu-container">
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="activeTab"
|
||||
@click="({key}) => openLink(key)">
|
||||
<a-menu-item v-for="tab in tabs" :key="tab.key">
|
||||
<a-icon :type="tab.icon"></a-icon>
|
||||
<span v-text="tab.title"></span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-copyright-wrapper" v-show="!collapsed">
|
||||
<div class="copyright-content">
|
||||
<div class="brand-row">
|
||||
<a-icon type="safety-certificate" theme="filled" class="brand-icon"></a-icon>
|
||||
<span class="brand-name">X-Panel</span>
|
||||
</div>
|
||||
<div class="year-row">
|
||||
<span>Copyright <a-icon type="copyright"></a-icon> 2022-2026</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
|
||||
<a-drawer placement="left" :closable="false" @close="closeDrawer" :visible="visible"
|
||||
:wrap-class-name="themeSwitcher.currentTheme" :wrap-style="{ padding: 0 }" :style="{ height: '100%' }">
|
||||
<div class="drawer-handle" @click="toggleDrawer" slot="handle">
|
||||
<a-icon :type="visible ? 'close' : 'menu-fold'"></a-icon>
|
||||
</div>
|
||||
<a-theme-switch></a-theme-switch>
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="activeTab"
|
||||
@click="({key}) => openLink(key)">
|
||||
<a-menu-item v-for="tab in tabs" :key="tab.key">
|
||||
<a-icon :type="tab.icon"></a-icon>
|
||||
<span v-text="tab.title"></span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
{{define "component/aSidebar"}}
|
||||
<style>
|
||||
/* 1. 基础高度设置 */
|
||||
.ant-sidebar, .ant-sidebar > .ant-layout-sider {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* [布局容器] */
|
||||
.sider-flex-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column; /* 从上到下排列 */
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* [菜单容器] */
|
||||
.sider-menu-container {
|
||||
/* [修改说明]:删除了 flex: 1。菜单高度现在由内容决定,不会自动撑满 */
|
||||
/* 添加 flex-shrink: 1 只是为了防止屏幕特别矮时菜单被切断,让它能出滚动条 */
|
||||
flex-shrink: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sider-menu-container .ant-menu {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* 2. 版权区域 */
|
||||
.sidebar-copyright-wrapper {
|
||||
/* [核心修改]:margin-top: auto 是关键 */
|
||||
/* 它的作用是:自动占据上方所有的空白空间,从而把自己推到容器的最底部 */
|
||||
margin-top: auto;
|
||||
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
padding: 15px 0 20px 0;
|
||||
text-align: center;
|
||||
background: inherit;
|
||||
/* [已删除] border-top: ... (那条横线已经彻底删除了) */
|
||||
}
|
||||
|
||||
/* 3. 品牌行样式 */
|
||||
.brand-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
opacity: 0.85;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
margin-right: 6px;
|
||||
font-size: 15px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 4. 年份版权行样式 */
|
||||
.year-row {
|
||||
font-size: 11px;
|
||||
opacity: 0.45;
|
||||
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.year-row .anticon-copyright {
|
||||
font-size: 10px;
|
||||
margin: 0 3px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 滚动条样式美化 */
|
||||
.sider-menu-container::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.sider-menu-container::-webkit-scrollbar-thumb {
|
||||
background: rgba(128, 128, 128, 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.sider-menu-container::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const SIDEBAR_COLLAPSED_KEY = "isSidebarCollapsed"
|
||||
|
||||
Vue.component('a-sidebar', {
|
||||
data() {
|
||||
return {
|
||||
tabs: [
|
||||
{
|
||||
key: '{{ .base_path }}panel/',
|
||||
icon: 'dashboard',
|
||||
title: '{{ i18n "menu.dashboard"}}'
|
||||
},
|
||||
{
|
||||
key: '{{ .base_path }}panel/inbounds',
|
||||
icon: 'user',
|
||||
title: '{{ i18n "menu.inbounds"}}'
|
||||
},
|
||||
{
|
||||
key: '{{ .base_path }}panel/settings',
|
||||
icon: 'setting',
|
||||
title: '{{ i18n "menu.settings"}}'
|
||||
},
|
||||
{
|
||||
key: '{{ .base_path }}panel/xray',
|
||||
icon: 'tool',
|
||||
title: '{{ i18n "menu.xray"}}'
|
||||
},
|
||||
{
|
||||
key: '{{ .base_path }}panel/servers',
|
||||
icon: 'cloud-server',
|
||||
title: '{{ i18n "pages.controlledmanagement.title"}}'
|
||||
},
|
||||
{
|
||||
key: '{{ .base_path }}panel/navigation',
|
||||
icon: 'link',
|
||||
title: '{{ i18n "menu.navigation"}}'
|
||||
},
|
||||
{
|
||||
key: '{{ .base_path }}logout/',
|
||||
icon: 'logout',
|
||||
title: '{{ i18n "menu.logout"}}'
|
||||
},
|
||||
],
|
||||
activeTab: [
|
||||
'{{ .request_uri }}'
|
||||
],
|
||||
visible: false,
|
||||
collapsed: JSON.parse(localStorage.getItem(SIDEBAR_COLLAPSED_KEY)),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openLink(key) {
|
||||
return key.startsWith('http') ?
|
||||
window.open(key) :
|
||||
location.href = key
|
||||
},
|
||||
closeDrawer() {
|
||||
this.visible = false;
|
||||
},
|
||||
toggleDrawer() {
|
||||
this.visible = !this.visible;
|
||||
},
|
||||
collapseHandle(collapsed, type) {
|
||||
if (type === "clickTrigger") {
|
||||
localStorage.setItem(SIDEBAR_COLLAPSED_KEY, collapsed);
|
||||
|
||||
this.collapsed = JSON.parse(localStorage.getItem(SIDEBAR_COLLAPSED_KEY));
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `{{template "component/sidebar/content"}}`,
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
237
web/html/component/aTableSortable.html
Normal file
237
web/html/component/aTableSortable.html
Normal file
@@ -0,0 +1,237 @@
|
||||
{{define "component/sortableTableTrigger"}}
|
||||
<a-icon type="drag" class="sortable-icon" :style="{ cursor: 'move' }" @mouseup="mouseUpHandler" @mousedown="mouseDownHandler"
|
||||
@click="clickHandler" />
|
||||
{{end}}
|
||||
|
||||
{{define "component/aTableSortable"}}
|
||||
<script>
|
||||
const DRAGGABLE_ROW_CLASS = 'draggable-row';
|
||||
const findParentRowElement = (el) => {
|
||||
if (!el || !el.tagName) {
|
||||
return null;
|
||||
} else if (el.classList.contains(DRAGGABLE_ROW_CLASS)) {
|
||||
return el;
|
||||
} else if (el.parentNode) {
|
||||
return findParentRowElement(el.parentNode);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Vue.component('a-table-sortable', {
|
||||
data() {
|
||||
return {
|
||||
sortingElementIndex: null,
|
||||
newElementIndex: null,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
'data-source': {
|
||||
type: undefined,
|
||||
required: false,
|
||||
},
|
||||
'customRow': {
|
||||
type: undefined,
|
||||
required: false,
|
||||
}
|
||||
},
|
||||
inheritAttrs: false,
|
||||
provide() {
|
||||
const sortable = {}
|
||||
Object.defineProperty(sortable, "setSortableIndex", {
|
||||
enumerable: true,
|
||||
get: () => this.setCurrentSortableIndex,
|
||||
});
|
||||
Object.defineProperty(sortable, "resetSortableIndex", {
|
||||
enumerable: true,
|
||||
get: () => this.resetSortableIndex,
|
||||
});
|
||||
return {
|
||||
sortable,
|
||||
}
|
||||
},
|
||||
render: function (createElement) {
|
||||
return createElement('a-table', {
|
||||
class: {
|
||||
'ant-table-is-sorting': this.isDragging(),
|
||||
},
|
||||
props: {
|
||||
...this.$attrs,
|
||||
'data-source': this.records,
|
||||
customRow: (record, index) => this.customRowRender(record, index),
|
||||
},
|
||||
on: this.$listeners,
|
||||
nativeOn: {
|
||||
drop: (e) => this.dropHandler(e),
|
||||
},
|
||||
scopedSlots: this.$scopedSlots,
|
||||
locale: {
|
||||
filterConfirm: `{{ i18n "confirm" }}`,
|
||||
filterReset: `{{ i18n "reset" }}`,
|
||||
emptyText: `{{ i18n "noData" }}`
|
||||
}
|
||||
}, this.$slots.default,)
|
||||
},
|
||||
created() {
|
||||
this.$memoSort = {};
|
||||
},
|
||||
methods: {
|
||||
isDragging() {
|
||||
const currentIndex = this.sortingElementIndex;
|
||||
return currentIndex !== null && currentIndex !== undefined;
|
||||
},
|
||||
resetSortableIndex(e, index) {
|
||||
this.sortingElementIndex = null;
|
||||
this.newElementIndex = null;
|
||||
this.$memoSort = {};
|
||||
},
|
||||
setCurrentSortableIndex(e, index) {
|
||||
this.sortingElementIndex = index;
|
||||
},
|
||||
dragStartHandler(e, index) {
|
||||
if (!this.isDragging()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
const hideDragImage = this.$el.cloneNode(true);
|
||||
hideDragImage.id = "hideDragImage-hide";
|
||||
hideDragImage.style.opacity = 0;
|
||||
e.dataTransfer.setDragImage(hideDragImage, 0, 0);
|
||||
},
|
||||
dragStopHandler(e, index) {
|
||||
const hideDragImage = document.getElementById('hideDragImage-hide');
|
||||
if (hideDragImage) hideDragImage.remove();
|
||||
this.resetSortableIndex(e, index);
|
||||
},
|
||||
dragOverHandler(e, index) {
|
||||
if (!this.isDragging()) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
const currentIndex = this.sortingElementIndex;
|
||||
if (index === currentIndex) {
|
||||
this.newElementIndex = null;
|
||||
return;
|
||||
}
|
||||
const row = findParentRowElement(e.target);
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
const rect = row.getBoundingClientRect();
|
||||
const offsetTop = e.pageY - rect.top;
|
||||
if (offsetTop < rect.height / 2) {
|
||||
this.newElementIndex = Math.max(index - 1, 0);
|
||||
} else {
|
||||
this.newElementIndex = index;
|
||||
}
|
||||
},
|
||||
dropHandler(e) {
|
||||
if (this.isDragging()) {
|
||||
this.$emit('onsort', this.sortingElementIndex, this.newElementIndex);
|
||||
}
|
||||
},
|
||||
customRowRender(record, index) {
|
||||
const parentMethodResult = this.customRow?.(record, index) || {};
|
||||
const newIndex = this.newElementIndex;
|
||||
const currentIndex = this.sortingElementIndex;
|
||||
return {
|
||||
...parentMethodResult,
|
||||
attrs: {
|
||||
...(parentMethodResult?.attrs || {}),
|
||||
draggable: true,
|
||||
},
|
||||
on: {
|
||||
...(parentMethodResult?.on || {}),
|
||||
dragstart: (e) => this.dragStartHandler(e, index),
|
||||
dragend: (e) => this.dragStopHandler(e, index),
|
||||
dragover: (e) => this.dragOverHandler(e, index),
|
||||
},
|
||||
class: {
|
||||
...(parentMethodResult?.class || {}),
|
||||
[DRAGGABLE_ROW_CLASS]: true,
|
||||
['dragging']: this.isDragging() ? (newIndex === null ? index === currentIndex : index === newIndex) : false,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
records() {
|
||||
const newIndex = this.newElementIndex;
|
||||
const currentIndex = this.sortingElementIndex;
|
||||
if (!this.isDragging() || newIndex === null || currentIndex === newIndex) {
|
||||
return this.dataSource;
|
||||
}
|
||||
if (this.$memoSort.newIndex === newIndex) {
|
||||
return this.$memoSort.list;
|
||||
}
|
||||
let list = [...this.dataSource];
|
||||
list.splice(newIndex, 0, list.splice(currentIndex, 1)[0]);
|
||||
this.$memoSort = {
|
||||
newIndex,
|
||||
list,
|
||||
};
|
||||
return list;
|
||||
}
|
||||
}
|
||||
});
|
||||
Vue.component('a-table-sort-trigger', {
|
||||
template: `{{template "component/sortableTableTrigger"}}`,
|
||||
props: {
|
||||
'item-index': {
|
||||
type: undefined,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
inject: ['sortable'],
|
||||
methods: {
|
||||
mouseDownHandler(e) {
|
||||
if (this.sortable) {
|
||||
this.sortable.setSortableIndex(e, this.itemIndex);
|
||||
}
|
||||
},
|
||||
mouseUpHandler(e) {
|
||||
if (this.sortable) {
|
||||
this.sortable.resetSortableIndex(e, this.itemIndex);
|
||||
}
|
||||
},
|
||||
clickHandler(e) {
|
||||
e.preventDefault();
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
@media only screen and (max-width: 767px) {
|
||||
.sortable-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-is-sorting .draggable-row td {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
.dark .ant-table-is-sorting .draggable-row td {
|
||||
background-color: var(--dark-color-surface-100) !important;
|
||||
}
|
||||
|
||||
.ant-table-is-sorting .dragging td {
|
||||
background-color: rgb(232 244 242) !important;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dark .ant-table-is-sorting .dragging td {
|
||||
background-color: var(--dark-color-table-hover) !important;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.ant-table-is-sorting .dragging {
|
||||
opacity: 1;
|
||||
box-shadow: 1px -2px 2px #008771;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.ant-table-is-sorting .dragging .ant-table-row-index {
|
||||
opacity: 0.3;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
119
web/html/component/aThemeSwitch.html
Normal file
119
web/html/component/aThemeSwitch.html
Normal file
@@ -0,0 +1,119 @@
|
||||
{{define "component/themeSwitchTemplate"}}
|
||||
<template>
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
|
||||
<a-sub-menu>
|
||||
<span slot="title">
|
||||
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
|
||||
<span>{{ i18n "menu.theme" }}</span>
|
||||
</span>
|
||||
<a-menu-item id="change-theme" class="ant-menu-theme-switch" @mousedown="themeSwitcher.animationsOff()">
|
||||
<span>{{ i18n "menu.dark" }}</span>
|
||||
<a-switch :style="{ marginLeft: '2px' }" size="small" :default-checked="themeSwitcher.isDarkTheme"
|
||||
@change="themeSwitcher.toggleTheme()"></a-switch>
|
||||
</a-menu-item>
|
||||
<a-menu-item id="change-theme-ultra" v-if="themeSwitcher.isDarkTheme" class="ant-menu-theme-switch"
|
||||
@mousedown="themeSwitcher.animationsOffUltra()">
|
||||
<span>{{ i18n "menu.ultraDark" }}</span>
|
||||
<a-checkbox :style="{ marginLeft: '2px' }" :checked="themeSwitcher.isUltra"
|
||||
@click="themeSwitcher.toggleUltra()"></a-checkbox>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</a-menu>
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
{{define "component/themeSwitchTemplateLogin"}}
|
||||
<template>
|
||||
<a-space @mousedown="themeSwitcher.animationsOff()" id="change-theme" direction="vertical" :size="10" :style="{ width: '100%' }">
|
||||
<a-space direction="horizontal" size="small">
|
||||
<a-switch size="small" :default-checked="themeSwitcher.isDarkTheme" @change="themeSwitcher.toggleTheme()"></a-switch>
|
||||
<span>{{ i18n "menu.dark" }}</span>
|
||||
</a-space>
|
||||
<a-space v-if="themeSwitcher.isDarkTheme" direction="horizontal" size="small">
|
||||
<a-checkbox :checked="themeSwitcher.isUltra" @click="themeSwitcher.toggleUltra()"></a-checkbox>
|
||||
<span>{{ i18n "menu.ultraDark" }}</span>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
{{define "component/aThemeSwitch"}}
|
||||
<script>
|
||||
function createThemeSwitcher() {
|
||||
const isDarkTheme = localStorage.getItem('dark-mode') === 'true';
|
||||
const isUltra = localStorage.getItem('isUltraDarkThemeEnabled') === 'true';
|
||||
if (isUltra) {
|
||||
document.documentElement.setAttribute('data-theme', 'ultra-dark');
|
||||
}
|
||||
const theme = isDarkTheme ? 'dark' : 'light';
|
||||
document.querySelector('body').setAttribute('class', theme);
|
||||
return {
|
||||
animationsOff() {
|
||||
document.documentElement.setAttribute('data-theme-animations', 'off');
|
||||
const themeAnimations = document.querySelector('#change-theme');
|
||||
themeAnimations.addEventListener('mouseleave', () => {
|
||||
document.documentElement.removeAttribute('data-theme-animations');
|
||||
});
|
||||
themeAnimations.addEventListener('touchend', () => {
|
||||
document.documentElement.removeAttribute('data-theme-animations');
|
||||
});
|
||||
},
|
||||
animationsOffUltra() {
|
||||
document.documentElement.setAttribute('data-theme-animations', 'off');
|
||||
const themeAnimationsUltra = document.querySelector('#change-theme-ultra');
|
||||
themeAnimationsUltra.addEventListener('mouseleave', () => {
|
||||
document.documentElement.removeAttribute('data-theme-animations');
|
||||
});
|
||||
themeAnimationsUltra.addEventListener('touchend', () => {
|
||||
document.documentElement.removeAttribute('data-theme-animations');
|
||||
});
|
||||
},
|
||||
isDarkTheme,
|
||||
isUltra,
|
||||
get currentTheme() {
|
||||
return this.isDarkTheme ? 'dark' : 'light';
|
||||
},
|
||||
toggleTheme() {
|
||||
this.isDarkTheme = !this.isDarkTheme;
|
||||
localStorage.setItem('dark-mode', this.isDarkTheme);
|
||||
document.querySelector('body').setAttribute('class', this.isDarkTheme ? 'dark' : 'light');
|
||||
document.getElementById('message').className = themeSwitcher.currentTheme;
|
||||
},
|
||||
toggleUltra() {
|
||||
this.isUltra = !this.isUltra;
|
||||
if (this.isUltra) {
|
||||
document.documentElement.setAttribute('data-theme', 'ultra-dark');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
}
|
||||
localStorage.setItem('isUltraDarkThemeEnabled', this.isUltra.toString());
|
||||
}
|
||||
};
|
||||
}
|
||||
const themeSwitcher = createThemeSwitcher();
|
||||
Vue.component('a-theme-switch', {
|
||||
template: `{{template "component/themeSwitchTemplate"}}`,
|
||||
data: () => ({
|
||||
themeSwitcher
|
||||
}),
|
||||
mounted() {
|
||||
this.$message.config({
|
||||
getContainer: () => document.getElementById('message')
|
||||
});
|
||||
document.getElementById('message').className = themeSwitcher.currentTheme;
|
||||
}
|
||||
});
|
||||
Vue.component('a-theme-switch-login', {
|
||||
template: `{{template "component/themeSwitchTemplateLogin"}}`,
|
||||
data: () => ({
|
||||
themeSwitcher
|
||||
}),
|
||||
mounted() {
|
||||
this.$message.config({
|
||||
getContainer: () => document.getElementById('message')
|
||||
});
|
||||
document.getElementById('message').className = themeSwitcher.currentTheme;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
196
web/html/form/client.html
Normal file
196
web/html/form/client.html
Normal file
@@ -0,0 +1,196 @@
|
||||
{{define "form/client"}}
|
||||
<a-form layout="horizontal" v-if="client" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
|
||||
<a-switch v-model="client.enable"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.email" }}
|
||||
<a-icon type="sync" @click="client.email = RandomUtil.randomLowerAndNum(9)"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="client.email"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
{{ i18n "password" }}
|
||||
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)" type="sync"></a-icon>
|
||||
<a-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)"type="sync"> </a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="client.password"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="client.id"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.protocol === Protocols.VMESS" label='{{ i18n "security" }}'>
|
||||
<a-select v-model="client.security" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email && app.subSettings?.enable">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
|
||||
</template>
|
||||
Subscription
|
||||
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="client.subId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email && app.tgBotEnable">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
|
||||
</template>
|
||||
Telegram ChatID
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number :style="{ width: '50%' }" v-model.number="client.tgId" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 中文注释: 增加【独立限速】 speedLimit 输入框 -->
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.speedLimitDesc" }}</span>
|
||||
</template>
|
||||
<span>
|
||||
{{ i18n "pages.inbounds.speedLimit" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number
|
||||
v-model.number="client.speedLimit"
|
||||
:min="0"
|
||||
style="width: 100%">
|
||||
<template slot="addonAfter">
|
||||
KB/s
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="client.email" label='{{ i18n "comment" }}'>
|
||||
<a-input v-model.trim="client.comment"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="app.ipLimitEnable">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.IPLimitDesc"}}</span>
|
||||
</template>
|
||||
<span>{{ i18n "pages.inbounds.IPLimit"}} </span>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model.number="client.limitIp" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.IPLimitlogDesc" }}</span>
|
||||
</template>
|
||||
<span>{{ i18n "pages.inbounds.IPLimitlog" }} </span>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
|
||||
</template>
|
||||
<span :style="{ color: '#FF4D4F' }">
|
||||
<a-icon type="delete" @click="clearDBClientIps(client.email)"></a-icon>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<a-form layout="block">
|
||||
<a-textarea id="clientIPs" readonly @click="getDBClientIps(client.email)" placeholder="Click To Get IPs"
|
||||
:auto-size="{ minRows: 5, maxRows: 10 }">
|
||||
</a-textarea>
|
||||
</a-form>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.canEnableTlsFlow()" label='Flow'>
|
||||
<a-select v-model="client.flow" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.totalFlow" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model.number="client._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="isEdit && clientStats" label='{{ i18n "usage" }}'>
|
||||
<a-tag :color="ColorUtils.clientUsageColor(clientStats, app.trafficDiff)">
|
||||
[[ SizeFormatter.sizeFormat(clientStats.up) ]] /
|
||||
[[ SizeFormatter.sizeFormat(clientStats.down) ]]
|
||||
([[ SizeFormatter.sizeFormat(clientStats.up + clientStats.down) ]])
|
||||
</a-tag>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
||||
<a-icon type="retweet"
|
||||
@click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
|
||||
v-if="client.email.length > 0"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
|
||||
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
|
||||
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item v-else>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</template>
|
||||
{{ i18n "pages.inbounds.expireDate" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme" v-model="client._expiryTime"></a-date-picker>
|
||||
<a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
|
||||
value="client._expiryTime" v-model="client._expiryTime"></a-persian-datepicker>
|
||||
<a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.expiryTime != 0">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.client.renewDesc" }}</template>
|
||||
{{ i18n "pages.client.renew" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model.number="client.reset" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
166
web/html/form/inbound.html
Normal file
166
web/html/form/inbound.html
Normal file
@@ -0,0 +1,166 @@
|
||||
{{define "form/inbound"}}
|
||||
<!-- base -->
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<!-- 开关:启用/禁用 -->
|
||||
<a-form-item label='{{ i18n "enable" }}'>
|
||||
<a-switch v-model="dbInbound.enable"></a-switch>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 备注 -->
|
||||
<a-form-item label='{{ i18n "remark" }}'>
|
||||
<a-input v-model.trim="dbInbound.remark"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 协议选择 -->
|
||||
<a-form-item label='{{ i18n "protocol" }}'>
|
||||
<a-select v-model="inbound.protocol" :disabled="isEdit" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 监听地址 -->
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.monitorDesc" }}</span>
|
||||
</template>
|
||||
{{ i18n "monitor" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="inbound.listen"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 端口 + 总流量 在同一行 -->
|
||||
<a-row :gutter="16">
|
||||
<!-- 左侧:端口 -->
|
||||
<a-col :span="12">
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
端口范围:1 - 65531(请尽量使用高位端口)
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.port" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model.number="inbound.port" :min="1" :max="65531" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<!-- 右侧:总流量 -->
|
||||
<a-col :span="12">
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.totalFlow" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model.number="dbInbound.totalGB" :min="0" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 设备限制 单独占一行 -->
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.inbounds.deviceLimitDesc" }}
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.deviceLimit" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number
|
||||
v-model.number="dbInbound.deviceLimit"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
placeholder="0 = 不限制" />
|
||||
</a-form-item>
|
||||
|
||||
<!-- 到期时间 -->
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.expireDate" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-date-picker :style="{ width: '100%' }" v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }"
|
||||
format="YYYY-MM-DD HH:mm:ss" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="dbInbound._expiryTime"></a-date-picker>
|
||||
<a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
|
||||
value="dbInbound._expiryTime" v-model="dbInbound._expiryTime">
|
||||
</a-persian-datepicker>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vmess settings -->
|
||||
<template v-if="inbound.protocol === Protocols.VMESS">
|
||||
{{template "form/vmess"}}
|
||||
</template>
|
||||
|
||||
<!-- vless settings -->
|
||||
<template v-if="inbound.protocol === Protocols.VLESS">
|
||||
{{template "form/vless"}}
|
||||
</template>
|
||||
|
||||
<!-- trojan settings -->
|
||||
<template v-if="inbound.protocol === Protocols.TROJAN">
|
||||
{{template "form/trojan"}}
|
||||
</template>
|
||||
|
||||
<!-- shadowsocks -->
|
||||
<template v-if="inbound.protocol === Protocols.SHADOWSOCKS">
|
||||
{{template "form/shadowsocks"}}
|
||||
</template>
|
||||
|
||||
<!-- tunnel -->
|
||||
<template v-if="inbound.protocol === Protocols.TUNNEL">
|
||||
{{template "form/tunnel"}}
|
||||
</template>
|
||||
|
||||
<!-- socks -->
|
||||
<template v-if="inbound.protocol === Protocols.SOCKS">
|
||||
{{template "form/socks"}}
|
||||
</template>
|
||||
|
||||
<!-- http -->
|
||||
<template v-if="inbound.protocol === Protocols.HTTP">
|
||||
{{template "form/http"}}
|
||||
</template>
|
||||
|
||||
<!-- wireguard -->
|
||||
<template v-if="inbound.protocol === Protocols.WIREGUARD">
|
||||
{{template "form/wireguard"}}
|
||||
</template>
|
||||
|
||||
<!-- stream settings -->
|
||||
<template v-if="inbound.canEnableStream()">
|
||||
{{template "form/streamSettings"}}
|
||||
{{template "form/externalProxy" }}
|
||||
</template>
|
||||
|
||||
<!-- tls settings -->
|
||||
<template v-if="inbound.canEnableTls()">
|
||||
{{template "form/tlsSettings"}}
|
||||
</template>
|
||||
|
||||
<!-- sniffing -->
|
||||
<a-collapse>
|
||||
<a-collapse-panel header='Sniffing'>
|
||||
{{template "form/sniffing"}}
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
|
||||
{{end}}
|
||||
830
web/html/form/outbound.html
Normal file
830
web/html/form/outbound.html
Normal file
@@ -0,0 +1,830 @@
|
||||
{{define "form/outbound"}}
|
||||
<!-- base -->
|
||||
<a-tabs :active-key="outModal.activeKey"
|
||||
:style="{ padding: '0', backgroundColor: 'transparent' }"
|
||||
@change="(activeKey) => {outModal.toggleJson(activeKey == '2'); }">
|
||||
<a-tab-pane key="1" tab="Form">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "protocol" }}'>
|
||||
<a-select v-model="outbound.protocol"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="x,y in Protocols" :value="x">[[ y
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.tag" }}' has-feedback
|
||||
:validate-status="outModal.duplicateTag? 'warning' : 'success'">
|
||||
<a-input v-model.trim="outbound.tag" @change="outModal.check()"
|
||||
placeholder='{{ i18n "pages.xray.outbound.tagDesc" }}'></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.sendThrough" }}'>
|
||||
<a-input v-model="outbound.sendThrough"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<!-- freedom settings-->
|
||||
<template v-if="outbound.protocol === Protocols.Freedom">
|
||||
<a-form-item label='Strategy'>
|
||||
<a-select v-model="outbound.settings.domainStrategy"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[
|
||||
s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Redirect'>
|
||||
<a-input v-model="outbound.settings.redirect"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Fragment'>
|
||||
<a-switch :checked="Object.keys(outbound.settings.fragment).length >0"
|
||||
@change="checked => outbound.settings.fragment = checked ? new Outbound.FreedomSettings.Fragment() : {}">
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="Object.keys(outbound.settings.fragment).length >0">
|
||||
<a-form-item label='Packets'>
|
||||
<a-select v-model="outbound.settings.fragment.packets"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['1-3','tlshello']" :value="s">[[ s
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Length'>
|
||||
<a-input v-model.trim="outbound.settings.fragment.length"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Interval'>
|
||||
<a-input
|
||||
v-model.trim="outbound.settings.fragment.interval"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Split'>
|
||||
<a-input
|
||||
v-model.trim="outbound.settings.fragment.maxSplit"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- Switch for Noises -->
|
||||
<a-form-item label='Noises'>
|
||||
<a-switch :checked="outbound.settings.noises.length > 0"
|
||||
@change="checked => outbound.settings.noises = checked ? [new Outbound.FreedomSettings.Noise()] : []">
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Add Noise Button -->
|
||||
<template v-if="outbound.settings.noises.length > 0">
|
||||
<a-form-item label="Noises">
|
||||
<a-button icon="plus" type="primary" size="small"
|
||||
@click="outbound.settings.addNoise()"></a-button>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Noise Configurations -->
|
||||
<a-form v-for="(noise, index) in outbound.settings.noises"
|
||||
:key="index" :colon="false"
|
||||
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '0' }"> Noise [[ index + 1 ]]
|
||||
<a-icon v-if="outbound.settings.noises.length > 1" type="delete"
|
||||
@click="() => outbound.settings.delNoise(index)"
|
||||
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='Type'>
|
||||
<a-select v-model="noise.type"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['rand','base64','str', 'hex']"
|
||||
:value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Packet'>
|
||||
<a-input v-model.trim="noise.packet"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Delay'>
|
||||
<a-input v-model.trim="noise.delay"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Apply To'>
|
||||
<a-select v-model="noise.applyTo"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['ip','ipv4','ipv6']" :value="s">[[
|
||||
s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- blackhole settings -->
|
||||
<template v-if="outbound.protocol === Protocols.Blackhole">
|
||||
<a-form-item label='Response Type'>
|
||||
<a-select v-model="outbound.settings.type"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- dns settings -->
|
||||
<template v-if="outbound.protocol === Protocols.DNS">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
|
||||
<a-select v-model="outbound.settings.network"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='non-IP queries'>
|
||||
<a-select v-model="outbound.settings.nonIPQuery"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['reject','drop','skip']" :value="s">[[
|
||||
s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="outbound.settings.nonIPQuery === 'skip'"
|
||||
label='Block Types'>
|
||||
<a-input v-model.number="outbound.settings.blockTypes"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- wireguard settings -->
|
||||
<template v-if="outbound.protocol === Protocols.Wireguard">
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.outbound.address" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="outbound.settings.address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.wireguard.secretKey" }}
|
||||
<a-icon type="sync"
|
||||
@click="[outbound.settings.pubKey, outbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())">
|
||||
</a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="outbound.settings.secretKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
|
||||
<a-input disabled v-model="outbound.settings.pubKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.domainStrategy" }}'>
|
||||
<a-select v-model="outbound.settings.domainStrategy"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="wds in ['', ...WireguardDomainStrategy]"
|
||||
:value="wds">[[ wds ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='MTU'>
|
||||
<a-input-number v-model.number="outbound.settings.mtu"
|
||||
min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Workers'>
|
||||
<a-input-number v-model.number="outbound.settings.workers"
|
||||
min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='No Kernel Tun'>
|
||||
<a-switch v-model="outbound.settings.noKernelTun"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template> Reserved <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model="outbound.settings.reserved"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Peers">
|
||||
<a-button icon="plus" type="primary" size="small"
|
||||
@click="outbound.settings.addPeer()"></a-button>
|
||||
</a-form-item>
|
||||
<a-form v-for="(peer, index) in outbound.settings.peers" :colon="false"
|
||||
:label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '0' }"> Peer [[ index + 1 ]] <a-icon
|
||||
v-if="outbound.settings.peers.length>1"
|
||||
type="delete" @click="() => outbound.settings.delPeer(index)"
|
||||
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.endpoint" }}'>
|
||||
<a-input v-model.trim="peer.endpoint"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
|
||||
<a-input v-model.trim="peer.publicKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.psk" }}'>
|
||||
<a-input v-model.trim="peer.psk"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
{{ i18n "pages.xray.wireguard.allowedIPs" }}
|
||||
<a-button icon="plus" type="primary" size="small"
|
||||
@click="peer.allowedIPs.push('')"></a-button>
|
||||
</template>
|
||||
<template v-for="(aip, index) in peer.allowedIPs"
|
||||
:style="{ marginBottom: '10px' }">
|
||||
<a-input v-model.trim="peer.allowedIPs[index]">
|
||||
<a-button icon="minus" v-if="peer.allowedIPs.length>1"
|
||||
slot="addonAfter" size="small"
|
||||
@click="peer.allowedIPs.splice(index, 1)"></a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label='Keep Alive'>
|
||||
<a-input-number v-model.number="peer.keepAlive"
|
||||
:min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<!-- Address + Port -->
|
||||
<template v-if="outbound.hasAddressPort()">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.address" }}'>
|
||||
<a-input v-model.trim="outbound.settings.address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input-number v-model.number="outbound.settings.port" :min="1"
|
||||
:max="65532"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- VLESS/VMess user settings -->
|
||||
<template
|
||||
v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
|
||||
<a-form-item label='ID'>
|
||||
<a-input v-model.trim="outbound.settings.id"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<!-- vmess settings -->
|
||||
<template v-if="outbound.protocol === Protocols.VMess">
|
||||
<a-form-item label='Security'>
|
||||
<a-select v-model="outbound.settings.security"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- vless settings -->
|
||||
<template v-if="outbound.protocol === Protocols.VLESS">
|
||||
<a-form-item label='encryption'>
|
||||
<a-input v-model.trim="outbound.settings.encryption"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-if="outbound.canEnableTlsFlow()">
|
||||
<a-form-item label='Flow'>
|
||||
<a-select v-model="outbound.settings.flow"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value selected>{{ i18n "none"
|
||||
}}</a-select-option>
|
||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[
|
||||
key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<!-- XTLS Vision Advanced Settings -->
|
||||
<template v-if="outbound.canEnableVisionSeed()">
|
||||
<a-form-item label="Vision Pre-Connect">
|
||||
<a-input-number v-model.number="outbound.settings.testpre" :min="0"
|
||||
:max="10" :style="{ width: '100%' }"
|
||||
placeholder="5"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="Vision Seed">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="6">
|
||||
<a-input-number v-model.number="outbound.settings.testseed[0]"
|
||||
:min="0" :max="9999"
|
||||
:style="{ width: '100%' }" placeholder="900"
|
||||
addon-before="[0]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number v-model.number="outbound.settings.testseed[1]"
|
||||
:min="0" :max="9999"
|
||||
:style="{ width: '100%' }" placeholder="500"
|
||||
addon-before="[1]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number v-model.number="outbound.settings.testseed[2]"
|
||||
:min="0" :max="9999"
|
||||
:style="{ width: '100%' }" placeholder="900"
|
||||
addon-before="[2]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number v-model.number="outbound.settings.testseed[3]"
|
||||
:min="0" :max="9999"
|
||||
:style="{ width: '100%' }" placeholder="256"
|
||||
addon-before="[3]"></a-input-number>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Servers (trojan/shadowsocks/socks/http) settings -->
|
||||
<template v-if="outbound.hasServers()">
|
||||
<!-- http / socks -->
|
||||
<template v-if="outbound.hasUsername()">
|
||||
<a-form-item label='{{ i18n "username" }}'>
|
||||
<a-input v-model.trim="outbound.settings.user"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="outbound.settings.pass"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- trojan/shadowsocks -->
|
||||
<template
|
||||
v-if="[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="outbound.settings.password"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- shadowsocks -->
|
||||
<template v-if="outbound.protocol === Protocols.Shadowsocks">
|
||||
<a-form-item label='{{ i18n "encryption" }}'>
|
||||
<a-select v-model="outbound.settings.method"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="(method, method_name) in SSMethods"
|
||||
:value="method">[[ method_name
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='UDP over TCP'>
|
||||
<a-switch v-model="outbound.settings.uot"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='UoTVersion'>
|
||||
<a-input-number v-model.number="outbound.settings.UoTVersion"
|
||||
:min="1" :max="2"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- hysteria settings -->
|
||||
<template v-if="outbound.protocol === Protocols.Hysteria">
|
||||
<a-form-item label='Version'>
|
||||
<a-input-number v-model.number="outbound.settings.version" :min="2"
|
||||
:max="2" disabled></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- stream settings -->
|
||||
<template v-if="outbound.canEnableStream()">
|
||||
<a-form-item label='{{ i18n "transmission" }}'>
|
||||
<a-select v-model="outbound.stream.network"
|
||||
@change="streamNetworkChange"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp">TCP (RAW)</a-select-option>
|
||||
<a-select-option value="kcp">mKCP</a-select-option>
|
||||
<a-select-option value="ws">WebSocket</a-select-option>
|
||||
<a-select-option value="grpc">gRPC</a-select-option>
|
||||
<a-select-option value="httpupgrade">HTTPUpgrade</a-select-option>
|
||||
<a-select-option value="xhttp">XHTTP</a-select-option>
|
||||
<a-select-option v-if="outbound.protocol === Protocols.Hysteria"
|
||||
value="hysteria">Hysteria2</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.network === 'tcp'">
|
||||
<a-form-item label='HTTP {{ i18n "camouflage" }}'>
|
||||
<a-switch :checked="outbound.stream.tcp.type === 'http'"
|
||||
@change="checked => outbound.stream.tcp.type = checked ? 'http' : 'none'"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.tcp.type == 'http'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="outbound.stream.tcp.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="outbound.stream.tcp.path"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- kcp -->
|
||||
<template v-if="outbound.stream.network === 'kcp'">
|
||||
<a-form-item label='MTU'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.mtu"
|
||||
min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='TTI (ms)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.tti"
|
||||
min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Uplink (MB/s)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.upCap"
|
||||
min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Downlink (MB/s)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.downCap"
|
||||
min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Congestion'>
|
||||
<a-switch v-model="outbound.stream.kcp.congestion"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Read Buffer (MB)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.readBuffer"
|
||||
min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Write Buffer (MB)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.writeBuffer"
|
||||
min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- ws -->
|
||||
<template v-if="outbound.stream.network === 'ws'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model="outbound.stream.ws.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="outbound.stream.ws.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Heartbeat Period'>
|
||||
<a-input-number v-model.number="outbound.stream.ws.heartbeatPeriod"
|
||||
:min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- grpc -->
|
||||
<template v-if="outbound.stream.network === 'grpc'">
|
||||
<a-form-item label='Service Name'>
|
||||
<a-input v-model.trim="outbound.stream.grpc.serviceName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Authority">
|
||||
<a-input v-model.trim="outbound.stream.grpc.authority"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Multi Mode'>
|
||||
<a-switch v-model="outbound.stream.grpc.multiMode"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- httpupgrade -->
|
||||
<template v-if="outbound.stream.network === 'httpupgrade'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model="outbound.stream.httpupgrade.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="outbound.stream.httpupgrade.path"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- xhttp -->
|
||||
<template v-if="outbound.stream.network === 'xhttp'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model="outbound.stream.xhttp.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="outbound.stream.xhttp.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Mode'>
|
||||
<a-select v-model="outbound.stream.xhttp.mode"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in MODE_OPTION" :value="key">[[ key
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="No gRPC Header"
|
||||
v-if="outbound.stream.xhttp.mode === 'stream-up' || outbound.stream.xhttp.mode === 'stream-one'">
|
||||
<a-switch v-model="outbound.stream.xhttp.noGRPCHeader"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Min Upload Interval (Ms)"
|
||||
v-if="outbound.stream.xhttp.mode === 'packet-up'">
|
||||
<a-input
|
||||
v-model.trim="outbound.stream.xhttp.scMinPostsIntervalMs"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Concurrency"
|
||||
v-if="!outbound.stream.xhttp.xmux.maxConnections">
|
||||
<a-input
|
||||
v-model="outbound.stream.xhttp.xmux.maxConcurrency"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Connections"
|
||||
v-if="!outbound.stream.xhttp.xmux.maxConcurrency">
|
||||
<a-input
|
||||
v-model="outbound.stream.xhttp.xmux.maxConnections"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Reuse Times">
|
||||
<a-input
|
||||
v-model="outbound.stream.xhttp.xmux.cMaxReuseTimes"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Request Times">
|
||||
<a-input
|
||||
v-model="outbound.stream.xhttp.xmux.hMaxRequestTimes"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Reusable Secs">
|
||||
<a-input
|
||||
v-model="outbound.stream.xhttp.xmux.hMaxReusableSecs"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Keep Alive Period'>
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.xhttp.xmux.hKeepAlivePeriod"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- hysteria -->
|
||||
<template v-if="outbound.stream.network === 'hysteria'">
|
||||
<a-form-item label='Auth Password'>
|
||||
<a-input v-model.trim="outbound.stream.hysteria.auth"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Congestion'>
|
||||
<a-select v-model="outbound.stream.hysteria.congestion"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>BBR (Auto)</a-select-option>
|
||||
<a-select-option value="brutal">Brutal</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Upload Speed'>
|
||||
<a-input v-model.trim="outbound.stream.hysteria.up"
|
||||
placeholder="0 (BBR mode), e.g., 100 mbps"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Download Speed'>
|
||||
<a-input v-model.trim="outbound.stream.hysteria.down"
|
||||
placeholder="0 (BBR mode), e.g., 100 mbps"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='UDP Hop Port'>
|
||||
<a-input v-model.trim="outbound.stream.hysteria.udphopPort"
|
||||
placeholder="e.g., 1145-1919 or 11,13,15-17"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='UDP Hop Interval Min (s)'
|
||||
v-if="outbound.stream.hysteria.udphopPort">
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.hysteria.udphopIntervalMin"
|
||||
:min="5"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='UDP Hop Interval Max (s)'
|
||||
v-if="outbound.stream.hysteria.udphopPort">
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.hysteria.udphopIntervalMax"
|
||||
:min="5"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Init Stream Receive'>
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.hysteria.initStreamReceiveWindow"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Stream Receive'>
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.hysteria.maxStreamReceiveWindow"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Init Connection Receive'>
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.hysteria.initConnectionReceiveWindow"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Connection Receive'>
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.hysteria.maxConnectionReceiveWindow"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Idle Timeout (s)'>
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.hysteria.maxIdleTimeout" :min="4"
|
||||
:max="120"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Keep Alive Period (s)'>
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.hysteria.keepAlivePeriod" :min="0"
|
||||
:max="60"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Disable Path MTU'>
|
||||
<a-switch
|
||||
v-model="outbound.stream.hysteria.disablePathMTUDiscovery"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- finalmask settings -->
|
||||
<template v-if="outbound.canEnableStream()">
|
||||
<a-form-item label="UDP Masks">
|
||||
<a-button icon="plus" type="primary" size="small"
|
||||
@click="outbound.stream.addUdpMask(outbound.protocol === Protocols.Hysteria ? 'salamander' : (outbound.stream.network === 'kcp' ? 'mkcp-aes128gcm' : 'xdns'))"></a-button>
|
||||
</a-form-item>
|
||||
<template
|
||||
v-if="outbound.stream.finalmask.udp && outbound.stream.finalmask.udp.length > 0">
|
||||
<a-form v-for="(mask, index) in outbound.stream.finalmask.udp"
|
||||
:key="index" :colon="false"
|
||||
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '0' }"> UDP Mask [[ index + 1 ]]
|
||||
<a-icon type="delete"
|
||||
@click="() => outbound.stream.delUdpMask(index)"
|
||||
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='Type'>
|
||||
<a-select v-model="mask.type"
|
||||
@change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); if(outbound.stream.network === 'kcp') { outbound.stream.kcp.mtu = type === 'xdns' ? 900 : 1350; } }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<!-- Salamander for Hysteria2 only -->
|
||||
<a-select-option v-if="outbound.protocol === Protocols.Hysteria"
|
||||
value="salamander">
|
||||
Salamander (Hysteria2)</a-select-option>
|
||||
<!-- mKCP-specific masks -->
|
||||
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
||||
value="mkcp-aes128gcm">
|
||||
mKCP AES-128-GCM</a-select-option>
|
||||
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
||||
value="header-dns">
|
||||
Header DNS</a-select-option>
|
||||
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
||||
value="header-dtls">
|
||||
Header DTLS 1.2</a-select-option>
|
||||
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
||||
value="header-srtp">
|
||||
Header SRTP</a-select-option>
|
||||
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
||||
value="header-utp">
|
||||
Header uTP</a-select-option>
|
||||
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
||||
value="header-wechat">
|
||||
Header WeChat Video</a-select-option>
|
||||
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
||||
value="header-wireguard">
|
||||
Header WireGuard</a-select-option>
|
||||
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
||||
value="mkcp-original">
|
||||
mKCP Original</a-select-option>
|
||||
<!-- xDNS for TCP/WS/HTTPUpgrade/XHTTP/KCP -->
|
||||
<a-select-option
|
||||
v-if="['tcp', 'ws', 'httpupgrade', 'xhttp', 'kcp'].includes(outbound.stream.network)"
|
||||
value="xdns">
|
||||
xDNS (Experimental)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<!-- Settings for password-based masks -->
|
||||
<a-form-item label='Password'
|
||||
v-if="['salamander', 'mkcp-aes128gcm'].includes(mask.type)">
|
||||
<a-input v-model.trim="mask.settings.password"
|
||||
placeholder="Obfuscation password"></a-input>
|
||||
</a-form-item>
|
||||
<!-- Settings for domain-based masks -->
|
||||
<a-form-item label='Domain'
|
||||
v-if="['header-dns', 'xdns'].includes(mask.type)">
|
||||
<a-input v-model.trim="mask.settings.domain"
|
||||
placeholder="e.g., www.example.com"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- tls settings -->
|
||||
<template v-if="outbound.canEnableTls()">
|
||||
<a-form-item label='{{ i18n "security" }}'>
|
||||
<a-radio-group v-model="outbound.stream.security"
|
||||
button-style="solid">
|
||||
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
|
||||
<a-radio-button value="tls">TLS</a-radio-button>
|
||||
<a-radio-button v-if="outbound.canEnableReality()"
|
||||
value="reality">Reality</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.isTls">
|
||||
<a-form-item label="SNI" placeholder="Server Name Indication">
|
||||
<a-input v-model.trim="outbound.stream.tls.serverName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="outbound.stream.tls.fingerprint"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>None</a-select-option>
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[
|
||||
key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="ALPN">
|
||||
<a-select mode="multiple"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="outbound.stream.tls.alpn">
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="ECH Config List">
|
||||
<a-input v-model.trim="outbound.stream.tls.echConfigList"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="verify Peer Cert By Name">
|
||||
<a-input
|
||||
v-model.trim="outbound.stream.tls.verifyPeerCertByName"
|
||||
placeholder="cloudflare-dns.com"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="pinned Peer Cert Sha256">
|
||||
<a-input v-model.trim="outbound.stream.tls.pinnedPeerCertSha256"
|
||||
placeholder="Enter SHA256 fingerprints (base64)">
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- reality settings -->
|
||||
<template v-if="outbound.stream.isReality">
|
||||
<a-form-item label="SNI">
|
||||
<a-input
|
||||
v-model.trim="outbound.stream.reality.serverName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="outbound.stream.reality.fingerprint"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[
|
||||
key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Short ID">
|
||||
<a-input v-model.trim="outbound.stream.reality.shortId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="SpiderX">
|
||||
<a-input v-model.trim="outbound.stream.reality.spiderX"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Public Key">
|
||||
<a-textarea
|
||||
v-model.trim="outbound.stream.reality.publicKey"></a-textarea>
|
||||
</a-form-item>
|
||||
<a-form-item label="mldsa65 Verify">
|
||||
<a-textarea
|
||||
v-model.trim="outbound.stream.reality.mldsa65Verify"></a-textarea>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- sockopt settings -->
|
||||
<a-form-item label="Sockopts">
|
||||
<a-switch v-model="outbound.stream.sockoptSwitch"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.sockoptSwitch">
|
||||
<a-form-item label="Dialer Proxy">
|
||||
<a-select v-model="outbound.stream.sockopt.dialerProxy"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="tag in ['', ...outModal.tags]"
|
||||
:value="tag">[[ tag ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Address Port Strategy'>
|
||||
<a-select v-model="outbound.stream.sockopt.addressPortStrategy"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in Address_Port_Strategy"
|
||||
:value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Keep Alive Interval">
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.sockopt.tcpKeepAliveInterval"
|
||||
:min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP Fast Open">
|
||||
<a-switch v-model="outbound.stream.sockopt.tcpFastOpen"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Multipath TCP">
|
||||
<a-switch v-model.trim="outbound.stream.sockopt.tcpMptcp"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Penetrate">
|
||||
<a-switch v-model="outbound.stream.sockopt.penetrate"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Trusted X-Forwarded-For">
|
||||
<a-select mode="tags"
|
||||
v-model="outbound.stream.sockopt.trustedXForwardedFor"
|
||||
:style="{ width: '100%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option
|
||||
value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
|
||||
<a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
|
||||
<a-select-option
|
||||
value="True-Client-IP">True-Client-IP</a-select-option>
|
||||
<a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- mux settings -->
|
||||
<template v-if="outbound.canEnableMux()">
|
||||
<a-form-item label="Mux">
|
||||
<a-switch v-model="outbound.mux.enabled"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.mux.enabled">
|
||||
<a-form-item label="Concurrency">
|
||||
<a-input-number v-model.number="outbound.mux.concurrency" :min="-1"
|
||||
:max="1024"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="xudp Concurrency">
|
||||
<a-input-number v-model.number="outbound.mux.xudpConcurrency"
|
||||
:min="-1" :max="1024"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="xudp UDP 443">
|
||||
<a-select v-model="outbound.mux.xudpProxyUDP443"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="c in ['reject', 'allow', 'skip']"
|
||||
:value="c">[[ c ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="JSON" force-render="true">
|
||||
<a-space direction="vertical" :size="10" :style="{ marginTop: '10px' }">
|
||||
<a-input addon-before='{{ i18n "pages.xray.outbound.link" }}'
|
||||
v-model.trim="outModal.link"
|
||||
placeholder="vmess:// vless:// trojan:// ss:// hysteria2://">
|
||||
<a-icon slot="addonAfter" type="form" @click="convertLink"></a-icon>
|
||||
</a-input>
|
||||
<textarea :style="{ position: 'absolute', left: '-800px' }"
|
||||
id="outboundJson"></textarea>
|
||||
</a-space>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
{{end}}
|
||||
37
web/html/form/protocol/dokodemo.html
Normal file
37
web/html/form/protocol/dokodemo.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{{define "form/tunnel"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
|
||||
<a-input v-model.trim="inbound.settings.address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
|
||||
<a-input-number v-model.number="inbound.settings.port"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.portMap"}}'>
|
||||
<a-button size="small" @click="inbound.settings.portMap.push({name: '', value: ''})">+</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(pm, index) in inbound.settings.portMap">
|
||||
<a-input style="width: 50%" v-model.trim="pm.name" placeholder='{{ i18n "pages.inbounds.port"}}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="pm.value" placeholder='{{ i18n "pages.inbounds.targetAddress" }}'>
|
||||
<a-button slot="addonAfter" size="small" @click="inbound.settings.portMap.splice(index,1)">-</a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.network"}}'>
|
||||
<a-select v-model="inbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp,udp">TCP,UDP</a-select-option>
|
||||
<a-select-option value="tcp">TCP</a-select-option>
|
||||
<a-select-option value="udp">UDP</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Follow Redirect'>
|
||||
<a-switch v-model="inbound.settings.followRedirect"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<!-- sockopt -->
|
||||
<template>
|
||||
{{template "form/streamSockopt"}}
|
||||
</template>
|
||||
{{end}}
|
||||
26
web/html/form/protocol/http.html
Normal file
26
web/html/form/protocol/http.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{{define "form/http"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<table :style="{ width: '100%', textAlign: 'center', margin: '1rem 0' }">
|
||||
<tr>
|
||||
<td width="45%">{{ i18n "username" }}</td>
|
||||
<td width="45%">{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())"></a-button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-input-group compact v-for="(account, index) in inbound.settings.accounts" :style="{ marginBottom: '10px' }">
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
|
||||
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button icon="minus" size="small" @click="inbound.settings.delAccount(index)"></a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
<a-form-item label="Allow Transparent">
|
||||
<a-switch v-model="inbound.settings.allowTransparent" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
50
web/html/form/protocol/shadowsocks.html
Normal file
50
web/html/form/protocol/shadowsocks.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{{define "form/shadowsocks"}}
|
||||
<template v-if="inbound.isSSMultiUser">
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||
{{template "form/client"}}
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<a-collapse v-else>
|
||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.shadowsockses.length">
|
||||
<table width="100%">
|
||||
<tr class="client-table-header">
|
||||
<th>{{ i18n "pages.inbounds.email" }}</th>
|
||||
<th>Password</th>
|
||||
</tr>
|
||||
<tr v-for="(client, index) in inbound.settings.shadowsockses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||
<td>[[ client.email ]]</td>
|
||||
<td>[[ client.password ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</template>
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "encryption" }}'>
|
||||
<a-select v-model="inbound.settings.method" @change="SSMethodChange" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="(method,method_name) in SSMethods" :value="method">[[ method_name ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.isSS2022">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template> Password <a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)" type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="inbound.settings.password"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
|
||||
<a-select v-model="inbound.settings.network" :style="{ width: '100px' }" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp,udp">TCP,UDP</a-select-option>
|
||||
<a-select-option value="tcp">TCP</a-select-option>
|
||||
<a-select-option value="udp">UDP</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='ivCheck'>
|
||||
<a-switch v-model="inbound.settings.ivCheck"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
34
web/html/form/protocol/socks.html
Normal file
34
web/html/form/protocol/socks.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{{define "form/socks"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'>
|
||||
<a-switch v-model="inbound.settings.udp"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="IP" v-if="inbound.settings.udp">
|
||||
<a-input v-model.trim="inbound.settings.ip"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-switch :checked="inbound.settings.auth === 'password'" @change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="inbound.settings.auth === 'password'">
|
||||
<table :style="{ width: '100%', textAlign: 'center', margin: '1rem 0' }">
|
||||
<tr>
|
||||
<td width="45%">{{ i18n "username" }}</td>
|
||||
<td width="45%">{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())"></a-button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-input-group compact v-for="(account, index) in inbound.settings.accounts" :style="{ marginBottom: '10px' }">
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
|
||||
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button icon="minus" size="small" @click="inbound.settings.delAccount(index)"></a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</template>
|
||||
</a-form>
|
||||
{{end}}
|
||||
50
web/html/form/protocol/trojan.html
Normal file
50
web/html/form/protocol/trojan.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{{define "form/trojan"}}
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||
{{template "form/client"}}
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<a-collapse v-else>
|
||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length">
|
||||
<table width="100%">
|
||||
<tr class="client-table-header">
|
||||
<th>{{ i18n "pages.inbounds.email" }}</th>
|
||||
<th>Password</th>
|
||||
</tr>
|
||||
<tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||
<td>[[ client.email ]]</td>
|
||||
<td>[[ client.password ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if="inbound.isTcp">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- trojan fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete" @click="() => inbound.settings.delFallback(index)" :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='SNI'>
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='ALPN'>
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Path'>
|
||||
<a-input v-model="fallback.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Dest'>
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='xVer'>
|
||||
<a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider style="margin:5px 0;"></a-divider>
|
||||
</template>
|
||||
{{end}}
|
||||
102
web/html/form/protocol/vless.html
Normal file
102
web/html/form/protocol/vless.html
Normal file
@@ -0,0 +1,102 @@
|
||||
{{define "form/vless"}}
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||
{{template "form/client"}}
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<a-collapse v-else>
|
||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
|
||||
<table width="100%">
|
||||
<tr class="client-table-header">
|
||||
<th>{{ i18n "pages.inbounds.email" }}</th>
|
||||
<th>ID</th>
|
||||
</tr>
|
||||
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||
<td>[[ client.email ]]</td>
|
||||
<td>[[ client.id ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if="!inbound.stream.isReality">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Authentication">
|
||||
<a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="X25519, not Post-Quantum">X25519 (not Post-Quantum)</a-select-option>
|
||||
<a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768 (Post-Quantum)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="decryption">
|
||||
<a-input v-model.trim="inbound.settings.decryption"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="encryption">
|
||||
<a-input v-model="inbound.settings.encryption" disabled></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button type="primary" icon="import" @click="getNewVlessEnc">Get New keys(获取新密钥)</a-button>
|
||||
<a-button danger @click="clearKeys">Clear</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider v-if="inbound.settings.selectedAuth" :style="{ margin: '5px 0' }"></a-divider>
|
||||
</template>
|
||||
<template v-if="inbound.isTcp">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vless fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete" @click="() => inbound.settings.delFallback(index)" :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='SNI'>
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='ALPN'>
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Path'>
|
||||
<a-input v-model="fallback.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Dest'>
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='xVer'>
|
||||
<a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
||||
</template>
|
||||
<template v-if="inbound.settings.vlesses.some(c => c.flow === 'xtls-rprx-vision' || c.flow === 'xtls-rprx-vision-udp443')">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Vision Seed">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="6">
|
||||
<a-input-number :value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900" @change="(val) => updateTestseed(0, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" addon-before="[0]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number :value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500" @change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="500" addon-before="[1]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number :value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900" @change="(val) => updateTestseed(2, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" addon-before="[2]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number :value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256" @change="(val) => updateTestseed(3, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="256" addon-before="[3]"></a-input-number>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-space :size="8" :style="{ marginTop: '8px' }">
|
||||
<a-button type="primary" @click="setRandomTestseed">
|
||||
Random(随机数值)
|
||||
</a-button>
|
||||
<a-button @click="resetTestseed">
|
||||
Reset(重置默认)
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
||||
</template>
|
||||
{{end}}
|
||||
23
web/html/form/protocol/vmess.html
Normal file
23
web/html/form/protocol/vmess.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{{define "form/vmess"}}
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||
{{template "form/client"}}
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<a-collapse v-else>
|
||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vmesses.length">
|
||||
<table width="100%">
|
||||
<tr class="client-table-header">
|
||||
<th>{{ i18n "pages.inbounds.email" }}</th>
|
||||
<th>ID</th>
|
||||
<th>{{ i18n "security" }}</th>
|
||||
</tr>
|
||||
<tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||
<td>[[ client.email ]]</td>
|
||||
<td>[[ client.id ]]</td>
|
||||
<td>[[ client.security ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
{{end}}
|
||||
76
web/html/form/protocol/wireguard.html
Normal file
76
web/html/form/protocol/wireguard.html
Normal file
@@ -0,0 +1,76 @@
|
||||
{{define "form/wireguard"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.wireguard.secretKey" }}
|
||||
<a-icon type="sync" @click="[inbound.settings.pubKey, inbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="inbound.settings.secretKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
|
||||
<a-input disabled v-model="inbound.settings.pubKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='MTU'>
|
||||
<a-input-number v-model.number="inbound.settings.mtu"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='No Kernel Tun'>
|
||||
<a-switch v-model="inbound.settings.noKernelTun"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Peers">
|
||||
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addPeer()"></a-button>
|
||||
</a-form-item>
|
||||
<a-form v-for="(peer, index) in inbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '0' }"> Peer [[ index + 1 ]] <a-icon v-if="inbound.settings.peers.length>1" type="delete" @click="() => inbound.settings.delPeer(index)" :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.wireguard.secretKey" }}
|
||||
<a-icon @click="[peer.publicKey, peer.privateKey] = Object.values(Wireguard.generateKeypair())" type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="peer.privateKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
{{ i18n "pages.xray.wireguard.publicKey" }}
|
||||
</template>
|
||||
<a-input v-model.trim="peer.publicKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.wireguard.psk" }}
|
||||
<a-icon @click="peer.psk = Wireguard.keyToBase64(Wireguard.generatePresharedKey())" type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="peer.psk"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
{{ i18n "pages.xray.wireguard.allowedIPs" }}
|
||||
<a-button icon="plus" type="primary" size="small" @click="peer.allowedIPs.push('')"></a-button>
|
||||
</template>
|
||||
<template v-for="(aip, index) in peer.allowedIPs" :style="{ marginBottom: '10px' }">
|
||||
<a-input v-model.trim="peer.allowedIPs[index]">
|
||||
<a-button icon="minus" v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small" @click="peer.allowedIPs.splice(index, 1)"></a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label='Keep Alive'>
|
||||
<a-input-number v-model.number="peer.keepAlive" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-form>
|
||||
{{end}}
|
||||
63
web/html/form/reality_settings.html
Normal file
63
web/html/form/reality_settings.html
Normal file
@@ -0,0 +1,63 @@
|
||||
{{define "form/realitySettings"}}
|
||||
<template>
|
||||
<a-form-item label='Show'>
|
||||
<a-switch v-model="inbound.stream.reality.show"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Xver'>
|
||||
<a-input-number v-model.number="inbound.stream.reality.xver" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='uTLS'>
|
||||
<a-select v-model="inbound.stream.reality.settings.fingerprint" :style="{ width: '100%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Target'>
|
||||
<a-input v-model.trim="inbound.stream.reality.target"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='SNI'>
|
||||
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Time Diff (ms)'>
|
||||
<a-input-number v-model.number="inbound.stream.reality.maxTimediff" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Min Client Ver'>
|
||||
<a-input v-model.trim="inbound.stream.reality.minClientVer"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Client Ver'>
|
||||
<a-input v-model.trim="inbound.stream.reality.maxClientVer"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template> Short IDs <a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortIds()"
|
||||
type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-textarea v-model.trim="inbound.stream.reality.shortIds"></a-textarea>
|
||||
</a-form-item>
|
||||
<a-form-item label='SpiderX'>
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.spiderX"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-textarea v-model="inbound.stream.reality.settings.publicKey"></a-textarea>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
|
||||
<a-textarea v-model="inbound.stream.reality.privateKey"></a-textarea>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert(随机获取新证书)</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item label="mldsa65 Seed">
|
||||
<a-textarea v-model="inbound.stream.reality.mldsa65Seed"></a-textarea>
|
||||
</a-form-item>
|
||||
<a-form-item label="mldsa65 Verify">
|
||||
<a-textarea v-model="inbound.stream.reality.settings.mldsa65Verify"></a-textarea>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" icon="import" @click="getNewmldsa65">Get New Seed(获取Mldsa65数值)</a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
{{end}}
|
||||
29
web/html/form/sniffing.html
Normal file
29
web/html/form/sniffing.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{{define "form/sniffing"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
{{ i18n "enabled" }}
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-switch v-model="inbound.sniffing.enabled"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="inbound.sniffing.enabled">
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-checkbox-group v-model="inbound.sniffing.destOverride">
|
||||
<a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key">[[ value ]]</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
<a-form-item label='Metadata Only'>
|
||||
<a-switch v-model="inbound.sniffing.metadataOnly"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Route Only'>
|
||||
<a-switch v-model="inbound.sniffing.routeOnly"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
{{end}}
|
||||
29
web/html/form/stream/external_proxy.html
Normal file
29
web/html/form/stream/external_proxy.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{{define "form/externalProxy"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '5px 0 0' }"></a-divider>
|
||||
<a-form-item label="External Proxy">
|
||||
<a-switch v-model="externalProxy"></a-switch>
|
||||
<a-button icon="plus" v-if="externalProxy" type="primary" :style="{ marginLeft: '10px' }" size="small" @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"></a-button>
|
||||
</a-form-item>
|
||||
<a-input-group :style="{ margin: '8px 0' }" compact v-for="(row, index) in inbound.stream.externalProxy">
|
||||
<template>
|
||||
<a-tooltip title="Force TLS">
|
||||
<a-select v-model="row.forceTls" :style="{ width: '20%', margin: '0px' }" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="same">{{ i18n "pages.inbounds.same" }}</a-select-option>
|
||||
<a-select-option value="none">{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option value="tls">TLS</a-select-option>
|
||||
</a-select>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input :style="{ width: '30%' }" v-model.trim="row.dest" placeholder='{{ i18n "host" }}'></a-input>
|
||||
<a-tooltip title='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input-number :style="{ width: '15%' }" v-model.number="row.port" min="1" max="65531"></a-input-number>
|
||||
</a-tooltip>
|
||||
<a-input :style="{ width: '30%', top: '0' }" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button icon="minus" size="small" @click="inbound.stream.externalProxy.splice(index, 1)"></a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form>
|
||||
{{end}}
|
||||
84
web/html/form/stream/stream_finalmask.html
Normal file
84
web/html/form/stream/stream_finalmask.html
Normal file
@@ -0,0 +1,84 @@
|
||||
{{define "form/streamFinalMask"}}
|
||||
<a-divider :style="{ margin: '5px 0 0' }"></a-divider>
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="UDP Masks">
|
||||
<a-button icon="plus" type="primary" size="small"
|
||||
@click="inbound.stream.addUdpMask(inbound.stream.network === 'kcp' ? 'mkcp-aes128gcm' : 'xdns')"></a-button>
|
||||
</a-form-item>
|
||||
<template
|
||||
v-if="inbound.stream.finalmask.udp && inbound.stream.finalmask.udp.length > 0">
|
||||
<a-form v-for="(mask, index) in inbound.stream.finalmask.udp"
|
||||
:key="index" :colon="false"
|
||||
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '0' }"> UDP Mask [[ index + 1 ]]
|
||||
<a-icon type="delete"
|
||||
@click="() => inbound.stream.delUdpMask(index)"
|
||||
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='Type'>
|
||||
<a-select v-model="mask.type"
|
||||
@change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); if(inbound.stream.network === 'kcp') { inbound.stream.kcp.mtu = type === 'xdns' ? 900 : 1350; } }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<!-- mKCP-specific masks -->
|
||||
<a-select-option v-if="inbound.stream.network === 'kcp'"
|
||||
value="mkcp-aes128gcm">
|
||||
mKCP AES-128-GCM</a-select-option>
|
||||
<a-select-option v-if="inbound.stream.network === 'kcp'"
|
||||
value="header-dns">
|
||||
Header DNS</a-select-option>
|
||||
<a-select-option v-if="inbound.stream.network === 'kcp'"
|
||||
value="header-dtls">
|
||||
Header DTLS 1.2</a-select-option>
|
||||
<a-select-option v-if="inbound.stream.network === 'kcp'"
|
||||
value="header-srtp">
|
||||
Header SRTP</a-select-option>
|
||||
<a-select-option v-if="inbound.stream.network === 'kcp'"
|
||||
value="header-utp">
|
||||
Header uTP</a-select-option>
|
||||
<a-select-option v-if="inbound.stream.network === 'kcp'"
|
||||
value="header-wechat">
|
||||
Header WeChat Video</a-select-option>
|
||||
<a-select-option v-if="inbound.stream.network === 'kcp'"
|
||||
value="header-wireguard">
|
||||
Header WireGuard</a-select-option>
|
||||
<a-select-option v-if="inbound.stream.network === 'kcp'"
|
||||
value="mkcp-original">
|
||||
mKCP Original</a-select-option>
|
||||
<a-select-option v-if="inbound.stream.network === 'kcp'"
|
||||
value="xicmp">
|
||||
xICMP (Experimental)</a-select-option>
|
||||
<!-- xDNS for TCP/WS/HTTPUpgrade/XHTTP/KCP -->
|
||||
<a-select-option
|
||||
v-if="['tcp', 'ws', 'httpupgrade', 'xhttp', 'kcp'].includes(inbound.stream.network)"
|
||||
value="xdns">
|
||||
xDNS (Experimental)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<!-- Settings for password-based masks -->
|
||||
<a-form-item label='Password'
|
||||
v-if="['mkcp-aes128gcm'].includes(mask.type)">
|
||||
<a-input v-model.trim="mask.settings.password"
|
||||
placeholder="Obfuscation password"></a-input>
|
||||
</a-form-item>
|
||||
<!-- Settings for domain-based masks -->
|
||||
<a-form-item label='Domain'
|
||||
v-if="['header-dns', 'xdns'].includes(mask.type)">
|
||||
<a-input v-model.trim="mask.settings.domain"
|
||||
placeholder="e.g., www.example.com"></a-input>
|
||||
</a-form-item>
|
||||
<!-- Settings for xICMP -->
|
||||
<a-form-item label='IP'
|
||||
v-if="mask.type === 'xicmp'">
|
||||
<a-input v-model.trim="mask.settings.ip"
|
||||
placeholder="e.g., 1.1.1.1"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='ID'
|
||||
v-if="mask.type === 'xicmp'">
|
||||
<a-input-number v-model.number="mask.settings.id"
|
||||
:min="0" :max="65535"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
</a-form>
|
||||
{{end}}
|
||||
13
web/html/form/stream/stream_grpc.html
Normal file
13
web/html/form/stream/stream_grpc.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{{define "form/streamGRPC"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Service Name">
|
||||
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Authority">
|
||||
<a-input v-model.trim="inbound.stream.grpc.authority"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Multi Mode">
|
||||
<a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
26
web/html/form/stream/stream_httpupgrade.html
Normal file
26
web/html/form/stream/stream_httpupgrade.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{{define "form/streamHTTPUpgrade"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Proxy Protocol">
|
||||
<a-switch v-model="inbound.stream.httpupgrade.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="inbound.stream.httpupgrade.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.httpupgrade.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
|
||||
<a-button icon="plus" size="small" @click="inbound.stream.httpupgrade.addHeader('', '')"></a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.httpupgrade.headers">
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
|
||||
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.httpupgrade.removeHeader(index)"></a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
32
web/html/form/stream/stream_kcp.html
Normal file
32
web/html/form/stream/stream_kcp.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{{define "form/streamKCP"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='MTU'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.mtu" :min="576"
|
||||
:max="1460"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='TTI (ms)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.tti" :min="10"
|
||||
:max="100"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Uplink (MB/s)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.upCap"
|
||||
:min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Downlink (MB/s)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.downCap"
|
||||
:min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Congestion'>
|
||||
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Read Buffer (MB)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.readBuffer"
|
||||
:min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Write Buffer (MB)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.writeBuffer"
|
||||
:min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
59
web/html/form/stream/stream_settings.html
Normal file
59
web/html/form/stream/stream_settings.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{{define "form/streamSettings"}}
|
||||
<!-- select stream network -->
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "transmission" }}'>
|
||||
<a-select v-model="inbound.stream.network" :style="{ width: '75%' }"
|
||||
@change="streamNetworkChange"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp">TCP (RAW)</a-select-option>
|
||||
<a-select-option value="kcp">mKCP</a-select-option>
|
||||
<a-select-option value="ws">WebSocket</a-select-option>
|
||||
<a-select-option value="grpc">gRPC</a-select-option>
|
||||
<a-select-option value="httpupgrade">HTTPUpgrade</a-select-option>
|
||||
<a-select-option value="xhttp">XHTTP</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- tcp -->
|
||||
<template v-if="inbound.stream.network === 'tcp'">
|
||||
{{template "form/streamTCP"}}
|
||||
</template>
|
||||
|
||||
<!-- kcp -->
|
||||
<template v-if="inbound.stream.network === 'kcp'">
|
||||
{{template "form/streamKCP"}}
|
||||
</template>
|
||||
|
||||
<!-- ws -->
|
||||
<template v-if="inbound.stream.network === 'ws'">
|
||||
{{template "form/streamWS"}}
|
||||
</template>
|
||||
|
||||
<!-- grpc -->
|
||||
<template v-if="inbound.stream.network === 'grpc'">
|
||||
{{template "form/streamGRPC"}}
|
||||
</template>
|
||||
|
||||
<!-- httpupgrade -->
|
||||
<template v-if="inbound.stream.network === 'httpupgrade'">
|
||||
{{template "form/streamHTTPUpgrade"}}
|
||||
</template>
|
||||
|
||||
<!-- xhttp -->
|
||||
<template v-if="inbound.stream.network === 'xhttp'">
|
||||
{{template "form/streamXHTTP"}}
|
||||
</template>
|
||||
|
||||
<!-- sockopt -->
|
||||
<template>
|
||||
{{template "form/streamSockopt"}}
|
||||
</template>
|
||||
|
||||
<!-- finalmask - only for TCP, WS, HTTPUpgrade, XHTTP, mKCP -->
|
||||
<template
|
||||
v-if="['tcp', 'ws', 'httpupgrade', 'xhttp', 'kcp'].includes(inbound.stream.network)">
|
||||
{{template "form/streamFinalMask"}}
|
||||
</template>
|
||||
{{end}}
|
||||
75
web/html/form/stream/stream_sockopt.html
Normal file
75
web/html/form/stream/stream_sockopt.html
Normal file
@@ -0,0 +1,75 @@
|
||||
{{define "form/streamSockopt"}}
|
||||
<a-divider :style="{ margin: '5px 0 0' }"></a-divider>
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Sockopt">
|
||||
<a-switch v-model="inbound.stream.sockoptSwitch"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="inbound.stream.sockoptSwitch">
|
||||
<a-form-item label="Route Mark">
|
||||
<a-input-number v-model.number="inbound.stream.sockopt.mark" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP Keep Alive Interval">
|
||||
<a-input-number v-model.number="inbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP Keep Alive Idle">
|
||||
<a-input-number v-model.number="inbound.stream.sockopt.tcpKeepAliveIdle" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP Max Seg">
|
||||
<a-input-number v-model.number="inbound.stream.sockopt.tcpMaxSeg" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP User Timeout">
|
||||
<a-input-number v-model.number="inbound.stream.sockopt.tcpUserTimeout" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP Window Clamp">
|
||||
<a-input-number v-model.number="inbound.stream.sockopt.tcpWindowClamp" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="Proxy Protocol">
|
||||
<a-switch v-model="inbound.stream.sockopt.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP Fast Open">
|
||||
<a-switch v-model.trim="inbound.stream.sockopt.tcpFastOpen"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Multipath TCP">
|
||||
<a-switch v-model.trim="inbound.stream.sockopt.tcpMptcp"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Penetrate">
|
||||
<a-switch v-model.trim="inbound.stream.sockopt.penetrate"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="V6 Only">
|
||||
<a-switch v-model.trim="inbound.stream.sockopt.V6Only"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Domain Strategy'>
|
||||
<a-select v-model="inbound.stream.sockopt.domainStrategy" :style="{ width: '50%' }" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in DOMAIN_STRATEGY_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='TCP Congestion'>
|
||||
<a-select v-model="inbound.stream.sockopt.tcpcongestion" :style="{ width: '50%' }" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in TCP_CONGESTION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="TProxy">
|
||||
<a-select v-model="inbound.stream.sockopt.tproxy" :style="{ width: '50%' }" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="off">Off</a-select-option>
|
||||
<a-select-option value="redirect">Redirect</a-select-option>
|
||||
<a-select-option value="tproxy">TProxy</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Dialer Proxy">
|
||||
<a-input v-model="inbound.stream.sockopt.dialerProxy"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Interface Name">
|
||||
<a-input v-model="inbound.stream.sockopt.interfaceName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Trusted X-Forwarded-For">
|
||||
<a-select mode="tags" v-model="inbound.stream.sockopt.trustedXForwardedFor" :style="{ width: '100%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
|
||||
<a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
|
||||
<a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
|
||||
<a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
{{end}}
|
||||
72
web/html/form/stream/stream_tcp.html
Normal file
72
web/html/form/stream/stream_tcp.html
Normal file
@@ -0,0 +1,72 @@
|
||||
{{define "form/streamTCP"}}
|
||||
<!-- tcp type -->
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Proxy Protocol" v-if="inbound.canEnableTls()">
|
||||
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='HTTP {{ i18n "camouflage" }}'>
|
||||
<a-switch :checked="inbound.stream.tcp.type === 'http'" @change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<a-form v-if="inbound.stream.tcp.type === 'http'" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<!-- tcp request -->
|
||||
<a-divider :style="{ margin: '0' }">{{ i18n "pages.inbounds.stream.general.request" }}</a-divider>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.method" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">{{ i18n "pages.inbounds.stream.tcp.path" }}
|
||||
<a-button icon="plus" size="small" @click="inbound.stream.tcp.request.addPath('/')"></a-button>
|
||||
</template>
|
||||
<template v-for="(path, index) in inbound.stream.tcp.request.path">
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.path[index]">
|
||||
<a-button icon="minus" size="small" slot="addonAfter" @click="inbound.stream.tcp.request.removePath(index)" v-if="inbound.stream.tcp.request.path.length>1"></a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
|
||||
<a-button icon="plus" size="small" @click="inbound.stream.tcp.request.addHeader('Host', '')"></a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.request.headers">
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
|
||||
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.tcp.request.removeHeader(index)"></a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- tcp response -->
|
||||
<a-divider :style="{ margin: '0' }">{{ i18n "pages.inbounds.stream.general.response" }}</a-divider>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.status" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.status"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.statusDescription" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
|
||||
<a-button icon="plus" size="small" @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')"></a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.response.headers">
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
|
||||
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button icon="minus" size="small" @click="inbound.stream.tcp.response.removeHeader(index)"></a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
29
web/html/form/stream/stream_ws.html
Normal file
29
web/html/form/stream/stream_ws.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{{define "form/streamWS"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Proxy Protocol">
|
||||
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="inbound.stream.ws.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Heartbeat Period'>
|
||||
<a-input-number v-model.number="inbound.stream.ws.heartbeatPeriod" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
|
||||
<a-button icon="plus" size="small" @click="inbound.stream.ws.addHeader('', '')"></a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.ws.headers">
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
|
||||
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.ws.removeHeader(index)"></a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
147
web/html/form/stream/stream_xhttp.html
Normal file
147
web/html/form/stream/stream_xhttp.html
Normal file
@@ -0,0 +1,147 @@
|
||||
{{define "form/streamXHTTP"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="inbound.stream.xhttp.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.xhttp.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
|
||||
<a-button icon="plus" size="small"
|
||||
@click="inbound.stream.xhttp.addHeader('', '')"></a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact
|
||||
v-for="(header, index) in inbound.stream.xhttp.headers">
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="header.name"
|
||||
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
|
||||
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1
|
||||
]]</template>
|
||||
</a-input>
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="header.value"
|
||||
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<a-button icon="minus" slot="addonAfter" size="small"
|
||||
@click="inbound.stream.xhttp.removeHeader(index)"></a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item label='Mode'>
|
||||
<a-select v-model="inbound.stream.xhttp.mode" :style="{ width: '50%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in MODE_OPTION" :value="key">[[ key
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Buffered Upload"
|
||||
v-if="inbound.stream.xhttp.mode === 'packet-up'">
|
||||
<a-input-number
|
||||
v-model.number="inbound.stream.xhttp.scMaxBufferedPosts"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Upload Size (Byte)"
|
||||
v-if="inbound.stream.xhttp.mode === 'packet-up'">
|
||||
<a-input
|
||||
v-model.trim="inbound.stream.xhttp.scMaxEachPostBytes"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Stream-Up Server"
|
||||
v-if="inbound.stream.xhttp.mode === 'stream-up'">
|
||||
<a-input
|
||||
v-model.trim="inbound.stream.xhttp.scStreamUpServerSecs"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Padding Bytes">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.xPaddingBytes"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Padding Obfs Mode">
|
||||
<a-switch v-model="inbound.stream.xhttp.xPaddingObfsMode"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="inbound.stream.xhttp.xPaddingObfsMode">
|
||||
<a-form-item label="Padding Key">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.xPaddingKey"
|
||||
placeholder="x_padding"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Padding Header">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.xPaddingHeader"
|
||||
placeholder="X-Padding"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Padding Placement">
|
||||
<a-select v-model="inbound.stream.xhttp.xPaddingPlacement"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (queryInHeader)</a-select-option>
|
||||
<a-select-option
|
||||
value="queryInHeader">queryInHeader</a-select-option>
|
||||
<a-select-option value="header">header</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Padding Method">
|
||||
<a-select v-model="inbound.stream.xhttp.xPaddingMethod"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (repeat-x)</a-select-option>
|
||||
<a-select-option value="repeat-x">repeat-x</a-select-option>
|
||||
<a-select-option value="tokenish">tokenish</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label="Uplink HTTP Method">
|
||||
<a-select v-model="inbound.stream.xhttp.uplinkHTTPMethod"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (POST)</a-select-option>
|
||||
<a-select-option value="POST">POST</a-select-option>
|
||||
<a-select-option value="PUT">PUT</a-select-option>
|
||||
<a-select-option value="GET">GET (packet-up only)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Session Placement">
|
||||
<a-select v-model="inbound.stream.xhttp.sessionPlacement"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (path)</a-select-option>
|
||||
<a-select-option value="path">path</a-select-option>
|
||||
<a-select-option value="header">header</a-select-option>
|
||||
<a-select-option value="cookie">cookie</a-select-option>
|
||||
<a-select-option value="query">query</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Session Key"
|
||||
v-if="inbound.stream.xhttp.sessionPlacement && inbound.stream.xhttp.sessionPlacement !== 'path'">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.sessionKey"
|
||||
placeholder="x_session"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Sequence Placement">
|
||||
<a-select v-model="inbound.stream.xhttp.seqPlacement"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (path)</a-select-option>
|
||||
<a-select-option value="path">path</a-select-option>
|
||||
<a-select-option value="header">header</a-select-option>
|
||||
<a-select-option value="cookie">cookie</a-select-option>
|
||||
<a-select-option value="query">query</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Sequence Key"
|
||||
v-if="inbound.stream.xhttp.seqPlacement && inbound.stream.xhttp.seqPlacement !== 'path'">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.seqKey"
|
||||
placeholder="x_seq"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Uplink Data Placement"
|
||||
v-if="inbound.stream.xhttp.mode === 'packet-up'">
|
||||
<a-select v-model="inbound.stream.xhttp.uplinkDataPlacement"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (body)</a-select-option>
|
||||
<a-select-option value="body">body</a-select-option>
|
||||
<a-select-option value="header">header</a-select-option>
|
||||
<a-select-option value="query">query</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Uplink Data Key"
|
||||
v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.uplinkDataKey"
|
||||
placeholder="x_data"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Uplink Chunk Size"
|
||||
v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'">
|
||||
<a-input-number v-model.number="inbound.stream.xhttp.uplinkChunkSize"
|
||||
:min="0" placeholder="0 (unlimited)"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="No SSE Header">
|
||||
<a-switch v-model="inbound.stream.xhttp.noSSEHeader"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
150
web/html/form/tls_settings.html
Normal file
150
web/html/form/tls_settings.html
Normal file
@@ -0,0 +1,150 @@
|
||||
{{define "form/tlsSettings"}}
|
||||
<!-- tls enable -->
|
||||
<a-form v-if="inbound.canEnableTls()" :colon="false"
|
||||
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '3px 0' }"></a-divider>
|
||||
<a-form-item label='{{ i18n "security" }}'>
|
||||
<a-radio-group v-model="inbound.stream.security" button-style="solid">
|
||||
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
|
||||
<a-radio-button v-if="inbound.canEnableReality()"
|
||||
value="reality">Reality</a-radio-button>
|
||||
<a-radio-button value="tls">TLS</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- tls settings -->
|
||||
<template v-if="inbound.stream.isTls">
|
||||
<a-form-item label="SNI" placeholder="Server Name Indication">
|
||||
<a-input v-model.trim="inbound.stream.tls.sni"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Cipher Suites">
|
||||
<a-select v-model="inbound.stream.tls.cipherSuites"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Auto</a-select-option>
|
||||
<a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[
|
||||
value ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Min/Max Version">
|
||||
<a-input-group compact>
|
||||
<a-select v-model="inbound.stream.tls.minVersion"
|
||||
:style="{ width: '50%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model="inbound.stream.tls.maxVersion"
|
||||
:style="{ width: '50%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="inbound.stream.tls.settings.fingerprint"
|
||||
:style="{ width: '100%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>None</a-select-option>
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="ALPN">
|
||||
<a-select mode="multiple"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="inbound.stream.tls.alpn">
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Reject Unknown SNI">
|
||||
<a-switch v-model="inbound.stream.tls.rejectUnknownSni"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Disable System Root">
|
||||
<a-switch v-model="inbound.stream.tls.disableSystemRoot"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Session Resumption">
|
||||
<a-switch v-model="inbound.stream.tls.enableSessionResumption"></a-switch>
|
||||
</a-form-item>
|
||||
<a-divider :style="{ margin: '3px 0' }"></a-divider>
|
||||
<template v-for="cert,index in inbound.stream.tls.certs">
|
||||
<a-form-item label='{{ i18n "certificate" }}'>
|
||||
<a-radio-group v-model="cert.useFile" button-style="solid"
|
||||
:style="{ display: 'inline-flex', whiteSpace: 'nowrap', maxWidth: '100%' }">
|
||||
<a-radio-button :value="true"
|
||||
:style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{
|
||||
i18n "pages.inbounds.certificatePath" }}</a-radio-button>
|
||||
<a-radio-button :value="false"
|
||||
:style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{
|
||||
i18n "pages.inbounds.certificateContent" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button icon="plus" v-if="index === 0" type="primary" size="small"
|
||||
@click="inbound.stream.tls.addCert()"></a-button>
|
||||
<a-button icon="minus" v-if="inbound.stream.tls.certs.length>1"
|
||||
type="primary" size="small"
|
||||
@click="inbound.stream.tls.removeCert(index)"></a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
<template v-if="cert.useFile">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-input v-model.trim="cert.certFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
|
||||
<a-input v-model.trim="cert.keyFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" icon="import"
|
||||
@click="setDefaultCertData(index)">
|
||||
{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-textarea v-model="cert.cert"></a-textarea>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
|
||||
<a-textarea v-model="cert.key"></a-textarea>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label="One Time Loading">
|
||||
<a-switch v-model="cert.oneTimeLoading"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Usage Option'>
|
||||
<a-select v-model="cert.usage" :style="{ width: '50%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in USAGE_OPTION" :value="key">[[ key
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Build Chain" v-if="cert.usage === 'issue'">
|
||||
<a-switch v-model="cert.buildChain"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label='ECH key'>
|
||||
<a-input v-model="inbound.stream.tls.echServerKeys"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='ECH config'>
|
||||
<a-input v-model="inbound.stream.tls.settings.echConfigList"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='ECH force query'>
|
||||
<a-select v-model="inbound.stream.tls.echForceQuery"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in ['none', 'half', 'full']" :value="key">[[
|
||||
key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" icon="import" @click="getNewEchCert">Get New ECH Cert</a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- reality settings -->
|
||||
<template v-if="inbound.stream.isReality && !inbound.settings.encryption">
|
||||
{{template "form/realitySettings"}}
|
||||
</template>
|
||||
</a-form>
|
||||
{{end}}
|
||||
1977
web/html/inbounds.html
Normal file
1977
web/html/inbounds.html
Normal file
File diff suppressed because it is too large
Load Diff
1362
web/html/index.html
Normal file
1362
web/html/index.html
Normal file
File diff suppressed because it is too large
Load Diff
644
web/html/login.html
Normal file
644
web/html/login.html
Normal file
@@ -0,0 +1,644 @@
|
||||
{{ template "page/head_start" .}}
|
||||
<style>
|
||||
html * {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
/*margin: 20px 0 50px 0;*/
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
.ant-form-item-children .ant-btn,
|
||||
.ant-input {
|
||||
height: 50px;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.ant-input-group-addon {
|
||||
border-radius: 0 30px 30px 0;
|
||||
width: 50px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.ant-input-affix-wrapper .ant-input-prefix {
|
||||
left: 23px;
|
||||
}
|
||||
|
||||
.ant-input-affix-wrapper .ant-input:not(:first-child) {
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
margin-block-end: 2rem;
|
||||
}
|
||||
|
||||
.title b {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
#app {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#login {
|
||||
animation: charge 0.5s both;
|
||||
background-color: #fff;
|
||||
border-radius: 2rem;
|
||||
padding: 4rem 3rem;
|
||||
transition: all 0.3s;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
#login:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
|
||||
}
|
||||
|
||||
@keyframes charge {
|
||||
from {
|
||||
transform: translateY(5rem);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.under {
|
||||
background-color: #c7ebe2;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.dark .under {
|
||||
background-color: var(--dark-color-login-wave);
|
||||
}
|
||||
|
||||
.dark #login {
|
||||
background-color: var(--dark-color-surface-100);
|
||||
}
|
||||
|
||||
.dark h1 {
|
||||
color: rgba(255, 255, 255);
|
||||
}
|
||||
|
||||
.ant-btn-primary-login {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-btn-primary-login:focus,
|
||||
.ant-btn-primary-login:hover {
|
||||
color: #fff;
|
||||
background-color: #006655;
|
||||
border-color: #006655;
|
||||
background-image: linear-gradient(270deg,
|
||||
rgba(123, 199, 77, 0) 30%,
|
||||
#009980,
|
||||
rgba(123, 199, 77, 0) 100%);
|
||||
background-repeat: no-repeat;
|
||||
animation: ma-bg-move ease-in-out 5s infinite;
|
||||
background-position-x: -500px;
|
||||
width: 95%;
|
||||
animation-delay: -0.5s;
|
||||
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.045);
|
||||
}
|
||||
|
||||
.ant-btn-primary-login.active,
|
||||
.ant-btn-primary-login:active {
|
||||
color: #fff;
|
||||
background-color: #006655;
|
||||
border-color: #006655;
|
||||
}
|
||||
|
||||
@keyframes ma-bg-move {
|
||||
0% {
|
||||
background-position: -500px 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wave-btn-bg {
|
||||
position: relative;
|
||||
border-radius: 25px;
|
||||
width: 100%;
|
||||
transition: all 0.3s cubic-bezier(.645, .045, .355, 1);
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg {
|
||||
color: #fff;
|
||||
position: relative;
|
||||
background-color: #0a7557;
|
||||
border: 2px double transparent;
|
||||
background-origin: border-box;
|
||||
background-clip: padding-box, border-box;
|
||||
background-size: 300%;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg:hover {
|
||||
animation: wave-btn-tara 4s ease infinite;
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg-cl {
|
||||
background-image: linear-gradient(rgba(13, 14, 33, 0), rgba(13, 14, 33, 0)),
|
||||
radial-gradient(circle at left top, #006655, #009980, #006655) !important;
|
||||
border-radius: 3em;
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg-cl:hover {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg-cl:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
bottom: -5px;
|
||||
right: -5px;
|
||||
z-index: -1;
|
||||
background: inherit;
|
||||
background-size: inherit;
|
||||
border-radius: 4em;
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg-cl:hover::before {
|
||||
opacity: 1;
|
||||
filter: blur(20px);
|
||||
animation: wave-btn-tara 8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes wave-btn-tara {
|
||||
to {
|
||||
background-position: 300%;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .ant-btn-primary-login {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-image: linear-gradient(rgba(13, 14, 33, 0.45),
|
||||
rgba(13, 14, 33, 0.35));
|
||||
border-radius: 2rem;
|
||||
border: none;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
height: 46px;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
padding: 0 15px;
|
||||
width: 100%;
|
||||
animation: none;
|
||||
background-position-x: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.waves-header {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
background-color: #dbf5ed;
|
||||
color: white;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.dark .waves-header {
|
||||
background-color: var(--dark-color-login-background);
|
||||
}
|
||||
|
||||
.waves-inner-header {
|
||||
height: 50vh;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.waves {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 15vh;
|
||||
margin-bottom: -8px;
|
||||
/*Fix for safari gap*/
|
||||
min-height: 100px;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.parallax>use {
|
||||
animation: move-forever 25s cubic-bezier(0.55, 0.5, 0.45, 0.5) infinite;
|
||||
}
|
||||
|
||||
.dark .parallax>use {
|
||||
fill: var(--dark-color-login-wave);
|
||||
}
|
||||
|
||||
.parallax>use:nth-child(1) {
|
||||
animation-delay: -2s;
|
||||
animation-duration: 4s;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.parallax>use:nth-child(2) {
|
||||
animation-delay: -3s;
|
||||
animation-duration: 7s;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.parallax>use:nth-child(3) {
|
||||
animation-delay: -4s;
|
||||
animation-duration: 10s;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.parallax>use:nth-child(4) {
|
||||
animation-delay: -5s;
|
||||
animation-duration: 13s;
|
||||
}
|
||||
|
||||
@keyframes move-forever {
|
||||
0% {
|
||||
transform: translate3d(-90px, 0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(85px, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.waves {
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.words-wrapper {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.words-wrapper b {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.words-wrapper b.is-visible {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.headline.zoom .words-wrapper {
|
||||
-webkit-perspective: 300px;
|
||||
-moz-perspective: 300px;
|
||||
perspective: 300px;
|
||||
}
|
||||
|
||||
.headline {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.headline.zoom b {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.headline.zoom b.is-visible {
|
||||
opacity: 1;
|
||||
-webkit-animation: zoom-in 0.8s;
|
||||
-moz-animation: zoom-in 0.8s;
|
||||
animation: cubic-bezier(0.215, 0.610, 0.355, 1.000) zoom-in 0.8s;
|
||||
}
|
||||
|
||||
.headline.zoom b.is-hidden {
|
||||
-webkit-animation: zoom-out 0.8s;
|
||||
-moz-animation: zoom-out 0.8s;
|
||||
animation: cubic-bezier(0.215, 0.610, 0.355, 1.000) zoom-out 0.4s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes zoom-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateZ(100px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateZ(0);
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes zoom-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-moz-transform: translateZ(100px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
-moz-transform: translateZ(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoom-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateZ(100px);
|
||||
-moz-transform: translateZ(100px);
|
||||
-ms-transform: translateZ(100px);
|
||||
-o-transform: translateZ(100px);
|
||||
transform: translateZ(100px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateZ(0);
|
||||
-moz-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
-o-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes zoom-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateZ(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateZ(-100px);
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes zoom-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
-moz-transform: translateZ(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-moz-transform: translateZ(-100px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoom-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateZ(0);
|
||||
-moz-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
-o-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateZ(-100px);
|
||||
-moz-transform: translateZ(-100px);
|
||||
-ms-transform: translateZ(-100px);
|
||||
-o-transform: translateZ(-100px);
|
||||
transform: translateZ(-100px);
|
||||
}
|
||||
}
|
||||
|
||||
.setting-section {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.ant-space-item .ant-switch {
|
||||
margin: 2px 0 4px;
|
||||
}
|
||||
</style>
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
<transition name="list" appear>
|
||||
<a-layout-content class="under" :style="{ minHeight: '0' }">
|
||||
<div class="waves-header">
|
||||
<div class="waves-inner-header"></div>
|
||||
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
|
||||
<defs>
|
||||
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
|
||||
</defs>
|
||||
<g class="parallax">
|
||||
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(0, 135, 113, 0.08)" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(0, 135, 113, 0.08)" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(0, 135, 113, 0.08)" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="7" fill="#c7ebe2" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<a-row type="flex" justify="center" align="middle"
|
||||
:style="{ height: '100%', overflow: 'auto', overflowX: 'hidden' }">
|
||||
<a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" :style="{ margin: '3rem 0' }">
|
||||
<template v-if="!loadingStates.fetched">
|
||||
<div :style="{ textAlign: 'center' }">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="setting-section">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}'
|
||||
placement="bottomRight" trigger="click">
|
||||
<template slot="content">
|
||||
<a-space direction="vertical" :size="10">
|
||||
<a-theme-switch-login></a-theme-switch-login>
|
||||
<span>{{ i18n "pages.settings.language" }}</span>
|
||||
<a-select ref="selectLang" :style="{ width: '100%' }" v-model="lang"
|
||||
@change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
|
||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-button shape="circle" icon="setting"></a-button>
|
||||
</a-popover>
|
||||
</div>
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col :style="{ width: '100%' }">
|
||||
<h2 class="title headline zoom">
|
||||
<span class="words-wrapper">
|
||||
<!-- 第一帧:上下两行组成整体 -->
|
||||
<b class="is-visible">
|
||||
<div>〔X-Panel〕</div>
|
||||
<div>{{ i18n "pages.login.XPanelSystem" }}</div>
|
||||
</b>
|
||||
<!-- 第二帧:单独一行 -->
|
||||
<b>
|
||||
<div>{{ i18n "pages.login.title" }}</div>
|
||||
</b>
|
||||
</span>
|
||||
</h2>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col span="24">
|
||||
<a-form @submit.prevent="login">
|
||||
<a-space direction="vertical" size="middle">
|
||||
<a-form-item>
|
||||
<a-input autocomplete="username" name="username" v-model.trim="user.username"
|
||||
placeholder='{{ i18n "username" }}' autofocus required>
|
||||
<a-icon slot="prefix" type="user" :style="{ fontSize: '1rem' }"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-input-password autocomplete="password" name="password" v-model.trim="user.password"
|
||||
placeholder='{{ i18n "password" }}' required>
|
||||
<a-icon slot="prefix" type="lock" :style="{ fontSize: '1rem' }"></a-icon>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="twoFactorEnable">
|
||||
<a-input autocomplete="one-time-code" name="twoFactorCode" v-model.trim="user.twoFactorCode"
|
||||
placeholder='{{ i18n "twoFactorCode" }}' required>
|
||||
<a-icon slot="prefix" type="key" :style="{ fontSize: '1rem' }"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-row justify="center" class="centered">
|
||||
<div
|
||||
:style="{ height: '50px', marginTop: '1rem', ...loadingStates.spinning ? { width: '52px' } : { display: 'inline-block' } }"
|
||||
class="wave-btn-bg wave-btn-bg-cl">
|
||||
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
|
||||
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
|
||||
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
|
||||
</a-button>
|
||||
</div>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-layout-content>
|
||||
</transition>
|
||||
</a-layout>
|
||||
{{template "page/body_scripts" .}}
|
||||
{{template "component/aThemeSwitch" .}}
|
||||
<script>
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
themeSwitcher,
|
||||
loadingStates: {
|
||||
fetched: false,
|
||||
spinning: false
|
||||
},
|
||||
user: {
|
||||
username: "",
|
||||
password: "",
|
||||
twoFactorCode: ""
|
||||
},
|
||||
twoFactorEnable: false,
|
||||
lang: ""
|
||||
},
|
||||
async mounted() {
|
||||
this.lang = LanguageManager.getLanguage();
|
||||
this.twoFactorEnable = await this.getTwoFactorEnable();
|
||||
},
|
||||
methods: {
|
||||
async login() {
|
||||
this.loadingStates.spinning = true;
|
||||
|
||||
const msg = await HttpUtil.post('/login', this.user);
|
||||
|
||||
if (msg.success) {
|
||||
location.href = basePath + 'panel/';
|
||||
}
|
||||
|
||||
this.loadingStates.spinning = false;
|
||||
},
|
||||
async getTwoFactorEnable() {
|
||||
const msg = await HttpUtil.post('/getTwoFactorEnable');
|
||||
|
||||
if (msg.success) {
|
||||
this.twoFactorEnable = msg.obj;
|
||||
this.loadingStates.fetched = true;
|
||||
|
||||
return msg.obj;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var animationDelay = 2000;
|
||||
initHeadline();
|
||||
|
||||
function initHeadline() {
|
||||
animateHeadline(document.querySelectorAll('.headline'));
|
||||
}
|
||||
|
||||
function animateHeadline(headlines) {
|
||||
var duration = animationDelay;
|
||||
headlines.forEach(function (headline) {
|
||||
setTimeout(function () {
|
||||
hideWord(headline.querySelector('.is-visible'));
|
||||
}, duration);
|
||||
});
|
||||
}
|
||||
|
||||
function hideWord(word) {
|
||||
var nextWord = takeNext(word);
|
||||
switchWord(word, nextWord);
|
||||
setTimeout(function () {
|
||||
hideWord(nextWord);
|
||||
}, animationDelay);
|
||||
}
|
||||
|
||||
function takeNext(word) {
|
||||
return word.nextElementSibling ? word.nextElementSibling : word.parentElement.firstElementChild;
|
||||
}
|
||||
|
||||
function switchWord(oldWord, newWord) {
|
||||
oldWord.classList.remove('is-visible');
|
||||
oldWord.classList.add('is-hidden');
|
||||
newWord.classList.remove('is-hidden');
|
||||
newWord.classList.add('is-visible');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{ template "page/body_end" .}}
|
||||
286
web/html/modals/client_bulk_modal.html
Normal file
286
web/html/modals/client_bulk_modal.html
Normal file
@@ -0,0 +1,286 @@
|
||||
{{define "modals/clientsBulkModal"}}
|
||||
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title"
|
||||
@ok="clientsBulkModal.ok" :confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.client.method" }}'>
|
||||
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="0">Random</a-select-option>
|
||||
<a-select-option :value="1">Random+Prefix</a-select-option>
|
||||
<a-select-option :value="2">Random+Prefix+Num</a-select-option>
|
||||
<a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
|
||||
<a-select-option :value="4">Prefix+Num+Postfix</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.first" }}' v-if="clientsBulkModal.emailMethod>1">
|
||||
<a-input-number v-model.number="clientsBulkModal.firstNum" :min="1"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.last" }}' v-if="clientsBulkModal.emailMethod>1">
|
||||
<a-input-number v-model.number="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.prefix" }}' v-if="clientsBulkModal.emailMethod>0">
|
||||
<a-input v-model.trim="clientsBulkModal.emailPrefix"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.postfix" }}' v-if="clientsBulkModal.emailMethod>2">
|
||||
<a-input v-model.trim="clientsBulkModal.emailPostfix"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.clientCount" }}' v-if="clientsBulkModal.emailMethod < 2">
|
||||
<a-input-number v-model.number="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "security" }}' v-if="inbound.protocol === Protocols.VMESS">
|
||||
<a-select v-model="clientsBulkModal.security" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Flow' v-if="clientsBulkModal.inbound.canEnableTlsFlow()">
|
||||
<a-select v-model="clientsBulkModal.flow" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="app.subSettings?.enable">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
|
||||
</template>
|
||||
Subscription
|
||||
<a-icon @click="clientsBulkModal.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="clientsBulkModal.subId"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 中文注释: 增加【独立限速】 speedLimit 输入框 -->
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.speedLimitDesc" }}</span>
|
||||
</template>
|
||||
<span>
|
||||
{{ i18n "pages.inbounds.speedLimit" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number
|
||||
v-model.number="clientsBulkModal.speedLimit"
|
||||
:min="0"
|
||||
style="width: 100%">
|
||||
<template slot="addonAfter">
|
||||
KB/s
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="app.tgBotEnable">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
|
||||
</template>
|
||||
Telegram ChatID
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number :style="{ width: '50%' }" v-model.number="clientsBulkModal.tgId" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="app.ipLimitEnable">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
|
||||
</template>
|
||||
<span>{{ i18n "pages.inbounds.IPLimit" }} </span>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model.number="clientsBulkModal.limitIp" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.totalFlow" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model.number="clientsBulkModal.totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
|
||||
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientsBulkModal.delayedStart">
|
||||
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item v-else>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.expireDate" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }"
|
||||
format="YYYY-MM-DD HH:mm:ss" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="clientsBulkModal.expiryTime"></a-date-picker>
|
||||
<a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
|
||||
value="clientsBulkModal.expiryTime" v-model="clientsBulkModal.expiryTime">
|
||||
</a-persian-datepicker>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="clientsBulkModal.expiryTime != 0">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.client.renewDesc" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.client.renew" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model.number="clientsBulkModal.reset" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<script>
|
||||
|
||||
const clientsBulkModal = {
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
title: '',
|
||||
okText: '',
|
||||
confirm: null,
|
||||
dbInbound: new DBInbound(),
|
||||
inbound: new Inbound(),
|
||||
quantity: 1,
|
||||
totalGB: 0,
|
||||
limitIp: 0,
|
||||
|
||||
<!-- 中文注释: 在这里为批量添加对象增加 speedLimit 属性 -->
|
||||
speedLimit: 0,
|
||||
|
||||
expiryTime: '',
|
||||
emailMethod: 0,
|
||||
firstNum: 1,
|
||||
lastNum: 1,
|
||||
emailPrefix: "",
|
||||
emailPostfix: "",
|
||||
subId: "",
|
||||
tgId: '',
|
||||
security: "auto",
|
||||
flow: "",
|
||||
delayedStart: false,
|
||||
reset: 0,
|
||||
ok() {
|
||||
clients = [];
|
||||
method = clientsBulkModal.emailMethod;
|
||||
if (method > 1) {
|
||||
start = clientsBulkModal.firstNum;
|
||||
end = clientsBulkModal.lastNum + 1;
|
||||
} else {
|
||||
start = 0;
|
||||
end = clientsBulkModal.quantity;
|
||||
}
|
||||
prefix = (method > 0 && clientsBulkModal.emailPrefix.length > 0) ? clientsBulkModal.emailPrefix : "";
|
||||
useNum = (method > 1);
|
||||
postfix = (method > 2 && clientsBulkModal.emailPostfix.length > 0) ? clientsBulkModal.emailPostfix : "";
|
||||
for (let i = start; i < end; i++) {
|
||||
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
|
||||
if (method == 4) newClient.email = "";
|
||||
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
|
||||
if (clientsBulkModal.subId.length > 0) newClient.subId = clientsBulkModal.subId;
|
||||
newClient.tgId = clientsBulkModal.tgId;
|
||||
|
||||
<!-- 中文注释: 在这里为每个新生成的 client 对象赋值 speedLimit -->
|
||||
newClient.speedLimit = clientsBulkModal.speedLimit;
|
||||
|
||||
newClient.security = clientsBulkModal.security;
|
||||
newClient.limitIp = clientsBulkModal.limitIp;
|
||||
newClient._totalGB = clientsBulkModal.totalGB;
|
||||
newClient._expiryTime = clientsBulkModal.expiryTime;
|
||||
if (clientsBulkModal.inbound.canEnableTlsFlow()) {
|
||||
newClient.flow = clientsBulkModal.flow;
|
||||
}
|
||||
newClient.reset = clientsBulkModal.reset;
|
||||
clients.push(newClient);
|
||||
}
|
||||
ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id);
|
||||
},
|
||||
show({
|
||||
title = '',
|
||||
okText = '{{ i18n "sure" }}',
|
||||
dbInbound = null,
|
||||
confirm = (inbound, dbInbound) => { }
|
||||
}) {
|
||||
this.visible = true;
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
this.confirm = confirm;
|
||||
this.quantity = 1;
|
||||
|
||||
<!-- 中文注释: 在这里重置 speedLimit 的值 -->
|
||||
this.speedLimit = 0;
|
||||
|
||||
this.totalGB = 0;
|
||||
this.expiryTime = 0;
|
||||
this.emailMethod = 0;
|
||||
this.limitIp = 0;
|
||||
this.firstNum = 1;
|
||||
this.lastNum = 1;
|
||||
this.emailPrefix = "";
|
||||
this.emailPostfix = "";
|
||||
this.subId = "";
|
||||
this.tgId = '';
|
||||
this.security = "auto";
|
||||
this.flow = "";
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.delayedStart = false;
|
||||
this.reset = 0;
|
||||
},
|
||||
newClient(protocol) {
|
||||
switch (protocol) {
|
||||
case Protocols.VMESS: return new Inbound.VmessSettings.VMESS();
|
||||
case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
|
||||
case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
|
||||
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks(clientsBulkModal.inbound.settings.shadowsockses[0].method);
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
close() {
|
||||
clientsBulkModal.visible = false;
|
||||
clientsBulkModal.loading(false);
|
||||
},
|
||||
loading(loading = true) {
|
||||
clientsBulkModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
||||
const clientsBulkModalApp = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#client-bulk-modal',
|
||||
data: {
|
||||
clientsBulkModal,
|
||||
get inbound() {
|
||||
return this.clientsBulkModal.inbound;
|
||||
},
|
||||
get delayedExpireDays() {
|
||||
return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 : 0;
|
||||
},
|
||||
get datepicker() {
|
||||
return app.datepicker;
|
||||
},
|
||||
set delayedExpireDays(days) {
|
||||
this.clientsBulkModal.expiryTime = -86400000 * days;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
172
web/html/modals/client_modal.html
Normal file
172
web/html/modals/client_modal.html
Normal file
@@ -0,0 +1,172 @@
|
||||
{{define "modals/clientsModal"}}
|
||||
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
|
||||
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:class="themeSwitcher.currentTheme"
|
||||
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
<template v-if="isEdit">
|
||||
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" :style="{ marginBottom: '10px', display: 'block', textAlign: 'center' }">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
||||
</template>
|
||||
{{template "form/client"}}
|
||||
</a-modal>
|
||||
<script>
|
||||
|
||||
const clientModal = {
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
title: '',
|
||||
okText: '',
|
||||
isEdit: false,
|
||||
dbInbound: new DBInbound(),
|
||||
inbound: new Inbound(),
|
||||
clients: [],
|
||||
clientStats: [],
|
||||
oldClientId: "",
|
||||
index: null,
|
||||
clientIps: null,
|
||||
delayedStart: false,
|
||||
ok() {
|
||||
if (clientModal.isEdit) {
|
||||
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
|
||||
} else {
|
||||
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
|
||||
}
|
||||
},
|
||||
show({ title = '', okText = '{{ i18n "sure" }}', index = null, dbInbound = null, confirm = () => { }, isEdit = false }) {
|
||||
this.visible = true;
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
this.isEdit = isEdit;
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.clients = this.inbound.clients;
|
||||
this.index = index === null ? this.clients.length : index;
|
||||
this.delayedStart = false;
|
||||
if (isEdit) {
|
||||
if (this.clients[index].expiryTime < 0) {
|
||||
this.delayedStart = true;
|
||||
}
|
||||
this.oldClientId = this.getClientId(dbInbound.protocol, clients[index]);
|
||||
} else {
|
||||
this.addClient(this.inbound, this.clients);
|
||||
}
|
||||
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
|
||||
this.confirm = confirm;
|
||||
},
|
||||
getClientId(protocol, client) {
|
||||
switch (protocol) {
|
||||
case Protocols.TROJAN: return client.password;
|
||||
case Protocols.SHADOWSOCKS: return client.email;
|
||||
default: return client.id;
|
||||
}
|
||||
},
|
||||
addClient(inbound, clients) {
|
||||
switch (inbound.protocol) {
|
||||
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.VMESS());
|
||||
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
|
||||
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
|
||||
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method, RandomUtil.randomShadowsocksPassword(inbound.settings.method)));
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
close() {
|
||||
clientModal.visible = false;
|
||||
clientModal.loading(false);
|
||||
},
|
||||
loading(loading=true) {
|
||||
clientModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
||||
const clientModalApp = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#client-modal',
|
||||
data: {
|
||||
clientModal,
|
||||
get inbound() {
|
||||
return this.clientModal.inbound;
|
||||
},
|
||||
get client() {
|
||||
return this.clientModal.clients[this.clientModal.index];
|
||||
},
|
||||
get clientStats() {
|
||||
return this.clientModal.clientStats;
|
||||
},
|
||||
get isEdit() {
|
||||
return this.clientModal.isEdit;
|
||||
},
|
||||
get datepicker() {
|
||||
return app.datepicker;
|
||||
},
|
||||
get isTrafficExhausted() {
|
||||
if (!clientStats) return false
|
||||
if (clientStats.total <= 0) return false
|
||||
if (clientStats.up + clientStats.down < clientStats.total) return false
|
||||
return true
|
||||
},
|
||||
get isExpiry() {
|
||||
return this.clientModal.isEdit && this.client.expiryTime >0 ? (this.client.expiryTime < new Date().getTime()) : false;
|
||||
},
|
||||
get delayedStart() {
|
||||
return this.clientModal.delayedStart;
|
||||
},
|
||||
set delayedStart(value) {
|
||||
this.clientModal.delayedStart = value;
|
||||
},
|
||||
get delayedExpireDays() {
|
||||
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
|
||||
},
|
||||
set delayedExpireDays(days) {
|
||||
this.client.expiryTime = -86400000 * days;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async getDBClientIps(email) {
|
||||
const msg = await HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`);
|
||||
if (!msg.success) {
|
||||
document.getElementById("clientIPs").value = msg.obj;
|
||||
return;
|
||||
}
|
||||
let ips = msg.obj;
|
||||
if (typeof ips === 'string' && ips.startsWith('[') && ips.endsWith(']')) {
|
||||
try {
|
||||
ips = JSON.parse(ips);
|
||||
ips = Array.isArray(ips) ? ips.join("\n") : ips;
|
||||
} catch (e) {
|
||||
console.error('Error parsing JSON:', e);
|
||||
}
|
||||
}
|
||||
document.getElementById("clientIPs").value = ips;
|
||||
},
|
||||
async clearDBClientIps(email) {
|
||||
try {
|
||||
const msg = await HttpUtil.post(`/panel/api/inbounds/clearClientIps/${email}`);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
document.getElementById("clientIPs").value = "";
|
||||
} catch (error) {
|
||||
}
|
||||
},
|
||||
resetClientTraffic(email, dbInboundId, iconElement) {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: async () => {
|
||||
iconElement.disabled = true;
|
||||
const msg = await HttpUtil.postWithModal('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + email);
|
||||
if (msg.success) {
|
||||
this.clientModal.clientStats.up = 0;
|
||||
this.clientModal.clientStats.down = 0;
|
||||
}
|
||||
iconElement.disabled = false;
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
110
web/html/modals/dns_presets_modal.html
Normal file
110
web/html/modals/dns_presets_modal.html
Normal file
@@ -0,0 +1,110 @@
|
||||
{{define "modals/dnsPresetsModal"}}
|
||||
<a-modal id="dnsPresets-modal" v-model="dnsPresetsModal.visible" :title="dnsPresetsModal.title" :closable="true"
|
||||
:mask-closable="false" :footer="null" :class="themeSwitcher.currentTheme">
|
||||
<a-list class="ant-dns-presets-list" bordered :style="{ width: '100%' }">
|
||||
<a-list-item v-for="dns in dnsPresetsDatabase" :style="{ padding: '12px 16px' }">
|
||||
<a-row justify="space-between" align="middle">
|
||||
<a-col :span="12">
|
||||
<a-space direction="vertical" size="small">
|
||||
<span class="ant-dns-presets-list-name">[[ dns.name ]]</span>
|
||||
<a-tag :color="dns.family ? 'purple' : 'green'">[[ dns.family ? '{{ i18n "pages.xray.dns.dnsPresetFamily" }}' : 'DNS' ]]</a-tag>
|
||||
</a-space>
|
||||
</a-col>
|
||||
<a-col :span="12" :style="{ textAlign: 'right' }">
|
||||
<a-button type="primary" @click="dnsPresetsModal.install(dns.data)">{{ i18n "install" }}</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-modal>
|
||||
|
||||
<style>
|
||||
.dark .ant-dns-presets-list {
|
||||
border-color: var(--dark-color-stroke)
|
||||
}
|
||||
|
||||
.dark .ant-dns-presets-list-name {
|
||||
color: var(--dark-color-text-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const dnsPresetsDatabase = [
|
||||
{
|
||||
name: 'Google DNS',
|
||||
family: false,
|
||||
data: [
|
||||
"8.8.8.8",
|
||||
"8.8.4.4",
|
||||
"2001:4860:4860::8888",
|
||||
"2001:4860:4860::8844"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Cloudflare DNS',
|
||||
family: false,
|
||||
data: [
|
||||
"1.1.1.1",
|
||||
"1.0.0.1",
|
||||
"2606:4700:4700::1111",
|
||||
"2606:4700:4700::1001"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Adguard DNS',
|
||||
family: false,
|
||||
data: [
|
||||
"94.140.14.14",
|
||||
"94.140.15.15",
|
||||
"2a10:50c0::ad1:ff",
|
||||
"2a10:50c0::ad2:ff"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Adguard Family DNS',
|
||||
family: true,
|
||||
data: [
|
||||
"94.140.14.14",
|
||||
"94.140.15.15",
|
||||
"2a10:50c0::ad1:ff",
|
||||
"2a10:50c0::ad2:ff"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Cloudflare Family DNS',
|
||||
family: true,
|
||||
data: [
|
||||
"1.1.1.3",
|
||||
"1.0.0.3",
|
||||
"2606:4700:4700::1113",
|
||||
"2606:4700:4700::1003"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const dnsPresetsModal = {
|
||||
title: '',
|
||||
visible: false,
|
||||
selected: null,
|
||||
install(selectedPreset) {
|
||||
return ObjectUtil.execute(dnsPresetsModal.selected, selectedPreset);
|
||||
},
|
||||
show({ title = '', selected = (selectedPreset) => { }, isEdit = false }) {
|
||||
this.title = title;
|
||||
this.selected = selected;
|
||||
this.visible = true;
|
||||
},
|
||||
close() {
|
||||
dnsPresetsModal.visible = false;
|
||||
},
|
||||
};
|
||||
|
||||
new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#dnsPresets-modal',
|
||||
data: {
|
||||
dnsPresetsModal: dnsPresetsModal,
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
628
web/html/modals/inbound_info_modal.html
Normal file
628
web/html/modals/inbound_info_modal.html
Normal file
@@ -0,0 +1,628 @@
|
||||
{{define "modals/inboundInfoModal"}}
|
||||
<a-modal id="inbound-info-modal" v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}' :closable="true" :mask-closable="true" :footer="null" width="600px" :class="themeSwitcher.currentTheme">
|
||||
<a-row>
|
||||
<a-col :xs="24" :md="12">
|
||||
<table>
|
||||
<tr>
|
||||
<td>{{ i18n "protocol" }}</td>
|
||||
<td>
|
||||
<a-tag color="purple">[[ dbInbound.protocol ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.address" }}</td>
|
||||
<td>
|
||||
<a-tooltip :title="[[ dbInbound.address ]]">
|
||||
<a-tag class="info-large-tag">[[ dbInbound.address ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.port" }}</td>
|
||||
<td>
|
||||
<a-tag>[[ dbInbound.port ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
||||
<table>
|
||||
<tr>
|
||||
<td>{{ i18n "transmission" }}</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ inbound.network ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-if="inbound.isTcp || inbound.isWs || inbound.isHttpupgrade || inbound.isXHTTP">
|
||||
<tr>
|
||||
<td>{{ i18n "host" }}</td>
|
||||
<td v-if="inbound.host">
|
||||
<a-tooltip :title="[[ inbound.host ]]">
|
||||
<a-tag class="info-large-tag">[[ inbound.host ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td v-else>
|
||||
<a-tag color="orange">{{ i18n "none" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "path" }}</td>
|
||||
<td v-if="inbound.path">
|
||||
<a-tooltip :title="[[ inbound.path ]]">
|
||||
<a-tag class="info-large-tag">[[ inbound.path ]]</a-tag>
|
||||
</a-tooltip>
|
||||
<td v-else>
|
||||
<a-tag color="orange">{{ i18n "none" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-if="inbound.isXHTTP">
|
||||
<tr>
|
||||
<td>Mode</td>
|
||||
<td>
|
||||
<a-tag>[[ inbound.stream.xhttp.mode ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-if="inbound.isKcp">
|
||||
<tr>
|
||||
<td>kcp {{ i18n "encryption" }}</td>
|
||||
<td>
|
||||
<a-tag>[[ inbound.kcpType ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>kcp {{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-tag>[[ inbound.kcpSeed ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-if="inbound.isGrpc">
|
||||
<tr>
|
||||
<td>grpc serviceName</td>
|
||||
<td>
|
||||
<a-tooltip :title="[[ inbound.serviceName ]]">
|
||||
<a-tag class="info-large-tag">[[ inbound.serviceName ]]</a-tag>
|
||||
</a-tooltip>
|
||||
<tr>
|
||||
<td>grpc multiMode</td>
|
||||
<td>
|
||||
<a-tag>[[ inbound.stream.grpc.multiMode ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
</template>
|
||||
</a-col>
|
||||
<template v-if="dbInbound.hasLink()">
|
||||
{{ i18n "security" }}
|
||||
<a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[ inbound.stream.security ]]</a-tag>
|
||||
<br />
|
||||
<td>Authentication</td>
|
||||
<a-tag :color="inbound.settings.selectedAuth ? 'green' : 'red'">[[ inbound.settings.selectedAuth ? inbound.settings.selectedAuth : '' ]]</a-tag>
|
||||
<br />
|
||||
{{ i18n "encryption" }}
|
||||
<a-tag :color="inbound.settings.encryption ? 'green' : 'red'">[[ inbound.settings.encryption ? inbound.settings.encryption : '' ]]</a-tag>
|
||||
<br />
|
||||
<template v-if="inbound.stream.security != 'none'">
|
||||
{{ i18n "domainName" }}
|
||||
<a-tag v-if="inbound.serverName" :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||
<a-tag v-else color="orange">{{ i18n "none" }}</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<table v-if="dbInbound.isSS" :style="{ marginBottom: '10px', width: '100%' }">
|
||||
<tr>
|
||||
<td>{{ i18n "encryption" }}</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ inbound.settings.method ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.isSS2022">
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-tooltip :title="[[ inbound.settings.password ]]">
|
||||
<a-tag class="info-large-tag">[[ inbound.settings.password ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.network" }}</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ inbound.settings.network ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<template v-if="infoModal.clientSettings">
|
||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||
<table :style="{ marginBottom: '10px' }">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.email" }}</td>
|
||||
<td v-if="infoModal.clientSettings.email">
|
||||
<a-tag color="green">[[ infoModal.clientSettings.email ]]</a-tag>
|
||||
</td>
|
||||
<td v-else>
|
||||
<a-tag color="red">{{ i18n "none" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientSettings.id">
|
||||
<td>ID</td>
|
||||
<td>
|
||||
<a-tag>[[ infoModal.clientSettings.id ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="dbInbound.isVMess">
|
||||
<td>{{ i18n "security" }}</td>
|
||||
<td>
|
||||
<a-tag>[[ infoModal.clientSettings.security ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.inbound.canEnableTlsFlow()">
|
||||
<td>Flow</td>
|
||||
<td v-if="infoModal.clientSettings.flow">
|
||||
<a-tag>[[ infoModal.clientSettings.flow ]]</a-tag>
|
||||
</td>
|
||||
<td v-else>
|
||||
<a-tag color="orange">{{ i18n "none" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientSettings.password">
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-tooltip :title="[[ infoModal.clientSettings.password ]]">
|
||||
<a-tag class="info-large-tag">[[ infoModal.clientSettings.password ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "status" }}</td>
|
||||
<td>
|
||||
<a-tag v-if="isEnable" color="green">{{ i18n "enabled" }}</a-tag>
|
||||
<a-tag v-else>{{ i18n "disabled" }}</a-tag>
|
||||
<a-tag v-if="!isActive" color="red">{{ i18n "depleted" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientStats">
|
||||
<td>{{ i18n "usage" }}</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ SizeFormatter.sizeFormat(infoModal.clientStats.up + infoModal.clientStats.down) ]]</a-tag>
|
||||
<a-tag>↑ [[ SizeFormatter.sizeFormat(infoModal.clientStats.up) ]] / [[ SizeFormatter.sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.createdAt" }}</td>
|
||||
<td>
|
||||
<template v-if="infoModal.clientSettings && infoModal.clientSettings.created_at">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
<a-tag>[[ DateUtil.formatMillis(infoModal.clientSettings.created_at) ]]</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.created_at)) ]]</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>-</a-tag>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.updatedAt" }}</td>
|
||||
<td>
|
||||
<template v-if="infoModal.clientSettings && infoModal.clientSettings.updated_at">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
<a-tag>[[ DateUtil.formatMillis(infoModal.clientSettings.updated_at) ]]</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.updated_at)) ]]</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>-</a-tag>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "lastOnline" }}</td>
|
||||
<td>
|
||||
<a-tag>[[ app.formatLastOnline(infoModal.clientSettings && infoModal.clientSettings.email ? infoModal.clientSettings.email : '') ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientSettings.comment">
|
||||
<td>{{ i18n "comment" }}</td>
|
||||
<td>
|
||||
<a-tooltip :title="[[ infoModal.clientSettings.comment ]]">
|
||||
<a-tag class="info-large-tag">[[ infoModal.clientSettings.comment ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="app.ipLimitEnable">
|
||||
<td>{{ i18n "pages.inbounds.IPLimit" }}</td>
|
||||
<td>
|
||||
<a-tag>[[ infoModal.clientSettings.limitIp ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
|
||||
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
|
||||
<td>
|
||||
<a-tag>[[ infoModal.clientIps ]]</a-tag>
|
||||
<a-icon type="sync" :spin="refreshing" @click="refreshIPs" :style="{ margin: '0 5px' }"></a-icon>
|
||||
<a-tooltip :title="[[ dbInbound.address ]]">
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
|
||||
</template>
|
||||
<a-icon type="delete" @click="clearClientIps"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table :style="{ display: 'inline-table', marginBlock: '10px', width: '100%', textAlign: 'center' }">
|
||||
<tr>
|
||||
<th>{{ i18n "remained" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.totalFlow" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.expireDate" }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a-tag v-if="infoModal.clientStats && infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)"> [[ getRemStats() ]] </a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)"> [[ SizeFormatter.sizeFormat(infoModal.clientSettings.totalGB) ]] </a-tag>
|
||||
<a-tag v-else color="purple" class="infinite-tag">
|
||||
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
|
||||
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="infoModal.clientSettings.expiryTime > 0">
|
||||
<a-tag :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.expiryTime)) ]]
|
||||
</template>
|
||||
</a-tag>
|
||||
</template>
|
||||
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="green">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}
|
||||
</a-tag>
|
||||
<a-tag v-else color="purple" class="infinite-tag">
|
||||
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
|
||||
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
|
||||
<a-divider>Subscription URL</a-divider>
|
||||
<tr-info-row class="tr-info-row">
|
||||
<tr-info-title class="tr-info-title">
|
||||
<a-tag color="purple">Subscription Link</a-tag>
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<a-button size="small" icon="snippets" @click="copy(infoModal.subLink)"></a-button>
|
||||
</a-tooltip>
|
||||
</tr-info-title>
|
||||
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a>
|
||||
</tr-info-row>
|
||||
<tr-info-row class="tr-info-row">
|
||||
<tr-info-title class="tr-info-title">
|
||||
<a-tag color="purple">Json Link</a-tag>
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<a-button size="small" icon="snippets" @click="copy(infoModal.subJsonLink)"></a-button>
|
||||
</a-tooltip>
|
||||
</tr-info-title>
|
||||
<a :href="[[ infoModal.subJsonLink ]]" target="_blank">[[ infoModal.subJsonLink ]]</a>
|
||||
</tr-info-row>
|
||||
</template>
|
||||
<template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
|
||||
<a-divider>Telegram ChatID</a-divider>
|
||||
<tr-info-row class="tr-info-row">
|
||||
<tr-info-title class="tr-info-title">
|
||||
<a-tag color="blue">[[ infoModal.clientSettings.tgId ]]</a-tag>
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<a-button size="small" icon="snippets" @click="copy(infoModal.clientSettings.tgId)"></a-button>
|
||||
</a-tooltip>
|
||||
</tr-info-title>
|
||||
</tr-info-row>
|
||||
</template>
|
||||
<template v-if="dbInbound.hasLink()">
|
||||
<a-divider>URL</a-divider>
|
||||
<tr-info-row v-for="(link,index) in infoModal.links" class="tr-info-row">
|
||||
<tr-info-title class="tr-info-title">
|
||||
<a-tag class="tr-info-tag" color="green">[[ link.remark ]]</a-tag>
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<a-button :style="{ minWidth: '24px' }" size="small" icon="snippets" @click="copy(link.link)"></a-button>
|
||||
</a-tooltip>
|
||||
</tr-info-title>
|
||||
<code>[[ link.link ]]</code>
|
||||
</tr-info-row>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="dbInbound.isSS && !inbound.isSSMultiUser">
|
||||
<a-divider>URL</a-divider>
|
||||
<tr-info-row v-for="(link,index) in infoModal.links" class="tr-info-row">
|
||||
<tr-info-title class="tr-info-title">
|
||||
<a-tag class="tr-info-tag" color="green">[[ link.remark ]]</a-tag>
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<a-button :style="{ minWidth: '24px' }" size="small" icon="snippets" @click="copy(link.link)"></a-button>
|
||||
</a-tooltip>
|
||||
</tr-info-title>
|
||||
<code>[[ link.link ]]</code>
|
||||
</tr-info-row>
|
||||
</template>
|
||||
<table v-if="inbound.protocol == Protocols.TUNNEL" class="tr-info-table">
|
||||
<tr>
|
||||
<th>{{ i18n "pages.inbounds.targetAddress" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.destinationPort" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.network" }}</th>
|
||||
<th>FollowRedirect</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a-tag color="green">[[ inbound.settings.address ]]</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ inbound.settings.port ]]</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ inbound.settings.network ]]</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ inbound.settings.followRedirect ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table v-if="dbInbound.isSocks" class="tr-info-table">
|
||||
<tr>
|
||||
<th>{{ i18n "password" }} Auth</th>
|
||||
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
|
||||
<th>IP</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a-tag color="green">[[ inbound.settings.auth ]]</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ inbound.settings.udp]]</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ inbound.settings.ip ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-if="inbound.settings.auth == 'password'">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{{ i18n "username" }}</td>
|
||||
<td>{{ i18n "password" }}</td>
|
||||
</tr>
|
||||
<tr v-for="account,index in inbound.settings.accounts">
|
||||
<td>[[ index ]]</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ account.user ]]</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ account.pass ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
<table v-if="dbInbound.isHTTP" class="tr-info-table">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ i18n "username" }}</th>
|
||||
<th>{{ i18n "password" }}</th>
|
||||
</tr>
|
||||
<tr v-for="account,index in inbound.settings.accounts">
|
||||
<td>[[ index ]]</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ account.user ]]</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ account.pass ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table v-if="dbInbound.isWireguard" class="tr-info-table">
|
||||
<tr class="client-table-odd-row">
|
||||
<td>{{ i18n "pages.xray.wireguard.secretKey" }}</td>
|
||||
<td>[[ inbound.settings.secretKey ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.xray.wireguard.publicKey" }}</td>
|
||||
<td>[[ inbound.settings.pubKey ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>MTU</td>
|
||||
<td>[[ inbound.settings.mtu ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>No Kernel Tun</td>
|
||||
<td>[[ inbound.settings.noKernelTun ]]</td>
|
||||
</tr>
|
||||
<template v-for="(peer, index) in inbound.settings.peers">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a-divider>Peer [[ index + 1 ]]</a-divider>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>{{ i18n "pages.xray.wireguard.secretKey" }}</td>
|
||||
<td>[[ peer.privateKey ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.xray.wireguard.publicKey" }}</td>
|
||||
<td>[[ peer.publicKey ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>{{ i18n "pages.xray.wireguard.psk" }}</td>
|
||||
<td>[[ peer.psk ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.xray.wireguard.allowedIPs" }}</td>
|
||||
<td>[[ peer.allowedIPs.join(",") ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>Keep Alive</td>
|
||||
<td>[[ peer.keepAlive ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<tr-info-row class="tr-info-row">
|
||||
<tr-info-title class="tr-info-title">
|
||||
<a-tag color="blue">Config</a-tag>
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<a-button :style="{ minWidth: '24px' }" size="small" icon="snippets" @click="copy(infoModal.links[index])"></a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title='{{ i18n "download" }}'>
|
||||
<a-button :style="{ minWidth: '24px' }" size="small" icon="download" @click="FileManager.downloadTextFile(infoModal.links[index], `peer-${index + 1}.conf`)"></a-button>
|
||||
</a-tooltip>
|
||||
</tr-info-title>
|
||||
<div v-html="infoModal.links[index].replaceAll(`\n`,`<br />`)" :style="{ borderRadius: '1rem', padding: '0.5rem' }" class="client-table-odd-row">
|
||||
</div>
|
||||
</tr-info-row>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
</template>
|
||||
</a-modal>
|
||||
<script>
|
||||
function refreshIPs(email) {
|
||||
return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
|
||||
if (msg.success) {
|
||||
try {
|
||||
return JSON.parse(msg.obj).join(', ');
|
||||
} catch (e) {
|
||||
return msg.obj;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const infoModal = {
|
||||
visible: false,
|
||||
inbound: new Inbound(),
|
||||
dbInbound: new DBInbound(),
|
||||
clientSettings: null,
|
||||
clientStats: [],
|
||||
upStats: 0,
|
||||
downStats: 0,
|
||||
links: [],
|
||||
index: null,
|
||||
isExpired: false,
|
||||
subLink: '',
|
||||
subJsonLink: '',
|
||||
clientIps: '',
|
||||
show(dbInbound, index) {
|
||||
this.index = index;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
|
||||
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry;
|
||||
this.clientStats = this.inbound.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
|
||||
|
||||
if (
|
||||
[
|
||||
Protocols.VMESS,
|
||||
Protocols.VLESS,
|
||||
Protocols.TROJAN,
|
||||
Protocols.SHADOWSOCKS
|
||||
].includes(this.inbound.protocol)
|
||||
) {
|
||||
if (app.ipLimitEnable && this.clientSettings.limitIp) {
|
||||
refreshIPs(this.clientStats.email).then((ips) => {
|
||||
this.clientIps = ips;
|
||||
})
|
||||
}
|
||||
}
|
||||
if (this.inbound.protocol == Protocols.WIREGUARD) {
|
||||
this.links = this.inbound.genInboundLinks(dbInbound.remark).split('\r\n')
|
||||
} else {
|
||||
this.links = this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, this.clientSettings);
|
||||
}
|
||||
if (this.clientSettings) {
|
||||
if (this.clientSettings.subId) {
|
||||
this.subLink = this.genSubLink(this.clientSettings.subId);
|
||||
this.subJsonLink = this.genSubJsonLink(this.clientSettings.subId);
|
||||
}
|
||||
}
|
||||
this.visible = true;
|
||||
},
|
||||
close() {
|
||||
infoModal.visible = false;
|
||||
},
|
||||
genSubLink(subID) {
|
||||
return app.subSettings.subURI + subID;
|
||||
},
|
||||
genSubJsonLink(subID) {
|
||||
return app.subSettings.subJsonURI + subID;
|
||||
}
|
||||
};
|
||||
const infoModalApp = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#inbound-info-modal',
|
||||
data: {
|
||||
infoModal,
|
||||
refreshing: false,
|
||||
get dbInbound() {
|
||||
return this.infoModal.dbInbound;
|
||||
},
|
||||
get inbound() {
|
||||
return this.infoModal.inbound;
|
||||
},
|
||||
get isActive() {
|
||||
if (infoModal.clientStats) {
|
||||
return infoModal.clientStats.enable;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
get isEnable() {
|
||||
if (infoModal.clientSettings) {
|
||||
return infoModal.clientSettings.enable;
|
||||
}
|
||||
return infoModal.dbInbound.isEnable;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
copy(content) {
|
||||
ClipboardManager
|
||||
.copyText(content)
|
||||
.then(() => {
|
||||
app.$message.success('{{ i18n "copied" }}')
|
||||
})
|
||||
},
|
||||
statsColor(stats) {
|
||||
return ColorUtils.usageColor(stats.up + stats.down, app.trafficDiff, stats.total);
|
||||
},
|
||||
getRemStats() {
|
||||
remained = this.infoModal.clientStats.total - this.infoModal.clientStats.up - this.infoModal.clientStats.down;
|
||||
return remained > 0 ? SizeFormatter.sizeFormat(remained) : '-';
|
||||
},
|
||||
refreshIPs() {
|
||||
this.refreshing = true;
|
||||
refreshIPs(this.infoModal.clientStats.email)
|
||||
.then((ips) => {
|
||||
this.infoModal.clientIps = ips;
|
||||
})
|
||||
.finally(() => {
|
||||
this.refreshing = false;
|
||||
});
|
||||
},
|
||||
clearClientIps() {
|
||||
HttpUtil.post(`/panel/api/inbounds/clearClientIps/${this.infoModal.clientStats.email}`)
|
||||
.then((msg) => {
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
this.infoModal.clientIps = 'No IP Record';
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
314
web/html/modals/inbound_modal.html
Normal file
314
web/html/modals/inbound_modal.html
Normal file
@@ -0,0 +1,314 @@
|
||||
{{define "modals/inboundModal"}}
|
||||
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" :dialog-style="{ top: '20px' }"
|
||||
@ok="inModal.ok" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:class="themeSwitcher.currentTheme" :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
{{template "form/inbound"}}
|
||||
</a-modal>
|
||||
<script>
|
||||
|
||||
// 将 inModal 设为全局可用,确保其在任何基础路径下都能工作
|
||||
const inModal = window.inModal = {
|
||||
title: '',
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
okText: '{{ i18n "sure" }}',
|
||||
isEdit: false,
|
||||
confirm: null,
|
||||
inbound: new Inbound(),
|
||||
dbInbound: new DBInbound(),
|
||||
ok() {
|
||||
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
|
||||
},
|
||||
show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => { }, isEdit = false }) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
if (inbound) {
|
||||
this.inbound = Inbound.fromJson(inbound.toJson());
|
||||
} else {
|
||||
this.inbound = new Inbound();
|
||||
}
|
||||
// 始终确保为 VLESS 协议初始化 testseed(即使尚未设置 vision 流)
|
||||
// 这确保了 Vue 的响应式系统能正常工作
|
||||
if (this.inbound.protocol === Protocols.VLESS && this.inbound.settings) {
|
||||
if (!this.inbound.settings.testseed || !Array.isArray(this.inbound.settings.testseed) || this.inbound.settings.testseed.length < 4) {
|
||||
// 创建新数组以确保 Vue 响应式
|
||||
this.inbound.settings.testseed = [900, 500, 900, 256].slice();
|
||||
}
|
||||
}
|
||||
if (dbInbound) {
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
} else {
|
||||
this.dbInbound = new DBInbound();
|
||||
}
|
||||
this.confirm = confirm;
|
||||
this.visible = true;
|
||||
this.isEdit = isEdit;
|
||||
},
|
||||
close() {
|
||||
inModal.visible = false;
|
||||
inModal.loading(false);
|
||||
},
|
||||
loading(loading = true) {
|
||||
inModal.confirmLoading = loading;
|
||||
},
|
||||
|
||||
// Vision Seed 方法 - 无论 Vue 上下文如何均可用
|
||||
updateTestseed(index, value) {
|
||||
if (!inModal.inbound || !inModal.inbound.settings) return;
|
||||
if (!inModal.inbound.settings.testseed || !Array.isArray(inModal.inbound.settings.testseed)) {
|
||||
inModal.inbound.settings.testseed = [900, 500, 900, 256];
|
||||
}
|
||||
while (inModal.inbound.settings.testseed.length <= index) {
|
||||
inModal.inbound.settings.testseed.push(0);
|
||||
}
|
||||
inModal.inbound.settings.testseed[index] = value;
|
||||
},
|
||||
|
||||
/**
|
||||
* 优化后的随机 Vision Seed 生成函数 (inModal 全局对象版)
|
||||
*/
|
||||
setRandomTestseed() {
|
||||
// 确保对象存在
|
||||
if (!inModal.inbound || !inModal.inbound.settings) return;
|
||||
|
||||
// 确保 testseed 已初始化为数组
|
||||
if (!inModal.inbound.settings.testseed || !Array.isArray(inModal.inbound.settings.testseed) || inModal.inbound.settings.testseed.length < 4) {
|
||||
inModal.inbound.settings.testseed = [900, 500, 900, 256].slice();
|
||||
}
|
||||
|
||||
// 1. 生成 [0] 大包阈值 (500-1000)
|
||||
let threshold = 500 + Math.floor(Math.random() * 501);
|
||||
// 2. 生成 [1] 大包浮动 (0-500)
|
||||
let largeVar = Math.floor(Math.random() * 501);
|
||||
// 3. 生成 [2] 大包目标最低长度 (必须 >= threshold)
|
||||
let targetLength = threshold + Math.floor(Math.random() * 201);
|
||||
// 4. 生成 [3] 小包浮动 (0-300)
|
||||
let smallVar = Math.floor(Math.random() * 301);
|
||||
|
||||
inModal.inbound.settings.testseed = [threshold, largeVar, targetLength, smallVar];
|
||||
},
|
||||
|
||||
resetTestseed() {
|
||||
if (!inModal.inbound || !inModal.inbound.settings) return;
|
||||
inModal.inbound.settings.testseed = [900, 500, 900, 256].slice();
|
||||
}
|
||||
};
|
||||
|
||||
// 全局存储 Vue 实例以确保方法始终可访问
|
||||
let inboundModalVueInstance = null;
|
||||
|
||||
inboundModalVueInstance = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#inbound-modal',
|
||||
data: {
|
||||
inModal: inModal,
|
||||
delayedStart: false,
|
||||
get inbound() {
|
||||
return inModal.inbound;
|
||||
},
|
||||
get dbInbound() {
|
||||
return inModal.dbInbound;
|
||||
},
|
||||
get isEdit() {
|
||||
return inModal.isEdit;
|
||||
},
|
||||
get client() {
|
||||
return inModal.inbound && inModal.inbound.clients && inModal.inbound.clients.length > 0 ? inModal.inbound.clients[0] : null;
|
||||
},
|
||||
get datepicker() {
|
||||
return app.datepicker;
|
||||
},
|
||||
get delayedExpireDays() {
|
||||
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
|
||||
},
|
||||
set delayedExpireDays(days) {
|
||||
this.client.expiryTime = -86400000 * days;
|
||||
},
|
||||
get externalProxy() {
|
||||
return this.inbound.stream.externalProxy.length > 0;
|
||||
},
|
||||
set externalProxy(value) {
|
||||
if (value) {
|
||||
inModal.inbound.stream.externalProxy = [{
|
||||
forceTls: "same",
|
||||
dest: window.location.hostname,
|
||||
port: inModal.inbound.port,
|
||||
remark: ""
|
||||
}];
|
||||
} else {
|
||||
inModal.inbound.stream.externalProxy = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'inModal.inbound.stream.security'(newVal, oldVal) {
|
||||
// 当安全设置从 reality/tls 更改为 none 时清除 flow
|
||||
if (inModal.inbound.protocol == Protocols.VLESS && !inModal.inbound.canEnableTlsFlow()) {
|
||||
inModal.inbound.settings.vlesses.forEach(client => {
|
||||
client.flow = "";
|
||||
});
|
||||
}
|
||||
},
|
||||
// 确保启用 vision 流时始终初始化 testseed
|
||||
'inModal.inbound.settings.vlesses': {
|
||||
handler() {
|
||||
if (inModal.inbound.protocol === Protocols.VLESS && inModal.inbound.settings && inModal.inbound.settings.vlesses) {
|
||||
const hasVisionFlow = inModal.inbound.settings.vlesses.some(c => c.flow === 'xtls-rprx-vision' || c.flow === 'xtls-rprx-vision-udp443');
|
||||
if (hasVisionFlow && (!inModal.inbound.settings.testseed || !Array.isArray(inModal.inbound.settings.testseed) || inModal.inbound.settings.testseed.length < 4)) {
|
||||
inModal.inbound.settings.testseed = [900, 500, 900, 256];
|
||||
}
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
streamNetworkChange() {
|
||||
if (!inModal.inbound.canEnableTls()) {
|
||||
this.inModal.inbound.stream.security = 'none';
|
||||
}
|
||||
if (!inModal.inbound.canEnableReality()) {
|
||||
this.inModal.inbound.reality = false;
|
||||
}
|
||||
if (this.inModal.inbound.protocol == Protocols.VLESS && !inModal.inbound.canEnableTlsFlow()) {
|
||||
this.inModal.inbound.settings.vlesses.forEach(client => {
|
||||
client.flow = "";
|
||||
});
|
||||
}
|
||||
},
|
||||
SSMethodChange() {
|
||||
this.inModal.inbound.settings.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
|
||||
|
||||
if (this.inModal.inbound.isSSMultiUser) {
|
||||
if (this.inModal.inbound.settings.shadowsockses.length == 0) {
|
||||
this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
|
||||
}
|
||||
if (!this.inModal.inbound.isSS2022) {
|
||||
this.inModal.inbound.settings.shadowsockses.forEach(client => {
|
||||
client.method = this.inModal.inbound.settings.method;
|
||||
})
|
||||
} else {
|
||||
this.inModal.inbound.settings.shadowsockses.forEach(client => {
|
||||
client.method = "";
|
||||
})
|
||||
}
|
||||
this.inModal.inbound.settings.shadowsockses.forEach(client => {
|
||||
client.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
|
||||
})
|
||||
} else {
|
||||
if (this.inModal.inbound.settings.shadowsockses.length > 0) {
|
||||
this.inModal.inbound.settings.shadowsockses = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
setDefaultCertData(index) {
|
||||
inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert;
|
||||
inModal.inbound.stream.tls.certs[index].keyFile = app.defaultKey;
|
||||
},
|
||||
async getNewX25519Cert() {
|
||||
inModal.loading(true);
|
||||
const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
|
||||
inModal.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
|
||||
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
|
||||
},
|
||||
async getNewmldsa65() {
|
||||
inModal.loading(true);
|
||||
const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
|
||||
inModal.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed;
|
||||
inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify;
|
||||
},
|
||||
async getNewEchCert() {
|
||||
inModal.loading(true);
|
||||
const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni: inModal.inbound.stream.tls.sni });
|
||||
inModal.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
inModal.inbound.stream.tls.echServerKeys = msg.obj.echServerKeys;
|
||||
inModal.inbound.stream.tls.settings.echConfigList = msg.obj.echConfigList;
|
||||
},
|
||||
async getNewVlessEnc() {
|
||||
inModal.loading(true);
|
||||
const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
|
||||
inModal.loading(false);
|
||||
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auths = msg.obj.auths || [];
|
||||
const selected = inModal.inbound.settings.selectedAuth;
|
||||
const block = auths.find(a => a.label === selected);
|
||||
|
||||
if (!block) {
|
||||
console.error("No auth block for", selected);
|
||||
return;
|
||||
}
|
||||
|
||||
inModal.inbound.settings.decryption = block.decryption;
|
||||
inModal.inbound.settings.encryption = block.encryption;
|
||||
},
|
||||
clearKeys() {
|
||||
this.inbound.settings.decryption = 'none';
|
||||
this.inbound.settings.encryption = '';
|
||||
},
|
||||
|
||||
// Vision Seed 方法 - 必须在 Vue methods 中以进行正确的绑定
|
||||
updateTestseed(index, value) {
|
||||
if (!this.inbound.settings.testseed || !Array.isArray(this.inbound.settings.testseed)) {
|
||||
this.$set(this.inbound.settings, 'testseed', [900, 500, 900, 256]);
|
||||
}
|
||||
while (this.inbound.settings.testseed.length <= index) {
|
||||
this.inbound.settings.testseed.push(0);
|
||||
}
|
||||
this.$set(this.inbound.settings.testseed, index, value);
|
||||
},
|
||||
|
||||
/**
|
||||
* 优化后的随机 Vision Seed 生成函数 (Vue 实例版)
|
||||
*/
|
||||
setRandomTestseed() {
|
||||
// 确保对象存在
|
||||
if (!this.inbound || !this.inbound.settings) return;
|
||||
|
||||
// 1. 生成 [0] 大包阈值
|
||||
// 建议范围:500 - 1000 (避免太小导致所有包都被当做大包)
|
||||
let threshold = 500 + Math.floor(Math.random() * 501);
|
||||
|
||||
// 2. 生成 [1] 大包浮动
|
||||
// 建议范围:0 - 500
|
||||
let largeVar = Math.floor(Math.random() * 501);
|
||||
|
||||
// 3. 生成 [2] 大包目标最低长度
|
||||
// 逻辑:必须 >= threshold。在 threshold 基础上增加 0-200 的随机量
|
||||
// 这样永远保证 seed[2] >= seed[0],避免内核逻辑崩溃
|
||||
let targetLength = threshold + Math.floor(Math.random() * 201);
|
||||
|
||||
// 4. 生成 [3] 小包浮动
|
||||
// 建议范围:0 - 300
|
||||
let smallVar = Math.floor(Math.random() * 301);
|
||||
|
||||
// 使用 Vue.set 确保 UI 响应式更新
|
||||
this.$set(this.inbound.settings, 'testseed', [threshold, largeVar, targetLength, smallVar]);
|
||||
},
|
||||
|
||||
resetTestseed() {
|
||||
// 使用 Vue.set 重置为默认值
|
||||
this.$set(this.inbound.settings, 'testseed', [900, 500, 900, 256]);
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
71
web/html/modals/prompt_modal.html
Normal file
71
web/html/modals/prompt_modal.html
Normal file
@@ -0,0 +1,71 @@
|
||||
{{define "modals/promptModal"}}
|
||||
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
|
||||
:closable="true" @ok="promptModal.ok" :mask-closable="false"
|
||||
:confirm-loading="promptModal.confirmLoading"
|
||||
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}' :class="themeSwitcher.currentTheme">
|
||||
<a-input id="prompt-modal-input" :type="promptModal.type"
|
||||
v-model="promptModal.value"
|
||||
:autosize="{minRows: 10, maxRows: 20}"
|
||||
@keydown.enter.native="promptModal.keyEnter"
|
||||
@keydown.ctrl.83="promptModal.ctrlS"></a-input>
|
||||
</a-modal>
|
||||
|
||||
<script>
|
||||
|
||||
const promptModal = {
|
||||
title: '',
|
||||
type: '',
|
||||
value: '',
|
||||
okText: '{{ i18n "sure"}}',
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
keyEnter(e) {
|
||||
if (this.type !== 'textarea') {
|
||||
e.preventDefault();
|
||||
this.ok();
|
||||
}
|
||||
},
|
||||
ctrlS(e) {
|
||||
if (this.type === 'textarea') {
|
||||
e.preventDefault();
|
||||
promptModal.confirm(promptModal.value);
|
||||
}
|
||||
},
|
||||
ok() {
|
||||
promptModal.confirm(promptModal.value);
|
||||
},
|
||||
confirm() {},
|
||||
open({
|
||||
title = '',
|
||||
type = 'text',
|
||||
value = '',
|
||||
okText = '{{ i18n "sure"}}',
|
||||
confirm = () => {},
|
||||
}) {
|
||||
this.title = title;
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
this.okText = okText;
|
||||
this.confirm = confirm;
|
||||
this.visible = true;
|
||||
promptModalApp.$nextTick(() => {
|
||||
document.querySelector('#prompt-modal-input').focus();
|
||||
});
|
||||
},
|
||||
close() {
|
||||
this.visible = false;
|
||||
},
|
||||
loading(loading=true) {
|
||||
this.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
||||
const promptModalApp = new Vue({
|
||||
el: '#prompt-modal',
|
||||
data: {
|
||||
promptModal: promptModal,
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
304
web/html/modals/qrcode_modal.html
Normal file
304
web/html/modals/qrcode_modal.html
Normal file
@@ -0,0 +1,304 @@
|
||||
{{define "modals/qrcodeModal"}}
|
||||
<a-modal id="qrcode-modal" v-model="qrModal.visible" :closable="true" :class="themeSwitcher.currentTheme"
|
||||
width="fit-content" :dialog-style="isMobile ? { top: '18px' } : {}" :footer="null">
|
||||
<template #title>
|
||||
<a-space direction="horizontal">
|
||||
<span>[[ qrModal.title ]]</span>
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme" trigger="click" placement="bottom">
|
||||
<template slot="content">
|
||||
<a-space direction="vertical">
|
||||
<template v-for="(row, index) in qrModal.qrcodes">
|
||||
<b>[[ row.remark ]]</b>
|
||||
<a-space direction="horizontal">
|
||||
<a-switch size="small" :checked="row.useIPv4" @click="toggleIPv4(index)"></a-switch>
|
||||
<span>{{ i18n "useIPv4ForHost" }}</span>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-icon type="setting"></a-icon>
|
||||
</a-popover>
|
||||
</a-space>
|
||||
</template>
|
||||
<tr-qr-modal class="qr-modal">
|
||||
<template v-if="app.subSettings?.enable && qrModal.subId">
|
||||
<tr-qr-box class="qr-box">
|
||||
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}}</span></a-tag>
|
||||
<tr-qr-bg class="qr-bg-sub">
|
||||
<tr-qr-bg-inner class="qr-bg-sub-inner">
|
||||
<canvas @click="copy(genSubLink(qrModal.client.subId))" id="qrCode-sub" class="qr-cv"></canvas>
|
||||
</tr-qr-bg-inner>
|
||||
</tr-qr-bg>
|
||||
</tr-qr-box>
|
||||
<tr-qr-box class="qr-box">
|
||||
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}} Json</span></a-tag>
|
||||
<tr-qr-bg class="qr-bg-sub">
|
||||
<tr-qr-bg-inner class="qr-bg-sub-inner">
|
||||
<canvas @click="copy(genSubJsonLink(qrModal.client.subId))" id="qrCode-subJson" class="qr-cv"></canvas>
|
||||
</tr-qr-bg-inner>
|
||||
</tr-qr-bg>
|
||||
</tr-qr-box>
|
||||
</template>
|
||||
<template v-for="(row, index) in qrModal.qrcodes">
|
||||
<tr-qr-box class="qr-box">
|
||||
<a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
|
||||
<tr-qr-bg class="qr-bg">
|
||||
<canvas @click="copy(row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
|
||||
</tr-qr-bg>
|
||||
</tr-qr-box>
|
||||
</template>
|
||||
</tr-qr-modal>
|
||||
</a-modal>
|
||||
|
||||
<style>
|
||||
.ant-table:not(.ant-table-expanded-row .ant-table) {
|
||||
outline: 1px solid #f0f0f0;
|
||||
outline-offset: -1px;
|
||||
border-radius: 1rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* QR code transition effects */
|
||||
.qr-cv {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.qr-transition-enter-active, .qr-transition-leave-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
|
||||
.qr-transition-enter, .qr-transition-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.qr-transition-enter-to, .qr-transition-leave {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.qr-flash {
|
||||
animation: qr-flash-animation 0.6s;
|
||||
}
|
||||
|
||||
@keyframes qr-flash-animation {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const qrModal = {
|
||||
title: '',
|
||||
dbInbound: new DBInbound(),
|
||||
client: null,
|
||||
qrcodes: [],
|
||||
visible: false,
|
||||
subId: '',
|
||||
show: function (title = '', dbInbound, client) {
|
||||
this.title = title;
|
||||
this.dbInbound = dbInbound;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.client = client;
|
||||
this.subId = '';
|
||||
this.qrcodes = [];
|
||||
// Reset the status fetched flag when showing the modal
|
||||
if (qrModalApp) qrModalApp.statusFetched = false;
|
||||
if (this.inbound.protocol == Protocols.WIREGUARD) {
|
||||
this.inbound.genInboundLinks(dbInbound.remark).split('\r\n').forEach((l, index) => {
|
||||
this.qrcodes.push({
|
||||
remark: "Peer " + (index + 1),
|
||||
link: l,
|
||||
useIPv4: false,
|
||||
originalLink: l
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
|
||||
this.qrcodes.push({
|
||||
remark: l.remark,
|
||||
link: l.link,
|
||||
useIPv4: false,
|
||||
originalLink: l.link
|
||||
});
|
||||
});
|
||||
}
|
||||
this.visible = true;
|
||||
},
|
||||
close: function () {
|
||||
this.visible = false;
|
||||
},
|
||||
};
|
||||
const qrModalApp = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#qrcode-modal',
|
||||
mixins: [MediaQueryMixin],
|
||||
data: {
|
||||
qrModal: qrModal,
|
||||
serverStatus: null,
|
||||
statusFetched: false,
|
||||
},
|
||||
methods: {
|
||||
async getStatus() {
|
||||
try {
|
||||
const msg = await HttpUtil.get('/panel/api/server/status');
|
||||
if (msg.success) {
|
||||
this.serverStatus = msg.obj;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to get status:", e);
|
||||
}
|
||||
},
|
||||
|
||||
toggleIPv4(index) {
|
||||
const row = qrModal.qrcodes[index];
|
||||
row.useIPv4 = !row.useIPv4;
|
||||
this.updateLink(index);
|
||||
},
|
||||
updateLink(index) {
|
||||
const row = qrModal.qrcodes[index];
|
||||
if (!this.serverStatus || !this.serverStatus.publicIP) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (row.useIPv4 && this.serverStatus.publicIP.ipv4) {
|
||||
// Replace the hostname or IP in the link with the IPv4 address
|
||||
const originalLink = row.originalLink;
|
||||
const url = new URL(originalLink);
|
||||
const ipv4 = this.serverStatus.publicIP.ipv4;
|
||||
|
||||
if (qrModal.inbound.protocol == Protocols.WIREGUARD) {
|
||||
// Special handling for WireGuard config
|
||||
const endpointRegex = /Endpoint = ([^:]+):(\d+)/;
|
||||
const match = originalLink.match(endpointRegex);
|
||||
if (match) {
|
||||
row.link = originalLink.replace(
|
||||
`Endpoint = ${match[1]}:${match[2]}`,
|
||||
`Endpoint = ${ipv4}:${match[2]}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// For other protocols using URL format
|
||||
url.hostname = ipv4;
|
||||
row.link = url.toString();
|
||||
}
|
||||
} else {
|
||||
// Restore original link
|
||||
row.link = row.originalLink;
|
||||
}
|
||||
|
||||
// Update QR code with transition effect
|
||||
const canvasElement = document.querySelector('#qrCode-' + index);
|
||||
if (canvasElement) {
|
||||
// Add flash animation class
|
||||
canvasElement.classList.add('qr-flash');
|
||||
|
||||
// Remove the class after animation completes
|
||||
setTimeout(() => {
|
||||
canvasElement.classList.remove('qr-flash');
|
||||
}, 600);
|
||||
}
|
||||
|
||||
this.setQrCode("qrCode-" + index, row.link);
|
||||
},
|
||||
copy(content) {
|
||||
ClipboardManager
|
||||
.copyText(content)
|
||||
.then(() => {
|
||||
app.$message.success('{{ i18n "copied" }}')
|
||||
})
|
||||
},
|
||||
setQrCode(elementId, content) {
|
||||
new QRious({
|
||||
element: document.querySelector('#' + elementId),
|
||||
size: 400,
|
||||
value: content,
|
||||
background: 'white',
|
||||
backgroundAlpha: 0,
|
||||
foreground: 'black',
|
||||
padding: 2,
|
||||
level: 'L'
|
||||
});
|
||||
},
|
||||
genSubLink(subID) {
|
||||
return app.subSettings.subURI + subID;
|
||||
},
|
||||
genSubJsonLink(subID) {
|
||||
return app.subSettings.subJsonURI + subID;
|
||||
},
|
||||
revertOverflow() {
|
||||
const elements = document.querySelectorAll(".qr-tag");
|
||||
elements.forEach((element) => {
|
||||
element.classList.remove("tr-marquee");
|
||||
element.children[0].style.animation = '';
|
||||
while (element.children.length > 1) {
|
||||
element.removeChild(element.lastChild);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
updated() {
|
||||
if (this.qrModal.visible) {
|
||||
fixOverflow();
|
||||
if (!this.statusFetched) {
|
||||
this.getStatus();
|
||||
this.statusFetched = true;
|
||||
}
|
||||
} else {
|
||||
this.revertOverflow();
|
||||
// Reset the flag when modal is closed so it will fetch again next time
|
||||
this.statusFetched = false;
|
||||
}
|
||||
if (qrModal.client && qrModal.client.subId) {
|
||||
qrModal.subId = qrModal.client.subId;
|
||||
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
|
||||
this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
|
||||
}
|
||||
qrModal.qrcodes.forEach((element, index) => {
|
||||
this.setQrCode("qrCode-" + index, element.link);
|
||||
// Update links based on current toggle state
|
||||
if (element.useIPv4 && this.serverStatus && this.serverStatus.publicIP) {
|
||||
this.updateLink(index);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function fixOverflow() {
|
||||
const elements = document.querySelectorAll(".qr-tag");
|
||||
elements.forEach((element) => {
|
||||
function isElementOverflowing(element) {
|
||||
const overflowX = element.offsetWidth < element.scrollWidth,
|
||||
overflowY = element.offsetHeight < element.scrollHeight;
|
||||
return overflowX || overflowY;
|
||||
}
|
||||
|
||||
function wrapContentsInMarquee(element) {
|
||||
element.classList.add("tr-marquee");
|
||||
element.children[0].style.animation = `move-ltr ${(element.children[0].clientWidth / element.clientWidth) * 5
|
||||
}s ease-in-out infinite`;
|
||||
const marqueeText = element.children[0];
|
||||
if (element.children.length < 2) {
|
||||
for (let i = 0; i < 1; i++) {
|
||||
const marqueeText = element.children[0].cloneNode(true);
|
||||
element.children[0].after(marqueeText);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isElementOverflowing(element)) {
|
||||
wrapContentsInMarquee(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
51
web/html/modals/text_modal.html
Normal file
51
web/html/modals/text_modal.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{{define "modals/textModal"}}
|
||||
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title" :closable="true"
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-input :style="{ overflowY: 'auto' }" type="textarea" v-model="txtModal.content"
|
||||
:autosize="{ minRows: 10, maxRows: 20}"></a-input>
|
||||
<template slot="footer">
|
||||
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" icon="download"
|
||||
@click="FileManager.downloadTextFile(txtModal.content, txtModal.fileName)">
|
||||
<span>[[ txtModal.fileName ]]</span>
|
||||
</a-button>
|
||||
<a-button type="primary" icon="copy" @click="txtModal.copy(txtModal.content)">
|
||||
<span>{{ i18n "copy" }}</span>
|
||||
</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<script>
|
||||
const txtModal = {
|
||||
title: '',
|
||||
content: '',
|
||||
fileName: '',
|
||||
qrcode: null,
|
||||
visible: false,
|
||||
show: function (title = '', content = '', fileName = '') {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.fileName = fileName;
|
||||
this.visible = true;
|
||||
},
|
||||
copy: function (content = '') {
|
||||
ClipboardManager
|
||||
.copyText(content)
|
||||
.then(() => {
|
||||
app.$message.success('{{ i18n "copied" }}')
|
||||
this.close();
|
||||
})
|
||||
},
|
||||
close: function () {
|
||||
this.visible = false;
|
||||
},
|
||||
};
|
||||
|
||||
const textModalApp = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#text-modal',
|
||||
data: {
|
||||
txtModal: txtModal,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
125
web/html/modals/two_factor_modal.html
Normal file
125
web/html/modals/two_factor_modal.html
Normal file
@@ -0,0 +1,125 @@
|
||||
{{define "modals/twoFactorModal"}}
|
||||
<a-modal id="two-factor-modal" v-model="twoFactorModal.visible" :title="twoFactorModal.title" :closable="true"
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<template v-if="twoFactorModal.type === 'set'">
|
||||
<p>{{ i18n "pages.settings.security.twoFactorModalSteps" }}</p>
|
||||
<a-divider></a-divider>
|
||||
<p>{{ i18n "pages.settings.security.twoFactorModalFirstStep" }}</p>
|
||||
<div :style="{ display: 'flex', alignItems: 'center', flexDirection: 'column', gap: '12px' }">
|
||||
<div class="qr-bg" :style="{ width: '180px', height: '180px' }">
|
||||
<canvas @click="copy(twoFactorModal.token)" id="twofactor-qrcode" class="qr-cv"></canvas>
|
||||
</div>
|
||||
<span :style="{ fontSize: '12px', fontFamily: 'monospace' }">[[ twoFactorModal.token ]]</span>
|
||||
</div>
|
||||
<a-divider></a-divider>
|
||||
<p>{{ i18n "pages.settings.security.twoFactorModalSecondStep" }}</p>
|
||||
<a-input v-model.trim="twoFactorModal.enteredCode" :style="{ width: '100%' }"></a-input>
|
||||
</template>
|
||||
<template v-if="twoFactorModal.type === 'confirm'">
|
||||
<p>[[ twoFactorModal.description ]]</p>
|
||||
<a-input v-model.trim="twoFactorModal.enteredCode" :style="{ width: '100%' }"></a-input>
|
||||
</template>
|
||||
<template slot="footer">
|
||||
<a-button @click="twoFactorModal.cancel">
|
||||
<span>{{ i18n "cancel" }}</span>
|
||||
</a-button>
|
||||
<a-button type="primary" :disabled="twoFactorModal.enteredCode.length < 6" @click="twoFactorModal.ok">
|
||||
<span>{{ i18n "confirm" }}</span>
|
||||
</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<script>
|
||||
const twoFactorModal = {
|
||||
title: '',
|
||||
description: '',
|
||||
fileName: '',
|
||||
token: '',
|
||||
enteredCode: '',
|
||||
visible: false,
|
||||
type: 'set',
|
||||
confirm: null,
|
||||
totpObject: null,
|
||||
qrImage: "",
|
||||
ok() {
|
||||
if (twoFactorModal.totpObject.generate() === twoFactorModal.enteredCode) {
|
||||
ObjectUtil.execute(twoFactorModal.confirm, true)
|
||||
|
||||
twoFactorModal.close()
|
||||
} else {
|
||||
Vue.prototype.$message['error']('{{ i18n "pages.settings.security.twoFactorModalError" }}')
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
ObjectUtil.execute(twoFactorModal.confirm, false)
|
||||
|
||||
twoFactorModal.close()
|
||||
},
|
||||
show: function ({
|
||||
title = '',
|
||||
description = '',
|
||||
token = '',
|
||||
type = 'set',
|
||||
confirm = (success) => { }
|
||||
}) {
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.token = token;
|
||||
this.visible = true;
|
||||
this.confirm = confirm;
|
||||
this.type = type;
|
||||
|
||||
this.totpObject = new OTPAuth.TOTP({
|
||||
issuer: "X-Panel",
|
||||
label: "TwoStepCode",
|
||||
algorithm: "SHA1",
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret: twoFactorModal.token,
|
||||
});
|
||||
},
|
||||
close: function () {
|
||||
twoFactorModal.enteredCode = "";
|
||||
twoFactorModal.visible = false;
|
||||
},
|
||||
};
|
||||
|
||||
const twoFactorModalApp = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#two-factor-modal',
|
||||
data: {
|
||||
twoFactorModal: twoFactorModal,
|
||||
},
|
||||
updated() {
|
||||
if (
|
||||
this.twoFactorModal.visible &&
|
||||
this.twoFactorModal.type === 'set' &&
|
||||
document.getElementById('twofactor-qrcode')
|
||||
) {
|
||||
this.setQrCode('twofactor-qrcode', this.twoFactorModal.totpObject.toString());
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setQrCode(elementId, content) {
|
||||
new QRious({
|
||||
element: document.getElementById(elementId),
|
||||
size: 200,
|
||||
value: content,
|
||||
background: 'white',
|
||||
backgroundAlpha: 0,
|
||||
foreground: 'black',
|
||||
padding: 2,
|
||||
level: 'L'
|
||||
});
|
||||
},
|
||||
copy(content) {
|
||||
ClipboardManager
|
||||
.copyText(content)
|
||||
.then(() => {
|
||||
app.$message.success('{{ i18n "copied" }}')
|
||||
})
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
246
web/html/modals/warp_modal.html
Normal file
246
web/html/modals/warp_modal.html
Normal file
@@ -0,0 +1,246 @@
|
||||
{{define "modals/warpModal"}}
|
||||
<a-modal id="warp-modal" v-model="warpModal.visible" title="Cloudflare WARP"
|
||||
:confirm-loading="warpModal.confirmLoading" :closable="true" :mask-closable="true"
|
||||
:footer="null" :class="themeSwitcher.currentTheme">
|
||||
<template v-if="ObjectUtil.isEmpty(warpModal.warpData)">
|
||||
<a-button icon="api" @click="register" :loading="warpModal.confirmLoading">{{ i18n "create" }}</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<table :style="{ margin: '5px 0', width: '100%' }">
|
||||
<tr class="client-table-odd-row">
|
||||
<td>Access Token</td>
|
||||
<td>[[ warpModal.warpData.access_token ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device ID</td>
|
||||
<td>[[ warpModal.warpData.device_id ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>License Key</td>
|
||||
<td>[[ warpModal.warpData.license_key ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Private Key</td>
|
||||
<td>[[ warpModal.warpData.private_key ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-button @click="delConfig" :loading="warpModal.confirmLoading" type="danger">{{ i18n "delete" }}</a-button>
|
||||
<a-divider :style="{ margin: '0' }">{{ i18n "pages.xray.outbound.settings" }}</a-divider>
|
||||
<a-collapse :style="{ margin: '10px 0' }">
|
||||
<a-collapse-panel header='WARP/WARP+ License Key'>
|
||||
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Key">
|
||||
<a-input v-model="warpPlus"></a-input>
|
||||
<a-button @click="updateLicense(warpPlus)" :disabled="warpPlus.length<26"
|
||||
:loading="warpModal.confirmLoading">{{ i18n "update" }}</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<a-divider :style="{ margin: '0' }">{{ i18n "pages.xray.outbound.accountInfo" }}</a-divider>
|
||||
<a-button icon="sync" @click="getConfig" :style="{ marginTop: '5px', marginBottom: '10px' }"
|
||||
:loading="warpModal.confirmLoading" type="primary">{{ i18n "info" }}</a-button>
|
||||
<template v-if="!ObjectUtil.isEmpty(warpModal.warpConfig)">
|
||||
<table :style="{ width: '100%' }">
|
||||
<tr class="client-table-odd-row">
|
||||
<td>Device Name</td>
|
||||
<td>[[ warpModal.warpConfig.name ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device Model</td>
|
||||
<td>[[ warpModal.warpConfig.model ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>Device Enabled</td>
|
||||
<td>[[ warpModal.warpConfig.enabled ]]</td>
|
||||
</tr>
|
||||
<template v-if="!ObjectUtil.isEmpty(warpModal.warpConfig.account)">
|
||||
<tr>
|
||||
<td>Account Type</td>
|
||||
<td>[[ warpModal.warpConfig.account.account_type ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>Role</td>
|
||||
<td>[[ warpModal.warpConfig.account.role ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WARP+ Data</td>
|
||||
<td>[[ SizeFormatter.sizeFormat(warpModal.warpConfig.account.premium_data) ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>Quota</td>
|
||||
<td>[[ SizeFormatter.sizeFormat(warpModal.warpConfig.account.quota) ]]</td>
|
||||
</tr>
|
||||
<tr v-if="!ObjectUtil.isEmpty(warpModal.warpConfig.account.usage)">
|
||||
<td>Usage</td>
|
||||
<td>[[ SizeFormatter.sizeFormat(warpModal.warpConfig.account.usage) ]]</td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
<a-divider :style="{ margin: '10px 0' }">{{ i18n "pages.xray.outbound.outboundStatus" }}</a-divider>
|
||||
<a-form :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<template v-if="warpOutboundIndex>=0">
|
||||
<a-tag color="green" :style="{ lineHeight: '31px' }">{{ i18n "enabled" }}</a-tag>
|
||||
<a-button @click="resetOutbound" :loading="warpModal.confirmLoading" type="danger">{{ i18n "reset" }}</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag color="orange" :style="{ lineHeight: '31px' }">{{ i18n "disabled" }}</a-tag>
|
||||
<a-button @click="addOutbound" :loading="warpModal.confirmLoading" type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
</template>
|
||||
</a-modal>
|
||||
<script>
|
||||
|
||||
const warpModal = {
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
warpData: null,
|
||||
warpConfig: null,
|
||||
warpOutbound: null,
|
||||
show() {
|
||||
this.visible = true;
|
||||
this.warpConfig = null;
|
||||
this.getData();
|
||||
},
|
||||
close() {
|
||||
this.visible = false;
|
||||
this.loading(false);
|
||||
},
|
||||
loading(loading = true) {
|
||||
this.confirmLoading = loading;
|
||||
},
|
||||
async getData() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post('/panel/xray/warp/data');
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
this.warpData = msg.obj.length > 0 ? JSON.parse(msg.obj) : null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#warp-modal',
|
||||
data: {
|
||||
warpModal: warpModal,
|
||||
warpPlus: '',
|
||||
},
|
||||
methods: {
|
||||
collectConfig() {
|
||||
config = warpModal.warpConfig.config;
|
||||
peer = config.peers[0];
|
||||
if (config) {
|
||||
warpModal.warpOutbound = Outbound.fromJson({
|
||||
tag: 'warp',
|
||||
protocol: Protocols.Wireguard,
|
||||
settings: {
|
||||
mtu: 1420,
|
||||
secretKey: warpModal.warpData.private_key,
|
||||
address: this.getAddresses(config.interface.addresses),
|
||||
reserved: this.getResolved(config.client_id),
|
||||
domainStrategy: 'ForceIP',
|
||||
peers: [{
|
||||
publicKey: peer.public_key,
|
||||
endpoint: peer.endpoint.host,
|
||||
}],
|
||||
noKernelTun: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
getAddresses(addrs) {
|
||||
let addresses = [];
|
||||
if (addrs.v4) addresses.push(addrs.v4 + "/32");
|
||||
if (addrs.v6) addresses.push(addrs.v6 + "/128");
|
||||
return addresses;
|
||||
},
|
||||
getResolved(client_id) {
|
||||
let reserved = [];
|
||||
let decoded = atob(client_id);
|
||||
let hexString = '';
|
||||
for (let i = 0; i < decoded.length; i++) {
|
||||
let hex = decoded.charCodeAt(i).toString(16);
|
||||
hexString += (hex.length === 1 ? '0' : '') + hex;
|
||||
}
|
||||
|
||||
for (let i = 0; i < hexString.length; i += 2) {
|
||||
let hexByte = hexString.slice(i, i + 2);
|
||||
let decValue = parseInt(hexByte, 16);
|
||||
reserved.push(decValue);
|
||||
}
|
||||
return reserved;
|
||||
},
|
||||
async register() {
|
||||
warpModal.loading(true);
|
||||
const keys = Wireguard.generateKeypair();
|
||||
const msg = await HttpUtil.post('/panel/xray/warp/reg', keys);
|
||||
if (msg.success) {
|
||||
const resp = JSON.parse(msg.obj);
|
||||
warpModal.warpData = resp.data;
|
||||
warpModal.warpConfig = resp.config;
|
||||
this.collectConfig();
|
||||
}
|
||||
warpModal.loading(false);
|
||||
},
|
||||
async updateLicense(l) {
|
||||
warpModal.loading(true);
|
||||
const msg = await HttpUtil.post('/panel/xray/warp/license', { license: l });
|
||||
if (msg.success) {
|
||||
warpModal.warpData = JSON.parse(msg.obj);
|
||||
warpModal.warpConfig = null;
|
||||
this.warpPlus = '';
|
||||
}
|
||||
warpModal.loading(false);
|
||||
},
|
||||
async getConfig() {
|
||||
warpModal.loading(true);
|
||||
const msg = await HttpUtil.post('/panel/xray/warp/config');
|
||||
warpModal.loading(false);
|
||||
if (msg.success) {
|
||||
warpModal.warpConfig = JSON.parse(msg.obj);
|
||||
this.collectConfig();
|
||||
}
|
||||
},
|
||||
async delConfig() {
|
||||
warpModal.loading(true);
|
||||
const msg = await HttpUtil.post('/panel/xray/warp/del');
|
||||
warpModal.loading(false);
|
||||
if (msg.success) {
|
||||
warpModal.warpData = null;
|
||||
warpModal.warpConfig = null;
|
||||
this.delOutbound();
|
||||
}
|
||||
},
|
||||
addOutbound() {
|
||||
app.templateSettings.outbounds.push(warpModal.warpOutbound.toJson());
|
||||
app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
|
||||
warpModal.close();
|
||||
},
|
||||
resetOutbound() {
|
||||
app.templateSettings.outbounds[this.warpOutboundIndex] = warpModal.warpOutbound.toJson();
|
||||
app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
|
||||
warpModal.close();
|
||||
},
|
||||
delOutbound() {
|
||||
if (this.warpOutboundIndex != -1) {
|
||||
app.templateSettings.outbounds.splice(this.warpOutboundIndex, 1);
|
||||
app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
|
||||
}
|
||||
warpModal.close();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
warpOutboundIndex: {
|
||||
get: function () {
|
||||
return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag == 'warp') : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
123
web/html/modals/xray_balancer_modal.html
Normal file
123
web/html/modals/xray_balancer_modal.html
Normal file
@@ -0,0 +1,123 @@
|
||||
{{define "modals/balancerModal"}}
|
||||
<a-modal
|
||||
id="balancer-modal"
|
||||
v-model="balancerModal.visible"
|
||||
:title="balancerModal.title"
|
||||
@ok="balancerModal.ok"
|
||||
:confirm-loading="balancerModal.confirmLoading"
|
||||
:ok-button-props="{ props: { disabled: !balancerModal.isValid } }"
|
||||
:closable="true"
|
||||
:mask-closable="false"
|
||||
:ok-text="balancerModal.okText"
|
||||
cancel-text='{{ i18n "close" }}'
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.xray.balancer.tag" }}' has-feedback
|
||||
:validate-status="balancerModal.duplicateTag? 'warning' : 'success'">
|
||||
<a-input v-model.trim="balancerModal.balancer.tag" @change="balancerModal.check()"
|
||||
placeholder='{{ i18n "pages.xray.balancer.tagDesc" }}'></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.balancer.balancerStrategy" }}'>
|
||||
<a-select v-model="balancerModal.balancer.strategy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="random">Random</a-select-option>
|
||||
<a-select-option value="roundRobin">Round Robin</a-select-option>
|
||||
<a-select-option value="leastLoad">Least Load</a-select-option>
|
||||
<a-select-option value="leastPing">Least Ping</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.balancer.balancerSelectors" }}' has-feedback
|
||||
:validate-status="balancerModal.emptySelector? 'warning' : 'success'">
|
||||
<a-select v-model="balancerModal.balancer.selector" mode="tags" @change="balancerModal.checkSelector()"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="tag in balancerModal.outboundTags" :value="tag">[[ tag ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Fallback">
|
||||
<a-select v-model="balancerModal.balancer.fallbackTag" clearable
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="tag in [ '', ...balancerModal.outboundTags]" :value="tag">[[ tag ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</table>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<script>
|
||||
const balancerModal = {
|
||||
title: '',
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
okText: '{{ i18n "sure" }}',
|
||||
isEdit: false,
|
||||
confirm: null,
|
||||
duplicateTag: false,
|
||||
emptySelector: false,
|
||||
balancer: {
|
||||
tag: '',
|
||||
strategy: 'random',
|
||||
selector: [],
|
||||
fallbackTag: ''
|
||||
},
|
||||
outboundTags: [],
|
||||
balancerTags:[],
|
||||
ok() {
|
||||
if (balancerModal.balancer.selector.length == 0) {
|
||||
balancerModal.emptySelector = true;
|
||||
return;
|
||||
}
|
||||
balancerModal.emptySelector = false;
|
||||
ObjectUtil.execute(balancerModal.confirm, balancerModal.balancer);
|
||||
},
|
||||
show({ title = '', okText = '{{ i18n "sure" }}', balancerTags = [], balancer, confirm = (balancer) => { }, isEdit = false }) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
this.confirm = confirm;
|
||||
this.visible = true;
|
||||
if (isEdit) {
|
||||
balancerModal.balancer = balancer;
|
||||
} else {
|
||||
balancerModal.balancer = {
|
||||
tag: '',
|
||||
strategy: 'random',
|
||||
selector: [],
|
||||
fallbackTag: ''
|
||||
};
|
||||
}
|
||||
this.balancerTags = balancerTags.filter((tag) => tag != balancer.tag);
|
||||
this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);
|
||||
this.isEdit = isEdit;
|
||||
this.check();
|
||||
this.checkSelector();
|
||||
},
|
||||
close() {
|
||||
this.visible = false;
|
||||
this.loading(false);
|
||||
},
|
||||
loading(loading=true) {
|
||||
this.confirmLoading = loading;
|
||||
},
|
||||
check() {
|
||||
if (this.balancer.tag == '' || this.balancerTags.includes(this.balancer.tag)) {
|
||||
this.duplicateTag = true;
|
||||
this.isValid = false;
|
||||
} else {
|
||||
this.duplicateTag = false;
|
||||
this.isValid = true;
|
||||
}
|
||||
},
|
||||
checkSelector() {
|
||||
this.emptySelector = this.balancer.selector.length == 0;
|
||||
}
|
||||
};
|
||||
|
||||
new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#balancer-modal',
|
||||
data: {
|
||||
balancerModal: balancerModal
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
127
web/html/modals/xray_dns_modal.html
Normal file
127
web/html/modals/xray_dns_modal.html
Normal file
@@ -0,0 +1,127 @@
|
||||
{{define "modals/dnsModal"}}
|
||||
<a-modal id="dns-modal" v-model="dnsModal.visible" :title="dnsModal.title" @ok="dnsModal.ok" :closable="true"
|
||||
:mask-closable="false" :ok-text="dnsModal.okText" cancel-text='{{ i18n "close" }}'
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.address" }}'>
|
||||
<a-input v-model.trim="dnsModal.dnsServer.address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input-number v-model.number="dnsModal.dnsServer.port" :min="1" :max="65531"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.dns.strategy" }}'>
|
||||
<a-select v-model="dnsModal.dnsServer.queryStrategy" :style="{ width: '100%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="l" :label="l" v-for="l in ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']"> [[ l ]] </a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
||||
<a-form-item label='{{ i18n "pages.xray.dns.domains" }}'>
|
||||
<a-button icon="plus" size="small" type="primary" @click="dnsModal.dnsServer.domains.push('')"></a-button>
|
||||
<template v-for="(domain, index) in dnsModal.dnsServer.domains">
|
||||
<a-input v-model.trim="dnsModal.dnsServer.domains[index]">
|
||||
<a-button icon="minus" size="small" slot="addonAfter"
|
||||
@click="dnsModal.dnsServer.domains.splice(index,1)"></a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.dns.expectIPs"}}'>
|
||||
<a-button icon="plus" size="small" type="primary" @click="dnsModal.dnsServer.expectIPs.push('')"></a-button>
|
||||
<template v-for="(domain, index) in dnsModal.dnsServer.expectIPs">
|
||||
<a-input v-model.trim="dnsModal.dnsServer.expectIPs[index]">
|
||||
<a-button icon="minus" size="small" slot="addonAfter"
|
||||
@click="dnsModal.dnsServer.expectIPs.splice(index,1)"></a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.dns.unexpectIPs"}}'>
|
||||
<a-button icon="plus" size="small" type="primary" @click="dnsModal.dnsServer.unexpectedIPs.push('')"></a-button>
|
||||
<template v-for="(domain, index) in dnsModal.dnsServer.unexpectedIPs">
|
||||
<a-input v-model.trim="dnsModal.dnsServer.unexpectedIPs[index]">
|
||||
<a-button icon="minus" size="small" slot="addonAfter"
|
||||
@click="dnsModal.dnsServer.unexpectedIPs.splice(index,1)"></a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
||||
<a-form-item label='Skip Fallback'>
|
||||
<a-switch v-model="dnsModal.dnsServer.skipFallback"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Disable Cache'>
|
||||
<a-switch v-model="dnsModal.dnsServer.disableCache"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Final Query'>
|
||||
<a-switch v-model="dnsModal.dnsServer.finalQuery"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<script>
|
||||
const defaultDnsObject = {
|
||||
address: "localhost",
|
||||
port: 53,
|
||||
domains: [],
|
||||
expectIPs: [],
|
||||
unexpectedIPs: [],
|
||||
queryStrategy: 'UseIP',
|
||||
skipFallback: true,
|
||||
disableCache: false,
|
||||
finalQuery: false
|
||||
}
|
||||
|
||||
const dnsModal = {
|
||||
title: '',
|
||||
visible: false,
|
||||
okText: '{{ i18n "confirm" }}',
|
||||
isEdit: false,
|
||||
confirm: null,
|
||||
dnsServer: { ...defaultDnsObject },
|
||||
ok() {
|
||||
ObjectUtil.execute(dnsModal.confirm, { ...dnsModal.dnsServer });
|
||||
},
|
||||
show({
|
||||
title = '',
|
||||
okText = '{{ i18n "confirm" }}',
|
||||
dnsServer,
|
||||
confirm = (dnsServer) => { },
|
||||
isEdit = false
|
||||
}) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
this.confirm = confirm;
|
||||
this.visible = true;
|
||||
this.isEdit = isEdit;
|
||||
|
||||
if (isEdit) {
|
||||
switch (typeof dnsServer) {
|
||||
case 'string':
|
||||
const dnsObj = { ...defaultDnsObject };
|
||||
|
||||
dnsObj.address = dnsServer;
|
||||
|
||||
this.dnsServer = dnsObj;
|
||||
break;
|
||||
case 'object':
|
||||
this.dnsServer = dnsServer;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this.dnsServer = { ...defaultDnsObject };
|
||||
|
||||
this.dnsServer.domains = [];
|
||||
this.dnsServer.expectIPs = [];
|
||||
this.dnsServer.unexpectedIPs = [];
|
||||
}
|
||||
},
|
||||
close() {
|
||||
dnsModal.visible = false;
|
||||
},
|
||||
};
|
||||
new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#dns-modal',
|
||||
data: {
|
||||
dnsModal: dnsModal,
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
56
web/html/modals/xray_fakedns_modal.html
Normal file
56
web/html/modals/xray_fakedns_modal.html
Normal file
@@ -0,0 +1,56 @@
|
||||
{{define "modals/fakednsModal"}}
|
||||
<a-modal id="fakedns-modal" v-model="fakednsModal.visible" :title="fakednsModal.title" @ok="fakednsModal.ok"
|
||||
:closable="true" :mask-closable="false" :ok-text="fakednsModal.okText" cancel-text='{{ i18n "close" }}'
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.xray.fakedns.ipPool" }}'>
|
||||
<a-input v-model.trim="fakednsModal.fakeDns.ipPool"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.fakedns.poolSize" }}'>
|
||||
<a-input-number v-model.number="fakednsModal.fakeDns.poolSize" :min="1"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<script>
|
||||
const fakednsDefaultData = {
|
||||
ipPool: "198.18.0.0/16",
|
||||
poolSize: 65535,
|
||||
}
|
||||
|
||||
const fakednsModal = {
|
||||
title: '',
|
||||
visible: false,
|
||||
okText: '{{ i18n "confirm" }}',
|
||||
isEdit: false,
|
||||
confirm: null,
|
||||
fakeDns: { ...fakednsDefaultData },
|
||||
ok() {
|
||||
ObjectUtil.execute(fakednsModal.confirm, fakednsModal.fakeDns);
|
||||
},
|
||||
show({ title = '', okText = '{{ i18n "confirm" }}', fakeDns, confirm = (fakeDns) => { }, isEdit = false }) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
this.confirm = confirm;
|
||||
this.visible = true;
|
||||
if (isEdit) {
|
||||
this.fakeDns = fakeDns;
|
||||
} else {
|
||||
this.fakeDns = { ...fakednsDefaultData }
|
||||
}
|
||||
this.isEdit = isEdit;
|
||||
},
|
||||
close() {
|
||||
fakednsModal.visible = false;
|
||||
},
|
||||
};
|
||||
|
||||
new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#fakedns-modal',
|
||||
data: {
|
||||
fakednsModal: fakednsModal,
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
127
web/html/modals/xray_outbound_modal.html
Normal file
127
web/html/modals/xray_outbound_modal.html
Normal file
@@ -0,0 +1,127 @@
|
||||
{{define "modals/outModal"}}
|
||||
<a-modal id="out-modal" v-model="outModal.visible" :title="outModal.title" @ok="outModal.ok"
|
||||
:confirm-loading="outModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:ok-button-props="{ props: { disabled: !outModal.isValid } }" :style="{ overflow: 'hidden' }"
|
||||
:ok-text="outModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
||||
{{template "form/outbound"}}
|
||||
</a-modal>
|
||||
<script>
|
||||
|
||||
const outModal = {
|
||||
title: '',
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
okText: '{{ i18n "sure" }}',
|
||||
isEdit: false,
|
||||
confirm: null,
|
||||
outbound: new Outbound(),
|
||||
jsonMode: false,
|
||||
link: '',
|
||||
cm: null,
|
||||
duplicateTag: false,
|
||||
isValid: true,
|
||||
activeKey: '1',
|
||||
tags: [],
|
||||
ok() {
|
||||
ObjectUtil.execute(outModal.confirm, outModal.outbound.toJson());
|
||||
},
|
||||
show({ title='', okText='{{ i18n "sure" }}', outbound, confirm=(outbound)=>{}, isEdit=false, tags=[] }) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
this.confirm = confirm;
|
||||
this.jsonMode = false;
|
||||
this.link = '';
|
||||
this.activeKey = '1';
|
||||
this.visible = true;
|
||||
this.outbound = isEdit ? Outbound.fromJson(outbound) : new Outbound();
|
||||
this.isEdit = isEdit;
|
||||
this.tags = tags;
|
||||
this.check()
|
||||
},
|
||||
close() {
|
||||
outModal.visible = false;
|
||||
outModal.loading(false);
|
||||
},
|
||||
loading(loading=true) {
|
||||
outModal.confirmLoading = loading;
|
||||
},
|
||||
check(){
|
||||
if(outModal.outbound.tag == '' || outModal.tags.includes(outModal.outbound.tag)){
|
||||
this.duplicateTag = true;
|
||||
this.isValid = false;
|
||||
} else {
|
||||
this.duplicateTag = false;
|
||||
this.isValid = true;
|
||||
}
|
||||
},
|
||||
toggleJson(jsonTab) {
|
||||
textAreaObj = document.getElementById('outboundJson');
|
||||
if(jsonTab){
|
||||
if(this.cm != null) {
|
||||
this.cm.toTextArea();
|
||||
this.cm=null;
|
||||
}
|
||||
textAreaObj.value = JSON.stringify(this.outbound.toJson(), null, 2);
|
||||
this.cm = CodeMirror.fromTextArea(textAreaObj, app.cmOptions);
|
||||
this.cm.on('change',editor => {
|
||||
value = editor.getValue();
|
||||
if(this.isJsonString(value)){
|
||||
this.outbound = Outbound.fromJson(JSON.parse(value));
|
||||
this.check();
|
||||
}
|
||||
});
|
||||
this.activeKey = '2';
|
||||
} else {
|
||||
if(this.cm != null) {
|
||||
this.cm.toTextArea();
|
||||
this.cm=null;
|
||||
}
|
||||
this.activeKey = '1';
|
||||
}
|
||||
},
|
||||
isJsonString(str) {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#out-modal',
|
||||
data: {
|
||||
outModal: outModal,
|
||||
get outbound() {
|
||||
return outModal.outbound;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
streamNetworkChange() {
|
||||
if (this.outModal.outbound.protocol == Protocols.VLESS && !outModal.outbound.canEnableTlsFlow()) {
|
||||
delete this.outModal.outbound.settings.flow;
|
||||
}
|
||||
},
|
||||
canEnableTls() {
|
||||
return this.outModal.outbound.canEnableTls();
|
||||
},
|
||||
convertLink(){
|
||||
newOutbound = Outbound.fromLink(outModal.link);
|
||||
if(newOutbound){
|
||||
this.outModal.outbound = newOutbound;
|
||||
this.outModal.toggleJson(true);
|
||||
this.outModal.check();
|
||||
this.$message.success('Link imported successfully...');
|
||||
outModal.link = '';
|
||||
} else {
|
||||
this.$message.error('Wrong Link!');
|
||||
outModal.link = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
138
web/html/modals/xray_reverse_modal.html
Normal file
138
web/html/modals/xray_reverse_modal.html
Normal file
@@ -0,0 +1,138 @@
|
||||
{{define "modals/reverseModal"}}
|
||||
<a-modal id="reverse-modal" v-model="reverseModal.visible" :title="reverseModal.title" @ok="reverseModal.ok"
|
||||
:confirm-loading="reverseModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:ok-text="reverseModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
|
||||
<a-select v-model="reverseModal.reverse.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="x,y in reverseTypes" :value="y">[[ x ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.tag" }}'>
|
||||
<a-input v-model.trim="reverseModal.reverse.tag"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.domain" }}'>
|
||||
<a-input v-model.trim="reverseModal.reverse.domain"></a-input>
|
||||
</a-form-item>
|
||||
<template v-if="reverseModal.reverse.type=='bridge'">
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.intercon" }}'>
|
||||
<a-select v-model="reverseModal.rules[0].outboundTag" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.rules.outbound" }}'>
|
||||
<a-select v-model="reverseModal.rules[1].outboundTag" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.intercon" }}'>
|
||||
<a-checkbox-group
|
||||
v-model="reverseModal.rules[0].inboundTag"
|
||||
:options="reverseModal.inboundTags"></a-checkbox-group>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.rules.inbound" }}'>
|
||||
<a-checkbox-group
|
||||
v-model="reverseModal.rules[1].inboundTag"
|
||||
:options="reverseModal.inboundTags"></a-checkbox-group>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<script>
|
||||
const reverseModal = {
|
||||
title: '',
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
okText: '{{ i18n "sure" }}',
|
||||
isEdit: false,
|
||||
confirm: null,
|
||||
reverse: {
|
||||
tag: "",
|
||||
type: "",
|
||||
domain: ""
|
||||
},
|
||||
rules: [
|
||||
{ outboundTag: '', inboundTag: []},
|
||||
{ outboundTag: '', inboundTag: []}
|
||||
],
|
||||
inboundTags: [],
|
||||
outboundTags: [],
|
||||
ok() {
|
||||
reverseModal.rules[0].domain = ["full:" + reverseModal.reverse.domain];
|
||||
reverseModal.rules[0].type = 'field';
|
||||
reverseModal.rules[1].type = 'field';
|
||||
|
||||
if(reverseModal.reverse.type == 'bridge'){
|
||||
reverseModal.rules[0].inboundTag = [reverseModal.reverse.tag];
|
||||
reverseModal.rules[1].inboundTag = [reverseModal.reverse.tag];
|
||||
} else {
|
||||
reverseModal.rules[0].outboundTag = reverseModal.reverse.tag;
|
||||
reverseModal.rules[1].outboundTag = reverseModal.reverse.tag;
|
||||
}
|
||||
ObjectUtil.execute(reverseModal.confirm, reverseModal.reverse, reverseModal.rules);
|
||||
},
|
||||
show({ title='', okText='{{ i18n "sure" }}', reverse, rules, confirm=(reverse, rules)=>{}, isEdit=false }) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
this.confirm = confirm;
|
||||
this.visible = true;
|
||||
if(isEdit) {
|
||||
this.reverse = {
|
||||
tag: reverse.tag,
|
||||
type: reverse.type,
|
||||
domain: reverse.domain,
|
||||
};
|
||||
reverse;
|
||||
rules0 = rules.filter(r => r.domain != null);
|
||||
if(rules0.length == 0) rules0 = [{ outboundTag: '', domain: ["full:" + this.reverse.domain], inboundTag: []}];
|
||||
rules1 = rules.filter(r => r.domain == null);
|
||||
if(rules1.length == 0) rules1 = [{ outboundTag: '', inboundTag: []}];
|
||||
this.rules = [];
|
||||
this.rules.push({
|
||||
domain: rules0[0].domain,
|
||||
outboundTag: rules0[0].outboundTag,
|
||||
inboundTag: rules0.map(r => r.inboundTag).flat()
|
||||
});
|
||||
this.rules.push({
|
||||
outboundTag: rules1[0].outboundTag,
|
||||
inboundTag: rules1.map(r => r.inboundTag).flat()
|
||||
});
|
||||
} else {
|
||||
this.reverse = {
|
||||
tag: "reverse-" + app.reverseData.length,
|
||||
type: "bridge",
|
||||
domain: "reverse.xui"
|
||||
}
|
||||
this.rules = [
|
||||
{ outboundTag: '', inboundTag: []},
|
||||
{ outboundTag: '', inboundTag: []}
|
||||
]
|
||||
}
|
||||
this.isEdit = isEdit;
|
||||
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
|
||||
this.inboundTags.push(...app.inboundTags);
|
||||
if (app.enableDNS && !ObjectUtil.isEmpty(app.dnsTag)) this.inboundTags.push(app.dnsTag)
|
||||
this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);
|
||||
},
|
||||
close() {
|
||||
reverseModal.visible = false;
|
||||
reverseModal.loading(false);
|
||||
},
|
||||
loading(loading=true) {
|
||||
reverseModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
||||
new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#reverse-modal',
|
||||
data: {
|
||||
reverseModal: reverseModal,
|
||||
reverseTypes: { bridge: '{{ i18n "pages.xray.outbound.bridge" }}', portal:'{{ i18n "pages.xray.outbound.portal" }}'},
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
237
web/html/modals/xray_rule_modal.html
Normal file
237
web/html/modals/xray_rule_modal.html
Normal file
@@ -0,0 +1,237 @@
|
||||
{{define "modals/ruleModal"}}
|
||||
<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok" :confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false" :ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template> {{ i18n "pages.xray.rules.SourceIPs" }} <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.sourceIP"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template> {{ i18n "pages.xray.rules.SourcePort" }} <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.sourcePort"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.rules.Network" }}'>
|
||||
<a-select v-model="ruleModal.rule.network" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="x in ['','TCP','UDP','TCP,UDP']" :value="x">[[ x ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.rules.Protocol" }}'>
|
||||
<a-select v-model="ruleModal.rule.protocol" mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="x in ['http','tls','bittorrent','quic']" :value="x">[[ x ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.rules.Attributes" }}'>
|
||||
<a-button icon="plus" size="small" :style="{ marginLeft: '10px' }" @click="ruleModal.rule.attrs.push(['', ''])"></a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span: 24}">
|
||||
<a-input-group compact v-for="(attr,index) in ruleModal.rule.attrs">
|
||||
<a-input :style="{ width: '50%' }" v-model="attr[0]" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
|
||||
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input :style="{ width: '50%' }" v-model="attr[1]" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<a-button icon="minus" slot="addonAfter" size="small" @click="ruleModal.rule.attrs.splice(index,1)"></a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template> IP <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.ip"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template> {{ i18n "pages.xray.rules.Domain" }} <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.domain"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template> {{ i18n "pages.xray.rules.User" }} <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.user"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template> {{ i18n "pages.xray.rules.Port" }} <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.port"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.rules.InboundTag" }}'>
|
||||
<a-select v-model="ruleModal.rule.inboundTag" mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="tag in ruleModal.inboundTags" :value="tag">[[ tag ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.rules.OutboundTag" }}'>
|
||||
<a-select v-model="ruleModal.rule.outboundTag" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="tag in ruleModal.outboundTags" :value="tag">[[ tag ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.balancer.balancerDesc" }}</span>
|
||||
</template> {{ i18n "pages.xray.rules.BalancerTag" }} <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-select v-model="ruleModal.rule.balancerTag" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="tag in ruleModal.balancerTags" :value="tag">[[ tag ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<script>
|
||||
const ruleModal = {
|
||||
title: '',
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
okText: '{{ i18n "sure" }}',
|
||||
isEdit: false,
|
||||
confirm: null,
|
||||
rule: {
|
||||
type: "field",
|
||||
domain: "",
|
||||
ip: "",
|
||||
port: "",
|
||||
sourcePort: "",
|
||||
network: "",
|
||||
sourceIP: "",
|
||||
user: "",
|
||||
inboundTag: [],
|
||||
protocol: [],
|
||||
attrs: [],
|
||||
outboundTag: "",
|
||||
balancerTag: "",
|
||||
},
|
||||
inboundTags: [],
|
||||
outboundTags: [],
|
||||
users: [],
|
||||
balancerTags: [],
|
||||
ok() {
|
||||
newRule = ruleModal.getResult();
|
||||
ObjectUtil.execute(ruleModal.confirm, newRule);
|
||||
},
|
||||
show({
|
||||
title = '',
|
||||
okText = '{{ i18n "sure" }}',
|
||||
rule,
|
||||
confirm = (rule) => {},
|
||||
isEdit = false
|
||||
}) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
this.confirm = confirm;
|
||||
this.visible = true;
|
||||
if (isEdit) {
|
||||
this.rule.domain = rule.domain ? rule.domain.join(',') : [];
|
||||
this.rule.ip = rule.ip ? rule.ip.join(',') : [];
|
||||
this.rule.port = rule.port;
|
||||
this.rule.sourcePort = rule.sourcePort;
|
||||
this.rule.network = rule.network;
|
||||
this.rule.sourceIP = rule.sourceIP ? rule.sourceIP.join(',') : [];
|
||||
this.rule.user = rule.user ? rule.user.join(',') : [];
|
||||
this.rule.inboundTag = rule.inboundTag;
|
||||
this.rule.protocol = rule.protocol;
|
||||
this.rule.attrs = rule.attrs ? Object.entries(rule.attrs) : [];
|
||||
this.rule.outboundTag = rule.outboundTag;
|
||||
this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : "";
|
||||
} else {
|
||||
this.rule = {
|
||||
domain: "",
|
||||
ip: "",
|
||||
port: "",
|
||||
sourcePort: "",
|
||||
network: "",
|
||||
sourceIP: "",
|
||||
user: "",
|
||||
inboundTag: [],
|
||||
protocol: [],
|
||||
attrs: [],
|
||||
outboundTag: "",
|
||||
balancerTag: "",
|
||||
}
|
||||
}
|
||||
this.isEdit = isEdit;
|
||||
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
|
||||
this.inboundTags.push(...app.inboundTags);
|
||||
if (app.enableDNS && !ObjectUtil.isEmpty(app.dnsTag)) this.inboundTags.push(app.dnsTag)
|
||||
this.outboundTags = ["", ...app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];
|
||||
if (app.templateSettings.reverse) {
|
||||
if (app.templateSettings.reverse.bridges) {
|
||||
this.inboundTags.push(...app.templateSettings.reverse.bridges.map(b => b.tag));
|
||||
}
|
||||
if (app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag));
|
||||
}
|
||||
if (app.templateSettings.routing && app.templateSettings.routing.balancers) {
|
||||
this.balancerTags = ["", ...app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];
|
||||
}
|
||||
},
|
||||
close() {
|
||||
ruleModal.visible = false;
|
||||
ruleModal.loading(false);
|
||||
},
|
||||
loading(loading = true) {
|
||||
ruleModal.confirmLoading = loading;
|
||||
},
|
||||
getResult() {
|
||||
value = ruleModal.rule;
|
||||
rule = {};
|
||||
newRule = {};
|
||||
rule.type = "field";
|
||||
rule.domain = value.domain.length > 0 ? value.domain.split(',').map(s => s.trim()) : [];
|
||||
rule.ip = value.ip.length > 0 ? value.ip.split(',').map(s => s.trim()) : [];
|
||||
rule.port = value.port;
|
||||
rule.sourcePort = value.sourcePort;
|
||||
rule.network = value.network;
|
||||
rule.sourceIP = value.sourceIP.length > 0 ? value.sourceIP.split(',').map(s => s.trim()) : [];
|
||||
rule.user = value.user.length > 0 ? value.user.split(',').map(s => s.trim()) : [];
|
||||
rule.inboundTag = value.inboundTag;
|
||||
rule.protocol = value.protocol;
|
||||
rule.attrs = Object.fromEntries(value.attrs);
|
||||
rule.outboundTag = value.outboundTag == "" ? undefined : value.outboundTag;
|
||||
rule.balancerTag = value.balancerTag == "" ? undefined : value.balancerTag;
|
||||
for (const [key, value] of Object.entries(rule)) {
|
||||
if (value !== null && value !== undefined && !(Array.isArray(value) && value.length === 0) && !(typeof value === 'object' && Object.keys(value).length === 0) && value !== '') {
|
||||
newRule[key] = value;
|
||||
}
|
||||
}
|
||||
return newRule;
|
||||
}
|
||||
};
|
||||
new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#rule-modal',
|
||||
data: {
|
||||
ruleModal: ruleModal,
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
428
web/html/navigation.html
Normal file
428
web/html/navigation.html
Normal file
@@ -0,0 +1,428 @@
|
||||
{{ template "page/head_start" .}}
|
||||
<style>
|
||||
/*
|
||||
样式优化:强制选项卡铺满主区域并等宽显示
|
||||
- display: flex; 使其成为弹性容器
|
||||
- flex-grow: 1; 让每个选项卡平均分配可用空间
|
||||
*/
|
||||
.ant-tabs-bar {
|
||||
margin: 0;
|
||||
display: flex !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* 新增规则:强制 .ant-tabs-nav-wrap 参与弹性布局,这是解决问题的关键 */
|
||||
.ant-tabs-nav-wrap {
|
||||
display: flex !important;
|
||||
flex: 1 !important;
|
||||
}
|
||||
|
||||
/* 优化选项卡导航的flexbox布局,确保等宽显示 */
|
||||
.ant-tabs-nav {
|
||||
width: 100%;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* 为选项卡内部添加内边距和文字大小调整 */
|
||||
.ant-tabs-tab {
|
||||
flex: 1 !important; /* 新增规则:强制选项卡等宽显示 */
|
||||
text-align: center;
|
||||
padding: 0 16px;
|
||||
font-size: 18px; /* 增大选项卡文字大小 */
|
||||
}
|
||||
|
||||
/* 为选项卡图标和文字之间添加间距 */
|
||||
.ant-tabs-tab .anticon + span {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/*
|
||||
根据 settings.html 文件的布局,为内容区设置 16px 的左右间距,
|
||||
使其与侧边栏保持一致。
|
||||
*/
|
||||
#content-layout .ant-layout-content {
|
||||
max-width: 1200px;
|
||||
padding-top: 0 !important; /* 强制移除顶部的间距 */
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/*
|
||||
为选项卡和内容卡片之间添加一个合适的间隙。
|
||||
使用 margin-top 和 padding 调整卡片的位置和内部空间。
|
||||
*/
|
||||
.tab-content-pane {
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.tab-content-pane .ant-card {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/*
|
||||
确保文本颜色继承父元素,以兼容不同主题
|
||||
(如:暗色主题下文本变为白色)
|
||||
*/
|
||||
.tab-content-pane h2,
|
||||
.tab-content-pane p,
|
||||
.tab-content-pane strong,
|
||||
.tab-content-pane ol,
|
||||
.tab-content-pane ul,
|
||||
.tab-content-pane li,
|
||||
.tab-content-pane code {
|
||||
color: inherit !important; /* 强制继承父元素的颜色 */
|
||||
}
|
||||
|
||||
/* 确保链接文字在任何主题下都显示为蓝色,以保持一致的视觉识别 */
|
||||
.tab-content-pane a {
|
||||
color: #1890ff !important;
|
||||
}
|
||||
.tab-content-pane a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 调整 p 标签字体大小,使其更易于阅读。*/
|
||||
#content-layout .tab-content-pane p {
|
||||
line-height: 1.8;
|
||||
font-size: 18px !important;
|
||||
margin-bottom: 1.5em; /* 为段落添加底部外边距 */
|
||||
}
|
||||
|
||||
/* 修复列表文字大小和行高问题 */
|
||||
.tab-content-pane ol, .tab-content-pane ul {
|
||||
padding-left: 20px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.tab-content-pane ol li, .tab-content-pane ul li {
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.8;
|
||||
font-size: 15px; /* 调整列表项字体大小 */
|
||||
}
|
||||
</style>
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
<a-sidebar></a-sidebar>
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="false">
|
||||
<a-tabs v-model="activeKey">
|
||||
<a-tab-pane key="1">
|
||||
<template #tab>
|
||||
<a-icon type="tool"></a-icon>
|
||||
<span>有用技巧</span>
|
||||
</template>
|
||||
<div class="tab-content-pane">
|
||||
<a-card>
|
||||
<h2>一、〔X-Panel面板〕交流群与安装教程</h2>
|
||||
<br>
|
||||
<p><a href="https://t.me/XUI_CN" target="_blank">点击加入〔X-Panel面板〕交流群:https://t.me/XUI_CN</a></p>
|
||||
<p><a href="https://xeefei.blogspot.com/2025/09/x-panel.html" target="_blank">详细安装流程步骤:https://xeefei.blogspot.com/2025/09/x-panel.html</a></p>
|
||||
<br>
|
||||
|
||||
<h2>二、判断VPS服务器的IP是否“送中”及解决方法</h2>
|
||||
<ol>
|
||||
<li><strong>判断方法:</strong></li>
|
||||
<li>点击打开 <a href="https://music.youtube.com/" target="_blank">https://music.youtube.com/</a>,能正常访问,代表没“送中”,反之就是“送中”了。</li>
|
||||
<li><strong>如果送中了如何解决去【拉回来】?解决方法:</strong></li>
|
||||
<li>关闭/取消登录了谷歌账户的APP定位权限/授权;</li>
|
||||
<li>将常用的一个谷歌账号的位置记录功能打开;</li>
|
||||
<li>打开谷歌浏览器,登录开了位置记录功能的谷歌账号,并安装 <a href="https://chrome.google.com/webstore/detail/location-guard/cfohepagpmnodfdmjliccbbigdkfcgia" target="_blank">Location Guard</a> 扩展插件;</li>
|
||||
<li>打开Location Guard插件,选择Fixed Location,并在给出的地图上单击,即可标记上你想要IP所处的国家/地区 Google IP定位错误,使用Location Guard修改;</li>
|
||||
<li>转换到Options选项,Default level默认设置为Use fixed location;</li>
|
||||
<li>访问 <a href="打开谷歌地图google.com/maps,点击右下角定位授权图标,使google" target="_blank">谷歌地图服务</a> 获取当前GPS位置,确认是否已修改成功;</li>
|
||||
<li>谷歌搜索my ip,即可看到谷歌IP定位到了刚才地图上标记的位置;</li>
|
||||
<li>最后,通过 <a href="https://support.google.com/websearch/workflow/9308722" target="_blank">此网页向谷歌报告IP问题</a>。</li>
|
||||
</ol>
|
||||
<br>
|
||||
|
||||
<h2>三、在VPS服务器部署“订阅转换”功能</h2>
|
||||
<ol>
|
||||
<li><strong>操作步骤::</strong> </li>
|
||||
<li>进入脚本输入x-ui命令调取面板,选择第【25】选项安装订阅转换模块;</li>
|
||||
<li>等待安装【订阅转换】成功之后,访问地址:<code>https://你的域名:15268</code> ;</li>
|
||||
<li>因为在转换过程中需要调取后端API,所以请确保端口 8000 和 15268 是打开放行的;</li>
|
||||
<li>直接复制脚本中提供的【登录地址】,进入后台,点击【节点列表】----->>>【添加节点】;</li>
|
||||
<li>接下来点击左边侧边栏的【订阅列表】去【添加订阅】;</li>
|
||||
<li>最后一步,点击【客户端】,即可导入Clash等软件中使用;</li>
|
||||
<li>此功能集成到〔X-Panel面板〕中,是为了保证安全,不会造成链接泄露。</li>
|
||||
</ol>
|
||||
<br>
|
||||
|
||||
<h2>四、如何保护自己的IP不被封锁被墙掉?</h2>
|
||||
<ol>
|
||||
<li><strong>使用安全代理协议::</strong> 加密是必备,推荐使用vless+reality+vision协议组合;</li>
|
||||
<li><strong>避免共享节点::</strong> 尽量不要在不同的地区,多个省份之间不要共同连接同一个IP;</li>
|
||||
<li><strong>隔离IP和端口::</strong> 避免多个用户或设备在不同地理位置漫游时使用同一个IP和端口,要分开;</li>
|
||||
<li><strong>控制流量::</strong> 不要在单台VPS上长时间进行大流量下载,在一天内不要流量过高,适时切换节点;</li>
|
||||
<li><strong>使用高位端口::</strong> 创建入站时,尽量使用 40000 至 65000 之间的高位端口号。</li>
|
||||
<li><strong>核心总结::</strong> 不要多终端、多省份、多朋友共同使用同一个IP和端口。多创建几个入站,各用各的,避免被 GFW 识别为机场特征。
|
||||
使用〔X-Panel面板〕多创建几个【入站】, 多做几条备用,各用各的!各行其道才比较安全!
|
||||
GFW的思维模式是干掉机场,机场的特征个人用户不要去沾染,自然IP就保护好了。</li>
|
||||
</ol>
|
||||
<br>
|
||||
|
||||
<h2>五、检测IP纯净度</h2>
|
||||
<p>访问 <a href="https://scamalytics.com/" target="_blank">https://scamalytics.com/</a>,输入IP检测欺诈分数,分数越高代表IP越“脏”。</p>
|
||||
<br>
|
||||
|
||||
<h2>六、查看指定端口的网络连接数</h2>
|
||||
<p><strong>Linux 命令:</strong><br>
|
||||
<code>netstat -ntu | grep :节点端口 | grep ESTABLISHED | awk '{print $5}'</code></p>
|
||||
<br>
|
||||
|
||||
<h2>七、如何用 X-Panel 实现“自己偷自己”?</h2>
|
||||
<ol>
|
||||
<li><strong>其实很简单,只要你为面板设置了证书, 开启了HTTPS登录,就可以将〔X-Panel面板〕自身作为web server, 无需Nginx等;</strong></li>
|
||||
<li><strong>这里给一个示例: 其中目标网站(Dest)请填写面板监听端口;</strong></li>
|
||||
<li><strong>可选域名(SNI)填写面板登录域名, 如果您使用其他web server(如nginx)等, 将目标网站改为对应监听端口也可;</strong></li>
|
||||
<li><strong>注意:需要说明的是,如果您处于白名单地区,自己“偷”自己并不适合你;</strong></li>
|
||||
<li><strong>可选域名一项实际上可以填写任意SNI,只要客户端保持一致即可,不过并不推荐这样做。</strong></li>
|
||||
</ol>
|
||||
<br>
|
||||
|
||||
<h2>八、项目声明与注意</h2>
|
||||
<br>
|
||||
<ol>
|
||||
<li><strong>声明:</strong> 此项目仅供个人学习、交流使用,请遵守当地法律法规,勿用于非法用途;请勿用于生产环境。</li>
|
||||
<li><strong>注意:</strong> 在使用此项目和〔教程〕过程中,若因违反以上声明使用规则而产生的一切后果由使用者自负。</li>
|
||||
</ol>
|
||||
<br>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="2">
|
||||
<template #tab>
|
||||
<a-icon type="wallet"></a-icon>
|
||||
<span>推广赞助</span>
|
||||
</template>
|
||||
<div class="tab-content-pane">
|
||||
<a-card>
|
||||
<h2>一、若此项目对你有帮助,可以考虑通过以下链接购买VPS:</h2>
|
||||
<br>
|
||||
<ol>
|
||||
<li><strong>搬瓦工GIA线路:</strong> <a href="https://bandwagonhost.com/aff.php?aff=75015" target="_blank">https://bandwagonhost.com/aff.php?aff=75015</a></li>
|
||||
<li><strong>Dmit高端GIA机:</strong> <a href="https://www.dmit.io/aff.php?aff=9326" target="_blank">https://www.dmit.io/aff.php?aff=9326</a></li>
|
||||
<li><strong>Gomami亚太顶尖优化线路:</strong> <a href="https://gomami.io/aff.php?aff=174" target="_blank">https://gomami.io/aff.php?aff=174</a></li>
|
||||
<li><strong>ISIF优质亚太优化线路:</strong> <a href="https://cloud.isif.net/login?affiliation_code=333" target="_blank">https://cloud.isif.net/login?affiliation_code=333</a></li>
|
||||
<li><strong>ZoroCloud全球优质原生家宽&住宅双lSP,跨境首选:</strong> <a href="https://my.zorocloud.com/aff.php?aff=1072" target="_blank">https://my.zorocloud.com/aff.php?aff=1072</a></li>
|
||||
<li><strong>三网直连 IEPL / IPLC 直播流量转发:</strong> <a href="https://idc333.top/#register/BCUZXNELNO" target="_blank">https://idc333.top/#register/BCUZXNELNO</a></li>
|
||||
<li><strong>Bagevm优质落地鸡(原生IP全解锁):</strong> <a href="https://www.bagevm.com/aff.php?aff=754" target="_blank">https://www.bagevm.com/aff.php?aff=754</a></li>
|
||||
<li><strong>白丝云【4837】量大管饱:</strong> <a href="https://cloudsilk.io/aff.php?aff=706" target="_blank">https://cloudsilk.io/aff.php?aff=706</a></li>
|
||||
<li><strong>RackNerd极致性价比机器:</strong> <a href="https://my.racknerd.com/aff.php?aff=15268&pid=912" target="_blank">https://my.racknerd.com/aff.php?aff=15268&pid=912</a></li>
|
||||
</ol>
|
||||
<br>
|
||||
|
||||
<h2>二、项目相关</h2>
|
||||
<br>
|
||||
<p><a href="https://t.me/is_Chat_Bot" target="_blank">--->合作咨询请联系作者<---</a></p>
|
||||
<p><a href="https://github.com/xeefei/x-panel" target="_blank">〔X-Panel面板〕项目地址</a></p>
|
||||
<br>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="3">
|
||||
<template #tab>
|
||||
<a-icon type="crown"></a-icon>
|
||||
<span>〔X-Panel-Pro 面板〕介绍</span>
|
||||
</template>
|
||||
<div class="tab-content-pane">
|
||||
<a-card>
|
||||
<h2>一、关于【Pro版】的“收费说明”</h2>
|
||||
<ol>
|
||||
<li><strong>【授权码】目前定价:</strong> 100RMB 或 15U 一个,一机一码,不能重复用于不同机器 VPS,后期视情况不定时会上涨价格;对于〔X-Panel 面板〕后期的“新功能”,都将在【付费Pro版】中进行更新。</li>
|
||||
<li><strong>目前的【安装界面】:</strong> 有两种可选,“免费基础版”一样可用,只是后期不再提供技术支持和重大更新,另外,在【免费基础版】中,【一键配置】功能将不再可用,全部放到了【付费Pro版】中。</li>
|
||||
<li><strong>后期的开发精力:</strong> 全部会放到【付费Pro版】中,免费基础版不删库,持续保留,会大幅降低更新频率,后期只会同步更新 Xray 那边的【内核版本】等基础,想继续用的不影响,只是没有【新功能】可用,翻墙也足够。</li>
|
||||
</ol>
|
||||
<br>
|
||||
|
||||
<h2>二、〔付费Pro版〕已实现的功能</h2>
|
||||
<ol>
|
||||
<li><strong>新增 -【付费Pro版】的面板后台UI:</strong> 添加醒目的“X-Panel-Pro”标识;</li>
|
||||
<li><strong>优化 -【付费Pro版】TG端 【版本更新】提示功能:</strong> 增加详细的“更新说明”;</li>
|
||||
<li><strong>增加 -〔Pro版面板后台〕:</strong> 使用 Reality 协议时,可点击“随机更换”所偷的域名;</li>
|
||||
<li><strong>新增 - 【付费Pro版】TG端 的【发送授权报告】:</strong> 增加“唯一授权防伪码”;</li>
|
||||
<li><strong>优化 -【付费Pro版】安装脚本界面:</strong> 增加【Pro版】该有的“明确标识”;</li>
|
||||
<li><strong>优化 -【付费Pro版】TG端的显示方式:</strong> 增加该有的“会员标识”;</li>
|
||||
<li><strong>新增 -【付费Pro版】安装脚本:</strong> 有“网页版SSH工具”可选部署,脚本中〔第26选项〕;</li>
|
||||
<li><strong>新增 - 【付费Pro版】安装脚本:</strong> 有“线路和IP质量检测”可去使用,脚本中〔第27选项〕;</li>
|
||||
<li><strong>新增 - 【付费Pro版】安装脚本:</strong> 有“地区服务器DNS检测”可去使用,脚本中〔第28选项〕;</li>
|
||||
<li><strong>新增 -【付费Pro版】---->>>TG端:</strong> 同步有“网页版SSH工具”可选安装;</li>
|
||||
<li><strong>优化 - 【付费Pro版】---->>>TG端:</strong> 点击“服务器状态”时的“版本号显示”;</li>
|
||||
<li><strong>说明 - 【付费Pro版】TG端中:</strong> 使用命令:/webssh,安装“网页版SSH”;</li>
|
||||
<li><strong>优化 -〔Pro版〕中的〔一键配置〕功能:</strong> 有更友好的提示方式;</li>
|
||||
<li><strong>新增 -【付费Pro版】---->>>面板后台的【首页 UI】:</strong> 目前是有“5种”可选:标准布局 (默认),炫彩动画,深海科技,暮光薰衣,和幽林秘境;你喜欢什么类型的主题,就去点击“选定”之后,就不会自动变了,若后期需要更换,就重选;</li>
|
||||
<li><strong>新增 -【付费Pro版】---->>>在“创建入站”时:</strong> 可以在页面上更加方便地选择【重置流量】的方式:有每日重置,每周重置,按月重置,或从不重置;</li>
|
||||
<li><strong>新增 -〔X-Panel 面板〕----->>【付费Pro版】TG端“每日报告”:</strong> 可定制【发送内容】,自己可点击“打开或关闭”,并且可以选择【发送时间】,可按天,或者每周,每月发;</li>
|
||||
<li><strong>优化 -【付费Pro版】的“授权码验证机制”:</strong> 增加【后台联网验证】,以及“机器指纹”等属性;</li>
|
||||
<li><strong>新增 -〔X-Panel 面板〕----->>【付费Pro版】TG端“多面板管理”:</strong> 一个机器人可同时管理其他面板,可以很丝滑地远程操作【被控端 VPS】;</li>
|
||||
<li><strong>新增 -〔X-Panel 面板〕----->>【付费Pro版】TG端“获取节点链接”功能:</strong> 支持【本机】和【远程被控端 VPS】都能获取,开发此功能的目的在于:不用进〔面板后台〕,就能在 TG端 获取到之前已经创建过的“链接”;</li>
|
||||
<li><strong>新增 -〔X-Panel 面板〕----->>【付费Pro版】“一键部署中转节点”:</strong> 解决了不懂配置的麻烦,已实现:远程Socks5创建 --> 本机路由配置 --> 本机入口创建 --> 生成“二维码和链接”,“小手一点”,直接可用;</li>
|
||||
<li><strong>新增 -〔X-Panel 面板〕----->>【申请安装证书】“第18选项”:</strong> 有“备用方式申请证书”,当用常规方式【1】申请不下来时,可以试试“备用方式5”;</li>
|
||||
<li><strong>新增 -〔X-Panel 面板〕----->>【申请安装证书】“第18选项”:</strong> 有“可自定义证书路径”,自己进入 VPS 中“手动上传证书”,复制路径,在脚本中填入即可;</li>
|
||||
<li><strong>新增 -〔X-Panel 面板〕----->>【付费Pro版】“深度调优脚本”:</strong> 包含 BBR+FQ, TCP Fast Open, 内存缓冲区及队列优化,在〔Pro版〕脚本中“第29选项”可直接用;</li>
|
||||
<li><strong>新增 -〔X-Panel 面板〕----->>【付费Pro版】“首页会员等级”显示:</strong> 能够明确展示:自己的会员等级,授权码信息,以及“版本更新”提示;</li>
|
||||
<li><strong>新增 -〔X-Panel 面板〕----->>【付费Pro版】“节点上/下线TG通知”功能:</strong> 对于【拼车】的宝子,能明确知道:哪个节点,什么时候上线?或者下线时间,做到“心中有数”;</li>
|
||||
<li><strong>新增 -〔X-Panel 面板〕----->>【付费Pro版】“TG端 签到得积分”功能:</strong> 后期针对有【积分】的宝子,会不断推出:相应的【特权】和【福利待遇】,</li>
|
||||
<li><strong>新增 -〔X-Panel 面板〕----->>【付费Pro版】“TG端 积分多重功能”;推出:</strong> 积分查询,积分换购,授权码查询,修改用户名,积分转移/打赏,以及“积分排行榜”,</li>
|
||||
<li><strong>新增 -〔X-Panel 面板〕----->>【付费Pro版】“TG端签到得积分”功能,推出【“积分换购”的可用功能】:</strong> A、消耗1000积分“自助重置换绑时间”,B、消耗5000积分“自助换购一个普通授权码”,</li>
|
||||
<li><strong>新增 -〔X-Panel 面板〕----->>【付费Pro版】“远程备份 + 急救还原”功能:</strong> 面板报错“崩了”,不用像之前那样:卸载面板 -->> 重装面板,更不用很麻烦去“重装系统”解决,直接:远程急救还原,前提就是:你自己要知道,在面板“正常运行”的时候,去「备份数据快照」,</li>
|
||||
<li><strong>新增 -〔X-Panel 面板〕----->>【付费Pro版】“每月重置流量”功能:</strong> 可输入1—31之间的任意数字,比如:输入12,即代表“每月12号”「重置入站流量」,以便提供更友好的“重置流量方式”,</li>
|
||||
<li><strong>新增 -〔X-Panel 面板〕----->>【付费Pro版】“批量部署节点”功能:</strong> 可直接在面板后台的“一键配置”中去使用,点击一次可批量部署生成10条「VLESS + TCP + Reality + Vision」协议组合的入站,</li>
|
||||
<li><strong>新增 -〔X-Panel 面板〕----->>【付费Pro版】推出“购买机器人”功能:</strong> 可自助全自动在“机器人”中:购买授权码,增加配额,充值积分,自助重置换绑等,联系:<a href="https://t.me/Buy_ShouQuan_Bot" target="_blank">https://t.me/Buy_ShouQuan_Bot</a></li>
|
||||
</ol>
|
||||
<br>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="4">
|
||||
<template #tab>
|
||||
<a-icon type="key"></a-icon>
|
||||
<span>〔授权码〕相关事宜</span>
|
||||
</template>
|
||||
<div class="tab-content-pane">
|
||||
<a-card>
|
||||
<h2>一、“授权码说明”与批量折扣</h2>
|
||||
<p><strong>【授权码】100RMB 或 15U一个,一机一码。</strong><br>
|
||||
包括:重装,后期的升级/更新,都能使用,但是不能重复用于不同的机器。<br>
|
||||
所以推荐稳定使用的机器用【授权码】。<br>
|
||||
注:“授权码”属虚拟商品,购买之后,一经激活生效,概不退款,<br>
|
||||
对于一年期限(年付/年抛)的机器,后期可以【换绑】,<br>
|
||||
为什么要有时间限制?就是为了防止,有些人拿【授权码】滥用。<br>
|
||||
购买方式/渠道联系机器人:<a href="https://t.me/Buy_ShouQuan_Bot" target="_blank">https://t.me/Buy_ShouQuan_Bot</a>
|
||||
</p>
|
||||
<p>经常换的机器,去使用“免费基础版”就行,目前的【安装界面】是有“两种方式”可选择的。</p>
|
||||
<br>
|
||||
|
||||
<h3 style="color: #1890ff;"><strong>【批量授权】折扣(适用于业务需求、Tk、跨境电商等):</strong></h3>
|
||||
|
||||
<p>若都要用【收费Pro版】的话,可以用【批量授权】,<br>以下列举出来的,就是【批量折扣】的统一“优惠”。<br>注:〔批量授权码〕要求最低5台以上,是“一码通用”,<br>一个“授权码”可以绑定验证多台 VPS 机器,并且有专属的“豹子号”授权码。</p>
|
||||
<ul>
|
||||
<li><strong>5 ——> 20台:</strong> 8折,尾号:5555</li>
|
||||
<li><strong>20 ——> 50台:</strong> 7折,尾号:66666</li>
|
||||
<li><strong>50 ——> 100台:</strong> 6折,尾号:777777</li>
|
||||
<li><strong>100 ——> 200台:</strong> 5折,尾号:8888888</li>
|
||||
<li><strong>200台 ——> 以上:</strong> 4折,尾号:99999999</li>
|
||||
</ul>
|
||||
<br>
|
||||
|
||||
<h2>二、购买赞助方式</h2>
|
||||
<p>1、若您需要购买【授权码】,请跟下面这个“机器人”去对话,</p>
|
||||
<p>———————————————-<br><br>
|
||||
<a href="https://t.me/Buy_ShouQuan_Bot" target="_blank">https://t.me/Buy_ShouQuan_Bot</a><br>
|
||||
(授权码购买机器人)<br><br>
|
||||
———————————————-<br>
|
||||
输入:/start 或 购买,即可“在线下单”,<br><br>
|
||||
2、弹出【购买页面】,选择或输入“数量”,<br>
|
||||
付款了支付系统会收到回调,金额到账之后,<br>
|
||||
就会通过那个机器人【发放授权码】给您,<br>
|
||||
整个流程,是【全自助】的“自动处理”方式,<br>
|
||||
若您是〔增加配额〕,基本也是一样的流程,<br><br>
|
||||
3、请注意:机器人发给您的所有信息,<br>
|
||||
尽量都去自己【耐心阅读】一遍,“使用说明”,<br>
|
||||
以及〔VIP 群〕的信息,也全部都包含在里面的,<br><br>
|
||||
4、按照之前您安装更新〔X-Panel 面板〕的方式,<br>
|
||||
直接重新输入【安装命令】,选择【2】,就能去,<br>
|
||||
把之前的【免费版】,“无缝升级”到最新的【Pro版】。</p>
|
||||
<br>
|
||||
|
||||
<h2>三、关于【授权码】的一些“问题解答”</h2>
|
||||
<br>
|
||||
<p><strong>1、“授权码”的有效期是多久?一直有效吗?能一直用吗?</strong></p>
|
||||
<p>Answer:一直有效,只要不换机器,能一直用,但是,【换绑机器】有“一年时间的冷却期”,<br>“验证系统”会自动从你绑定这个授权码的时候开始计算,自动判断“换绑剩余时间”,<br>如果您等不及一年时间,需要提前【换绑机器】,那就绑定TG机器人,<br>每天“签到拿积分”,可以自己用“积分”去自助重置换绑,只要您有积分,想什么时候换绑随您愿意,<br>意思就是说:现在不限制了,用“1000积分”或者去〔购买机器人〕花20元/次可以“自由换绑”。</p>
|
||||
<br>
|
||||
<p><strong>2、“授权码”都有什么样的?我怎么能得到一个顺一点的“授权码”?</strong></p>
|
||||
<p>Answer:目前的【授权码】,分为:普通授权码,除了开头是“XPANEL”以外,其他完全随机;<br>【批量豹子号授权码】,根据一次性“购买数量”的不同,有相应的“规则”去生成对应不同的“豹子号尾号”,规则是在程序中“写死了的”;<br>并且,不管“普通授权码”,还是“豹子号授权码”,都是“大写字母+数字”的组合,至少24位以上。</p>
|
||||
<br>
|
||||
<p><strong>3、那我的【授权码】怎么才能升级到【豹子号授权码】?</strong></p>
|
||||
<p>Answer:两种方式:另外补足差价购买【豹子号】,或推广卖【授权码】拿“返佣”,<br>因为【批量授权】有折扣力度,所以这个就相当于自己可以拿【配额】出去卖“授权码”。</p>
|
||||
<br>
|
||||
<p><strong>4、为什么有时候重装系统【授权码】会显示“换绑冻结”?</strong></p>
|
||||
<p>Answer:因为【授权码】跟 VPS 机器是绑定在一起的,而这个绑定,<br>识别的是由这台 VPS 机器的“硬件信息”构成的【机器指纹】,这个是唯一的,<br>一般来讲,重装系统,不会导致这个【机器指纹】改变,<br>但是,有的宝子,会存在极少数情况,重装系统之后,这台 VPS 的【机器指纹】变了,导致“授权码”冻结。<br>
|
||||
所以,建议就是:尽量一次性把系统和软件什么的都弄好,既然你这台 VPS 重装系统会变,那只能稳定使用之后,一次性搞好别轻易重装,<br>至于:测试,练手,可以先拿【免费基础版】去搞,熟悉了之后,再从【免费版】“无缝升级”到【Pro版】,所有的“节点数据”都是在的,<br>PS:【授权码验证系统】识别绑定的是“机器指纹”,跟 IP 没有关系,即使 IP 被墙,<br>只要您没有更换机器,换 IP 都不影响;另外,重装系统尽量用 DD脚本 去搞,指纹不会变。</p>
|
||||
<br>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="5">
|
||||
<template #tab>
|
||||
<a-icon type="appstore"></a-icon>
|
||||
<span>其他资源</span>
|
||||
</template>
|
||||
<div class="tab-content-pane">
|
||||
<a-card>
|
||||
<h2>一、常见的代理软件/工具</h2>
|
||||
<br>
|
||||
<ol>
|
||||
<li><strong>Windows系统v2rayN:</strong> <a href="https://github.com/2dust/v2rayN" target="_blank">https://github.com/2dust/v2rayN</a></li>
|
||||
<li><strong>安卓手机版【v2rayNG】:</strong> <a href="https://github.com/2dust/v2rayNG" target="_blank">https://github.com/2dust/v2rayNG</a></li>
|
||||
<li><strong>苹果手机IOS【小火箭】:</strong> <a href="https://apple02.com/" target="_blank">https://apple02.com/</a></li>
|
||||
<li><strong>苹果MacOS电脑【Clash Verge】:</strong> <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases" target="_blank">https://github.com/clash-verge-rev/clash-verge-rev/releases</a></li>
|
||||
</ol>
|
||||
<br>
|
||||
|
||||
<h2>二、“接码”网站</h2>
|
||||
<p><a href="https://sms-activate.org/cn" target="_blank">https://sms-activate.org/cn</a>直接注册账号购买,可用于注册各种在线服务。</p>
|
||||
<br>
|
||||
|
||||
<h2>三、常用网站和群组</h2>
|
||||
<br>
|
||||
<ol>
|
||||
<li><strong>NodeSeek 论坛:</strong> <a href="https://www.nodeseek.com/" target="_blank">https://www.nodeseek.com/</a></li>
|
||||
<li><strong>V2EX 论坛::</strong> <a href="https://www.v2ex.com/" target="_blank">https://www.v2ex.com/</a></li>
|
||||
<li><strong>搬瓦工 TG 群::</strong> <a href="https://t.me/BWHOfficial" target="_blank">https://t.me/BWHOfficial</a></li>
|
||||
<li><strong>Xray 官方群::</strong> <a href="https://t.me/projectXray" target="_blank">https://t.me/projectXray</a></li>
|
||||
<li><strong>Dmit 交流群::</strong> <a href="https://t.me/DmitChat" target="_blank">https://t.me/DmitChat</a></li>
|
||||
<li><strong>白丝云用户群::</strong> <a href="https://t.me/+VHZLKELTQyzPNgOV" target="_blank">https://t.me/+VHZLKELTQyzPNgOV</a></li>
|
||||
<li><strong>NameSilo 域名注册::</strong> <a href="https://www.namesilo.com/" target="_blank">https://www.namesilo.com/</a></li>
|
||||
</ol>
|
||||
<br>
|
||||
|
||||
<br>
|
||||
<h2>四、其他内容</h2>
|
||||
<br>
|
||||
<p><a href="https://www.youtube.com/results?search_query=4k%E6%B5%8B%E9%80%9F" target="_blank">油管4K测速:https://www.youtube.com/results?search_query=4k%E6%B5%8B%E9%90%A6</a></p>
|
||||
<p><a href="https://xtls.github.io/" target="_blank">Project X:https://xtls.github.io/</a></p>
|
||||
<p><a href="https://whatismyipaddress.com/" target="_blank">我的IP查询:https://whatismyipaddress.com/</a></p>
|
||||
<p><a href="https://translate.google.com/?hl=zh-CN" target="_blank">Google翻译:https://translate.google.com/?hl=zh-CN</a></p>
|
||||
<br>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
|
||||
{{template "page/body_scripts" .}}
|
||||
{{template "component/aSidebar" .}}
|
||||
{{template "component/aThemeSwitch" .}}
|
||||
<script>
|
||||
// 初始化 Vue 应用实例
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
themeSwitcher, // 确保主题切换器对象在 Vue 实例中可用
|
||||
|
||||
// 【新增】:用于控制当前选中的标签页,默认为 '1'
|
||||
activeKey: '1',
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
// 【新增】:检查 URL 是否带有 ?tab=x 参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const tab = urlParams.get('tab');
|
||||
if (tab) {
|
||||
// 如果有参数,就自动切换到对应的标签页 (例如 '3')
|
||||
this.activeKey = tab;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
{{ template "page/body_end" .}}
|
||||
509
web/html/servers.html
Normal file
509
web/html/servers.html
Normal file
@@ -0,0 +1,509 @@
|
||||
{{ template "page/head_start" .}}
|
||||
<style>
|
||||
/* 复制自 settings.html 的样式,确保布局响应式一致 */
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
}
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.ant-layout-content {
|
||||
margin: 10px 10px;
|
||||
}
|
||||
}
|
||||
/* 调整表格卡片样式 */
|
||||
.ant-card-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* 针对暗色主题(dark-theme)的 a-alert 强制样式覆盖 */
|
||||
/* 1、去掉暗色主题下 Modal 的默认灰色遮罩(解决“灰蒙蒙”问题) */
|
||||
/*
|
||||
.dark-theme .ant-modal-mask {
|
||||
background: transparent !important; /* 变为完全透明,更通透 */
|
||||
} */
|
||||
|
||||
/* 1、只去掉这个弹窗的灰色遮罩 */
|
||||
.dark-theme .ping-check-modal + .ant-modal-wrap .ant-modal-mask,
|
||||
.dark-theme .ping-check-modal .ant-modal-mask {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 2、弹窗中 info 提示框 强制使用浅色背景 + 蓝色边框 */
|
||||
.ant-modal .ant-alert-info {
|
||||
background-color: #f0f8ff !important; /* 浅蓝色背景 */
|
||||
border: 1px solid #91d5ff !important; /* 蓝色边框 */
|
||||
border-radius: 8px; /* 圆角更好看 */
|
||||
}
|
||||
|
||||
/* 3、弹窗中 a-alert 内所有文字,强制为深色(防止发灰/发白) */
|
||||
.ant-modal .ant-alert-info,
|
||||
.ant-modal .ant-alert-info * {
|
||||
color: #333333 !important; /* 深灰文字,更清晰 */
|
||||
}
|
||||
|
||||
/* 4、强制 info-circle 图标为标准 Ant 蓝色(恢复你之前看到的效果) */
|
||||
.ant-modal .ant-alert-info .anticon-info-circle {
|
||||
color: #1890ff !important;
|
||||
}
|
||||
|
||||
/* 5、部分浏览器会把 svg 填充色变灰,这里强制修正 */
|
||||
.ant-modal .ant-alert-info svg {
|
||||
fill: #1890ff !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
<a-sidebar></a-sidebar>
|
||||
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="loading" tip="加载中...">
|
||||
<transition name="list" appear>
|
||||
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]">
|
||||
<a-col>
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :span="24">
|
||||
<a-space :size="15">
|
||||
|
||||
<a-button type="primary" @click="openAddModal(0)">
|
||||
<a-icon type="plus"></a-icon> 添加〔被控端 VPS〕
|
||||
</a-button>
|
||||
|
||||
<a-button type="primary" style="cursor: default;">
|
||||
<a-icon type="info-circle"></a-icon>
|
||||
当前已绑定:[[ normalCount ]] 台 (最多可绑:[[ maxLimit ]] 台)
|
||||
</a-button>
|
||||
|
||||
<a-button type="default" icon="reload" @click="getServers">
|
||||
刷新列表
|
||||
</a-button>
|
||||
|
||||
<a-button type="primary" icon="setting" @click="goToTgSettings">
|
||||
配置本机“主控机器人”
|
||||
</a-button>
|
||||
|
||||
</a-space>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-table :columns="columns" :data-source="normalServersPage" :row-key="record => record.ID" :pagination="normalPagination" @change="handleNormalTableChange" style="margin-top: 20px">
|
||||
<template slot="action" slot-scope="text, record">
|
||||
<a-space>
|
||||
<a-button type="danger" size="small" icon="delete" @click="deleteServer(record)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-card hoverable title="一键部署中转节点">
|
||||
<a-row>
|
||||
<a-col :span="24">
|
||||
<div style="margin-bottom: 15px; color: #666;">
|
||||
<a-icon type="info-circle"></a-icon>
|
||||
说明:在此添加远程中转机(落地机)信息,点击“一键部署”即可自动完成:
|
||||
远程Socks创建 --> 本机路由配置 --> 本机入口创建 --> 生成“二维码和链接”。
|
||||
</div>
|
||||
<a-space :size="15">
|
||||
<a-button type="primary" @click="openAddModal(1)">
|
||||
<a-icon type="plus"></a-icon> 添加〔中转机 VPS〕
|
||||
</a-button>
|
||||
|
||||
<a-button type="primary" style="cursor: default;">
|
||||
<a-icon type="info-circle"></a-icon>
|
||||
当前已绑定:[[ transitCount ]] 台 (最多可绑:[[ maxLimit ]] 台)
|
||||
</a-button>
|
||||
|
||||
<a-button type="default" icon="reload" @click="getServers">
|
||||
刷新列表
|
||||
</a-button>
|
||||
|
||||
<a-button type="primary" icon="thunderbolt" @click="openCheckModal">
|
||||
检测中转节点“连通性”
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-table :columns="columns" :data-source="transitServersPage" :row-key="record => record.ID" :pagination="transitPagination" @change="handleTransitTableChange" :scroll="isMobile ? { x: 'max-content' } : {}" style="margin-top: 20px">
|
||||
<template slot="action" slot-scope="text, record">
|
||||
<a-space>
|
||||
<a-button v-if="record.LastLink || record.last_link" type="default" size="small" icon="link" @click="getLastLink(record)" :loading="record.linkLoading">
|
||||
获取上次链接
|
||||
</a-button>
|
||||
|
||||
<a-button type="primary" size="small" icon="rocket" @click="setupRelay(record)" :loading="record.setupLoading" :type="(record.LastLink || record.last_link) ? 'dashed' : 'primary'">
|
||||
[[ (record.LastLink || record.last_link) ? '重新部署' : '一键部署中转' ]]
|
||||
</a-button>
|
||||
|
||||
<a-button type="danger" size="small" icon="delete" @click="deleteServer(record)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
</a-col>
|
||||
</a-row>
|
||||
</transition>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
|
||||
<a-modal v-model="visible" :title="formType === 1 ? '添加〔中转机 VPS〕' : '添加〔被控端 VPS〕'" @ok="submitServer" :confirm-loading="modalLoading">
|
||||
<a-form :model="form" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<a-form-item label="备注名称">
|
||||
<a-input v-model="form.name" placeholder="例如:香港鸡"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="面板地址">
|
||||
<a-input v-model="form.url" placeholder="https://aaa.xxxx.com:54321/nbxuiplus"></a-input>
|
||||
<span style="font-size: 12px; color: #888;">* 必须包含协议头 (https://) 和 “端口 + 路径”</span>
|
||||
</a-form-item>
|
||||
<a-form-item label="用 户 名">
|
||||
<a-input v-model="form.username" placeholder="面板登录用户名"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="登录密码">
|
||||
<a-input-password v-model="form.password" placeholder="面板登录密码"></a-input-password>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<div v-if="formType === 0" style="margin-top: 20px; color: red; font-style: italic; text-align: center; line-height: 1.6;">
|
||||
1、此功能用于集中管理多个〔X-Panel 面板〕,<br>
|
||||
2、是通过在后台〔配置绑定机器人〕去管理的,<br>
|
||||
3、若在输入框您已经添加了〔被控端 的 VPS 信息〕,<br>
|
||||
4、则您不能在对应的那台 VPS 中去绑定任何其他机器人,<br>
|
||||
5、并且,您的〔被控端 VPS〕面板不能去开启“两步验证”。
|
||||
</div>
|
||||
|
||||
<div v-else style="margin-top: 20px; color: #1890ff; font-style: italic; text-align: center; line-height: 1.6;">
|
||||
1、此功能仅用于建立〔VLESS Reality --> Socks5〕中转链路,<br>
|
||||
2、在此处添加的〔中转/落地机 VPS〕面板不能去开启“两步验证”,<br>
|
||||
3、在添加输入信息后,请点击列表中的“一键部署”按钮即可去使用,<br>
|
||||
4、而“中转/落地机”无需做任何设置,只需确保防火墙放行端口即可,<br>
|
||||
5、请勿在连“本机节点”时去进行〔一键部署〕,因“Xray重启”会有卡顿,<br>
|
||||
6、注:在已经创建好“中转节点”之后,勿去随意更改“用户Email + 端口”。
|
||||
</div>
|
||||
|
||||
</a-modal>
|
||||
|
||||
<a-modal v-model="resultModalVisible" :title="resultTitle" :footer="null" :width="450">
|
||||
<div style="text-align: center;">
|
||||
|
||||
<div style="text-align: left; color: #555; white-space: pre-wrap; line-height: 1.6; margin-bottom: 20px;">[[ resultSubtitle ]]</div>
|
||||
|
||||
<p style="color: #888; margin-bottom: 5px;">链路:本机(Reality) --->> 中转机(Socks) --->> 互联网</p>
|
||||
|
||||
<div
|
||||
id="qrcode-canvas"
|
||||
style="display: flex; justify-content: center; margin: 10px 0; cursor: pointer;"
|
||||
title="点击复制链接"
|
||||
@click="copyLink"
|
||||
></div>
|
||||
<p style="font-size: 12px; color: #aaa;">(点击二维码或链接即可复制)</p>
|
||||
|
||||
<p style="font-weight:bold; margin-top:15px; text-align: left;">👇 VLESS Reality 中转链接:</p>
|
||||
<a-input v-model="resultLink" read-only @click="copyLink">
|
||||
<a-icon slot="addonAfter" type="copy" @click="copyLink" style="cursor: pointer;"/>
|
||||
</a-input>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<a-modal v-model="checkModalVisible" title="检测中转节点“连通性”" :footer="null" width="700px">
|
||||
|
||||
<!-- 这里是提示框:在暗黑模式下文字看不清的地方 -->
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
bordered
|
||||
style="margin-bottom: 15px;"
|
||||
>
|
||||
<template slot="message">
|
||||
此功能通过 TCP Ping 模拟 v2rayN 测试原理,<br><br>
|
||||
检测〔上次部署生成的链接〕对应的端口是否通畅?
|
||||
</template>
|
||||
</a-alert>
|
||||
|
||||
<a-table
|
||||
:columns="checkColumns"
|
||||
:data-source="checkList"
|
||||
:row-key="record => record.ID"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
>
|
||||
<template slot="status" slot-scope="text, record">
|
||||
<span v-if="record.pingLoading">
|
||||
<a-icon type="loading" /> 检测中...
|
||||
</span>
|
||||
<span v-else>
|
||||
<a-tag v-if="record.pingResult === undefined">待检测</a-tag>
|
||||
<a-tag v-else-if="record.pingResult > 0" color="green">正常: [[ record.pingResult ]]ms</a-tag>
|
||||
<a-tag v-else color="red">无效 / 不通</a-tag>
|
||||
</span>
|
||||
</template>
|
||||
<template slot="action" slot-scope="text, record">
|
||||
<a-button type="primary" size="small" ghost @click="performPing(record)" :loading="record.pingLoading">
|
||||
立即检测
|
||||
</a-button>
|
||||
</template>
|
||||
</a-table>
|
||||
<div style="margin-top: 20px; text-align: right;">
|
||||
<a-button @click="checkModalVisible = false">关闭</a-button>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
</a-layout>
|
||||
|
||||
{{template "page/body_scripts" .}}
|
||||
{{template "component/aSidebar" .}}
|
||||
{{template "component/aThemeSwitch" .}}
|
||||
|
||||
<script>
|
||||
// 初始化 Vue 实例
|
||||
const app = new Vue({
|
||||
// 重要:修改 Vue 分隔符,防止与 Go 模板引擎冲突
|
||||
delimiters: ['[[', ']]'],
|
||||
// 【关键修复】:引入 MediaQueryMixin,这能让布局像 settings.html 一样自动适应,消除异常间隙
|
||||
mixins: [MediaQueryMixin],
|
||||
el: '#app',
|
||||
data: {
|
||||
themeSwitcher, // 引入主题切换数据
|
||||
loading: false,
|
||||
modalLoading: false,
|
||||
visible: false,
|
||||
checkModalVisible: false,
|
||||
resultModalVisible: false,
|
||||
resultTitle: '部署结果',
|
||||
resultSubtitle: '',
|
||||
resultLink: '',
|
||||
|
||||
checkList: [],
|
||||
checkColumns: [
|
||||
{ title: 'ID', dataIndex: 'ID', key: 'ID', width: 60 },
|
||||
{ title: '节点名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '状态', key: 'status', scopedSlots: { customRender: 'status' } },
|
||||
{ title: '操作', key: 'action', scopedSlots: { customRender: 'action' }, align: 'right' }
|
||||
],
|
||||
|
||||
servers: [],
|
||||
// 【修改这里】:定义两个独立的计数变量
|
||||
normalCount: 0, // 被控端数量
|
||||
transitCount: 0, // 中转机数量
|
||||
maxLimit: 0,
|
||||
// 【新增数据字段结束】
|
||||
form: {
|
||||
name: '',
|
||||
url: '',
|
||||
username: '',
|
||||
password: '',
|
||||
type: 0
|
||||
},
|
||||
|
||||
// 用于控制模态框显示的类型状态 (0=被控端, 1=中转机)
|
||||
formType: 0,
|
||||
// 【分页新增】:分页配置
|
||||
paginationConfig: {
|
||||
pageSize: 5, // 每页显示5条
|
||||
showSizeChanger: false,
|
||||
hideOnSinglePage: true,
|
||||
},
|
||||
// 【分页新增】:当前页数
|
||||
normalCurrentPage: 1,
|
||||
transitCurrentPage: 1,
|
||||
|
||||
// 表格列定义
|
||||
columns: [
|
||||
{ title: 'ID', dataIndex: 'ID', key: 'ID', width: 80 },
|
||||
{ title: '备注名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '面板地址', dataIndex: 'url', key: 'url' },
|
||||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||||
{
|
||||
title: '添加时间',
|
||||
dataIndex: 'CreatedAt', // 修改点1:这里必须改成大写的 CreatedAt
|
||||
key: 'CreatedAt',
|
||||
width: 180,
|
||||
// 修改点2:添加格式化,把 "2023-11-21T..." 变成 "2023-11-21 10:00"
|
||||
customRender: (text) => {
|
||||
if (!text) return '';
|
||||
// 简单处理:去掉 T,截取前19位
|
||||
return text.replace('T', ' ').substring(0, 19);
|
||||
}
|
||||
},
|
||||
{ title: '操作', key: 'action', scopedSlots: { customRender: 'action' }, width: 220 }
|
||||
]
|
||||
},
|
||||
|
||||
computed: {
|
||||
// 过滤普通被控端 (type=0)
|
||||
normalServers() {
|
||||
return this.servers.filter(s => s.type === 0 || s.type === undefined);
|
||||
},
|
||||
// 过滤中转机 (type=1)
|
||||
transitServers() {
|
||||
return this.servers.filter(s => s.type === 1);
|
||||
},
|
||||
|
||||
// 【分页核心】:普通被控端 - 当前页数据 (返回空列表)
|
||||
normalServersPage() {
|
||||
// 因为 this.normalServers 始终为空,所以这里返回空
|
||||
return [];
|
||||
},
|
||||
// 【分页核心】:中转机 - 当前页数据 (返回空列表)
|
||||
transitServersPage() {
|
||||
// 因为 this.transitServers 始终为空,所以这里返回空
|
||||
return [];
|
||||
},
|
||||
|
||||
// 【分页核心】:分页对象 (total 始终为 0)
|
||||
normalPagination() {
|
||||
return {...this.paginationConfig, current: this.normalCurrentPage, total: 0};
|
||||
},
|
||||
// 【分页核心】:分页对象 (total 始终为 0)
|
||||
transitPagination() {
|
||||
return {...this.paginationConfig, current: this.transitCurrentPage, total: 0};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 【新增跳转逻辑】
|
||||
// 点击后跳转到 settings 页面,并带上 query 参数 tab=telegram
|
||||
goToTgSettings() {
|
||||
window.location.href = "./settings?tab=telegram";
|
||||
},
|
||||
|
||||
// 【分页新增】:处理普通列表分页切换
|
||||
handleNormalTableChange(pagination) {
|
||||
this.normalCurrentPage = pagination.current;
|
||||
},
|
||||
// 【分页新增】:处理中转列表分页切换
|
||||
handleTransitTableChange(pagination) {
|
||||
this.transitCurrentPage = pagination.current;
|
||||
},
|
||||
|
||||
// 【拦截方法】:拦截获取链接操作
|
||||
getLastLink(record) {
|
||||
// 【免费版】:拦截获取链接操作
|
||||
this.$message.warning("〔免费基础版〕不支持此操作");
|
||||
return;
|
||||
},
|
||||
|
||||
// 执行一键中转部署
|
||||
async setupRelay(record) {
|
||||
// 【免费版修改】:拦截部署操作
|
||||
this.$message.warning("〔免费基础版〕不支持此操作");
|
||||
return;
|
||||
},
|
||||
|
||||
// 获取服务器列表
|
||||
async getServers() {
|
||||
// 1. 模拟加载结束
|
||||
this.loading = false;
|
||||
// 2. 强制清空列表
|
||||
this.servers = [];
|
||||
// 3. 【关键】:强制设置数量和额度为 0
|
||||
this.normalCount = 0;
|
||||
this.transitCount = 0;
|
||||
this.maxLimit = 0; // 强制显示“最多可绑:0 台”
|
||||
},
|
||||
|
||||
|
||||
// 打开添加弹窗
|
||||
// openAddModal 方法接收 type 参数
|
||||
// type: 0 = 普通被控端, 1 = 中转机
|
||||
openAddModal(type = 0) {
|
||||
// 1. 设置当前模态框的 UI 类型
|
||||
this.formType = type;
|
||||
|
||||
// 2. 初始化表单,并将 type 写入表单数据中
|
||||
this.form = {
|
||||
name: '',
|
||||
url: 'https://',
|
||||
username: '',
|
||||
password: '',
|
||||
type: type // 【关键】:这里确保提交给后端的数据包含正确的类型
|
||||
};
|
||||
|
||||
this.visible = true;
|
||||
},
|
||||
|
||||
// 提交添加请求
|
||||
async submitServer() {
|
||||
|
||||
// 1. 先关闭刚才填写的模态框,
|
||||
this.visible = false;
|
||||
|
||||
// 2. 使用 Vue 的 createElement 函数构建支持 HTML 的提示内容
|
||||
const h = this.$createElement;
|
||||
|
||||
// 3. 弹出警告提示框
|
||||
this.$warning({
|
||||
title: '功能受限提示',
|
||||
okText: '知道了',
|
||||
// 【核心修改点 1】:调整弹窗宽度,使其足够容纳一行文字
|
||||
width: 580,
|
||||
// 这里定义提示框的具体内容和样式
|
||||
content: h('div', { style: 'margin-top: 10px; font-size: 15px; line-height: 1.6;' }, [ // 增加行高,提升阅读体验
|
||||
|
||||
// 第一段:设置底部外边距,模拟空行
|
||||
h('p', { style: 'margin-bottom: 15px;' }, '此项功能是“付费Pro版”专属功能,免费版不能用,'),
|
||||
|
||||
// 第二段:设置底部外边距,模拟空行
|
||||
h('p', { style: 'margin-bottom: 15px;' }, '请联系面板管理员〔购买授权码〕之后才能继续使用。'),
|
||||
|
||||
// 第三段:TG 链接(无需底部外边距)
|
||||
h('p', { style: 'color: #ff4d4f; font-weight: bold; margin-bottom: 0;' }, [
|
||||
'----->>> “授权码购买”机器人:',
|
||||
h('a', {
|
||||
// 设置跳转链接到 Telegram
|
||||
attrs: {
|
||||
href: 'https://t.me/Buy_ShouQuan_Bot',
|
||||
target: '_blank' // 在新标签页打开
|
||||
},
|
||||
style: {
|
||||
color: '#1890ff', // 链接颜色
|
||||
fontWeight: 'bold',
|
||||
textDecoration: 'underline' // 链接下划线
|
||||
}
|
||||
}, '@Buy_ShouQuan_Bot')
|
||||
]),
|
||||
]),
|
||||
onOk() {}
|
||||
});
|
||||
},
|
||||
|
||||
// 删除服务器
|
||||
deleteServer(record) {
|
||||
// 【免费版修改】:拦截删除操作
|
||||
this.$message.warning("〔免费基础版〕不支持此操作");
|
||||
return;
|
||||
},
|
||||
|
||||
// 【拦截方法】:拦截【检测中转节点】操作
|
||||
openCheckModal() {
|
||||
// 【免费版修改】:拦截【检测中转节点】操作
|
||||
this.$message.warning("〔免费基础版〕不支持此操作");
|
||||
return;
|
||||
},
|
||||
copyLink() {
|
||||
if(!this.resultLink) return;
|
||||
const input = document.createElement('input');
|
||||
input.value = this.resultLink;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand('Copy');
|
||||
document.body.removeChild(input);
|
||||
this.$message.success('复制成功');
|
||||
},
|
||||
performPing(record) {
|
||||
// 占位
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getServers();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{ template "page/body_end" .}}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user