feat(panel): add 'Edit' button to tables and enhance layout (#4355)
- Move 'Edit' button from dropdown to the table since it's the most used action. Only for desktop. - Increase column widths for action keys in Inbounds, Balancers, Outbounds and Routing tables. - Slightly enhance layout for consistency.
This commit is contained in:
@@ -230,7 +230,7 @@ const hasAnyRemark = computed(() =>
|
||||
const desktopColumns = computed(() => {
|
||||
const cols = [
|
||||
sortableCol({ title: 'ID', dataIndex: 'id', key: 'id', align: 'right', width: 30 }, 'id'),
|
||||
{ title: t('pages.inbounds.operate'), key: 'action', align: 'center', width: 30 },
|
||||
{ title: t('pages.inbounds.operate'), key: 'action', align: 'center', width: 60 },
|
||||
sortableCol({ title: t('pages.inbounds.enable'), key: 'enable', align: 'center', width: 35 }, 'enable'),
|
||||
];
|
||||
if (hasAnyRemark.value) {
|
||||
@@ -571,59 +571,68 @@ function showQrCodeMenu(dbInbound) {
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- ============== Action dropdown ============== -->
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-dropdown :trigger="['click']">
|
||||
<MoreOutlined class="row-action-trigger" @click.prevent />
|
||||
<template #overlay>
|
||||
<a-menu @click="(a) => emit('row-action', { key: a.key, dbInbound: record })">
|
||||
<a-menu-item key="edit">
|
||||
<EditOutlined /> {{ t('edit') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="showQrCodeMenu(record)" key="qrcode">
|
||||
<QrcodeOutlined /> {{ t('qrCode') }}
|
||||
</a-menu-item>
|
||||
<template v-if="record.isMultiUser()">
|
||||
<a-menu-item key="addClient">
|
||||
<UserAddOutlined /> {{ t('pages.client.add') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="addBulkClient">
|
||||
<UsergroupAddOutlined /> {{ t('pages.client.bulk') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="copyClients">
|
||||
<CopyOutlined /> {{ t('pages.client.copyFromInbound') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetClients">
|
||||
<FileDoneOutlined /> {{ t('pages.inbounds.resetInboundClientTraffics') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="export">
|
||||
<ExportOutlined /> {{ t('pages.inbounds.export') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="subEnable" key="subs">
|
||||
<ExportOutlined /> {{ t('pages.inbounds.export') }} — {{ t('pages.settings.subSettings') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delDepletedClients" class="danger-item">
|
||||
<RestOutlined /> {{ t('pages.inbounds.delDepletedClients') }}
|
||||
</a-menu-item>
|
||||
<div class="action-buttons">
|
||||
<a-button type="text" size="small" @click.prevent="emit('row-action', {key: 'edit', dbInbound: record})">
|
||||
<template #icon>
|
||||
<EditOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button type="text" size="small" @click.prevent>
|
||||
<template #icon>
|
||||
<MoreOutlined />
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-menu-item key="showInfo">
|
||||
<InfoCircleOutlined /> {{ t('info') }}
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="(a) => emit('row-action', { key: a.key, dbInbound: record })">
|
||||
<a-menu-item v-if="showQrCodeMenu(record)" key="qrcode">
|
||||
<QrcodeOutlined /> {{ t('qrCode') }}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
<a-menu-item key="clipboard">
|
||||
<CopyOutlined /> {{ t('pages.inbounds.exportInbound') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetTraffic">
|
||||
<RetweetOutlined /> {{ t('pages.inbounds.resetTraffic') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="clone">
|
||||
<BlockOutlined /> {{ t('pages.inbounds.clone') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delete" class="danger-item">
|
||||
<DeleteOutlined /> {{ t('delete') }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<template v-if="record.isMultiUser()">
|
||||
<a-menu-item key="addClient">
|
||||
<UserAddOutlined /> {{ t('pages.client.add') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="addBulkClient">
|
||||
<UsergroupAddOutlined /> {{ t('pages.client.bulk') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="copyClients">
|
||||
<CopyOutlined /> {{ t('pages.client.copyFromInbound') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetClients">
|
||||
<FileDoneOutlined /> {{ t('pages.inbounds.resetInboundClientTraffics') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="export">
|
||||
<ExportOutlined /> {{ t('pages.inbounds.export') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="subEnable" key="subs">
|
||||
<ExportOutlined /> {{ t('pages.inbounds.export') }} — {{ t('pages.settings.subSettings') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delDepletedClients" class="danger-item">
|
||||
<RestOutlined /> {{ t('pages.inbounds.delDepletedClients') }}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-menu-item key="showInfo">
|
||||
<InfoCircleOutlined /> {{ t('info') }}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
<a-menu-item key="clipboard">
|
||||
<CopyOutlined /> {{ t('pages.inbounds.exportInbound') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetTraffic">
|
||||
<RetweetOutlined /> {{ t('pages.inbounds.resetTraffic') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="clone">
|
||||
<BlockOutlined /> {{ t('pages.inbounds.clone') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delete" class="danger-item">
|
||||
<DeleteOutlined /> {{ t('delete') }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ============== Enable switch (desktop) ============== -->
|
||||
@@ -764,6 +773,13 @@ function showQrCodeMenu(dbInbound) {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.protocol-tags {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -22,6 +22,7 @@ const { t } = useI18n();
|
||||
const props = defineProps({
|
||||
templateSettings: { type: Object, default: null },
|
||||
clientReverseTags: { type: Array, default: () => [] },
|
||||
isMobile: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const STRATEGY_LABELS = {
|
||||
@@ -197,7 +198,7 @@ function confirmDelete(idx) {
|
||||
}
|
||||
|
||||
const columns = computed(() => [
|
||||
{ title: '#', key: 'action', align: 'center', width: 80 },
|
||||
{ title: '#', key: 'action', align: 'center', width: 100 },
|
||||
{ title: 'Tag', dataIndex: 'tag', key: 'tag', align: 'center', width: 160 },
|
||||
{ title: 'Strategy', key: 'strategy', align: 'center', width: 140 },
|
||||
{ title: 'Selector', key: 'selector', align: 'center' },
|
||||
@@ -267,25 +268,39 @@ const obsText = computed({
|
||||
{{ t('pages.xray.Balancers') }}
|
||||
</a-button>
|
||||
|
||||
<a-table :columns="columns" :data-source="rows" :row-key="(r) => r.key" :pagination="false" size="small" bordered>
|
||||
<a-table :columns="columns" :data-source="rows" :row-key="(r) => r.key" :pagination="false"
|
||||
size="small" :scroll="{ x: 400 }">
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<span class="row-index">{{ index + 1 }}</span>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button shape="circle" size="small" class="action-btn">
|
||||
<MoreOutlined />
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item @click="openEdit(index)">
|
||||
<EditOutlined /> {{ t('edit') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item class="danger" @click="confirmDelete(index)">
|
||||
<DeleteOutlined /> {{ t('delete') }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<div class="action-cell">
|
||||
<span class="row-index">{{ index + 1 }}</span>
|
||||
|
||||
<div :class="!isMobile ? 'action-buttons' : ''">
|
||||
<a-button v-if="!isMobile" shape="circle" size="small" @click="openEdit(index)">
|
||||
<template #icon>
|
||||
<EditOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button shape="circle" size="small">
|
||||
<template #icon>
|
||||
<MoreOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item v-if="isMobile" @click="openEdit(index)">
|
||||
<EditOutlined /> {{ t('edit') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item class="danger" @click="confirmDelete(index)">
|
||||
<DeleteOutlined /> {{ t('delete') }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'strategy'">
|
||||
@@ -316,14 +331,25 @@ const obsText = computed({
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.action-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.row-index {
|
||||
font-weight: 500;
|
||||
opacity: 0.7;
|
||||
margin-right: 6px;
|
||||
min-width: 18px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
vertical-align: middle;
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.danger {
|
||||
|
||||
@@ -157,9 +157,9 @@ function hasBreakdown(r) {
|
||||
// === Columns ========================================================
|
||||
// Computed so titles re-render after a locale swap.
|
||||
const columns = computed(() => [
|
||||
{ title: '#', key: 'action', align: 'center', width: 70 },
|
||||
{ title: 'Tag', key: 'identity', align: 'left', width: 220 },
|
||||
{ title: t('pages.inbounds.address'), key: 'address', align: 'left', width: 230 },
|
||||
{ title: '#', key: 'action', align: 'center', width: 100 },
|
||||
{ title: 'Tag', key: 'identity', align: 'left' },
|
||||
{ title: t('pages.inbounds.address'), key: 'address', align: 'left' },
|
||||
{ title: t('pages.inbounds.traffic'), key: 'traffic', align: 'left', width: 200 },
|
||||
{ title: t('pages.xray.latency') !== 'pages.xray.latency' ? t('pages.xray.latency') : 'Latency', key: 'testResult', align: 'left', width: 140 },
|
||||
{ title: t('check'), key: 'test', align: 'center', width: 80 },
|
||||
@@ -322,33 +322,41 @@ const rows = computed(() => {
|
||||
<template v-if="column.key === 'action'">
|
||||
<div class="action-cell">
|
||||
<span class="row-index">{{ index + 1 }}</span>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button shape="circle" size="small">
|
||||
<MoreOutlined />
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item v-if="index > 0" @click="setFirst(index)">
|
||||
<VerticalAlignTopOutlined /> Move to top
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="openEdit(index)">
|
||||
<EditOutlined /> Edit
|
||||
</a-menu-item>
|
||||
<a-menu-item :disabled="index === 0" @click="moveUp(index)">
|
||||
<ArrowUpOutlined />
|
||||
</a-menu-item>
|
||||
<a-menu-item :disabled="index === rows.length - 1" @click="moveDown(index)">
|
||||
<ArrowDownOutlined />
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="emit('reset-traffic', record.tag || '')">
|
||||
<RetweetOutlined /> Reset traffic
|
||||
</a-menu-item>
|
||||
<a-menu-item class="danger" @click="confirmDelete(index)">
|
||||
<DeleteOutlined /> Delete
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
||||
<div class="action-buttons">
|
||||
<a-button shape="circle" size="small" @click="openEdit(index)">
|
||||
<template #icon>
|
||||
<EditOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button shape="circle" size="small">
|
||||
<template #icon>
|
||||
<MoreOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item v-if="index > 0" @click="setFirst(index)">
|
||||
<VerticalAlignTopOutlined /> Move to top
|
||||
</a-menu-item>
|
||||
<a-menu-item :disabled="index === 0" @click="moveUp(index)">
|
||||
<ArrowUpOutlined />
|
||||
</a-menu-item>
|
||||
<a-menu-item :disabled="index === rows.length - 1" @click="moveDown(index)">
|
||||
<ArrowDownOutlined />
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="emit('reset-traffic', record.tag || '')">
|
||||
<RetweetOutlined /> Reset traffic
|
||||
</a-menu-item>
|
||||
<a-menu-item class="danger" @click="confirmDelete(index)">
|
||||
<DeleteOutlined /> Delete
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -444,6 +452,11 @@ const rows = computed(() => {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.toolbar-right :global(.ant-space),
|
||||
.header-actions :global(.ant-space) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.card-empty {
|
||||
text-align: center;
|
||||
opacity: 0.4;
|
||||
@@ -526,6 +539,14 @@ const rows = computed(() => {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.identity-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -220,7 +220,7 @@ function rowProps(_record, index) {
|
||||
// === Columns =========================================================
|
||||
// Computed so titles re-render after a locale swap.
|
||||
const desktopColumns = computed(() => [
|
||||
{ title: '#', align: 'center', width: 70, key: 'action' },
|
||||
{ title: '#', align: 'center', width: 100, key: 'action' },
|
||||
{ title: 'Source', align: 'left', width: 180, key: 'source' },
|
||||
{ title: t('pages.inbounds.network'), align: 'left', width: 180, key: 'network' },
|
||||
{ title: 'Destination', align: 'left', key: 'destination' },
|
||||
@@ -340,27 +340,38 @@ function chipPreview(value) {
|
||||
<HolderOutlined class="drag-handle" :title="t('drag') || 'Drag to reorder'"
|
||||
@pointerdown="onHandlePointerDown(index, $event)" />
|
||||
<span class="row-index">{{ index + 1 }}</span>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button shape="circle" size="small">
|
||||
<MoreOutlined />
|
||||
|
||||
<div :class="!isMobile ? 'action-buttons' : ''">
|
||||
<a-button v-if="!isMobile" shape="circle" size="small" @click="openEdit(index)">
|
||||
<template #icon>
|
||||
<EditOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item @click="openEdit(index)">
|
||||
<EditOutlined /> {{ t('edit') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item :disabled="index === 0" @click="moveUp(index)">
|
||||
<ArrowUpOutlined />
|
||||
</a-menu-item>
|
||||
<a-menu-item :disabled="index === rows.length - 1" @click="moveDown(index)">
|
||||
<ArrowDownOutlined />
|
||||
</a-menu-item>
|
||||
<a-menu-item class="danger" @click="confirmDelete(index)">
|
||||
<DeleteOutlined /> {{ t('delete') }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button shape="circle" size="small">
|
||||
<template #icon>
|
||||
<MoreOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item v-if="isMobile" @click="openEdit(index)">
|
||||
<EditOutlined /> {{ t('edit') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item :disabled="index === 0" @click="moveUp(index)">
|
||||
<ArrowUpOutlined />
|
||||
</a-menu-item>
|
||||
<a-menu-item :disabled="index === rows.length - 1" @click="moveDown(index)">
|
||||
<ArrowDownOutlined />
|
||||
</a-menu-item>
|
||||
<a-menu-item class="danger" @click="confirmDelete(index)">
|
||||
<DeleteOutlined /> {{ t('delete') }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -550,6 +561,14 @@ function chipPreview(value) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.criterion-flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -349,7 +349,8 @@ onBeforeUnmount(() => {
|
||||
</a-tooltip>
|
||||
<span v-if="!isMobile">{{ t('pages.xray.Balancers') }}</span>
|
||||
</template>
|
||||
<BalancersTab :template-settings="templateSettings" :client-reverse-tags="clientReverseTags" />
|
||||
<BalancersTab :template-settings="templateSettings"
|
||||
:client-reverse-tags="clientReverseTags" :is-mobile="isMobile" />
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="tpl-dns" class="tab-pane">
|
||||
|
||||
Reference in New Issue
Block a user