fix: 修复 ESLint 错误,添加 lint-first 工作流规范
Some checks failed
Build & Deploy Admin / deploy (push) Failing after 16s
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:
parent
e0871d0b7a
commit
6add8bf027
@ -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/`
|
||||
|
||||
@ -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} />,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@ -45,4 +45,5 @@ function Badge({
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export { Badge, badgeVariants }
|
||||
|
||||
@ -61,4 +61,5 @@ function Button({
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export { Button, buttonVariants }
|
||||
|
||||
@ -86,4 +86,5 @@ function TabsContent({
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
||||
|
||||
@ -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 调用来保存审核备注
|
||||
|
||||
Loading…
Reference in New Issue
Block a user