diff --git a/.claude/rules/typescript/coding-style.md b/.claude/rules/typescript/coding-style.md new file mode 100644 index 0000000..090c0a1 --- /dev/null +++ b/.claude/rules/typescript/coding-style.md @@ -0,0 +1,199 @@ +--- +paths: + - "**/*.ts" + - "**/*.tsx" + - "**/*.js" + - "**/*.jsx" +--- +# TypeScript/JavaScript Coding Style + +> This file extends [common/coding-style.md](../common/coding-style.md) with TypeScript/JavaScript specific content. + +## Types and Interfaces + +Use types to make public APIs, shared models, and component props explicit, readable, and reusable. + +### Public APIs + +- Add parameter and return types to exported functions, shared utilities, and public class methods +- Let TypeScript infer obvious local variable types +- Extract repeated inline object shapes into named types or interfaces + +```typescript +// WRONG: Exported function without explicit types +export function formatUser(user) { + return `${user.firstName} ${user.lastName}` +} + +// CORRECT: Explicit types on public APIs +interface User { + firstName: string + lastName: string +} + +export function formatUser(user: User): string { + return `${user.firstName} ${user.lastName}` +} +``` + +### Interfaces vs. Type Aliases + +- Use `interface` for object shapes that may be extended or implemented +- Use `type` for unions, intersections, tuples, mapped types, and utility types +- Prefer string literal unions over `enum` unless an `enum` is required for interoperability + +```typescript +interface User { + id: string + email: string +} + +type UserRole = 'admin' | 'member' +type UserWithRole = User & { + role: UserRole +} +``` + +### Avoid `any` + +- Avoid `any` in application code +- Use `unknown` for external or untrusted input, then narrow it safely +- Use generics when a value's type depends on the caller + +```typescript +// WRONG: any removes type safety +function getErrorMessage(error: any) { + return error.message +} + +// CORRECT: unknown forces safe narrowing +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message + } + + return 'Unexpected error' +} +``` + +### React Props + +- Define component props with a named `interface` or `type` +- Type callback props explicitly +- Do not use `React.FC` unless there is a specific reason to do so + +```typescript +interface User { + id: string + email: string +} + +interface UserCardProps { + user: User + onSelect: (id: string) => void +} + +function UserCard({ user, onSelect }: UserCardProps) { + return +} +``` + +### JavaScript Files + +- In `.js` and `.jsx` files, use JSDoc when types improve clarity and a TypeScript migration is not practical +- Keep JSDoc aligned with runtime behavior + +```javascript +/** + * @param {{ firstName: string, lastName: string }} user + * @returns {string} + */ +export function formatUser(user) { + return `${user.firstName} ${user.lastName}` +} +``` + +## Immutability + +Use spread operator for immutable updates: + +```typescript +interface User { + id: string + name: string +} + +// WRONG: Mutation +function updateUser(user: User, name: string): User { + user.name = name // MUTATION! + return user +} + +// CORRECT: Immutability +function updateUser(user: Readonly, name: string): User { + return { + ...user, + name + } +} +``` + +## Error Handling + +Use async/await with try-catch and narrow unknown errors safely: + +```typescript +interface User { + id: string + email: string +} + +declare function riskyOperation(userId: string): Promise + +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message + } + + return 'Unexpected error' +} + +const logger = { + error: (message: string, error: unknown) => { + // Replace with your production logger (for example, pino or winston). + } +} + +async function loadUser(userId: string): Promise { + try { + const result = await riskyOperation(userId) + return result + } catch (error: unknown) { + logger.error('Operation failed', error) + throw new Error(getErrorMessage(error)) + } +} +``` + +## Input Validation + +Use Zod for schema-based validation and infer types from the schema: + +```typescript +import { z } from 'zod' + +const userSchema = z.object({ + email: z.string().email(), + age: z.number().int().min(0).max(150) +}) + +type UserInput = z.infer + +const validated: UserInput = userSchema.parse(input) +``` + +## Console.log + +- No `console.log` statements in production code +- Use proper logging libraries instead +- See hooks for automatic detection diff --git a/.claude/rules/typescript/hooks.md b/.claude/rules/typescript/hooks.md new file mode 100644 index 0000000..cd4754b --- /dev/null +++ b/.claude/rules/typescript/hooks.md @@ -0,0 +1,22 @@ +--- +paths: + - "**/*.ts" + - "**/*.tsx" + - "**/*.js" + - "**/*.jsx" +--- +# TypeScript/JavaScript Hooks + +> This file extends [common/hooks.md](../common/hooks.md) with TypeScript/JavaScript specific content. + +## PostToolUse Hooks + +Configure in `~/.claude/settings.json`: + +- **Prettier**: Auto-format JS/TS files after edit +- **TypeScript check**: Run `tsc` after editing `.ts`/`.tsx` files +- **console.log warning**: Warn about `console.log` in edited files + +## Stop Hooks + +- **console.log audit**: Check all modified files for `console.log` before session ends diff --git a/.claude/rules/typescript/patterns.md b/.claude/rules/typescript/patterns.md new file mode 100644 index 0000000..d50729d --- /dev/null +++ b/.claude/rules/typescript/patterns.md @@ -0,0 +1,52 @@ +--- +paths: + - "**/*.ts" + - "**/*.tsx" + - "**/*.js" + - "**/*.jsx" +--- +# TypeScript/JavaScript Patterns + +> This file extends [common/patterns.md](../common/patterns.md) with TypeScript/JavaScript specific content. + +## API Response Format + +```typescript +interface ApiResponse { + success: boolean + data?: T + error?: string + meta?: { + total: number + page: number + limit: number + } +} +``` + +## Custom Hooks Pattern + +```typescript +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const handler = setTimeout(() => setDebouncedValue(value), delay) + return () => clearTimeout(handler) + }, [value, delay]) + + return debouncedValue +} +``` + +## Repository Pattern + +```typescript +interface Repository { + findAll(filters?: Filters): Promise + findById(id: string): Promise + create(data: CreateDto): Promise + update(id: string, data: UpdateDto): Promise + delete(id: string): Promise +} +``` diff --git a/.claude/rules/typescript/security.md b/.claude/rules/typescript/security.md new file mode 100644 index 0000000..98ba400 --- /dev/null +++ b/.claude/rules/typescript/security.md @@ -0,0 +1,28 @@ +--- +paths: + - "**/*.ts" + - "**/*.tsx" + - "**/*.js" + - "**/*.jsx" +--- +# TypeScript/JavaScript Security + +> This file extends [common/security.md](../common/security.md) with TypeScript/JavaScript specific content. + +## Secret Management + +```typescript +// NEVER: Hardcoded secrets +const apiKey = "sk-proj-xxxxx" + +// ALWAYS: Environment variables +const apiKey = process.env.OPENAI_API_KEY + +if (!apiKey) { + throw new Error('OPENAI_API_KEY not configured') +} +``` + +## Agent Support + +- Use **security-reviewer** skill for comprehensive security audits diff --git a/.claude/rules/typescript/testing.md b/.claude/rules/typescript/testing.md new file mode 100644 index 0000000..6f2f402 --- /dev/null +++ b/.claude/rules/typescript/testing.md @@ -0,0 +1,18 @@ +--- +paths: + - "**/*.ts" + - "**/*.tsx" + - "**/*.js" + - "**/*.jsx" +--- +# TypeScript/JavaScript Testing + +> This file extends [common/testing.md](../common/testing.md) with TypeScript/JavaScript specific content. + +## E2E Testing + +Use **Playwright** as the E2E testing framework for critical user flows. + +## Agent Support + +- **e2e-runner** - Playwright E2E testing specialist diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..0cbf4db --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "typescript-lsp@claude-plugins-official": true + } +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..4f9c81f --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,20 @@ +{ + "permissions": { + "allow": [ + "Bash(bun --version)", + "mcp__plugin_everything-claude-code_sequential-thinking__sequentialthinking", + "mcp__plugin_everything-claude-code_context7__resolve-library-id", + "mcp__plugin_everything-claude-code_context7__query-docs", + "Bash(cp /tmp/duoqi-admin-init/.gitignore /tmp/duoqi-admin-init/eslint.config.js /tmp/duoqi-admin-init/index.html /tmp/duoqi-admin-init/package.json /tmp/duoqi-admin-init/tsconfig.app.json /tmp/duoqi-admin-init/tsconfig.json /tmp/duoqi-admin-init/tsconfig.node.json /tmp/duoqi-admin-init/vite.config.ts .)", + "Bash(cp -r /tmp/duoqi-admin-init/public .)", + "Bash(cp -r /tmp/duoqi-admin-init/src .)", + "Bash(rm -rf /tmp/duoqi-admin-init)", + "Bash(bun install:*)", + "Bash(bun add:*)", + "Bash(bunx:*)", + "Bash(bun run:*)", + "Bash(git add:*)", + "Bash(git commit:*)" + ] + } +} diff --git a/src/App.tsx b/src/App.tsx index ecc911c..a54d269 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,8 @@ import SkillTreePage from "@/routes/skill-tree" import UsersPage from "@/routes/users" import UserDetailPage from "@/routes/users/$id" import FeedbackPage from "@/routes/feedback" +import ReportsPage from "@/routes/reports" +import AdminsPage from "@/routes/admins" import SettingsPage from "@/routes/settings" const router = createBrowserRouter([ @@ -33,6 +35,8 @@ const router = createBrowserRouter([ { path: "categories", Component: CategoriesPage }, { path: "skill-tree", Component: SkillTreePage }, { path: "feedback", Component: FeedbackPage }, + { path: "reports", Component: ReportsPage }, + { path: "admins", Component: AdminsPage }, { path: "users", children: [ diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 91f73fb..04b922e 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -8,6 +8,9 @@ import { MessageSquare, Settings, LogOut, + FileCheck, + AlertCircle, + Shield, } from "lucide-react" import { cn } from "@/lib/utils" import { useAuth } from "@/hooks/use-auth" @@ -15,10 +18,13 @@ import { useAuth } from "@/hooks/use-auth" const navItems = [ { to: "/", label: "数据看板", icon: LayoutDashboard, end: true }, { to: "/questions", label: "题库管理", icon: BookOpen }, + { to: "/questions?source=ugc", label: "UGC 审核", icon: FileCheck }, { to: "/categories", label: "分类管理", icon: FolderOpen }, { to: "/skill-tree", label: "技能树", icon: TreePine }, { to: "/users", label: "用户管理", icon: Users }, { to: "/feedback", label: "用户反馈", icon: MessageSquare }, + { to: "/reports", label: "举报处理", icon: AlertCircle }, + { to: "/admins", label: "管理员", icon: Shield }, { to: "/settings", label: "系统设置", icon: Settings }, ] diff --git a/src/components/question/UgcReviewDialog.tsx b/src/components/question/UgcReviewDialog.tsx new file mode 100644 index 0000000..2df9a07 --- /dev/null +++ b/src/components/question/UgcReviewDialog.tsx @@ -0,0 +1,233 @@ +import { useState } from "react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Textarea } from "@/components/ui/textarea" +import { Label } from "@/components/ui/label" +import { Separator } from "@/components/ui/separator" +import { DIFFICULTY_LABELS, QUESTION_SOURCE_LABELS, QUESTION_STATUSES } from "@/lib/constants" +import type { Question } from "@/types/question" +import type { Category } from "@/types/category" + +interface UgcReviewDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + question: Question | null + categories: Category[] + onApprove: (note?: string) => Promise + onReject: (note: string) => Promise +} + +export function UgcReviewDialog({ + open, + onOpenChange, + question, + categories, + onApprove, + onReject, +}: UgcReviewDialogProps) { + const [note, setNote] = useState("") + const [submitting, setSubmitting] = useState(false) + const [action, setAction] = useState<"approve" | "reject" | null>(null) + + if (!question) return null + + const category = categories.find((c) => c.id === question.categoryId) + + async function handleSubmit() { + if (!action) return + + setSubmitting(true) + try { + if (action === "approve") { + await onApprove(note || undefined) + } else { + if (!note.trim()) { + // 拒绝时必须填写备注 + setSubmitting(false) + return + } + await onReject(note) + } + setNote("") + setAction(null) + onOpenChange(false) + } finally { + setSubmitting(false) + } + } + + function handleActionClick(a: "approve" | "reject") { + setAction(a) + if (a === "approve") { + // 通过可以直接提交,备注可选 + handleSubmit() + } + } + + return ( + + + + UGC 题目审核 + + 请仔细检查用户投稿的题目内容,审核通过后题目将发布到题库。 + + + +
+ {/* 题目基本信息 */} +
+
+ +

{category?.name ?? question.categoryId}

+
+
+ +

{DIFFICULTY_LABELS[question.difficulty]}

+
+
+ +

{QUESTION_SOURCE_LABELS[question.source]}

+
+
+ + {QUESTION_STATUSES[question.status]} +
+
+ + + + {/* 题干 */} +
+ +

{question.stem}

+
+ + {/* 正确答案 */} +
+ +

{question.correctAnswer}

+
+ + {/* 干扰项 */} +
+ +
    + {question.distractors.map((d, i) => ( +
  • + · {d} +
  • + ))} +
+
+ + {/* 知识卡 */} +
+ +

{question.knowledgeCardBasic}

+
+ + {question.knowledgeCardDeep && ( +
+ +

+ {question.knowledgeCardDeep} +

+
+ )} + + + + {/* 统计信息 */} +
+
+ +

{question.stats.timesAnswered}

+
+
+ +

{(question.stats.correctRate * 100).toFixed(0)}%

+
+
+ +

{(question.stats.avgTimeMs / 1000).toFixed(1)}秒

+
+
+ + + + {/* 审核备注 */} +
+ +