refactor: 将 WebUI 源码纳入仓库,移除打包产物

- 新增 webui/ 前端源码目录(React+Vite+TypeScript+Tailwind)
- 删除 api/webui/ 中旧的打包静态资源
- 更新 .gitignore 忽略 api/webui/ 构建输出和 webui/node_modules
- 更新 README 增加前端构建说明
This commit is contained in:
程序员阿江(Relakkes)
2026-07-01 23:05:03 +08:00
parent 2ac0716c41
commit 65ddaa8828
68 changed files with 9064 additions and 356 deletions
@@ -0,0 +1,80 @@
import { useTranslation } from 'react-i18next'
import { Sparkles, Heart } from 'lucide-react'
export function AuthorFooter() {
const { t } = useTranslation('license')
return (
<footer className="h-24 flex-shrink-0 glass-panel border-t border-cyber-border-subtle">
<div className="h-full px-6 flex items-center justify-center gap-6">
{/* Author Avatar */}
<div className="w-14 h-14 rounded-lg overflow-hidden border-2 border-cyber-neon-cyan/60 flex-shrink-0 shadow-glow-cyan-sm">
<img
src="/logos/my_logo.png"
alt="程序员阿江-Relakkes"
className="w-full h-full object-cover"
/>
</div>
{/* Author Info */}
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="text-lg font-bold text-cyber-text-primary">
{t('author.name')}
</span>
<Sparkles className="w-5 h-5 text-cyber-neon-cyan animate-pulse" />
</div>
<span className="text-sm text-cyber-text-muted hidden sm:inline">
{t('author.description')}
</span>
<div className="flex items-center gap-2 text-cyber-neon-cyan">
<Heart className="w-4 h-4 fill-current animate-pulse" />
<span className="text-sm font-medium">
{t('author.slogan')}
</span>
</div>
</div>
{/* Social Links */}
<div className="flex items-center gap-3">
<a
href="https://github.com/NanmiCoder"
target="_blank"
rel="noopener noreferrer"
className="w-11 h-11 rounded-lg flex items-center justify-center border border-cyber-border-subtle hover:border-cyber-neon-cyan hover:shadow-glow-cyan-sm transition-all bg-cyber-bg-tertiary hover:scale-110"
title="GitHub"
>
<img src="/logos/github.png" alt="GitHub" className="w-6 h-6 object-contain" />
</a>
<a
href="https://space.bilibili.com/434377496"
target="_blank"
rel="noopener noreferrer"
className="w-11 h-11 rounded-lg flex items-center justify-center border border-cyber-border-subtle hover:border-pink-400 hover:shadow-[0_0_10px_rgba(251,113,133,0.4)] transition-all bg-cyber-bg-tertiary hover:scale-110"
title="哔哩哔哩"
>
<img src="/logos/bilibili_logo.png" alt="Bilibili" className="w-6 h-6 object-contain" />
</a>
<a
href="https://www.xiaohongshu.com/user/profile/5f58bd990000000001003753"
target="_blank"
rel="noopener noreferrer"
className="w-11 h-11 rounded-lg flex items-center justify-center border border-cyber-border-subtle hover:border-red-400 hover:shadow-[0_0_10px_rgba(248,113,113,0.4)] transition-all bg-cyber-bg-tertiary hover:scale-110"
title="小红书"
>
<img src="/logos/xiaohongshu_logo.png" alt="小红书" className="w-6 h-6 object-contain" />
</a>
<a
href="https://www.douyin.com/user/MS4wLjABAAAATJPY7LAlaa5X-c8uNdWkvz0jUGgpw4eeXIwu_8BhvqE"
target="_blank"
rel="noopener noreferrer"
className="w-11 h-11 rounded-lg flex items-center justify-center border border-cyber-border-subtle hover:border-cyber-text-primary hover:shadow-[0_0_10px_rgba(255,255,255,0.3)] transition-all bg-cyber-bg-tertiary hover:scale-110"
title="抖音"
>
<img src="/logos/douyin.png" alt="抖音" className="w-6 h-6 object-contain" />
</a>
</div>
</div>
</footer>
)
}
@@ -0,0 +1,36 @@
import { Globe } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
const languages = [
{ code: 'zh-CN', label: '中文' },
{ code: 'en-US', label: 'EN' },
]
export function LanguageSwitch() {
const { i18n } = useTranslation()
const currentLang = languages.find(l => l.code === i18n.language) || languages[0]
return (
<Select value={i18n.language} onValueChange={(lang) => i18n.changeLanguage(lang)}>
<SelectTrigger className="w-20 h-7 text-xs font-mono border-cyber-border-subtle bg-cyber-bg-tertiary/50 hover:border-cyber-neon-cyan/50 transition-colors">
<Globe className="w-3 h-3 mr-1 text-cyber-text-secondary" />
<SelectValue>{currentLang.label}</SelectValue>
</SelectTrigger>
<SelectContent>
{languages.map((lang) => (
<SelectItem key={lang.code} value={lang.code} className="text-xs font-mono">
{lang.label}
</SelectItem>
))}
</SelectContent>
</Select>
)
}
@@ -0,0 +1,13 @@
import { Terminal } from '@/components/console/Terminal'
import { useLogWebSocket } from '@/hooks/useWebSocket'
export function MainContent() {
// Connect to WebSocket for logs
useLogWebSocket()
return (
<main className="flex-1 flex flex-col overflow-hidden min-h-0 relative z-10">
<Terminal />
</main>
)
}
+88
View File
@@ -0,0 +1,88 @@
import { Bug, Wifi, AlertTriangle, Github } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Badge } from '@/components/ui/badge'
import { useCrawlerStore } from '@/store/crawlerStore'
import { useCrawlerStatus } from '@/hooks/useCrawler'
import { LanguageSwitch } from './LanguageSwitch'
import { ThemeToggle } from './ThemeToggle'
interface SidebarProps {
onShowDisclaimer?: () => void
}
export function Sidebar({ onShowDisclaimer }: SidebarProps) {
const { t } = useTranslation()
const { t: tLicense } = useTranslation('license')
const status = useCrawlerStore((state) => state.status)
// Poll status
useCrawlerStatus()
const isRunning = status === 'running'
return (
<header className="h-14 flex-shrink-0 glass-panel border-b border-cyber-border-subtle relative z-10">
<div className="h-full px-4 flex items-center justify-between">
{/* Left: Logo and GitHub Star */}
<div className="flex items-center gap-3">
<Bug className="w-5 h-5 text-cyber-neon-cyan" />
<span className="font-mono font-bold text-cyber-text-primary tracking-wider text-sm">
MediaCrawler
</span>
<a
href="https://github.com/NanmiCoder/MediaCrawler"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 px-2 py-1 rounded-md border border-cyber-border-subtle hover:border-cyber-neon-cyan hover:shadow-glow-cyan-sm transition-all bg-cyber-bg-tertiary"
>
<Github className="w-4 h-4 text-cyber-text-secondary" />
<span className="text-xs font-mono text-cyber-text-secondary">Star</span>
</a>
{isRunning && (
<Badge variant="running" className="text-[10px]">
{t('status.active')}
</Badge>
)}
{isRunning && (
<span className="w-2 h-2 bg-cyber-neon-green rounded-full shadow-glow-green-sm animate-pulse-fast" />
)}
</div>
{/* Center: Warning Text */}
<button
onClick={onShowDisclaimer}
className="flex items-center gap-3 px-4 py-1.5 rounded-lg border border-cyber-neon-orange/50 bg-cyber-neon-orange/10 hover:bg-cyber-neon-orange/20 transition-all cursor-pointer"
>
<AlertTriangle className="w-4 h-4 text-cyber-neon-orange flex-shrink-0" />
<div className="flex items-center gap-4 text-xs font-mono">
<span className="text-cyber-neon-orange">
<span className="text-cyber-neon-pink font-bold">1.</span> {tLicense('content.line1')}
</span>
<span className="text-cyber-neon-orange">
<span className="text-cyber-neon-pink font-bold">2.</span> {tLicense('content.line2')}
</span>
</div>
</button>
{/* Right: Actions and Status */}
<div className="flex items-center gap-3">
{/* Theme Toggle */}
<ThemeToggle />
{/* Language Switch */}
<LanguageSwitch />
{/* Status Info */}
<div className="hidden lg:flex items-center gap-2 text-xs font-mono">
<span className="text-cyber-text-muted">{t('sidebar.api')}:</span>
<span className="text-cyber-neon-green">v1.0.0</span>
<div className="flex items-center gap-1.5">
<Wifi className="w-3 h-3 text-cyber-text-secondary" />
<span className="text-cyber-text-secondary">{t('sidebar.local')}</span>
<span className="status-dot status-dot-online" />
</div>
</div>
</div>
</div>
</header>
)
}
@@ -0,0 +1,43 @@
import { Sun, Moon, Monitor } from 'lucide-react'
import { useThemeStore } from '@/store/themeStore'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
type Theme = 'light' | 'dark' | 'system'
const themes: { value: Theme; label: string; icon: typeof Sun }[] = [
{ value: 'light', label: 'Light', icon: Sun },
{ value: 'dark', label: 'Dark', icon: Moon },
{ value: 'system', label: 'Auto', icon: Monitor },
]
export function ThemeToggle() {
const { theme, setTheme } = useThemeStore()
const currentTheme = themes.find(t => t.value === theme) || themes[0]
const Icon = currentTheme.icon
return (
<Select value={theme} onValueChange={(value: Theme) => setTheme(value)}>
<SelectTrigger className="w-20 h-7 text-xs font-mono border-cyber-border-subtle bg-cyber-bg-tertiary/50 hover:border-cyber-neon-cyan/50 transition-colors">
<Icon className="w-3 h-3 mr-1 text-cyber-text-secondary" />
<SelectValue>{currentTheme.label}</SelectValue>
</SelectTrigger>
<SelectContent>
{themes.map(({ value, label, icon: ItemIcon }) => (
<SelectItem key={value} value={value} className="text-xs font-mono">
<div className="flex items-center gap-2">
<ItemIcon className="w-3 h-3" />
{label}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
)
}