Improve website groups loading state
This commit is contained in:
@@ -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,10 +69,39 @@
|
|||||||
>
|
>
|
||||||
<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>
|
||||||
<el-table :data="sortedWebsiteGroups" v-loading="groupsLoading" row-key="id" size="small" style="width:100%">
|
<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="name" label="分组" min-width="150" />
|
||||||
<el-table-column prop="id" label="ID" min-width="130" />
|
<el-table-column prop="id" label="ID" min-width="130" />
|
||||||
<el-table-column prop="description" label="描述" min-width="150" />
|
<el-table-column prop="description" label="描述" min-width="150" />
|
||||||
@@ -81,11 +110,13 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="140">
|
<el-table-column label="操作" width="140">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button size="small" text :disabled="!selectedWebsite" @click="openBindingCreate(selectedWebsite, row)">绑定</el-button>
|
<el-button size="small" text :disabled="!selectedWebsite || groupsLoading" @click="openBindingCreate(selectedWebsite, row)">绑定</el-button>
|
||||||
<el-button size="small" text :disabled="!selectedWebsite" @click="openGroupEdit(row)">编辑</el-button>
|
<el-button size="small" text :disabled="!selectedWebsite || groupsLoading" @click="openGroupEdit(row)">编辑</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</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,17 +1357,31 @@ 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 {
|
||||||
|
if (requestSeq === groupRequestSeq.value && selectedWebsite.value?.id === websiteId) {
|
||||||
groupsLoading.value = false
|
groupsLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadBindingWebsiteGroups(websiteId: number) {
|
async function loadBindingWebsiteGroups(websiteId: number) {
|
||||||
if (!websiteId) {
|
if (!websiteId) {
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user