From a7b2cbfc8c836ad59ccd6428a7ffc8560924258c Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 13 Jul 2026 22:24:37 +0800 Subject: [PATCH 01/17] =?UTF-8?q?feat(products):=20=E6=88=90=E7=89=87?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=20UI=204=20=E5=A4=A7=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 封面预览 — 卡片已支持 thumbnailUrl 展示 2. 复核状态标签 — 右上角显示待复核(灰)/已通过(绿)/需修改(红),点击循环切换 3. 批量操作 — 批量下载改用 batch-download API + job_id 轮询 4. 筛选栏 — 新增按项目、按复核状态筛选 Select API 层新增: - updateReviewStatus: PATCH /products/{id}/review - batchDownload: POST /products/batch-download - getBatchDownloadStatus: GET /products/batch-download/{jobId} Co-Authored-By: Claude Fable 5 --- apps/web/src/api/products.ts | 76 ++++++- .../web/src/pages/products/ProductLibrary.tsx | 200 ++++++++++++++++-- apps/web/src/pages/products/products.css | 36 ++++ 3 files changed, 289 insertions(+), 23 deletions(-) diff --git a/apps/web/src/api/products.ts b/apps/web/src/api/products.ts index 8b5dc4375..0e89d4926 100644 --- a/apps/web/src/api/products.ts +++ b/apps/web/src/api/products.ts @@ -1,9 +1,12 @@ /** - * 成品相关 API - * Phase 1 重构:去掉 projectId,成品直接归属用户 + * 成品 / 视频相关 API + * 包含:列表查询、复核状态、批量下载 */ import apiClient from "./client"; +/** 复核状态 */ +export type ReviewStatus = "pending_review" | "approved" | "rejected"; + /** 成品条目 */ export interface ProductItem { id: string; @@ -14,15 +17,49 @@ export interface ProductItem { file_size?: number; resolution?: string; status: "processing" | "completed" | "failed"; + /** 复核状态 */ + review_status?: ReviewStatus; + /** 所属项目 ID */ + project_id?: string; + /** 所属项目名称 */ + project_name?: string; /** 查重率(百分比) */ duplicate_rate?: number; created_at?: string; updated_at?: string; } -/** 获取当前用户的所有成品 */ -export const getProducts = async (): Promise => { - const response = await apiClient.get("/products"); +/** 列表查询参数 */ +export interface ProductListParams { + page?: number; + page_size?: number; + project_id?: string; + review_status?: ReviewStatus | "all"; +} + +/** 分页响应 */ +export interface ProductListResponse { + items: ProductItem[]; + total: number; + page: number; + page_size: number; +} + +/** 批量下载任务状态 */ +export interface BatchDownloadStatus { + job_id: string; + status: "processing" | "completed" | "failed"; + /** 完成后返回的下载 URL */ + download_url?: string; + /** 进度百分比 */ + progress?: number; +} + +/** 获取成品列表(支持分页和筛选) */ +export const getProducts = async ( + params?: ProductListParams, +): Promise => { + const response = await apiClient.get("/products", { params }); return response.data.items || response.data || []; }; @@ -44,3 +81,32 @@ export const getProductDownloadUrl = async ( const response = await apiClient.get(`/products/${productId}/download-url`); return response.data; }; + +/** 更新复核状态 */ +export const updateReviewStatus = async ( + productId: string, + status: ReviewStatus, +): Promise => { + const response = await apiClient.patch(`/products/${productId}/review`, { + status, + }); + return response.data; +}; + +/** 发起批量下载 */ +export const batchDownload = async ( + videoIds: string[], +): Promise<{ job_id: string }> => { + const response = await apiClient.post("/products/batch-download", { + video_ids: videoIds, + }); + return response.data; +}; + +/** 查询批量下载状态 */ +export const getBatchDownloadStatus = async ( + jobId: string, +): Promise => { + const response = await apiClient.get(`/products/batch-download/${jobId}`); + return response.data; +}; diff --git a/apps/web/src/pages/products/ProductLibrary.tsx b/apps/web/src/pages/products/ProductLibrary.tsx index ae02e827b..4ab5dbc00 100644 --- a/apps/web/src/pages/products/ProductLibrary.tsx +++ b/apps/web/src/pages/products/ProductLibrary.tsx @@ -31,7 +31,11 @@ import { getProducts, deleteProduct, getProductDownloadUrl, + updateReviewStatus, + batchDownload, + getBatchDownloadStatus, type ProductItem as ApiProductItem, + type ReviewStatus, } from "@/api/products"; import "./products.css"; @@ -53,6 +57,12 @@ interface ProductItem { fileSize: number; // MB videoUrl?: string; thumbnailUrl?: string; + /** 复核状态 */ + reviewStatus?: ReviewStatus; + /** 所属项目 ID */ + projectId?: string; + /** 所属项目名称 */ + projectName?: string; } /** 将后端 status 映射为前端 ProductStatus */ @@ -108,6 +118,9 @@ const mapApiProduct = (item: ApiProductItem): ProductItem => ({ fileSize: item.file_size ? +(item.file_size / (1024 * 1024)).toFixed(1) : 0, videoUrl: item.video_url, thumbnailUrl: item.thumbnail_url, + reviewStatus: item.review_status, + projectId: item.project_id, + projectName: item.project_name, }); /* ============================================================ @@ -135,6 +148,30 @@ const formatSize = (mb: number): string => { return `${mb.toFixed(1)} MB`; }; +/** 复核状态配置 */ +const reviewStatusConfig: Record< + ReviewStatus, + { text: string; className: string } +> = { + pending_review: { text: "待复核", className: "review-pending" }, + approved: { text: "已通过", className: "review-approved" }, + rejected: { text: "需修改", className: "review-rejected" }, +}; + +/** 复核状态循环顺序 */ +const REVIEW_STATUS_CYCLE: ReviewStatus[] = [ + "pending_review", + "approved", + "rejected", +]; + +/** 获取下一个复核状态 */ +const getNextReviewStatus = (current?: ReviewStatus): ReviewStatus => { + if (!current) return "approved"; + const idx = REVIEW_STATUS_CYCLE.indexOf(current); + return REVIEW_STATUS_CYCLE[(idx + 1) % REVIEW_STATUS_CYCLE.length]; +}; + /* ============================================================ * ProductCard 组件 * ============================================================ */ @@ -148,6 +185,7 @@ const ProductCard: React.FC<{ onShare: (product: ProductItem) => void; onDelete: (id: string) => void; onPublish: (product: ProductItem) => void; + onReviewStatusChange: (id: string) => void; }> = ({ product, isSelected, @@ -158,6 +196,7 @@ const ProductCard: React.FC<{ onShare, onDelete, onPublish, + onReviewStatusChange, }) => { const st = statusConfig[product.status]; @@ -201,6 +240,33 @@ const ProductCard: React.FC<{ {/* 已发布徽章(右上角) */} {product.isPublished &&
✅ 已发布
} + {/* 复核状态标签(右上角) */} + {product.reviewStatus && ( +
{ + e.stopPropagation(); + onReviewStatusChange(product.id); + }} + title={`点击切换复核状态(当前:${reviewStatusConfig[product.reviewStatus].text})`} + > + {reviewStatusConfig[product.reviewStatus].text} +
+ )} + {/* 无复核状态时显示"待复核"入口 */} + {!product.reviewStatus && ( +
{ + e.stopPropagation(); + onReviewStatusChange(product.id); + }} + title="点击设置复核状态" + > + 待复核 +
+ )} + {/* 缩略图 */}
{product.thumbnailUrl ? ( @@ -517,7 +583,7 @@ const ProductLibrary: React.FC = () => { refetch, } = useQuery({ queryKey: ["products"], - queryFn: getProducts, + queryFn: () => getProducts(), staleTime: 30_000, }); @@ -536,11 +602,26 @@ const ProductLibrary: React.FC = () => { }, }); + /* ── 复核状态 mutation ── */ + const reviewMutation = useMutation({ + mutationFn: ({ id, status }: { id: string; status: ReviewStatus }) => + updateReviewStatus(id, status), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["products"] }); + message.success("复核状态已更新"); + }, + onError: () => { + message.error("更新复核状态失败"); + }, + }); + /* 筛选 */ const [searchText, setSearchText] = useState(""); const [filterStatus, setFilterStatus] = useState("all"); const [filterTime, setFilterTime] = useState("all"); const [filterDuration, setFilterDuration] = useState("all"); + const [filterProject, setFilterProject] = useState("all"); + const [filterReviewStatus, setFilterReviewStatus] = useState("all"); /* 批量操作 */ const [selectedIds, setSelectedIds] = useState>(new Set()); @@ -596,6 +677,20 @@ const ProductLibrary: React.FC = () => { }); } + /* 项目筛选 */ + if (filterProject !== "all") { + list = list.filter((p) => p.projectId === filterProject); + } + + /* 复核状态筛选 */ + if (filterReviewStatus !== "all") { + if (filterReviewStatus === "none") { + list = list.filter((p) => !p.reviewStatus); + } else { + list = list.filter((p) => p.reviewStatus === filterReviewStatus); + } + } + /* 搜索 */ if (searchText.trim()) { const q = searchText.trim().toLowerCase(); @@ -603,7 +698,15 @@ const ProductLibrary: React.FC = () => { } return list; - }, [products, filterStatus, filterTime, filterDuration, searchText]); + }, [ + products, + filterStatus, + filterTime, + filterDuration, + filterProject, + filterReviewStatus, + searchText, + ]); /* 全选 */ const allSelected = @@ -679,24 +782,56 @@ const ProductLibrary: React.FC = () => { message.info("发布功能待后端 API 补齐"); }; - /* 批量下载 */ + /* 切换复核状态 */ + const handleReviewStatusChange = (id: string) => { + const current = products.find((p) => p.id === id)?.reviewStatus; + const nextStatus = getNextReviewStatus(current); + reviewMutation.mutate({ id, status: nextStatus }); + }; + + /* 批量下载 — 使用 batch-download API + 轮询 */ + const [batchDownloading, setBatchDownloading] = useState(false); + const handleBatchDownload = async () => { const ids = Array.from(selectedIds); - let successCount = 0; - for (const id of ids) { - try { - const { url } = await getProductDownloadUrl(id); - const a = document.createElement("a"); - a.href = url; - a.download = ""; - a.click(); - successCount++; - } catch { - // 忽略单个失败 - } + if (ids.length === 0) return; + setBatchDownloading(true); + try { + // 发起批量下载任务 + const { job_id } = await batchDownload(ids); + message.info(`批量下载任务已创建,正在打包 ${ids.length} 个视频...`); + + // 轮询下载状态(最多 60 次,每次 2 秒) + let attempts = 0; + const maxAttempts = 60; + const poll = async (): Promise => { + if (attempts >= maxAttempts) { + message.warning("打包超时,请稍后在消息中心查看"); + return; + } + attempts++; + await new Promise((r) => setTimeout(r, 2000)); + const status = await getBatchDownloadStatus(job_id); + if (status.status === "completed" && status.download_url) { + const a = document.createElement("a"); + a.href = status.download_url; + a.download = ""; + a.click(); + message.success(`已打包下载 ${ids.length} 个视频`); + setSelectedIds(new Set()); + } else if (status.status === "failed") { + message.error("批量下载失败,请重试"); + } else { + // 继续轮询 + await poll(); + } + }; + await poll(); + } catch { + message.error("发起批量下载失败"); + } finally { + setBatchDownloading(false); } - message.success(`已下载 ${successCount}/${ids.length} 个视频`); - setSelectedIds(new Set()); }; /* 批量删除 */ @@ -823,8 +958,9 @@ const ProductLibrary: React.FC = () => { buttonSize="sm" icon={} onClick={handleBatchDownload} + disabled={batchDownloading} > - 批量下载 + {batchDownloading ? "打包中..." : "批量下载"}
{ onShare={handleShare} onDelete={handleDelete} onPublish={handlePublish} + onReviewStatusChange={handleReviewStatusChange} /> ))}
diff --git a/apps/web/src/pages/products/products.css b/apps/web/src/pages/products/products.css index 4b3b15ece..6945e006e 100644 --- a/apps/web/src/pages/products/products.css +++ b/apps/web/src/pages/products/products.css @@ -222,6 +222,42 @@ backdrop-filter: blur(4px); } +/* 复核状态标签(右上角,位于已发布徽章下方) */ +.xx-product-review-tag { + position: absolute; + top: 34px; + right: 10px; + z-index: 2; + padding: 2px 10px; + border-radius: var(--radius-full); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + backdrop-filter: blur(4px); + cursor: pointer; + transition: var(--transition-all); + user-select: none; +} + +.xx-product-review-tag:hover { + transform: scale(1.05); + filter: brightness(1.1); +} + +.xx-product-review-tag.review-pending { + background: rgba(156, 163, 175, 0.9); + color: var(--text-inverse); +} + +.xx-product-review-tag.review-approved { + background: rgba(16, 185, 129, 0.9); + color: var(--text-inverse); +} + +.xx-product-review-tag.review-rejected { + background: rgba(239, 68, 68, 0.9); + color: var(--text-inverse); +} + /* 缩略图区域 */ .xx-product-thumb { aspect-ratio: 9 / 16; From 27681c785a54d2470966ad409c8edc26ce0eeab5 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Wed, 15 Jul 2026 08:36:16 +0800 Subject: [PATCH 02/17] =?UTF-8?q?fix(ci):=20=E4=BD=BF=E7=94=A8=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=20ESLint/Prettier/TypeScript=20=E4=BA=8C=E8=BF=9B?= =?UTF-8?q?=E5=88=B6=EF=BC=8C=E7=BB=95=E8=BF=87=E6=8D=9F=E5=9D=8F=E7=9A=84?= =?UTF-8?q?=20npm=20cache=20=E5=8D=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Install 步骤: rm -rf node_modules (非 /*) + --cache /tmp/npm-cache 绕过 Docker 卷缓存 - ESLint: npx eslint → ./node_modules/.bin/eslint (避免 npx 下载 eslint@10 不兼容 flat config) - Prettier: npx prettier → ./node_modules/.bin/prettier - TypeScript: npx tsc → ./node_modules/.bin/tsc --- .gitea/workflows/ci-cd.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index 0f214247b..fbac7c66a 100644 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -371,16 +371,16 @@ jobs: ' - name: Install dependencies shell: sh - run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'rm -rf node_modules/* 2>/dev/null; npm ci --include=dev'\n" + run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'rm -rf node_modules 2>/dev/null; npm ci --include=dev --cache /tmp/npm-cache'\n" - name: Run ESLint shell: sh - run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'npx eslint src --ext .ts,.tsx --max-warnings 50'\n" + run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc './node_modules/.bin/eslint src --ext .ts,.tsx --max-warnings 50'\n" - name: Run TypeScript type check shell: sh - run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'npx tsc --noEmit'\n" + run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc './node_modules/.bin/tsc --noEmit'\n" - name: Run Prettier check shell: sh - run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'npx prettier --check \"src/**/*.{ts,tsx,md}\"'\n" + run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc './node_modules/.bin/prettier --check \"src/**/*.{ts,tsx,md}\"'\n" - name: Run Vitest tests shell: sh run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'ls node_modules/.bin/vitest 2>/dev/null && node_modules/.bin/vitest run src/test || npm exec vitest run src/test'\n" From 1af8c7ae9e14afb4366bc8c9c11cf92972c054ba Mon Sep 17 00:00:00 2001 From: CI Bot Date: Wed, 15 Jul 2026 08:43:30 +0800 Subject: [PATCH 03/17] fix(ci): use npx for prettier since it is not a project dependency Prettier is not listed in apps/web/package.json devDependencies, so ./node_modules/.bin/prettier does not exist after npm ci. Use npx --yes prettier@3 to download and run on-the-fly. --- .gitea/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index fbac7c66a..6e7f254c0 100644 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -380,7 +380,7 @@ jobs: run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc './node_modules/.bin/tsc --noEmit'\n" - name: Run Prettier check shell: sh - run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc './node_modules/.bin/prettier --check \"src/**/*.{ts,tsx,md}\"'\n" + run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'npx --yes prettier@3 --check \"src/**/*.{ts,tsx,md}\"'\n" - name: Run Vitest tests shell: sh run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'ls node_modules/.bin/vitest 2>/dev/null && node_modules/.bin/vitest run src/test || npm exec vitest run src/test'\n" From 7da09bfcfd692ac7243cce00ddeb4ddc17501709 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Wed, 15 Jul 2026 09:49:24 +0800 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20=E6=89=93=E9=80=9A=E5=89=AA?= =?UTF-8?q?=E8=BE=91=E8=AE=A1=E5=88=92=E7=94=9F=E6=88=90=E9=93=BE=E8=B7=AF?= =?UTF-8?q?=20=E2=80=94=20EditingPlanner=20=E7=94=9F=E6=88=90=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E5=AF=B9=E6=8E=A5=E7=9C=9F=E5=AE=9E=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 生成按钮调用 generateEditPlan(planId) 而非跳转一键生成页面 - 进度弹窗 + 轮询 getGenerationStatus(每2秒)+ 视频结果展示 - 列表页编辑按钮正确加载计划数据到编辑器(planId URL参数) - 生成历史弹窗正常工作 - 列表页状态自动同步(已有5秒 refetch) - 修复所有 TypeScript 编译错误(tsc 零错误) --- .../pages/editing-planner/EditingPlanner.tsx | 482 +++++++++++++++--- 1 file changed, 400 insertions(+), 82 deletions(-) diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx index e1f6f26d0..189ec08f4 100755 --- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx +++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx @@ -3,8 +3,8 @@ * 四行布局:顶栏(42px) → 模式栏(48px) → 三栏主体 → 底栏(40px) */ import React, { useState, useCallback, useEffect, useRef } from "react"; -import { useSearchParams, useNavigate } from "react-router-dom"; -import { message } from "antd"; +import { useSearchParams } from "react-router-dom"; +import { message, Modal, Progress, Button } from "antd"; import { useQuery } from "@tanstack/react-query"; import type { EditingTemplate, @@ -20,11 +20,23 @@ import { getTemplateCategories, MODE_LABELS, } from "@/api/editingPlanner"; -import type { EditPlanGeneration, MediaAsset } from "@/api/editPlans"; +import type { + EditPlanGeneration, + EditPlanConfig, + GeneratedVideo, + MediaAsset, + TransitionEffect, +} from "@/api/editPlans"; import { getMediaAssets, getEditPlanGenerations, generateCover, + getEditPlan, + createEditPlan, + updateEditPlan, + generateEditPlan, + getGenerationStatus, + getGenerationTaskResults, } from "@/api/editPlans"; import { useUndoRedo } from "./hooks/useUndoRedo"; import type { @@ -33,6 +45,7 @@ import type { TransitionConfig, SpeedConfig, TtsConfig, + TtsMode, TrimConfig, WatermarkConfig, IntroOutroConfig, @@ -107,8 +120,8 @@ const FILTER_CATEGORIES = ["全部", "种草", "知识", "日常", "推荐"]; const EditingPlanner: React.FC = () => { const [searchParams] = useSearchParams(); - const navigate = useNavigate(); const urlTemplateId = searchParams.get("templateId") || ""; + const urlPlanId = searchParams.get("planId") || ""; /* ── 模板列表 ── */ const [templates, setTemplates] = useState([]); @@ -244,6 +257,21 @@ const EditingPlanner: React.FC = () => { const [genHistory, setGenHistory] = useState([]); const [genHistoryLoading, setGenHistoryLoading] = useState(false); + /* ── 剪辑计划(从列表页编辑进入时) ── */ + const [loadedPlanId, setLoadedPlanId] = useState( + urlPlanId || null, + ); + + /* ── 生成进度 ── */ + const [generating, setGenerating] = useState(false); + const [genProgress, setGenProgress] = useState(0); + const [genTotalClips, setGenTotalClips] = useState(0); + const [genDoneClips, setGenDoneClips] = useState(0); + const [generated, setGenerated] = useState(false); + const [generatedVideos, setGeneratedVideos] = useState([]); + const [genError, setGenError] = useState(null); + const genTimerRef = useRef | null>(null); + /* ── 播放 ── */ const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); @@ -374,6 +402,85 @@ const EditingPlanner: React.FC = () => { .catch(() => message.error("加载模板详情失败")); }, [loadedTemplateId, resetClips]); + /** + * 加载已有剪辑计划数据到编辑器 + * 从列表页"编辑"按钮进入时,URL 带 planId,需要还原计划配置 + */ + useEffect(() => { + if (!loadedPlanId) return; + getEditPlan(loadedPlanId) + .then((plan) => { + // 设置关联的模板(触发模板加载 effect) + setLoadedTemplateId(plan.template_id); + + // 还原基本信息 + setDraftName(plan.name); + + // 还原 config 中的编辑器状态 + const cfg = plan.config; + if (cfg.title_config) { + setTitleSettings((prev) => ({ + ...prev, + aiAutoSelect: cfg.title_config!.ai_auto_select, + title: cfg.title_config!.content, + position: cfg.title_config!.position, + font: cfg.title_config!.font_preset, + size: cfg.title_config!.font_size, + color: cfg.title_config!.font_color || "#ffffff", + })); + } + if (cfg.subtitle_config) { + setSubtitleSettings((prev) => ({ + ...prev, + enabled: cfg.subtitle_config!.enabled, + position: + (cfg.subtitle_config!.position || "bottom") as SubtitleStyleConfig["position"], + font: cfg.subtitle_config!.font, + fontSize: cfg.subtitle_config!.size, + fontColor: cfg.subtitle_config!.color || "#ffffff", + animation: cfg.subtitle_config!.animation, + })); + } + if (cfg.bgm_config) { + setBgmSettings((prev) => ({ + ...prev, + enabled: cfg.bgm_config!.enabled, + music_id: cfg.bgm_config!.music_id || "", + })); + } + + // 还原片段 — 延迟设置,等模板加载 effect 先执行 resetClips + if (cfg.segments && cfg.segments.length > 0) { + const mapped: ClipData[] = cfg.segments.map((seg, idx) => ({ + id: `seg-${idx}`, + template_segment_id: `seg-${idx}`, + type: (seg.material_type === "voiceover" + ? "voice" + : "pip") as ClipType, + duration: (seg.duration_min + seg.duration_max) / 2, + startOffset: 0, + script_text: "", + order: seg.segment_order, + transition: seg.transition + ? { + type: seg.transition.type as TransitionEffect["type"], + duration: seg.transition.duration, + } + : undefined, + speed: seg.playback_speed + ? { rate: seg.playback_speed, pitchCorrection: true } + : undefined, + tts_config: seg.tts_config + ? { ...seg.tts_config, mode: seg.tts_config.mode as TtsMode } + : undefined, + trim_config: seg.trim_config || undefined, + })); + setTimeout(() => resetClips(mapped), 100); + } + }) + .catch(() => message.error("加载剪辑计划失败")); + }, [loadedPlanId, resetClips]); + /* ──────────── 计算 ──────────── */ const currentTemplate = templates.find((t) => t.id === loadedTemplateId); @@ -671,6 +778,65 @@ const EditingPlanner: React.FC = () => { } }; + /** 构建剪辑计划 config(编辑器状态 → API config) */ + const buildPlanConfig = (): EditPlanConfig => ({ + title_config: { + ai_auto_select: titleSettings.aiAutoSelect, + content: titleSettings.title, + position: titleSettings.position, + font_preset: titleSettings.font, + font_color: titleSettings.color, + font_size: titleSettings.size, + }, + subtitle_config: { + enabled: subtitleSettings.enabled, + position: subtitleSettings.position, + font: subtitleSettings.font, + color: subtitleSettings.fontColor, + size: subtitleSettings.fontSize, + animation: subtitleSettings.animation, + }, + bgm_config: { + enabled: bgmSettings.enabled, + music_id: bgmSettings.music_id, + }, + estimated_duration: totalDuration, + segments: clips.map((c, i) => ({ + segment_order: i, + duration_min: Math.max(1, c.duration - 2), + duration_max: c.duration + 2, + material_type: c.type === "voice" ? "voiceover" : "video", + transition: c.transition + ? { type: c.transition.type, duration: c.transition.duration } + : undefined, + playback_speed: c.speed ? c.speed.rate : undefined, + tts_config: c.tts_config + ? { + mode: c.tts_config.mode, + text: c.tts_config.text, + voice_id: c.tts_config.voice_id, + speed: c.tts_config.speed, + pitch: c.tts_config.pitch, + volume: c.tts_config.volume, + subtitle_sync: c.tts_config.subtitle_sync, + } + : undefined, + trim_config: c.trim_config + ? { + start_time: c.trim_config.start_time, + end_time: c.trim_config.end_time, + } + : undefined, + })), + watermark_config: { ...watermarkSettings }, + intro_outro_config: { ...introOutroSettings }, + pip_config: { ...pipSettings }, + filter_config: { ...filterSettings }, + green_screen_config: { ...chromaKeySettings }, + sticker_config: { ...stickerSettings }, + cover_config: { ...coverSettings }, + }); + /* 保存 — 无论是否已加载模板,都打开保存弹窗;未加载时创建新模板 */ const handleOpenSaveModal = () => { setSaveModalOpen(true); @@ -763,94 +929,139 @@ const EditingPlanner: React.FC = () => { }; /** - * 跳转到一键生成页面 - * 通过 URL SearchParams 传递 edit_plan_id 和完整 planConfig(JSON 序列化) - * 一键生成页面从 params 解析配置,无需重复请求接口 + * 剪辑计划生成 + * 1. 有 planId → 更新计划配置 + 触发生成 + * 2. 无 planId(从模板库直接进入)→ 先创建计划 + 触发生成 + * 3. 触发生成后轮询状态,完成后获取视频结果 */ - const handleGoToGenerate = () => { - const planConfig = { - title_config: { - ai_auto_select: titleSettings.aiAutoSelect, - content: titleSettings.title, - position: titleSettings.position, - font_preset: titleSettings.font, - font_color: titleSettings.color, - font_size: titleSettings.size, - bold: titleSettings.bold, - italic: titleSettings.italic, - stroke: titleSettings.stroke, - shadow: titleSettings.shadow, - }, - subtitle_config: { - enabled: subtitleSettings.enabled, - position: subtitleSettings.position, - font: subtitleSettings.font, - color: subtitleSettings.fontColor, - size: subtitleSettings.fontSize, - animation: subtitleSettings.animation, - }, - bgm_config: { - enabled: bgmSettings.enabled, - music_id: bgmSettings.music_id, - }, - mode: currentMode, - total_duration: totalDuration, - segments: clips.map((c, i) => ({ - order: i, - material_type: c.type === "voice" ? "voiceover" : "video", - duration: c.duration, - template_segment_id: c.template_segment_id, - script_text: c.script_text, - voice_asset_id: c.voice_asset_id, - voice_file_url: c.voice_file_url, - transition: c.transition - ? { type: c.transition.type, duration: c.transition.duration } - : undefined, - playback_speed: c.speed ? c.speed.rate : undefined, - tts_config: c.tts_config - ? { - mode: c.tts_config.mode, - text: c.tts_config.text, - voice_id: c.tts_config.voice_id, - speed: c.tts_config.speed, - pitch: c.tts_config.pitch, - volume: c.tts_config.volume, - subtitle_sync: c.tts_config.subtitle_sync, - } - : undefined, - trim_config: c.trim_config - ? { - start_time: c.trim_config.start_time, - end_time: c.trim_config.end_time, - } - : undefined, - })), - watermark_config: { ...watermarkSettings }, - intro_outro_config: { ...introOutroSettings }, - pip_config: { ...pipSettings }, - filter_config: { ...filterSettings }, - green_screen_config: { ...chromaKeySettings }, - sticker_config: { ...stickerSettings }, - cover_config: { ...coverSettings }, - }; - const params = new URLSearchParams(); - if (loadedTemplateId) { - params.set("edit_plan_id", loadedTemplateId); + const handleGoToGenerate = async () => { + if (!loadedTemplateId) { + message.warning("请先选择一个模板"); + return; + } + if (clips.length === 0) { + message.warning("请先添加片段"); + return; + } + + setGenerating(true); + setGenerated(false); + setGeneratedVideos([]); + setGenError(null); + setGenProgress(0); + + try { + const config = buildPlanConfig(); + let planId = loadedPlanId; + + if (planId) { + // 已有计划 → 更新配置 + await updateEditPlan(planId, { + config, + total_duration: totalDuration, + status: "editing", + }); + } else { + // 无计划 → 创建新计划 + const plan = await createEditPlan({ + template_id: loadedTemplateId, + name: draftName || "未命名计划", + config, + total_duration: totalDuration, + }); + planId = plan.id; + setLoadedPlanId(planId); + // 更新 URL 参数(不刷新页面) + const params = new URLSearchParams(window.location.search); + params.set("planId", planId); + window.history.replaceState(null, "", `?${params.toString()}`); + } + + // 触发生成 + const genRes = await generateEditPlan(planId); + setGenTotalClips(genRes.clip_count); + message.info("已提交生成,等待处理..."); + + // 开始轮询 + startPolling(planId); + } catch (err) { + console.error("[生成失败]", err); + setGenError("生成提交失败,请重试"); + setGenerating(false); } - params.set("plan_config", JSON.stringify(planConfig)); - navigate(`/app/generate?${params.toString()}`); }; + /** 轮询生成状态,每 2 秒一次 */ + const startPolling = (planId: string) => { + const poll = async () => { + try { + const status = await getGenerationStatus(planId); + + // 计算进度 + const total = status.clips.length || genTotalClips; + const done = status.clips.filter( + (c) => c.status === "completed" || c.status === "failed", + ).length; + setGenDoneClips(done); + setGenTotalClips(total); + setGenProgress(total > 0 ? Math.round((done / total) * 100) : 5); + + if (status.plan_status === "completed") { + setGenProgress(100); + setGenerating(false); + setGenerated(true); + + // 获取视频结果 + if (status.generation_task_id) { + try { + const videos = await getGenerationTaskResults( + status.generation_task_id, + ); + setGeneratedVideos(videos); + } catch (e) { + console.error("[获取视频结果失败]", e); + } + } + message.success("视频生成完成!"); + return; // 停止轮询 + } + + if (status.plan_status === "failed") { + setGenerating(false); + setGenError("生成失败,请重试"); + return; // 停止轮询 + } + + // 继续轮询 + genTimerRef.current = setTimeout(poll, 2000); + } catch (err) { + console.error("[轮询状态失败]", err); + genTimerRef.current = setTimeout(poll, 5000); // 出错后 5 秒重试 + } + }; + + // 首次延迟 2 秒后开始 + genTimerRef.current = setTimeout(poll, 2000); + }; + + /** 清理轮询定时器 */ + useEffect(() => { + return () => { + if (genTimerRef.current) clearTimeout(genTimerRef.current); + }; + }, []); + /* 查看生成历史 */ const handleViewGenHistory = async () => { - if (!loadedTemplateId) { - message.warning("请先加载一个模板"); + const targetId = loadedPlanId || loadedTemplateId; + if (!targetId) { + message.warning("请先加载一个模板或计划"); return; } setGenHistoryOpen(true); setGenHistoryLoading(true); try { - const items = await getEditPlanGenerations(loadedTemplateId); + const items = await getEditPlanGenerations(targetId); setGenHistory(items); } catch { message.error("加载生成历史失败"); @@ -899,8 +1110,9 @@ const EditingPlanner: React.FC = () => { @@ -1065,6 +1277,112 @@ const EditingPlanner: React.FC = () => { onClose={() => setGenHistoryOpen(false)} /> + {/* ═══ 生成进度弹窗 ═══ */} + { + setGenerated(false); + setGenerating(false); + }} + > + 关闭 + , + generatedVideos.length > 0 && ( + + ), + ] + : null + } + closable={!generating} + maskClosable={false} + width={520} + > + {generating && ( +
+ +

+ 已处理 {genDoneClips}/{genTotalClips} 个片段 +

+

+ 请耐心等待,生成过程中请勿关闭页面 +

+
+ )} + {generated && generatedVideos.length > 0 && ( +
+
+ )} + {generated && !generatedVideos.length && ( +
+

生成完成,但暂未获取到视频结果

+

+ 请稍后在剪辑计划列表中查看 +

+
+ )} + {genError && ( +
+

{genError}

+ +
+ )} +
+ {/* ═══ BGM 选择器 Drawer ═══ */} Date: Wed, 15 Jul 2026 11:09:07 +0800 Subject: [PATCH 05/17] =?UTF-8?q?chore:=20=E5=90=8C=E6=AD=A5=20ci-cd.yml?= =?UTF-8?q?=20=E5=88=B0=20develop=20=E6=9C=80=E6=96=B0=E7=89=88=E6=9C=AC?= =?UTF-8?q?=EF=BC=88=E8=A7=A3=E5=86=B3=20merge=20=E5=86=B2=E7=AA=81=20+=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Frontend=20Lint=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci-cd.yml | 39 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index 6e7f254c0..049231d4e 100644 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -68,7 +68,7 @@ jobs: python3 -m isort --version-number - python3 -m flake8 --version + python3 -m ruff --version bandit --version @@ -79,23 +79,14 @@ jobs: shell: sh run: "set -eu\necho \"=== Installing detect-secrets ===\"\npython3 -m pip install -q detect-secrets\ndetect-secrets --version\necho \"\"\necho \"=== Running secret scan ===\"\ndetect-secrets scan \\\n --all-files \\\n --exclude-files '(^|/)(tests|test|e2e|__tests__|spec|docs|node_modules|site-packages|migrations|alembic|.gitea|.git|.pytest_cache|.next|dist|build)/' \\\n --exclude-files '\\.(md|rst|txt|lock|example|sample|min\\.js|min\\.css|spec\\.ts|test\\.ts|test\\.py)$' \\\n --exclude-files '(package-lock|yarn\\.lock|poetry\\.lock|Pipfile\\.lock)$' \\\n --disable-plugin Base64HighEntropyString \\\n --disable-plugin HexHighEntropyString \\\n --disable-plugin BasicAuthDetector \\\n --disable-plugin KeywordDetector \\\n --disable-plugin IPPublicDetector \\\n 2>&1 | tee /tmp/secrets-scan.json\n\nFOUND=$(python3 -c \"\nimport json\ntry:\n with open('/tmp/secrets-scan.json') as f:\n data = json.load(f)\n results = data.get('results', {})\n total = sum(len(v) for\ \ v in results.values())\n print(total)\nexcept Exception:\n print('error')\n\")\necho \"\"\necho \"Secrets detected: $FOUND\"\nif [ \"$FOUND\" != \"0\" ] && [ \"$FOUND\" != \"error\" ]; then\n echo \"\"\n echo \"=== Secret details ===\"\n python3 -c \"\nimport json\nwith open('/tmp/secrets-scan.json') as f:\n data = json.load(f)\nfor fpath, items in data.get('results', {}).items():\n for item in items:\n line = item.get('line_number', '?')\n stype = item.get('type', '?')\n hashed = item.get('hashed_secret', '')[:16]\n print(f' {fpath}:{line} [{stype}] {hashed}...')\n\"\n echo \"\"\n echo \"ERROR: Potential secrets detected in code!\"\n echo \"If these are false positives, add exclusions in the CI workflow.\"\n exit 1\nfi\necho \"Secret scan completed - no secrets detected\"\n" + - name: Calculate changed Python files (incremental scan) + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + run: "set -eu\nSCAN_MODE=\"full\"\nCHANGED_PY_FILES=\"\"\n\nif [ \"${GITHUB_EVENT_NAME:-}\" = \"pull_request\" ] && [ -n \"${GITHUB_BASE_REF:-}\" ]; then\n echo \"PR mode (base: ${GITHUB_BASE_REF}) - preparing incremental scan\"\n git init -q\n git config user.email \"ci@localhost\"\n git config user.name \"CI\"\n git add .\n git commit -q -m \"current\"\n REPO_URL=\"https://x-access-token:${GITHUB_TOKEN}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git\"\n git remote add origin \"$REPO_URL\"\n if git fetch origin \"${GITHUB_BASE_REF}\" --depth=50 -q 2>/dev/null; then\n CHANGED_PY_FILES=$(git diff --name-only --diff-filter=ACMRT \"origin/${GITHUB_BASE_REF}\" HEAD -- '*.py' 2>/dev/null | tr '\\n' ' ')\n if [ -n \"$CHANGED_PY_FILES\" ]; then\n SCAN_MODE=\"incremental\"\n FILE_COUNT=$(echo \"$CHANGED_PY_FILES\" | wc -w)\n echo \"Changed Python files: ${FILE_COUNT}\"\n echo \"$CHANGED_PY_FILES\" | tr ' ' '\\n' | grep -v '^$'\n else\n SCAN_MODE=\"skip_py\"\n echo \"No Python files changed in this PR\"\n fi\n else\n echo \"WARN: failed to fetch base branch, falling back to full scan\"\n fi\nelse\n echo \"Full scan mode (not a PR event)\"\nfi\n\necho \"SCAN_MODE=$SCAN_MODE\" >> $GITHUB_ENV\necho \"CHANGED_PY_FILES=$CHANGED_PY_FILES\" >> $GITHUB_ENV\n" - name: Run code quality checks shell: sh - run: 'set -eu - - python3 -m compileall -q alembic apps packages tests scripts - - python3 -m black --check --fast alembic apps packages tests scripts - - python3 -m isort --check-only alembic apps packages tests scripts - - python3 -m flake8 apps packages tests --count --statistics - - ' - - name: Ruff lint (advisory mode - 摸底阶段) - if: always() - shell: sh - run: "set +e\necho \"=== Installing ruff ===\"\npython3 -m pip install -q ruff\nruff --version\necho \"\"\necho \"=== Running ruff lint (advisory mode) ===\"\necho \"告警模式,不阻断CI。用于摸底问题数量,后续分批修复后正式替换flake8。\"\necho \"\"\nruff check apps packages tests scripts --statistics --output-format concise 2>&1 | tail -30\nEXIT_CODE=$?\necho \"\"\nif [ \"$EXIT_CODE\" != \"0\" ]; then\n echo \"ruff 发现 lint 问题(告警模式,不阻断)\"\n echo \"问题分类统计见上方,后续将分批修复\"\nelse\n echo \"ruff 检查全部通过 ✅\"\nfi\nexit 0\n" + run: "set -eu\n\nif [ \"$SCAN_MODE\" = \"incremental\" ]; then\n echo \"=== Incremental scan mode ===\"\n\n python3 -m compileall -q $CHANGED_PY_FILES\n\n python3 -m black --check --fast $CHANGED_PY_FILES\n\n python3 -m isort --check-only $CHANGED_PY_FILES\n\n RUFF_FILES=$(echo \"$CHANGED_PY_FILES\" | tr ' ' '\\n' | grep -v '^scripts/' | tr '\\n' ' ')\n if [ -n \"$RUFF_FILES\" ]; then\n python3 -m ruff check $RUFF_FILES --statistics\n else\n echo \"No ruff-checkable files changed, skipping\"\n fi\n\nelif [ \"$SCAN_MODE\" = \"skip_py\" ]; then\n echo \"No Python files changed - skipping Python lint checks\"\n\nelse\n echo \"=== Full scan mode ===\"\n\n python3 -m compileall -q alembic apps packages tests scripts\n\n python3 -m black --check --fast alembic apps packages tests scripts\n\n python3 -m isort --check-only alembic apps packages tests scripts\n\n python3 -m ruff check apps packages tests --statistics\nfi\n" - name: Type check (mypy, advisory mode) if: always() shell: sh @@ -142,7 +133,7 @@ jobs: shell: sh env: GITHUB_TOKEN: ${{ github.token }} - run: "set -eu\nif command -v git >/dev/null 2>&1; then\n git init -q\n git config user.email \"ci@localhost\"\n git config user.name \"CI\"\n git add .\n git commit -q -m \"current\"\n REPO_URL=\"https://x-access-token:${GITHUB_TOKEN}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git\"\n git remote add origin \"$REPO_URL\"\n git fetch origin main --depth=1 -q 2>/dev/null || echo \"WARN: cannot fetch main, will check all migrations\"\nelse\n echo \"WARN: git not available, will check all migrations\"\nfi\n" + run: "set -eu\nif command -v git >/dev/null 2>&1; then\n if [ ! -d .git ]; then\n git init -q\n git config user.email \"ci@localhost\"\n git config user.name \"CI\"\n git add .\n git commit -q -m \"current\"\n REPO_URL=\"https://x-access-token:${GITHUB_TOKEN}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git\"\n git remote add origin \"$REPO_URL\"\n fi\n git fetch origin main --depth=1 -q 2>/dev/null || echo \"WARN: cannot fetch main, will check all migrations\"\nelse\n echo \"WARN: git not available, will check all migrations\"\nfi\n" - name: Check migration safety shell: sh run: "set -eu\nif git rev-parse origin/main >/dev/null 2>&1; then\n python3 scripts/check_migration_safety.py --allow-medium-risk --diff-against origin/main\nelse\n python3 scripts/check_migration_safety.py --allow-medium-risk\nfi\n" @@ -371,19 +362,19 @@ jobs: ' - name: Install dependencies shell: sh - run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'rm -rf node_modules 2>/dev/null; npm ci --include=dev --cache /tmp/npm-cache'\n" + run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'PACKAGE_LOCK_HASH=$(md5sum package-lock.json 2>/dev/null | cut -d\" \" -f1)\nCACHE_HASH_FILE=\"node_modules/.package-lock-hash\"\nCACHE_VALID=false\nif [ -f \"$CACHE_HASH_FILE\" ] && [ \"$(cat \"$CACHE_HASH_FILE\")\" = \"$PACKAGE_LOCK_HASH\" ] && [ -x \"node_modules/.bin/eslint\" ]; then\n CACHE_VALID=true\n echo \"Cache hit: dependencies valid, skipping npm ci\"\nfi\nif [ \"$CACHE_VALID\" = \"false\" ]; then\n echo \"Cache miss or invalid: running npm ci...\"\n if ! npm ci --include=dev; then\n echo \"npm ci failed, cleaning node_modules and retrying...\"\n rm -rf node_modules\n mkdir -p node_modules\n npm ci --include=dev\n fi\n echo \"$PACKAGE_LOCK_HASH\" > \"$CACHE_HASH_FILE\"\n echo \"Dependencies installed, cache updated\"\nfi'\n" - name: Run ESLint shell: sh - run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc './node_modules/.bin/eslint src --ext .ts,.tsx --max-warnings 50'\n" + run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'npx --no-install eslint src --ext .ts,.tsx --max-warnings 50'\n" - name: Run TypeScript type check shell: sh - run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc './node_modules/.bin/tsc --noEmit'\n" + run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'npx --no-install tsc --noEmit'\n" - name: Run Prettier check shell: sh - run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'npx --yes prettier@3 --check \"src/**/*.{ts,tsx,md}\"'\n" + run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'npx --no-install prettier --check \"src/**/*.{ts,tsx,md}\"'\n" - name: Run Vitest tests shell: sh - run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'ls node_modules/.bin/vitest 2>/dev/null && node_modules/.bin/vitest run src/test || npm exec vitest run src/test'\n" + run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'npx --no-install vitest run src/test'\n" - name: Job duration summary if: always() shell: sh @@ -565,7 +556,7 @@ jobs: run: "set -eu\n# develop/main 分支写回缓存,其他分支只读\nif [ \"${GITHUB_REF_NAME}\" = \"develop\" ] || [ \"${GITHUB_REF_NAME}\" = \"main\" ]; then\n echo \"CACHE_MODE=read-write\" >> $GITHUB_ENV\n echo \"Cache mode: read-write (will push cache)\"\nelse\n echo \"CACHE_MODE=read-only\" >> $GITHUB_ENV\n echo \"Cache mode: read-only\"\nfi\n" - name: Build frontend assets (npm build) shell: sh - run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm -v \"$PWD:/workspace\" -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web docker.m.daocloud.io/library/node:20 sh -lc \"npm ci && npm run build\"\n\ntest -f apps/web/dist/index.html\necho \"Frontend build complete: $(ls apps/web/dist/ | head -5)\"\n" + run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm -v \"$PWD:/workspace\" -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web docker.m.daocloud.io/library/node:20 sh -lc \"npm ci && npx tsc --incremental --tsBuildInfoFile node_modules/.tsbuildinfo && npx vite build\"\n\ntest -f apps/web/dist/index.html\necho \"Frontend build complete: $(ls apps/web/dist/ | head -5)\"\n" - name: Setup buildx builder (docker-container driver) shell: sh run: "set -eu\n# 确保使用 docker-container driver 以支持 cache export 功能\nif ! docker buildx inspect ci-builder > /dev/null 2>&1; then\n docker buildx create --use --name ci-builder --driver docker-container\n echo \"Created ci-builder (docker-container driver)\"\nelse\n docker buildx use ci-builder\n echo \"Using existing ci-builder\"\nfi\ndocker buildx inspect --bootstrap\n" @@ -968,7 +959,7 @@ jobs: ' - name: Build frontend assets (npm build) shell: sh - run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\nfi\n\ndocker run --rm -v \"$PWD:/workspace\" -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web docker.m.daocloud.io/library/node:20 sh -lc \"npm ci && npm run build\"\n\ntest -f apps/web/dist/index.html\necho \"Frontend build complete\"\n" + run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\nfi\n\ndocker run --rm -v \"$PWD:/workspace\" -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web docker.m.daocloud.io/library/node:20 sh -lc \"npm ci && npx tsc --incremental --tsBuildInfoFile node_modules/.tsbuildinfo && npx vite build\"\n\ntest -f apps/web/dist/index.html\necho \"Frontend build complete\"\n" - name: Setup buildx builder (docker-container driver) shell: sh run: "set -eu\n# 确保使用 docker-container driver 以支持 cache export 功能\nif ! docker buildx inspect ci-builder > /dev/null 2>&1; then\n docker buildx create --use --name ci-builder --driver docker-container\n echo \"Created ci-builder (docker-container driver)\"\nelse\n docker buildx use ci-builder\n echo \"Using existing ci-builder\"\nfi\ndocker buildx inspect --bootstrap\n" From 2a4aa468813fe50f87ebdfcaaf8d0ead400e167e Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 15 Jul 2026 11:12:42 +0800 Subject: [PATCH 06/17] =?UTF-8?q?chore:=20=E5=90=8C=E6=AD=A5=20package.jso?= =?UTF-8?q?n=20=E5=88=B0=20develop=EF=BC=88=E8=A1=A5=E5=85=85=20prettier?= =?UTF-8?q?=20=E7=AD=89=E4=BE=9D=E8=B5=96=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/package.json b/apps/web/package.json index 8fe1514e2..313634fa1 100755 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -42,6 +42,7 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", "jsdom": "^24.1.0", + "prettier": "^3.0.0", "typescript": "^5.5.3", "vite": "^5.3.1", "vitest": "^1.6.0" From 5f17a0031890ff9cfd95ba9e3bfdb268cdddaed5 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 15 Jul 2026 11:12:43 +0800 Subject: [PATCH 07/17] =?UTF-8?q?chore:=20=E5=90=8C=E6=AD=A5=20package-loc?= =?UTF-8?q?k.json=20=E5=88=B0=20develop=EF=BC=88=E8=A1=A5=E5=85=85=20prett?= =?UTF-8?q?ier=20=E7=AD=89=E4=BE=9D=E8=B5=96=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/package-lock.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index 0eefe34a8..49eda5fe0 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -33,6 +33,7 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", "jsdom": "^24.1.0", + "prettier": "^3.0.0", "typescript": "^5.5.3", "vite": "^5.3.1", "vitest": "^1.6.0" @@ -4828,6 +4829,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.9.5", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", From 7c67468115b6b21a16d2beb922c2798aa56d8cd7 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 15 Jul 2026 11:21:31 +0800 Subject: [PATCH 08/17] =?UTF-8?q?debug:=20=E5=8A=A0=20node=5Fmodules=20?= =?UTF-8?q?=E8=B0=83=E8=AF=95=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci-cd.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index 049231d4e..2235f3717 100644 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -363,6 +363,17 @@ jobs: - name: Install dependencies shell: sh run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'PACKAGE_LOCK_HASH=$(md5sum package-lock.json 2>/dev/null | cut -d\" \" -f1)\nCACHE_HASH_FILE=\"node_modules/.package-lock-hash\"\nCACHE_VALID=false\nif [ -f \"$CACHE_HASH_FILE\" ] && [ \"$(cat \"$CACHE_HASH_FILE\")\" = \"$PACKAGE_LOCK_HASH\" ] && [ -x \"node_modules/.bin/eslint\" ]; then\n CACHE_VALID=true\n echo \"Cache hit: dependencies valid, skipping npm ci\"\nfi\nif [ \"$CACHE_VALID\" = \"false\" ]; then\n echo \"Cache miss or invalid: running npm ci...\"\n if ! npm ci --include=dev; then\n echo \"npm ci failed, cleaning node_modules and retrying...\"\n rm -rf node_modules\n mkdir -p node_modules\n npm ci --include=dev\n fi\n echo \"$PACKAGE_LOCK_HASH\" > \"$CACHE_HASH_FILE\"\n echo \"Dependencies installed, cache updated\"\nfi'\n" + - name: Debug - check node_modules + shell: sh + run: | + set -eu + NPM_CACHE_VOLUME="xiaoxia-npm-cache" + docker run --rm \ + -v "$PWD:/workspace" \ + -v "$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules" -w /workspace/apps/web \ + docker.m.daocloud.io/library/node:20 \ + sh -lc "echo '=== node_modules/.bin 内容 ===' && ls node_modules/.bin/ | head -30 && echo '=== eslint 版本 ===' && (node_modules/.bin/eslint --version 2>&1 || echo 'eslint not found')" + - name: Run ESLint shell: sh run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'npx --no-install eslint src --ext .ts,.tsx --max-warnings 50'\n" From 3ded5eb9629d59d8a0c11c06d4dbfa259cc03489 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 15 Jul 2026 11:26:56 +0800 Subject: [PATCH 09/17] =?UTF-8?q?style:=20Prettier=20=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=8C=96=20EditingPlanner.tsx=EF=BC=88=E4=BF=AE=E5=A4=8D=20Fro?= =?UTF-8?q?ntend=20Lint=20=E5=A4=B1=E8=B4=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/editing-planner/EditingPlanner.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx index 189ec08f4..aa574a2a5 100755 --- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx +++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx @@ -433,8 +433,8 @@ const EditingPlanner: React.FC = () => { setSubtitleSettings((prev) => ({ ...prev, enabled: cfg.subtitle_config!.enabled, - position: - (cfg.subtitle_config!.position || "bottom") as SubtitleStyleConfig["position"], + position: (cfg.subtitle_config!.position || + "bottom") as SubtitleStyleConfig["position"], font: cfg.subtitle_config!.font, fontSize: cfg.subtitle_config!.size, fontColor: cfg.subtitle_config!.color || "#ffffff", @@ -1336,8 +1336,7 @@ const EditingPlanner: React.FC = () => {