1
0

feat: 前端 ESLint 规则增强,自动检测 LLM 编码违规

- 启用 TanStack Query flat/recommended(7 条规则)
- 新增 no-console(允许 warn/error)、consistent-type-imports(inline 风格)、no-non-null-assertion 规则
- 新增自定义规则 no-hardcoded-color-in-style,检测 JSX style 中硬编码颜色值
- 将 ESLint 检查集成到 build 命令(tsc -b && eslint . && vite build)
- 修复现有代码中的 lint 违规(import 顺序、type import 风格、unused vars)
- 使用 @typescript-eslint/rule-tester 编写自定义规则集成测试
This commit is contained in:
2026-04-23 22:47:32 +08:00
parent 1d7e839b49
commit 52007c9461
32 changed files with 531 additions and 55 deletions

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest';
import { request, fromApi, toApi } from '@/api/client';
import { ApiError } from '@/types';

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest';
import { listModels, createModel, updateModel, deleteModel } from '@/api/models';
const mockModels = [

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest';
import { listProviders, createProvider, updateProvider, deleteProvider } from '@/api/providers';
const mockProviders = [
@@ -119,7 +119,7 @@ describe('providers API', () => {
let receivedBody: Record<string, unknown> | null = null;
server.use(
http.put('http://localhost:3000/api/providers/:id', async ({ request, params }) => {
http.put('http://localhost:3000/api/providers/:id', async ({ request }) => {
receivedMethod = request.method;
receivedUrl = new URL(request.url).pathname;
receivedBody = (await request.json()) as Record<string, unknown>;
@@ -153,7 +153,7 @@ describe('providers API', () => {
let receivedUrl: string | null = null;
server.use(
http.delete('http://localhost:3000/api/providers/:id', ({ request, params }) => {
http.delete('http://localhost:3000/api/providers/:id', ({ request }) => {
receivedMethod = request.method;
receivedUrl = new URL(request.url).pathname;
return new HttpResponse(null, { status: 204 });

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest';
import { getStats } from '@/api/stats';
const mockStats = [

View File

@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { BrowserRouter } from 'react-router';
import { describe, it, expect } from 'vitest';
import { AppLayout } from '@/components/AppLayout';
const renderWithRouter = (component: React.ReactNode) => {

View File

@@ -0,0 +1,124 @@
import { RuleTester } from '@typescript-eslint/rule-tester'
import { describe, it, afterAll } from 'vitest'
import rule, {
RULE_NAME,
} from '../../../eslint-rules/rules/no-hardcoded-color-in-style.js'
RuleTester.it = it
RuleTester.describe = describe
RuleTester.afterAll = afterAll
const ruleTester = new RuleTester({
languageOptions: {
parserOptions: {
ecmaFeatures: { jsx: true },
ecmaVersion: 2023,
sourceType: 'module',
},
},
})
describe('no-hardcoded-color-in-style (ESLint rule)', () => {
ruleTester.run(RULE_NAME, rule, {
valid: [
{
name: 'CSS var token',
code: `<div style={{ color: 'var(--td-text-color-placeholder)' }} />`,
},
{
name: 'numeric value 0',
code: `<div style={{ opacity: 0 }} />`,
},
{
name: 'numeric value 16',
code: `<div style={{ width: 16 }} />`,
},
{
name: 'inherit keyword',
code: `<div style={{ color: 'inherit' }} />`,
},
{
name: 'transparent keyword',
code: `<div style={{ color: 'transparent' }} />`,
},
{
name: 'currentColor keyword',
code: `<div style={{ color: 'currentColor' }} />`,
},
{
name: 'none keyword',
code: `<div style={{ display: 'none' }} />`,
},
{
name: 'unset keyword',
code: `<div style={{ color: 'unset' }} />`,
},
{
name: 'initial keyword',
code: `<div style={{ color: 'initial' }} />`,
},
{
name: 'pixel string value',
code: `<div style={{ width: '100px' }} />`,
},
{
name: 'percentage string value',
code: `<div style={{ width: '50%' }} />`,
},
{
name: 'plain numeric string value',
code: `<div style={{ zIndex: '10' }} />`,
},
{
name: 'auto keyword',
code: `<div style={{ width: 'auto' }} />`,
},
{
name: 'contain keyword',
code: `<div style={{ backgroundSize: 'contain' }} />`,
},
{
name: 'cover keyword',
code: `<div style={{ backgroundSize: 'cover' }} />`,
},
],
invalid: [
{
name: 'hex3 color #fff',
code: `<div style={{ color: '#fff' }} />`,
errors: [{ messageId: 'hardcodedColor', data: { value: '#fff' } }],
},
{
name: 'hex6 color #ffffff',
code: `<div style={{ color: '#ffffff' }} />`,
errors: [{ messageId: 'hardcodedColor', data: { value: '#ffffff' } }],
},
{
name: 'hex8 color #ffffffff',
code: `<div style={{ color: '#ffffffff' }} />`,
errors: [{ messageId: 'hardcodedColor', data: { value: '#ffffffff' } }],
},
{
name: 'rgb color',
code: `<div style={{ color: 'rgb(255, 255, 255)' }} />`,
errors: [{ messageId: 'hardcodedColor', data: { value: 'rgb(255, 255, 255)' } }],
},
{
name: 'rgba color',
code: `<div style={{ color: 'rgba(255, 255, 255, 0.5)' }} />`,
errors: [{ messageId: 'hardcodedColor', data: { value: 'rgba(255, 255, 255, 0.5)' } }],
},
{
name: 'hsl color',
code: `<div style={{ color: 'hsl(120, 50%, 50%)' }} />`,
errors: [{ messageId: 'hardcodedColor', data: { value: 'hsl(120, 50%, 50%)' } }],
},
{
name: 'multiple style properties with one hardcoded',
code: `<div style={{ width: 100, color: '#999', opacity: 0.5 }} />`,
errors: [{ messageId: 'hardcodedColor', data: { value: '#999' } }],
},
],
})
})

View File

@@ -1,11 +1,11 @@
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import React from 'react';
import { MessagePlugin } from 'tdesign-react';
import { useModels, useCreateModel, useUpdateModel, useDeleteModel } from '@/hooks/useModels';
import type { Model, CreateModelInput, UpdateModelInput } from '@/types';
import { MessagePlugin } from 'tdesign-react';
// Mock MessagePlugin
vi.mock('tdesign-react', () => ({

View File

@@ -1,11 +1,11 @@
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import React from 'react';
import { MessagePlugin } from 'tdesign-react';
import { useProviders, useCreateProvider, useUpdateProvider, useDeleteProvider } from '@/hooks/useProviders';
import type { Provider, CreateProviderInput, UpdateProviderInput } from '@/types';
import { MessagePlugin } from 'tdesign-react';
// Mock MessagePlugin
vi.mock('tdesign-react', () => ({

View File

@@ -1,8 +1,8 @@
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import React from 'react';
import { useStats } from '@/hooks/useStats';
import type { UsageStats, StatsQueryParams } from '@/types';

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import { Layout, Menu, Button } from 'tdesign-react';
import { ServerIcon, ChartLineIcon, SettingIcon, ChevronLeftIcon, ChevronRightIcon } from 'tdesign-icons-react';
import { Outlet, useLocation, useNavigate } from 'react-router';
import { ServerIcon, ChartLineIcon, SettingIcon, ChevronLeftIcon, ChevronRightIcon } from 'tdesign-icons-react';
import { Layout, Menu, Button } from 'tdesign-react';
const { MenuItem } = Menu;

View File

@@ -1,7 +1,7 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { MessagePlugin } from 'tdesign-react';
import type { CreateModelInput, UpdateModelInput, ApiError } from '@/types';
import * as api from '@/api/models';
import type { CreateModelInput, UpdateModelInput, ApiError } from '@/types';
const ERROR_MESSAGES: Record<string, string> = {
duplicate_model: '同一供应商下模型名称已存在',

View File

@@ -1,7 +1,7 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { MessagePlugin } from 'tdesign-react';
import type { CreateProviderInput, UpdateProviderInput, ApiError } from '@/types';
import * as api from '@/api/providers';
import type { CreateProviderInput, UpdateProviderInput, ApiError } from '@/types';
const ERROR_MESSAGES: Record<string, string> = {
duplicate_model: '同一供应商下模型名称已存在',

View File

@@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import type { StatsQueryParams } from '@/types';
import * as api from '@/api/stats';
import type { StatsQueryParams } from '@/types';
export const statsKeys = {
filtered: (params?: StatsQueryParams) => ['stats', params] as const,

View File

@@ -5,7 +5,11 @@ import 'tdesign-react/es/_util/react-19-adapter'
import './index.scss'
import App from './App'
createRoot(document.getElementById('root')!).render(
const root = document.getElementById('root')
if (!root) {
throw new Error('Root element not found')
}
createRoot(root).render(
<StrictMode>
<App />
</StrictMode>,

View File

@@ -1,5 +1,5 @@
import { Button } from 'tdesign-react';
import { useNavigate } from 'react-router';
import { Button } from 'tdesign-react';
export default function NotFound() {
const navigate = useNavigate();

View File

@@ -1,7 +1,7 @@
import { Button, Table, Tag, Popconfirm, Space } from 'tdesign-react';
import type { PrimaryTableCol } from 'tdesign-react/es/table/type';
import type { Model } from '@/types';
import { useModels, useDeleteModel } from '@/hooks/useModels';
import type { Model } from '@/types';
import type { PrimaryTableCol } from 'tdesign-react/es/table/type';
interface ModelTableProps {
providerId: string;

View File

@@ -1,7 +1,7 @@
import { Button, Table, Tag, Popconfirm, Space, Card } from 'tdesign-react';
import type { PrimaryTableCol } from 'tdesign-react/es/table/type';
import type { Provider, Model } from '@/types';
import { ModelTable } from './ModelTable';
import type { PrimaryTableCol } from 'tdesign-react/es/table/type';
interface ProviderTableProps {
providers: Provider[];

View File

@@ -1,10 +1,10 @@
import { useState } from 'react';
import type { Provider, Model, UpdateProviderInput, UpdateModelInput } from '@/types';
import { useProviders, useCreateProvider, useUpdateProvider, useDeleteProvider } from '@/hooks/useProviders';
import { useCreateModel, useUpdateModel } from '@/hooks/useModels';
import { ProviderTable } from './ProviderTable';
import { ProviderForm } from './ProviderForm';
import { useProviders, useCreateProvider, useUpdateProvider, useDeleteProvider } from '@/hooks/useProviders';
import type { Provider, Model, UpdateProviderInput, UpdateModelInput } from '@/types';
import { ModelForm } from './ModelForm';
import { ProviderForm } from './ProviderForm';
import { ProviderTable } from './ProviderTable';
export default function ProvidersPage() {
const { data: providers = [], isLoading } = useProviders();

View File

@@ -1,5 +1,5 @@
import { Row, Col, Card, Statistic } from 'tdesign-react';
import { ChartBarIcon, ChartLineIcon, ServerIcon, Calendar1Icon } from 'tdesign-icons-react';
import { Row, Col, Card, Statistic } from 'tdesign-react';
import type { UsageStats } from '@/types';
interface StatCardsProps {

View File

@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { Table, Select, Input, DateRangePicker, Space, Card } from 'tdesign-react';
import type { PrimaryTableCol } from 'tdesign-react/es/table/type';
import type { UsageStats, Provider } from '@/types';
import type { PrimaryTableCol } from 'tdesign-react/es/table/type';
interface StatsTableProps {
providers: Provider[];

View File

@@ -1,5 +1,5 @@
import { Card } from 'tdesign-react';
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip } from 'recharts';
import { Card } from 'tdesign-react';
import type { UsageStats } from '@/types';
interface UsageChartProps {

View File

@@ -2,8 +2,8 @@ import { useState, useMemo } from 'react';
import { useProviders } from '@/hooks/useProviders';
import { useStats } from '@/hooks/useStats';
import { StatCards } from './StatCards';
import { UsageChart } from './UsageChart';
import { StatsTable } from './StatsTable';
import { UsageChart } from './UsageChart';
export default function StatsPage() {
const { data: providers = [] } = useProviders();