refactor(api): 渠道与存储管理调用适配后端通用 manage 接口

- 新增 src/api/manage.ts 通用封装:ManageRequest(target+action+params)
  与 manageNotificationChannel/manageStorage 两个调用入口
- 存储类调用全部改走 POST /storage/manage:用量、整理方式、
  配置保存/重置、二维码与 OAuth 登录确认(smb/alist/rclone/u115/alipan)
- ClawBot 状态查询、二维码刷新、登出与缓存迁移改走
  POST /notification/manage,原 query 参数转入 body params
- 同步更新 AccountSettingNotification 单测断言,prune 失效 lint 抑制项
This commit is contained in:
jxxghp
2026-08-16 07:19:05 +08:00
parent 6bd684d1bd
commit f8f43058c5
12 changed files with 132 additions and 50 deletions
+1 -1
View File
@@ -101,7 +101,7 @@
},
"src/components/cards/DirectoryCard.vue": {
"@typescript-eslint/no-explicit-any": {
"count": 3
"count": 1
},
"sonarjs/super-linear-regex": {
"count": 1
+43
View File
@@ -0,0 +1,43 @@
import type { AxiosRequestConfig } from 'axios'
import api from './index'
/** 通用管理请求:目标标识 + 管理动作 + 透传参数,与后端 ManageRequest 一致。 */
export interface ManageRequest {
target: string
action: string
params?: Record<string, unknown>
}
/**
* 调用统一管理端点(通知渠道 / 网盘存储)
*
* 端点层不定义任何目标特定的名称与参数,
* 目标标识、管理动作与表单参数原样透传给后端模块
*/
function manageTarget<T = Record<string, unknown>>(
endpoint: 'notification/manage' | 'storage/manage',
request: ManageRequest,
config?: AxiosRequestConfig,
): Promise<T> {
return api.post<T>(endpoint, { params: {}, ...request }, config)
}
/** 对指定通知渠道执行管理动作,返回响应中的业务数据。 */
export function manageNotificationChannel<T = Record<string, unknown>>(
channel: string,
action: string,
params: Record<string, unknown> = {},
config?: AxiosRequestConfig,
): Promise<T> {
return manageTarget<T>('notification/manage', { target: channel, action, params }, config)
}
/** 对指定网盘存储执行管理动作,返回响应中的业务数据。 */
export function manageStorage<T = Record<string, unknown>>(
storage: string,
action: string,
params: Record<string, unknown> = {},
config?: AxiosRequestConfig,
): Promise<T> {
return manageTarget<T>('storage/manage', { target: storage, action, params }, config)
}
+11 -5
View File
@@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { StorageConf, TransferDirectoryConf } from '@/api/types'
import api from '@/api'
import { manageStorage } from '@/api/manage'
import { nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { storageRemoteDict } from '@/api/constants'
@@ -138,11 +138,17 @@ async function loadTransferTypeItems() {
if (!props.directory.library_storage || !props.directory.storage) return
try {
// 下载器储存整理方法
const storage_res = await api.get(`storage/transtype/${props.directory.storage}`)
const storage_transtype = (storage_res as any).transtype
const storage_res = await manageStorage<{ transtype?: Record<string, string> }>(
props.directory.storage,
'support_transtype',
)
const storage_transtype = storage_res?.transtype
// 媒体库储存整理方法
const library_storage_res = await api.get(`storage/transtype/${props.directory.library_storage}`)
const library_storage_transtype = (library_storage_res as any).transtype
const library_storage_res = await manageStorage<{ transtype?: Record<string, string> }>(
props.directory.library_storage,
'support_transtype',
)
const library_storage_transtype = library_storage_res?.transtype
// 为空终止
if (!library_storage_transtype || !storage_transtype) return
// 取并集
+2 -2
View File
@@ -9,7 +9,7 @@ import alist_png from '@images/misc/openlist.svg'
import alistgo_png from '@images/misc/alist.svg'
import custom_png from '@images/misc/database.png'
import smb_png from '@images/misc/smb.png'
import api from '@/api'
import { manageStorage } from '@/api/manage'
import { useToast } from 'vue-toastification'
import { isNullOrEmptyObject } from '@/@core/utils'
import { useI18n } from 'vue-i18n'
@@ -127,7 +127,7 @@ const usage = computed(() => {
/** 查询存储空间使用信息。 */
async function queryStorage() {
try {
const data: { total: number; available: number } = await api.get(`storage/usage/${props.storage.type}`)
const data = await manageStorage<{ total: number; available: number }>(props.storage.type, 'usage')
total.value = data.total
available.value = data.available
} catch (error) {
+3 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import api from '@/api'
import { manageStorage } from '@/api/manage'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
@@ -33,7 +33,7 @@ async function handleDone() {
// 重置配置
async function handleReset() {
try {
await api.get(`/storage/reset/${props.type}`)
await manageStorage(props.type, 'reset_config')
// 重置成功
handleDone()
} catch (e) {
@@ -64,7 +64,7 @@ const sourceItems = [
// 保存alist设置
async function savaAlistConfig() {
try {
await api.post(`storage/save/${props.type}`, props.conf)
await manageStorage(props.type, 'save_config', { conf: props.conf })
} catch (e) {
console.error(e)
}
+16 -6
View File
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import api from '@/api'
import { manageStorage } from '@/api/manage'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
@@ -38,10 +38,15 @@ async function handleDone() {
emit('done')
}
// 调用/aliyun/qrcode api生成二维码
// 调用存储统一管理接口生成二维码
async function getQrcode() {
try {
const result = await api.get<{ codeUrl: string }>('/storage/qrcode/alipan', { feedback: 'silent' })
const result = await manageStorage<{ codeUrl: string }>(
'alipan',
'generate_qrcode',
{},
{ feedback: 'silent' },
)
qrCodeUrl.value = result.codeUrl
timeoutTimer = setTimeout(checkQrcode, 3000)
} catch (e) {
@@ -50,10 +55,15 @@ async function getQrcode() {
}
}
// 调用/aliyun/check api验证二维码
// 调用存储统一管理接口验证二维码
async function checkQrcode() {
try {
const result = await api.get<{ status: string; tip: string }>('/storage/check/alipan', { feedback: 'silent' })
const result = await manageStorage<{ status: string; tip: string }>(
'alipan',
'check_login',
{},
{ feedback: 'silent' },
)
const qrCodeStatus = result.status
text.value = result.tip
if (qrCodeStatus == 'LoginSuccess') {
@@ -79,7 +89,7 @@ async function checkQrcode() {
// 重置配置
async function handleReset() {
try {
await api.get<null>('/storage/reset/alipan', { feedback: 'silent' })
await manageStorage('alipan', 'reset_config', {}, { feedback: 'silent' })
// 重置成功
alertType.value = 'success'
handleDone()
@@ -1,5 +1,5 @@
<script setup lang="ts">
import api from '@/api'
import { manageNotificationChannel } from '@/api/manage'
import { NotificationConf } from '@/api/types'
import { useToast } from 'vue-toastification'
import { cloneDeep } from 'lodash-es'
@@ -280,10 +280,12 @@ async function fetchWechatClawBotStatus(options: WechatClawBotStatusFetchOptions
wechatClawBotLoading.value = true
}
try {
const result = await api.get<WechatClawBotStatus>('notification/wechatclawbot/status', {
params: getWechatClawBotRequestParams({ auto_generate_qrcode: autoGenerateQrcode }),
feedback: 'silent',
})
const result = await manageNotificationChannel<WechatClawBotStatus>(
'WechatClawBot',
'status',
getWechatClawBotRequestParams({ auto_generate_qrcode: autoGenerateQrcode }),
{ feedback: 'silent' },
)
wechatClawBotStatus.value = result
await updateWechatClawBotQrImage(result)
const status = (result.qrcode_status || '').toLowerCase()
@@ -329,10 +331,12 @@ async function refreshWechatClawBotQrcode(options: WechatClawBotRefreshOptions =
wechatClawBotActionLoading.value = true
}
try {
const result = await api.post<WechatClawBotStatus>('notification/wechatclawbot/refresh', null, {
params: getWechatClawBotRequestParams(),
feedback: 'silent',
})
const result = await manageNotificationChannel<WechatClawBotStatus>(
'WechatClawBot',
'refresh_qrcode',
getWechatClawBotRequestParams(),
{ feedback: 'silent' },
)
wechatClawBotStatus.value = result
await updateWechatClawBotQrImage(result)
wechatClawBotExpiredRefreshAttempted.value = false
@@ -359,10 +363,12 @@ async function logoutWechatClawBot() {
}
wechatClawBotActionLoading.value = true
try {
await api.post<null>('notification/wechatclawbot/logout', null, {
params: getWechatClawBotRequestParams(),
feedback: 'silent',
})
await manageNotificationChannel(
'WechatClawBot',
'logout',
getWechatClawBotRequestParams(),
{ feedback: 'silent' },
)
$toast.success(t('notification.wechatclawbot.logoutSuccess'))
await fetchWechatClawBotStatus({
autoGenerateQrcode: true,
+3 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import api from '@/api'
import { manageStorage } from '@/api/manage'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
import { configureAceEditorPadding } from '@/utils/aceEditor'
@@ -38,7 +38,7 @@ async function handleDone() {
// 保存rclone设置
async function savaRcloneConfig() {
try {
await api.post(`storage/save/rclone`, props.conf)
await manageStorage('rclone', 'save_config', { conf: props.conf })
} catch (e) {
console.error(e)
}
@@ -47,7 +47,7 @@ async function savaRcloneConfig() {
// 重置配置
async function handleReset() {
try {
await api.get('/storage/reset/rclone')
await manageStorage('rclone', 'reset_config')
handleDone()
} catch (e) {
console.error(e)
+3 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import api from '@/api'
import { manageStorage } from '@/api/manage'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
@@ -29,7 +29,7 @@ async function handleDone() {
// 重置配置
async function handleReset() {
try {
await api.get('/storage/reset/smb')
await manageStorage('smb', 'reset_config')
// 重置成功
handleDone()
} catch (e) {
@@ -40,7 +40,7 @@ async function handleReset() {
// 保存 SMB 设置
async function saveSmbConfig() {
try {
await api.post(`storage/save/smb`, props.conf)
await manageStorage('smb', 'save_config', { conf: props.conf })
} catch (e) {
console.error(e)
}
+14 -4
View File
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import api from '@/api'
import { manageStorage } from '@/api/manage'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
@@ -64,7 +64,7 @@ function handleDone() {
// 重置配置
async function handleReset() {
try {
await api.get<null>('/storage/reset/u115', { feedback: 'silent' })
await manageStorage('u115', 'reset_config', {}, { feedback: 'silent' })
setMessage('success', t('dialog.u115Auth.authSuccess'))
handleDone()
} catch (error) {
@@ -76,7 +76,12 @@ async function handleReset() {
// 获取授权URL
async function fetchAuthUrl() {
try {
const result = await api.get<{ authUrl: string; state: string }>('/storage/auth_url/u115', { feedback: 'silent' })
const result = await manageStorage<{ authUrl: string; state: string }>(
'u115',
'generate_auth_url',
{},
{ feedback: 'silent' },
)
authUrl.value = result.authUrl
authState.value = result.state
} catch (error) {
@@ -120,7 +125,12 @@ function openAuthWindow() {
// 检查授权状态
async function checkAuthStatus() {
try {
const result = await api.get<{ status: number; tip?: string }>('/storage/check/u115', { feedback: 'silent' })
const result = await manageStorage<{ status: number; tip?: string }>(
'u115',
'check_login',
{},
{ feedback: 'silent' },
)
const { status, tip } = result
if (status === AUTH_STATUS_SUCCESS) {
@@ -1,6 +1,7 @@
<script lang="ts" setup>
import { useToast } from 'vue-toastification'
import api from '@/api'
import { manageNotificationChannel } from '@/api/manage'
import type { NotificationConf, NotificationSwitchConf } from '@/api/types'
import NotificationChannelCard from '@/components/cards/NotificationChannelCard.vue'
import { useI18n } from 'vue-i18n'
@@ -230,13 +231,15 @@ async function migrateWechatClawBotRenames() {
([oldName, newName]) => oldName && newName && oldName !== newName && activeWechatClawBotNames.has(newName),
)
for (const [oldName, newName] of renameEntries) {
await api.post('notification/wechatclawbot/migrate', null, {
feedback: 'silent',
params: {
old_source: oldName,
new_source: newName,
await manageNotificationChannel(
'WechatClawBot',
'migrate_cache',
{
old_name: oldName,
new_name: newName,
},
})
{ feedback: 'silent' },
)
}
}
@@ -190,8 +190,10 @@ describe('AccountSettingNotification', () => {
await user.click(getCard('通知渠道').getByRole('button', { name: '保存' }))
await waitFor(() => expect(mocks.apiPost).toHaveBeenCalledTimes(2))
expect(mocks.apiPost).toHaveBeenNthCalledWith(1, 'notification/wechatclawbot/migrate', null, {
params: { old_source: 'Alpha', new_source: 'Gamma' },
expect(mocks.apiPost).toHaveBeenNthCalledWith(1, 'notification/manage', {
target: 'WechatClawBot',
action: 'migrate_cache',
params: { old_name: 'Alpha', new_name: 'Gamma' },
})
expect(mocks.apiPost).toHaveBeenNthCalledWith(
2,
@@ -216,8 +218,10 @@ describe('AccountSettingNotification', () => {
mocks.apiPost.mockResolvedValue({ success: true })
await user.click(save)
await waitFor(() => expect(mocks.apiPost).toHaveBeenCalledTimes(2))
expect(mocks.apiPost).toHaveBeenNthCalledWith(1, 'notification/wechatclawbot/migrate', null, {
params: { old_source: 'Alpha', new_source: 'Beta' },
expect(mocks.apiPost).toHaveBeenNthCalledWith(1, 'notification/manage', {
target: 'WechatClawBot',
action: 'migrate_cache',
params: { old_name: 'Alpha', new_name: 'Beta' },
})
expect(mocks.apiPost).toHaveBeenNthCalledWith(
2,