mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 16:16:42 +08:00
站点添加筛选功能
This commit is contained in:
@@ -24,10 +24,11 @@ const { t } = useI18n()
|
|||||||
const cardProps = defineProps({
|
const cardProps = defineProps({
|
||||||
site: Object as PropType<Site>,
|
site: Object as PropType<Site>,
|
||||||
data: Object as PropType<SiteUserData>,
|
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()
|
const createConfirm = useConfirm()
|
||||||
@@ -56,9 +57,6 @@ const resourceDialog = ref(false)
|
|||||||
// 用户数据弹窗
|
// 用户数据弹窗
|
||||||
const siteUserDataDialog = ref(false)
|
const siteUserDataDialog = ref(false)
|
||||||
|
|
||||||
// 站点使用统计
|
|
||||||
const siteStats = ref<SiteStatistic>({})
|
|
||||||
|
|
||||||
// 查询站点图标
|
// 查询站点图标
|
||||||
async function getSiteIcon() {
|
async function getSiteIcon() {
|
||||||
try {
|
try {
|
||||||
@@ -84,16 +82,8 @@ async function testSite() {
|
|||||||
testButtonText.value = t('site.testConnectivity')
|
testButtonText.value = t('site.testConnectivity')
|
||||||
testButtonDisable.value = false
|
testButtonDisable.value = false
|
||||||
|
|
||||||
getSiteStats()
|
// 测试完成后刷新统计数据
|
||||||
} catch (error) {
|
emit('refresh-stats', cardProps.site?.domain)
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询站点使用统计
|
|
||||||
async function getSiteStats() {
|
|
||||||
try {
|
|
||||||
siteStats.value = await api.get(`site/statistic/${cardProps.site?.domain}`)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
@@ -140,16 +130,17 @@ async function deleteSiteInfo() {
|
|||||||
|
|
||||||
// 根据站点状态显示不同的状态图标
|
// 根据站点状态显示不同的状态图标
|
||||||
const statColor = computed(() => {
|
const statColor = computed(() => {
|
||||||
if (isNullOrEmptyObject(siteStats.value)) {
|
if (!cardProps.stats || isNullOrEmptyObject(cardProps.stats)) {
|
||||||
return 'secondary'
|
return 'secondary'
|
||||||
}
|
}
|
||||||
if (siteStats.value?.lst_state == 1) {
|
if (cardProps.stats?.lst_state === 1) {
|
||||||
return 'error'
|
return 'error'
|
||||||
} else if (siteStats.value?.lst_state == 0) {
|
} else if (cardProps.stats?.lst_state === 0) {
|
||||||
if (!siteStats.value?.seconds) return 'secondary'
|
if (!cardProps.stats?.seconds) return 'secondary'
|
||||||
if (siteStats.value?.seconds >= 5) return 'warning'
|
if (cardProps.stats?.seconds >= 5) return 'warning'
|
||||||
return 'success'
|
return 'success'
|
||||||
}
|
}
|
||||||
|
return 'secondary'
|
||||||
})
|
})
|
||||||
|
|
||||||
// 数据百分比计算
|
// 数据百分比计算
|
||||||
@@ -185,19 +176,20 @@ function saveSite() {
|
|||||||
// 更新站点Cookie UA后的回调
|
// 更新站点Cookie UA后的回调
|
||||||
function onSiteCookieUpdated() {
|
function onSiteCookieUpdated() {
|
||||||
siteCookieDialog.value = false
|
siteCookieDialog.value = false
|
||||||
getSiteStats()
|
// Cookie更新后刷新统计数据
|
||||||
|
emit('refresh-stats', cardProps.site?.domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 资源浏览弹窗关闭后的回调
|
// 资源浏览弹窗关闭后的回调
|
||||||
function onSiteResourceDone() {
|
function onSiteResourceDone() {
|
||||||
resourceDialog.value = false
|
resourceDialog.value = false
|
||||||
getSiteStats()
|
// 资源操作完成后刷新统计数据
|
||||||
|
emit('refresh-stats', cardProps.site?.domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 装载时查询站点图标
|
// 装载时查询站点图标
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getSiteIcon()
|
getSiteIcon()
|
||||||
getSiteStats()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ export default {
|
|||||||
noData: 'No data',
|
noData: 'No data',
|
||||||
noContent: 'No relevant content found',
|
noContent: 'No relevant content found',
|
||||||
all: 'All',
|
all: 'All',
|
||||||
|
active: 'Active',
|
||||||
|
inactive: 'Inactive',
|
||||||
|
filter: 'Filter',
|
||||||
|
noMatchingData: 'No matching data',
|
||||||
|
tryChangingFilters: 'Try changing filters',
|
||||||
default: 'Default',
|
default: 'Default',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
create: 'Create',
|
create: 'Create',
|
||||||
@@ -884,6 +889,10 @@ export default {
|
|||||||
testing: 'Testing ...',
|
testing: 'Testing ...',
|
||||||
testSuccess: '{name} connectivity test successful, ready to use!',
|
testSuccess: '{name} connectivity test successful, ready to use!',
|
||||||
testFailed: '{name} connectivity test failed: {message}',
|
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?',
|
deleteConfirm: 'Are you sure you want to delete this site?',
|
||||||
deleteSuccess: '{name} deleted successfully!',
|
deleteSuccess: '{name} deleted successfully!',
|
||||||
deleteFailed: '{name} deletion failed: {message}',
|
deleteFailed: '{name} deletion failed: {message}',
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ export default {
|
|||||||
noData: '暂无数据',
|
noData: '暂无数据',
|
||||||
noContent: '没有找到相关内容',
|
noContent: '没有找到相关内容',
|
||||||
all: '全部',
|
all: '全部',
|
||||||
|
active: '激活',
|
||||||
|
inactive: '未激活',
|
||||||
|
filter: '筛选',
|
||||||
|
noMatchingData: '没有符合条件的数据',
|
||||||
|
tryChangingFilters: '请尝试更改筛选条件',
|
||||||
default: '默认',
|
default: '默认',
|
||||||
name: '名称',
|
name: '名称',
|
||||||
create: '新建',
|
create: '新建',
|
||||||
@@ -881,6 +886,10 @@ export default {
|
|||||||
testing: '测试中 ...',
|
testing: '测试中 ...',
|
||||||
testSuccess: '{name} 连通性测试成功,可正常使用!',
|
testSuccess: '{name} 连通性测试成功,可正常使用!',
|
||||||
testFailed: '{name} 连通性测试失败:{message}',
|
testFailed: '{name} 连通性测试失败:{message}',
|
||||||
|
connectionNormal: '连接正常',
|
||||||
|
connectionSlow: '连接缓慢',
|
||||||
|
connectionFailed: '连接失败',
|
||||||
|
connectionUnknown: '连接未知',
|
||||||
deleteConfirm: '是否确认删除站点?',
|
deleteConfirm: '是否确认删除站点?',
|
||||||
deleteSuccess: '{name} 删除成功!',
|
deleteSuccess: '{name} 删除成功!',
|
||||||
deleteFailed: '{name} 删除失败:{message}',
|
deleteFailed: '{name} 删除失败:{message}',
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ export default {
|
|||||||
noData: '暫無數據',
|
noData: '暫無數據',
|
||||||
noContent: '沒有找到相關內容',
|
noContent: '沒有找到相關內容',
|
||||||
all: '全部',
|
all: '全部',
|
||||||
|
active: '激活',
|
||||||
|
inactive: '未激活',
|
||||||
|
filter: '篩選',
|
||||||
|
noMatchingData: '沒有符合條件的數據',
|
||||||
|
tryChangingFilters: '請嘗試更改篩選條件',
|
||||||
default: '默認',
|
default: '默認',
|
||||||
name: '名稱',
|
name: '名稱',
|
||||||
create: '新建',
|
create: '新建',
|
||||||
@@ -880,6 +885,10 @@ export default {
|
|||||||
testing: '測試中 ...',
|
testing: '測試中 ...',
|
||||||
testSuccess: '{name} 連通性測試成功,可正常使用!',
|
testSuccess: '{name} 連通性測試成功,可正常使用!',
|
||||||
testFailed: '{name} 連通性測試失敗:{message}',
|
testFailed: '{name} 連通性測試失敗:{message}',
|
||||||
|
connectionNormal: '連接正常',
|
||||||
|
connectionSlow: '連接緩慢',
|
||||||
|
connectionFailed: '連接失敗',
|
||||||
|
connectionUnknown: '連接未知',
|
||||||
deleteConfirm: '是否確認刪除站點?',
|
deleteConfirm: '是否確認刪除站點?',
|
||||||
deleteSuccess: '{name} 刪除成功!',
|
deleteSuccess: '{name} 刪除成功!',
|
||||||
deleteFailed: '{name} 刪除失敗:{message}',
|
deleteFailed: '{name} 刪除失敗:{message}',
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ const siteList = ref<Site[]>([])
|
|||||||
// 站点数据列表
|
// 站点数据列表
|
||||||
const userDataList = ref<SiteUserData[]>([])
|
const userDataList = ref<SiteUserData[]>([])
|
||||||
|
|
||||||
|
// 站点统计数据列表
|
||||||
|
const siteStatsList = ref<{ [domain: string]: any }>({})
|
||||||
|
|
||||||
// 是否刷新过
|
// 是否刷新过
|
||||||
const isRefreshed = ref(false)
|
const isRefreshed = ref(false)
|
||||||
|
|
||||||
@@ -31,6 +34,44 @@ const loading = ref(false)
|
|||||||
// 新增站点对话框
|
// 新增站点对话框
|
||||||
const siteAddDialog = 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() {
|
async function fetchData() {
|
||||||
try {
|
try {
|
||||||
@@ -38,6 +79,8 @@ async function fetchData() {
|
|||||||
siteList.value = await api.get('site/')
|
siteList.value = await api.get('site/')
|
||||||
loading.value = false
|
loading.value = false
|
||||||
isRefreshed.value = true
|
isRefreshed.value = true
|
||||||
|
// 获取站点列表后,获取统计数据
|
||||||
|
await fetchSiteStats()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(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() {
|
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 {
|
try {
|
||||||
const result: { [key: string]: any } = await api.post('site/priorities', priorities)
|
const result: { [key: string]: any } = await api.post('site/priorities', priorities)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -71,12 +158,39 @@ function getUserData(domain: string) {
|
|||||||
return userDataList.value.find(userData => userData.domain === domain)
|
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() {
|
function onSiteSave() {
|
||||||
siteAddDialog.value = false
|
siteAddDialog.value = false
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 选择筛选选项
|
||||||
|
function selectFilter(value: string) {
|
||||||
|
filterOption.value = value
|
||||||
|
filterMenu.value = false
|
||||||
|
}
|
||||||
|
|
||||||
// 加载时获取数据
|
// 加载时获取数据
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
fetchData()
|
fetchData()
|
||||||
@@ -101,28 +215,77 @@ useDynamicButton({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="card-list-container">
|
<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" />
|
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||||
<draggable
|
<draggable
|
||||||
v-if="siteList.length > 0"
|
v-if="filteredSiteList.length > 0"
|
||||||
v-model="siteList"
|
v-model="filteredSiteList"
|
||||||
@end="savaSitesPriority"
|
@end="savaSitesPriority"
|
||||||
handle=".cursor-move"
|
handle=".cursor-move"
|
||||||
item-key="id"
|
item-key="id"
|
||||||
tag="div"
|
tag="div"
|
||||||
:component-data="{ 'class': 'grid gap-4 grid-site-card px-2' }"
|
:component-data="{ 'class': 'grid gap-4 grid-site-card px-2' }"
|
||||||
|
:disabled="filterOption !== 'all'"
|
||||||
>
|
>
|
||||||
<template #item="{ element }">
|
<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>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
</div>
|
</div>
|
||||||
<NoDataFound
|
<NoDataFound
|
||||||
v-if="siteList.length === 0 && isRefreshed"
|
v-if="filteredSiteList.length === 0 && isRefreshed"
|
||||||
error-code="404"
|
error-code="404"
|
||||||
:error-title="t('site.noSites')"
|
:error-title="filterOption === 'all' ? t('site.noSites') : t('common.noMatchingData')"
|
||||||
:error-description="t('site.sitesWillBeShownHere')"
|
:error-description="filterOption === 'all' ? t('site.sitesWillBeShownHere') : t('common.tryChangingFilters')"
|
||||||
/>
|
/>
|
||||||
<!-- 新增站点按钮 -->
|
<!-- 新增站点按钮 -->
|
||||||
<VFab
|
<VFab
|
||||||
|
|||||||
Reference in New Issue
Block a user