1.4.5.2
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
Vue.component("bandwidth-size-capacity-box", {
|
||||
props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"],
|
||||
data: function () {
|
||||
let v = this.vValue
|
||||
if (v == null) {
|
||||
v = {
|
||||
count: this.vCount,
|
||||
unit: this.vUnit
|
||||
}
|
||||
}
|
||||
if (v.unit == null || v.unit.length == 0) {
|
||||
v.unit = "mb"
|
||||
}
|
||||
|
||||
if (typeof (v["count"]) != "number") {
|
||||
v["count"] = -1
|
||||
}
|
||||
|
||||
let vSize = this.size
|
||||
if (vSize == null) {
|
||||
vSize = 6
|
||||
}
|
||||
|
||||
let vMaxlength = this.maxlength
|
||||
if (vMaxlength == null) {
|
||||
vMaxlength = 10
|
||||
}
|
||||
|
||||
let supportedUnits = this.vSupportedUnits
|
||||
if (supportedUnits == null) {
|
||||
supportedUnits = []
|
||||
}
|
||||
|
||||
return {
|
||||
capacity: v,
|
||||
countString: (v.count >= 0) ? v.count.toString() : "",
|
||||
vSize: vSize,
|
||||
vMaxlength: vMaxlength,
|
||||
supportedUnits: supportedUnits
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"countString": function (newValue) {
|
||||
let value = newValue.trim()
|
||||
if (value.length == 0) {
|
||||
this.capacity.count = -1
|
||||
this.change()
|
||||
return
|
||||
}
|
||||
let count = parseInt(value)
|
||||
if (!isNaN(count)) {
|
||||
this.capacity.count = count
|
||||
}
|
||||
this.change()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function () {
|
||||
this.$emit("change", this.capacity)
|
||||
}
|
||||
},
|
||||
template: `<div class="ui fields inline">
|
||||
<input type="hidden" :name="vName" :value="JSON.stringify(capacity)"/>
|
||||
<div class="ui field">
|
||||
<input type="text" v-model="countString" :maxlength="vMaxlength" :size="vSize"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" v-model="capacity.unit" @change="change">
|
||||
<option value="b" v-if="supportedUnits.length == 0 || supportedUnits.$contains('b')">Bps</option>
|
||||
<option value="kb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('kb')">Kbps</option>
|
||||
<option value="mb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('mb')">Mbps</option>
|
||||
<option value="gb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('gb')">Gbps</option>
|
||||
<option value="tb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('tb')">Tbps</option>
|
||||
<option value="pb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('pb')">Pbps</option>
|
||||
<option value="eb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('eb')">Ebps</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
Vue.component("bandwidth-size-capacity-view", {
|
||||
props: ["v-value"],
|
||||
data: function () {
|
||||
let capacity = this.vValue
|
||||
if (capacity != null && capacity.count > 0 && typeof capacity.unit === "string") {
|
||||
capacity.unit = capacity.unit[0].toUpperCase() + capacity.unit.substring(1) + "ps"
|
||||
}
|
||||
return {
|
||||
capacity: capacity
|
||||
}
|
||||
},
|
||||
template: `<span>
|
||||
<span v-if="capacity != null && capacity.count > 0">{{capacity.count}}{{capacity.unit}}</span>
|
||||
</span>`
|
||||
})
|
||||
16
EdgeUser/web/public/js/components/common/bits-var.js
Normal file
16
EdgeUser/web/public/js/components/common/bits-var.js
Normal file
@@ -0,0 +1,16 @@
|
||||
Vue.component("bits-var", {
|
||||
props: ["v-bits"],
|
||||
data: function () {
|
||||
let bits = this.vBits
|
||||
if (typeof bits != "number") {
|
||||
bits = 0
|
||||
}
|
||||
let format = teaweb.splitFormat(teaweb.formatBits(bits))
|
||||
return {
|
||||
format: format
|
||||
}
|
||||
},
|
||||
template:`<var class="normal">
|
||||
<span>{{format[0]}}</span>{{format[1]}}
|
||||
</var>`
|
||||
})
|
||||
16
EdgeUser/web/public/js/components/common/bytes-var.js
Normal file
16
EdgeUser/web/public/js/components/common/bytes-var.js
Normal file
@@ -0,0 +1,16 @@
|
||||
Vue.component("bytes-var", {
|
||||
props: ["v-bytes"],
|
||||
data: function () {
|
||||
let bytes = this.vBytes
|
||||
if (typeof bytes != "number") {
|
||||
bytes = 0
|
||||
}
|
||||
let format = teaweb.splitFormat(teaweb.formatBytes(bytes))
|
||||
return {
|
||||
format: format
|
||||
}
|
||||
},
|
||||
template:`<var class="normal">
|
||||
<span>{{format[0]}}</span>{{format[1]}}
|
||||
</var>`
|
||||
})
|
||||
52
EdgeUser/web/public/js/components/common/checkbox.js
Normal file
52
EdgeUser/web/public/js/components/common/checkbox.js
Normal file
@@ -0,0 +1,52 @@
|
||||
let checkboxId = 0
|
||||
Vue.component("checkbox", {
|
||||
props: ["name", "value", "v-value", "id", "checked"],
|
||||
data: function () {
|
||||
checkboxId++
|
||||
let elementId = this.id
|
||||
if (elementId == null) {
|
||||
elementId = "checkbox" + checkboxId
|
||||
}
|
||||
|
||||
let elementValue = this.vValue
|
||||
if (elementValue == null) {
|
||||
elementValue = "1"
|
||||
}
|
||||
|
||||
let checkedValue = this.value
|
||||
if (checkedValue == null && this.checked == "checked") {
|
||||
checkedValue = elementValue
|
||||
}
|
||||
|
||||
return {
|
||||
elementId: elementId,
|
||||
elementValue: elementValue,
|
||||
newValue: checkedValue
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function () {
|
||||
this.$emit("input", this.newValue)
|
||||
},
|
||||
check: function () {
|
||||
this.newValue = this.elementValue
|
||||
},
|
||||
uncheck: function () {
|
||||
this.newValue = ""
|
||||
},
|
||||
isChecked: function () {
|
||||
return (typeof (this.newValue) == "boolean" && this.newValue) || this.newValue == this.elementValue
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: function (v) {
|
||||
if (typeof v == "boolean") {
|
||||
this.newValue = v
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<div class="ui checkbox">
|
||||
<input type="checkbox" :name="name" :value="elementValue" :id="elementId" @change="change" v-model="newValue"/>
|
||||
<label :for="elementId"><slot></slot></label>
|
||||
</div>`
|
||||
})
|
||||
71
EdgeUser/web/public/js/components/common/columns-grid.js
Normal file
71
EdgeUser/web/public/js/components/common/columns-grid.js
Normal file
@@ -0,0 +1,71 @@
|
||||
Vue.component("columns-grid", {
|
||||
props: [],
|
||||
mounted: function () {
|
||||
this.columns = this.calculateColumns()
|
||||
|
||||
let that = this
|
||||
window.addEventListener("resize", function () {
|
||||
that.columns = that.calculateColumns()
|
||||
})
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
columns: "four"
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
calculateColumns: function () {
|
||||
let w = window.innerWidth
|
||||
let columns = Math.floor(w / 250)
|
||||
if (columns == 0) {
|
||||
columns = 1
|
||||
}
|
||||
|
||||
let columnElements = this.$el.getElementsByClassName("column")
|
||||
if (columnElements.length == 0) {
|
||||
return
|
||||
}
|
||||
let maxColumns = columnElements.length
|
||||
if (columns > maxColumns) {
|
||||
columns = maxColumns
|
||||
}
|
||||
|
||||
// 添加右侧边框
|
||||
for (let index = 0; index < columnElements.length; index++) {
|
||||
let el = columnElements[index]
|
||||
el.className = el.className.replace("with-border", "")
|
||||
if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) {
|
||||
el.className += " with-border"
|
||||
}
|
||||
}
|
||||
|
||||
switch (columns) {
|
||||
case 1:
|
||||
return "one"
|
||||
case 2:
|
||||
return "two"
|
||||
case 3:
|
||||
return "three"
|
||||
case 4:
|
||||
return "four"
|
||||
case 5:
|
||||
return "five"
|
||||
case 6:
|
||||
return "six"
|
||||
case 7:
|
||||
return "seven"
|
||||
case 8:
|
||||
return "eight"
|
||||
case 9:
|
||||
return "nine"
|
||||
case 10:
|
||||
return "ten"
|
||||
default:
|
||||
return "ten"
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<div :class="'ui ' + columns + ' columns grid counter-chart'">
|
||||
<slot></slot>
|
||||
</div>`
|
||||
})
|
||||
273
EdgeUser/web/public/js/components/common/combo-box.js
Normal file
273
EdgeUser/web/public/js/components/common/combo-box.js
Normal file
@@ -0,0 +1,273 @@
|
||||
Vue.component("combo-box", {
|
||||
// data-url 和 data-key 成对出现
|
||||
props: [
|
||||
"name", "title", "placeholder", "size", "v-items", "v-value",
|
||||
"data-url", // 数据源URL
|
||||
"data-key", // 数据源中数据的键名
|
||||
"data-search", // 是否启用动态搜索,如果值为on或true,则表示启用
|
||||
"width"
|
||||
],
|
||||
mounted: function () {
|
||||
if (this.dataURL.length > 0) {
|
||||
this.search("")
|
||||
}
|
||||
|
||||
// 设定菜单宽度
|
||||
let searchBox = this.$refs.searchBox
|
||||
if (searchBox != null) {
|
||||
let inputWidth = searchBox.offsetWidth
|
||||
if (inputWidth != null && inputWidth > 0) {
|
||||
this.$refs.menu.style.width = inputWidth + "px"
|
||||
} else if (this.styleWidth.length > 0) {
|
||||
this.$refs.menu.style.width = this.styleWidth
|
||||
}
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
let items = this.vItems
|
||||
if (items == null || !(items instanceof Array)) {
|
||||
items = []
|
||||
}
|
||||
items = this.formatItems(items)
|
||||
|
||||
// 当前选中项
|
||||
let selectedItem = null
|
||||
if (this.vValue != null) {
|
||||
let that = this
|
||||
items.forEach(function (v) {
|
||||
if (v.value == that.vValue) {
|
||||
selectedItem = v
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let width = this.width
|
||||
if (width == null || width.length == 0) {
|
||||
width = "11em"
|
||||
} else {
|
||||
if (/\d+$/.test(width)) {
|
||||
width += "em"
|
||||
}
|
||||
}
|
||||
|
||||
// data url
|
||||
let dataURL = ""
|
||||
if (typeof this.dataUrl == "string" && this.dataUrl.length > 0) {
|
||||
dataURL = this.dataUrl
|
||||
}
|
||||
|
||||
return {
|
||||
allItems: items, // 原始的所有的items
|
||||
items: items.$copy(), // 候选的items
|
||||
selectedItem: selectedItem, // 选中的item
|
||||
keyword: "",
|
||||
visible: false,
|
||||
hideTimer: null,
|
||||
hoverIndex: 0,
|
||||
styleWidth: width,
|
||||
|
||||
isInitial: true,
|
||||
dataURL: dataURL,
|
||||
urlRequestId: 0 // 记录URL请求ID,防止并行冲突
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
search: function (keyword) {
|
||||
// 从URL中获取选项数据
|
||||
let dataUrl = this.dataURL
|
||||
let dataKey = this.dataKey
|
||||
let that = this
|
||||
|
||||
let requestId = Math.random()
|
||||
this.urlRequestId = requestId
|
||||
|
||||
Tea.action(dataUrl)
|
||||
.params({
|
||||
keyword: (keyword == null) ? "" : keyword
|
||||
})
|
||||
.post()
|
||||
.success(function (resp) {
|
||||
if (requestId != that.urlRequestId) {
|
||||
return
|
||||
}
|
||||
|
||||
if (resp.data != null) {
|
||||
if (typeof (resp.data[dataKey]) == "object") {
|
||||
let items = that.formatItems(resp.data[dataKey])
|
||||
that.allItems = items
|
||||
that.items = items.$copy()
|
||||
|
||||
if (that.isInitial) {
|
||||
that.isInitial = false
|
||||
if (that.vValue != null) {
|
||||
items.forEach(function (v) {
|
||||
if (v.value == that.vValue) {
|
||||
that.selectedItem = v
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
formatItems: function (items) {
|
||||
items.forEach(function (v) {
|
||||
if (v.value == null) {
|
||||
v.value = v.id
|
||||
}
|
||||
})
|
||||
return items
|
||||
},
|
||||
reset: function () {
|
||||
this.selectedItem = null
|
||||
this.change()
|
||||
this.hoverIndex = 0
|
||||
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
if (that.$refs.searchBox) {
|
||||
that.$refs.searchBox.focus()
|
||||
}
|
||||
})
|
||||
},
|
||||
clear: function () {
|
||||
this.selectedItem = null
|
||||
this.change()
|
||||
this.hoverIndex = 0
|
||||
},
|
||||
changeKeyword: function () {
|
||||
let shouldSearch = this.dataURL.length > 0 && (this.dataSearch == "on" || this.dataSearch == "true")
|
||||
|
||||
this.hoverIndex = 0
|
||||
let keyword = this.keyword
|
||||
if (keyword.length == 0) {
|
||||
if (shouldSearch) {
|
||||
this.search(keyword)
|
||||
} else {
|
||||
this.items = this.allItems.$copy()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (shouldSearch) {
|
||||
this.search(keyword)
|
||||
} else {
|
||||
this.items = this.allItems.$copy().filter(function (v) {
|
||||
if (v.fullname != null && v.fullname.length > 0 && teaweb.match(v.fullname, keyword)) {
|
||||
return true
|
||||
}
|
||||
return teaweb.match(v.name, keyword)
|
||||
})
|
||||
}
|
||||
},
|
||||
selectItem: function (item) {
|
||||
this.selectedItem = item
|
||||
this.change()
|
||||
this.hoverIndex = 0
|
||||
this.keyword = ""
|
||||
this.changeKeyword()
|
||||
},
|
||||
confirm: function () {
|
||||
if (this.items.length > this.hoverIndex) {
|
||||
this.selectItem(this.items[this.hoverIndex])
|
||||
}
|
||||
},
|
||||
show: function () {
|
||||
this.visible = true
|
||||
|
||||
// 不要重置hoverIndex,以便焦点可以在输入框和可选项之间切换
|
||||
},
|
||||
hide: function () {
|
||||
let that = this
|
||||
this.hideTimer = setTimeout(function () {
|
||||
that.visible = false
|
||||
}, 500)
|
||||
},
|
||||
downItem: function () {
|
||||
this.hoverIndex++
|
||||
if (this.hoverIndex > this.items.length - 1) {
|
||||
this.hoverIndex = 0
|
||||
}
|
||||
this.focusItem()
|
||||
},
|
||||
upItem: function () {
|
||||
this.hoverIndex--
|
||||
if (this.hoverIndex < 0) {
|
||||
this.hoverIndex = 0
|
||||
}
|
||||
this.focusItem()
|
||||
},
|
||||
focusItem: function () {
|
||||
if (this.hoverIndex < this.items.length) {
|
||||
this.$refs.itemRef[this.hoverIndex].focus()
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.searchBox.focus()
|
||||
if (that.hideTimer != null) {
|
||||
clearTimeout(that.hideTimer)
|
||||
that.hideTimer = null
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
change: function () {
|
||||
this.$emit("change", this.selectedItem)
|
||||
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
if (that.$refs.selectedLabel != null) {
|
||||
that.$refs.selectedLabel.focus()
|
||||
}
|
||||
})
|
||||
},
|
||||
submitForm: function (event) {
|
||||
if (event.target.tagName != "A") {
|
||||
return
|
||||
}
|
||||
let parentBox = this.$refs.selectedLabel.parentNode
|
||||
while (true) {
|
||||
parentBox = parentBox.parentNode
|
||||
if (parentBox == null || parentBox.tagName == "BODY") {
|
||||
return
|
||||
}
|
||||
if (parentBox.tagName == "FORM") {
|
||||
parentBox.submit()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setDataURL: function (dataURL) {
|
||||
this.dataURL = dataURL
|
||||
},
|
||||
reloadData: function () {
|
||||
this.search("")
|
||||
}
|
||||
},
|
||||
template: `<div style="display: inline; z-index: 10; background: white" class="combo-box">
|
||||
<!-- 搜索框 -->
|
||||
<div v-if="selectedItem == null">
|
||||
<input type="text" v-model="keyword" :placeholder="placeholder" :size="size" :style="{'width': styleWidth}" @input="changeKeyword" @focus="show" @blur="hide" @keyup.enter="confirm()" @keypress.enter.prevent="1" ref="searchBox" @keydown.down.prevent="downItem" @keydown.up.prevent="upItem"/>
|
||||
</div>
|
||||
|
||||
<!-- 当前选中 -->
|
||||
<div v-if="selectedItem != null">
|
||||
<input type="hidden" :name="name" :value="selectedItem.value"/>
|
||||
<span class="ui label basic" style="line-height: 1.4; font-weight: normal; font-size: 1em" ref="selectedLabel"><span><span v-if="title != null && title.length > 0">{{title}}:</span>{{selectedItem.name}}</span>
|
||||
<a href="" title="清除" @click.prevent="reset"><i class="icon remove small"></i></a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 菜单 -->
|
||||
<div v-show="selectedItem == null && items.length > 0 && visible">
|
||||
<div class="ui menu vertical small narrow-scrollbar" ref="menu">
|
||||
<a href="" v-for="(item, index) in items" ref="itemRef" class="item" :class="{active: index == hoverIndex, blue: index == hoverIndex}" @click.prevent="selectItem(item)" style="line-height: 1.4">
|
||||
<span v-if="item.fullname != null && item.fullname.length > 0">{{item.fullname}}</span>
|
||||
<span v-else>{{item.name}}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
Vue.component("copy-to-clipboard", {
|
||||
props: ["v-target"],
|
||||
created: function () {
|
||||
if (typeof ClipboardJS == "undefined") {
|
||||
let jsFile = document.createElement("script")
|
||||
jsFile.setAttribute("src", "/js/clipboard.min.js")
|
||||
document.head.appendChild(jsFile)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copy: function () {
|
||||
new ClipboardJS('[data-clipboard-target]');
|
||||
teaweb.success("已复制到剪切板")
|
||||
}
|
||||
},
|
||||
template: `<a href="" title="拷贝到剪切板" :data-clipboard-target="'#' + vTarget" @click.prevent="copy"><i class="ui icon copy small"></i></em></a>`
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
Vue.component("countries-selector", {
|
||||
props: ["v-countries"],
|
||||
data: function () {
|
||||
let countries = this.vCountries
|
||||
if (countries == null) {
|
||||
countries = []
|
||||
}
|
||||
let countryIds = countries.$map(function (k, v) {
|
||||
return v.id
|
||||
})
|
||||
return {
|
||||
countries: countries,
|
||||
countryIds: countryIds
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
let countryStringIds = this.countryIds.map(function (v) {
|
||||
return v.toString()
|
||||
})
|
||||
let that = this
|
||||
teaweb.popup("/ui/selectCountriesPopup?countryIds=" + countryStringIds.join(","), {
|
||||
width: "48em",
|
||||
height: "23em",
|
||||
callback: function (resp) {
|
||||
that.countries = resp.data.countries
|
||||
that.change()
|
||||
}
|
||||
})
|
||||
},
|
||||
remove: function (index) {
|
||||
this.countries.$remove(index)
|
||||
this.change()
|
||||
},
|
||||
change: function () {
|
||||
this.countryIds = this.countries.$map(function (k, v) {
|
||||
return v.id
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="countryIdsJSON" :value="JSON.stringify(countryIds)"/>
|
||||
<div v-if="countries.length > 0" style="margin-bottom: 0.5em">
|
||||
<div v-for="(country, index) in countries" class="ui label tiny basic">{{country.name}} <a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove"></i></a></div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
169
EdgeUser/web/public/js/components/common/csrf-token.js
Normal file
169
EdgeUser/web/public/js/components/common/csrf-token.js
Normal file
@@ -0,0 +1,169 @@
|
||||
Vue.component("csrf-token", {
|
||||
created: function () {
|
||||
this.refreshToken()
|
||||
},
|
||||
mounted: function () {
|
||||
let that = this
|
||||
var form = this.$refs.token.form
|
||||
|
||||
// 监听表单提交,在提交前刷新token并确保更新到 DOM
|
||||
form.addEventListener("submit", function (e) {
|
||||
// 如果正在刷新,等待刷新完成
|
||||
if (that.refreshing) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return false
|
||||
}
|
||||
|
||||
// 阻止默认提交,先刷新 token
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
that.refreshing = true
|
||||
|
||||
// 刷新 token
|
||||
that.refreshToken(function () {
|
||||
// 确保 DOM 中的 token 值是最新的
|
||||
that.$forceUpdate()
|
||||
that.$nextTick(function () {
|
||||
var tokenInput = form.querySelector('input[name="csrfToken"]')
|
||||
if (tokenInput) {
|
||||
tokenInput.value = that.token
|
||||
}
|
||||
if (that.$refs.token) {
|
||||
that.$refs.token.value = that.token
|
||||
}
|
||||
|
||||
// 确保 DOM 已更新后,再触发表单提交
|
||||
setTimeout(function () {
|
||||
that.refreshing = false
|
||||
// 重新触发表单提交
|
||||
Tea.runActionOn(form)
|
||||
}, 50)
|
||||
})
|
||||
})
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
// 自动刷新
|
||||
setInterval(function () {
|
||||
that.refreshToken()
|
||||
}, 10 * 60 * 1000)
|
||||
|
||||
// 监听表单提交失败,如果是 CSRF token 错误,自动刷新 token 并重试
|
||||
this.setupAutoRetry(form)
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
token: "",
|
||||
retrying: false,
|
||||
refreshing: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
refreshToken: function (callback) {
|
||||
let that = this
|
||||
Tea.action("/csrf/token")
|
||||
.get()
|
||||
.success(function (resp) {
|
||||
that.token = resp.data.token
|
||||
if (callback) {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
if (callback) {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
},
|
||||
setupAutoRetry: function (form) {
|
||||
let that = this
|
||||
var originalFail = form.getAttribute("data-tea-fail")
|
||||
|
||||
// 确保 Tea.Vue 存在
|
||||
if (typeof Tea === "undefined" || Tea.Vue == null) {
|
||||
if (typeof Tea === "undefined") {
|
||||
window.Tea = {}
|
||||
}
|
||||
if (Tea.Vue == null) {
|
||||
Tea.Vue = {}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建一个包装的 fail 函数
|
||||
var wrappedFailName = "csrfAutoRetryFail_" + Math.random().toString(36).substr(2, 9)
|
||||
form.setAttribute("data-tea-fail", wrappedFailName)
|
||||
|
||||
Tea.Vue[wrappedFailName] = function (resp) {
|
||||
// 检查是否是 CSRF token 错误
|
||||
var isCSRFError = false
|
||||
if (resp && resp.message) {
|
||||
// 检查消息是否包含 "表单已失效" 或 "001"
|
||||
if (resp.message.indexOf("表单已失效") >= 0 || resp.message.indexOf("(001)") >= 0) {
|
||||
isCSRFError = true
|
||||
}
|
||||
}
|
||||
// 检查 HTTP 状态码是否为 403 或 400
|
||||
if (!isCSRFError && resp && (resp.statusCode === 403 || resp.status === 403 || resp.statusCode === 400 || resp.status === 400)) {
|
||||
isCSRFError = true
|
||||
}
|
||||
|
||||
if (isCSRFError) {
|
||||
// 如果不是正在重试,则立即刷新 token 并自动重试
|
||||
if (!that.retrying) {
|
||||
that.retrying = true
|
||||
// 立即刷新 token
|
||||
that.refreshToken(function () {
|
||||
// 强制更新 Vue,确保响应式数据已更新
|
||||
that.$forceUpdate()
|
||||
|
||||
// 使用 $nextTick 等待 Vue 完成 DOM 更新
|
||||
that.$nextTick(function () {
|
||||
// 直接查找并更新 DOM 中的 input 元素(通过 name 属性)
|
||||
var tokenInput = form.querySelector('input[name="csrfToken"]')
|
||||
if (tokenInput) {
|
||||
tokenInput.value = that.token
|
||||
}
|
||||
|
||||
// 如果 ref 存在,也更新它
|
||||
if (that.$refs.token) {
|
||||
that.$refs.token.value = that.token
|
||||
}
|
||||
|
||||
// 使用 setTimeout 确保 DOM 已完全更新
|
||||
setTimeout(function () {
|
||||
// 再次确认 token 值已更新
|
||||
var finalTokenInput = form.querySelector('input[name="csrfToken"]')
|
||||
if (finalTokenInput && finalTokenInput.value !== that.token) {
|
||||
finalTokenInput.value = that.token
|
||||
}
|
||||
|
||||
that.retrying = false
|
||||
// 重新触发表单提交
|
||||
Tea.runActionOn(form)
|
||||
}, 150)
|
||||
})
|
||||
})
|
||||
return // 不调用原始 fail 函数
|
||||
} else {
|
||||
// 如果正在重试,说明已经刷新过 token,直接调用原始 fail 函数
|
||||
if (originalFail && typeof Tea.Vue[originalFail] === "function") {
|
||||
return Tea.Vue[originalFail].call(Tea.Vue, resp)
|
||||
} else {
|
||||
Tea.failResponse(resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 不是 CSRF 错误,调用原始 fail 函数或默认处理
|
||||
if (originalFail && typeof Tea.Vue[originalFail] === "function") {
|
||||
return Tea.Vue[originalFail].call(Tea.Vue, resp)
|
||||
} else {
|
||||
Tea.failResponse(resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<input type="hidden" name="csrfToken" :value="token" ref="token"/>`
|
||||
})
|
||||
59
EdgeUser/web/public/js/components/common/datepicker.js
Normal file
59
EdgeUser/web/public/js/components/common/datepicker.js
Normal file
@@ -0,0 +1,59 @@
|
||||
Vue.component("datepicker", {
|
||||
props: ["value", "v-name", "name", "v-value", "v-bottom-left", "placeholder"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
teaweb.datepicker(this.$refs.dayInput, function (v) {
|
||||
that.day = v
|
||||
that.change()
|
||||
}, !!this.vBottomLeft)
|
||||
},
|
||||
data: function () {
|
||||
let name = this.vName
|
||||
if (name == null) {
|
||||
name = this.name
|
||||
}
|
||||
if (name == null) {
|
||||
name = "day"
|
||||
}
|
||||
|
||||
let day = this.vValue
|
||||
if (day == null) {
|
||||
day = this.value
|
||||
if (day == null) {
|
||||
day = ""
|
||||
}
|
||||
}
|
||||
|
||||
let placeholder = "YYYY-MM-DD"
|
||||
if (this.placeholder != null) {
|
||||
placeholder = this.placeholder
|
||||
}
|
||||
|
||||
return {
|
||||
realName: name,
|
||||
realPlaceholder: placeholder,
|
||||
day: day
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: function (v) {
|
||||
this.day = v
|
||||
|
||||
let picker = this.$refs.dayInput.picker
|
||||
if (picker != null) {
|
||||
if (v != null && /^\d+-\d+-\d+$/.test(v)) {
|
||||
picker.setDate(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function () {
|
||||
this.$emit("input", this.day) // support v-model,事件触发需要在 change 之前
|
||||
this.$emit("change", this.day)
|
||||
}
|
||||
},
|
||||
template: `<div style="display: inline-block">
|
||||
<input type="text" :name="realName" v-model="day" :placeholder="realPlaceholder" style="width:8.6em" maxlength="10" @input="change" ref="dayInput" autocomplete="off"/>
|
||||
</div>`
|
||||
})
|
||||
185
EdgeUser/web/public/js/components/common/datetime-input.js
Normal file
185
EdgeUser/web/public/js/components/common/datetime-input.js
Normal file
@@ -0,0 +1,185 @@
|
||||
Vue.component("datetime-input", {
|
||||
props: ["v-name", "v-timestamp"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
teaweb.datepicker(this.$refs.dayInput, function (v) {
|
||||
that.day = v
|
||||
that.hour = "23"
|
||||
that.minute = "59"
|
||||
that.second = "59"
|
||||
that.change()
|
||||
})
|
||||
},
|
||||
data: function () {
|
||||
let timestamp = this.vTimestamp
|
||||
if (timestamp != null) {
|
||||
timestamp = parseInt(timestamp)
|
||||
if (isNaN(timestamp)) {
|
||||
timestamp = 0
|
||||
}
|
||||
} else {
|
||||
timestamp = 0
|
||||
}
|
||||
|
||||
let day = ""
|
||||
let hour = ""
|
||||
let minute = ""
|
||||
let second = ""
|
||||
|
||||
if (timestamp > 0) {
|
||||
let date = new Date()
|
||||
date.setTime(timestamp * 1000)
|
||||
|
||||
let year = date.getFullYear().toString()
|
||||
let month = this.leadingZero((date.getMonth() + 1).toString(), 2)
|
||||
day = year + "-" + month + "-" + this.leadingZero(date.getDate().toString(), 2)
|
||||
|
||||
hour = this.leadingZero(date.getHours().toString(), 2)
|
||||
minute = this.leadingZero(date.getMinutes().toString(), 2)
|
||||
second = this.leadingZero(date.getSeconds().toString(), 2)
|
||||
}
|
||||
|
||||
return {
|
||||
timestamp: timestamp,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
|
||||
hasDayError: false,
|
||||
hasHourError: false,
|
||||
hasMinuteError: false,
|
||||
hasSecondError: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function () {
|
||||
// day
|
||||
if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(this.day)) {
|
||||
this.hasDayError = true
|
||||
return
|
||||
}
|
||||
let pieces = this.day.split("-")
|
||||
let year = parseInt(pieces[0])
|
||||
|
||||
let month = parseInt(pieces[1])
|
||||
if (month < 1 || month > 12) {
|
||||
this.hasDayError = true
|
||||
return
|
||||
}
|
||||
|
||||
let day = parseInt(pieces[2])
|
||||
if (day < 1 || day > 32) {
|
||||
this.hasDayError = true
|
||||
return
|
||||
}
|
||||
|
||||
this.hasDayError = false
|
||||
|
||||
// hour
|
||||
if (!/^\d+$/.test(this.hour)) {
|
||||
this.hasHourError = true
|
||||
return
|
||||
}
|
||||
let hour = parseInt(this.hour)
|
||||
if (isNaN(hour)) {
|
||||
this.hasHourError = true
|
||||
return
|
||||
}
|
||||
if (hour < 0 || hour >= 24) {
|
||||
this.hasHourError = true
|
||||
return
|
||||
}
|
||||
this.hasHourError = false
|
||||
|
||||
// minute
|
||||
if (!/^\d+$/.test(this.minute)) {
|
||||
this.hasMinuteError = true
|
||||
return
|
||||
}
|
||||
let minute = parseInt(this.minute)
|
||||
if (isNaN(minute)) {
|
||||
this.hasMinuteError = true
|
||||
return
|
||||
}
|
||||
if (minute < 0 || minute >= 60) {
|
||||
this.hasMinuteError = true
|
||||
return
|
||||
}
|
||||
this.hasMinuteError = false
|
||||
|
||||
// second
|
||||
if (!/^\d+$/.test(this.second)) {
|
||||
this.hasSecondError = true
|
||||
return
|
||||
}
|
||||
let second = parseInt(this.second)
|
||||
if (isNaN(second)) {
|
||||
this.hasSecondError = true
|
||||
return
|
||||
}
|
||||
if (second < 0 || second >= 60) {
|
||||
this.hasSecondError = true
|
||||
return
|
||||
}
|
||||
this.hasSecondError = false
|
||||
|
||||
let date = new Date(year, month - 1, day, hour, minute, second)
|
||||
this.timestamp = Math.floor(date.getTime() / 1000)
|
||||
},
|
||||
leadingZero: function (s, l) {
|
||||
s = s.toString()
|
||||
if (l <= s.length) {
|
||||
return s
|
||||
}
|
||||
for (let i = 0; i < l - s.length; i++) {
|
||||
s = "0" + s
|
||||
}
|
||||
return s
|
||||
},
|
||||
resultTimestamp: function () {
|
||||
return this.timestamp
|
||||
},
|
||||
nextYear: function () {
|
||||
let date = new Date()
|
||||
date.setFullYear(date.getFullYear()+1)
|
||||
this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2)
|
||||
this.hour = this.leadingZero(date.getHours(), 2)
|
||||
this.minute = this.leadingZero(date.getMinutes(), 2)
|
||||
this.second = this.leadingZero(date.getSeconds(), 2)
|
||||
this.change()
|
||||
},
|
||||
nextDays: function (days) {
|
||||
let date = new Date()
|
||||
date.setTime(date.getTime() + days * 86400 * 1000)
|
||||
this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2)
|
||||
this.hour = this.leadingZero(date.getHours(), 2)
|
||||
this.minute = this.leadingZero(date.getMinutes(), 2)
|
||||
this.second = this.leadingZero(date.getSeconds(), 2)
|
||||
this.change()
|
||||
},
|
||||
nextHours: function (hours) {
|
||||
let date = new Date()
|
||||
date.setTime(date.getTime() + hours * 3600 * 1000)
|
||||
this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2)
|
||||
this.hour = this.leadingZero(date.getHours(), 2)
|
||||
this.minute = this.leadingZero(date.getMinutes(), 2)
|
||||
this.second = this.leadingZero(date.getSeconds(), 2)
|
||||
this.change()
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" :name="vName" :value="timestamp"/>
|
||||
<div class="ui fields inline" style="padding: 0; margin:0">
|
||||
<div class="ui field" :class="{error: hasDayError}">
|
||||
<input type="text" v-model="day" placeholder="YYYY-MM-DD" style="width:8.6em" maxlength="10" @input="change" ref="dayInput"/>
|
||||
</div>
|
||||
<div class="ui field" :class="{error: hasHourError}"><input type="text" v-model="hour" maxlength="2" style="width:4em" placeholder="时" @input="change"/></div>
|
||||
<div class="ui field">:</div>
|
||||
<div class="ui field" :class="{error: hasMinuteError}"><input type="text" v-model="minute" maxlength="2" style="width:4em" placeholder="分" @input="change"/></div>
|
||||
<div class="ui field">:</div>
|
||||
<div class="ui field" :class="{error: hasSecondError}"><input type="text" v-model="second" maxlength="2" style="width:4em" placeholder="秒" @input="change"/></div>
|
||||
</div>
|
||||
<p class="comment">常用时间:<a href="" @click.prevent="nextHours(1)"> 1小时 </a> <span class="disabled">|</span> <a href="" @click.prevent="nextDays(1)"> 1天 </a> <span class="disabled">|</span> <a href="" @click.prevent="nextDays(3)"> 3天 </a> <span class="disabled">|</span> <a href="" @click.prevent="nextDays(7)"> 1周 </a> <span class="disabled">|</span> <a href="" @click.prevent="nextDays(30)"> 30天 </a> <span class="disabled">|</span> <a href="" @click.prevent="nextYear()"> 1年 </a> </p>
|
||||
</div>`
|
||||
})
|
||||
37
EdgeUser/web/public/js/components/common/download-link.js
Normal file
37
EdgeUser/web/public/js/components/common/download-link.js
Normal file
@@ -0,0 +1,37 @@
|
||||
Vue.component("download-link", {
|
||||
props: ["v-element", "v-file"],
|
||||
created: function () {
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.url = that.composeURL()
|
||||
}, 1000)
|
||||
},
|
||||
data: function () {
|
||||
let filename = this.vFile
|
||||
if (filename == null || filename.length == 0) {
|
||||
filename = "unknown-file"
|
||||
}
|
||||
return {
|
||||
file: filename,
|
||||
url: this.composeURL()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
composeURL: function () {
|
||||
let e = document.getElementById(this.vElement)
|
||||
if (e == null) {
|
||||
teaweb.warn("找不到要下载的内容")
|
||||
return
|
||||
}
|
||||
let text = e.innerText
|
||||
if (text == null) {
|
||||
text = e.textContent
|
||||
}
|
||||
return Tea.url("/ui/download", {
|
||||
file: this.file,
|
||||
text: text
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<a :href="url" target="_blank" style="font-weight: normal"><slot></slot></a>`,
|
||||
})
|
||||
30
EdgeUser/web/public/js/components/common/file-textarea.js
Normal file
30
EdgeUser/web/public/js/components/common/file-textarea.js
Normal file
@@ -0,0 +1,30 @@
|
||||
Vue.component("file-textarea", {
|
||||
props: ["value"],
|
||||
data: function () {
|
||||
let value = this.value
|
||||
if (typeof value != "string") {
|
||||
value = ""
|
||||
}
|
||||
return {
|
||||
realValue: value
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
},
|
||||
methods: {
|
||||
dragover: function () {},
|
||||
drop: function (e) {
|
||||
let that = this
|
||||
e.dataTransfer.items[0].getAsFile().text().then(function (data) {
|
||||
that.setValue(data)
|
||||
})
|
||||
},
|
||||
setValue: function (value) {
|
||||
this.realValue = value
|
||||
},
|
||||
focus: function () {
|
||||
this.$refs.textarea.focus()
|
||||
}
|
||||
},
|
||||
template: `<textarea @drop.prevent="drop" @dragover.prevent="dragover" ref="textarea" v-model="realValue"></textarea>`
|
||||
})
|
||||
13
EdgeUser/web/public/js/components/common/first-menu.js
Normal file
13
EdgeUser/web/public/js/components/common/first-menu.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 一级菜单
|
||||
*/
|
||||
Vue.component("first-menu", {
|
||||
props: [],
|
||||
template: ' \
|
||||
<div class="first-menu"> \
|
||||
<div class="ui menu text blue small">\
|
||||
<slot></slot>\
|
||||
</div> \
|
||||
<div class="ui divider"></div> \
|
||||
</div>'
|
||||
});
|
||||
23
EdgeUser/web/public/js/components/common/inner-menu-item.js
Normal file
23
EdgeUser/web/public/js/components/common/inner-menu-item.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 菜单项
|
||||
*/
|
||||
Vue.component("inner-menu-item", {
|
||||
props: ["href", "active", "code"],
|
||||
data: function () {
|
||||
var active = this.active;
|
||||
if (typeof(active) =="undefined") {
|
||||
var itemCode = "";
|
||||
if (typeof (window.TEA.ACTION.data.firstMenuItem) != "undefined") {
|
||||
itemCode = window.TEA.ACTION.data.firstMenuItem;
|
||||
}
|
||||
active = (itemCode == this.code);
|
||||
}
|
||||
return {
|
||||
vHref: (this.href == null) ? "" : this.href,
|
||||
vActive: active
|
||||
};
|
||||
},
|
||||
template: '\
|
||||
<a :href="vHref" class="item right" style="color:#4183c4" :class="{active:vActive}">[<slot></slot>]</a> \
|
||||
'
|
||||
});
|
||||
11
EdgeUser/web/public/js/components/common/inner-menu.js
Normal file
11
EdgeUser/web/public/js/components/common/inner-menu.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 二级菜单
|
||||
*/
|
||||
Vue.component("inner-menu", {
|
||||
template: `
|
||||
<div class="second-menu" style="width:80%;position: absolute;top:-8px;right:1em">
|
||||
<div class="ui menu text blue small">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>`
|
||||
});
|
||||
27
EdgeUser/web/public/js/components/common/js-page.js
Normal file
27
EdgeUser/web/public/js/components/common/js-page.js
Normal file
@@ -0,0 +1,27 @@
|
||||
Vue.component("js-page", {
|
||||
props: ["v-max"],
|
||||
data: function () {
|
||||
let max = this.vMax
|
||||
if (max == null) {
|
||||
max = 0
|
||||
}
|
||||
return {
|
||||
max: max,
|
||||
page: 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateMax: function (max) {
|
||||
this.max = max
|
||||
},
|
||||
selectPage: function(page) {
|
||||
this.page = page
|
||||
this.$emit("change", page)
|
||||
}
|
||||
},
|
||||
template:`<div>
|
||||
<div class="page" v-if="max > 1">
|
||||
<a href="" v-for="i in max" :class="{active: i == page}" @click.prevent="selectPage(i)">{{i}}</a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
58
EdgeUser/web/public/js/components/common/keyword.js
Normal file
58
EdgeUser/web/public/js/components/common/keyword.js
Normal file
@@ -0,0 +1,58 @@
|
||||
Vue.component("keyword", {
|
||||
props: ["v-word"],
|
||||
data: function () {
|
||||
let word = this.vWord
|
||||
if (word == null) {
|
||||
word = ""
|
||||
} else {
|
||||
word = word.replace(/\)/g, "\\)")
|
||||
word = word.replace(/\(/g, "\\(")
|
||||
word = word.replace(/\+/g, "\\+")
|
||||
word = word.replace(/\^/g, "\\^")
|
||||
word = word.replace(/\$/g, "\\$")
|
||||
word = word.replace(/\?/g, "\\?")
|
||||
word = word.replace(/\*/g, "\\*")
|
||||
word = word.replace(/\[/g, "\\[")
|
||||
word = word.replace(/{/g, "\\{")
|
||||
word = word.replace(/\./g, "\\.")
|
||||
}
|
||||
|
||||
let slot = this.$slots["default"][0]
|
||||
let text = slot.text
|
||||
if (word.length > 0) {
|
||||
let that = this
|
||||
let m = [] // replacement => tmp
|
||||
let tmpIndex = 0
|
||||
text = text.replaceAll(new RegExp("(" + word + ")", "ig"), function (replacement) {
|
||||
tmpIndex++
|
||||
let s = "<span style=\"border: 1px #ccc dashed; color: #ef4d58\">" + that.encodeHTML(replacement) + "</span>"
|
||||
let tmpKey = "$TMP__KEY__" + tmpIndex.toString() + "$"
|
||||
m.push([tmpKey, s])
|
||||
return tmpKey
|
||||
})
|
||||
text = this.encodeHTML(text)
|
||||
|
||||
m.forEach(function (r) {
|
||||
text = text.replace(r[0], r[1])
|
||||
})
|
||||
|
||||
} else {
|
||||
text = this.encodeHTML(text)
|
||||
}
|
||||
|
||||
return {
|
||||
word: word,
|
||||
text: text
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
encodeHTML: function (s) {
|
||||
s = s.replace(/&/g, "&")
|
||||
s = s.replace(/</g, "<")
|
||||
s = s.replace(/>/g, ">")
|
||||
s = s.replace(/"/g, """)
|
||||
return s
|
||||
}
|
||||
},
|
||||
template: `<span><span style="display: none"><slot></slot></span><span v-html="text"></span></span>`
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
Vue.component("labeled-input", {
|
||||
props: ["name", "size", "maxlength", "label", "value"],
|
||||
template: '<div class="ui input right labeled"> \
|
||||
<input type="text" :name="name" :size="size" :maxlength="maxlength" :value="value"/>\
|
||||
<span class="ui label">{{label}}</span>\
|
||||
</div>'
|
||||
});
|
||||
40
EdgeUser/web/public/js/components/common/labels.js
Normal file
40
EdgeUser/web/public/js/components/common/labels.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// 启用状态标签
|
||||
Vue.component("label-on", {
|
||||
props: ["v-is-on"],
|
||||
template: '<div><span v-if="vIsOn" class="green">已启用</span><span v-if="!vIsOn" class="red">已停用</span></div>'
|
||||
})
|
||||
|
||||
// 文字代码标签
|
||||
Vue.component("code-label", {
|
||||
methods: {
|
||||
click: function (args) {
|
||||
this.$emit("click", args)
|
||||
}
|
||||
},
|
||||
template: `<span class="ui label basic small" style="padding: 3px;margin-left:2px;margin-right:2px" @click.prevent="click"><slot></slot></span>`
|
||||
})
|
||||
|
||||
// tiny标签
|
||||
Vue.component("tiny-label", {
|
||||
template: `<span class="ui label tiny" style="margin-bottom: 0.5em"><slot></slot></span>`
|
||||
})
|
||||
|
||||
Vue.component("tiny-basic-label", {
|
||||
template: `<span class="ui label tiny basic" style="margin-bottom: 0.5em"><slot></slot></span>`
|
||||
})
|
||||
|
||||
// 更小的标签
|
||||
Vue.component("micro-basic-label", {
|
||||
template: `<span class="ui label tiny basic" style="margin-bottom: 0.5em; font-size: 0.7em; padding: 4px"><slot></slot></span>`
|
||||
})
|
||||
|
||||
|
||||
// 灰色的Label
|
||||
Vue.component("grey-label", {
|
||||
template: `<span class="ui label basic grey tiny" style="margin-top: 0.4em; font-size: 0.7em; border: 1px solid #ddd!important; font-weight: normal;"><slot></slot></span>`
|
||||
})
|
||||
|
||||
// Plus专属
|
||||
Vue.component("plus-label", {
|
||||
template: `<span></span>`
|
||||
})
|
||||
74
EdgeUser/web/public/js/components/common/links.js
Normal file
74
EdgeUser/web/public/js/components/common/links.js
Normal file
@@ -0,0 +1,74 @@
|
||||
// 使用Icon的链接方式
|
||||
Vue.component("link-icon", {
|
||||
props: ["href", "title"],
|
||||
data: function () {
|
||||
return {
|
||||
vTitle: (this.title == null) ? "打开链接" : this.title
|
||||
}
|
||||
},
|
||||
template: `<span><slot></slot> <a :href="href" :title="vTitle" class="link grey"><i class="icon linkify small"></i></a></span>`
|
||||
})
|
||||
|
||||
// 带有下划虚线的连接
|
||||
Vue.component("link-red", {
|
||||
props: ["href", "title"],
|
||||
data: function () {
|
||||
let href = this.href
|
||||
if (href == null) {
|
||||
href = ""
|
||||
}
|
||||
return {
|
||||
vHref: href
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickPrevent: function () {
|
||||
emitClick(this, arguments)
|
||||
}
|
||||
},
|
||||
template: `<a :href="vHref" :title="title" style="border-bottom: 1px #db2828 dashed" @click.prevent="clickPrevent"><span class="red"><slot></slot></span></a>`
|
||||
})
|
||||
|
||||
// 会弹出窗口的链接
|
||||
Vue.component("link-popup", {
|
||||
props: ["title"],
|
||||
methods: {
|
||||
clickPrevent: function () {
|
||||
emitClick(this, arguments)
|
||||
}
|
||||
},
|
||||
template: `<a href="" :title="title" @click.prevent="clickPrevent"><slot></slot></a>`
|
||||
})
|
||||
|
||||
Vue.component("popup-icon", {
|
||||
props: ["title", "href", "height"],
|
||||
methods: {
|
||||
clickPrevent: function () {
|
||||
teaweb.popup(this.href, {
|
||||
height: this.height
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<span><slot></slot> <a href="" :title="title" @click.prevent="clickPrevent"><i class="icon clone outline small"></i></a></span>`
|
||||
})
|
||||
|
||||
// 小提示
|
||||
Vue.component("tip-icon", {
|
||||
props: ["content"],
|
||||
methods: {
|
||||
showTip: function () {
|
||||
teaweb.popupTip(this.content)
|
||||
}
|
||||
},
|
||||
template: `<a href="" title="查看帮助" @click.prevent="showTip"><i class="icon question circle"></i></a>`
|
||||
})
|
||||
|
||||
// 提交点击事件
|
||||
function emitClick(obj, arguments) {
|
||||
let event = "click"
|
||||
let newArgs = [event]
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
newArgs.push(arguments[i])
|
||||
}
|
||||
obj.$emit.apply(obj, newArgs)
|
||||
}
|
||||
45
EdgeUser/web/public/js/components/common/menu-item.js
Normal file
45
EdgeUser/web/public/js/components/common/menu-item.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 菜单项
|
||||
*/
|
||||
Vue.component("menu-item", {
|
||||
props: ["href", "active", "code"],
|
||||
data: function () {
|
||||
let active = this.active
|
||||
if (typeof (active) == "undefined") {
|
||||
var itemCode = ""
|
||||
if (typeof (window.TEA.ACTION.data.firstMenuItem) != "undefined") {
|
||||
itemCode = window.TEA.ACTION.data.firstMenuItem
|
||||
}
|
||||
if (itemCode != null && itemCode.length > 0 && this.code != null && this.code.length > 0) {
|
||||
if (itemCode.indexOf(",") > 0) {
|
||||
active = itemCode.split(",").$contains(this.code)
|
||||
} else {
|
||||
active = (itemCode == this.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let href = (this.href == null) ? "" : this.href
|
||||
if (typeof (href) == "string" && href.length > 0 && href.startsWith(".")) {
|
||||
let qIndex = href.indexOf("?")
|
||||
if (qIndex >= 0) {
|
||||
href = Tea.url(href.substring(0, qIndex)) + href.substring(qIndex)
|
||||
} else {
|
||||
href = Tea.url(href)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
vHref: href,
|
||||
vActive: active
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
click: function (e) {
|
||||
this.$emit("click", e)
|
||||
}
|
||||
},
|
||||
template: '\
|
||||
<a :href="vHref" class="item" :class="{active:vActive}" @click="click"><slot></slot></a> \
|
||||
'
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
Vue.component("more-options-angle", {
|
||||
data: function () {
|
||||
return {
|
||||
isVisible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show: function () {
|
||||
this.isVisible = !this.isVisible
|
||||
this.$emit("change", this.isVisible)
|
||||
}
|
||||
},
|
||||
template: `<a href="" @click.prevent="show()"><span v-if="!isVisible">更多选项</span><span v-if="isVisible">收起选项</span><i class="icon angle" :class="{down:!isVisible, up:isVisible}"></i></a>`
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 更多选项
|
||||
*/
|
||||
Vue.component("more-options-indicator", {
|
||||
data: function () {
|
||||
return {
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeVisible: function () {
|
||||
this.visible = !this.visible
|
||||
if (Tea.Vue != null) {
|
||||
Tea.Vue.moreOptionsVisible = this.visible
|
||||
}
|
||||
this.$emit("change", this.visible)
|
||||
this.$emit("input", this.visible)
|
||||
}
|
||||
},
|
||||
template: '<a href="" style="font-weight: normal" @click.prevent="changeVisible()"><slot><span v-if="!visible">更多选项</span><span v-if="visible">收起选项</span></slot> <i class="icon angle" :class="{down:!visible, up:visible}"></i> </a>'
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
Vue.component("more-options-tbody", {
|
||||
data: function () {
|
||||
return {
|
||||
isVisible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show: function () {
|
||||
this.isVisible = !this.isVisible
|
||||
this.$emit("change", this.isVisible)
|
||||
}
|
||||
},
|
||||
template: `<tbody>
|
||||
<tr>
|
||||
<td colspan="2"><a href="" @click.prevent="show()"><span v-if="!isVisible">更多选项</span><span v-if="isVisible">收起选项</span><i class="icon angle" :class="{down:!isVisible, up:isVisible}"></i></a></td>
|
||||
</tr>
|
||||
</tbody>`
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
Vue.component("network-addresses-box", {
|
||||
props: ["v-server-type", "v-addresses", "v-protocol", "v-name"],
|
||||
data: function () {
|
||||
let addresses = this.vAddresses
|
||||
if (addresses == null) {
|
||||
addresses = []
|
||||
}
|
||||
let protocol = this.vProtocol
|
||||
if (protocol == null) {
|
||||
protocol = ""
|
||||
}
|
||||
|
||||
let name = this.vName
|
||||
if (name == null) {
|
||||
name = "addresses"
|
||||
}
|
||||
|
||||
return {
|
||||
addresses: addresses,
|
||||
protocol: protocol,
|
||||
name: name
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"vServerType": function () {
|
||||
this.addresses = []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addAddr: function () {
|
||||
let that = this
|
||||
window.UPDATING_ADDR = null
|
||||
teaweb.popup("/servers/addPortPopup?serverType=" + this.vServerType + "&protocol=" + this.protocol, {
|
||||
height: "16em",
|
||||
callback: function (resp) {
|
||||
var addr = resp.data.address
|
||||
that.addresses.push(addr)
|
||||
if (["https", "https4", "https6"].$contains(addr.protocol)) {
|
||||
this.tlsProtocolName = "HTTPS"
|
||||
} else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) {
|
||||
this.tlsProtocolName = "TLS"
|
||||
}
|
||||
|
||||
// 发送事件
|
||||
that.$emit("change", that.addresses)
|
||||
}
|
||||
})
|
||||
},
|
||||
removeAddr: function (index) {
|
||||
this.addresses.$remove(index);
|
||||
|
||||
// 发送事件
|
||||
this.$emit("change", this.addresses)
|
||||
},
|
||||
updateAddr: function (index, addr) {
|
||||
let that = this
|
||||
window.UPDATING_ADDR = addr
|
||||
teaweb.popup("/servers/addPortPopup?serverType=" + this.vServerType + "&protocol=" + this.protocol, {
|
||||
height: "16em",
|
||||
callback: function (resp) {
|
||||
var addr = resp.data.address
|
||||
Vue.set(that.addresses, index, addr)
|
||||
|
||||
if (["https", "https4", "https6"].$contains(addr.protocol)) {
|
||||
this.tlsProtocolName = "HTTPS"
|
||||
} else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) {
|
||||
this.tlsProtocolName = "TLS"
|
||||
}
|
||||
|
||||
// 发送事件
|
||||
that.$emit("change", that.addresses)
|
||||
}
|
||||
})
|
||||
|
||||
// 发送事件
|
||||
this.$emit("change", this.addresses)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" :name="name" :value="JSON.stringify(addresses)"/>
|
||||
<div v-if="addresses.length > 0">
|
||||
<div class="ui label small basic" v-for="(addr, index) in addresses">
|
||||
{{addr.protocol}}://<span v-if="addr.host.length > 0">{{addr.host}}</span><span v-if="addr.host.length == 0">*</span>:{{addr.portRange}}
|
||||
<a href="" @click.prevent="updateAddr(index, addr)" title="修改"><i class="icon pencil small"></i></a>
|
||||
<a href="" @click.prevent="removeAddr(index)" title="删除"><i class="icon remove"></i></a> </div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<a href="" @click.prevent="addAddr()">[添加端口绑定]</a>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
Vue.component("network-addresses-view", {
|
||||
props: ["v-addresses"],
|
||||
template: `<div>
|
||||
<div class="ui label tiny basic" v-if="vAddresses != null" v-for="addr in vAddresses">
|
||||
{{addr.protocol}}://{{addr.host}}:{{addr.portRange}}
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
Vue.component("not-found-box", {
|
||||
props: ["message"],
|
||||
template: `<div style="text-align: center; margin-top: 5em;">
|
||||
<div style="font-size: 2em; margin-bottom: 1em"><i class="icon exclamation triangle large grey"></i></div>
|
||||
<p class="comment">{{message}}<slot></slot></p>
|
||||
</div>`
|
||||
})
|
||||
18
EdgeUser/web/public/js/components/common/page-box.js
Normal file
18
EdgeUser/web/public/js/components/common/page-box.js
Normal file
@@ -0,0 +1,18 @@
|
||||
Vue.component("page-box", {
|
||||
data: function () {
|
||||
return {
|
||||
page: ""
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
let that = this;
|
||||
setTimeout(function () {
|
||||
if (Tea && Tea.Vue && Tea.Vue.page) {
|
||||
that.page = Tea.Vue.page;
|
||||
}
|
||||
})
|
||||
},
|
||||
template: `<div>
|
||||
<div class="page" v-html="page"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
Vue.component("provinces-selector", {
|
||||
props: ["v-provinces"],
|
||||
data: function () {
|
||||
let provinces = this.vProvinces
|
||||
if (provinces == null) {
|
||||
provinces = []
|
||||
}
|
||||
let provinceIds = provinces.$map(function (k, v) {
|
||||
return v.id
|
||||
})
|
||||
return {
|
||||
provinces: provinces,
|
||||
provinceIds: provinceIds
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
let provinceStringIds = this.provinceIds.map(function (v) {
|
||||
return v.toString()
|
||||
})
|
||||
let that = this
|
||||
teaweb.popup("/ui/selectProvincesPopup?provinceIds=" + provinceStringIds.join(","), {
|
||||
width: "48em",
|
||||
height: "23em",
|
||||
callback: function (resp) {
|
||||
that.provinces = resp.data.provinces
|
||||
that.change()
|
||||
}
|
||||
})
|
||||
},
|
||||
remove: function (index) {
|
||||
this.provinces.$remove(index)
|
||||
this.change()
|
||||
},
|
||||
change: function () {
|
||||
this.provinceIds = this.provinces.$map(function (k, v) {
|
||||
return v.id
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="provinceIdsJSON" :value="JSON.stringify(provinceIds)"/>
|
||||
<div v-if="provinces.length > 0" style="margin-bottom: 0.5em">
|
||||
<div v-for="(province, index) in provinces" class="ui label tiny basic">{{province.name}} <a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove"></i></a></div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
23
EdgeUser/web/public/js/components/common/radio.js
Normal file
23
EdgeUser/web/public/js/components/common/radio.js
Normal file
@@ -0,0 +1,23 @@
|
||||
let radioId = 0
|
||||
Vue.component("radio", {
|
||||
props: ["name", "value", "v-value", "id"],
|
||||
data: function () {
|
||||
radioId++
|
||||
let elementId = this.id
|
||||
if (elementId == null) {
|
||||
elementId = "radio" + radioId
|
||||
}
|
||||
return {
|
||||
"elementId": elementId
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function () {
|
||||
this.$emit("input", this.vValue)
|
||||
}
|
||||
},
|
||||
template: `<div class="ui checkbox radio">
|
||||
<input type="radio" :name="name" :value="vValue" :id="elementId" @change="change" :checked="(vValue == value)"/>
|
||||
<label :for="elementId"><slot></slot></label>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
// 将变量转换为中文
|
||||
Vue.component("request-variables-describer", {
|
||||
data: function () {
|
||||
return {
|
||||
vars:[]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update: function (variablesString) {
|
||||
this.vars = []
|
||||
let that = this
|
||||
variablesString.replace(/\${.+?}/g, function (v) {
|
||||
let def = that.findVar(v)
|
||||
if (def == null) {
|
||||
return v
|
||||
}
|
||||
that.vars.push(def)
|
||||
})
|
||||
},
|
||||
findVar: function (name) {
|
||||
let def = null
|
||||
window.REQUEST_VARIABLES.forEach(function (v) {
|
||||
if (v.code == name) {
|
||||
def = v
|
||||
}
|
||||
})
|
||||
return def
|
||||
}
|
||||
},
|
||||
template: `<span>
|
||||
<span v-for="(v, index) in vars"><code-label :title="v.description">{{v.code}}</code-label> - {{v.name}}<span v-if="index < vars.length-1">;</span></span>
|
||||
</span>`
|
||||
})
|
||||
33
EdgeUser/web/public/js/components/common/search-box.js
Normal file
33
EdgeUser/web/public/js/components/common/search-box.js
Normal file
@@ -0,0 +1,33 @@
|
||||
Vue.component("search-box", {
|
||||
props: ["placeholder", "width"],
|
||||
data: function () {
|
||||
let width = this.width
|
||||
if (width == null) {
|
||||
width = "10em"
|
||||
}
|
||||
return {
|
||||
realWidth: width,
|
||||
realValue: ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onInput: function () {
|
||||
this.$emit("input", { value: this.realValue})
|
||||
this.$emit("change", { value: this.realValue})
|
||||
},
|
||||
clearValue: function () {
|
||||
this.realValue = ""
|
||||
this.focus()
|
||||
this.onInput()
|
||||
},
|
||||
focus: function () {
|
||||
this.$refs.valueRef.focus()
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div class="ui input small" :class="{'right labeled': realValue.length > 0}">
|
||||
<input type="text" :placeholder="placeholder" :style="{width: realWidth}" @input="onInput" v-model="realValue" ref="valueRef"/>
|
||||
<a href="" class="ui label blue" v-if="realValue.length > 0" @click.prevent="clearValue" style="padding-right: 0"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
12
EdgeUser/web/public/js/components/common/second-menu.js
Normal file
12
EdgeUser/web/public/js/components/common/second-menu.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 二级菜单
|
||||
*/
|
||||
Vue.component("second-menu", {
|
||||
template: ' \
|
||||
<div class="second-menu"> \
|
||||
<div class="ui menu text blue small">\
|
||||
<slot></slot>\
|
||||
</div> \
|
||||
<div class="ui divider"></div> \
|
||||
</div>'
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
Vue.component("server-group-selector", {
|
||||
props: ["v-groups"],
|
||||
data: function () {
|
||||
let groups = this.vGroups
|
||||
if (groups == null) {
|
||||
groups = []
|
||||
}
|
||||
return {
|
||||
groups: groups
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectGroup: function () {
|
||||
let that = this
|
||||
let groupIds = this.groups.map(function (v) {
|
||||
return v.id.toString()
|
||||
}).join(",")
|
||||
teaweb.popup("/servers/groups/selectPopup?selectedGroupIds=" + groupIds, {
|
||||
callback: function (resp) {
|
||||
that.groups.push(resp.data.group)
|
||||
}
|
||||
})
|
||||
},
|
||||
addGroup: function () {
|
||||
let that = this
|
||||
teaweb.popup("/servers/groups/createPopup", {
|
||||
callback: function (resp) {
|
||||
that.groups.push(resp.data.group)
|
||||
}
|
||||
})
|
||||
},
|
||||
removeGroup: function (index) {
|
||||
this.groups.$remove(index)
|
||||
},
|
||||
groupIds: function () {
|
||||
return this.groups.map(function (v) {
|
||||
return v.id
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div v-if="groups.length > 0">
|
||||
<div class="ui label small basic" v-if="groups.length > 0" v-for="(group, index) in groups">
|
||||
<input type="hidden" name="groupIds" :value="group.id"/>
|
||||
{{group.name}} <a href="" title="删除" @click.prevent="removeGroup(index)"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="" @click.prevent="selectGroup()">[选择分组]</a> <a href="" @click.prevent="addGroup()">[添加分组]</a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
Vue.component("size-capacity-box", {
|
||||
props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength"],
|
||||
data: function () {
|
||||
let v = this.vValue
|
||||
if (v == null) {
|
||||
v = {
|
||||
count: this.vCount,
|
||||
unit: this.vUnit
|
||||
}
|
||||
}
|
||||
if (typeof (v["count"]) != "number") {
|
||||
v["count"] = -1
|
||||
}
|
||||
|
||||
let vSize = this.size
|
||||
if (vSize == null) {
|
||||
vSize = 6
|
||||
}
|
||||
|
||||
let vMaxlength = this.maxlength
|
||||
if (vMaxlength == null) {
|
||||
vMaxlength = 10
|
||||
}
|
||||
|
||||
return {
|
||||
capacity: v,
|
||||
countString: (v.count >= 0) ? v.count.toString() : "",
|
||||
vSize: vSize,
|
||||
vMaxlength: vMaxlength
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"countString": function (newValue) {
|
||||
let value = newValue.trim()
|
||||
if (value.length == 0) {
|
||||
this.capacity.count = -1
|
||||
this.change()
|
||||
return
|
||||
}
|
||||
let count = parseInt(value)
|
||||
if (!isNaN(count)) {
|
||||
this.capacity.count = count
|
||||
}
|
||||
this.change()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function () {
|
||||
this.$emit("change", this.capacity)
|
||||
}
|
||||
},
|
||||
template: `<div class="ui fields inline">
|
||||
<input type="hidden" :name="vName" :value="JSON.stringify(capacity)"/>
|
||||
<div class="ui field">
|
||||
<input type="text" v-model="countString" :maxlength="vMaxlength" :size="vSize"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" v-model="capacity.unit" @change="change">
|
||||
<option value="byte">字节</option>
|
||||
<option value="kb">KiB</option>
|
||||
<option value="mb">MiB</option>
|
||||
<option value="gb">GiB</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
Vue.component("size-capacity-view", {
|
||||
props:["v-default-text", "v-value"],
|
||||
methods: {
|
||||
composeCapacity: function (capacity) {
|
||||
return teaweb.convertSizeCapacityToString(capacity)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<span v-if="vValue != null && vValue.count > 0">{{composeCapacity(vValue)}}</span>
|
||||
<span v-else>{{vDefaultText}}</span>
|
||||
</div>`
|
||||
})
|
||||
53
EdgeUser/web/public/js/components/common/sort-arrow.js
Normal file
53
EdgeUser/web/public/js/components/common/sort-arrow.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// 排序使用的箭头
|
||||
Vue.component("sort-arrow", {
|
||||
props: ["name"],
|
||||
data: function () {
|
||||
let url = window.location.toString()
|
||||
let order = ""
|
||||
let iconTitle = ""
|
||||
let newArgs = []
|
||||
if (window.location.search != null && window.location.search.length > 0) {
|
||||
let queryString = window.location.search.substring(1)
|
||||
let pieces = queryString.split("&")
|
||||
let that = this
|
||||
pieces.forEach(function (v) {
|
||||
let eqIndex = v.indexOf("=")
|
||||
if (eqIndex > 0) {
|
||||
let argName = v.substring(0, eqIndex)
|
||||
let argValue = v.substring(eqIndex + 1)
|
||||
if (argName == that.name) {
|
||||
order = argValue
|
||||
} else if (argName != "page" && argValue != "asc" && argValue != "desc") {
|
||||
newArgs.push(v)
|
||||
}
|
||||
} else {
|
||||
newArgs.push(v)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (order == "asc") {
|
||||
newArgs.push(this.name + "=desc")
|
||||
iconTitle = "当前正序排列"
|
||||
} else if (order == "desc") {
|
||||
newArgs.push(this.name + "=asc")
|
||||
iconTitle = "当前倒序排列"
|
||||
} else {
|
||||
newArgs.push(this.name + "=desc")
|
||||
iconTitle = "当前正序排列"
|
||||
}
|
||||
|
||||
let qIndex = url.indexOf("?")
|
||||
if (qIndex > 0) {
|
||||
url = url.substring(0, qIndex) + "?" + newArgs.join("&")
|
||||
} else {
|
||||
url = url + "?" + newArgs.join("&")
|
||||
}
|
||||
|
||||
return {
|
||||
order: order,
|
||||
url: url,
|
||||
iconTitle: iconTitle
|
||||
}
|
||||
},
|
||||
template: `<a :href="url" :title="iconTitle"> <i class="ui icon long arrow small" :class="{down: order == 'asc', up: order == 'desc', 'down grey': order == '' || order == null}"></i></a>`
|
||||
})
|
||||
39
EdgeUser/web/public/js/components/common/sortable.js
Normal file
39
EdgeUser/web/public/js/components/common/sortable.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// 给Table增加排序功能
|
||||
function sortTable(callback) {
|
||||
// 引入js
|
||||
let jsFile = document.createElement("script")
|
||||
jsFile.setAttribute("src", "/js/sortable.min.js")
|
||||
jsFile.addEventListener("load", function () {
|
||||
// 初始化
|
||||
let box = document.querySelector("#sortable-table")
|
||||
if (box == null) {
|
||||
return
|
||||
}
|
||||
Sortable.create(box, {
|
||||
draggable: "tbody",
|
||||
handle: ".icon.handle",
|
||||
onStart: function () {
|
||||
},
|
||||
onUpdate: function (event) {
|
||||
let rows = box.querySelectorAll("tbody")
|
||||
let rowIds = []
|
||||
rows.forEach(function (row) {
|
||||
rowIds.push(parseInt(row.getAttribute("v-id")))
|
||||
})
|
||||
callback(rowIds)
|
||||
}
|
||||
})
|
||||
})
|
||||
document.head.appendChild(jsFile)
|
||||
}
|
||||
|
||||
function sortLoad(callback) {
|
||||
let jsFile = document.createElement("script")
|
||||
jsFile.setAttribute("src", "/js/sortable.min.js")
|
||||
jsFile.addEventListener("load", function () {
|
||||
if (typeof (callback) == "function") {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
document.head.appendChild(jsFile)
|
||||
}
|
||||
93
EdgeUser/web/public/js/components/common/source-code-box.js
Normal file
93
EdgeUser/web/public/js/components/common/source-code-box.js
Normal file
@@ -0,0 +1,93 @@
|
||||
let sourceCodeBoxIndex = 0
|
||||
|
||||
Vue.component("source-code-box", {
|
||||
props: ["name", "type", "id", "read-only", "width", "height", "focus"],
|
||||
mounted: function () {
|
||||
let readOnly = this.readOnly
|
||||
if (typeof readOnly != "boolean") {
|
||||
readOnly = true
|
||||
}
|
||||
let box = document.getElementById("source-code-box-" + this.index)
|
||||
let valueBox = document.getElementById(this.valueBoxId)
|
||||
let value = ""
|
||||
if (valueBox.textContent != null) {
|
||||
value = valueBox.textContent
|
||||
} else if (valueBox.innerText != null) {
|
||||
value = valueBox.innerText
|
||||
}
|
||||
|
||||
this.createEditor(box, value, readOnly)
|
||||
},
|
||||
data: function () {
|
||||
let index = sourceCodeBoxIndex++
|
||||
|
||||
let valueBoxId = 'source-code-box-value-' + sourceCodeBoxIndex
|
||||
if (this.id != null) {
|
||||
valueBoxId = this.id
|
||||
}
|
||||
|
||||
return {
|
||||
index: index,
|
||||
valueBoxId: valueBoxId
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createEditor: function (box, value, readOnly) {
|
||||
let boxEditor = CodeMirror.fromTextArea(box, {
|
||||
theme: "idea",
|
||||
lineNumbers: true,
|
||||
value: "",
|
||||
readOnly: readOnly,
|
||||
showCursorWhenSelecting: true,
|
||||
height: "auto",
|
||||
//scrollbarStyle: null,
|
||||
viewportMargin: Infinity,
|
||||
lineWrapping: true,
|
||||
highlightFormatting: false,
|
||||
indentUnit: 4,
|
||||
indentWithTabs: true,
|
||||
})
|
||||
let that = this
|
||||
boxEditor.on("change", function () {
|
||||
that.change(boxEditor.getValue())
|
||||
})
|
||||
boxEditor.setValue(value)
|
||||
|
||||
if (this.focus) {
|
||||
boxEditor.focus()
|
||||
}
|
||||
|
||||
let width = this.width
|
||||
let height = this.height
|
||||
if (width != null && height != null) {
|
||||
width = parseInt(width)
|
||||
height = parseInt(height)
|
||||
if (!isNaN(width) && !isNaN(height)) {
|
||||
if (width <= 0) {
|
||||
width = box.parentNode.offsetWidth
|
||||
}
|
||||
boxEditor.setSize(width, height)
|
||||
}
|
||||
} else if (height != null) {
|
||||
height = parseInt(height)
|
||||
if (!isNaN(height)) {
|
||||
boxEditor.setSize("100%", height)
|
||||
}
|
||||
}
|
||||
|
||||
let info = CodeMirror.findModeByMIME(this.type)
|
||||
if (info != null) {
|
||||
boxEditor.setOption("mode", info.mode)
|
||||
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js"
|
||||
CodeMirror.autoLoadMode(boxEditor, info.mode)
|
||||
}
|
||||
},
|
||||
change: function (code) {
|
||||
this.$emit("change", code)
|
||||
}
|
||||
},
|
||||
template: `<div class="source-code-box">
|
||||
<div style="display: none" :id="valueBoxId"><slot></slot></div>
|
||||
<textarea :id="'source-code-box-' + index" :name="name"></textarea>
|
||||
</div>`
|
||||
})
|
||||
6
EdgeUser/web/public/js/components/common/submit-btn.js
Normal file
6
EdgeUser/web/public/js/components/common/submit-btn.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 保存按钮
|
||||
*/
|
||||
Vue.component("submit-btn", {
|
||||
template: '<button class="ui button primary" type="submit"><slot>保存</slot></button>'
|
||||
});
|
||||
292
EdgeUser/web/public/js/components/common/theme-color-picker.js
Normal file
292
EdgeUser/web/public/js/components/common/theme-color-picker.js
Normal file
@@ -0,0 +1,292 @@
|
||||
Vue.component("theme-color-picker", {
|
||||
props: ["v-model"],
|
||||
data: function () {
|
||||
return {
|
||||
showPicker: false,
|
||||
r: 20,
|
||||
g: 83,
|
||||
b: 196,
|
||||
hex: "#14539A",
|
||||
clickHandler: null
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
console.log("Theme color picker component mounted, element:", this.$el)
|
||||
// 从localStorage加载保存的颜色
|
||||
let savedColor = localStorage.getItem("themeColor")
|
||||
if (savedColor) {
|
||||
this.setColor(savedColor)
|
||||
} else {
|
||||
// 从当前页面获取默认颜色
|
||||
let defaultColor = this.getCurrentThemeColor()
|
||||
if (defaultColor) {
|
||||
this.setColor(defaultColor)
|
||||
}
|
||||
}
|
||||
|
||||
// 应用颜色
|
||||
this.applyColor()
|
||||
},
|
||||
beforeDestroy: function() {
|
||||
// 移除点击监听器
|
||||
if (this.clickHandler) {
|
||||
document.removeEventListener("click", this.clickHandler)
|
||||
this.clickHandler = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 打开/关闭颜色选择器
|
||||
togglePicker: function (e) {
|
||||
if (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
console.log("Toggle picker clicked, current showPicker:", this.showPicker)
|
||||
this.showPicker = !this.showPicker
|
||||
console.log("Toggle picker clicked, new showPicker:", this.showPicker)
|
||||
console.log("Popup should be visible:", this.showPicker)
|
||||
|
||||
// 管理外部点击监听器
|
||||
if (this.showPicker) {
|
||||
// 打开时,延迟添加监听器
|
||||
let that = this
|
||||
setTimeout(function() {
|
||||
if (!that.clickHandler) {
|
||||
that.clickHandler = function(e) {
|
||||
if (that.showPicker && !that.$el.contains(e.target)) {
|
||||
that.showPicker = false
|
||||
}
|
||||
}
|
||||
document.addEventListener("click", that.clickHandler)
|
||||
}
|
||||
}, 100)
|
||||
} else {
|
||||
// 关闭时,移除监听器
|
||||
if (this.clickHandler) {
|
||||
document.removeEventListener("click", this.clickHandler)
|
||||
this.clickHandler = null
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// RGB转16进制
|
||||
rgbToHex: function (r, g, b) {
|
||||
return "#" + [r, g, b].map(function (x) {
|
||||
x = parseInt(x)
|
||||
const hex = x.toString(16)
|
||||
return hex.length === 1 ? "0" + hex : hex
|
||||
}).join("").toUpperCase()
|
||||
},
|
||||
|
||||
// 16进制转RGB
|
||||
hexToRgb: function (hex) {
|
||||
hex = hex.replace("#", "")
|
||||
const r = parseInt(hex.substring(0, 2), 16)
|
||||
const g = parseInt(hex.substring(2, 4), 16)
|
||||
const b = parseInt(hex.substring(4, 6), 16)
|
||||
return { r: r, g: g, b: b }
|
||||
},
|
||||
|
||||
// 设置颜色(支持hex或rgb对象)
|
||||
setColor: function (color) {
|
||||
if (typeof color === "string") {
|
||||
if (color.startsWith("#")) {
|
||||
let rgb = this.hexToRgb(color)
|
||||
this.r = rgb.r
|
||||
this.g = rgb.g
|
||||
this.b = rgb.b
|
||||
this.hex = color.toUpperCase()
|
||||
} else {
|
||||
// 假设是16进制不带#号
|
||||
this.setColor("#" + color)
|
||||
}
|
||||
} else if (typeof color === "object" && color.r !== undefined) {
|
||||
this.r = color.r
|
||||
this.g = color.g
|
||||
this.b = color.b
|
||||
this.hex = this.rgbToHex(color.r, color.g, color.b)
|
||||
}
|
||||
},
|
||||
|
||||
// RGB值变化时更新hex
|
||||
updateHex: function () {
|
||||
this.hex = this.rgbToHex(this.r, this.g, this.b)
|
||||
this.applyColor()
|
||||
},
|
||||
|
||||
// Hex值变化时更新RGB
|
||||
updateRgb: function () {
|
||||
if (this.hex.match(/^#[0-9A-Fa-f]{6}$/)) {
|
||||
let rgb = this.hexToRgb(this.hex)
|
||||
this.r = rgb.r
|
||||
this.g = rgb.g
|
||||
this.b = rgb.b
|
||||
this.applyColor()
|
||||
}
|
||||
},
|
||||
|
||||
// 应用颜色到整个网站
|
||||
applyColor: function () {
|
||||
let color = this.rgbToHex(this.r, this.g, this.b)
|
||||
|
||||
// 保存到localStorage
|
||||
localStorage.setItem("themeColor", color)
|
||||
|
||||
// 创建或更新样式
|
||||
let styleId = "theme-color-custom"
|
||||
let styleEl = document.getElementById(styleId)
|
||||
if (!styleEl) {
|
||||
styleEl = document.createElement("style")
|
||||
styleEl.id = styleId
|
||||
document.head.appendChild(styleEl)
|
||||
}
|
||||
|
||||
// 应用颜色到主题元素
|
||||
styleEl.textContent = `
|
||||
.top-nav, .main-menu, .main-menu .menu {
|
||||
background: ${color} !important;
|
||||
}
|
||||
.main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless {
|
||||
background: ${color} !important;
|
||||
}
|
||||
.main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .item {
|
||||
background: ${color} !important;
|
||||
}
|
||||
.main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .sub-items {
|
||||
background: ${color} !important;
|
||||
}
|
||||
.main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .sub-items .item {
|
||||
background: ${color} !important;
|
||||
}
|
||||
.main-menu .ui.menu .sub-items {
|
||||
background: ${color} !important;
|
||||
}
|
||||
.main-menu .ui.menu .sub-items .item {
|
||||
background: ${color} !important;
|
||||
}
|
||||
.main-menu .ui.labeled.menu .sub-items {
|
||||
background: ${color} !important;
|
||||
}
|
||||
.main-menu .ui.labeled.menu .sub-items .item {
|
||||
background: ${color} !important;
|
||||
}
|
||||
.main-menu .sub-items {
|
||||
background: ${color} !important;
|
||||
}
|
||||
.main-menu .sub-items .item {
|
||||
background: ${color} !important;
|
||||
}
|
||||
.ui.menu.blue.inverted {
|
||||
background: ${color} !important;
|
||||
}
|
||||
.ui.menu.blue.inverted .item {
|
||||
background: ${color} !important;
|
||||
}
|
||||
.ui.menu.vertical.blue.inverted {
|
||||
background: ${color} !important;
|
||||
}
|
||||
.ui.menu.vertical.blue.inverted .item {
|
||||
background: ${color} !important;
|
||||
}
|
||||
.ui.labeled.menu.vertical.blue.inverted {
|
||||
background: ${color} !important;
|
||||
}
|
||||
.ui.labeled.menu.vertical.blue.inverted .item {
|
||||
background: ${color} !important;
|
||||
}
|
||||
`
|
||||
|
||||
// 触发事件通知其他组件
|
||||
if (window.Tea && window.Tea.Vue) {
|
||||
window.Tea.Vue.$emit("theme-color-changed", color)
|
||||
}
|
||||
|
||||
// 更新v-model(如果使用)
|
||||
this.$emit("input", color.replace("#", ""))
|
||||
},
|
||||
|
||||
// 获取当前主题颜色
|
||||
getCurrentThemeColor: function () {
|
||||
let topNav = document.querySelector(".top-nav")
|
||||
if (topNav) {
|
||||
let bgColor = window.getComputedStyle(topNav).backgroundColor
|
||||
if (bgColor && bgColor !== "rgba(0, 0, 0, 0)" && bgColor !== "transparent") {
|
||||
// 转换rgb/rgba为hex
|
||||
let match = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/)
|
||||
if (match) {
|
||||
return this.rgbToHex(parseInt(match[1]), parseInt(match[2]), parseInt(match[3]))
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
// 重置为默认颜色
|
||||
resetColor: function () {
|
||||
this.setColor("#14539A") // 默认蓝色
|
||||
this.applyColor()
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="theme-color-picker-wrapper" style="position: relative; display: inline-block !important; visibility: visible !important;">
|
||||
<a href="javascript:void(0)" class="item" @click="togglePicker" title="自定义主题颜色">
|
||||
<i class="icon paint brush" :style="'background-color: ' + hex + ' !important; border-radius: 4px; padding: 0.2em 0.3em; margin-right: 0.3em; display: inline-block;'"></i>主题色
|
||||
</a>
|
||||
|
||||
<div v-if="showPicker" class="theme-color-picker-popup" style="position: fixed !important; top: auto !important; bottom: auto !important; left: auto !important; right: 20px !important; margin-top: 0.5em; background: white !important; border: 1px solid #ddd !important; border-radius: 4px !important; padding: 1em !important; box-shadow: 0 2px 8px rgba(0,0,0,0.15) !important; z-index: 99999 !important; min-width: 280px !important; display: block !important; visibility: visible !important; opacity: 1 !important;">
|
||||
<div style="margin-bottom: 1em;">
|
||||
<strong>RGB颜色调节</strong>
|
||||
</div>
|
||||
|
||||
<!-- 颜色预览 -->
|
||||
<div style="margin-bottom: 1em; text-align: center;">
|
||||
<div :style="'width: 100%; height: 60px; background-color: ' + hex + '; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 0.5em;'"></div>
|
||||
<div style="font-size: 0.9em; color: #666;">
|
||||
{{ hex }} | RGB({{ r }}, {{ g }}, {{ b }})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- R滑块 -->
|
||||
<div style="margin-bottom: 1em;">
|
||||
<label style="display: flex; align-items: center; margin-bottom: 0.3em;">
|
||||
<span style="width: 30px; font-weight: bold; color: #db2828;">R:</span>
|
||||
<input type="range" min="0" max="255" v-model.number="r" @input="updateHex" style="flex: 1; margin: 0 0.5em;" />
|
||||
<input type="number" min="0" max="255" v-model.number="r" @input="updateHex" style="width: 60px; text-align: center;" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- G滑块 -->
|
||||
<div style="margin-bottom: 1em;">
|
||||
<label style="display: flex; align-items: center; margin-bottom: 0.3em;">
|
||||
<span style="width: 30px; font-weight: bold; color: #21ba45;">G:</span>
|
||||
<input type="range" min="0" max="255" v-model.number="g" @input="updateHex" style="flex: 1; margin: 0 0.5em;" />
|
||||
<input type="number" min="0" max="255" v-model.number="g" @input="updateHex" style="width: 60px; text-align: center;" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- B滑块 -->
|
||||
<div style="margin-bottom: 1em;">
|
||||
<label style="display: flex; align-items: center; margin-bottom: 0.3em;">
|
||||
<span style="width: 30px; font-weight: bold; color: #2185d0;">B:</span>
|
||||
<input type="range" min="0" max="255" v-model.number="b" @input="updateHex" style="flex: 1; margin: 0 0.5em;" />
|
||||
<input type="number" min="0" max="255" v-model.number="b" @input="updateHex" style="width: 60px; text-align: center;" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 16进制输入 -->
|
||||
<div style="margin-bottom: 1em;">
|
||||
<label style="display: block; margin-bottom: 0.3em; font-size: 0.9em;">16进制颜色:</label>
|
||||
<div class="ui input" style="width: 100%;">
|
||||
<input type="text" v-model="hex" @input="updateRgb" maxlength="7" placeholder="#000000" style="width: 100%; text-transform: uppercase;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div style="display: flex; gap: 0.5em;">
|
||||
<button type="button" class="ui button small" @click="resetColor" style="flex: 1;">重置默认</button>
|
||||
<button type="button" class="ui button small primary" @click="showPicker = false" style="flex: 1;">完成</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
119
EdgeUser/web/public/js/components/common/time-duration-box.js
Normal file
119
EdgeUser/web/public/js/components/common/time-duration-box.js
Normal file
@@ -0,0 +1,119 @@
|
||||
Vue.component("time-duration-box", {
|
||||
props: ["v-name", "v-value", "v-count", "v-unit", "placeholder", "v-min-unit", "maxlength"],
|
||||
mounted: function () {
|
||||
this.change()
|
||||
},
|
||||
data: function () {
|
||||
let v = this.vValue
|
||||
if (v == null) {
|
||||
v = {
|
||||
count: this.vCount,
|
||||
unit: this.vUnit
|
||||
}
|
||||
}
|
||||
if (typeof (v["count"]) != "number") {
|
||||
v["count"] = -1
|
||||
}
|
||||
|
||||
let minUnit = this.vMinUnit
|
||||
let units = [
|
||||
{
|
||||
code: "ms",
|
||||
name: "毫秒"
|
||||
},
|
||||
{
|
||||
code: "second",
|
||||
name: "秒"
|
||||
},
|
||||
{
|
||||
code: "minute",
|
||||
name: "分钟"
|
||||
},
|
||||
{
|
||||
code: "hour",
|
||||
name: "小时"
|
||||
},
|
||||
{
|
||||
code: "day",
|
||||
name: "天"
|
||||
}
|
||||
]
|
||||
let minUnitIndex = -1
|
||||
if (minUnit != null && typeof minUnit == "string" && minUnit.length > 0) {
|
||||
for (let i = 0; i < units.length; i++) {
|
||||
if (units[i].code == minUnit) {
|
||||
minUnitIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (minUnitIndex > -1) {
|
||||
units = units.slice(minUnitIndex)
|
||||
}
|
||||
|
||||
let maxLength = parseInt(this.maxlength)
|
||||
if (typeof maxLength != "number") {
|
||||
maxLength = 10
|
||||
}
|
||||
|
||||
return {
|
||||
duration: v,
|
||||
countString: (v.count >= 0) ? v.count.toString() : "",
|
||||
units: units,
|
||||
realMaxLength: maxLength
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"countString": function (newValue) {
|
||||
let value = newValue.trim()
|
||||
if (value.length == 0) {
|
||||
this.duration.count = -1
|
||||
return
|
||||
}
|
||||
let count = parseInt(value)
|
||||
if (!isNaN(count)) {
|
||||
this.duration.count = count
|
||||
}
|
||||
this.change()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function () {
|
||||
this.$emit("change", this.duration)
|
||||
}
|
||||
},
|
||||
template: `<div class="ui fields inline" style="padding-bottom: 0; margin-bottom: 0">
|
||||
<input type="hidden" :name="vName" :value="JSON.stringify(duration)"/>
|
||||
<div class="ui field">
|
||||
<input type="text" v-model="countString" :maxlength="realMaxLength" :size="realMaxLength" :placeholder="placeholder" @keypress.enter.prevent="1"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" v-model="duration.unit" @change="change">
|
||||
<option v-for="unit in units" :value="unit.code">{{unit.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
|
||||
Vue.component("time-duration-text", {
|
||||
props: ["v-value"],
|
||||
methods: {
|
||||
unitName: function (unit) {
|
||||
switch (unit) {
|
||||
case "ms":
|
||||
return "毫秒"
|
||||
case "second":
|
||||
return "秒"
|
||||
case "minute":
|
||||
return "分钟"
|
||||
case "hour":
|
||||
return "小时"
|
||||
case "day":
|
||||
return "天"
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<span>
|
||||
{{vValue.count}} {{unitName(vValue.unit)}}
|
||||
</span>`
|
||||
})
|
||||
149
EdgeUser/web/public/js/components/common/url-patterns-box.js
Normal file
149
EdgeUser/web/public/js/components/common/url-patterns-box.js
Normal file
@@ -0,0 +1,149 @@
|
||||
Vue.component("url-patterns-box", {
|
||||
props: ["value"],
|
||||
data: function () {
|
||||
let patterns = []
|
||||
if (this.value != null) {
|
||||
patterns = this.value
|
||||
}
|
||||
|
||||
return {
|
||||
patterns: patterns,
|
||||
isAdding: false,
|
||||
|
||||
addingPattern: {"type": "wildcard", "pattern": ""},
|
||||
editingIndex: -1,
|
||||
|
||||
patternIsInvalid: false,
|
||||
|
||||
windowIsSmall: window.innerWidth < 600
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
this.isAdding = true
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.patternInput.focus()
|
||||
})
|
||||
},
|
||||
edit: function (index) {
|
||||
this.isAdding = true
|
||||
this.editingIndex = index
|
||||
this.addingPattern = {
|
||||
type: this.patterns[index].type,
|
||||
pattern: this.patterns[index].pattern
|
||||
}
|
||||
},
|
||||
confirm: function () {
|
||||
if (this.requireURL(this.addingPattern.type)) {
|
||||
let pattern = this.addingPattern.pattern.trim()
|
||||
if (pattern.length == 0) {
|
||||
let that = this
|
||||
teaweb.warn("请输入URL", function () {
|
||||
that.$refs.patternInput.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if (this.editingIndex < 0) {
|
||||
this.patterns.push({
|
||||
type: this.addingPattern.type,
|
||||
pattern: this.addingPattern.pattern
|
||||
})
|
||||
} else {
|
||||
this.patterns[this.editingIndex].type = this.addingPattern.type
|
||||
this.patterns[this.editingIndex].pattern = this.addingPattern.pattern
|
||||
}
|
||||
this.notifyChange()
|
||||
this.cancel()
|
||||
},
|
||||
remove: function (index) {
|
||||
this.patterns.$remove(index)
|
||||
this.cancel()
|
||||
this.notifyChange()
|
||||
},
|
||||
cancel: function () {
|
||||
this.isAdding = false
|
||||
this.addingPattern = {"type": "wildcard", "pattern": ""}
|
||||
this.editingIndex = -1
|
||||
},
|
||||
patternTypeName: function (patternType) {
|
||||
switch (patternType) {
|
||||
case "wildcard":
|
||||
return "通配符"
|
||||
case "regexp":
|
||||
return "正则"
|
||||
case "images":
|
||||
return "常见图片文件"
|
||||
case "audios":
|
||||
return "常见音频文件"
|
||||
case "videos":
|
||||
return "常见视频文件"
|
||||
}
|
||||
return ""
|
||||
},
|
||||
notifyChange: function () {
|
||||
this.$emit("input", this.patterns)
|
||||
},
|
||||
changePattern: function () {
|
||||
this.patternIsInvalid = false
|
||||
let pattern = this.addingPattern.pattern
|
||||
switch (this.addingPattern.type) {
|
||||
case "wildcard":
|
||||
if (pattern.indexOf("?") >= 0) {
|
||||
this.patternIsInvalid = true
|
||||
}
|
||||
break
|
||||
case "regexp":
|
||||
if (pattern.indexOf("?") >= 0) {
|
||||
let pieces = pattern.split("?")
|
||||
for (let i = 0; i < pieces.length - 1; i++) {
|
||||
if (pieces[i].length == 0 || pieces[i][pieces[i].length - 1] != "\\") {
|
||||
this.patternIsInvalid = true
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
requireURL: function (patternType) {
|
||||
return patternType == "wildcard" || patternType == "regexp"
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div v-show="patterns.length > 0">
|
||||
<div v-for="(pattern, index) in patterns" class="ui label basic small" :class="{blue: index == editingIndex, disabled: isAdding && index != editingIndex}" style="margin-bottom: 0.8em">
|
||||
<span class="grey" style="font-weight: normal">[{{patternTypeName(pattern.type)}}]</span> <span >{{pattern.pattern}}</span>
|
||||
<a href="" title="修改" @click.prevent="edit(index)"><i class="icon pencil tiny"></i></a>
|
||||
<a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="isAdding" style="margin-top: 0.5em">
|
||||
<div :class="{'ui fields inline': !windowIsSmall}">
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown auto-width" v-model="addingPattern.type">
|
||||
<option value="wildcard">通配符</option>
|
||||
<option value="regexp">正则表达式</option>
|
||||
<option value="images">常见图片</option>
|
||||
<option value="audios">常见音频</option>
|
||||
<option value="videos">常见视频</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui field" v-show="addingPattern.type == 'wildcard' || addingPattern.type == 'regexp'">
|
||||
<input type="text" :placeholder="(addingPattern.type == 'wildcard') ? '可以使用星号(*)通配符,不区分大小写' : '可以使用正则表达式,不区分大小写'" v-model="addingPattern.pattern" @input="changePattern" size="36" ref="patternInput" @keyup.enter="confirm()" @keypress.enter.prevent="1" spellcheck="false"/>
|
||||
<p class="comment" v-if="patternIsInvalid"><span class="red" style="font-weight: normal"><span v-if="addingPattern.type == 'wildcard'">通配符</span><span v-if="addingPattern.type == 'regexp'">正则表达式</span>中不能包含问号(?)及问号以后的内容。</span></p>
|
||||
</div>
|
||||
<div class="ui field" style="padding-left: 0" v-show="addingPattern.type == 'wildcard' || addingPattern.type == 'regexp'">
|
||||
<tip-icon content="通配符示例:<br/>单个路径开头:/hello/world/*<br/>单个路径结尾:*/hello/world<br/>包含某个路径:*/article/*<br/>某个域名下的所有URL:*example.com/*<br/>忽略某个扩展名:*.js" v-if="addingPattern.type == 'wildcard'"></tip-icon>
|
||||
<tip-icon content="正则表达式示例:<br/>单个路径开头:^/hello/world<br/>单个路径结尾:/hello/world$<br/>包含某个路径:/article/<br/>匹配某个数字路径:/article/(\\d+)<br/>某个域名下的所有URL:^(http|https)://example.com/" v-if="addingPattern.type == 'regexp'"></tip-icon>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button tiny" :class="{disabled:this.patternIsInvalid}" type="button" @click.prevent="confirm">确定</button><a href="" title="取消" @click.prevent="cancel"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if=!isAdding style="margin-top: 0.5em">
|
||||
<button class="ui button tiny basic" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
132
EdgeUser/web/public/js/components/common/values-box.js
Normal file
132
EdgeUser/web/public/js/components/common/values-box.js
Normal file
@@ -0,0 +1,132 @@
|
||||
Vue.component("values-box", {
|
||||
props: ["values", "v-values", "size", "maxlength", "name", "placeholder", "v-allow-empty", "validator"],
|
||||
data: function () {
|
||||
let values = this.values;
|
||||
if (values == null) {
|
||||
values = [];
|
||||
}
|
||||
|
||||
if (this.vValues != null && typeof this.vValues == "object") {
|
||||
values = this.vValues
|
||||
}
|
||||
|
||||
return {
|
||||
"realValues": values,
|
||||
"isUpdating": false,
|
||||
"isAdding": false,
|
||||
"index": 0,
|
||||
"value": "",
|
||||
isEditing: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
create: function () {
|
||||
this.isAdding = true;
|
||||
var that = this;
|
||||
setTimeout(function () {
|
||||
that.$refs.value.focus();
|
||||
}, 200);
|
||||
},
|
||||
update: function (index) {
|
||||
this.cancel()
|
||||
this.isUpdating = true;
|
||||
this.index = index;
|
||||
this.value = this.realValues[index];
|
||||
var that = this;
|
||||
setTimeout(function () {
|
||||
that.$refs.value.focus();
|
||||
}, 200);
|
||||
},
|
||||
confirm: function () {
|
||||
if (this.value.length == 0) {
|
||||
if (typeof(this.vAllowEmpty) != "boolean" || !this.vAllowEmpty) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// validate
|
||||
if (typeof(this.validator) == "function") {
|
||||
let resp = this.validator.call(this, this.value)
|
||||
if (typeof resp == "object") {
|
||||
if (typeof resp.isOk == "boolean" && !resp.isOk) {
|
||||
if (typeof resp.message == "string") {
|
||||
let that = this
|
||||
teaweb.warn(resp.message, function () {
|
||||
that.$refs.value.focus();
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isUpdating) {
|
||||
Vue.set(this.realValues, this.index, this.value);
|
||||
} else {
|
||||
this.realValues.push(this.value);
|
||||
}
|
||||
this.cancel()
|
||||
this.$emit("change", this.realValues)
|
||||
},
|
||||
remove: function (index) {
|
||||
this.realValues.$remove(index)
|
||||
this.$emit("change", this.realValues)
|
||||
},
|
||||
cancel: function () {
|
||||
this.isUpdating = false;
|
||||
this.isAdding = false;
|
||||
this.value = "";
|
||||
},
|
||||
updateAll: function (values) {
|
||||
this.realValues = values
|
||||
},
|
||||
addValue: function (v) {
|
||||
this.realValues.push(v)
|
||||
},
|
||||
|
||||
startEditing: function () {
|
||||
this.isEditing = !this.isEditing
|
||||
},
|
||||
allValues: function () {
|
||||
return this.realValues
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div v-show="!isEditing && realValues.length > 0">
|
||||
<div class="ui label tiny basic" v-for="(value, index) in realValues" style="margin-top:0.4em;margin-bottom:0.4em">
|
||||
<span v-if="value.toString().length > 0">{{value}}</span>
|
||||
<span v-if="value.toString().length == 0" class="disabled">[空]</span>
|
||||
</div>
|
||||
<a href="" @click.prevent="startEditing" style="font-size: 0.8em; margin-left: 0.2em">[修改]</a>
|
||||
</div>
|
||||
<div v-show="isEditing || realValues.length == 0">
|
||||
<div style="margin-bottom: 1em" v-if="realValues.length > 0">
|
||||
<div class="ui label tiny basic" v-for="(value, index) in realValues" style="margin-top:0.4em;margin-bottom:0.4em">
|
||||
<span v-if="value.toString().length > 0">{{value}}</span>
|
||||
<span v-if="value.toString().length == 0" class="disabled">[空]</span>
|
||||
<input type="hidden" :name="name" :value="value"/>
|
||||
<a href="" @click.prevent="update(index)" title="修改"><i class="icon pencil small" ></i></a>
|
||||
<a href="" @click.prevent="remove(index)" title="删除"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<!-- 添加|修改 -->
|
||||
<div v-if="isAdding || isUpdating">
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" :size="size" :maxlength="maxlength" :placeholder="placeholder" v-model="value" ref="value" @keyup.enter="confirm()" @keypress.enter.prevent="1"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button small" type="button" @click.prevent="confirm()">确定</button>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<a href="" @click.prevent="cancel()" title="取消"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isAdding && !isUpdating">
|
||||
<button class="ui button tiny" type="button" @click.prevent="create()">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
// 警告消息
|
||||
Vue.component("warning-message", {
|
||||
template: `<div class="ui icon message warning"><i class="icon warning circle"></i><div class="content"><slot></slot></div></div>`
|
||||
})
|
||||
Reference in New Issue
Block a user