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
|
```bash
|
||||||
bun install
|
bun install
|
||||||
bun run dev # Vite dev server, default port 5173
|
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/
|
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.
|
- **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).
|
- **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.
|
- **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`.
|
- **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/`
|
- **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 type { ColumnDef } from "@tanstack/react-table"
|
||||||
import { useNavigate } from "react-router"
|
import { useNavigate } from "react-router"
|
||||||
import { MoreHorizontal } from "lucide-react"
|
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>[] {
|
export function getColumns(ctx: ColumnContext): ColumnDef<Question>[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -159,59 +213,7 @@ export function getColumns(ctx: ColumnContext): ColumnDef<Question>[] {
|
|||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
header: "",
|
header: "",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => <QuestionActionsCell question={row.original} ctx={ctx} />,
|
||||||
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>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,4 +45,5 @@ function Badge({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export { Badge, badgeVariants }
|
export { Badge, badgeVariants }
|
||||||
|
|||||||
@ -61,4 +61,5 @@ function Button({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants }
|
||||||
|
|||||||
@ -86,4 +86,5 @@ function TabsContent({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
||||||
|
|||||||
@ -218,7 +218,7 @@ export default function QuestionsPage() {
|
|||||||
setUgcReviewOpen(true)
|
setUgcReviewOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleApproveUgc(_note?: string) {
|
async function handleApproveUgc() {
|
||||||
if (!ugcReviewQuestion) return
|
if (!ugcReviewQuestion) return
|
||||||
try {
|
try {
|
||||||
await updateQuestionStatus(ugcReviewQuestion.id, "published")
|
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
|
if (!ugcReviewQuestion) return
|
||||||
try {
|
try {
|
||||||
// TODO: 这里可以添加 API 调用来保存审核备注
|
// TODO: 这里可以添加 API 调用来保存审核备注
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user