mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-04 12:36:37 +08:00
fix: 修复站点管理API Key保存问题,移动RSS字段到主表单,修复Discover页面提示
This commit is contained in:
@@ -49,11 +49,70 @@ func (h *SiteHandler) GetSite(c *gin.Context) {
|
||||
|
||||
// CreateSite 创建站点。
|
||||
func (h *SiteHandler) CreateSite(c *gin.Context) {
|
||||
var site model.Site
|
||||
if err := c.ShouldBindJSON(&site); err != nil {
|
||||
var body map[string]any
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var site model.Site
|
||||
// 手动映射字段(因为敏感字段有 json:"-" 标签,不会自动反序列化)
|
||||
if v, ok := body["name"].(string); ok {
|
||||
site.Name = v
|
||||
}
|
||||
if v, ok := body["url"].(string); ok {
|
||||
site.URL = v
|
||||
}
|
||||
if v, ok := body["type"].(string); ok {
|
||||
site.Type = v
|
||||
}
|
||||
if v, ok := body["auth_type"].(string); ok {
|
||||
site.AuthType = v
|
||||
}
|
||||
if v, ok := body["api_key"].(string); ok {
|
||||
site.APIKey = v
|
||||
}
|
||||
if v, ok := body["cookie"].(string); ok {
|
||||
site.Cookie = v
|
||||
}
|
||||
if v, ok := body["auth_header"].(string); ok {
|
||||
site.AuthHeader = v
|
||||
}
|
||||
if v, ok := body["enabled"].(bool); ok {
|
||||
site.Enabled = v
|
||||
}
|
||||
if v, ok := body["is_default"].(bool); ok {
|
||||
site.IsDefault = v
|
||||
}
|
||||
if v, ok := body["extra"].(string); ok {
|
||||
site.Extra = v
|
||||
}
|
||||
// 高级设置字段
|
||||
if v, ok := body["user_agent"].(string); ok {
|
||||
site.UserAgent = v
|
||||
}
|
||||
if v, ok := body["rss_url"].(string); ok {
|
||||
site.RSSURL = v
|
||||
}
|
||||
if v, ok := body["timeout"].(float64); ok {
|
||||
site.Timeout = int(v)
|
||||
}
|
||||
if v, ok := body["priority"].(float64); ok {
|
||||
site.Priority = int(v)
|
||||
}
|
||||
if v, ok := body["use_proxy"].(bool); ok {
|
||||
site.UseProxy = v
|
||||
}
|
||||
if v, ok := body["rate_limit"].(bool); ok {
|
||||
site.RateLimit = v
|
||||
}
|
||||
if v, ok := body["browser_emulation"].(bool); ok {
|
||||
site.BrowserEmulation = v
|
||||
}
|
||||
if v, ok := body["downloader"].(string); ok {
|
||||
site.Downloader = v
|
||||
}
|
||||
|
||||
if err := h.svc.Site.Create(c.Request.Context(), &site); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
@@ -68,6 +127,8 @@ func (h *SiteHandler) UpdateSite(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
// 字段名映射:前端使用蛇形命名,GORM 会正确映射到列名
|
||||
// 无需额外处理,Updates 直接使用 patch 中的 key-value
|
||||
if err := h.svc.Site.Update(c.Request.Context(), c.Param("id"), patch); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
|
||||
@@ -29,8 +29,8 @@ export function DiscoverPage() {
|
||||
<Sparkles className="h-6 w-6 text-primary-400" />
|
||||
<div>
|
||||
<h1 className="font-display text-3xl font-bold text-white">发现</h1>
|
||||
<p className="text-sm text-slate-400">
|
||||
来自 TMDb 的当日热门与流行榜单(需在 secrets.tmdb_api_key 配置 API Key)。
|
||||
<p className="text-sm text-slate-400">
|
||||
来自 TMDb 的当日热门与流行榜单(需在外部 API 中配置 TMDb API Key)。
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
@@ -41,9 +41,9 @@ export function DiscoverPage() {
|
||||
{popular.length > 0 && <Row title="热门电影" items={popular} />}
|
||||
{!loading && trending.length === 0 && popular.length === 0 && (
|
||||
<div className="glass-panel">
|
||||
<p className="text-slate-300">
|
||||
还没有可用的发现数据。前往 <span className="text-primary-400">管理后台 → 设置</span>
|
||||
,在 secrets.tmdb_api_key 中填写一个有效的 TMDb API Key。
|
||||
<p className="text-slate-300">
|
||||
还没有可用的发现数据。请前往 <span className="text-primary-400">管理后台 → 外部API</span>
|
||||
,配置 TMDb API Key 后刷新页面。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+46
-42
@@ -35,53 +35,57 @@ export function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full items-center justify-center px-4 py-10">
|
||||
<form onSubmit={handleSubmit} className="glass-panel w-full max-w-md">
|
||||
<div className="mb-8 flex flex-col items-center gap-2">
|
||||
<Film className="h-10 w-10 text-primary-400" />
|
||||
<h1 className="font-display text-2xl font-bold tracking-wide text-white">
|
||||
MediaStationGo
|
||||
</h1>
|
||||
<p className="text-sm text-slate-400">登录到你的家庭媒体中心</p>
|
||||
</div>
|
||||
<div className="flex min-h-screen flex-col items-center px-4">
|
||||
{/* 表单居中区域:flex-grow + flex center 使表单在剩余空间内垂直居中 */}
|
||||
<div className="flex flex-1 flex-col items-center justify-center">
|
||||
<form onSubmit={handleSubmit} className="glass-panel w-full max-w-md">
|
||||
<div className="mb-8 flex flex-col items-center gap-2">
|
||||
<Film className="h-10 w-10 text-primary-400" />
|
||||
<h1 className="font-display text-2xl font-bold tracking-wide text-white">
|
||||
MediaStationGo
|
||||
</h1>
|
||||
<p className="text-sm text-slate-400">登录到你的家庭媒体中心</p>
|
||||
</div>
|
||||
|
||||
<label className="mb-4 block">
|
||||
<span className="mb-1 block text-sm text-slate-300">用户名</span>
|
||||
<input
|
||||
className="input-base"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoComplete="username"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label className="mb-4 block">
|
||||
<span className="mb-1 block text-sm text-slate-300">用户名</span>
|
||||
<input
|
||||
className="input-base"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoComplete="username"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="mb-6 block">
|
||||
<span className="mb-1 block text-sm text-slate-300">密码</span>
|
||||
<input
|
||||
type="password"
|
||||
className="input-base"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label className="mb-6 block">
|
||||
<span className="mb-1 block text-sm text-slate-300">密码</span>
|
||||
<input
|
||||
type="password"
|
||||
className="input-base"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="neon-button w-full justify-center"
|
||||
>
|
||||
{loading ? '登录中…' : '登录'}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="neon-button w-full justify-center"
|
||||
>
|
||||
{loading ? '登录中…' : '登录'}
|
||||
</button>
|
||||
|
||||
<p className="mt-6 text-center text-xs text-slate-500">
|
||||
首次部署默认账号:<code className="text-primary-400">admin / admin123</code>
|
||||
</p>
|
||||
</form>
|
||||
<p className="mt-6 text-center text-xs text-slate-500">
|
||||
首次部署默认账号:<code className="text-primary-400">admin / admin123</code>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<AppFooter className="mt-8" />
|
||||
{/* 页脚始终在底部 */}
|
||||
<AppFooter className="mb-4 mt-auto" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { FormEvent, useEffect, useState } from 'react'
|
||||
import { FolderOpen, Loader2, Save, SettingsIcon } from 'lucide-react'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { AppFooter } from '../components/AppFooter'
|
||||
import { adminAPI } from '../api/admin'
|
||||
import { libraryAPI } from '../api/library'
|
||||
import type { Library, Setting } from '../types'
|
||||
@@ -340,8 +339,6 @@ export function SettingsPage() {
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AppFooter className="mt-6" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+186
-33
@@ -51,6 +51,15 @@ const defaultForm = () => ({
|
||||
enabled: true,
|
||||
is_default: false,
|
||||
extra: '',
|
||||
// 高级设置
|
||||
user_agent: '',
|
||||
rss_url: '',
|
||||
timeout: 15,
|
||||
priority: 50,
|
||||
use_proxy: false,
|
||||
rate_limit: false,
|
||||
browser_emulation: false,
|
||||
downloader: '',
|
||||
})
|
||||
|
||||
export function SitesPage() {
|
||||
@@ -103,6 +112,15 @@ export function SitesPage() {
|
||||
enabled: s.enabled !== false,
|
||||
is_default: s.is_default || false,
|
||||
extra: s.extra || '',
|
||||
// 高级设置
|
||||
user_agent: s.user_agent || '',
|
||||
rss_url: s.rss_url || '',
|
||||
timeout: s.timeout ?? 15,
|
||||
priority: s.priority ?? 50,
|
||||
use_proxy: s.use_proxy || false,
|
||||
rate_limit: s.rate_limit || false,
|
||||
browser_emulation: s.browser_emulation || false,
|
||||
downloader: s.downloader || '',
|
||||
})
|
||||
setAdvancedOpen(false)
|
||||
setShowModal(true)
|
||||
@@ -116,49 +134,69 @@ export function SitesPage() {
|
||||
setEditingId(null)
|
||||
}
|
||||
|
||||
// ── 保存 ──
|
||||
const handleSave = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!form.name.trim() || !form.url.trim()) {
|
||||
toast.error('站点名称和地址不能为空')
|
||||
return
|
||||
// ── 保存(支持静默模式)──
|
||||
const silentSave = async (): Promise<boolean> => {
|
||||
if (!form.name.trim() || !form.url.trim()) return false
|
||||
const payload: Record<string, unknown> = {
|
||||
name: form.name.trim(),
|
||||
url: form.url.trim(),
|
||||
type: form.type,
|
||||
auth_type: form.auth_type,
|
||||
cookie: form.cookie || '',
|
||||
api_key: form.api_key || '',
|
||||
auth_header: form.auth_header || '',
|
||||
enabled: form.enabled,
|
||||
is_default: form.is_default,
|
||||
extra: form.extra || '',
|
||||
user_agent: form.user_agent || '',
|
||||
rss_url: form.rss_url || '',
|
||||
timeout: Number(form.timeout) || 15,
|
||||
priority: Number(form.priority) || 50,
|
||||
use_proxy: !!form.use_proxy,
|
||||
rate_limit: !!form.rate_limit,
|
||||
browser_emulation: !!form.browser_emulation,
|
||||
downloader: form.downloader || '',
|
||||
}
|
||||
setSaving(true)
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
name: form.name.trim(),
|
||||
url: form.url.trim(),
|
||||
type: form.type,
|
||||
auth_type: form.auth_type,
|
||||
cookie: form.cookie || '',
|
||||
api_key: form.api_key || '',
|
||||
auth_header: form.auth_header || '',
|
||||
enabled: form.enabled,
|
||||
is_default: form.is_default,
|
||||
extra: form.extra || '',
|
||||
}
|
||||
|
||||
if (editingId) {
|
||||
await sitesAPI.update(editingId, payload)
|
||||
toast.success('站点已更新')
|
||||
} else {
|
||||
await sitesAPI.create(payload)
|
||||
toast.success('站点已添加')
|
||||
const res = await sitesAPI.create(payload)
|
||||
setEditingId((res.data as Site)?.id ?? null)
|
||||
}
|
||||
closeModal()
|
||||
await loadSites()
|
||||
} catch (err: unknown) {
|
||||
const msg =
|
||||
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
|
||||
'保存失败'
|
||||
toast.error(msg)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ── 测试 ──
|
||||
const handleSave = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
setSaving(true)
|
||||
const ok = await silentSave()
|
||||
if (ok) {
|
||||
toast.success(editingId ? '站点已更新' : '站点已添加')
|
||||
closeModal()
|
||||
await loadSites()
|
||||
} else {
|
||||
const msg = '保存失败'
|
||||
toast.error(msg)
|
||||
}
|
||||
setSaving(false)
|
||||
}
|
||||
|
||||
// 测试(先自动保存)
|
||||
const handleTest = async (id: string) => {
|
||||
// 如果正在编辑当前站点,先保存
|
||||
if (editingId === id) {
|
||||
setSaving(true)
|
||||
const ok = await silentSave()
|
||||
setSaving(false)
|
||||
if (!ok) {
|
||||
toast.error('保存失败,无法测试')
|
||||
return
|
||||
}
|
||||
}
|
||||
setTestingId(id)
|
||||
try {
|
||||
const res = await sitesAPI.test(id)
|
||||
@@ -247,6 +285,33 @@ export function SitesPage() {
|
||||
{site.is_default && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-primary-400/15 text-primary-400">默认</span>
|
||||
)}
|
||||
{site.use_proxy && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-blue-500/15 text-blue-400">代理</span>
|
||||
)}
|
||||
{site.rate_limit && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-yellow-500/15 text-yellow-400">限流</span>
|
||||
)}
|
||||
{site.browser_emulation && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-purple-500/15 text-purple-400">浏览器</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 状态与统计(只读) */}
|
||||
<div className="text-xs text-slate-500 space-y-0.5">
|
||||
<div>状态:
|
||||
<span className={
|
||||
site.login_status === 'ok' ? 'text-green-400' :
|
||||
site.login_status === 'failed' ? 'text-red-400' : 'text-slate-400'
|
||||
}>
|
||||
{site.login_status || 'unknown'}
|
||||
</span>
|
||||
</div>
|
||||
{(site.upload_bytes || 0) > 0 && (
|
||||
<div>↑ {Math.round((site.upload_bytes ?? 0) / 1073741824 * 100) / 100} GB / ↓ {Math.round((site.download_bytes ?? 0) / 1073741824 * 100) / 100} GB</div>
|
||||
)}
|
||||
{site.priority !== 50 && (
|
||||
<div>优先级:{site.priority}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
@@ -466,6 +531,18 @@ export function SitesPage() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* RSS 地址(主表单) */}
|
||||
<div className="mt-4">
|
||||
<label className="block text-xs text-slate-400 mb-1">RSS 地址</label>
|
||||
<input
|
||||
className="input-base w-full text-xs font-mono"
|
||||
placeholder="https://.../torrents/rss?..."
|
||||
value={form.rss_url}
|
||||
onChange={(e) => setForm((f) => ({ ...f, rss_url: e.target.value }))}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-1">站点 RSS 订阅地址,用于获取最新资源</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 高级选项 */}
|
||||
@@ -479,6 +556,82 @@ export function SitesPage() {
|
||||
</button>
|
||||
{advancedOpen && (
|
||||
<div className="mt-3 pl-4 space-y-3 border-l border-white/10">
|
||||
{/* 新增高级字段 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-slate-400 mb-1">User-Agent</label>
|
||||
<input
|
||||
className="input-base w-full text-xs"
|
||||
placeholder="自定义 UA,留空使用默认"
|
||||
value={form.user_agent}
|
||||
onChange={(e) => setForm((f) => ({ ...f, user_agent: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-400 mb-1">请求超时 (秒)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input-base w-full"
|
||||
min={1}
|
||||
max={300}
|
||||
value={form.timeout}
|
||||
onChange={(e) => setForm((f) => ({ ...f, timeout: Number(e.target.value) }))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-400 mb-1">优先级 (数字越大越优先)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input-base w-full"
|
||||
min={1}
|
||||
max={100}
|
||||
value={form.priority}
|
||||
onChange={(e) => setForm((f) => ({ ...f, priority: Number(e.target.value) }))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-400 mb-1">关联下载器</label>
|
||||
<input
|
||||
className="input-base w-full text-xs"
|
||||
placeholder="下载器 ID 或名称"
|
||||
value={form.downloader}
|
||||
onChange={(e) => setForm((f) => ({ ...f, downloader: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 开关类字段 */}
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-4 w-4 accent-primary-400"
|
||||
checked={form.use_proxy}
|
||||
onChange={(e) => setForm((f) => ({ ...f, use_proxy: e.target.checked }))}
|
||||
/>
|
||||
<span className="text-xs text-slate-300">使用代理</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-4 w-4 accent-primary-400"
|
||||
checked={form.rate_limit}
|
||||
onChange={(e) => setForm((f) => ({ ...f, rate_limit: e.target.checked }))}
|
||||
/>
|
||||
<span className="text-xs text-slate-300">启用限流</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-4 w-4 accent-primary-400"
|
||||
checked={form.browser_emulation}
|
||||
onChange={(e) => setForm((f) => ({ ...f, browser_emulation: e.target.checked }))}
|
||||
/>
|
||||
<span className="text-xs text-slate-300">浏览器模拟</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Extra JSON */}
|
||||
<div>
|
||||
<label className="block text-xs text-slate-400 mb-1">Extra 扩展配置 (JSON)</label>
|
||||
<textarea
|
||||
|
||||
Reference in New Issue
Block a user