mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-08 03:07:51 +00:00
fix: add my skills preview to dashboard
This commit is contained in:
parent
04ff4a4a9f
commit
6d2803f41f
5 changed files with 153 additions and 3 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "我的技能",
|
||||
|
|
|
|||
20
web/src/pages/dashboard-preview.test.ts
Normal file
20
web/src/pages/dashboard-preview.test.ts
Normal 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,
|
||||
})
|
||||
})
|
||||
})
|
||||
14
web/src/pages/dashboard-preview.ts
Normal file
14
web/src/pages/dashboard-preview.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue