mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-07-01 12:32:09 +00:00
Added drag'n'drop for routes (#1915)
* Added drag'n'drop for routes * Drop handler works only for local dnd events * Cleanup console.log
This commit is contained in:
parent
688ae4da10
commit
8b64180136
2 changed files with 229 additions and 6 deletions
218
web/html/xui/component/sortableTable.html
Normal file
218
web/html/xui/component/sortableTable.html
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
{{define "component/sortableTableTrigger"}}
|
||||||
|
<a-icon type="drag"
|
||||||
|
style="cursor: move;"
|
||||||
|
@mouseup="mouseUpHandler"
|
||||||
|
@mousedown="mouseDownHandler"
|
||||||
|
@click="clickHandler" />
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "component/sortableTable"}}
|
||||||
|
<script>
|
||||||
|
const DRAGGABLE_ROW_CLASS = 'draggable-row';
|
||||||
|
|
||||||
|
const findParentRowElement = (el) => {
|
||||||
|
if (!el || !el.tagName) {
|
||||||
|
return null;
|
||||||
|
} else if (el.classList.contains(DRAGGABLE_ROW_CLASS)) {
|
||||||
|
return el;
|
||||||
|
} else if (el.parentNode) {
|
||||||
|
return findParentRowElement(el.parentNode);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.component('a-table-sortable', {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
sortingElementIndex: null,
|
||||||
|
newElementIndex: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: ['data-source', 'customRow'],
|
||||||
|
inheritAttrs: false,
|
||||||
|
provide() {
|
||||||
|
const sortable = {}
|
||||||
|
|
||||||
|
Object.defineProperty(sortable, "setSortableIndex", {
|
||||||
|
enumerable: true,
|
||||||
|
get: () => this.setCurrentSortableIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(sortable, "resetSortableIndex", {
|
||||||
|
enumerable: true,
|
||||||
|
get: () => this.resetSortableIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
sortable,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render: function (createElement) {
|
||||||
|
return createElement(
|
||||||
|
'a-table',
|
||||||
|
{
|
||||||
|
class: {
|
||||||
|
'ant-table-is-sorting': this.isDragging(),
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
...this.$attrs,
|
||||||
|
'data-source': this.records,
|
||||||
|
customRow: (record, index) => this.customRowRender(record, index),
|
||||||
|
},
|
||||||
|
on: this.$listeners,
|
||||||
|
nativeOn: {
|
||||||
|
drop: (e) => this.dropHandler(e),
|
||||||
|
},
|
||||||
|
scopedSlots: this.$scopedSlots,
|
||||||
|
},
|
||||||
|
this.$slots.default,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$memoSort = {};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isDragging() {
|
||||||
|
const currentIndex = this.sortingElementIndex;
|
||||||
|
return currentIndex !== null && currentIndex !== undefined;
|
||||||
|
},
|
||||||
|
resetSortableIndex(e, index) {
|
||||||
|
this.sortingElementIndex = null;
|
||||||
|
this.newElementIndex = null;
|
||||||
|
this.$memoSort = {};
|
||||||
|
},
|
||||||
|
setCurrentSortableIndex(e, index) {
|
||||||
|
this.sortingElementIndex = index;
|
||||||
|
},
|
||||||
|
dragStartHandler(e, index) {
|
||||||
|
if (!this.isDragging()) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dragStopHandler(e, index) {
|
||||||
|
this.resetSortableIndex(e, index);
|
||||||
|
},
|
||||||
|
dragOverHandler(e, index) {
|
||||||
|
if (!this.isDragging()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const currentIndex = this.sortingElementIndex;
|
||||||
|
if (index === currentIndex) {
|
||||||
|
this.newElementIndex = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = findParentRowElement(e.target);
|
||||||
|
if (!row) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = row.getBoundingClientRect();
|
||||||
|
const offsetTop = e.pageY - rect.top;
|
||||||
|
|
||||||
|
if (offsetTop < rect.height / 2) {
|
||||||
|
this.newElementIndex = Math.max(index - 1, 0);
|
||||||
|
} else {
|
||||||
|
this.newElementIndex = index;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dropHandler(e) {
|
||||||
|
if (this.isDragging()) {
|
||||||
|
this.$emit('onsort', this.sortingElementIndex, this.newElementIndex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
customRowRender(record, index) {
|
||||||
|
const parentMethodResult = this.customRow?.(record, index) || {};
|
||||||
|
const newIndex = this.newElementIndex;
|
||||||
|
const currentIndex = this.sortingElementIndex;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...parentMethodResult,
|
||||||
|
attrs: {
|
||||||
|
...(parentMethodResult?.attrs || {}),
|
||||||
|
draggable: true,
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
...(parentMethodResult?.on || {}),
|
||||||
|
dragstart: (e) => this.dragStartHandler(e, index),
|
||||||
|
dragend: (e) => this.dragStopHandler(e, index),
|
||||||
|
dragover: (e) => this.dragOverHandler(e, index),
|
||||||
|
},
|
||||||
|
class: {
|
||||||
|
...(parentMethodResult?.class || {}),
|
||||||
|
[DRAGGABLE_ROW_CLASS]: true,
|
||||||
|
['dragging']: this.isDragging()
|
||||||
|
? (newIndex === null ? index === currentIndex : index === newIndex)
|
||||||
|
: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
records() {
|
||||||
|
const newIndex = this.newElementIndex;
|
||||||
|
const currentIndex = this.sortingElementIndex;
|
||||||
|
|
||||||
|
if (!this.isDragging() || newIndex === null || currentIndex === newIndex) {
|
||||||
|
return this.dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$memoSort.newIndex === newIndex) {
|
||||||
|
return this.$memoSort.list;
|
||||||
|
}
|
||||||
|
|
||||||
|
let list = [...this.dataSource];
|
||||||
|
list.splice(newIndex, 0, list.splice(currentIndex, 1)[0]);
|
||||||
|
|
||||||
|
this.$memoSort = {
|
||||||
|
newIndex,
|
||||||
|
list,
|
||||||
|
};
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Vue.component('table-sort-trigger', {
|
||||||
|
template: `{{template "component/sortableTableTrigger"}}`,
|
||||||
|
props: ['item-index'],
|
||||||
|
inject: ['sortable'],
|
||||||
|
methods: {
|
||||||
|
mouseDownHandler(e) {
|
||||||
|
if (this.sortable) {
|
||||||
|
this.sortable.setSortableIndex(e, this.itemIndex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mouseUpHandler(e) {
|
||||||
|
if (this.sortable) {
|
||||||
|
this.sortable.resetSortableIndex(e, this.itemIndex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clickHandler(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ant-table-is-sorting .draggable-row td {
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
.dark .ant-table-is-sorting .draggable-row td {
|
||||||
|
background-color: var(--dark-color-surface-100) !important;
|
||||||
|
}
|
||||||
|
.ant-table-is-sorting .dragging {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.ant-table-is-sorting .dragging .ant-table-row-index {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{end}}
|
|
@ -290,15 +290,19 @@
|
||||||
<a-alert type="warning" style="margin-bottom: 10px; width: fit-content"
|
<a-alert type="warning" style="margin-bottom: 10px; width: fit-content"
|
||||||
message='{{ i18n "pages.xray.RoutingsDesc"}}' show-icon></a-alert>
|
message='{{ i18n "pages.xray.RoutingsDesc"}}' show-icon></a-alert>
|
||||||
<a-button type="primary" icon="plus" @click="addRule">{{ i18n "pages.xray.rules.add" }}</a-button>
|
<a-button type="primary" icon="plus" @click="addRule">{{ i18n "pages.xray.rules.add" }}</a-button>
|
||||||
<a-table :columns="isMobile ? rulesMobileColumns : rulesColumns" bordered
|
<a-table-sortable :columns="isMobile ? rulesMobileColumns : rulesColumns" bordered
|
||||||
:row-key="r => r.key"
|
:row-key="r => r.key"
|
||||||
:data-source="routingRuleData"
|
:data-source="routingRuleData"
|
||||||
:scroll="isMobile ? {} : { x: 1000 }"
|
:scroll="isMobile ? {} : { x: 1000 }"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
:indent-size="0"
|
:indent-size="0"
|
||||||
:style="isMobile ? 'padding: 5px 0' : 'margin-top: 10px;'">
|
:style="isMobile ? 'padding: 5px 0' : 'margin-top: 10px;'"
|
||||||
|
v-on:onSort="replaceRule">
|
||||||
<template slot="action" slot-scope="text, rule, index">
|
<template slot="action" slot-scope="text, rule, index">
|
||||||
[[ index+1 ]]
|
<table-sort-trigger :item-index="index"></table-sort-trigger>
|
||||||
|
<span class="ant-table-row-index">
|
||||||
|
[[ index+1 ]]
|
||||||
|
</span>
|
||||||
<a-dropdown :trigger="['click']">
|
<a-dropdown :trigger="['click']">
|
||||||
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
|
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
|
||||||
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
||||||
|
@ -404,7 +408,7 @@
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table-sortable>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.xray.Outbounds"}}' style="padding-top: 20px;" force-render="true">
|
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.xray.Outbounds"}}' style="padding-top: 20px;" force-render="true">
|
||||||
<a-row>
|
<a-row>
|
||||||
|
@ -530,7 +534,7 @@
|
||||||
<template slot="selector" slot-scope="text, balancer, index">
|
<template slot="selector" slot-scope="text, balancer, index">
|
||||||
<a-tag class="info-large-tag" style="margin:1;" v-for="sel in balancer.selector">[[ sel ]]</a-tag>
|
<a-tag class="info-large-tag" style="margin:1;" v-for="sel in balancer.selector">[[ sel ]]</a-tag>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="tpl-6" tab='DNS' style="padding-top: 20px;" force-render="true">
|
<a-tab-pane key="tpl-6" tab='DNS' style="padding-top: 20px;" force-render="true">
|
||||||
<setting-list-item type="switch" title='{{ i18n "pages.xray.dns.enable" }}' desc='{{ i18n "pages.xray.dns.enableDesc" }}' v-model="enableDNS"></setting-list-item>
|
<setting-list-item type="switch" title='{{ i18n "pages.xray.dns.enable" }}' desc='{{ i18n "pages.xray.dns.enableDesc" }}' v-model="enableDNS"></setting-list-item>
|
||||||
|
@ -630,6 +634,7 @@
|
||||||
</a-layout>
|
</a-layout>
|
||||||
{{template "js" .}}
|
{{template "js" .}}
|
||||||
{{template "component/themeSwitcher" .}}
|
{{template "component/themeSwitcher" .}}
|
||||||
|
{{template "component/sortableTable" .}}
|
||||||
{{template "component/setting"}}
|
{{template "component/setting"}}
|
||||||
{{template "ruleModal"}}
|
{{template "ruleModal"}}
|
||||||
{{template "outModal"}}
|
{{template "outModal"}}
|
||||||
|
@ -1269,7 +1274,7 @@
|
||||||
newRules = newTemplateSettings.routing.rules.filter(r => r.outboundTag != oldData.tag);
|
newRules = newTemplateSettings.routing.rules.filter(r => r.outboundTag != oldData.tag);
|
||||||
}
|
}
|
||||||
newTemplateSettings.routing.rules = newRules;
|
newTemplateSettings.routing.rules = newRules;
|
||||||
|
|
||||||
this.templateSettings = newTemplateSettings;
|
this.templateSettings = newTemplateSettings;
|
||||||
},
|
},
|
||||||
addDNSServer(){
|
addDNSServer(){
|
||||||
|
|
Loading…
Reference in a new issue