Improve website groups loading state

This commit is contained in:
SmartUp Developer
2026-07-04 00:25:01 +08:00
parent 1bba833291
commit 0cce5d53fe
+150 -21
View File
@@ -53,7 +53,7 @@
</div> </div>
<div class="content-grid"> <div class="content-grid">
<div class="panel"> <div class="panel website-groups-panel">
<div class="panel-head"> <div class="panel-head">
<div> <div>
<div class="panel-title">我的网站分组</div> <div class="panel-title">我的网站分组</div>
@@ -69,23 +69,54 @@
> >
<el-option v-for="site in websites" :key="site.id" :label="site.name" :value="site.id" /> <el-option v-for="site in websites" :key="site.id" :label="site.name" :value="site.id" />
</el-select> </el-select>
<el-button size="small" :disabled="!selectedWebsite" :loading="groupsLoading" @click="loadWebsiteGroups">拉取分组</el-button> <el-button size="small" :disabled="!selectedWebsite || groupsLoading" :loading="groupsLoading" @click="loadWebsiteGroups">拉取分组</el-button>
<el-button size="small" type="primary" :disabled="!selectedWebsite" @click="openGroupCreate">新增分组</el-button> <el-button size="small" type="primary" :disabled="!selectedWebsite || groupsLoading" @click="openGroupCreate">新增分组</el-button>
</div>
<div class="website-groups-body">
<div v-if="!selectedWebsite" class="groups-state-panel">
<div class="groups-state-title">请选择网站</div>
<div class="groups-state-sub">选择一个目标网站后查看分组</div>
</div>
<div v-else-if="groupsLoading && sortedWebsiteGroups.length === 0" class="groups-state-panel groups-state-loading">
<span class="groups-spinner" aria-hidden="true" />
<div class="groups-state-title">正在拉取网站分组...</div>
<div class="groups-state-sub">如果上游响应较慢请稍等超时后会显示具体错误</div>
</div>
<div v-else-if="groupsError" class="groups-state-panel groups-state-error">
<div class="groups-state-title">分组拉取失败</div>
<div class="groups-state-sub">{{ groupsError }}</div>
<el-button size="small" :loading="groupsLoading" @click="loadWebsiteGroups">重新拉取</el-button>
</div>
<div v-else-if="sortedWebsiteGroups.length === 0" class="groups-state-panel">
<div class="groups-state-title">暂无分组</div>
<div class="groups-state-sub">当前网站没有返回分组或还没有点击拉取</div>
<el-button size="small" :loading="groupsLoading" @click="loadWebsiteGroups">拉取分组</el-button>
</div>
<template v-else>
<div v-if="groupsLoading" class="groups-refresh-strip">
<span class="groups-spinner groups-spinner-small" aria-hidden="true" />
正在刷新分组...
</div>
<el-table :data="sortedWebsiteGroups" row-key="id" size="small" style="width:100%">
<el-table-column prop="name" label="分组" min-width="150" />
<el-table-column prop="id" label="ID" min-width="130" />
<el-table-column prop="description" label="描述" min-width="150" />
<el-table-column label="倍率" width="100">
<template #default="{ row }"><span class="rate-value">{{ row.rate_multiplier ?? '—' }}</span></template>
</el-table-column>
<el-table-column label="操作" width="140">
<template #default="{ row }">
<el-button size="small" text :disabled="!selectedWebsite || groupsLoading" @click="openBindingCreate(selectedWebsite, row)">绑定</el-button>
<el-button size="small" text :disabled="!selectedWebsite || groupsLoading" @click="openGroupEdit(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</template>
</div> </div>
<el-table :data="sortedWebsiteGroups" v-loading="groupsLoading" row-key="id" size="small" style="width:100%">
<el-table-column prop="name" label="分组" min-width="150" />
<el-table-column prop="id" label="ID" min-width="130" />
<el-table-column prop="description" label="描述" min-width="150" />
<el-table-column label="倍率" width="100">
<template #default="{ row }"><span class="rate-value">{{ row.rate_multiplier ?? '—' }}</span></template>
</el-table-column>
<el-table-column label="操作" width="140">
<template #default="{ row }">
<el-button size="small" text :disabled="!selectedWebsite" @click="openBindingCreate(selectedWebsite, row)">绑定</el-button>
<el-button size="small" text :disabled="!selectedWebsite" @click="openGroupEdit(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</div> </div>
<div class="panel"> <div class="panel">
@@ -1050,6 +1081,8 @@ const websites = ref<(WebsiteData & { _testing?: boolean })[]>([])
const upstreams = ref<UpstreamData[]>([]) const upstreams = ref<UpstreamData[]>([])
const selectedWebsite = ref<WebsiteData | null>(null) const selectedWebsite = ref<WebsiteData | null>(null)
const websiteGroups = ref<WebsiteGroup[]>([]) const websiteGroups = ref<WebsiteGroup[]>([])
const groupsError = ref('')
const groupRequestSeq = ref(0)
const sortedWebsiteGroups = computed(() => { const sortedWebsiteGroups = computed(() => {
return [...websiteGroups.value].sort((a, b) => { return [...websiteGroups.value].sort((a, b) => {
const rateA = a.rate_multiplier !== null && a.rate_multiplier !== undefined && a.rate_multiplier !== '—' const rateA = a.rate_multiplier !== null && a.rate_multiplier !== undefined && a.rate_multiplier !== '—'
@@ -1303,6 +1336,8 @@ async function onSelectedWebsiteChange(value: number | string) {
const next = websites.value.find((site) => site.id === nextId) const next = websites.value.find((site) => site.id === nextId)
if (!next) return if (!next) return
selectedWebsite.value = next selectedWebsite.value = next
websiteGroups.value = []
groupsError.value = ''
await loadWebsiteGroups() await loadWebsiteGroups()
} }
@@ -1322,15 +1357,29 @@ async function loadUpstreamGroups() {
} }
async function loadWebsiteGroups() { async function loadWebsiteGroups() {
if (!selectedWebsite.value) return if (!selectedWebsite.value) {
groupRequestSeq.value += 1
websiteGroups.value = []
groupsError.value = ''
groupsLoading.value = false
return
}
const websiteId = selectedWebsite.value.id
const requestSeq = ++groupRequestSeq.value
groupsLoading.value = true groupsLoading.value = true
groupsError.value = ''
try { try {
const res = await websitesApi.groups(selectedWebsite.value.id) const res = await websitesApi.groups(websiteId)
if (requestSeq !== groupRequestSeq.value || selectedWebsite.value?.id !== websiteId) return
websiteGroups.value = res.data websiteGroups.value = res.data
} catch (e: any) { } catch (e: any) {
ElMessage.error(e.response?.data?.detail || '拉取分组失败') if (requestSeq !== groupRequestSeq.value || selectedWebsite.value?.id !== websiteId) return
groupsError.value = e.response?.data?.detail || e.message || '拉取分组失败'
ElMessage.error(groupsError.value)
} finally { } finally {
groupsLoading.value = false if (requestSeq === groupRequestSeq.value && selectedWebsite.value?.id === websiteId) {
groupsLoading.value = false
}
} }
} }
@@ -1460,6 +1509,8 @@ async function loadAll() {
function selectWebsite(row: WebsiteData) { function selectWebsite(row: WebsiteData) {
selectedWebsite.value = row selectedWebsite.value = row
websiteGroups.value = []
groupsError.value = ''
loadWebsiteGroups() loadWebsiteGroups()
} }
@@ -1547,6 +1598,9 @@ async function deleteWebsite(row: WebsiteData) {
ElMessage.success('已删除') ElMessage.success('已删除')
selectedWebsite.value = null selectedWebsite.value = null
websiteGroups.value = [] websiteGroups.value = []
groupsError.value = ''
groupRequestSeq.value += 1
groupsLoading.value = false
await loadAll() await loadAll()
} catch {} } catch {}
} }
@@ -2365,6 +2419,81 @@ onMounted(loadAll)
flex-shrink: 0; flex-shrink: 0;
} }
.website-groups-body {
min-height: 220px;
position: relative;
}
.groups-state-panel {
min-height: 220px;
padding: 32px 18px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
text-align: center;
color: var(--text-muted);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent),
rgba(255, 255, 255, 0.01);
}
.groups-state-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.groups-state-sub {
max-width: 360px;
font-size: 12px;
line-height: 1.6;
color: var(--text-muted);
overflow-wrap: anywhere;
}
.groups-state-loading .groups-state-title {
color: var(--color-primary-strong);
}
.groups-state-error .groups-state-title {
color: var(--color-danger);
}
.groups-spinner {
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid rgba(166, 141, 133, 0.25);
border-top-color: var(--color-primary-strong);
animation: groups-spin 0.8s linear infinite;
}
.groups-spinner-small {
width: 14px;
height: 14px;
border-width: 2px;
}
.groups-refresh-strip {
min-height: 34px;
padding: 0 14px;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid var(--border-color);
color: var(--text-muted);
font-size: 12px;
background: rgba(255, 255, 255, 0.02);
}
@keyframes groups-spin {
to {
transform: rotate(360deg);
}
}
.binding-actions { .binding-actions {
display: flex; display: flex;
align-items: center; align-items: center;