<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
  Check,
  CopyDocument,
  Delete,
  Plus,
  Refresh,
  Search,
  UploadFilled,
} from '@element-plus/icons-vue'

import {
  batchCreateQrCodes,
  batchDeleteQrCodes,
  deleteQrCode,
  fetchQrCodeStats,
  fetchQrCodes,
  getOCRJobResult,
  submitOCRJob,
  updateQrCode,
  type ChatQrCode,
  type CreateQrCodePayload,
  type QrCodeBooleanFilter,
  type QrCodeExpireStatus,
  type QrCodeStatus,
  type QrCodeStats,
} from '@/features/admin/api/adminQrCodes'
import { uploadAdminFile } from '@/shared/api/files'
import { formatDateTime } from '@/shared/utils/time'
import AdminTablePagination from '../components/AdminTablePagination.vue'
import AuthImage from '@/shared/components/business/AuthImage.vue'
import { readError } from '@/shared/utils/error'
import { useAdminSessionStore } from '@/stores/adminSession'

const adminSession = useAdminSessionStore()
const loading = ref(false)
const qrcodes = ref<ChatQrCode[]>([])
const stats = ref<QrCodeStats>({ unused_count: 0, used_count: 0, disabled_count: 0, total_count: 0 })
const canManageQrCodes = computed(() => adminSession.hasPermission('qrcode:manage'))

const currentPage = ref(1)
const currentPageSize = ref(20)
const total = ref(0)

type QrCodeFilters = {
  keyword: string
  status: QrCodeStatus | ''
  wecom_renamed: QrCodeBooleanFilter | ''
  bound: QrCodeBooleanFilter | ''
  expire_status: QrCodeExpireStatus | ''
  created_range: string[]
}

const filters = reactive<QrCodeFilters>({
  keyword: '',
  status: '',
  wecom_renamed: '',
  bound: '',
  expire_status: '',
  created_range: [],
})

// 上传弹窗
const uploadVisible = ref(false)
const uploadPendingCount = ref(0)
const uploadSaving = ref(false)
type UploadedQrImage = {
  _id: string
  url: string
  previewUrl: string
  file: File
  groupName: string
  ocrStatus: 'recognizing' | 'done' | 'failed'
}

const uploadedImages = ref<UploadedQrImage[]>([])
const uploadForm = reactive({
  note: '',
  expires_at: '' as string,
})

// 编辑弹窗
const editVisible = ref(false)
const editUploading = ref(false)
const editForm = reactive({
  id: 0,
  image_url: '',
  group_name: '',
  note: '',
  status: 'unused' as QrCodeStatus,
  wecom_renamed: false,
  expires_at: '' as string,
  was_issued: false,
})

const statusOptions: { label: string; value: QrCodeStatus | ''; type: string }[] = [
  { label: '全部', value: '', type: 'info' },
  { label: '待用', value: 'unused', type: 'success' },
  { label: '已发放', value: 'used', type: 'primary' },
  { label: '已停用', value: 'disabled', type: 'danger' },
]

const renameFilterOptions: { label: string; value: QrCodeBooleanFilter }[] = [
  { label: '已改名', value: 'true' },
  { label: '未改名', value: 'false' },
]

const boundFilterOptions: { label: string; value: QrCodeBooleanFilter }[] = [
  { label: '已绑定', value: 'true' },
  { label: '未绑定', value: 'false' },
]

const expireStatusOptions: { label: string; value: QrCodeExpireStatus }[] = [
  { label: '未过期', value: 'valid' },
  { label: '即将过期', value: 'expiring_soon' },
  { label: '已过期', value: 'expired' },
  { label: '永久有效', value: 'permanent' },
]

const statusMap = computed(() => {
  const m: Record<string, { label: string; type: string }> = {}
  statusOptions.forEach(o => {
    if (o.value) m[o.value] = { label: o.label, type: o.type }
  })
  return m
})

const editStatusOptions = computed(() =>
  editForm.was_issued
    ? statusOptions.filter(option => option.value === 'used' || option.value === 'disabled')
    : statusOptions.filter(option => option.value === 'unused' || option.value === 'disabled')
)

const maxBatchUploadCount = 20
const uploading = computed(() => uploadPendingCount.value > 0)
const uploadBusy = computed(() => uploading.value || uploadSaving.value)
const uploadedImageCountText = computed(() => `${uploadedImages.value.length}/${maxBatchUploadCount}`)
const renameLoadingIds = ref(new Set<number>())
const selectedRows = ref<ChatQrCode[]>([])
const selectedCount = computed(() => selectedRows.value.length)
const selectedIssuedCount = computed(() => selectedRows.value.filter(qrcodeWasIssued).length)

