Initial commit (code only without large binaries)

This commit is contained in:
robin
2026-02-15 18:58:44 +08:00
commit 35df75498f
9442 changed files with 1495866 additions and 0 deletions

View File

@@ -0,0 +1 @@
{$layout}

View File

@@ -0,0 +1,9 @@
{$var "header"}
<!-- code editor -->
<script src="/codemirror/lib/codemirror.js" type="text/javascript"></script>
<script src="/codemirror/mode/meta.js" type="text/javascript"></script>
<script src="/codemirror/addon/edit/matchbrackets.js" type="text/javascript"></script>
<script src="/codemirror/addon/mode/loadmode.js" type="text/javascript"></script>
<link rel="stylesheet" href="/codemirror/lib/codemirror.css" type="text/css"/>
<link rel="stylesheet" href="/codemirror/theme/idea.css" type="text/css"/>
{$end}

View File

@@ -0,0 +1,8 @@
{$var "header"}
<!-- datepicker -->
<script type="text/javascript" src="/js/moment.min.js"></script>
<script type="text/javascript" src="/js/pikaday.js"></script>
<link rel="stylesheet" href="/js/pikaday.css"/>
<link rel="stylesheet" href="/js/pikaday.theme.css"/>
<link rel="stylesheet" href="/js/pikaday.triangle.css"/>
{$end}

View File

@@ -0,0 +1,4 @@
{$var "header"}
<!-- echart -->
<script type="text/javascript" src="/js/echarts/echarts.min.js"></script>
{$end}

View File

@@ -0,0 +1,9 @@
<div id="footer" class="ui menu inverted light-blue borderless small" v-if="teaShowOpenSourceInfo" v-cloak>
<a href="/settings/upgrade" class="item" title="点击进入检查版本更新页面"><span v-if="teaName && teaVersion">{{teaName}} v{{teaVersion}}</span><span v-else style="display:none;"></span></a>
<a href="https://goedge.cn" target="_blank" class="item">官网</a>
<a href="https://goedge.cn/docs" target="_blank" class="item">文档</a>
<a href="https://github.com/TeaOSLab/EdgeAdmin" target="_blank" class="item">GitHub</a>
<a href="https://github.com/TeaOSLab/EdgeAdmin/issues" target="_blank" class="item">提Bug</a>
<a class="item" href="https://goedge.cn/community/telegram" target="_blank" title="点击跳转到加群页面">Telegram群 &nbsp;<i class="icon paper plane"></i></a>
<a class="item right" href="https://goedge.cn/commercial" target="_blank" v-if="!teaIsPlus">企业版</a>
</div>

View File

