feat(admin-frontend): 补齐活跃筛选与支付快照能力

新增用户管理“活跃状态”高级筛选,并在后端支持
activity_status 复合规则,支持按活跃与非活跃筛选用户。

补齐订单支付成功快照落库与后台展示,保存支付渠道、
支付方法、实付金额和支付 IP,并在订单详情中优先展示。

同时增强节点页在线/离线筛选与批量删除、仪表盘快捷入口,
并修复已关闭工单再次回复后自动重开的统一语义。

附带同步测试、迁移、CI 工作流命名及知识库记录
This commit is contained in:
yinjianm
2026-04-25 00:59:08 +08:00
parent 2218457237
commit c64badfc23
55 changed files with 2023 additions and 71 deletions
+85 -16
View File
@@ -5,6 +5,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import type { TableInstance } from 'element-plus'
import {
Connection,
Delete,
MoreFilled,
Plus,
RefreshRight,
@@ -12,6 +13,7 @@ import {
User,
} from '@element-plus/icons-vue'
import {
batchDeleteNodes,
batchUpdateNodes,
copyNode,
deleteNode,
@@ -42,6 +44,7 @@ import {
getNodeStatusMeta,
getNodeTypeLabel,
type NodeRelationFilter,
type NodeStatusFilter,
} from '@/utils/nodes'
import { sortNodesByOrder } from '@/utils/nodeEditor'
@@ -60,10 +63,12 @@ const routes = ref<AdminNodeRouteItem[]>([])
const keyword = ref('')
const typeFilter = ref('all')
const groupFilter = ref('all')
const statusFilter = ref<NodeStatusFilter>('all')
const relationFilter = ref<NodeRelationFilter>('all')
const currentPage = ref(1)
const pageSize = ref(20)
const selectedNodeIds = ref<number[]>([])
const syncingSelection = ref(false)
const switchingIds = ref<number[]>([])
const workingIds = ref<number[]>([])
const editorVisible = ref(false)
@@ -72,12 +77,14 @@ const activeNode = ref<AdminNodeItem | null>(null)
const sortDialogVisible = ref(false)
const batchEditVisible = ref(false)
const batchSubmitting = ref(false)
const batchDeleting = ref(false)
const filteredNodes = computed(() => sortNodesByOrder(filterNodes(
nodes.value,
keyword.value,
typeFilter.value,
groupFilter.value,
statusFilter.value,
relationFilter.value,
)))
@@ -93,6 +100,7 @@ const hasActiveFilters = computed(() => (
keyword.value !== ''
|| typeFilter.value !== 'all'
|| groupFilter.value !== 'all'
|| statusFilter.value !== 'all'
|| relationFilter.value !== 'all'
))
@@ -106,7 +114,7 @@ const summaryCards = computed(() => [
const batchTargetLabel = computed(() => (
hasSelectedNodes.value
? `当前已选 ${selectedNodes.value.length} 个节点`
: '批量修改仅作用于已勾选节点'
: '批量修改 / 删除仅作用于已勾选节点'
))
function getRouteGroupQuery(): string {
@@ -182,12 +190,20 @@ function syncTableSelection() {
return
}
table.clearSelection()
paginatedNodes.value.forEach((node) => {
if (selectedNodeIds.value.includes(node.id)) {
table.toggleRowSelection(node, true)
}
})
syncingSelection.value = true
try {
table.clearSelection()
paginatedNodes.value.forEach((node) => {
if (selectedNodeIds.value.includes(node.id)) {
table.toggleRowSelection(node, true)
}
})
} finally {
nextTick(() => {
syncingSelection.value = false
})
}
})
}
@@ -220,6 +236,7 @@ function handleReset() {
keyword.value = ''
typeFilter.value = 'all'
groupFilter.value = 'all'
statusFilter.value = 'all'
relationFilter.value = 'all'
currentPage.value = 1
}
@@ -229,6 +246,10 @@ function openNodeGroupManagement() {
}
function handleSelectionChange(selection: AdminNodeItem[]) {
if (syncingSelection.value) {
return
}
const currentPageIds = paginatedNodes.value.map((item) => item.id)
const selectionIds = selection.map((item) => item.id)
const persistedIds = selectedNodeIds.value.filter((id) => !currentPageIds.includes(id))
@@ -281,6 +302,41 @@ async function handleBatchSubmit(payload: NodeBatchEditPayload) {
}
}
async function handleBatchDelete() {
if (!hasSelectedNodes.value) {
ElMessage.warning('请先勾选需要批量删除的节点')
return
}
const deleteCount = selectedNodes.value.length
try {
await ElMessageBox.confirm(
`确认批量删除 ${deleteCount} 个节点吗?此操作不可恢复。`,
'批量删除节点',
{
type: 'warning',
confirmButtonText: '确认删除',
cancelButtonText: '取消',
},
)
batchDeleting.value = true
await batchDeleteNodes([...selectedNodeIds.value])
clearSelection()
ElMessage.success(`已批量删除 ${deleteCount} 个节点`)
await loadNodeBoard()
} catch (error) {
if (error === 'cancel' || error === 'close') {
return
}
ElMessage.error(error instanceof Error ? error.message : '批量删除失败')
} finally {
batchDeleting.value = false
}
}
async function handleToggleShow(node: AdminNodeItem, nextValue: boolean) {
const previous = Boolean(node.show)
if (previous === nextValue) {
@@ -381,7 +437,7 @@ watch(
},
)
watch([keyword, typeFilter, groupFilter, relationFilter], () => {
watch([keyword, typeFilter, groupFilter, statusFilter, relationFilter], () => {
currentPage.value = 1
})
@@ -397,10 +453,7 @@ watch(
)
watch(
() => [
paginatedNodes.value.map((item) => item.id).join(','),
selectedNodeIds.value.join(','),
],
() => paginatedNodes.value.map((item) => item.id).join(','),
() => {
syncTableSelection()
},
@@ -415,7 +468,7 @@ watch(
<p class="nodes-kicker">Nodes</p>
<h1>节点管理</h1>
<span>
现在可以在同一页完成节点筛选分页浏览单行置顶批量修改以及新增编辑显隐和删除等运营动作
现在可以在同一页完成节点筛选在线 / 离线排查分页浏览单行置顶批量修改与批量删除以及新增编辑显隐和删除等运营动作
</span>
</div>
@@ -466,6 +519,12 @@ watch(
/>
</ElSelect>
<ElSelect v-model="statusFilter" class="toolbar-select" placeholder="状态">
<ElOption label="全部节点" value="all" />
<ElOption label="在线节点" value="online" />
<ElOption label="离线节点" value="offline" />
</ElSelect>
<ElSelect v-model="relationFilter" class="toolbar-select" placeholder="节点关系">
<ElOption label="全部节点" value="all" />
<ElOption label="父节点" value="parent" />
@@ -475,7 +534,17 @@ watch(
<div class="toolbar-actions">
<span class="scope-hint">{{ batchTargetLabel }}</span>
<ElButton :disabled="!hasSelectedNodes" @click="openBatchEditor">批量修改</ElButton>
<ElButton :disabled="!hasSelectedNodes || batchDeleting" @click="openBatchEditor">批量修改</ElButton>
<ElButton
type="danger"
plain
:disabled="!hasSelectedNodes"
:loading="batchDeleting"
@click="handleBatchDelete"
>
<ElIcon><Delete /></ElIcon>
批量删除
</ElButton>
<ElButton @click="openNodeGroupManagement">管理权限组</ElButton>
<ElButton @click="handleReset" :disabled="!hasActiveFilters">
<ElIcon><RefreshRight /></ElIcon>
@@ -486,7 +555,7 @@ watch(
</header>
<div v-if="hasSelectedNodes" class="selection-summary">
<span class="selection-summary__label">已勾选 {{ selectedNodes.length }} 个节点批量修改只会作用于这些节点</span>
<span class="selection-summary__label">已勾选 {{ selectedNodes.length }} 个节点批量修改与批量删除只会作用于这些节点</span>
<ElButton text @click="clearSelection">清空勾选</ElButton>
</div>
@@ -655,7 +724,7 @@ watch(
/>
<div class="footer-hint">
<ElIcon><Connection /></ElIcon>
<span>节点新增编辑置顶排序与批量修改已收敛到同一工作台</span>
<span>节点新增编辑置顶排序批量修改与批量删除已收敛到同一工作台</span>
</div>
</footer>
</section>