function filterDateToISO(value?: string) {
  if (!value) return undefined
  const date = new Date(value)
  if (Number.isNaN(date.getTime())) return undefined
  return date.toISOString()
}

async function loadList() {
  loading.value = true
  try {
    const [createdStart, createdEnd] = filters.created_range || []
    const res = await fetchQrCodes({
      status: filters.status || undefined,
      keyword: filters.keyword.trim() || undefined,
      wecom_renamed: filters.wecom_renamed || undefined,
      bound: filters.bound || undefined,
      expire_status: filters.expire_status || undefined,
      created_start: filterDateToISO(createdStart),
      created_end: filterDateToISO(createdEnd),
      page: currentPage.value,
      limit: currentPageSize.value,
    })
    qrcodes.value = res.items
    total.value = res.total
    selectedRows.value = []
  } finally {
    loading.value = false
  }
}

async function loadStats() {
  stats.value = await fetchQrCodeStats()
}

async function reloadAll() {
  await Promise.all([loadList(), loadStats()])
}

function handleFilter() {
  currentPage.value = 1
  loadList()
}

function resetFilters() {
  filters.keyword = ''
  filters.status = ''
  filters.wecom_renamed = ''
  filters.bound = ''
  filters.expire_status = ''
  filters.created_range = []
  handleFilter()
}

function handlePageChange() {
  loadList()
}

function handleSelectionChange(rows: ChatQrCode[]) {
  selectedRows.value = rows
}

function parseGroupNameFromOcrText(text: string) {
  const lines = text
    .split(/\r?\n/)
    .map(line => line.replace(/\s+/g, '').replace(/[|｜]/g, ''))
    .map(line => line.replace(/[^\u4e00-\u9fa5a-zA-Z0-9_（）()《》\-—·]+/g, ''))
    .filter(Boolean)

  const excludedPatterns = [
    /使用.*微信/,
    /企业微信/,
    /扫码/,
    /加入/,
    /二维码/,
    /有效/,
    /天内/,
    /前/,
    /^\d+$/,
  ]

  const candidates = lines.filter(line => {
    if (line.length < 3 || line.length > 64) return false
    if (!/[\u4e00-\u9fa5]/.test(line)) return false
    return !excludedPatterns.some(pattern => pattern.test(line))
  })

  return candidates.find(line => line.includes('群')) || candidates.find(line => /[-—]/.test(line)) || candidates[0] || ''
}

const ocrConcurrencyLimit = 4
let ocrRunningCount = 0
const ocrWaitQueue: (() => void)[] = []

function ocrAcquireSlot(): Promise<void> {
  if (ocrRunningCount < ocrConcurrencyLimit) {
    ocrRunningCount++
    return Promise.resolve()
  }
  return new Promise(resolve => {
    ocrWaitQueue.push(() => {
      ocrRunningCount++
      resolve()
    })
  })
}

function ocrReleaseSlot() {
  ocrRunningCount = Math.max(0, ocrRunningCount - 1)
  const next = ocrWaitQueue.shift()
  if (next) next()
}

function ocrResetSlots() {
  ocrRunningCount = 0
  while (ocrWaitQueue.length > 0) {
    const next = ocrWaitQueue.shift()
    next?.()
  }
}

async function recognizeGroupName(file: File) {
  const { job_id } = await submitOCRJob(file)
  const delays = [1000, 2000, 3000, 3000, 3000]
  for (let i = 0; i < delays.length; i++) {
    await new Promise(resolve => setTimeout(resolve, delays[i]))
    const record = await getOCRJobResult(job_id)
    if (record.status === 'done' && record.result) {
      return record.result.group_name || parseGroupNameFromOcrText(record.result.raw_text || '')
    }
    if (record.status === 'failed') {
      throw new Error(record.error || 'OCR 识别失败')
    }
  }
  throw new Error('OCR 识别超时')
}

async function recognizeGroupNameWithLimit(file: File) {
  await ocrAcquireSlot()
  try {
    return await recognizeGroupName(file)
  } finally {
    ocrReleaseSlot()
  }
}

