v0.3.7: 恢复前端Vue源码 + 修复AdminDashboard 401根源

This commit is contained in:
2026-05-17 13:26:36 +08:00
parent 09be4c307e
commit 8cd4dabb60
178 changed files with 20570 additions and 5 deletions

View 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
}

View File

@@ -0,0 +1 @@
// This will be done in chunks via multiple commands to avoid escaping issues