From ab88e600d2aa184db1ed71ba2c097725bd218ea4 Mon Sep 17 00:00:00 2001 From: Jesse <15653378+squarezw@user.noreply.gitee.com> Date: Thu, 17 Sep 2026 17:23:21 +0800 Subject: [PATCH] chore: remove SOP management module Drop the unused SOP UI, APIs, i18n, and related seed/schema artifacts so the product surface matches the retirement of this feature. Co-authored-by: Cursor --- app/components/AppSidebar.tsx | 2 - app/components/AuthGate.tsx | 1 - app/components/DynamicTitle.tsx | 1 - app/sop/page.tsx | 1189 ------------------------ docs/assets/quickStart/schema.sql | 160 ---- docs/assets/quickStart/seed.sql | 4 - docs/troubleshooting.md | 9 +- messages/en/navigation.json | 1 - messages/en/sop.json | 41 - messages/zh-CN/navigation.json | 1 - messages/zh-CN/sop.json | 41 - pages/api/oss/[...key].ts | 2 +- pages/api/sop/category.ts | 51 - pages/api/sop/detail.ts | 64 -- pages/api/sop/subcategory.ts | 71 -- pages/api/sop/subcategory_vectorize.ts | 124 --- pages/api/sop/upload-confirm.ts | 21 - scripts/migrate-local-files-to-oss.ts | 109 +-- types/i18n.d.ts | 1 - 19 files changed, 5 insertions(+), 1888 deletions(-) delete mode 100644 app/sop/page.tsx delete mode 100644 messages/en/sop.json delete mode 100644 messages/zh-CN/sop.json delete mode 100644 pages/api/sop/category.ts delete mode 100644 pages/api/sop/detail.ts delete mode 100644 pages/api/sop/subcategory.ts delete mode 100644 pages/api/sop/subcategory_vectorize.ts delete mode 100644 pages/api/sop/upload-confirm.ts diff --git a/app/components/AppSidebar.tsx b/app/components/AppSidebar.tsx index 982b8ca..89dae64 100644 --- a/app/components/AppSidebar.tsx +++ b/app/components/AppSidebar.tsx @@ -39,7 +39,6 @@ import { Plus, Search, Settings, - ShieldCheck, Smartphone, Sparkles, User, @@ -126,7 +125,6 @@ export default function AppSidebar() { path: "/skills", }, { title: t("toolsManagement"), icon: Wrench, path: "/tools", visible: canManageTools }, - { title: t("sopManagement"), icon: ShieldCheck, path: "/sop", visible: canManageOperation }, { title: t("skuManagement"), icon: Package, diff --git a/app/components/AuthGate.tsx b/app/components/AuthGate.tsx index b48a564..f4561f9 100644 --- a/app/components/AuthGate.tsx +++ b/app/components/AuthGate.tsx @@ -14,7 +14,6 @@ const TOKEN_KEY = "ragent_token"; // 不需要认证的公开路径 const PUBLIC_PATHS = [ /^\/feedback\/[A-Za-z0-9_-]+$/, // 反馈页面(加密): /feedback/{token} - /^\/sop-images\//, // SOP 图片 /^\/forgot-password$/, // 忘记密码页面 /^\/reset-password$/, // 重置密码页面 ]; diff --git a/app/components/DynamicTitle.tsx b/app/components/DynamicTitle.tsx index 71d6df2..fe5554d 100644 --- a/app/components/DynamicTitle.tsx +++ b/app/components/DynamicTitle.tsx @@ -15,7 +15,6 @@ const routeTitleKeys: { [key: string]: string } = { "/search": "search", "/graph": "knowledgeGraph", "/process-management": "processManagement", - "/sop": "sopManagement", "/organization": "organization", "/user": "userManagement", "/settings": "settings", diff --git a/app/sop/page.tsx b/app/sop/page.tsx deleted file mode 100644 index aa52098..0000000 --- a/app/sop/page.tsx +++ /dev/null @@ -1,1189 +0,0 @@ -"use client"; -import { useEffect, useState, useRef } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { - Edit, - Trash2, - Rocket, - Plus, - Pencil, - ChevronLeft, - ChevronRight, - FileText, - Image, - Settings, - AlertCircle, -} from "lucide-react"; - -import { Label } from "@/components/ui/label"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import * as Dialog from "@radix-ui/react-dialog"; -import { useCurrentUser } from "@/hooks/useCurrentUser"; -import { checkSuperAdmin, checkTenantAdmin } from "@/lib/clientPermissions"; -import axios from "@/lib/axios"; -import { uploadFile } from "@/lib/ossUpload"; -import { useTranslations } from "next-intl"; - -interface Category { - id: number; - name: string; -} -interface SubCategory { - id: number; - category_id: number; - name: string; - vector_status?: string; - embedding_model?: string; - type?: string; -} - -interface SopDetail { - id: number; - subcategory_id: number; - step_number: string; - image_url: string | null; - content: string; - vector_status?: string; -} - -export default function SopPage() { - const t = useTranslations("sop"); - const tc = useTranslations("common"); - const { user, loading } = useCurrentUser(); - const isSuperAdmin = checkSuperAdmin(user); - const isTenantAdmin = checkTenantAdmin(user); - const isDeptAdmin = user?.isDeptAdmin || false; - const canManageSOP = isSuperAdmin || isTenantAdmin || isDeptAdmin; - - const [categories, setCategories] = useState([]); - const [subcategories, setSubcategories] = useState([]); - const [selectedCategory, setSelectedCategory] = useState(null); - const [newCategory, setNewCategory] = useState(""); - const [newSubCategory, setNewSubCategory] = useState(""); - const [newSubCategoryType, setNewSubCategoryType] = useState("process"); - const [editingCategory, setEditingCategory] = useState(null); - const [editingSubCategory, setEditingSubCategory] = useState(null); - const [editCategoryName, setEditCategoryName] = useState(""); - const [editSubCategoryName, setEditSubCategoryName] = useState(""); - const [editSubCategoryType, setEditSubCategoryType] = useState("process"); - const [selectedSubCategory, setSelectedSubCategory] = useState(null); - const [details, setDetails] = useState([]); - const [processDetails, setProcessDetails] = useState([]); - const [isoDetails, setIsoDetails] = useState([]); - const [newDetail, setNewDetail] = useState<{ - step_number: string; - content: string; - image: File | null; - }>({ step_number: "", content: "", image: null }); - const [editingDetail, setEditingDetail] = useState(null); - const [editDetail, setEditDetail] = useState<{ - step_number: string; - content: string; - image: File | null; - }>({ step_number: "", content: "", image: null }); - const [showAddDetailDialog, setShowAddDetailDialog] = useState(false); - const [addDetailError, setAddDetailError] = useState(""); - const [isAddingDetail, setIsAddingDetail] = useState(false); - const [showImagePreview, setShowImagePreview] = useState(false); - const [previewImage, setPreviewImage] = useState<{ url: string; alt: string } | null>(null); - const fileInputRef = useRef(null); - const editFileInputRef = useRef(null); - const [vectorizingId, setVectorizingId] = useState(null); - const [vectorizeProgress, setVectorizeProgress] = useState<{ [id: number]: number }>({}); - const [vectorizePolling, setVectorizePolling] = useState<{ [id: number]: boolean }>({}); - const [subcatVectorStatus, setSubcatVectorStatus] = useState("pending"); - const [subcatVectorizing, setSubcatVectorizing] = useState(false); - const scrollContainerRef = useRef(null); - - // 加载行业大类 - const fetchCategories = async () => { - const res = await axios.get("/api/sop/category"); - setCategories(res.data.categories || []); - if (!selectedCategory && res.data.categories?.length > 0) { - setSelectedCategory(res.data.categories[0]); - } - }; - // 加载小类 - const fetchSubCategories = async (categoryId: number) => { - const res = await axios.get(`/api/sop/subcategory?category_id=${categoryId}`); - setSubcategories(res.data.subcategories || []); - // 自动同步当前选中子类的 vector_status - if (selectedSubCategory) { - const found = res.data.subcategories.find((s: any) => s.id === selectedSubCategory.id); - if (found) setSubcatVectorStatus(found.vector_status || "pending"); - } - }; - - useEffect(() => { - fetchCategories(); - }, []); - useEffect(() => { - if (selectedCategory) fetchSubCategories(selectedCategory.id); - }, [selectedCategory]); - - // 轮询子类向量化状态 - useEffect(() => { - if (!selectedSubCategory) return; - const polling = true; - const poll = async () => { - const res = await axios.get(`/api/sop/subcategory?category_id=${selectedCategory?.id}`); - const found = res.data.subcategories.find((s: any) => s.id === selectedSubCategory.id); - if (found) setSubcatVectorStatus(found.vector_status || "pending"); - if (found && found.vector_status === "processing") { - setTimeout(poll, 1000); - } else { - setSubcatVectorizing(false); - fetchDetails(selectedSubCategory.id); - } - }; - if (subcatVectorizing) poll(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [subcatVectorizing, selectedSubCategory]); - - // 联动加载细项 - useEffect(() => { - if (!selectedCategory) { - setDetails([]); - setProcessDetails([]); - setIsoDetails([]); - return; - } - if (!selectedSubCategory) { - // 选中大类但未选子类,加载所有子类的 SOP 细项 - const loadAllDetails = async () => { - let allDetails: SopDetail[] = []; - let processDetailsList: SopDetail[] = []; - let isoDetailsList: SopDetail[] = []; - - for (const sub of subcategories) { - try { - const res = await axios.get(`/api/sop/detail?subcategory_id=${sub.id}`); - if (res.data.details) { - allDetails = allDetails.concat(res.data.details); - // 根据子类类型分类 - if (sub.type === "iso") { - isoDetailsList = isoDetailsList.concat(res.data.details); - } else { - processDetailsList = processDetailsList.concat(res.data.details); - } - } - } catch (error) { - console.error("加载子类详情失败:", error); - } - } - setDetails(allDetails); - setProcessDetails(processDetailsList); - setIsoDetails(isoDetailsList); - }; - loadAllDetails(); - } else { - fetchDetails(selectedSubCategory.id); - // 如果选中了特定子类,根据其类型设置对应的详情 - if (selectedSubCategory.type === "iso") { - setIsoDetails(details); - setProcessDetails([]); - } else { - setProcessDetails(details); - setIsoDetails([]); - } - } - }, [selectedCategory, selectedSubCategory, subcategories]); - - const fetchDetails = async (subcategoryId: number) => { - try { - const res = await axios.get(`/api/sop/detail?subcategory_id=${subcategoryId}`); - const detailsData = res.data.details || []; - setDetails(detailsData); - - // 根据选中的子类类型更新对应的详情列表 - if (selectedSubCategory) { - if (selectedSubCategory.type === "iso") { - setIsoDetails(detailsData); - setProcessDetails([]); - } else { - setProcessDetails(detailsData); - setIsoDetails([]); - } - } - } catch (error) { - console.error("获取详情失败:", error); - setDetails([]); - setProcessDetails([]); - setIsoDetails([]); - } - }; - - // 新增行业大类 - const handleAddCategory = async () => { - if (!newCategory.trim()) return; - await axios.post("/api/sop/category", { name: newCategory }); - setNewCategory(""); - fetchCategories(); - }; - // 删除行业大类 - const handleDeleteCategory = async (id: number) => { - await axios.delete("/api/sop/category", { data: { id } }); - if (selectedCategory?.id === id) setSelectedCategory(null); - fetchCategories(); - }; - // 编辑行业大类 - const handleEditCategory = (cat: Category) => { - setEditingCategory(cat); - setEditCategoryName(cat.name); - }; - const handleSaveEditCategory = async () => { - if (!editingCategory) return; - await axios.put("/api/sop/category", { id: editingCategory.id, name: editCategoryName }); - setEditingCategory(null); - setEditCategoryName(""); - fetchCategories(); - }; - - // 新增小类 - const handleAddSubCategory = async () => { - if (!newSubCategory.trim() || !selectedCategory) return; - await axios.post("/api/sop/subcategory", { - category_id: selectedCategory.id, - name: newSubCategory, - type: newSubCategoryType, - }); - setNewSubCategory(""); - setNewSubCategoryType("process"); - fetchSubCategories(selectedCategory.id); - }; - // 删除小类 - const handleDeleteSubCategory = async (id: number) => { - await axios.delete("/api/sop/subcategory", { data: { id } }); - fetchSubCategories(selectedCategory!.id); - }; - // 编辑小类 - const handleEditSubCategory = (sub: SubCategory) => { - setEditingSubCategory(sub); - setEditSubCategoryName(sub.name); - setEditSubCategoryType(sub.type || "process"); - }; - const handleSaveEditSubCategory = async () => { - if (!editingSubCategory) return; - await axios.put("/api/sop/subcategory", { - id: editingSubCategory.id, - name: editSubCategoryName, - type: editSubCategoryType, - }); - setEditingSubCategory(null); - setEditSubCategoryName(""); - setEditSubCategoryType("process"); - fetchSubCategories(selectedCategory!.id); - }; - - // 新增细项 - const handleAddDetail = async () => { - if (!selectedSubCategory || !newDetail.step_number.trim() || !newDetail.content.trim()) - return false; - - setIsAddingDetail(true); - setAddDetailError(""); - - try { - let image_url = null; - if (newDetail.image) { - const objectKey = await uploadFile({ file: newDetail.image, category: "sop-images" }); - const res = await axios.post("/api/sop/upload-confirm", { objectKey }); - image_url = res.data.url ? res.data.url : null; - } - - await axios.post("/api/sop/detail", { - subcategory_id: selectedSubCategory.id, - step_number: newDetail.step_number, - image_url: image_url ?? null, - content: newDetail.content, - }); - - setNewDetail({ step_number: "", content: "", image: null }); - if (fileInputRef.current) fileInputRef.current.value = ""; - fetchDetails(selectedSubCategory.id); - // 重新加载子类数据以更新类型信息 - fetchSubCategories(selectedCategory?.id || 0); - return true; // 返回成功状态 - } catch (error: any) { - console.error("添加详情失败:", error); - setAddDetailError(error.response?.data?.error || "添加失败,请重试"); - return false; - } finally { - setIsAddingDetail(false); - } - }; - // 删除细项 - const handleDeleteDetail = async (id: number) => { - await axios.delete("/api/sop/detail", { data: { id } }); - if (selectedSubCategory) fetchDetails(selectedSubCategory.id); - // 重新加载子类数据以更新类型信息 - fetchSubCategories(selectedCategory?.id || 0); - }; - // 编辑细项 - const handleEditDetail = (detail: SopDetail) => { - setEditingDetail(detail); - setEditDetail({ step_number: detail.step_number, content: detail.content, image: null }); - }; - const handleSaveEditDetail = async () => { - if (!editingDetail || !editDetail.step_number.trim() || !editDetail.content.trim()) return; - let image_url = editingDetail.image_url; - if (editDetail.image) { - const objectKey = await uploadFile({ file: editDetail.image, category: "sop-images" }); - const res = await axios.post("/api/sop/upload-confirm", { objectKey }); - image_url = res.data.url ? res.data.url : null; - } - await axios.put("/api/sop/detail", { - id: editingDetail.id, - step_number: editDetail.step_number, - image_url: image_url ?? null, - content: editDetail.content, - }); - setEditingDetail(null); - setEditDetail({ step_number: "", content: "", image: null }); - if (editFileInputRef.current) editFileInputRef.current.value = ""; - if (selectedSubCategory) fetchDetails(selectedSubCategory.id); - // 重新加载子类数据以更新类型信息 - fetchSubCategories(selectedCategory?.id || 0); - }; - - // 向量化操作 - const handleVectorize = async (id: number) => { - setVectorizingId(id); - setVectorizePolling((prev) => ({ ...prev, [id]: true })); - const res = await axios.post(`/api/sop/vectorize?id=${id}`); - if (res.data.status === "pending" || res.data.status === "processing") { - let polling = true; - const poll = async () => { - const statusRes = await axios.get(`/api/sop/vector_status?id=${id}`); - setVectorizeProgress((prev) => ({ ...prev, [id]: statusRes.data.progress })); - if (statusRes.data.status === "indexed") { - setVectorizingId(null); - setVectorizePolling((prev) => ({ ...prev, [id]: false })); - fetchDetails(selectedSubCategory?.id ?? 0); - polling = false; - return; - } - if (polling) setTimeout(poll, 1000); - }; - poll(); - } else { - setVectorizingId(null); - fetchDetails(selectedSubCategory?.id ?? 0); - } - }; - - // 滚动控制 - const scrollLeft = () => { - if (scrollContainerRef.current) { - scrollContainerRef.current.scrollBy({ left: -300, behavior: "smooth" }); - } - }; - - const scrollRight = () => { - if (scrollContainerRef.current) { - scrollContainerRef.current.scrollBy({ left: 300, behavior: "smooth" }); - } - }; - - // 处理图片点击放大 - const handleImageClick = (imageUrl: string, alt: string) => { - setPreviewImage({ url: imageUrl, alt }); - setShowImagePreview(true); - }; - - return ( -
- {/* 顶部下拉联动选择 */} -
- - - {canManageSOP && ( - - )} - {/* 新增/编辑大类 Dialog */} - !v && setEditingCategory(null)}> - - - - - {editingCategory?.id ? t("editCategory") : t("newCategory")} - - setEditCategoryName(e.target.value)} - autoFocus - /> -
- - -
-
-
-
- -
- { - if (v === "__add__") { - setEditingSubCategory({ id: 0, category_id: selectedCategory?.id || 0, name: "" }); - return; - } - setSelectedSubCategory(subcategories.find((s) => String(s.id) === v) || null); - }} - > - - - {t("all")} - - {subcategories.map((sub) => ( - -
- {sub.name} -
-
- ))} - {canManageSOP && - (selectedSubCategory ? ( - { - e.preventDefault(); - e.stopPropagation(); - setEditingSubCategory(selectedSubCategory); - setEditSubCategoryName(selectedSubCategory.name); - }} - tabIndex={-1} - style={{ pointerEvents: "auto" }} - > - - - ) : ( - - - - ))} -
-
-
- {/* 新增/编辑小类 Dialog */} - !v && setEditingSubCategory(null)} - > - - - - - {editingSubCategory?.id ? t("editSubCategory") : t("newSubCategory")} - - setEditSubCategoryName(e.target.value)} - autoFocus - /> - -
- - -
-
-
-
-
- - {/* 上半部分:图片横排+文字+icon,可左右滑动 */} - - -
- - - {t("workflowProcess")} - -
- {selectedSubCategory && selectedSubCategory.type === "process" && canManageSOP && ( - - )} - {selectedSubCategory && canManageSOP && ( - - )} -
-
-
- - {details.length > 0 ? ( -
- {/* 滚动按钮 */} - - - - {/* 可滑动的工序卡片容器 */} -
- {processDetails.map((detail, index) => ( -
- {/* 编辑模式 */} - {editingDetail && editingDetail.id === detail.id ? ( -
-
- - setEditDetail((d) => ({ ...d, step_number: e.target.value })) - } - type="text" - className="w-32 font-semibold text-lg" - placeholder={t("processNumberPlaceholder")} - /> -
- - -
-
- -
-