From 5156255c7718bce06b7ef518582226c3e80a5ac7 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Thu, 12 Feb 2026 15:39:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BE=BD=E7=AB=A0=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/badge.ts | 65 +++ src/api/index.ts | 2 - src/api/post.ts | 68 +++ src/api/topic.ts | 56 +++ src/api/{business.ts => wiki.ts} | 125 ----- src/layouts/AdminLayout.tsx | 8 +- src/pages/Dashboard.tsx | 55 ++- src/pages/TopicsPage.tsx | 2 +- src/pages/plant/community/Post.tsx | 2 +- src/pages/plant/community/Topic.tsx | 7 +- src/pages/plant/conf/Badge.tsx | 715 ++++++++++++++++++++++++++-- src/pages/plant/wiki/Class.tsx | 4 +- src/pages/plant/wiki/Wiki.tsx | 2 +- 13 files changed, 906 insertions(+), 205 deletions(-) create mode 100644 src/api/badge.ts create mode 100644 src/api/post.ts create mode 100644 src/api/topic.ts rename src/api/{business.ts => wiki.ts} (57%) diff --git a/src/api/badge.ts b/src/api/badge.ts new file mode 100644 index 0000000..a1e7340 --- /dev/null +++ b/src/api/badge.ts @@ -0,0 +1,65 @@ +import {get, post} from '@/lib/request' +import type {SystemOss} from './system' + +// ==================== 徽章配置 ==================== + +export interface Badge { + id: string + name: string + description?: string + iconId?: string + icon?: SystemOss + + dimension: string + groupId: string + tier: number + + targetAction: string + threshold: number + comparator: string + + rewardSunlight: number + sort: number + isHidden?: boolean + + createdAt?: string + updatedAt?: string + createdAtStr?: string +} + +export interface BadgeGroup { + groupId: string + groupLabel: string + badges: Badge[] +} + +export interface BadgeDimension { + dimension: string + label: string + groups: BadgeGroup[] +} + +// 徽章树 +export function getBadgeTree() { + return get<{ data: BadgeDimension[] }>('/config/badge/tree') +} + +// 添加徽章 +export function getBadgeDetail(id: string) { + return get<{ msg: string }>('/config/badge/find', {id}) +} + +// 添加徽章 +export function addBadge(data: Partial) { + return post<{ msg: string }>('/config/badge/add', data) +} + +// 修改徽章 +export function updateBadge(data: Partial) { + return post<{ msg: string }>('/config/badge/update', data) +} + +// 删除徽章 +export function deleteBadge(id: string) { + return get<{ msg: string }>('/config/badge/delete', {id}) +} diff --git a/src/api/index.ts b/src/api/index.ts index 3637b0c..af35795 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,5 +1,3 @@ // 系统相关 API export * from './system' -// 业务相关 API -export * from './business' diff --git a/src/api/post.ts b/src/api/post.ts new file mode 100644 index 0000000..4b4070c --- /dev/null +++ b/src/api/post.ts @@ -0,0 +1,68 @@ +import { get, post, type PageParams } from '@/lib/request' +import type { SystemOss } from './system' + +export interface Post { + id: string + title: string + content: string + location?: string + imgList?: SystemOss[] + ossIds?: string[] + publisher?: { + id: string + nickName: string + avatar?: SystemOss + } + author?: { // Keep for compatibility if needed, or remove + id: string + nickName: string + avatar?: SystemOss + } + viewCount?: number + likeCount?: number + commentCount?: number + hasLiked?: number // 0 or 1 + hasReviewed?: number // 0 or 1 + createdAt?: string + updatedAt?: string + createdAtStr?: string + commentList?: any[] + likeList?: any[] +} + +export interface PostPageParams extends PageParams { + title?: string + hasReviewed?: number // 是否审核通过 +} + +export interface CreatePostParams { + title: string + content: string + location?: string + ossIds?: string[] +} + +export interface CreateCommentParams { + postId: string + content: string +} + +// 帖子列表 +export function getPostPage(data: PostPageParams) { + return post<{ data: { list: Post[]; total: number } }>('/post/page', data) +} + +// 发布帖子 +export function publishPost(data: CreatePostParams) { + return post<{ msg: string }>('/post/publish', data) +} + +// 点赞帖子 +export function likePost(id: string, type: 1 | 2) { + return get<{ msg: string }>('/post/like', { id, type }) +} + +// 评论帖子 +export function commentPost(data: CreateCommentParams) { + return post<{ msg: string }>('/post/comment', data) +} diff --git a/src/api/topic.ts b/src/api/topic.ts new file mode 100644 index 0000000..ddaf3c7 --- /dev/null +++ b/src/api/topic.ts @@ -0,0 +1,56 @@ +import { get, post, type PageParams } from '@/lib/request' + +export interface Topic { + id: string + title: string + remark?: string + startTime?: string + endTime?: string + createdAt?: string + updatedAt?: string +} + +export interface CreateTopicParams { + title: string + remark?: string + startTime?: string + endTime?: string +} + +export interface UpdateTopicParams { + id: string + title?: string + remark?: string + startTime?: string + endTime?: string +} + +// 话题列表 +export function getTopicList() { + return get<{ data: Topic[] }>('/topic/list') +} + +// 话题分页 +export function getTopicPage(data: PageParams) { + return post<{ data: { list: Topic[]; total: number } }>('/topic/page', data) +} + +// 话题详情 +export function getTopicDetail(id: string) { + return get<{ data: Topic }>('/topic/detail', { id }) +} + +// 添加话题 +export function addTopic(data: CreateTopicParams) { + return post<{ msg: string }>('/topic/add', data) +} + +// 修改话题 +export function updateTopic(data: UpdateTopicParams) { + return post<{ msg: string }>('/topic/add', data) // API uses /topic/add for update as well +} + +// 删除话题 +export function deleteTopic(ids: string[]) { + return post<{ msg: string }>('/topic/delete', { ids }) +} diff --git a/src/api/business.ts b/src/api/wiki.ts similarity index 57% rename from src/api/business.ts rename to src/api/wiki.ts index 98012c0..f40af72 100644 --- a/src/api/business.ts +++ b/src/api/wiki.ts @@ -1,131 +1,6 @@ import { get, post, type PageParams } from '@/lib/request' import type { SystemOss } from './system' -// ==================== 帖子话题 ==================== - -export interface Topic { - id: string - title: string - remark?: string - startTime?: string - endTime?: string - createdAt?: string - updatedAt?: string -} - -export interface CreateTopicParams { - title: string - remark?: string - startTime?: string - endTime?: string -} - -export interface UpdateTopicParams { - id: number - title?: string - remark?: string - startTime?: string - endTime?: string -} - -// 话题列表 -export function getTopicList() { - return get<{ data: Topic[] }>('/topic/list') -} - -// 话题分页 -export function getTopicPage(data: PageParams) { - return post<{ data: { list: Topic[]; total: number } }>('/topic/page', data) -} - -// 话题详情 -export function getTopicDetail(id: string) { - return get<{ data: Topic }>('/topic/detail', { id }) -} - -// 添加话题 -export function addTopic(data: CreateTopicParams) { - return post<{ msg: string }>('/topic/add', data) -} - -// 修改话题 -export function updateTopic(data: UpdateTopicParams) { - return post<{ msg: string }>('/topic/add', data) // API uses /topic/add for update as well -} - -// 删除话题 -export function deleteTopic(ids: string[]) { - return post<{ msg: string }>('/topic/delete', { ids }) -} - -// ==================== 帖子/社区 ==================== - -export interface Post { - id: string - title: string - content: string - location?: string - imgList?: SystemOss[] - ossIds?: string[] - publisher?: { - id: string - nickName: string - avatar?: SystemOss - } - author?: { // Keep for compatibility if needed, or remove - id: string - nickName: string - avatar?: SystemOss - } - viewCount?: number - likeCount?: number - commentCount?: number - hasLiked?: number // 0 or 1 - hasReviewed?: number // 0 or 1 - createdAt?: string - updatedAt?: string - createdAtStr?: string - commentList?: any[] - likeList?: any[] -} - -export interface PostPageParams extends PageParams { - title?: string - hasReviewed?: number // 是否审核通过 -} - -export interface CreatePostParams { - title: string - content: string - location?: string - ossIds?: string[] -} - -export interface CreateCommentParams { - postId: string - content: string -} - -// 帖子列表 -export function getPostPage(data: PostPageParams) { - return post<{ data: { list: Post[]; total: number } }>('/post/page', data) -} - -// 发布帖子 -export function publishPost(data: CreatePostParams) { - return post<{ msg: string }>('/post/publish', data) -} - -// 点赞帖子 -export function likePost(id: string, type: 1 | 2) { - return get<{ msg: string }>('/post/like', { id, type }) -} - -// 评论帖子 -export function commentPost(data: CreateCommentParams) { - return post<{ msg: string }>('/post/comment', data) -} - // ==================== 百科分类 ==================== export interface WikiClass { diff --git a/src/layouts/AdminLayout.tsx b/src/layouts/AdminLayout.tsx index ba560c2..afc8dab 100644 --- a/src/layouts/AdminLayout.tsx +++ b/src/layouts/AdminLayout.tsx @@ -9,7 +9,6 @@ import { LogOut, ChevronDown, Menu, - X, FileText, Settings, Book, @@ -119,11 +118,11 @@ function NavItemComponent({ item, collapsed, level = 0 }: { item: NavItem; colla const location = useLocation() // Check if active or child is active - const isActiveLink = location.pathname === item.href const hasActiveChild = item.children?.some(child => location.pathname.startsWith(child.href)) useEffect(() => { if (hasActiveChild && !collapsed) { + // eslint-disable-next-line react-hooks/set-state-in-effect setOpen(true) } }, [hasActiveChild, collapsed]) @@ -232,7 +231,7 @@ function Breadcrumb() { return undefined } - segments.forEach((segment, index) => { + segments.forEach((segment) => { currentPath += `/${segment}` const menuItem = menus ? findMenuItem(menus, currentPath) : null @@ -336,7 +335,6 @@ export default function AdminLayout() { {sidebarOpen && (