// 上传：逐个上传图片拿 URL
async function handleFileUpload(options: { file: File }) {
  if (uploadedImages.value.length + uploadPendingCount.value >= maxBatchUploadCount) {
    ElMessage.warning(`单次最多添加 ${maxBatchUploadCount} 张二维码`)
    return
  }
  if (!options.file.type.startsWith('image/')) {
    ElMessage.warning('只能上传图片文件')
    return
  }

  uploadPendingCount.value += 1
  try {
    const item: UploadedQrImage = {
      _id: `${Date.now()}_${Math.random().toString(36).slice(2)}`,
      url: '',
      previewUrl: URL.createObjectURL(options.file),
      file: options.file,
      groupName: '',
      ocrStatus: 'recognizing',
    }
    uploadedImages.value.push(item)

    const [uploaded, groupName] = await Promise.all([
      uploadAdminFile(options.file, 'qrcode'),
      recognizeGroupNameWithLimit(options.file).catch(error => {
        console.warn('二维码群名 OCR 失败', error)
        ElMessage.warning(readError(error, '二维码群名 OCR 失败'))
        return ''
      }),
    ])
    item.url = uploaded.url
    item.groupName = groupName
    item.ocrStatus = groupName ? 'done' : 'failed'
    if (!groupName) {
      console.warn('二维码群名 OCR 未识别到有效群名', options.file.name)
    }
  } catch {
    const index = uploadedImages.value.findIndex(item => item.file === options.file && !item.url)
    if (index >= 0) removeImage(index)
    ElMessage.error('图片上传失败')
  } finally {
    uploadPendingCount.value = Math.max(0, uploadPendingCount.value - 1)
  }
}

function imagePreviewSources() {
  return uploadedImages.value.map(item => item.url).filter(Boolean)
}

function removeImage(index: number) {
  const [removed] = uploadedImages.value.splice(index, 1)
  if (removed?.previewUrl) {
    URL.revokeObjectURL(removed.previewUrl)
  }
}

function clearUploadedImages() {
  uploadedImages.value.forEach(item => {
    if (item.previewUrl) URL.revokeObjectURL(item.previewUrl)
  })
  uploadedImages.value = []
  uploadPendingCount.value = 0
  ocrResetSlots()
}

function handleUploadExceed() {
  ElMessage.warning(`单次最多添加 ${maxBatchUploadCount} 张二维码`)
}

function openUpload() {
  if (!canManageQrCodes.value) return
  clearUploadedImages()
  uploadForm.note = ''
  uploadForm.expires_at = ''
  uploadVisible.value = true
}

async function submitUpload() {
  if (!canManageQrCodes.value) return
  if (uploadPendingCount.value > 0) {
    ElMessage.warning('图片还在上传中，请稍后保存')
    return
  }
  if (uploadedImages.value.length === 0) {
    ElMessage.warning('请先上传二维码图片')
    return
  }
  uploadSaving.value = true
  try {
    const expiresAt = uploadForm.expires_at
      ? new Date(uploadForm.expires_at).toISOString()
      : null
    const items: CreateQrCodePayload[] = uploadedImages.value.map(img => ({
      image_url: img.url,
      group_name: img.groupName,
      note: uploadForm.note,
      expires_at: expiresAt,
    }))
    await batchCreateQrCodes(items)
    ElMessage.success(`已批量添加 ${items.length} 张二维码`)
    uploadVisible.value = false
    await reloadAll()
  } finally {
    uploadSaving.value = false
  }
}

onBeforeUnmount(() => {
  clearUploadedImages()
})

function openEdit(row: ChatQrCode) {
  if (!canManageQrCodes.value) return
  editForm.id = row.id
  editForm.image_url = row.image_url
  editForm.group_name = row.group_name || ''
  editForm.note = row.note
  editForm.status = row.status
  editForm.wecom_renamed = row.wecom_renamed
  editForm.expires_at = row.expires_at ? row.expires_at.replace('T', ' ').slice(0, 16) : ''
  editForm.was_issued = row.status === 'used' || Boolean(row.conversation_id || row.used_at)
  editVisible.value = true
}

function clearEditExpiresAt() {
  editForm.expires_at = ''
}

async function handleEditImageUpload(options: { file: File }) {
  if (!canManageQrCodes.value) return
  editUploading.value = true
  try {
    const uploaded = await uploadAdminFile(options.file, 'qrcode')
    editForm.image_url = uploaded.url
    ElMessage.success('图片已上传')
  } catch {
    ElMessage.error('图片上传失败')
  } finally {
    editUploading.value = false
  }
}

async function submitEdit() {
  if (!canManageQrCodes.value) return
  await updateQrCode(editForm.id, {
    image_url: editForm.image_url,
    group_name: editForm.group_name,
    note: editForm.note,
    status: editForm.status,
    wecom_renamed: editForm.wecom_renamed,
    expires_at: editForm.expires_at ? new Date(editForm.expires_at).toISOString() : undefined,
    clear_expires_at: !editForm.expires_at,
  })
  ElMessage.success('已更新')
  editVisible.value = false
  await reloadAll()
}

async function copyText(text: string, successMessage: string) {
  if (!text) {
    ElMessage.warning('暂无可复制内容')
    return
  }
  try {
    await navigator.clipboard.writeText(text)
    ElMessage.success(successMessage)
  } catch {
    ElMessage.error('复制失败')
  }
}

