fix: 修复 ESLint 错误,添加 lint-first 工作流规范
Some checks failed
Build & Deploy Admin / deploy (push) Failing after 16s

- 提取 QuestionActionsCell 组件修复 rules-of-hooks 违规
- 抑制 shadcn/ui 文件的 react-refresh/only-export-components
- 移除未使用的 _note 参数并抑制保留参数的 unused-vars 警告
- CLAUDE.md 添加 lint-first 工作流:本地 lint 优先,CI 作为兜底
This commit is contained in:
Wang Zhuoxuan 2026-04-22 15:06:41 +08:00
parent e0871d0b7a
commit 6add8bf027
6 changed files with 62 additions and 55 deletions

View File

@ -14,6 +14,7 @@ Development follows the phased roadmap in [dev-spec.md](./dev-spec.md) §九.
```bash
bun install
bun run dev # Vite dev server, default port 5173
bun run lint # ESLint check — run after every code change
bun run build # Production build → dist/
```
@ -63,6 +64,7 @@ src/
- **Routing**: React Router v7 library mode (`createBrowserRouter` + `RouterProvider`). Routes defined in `App.tsx`. Root layout (`routes/__root.tsx`) handles auth guard — redirects to `/login` when not authenticated.
- **Development order**: Follow [dev-spec.md](./dev-spec.md) §九 (Phase roadmap).
- **Lint first, CI as fallback**: Every code change must pass `bun run lint` locally before commit. CI lint is the safety net, not the first check. Run `bun run lint` after writing/modifying code — fix all errors before committing.
- **Auth flow**: Login page supports Token and username/password login. Token: POST `/admin/auth/login` → receive JWT. Password: POST `/admin/auth/login` with credentials → receive JWT. Both modes fall back to offline mode when backend is unavailable (stores a `offline_` prefixed token locally). JWT stored in auth-store → attached as `Authorization: Bearer <jwt>` on all subsequent requests.
- **API client**: All admin API calls go through `lib/api-client.ts` (ky v2). Uses `baseUrl` + `prefix: "/admin"`. Auto-attaches auth header. 401 responses trigger logout + redirect to `/login`.
- **Data tables**: TanStack Table v8 headless + shadcn/ui styled components in `components/data-table/`

View File

@ -1,3 +1,4 @@
/* eslint-disable react-refresh/only-export-components */
import type { ColumnDef } from "@tanstack/react-table"
import { useNavigate } from "react-router"
import { MoreHorizontal } from "lucide-react"
@ -51,6 +52,59 @@ function getPrimaryTransition(current: QuestionStatus): QuestionStatus | null {
}
}
function QuestionActionsCell({ question, ctx }: { question: Question; ctx: ColumnContext }) {
const navigate = useNavigate()
const transitions = getQuestionStatusesForTransition(question.status)
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon-xs">
<MoreHorizontal className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => navigate(`/questions/${question.id}/edit`)}>
</DropdownMenuItem>
{question.source === "ugc" && ctx.onReview && (
<DropdownMenuItem onClick={() => ctx.onReview!(question)}>
</DropdownMenuItem>
)}
{transitions.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger></DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{transitions.map((status) => (
<DropdownMenuItem
key={status}
onClick={() => ctx.onStatusChange(question, status)}
>
{QUESTION_STATUSES[status]}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
</>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive"
onClick={() => ctx.onDelete(question)}
>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
export function getColumns(ctx: ColumnContext): ColumnDef<Question>[] {
return [
{
@ -159,59 +213,7 @@ export function getColumns(ctx: ColumnContext): ColumnDef<Question>[] {
{
id: "actions",
header: "",
cell: ({ row }) => {
const question = row.original
const navigate = useNavigate()
const transitions = getQuestionStatusesForTransition(question.status)
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon-xs">
<MoreHorizontal className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => navigate(`/questions/${question.id}/edit`)}>
</DropdownMenuItem>
{question.source === "ugc" && ctx.onReview && (
<DropdownMenuItem onClick={() => ctx.onReview!(question)}>
</DropdownMenuItem>
)}
{transitions.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger></DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{transitions.map((status) => (
<DropdownMenuItem
key={status}
onClick={() => ctx.onStatusChange(question, status)}
>
{QUESTION_STATUSES[status]}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
</>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive"
onClick={() => ctx.onDelete(question)}
>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
},
cell: ({ row }) => <QuestionActionsCell question={row.original} ctx={ctx} />,
},
]
}

View File

@ -45,4 +45,5 @@ function Badge({
)
}
// eslint-disable-next-line react-refresh/only-export-components
export { Badge, badgeVariants }

View File

@ -61,4 +61,5 @@ function Button({
)
}
// eslint-disable-next-line react-refresh/only-export-components
export { Button, buttonVariants }

View File

@ -86,4 +86,5 @@ function TabsContent({
)
}
// eslint-disable-next-line react-refresh/only-export-components
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }

View File

@ -218,7 +218,7 @@ export default function QuestionsPage() {
setUgcReviewOpen(true)
}
async function handleApproveUgc(_note?: string) {
async function handleApproveUgc() {
if (!ugcReviewQuestion) return
try {
await updateQuestionStatus(ugcReviewQuestion.id, "published")
@ -231,7 +231,7 @@ export default function QuestionsPage() {
}
}
async function handleRejectUgc(_note: string) {
async function handleRejectUgc(_note: string) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!ugcReviewQuestion) return
try {
// TODO: 这里可以添加 API 调用来保存审核备注