mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-11 18:10:49 +08:00
站点添加筛选功能
This commit is contained in:
@@ -24,10 +24,11 @@ const { t } = useI18n()
|
||||
const cardProps = defineProps({
|
||||
site: Object as PropType<Site>,
|
||||
data: Object as PropType<SiteUserData>,
|
||||
stats: Object as PropType<SiteStatistic>,
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['update', 'remove'])
|
||||
const emit = defineEmits(['update', 'remove', 'refresh-stats'])
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
@@ -56,9 +57,6 @@ const resourceDialog = ref(false)
|
||||
// 用户数据弹窗
|
||||
const siteUserDataDialog = ref(false)
|
||||
|
||||
// 站点使用统计
|
||||
const siteStats = ref<SiteStatistic>({})
|
||||
|
||||
// 查询站点图标
|
||||
async function getSiteIcon() {
|
||||
try {
|
||||
@@ -84,16 +82,8 @@ async function testSite() {
|
||||
testButtonText.value = t('site.testConnectivity')
|
||||
testButtonDisable.value = false
|
||||
|
||||
getSiteStats()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询站点使用统计
|
||||
async function getSiteStats() {
|
||||
try {
|
||||
siteStats.value = await api.get(`site/statistic/${cardProps.site?.domain}`)
|
||||
// 测试完成后刷新统计数据
|
||||
emit('refresh-stats', cardProps.site?.domain)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
@@ -140,16 +130,17 @@ async function deleteSiteInfo() {
|
||||
|
||||
// 根据站点状态显示不同的状态图标
|
||||
const statColor = computed(() => {
|
||||
if (isNullOrEmptyObject(siteStats.value)) {
|
||||
if (!cardProps.stats || isNullOrEmptyObject(cardProps.stats)) {
|
||||
return 'secondary'
|
||||
}
|
||||
if (siteStats.value?.lst_state == 1) {
|
||||
if (cardProps.stats?.lst_state === 1) {
|
||||
return 'error'
|
||||
} else if (siteStats.value?.lst_state == 0) {
|
||||
if (!siteStats.value?.seconds) return 'secondary'
|
||||
if (siteStats.value?.seconds >= 5) return 'warning'
|
||||
} else if (cardProps.stats?.lst_state === 0) {
|
||||
if (!cardProps.stats?.seconds) return 'secondary'
|
||||
if (cardProps.stats?.seconds >= 5) return 'warning'
|
||||
return 'success'
|
||||
}
|
||||
return 'secondary'
|
||||
})
|
||||
|
||||
// 数据百分比计算
|
||||
@@ -185,19 +176,20 @@ function saveSite() {
|
||||
// 更新站点Cookie UA后的回调
|
||||
function onSiteCookieUpdated() {
|
||||
siteCookieDialog.value = false
|
||||
getSiteStats()
|
||||
// Cookie更新后刷新统计数据
|
||||
emit('refresh-stats', cardProps.site?.domain)
|
||||
}
|
||||
|
||||
// 资源浏览弹窗关闭后的回调
|
||||
function onSiteResourceDone() {
|
||||
resourceDialog.value = false
|
||||
getSiteStats()
|
||||
// 资源操作完成后刷新统计数据
|
||||
emit('refresh-stats', cardProps.site?.domain)
|
||||
}
|
||||
|
||||
// 装载时查询站点图标
|
||||
onMounted(() => {
|
||||
getSiteIcon()
|
||||
getSiteStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ export default {
|
||||
noData: 'No data',
|
||||
noContent: 'No relevant content found',
|
||||
all: 'All',
|
||||
active: 'Active',
|
||||
inactive: 'Inactive',
|
||||
filter: 'Filter',
|
||||
noMatchingData: 'No matching data',
|
||||
tryChangingFilters: 'Try changing filters',
|
||||
default: 'Default',
|
||||
name: 'Name',
|
||||
create: 'Create',
|
||||
@@ -884,6 +889,10 @@ export default {
|
||||
testing: 'Testing ...',
|
||||
testSuccess: '{name} connectivity test successful, ready to use!',
|
||||
testFailed: '{name} connectivity test failed: {message}',
|
||||
connectionNormal: 'Connection Normal',
|
||||
connectionSlow: 'Connection Slow',
|
||||
connectionFailed: 'Connection Failed',
|
||||
connectionUnknown: 'Connection Unknown',
|
||||
deleteConfirm: 'Are you sure you want to delete this site?',
|
||||
deleteSuccess: '{name} deleted successfully!',
|
||||
deleteFailed: '{name} deletion failed: {message}',
|
||||
|
||||
@@ -19,6 +19,11 @@ export default {
|
||||
noData: '暂无数据',
|
||||
noContent: '没有找到相关内容',
|
||||
all: '全部',
|
||||
active: '激活',
|
||||
inactive: '未激活',
|
||||
filter: '筛选',
|
||||
noMatchingData: '没有符合条件的数据',
|
||||
tryChangingFilters: '请尝试更改筛选条件',
|
||||
default: '默认',
|
||||
name: '名称',
|
||||
create: '新建',
|
||||
@@ -881,6 +886,10 @@ export default {
|
||||
testing: '测试中 ...',
|
||||
testSuccess: '{name} 连通性测试成功,可正常使用!',
|
||||
testFailed: '{name} 连通性测试失败:{message}',
|
||||
connectionNormal: '连接正常',
|
||||
connectionSlow: '连接缓慢',
|
||||
connectionFailed: '连接失败',
|
||||
connectionUnknown: '连接未知',
|
||||
deleteConfirm: '是否确认删除站点?',
|
||||
deleteSuccess: '{name} 删除成功!',
|
||||
deleteFailed: '{name} 删除失败:{message}',
|
||||
|
||||
@@ -19,6 +19,11 @@ export default {
|
||||
noData: '暫無數據',
|
||||
noContent: '沒有找到相關內容',
|
||||
all: '全部',
|
||||
active: '激活',
|
||||
inactive: '未激活',
|
||||
filter: '篩選',
|
||||
noMatchingData: '沒有符合條件的數據',
|
||||
tryChangingFilters: '請嘗試更改篩選條件',
|
||||
default: '默認',
|
||||
name: '名稱',
|
||||
create: '新建',
|
||||
@@ -880,6 +885,10 @@ export default {
|
||||
testing: '測試中 ...',
|
||||
testSuccess: '{name} 連通性測試成功,可正常使用!',
|
||||
testFailed: '{name} 連通性測試失敗:{message}',
|
||||
connectionNormal: '連接正常',
|
||||
connectionSlow: '連接緩慢',
|
||||
connectionFailed: '連接失敗',
|
||||
connectionUnknown: '連接未知',
|
||||
deleteConfirm: '是否確認刪除站點?',
|
||||
deleteSuccess: '{name} 刪除成功!',
|
||||
deleteFailed: '{name} 刪除失敗:{message}',
|
||||
|
||||
@@ -22,6 +22,9 @@ const siteList = ref<Site[]>([])
|
||||
// 站点数据列表
|
||||
const userDataList = ref<SiteUserData[]>([])
|
||||
|
||||
// 站点统计数据列表
|
||||
const siteStatsList = ref<{ [domain: string]: any }>({})
|
||||
|
||||
// 是否刷新过
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
@@ -31,6 +34,44 @@ const loading = ref(false)
|
||||
// 新增站点对话框
|
||||
const siteAddDialog = ref(false)
|
||||
|
||||
// 筛选相关
|
||||
const filterMenu = ref(false)
|
||||
const filterOption = ref('all') // all, active, inactive, connected, slow, failed, unknown
|
||||
|
||||
// 筛选选项
|
||||
const filterOptions = computed(() => [
|
||||
{ value: 'all', label: t('common.all'), icon: 'mdi-format-list-bulleted' },
|
||||
{ value: 'active', label: t('common.active'), icon: 'mdi-check-circle', color: 'success' },
|
||||
{ value: 'inactive', label: t('common.inactive'), icon: 'mdi-stop-circle', color: 'error' },
|
||||
{ value: 'connected', label: t('site.connectionNormal'), icon: 'mdi-wifi', color: 'success' },
|
||||
{ value: 'slow', label: t('site.connectionSlow'), icon: 'mdi-wifi-strength-2', color: 'warning' },
|
||||
{ value: 'failed', label: t('site.connectionFailed'), icon: 'mdi-wifi-off', color: 'error' },
|
||||
{ value: 'unknown', label: t('site.connectionUnknown'), icon: 'mdi-help-circle', color: 'secondary' },
|
||||
])
|
||||
|
||||
// 筛选后的站点列表
|
||||
const filteredSiteList = computed(() => {
|
||||
if (filterOption.value === 'all') {
|
||||
return siteList.value
|
||||
}
|
||||
return siteList.value.filter(site => {
|
||||
if (filterOption.value === 'active') {
|
||||
return site.is_active
|
||||
} else if (filterOption.value === 'inactive') {
|
||||
return !site.is_active
|
||||
} else if (['connected', 'slow', 'failed', 'unknown'].includes(filterOption.value)) {
|
||||
const connectionStatus = getConnectionStatus(site.domain)
|
||||
return connectionStatus === filterOption.value
|
||||
}
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
// 当前筛选选项的显示信息
|
||||
const currentFilter = computed(() => {
|
||||
return filterOptions.value.find(option => option.value === filterOption.value)
|
||||
})
|
||||
|
||||
// 获取站点列表数据
|
||||
async function fetchData() {
|
||||
try {
|
||||
@@ -38,6 +79,8 @@ async function fetchData() {
|
||||
siteList.value = await api.get('site/')
|
||||
loading.value = false
|
||||
isRefreshed.value = true
|
||||
// 获取站点列表后,获取统计数据
|
||||
await fetchSiteStats()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
@@ -52,10 +95,54 @@ async function fetchUserData() {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取站点统计数据
|
||||
async function fetchSiteStats() {
|
||||
try {
|
||||
// 使用批量接口一次性获取所有站点统计数据
|
||||
const response = await api.get('site/statistic')
|
||||
const stats = response.data || response
|
||||
|
||||
// 将数组转换为以domain为键的对象
|
||||
const statsMap: { [domain: string]: any } = {}
|
||||
if (Array.isArray(stats)) {
|
||||
stats.forEach((stat: any) => {
|
||||
if (stat.domain) {
|
||||
statsMap[stat.domain] = stat
|
||||
}
|
||||
})
|
||||
}
|
||||
siteStatsList.value = statsMap
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch site statistics:', error)
|
||||
siteStatsList.value = {}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据站点统计数据判断连接状态
|
||||
function getConnectionStatus(domain: string) {
|
||||
const stats = siteStatsList.value[domain]
|
||||
if (!stats || Object.keys(stats).length === 0) {
|
||||
return 'unknown'
|
||||
}
|
||||
if (stats.lst_state === 1) {
|
||||
return 'failed'
|
||||
} else if (stats.lst_state === 0) {
|
||||
if (!stats.seconds) return 'unknown'
|
||||
if (stats.seconds >= 5) return 'slow'
|
||||
return 'connected'
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
// 保存站点排序
|
||||
async function savaSitesPriority() {
|
||||
// 只在显示全部站点时允许排序
|
||||
if (filterOption.value !== 'all') {
|
||||
return
|
||||
}
|
||||
|
||||
// 重新排序
|
||||
const priorities = siteList.value.map((site, index) => ({ id: site.id, pri: index + 1 }))
|
||||
const priorities = filteredSiteList.value.map((site, index) => ({ id: site.id, pri: index + 1 }))
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('site/priorities', priorities)
|
||||
if (result.success) {
|
||||
@@ -71,12 +158,39 @@ function getUserData(domain: string) {
|
||||
return userDataList.value.find(userData => userData.domain === domain)
|
||||
}
|
||||
|
||||
// 根据站点域名获取统计数据
|
||||
function getSiteStats(domain: string) {
|
||||
return siteStatsList.value[domain] || {}
|
||||
}
|
||||
|
||||
// 处理站点统计数据刷新请求
|
||||
async function handleRefreshStats(domain?: string) {
|
||||
if (domain) {
|
||||
// 刷新特定站点的统计数据
|
||||
try {
|
||||
const stats = await api.get(`site/statistic/${domain}`)
|
||||
siteStatsList.value[domain] = stats
|
||||
} catch (error) {
|
||||
console.error(`Failed to refresh stats for ${domain}:`, error)
|
||||
}
|
||||
} else {
|
||||
// 刷新所有站点统计数据
|
||||
await fetchSiteStats()
|
||||
}
|
||||
}
|
||||
|
||||
// 更新站点事件时
|
||||
function onSiteSave() {
|
||||
siteAddDialog.value = false
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 选择筛选选项
|
||||
function selectFilter(value: string) {
|
||||
filterOption.value = value
|
||||
filterMenu.value = false
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(() => {
|
||||
fetchData()
|
||||
@@ -101,28 +215,77 @@ useDynamicButton({
|
||||
|
||||
<template>
|
||||
<div class="card-list-container">
|
||||
<!-- 页面标题 -->
|
||||
<VPageContentTitle :title="t('navItems.siteManager')" />
|
||||
<!-- 页面标题和筛选按钮 -->
|
||||
<div class="d-flex justify-space-between align-center mb-4">
|
||||
<VPageContentTitle :title="t('navItems.siteManager')" class="mb-0" />
|
||||
<!-- 筛选按钮 -->
|
||||
<VMenu v-model="filterMenu" offset-y :close-on-content-click="false" location="bottom end">
|
||||
<template #activator="{ props }">
|
||||
<VBtn
|
||||
v-bind="props"
|
||||
:icon="display.smAndDown.value"
|
||||
:variant="filterOption === 'all' ? 'text' : 'tonal'"
|
||||
:color="currentFilter?.color"
|
||||
>
|
||||
<VIcon :icon="currentFilter?.icon || 'mdi-filter'" />
|
||||
<span v-if="!display.smAndDown.value" class="ml-2">
|
||||
{{ currentFilter?.label }}
|
||||
</span>
|
||||
<VIcon v-if="!display.smAndDown.value" icon="mdi-chevron-down" class="ml-1" />
|
||||
</VBtn>
|
||||
</template>
|
||||
|
||||
<!-- 筛选菜单 -->
|
||||
<VCard min-width="200">
|
||||
<VList>
|
||||
<VListSubheader>{{ t('common.filter') }}</VListSubheader>
|
||||
<VListItem
|
||||
v-for="option in filterOptions"
|
||||
:key="option.value"
|
||||
:active="filterOption === option.value"
|
||||
@click="selectFilter(option.value)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="option.icon" :color="option.color" />
|
||||
</template>
|
||||
<VListItemTitle>{{ option.label }}</VListItemTitle>
|
||||
<template #append>
|
||||
<VIcon v-if="filterOption === option.value" icon="mdi-check" color="primary" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCard>
|
||||
</VMenu>
|
||||
</div>
|
||||
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<draggable
|
||||
v-if="siteList.length > 0"
|
||||
v-model="siteList"
|
||||
v-if="filteredSiteList.length > 0"
|
||||
v-model="filteredSiteList"
|
||||
@end="savaSitesPriority"
|
||||
handle=".cursor-move"
|
||||
item-key="id"
|
||||
tag="div"
|
||||
:component-data="{ 'class': 'grid gap-4 grid-site-card px-2' }"
|
||||
:disabled="filterOption !== 'all'"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<SiteCard :site="element" :data="getUserData(element.domain)" @remove="fetchData" @update="fetchData" />
|
||||
<SiteCard
|
||||
:site="element"
|
||||
:data="getUserData(element.domain)"
|
||||
:stats="getSiteStats(element.domain)"
|
||||
@remove="fetchData"
|
||||
@update="fetchData"
|
||||
@refresh-stats="handleRefreshStats"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="siteList.length === 0 && isRefreshed"
|
||||
v-if="filteredSiteList.length === 0 && isRefreshed"
|
||||
error-code="404"
|
||||
:error-title="t('site.noSites')"
|
||||
:error-description="t('site.sitesWillBeShownHere')"
|
||||
:error-title="filterOption === 'all' ? t('site.noSites') : t('common.noMatchingData')"
|
||||
:error-description="filterOption === 'all' ? t('site.sitesWillBeShownHere') : t('common.tryChangingFilters')"
|
||||
/>
|
||||
<!-- 新增站点按钮 -->
|
||||
<VFab
|
||||
|
||||
Reference in New Issue
Block a user