function boundConversationLabel(row: ChatQrCode) {
  if (row.bound_conversation_title) return row.bound_conversation_title
  if (row.conversation_id) return `#${row.conversation_id}`
  return ''
}

function qrcodeWasIssued(row: ChatQrCode) {
  return row.status === 'used' || Boolean(row.conversation_id || row.used_at)
}

function setRenameLoading(id: number, loading: boolean) {
  const next = new Set(renameLoadingIds.value)
  if (loading) {
    next.add(id)
  } else {
    next.delete(id)
  }
  renameLoadingIds.value = next
}

async function handleRenameFlagChange(row: ChatQrCode, value: string | number | boolean) {
  if (!canManageQrCodes.value) return
  const nextValue = Boolean(value)
  const previous = !nextValue
  row.wecom_renamed = nextValue
  setRenameLoading(row.id, true)
  try {
    await updateQrCode(row.id, { wecom_renamed: nextValue })
    ElMessage.success(nextValue ? '已标记企微已改名' : '已取消企微改名标记')
  } catch {
    row.wecom_renamed = previous
    ElMessage.error('标记失败')
  } finally {
    setRenameLoading(row.id, false)
  }
}

async function handleDisable(row: ChatQrCode) {
  if (!canManageQrCodes.value) return
  await ElMessageBox.confirm('确定停用该二维码？停用后不再发放给新发布群。', '停用确认', {
    type: 'warning',
  })
  await updateQrCode(row.id, { status: 'disabled' })
  ElMessage.success('已停用')
  await reloadAll()
}

async function handleDelete(row: ChatQrCode) {
  if (!canManageQrCodes.value) return
  const tip = qrcodeWasIssued(row)
    ? '确定删除这条已发放二维码记录？删除后仅清理二维码池旧记录，不会影响已发送到群聊里的图片消息。'
    : '确定删除该二维码？删除后将从二维码池移除。'
  await ElMessageBox.confirm(tip, '删除确认', {
    type: 'warning',
  })
  await deleteQrCode(row.id)
  ElMessage.success('已删除')
  await reloadAll()
}

async function handleBatchDelete() {
  if (!canManageQrCodes.value) return
  if (!selectedCount.value) {
    ElMessage.warning('请先选择要删除的二维码')
    return
  }

  const tip = selectedIssuedCount.value
    ? `确定删除选中的 ${selectedCount.value} 条二维码记录？其中 ${selectedIssuedCount.value} 条已发放，删除后仅清理二维码池旧记录，不会影响已发送到群聊里的图片消息。`
    : `确定删除选中的 ${selectedCount.value} 条二维码？删除后将从二维码池移除。`
  await ElMessageBox.confirm(tip, '批量删除确认', {
    type: 'warning',
  })

  const result = await batchDeleteQrCodes(selectedRows.value.map(row => row.id))
  selectedRows.value = []
  ElMessage.success(`已删除 ${result.deleted} 条`)
  await reloadAll()
}

onMounted(reloadAll)
</script>

