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

View File

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

View File

@@ -56,8 +56,8 @@ export async function postInputImageWithProgress(
try { try {
const response = xhr.responseText ? JSON.parse(xhr.responseText) : {}; const response = xhr.responseText ? JSON.parse(xhr.responseText) : {};
resolve(response); resolve(response);
} catch (error) { } catch {
resolve({error_code: 1, error_info: '解析响应失败'}); resolve({ error_code: 1, error_info: '解析响应失败' });
} }
} else { } else {
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`)); 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 InputDrawer from './InputDrawer.tsx'
import { createPeoplesBatch, type People } from '../apis' import { createPeoplesBatch, type People } from '../apis'
const { Panel } = Collapse as any const Panel = Collapse.Panel
const { Content } = Layout 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 } 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] 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 { return {
name: values.name, name: values.name,
contact: values.contact || common.contact || undefined, 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 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]) setItems((prev) => [...prev, ...next])
} }
@@ -61,12 +72,12 @@ const BatchRegister: React.FC<Props> = ({ inputOpen = false, onCloseInput, conta
message.error('表单未就绪') message.error('表单未就绪')
return return
} }
const allValues: any[] = [] const allValues: FormValues[] = []
for (const f of forms) { for (const f of forms) {
try { try {
const v = await f.validateFields() const v = await f.validateFields()
allValues.push(v) allValues.push(v)
} catch (err: any) { } catch {
setLoading(false) setLoading(false)
message.error('请完善全部表单后再提交') message.error('请完善全部表单后再提交')
return return
@@ -87,7 +98,7 @@ const BatchRegister: React.FC<Props> = ({ inputOpen = false, onCloseInput, conta
setItems([{ id: `${Date.now()}-${Math.random()}` }]) setItems([{ id: `${Date.now()}-${Math.random()}` }])
commonForm.resetFields() commonForm.resetFields()
} }
} catch (e: any) { } catch {
message.error('提交失败') message.error('提交失败')
} finally { } finally {
setLoading(false) setLoading(false)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import LoginModal from './LoginModal'; import LoginModal from './LoginModal';
import RegisterModal from './RegisterModal'; import RegisterModal from './RegisterModal';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/useAuth';
import React from 'react'; import React from 'react';
import { Layout, Menu, Grid, Drawer, Button } from 'antd'; import { Layout, Menu, Grid, Drawer, Button } from 'antd';
import { FormOutlined, UnorderedListOutlined, MenuOutlined, CopyOutlined, UserOutlined, SettingOutlined } from '@ant-design/icons'; 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 { Form, Input, Button, message, Card, Space, Upload, Modal } from 'antd'
import 'react-image-crop/dist/ReactCrop.css' import 'react-image-crop/dist/ReactCrop.css'
import ReactCrop, { centerCrop, makeAspectCrop, type Crop } from 'react-image-crop' 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 { updateMe, deleteUser, uploadAvatar, updatePhone, updateEmail } from '../apis'
import { UserOutlined, EditOutlined } from '@ant-design/icons' import { UserOutlined, EditOutlined } from '@ant-design/icons'
import LoginModal from './LoginModal'; 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 type { ReactNode } from 'react';
import { login as apiLogin, register as apiRegister, sendCode as apiSendCode, getMe as apiGetMe, logout as apiLogout } from '../apis'; 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'; import type { LoginRequest, RegisterRequest, User, SendCodeRequest } from '../apis/types';
@@ -28,9 +28,10 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
if (response.data) { if (response.data) {
setUser(response.data); setUser(response.data);
} }
} catch (error) { } catch {
setToken(null); setToken(null);
localStorage.removeItem('token'); localStorage.removeItem('token');
void 0;
} }
}; };
validateSession(); validateSession();
@@ -48,7 +49,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
if (me.data) { if (me.data) {
setUser(me.data); setUser(me.data);
} }
} catch (e) { void e; } } catch {
void 0;
}
}; };
const logout = async () => { const logout = async () => {
@@ -79,7 +82,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
if (me.data) { if (me.data) {
setUser(me.data); setUser(me.data);
} }
} catch (e) { void e; } } catch {
void 0;
}
}; };
const value = { const value = {
@@ -97,10 +102,4 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}; };
export const useAuth = () => { export default AuthContext;
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

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;