feat(admin-frontend): 补齐用户节点与订单运营工作台
新增用户高级筛选、批量操作与更多行级动作,支持邮件、 CSV、封禁恢复、订单分配、邀请查看、流量记录与重置流量 增强节点管理页的分页、父子筛选、跨页勾选、批量修改与 单节点置顶,并补齐后端批量更新 host、group_ids、rate 修复订单佣金状态误判问题,新增真实佣金筛选与行级确认, 同时优化仪表盘排行悬浮详情展示 补充 admin-frontend 独立 Dockerfile、Caddy 配置与 GHCR 发布工作流,支持通过独立镜像部署管理前端
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
.node-batch-edit-dialog {
|
||||
.batch-shell,
|
||||
.batch-section {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.batch-shell {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.batch-hero,
|
||||
.batch-switch-card,
|
||||
.batch-footer,
|
||||
.batch-footer__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.batch-hero,
|
||||
.batch-footer,
|
||||
.batch-switch-card {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.batch-hero h2 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 28px;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.24px;
|
||||
color: var(--xboard-text-strong);
|
||||
}
|
||||
|
||||
.batch-hero p,
|
||||
.batch-switch-card span,
|
||||
.batch-footer__hint {
|
||||
color: var(--xboard-text-muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.batch-section {
|
||||
padding: 18px 20px;
|
||||
border-radius: 22px;
|
||||
background: #fbfbfd;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.batch-switch-card {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.batch-switch-card strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: var(--xboard-text-strong);
|
||||
}
|
||||
|
||||
.full-width,
|
||||
.full-width .el-input__wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.batch-footer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.batch-hero,
|
||||
.batch-switch-card,
|
||||
.batch-footer,
|
||||
.batch-footer__actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { AdminServerGroupItem } from '@/types/api'
|
||||
|
||||
interface NodeBatchEditPayload {
|
||||
host?: string
|
||||
rate?: number
|
||||
group_ids?: number[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
groups: AdminServerGroupItem[]
|
||||
selectedCount: number
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
submit: [payload: NodeBatchEditPayload]
|
||||
}>()
|
||||
|
||||
const form = reactive({
|
||||
updateHost: false,
|
||||
host: '',
|
||||
updateRate: false,
|
||||
rate: 1,
|
||||
updateGroups: false,
|
||||
groupIds: [] as number[],
|
||||
})
|
||||
|
||||
const hasEnabledField = computed(() => form.updateHost || form.updateRate || form.updateGroups)
|
||||
|
||||
function resetForm() {
|
||||
form.updateHost = false
|
||||
form.host = ''
|
||||
form.updateRate = false
|
||||
form.rate = 1
|
||||
form.updateGroups = false
|
||||
form.groupIds = []
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (!hasEnabledField.value) {
|
||||
ElMessage.warning('请至少开启一个需要批量修改的字段')
|
||||
return
|
||||
}
|
||||
|
||||
if (form.updateHost && !form.host.trim()) {
|
||||
ElMessage.warning('请输入新的节点地址 host')
|
||||
return
|
||||
}
|
||||
|
||||
if (form.updateRate && (!Number.isFinite(Number(form.rate)) || Number(form.rate) <= 0)) {
|
||||
ElMessage.warning('请输入大于 0 的倍率')
|
||||
return
|
||||
}
|
||||
|
||||
emit('submit', {
|
||||
host: form.updateHost ? form.host.trim() : undefined,
|
||||
rate: form.updateRate ? Number(form.rate) : undefined,
|
||||
group_ids: form.updateGroups ? [...form.groupIds] : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
resetForm()
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
:model-value="props.visible"
|
||||
width="min(680px, calc(100vw - 24px))"
|
||||
class="node-batch-edit-dialog"
|
||||
destroy-on-close
|
||||
@close="closeDialog"
|
||||
@update:model-value="emit('update:visible', $event)"
|
||||
>
|
||||
<div class="batch-shell">
|
||||
<header class="batch-hero">
|
||||
<div>
|
||||
<h2>批量修改节点</h2>
|
||||
<p>本轮仅对已勾选节点生效;支持统一修改节点地址 host、权限组和倍率。</p>
|
||||
</div>
|
||||
<ElTag round effect="dark">
|
||||
已选 {{ props.selectedCount }} 个节点
|
||||
</ElTag>
|
||||
</header>
|
||||
|
||||
<section class="batch-section">
|
||||
<label class="batch-switch-card">
|
||||
<div>
|
||||
<strong>批量修改节点地址</strong>
|
||||
<span>只修改 `host`,不改端口;适合整批切换域名或 IP。</span>
|
||||
</div>
|
||||
<ElSwitch v-model="form.updateHost" />
|
||||
</label>
|
||||
|
||||
<ElInput
|
||||
v-model="form.host"
|
||||
:disabled="!form.updateHost"
|
||||
placeholder="例如 node.example.com 或 1.2.3.4"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="batch-section">
|
||||
<label class="batch-switch-card">
|
||||
<div>
|
||||
<strong>批量修改权限组</strong>
|
||||
<span>启用后会整体替换所选节点的权限组;留空表示清空权限组。</span>
|
||||
</div>
|
||||
<ElSwitch v-model="form.updateGroups" />
|
||||
</label>
|
||||
|
||||
<ElSelect
|
||||
v-model="form.groupIds"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:disabled="!form.updateGroups"
|
||||
placeholder="请选择权限组"
|
||||
>
|
||||
<ElOption
|
||||
v-for="group in props.groups"
|
||||
:key="group.id"
|
||||
:label="group.name"
|
||||
:value="group.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</section>
|
||||
|
||||
<section class="batch-section">
|
||||
<label class="batch-switch-card">
|
||||
<div>
|
||||
<strong>批量修改倍率</strong>
|
||||
<span>适合统一调整节点倍率,不会改动动态倍率规则。</span>
|
||||
</div>
|
||||
<ElSwitch v-model="form.updateRate" />
|
||||
</label>
|
||||
|
||||
<ElInputNumber
|
||||
v-model="form.rate"
|
||||
:disabled="!form.updateRate"
|
||||
:min="0.01"
|
||||
:step="0.01"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
class="full-width"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="batch-footer">
|
||||
<span class="batch-footer__hint">批量修改不会影响端口、协议配置与显隐状态。</span>
|
||||
<div class="batch-footer__actions">
|
||||
<ElButton @click="closeDialog">取消</ElButton>
|
||||
<ElButton type="primary" :loading="props.loading" @click="handleSubmit">
|
||||
确认批量修改
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss" src="./NodeBatchEditDialog.scss"></style>
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { TableInstance } from 'element-plus'
|
||||
import {
|
||||
Connection,
|
||||
MoreFilled,
|
||||
@@ -11,14 +12,22 @@ import {
|
||||
User,
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
batchUpdateNodes,
|
||||
copyNode,
|
||||
deleteNode,
|
||||
fetchNodes,
|
||||
fetchNodeRoutes,
|
||||
getServerGroups,
|
||||
sortNodes,
|
||||
updateNode,
|
||||
} from '@/api/admin'
|
||||
import type { AdminNodeItem, AdminNodeRouteItem, AdminServerGroupItem } from '@/types/api'
|
||||
import type {
|
||||
AdminNodeBatchUpdatePayload,
|
||||
AdminNodeItem,
|
||||
AdminNodeRouteItem,
|
||||
AdminServerGroupItem,
|
||||
} from '@/types/api'
|
||||
import NodeBatchEditDialog from './NodeBatchEditDialog.vue'
|
||||
import NodeEditorDialog from './NodeEditorDialog.vue'
|
||||
import NodeSortDialog from './NodeSortDialog.vue'
|
||||
import {
|
||||
@@ -32,14 +41,17 @@ import {
|
||||
getNodeIdLabel,
|
||||
getNodeStatusMeta,
|
||||
getNodeTypeLabel,
|
||||
type NodeRelationFilter,
|
||||
} from '@/utils/nodes'
|
||||
import { sortNodesByOrder } from '@/utils/nodeEditor'
|
||||
|
||||
type NodeAction = 'edit' | 'copy' | 'delete'
|
||||
type NodeAction = 'edit' | 'copy' | 'pin-top' | 'delete'
|
||||
type NodeDialogMode = 'create' | 'edit'
|
||||
type NodeBatchEditPayload = Omit<AdminNodeBatchUpdatePayload, 'ids'>
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const tableRef = ref<TableInstance>()
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const nodes = ref<AdminNodeItem[]>([])
|
||||
@@ -48,30 +60,55 @@ const routes = ref<AdminNodeRouteItem[]>([])
|
||||
const keyword = ref('')
|
||||
const typeFilter = ref('all')
|
||||
const groupFilter = ref('all')
|
||||
const relationFilter = ref<NodeRelationFilter>('all')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const selectedNodeIds = ref<number[]>([])
|
||||
const switchingIds = ref<number[]>([])
|
||||
const workingIds = ref<number[]>([])
|
||||
const editorVisible = ref(false)
|
||||
const editorMode = ref<NodeDialogMode>('create')
|
||||
const activeNode = ref<AdminNodeItem | null>(null)
|
||||
const sortDialogVisible = ref(false)
|
||||
const batchEditVisible = ref(false)
|
||||
const batchSubmitting = ref(false)
|
||||
|
||||
const filteredNodes = computed(() => sortNodesByOrder(filterNodes(
|
||||
nodes.value,
|
||||
keyword.value,
|
||||
typeFilter.value,
|
||||
groupFilter.value,
|
||||
relationFilter.value,
|
||||
)))
|
||||
|
||||
const paginatedNodes = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return filteredNodes.value.slice(start, start + pageSize.value)
|
||||
})
|
||||
|
||||
const selectedNodes = computed(() => nodes.value.filter((node) => selectedNodeIds.value.includes(node.id)))
|
||||
const typeOptions = computed(() => buildNodeTypeOptions(nodes.value))
|
||||
const hasActiveFilters = computed(() => keyword.value !== '' || typeFilter.value !== 'all' || groupFilter.value !== 'all')
|
||||
const hasSelectedNodes = computed(() => selectedNodes.value.length > 0)
|
||||
const hasActiveFilters = computed(() => (
|
||||
keyword.value !== ''
|
||||
|| typeFilter.value !== 'all'
|
||||
|| groupFilter.value !== 'all'
|
||||
|| relationFilter.value !== 'all'
|
||||
))
|
||||
|
||||
const summaryCards = computed(() => [
|
||||
{ label: '节点总数', value: String(nodes.value.length) },
|
||||
{ label: '在线节点', value: String(countOnlineNodes(nodes.value)) },
|
||||
{ label: '显示中', value: String(countVisibleNodes(nodes.value)) },
|
||||
{ label: '当前结果', value: String(filteredNodes.value.length) },
|
||||
{ label: '已勾选', value: String(selectedNodes.value.length) },
|
||||
])
|
||||
|
||||
const batchTargetLabel = computed(() => (
|
||||
hasSelectedNodes.value
|
||||
? `当前已选 ${selectedNodes.value.length} 个节点`
|
||||
: '批量修改仅作用于已勾选节点'
|
||||
))
|
||||
|
||||
function getRouteGroupQuery(): string {
|
||||
const rawValue = route.query.group
|
||||
if (Array.isArray(rawValue)) {
|
||||
@@ -88,6 +125,7 @@ function applyRouteGroupFilter() {
|
||||
|
||||
const exists = groups.value.some((group) => String(group.id) === groupValue)
|
||||
groupFilter.value = exists ? groupValue : 'all'
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
function markPending(list: typeof switchingIds, id: number, pending: boolean) {
|
||||
@@ -125,6 +163,34 @@ function openSortEditor() {
|
||||
sortDialogVisible.value = true
|
||||
}
|
||||
|
||||
function setCurrentPageInRange() {
|
||||
const totalPages = Math.max(1, Math.ceil(filteredNodes.value.length / pageSize.value))
|
||||
if (currentPage.value > totalPages) {
|
||||
currentPage.value = totalPages
|
||||
}
|
||||
}
|
||||
|
||||
function pruneSelection() {
|
||||
const validIds = new Set(nodes.value.map((node) => node.id))
|
||||
selectedNodeIds.value = selectedNodeIds.value.filter((id) => validIds.has(id))
|
||||
}
|
||||
|
||||
function syncTableSelection() {
|
||||
nextTick(() => {
|
||||
const table = tableRef.value
|
||||
if (!table) {
|
||||
return
|
||||
}
|
||||
|
||||
table.clearSelection()
|
||||
paginatedNodes.value.forEach((node) => {
|
||||
if (selectedNodeIds.value.includes(node.id)) {
|
||||
table.toggleRowSelection(node, true)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function loadNodeBoard() {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
@@ -139,7 +205,10 @@ async function loadNodeBoard() {
|
||||
nodes.value = sortNodesByOrder(nodesResponse.data ?? [])
|
||||
groups.value = groupsResponse.data ?? []
|
||||
routes.value = routesResponse.data ?? []
|
||||
pruneSelection()
|
||||
applyRouteGroupFilter()
|
||||
setCurrentPageInRange()
|
||||
syncTableSelection()
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '节点数据加载失败'
|
||||
} finally {
|
||||
@@ -151,12 +220,67 @@ function handleReset() {
|
||||
keyword.value = ''
|
||||
typeFilter.value = 'all'
|
||||
groupFilter.value = 'all'
|
||||
relationFilter.value = 'all'
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
function openNodeGroupManagement() {
|
||||
void router.push('/node-groups')
|
||||
}
|
||||
|
||||
function handleSelectionChange(selection: AdminNodeItem[]) {
|
||||
const currentPageIds = paginatedNodes.value.map((item) => item.id)
|
||||
const selectionIds = selection.map((item) => item.id)
|
||||
const persistedIds = selectedNodeIds.value.filter((id) => !currentPageIds.includes(id))
|
||||
selectedNodeIds.value = [...new Set([...persistedIds, ...selectionIds])]
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedNodeIds.value = []
|
||||
syncTableSelection()
|
||||
}
|
||||
|
||||
function openBatchEditor() {
|
||||
if (!hasSelectedNodes.value) {
|
||||
ElMessage.warning('请先勾选需要批量修改的节点')
|
||||
return
|
||||
}
|
||||
|
||||
batchEditVisible.value = true
|
||||
}
|
||||
|
||||
async function handleBatchSubmit(payload: NodeBatchEditPayload) {
|
||||
const updatePayload: AdminNodeBatchUpdatePayload = {
|
||||
ids: [...selectedNodeIds.value],
|
||||
host: payload.host,
|
||||
rate: payload.rate,
|
||||
group_ids: payload.group_ids,
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认批量修改 ${selectedNodeIds.value.length} 个节点吗?本次只会更新已启用的字段。`,
|
||||
'批量修改节点',
|
||||
{ type: 'warning' },
|
||||
)
|
||||
|
||||
batchSubmitting.value = true
|
||||
await batchUpdateNodes(updatePayload)
|
||||
batchEditVisible.value = false
|
||||
clearSelection()
|
||||
ElMessage.success(`已批量更新 ${updatePayload.ids.length} 个节点`)
|
||||
await loadNodeBoard()
|
||||
} catch (error) {
|
||||
if (error === 'cancel' || error === 'close') {
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.error(error instanceof Error ? error.message : '批量修改失败')
|
||||
} finally {
|
||||
batchSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleShow(node: AdminNodeItem, nextValue: boolean) {
|
||||
const previous = Boolean(node.show)
|
||||
if (previous === nextValue) {
|
||||
@@ -180,12 +304,42 @@ async function handleToggleShow(node: AdminNodeItem, nextValue: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePinTop(node: AdminNodeItem) {
|
||||
const orderedNodes = sortNodesByOrder(nodes.value)
|
||||
if (orderedNodes[0]?.id === node.id) {
|
||||
ElMessage.info('当前节点已经在列表顶部')
|
||||
return
|
||||
}
|
||||
|
||||
markPending(workingIds, node.id, true)
|
||||
|
||||
try {
|
||||
const nextOrder = [node, ...orderedNodes.filter((item) => item.id !== node.id)]
|
||||
await sortNodes(nextOrder.map((item, index) => ({
|
||||
id: item.id,
|
||||
order: index + 1,
|
||||
})))
|
||||
currentPage.value = 1
|
||||
ElMessage.success(`已将“${node.name}”置顶`)
|
||||
await loadNodeBoard()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '节点置顶失败')
|
||||
} finally {
|
||||
markPending(workingIds, node.id, false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAction(action: NodeAction, node: AdminNodeItem) {
|
||||
if (action === 'edit') {
|
||||
openEditEditor(node)
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'pin-top') {
|
||||
await handlePinTop(node)
|
||||
return
|
||||
}
|
||||
|
||||
markPending(workingIds, node.id, true)
|
||||
|
||||
try {
|
||||
@@ -226,6 +380,32 @@ watch(
|
||||
applyRouteGroupFilter()
|
||||
},
|
||||
)
|
||||
|
||||
watch([keyword, typeFilter, groupFilter, relationFilter], () => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
|
||||
watch(pageSize, () => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
|
||||
watch(
|
||||
() => filteredNodes.value.length,
|
||||
() => {
|
||||
setCurrentPageInRange()
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [
|
||||
paginatedNodes.value.map((item) => item.id).join(','),
|
||||
selectedNodeIds.value.join(','),
|
||||
],
|
||||
() => {
|
||||
syncTableSelection()
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -235,7 +415,7 @@ watch(
|
||||
<p class="nodes-kicker">Nodes</p>
|
||||
<h1>节点管理</h1>
|
||||
<span>
|
||||
管理所有节点,包括添加、筛选、显隐控制、复制和删除等首批运营动作。
|
||||
现在可以在同一页完成节点筛选、分页浏览、单行置顶、批量修改,以及新增、编辑、显隐和删除等运营动作。
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -285,9 +465,17 @@ watch(
|
||||
:value="String(group.id)"
|
||||
/>
|
||||
</ElSelect>
|
||||
|
||||
<ElSelect v-model="relationFilter" class="toolbar-select" placeholder="节点关系">
|
||||
<ElOption label="全部节点" value="all" />
|
||||
<ElOption label="父节点" value="parent" />
|
||||
<ElOption label="子节点" value="child" />
|
||||
</ElSelect>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-actions">
|
||||
<span class="scope-hint">{{ batchTargetLabel }}</span>
|
||||
<ElButton :disabled="!hasSelectedNodes" @click="openBatchEditor">批量修改</ElButton>
|
||||
<ElButton @click="openNodeGroupManagement">管理权限组</ElButton>
|
||||
<ElButton @click="handleReset" :disabled="!hasActiveFilters">
|
||||
<ElIcon><RefreshRight /></ElIcon>
|
||||
@@ -297,6 +485,11 @@ watch(
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div v-if="hasSelectedNodes" class="selection-summary">
|
||||
<span class="selection-summary__label">已勾选 {{ selectedNodes.length }} 个节点,批量修改只会作用于这些节点。</span>
|
||||
<ElButton text @click="clearSelection">清空勾选</ElButton>
|
||||
</div>
|
||||
|
||||
<ElAlert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
@@ -311,11 +504,14 @@ watch(
|
||||
</ElAlert>
|
||||
|
||||
<ElTable
|
||||
:data="filteredNodes"
|
||||
ref="tableRef"
|
||||
:data="paginatedNodes"
|
||||
v-loading="loading"
|
||||
row-key="id"
|
||||
class="nodes-table"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<ElTableColumn type="selection" width="52" reserve-selection />
|
||||
<ElTableColumn label="节点ID" width="132">
|
||||
<template #default="{ row }">
|
||||
<ElTag
|
||||
@@ -420,8 +616,9 @@ watch(
|
||||
<ElIcon><MoreFilled /></ElIcon>
|
||||
</ElButton>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem command="edit">编辑节点</ElDropdownItem>
|
||||
<ElDropdownItem command="pin-top">置顶节点</ElDropdownItem>
|
||||
<ElDropdownItem command="copy">复制节点</ElDropdownItem>
|
||||
<ElDropdownItem command="delete" divided>删除节点</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
@@ -446,10 +643,19 @@ watch(
|
||||
</ElTable>
|
||||
|
||||
<footer class="board-footer">
|
||||
<span>已显示 {{ filteredNodes.length }} / {{ nodes.length }} 个节点</span>
|
||||
<span>第 {{ currentPage }} 页 · 已显示 {{ paginatedNodes.length }} / {{ filteredNodes.length }} 个节点</span>
|
||||
<ElPagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:total="filteredNodes.length"
|
||||
background
|
||||
class="footer-pagination"
|
||||
/>
|
||||
<div class="footer-hint">
|
||||
<ElIcon><Connection /></ElIcon>
|
||||
<span>节点新增、编辑与排序已在当前工作台内接入真实流程。</span>
|
||||
<span>节点新增、编辑、置顶、排序与批量修改已收敛到同一工作台。</span>
|
||||
</div>
|
||||
</footer>
|
||||
</section>
|
||||
@@ -469,6 +675,14 @@ watch(
|
||||
:nodes="nodes"
|
||||
@success="() => loadNodeBoard()"
|
||||
/>
|
||||
|
||||
<NodeBatchEditDialog
|
||||
v-model:visible="batchEditVisible"
|
||||
:groups="groups"
|
||||
:selected-count="selectedNodes.length"
|
||||
:loading="batchSubmitting"
|
||||
@submit="handleBatchSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -550,7 +764,8 @@ watch(
|
||||
.toolbar-fields,
|
||||
.toolbar-actions,
|
||||
.board-footer,
|
||||
.footer-hint {
|
||||
.footer-hint,
|
||||
.selection-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
@@ -578,6 +793,21 @@ watch(
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.scope-hint,
|
||||
.selection-summary__label {
|
||||
color: var(--xboard-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.selection-summary {
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
padding: 14px 16px;
|
||||
border-radius: 18px;
|
||||
background: #fbfbfd;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.nodes-table :deep(th.el-table__cell) {
|
||||
color: var(--xboard-text-secondary);
|
||||
background: #fbfbfd;
|
||||
@@ -667,6 +897,10 @@ watch(
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer-pagination {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.footer-hint {
|
||||
justify-content: flex-end;
|
||||
color: var(--xboard-text-muted);
|
||||
@@ -675,7 +909,8 @@ watch(
|
||||
@media (max-width: 1180px) {
|
||||
.nodes-hero,
|
||||
.board-toolbar,
|
||||
.board-footer {
|
||||
.board-footer,
|
||||
.selection-summary {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
@@ -687,6 +922,7 @@ watch(
|
||||
|
||||
.toolbar-actions {
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user