diff --git a/admin/src/views/SeriesManage.vue b/admin/src/views/SeriesManage.vue
index f9c3783..2d221ca 100644
--- a/admin/src/views/SeriesManage.vue
+++ b/admin/src/views/SeriesManage.vue
@@ -58,8 +58,30 @@
-
-
+
+
+
+
+
![]()
+
上传中…
+
+ 选择图片
+
+
移除
+
+
+
+
@@ -93,11 +115,38 @@ const categories = ref([])
const dialogVisible = ref(false)
const editing = ref(null)
const submitting = ref(false)
+const coverUploading = ref(false)
const form = reactive({
title: '', cover_url: '', description: '', category_id: 0,
})
+async function uploadCover(file) {
+ if (!file || !file.raw) return
+ const allowed = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']
+ if (!allowed.includes(file.raw.type)) {
+ return ElMessage.error('仅支持 jpg/png/webp/gif')
+ }
+ if (file.raw.size > 5 * 1024 * 1024) {
+ return ElMessage.error('图片不能超过 5MB')
+ }
+ coverUploading.value = true
+ try {
+ const fd = new FormData()
+ fd.append('file', file.raw)
+ const r = await request({
+ url: '/admin/upload/image', method: 'post', data: fd,
+ headers: { 'Content-Type': 'multipart/form-data' },
+ })
+ form.cover_url = r.data.url
+ ElMessage.success('封面上传成功')
+ } catch (e) {
+ ElMessage.error(e?.message || '上传失败')
+ } finally {
+ coverUploading.value = false
+ }
+}
+
onMounted(async () => {
load()
try {
@@ -181,4 +230,15 @@ async function del(row) {
.header { display: flex; justify-content: space-between; align-items: center; }
.title-col { display: flex; align-items: center; gap: 8px; }
.cover { width: 40px; height: 40px; border-radius: 4px; object-fit: cover; }
+.cover-upload { display: flex; align-items: center; }
+.cover-preview {
+ width: 120px; height: 70px; border-radius: 6px; overflow: hidden;
+ border: 1px dashed #dcdfe6; cursor: pointer; position: relative;
+ display: flex; align-items: center; justify-content: center;
+}
+.cover-preview img { width: 100%; height: 100%; object-fit: cover; }
+.cover-preview .mask {
+ position: absolute; inset: 0; background: rgba(0,0,0,.6);
+ color: #fff; font-size: 12px; display: flex; align-items: center; justify-content: center;
+}
diff --git a/server/app/api/v1/__pycache__/admin.cpython-311.pyc b/server/app/api/v1/__pycache__/admin.cpython-311.pyc
index e525698..793f291 100644
Binary files a/server/app/api/v1/__pycache__/admin.cpython-311.pyc and b/server/app/api/v1/__pycache__/admin.cpython-311.pyc differ
diff --git a/server/app/api/v1/admin.py b/server/app/api/v1/admin.py
index 2c8cc09..2509a56 100644
--- a/server/app/api/v1/admin.py
+++ b/server/app/api/v1/admin.py
@@ -174,6 +174,48 @@ async def upload_video_file(request: Request, token: str = "",
})
+@router.post("/upload/image")
+async def upload_admin_image(request: Request,
+ admin: AdminIdentity = Depends(require_super)):
+ """admin 通用图片上传:封面/头像/系列图等。存到 runtime/uploads/images/{YYYYMMDD}/,
+ 返回 h5 域名可直接访问的完整 URL。"""
+ from fastapi import UploadFile, HTTPException
+ from datetime import datetime
+ from pathlib import Path
+ from uuid import uuid4
+
+ form = await request.form()
+ files = [v for v in form.values() if hasattr(v, "filename") and v.filename]
+ if not files:
+ raise HTTPException(400, "缺少 file 字段")
+ upload: UploadFile = files[0]
+
+ allowed = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
+ suffix = Path(upload.filename or "").suffix.lower()
+ if suffix not in allowed:
+ raise HTTPException(400, f"仅支持 {','.join(sorted(allowed))}")
+
+ PROJECT_ROOT = Path("/root/project/video")
+ day = datetime.now().strftime("%Y%m%d")
+ out_dir = PROJECT_ROOT / "runtime" / "uploads" / "images" / day
+ out_dir.mkdir(parents=True, exist_ok=True)
+ file_name = f"{uuid4().hex}{suffix}"
+ file_path = out_dir / file_name
+
+ size = 0
+ with open(file_path, "wb") as f:
+ while True:
+ chunk = await upload.read(1024 * 1024)
+ if not chunk:
+ break
+ size += len(chunk)
+ f.write(chunk)
+ await upload.close()
+
+ url = f"https://h5.256kb.top/uploads/images/{day}/{file_name}"
+ return ok({"url": url, "file_size": size})
+
+
# ---------- 用户 / 积分 ----------
@router.get("/users")