From a0078a42bc800f5da8be12e8838de2293413f926 Mon Sep 17 00:00:00 2001 From: Chenyme <118253778+chenyme@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:45:49 +0800 Subject: [PATCH 1/5] feat: Update dashboard components for improved styling and functionality --- .../common/dashboard/DashboardMain.tsx | 111 +++--- .../components/common/dashboard/DataCards.tsx | 243 +++++++----- .../common/dashboard/DataCharts.tsx | 359 +++++++++--------- frontend/hooks/use-dashboard.ts | 1 - frontend/lib/services/dashboard/types.ts | 1 - 5 files changed, 393 insertions(+), 322 deletions(-) diff --git a/frontend/components/common/dashboard/DashboardMain.tsx b/frontend/components/common/dashboard/DashboardMain.tsx index 93a51453..49136206 100644 --- a/frontend/components/common/dashboard/DashboardMain.tsx +++ b/frontend/components/common/dashboard/DashboardMain.tsx @@ -53,28 +53,28 @@ export function DashboardMain() { { title: '总用户数', value: data?.summary?.totalUsers, - icon: , + icon: , desc: `+${data?.summary?.newUsers || 0} 新用户`, descColor: 'text-green-600 dark:text-green-400', }, { title: '总项目数', value: data?.summary?.totalProjects, - icon: , + icon: , desc: '项目总数', descColor: 'text-muted-foreground', }, { title: '总领取数', value: data?.summary?.totalReceived, - icon: , + icon: , desc: '历史累计', descColor: 'text-muted-foreground', }, { title: '最近领取数', value: data?.summary?.recentReceived, - icon: , + icon: , desc: `最近${range}天`, descColor: 'text-blue-600 dark:text-blue-400', }, @@ -125,33 +125,31 @@ export function DashboardMain() { }, }; - return ( {/* 问候语标题和时间选择器 */} - +
-

- 👋 {getTimeGreeting()}好,{user?.username || 'Linux Do User'} +

+ {getTimeGreeting()}好,{user?.username || 'Linux Do User'}

-

平台数据概览和趋势分析

{/* 时间范围选择器 */} -
-
+
+
{timeRangeOptions.map((option) => (
- - {/* 统计卡片 - 响应式网格 */} {statsCards.map((card) => ( @@ -182,53 +178,61 @@ export function DashboardMain() { {/* 图表区域 - 1x3 网格布局 */} - + {/* 左侧标签页图表 - 2/3 宽度 */} -
-
+
+
{/* 标签页导航 */} -
-
+
+
+
+ +
+
+ 核心趋势 +
+
+
{/* 标签页内容 */} -
+
{activeTab === 'activity' && ( -
+
)} {activeTab === 'users' && ( -
+
)} {activeTab === 'tags' && ( -
+
{/* 右侧饼图 - 1/3 宽度 */} -
-
- {/* 饼图标题 */} -
-
-
- -
-

分发模式统计

-
-
- {/* 饼图内容 */} -
- } - hideHeader={true} - /> -
-
+
+ } + />
{/* 列表卡片区 - 热门项目、活跃创建者、活跃领取者 */} - + {listCards.map((card) => ( -
-
{title}
-
{icon}
-
-
- {numericValue !== null ? ( - - ) : ( - value - )} +
+
+
+
+ {title} +
+
+
+ {icon} +
- {desc && ( -
- {desc} +
+
+
+ {numericValue !== null ? ( + + ) : ( + value || '--' + )} +
+
+ {desc || '\u00a0'} +
- )} +
); } @@ -40,22 +61,80 @@ export function StatCard({title, value, icon, desc, descColor}: StatCardProps) { * 卡片列表组件 */ export function CardList({title, icon, list, type}: Omit) { + const displayedList = (list || []).slice(0, 10); + + const getMetricValue = (item: ListItemData) => { + switch (type) { + case 'project': + return 'receiveCount' in item ? item.receiveCount : 0; + case 'creator': + return 'projectCount' in item ? item.projectCount : 0; + case 'receiver': + return 'receiveCount' in item ? item.receiveCount : 0; + default: + return 0; + } + }; + + const getMetricLabel = () => { + switch (type) { + case 'project': + return '领取'; + case 'creator': + return '项目'; + case 'receiver': + return '领取'; + default: + return ''; + } + }; + + const getMetricTitle = (item: ListItemData) => { + const value = getMetricValue(item); + + switch (type) { + case 'project': + return `领取数: ${value}`; + case 'creator': + return `项目数: ${value}`; + case 'receiver': + return `领取数: ${value}`; + default: + return String(value); + } + }; + /** * 渲染列表项头像或序号 */ - const renderItemAvatar = (item: ListItemData) => { - if ((type === 'creator' || type === 'receiver') && 'avatar' in item && item.avatar) { + const renderItemLeading = (item: ListItemData, index: number) => { + const rank = String(index + 1).padStart(2, '0'); + + if ( + (type === 'creator' || type === 'receiver') && + 'avatar' in item && + item.avatar + ) { return ( - - - - {item.name?.charAt(0)} - - +
+ + {rank} + + + + + {item.name?.charAt(0)} + + +
); } - return null; + return ( +
+ {rank} +
+ ); }; /** @@ -69,11 +148,9 @@ export function CardList({title, icon, list, type}: Omit { if (type === 'project' && 'tags' in item) { if (item.tags && Array.isArray(item.tags) && item.tags.length > 0) { - const displayTags = item.tags.slice(0, 3); - const remainingCount = item.tags.length - displayTags.length; - return displayTags.join('、') + (remainingCount > 0 ? ` +${remainingCount}` : ''); + return item.tags.slice(0, 2); } else { - return '无标签'; + return []; } } return null; @@ -98,21 +175,29 @@ export function CardList({title, icon, list, type}: Omit -
- +
+ {mainText} - {projectTags && ( -
- {projectTags} +
+
+ {projectTags && projectTags.length > 0 ? ( +
+ {projectTags.map((tag) => ( + + {tag} + + ))}
- )} + ) : subText ? ( +

+ {subText} +

+ ) : null}
- {subText && ( -

- {subText} -

- )}
); }; @@ -121,71 +206,55 @@ export function CardList({title, icon, list, type}: Omit { - const getValue = () => { - switch (type) { - case 'project': - return 'receiveCount' in item ? item.receiveCount : ''; - case 'creator': - return 'projectCount' in item ? item.projectCount : ''; - case 'receiver': - return 'receiveCount' in item ? item.receiveCount : ''; - default: - return ''; - } - }; - - const getTitle = () => { - switch (type) { - case 'project': - return 'receiveCount' in item ? `领取数: ${item.receiveCount}` : ''; - case 'creator': - return 'projectCount' in item ? `项目数: ${item.projectCount}` : ''; - case 'receiver': - return 'receiveCount' in item ? `领取数: ${item.receiveCount}` : ''; - default: - return ''; - } - }; + const value = getMetricValue(item); + const label = getMetricLabel(); return (
- {getValue()} +
+ {value} +
+
+ {label} +
); }; return ( -
-
-
-
+
+
+
+
{icon}
-
+
{title}
+
+ {displayedList.length} +
-
-
- {list?.map((item, index) => ( +
+
+ {displayedList.map((item, index) => (
- {renderItemAvatar(item)} + {renderItemLeading(item, index)} {renderItemContent(item)} {renderItemMetric(item)}
))} - {/* 空数据状态 */} - {(!list || list.length === 0) && ( -
+ {displayedList.length === 0 && ( +
暂无数据
)} @@ -231,5 +300,3 @@ export function TagsDisplay({title, tags, icon}: Omit ); } - - diff --git a/frontend/components/common/dashboard/DataCharts.tsx b/frontend/components/common/dashboard/DataCharts.tsx index 075cdc5c..a8d4c9d9 100644 --- a/frontend/components/common/dashboard/DataCharts.tsx +++ b/frontend/components/common/dashboard/DataCharts.tsx @@ -1,9 +1,79 @@ 'use client'; -import {useMemo} from 'react'; +import {useMemo, useState} from 'react'; import {DISTRIBUTION_MODE_NAMES} from '../project/constants'; import {ChartContainerProps, UserGrowthChartProps, ActivityChartProps, CategoryChartProps, DistributeModeChartProps, TooltipProps} from '@/lib/services/dashboard/types'; -import {AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, BarChart, Bar, Legend} from 'recharts'; +import {AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, BarChart, Bar, Legend, CartesianGrid} from 'recharts'; +import {ChartConfig, ChartContainer as UIChartContainer, ChartTooltip, ChartTooltipContent} from '@/components/ui/chart'; + +function formatDateTick(value: string): string { + return value.replace('/', '.'); +} + +function formatYAxisTick(value: number | string): string { + return Number(value) === 0 ? '' : String(value); +} + +function getVisibleDateTicks(data: { date: string }[], range: number): string[] { + if (!data.length) { + return []; + } + + if (range <= 7) { + return data.map((item) => item.date); + } + + const step = range <= 15 ? 2 : 3; + const ticks = data + .filter((_, index) => index % step === 0) + .map((item) => item.date); + + const lastTick = data[data.length - 1]?.date; + if (lastTick && !ticks.includes(lastTick)) { + ticks.push(lastTick); + } + + return ticks; +} + +function truncateCategoryTick(value: string, maxLength = 8): string { + if (value.length <= maxLength) { + return value; + } + + return `${value.slice(0, maxLength - 1)}…`; +} + +const SHARED_CHART_COLORS = [ + '#5b84e6', + '#4cad87', + '#c4963f', + '#c86f9d', + '#846fe6', + '#42a9bc', + '#c57d49', +]; + +const USER_GROWTH_CHART_CONFIG = { + value: { + label: '新增用户', + color: 'var(--chart-1)', + }, +} satisfies ChartConfig; + +const ACTIVITY_CHART_CONFIG = { + value: { + label: '领取数', + color: '#10b981', + }, +} satisfies ChartConfig; + +const CATEGORY_CHART_CONFIG = { + value: { + label: '标签热度', + color: 'var(--chart-1)', + }, +} satisfies ChartConfig; /** * 生成时间范围图表数据 @@ -76,37 +146,26 @@ const ANIMATION_CONFIG = { }, area: { animationBegin: 0, - animationDuration: 800, + animationDuration: 1150, }, line: { animationBegin: 100, - animationDuration: 800, + animationDuration: 1100, }, pie: { - animationBegin: 200, - animationDuration: 1000, + animationBegin: 120, + animationDuration: 1350, }, bar: { - animationBegin: 300, - animationDuration: 900, + animationBegin: 160, + animationDuration: 1200, }, }; // 配色方案 const ENHANCED_COLORS = { // 饼图配色 - pieChart: [ - '#6366f1', - '#10b981', - '#f59e0b', - '#ef4444', - '#8b5cf6', - '#06b6d4', - '#84cc16', - '#f97316', - '#ec4899', - '#6b7280', - ], + pieChart: SHARED_CHART_COLORS, // 柱状图配色 barChart: [ '#3b82f6', @@ -121,50 +180,6 @@ const ENHANCED_COLORS = { }, }; -/** - * 工具提示组件 - */ -const EnhancedTooltip = ({active, payload, label}: TooltipProps) => { - if (active && payload && payload.length) { - return ( -
-
- {/* 标题 */} -
- {label} -
- - {/* 数据项 */} -
- {payload.map((entry, index) => { - let itemColor = entry.color; - if (!itemColor) { - itemColor = entry.payload?.color as string | undefined; - } - - return ( -
-
-
- 数量 -
- - {typeof entry.value === 'number' ? entry.value.toLocaleString() : entry.value} - -
- ); - })} -
-
-
- ); - } - return null; -}; - /** * 饼图工具提示 */ @@ -177,9 +192,9 @@ const PieTooltip = ({active, payload}: TooltipProps) => { const correctColor = (data.payload?.color as string) || data.color; return ( -
-
-
+
+
+
{data.name}
@@ -190,9 +205,9 @@ const PieTooltip = ({active, payload}: TooltipProps) => { className="w-1.5 h-1.5 rounded-full" style={{backgroundColor: correctColor as string}} /> - 数量 + 数量
- + {(data.value as number).toLocaleString()}
@@ -202,9 +217,9 @@ const PieTooltip = ({active, payload}: TooltipProps) => { className="w-1.5 h-1.5 rounded-full" style={{backgroundColor: correctColor as string}} /> - 占比 + 占比
- + {percentage}%
@@ -219,26 +234,26 @@ const PieTooltip = ({active, payload}: TooltipProps) => { /** * 图表容器 */ -function ChartContainer({title, icon, isLoading, children, hideHeader = false}: ChartContainerProps & {hideHeader?: boolean}) { +function DashboardChartContainer({title, icon, isLoading, children, hideHeader = false}: ChartContainerProps & {hideHeader?: boolean}) { return ( -
+
{!hideHeader && (
-
+
{icon && ( -
+
{icon}
)} -

{title}

+

{title}

)} -
+
{isLoading ? (
- 数据加载中... + 数据加载中...
) : children}
@@ -251,57 +266,65 @@ function ChartContainer({title, icon, isLoading, children, hideHeader = false}: */ export function UserGrowthChart({data, isLoading, icon, range = 7, hideHeader = false}: UserGrowthChartProps & {hideHeader?: boolean}) { const chartData = useMemo(() => generateTimeRangeChartData(data, range), [data, range]); + const visibleTicks = useMemo(() => getVisibleDateTicks(chartData, range), [chartData, range]); return ( - +
- - + + - - + + + - `日期: ${label}`} />} - cursor={{stroke: '#3b82f6', strokeWidth: 1, strokeDasharray: '3 3', strokeOpacity: 0.6}} + `日期: ${label}`} />} /> - +
-
+ ); } @@ -310,57 +333,65 @@ export function UserGrowthChart({data, isLoading, icon, range = 7, hideHeader = */ export function ActivityChart({data, isLoading, icon, range = 7, hideHeader = false}: ActivityChartProps & {hideHeader?: boolean}) { const chartData = useMemo(() => generateTimeRangeChartData(data, range), [data, range]); + const visibleTicks = useMemo(() => getVisibleDateTicks(chartData, range), [chartData, range]); return ( - +
- - + + - - + + + - `日期: ${label}`} />} - cursor={{stroke: '#10b981', strokeWidth: 1, strokeDasharray: '3 3', strokeOpacity: 0.6}} + `日期: ${label}`} />} /> - +
-
+ ); } @@ -378,65 +409,61 @@ export function CategoryChart({data, isLoading, icon, hideHeader = false}: Categ if (!isLoading && (!chartData || chartData.length === 0)) { return ( - -
+ +
{icon}
暂无标签数据
- +
); } - // 使用映射并添加颜色信息 - const enhancedData = chartData.map((item, index) => ({ - ...item, - color: ENHANCED_COLORS.barChart[index % ENHANCED_COLORS.barChart.length], - })); - return ( - +
- - + + + truncateCategoryTick(String(value))} /> - `标签: ${label}`} />} - cursor={{fill: 'rgba(59, 130, 246, 0.05)'}} + `标签: ${label}`} />} /> - {enhancedData.map((entry, index) => ( + {chartData.map((_, index) => ( ))} - +
-
+ ); } @@ -445,18 +472,19 @@ export function CategoryChart({data, isLoading, icon, hideHeader = false}: Categ * 分发模式统计饼图 */ export function DistributeModeChart({data, isLoading, icon, hideHeader = false}: DistributeModeChartProps & {hideHeader?: boolean}) { + const [activeIndex, setActiveIndex] = useState(null); const chartData = useMemo(() => { return data && data.length > 0 ? data : []; }, [data]); if (!isLoading && (!chartData || chartData.length === 0)) { return ( - +
{icon}
暂无分发数据
-
+ ); } @@ -469,19 +497,19 @@ export function DistributeModeChart({data, isLoading, icon, hideHeader = false}: })); return ( - -
+ +
- + { - const target = e.target as HTMLElement; - target.style.transform = 'scale(1.05)'; - target.style.filter = 'drop-shadow(0 4px 8px rgba(0,0,0,0.2))'; - }} - onMouseLeave={(e) => { - const target = e.target as HTMLElement; - target.style.transform = 'scale(1)'; - target.style.filter = 'drop-shadow(0 2px 4px rgba(0,0,0,0.1))'; }} + onMouseEnter={() => setActiveIndex(index)} + onMouseLeave={() => setActiveIndex(null)} /> ))} @@ -520,7 +537,7 @@ export function DistributeModeChart({data, isLoading, icon, hideHeader = false}: iconType="circle" wrapperStyle={{ position: 'absolute', - bottom: '-10px', + bottom: '0px', width: '100%', fontSize: '11px', display: 'flex', @@ -545,6 +562,6 @@ export function DistributeModeChart({data, isLoading, icon, hideHeader = false}:
- +
); } diff --git a/frontend/hooks/use-dashboard.ts b/frontend/hooks/use-dashboard.ts index 93552350..b1a4ad14 100644 --- a/frontend/hooks/use-dashboard.ts +++ b/frontend/hooks/use-dashboard.ts @@ -26,7 +26,6 @@ interface UseDashboardReturn { // 全局数据缓存 - 所有组件实例共享 const dataCache = new Map(); const CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存过期时间 - /** * 检查缓存是否有效 * @param cachedData - 缓存的数据 diff --git a/frontend/lib/services/dashboard/types.ts b/frontend/lib/services/dashboard/types.ts index 7e483e5b..cf738800 100644 --- a/frontend/lib/services/dashboard/types.ts +++ b/frontend/lib/services/dashboard/types.ts @@ -264,4 +264,3 @@ export interface TooltipProps { label?: string; labelFormatter?: (label: string, payload?: Record[]) => ReactNode; } - From 4a027e2a3bc925d0c7ebcd22669052d5fdd67cfc Mon Sep 17 00:00:00 2001 From: Chenyme <118253778+chenyme@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:03:54 +0800 Subject: [PATCH 2/5] refactor: Enhance project components with improved styling --- .../components/common/project/MineProject.tsx | 106 +++---- .../components/common/project/ProjectCard.tsx | 297 +++++++----------- .../components/common/project/ProjectMain.tsx | 132 ++++---- frontend/components/ui/tag-filter-popover.tsx | 33 +- 4 files changed, 263 insertions(+), 305 deletions(-) diff --git a/frontend/components/common/project/MineProject.tsx b/frontend/components/common/project/MineProject.tsx index 7fa0f7e5..8fca2e28 100644 --- a/frontend/components/common/project/MineProject.tsx +++ b/frontend/components/common/project/MineProject.tsx @@ -4,7 +4,6 @@ import {useState} from 'react'; import {useRouter} from 'next/navigation'; import {toast} from 'sonner'; import {Button} from '@/components/ui/button'; -import {Card, CardContent} from '@/components/ui/card'; import {Badge} from '@/components/ui/badge'; import { AlertDialog, @@ -170,30 +169,28 @@ export function MineProject({data, LoadingSkeleton}: MineProjectProps) { const renderContent = () => { if ((!(projects || []).length && !loading) || error) { return ( - - - 0 ? - '未找到符合条件的分发项目' : - '点击右上方按钮创建您的第一个分发项目' - } - className="p-12 text-center" - > - {(selectedTags || []).length > 0 && ( - - )} - - - +
+ 0 ? + '未找到符合条件的分发项目' : + '点击右上方按钮创建您的第一个分发项目' + } + className="p-12 text-center" + > + {(selectedTags || []).length > 0 && ( + + )} + +
); } @@ -203,7 +200,7 @@ export function MineProject({data, LoadingSkeleton}: MineProjectProps) { return ( <> -
+
{(projects || []).map((project, index) => ( - + - +
@@ -250,28 +247,28 @@ export function MineProject({data, LoadingSkeleton}: MineProjectProps) {
{totalPages > 1 && ( -
-
+
+
共 {total} 个项目,第 {currentPage} / {totalPages} 页
-
+
@@ -304,32 +301,31 @@ export function MineProject({data, LoadingSkeleton}: MineProjectProps) { return ( - {/* 标题和标签过滤器 */} -
-

所有项目

- +
+

所有项目

+
{total} - +
- {/* 标签筛选器 */} + 筛选标签 } tags={tags} @@ -337,23 +333,23 @@ export function MineProject({data, LoadingSkeleton}: MineProjectProps) { tagSearchKeyword={tagSearchKeyword} isOpen={isTagFilterOpen} onTagToggle={onTagToggle} + onClearAllTags={onClearAllFilters} onTagSearchKeywordChange={onTagSearchKeywordChange} onOpenChange={onTagFilterOpenChange} /> - {/* 当前选择的标签展示 */} {(selectedTags || []).length > 0 && ( - 筛选条件: + 已筛选 {(selectedTags || []).map((tag) => ( onTagToggle(tag)} > {tag} @@ -363,7 +359,7 @@ export function MineProject({data, LoadingSkeleton}: MineProjectProps) { + ))} + {onDelete && ( + + )} +
+ )} +
+ +

+ {description || '暂无描述'} +

+ +
+
+
+ {visibleTags.map((tag) => ( + + #{tag} + ))} - {onDelete && ( - - )} -
- )} + {hiddenTagCount > 0 && ( + + +{hiddenTagCount} + + )} +
-
-

- {project.name} -

+
+ {modeText} +
+
- {isPaid && ( -
- - - {priceNum} {CURRENCY_LABEL} +
+
+ + + {itemText} + + {showRisk && ( + + + {riskText} + + )} + {showPrice && ( + + + {valueText} + + )} + + + {trustText} + {showIpLimit && ( + + + 限制 IP + + )}
- )} -
-
- - - {project.total_items} - +
+ + {statusLabel}
- -

分发总数: {project.total_items}

+ +

开始: {formatDateTimeWithSeconds(project.start_time)}

+

结束: {formatDateTimeWithSeconds(project.end_time)}

- -
-
-

- {project.name} -

- - {DISTRIBUTION_MODE_NAMES[project.distribution_type]} - -
- -
-
- {project.tags && project.tags.length > 0 ? ( - <> - {project.tags.slice(0, 2).map((tag, index) => ( - - {tag} - - ))} - {project.tags.length > 2 && ( - - - - +{project.tags.length - 2} - - - -
-
- 更多标签 ({project.tags.length - 2}个) -
-
- {project.tags.slice(2).map((tag, index) => ( -
- {tag} -
- ))} -
-
-
-
- )} - - ) : ( - - 无标签 - - )} -
- -
- - - {formatDate(project.created_at)} - -
-
-
diff --git a/frontend/components/common/project/ProjectMain.tsx b/frontend/components/common/project/ProjectMain.tsx index ac527424..a6ca4454 100644 --- a/frontend/components/common/project/ProjectMain.tsx +++ b/frontend/components/common/project/ProjectMain.tsx @@ -1,50 +1,43 @@ 'use client'; -import {useState, useEffect, useCallback} from 'react'; +import {useState, useEffect, useCallback, useRef} from 'react'; import {Skeleton} from '@/components/ui/skeleton'; -import {Separator} from '@/components/ui/separator'; import {CreateDialog, MineProject} from '@/components/common/project'; import services from '@/lib/services'; -import {ProjectListItem, ListProjectsRequest} from '@/lib/services/project/types'; +import {ProjectListData, ProjectListItem, ListProjectsRequest} from '@/lib/services/project/types'; import {motion} from 'motion/react'; -const PAGE_SIZE = 12; +const PAGE_SIZE = 18; /** * 加载骨架屏组件 */ const LoadingSkeleton = () => ( -
- {Array.from({length: 12}).map((_, index) => ( -
-
-
- - -
- -
- +
+ {Array.from({length: PAGE_SIZE}).map((_, index) => ( +
+
+ +
+ + +
+
-
- -
+
+ +
-
-
- - -
+
+ + +
-
-
- - -
- -
+
+ +
))} @@ -60,11 +53,12 @@ export function ProjectMain() { const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [currentPage, setCurrentPage] = useState(1); - const [pageCache, setPageCache] = useState>(new Map()); + const [pageCache, setPageCache] = useState>(new Map()); const [tags, setTags] = useState([]); const [selectedTags, setSelectedTags] = useState([]); const [tagSearchKeyword, setTagSearchKeyword] = useState(''); const [isTagFilterOpen, setIsTagFilterOpen] = useState(false); + const latestRequestIdRef = useRef(0); /** * 获取标签列表 @@ -81,13 +75,17 @@ export function ProjectMain() { */ const fetchProjects = useCallback( async (page: number = 1, forceRefresh: boolean = false) => { + const requestId = ++latestRequestIdRef.current; const cacheKey = `${page}-${selectedTags.join(',')}`; if (!forceRefresh && pageCache.has(cacheKey) && !(selectedTags || []).length) { const cachedData = pageCache.get(cacheKey)!; - setProjects(cachedData || []); - setLoading(false); + if (requestId === latestRequestIdRef.current) { + setProjects(cachedData.results || []); + setTotal(cachedData.total || 0); + setLoading(false); + } return; } @@ -100,21 +98,44 @@ export function ProjectMain() { tags: (selectedTags || []).length > 0 ? selectedTags : undefined, }; - const result = await services.project.getMyProjectsSafe(params); + let hasError = false; - if (result.success && result.data) { - setProjects(result.data.results || []); - setTotal(result.data.total || 0); - if (!(selectedTags || []).length) { - setPageCache((prev) => new Map(prev.set(cacheKey, result.data!.results || []))); + try { + const result = await services.project.getMyProjectsSafe(params); + if (requestId !== latestRequestIdRef.current) { + return; + } + + if (result.success && result.data) { + setProjects(result.data.results || []); + setTotal(result.data.total || 0); + if (!(selectedTags || []).length) { + setPageCache((prev) => new Map(prev.set(cacheKey, result.data!))); + } + } else { + hasError = true; + setError(result.error || '获取项目列表失败'); + setProjects([]); + setTotal(0); + } + } catch { + if (requestId !== latestRequestIdRef.current) { + return; } - } else { - setError(result.error || '获取项目列表失败'); + hasError = true; + setError('获取项目列表失败'); setProjects([]); setTotal(0); - } + } finally { + if (requestId !== latestRequestIdRef.current) { + return; + } - setLoading(false); + if (!hasError) { + setError(''); + } + setLoading(false); + } }, [pageCache, selectedTags], ); @@ -147,6 +168,7 @@ export function ProjectMain() { */ const clearAllFilters = () => { setSelectedTags([]); + setTagSearchKeyword(''); setCurrentPage(1); }; @@ -205,34 +227,24 @@ export function ProjectMain() { }, }; - const separatorVariants = { - hidden: {opacity: 0, scaleX: 0}, - visible: { - opacity: 1, - scaleX: 1, - transition: {duration: 0.4, ease: 'easeOut'}, - }, - }; - return ( - -
-

我的项目

-

管理您的项目和分发内容

+ +
+

我的项目

-
+
- - + +
diff --git a/frontend/components/ui/tag-filter-popover.tsx b/frontend/components/ui/tag-filter-popover.tsx index 2850929d..daa0e1a9 100644 --- a/frontend/components/ui/tag-filter-popover.tsx +++ b/frontend/components/ui/tag-filter-popover.tsx @@ -20,6 +20,7 @@ interface TagFilterPopoverProps { isOpen: boolean; align?: 'start' | 'center' | 'end'; onTagToggle: (tag: string) => void; + onClearAllTags?: () => void; onTagSearchKeywordChange: (keyword: string) => void; onOpenChange: (open: boolean) => void; } @@ -32,6 +33,7 @@ export function TagFilterPopover({ isOpen, align = 'end', onTagToggle, + onClearAllTags, onTagSearchKeywordChange, onOpenChange, }: TagFilterPopoverProps) { @@ -39,8 +41,9 @@ export function TagFilterPopover({ tag.toLowerCase().includes(tagSearchKeyword.toLowerCase()), ); - const handleClearAllTags = () => { - (selectedTags || []).forEach(onTagToggle); + const handleClearSelectedTags = () => { + onClearAllTags?.(); + onTagSearchKeywordChange(''); }; const handleSelectAllTags = () => { @@ -55,16 +58,16 @@ export function TagFilterPopover({ {trigger} -
+
- 标签筛选 + 标签筛选 {(selectedTags || []).length > 0 && ( @@ -77,7 +80,7 @@ export function TagFilterPopover({ placeholder="搜索标签..." value={tagSearchKeyword} onChange={(e) => onTagSearchKeywordChange(e.target.value)} - className="pl-7 h-8 text-xs" + className="h-8 pl-7 text-xs" />
@@ -93,7 +96,7 @@ export function TagFilterPopover({
{/* 标签页内容 */} -
+
{activeTab === 'activity' && (
{/* 右侧饼图 - 1/3 宽度 */} -
+
} + icon={} />
diff --git a/frontend/components/common/dashboard/DataCards.tsx b/frontend/components/common/dashboard/DataCards.tsx index 5507cd14..4c6e383b 100644 --- a/frontend/components/common/dashboard/DataCards.tsx +++ b/frontend/components/common/dashboard/DataCards.tsx @@ -3,6 +3,8 @@ import {Avatar, AvatarImage, AvatarFallback} from '@/components/ui/avatar'; import {CountingNumber} from '@/components/animate-ui/text/counting-number'; import {StatCardProps, CardListProps, TagsDisplayProps, ListItemData} from '@/lib/services/dashboard/types'; +import {Tags} from 'lucide-react'; +import {DashboardEmptyState} from './DashboardEmptyState'; /** * 统计卡片组件 @@ -254,9 +256,7 @@ export function CardList({title, icon, list, type}: Omit - 暂无数据 -
+ )}
@@ -293,7 +293,7 @@ export function TagsDisplay({title, tags, icon}: Omit )) ) : ( - 暂无标签数据 + } className="h-[160px]" /> )}
diff --git a/frontend/components/common/dashboard/DataCharts.tsx b/frontend/components/common/dashboard/DataCharts.tsx index a8d4c9d9..1df424a0 100644 --- a/frontend/components/common/dashboard/DataCharts.tsx +++ b/frontend/components/common/dashboard/DataCharts.tsx @@ -5,6 +5,7 @@ import {DISTRIBUTION_MODE_NAMES} from '../project/constants'; import {ChartContainerProps, UserGrowthChartProps, ActivityChartProps, CategoryChartProps, DistributeModeChartProps, TooltipProps} from '@/lib/services/dashboard/types'; import {AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, BarChart, Bar, Legend, CartesianGrid} from 'recharts'; import {ChartConfig, ChartContainer as UIChartContainer, ChartTooltip, ChartTooltipContent} from '@/components/ui/chart'; +import {DashboardEmptyState} from './DashboardEmptyState'; function formatDateTick(value: string): string { return value.replace('/', '.'); @@ -410,10 +411,7 @@ export function CategoryChart({data, isLoading, icon, hideHeader = false}: Categ if (!isLoading && (!chartData || chartData.length === 0)) { return ( -
-
{icon}
- 暂无标签数据 -
+
); } @@ -480,10 +478,7 @@ export function DistributeModeChart({data, isLoading, icon, hideHeader = false}: if (!isLoading && (!chartData || chartData.length === 0)) { return ( -
-
{icon}
- 暂无分发数据 -
+
); } diff --git a/frontend/components/common/layout/ManagementBar.tsx b/frontend/components/common/layout/ManagementBar.tsx index 61446b1b..0e5d74b1 100644 --- a/frontend/components/common/layout/ManagementBar.tsx +++ b/frontend/components/common/layout/ManagementBar.tsx @@ -5,9 +5,9 @@ import { SendIcon, BarChartIcon, FolderIcon, + FolderGit2, ShoppingBag, PlusCircle, - Github, ExternalLinkIcon, User, LogOutIcon, @@ -233,7 +233,7 @@ export function ManagementBar() { className="flex items-center gap-3 p-2 rounded-md hover:bg-muted/50 transition-colors group" >
- +
GitHub 仓库 diff --git a/frontend/components/common/project/MineProject.tsx b/frontend/components/common/project/MineProject.tsx index 8fca2e28..5440bd9b 100644 --- a/frontend/components/common/project/MineProject.tsx +++ b/frontend/components/common/project/MineProject.tsx @@ -152,6 +152,7 @@ export function MineProject({data, LoadingSkeleton}: MineProjectProps) { }; const totalPages = Math.ceil(total / pageSize); + const isEmptyState = ((!(projects || []).length && !loading) || !!error); const handlePrevPage = () => { if (currentPage > 1) { @@ -169,7 +170,7 @@ export function MineProject({data, LoadingSkeleton}: MineProjectProps) { const renderContent = () => { if ((!(projects || []).length && !loading) || error) { return ( -
+
{(selectedTags || []).length > 0 && ( - ))} -
+ setRange(Number(value))} + variant="pill" + > + + {timeRangeOptions.map((option) => ( + + {option.label} + + ))} + +
{/* 统计卡片 - 响应式网格 */} @@ -194,7 +192,7 @@ export function DashboardMain() { {/* 左侧标签页图表 - 2/3 宽度 */}
-
+
{/* 标签页导航 */}
@@ -205,41 +203,27 @@ export function DashboardMain() { 核心趋势
-
- - - -
+ setActiveTab(value as 'activity' | 'users' | 'tags')} + variant="fill" + className="self-start" + > + + + + 领取趋势 + + + + 用户增长 + + + + 标签分布 + + +
{/* 标签页内容 */} diff --git a/frontend/components/common/dashboard/DataCards.tsx b/frontend/components/common/dashboard/DataCards.tsx index 4c6e383b..84587996 100644 --- a/frontend/components/common/dashboard/DataCards.tsx +++ b/frontend/components/common/dashboard/DataCards.tsx @@ -24,7 +24,7 @@ export function StatCard({ return (
@@ -227,7 +227,7 @@ export function CardList({title, icon, list, type}: Omit +
@@ -269,7 +269,7 @@ export function CardList({title, icon, list, type}: Omit) { return ( -
+
{icon && ( @@ -286,7 +286,7 @@ export function TagsDisplay({title, tags, icon}: Omit ( {tag.name} {tag.count} diff --git a/frontend/components/common/dashboard/DataCharts.tsx b/frontend/components/common/dashboard/DataCharts.tsx index 1df424a0..22b8467d 100644 --- a/frontend/components/common/dashboard/DataCharts.tsx +++ b/frontend/components/common/dashboard/DataCharts.tsx @@ -237,7 +237,7 @@ const PieTooltip = ({active, payload}: TooltipProps) => { */ function DashboardChartContainer({title, icon, isLoading, children, hideHeader = false}: ChartContainerProps & {hideHeader?: boolean}) { return ( -
+
{!hideHeader && (
diff --git a/frontend/components/common/layout/ManagementBar.tsx b/frontend/components/common/layout/ManagementBar.tsx index 0e5d74b1..796bc5c1 100644 --- a/frontend/components/common/layout/ManagementBar.tsx +++ b/frontend/components/common/layout/ManagementBar.tsx @@ -19,6 +19,7 @@ import {CreateDialog} from '@/components/common/project/CreateDialog'; import {CountingNumber} from '@/components/animate-ui/text/counting-number'; import {Button} from '@/components/ui/button'; import Link from 'next/link'; +import {PaymentSettingsDialog} from '@/components/common/payment'; import { Dialog, DialogContent, @@ -28,7 +29,6 @@ import { } from '@/components/animate-ui/radix/dialog'; import {Avatar, AvatarFallback, AvatarImage} from '@/components/ui/avatar'; import {TrustLevel} from '@/lib/services/core'; -import {DialogClose} from '@/components/ui/dialog'; const IconOptions = { className: 'h-4 w-4', @@ -58,11 +58,25 @@ export function ManagementBar() { const themeUtils = useThemeUtils(); const {user, isLoading, logout} = useAuth(); const [mounted, setMounted] = useState(false); + const [profileOpen, setProfileOpen] = useState(false); + const [paymentOpen, setPaymentOpen] = useState(false); useEffect(() => { setMounted(true); }, []); + useEffect(() => { + const handleOpenPaymentSettings = () => { + setProfileOpen(false); + setPaymentOpen(true); + }; + + window.addEventListener('linux-do-cdk:open-payment-settings', handleOpenPaymentSettings); + return () => { + window.removeEventListener('linux-do-cdk:open-payment-settings', handleOpenPaymentSettings); + }; + }, []); + const handleLogout = () => { logout('/login').catch((error) => { console.error('登出失败:', error); @@ -104,181 +118,185 @@ export function ManagementBar() { title: '个人信息', icon: , customComponent: ( - - -
- -
-
- - - 个人信息 - -
- {!isLoading && user && ( - <> - {/* 用户信息卡片 */} -
- {/* 用户基本信息和登出按钮 */} -
-
- - - CN - -
-
- {user.username} -
- {user.nickname && ( -
- {user.nickname} + <> + + +
+ +
+
+ + + 个人信息 + +
+ {!isLoading && user && ( + <> + {/* 用户信息卡片 */} +
+ {/* 用户基本信息和登出按钮 */} +
+
+ + + CN + +
+
+ {user.username} +
+ {user.nickname && ( +
+ {user.nickname} +
+ )} +
+ {user.trust_level !== undefined ? getTrustLevelText(user.trust_level) : '未知'} + + {user.id}
- )} -
- {user.trust_level !== undefined ? getTrustLevelText(user.trust_level) : '未知'} - - {user.id}
-
- -
+ +
-
+
- {/* 用户分数 */} - {user.score !== undefined && ( -
-

社区分数

-
- - + {/* 用户分数 */} + {user.score !== undefined && ( +
+

社区分数

+
+ + +
-
- )} + )} - - )} + + )} - {/* 主题设置 */} - {mounted && ( + {/* 主题设置 */} + {mounted && ( +
+

主题设置

+
+ +
+
+ )} + + {/* 账户设置 */}
-

主题设置

-
+

账户设置

+
- )} - {/* 账户设置 */} -
-

账户设置

-
- + {/* 快速链接区域 */} +
+

快速链接

+
-
- +
+
-
- 支付设置 - 配置你作为收款商户的 clientID / clientSecret + Linux Do 社区 + + +
+
+ GitHub 仓库 - -
-
- - {/* 快速链接区域 */} -
-

快速链接

-
- -
- -
- Linux Do 社区 - - -
- -
- GitHub 仓库 - - -
- -
- 功能反馈 - - -
- -
- 群组交流 - + +
+ +
+ 功能反馈 + + +
+ +
+ 群组交流 + +
-
-
-

关于 LINUX DO CDK

-
-
版本: 1.1.0
-
构建时间: 2025-09-27
-
LINUX DO CDK 是一个为 Linux Do 社区打造的内容分发工具平台,旨在提供快速、安全、便捷的 CDK 分享服务。平台支持多种分发方式,具备完善的用户权限管理和风险控制机制。
+
+

关于 LINUX DO CDK

+
+
版本: 1.2.3
+
构建时间: 2026-04-21
+
LINUX DO CDK 是一个为 Linux Do 社区打造的内容分发工具平台,旨在提供快速、安全、便捷的 CDK 分享服务。平台支持多种分发方式,具备完善的用户权限管理和风险控制机制。
+
-
- {!isLoading && !user && ( -
+ {!isLoading && !user && ( +
未登录用户 -
- )} -
- -
+
+ )} +
+ + + + ), }, ]; diff --git a/frontend/components/common/markdown/ContentRender.tsx b/frontend/components/common/markdown/ContentRender.tsx index 3234db3a..0c6be110 100644 --- a/frontend/components/common/markdown/ContentRender.tsx +++ b/frontend/components/common/markdown/ContentRender.tsx @@ -114,7 +114,7 @@ const markdownComponents: Components = { } return ( - + {children} ); @@ -174,7 +174,7 @@ const markdownComponents: Components = { return (
{/* 代码块头部:包含语言标识和复制按钮 */} -
+
@@ -203,7 +203,7 @@ const markdownComponents: Components = { console.error('复制失败:', error); } }} - className="flex items-center gap-1.5 px-2 py-1 text-xs bg-gray-300 dark:bg-gray-700 hover:bg-gray-400 dark:hover:bg-gray-600 rounded transition-colors duration-200 text-gray-700 dark:text-gray-300" + className="flex items-center gap-1.5 rounded bg-muted px-2 py-1 text-xs text-muted-foreground transition-colors duration-200 hover:bg-muted/80" title="复制代码" > @@ -216,7 +216,7 @@ const markdownComponents: Components = { {/* 代码内容区域:支持自动换行和语法高亮 */}
 {
             const target = e.target as HTMLImageElement;
             const errorDiv = document.createElement('div');
-            errorDiv.className = 'bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg p-4 text-center text-gray-500 dark:text-gray-400 my-6';
+            errorDiv.className = 'bg-muted border border-border rounded-lg p-4 text-center text-muted-foreground my-6';
             errorDiv.innerHTML = `
               
                 
@@ -294,7 +294,7 @@ const markdownComponents: Components = {
   },
   table: ({children}) => (
     
- +
{children}
@@ -305,12 +305,12 @@ const markdownComponents: Components = { ), tbody: ({children}) => ( - + {children} ), tr: ({children}) => ( - + {children} ), diff --git a/frontend/components/common/markdown/Editor.tsx b/frontend/components/common/markdown/Editor.tsx index f65cf240..5daf90c5 100644 --- a/frontend/components/common/markdown/Editor.tsx +++ b/frontend/components/common/markdown/Editor.tsx @@ -329,15 +329,14 @@ export function MarkdownEditor({ const primaryButtons = getPrimaryButtons(); const secondaryButtons = getSecondaryButtons(); + const editorSurfaceClass = 'bg-muted/55 dark:bg-white/[0.04]'; return ( -
- {/* 工具栏 */} -
-
+
+
+
- {/* 主要按钮 */} {primaryButtons.map((button, index) => ( @@ -357,7 +356,6 @@ export function MarkdownEditor({ ))} - {/* 更多按钮下拉菜单 */} {secondaryButtons.length > 0 && ( @@ -387,16 +385,15 @@ export function MarkdownEditor({
- {/* 模式切换 */}
- setMode(value as 'edit' | 'preview')}> - - - + setMode(value as 'edit' | 'preview')} variant="fill"> + + + 编辑 - - + + 预览 @@ -405,14 +402,12 @@ export function MarkdownEditor({
- {/* 内容区域 */}
{mode === 'edit' ? (
- {/* 行号区域 */}
))} - {/* 填充空行以保持最小高度 */} {displayLineCount < 15 && Array.from({length: 15 - displayLineCount}, (_, i) => (
- {/* 文本编辑区域 */} -
+