474 lines
13 KiB
TypeScript
Executable File
474 lines
13 KiB
TypeScript
Executable File
import axios from 'axios'
|
||
import type {
|
||
SearchResponse,
|
||
VideoParseResult,
|
||
SaveResult,
|
||
QueryResponse,
|
||
RankingItem,
|
||
Promotion,
|
||
CloudConfig,
|
||
StatsData,
|
||
} from '../types'
|
||
|
||
const api = axios.create({
|
||
baseURL: '/api',
|
||
timeout: 30000,
|
||
})
|
||
|
||
// 请求拦截器 — 添加管理员 Token
|
||
api.interceptors.request.use((config) => {
|
||
const token = localStorage.getItem('admin_token')
|
||
if (token) {
|
||
config.headers.Authorization = `Bearer ${token}`
|
||
}
|
||
return config
|
||
})
|
||
|
||
// 响应拦截器 — 统一错误处理
|
||
api.interceptors.response.use(
|
||
(res) => res,
|
||
(err) => {
|
||
if (err.response?.status === 401) {
|
||
localStorage.removeItem('admin_token')
|
||
// Don't redirect if already on the login page or if this was a login attempt itself
|
||
if (!window.location.pathname.startsWith('/admin/login') && !err.config?.url?.includes('/admin/login')) {
|
||
window.location.href = '/admin/login'
|
||
}
|
||
}
|
||
return Promise.reject(err)
|
||
}
|
||
)
|
||
|
||
// ===== 搜索与解析 =====
|
||
export async function query(q: string, page = 1): Promise<QueryResponse> {
|
||
const { data } = await api.post<QueryResponse>('/query', { q, page })
|
||
return data
|
||
}
|
||
|
||
/**
|
||
* 流式搜索 — 使用 NDJSON stream,逐条返回验证结果
|
||
* callback 接收五种事件:
|
||
* onSearching() - 搜索开始(立即返回)
|
||
* onSaved({results, total}) - 本地已保存资源(DB缓存,即时返回)
|
||
* onStats({total, channels, content_info, content_tags}) - 统计信息
|
||
* onResult(id, valid, message) - 单条链接验证结果
|
||
* onComplete({results, channels, total, filtered}) - 全部完成
|
||
*/
|
||
export async function streamSearch(
|
||
q: string,
|
||
callbacks: {
|
||
onSearching?: () => void
|
||
onSaved?: (data: { results: any[]; total: number }) => void
|
||
onStats: (stats: any) => void
|
||
onResult: (id: string, valid: boolean, message?: string) => void
|
||
onComplete: (data: any) => void
|
||
onError?: (err: any) => void
|
||
}
|
||
): Promise<void> {
|
||
const token = localStorage.getItem('admin_token')
|
||
const headers: Record<string, string> = {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/query', {
|
||
method: 'POST',
|
||
headers,
|
||
body: JSON.stringify({ q }),
|
||
})
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}`)
|
||
}
|
||
|
||
const reader = response.body!.getReader()
|
||
const decoder = new TextDecoder()
|
||
let buffer = ''
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read()
|
||
if (done) break
|
||
|
||
buffer += decoder.decode(value, { stream: true })
|
||
const lines = buffer.split('\n')
|
||
buffer = lines.pop() || ''
|
||
|
||
for (const line of lines) {
|
||
if (!line.trim()) continue
|
||
try {
|
||
const msg = JSON.parse(line)
|
||
switch (msg.type) {
|
||
case 'searching':
|
||
callbacks.onSearching?.()
|
||
break
|
||
case 'saved':
|
||
callbacks.onSaved?.(msg)
|
||
break
|
||
case 'stats':
|
||
callbacks.onStats(msg)
|
||
break
|
||
case 'result':
|
||
callbacks.onResult(msg.id, msg.valid, msg.message)
|
||
break
|
||
case 'complete':
|
||
callbacks.onComplete(msg)
|
||
break
|
||
}
|
||
} catch {
|
||
// skip malformed lines
|
||
}
|
||
}
|
||
}
|
||
} catch (err: any) {
|
||
callbacks.onError?.(err)
|
||
}
|
||
}
|
||
|
||
export async function searchPanSou(
|
||
kw: string,
|
||
page = 1,
|
||
pageSize = 20
|
||
): Promise<SearchResponse> {
|
||
const { data } = await api.get<SearchResponse>('/search', {
|
||
params: { kw, page, page_size: pageSize },
|
||
})
|
||
return data
|
||
}
|
||
|
||
export async function parseVideo(url: string): Promise<VideoParseResult> {
|
||
const { data } = await api.post<VideoParseResult>('/video/parse', { url })
|
||
return data
|
||
}
|
||
|
||
// ===== 保存与分享 =====
|
||
export async function saveToCloud(params: {
|
||
type: 'search' | 'video'
|
||
source: any
|
||
target_cloud: string
|
||
}): Promise<SaveResult> {
|
||
const { data } = await api.post<SaveResult>('/save', params)
|
||
return data
|
||
}
|
||
|
||
export async function saveVideoToCloud(params: {
|
||
video_url: string
|
||
title: string
|
||
target_cloud: string
|
||
}): Promise<SaveResult> {
|
||
const { data } = await api.post<SaveResult>('/video/save-to-cloud', params)
|
||
return data
|
||
}
|
||
|
||
// ===== 排行榜 =====
|
||
export async function getRankings(): Promise<RankingItem[]> {
|
||
const { data } = await api.get<RankingItem[]>('/rankings')
|
||
return data
|
||
}
|
||
|
||
export async function getHotKeywords(): Promise<string[]> {
|
||
const { data } = await api.get<string[]>('/rankings/hot')
|
||
return data
|
||
}
|
||
|
||
export async function getCategorizedRankings(): Promise<any> {
|
||
const { data } = await api.get<any>('/rankings/categorized')
|
||
return data
|
||
}
|
||
|
||
// ===== 管理员 =====
|
||
export async function adminLogin(
|
||
username: string,
|
||
password: string
|
||
): Promise<{ token: string }> {
|
||
const { data } = await api.post('/admin/login', { username, password })
|
||
return data
|
||
}
|
||
|
||
export async function getMe(): Promise<{ loggedIn: boolean; id?: number; username?: string }> {
|
||
const { data } = await api.get('/me')
|
||
return data
|
||
}
|
||
|
||
export async function getCloudConfigs(): Promise<CloudConfig[]> {
|
||
const { data } = await api.get('/admin/cloud-configs')
|
||
return data
|
||
}
|
||
|
||
export async function saveCloudConfig(
|
||
config: CloudConfig & { cookie?: string }
|
||
): Promise<CloudConfig> {
|
||
const { data } = await api.post('/admin/cloud-configs', config)
|
||
return data
|
||
}
|
||
|
||
export async function updateCloudConfig(
|
||
config: CloudConfig & { cookie?: string }
|
||
): Promise<CloudConfig> {
|
||
const { data } = await api.put(`/admin/cloud-configs/${config.id}`, config)
|
||
return data
|
||
}
|
||
|
||
export async function testCloudConnection(
|
||
cloudType: string,
|
||
cookie?: string,
|
||
id?: number
|
||
): Promise<{
|
||
success: boolean
|
||
message: string
|
||
nickname?: string
|
||
storage_used?: string
|
||
storage_total?: string
|
||
}> {
|
||
const { data } = await api.post(`/admin/cloud-configs/${cloudType}/test`, { cookie, id })
|
||
return data
|
||
}
|
||
|
||
export async function dailyCheckIn(
|
||
id: number
|
||
): Promise<{
|
||
success: boolean
|
||
message: string
|
||
signedDays?: number
|
||
}> {
|
||
const { data } = await api.post(`/admin/cloud-configs/${id}/checkin`)
|
||
return data
|
||
}
|
||
|
||
export async function skipCheckin(id: number): Promise<boolean> {
|
||
const { data } = await api.post(`/admin/cloud-configs/${id}/skip-checkin`)
|
||
return data.success
|
||
}
|
||
|
||
export async function checkinAll(): Promise<{
|
||
total: number
|
||
results: { id: number; nickname: string; success: boolean; message: string }[]
|
||
}> {
|
||
const { data } = await api.post('/admin/cloud-configs/checkin-all')
|
||
return data
|
||
}
|
||
|
||
export async function checkinSummary(): Promise<{
|
||
total: number
|
||
success: number
|
||
failed: number
|
||
pending: number
|
||
skipped: number
|
||
}> {
|
||
const { data } = await api.get('/admin/cloud-configs/checkin-summary')
|
||
return data
|
||
}
|
||
|
||
export async function deleteCloudConfig(
|
||
id: number
|
||
): Promise<void> {
|
||
await api.delete(`/admin/cloud-configs/${id}`)
|
||
}
|
||
|
||
export async function setPrimary(
|
||
id: number,
|
||
primary: boolean
|
||
): Promise<any> {
|
||
const { data } = await api.put(`/admin/cloud-configs/${id}/primary`, { primary })
|
||
return data
|
||
}
|
||
|
||
export async function getStats(days?: number): Promise<StatsData> {
|
||
const params: Record<string, number> = {}
|
||
if (days) params.days = days
|
||
const { data } = await api.get('/admin/stats', { params })
|
||
return data
|
||
}
|
||
|
||
// ===== 转存日志 =====
|
||
export async function getSaveRecords(page = 1, pageSize = 20, startDate?: string, endDate?: string, status?: string, cloud?: string, keyword?: string): Promise<{
|
||
total: number
|
||
records: SaveRecord[]
|
||
summary?: { total: number; success: number; failed: number; reused: number }
|
||
}> {
|
||
const params: Record<string, number | string> = { page, pageSize }
|
||
if (startDate) params.startDate = startDate
|
||
if (endDate) params.endDate = endDate
|
||
if (status) params.status = status
|
||
if (cloud) params.sourceType = cloud
|
||
if (keyword) params.keyword = keyword
|
||
const { data } = await api.get('/admin/save-records', { params })
|
||
return data
|
||
}
|
||
|
||
export interface SaveRecord {
|
||
id: number
|
||
source_type: string
|
||
source_title: string | null
|
||
source_url: string
|
||
target_cloud: string
|
||
share_url: string | null
|
||
share_pwd: string | null
|
||
file_size: string | null
|
||
file_count: number
|
||
duration_ms: number
|
||
status: string
|
||
error_message: string | null
|
||
folder_name: string | null
|
||
folder_count: number
|
||
original_folder_name: string | null
|
||
ip_address: string | null
|
||
ip_location: string | null
|
||
created_at: string
|
||
}
|
||
|
||
// ===== 系统配置 =====
|
||
|
||
/** Save/update per-config notification settings */
|
||
export async function saveConfigNotify(
|
||
configId: number,
|
||
settings: Record<string, any>
|
||
): Promise<{ success: boolean; message: string }> {
|
||
const { data } = await api.put(`/admin/cloud-configs/${configId}/notify`, settings)
|
||
return data
|
||
}
|
||
|
||
/** Get per-config notification settings */
|
||
export async function getConfigNotify(
|
||
configId: number
|
||
): Promise<Record<string, any>> {
|
||
const { data } = await api.get(`/admin/cloud-configs/${configId}/notify`)
|
||
return data
|
||
}
|
||
|
||
/** Test a notification channel (global or per-config) */
|
||
export async function getAllNotifierProviders(): Promise<Record<string, { name: string; label: string; params: { key: string; label: string; type: string; required: boolean; placeholder?: string; default?: any }[] }>> {
|
||
const { data } = await api.get('/admin/notify/providers')
|
||
return data
|
||
}
|
||
|
||
export async function testNotifyChannel(
|
||
channelType: string,
|
||
configId?: number,
|
||
params?: Record<string, any>
|
||
): Promise<{ success: boolean; message: string }> {
|
||
const { data } = await api.post('/admin/notify/test', { channelType, configId, params })
|
||
return data
|
||
}
|
||
|
||
export async function getSystemConfigs(): Promise<{ key: string; value: string; description: string }[]> {
|
||
const { data } = await api.get('/admin/system-configs')
|
||
return data
|
||
}
|
||
|
||
export async function updateSystemConfigs(
|
||
entries: { key: string; value: string }[]
|
||
): Promise<void> {
|
||
await api.put('/admin/system-configs', { entries })
|
||
}
|
||
|
||
// ===== 网盘类型开关 =====
|
||
export async function getCloudTypes(): Promise<{ types: { type: string; label: string; icon: string; enabled: boolean }[] }> {
|
||
const { data } = await api.get('/admin/cloud-types')
|
||
return data
|
||
}
|
||
|
||
export async function toggleCloudType(type: string, enabled: boolean): Promise<void> {
|
||
await api.put('/admin/cloud-types', { type, enabled })
|
||
}
|
||
|
||
// ===== 修改密码 =====
|
||
export async function changePassword(
|
||
oldPassword: string,
|
||
newPassword: string
|
||
): Promise<{ success: boolean; message: string }> {
|
||
const { data } = await api.post('/admin/change-password', { oldPassword, newPassword })
|
||
return data
|
||
}
|
||
|
||
export default api
|
||
export { query as searchQuery }
|
||
|
||
// ===== 系统设置(SettingsManage.vue) =====
|
||
export async function getSettings(): Promise<any[]> {
|
||
const { data } = await api.get('/admin/system-configs')
|
||
return data
|
||
}
|
||
export async function updateSetting(key: string, value: string): Promise<void> {
|
||
await api.put('/admin/system-configs', { entries: [{ key, value }] })
|
||
}
|
||
|
||
export async function uploadFallbackImage(file: File): Promise<{ success: boolean; url: string; message: string }> {
|
||
const form = new FormData()
|
||
form.append('image', file)
|
||
const { data } = await api.post('/admin/upload-fallback-image', form, {
|
||
headers: { 'Content-Type': 'multipart/form-data' },
|
||
})
|
||
return data
|
||
}
|
||
|
||
export async function uploadLogo(file: File): Promise<{ success: boolean; url: string; message: string }> {
|
||
const form = new FormData()
|
||
form.append('image', file)
|
||
const { data } = await api.post('/admin/upload-logo', form, {
|
||
headers: { 'Content-Type': 'multipart/form-data' },
|
||
})
|
||
return data
|
||
}
|
||
|
||
export async function getSiteConfig(): Promise<{ site_logo: string; site_name: string; search_fallback_image: string; site_disclaimer: string }> {
|
||
const { data } = await api.get('/site-config')
|
||
return data
|
||
}
|
||
|
||
// ===== Redis 连接测试 =====
|
||
export async function testRedisConnection(url: string): Promise<{ ok: boolean; latency: number; info: string }> {
|
||
const { data } = await api.post('/admin/test-redis', { url })
|
||
return data
|
||
}
|
||
|
||
// ===== 外部服务连接测试 =====
|
||
export async function testExternalService(params: {
|
||
type: 'pansou' | 'video_parser' | 'tmdb' | 'proxy' | 'ip_geo'
|
||
url?: string
|
||
token?: string
|
||
}): Promise<{ ok: boolean; latency: number; info: string }> {
|
||
const { data } = await api.post('/admin/test-external-service', params)
|
||
return data
|
||
}
|
||
|
||
// ===== 数据库状态 =====
|
||
export async function getDbStatus(): Promise<{
|
||
db_size: string
|
||
db_path: string
|
||
save_records: number
|
||
search_stats: number
|
||
system_configs: number
|
||
cloud_configs: number
|
||
content_cache: number
|
||
redis_status: string
|
||
redis_url: string
|
||
}> {
|
||
const { data } = await api.get('/admin/db-status')
|
||
return data
|
||
}
|
||
|
||
// ===== 存储清理 =====
|
||
export async function runCleanup(): Promise<{
|
||
success: boolean
|
||
files_trashed: number
|
||
logs_deleted: number
|
||
trash_emptied: boolean
|
||
errors: string[]
|
||
message: string
|
||
}> {
|
||
const { data } = await api.post('/admin/cleanup/run')
|
||
return data
|
||
}
|
||
|
||
export async function emptyAllTrash(): Promise<{
|
||
success: boolean
|
||
emptied: boolean
|
||
errors: string[]
|
||
message: string
|
||
}> {
|
||
const { data } = await api.post('/admin/cleanup/empty-trash')
|
||
return data
|
||
} |