@@ -0,0 +1,42 @@
.grid.counter-chart {
margin-top: 1em !important;
margin-left: 0.4em !important;
}
.grid.counter-chart .column {
margin-bottom: 1em;
font-size: 0.85em;
text-align: center;
position: relative;
border: 1px rgba(0, 0, 0, 0.1) solid;
border-right: 0;
}
.grid.counter-chart .column div.value {
margin-top: 1.5em;
font-weight: normal;
}
.grid.counter-chart .column div.value span {
font-size: 1.5em;
margin-right: 0.2em;
}
.grid.counter-chart .column.with-border {
border-right: 1px rgba(0, 0, 0, 0.1) solid;
}
.grid.counter-chart h4 {
color: grey;
position: relative;
font-size: 1em;
text-align: left;
}
.grid.counter-chart h4 a {
position: absolute;
right: 0.1em;
font-size: 1.26em;
display: none;
}
.grid.counter-chart .column:hover {
background: rgba(0, 0, 0, 0.03) !important;
}
.grid.counter-chart .column:hover a {
display: inline;
}
/*# sourceMappingURL=@grids.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["@grids.less"],"names":[],"mappings":"AAAA,KAAK;EACJ,0BAAA;EACA,kBAAA;;AAFD,KAAK,cAIJ;EACC,kBAAA;EACA,iBAAA;EACA,kBAAA;EACA,kBAAA;EACA,oCAAA;EACA,eAAA;;AAVF,KAAK,cAIJ,QAQC,IAAG;EACF,iBAAA;EACA,mBAAA;;AAdH,KAAK,cAIJ,QAQC,IAAG,MAIF;EACC,gBAAA;EACA,mBAAA;;AAlBJ,KAAK,cAuBJ,QAAO;EACN,0CAAA;;AAxBF,KAAK,cA2BJ;EACC,WAAA;EACA,kBAAA;EASA,cAAA;EACA,gBAAA;;AAvCF,KAAK,cA2BJ,GAIC;EACC,kBAAA;EACA,YAAA;EACA,iBAAA;EACA,aAAA;;AAnCH,KAAK,cA0CJ,QAAO;EACN,+BAAA;;AA3CF,KAAK,cA0CJ,QAAO,MAGN;EACC,eAAA","file":"@grids.css"}

View File

@@ -0,0 +1,77 @@
.grid.counter-chart {
margin-top: 1em !important;
margin-left: 0.4em !important;
.column {
margin-bottom: 1em;
font-size: 0.85em;
text-align: center;
position: relative;
border: 1px rgba(0, 0, 0, .1) solid;
border-right: 0;
div.value {
margin-top: 1.5em;
font-weight: normal;
span {
font-size: 1.5em;
margin-right: 0.2em;
}
}
}
.column.with-border {
border-right: 1px rgba(0, 0, 0, .1) solid;
}
h4 {
color: grey;
position: relative;
a {
position: absolute;
right: 0.1em;
font-size: 1.26em;
display: none;
}
font-size: 1.0em;
text-align: left;
}
.column:hover {
background: rgba(0, 0, 0, .03) !important;
a {
display: inline;
}
}
}
.grid.chart-grid {
margin-top: 1em !important;
margin-left: 0.4em !important;
.column {
margin-bottom: 1em;
border: 1px rgba(0, 0, 0, .1) solid;
border-right: 0;
.menu {
margin-top: -0.6em !important;
margin-bottom: -0.6em !important;
}
h4 {
span {
font-size: 0.8em;
color: grey;
}
}
}
.column.with-border {
border-right: 1px rgba(0, 0, 0, .1) solid;
}
}

View File

@@ -0,0 +1,937 @@
.left-box {
width: 8.5em;
position: fixed;
top: 7.5em;
bottom: 2.4em;
overflow-y: auto;
overflow-x: hidden;
border-right: 1px #ddd solid;
}
.left-box .menu {
width: 95% !important;
}
.left-box .menu .item {
line-height: 1.2;
position: relative;
padding-left: 1em !important;
}
.left-box .menu .item .icon {
position: absolute;
top: 50%;
left: 0;
margin-top: -0.4em !important;
}
.left-box .menu .item.separator {
border-bottom: 1px #eee solid !important;
padding-top: 0;
padding-bottom: 0;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.left-box .menu .item.on span {
border-bottom: 1px #666 dashed;
}
.left-box .menu .item.off span var {
font-style: normal;
background: #db2828;
color: white;
font-size: 8px;
padding: 2px;
border-radius: 2px;
margin-left: 1em;
}
.left-box .menu .item.active {
background: rgba(230, 230, 230, 0.35) !important;
border-radius: 3px;
}
.left-box .menu .header {
border-bottom: 1px #ddd solid;
padding-left: 0 !important;
padding-bottom: 1em !important;
}
.left-box::-webkit-scrollbar {
width: 4px;
}
.left-box.disabled {
opacity: 0.1;
}
.left-box.tiny {
top: 10em;
}
.left-box.without-tabbar {
top: 3em;
}
.left-box.with-menu {
top: 8.7em;
}
.left-box.without-menu {
top: 6em;
}
.right-box {
position: fixed;
top: 7em;
bottom: 1.3em;
right: 0;
left: 18em;
padding-right: 2em;
padding-bottom: 2em;
overflow-y: auto;
}
.right-box h4:first-child {
margin-top: 1em;
}
.right-box > .comment:first-child {
margin-top: 0.5em;
}
@media screen and (max-width: 512px) {
.right-box {
left: 13em;
padding-right: 1em;
}
}
body.expanded .right-box {
left: 10em;
}
.right-box.tiny {
top: 10em;
left: 26.5em;
}
.right-box::-webkit-scrollbar {
width: 4px;
}
.right-box.without-tabbar {
top: 3em;
}
.right-box.with-menu {
top: 8.6em;
}
.right-box.without-menu {
top: 6em;
}
.main.without-footer .left-box {
bottom: 0.2em;
}
.narrow-scrollbar::-webkit-scrollbar {
width: 4px;
}
.grid.counter-chart {
margin-top: 1em !important;
margin-left: 0.4em !important;
}
.grid.counter-chart .column {
margin-bottom: 1em;
font-size: 0.85em;
text-align: center;
position: relative;
border: 1px rgba(0, 0, 0, 0.1) solid;
border-right: 0;
}
.grid.counter-chart .column div.value {
margin-top: 1.5em;
font-weight: normal;
}
.grid.counter-chart .column div.value span {
font-size: 1.5em;
margin-right: 0.2em;
}
.grid.counter-chart .column.with-border {
border-right: 1px rgba(0, 0, 0, 0.1) solid;
}
.grid.counter-chart h4 {
color: grey;
position: relative;
font-size: 1em;
text-align: left;
}
.grid.counter-chart h4 a {
position: absolute;
right: 0.1em;
font-size: 1.26em;
display: none;
}
.grid.counter-chart .column:hover {
background: rgba(0, 0, 0, 0.03) !important;
}
.grid.counter-chart .column:hover a {
display: inline;
}
.grid.chart-grid {
margin-top: 1em !important;
margin-left: 0.4em !important;
}
.grid.chart-grid .column {
margin-bottom: 1em;
border: 1px rgba(0, 0, 0, 0.1) solid;
border-right: 0;
}
.grid.chart-grid .column .menu {
margin-top: -0.6em !important;
margin-bottom: -0.6em !important;
}
.grid.chart-grid .column h4 span {
font-size: 0.8em;
color: grey;
}
.grid.chart-grid .column.with-border {
border-right: 1px rgba(0, 0, 0, 0.1) solid;
}
/** 通用 **/
* {
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
scrollbar-width: thin;
}
.clear {
clear: both;
}
.hidden {
display: none;
}
pre {
white-space: pre-wrap;
}
a.disabled,
a.disabled:hover,
a.disabled:active,
span.disabled {
color: #ccc !important;
}
a.enabled,
span.enabled,
span.green {
color: #21ba45;
}
span.grey,
label.grey,
p.grey {
color: grey !important;
}
p.grey {
margin-top: 0.8em;
}
span.red,
pre.red {
color: #db2828;
}
span.blue {
color: #4183c4;
}
span.orange {
color: #ff851b;
}
pre:not(.CodeMirror-line) {
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif !important;
}
tbody {
background: transparent;
}
.table-box {
margin-top: 1em;
overflow-x: auto;
}
.table-box::-webkit-scrollbar {
height: 6px;
}
.table.width30 {
width: 30em !important;
}
.table.width35 {
width: 35em !important;
}
.table.width40 {
width: 40em !important;
}
.table.width1024 {
width: 1024px !important;
}
.table th,
.table td {
font-size: 0.9em !important;
}
.table tr.active td {
background: rgba(0, 0, 0, 0.01) !important;
}
p.comment,
div.comment {
color: #959da6;
padding-top: 0.4em;
font-size: 1em;
}
p.comment em,
div.comment em {
font-style: italic !important;
}
.truncate {
white-space: nowrap;
-ms-text-overflow: ellipsis;
overflow: hidden;
text-overflow: ellipsis;
}
div.margin,
p.margin {
margin-top: 1em;
}
.opacity-mask {
opacity: 0.3;
}
/** 操作按钮容器 **/
.op.one {
width: 4em;
}
.op.two {
width: 7.4em;
}
.op.three {
width: 9em;
}
.op.four {
width: 12em;
}
/** 主菜单 **/
.main-menu {
width: 8em !important;
}
.main-menu .ui.menu {
width: 100% !important;
}
.main-menu .ui.menu .item.separator {
border-top: 1px rgba(0, 0, 0, 0.2) solid;
height: 1px;
min-height: 0;
padding: 0;
}
@media screen and (max-width: 512px) {
.main-menu {
width: auto !important;
}
.main-menu .ui.menu {
width: 3.6em !important;
}
.main-menu .ui.menu .item.separator {
display: none;
}
.main-menu .ui.menu .item {
padding-top: 2em;
padding-bottom: 2.4em;
}
}
.main-menu .ui.labeled.icon.menu .item {
font-size: 0.9em;
}
.main-menu .ui.menu {
padding-bottom: 3em;
}
.main-menu .ui.menu .item .subtitle {
display: none;
}
.main-menu .ui.menu .item.expend .subtitle {
display: block;
font-size: 10px;
padding-left: 2em;
margin-top: 0.5em;
color: grey;
}
@media screen and (max-width: 512px) {
.main-menu .ui.menu .item.expend .subtitle {
display: none;
}
}
.main-menu .ui.menu .sub-items .item {
padding-left: 2.8em !important;
padding-right: 0.4em !important;
}
.main-menu .ui.menu .sub-items .item .icon {
position: absolute;
left: 1.1em;
top: 0.93em;
}
.main-menu .ui.menu .sub-items .item .label {
margin-left: 0;
margin-right: 0;
padding-left: 0.4em;
padding-right: 0.4em;
min-width: 2em;
}
@media screen and (max-width: 512px) {
.main-menu .ui.menu .sub-items .item {
padding-left: 1em !important;
}
}
.main-menu .ui.menu .sub-items .item.active {
background-color: #2185d0 !important;
}
/** 扩展UI **/
.field.text {
padding: 0.5em;
}
.form .fields:not(.inline) .field {
margin-bottom: 0.5em !important;
}
.form .fields:not(.inline) .field .button {
min-width: 5em;
}
/** body **/
@keyframes blink {
from {
opacity: 0.1;
}
to {
opacity: 0.8;
}
}
@keyframes rotation {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
body .ui.menu .item .blink {
animation: blink 1s infinite;
}
body .ui.menu .item:not(:hover) span.rotate {
animation: rotation 3s infinite;
}
body.expanded .main-menu {
display: none;
}
body.expanded .main {
left: 1em;
}
/** 布局相关 */
.top-nav {
border-radius: 0 !important;
position: fixed;
width: 100%;
z-index: 1000;
overflow-x: auto;
border: 0 !important;
}
.top-nav img.avatar {
width: 1.6em !important;
height: 1.6em !important;
padding: 0.2em;
background: #fff;
border-radius: 0.9em;
margin-right: 0.5em !important;
}
.top-nav em {
font-style: normal;
font-size: 0.9em;
padding-left: 0.2em;
}
.top-nav .item .hover-span span {
display: none;
}
.top-nav .item:hover .hover-span span {
display: inline;
}
.top-nav .item.red {
color: red !important;
}
.top-nav.theme1 {
background: #14539A !important;
}
.top-nav.theme2 {
background: #276AC6 !important;
}
.top-nav.theme3 {
background: #007D9C !important;
}
.top-nav.theme4 {
background: #A12568 !important;
}
.top-nav.theme5 {
background: #1C7947 !important;
}
.top-nav.theme6 {
background: #1D365D !important;
}
.top-nav.theme7 {
background: black !important;
}
.top-nav::-webkit-scrollbar {
height: 2px;
}
/** 顶部菜单 **/
.top-secondary-menu {
position: fixed;
top: 2.6em;
left: 8.2em;
right: 0;
z-index: 100;
background: white;
}
.top-secondary-menu .menu {
margin-top: 0 !important;
margin-bottom: 0 !important;
border-radius: 0 !important;
}
.top-secondary-menu .menu var {
font-style: normal;
}
.top-secondary-menu .divider {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
@media screen and (max-width: 512px) {
.top-secondary-menu {
left: 4em;
}
}
/** 右侧主操作区 **/
.main {
position: absolute;
left: 22em;
top: 5.6em;
padding-bottom: 5em;
padding-right: 0.2em;
right: 1em;
}
@media screen and (max-width: 512px) {
.main {
left: 4em;
}
.main .main-box {
display: block;
}
}
.main.without-menu {
left: 9em;
}
.main.without-secondary-menu {
top: 2.9em;
}
@media screen and (max-width: 512px) {
.main.without-menu {
left: 4em;
}
}
.main table td.title {
width: 10em;
}
.main table td.middle-title {
width: 14em;
}
.main table td {
vertical-align: top;
}
.main table td.color-border {
border-left: 1px #276ac6 solid !important;
}
.main table td.vertical-top {
vertical-align: top;
}
.main table td.vertical-middle {
vertical-align: middle;
}
.main table td[colspan="2"] a {
font-weight: normal;
}
.main table td em {
font-weight: normal;
font-style: normal;
font-size: 0.9em;
}
.main table td em.grey {
color: grey;
}
.main h3 {
font-weight: normal;
margin-top: 1em !important;
position: relative;
}
.main h3 span {
font-size: 0.8em;
}
.main h3 span.label {
color: #6435c9;
}
.main h3 a {
margin-left: 1em;
font-size: 14px !important;
right: 1em;
}
.main h4 {
font-weight: normal;
}
.main form h4 {
margin-top: 0.6em;
}
.main td span.small {
font-size: 0.8em;
}
.main .button.mini {
font-size: 0.8em;
padding: 0.2em;
margin-left: 1em;
}
.main-menu {
position: fixed;
/**top: 1.05em;**/
top: 2em;
bottom: 0;
overflow-y: auto;
z-index: 10;
}
.main-menu .menu {
border: 0 !important;
box-shadow: none !important;
}
.main-menu.theme1 {
background: #14539A !important;
}
.main-menu.theme1 .menu {
background: #14539A !important;
}
.main-menu.theme2 {
background: #276AC6 !important;
}
.main-menu.theme2 .menu {
background: #276AC6 !important;
}
.main-menu.theme3 {
background: #007D9C !important;
}
.main-menu.theme3 .menu {
background: #007D9C !important;
}
.main-menu.theme4 {
background: #A12568 !important;
}
.main-menu.theme4 .menu {
background: #A12568 !important;
}
.main-menu.theme5 {
background: #1C7947 !important;
}
.main-menu.theme5 .menu {
background: #1C7947 !important;
}
.main-menu.theme6 {
background: #1D365D !important;
}
.main-menu.theme6 .menu {
background: #1D365D !important;
}
.main-menu.theme7 {
background: black !important;
}
.main-menu.theme7 .menu {
background: black !important;
}
.main-menu::-webkit-scrollbar {
width: 4px;
}
.main .tab-menu {
margin-top: 0.3em !important;
margin-bottom: 0 !important;
overflow-x: auto;
overflow-y: hidden;
}
.main .tab-menu .item {
padding: 0 1em !important;
}
.main .tab-menu .item var {
font-style: normal;
}
.main .tab-menu .item span {
font-size: 0.8em;
padding-left: 0.3em;
}
.main .tab-menu .item .icon {
margin-left: 0.6em;
}
.main .tab-menu .item.active.title {
font-weight: normal !important;
margin-right: 1em !important;
border-radius: 0 !important;
}
.main .tab-menu .item:hover {
background: #f8f8f9 !important;
border-width: 1px;
}
.main .tab-menu .item.active:not(.title) {
font-weight: normal !important;
border: none;
border-radius: 0 !important;
color: #2185d0 !important;
}
.main .tab-menu .item.active:not(.title) .bottom-indicator {
border-bottom: 1px #2185d0 solid;
position: absolute;
left: 0;
right: 0;
bottom: 1px;
}
.main .tab-menu .item.active:not(.title).icon .bottom-indicator {
border-bottom: 1px #2185d0 solid;
position: absolute;
left: 0;
right: 0.6em;
bottom: 1px;
}
.main .tab-menu .item.active.blue {
font-weight: bold !important;
}
.main .tab-menu::-webkit-scrollbar {
height: 4px;
}
.main .go-top-btn {
position: fixed;
right: 2.6em;
bottom: 2em;
font-size: 2em;
line-height: 1.4em;
border-radius: 1em;
z-index: 999999;
background: white;
}
/** 右侧文本子菜单 **/
.text.menu {
overflow-x: auto;
}
.text.menu::-webkit-scrollbar {
width: 4px;
height: 4px;
}
/** 脚部相关样式 **/
#footer {
position: fixed;
bottom: 0;
text-align: left;
color: gray;
width: 100%;
border-radius: 0 !important;
z-index: 10;
overflow-x: auto;
}
#footer::-webkit-scrollbar {
height: 2px;
}
#footer a {
font-size: 0.9em;
}
#footer a form {
display: none;
}
#footer a:hover span,
#footer a:active span {
display: none;
}
#footer a:hover form,
#footer a:active form {
display: block;
}
#footer form input {
padding: 0;
margin: 0;
}
#footer-outer-box {
z-index: 999999;
position: fixed;
left: 0;
top: 0;
right: 0;
background: rgba(0, 0, 0, 0.8);
bottom: 2.6em;
}
#footer-outer-box .qrcode {
width: 20em;
position: absolute;
top: 50%;
left: 50%;
margin-top: -14em;
margin-left: -10em;
}
#footer-outer-box .qrcode img {
width: 100%;
}
#footer-outer-box .qrcode a {
position: absolute;
right: 0.5em;
top: 0.5em;
}
@media screen and (max-width: 512px) {
#footer-outer-box .qrcode {
margin-left: 0;
left: 3.5em;
}
}
/** Vue **/
[v-cloak] {
display: none !important;
}
/** auto complete **/
.autocomplete-box .menu {
background: #eee !important;
}
.autocomplete-box .menu::-webkit-scrollbar {
width: 4px;
}
.autocomplete-box .menu .item {
border-top: none !important;
}
select.auto-width {
width: auto !important;
}
/** column **/
@media screen and (max-width: 512px) {
.column:not(.one) {
width: 100% !important;
}
}
label[for] {
cursor: pointer !important;
}
label.blue {
color: #2185d0 !important;
}
/** Menu **/
.first-menu .menu.text {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.first-menu .divider {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.second-menu .menu.text {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.second-menu .menu.text em {
font-style: normal;
}
.second-menu .divider {
margin-top: 0 !important;
}
.menu a {
outline: none;
}
/** var **/
span.olive,
var.olive {
color: #b5cc18 !important;
}
span.dash {
border-bottom: 1px dashed grey;
}
span.hover:hover {
background: #eee;
}
var.normal {
font-style: normal;
}
/** checkbox **/
.checkbox label a,
.checkbox label {
font-size: 0.9em !important;
}
/** page **/
.page {
margin-top: 1em;
border-left: 1px solid #ddd;
}
.page a {
display: inline-block;
background: #fafafa;
color: #666;
padding: 6px 12px;
margin: 0;
font-size: 0.9em;
border: 1px solid #ddd;
border-left: 0;
}
.page a.active {
background: #2185d0 !important;
color: white;
}
.page a:hover {
background: #eee;
}
.page select {
padding-top: 0.3em !important;
padding-bottom: 0.3em !important;
}
/** popup **/
.swal2-html-container {
overflow-x: hidden;
}
.swal2-close,
.swal2-close:focus {
border: 0;
}
.swal2-confirm:focus,
.swal2-cancel:focus {
border: 3px #ddd solid !important;
}
.swal2-confirm,
.swal2-cancel {
border: 3px #fff solid !important;
}
.swal2-cancel {
margin-left: 2em !important;
}
/** 排序 **/
.sortable-ghost {
background: #ddd !important;
opacity: 0.1;
}
.sortable-drag {
opacity: 1;
}
.icon.handle {
cursor: pointer;
}
.label.port-label {
margin-top: 0.4em !important;
margin-bottom: 0.4em !important;
display: block;
line-height: 1.5;
}
.label {
word-break: break-all;
}
td .label.small {
margin-bottom: 0.2em !important;
}
td {
word-break: break-all;
}
.source-code-box .CodeMirror {
border: 1px solid #eee;
height: auto !important;
}
.source-code-box .CodeMirror-vscrollbar {
width: 6px;
border-radius: 3px !important;
}
.source-code-box .CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
border-radius: 2px;
}
.scroll-box {
overflow-y: auto;
}
.scroll-box::-webkit-scrollbar {
width: 4px;
}
input.error {
border: 1px #e0b4b4 solid !important;
}
textarea.wide-code {
font-family: Menlo, Monaco, "Courier New", monospace !important;
line-height: 1.6 !important;
}
.combo-box .menu {
max-height: 17em;
overflow-y: auto;
position: absolute;
border: rgba(129, 177, 210, 0.81) 1px solid;
border-top: 0;
z-index: 100;
}
.combo-box .menu::-webkit-scrollbar {
width: 4px;
}
/*# sourceMappingURL=@layout.css.map */

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,157 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<title>{$ htmlEncode .teaTitle}</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
{$if eq .teaFaviconFileId 0}
<link rel="shortcut icon" href="/images/favicon.png"/>
{$else}
<link rel="shortcut icon" href="/ui/image/{$.teaFaviconFileId}"/>
{$end}
<link rel="stylesheet" type="text/css" href="/_/@default/@layout.css" media="all"/>
{$TEA.SEMANTIC}
{$TEA.VUE}
{$echo "header"}
<!-- 品牌配置 -->
<script type="text/javascript">
window.BRAND_OFFICIAL_SITE = {$ jsonEncode .brandConfig.officialSite};
window.BRAND_DOCS_SITE = {$ jsonEncode .brandConfig.docsSite};
window.BRAND_DOCS_PREFIX = {$ jsonEncode .brandConfig.docsPathPrefix};
window.BRAND_PRODUCT_NAME = {$ jsonEncode .brandConfig.productName};
// 确保 teaName 和 teaVersion 在 Vue 初始化前可用
if (typeof window.TEA === "undefined") {
window.TEA = {};
}
if (typeof window.TEA.ACTION === "undefined") {
window.TEA.ACTION = {};
}
if (typeof window.TEA.ACTION.data === "undefined") {
window.TEA.ACTION.data = {};
}
window.TEA.ACTION.data.teaName = {$ jsonEncode .teaName};
window.TEA.ACTION.data.teaVersion = {$ jsonEncode .teaVersion};
</script>
<script type="text/javascript" src="/js/config/brand.js"></script>
<script type="text/javascript" src="/_/@default/@layout.js"></script>
<script type="text/javascript" src="/js/components.js"></script>
<script type="text/javascript" src="/js/utils.min.js"></script>
<script type="text/javascript" src="/js/sweetalert2/dist/sweetalert2.all.min.js" async></script>
<script type="text/javascript" src="/js/date.tea.js"></script>
<script type="text/javascript" src="/js/langs/base.js?v={$ .teaVersion}"></script>
<script type="text/javascript" src="/js/langs/{$.teaLang}.js?v={$ .teaVersion}"></script>
<link rel="stylesheet" type="text/css" href="/js/langs/{$.teaLang}.css?v={$ .teaVersion}"/>
<link rel="stylesheet" type="text/css" href="/_/@default/@layout_override.css" media="all"/>
</head>
<body>
<div>
<!-- 顶部导航 -->
<div class="ui menu top-nav blue inverted small borderless" :class="(teaTheme == null || teaTheme.length == 0) ? 'theme2': teaTheme" v-cloak="">
<a href="/" class="item">
<i class="ui icon leaf" v-if="teaLogoFileId == 0"></i><img alt="logo" v-if="teaLogoFileId > 0" :src="'/ui/image/' + teaLogoFileId" style="width: auto;height: 1.6em"/> &nbsp; {{teaTitle}}&nbsp;<sup v-if="teaShowVersion">v{{teaVersion}}<span v-if="teaVersion.split('.').length == 4" title="当前版本为测试版,再次感谢您参与测试"> beta</span></sup> &nbsp;
</a>
<div class="right menu">
<!-- 集群同步 -->
<a href="" class="item" v-if="teaCheckNodeTasks && doingNodeTasks.isUpdated" @click.prevent="showNodeTasks()">
<span v-if="!doingNodeTasks.isDoing && !doingNodeTasks.hasError" class="hover-span"><i class="icon cloud disabled"></i><span class="disabled">已同步节点</span></span>
<span v-if="doingNodeTasks.isDoing && !doingNodeTasks.hasError" class="hover-span rotate"><i class="icon cloud"></i><span>正在同步节点...</span></span>
<span v-if="doingNodeTasks.hasError" class="red"><i class="icon cloud"></i>节点同步失败</span>
</a>
<!-- DNS同步 -->
<a href="" class="item" v-if="teaCheckDNSTasks && doingDNSTasks.isUpdated" @click.prevent="showDNSTasks()">
<span v-if="!doingDNSTasks.isDoing && !doingDNSTasks.hasError" class="hover-span"><i class="icon globe disabled"></i><span class="disabled">已同步DNS</span></span>
<span v-if="doingDNSTasks.isDoing && !doingDNSTasks.hasError" class="hover-span rotate"><i class="icon globe"></i><span>正在同步DNS...</span></span>
<span v-if="doingDNSTasks.hasError" class="red"><i class="icon globe"></i>DNS同步失败</span>
</a>
<!-- 消息 -->
<a href="" class="item" :class="{active:teaMenu == 'message'}" @click.prevent="showMessages()">
<span v-if="globalMessageBadge > 0" class="blink hover-span"><i class="icon bell"></i><span>消息({{globalMessageBadge}}) </span></span>
<span v-if="globalMessageBadge == 0" class="hover-span"><i class="icon bell disabled"></i><span class="disabled">消息(0)</span></span>
</a>
<!-- 用户信息 -->
<a href="/settings/profile" class="item">
<i class="icon user" v-if="teaUserAvatar.length == 0"></i>
<img class="avatar" alt="" :src="teaUserAvatar" v-if="teaUserAvatar.length > 0"/>
<span class="hover-span"><span class="disabled">{{teaUsername}}</span></span>
</a>
<a href="" class="item" title="switch language" @click.prevent="switchLang" v-show="false"><i class="icon language"></i> </a>
<!-- 背景颜色 -->
<a href="" class="item" title="点击切换界面风格" @click.prevent="changeTheme()" v-if="false"><i class="icon adjust"></i></a>
<!-- 企业版 -->
<!-- <a :href="'/settings/authority'" class="item" title="商业版" :v-if="teaIsPlus"><i class="icon gem outline yellow"></i></a>-->
<!-- 退出登录 -->
<a :href="Tea.url('logout')" class="item" title="安全退出登录"><i class="icon sign out"></i>
<span class="hover-span"><span class="disabled">退出登录</span></span>
</a>
</div>
</div>
<!-- 左侧主菜单 -->
<div class="main-menu" :class="(teaTheme == null || teaTheme.length == 0) ? 'theme2': teaTheme" v-cloak="">
<div class="ui labeled menu vertical blue inverted tiny borderless">
<div class="item"></div>
<!--<a :href="Tea.url('dashboard')" class="item" :class="{active:teaMenu == 'dashboard'}">
<i class="ui dashboard icon"></i>
<span>仪表板</span>
</a>-->
<!-- 模块 -->
<div v-for="module in teaModules">
<a class="item" :href="Tea.url(module.code)" :class="{active:teaMenu == module.code && teaSubMenu.length == 0, separator:module.code.length == 0, expend: teaMenu == module.code}" :style="(teaMenu == module.code && teaSubMenu.length == 0) ? 'background: rgba(230, 230, 230, 0.45) !important;' : ''" v-if="module.isOn !== false">
<span v-if="module.code.length > 0">
<i class="window restore outline icon" v-if="module.icon == null"></i>
<i class="ui icon" v-if="module.icon != null" :class="module.icon"></i>
<span class="module-name">{{module.name}}</span>
</span>
<div class="subtitle" v-if="module.subtitle != null && module.subtitle.length > 0">{{module.subtitle}}</div>
</a>
<div v-if="teaMenu == module.code" class="sub-items">
<a class="item" :class="{separator:subItem.name == '-', active: subItem.code == teaSubMenu}" v-for="subItem in module.subItems" v-if="subItem.isOn !== false" :href="subItem.url" :style="(subItem.code == teaSubMenu) ? 'background: rgba(230, 230, 230, 0.55) !important;' : ''"><i class="icon angle right" v-if="subItem.name != '-' && subItem.code == teaSubMenu"></i> <span v-if="subItem.name != '-'">{{subItem.name}}</span>
<span class="ui label tiny red" v-if="subItem.badge != null && subItem.badge > 0">
<span v-if="subItem.badge < 100">{{subItem.badge}}</span>
<span v-else>99+</span>
</span>
</a>
</div>
</div>
</div>
</div>
<!-- 右侧主操作栏 -->
<div class="main" :class="{'without-menu':teaSubMenus.menus == null || teaSubMenus.menus.length == 0 || (teaSubMenus.menus.length == 1 && teaSubMenus.menus[0].alwaysActive), 'without-secondary-menu':teaSubMenus.alwaysMenu == null || teaSubMenus.alwaysMenu.items.length <= 1, 'without-footer':!teaShowOpenSourceInfo}" v-cloak="">
<!-- 操作菜单 -->
<div class="ui top menu tabular tab-menu small" v-if="teaTabbar.length > 1">
<a class="item" v-for="item in teaTabbar" :class="{'active':item.isActive && !item.isDisabled, right:item.isRight, title: item.isTitle, icon: item.icon != null && item.icon.length > 0, disabled: item.isDisabled}" :href="item.url">
<var>{{item.name}}<span v-if="item.subName.length > 0">({{item.subName}})</span><i class="icon small" :class="item.icon" v-if="item.icon != null && item.icon.length > 0"></i> </var>
<var v-if="item.isTitle && typeof _data.node == 'object'">{{node.name}}</var>
<div class="bottom-indicator" v-if="item.isActive && !item.isTitle"></div>
</a>
</div>
<!-- 功能区 -->
<div class="main-box">
{$TEA.VIEW}
<div class="clear"></div>
</div>
</div>
<!-- 底部 -->
{$template "/footer"}
</div>
{$echo "footer"}
</body>
</html>

View File

@@ -0,0 +1,276 @@
Tea.context(function () {
this.moreOptionsVisible = false
this.globalMessageBadge = 0
if (typeof this.leftMenuItemIsDisabled == "undefined") {
this.leftMenuItemIsDisabled = false
}
this.$delay(function () {
if (this.$refs.focus != null) {
this.$refs.focus.focus()
}
if (!window.IS_POPUP) {
// 检查消息
this.checkMessages()
// 检查集群节点同步
this.loadNodeTasks();
// 检查DNS同步
this.loadDNSTasks()
}
})
/**
* 切换模板
*/
this.changeTheme = function () {
this.$post("/ui/theme")
.success(function (resp) {
teaweb.successToast("界面风格已切换")
this.teaTheme = resp.data.theme
})
}
/**
* 左侧子菜单
*/
this.showSubMenu = function (menu) {
if (menu.alwaysActive) {
return
}
if (this.teaSubMenus.menus != null && this.teaSubMenus.menus.length > 0) {
this.teaSubMenus.menus.$each(function (k, v) {
if (menu.id == v.id) {
return
}
v.isActive = false
})
}
menu.isActive = !menu.isActive
};
/**
* 检查消息
*/
this.checkMessages = function () {
this.$post("/messages/badge")
.params({})
.success(function (resp) {
this.globalMessageBadge = resp.data.count
// add dot to title
let dots = "••• "
if (typeof document.title == "string") {
if (resp.data.count > 0) {
if (!document.title.startsWith(dots)) {
document.title = dots + document.title
}
} else if (document.title.startsWith(dots)) {
document.title = document.title.substring(dots.length)
}
}
})
.done(function () {
let delay = 6000
if (this.globalMessageBadge > 0) {
delay = 30000
}
this.$delay(function () {
this.checkMessages()
}, delay)
})
}
this.checkMessagesOnce = function () {
this.$post("/messages/badge")
.params({})
.success(function (resp) {
this.globalMessageBadge = resp.data.count
})
}
this.showMessages = function () {
teaweb.popup("/messages", {
height: "28em",
width: "50em"
})
}
/**
* 底部伸展框
*/
this.showQQGroupQrcode = function () {
teaweb.popup("/about/qq", {
width: "21em",
height: "30em"
})
}
/**
* 弹窗中默认成功回调
*/
if (window.IS_POPUP === true) {
this.success = window.NotifyPopup
}
/**
* 节点同步任务
*/
this.doingNodeTasks = {
isDoing: false,
hasError: false,
isUpdated: false
}
this.loadNodeTasks = function () {
if (!Tea.Vue.teaCheckNodeTasks) {
return
}
let isStream = false
this.$post("/clusters/tasks/check")
.params({
isDoing: this.doingNodeTasks.isDoing ? 1 : 0,
hasError: this.doingNodeTasks.hasError ? 1 : 0,
isUpdated: this.doingNodeTasks.isUpdated ? 1 : 0
})
.timeout(60)
.success(function (resp) {
this.doingNodeTasks.isDoing = resp.data.isDoing
this.doingNodeTasks.hasError = resp.data.hasError
this.doingNodeTasks.isUpdated = true
isStream = resp.data.shouldWait
})
.done(function () {
this.$delay(function () {
this.loadNodeTasks()
}, isStream ? 5000 : 30000)
})
}
this.showNodeTasks = function () {
teaweb.popup("/clusters/tasks/listPopup", {
height: "28em",
width: "54em"
})
}
/**
* DNS同步任务
*/
this.doingDNSTasks = {
isDoing: false,
hasError: false,
isUpdated: false
}
this.loadDNSTasks = function () {
if (!Tea.Vue.teaCheckDNSTasks) {
return
}
let isStream = false
this.$post("/dns/tasks/check")
.params({
isDoing: this.doingDNSTasks.isDoing ? 1 : 0,
hasError: this.doingDNSTasks.hasError ? 1 : 0,
isUpdated: this.doingDNSTasks.isUpdated ? 1 : 0
})
.timeout(60)
.success(function (resp) {
this.doingDNSTasks.isDoing = resp.data.isDoing
this.doingDNSTasks.hasError = resp.data.hasError
this.doingDNSTasks.isUpdated = true
isStream = resp.data.isStream
})
.done(function () {
this.$delay(function () {
this.loadDNSTasks()
}, isStream ? 5000 : 30000)
})
}
this.showDNSTasks = function () {
teaweb.popup("/dns/tasks/listPopup", {
height: "28em",
width: "54em"
})
}
this.LANG = function (code) {
if (window.LANG_MESSAGES != null) {
let message = window.LANG_MESSAGES[code]
if (typeof message == "string") {
return message
}
}
if (window.LANG_MESSAGES_BASE != null) {
let message = window.LANG_MESSAGES_BASE[code]
if (typeof message == "string") {
return message
}
}
return "{{ LANG('" + code + "') }}"
}
this.switchLang = function () {
this.$post("/settings/lang/switch")
.success(function () {
window.location.reload()
})
}
});
window.NotifySuccess = function (message, url, params) {
if (typeof (url) == "string" && url.length > 0) {
if (url[0] != "/") {
url = Tea.url(url, params);
}
}
return function () {
teaweb.success(message, function () {
window.location = url;
});
};
};
window.NotifyReloadSuccess = function (message) {
return function () {
teaweb.success(message, function () {
window.location.reload()
})
}
}
window.NotifyDelete = function (message, url, params) {
teaweb.confirm(message, function () {
Tea.Vue.$post(url)
.params(params)
.refresh();
});
};
window.NotifyPopup = function (resp) {
window.parent.teaweb.popupFinish(resp);
};
window.ChangePageSize = function (size) {
let url = window.location.toString();
url = url.replace(/page=\d+/g, "page=1")
if (url.indexOf("pageSize") > 0) {
url = url.replace(/pageSize=\d+/g, "pageSize=" + size)
} else {
if (url.indexOf("?") > 0) {
let anchorIndex = url.indexOf("#")
if (anchorIndex < 0) {
url += "&pageSize=" + size;
} else {
url = url.substring(0, anchorIndex) + "&pageSize=" + size + url.substr(anchorIndex);
}
} else {
url += "?pageSize=" + size;
}
}
window.location = url;
};

View File

@@ -0,0 +1,975 @@
@import "./@left_menu";
@import "./@grids";
/** 通用 **/
* {
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
scrollbar-width: thin;
}
.clear {
clear: both;
}
.hidden {
display: none;
}
pre {
white-space: pre-wrap;
}
a.disabled, a.disabled:hover, a.disabled:active, span.disabled {
color: #ccc !important;
}
a.enabled, span.enabled, span.green {
color: #21ba45;
}
span.grey, label.grey, p.grey {
color: grey !important;
}
p.grey {
margin-top: 0.8em;
}
span.red, pre.red {
color: #db2828;
}
span.blue {
color: #4183c4;
}
span.orange {
color: #ff851b;
}
pre:not(.CodeMirror-line) {
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif !important;
}
tbody {
background: transparent;
}
.table-box {
margin-top: 1em;
overflow-x: auto;
}
.table-box::-webkit-scrollbar {
height: 6px;
}
.table.width30 {
width: 30em !important;
}
.table.width35 {
width: 35em !important;
}
.table.width40 {
width: 40em !important;
}
.table.width1024 {
width: 1024px !important;
}
.table th, .table td {
font-size: 0.9em !important;
}
.table tr.active td {
background: rgba(0, 0, 0, 0.01) !important;
}
p.comment, div.comment {
color: #959da6;
padding-top: 0.4em;
font-size: 1em;
}
p.comment em, div.comment em {
font-style: italic !important;
}
.truncate {
white-space: nowrap;
-ms-text-overflow: ellipsis;
overflow: hidden;
text-overflow: ellipsis;
}
div.margin, p.margin {
margin-top: 1em;
}
.opacity-mask {
opacity: 0.3;
}
/** 操作按钮容器 **/
.op.one {
width: 4em;
}
.op.two {
width: 7.4em;
}
.op.three {
width: 9em;
}
.op.four {
width: 12em;
}
/** 主菜单 **/
.main-menu {
width: 8em !important;
.ui.menu {
width: 100% !important;
// menu
.item.separator {
border-top: 1px rgba(0, 0, 0, 0.2) solid;
height: 1px;
min-height: 0;
padding: 0;
}
}
}
@media screen and (max-width: 512px) {
.main-menu {
width: auto !important;
}
.main-menu .ui.menu {
width: 3.6em !important;
.item.separator {
display: none;
}
}
.main-menu .ui.menu .item {
padding-top: 2em;
padding-bottom: 2.4em;
}
.main-menu .ui.menu .item span {
}
}
.main-menu .ui.labeled.icon.menu .item {
font-size: 0.9em;
}
.main-menu {
.ui.menu {
padding-bottom: 3em;
.item {
.subtitle {
display: none;
}
}
.item.expend .subtitle {
display: block;
font-size: 10px;
padding-left: 2.0em;
margin-top: 0.5em;
color: grey;
}
@media screen and (max-width: 512px) {
.item.expend .subtitle {
display: none;
}
}
.sub-items {
.item {
padding-left: 2.8em !important;
padding-right: 0.4em !important;
.icon {
position: absolute;
left: 1.1em;
top: 0.93em;
}
.label {
margin-left: 0;
margin-right: 0;
padding-left: 0.4em;
padding-right: 0.4em;
min-width: 2em;
}
}
@media screen and (max-width: 512px) {
.item {
padding-left: 1em !important;
}
}
.item.active {
background-color: #2185d0 !important;
}
}
}
}
/** 扩展UI **/
.field.text {
padding: .5em;
}
.form .fields:not(.inline) {
.field {
margin-bottom: 0.5em !important;
.button {
min-width: 5em;
}
}
}
/** body **/
@keyframes blink {
from {
opacity: 0.1;
}
to {
opacity: 0.8;
}
}
@keyframes rotation {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
body .ui.menu .item .blink {
animation: blink 1s infinite;
}
body .ui.menu .item:not(:hover) span.rotate {
animation: rotation 3s infinite;
}
body.expanded .main-menu {
display: none;
}
body.expanded .main {
left: 1em;
}
/** 布局相关 */
.top-nav {
border-radius: 0 !important;
position: fixed;
width: 100%;
z-index: 1000;
overflow-x: auto;
border: 0 !important;
img.avatar {
width: 1.6em !important;
height: 1.6em !important;
padding: 0.2em;
background: #fff;
border-radius: 0.9em;
margin-right: 0.5em !important;
}
em {
font-style: normal;
font-size: 0.9em;
padding-left: 0.2em;
}
.item {
.hover-span {
span {
display: none;
}
}
}
.item:hover {
.hover-span {
span {
display: inline;
}
}
}
.item.red {
color: red !important;
}
}
.top-nav.theme1 {
background: #14539A !important;
}
.top-nav.theme2 {
background: #276AC6 !important;
}
.top-nav.theme3 {
background: #007D9C !important;
}
.top-nav.theme4 {
background: #A12568 !important;
}
.top-nav.theme5 {
background: #1C7947 !important;
}
.top-nav.theme6 {
background: #1D365D !important;
}
.top-nav.theme7 {
background: black !important;
}
.top-nav::-webkit-scrollbar {
height: 2px;
}
/** 顶部菜单 **/
.top-secondary-menu {
position: fixed;
top: 2.6em;
left: 8.2em;
right: 0;
z-index: 100;
background: white;
}
.top-secondary-menu .menu {
margin-top: 0 !important;
margin-bottom: 0 !important;
border-radius: 0 !important;
}
.top-secondary-menu .menu var {
font-style: normal;
}
.top-secondary-menu .divider {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
@media screen and (max-width: 512px) {
.top-secondary-menu {
left: 4em;
}
}
/** 右侧主操作区 **/
.main {
position: absolute;
left: 22em;
top: 5.6em;
padding-bottom: 5em;
padding-right: 0.2em;
right: 1em;
.main-box {
}
}
@media screen and (max-width: 512px) {
.main {
left: 4em;
.main-box {
display: block;
}
}
}
.main.without-menu {
left: 9em;
}
.main.without-secondary-menu {
top: 2.9em;
}
@media screen and (max-width: 512px) {
.main.without-menu {
left: 4em;
}
}
.main table td.title {
width: 10em;
}
.main table td.middle-title {
width: 14em;
}
.main table td {
vertical-align: top;
}
.main table td.color-border {
border-left: 1px #276ac6 solid !important;
}
.main table td.vertical-top {
vertical-align: top;
}
.main table td.vertical-middle {
vertical-align: middle;
}
.main table td[colspan="2"] a {
font-weight: normal;
}
.main table td em {
font-weight: normal;
font-style: normal;
font-size: 0.9em;
}
.main table td em.grey {
color: grey;
}
.main h3 {
font-weight: normal;
margin-top: 1em !important;
position: relative;
}
.main h3 span {
font-size: 0.8em;
}
.main h3 span.label {
color: #6435c9;
}
.main h3 a {
margin-left: 1em;
font-size: 14px !important;
right: 1em;
}
.main h4 {
font-weight: normal;
}
.main form h4 {
margin-top: 0.6em;
}
.main td span.small {
font-size: 0.8em;
}
.main .button.mini {
font-size: 0.8em;
padding: 0.2em;
margin-left: 1em;
}
.main-menu {
position: fixed;
/**top: 1.05em;**/
top: 2em;
bottom: 0;
overflow-y: auto;
z-index: 10;
.menu {
border: 0 !important;
box-shadow: none !important;
}
}
.main-menu.theme1 {
background: #14539A !important;
.menu {
background: #14539A !important;
}
}
.main-menu.theme2 {
background: #276AC6 !important;
.menu {
background: #276AC6 !important;
}
}
.main-menu.theme3 {
background: #007D9C !important;
.menu {
background: #007D9C !important;
}
}
.main-menu.theme4 {
background: #A12568 !important;
.menu {
background: #A12568 !important;
}
}
.main-menu.theme5 {
background: #1C7947 !important;
.menu {
background: #1C7947 !important;
}
}
.main-menu.theme6 {
background: #1D365D !important;
.menu {
background: #1D365D !important;
}
}
.main-menu.theme7 {
background: black !important;
.menu {
background: black !important;
}
}
.main-menu::-webkit-scrollbar {
width: 4px;
}
.main {
.tab-menu {
margin-top: 0.3em !important;
margin-bottom: 0 !important;
overflow-x: auto;
overflow-y: hidden;
.item {
padding: 0 1em !important;
var {
font-style: normal;
}
span {
font-size: 0.8em;
padding-left: 0.3em;
}
.icon {
margin-left: 0.6em;
}
}
.item.active.title {
font-weight: normal !important;
margin-right: 1em !important;
border-radius: 0 !important;
}
.item:hover {
background: #f8f8f9 !important;
border-width: 1px;
}
.item.active:not(.title) {
font-weight: normal !important;
border: none;
border-radius: 0 !important;
color: #2185d0 !important;
.bottom-indicator {
border-bottom: 1px #2185d0 solid;
position: absolute;
left: 0;
right: 0;
bottom: 1px;
}
}
.item.active:not(.title).icon {
.bottom-indicator {
border-bottom: 1px #2185d0 solid;
position: absolute;
left: 0;
right: 0.6em;
bottom: 1px;
}
}
.item.active.blue {
font-weight: bold !important;
}
}
.tab-menu::-webkit-scrollbar {
height: 4px;
}
}
.main .go-top-btn {
position: fixed;
right: 2.6em;
bottom: 2em;
font-size: 2em;
line-height: 1.4em;
border-radius: 1em;
z-index: 999999;
background: white;
}
/** 右侧文本子菜单 **/
.text.menu {
overflow-x: auto;
}
.text.menu::-webkit-scrollbar {
width: 4px;
height: 4px;
}
/** 脚部相关样式 **/
#footer {
position: fixed;
bottom: 0;
text-align: left;
color: gray;
width: 100%;
border-radius: 0 !important;
z-index: 10;
overflow-x: auto;
}
#footer::-webkit-scrollbar {
height: 2px;
}
#footer a {
font-size: 0.9em;
}
#footer a form {
display: none;
}
#footer a:hover span, #footer a:active span {
display: none;
}
#footer a:hover form, #footer a:active form {
display: block;
}
#footer form input {
padding: 0;
margin: 0;
}
#footer-outer-box {
z-index: 999999;
position: fixed;
left: 0;
top: 0;
right: 0;
background: rgba(0, 0, 0, 0.8);
bottom: 2.6em;
}
#footer-outer-box .qrcode {
width: 20em;
position: absolute;
top: 50%;
left: 50%;
margin-top: -14em;
margin-left: -10em;
}
#footer-outer-box .qrcode img {
width: 100%;
}
#footer-outer-box .qrcode a {
position: absolute;
right: 0.5em;
top: 0.5em;
}
@media screen and (max-width: 512px) {
#footer-outer-box .qrcode {
margin-left: 0;
left: 3.5em;
}
}
/** Vue **/
[v-cloak] {
display: none !important;
}
/** auto complete **/
.autocomplete-box .menu {
background: #eee !important;
}
.autocomplete-box .menu::-webkit-scrollbar {
width: 4px;
}
.autocomplete-box .menu .item {
border-top: none !important;
}
select.auto-width {
width: auto !important;
}
/** column **/
@media screen and (max-width: 512px) {
.column:not(.one) {
width: 100% !important;
}
}
// label
label[for] {
cursor: pointer !important;
}
label.blue {
color: #2185d0 !important;
}
/** Menu **/
.first-menu .menu.text {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.first-menu .divider {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.second-menu .menu.text {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.second-menu .menu.text em {
font-style: normal;
}
.second-menu .divider {
margin-top: 0 !important;
}
.menu a {
outline: none;
}
/** var **/
span.olive, var.olive {
color: #b5cc18 !important;
}
span.dash {
border-bottom: 1px dashed grey;
}
span.hover:hover {
background: #eee;
}
var.normal {
font-style: normal;
}
/** checkbox **/
.checkbox label a, .checkbox label {
font-size: 0.9em !important;
}
.checkbox label {
}
/** page **/
.page {
margin-top: 1em;
border-left: 1px solid #ddd;
a {
display: inline-block;
background: #fafafa;
color: #666;
padding: 6px 12px;
margin: 0;
font-size: 0.9em;
border: 1px solid #ddd;
border-left: 0;
}
a.active {
background: #2185d0 !important;
color: white;
}
a:hover {
background: #eee;
}
select {
padding-top: 0.3em !important;
padding-bottom: 0.3em !important;
}
}
/** popup **/
.swal2-html-container {
overflow-x: hidden;
}
.swal2-close, .swal2-close:focus {
border: 0;
}
.swal2-confirm:focus, .swal2-cancel:focus {
border: 3px #ddd solid !important;
}
.swal2-confirm, .swal2-cancel {
border: 3px #fff solid !important;
}
.swal2-cancel {
margin-left: 2em !important;
}
/** 排序 **/
.sortable-ghost {
background: #ddd !important;
opacity: 0.1;
}
.sortable-drag {
opacity: 1.0;
}
.icon.handle {
cursor: pointer;
}
.label.port-label {
margin-top: 0.4em !important;
margin-bottom: 0.4em !important;
display: block;
line-height: 1.5;
}
// .label
.label {
word-break: break-all;
}
td .label.small {
margin-bottom: 0.2em !important;
}
// td
td {
word-break: break-all;
}
// codemirror
.source-code-box {
.CodeMirror {
border: 1px solid #eee;
height: auto !important;
}
.CodeMirror-vscrollbar {
width: 6px;
border-radius: 3px !important;
}
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
border-radius: 2px;
}
}
// 表格
.scroll-box {
overflow-y: auto;
}
.scroll-box::-webkit-scrollbar {
width: 4px;
}
// input
input.error {
border: 1px #e0b4b4 solid !important;
}
// textarea
textarea.wide-code {
font-family: Menlo, Monaco, "Courier New", monospace !important;
line-height: 1.6 !important;
}
// combo-box
.combo-box .menu {
max-height: 17em;
overflow-y: auto;
position: absolute;
border: rgba(129, 177, 210, 0.81) 1px solid;
border-top: 0;
z-index: 100
}
.combo-box .menu::-webkit-scrollbar {
width: 4px;
}

View File

@@ -0,0 +1,128 @@
/* 全局字体大小调整 - 增加 10% */
html {
font-size: 16px !important;
}
body {
font-size: 16px !important;
}
.ui.toggle.checkbox input:focus:checked ~ .box:before,
.ui.toggle.checkbox input:focus:checked ~ label:before {
background-color: #21ba45 !important;
}
.ui.toggle.checkbox input:checked ~ .box:before,
.ui.toggle.checkbox input:checked ~ label:before {
background-color: #21ba45 !important;
}
.ui.label.basic {
background-color: white !important;
}
.ui.label.basic.grey {
border: 1px #ccc solid !important;
}
.disabled .ui.label {
color: #ccc !important;
}
.ui.basic.grey.label.border-grey {
border-color: #ddd !important;
}
form .fields {
margin-bottom: 0 !important;
}
.link.grey {
color: grey !important;
}
.link.grey:hover {
color: #4183c4 !important;
}
table th.center,
table td.center {
text-align: center !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
table th.width10 {
width: 10em;
}
table th.width5 {
width: 5em;
}
table th.width6 {
width: 6em;
}
.ui.table tbody[style*="display: none;"],
.ui.table tr[style*="display: none;"],
.ui.table tr > td[style*="display: none;"],
.ui.table tr > th[style*="display: none;"] {
display: none!important;
}
textarea::-webkit-scrollbar {
width: 6px !important;
}
select.dropdown {
height: auto !important;
}
.message .icon.warning {
font-size: 2em !important;
}
body.swal2-shown {
overflow: auto !important;
}
.grid {
margin-right: 0 !important;
}
.fields button {
min-width: 5em;
}
.left-box .menu .item.active {
background: rgba(230, 230, 230, 0.35) !important;
border-radius: 3px;
}
:root {
--admin-top-nav-bg: #0a1f3f;
--admin-side-menu-bg: #102a4d;
}
.top-nav.theme1,
.top-nav.theme2,
.top-nav.theme3,
.top-nav.theme4,
.top-nav.theme5,
.top-nav.theme6,
.top-nav.theme7 {
background: var(--admin-top-nav-bg) !important;
}
.main-menu.theme1,
.main-menu.theme2,
.main-menu.theme3,
.main-menu.theme4,
.main-menu.theme5,
.main-menu.theme6,
.main-menu.theme7,
.main-menu.theme1 .menu,
.main-menu.theme2 .menu,
.main-menu.theme3 .menu,
.main-menu.theme4 .menu,
.main-menu.theme5 .menu,
.main-menu.theme6 .menu,
.main-menu.theme7 .menu {
background: var(--admin-side-menu-bg) !important;
}
.main-menu .ui.menu .item.separator,
.main-menu .ui.menu .sub-items .item.separator {
display: none !important;
border: 0 !important;
height: 0 !important;
min-height: 0 !important;
padding: 0 !important;
margin: 0 !important;
}
.main-menu {
top: 2.6em !important;
}
.main-menu > .ui.menu > .item:first-child:empty {
display: none !important;
height: 0 !important;
min-height: 0 !important;
padding: 0 !important;
margin: 0 !important;
}
/*# sourceMappingURL=@layout_override.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["@layout_override.less"],"names":[],"mappings":"AACA,GAAG,OAAO,SAAU,MAAK,MAAM,QAAS,OAAM;AAAS,GAAG,OAAO,SAAU,MAAK,MAAM,QAAS,QAAO;EACrG,yBAAA;;AAGD,GAAG,OAAO,SAAU,MAAK,QAAS,OAAM;AAAS,GAAG,OAAO,SAAU,MAAK,QAAS,QAAO;EACzF,yBAAA;;AAGD,GAAG,MAAM;EACR,kCAAA;;AAGD,GAAG,MAAM,MAAM;EACd,sBAAA;;AAGD,SAAU,IAAG;EACZ,WAAA;;AAGD,GAAG,MAAM,KAAK,MAAM;EACnB,kBAAA;;AAID,IACC;EACC,2BAAA;;AAKF,KAAK;EACJ,sBAAA;;AAGD,KAAK,KAAK;EACT,cAAA;;AAID,KACC,GAAE;AADH,KACY,GAAE;EACZ,6BAAA;EACA,0BAAA;EACA,2BAAA;;AAJF,KAOC,GAAE;EACD,WAAA;;AARF,KAWC,GAAE;EACD,UAAA;;AAZF,KAeC,GAAE;EACD,UAAA;;AAIF,GAAG,MAAO,MAAK;AAA2B,GAAG,MAAO,GAAE;AAA2B,GAAG,MAAO,GAAE,KAAG;AAA2B,GAAG,MAAO,GAAE,KAAG;EACzI,uBAAA;;AAID,QAAQ;EACP,qBAAA;;AAID,MAAM;EACL,uBAAA;;AAID,QACC,MAAK;EACJ,yBAAA;;AAKF,IAAI;EACH,yBAAA;;AAID;EACC,0BAAA;;AAID,OACC;EACC,cAAA","file":"@layout_override.css"}

View File

@@ -0,0 +1,165 @@
// 全局字体大小调整 - 增加 10%
html {
font-size: 16px !important;
}
body {
font-size: 16px !important;
}
// labels
.ui.toggle.checkbox input:focus:checked ~ .box:before, .ui.toggle.checkbox input:focus:checked ~ label:before {
background-color: #21ba45 !important;
}
.ui.toggle.checkbox input:checked ~ .box:before, .ui.toggle.checkbox input:checked ~ label:before {
background-color: #21ba45 !important;
}
.ui.label.basic {
background-color: white !important;
}
.ui.label.basic.grey {
border: 1px #ccc solid !important;
}
.disabled .ui.label {
color: #ccc !important;
}
.ui.basic.grey.label.border-grey {
border-color: #ddd!important;
}
// fields
form {
.fields {
margin-bottom: 0 !important;
}
}
// links
.link.grey {
color: grey !important;
}
.link.grey:hover {
color: #4183c4 !important;
}
// table
table {
th.center, td.center {
text-align: center !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
th.width10 {
width: 10em;
}
th.width5 {
width: 5em;
}
th.width6 {
width: 6em;
}
}
.ui.table tbody[style*="display: none;"], .ui.table tr[style*="display: none;"], .ui.table tr>td[style*="display: none;"], .ui.table tr>th[style*="display: none;"] {
display: none!important;
}
// textarea
textarea::-webkit-scrollbar {
width: 6px !important;
}
// dropdown
select.dropdown {
height: auto !important;
}
// message
.message {
.icon.warning {
font-size: 2em !important;
}
}
// popup
body.swal2-shown {
overflow: auto !important;
}
// grid
.grid {
margin-right: 0 !important;
}
// fields
.fields {
button {
min-width: 5em;
}
}
// Menu background colors only
:root {
--admin-top-nav-bg: #0a1f3f;
--admin-side-menu-bg: #102a4d;
}
.top-nav.theme1,
.top-nav.theme2,
.top-nav.theme3,
.top-nav.theme4,
.top-nav.theme5,
.top-nav.theme6,
.top-nav.theme7 {
background: var(--admin-top-nav-bg) !important;
}
.main-menu.theme1,
.main-menu.theme2,
.main-menu.theme3,
.main-menu.theme4,
.main-menu.theme5,
.main-menu.theme6,
.main-menu.theme7,
.main-menu.theme1 .menu,
.main-menu.theme2 .menu,
.main-menu.theme3 .menu,
.main-menu.theme4 .menu,
.main-menu.theme5 .menu,
.main-menu.theme6 .menu,
.main-menu.theme7 .menu {
background: var(--admin-side-menu-bg) !important;
}
// Hide menu separators
.main-menu .ui.menu .item.separator,
.main-menu .ui.menu .sub-items .item.separator {
display: none !important;
border: 0 !important;
height: 0 !important;
min-height: 0 !important;
padding: 0 !important;
margin: 0 !important;
}
// Align left menu with top nav and remove leading spacer item
.main-menu {
top: 2.6em !important;
}
.main-menu > .ui.menu > .item:first-child:empty {
display: none !important;
height: 0 !important;
min-height: 0 !important;
padding: 0 !important;
margin: 0 !important;
}

View File

@@ -0,0 +1,336 @@
/** 通用 **/
* {
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
scrollbar-width: thin;
}
.clear {
clear: both;
}
.hidden {
display: none;
}
pre {
white-space: pre-wrap;
}
a.disabled,
a.disabled:hover,
a.disabled:active,
span.disabled {
color: #ccc !important;
}
a.enabled,
span.enabled,
span.green {
color: #21ba45;
}
span.grey,
label.grey,
p.grey {
color: grey !important;
}
p.grey {
margin-top: 0.8em;
}
span.red,
pre.red {
color: #db2828;
}
span.blue {
color: #4183c4;
}
pre:not(.CodeMirror-line) {
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif !important;
}
tbody {
background: transparent;
}
.table.width30 {
width: 30em !important;
}
.table.width35 {
width: 35em !important;
}
.table.width40 {
width: 40em !important;
}
.table th,
.table td {
font-size: 0.9em !important;
}
p.comment,
div.comment {
color: #959da6;
padding-top: 0.4em;
font-weight: normal;
word-break: break-all;
line-height: 1.8;
}
p.comment em,
div.comment em {
font-style: italic !important;
}
.truncate {
white-space: nowrap;
-ms-text-overflow: ellipsis;
overflow: hidden;
text-overflow: ellipsis;
}
div.margin,
p.margin {
margin-top: 1em;
}
/** 操作按钮容器 **/
.op.one {
width: 4em;
}
.op.two {
width: 7.4em;
}
.op.three {
width: 9em;
}
.op.four {
width: 10em;
}
/** 扩展UI **/
.field.text {
padding: 0.5em;
}
/** 右侧主操作区 **/
.main {
position: absolute;
left: 22em;
top: 5.6em;
padding-bottom: 5em;
padding-right: 1em;
right: 1em;
}
@media screen and (max-width: 512px) {
.main {
left: 4em;
}
}
.main.without-menu {
left: 9em;
}
.main.without-secondary-menu {
top: 2.9em;
}
@media screen and (max-width: 512px) {
.main.without-menu {
left: 4em;
}
}
.main table td.title {
width: 10em;
}
.main table td.middle-title {
width: 14em;
}
.main table td {
vertical-align: top;
}
table td.color-border {
border-left: 1px #276ac6 solid !important;
}
.main table td.vertical-top {
vertical-align: top;
}
.main table td.vertical-middle {
vertical-align: middle;
}
.main table td[colspan="2"] a {
font-weight: normal;
}
.main table td em {
font-weight: normal;
font-style: normal;
font-size: 0.9em;
}
.main h3 {
font-weight: normal;
margin-top: 0.5em !important;
}
.main h3 span {
font-size: 0.8em;
}
.main h3 span.label {
color: #6435c9;
}
.main h3 a {
margin-left: 1em;
font-size: 14px !important;
right: 1em;
}
.main h3 a::before {
content: "[";
}
.main h3 a::after {
content: "]";
}
.main h4 {
font-weight: normal;
}
.main td span.small {
font-size: 0.8em;
}
.main .button.mini {
font-size: 0.8em;
padding: 0.2em;
margin-left: 1em;
}
/** 右侧文本子菜单 **/
.text.menu {
overflow-x: auto;
}
.text.menu::-webkit-scrollbar {
width: 4px;
height: 4px;
}
/** Vue **/
[v-cloak] {
display: none !important;
}
/** auto complete **/
.autocomplete-box .menu {
background: #eee !important;
}
.autocomplete-box .menu::-webkit-scrollbar {
width: 4px;
}
.autocomplete-box .menu .item {
border-top: none !important;
}
select.auto-width {
width: auto !important;
}
/** column **/
@media screen and (max-width: 512px) {
.column:not(.one) {
width: 100% !important;
}
}
/** label **/
label[for] {
cursor: pointer !important;
}
label.blue {
color: #2185d0 !important;
}
/** Menu **/
.first-menu .menu.text {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.first-menu .divider {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.second-menu .menu.text {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.second-menu .menu.text em {
font-style: normal;
}
.second-menu .divider {
margin-top: 0 !important;
}
.menu a {
outline: none;
}
/** var **/
span.olive,
var.olive {
color: #b5cc18 !important;
}
span.dash {
border-bottom: 1px dashed grey;
}
span.hover:hover {
background: #eee;
}
/** Message **/
.message .gopher {
width: 30px;
margin-right: 10px;
}
/** checkbox **/
.checkbox label a,
.checkbox label {
font-size: 0.8em !important;
}
/** page **/
.page {
margin-top: 1em;
border-left: 1px solid #ddd;
}
.page a {
display: inline-block;
background: #fafafa;
color: #666;
padding: 6px 12px;
margin: 0;
font-size: 0.9em;
border: 1px solid #ddd;
border-left: 0;
}
.page a.active {
background: #2185d0 !important;
color: white;
}
.page a:hover {
background: #eee;
}
.page select {
padding-top: 0.3em !important;
padding-bottom: 0.3em !important;
}
.swal2-html-container {
overflow-x: hidden;
}
.swal2-confirm:focus,
.swal2-cancel:focus,
.swal2-close:focus {
border: 3px #ddd solid !important;
}
.swal2-confirm,
.swal2-cancel,
.swal2-close {
border: 3px #fff solid !important;
}
.swal2-cancel {
margin-left: 2em !important;
}
input.error {
border: 1px #e0b4b4 solid !important;
}
textarea.wide-code {
font-family: Menlo, Monaco, "Courier New", monospace !important;
line-height: 1.6 !important;
}
.combo-box .menu {
max-height: 17em;
overflow-y: auto;
position: absolute;
border: rgba(129, 177, 210, 0.81) 1px solid;
border-top: 0;
z-index: 100;
}
.combo-box .menu::-webkit-scrollbar {
width: 4px;
}
code-label {
background: #fff;
border: 1px solid rgba(34, 36, 38, 0.15);
color: rgba(0, 0, 0, 0.87);
font-size: 0.71428571rem;
padding: 3px;
margin-left: 2px;
margin-right: 2px;
line-height: 1;
display: inline-block;
font-weight: 700;
vertical-align: baseline;
}
/*# sourceMappingURL=@layout_popup.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["@layout_popup.less"],"names":[],"mappings":";AACA;EACC,+CAAA;EACA,qBAAA;;AAGD;EACC,WAAA;;AAGD;EACC,aAAA;;AAGD;EACC,qBAAA;;AAGD,CAAC;AAAW,CAAC,SAAS;AAAQ,CAAC,SAAS;AAAS,IAAI;EACpD,WAAA;;AAGD,CAAC;AAAU,IAAI;AAAU,IAAI;EAC5B,cAAA;;AAGD,IAAI;AAAO,KAAK;AAAO,CAAC;EACvB,sBAAA;;AAGD,CAAC;EACA,iBAAA;;AAGD,IAAI;AAAM,GAAG;EACZ,cAAA;;AAGD,IAAI;EACH,cAAA;;AAGD,GAAG,IAAI;EACN,mBAAmB,8CAAnB;;AAGD;EACC,uBAAA;;AAGD,MAAM;EACL,sBAAA;;AAGD,MAAM;EACL,sBAAA;;AAGD,MAAM;EACL,sBAAA;;AAGD,MAAO;AAAI,MAAO;EACjB,gBAAA;;AAGD,CAAC;AAAU,GAAG;EACb,cAAA;EACA,kBAAA;EACA,mBAAA;EACA,qBAAA;EACA,gBAAA;;AAGD,CAAC,QAAS;AAAI,GAAG,QAAS;EACzB,6BAAA;;AAGD;EACC,mBAAA;EACA,2BAAA;EACA,gBAAA;EACA,uBAAA;;AAGD,GAAG;AAAS,CAAC;EACZ,eAAA;;;AAID,GAAG;EACF,UAAA;;AAGD,GAAG;EACF,YAAA;;AAGD,GAAG;EACF,UAAA;;AAGD,GAAG;EACF,WAAA;;;AAID,MAAM;EACL,cAAA;;;AAID;EACC,kBAAA;EACA,UAAA;EACA,UAAA;EACA,mBAAA;EACA,kBAAA;EACA,UAAA;;AAGD,mBAAqC;EACpC;IACC,SAAA;;;AAIF,KAAK;EACJ,SAAA;;AAGD,KAAK;EACJ,UAAA;;AAGD,mBAAqC;EACpC,KAAK;IACJ,SAAA;;;AAIF,KAAM,MAAM,GAAE;EACb,WAAA;;AAGD,KAAM,MAAM,GAAE;EACb,WAAA;;AAGD,KAAM,MAAM;EACX,mBAAA;;AAGD,KAAM,GAAE;EACP,8BAAA;;AAGD,KAAM,MAAM,GAAE;EACb,mBAAA;;AAGD,KAAM,MAAM,GAAE;EACb,sBAAA;;AAGD,KAAM,MAAM,GAAE,aAAc;EAC3B,mBAAA;;AAGD,KAAM,MAAM,GAAG;EACd,mBAAA;EACA,kBAAA;EACA,gBAAA;;AAGD,KAAM;EACL,mBAAA;EACA,iBAAA;;AAGD,KAAM,GAAG;EACR,gBAAA;;AAGD,KAAM,GAAG,KAAI;EACZ,cAAA;;AAGD,KAAM,GAAG;EACR,gBAAA;EACA,0BAAA;EACA,UAAA;;AAGD,KAAM,GAAG,EAAC;EACT,SAAS,GAAT;;AAGD,KAAM,GAAG,EAAC;EACT,SAAS,GAAT;;AAGD,KAAM;EACL,mBAAA;;AAGD,KAAM,GAAG,KAAI;EACZ,gBAAA;;AAGD,KAAM,QAAO;EACZ,gBAAA;EACA,cAAA;EACA,gBAAA;;;AAID,KAAK;EACJ,gBAAA;;AAGD,KAAK,KAAK;EACT,UAAA;EACA,WAAA;;;AAID;EACC,wBAAA;;;AAID,iBAAkB;EACjB,gBAAA;;AAGD,iBAAkB,MAAK;EACtB,UAAA;;AAGD,iBAAkB,MAAM;EACvB,2BAAA;;AAGD,MAAM;EACL,sBAAA;;;AAID,mBAAqC;EACpC,OAAO,IAAI;IACV,sBAAA;;;;AAKF,KAAK;EACJ,0BAAA;;AAGD,KAAK;EACJ,cAAA;;;AAOD,WAAY,MAAK;EAChB,wBAAA;EACA,2BAAA;;AAGD,WAAY;EACX,wBAAA;EACA,2BAAA;;AAGD,YAAa,MAAK;EACjB,wBAAA;EACA,2BAAA;;AAGD,YAAa,MAAK,KAAM;EACvB,kBAAA;;AAGD,YAAa;EACZ,wBAAA;;AAGD,KAAM;EACL,aAAA;;;AAID,IAAI;AAAQ,GAAG;EACd,cAAA;;AAGD,IAAI;EACH,8BAAA;;AAGD,IAAI,MAAM;EACT,gBAAA;;;AAID,QAAS;EACR,WAAA;EACA,kBAAA;;;AAID,SAAU,MAAM;AAAG,SAAU;EAC5B,gBAAA;;;AAID;EACC,eAAA;EAEA,2BAAA;;AAHD,KAKC;EACC,qBAAA;EACA,mBAAA;EACA,WAAA;EACA,iBAAA;EACA,SAAA;EACA,gBAAA;EACA,sBAAA;EACA,cAAA;;AAbF,KAgBC,EAAC;EACA,mBAAA;EACA,YAAA;;AAlBF,KAqBC,EAAC;EACA,gBAAA;;AAtBF,KAyBC;EACC,kBAAA;EACA,qBAAA;;AAKF;EACC,kBAAA;;AAGD,cAAc;AAAQ,aAAa;AAAQ,YAAY;EACtD,sBAAA;;AAGD;AAAgB;AAAe;EAC9B,sBAAA;;AAGD;EACC,2BAAA;;AAID,KAAK;EACJ,yBAAA;;AAID,QAAQ;EACP,4BAA4B,wBAA5B;EACA,gBAAA;;AAID,UAAW;EACV,gBAAA;EACA,gBAAA;EACA,kBAAA;EACA,2CAAA;EACA,aAAA;EACA,YAAA;;AAGD,UAAW,MAAK;EACf,UAAA;;AAID;EACC,gBAAA;EACA,wCAAA;EACA,0BAAA;EACA,wBAAA;EACA,YAAA;EACA,gBAAA;EACA,iBAAA;EACA,cAAA;EACA,qBAAA;EACA,gBAAA;EACA,wBAAA","file":"@layout_popup.css"}

View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<title>{$ htmlEncode .teaTitle}控制台</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
{$if eq .teaFaviconFileId 0}
<link rel="shortcut icon" href="/images/favicon.png"/>
{$else}
<link rel="shortcut icon" href="/ui/image/{$.teaFaviconFileId}"/>
{$end}
<link rel="stylesheet" type="text/css" href="/_/@default/@layout_popup.css" media="all"/>
<link rel="stylesheet" type="text/css" href="/css/semantic.iframe.min.css?v=bRafhK" media="all"/>
{$TEA.VUE}
<link rel="stylesheet" type="text/css" href="/_/@default/@layout_override.css" media="all"/>
{$echo "header"}
<script type="text/javascript" src="/_/@default/@layout.js"></script>
<script type="text/javascript" src="/js/components.js"></script>
<script type="text/javascript" src="/js/utils.min.js"></script>
<script type="text/javascript" src="/js/sweetalert2/dist/sweetalert2.all.min.js"></script>
<script type="text/javascript" src="/js/date.tea.js"></script>
<script type="text/javascript" src="/js/langs/{$.teaLang}.js?v=1.2.0"></script>
<style type="text/css">
.main {
left: 0;
top: 0;
bottom: 0;
right: 0;
overflow-y: auto;
padding-bottom: 0;
}
.main::-webkit-scrollbar {
width: 4px;
}
</style>
<script type="text/javascript">
window.IS_POPUP = true
</script>
</head>
<body>
<div class="main">
<!-- 功能区 -->
{$TEA.VIEW}
</div>
</body>
</html>

View File

@@ -0,0 +1,407 @@
/** 通用 **/
* {
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
scrollbar-width: thin;
}
.clear {
clear: both;
}
.hidden {
display: none;
}
pre {
white-space: pre-wrap;
}
a.disabled, a.disabled:hover, a.disabled:active, span.disabled {
color: #ccc !important;
}
a.enabled, span.enabled, span.green {
color: #21ba45;
}
span.grey, label.grey, p.grey {
color: grey !important;
}
p.grey {
margin-top: 0.8em;
}
span.red, pre.red {
color: #db2828;
}
span.blue {
color: #4183c4;
}
pre:not(.CodeMirror-line) {
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif !important;
}
tbody {
background: transparent;
}
.table.width30 {
width: 30em !important;
}
.table.width35 {
width: 35em !important;
}
.table.width40 {
width: 40em !important;
}
.table th, .table td {
font-size: 0.9em !important;
}
p.comment, div.comment {
color: #959da6;
padding-top: 0.4em;
font-weight: normal;
word-break: break-all;
line-height: 1.8;
}
p.comment em, div.comment em {
font-style: italic !important;
}
.truncate {
white-space: nowrap;
-ms-text-overflow: ellipsis;
overflow: hidden;
text-overflow: ellipsis;
}
div.margin, p.margin {
margin-top: 1em;
}
/** 操作按钮容器 **/
.op.one {
width: 4em;
}
.op.two {
width: 7.4em;
}
.op.three {
width: 9em;
}
.op.four {
width: 10em;
}
/** 扩展UI **/
.field.text {
padding: .5em;
}
/** 右侧主操作区 **/
.main {
position: absolute;
left: 22em;
top: 5.6em;
padding-bottom: 5em;
padding-right: 1em;
right: 1em;
}
@media screen and (max-width: 512px) {
.main {
left: 4em;
}
}
.main.without-menu {
left: 9em;
}
.main.without-secondary-menu {
top: 2.9em;
}
@media screen and (max-width: 512px) {
.main.without-menu {
left: 4em;
}
}
.main table td.title {
width: 10em;
}
.main table td.middle-title {
width: 14em;
}
.main table td {
vertical-align: top;
}
table td.color-border {
border-left: 1px #276ac6 solid !important;
}
.main table td.vertical-top {
vertical-align: top;
}
.main table td.vertical-middle {
vertical-align: middle;
}
.main table td[colspan="2"] a {
font-weight: normal;
}
.main table td em {
font-weight: normal;
font-style: normal;
font-size: 0.9em;
}
.main h3 {
font-weight: normal;
margin-top: 0.5em !important;
}
.main h3 span {
font-size: 0.8em;
}
.main h3 span.label {
color: #6435c9;
}
.main h3 a {
margin-left: 1em;
font-size: 14px !important;
right: 1em;
}
.main h3 a::before {
content: "[";
}
.main h3 a::after {
content: "]";
}
.main h4 {
font-weight: normal;
}
.main td span.small {
font-size: 0.8em;
}
.main .button.mini {
font-size: 0.8em;
padding: 0.2em;
margin-left: 1em;
}
/** 右侧文本子菜单 **/
.text.menu {
overflow-x: auto;
}
.text.menu::-webkit-scrollbar {
width: 4px;
height: 4px;
}
/** Vue **/
[v-cloak] {
display: none !important;
}
/** auto complete **/
.autocomplete-box .menu {
background: #eee !important;
}
.autocomplete-box .menu::-webkit-scrollbar {
width: 4px;
}
.autocomplete-box .menu .item {
border-top: none !important;
}
select.auto-width {
width: auto !important;
}
/** column **/
@media screen and (max-width: 512px) {
.column:not(.one) {
width: 100% !important;
}
}
/** label **/
label[for] {
cursor: pointer !important;
}
label.blue {
color: #2185d0 !important;
}
td .label.tiny {
}
/** Menu **/
.first-menu .menu.text {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.first-menu .divider {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.second-menu .menu.text {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.second-menu .menu.text em {
font-style: normal;
}
.second-menu .divider {
margin-top: 0 !important;
}
.menu a {
outline: none;
}
/** var **/
span.olive, var.olive {
color: #b5cc18 !important;
}
span.dash {
border-bottom: 1px dashed grey;
}
span.hover:hover {
background: #eee;
}
/** Message **/
.message .gopher {
width: 30px;
margin-right: 10px;
}
/** checkbox **/
.checkbox label a, .checkbox label {
font-size: 0.8em !important;
}
/** page **/
.page {
margin-top: 1em;
border-left: 1px solid #ddd;
a {
display: inline-block;
background: #fafafa;
color: #666;
padding: 6px 12px;
margin: 0;
font-size: 0.9em;
border: 1px solid #ddd;
border-left: 0;
}
a.active {
background: #2185d0 !important;
color: white;
}
a:hover {
background: #eee;
}
select {
padding-top: 0.3em !important;
padding-bottom: 0.3em !important;
}
}
// popup
.swal2-html-container {
overflow-x: hidden;
}
.swal2-confirm:focus, .swal2-cancel:focus, .swal2-close:focus {
border: 3px #ddd solid !important;
}
.swal2-confirm, .swal2-cancel, .swal2-close {
border: 3px #fff solid !important;
}
.swal2-cancel {
margin-left: 2em !important;
}
// input
input.error {
border: 1px #e0b4b4 solid !important;
}
// textarea
textarea.wide-code {
font-family: Menlo, Monaco, "Courier New", monospace !important;
line-height: 1.6 !important;
}
// combo-box
.combo-box .menu {
max-height: 17em;
overflow-y: auto;
position: absolute;
border: rgba(129, 177, 210, 0.81) 1px solid;
border-top: 0;
z-index: 100
}
.combo-box .menu::-webkit-scrollbar {
width: 4px;
}
// code-label
code-label {
background: #fff;
border: 1px solid rgba(34, 36, 38, .15);
color: rgba(0, 0, 0, .87);
font-size: .71428571rem;
padding: 3px;
margin-left: 2px;
margin-right: 2px;
line-height: 1;
display: inline-block;
font-weight: 700;
vertical-align: baseline;
}

View File

@@ -0,0 +1,113 @@
.left-box {
width: 8em;
position: fixed;
top: 7.5em;
bottom: 2.4em;
overflow-y: auto;
overflow-x: hidden;
border-right: 1px #ddd solid;
}
.left-box .menu {
width: 90% !important;
}
.left-box .menu .item {
line-height: 1.2;
position: relative;
padding-left: 1em !important;
}
.left-box .menu .item .icon {
position: absolute;
top: 50%;
left: 0;
margin-top: -0.4em !important;
}
.left-box .menu .item.separator {
border-bottom: 1px #eee solid !important;
padding-top: 0;
padding-bottom: 0;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.left-box .menu .item.on span {
border-bottom: 1px #666 dashed;
}
.left-box .menu .item.off span var {
font-style: normal;
background: #db2828;
color: white;
font-size: 8px;
padding: 2px;
border-radius: 2px;
margin-left: 1em;
}
.left-box .menu .header {
border-bottom: 1px #ddd solid;
padding-left: 0 !important;
padding-bottom: 1em !important;
}
.left-box::-webkit-scrollbar {
width: 4px;
}
.left-box.disabled {
opacity: 0.1;
}
.left-box.tiny {
top: 10em;
}
.left-box.without-tabbar {
top: 3em;
}
.left-box.with-menu {
top: 8.7em;
}
.left-box.without-menu {
top: 6em;
}
.right-box {
position: fixed;
top: 7em;
bottom: 1.3em;
right: 0;
left: 18em;
padding-right: 2em;
padding-bottom: 2em;
overflow-y: auto;
}
.right-box h4:first-child {
margin-top: 1em;
}
.right-box > .comment:first-child {
margin-top: 0.5em;
}
@media screen and (max-width: 512px) {
.right-box {
left: 13em;
padding-right: 1em;
}
}
body.expanded .right-box {
left: 10em;
}
.right-box.tiny {
top: 10em;
left: 26.5em;
}
.right-box::-webkit-scrollbar {
width: 4px;
}
.right-box.without-tabbar {
top: 3em;
}
.right-box.with-menu {
top: 8.6em;
}
.right-box.without-menu {
top: 6em;
}
.main.without-footer .left-box {
bottom: 0.2em;
}
.narrow-scrollbar::-webkit-scrollbar {
width: 4px;
}
/*# sourceMappingURL=@left_menu.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["@left_menu.less"],"names":[],"mappings":"AAAA;EACC,UAAA;EACA,eAAA;EACA,UAAA;EACA,aAAA;EACA,gBAAA;EACA,kBAAA;EACA,4BAAA;;AAPD,SASC;EACC,qBAAA;;AAVF,SASC,MAGC;EACC,gBAAA;EACA,kBAAA;EACA,4BAAA;;AAfH,SASC,MAGC,MAKC;EACC,kBAAA;EACA,QAAA;EACA,OAAA;EACA,kBAAA;;AArBJ,SASC,MAgBC,MAAK;EACJ,6BAAA;EACA,cAAA;EACA,iBAAA;EACA,wBAAA;EACA,2BAAA;;AA9BH,SASC,MAwBC,MAAK,GACJ;EACC,8BAAA;;AAnCJ,SASC,MA8BC,MAAK,IACJ,KACC;EACC,kBAAA;EACA,mBAAA;EACA,YAAA;EACA,cAAA;EACA,YAAA;EACA,kBAAA;EACA,gBAAA;;AAhDL,SASC,MA6CC;EACC,6BAAA;EACA,0BAAA;EACA,8BAAA;;AAQH,SAAS;EACR,UAAA;;AAGD,SAAS;EACR,YAAA;;AAGD,SAAS;EACR,SAAA;;AAGD,SAAS;EACR,QAAA;;AAGD,SAAS;EACR,UAAA;;AAGD,SAAS;EACR,QAAA;;AAGD;EACC,eAAA;EACA,QAAA;EACA,aAAA;EACA,QAAA;EACA,UAAA;EACA,kBAAA;EACA,mBAAA;EACA,gBAAA;;AARD,UAUC,GAAE;EACD,eAAA;;AAIF,UAAW,WAAU;EACpB,iBAAA;;AAGD,mBAAqC;EACpC;IACC,UAAA;IACA,kBAAA;;;AAIF,IAAI,SAAU;EACb,UAAA;;AAGD,UAAU;EACT,SAAA;EACA,YAAA;;AAGD,UAAU;EACT,UAAA;;AAGD,UAAU;EACT,QAAA;;AAGD,UAAU;EACT,UAAA;;AAGD,UAAU;EACT,QAAA;;AAID,KAAK,eAAgB;EACpB,aAAA;;AAID,iBAAiB;EAChB,UAAA","file":"@left_menu.css"}

View File

@@ -0,0 +1,9 @@
<div class="margin"></div>
<div class="left-box" :class="{disabled:leftMenuItemIsDisabled}">
<div class="ui menu text blue vertical small">
<a class="item" v-for="item in leftMenuItems" :href="item.url" :class="{active:item.isActive, separator:item.name == '-', on:item.isOn, off:item.isOff||item.isImportant, 'has-divider':item.hasDivider}" :style="item.isActive ? 'background: rgba(230, 230, 230, 0.35) !important; border-radius: 3px;' : ''">
<span v-if="item.name != '-'"><i class="icon play tiny" :style="{'visibility':item.isActive ? 'visible' : 'hidden'}"></i>{{item.name}}<var v-if="item.isOff"></var><var v-if="item.isImportant"></var></span>
</a>
</div>
</div>

View File

@@ -0,0 +1,157 @@
.left-box {
width: 8.5em;
position: fixed;
top: 7.5em;
bottom: 2.4em;
overflow-y: auto;
overflow-x: hidden;
border-right: 1px #ddd solid;
.menu {
width: 95% !important;
.item {
line-height: 1.2;
position: relative;
padding-left: 1em !important;
.icon {
position: absolute;
top: 50%;
left: 0;
margin-top: -0.4em !important;
}
}
.item.separator {
border-bottom: 1px #eee solid !important;
padding-top: 0;
padding-bottom: 0;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.item.on {
span {
border-bottom: 1px #666 dashed;
}
}
.item.off {
span {
var {
font-style: normal;
background: #db2828;
color: white;
font-size: 8px;
padding: 2px;
border-radius: 2px;
margin-left: 1em;
}
}
}
// 菜单项底部虚线(参考 separator 的实现)
.item.has-divider {
border-bottom: 1px dashed #ddd !important;
margin-bottom: 0.3em !important;
}
// .header需要在.item下面
.header {
border-bottom: 1px #ddd solid;
padding-left: 0 !important;
padding-bottom: 1em !important;
}
.item.active {
background: rgba(230, 230, 230, 0.35) !important;
border-radius: 3px;
}
}
}
.left-box::-webkit-scrollbar {
width: 4px;
}
.left-box.disabled {
opacity: 0.1;
}
.left-box.tiny {
top: 10em;
}
.left-box.without-tabbar {
top: 3em;
}
.left-box.with-menu {
top: 8.7em;
}
.left-box.without-menu {
top: 6em;
}
.right-box {
position: fixed;
top: 7em;
bottom: 1.3em;
right: 0;
left: 18em;
padding-right: 2em;
padding-bottom: 2em;
overflow-y: auto;
h4:first-child {
margin-top: 1em;
}
}
.right-box > .comment:first-child {
margin-top: 0.5em;
}
@media screen and (max-width: 512px) {
.right-box {
left: 13em;
padding-right: 1em;
}
}
body.expanded .right-box {
left: 10em;
}
.right-box.tiny {
top: 10em;
left: 26.5em;
}
.right-box::-webkit-scrollbar {
width: 4px;
}
.right-box.without-tabbar {
top: 3em;
}
.right-box.with-menu {
top: 8.6em;
}
.right-box.without-menu {
top: 6em;
}
// main
.main.without-footer .left-box {
bottom: 0.2em;
}
// scrollbar
.narrow-scrollbar::-webkit-scrollbar {
width: 4px;
}

View File

@@ -0,0 +1,9 @@
<div class="margin"></div>
<div class="left-box without-tabbar" :class="{disabled:leftMenuItemIsDisabled}">
<div class="ui menu text blue vertical small">
<a class="item" v-for="item in leftMenuItems" :href="item.url" :class="{active:item.isActive, separator:item.name == '-', on:item.isOn, off:item.isOff||item.isImportant}">
<span v-if="item.name != '-'"><i class="icon play tiny" :style="{'visibility':item.isActive ? 'visible' : 'hidden'}"></i>{{item.name}}<var v-if="item.isOff"></var><var v-if="item.isImportant"></var></span>
</a>
</div>
</div>

View File

@@ -0,0 +1,9 @@
<div class="margin"></div>
<div class="left-box with-menu" :class="{disabled:leftMenuItemIsDisabled}">
<div class="ui menu text blue vertical small">
<a class="item" v-for="item in leftMenuItems" :href="item.url" :class="{active:item.isActive, separator:item.name == '-', on:item.isOn, off:item.isOff||item.isImportant}">
<span v-if="item.name != '-'"><i class="icon play tiny" :style="{'visibility':item.isActive ? 'visible' : 'hidden'}"></i>{{item.name}}<var v-if="item.isOff"></var><var v-if="item.isImportant"></var></span>
</a>
</div>
</div>

View File

@@ -0,0 +1,9 @@
<div class="margin"></div>
<div class="left-box without-menu" :class="{disabled:leftMenuItemIsDisabled}">
<div class="ui menu text blue vertical small">
<a class="item" v-for="item in leftMenuItems" :href="item.url" :class="{active:item.isActive, separator:item.name == '-', on:item.isOn, off:item.isOff||item.isImportant}">
<span v-if="item.name != '-'"><i class="icon play tiny" :style="{'visibility':item.isActive ? 'visible' : 'hidden'}"></i>{{item.name}}<var v-if="item.isOff"></var><var v-if="item.isImportant"></var></span>
</a>
</div>
</div>

View File

@@ -0,0 +1 @@
<!-- 预留的菜单位置 -->

View File

@@ -0,0 +1,7 @@
<first-menu>
<menu-item href="/admins">系统用户</menu-item>
<span class="item">|</span>
<menu-item :href="'/admins/admin?adminId=' + admin.id" code="index">"{{admin.fullname}}" 详情</menu-item>
<menu-item :href="'/admins/update?adminId=' + admin.id" code="update">修改</menu-item>
<menu-item :href="'/admins/accesskeys?adminId=' + admin.id" code="accessKey">API AccessKey({{admin.countAccessKeys}})</menu-item>
</first-menu>

View File

@@ -0,0 +1,3 @@
<first-menu>
<menu-item @click.prevent="createAdmin">[创建管理员]</menu-item>
</first-menu>

View File

@@ -0,0 +1,28 @@
{$layout "layout_popup"}
<h3>创建新AccessKey</h3>
<form class="ui form" method="post" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="adminId" :value="adminId"/>
<table class="ui table definition selectable">
<tr>
<td class="title">AccessKey ID</td>
<td><span class="disabled">自动生成</span></td>
</tr>
<tr>
<td>AccessKey密钥</td>
<td><span class="disabled">自动生成</span></td>
</tr>
<tr>
<td class="title">备注 *</td>
<td>
<textarea rows="2" name="description" maxlength="100" ref="focus"></textarea>
<p class="comment">描述AccessKey的用途等。</p>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>

View File

@@ -0,0 +1,39 @@
{$layout}
{$template "../admin_menu"}
<second-menu>
<menu-item @click.prevent="createAccessKey()">[创建AccessKey]</menu-item>
</second-menu>
<p class="comment" v-if="accessKeys.length == 0">暂时还没有AccessKey。</p>
<table class="ui table selectable" v-if="accessKeys.length > 0">
<thead>
<tr>
<th>AccessKey ID</th>
<th>AccessKey密钥</th>
<th>备注</th>
<th>最后访问</th>
<th>状态</th>
<th class="two op">操作</th>
</tr>
</thead>
<tr v-for="accessKey in accessKeys">
<td :class="{disabled: !accessKey.isOn}">{{accessKey.uniqueId}}</td>
<td :class="{disabled: !accessKey.isOn}">{{accessKey.secret}}</td>
<td :class="{disabled: !accessKey.isOn}">{{accessKey.description}}</td>
<td :class="{disabled: !accessKey.isOn}">
<span v-if="accessKey.accessedTime.length > 0">{{accessKey.accessedTime}}</span>
<span v-else class="disabled">尚无访问</span>
</td>
<td>
<span v-if="accessKey.isOn" class="green">已启用</span>
<span v-else class="disabled">已禁用</span>
</td>
<td>
<a href="" v-if="accessKey.isOn" @click.prevent="updateAccessKeyIsOn(accessKey.id, false)">禁用</a>
<a href="" v-if="!accessKey.isOn" @click.prevent="updateAccessKeyIsOn(accessKey.id, true)">启用</a>
&nbsp; <a href="" @click.prevent="deleteAccessKey(accessKey.id)">删除</a>
</td>
</tr>
</table>

View File

@@ -0,0 +1,41 @@
Tea.context(function () {
this.createAccessKey = function () {
teaweb.popup("/admins/accesskeys/createPopup?adminId=" + this.admin.id, {
callback: function () {
teaweb.success("保存成功", function () {
teaweb.reload()
})
}
})
}
this.updateAccessKeyIsOn = function (accessKeyId, isOn) {
let that = this
let message = ""
if (isOn) {
message = "确定要启用此AccessKey吗"
} else {
message = "确定要禁用此AccessKey吗"
}
teaweb.confirm(message, function () {
that.$post(".updateIsOn")
.params({
accessKeyId: accessKeyId,
isOn: isOn ? 1 : 0
})
.refresh()
})
}
this.deleteAccessKey = function (accessKeyId) {
let that = this
teaweb.confirm("确定要删除此AccessKey吗", function () {
that.$post(".delete")
.params({
accessKeyId: accessKeyId
})
.refresh()
})
}
})

View File

@@ -0,0 +1,7 @@
.modules-box .module-box {
float: left;
width: 9em;
margin-top: 0.3em;
margin-bottom: 0.3em;
}
/*# sourceMappingURL=admin.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["admin.less"],"names":[],"mappings":"AAAA,YACC;EACC,WAAA;EACA,UAAA;EACA,iBAAA;EACA,oBAAA","file":"admin.css"}

View File

@@ -0,0 +1,75 @@
{$layout}
{$template "admin_menu"}
<table class="ui table definition selectable">
<tr>
<td class="title">全名</td>
<td>
{{admin.fullname}}
</td>
</tr>
<tr>
<td>状态</td>
<td>
<label-on :v-is-on="admin.isOn"></label-on>
</td>
</tr>
<tr>
<td>登录用户名</td>
<td>
{{admin.username}}
</td>
</tr>
<tr>
<td>允许登录</td>
<td>
<span class="green" v-if="admin.canLogin">Y</span>
<span class="disabled" v-else>N</span>
<p class="comment" v-if="admin.canLogin">允许登录当前管理平台。</p>
<p class="comment" v-else="">不允许登录当前管理平台。</p>
</td>
</tr>
<tr>
<td>超级管理员</td>
<td>
<span v-if="admin.isSuper" class="green">Y</span>
<span v-else class="disabled">N</span>
</td>
</tr>
<tr v-show="!admin.isSuper">
<td>权限</td>
<td>
<div class="modules-box" v-if="modules.length > 0">
<div class="module-box" v-for="module in modules">
<i class="icon circle small grey"></i> {{module.name}}
</div>
</div>
<span v-else class="disabled">暂时还没有可以管理的模块。</span>
</td>
</tr>
</table>
<h3>OTP认证</h3>
<table class="ui table definition selectable">
<tr>
<td class="title">状态</td>
<td>
<span v-if="otp != null && otp.isOn" class="green">已启用</span>
<span v-else class="disabled">未启用</span>
</td>
</tr>
<tr v-if="otp != null && otp.isOn">
<td colspan="2"><more-options-indicator>更多信息</more-options-indicator></td>
</tr>
<tr v-if="otp != null && otp.isOn && moreOptionsVisible">
<td>认证二维码</td>
<td>
<img alt="qrcode" :src="'./otpQrcode?adminId=' + admin.id"/>
<p class="comment"><a :href="'./otpQrcode?adminId=' + admin.id + '&download=true'">[下载]</a> &nbsp; 可以通过二维码快速添加OTP认证信息到认证App中。</p>
</td>
</tr>
<tr v-if="otp != null && otp.isOn && moreOptionsVisible">
<td>密钥</td>
<td>{{otp.params.secret}}</td>
</tr>
</table>

View File

@@ -0,0 +1,8 @@
.modules-box {
.module-box {
float: left;
width: 9em;
margin-top: 0.3em;
margin-bottom: 0.3em;
}
}

View File

@@ -0,0 +1,7 @@
.modules-box .module-box {
float: left;
width: 10em;
margin-top: 0.3em;
margin-bottom: 0.3em;
}
/*# sourceMappingURL=createPopup.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["createPopup.less"],"names":[],"mappings":"AAAA,YACC;EACC,WAAA;EACA,WAAA;EACA,iBAAA;EACA,oBAAA","file":"createPopup.css"}

View File

@@ -0,0 +1,69 @@
{$layout "layout_popup"}
<h3>创建系统用户</h3>
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<table class="ui table definition selectable">
<tr>
<td class="title">全名 *</td>
<td>
<input type="text" name="fullname" maxlength="100" ref="focus"/>
<p class="comment">可以输入姓名、公司名等容易识别的名称。</p>
</td>
</tr>
<tr>
<td>登录用户名 *</td>
<td>
<input type="text" name="username" maxlength="100"/>
<p class="comment">用户名只能英文、数字、下划线的组合。</p>
</td>
</tr>
<tr>
<td>登录密码 *</td>
<td>
<input type="password" name="pass1" maxlength="100"/>
</td>
</tr>
<tr>
<td>确认登录密码 *</td>
<td>
<input type="password" name="pass2" maxlength="100"/>
</td>
</tr>
<tr>
<td>允许登录</td>
<td>
<checkbox name="canLogin" value="1"></checkbox>
<p class="comment">选中后,当前管理员才可以登录当前的管理平台。</p>
</td>
</tr>
<tr>
<td>超级管理员</td>
<td>
<checkbox name="isSuper" v-model="isSuper"></checkbox>
<p class="comment">选中后,表示当前管理员为超级管理员;超级管理员自动拥有所有的管理权限。</p>
</td>
</tr>
<tr v-show="!isSuper">
<td>权限</td>
<td>
<div class="modules-box">
<div class="module-box" v-for="module in modules">
<checkbox name="moduleCodes" :v-value="module.code">{{module.name}}</checkbox>
</div>
</div>
</td>
</tr>
<tr>
<td>OTP认证</td>
<td>
<checkbox name="otpOn">启用OTP</checkbox>
<p class="comment">启用OTP认证后在用户登录的时候需要同时填写OTP动态密码。</p>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>

View File

@@ -0,0 +1,3 @@
Tea.context(function () {
this.isSuper = false
})

View File

@@ -0,0 +1,8 @@
.modules-box {
.module-box {
float: left;
width: 10em;
margin-top: 0.3em;
margin-bottom: 0.3em;
}
}

View File

@@ -0,0 +1,63 @@
{$layout}
{$template "menu"}
<div class="margin"></div>
<form class="ui form" method="get" action="/admins" v-show="!hasWeakPassword">
<div class="ui fields inline">
<div class="ui field">
<input type="text" name="keyword" placeholder="用户名、全名 ..." v-model="keyword"/>
</div>
<div class="ui field">
<button class="ui button" type="submit">搜索</button>
&nbsp;
<a href="/admins" v-if="keyword.length > 0">[清除条件]</a>
</div>
</div>
</form>
<div v-if="admins.length == 0">
<div class="margin"></div>
<p class="comment">暂时还没有<span v-if="keyword.length > 0">跟关键词匹配</span>管理员。</p>
</div>
<div v-if="hasWeakPassword">
<span class="ui label small basic blue">当前正在筛选弱密码管理员 <a href="/admins"><i class="icon remove small"></i></a></span>
</div>
<table class="ui table selectable" v-show="admins.length > 0">
<thead>
<tr>
<th>用户名</th>
<th>全名</th>
<th>允许登录</th>
<th>OTP认证</th>
<th class="center width10">状态</th>
<th class="two op">操作</th>
</tr>
</thead>
<tr v-for="admin in admins">
<td :class="{disabled:!admin.isOn}"><a :href="'/admins/admin?adminId=' + admin.id"><keyword :v-word="keyword">{{admin.username}}</keyword></a>
<div v-if="admin.isSuper || admin.hasWeakPassword" style="margin-top: 0.5em">
<tiny-basic-label class="olive" v-if="admin.isSuper">超级管理员</tiny-basic-label>
<a :href="'/admins/update?adminId=' + admin.id" v-if="admin.hasWeakPassword"><tiny-basic-label class="red" title="当前管理员已设置密码为弱密码,有极大的安全风险,请及时修改">弱密码</tiny-basic-label></a>
</div>
</td>
<td :class="{disabled:!admin.isOn}"><keyword :v-word="keyword">{{admin.fullname}}</keyword></td>
<td>
<span v-if="admin.canLogin" class="green">Y</span>
<span v-else class="disabled">N</span>
</td>
<td>
<span v-if="admin.otpLoginIsOn" class="green">Y</span>
<span v-else class="disabled">N</span>
</td>
<td class="center">
<label-on :v-is-on="admin.isOn"></label-on>
</td>
<td>
<a :href="'/admins/admin?adminId=' + admin.id">详情</a> &nbsp; <a href="" @click.prevent="deleteAdmin(admin.id)">删除</a>
</td>
</tr>
</table>
<div class="page" v-html="page"></div>

View File

@@ -0,0 +1,24 @@
Tea.context(function () {
this.createAdmin = function () {
teaweb.popup("/admins/createPopup", {
height: "30em",
callback: function () {
teaweb.success("保存成功", function () {
teaweb.reload()
})
}
})
}
this.deleteAdmin = function (adminId) {
let that = this
teaweb.confirm("确定要删除此系统用户吗?", function () {
that.$post(".delete")
.params({
adminId: adminId
})
.post()
.refresh()
})
}
})

View File

@@ -0,0 +1,7 @@
<first-menu>
<menu-item href="/admins/recipients" code="recipient">接收人</menu-item>
<menu-item href="/admins/recipients/groups" code="group">接收人分组</menu-item>
<menu-item href="/admins/recipients/instances" code="instance">媒介</menu-item>
<menu-item href="/admins/recipients/logs" code="log">发送记录</menu-item>
<menu-item href="/admins/recipients/tasks" code="task">任务队列</menu-item>
</first-menu>

View File

@@ -0,0 +1,7 @@
<first-menu>
<menu-item href="/admins/recipients">所有接收人</menu-item>
<span class="item">|</span>
<menu-item :href="'/admins/recipients/recipient?recipientId=' + recipient.id" code="recipient">"{{recipient.admin.fullname}} &nbsp;<span class="small grey">({{recipient.instance.name}})</span>"详情</menu-item>
<menu-item :href="'/admins/recipients/update?recipientId=' + recipient.id" code="update">修改</menu-item>
<menu-item :href="'/admins/recipients/test?recipientId=' + recipient.id" code="test">测试</menu-item>
</first-menu>

View File

@@ -0,0 +1,8 @@
.clusters-box .checkbox {
margin-bottom: 1em;
float: left;
width: 23%;
height: 1.2em;
overflow: hidden;
}
/*# sourceMappingURL=createPopup.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["createPopup.less"],"names":[],"mappings":"AAAA,aACC;EACC,kBAAA;EACA,WAAA;EACA,UAAA;EACA,aAAA;EACA,gBAAA","file":"createPopup.css"}

View File

@@ -0,0 +1,111 @@
{$layout "layout_popup"}
{$template "/code_editor"}
<h3>创建接收人</h3>
<form class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<table class="ui table definition selectable">
<tr>
<td class="title">系统用户 *</td>
<td>
<admin-selector></admin-selector>
<p class="comment">选择关联的系统用户。</p>
</td>
</tr>
<tr>
<td>媒介 *</td>
<td>
<message-media-instance-selector @change="changeInstance"></message-media-instance-selector>
</td>
</tr>
<tr>
<td>接收人标识</td>
<td>
<input type="text" name="user" maxlength="300"/>
<p class="comment">{{userDescription}}</p>
</td>
</tr>
<tr>
<td>分组</td>
<td>
<message-recipient-group-selector></message-recipient-group-selector>
<p class="comment">选择当前接收人所属分组。</p>
</td>
</tr>
<tr v-show="nodeClusters.length > 0">
<td>关联的边缘节点集群</td>
<td>
<div class="clusters-box">
<checkbox v-for="nodeCluster in nodeClusters" name="nodeClusterIds" :v-value="nodeCluster.id">{{nodeCluster.name}}</checkbox>
</div>
</td>
</tr>
<tr>
<td colspan="2"><more-options-indicator></more-options-indicator></td>
</tr>
<tbody v-show="moreOptionsVisible">
<tr>
<td>发送时间</td>
<td>
<div class="ui fields inline">
<div class="ui field">
开始时间:
</div>
<div class="ui field">
<div class="ui input right labeled">
<input type="text" name="timeFromHour" size="2" maxlength="2" placeholder="" v-model="timeFromHour"/>
<span class="ui label"></span>
</div>
</div>
<div class="ui field">
<div class="ui input right labeled">
<input type="text" name="timeFromMinute" size="2" maxlength="2" placeholder="" v-model="timeFromMinute"/>
<span class="ui label"></span>
</div>
</div>
<div class="ui field">
<div class="ui input right labeled">
<input type="text" name="timeFromSecond" size="2" maxlength="2" placeholder="" v-model="timeFromSecond"/>
<span class="ui label"></span>
</div>
</div>
</div>
<div class="ui divider"></div>
<div class="ui fields inline">
<div class="ui field">
结束时间:
</div>
<div class="ui field">
<div class="ui input right labeled">
<input type="text" name="timeToHour" size="2" maxlength="2" placeholder="" v-model="timeToHour"/>
<span class="ui label"></span>
</div>
</div>
<div class="ui field">
<div class="ui input right labeled">
<input type="text" name="timeToMinute" size="2" maxlength="2" placeholder="" v-model="timeToMinute"/>
<span class="ui label"></span>
</div>
</div>
<div class="ui field">
<div class="ui input right labeled">
<input type="text" name="timeToSecond" size="2" maxlength="2" placeholder="" v-model="timeToSecond"/>
<span class="ui label"></span>
</div>
</div>
</div>
<p class="comment">24小时制即小时从0到23。如果填写了发送时间系统只会在这个时间段内给当前接收人发送消息。 <a href="" v-if="timeFromHour.length > 0 || timeFromMinute.length > 0 || timeFromSecond.length > 0 || timeToHour.length > 0 || timeToMinute.length > 0 || timeToSecond.length > 0" @click.prevent="clearTime">[清除]</a></p>
</td>
</tr>
<tr>
<td>备注</td>
<td>
<textarea rows="3" name="description" maxlength="100"></textarea>
</td>
</tr>
</tbody>
</table>
<submit-btn></submit-btn>
</form>

View File

@@ -0,0 +1,30 @@
Tea.context(function () {
this.userDescription = ""
this.changeInstance = function (instance) {
if (instance != null) {
this.userDescription = instance.media.userDescription
} else {
this.userDescription = ""
}
}
/**
* 发送时间
*/
this.timeFromHour = ""
this.timeFromMinute = ""
this.timeFromSecond = ""
this.timeToHour = ""
this.timeToMinute = ""
this.timeToSecond = ""
this.clearTime = function () {
this.timeFromHour = ""
this.timeFromMinute = ""
this.timeFromSecond = ""
this.timeToHour = ""
this.timeToMinute = ""
this.timeToSecond = ""
}
})

View File

@@ -0,0 +1,9 @@
.clusters-box {
.checkbox {
margin-bottom: 1em;
float: left;
width: 23%;
height: 1.2em;
overflow: hidden;
}
}

View File

@@ -0,0 +1,18 @@
{$layout "layout_popup"}
<h3>创建接收人分组</h3>
<form class="ui form" data-tea-success="success" data-tea-action="$">
<csrf-token></csrf-token>
<table class="ui table definition selectable">
<tr>
<td class="title">分组名称 *</td>
<td>
<input type="text" name="name" maxlength="50" ref="focus"/>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>

View File

@@ -0,0 +1,28 @@
{$layout}
{$template "../menu"}
<second-menu>
<menu-item @click.prevent="createGroup">[创建接收人分组]</menu-item>
</second-menu>
<p class="comment" v-if="groups.length == 0">暂时还没有分组。</p>
<table class="ui table selectable celled" v-if="groups.length > 0">
<thead>
<tr>
<th>分组名称</th>
<th class="two wide">状态</th>
<th class="two op">操作</th>
</tr>
</thead>
<tr v-for="group in groups">
<td>{{group.name}}</td>
<td>
<label-on :v-is-on="group.isOn"></label-on>
</td>
<td>
<a href="" @click.prevent="updateGroup(group.id)">修改</a> &nbsp;
<a href="" @click.prevent="deleteGroup(group.id)">删除</a>
</td>
</tr>
</table>

View File

@@ -0,0 +1,33 @@
Tea.context(function () {
this.createGroup = function () {
teaweb.popup(Tea.url(".createPopup"), {
callback: function () {
teaweb.success("保存成功", function () {
teaweb.reload()
})
}
})
}
this.updateGroup = function (groupId) {
teaweb.popup(Tea.url(".updatePopup", {groupId: groupId}), {
callback: function () {
teaweb.success("保存成功", function () {
teaweb.reload()
})
}
})
}
this.deleteGroup = function (groupId) {
teaweb.confirm("确定要删除此分组吗?", function () {
this.$post(".delete")
.params({groupId: groupId})
.success(function () {
teaweb.success("删除成功", function () {
teaweb.reload()
})
})
})
}
})

View File

@@ -0,0 +1,15 @@
{$layout "layout_popup"}
<h3>选择分组</h3>
<table class="ui table definition selectable">
<tr>
<td class="title">选择分组</td>
<td>
<p class="comment" v-if="groups.length == 0">暂时没有可以选择的分组。</p>
<div v-if="groups.length > 0">
<a class="ui label small basic" v-for="group in groups" @click.prevent="selectGroup(group)">{{group.name}}</a>
</div>
</td>
</tr>
</table>

View File

@@ -0,0 +1,10 @@
Tea.context(function () {
this.selectGroup = function (group) {
NotifyPopup({
code: 200,
data: {
group: group
}
})
}
})

View File

@@ -0,0 +1,24 @@
{$layout "layout_popup"}
<h3>修改接收人分组</h3>
<form class="ui form" data-tea-success="success" data-tea-action="$">
<csrf-token></csrf-token>
<input type="hidden" name="groupId" :value="group.id"/>
<table class="ui table definition selectable">
<tr>
<td class="title">分组名称 *</td>
<td>
<input type="text" name="name" maxlength="50" ref="focus" v-model="group.name"/>
</td>
</tr>
<tr>
<td>启用当前分组</td>
<td>
<checkbox name="isOn" value="1" v-model="group.isOn"></checkbox>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>

View File

@@ -0,0 +1,51 @@
{$layout}
{$template "menu"}
<second-menu>
<menu-item @click.prevent="createRecipient">[创建接收人]</menu-item>
</second-menu>
<p class="comment" v-if="recipients.length == 0">暂时还没有媒介接收人。</p>
<table class="ui table selectable celled" v-if="recipients.length > 0">
<thead>
<tr>
<th>系统用户</th>
<th>媒介类型</th>
<th>接收人标识</th>
<th>所属分组</th>
<th>备注</th>
<th class="width5">状态</th>
<th class="two op">操作</th>
</tr>
</thead>
<tr v-for="recipient in recipients">
<td><a :href="'/admins/recipients/recipient?recipientId=' + recipient.id">{{recipient.admin.fullname}}</a> <span class="small grey">{{recipient.admin.username}}</span><link-icon :href="'/admins/admin?adminId=' + recipient.admin.id" size="tiny"></link-icon></td>
<td>{{recipient.instance.name}}</td>
<td>
<span v-if="recipient.user.length > 0">{{recipient.user}}</span>
<span v-else class="disabled">-</span>
</td>
<td>
<div v-if="recipient.groups != null && recipient.groups.length > 0">
<div v-for="group in recipient.groups" class="ui label small basic">{{group.name}}</div>
</div>
<div v-else>
<span class="disabled">-</span>
</div>
</td>
<td>
<span v-if="recipient.description.length > 0">{{recipient.description}}</span>
<span v-else class="disabled">-</span>
</td>
<td>
<label-on :v-is-on="recipient.isOn"></label-on>
</td>
<td>
<a :href="'/admins/recipients/recipient?recipientId=' + recipient.id">详情</a> &nbsp;
<a href="" @click.prevent="deleteRecipient(recipient.id)">删除</a>
</td>
</tr>
</table>
<div class="page" v-html="page"></div>

View File

@@ -0,0 +1,24 @@
Tea.context(function () {
this.createRecipient = function () {
teaweb.popup(Tea.url(".createPopup"), {
height: "27em",
callback: function () {
teaweb.success("保存成功", function () {
teaweb.reload()
})
}
})
}
this.deleteRecipient = function (recipientId) {
teaweb.confirm("确定要删除此接收媒介吗?", function () {
this.$post(".delete")
.params({recipientId: recipientId})
.success(function () {
teaweb.success("删除成功", function () {
teaweb.reload()
})
})
})
}
})

View File

@@ -0,0 +1,7 @@
<first-menu>
<menu-item href="/admins/recipients/instances">所有媒介</menu-item>
<span class="item">|</span>
<menu-item :href="'/admins/recipients/instances/instance?instanceId=' + instance.id" code="instance">"{{instance.media.name}}"详情</menu-item>
<menu-item :href="'/admins/recipients/instances/update?instanceId=' + instance.id" code="update">修改</menu-item>
<menu-item :href="'/admins/recipients/instances/test?instanceId=' + instance.id" code="test">测试</menu-item>
</first-menu>

View File

@@ -0,0 +1,12 @@
.CodeMirror {
border: 1px solid #eee;
height: auto !important;
}
.CodeMirror-vscrollbar {
width: 6px;
border-radius: 3px !important;
}
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
border-radius: 2px;
}
/*# sourceMappingURL=createPopup.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["createPopup.less"],"names":[],"mappings":"AACA;EACC,sBAAA;EACA,uBAAA;;AAGD;EACC,UAAA;EACA,6BAAA;;AAGD,sBAAsB;EACrB,kBAAA","file":"createPopup.css"}

View File

@@ -0,0 +1,414 @@
{$layout "layout_popup"}
{$template "/code_editor"}
<h3>创建媒介</h3>
<form class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<table class="ui table definition selectable">
<tr>
<td class="title">媒介名称 *</td>
<td>
<input type="text" name="name" ref="focus" maxlength="100"/>
<p class="comment">给当前媒介填写一个容易识别的名称。</p>
</td>
</tr>
<tr>
<td class="color-border title">媒介类型 *</td>
<td>
<message-media-selector @change="changeMediaType"></message-media-selector>
</td>
</tr>
<!-- E-mail -->
<tbody v-show="mediaType == 'email'">
<tr>
<td class="color-border">SMTP *</td>
<td>
<input type="text" name="emailSmtp" maxlength="100" placeholder="类似于 smtp.example.com:465"/>
<p class="comment">只支持SSL或TLS连接端口通常为465或587。</p>
</td>
</tr>
<tr>
<td class="color-border">账号 *</td>
<td>
<input type="text" name="emailUsername" v-model="emailUsername" maxlength="500" placeholder="类似于 xxx@example.com" @input="changeEmailUsername()"/>
<p class="comment">邮箱账号,比如 123456@qq.com<span v-html="emailUsernameHelp"></span></p>
</td>
</tr>
<tr>
<td class="color-border">密码 *</td>
<td>
<input type="password" name="emailPassword" maxlength="100"/>
<p class="comment">账号对应的密码或者授权码比如QQ邮箱就需要授权码</p>
</td>
</tr>
<tr>
<td class="color-border">发送者Email</td>
<td>
<input type="text" name="emailFrom" maxlength="500" placeholder="类似于 xxx@example.com"/>
<p class="comment">默认和账号一致</p>
</td>
</tr>
</tbody>
<!-- webHook -->
<tbody v-show="mediaType == 'webHook'">
<tr>
<td class="color-border">URL *</td>
<td>
<input type="text" name="webHookURL" maxlength="500" placeholder="http://..."/>
<p class="comment">可以在URL中使用<code-label>${MessageUser}</code-label><code-label>${MessageSubject}</code-label><code-label>${MessageBody}</code-label>来代表接收人标识、标题和内容。</p>
</td>
</tr>
<tr>
<td class="color-border">请求方法 *</td>
<td>
<select name="webHookMethod" v-model="webHookMethod" class="ui dropdown" style="width:10em">
<option v-for="method in methods" :value="method">{{method}}</option>
</select>
<p class="comment" v-if="webHookMethod == 'POST'">将以POST方式发送<code-label>${MessageUser}</code-label><code-label>${MessageSubject}</code-label><code-label>${MessageBody}</code-label>参数,分别代表接收人标识、标题和内容</p>
</td>
</tr>
<tr v-show="advancedOptionsVisible">
<td class="color-border">自定义报头</td>
<td>
<div class="webHook-headers-box">
<span class="ui label small basic" v-for="(header,index) in webHookHeaders">{{header.name}}:{{header.value}}
<input type="hidden" name="webHookHeaderNames" :value="header.name"/>
<input type="hidden" name="webHookHeaderValues" :value="header.value"/>
<a href="" title="删除" @click.prevent="removeWebHookHeader(index)"><i class="icon remove"></i></a>
</span>
</div>
<div style="margin-top: 1em">
<button class="ui button tiny" type="button" @click.prevent="addWebHookHeader()" v-if="!webHookHeadersAdding">+</button>
</div>
<div v-if="webHookHeadersAdding" style="margin-top: 1em">
<div class="ui fields inline">
<div class="ui field">
<input type="text" name="webHookHeaderName" v-model="webHookHeadersAddingName" size="8" placeholder="名称" maxlength="100" @keyup.enter="confirmWebHookHeadersAdding" @keypress.enter.prevent="1"/>
</div>
<div class="ui field">:</div>
<div class="ui field">
<input type="text" placeholder="值" v-model="webHookHeadersAddingValue" size="12" maxlength="256" @keyup.enter="confirmWebHookHeadersAdding" @keypress.enter.prevent="1"/>
</div>
<div class="ui field">
<button class="ui button tiny" type="button" @click.prevent="confirmWebHookHeadersAdding()">确定</button>
&nbsp; <a href="" @click.prevent="cancelWebHookHeadersAdding()">取消</a>
</div>
</div>
</div>
</td>
</tr>
<tr v-show="webHookMethod == 'POST' && advancedOptionsVisible">
<td class="color-border">自定义内容</td>
<td>
<div class="ui menu tabular small attached">
<a href="" class="item" :class="{active:webHookContentType == 'params'}" @click.prevent="selectWebHookContentType('params')">参数对</a>
<a href="" class="item" :class="{active:webHookContentType == 'body'}" @click.prevent="selectWebHookContentType('body')">文本内容</a>
</div>
<div class="ui segment attached" v-if="webHookContentType == 'params'">
<input type="hidden" name="webHookContentType" value="params"/>
<div class="webHook-headers-box">
<span class="ui label small basic" v-for="(param,index) in webHookParams">{{param.name}}:{{param.value}}
<input type="hidden" name="webHookParamNames" :value="param.name"/>
<input type="hidden" name="webHookParamValues" :value="param.value"/>
<a href="" title="删除" @click.prevent="removeWebHookParam(index)"><i class="icon remove"></i></a>
</span>
</div>
<button class="ui button tiny" type="button" @click.prevent="addWebHookParam()" v-if="!webHookParamsAdding">+</button>
<div v-if="webHookParamsAdding">
<input type="hidden" name="webHookContentType" value="params"/>
<div class="ui fields inline">
<div class="ui field">
<input type="text" name="webHookParamName" v-model="webHookParamsAddingName" placeholder="名称" maxlength="100" @keyup.enter="confirmWebHookParamsAdding" @keypress.enter.prevent="1"/>
</div>
<div class="ui field">:</div>
<div class="ui field">
<textarea type="text" placeholder="值" v-model="webHookParamsAddingValue" cols="50" maxlength="1024" rows="2"></textarea>
</div>
</div>
<button class="ui button tiny" type="button" @click.prevent="confirmWebHookParamsAdding()">确认添加</button>
&nbsp; <a href="" @click.prevent="cancelWebHookParamsAdding()">取消</a>
</div>
</div>
<div class="ui segment attached" v-if="webHookContentType == 'body'">
<input type="hidden" name="webHookContentType" value="body"/>
<textarea name="webHookBody" v-model="webHookBody" rows="5" placeholder="发送的内容"></textarea>
<p class="comment">
内容中可以使用三个变量:<code-label>${MessageUser}</code-label><code-label>${MessageSubject}</code-label><code-label>${MessageBody}</code-label>参数,分别代表接收人标识、标题和内容
</p>
</div>
</td>
</tr>
</tbody>
<!-- 脚本 -->
<tbody v-show="mediaType == 'script'">
<tr>
<td class="color-border">脚本 *</td>
<td>
<input type="hidden" name="scriptType" :value="scriptTab"/>
<input type="hidden" name="scriptLang" :value="scriptLang"/>
<div class="ui tabular menu attached small">
<a class="item" :class="{active:scriptTab == 'path'}" @click.prevent="selectScriptTab('path')">脚本文件</a>
<a class="item" :class="{active:scriptTab == 'code'}" @click.prevent="selectScriptTab('code')">脚本代码</a>
</div>
<div class="ui bottom segment attached" v-show="scriptTab == 'path'">
<input type="text" name="scriptPath" maxlength="500"/>
<p class="comment">如果是Shell脚本请不要忘记在头部添加 <em>#!脚本解释工具</em>,比如 <em>#!/bin/bash</em><br/>
执行此脚本时,在脚本中可以使用<code-label>${MessageUser}</code-label><code-label>${MessageSubject}</code-label><code-label>${MessageBody}</code-label>三个环境变量分别代表通知的接收人标识、标题和内容。
</p>
</div>
<div class="ui bottom segment attached" v-show="scriptTab == 'code'" style="padding-top:0">
<div class="ui menu text small">
<a class="item" v-for="lang in scriptLangs" :class="{active:lang.code == scriptLang}" @click.prevent="selectScriptLang(lang.code)">{{lang.name}}</a>
</div>
<textarea name="scriptCode" id="script-code-editor" rows="1"></textarea>
<p class="comment">如果是Shell脚本请不要忘记在头部添加 <em>#!脚本解释工具</em>,比如 <em>#!/bin/bash</em><br/>
执行此脚本时,在脚本中可以使用<code-label>${MessageUser}</code-label><code-label>${MessageSubject}</code-label><code-label>${MessageBody}</code-label>三个环境变量分别代表通知的接收人标识、标题和内容。
</p>
</div>
</td>
</tr>
<tr v-show="advancedOptionsVisible">
<td class="color-border">当前工作目录<em>CWD</em></td>
<td>
<input type="text" name="scriptCwd" maxlength="500"/>
</td>
</tr>
<tr v-show="advancedOptionsVisible">
<td class="color-border">环境变量<em>ENV</em></td>
<td>
<div class="ui field">
<span class="ui label small basic" v-for="(var1, index) in env">
<input type="hidden" name="scriptEnvNames" :value="var1.name"/>
<input type="hidden" name="scriptEnvValues" :value="var1.value"/>
<em>{{var1.name}}</em>: {{var1.value}}
<a href="" @click.prevent="removeEnv(index)"><i class="icon remove"></i></a>
</span>
</div>
<div v-if="envAdding" class="ui fields inline">
<div class="ui field">
<input type="text" name="envAddingName" v-model="envAddingName" placeholder="变量名" style="width:9em" @keyup.enter="confirmAddEnv" @keypress.enter.prevent="1"/>
</div>
<div class="ui field">
<input type="text" name="envAddingValue" v-model="envAddingValue" placeholder="变量值" style="width:15em" @keyup.enter="confirmAddEnv" @keypress.enter.prevent="1"/>
</div>
<div class="ui field">
<button class="ui button" type="button" @click="confirmAddEnv()">添加</button>
</div>
<div class="ui field">
<a href="" @click.prevent="cancelEnv()"><i class="icon remove"></i></a>
</div>
</div>
<div class="ui field">
<button class="ui button small" type="button" @click="addEnv()">+</button>
</div>
</td>
</tr>
</tbody>
<!-- 钉钉群机器人 -->
<tbody v-show="mediaType == 'dingTalk'">
<tr>
<td class="color-border">Hook地址 *</td>
<td>
<textarea name="dingTalkWebHookURL" maxlength="500" placeholder="https://oapi.dingtalk.com/robot/send?access_token=xxx" rows="2"></textarea>
<p class="comment">填入自定义群机器人的Hook地址<a href="https://open.dingtalk.com/document/orgapp/custom-robot-access" target="_blank">获取方法</a></p>
</td>
</tr>
</tbody>
<!-- 企业微信 -->
<tbody v-show="mediaType == 'qyWeixin'">
<tr>
<td class="color-border">企业ID *</td>
<td>
<input type="text" name="qyWeixinCorporateId" maxlength="100" />
</td>
</tr>
<tr>
<td class="color-border">应用AgentId *</td>
<td>
<input type="text" name="qyWeixinAgentId" maxlength="100"/>
</td>
</tr>
<tr>
<td class="color-border">应用Secret *</td>
<td>
<input type="text" name="qyWeixinAppSecret" maxlength="100"/>
</td>
</tr>
<tr>
<td class="color-border">内容文本格式</td>
<td>
<select name="qyWeixinTextFormat" v-model="qyWeixinTextFormat" class="ui dropdown" style="width:10em">
<option value="text">普通文本</option>
<option value="markdown">Markdown</option>
</select>
<p class="comment" v-if="qyWeixinTextFormat == 'markdown'">企业微信目前只支持少数的Markdown语法<a href="https://work.weixin.qq.com/api/doc#90000/90135/90236/%E6%94%AF%E6%8C%81%E7%9A%84markdown%E8%AF%AD%E6%B3%95" target="_blank">点击这里了解</a></p>
</td>
</tr>
</tbody>
<!-- 企业微信群机器人 -->
<tbody v-show="mediaType == 'qyWeixinRobot'">
<tr>
<td class="color-border">WebHook地址 *</td>
<td>
<textarea name="qyWeixinRobotWebHookURL" maxlength="500" placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx" rows="2"></textarea>
<p class="comment">填入自定义群机器人的WebHook地址<a href="https://open.work.weixin.qq.com/help2/pc/14931" target="_blank">获取方法</a></p>
</td>
</tr>
<tr>
<td class="color-border">内容文本格式</td>
<td>
<select name="qyWeixinRobotTextFormat" v-model="qyWeixinRobotTextFormat" class="ui dropdown" style="width:10em">
<option value="text">普通文本</option>
<option value="markdown">Markdown</option>
</select>
<p class="comment" v-if="qyWeixinRobotTextFormat == 'markdown'">企业微信目前只支持少数的Markdown语法<a href="https://work.weixin.qq.com/api/doc#90000/90135/91760/markdown%E7%B1%BB%E5%9E%8B" target="_blank">点击这里了解</a></p>
</td>
</tr>
</tbody>
<!-- 阿里云短信 -->
<tbody v-show="mediaType == 'aliyunSms'">
<tr>
<td class="color-border">签名名称 *</td>
<td>
<input type="text" name="aliyunSmsSign" maxlength="100"/>
<p class="comment">已经审核通过的短信签名名称</p>
</td>
</tr>
<tr>
<td class="color-border">模板CODE *</td>
<td>
<input type="text" name="aliyunSmsTemplateCode" maxlength="100" placeholder="类似于SMS_12345"/>
<p class="comment">已经审核通过的模板CODE</p>
</td>
</tr>
<tr>
<td class="color-border">模板变量 *</td>
<td>
<div class="ui field">
<span class="ui label small basic" v-for="(var1, index) in aliyunSmsTemplateVars">
<input type="hidden" name="aliyunSmsTemplateVarNames" :value="var1.name"/>
<input type="hidden" name="aliyunSmsTemplateVarValues" :value="var1.value"/>
<em>{{var1.name}}</em>: {{var1.value}}
<a href="" @click.prevent="removeAliyunSmsTemplateVar(index)"><i class="icon remove"></i></a>
</span>
</div>
<div v-if="aliyunSmsTemplateVarAdding" class="ui fields inline">
<div class="ui field">
<input type="text" name="aliyunSmsTemplateVarAddingName" v-model="aliyunSmsTemplateVarAddingName" placeholder="变量名" style="width:9em" @keyup.enter="confirmAddAliyunSmsTemplateVar" @keypress.enter.prevent="1"/>
</div>
<div class="ui field">
<input type="text" name="aliyunSmsTemplateVarAddingValue" v-model="aliyunSmsTemplateVarAddingValue" placeholder="变量值" style="width:15em" @keyup.enter="confirmAddAliyunSmsTemplateVar" @keypress.enter.prevent="1"/>
</div>
<div class="ui field">
<button class="ui button" type="button" @click="confirmAddAliyunSmsTemplateVar()">添加</button>
</div>
<div class="ui field">
<a href="" @click.prevent="cancelAliyunSmsTemplateVar()"><i class="icon remove"></i></a>
</div>
</div>
<div class="ui field">
<button class="ui button small" type="button" @click="addAliyunSmsTemplateVar()">+</button>
</div>
<p class="comment">模板中使用的变量,在变量中可以使用<code-label>${MessageUser}</code-label><code-label>${MessageSubject}</code-label><code-label>${MessageBody}</code-label>来代表接收人标识、标题和内容。</p>
</td>
</tr>
<tr>
<td class="color-border">AccessKey ID *</td>
<td>
<input type="text" name="aliyunSmsAccessKeyId" maxlength="100"/>
<p class="comment">在阿里云控制台中的访问控制中某个用户的AccessKey ID</p>
</td>
</tr>
<tr>
<td class="color-border">AccessKey Secret *</td>
<td>
<input type="text" name="aliyunSmsAccessKeySecret" maxlength="100"/>
<p class="comment">在阿里云控制台中的访问控制中某个用户的AccessKey Secret和上面的AccessKey ID匹配</p>
</td>
</tr>
</tbody>
<!-- Telegram -->
<tbody v-if="mediaType == 'telegram'">
<tr>
<td class="color-border">机器人Token *</td>
<td>
<input type="text" name="telegramToken"/>
<p class="comment">创建机器人的时候可以获得,类似于 <code-label>123456:AAAA-AAAAAAAAAAAAAAAAAAAA</code-label>,可以向 <code-label>@BotFather</code-label> 发送 <code-label>/newbot</code-label>指令创建新的机器人。</p>
</td>
</tr>
<tr>
<td class="color-border">代理服务</td>
<td>
<div class="ui fields inline">
<div class="ui field">
<select class="ui dropdown" name="telegramProxyScheme">
<option value="socks5">socks5://</option>
<option value="https">https://</option>
<option value="http">http://</option>
</select>
</div>
<div class="ui field">
<input type="text" name="telegramProxyHost" placeholder="HOST:PORT"/>
</div>
</div>
<p class="comment">可选项仅当API节点所在服务器无法直接连接Telegram API时填写测试是否能够连接Telegram API的方法<code-label>curl https://api.telegram.org</code-label>出现2XX、3XX、4XX之类提示时说明能够直接连接如果你设置了代理请使用<code-label>curl -x "你的代理服务地址" https://api.telegram.org</code-label>来测试你的代理是否可用。</p>
</td>
</tr>
</tbody>
<tr>
<td>发送频率<em>(次/分钟)</em></td>
<td>
<div class="ui fields inline">
<div class="ui field">
<div class="ui right labeled input">
<input type="text" name="rateCount" size="4" maxlength="4"/>
<span class="ui label"></span>
</div>
</div>
<div class="ui field">/</div>
<div class="ui field">
<div class="ui right labeled input">
<input type="text" name="rateMinutes" size="4" maxlength="4"/>
<span class="ui label">分钟</span>
</div>
</div>
</div>
<p class="comment">用来限制此媒介的发送频率,防止短时间内消息数量过载。</p>
</td>
</tr>
<tr>
<td>忽略相似消息周期</td>
<td>
<div class="ui input right labeled">
<input type="text" name="hashLife" style="width: 5em" maxlength="4"/>
<span class="ui label"></span>
</div>
<p class="comment">可以在这个时间内忽略相似消息防止短时间内消息数量过载。默认60秒。</p>
</td>
</tr>
<tr>
<td colspan="2"><more-options-indicator></more-options-indicator></td>
</tr>
<tbody v-show="moreOptionsVisible">
<tr>
<td>备注</td>
<td>
<textarea rows="3" name="description" maxlength="100"></textarea>
</td>
</tr>
</tbody>
</table>
<submit-btn></submit-btn>
</form>

View File

@@ -0,0 +1,334 @@
Tea.context(function () {
this.mediaType = ""
this.advancedOptionsVisible = true
let that = this
this.changeMediaType = function (media) {
that.mediaType = media.type
}
/**
* 邮箱
*/
this.emailUsername = "";
this.emailUsernameHelp = "";
this.changeEmailUsername = function () {
this.emailUsernameHelp = "";
if (this.emailUsername.indexOf("qq.com") > 0) {
this.emailUsernameHelp = "<a href=\"https://service.mail.qq.com\" target='_blank'>QQ邮箱相关设置帮助</a>";
} else if (this.emailUsername.indexOf("163.com") > 0) {
this.emailUsernameHelp = "<a href=\"https://help.mail.163.com/faqDetail.do?code=d7a5dc8471cd0c0e8b4b8f4f8e49998b374173cfe9171305fa1ce630d7f67ac22dc0e9af8168582a\" target='_blank'>网易邮箱相关设置帮助</a>";
}
};
/**
* webHook
*/
this.methods = ["GET", "POST"]
this.webHookMethod = "GET";
this.webHookHeadersAdding = false;
this.webHookHeaders = [];
this.webHookHeadersAddingName = "";
this.webHookHeadersAddingValue = "";
this.addWebHookHeader = function () {
this.webHookHeadersAdding = true;
this.$delay(function () {
this.$find("form input[name='webHookHeaderName']").focus();
});
};
this.cancelWebHookHeadersAdding = function () {
this.webHookHeadersAdding = false;
};
this.confirmWebHookHeadersAdding = function () {
this.webHookHeaders.push({
"name": this.webHookHeadersAddingName,
"value": this.webHookHeadersAddingValue
});
this.webHookHeadersAddingName = "";
this.webHookHeadersAddingValue = "";
this.webHookHeadersAdding = false;
};
this.removeWebHookHeader = function (index) {
if (!window.confirm("确定要删除此Header吗")) {
return;
}
this.webHookHeaders.$remove(index);
};
this.webHookContentType = "params";
this.selectWebHookContentType = function (contentType) {
this.webHookContentType = contentType;
this.$delay(function () {
if (contentType == "params") {
} else if (contentType == "body") {
this.$find("form textarea[name='webHookBody']").focus();
}
});
};
this.webHookParamsAdding = false;
this.webHookParams = [];
this.webHookParamsAddingName = "";
this.webHookParamsAddingValue = "";
this.addWebHookParam = function () {
this.webHookParamsAdding = true;
this.$delay(function () {
this.$find("form input[name='webHookParamName']").focus();
});
};
this.cancelWebHookParamsAdding = function () {
this.webHookParamsAdding = false;
};
this.confirmWebHookParamsAdding = function () {
this.webHookParams.push({
"name": this.webHookParamsAddingName,
"value": this.webHookParamsAddingValue
});
this.webHookParamsAddingName = "";
this.webHookParamsAddingValue = "";
this.webHookParamsAdding = false;
};
this.removeWebHookParam = function (index) {
if (!window.confirm("确定要删除此参数吗?")) {
return;
}
this.webHookParams.$remove(index);
};
this.webHookBody = "";
/**
* 企业微信
*/
this.qyWeixinTextFormat = "text";
/**
* 企业微信群机器人
*/
this.qyWeixinRobotTextFormat = "text";
/**
* 脚本
*/
let scriptEditor = null
this.scriptTab = "path";
this.scriptLang = "shell";
this.scriptLangs = [
{
"name": "Shell",
"code": "shell"
},
{
"name": "批处理(bat)",
"code": "bat"
},
{
"name": "PHP",
"code": "php"
},
{
"name": "Python",
"code": "python"
},
{
"name": "Ruby",
"code": "ruby"
},
{
"name": "NodeJS",
"code": "nodejs"
}
];
this.selectScriptTab = function (tab) {
this.scriptTab = tab;
if (tab == "path") {
this.$delay(function () {
this.$find("form input[name='scriptPath']").focus();
});
} else if (tab == "code") {
this.$delay(function () {
this.loadEditor();
});
}
};
this.selectScriptLang = function (lang) {
this.scriptLang = lang;
switch (lang) {
case "shell":
scriptEditor.setValue("#!/usr/bin/env bash\n\n# your commands here\n");
var info = CodeMirror.findModeByMIME("text/x-sh");
if (info != null) {
scriptEditor.setOption("mode", info.mode);
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js";
CodeMirror.autoLoadMode(scriptEditor, info.mode);
}
break;
case "bat":
scriptEditor.setValue("");
break;
case "php":
scriptEditor.setValue("#!/usr/bin/env php\n\n<?php\n// your PHP codes here");
var info = CodeMirror.findModeByMIME("text/x-php");
if (info != null) {
scriptEditor.setOption("mode", info.mode);
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js";
CodeMirror.autoLoadMode(scriptEditor, info.mode);
}
break;
case "python":
scriptEditor.setValue("#!/usr/bin/env python\n\n''' your Python codes here '''");
var info = CodeMirror.findModeByMIME("text/x-python");
if (info != null) {
scriptEditor.setOption("mode", info.mode);
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js";
CodeMirror.autoLoadMode(scriptEditor, info.mode);
}
break;
case "ruby":
scriptEditor.setValue("#!/usr/bin/env ruby\n\n# your Ruby codes here");
var info = CodeMirror.findModeByMIME("text/x-ruby");
if (info != null) {
scriptEditor.setOption("mode", info.mode);
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js";
CodeMirror.autoLoadMode(scriptEditor, info.mode);
}
break;
case "nodejs":
scriptEditor.setValue("#!/usr/bin/env node\n\n// your javascript codes here");
var info = CodeMirror.findModeByMIME("text/javascript");
if (info != null) {
scriptEditor.setOption("mode", info.mode);
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js";
CodeMirror.autoLoadMode(scriptEditor, info.mode);
}
break;
}
scriptEditor.save();
scriptEditor.focus();
};
this.loadEditor = function () {
if (scriptEditor == null) {
scriptEditor = CodeMirror.fromTextArea(document.getElementById("script-code-editor"), {
theme: "idea",
lineNumbers: true,
value: "",
readOnly: false,
showCursorWhenSelecting: true,
height: "auto",
//scrollbarStyle: null,
viewportMargin: Infinity,
lineWrapping: true,
highlightFormatting: false,
indentUnit: 4,
indentWithTabs: true
});
}
scriptEditor.setValue("#!/usr/bin/env bash\n\n# your commands here\n");
scriptEditor.save();
scriptEditor.focus();
var info = CodeMirror.findModeByMIME("text/x-sh");
if (info != null) {
scriptEditor.setOption("mode", info.mode);
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js";
CodeMirror.autoLoadMode(scriptEditor, info.mode);
}
scriptEditor.on("change", function () {
scriptEditor.save();
});
};
/**
* 环境变量
*/
this.env = [];
this.envAdding = false;
this.envAddingName = "";
this.envAddingValue = "";
this.addEnv = function () {
this.envAdding = !this.envAdding;
this.$delay(function () {
this.$find("form input[name='envAddingName']").focus();
});
};
this.confirmAddEnv = function () {
if (this.envAddingName.length == 0) {
alert("请输入变量名");
this.$find("form input[name='envAddingName']").focus();
return;
}
this.env.push({
"name": this.envAddingName,
"value": this.envAddingValue
});
this.envAdding = false;
this.envAddingName = "";
this.envAddingValue = "";
};
this.removeEnv = function (index) {
this.env.$remove(index);
};
this.cancelEnv = function () {
this.envAdding = false;
};
/**
* 阿里云短信模板
*/
this.aliyunSmsTemplateVars = [];
this.aliyunSmsTemplateVarAdding = false;
this.aliyunSmsTemplateVarAddingName = "";
this.aliyunSmsTemplateVarAddingValue = "";
this.addAliyunSmsTemplateVar = function () {
this.aliyunSmsTemplateVarAdding = !this.aliyunSmsTemplateVarAdding;
this.$delay(function () {
this.$find("form input[name='aliyunSmsTemplateVarAddingName']").focus();
});
};
this.confirmAddAliyunSmsTemplateVar = function () {
if (this.aliyunSmsTemplateVarAddingName.length == 0) {
alert("请输入变量名");
this.$find("form input[name='aliyunSmsTemplateVarAddingName']").focus();
return;
}
this.aliyunSmsTemplateVars.push({
"name": this.aliyunSmsTemplateVarAddingName,
"value": this.aliyunSmsTemplateVarAddingValue
});
this.aliyunSmsTemplateVarAdding = false;
this.aliyunSmsTemplateVarAddingName = "";
this.aliyunSmsTemplateVarAddingValue = "";
};
this.removeAliyunSmsTemplateVar = function (index) {
this.aliyunSmsTemplateVars.$remove(index);
};
this.cancelAliyunSmsTemplateVar = function () {
this.aliyunSmsTemplateVarAdding = false;
};
})

View File

@@ -0,0 +1,14 @@
// code mirror
.CodeMirror {
border: 1px solid #eee;
height: auto !important;
}
.CodeMirror-vscrollbar {
width: 6px;
border-radius: 3px !important;
}
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
border-radius: 2px;
}

View File

@@ -0,0 +1,37 @@
{$layout}
{$template "../menu"}
<second-menu>
<menu-item @click.prevent="createInstance">[创建媒介]</menu-item>
</second-menu>
<p class="comment" v-if="instances.length == 0">暂时还没有媒介接收人。</p>
<table class="ui table selectable celled" v-if="instances.length > 0">
<thead>
<tr>
<th>媒介名称</th>
<th class="three wide">媒介类型</th>
<th>备注</th>
<th class="two wide">状态</th>
<th class="two op">操作</th>
</tr>
</thead>
<tr v-for="instance in instances">
<td><a :href="'/admins/recipients/instances/instance?instanceId=' + instance.id">{{instance.name}}</a></td>
<td>{{instance.media.name}}</td>
<td>
<span v-if="instance.description.length > 0">{{instance.description}}</span>
<span v-else class="disabled">-</span>
</td>
<td>
<label-on :v-is-on="instance.isOn"></label-on>
</td>
<td>
<a :href="'/admins/recipients/instances/instance?instanceId=' + instance.id">详情</a> &nbsp;
<a href="" @click.prevent="deleteInstance(instance.id)">删除</a>
</td>
</tr>
</table>
<div class="page" v-html="page"></div>

View File

@@ -0,0 +1,24 @@
Tea.context(function () {
this.createInstance = function () {
teaweb.popup(Tea.url(".createPopup"), {
height: "30em",
callback: function () {
teaweb.success("保存成功", function () {
teaweb.reload()
})
}
})
}
this.deleteInstance = function (instanceId) {
teaweb.confirm("确定要删除此接收媒介吗?", function () {
this.$post(".delete")
.params({instanceId: instanceId})
.success(function () {
teaweb.success("删除成功", function () {
teaweb.reload()
})
})
})
}
})

View File

@@ -0,0 +1,12 @@
.CodeMirror {
border: 1px solid #eee;
height: auto!important;
}
.CodeMirror-vscrollbar {
width: 6px;
border-radius: 3px!important;
}
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
border-radius: 2px;
}
/*# sourceMappingURL=instance.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["instance.less"],"names":[],"mappings":"AACA;EACC,sBAAA;EACA,sBAAA;;AAGD;EACC,UAAA;EACA,4BAAA;;AAGD,sBAAsB;EACrB,kBAAA","file":"instance.css"}

View File

@@ -0,0 +1,250 @@
{$layout}
{$template "instance_menu"}
{$template "/code_editor"}
<table class="ui table definition selectable">
<tr>
<td class="title">媒介名称</td>
<td>{{instance.name}}</td>
</tr>
<tr>
<td>状态</td>
<td>
<label-on :v-is-on="instance.isOn"></label-on>
</td>
</tr>
<tr>
<td class="color-border">媒介类型</td>
<td>
{{instance.media.name}}
</td>
</tr>
<!-- E-mail -->
<tbody v-if="instance.media.type == 'email'">
<tr>
<td class="color-border">SMTP</td>
<td>
{{instance.params.smtp}}
</td>
</tr>
<tr>
<td class="color-border">账号</td>
<td>
{{instance.params.username}}
</td>
</tr>
<tr>
<td class="color-border">密码</td>
<td>
{{instance.params.password}}
</td>
</tr>
<tr>
<td class="color-border">发送者Email</td>
<td>
<span v-if="instance.params.from.length > 0">{{instance.params.from}}</span>
<span v-if="instance.params.from.length == 0" class="disabled">没有设置</span>
</td>
</tr>
</tbody>
<!-- WebHook -->
<tbody v-if="instance.media.type == 'webHook'">
<tr>
<td class="color-border">URL</td>
<td>
{{instance.params.url}}
</td>
</tr>
<tr>
<td class="color-border">请求方法</td>
<td>
{{instance.params.method}}
</td>
</tr>
<tr>
<td class="color-border">自定义报头</td>
<td>
<span v-if="instance.params.headers == null || instance.params.headers.length == 0" class="disabled">还没有自定义Header</span>
<div v-if="instance.params.headers != null && instance.params.headers.length > 0">
<span class="ui label small" v-for="header in instance.params.headers">{{header.name}}:{{header.value}}</span>
</div>
</td>
</tr>
<tr v-if="instance.params.contentType != null && instance.params.contentType.length > 0">
<td class="color-border">
<span v-if="instance.params.contentType == 'params'">自定义参数</span>
<span v-if="instance.params.contentType == 'body'">自定义内容</span>
</td>
<td class="color-border">
<div v-if="instance.params.contentType == 'params'">
<span v-if="instance.params.params == null || instance.params.params.length == 0" class="disabled">还没有自定义参数</span>
<div v-if="instance.params.params != null && instance.params.params.length > 0">
<span class="ui label small" v-for="param in instance.params.params">{{param.name}}:{{param.value}}</span>
</div>
</div>
<div v-if="instance.params.contentType == 'body'">
<xmp style="margin-top:0;margin-bottom:0">{{instance.params.body}}</xmp>
</div>
</td>
</tr>
</tbody>
<!-- 脚本 -->
<tbody v-if="instance.media.type == 'script'">
<tr>
<td class="color-border">脚本</td>
<td>
<span v-if="instance.params.scriptType == 'path'">{{instance.params.path}}</span>
<div id="script-code-editor" v-show="instance.params.scriptType == 'code'"></div>
</td>
</tr>
<tr>
<td class="color-border">当前工作目录<em>CWD</em></td>
<td>
<span v-if="instance.params.cwd.length > 0">{{instance.params.cwd}}</span>
<span v-if="instance.params.cwd.length == 0" class="disabled">没有设置</span>
</td>
</tr>
<tr>
<td class="color-border">环境变量<em>ENV</em></td>
<td>
<span v-if="instance.params.env == null || instance.params.env.length == 0" class="disabled">没有设置</span>
<div v-if="instance.params.env != null && instance.params.env.length > 0">
<span class="ui label small" v-for="(var1, index) in instance.params.env">
<em>{{var1.name}}</em>: {{var1.value}}
</span>
</div>
</td>
</tr>
</tbody>
<tbody v-if="instance.media.type == 'dingTalk'">
<tr>
<td class="color-border">Hook地址</td>
<td>{{instance.params.webHookURL}}</td>
</tr>
</tbody>
<!-- 企业微信 -->
<tbody v-if="instance.media.type == 'qyWeixin'">
<tr>
<td class="color-border">企业ID</td>
<td>
{{instance.params.corporateId}}
</td>
</tr>
<tr>
<td class="color-border">应用AgentId</td>
<td>
{{instance.params.agentId}}
</td>
</tr>
<tr>
<td class="color-border">应用Secret</td>
<td>
{{instance.params.appSecret}}
</td>
</tr>
<tr>
<td class="color-border">内容文本格式</td>
<td>
<span v-if="instance.params.textFormat == null">text</span>
{{instance.params.textFormat}}
</td>
</tr>
</tbody>
<!-- 企业微信群机器人 -->
<tbody v-if="instance.media.type == 'qyWeixinRobot'">
<tr>
<td class="color-border">WebHook地址</td>
<td>{{instance.params.webHookURL}}</td>
</tr>
<tr>
<td class="color-border">内容文本格式</td>
<td>
<span v-if="instance.params.textFormat == null">text</span>
{{instance.params.textFormat}}
</td>
</tr>
</tbody>
<!-- 阿里云短信 -->
<tbody v-show="instance.media.type == 'aliyunSms'">
<tr>
<td class="color-border">签名名称</td>
<td>
{{instance.params.sign}}
</td>
</tr>
<tr>
<td class="color-border">模板CODE</td>
<td>
{{instance.params.templateCode}}
</td>
</tr>
<tr>
<td class="color-border">模板变量</td>
<td>
<div v-if="instance.params.variables != null">
<span class="ui label small" v-for="(var1, index) in instance.params.variables">
<em>{{var1.name}}</em>: {{var1.value}}
</span>
</div>
</td>
</tr>
<tr>
<td class="color-border">AccessKey ID</td>
<td>
{{instance.params.accessKeyId}}
</td>
</tr>
<tr>
<td class="color-border">AccessKey Secret</td>
<td>
{{instance.params.accessKeySecret}}
</td>
</tr>
</tbody>
<!-- Telegram机器人 -->
<tbody v-show="instance.media.type == 'telegram'">
<tr>
<td class="color-border">机器人Token</td>
<td>{{instance.params.token}}</td>
</tr>
<tr>
<td class="color-border">代理服务</td>
<td>
<span v-if="instance.params.proxyURL != null && instance.params.proxyURL.length > 0 && instance.params.proxyURL.match(/^\w+:\/\/.+/)">
{{instance.params.proxyURL}}
</span>
<span v-else class="disabled">没有设置</span>
</td>
</tr>
</tbody>
<tr>
<td>发送频率<em>(次/分钟)</em></td>
<td>
<span v-if="instance.rate.minutes <= 0 || instance.rate.count <= 0" class="disabled">没有限制</span>
<span v-else>{{instance.rate.count}}次/{{instance.rate.minutes}}分钟</span>
</td>
</tr>
<tr>
<td>忽略相似消息周期</td>
<td>
<span v-if="instance.hashLife > 0">{{instance.hashLife}}秒</span>
<span v-else-if="instance.hashLife == 0" class="disabled">使用默认</span>
<span v-else class="disabled">没有设置</span>
</td>
</tr>
<tr>
<td>备注</td>
<td>
<span v-if="instance.description.length > 0">{{instance.description}}</span>
<span v-else class="disabled">没有设置</span>
</td>
</tr>
</table>

View File

@@ -0,0 +1,48 @@
Tea.context(function () {
let scriptEditor = null
this.from = encodeURIComponent(window.location.toString())
if (this.instance.media.type == "script" && this.instance.params.scriptType == "code") {
this.$delay(function () {
this.loadEditor()
})
}
this.loadEditor = function () {
if (scriptEditor == null) {
scriptEditor = CodeMirror(document.getElementById("script-code-editor"), {
theme: "idea",
lineNumbers: false,
value: "",
readOnly: true,
showCursorWhenSelecting: true,
height: "auto",
//scrollbarStyle: null,
viewportMargin: Infinity,
lineWrapping: true,
highlightFormatting: false,
indentUnit: 4,
indentWithTabs: true
})
}
scriptEditor.setValue(this.instance.params.script)
let lang = "shell"
if (this.instance.params.scriptLang != null && this.instance.params.scriptLang.length > 0) {
lang = this.instance.params.scriptLang
}
let mimeType = "text/x-" + lang
if (lang == "nodejs") {
mimeType = "text/javascript"
} else if (lang == "shell") {
mimeType = "text/x-sh"
}
let info = CodeMirror.findModeByMIME(mimeType)
if (info != null) {
scriptEditor.setOption("mode", info.mode)
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js"
CodeMirror.autoLoadMode(scriptEditor, info.mode)
}
}
})

View File

@@ -0,0 +1,14 @@
// codemirror
.CodeMirror {
border: 1px solid #eee;
height: auto!important;
}
.CodeMirror-vscrollbar {
width: 6px;
border-radius: 3px!important;
}
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
border-radius: 2px;
}

View File

@@ -0,0 +1,44 @@
{$layout}
{$template "instance_menu"}
<form class="ui form" data-tea-action="$" data-tea-timeout="120" data-tea-before="submitBefore" data-tea-success="submitSuccess" data-tea-error="submitError" data-tea-fail="submitFail" style="margin-top:1em">
<csrf-token></csrf-token>
<input type="hidden" name="instanceId" :value="instance.id"/>
<table class="ui table definition selectable">
<tr>
<td class="title">媒介名称</td>
<td>{{instance.media.name}}</td>
</tr>
<tr>
<td>通知标题</td>
<td>
<input type="text" name="subject" value="这是通知标题" maxlength="100"/>
</td>
</tr>
<tr>
<td>通知内容</td>
<td>
<textarea name="body" rows="2" maxlength="100">这是通知内容</textarea>
</td>
</tr>
<tr>
<td>接收人标识</td>
<td>
<input type="text" name="user" maxlength="500"/>
<p class="comment" v-html="instance.media.userDescription"></p>
</td>
</tr>
</table>
<div class="ui segment response-box" :class="{green:isOk, red:!isOk}" v-if="isFinished">
<div v-if="response.length > 0"><span v-if="error.length == 0">成功</span>返回结果:
<div v-for="line in responseLines">{{line}}</div>
</div>
<div v-if="error.length > 0">错误信息:
<div v-for="line in errorLines">{{line}}</div>
</div>
<span class="disabled" v-if="response.length == 0 && error.length == 0">成功执行,没有返回结果</span>
</div>
<button class="ui button primary" type="submit" v-if="!isRunning">提交测试</button>
<span v-if="isRunning">发送测试中,请耐心等待...</span>
</form>

View File

@@ -0,0 +1,51 @@
Tea.context(function () {
this.isRunning = false
this.isFinished = false
this.response = ""
this.error = ""
this.isOk = false
this.submitBefore = function () {
this.isRunning = true
this.isFinished = false
this.response = ""
this.error = ""
this.isOk = false
}
this.submitSuccess = function (resp) {
this.updateStatus(resp.data.result)
}
this.submitFail = function (resp) {
this.isRunning = false
this.isFinished = true
this.response = ""
this.error = resp.errors[0].messages[0]
this.errorLines = []
}
this.submitError = function () {
this.isRunning = false
this.isFinished = true
this.response = ""
this.errorLines = []
this.error = "请求超时"
}
this.updateStatus = function (result) {
this.isRunning = false
this.isFinished = true
this.isOk = result.isOk
this.response = result.response
this.responseLines = []
if (this.response != null) {
this.responseLines = this.response.split("\n")
}
this.error = result.error
this.errorLines = []
if (this.error.length > 0) {
this.errorLines = this.error.split("\n")
}
}
})

View File

@@ -0,0 +1,12 @@
.CodeMirror {
border: 1px solid #eee;
height: auto !important;
}
.CodeMirror-vscrollbar {
width: 6px;
border-radius: 3px !important;
}
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
border-radius: 2px;
}
/*# sourceMappingURL=update.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["update.less"],"names":[],"mappings":"AACA;EACC,sBAAA;EACA,uBAAA;;AAGD;EACC,UAAA;EACA,6BAAA;;AAGD,sBAAsB;EACrB,kBAAA","file":"update.css"}

View File

@@ -0,0 +1,414 @@
{$layout}
{$template "instance_menu"}
{$template "/code_editor"}
<form class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="instanceId" :value="instance.id"/>
<table class="ui table definition selectable">
<tr>
<td class="title">媒介名称 *</td>
<td>
<input type="text" name="name" ref="focus" maxlength="100" v-model="instance.name"/>
<p class="comment">给当前媒介填写一个容易识别的名称。</p>
</td>
</tr>
<tr>
<td class="color-border title">媒介类型 *</td>
<td>
<message-media-selector :v-media-type="instance.media.type" @change="changeMediaType"></message-media-selector>
</td>
</tr>
<!-- E-mail -->
<tbody v-show="mediaType == 'email'">
<tr>
<td class="color-border">SMTP *</td>
<td>
<input type="text" name="emailSmtp" v-model="instance.params.smtp" maxlength="500" placeholder="类似于 smtp.example.com:465"/>
<p class="comment">只支持SSL或TLS连接端口通常为465或587。</p>
</td>
</tr>
<tr>
<td class="color-border">账号 *</td>
<td>
<input type="text" name="emailUsername" v-model="instance.params.username" maxlength="500" placeholder="类似于 xxx@example.com" @input="changeEmailUsername"/>
<p class="comment">邮箱账号,比如 123456@qq.com<span v-html="emailUsernameHelp"></span></p>
</td>
</tr>
<tr>
<td class="color-border">密码 *</td>
<td>
<input type="password" name="emailPassword" v-model="instance.params.password" maxlength="100"/>
<p class="comment">账号对应的密码或者授权码比如QQ邮箱就需要授权码</p>
</td>
</tr>
<tr>
<td class="color-border">发送者Email</td>
<td>
<input type="text" name="emailFrom" v-model="instance.params.from" maxlength="500" placeholder="类似于 xxx@example.com"/>
<p class="comment">默认和账号一致</p>
</td>
</tr>
</tbody>
<!-- WebHook -->
<tbody v-show="mediaType == 'webHook'">
<tr>
<td class="color-border">URL *</td>
<td>
<input type="text" name="webHookURL" v-model="instance.params.url" maxlength="500" placeholder="http://..."/>
<p class="comment">可以在URL中使用<code-label>${MessageUser}</code-label><code-label>${MessageSubject}</code-label><code-label>${MessageBody}</code-label>来代表接收人标识、标题和内容。</p>
</td>
</tr>
<tr>
<td class="color-border">请求方法 *</td>
<td>
<select name="webHookMethod" v-model="webHookMethod" class="ui dropdown" style="width:10em">
<option v-for="method in methods" :value="method">{{method}}</option>
</select>
<p class="comment" v-if="webHookMethod == 'POST'">将以POST方式发送<code-label>${MessageUser}</code-label><code-label>${MessageSubject}</code-label><code-label>${MessageBody}</code-label>参数,分别代表接收人标识、标题和内容</p>
</td>
</tr>
<tr v-show="advancedOptionsVisible">
<td class="color-border">自定义报头</td>
<td>
<div class="webHook-headers-box">
<span class="ui label small basic" v-for="(header,index) in webHookHeaders">{{header.name}}:{{header.value}}
<input type="hidden" name="webHookHeaderNames" :value="header.name"/>
<input type="hidden" name="webHookHeaderValues" :value="header.value"/>
<a href="" title="删除" @click.prevent="removeWebHookHeader(index)"><i class="icon remove"></i></a>
</span>
</div>
<div style="margin-top: 1em">
<button class="ui button tiny" type="button" @click.prevent="addWebHookHeader()" v-if="!webHookHeadersAdding">+</button>
</div>
<div v-if="webHookHeadersAdding" style="margin-top: 1em">
<div class="ui fields inline">
<div class="ui field">
<input type="text" name="webHookHeaderName" v-model="webHookHeadersAddingName" size="10" placeholder="名称" maxlength="100" @keyup.enter="confirmWebHookHeadersAdding" @keypress.enter.prevent="1"/>
</div>
<div class="ui field">:</div>
<div class="ui field">
<input type="text" placeholder="值" v-model="webHookHeadersAddingValue" size="20" maxlength="256" @keyup.enter="confirmWebHookHeadersAdding" @keypress.enter.prevent="1"/>
</div>
<div class="ui field">
<button class="ui button tiny" type="button" @click.prevent="confirmWebHookHeadersAdding()">确定</button>
&nbsp; <a href="" @click.prevent="cancelWebHookHeadersAdding()">取消</a>
</div>
</div>
</div>
</td>
</tr>
<tr v-show="webHookMethod == 'POST' && advancedOptionsVisible">
<td class="color-border">自定义内容</td>
<td>
<div class="ui menu tabular small attached">
<a href="" class="item" :class="{active:webHookContentType == 'params'}" @click.prevent="selectWebHookContentType('params')">参数对</a>
<a href="" class="item" :class="{active:webHookContentType == 'body'}" @click.prevent="selectWebHookContentType('body')">文本内容</a>
</div>
<div class="ui segment attached" v-if="webHookContentType == 'params'">
<input type="hidden" name="webHookContentType" value="params"/>
<div class="webHook-headers-box">
<span class="ui label small basic" v-for="(param,index) in webHookParams">{{param.name}}:{{param.value}}
<input type="hidden" name="webHookParamNames" :value="param.name"/>
<input type="hidden" name="webHookParamValues" :value="param.value"/>
<a href="" title="删除" @click.prevent="removeWebHookParam(index)"><i class="icon remove"></i></a>
</span>
</div>
<button class="ui button tiny" type="button" @click.prevent="addWebHookParam()" v-if="!webHookParamsAdding">+</button>
<div v-if="webHookParamsAdding">
<input type="hidden" name="webHookContentType" value="params"/>
<div class="ui fields inline">
<div class="ui field">
<input type="text" name="webHookParamName" v-model="webHookParamsAddingName" placeholder="名称" maxlength="100" @keyup.enter="confirmWebHookParamsAdding" @keypress.enter.prevent="1"/>
</div>
<div class="ui field">:</div>
<div class="ui field">
<textarea type="text" placeholder="值" v-model="webHookParamsAddingValue" cols="50" maxlength="1024" rows="2"></textarea>
</div>
</div>
<button class="ui button tiny" type="button" @click.prevent="confirmWebHookParamsAdding()">确认添加</button>
&nbsp; <a href="" @click.prevent="cancelWebHookParamsAdding()">取消</a>
</div>
</div>
<div class="ui segment attached" v-if="webHookContentType == 'body'">
<input type="hidden" name="webHookContentType" value="body"/>
<textarea name="webHookBody" v-model="webHookBody" rows="5" placeholder="发送的内容"></textarea>
<p class="comment">
内容中可以使用三个变量:<code-label>${MessageUser}</code-label><code-label>${MessageSubject}</code-label><code-label>${MessageBody}</code-label>参数,分别代表接收人标识、标题和内容
</p>
</div>
</td>
</tr>
</tbody>
<!-- Script -->
<tbody v-show="mediaType == 'script'">
<tr>
<td class="color-border">脚本 *</td>
<td>
<input type="hidden" name="scriptType" :value="scriptTab"/>
<input type="hidden" name="scriptLang" :value="scriptLang"/>
<div class="ui tabular menu attached small">
<a class="item" :class="{active:scriptTab == 'path'}" @click.prevent="selectScriptTab('path')">脚本文件</a>
<a class="item" :class="{active:scriptTab == 'code'}" @click.prevent="selectScriptTab('code')">脚本代码</a>
</div>
<div class="ui bottom segment attached" v-show="scriptTab == 'path'">
<input type="text" name="scriptPath" v-model="instance.params.path" maxlength="500"/>
<p class="comment">如果是Shell脚本请不要忘记在头部添加 <em>#!脚本解释工具</em>,比如 <em>#!/bin/bash</em><br/>
执行此脚本时,在脚本中可以使用<code-label>${MessageUser}</code-label><code-label>${MessageSubject}</code-label><code-label>${MessageBody}</code-label>三个环境变量分别代表通知的接收人标识、标题和内容。
</p>
</div>
<div class="ui bottom segment attached" v-show="scriptTab == 'code'" style="padding-top:0">
<div class="ui menu text small">
<a class="item" v-for="lang in scriptLangs" :class="{active:lang.code == scriptLang}" @click.prevent="selectScriptLang(lang.code)">{{lang.name}}</a>
</div>
<textarea name="scriptCode" id="script-code-editor" rows="1"></textarea>
<p class="comment">如果是Shell脚本请不要忘记在头部添加 <em>#!脚本解释工具</em>,比如 <em>#!/bin/bash</em><br/>
执行此脚本时,在脚本中可以使用<code-label>${MessageUser}</code-label><code-label>${MessageSubject}</code-label><code-label>${MessageBody}</code-label>三个环境变量分别代表通知的接收人标识、标题和内容。
</p>
</div>
</td>
</tr>
<tr v-show="advancedOptionsVisible">
<td class="color-border">当前工作目录<em>CWD</em></td>
<td>
<input type="text" name="scriptCwd" v-model="instance.params.cwd" maxlength="500"/>
</td>
</tr>
<tr v-show="advancedOptionsVisible">
<td class="color-border">环境变量<em>ENV</em></td>
<td>
<div class="ui field">
<span class="ui label small basic" v-for="(var1, index) in env">
<input type="hidden" name="scriptEnvNames" :value="var1.name"/>
<input type="hidden" name="scriptEnvValues" :value="var1.value"/>
<em>{{var1.name}}</em>: {{var1.value}}
<a href="" @click.prevent="removeEnv(index)"><i class="icon remove"></i></a>
</span>
</div>
<div v-if="envAdding" class="ui fields inline">
<div class="ui field">
<input type="text" name="envAddingName" v-model="envAddingName" placeholder="变量名" style="width:9em" @keyup.enter="confirmAddEnv" @keypress.enter.prevent="1"/>
</div>
<div class="ui field">
<input type="text" name="envAddingValue" v-model="envAddingValue" placeholder="变量值" style="width:15em" @keyup.enter="confirmAddEnv" @keypress.enter.prevent="1"/>
</div>
<div class="ui field">
<button class="ui button" type="button" @click="confirmAddEnv()">添加</button>
</div>
<div class="ui field">
<a href="" @click.prevent="cancelEnv()"><i class="icon remove"></i></a>
</div>
</div>
<div class="ui field">
<button class="ui button small" type="button" @click="addEnv()">+</button>
</div>
</td>
</tr>
</tbody>
<!-- 钉钉 -->
<tbody v-show="mediaType == 'dingTalk'">
<tr>
<td class="color-border">Hook地址 *</td>
<td>
<textarea name="dingTalkWebHookURL" maxlength="500" placeholder="https://oapi.dingtalk.com/robot/send?access_token=xxx" v-model="instance.params.webHookURL" rows="2"></textarea>
<p class="comment">填入自定义群机器人的Hook地址<a href="https://open.dingtalk.com/document/orgapp/custom-robot-access" target="_blank">获取方法</a></p>
</td>
</tr>
</tbody>
<!-- 企业微信 -->
<tbody v-show="mediaType == 'qyWeixin'">
<tr>
<td class="color-border">企业ID *</td>
<td>
<input type="text" name="qyWeixinCorporateId" maxlength="100" v-model="instance.params.corporateId" />
</td>
</tr>
<tr>
<td class="color-border">应用AgentId *</td>
<td>
<input type="text" name="qyWeixinAgentId" maxlength="100" v-model="instance.params.agentId"/>
</td>
</tr>
<tr>
<td class="color-border">应用Secret *</td>
<td>
<input type="text" name="qyWeixinAppSecret" maxlength="100" v-model="instance.params.appSecret"/>
</td>
</tr>
<tr>
<td class="color-border">内容文本格式</td>
<td>
<select name="qyWeixinTextFormat" v-model="instance.params.textFormat" class="ui dropdown" style="width:10em">
<option value="text">普通文本</option>
<option value="markdown">Markdown</option>
</select>
<p class="comment" v-if="instance.params.textFormat == 'markdown'">企业微信目前只支持少数的Markdown语法<a href="https://work.weixin.qq.com/api/doc#90000/90135/90236/%E6%94%AF%E6%8C%81%E7%9A%84markdown%E8%AF%AD%E6%B3%95" target="_blank">点击这里了解</a></p>
</td>
</tr>
</tbody>
<!-- 企业微信群机器人 -->
<tbody v-show="mediaType == 'qyWeixinRobot'">
<tr>
<td class="color-border">WebHook地址 *</td>
<td>
<textarea name="qyWeixinRobotWebHookURL" v-model="instance.params.webHookURL" maxlength="500" placeholder="https://qyapi.weixin.qq.com/cgi-bin/webHook/send?key=xxx" rows="2"></textarea>
<p class="comment">填入自定义群机器人的WebHook地址<a href="https://open.work.weixin.qq.com/help2/pc/14931" target="_blank">获取方法</a></p>
</td>
</tr>
<tr>
<td class="color-border">内容文本格式</td>
<td>
<select name="qyWeixinRobotTextFormat" v-model="instance.params.textFormat" class="ui dropdown" style="width:10em">
<option value="text">普通文本</option>
<option value="markdown">Markdown</option>
</select>
<p class="comment" v-if="instance.params.textFormat == 'markdown'">企业微信目前只支持少数的Markdown语法<a href="https://work.weixin.qq.com/api/doc#90000/90135/91760/markdown%E7%B1%BB%E5%9E%8B" target="_blank">点击这里了解</a></p>
</td>
</tr>
</tbody>
<!-- 阿里云短信 -->
<tbody v-show="mediaType == 'aliyunSms'">
<tr>
<td class="color-border">签名名称 *</td>
<td>
<input type="text" name="aliyunSmsSign" maxlength="100" v-model="instance.params.sign"/>
<p class="comment">已经审核通过的短信签名名称</p>
</td>
</tr>
<tr>
<td class="color-border">模板CODE *</td>
<td>
<input type="text" name="aliyunSmsTemplateCode" maxlength="100" v-model="instance.params.templateCode" placeholder="类似于SMS_12345"/>
<p class="comment">已经审核通过的模板CODE</p>
</td>
</tr>
<tr>
<td class="color-border">模板变量 *</td>
<td>
<div class="ui field">
<span class="ui label small basic" v-for="(var1, index) in aliyunSmsTemplateVars">
<input type="hidden" name="aliyunSmsTemplateVarNames" :value="var1.name"/>
<input type="hidden" name="aliyunSmsTemplateVarValues" :value="var1.value"/>
<em>{{var1.name}}</em>: {{var1.value}}
<a href="" @click.prevent="removeAliyunSmsTemplateVar(index)"><i class="icon remove"></i></a>
</span>
</div>
<div v-if="aliyunSmsTemplateVarAdding" class="ui fields inline">
<div class="ui field">
<input type="text" name="aliyunSmsTemplateVarAddingName" v-model="aliyunSmsTemplateVarAddingName" placeholder="变量名" style="width:9em" @keyup.enter="confirmAddAliyunSmsTemplateVar" @keypress.enter.prevent="1"/>
</div>
<div class="ui field">
<input type="text" name="aliyunSmsTemplateVarAddingValue" v-model="aliyunSmsTemplateVarAddingValue" placeholder="变量值" style="width:15em" @keyup.enter="confirmAddAliyunSmsTemplateVar" @keypress.enter.prevent="1"/>
</div>
<div class="ui field">
<button class="ui button" type="button" @click="confirmAddAliyunSmsTemplateVar()">添加</button>
</div>
<div class="ui field">
<a href="" @click.prevent="cancelAliyunSmsTemplateVar()"><i class="icon remove"></i></a>
</div>
</div>
<div class="ui field">
<button class="ui button small" type="button" @click="addAliyunSmsTemplateVar()">+</button>
</div>
<p class="comment">模板中使用的变量,在变量中可以使用<code-label>${MessageUser}</code-label><code-label>${MessageSubject}</code-label><code-label>${MessageBody}</code-label>来代表接收人标识、标题和内容。</p>
</td>
</tr>
<tr>
<td class="color-border">AccessKey ID *</td>
<td>
<input type="text" name="aliyunSmsAccessKeyId" maxlength="100" v-model="instance.params.accessKeyId"/>
<p class="comment">在阿里云控制台中的访问控制中某个用户的AccessKey ID</p>
</td>
</tr>
<tr>
<td class="color-border">AccessKey Secret *</td>
<td>
<input type="text" name="aliyunSmsAccessKeySecret" maxlength="100" v-model="instance.params.accessKeySecret"/>
<p class="comment">在阿里云控制台中的访问控制中某个用户的AccessKey Secret和上面的AccessKey ID匹配</p>
</td>
</tr>
</tbody>
<!-- Telegram -->
<tbody v-if="mediaType == 'telegram'">
<tr>
<td class="color-border">机器人Token</td>
<td>
<input type="text" name="telegramToken" v-model="instance.params.token"/>
<p class="comment">在创建机器人的时候可以获得,类似于 <code-label>123456:AAAA-AAAAAAAAAAAAAAAAAAAA</code-label>,可以向 <code-label>@BotFather</code-label> 发送 <code-label>/newbot</code-label>指令创建新的机器人。</p>
</td>
</tr>
<tr>
<td class="color-border">代理服务</td>
<td>
<div class="ui fields inline">
<div class="ui field">
<select class="ui dropdown" name="telegramProxyScheme" v-model="telegramProxyScheme">
<option value="socks5">socks5://</option>
<option value="https">https://</option>
<option value="http">http://</option>
</select>
</div>
<div class="ui field">
<input type="text" name="telegramProxyHost" v-model="telegramProxyHost" placeholder="HOST:PORT"/>
</div>
</div>
<p class="comment">可选项仅当API节点所在服务器无法直接连接Telegram API时填写测试是否能够连接Telegram API的方法<code-label>curl https://api.telegram.org</code-label>出现2XX、3XX、4XX之类提示时说明能够直接连接如果你设置了代理请使用<code-label>curl -x "你的代理服务地址" https://api.telegram.org</code-label>来测试你的代理是否可用。</p>
</td>
</tr>
</tbody>
<tr>
<td>发送频率<em>(次/分钟)</em></td>
<td>
<div class="ui fields inline">
<div class="ui field">
<div class="ui right labeled input">
<input type="text" name="rateCount" size="4" v-model="instance.rate.count" maxlength="4"/>
<span class="ui label"></span>
</div>
</div>
<div class="ui field">/</div>
<div class="ui field">
<div class="ui right labeled input">
<input type="text" name="rateMinutes" size="4" v-model="instance.rate.minutes" maxlength="4"/>
<span class="ui label">分钟</span>
</div>
</div>
</div>
<p class="comment">用来限制此媒介的发送频率。</p>
</td>
</tr>
<tr>
<td>忽略相似消息周期</td>
<td>
<div class="ui input right labeled">
<input type="text" name="hashLife" style="width: 5em" maxlength="4" v-model="instance.hashLife"/>
<span class="ui label"></span>
</div>
<p class="comment">可以在这个时间内忽略相似消息防止短时间内消息数量过载。不填或者为0表示默认为60秒。</p>
</td>
</tr>
<tr>
<td>备注</td>
<td>
<textarea rows="3" name="description" maxlength="100" v-model="instance.description"></textarea>
</td>
</tr>
<tr>
<td>启用当前媒介</td>
<td>
<checkbox name="isOn" value="1" v-model="instance.isOn"></checkbox>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>

View File

@@ -0,0 +1,460 @@
Tea.context(function () {
let scriptEditor = null
let isLoaded = false;
this.$delay(function () {
isLoaded = true;
if (this.instance.media.type == "email") {
this.changeEmailUsername()
}
})
this.success = function () {
let that = this
teaweb.success("保存成功", function () {
window.location = "/admins/recipients/instances/instance?instanceId=" + that.instance.id
})
};
/**
* 名称
*/
this.rateNoticeVisible = false
this.changeName = function (name) {
if (name.indexOf("短信") > -1 || name.indexOf("钉钉") > -1 || name.indexOf("微信") > -1) {
this.rateNoticeVisible = true
} else {
this.rateNoticeVisible = false
}
};
this.changeName(this.instance.media.name)
/**
* 类型
*/
this.mediaType = this.instance.media.type;
this.changeMediaType = function (media) {
this.mediaType = media.type
if (!isLoaded) {
return;
}
if (this.mediaType == "email") {
this.$delay(function () {
this.$find("form input[name='emailSmtp']").focus();
});
} else if (this.mediaType == "webHook") {
this.$delay(function () {
this.$find("form input[name='webHookURL']").focus();
});
} else if (this.mediaType == "script") {
this.$delay(function () {
this.$find("form input[name='scriptPath']").focus();
});
} else if (this.mediaType == "dingTalk") {
this.$delay(function () {
this.$find("form textarea[name='dingTalkWebHookURL']").focus();
});
} else if (this.mediaType == "qyWeixin") {
this.$delay(function () {
this.$find("form input[name='qyWeixinCorporateId']").focus();
});
} else if (this.mediaType == "qyWeixinRobot") {
this.$delay(function () {
this.$find("form textarea[name='qyWeixinRobotWebHookURL']").focus();
});
}
};
this.changeMediaType(this.instance.media);
/**
* 邮箱
*/
this.emailUsernameHelp = "";
this.changeEmailUsername = function () {
this.emailUsernameHelp = "";
if (this.instance.params.username.indexOf("qq.com") > 0) {
this.emailUsernameHelp = "<a href=\"https://service.mail.qq.com\" target='_blank'>QQ邮箱相关设置帮助</a>";
} else if (this.instance.params.username.indexOf("163.com") > 0) {
this.emailUsernameHelp = "<a href=\"https://help.mail.163.com/faqDetail.do?code=d7a5dc8471cd0c0e8b4b8f4f8e49998b374173cfe9171305fa1ce630d7f67ac22dc0e9af8168582a\" target='_blank'>网易邮箱相关设置帮助</a>";
}
};
/**
* webHook
*/
this.methods = ["GET", "POST"]
this.webHookMethod = "GET";
this.webHookHeadersAdding = false;
this.webHookHeaders = [];
this.webHookHeadersAddingName = "";
this.webHookHeadersAddingValue = "";
this.addWebHookHeader = function () {
this.webHookHeadersAdding = true;
this.$delay(function () {
this.$find("form input[name='webHookHeaderName']").focus();
});
};
this.cancelWebHookHeadersAdding = function () {
this.webHookHeadersAdding = false;
};
this.confirmWebHookHeadersAdding = function () {
this.webHookHeaders.push({
"name": this.webHookHeadersAddingName,
"value": this.webHookHeadersAddingValue
});
this.webHookHeadersAddingName = "";
this.webHookHeadersAddingValue = "";
this.webHookHeadersAdding = false;
};
this.removeWebHookHeader = function (index) {
if (!window.confirm("确定要删除此Header吗")) {
return;
}
this.webHookHeaders.$remove(index);
};
this.webHookContentType = "params";
this.selectWebHookContentType = function (contentType) {
this.webHookContentType = contentType;
this.$delay(function () {
if (contentType == "params") {
} else if (contentType == "body") {
this.$find("form textarea[name='webHookBody']").focus();
}
});
};
this.webHookParamsAdding = false;
this.webHookParams = [];
this.webHookParamsAddingName = "";
this.webHookParamsAddingValue = "";
this.addWebHookParam = function () {
this.webHookParamsAdding = true;
this.$delay(function () {
this.$find("form input[name='webHookParamName']").focus();
});
};
this.cancelWebHookParamsAdding = function () {
this.webHookParamsAdding = false;
};
this.confirmWebHookParamsAdding = function () {
this.webHookParams.push({
"name": this.webHookParamsAddingName,
"value": this.webHookParamsAddingValue
});
this.webHookParamsAddingName = "";
this.webHookParamsAddingValue = "";
this.webHookParamsAdding = false;
};
this.removeWebHookParam = function (index) {
if (!window.confirm("确定要删除此参数吗?")) {
return;
}
this.webHookParams.$remove(index);
};
this.webHookBody = "";
if (this.instance.media.type == "webHook") {
this.webHookMethod = this.instance.params.method;
if (this.instance.params.headers != null) {
this.webHookHeaders = this.instance.params.headers;
}
if (this.instance.params.contentType == "params") {
this.webHookContentType = "params";
if (this.instance.params.params != null) {
this.webHookParams = this.instance.params.params;
}
}
if (this.instance.params.contentType == "body") {
this.webHookContentType = "body";
this.webHookBody = this.instance.params.body;
}
}
/**
* 脚本
*/
this.scriptTab = "path";
this.scriptLang = "shell";
if (this.instance.media.type == "script") {
if (this.instance.params.scriptType == "path") {
this.scriptTab = "path";
} else {
this.scriptTab = "code";
this.scriptLang = this.instance.params.scriptLang;
this.$delay(function () {
this.loadEditor();
});
}
}
this.scriptLangs = [
{
"name": "Shell",
"code": "shell"
},
{
"name": "批处理(bat)",
"code": "bat"
},
{
"name": "PHP",
"code": "php"
},
{
"name": "Python",
"code": "python"
},
{
"name": "Ruby",
"code": "ruby"
},
{
"name": "NodeJS",
"code": "nodejs"
}
]
this.selectScriptTab = function (tab) {
this.scriptTab = tab
if (tab == "path") {
this.$delay(function () {
this.$find("form input[name='scriptPath']").focus()
})
} else if (tab == "code") {
this.$delay(function () {
this.loadEditor()
})
}
};
this.selectScriptLang = function (lang) {
this.scriptLang = lang;
switch (lang) {
case "shell":
if (this.instance.media.type == "script" && this.instance.params.scriptType == "code" && this.instance.params.scriptLang == "shell") {
scriptEditor.setValue(this.instance.params.script);
} else {
scriptEditor.setValue("#!/usr/bin/env bash\n\n# your commands here\n");
}
var info = CodeMirror.findModeByMIME("text/x-sh");
if (info != null) {
scriptEditor.setOption("mode", info.mode);
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js";
CodeMirror.autoLoadMode(scriptEditor, info.mode);
}
break;
case "bat":
if (this.instance.media.type == "script" && this.instance.params.scriptType == "code" && this.instance.params.scriptLang == "bat") {
scriptEditor.setValue(this.instance.params.script);
} else {
scriptEditor.setValue("");
}
break;
case "php":
if (this.instance.media.type == "script" && this.instance.params.scriptType == "code" && this.instance.params.scriptLang == "php") {
scriptEditor.setValue(this.instance.params.script);
} else {
scriptEditor.setValue("#!/usr/bin/env php\n\n<?php\n// your PHP codes here");
}
var info = CodeMirror.findModeByMIME("text/x-php");
if (info != null) {
scriptEditor.setOption("mode", info.mode);
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js";
CodeMirror.autoLoadMode(scriptEditor, info.mode);
}
break;
case "python":
if (this.instance.media.type == "script" && this.instance.params.scriptType == "code" && this.instance.params.scriptLang == "python") {
scriptEditor.setValue(this.instance.params.script);
} else {
scriptEditor.setValue("#!/usr/bin/env python\n\n''' your Python codes here '''");
}
var info = CodeMirror.findModeByMIME("text/x-python");
if (info != null) {
scriptEditor.setOption("mode", info.mode);
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js";
CodeMirror.autoLoadMode(scriptEditor, info.mode);
}
break;
case "ruby":
if (this.instance.media.type == "script" && this.instance.params.scriptType == "code" && this.instance.params.scriptLang == "ruby") {
scriptEditor.setValue(this.instance.params.script);
} else {
scriptEditor.setValue("#!/usr/bin/env ruby\n\n# your Ruby codes here");
}
var info = CodeMirror.findModeByMIME("text/x-ruby");
if (info != null) {
scriptEditor.setOption("mode", info.mode);
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js";
CodeMirror.autoLoadMode(scriptEditor, info.mode);
}
break;
case "nodejs":
if (this.instance.media.type == "script" && this.instance.params.scriptType == "code" && this.instance.params.scriptLang == "nodejs") {
scriptEditor.setValue(this.instance.params.script);
} else {
scriptEditor.setValue("#!/usr/bin/env node\n\n// your javascript codes here");
}
var info = CodeMirror.findModeByMIME("text/javascript");
if (info != null) {
scriptEditor.setOption("mode", info.mode);
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js";
CodeMirror.autoLoadMode(scriptEditor, info.mode);
}
break;
}
scriptEditor.save();
scriptEditor.focus();
};
this.loadEditor = function () {
if (scriptEditor == null) {
scriptEditor = CodeMirror.fromTextArea(document.getElementById("script-code-editor"), {
theme: "idea",
lineNumbers: true,
value: "",
readOnly: false,
showCursorWhenSelecting: true,
height: "auto",
//scrollbarStyle: null,
viewportMargin: Infinity,
lineWrapping: true,
highlightFormatting: false,
indentUnit: 4,
indentWithTabs: true
});
}
if (this.instance.params.script != null && this.instance.params.script.length > 0) {
scriptEditor.setValue(this.instance.params.script);
} else {
scriptEditor.setValue("#!/usr/bin/env bash\n\n# your commands here\n");
}
scriptEditor.save();
scriptEditor.focus();
var info = CodeMirror.findModeByMIME("text/x-sh");
if (info != null) {
scriptEditor.setOption("mode", info.mode);
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js";
CodeMirror.autoLoadMode(scriptEditor, info.mode);
}
scriptEditor.on("change", function () {
scriptEditor.save();
});
};
/**
* 环境变量
*/
this.env = [];
if (this.instance.media.type == "script" && this.instance.params.env != null) {
this.env = this.instance.params.env;
}
this.envAdding = false;
this.envAddingName = "";
this.envAddingValue = "";
this.addEnv = function () {
this.envAdding = !this.envAdding;
this.$delay(function () {
this.$find("form input[name='envAddingName']").focus();
});
};
this.confirmAddEnv = function () {
if (this.envAddingName.length == 0) {
alert("请输入变量名");
this.$find("form input[name='envAddingName']").focus();
return;
}
this.env.push({
"name": this.envAddingName,
"value": this.envAddingValue
});
this.envAdding = false;
this.envAddingName = "";
this.envAddingValue = "";
};
this.removeEnv = function (index) {
this.env.$remove(index);
};
this.cancelEnv = function () {
this.envAdding = false;
};
/**
* 阿里云短信模板
*/
this.aliyunSmsTemplateVars = [];
if (this.instance.params.variables != null) {
this.aliyunSmsTemplateVars = this.instance.params.variables;
}
this.aliyunSmsTemplateVarAdding = false;
this.aliyunSmsTemplateVarAddingName = "";
this.aliyunSmsTemplateVarAddingValue = "";
this.addAliyunSmsTemplateVar = function () {
this.aliyunSmsTemplateVarAdding = !this.aliyunSmsTemplateVarAdding;
this.$delay(function () {
this.$find("form input[name='aliyunSmsTemplateVarAddingName']").focus();
});
};
this.confirmAddAliyunSmsTemplateVar = function () {
if (this.aliyunSmsTemplateVarAddingName.length == 0) {
alert("请输入变量名");
this.$find("form input[name='aliyunSmsTemplateVarAddingName']").focus();
return;
}
this.aliyunSmsTemplateVars.push({
"name": this.aliyunSmsTemplateVarAddingName,
"value": this.aliyunSmsTemplateVarAddingValue
});
this.aliyunSmsTemplateVarAdding = false;
this.aliyunSmsTemplateVarAddingName = "";
this.aliyunSmsTemplateVarAddingValue = "";
};
this.removeAliyunSmsTemplateVar = function (index) {
this.aliyunSmsTemplateVars.$remove(index);
};
this.cancelAliyunSmsTemplateVar = function () {
this.aliyunSmsTemplateVarAdding = false;
};
/**
* 更多选项
*/
this.advancedOptionsVisible = true;
this.showAdvancedOptions = function () {
this.advancedOptionsVisible = !this.advancedOptionsVisible;
};
});

View File

@@ -0,0 +1,14 @@
// code mirror
.CodeMirror {
border: 1px solid #eee;
height: auto !important;
}
.CodeMirror-vscrollbar {
width: 6px;
border-radius: 3px !important;
}
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
border-radius: 2px;
}

