<input type=“file“> accept属性筛选文件类型上传

本文介绍如何使用HTML表单的accept属性来限定用户上传特定类型的文件。通过设置不同的MIME类型或文件扩展名,可以实现对.doc、.docx、.xls、.xlsx、.pdf等格式的支持。

关于form表单上传文件类型的筛选,如果你不希望用户上传任何类型的文件, 你可以使用 input 的 accept 属性.

 

设置支持 .doc / .docx / .xls / .xlsx / .pdf 格式:

 

1

<input type="file" accept=".doc,.docx,.xls,.xlsx,.pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document" >

如果需要支持 .png  等,则在 accept 添加上既可,都逗号分隔。

扩展知识:

accept 属性接受一个逗号分隔的 MIME 类型字符串, 如:

  • accept="image/png" or accept=".png" — 只接受 png 图片.
  • accept="image/png, image/jpeg" or accept=".png, .jpg, .jpeg" — PNG/JPEG 文件.
  • accept="image/*" — 接受任何图片文件类型. 
  • accept=".doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document" — 接受任何 MS Doc 文件类型

如果不限制上传图片的格式可写成:accept="image/*"

其它格式参考如下:

 

*.3gppaudio/3gpp, video/3gpp3GPP Audio/Video
*.ac3audio/ac3AC3 Audio
*.asfallpication/vnd.ms-asfAdvanced Streaming Format
*.auaudio/basicAU Audio
*.csstext/cssCascading Style Sheets
*.csvtext/csvComma Separated Values
*.docapplication/mswordMS Word Document
*.dotapplication/mswordMS Word Template
*.dtdapplication/xml-dtdDocument Type Definition
*.dwgimage/vnd.dwgAutoCAD Drawing Database
*.dxfimage/vnd.dxfAutoCAD Drawing Interchange Format
*.gifimage/gifGraphic Interchange Format
*.htmtext/htmlHyperText Markup Language
*.htmltext/htmlHyperText Markup Language
*.jp2image/jp2JPEG-2000
*.jpeimage/jpegJPEG
*.jpegimage/jpegJPEG
*.jpgimage/jpegJPEG
*.jstext/javascript, application/javascriptJavaScript
*.jsonapplication/jsonJavaScript Object Notation
*.mp2audio/mpeg, video/mpegMPEG Audio/Video Stream, Layer II
*.mp3audio/mpegMPEG Audio Stream, Layer III
*.mp4audio/mp4, video/mp4MPEG-4 Audio/Video
*.mpegvideo/mpegMPEG Video Stream, Layer II
*.mpgvideo/mpegMPEG Video Stream, Layer II
*.mppapplication/vnd.ms-projectMS Project Project
*.oggapplication/ogg, audio/oggOgg Vorbis
*.pdfapplication/pdfPortable Document Format
*.pngimage/pngPortable Network Graphics
*.potapplication/vnd.ms-powerpointMS PowerPoint Template
*.ppsapplication/vnd.ms-powerpointMS PowerPoint Slideshow
*.pptapplication/vnd.ms-powerpointMS PowerPoint Presentation
*.rtfapplication/rtf, text/rtfRich Text Format
*.svfimage/vnd.svfSimple Vector Format
*.tifimage/tiffTagged Image Format File
*.tiffimage/tiffTagged Image Format File
*.txttext/plainPlain Text
*.wdbapplication/vnd.ms-worksMS Works Database
*.wpsapplication/vnd.ms-worksWorks Text Document
*.xhtmlapplication/xhtml+xmlExtensible HyperText Markup Language
*.xlcapplication/vnd.ms-excelMS Excel Chart
*.xlmapplication/vnd.ms-excelMS Excel Macro
*.xlsapplication/vnd.ms-excelMS Excel Spreadsheet
*.xltapplication/vnd.ms-excelMS Excel Template
*.xlwapplication/vnd.ms-excelMS Excel Workspace
*.xmltext/xml, application/xmlExtensible Markup Language
*.zipaplication/zipCompressed Archive

举例:

html input=”file” 浏览时只显示指定文件类型 xls、xlsx、csv


<input id="fileSelect" type="file" accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" />

Valid Accept Types:

For CSV files (.csv), use: 

<input type="file" accept=".csv" />

For Excel Files 2003-2007 (.xls), use: 

<input type="file" accept="application/vnd.ms-excel" />

For Excel Files 2010 (.xlsx), use: 

<input type="file" accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" />

For Text Files (.txt) use: 

<input type="file" accept="text/plain" />

For Image Files (.png/.jpg/etc), use: 