{user?.name || user?.account}

-

{user?.role || '管理员'}

)} {sidebarOpen && } @@ -424,7 +422,7 @@ export default function AdminLayout() { {/* Page content */}
-
+
diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 424b5d8..64a7469 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,6 +1,6 @@ -import { Users, MessageSquare, FolderTree, Leaf, TrendingUp, Eye, ArrowUpRight } from 'lucide-react' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { mockUsers, mockTopics, mockCategories, mockPlants } from '@/data/mockData' +import {Users, MessageSquare, FolderTree, Leaf, TrendingUp, Eye, ArrowUpRight} from 'lucide-react' +import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@/components/ui/card' +import {mockUsers, mockTopics, mockCategories, mockPlants} from '@/data/mockData' const stats = [ { @@ -42,10 +42,10 @@ const stats = [ ] const recentActivities = [ - { action: '添加了新植物', target: '龟背竹', time: '2分钟前', user: 'admin' }, - { action: '发布了新话题', target: '春季植物养护小技巧', time: '1小时前', user: 'editor' }, - { action: '更新了分类', target: '观叶植物', time: '3小时前', user: 'admin' }, - { action: '添加了新用户', target: 'viewer', time: '昨天', user: 'admin' }, + {action: '添加了新植物', target: '龟背竹', time: '2分钟前', user: 'admin'}, + {action: '发布了新话题', target: '春季植物养护小技巧', time: '1小时前', user: 'editor'}, + {action: '更新了分类', target: '观叶植物', time: '3小时前', user: 'admin'}, + {action: '添加了新用户', target: 'viewer', time: '昨天', user: 'admin'}, ] export default function DashboardPage() { @@ -58,37 +58,42 @@ export default function DashboardPage() {

欢迎回来 👋

- {new Date().toLocaleDateString('zh-CN', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })} + {new Date().toLocaleDateString('zh-CN', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + })}
{/* Stats Grid */}
{stats.map((stat, index) => ( - -
+ +
{stat.title}
- +
{stat.value}
- {stat.changeType === 'positive' && } + {stat.changeType === 'positive' && } {stat.change} - + 较上月
@@ -105,7 +110,7 @@ export default function DashboardPage() {
- +
最新话题
@@ -134,7 +139,7 @@ export default function DashboardPage() {

- + {topic.viewCount} {topic.authorName} @@ -153,7 +158,7 @@ export default function DashboardPage() {
- +
最近活动
@@ -168,7 +173,8 @@ export default function DashboardPage() { key={index} className="flex items-center gap-4 rounded-lg px-3 py-3 transition-colors hover:bg-muted/30" > -
+
{activity.user.charAt(0).toUpperCase()} @@ -193,7 +199,7 @@ export default function DashboardPage() {
- +
植物百科概览
@@ -206,7 +212,8 @@ export default function DashboardPage() { key={category.id} className="group flex items-center gap-4 rounded-xl border border-border/50 p-4 transition-all duration-200 hover:bg-muted/30 hover:border-border hover:shadow-sm cursor-pointer" > -
+
{category.icon || '🌱'}
diff --git a/src/pages/TopicsPage.tsx b/src/pages/TopicsPage.tsx index 3238595..ec7e960 100644 --- a/src/pages/TopicsPage.tsx +++ b/src/pages/TopicsPage.tsx @@ -124,7 +124,7 @@ export default function TopicsPage() { title: formData.title, content: formData.content, authorId: user?.id || '', - authorName: user?.username || '', + authorName: user?.name || '', status: formData.status, viewCount: 0, likeCount: 0, diff --git a/src/pages/plant/community/Post.tsx b/src/pages/plant/community/Post.tsx index df2a391..b264708 100644 --- a/src/pages/plant/community/Post.tsx +++ b/src/pages/plant/community/Post.tsx @@ -35,7 +35,7 @@ import { getPostPage, type Post, type PostPageParams, -} from '@/api/business' +} from '@/api/post' export default function PostsPage() { diff --git a/src/pages/plant/community/Topic.tsx b/src/pages/plant/community/Topic.tsx index 6fbc303..3186bf6 100644 --- a/src/pages/plant/community/Topic.tsx +++ b/src/pages/plant/community/Topic.tsx @@ -29,7 +29,7 @@ import { updateTopic, deleteTopic, type Topic, -} from '@/api/business' +} from '@/api/topic' import { cn } from '@/lib/utils' interface TopicFormData { @@ -136,7 +136,8 @@ export default function TopicsPage() { if (!selectedTopic) return try { - await deleteTopic([selectedTopic.id]) + // Ensure ID is string as backend expects []string + await deleteTopic([String(selectedTopic.id)]) setDeleteDialogOpen(false) setSelectedTopic(null) fetchTopics() @@ -161,7 +162,7 @@ export default function TopicsPage() { } if (isEdit && formData.id) { - await updateTopic({ ...payload, id: Number(formData.id) }) + await updateTopic({ ...payload, id: String(formData.id) }) } else { await addTopic(payload) } diff --git a/src/pages/plant/conf/Badge.tsx b/src/pages/plant/conf/Badge.tsx index 9cefed1..05e6a4d 100644 --- a/src/pages/plant/conf/Badge.tsx +++ b/src/pages/plant/conf/Badge.tsx @@ -1,51 +1,684 @@ -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Medal, Award, Lock } from 'lucide-react' +import { useState, useEffect } from 'react' +import { Award, Trophy, Medal, Grid, Plus, Edit2, Trash2, Eye, Upload, X, Crown, Sprout, Star, Sparkles, Search, ChevronRight, ChevronDown, Layers, Hash } from 'lucide-react' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Textarea } from '@/components/ui/textarea' +import { Switch } from '@/components/ui/switch' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { getBadgeTree, addBadge, updateBadge, deleteBadge, getBadgeDetail, type BadgeDimension, type Badge } from '@/api/badge' +import { uploadFile, type SystemOss } from '@/api/system' +import { cn } from '@/lib/utils' + +// Types +interface BadgeFormData { + id?: string + name: string + description: string + iconId: string + icon?: SystemOss + dimension: string + groupId: string + tier: number + targetAction: string + threshold: number + comparator: string + rewardSunlight: number + sort: number + isHidden: boolean +} + +const defaultFormData: BadgeFormData = { + name: '', + description: '', + iconId: '', + dimension: 'EXPERTISE', + groupId: '', + tier: 1, + targetAction: '', + threshold: 1, + comparator: '>=', + rewardSunlight: 10, + sort: 1, + isHidden: false, +} + +const DIMENSIONS = [ + { value: 'EXPERTISE', label: '专家成就 (Expertise)' }, + { value: 'PERSISTENCE', label: '勤勉成就 (Persistence)' }, + { value: 'JOURNAL', label: '记录成就 (Journal)' }, + { value: 'DISCOVERY', label: '探索成就 (Discovery)' }, +] + +export default function BadgeConfigPage() { + const [treeData, setTreeData] = useState([]) + const [loading, setLoading] = useState(true) + + // Dialog State + const [dialogOpen, setDialogOpen] = useState(false) + const [formData, setFormData] = useState(defaultFormData) + const [isEdit, setIsEdit] = useState(false) + const [submitting, setSubmitting] = useState(false) + const [uploading, setUploading] = useState(false) + + // Delete Dialog + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [badgeToDelete, setBadgeToDelete] = useState(null) + + // Navigation State + const [expandedDims, setExpandedDims] = useState([]) + const [selectedDimId, setSelectedDimId] = useState('') + const [selectedGroupId, setSelectedGroupId] = useState('') + + useEffect(() => { + fetchData() + }, []) + + const fetchData = async () => { + setLoading(true) + try { + const res = await getBadgeTree() + const data = res.data || [] + setTreeData(data) + + // Initial Selection if not set + if (data.length > 0 && !selectedDimId) { + // Expand all by default + setExpandedDims(data.map(d => d.dimension)) + + // Select first group of first dimension + const firstDim = data[0] + setSelectedDimId(firstDim.dimension) + if (firstDim.groups && firstDim.groups.length > 0) { + setSelectedGroupId(firstDim.groups[0].groupId) + } + } + } catch (error) { + console.error('Failed to fetch badge tree:', error) + } finally { + setLoading(false) + } + } + + const toggleDim = (dim: string) => { + setExpandedDims(prev => + prev.includes(dim) ? prev.filter(d => d !== dim) : [...prev, dim] + ) + } + + const handleSelectGroup = (dimId: string, groupId: string) => { + setSelectedDimId(dimId) + setSelectedGroupId(groupId) + } + + const handleAdd = (dimension?: string, groupId?: string) => { + setFormData({ + ...defaultFormData, + dimension: dimension || selectedDimId || 'EXPERTISE', + groupId: groupId || selectedGroupId || '', + }) + setIsEdit(false) + setDialogOpen(true) + } + + const handleEdit = async (badge: Badge) => { + try { + const res = await getBadgeDetail(badge.id) + const detail = (res as any).data || badge + + setFormData({ + id: detail.id, + name: detail.name, + description: detail.description || '', + iconId: detail.iconId || '', + icon: detail.icon, + dimension: detail.dimension, + groupId: detail.groupId, + tier: detail.tier, + targetAction: detail.targetAction, + threshold: detail.threshold, + comparator: detail.comparator, + rewardSunlight: detail.rewardSunlight, + sort: detail.sort, + isHidden: detail.isHidden || false, + }) + setIsEdit(true) + setDialogOpen(true) + } catch (e) { + console.error(e) + setFormData({ + ...defaultFormData, + ...badge, + iconId: badge.icon?.id || '', + }) + setIsEdit(true) + setDialogOpen(true) + } + } + + const handleDelete = (badge: Badge) => { + setBadgeToDelete(badge) + setDeleteDialogOpen(true) + } + + const confirmDelete = async () => { + if (!badgeToDelete) return + try { + await deleteBadge(badgeToDelete.id) + setDeleteDialogOpen(false) + fetchData() + } catch (e) { + console.error(e) + } + } + + const handleUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + + setUploading(true) + try { + const res = await uploadFile(file) + setFormData(prev => ({ + ...prev, + iconId: res.data.file.id, + icon: res.data.file, + })) + } catch (err) { + console.error('Upload failed:', err) + } finally { + setUploading(false) + } + } + + const handleSubmit = async () => { + if (!formData.name || !formData.targetAction) { + alert('请完善必填信息 (名称, 触发条件)') + return + } + + setSubmitting(true) + try { + const { icon, ...payload } = formData + + if (isEdit && formData.id) { + await updateBadge(payload) + } else { + await addBadge(payload) + } + setDialogOpen(false) + fetchData() + } catch (e) { + console.error(e) + } finally { + setSubmitting(false) + } + } + + // Helper to get icon for tier + const getTierIcon = (tier: number) => { + switch (tier) { + case 3: return + case 2: return + case 1: return + default: return + } + } + + const getTierLabel = (tier: number) => { + switch (tier) { + case 3: return '金' + case 2: return '银' + case 1: return '铜' + default: return `Tier ${tier}` + } + } + + // Helper for Tier Styles + const getTierStyle = (tier: number) => { + switch (tier) { + case 3: return 'border-yellow-400/30 bg-gradient-to-br from-yellow-50/50 to-yellow-100/20 dark:from-yellow-900/10 dark:to-yellow-800/5' + case 2: return 'border-slate-300/30 bg-gradient-to-br from-slate-50/50 to-slate-100/20 dark:from-slate-800/10 dark:to-slate-700/5' + case 1: return 'border-orange-300/30 bg-gradient-to-br from-orange-50/50 to-orange-100/20 dark:from-orange-900/10 dark:to-orange-800/5' + default: return 'border-border bg-card' + } + } + + const getTierTextColor = (tier: number) => { + switch (tier) { + case 3: return 'text-yellow-600 dark:text-yellow-400' + case 2: return 'text-slate-600 dark:text-slate-400' + case 1: return 'text-orange-600 dark:text-orange-400' + default: return 'text-foreground' + } + } + + // Current Selection Data + const currentDim = treeData.find(d => d.dimension === selectedDimId) + const currentGroup = currentDim?.groups?.find(g => g.groupId === selectedGroupId) + + // Stats + const totalBadges = treeData.reduce((acc, dim) => acc + (dim.groups?.reduce((a, g) => a + (g.badges?.length || 0), 0) || 0), 0) + const maxReward = treeData.reduce((acc, dim) => Math.max(acc, dim.groups?.reduce((a, g) => Math.max(a, g.badges?.reduce((b, bd) => Math.max(b, bd.rewardSunlight), 0) || 0), 0) || 0), 0) + const totalDimensions = treeData.length -export default function BadgePage() { return ( -
-
-

- - 徽章管理 -

-

配置用户可获得的徽章及其获取条件。

-
- -
- {/* Placeholder Cards to look nice */} - {[1, 2, 3].map((i) => ( - - - 徽章示例 {i} - - - -
-
- -
-
-
-
-
-
- ))} - - - -
- +
+ {/* Header Stats */} +
+ + + 徽章总数 + + + +
{totalBadges}
+

系统已配置的徽章总量

+
+
+ + + 最高奖励 + + + +
{maxReward} ☀️
+

单徽章最高阳光奖励

+
+
+ + + 维度覆盖 + + + +
{totalDimensions}
+

专家、勤勉、记录等维度

+
+
+ handleAdd()}> + + 快速添加 +
+
-

功能开发中

-

- 徽章配置模块即将上线。届时您将能够自定义徽章图标、达成条件以及显示特效。 -

- +
+ +
New
+

创建新的徽章成就

+ + {loading ? ( +
+
+
+ ) : ( +
+ {/* Left Sidebar: Tree Navigation */} + + + + + 维度导航 + + + +
+ {treeData.map(dim => { + const isExpanded = expandedDims.includes(dim.dimension) + const isActiveDim = dim.dimension === selectedDimId + + return ( +
+ {/* Dimension Item */} + + + {/* Groups List */} + {isExpanded && ( +
+ {dim.groups?.map(group => { + const isSelected = group.groupId === selectedGroupId + return ( + + ) + })} + {(!dim.groups || dim.groups.length === 0) && ( +
暂无分组
+ )} +
+ )} +
+ ) + })} +
+
+
+ + {/* Right Content: Stats & Grid */} +
+ {currentDim && currentGroup ? ( + + +
+
+
+ {currentDim.label || currentDim.dimension} + + {currentGroup.groupLabel || currentGroup.groupId} +
+ {currentGroup.groupLabel || currentGroup.groupId} + + 共 {currentGroup.badges?.length || 0} 个徽章配置。 + +
+ +
+
+ + {currentGroup.badges && currentGroup.badges.length > 0 ? ( +
+ {currentGroup.badges.sort((a, b) => a.tier - b.tier).map((badge) => ( +
+ {/* Actions Overlay */} +
+ + +
+ + {/* Tier Label */} +
+ + {getTierIcon(badge.tier)} {getTierLabel(badge.tier)} + + {badge.isHidden && ( + + 隐藏 + + )} +
+ + {/* Icon & Name */} +
+
+
+ {badge.icon && badge.icon.url ? ( + {badge.name} + ) : ( +
{getTierIcon(badge.tier)}
+ )} +
+
+
{badge.name}
+
+ {badge.description || "暂无描述"} +
+
+
+ + {/* Info Footer */} +
+
+ 条件 + {badge.targetAction} +
+
+ 奖励 + + ☀️ +{badge.rewardSunlight} + +
+
+
+ ))} +
+ ) : ( +
+
+ +
+

该分组下暂无徽章

+ +
+ )} +
+
+ ) : ( + +
+ +

请在左侧选择一个维度分组查看详情

+

如果没有任何数据,请先创建维度或联系管理员。

+
+
+ )} +
+
+ )} + + {/* Config Dialog */} + + + + {isEdit ? '编辑徽章' : '新增徽章'} + + 配置徽章的基础信息、触发条件及奖励规则。 + + + +
+ {/* Basic Info */} +
+
+ + setFormData({ ...formData, name: e.target.value })} placeholder="例如:炼金术士(金)" /> +
+
+ +
+ {formData.icon && formData.icon.url ? ( +
+ Badge Icon +
setFormData({ ...formData, iconId: '', icon: undefined })}> + +
+
+ ) : ( +
document.getElementById('icon-upload')?.click()}> + + 点击上传 +
+ )} + +
+
+
+ +
+ +