View File

@@ -0,0 +1,38 @@
{$layout}
{$template "../menu"}
<p class="comment" v-if="logs.length == 0">暂时还没有发送记录。</p>
<div v-if="logs.length > 0">
<div class="margin"></div>
<table class="ui table selectable definition" v-for="log in logs" :class="{red: !log.isOk, green: log.isOk}">
<tr>
<td class="title">简介</td>
<td>接收人:{{log.task.user}} <span class="disabled">&nbsp; | &nbsp;</span> 媒介:{{log.task.instance.name}}<link-icon :href="'/admins/recipients/instances/instance?instanceId=' + log.task.instance.id"></link-icon>
<span class="disabled">&nbsp; | &nbsp;</span> 时间:{{log.createdTime}} &nbsp;|&nbsp; <span class="ui green basic label tiny" v-if="log.isOk">成功</span><span class="ui red basic label tiny" v-if="!log.isOk">失败</span>
</td>
</tr>
<tr v-if="log.task.subject.length > 0">
<td>标题</td>
<td>{{log.task.subject}}</td>
</tr>
<tr v-if="log.task.body.length > 0">
<td>内容</td>
<td>{{log.task.body}}</td>
</tr>
<tr v-if="log.error.length > 0" class="error">
<td>错误信息</td>
<td>{{log.error}}</td>
</tr>
<tr v-if="log.response.length > 0">
<td>响应信息</td>
<td>{{log.response}}</td>
</tr>
<tr v-if="log.task != null && log.task.id > 0">
<td>操作</td>
<td><a href="" @click.prevent="resendTask(log.task.id)">重新发送</a> </td>
</tr>
</table>
</div>
<div class="page" v-html="page"></div>

