diff --git a/src/components/categoryPanel/CategoryPanel.tsx b/src/components/categoryPanel/CategoryPanel.tsx index 15c515f..bacffb9 100644 --- a/src/components/categoryPanel/CategoryPanel.tsx +++ b/src/components/categoryPanel/CategoryPanel.tsx @@ -6,6 +6,7 @@ import styles from "./CategoryPanel.module.css"; import CategoryService from "@/services/CategoryService.ts"; import Skeleton from "../skeleton/Skeleton"; import { useAsyncResource } from "@/hooks/UseAsyncResource"; +import { useAuth } from "@/contexts/AuthContext"; interface CategoryPanelProps { selectedCategoryId?: string | number; @@ -55,11 +56,21 @@ const buildCategoryTree = (list: RawCategory[]): Category[] => { const CategoryPanel: React.FC = ({ selectedCategoryId, onCategoryClick }) => { const { t } = useTranslation("components"); + const { user, loading: authLoading } = useAuth(); const [expanded, setExpanded] = useState>({}); - const { data: categories = [], loading } = useAsyncResource(async (signal) => { - const response = await CategoryService.queryCategory({ signal }); - return buildCategoryTree(response?.list || []); - }, []); + const { data: categories = [], loading } = useAsyncResource( + async (signal) => { + const response = await CategoryService.queryMyCategory({ signal }); + return buildCategoryTree(response?.list || []); + }, + [user?.id], + { enabled: !!user?.id }, + ); + + // 分类已下放到个人用户,游客没有分类,整块面板不展示 + if (authLoading || !user) { + return null; + } const toggleExpand = (categoryId: string | number) => { setExpanded((prev) => ({ diff --git a/src/i18n/locales/en-US/Article.ts b/src/i18n/locales/en-US/Article.ts index 99aacc5..9a22fc3 100644 --- a/src/i18n/locales/en-US/Article.ts +++ b/src/i18n/locales/en-US/Article.ts @@ -101,6 +101,7 @@ const article = { typeRepost: "Repost", categoryLabel: "Category", categorySelect: "Category", + categoryCreateFailed: "Failed to create category", tagLabel: "Tags", tagSelect: "Tag", contentLabel: "Content", diff --git a/src/i18n/locales/en-US/Personal.ts b/src/i18n/locales/en-US/Personal.ts index aa06718..6dcae08 100644 --- a/src/i18n/locales/en-US/Personal.ts +++ b/src/i18n/locales/en-US/Personal.ts @@ -2,6 +2,7 @@ import stats from "./personal/Stats"; import center from "./personal/Center"; import articles from "./personal/Articles"; import tags from "./personal/Tags"; +import categories from "./personal/Categories"; import assignments from "./personal/Assignments"; import organizations from "./personal/Organizations"; import notifications from "./personal/Notifications"; @@ -15,6 +16,7 @@ const personal = { center, articles, tags, + categories, assignments, organizations, notifications, diff --git a/src/i18n/locales/en-US/admin/Categories.ts b/src/i18n/locales/en-US/admin/Categories.ts index c22bde4..94dd861 100644 --- a/src/i18n/locales/en-US/admin/Categories.ts +++ b/src/i18n/locales/en-US/admin/Categories.ts @@ -14,6 +14,9 @@ const section = { clear: "Clear Filters", searchLabel: "Search Categories", searchPlaceholder: "Category name or description", + userLabel: "Owner", + userPlaceholder: "Select user...", + allUsers: "All Users", statusLabel: "Category Status", statusPlaceholder: "Select status...", sortLabel: "Sort By", @@ -41,6 +44,7 @@ const section = { }, icons: { default: "Default", + folder: "Folder", tech: "Technology", life: "Life", travel: "Travel", @@ -58,6 +62,7 @@ const section = { total: "{{total}} categories in total", headers: { info: "Category Info", + owner: "Owner", parent: "Parent Category", status: "Status", stats: "Stats", @@ -74,6 +79,8 @@ const section = { modal: { createTitle: "New Category", editTitle: "Edit Category", + ownerLabel: "Owner", + ownerPlaceholder: "Select owner", nameLabel: "Category Name *", namePlaceholder: "Enter category name", slugLabel: "URL Slug *", diff --git a/src/i18n/locales/en-US/personal/Categories.ts b/src/i18n/locales/en-US/personal/Categories.ts new file mode 100644 index 0000000..c5138b0 --- /dev/null +++ b/src/i18n/locales/en-US/personal/Categories.ts @@ -0,0 +1,36 @@ +const section = { + title: "My Categories", + createCategory: "New Category", + loading: "Loading categories...", + emptyTitle: "No categories yet", + emptyHint: 'Click the "New Category" button to create your first article category', + unknownDescription: "N/A", + unknownTime: "Unknown time", + articleCount: "{{value}} articles", + createdOn: "Created on {{value}}", + parentNone: "None (Top-level category)", + parentLabel: "Parent Category", + parentPlaceholder: "Select parent category", + parentValue: "Parent: {{value}}", + editTitle: "Edit Category", + nameLabel: "Category Name", + namePlaceholder: "Enter category name", + descLabel: "Category Description", + descPlaceholder: "Enter category description", + colorLabel: "Category Color", + cancel: "Cancel", + saving: "Saving...", + update: "Update", + create: "Create", + nameRequired: "Please enter a category name", + deleteConfirmTitle: "Confirm Deletion", + deleteConfirmContent: + 'Are you sure you want to delete the category "{{name}}"? Articles associated with it will lose this category.', + delete: "Delete", + message: { + saveFailed: "Failed to save the category, please try again", + deleteFailed: "Failed to delete the category, please try again", + }, +} as const; + +export default section; diff --git a/src/i18n/locales/en-US/personal/Center.ts b/src/i18n/locales/en-US/personal/Center.ts index c2bd541..45acf03 100644 --- a/src/i18n/locales/en-US/personal/Center.ts +++ b/src/i18n/locales/en-US/personal/Center.ts @@ -6,6 +6,7 @@ const section = { assignments: "My Tasks", edit: "Edit Profile", tags: "My Tags", + categories: "My Categories", stats: "Statistics", organizations: "My Organizations", following: "My Following", diff --git a/src/i18n/locales/zh-CN/Article.ts b/src/i18n/locales/zh-CN/Article.ts index e3078b7..2e89b71 100644 --- a/src/i18n/locales/zh-CN/Article.ts +++ b/src/i18n/locales/zh-CN/Article.ts @@ -101,6 +101,7 @@ const article = { typeRepost: "转载文章", categoryLabel: "文章分类", categorySelect: "分类", + categoryCreateFailed: "创建分类失败", tagLabel: "文章标签", tagSelect: "标签", contentLabel: "文章内容", diff --git a/src/i18n/locales/zh-CN/Personal.ts b/src/i18n/locales/zh-CN/Personal.ts index aa06718..6dcae08 100644 --- a/src/i18n/locales/zh-CN/Personal.ts +++ b/src/i18n/locales/zh-CN/Personal.ts @@ -2,6 +2,7 @@ import stats from "./personal/Stats"; import center from "./personal/Center"; import articles from "./personal/Articles"; import tags from "./personal/Tags"; +import categories from "./personal/Categories"; import assignments from "./personal/Assignments"; import organizations from "./personal/Organizations"; import notifications from "./personal/Notifications"; @@ -15,6 +16,7 @@ const personal = { center, articles, tags, + categories, assignments, organizations, notifications, diff --git a/src/i18n/locales/zh-CN/admin/Categories.ts b/src/i18n/locales/zh-CN/admin/Categories.ts index 4abcd63..3d3f395 100644 --- a/src/i18n/locales/zh-CN/admin/Categories.ts +++ b/src/i18n/locales/zh-CN/admin/Categories.ts @@ -14,6 +14,9 @@ const section = { clear: "清除筛选", searchLabel: "搜索分类", searchPlaceholder: "分类名称或描述", + userLabel: "所属用户", + userPlaceholder: "选择用户...", + allUsers: "全部用户", statusLabel: "分类状态", statusPlaceholder: "选择状态...", sortLabel: "排序方式", @@ -41,6 +44,7 @@ const section = { }, icons: { default: "默认", + folder: "文件夹", tech: "技术", life: "生活", travel: "旅行", @@ -58,6 +62,7 @@ const section = { total: "共 {{total}} 个分类", headers: { info: "分类信息", + owner: "所属用户", parent: "父级分类", status: "状态", stats: "统计数据", @@ -74,6 +79,8 @@ const section = { modal: { createTitle: "新增分类", editTitle: "编辑分类", + ownerLabel: "所属用户", + ownerPlaceholder: "选择所属用户", nameLabel: "分类名称 *", namePlaceholder: "输入分类名称", slugLabel: "URL标识 *", diff --git a/src/i18n/locales/zh-CN/personal/Categories.ts b/src/i18n/locales/zh-CN/personal/Categories.ts new file mode 100644 index 0000000..cd353e7 --- /dev/null +++ b/src/i18n/locales/zh-CN/personal/Categories.ts @@ -0,0 +1,35 @@ +const section = { + title: "我的分类", + createCategory: "新建分类", + loading: "加载分类中...", + emptyTitle: "还没有分类", + emptyHint: "点击“新建分类”创建你的第一个文章分类", + unknownDescription: "暂无描述", + unknownTime: "未知时间", + articleCount: "{{value}} 篇文章", + createdOn: "创建于 {{value}}", + parentNone: "无(顶级分类)", + parentLabel: "父级分类", + parentPlaceholder: "选择父级分类", + parentValue: "父级:{{value}}", + editTitle: "编辑分类", + nameLabel: "分类名称", + namePlaceholder: "输入分类名称", + descLabel: "分类描述", + descPlaceholder: "输入分类描述", + colorLabel: "分类颜色", + cancel: "取消", + saving: "保存中...", + update: "更新", + create: "创建", + nameRequired: "请输入分类名称", + deleteConfirmTitle: "确认删除", + deleteConfirmContent: '确定要删除分类 "{{name}}" 吗?相关文章将失去该分类。', + delete: "删除", + message: { + saveFailed: "保存分类失败,请稍后重试", + deleteFailed: "删除分类失败,请稍后重试", + }, +} as const; + +export default section; diff --git a/src/i18n/locales/zh-CN/personal/Center.ts b/src/i18n/locales/zh-CN/personal/Center.ts index 511766e..6007f0a 100644 --- a/src/i18n/locales/zh-CN/personal/Center.ts +++ b/src/i18n/locales/zh-CN/personal/Center.ts @@ -6,6 +6,7 @@ const section = { assignments: "我的任务", edit: "编辑资料", tags: "我的标签", + categories: "我的分类", stats: "数据统计", organizations: "我的组织", following: "我的关注", diff --git a/src/pages/admin/ArticleManagement.tsx b/src/pages/admin/ArticleManagement.tsx index c46d871..7346244 100644 --- a/src/pages/admin/ArticleManagement.tsx +++ b/src/pages/admin/ArticleManagement.tsx @@ -140,7 +140,7 @@ const ArticleManagement: React.FC = () => { if (res) { const options = res.list.map((cat) => ({ id: cat.id, - name: cat.name, + name: cat.author ? `${cat.name} · ${cat.author}` : cat.name, color: cat.color || "#007bff", })); setCategoryOptions([{ id: "", name: t("articles.filter.allCategories"), color: "#6c757d" }, ...options]); diff --git a/src/pages/admin/CategoryManagement.tsx b/src/pages/admin/CategoryManagement.tsx index 7f22ab3..eb3c794 100644 --- a/src/pages/admin/CategoryManagement.tsx +++ b/src/pages/admin/CategoryManagement.tsx @@ -11,6 +11,8 @@ import message from "@/components/message/Message"; import type { SelectOption } from "@/types"; import styles from "./CategoryManagement.module.css"; import CategoryService from "@/services/CategoryService.ts"; +import AuthService from "@/services/AuthService.ts"; +import { useAuth } from "@/contexts/AuthContext"; import { formatDateTime } from "@/utils/Utils.ts"; interface Category { @@ -21,6 +23,8 @@ interface Category { color: string; icon?: string; parentId?: string | number; + userId: string | number; + author?: string; articleCount: number; views: number; createdAt: string; @@ -37,6 +41,7 @@ interface CategoryFormData { color: string; icon: string; parentId: string; + ownerId: string; status: "active" | "inactive"; } @@ -49,6 +54,7 @@ interface CategoryStats { interface FilterOptions { search: string; + userId: string; status: string; sortBy: "name" | "articleCount" | "views" | "createdAt"; sortOrder: "asc" | "desc"; @@ -56,8 +62,10 @@ interface FilterOptions { const CategoryManagement: React.FC = () => { const { t } = useTranslation("admin"); + const { user } = useAuth(); const iconMap: Record = { "": t("categories.icons.default"), + "📁": t("categories.icons.folder"), "💻": t("categories.icons.tech"), "🌟": t("categories.icons.life"), "✈️": t("categories.icons.travel"), @@ -69,9 +77,11 @@ const CategoryManagement: React.FC = () => { "📷": t("categories.icons.photography"), }; const [categories, setCategories] = useState([]); + const [userOptions, setUserOptions] = useState([]); const [loading, setLoading] = useState(true); const [filters, setFilters] = useState({ search: "", + userId: "", status: "", sortBy: "name", sortOrder: "asc", @@ -85,6 +95,7 @@ const CategoryManagement: React.FC = () => { color: "#4361ee", icon: "", parentId: "", + ownerId: "", status: "active", }); const [submitting, setSubmitting] = useState(false); @@ -110,7 +121,14 @@ const CategoryManagement: React.FC = () => { const getParentCategoryOptions = (): SelectOption[] => { const options: SelectOption[] = [{ id: "", name: t("categories.parent.none"), color: "#6c757d" }]; - const activeCategories = categories.filter((cat) => cat.level === 0 && cat.status === "active"); + const ownerId = formData.ownerId || String(user?.id || ""); + const activeCategories = categories.filter( + (cat) => + cat.level === 0 && + cat.status === "active" && + String(cat.userId) === ownerId && + String(cat.id) !== String(editingCategory?.id ?? ""), + ); options.push( ...activeCategories.map((cat) => ({ id: cat.id.toString(), @@ -125,6 +143,23 @@ const CategoryManagement: React.FC = () => { loadCategories(); }, []); + useEffect(() => { + const fetchUsers = async () => { + try { + const res = await AuthService.listUsersAdmin({ page_num: 1, page_size: 1000 }); + const options = (res.list || []).map((item) => ({ + id: String(item.id), + name: item.name || item.email, + color: "#17a2b8", + })); + setUserOptions([{ id: "", name: t("categories.filter.allUsers"), color: "#6c757d" }, ...options]); + } catch (error) { + console.error("加载用户列表失败:", error); + } + }; + fetchUsers(); + }, []); + const loadCategories = async () => { setLoading(true); try { @@ -139,6 +174,8 @@ const CategoryManagement: React.FC = () => { color: item.color, icon: item.icon, parentId: item.parent_id, + userId: item.user_id ?? "", + author: item.author || "", articleCount: item.article_count || 0, views: item.view_count || 0, createdAt: item.create_time ? formatDateTime(item.create_time) : "-", @@ -175,7 +212,7 @@ const CategoryManagement: React.FC = () => { }; const clearFilters = () => { - setFilters({ search: "", status: "", sortBy: "name", sortOrder: "asc" }); + setFilters({ search: "", userId: "", status: "", sortBy: "name", sortOrder: "asc" }); }; const openModal = (category?: Category) => { @@ -183,11 +220,12 @@ const CategoryManagement: React.FC = () => { setEditingCategory(category); setFormData({ name: category.name, - slug: category.slug, + slug: category.slug || category.name, description: category.description, color: category.color, icon: category.icon || "", - parentId: category.parentId?.toString() || "", + parentId: category.parentId && String(category.parentId) !== "0" ? String(category.parentId) : "", + ownerId: String(category.userId || ""), status: category.status, }); } else { @@ -199,6 +237,7 @@ const CategoryManagement: React.FC = () => { color: "#4361ee", icon: "", parentId: "", + ownerId: String(user?.id || ""), status: "active", }); } @@ -222,12 +261,15 @@ const CategoryManagement: React.FC = () => { } setSubmitting(true); try { + const ownerId = formData.ownerId || String(user?.id || ""); + const ownerName = userOptions.find((option) => option.id === ownerId)?.name || ""; const response = await CategoryService.createCategory({ id: editingCategory ? editingCategory.id : undefined, + user_id: ownerId || undefined, name: formData.name, - url: formData.slug, + url: formData.slug || formData.name, color: formData.color, - icon: formData.icon, + icon: formData.icon || "📁", description: formData.description, parent_id: formData.parentId, status: formData.status === "active" ? 1 : 0, @@ -236,7 +278,19 @@ const CategoryManagement: React.FC = () => { if (editingCategory) { setCategories((prev) => prev.map((cat) => - cat.id === editingCategory.id ? { ...cat, ...formData, updatedAt: formatDateTime(new Date()) } : cat, + cat.id === editingCategory.id + ? { + ...cat, + name: response.name, + slug: formData.slug || formData.name, + description: formData.description, + color: response.color, + icon: formData.icon || "📁", + parentId: response.parent_id, + status: formData.status, + updatedAt: formatDateTime(new Date()), + } + : cat, ), ); message.success(t("categories.request.updated")); @@ -244,11 +298,13 @@ const CategoryManagement: React.FC = () => { const newCategory: Category = { id: response.id, name: response.name, - slug: formData.slug, + slug: formData.slug || formData.name, description: formData.description, color: response.color, - icon: formData.icon, + icon: formData.icon || "📁", parentId: response.parent_id, + userId: ownerId, + author: ownerName, articleCount: 0, views: 0, createdAt: formatDateTime(new Date()), @@ -289,8 +345,8 @@ const CategoryManagement: React.FC = () => { } try { - await CategoryService.deleteCategory({ ids: String(category.id) }); - setCategories((prev) => prev.filter((cat) => cat.id !== category.id)); + await CategoryService.deleteCategory({ ids: String(category.id), user_id: category.userId }); + await loadCategories(); message.success(t("categories.request.deleted")); } catch (error) { message.error(t("categories.request.deleteFailed")); @@ -301,12 +357,14 @@ const CategoryManagement: React.FC = () => { const newStatus = category.status === "active" ? "inactive" : "active"; try { await CategoryService.createCategory({ + id: category.id, + user_id: category.userId, name: category.name, - url: category.slug, + url: category.slug || category.name, color: category.color, - icon: category.icon || "", + icon: category.icon || "📁", description: category.description, - parent_id: category.parentId, + parent_id: category.parentId || "", status: newStatus === "active" ? 1 : 0, }); setCategories((prev) => @@ -328,6 +386,9 @@ const CategoryManagement: React.FC = () => { const filteredCategories = categories .filter((cat) => { + if (filters.userId && String(cat.userId) !== filters.userId) { + return false; + } if ( filters.search && !cat.name.toLowerCase().includes(filters.search.toLowerCase()) && @@ -438,6 +499,18 @@ const CategoryManagement: React.FC = () => { style={{ minHeight: "46px", height: "50px" }} /> +
+ + option.id === filters.userId) || userOptions[0] || null} + onChange={handleSelectChange("userId")} + placeholder={t("categories.filter.userPlaceholder")} + className="adminCustomSelect" + hideBadge={true} + /> +
{ {t("categories.table.headers.info")} + {t("categories.table.headers.owner")} {t("categories.table.headers.parent")} {t("categories.table.headers.status")} {t("categories.table.headers.stats")} @@ -514,6 +588,12 @@ const CategoryManagement: React.FC = () => {
+ + {category.author || + userOptions.find((option) => option.id === String(category.userId))?.name || + category.userId || + "-"} + {category.parentId ? ( @@ -581,7 +661,7 @@ const CategoryManagement: React.FC = () => { ) : ( { } > +
+ + option.id !== "")} + value={userOptions.find((option) => option.id === formData.ownerId) || null} + onChange={(option) => + setFormData((prev) => ({ + ...prev, + ownerId: String(option?.id || ""), + parentId: "", + })) + } + hideBadge={true} + placeholder={t("categories.modal.ownerPlaceholder")} + disabled={!!editingCategory} + /> +
{ - setFormData((prev) => ({ ...prev, name: value, slug: value ? generateSlug(value) : "" })); + setFormData((prev) => ({ + ...prev, + name: value, + slug: value ? generateSlug(value) || value : "", + })); }} className={styles.formInput} size="large" diff --git a/src/pages/article/ArticleCreate.tsx b/src/pages/article/ArticleCreate.tsx index d2135ea..d8d9dd3 100644 --- a/src/pages/article/ArticleCreate.tsx +++ b/src/pages/article/ArticleCreate.tsx @@ -132,8 +132,8 @@ const ArticleCreate: React.FC = ({ onSaveDraft, onPublish, i let formattedTags: Tag[] = []; try { - // 获取所有分类,保证每项都有 color 字段 - const categoryData = await CategoryService.queryCategory(); + // 获取当前用户的分类,保证每项都有 color 字段 + const categoryData = await CategoryService.queryMyCategory(); formattedCategories = categoryData.list.map((cat) => ({ id: cat.id, name: cat.name, @@ -307,6 +307,33 @@ const ArticleCreate: React.FC = ({ onSaveDraft, onPublish, i } }; + const handleAddCategory = async (newCategory: SelectOption) => { + // 创建个人分类到后端,成功后自动选中 + try { + const created = await CategoryService.createMyCategory({ + name: newCategory.name, + url: newCategory.name, + icon: "📁", + color: newCategory.color || "#4361ee", + }); + const categoryToAdd: SelectOption = { + id: created.id, + name: created.name, + color: created.color || newCategory.color || "#4361ee", + }; + setCategories((prev) => { + if (prev.some((item) => item.id === categoryToAdd.id)) { + return prev; + } + return [...prev, categoryToAdd]; + }); + setFormData((prev) => ({ ...prev, category: categoryToAdd })); + } catch (err) { + console.error("分类创建失败:", err); + message.error(t("create.categoryCreateFailed")); + } + }; + // 保存草稿:只创建文章 const handleSaveDraft = async () => { if (submitting) { @@ -637,6 +664,7 @@ const ArticleCreate: React.FC = ({ onSaveDraft, onPublish, i
{t("create.categoryLabel")} +
{ onTagClick={handleTagClick} />
-
- -
+ {user && ( +
+ +
+ )}
@@ -212,9 +214,11 @@ const IndexPage: React.FC = () => { />
{/* 文章分类 */} -
- -
+ {user && ( +
+ +
+ )} diff --git a/src/pages/personal/PersonalCenter.tsx b/src/pages/personal/PersonalCenter.tsx index a63da73..3c3312f 100644 --- a/src/pages/personal/PersonalCenter.tsx +++ b/src/pages/personal/PersonalCenter.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useCallback } from "react"; import { FaTag, + FaFolderOpen, FaFileAlt, FaChartBar, FaBars, @@ -32,6 +33,7 @@ import AuthService, { type UserStats } from "@/services/AuthService.ts"; import styles from "./PersonalCenter.module.css"; import MyArticlesTab from "./components/MyArticlesTab"; import MyTagsTab from "./components/MyTagsTab"; +import MyCategoriesTab from "./components/MyCategoriesTab"; import StatsTab from "./components/StatsTab"; import MyOrganizationsTab from "./components/MyOrganizationsTab"; import NotificationsTab from "./components/NotificationsTab"; @@ -53,6 +55,7 @@ type TabId = | "articles" | "assignments" | "tags" + | "categories" | "stats" | "organizations" | "notifications" @@ -66,6 +69,7 @@ const TAB_IDS: TabId[] = [ "articles", "assignments", "tags", + "categories", "stats", "organizations", "notifications", @@ -148,6 +152,12 @@ const PersonalCenter: React.FC = () => { path: "/personal?tab=edit", }, { id: "tags", label: t("center.nav.tags"), icon: , path: "/personal?tab=tags" }, + { + id: "categories", + label: t("center.nav.categories"), + icon: , + path: "/personal?tab=categories", + }, { id: "stats", label: t("center.nav.stats"), @@ -585,6 +595,9 @@ const PersonalCenter: React.FC = () => { {/* 标签管理 */} {activeTab === "tags" && } + {/* 分类管理 */} + {activeTab === "categories" && } + {/* 数据统计 */} {activeTab === "stats" && stats && } diff --git a/src/pages/personal/components/MyCategoriesTab.tsx b/src/pages/personal/components/MyCategoriesTab.tsx new file mode 100644 index 0000000..756b07e --- /dev/null +++ b/src/pages/personal/components/MyCategoriesTab.tsx @@ -0,0 +1,328 @@ +import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { FaFolderOpen, FaPlus, FaEdit, FaTrash } from "react-icons/fa"; +import Loading from "@/components/loading/Loading"; +import CustomSelect from "@/components/customSelect/CustomSelect"; +import { confirm } from "@/components/confirm/Confirm"; +import { message } from "@/components/message/Message"; +import { useAuth } from "@/contexts/AuthContext"; +import CategoryService from "@/services/CategoryService.ts"; +import type { SelectOption } from "@/types"; +import { formatToChinaTime } from "@/utils/Utils.ts"; +import styles from "../PersonalCenter.module.css"; + +interface PersonalCategory { + id: string | number; + name: string; + description: string; + color: string; + parentId: string | number | null; + status: number; + articleCount: number; + createTime: string; +} + +const MyCategoriesTab: React.FC = () => { + const { t } = useTranslation("personal"); + const { user } = useAuth(); + const userId = user?.id; + const [categories, setCategories] = useState([]); + const [categoriesLoading, setCategoriesLoading] = useState(false); + const [showCategoryModal, setShowCategoryModal] = useState(false); + const [editingCategory, setEditingCategory] = useState(null); + const [categoryForm, setCategoryForm] = useState({ + name: "", + description: "", + color: "#4361ee", + parentId: "", + }); + const [savingCategory, setSavingCategory] = useState(false); + + useEffect(() => { + const fetchPersonalCategories = async () => { + setCategoriesLoading(true); + if (!userId) { + setCategories([]); + setCategoriesLoading(false); + return; + } + + try { + const res = await CategoryService.queryMyCategory(); + const mapped = (res?.list || []).map((item) => ({ + id: item.id, + name: item.name, + description: item.description || t("categories.unknownDescription"), + color: item.color || "#4361ee", + parentId: item.parent_id || null, + status: item.status ?? 1, + articleCount: item.article_count || 0, + createTime: item.create_time ? formatToChinaTime(item.create_time) : t("categories.unknownTime"), + })); + setCategories(mapped); + } catch (err) { + console.error("获取个人分类失败:", err); + setCategories([]); + } finally { + setCategoriesLoading(false); + } + }; + + fetchPersonalCategories(); + }, [userId]); + + const parentOptions: SelectOption[] = [ + { id: "", name: t("categories.parentNone"), color: "#6c757d" }, + ...categories + .filter((cat) => !cat.parentId && String(cat.id) !== String(editingCategory?.id ?? "")) + .map((cat) => ({ + id: String(cat.id), + name: cat.name, + color: cat.color, + })), + ]; + + const getParentName = (category: PersonalCategory) => { + if (!category.parentId) { + return ""; + } + return categories.find((item) => item.id === category.parentId)?.name || ""; + }; + + const resetForm = () => { + setEditingCategory(null); + setCategoryForm({ name: "", description: "", color: "#4361ee", parentId: "" }); + }; + + const closeModal = () => { + setShowCategoryModal(false); + resetForm(); + }; + + const handleSaveCategory = async () => { + if (!categoryForm.name.trim()) { + message.error(t("categories.nameRequired")); + return; + } + + setSavingCategory(true); + try { + if (editingCategory) { + const res = await CategoryService.createMyCategory({ + id: editingCategory.id, + name: categoryForm.name.trim(), + url: categoryForm.name.trim(), + icon: "📁", + color: categoryForm.color, + description: categoryForm.description, + parent_id: categoryForm.parentId, + status: editingCategory.status, + }); + + setCategories((prev) => + prev.map((category) => + category.id === editingCategory.id + ? { + ...category, + name: res.name, + description: categoryForm.description || t("categories.unknownDescription"), + color: res.color || categoryForm.color, + parentId: res.parent_id || null, + } + : category, + ), + ); + } else { + const res = await CategoryService.createMyCategory({ + name: categoryForm.name.trim(), + url: categoryForm.name.trim(), + icon: "📁", + color: categoryForm.color, + description: categoryForm.description, + parent_id: categoryForm.parentId, + status: 1, + }); + + const newCategory: PersonalCategory = { + id: res.id, + name: res.name, + description: categoryForm.description || t("categories.unknownDescription"), + color: res.color || categoryForm.color, + parentId: res.parent_id || null, + status: res.status ?? 1, + articleCount: 0, + createTime: new Date().toISOString().split("T")[0], + }; + setCategories((prev) => { + if (prev.some((item) => item.id === newCategory.id)) { + return prev.map((item) => (item.id === newCategory.id ? newCategory : item)); + } + return [...prev, newCategory]; + }); + } + + closeModal(); + } catch (err) { + console.error("保存分类失败:", err); + message.error(t("categories.message.saveFailed")); + } finally { + setSavingCategory(false); + } + }; + + const handleDeleteCategory = async (category: PersonalCategory) => { + const confirmed = await confirm({ + title: t("categories.deleteConfirmTitle"), + content: t("categories.deleteConfirmContent", { name: category.name }), + confirmText: t("categories.delete"), + cancelText: t("categories.cancel"), + }); + + if (confirmed) { + try { + await CategoryService.deleteMyCategory({ ids: String(category.id) }); + setCategories((prev) => prev.filter((item) => item.id !== category.id)); + } catch (err) { + console.error("删除分类失败:", err); + message.error(t("categories.message.deleteFailed")); + } + } + }; + + const handleEditCategory = (category: PersonalCategory) => { + setEditingCategory(category); + setCategoryForm({ + name: category.name, + description: category.description === t("categories.unknownDescription") ? "" : category.description, + color: category.color, + parentId: category.parentId ? String(category.parentId) : "", + }); + setShowCategoryModal(true); + }; + + return ( +
+
+

{t("categories.title")}

+ +
+ + {categoriesLoading ? ( +
+ +
+ ) : categories.length === 0 ? ( +
+ +

{t("categories.emptyTitle")}

+

{t("categories.emptyHint")}

+
+ ) : ( +
+ {categories.map((category) => ( +
+
+
+

{category.name}

+
+

{category.description}

+
+ {t("categories.articleCount", { value: category.articleCount })} + {t("categories.createdOn", { value: category.createTime })} + {getParentName(category) && ( + {t("categories.parentValue", { value: getParentName(category) })} + )} +
+
+ + +
+
+ ))} +
+ )} + + {showCategoryModal && ( +
+
+
+

{editingCategory ? t("categories.editTitle") : t("categories.createCategory")}

+ +
+
+
+ + setCategoryForm({ ...categoryForm, name: e.target.value })} + placeholder={t("categories.namePlaceholder")} + /> +
+
+ +