<input type="file" accept="image/*" />

For HTML Files (.htm,.html), use:

<input type="file" accept="text/html" />

For Video Files (.avi, .mpg, .mpeg, .mp4), use:

<input type="file" accept="video/*" />

For Audio Files (.mp3, .wav, etc), use:

<input type="file" accept="audio/*" />

For PDF Files, use:

<input type="file" accept=".pdf" /> 

 

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>商品管理</title> <script src="./js/echarts.js"></script> <link rel="stylesheet" href="./css/public.css" /> <link rel="stylesheet" href="./css/goodschild.css" /> <link rel="stylesheet" href="./css/myPagination.css"> <script src="./js/jquery-3.7.1.min.js"></script> <script type="text/javascript" charset="utf-8" src="./ueditor/ueditor.config.js"></script> <script type="text/javascript" charset="utf-8" src="./ueditor/ueditor.all.min.js"> </script> <script type="text/javascript" charset="utf-8" src="./ueditor/lang/zh-cn/zh-cn.js"></script> </head> <body> <!-- 预览的幕布 --> <div id="imagePreview" class="preview_modal" onclick="previewCancel()"> <!-- 预览的图片 --> <img id="previewImage" src="" alt="Preview Image"> </div> <!-- 顶部下拉框 --> <div class="top_box" id="top_box"> <div class="top_left"> <input type="text" placeholder="请输入产品名称搜索" id="searchInput"> </div> <div class="top_right"> <button id="searchBtn">搜索</button> <button id="resetBtn">重置</button> </div> </div> <!-- 按钮区域 --> <div class="top_boxs"> <div class="top_button"> <button id="addition" class="icon-btns"> <img src="./img/返回.png" alt="图标描述" width="15" height="15"> <p>返回</p> </button> <button id="additionst" class="icon-btns"> <img src="./img/加号.png" alt="图标描述" width="20" height="20"> <p>新增</p> </button> <button id="expurgate" class="icon-btn" disabled> <p>全部</p> <span class="badge" id="badge_first"></span> </button> <button id="search" class="icon-btn" disabled> <p>待审核</p> <span class="badge" id="badge_second"></span> </button> <button id="searchsa" class="icon-btn" disabled> <p>回收站</p> <span class="badge" id="badge_seconds"></span> </button> <button id="additions" class="icon-btns"> <p>批量审核</p> </button> <button id="expurgates" class="icon-btn"> <img src="./img/删除.png" alt="图标描述" width="20" height="20"> <p>批量删除</p> </button> </div> <div class="screen" id="refreshs"> <div class="screen_box"> <img src="./img/搜索-灰色.png" alt="刷新" class="renovate" id="searchs" /> </div> <div class="screen_box"> <img src="./img/刷新.png" alt="刷新" class="renovate" id="refresh" /> </div> <div class="screen_box"> <div class="screen_img"> <img src="./img/设置-灰色.png" alt="刷新" class="renovate" id="install" /> </div> <div class="setting_box"> <p class="setting_title">显示设置</p> <div class="setting_items"> <label> <input type="checkbox" value="id" checked> ID </label> <label> <input type="checkbox" value="company" checked> 公司名称 </label> <label> <input type="checkbox" value="tradeid" checked> 产品名称 </label> <label> <input type="checkbox" value="img" checked> 商品图片 </label> <label> <input type="checkbox" value="name" checked>行业 </label> <label> <input type="checkbox" value="status" checked> 审核状态 </label> <label> <input type="checkbox" value="type" checked> 上架状态 </label> <label> <input type="checkbox" value="create_time" checked> 创建时间 </label> </div> </div> </div> </div> </div> <div class="base_box"> <div id="base_package"> <table class="table_table-bordered"> <thead> <tr> <th> <input type="checkbox" id="selectAll" name="check-all"> </th> <th>id</th> <th>公司名称</th> <th>产品名称</th> <th>商品图片</th> <th>行业</th> <th>审核状态</th> <th>上架状态</th> <th>创建时间</th> <th>操作</th> </tr> </thead> <tbody> </tbody> </table> </div> <!-- 底部分页 --> <div class="pagination_box"> <div id="pagination" class="pagination"></div> </div> </div> <!-- 新增弹窗 --> <div class="windows_box"> <div class="paddle_box"> <p class="title">添加/编辑</p> <div class="form-item"> <p>*产品名称</p> <input type="text" name="productName" id="productName" placeholder="请输入内容名称"> </div> <div class="form-item"> <p>*产品图片</p> <div class="img_pop"> <form id="myForm"> <label> <input type="file" name="file" id="file" style="display: none;" onchange="upload(true)" accept="image/*"> <img src="./img/10050.png" id="pic"> </label> </form> <p class="error" id="img_error">图片</p> </div> </div> <div class="form-item"> <p>*产品介绍</p> <div class="described"> <div class="texta" id="editor"></div> <p class="error" id="text_error">详情</p> </div> </div> <div id="parcel" class="parcel"> <div class="radio-group"> <p>*上下架</p> <input type="radio" name="shelf" value="on" checked> <span>上架</span> <input type="radio" name="shelf" value="off"> <span>不需要</span> </div> <!-- 操作按钮 --> <div class="hint_button"> <button class="hint_right" onclick="zxc()">确认</button> <button class="hint_left" onclick="asd()">确定并继续添加</button> </div> </div> </div> </div> <!-- 删除弹窗 --> <div class="delete-mask" id="deleteMask"> <div class="delete-dialog" id="deleteDialog"> <div class="dialog-content" id="dialogContent"> <h3>确定要删除选中的行业吗?</h3> </div> <div class="dialog-actions" id="dialogActions"> <button class="action-cancel" id="cancelBtn" onclick="cancelDelete()">取消</button> <button class="action-confirm" id="confirmBtn" onclick="confirmDelete()">确定</button> </div> </div> </div> <!-- 失败 --> <div id="error"> <div class="errorCenter"> <img id="errorImg" src="./img/error.png" alt="" /> <span class="errorText"></span> </div> </div> <!-- 成功 --> <div id="success"> <div class="successCenter"> <img id="successImg" src="./img/yes.png" alt="" /> <span class="successText"></span> </div> </div> <script src="./js/public.js"></script> <script src="./js/goodschild.js"></script> <!-- 分页插件 --> <script src="./js/myPagination.js"></script> <!-- 富文本编辑器容器 --> <div class="texta" id="editor"></div> <script> // 初始化 UEditor 富文本编辑器 var ue = UE.getEditor('editor', { initialContent: '<span style="color: #999">请输入内容...</span>', // 初始占位符文本 autoHeight: false, }); // 监听编辑器准备就绪事件,为聚焦、失焦绑定占位符处理逻辑 ue.ready(() => { // 聚焦时,若内容是初始占位符则清空 ue.addListener('focus', () => { if (ue.getContent() === '<p><span style="color: #999">请输入内容...</span></p>') { ue.setContent(''); } }); // 失焦时,若内容为空或仅含换行则恢复占位符 ue.addListener('blur', () => { if (ue.getContent() === '' || ue.getContent() === '<p><br></p>') { ue.setContent('<span style="color: #999">请输入内容...</span>'); } }); }); </script> </body>
08-13
import { useRef, useState, useEffect } from 'react'; import { Typography, Alert, Table, Pagination, Form, Radio, message, Row, Col, DatePicker, Input, Tabs, Button, Drawer, FloatButton, Tooltip, Spin, Tag, } from 'antd'; import { UploadOutlined, FilterOutlined } from '@ant-design/icons'; import FileUploadPanel from './components/FileUploadPanel'; import EventListModal from './components/EventListModal'; import DiffListModal from './components/DiffListModal'; import SummaryAnalysis from './components/SummaryAnalysis'; import useJsonProcessor from './hooks/useJsonProcessor'; // 替换为 useJsonProcessor import { playOptions, booleanOption, userOption } from './data'; import './index.css'; const { RangePicker } = DatePicker; const { Paragraph } = Typography; const { TabPane } = Tabs; const UnifiedJsonMerger = () => { const fileInputRef = useRef(null); const resultRef = useRef(null); const [form] = Form.useForm(); const [filters, setFilters] = useState({}); const [drawerOpen, setDrawerOpen] = useState(false); // 文件管理抽屉 const [filterDrawerOpen, setFilterDrawerOpen] = useState(false); // 过滤抽屉(左) const [lastParseCompleted, setLastParseCompleted] = useState(false); const [activeTab, setActiveTab] = useState('raw-data'); const [eventModalVisible, setEventModalVisible] = useState(false); const [currentEvents, setCurrentEvents] = useState([]); const [diffModalVisible, setDiffModalVisible] = useState(false); const [currentDiffs, setCurrentDiffs] = useState([]); // 打开事件模态框 const showEventModal = events => { setCurrentEvents(events); setEventModalVisible(true); }; // 打开耗时详情模态框 const showDiffModal = playEpList => { setCurrentDiffs(playEpList); setDiffModalVisible(true); }; const { processedData, selectedFiles, addFiles, removeFile, isParsing, error, totalRows, parseAllFiles: originalParseAllFiles, pagination, handleChangePage, handleChangeRowsPerPage, getTableData, } = useJsonProcessor(); // 包装解析函数:防止重复提交 const parseAllFiles = async () => { if (isParsing) return; setLastParseCompleted(false); await originalParseAllFiles(); }; // 解析完成后关闭抽屉并提示 useEffect(() => { if (totalRows > 0 && !lastParseCompleted) { setDrawerOpen(false); setLastParseCompleted(true); message.success(`解析完成,共加载 ${totalRows} 行原始数据`); } }, [totalRows, lastParseCompleted]); const initialFilterValues = { playType: 'all', isPreConnect: 'all', isValid: 'all', isPerceived: 'all', isError: 'all', dateRange: undefined, targetPlayId: '', targetDevId: '', targetKeyword: '', }; // 设置表单初始值 useEffect(() => { form.setFieldsValue(initialFilterValues); }, []); // 监听表单变化并更新 filters const allFormValues = Form.useWatch([], form); useEffect(() => { if (allFormValues) { const newFilters = Object.fromEntries( Object.entries(allFormValues).filter(([_, value]) => { if (value === null || value === undefined) return false; if (typeof value === 'string' && value.trim() === '') return false; if (Array.isArray(value) && value.length === 0) return false; return true; }) ); setFilters(prev => { if (JSON.stringify(prev) === JSON.stringify(newFilters)) { return prev; } return newFilters; }); handleChangePage(1); // 筛选变更回到第一页 } }, [allFormValues, handleChangePage]); // 获取过滤后数据 const currentData = getTableData(filters); const totalCount = currentData.length; // 动态生成列 const getColumns = data => { if (data.length === 0) return []; const sample = data.find(item => typeof item === 'object' && item !== null); if (!sample) return []; return Object.keys(sample).map(key => ({ title: key, dataIndex: key, key, render: text => { // epList 列 if (key === 'epList' && Array.isArray(text)) { return ( <Button type="link" size="small" onClick={e => { e.stopPropagation(); showEventModal(text); }} > {text.length} 个埋点事件 </Button> ); } // playEpList 列 if (key === 'playEpList' && Array.isArray(text)) { return ( <Button type="link" size="small" onClick={e => { e.stopPropagation(); showDiffModal(text); }} > {text.length} 条起播记录 </Button> ); } // playId 和 devId 截断显示 + tooltip if ((key === 'playId' || key === 'devId') && typeof text === 'string') { const displayText = text.length <= 6 ? text : `...${text.slice(-6)}`; return ( <Tooltip title={text}> <span style={{ fontFamily: 'monospace', cursor: 'help' }}> {displayText} </span> </Tooltip> ); } // 布尔字段美化 if ( [ 'validStatus', 'isUserPerceivedSuccess', 'isUserPerceivedFailure', ].includes(key) ) { if (typeof text === 'boolean') { return text ? ( <Tag color="green">是</Tag> ) : ( <Tag color="red">否</Tag> ); } return '-'; } // 数组渲染(非对象数组用逗号分隔,对象数组用 JSON 格式展示) if (Array.isArray(text)) { if (text.length === 0) return <span className="array-empty">-</span>; const isObjectArray = text.some( item => typeof item === 'object' && item !== null ); if (isObjectArray) { const items = text.map(item => JSON.stringify(item, null, 2)); return ( <div style={{ whiteSpace: 'pre-wrap', fontSize: '12px', fontFamily: 'monospace', }} > {items.join(',\n')} </div> ); } else { return String(text.join(', ')); } } // null/undefined 显示 - if (text == null) return '-'; const str = String(text).trim(); return str ? str : '-'; }, })); }; const columns = getColumns(currentData); // 分页数据 const previewData = currentData.slice( (pagination.current - 1) * pagination.pageSize, pagination.current * pagination.pageSize ); return ( <> {/* 浮动上传按钮 */} <FloatButton icon={<UploadOutlined />} tooltip="管理 JSON 文件" onClick={() => setDrawerOpen(true)} style={{ right: 40, bottom: 40 }} /> {/* 右侧抽屉:文件上传与管理 */} <Drawer title="管理 JSON 文件" placement="right" width={480} onClose={() => setDrawerOpen(false)} open={drawerOpen} destroyOnClose={false} extra={ <Button type="primary" onClick={parseAllFiles} disabled={isParsing || selectedFiles.length === 0} > {isParsing ? '解析中...' : '开始解析'} </Button> } > <FileUploadPanel fileInputRef={fileInputRef} selectedFiles={selectedFiles} isParsing={isParsing} onAddFiles={() => { const files = fileInputRef.current?.files; if (files?.length) { const jsonFiles = Array.from(files).filter(f => f.name.endsWith('.json') ); if (jsonFiles.length > 0) { addFiles(jsonFiles); message.success(`已添加 ${jsonFiles.length} 个 JSON 文件`); } else { message.warning('请选择 .json 文件'); } if (fileInputRef.current) fileInputRef.current.value = ''; } }} onRemoveFile={removeFile} onParseAllFiles={parseAllFiles} /> </Drawer> {/* 错误提示 */} {error && ( <Alert message="解析出错" description={error} type="error" showIcon style={{ margin: '24px' }} /> )} {/* 引导页:尚未上传任何数据 */} {totalRows === 0 && !isParsing && ( <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '70vh', textAlign: 'center', color: '#666', padding: '24px', }} > <UploadOutlined style={{ fontSize: '48px', color: '#1890ff', marginBottom: '16px' }} /> <Typography.Title level={3} style={{ color: '#1890ff' }}> 欢迎使用 JSON 数据分析工具 </Typography.Title> <Paragraph style={{ fontSize: '16px', maxWidth: '500px' }}> 请上传您的 JSON 文件并点击“开始解析”,我们将为您加载和分析数据。 </Paragraph> <Button type="primary" icon={<UploadOutlined />} size="large" onClick={() => setDrawerOpen(true)} > 上传 JSON 文件 </Button> </div> )} {/* 主界面:正在解析或已有数据 */} <Spin spinning={isParsing} tip="解析中,请稍候..." size="large"> {(totalRows > 0 || isParsing) && ( <div style={{ padding: '0 24px' }}> <Tabs activeKey={activeTab} onChange={setActiveTab}> {/* Tab 1: 数据列表 */} <TabPane tab="数据列表" key="raw-data"> <div ref={resultRef}> <EventListModal visible={eventModalVisible} events={currentEvents} onClose={() => setEventModalVisible(false)} /> <DiffListModal visible={diffModalVisible} playEpList={currentDiffs} onClose={() => setDiffModalVisible(false)} /> {/* 筛选按钮 */} <div style={{ marginBottom: '16px' }}> <Button type="primary" icon={<FilterOutlined />} onClick={() => setFilterDrawerOpen(true)} disabled={isParsing} > 筛选条件 </Button> </div> {/* 表格区域 */} <div style={{ position: 'relative', minHeight: 200 }}> {!isParsing && ( <> {columns.length > 0 ? ( <> <Table columns={columns} dataSource={previewData.map((item, idx) => ({ ...item, key: idx, }))} pagination={false} scroll={{ x: true }} size="small" bordered style={{ marginBottom: '16px' }} /> <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '24px', }} > <Pagination current={pagination.current} pageSize={pagination.pageSize} total={totalCount} pageSizeOptions={['100', '200', '500']} showSizeChanger onChange={(page, pageSize) => { handleChangePage(page); if (pageSize !== pagination.pageSize) { handleChangeRowsPerPage(pageSize); } }} showTotal={(total, range) => `${range[0]}-${range[1]} 条,共 ${total} 条` } /> </div> </> ) : ( <div style={{ textAlign: 'center', padding: '32px', color: '#999', backgroundColor: '#fafafa', borderRadius: '4px', }} > 暂无符合条件的数据 </div> )} </> )} </div> </div> </TabPane> {/* Tab 2: 总结分析 */} <TabPane tab="总结分析" key="summary"> <SummaryAnalysis data={processedData} /> </TabPane> </Tabs> {/* 左侧抽屉:过滤表单 */} <Drawer title="数据过滤" placement="left" width={400} onClose={() => setFilterDrawerOpen(false)} open={filterDrawerOpen} extra={ <Button size="small" onClick={() => { form.resetFields(); message.info('已重置筛选条件'); }} > 重置 </Button> } > <Form form={form} layout="horizontal" className="compact-form" size="middle" > <Row gutter={[16, 16]}> <Col span={24}> <Form.Item name="playType" label="播放类型" initialValue="all" > <Radio.Group options={playOptions} buttonStyle="solid" /> </Form.Item> </Col> <Col span={24}> <Form.Item name="isPreConnect" label="命中预连接" initialValue="all" > <Radio.Group options={booleanOption} buttonStyle="solid" /> </Form.Item> </Col> <Col span={24}> <Form.Item name="isValid" label="是否有效" initialValue="all" > <Radio.Group options={booleanOption} buttonStyle="solid" /> </Form.Item> </Col> <Col span={24}> <Form.Item name="isPerceived" label="用户" initialValue="all" > <Radio.Group options={userOption} buttonStyle="solid" /> </Form.Item> </Col> <Col span={24}> <Form.Item name="isError" label="是否存在报错" initialValue="all" > <Radio.Group options={booleanOption} buttonStyle="solid" /> </Form.Item> </Col> <Col span={24}> <Form.Item name="dateRange" label="日期范围"> <RangePicker format="YYYY-MM-DD" allowClear style={{ width: '100%' }} /> </Form.Item> </Col> <Col span={24}> <Form.Item name="targetPlayId" label="目标playId"> <Input placeholder="请输入 playId" allowClear /> </Form.Item> </Col> <Col span={24}> <Form.Item name="targetDevId" label="目标devId"> <Input placeholder="请输入 devId" allowClear /> </Form.Item> </Col> <Col span={24}> <Form.Item name="targetKeyword" label="关键字模糊查询"> <Input placeholder="请输入关键字" allowClear /> </Form.Item> </Col> </Row> </Form> </Drawer> </div> )} </Spin> </> ); }; export default UnifiedJsonMerger; 这个怎么改
最新发布
01-08
import { useRef, useState, useEffect } from 'react'; import { Typography, Alert, Table, Pagination, Form, Radio, message, Row, Col, DatePicker, Input, Tabs, Button, Drawer, FloatButton, Tooltip, Spin, Tag, } from 'antd'; import { UploadOutlined, FilterOutlined } from '@ant-design/icons'; import FileUploadPanel from './components/FileUploadPanel'; import EventListModal from './components/EventListModal'; import DiffListModal from './components/DiffListModal'; import SummaryAnalysis from './components/SummaryAnalysis'; import useCsvProcessor from './hooks/useCsvProcessor'; import { playOptions, booleanOption, userOption } from './data'; import './index.css'; const { RangePicker } = DatePicker; const { Paragraph } = Typography; const { TabPane } = Tabs; const UnifiedCsvMerger = () => { const fileInputRef = useRef(null); const resultRef = useRef(null); const [form] = Form.useForm(); const [filters, setFilters] = useState({}); const [drawerOpen, setDrawerOpen] = useState(false); // 文件管理抽屉 const [filterDrawerOpen, setFilterDrawerOpen] = useState(false); // 过滤抽屉(左) const [lastParseCompleted, setLastParseCompleted] = useState(false); const [activeTab, setActiveTab] = useState('raw-data'); const [eventModalVisible, setEventModalVisible] = useState(false); const [currentEvents, setCurrentEvents] = useState([]); const [diffModalVisible, setDiffModalVisible] = useState(false); // 耗时详情模态框 const [currentDiffs, setCurrentDiffs] = useState([]); // 打开事件模态框(用于 epList) const showEventModal = events => { setCurrentEvents(events); setEventModalVisible(true); }; // 打开耗时详情模态框(用于 playEpList) const showDiffModal = playEpList => { setCurrentDiffs(playEpList); setDiffModalVisible(true); }; const { processedData, selectedFiles, addFiles, removeFile, isParsing, error, totalRows, parseAllFiles: originalParseAllFiles, pagination, handleChangePage, handleChangeRowsPerPage, getTableData, } = useCsvProcessor(); // 包装解析函数:防重复提交 const parseAllFiles = async () => { if (isParsing) return; // 防止重复触发 setLastParseCompleted(false); await originalParseAllFiles(); }; // 监听 totalRows 自动关闭抽屉 useEffect(() => { if (totalRows > 0 && !lastParseCompleted) { setDrawerOpen(false); setLastParseCompleted(true); message.success(`解析完成,共加载 ${totalRows} 行原始数据`); } }, [totalRows, lastParseCompleted]); const initialFilterValues = { playType: 'all', isPreConnect: 'all', isValid: 'all', isPerceived: 'all', isError: 'all', dateRange: undefined, targetPlayId: '', targetDevId: '', targetKeyword: '', }; // 第一次挂载时:设置表单初始值,激活 useWatch useEffect(() => { form.setFieldsValue(initialFilterValues); }, []); // 监听所有表单值变化(包括初始化后) const allFormValues = Form.useWatch([], form); useEffect(() => { if (allFormValues) { const newFilters = Object.fromEntries( Object.entries(allFormValues).filter(([_, value]) => { if (value === null || value === undefined) return false; if (typeof value === 'string' && value.trim() === '') return false; if (Array.isArray(value) && value.length === 0) return false; return true; }) ); setFilters(prev => { // 防止重复设置相同对象导致不必要重渲染 if (JSON.stringify(prev) === JSON.stringify(newFilters)) { return prev; } return newFilters; }); handleChangePage(1); // 更改筛选条件时回到第一页 } }, [allFormValues, handleChangePage]); // 获取当前过滤后的数据 const currentData = getTableData(filters); const totalCount = currentData.length; // 动态生成列 const getColumns = data => { return data.length > 0 ? Object.keys(data[0]).map(key => ({ title: key, dataIndex: key, key, render: text => { // 处理 epList 列 if (key === 'epList' && Array.isArray(text)) { return ( <Button type="link" size="small" onClick={e => { e.stopPropagation(); showEventModal(text); }} > {text.length} 个埋点事件 </Button> ); } // 处理 playEpList 列 if (key === 'playEpList' && Array.isArray(text)) { return ( <Button type="link" size="small" onClick={e => { e.stopPropagation(); showDiffModal(text); }} > {text.length} 条起播记录 </Button> ); } // playId 和 devId 截断 + tooltip if ( (key === 'playId' || key === 'devId') && typeof text === 'string' ) { const displayText = text.length <= 6 ? text : `...${text.slice(-6)}`; return ( <Tooltip title={text}> <span style={{ fontFamily: 'monospace', cursor: 'help' }}> {displayText} </span> </Tooltip> ); } // 布尔字段美化 if ( [ 'validStatus', 'isUserPerceivedSuccess', 'isUserPerceivedFailure', ].includes(key) ) { if (typeof text === 'boolean') { return text ? ( <Tag color="green">是</Tag> ) : ( <Tag color="red">否</Tag> ); } return '-'; } // 数组处理(非对象数组) if (Array.isArray(text)) { if (text.length === 0) return <span className="array-empty">-</span>; const isObjectArray = text.some( item => typeof item === 'object' && item !== null ); if (isObjectArray) { const items = text.map(item => JSON.stringify(item, null, 2)); return ( <div style={{ whiteSpace: 'pre-wrap', fontSize: '12px', fontFamily: 'monospace', }} > {items.join(',\n')} </div> ); } else { return String(text.join(', ')); } } // null/undefined 显示为 - if (text == null) return '-'; const str = String(text).trim(); return str ? str : '-'; }, })) : []; }; const columns = getColumns(currentData); // 分页展示数据 const previewData = currentData.slice( (pagination.current - 1) * pagination.pageSize, pagination.current * pagination.pageSize ); return ( <> {/* 浮动上传按钮 */} <FloatButton icon={<UploadOutlined />} tooltip="管理 CSV 文件" onClick={() => setDrawerOpen(true)} style={{ right: 40, bottom: 40 }} /> {/* 右侧抽屉:文件上传与管理 */} <Drawer title="管理 CSV 文件" placement="right" width={480} onClose={() => setDrawerOpen(false)} open={drawerOpen} destroyOnClose={false} extra={ <Button type="primary" onClick={parseAllFiles} disabled={isParsing || selectedFiles.length === 0} > {isParsing ? '解析中...' : '开始解析'} </Button> } > <FileUploadPanel fileInputRef={fileInputRef} selectedFiles={selectedFiles} isParsing={isParsing} onAddFiles={() => { const files = fileInputRef.current?.files; if (files?.length) { const csvFiles = Array.from(files).filter(f => f.name.endsWith('.csv') ); if (csvFiles.length > 0) { addFiles(csvFiles); message.success(`已添加 ${csvFiles.length} 个 CSV 文件`); } else { message.warning('未检测到有效的 CSV 文件'); } } if (fileInputRef.current) fileInputRef.current.value = ''; }} onRemoveFile={removeFile} onParseAllFiles={parseAllFiles} /> </Drawer> {/* 错误提示 */} {error && ( <Alert message="解析出错" description={error} type="error" showIcon style={{ margin: '24px' }} /> )} {/* 引导页:尚未解析任何数据 */} {totalRows === 0 && !isParsing && ( <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '70vh', textAlign: 'center', color: '#666', padding: '24px', }} > <UploadOutlined style={{ fontSize: '48px', color: '#1890ff', marginBottom: '16px' }} /> <Typography.Title level={3} style={{ color: '#1890ff' }}> 欢迎使用新埋点生产环境数据分析工具 </Typography.Title> <Paragraph style={{ fontSize: '16px', maxWidth: '500px' }}> 请上传您的 CSV 文件并点击“开始解析”,我们将为您合并和分析数据。 </Paragraph> <Button type="primary" icon={<UploadOutlined />} size="large" onClick={() => setDrawerOpen(true)} > 上传并解析文件 </Button> </div> )} {/* 主界面:已解析出数据 或 正在解析 */} <Spin spinning={isParsing} tip="解析中,请稍候..." size="large"> {(totalRows > 0 || isParsing) && ( <div style={{ padding: '0 24px' }}> <Tabs defaultActiveKey="raw-data" activeKey={activeTab} onChange={setActiveTab} > {/* Tab 1: 数据列表 */} <TabPane tab="数据列表" key="raw-data"> <div ref={resultRef}> {/* 模态框 */} <EventListModal visible={eventModalVisible} events={currentEvents} onClose={() => setEventModalVisible(false)} /> <DiffListModal visible={diffModalVisible} playEpList={currentDiffs} onClose={() => setDiffModalVisible(false)} /> {/* 筛选按钮 */} <div style={{ marginBottom: '16px' }}> <Button type="primary" icon={<FilterOutlined />} onClick={() => setFilterDrawerOpen(true)} disabled={isParsing} > 筛选条件 </Button> </div> {/* 表格区域 + Loading 覆盖 */} <div style={{ position: 'relative', minHeight: 200 }}> {!isParsing && ( <> {columns.length > 0 ? ( <> <Table columns={columns} dataSource={previewData.map((item, idx) => ({ ...item, key: idx, }))} pagination={false} scroll={{ x: true }} size="small" bordered style={{ marginBottom: '16px' }} /> <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '24px', }} > <Pagination current={pagination.current} pageSize={pagination.pageSize} total={totalCount} pageSizeOptions={['100', '200', '500']} showSizeChanger onChange={(page, pageSize) => { handleChangePage(page); if (pageSize !== pagination.pageSize) { handleChangeRowsPerPage(pageSize); } }} showTotal={(total, range) => `${range[0]}-${range[1]} 条,共 ${total} 条` } /> </div> </> ) : ( <div style={{ textAlign: 'center', padding: '32px', color: '#999', backgroundColor: '#fafafa', borderRadius: '4px', }} > 暂无符合条件的数据 </div> )} </> )} </div> </div> </TabPane> {/* Tab 2: 总结分析 */} <TabPane tab="总结分析" key="summary"> <SummaryAnalysis data={processedData} /> </TabPane> </Tabs> {/* 左侧抽屉:过滤表单 */} <Drawer title="数据过滤" placement="left" width={400} onClose={() => setFilterDrawerOpen(false)} open={filterDrawerOpen} extra={ <Button size="small" onClick={() => { form.resetFields(); message.info('已重置筛选条件'); }} > 重置 </Button> } > <Form form={form} layout="horizontal" className="compact-form" size="middle" > <Row gutter={[16, 16]}> <Col span={24}> <Form.Item name="playType" label="播放类型" initialValue="all" > <Radio.Group options={playOptions} buttonStyle="solid" /> </Form.Item> </Col> <Col span={24}> <Form.Item name="isPreConnect" label="命中预连接" initialValue="all" > <Radio.Group options={booleanOption} buttonStyle="solid" /> </Form.Item> </Col> <Col span={24}> <Form.Item name="isValid" label="是否有效" initialValue="all" > <Radio.Group options={booleanOption} buttonStyle="solid" /> </Form.Item> </Col> <Col span={24}> <Form.Item name="isPerceived" label="用户" initialValue="all" > <Radio.Group options={userOption} buttonStyle="solid" /> </Form.Item> </Col> <Col span={24}> <Form.Item name="isError" label="是否存在报错" initialValue="all" > <Radio.Group options={booleanOption} buttonStyle="solid" /> </Form.Item> </Col> <Col span={24}> <Form.Item name="dateRange" label="日期范围"> <RangePicker format="YYYY-MM-DD" allowClear style={{ width: '100%' }} /> </Form.Item> </Col> <Col span={24}> <Form.Item name="targetPlayId" label="目标playId"> <Input placeholder="请输入 playId" allowClear /> </Form.Item> </Col> <Col span={24}> <Form.Item name="targetDevId" label="目标devId"> <Input placeholder="请输入 devId" allowClear /> </Form.Item> </Col> <Col span={24}> <Form.Item name="targetKeyword" label="关键字模糊查询"> <Input placeholder="请输入关键字" allowClear /> </Form.Item> </Col> </Row> </Form> </Drawer> </div> )} </Spin> </> ); }; export default UnifiedCsvMerger; 先在上传的是json文件,哪些地方需要修改
01-08
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值