From 02e41fa80eced344c5533dd4fa64d6c2afb798b0 Mon Sep 17 00:00:00 2001 From: liumangmang Date: Wed, 1 Jul 2026 15:11:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=9B=91=E5=90=AC=E4=B8=8A=E6=B8=B8?= =?UTF-8?q?=E5=88=86=E7=BB=84=E6=94=B9=E7=94=A8=E4=B8=93=E7=94=A8=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E9=80=89=E6=8B=A9=E5=99=A8=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E3=80=81=E5=A4=9A=E9=80=89=E5=8F=8A=E5=B7=B2?= =?UTF-8?q?=E9=80=89Tag=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/views/Websites.vue | 545 +++++++++++++++++++++++++++++++- 1 file changed, 540 insertions(+), 5 deletions(-) diff --git a/frontend/src/views/Websites.vue b/frontend/src/views/Websites.vue index 2172a2d..3690616 100644 --- a/frontend/src/views/Websites.vue +++ b/frontend/src/views/Websites.vue @@ -219,9 +219,22 @@ - - - +
+ 已选 {{ sourceGroupKeys.length }} 个上游分组 + 选择分组 +
+
+ + {{ getGroupLabel(key) }} + +
+
暂无选择,请点击按钮进行选择
@@ -247,6 +260,135 @@ + + + + + +
+ +
+
上游站点
+
+
+ 全部上游 + {{ upstreamGroupOptions.length }} +
+
+ {{ upstream.name }} + {{ upstreamCounts[upstream.id] || 0 }} +
+
+
+ + +
+
+ + 可选分组列表 (已选 {{ tempSelectedKeys.filter(k => filteredGroupsForModal.some(g => g.key === k)).length }}/{{ filteredGroupsForModal.length }}) + +
+ +
+
+ 暂无分组快照,请先同步上游 +
+
+ 没有匹配的分组快照 +
+
+ +
+
+ {{ item.source.upstream_name }} + + 倍率: {{ item.rate }} + +
+
+ {{ item.source.group_name }} + ID: {{ item.source.group_id }} +
+
+
+
+
+ + +
+
+ 已选分组 ({{ tempSelectedKeys.length }}) + 清空 +
+ +
+
+ 暂无选定分组 +
+
+
+ + {{ getGroupDetails(key)?.source.upstream_name }} + + + {{ getGroupDetails(key)?.source.group_name }} + +
+
+ {{ key }} +
+ + + +
+
+
+
+ + +
+ @@ -519,7 +661,7 @@ import { computed, onMounted, ref } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import type { FormInstance } from 'element-plus' import dayjs from 'dayjs' -import { ArrowDown, Delete, Edit, Plus, Grid, Connection, Link, Upload, Key, Refresh, Sort, WarningFilled } from '@element-plus/icons-vue' +import { ArrowDown, Delete, Edit, Plus, Grid, Connection, Link, Upload, Key, Refresh, Sort, WarningFilled, Search, Close } from '@element-plus/icons-vue' import { upstreamsApi, websitesApi, @@ -645,7 +787,7 @@ const organizeResults = ref([]) const organizeMessage = ref('') const upstreamGroupOptions = computed(() => { - const rows: Array<{ key: string; label: string; source: BindingSourceGroup }> = [] + const rows: Array<{ key: string; label: string; rate: string | number; source: BindingSourceGroup }> = [] for (const upstream of upstreams.value) { const groups = snapshotsByUpstream.value[upstream.id] || [] for (const group of groups) { @@ -658,6 +800,7 @@ const upstreamGroupOptions = computed(() => { rows.push({ key: `${upstream.id}::${group.group_id}`, label: `${upstream.name} / ${source.group_name} (${group.rate || '—'})`, + rate: group.rate || '—', source, }) } @@ -1041,6 +1184,104 @@ async function saveBinding() { } } +// 选择上游分组弹窗相关状态与方法 +const groupSelectVisible = ref(false) +const groupSearchQuery = ref('') +const selectedUpstreamId = ref('') +const tempSelectedKeys = ref([]) + +function openGroupSelectModal() { + tempSelectedKeys.value = [...sourceGroupKeys.value] + groupSearchQuery.value = '' + selectedUpstreamId.value = '' + groupSelectVisible.value = true +} + +function getGroupLabel(key: string) { + const found = upstreamGroupOptions.value.find(item => item.key === key) + return found ? found.label : key +} + +function removeSelectedKey(key: string) { + sourceGroupKeys.value = sourceGroupKeys.value.filter(k => k !== key) + onSourceGroupsChange(sourceGroupKeys.value) +} + +function getGroupDetails(key: string) { + return upstreamGroupOptions.value.find(item => item.key === key) +} + +const upstreamCounts = computed(() => { + const counts: Record = {} + for (const upstream of upstreams.value) { + const groups = snapshotsByUpstream.value[upstream.id] || [] + counts[upstream.id] = groups.length + } + return counts +}) + +const filteredGroupsForModal = computed(() => { + const query = groupSearchQuery.value.trim().toLowerCase() + return upstreamGroupOptions.value.filter(item => { + if (selectedUpstreamId.value !== '' && item.source.upstream_id !== selectedUpstreamId.value) { + return false + } + if (query) { + const matchUpstreamName = item.source.upstream_name.toLowerCase().includes(query) + const matchGroupName = item.source.group_name.toLowerCase().includes(query) + const matchGroupId = String(item.source.group_id).toLowerCase().includes(query) + const matchRate = String(item.rate).toLowerCase().includes(query) + return matchUpstreamName || matchGroupName || matchGroupId || matchRate + } + return true + }) +}) + +const isAllFilteredSelected = computed(() => { + if (filteredGroupsForModal.value.length === 0) return false + return filteredGroupsForModal.value.every(item => tempSelectedKeys.value.includes(item.key)) +}) + +const isSomeFilteredSelected = computed(() => { + if (filteredGroupsForModal.value.length === 0) return false + const selectedCount = filteredGroupsForModal.value.filter(item => tempSelectedKeys.value.includes(item.key)).length + return selectedCount > 0 && selectedCount < filteredGroupsForModal.value.length +}) + +function toggleSelectAllFiltered(checked: boolean) { + if (checked) { + const keysToAdd = filteredGroupsForModal.value.map(item => item.key) + tempSelectedKeys.value = Array.from(new Set([...tempSelectedKeys.value, ...keysToAdd])) + } else { + const keysToRemove = new Set(filteredGroupsForModal.value.map(item => item.key)) + tempSelectedKeys.value = tempSelectedKeys.value.filter(key => !keysToRemove.has(key)) + } +} + +function toggleGroupKey(key: string) { + const idx = tempSelectedKeys.value.indexOf(key) + if (idx > -1) { + tempSelectedKeys.value.splice(idx, 1) + } else { + tempSelectedKeys.value.push(key) + } +} + +function removeTempKey(key: string) { + tempSelectedKeys.value = tempSelectedKeys.value.filter(k => k !== key) +} + +function clearAllTempKeys() { + tempSelectedKeys.value = [] +} + +function confirmGroupSelection() { + sourceGroupKeys.value = [...tempSelectedKeys.value] + onSourceGroupsChange(sourceGroupKeys.value) + groupSelectVisible.value = false +} + + async function syncBinding(row: GroupBindingData & { _syncing?: boolean }) { row._syncing = true try { @@ -1483,4 +1724,298 @@ onMounted(loadAll) } .mapping-row .el-select { width: 100% !important; } } + +/* Custom group selector layout styles */ +.selected-groups-summary { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + background: rgba(255, 244, 232, 0.02); + border: 1px solid var(--border-color); + border-radius: 8px; + margin-bottom: 8px; +} + +.summary-text { + font-size: 13px; + color: var(--text-secondary); +} + +.highlight-text { + color: var(--color-primary-strong); + font-weight: 700; +} + +.selected-tags-box { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 10px; + background: rgba(0, 0, 0, 0.1); + border: 1px solid var(--border-color); + border-radius: 8px; + max-height: 140px; + overflow-y: auto; +} + +.group-tag { + background: rgba(217, 139, 66, 0.08) !important; + border-color: rgba(217, 139, 66, 0.2) !important; + color: var(--color-primary-strong) !important; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.no-groups-hint { + font-size: 12px; + color: var(--text-muted); + text-align: center; + padding: 12px; + border: 1px dashed var(--border-color); + border-radius: 8px; +} + +.group-selector-layout { + display: grid; + grid-template-columns: 180px 1fr 200px; + gap: 16px; + height: 450px; + border: 1px solid var(--border-color); + border-radius: 8px; + background: var(--bg-surface); + overflow: hidden; +} + +.left-panel, .middle-panel, .right-panel { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.left-panel { + border-right: 1px solid var(--border-color); +} +.right-panel { + border-left: 1px solid var(--border-color); +} + +.panel-header { + padding: 10px 14px; + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + background: rgba(255, 244, 232, 0.03); + border-bottom: 1px solid var(--border-color); + min-height: 38px; + display: flex; + align-items: center; +} + +.panel-header.flex-header { + justify-content: space-between; +} + +.list-container { + flex: 1; + overflow-y: auto; + padding: 8px; +} + +/* Left panel items */ +.left-panel .list-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 8px 12px; + border-radius: 6px; + cursor: pointer; + margin-bottom: 4px; + transition: background 0.15s ease; + font-size: 13px; + color: var(--text-secondary); +} + +.left-panel .list-item:hover { + background: rgba(255, 244, 232, 0.04); + color: var(--text-primary); +} + +.left-panel .list-item.active { + background: var(--color-primary-soft); + color: var(--color-primary-strong); + font-weight: 500; +} + +.upstream-name-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Middle panel group items */ +.group-row-item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + border-radius: 6px; + cursor: pointer; + margin-bottom: 6px; + border: 1px solid transparent; + transition: all 0.15s ease; +} + +.group-row-item:hover { + background: rgba(255, 244, 232, 0.03); + border-color: rgba(218, 183, 142, 0.1); +} + +.group-row-item.selected { + background: rgba(217, 139, 66, 0.04); + border-color: rgba(217, 139, 66, 0.15); +} + +.group-row-content { + flex: 1; + min-width: 0; +} + +.row-top, .row-bottom { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; +} + +.row-bottom { + margin-top: 4px; +} + +.row-upstream-name { + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + color: var(--text-muted); +} + +.row-rate { + font-size: 11px; + color: var(--text-muted); +} + +.rate-highlight { + color: var(--color-warning); + font-weight: 600; +} + +.row-group-name { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.row-group-id { + font-size: 11px; + color: var(--text-soft); + font-family: monospace; +} + +/* Right panel selected items */ +.selected-row-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 6px 10px; + background: rgba(255, 244, 232, 0.02); + border: 1px solid var(--border-color); + border-radius: 6px; + margin-bottom: 6px; + font-size: 12px; +} + +.selected-row-info { + display: flex; + flex-direction: column; + min-width: 0; + flex: 1; +} + +.selected-upstream-name { + font-size: 10px; + color: var(--text-muted); + text-transform: uppercase; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.selected-group-name { + font-weight: 500; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Empty Hints */ +.empty-hint-dialog { + text-align: center; + padding: 40px 16px; + color: var(--text-muted); + font-size: 13px; +} +.empty-hint-dialog.small { + padding: 24px 12px; + font-size: 12px; +} + +/* Responsive dialog styling */ +@media (max-width: 768px) { + .group-selector-layout { + display: flex; + flex-direction: column; + height: auto; + max-height: 480px; + overflow-y: auto; + } + + .left-panel { + border-right: none; + border-bottom: 1px solid var(--border-color); + max-height: 120px; + height: auto; + } + + .left-panel .list-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 6px; + padding: 8px; + } + + .left-panel .list-item { + margin-bottom: 0; + padding: 4px 10px; + } + + .middle-panel { + max-height: 240px; + } + + .right-panel { + border-left: none; + border-top: 1px solid var(--border-color); + max-height: 160px; + } +}