ResultTable.jsx 5.94 KB
import React, { useState, useEffect } from 'react'
import { Table, Input, Button, Pagination, Space, message, Empty, Tag, Modal, Form } from 'antd'
import { SearchOutlined, DownloadOutlined } from '@ant-design/icons'
import request from '../utils/request'

export default function ResultTable({ queryId, totalCount, columns, duration }) {
  const [data, setData] = useState([])
  const [loading, setLoading] = useState(false)
  const [page, setPage] = useState(1)
  const [pageSize, setPageSize] = useState(20)
  const [total, setTotal] = useState(totalCount || 0)
  const [keyword, setKeyword] = useState('')
  const [currentColumns, setCurrentColumns] = useState(columns || [])

  useEffect(() => {
    if (queryId) {
      setPage(1)
      setKeyword('')
      setTotal(totalCount || 0)
      setCurrentColumns(columns || [])
      fetchPage(1, pageSize, '')
    }
  }, [queryId])

  const fetchPage = async (p, ps, kw) => {
    if (!queryId) return
    setLoading(true)
    try {
      const params = `page=${p}&pageSize=${ps}${kw ? `&keyword=${encodeURIComponent(kw)}` : ''}`
      const res = await request.get(`/query/page/${queryId}?${params}`)
      if (res.success) {
        setData(res.data.rows)
        setTotal(res.data.totalCount)
        if (res.data.columns && res.data.columns.length > 0) {
          setCurrentColumns(res.data.columns)
        }
      } else {
        message.error(res.message)
      }
    } finally {
      setLoading(false)
    }
  }

  const handlePageChange = (p, ps) => {
    setPage(p)
    setPageSize(ps)
    fetchPage(p, ps, keyword)
  }

  const handleSearch = () => {
    setPage(1)
    fetchPage(1, pageSize, keyword)
  }

  const [exportModalOpen, setExportModalOpen] = useState(false)
  const [exportForm] = Form.useForm()

  const handleExportClick = () => {
    if (!queryId) return
    exportForm.setFieldsValue({ fileName: '' })
    setExportModalOpen(true)
  }

  const handleExportConfirm = () => {
    exportForm.validateFields().then(values => {
      const fileName = (values.fileName || '').trim()
      const kw = keyword ? `&keyword=${encodeURIComponent(keyword)}` : ''
      const nameParam = fileName ? `&fileName=${encodeURIComponent(fileName)}` : ''
      window.open(`/api/export/excel/${queryId}?${kw}${nameParam}`)
      setExportModalOpen(false)
    })
  }

  const handleExportOld = () => {
    if (!queryId) return
    const kw = keyword ? `?keyword=${encodeURIComponent(keyword)}` : ''
    window.open(`/api/export/excel/${queryId}${kw}`)
  }

  const tableColumns = currentColumns.map(col => ({
    title: col,
    dataIndex: col,
    key: col,
    ellipsis: true,
    width: 150,
    render: (val) => {
      if (val === null) return <span style={{ color: '#ccc' }}>NULL</span>
      if (val === undefined) return ''
      if (typeof val === 'object') return JSON.stringify(val)
      // Date对象
      if (val instanceof Date) return val.toISOString().replace('T', ' ').replace(/\.\d+Z$/, '')
      // 长文本截断
      const str = String(val)
      return str.length > 100 ? str.substring(0, 100) + '...' : str
    },
  }))

  if (!queryId) {
    return (
      <div className="result-section">
        <div className="panel-title">查询结果</div>
        <Empty description="执行SQL后将在此显示结果" style={{ marginTop: 60 }} />
      </div>
    )
  }

  return (
    <div className="result-section">
      <div className="result-toolbar">
        <div className="left-actions">
          <span style={{ fontSize: 13, color: '#666' }}>
            查询结果
            <Tag color="blue" style={{ marginLeft: 8 }}>{total}</Tag>
            {duration != null && (
              <Tag color="default" style={{ marginLeft: 4 }}>{(duration / 1000).toFixed(2)}s</Tag>
            )}
          </span>
        </div>
        <div className="right-actions">
          <Input
            size="small"
            placeholder="结果内搜索..."
            value={keyword}
            onChange={e => setKeyword(e.target.value)}
            onPressEnter={handleSearch}
            style={{ width: 180 }}
            prefix={<SearchOutlined />}
            allowClear
          />
          <Button size="small" onClick={handleSearch} icon={<SearchOutlined />}>
            搜索
          </Button>
          <Button
            size="small"
            type="primary"
            onClick={handleExportClick}
            icon={<DownloadOutlined />}
          >
            导出Excel
          </Button>
        </div>
      </div>

      <div className="result-table-wrapper">
        <Table
          dataSource={data}
          columns={tableColumns}
          rowKey={(_, i) => i}
          loading={loading}
          size="small"
          pagination={false}
          scroll={{ x: 'max-content', y: 'calc(100vh - 340px)' }}
          bordered
        />
      </div>

      <div className="result-pagination">
        <Pagination
          current={page}
          pageSize={pageSize}
          total={total}
          onChange={handlePageChange}
          showSizeChanger
          showQuickJumper
          pageSizeOptions={['10', '20', '50', '100', '200']}
          showTotal={t => `共 ${t} 条`}
          size="small"
        />
      </div>
      {/* 导出文件名弹窗 */}
      <Modal
        title="导出Excel"
        open={exportModalOpen}
        onCancel={() => setExportModalOpen(false)}
        onOk={handleExportConfirm}
        okText="导出"
        cancelText="取消"
        width={440}
      >
        <Form form={exportForm} layout="vertical" style={{ marginTop: 16 }}>
          <Form.Item
            name="fileName"
            label="文件名"
            rules={[{ required: true, message: '请输入导出文件名' }]}
          >
            <Input placeholder="请输入导出的文件名(不含扩展名)" autoFocus />
          </Form.Item>
          <p style={{ fontSize: 12, color: '#999', marginTop: -8 }}>
            将导出为 .xlsx 格式,共 {total} 条数据
          </p>
        </Form>
      </Modal>
    </div>
  )
}