<template>
  <div class="qrcode-pool-view">
    <!-- 库存统计卡片 -->
    <div class="stats-cards">
      <el-card shadow="hover" class="stat-card">
        <div class="stat-value success">{{ stats.unused_count }}</div>
        <div class="stat-label">待用库存</div>
      </el-card>
      <el-card shadow="hover" class="stat-card">
        <div class="stat-value primary">{{ stats.used_count }}</div>
        <div class="stat-label">已发放</div>
      </el-card>
      <el-card shadow="hover" class="stat-card">
        <div class="stat-value danger">{{ stats.disabled_count }}</div>
        <div class="stat-label">已停用</div>
      </el-card>
      <el-card shadow="hover" class="stat-card">
        <div class="stat-value">{{ stats.total_count }}</div>
        <div class="stat-label">合计</div>
      </el-card>
    </div>

    <!-- 工具栏 -->
    <el-card shadow="never" class="toolbar-card">
      <div class="toolbar">
        <div class="filter-area">
          <el-input
            v-model="filters.keyword"
            clearable
            class="keyword-filter"
            :prefix-icon="Search"
            placeholder="群名 / 备注 / 绑定群 / ID"
            @clear="handleFilter"
            @keyup.enter="handleFilter"
          />
          <el-select
            v-model="filters.status"
            placeholder="状态筛选"
            clearable
            class="compact-filter"
            @change="handleFilter"
          >
            <el-option
              v-for="opt in statusOptions.filter(o => o.value)"
              :key="opt.value"
              :label="opt.label"
              :value="opt.value"
            />
          </el-select>
          <el-select
            v-model="filters.wecom_renamed"
            placeholder="企微改名"
            clearable
            class="compact-filter"
            @change="handleFilter"
          >
            <el-option
              v-for="opt in renameFilterOptions"
              :key="`rename-${opt.value}`"
              :label="opt.label"
              :value="opt.value"
            />
          </el-select>
          <el-select
            v-model="filters.bound"
            placeholder="绑定群"
            clearable
            class="compact-filter"
            @change="handleFilter"
          >
            <el-option
              v-for="opt in boundFilterOptions"
              :key="`bound-${opt.value}`"
              :label="opt.label"
              :value="opt.value"
            />
          </el-select>
          <el-select
            v-model="filters.expire_status"
            placeholder="过期状态"
            clearable
            class="compact-filter"
            @change="handleFilter"
          >
            <el-option
              v-for="opt in expireStatusOptions"
              :key="opt.value"
              :label="opt.label"
              :value="opt.value"
            />
          </el-select>
          <el-date-picker
            v-model="filters.created_range"
            type="datetimerange"
            range-separator="至"
            start-placeholder="上传开始"
            end-placeholder="上传结束"
            format="YYYY-MM-DD HH:mm"
            value-format="YYYY-MM-DDTHH:mm:ss"
            class="date-range-filter"
            @change="handleFilter"
          />
          <el-button type="primary" :icon="Search" @click="handleFilter">筛选</el-button>
          <el-button @click="resetFilters">重置</el-button>
        </div>
        <div class="action-area">
          <span v-if="selectedCount" class="selection-summary">已选 {{ selectedCount }} 条</span>
          <el-button
            v-if="canManageQrCodes"
            type="danger"
            :icon="Delete"
            :disabled="!selectedCount"
            @click="handleBatchDelete"
          >
            批量删除
          </el-button>
          <el-button :icon="Refresh" @click="reloadAll">刷新</el-button>
          <el-button v-if="canManageQrCodes" type="primary" :icon="Plus" @click="openUpload">
            添加二维码
          </el-button>
        </div>
      </div>
    </el-card>

    <!-- 列表 -->
    <el-card shadow="never" class="table-card">
      <el-table
        v-loading="loading"
        :data="qrcodes"
        class="table-panel qrcode-table"
        :fit="true"
        row-key="id"
        @selection-change="handleSelectionChange"
      >
        <el-table-column
          v-if="canManageQrCodes"
          type="selection"
          class-name="selection-col"
          align="center"
        />
        <el-table-column label="二维码" class-name="qr-col" align="center">
          <template #default="{ row }">
            <div class="qrcode-thumb">
              <AuthImage
                :source="row.image_url"
                fit="cover"
                :image-style="{ width: '56px', height: '56px', borderRadius: '6px' }"
                :preview-src-list="[row.image_url]"
              />
            </div>
          </template>
        </el-table-column>
        <el-table-column label="状态" class-name="status-col" align="center">
          <template #default="{ row }">
            <el-tag :type="(statusMap[row.status]?.type as any) || 'info'" size="small">
              {{ statusMap[row.status]?.label || row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="群名" class-name="name-col" show-overflow-tooltip>
          <template #default="{ row }">
            <button
              class="copy-cell-button"
              type="button"
              :disabled="!row.group_name"
              @click="copyText(row.group_name, '群名已复制')"
            >
              <span>{{ row.group_name || '-' }}</span>
              <el-icon v-if="row.group_name"><CopyDocument /></el-icon>
            </button>
          </template>
        </el-table-column>
        <el-table-column label="备注" class-name="note-col" show-overflow-tooltip>
          <template #default="{ row }">
            <span class="note-cell">{{ row.note || '-' }}</span>
          </template>
        </el-table-column>
        <el-table-column label="绑定群" class-name="group-col" show-overflow-tooltip>
          <template #default="{ row }">
            <button
              class="copy-cell-button"
              type="button"
              :disabled="!boundConversationLabel(row)"
              @click="copyText(boundConversationLabel(row), '绑定群聊名称已复制')"
            >
              <span>{{ boundConversationLabel(row) || '-' }}</span>
              <el-icon v-if="boundConversationLabel(row)"><CopyDocument /></el-icon>
            </button>
          </template>
        </el-table-column>
        <el-table-column label="企微改名" class-name="rename-col" align="center">
          <template #default="{ row }">
            <el-switch
              :model-value="row.wecom_renamed"
              :loading="renameLoadingIds.has(row.id)"
              :disabled="!canManageQrCodes"
              inline-prompt
              :active-icon="Check"
              active-text="已改"
              inactive-text="未改"
              @change="(value: string | number | boolean) => handleRenameFlagChange(row, value)"
            />
          </template>
        </el-table-column>
        <el-table-column label="时间" class-name="time-col" align="center">
          <template #default="{ row }">
            <div class="time-stack">
              <span>上传 {{ formatDateTime(row.created_at) }}</span>
              <span>{{ row.used_at ? `使用 ${formatDateTime(row.used_at)}` : '使用 -' }}</span>
              <span
                :class="{ 'expired-tag': row.expires_at && new Date(row.expires_at) < new Date() }"
              >
                {{ row.expires_at ? `过期 ${formatDateTime(row.expires_at)}` : '过期 永久' }}
              </span>
            </div>
          </template>
        </el-table-column>
        <el-table-column
          v-if="canManageQrCodes"
          label="操作"
          class-name="action-col"
          align="center"
        >
          <template #default="{ row }">
            <div class="table-actions">
              <el-button text type="primary" size="small" @click="openEdit(row)">编辑</el-button>
              <el-button
                v-if="row.status !== 'disabled'"
                text
                type="warning"
                size="small"
                @click="handleDisable(row)"
                >停用</el-button
              >
              <el-button text type="danger" size="small" @click="handleDelete(row)">删除</el-button>
            </div>
          </template>
        </el-table-column>
      </el-table>

      <AdminTablePagination
        v-model:current-page="currentPage"
        v-model:page-size="currentPageSize"
        :total="total"
        :loading="loading"
        @page-change="handlePageChange"
      />
    </el-card>

    <!-- 上传弹窗 -->
    <el-dialog
      v-model="uploadVisible"
      title="批量添加企业微信群二维码"
      width="680px"
      @close="clearUploadedImages"
    >
      <el-form class="qrcode-upload-form" label-width="108px">
        <el-form-item label="二维码图片" required>
          <div class="batch-upload-area">
            <el-upload
              :auto-upload="true"
              :show-file-list="false"
              :http-request="(opt: any) => handleFileUpload(opt)"
              accept="image/*"
              multiple
              drag
              :limit="maxBatchUploadCount"
              :disabled="uploadBusy || uploadedImages.length >= maxBatchUploadCount"
              :on-exceed="handleUploadExceed"
            >
              <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
              <div class="el-upload__text">点击或拖拽上传，支持多选</div>
              <template #tip>
                <div class="upload-tip">
                  已选择 {{ uploadedImageCountText }}
                  <span v-if="uploadPendingCount > 0">，{{ uploadPendingCount }} 张上传中</span>
                </div>
              </template>
            </el-upload>
            <div v-if="uploadedImages.length" class="uploaded-list">
              <div v-for="(img, idx) in uploadedImages" :key="img._id" class="uploaded-item">
                <div class="uploaded-main">
                  <div class="uploaded-thumb">
                    <AuthImage
                      v-if="img.url"
                      :source="img.url"
                      fit="cover"
                      :image-style="{ width: '52px', height: '52px', borderRadius: '6px' }"
                      :preview-src-list="[img.url]"
                    />
                    <img
                      v-else
                      class="uploaded-local-preview"
                      :src="img.previewUrl"
                      alt=""
                    />
                    <button
                      class="uploaded-remove"
                      type="button"
                      :disabled="uploadBusy"
                      @click="removeImage(idx)"
                    >
                      ×
                    </button>
                  </div>
                  <div class="uploaded-meta">
                    <div class="uploaded-name" :title="img.file.name">{{ img.file.name }}</div>
                    <el-tag v-if="img.ocrStatus === 'recognizing'" size="small" type="info"
                      >识别中</el-tag
                    >
                    <el-tag v-if="img.ocrStatus === 'done'" size="small" type="success">OCR</el-tag>
                    <el-tag v-if="img.ocrStatus === 'failed'" size="small" type="warning"
                      >待校对</el-tag
                    >
                  </div>
                </div>
                <div class="uploaded-group-editor">
                  <el-input
                    v-model="img.groupName"
                    size="small"
                    placeholder="群名"
                    maxlength="64"
                    clearable
                  />
                </div>
              </div>
            </div>
          </div>
        </el-form-item>
        <el-form-item label="备注">
          <el-input v-model="uploadForm.note" placeholder="如：7月企微群A（统一备注，适用于同一批次）" maxlength="50" />
        </el-form-item>
        <el-form-item label="过期时间">
          <el-date-picker
            v-model="uploadForm.expires_at"
            type="datetime"
            placeholder="留空默认7天后过期"
            format="YYYY-MM-DD HH:mm"
            value-format="YYYY-MM-DDTHH:mm"
            style="width: 100%"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button :disabled="uploadBusy" @click="uploadVisible = false">取消</el-button>
        <el-button type="primary" :loading="uploadBusy" @click="submitUpload">
          保存{{ uploadedImages.length > 0 ? `（${uploadedImages.length} 张）` : '' }}
        </el-button>
      </template>
    </el-dialog>

    <!-- 编辑弹窗 -->
    <el-dialog v-model="editVisible" title="编辑二维码" width="440px">
      <el-form label-width="90px">
        <el-form-item label="二维码图片">
          <div class="edit-image-area">
            <div class="edit-image-preview">
              <AuthImage
                v-if="editForm.image_url"
                :source="editForm.image_url"
                fit="cover"
                :preview-src-list="[editForm.image_url]"
                :image-style="{ width: '100%', height: '100%', borderRadius: '6px' }"
              />
              <div v-else class="edit-image-empty">暂无图片</div>
            </div>
            <el-upload
              :auto-upload="true"
              :show-file-list="false"
              :http-request="(opt: any) => handleEditImageUpload(opt)"
              accept="image/*"
              :disabled="editUploading"
            >
              <el-button size="small" :loading="editUploading">
                {{ editForm.image_url ? '替换图片' : '上传图片' }}
              </el-button>
            </el-upload>
          </div>
        </el-form-item>
        <el-form-item label="状态">
          <el-select v-model="editForm.status" style="width: 100%">
            <el-option
              v-for="option in editStatusOptions"
              :key="option.value"
              :label="option.label"
              :value="option.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="群名">
          <el-input v-model="editForm.group_name" placeholder="企业微信群名" maxlength="64" clearable />
        </el-form-item>
        <el-form-item label="企微改名">
          <el-switch v-model="editForm.wecom_renamed" inline-prompt active-text="已改" inactive-text="未改" />
        </el-form-item>
        <el-form-item label="备注">
          <el-input v-model="editForm.note" placeholder="如：7月企微群A" maxlength="50" />
        </el-form-item>
        <el-form-item label="过期时间">
          <div class="expire-editor">
            <el-date-picker
              v-model="editForm.expires_at"
              type="datetime"
              placeholder="留空为永久"
              format="YYYY-MM-DD HH:mm"
              value-format="YYYY-MM-DDTHH:mm"
              style="width: 100%"
            />
            <el-button :disabled="!editForm.expires_at" @click="clearEditExpiresAt">清空</el-button>
          </div>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="editVisible = false">取消</el-button>
        <el-button type="primary" @click="submitEdit">保存</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<style scoped>
.qrcode-pool-view {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.stats-cards {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
}

.stat-card {
  text-align: center;
}

.toolbar-card,
.table-card {
  border-radius: 10px;
}

.table-card :deep(.el-card__body) {
  padding: 20px;
}

.stat-value {
  font-size: 28px;
  font-weight: 600;
  line-height: 1.2;
}

.stat-value.success {
  color: var(--el-color-success);
}
.stat-value.primary {
  color: var(--el-color-primary);
}
.stat-value.danger {
  color: var(--el-color-danger);
}

.stat-label {
  margin-top: 6px;
  color: var(--el-text-color-secondary);
  font-size: 13px;
}

.toolbar {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}

.filter-area,
.action-area {
  display: flex;
  gap: 8px;
  align-items: center;
}

.filter-area {
  flex: 1 1 auto;
  flex-wrap: wrap;
}

.action-area {
  flex: 0 0 auto;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.keyword-filter {
  width: 240px;
}

.compact-filter {
  width: 128px;
}

.date-range-filter {
  width: 330px;
}

.qrcode-table {
  border-radius: 10px;
  box-shadow: none;
  padding: 0;
}

.qrcode-table :deep(.el-table__inner-wrapper) {
  min-width: 0;
}

.qrcode-table :deep(.el-table__header th) {
  height: 42px;
  background: #f8f9fb;
}

.qrcode-table :deep(.el-table__cell) {
  padding: 10px 0;
}

.qrcode-table :deep(.cell) {
  padding: 0 14px;
}

.qrcode-table :deep(.el-table__fixed-right) {
  box-shadow: -8px 0 12px rgba(31, 45, 61, 0.04);
}

.qrcode-table :deep(.el-table__header),
.qrcode-table :deep(.el-table__body) {
  width: 100% !important;
}

.qrcode-table :deep(.el-table__header-wrapper),
.qrcode-table :deep(.el-table__body-wrapper) {
  overflow-x: auto;
}

.qrcode-table :deep(.selection-col) {
  width: 4%;
}

.qrcode-table :deep(.qr-col) {
  width: 8%;
}

.qrcode-table :deep(.status-col) {
  width: 8%;
}

.qrcode-table :deep(.name-col) {
  width: 17%;
}

.qrcode-table :deep(.note-col) {
  width: 17%;
}

.qrcode-table :deep(.group-col) {
  width: 17%;
}

.qrcode-table :deep(.rename-col) {
  width: 8%;
}

.qrcode-table :deep(.time-col) {
  width: 12%;
}

.qrcode-table :deep(.action-col) {
  width: 10%;
}

.qrcode-thumb {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 64px;
  border: 1px solid #eef1f5;
  border-radius: 8px;
  background: #ffffff;
}

.note-cell {
  color: #303846;
  font-weight: 500;
}

.time-stack {
  display: inline-flex;
  flex-direction: column;
  gap: 2px;
  color: #5f6b7a;
  font-size: 12px;
  line-height: 18px;
  text-align: left;
  white-space: nowrap;
}

.copy-cell-button {
  display: inline-flex;
  max-width: 100%;
  align-items: center;
  gap: 4px;
  padding: 0;
  border: 0;
  background: transparent;
  color: #303846;
  cursor: pointer;
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  line-height: 20px;
  text-align: left;
}

.copy-cell-button span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.copy-cell-button .el-icon {
  flex: 0 0 auto;
  color: var(--el-color-primary);
  font-size: 14px;
}

.copy-cell-button:disabled {
  color: #a8b0bd;
  cursor: default;
}

.table-actions {
  display: flex;
  justify-content: center;
  gap: 6px;
  white-space: nowrap;
}

.table-actions :deep(.el-button) {
  margin-left: 0;
  padding: 4px 6px;
}

@media (max-width: 1280px) {
  .toolbar {
    flex-direction: column;
    align-items: stretch;
  }

  .action-area {
    justify-content: flex-start;
  }
}

@media (max-width: 720px) {
  .keyword-filter,
  .compact-filter,
  .date-range-filter {
    width: 100%;
  }

  .filter-area,
  .action-area {
    align-items: stretch;
  }
}

.upload-preview {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.batch-upload-area {
  width: 100%;
}

.qrcode-upload-form :deep(.el-form-item__label) {
  align-items: center;
  padding-right: 18px;
  line-height: 32px;
  white-space: nowrap;
  word-break: keep-all;
}

.qrcode-upload-form :deep(.el-form-item) {
  margin-bottom: 18px;
}

.qrcode-upload-form :deep(.el-form-item__content) {
  min-width: 0;
}

.batch-upload-area :deep(.el-upload) {
  width: 100%;
}

.batch-upload-area :deep(.el-upload-dragger) {
  width: 100%;
  padding: 12px 18px;
}

.batch-upload-area :deep(.el-icon--upload) {
  margin-bottom: 4px;
  font-size: 28px;
  line-height: 1;
}

.batch-upload-area :deep(.el-upload__text) {
  line-height: 20px;
}

.upload-tip {
  color: var(--el-text-color-secondary);
  font-size: 12px;
  line-height: 18px;
}

.uploaded-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
  margin-top: 10px;
  max-height: 320px;
  overflow-y: auto;
  padding: 10px;
  border: 1px solid #eef1f5;
  border-radius: 8px;
  background: #fafbfc;
}

.uploaded-item {
  min-width: 0;
  padding: 10px;
  border: 1px solid #e7ebf2;
  border-radius: 8px;
  background: #ffffff;
}

.uploaded-main {
  display: flex;
  min-width: 0;
  align-items: center;
  gap: 10px;
}

.uploaded-thumb {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 60px;
  height: 60px;
  flex: 0 0 auto;
  border: 1px solid #e7ebf2;
  border-radius: 8px;
  background: #ffffff;
}

.uploaded-local-preview {
  display: block;
  width: 52px;
  height: 52px;
  border-radius: 6px;
  object-fit: cover;
}

.uploaded-remove {
  position: absolute;
  top: -7px;
  right: -7px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  padding: 0;
  border: 1px solid #ffffff;
  border-radius: 50%;
  background: var(--el-color-danger);
  color: #ffffff;
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
}

.uploaded-remove:disabled {
  cursor: not-allowed;
  opacity: 0.45;
}

.uploaded-name {
  overflow: hidden;
  color: var(--el-text-color-secondary);
  font-size: 12px;
  line-height: 16px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.uploaded-meta {
  display: flex;
  min-width: 0;
  flex: 1;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
}

.uploaded-meta .el-tag {
  height: 20px;
}

.uploaded-group-editor {
  margin-top: 8px;
}

.expired-tag {
  color: var(--el-color-danger);
}

.edit-image-area {
  display: flex;
  align-items: center;
  gap: 12px;
}

.edit-image-preview {
  flex: 0 0 auto;
  width: 88px;
  height: 88px;
  overflow: hidden;
  border: 1px solid #e7ebf2;
  border-radius: 8px;
  background: #fafbfc;
}

.edit-image-preview :deep(.el-image),
.edit-image-preview :deep(img) {
  display: block;
  width: 100%;
  height: 100%;
}

.edit-image-empty {
  display: grid;
  width: 100%;
  height: 100%;
  place-items: center;
  color: var(--el-text-color-secondary);
  font-size: 12px;
}

.expire-editor {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
}
</style>
