mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 07:28:37 +08:00
refactor(api): adopt unified responses and restore media config hints
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import api, { isApiResponse } from '@/api'
|
||||
import { useAuthStore, useUserStore } from '@/stores'
|
||||
import { getCurrentLocale } from '@/plugins/i18n'
|
||||
import { AGENT_ASSISTANT_LAYER_Z_INDEX } from '@/constants/agentAssistant'
|
||||
@@ -73,6 +74,20 @@ interface AgentChoiceSelection {
|
||||
selected_description?: string
|
||||
}
|
||||
|
||||
interface AgentChoiceCallbackData {
|
||||
message?: string
|
||||
traditional?: boolean
|
||||
original_message_id?: string
|
||||
original_chat_id?: string
|
||||
choice_selection?: unknown
|
||||
feedback?: {
|
||||
selected_label?: string
|
||||
selected_value?: string
|
||||
selected_description?: string
|
||||
}
|
||||
display_message?: string
|
||||
}
|
||||
|
||||
interface AgentChatMessage {
|
||||
id: string
|
||||
role: AgentMessageRole
|
||||
@@ -712,22 +727,6 @@ function dedupeHistorySessions(sessions: AgentSessionHistoryItem[]) {
|
||||
return deduped.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
}
|
||||
|
||||
// 调用智能助手接口,并统一处理鉴权和标准响应格式。
|
||||
async function fetchAgentApi(path: string, options: RequestInit = {}) {
|
||||
const response = await fetch(resolveApiUrl(path), {
|
||||
...options,
|
||||
headers: buildAgentRequestHeaders(options.headers),
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (!response.ok) throw new Error(await resolveAgentResponseErrorMessage(response))
|
||||
|
||||
const result = await response.json()
|
||||
if (!result?.success) throw new Error(result?.message_i18n || result?.message || t('agentAssistant.error'))
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
// 从 localStorage 读取历史会话索引,读取失败时回退为空列表。
|
||||
function restoreHistorySessions() {
|
||||
try {
|
||||
@@ -743,7 +742,9 @@ async function loadServerHistorySessions() {
|
||||
historyHasMore.value = true
|
||||
historyLoading.value = true
|
||||
try {
|
||||
const data = await fetchAgentApi(`message/agent/sessions?page=1&count=${HISTORY_PAGE_SIZE}`)
|
||||
const data = await api.get<unknown>(`message/agent/sessions?page=1&count=${HISTORY_PAGE_SIZE}`, {
|
||||
feedback: 'silent',
|
||||
})
|
||||
const sessions = Array.isArray(data)
|
||||
? (data
|
||||
.map(item => normalizeServerSession(item as AgentServerSession))
|
||||
@@ -770,7 +771,9 @@ async function loadMoreServerHistorySessions(options?: { done?: (status: Infinit
|
||||
historyLoadingMore.value = true
|
||||
try {
|
||||
const nextPage = historyPage.value + 1
|
||||
const data = await fetchAgentApi(`message/agent/sessions?page=${nextPage}&count=${HISTORY_PAGE_SIZE}`)
|
||||
const data = await api.get<unknown>(`message/agent/sessions?page=${nextPage}&count=${HISTORY_PAGE_SIZE}`, {
|
||||
feedback: 'silent',
|
||||
})
|
||||
const sessions = Array.isArray(data)
|
||||
? (data
|
||||
.map(item => normalizeServerSession(item as AgentServerSession))
|
||||
@@ -798,7 +801,7 @@ async function loadSlashCommands() {
|
||||
|
||||
slashCommandsLoading.value = true
|
||||
try {
|
||||
const data = await fetchAgentApi('message/agent/commands')
|
||||
const data = await api.get<unknown>('message/agent/commands', { feedback: 'silent' })
|
||||
slashCommands.value = Array.isArray(data)
|
||||
? data
|
||||
.map(item => ({
|
||||
@@ -845,7 +848,9 @@ async function handleHistoryInfiniteLoad({
|
||||
|
||||
// 加载服务端历史会话详情,并更新本地缓存。
|
||||
async function loadServerHistorySession(targetSessionId: string) {
|
||||
const data = await fetchAgentApi(`message/agent/sessions/${encodeURIComponent(targetSessionId)}`)
|
||||
const data = await api.get<unknown>(`message/agent/sessions/${encodeURIComponent(targetSessionId)}`, {
|
||||
feedback: 'silent',
|
||||
})
|
||||
const session = normalizeServerSession(data as AgentServerSession, true)
|
||||
if (!session) throw new Error(t('agentAssistant.historyLoadFailed'))
|
||||
|
||||
@@ -1105,16 +1110,14 @@ function upsertCurrentSessionHistory() {
|
||||
async function saveCurrentSessionToServer() {
|
||||
if (!sessionId.value || messages.value.length === 0) return
|
||||
|
||||
await fetchAgentApi(`message/agent/sessions/${encodeURIComponent(sessionId.value)}/display`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
await api.put<unknown>(
|
||||
`message/agent/sessions/${encodeURIComponent(sessionId.value)}/display`,
|
||||
{
|
||||
title: buildSessionHistoryTitle(messages.value),
|
||||
messages: normalizeStoredMessages(messages.value),
|
||||
}),
|
||||
})
|
||||
},
|
||||
{ feedback: 'silent' },
|
||||
)
|
||||
}
|
||||
|
||||
// 持久化当前会话状态,并按需同步到历史会话列表。
|
||||
@@ -1158,9 +1161,12 @@ function buildAgentRequestHeaders(headers?: HeadersInit) {
|
||||
// 解析智能助手 fetch 失败响应,优先使用后端返回的本地化错误文本。
|
||||
async function resolveAgentResponseErrorMessage(response: Response) {
|
||||
try {
|
||||
const payload = await response.clone().json()
|
||||
const message = payload?.detail_i18n || payload?.message_i18n || payload?.detail || payload?.message
|
||||
if (typeof message === 'string' && message) return message
|
||||
const payload: unknown = await response.clone().json()
|
||||
if (isApiResponse(payload) && payload.message.trim()) return payload.message
|
||||
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||||
const detail = (payload as Record<string, unknown>).detail
|
||||
if (typeof detail === 'string' && detail.trim()) return detail
|
||||
}
|
||||
} catch {
|
||||
// 非 JSON 错误响应保留 HTTP 状态文本,避免吞掉原始错误。
|
||||
}
|
||||
@@ -1793,19 +1799,9 @@ async function uploadAgentAttachment(file: File) {
|
||||
formData.append('file', file)
|
||||
formData.append('session_id', sessionId.value)
|
||||
|
||||
const response = await fetch(resolveApiUrl('message/agent/upload'), {
|
||||
method: 'POST',
|
||||
headers: buildAgentRequestHeaders(),
|
||||
body: formData,
|
||||
credentials: 'include',
|
||||
return await api.post<AgentMessageAttachment & AgentOutgoingFile>('message/agent/upload', formData, {
|
||||
feedback: 'silent',
|
||||
})
|
||||
|
||||
if (!response.ok) throw new Error(await resolveAgentResponseErrorMessage(response))
|
||||
|
||||
const result = await response.json()
|
||||
if (!result?.success) throw new Error(result?.message_i18n || result?.message || t('agentAssistant.uploadFailed'))
|
||||
|
||||
return result.data as AgentMessageAttachment & AgentOutgoingFile
|
||||
}
|
||||
|
||||
// 准备本轮发送给 Agent 的图片、文件、音频和展示附件。
|
||||
@@ -2137,27 +2133,19 @@ async function handleChoiceClick(message: AgentChatMessage, choice: AgentChoiceC
|
||||
streamError.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetch(resolveApiUrl('message/agent/callback'), {
|
||||
method: 'POST',
|
||||
headers: buildAgentRequestHeaders({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
const data = await api.post<AgentChoiceCallbackData>(
|
||||
'message/agent/callback',
|
||||
{
|
||||
session_id: sessionId.value,
|
||||
callback_data: button.callback_data,
|
||||
original_message_id: message.id,
|
||||
original_chat_id: sessionId.value,
|
||||
}),
|
||||
credentials: 'include',
|
||||
})
|
||||
},
|
||||
{ feedback: 'silent' },
|
||||
)
|
||||
|
||||
if (!response.ok) throw new Error(await resolveAgentResponseErrorMessage(response))
|
||||
|
||||
const result = await response.json()
|
||||
if (!result?.success) throw new Error(result?.message_i18n || result?.message || t('agentAssistant.choiceExpired'))
|
||||
|
||||
const agentMessage = String(result.data?.message || '')
|
||||
if (result.data?.traditional) {
|
||||
const agentMessage = String(data.message || '')
|
||||
if (data.traditional) {
|
||||
const choiceSelection = buildChoiceSelection(choice, button)
|
||||
choiceSelection.selected_description = getChoiceButtonSelectionText(button)
|
||||
markChoiceSelected(choice, button, choiceSelection)
|
||||
@@ -2167,21 +2155,19 @@ async function handleChoiceClick(message: AgentChatMessage, choice: AgentChoiceC
|
||||
echoUser: false,
|
||||
displayText: choiceSelection.selected_label || choiceSelection.selected_description,
|
||||
choiceSelection,
|
||||
originalMessageId: String(result.data?.original_message_id || message.id),
|
||||
originalChatId: String(result.data?.original_chat_id || sessionId.value),
|
||||
originalMessageId: String(data.original_message_id || message.id),
|
||||
originalChatId: String(data.original_chat_id || sessionId.value),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const backendSelection = normalizeChoiceSelection(result.data?.choice_selection)
|
||||
const backendSelection = normalizeChoiceSelection(data.choice_selection)
|
||||
const choiceSelection = backendSelection || buildChoiceSelection(choice, button)
|
||||
choiceSelection.selected_label =
|
||||
result.data?.feedback?.selected_label || choiceSelection.selected_label || button.label
|
||||
choiceSelection.selected_value =
|
||||
result.data?.feedback?.selected_value || choiceSelection.selected_value || agentMessage
|
||||
choiceSelection.selected_label = data.feedback?.selected_label || choiceSelection.selected_label || button.label
|
||||
choiceSelection.selected_value = data.feedback?.selected_value || choiceSelection.selected_value || agentMessage
|
||||
choiceSelection.selected_description =
|
||||
result.data?.display_message ||
|
||||
result.data?.feedback?.selected_description ||
|
||||
data.display_message ||
|
||||
data.feedback?.selected_description ||
|
||||
choiceSelection.selected_description ||
|
||||
getChoiceButtonSelectionText(button)
|
||||
|
||||
@@ -2223,11 +2209,13 @@ function stopGeneration() {
|
||||
}
|
||||
persistState()
|
||||
if (sessionId.value) {
|
||||
fetchAgentApi(`message/agent/sessions/${encodeURIComponent(sessionId.value)}/stop`, {
|
||||
method: 'POST',
|
||||
}).catch(() => {
|
||||
// 本地中止优先,停止接口失败不阻塞用户操作。
|
||||
})
|
||||
api
|
||||
.post<unknown>(`message/agent/sessions/${encodeURIComponent(sessionId.value)}/stop`, undefined, {
|
||||
feedback: 'silent',
|
||||
})
|
||||
.catch(() => {
|
||||
// 本地中止优先,停止接口失败不阻塞用户操作。
|
||||
})
|
||||
}
|
||||
abortController?.abort()
|
||||
}
|
||||
@@ -2275,8 +2263,8 @@ async function deleteHistorySession(targetSessionId: string) {
|
||||
if (isBusy.value && targetSessionId === sessionId.value) return
|
||||
|
||||
try {
|
||||
await fetchAgentApi(`message/agent/sessions/${encodeURIComponent(targetSessionId)}`, {
|
||||
method: 'DELETE',
|
||||
await api.delete<null>(`message/agent/sessions/${encodeURIComponent(targetSessionId)}`, {
|
||||
feedback: 'silent',
|
||||
})
|
||||
} catch (error) {
|
||||
// 删除接口失败时仍允许清理本地兜底历史,避免坏记录一直挡在列表里。
|
||||
@@ -2502,10 +2490,10 @@ onScopeDispose(() => {
|
||||
class="agent-assistant-history-infinite"
|
||||
@load="handleHistoryInfiniteLoad"
|
||||
>
|
||||
<VVirtualScroll renderless :items="historySessions" :item-height="HISTORY_ITEM_HEIGHT">
|
||||
<template #default="{ item: historySession, itemRef }">
|
||||
<VVirtualScroll :renderless="true" :items="historySessions" :item-height="HISTORY_ITEM_HEIGHT">
|
||||
<template #default="{ item: historySession, ...slotProps }">
|
||||
<button
|
||||
:ref="itemRef"
|
||||
:ref="'itemRef' in slotProps ? slotProps.itemRef : undefined"
|
||||
:key="historySession.sessionId"
|
||||
class="agent-assistant-history-item"
|
||||
:class="{ 'is-active': isCurrentHistorySession(historySession.sessionId) }"
|
||||
|
||||
@@ -22,6 +22,48 @@ vi.mock('@/plugins/i18n', () => ({
|
||||
getCurrentLocale: () => 'zh-CN',
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => {
|
||||
/** 与生产客户端一致,只接受 success/message/data 三个顶层字段。 */
|
||||
function isApiResponse(payload: unknown) {
|
||||
const keys = payload && typeof payload === 'object' && !Array.isArray(payload) ? Object.keys(payload) : []
|
||||
return (
|
||||
keys.length === 3 &&
|
||||
keys.every(key => key === 'success' || key === 'message' || key === 'data') &&
|
||||
typeof (payload as { success?: unknown }).success === 'boolean' &&
|
||||
typeof (payload as { message?: unknown }).message === 'string' &&
|
||||
Object.hasOwn(payload as object, 'data')
|
||||
)
|
||||
}
|
||||
|
||||
/** 让组件单测继续通过 fetch 控制网络,同时模拟生产 DataApiClient 的严格解包语义。 */
|
||||
async function request(path: string, init: RequestInit = {}) {
|
||||
const response = await fetch(`/api/v1/${path}`, init)
|
||||
const payload = await response.json()
|
||||
|
||||
if (!isApiResponse(payload)) throw new Error('Invalid API response envelope')
|
||||
if (!response.ok || !payload.success) throw new Error(payload.message || 'API request failed')
|
||||
return payload.data
|
||||
}
|
||||
|
||||
return {
|
||||
default: {
|
||||
delete: (path: string) => request(path, { method: 'DELETE' }),
|
||||
get: (path: string) => request(path),
|
||||
post: (path: string, data?: unknown) =>
|
||||
request(path, {
|
||||
method: 'POST',
|
||||
body: data instanceof FormData ? data : data === undefined ? undefined : JSON.stringify(data),
|
||||
}),
|
||||
put: (path: string, data?: unknown) =>
|
||||
request(path, {
|
||||
method: 'PUT',
|
||||
body: data === undefined ? undefined : JSON.stringify(data),
|
||||
}),
|
||||
},
|
||||
isApiResponse,
|
||||
}
|
||||
})
|
||||
|
||||
interface MockServerSession {
|
||||
session_id: string
|
||||
client_session_id: string
|
||||
@@ -44,11 +86,19 @@ const agentMarkdownContentStub = {
|
||||
// 构造符合 Agent 标准响应包装的 fetch 返回值。
|
||||
function createAgentResponse(data: unknown) {
|
||||
return {
|
||||
json: vi.fn().mockResolvedValue({ success: true, data }),
|
||||
json: vi.fn().mockResolvedValue({ success: true, message: '', data }),
|
||||
ok: true,
|
||||
} as unknown as Response
|
||||
}
|
||||
|
||||
// 构造业务失败或协议异常响应。
|
||||
function createAgentEnvelopeResponse(payload: Record<string, unknown>, ok = true) {
|
||||
return {
|
||||
json: vi.fn().mockResolvedValue(payload),
|
||||
ok,
|
||||
} as unknown as Response
|
||||
}
|
||||
|
||||
function legacySseFrame(data: Record<string, unknown>): SyntheticSseFrame {
|
||||
return { data }
|
||||
}
|
||||
@@ -1052,6 +1102,41 @@ describe('AgentAssistantPanel stream recovery', () => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it.each([
|
||||
[
|
||||
'uses the localized backend message for a standard failure envelope',
|
||||
{ success: false, message: '附件不受支持', data: null },
|
||||
'附件不受支持',
|
||||
],
|
||||
[
|
||||
'rejects a legacy envelope with an extra localized-message field',
|
||||
{ success: false, message: '标准错误', message_i18n: '旧字段错误', data: null },
|
||||
'Invalid API response envelope',
|
||||
],
|
||||
])('%s', async (_caseName, payload, expectedMessage) => {
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
if (String(input).endsWith('/message/agent/upload') && init?.method === 'POST') {
|
||||
return createAgentEnvelopeResponse(payload)
|
||||
}
|
||||
return createAgentResponse([])
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
const wrapper = mountPanel()
|
||||
const fileInput = wrapper.get('input[type="file"]')
|
||||
Object.defineProperty(fileInput.element, 'files', {
|
||||
configurable: true,
|
||||
value: [new File(['proof'], 'proof.txt', { type: 'text/plain' })],
|
||||
})
|
||||
await fileInput.trigger('change')
|
||||
await wrapper.find('textarea').setValue('检查附件')
|
||||
await wrapper.find('textarea').trigger('keydown', { key: 'Enter' })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain(expectedMessage)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('clears protected delivery through Escape and the close button', async () => {
|
||||
const protectedMarker = 'MP-CLOSE-PROTECTED-MARKER'
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
|
||||
+7
-22
@@ -4,7 +4,6 @@ import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import api from '@/api'
|
||||
import type {
|
||||
ApiResponse,
|
||||
MusicRecognitionCacheData,
|
||||
MusicRecognitionCacheItem,
|
||||
RecognitionCacheData,
|
||||
@@ -148,26 +147,17 @@ async function loadCacheData(showSuccess = false) {
|
||||
const requestId = ++cacheLoadRequestId
|
||||
try {
|
||||
loading.value = true
|
||||
const [response, musicResponse] = (await Promise.all([
|
||||
api.get(TMDB_CACHE_ENDPOINT),
|
||||
api.get(MUSIC_CACHE_ENDPOINT),
|
||||
])) as unknown as [ApiResponse<RecognitionCacheData>, ApiResponse<MusicRecognitionCacheData>]
|
||||
const [responseData, musicData] = await Promise.all([
|
||||
api.get<RecognitionCacheData>(TMDB_CACHE_ENDPOINT),
|
||||
api.get<MusicRecognitionCacheData>(MUSIC_CACHE_ENDPOINT),
|
||||
])
|
||||
if (requestId !== cacheLoadRequestId) return
|
||||
const responseData = response.data ?? {
|
||||
count: 0,
|
||||
recognized: 0,
|
||||
unrecognized: 0,
|
||||
shared_recognized: 0,
|
||||
shared_recognize_enabled: false,
|
||||
data: [],
|
||||
}
|
||||
cacheData.value = {
|
||||
...responseData,
|
||||
shared_recognized: responseData.shared_recognized ?? 0,
|
||||
shared_recognize_enabled: responseData.shared_recognize_enabled ?? false,
|
||||
data: responseData.data.map(item => ({ ...item, recognition_id: getRecognitionId(item) })),
|
||||
}
|
||||
const musicData = musicResponse.data ?? { count: 0, recognized: 0, unrecognized: 0, data: [] }
|
||||
musicCacheData.value = {
|
||||
...musicData,
|
||||
data: (musicData.data ?? []).map(item => ({ ...item })),
|
||||
@@ -207,10 +197,7 @@ async function clearAllCache() {
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const responses = (await Promise.all(
|
||||
clearTargets.map(endpoint => api.delete(endpoint)),
|
||||
)) as unknown as ApiResponse[]
|
||||
if (responses.some(item => !item.success)) throw new Error(responses.find(item => !item.success)?.message)
|
||||
await Promise.all(clearTargets.map(endpoint => api.delete<null>(endpoint, { feedback: 'silent' })))
|
||||
$toast.success(t('setting.cache.clearSuccess'))
|
||||
await loadCacheData()
|
||||
selectedItems.value = []
|
||||
@@ -225,14 +212,12 @@ async function clearAllCache() {
|
||||
|
||||
/** 请求接口删除指定影视识别缓存。 */
|
||||
async function deleteCacheItem(key: string) {
|
||||
const response = (await api.delete(`${TMDB_CACHE_ENDPOINT}/${encodeURIComponent(key)}`)) as unknown as ApiResponse
|
||||
if (!response.success) throw new Error(response.message)
|
||||
await api.delete<null>(`${TMDB_CACHE_ENDPOINT}/${encodeURIComponent(key)}`, { feedback: 'silent' })
|
||||
}
|
||||
|
||||
/** 请求接口删除指定音乐识别缓存。 */
|
||||
async function deleteMusicCacheItem(key: string) {
|
||||
const response = (await api.delete(`${MUSIC_CACHE_ENDPOINT}/${encodeURIComponent(key)}`)) as unknown as ApiResponse
|
||||
if (!response.success) throw new Error(response.message)
|
||||
await api.delete<null>(`${MUSIC_CACHE_ENDPOINT}/${encodeURIComponent(key)}`, { feedback: 'silent' })
|
||||
}
|
||||
|
||||
/** 删除两个表格中选中的识别缓存。 */
|
||||
|
||||
@@ -10,9 +10,9 @@ const mocks = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: {
|
||||
default: createDataApiMock({
|
||||
get: mocks.apiGet,
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('vue-toastification', () => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import type { ApiResponse, DownloadingInfo } from '@/api/types'
|
||||
import type { DownloadingInfo } from '@/api/types'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import { useGlobalSettingsStore } from '@/stores'
|
||||
import { getDisplayImageUrl } from '@/utils/imageUtils'
|
||||
@@ -122,13 +122,12 @@ async function toggleDownload() {
|
||||
const operation = isDownloading.value ? 'stop' : 'start'
|
||||
pendingAction.value = 'toggle'
|
||||
try {
|
||||
const result: ApiResponse<unknown> = await api.get(`download/${operation}/${props.info?.hash}`, {
|
||||
await api.get(`download/${operation}/${props.info?.hash}`, {
|
||||
params: {
|
||||
name: props.downloaderName,
|
||||
},
|
||||
})
|
||||
|
||||
if (result.success) isDownloading.value = !isDownloading.value
|
||||
isDownloading.value = !isDownloading.value
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
@@ -142,10 +141,10 @@ async function deleteDownload() {
|
||||
|
||||
pendingAction.value = 'delete'
|
||||
try {
|
||||
const result: ApiResponse<unknown> = await api.delete(`download/${props.info?.hash}`, {
|
||||
await api.delete(`download/${props.info?.hash}`, {
|
||||
params: { name: props.downloaderName },
|
||||
})
|
||||
if (result.success) cardState.value = false
|
||||
cardState.value = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
|
||||
@@ -176,8 +176,8 @@ async function querySites() {
|
||||
// 查询用户选中的站点
|
||||
async function querySelectedSites() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/public/IndexerSites')
|
||||
selectedSites.value = result.data?.value ?? []
|
||||
const result = await api.get<{ value?: number[] }>('system/setting/public/IndexerSites')
|
||||
selectedSites.value = result.value ?? []
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@@ -273,7 +273,8 @@ async function handleCheckExists() {
|
||||
try {
|
||||
const exists = await getCachedMediaExistsStatus(getExistsStatusKey(), async () => {
|
||||
const identity = getMediaSubscribeIdentity(props.media)
|
||||
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
||||
const result = await api.get<{ item?: { id?: string } }>('mediaserver/exists', {
|
||||
feedback: 'silent',
|
||||
params: {
|
||||
...(identity ? { media_source: identity.source, media_id: identity.mediaId } : {}),
|
||||
title: props.media?.title,
|
||||
@@ -283,7 +284,7 @@ async function handleCheckExists() {
|
||||
},
|
||||
})
|
||||
|
||||
return Boolean(result.success)
|
||||
return Boolean(result.item?.id)
|
||||
})
|
||||
|
||||
isExists.value = exists
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import type { ApiResponse, Plugin } from '@/api/types'
|
||||
import { getApiBusinessErrorMessage } from '@/api/client'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import { getLogoUrl, getProxyImageUrl } from '@/utils/imageUtils'
|
||||
import { useGlobalSettingsStore } from '@/stores'
|
||||
import { usePluginCardAccent } from '@/composables/usePluginCardAccent'
|
||||
@@ -153,7 +154,8 @@ async function installPlugin(releaseVersion?: string, repoUrl?: string) {
|
||||
}),
|
||||
)
|
||||
|
||||
const result: ApiResponse<unknown> = await api.get(`plugin/install/${props.plugin?.id}`, {
|
||||
await api.get(`plugin/install/${props.plugin?.id}`, {
|
||||
feedback: 'silent',
|
||||
params: {
|
||||
repo_url: repoUrl || props.plugin?.repo_url,
|
||||
release_version: releaseVersion,
|
||||
@@ -161,19 +163,15 @@ async function installPlugin(releaseVersion?: string, repoUrl?: string) {
|
||||
},
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(t('plugin.installSuccess', { name: props.plugin?.plugin_name }))
|
||||
versionHistoryDialogController?.close()
|
||||
versionHistoryDialogController = null
|
||||
emit('install')
|
||||
} else {
|
||||
$toast.error(t('plugin.installFailed', { name: props.plugin?.plugin_name, message: result.message }))
|
||||
}
|
||||
$toast.success(t('plugin.installSuccess', { name: props.plugin?.plugin_name }))
|
||||
versionHistoryDialogController?.close()
|
||||
versionHistoryDialogController = null
|
||||
emit('install')
|
||||
} catch (error) {
|
||||
$toast.error(
|
||||
t('plugin.installFailed', {
|
||||
name: props.plugin?.plugin_name,
|
||||
message: t('common.serverConnectionFailed'),
|
||||
message: getApiBusinessErrorMessage(error) || t('common.serverConnectionFailed'),
|
||||
}),
|
||||
)
|
||||
console.error(error)
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import api from '@/api'
|
||||
import type { ApiResponse, Plugin, PluginRating } from '@/api/types'
|
||||
import { getApiBusinessErrorMessage } from '@/api/client'
|
||||
import type { Plugin, PluginRating } from '@/api/types'
|
||||
import { getLogoUrl, getProxyImageUrl } from '@/utils/imageUtils'
|
||||
import { usePluginCardAccent } from '@/composables/usePluginCardAccent'
|
||||
import { formatDownloadCount } from '@/@core/utils/formatters'
|
||||
@@ -136,26 +137,17 @@ async function uninstallPlugin() {
|
||||
|
||||
showPluginProgress(t('plugin.uninstalling', { name: props.plugin?.plugin_name }))
|
||||
try {
|
||||
const result: ApiResponse<unknown> = await api.delete(`plugin/${props.plugin?.id}`)
|
||||
if (result.success) {
|
||||
$toast.success(t('plugin.uninstallSuccess', { name: props.plugin?.plugin_name }))
|
||||
await api.delete(`plugin/${props.plugin?.id}`, { feedback: 'silent' })
|
||||
$toast.success(t('plugin.uninstallSuccess', { name: props.plugin?.plugin_name }))
|
||||
|
||||
emit('remove')
|
||||
// 生命周期成功后刷新动态导航。
|
||||
void pluginSidebarNavStore.ensureSidebarNav(true)
|
||||
} else {
|
||||
$toast.error(
|
||||
t('plugin.uninstallFailed', {
|
||||
name: props.plugin?.plugin_name,
|
||||
message: result.message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
emit('remove')
|
||||
// 生命周期成功后刷新动态导航。
|
||||
void pluginSidebarNavStore.ensureSidebarNav(true)
|
||||
} catch (error) {
|
||||
$toast.error(
|
||||
t('plugin.uninstallFailed', {
|
||||
name: props.plugin?.plugin_name,
|
||||
message: t('common.serverConnectionFailed'),
|
||||
message: getApiBusinessErrorMessage(error) || t('common.serverConnectionFailed'),
|
||||
}),
|
||||
)
|
||||
console.error(error)
|
||||
@@ -222,25 +214,16 @@ async function resetPlugin() {
|
||||
if (!isConfirmed) return
|
||||
|
||||
try {
|
||||
const result: ApiResponse<unknown> = await api.get(`plugin/reset/${props.plugin?.id}`)
|
||||
if (result.success) {
|
||||
$toast.success(t('plugin.resetSuccess', { name: props.plugin?.plugin_name }))
|
||||
emit('save')
|
||||
// 生命周期成功后刷新动态导航。
|
||||
void pluginSidebarNavStore.ensureSidebarNav(true)
|
||||
} else {
|
||||
$toast.error(
|
||||
t('plugin.resetFailed', {
|
||||
name: props.plugin?.plugin_name,
|
||||
message: result.message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
await api.get(`plugin/reset/${props.plugin?.id}`, { feedback: 'silent' })
|
||||
$toast.success(t('plugin.resetSuccess', { name: props.plugin?.plugin_name }))
|
||||
emit('save')
|
||||
// 生命周期成功后刷新动态导航。
|
||||
void pluginSidebarNavStore.ensureSidebarNav(true)
|
||||
} catch (error) {
|
||||
$toast.error(
|
||||
t('plugin.resetFailed', {
|
||||
name: props.plugin?.plugin_name,
|
||||
message: t('common.serverConnectionFailed'),
|
||||
message: getApiBusinessErrorMessage(error) || t('common.serverConnectionFailed'),
|
||||
}),
|
||||
)
|
||||
console.error(error)
|
||||
@@ -274,7 +257,8 @@ async function updatePlugin(releaseVersion?: string, repoUrl?: string) {
|
||||
: t('plugin.updating', { name: props.plugin?.plugin_name }),
|
||||
)
|
||||
|
||||
const result: ApiResponse<unknown> = await api.get(`plugin/install/${props.plugin?.id}`, {
|
||||
await api.get(`plugin/install/${props.plugin?.id}`, {
|
||||
feedback: 'silent',
|
||||
params: {
|
||||
repo_url: repoUrl || props.plugin?.repo_url,
|
||||
release_version: releaseVersion,
|
||||
@@ -282,27 +266,18 @@ async function updatePlugin(releaseVersion?: string, repoUrl?: string) {
|
||||
},
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(t('plugin.updateSuccess', { name: props.plugin?.plugin_name }))
|
||||
versionHistoryDialogController?.close()
|
||||
versionHistoryDialogController = null
|
||||
$toast.success(t('plugin.updateSuccess', { name: props.plugin?.plugin_name }))
|
||||
versionHistoryDialogController?.close()
|
||||
versionHistoryDialogController = null
|
||||
|
||||
emit('save')
|
||||
// 生命周期成功后刷新动态导航。
|
||||
void pluginSidebarNavStore.ensureSidebarNav(true)
|
||||
} else {
|
||||
$toast.error(
|
||||
t('plugin.updateFailed', {
|
||||
name: props.plugin?.plugin_name,
|
||||
message: result.message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
emit('save')
|
||||
// 生命周期成功后刷新动态导航。
|
||||
void pluginSidebarNavStore.ensureSidebarNav(true)
|
||||
} catch (error) {
|
||||
$toast.error(
|
||||
t('plugin.updateFailed', {
|
||||
name: props.plugin?.plugin_name,
|
||||
message: t('common.serverConnectionFailed'),
|
||||
message: getApiBusinessErrorMessage(error) || t('common.serverConnectionFailed'),
|
||||
}),
|
||||
)
|
||||
console.error(error)
|
||||
@@ -479,26 +454,27 @@ async function executePluginClone(cloneForm: {
|
||||
try {
|
||||
showPluginProgress(t('plugin.cloning', { name: props.plugin?.plugin_name }))
|
||||
|
||||
const result: ApiResponse<unknown> = await api.post(`plugin/clone/${props.plugin?.id}`, {
|
||||
suffix: cloneForm.suffix.trim(),
|
||||
name: cloneForm.name.trim(),
|
||||
description: cloneForm.description.trim(),
|
||||
version: cloneForm.version.trim(),
|
||||
icon: cloneForm.icon.trim(),
|
||||
})
|
||||
await api.post(
|
||||
`plugin/clone/${props.plugin?.id}`,
|
||||
{
|
||||
suffix: cloneForm.suffix.trim(),
|
||||
name: cloneForm.name.trim(),
|
||||
description: cloneForm.description.trim(),
|
||||
version: cloneForm.version.trim(),
|
||||
icon: cloneForm.icon.trim(),
|
||||
},
|
||||
{ feedback: 'silent' },
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(t('plugin.cloneSuccess', { name: cloneForm.name }))
|
||||
cloneDialogController?.close()
|
||||
cloneDialogController = null
|
||||
emit('remove')
|
||||
// 生命周期成功后刷新动态导航。
|
||||
void pluginSidebarNavStore.ensureSidebarNav(true)
|
||||
} else {
|
||||
$toast.error(t('plugin.cloneFailed', { message: result.message }))
|
||||
}
|
||||
$toast.success(t('plugin.cloneSuccess', { name: cloneForm.name }))
|
||||
cloneDialogController?.close()
|
||||
cloneDialogController = null
|
||||
emit('remove')
|
||||
// 生命周期成功后刷新动态导航。
|
||||
void pluginSidebarNavStore.ensureSidebarNav(true)
|
||||
} catch (error) {
|
||||
$toast.error(t('plugin.cloneFailedGeneral'))
|
||||
const message = getApiBusinessErrorMessage(error)
|
||||
$toast.error(message ? t('plugin.cloneFailed', { message }) : t('plugin.cloneFailedGeneral'))
|
||||
console.error(error)
|
||||
} finally {
|
||||
closePluginProgress()
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useGlobalSettingsStore } from '@/stores'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import api from '@/api'
|
||||
import type { ApiResponse, Site, SiteStatistic, SiteUserData } from '@/api/types'
|
||||
import { isApiBusinessFailure } from '@/api/client'
|
||||
import type { Site, SiteStatistic, SiteUserData } from '@/api/types'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
@@ -65,9 +66,8 @@ async function getSiteIcon() {
|
||||
|
||||
try {
|
||||
const icon = await getCachedSiteIcon(siteId, async () => {
|
||||
const response = await api.get(`site/icon/${siteId}`)
|
||||
|
||||
return response?.data?.icon || defaultSiteIcon
|
||||
const response = await api.get<{ icon?: string }>(`site/icon/${siteId}`)
|
||||
return response?.icon || defaultSiteIcon
|
||||
})
|
||||
siteIcon.value = getDisplayImageUrl(icon, globalSettingsStore.globalSettings.GLOBAL_IMAGE_CACHE)
|
||||
} catch (error) {
|
||||
@@ -82,13 +82,13 @@ async function testSite() {
|
||||
testButtonText.value = t('site.testing')
|
||||
testButtonDisable.value = true
|
||||
|
||||
const result = (await api.get(`site/test/${cardProps.site?.id}`)) as ApiResponse<unknown>
|
||||
if (result.success) $toast.success(t('site.testSuccess', { name: cardProps.site?.name }))
|
||||
else $toast.error(t('site.testFailed', { name: cardProps.site?.name, message: result.message }))
|
||||
await api.get(`site/test/${cardProps.site?.id}`)
|
||||
$toast.success(t('site.testSuccess', { name: cardProps.site?.name }))
|
||||
|
||||
// 测试完成后刷新统计数据
|
||||
emit('refresh-stats', cardProps.site?.domain)
|
||||
} catch (error) {
|
||||
if (isApiBusinessFailure(error)) emit('refresh-stats', cardProps.site?.domain)
|
||||
console.error(error)
|
||||
} finally {
|
||||
testButtonText.value = t('site.testConnectivity')
|
||||
@@ -169,9 +169,8 @@ async function deleteSiteInfo() {
|
||||
if (!isConfirmed) return
|
||||
|
||||
try {
|
||||
const result = (await api.delete(`site/${cardProps.site?.id}`)) as ApiResponse<unknown>
|
||||
if (result.success) emit('remove')
|
||||
else $toast.error(t('site.deleteFailed', { name: cardProps.site?.name, message: result.message }))
|
||||
await api.delete(`site/${cardProps.site?.id}`, { feedback: 'silent' })
|
||||
emit('remove')
|
||||
} catch (error) {
|
||||
$toast.error(t('site.deleteFailed', { name: cardProps.site?.name, message: error }))
|
||||
console.error(error)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useConfirm } from '@/composables/useConfirm'
|
||||
import { formatDateDifference } from '@/@core/utils/formatters'
|
||||
import { formatSeasonLabel } from '@/@core/utils/season'
|
||||
import api from '@/api'
|
||||
import { getApiBusinessErrorMessage } from '@/api/client'
|
||||
import type { Subscribe } from '@/api/types'
|
||||
import router from '@/router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -226,12 +227,9 @@ function getBufferPercentage() {
|
||||
// 删除订阅
|
||||
async function removeSubscribe() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(`subscribe/${props.media?.id}`)
|
||||
|
||||
if (result.success) {
|
||||
// 通知父组件刷新
|
||||
emit('remove')
|
||||
}
|
||||
await api.delete(`subscribe/${props.media?.id}`, { feedback: 'silent' })
|
||||
// 通知父组件刷新
|
||||
emit('remove')
|
||||
} catch (e) {
|
||||
$toast.error(t('subscribe.requestFailed'))
|
||||
console.log(e)
|
||||
@@ -241,11 +239,8 @@ async function removeSubscribe() {
|
||||
// 搜索订阅
|
||||
async function searchSubscribe() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(`subscribe/search/${props.media?.id}`)
|
||||
|
||||
// 提示
|
||||
if (result.success) $toast.success(`${props.media?.name} 提交搜索请求成功!`)
|
||||
else $toast.error(t('subscribe.requestFailed'))
|
||||
await api.get(`subscribe/search/${props.media?.id}`, { feedback: 'silent' })
|
||||
$toast.success(`${props.media?.name} 提交搜索请求成功!`)
|
||||
} catch (e) {
|
||||
$toast.error(t('subscribe.requestFailed'))
|
||||
console.log(e)
|
||||
@@ -254,9 +249,9 @@ async function searchSubscribe() {
|
||||
|
||||
// 切换订阅状态
|
||||
async function toggleSubscribeStatus(state: 'R' | 'S') {
|
||||
const action = state === 'S' ? t('common.pause') : t('common.enable')
|
||||
try {
|
||||
// 根据传入的 state 判断对应的操作文字
|
||||
const action = state === 'S' ? t('common.pause') : t('common.enable')
|
||||
// 弹出确认框
|
||||
const isConfirmed = await createConfirm({
|
||||
title: t('common.confirmAction', { action }),
|
||||
@@ -264,17 +259,13 @@ async function toggleSubscribeStatus(state: 'R' | 'S') {
|
||||
})
|
||||
if (!isConfirmed) return
|
||||
// 调用 API 更新订阅状态
|
||||
const result: { [key: string]: any } = await api.put(`subscribe/status/${props.media?.id}?state=${state}`)
|
||||
// 提示
|
||||
if (result.success) {
|
||||
$toast.success(t('subscribe.toggleSuccess', { name: props.media?.name, action }))
|
||||
subscribeState.value = state
|
||||
emit('save')
|
||||
} else {
|
||||
$toast.error(t('subscribe.toggleFailed', { action, message: result.message }))
|
||||
}
|
||||
await api.put(`subscribe/status/${props.media?.id}?state=${state}`, undefined, { feedback: 'silent' })
|
||||
$toast.success(t('subscribe.toggleSuccess', { name: props.media?.name, action }))
|
||||
subscribeState.value = state
|
||||
emit('save')
|
||||
} catch (e) {
|
||||
$toast.error(t('subscribe.requestFailed'))
|
||||
const message = getApiBusinessErrorMessage(e)
|
||||
$toast.error(message ? t('subscribe.toggleFailed', { action, message }) : t('subscribe.requestFailed'))
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
@@ -289,15 +280,15 @@ async function resetSubscribe() {
|
||||
})
|
||||
if (!isConfirmed) return
|
||||
// 重置
|
||||
const result: { [key: string]: any } = await api.get(`subscribe/reset/${props.media?.id}`)
|
||||
// 提示
|
||||
if (result.success) {
|
||||
$toast.success(t('subscribe.resetSuccess', { name: props.media?.name }))
|
||||
subscribeState.value = 'R'
|
||||
emit('save')
|
||||
} else $toast.error(t('subscribe.resetFailed', { name: props.media?.name, message: result.message }))
|
||||
await api.get(`subscribe/reset/${props.media?.id}`, { feedback: 'silent' })
|
||||
$toast.success(t('subscribe.resetSuccess', { name: props.media?.name }))
|
||||
subscribeState.value = 'R'
|
||||
emit('save')
|
||||
} catch (e) {
|
||||
$toast.error(t('subscribe.requestFailed'))
|
||||
const message = getApiBusinessErrorMessage(e)
|
||||
$toast.error(
|
||||
message ? t('subscribe.resetFailed', { name: props.media?.name, message }) : t('subscribe.requestFailed'),
|
||||
)
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +44,8 @@ async function getSiteIcon() {
|
||||
try {
|
||||
const icon = await getCachedSiteIcon(subtitle.value.site, async () => {
|
||||
try {
|
||||
const response = await api.get(`site/icon/${subtitle.value?.site}`)
|
||||
|
||||
return response?.data?.icon || ''
|
||||
const response = await api.get<{ icon?: string }>(`site/icon/${subtitle.value?.site}`)
|
||||
return response?.icon || ''
|
||||
} catch (error) {
|
||||
console.error('Failed to load site icon:', error)
|
||||
return ''
|
||||
|
||||
@@ -43,9 +43,8 @@ async function getSiteIcon() {
|
||||
try {
|
||||
const icon = await getCachedSiteIcon(subtitle.value.site, async () => {
|
||||
try {
|
||||
const response = await api.get(`site/icon/${subtitle.value?.site}`)
|
||||
|
||||
return response?.data?.icon || ''
|
||||
const response = await api.get<{ icon?: string }>(`site/icon/${subtitle.value?.site}`)
|
||||
return response?.icon || ''
|
||||
} catch (error) {
|
||||
console.error('Failed to load site icon:', error)
|
||||
return ''
|
||||
|
||||
@@ -56,9 +56,8 @@ async function getSiteIcon(site: number | undefined) {
|
||||
try {
|
||||
const icon = await getCachedSiteIcon(site, async () => {
|
||||
try {
|
||||
const response = await api.get(`site/icon/${site}`)
|
||||
|
||||
return response?.data?.icon || ''
|
||||
const response = await api.get<{ icon?: string }>(`site/icon/${site}`)
|
||||
return response?.icon || ''
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return ''
|
||||
|
||||
@@ -41,9 +41,8 @@ async function getSiteIcon(site: number | undefined) {
|
||||
try {
|
||||
const icon = await getCachedSiteIcon(site, async () => {
|
||||
try {
|
||||
const response = await api.get(`site/icon/${site}`)
|
||||
|
||||
return response?.data?.icon || ''
|
||||
const response = await api.get<{ icon?: string }>(`site/icon/${site}`)
|
||||
return response?.icon || ''
|
||||
} catch (error) {
|
||||
console.error('Failed to load site icon:', error)
|
||||
return ''
|
||||
|
||||
@@ -96,13 +96,9 @@ async function removeUser() {
|
||||
content: t('user.confirmDeleteUser', { username: props.user?.name }),
|
||||
})
|
||||
if (!isConfirmed) return
|
||||
const result: Record<string, unknown> = await api.delete(`user/id/${props.user.id}`)
|
||||
if (result.success) {
|
||||
$toast.success(t('user.deleteSuccess'))
|
||||
emit('remove')
|
||||
} else {
|
||||
$toast.error(t('user.deleteFailed'))
|
||||
}
|
||||
await api.delete(`user/id/${props.user.id}`, { feedback: 'silent' })
|
||||
$toast.success(t('user.deleteSuccess'))
|
||||
emit('remove')
|
||||
} catch (error) {
|
||||
$toast.error(t('user.deleteFailed'))
|
||||
console.log(error)
|
||||
|
||||
@@ -87,13 +87,9 @@ async function handleDelete(item: Workflow) {
|
||||
if (!isConfirmed) return
|
||||
|
||||
try {
|
||||
const result: { [key: string]: string } = await api.delete(`workflow/${item.id}`)
|
||||
if (result.success) {
|
||||
$toast.success(t('workflow.task.deleteSuccess'))
|
||||
emit('refresh')
|
||||
} else {
|
||||
$toast.error(t('workflow.task.deleteFailed', { message: result.message }))
|
||||
}
|
||||
await api.delete(`workflow/${item.id}`)
|
||||
$toast.success(t('workflow.task.deleteSuccess'))
|
||||
emit('refresh')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
@@ -103,13 +99,9 @@ async function handleDelete(item: Workflow) {
|
||||
async function handleEnable(item: Workflow) {
|
||||
loading.value = true
|
||||
try {
|
||||
const result: { [key: string]: string } = await api.post(`workflow/${item.id}/start`)
|
||||
if (result.success) {
|
||||
$toast.success(t('workflow.task.enableSuccess'))
|
||||
emit('refresh')
|
||||
} else {
|
||||
$toast.error(t('workflow.task.enableFailed', { message: result.message }))
|
||||
}
|
||||
await api.post(`workflow/${item.id}/start`)
|
||||
$toast.success(t('workflow.task.enableSuccess'))
|
||||
emit('refresh')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
@@ -120,13 +112,9 @@ async function handleEnable(item: Workflow) {
|
||||
async function handlePause(item: Workflow) {
|
||||
loading.value = true
|
||||
try {
|
||||
const result: { [key: string]: string } = await api.post(`workflow/${item.id}/pause`)
|
||||
if (result.success) {
|
||||
$toast.success(t('workflow.task.pauseSuccess'))
|
||||
emit('refresh')
|
||||
} else {
|
||||
$toast.error(t('workflow.task.pauseFailed', { message: result.message }))
|
||||
}
|
||||
await api.post(`workflow/${item.id}/pause`)
|
||||
$toast.success(t('workflow.task.pauseSuccess'))
|
||||
emit('refresh')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
@@ -140,16 +128,11 @@ async function handleRun(item: Workflow, from_begin: boolean) {
|
||||
setTimeout(() => {
|
||||
emit('refresh')
|
||||
}, 500)
|
||||
const result: { [key: string]: string } = await api.post(`workflow/${item.id}/run?from_begin=${from_begin}`, {
|
||||
await api.post(`workflow/${item.id}/run?from_begin=${from_begin}`, {
|
||||
from_begin,
|
||||
})
|
||||
if (result.success) {
|
||||
$toast.success(t('workflow.task.runSuccess'))
|
||||
emit('refresh')
|
||||
} else {
|
||||
$toast.error(t('workflow.task.runFailed', { message: result.message }))
|
||||
emit('refresh')
|
||||
}
|
||||
$toast.success(t('workflow.task.runSuccess'))
|
||||
emit('refresh')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
@@ -166,13 +149,9 @@ async function handleReset(item: Workflow) {
|
||||
if (!isConfirmed) return
|
||||
|
||||
try {
|
||||
const result: { [key: string]: string } = await api.post(`workflow/${item.id}/reset`)
|
||||
if (result.success) {
|
||||
$toast.success(t('workflow.task.resetSuccess'))
|
||||
emit('refresh')
|
||||
} else {
|
||||
$toast.error(t('workflow.task.resetFailed', { message: result.message }))
|
||||
}
|
||||
await api.post(`workflow/${item.id}/reset`)
|
||||
$toast.success(t('workflow.task.resetSuccess'))
|
||||
emit('refresh')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const mocks = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: { get: mocks.apiGet },
|
||||
default: createDataApiMock({ get: mocks.apiGet }),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useConfirm', () => ({
|
||||
|
||||
@@ -20,11 +20,11 @@ const mocks = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: {
|
||||
default: createDataApiMock({
|
||||
delete: mocks.apiDelete,
|
||||
get: mocks.apiGet,
|
||||
post: mocks.apiPost,
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useConfirm', () => ({
|
||||
|
||||
@@ -15,10 +15,10 @@ const mocks = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: {
|
||||
default: createDataApiMock({
|
||||
delete: (...args: unknown[]) => mocks.apiDelete(...args),
|
||||
get: (...args: unknown[]) => mocks.apiGet(...args),
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useConfirm', () => ({ useConfirm: () => mocks.confirm }))
|
||||
|
||||
@@ -234,9 +234,8 @@ async function queryVersionStatistic() {
|
||||
if (!systemEnv.value.USAGE_STATISTIC_SHARE) return
|
||||
versionStatisticLoading.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/usage/statistic')
|
||||
|
||||
versionStatistic.value = result.data ?? {}
|
||||
const statistic = await api.get<{ [key: string]: any } | null>('system/usage/statistic')
|
||||
versionStatistic.value = statistic ?? {}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
versionStatistic.value = {}
|
||||
@@ -254,9 +253,7 @@ async function showVersionStatisticDialog() {
|
||||
// 查询系统环境变量
|
||||
async function querySystemEnv() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env')
|
||||
|
||||
systemEnv.value = result.data
|
||||
systemEnv.value = (await api.get<{ [key: string]: any } | null>('system/env')) ?? {}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@@ -276,9 +273,7 @@ async function querySystemUptime() {
|
||||
// 查询所有Release
|
||||
async function queryAllRelease() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/versions')
|
||||
|
||||
allRelease.value = result.data ?? []
|
||||
allRelease.value = (await api.get<any[] | null>('system/versions')) ?? []
|
||||
|
||||
// 最新版本
|
||||
if (allRelease.value.length > 0) latestRelease.value = allRelease.value[0].tag_name
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useToast } from 'vue-toastification'
|
||||
import api from '@/api'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import type {
|
||||
ApiResponse,
|
||||
DownloaderConf,
|
||||
MediaDataSource,
|
||||
MediaInfo,
|
||||
@@ -144,11 +143,8 @@ const dialogSubtitle = computed(() => {
|
||||
// 加载目录设置
|
||||
async function loadDirectories() {
|
||||
try {
|
||||
const result = await api.get<
|
||||
ApiResponse<{ value?: TransferDirectoryConf[] }>,
|
||||
ApiResponse<{ value?: TransferDirectoryConf[] }>
|
||||
>('system/setting/public/Directories')
|
||||
directories.value = result.data?.value ?? []
|
||||
const result = await api.get<{ value?: TransferDirectoryConf[] }>('system/setting/public/Directories')
|
||||
directories.value = result.value ?? []
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@@ -226,29 +222,25 @@ async function addDownload() {
|
||||
|
||||
const endpoint = props.media ? 'download/' : 'download/add'
|
||||
|
||||
const result = await api.post<ApiResponse<unknown>, ApiResponse<unknown>>(endpoint, payload)
|
||||
await api.post<null>(endpoint, payload, { feedback: 'silent' })
|
||||
|
||||
if (result && result.success) {
|
||||
// 添加下载成功
|
||||
$toast.success(
|
||||
t('dialog.addDownload.downloadSuccess', { site: props.torrent?.site_name, title: props.torrent?.title }),
|
||||
)
|
||||
// 下载成功,返回链接
|
||||
emit('done', props.torrent?.enclosure)
|
||||
} else {
|
||||
// 添加下载失败
|
||||
$toast.error(
|
||||
t('dialog.addDownload.downloadFailed', {
|
||||
site: props.torrent?.site_name,
|
||||
title: props.torrent?.title,
|
||||
message: result?.message,
|
||||
}),
|
||||
)
|
||||
// 下载失败,返回错误原因
|
||||
emit('error', result?.message)
|
||||
}
|
||||
// 添加下载成功
|
||||
$toast.success(
|
||||
t('dialog.addDownload.downloadSuccess', { site: props.torrent?.site_name, title: props.torrent?.title }),
|
||||
)
|
||||
// 下载成功,返回链接
|
||||
emit('done', props.torrent?.enclosure)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
$toast.error(
|
||||
t('dialog.addDownload.downloadFailed', {
|
||||
site: props.torrent?.site_name,
|
||||
title: props.torrent?.title,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
emit('error', message)
|
||||
}
|
||||
loading.value = false
|
||||
doneNProgress()
|
||||
|
||||
@@ -2,13 +2,7 @@
|
||||
import { useToast } from 'vue-toastification'
|
||||
import api from '@/api'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import {
|
||||
MediaSource,
|
||||
type ApiResponse,
|
||||
type MediaDataSource,
|
||||
type SubtitleInfo,
|
||||
type TransferDirectoryConf,
|
||||
} from '@/api/types'
|
||||
import { MediaSource, type MediaDataSource, type SubtitleInfo, type TransferDirectoryConf } from '@/api/types'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import MediaIdSelector from '../misc/MediaIdSelector.vue'
|
||||
@@ -114,11 +108,8 @@ const buttonText = computed(() =>
|
||||
// 加载目录设置
|
||||
async function loadDirectories() {
|
||||
try {
|
||||
const result = await api.get<
|
||||
ApiResponse<{ value?: TransferDirectoryConf[] }>,
|
||||
ApiResponse<{ value?: TransferDirectoryConf[] }>
|
||||
>('system/setting/public/Directories')
|
||||
directories.value = result.data?.value ?? []
|
||||
const result = await api.get<{ value?: TransferDirectoryConf[] }>('system/setting/public/Directories')
|
||||
directories.value = result.value ?? []
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@@ -162,29 +153,26 @@ async function addSubtitleDownload() {
|
||||
media_id: normalizedMediaId.value,
|
||||
}
|
||||
|
||||
const result = await api.post<ApiResponse<unknown>, ApiResponse<unknown>>('download/subtitle', payload)
|
||||
await api.post<null>('download/subtitle', payload, { feedback: 'silent' })
|
||||
|
||||
if (result && result.success) {
|
||||
$toast.success(
|
||||
t('dialog.addSubtitleDownload.downloadSuccess', {
|
||||
site: props.subtitle?.site_name,
|
||||
title: props.subtitle?.title,
|
||||
}),
|
||||
)
|
||||
emit('done', props.subtitle?.enclosure)
|
||||
} else {
|
||||
$toast.error(
|
||||
t('dialog.addSubtitleDownload.downloadFailed', {
|
||||
site: props.subtitle?.site_name,
|
||||
title: props.subtitle?.title,
|
||||
message: result?.message,
|
||||
}),
|
||||
)
|
||||
emit('error', result?.message)
|
||||
}
|
||||
$toast.success(
|
||||
t('dialog.addSubtitleDownload.downloadSuccess', {
|
||||
site: props.subtitle?.site_name,
|
||||
title: props.subtitle?.title,
|
||||
}),
|
||||
)
|
||||
emit('done', props.subtitle?.enclosure)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
emit('error', String(error))
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
$toast.error(
|
||||
t('dialog.addSubtitleDownload.downloadFailed', {
|
||||
site: props.subtitle?.site_name,
|
||||
title: props.subtitle?.title,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
emit('error', message)
|
||||
}
|
||||
loading.value = false
|
||||
doneNProgress()
|
||||
|
||||
@@ -41,6 +41,11 @@ interface AgentMcpTestState {
|
||||
tools?: AgentMcpToolInfo[]
|
||||
}
|
||||
|
||||
interface AgentMcpTestResult {
|
||||
message?: string
|
||||
tools?: AgentMcpToolInfo[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
servers: AgentMcpServer[]
|
||||
@@ -169,12 +174,16 @@ async function testServer(server: EditableAgentMcpServer) {
|
||||
const payload = toServerPayload(server)
|
||||
testStates.value[payload.id] = { loading: true }
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('message/agent/mcp/servers/test', { server: payload })
|
||||
const result = await api.post<AgentMcpTestResult>(
|
||||
'message/agent/mcp/servers/test',
|
||||
{ server: payload },
|
||||
{ feedback: 'silent' },
|
||||
)
|
||||
testStates.value[payload.id] = {
|
||||
loading: false,
|
||||
success: Boolean(result.success),
|
||||
message: result.message || result.data?.message || '',
|
||||
tools: result.data?.tools || [],
|
||||
success: true,
|
||||
message: result.message || '',
|
||||
tools: result.tools || [],
|
||||
}
|
||||
} catch (error) {
|
||||
testStates.value[payload.id] = {
|
||||
@@ -190,14 +199,10 @@ async function saveServers() {
|
||||
saving.value = true
|
||||
try {
|
||||
const servers = localServers.value.map(toServerPayload)
|
||||
const result: { [key: string]: any } = await api.post('message/agent/mcp/servers', { servers })
|
||||
if (result.success) {
|
||||
toast.success(t('setting.system.aiAgentMcpSaveSuccess'))
|
||||
emit('saved', servers)
|
||||
dialogVisible.value = false
|
||||
return
|
||||
}
|
||||
toast.error(result.message || t('setting.system.aiAgentMcpSaveFailed'))
|
||||
await api.post<null>('message/agent/mcp/servers', { servers }, { feedback: 'silent' })
|
||||
toast.success(t('setting.system.aiAgentMcpSaveSuccess'))
|
||||
emit('saved', servers)
|
||||
dialogVisible.value = false
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : String(error))
|
||||
} finally {
|
||||
|
||||
@@ -33,11 +33,9 @@ async function handleDone() {
|
||||
// 重置配置
|
||||
async function handleReset() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(`/storage/reset/${props.type}`)
|
||||
if (result.success) {
|
||||
// 重置成功
|
||||
handleDone()
|
||||
}
|
||||
await api.get(`/storage/reset/${props.type}`)
|
||||
// 重置成功
|
||||
handleDone()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
@@ -41,62 +41,52 @@ async function handleDone() {
|
||||
// 调用/aliyun/qrcode api生成二维码
|
||||
async function getQrcode() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/storage/qrcode/alipan')
|
||||
if (result.success && result.data) {
|
||||
qrCodeUrl.value = result.data.codeUrl
|
||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||
} else {
|
||||
text.value = result.message
|
||||
}
|
||||
const result = await api.get<{ codeUrl: string }>('/storage/qrcode/alipan', { feedback: 'silent' })
|
||||
qrCodeUrl.value = result.codeUrl
|
||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
text.value = e instanceof Error ? e.message : t('common.apiRequestFailed')
|
||||
}
|
||||
}
|
||||
|
||||
// 调用/aliyun/check api验证二维码
|
||||
async function checkQrcode() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/storage/check/alipan')
|
||||
if (result.success && result.data) {
|
||||
const qrCodeStatus = result.data.status
|
||||
text.value = result.data.tip
|
||||
if (qrCodeStatus == 'LoginSuccess') {
|
||||
// 登录成功
|
||||
alertType.value = 'success'
|
||||
handleDone()
|
||||
} else if (qrCodeStatus == 'WaitLogin' || qrCodeStatus == 'ScanSuccess') {
|
||||
// 等待登录扫码成功
|
||||
alertType.value = 'info'
|
||||
clearTimeout(timeoutTimer)
|
||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||
} else {
|
||||
// 二维码过期
|
||||
alertType.value = 'error'
|
||||
}
|
||||
const result = await api.get<{ status: string; tip: string }>('/storage/check/alipan', { feedback: 'silent' })
|
||||
const qrCodeStatus = result.status
|
||||
text.value = result.tip
|
||||
if (qrCodeStatus == 'LoginSuccess') {
|
||||
// 登录成功
|
||||
alertType.value = 'success'
|
||||
handleDone()
|
||||
} else if (qrCodeStatus == 'WaitLogin' || qrCodeStatus == 'ScanSuccess') {
|
||||
// 等待登录扫码成功
|
||||
alertType.value = 'info'
|
||||
clearTimeout(timeoutTimer)
|
||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||
} else {
|
||||
// 二维码过期
|
||||
alertType.value = 'error'
|
||||
text.value = result.message
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
alertType.value = 'error'
|
||||
text.value = e instanceof Error ? e.message : t('common.apiRequestFailed')
|
||||
}
|
||||
}
|
||||
|
||||
// 重置配置
|
||||
async function handleReset() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/storage/reset/alipan')
|
||||
console.log(result.success)
|
||||
if (result.success) {
|
||||
// 重置成功
|
||||
alertType.value = 'success'
|
||||
handleDone()
|
||||
} else {
|
||||
alertType.value = 'error'
|
||||
text.value = result.message
|
||||
}
|
||||
await api.get<null>('/storage/reset/alipan', { feedback: 'silent' })
|
||||
// 重置成功
|
||||
alertType.value = 'success'
|
||||
handleDone()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
alertType.value = 'error'
|
||||
text.value = e instanceof Error ? e.message : t('common.apiRequestFailed')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -143,10 +143,8 @@ const countryOptions = [
|
||||
const fetchConfig = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await api.get('media/category/config')
|
||||
if (res && res.data) {
|
||||
parseConfig(res.data)
|
||||
}
|
||||
const config = await api.get<CategoryConfig | null>('media/category/config', { feedback: 'silent' })
|
||||
if (config) parseConfig(config)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error(t('setting.category.loadFailed'))
|
||||
@@ -334,17 +332,13 @@ const saveConfig = async () => {
|
||||
}
|
||||
})
|
||||
|
||||
const res: any = await api.post('media/category/config', payload)
|
||||
if (res && res.success) {
|
||||
toast.success(t('setting.category.saveSuccess'))
|
||||
emit('save')
|
||||
emit('close')
|
||||
} else {
|
||||
toast.error(t('setting.category.saveFailed', { message: res.message || 'Error' }))
|
||||
}
|
||||
await api.post<null>('media/category/config', payload, { feedback: 'silent' })
|
||||
toast.success(t('setting.category.saveSuccess'))
|
||||
emit('save')
|
||||
emit('close')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error(t('setting.category.saveFailed', { message: 'Network or Config Error' }))
|
||||
toast.error(t('setting.category.saveFailed', { message: e instanceof Error ? e.message : t('common.error') }))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
@@ -58,12 +58,8 @@ async function loadHistory({ done }: { done: (status: 'empty' | 'error' | 'ok')
|
||||
/** 删除指定下载历史,并在成功后同步移除当前列表项。 */
|
||||
async function deleteHistory(item: DownloadHistory) {
|
||||
try {
|
||||
const result: { success?: boolean } = await api.delete('history/download', { data: item })
|
||||
if (result.success) {
|
||||
historyList.value = historyList.value.filter(history => history.id !== item.id)
|
||||
return
|
||||
}
|
||||
$toast.error(t('dialog.downloadHistory.deleteFailed'))
|
||||
await api.delete('history/download', { data: item, feedback: 'silent' })
|
||||
historyList.value = historyList.value.filter(history => history.id !== item.id)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('dialog.downloadHistory.deleteFailed'))
|
||||
@@ -124,9 +120,9 @@ function getSeasonEpisode(item: DownloadHistory) {
|
||||
<template #empty />
|
||||
|
||||
<VList lines="three" class="download-history-dialog__content py-0">
|
||||
<VVirtualScroll v-if="historyList.length > 0" renderless :items="historyList" :item-height="120">
|
||||
<template #default="{ item, itemRef }">
|
||||
<div :ref="itemRef">
|
||||
<VVirtualScroll v-if="historyList.length > 0" :renderless="true" :items="historyList" :item-height="120">
|
||||
<template #default="{ item, ...slotProps }">
|
||||
<div :ref="'itemRef' in slotProps ? slotProps.itemRef : undefined">
|
||||
<VListItem class="download-history-item">
|
||||
<template #prepend>
|
||||
<VImg
|
||||
|
||||
@@ -187,363 +187,363 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
v-if="downloaderInfoDialog"
|
||||
v-model="downloaderInfoDialog"
|
||||
scrollable
|
||||
max-width="40rem"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-download" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('common.config') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ props.downloader.name }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn v-model="downloaderInfoDialog" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VForm ref="downloaderForm">
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch v-model="downloaderInfo.enabled" :label="t('downloader.enabled')" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="downloaderInfo.default"
|
||||
:label="t('downloader.default')"
|
||||
:disabled="!downloaderInfo.enabled"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-if="downloaderInfo.type == 'qbittorrent'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.name"
|
||||
:label="t('downloader.name')"
|
||||
:placeholder="t('downloader.nameRequired')"
|
||||
:hint="t('downloader.name')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.host"
|
||||
:label="t('downloader.host')"
|
||||
placeholder="http(s)://ip:port"
|
||||
:hint="t('downloader.host')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.apikey"
|
||||
type="password"
|
||||
:label="t('downloader.apiKey')"
|
||||
:hint="t('downloader.qbittorrentApiKeyHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-key-variant"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.username"
|
||||
:label="t('downloader.username')"
|
||||
:hint="t('downloader.username')"
|
||||
:disabled="!!downloaderInfo.config.apikey"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.password"
|
||||
type="password"
|
||||
:label="t('downloader.password')"
|
||||
:hint="t('downloader.password')"
|
||||
:disabled="!!downloaderInfo.config.apikey"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="downloaderInfo.config.category"
|
||||
:label="t('downloader.category')"
|
||||
:hint="t('downloader.category')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="downloaderInfo.config.sequentail"
|
||||
:label="t('downloader.sequentail')"
|
||||
:hint="t('downloader.sequentail')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="downloaderInfo.config.force_resume"
|
||||
:label="t('downloader.force_resume')"
|
||||
:hint="t('downloader.force_resume')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="downloaderInfo.config.first_last_piece"
|
||||
:label="t('downloader.first_last_piece')"
|
||||
:hint="t('downloader.first_last_piece')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="downloaderInfo.config.incomplete_files_ext"
|
||||
:label="t('downloader.incomplete_files_ext')"
|
||||
:hint="t('downloader.incomplete_files_extHint')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="downloaderInfo.type == 'transmission'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.name"
|
||||
:label="t('downloader.name')"
|
||||
:placeholder="t('downloader.nameRequired')"
|
||||
:hint="t('downloader.name')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.host"
|
||||
:label="t('downloader.host')"
|
||||
placeholder="http(s)://ip:port"
|
||||
:hint="t('downloader.host')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.username"
|
||||
:label="t('downloader.username')"
|
||||
:hint="t('downloader.username')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.password"
|
||||
type="password"
|
||||
:label="t('downloader.password')"
|
||||
:hint="t('downloader.password')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="downloaderInfo.config.rename_partial_files"
|
||||
:label="t('downloader.rename_partial_files')"
|
||||
:hint="t('downloader.rename_partial_filesHint')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="downloaderInfo.type == 'rtorrent'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.name"
|
||||
:label="t('downloader.name')"
|
||||
:placeholder="t('downloader.nameRequired')"
|
||||
:hint="t('downloader.name')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.host"
|
||||
:label="t('downloader.host')"
|
||||
placeholder="http(s)://ip:port/RPC2"
|
||||
:hint="t('downloader.rtorrentHostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.username"
|
||||
:label="t('downloader.username')"
|
||||
:hint="t('downloader.username')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.password"
|
||||
type="password"
|
||||
:label="t('downloader.password')"
|
||||
:hint="t('downloader.password')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.type"
|
||||
:label="t('downloader.type')"
|
||||
:hint="t('downloader.customTypeHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-cog"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.name"
|
||||
:label="t('downloader.name')"
|
||||
:hint="t('downloader.nameRequired')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VDivider class="my-2">
|
||||
<span class="text-body-1 font-weight-medium">{{ t('downloader.pathMapping') }}</span>
|
||||
</VDivider>
|
||||
v-if="downloaderInfoDialog"
|
||||
v-model="downloaderInfoDialog"
|
||||
scrollable
|
||||
max-width="40rem"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-download" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('common.config') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ props.downloader.name }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn v-model="downloaderInfoDialog" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VForm ref="downloaderForm">
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch v-model="downloaderInfo.enabled" :label="t('downloader.enabled')" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="downloaderInfo.default"
|
||||
:label="t('downloader.default')"
|
||||
:disabled="!downloaderInfo.enabled"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-if="downloaderInfo.type == 'qbittorrent'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.name"
|
||||
:label="t('downloader.name')"
|
||||
:placeholder="t('downloader.nameRequired')"
|
||||
:hint="t('downloader.name')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.host"
|
||||
:label="t('downloader.host')"
|
||||
placeholder="http(s)://ip:port"
|
||||
:hint="t('downloader.host')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.apikey"
|
||||
type="password"
|
||||
:label="t('downloader.apiKey')"
|
||||
:hint="t('downloader.qbittorrentApiKeyHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-key-variant"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.username"
|
||||
:label="t('downloader.username')"
|
||||
:hint="t('downloader.username')"
|
||||
:disabled="!!downloaderInfo.config.apikey"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.password"
|
||||
type="password"
|
||||
:label="t('downloader.password')"
|
||||
:hint="t('downloader.password')"
|
||||
:disabled="!!downloaderInfo.config.apikey"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="downloaderInfo.config.category"
|
||||
:label="t('downloader.category')"
|
||||
:hint="t('downloader.category')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="downloaderInfo.config.sequentail"
|
||||
:label="t('downloader.sequentail')"
|
||||
:hint="t('downloader.sequentail')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="downloaderInfo.config.force_resume"
|
||||
:label="t('downloader.force_resume')"
|
||||
:hint="t('downloader.force_resume')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="downloaderInfo.config.first_last_piece"
|
||||
:label="t('downloader.first_last_piece')"
|
||||
:hint="t('downloader.first_last_piece')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="downloaderInfo.config.incomplete_files_ext"
|
||||
:label="t('downloader.incomplete_files_ext')"
|
||||
:hint="t('downloader.incomplete_files_extHint')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="downloaderInfo.type == 'transmission'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.name"
|
||||
:label="t('downloader.name')"
|
||||
:placeholder="t('downloader.nameRequired')"
|
||||
:hint="t('downloader.name')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.host"
|
||||
:label="t('downloader.host')"
|
||||
placeholder="http(s)://ip:port"
|
||||
:hint="t('downloader.host')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.username"
|
||||
:label="t('downloader.username')"
|
||||
:hint="t('downloader.username')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.password"
|
||||
type="password"
|
||||
:label="t('downloader.password')"
|
||||
:hint="t('downloader.password')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="downloaderInfo.config.rename_partial_files"
|
||||
:label="t('downloader.rename_partial_files')"
|
||||
:hint="t('downloader.rename_partial_filesHint')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="downloaderInfo.type == 'rtorrent'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.name"
|
||||
:label="t('downloader.name')"
|
||||
:placeholder="t('downloader.nameRequired')"
|
||||
:hint="t('downloader.name')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.host"
|
||||
:label="t('downloader.host')"
|
||||
placeholder="http(s)://ip:port/RPC2"
|
||||
:hint="t('downloader.rtorrentHostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.username"
|
||||
:label="t('downloader.username')"
|
||||
:hint="t('downloader.username')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.password"
|
||||
type="password"
|
||||
:label="t('downloader.password')"
|
||||
:hint="t('downloader.password')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.type"
|
||||
:label="t('downloader.type')"
|
||||
:hint="t('downloader.customTypeHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-cog"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.name"
|
||||
:label="t('downloader.name')"
|
||||
:hint="t('downloader.nameRequired')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VDivider class="my-2">
|
||||
<span class="text-body-1 font-weight-medium">{{ t('downloader.pathMapping') }}</span>
|
||||
</VDivider>
|
||||
|
||||
<div v-if="pathMappingRows.length === 0" class="text-center py-2">
|
||||
<VIcon icon="mdi-folder-network" size="48" class="text-disabled mb-1" />
|
||||
<div class="text-body-2 text-disabled">{{ t('common.noData') }}</div>
|
||||
</div>
|
||||
<div v-if="pathMappingRows.length === 0" class="text-center py-2">
|
||||
<VIcon icon="mdi-folder-network" size="48" class="text-disabled mb-1" />
|
||||
<div class="text-body-2 text-disabled">{{ t('common.noData') }}</div>
|
||||
</div>
|
||||
|
||||
<VCard
|
||||
v-for="(row, index) in pathMappingRows"
|
||||
:key="row.id"
|
||||
variant="outlined"
|
||||
class="path-mapping-card my-2"
|
||||
>
|
||||
<VCardText class="pa-3">
|
||||
<VRow align="center" no-gutters>
|
||||
<VCol cols="12" class="mb-2">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<VIcon icon="mdi-folder-outline" size="18" class="me-1 text-primary" />
|
||||
<span class="text-caption text-medium-emphasis">{{ t('downloader.storagePath') }}</span>
|
||||
</div>
|
||||
<VRow no-gutters>
|
||||
<VCol cols="12" sm="4" class="path-storage-select-col pe-sm-2">
|
||||
<VSelect
|
||||
:model-value="getStorageType(row.storage)"
|
||||
:items="prefixOptions"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
@update:model-value="v => updateStoragePrefix(row, v)"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" sm="8">
|
||||
<VTextField
|
||||
:model-value="parseStoragePath(row.storage)[1]"
|
||||
:placeholder="'/path/to/storage'"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details="auto"
|
||||
:rules="pathValidationRules"
|
||||
@update:model-value="v => updateStorageSuffix(row, v)"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCol>
|
||||
<VCard
|
||||
v-for="(row, index) in pathMappingRows"
|
||||
:key="row.id"
|
||||
variant="outlined"
|
||||
class="path-mapping-card my-2"
|
||||
>
|
||||
<VCardText class="pa-3">
|
||||
<VRow align="center" no-gutters>
|
||||
<VCol cols="12" class="mb-2">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<VIcon icon="mdi-folder-outline" size="18" class="me-1 text-primary" />
|
||||
<span class="text-caption text-medium-emphasis">{{ t('downloader.storagePath') }}</span>
|
||||
</div>
|
||||
<VRow no-gutters>
|
||||
<VCol cols="12" sm="4" class="path-storage-select-col pe-sm-2">
|
||||
<VSelect
|
||||
:model-value="getStorageType(row.storage)"
|
||||
:items="prefixOptions"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
@update:model-value="(v: string) => updateStoragePrefix(row, v)"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" sm="8">
|
||||
<VTextField
|
||||
:model-value="parseStoragePath(row.storage)[1]"
|
||||
:placeholder="'/path/to/storage'"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details="auto"
|
||||
:rules="pathValidationRules"
|
||||
@update:model-value="v => updateStorageSuffix(row, v)"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" class="mb-1">
|
||||
<div class="d-flex align-center justify-center my-1">
|
||||
<VIcon icon="mdi-arrow-down" size="18" class="text-medium-emphasis" />
|
||||
</div>
|
||||
<div class="d-flex align-center mb-1">
|
||||
<VIcon icon="mdi-download-outline" size="18" class="me-1 text-success" />
|
||||
<span class="text-caption text-medium-emphasis">{{ t('downloader.downloadPath') }}</span>
|
||||
</div>
|
||||
<VRow no-gutters>
|
||||
<VCol cols="12" sm="4" class="d-none d-sm-block" />
|
||||
<VCol cols="12" sm="8">
|
||||
<VTextField
|
||||
v-model="row.download"
|
||||
:placeholder="'/path/to/download'"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details="auto"
|
||||
:rules="pathValidationRules"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCol>
|
||||
<VCol cols="12" class="mb-1">
|
||||
<div class="d-flex align-center justify-center my-1">
|
||||
<VIcon icon="mdi-arrow-down" size="18" class="text-medium-emphasis" />
|
||||
</div>
|
||||
<div class="d-flex align-center mb-1">
|
||||
<VIcon icon="mdi-download-outline" size="18" class="me-1 text-success" />
|
||||
<span class="text-caption text-medium-emphasis">{{ t('downloader.downloadPath') }}</span>
|
||||
</div>
|
||||
<VRow no-gutters>
|
||||
<VCol cols="12" sm="4" class="d-none d-sm-block" />
|
||||
<VCol cols="12" sm="8">
|
||||
<VTextField
|
||||
v-model="row.download"
|
||||
:placeholder="'/path/to/download'"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details="auto"
|
||||
:rules="pathValidationRules"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" class="d-flex justify-end pt-1">
|
||||
<IconBtn variant="text" color="error" size="small" @click="removePathMapping(index)">
|
||||
<VIcon icon="mdi-delete-outline" />
|
||||
</IconBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<VCol cols="12" class="d-flex justify-end pt-1">
|
||||
<IconBtn variant="text" color="error" size="small" @click="removePathMapping(index)">
|
||||
<VIcon icon="mdi-delete-outline" />
|
||||
</IconBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus-circle-outline"
|
||||
@click="addPathMapping"
|
||||
class="mt-1"
|
||||
size="small"
|
||||
>
|
||||
{{ t('common.add') }} {{ t('downloader.pathMapping') }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions class="app-dialog-actions">
|
||||
<VSpacer />
|
||||
<VBtn color="primary" variant="flat" @click="saveDownloaderInfo" prepend-icon="mdi-content-save" class="px-5">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus-circle-outline"
|
||||
@click="addPathMapping"
|
||||
class="mt-1"
|
||||
size="small"
|
||||
>
|
||||
{{ t('common.add') }} {{ t('downloader.pathMapping') }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions class="app-dialog-actions">
|
||||
<VSpacer />
|
||||
<VBtn color="primary" variant="flat" @click="saveDownloaderInfo" prepend-icon="mdi-content-save" class="px-5">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import { getApiBusinessErrorMessage } from '@/api/client'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import { SubscribeShare } from '@/api/types'
|
||||
import router from '@/router'
|
||||
@@ -51,8 +52,8 @@ function toggleExpand() {
|
||||
// 加载follow用户列表
|
||||
async function queryFollowUsers() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/public/FollowSubscribers')
|
||||
followUsers.value = result.data?.value ?? []
|
||||
const result = await api.get<{ value?: string[] }>('system/setting/public/FollowSubscribers')
|
||||
followUsers.value = result.value ?? []
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('subscribe.requestFailed'))
|
||||
@@ -62,10 +63,8 @@ async function queryFollowUsers() {
|
||||
// follow用户
|
||||
async function followUser() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(`subscribe/follow?share_uid=${props.media?.share_uid}`)
|
||||
if (result.success) {
|
||||
queryFollowUsers()
|
||||
}
|
||||
await api.post<null>(`subscribe/follow?share_uid=${props.media?.share_uid}`, undefined, { feedback: 'silent' })
|
||||
queryFollowUsers()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('subscribe.requestFailed'))
|
||||
@@ -75,14 +74,13 @@ async function followUser() {
|
||||
// unfollow用户
|
||||
async function unfollowUser() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete('subscribe/follow', {
|
||||
await api.delete<null>('subscribe/follow', {
|
||||
params: {
|
||||
share_uid: props.media?.share_uid,
|
||||
},
|
||||
feedback: 'silent',
|
||||
})
|
||||
if (result.success) {
|
||||
queryFollowUsers()
|
||||
}
|
||||
queryFollowUsers()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('subscribe.requestFailed'))
|
||||
@@ -124,21 +122,16 @@ async function doFork() {
|
||||
try {
|
||||
processing.value = true
|
||||
// 请求API
|
||||
const result: { [key: string]: any } = await api.post('subscribe/fork', props.media)
|
||||
// 订阅状态
|
||||
if (result.success) {
|
||||
$toast.success(t('subscribe.addSuccess', { name: props.media?.share_title }))
|
||||
// 完成
|
||||
emit('fork', result.data.id)
|
||||
} else {
|
||||
$toast.error(t('subscribe.addFailed', { name: props.media?.share_title, message: result.message }))
|
||||
}
|
||||
const result = await api.post<{ id: number }>('subscribe/fork', props.media, { feedback: 'silent' })
|
||||
$toast.success(t('subscribe.addSuccess', { name: props.media?.share_title }))
|
||||
// 完成
|
||||
emit('fork', result.id)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(
|
||||
t('subscribe.addFailed', {
|
||||
name: props.media?.share_title,
|
||||
message: t('subscribe.requestFailed'),
|
||||
message: getApiBusinessErrorMessage(error) || t('subscribe.requestFailed'),
|
||||
}),
|
||||
)
|
||||
} finally {
|
||||
@@ -154,22 +147,22 @@ async function doDelete() {
|
||||
try {
|
||||
deleting.value = true
|
||||
// 请求API
|
||||
const result: { [key: string]: any } = await api.delete(`subscribe/share/${props.media?.id}`, {
|
||||
await api.delete<null>(`subscribe/share/${props.media?.id}`, {
|
||||
params: {
|
||||
share_uid: globalSettings.USER_UNIQUE_ID,
|
||||
},
|
||||
feedback: 'silent',
|
||||
})
|
||||
// 订阅状态
|
||||
if (result.success) {
|
||||
$toast.success(t('subscribe.cancelSuccess'))
|
||||
// 完成
|
||||
emit('delete')
|
||||
} else {
|
||||
$toast.error(t('subscribe.cancelFailed', { message: result.message }))
|
||||
}
|
||||
$toast.success(t('subscribe.cancelSuccess'))
|
||||
// 完成
|
||||
emit('delete')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('subscribe.cancelFailed', { message: t('subscribe.requestFailed') }))
|
||||
$toast.error(
|
||||
t('subscribe.cancelFailed', {
|
||||
message: getApiBusinessErrorMessage(error) || t('subscribe.requestFailed'),
|
||||
}),
|
||||
)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
doneNProgress()
|
||||
|
||||
@@ -78,17 +78,18 @@ async function doFork() {
|
||||
try {
|
||||
processing.value = true
|
||||
// 请求API
|
||||
const result: { [key: string]: any } = await api.post('workflow/fork', props.workflow)
|
||||
// 工作流状态
|
||||
if (result.success) {
|
||||
$toast.success(t('workflow.addSuccess', { name: props.workflow?.share_title }))
|
||||
// 完成
|
||||
emit('fork', result.data.id)
|
||||
} else {
|
||||
$toast.error(t('workflow.addFailed', { name: props.workflow?.share_title, message: result.message }))
|
||||
}
|
||||
const result = await api.post<{ id: string }>('workflow/fork', props.workflow, { feedback: 'silent' })
|
||||
$toast.success(t('workflow.addSuccess', { name: props.workflow?.share_title }))
|
||||
// 完成
|
||||
emit('fork', result.id)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(
|
||||
t('workflow.addFailed', {
|
||||
name: props.workflow?.share_title,
|
||||
message: error instanceof Error ? error.message : '',
|
||||
}),
|
||||
)
|
||||
} finally {
|
||||
processing.value = false
|
||||
doneNProgress()
|
||||
@@ -102,21 +103,18 @@ async function doDelete() {
|
||||
try {
|
||||
deleting.value = true
|
||||
// 请求API
|
||||
const result: { [key: string]: any } = await api.delete(`workflow/share/${props.workflow?.id}`, {
|
||||
const result = await api.delete<{ id: string }>(`workflow/share/${props.workflow?.id}`, {
|
||||
params: {
|
||||
share_uid: globalSettings.USER_UNIQUE_ID,
|
||||
},
|
||||
feedback: 'silent',
|
||||
})
|
||||
// 工作流状态
|
||||
if (result.success) {
|
||||
$toast.success(t('workflow.cancelSuccess'))
|
||||
// 完成
|
||||
emit('delete', result.data.id)
|
||||
} else {
|
||||
$toast.error(t('workflow.cancelFailed', { message: result.message }))
|
||||
}
|
||||
$toast.success(t('workflow.cancelSuccess'))
|
||||
// 完成
|
||||
emit('delete', result.id)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('workflow.cancelFailed', { message: error instanceof Error ? error.message : '' }))
|
||||
} finally {
|
||||
deleting.value = false
|
||||
doneNProgress()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@ import QRCode from 'qrcode'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import api from '@/api'
|
||||
import type { ApiResponse } from '@/api/types'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -77,14 +76,14 @@ async function getOtpUri() {
|
||||
// 未启用OTP,生成新的二维码
|
||||
otpLoading.value = true
|
||||
try {
|
||||
const result = (await api.post('mfa/otp/generate')) as ApiResponse<{
|
||||
const result = await api.post<{
|
||||
uri: string
|
||||
secret: string
|
||||
}>
|
||||
const uri = result.data?.uri?.trim()
|
||||
const otpSecret = result.data?.secret?.trim()
|
||||
}>('mfa/otp/generate', undefined, { feedback: 'silent' })
|
||||
const uri = result.uri?.trim()
|
||||
const otpSecret = result.secret?.trim()
|
||||
|
||||
if (result.success && uri && otpSecret) {
|
||||
if (uri && otpSecret) {
|
||||
const image = await QRCode.toDataURL(uri, {
|
||||
width: 200,
|
||||
margin: 1,
|
||||
@@ -96,7 +95,7 @@ async function getOtpUri() {
|
||||
qrCodeImage.value = image
|
||||
} else {
|
||||
if (generation !== otpGeneration || !props.modelValue) return
|
||||
setOtpGenerateError(result.message || 'empty otp uri')
|
||||
setOtpGenerateError('empty otp uri')
|
||||
}
|
||||
} catch (error) {
|
||||
if (generation !== otpGeneration || !props.modelValue) return
|
||||
@@ -114,18 +113,10 @@ async function judgeOtpPassword() {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const result = (await api.post('mfa/otp/verify', {
|
||||
uri: otpUri.value,
|
||||
otpPassword: otpPassword.value,
|
||||
})) as ApiResponse
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(t('profile.otpEnableSuccess'))
|
||||
show.value = false
|
||||
emit('update:isOtp', true)
|
||||
} else {
|
||||
$toast.error(t('profile.otpEnableFailed', { message: result.message }))
|
||||
}
|
||||
await api.post('mfa/otp/verify', { uri: otpUri.value, otpPassword: otpPassword.value }, { feedback: 'silent' })
|
||||
$toast.success(t('profile.otpEnableSuccess'))
|
||||
show.value = false
|
||||
emit('update:isOtp', true)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('profile.otpEnableFailed', { message: error instanceof Error ? error.message : String(error) }))
|
||||
@@ -139,16 +130,10 @@ function disableOtp() {
|
||||
text: t('profile.confirmToDisableOtp'),
|
||||
callback: async (password: string) => {
|
||||
try {
|
||||
const result = (await api.post('mfa/otp/disable', {
|
||||
password,
|
||||
})) as ApiResponse
|
||||
if (result.success) {
|
||||
emit('update:isOtp', false)
|
||||
$toast.success(t('profile.otpDisableSuccess'))
|
||||
show.value = false
|
||||
} else {
|
||||
$toast.error(t('profile.otpDisableFailed', { message: result.message }))
|
||||
}
|
||||
await api.post('mfa/otp/disable', { password }, { feedback: 'silent' })
|
||||
emit('update:isOtp', false)
|
||||
$toast.success(t('profile.otpDisableSuccess'))
|
||||
show.value = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('profile.otpDisableFailed', { message: error instanceof Error ? error.message : String(error) }))
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { formatDateDifference } from '@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import type { ApiResponse, PassKey } from '@/api/types'
|
||||
import { getApiBusinessErrorMessage } from '@/api/client'
|
||||
import type { PassKey } from '@/api/types'
|
||||
import { isAxiosError } from 'axios'
|
||||
|
||||
interface Props {
|
||||
@@ -69,14 +70,10 @@ async function fetchPassKeyList() {
|
||||
passkeyListLoading.value = true
|
||||
passkeyListFailed.value = false
|
||||
try {
|
||||
const result = (await api.get('mfa/passkey/list')) as ApiResponse<PassKey[]>
|
||||
const result = await api.get<PassKey[]>('mfa/passkey/list')
|
||||
if (generation !== passkeyListGeneration || !props.modelValue) return
|
||||
if (result.success) {
|
||||
passkeyList.value = result.data || []
|
||||
emit('update:passkeyList', passkeyList.value)
|
||||
} else {
|
||||
passkeyListFailed.value = true
|
||||
}
|
||||
passkeyList.value = result || []
|
||||
emit('update:passkeyList', passkeyList.value)
|
||||
} catch (error) {
|
||||
if (generation !== passkeyListGeneration || !props.modelValue) return
|
||||
passkeyListFailed.value = true
|
||||
@@ -111,24 +108,20 @@ async function registerPassKey() {
|
||||
passkeyRegistering.value = true
|
||||
try {
|
||||
// 1. 开始注册
|
||||
const startResult = (await api.post(
|
||||
const startResult = await api.post<{ options: string; transaction_token: string }>(
|
||||
'mfa/passkey/register/start',
|
||||
{
|
||||
name: registrationName,
|
||||
},
|
||||
{
|
||||
signal: registrationAbortController.signal,
|
||||
feedback: 'silent',
|
||||
},
|
||||
)) as ApiResponse<{ options: string; transaction_token: string }>
|
||||
)
|
||||
|
||||
if (generation !== passkeyRegistrationGeneration || !props.modelValue) return
|
||||
|
||||
if (!startResult.success) {
|
||||
$toast.error(startResult.message || t('profile.passkeyRegisterFailed'))
|
||||
return
|
||||
}
|
||||
|
||||
const { options, transaction_token: transactionToken } = startResult.data
|
||||
const { options, transaction_token: transactionToken } = startResult
|
||||
const publicKeyOptions = JSON.parse(options)
|
||||
|
||||
// 2. 调用WebAuthn API
|
||||
@@ -169,7 +162,7 @@ async function registerPassKey() {
|
||||
}
|
||||
|
||||
// 4. 完成注册
|
||||
const finishResult = (await api.post(
|
||||
await api.post(
|
||||
'mfa/passkey/register/finish',
|
||||
{
|
||||
credential: credentialJSON,
|
||||
@@ -178,18 +171,15 @@ async function registerPassKey() {
|
||||
},
|
||||
{
|
||||
signal: registrationAbortController.signal,
|
||||
feedback: 'silent',
|
||||
},
|
||||
)) as ApiResponse
|
||||
)
|
||||
|
||||
if (generation !== passkeyRegistrationGeneration || !props.modelValue) return
|
||||
|
||||
if (finishResult.success) {
|
||||
$toast.success(t('profile.passkeyRegisterSuccess'))
|
||||
passkeyName.value = ''
|
||||
await fetchPassKeyList()
|
||||
} else {
|
||||
$toast.error(finishResult.message || t('profile.passkeyRegisterFailed'))
|
||||
}
|
||||
$toast.success(t('profile.passkeyRegisterSuccess'))
|
||||
passkeyName.value = ''
|
||||
await fetchPassKeyList()
|
||||
} catch (error) {
|
||||
if (generation !== passkeyRegistrationGeneration || !props.modelValue) return
|
||||
console.error('PassKey注册失败:', error)
|
||||
@@ -219,19 +209,12 @@ async function deletePassKey(passkeyId: number) {
|
||||
text: t('profile.confirmToDeletePasskey'),
|
||||
callback: async (password: string) => {
|
||||
try {
|
||||
const result = (await api.post('mfa/passkey/delete', {
|
||||
passkey_id: passkeyId,
|
||||
password,
|
||||
})) as ApiResponse
|
||||
if (result.success) {
|
||||
$toast.success(t('profile.passkeyDeleteSuccess'))
|
||||
await fetchPassKeyList()
|
||||
} else {
|
||||
$toast.error(result.message || t('profile.passkeyDeleteFailed'))
|
||||
}
|
||||
await api.post('mfa/passkey/delete', { passkey_id: passkeyId, password }, { feedback: 'silent' })
|
||||
$toast.success(t('profile.passkeyDeleteSuccess'))
|
||||
await fetchPassKeyList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('profile.passkeyDeleteFailed'))
|
||||
$toast.error(getApiBusinessErrorMessage(error) || t('profile.passkeyDeleteFailed'))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useDisplay } from 'vuetify'
|
||||
import type { Plugin, RenderProps } from '@/api/types'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import api from '@/api'
|
||||
import api, { pluginApi } from '@/api'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import FormRender from '../render/FormRender.vue'
|
||||
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
||||
@@ -163,19 +163,12 @@ async function savePluginConf() {
|
||||
progressDialog.value = true
|
||||
progressText.value = t('dialog.pluginConfig.saving', { name: props.plugin?.plugin_name })
|
||||
try {
|
||||
const result = (await api.put(`plugin/${props.plugin?.id}`, pluginConfigForm.value)) as {
|
||||
message?: string
|
||||
success: boolean
|
||||
}
|
||||
if (result.success) {
|
||||
$toast.success(t('dialog.pluginConfig.saveSuccess', { name: props.plugin?.plugin_name }))
|
||||
// 通知父组件刷新
|
||||
emit('save')
|
||||
// 导航声明可能由插件配置控制;刷新失败不改变已经成功的配置保存结果。
|
||||
void pluginSidebarNavStore.ensureSidebarNav(true).catch(error => console.error(error))
|
||||
} else {
|
||||
$toast.error(t('dialog.pluginConfig.saveFailed', { name: props.plugin?.plugin_name, message: result.message }))
|
||||
}
|
||||
await api.put(`plugin/${props.plugin?.id}`, pluginConfigForm.value, { feedback: 'silent' })
|
||||
$toast.success(t('dialog.pluginConfig.saveSuccess', { name: props.plugin?.plugin_name }))
|
||||
// 通知父组件刷新
|
||||
emit('save')
|
||||
// 导航声明可能由插件配置控制;刷新失败不改变已经成功的配置保存结果。
|
||||
void pluginSidebarNavStore.ensureSidebarNav(true).catch(error => console.error(error))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
@@ -238,7 +231,7 @@ onBeforeMount(async () => {
|
||||
<component
|
||||
:is="dynamicComponent"
|
||||
:initial-config="pluginConfigForm"
|
||||
:api="api"
|
||||
:api="pluginApi"
|
||||
:native-subscribe="nativeSubscribe"
|
||||
@save="handleVueComponentSave"
|
||||
@layout="handleVueComponentLayout"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useDisplay } from 'vuetify'
|
||||
import type { Plugin, RenderProps } from '@/api/types'
|
||||
import PageRender from '@/components/render/PageRender.vue'
|
||||
import api from '@/api'
|
||||
import api, { pluginApi } from '@/api'
|
||||
import { loadRemoteComponent } from '@/utils/federationLoader'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { useToast } from 'vue-toastification'
|
||||
@@ -174,7 +174,7 @@ onMounted(() => {
|
||||
<VCardText class="pa-0">
|
||||
<component
|
||||
:is="dynamicComponent"
|
||||
:api="api"
|
||||
:api="pluginApi"
|
||||
:native-subscribe="nativeSubscribe"
|
||||
:show_switch="show_switch"
|
||||
@action="handleAction"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import type { ApiResponse, Plugin, PluginRating } from '@/api/types'
|
||||
import { getApiBusinessErrorMessage, isApiBusinessFailure } from '@/api/client'
|
||||
import type { Plugin, PluginRating } from '@/api/types'
|
||||
import { formatDownloadCount } from '@/@core/utils/formatters'
|
||||
import PluginRatingDisplay from '@/components/common/PluginRatingDisplay.vue'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
@@ -141,32 +142,29 @@ async function installPlugin(releaseVersion?: string, repoUrl?: string) {
|
||||
}),
|
||||
)
|
||||
|
||||
const result: ApiResponse<unknown> = await api.get(`plugin/install/${props.plugin?.id}`, {
|
||||
await api.get(`plugin/install/${props.plugin?.id}`, {
|
||||
params: {
|
||||
repo_url: repoUrl || props.plugin?.repo_url,
|
||||
release_version: releaseVersion,
|
||||
force: isInstalled.value || props.plugin?.has_update || Boolean(releaseVersion),
|
||||
},
|
||||
feedback: 'silent',
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(
|
||||
isInstalled.value
|
||||
? t('plugin.updateSuccess', { name: props.plugin?.plugin_name })
|
||||
: t('plugin.installSuccess', { name: props.plugin?.plugin_name }),
|
||||
)
|
||||
versionHistoryDialogController?.close()
|
||||
versionHistoryDialogController = null
|
||||
visible.value = false
|
||||
emit('install')
|
||||
} else {
|
||||
$toast.error(t(failureMessageKey, { name: props.plugin?.plugin_name, message: result.message }))
|
||||
}
|
||||
$toast.success(
|
||||
isInstalled.value
|
||||
? t('plugin.updateSuccess', { name: props.plugin?.plugin_name })
|
||||
: t('plugin.installSuccess', { name: props.plugin?.plugin_name }),
|
||||
)
|
||||
versionHistoryDialogController?.close()
|
||||
versionHistoryDialogController = null
|
||||
visible.value = false
|
||||
emit('install')
|
||||
} catch (error) {
|
||||
$toast.error(
|
||||
t(failureMessageKey, {
|
||||
name: props.plugin?.plugin_name,
|
||||
message: t('common.serverConnectionFailed'),
|
||||
message: getApiBusinessErrorMessage(error) || t('common.serverConnectionFailed'),
|
||||
}),
|
||||
)
|
||||
console.error(error)
|
||||
@@ -214,20 +212,24 @@ async function submitPluginRating() {
|
||||
|
||||
ratingSubmitting.value = true
|
||||
try {
|
||||
const result: ApiResponse<PluginRating> = await api.post(`plugin/rating/${props.plugin.id}`, {
|
||||
rating: selectedRating.value,
|
||||
})
|
||||
if (result.success) {
|
||||
rating.value = result.data
|
||||
selectedRating.value = result.data.user_rating || selectedRating.value
|
||||
emit('rating', result.data)
|
||||
$toast.success(t('plugin.ratingSuccess', { name: props.plugin?.plugin_name }))
|
||||
} else {
|
||||
$toast.error(t('plugin.ratingFailed', { message: result.message || t('common.unknown') }))
|
||||
}
|
||||
const result = await api.post<PluginRating>(
|
||||
`plugin/rating/${props.plugin.id}`,
|
||||
{ rating: selectedRating.value },
|
||||
{ feedback: 'silent' },
|
||||
)
|
||||
rating.value = result
|
||||
selectedRating.value = result.user_rating || selectedRating.value
|
||||
emit('rating', result)
|
||||
$toast.success(t('plugin.ratingSuccess', { name: props.plugin?.plugin_name }))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('plugin.ratingFailed', { message: t('common.serverConnectionFailed') }))
|
||||
const businessMessage = getApiBusinessErrorMessage(error)
|
||||
$toast.error(
|
||||
t('plugin.ratingFailed', {
|
||||
message:
|
||||
businessMessage || (isApiBusinessFailure(error) ? t('common.unknown') : t('common.serverConnectionFailed')),
|
||||
}),
|
||||
)
|
||||
} finally {
|
||||
ratingSubmitting.value = false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import type { ApiResponse } from '@/api/types'
|
||||
import draggable from 'vuedraggable'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -136,12 +135,11 @@ async function queryMarketRepoSetting() {
|
||||
loadReposFailed.value = false
|
||||
|
||||
try {
|
||||
const result = (await api.get('system/setting/public/PLUGIN_MARKET')) as unknown as ApiResponse<{
|
||||
const result = await api.get<{
|
||||
value?: string
|
||||
}>
|
||||
if (!result.success) throw new Error(result.message || '')
|
||||
}>('system/setting/public/PLUGIN_MARKET')
|
||||
|
||||
repoList.value = parseRepoInput(result.data?.value || '').repos
|
||||
repoList.value = parseRepoInput(result.value || '').repos
|
||||
syncTextFromList()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
@@ -161,12 +159,9 @@ async function saveHandle() {
|
||||
|
||||
saving.value = true
|
||||
const repoStringToSave = reposToSave.join(',')
|
||||
const result = (await api.post('system/setting/PLUGIN_MARKET', repoStringToSave)) as unknown as ApiResponse<unknown>
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(t('dialog.pluginMarketSetting.saveSuccess'))
|
||||
emit('save')
|
||||
} else $toast.error(t('dialog.pluginMarketSetting.saveFailed', { message: result?.message }))
|
||||
await api.post('system/setting/PLUGIN_MARKET', repoStringToSave, { feedback: 'silent' })
|
||||
$toast.success(t('dialog.pluginMarketSetting.saveSuccess'))
|
||||
emit('save')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
$toast.error(t('dialog.pluginMarketSetting.saveFailed', { message: error instanceof Error ? error.message : '' }))
|
||||
@@ -179,29 +174,23 @@ async function saveHandle() {
|
||||
async function syncPluginSources() {
|
||||
try {
|
||||
syncingSources.value = true
|
||||
const result = (await api.post('system/setting/PLUGIN_MARKET/sync-wiki', {})) as unknown as ApiResponse<{
|
||||
const result = await api.post<{
|
||||
added_count?: number
|
||||
repos?: string[]
|
||||
total_count?: number
|
||||
value?: string
|
||||
}>
|
||||
}>('system/setting/PLUGIN_MARKET/sync-wiki', {}, { feedback: 'silent' })
|
||||
|
||||
if (result.success) {
|
||||
const repos = Array.isArray(result.data?.repos)
|
||||
? result.data.repos
|
||||
: parseRepoInput(result.data?.value || '').repos
|
||||
repoList.value = repos
|
||||
syncTextFromList()
|
||||
emit('changed')
|
||||
$toast.success(
|
||||
t('dialog.pluginMarketSetting.syncSuccess', {
|
||||
added: result.data?.added_count ?? 0,
|
||||
total: result.data?.total_count ?? repos.length,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
$toast.error(t('dialog.pluginMarketSetting.syncFailed', { message: result?.message }))
|
||||
}
|
||||
const repos = Array.isArray(result.repos) ? result.repos : parseRepoInput(result.value || '').repos
|
||||
repoList.value = repos
|
||||
syncTextFromList()
|
||||
emit('changed')
|
||||
$toast.success(
|
||||
t('dialog.pluginMarketSetting.syncSuccess', {
|
||||
added: result.added_count ?? 0,
|
||||
total: result.total_count ?? repos.length,
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
$toast.error(t('dialog.pluginMarketSetting.syncFailed', { message: error instanceof Error ? error.message : '' }))
|
||||
|
||||
@@ -47,10 +47,8 @@ async function savaRcloneConfig() {
|
||||
// 重置配置
|
||||
async function handleReset() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/storage/reset/rclone')
|
||||
if (result.success) {
|
||||
handleDone()
|
||||
}
|
||||
await api.get('/storage/reset/rclone')
|
||||
handleDone()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ import CryptoJS from 'crypto-js'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import { getApiBusinessErrorMessage, isApiBusinessFailure } from '@/api/client'
|
||||
import { transferTypeOptions } from '@/api/constants'
|
||||
import {
|
||||
ApiResponse,
|
||||
FileItem,
|
||||
ManualTransferHistoryInfo,
|
||||
ManualTransferPayload,
|
||||
@@ -208,7 +208,7 @@ async function loadStorages() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/public/Storages')
|
||||
|
||||
storages.value = result.data?.value ?? []
|
||||
storages.value = result.value ?? []
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@@ -380,7 +380,7 @@ const directories = ref<TransferDirectoryConf[]>([])
|
||||
async function loadDirectories() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/public/Directories')
|
||||
directories.value = result.data?.value ?? []
|
||||
directories.value = result.value ?? []
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@@ -967,8 +967,16 @@ function createTransferPayload(options: { item?: FileItem; items?: FileItem[]; l
|
||||
async function requestManualTransfer<T = unknown>(
|
||||
payload: ManualTransferPayload,
|
||||
background: boolean = false,
|
||||
): Promise<ApiResponse<T>> {
|
||||
return await api.post<ApiResponse<T>, ApiResponse<T>>(`transfer/manual?background=${background}`, payload)
|
||||
): Promise<T> {
|
||||
return await api.post<T>(`transfer/manual?background=${background}`, payload, { feedback: 'silent' })
|
||||
}
|
||||
|
||||
/** 保留业务失败详情,并在空消息时使用当前操作的本地提示。 */
|
||||
function getManualTransferErrorMessage(error: unknown, fallback: string) {
|
||||
const businessMessage = getApiBusinessErrorMessage(error)
|
||||
if (businessMessage) return businessMessage
|
||||
if (isApiBusinessFailure(error)) return fallback
|
||||
return error instanceof Error && error.message ? error.message : fallback
|
||||
}
|
||||
|
||||
// 查询当前文件或目录是否存在成功整理历史,决定是否展示重新整理语义。
|
||||
@@ -979,14 +987,12 @@ async function loadManualTransferHistory() {
|
||||
try {
|
||||
const payload =
|
||||
normalizedItems.value.length === 1 ? { fileitem: normalizedItems.value[0] } : { fileitems: normalizedItems.value }
|
||||
const result = await api.post<ApiResponse<ManualTransferHistoryInfo>, ApiResponse<ManualTransferHistoryInfo>>(
|
||||
'transfer/manual/history',
|
||||
payload,
|
||||
)
|
||||
if (!result.success) return
|
||||
const result = await api.post<ManualTransferHistoryInfo>('transfer/manual/history', payload, {
|
||||
feedback: 'silent',
|
||||
})
|
||||
|
||||
manualHistoryCount.value = result.data?.history_count ?? 0
|
||||
transferForm.reorganize = Boolean(result.data?.reorganize)
|
||||
manualHistoryCount.value = result.history_count ?? 0
|
||||
transferForm.reorganize = Boolean(result.reorganize)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
@@ -997,8 +1003,8 @@ async function loadManualTransferHistory() {
|
||||
// 加载剧集格式规则配置状态,用于决定是否允许自动推荐。
|
||||
async function loadEpisodeFormatRuleConfiguration() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/public/EpisodeFormatRuleTable')
|
||||
episodeFormatRuleConfigured.value = Boolean(result.data?.value?.length)
|
||||
const result = await api.get<{ value?: unknown[] }>('system/setting/public/EpisodeFormatRuleTable')
|
||||
episodeFormatRuleConfigured.value = Boolean(result.value?.length)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
episodeFormatRuleConfigured.value = undefined
|
||||
@@ -1028,7 +1034,7 @@ async function handleRecommendEpisodeFormat() {
|
||||
|
||||
try {
|
||||
const hasExistingEpisodeFormat = Boolean(transferForm.episode_format?.trim())
|
||||
const result = await api.post<ApiResponse<EpisodeFormatRecommendData>, ApiResponse<EpisodeFormatRecommendData>>(
|
||||
const data = await api.post<EpisodeFormatRecommendData>(
|
||||
'transfer/episode-format/recommend',
|
||||
hasValidSelectedFiles
|
||||
? {
|
||||
@@ -1037,14 +1043,9 @@ async function handleRecommendEpisodeFormat() {
|
||||
: {
|
||||
fileitem: sourceItem,
|
||||
},
|
||||
{ feedback: 'silent' },
|
||||
)
|
||||
|
||||
if (!result.success) {
|
||||
$toast.error(result.message || t('dialog.reorganize.episodeFormatRecommendFailed'))
|
||||
return
|
||||
}
|
||||
|
||||
const data = result.data ?? {}
|
||||
if (!data.episode_format) {
|
||||
$toast.error(t('dialog.reorganize.episodeFormatRecommendFailed'))
|
||||
return
|
||||
@@ -1155,19 +1156,6 @@ function mergePreviewData(target: ManualTransferPreviewData, incoming?: ManualTr
|
||||
}
|
||||
}
|
||||
|
||||
// 从标准响应中提取可展示的整理预览数据,优先保留顶层本地化消息。
|
||||
function resolvePreviewResponseData(result: ApiResponse<ManualTransferPreviewData>) {
|
||||
if (!result.data) return result.data
|
||||
|
||||
const message = result.message_i18n || result.message || result.data.message
|
||||
if (!message || message === result.data.message) return result.data
|
||||
|
||||
return {
|
||||
...result.data,
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
// 预览整理结果
|
||||
async function previewTransfer() {
|
||||
if (!props.logids?.length && !normalizedItems.value.length) return
|
||||
@@ -1186,24 +1174,15 @@ async function previewTransfer() {
|
||||
const result = await requestManualTransfer<ManualTransferPreviewData>(
|
||||
createTransferPayload({ items: normalizedItems.value, preview: true }),
|
||||
)
|
||||
if (!result.success) {
|
||||
mergePreviewData(
|
||||
mergedPreviewData,
|
||||
createFailedPreviewData({
|
||||
source: getBatchItemsLabel(normalizedItems.value),
|
||||
message: result.message || t('dialog.reorganize.previewRequestFailed'),
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
mergePreviewData(mergedPreviewData, resolvePreviewResponseData(result))
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.warn(`预览请求异常: ${err?.message}`)
|
||||
mergePreviewData(mergedPreviewData, result)
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = getManualTransferErrorMessage(err, t('dialog.reorganize.previewRequestFailed'))
|
||||
console.warn(`预览请求异常: ${errorMessage}`)
|
||||
mergePreviewData(
|
||||
mergedPreviewData,
|
||||
createFailedPreviewData({
|
||||
source: getBatchItemsLabel(normalizedItems.value),
|
||||
message: `${getBatchItemsLabel(normalizedItems.value)}: ${err?.message || t('dialog.reorganize.previewRequestFailed')}`,
|
||||
message: errorMessage,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1214,29 +1193,17 @@ async function previewTransfer() {
|
||||
const result = await requestManualTransfer<ManualTransferPreviewData>(
|
||||
createTransferPayload({ item, preview: true }),
|
||||
)
|
||||
if (!result.success) {
|
||||
mergePreviewData(
|
||||
mergedPreviewData,
|
||||
createFailedPreviewData({
|
||||
source: item.path || item.name,
|
||||
type: item.type,
|
||||
title: item.name,
|
||||
message: result.message || t('dialog.reorganize.previewRequestFailed'),
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
mergePreviewData(mergedPreviewData, resolvePreviewResponseData(result))
|
||||
} catch (err: any) {
|
||||
console.warn(`预览请求异常: ${err?.message}`)
|
||||
mergePreviewData(mergedPreviewData, result)
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = getManualTransferErrorMessage(err, t('dialog.reorganize.previewRequestFailed'))
|
||||
console.warn(`预览请求异常: ${errorMessage}`)
|
||||
mergePreviewData(
|
||||
mergedPreviewData,
|
||||
createFailedPreviewData({
|
||||
source: item.path || item.name,
|
||||
type: item.type,
|
||||
title: item.name,
|
||||
message: `${item.name || item.path}: ${err?.message || t('dialog.reorganize.previewRequestFailed')}`,
|
||||
message: errorMessage,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1252,25 +1219,15 @@ async function previewTransfer() {
|
||||
const result = await requestManualTransfer<ManualTransferPreviewData>(
|
||||
createTransferPayload({ logid, preview: true }),
|
||||
)
|
||||
if (!result.success) {
|
||||
mergePreviewData(
|
||||
mergedPreviewData,
|
||||
createFailedPreviewData({
|
||||
source: `历史记录 ${logid}`,
|
||||
message: result.message || t('dialog.reorganize.previewRequestFailed'),
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
mergePreviewData(mergedPreviewData, resolvePreviewResponseData(result))
|
||||
} catch (err: any) {
|
||||
console.warn(`预览请求异常: ${err?.message}`)
|
||||
mergePreviewData(mergedPreviewData, result)
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = getManualTransferErrorMessage(err, t('dialog.reorganize.previewRequestFailed'))
|
||||
console.warn(`预览请求异常: ${errorMessage}`)
|
||||
mergePreviewData(
|
||||
mergedPreviewData,
|
||||
createFailedPreviewData({
|
||||
source: `历史记录 ${logid}`,
|
||||
message: `历史记录 ${logid}: ${err?.message || t('dialog.reorganize.previewRequestFailed')}`,
|
||||
message: errorMessage,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1286,10 +1243,10 @@ async function previewTransfer() {
|
||||
if (previewHasFailures(mergedPreviewData)) {
|
||||
$toast.warning(getPreviewResultSummaryMessage(mergedPreviewData))
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
previewVisible.value = false
|
||||
resetPreviewState()
|
||||
$toast.error(error?.message || t('dialog.reorganize.previewRequestFailed'))
|
||||
$toast.error(getManualTransferErrorMessage(error, t('dialog.reorganize.previewRequestFailed')))
|
||||
} finally {
|
||||
previewLoading.value = false
|
||||
}
|
||||
@@ -1311,16 +1268,12 @@ async function togglePreview() {
|
||||
// 整理文件
|
||||
async function handleTransfer(item: FileItem, background: boolean = false) {
|
||||
try {
|
||||
const result = await requestManualTransfer(createTransferPayload({ item }), background)
|
||||
if (!result.success) {
|
||||
$toast.error(result.message || t('dialog.reorganize.transferRequestFailed'))
|
||||
return false
|
||||
}
|
||||
await requestManualTransfer<null>(createTransferPayload({ item }), background)
|
||||
if (background) $toast.success(t('dialog.reorganize.successMessage', { name: item.name }))
|
||||
return true
|
||||
} catch (error: unknown) {
|
||||
console.log(error)
|
||||
if (error instanceof Error) $toast.error(error.message)
|
||||
$toast.error(getManualTransferErrorMessage(error, t('dialog.reorganize.transferRequestFailed')))
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1328,16 +1281,12 @@ async function handleTransfer(item: FileItem, background: boolean = false) {
|
||||
// 批量整理文件并按后台模式决定是否提示入队成功。
|
||||
async function handleTransferBatch(items: FileItem[], background: boolean = false) {
|
||||
try {
|
||||
const result = await requestManualTransfer(createTransferPayload({ items }), background)
|
||||
if (!result.success) {
|
||||
$toast.error(result.message || t('dialog.reorganize.transferRequestFailed'))
|
||||
return false
|
||||
}
|
||||
await requestManualTransfer<null>(createTransferPayload({ items }), background)
|
||||
if (background) $toast.success(t('dialog.reorganize.successMessage', { name: getBatchItemsLabel(items) }))
|
||||
return true
|
||||
} catch (error: unknown) {
|
||||
console.log(error)
|
||||
if (error instanceof Error) $toast.error(error.message)
|
||||
$toast.error(getManualTransferErrorMessage(error, t('dialog.reorganize.transferRequestFailed')))
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1345,16 +1294,12 @@ async function handleTransferBatch(items: FileItem[], background: boolean = fals
|
||||
// 整理日志
|
||||
async function handleTransferLog(logid: number, background: boolean = false) {
|
||||
try {
|
||||
const result = await requestManualTransfer(createTransferPayload({ logid }), background)
|
||||
if (!result.success) {
|
||||
$toast.error(result.message || t('dialog.reorganize.transferRequestFailed'))
|
||||
return false
|
||||
}
|
||||
await requestManualTransfer<null>(createTransferPayload({ logid }), background)
|
||||
if (background) $toast.success(`历史记录 ${logid} 已加入整理队列!`)
|
||||
return true
|
||||
} catch (error: unknown) {
|
||||
console.log(error)
|
||||
if (error instanceof Error) $toast.error(error.message)
|
||||
$toast.error(getManualTransferErrorMessage(error, t('dialog.reorganize.transferRequestFailed')))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,9 +372,9 @@ async function fetchSubscribes() {
|
||||
/** 从接口加载用户的站点搜索偏好。 */
|
||||
const loadUserSitePreferences = async () => {
|
||||
try {
|
||||
const result = await api.get('system/setting/public/IndexerSites')
|
||||
if (result && result.data && result.data.value) {
|
||||
selectedSites.value = result.data.value
|
||||
const result = await api.get<{ value?: number[] }>('system/setting/public/IndexerSites')
|
||||
if (result.value) {
|
||||
selectedSites.value = result.value
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { DownloaderConf, Site } from '@/api/types'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import { numberValidator, requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import { getApiBusinessErrorMessage } from '@/api/client'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -95,14 +96,12 @@ async function addSite() {
|
||||
if (!siteForm.value?.url) return
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: string } = await api.post('site/', siteForm.value)
|
||||
if (result.success) {
|
||||
$toast.success(t('site.messages.addSuccess'))
|
||||
emit('save')
|
||||
} else {
|
||||
$toast.error(`${t('site.messages.addFailed')}:${result.message}`)
|
||||
}
|
||||
await api.post('site/', siteForm.value, { feedback: 'silent' })
|
||||
$toast.success(t('site.messages.addSuccess'))
|
||||
emit('save')
|
||||
} catch (error) {
|
||||
const message = getApiBusinessErrorMessage(error)
|
||||
$toast.error(message ? `${t('site.messages.addFailed')}:${message}` : t('site.messages.addFailed'))
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
@@ -121,15 +120,16 @@ async function updateSiteInfo() {
|
||||
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} ${t('site.messages.updateSuccess')}`)
|
||||
emit('save')
|
||||
} else {
|
||||
$toast.error(`${siteForm.value?.name} ${t('site.messages.updateFailed')}:${result.message}`)
|
||||
}
|
||||
await api.put('site/', siteForm.value, { feedback: 'silent' })
|
||||
$toast.success(`${siteForm.value?.name} ${t('site.messages.updateSuccess')}`)
|
||||
emit('save')
|
||||
} catch (error) {
|
||||
$toast.error(`${siteForm.value?.name} ${t('site.messages.updateFailed')}!`)
|
||||
const message = getApiBusinessErrorMessage(error)
|
||||
$toast.error(
|
||||
message
|
||||
? `${siteForm.value?.name} ${t('site.messages.updateFailed')}:${message}`
|
||||
: `${siteForm.value?.name} ${t('site.messages.updateFailed')}!`,
|
||||
)
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
|
||||
@@ -50,29 +50,21 @@ async function updateSiteCookie() {
|
||||
progressDialog.value = true
|
||||
progressText.value = t('dialog.siteCookieUpdate.updating', { site: cardProps.site?.name })
|
||||
|
||||
const result: { [key: string]: any } = await api.post(`site/cookie/${cardProps.site?.id}`, {
|
||||
username: userPwForm.value.username,
|
||||
password: userPwForm.value.password,
|
||||
code: userPwForm.value.code,
|
||||
})
|
||||
await api.post(
|
||||
`site/cookie/${cardProps.site?.id}`,
|
||||
{
|
||||
username: userPwForm.value.username,
|
||||
password: userPwForm.value.password,
|
||||
code: userPwForm.value.code,
|
||||
},
|
||||
{ feedback: 'silent' },
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(t('dialog.siteCookieUpdate.success', { site: cardProps.site?.name }))
|
||||
emit('done')
|
||||
} else {
|
||||
$toast.error(
|
||||
t('dialog.siteCookieUpdate.failed', {
|
||||
site: cardProps.site?.name,
|
||||
message: result.message || t('dialog.siteCookieUpdate.requestFailed'),
|
||||
}),
|
||||
)
|
||||
}
|
||||
$toast.success(t('dialog.siteCookieUpdate.success', { site: cardProps.site?.name }))
|
||||
emit('done')
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
const message =
|
||||
error?.response?.data?.message ||
|
||||
(typeof error?.response?.data?.detail === 'string' ? error.response.data.detail : error?.message) ||
|
||||
t('dialog.siteCookieUpdate.requestFailed')
|
||||
const message = error?.message || t('dialog.siteCookieUpdate.requestFailed')
|
||||
$toast.error(t('dialog.siteCookieUpdate.failed', { site: cardProps.site?.name, message }))
|
||||
} finally {
|
||||
progressDialog.value = false
|
||||
|
||||
@@ -139,19 +139,10 @@ async function importSites() {
|
||||
try {
|
||||
// 移除id字段,避免冲突
|
||||
const { id, ...siteData } = site
|
||||
const result: { success: boolean; message?: string } = await api.post('site/', siteData)
|
||||
if (result.success) {
|
||||
// 记录成功的站点
|
||||
successCount++
|
||||
importSuccesses.value.push(site)
|
||||
} else {
|
||||
failCount++
|
||||
// 记录失败信息
|
||||
importErrors.value.push({
|
||||
site,
|
||||
error: result.message || t('site.messages.importFailed'),
|
||||
})
|
||||
}
|
||||
await api.post<null>('site/', siteData, { feedback: 'silent' })
|
||||
// 记录成功的站点
|
||||
successCount++
|
||||
importSuccesses.value.push(site)
|
||||
} catch (error) {
|
||||
console.error(`Import site ${site.name} failed:`, error)
|
||||
failCount++
|
||||
|
||||
@@ -38,8 +38,7 @@ const detailDialog = ref(false)
|
||||
async function fetchSiteStats() {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await api.get('site/statistic')
|
||||
siteStats.value = Array.isArray(response) ? response : response.data || []
|
||||
siteStats.value = await api.get<SiteStatistic[]>('site/statistic')
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch site statistics:', error)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ApiResponse, Site, SiteUserData } from '@/api/types'
|
||||
import type { Site, SiteUserData } from '@/api/types'
|
||||
import api from '@/api'
|
||||
import { useDisplay, useTheme } from 'vuetify'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
@@ -335,18 +335,14 @@ async function fetchSiteUserData(failureOperation: 'load' | 'refresh' = 'load',
|
||||
const activeGeneration = generation ?? ++operationGeneration
|
||||
|
||||
try {
|
||||
const result = await api.get<ApiResponse<SiteUserData[]>, ApiResponse<SiteUserData[]>>(
|
||||
`site/userdata/${props.site?.id}`,
|
||||
)
|
||||
const result = await api.get<SiteUserData[]>(`site/userdata/${props.site?.id}`, { feedback: 'silent' })
|
||||
if (activeGeneration !== operationGeneration) return false
|
||||
|
||||
if (result.success) {
|
||||
// 使用nextTick确保DOM更新完成后再更新图表数据
|
||||
await nextTick()
|
||||
if (activeGeneration !== operationGeneration) return false
|
||||
// 使用nextTick确保DOM更新完成后再更新图表数据
|
||||
await nextTick()
|
||||
if (activeGeneration !== operationGeneration) return false
|
||||
|
||||
siteDatas.value = result.data.sort((a, b) => (a.updated_day || '').localeCompare(b.updated_day || ''))
|
||||
}
|
||||
siteDatas.value = result.sort((a, b) => (a.updated_day || '').localeCompare(b.updated_day || ''))
|
||||
|
||||
failedOperation.value = undefined
|
||||
return true
|
||||
@@ -365,14 +361,10 @@ async function refreshSiteData() {
|
||||
const generation = ++operationGeneration
|
||||
progressDialog.value = true
|
||||
try {
|
||||
const result = await api.post<ApiResponse<unknown>, ApiResponse<unknown>>(`site/userdata/${props.site?.id}`)
|
||||
await api.post<null>(`site/userdata/${props.site?.id}`, undefined, { feedback: 'silent' })
|
||||
if (generation !== operationGeneration) return
|
||||
|
||||
if (result.success) {
|
||||
await fetchSiteUserData('refresh', generation)
|
||||
} else {
|
||||
failedOperation.value = 'refresh'
|
||||
}
|
||||
await fetchSiteUserData('refresh', generation)
|
||||
} catch (error) {
|
||||
if (generation !== operationGeneration) return
|
||||
|
||||
|
||||
@@ -29,11 +29,9 @@ async function handleDone() {
|
||||
// 重置配置
|
||||
async function handleReset() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/storage/reset/smb')
|
||||
if (result.success) {
|
||||
// 重置成功
|
||||
handleDone()
|
||||
}
|
||||
await api.get('/storage/reset/smb')
|
||||
// 重置成功
|
||||
handleDone()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ async function queryFilterRuleGroups() {
|
||||
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/UserFilterRuleGroups')
|
||||
filterRuleGroups.value = result.data?.value ?? []
|
||||
filterRuleGroups.value = result.value ?? []
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@@ -193,20 +193,10 @@ const filterRuleGroupOptions = computed(() => {
|
||||
async function updateSubscribeInfo() {
|
||||
const displayName = getSubscribeDisplayName()
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.put('subscribe/', subscribeForm.value)
|
||||
// 提示
|
||||
if (result.success) {
|
||||
$toast.success(t('dialog.subscribeEdit.updateSuccess', { name: displayName }))
|
||||
// 通知父组件刷新
|
||||
emit('save', subscribeForm.value)
|
||||
} else {
|
||||
$toast.error(
|
||||
t('dialog.subscribeEdit.updateFailed', {
|
||||
name: displayName,
|
||||
message: result.message ?? t('subscribe.requestFailed'),
|
||||
}),
|
||||
)
|
||||
}
|
||||
await api.put<null>('subscribe/', subscribeForm.value, { feedback: 'silent' })
|
||||
$toast.success(t('dialog.subscribeEdit.updateSuccess', { name: displayName }))
|
||||
// 通知父组件刷新
|
||||
emit('save', subscribeForm.value)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(
|
||||
@@ -229,19 +219,10 @@ async function saveDefaultSubscribeConfig() {
|
||||
else if (props.type === '电视剧') subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
|
||||
else subscribe_config_url = 'system/setting/DefaultMusicSubscribeConfig'
|
||||
|
||||
const result: { [key: string]: any } = await api.post(subscribe_config_url, subscribeForm.value)
|
||||
if (result.success) {
|
||||
$toast.success(t('dialog.subscribeEdit.defaultSaveSuccess', { type: typeName }))
|
||||
// 通知父组件刷新
|
||||
emit('save', subscribeForm.value)
|
||||
} else {
|
||||
$toast.error(
|
||||
t('dialog.subscribeEdit.defaultSaveFailed', {
|
||||
type: typeName,
|
||||
message: result.message ?? t('subscribe.requestFailed'),
|
||||
}),
|
||||
)
|
||||
}
|
||||
await api.post<null>(subscribe_config_url, subscribeForm.value, { feedback: 'silent' })
|
||||
$toast.success(t('dialog.subscribeEdit.defaultSaveSuccess', { type: typeName }))
|
||||
// 通知父组件刷新
|
||||
emit('save', subscribeForm.value)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
$toast.error(
|
||||
@@ -261,9 +242,9 @@ async function queryDefaultSubscribeConfig() {
|
||||
else if (props.type === '电视剧') subscribe_config_url = 'system/setting/public/DefaultTvSubscribeConfig'
|
||||
else subscribe_config_url = 'system/setting/public/DefaultMusicSubscribeConfig'
|
||||
|
||||
const result: { [key: string]: any } = await api.get(subscribe_config_url)
|
||||
const result = await api.get<{ value?: Record<string, unknown> }>(subscribe_config_url)
|
||||
|
||||
if (result.data.value) subscribeForm.value = result.data?.value ?? ''
|
||||
if (result.value) Object.assign(subscribeForm.value, result.value)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@@ -321,19 +302,10 @@ async function removeSubscribe() {
|
||||
if (!isConfirmed) return
|
||||
const displayName = getSubscribeDisplayName()
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(`subscribe/${props.subid}`)
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(`${displayName} ${t('subscribe.cancelSuccess')}`)
|
||||
// 通知父组件刷新
|
||||
emit('remove')
|
||||
} else {
|
||||
$toast.error(
|
||||
`${displayName} ${t('subscribe.cancelFailed', {
|
||||
message: result.message ?? t('subscribe.requestFailed'),
|
||||
})}`,
|
||||
)
|
||||
}
|
||||
await api.delete<null>(`subscribe/${props.subid}`, { feedback: 'silent' })
|
||||
$toast.success(`${displayName} ${t('subscribe.cancelSuccess')}`)
|
||||
// 通知父组件刷新
|
||||
emit('remove')
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(
|
||||
@@ -347,10 +319,8 @@ async function removeSubscribe() {
|
||||
// 查询下载目录
|
||||
async function loadDownloadDirectories() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/public/Directories')
|
||||
if (result.success && result.data?.value) {
|
||||
downloadDirectories.value = result.data.value
|
||||
}
|
||||
const result = await api.get<{ value?: TransferDirectoryConf[] }>('system/setting/public/Directories')
|
||||
downloadDirectories.value = result.value ?? []
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
@@ -99,12 +99,8 @@ async function reSubscribe(item: Subscribe) {
|
||||
}
|
||||
progressDialog.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('subscribe/', item)
|
||||
if (result.success) {
|
||||
emit('save')
|
||||
} else {
|
||||
$toast.error(t('subscribe.requestFailed'))
|
||||
}
|
||||
await api.post('subscribe/', item, { feedback: 'silent' })
|
||||
emit('save')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
$toast.error(t('subscribe.requestFailed'))
|
||||
@@ -115,10 +111,8 @@ async function reSubscribe(item: Subscribe) {
|
||||
// 删除记录
|
||||
async function deleteHistory(item: Subscribe) {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(`subscribe/history/${item.id}`)
|
||||
if (result.success) {
|
||||
historyList.value = historyList.value.filter(i => i.id !== item.id)
|
||||
}
|
||||
await api.delete(`subscribe/history/${item.id}`, { feedback: 'silent' })
|
||||
historyList.value = historyList.value.filter(i => i.id !== item.id)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
$toast.error(t('subscribe.requestFailed'))
|
||||
@@ -176,9 +170,9 @@ function getMediaTypeText(type: string | undefined) {
|
||||
</div>
|
||||
</template>
|
||||
<template #empty />
|
||||
<VVirtualScroll v-if="historyList.length > 0" renderless :items="historyList" :item-height="104">
|
||||
<template #default="{ item, itemRef }">
|
||||
<div :ref="itemRef">
|
||||
<VVirtualScroll v-if="historyList.length > 0" :renderless="true" :items="historyList" :item-height="104">
|
||||
<template #default="{ item, ...slotProps }">
|
||||
<div :ref="'itemRef' in slotProps ? slotProps.itemRef : undefined">
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VImg
|
||||
|
||||
@@ -37,21 +37,16 @@ async function doShare() {
|
||||
if (!shareForm.value.share_title || !shareForm.value.share_comment || !shareForm.value.share_user) return
|
||||
try {
|
||||
shareDoing.value = true
|
||||
const result: { [key: string]: any } = await api.post('subscribe/share', shareForm.value)
|
||||
// 提示
|
||||
if (result.success) {
|
||||
$toast.success(t('dialog.subscribeShare.shareSuccess', { name: props.sub?.name }))
|
||||
// 通知父组件刷新
|
||||
emit('close')
|
||||
} else {
|
||||
$toast.error(t('dialog.subscribeShare.shareFailed', { name: props.sub?.name, message: result.message }))
|
||||
}
|
||||
await api.post('subscribe/share', shareForm.value, { feedback: 'silent' })
|
||||
$toast.success(t('dialog.subscribeShare.shareSuccess', { name: props.sub?.name }))
|
||||
// 通知父组件刷新
|
||||
emit('close')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
$toast.error(
|
||||
t('dialog.subscribeShare.shareFailed', {
|
||||
name: props.sub?.name,
|
||||
message: t('subscribe.requestFailed'),
|
||||
message: e instanceof Error ? e.message : t('subscribe.requestFailed'),
|
||||
}),
|
||||
)
|
||||
} finally {
|
||||
|
||||
@@ -64,37 +64,24 @@ function handleDone() {
|
||||
// 重置配置
|
||||
async function handleReset() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/storage/reset/u115')
|
||||
if (result.success) {
|
||||
setMessage('success', t('dialog.u115Auth.authSuccess'))
|
||||
handleDone()
|
||||
}
|
||||
else {
|
||||
setMessage('error', result.message || t('dialog.u115Auth.authFailed'))
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
await api.get<null>('/storage/reset/u115', { feedback: 'silent' })
|
||||
setMessage('success', t('dialog.u115Auth.authSuccess'))
|
||||
handleDone()
|
||||
} catch (error) {
|
||||
console.error('Reset failed:', error)
|
||||
setMessage('error', t('dialog.u115Auth.authFailed'))
|
||||
setMessage('error', error instanceof Error ? error.message : t('dialog.u115Auth.authFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取授权URL
|
||||
async function fetchAuthUrl() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/storage/auth_url/u115')
|
||||
|
||||
if (result.success && result.data) {
|
||||
authUrl.value = result.data.authUrl
|
||||
authState.value = result.data.state
|
||||
}
|
||||
else {
|
||||
setMessage('error', result.message || t('dialog.u115Auth.urlFetchFailed'))
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
const result = await api.get<{ authUrl: string; state: string }>('/storage/auth_url/u115', { feedback: 'silent' })
|
||||
authUrl.value = result.authUrl
|
||||
authState.value = result.state
|
||||
} catch (error) {
|
||||
console.error('Fetch auth URL failed:', error)
|
||||
setMessage('error', t('dialog.u115Auth.urlFetchFailed'))
|
||||
setMessage('error', error instanceof Error ? error.message : t('dialog.u115Auth.urlFetchFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,8 +112,7 @@ function openAuthWindow() {
|
||||
if (authWindow) {
|
||||
setMessage('info', t('dialog.u115Auth.authorizing'))
|
||||
pollTimer = setTimeout(checkAuthStatus, POLL_INTERVAL)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
setMessage('error', t('dialog.u115Auth.popupBlocked'))
|
||||
}
|
||||
}
|
||||
@@ -134,29 +120,25 @@ function openAuthWindow() {
|
||||
// 检查授权状态
|
||||
async function checkAuthStatus() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/storage/check/u115')
|
||||
const result = await api.get<{ status: number; tip?: string }>('/storage/check/u115', { feedback: 'silent' })
|
||||
const { status, tip } = result
|
||||
|
||||
if (result.success && result.data) {
|
||||
const { status, tip } = result.data
|
||||
|
||||
if (status === AUTH_STATUS_SUCCESS) {
|
||||
// 授权成功
|
||||
setMessage('success', t('dialog.u115Auth.authSuccess'))
|
||||
handleDone()
|
||||
return
|
||||
}
|
||||
|
||||
if (status === AUTH_STATUS_FAILED) {
|
||||
// 授权失败或过期
|
||||
setMessage('error', tip || t('dialog.u115Auth.authFailed'))
|
||||
cleanup()
|
||||
return
|
||||
}
|
||||
|
||||
// status === 0 或 1,继续等待
|
||||
if (status === AUTH_STATUS_SUCCESS) {
|
||||
// 授权成功
|
||||
setMessage('success', t('dialog.u115Auth.authSuccess'))
|
||||
handleDone()
|
||||
return
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
|
||||
if (status === AUTH_STATUS_FAILED) {
|
||||
// 授权失败或过期
|
||||
setMessage('error', tip || t('dialog.u115Auth.authFailed'))
|
||||
cleanup()
|
||||
return
|
||||
}
|
||||
|
||||
// status === 0 或 1,继续等待
|
||||
} catch (error) {
|
||||
console.error('Check auth status failed:', error)
|
||||
}
|
||||
|
||||
@@ -214,36 +196,20 @@ onUnmounted(() => {
|
||||
|
||||
<!-- 状态提示 -->
|
||||
<div v-if="text" class="w-full">
|
||||
<VAlert
|
||||
variant="tonal"
|
||||
:type="alertType"
|
||||
:text="text"
|
||||
class="my-4 text-center"
|
||||
>
|
||||
<VAlert variant="tonal" :type="alertType" :text="text" class="my-4 text-center">
|
||||
<template #prepend />
|
||||
</VAlert>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions class="app-dialog-actions">
|
||||
<VBtn
|
||||
color="error"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-restore"
|
||||
@click="handleReset"
|
||||
>
|
||||
<VBtn color="error" variant="tonal" prepend-icon="mdi-restore" @click="handleReset">
|
||||
{{ t('dialog.u115Auth.reset') }}
|
||||
</VBtn>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
prepend-icon="mdi-check"
|
||||
class="px-5"
|
||||
@click="handleDone"
|
||||
>
|
||||
<VBtn color="primary" variant="flat" prepend-icon="mdi-check" class="px-5" @click="handleDone">
|
||||
{{ t('dialog.u115Auth.complete') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useToast } from 'vue-toastification'
|
||||
import type { User } from '@/api/types'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import api from '@/api'
|
||||
import { getApiBusinessErrorMessage } from '@/api/client'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import { useUserStore } from '@/stores'
|
||||
@@ -322,17 +323,15 @@ async function addUser() {
|
||||
isAdding.value = true
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: string } = await api.post('user/', userForm.value)
|
||||
if (result.success) {
|
||||
$toast.success(t('dialog.userAddEdit.userCreated', { name: userForm.value.name }))
|
||||
emit('save')
|
||||
} else {
|
||||
$toast.error(t('dialog.userAddEdit.userCreateFailed', { message: result.message }))
|
||||
// 清除用户名
|
||||
userForm.value.name = ''
|
||||
}
|
||||
await api.post<null>('user/', userForm.value, { feedback: 'silent' })
|
||||
$toast.success(t('dialog.userAddEdit.userCreated', { name: userForm.value.name }))
|
||||
emit('save')
|
||||
} catch (error) {
|
||||
$toast.error(t('dialog.userAddEdit.userCreateFailed', { message: t('common.serverConnectionFailed') }))
|
||||
$toast.error(
|
||||
t('dialog.userAddEdit.userCreateFailed', {
|
||||
message: getApiBusinessErrorMessage(error) || t('common.serverConnectionFailed'),
|
||||
}),
|
||||
)
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
@@ -375,37 +374,28 @@ async function updateUser() {
|
||||
// 确保权限数据正确传递
|
||||
userData.permissions = userPermissions.value
|
||||
|
||||
const result: Record<string, unknown> = await api.put('user/', userData)
|
||||
await api.put<null>('user/', userData, { feedback: 'silent' })
|
||||
|
||||
if (result.success) {
|
||||
if (oldUserName !== currentUserName.value) {
|
||||
$toast.success(t('dialog.userAddEdit.userUpdateSuccess', { name: `${oldUserName} → ${currentUserName.value}` }))
|
||||
// 如果是当前登录用户,更新当前用户名称显示
|
||||
if (isCurrentUser.value) {
|
||||
userStore.setUserName(currentUserName.value)
|
||||
}
|
||||
} else {
|
||||
$toast.success(t('dialog.userAddEdit.userUpdateSuccess', { name: userForm.value?.name }))
|
||||
}
|
||||
// 更新本地头像显示
|
||||
if (oldAvatar !== currentAvatar.value && isCurrentUser.value) {
|
||||
userStore.setAvatar(currentAvatar.value)
|
||||
}
|
||||
// 如果是当前登录用户,更新权限信息
|
||||
if (oldUserName !== currentUserName.value) {
|
||||
$toast.success(t('dialog.userAddEdit.userUpdateSuccess', { name: `${oldUserName} → ${currentUserName.value}` }))
|
||||
// 如果是当前登录用户,更新当前用户名称显示
|
||||
if (isCurrentUser.value) {
|
||||
userStore.setPermissions(userPermissions.value)
|
||||
userStore.setUserName(currentUserName.value)
|
||||
}
|
||||
emit('save')
|
||||
} else {
|
||||
if (oldUserName !== currentUserName.value) {
|
||||
$toast.error(t('dialog.userAddEdit.userUpdateFailed', { message: result.message }))
|
||||
currentUserName.value = oldUserName
|
||||
} else {
|
||||
$toast.error(t('dialog.userAddEdit.userUpdateFailed', { message: result.message }))
|
||||
}
|
||||
$toast.success(t('dialog.userAddEdit.userUpdateSuccess', { name: userForm.value?.name }))
|
||||
}
|
||||
// 更新本地头像显示
|
||||
if (oldAvatar !== currentAvatar.value && isCurrentUser.value) {
|
||||
userStore.setAvatar(currentAvatar.value)
|
||||
}
|
||||
// 如果是当前登录用户,更新权限信息
|
||||
if (isCurrentUser.value) {
|
||||
userStore.setPermissions(userPermissions.value)
|
||||
}
|
||||
emit('save')
|
||||
} catch (error) {
|
||||
$toast.error(t('dialog.userAddEdit.userUpdateFailed', { message: '' }))
|
||||
$toast.error(t('dialog.userAddEdit.userUpdateFailed', { message: error instanceof Error ? error.message : '' }))
|
||||
console.error('更新失败:', error)
|
||||
} finally {
|
||||
// 表单中的已保存值用于恢复操作,待提交值只保留在对应的编辑状态中。
|
||||
|
||||
@@ -64,12 +64,10 @@ const formFields = computed(() => {
|
||||
// 查询之前使用的认证参数
|
||||
async function loadLastAuthParams() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(`system/setting/UserSiteAuthParams`)
|
||||
if (result.success) {
|
||||
const ret = result.data?.value
|
||||
if (ret && !isNullOrEmptyObject(ret.params)) {
|
||||
authForm.value = ret
|
||||
}
|
||||
const result = await api.get<{ value?: typeof authForm.value }>(`system/setting/UserSiteAuthParams`)
|
||||
const ret = result.value
|
||||
if (ret && !isNullOrEmptyObject(ret.params)) {
|
||||
authForm.value = ret
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
@@ -110,18 +108,15 @@ async function checkUser() {
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(`site/auth`, authForm.value)
|
||||
if (result.success) {
|
||||
$toast.success(t('dialog.userAuth.authSuccess'))
|
||||
// 1秒后刷新页面
|
||||
setTimeout(() => {
|
||||
emit('done')
|
||||
}, 1000)
|
||||
} else {
|
||||
$toast.error(t('dialog.userAuth.authFailed', { message: result.message }))
|
||||
}
|
||||
await api.post<null>(`site/auth`, authForm.value, { feedback: 'silent' })
|
||||
$toast.success(t('dialog.userAuth.authSuccess'))
|
||||
// 1秒后刷新页面
|
||||
setTimeout(() => {
|
||||
emit('done')
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
$toast.error(t('dialog.userAuth.authFailed', { message: e instanceof Error ? e.message : '' }))
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -48,10 +48,13 @@ const actionDefinitions = ref<any[]>([])
|
||||
|
||||
// 动作类型到契约的映射
|
||||
const actionContractMap = computed(() => {
|
||||
return actionDefinitions.value.reduce((result, action) => {
|
||||
result[action.type] = action.contract || {}
|
||||
return result
|
||||
}, {} as Record<string, any>)
|
||||
return actionDefinitions.value.reduce(
|
||||
(result, action) => {
|
||||
result[action.type] = action.contract || {}
|
||||
return result
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
)
|
||||
})
|
||||
|
||||
// 获取指定节点端口的类型(输入/输出)
|
||||
@@ -159,14 +162,10 @@ const getNodeName = (nodeId?: string) => {
|
||||
|
||||
// 获取流程边源节点可用于条件判断的输出字段
|
||||
const getEdgeConditionFields = (edge: any) => {
|
||||
const sourceNode = edge
|
||||
? nodes.value.find(node => node.id === edge.source)
|
||||
: null
|
||||
const sourceNode = edge ? nodes.value.find(node => node.id === edge.source) : null
|
||||
const contract = sourceNode ? actionContractMap.value[sourceNode.type] || {} : {}
|
||||
const fields = contract.condition_fields || contract.outputs || []
|
||||
return Array.isArray(fields)
|
||||
? fields.filter((field: any) => field?.name || field)
|
||||
: []
|
||||
return Array.isArray(fields) ? fields.filter((field: any) => field?.name || field) : []
|
||||
}
|
||||
|
||||
// 判断流程边是否存在可编辑条件
|
||||
@@ -232,15 +231,13 @@ const selectedEdge = computed(() => {
|
||||
})
|
||||
|
||||
// 当前边可用于条件判断的输出字段
|
||||
const selectedEdgeConditionFields = computed(() => (
|
||||
selectedEdge.value ? getEdgeConditionFields(selectedEdge.value) : []
|
||||
))
|
||||
const selectedEdgeConditionFields = computed(() =>
|
||||
selectedEdge.value ? getEdgeConditionFields(selectedEdge.value) : [],
|
||||
)
|
||||
|
||||
// 当前边的条件下拉选项,按源节点固定输出自动生成
|
||||
const edgeConditionOptions = computed(() => {
|
||||
const sourceNode = selectedEdge.value
|
||||
? nodes.value.find(node => node.id === selectedEdge.value?.source)
|
||||
: null
|
||||
const sourceNode = selectedEdge.value ? nodes.value.find(node => node.id === selectedEdge.value?.source) : null
|
||||
const options = [{ title: t('dialog.workflowActions.conditionAlways'), value: '' }]
|
||||
selectedEdgeConditionFields.value.forEach((field: any) => {
|
||||
const fieldName = field.name || field
|
||||
@@ -376,13 +373,9 @@ async function updateWorkflow() {
|
||||
workflowForm.value.flows = edges.value
|
||||
|
||||
try {
|
||||
const result: { [key: string]: string } = await api.put(`workflow/${workflowForm.value.id}`, workflowForm.value)
|
||||
if (result.success) {
|
||||
$toast.success(t('dialog.workflowActions.saveSuccess'))
|
||||
emit('save')
|
||||
} else {
|
||||
$toast.error(t('dialog.workflowActions.saveFailed', { message: result.message }))
|
||||
}
|
||||
await api.put<null>(`workflow/${workflowForm.value.id}`, workflowForm.value)
|
||||
$toast.success(t('dialog.workflowActions.saveSuccess'))
|
||||
emit('save')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
@@ -720,6 +713,5 @@ const isMacOS = computed(() => {
|
||||
inset-inline: 16px;
|
||||
max-block-size: min(72vh, calc(100% - 112px));
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -166,13 +166,9 @@ async function addWorkflow() {
|
||||
normalizeWorkflowExecutionConfig()
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: string } = await api.post('workflow/', workflowForm.value)
|
||||
if (result.success) {
|
||||
$toast.success(t('dialog.workflowAddEdit.addSuccess'))
|
||||
emit('save')
|
||||
} else {
|
||||
$toast.error(t('dialog.workflowAddEdit.addFailed', { message: result.message }))
|
||||
}
|
||||
await api.post<null>('workflow/', workflowForm.value)
|
||||
$toast.success(t('dialog.workflowAddEdit.addSuccess'))
|
||||
emit('save')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
@@ -205,13 +201,9 @@ async function editWorkflow() {
|
||||
normalizeWorkflowExecutionConfig()
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: string } = await api.put(`workflow/${workflowForm.value.id}`, workflowForm.value)
|
||||
if (result.success) {
|
||||
$toast.success(t('dialog.workflowAddEdit.editSuccess'))
|
||||
emit('save')
|
||||
} else {
|
||||
$toast.error(t('dialog.workflowAddEdit.editFailed', { message: result.message }))
|
||||
}
|
||||
await api.put<null>(`workflow/${workflowForm.value.id}`, workflowForm.value)
|
||||
$toast.success(t('dialog.workflowAddEdit.editSuccess'))
|
||||
emit('save')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
@@ -48,18 +48,20 @@ async function doShare() {
|
||||
if (!shareForm.value.share_title || !shareForm.value.share_comment || !shareForm.value.share_user) return
|
||||
try {
|
||||
shareDoing.value = true
|
||||
const result: { [key: string]: any } = await api.post('workflow/share', shareForm.value)
|
||||
shareDoing.value = false
|
||||
// 提示
|
||||
if (result.success) {
|
||||
$toast.success(t('dialog.workflowShare.shareSuccess', { name: props.workflow?.name }))
|
||||
// 通知父组件刷新
|
||||
emit('close')
|
||||
} else {
|
||||
$toast.error(t('dialog.workflowShare.shareFailed', { name: props.workflow?.name, message: result.message }))
|
||||
}
|
||||
await api.post('workflow/share', shareForm.value, { feedback: 'silent' })
|
||||
$toast.success(t('dialog.workflowShare.shareSuccess', { name: props.workflow?.name }))
|
||||
// 通知父组件刷新
|
||||
emit('close')
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(
|
||||
t('dialog.workflowShare.shareFailed', {
|
||||
name: props.workflow?.name,
|
||||
message: e instanceof Error ? e.message : '',
|
||||
}),
|
||||
)
|
||||
} finally {
|
||||
shareDoing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,28 +30,24 @@ describe('AboutDialog version statistics', () => {
|
||||
mocks.apiGet.mockImplementation((path: string) => {
|
||||
if (path === 'system/env') {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
USAGE_STATISTIC_SHARE: true,
|
||||
VERSION: 'v2.0.0',
|
||||
FRONTEND_VERSION: 'v2.0.0',
|
||||
},
|
||||
USAGE_STATISTIC_SHARE: true,
|
||||
VERSION: 'v2.0.0',
|
||||
FRONTEND_VERSION: 'v2.0.0',
|
||||
})
|
||||
}
|
||||
if (path === 'dashboard/processes') return Promise.resolve([])
|
||||
if (path === 'system/versions') return Promise.resolve({ data: [] })
|
||||
if (path === 'system/versions') return Promise.resolve([])
|
||||
if (path === 'site/supporting') return Promise.resolve({})
|
||||
if (path === 'system/usage/statistic') {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
backend_versions: [
|
||||
{ version: 'backend-hidden', count: 9 },
|
||||
{ version: 'backend-visible', count: 10 },
|
||||
],
|
||||
frontend_versions: [
|
||||
{ version: 'frontend-hidden', count: 0 },
|
||||
{ version: 'frontend-visible', count: 11 },
|
||||
],
|
||||
},
|
||||
backend_versions: [
|
||||
{ version: 'backend-hidden', count: 9 },
|
||||
{ version: 'backend-visible', count: 10 },
|
||||
],
|
||||
frontend_versions: [
|
||||
{ version: 'frontend-hidden', count: 0 },
|
||||
{ version: 'frontend-visible', count: 11 },
|
||||
],
|
||||
})
|
||||
}
|
||||
return Promise.reject(new Error(`Unexpected API path: ${path}`))
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
|
||||
import CategoryEditDialog from '@/components/dialog/CategoryEditDialog.vue'
|
||||
import { screen, waitFor } from '@testing-library/vue'
|
||||
import { renderWithProviders } from '@tests/support/render'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
apiGet: vi.fn(),
|
||||
toastError: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: {
|
||||
get: mocks.apiGet,
|
||||
post: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('vue-toastification', () => ({
|
||||
useToast: () => ({ error: mocks.toastError, success: vi.fn() }),
|
||||
}))
|
||||
|
||||
vi.mock('vuedraggable', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
return {
|
||||
default: defineComponent({
|
||||
props: { modelValue: { type: Array, default: () => [] } },
|
||||
setup(props, { slots }) {
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
props.modelValue.map((element, index) => slots.item?.({ element, index })),
|
||||
)
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
describe('CategoryEditDialog data client contract', () => {
|
||||
beforeEach(() => {
|
||||
mocks.apiGet.mockReset()
|
||||
mocks.toastError.mockReset()
|
||||
})
|
||||
|
||||
it('parses the unwrapped category configuration returned by the default API client', async () => {
|
||||
mocks.apiGet.mockResolvedValue({
|
||||
movie: {
|
||||
'电影直返分类': { genre_ids: '28', original_language: 'zh', production_countries: 'CN' },
|
||||
},
|
||||
tv: {},
|
||||
})
|
||||
|
||||
await renderWithProviders(CategoryEditDialog, {
|
||||
global: { components: { VDialogCloseBtn: DialogCloseBtn } },
|
||||
props: { modelValue: true },
|
||||
})
|
||||
|
||||
await waitFor(() => expect(mocks.apiGet).toHaveBeenCalledWith('media/category/config', { feedback: 'silent' }))
|
||||
expect(await screen.findByDisplayValue('电影直返分类')).toBeInTheDocument()
|
||||
expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -11,7 +11,9 @@ const mocks = vi.hoisted(() => ({
|
||||
toastSuccess: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({ default: { post: (...args: unknown[]) => mocks.apiPost(...args) } }))
|
||||
vi.mock('@/api', () => ({
|
||||
default: createDataApiMock({ post: (...args: unknown[]) => mocks.apiPost(...args) }),
|
||||
}))
|
||||
vi.mock('qrcode', () => ({ default: { toDataURL: (...args: unknown[]) => mocks.qrCode(...args) } }))
|
||||
vi.mock('vue-toastification', () => ({
|
||||
useToast: () => ({ error: mocks.toastError, success: mocks.toastSuccess }),
|
||||
|
||||
@@ -15,10 +15,10 @@ const mocks = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: {
|
||||
default: createDataApiMock({
|
||||
get: (...args: unknown[]) => mocks.apiGet(...args),
|
||||
post: (...args: unknown[]) => mocks.apiPost(...args),
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('vue-toastification', () => ({
|
||||
|
||||
@@ -5,20 +5,24 @@ import { fireEvent, screen, waitFor } from '@testing-library/vue'
|
||||
import { defineComponent, h, inject, type Component, type PropType } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
apiGet: vi.fn(),
|
||||
apiPut: vi.fn(),
|
||||
ensureSidebarNav: vi.fn(),
|
||||
loadRemoteComponent: vi.fn(),
|
||||
nativeSubscribe: vi.fn(),
|
||||
toast: { error: vi.fn(), success: vi.fn() },
|
||||
}))
|
||||
const mocks = vi.hoisted(() => {
|
||||
const apiGet = vi.fn()
|
||||
const apiPut = vi.fn()
|
||||
|
||||
return {
|
||||
api: { get: apiGet, put: apiPut },
|
||||
apiGet,
|
||||
apiPut,
|
||||
ensureSidebarNav: vi.fn(),
|
||||
loadRemoteComponent: vi.fn(),
|
||||
nativeSubscribe: vi.fn(),
|
||||
toast: { error: vi.fn(), success: vi.fn() },
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: {
|
||||
get: mocks.apiGet,
|
||||
put: mocks.apiPut,
|
||||
},
|
||||
pluginApi: mocks.api,
|
||||
default: mocks.api,
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/federationLoader', () => ({
|
||||
@@ -174,7 +178,7 @@ describe('PluginConfigDialog', () => {
|
||||
expect(mocks.apiGet).toHaveBeenCalledTimes(2)
|
||||
|
||||
await fireEvent.click(screen.getByRole('button', { name: '保存' }))
|
||||
await waitFor(() => expect(mocks.apiPut).toHaveBeenCalledWith('plugin/DemoPlugin', {}))
|
||||
await waitFor(() => expect(mocks.apiPut).toHaveBeenCalledWith('plugin/DemoPlugin', {}, { feedback: 'silent' }))
|
||||
})
|
||||
|
||||
it('does not expose configuration saving while the form is loading or failed', async () => {
|
||||
@@ -229,7 +233,9 @@ describe('PluginConfigDialog', () => {
|
||||
await fireEvent.click(screen.getByRole('button', { name: '调整布局' }))
|
||||
expect(screen.getByRole('dialog')).toHaveAttribute('data-max-width', '72rem')
|
||||
await fireEvent.click(screen.getByRole('button', { name: '远程保存' }))
|
||||
await waitFor(() => expect(mocks.apiPut).toHaveBeenCalledWith('plugin/DemoPlugin', { enabled: false }))
|
||||
await waitFor(() =>
|
||||
expect(mocks.apiPut).toHaveBeenCalledWith('plugin/DemoPlugin', { enabled: false }, { feedback: 'silent' }),
|
||||
)
|
||||
await fireEvent.click(screen.getByRole('button', { name: '切换数据' }))
|
||||
await fireEvent.click(screen.getByRole('button', { name: '关闭远程配置' }))
|
||||
|
||||
@@ -265,7 +271,7 @@ describe('PluginConfigDialog', () => {
|
||||
await fireEvent.click(await screen.findByRole('button', { name: '保存' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.apiPut).toHaveBeenCalledWith('plugin/DemoPlugin', { enabled: true })
|
||||
expect(mocks.apiPut).toHaveBeenCalledWith('plugin/DemoPlugin', { enabled: true }, { feedback: 'silent' })
|
||||
expect(mocks.ensureSidebarNav).toHaveBeenCalledWith(true)
|
||||
expect(result.emitted().save).toHaveLength(1)
|
||||
})
|
||||
@@ -275,7 +281,7 @@ describe('PluginConfigDialog', () => {
|
||||
})
|
||||
|
||||
it.each([
|
||||
['business failure', () => Promise.resolve({ message: '配置被拒绝', success: false })],
|
||||
['business failure', () => Promise.reject(new Error('配置被拒绝'))],
|
||||
['HTTP failure', () => Promise.reject(new Error('request failed'))],
|
||||
])('keeps the dialog open and reports a %s', async (_case, saveResult) => {
|
||||
mocks.apiGet.mockResolvedValue({ conf: [], model: {}, render_mode: 'vuetify' })
|
||||
|
||||
@@ -5,15 +5,21 @@ import { fireEvent, screen, waitFor } from '@testing-library/vue'
|
||||
import { defineComponent, h, inject, type Component, type PropType } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
apiGet: vi.fn(),
|
||||
loadRemoteComponent: vi.fn(),
|
||||
nativeSubscribe: vi.fn(),
|
||||
toast: { error: vi.fn(), success: vi.fn() },
|
||||
}))
|
||||
const mocks = vi.hoisted(() => {
|
||||
const apiGet = vi.fn()
|
||||
|
||||
return {
|
||||
api: { get: apiGet },
|
||||
apiGet,
|
||||
loadRemoteComponent: vi.fn(),
|
||||
nativeSubscribe: vi.fn(),
|
||||
toast: { error: vi.fn(), success: vi.fn() },
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: { get: mocks.apiGet },
|
||||
pluginApi: mocks.api,
|
||||
default: mocks.api,
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/federationLoader', () => ({
|
||||
|
||||
@@ -18,10 +18,10 @@ const mocks = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: {
|
||||
default: createDataApiMock({
|
||||
get: mocks.apiGet,
|
||||
post: mocks.apiPost,
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('vue-toastification', () => ({
|
||||
|
||||
@@ -17,10 +17,10 @@ const mocks = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: {
|
||||
default: createDataApiMock({
|
||||
get: mocks.apiGet,
|
||||
post: mocks.apiPost,
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('vue-toastification', () => ({
|
||||
|
||||
@@ -244,6 +244,11 @@ function createDeferred<T>() {
|
||||
return { promise, reject, resolve }
|
||||
}
|
||||
|
||||
/** 构造后端网络层使用的严格三段式响应。 */
|
||||
function apiEnvelope<T>(data: T | null, success = true, message = ''): ApiResponse<T> {
|
||||
return { data, message, success }
|
||||
}
|
||||
|
||||
function publicSettingHandlers({
|
||||
directories = [],
|
||||
episodeRules = [],
|
||||
@@ -256,19 +261,19 @@ function publicSettingHandlers({
|
||||
return [
|
||||
http.get(new URL('system/setting/public/Directories', API_BASE_URL).href, () => {
|
||||
initializationRequestCount += 1
|
||||
return HttpResponse.json({ data: { value: directories }, success: true })
|
||||
return HttpResponse.json(apiEnvelope({ value: directories }))
|
||||
}),
|
||||
http.get(new URL('system/setting/public/Storages', API_BASE_URL).href, () => {
|
||||
initializationRequestCount += 1
|
||||
return HttpResponse.json({ data: { value: storages }, success: true })
|
||||
return HttpResponse.json(apiEnvelope({ value: storages }))
|
||||
}),
|
||||
http.get(new URL('system/setting/public/EpisodeFormatRuleTable', API_BASE_URL).href, () => {
|
||||
initializationRequestCount += 1
|
||||
return HttpResponse.json({ data: { value: episodeRules }, success: true })
|
||||
return HttpResponse.json(apiEnvelope({ value: episodeRules }))
|
||||
}),
|
||||
http.post(new URL('transfer/manual/history', API_BASE_URL).href, () => {
|
||||
initializationRequestCount += 1
|
||||
return HttpResponse.json({ data: { history_count: 0, reorganize: false }, success: true })
|
||||
return HttpResponse.json(apiEnvelope({ history_count: 0, reorganize: false }))
|
||||
}),
|
||||
]
|
||||
}
|
||||
@@ -354,6 +359,7 @@ function previewResponse(
|
||||
total: items.length,
|
||||
},
|
||||
},
|
||||
message: '',
|
||||
success: true,
|
||||
}
|
||||
}
|
||||
@@ -375,7 +381,7 @@ describe('ReorganizeDialog submission safety', () => {
|
||||
it('keeps the dialog open when the backend reports a business failure', async () => {
|
||||
server.use(
|
||||
http.post(new URL('transfer/manual', API_BASE_URL).href, () =>
|
||||
HttpResponse.json({ message: '整理失败', success: false }),
|
||||
HttpResponse.json(apiEnvelope(null, false, '整理失败')),
|
||||
),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
@@ -388,7 +394,9 @@ describe('ReorganizeDialog submission safety', () => {
|
||||
})
|
||||
|
||||
it('shows a fallback error when a business failure has no message', async () => {
|
||||
server.use(http.post(new URL('transfer/manual', API_BASE_URL).href, () => HttpResponse.json({ success: false })))
|
||||
server.use(
|
||||
http.post(new URL('transfer/manual', API_BASE_URL).href, () => HttpResponse.json(apiEnvelope(null, false))),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
const { onDone } = await renderDialog()
|
||||
|
||||
@@ -415,7 +423,7 @@ describe('ReorganizeDialog submission safety', () => {
|
||||
})
|
||||
|
||||
it('coalesces repeated submit clicks while a transfer request is pending', async () => {
|
||||
const response = createDeferred<ApiResponse>()
|
||||
const response = createDeferred<ApiResponse<null>>()
|
||||
let requestCount = 0
|
||||
server.use(
|
||||
http.post(new URL('transfer/manual', API_BASE_URL).href, async () => {
|
||||
@@ -430,7 +438,7 @@ describe('ReorganizeDialog submission safety', () => {
|
||||
await fireEvent.click(submitButton)
|
||||
|
||||
await waitFor(() => expect(requestCount).toBe(1))
|
||||
response.resolve({ data: undefined, success: true })
|
||||
response.resolve(apiEnvelope(null))
|
||||
await waitFor(() => expect(onDone).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
|
||||
@@ -439,7 +447,7 @@ describe('ReorganizeDialog submission safety', () => {
|
||||
server.use(
|
||||
http.post(new URL('transfer/manual', API_BASE_URL).href, () => {
|
||||
requestCount += 1
|
||||
return HttpResponse.json({ success: true })
|
||||
return HttpResponse.json(apiEnvelope(null))
|
||||
}),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
@@ -471,7 +479,7 @@ describe('ReorganizeDialog payloads and lifecycle', () => {
|
||||
http.post(new URL('transfer/manual', API_BASE_URL).href, async ({ request }) => {
|
||||
bodies.push(await request.json())
|
||||
backgrounds.push(new URL(request.url).searchParams.get('background') ?? '')
|
||||
return HttpResponse.json({ success: true })
|
||||
return HttpResponse.json(apiEnvelope(null))
|
||||
}),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
@@ -504,7 +512,7 @@ describe('ReorganizeDialog payloads and lifecycle', () => {
|
||||
server.use(
|
||||
http.post(new URL('transfer/manual', API_BASE_URL).href, async ({ request }) => {
|
||||
bodies.push(await request.json())
|
||||
return HttpResponse.json({ success: true })
|
||||
return HttpResponse.json(apiEnvelope(null))
|
||||
}),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
@@ -530,7 +538,7 @@ describe('ReorganizeDialog payloads and lifecycle', () => {
|
||||
server.use(
|
||||
http.post(new URL('transfer/manual', API_BASE_URL).href, async ({ request }) => {
|
||||
bodies.push(await request.json())
|
||||
return HttpResponse.json({ success: true })
|
||||
return HttpResponse.json(apiEnvelope(null))
|
||||
}),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
@@ -563,7 +571,7 @@ describe('ReorganizeDialog payloads and lifecycle', () => {
|
||||
})
|
||||
|
||||
it('updates synchronous progress from SSE and always stops it after success', async () => {
|
||||
const response = createDeferred<ApiResponse>()
|
||||
const response = createDeferred<ApiResponse<null>>()
|
||||
let payload: unknown
|
||||
server.use(
|
||||
http.post(new URL('transfer/manual', API_BASE_URL).href, async ({ request }) => {
|
||||
@@ -582,7 +590,7 @@ describe('ReorganizeDialog payloads and lifecycle', () => {
|
||||
)
|
||||
await waitFor(() => expect(screen.getByTestId('transfer-progress')).toHaveTextContent('正在写入媒体库:65'))
|
||||
|
||||
response.resolve({ data: undefined, success: true })
|
||||
response.resolve(apiEnvelope(null))
|
||||
await waitFor(() => expect(onDone).toHaveBeenCalledTimes(1))
|
||||
expect(payload).toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -596,7 +604,7 @@ describe('ReorganizeDialog payloads and lifecycle', () => {
|
||||
})
|
||||
|
||||
it('stops the active progress stream when unmounted during a request', async () => {
|
||||
const response = createDeferred<ApiResponse>()
|
||||
const response = createDeferred<ApiResponse<null>>()
|
||||
const requestCompleted = createDeferred<void>()
|
||||
server.use(
|
||||
http.post(new URL('transfer/manual', API_BASE_URL).href, async () => {
|
||||
@@ -612,7 +620,7 @@ describe('ReorganizeDialog payloads and lifecycle', () => {
|
||||
unmount()
|
||||
|
||||
expect(mocks.progressControllers[0].stop).toHaveBeenCalledTimes(1)
|
||||
response.resolve({ data: undefined, success: true })
|
||||
response.resolve(apiEnvelope(null))
|
||||
await requestCompleted.promise
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
})
|
||||
@@ -633,7 +641,7 @@ describe('ReorganizeDialog payloads and lifecycle', () => {
|
||||
server.use(
|
||||
http.post(new URL('transfer/manual', API_BASE_URL).href, async ({ request }) => {
|
||||
bodies.push(await request.json())
|
||||
return HttpResponse.json({ success: true })
|
||||
return HttpResponse.json(apiEnvelope(null))
|
||||
}),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
@@ -677,11 +685,11 @@ describe('ReorganizeDialog payloads and lifecycle', () => {
|
||||
const bodies: unknown[] = []
|
||||
server.use(
|
||||
http.get(new URL('media/groups/600', API_BASE_URL).href, () =>
|
||||
HttpResponse.json([{ episode_count: 12, group_count: 1, id: 'group-1', name: '播出顺序' }]),
|
||||
HttpResponse.json(apiEnvelope([{ episode_count: 12, group_count: 1, id: 'group-1', name: '播出顺序' }])),
|
||||
),
|
||||
http.post(new URL('transfer/manual', API_BASE_URL).href, async ({ request }) => {
|
||||
bodies.push(await request.json())
|
||||
return HttpResponse.json({ success: true })
|
||||
return HttpResponse.json(apiEnvelope(null))
|
||||
}),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
@@ -739,7 +747,7 @@ describe('ReorganizeDialog payloads and lifecycle', () => {
|
||||
server.use(
|
||||
http.post(new URL('transfer/manual', API_BASE_URL).href, async ({ request }) => {
|
||||
bodies.push(await request.json())
|
||||
return HttpResponse.json({ success: true })
|
||||
return HttpResponse.json(apiEnvelope(null))
|
||||
}),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
@@ -780,12 +788,13 @@ describe('ReorganizeDialog payloads and lifecycle', () => {
|
||||
rule_name: '标准季集',
|
||||
sample_file: 'Movie.mkv',
|
||||
},
|
||||
message: '',
|
||||
success: true,
|
||||
})
|
||||
}),
|
||||
http.post(new URL('transfer/manual', API_BASE_URL).href, async ({ request }) => {
|
||||
transferBodies.push(await request.json())
|
||||
return HttpResponse.json({ success: true })
|
||||
return HttpResponse.json(apiEnvelope(null))
|
||||
}),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
@@ -810,7 +819,7 @@ describe('ReorganizeDialog payloads and lifecycle', () => {
|
||||
server.use(
|
||||
http.post(new URL('transfer/manual', API_BASE_URL).href, async ({ request }) => {
|
||||
bodies.push(await request.json())
|
||||
return HttpResponse.json({ success: true })
|
||||
return HttpResponse.json(apiEnvelope(null))
|
||||
}),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
@@ -941,7 +950,7 @@ describe('ReorganizeDialog preview', () => {
|
||||
it('summarizes a business-level preview failure without closing the preview', async () => {
|
||||
server.use(
|
||||
http.post(new URL('transfer/manual', API_BASE_URL).href, () =>
|
||||
HttpResponse.json({ message: '目标目录不可用', success: false }),
|
||||
HttpResponse.json(apiEnvelope(null, false, '目标目录不可用')),
|
||||
),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
|
||||
@@ -176,7 +176,7 @@ describe('SiteImportDialog', () => {
|
||||
expect(screen.getByText('未支持站点 - 错误详情')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('reports partial success and preserves the HTTP error message', async () => {
|
||||
it('reports partial success and preserves the backend HTTP error message', async () => {
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const sites = [createSite({ name: '成功站点' }), createSite({ name: '失败站点' })]
|
||||
let requestIndex = 0
|
||||
@@ -196,7 +196,7 @@ describe('SiteImportDialog', () => {
|
||||
expect(screen.getByText('成功导入 1 个站点')).toBeInTheDocument()
|
||||
expect(mocks.toastError).toHaveBeenCalledWith('导入完成,成功 1 个,失败 1 个')
|
||||
await fireEvent.click(screen.getByText('失败站点 - 错误详情'))
|
||||
expect(await screen.findByText('Request failed with status code 500')).toBeInTheDocument()
|
||||
expect(await screen.findByText('第二站请求失败')).toBeInTheDocument()
|
||||
expect(consoleError).toHaveBeenCalledWith('Import site 失败站点 failed:', expect.any(Error))
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import SiteUserDataDialog from '@/components/dialog/SiteUserDataDialog.vue'
|
||||
import type { ApiResponse, SiteUserData } from '@/api/types'
|
||||
import type { SiteUserData } from '@/api/types'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/vue'
|
||||
import { createSite, createSiteUserData } from '@tests/support/factories/site'
|
||||
import { refreshSiteUserDataHandler, siteUserDataHandler } from '@tests/support/msw/handlers/site'
|
||||
@@ -55,7 +55,7 @@ function deferred<T>() {
|
||||
}
|
||||
|
||||
async function renderDialog(
|
||||
initialResult: Pick<ApiResponse<SiteUserData[]>, 'data' | 'message' | 'success'> = { data: [], success: false },
|
||||
initialResult: { data: SiteUserData[]; message?: string; success: boolean } = { data: [], success: true },
|
||||
status = 200,
|
||||
onRequest: () => void | Promise<void> = () => {},
|
||||
) {
|
||||
@@ -390,7 +390,7 @@ describe('SiteUserDataDialog refresh and recovery', () => {
|
||||
it('clears an initial HTTP error when retry returns the legal empty state', async () => {
|
||||
const { site } = await renderDialog({ data: [], success: false }, 500)
|
||||
expect(await screen.findByText(/加载站点数据失败/)).toBeInTheDocument()
|
||||
server.use(siteUserDataHandler(site.id, { data: [], success: false }))
|
||||
server.use(siteUserDataHandler(site.id, { data: [], success: true }))
|
||||
|
||||
await fireEvent.click(getRetryButton())
|
||||
|
||||
|
||||
@@ -26,11 +26,11 @@ const DialogCloseBtn = defineComponent({
|
||||
})
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: {
|
||||
default: createDataApiMock({
|
||||
get: (...args: unknown[]) => mocks.apiGet(...args),
|
||||
post: (...args: unknown[]) => mocks.apiPost(...args),
|
||||
put: (...args: unknown[]) => mocks.apiPut(...args),
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/nprogress', () => ({
|
||||
|
||||
@@ -4,7 +4,7 @@ import FileToolbar from './FileToolbar.vue'
|
||||
import FileNavigator from './FileNavigator.vue'
|
||||
import type { EndPoints, FileItem, StorageConf } from '@/api/types'
|
||||
import { storageIconDict } from '@/api/constants'
|
||||
import type { AxiosInstance } from 'axios'
|
||||
import type { DataApiClient } from '@/api'
|
||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { useUserStore } from '@/stores'
|
||||
@@ -22,7 +22,7 @@ const props = defineProps({
|
||||
endpoints: Object as PropType<EndPoints>,
|
||||
// Axios 实例是可调用函数,运行时 prop 类型需与其实际形态一致。
|
||||
axios: {
|
||||
type: Function as PropType<AxiosInstance>,
|
||||
type: Function as PropType<DataApiClient>,
|
||||
required: true,
|
||||
},
|
||||
axiosconfig: Object,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AxiosRequestConfig, AxiosInstance } from 'axios'
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
import type { PropType } from 'vue'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { formatBytes } from '@core/utils/formatters'
|
||||
import type { ApiResponse, Context, EndPoints, FileItem, ManualScrapeOptions } from '@/api/types'
|
||||
import type { Context, EndPoints, FileItem, ManualScrapeOptions } from '@/api/types'
|
||||
import api from '@/api'
|
||||
import type { DataApiClient } from '@/api'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useBackground } from '@/composables/useBackground'
|
||||
@@ -38,7 +39,7 @@ const inProps = defineProps({
|
||||
endpoints: Object as PropType<EndPoints>,
|
||||
// Axios 实例是可调用函数,运行时 prop 类型需与其实际形态一致。
|
||||
axios: {
|
||||
type: Function as PropType<AxiosInstance>,
|
||||
type: Function as PropType<DataApiClient>,
|
||||
required: true,
|
||||
},
|
||||
refreshpending: Boolean,
|
||||
@@ -303,8 +304,9 @@ async function requestDeleteItem(item: FileItem) {
|
||||
url: inProps.endpoints?.delete.url,
|
||||
method: inProps.endpoints?.delete.method || 'post',
|
||||
data: item,
|
||||
feedback: 'silent',
|
||||
}
|
||||
return inProps.axios.request<ApiResponse<unknown>, ApiResponse<unknown>>(config)
|
||||
return inProps.axios.request<null>(config)
|
||||
}
|
||||
|
||||
// 删除项目
|
||||
@@ -324,11 +326,7 @@ async function deleteItem(item: FileItem, confirm: boolean = true) {
|
||||
emit('loading', true)
|
||||
|
||||
try {
|
||||
const result = await requestDeleteItem(item)
|
||||
if (!result.success) {
|
||||
$toast.error(result.message || t('common.error'))
|
||||
return false
|
||||
}
|
||||
await requestDeleteItem(item)
|
||||
|
||||
emit('filedeleted')
|
||||
await list_files()
|
||||
@@ -368,8 +366,7 @@ async function batchDelete() {
|
||||
progressValue.value = Math.round(((index + 1) / selectedItems.length) * 100)
|
||||
progressDialogController?.updateProps({ text: progressText.value, value: progressValue.value })
|
||||
try {
|
||||
const result = await requestDeleteItem(item)
|
||||
if (!result.success) failedItems.push(item)
|
||||
await requestDeleteItem(item)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
failedItems.push(item)
|
||||
@@ -503,17 +500,13 @@ async function get_recommend_name() {
|
||||
renameLoading.value = true
|
||||
renameDialogController?.updateProps({ loading: true })
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('transfer/name', {
|
||||
const result = await api.get<{ name: string }>('transfer/name', {
|
||||
params: {
|
||||
path: `${inProps.item.path}${currentItem.value?.name}`,
|
||||
filetype: currentItem.value?.type ?? 'file',
|
||||
},
|
||||
})
|
||||
if (result.success && result.data) {
|
||||
newName.value = result.data.name
|
||||
} else {
|
||||
$toast.error(result.message)
|
||||
}
|
||||
newName.value = result.name
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
@@ -546,12 +539,9 @@ async function rename() {
|
||||
url,
|
||||
method: inProps.endpoints?.rename.method || 'post',
|
||||
data: currentItem.value,
|
||||
feedback: 'silent',
|
||||
}
|
||||
const result: { [key: string]: any } = await inProps.axios.request<any, { [key: string]: any }>(config)
|
||||
if (!result.success) {
|
||||
$toast.error(result.message || t('common.error'))
|
||||
return
|
||||
}
|
||||
await inProps.axios.request<null>(config)
|
||||
|
||||
newName.value = ''
|
||||
renameAll.value = false
|
||||
@@ -714,19 +704,21 @@ async function recognize(path: string) {
|
||||
}
|
||||
|
||||
// 调用 API 按请求级媒体条件刮削单个文件项。
|
||||
async function scrape(item: FileItem, options: ManualScrapeOptions) {
|
||||
async function scrape(item: FileItem, options: ManualScrapeOptions, silent = false) {
|
||||
try {
|
||||
progressText.value = t('file.scraping', { path: item.path })
|
||||
progressDialogController?.updateProps({ text: progressText.value })
|
||||
|
||||
const result: { [key: string]: any } = await api.post(`media/scrape/${inProps.item.storage}`, item, {
|
||||
await api.post<null>(`media/scrape/${inProps.item.storage}`, item, {
|
||||
params: options,
|
||||
feedback: 'silent',
|
||||
})
|
||||
|
||||
if (!result.success) $toast.error(result.message)
|
||||
else $toast.success(t('file.scrapeCompleted', { path: item.path }))
|
||||
if (!silent) $toast.success(t('file.scrapeCompleted', { path: item.path }))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
if (!silent) $toast.error(error instanceof Error ? error.message : t('common.error'))
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -738,12 +730,21 @@ async function scrapeItems(itemsToScrape: FileItem[], options: ManualScrapeOptio
|
||||
progressText.value = t('file.scraping', { path: normalizedItems[0].path })
|
||||
progressValue.value = 0
|
||||
openProgressDialog(progressText.value, progressValue.value)
|
||||
const failedItems: FileItem[] = []
|
||||
try {
|
||||
for (const [index, item] of normalizedItems.entries()) {
|
||||
await scrape(item, options)
|
||||
try {
|
||||
await scrape(item, options, true)
|
||||
$toast.success(t('file.scrapeCompleted', { path: item.path }))
|
||||
} catch {
|
||||
failedItems.push(item)
|
||||
}
|
||||
progressValue.value = Math.round(((index + 1) / normalizedItems.length) * 100)
|
||||
progressDialogController?.updateProps({ value: progressValue.value })
|
||||
}
|
||||
if (failedItems.length) {
|
||||
$toast.error(`${t('common.error')}: ${failedItems.map(item => item.name).join(', ')}`)
|
||||
}
|
||||
} finally {
|
||||
closeProgressDialog()
|
||||
if (selectMode.value) exitSelectMode()
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import type { PropType } from 'vue'
|
||||
import type { FileItem } from '@/api/types'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import type { AxiosRequestConfig, AxiosInstance } from 'axios'
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
import type { DataApiClient } from '@/api'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAvailableHeight } from '@/composables/useAvailableHeight'
|
||||
|
||||
@@ -36,7 +37,7 @@ const props = defineProps({
|
||||
endpoints: Object,
|
||||
// Axios 实例是可调用函数,运行时 prop 类型需与其实际形态一致。
|
||||
axios: {
|
||||
type: Function as PropType<AxiosInstance>,
|
||||
type: Function as PropType<DataApiClient>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
@@ -113,7 +114,7 @@ async function loadSubdirectories(path: string) {
|
||||
data: fakeItem,
|
||||
}
|
||||
|
||||
const result = await props.axios?.request(config)
|
||||
const result = await props.axios?.request<FileItem[]>(config)
|
||||
if (result && Array.isArray(result)) {
|
||||
// 过滤出目录项
|
||||
const dirs = result.filter(item => item.type === 'dir')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AxiosRequestConfig, AxiosInstance } from 'axios'
|
||||
import type { ApiResponse, EndPoints, FileItem } from '@/api/types'
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
import type { EndPoints, FileItem } from '@/api/types'
|
||||
import type { DataApiClient } from '@/api'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { openSharedDialog } from '@/composables/useSharedDialog'
|
||||
|
||||
@@ -33,7 +34,7 @@ const inProps = defineProps({
|
||||
endpoints: Object as PropType<EndPoints>,
|
||||
// Axios 实例是可调用函数,运行时 prop 类型需与其实际形态一致。
|
||||
axios: {
|
||||
type: Function as PropType<AxiosInstance>,
|
||||
type: Function as PropType<DataApiClient>,
|
||||
required: true,
|
||||
},
|
||||
sort: {
|
||||
@@ -109,12 +110,10 @@ async function mkdir() {
|
||||
url,
|
||||
method: inProps.endpoints?.mkdir.method || 'post',
|
||||
data: inProps.item,
|
||||
feedback: 'silent',
|
||||
}
|
||||
|
||||
const result = await inProps.axios.request<unknown, ApiResponse<unknown>>(config)
|
||||
if (!result?.success) {
|
||||
return
|
||||
}
|
||||
await inProps.axios.request<null>(config)
|
||||
|
||||
newFolderDialogController?.close()
|
||||
newFolderDialogController = null
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import FileBrowser from '@/components/filebrowser/FileBrowser.vue'
|
||||
import type { DataApiClient } from '@/api'
|
||||
import type { EndPoints } from '@/api/types'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { defineComponent, h, KeepAlive, nextTick, ref } from 'vue'
|
||||
import type { AxiosInstance } from 'axios'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -79,7 +79,7 @@ const FileListStub = defineComponent({
|
||||
|
||||
function createBrowserProps() {
|
||||
const request = vi.fn()
|
||||
const axios = Object.assign(vi.fn(), { request }) as unknown as AxiosInstance
|
||||
const axios = Object.assign(vi.fn(), { request }) as unknown as DataApiClient
|
||||
const endpoint = { method: 'post', url: '/unused' }
|
||||
const endpoints: EndPoints = {
|
||||
delete: endpoint,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { EndPoints, FileItem } from '@/api/types'
|
||||
import type { DataApiClient } from '@/api'
|
||||
import FileList from '@/components/filebrowser/FileList.vue'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/vue'
|
||||
import { renderWithProviders } from '@tests/support/render'
|
||||
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
import { defineComponent, nextTick } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -21,10 +22,10 @@ const mocks = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: {
|
||||
default: createDataApiMock({
|
||||
get: (...args: unknown[]) => mocks.apiGet(...args),
|
||||
post: (...args: unknown[]) => mocks.apiPost(...args),
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('vue-toastification', () => ({
|
||||
@@ -171,7 +172,7 @@ async function renderList(
|
||||
sort?: string
|
||||
} = {},
|
||||
) {
|
||||
const axios = Object.assign(vi.fn(), { request: vi.fn(request) }) as unknown as AxiosInstance
|
||||
const axios = Object.assign(vi.fn(), { request: vi.fn(request) }) as unknown as DataApiClient
|
||||
const result = await renderWithProviders(FileList, {
|
||||
global: { stubs },
|
||||
props: {
|
||||
@@ -186,7 +187,7 @@ async function renderList(
|
||||
return { ...result, axios }
|
||||
}
|
||||
|
||||
function getRequestConfig(axios: AxiosInstance, index: number) {
|
||||
function getRequestConfig(axios: DataApiClient, index: number) {
|
||||
return vi.mocked(axios.request).mock.calls[index]?.[0] as AxiosRequestConfig
|
||||
}
|
||||
|
||||
@@ -415,7 +416,7 @@ describe('FileList destructive operations', () => {
|
||||
|
||||
it('does not emit deletion success for a business failure and always closes loading', async () => {
|
||||
const request = vi.fn((config: AxiosRequestConfig) => {
|
||||
if (config.url === '/storage/delete') return Promise.resolve({ message: 'delete denied', success: false })
|
||||
if (config.url === '/storage/delete') return Promise.reject(new Error('delete denied'))
|
||||
return Promise.resolve([createItem({ name: 'failed.mkv' })])
|
||||
})
|
||||
const { emitted } = await renderList(request)
|
||||
@@ -451,7 +452,7 @@ describe('FileList destructive operations', () => {
|
||||
listCount += 1
|
||||
return Promise.resolve(listCount === 1 ? [item] : [])
|
||||
}
|
||||
return Promise.resolve({ success: true })
|
||||
return Promise.resolve(null)
|
||||
})
|
||||
const { emitted } = await renderList(request)
|
||||
await screen.findByText('deleted.mkv')
|
||||
@@ -474,9 +475,9 @@ describe('FileList destructive operations', () => {
|
||||
return Promise.resolve(listCount === 1 ? [first, failed] : [failed])
|
||||
}
|
||||
if ((config.data as FileItem).name === 'failed.mkv') {
|
||||
return Promise.resolve({ message: 'permission denied', success: false })
|
||||
return Promise.reject(new Error('permission denied'))
|
||||
}
|
||||
return Promise.resolve({ success: true })
|
||||
return Promise.resolve(null)
|
||||
})
|
||||
const { emitted } = await renderList(request)
|
||||
await screen.findByText('first.mkv')
|
||||
@@ -532,7 +533,7 @@ describe('FileList dialogs, download and lifecycle', () => {
|
||||
const first = createItem({ name: 'first.mkv', path: '/media/first.mkv' })
|
||||
const second = createItem({ name: 'second.mkv', path: '/media/second.mkv' })
|
||||
const request = vi.fn().mockResolvedValue([first, second])
|
||||
mocks.apiPost.mockResolvedValue({ success: true })
|
||||
mocks.apiPost.mockResolvedValue(null)
|
||||
await renderList(request)
|
||||
await screen.findByText('first.mkv')
|
||||
|
||||
@@ -665,7 +666,7 @@ describe('FileList dialogs, download and lifecycle', () => {
|
||||
it('wires recursive rename progress SSE and closes resources on unmount', async () => {
|
||||
const controller = { close: vi.fn(), id: 1, updateProps: vi.fn() }
|
||||
mocks.openSharedDialog.mockReturnValue(controller)
|
||||
const renameResult = deferred<{ success: boolean }>()
|
||||
const renameResult = deferred<null>()
|
||||
const request = vi.fn((config: AxiosRequestConfig) =>
|
||||
config.url?.startsWith('/storage/rename')
|
||||
? renameResult.promise
|
||||
@@ -687,7 +688,7 @@ describe('FileList dialogs, download and lifecycle', () => {
|
||||
expect(mocks.progressStart).toHaveBeenCalledOnce()
|
||||
mocks.progressHandler?.(new MessageEvent('message', { data: JSON.stringify({ text_i18n: '重命名中', value: 50 }) }))
|
||||
expect(controller.updateProps).toHaveBeenCalledWith(expect.objectContaining({ text: '重命名中', value: 50 }))
|
||||
renameResult.resolve({ success: true })
|
||||
renameResult.resolve(null)
|
||||
await renamePromise
|
||||
|
||||
unmount()
|
||||
@@ -701,7 +702,7 @@ describe('FileList dialogs, download and lifecycle', () => {
|
||||
mocks.openSharedDialog.mockReturnValueOnce(renameController).mockReturnValueOnce(progressController)
|
||||
const request = vi.fn((config: AxiosRequestConfig) =>
|
||||
config.url?.startsWith('/storage/rename')
|
||||
? Promise.resolve({ message: 'rename denied', success: false })
|
||||
? Promise.reject(new Error('rename denied'))
|
||||
: Promise.resolve([createItem({ name: 'rename.mkv' })]),
|
||||
)
|
||||
const { emitted } = await renderList(request)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import FileNavigator from '@/components/filebrowser/FileNavigator.vue'
|
||||
import type { DataApiClient } from '@/api'
|
||||
import type { EndPoints, FileItem } from '@/api/types'
|
||||
import i18n from '@/plugins/i18n'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { defineComponent, h, nextTick } from 'vue'
|
||||
import type { AxiosInstance } from 'axios'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -43,7 +43,7 @@ function mountNavigator(
|
||||
request = vi.fn().mockResolvedValue([]),
|
||||
overrides: { currentPath?: string; items?: FileItem[] } = {},
|
||||
) {
|
||||
const axios = Object.assign(vi.fn(), { request }) as unknown as AxiosInstance
|
||||
const axios = Object.assign(vi.fn(), { request }) as unknown as DataApiClient
|
||||
const endpoint = { method: 'post', url: '/unused' }
|
||||
const endpoints: EndPoints = {
|
||||
delete: endpoint,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import FileToolbar from '@/components/filebrowser/FileToolbar.vue'
|
||||
import type { DataApiClient } from '@/api'
|
||||
import type { EndPoints, FileItem } from '@/api/types'
|
||||
import i18n from '@/plugins/i18n'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { defineComponent, h } from 'vue'
|
||||
import type { AxiosInstance } from 'axios'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -41,7 +41,7 @@ function mountToolbar(
|
||||
{ name: 'downloads', path: '/downloads/', storage: 'local', type: 'dir' },
|
||||
],
|
||||
) {
|
||||
const axios = Object.assign(vi.fn(), { request }) as unknown as AxiosInstance
|
||||
const axios = Object.assign(vi.fn(), { request }) as unknown as DataApiClient
|
||||
const endpoint = { method: 'post', url: '/unused' }
|
||||
const endpoints: EndPoints = {
|
||||
delete: endpoint,
|
||||
@@ -112,7 +112,7 @@ describe('FileToolbar mkdir', () => {
|
||||
})
|
||||
|
||||
it('does not report creation when the API returns a business failure and always finishes loading', async () => {
|
||||
const request = vi.fn().mockResolvedValue({ message: '目录已存在', success: false })
|
||||
const request = vi.fn().mockRejectedValue(new Error('目录已存在'))
|
||||
const wrapper = mountToolbar(request)
|
||||
|
||||
await openDialogAndCreate(wrapper)
|
||||
@@ -137,7 +137,7 @@ describe('FileToolbar mkdir', () => {
|
||||
})
|
||||
|
||||
it('submits the current directory and closes only after a successful creation', async () => {
|
||||
const request = vi.fn().mockResolvedValue({ success: true })
|
||||
const request = vi.fn().mockResolvedValue(null)
|
||||
const wrapper = mountToolbar(request)
|
||||
|
||||
await openDialogAndCreate(wrapper, 'Season 01')
|
||||
@@ -145,6 +145,7 @@ describe('FileToolbar mkdir', () => {
|
||||
|
||||
expect(request).toHaveBeenCalledWith({
|
||||
data: { name: 'downloads', path: '/downloads/', storage: 'local', type: 'dir' },
|
||||
feedback: 'silent',
|
||||
method: 'post',
|
||||
url: '/storage/mkdir?name=Season 01',
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { h, resolveComponent } from 'vue'
|
||||
import api from '@/api'
|
||||
import { pluginApi } from '@/api'
|
||||
import { DashboardItem } from '@/api/types'
|
||||
import DashboardRender from '@/components/render/DashboardRender.vue'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
@@ -248,7 +248,7 @@ onUnmounted(() => {
|
||||
:is="dynamicPluginComponent"
|
||||
:config="props.config"
|
||||
:allow-refresh="props.allowRefresh"
|
||||
:api="api"
|
||||
:api="pluginApi"
|
||||
:native-subscribe="nativeSubscribe"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,8 @@ const mocks = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
default: { get: mocks.apiGet },
|
||||
default: createDataApiMock({ get: mocks.apiGet }),
|
||||
pluginApi: { get: mocks.apiGet },
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/federationLoader', () => ({
|
||||
|
||||
@@ -33,7 +33,7 @@ async function queryFilterRuleGroups() {
|
||||
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/UserFilterRuleGroups')
|
||||
filterRuleGroups.value = result.data?.value ?? []
|
||||
filterRuleGroups.value = result.value ?? []
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ const storages = ref<StorageConf[]>([])
|
||||
// 查询存储
|
||||
async function loadStorages() {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/public/Storages')
|
||||
storages.value = result.data?.value ?? []
|
||||
storages.value = result.value ?? []
|
||||
}
|
||||
|
||||
// 存储字典
|
||||
|
||||
@@ -32,7 +32,7 @@ async function loadNotificationSetting() {
|
||||
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/Notifications')
|
||||
notifications.value = result.data?.value ?? []
|
||||
notifications.value = result.value ?? []
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user