diff --git a/package.json b/package.json index be58faca..d9b6a441 100644 --- a/package.json +++ b/package.json @@ -101,4 +101,4 @@ "resolutions": { "postcss": "8" } -} \ No newline at end of file +} diff --git a/src/@core/utils/formatters.ts b/src/@core/utils/formatters.ts index 1b54972c..4a8b4d3f 100644 --- a/src/@core/utils/formatters.ts +++ b/src/@core/utils/formatters.ts @@ -60,19 +60,25 @@ export const prefixWithPlus = (value: number) => (value > 0 ? `+${value}` : valu export const formatSeason = (value: string) => (value ? `S${value.padStart(2, '0')}` : '') // 格式化为xx[TGMK]B -export function formatFileSize(bytes: number, decimals = 2) { - if (bytes < 0) throw new Error('字节数不能为负数。') +export function formatFileSize(bytes: number, decimals = 2, prefix = false) { + // 负数标记 + let negative = false + let size = bytes + if (bytes < 0) { + negative = true + size = Math.abs(bytes) + } const units = ['B', 'KB', 'MB', 'GB', 'TB'] - let size = bytes let unitIndex = 0 while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024 unitIndex++ } - - return `${size.toFixed(decimals)} ${units[unitIndex]}` + if (negative) return `-${size.toFixed(decimals)} ${units[unitIndex]}` + else + return prefix ? `+${size.toFixed(decimals)} ${units[unitIndex]}` : `${size.toFixed(decimals)} ${units[unitIndex]}` } // 将时间秒格式化为时分秒 diff --git a/src/api/types.ts b/src/api/types.ts index b4041a75..7ecea116 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -435,6 +435,48 @@ export interface SiteStatistic { note?: string } +// 站点用户数据 +export interface SiteUserData { + // 站点域名 + domain?: string + // 用户名 + username?: string + // 用户ID + userid?: number + // 用户等级 + user_level?: string + // 加入时间 + join_at?: string + // 积分 + bonus?: number // 默认为 0.0 + // 上传量 + upload?: number // 默认为 0 + // 下载量 + download?: number // 默认为 0 + // 分享率 + ratio?: number // 默认为 0 + // 做种数 + seeding?: number // 默认为 0 + // 下载数 + leeching?: number // 默认为 0 + // 做种体积 + seeding_size?: number // 默认为 0 + // 下载体积 + leeching_size?: number // 默认为 0 + // 做种人数, 种子大小 + seeding_info?: any[] // 默认为空数组 + // 未读消息 + message_unread?: number // 默认为 0 + // 未读消息内容 + message_unread_contents?: any[] // 默认为空数组 + // 错误信息 + err_msg?: string | null // 默认为 null + // 更新日期 + updated_day?: string + // 更新时间 + updated_time?: string +} + // 正在下载 export interface DownloadingInfo { // HASH diff --git a/src/components/cards/MediaCard.vue b/src/components/cards/MediaCard.vue index 87b577e1..ff6cd459 100644 --- a/src/components/cards/MediaCard.vue +++ b/src/components/cards/MediaCard.vue @@ -374,7 +374,7 @@ const getImgUrl: Ref = computed(() => { return `${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}` // 如果地址中包含douban则使用中转代理 if (url.includes('doubanio.com')) - return `${import.meta.env.VITE_API_BASE_URL}douban/img?imgurl=${encodeURIComponent(url)}` + return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}` return url }) diff --git a/src/components/cards/MediaServerCard.vue b/src/components/cards/MediaServerCard.vue index 44815bdb..686e90a7 100644 --- a/src/components/cards/MediaServerCard.vue +++ b/src/components/cards/MediaServerCard.vue @@ -16,8 +16,8 @@ const props = defineProps({ // 所有媒体服务器 mediaservers: { type: Array as PropType, - required: true - } + required: true, + }, }) // 提示框 @@ -46,7 +46,12 @@ const infoItems = ref([ ]) // 同步媒体库选项 -const librariesOptions = ref<{ title: string; value: string | undefined }[]>([]) +const librariesOptions = ref<{ title: string; value: string | undefined }[]>([ + { + title: '全部', + value: 'all', + }, +]) // 媒体服务器详情弹窗 const mediaServerInfoDialog = ref(false) @@ -68,6 +73,9 @@ function openMediaServerInfoDialog() { mediaServerInfo.value = props.mediaserver mediaServerName.value = props.mediaserver.name mediaServerInfoDialog.value = true + if (!props.mediaserver.sync_libraries) { + mediaServerInfo.value.sync_libraries = ['all'] + } } // 保存详情数据 @@ -78,7 +86,7 @@ function saveMediaServerInfo() { return } // 重名判断 - if (props.mediaservers.some(item => item.name === mediaServerName.value && item!== props.mediaserver)) { + if (props.mediaservers.some(item => item.name === mediaServerName.value && item !== props.mediaserver)) { $toast.error(`【${mediaServerName.value}】已存在,请替换为其他名称`) return } @@ -148,7 +156,13 @@ async function loadLibrary(server: string) { title: item.name, value: item.id?.toString(), })) + } else { + librariesOptions.value = [] } + librariesOptions.value.unshift({ + title: '全部', + value: 'all', + }) } catch (e) { console.log(e) } @@ -174,7 +188,7 @@ onMounted(() => { - + diff --git a/src/components/cards/SiteCard.vue b/src/components/cards/SiteCard.vue index 91b6cedf..379ba3cf 100644 --- a/src/components/cards/SiteCard.vue +++ b/src/components/cards/SiteCard.vue @@ -2,30 +2,24 @@ import type { PropType } from 'vue' import { useToast } from 'vue-toast-notification' import SiteAddEditDialog from '../dialog/SiteAddEditDialog.vue' -import SiteTorrentTable from '../table/SiteTorrentTable.vue' -import { requiredValidator } from '@/@validators' +import SiteUserDataDialog from '../dialog/SiteUserDataDialog.vue' +import SiteResourceDialog from '../dialog/SiteResourceDialog.vue' +import SiteCookieUpdateDialog from '../dialog/SiteCookieUpdateDialog.vue' import api from '@/api' -import type { Site, SiteStatistic } from '@/api/types' +import type { Site, SiteStatistic, SiteUserData } from '@/api/types' import { isNullOrEmptyObject } from '@/@core/utils' -import { useDisplay } from 'vuetify' -import ProgressDialog from '../dialog/ProgressDialog.vue' - -// 显示器宽度 -const display = useDisplay() +import { VCardActions, VExpandTransition, VProgressLinear, VSpacer } from 'vuetify/lib/components/index.mjs' +import { formatFileSize } from '@/@core/utils/formatters' // 输入参数 const cardProps = defineProps({ site: Object as PropType, - width: String, - height: String, + data: Object as PropType, }) // 定义触发的自定义事件 const emit = defineEmits(['update', 'remove']) -// 密码输入 -const isPasswordVisible = ref(false) - // 图标 const siteIcon = ref('') @@ -38,9 +32,6 @@ const testButtonText = ref('测试') // 测试按钮可用性 const testButtonDisable = ref(false) -// 更新按钮可用性 -const updateButtonDisable = ref(false) - // 更新站点Cookie UA弹窗 const siteCookieDialog = ref(false) @@ -50,18 +41,11 @@ const siteEditDialog = ref(false) // 资源浏览弹窗 const resourceDialog = ref(false) -// 进度条 -const progressDialog = ref(false) +// 用户数据弹窗 +const siteUserDataDialog = ref(false) -// 进度文本 -const progressText = ref('请稍候 ...') - -// 用户名密码表单 -const userPwForm = ref({ - username: '', - password: '', - code: '', -}) +// 站点操作显示 +const siteActionShow = ref(false) // 站点使用统计 const siteStats = ref({}) @@ -113,34 +97,9 @@ async function handleResourceBrowse() { resourceDialog.value = true } -// 调用API,更新站点Cookie UA -async function updateSiteCookie() { - try { - if (!userPwForm.value.username || !userPwForm.value.password) return - - // 更新按钮状态 - siteCookieDialog.value = false - updateButtonDisable.value = true - - progressDialog.value = true - progressText.value = `正在更新 ${cardProps.site?.name} Cookie & UA ...` - - const result: { [key: string]: any } = await api.get(`site/cookie/${cardProps.site?.id}`, { - params: { - username: userPwForm.value.username, - password: userPwForm.value.password, - code: userPwForm.value.code, - }, - }) - - if (result.success) $toast.success(`${cardProps.site?.name} 更新Cookie & UA 成功!`) - else $toast.error(`${cardProps.site?.name} 更新失败:${result.message}`) - - progressDialog.value = false - updateButtonDisable.value = false - } catch (error) { - console.error(error) - } +// 打开站点用户数据弹窗 +async function handleSiteUserData() { + siteUserDataDialog.value = true } // 打开站点页面 @@ -162,9 +121,10 @@ const statColor = computed(() => { } }) -// 监听resourceDialog,如果为false则重新查询站点使用统计 -watch(resourceDialog, value => { - if (!value) getSiteStats() +// 计算上传量和下载量的百分比 +const getPercentage = computed(() => { + if (cardProps.data?.upload === 0) return 100 + return ((cardProps.data?.download ?? 0) / ((cardProps.data?.download ?? 0) + (cardProps.data?.upload ?? 0))) * 100 }) // 保存站点 @@ -173,6 +133,18 @@ function saveSite() { emit('update') } +// 更新站点Cookie UA后的回调 +function onSiteCookieUpdated() { + siteCookieDialog.value = false + getSiteStats() +} + +// 资源浏览弹窗关闭后的回调 +function onSiteResourceDone() { + resourceDialog.value = false + getSiteStats() +} + // 装载时查询站点图标 onMounted(() => { getSiteIcon() @@ -183,8 +155,6 @@ onMounted(() => { - + {{ cardProps.site?.name }} @@ -202,7 +172,7 @@ onMounted(() => { {{ cardProps.site?.url }} - + - - - - 更新 - - - - {{ testButtonText }} - - - - 浏览 - + + + ↑ {{ formatFileSize(cardProps.data?.upload || 0) }} / ↓ {{ formatFileSize(cardProps.data?.download || 0) }} + + + + +
+ + + 更新 + + + + {{ testButtonText }} + + + + 浏览 + + + + 数据 + +
+
mdi-drag +
+ +
- - - - - - - - - - - - - - - - - - - - - - - 开始更新 - - - + { @remove="emit('remove')" @close="siteEditDialog = false" /> + + - - - - - - - - - - - + :site="cardProps.site" + @close="onSiteResourceDone" + /> - - diff --git a/src/components/dialog/SiteAddEditDialog.vue b/src/components/dialog/SiteAddEditDialog.vue index 8c870d46..56f71461 100644 --- a/src/components/dialog/SiteAddEditDialog.vue +++ b/src/components/dialog/SiteAddEditDialog.vue @@ -40,6 +40,12 @@ const siteForm = ref({ // 提示框 const $toast = useToast() +// 维护类型 +const siteType = ref('cookie') + +// 是否限流 +const isLimit = ref(false) + // 状态下拉项 const statusItems = [ { title: '启用', value: true }, @@ -106,6 +112,15 @@ async function deleteSiteInfo() { async function updateSiteInfo() { startNProgress() try { + if (isLimit.value) { + siteForm.value.limit_interval = siteForm.value.limit_interval || 0 + siteForm.value.limit_count = siteForm.value.limit_count || 0 + siteForm.value.limit_seconds = siteForm.value.limit_seconds || 0 + } else { + siteForm.value.limit_interval = 0 + siteForm.value.limit_count = 0 + siteForm.value.limit_seconds = 0 + } const result: { [key: string]: any } = await api.put('site/', siteForm.value) if (result.success) { $toast.success(`${siteForm.value?.name} 更新成功!`) @@ -120,9 +135,12 @@ async function updateSiteInfo() { doneNProgress() } -onMounted(() => { +onMounted(async () => { if (props.oper !== 'add') { - fetchSiteInfo() + await fetchSiteInfo() + if (siteForm.value.limit_interval || siteForm.value.limit_count || siteForm.value.limit_seconds) + isLimit.value = true + if (siteForm.value.apikey) siteType.value = 'api' } }) @@ -179,35 +197,69 @@ onMounted(() => { - - - - - - - - - - - + + + +
+ + Cookie +
+
+ +
+ + API +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + - + { - + - + diff --git a/src/components/dialog/SiteCookieUpdateDialog.vue b/src/components/dialog/SiteCookieUpdateDialog.vue new file mode 100644 index 00000000..a3d0d366 --- /dev/null +++ b/src/components/dialog/SiteCookieUpdateDialog.vue @@ -0,0 +1,114 @@ + + diff --git a/src/components/dialog/SiteResourceDialog.vue b/src/components/dialog/SiteResourceDialog.vue new file mode 100644 index 00000000..5ef8eeb5 --- /dev/null +++ b/src/components/dialog/SiteResourceDialog.vue @@ -0,0 +1,222 @@ + + + + diff --git a/src/components/dialog/SiteUserDataDialog.vue b/src/components/dialog/SiteUserDataDialog.vue new file mode 100644 index 00000000..d4ad2e82 --- /dev/null +++ b/src/components/dialog/SiteUserDataDialog.vue @@ -0,0 +1,436 @@ + + + diff --git a/src/components/dialog/UserAddEditDialog.vue b/src/components/dialog/UserAddEditDialog.vue index 448d24df..6704bca1 100644 --- a/src/components/dialog/UserAddEditDialog.vue +++ b/src/components/dialog/UserAddEditDialog.vue @@ -270,7 +270,6 @@ onMounted(() => { - 重置 diff --git a/src/components/table/SiteTorrentTable.vue b/src/components/table/SiteTorrentTable.vue deleted file mode 100644 index 0eeedc88..00000000 --- a/src/components/table/SiteTorrentTable.vue +++ /dev/null @@ -1,202 +0,0 @@ - - - diff --git a/src/layouts/components/UserNotification.vue b/src/layouts/components/UserNotification.vue index 48abca0a..a8e9170d 100644 --- a/src/layouts/components/UserNotification.vue +++ b/src/layouts/components/UserNotification.vue @@ -16,15 +16,18 @@ const appsMenu = ref(false) // SSE持续接收消息 function startSSEMessager() { - eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/message`) - eventSource.addEventListener('message', event => { - if (event.data) { - const noti: SystemNotification = JSON.parse(event.data) - notificationList.value.unshift(noti) - hasNewMessage.value = true - // TODO 在顶部显示消息汽泡 - } - }) + // 延迟 3 秒启动 SSE,避免相关认证信息尚未写入 Cookie 导致 403 + setTimeout(() => { + eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/message`) + eventSource.addEventListener('message', event => { + if (event.data) { + const noti: SystemNotification = JSON.parse(event.data) + notificationList.value.unshift(noti) + hasNewMessage.value = true + // TODO 在顶部显示消息汽泡 + } + }) + }, 3000) } // 页面加载时,加载当前用户数据 diff --git a/src/pages/downloading.vue b/src/pages/downloading.vue index 141b3a2c..c6c20df2 100644 --- a/src/pages/downloading.vue +++ b/src/pages/downloading.vue @@ -3,6 +3,7 @@ import api from '@/api' import { DownloaderConf } from '@/api/types' import DownloadingListView from '@/views/reorganize/DownloadingListView.vue' import router from '@/router' +import NoDataFound from '@/components/NoDataFound.vue' const route = useRoute() const activeTab = ref(route.query.tab) @@ -36,7 +37,7 @@ onMounted(() => { diff --git a/src/views/discover/MediaDetailView.vue b/src/views/discover/MediaDetailView.vue index aa75aa9d..205c835b 100644 --- a/src/views/discover/MediaDetailView.vue +++ b/src/views/discover/MediaDetailView.vue @@ -334,7 +334,7 @@ const getPosterUrl: Ref = computed(() => { return `${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}` // 如果地址中包含douban则使用中转代理 if (url.includes('doubanio.com')) - return `${import.meta.env.VITE_API_BASE_URL}douban/img?imgurl=${encodeURIComponent(url)}` + return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}` return url }) diff --git a/src/views/setting/AccountSettingDirectory.vue b/src/views/setting/AccountSettingDirectory.vue index e09b22e3..0e9c06c5 100644 --- a/src/views/setting/AccountSettingDirectory.vue +++ b/src/views/setting/AccountSettingDirectory.vue @@ -63,7 +63,6 @@ async function saveStorages() { // 修改后生效 async function updatedStorage() { - await saveStorages() loadStorages() } @@ -98,9 +97,9 @@ async function saveDirectories() { // 添加媒体库目录 function addDirectory() { - let name = `目录${directories.value.length + 1}`; + let name = `目录${directories.value.length + 1}` while (directories.value.some(item => item.name === name)) { - name = `目录${parseInt(name.split('目录')[1]) + 1}`; + name = `目录${parseInt(name.split('目录')[1]) + 1}` } directories.value.push({ name: name, diff --git a/src/views/setting/AccountSettingRule.vue b/src/views/setting/AccountSettingRule.vue index dd0d3f70..c1713b66 100644 --- a/src/views/setting/AccountSettingRule.vue +++ b/src/views/setting/AccountSettingRule.vue @@ -25,9 +25,10 @@ const $toast = useToast() // 种子优先规则下拉框 const TorrentPriorityItems = [ - { title: '站点排序优先', value: 'site' }, - { title: '站点上传量优先', value: 'upload' }, - { title: '资源做种数优先', value: 'seeder' }, + { title: '资源优先级', value: 'torrent' }, + { title: '站点优先级', value: 'site' }, + { title: '站点上传量', value: 'upload' }, + { title: '资源做种数', value: 'seeder' }, ] // 调用API查询自动分类配置 @@ -52,13 +53,13 @@ async function saveCustomRules() { // 添加自定义规则 function addCustomRule() { - let id = `RULE${customRules.value.length + 1}`; + let id = `RULE${customRules.value.length + 1}` while (customRules.value.some(item => item.id === id)) { - id = `RULE${parseInt(id.split('RULE')[1]) + 1}`; + id = `RULE${parseInt(id.split('RULE')[1]) + 1}` } - let name = `规则${customRules.value.length + 1}`; + let name = `规则${customRules.value.length + 1}` while (customRules.value.some(item => item.name === name)) { - name = `规则${parseInt(name.split('规则')[1]) + 1}`; + name = `规则${parseInt(name.split('规则')[1]) + 1}` } customRules.value.push({ id: id, @@ -97,9 +98,9 @@ async function saveFilterRuleGroups() { // 添加规则组 function addFilterRuleGroup() { - let name = `规则组${filterRuleGroups.value.length + 1}`; + let name = `规则组${filterRuleGroups.value.length + 1}` while (filterRuleGroups.value.some(item => item.name === name)) { - name = `规则组${parseInt(name.split('规则组')[1]) + 1}`; + name = `规则组${parseInt(name.split('规则组')[1]) + 1}` } filterRuleGroups.value.push({ name: name, @@ -245,7 +246,7 @@ onMounted(() => { 下载规则 - 按站点或做种数量优先下载。 + 同时命中多个资源时择优下载。 @@ -254,8 +255,10 @@ onMounted(() => {
diff --git a/src/views/setting/AccountSettingSite.vue b/src/views/setting/AccountSettingSite.vue index 91f165b7..8df2f888 100644 --- a/src/views/setting/AccountSettingSite.vue +++ b/src/views/setting/AccountSettingSite.vue @@ -14,6 +14,8 @@ const resetSitesText = ref('重置站点数据') // 站点重置按钮可用状态 const resetSitesDisabled = ref(false) +const isPasswordVisible = ref(false) + // CookieCloud设置项 const siteSetting = ref({ COOKIECLOUD_HOST: '', @@ -155,7 +157,9 @@ onMounted(() => { import draggable from 'vuedraggable' import api from '@/api' -import type { Site } from '@/api/types' +import type { Site, SiteUserData } from '@/api/types' import SiteCard from '@/components/cards/SiteCard.vue' import NoDataFound from '@/components/NoDataFound.vue' import SiteAddEditDialog from '@/components/dialog/SiteAddEditDialog.vue' @@ -15,8 +15,11 @@ const appMode = computed(() => { return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value }) -// 数据列表 -const dataList = ref([]) +// 站点列表 +const siteList = ref([]) + +// 站点数据列表 +const userDataList = ref([]) // 是否刷新过 const isRefreshed = ref(false) @@ -31,7 +34,7 @@ const siteAddDialog = ref(false) async function fetchData() { try { loading.value = true - dataList.value = await api.get('site/') + siteList.value = await api.get('site/') loading.value = false isRefreshed.value = true } catch (error) { @@ -39,10 +42,19 @@ async function fetchData() { } } +// 获取站点最新数据 +async function fetchUserData() { + try { + userDataList.value = await api.get('site/userdata/latest') + } catch (error) { + console.error(error) + } +} + // 保存站点排序 async function savaSitesPriority() { // 重新排序 - const priorities = dataList.value.map((site, index) => ({ id: site.id, pri: index + 1 })) + const priorities = siteList.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) { @@ -53,12 +65,27 @@ async function savaSitesPriority() { } } +// 根据站点ID获取站点数据 +function getUserData(domain: string) { + return userDataList.value.find(userData => userData.domain === domain) +} + +// 更新站点事件时 +function onSiteSave() { + siteAddDialog.value = false + fetchData() +} + // 加载时获取数据 -onBeforeMount(fetchData) +onBeforeMount(() => { + fetchData() + fetchUserData() +}) onActivated(() => { if (!loading.value) { fetchData() + fetchUserData() } }) @@ -67,8 +94,8 @@ onActivated(() => {
{ :component-data="{ 'class': 'grid gap-3 grid-site-card' }" >
{ v-if="siteAddDialog" v-model="siteAddDialog" oper="add" - @save=" - () => { - siteAddDialog = false - fetchData() - } - " + @save="onSiteSave" @close="siteAddDialog = false" /> diff --git a/src/views/user/UserProfileView.vue b/src/views/user/UserProfileView.vue index 941a8a18..65f20bbd 100644 --- a/src/views/user/UserProfileView.vue +++ b/src/views/user/UserProfileView.vue @@ -229,9 +229,12 @@ onMounted(() => { }) // 监听 localStorage 中的用户头像变化 -watch(() => store.state.auth.avatar, () => { - currentAvatar.value = store.state.auth.avatar -}) +watch( + () => store.state.auth.avatar, + () => { + currentAvatar.value = store.state.auth.avatar + }, +)