v0.3.7: 恢复前端Vue源码 + 修复AdminDashboard 401根源
This commit is contained in:
473
source_clean/frontend-src/src/api/index.ts
Executable file
473
source_clean/frontend-src/src/api/index.ts
Executable file
@@ -0,0 +1,473 @@
|
||||
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
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
const { data } = await api.post('/admin/notify/test', { channelType, configId })
|
||||
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
|
||||
}
|
||||
1
source_clean/frontend-src/src/api/index.ts.new
Normal file
1
source_clean/frontend-src/src/api/index.ts.new
Normal file
@@ -0,0 +1 @@
|
||||
// This will be done in chunks via multiple commands to avoid escaping issues
|
||||
Reference in New Issue
Block a user