mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-10 22:41:02 +00:00
Merge branch 'feature/improvement'
This commit is contained in:
commit
0cb940e2f8
41 changed files with 1203 additions and 321 deletions
|
|
@ -26,9 +26,10 @@ public class ReviewPermissionChecker {
|
|||
NamespaceType namespaceType,
|
||||
Map<Long, NamespaceRole> userNamespaceRoles,
|
||||
Set<String> platformRoles) {
|
||||
// Cannot review own submission
|
||||
// Admins can review their own submissions
|
||||
if (task.getSubmittedBy().equals(userId)) {
|
||||
return false;
|
||||
return platformRoles.contains("SKILL_ADMIN")
|
||||
|| platformRoles.contains("SUPER_ADMIN");
|
||||
}
|
||||
|
||||
return canReviewNamespace(task.getNamespaceId(), namespaceType, userNamespaceRoles, platformRoles);
|
||||
|
|
|
|||
|
|
@ -16,13 +16,29 @@ class ReviewPermissionCheckerTest {
|
|||
// --- canReview tests ---
|
||||
|
||||
@Test
|
||||
void cannotReviewOwnSubmission() {
|
||||
void regularUserCannotReviewOwnSubmission() {
|
||||
String userId = "user-1";
|
||||
ReviewTask task = new ReviewTask(1L, 10L, userId);
|
||||
assertFalse(checker.canReview(task, userId,
|
||||
NamespaceType.TEAM, Map.of(), Set.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void skillAdminCanReviewOwnSubmission() {
|
||||
String userId = "user-1";
|
||||
ReviewTask task = new ReviewTask(1L, 10L, userId);
|
||||
assertTrue(checker.canReview(task, userId,
|
||||
NamespaceType.TEAM, Map.of(), Set.of("SKILL_ADMIN")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void superAdminCanReviewOwnSubmission() {
|
||||
String userId = "user-1";
|
||||
ReviewTask task = new ReviewTask(1L, 10L, userId);
|
||||
assertTrue(checker.canReview(task, userId,
|
||||
NamespaceType.TEAM, Map.of(), Set.of("SUPER_ADMIN")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void teamAdminCanReviewTeamSkill() {
|
||||
ReviewTask task = new ReviewTask(1L, 10L, "user-2");
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export function Layout() {
|
|||
<span className="text-xl font-bold font-heading text-foreground">SkillHub</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground max-w-sm">
|
||||
现代化的技能注册中心,为开发者提供高效的技能管理和分发平台。
|
||||
{t('layout.footerDescription')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { authApi } from '@/api/client'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import type { OAuthProvider } from '@/api/types'
|
||||
|
|
@ -8,6 +9,7 @@ interface LoginButtonProps {
|
|||
}
|
||||
|
||||
export function LoginButton({ returnTo }: LoginButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
const { data, isLoading } = useQuery<OAuthProvider[]>({
|
||||
queryKey: ['auth', 'providers', returnTo ?? ''],
|
||||
queryFn: () => authApi.getProviders(returnTo),
|
||||
|
|
@ -20,7 +22,7 @@ export function LoginButton({ returnTo }: LoginButtonProps) {
|
|||
<div className="space-y-3">
|
||||
<Button className="w-full h-12" disabled>
|
||||
<div className="w-5 h-5 rounded-full animate-shimmer mr-3" />
|
||||
加载中...
|
||||
{t('loginButton.loading')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -40,7 +42,7 @@ export function LoginButton({ returnTo }: LoginButtonProps) {
|
|||
<svg className="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||
</svg>
|
||||
使用 {provider.name} 登录
|
||||
{t('loginButton.loginWith', { name: provider.name })}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,25 @@
|
|||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { authApi } from '@/api/client'
|
||||
import type { LocalLoginRequest, LocalRegisterRequest } from '@/api/types'
|
||||
import type { LocalLoginRequest, LocalRegisterRequest, User } from '@/api/types'
|
||||
|
||||
export function useLocalLogin() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (request: LocalLoginRequest) => authApi.localLogin(request),
|
||||
onSuccess: (user) => {
|
||||
queryClient.setQueryData<User | null>(['auth', 'me'], user)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useLocalRegister() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (request: LocalRegisterRequest) => authApi.localRegister(request),
|
||||
onSuccess: (user) => {
|
||||
queryClient.setQueryData<User | null>(['auth', 'me'], user)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import type { Namespace } from '@/api/types'
|
||||
import { NamespaceBadge } from '@/shared/components/namespace-badge'
|
||||
|
||||
|
|
@ -6,6 +7,7 @@ interface NamespaceHeaderProps {
|
|||
}
|
||||
|
||||
export function NamespaceHeader({ namespace }: NamespaceHeaderProps) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="flex items-start gap-4 p-6 border rounded-lg bg-card">
|
||||
{namespace.avatarUrl && (
|
||||
|
|
@ -18,7 +20,7 @@ export function NamespaceHeader({ namespace }: NamespaceHeaderProps) {
|
|||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-2xl font-bold">{namespace.displayName}</h1>
|
||||
<NamespaceBadge type={namespace.type} name={namespace.type === 'GLOBAL' ? '全局' : '团队'} />
|
||||
<NamespaceBadge type={namespace.type} name={namespace.type === 'GLOBAL' ? t('myNamespaces.typeGlobal') : t('myNamespaces.typeTeam')} />
|
||||
</div>
|
||||
{namespace.description && (
|
||||
<p className="text-muted-foreground">{namespace.description}</p>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import { cn } from '@/shared/lib/utils'
|
||||
|
||||
|
|
@ -8,6 +9,7 @@ interface UploadZoneProps {
|
|||
}
|
||||
|
||||
export function UploadZone({ onFileSelect, disabled }: UploadZoneProps) {
|
||||
const { t } = useTranslation()
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
if (acceptedFiles.length > 0) {
|
||||
|
|
@ -57,11 +59,11 @@ export function UploadZone({ onFileSelect, disabled }: UploadZoneProps) {
|
|||
</svg>
|
||||
</div>
|
||||
{isDragActive ? (
|
||||
<p className="text-sm text-primary font-medium">放开以上传文件...</p>
|
||||
<p className="text-sm text-primary font-medium">{t('upload.dropHint')}</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm font-medium text-foreground">拖拽 ZIP 文件到此处,或点击选择</p>
|
||||
<p className="text-xs text-muted-foreground">仅支持 .zip 格式</p>
|
||||
<p className="text-sm font-medium text-foreground">{t('upload.dragHint')}</p>
|
||||
<p className="text-xs text-muted-foreground">{t('upload.formatHint')}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
|
||||
|
|
@ -8,7 +9,8 @@ interface SearchBarProps {
|
|||
onSearch?: (query: string) => void
|
||||
}
|
||||
|
||||
export function SearchBar({ defaultValue = '', placeholder = '搜索技能...', onSearch }: SearchBarProps) {
|
||||
export function SearchBar({ defaultValue = '', placeholder, onSearch }: SearchBarProps) {
|
||||
const { t } = useTranslation()
|
||||
const [query, setQuery] = useState(defaultValue)
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
|
|
@ -38,12 +40,12 @@ export function SearchBar({ defaultValue = '', placeholder = '搜索技能...',
|
|||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
placeholder={placeholder || t('searchBar.placeholder')}
|
||||
className="pl-10 border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 h-12"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" size="lg" className="px-8">
|
||||
搜索
|
||||
{t('searchBar.button')}
|
||||
</Button>
|
||||
</form>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import type { SkillFile } from '@/api/types'
|
||||
|
||||
interface FileTreeProps {
|
||||
|
|
@ -12,10 +13,11 @@ function formatFileSize(bytes: number): string {
|
|||
}
|
||||
|
||||
export function FileTree({ files, onFileClick }: FileTreeProps) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="bg-muted px-4 py-2 text-sm font-medium">
|
||||
文件列表 ({files.length})
|
||||
{t('fileTree.title', { count: files.length })}
|
||||
</div>
|
||||
<div className="divide-y">
|
||||
{files.map((file) => (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { CopyButton } from '@/shared/components/copy-button'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Check, Copy } from 'lucide-react'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
|
||||
interface InstallCommandProps {
|
||||
namespace: string
|
||||
|
|
@ -7,14 +10,40 @@ interface InstallCommandProps {
|
|||
}
|
||||
|
||||
export function InstallCommand({ namespace, slug, version }: InstallCommandProps) {
|
||||
const { t } = useTranslation()
|
||||
const [copied, setCopied] = useState(false)
|
||||
const command = version
|
||||
? `skillhub install ${namespace}/${slug}@${version}`
|
||||
: `skillhub install ${namespace}/${slug}`
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(command)
|
||||
setCopied(true)
|
||||
window.setTimeout(() => setCopied(false), 2000)
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 p-3 bg-muted rounded-lg">
|
||||
<code className="flex-1 text-sm font-mono">{command}</code>
|
||||
<CopyButton text={command} />
|
||||
<div className="relative overflow-hidden rounded-xl border border-border/60 bg-muted/50">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleCopy}
|
||||
title={copied ? t('copyButton.copied') : t('copyButton.copy')}
|
||||
aria-label={copied ? t('copyButton.copied') : t('copyButton.copy')}
|
||||
className="absolute right-2 top-2 z-10 h-8 w-8 rounded-md bg-background/80 backdrop-blur hover:bg-background"
|
||||
>
|
||||
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
<pre className="p-4 pr-14 whitespace-pre-wrap break-words">
|
||||
<code className="font-mono text-sm leading-6 text-foreground whitespace-pre-wrap break-words">
|
||||
{command}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,23 +2,51 @@ import ReactMarkdown from 'react-markdown'
|
|||
import rehypeHighlight from 'rehype-highlight'
|
||||
import rehypeSanitize from 'rehype-sanitize'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { cn } from '@/shared/lib/utils'
|
||||
|
||||
interface MarkdownRendererProps {
|
||||
content: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
function stripFrontmatter(content: string) {
|
||||
return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '')
|
||||
}
|
||||
|
||||
export function MarkdownRenderer({ content, className }: MarkdownRendererProps) {
|
||||
const markdown = stripFrontmatter(content).trim()
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div
|
||||
className={cn(
|
||||
'max-w-none text-sm leading-7 text-foreground',
|
||||
'[&_a]:text-primary [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-primary/80',
|
||||
'[&_blockquote]:border-l-4 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:text-muted-foreground',
|
||||
'[&_code]:rounded-md [&_code]:bg-muted/70 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em]',
|
||||
'[&_h1]:mt-0 [&_h1]:mb-4 [&_h1]:text-3xl [&_h1]:font-bold [&_h1]:font-heading [&_h1]:leading-tight',
|
||||
'[&_h2]:mt-10 [&_h2]:mb-4 [&_h2]:border-b [&_h2]:border-border/60 [&_h2]:pb-2 [&_h2]:text-2xl [&_h2]:font-semibold [&_h2]:font-heading',
|
||||
'[&_h3]:mt-8 [&_h3]:mb-3 [&_h3]:text-xl [&_h3]:font-semibold [&_h3]:font-heading',
|
||||
'[&_h4]:mt-6 [&_h4]:mb-2 [&_h4]:text-lg [&_h4]:font-semibold [&_h4]:font-heading',
|
||||
'[&_hr]:my-8 [&_hr]:border-border/60',
|
||||
'[&_img]:rounded-xl [&_img]:border [&_img]:border-border/60',
|
||||
'[&_li]:my-1.5',
|
||||
'[&_ol]:my-4 [&_ol]:list-decimal [&_ol]:pl-6',
|
||||
'[&_p]:my-4',
|
||||
'[&_pre]:my-5 [&_pre]:overflow-x-auto [&_pre]:rounded-xl [&_pre]:border [&_pre]:border-border/60 [&_pre]:bg-slate-950 [&_pre]:p-4 [&_pre]:text-sm [&_pre]:text-slate-100',
|
||||
'[&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-inherit',
|
||||
'[&_table]:my-6 [&_table]:w-full [&_table]:border-collapse [&_table]:overflow-hidden',
|
||||
'[&_tbody_tr]:border-t [&_tbody_tr]:border-border/60',
|
||||
'[&_td]:border [&_td]:border-border/60 [&_td]:px-3 [&_td]:py-2 [&_td]:align-top',
|
||||
'[&_th]:border [&_th]:border-border/60 [&_th]:bg-muted/50 [&_th]:px-3 [&_th]:py-2 [&_th]:text-left [&_th]:font-semibold',
|
||||
'[&_ul]:my-4 [&_ul]:list-disc [&_ul]:pl-6',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeSanitize, rehypeHighlight]}
|
||||
components={{
|
||||
div: (props) => <div className="prose prose-sm dark:prose-invert max-w-none" {...props} />,
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
{markdown}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Star } from 'lucide-react'
|
||||
import { useUserRating, useRate } from './use-rating'
|
||||
import { useAuth } from '@/features/auth/use-auth'
|
||||
|
|
@ -9,6 +10,7 @@ interface RatingInputProps {
|
|||
}
|
||||
|
||||
export function RatingInput({ skillId, onRequireLogin }: RatingInputProps) {
|
||||
const { t } = useTranslation()
|
||||
const { data: userRating, isLoading } = useUserRating(skillId)
|
||||
const rateMutation = useRate(skillId)
|
||||
const { isAuthenticated } = useAuth()
|
||||
|
|
@ -56,7 +58,7 @@ export function RatingInput({ skillId, onRequireLogin }: RatingInputProps) {
|
|||
</div>
|
||||
{currentRating > 0 && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
你的评分: {currentRating} 星
|
||||
{t('ratingInput.yourRating', { score: currentRating })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { useStar, useToggleStar } from './use-star'
|
||||
import { Star } from 'lucide-react'
|
||||
|
|
@ -10,6 +11,7 @@ interface StarButtonProps {
|
|||
}
|
||||
|
||||
export function StarButton({ skillId, starCount, onRequireLogin }: StarButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
const { data: starStatus, isLoading } = useStar(skillId)
|
||||
const toggleMutation = useToggleStar(skillId)
|
||||
const { isAuthenticated } = useAuth()
|
||||
|
|
@ -36,7 +38,7 @@ export function StarButton({ skillId, starCount, onRequireLogin }: StarButtonPro
|
|||
disabled={toggleMutation.isPending}
|
||||
>
|
||||
<Star className={`w-4 h-4 mr-2 ${starStatus.starred ? 'fill-current' : ''}`} />
|
||||
{starStatus.starred ? '已收藏' : '收藏'} ({starCount})
|
||||
{starStatus.starred ? t('starButton.starred') : t('starButton.star')} ({starCount})
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { tokenApi } from '@/api/client'
|
||||
import {
|
||||
|
|
@ -20,6 +21,7 @@ interface CreateTokenDialogProps {
|
|||
}
|
||||
|
||||
export function CreateTokenDialog({ children }: CreateTokenDialogProps) {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [name, setName] = useState('')
|
||||
const [createdToken, setCreatedToken] = useState<CreateTokenResponse | null>(null)
|
||||
|
|
@ -53,17 +55,17 @@ export function CreateTokenDialog({ children }: CreateTokenDialogProps) {
|
|||
{!createdToken ? (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>创建 API Token</DialogTitle>
|
||||
<DialogTitle>{t('createToken.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
创建一个新的 API Token 用于 CLI 或 API 访问
|
||||
{t('createToken.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="token-name">Token 名称</Label>
|
||||
<Label htmlFor="token-name">{t('createToken.nameLabel')}</Label>
|
||||
<Input
|
||||
id="token-name"
|
||||
placeholder="例如: my-cli-token"
|
||||
placeholder={t('createToken.namePlaceholder')}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
|
|
@ -76,33 +78,33 @@ export function CreateTokenDialog({ children }: CreateTokenDialogProps) {
|
|||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
取消
|
||||
{t('dialog.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
disabled={!name.trim() || createMutation.isPending}
|
||||
>
|
||||
{createMutation.isPending ? '创建中...' : '创建'}
|
||||
{createMutation.isPending ? t('createToken.creating') : t('createToken.create')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Token 创建成功</DialogTitle>
|
||||
<DialogTitle>{t('createToken.successTitle')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
请立即复制并保存此 Token,它只会显示一次
|
||||
{t('createToken.successDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Token</Label>
|
||||
<Label>{t('createToken.tokenLabel')}</Label>
|
||||
<div className="rounded-md bg-muted p-3 font-mono text-sm break-all">
|
||||
{createdToken.token}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>名称</Label>
|
||||
<Label>{t('createToken.nameDisplay')}</Label>
|
||||
<div className="text-sm">{createdToken.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -112,10 +114,10 @@ export function CreateTokenDialog({ children }: CreateTokenDialogProps) {
|
|||
navigator.clipboard.writeText(createdToken.token)
|
||||
}}
|
||||
>
|
||||
复制 Token
|
||||
{t('createToken.copyToken')}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
关闭
|
||||
{t('dialog.close')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -25,19 +25,383 @@
|
|||
"title": "Easy Integration",
|
||||
"description": "Seamlessly integrate with your existing tools"
|
||||
}
|
||||
}
|
||||
},
|
||||
"badge": "Enterprise Skill Registry",
|
||||
"tagline": "Publish, Discover, Manage",
|
||||
"taglineHighlight": " Agent Skills",
|
||||
"description": "Self-hosted private skill registry, providing secure and efficient skill sharing and collaboration for teams",
|
||||
"publishSkill": "Publish Skill",
|
||||
"statsSkills": "Skills",
|
||||
"statsDownloads": "Downloads",
|
||||
"statsTeams": "Teams",
|
||||
"whyTitle": "Why Choose",
|
||||
"whyDescription": "Enterprise-grade skill management platform with complete publishing, discovery, and governance capabilities",
|
||||
"featuresList": {
|
||||
"privateDeploy": {
|
||||
"title": "Private Deployment",
|
||||
"description": "Fully self-hosted with data sovereignty. One-click deploy to your infrastructure, running securely behind your firewall."
|
||||
},
|
||||
"versionControl": {
|
||||
"title": "Version Control",
|
||||
"description": "Semantic versioning, custom tags (beta, stable), automatic latest version tracking."
|
||||
},
|
||||
"smartSearch": {
|
||||
"title": "Smart Search",
|
||||
"description": "Full-text search with multi-dimensional filtering by namespace, downloads, ratings, and time."
|
||||
},
|
||||
"teamwork": {
|
||||
"title": "Team Collaboration",
|
||||
"description": "Namespace management, role-based access control (Owner/Admin/Member), publishing policy configuration."
|
||||
},
|
||||
"governance": {
|
||||
"title": "Review & Governance",
|
||||
"description": "Team-level review, platform-level approval, full audit trail, meeting compliance requirements."
|
||||
},
|
||||
"cliFirst": {
|
||||
"title": "CLI First",
|
||||
"description": "Native REST API, compatible with existing ClawHub CLI tools, no client-side changes needed."
|
||||
}
|
||||
},
|
||||
"ctaTitle": "Ready to Get Started?",
|
||||
"ctaDescription": "Start your local development environment with a single command and experience the complete skill management workflow",
|
||||
"ctaButton": "Try Now",
|
||||
"footerDocs": "Docs",
|
||||
"footerGithub": "GitHub",
|
||||
"footerCommunity": "Community"
|
||||
},
|
||||
"home": {
|
||||
"subtitle": "Modern Skill Registry",
|
||||
"description": "Efficient skill management, distribution, and collaboration platform for developers",
|
||||
"browseSkills": "Browse Skills",
|
||||
"publishSkill": "Publish Skill",
|
||||
"popularTitle": "Popular Downloads",
|
||||
"popularDescription": "Most popular skills in the community",
|
||||
"latestTitle": "Latest Releases",
|
||||
"latestDescription": "Freshly published skills",
|
||||
"viewAll": "View All →"
|
||||
},
|
||||
"search": {
|
||||
"title": "Search Skills",
|
||||
"placeholder": "Search skills...",
|
||||
"sort": {
|
||||
"label": "Sort:",
|
||||
"relevance": "Relevance",
|
||||
"downloads": "Downloads",
|
||||
"stars": "Stars",
|
||||
"newest": "Newest"
|
||||
},
|
||||
"noResults": "No skills found",
|
||||
"results": "{{count}} skills found"
|
||||
"noResults": "No results found",
|
||||
"noResultsFor": "No skills found matching \"{{q}}\"",
|
||||
"enterKeyword": "Please enter a search keyword",
|
||||
"results": "{{count}} skills found",
|
||||
"resultCount": "Found <1>{{count}}</1> results"
|
||||
},
|
||||
"searchBar": {
|
||||
"placeholder": "Search skills...",
|
||||
"button": "Search"
|
||||
},
|
||||
"login": {
|
||||
"title": "Login to SkillHub",
|
||||
"subtitle": "Choose a method to continue",
|
||||
"tabPassword": "Password",
|
||||
"tabOAuth": "GitHub",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"usernamePlaceholder": "Enter username",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"submitting": "Logging in...",
|
||||
"submit": "Login",
|
||||
"noAccount": "Don't have an account?",
|
||||
"register": "Sign up now",
|
||||
"oauthHint": "After GitHub authentication, you will be automatically redirected back to this site.",
|
||||
"agreementPrefix": "By logging in, you agree to our",
|
||||
"terms": "Terms of Service",
|
||||
"and": "and",
|
||||
"privacy": "Privacy Policy"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create Account",
|
||||
"subtitle": "Register locally or sign in directly with OAuth.",
|
||||
"tabLocal": "Local Account",
|
||||
"tabOAuth": "OAuth",
|
||||
"username": "Username",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"usernamePlaceholder": "3-64 characters: letters, numbers, or underscores",
|
||||
"emailPlaceholder": "Optional, for account identification",
|
||||
"passwordPlaceholder": "At least 8 characters with 3 character types",
|
||||
"submitting": "Registering...",
|
||||
"submit": "Register & Login",
|
||||
"hasAccount": "Already have an account?",
|
||||
"login": "Back to login",
|
||||
"oauthHint": "Sign in directly with your existing OAuth account, no local password needed."
|
||||
},
|
||||
"device": {
|
||||
"title": "Device Authorization",
|
||||
"subtitle": "Enter the 8-digit user code shown on your device",
|
||||
"codeLabel": "User Code",
|
||||
"codeHint": "Format: XXXX-XXXX (paste supported)",
|
||||
"incompleteCode": "Please enter the complete 8-digit user code",
|
||||
"success": "Device authorized successfully!",
|
||||
"defaultError": "Authorization failed, please check the user code",
|
||||
"submitting": "Authorizing...",
|
||||
"submit": "Authorize Device",
|
||||
"notice": "After authorization, the device will have access to your account"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"subtitle": "Manage your account and API Tokens",
|
||||
"userInfo": "User Info",
|
||||
"userInfoDesc": "Your account details",
|
||||
"loginVia": "Logged in via {{provider}}",
|
||||
"platformRoles": "Platform Roles",
|
||||
"starsAndRatings": "Stars & Ratings",
|
||||
"viewStars": "View My Stars",
|
||||
"credentials": "Credentials",
|
||||
"openTokens": "Open Token Page",
|
||||
"governanceTitle": "Review & Governance",
|
||||
"viewPromotions": "View Promotions"
|
||||
},
|
||||
"mySkills": {
|
||||
"title": "My Skills",
|
||||
"subtitle": "Manage your published skills",
|
||||
"publishNew": "Publish New Skill",
|
||||
"emptyTitle": "No skills yet",
|
||||
"emptyDescription": "Start publishing your first skill",
|
||||
"publishSkill": "Publish Skill"
|
||||
},
|
||||
"myNamespaces": {
|
||||
"title": "My Namespaces",
|
||||
"subtitle": "Manage your namespaces and teams",
|
||||
"create": "Create Namespace",
|
||||
"typeGlobal": "Global",
|
||||
"typeTeam": "Team",
|
||||
"manageMembers": "Manage Members",
|
||||
"reviewTasks": "Review Tasks",
|
||||
"emptyTitle": "No namespaces yet",
|
||||
"emptyDescription": "Create a namespace to organize your skills"
|
||||
},
|
||||
"tokens": {
|
||||
"pageTitle": "Token Management",
|
||||
"pageSubtitle": "Manage access credentials for CLI and API"
|
||||
},
|
||||
"reviews": {
|
||||
"title": "Review Center",
|
||||
"subtitle": "Manage skill version reviews",
|
||||
"tabPending": "Pending",
|
||||
"tabApproved": "Approved",
|
||||
"tabRejected": "Rejected",
|
||||
"empty": "No review tasks",
|
||||
"colSkill": "Skill",
|
||||
"colVersion": "Version",
|
||||
"colSubmitter": "Submitted By",
|
||||
"colSubmitTime": "Submitted At",
|
||||
"colReviewer": "Reviewed By",
|
||||
"colReviewTime": "Reviewed At"
|
||||
},
|
||||
"stars": {
|
||||
"title": "My Stars",
|
||||
"subtitle": "View your starred skills",
|
||||
"empty": "No starred skills yet"
|
||||
},
|
||||
"promotions": {
|
||||
"title": "Promotion Review",
|
||||
"subtitle": "Review team skill promotion requests to global namespace",
|
||||
"tabPending": "Pending",
|
||||
"tabApproved": "Approved",
|
||||
"tabRejected": "Rejected",
|
||||
"commentPlaceholder": "Review comment (optional)",
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"empty": "No promotion requests"
|
||||
},
|
||||
"adminUsers": {
|
||||
"title": "User Management",
|
||||
"subtitle": "Manage platform users and permissions",
|
||||
"searchPlaceholder": "Search username or email...",
|
||||
"filterAll": "All",
|
||||
"filterActive": "Active",
|
||||
"filterPending": "Pending",
|
||||
"filterDisabled": "Disabled",
|
||||
"empty": "No user data",
|
||||
"colUsername": "Username",
|
||||
"colEmail": "Email",
|
||||
"colStatus": "Status",
|
||||
"colRole": "Role",
|
||||
"colCreatedAt": "Created At",
|
||||
"colActions": "Actions",
|
||||
"statusActive": "Active",
|
||||
"statusPending": "Pending",
|
||||
"statusDisabled": "Disabled",
|
||||
"changeRole": "Change Role",
|
||||
"approveUser": "Approve",
|
||||
"disable": "Disable",
|
||||
"enable": "Enable",
|
||||
"totalRecords": "Total {{total}} records, page {{page}}",
|
||||
"prevPage": "Previous",
|
||||
"nextPage": "Next",
|
||||
"changeRoleTitle": "Change User Role",
|
||||
"changeRoleDesc": "Assign a new role to user {{username}}",
|
||||
"roleLabel": "Role",
|
||||
"selectRole": "Select role",
|
||||
"roleUser": "User",
|
||||
"roleReviewer": "Reviewer",
|
||||
"roleUserAdmin": "User Admin",
|
||||
"roleAuditor": "Auditor",
|
||||
"roleSuperAdmin": "Super Admin",
|
||||
"confirmAction": "Confirm Action",
|
||||
"confirmDisable": "Are you sure you want to disable user {{username}}?",
|
||||
"confirmEnable": "Are you sure you want to enable user {{username}}?"
|
||||
},
|
||||
"auditLog": {
|
||||
"title": "Audit Log",
|
||||
"subtitle": "View system operation records",
|
||||
"filterAll": "All",
|
||||
"filterCliPublish": "CLI Publish",
|
||||
"filterCompatPublish": "Compat Publish",
|
||||
"filterReviewApprove": "Review Approved",
|
||||
"filterReviewReject": "Review Rejected",
|
||||
"filterPromotionApprove": "Promotion Approved",
|
||||
"filterYankVersion": "Version Yanked",
|
||||
"userIdPlaceholder": "User ID...",
|
||||
"empty": "No audit logs",
|
||||
"colTime": "Time",
|
||||
"colAction": "Action",
|
||||
"colUserId": "User ID",
|
||||
"colUsername": "Username",
|
||||
"colIp": "IP Address",
|
||||
"colDetail": "Details",
|
||||
"totalRecords": "Total {{total}} records, page {{page}}",
|
||||
"prevPage": "Previous",
|
||||
"nextPage": "Next"
|
||||
},
|
||||
"security": {
|
||||
"title": "Security Settings",
|
||||
"subtitle": "Update your password when local account login is enabled.",
|
||||
"currentPassword": "Current Password",
|
||||
"newPassword": "New Password",
|
||||
"success": "Password changed successfully",
|
||||
"defaultError": "Failed to change password",
|
||||
"submitting": "Submitting...",
|
||||
"submit": "Update Password"
|
||||
},
|
||||
"accounts": {
|
||||
"initiateTitle": "Initiate Account Merge",
|
||||
"initiateDesc": "Enter the secondary account identifier. Supports local username or `provider:subject` format for external identities.",
|
||||
"secondaryLabel": "Secondary Identifier",
|
||||
"secondaryPlaceholder": "e.g.: other_user or github:123456",
|
||||
"initiating": "Initiating...",
|
||||
"initiate": "Initiate Merge",
|
||||
"initiateSuccess": "Merge request created, secondary={{secondaryUserId}}",
|
||||
"initiateError": "Failed to initiate merge",
|
||||
"verifyTitle": "Verify & Complete Merge",
|
||||
"verifyDesc": "Complete token verification first, then confirm to execute data migration.",
|
||||
"mergeRequestId": "Merge Request ID",
|
||||
"verificationToken": "Verification Token",
|
||||
"verifying": "Verifying...",
|
||||
"verify": "Complete Merge",
|
||||
"verifySuccess": "Verification successful, confirm to execute the merge",
|
||||
"verifyError": "Merge verification failed",
|
||||
"confirming": "Confirming...",
|
||||
"confirm": "Confirm & Complete Merge",
|
||||
"confirmSuccess": "Account merge completed",
|
||||
"confirmError": "Merge confirmation failed"
|
||||
},
|
||||
"namespace": {
|
||||
"notFound": "Namespace not found",
|
||||
"skillList": "Skills",
|
||||
"emptyTitle": "No skills",
|
||||
"emptyDescription": "No skills have been published in this namespace yet"
|
||||
},
|
||||
"skillDetail": {
|
||||
"notFound": "Skill not found",
|
||||
"notFoundDesc": "This skill may have been deleted or never existed",
|
||||
"tabReadme": "README",
|
||||
"tabFiles": "Files",
|
||||
"tabVersions": "Versions",
|
||||
"noReadme": "No README",
|
||||
"noFiles": "No files",
|
||||
"noVersions": "No versions",
|
||||
"fileCount": "{{count}} files",
|
||||
"version": "Version",
|
||||
"downloads": "Downloads",
|
||||
"rating": "Rating",
|
||||
"ratingNone": "None",
|
||||
"namespaceLabel": "Namespace",
|
||||
"loginToRate": "Login to star and rate",
|
||||
"install": "Install",
|
||||
"download": "Download",
|
||||
"governance": "Governance",
|
||||
"processing": "Processing...",
|
||||
"hideSkill": "Hide Skill",
|
||||
"unhideSkill": "Unhide Skill",
|
||||
"yankVersion": "Yank Current Version"
|
||||
},
|
||||
"members": {
|
||||
"title": "Member Management",
|
||||
"addMember": "Add Member",
|
||||
"colUserId": "User ID",
|
||||
"colRole": "Role",
|
||||
"colJoinedAt": "Joined At",
|
||||
"colActions": "Actions",
|
||||
"remove": "Remove",
|
||||
"empty": "No members",
|
||||
"namespaceNotFound": "Namespace not found"
|
||||
},
|
||||
"upload": {
|
||||
"dropHint": "Drop to upload...",
|
||||
"dragHint": "Drag a ZIP file here, or click to select",
|
||||
"formatHint": "Only .zip format supported"
|
||||
},
|
||||
"layout": {
|
||||
"footerDescription": "Modern skill registry, providing efficient skill management and distribution for developers."
|
||||
},
|
||||
"nsReviews": {
|
||||
"title": "Namespace Reviews",
|
||||
"loadingNamespace": "Loading namespace info",
|
||||
"reviewsFor": "Review tasks for {{name}}",
|
||||
"empty": "No review records",
|
||||
"version": "Version {{version}}",
|
||||
"tabPending": "Pending",
|
||||
"tabApproved": "Approved",
|
||||
"tabRejected": "Rejected"
|
||||
},
|
||||
"fileTree": {
|
||||
"title": "Files ({{count}})"
|
||||
},
|
||||
"loginButton": {
|
||||
"loading": "Loading...",
|
||||
"loginWith": "Login with {{name}}"
|
||||
},
|
||||
"ratingInput": {
|
||||
"yourRating": "Your rating: {{score}} stars"
|
||||
},
|
||||
"createToken": {
|
||||
"title": "Create API Token",
|
||||
"description": "Create a new API Token for CLI or API access",
|
||||
"nameLabel": "Token Name",
|
||||
"namePlaceholder": "e.g.: my-cli-token",
|
||||
"creating": "Creating...",
|
||||
"create": "Create",
|
||||
"successTitle": "Token Created",
|
||||
"successDescription": "Copy and save this token now. It will only be shown once.",
|
||||
"tokenLabel": "Token",
|
||||
"nameDisplay": "Name",
|
||||
"copyToken": "Copy Token"
|
||||
},
|
||||
"starButton": {
|
||||
"starred": "Starred",
|
||||
"star": "Star"
|
||||
},
|
||||
"copyButton": {
|
||||
"copied": "Copied",
|
||||
"copy": "Copy"
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "Previous",
|
||||
"next": "Next",
|
||||
"pagePrefix": "Page",
|
||||
"pageSuffix": ""
|
||||
},
|
||||
"user": {
|
||||
"menu": {
|
||||
|
|
|
|||
|
|
@ -25,19 +25,383 @@
|
|||
"title": "轻松集成",
|
||||
"description": "无缝集成到您现有的工具中"
|
||||
}
|
||||
}
|
||||
},
|
||||
"badge": "企业级技能注册中心",
|
||||
"tagline": "发布、发现、管理",
|
||||
"taglineHighlight": " Agent 技能包",
|
||||
"description": "自托管的私有技能注册平台,为团队提供安全、高效的技能共享与协作空间",
|
||||
"publishSkill": "发布技能",
|
||||
"statsSkills": "技能包",
|
||||
"statsDownloads": "下载量",
|
||||
"statsTeams": "团队",
|
||||
"whyTitle": "为什么选择",
|
||||
"whyDescription": "专为企业打造的技能管理平台,提供完整的发布、发现、治理能力",
|
||||
"featuresList": {
|
||||
"privateDeploy": {
|
||||
"title": "私有部署",
|
||||
"description": "完全自托管,数据主权在您手中。一键部署到您的基础设施,防火墙后安全运行。"
|
||||
},
|
||||
"versionControl": {
|
||||
"title": "版本管理",
|
||||
"description": "语义化版本控制,自定义标签(beta、stable),自动追踪 latest 版本。"
|
||||
},
|
||||
"smartSearch": {
|
||||
"title": "智能搜索",
|
||||
"description": "全文搜索,支持命名空间、下载量、评分、时间等多维度筛选。"
|
||||
},
|
||||
"teamwork": {
|
||||
"title": "团队协作",
|
||||
"description": "命名空间管理,角色权限控制(Owner/Admin/Member),发布策略配置。"
|
||||
},
|
||||
"governance": {
|
||||
"title": "审核治理",
|
||||
"description": "团队内审核,平台级审批,全流程审计日志,满足合规要求。"
|
||||
},
|
||||
"cliFirst": {
|
||||
"title": "CLI 优先",
|
||||
"description": "原生 REST API,兼容现有 ClawHub CLI 工具,无需客户端改动。"
|
||||
}
|
||||
},
|
||||
"ctaTitle": "准备好开始了吗?",
|
||||
"ctaDescription": "一条命令即可启动本地开发环境,体验完整的技能管理流程",
|
||||
"ctaButton": "立即体验",
|
||||
"footerDocs": "文档",
|
||||
"footerGithub": "GitHub",
|
||||
"footerCommunity": "社区"
|
||||
},
|
||||
"home": {
|
||||
"subtitle": "现代化的技能注册中心",
|
||||
"description": "为开发者提供高效的技能管理、分发和协作平台",
|
||||
"browseSkills": "浏览技能",
|
||||
"publishSkill": "发布技能",
|
||||
"popularTitle": "热门下载",
|
||||
"popularDescription": "社区最受欢迎的技能",
|
||||
"latestTitle": "最新发布",
|
||||
"latestDescription": "刚刚发布的新技能",
|
||||
"viewAll": "查看全部 →"
|
||||
},
|
||||
"search": {
|
||||
"title": "搜索技能",
|
||||
"placeholder": "搜索技能...",
|
||||
"sort": {
|
||||
"label": "排序:",
|
||||
"relevance": "相关性",
|
||||
"downloads": "下载量",
|
||||
"stars": "星标数",
|
||||
"newest": "最新"
|
||||
},
|
||||
"noResults": "未找到技能",
|
||||
"results": "找到 {{count}} 个技能"
|
||||
"noResults": "未找到结果",
|
||||
"noResultsFor": "没有找到与 \"{{q}}\" 相关的技能",
|
||||
"enterKeyword": "请输入搜索关键词",
|
||||
"results": "找到 {{count}} 个技能",
|
||||
"resultCount": "找到 <1>{{count}}</1> 个结果"
|
||||
},
|
||||
"searchBar": {
|
||||
"placeholder": "搜索技能...",
|
||||
"button": "搜索"
|
||||
},
|
||||
"login": {
|
||||
"title": "登录 SkillHub",
|
||||
"subtitle": "选择一个方式登录以继续",
|
||||
"tabPassword": "账号密码",
|
||||
"tabOAuth": "GitHub",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"usernamePlaceholder": "输入用户名",
|
||||
"passwordPlaceholder": "输入密码",
|
||||
"submitting": "登录中...",
|
||||
"submit": "登录",
|
||||
"noAccount": "还没有账号?",
|
||||
"register": "立即注册",
|
||||
"oauthHint": "使用 GitHub 登录时,认证完成后会自动返回当前站点。",
|
||||
"agreementPrefix": "登录即表示你同意我们的",
|
||||
"terms": "服务条款",
|
||||
"and": "和",
|
||||
"privacy": "隐私政策"
|
||||
},
|
||||
"register": {
|
||||
"title": "创建账号",
|
||||
"subtitle": "支持本地注册,也可以直接使用 OAuth 登录进入平台。",
|
||||
"tabLocal": "本地账号",
|
||||
"tabOAuth": "OAuth",
|
||||
"username": "用户名",
|
||||
"email": "邮箱",
|
||||
"password": "密码",
|
||||
"usernamePlaceholder": "3-64 位字母、数字或下划线",
|
||||
"emailPlaceholder": "可选,用于后续账号识别",
|
||||
"passwordPlaceholder": "至少 8 位,包含 3 种字符类型",
|
||||
"submitting": "注册中...",
|
||||
"submit": "注册并登录",
|
||||
"hasAccount": "已有账号?",
|
||||
"login": "返回登录",
|
||||
"oauthHint": "直接使用现有 OAuth 账户进入平台,无需再创建本地密码。"
|
||||
},
|
||||
"device": {
|
||||
"title": "设备授权",
|
||||
"subtitle": "请输入设备上显示的 8 位用户码",
|
||||
"codeLabel": "用户码",
|
||||
"codeHint": "格式: XXXX-XXXX (支持粘贴)",
|
||||
"incompleteCode": "请输入完整的 8 位用户码",
|
||||
"success": "设备授权成功!",
|
||||
"defaultError": "授权失败,请检查用户码是否正确",
|
||||
"submitting": "授权中...",
|
||||
"submit": "授权设备",
|
||||
"notice": "授权后,设备将可以访问你的账户"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"subtitle": "管理你的账户和 API Tokens",
|
||||
"userInfo": "用户信息",
|
||||
"userInfoDesc": "你的账户详情",
|
||||
"loginVia": "通过 {{provider}} 登录",
|
||||
"platformRoles": "平台角色",
|
||||
"starsAndRatings": "收藏与评分",
|
||||
"viewStars": "查看我的收藏",
|
||||
"credentials": "访问凭证",
|
||||
"openTokens": "打开 Token 页面",
|
||||
"governanceTitle": "审核与治理",
|
||||
"viewPromotions": "查看提升审核"
|
||||
},
|
||||
"mySkills": {
|
||||
"title": "我的技能",
|
||||
"subtitle": "管理你发布的技能",
|
||||
"publishNew": "发布新技能",
|
||||
"emptyTitle": "还没有技能",
|
||||
"emptyDescription": "开始发布你的第一个技能吧",
|
||||
"publishSkill": "发布技能"
|
||||
},
|
||||
"myNamespaces": {
|
||||
"title": "我的命名空间",
|
||||
"subtitle": "管理你的命名空间和团队",
|
||||
"create": "创建命名空间",
|
||||
"typeGlobal": "全局",
|
||||
"typeTeam": "团队",
|
||||
"manageMembers": "管理成员",
|
||||
"reviewTasks": "审核任务",
|
||||
"emptyTitle": "还没有命名空间",
|
||||
"emptyDescription": "创建一个命名空间来组织你的技能"
|
||||
},
|
||||
"tokens": {
|
||||
"pageTitle": "Token 管理",
|
||||
"pageSubtitle": "管理 CLI 和 API 使用的访问凭证"
|
||||
},
|
||||
"reviews": {
|
||||
"title": "审核中心",
|
||||
"subtitle": "管理技能版本审核",
|
||||
"tabPending": "待审核",
|
||||
"tabApproved": "已通过",
|
||||
"tabRejected": "已拒绝",
|
||||
"empty": "暂无审核任务",
|
||||
"colSkill": "技能",
|
||||
"colVersion": "版本",
|
||||
"colSubmitter": "提交者",
|
||||
"colSubmitTime": "提交时间",
|
||||
"colReviewer": "审核者",
|
||||
"colReviewTime": "审核时间"
|
||||
},
|
||||
"stars": {
|
||||
"title": "我的收藏",
|
||||
"subtitle": "查看你标记过的技能",
|
||||
"empty": "还没有收藏任何技能"
|
||||
},
|
||||
"promotions": {
|
||||
"title": "提升审核",
|
||||
"subtitle": "审核团队技能提升到全局空间的申请",
|
||||
"tabPending": "待审核",
|
||||
"tabApproved": "已通过",
|
||||
"tabRejected": "已拒绝",
|
||||
"commentPlaceholder": "审核意见(可选)",
|
||||
"approve": "通过",
|
||||
"reject": "拒绝",
|
||||
"empty": "暂无提升申请"
|
||||
},
|
||||
"adminUsers": {
|
||||
"title": "用户管理",
|
||||
"subtitle": "管理平台用户和权限",
|
||||
"searchPlaceholder": "搜索用户名或邮箱...",
|
||||
"filterAll": "全部",
|
||||
"filterActive": "活跃",
|
||||
"filterPending": "待审批",
|
||||
"filterDisabled": "已禁用",
|
||||
"empty": "暂无用户数据",
|
||||
"colUsername": "用户名",
|
||||
"colEmail": "邮箱",
|
||||
"colStatus": "状态",
|
||||
"colRole": "角色",
|
||||
"colCreatedAt": "创建时间",
|
||||
"colActions": "操作",
|
||||
"statusActive": "活跃",
|
||||
"statusPending": "待审批",
|
||||
"statusDisabled": "已禁用",
|
||||
"changeRole": "修改角色",
|
||||
"approveUser": "审批通过",
|
||||
"disable": "禁用",
|
||||
"enable": "启用",
|
||||
"totalRecords": "共 {{total}} 条记录,第 {{page}} 页",
|
||||
"prevPage": "上一页",
|
||||
"nextPage": "下一页",
|
||||
"changeRoleTitle": "修改用户角色",
|
||||
"changeRoleDesc": "为用户 {{username}} 分配新角色",
|
||||
"roleLabel": "角色",
|
||||
"selectRole": "选择角色",
|
||||
"roleUser": "普通用户",
|
||||
"roleReviewer": "审核员",
|
||||
"roleUserAdmin": "用户管理员",
|
||||
"roleAuditor": "审计员",
|
||||
"roleSuperAdmin": "超级管理员",
|
||||
"confirmAction": "确认操作",
|
||||
"confirmDisable": "确定要禁用用户 {{username}} 吗?",
|
||||
"confirmEnable": "确定要启用用户 {{username}} 吗?"
|
||||
},
|
||||
"auditLog": {
|
||||
"title": "审计日志",
|
||||
"subtitle": "查看系统操作记录",
|
||||
"filterAll": "全部",
|
||||
"filterCliPublish": "CLI 发布",
|
||||
"filterCompatPublish": "Compat 发布",
|
||||
"filterReviewApprove": "审核通过",
|
||||
"filterReviewReject": "审核拒绝",
|
||||
"filterPromotionApprove": "提升通过",
|
||||
"filterYankVersion": "版本撤回",
|
||||
"userIdPlaceholder": "用户 ID...",
|
||||
"empty": "暂无审计日志",
|
||||
"colTime": "时间",
|
||||
"colAction": "操作",
|
||||
"colUserId": "用户 ID",
|
||||
"colUsername": "用户名",
|
||||
"colIp": "IP 地址",
|
||||
"colDetail": "详情",
|
||||
"totalRecords": "共 {{total}} 条记录,第 {{page}} 页",
|
||||
"prevPage": "上一页",
|
||||
"nextPage": "下一页"
|
||||
},
|
||||
"security": {
|
||||
"title": "安全设置",
|
||||
"subtitle": "已启用本地账号密码登录时,可以在这里更新密码。",
|
||||
"currentPassword": "当前密码",
|
||||
"newPassword": "新密码",
|
||||
"success": "密码修改成功",
|
||||
"defaultError": "修改密码失败",
|
||||
"submitting": "提交中...",
|
||||
"submit": "更新密码"
|
||||
},
|
||||
"accounts": {
|
||||
"initiateTitle": "发起账号合并",
|
||||
"initiateDesc": "输入 secondary 账号标识。支持本地用户名,或 `provider:subject` 格式的外部身份。",
|
||||
"secondaryLabel": "Secondary 标识",
|
||||
"secondaryPlaceholder": "例如:other_user 或 github:123456",
|
||||
"initiating": "发起中...",
|
||||
"initiate": "发起合并",
|
||||
"initiateSuccess": "已创建合并请求,secondary={{secondaryUserId}}",
|
||||
"initiateError": "发起合并失败",
|
||||
"verifyTitle": "验证并完成合并",
|
||||
"verifyDesc": "先完成 token 验证,再单独确认执行数据迁移。",
|
||||
"mergeRequestId": "Merge Request ID",
|
||||
"verificationToken": "Verification Token",
|
||||
"verifying": "验证中...",
|
||||
"verify": "完成合并",
|
||||
"verifySuccess": "验证成功,确认后将执行正式合并",
|
||||
"verifyError": "验证合并失败",
|
||||
"confirming": "确认中...",
|
||||
"confirm": "确认并完成合并",
|
||||
"confirmSuccess": "账号合并已完成",
|
||||
"confirmError": "确认合并失败"
|
||||
},
|
||||
"namespace": {
|
||||
"notFound": "命名空间不存在",
|
||||
"skillList": "技能列表",
|
||||
"emptyTitle": "暂无技能",
|
||||
"emptyDescription": "该命名空间下还没有发布任何技能"
|
||||
},
|
||||
"skillDetail": {
|
||||
"notFound": "技能不存在",
|
||||
"notFoundDesc": "该技能可能已被删除或从未存在",
|
||||
"tabReadme": "README",
|
||||
"tabFiles": "文件",
|
||||
"tabVersions": "版本",
|
||||
"noReadme": "暂无 README",
|
||||
"noFiles": "暂无文件",
|
||||
"noVersions": "暂无版本",
|
||||
"fileCount": "{{count}} 个文件",
|
||||
"version": "版本",
|
||||
"downloads": "下载量",
|
||||
"rating": "评分",
|
||||
"ratingNone": "暂无",
|
||||
"namespaceLabel": "命名空间",
|
||||
"loginToRate": "登录后可以收藏和评分",
|
||||
"install": "安装",
|
||||
"download": "下载",
|
||||
"governance": "治理操作",
|
||||
"processing": "处理中...",
|
||||
"hideSkill": "隐藏技能",
|
||||
"unhideSkill": "恢复技能",
|
||||
"yankVersion": "撤回当前版本"
|
||||
},
|
||||
"members": {
|
||||
"title": "成员管理",
|
||||
"addMember": "添加成员",
|
||||
"colUserId": "用户 ID",
|
||||
"colRole": "角色",
|
||||
"colJoinedAt": "加入时间",
|
||||
"colActions": "操作",
|
||||
"remove": "移除",
|
||||
"empty": "暂无成员",
|
||||
"namespaceNotFound": "命名空间不存在"
|
||||
},
|
||||
"upload": {
|
||||
"dropHint": "放开以上传文件...",
|
||||
"dragHint": "拖拽 ZIP 文件到此处,或点击选择",
|
||||
"formatHint": "仅支持 .zip 格式"
|
||||
},
|
||||
"layout": {
|
||||
"footerDescription": "现代化的技能注册中心,为开发者提供高效的技能管理和分发平台。"
|
||||
},
|
||||
"nsReviews": {
|
||||
"title": "命名空间审核",
|
||||
"loadingNamespace": "加载命名空间信息中",
|
||||
"reviewsFor": "{{name}} 的审核任务",
|
||||
"empty": "暂无审核记录",
|
||||
"version": "版本 {{version}}",
|
||||
"tabPending": "待审核",
|
||||
"tabApproved": "已通过",
|
||||
"tabRejected": "已拒绝"
|
||||
},
|
||||
"fileTree": {
|
||||
"title": "文件列表 ({{count}})"
|
||||
},
|
||||
"loginButton": {
|
||||
"loading": "加载中...",
|
||||
"loginWith": "使用 {{name}} 登录"
|
||||
},
|
||||
"ratingInput": {
|
||||
"yourRating": "你的评分: {{score}} 星"
|
||||
},
|
||||
"createToken": {
|
||||
"title": "创建 API Token",
|
||||
"description": "创建一个新的 API Token 用于 CLI 或 API 访问",
|
||||
"nameLabel": "Token 名称",
|
||||
"namePlaceholder": "例如: my-cli-token",
|
||||
"creating": "创建中...",
|
||||
"create": "创建",
|
||||
"successTitle": "Token 创建成功",
|
||||
"successDescription": "请立即复制并保存此 Token,它只会显示一次",
|
||||
"tokenLabel": "Token",
|
||||
"nameDisplay": "名称",
|
||||
"copyToken": "复制 Token"
|
||||
},
|
||||
"starButton": {
|
||||
"starred": "已收藏",
|
||||
"star": "收藏"
|
||||
},
|
||||
"copyButton": {
|
||||
"copied": "已复制",
|
||||
"copy": "复制"
|
||||
},
|
||||
"pagination": {
|
||||
"prev": "上一页",
|
||||
"next": "下一页",
|
||||
"pagePrefix": "第",
|
||||
"pageSuffix": "页"
|
||||
},
|
||||
"user": {
|
||||
"menu": {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Card } from '@/shared/ui/card'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
|
|
@ -14,6 +15,7 @@ import {
|
|||
import { useAuditLog } from '@/features/admin/use-audit-log'
|
||||
|
||||
export function AuditLogPage() {
|
||||
const { t, i18n } = useTranslation()
|
||||
const [actionFilter, setActionFilter] = useState<string>('')
|
||||
const [userIdFilter, setUserIdFilter] = useState('')
|
||||
const [page, setPage] = useState(0)
|
||||
|
|
@ -26,29 +28,29 @@ export function AuditLogPage() {
|
|||
})
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString('zh-CN')
|
||||
return new Date(dateString).toLocaleString(i18n.language)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-up">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">审计日志</h1>
|
||||
<p className="text-muted-foreground text-lg">查看系统操作记录</p>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">{t('auditLog.title')}</h1>
|
||||
<p className="text-muted-foreground text-lg">{t('auditLog.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<Card className="p-5">
|
||||
<div className="flex gap-4">
|
||||
<Select value={actionFilter} onChange={(e) => setActionFilter(e.target.value)} className="w-[200px]">
|
||||
<option value="">全部</option>
|
||||
<option value="CLI_PUBLISH">CLI 发布</option>
|
||||
<option value="COMPAT_PUBLISH">Compat 发布</option>
|
||||
<option value="REVIEW_APPROVE">审核通过</option>
|
||||
<option value="REVIEW_REJECT">审核拒绝</option>
|
||||
<option value="PROMOTION_APPROVE">提升通过</option>
|
||||
<option value="YANK_SKILL_VERSION">版本撤回</option>
|
||||
<option value="">{t('auditLog.filterAll')}</option>
|
||||
<option value="CLI_PUBLISH">{t('auditLog.filterCliPublish')}</option>
|
||||
<option value="COMPAT_PUBLISH">{t('auditLog.filterCompatPublish')}</option>
|
||||
<option value="REVIEW_APPROVE">{t('auditLog.filterReviewApprove')}</option>
|
||||
<option value="REVIEW_REJECT">{t('auditLog.filterReviewReject')}</option>
|
||||
<option value="PROMOTION_APPROVE">{t('auditLog.filterPromotionApprove')}</option>
|
||||
<option value="YANK_SKILL_VERSION">{t('auditLog.filterYankVersion')}</option>
|
||||
</Select>
|
||||
<Input
|
||||
placeholder="用户 ID..."
|
||||
placeholder={t('auditLog.userIdPlaceholder')}
|
||||
value={userIdFilter}
|
||||
onChange={(e) => setUserIdFilter(e.target.value)}
|
||||
className="w-[200px]"
|
||||
|
|
@ -64,7 +66,7 @@ export function AuditLogPage() {
|
|||
</div>
|
||||
) : !data || data.items.length === 0 ? (
|
||||
<Card className="p-12 text-center">
|
||||
<p className="text-muted-foreground">暂无审计日志</p>
|
||||
<p className="text-muted-foreground">{t('auditLog.empty')}</p>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -72,12 +74,12 @@ export function AuditLogPage() {
|
|||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>时间</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
<TableHead>用户 ID</TableHead>
|
||||
<TableHead>用户名</TableHead>
|
||||
<TableHead>IP 地址</TableHead>
|
||||
<TableHead>详情</TableHead>
|
||||
<TableHead>{t('auditLog.colTime')}</TableHead>
|
||||
<TableHead>{t('auditLog.colAction')}</TableHead>
|
||||
<TableHead>{t('auditLog.colUserId')}</TableHead>
|
||||
<TableHead>{t('auditLog.colUsername')}</TableHead>
|
||||
<TableHead>{t('auditLog.colIp')}</TableHead>
|
||||
<TableHead>{t('auditLog.colDetail')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
|
@ -99,7 +101,7 @@ export function AuditLogPage() {
|
|||
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
共 {data.total} 条记录,第 {page + 1} 页
|
||||
{t('auditLog.totalRecords', { total: data.total, page: page + 1 })}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
|
|
@ -108,7 +110,7 @@ export function AuditLogPage() {
|
|||
disabled={page === 0}
|
||||
onClick={() => setPage(page - 1)}
|
||||
>
|
||||
上一页
|
||||
{t('auditLog.prevPage')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -116,7 +118,7 @@ export function AuditLogPage() {
|
|||
disabled={(page + 1) * 20 >= data.total}
|
||||
onClick={() => setPage(page + 1)}
|
||||
>
|
||||
下一页
|
||||
{t('auditLog.nextPage')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Card } from '@/shared/ui/card'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
|
|
@ -24,6 +25,7 @@ import { useAdminUsers, useApproveUser, useDisableUser, useEnableUser, useUpdate
|
|||
import type { AdminUser } from '@/features/admin/use-admin-users'
|
||||
|
||||
export function AdminUsersPage() {
|
||||
const { t, i18n } = useTranslation()
|
||||
const [search, setSearch] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState<string>('')
|
||||
const [page, setPage] = useState(0)
|
||||
|
|
@ -46,7 +48,7 @@ export function AdminUsersPage() {
|
|||
const enableUserMutation = useEnableUser()
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString('zh-CN')
|
||||
return new Date(dateString).toLocaleString(i18n.language)
|
||||
}
|
||||
|
||||
const handleChangeRole = (user: AdminUser) => {
|
||||
|
|
@ -90,23 +92,23 @@ export function AdminUsersPage() {
|
|||
return (
|
||||
<div className="space-y-8 animate-fade-up">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">用户管理</h1>
|
||||
<p className="text-muted-foreground text-lg">管理平台用户和权限</p>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">{t('adminUsers.title')}</h1>
|
||||
<p className="text-muted-foreground text-lg">{t('adminUsers.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<Card className="p-5">
|
||||
<div className="flex gap-4">
|
||||
<Input
|
||||
placeholder="搜索用户名或邮箱..."
|
||||
placeholder={t('adminUsers.searchPlaceholder')}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
|
||||
<option value="">全部</option>
|
||||
<option value="ACTIVE">活跃</option>
|
||||
<option value="PENDING">待审批</option>
|
||||
<option value="DISABLED">已禁用</option>
|
||||
<option value="">{t('adminUsers.filterAll')}</option>
|
||||
<option value="ACTIVE">{t('adminUsers.filterActive')}</option>
|
||||
<option value="PENDING">{t('adminUsers.filterPending')}</option>
|
||||
<option value="DISABLED">{t('adminUsers.filterDisabled')}</option>
|
||||
</Select>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
@ -119,7 +121,7 @@ export function AdminUsersPage() {
|
|||
</div>
|
||||
) : !data || data.items.length === 0 ? (
|
||||
<Card className="p-12 text-center">
|
||||
<p className="text-muted-foreground">暂无用户数据</p>
|
||||
<p className="text-muted-foreground">{t('adminUsers.empty')}</p>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -127,12 +129,12 @@ export function AdminUsersPage() {
|
|||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>用户名</TableHead>
|
||||
<TableHead>邮箱</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead>创建时间</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
<TableHead>{t('adminUsers.colUsername')}</TableHead>
|
||||
<TableHead>{t('adminUsers.colEmail')}</TableHead>
|
||||
<TableHead>{t('adminUsers.colStatus')}</TableHead>
|
||||
<TableHead>{t('adminUsers.colRole')}</TableHead>
|
||||
<TableHead>{t('adminUsers.colCreatedAt')}</TableHead>
|
||||
<TableHead>{t('adminUsers.colActions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
|
@ -150,7 +152,7 @@ export function AdminUsersPage() {
|
|||
: 'bg-red-500/10 text-red-400 border-red-500/20'
|
||||
}`}
|
||||
>
|
||||
{user.status === 'ACTIVE' ? '活跃' : user.status === 'PENDING' ? '待审批' : '已禁用'}
|
||||
{user.status === 'ACTIVE' ? t('adminUsers.statusActive') : user.status === 'PENDING' ? t('adminUsers.statusPending') : t('adminUsers.statusDisabled')}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{user.platformRoles.join(', ')}</TableCell>
|
||||
|
|
@ -162,7 +164,7 @@ export function AdminUsersPage() {
|
|||
size="sm"
|
||||
onClick={() => handleChangeRole(user)}
|
||||
>
|
||||
修改角色
|
||||
{t('adminUsers.changeRole')}
|
||||
</Button>
|
||||
{user.status === 'PENDING' && (
|
||||
<Button
|
||||
|
|
@ -170,7 +172,7 @@ export function AdminUsersPage() {
|
|||
size="sm"
|
||||
onClick={() => approveUserMutation.mutate(user.userId)}
|
||||
>
|
||||
审批通过
|
||||
{t('adminUsers.approveUser')}
|
||||
</Button>
|
||||
)}
|
||||
{user.status === 'ACTIVE' ? (
|
||||
|
|
@ -179,7 +181,7 @@ export function AdminUsersPage() {
|
|||
size="sm"
|
||||
onClick={() => handleToggleStatus(user, 'ban')}
|
||||
>
|
||||
禁用
|
||||
{t('adminUsers.disable')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
|
|
@ -187,7 +189,7 @@ export function AdminUsersPage() {
|
|||
size="sm"
|
||||
onClick={() => handleToggleStatus(user, 'unban')}
|
||||
>
|
||||
启用
|
||||
{t('adminUsers.enable')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -200,7 +202,7 @@ export function AdminUsersPage() {
|
|||
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
共 {data.total} 条记录,第 {page + 1} 页
|
||||
{t('adminUsers.totalRecords', { total: data.total, page: page + 1 })}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
|
|
@ -209,7 +211,7 @@ export function AdminUsersPage() {
|
|||
disabled={page === 0}
|
||||
onClick={() => setPage(page - 1)}
|
||||
>
|
||||
上一页
|
||||
{t('adminUsers.prevPage')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -217,7 +219,7 @@ export function AdminUsersPage() {
|
|||
disabled={(page + 1) * 20 >= data.total}
|
||||
onClick={() => setPage(page + 1)}
|
||||
>
|
||||
下一页
|
||||
{t('adminUsers.nextPage')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -227,30 +229,30 @@ export function AdminUsersPage() {
|
|||
<Dialog open={roleDialogOpen} onOpenChange={setRoleDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>修改用户角色</DialogTitle>
|
||||
<DialogTitle>{t('adminUsers.changeRoleTitle')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
为用户 {selectedUser?.username} 分配新角色
|
||||
{t('adminUsers.changeRoleDesc', { username: selectedUser?.username })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="role">角色</Label>
|
||||
<Label htmlFor="role">{t('adminUsers.roleLabel')}</Label>
|
||||
<Select id="role" value={newRole} onChange={(e) => setNewRole(e.target.value)}>
|
||||
<option value="">选择角色</option>
|
||||
<option value="USER">普通用户</option>
|
||||
<option value="REVIEWER">审核员</option>
|
||||
<option value="USER_ADMIN">用户管理员</option>
|
||||
<option value="AUDITOR">审计员</option>
|
||||
<option value="SUPER_ADMIN">超级管理员</option>
|
||||
<option value="">{t('adminUsers.selectRole')}</option>
|
||||
<option value="USER">{t('adminUsers.roleUser')}</option>
|
||||
<option value="REVIEWER">{t('adminUsers.roleReviewer')}</option>
|
||||
<option value="USER_ADMIN">{t('adminUsers.roleUserAdmin')}</option>
|
||||
<option value="AUDITOR">{t('adminUsers.roleAuditor')}</option>
|
||||
<option value="SUPER_ADMIN">{t('adminUsers.roleSuperAdmin')}</option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setRoleDialogOpen(false)}>
|
||||
取消
|
||||
{t('dialog.cancel')}
|
||||
</Button>
|
||||
<Button onClick={confirmRoleChange} disabled={updateRoleMutation.isPending}>
|
||||
确认
|
||||
{t('dialog.confirm')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
|
@ -259,17 +261,17 @@ export function AdminUsersPage() {
|
|||
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>确认操作</DialogTitle>
|
||||
<DialogTitle>{t('adminUsers.confirmAction')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
确定要{actionType === 'ban' ? '禁用' : '启用'}用户 {selectedUser?.username} 吗?
|
||||
{actionType === 'ban' ? t('adminUsers.confirmDisable', { username: selectedUser?.username }) : t('adminUsers.confirmEnable', { username: selectedUser?.username })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setConfirmDialogOpen(false)}>
|
||||
取消
|
||||
{t('dialog.cancel')}
|
||||
</Button>
|
||||
<Button onClick={confirmStatusChange} disabled={disableUserMutation.isPending || enableUserMutation.isPending}>
|
||||
确认
|
||||
{t('dialog.confirm')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,26 @@
|
|||
import { Link } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from '@/features/auth/use-auth'
|
||||
import { TokenList } from '@/features/token/token-list'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card'
|
||||
|
||||
export function DashboardPage() {
|
||||
const { t } = useTranslation()
|
||||
const { user } = useAuth()
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-up">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold font-heading text-foreground">Dashboard</h1>
|
||||
<h1 className="text-4xl font-bold font-heading text-foreground">{t('dashboard.title')}</h1>
|
||||
<p className="text-muted-foreground mt-2 text-lg">
|
||||
管理你的账户和 API Tokens
|
||||
{t('dashboard.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>用户信息</CardTitle>
|
||||
<CardDescription>你的账户详情</CardDescription>
|
||||
<CardTitle>{t('dashboard.userInfo')}</CardTitle>
|
||||
<CardDescription>{t('dashboard.userInfoDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center gap-5">
|
||||
|
|
@ -34,13 +36,13 @@ export function DashboardPage() {
|
|||
<div className="text-sm text-muted-foreground">{user?.email}</div>
|
||||
<div className="text-xs text-muted-foreground flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500" />
|
||||
通过 {user?.oauthProvider} 登录
|
||||
{t('dashboard.loginVia', { provider: user?.oauthProvider })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{user?.platformRoles && user.platformRoles.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium font-heading">平台角色</div>
|
||||
<div className="text-sm font-medium font-heading">{t('dashboard.platformRoles')}</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{user.platformRoles.map((role: string) => (
|
||||
<span
|
||||
|
|
@ -58,21 +60,21 @@ export function DashboardPage() {
|
|||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card className="p-5">
|
||||
<div className="text-sm text-muted-foreground">收藏与评分</div>
|
||||
<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">访问凭证</div>
|
||||
<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">
|
||||
打开 Token 页面
|
||||
{t('dashboard.openTokens')}
|
||||
</Link>
|
||||
</Card>
|
||||
<Card className="p-5">
|
||||
<div className="text-sm text-muted-foreground">审核与治理</div>
|
||||
<div className="text-sm text-muted-foreground">{t('dashboard.governanceTitle')}</div>
|
||||
<Link to="/dashboard/promotions" className="mt-2 inline-block font-semibold text-primary hover:underline">
|
||||
查看提升审核
|
||||
{t('dashboard.viewPromotions')}
|
||||
</Link>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Card } from '@/shared/ui/card'
|
||||
import { NamespaceBadge } from '@/shared/components/namespace-badge'
|
||||
|
|
@ -7,6 +8,7 @@ import { useMyNamespaces } from '@/shared/hooks/use-skill-queries'
|
|||
|
||||
export function MyNamespacesPage() {
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
const { data: namespaces, isLoading } = useMyNamespaces()
|
||||
|
||||
const handleNamespaceClick = (slug: string) => {
|
||||
|
|
@ -37,10 +39,10 @@ export function MyNamespacesPage() {
|
|||
<div className="space-y-8 animate-fade-up">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">我的命名空间</h1>
|
||||
<p className="text-muted-foreground text-lg">管理你的命名空间和团队</p>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">{t('myNamespaces.title')}</h1>
|
||||
<p className="text-muted-foreground text-lg">{t('myNamespaces.subtitle')}</p>
|
||||
</div>
|
||||
<Button disabled>创建命名空间</Button>
|
||||
<Button disabled>{t('myNamespaces.create')}</Button>
|
||||
</div>
|
||||
|
||||
{namespaces && namespaces.length > 0 ? (
|
||||
|
|
@ -60,7 +62,7 @@ export function MyNamespacesPage() {
|
|||
</h3>
|
||||
<NamespaceBadge
|
||||
type={namespace.type}
|
||||
name={namespace.type === 'GLOBAL' ? '全局' : '团队'}
|
||||
name={namespace.type === 'GLOBAL' ? t('myNamespaces.typeGlobal') : t('myNamespaces.typeTeam')}
|
||||
/>
|
||||
</div>
|
||||
{namespace.description && (
|
||||
|
|
@ -78,7 +80,7 @@ export function MyNamespacesPage() {
|
|||
size="sm"
|
||||
onClick={(e) => handleMembersClick(namespace.slug, e)}
|
||||
>
|
||||
管理成员
|
||||
{t('myNamespaces.manageMembers')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
|
|
@ -86,7 +88,7 @@ export function MyNamespacesPage() {
|
|||
size="sm"
|
||||
onClick={(e) => handleReviewsClick(namespace.slug, e)}
|
||||
>
|
||||
审核任务
|
||||
{t('myNamespaces.reviewTasks')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -95,9 +97,9 @@ export function MyNamespacesPage() {
|
|||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
title="还没有命名空间"
|
||||
description="创建一个命名空间来组织你的技能"
|
||||
action={<Button disabled>创建命名空间</Button>}
|
||||
title={t('myNamespaces.emptyTitle')}
|
||||
description={t('myNamespaces.emptyDescription')}
|
||||
action={<Button disabled>{t('myNamespaces.create')}</Button>}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Card } from '@/shared/ui/card'
|
||||
import { EmptyState } from '@/shared/components/empty-state'
|
||||
|
|
@ -6,6 +7,7 @@ import { useMySkills } from '@/shared/hooks/use-skill-queries'
|
|||
|
||||
export function MySkillsPage() {
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
const { data: skills, isLoading } = useMySkills()
|
||||
|
||||
const handleSkillClick = (namespace: string, slug: string) => {
|
||||
|
|
@ -26,11 +28,11 @@ export function MySkillsPage() {
|
|||
<div className="space-y-8 animate-fade-up">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">我的技能</h1>
|
||||
<p className="text-muted-foreground text-lg">管理你发布的技能</p>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">{t('mySkills.title')}</h1>
|
||||
<p className="text-muted-foreground text-lg">{t('mySkills.subtitle')}</p>
|
||||
</div>
|
||||
<Button size="lg" onClick={() => navigate({ to: '/dashboard/publish' })}>
|
||||
发布新技能
|
||||
{t('mySkills.publishNew')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -72,11 +74,11 @@ export function MySkillsPage() {
|
|||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
title="还没有技能"
|
||||
description="开始发布你的第一个技能吧"
|
||||
title={t('mySkills.emptyTitle')}
|
||||
description={t('mySkills.emptyDescription')}
|
||||
action={
|
||||
<Button size="lg" onClick={() => navigate({ to: '/dashboard/publish' })}>
|
||||
发布技能
|
||||
{t('mySkills.publishSkill')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import { useParams } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { NamespaceHeader } from '@/features/namespace/namespace-header'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Card } from '@/shared/ui/card'
|
||||
import { useNamespaceDetail, useNamespaceMembers } from '@/shared/hooks/use-skill-queries'
|
||||
|
||||
export function NamespaceMembersPage() {
|
||||
const { slug } = useParams({ from: '/dashboard/namespaces/$slug/members' })
|
||||
const translation = useTranslation()
|
||||
const t = translation.t
|
||||
const language = translation.i18n.language
|
||||
const params = useParams({ from: '/dashboard/namespaces/$slug/members' })
|
||||
const slug = params.slug
|
||||
|
||||
const { data: namespace, isLoading: isLoadingNamespace } = useNamespaceDetail(slug)
|
||||
const { data: members, isLoading: isLoadingMembers } = useNamespaceMembers(slug)
|
||||
|
|
@ -22,7 +27,7 @@ export function NamespaceMembersPage() {
|
|||
if (!namespace) {
|
||||
return (
|
||||
<div className="text-center py-20 animate-fade-up">
|
||||
<h2 className="text-2xl font-bold font-heading mb-2">命名空间不存在</h2>
|
||||
<h2 className="text-2xl font-bold font-heading mb-2">{t('members.namespaceNotFound')}</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -33,8 +38,8 @@ export function NamespaceMembersPage() {
|
|||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold font-heading">成员管理</h2>
|
||||
<Button disabled>添加成员</Button>
|
||||
<h2 className="text-2xl font-bold font-heading">{t('members.title')}</h2>
|
||||
<Button disabled>{t('members.addMember')}</Button>
|
||||
</div>
|
||||
|
||||
{isLoadingMembers ? (
|
||||
|
|
@ -49,10 +54,10 @@ export function NamespaceMembersPage() {
|
|||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-border/40">
|
||||
<th className="text-left p-4 font-medium font-heading text-sm text-muted-foreground">用户 ID</th>
|
||||
<th className="text-left p-4 font-medium font-heading text-sm text-muted-foreground">角色</th>
|
||||
<th className="text-left p-4 font-medium font-heading text-sm text-muted-foreground">加入时间</th>
|
||||
<th className="text-right p-4 font-medium font-heading text-sm text-muted-foreground">操作</th>
|
||||
<th className="text-left p-4 font-medium font-heading text-sm text-muted-foreground">{t('members.colUserId')}</th>
|
||||
<th className="text-left p-4 font-medium font-heading text-sm text-muted-foreground">{t('members.colRole')}</th>
|
||||
<th className="text-left p-4 font-medium font-heading text-sm text-muted-foreground">{t('members.colJoinedAt')}</th>
|
||||
<th className="text-right p-4 font-medium font-heading text-sm text-muted-foreground">{t('members.colActions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -65,11 +70,11 @@ export function NamespaceMembersPage() {
|
|||
</span>
|
||||
</td>
|
||||
<td className="p-4 text-sm text-muted-foreground">
|
||||
{new Date(member.createdAt).toLocaleDateString('zh-CN')}
|
||||
{new Date(member.createdAt).toLocaleDateString(language)}
|
||||
</td>
|
||||
<td className="p-4 text-right">
|
||||
<Button variant="destructive" size="sm" disabled>
|
||||
移除
|
||||
{t('members.remove')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -80,7 +85,7 @@ export function NamespaceMembersPage() {
|
|||
</Card>
|
||||
) : (
|
||||
<Card className="p-6 text-center text-muted-foreground">
|
||||
暂无成员
|
||||
{t('members.empty')}
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
import { useParams } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Card } from '@/shared/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui/tabs'
|
||||
import { useNamespaceDetail } from '@/shared/hooks/use-skill-queries'
|
||||
import { useReviewList } from '@/features/review/use-review-list'
|
||||
|
||||
function ReviewListSection({ namespaceId }: { namespaceId?: number }) {
|
||||
const { t } = useTranslation()
|
||||
const { data: pending } = useReviewList('PENDING', namespaceId)
|
||||
const { data: approved } = useReviewList('APPROVED', namespaceId)
|
||||
const { data: rejected } = useReviewList('REJECTED', namespaceId)
|
||||
|
||||
const renderItems = (items?: typeof pending) => {
|
||||
if (!items || items.length === 0) {
|
||||
return <Card className="p-10 text-center text-muted-foreground">暂无审核记录</Card>
|
||||
return <Card className="p-10 text-center text-muted-foreground">{t('nsReviews.empty')}</Card>
|
||||
}
|
||||
return (
|
||||
<Card className="divide-y divide-border/40">
|
||||
|
|
@ -20,7 +22,7 @@ function ReviewListSection({ namespaceId }: { namespaceId?: number }) {
|
|||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="font-semibold font-heading">{review.namespace}/{review.skillSlug}</div>
|
||||
<div className="text-sm text-muted-foreground">版本 {review.version}</div>
|
||||
<div className="text-sm text-muted-foreground">{t('nsReviews.version', { version: review.version })}</div>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{new Date(review.submittedAt).toLocaleString('zh-CN')}</div>
|
||||
</div>
|
||||
|
|
@ -36,9 +38,9 @@ function ReviewListSection({ namespaceId }: { namespaceId?: number }) {
|
|||
return (
|
||||
<Tabs defaultValue="PENDING">
|
||||
<TabsList>
|
||||
<TabsTrigger value="PENDING">待审核</TabsTrigger>
|
||||
<TabsTrigger value="APPROVED">已通过</TabsTrigger>
|
||||
<TabsTrigger value="REJECTED">已拒绝</TabsTrigger>
|
||||
<TabsTrigger value="PENDING">{t('nsReviews.tabPending')}</TabsTrigger>
|
||||
<TabsTrigger value="APPROVED">{t('nsReviews.tabApproved')}</TabsTrigger>
|
||||
<TabsTrigger value="REJECTED">{t('nsReviews.tabRejected')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="PENDING" className="mt-6">{renderItems(pending)}</TabsContent>
|
||||
<TabsContent value="APPROVED" className="mt-6">{renderItems(approved)}</TabsContent>
|
||||
|
|
@ -48,15 +50,16 @@ function ReviewListSection({ namespaceId }: { namespaceId?: number }) {
|
|||
}
|
||||
|
||||
export function NamespaceReviewsPage() {
|
||||
const { t } = useTranslation()
|
||||
const { slug } = useParams({ from: '/dashboard/namespaces/$slug/reviews' })
|
||||
const { data: namespace } = useNamespaceDetail(slug)
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-up">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">命名空间审核</h1>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">{t('nsReviews.title')}</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
{namespace ? `${namespace.displayName} 的审核任务` : '加载命名空间信息中'}
|
||||
{namespace ? t('nsReviews.reviewsFor', { name: namespace.displayName }) : t('nsReviews.loadingNamespace')}
|
||||
</p>
|
||||
</div>
|
||||
<ReviewListSection namespaceId={namespace?.id} />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useApprovePromotion, usePromotionList, useRejectPromotion } from '@/features/promotion/use-promotion-list'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Card } from '@/shared/ui/card'
|
||||
|
|
@ -6,6 +7,7 @@ import { Input } from '@/shared/ui/input'
|
|||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui/tabs'
|
||||
|
||||
function PromotionSection({ status }: { status: 'PENDING' | 'APPROVED' | 'REJECTED' }) {
|
||||
const { t, i18n } = useTranslation()
|
||||
const { data: items, isLoading } = usePromotionList(status)
|
||||
const approveMutation = useApprovePromotion()
|
||||
const rejectMutation = useRejectPromotion()
|
||||
|
|
@ -16,7 +18,7 @@ function PromotionSection({ status }: { status: 'PENDING' | 'APPROVED' | 'REJECT
|
|||
}
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
return <Card className="p-10 text-center text-muted-foreground">暂无提升申请</Card>
|
||||
return <Card className="p-10 text-center text-muted-foreground">{t('promotions.empty')}</Card>
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -30,12 +32,12 @@ function PromotionSection({ status }: { status: 'PENDING' | 'APPROVED' | 'REJECT
|
|||
{item.sourceVersion} {'->'} @{item.targetNamespace}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{new Date(item.submittedAt).toLocaleString('zh-CN')}</div>
|
||||
<div className="text-sm text-muted-foreground">{new Date(item.submittedAt).toLocaleString(i18n.language)}</div>
|
||||
</div>
|
||||
{status === 'PENDING' ? (
|
||||
<>
|
||||
<Input
|
||||
placeholder="审核意见(可选)"
|
||||
placeholder={t('promotions.commentPlaceholder')}
|
||||
value={commentById[item.id] ?? ''}
|
||||
onChange={(event) => setCommentById((prev) => ({ ...prev, [item.id]: event.target.value }))}
|
||||
/>
|
||||
|
|
@ -44,14 +46,14 @@ function PromotionSection({ status }: { status: 'PENDING' | 'APPROVED' | 'REJECT
|
|||
onClick={() => approveMutation.mutate({ id: item.id, comment: commentById[item.id] })}
|
||||
disabled={approveMutation.isPending || rejectMutation.isPending}
|
||||
>
|
||||
通过
|
||||
{t('promotions.approve')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => rejectMutation.mutate({ id: item.id, comment: commentById[item.id] })}
|
||||
disabled={approveMutation.isPending || rejectMutation.isPending}
|
||||
>
|
||||
拒绝
|
||||
{t('promotions.reject')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -65,17 +67,18 @@ function PromotionSection({ status }: { status: 'PENDING' | 'APPROVED' | 'REJECT
|
|||
}
|
||||
|
||||
export function PromotionsPage() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-up">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">提升审核</h1>
|
||||
<p className="text-muted-foreground text-lg">审核团队技能提升到全局空间的申请</p>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">{t('promotions.title')}</h1>
|
||||
<p className="text-muted-foreground text-lg">{t('promotions.subtitle')}</p>
|
||||
</div>
|
||||
<Tabs defaultValue="PENDING">
|
||||
<TabsList>
|
||||
<TabsTrigger value="PENDING">待审核</TabsTrigger>
|
||||
<TabsTrigger value="APPROVED">已通过</TabsTrigger>
|
||||
<TabsTrigger value="REJECTED">已拒绝</TabsTrigger>
|
||||
<TabsTrigger value="PENDING">{t('promotions.tabPending')}</TabsTrigger>
|
||||
<TabsTrigger value="APPROVED">{t('promotions.tabApproved')}</TabsTrigger>
|
||||
<TabsTrigger value="REJECTED">{t('promotions.tabRejected')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="PENDING" className="mt-6"><PromotionSection status="PENDING" /></TabsContent>
|
||||
<TabsContent value="APPROVED" className="mt-6"><PromotionSection status="APPROVED" /></TabsContent>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { useReviewDetail, useApproveReview, useRejectReview } from '@/features/r
|
|||
export function ReviewDetailPage() {
|
||||
const { id } = useParams({ from: '/dashboard/reviews/$id' })
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
const { t, i18n } = useTranslation()
|
||||
const taskId = Number(id)
|
||||
|
||||
const { data: review, isLoading } = useReviewDetail(taskId)
|
||||
|
|
@ -25,7 +25,7 @@ export function ReviewDetailPage() {
|
|||
const [rejectDialog, setRejectDialog] = useState(false)
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString('zh-CN')
|
||||
return new Date(dateString).toLocaleString(i18n.language)
|
||||
}
|
||||
|
||||
const handleApprove = async () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Card } from '@/shared/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui/tabs'
|
||||
import {
|
||||
|
|
@ -12,13 +13,14 @@ import {
|
|||
import { useReviewList } from '@/features/review/use-review-list'
|
||||
|
||||
export function ReviewsPage() {
|
||||
const { t, i18n } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { data: pendingReviews, isLoading: isPendingLoading } = useReviewList('PENDING')
|
||||
const { data: approvedReviews, isLoading: isApprovedLoading } = useReviewList('APPROVED')
|
||||
const { data: rejectedReviews, isLoading: isRejectedLoading } = useReviewList('REJECTED')
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString('zh-CN')
|
||||
return new Date(dateString).toLocaleString(i18n.language)
|
||||
}
|
||||
|
||||
const handleRowClick = (reviewId: number) => {
|
||||
|
|
@ -39,7 +41,7 @@ export function ReviewsPage() {
|
|||
if (!reviews || reviews.length === 0) {
|
||||
return (
|
||||
<Card className="p-12 text-center">
|
||||
<p className="text-muted-foreground">暂无审核任务</p>
|
||||
<p className="text-muted-foreground">{t('reviews.empty')}</p>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
|
@ -49,12 +51,12 @@ export function ReviewsPage() {
|
|||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>技能</TableHead>
|
||||
<TableHead>版本</TableHead>
|
||||
<TableHead>提交者</TableHead>
|
||||
<TableHead>提交时间</TableHead>
|
||||
{status !== 'PENDING' && <TableHead>审核者</TableHead>}
|
||||
{status !== 'PENDING' && <TableHead>审核时间</TableHead>}
|
||||
<TableHead>{t('reviews.colSkill')}</TableHead>
|
||||
<TableHead>{t('reviews.colVersion')}</TableHead>
|
||||
<TableHead>{t('reviews.colSubmitter')}</TableHead>
|
||||
<TableHead>{t('reviews.colSubmitTime')}</TableHead>
|
||||
{status !== 'PENDING' && <TableHead>{t('reviews.colReviewer')}</TableHead>}
|
||||
{status !== 'PENDING' && <TableHead>{t('reviews.colReviewTime')}</TableHead>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
|
@ -93,15 +95,15 @@ export function ReviewsPage() {
|
|||
return (
|
||||
<div className="space-y-8 animate-fade-up">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">审核中心</h1>
|
||||
<p className="text-muted-foreground text-lg">管理技能版本审核</p>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">{t('reviews.title')}</h1>
|
||||
<p className="text-muted-foreground text-lg">{t('reviews.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="PENDING">
|
||||
<TabsList>
|
||||
<TabsTrigger value="PENDING">待审核</TabsTrigger>
|
||||
<TabsTrigger value="APPROVED">已通过</TabsTrigger>
|
||||
<TabsTrigger value="REJECTED">已拒绝</TabsTrigger>
|
||||
<TabsTrigger value="PENDING">{t('reviews.tabPending')}</TabsTrigger>
|
||||
<TabsTrigger value="APPROVED">{t('reviews.tabApproved')}</TabsTrigger>
|
||||
<TabsTrigger value="REJECTED">{t('reviews.tabRejected')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="PENDING" className="mt-6">
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SkillCard } from '@/features/skill/skill-card'
|
||||
import { useMyStars } from '@/shared/hooks/use-skill-queries'
|
||||
import { Card } from '@/shared/ui/card'
|
||||
|
||||
export function MyStarsPage() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { data: skills, isLoading } = useMyStars()
|
||||
|
||||
|
|
@ -20,12 +22,12 @@ export function MyStarsPage() {
|
|||
return (
|
||||
<div className="space-y-8 animate-fade-up">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">我的收藏</h1>
|
||||
<p className="text-muted-foreground text-lg">查看你标记过的技能</p>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">{t('stars.title')}</h1>
|
||||
<p className="text-muted-foreground text-lg">{t('stars.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
{!skills || skills.length === 0 ? (
|
||||
<Card className="p-12 text-center text-muted-foreground">还没有收藏任何技能</Card>
|
||||
<Card className="p-12 text-center text-muted-foreground">{t('stars.empty')}</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{skills.map((skill) => (
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { TokenList } from '@/features/token/token-list'
|
||||
|
||||
export function TokensPage() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-up">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">Token 管理</h1>
|
||||
<p className="text-muted-foreground text-lg">管理 CLI 和 API 使用的访问凭证</p>
|
||||
<h1 className="text-4xl font-bold font-heading mb-2">{t('tokens.pageTitle')}</h1>
|
||||
<p className="text-muted-foreground text-lg">{t('tokens.pageSubtitle')}</p>
|
||||
</div>
|
||||
<TokenList />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Card } from '@/shared/ui/card'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
|
|
@ -16,6 +17,7 @@ async function authorizeDevice(userCode: string): Promise<void> {
|
|||
}
|
||||
|
||||
export function DeviceAuthPage() {
|
||||
const { t } = useTranslation()
|
||||
const [part1, setPart1] = useState('')
|
||||
const [part2, setPart2] = useState('')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
|
@ -61,7 +63,7 @@ export function DeviceAuthPage() {
|
|||
e.preventDefault()
|
||||
|
||||
if (part1.length !== 4 || part2.length !== 4) {
|
||||
setMessage({ type: 'error', text: '请输入完整的 8 位用户码' })
|
||||
setMessage({ type: 'error', text: t('device.incompleteCode') })
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -71,14 +73,14 @@ export function DeviceAuthPage() {
|
|||
|
||||
try {
|
||||
await authorizeDevice(userCode)
|
||||
setMessage({ type: 'success', text: '设备授权成功!' })
|
||||
setMessage({ type: 'success', text: t('device.success') })
|
||||
setPart1('')
|
||||
setPart2('')
|
||||
input1Ref.current?.focus()
|
||||
} catch (error) {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: error instanceof Error ? error.message : '授权失败,请检查用户码是否正确'
|
||||
text: error instanceof Error ? error.message : t('device.defaultError')
|
||||
})
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
|
|
@ -94,15 +96,15 @@ export function DeviceAuthPage() {
|
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold font-heading">设备授权</h1>
|
||||
<h1 className="text-3xl font-bold font-heading">{t('device.title')}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
请输入设备上显示的 8 位用户码
|
||||
{t('device.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label>用户码</Label>
|
||||
<Label>{t('device.codeLabel')}</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
ref={input1Ref}
|
||||
|
|
@ -127,7 +129,7 @@ export function DeviceAuthPage() {
|
|||
/>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
格式: XXXX-XXXX (支持粘贴)
|
||||
{t('device.codeHint')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -148,12 +150,12 @@ export function DeviceAuthPage() {
|
|||
className="w-full"
|
||||
disabled={isSubmitting || part1.length !== 4 || part2.length !== 4}
|
||||
>
|
||||
{isSubmitting ? '授权中...' : '授权设备'}
|
||||
{isSubmitting ? t('device.submitting') : t('device.submit')}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
<p>授权后,设备将可以访问你的账户</p>
|
||||
<p>{t('device.notice')}</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SearchBar } from '@/features/search/search-bar'
|
||||
import { SkillCard } from '@/features/skill/skill-card'
|
||||
import { SkeletonList } from '@/shared/components/skeleton-loader'
|
||||
|
|
@ -6,6 +7,7 @@ import { useSearchSkills } from '@/shared/hooks/use-skill-queries'
|
|||
import { Button } from '@/shared/ui/button'
|
||||
|
||||
export function HomePage() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { data: popularSkills, isLoading: isLoadingPopular } = useSearchSkills({
|
||||
|
|
@ -35,10 +37,10 @@ export function HomePage() {
|
|||
SkillHub
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl text-muted-foreground font-light max-w-2xl mx-auto">
|
||||
现代化的技能注册中心
|
||||
{t('home.subtitle')}
|
||||
</p>
|
||||
<p className="text-base text-muted-foreground/80 max-w-xl mx-auto">
|
||||
为开发者提供高效的技能管理、分发和协作平台
|
||||
{t('home.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -48,10 +50,10 @@ export function HomePage() {
|
|||
|
||||
<div className="flex items-center justify-center gap-4 animate-fade-up delay-2">
|
||||
<Button size="lg" onClick={() => navigate({ to: '/search', search: { q: '', sort: 'relevance', page: 0 } })}>
|
||||
浏览技能
|
||||
{t('home.browseSkills')}
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" onClick={() => navigate({ to: '/dashboard/publish' })}>
|
||||
发布技能
|
||||
{t('home.publishSkill')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -60,14 +62,14 @@ export function HomePage() {
|
|||
<section className="space-y-6 animate-fade-up delay-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold font-heading text-foreground mb-2">热门下载</h2>
|
||||
<p className="text-muted-foreground">社区最受欢迎的技能</p>
|
||||
<h2 className="text-3xl font-bold font-heading text-foreground mb-2">{t('home.popularTitle')}</h2>
|
||||
<p className="text-muted-foreground">{t('home.popularDescription')}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => navigate({ to: '/search', search: { q: '', sort: 'downloads', page: 0 } })}
|
||||
>
|
||||
查看全部 →
|
||||
{t('home.viewAll')}
|
||||
</Button>
|
||||
</div>
|
||||
{isLoadingPopular ? (
|
||||
|
|
@ -90,14 +92,14 @@ export function HomePage() {
|
|||
<section className="space-y-6 animate-fade-up delay-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold font-heading text-foreground mb-2">最新发布</h2>
|
||||
<p className="text-muted-foreground">刚刚发布的新技能</p>
|
||||
<h2 className="text-3xl font-bold font-heading text-foreground mb-2">{t('home.latestTitle')}</h2>
|
||||
<p className="text-muted-foreground">{t('home.latestDescription')}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => navigate({ to: '/search', search: { q: '', sort: 'newest', page: 0 } })}
|
||||
>
|
||||
查看全部 →
|
||||
{t('home.viewAll')}
|
||||
</Button>
|
||||
</div>
|
||||
{isLoadingLatest ? (
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SearchBar } from '@/features/search/search-bar'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
export function LandingPage() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const [stats] = useState({
|
||||
|
|
@ -112,33 +114,33 @@ export function LandingPage() {
|
|||
const features = [
|
||||
{
|
||||
icon: '🔒',
|
||||
title: '私有部署',
|
||||
description: '完全自托管,数据主权在您手中。一键部署到您的基础设施,防火墙后安全运行。',
|
||||
title: t('landing.featuresList.privateDeploy.title'),
|
||||
description: t('landing.featuresList.privateDeploy.description'),
|
||||
},
|
||||
{
|
||||
icon: '📦',
|
||||
title: '版本管理',
|
||||
description: '语义化版本控制,自定义标签(beta、stable),自动追踪 latest 版本。',
|
||||
title: t('landing.featuresList.versionControl.title'),
|
||||
description: t('landing.featuresList.versionControl.description'),
|
||||
},
|
||||
{
|
||||
icon: '🔍',
|
||||
title: '智能搜索',
|
||||
description: '全文搜索,支持命名空间、下载量、评分、时间等多维度筛选。',
|
||||
title: t('landing.featuresList.smartSearch.title'),
|
||||
description: t('landing.featuresList.smartSearch.description'),
|
||||
},
|
||||
{
|
||||
icon: '👥',
|
||||
title: '团队协作',
|
||||
description: '命名空间管理,角色权限控制(Owner/Admin/Member),发布策略配置。',
|
||||
title: t('landing.featuresList.teamwork.title'),
|
||||
description: t('landing.featuresList.teamwork.description'),
|
||||
},
|
||||
{
|
||||
icon: '✅',
|
||||
title: '审核治理',
|
||||
description: '团队内审核,平台级审批,全流程审计日志,满足合规要求。',
|
||||
title: t('landing.featuresList.governance.title'),
|
||||
description: t('landing.featuresList.governance.description'),
|
||||
},
|
||||
{
|
||||
icon: '⚡',
|
||||
title: 'CLI 优先',
|
||||
description: '原生 REST API,兼容现有 ClawHub CLI 工具,无需客户端改动。',
|
||||
title: t('landing.featuresList.cliFirst.title'),
|
||||
description: t('landing.featuresList.cliFirst.description'),
|
||||
},
|
||||
]
|
||||
|
||||
|
|
@ -157,7 +159,7 @@ export function LandingPage() {
|
|||
<div className="flex flex-col items-center text-center space-y-12 pt-20 pb-32">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-cyan-500/10 border border-cyan-500/30 backdrop-blur-sm animate-fade-in">
|
||||
<div className="w-2 h-2 rounded-full bg-cyan-400 animate-pulse" />
|
||||
<span className="text-sm text-cyan-300 font-medium">企业级技能注册中心</span>
|
||||
<span className="text-sm text-cyan-300 font-medium">{t('landing.badge')}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 max-w-5xl">
|
||||
|
|
@ -167,11 +169,11 @@ export function LandingPage() {
|
|||
</span>
|
||||
</h1>
|
||||
<p className="text-2xl md:text-3xl text-slate-300 font-light leading-relaxed animate-fade-up delay-1">
|
||||
发布、发现、管理
|
||||
<span className="text-cyan-400 font-medium"> Agent 技能包</span>
|
||||
{t('landing.tagline')}
|
||||
<span className="text-cyan-400 font-medium">{t('landing.taglineHighlight')}</span>
|
||||
</p>
|
||||
<p className="text-lg text-slate-400 max-w-2xl mx-auto animate-fade-up delay-2">
|
||||
自托管的私有技能注册平台,为团队提供安全、高效的技能共享与协作空间
|
||||
{t('landing.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -190,7 +192,7 @@ export function LandingPage() {
|
|||
className="bg-gradient-to-r from-cyan-500 to-blue-500 hover:from-cyan-400 hover:to-blue-400 text-white font-semibold px-8 py-6 text-lg rounded-xl shadow-lg shadow-cyan-500/25 hover:shadow-cyan-500/40 transition-all duration-300 hover:scale-105"
|
||||
onClick={() => navigate({ to: '/search', search: { q: '', sort: 'relevance', page: 0 } })}
|
||||
>
|
||||
探索技能
|
||||
{t('landing.hero.exploreSkills')}
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
|
|
@ -198,7 +200,7 @@ export function LandingPage() {
|
|||
className="border-2 border-slate-600 hover:border-cyan-500 text-slate-200 hover:text-cyan-300 font-semibold px-8 py-6 text-lg rounded-xl backdrop-blur-sm bg-slate-800/30 hover:bg-slate-800/50 transition-all duration-300"
|
||||
onClick={() => navigate({ to: '/dashboard/publish' })}
|
||||
>
|
||||
发布技能
|
||||
{t('landing.publishSkill')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -209,9 +211,9 @@ export function LandingPage() {
|
|||
{value}
|
||||
</div>
|
||||
<div className="text-sm md:text-base text-slate-400 mt-2 capitalize">
|
||||
{key === 'skills' && '技能包'}
|
||||
{key === 'downloads' && '下载量'}
|
||||
{key === 'teams' && '团队'}
|
||||
{key === 'skills' && t('landing.statsSkills')}
|
||||
{key === 'downloads' && t('landing.statsDownloads')}
|
||||
{key === 'teams' && t('landing.statsTeams')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -221,10 +223,10 @@ export function LandingPage() {
|
|||
<div className="py-20 space-y-16">
|
||||
<div className="text-center space-y-4 animate-fade-up">
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-slate-100">
|
||||
为什么选择 <span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-violet-400">SkillHub</span>
|
||||
{t('landing.whyTitle')} <span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-violet-400">SkillHub</span>
|
||||
</h2>
|
||||
<p className="text-lg text-slate-400 max-w-2xl mx-auto">
|
||||
专为企业打造的技能管理平台,提供完整的发布、发现、治理能力
|
||||
{t('landing.whyDescription')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -255,10 +257,10 @@ export function LandingPage() {
|
|||
<div className="absolute inset-0 bg-gradient-to-br from-cyan-500/5 to-violet-500/5 rounded-3xl blur-xl" />
|
||||
<div className="relative space-y-6">
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-slate-100">
|
||||
准备好开始了吗?
|
||||
{t('landing.ctaTitle')}
|
||||
</h2>
|
||||
<p className="text-lg text-slate-300 max-w-2xl mx-auto">
|
||||
一条命令即可启动本地开发环境,体验完整的技能管理流程
|
||||
{t('landing.ctaDescription')}
|
||||
</p>
|
||||
<div className="inline-block p-4 rounded-xl bg-slate-900/80 backdrop-blur-sm border border-slate-700/50">
|
||||
<code className="text-cyan-400 text-lg font-mono">make dev-all</code>
|
||||
|
|
@ -269,7 +271,7 @@ export function LandingPage() {
|
|||
className="bg-gradient-to-r from-cyan-500 to-blue-500 hover:from-cyan-400 hover:to-blue-400 text-white font-semibold px-8 py-6 text-lg rounded-xl shadow-lg shadow-cyan-500/25 hover:shadow-cyan-500/40 transition-all duration-300 hover:scale-105"
|
||||
onClick={() => navigate({ to: '/search', search: { q: '', sort: 'relevance', page: 0 } })}
|
||||
>
|
||||
立即体验
|
||||
{t('landing.ctaButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -282,9 +284,9 @@ export function LandingPage() {
|
|||
<div className="flex flex-col md:flex-row items-center justify-between gap-4 text-slate-400 text-sm">
|
||||
<div>© 2026 SkillHub. MIT License.</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<a href="#" className="hover:text-cyan-400 transition-colors">文档</a>
|
||||
<a href="#" className="hover:text-cyan-400 transition-colors">GitHub</a>
|
||||
<a href="#" className="hover:text-cyan-400 transition-colors">社区</a>
|
||||
<a href="#" className="hover:text-cyan-400 transition-colors">{t('landing.footerDocs')}</a>
|
||||
<a href="#" className="hover:text-cyan-400 transition-colors">{t('landing.footerGithub')}</a>
|
||||
<a href="#" className="hover:text-cyan-400 transition-colors">{t('landing.footerCommunity')}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Link, useNavigate, useSearch } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoginButton } from '@/features/auth/login-button'
|
||||
import { useLocalLogin } from '@/features/auth/use-local-auth'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
|
|
@ -7,6 +8,7 @@ import { Input } from '@/shared/ui/input'
|
|||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui/tabs'
|
||||
|
||||
export function LoginPage() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const search = useSearch({ from: '/login' })
|
||||
const loginMutation = useLocalLogin()
|
||||
|
|
@ -32,57 +34,57 @@ export function LoginPage() {
|
|||
<div className="inline-flex w-16 h-16 rounded-2xl bg-gradient-to-br from-primary to-primary/70 items-center justify-center shadow-glow mb-4">
|
||||
<span className="text-primary-foreground font-bold text-2xl">S</span>
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold font-heading text-foreground">登录 SkillHub</h1>
|
||||
<h1 className="text-4xl font-bold font-heading text-foreground">{t('login.title')}</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
选择一个方式登录以继续
|
||||
{t('login.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="glass-strong p-8 rounded-2xl">
|
||||
<Tabs defaultValue="password" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="password">账号密码</TabsTrigger>
|
||||
<TabsTrigger value="oauth">GitHub</TabsTrigger>
|
||||
<TabsTrigger value="password">{t('login.tabPassword')}</TabsTrigger>
|
||||
<TabsTrigger value="oauth">{t('login.tabOAuth')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="password">
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="username">用户名</label>
|
||||
<label className="text-sm font-medium" htmlFor="username">{t('login.username')}</label>
|
||||
<Input
|
||||
id="username"
|
||||
autoComplete="username"
|
||||
value={username}
|
||||
onChange={(event) => setUsername(event.target.value)}
|
||||
placeholder="输入用户名"
|
||||
placeholder={t('login.usernamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="password">密码</label>
|
||||
<label className="text-sm font-medium" htmlFor="password">{t('login.password')}</label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder="输入密码"
|
||||
placeholder={t('login.passwordPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
{loginMutation.error ? (
|
||||
<p className="text-sm text-red-600">{loginMutation.error.message}</p>
|
||||
) : null}
|
||||
<Button className="w-full" disabled={loginMutation.isPending} type="submit">
|
||||
{loginMutation.isPending ? '登录中...' : '登录'}
|
||||
{loginMutation.isPending ? t('login.submitting') : t('login.submit')}
|
||||
</Button>
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
还没有账号?
|
||||
{t('login.noAccount')}
|
||||
{' '}
|
||||
<Link
|
||||
to="/register"
|
||||
search={{ returnTo }}
|
||||
className="font-medium text-primary hover:underline"
|
||||
>
|
||||
立即注册
|
||||
{t('login.register')}
|
||||
</Link>
|
||||
</p>
|
||||
</form>
|
||||
|
|
@ -90,7 +92,7 @@ export function LoginPage() {
|
|||
|
||||
<TabsContent value="oauth" className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
使用 GitHub 登录时,认证完成后会自动返回当前站点。
|
||||
{t('login.oauthHint')}
|
||||
</p>
|
||||
<LoginButton returnTo={returnTo} />
|
||||
</TabsContent>
|
||||
|
|
@ -98,10 +100,10 @@ export function LoginPage() {
|
|||
</div>
|
||||
|
||||
<p className="text-center text-xs text-muted-foreground">
|
||||
登录即表示你同意我们的
|
||||
<a href="#" className="text-primary hover:underline ml-1">服务条款</a>
|
||||
和
|
||||
<a href="#" className="text-primary hover:underline ml-1">隐私政策</a>
|
||||
{t('login.agreementPrefix')}
|
||||
<a href="#" className="text-primary hover:underline ml-1">{t('login.terms')}</a>
|
||||
{t('login.and')}
|
||||
<a href="#" className="text-primary hover:underline ml-1">{t('login.privacy')}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useNavigate, useParams } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { NamespaceHeader } from '@/features/namespace/namespace-header'
|
||||
import { SkillCard } from '@/features/skill/skill-card'
|
||||
import { SkeletonList } from '@/shared/components/skeleton-loader'
|
||||
|
|
@ -6,6 +7,7 @@ import { EmptyState } from '@/shared/components/empty-state'
|
|||
import { useNamespaceDetail, useSearchSkills } from '@/shared/hooks/use-skill-queries'
|
||||
|
||||
export function NamespacePage() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { namespace } = useParams({ from: '/space/$namespace' })
|
||||
|
||||
|
|
@ -29,7 +31,7 @@ export function NamespacePage() {
|
|||
}
|
||||
|
||||
if (!namespaceData) {
|
||||
return <EmptyState title="命名空间不存在" />
|
||||
return <EmptyState title={t('namespace.notFound')} />
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -37,7 +39,7 @@ export function NamespacePage() {
|
|||
<NamespaceHeader namespace={namespaceData} />
|
||||
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-bold font-heading">技能列表</h2>
|
||||
<h2 className="text-2xl font-bold font-heading">{t('namespace.skillList')}</h2>
|
||||
{isLoadingSkills ? (
|
||||
<SkeletonList count={6} />
|
||||
) : skillsData && skillsData.items.length > 0 ? (
|
||||
|
|
@ -53,8 +55,8 @@ export function NamespacePage() {
|
|||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
title="暂无技能"
|
||||
description="该命名空间下还没有发布任何技能"
|
||||
title={t('namespace.emptyTitle')}
|
||||
description={t('namespace.emptyDescription')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Link, useNavigate, useSearch } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoginButton } from '@/features/auth/login-button'
|
||||
import { useLocalRegister } from '@/features/auth/use-local-auth'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
|
|
@ -8,6 +9,7 @@ import { Input } from '@/shared/ui/input'
|
|||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui/tabs'
|
||||
|
||||
export function RegisterPage() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const search = useSearch({ from: '/register' })
|
||||
const registerMutation = useLocalRegister()
|
||||
|
|
@ -31,65 +33,65 @@ export function RegisterPage() {
|
|||
<div className="mx-auto flex min-h-[70vh] max-w-2xl items-center justify-center">
|
||||
<Card className="w-full border-slate-200 bg-white/95 shadow-xl">
|
||||
<CardHeader className="space-y-3 text-center">
|
||||
<CardTitle>创建账号</CardTitle>
|
||||
<CardDescription>支持本地注册,也可以直接使用 OAuth 登录进入平台。</CardDescription>
|
||||
<CardTitle>{t('register.title')}</CardTitle>
|
||||
<CardDescription>{t('register.subtitle')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="local" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="local">本地账号</TabsTrigger>
|
||||
<TabsTrigger value="oauth">OAuth</TabsTrigger>
|
||||
<TabsTrigger value="local">{t('register.tabLocal')}</TabsTrigger>
|
||||
<TabsTrigger value="oauth">{t('register.tabOAuth')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="local">
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="register-username">用户名</label>
|
||||
<label className="text-sm font-medium" htmlFor="register-username">{t('register.username')}</label>
|
||||
<Input
|
||||
id="register-username"
|
||||
autoComplete="username"
|
||||
value={username}
|
||||
onChange={(event) => setUsername(event.target.value)}
|
||||
placeholder="3-64 位字母、数字或下划线"
|
||||
placeholder={t('register.usernamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="register-email">邮箱</label>
|
||||
<label className="text-sm font-medium" htmlFor="register-email">{t('register.email')}</label>
|
||||
<Input
|
||||
id="register-email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
placeholder="可选,用于后续账号识别"
|
||||
placeholder={t('register.emailPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="register-password">密码</label>
|
||||
<label className="text-sm font-medium" htmlFor="register-password">{t('register.password')}</label>
|
||||
<Input
|
||||
id="register-password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder="至少 8 位,包含 3 种字符类型"
|
||||
placeholder={t('register.passwordPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
{registerMutation.error ? (
|
||||
<p className="text-sm text-red-600">{registerMutation.error.message}</p>
|
||||
) : null}
|
||||
<Button className="w-full" disabled={registerMutation.isPending} type="submit">
|
||||
{registerMutation.isPending ? '注册中...' : '注册并登录'}
|
||||
{registerMutation.isPending ? t('register.submitting') : t('register.submit')}
|
||||
</Button>
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
已有账号?
|
||||
{t('register.hasAccount')}
|
||||
{' '}
|
||||
<Link
|
||||
to="/login"
|
||||
search={{ returnTo }}
|
||||
className="font-medium text-primary hover:underline"
|
||||
>
|
||||
返回登录
|
||||
{t('register.login')}
|
||||
</Link>
|
||||
</p>
|
||||
</form>
|
||||
|
|
@ -97,7 +99,7 @@ export function RegisterPage() {
|
|||
|
||||
<TabsContent value="oauth" className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
直接使用现有 OAuth 账户进入平台,无需再创建本地密码。
|
||||
{t('register.oauthHint')}
|
||||
</p>
|
||||
<LoginButton returnTo={returnTo} />
|
||||
</TabsContent>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useNavigate, useSearch } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SearchBar } from '@/features/search/search-bar'
|
||||
import { SkillCard } from '@/features/skill/skill-card'
|
||||
import { SkeletonList } from '@/shared/components/skeleton-loader'
|
||||
|
|
@ -8,6 +9,7 @@ import { useSearchSkills } from '@/shared/hooks/use-skill-queries'
|
|||
import { Button } from '@/shared/ui/button'
|
||||
|
||||
export function SearchPage() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const searchParams = useSearch({ from: '/search' })
|
||||
|
||||
|
|
@ -50,35 +52,35 @@ export function SearchPage() {
|
|||
{/* Sort Selector */}
|
||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-muted-foreground">排序:</span>
|
||||
<span className="text-sm font-medium text-muted-foreground">{t('search.sort.label')}</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={sort === 'relevance' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => handleSortChange('relevance')}
|
||||
>
|
||||
相关性
|
||||
{t('search.sort.relevance')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={sort === 'downloads' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => handleSortChange('downloads')}
|
||||
>
|
||||
下载量
|
||||
{t('search.sort.downloads')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={sort === 'newest' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => handleSortChange('newest')}
|
||||
>
|
||||
最新
|
||||
{t('search.sort.newest')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data && data.total > 0 && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
找到 <span className="text-primary font-semibold">{data.total}</span> 个结果
|
||||
{t('search.results', { count: data.total })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -108,8 +110,8 @@ export function SearchPage() {
|
|||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
title="未找到结果"
|
||||
description={q ? `没有找到与 "${q}" 相关的技能` : '请输入搜索关键词'}
|
||||
title={t('search.noResults')}
|
||||
description={q ? t('search.noResultsFor', { q }) : t('search.enterKeyword')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useConfirmAccountMerge, useInitiateAccountMerge, useVerifyAccountMerge } from '@/features/auth/use-account-merge'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
|
||||
export function AccountSettingsPage() {
|
||||
const { t } = useTranslation()
|
||||
const [secondaryIdentifier, setSecondaryIdentifier] = useState('')
|
||||
const [mergeRequestId, setMergeRequestId] = useState('')
|
||||
const [verificationToken, setVerificationToken] = useState('')
|
||||
|
|
@ -21,9 +23,9 @@ export function AccountSettingsPage() {
|
|||
const result = await initiateMutation.mutateAsync({ secondaryIdentifier })
|
||||
setMergeRequestId(String(result.mergeRequestId))
|
||||
setVerificationToken(result.verificationToken)
|
||||
setStatusMessage(`已创建合并请求,secondary=${result.secondaryUserId}`)
|
||||
setStatusMessage(t('accounts.initiateSuccess', { secondaryUserId: result.secondaryUserId }))
|
||||
} catch (error) {
|
||||
setStatusMessage(error instanceof Error ? error.message : '发起合并失败')
|
||||
setStatusMessage(error instanceof Error ? error.message : t('accounts.initiateError'))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -35,9 +37,9 @@ export function AccountSettingsPage() {
|
|||
mergeRequestId: Number(mergeRequestId),
|
||||
verificationToken,
|
||||
})
|
||||
setStatusMessage('验证成功,确认后将执行正式合并')
|
||||
setStatusMessage(t('accounts.verifySuccess'))
|
||||
} catch (error) {
|
||||
setStatusMessage(error instanceof Error ? error.message : '验证合并失败')
|
||||
setStatusMessage(error instanceof Error ? error.message : t('accounts.verifyError'))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -45,9 +47,9 @@ export function AccountSettingsPage() {
|
|||
setStatusMessage('')
|
||||
try {
|
||||
await confirmMutation.mutateAsync({ mergeRequestId: Number(mergeRequestId) })
|
||||
setStatusMessage('账号合并已完成')
|
||||
setStatusMessage(t('accounts.confirmSuccess'))
|
||||
} catch (error) {
|
||||
setStatusMessage(error instanceof Error ? error.message : '确认合并失败')
|
||||
setStatusMessage(error instanceof Error ? error.message : t('accounts.confirmError'))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,22 +57,22 @@ export function AccountSettingsPage() {
|
|||
<div className="mx-auto max-w-3xl space-y-6">
|
||||
<Card className="glass-strong">
|
||||
<CardHeader>
|
||||
<CardTitle>发起账号合并</CardTitle>
|
||||
<CardDescription>输入 secondary 账号标识。支持本地用户名,或 `provider:subject` 格式的外部身份。</CardDescription>
|
||||
<CardTitle>{t('accounts.initiateTitle')}</CardTitle>
|
||||
<CardDescription>{t('accounts.initiateDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4" onSubmit={handleInitiate}>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="secondary-identifier">Secondary 标识</label>
|
||||
<label className="text-sm font-medium" htmlFor="secondary-identifier">{t('accounts.secondaryLabel')}</label>
|
||||
<Input
|
||||
id="secondary-identifier"
|
||||
value={secondaryIdentifier}
|
||||
onChange={(event) => setSecondaryIdentifier(event.target.value)}
|
||||
placeholder="例如:other_user 或 github:123456"
|
||||
placeholder={t('accounts.secondaryPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={initiateMutation.isPending}>
|
||||
{initiateMutation.isPending ? '发起中...' : '发起合并'}
|
||||
{initiateMutation.isPending ? t('accounts.initiating') : t('accounts.initiate')}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
|
@ -78,13 +80,13 @@ export function AccountSettingsPage() {
|
|||
|
||||
<Card className="glass-strong">
|
||||
<CardHeader>
|
||||
<CardTitle>验证并完成合并</CardTitle>
|
||||
<CardDescription>先完成 token 验证,再单独确认执行数据迁移。</CardDescription>
|
||||
<CardTitle>{t('accounts.verifyTitle')}</CardTitle>
|
||||
<CardDescription>{t('accounts.verifyDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4" onSubmit={handleVerify}>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="merge-request-id">Merge Request ID</label>
|
||||
<label className="text-sm font-medium" htmlFor="merge-request-id">{t('accounts.mergeRequestId')}</label>
|
||||
<Input
|
||||
id="merge-request-id"
|
||||
value={mergeRequestId}
|
||||
|
|
@ -92,7 +94,7 @@ export function AccountSettingsPage() {
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="merge-token">Verification Token</label>
|
||||
<label className="text-sm font-medium" htmlFor="merge-token">{t('accounts.verificationToken')}</label>
|
||||
<Input
|
||||
id="merge-token"
|
||||
value={verificationToken}
|
||||
|
|
@ -100,12 +102,12 @@ export function AccountSettingsPage() {
|
|||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={verifyMutation.isPending}>
|
||||
{verifyMutation.isPending ? '验证中...' : '完成合并'}
|
||||
{verifyMutation.isPending ? t('accounts.verifying') : t('accounts.verify')}
|
||||
</Button>
|
||||
</form>
|
||||
<div className="mt-4">
|
||||
<Button type="button" onClick={handleConfirm} disabled={confirmMutation.isPending || !mergeRequestId}>
|
||||
{confirmMutation.isPending ? '确认中...' : '确认并完成合并'}
|
||||
{confirmMutation.isPending ? t('accounts.confirming') : t('accounts.confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
{statusMessage ? <p className="mt-4 text-sm text-muted-foreground">{statusMessage}</p> : null}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { authApi } from '@/api/client'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
|
||||
export function SecuritySettingsPage() {
|
||||
const { t } = useTranslation()
|
||||
const [currentPassword, setCurrentPassword] = useState('')
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
const [statusMessage, setStatusMessage] = useState('')
|
||||
|
|
@ -18,11 +20,11 @@ export function SecuritySettingsPage() {
|
|||
setIsSubmitting(true)
|
||||
try {
|
||||
await authApi.changePassword({ currentPassword, newPassword })
|
||||
setStatusMessage('密码修改成功')
|
||||
setStatusMessage(t('security.success'))
|
||||
setCurrentPassword('')
|
||||
setNewPassword('')
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : '修改密码失败')
|
||||
setErrorMessage(error instanceof Error ? error.message : t('security.defaultError'))
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
|
@ -32,13 +34,13 @@ export function SecuritySettingsPage() {
|
|||
<div className="mx-auto max-w-2xl">
|
||||
<Card className="glass-strong">
|
||||
<CardHeader>
|
||||
<CardTitle>安全设置</CardTitle>
|
||||
<CardDescription>已启用本地账号密码登录时,可以在这里更新密码。</CardDescription>
|
||||
<CardTitle>{t('security.title')}</CardTitle>
|
||||
<CardDescription>{t('security.subtitle')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="current-password">当前密码</label>
|
||||
<label className="text-sm font-medium" htmlFor="current-password">{t('security.currentPassword')}</label>
|
||||
<Input
|
||||
id="current-password"
|
||||
type="password"
|
||||
|
|
@ -48,7 +50,7 @@ export function SecuritySettingsPage() {
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="new-password">新密码</label>
|
||||
<label className="text-sm font-medium" htmlFor="new-password">{t('security.newPassword')}</label>
|
||||
<Input
|
||||
id="new-password"
|
||||
type="password"
|
||||
|
|
@ -60,7 +62,7 @@ export function SecuritySettingsPage() {
|
|||
{statusMessage ? <p className="text-sm text-emerald-600">{statusMessage}</p> : null}
|
||||
{errorMessage ? <p className="text-sm text-red-600">{errorMessage}</p> : null}
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? '提交中...' : '更新密码'}
|
||||
{isSubmitting ? t('security.submitting') : t('security.submit')}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams, useNavigate, useRouterState } from '@tanstack/react-router'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { MarkdownRenderer } from '@/features/skill/markdown-renderer'
|
||||
|
|
@ -19,6 +20,7 @@ import {
|
|||
} from '@/shared/hooks/use-skill-queries'
|
||||
|
||||
export function SkillDetailPage() {
|
||||
const { t, i18n } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const location = useRouterState({ select: (s) => s.location })
|
||||
const queryClient = useQueryClient()
|
||||
|
|
@ -84,8 +86,8 @@ export function SkillDetailPage() {
|
|||
if (!skill) {
|
||||
return (
|
||||
<div className="text-center py-20 animate-fade-up">
|
||||
<h2 className="text-2xl font-bold font-heading mb-2">技能不存在</h2>
|
||||
<p className="text-muted-foreground">该技能可能已被删除或从未存在</p>
|
||||
<h2 className="text-2xl font-bold font-heading mb-2">{t('skillDetail.notFound')}</h2>
|
||||
<p className="text-muted-foreground">{t('skillDetail.notFoundDesc')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -106,9 +108,9 @@ export function SkillDetailPage() {
|
|||
|
||||
<Tabs defaultValue="readme">
|
||||
<TabsList>
|
||||
<TabsTrigger value="readme">README</TabsTrigger>
|
||||
<TabsTrigger value="files">文件</TabsTrigger>
|
||||
<TabsTrigger value="versions">版本</TabsTrigger>
|
||||
<TabsTrigger value="readme">{t('skillDetail.tabReadme')}</TabsTrigger>
|
||||
<TabsTrigger value="files">{t('skillDetail.tabFiles')}</TabsTrigger>
|
||||
<TabsTrigger value="versions">{t('skillDetail.tabVersions')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="readme" className="mt-6">
|
||||
|
|
@ -118,7 +120,7 @@ export function SkillDetailPage() {
|
|||
</Card>
|
||||
) : (
|
||||
<Card className="p-8 text-muted-foreground text-center">
|
||||
暂无 README
|
||||
{t('skillDetail.noReadme')}
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
|
@ -128,7 +130,7 @@ export function SkillDetailPage() {
|
|||
<FileTree files={files} />
|
||||
) : (
|
||||
<Card className="p-8 text-muted-foreground text-center">
|
||||
暂无文件
|
||||
{t('skillDetail.noFiles')}
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
|
@ -146,14 +148,14 @@ export function SkillDetailPage() {
|
|||
</span>
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{new Date(version.publishedAt).toLocaleDateString('zh-CN')}
|
||||
{new Date(version.publishedAt).toLocaleDateString(i18n.language)}
|
||||
</span>
|
||||
</div>
|
||||
{version.changelog && (
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{version.changelog}</p>
|
||||
)}
|
||||
<div className="text-xs text-muted-foreground mt-2 flex items-center gap-3">
|
||||
<span>{version.fileCount} 个文件</span>
|
||||
<span>{t('skillDetail.fileCount', { count: version.fileCount })}</span>
|
||||
<span className="w-1 h-1 rounded-full bg-muted-foreground/40" />
|
||||
<span>{(version.totalSize / 1024).toFixed(1)} KB</span>
|
||||
</div>
|
||||
|
|
@ -161,7 +163,7 @@ export function SkillDetailPage() {
|
|||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground text-center py-8">暂无版本</div>
|
||||
<div className="text-muted-foreground text-center py-8">{t('skillDetail.noVersions')}</div>
|
||||
)}
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
|
@ -172,7 +174,7 @@ export function SkillDetailPage() {
|
|||
<div className="space-y-5">
|
||||
<Card className="p-5 space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">版本</div>
|
||||
<div className="text-sm text-muted-foreground">{t('skillDetail.version')}</div>
|
||||
<div className="font-semibold font-mono text-foreground">
|
||||
{skill.latestVersion ? `v${skill.latestVersion}` : '—'}
|
||||
</div>
|
||||
|
|
@ -181,23 +183,23 @@ export function SkillDetailPage() {
|
|||
<div className="h-px bg-border/40" />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">下载量</div>
|
||||
<div className="text-sm text-muted-foreground">{t('skillDetail.downloads')}</div>
|
||||
<div className="font-semibold text-foreground">{skill.downloadCount.toLocaleString()}</div>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-border/40" />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">评分</div>
|
||||
<div className="text-sm text-muted-foreground">{t('skillDetail.rating')}</div>
|
||||
<div className="font-semibold text-foreground">
|
||||
{skill.ratingCount > 0 && skill.ratingAvg !== undefined ? `${skill.ratingAvg.toFixed(1)} / 5` : '暂无'}
|
||||
{skill.ratingCount > 0 && skill.ratingAvg !== undefined ? `${skill.ratingAvg.toFixed(1)} / 5` : t('skillDetail.ratingNone')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-border/40" />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">命名空间</div>
|
||||
<div className="text-sm text-muted-foreground">{t('skillDetail.namespaceLabel')}</div>
|
||||
<NamespaceBadge type="GLOBAL" name={namespace} />
|
||||
</div>
|
||||
|
||||
|
|
@ -207,14 +209,14 @@ export function SkillDetailPage() {
|
|||
<StarButton skillId={skill.id} starCount={skill.starCount} onRequireLogin={requireLogin} />
|
||||
<RatingInput skillId={skill.id} onRequireLogin={requireLogin} />
|
||||
{!user && (
|
||||
<p className="text-xs text-muted-foreground">登录后可以收藏和评分</p>
|
||||
<p className="text-xs text-muted-foreground">{t('skillDetail.loginToRate')}</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{skill.latestVersion && (
|
||||
<Card className="p-5 space-y-4">
|
||||
<div className="text-sm font-semibold font-heading text-foreground">安装</div>
|
||||
<div className="text-sm font-semibold font-heading text-foreground">{t('skillDetail.install')}</div>
|
||||
<InstallCommand
|
||||
namespace={namespace}
|
||||
slug={slug}
|
||||
|
|
@ -233,25 +235,25 @@ export function SkillDetailPage() {
|
|||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
|
||||
</svg>
|
||||
下载
|
||||
{t('skillDetail.download')}
|
||||
</Button>
|
||||
|
||||
{governanceVisible && (
|
||||
<Card className="p-5 space-y-3">
|
||||
<div className="text-sm font-semibold font-heading text-foreground">治理操作</div>
|
||||
<div className="text-sm font-semibold font-heading text-foreground">{t('skillDetail.governance')}</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{!skill.hidden ? (
|
||||
<Button variant="outline" onClick={() => hideMutation.mutate()} disabled={hideMutation.isPending}>
|
||||
{hideMutation.isPending ? '处理中...' : '隐藏技能'}
|
||||
{hideMutation.isPending ? t('skillDetail.processing') : t('skillDetail.hideSkill')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" onClick={() => unhideMutation.mutate()} disabled={unhideMutation.isPending}>
|
||||
{unhideMutation.isPending ? '处理中...' : '恢复技能'}
|
||||
{unhideMutation.isPending ? t('skillDetail.processing') : t('skillDetail.unhideSkill')}
|
||||
</Button>
|
||||
)}
|
||||
{latestVersion && (
|
||||
<Button variant="destructive" onClick={() => yankMutation.mutate()} disabled={yankMutation.isPending}>
|
||||
{yankMutation.isPending ? '处理中...' : '撤回当前版本'}
|
||||
{yankMutation.isPending ? t('skillDetail.processing') : t('skillDetail.yankVersion')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
|
||||
interface CopyButtonProps {
|
||||
|
|
@ -7,6 +8,7 @@ interface CopyButtonProps {
|
|||
}
|
||||
|
||||
export function CopyButton({ text, className }: CopyButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = async () => {
|
||||
|
|
@ -26,7 +28,7 @@ export function CopyButton({ text, className }: CopyButtonProps) {
|
|||
onClick={handleCopy}
|
||||
className={className}
|
||||
>
|
||||
{copied ? '已复制' : '复制'}
|
||||
{copied ? t('copyButton.copied') : t('copyButton.copy')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
|
||||
interface PaginationProps {
|
||||
|
|
@ -7,6 +8,7 @@ interface PaginationProps {
|
|||
}
|
||||
|
||||
export function Pagination({ page, totalPages, onPageChange }: PaginationProps) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-3 py-4">
|
||||
<Button
|
||||
|
|
@ -16,14 +18,14 @@ export function Pagination({ page, totalPages, onPageChange }: PaginationProps)
|
|||
disabled={page <= 0}
|
||||
className="min-w-[90px]"
|
||||
>
|
||||
上一页
|
||||
{t('pagination.prev')}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2 px-4 py-1.5 rounded-lg bg-secondary/40 text-sm font-medium text-foreground">
|
||||
<span className="text-muted-foreground">第</span>
|
||||
<span className="text-muted-foreground">{t('pagination.pagePrefix')}</span>
|
||||
<span className="text-primary">{page + 1}</span>
|
||||
<span className="text-muted-foreground">/</span>
|
||||
<span>{totalPages}</span>
|
||||
<span className="text-muted-foreground">页</span>
|
||||
{t('pagination.pageSuffix') && <span className="text-muted-foreground">{t('pagination.pageSuffix')}</span>}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -32,7 +34,7 @@ export function Pagination({ page, totalPages, onPageChange }: PaginationProps)
|
|||
disabled={page >= totalPages - 1}
|
||||
className="min-w-[90px]"
|
||||
>
|
||||
下一页
|
||||
{t('pagination.next')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { Link, useNavigate } from '@tanstack/react-router'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { authApi } from '@/api/client'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -21,6 +23,7 @@ interface UserMenuProps {
|
|||
export function UserMenu({ user }: UserMenuProps) {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const hasRole = (role: string) => user.platformRoles?.includes(role) ?? false
|
||||
const isReviewer = hasRole('SKILL_ADMIN') || hasRole('NAMESPACE_ADMIN') || hasRole('SUPER_ADMIN')
|
||||
|
|
@ -30,9 +33,9 @@ export function UserMenu({ user }: UserMenuProps) {
|
|||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await fetch('/api/auth/logout', { method: 'POST' })
|
||||
await authApi.logout()
|
||||
queryClient.setQueryData(['auth', 'me'], null)
|
||||
navigate({ to: '/' })
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue