fix: add my skills preview to dashboard

This commit is contained in:
yun-zhi-ztl 2026-03-16 17:41:03 +08:00
parent 04ff4a4a9f
commit 6d2803f41f
5 changed files with 153 additions and 3 deletions

View file

@ -223,13 +223,20 @@
"platformRoles": "Platform Roles",
"starsAndRatings": "Stars & Ratings",
"viewStars": "View My Stars",
"mySkillsTitle": "My Skills",
"openMySkills": "Open my skills",
"mySkillsPreviewDescription": "Only the first few items are shown here. Open My Skills to see the rest.",
"mySkillsPreviewEmpty": "You have not published any skills yet",
"credentials": "Credentials",
"openTokens": "Open Token Page",
"tokensPreviewDescription": "Only the first few tokens are shown here. Open the token page to see the rest.",
"tokensPreviewEmpty": "No API tokens yet",
"governanceTitle": "Review & Governance",
"viewGovernance": "Open governance center",
"viewPromotions": "View Promotions",
"reportsTitle": "Report Management",
"viewReports": "View skill reports"
"viewReports": "View skill reports",
"previewMore": "..."
},
"mySkills": {
"title": "My Skills",

View file

@ -223,13 +223,20 @@
"platformRoles": "平台角色",
"starsAndRatings": "收藏与评分",
"viewStars": "查看我的收藏",
"mySkillsTitle": "我的技能",
"openMySkills": "打开我的技能",
"mySkillsPreviewDescription": "这里只展示最近的几条,更多内容请进入我的技能页查看。",
"mySkillsPreviewEmpty": "你还没有发布任何技能",
"credentials": "访问凭证",
"openTokens": "打开 Token 页面",
"tokensPreviewDescription": "这里只展示前几条 Token,更多内容请进入 Token 页面查看。",
"tokensPreviewEmpty": "你还没有创建任何 Token",
"governanceTitle": "审核与治理",
"viewGovernance": "打开治理中心",
"viewPromotions": "查看提升审核",
"reportsTitle": "举报管理",
"viewReports": "查看技能举报"
"viewReports": "查看技能举报",
"previewMore": "..."
},
"mySkills": {
"title": "我的技能",

View file

@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest'
import { limitPreviewItems } from './dashboard-preview'
describe('limitPreviewItems', () => {
it('returns all items when the list is within the limit', () => {
expect(limitPreviewItems(['a', 'b'], 3)).toEqual({
items: ['a', 'b'],
hasMore: false,
remainingCount: 0,
})
})
it('returns only the first items and reports the remaining count', () => {
expect(limitPreviewItems(['a', 'b', 'c', 'd'], 3)).toEqual({
items: ['a', 'b', 'c'],
hasMore: true,
remainingCount: 1,
})
})
})

View file

@ -0,0 +1,14 @@
export function limitPreviewItems<T>(items: T[], limit: number): {
items: T[]
hasMore: boolean
remainingCount: number
} {
const visibleItems = items.slice(0, limit)
const remainingCount = Math.max(items.length - visibleItems.length, 0)
return {
items: visibleItems,
hasMore: remainingCount > 0,
remainingCount,
}
}

View file

@ -1,13 +1,26 @@
import { useQuery } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { tokenApi } from '@/api/client'
import { useAuth } from '@/features/auth/use-auth'
import { useMySkills } from '@/shared/hooks/use-skill-queries'
import { TokenList } from '@/features/token/token-list'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card'
import { limitPreviewItems } from './dashboard-preview'
const DASHBOARD_PREVIEW_LIMIT = 3
export function DashboardPage() {
const { t } = useTranslation()
const { user, hasRole } = useAuth()
const governanceVisible = hasRole('SKILL_ADMIN') || hasRole('SUPER_ADMIN')
const { data: skills, isLoading: isLoadingSkills } = useMySkills()
const { data: tokenPage, isLoading: isLoadingTokens } = useQuery({
queryKey: ['tokens', 'dashboard-preview'],
queryFn: () => tokenApi.getTokens({ page: 0, size: DASHBOARD_PREVIEW_LIMIT + 1 }),
})
const skillPreview = limitPreviewItems(skills ?? [], DASHBOARD_PREVIEW_LIMIT)
const tokenPreview = limitPreviewItems(tokenPage?.items ?? [], DASHBOARD_PREVIEW_LIMIT)
return (
<div className="space-y-8 animate-fade-up">
@ -59,13 +72,19 @@ export function DashboardPage() {
</CardContent>
</Card>
<div className={`grid grid-cols-1 gap-4 ${governanceVisible ? 'md:grid-cols-4' : 'md:grid-cols-3'}`}>
<div className={`grid grid-cols-1 gap-4 ${governanceVisible ? 'md:grid-cols-5' : 'md:grid-cols-4'}`}>
<Card className="p-5">
<div className="text-sm text-muted-foreground">{t('dashboard.starsAndRatings')}</div>
<Link to="/dashboard/stars" className="mt-2 inline-block font-semibold text-primary hover:underline">
{t('dashboard.viewStars')}
</Link>
</Card>
<Card className="p-5">
<div className="text-sm text-muted-foreground">{t('dashboard.mySkillsTitle')}</div>
<Link to="/dashboard/skills" className="mt-2 inline-block font-semibold text-primary hover:underline">
{t('dashboard.openMySkills')}
</Link>
</Card>
<Card className="p-5">
<div className="text-sm text-muted-foreground">{t('dashboard.credentials')}</div>
<Link to="/dashboard/tokens" className="mt-2 inline-block font-semibold text-primary hover:underline">
@ -88,6 +107,89 @@ export function DashboardPage() {
) : null}
</div>
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>{t('mySkills.title')}</CardTitle>
<CardDescription>{t('dashboard.mySkillsPreviewDescription')}</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{isLoadingSkills ? (
<div className="space-y-2">
{Array.from({ length: DASHBOARD_PREVIEW_LIMIT }).map((_, index) => (
<div key={index} className="h-10 animate-shimmer rounded-lg" />
))}
</div>
) : skillPreview.items.length > 0 ? (
<>
{skillPreview.items.map((skill) => (
<Link
key={skill.id}
to="/space/$namespace/$slug"
params={{ namespace: skill.namespace, slug: skill.slug }}
className="flex items-center justify-between rounded-lg border border-border/60 px-3 py-2 transition-colors hover:bg-accent/40"
>
<div className="min-w-0">
<div className="truncate text-sm font-medium">{skill.displayName}</div>
<div className="truncate text-xs text-muted-foreground">@{skill.namespace}</div>
</div>
{skill.latestVersion ? (
<span className="ml-3 shrink-0 rounded-full bg-secondary px-2 py-1 text-xs text-muted-foreground">
v{skill.latestVersion}
</span>
) : null}
</Link>
))}
{skillPreview.hasMore ? (
<div className="inline-flex rounded-full bg-secondary px-3 py-1 text-xs text-muted-foreground">
{t('dashboard.previewMore', { count: skillPreview.remainingCount })}
</div>
) : null}
</>
) : (
<div className="text-sm text-muted-foreground">{t('dashboard.mySkillsPreviewEmpty')}</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{t('token.title')}</CardTitle>
<CardDescription>{t('dashboard.tokensPreviewDescription')}</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{isLoadingTokens ? (
<div className="space-y-2">
{Array.from({ length: DASHBOARD_PREVIEW_LIMIT }).map((_, index) => (
<div key={index} className="h-10 animate-shimmer rounded-lg" />
))}
</div>
) : tokenPreview.items.length > 0 ? (
<>
{tokenPreview.items.map((token) => (
<div
key={token.id}
className="flex items-center justify-between rounded-lg border border-border/60 px-3 py-2"
>
<div className="min-w-0">
<div className="truncate text-sm font-medium">{token.name}</div>
<div className="truncate text-xs text-muted-foreground">{token.tokenPrefix}...</div>
</div>
</div>
))}
{tokenPreview.hasMore ? (
<div className="inline-flex rounded-full bg-secondary px-3 py-1 text-xs text-muted-foreground">
{t('dashboard.previewMore', { count: tokenPreview.remainingCount })}
</div>
) : null}
</>
) : (
<div className="text-sm text-muted-foreground">{t('dashboard.tokensPreviewEmpty')}</div>
)}
</CardContent>
</Card>
</div>
<TokenList />
</div>
)