ConditionPanel.jsx 11.1 KB
import React, { useState, useRef } from 'react'
import { Modal, Button, Select, Input, DatePicker, Tag, Space, Upload, message, Popconfirm, Empty } from 'antd'
import { UploadOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons'
import dayjs from 'dayjs'

const { RangePicker } = DatePicker

export const CONDITION_TYPES = [
  { value: 'exact', label: '精准匹配' },
  { value: 'fuzzy', label: '模糊查询' },
  { value: 'daterange', label: '日期区间' },
  { value: 'datemonth', label: '日期按月' },
  { value: 'dateyear', label: '日期按年' },
  { value: 'dateday', label: '日期按天' },
  { value: 'dictionary', label: '字典下拉' },
  { value: 'excel_import', label: 'Excel导入(IN)' },
]

/**
 * 从SQL中提取 {{变量名}} 占位符
 */
export function extractVariables(sql) {
  const regex = /\{\{(\w+)\}\}/g
  const vars = []
  let match
  while ((match = regex.exec(sql)) !== null) {
    if (!vars.includes(match[1])) {
      vars.push(match[1])
    }
  }
  return vars
}

/**
 * 根据条件配置,将SQL中的占位符替换为实际值
 */
export function buildSql(sql, conditions) {
  let result = sql
  for (const cond of conditions) {
    const placeholder = `{{${cond.name}}}`
    const replacement = buildReplacement(cond)
    result = result.replaceAll(placeholder, replacement)
  }
  return result
}

function buildReplacement(cond) {
  const { type, value, valueStart, valueEnd, importedValues, dictOptions } = cond

  switch (type) {
    case 'exact':
      return value ? `'${value}'` : "''"
    case 'fuzzy':
      return value ? `'%${value}%'` : "'%%'"
    case 'daterange':
      return valueStart ? `'${dayjs(valueStart).format('YYYY-MM-DD')}'` : "'2024-01-01'"
    case 'datemonth':
      return value ? `'${dayjs(value).format('YYYY-MM')}'` : "'2024-01'"
    case 'dateyear':
      return value ? `'${dayjs(value).format('YYYY')}'` : "'2024'"
    case 'dateday':
      return value ? `'${dayjs(value).format('YYYY-MM-DD')}'` : "'2024-01-01'"
    case 'dictionary':
      return value ? `'${value}'` : "''"
    case 'excel_import':
      if (importedValues && importedValues.length > 0) {
        const quoted = importedValues.map(v => `'${v}'`).join(',')
        return quoted
      }
      return "''"
    default:
      return value ? `'${value}'` : "''"
  }
}

/**
 * 处理日期区间中的结束日期占位符
 * 约定:{{end_xxx}} 对应 xxx 的日期区间结束值
 */
export function buildSqlWithDateRange(sql, conditions) {
  let result = sql
  for (const cond of conditions) {
    const placeholder = `{{${cond.name}}}`
    const replacement = buildReplacement(cond)
    result = result.replaceAll(placeholder, replacement)

    // 处理日期区间结束占位符 {{end_xxx}}
    if (cond.type === 'daterange' && cond.valueEnd) {
      const endPlaceholder = `{{end_${cond.name}}}`
      const endReplacement = `'${dayjs(cond.valueEnd).format('YYYY-MM-DD')}'`
      result = result.replaceAll(endPlaceholder, endReplacement)
    }
  }
  return result
}

/**
 * 解析SQL变量并智能推断类型
 */
export function parseVariables(sqlText, existingConditions) {
  const vars = extractVariables(sqlText)
  if (vars.length === 0) return []

  return vars.map(name => {
    const existing = existingConditions.find(c => c.name === name)
    if (existing) return existing

    let type = 'exact'
    const lower = name.toLowerCase()
    if (lower.includes('name') || lower.includes('名称') || lower.includes('名')) {
      type = 'fuzzy'
    } else if (lower.includes('date') || lower.includes('日期') || lower.includes('时间') || lower.includes('time')) {
      type = 'dateday'
    } else if (lower.includes('month') || lower.includes('月')) {
      type = 'datemonth'
    } else if (lower.includes('year') || lower.includes('年')) {
      type = 'dateyear'
    }

    return { name, type, value: '', valueStart: '', valueEnd: '', importedValues: [], dictOptions: [] }
  })
}

export default function ConditionModal({ open, onClose, conditions, onConditionsChange, sqlText, onParse }) {
  const updateCondition = (index, updates) => {
    const newConds = [...conditions]
    newConds[index] = { ...newConds[index], ...updates }
    onConditionsChange(newConds)
  }

  const handleExcelImport = (index, file) => {
    const reader = new FileReader()
    reader.onload = async (e) => {
      const base64 = e.target.result.split(',')[1]
      try {
        const response = await fetch('/api/export/import', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ fileData: base64, fileName: file.name }),
        })
        const res = await response.json()
        if (res.success) {
          updateCondition(index, { importedValues: res.data.values })
          message.success(`成功导入 ${res.data.count} 条数据`)
        } else {
          message.error(res.message)
        }
      } catch (err) {
        message.error('导入失败: ' + err.message)
      }
    }
    reader.readAsDataURL(file)
    return false
  }

  const renderValueInput = (cond, index) => {
    switch (cond.type) {
      case 'exact':
        return (
          <Input
            size="small"
            placeholder="请输入值"
            value={cond.value}
            onChange={e => updateCondition(index, { value: e.target.value })}
            style={{ width: 200 }}
          />
        )
      case 'fuzzy':
        return (
          <Input
            size="small"
            placeholder="模糊关键词"
            value={cond.value}
            onChange={e => updateCondition(index, { value: e.target.value })}
            style={{ width: 200 }}
          />
        )
      case 'daterange':
        return (
          <RangePicker
            size="small"
            value={cond.valueStart && cond.valueEnd ? [dayjs(cond.valueStart), dayjs(cond.valueEnd)] : null}
            onChange={dates => {
              if (dates) {
                updateCondition(index, { valueStart: dates[0], valueEnd: dates[1] })
              } else {
                updateCondition(index, { valueStart: '', valueEnd: '' })
              }
            }}
            style={{ width: 260 }}
          />
        )
      case 'datemonth':
        return (
          <DatePicker
            size="small"
            picker="month"
            value={cond.value ? dayjs(cond.value) : null}
            onChange={d => updateCondition(index, { value: d })}
            style={{ width: 200 }}
          />
        )
      case 'dateyear':
        return (
          <DatePicker
            size="small"
            picker="year"
            value={cond.value ? dayjs(cond.value) : null}
            onChange={d => updateCondition(index, { value: d })}
            style={{ width: 200 }}
          />
        )
      case 'dateday':
        return (
          <DatePicker
            size="small"
            value={cond.value ? dayjs(cond.value) : null}
            onChange={d => updateCondition(index, { value: d })}
            style={{ width: 200 }}
          />
        )
      case 'dictionary':
        return (
          <Space direction="vertical" size={4} style={{ width: 240 }}>
            <Select
              size="small"
              placeholder="请选择"
              value={cond.value || undefined}
              onChange={v => updateCondition(index, { value: v })}
              options={cond.dictOptions || []}
              style={{ width: '100%' }}
              allowClear
            />
            <Button
              size="small"
              type="dashed"
              onClick={() => {
                const input = prompt('输入字典项,格式:标签:值,每行一个\n例如:\n启用:1\n停用:0')
                if (input) {
                  const opts = input.split('\n').filter(s => s.trim()).map(s => {
                    const [label, value] = s.split(':').map(p => p.trim())
                    return { label: label || value, value: value || label }
                  })
                  updateCondition(index, { dictOptions: opts })
                }
              }}
            >
              配置字典
            </Button>
          </Space>
        )
      case 'excel_import':
        return (
          <Space size={4}>
            <Upload
              accept=".xlsx,.xls,.csv"
              showUploadList={false}
              beforeUpload={(file) => handleExcelImport(index, file)}
            >
              <Button size="small" icon={<UploadOutlined />}>导入Excel</Button>
            </Upload>
            {cond.importedValues && cond.importedValues.length > 0 && (
              <Tag color="blue">{cond.importedValues.length}</Tag>
            )}
          </Space>
        )
      default:
        return (
          <Input
            size="small"
            placeholder="请输入值"
            value={cond.value}
            onChange={e => updateCondition(index, { value: e.target.value })}
            style={{ width: 200 }}
          />
        )
    }
  }

  return (
    <Modal
      title="查询条件配置"
      open={open}
      onCancel={onClose}
      width={720}
      footer={
        <Space>
          <Button onClick={onParse} icon={<PlusOutlined />}>
            重新解析条件
          </Button>
          <Button type="primary" onClick={onClose}>
            确定
          </Button>
        </Space>
      }
    >
      {conditions.length === 0 ? (
        <Empty
          description={
            <span>
              未配置条件。请在SQL中使用 {'{{变量名}}'} 定义条件,<br />
              然后点击工具栏的"解析条件"按钮自动识别
            </span>
          }
          style={{ padding: '30px 0' }}
        />
      ) : (
        <div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
          {conditions.map((cond, index) => (
            <div
              key={cond.name}
              style={{
                display: 'flex',
                alignItems: 'center',
                gap: 8,
                padding: '10px 12px',
                borderBottom: '1px solid #f0f0f0',
                flexWrap: 'wrap',
              }}
            >
              <Tag color="blue" style={{ minWidth: 90, textAlign: 'center' }}>{cond.name}</Tag>
              <Select
                size="small"
                value={cond.type}
                onChange={t => updateCondition(index, { type: t })}
                options={CONDITION_TYPES}
                style={{ width: 140 }}
              />
              <div style={{ flex: 1, minWidth: 200 }}>
                {renderValueInput(cond, index)}
              </div>
              <Popconfirm title="删除该条件?" onConfirm={() => {
                const newConds = conditions.filter((_, i) => i !== index)
                onConditionsChange(newConds)
              }}>
                <Button size="small" type="text" danger icon={<DeleteOutlined />} />
              </Popconfirm>
            </div>
          ))}
        </div>
      )}

      <div style={{ marginTop: 12, padding: '8px 12px', background: '#f6f6f6', borderRadius: 6, fontSize: 12, color: '#888' }}>
        提示:日期区间类型需在SQL中使用 {'{{end_变量名}}'} 表示结束日期,例如 {'{{start_date}}'}{'{{end_start_date}}'}
      </div>
    </Modal>
  )
}