fix: 修复 WebUI 源码缺失、环境检测路径及文档说明
- 调整 .gitignore,避免误忽略 webui/src/lib/ 和 webui/src/components/env/ - 补齐 WebUI 缺失的 lib 工具函数、API 封装及环境检测组件 - 修复 /api/env/check 使用相对路径导致启动目录不同时检测失败的问题 - 更新中/英/西三语 README 的 WebUI 开发调试与生产构建说明
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
// Types
|
||||
export interface CrawlerConfig {
|
||||
platform: string
|
||||
login_type: string
|
||||
crawler_type: string
|
||||
keywords: string
|
||||
start_page: number
|
||||
enable_comments: boolean
|
||||
enable_sub_comments: boolean
|
||||
save_option: string
|
||||
cookies: string
|
||||
headless: boolean
|
||||
}
|
||||
|
||||
export interface CrawlerStatus {
|
||||
status: 'idle' | 'running' | 'stopping' | 'error'
|
||||
platform: string | null
|
||||
crawler_type: string | null
|
||||
started_at: string | null
|
||||
error_message: string | null
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
id: number
|
||||
timestamp: string
|
||||
level: 'info' | 'warning' | 'error' | 'success' | 'debug'
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface DataFile {
|
||||
name: string
|
||||
path: string
|
||||
size: number
|
||||
modified_at: number
|
||||
record_count: number | null
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface FilePreviewResponse {
|
||||
data: Record<string, unknown>[]
|
||||
total: number
|
||||
columns?: string[]
|
||||
}
|
||||
|
||||
export interface Platform {
|
||||
value: string
|
||||
label: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
export interface ConfigOption {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
// API functions
|
||||
export const crawlerApi = {
|
||||
start: (config: CrawlerConfig) => api.post('/crawler/start', config),
|
||||
stop: () => api.post('/crawler/stop'),
|
||||
getStatus: () => api.get<CrawlerStatus>('/crawler/status'),
|
||||
getLogs: (limit = 100) => api.get<{ logs: LogEntry[] }>('/crawler/logs', { params: { limit } }),
|
||||
}
|
||||
|
||||
export const dataApi = {
|
||||
getFiles: (platform?: string, fileType?: string) =>
|
||||
api.get<{ files: DataFile[] }>('/data/files', { params: { platform, file_type: fileType } }),
|
||||
getFileContent: (path: string, limit = 100) =>
|
||||
api.get<FilePreviewResponse>('/data/files/' + path, { params: { preview: true, limit } }),
|
||||
getStats: () => api.get('/data/stats'),
|
||||
getDownloadUrl: (path: string) => `/api/data/download/${path}`,
|
||||
}
|
||||
|
||||
export const configApi = {
|
||||
getPlatforms: () => api.get<{ platforms: Platform[] }>('/config/platforms'),
|
||||
getOptions: () =>
|
||||
api.get<{
|
||||
login_types: ConfigOption[]
|
||||
crawler_types: ConfigOption[]
|
||||
save_options: ConfigOption[]
|
||||
}>('/config/options'),
|
||||
}
|
||||
|
||||
export interface EnvCheckResult {
|
||||
success: boolean
|
||||
message: string
|
||||
output?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export const envApi = {
|
||||
check: () => api.get<EnvCheckResult>('/env/check'),
|
||||
}
|
||||
|
||||
export default api
|
||||
@@ -0,0 +1,141 @@
|
||||
export interface ParsedId {
|
||||
id: string
|
||||
type: 'video' | 'creator' | 'unknown'
|
||||
original: string
|
||||
isValid: boolean
|
||||
}
|
||||
|
||||
// URL patterns for different platforms
|
||||
const platformPatterns: Record<string, {
|
||||
video: RegExp[]
|
||||
creator: RegExp[]
|
||||
}> = {
|
||||
xhs: {
|
||||
video: [
|
||||
/xiaohongshu\.com\/explore\/([a-zA-Z0-9]+)/,
|
||||
/xiaohongshu\.com\/discovery\/item\/([a-zA-Z0-9]+)/,
|
||||
/xhslink\.com\/([a-zA-Z0-9]+)/,
|
||||
],
|
||||
creator: [
|
||||
/xiaohongshu\.com\/user\/profile\/([a-zA-Z0-9]+)/,
|
||||
],
|
||||
},
|
||||
dy: {
|
||||
video: [
|
||||
/douyin\.com\/video\/(\d+)/,
|
||||
/v\.douyin\.com\/([a-zA-Z0-9]+)/,
|
||||
/iesdouyin\.com\/share\/video\/(\d+)/,
|
||||
],
|
||||
creator: [
|
||||
/douyin\.com\/user\/([a-zA-Z0-9_-]+)/,
|
||||
],
|
||||
},
|
||||
bili: {
|
||||
video: [
|
||||
/bilibili\.com\/video\/(BV[a-zA-Z0-9]+)/,
|
||||
/bilibili\.com\/video\/(av\d+)/,
|
||||
/b23\.tv\/([a-zA-Z0-9]+)/,
|
||||
],
|
||||
creator: [
|
||||
/space\.bilibili\.com\/(\d+)/,
|
||||
],
|
||||
},
|
||||
wb: {
|
||||
video: [
|
||||
/weibo\.com\/\d+\/([a-zA-Z0-9]+)/,
|
||||
/m\.weibo\.cn\/status\/(\d+)/,
|
||||
],
|
||||
creator: [
|
||||
/weibo\.com\/u\/(\d+)/,
|
||||
/weibo\.com\/([a-zA-Z0-9]+)$/,
|
||||
],
|
||||
},
|
||||
ks: {
|
||||
video: [
|
||||
/kuaishou\.com\/short-video\/([a-zA-Z0-9_-]+)/,
|
||||
/v\.kuaishou\.com\/([a-zA-Z0-9]+)/,
|
||||
],
|
||||
creator: [
|
||||
/kuaishou\.com\/profile\/([a-zA-Z0-9_-]+)/,
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export function parseUrl(input: string, platform: string): ParsedId {
|
||||
const trimmed = input.trim()
|
||||
|
||||
// If it's just an ID (no URL structure), return as unknown type
|
||||
if (!trimmed.includes('/') && !trimmed.includes('.')) {
|
||||
return {
|
||||
id: trimmed,
|
||||
type: 'unknown',
|
||||
original: trimmed,
|
||||
isValid: trimmed.length > 0,
|
||||
}
|
||||
}
|
||||
|
||||
const patterns = platformPatterns[platform]
|
||||
if (!patterns) {
|
||||
return {
|
||||
id: trimmed,
|
||||
type: 'unknown',
|
||||
original: trimmed,
|
||||
isValid: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Try video patterns
|
||||
for (const pattern of patterns.video) {
|
||||
const match = trimmed.match(pattern)
|
||||
if (match && match[1]) {
|
||||
return {
|
||||
id: match[1],
|
||||
type: 'video',
|
||||
original: trimmed,
|
||||
isValid: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try creator patterns
|
||||
for (const pattern of patterns.creator) {
|
||||
const match = trimmed.match(pattern)
|
||||
if (match && match[1]) {
|
||||
return {
|
||||
id: match[1],
|
||||
type: 'creator',
|
||||
original: trimmed,
|
||||
isValid: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try to extract any ID-like segment
|
||||
const urlMatch = trimmed.match(/([a-zA-Z0-9_-]{6,})/)
|
||||
if (urlMatch) {
|
||||
return {
|
||||
id: urlMatch[1],
|
||||
type: 'unknown',
|
||||
original: trimmed,
|
||||
isValid: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: trimmed,
|
||||
type: 'unknown',
|
||||
original: trimmed,
|
||||
isValid: false,
|
||||
}
|
||||
}
|
||||
|
||||
export function parseMultipleUrls(input: string, platform: string): ParsedId[] {
|
||||
if (!input.trim()) return []
|
||||
|
||||
const items = input
|
||||
.split(/[,\n]+/)
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
return items.map(item => parseUrl(item, platform))
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
export function formatDateTime(date: string | Date | number): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : typeof date === 'number' ? new Date(date * 1000) : date
|
||||
return d.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user