View File

@@ -0,0 +1,17 @@
Tea.context(function () {
this.resendTask = function (taskId) {
let that = this
teaweb.confirm("确定要重新发送此消息吗?", function () {
that.$post(".updateTaskStatus")
.params({
taskId: taskId,
status: 0
})
.success(function () {
teaweb.success("已成功重新放入发送队列", function () {
teaweb.reload()
})
})
})
}
})

View File

@@ -0,0 +1,80 @@
{$layout}
{$template "recipient_menu"}
{$template "/code_editor"}
<table class="ui table definition selectable">
<tr>
<td class="title">状态</td>
<td>
<label-on :v-is-on="recipient.isOn"></label-on>
</td>
</tr>
<tr>
<td class="title">系统用户</td>
<td>
{{recipient.admin.fullname}} <span class="small grey">{{recipient.admin.username}}</span> <link-icon :href="'/admins/admin?adminId=' + recipient.admin.id"></link-icon>
</td>
</tr>
<tr>
<td>媒介</td>
<td>
{{recipient.instance.name}} <link-icon v-if="recipient.instance.id > 0" :href="'/admins/recipients/instances/instance?instanceId=' + recipient.instance.id"></link-icon>
<p class="comment">{{recipient.instance.description}}</p>
</td>
</tr>
<tr>
<td>接收人标识</td>
<td>
<span v-if="recipient.user.length > 0">{{recipient.user}}</span>
<span v-else class="disabled">-</span>
</td>
</tr>
<tr>
<td>所属分组</td>
<td>
<div v-if="recipient.groups != null && recipient.groups.length > 0">
<div v-for="group in recipient.groups" class="ui label small basic">{{group.name}}</div>
</div>
<div v-else>
<span class="disabled">-</span>
</div>
</td>
</tr>
<tr>
<td>发送时间</td>
<td>
<span v-if="recipient.timeFrom.length > 0 && recipient.timeTo.length > 0">{{recipient.timeFrom}} - {{recipient.timeTo}}</span>
<span v-else class="disabled">-</span>
</td>
</tr>
<tr>
<td>备注</td>
<td>
<span v-if="recipient.description.length > 0">{{recipient.description}}</span>
<span v-else class="disabled">-</span>
</td>
</tr>
</table>
<h4>关联的CDN集群</h4>
<div v-if="nodeClusters.length == 0">
<p class="comment">暂时还没有关联的CDN集群。</p>
</div>
<div v-if="nodeClusters.length > 0">
<table class="ui table selectable celled" style="width: 20em">
<thead>
<tr>
<th>集群名称</th>
<th class="one op">操作</th>
</tr>
</thead>
<tr v-for="cluster in nodeClusters">
<td>
<a :href="'/clusters/cluster/settings/message?clusterId=' + cluster.id">{{cluster.name}}</a>
</td>
<td>
<a href="" @click.prevent="deleteReceiver(cluster.receiverId)">取消</a>
</td>
</tr>
</table>
</div>

