fix: npm lint errors

This commit is contained in:
2025-11-24 09:39:30 +08:00
parent b1fb054714
commit 0256f0cf22
17 changed files with 195 additions and 222 deletions

View File

@@ -6,16 +6,16 @@ import { API_CONFIG } from './config';
export interface RequestOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
headers?: Record<string, string>;
body?: any;
body?: unknown;
timeout?: number;
}
// 自定义错误类
export class ApiError extends Error {
status?: number;
data?: any;
data?: unknown;
constructor(message: string, status?: number, data?: any) {
constructor(message: string, status?: number, data?: unknown) {
super(message);
this.name = 'ApiError';
this.status = status;
@@ -24,7 +24,7 @@ export class ApiError extends Error {
}
// 基础请求函数
export async function request<T = any>(
export async function request<T = unknown>(
url: string,
options: RequestOptions = {}
): Promise<T> {
@@ -67,18 +67,19 @@ export async function request<T = any>(
clearTimeout(timeoutId);
if (!response.ok) {
let errorData: any;
let errorData: unknown;
try {
errorData = await response.json();
} catch {
errorData = { message: response.statusText };
}
throw new ApiError(
errorData.message || `HTTP ${response.status}: ${response.statusText}`,
response.status,
errorData
);
let messageText = `HTTP ${response.status}: ${response.statusText}`;
if (errorData && typeof errorData === 'object') {
const maybe = errorData as { message?: string };
messageText = maybe.message ?? messageText;
}
throw new ApiError(messageText, response.status, errorData);
}
// 检查响应是否有内容
@@ -108,7 +109,8 @@ export async function request<T = any>(
}
// GET 请求
export function get<T = any>(url: string, params?: Record<string, any>): Promise<T> {
type QueryParamValue = string | number | boolean | null | undefined;
export function get<T = unknown>(url: string, params?: Record<string, QueryParamValue>): Promise<T> {
let fullUrl = url;
if (params) {
@@ -129,7 +131,7 @@ export function get<T = any>(url: string, params?: Record<string, any>): Promise
}
// POST 请求
export function post<T = any>(url: string, data?: any, options?: Partial<RequestOptions>): Promise<T> {
export function post<T = unknown>(url: string, data?: unknown, options?: Partial<RequestOptions>): Promise<T> {
return request<T>(url, {
method: 'POST',
body: data,
@@ -138,7 +140,7 @@ export function post<T = any>(url: string, data?: any, options?: Partial<Request
}
// PUT 请求
export function put<T = any>(url: string, data?: any): Promise<T> {
export function put<T = unknown>(url: string, data?: unknown): Promise<T> {
return request<T>(url, {
method: 'PUT',
body: data,
@@ -146,12 +148,12 @@ export function put<T = any>(url: string, data?: any): Promise<T> {
}
// DELETE 请求
export function del<T = any>(url: string): Promise<T> {
export function del<T = unknown>(url: string): Promise<T> {
return request<T>(url, { method: 'DELETE' });
}
// 文件上传请求
export function upload<T = any>(url: string, file: File, fieldName = 'file', options?: Partial<RequestOptions>): Promise<T> {
export function upload<T = unknown>(url: string, file: File, fieldName = 'file', options?: Partial<RequestOptions>): Promise<T> {
const formData = new FormData();
formData.append(fieldName, file);

View File

@@ -1,7 +1,7 @@
// API 请求和响应类型定义
// 基础响应类型
export interface ApiResponse<T = any> {
export interface ApiResponse<T = unknown> {
data?: T;
error_code: number;
error_info?: string;
@@ -25,7 +25,7 @@ export interface PostInputRequest {
// 人员信息请求类型
export interface PostPeopleRequest {
people: Record<string, any>;
people: People;
}
// 人员查询参数类型
@@ -39,6 +39,7 @@ export interface GetPeoplesParams {
offset?: number;
search?: string;
top_k?: number;
[key: string]: string | number | boolean | null | undefined;
}
// 人员信息类型
@@ -52,8 +53,9 @@ export interface People {
marital_status?: string;
created_at?: number;
match_requirement?: string;
[key: string]: any;
cover?: string;
introduction?: Record<string, string>;
comments?: { remark?: { content: string; updated_at: number } };
}
// 分页响应类型

View File

@@ -56,8 +56,8 @@ export async function postInputImageWithProgress(
try {
const response = xhr.responseText ? JSON.parse(xhr.responseText) : {};
resolve(response);
} catch (error) {
resolve({error_code: 1, error_info: '解析响应失败'});
} catch {
resolve({ error_code: 1, error_info: '解析响应失败' });
}
} else {
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));

View File

@@ -5,10 +5,10 @@ import PeopleForm from './PeopleForm.tsx'
import InputDrawer from './InputDrawer.tsx'
import { createPeoplesBatch, type People } from '../apis'
const { Panel } = Collapse as any
const Panel = Collapse.Panel
const { Content } = Layout
type FormItem = { id: string; initialData?: any }
type FormItem = { id: string; initialData?: Partial<People> }
type Props = { inputOpen?: boolean; onCloseInput?: () => void; containerEl?: HTMLElement | null }
@@ -29,7 +29,18 @@ const BatchRegister: React.FC<Props> = ({ inputOpen = false, onCloseInput, conta
delete instancesRef.current[id]
}
const buildPeople = (values: any, common: any): People => {
type FormValues = {
name: string;
contact?: string;
gender: string;
age: number;
height?: number;
marital_status?: string;
introduction?: Record<string, string>;
match_requirement?: string;
cover?: string;
}
const buildPeople = (values: FormValues, common: { contact?: string }): People => {
return {
name: values.name,
contact: values.contact || common.contact || undefined,
@@ -43,9 +54,9 @@ const BatchRegister: React.FC<Props> = ({ inputOpen = false, onCloseInput, conta
}
}
const handleInputResult = (list: any) => {
const handleInputResult = (list: unknown) => {
const arr = Array.isArray(list) ? list : [list]
const next: FormItem[] = arr.map((data: any) => ({ id: `${Date.now()}-${Math.random()}`, initialData: data }))
const next: FormItem[] = arr.map((data) => ({ id: `${Date.now()}-${Math.random()}`, initialData: data as Partial<People> }))
setItems((prev) => [...prev, ...next])
}
@@ -61,12 +72,12 @@ const BatchRegister: React.FC<Props> = ({ inputOpen = false, onCloseInput, conta
message.error('表单未就绪')
return
}
const allValues: any[] = []
const allValues: FormValues[] = []
for (const f of forms) {
try {
const v = await f.validateFields()
allValues.push(v)
} catch (err: any) {
} catch {
setLoading(false)
message.error('请完善全部表单后再提交')
return
@@ -87,7 +98,7 @@ const BatchRegister: React.FC<Props> = ({ inputOpen = false, onCloseInput, conta
setItems([{ id: `${Date.now()}-${Math.random()}` }])
commonForm.resetFields()
}
} catch (e: any) {
} catch {
message.error('提交失败')
} finally {
setLoading(false)

View File

@@ -7,7 +7,7 @@ import './InputDrawer.css';
type Props = {
open: boolean;
onClose: () => void;
onResult?: (data: any) => void;
onResult?: (data: unknown) => void;
containerEl?: HTMLElement | null; // 抽屉挂载容器(用于放在标题栏下方)
showUpload?: boolean; // 透传到输入面板,控制图片上传按钮
mode?: 'input' | 'search' | 'batch-image'; // 透传到输入面板,控制工作模式
@@ -31,7 +31,7 @@ const InputDrawer: React.FC<Props> = ({ open, onClose, onResult, containerEl, sh
// 在输入处理成功onResult 被调用)后,自动关闭抽屉
const handleResult = React.useCallback(
(data: any) => {
(data: unknown) => {
onResult?.(data);
onClose();
},

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { Input, Upload, message, Button, Spin, Tag } from 'antd';
import type { UploadFile, RcFile } from 'antd/es/upload/interface';
import { PictureOutlined, SendOutlined, LoadingOutlined, SearchOutlined } from '@ant-design/icons';
import { postInput, postInputImage, getPeoples } from '../apis';
import './InputPanel.css';
@@ -7,14 +8,14 @@ import './InputPanel.css';
const { TextArea } = Input;
interface InputPanelProps {
onResult?: (data: any) => void;
onResult?: (data: unknown) => void;
showUpload?: boolean; // 是否显示图片上传按钮,默认显示
mode?: 'input' | 'search' | 'batch-image'; // 输入面板工作模式,新增批量图片模式
}
const InputPanel: React.FC<InputPanelProps> = ({ onResult, showUpload = true, mode = 'input' }) => {
const [value, setValue] = React.useState('');
const [fileList, setFileList] = React.useState<any[]>([]);
const [fileList, setFileList] = React.useState<UploadFile[]>([]);
const [loading, setLoading] = React.useState(false);
// 批量模式不保留文本内容
@@ -63,9 +64,9 @@ const InputPanel: React.FC<InputPanelProps> = ({ onResult, showUpload = true, mo
}
setLoading(true);
try {
const results: any[] = [];
const results: unknown[] = [];
for (let i = 0; i < fileList.length; i++) {
const f = fileList[i].originFileObj || fileList[i];
const f = fileList[i].originFileObj as RcFile | undefined;
if (!f) continue;
const resp = await postInputImage(f);
if (resp && resp.error_code === 0 && resp.data) {
@@ -95,7 +96,7 @@ const InputPanel: React.FC<InputPanelProps> = ({ onResult, showUpload = true, mo
// 如果有图片,优先处理图片上传
if (hasImage) {
const file = fileList[0].originFileObj || fileList[0];
const file = fileList[0].originFileObj as RcFile | undefined;
if (!file) {
message.error('图片文件无效,请重新选择');
return;
@@ -152,18 +153,18 @@ const InputPanel: React.FC<InputPanelProps> = ({ onResult, showUpload = true, mo
if (!items || items.length === 0) return;
if (mode === 'batch-image') {
const newEntries: any[] = [];
const newEntries: UploadFile[] = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.kind === 'file') {
const file = item.getAsFile();
if (file && file.type.startsWith('image/')) {
const entry = {
const entry: UploadFile<RcFile> = {
uid: `${Date.now()}-${Math.random()}`,
name: 'image',
status: 'done',
originFileObj: file,
} as any;
originFileObj: file as unknown as RcFile,
};
newEntries.push(entry);
}
}
@@ -190,14 +191,13 @@ const InputPanel: React.FC<InputPanelProps> = ({ onResult, showUpload = true, mo
if (firstImage) {
e.preventDefault();
setValue('');
setFileList([
{
uid: `${Date.now()}-${Math.random()}`,
name: 'image',
status: 'done',
originFileObj: firstImage,
} as any,
]);
const item: UploadFile<RcFile> = {
uid: `${Date.now()}-${Math.random()}`,
name: 'image',
status: 'done',
originFileObj: firstImage as unknown as RcFile,
};
setFileList([item]);
message.success('已添加剪贴板图片');
}
}
@@ -278,9 +278,9 @@ const InputPanel: React.FC<InputPanelProps> = ({ onResult, showUpload = true, mo
fileList={fileList}
onChange={({ fileList: nextFileList }) => {
if (mode === 'batch-image') {
const normalized = nextFileList.map((entry: any) => {
const raw = entry.originFileObj || entry;
return { ...entry, name: 'image', originFileObj: raw };
const normalized: UploadFile<RcFile>[] = nextFileList.map((entry) => {
const raw = (entry as UploadFile<RcFile>).originFileObj;
return { ...(entry as UploadFile<RcFile>), name: 'image', originFileObj: raw };
});
setValue('');
setFileList(normalized);
@@ -290,15 +290,15 @@ const InputPanel: React.FC<InputPanelProps> = ({ onResult, showUpload = true, mo
return;
}
// 仅添加第一张
const first = nextFileList[0] as any;
const raw = first.originFileObj || first;
const renamed = { ...first, name: 'image', originFileObj: raw };
const first = nextFileList[0] as UploadFile<RcFile>;
const raw = first.originFileObj;
const renamed: UploadFile<RcFile> = { ...first, name: 'image', originFileObj: raw };
setValue('');
setFileList([renamed]);
}
}}
onRemove={(file) => {
setFileList((prev) => prev.filter((x) => x.uid !== (file as any).uid));
setFileList((prev) => prev.filter((x) => x.uid !== (file as UploadFile<RcFile>).uid));
return true;
}}
showUploadList={false}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { Row, Col, Input, Button } from 'antd';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
import './KeyValueList.css';
@@ -15,11 +15,8 @@ type Props = {
const KeyValueList: React.FC<Props> = ({ value, onChange }) => {
const [rows, setRows] = useState<KeyValuePair[]>([]);
const initializedRef = useRef(false);
useEffect(() => {
// 初始化时提供一行空输入;之后只合并父值,不再自动新增空行
const initializedRef = (KeyValueList as any)._initializedRef || { current: false };
(KeyValueList as any)._initializedRef = initializedRef;
setRows((prev) => {
const existingIdByKey = new Map(prev.filter((r) => r.k).map((r) => [r.k, r.id]));
const valuePairs: KeyValuePair[] = value

View File

@@ -66,7 +66,7 @@ const LayoutWrapper: React.FC = () => {
showInput={isHome || isList || isBatch}
/>
{/* 下方为主布局:左侧菜单 + 右侧内容 */}
<Layout ref={layoutShellRef as any} className="layout-shell">
<Layout ref={layoutShellRef} className="layout-shell">
<SiderMenu
onNavigate={handleNavigate}
selectedKey={selectedKey}

View File

@@ -1,10 +1,11 @@
import React from 'react';
import { Modal, Form, Input, Button, message } from 'antd';
import type { LoginRequest } from '../apis/types';
interface Props {
open: boolean;
onCancel: () => void;
onOk: (values: any) => Promise<void>;
onOk: (values: LoginRequest) => Promise<void>;
title: string;
username?: string;
usernameReadOnly?: boolean;
@@ -30,12 +31,12 @@ const LoginModal: React.FC<Props> = ({ open, onCancel, onOk, title, username, us
const payload = isEmail
? { email: username, password }
: { phone: username, password };
await onOk(payload as any);
await onOk(payload as LoginRequest);
if (!hideSuccessMessage) {
message.success('登录成功');
}
onCancel();
} catch (error) {
} catch {
message.error('登录失败');
}
};

View File

@@ -3,15 +3,16 @@ import { Layout, Typography } from 'antd';
import PeopleForm from './PeopleForm.tsx';
import InputDrawer from './InputDrawer.tsx';
import './MainContent.css';
import type { People } from '../apis';
const { Content } = Layout;
type Props = { inputOpen?: boolean; onCloseInput?: () => void; containerEl?: HTMLElement | null };
const MainContent: React.FC<Props> = ({ inputOpen = false, onCloseInput, containerEl }) => {
const [formData, setFormData] = React.useState<any>(null);
const [formData, setFormData] = React.useState<Partial<People> | null>(null);
const handleInputResult = (data: any) => {
setFormData(data);
const handleInputResult = (data: unknown) => {
setFormData(data as Partial<People>);
};
return (
@@ -24,7 +25,7 @@ const MainContent: React.FC<Props> = ({ inputOpen = false, onCloseInput, contain
</Typography.Paragraph>
<PeopleForm initialData={formData} />
<PeopleForm initialData={formData || undefined} />
</div>
{/* 首页右侧输入抽屉,仅在顶栏点击后弹出;挂载到标题栏下方容器 */}

View File

@@ -8,7 +8,7 @@ import { createPeople, type People } from '../apis';
const { TextArea } = Input;
interface PeopleFormProps {
initialData?: any;
initialData?: Partial<People>;
// 编辑模式下由父组件控制提交,隐藏内部提交按钮
hideSubmitButton?: boolean;
// 暴露 AntD Form 实例给父组件,用于在外部触发校验与取值
@@ -22,11 +22,7 @@ const PeopleForm: React.FC<PeopleFormProps> = ({ initialData, hideSubmitButton =
// 当 initialData 变化时,自动填充表单
useEffect(() => {
if (initialData) {
console.log('收到API返回数据自动填充表单:', initialData);
// 处理返回的数据,将其转换为表单需要的格式
const formData: any = {};
const formData: Partial<People> = {};
if (initialData.name) formData.name = initialData.name;
if (initialData.contact) formData.contact = initialData.contact;
if (initialData.cover) formData.cover = initialData.cover;
@@ -35,13 +31,8 @@ const PeopleForm: React.FC<PeopleFormProps> = ({ initialData, hideSubmitButton =
if (initialData.height) formData.height = initialData.height;
if (initialData.marital_status) formData.marital_status = initialData.marital_status;
if (initialData.match_requirement) formData.match_requirement = initialData.match_requirement;
if (initialData.introduction) formData.introduction = initialData.introduction;
// 设置表单字段值
if (initialData.introduction) formData.introduction = initialData.introduction as Record<string, string>;
form.setFieldsValue(formData);
// 显示成功消息
// message.success('已自动填充表单,请检查并确认信息');
}
}, [initialData, form]);
@@ -52,7 +43,18 @@ const PeopleForm: React.FC<PeopleFormProps> = ({ initialData, hideSubmitButton =
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onFinish = async (values: any) => {
type FormValues = {
name: string;
contact?: string;
gender: string;
age: number;
height?: number;
marital_status?: string;
introduction?: Record<string, string>;
match_requirement?: string;
cover?: string;
};
const onFinish = async (values: FormValues) => {
setLoading(true);
try {
@@ -81,16 +83,14 @@ const PeopleForm: React.FC<PeopleFormProps> = ({ initialData, hideSubmitButton =
message.error(response.error_info || '提交失败,请重试');
}
} catch (error: any) {
console.error('提交失败:', error);
// 根据错误类型显示不同的错误信息
if (error.status === 422) {
} catch (e) {
const err = e as { status?: number; message?: string };
if (err.status === 422) {
message.error('表单数据格式有误,请检查输入内容');
} else if (error.status >= 500) {
} else if ((err.status ?? 0) >= 500) {
message.error('服务器错误,请稍后重试');
} else {
message.error(error.message || '提交失败,请重试');
message.error(err.message || '提交失败,请重试');
}
} finally {
setLoading(false);

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Modal, Form, Input, Button, message } from 'antd';
import { useAuth } from '../contexts/AuthContext';
import { useAuth } from '../contexts/useAuth';
import type { RegisterRequest } from '../apis/types';
interface Props {
@@ -29,7 +29,7 @@ const RegisterModal: React.FC<Props> = ({ open, onCancel }) => {
await sendCode({ target_type, target, scene: 'register' });
message.success('验证码已发送');
setStep('verify');
} catch (error) {
} catch {
message.error('发送验证码失败');
}
};
@@ -40,7 +40,7 @@ const RegisterModal: React.FC<Props> = ({ open, onCancel }) => {
await register({ ...registerPayload, ...values } as RegisterRequest);
message.success('注册成功');
onCancel();
} catch (error) {
} catch {
message.error('注册失败');
}
};

View File

@@ -22,7 +22,7 @@ export type Resource = Omit<People, 'id'> & { id: string };
// 统一转换 API 返回的人员列表为表格需要的结构
function transformPeoples(list: People[] = []): Resource[] {
return (list || []).map((person: any) => ({
return (list || []).map((person: Partial<People>) => ({
id: person.id || `person-${Date.now()}-${Math.random()}`,
name: person.name || '未知',
gender: person.gender || '其他/保密',
@@ -72,7 +72,7 @@ async function fetchResources(): Promise<Resource[]> {
return transformed;
} catch (error: any) {
} catch (error: unknown) {
console.error('获取人员列表失败:', error);
message.error('获取人员列表失败,使用模拟数据');
@@ -463,124 +463,70 @@ async function fetchResources(): Promise<Resource[]> {
}
// 数字范围筛选下拉
function NumberRangeFilterDropdown({ setSelectedKeys, selectedKeys, confirm, clearFilters }: FilterDropdownProps) {
const [min, max] = String(selectedKeys?.[0] ?? ':').split(':');
const [localMin, setLocalMin] = React.useState<number | undefined>(min ? Number(min) : undefined);
const [localMax, setLocalMax] = React.useState<number | undefined>(max ? Number(max) : undefined);
return (
<div style={{ padding: 8 }}>
<Space direction="vertical" style={{ width: 200 }}>
<InputNumber placeholder="最小值" value={localMin} onChange={(v) => setLocalMin(v ?? undefined)} style={{ width: '100%' }} />
<InputNumber placeholder="最大值" value={localMax} onChange={(v) => setLocalMax(v ?? undefined)} style={{ width: '100%' }} />
<Space>
<Button type="primary" size="small" icon={<SearchOutlined />} onClick={() => { const key = `${localMin ?? ''}:${localMax ?? ''}`; setSelectedKeys?.([key]); confirm?.({ closeDropdown: true }); }}></Button>
<Button size="small" onClick={() => { setLocalMin(undefined); setLocalMax(undefined); setSelectedKeys?.([]); clearFilters?.(); confirm?.({ closeDropdown: true }); }}></Button>
</Space>
</Space>
</div>
);
}
function buildNumberRangeFilter(dataIndex: keyof Resource, label: string): ColumnType<Resource> {
return {
title: label,
dataIndex,
sorter: (a: Resource, b: Resource) => Number((a as any)[dataIndex] ?? 0) - Number((b as any)[dataIndex] ?? 0),
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }: FilterDropdownProps) => {
const [min, max] = String(selectedKeys?.[0] ?? ':').split(':');
const [localMin, setLocalMin] = React.useState<number | undefined>(min ? Number(min) : undefined);
const [localMax, setLocalMax] = React.useState<number | undefined>(max ? Number(max) : undefined);
return (
<div style={{ padding: 8 }}>
<Space direction="vertical" style={{ width: 200 }}>
<InputNumber
placeholder="最小值"
value={localMin}
onChange={(v) => setLocalMin(v ?? undefined)}
style={{ width: '100%' }}
/>
<InputNumber
placeholder="最大值"
value={localMax}
onChange={(v) => setLocalMax(v ?? undefined)}
style={{ width: '100%' }}
/>
<Space>
<Button
type="primary"
size="small"
icon={<SearchOutlined />}
onClick={() => {
const key = `${localMin ?? ''}:${localMax ?? ''}`;
setSelectedKeys?.([key]);
confirm?.({ closeDropdown: true });
}}
>
</Button>
<Button
size="small"
onClick={() => {
setLocalMin(undefined);
setLocalMax(undefined);
setSelectedKeys?.([]);
clearFilters?.();
confirm?.({ closeDropdown: true });
}}
>
</Button>
</Space>
</Space>
</div>
);
sorter: (a: Resource, b: Resource) => {
const av = a[dataIndex] as number | undefined;
const bv = b[dataIndex] as number | undefined;
return Number(av ?? 0) - Number(bv ?? 0);
},
filterDropdown: (props) => <NumberRangeFilterDropdown {...props} />,
onFilter: (filterValue: React.Key | boolean, record: Resource) => {
const [minStr, maxStr] = String(filterValue).split(':');
const min = minStr ? Number(minStr) : undefined;
const max = maxStr ? Number(maxStr) : undefined;
const val = Number((record as any)[dataIndex] ?? NaN);
if (Number.isNaN(val)) return false;
if (min !== undefined && val < min) return false;
if (max !== undefined && val > max) return false;
const val = record[dataIndex] as number | undefined;
if (val === undefined || Number.isNaN(Number(val))) return false;
if (min !== undefined && Number(val) < min) return false;
if (max !== undefined && Number(val) > max) return false;
return true;
},
} as ColumnType<Resource>;
}
// 枚举筛选下拉(用于性别等枚举类列)
function EnumFilterDropdown({ setSelectedKeys, selectedKeys, confirm, clearFilters, options, label }: FilterDropdownProps & { options: Array<{ label: string; value: string }>; label: string }) {
const [val, setVal] = React.useState<string | undefined>(selectedKeys && selectedKeys[0] ? String(selectedKeys[0]) : undefined);
return (
<div className="byte-table-custom-filter" style={{ padding: 8 }}>
<Space direction="vertical" style={{ width: 200 }}>
<Select allowClear placeholder={`请选择${label}`} value={val} onChange={(v) => setVal(v)} options={options} style={{ width: '100%' }} />
<Space>
<Button type="primary" size="small" icon={<SearchOutlined />} onClick={() => { setSelectedKeys?.(val ? [val] : []); confirm?.({ closeDropdown: true }); }}></Button>
<Button size="small" onClick={() => { setVal(undefined); setSelectedKeys?.([]); clearFilters?.(); confirm?.({ closeDropdown: true }); }}></Button>
</Space>
</Space>
</div>
);
}
function buildEnumFilter(dataIndex: keyof Resource, label: string, options: Array<{ label: string; value: string }>): ColumnType<Resource> {
return {
title: label,
dataIndex,
key: String(dataIndex),
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }: FilterDropdownProps) => {
const [val, setVal] = React.useState<string | undefined>(
(selectedKeys && selectedKeys[0] ? String(selectedKeys[0]) : undefined)
);
return (
<div className="byte-table-custom-filter" style={{ padding: 8 }}>
<Space direction="vertical" style={{ width: 200 }}>
<Select
allowClear
placeholder={`请选择${label}`}
value={val}
onChange={(v) => setVal(v)}
options={options}
style={{ width: '100%' }}
/>
<Space>
<Button
type="primary"
size="small"
icon={<SearchOutlined />}
onClick={() => {
setSelectedKeys?.(val ? [val] : []);
confirm?.({ closeDropdown: true });
}}
>
</Button>
<Button
size="small"
onClick={() => {
setVal(undefined);
setSelectedKeys?.([]);
clearFilters?.();
confirm?.({ closeDropdown: true });
}}
>
</Button>
</Space>
</Space>
</div>
);
},
onFilter: (filterValue: React.Key | boolean, record: Resource) =>
String((record as any)[dataIndex]) === String(filterValue),
filterDropdown: (props) => <EnumFilterDropdown {...props} options={options} label={label} />,
onFilter: (filterValue: React.Key | boolean, record: Resource) => String(record[dataIndex] ?? '') === String(filterValue),
render: (g: string) => {
const color = g === '男' ? 'blue' : g === '女' ? 'magenta' : 'default';
return <Tag color={color}>{g}</Tag>;
@@ -654,7 +600,7 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
} else {
message.error(res.error_info || '删除失败');
}
} catch (err: any) {
} catch {
message.error('删除失败');
} finally {
await reloadResources();
@@ -688,7 +634,7 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
onFilter: (filterValue: React.Key | boolean, record: Resource) => String(record.name).includes(String(filterValue)),
render: (text: string, record: Resource) => {
// 图片图标逻辑
const hasCover = record.cover && record.cover.trim() !== '';
const hasCover = typeof record.cover === 'string' && record.cover.trim() !== '';
const pictureIcon = (
<PictureOutlined
style={{
@@ -696,7 +642,7 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
cursor: hasCover ? 'pointer' : 'default'
}}
onClick={hasCover ? () => {
setCurrentImageUrl(record.cover);
setCurrentImageUrl(record.cover as string);
setImageModalVisible(true);
} : undefined}
/>
@@ -834,7 +780,7 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
title: '操作',
key: 'actions',
width: 80,
render: (_: any, record: Resource) => (
render: (_: unknown, record: Resource) => (
<Dropdown
trigger={["click"]}
menu={{
@@ -952,8 +898,9 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
} else {
message.error(res.error_info || '更新失败');
}
} catch (err: any) {
if (err?.errorFields) {
} catch (err) {
const hasErrorFields = typeof err === 'object' && err !== null && 'errorFields' in err;
if (hasErrorFields) {
message.error('请完善表单后再保存');
} else {
message.error('更新失败');
@@ -1003,7 +950,7 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
}
},
}
: ({} as any)
: {}
}
pagination={{
...pagination,
@@ -1055,7 +1002,7 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
)}
{record.created_at && (
<div style={{ fontSize: '12px', color: '#999', marginTop: '12px' }}>
{record.created_at ? '录入于: ' + formatDate(record.created_at) : ''}
{record.created_at ? '录入于: ' + formatDate(Number(record.created_at)) : ''}
</div>
)}
@@ -1064,7 +1011,7 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
<span></span>
</Typography.Title>
{record.match_requirement ? (
<div>{record.match_requirement}</div>
<div>{String(record.match_requirement)}</div>
) : (
<div style={{ color: '#9ca3af' }}></div>
)}
@@ -1078,7 +1025,7 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
{record.comments && record.comments.remark ? (
<Space>
<Button size="small" onClick={() => {
setEditingRemark({ recordId: record.id, content: record.comments.remark.content });
setEditingRemark({ recordId: record.id, content: record.comments?.remark?.content || '' });
setRemarkModalVisible(true);
}}></Button>
<Button size="small" danger onClick={async () => {
@@ -1090,7 +1037,7 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
} else {
message.error(res.error_info || '清空失败');
}
} catch (err) {
} catch {
message.error('清空失败');
}
}}></Button>
@@ -1121,7 +1068,7 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
<InputDrawer
open={inputOpen}
onClose={onCloseInput || (() => {})}
onResult={(list: any) => {
onResult={(list: unknown) => {
// setInputResult(list);
const mapped = transformPeoples(Array.isArray(list) ? list : []);
setData(mapped);
@@ -1186,13 +1133,14 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
} else {
message.error(res.error_info || '更新失败');
}
} catch (err: any) {
if (err?.errorFields) {
message.error('请完善表单后再确认');
} else {
message.error('更新失败');
} catch (err) {
const hasErrorFields = typeof err === 'object' && err !== null && 'errorFields' in err;
if (hasErrorFields) {
message.error('请完善表单后再确认');
} else {
message.error('更新失败');
}
}
}
}}
destroyOnHidden
okText="确认"
@@ -1225,7 +1173,7 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
} else {
message.error(res.error_info || '操作失败');
}
} catch (err) {
} catch {
message.error('操作失败');
}
}}

View File

@@ -1,6 +1,6 @@
import LoginModal from './LoginModal';
import RegisterModal from './RegisterModal';
import { useAuth } from '../contexts/AuthContext';
import { useAuth } from '../contexts/useAuth';
import React from 'react';
import { Layout, Menu, Grid, Drawer, Button } from 'antd';
import { FormOutlined, UnorderedListOutlined, MenuOutlined, CopyOutlined, UserOutlined, SettingOutlined } from '@ant-design/icons';

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { Form, Input, Button, message, Card, Space, Upload, Modal } from 'antd'
import 'react-image-crop/dist/ReactCrop.css'
import ReactCrop, { centerCrop, makeAspectCrop, type Crop } from 'react-image-crop'
import { useAuth } from '../contexts/AuthContext'
import { useAuth } from '../contexts/useAuth'
import { updateMe, deleteUser, uploadAvatar, updatePhone, updateEmail } from '../apis'
import { UserOutlined, EditOutlined } from '@ant-design/icons'
import LoginModal from './LoginModal';

View File

@@ -1,4 +1,4 @@
import { createContext, useContext, useState, useEffect } from 'react';
import { createContext, useState, useEffect } from 'react';
import type { ReactNode } from 'react';
import { login as apiLogin, register as apiRegister, sendCode as apiSendCode, getMe as apiGetMe, logout as apiLogout } from '../apis';
import type { LoginRequest, RegisterRequest, User, SendCodeRequest } from '../apis/types';
@@ -28,9 +28,10 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
if (response.data) {
setUser(response.data);
}
} catch (error) {
} catch {
setToken(null);
localStorage.removeItem('token');
void 0;
}
};
validateSession();
@@ -48,7 +49,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
if (me.data) {
setUser(me.data);
}
} catch (e) { void e; }
} catch {
void 0;
}
};
const logout = async () => {
@@ -79,7 +82,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
if (me.data) {
setUser(me.data);
}
} catch (e) { void e; }
} catch {
void 0;
}
};
const value = {
@@ -97,10 +102,4 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
export default AuthContext;

12
src/contexts/useAuth.ts Normal file
View File

@@ -0,0 +1,12 @@
import { useContext } from 'react';
import AuthContext from './AuthContext';
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
export default useAuth;