View File

@@ -0,0 +1,14 @@
Tea.context(function () {
this.deleteReceiver = function (receiverId) {
let that = this
teaweb.confirm("确定要取消当前接收人和集群的关联吗?", function () {
that.$post("/admins/recipients/receivers/delete")
.params({
receiverId: receiverId
})
.success(function () {
teaweb.reload()
})
})
}
})

View File

@@ -0,0 +1,9 @@
td {
position: relative;
}
td a.op {
position: absolute;
right: 1em;
top: 0.5em;
}
/*# sourceMappingURL=index.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["index.less"],"names":[],"mappings":"AAAA;EACC,kBAAA;;AAGD,EAAG,EAAC;EACH,kBAAA;EACA,UAAA;EACA,UAAA","file":"index.css"}

View File

@@ -0,0 +1,39 @@
{$layout}
{$template "../menu"}
<second-menu>
<menu-item href=".?status=0" :active="status == 0">等待发送({{countWaiting}})</menu-item>
<menu-item href=".?status=3" :active="status == 3">发送错误({{countFailed}})</menu-item>
</second-menu>
<p class="comment" v-if="tasks.length == 0">暂时还没有发送任务。</p>
<div v-if="tasks.length > 0">
<div class="margin"></div>
<table class="ui table selectable definition" v-for="task in tasks">
<tr>
<td class="title">简介</td>
<td>
{{task.user}} <span class="disabled">&nbsp; | &nbsp;</span> <span v-if="task.instance != null">媒介:{{task.instance.name}}<link-icon :href="'/admins/recipients/instances/instance?instanceId=' + task.instance.id"></link-icon></span>
<span class="disabled">&nbsp; | &nbsp;</span> 时间:{{task.createdTime}}
<a href="" title="删除" class="op" @click.prevent="deleteTask(task.id)"><i class="icon remove small"></i></a>
</td>
</tr>
<tr v-if="task.subject.length > 0">
<td>标题</td>
<td>{{task.subject}}</td>
</tr>
<tr v-if="task.body.length > 0">
<td>内容</td>
<td>{{task.body}}</td>
</tr>
<tr v-if="task.status == 3 && task.result != null && !task.result.isOk" class="error">
<td>错误信息</td>
<td>{{task.result.error}}</td>
</tr>
</table>
</div>
<div class="page" v-html="page"></div>

View File

@@ -0,0 +1,16 @@
Tea.context(function () {
this.deleteTask = function (taskId) {
let that = this
teaweb.confirm("确定要删除这个发送任务吗?", function () {
that.$post(".delete")
.params({
taskId: taskId
})
.success(function () {
teaweb.successToast("删除成功", null, function () {
teaweb.reload()
})
})
})
}
})

View File

@@ -0,0 +1,9 @@
td {
position: relative;
}
td a.op {
position: absolute;
right: 1em;
top: 0.5em;
}

View File

@@ -0,0 +1,46 @@
{$layout}
{$template "recipient_menu"}
{$template "/code_editor"}
<form class="ui form" data-tea-action="$" data-tea-timeout="120" data-tea-before="submitBefore" data-tea-success="submitSuccess" data-tea-error="submitError" data-tea-fail="submitFail" style="margin-top:1em">
<csrf-token></csrf-token>
<input type="hidden" name="instanceId" :value="instance.id"/>
<table class="ui table definition selectable">
<tr>
<td class="title">媒介名称</td>
<td>{{instance.media.name}}</td>
</tr>
<tr>
<td>通知标题</td>
<td>
<input type="text" name="subject" value="这是通知标题" maxlength="100"/>
</td>
</tr>
<tr>
<td>通知内容</td>
<td>
<textarea name="body" rows="2" maxlength="100">这是通知内容</textarea>
</td>
</tr>
<tr>
<td>接收人标识</td>
<td>
<input type="text" name="user" maxlength="500" v-model="recipient.user"/>
<p class="comment" v-html="instance.media.userDescription"></p>
</td>
</tr>
</table>
<div class="ui segment response-box" :class="{green:isOk, red:!isOk}" v-if="isFinished">
<div v-if="response.length > 0"><span v-if="error.length == 0">成功</span>返回结果:
<div v-for="line in responseLines">{{line}}</div>
</div>
<div v-if="error.length > 0">错误信息:
<div v-for="line in errorLines">{{line}}</div>
</div>
<span class="disabled" v-if="response.length == 0 && error.length == 0">成功执行,没有返回结果</span>
</div>
<button class="ui button primary" type="submit" v-if="!isRunning">提交测试</button>
<span v-if="isRunning">发送测试中,请耐心等待...</span>
</form>

View File

@@ -0,0 +1,51 @@
Tea.context(function () {
this.isRunning = false
this.isFinished = false
this.response = ""
this.error = ""
this.isOk = false
this.submitBefore = function () {
this.isRunning = true
this.isFinished = false
this.response = ""
this.error = ""
this.isOk = false
}
this.submitSuccess = function (resp) {
this.updateStatus(resp.data.result)
}
this.submitFail = function (resp) {
this.isRunning = false
this.isFinished = true
this.response = ""
this.error = resp.errors[0].messages[0]
this.errorLines = []
}
this.submitError = function () {
this.isRunning = false
this.isFinished = true
this.response = ""
this.errorLines = []
this.error = "请求超时"
}
this.updateStatus = function (result) {
this.isRunning = false
this.isFinished = true
this.isOk = result.isOk
this.response = result.response
this.responseLines = []
if (this.response != null) {
this.responseLines = this.response.split("\n")
}
this.error = result.error
this.errorLines = []
if (this.error.length > 0) {
this.errorLines = this.error.split("\n")
}
}
})

View File

@@ -0,0 +1,8 @@
.clusters-box .checkbox {
margin-bottom: 1em;
float: left;
width: 23%;
height: 1.2em;
overflow: hidden;
}
/*# sourceMappingURL=update.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["update.less"],"names":[],"mappings":"AAAA,aACC;EACC,kBAAA;EACA,WAAA;EACA,UAAA;EACA,aAAA;EACA,gBAAA","file":"update.css"}

View File

@@ -0,0 +1,119 @@
{$layout}
{$template "recipient_menu"}
{$template "/code_editor"}
<form class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="recipientId" :value="recipient.id"/>
<table class="ui table definition selectable">
<tr>
<td class="title">系统用户 *</td>
<td>
<admin-selector :v-admin-id="recipient.admin.id"></admin-selector>
<p class="comment">选择关联的系统用户。</p>
</td>
</tr>
<tr>
<td>媒介 *</td>
<td>
<message-media-instance-selector :v-instance-id="recipient.instance.id" @change="changeInstance"></message-media-instance-selector>
</td>
</tr>
<tr>
<td>接收人标识</td>
<td>
<input type="text" name="user" maxlength="300" v-model="recipient.user"/>
<p class="comment">{{userDescription}}</p>
</td>
</tr>
<tr v-show="nodeClusters.length > 0">
<td>关联的边缘节点集群</td>
<td>
<div class="clusters-box">
<checkbox v-for="nodeCluster in nodeClusters" name="nodeClusterIds" :v-value="nodeCluster.id" :checked="nodeCluster.isChecked ? 'checked': ''">{{nodeCluster.name}}</checkbox>
</div>
</td>
</tr>
<tr>
<td>分组</td>
<td>
<message-recipient-group-selector :v-groups="recipient.groups"></message-recipient-group-selector>
<p class="comment">选择当前接收人所属分组。</p>
</td>
</tr>
<tr>
<td colspan="2"><more-options-indicator></more-options-indicator></td>
</tr>
<tbody v-show="moreOptionsVisible">
<tr>
<td>发送时间</td>
<td>
<div class="ui fields inline">
<div class="ui field">
开始时间:
</div>
<div class="ui field">
<div class="ui input right labeled">
<input type="text" name="timeFromHour" size="2" maxlength="2" placeholder="" v-model="recipient.timeFromHour"/>
<span class="ui label"></span>
</div>
</div>
<div class="ui field">
<div class="ui input right labeled">
<input type="text" name="timeFromMinute" size="2" maxlength="2" placeholder="" v-model="recipient.timeFromMinute"/>
<span class="ui label"></span>
</div>
</div>
<div class="ui field">
<div class="ui input right labeled">
<input type="text" name="timeFromSecond" size="2" maxlength="2" placeholder="" v-model="recipient.timeFromSecond"/>
<span class="ui label"></span>
</div>
</div>
</div>
<div class="ui divider"></div>
<div class="ui fields inline">
<div class="ui field">
结束时间:
</div>
<div class="ui field">
<div class="ui input right labeled">
<input type="text" name="timeToHour" size="2" maxlength="2" placeholder="" v-model="recipient.timeToHour"/>
<span class="ui label"></span>
</div>
</div>
<div class="ui field">
<div class="ui input right labeled">
<input type="text" name="timeToMinute" size="2" maxlength="2" placeholder="" v-model="recipient.timeToMinute"/>
<span class="ui label"></span>
</div>
</div>
<div class="ui field">
<div class="ui input right labeled">
<input type="text" name="timeToSecond" size="2" maxlength="2" placeholder="" v-model="recipient.timeToSecond"/>
<span class="ui label"></span>
</div>
</div>
</div>
<p class="comment">24小时制即小时从0到23。如果填写了发送时间系统只会在这个时间段内给当前接收人发送消息。
<a href="" v-if="recipient.timeFromHour.length > 0 || recipient.timeFromMinute.length > 0 || recipient.timeFromSecond.length > 0 || recipient.timeToHour.length > 0 || recipient.timeToMinute.length > 0 || recipient.timeToSecond.length > 0" @click.prevent="clearTime">[清除]</a> </p>
</td>
</tr>
<tr>
<td>备注</td>
<td>
<textarea rows="3" name="description" maxlength="100" v-model="recipient.description"></textarea>
</td>
</tr>
<tr>
<td>启用当前接收人</td>
<td>
<checkbox name="isOn" value="1" v-model="recipient.isOn"></checkbox>
</td>
</tr>
</tbody>
</table>
<submit-btn></submit-btn>
</form>

View File

@@ -0,0 +1,29 @@
Tea.context(function () {
this.userDescription = ""
this.changeInstance = function (instance) {
if (instance != null) {
this.userDescription = instance.media.userDescription
} else {
this.userDescription = ""
}
}
this.success = function () {
let that = this
teaweb.success("保存成功", function () {
window.location = Tea.url(".recipient", {
recipientId: that.recipient.id
})
})
}
this.clearTime = function () {
this.recipient.timeFromHour = ""
this.recipient.timeFromMinute = ""
this.recipient.timeFromSecond = ""
this.recipient.timeToHour = ""
this.recipient.timeToMinute = ""
this.recipient.timeToSecond = ""
}
})

View File

@@ -0,0 +1,9 @@
.clusters-box {
.checkbox {
margin-bottom: 1em;
float: left;
width: 23%;
height: 1.2em;
overflow: hidden;
}
}

View File

@@ -0,0 +1,7 @@
.modules-box .module-box {
float: left;
width: 10em;
margin-top: 0.3em;
margin-bottom: 0.3em;
}
/*# sourceMappingURL=update.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["update.less"],"names":[],"mappings":"AAAA,YACC;EACC,WAAA;EACA,WAAA;EACA,iBAAA;EACA,oBAAA","file":"update.css"}

View File

@@ -0,0 +1,75 @@
{$layout}
{$template "admin_menu"}
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="adminId" :value="admin.id"/>
<table class="ui table definition selectable">
<tr>
<td class="title">全名 *</td>
<td>
<input type="text" name="fullname" maxlength="100" ref="focus" v-model="admin.fullname"/>
<p class="comment">可以输入姓名、公司名等容易识别的名称。</p>
</td>
</tr>
<tr>
<td>登录用户名 *</td>
<td>
<input type="text" name="username" maxlength="100" v-model="admin.username"/>
<p class="comment">用户名只能英文、数字、下划线的组合。</p>
</td>
</tr>
<tr>
<td>登录密码</td>
<td>
<input type="password" name="pass1" maxlength="100"/>
<p class="comment">留空表示不修改。</p>
</td>
</tr>
<tr>
<td>确认登录密码</td>
<td>
<input type="password" name="pass2" maxlength="100"/>
</td>
</tr>
<tr>
<td>允许登录</td>
<td>
<checkbox name="canLogin" value="1" v-model="admin.canLogin"></checkbox>
<p class="comment">选中后,当前管理员才可以登录当前的管理平台。</p>
</td>
</tr>
<tr>
<td>超级管理员</td>
<td>
<checkbox name="isSuper" v-model="admin.isSuper"></checkbox>
<p class="comment">选中后,表示当前管理员为超级管理员;超级管理员自动拥有所有的管理权限。</p>
</td>
</tr>
<tr v-show="!admin.isSuper">
<td>权限</td>
<td>
<div class="modules-box">
<div class="module-box" v-for="module in modules">
<checkbox name="moduleCodes" :v-value="module.code" v-model="module.isChecked">{{module.name}}</checkbox>
</div>
</div>
</td>
</tr>
<tr>
<td>OTP认证</td>
<td>
<checkbox name="otpOn" v-model="admin.otpLoginIsOn">启用OTP</checkbox>
<p class="comment">启用OTP认证后在用户登录的时候需要同时填写OTP动态密码。</p>
</td>
</tr>
<tr>
<td>启用当前管理员</td>
<td>
<checkbox name="isOn" v-model="admin.isOn"></checkbox>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>

View File

@@ -0,0 +1,3 @@
Tea.context(function () {
this.success = NotifySuccess("保存成功", "/admins/admin?adminId=" + this.admin.id)
})

View File

@@ -0,0 +1,8 @@
.modules-box {
.module-box {
float: left;
width: 10em;
margin-top: 0.3em;
margin-bottom: 0.3em;
}
}

Some files were not shown because too many files have changed in this diff Show More