<template>
  <div class="camera-container" ref="cameraRef">
    <div class="camera-content">
      <div class="video-section">
        <!-- 历史波次选择器 -->
        <div class="wave-switch-bar">
          <span class="wave-selector-label">切换波次</span>
          <el-select v-model="selectedWaveNo" filterable clearable placeholder="选择波次" :loading="waveSearchLoading"
            style="width: 220px" @change="handleWaveSelect" @visible-change="handleWaveDropdownVisible">
            <el-option v-for="wave in waveSearchResults" :key="wave.id" :label="wave.wave_no" :value="wave.id" />
          </el-select>
        </div>

        <div class="video-wrapper">
          <video ref="videoRef" autoplay playsinline class="camera-video"></video>
        </div>
        <canvas ref="canvasRef" style="display: none;"></canvas>
        <div class="controls">
          <div class="button-group">
            <!-- 模式切换按钮组 -->
            <el-button-group>
              <el-button :type="scanMode === 'scan' ? 'primary' : 'default'" @click="scanMode = 'scan'">
                仅拍照(alt+c)
              </el-button>
              <el-button :type="scanMode === 'recognize' ? 'primary' : 'default'" @click="scanMode = 'recognize'">
                切换OCR识别(alt+c)
              </el-button>
            </el-button-group>
            <!-- 拍照识别按钮 -->
            <el-button type="default" :loading="isProcessing" @click="handlePhotoAction">
              拍照识别(alt+a)
            </el-button>
          </div>

          <!-- 隐藏输入框始终渲染(不受 scanMode 影响),供扫码枪输入快捷键指令 -->
          <input v-model="scannedCode" class="hidden-scanner-input" @input="handleScanInput" @keyup.enter="handleScan"
            :disabled="isProcessing || isPageLocked" />

          <!-- <div class="status" :class="{ 'status-success': barcodeSuccess, 'status-error': barcodeError }">
            {{ barcodeStatus }}
          </div> -->

          <!-- 创建波次按钮区域 -->
          <div class="wave-create-section">
            <!-- 无活跃波次时:显示创建波次按钮 -->
            <template v-if="!hasActiveWave">
              <el-button type="warning" :disabled="scannedCount === 0" @click="handleCreateWave">
                创建波次(alt+x)
              </el-button>
            </template>
            <!-- 有活跃波次时:显示创建新波次 + 向当前波次追加 -->
            <template v-else>
              <div class="wave-buttons-row">
                <el-button type="warning" @click="handleCreateNewWave">
                  创建新波次(alt+x)
                </el-button>
                <!-- <el-button
                  type="success"
                  :disabled="uncommittedCount === 0"
                  @click="handleAppendToWave"
                >
                  向当前波次追加
                </el-button> -->
              </div>
            </template>
            <span v-if="scannedCount > 0" class="wave-hint">
              已扫描 {{ scannedCount }} 本书,已入波次 {{ committedCount }} 本,待追加 {{ uncommittedCount }} 本
            </span>
          </div>
          <div class="wave-create-section">
            <!-- 当前波次信息提示 -->
            <span v-if="hasActiveWave" class="wave-hint wave-active-hint">
              当前波次:{{ waveNo }}
            </span>
            <!-- 生成条形码按钮 - 波次创建成功后显示 -->
            <el-button v-if="waveNo" type="primary" :loading="barcodeLoading" @click="generateWaveBarcode">
              生成波次条形码(alt+b)
            </el-button>
          </div>
          <!-- 波次条形码图片展示 -->
          <div v-if="barcodeImageBase64" class="barcode-display-section">
            <div class="barcode-wave-no">波次号:{{ waveNo }}</div>
            <div class="barcode-img-wrapper">
              <img :src="barcodeImageUrl" alt="波次条形码" />
            </div>
          </div>


        </div>
      </div>

    </div>
  </div>

  <!-- 套装书选择弹窗 -->
  <SuitBookDialog v-model="suitBookDialogVisible" :isbn="currentSuitIsbn" :bookInfo="currentSuitBookInfo ?? undefined" @select="handleSuitBookSelect" />

  <!-- OCR 识别结果弹窗 -->
  <OcrResultDialog v-model:visible="ocrDialogVisible" :loading="ocrLoading" :ocr-result="ocrResult"
    @assign="handleOcrAssign" />
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch, computed, Ref, nextTick, unref, inject, provide } from 'vue'
import { ElButton, ElMessage, ElMessageBox } from 'element-plus'
import { createPurchaseOrderWithWave, releaseWave, updatePurchaseOrder } from '@/api/purchaseOrder'
import { getWaveStatusById } from '@/api/waveTask'
import type { GoodsInfo } from './goodsInfo.vue'
import SuitBookDialog from './suitBookDialog.vue'
import OcrResultDialog from './ocrResultDialog.vue'
import { ocrImage } from '@/api/product'
import { generateBarcode } from '@/api/barcode'
import { getBookInfo, syncBook } from '@/api/book'
import { queryGoodsPrice } from '@/api/config'
import { getAdminUserInfo } from '@/utils/auth' 
import axios from 'axios'
import { uploadLivingPicture } from '@/utils/uploadLivingPicture'

// 定义 BookInfo 接口
interface BookInfo {
  bookName: string
  author: string
  publisher: string
  publishDate: string
  binding: string
  price: number
  pageCount: number
  wordCount: number
  book_pic?: {
    localPath: string
    pddPath: string
  }
  totalBook: number
  ownPrice: number
  capturedPhoto?: string
  /** 是否为套装书子书（用于核价时走套装书专用逻辑） */
  isSuit?: boolean
}

const props = defineProps<{
  modelValue: {
    photoSrc: string
    isbn: string
    firstPagePhoto: string
    barcode: string
  }
  /** 从 Wave.vue 传入的仓库ID(computed 自动解包) */
  warehouseId?: number | null
  /** 从 Wave.vue 传入的品相值(来自 Car 组件品相下拉) */
  quality?: number
}>()

// inject 页面锁定状态（来自 Wave.vue）
const isPageLocked = inject<{ value: boolean }>('isPageLocked', { value: false })
const connectionStatus = inject<{ value: string }>('connectionStatus', { value: 'idle' })
// 错误阻断器（来自 Wave.vue）
const errorBlocker = inject<any>('errorBlocker', undefined)

const emit = defineEmits<{
  (e: 'update:modelValue', value: typeof props.modelValue): void
  (e: 'book-info-update', value: BookInfo | null): void
  /** 拍照预览确认,携带书籍信息、拍摄的图片、商品ID和售价 */
  (e: 'photo-preview-confirm', value: { bookInfo: BookInfo; photoSrc: string; productId: number | null; salePrice?: number | null }): void
}>()

const videoRef = ref<HTMLVideoElement | null>(null)
const canvasRef = ref<HTMLCanvasElement | null>(null)

// 提供主摄像头视频流引用，供子组件（suitBookDialog）复用拍照
provide('mainVideoRef', videoRef)

const barcodeStatus = ref('拍照后自动识别条形码')
const barcodeSuccess = ref(false)
const barcodeError = ref(false)
const isProcessing = ref(false)
const actualWidth = ref(0)
const actualHeight = ref(0)
const scannedCode = ref('')

// 专门保存最后一次扫码的ISBN,用于拍照上传等场景
const lastScannedIsbn = ref('')

// 保存 /api/getBookInfo 返回的 book_pic_s,供保存商品时使用
const lastBookPicS = ref('')

/** 扫码输入防抖定时器 */
let scanInputTimer: ReturnType<typeof setTimeout> | null = null

/**
 * 检查是否已选择小车，若未选择则弹出警告并阻止后续操作
 */
function checkCarSelected(): boolean {
  if (!injectedCarId.value) {
    ElMessage.warning({ message: '请先选择小车', duration: 1000, customClass: 'scan-warning-message' })
    return false
  }
  return true
}

// ========== 套装书选择相关 ==========
const suitBookDialogVisible = ref(false)
const currentSuitIsbn = ref('')
/** 首次调用 syncBook 返回的 fid，用于后续同步时作为 f_id 传入 */
const currentSuitFid = ref<number | null>(null)
/** 当前扫码的书籍信息，用于套装书弹窗自定义添加项的预填 */
const currentSuitBookInfo = ref<{
  bookName?: string
  author?: string
  publisher?: string
  publishDate?: string
  binding?: string
  price?: number
  pageCount?: number | string
  wordCount?: number | string
  book_pic?: any
} | null>(null)
/** 套装书预览模式标记：来自 Car 搜索时，选择后仅更新预览不保存加入列表 */
const suitPreviewOnly = ref(false)

// ========== OCR 识别相关 ==========
const ocrDialogVisible = ref(false)
const ocrLoading = ref(false)
const ocrResult = ref<{
  success: boolean
  texts: string[]
  BookName?: string
  Author?: string
  Publisher?: string
} | null>(null)
// OCR 模式下捕获的 base64 图片（用于后续上传和预览）
const ocrCapturedBase64 = ref('')

// 保存最后一次上传实拍图片的文件名(用于后续关联),格式:{id}-{isbn}-{5位随机数}.jpg
const lastLivingPictureName = ref('')

// 记录每个 ISBN 对应的 product ID 和自定义价格,结构:{ id: number, ownPrice: number }
// id=0 表示首次创建,之后复用响应中的 id
const isbnProductIdMap = new Map<string, { id: number; ownPrice: number }>()

// ========== goods/query 队列相关 ==========
interface PendingGoodsQuery {
  isbn: string
  outId: string
  quality: string
  productId: number
  /** 套装书时传递书名 */
  bookName?: string
  /** 套装书时传递作者 */
  author?: string
  /** 套装书时传递出版社 */
  publisher?: string
  /** 是否为套装书 */
  isSuit?: boolean
}
const pendingGoodsQueryQueue: PendingGoodsQuery[] = []

/**
 * 调用 goods/query 接口（直连核价器）
 * @param isbn - ISBN（常规图书使用）
 * @param productId - 商品ID
 * @param quality - 品质
 * @param options - 套装书时的额外信息
 */
async function queryGoodsApi(isbn: string, productId: number, quality: string, options?: { bookName: string; author: string; publisher: string }): Promise<void> {
  const userInfo = getAdminUserInfo()
  const userId = userInfo?.about_id ?? ''
  const queryIndex = parseInt(localStorage.getItem('verify_index') || '1')

  // 从 localStorage 获取 IP 和端口
  const ip = localStorage.getItem('test_ip') || '127.0.0.1'
  const port = localStorage.getItem('test_port') || '8080'

  const isSuit = !!options
  const bookName = options?.bookName || ''
  const author = options?.author || ''
  const publisher = options?.publisher || ''

  console.log('[goods/query] 发送请求 - 直连:', { ip, port, isbn, productId, quality, isSuit, bookName, author, publisher })

  try {
    // 直接请求核价器，避免 CORS 需服务端支持
    const response = await queryGoodsPrice({
      ip,
      port,
      isbn,
      bookName,
      author,
      publisher,
      isSuit,
      outId: productId,
      quality,
      queryIndex,
      userId,
      placeholderDownPrice: localStorage.getItem('placeholder_down_price') || '0.01',
      minShippingFee: localStorage.getItem('min_shipping_fee') || '5.00',
      minPrice: localStorage.getItem('min_price') || '1.00'
    })

    console.log('[goods/query] 响应:', response)
  } catch (err) {
    // console.error('[goods/query] 请求失败:', err instanceof Error ? err.message : String(err))
    // 网络错误不中断流程
  }
}

/**
 * 处理堆积的书籍查询队列
 */
async function processPendingGoodsQueryQueue(): Promise<void> {
  if (pendingGoodsQueryQueue.length === 0) return

  console.log(`[goods/query] 开始处理堆积队列，数量: ${pendingGoodsQueryQueue.length}`)

  const queueCopy = [...pendingGoodsQueryQueue]
  pendingGoodsQueryQueue.length = 0 // 清空队列

  for (const item of queueCopy) {
    try {
      if (item.isSuit) {
        await queryGoodsApi(item.isbn, item.productId, item.quality, {
          bookName: item.bookName || '',
          author: item.author || '',
          publisher: item.publisher || ''
        })
      } else {
        await queryGoodsApi(item.isbn, item.productId, item.quality)
      }
    } catch (err) {
      // console.error('[goods/query] 处理队列项失败:', err)
    }
  }

  console.log('[goods/query] 队列处理完成')
}

// ========== 波次号识别相关 ==========
// 波次号正则：以1-4个字母开头，后跟数字（如 WH20260426001, WV001）
// 可根据后端实际格式调整
const WAVE_NO_PATTERN = /^[A-Za-z]{1,4}\d+$/

/**
 * 判断扫码内容是否可能是波次号
 * 排除纯数字（ISBN）和过短/过长的内容
 */
function isWaveNoPattern(code: string): boolean {
  const trimmed = code.trim()
  // 长度合理：波次号通常 4-30 个字符
  if (trimmed.length < 4 || trimmed.length > 30) return false
  return WAVE_NO_PATTERN.test(trimmed)
}

// 波次选择器相关状态
const selectedWaveNo = ref('')
const waveSearchLoading = ref(false)
const waveSearchResults = ref<any[]>([])

// 扫描模式:'scan' 表示仅拍照(使用扫码枪),'recognize' 表示拍照并识别条码
const scanMode = ref('scan')

// 波次号和条形码相关状态
const waveNo = ref('')
const barcodeImageBase64 = ref('')
const barcodeLoading = ref(false)

// 当前活跃波次状态(创建波次后持续存在,直到用户主动结束)
const currentWaveId = ref<number | null>(null)
const currentOrderId = ref<number | null>(null)
const currentWarehouseId = ref<number | null>(null)
// 当前波次已追加的商品列表(已废弃,改用 _committed 追踪)
// const waveItems = ref<Array<{ product_id: number; quantity: number; unit_price: number }>>([])

/** 注入 goosListRef（由 Wave.vue 通过 provide 提供） */
const injectedGoosListRef = inject<{ value: { getAllGoods: () => any[]; getUncommittedGoods?: () => any[]; markAllCommitted?: () => void; clearAll?: () => void } | null } | null>('goosListRef', null)

/** 获取 goosList 组件实例的辅助函数 */
const injectedCarRef = inject<{ value: { selectedWarehouseId: number | null; quality: number; selectedCarId: number | null; selectedCarCode: string | null; selectedCarCapacity: number | null } | null } | null>('carRef', null)

/** 获取 goosList 组件实例的辅助函数 */
function getGoosListInstance() {
  // 使用 inject 获取
  if (injectedGoosListRef?.value) {
    return injectedGoosListRef.value
  }
  return null
}

/** 监听仓库切换,自动恢复扫码输入框焦点 */
watch(
  () => props.warehouseId,
  () => {
    setTimeout(() => {
      const inputElement = document.querySelector('.hidden-scanner-input') as HTMLInputElement
      if (inputElement) {
        inputElement.focus()
      }
    }, 50)
  }
)

// 条形码图片 URL(computed)
const barcodeImageUrl = computed(() => {
  if (!barcodeImageBase64.value) return ''
  return `data:image/png;base64,${barcodeImageBase64.value}`
})

// 是否已有活跃波次
const hasActiveWave = computed(() => !!currentWaveId.value)

// 已扫描书籍总数
const scannedCount = computed(() => {
  const goosListInstance = getGoosListInstance()
  return goosListInstance?.getAllGoods?.()?.length ?? 0
})

// 待追加书籍数(未提交到波次的)
const uncommittedCount = computed(() => {
  const goosListInstance = getGoosListInstance()
  return goosListInstance?.getUncommittedGoods?.()?.length ?? 0
})

// 已入波次书籍数
const committedCount = computed(() => scannedCount.value - uncommittedCount.value)

// 输出分辨率(1:1 正方形)
const OUTPUT_SIZE = 1080

let stream: MediaStream | null = null

// 书籍信息
const bookInfo = ref<BookInfo | null>(null)

// ========== 仓库选择 end ==========

// Wave.vue 模板 ref,供读取用户自定义价格(ownPrice,分单位)
const cameraRef = ref<HTMLElement | null>(null)

/** 品质值：从 carRef inject 获取 */
const injectedQuality = computed(() => {
  const propQ = props.quality
  if (typeof propQ === 'number') return propQ
  const injectedQ = injectedCarRef?.value?.quality
  return typeof injectedQ === 'number' ? injectedQ : 85
})

/** 小车信息：从 carRef inject 获取 */
const injectedCarId = computed(() => injectedCarRef?.value?.selectedCarId ?? null)
const injectedCarCode = computed(() => injectedCarRef?.value?.selectedCarCode ?? null)
const injectedCarCapacity = computed(() => injectedCarRef?.value?.selectedCarCapacity ?? null)

// Wave.vue 组件实例（已废弃，改用 inject，保留引用避免 TypeScript 报错）
const waveComponent = ref<{ currentGoods: GoodsInfo | null } | null>(null)

// 价格格式化计算属性
const priceFormatted = computed({
  get: () => {
    if (!bookInfo.value) return ''
    return (bookInfo.value.price / 100).toFixed(2)
  },
  set: (value) => {
    if (bookInfo.value) {
      const price = parseFloat(value) * 100
      bookInfo.value.price = isNaN(price) ? 0 : Math.round(price)
    }
  }
})

/**
 * 规范化条码文本,去除空白和连字符
 */
function normalizeBarcode(barcode: string): string {
  return barcode.replace(/[\s-]+/g, '')
}

/**
 * 校验 ISBN-13
 */
function isValidISBN13(barcode: string): boolean {
  const digits = normalizeBarcode(barcode)
  if (!/^\d{13}$/.test(digits)) return false

  let sum = 0
  for (let i = 0; i < 13; i++) {
    const n = Number(digits[i])
    sum += i % 2 === 0 ? n : n * 3
  }
  return sum % 10 === 0
}

/**
 * 打开摄像头（失败后自动重试一次）
 * @param retryCount 当前重试次数（0=首次，1=重试）
 */
const openCamera = async (retryCount = 0) => {
  try {
    // 先释放可能残留的旧流
    if (stream) {
      stream.getTracks().forEach(track => track.stop())
      stream = null
    }
    stream = await navigator.mediaDevices.getUserMedia({
      video: {
        width: { ideal: 1920 },
        height: { ideal: 1080 },
        frameRate: { ideal: 30 }
      }
    })
    if (videoRef.value) {
      videoRef.value.srcObject = stream
      videoRef.value.onloadedmetadata = () => {
        if (videoRef.value) {
          actualWidth.value = videoRef.value.videoWidth
          actualHeight.value = videoRef.value.videoHeight
          console.log(`实际视频分辨率: ${actualWidth.value}x${actualHeight.value}`)
        }
      }
    }
    barcodeStatus.value = '摄像头已就绪'
  } catch (err) {
    const errMsg = err instanceof Error ? err.message : String(err)
    console.warn(`摄像头打开失败(第${retryCount + 1}次):`, errMsg)

    if (retryCount === 0) {
      // 首次失败，等待800ms后重试一次（可能是页面刷新后旧流未完全释放）
      barcodeStatus.value = '摄像头打开失败，正在重试...'
      await new Promise(resolve => setTimeout(resolve, 800))
      return openCamera(1)
    } else {
      // 重试仍失败，提示用户
      barcodeStatus.value = '摄像头打开失败，请检查权限或重启浏览器'
      // 区分错误类型给更精确的提示
      if (errMsg.includes('NotAllowedError') || errMsg.includes('PermissionDenied')) {
        barcodeStatus.value = '摄像头权限被拒绝，请在浏览器设置中允许摄像头访问'
      } else if (errMsg.includes('NotFoundError')) {
        barcodeStatus.value = '未检测到摄像头设备，请检查摄像头连接'
      } else if (errMsg.includes('NotReadableError') || errMsg.includes('TrackStartError')) {
        barcodeStatus.value = '摄像头被其他程序占用，请关闭其他使用摄像头的应用后重试'
      }
    }
  }
}

const closeCamera = () => {
  if (stream) {
    stream.getTracks().forEach(track => track.stop())
    stream = null
  }
}

/**
 * 仅拍照功能
 */
const takePhotoOnly = async () => {
  const video = videoRef.value
  const canvas = canvasRef.value
  if (!video || !canvas) {
    barcodeStatus.value = '摄像头未就绪'
    return
  }
  if (video.videoWidth === 0 || video.videoHeight === 0) {
    barcodeStatus.value = '视频流未就绪,请稍后重试'
    return
  }

  isProcessing.value = true
  barcodeStatus.value = '正在拍照...'
  barcodeSuccess.value = false
  barcodeError.value = false

  const ctx = canvas.getContext('2d')
  if (!ctx) {
    isProcessing.value = false
    return
  }

  canvas.width = OUTPUT_SIZE
  canvas.height = OUTPUT_SIZE

  const videoW = video.videoWidth
  const videoH = video.videoHeight
  let sx, sy, sWidth, sHeight
  if (videoW > videoH) {
    sHeight = videoH
    sWidth = videoH
    sx = (videoW - sWidth) / 2
    sy = 0
  } else {
    sWidth = videoW
    sHeight = videoW
    sx = 0
    sy = (videoH - sHeight) / 2
  }

  ctx.drawImage(video, sx, sy, sWidth, sHeight, 0, 0, OUTPUT_SIZE, OUTPUT_SIZE)

  const base64Data = canvas.toDataURL('image/jpeg', 0.9)
  const newFormData = {
    ...props.modelValue,
    photoSrc: base64Data
  }
  emit('update:modelValue', newFormData)

  barcodeStatus.value = '拍照成功'
  barcodeSuccess.value = true
  barcodeError.value = false
  isProcessing.value = false
}

/**
 * 重新拍照（仅替换照片预览，不新增到已扫描列表）
 */
const retakePhoto = async () => {
  const video = videoRef.value
  const canvas = canvasRef.value
  if (!video || !canvas) {
    barcodeStatus.value = '摄像头未就绪'
    return
  }
  if (video.videoWidth === 0 || video.videoHeight === 0) {
    barcodeStatus.value = '视频流未就绪,请稍后重试'
    return
  }

  isProcessing.value = true
  barcodeStatus.value = '正在重新拍照...'
  barcodeSuccess.value = false
  barcodeError.value = false

  const ctx = canvas.getContext('2d')
  if (!ctx) {
    isProcessing.value = false
    return
  }

  canvas.width = OUTPUT_SIZE
  canvas.height = OUTPUT_SIZE

  const videoW = video.videoWidth
  const videoH = video.videoHeight
  let sx, sy, sWidth, sHeight
  if (videoW > videoH) {
    sHeight = videoH
    sWidth = videoH
    sx = (videoW - sWidth) / 2
    sy = 0
  } else {
    sWidth = videoW
    sHeight = videoW
    sx = 0
    sy = (videoH - sHeight) / 2
  }

  ctx.drawImage(video, sx, sy, sWidth, sHeight, 0, 0, OUTPUT_SIZE, OUTPUT_SIZE)

  const base64Data = canvas.toDataURL('image/jpeg', 0.9)

  // 上传封面图片到图片服务(使用最后一次扫码的ISBN)
  const isbn = lastScannedIsbn.value
  const bookPicS = lastBookPicS.value

  console.log('[重新拍照] ISBN:', isbn, '| lastScannedIsbn:', lastScannedIsbn.value)

  if (!isbn) {
    console.warn('[重新拍照] ISBN 为空,跳过上传')
    barcodeSuccess.value = true
    barcodeError.value = false
    isProcessing.value = false
    return
  }

  // 价格来源:用户通过 priceFormatted 输入或 OCR/查询结果（已转为分）
  const productPrice = bookInfo.value?.price ?? 0

  // 从已扫描列表中查找已有 productId，用于更新而非新建
  let existingProductId = 0
  const goosListRef_tmp = injectedGoosListRef?.value as { goodsList?: any[] } | null | undefined
  if (goosListRef_tmp?.goodsList) {
    const item = goosListRef_tmp.goodsList.find((g: any) => g.isbn === isbn)
    if (item?.productId) {
      existingProductId = item.productId
    }
  }

  // 第一步:调用 /api/product/save 保存（有 productId 则更新，无则新建）
  const { updateProductLiveImageAsPddUrl } = await import('@/api/product')
  const productId = await saveProductBeforeUpload({
    isbn,
    name: bookInfo.value?.bookName || '',
    price: productPrice,
    bookPicS,
    appearance: injectedQuality.value as number | undefined,
    binding: bookInfo.value?.binding,
    pageCount: bookInfo.value?.pageCount,
    wordCount: bookInfo.value?.wordCount,
    productId: existingProductId
  })

  // 第二步:上传实拍图片
  const random5 = String(Math.floor(Math.random() * 100000)).padStart(5, '0')
  const pictureName = productId ? `${productId}-${isbn}-${random5}.jpg` : null
  if (pictureName) {
    lastLivingPictureName.value = pictureName
    await uploadLivingPicture(base64Data, pictureName, isbn)

    // 第三步:上传拼多多图片空间
    let pddUrl = ''
    try {
      const CryptoJS = (await import('crypto-js')).default
      const timestamp = String(Math.floor(Date.now() / 1000))
      const signParams: Record<string, string> = {
        type: 'pdd.goods.filespace.image.upload',
        access_token: '5f7dcc92211549f3b8b05451288a92fa9546732d',
        timestamp,
        client_id: '203c5a7ba8bd4b8488d5e26f93052642',
        data_type: 'JSON'
      }
      const sortedKeys = Object.keys(signParams).sort()
      let signStr = '892ffaa86e12b7a3d8d2942b669d9aa520ad8179'
      for (const key of sortedKeys) {
        signStr += key + signParams[key]
      }
      signStr += '892ffaa86e12b7a3d8d2942b669d9aa520ad8179'
      const sign = CryptoJS.MD5(signStr).toString().toUpperCase()

      const pddFormData = new FormData()
      pddFormData.append('type', signParams.type)
      pddFormData.append('access_token', signParams.access_token)
      pddFormData.append('timestamp', signParams.timestamp)
      pddFormData.append('client_id', signParams.client_id)
      pddFormData.append('data_type', signParams.data_type)
      pddFormData.append('sign', sign)

      const base64 = base64Data.replace(/^data:image\/\w+;base64,/, '')
      const binaryStr = atob(base64)
      const bytes = new Uint8Array(binaryStr.length)
      for (let i = 0; i < binaryStr.length; i++) {
        bytes[i] = binaryStr.charCodeAt(i)
      }
      const blob = new Blob([bytes], { type: 'image/jpeg' })
      pddFormData.append('file', blob, `${isbn}.jpg`)

      const pddRes = await axios.post('https://gw-upload.pinduoduo.com/api/upload', pddFormData, {
        headers: { 'Content-Type': 'multipart/form-data' },
        timeout: 30000
      })
      const pddResult = pddRes.data
      if (pddResult?.goods_filespace_image_upload_response?.image_url) {
        pddUrl = pddResult.goods_filespace_image_upload_response.image_url
      } else if (pddResult?.goods_filespace_image_upload_response?.url) {
        pddUrl = pddResult.goods_filespace_image_upload_response.url
      }
    } catch (pddErr) {
      console.warn('[重新拍照] PDD上传异常:', pddErr instanceof Error ? pddErr.message : String(pddErr))
    }

    // 第四步:更新商品 live_image
    await updateProductLiveImageAsPddUrl(productId, pddUrl, {
      barcode: isbn,
      name: bookInfo.value?.bookName || '',
      price: productPrice,
      appearance: injectedQuality.value
    })

    const photoUrl = pddUrl

    // 仅更新 imageCut 预览（不触发 photo-preview-confirm，不新增到已扫描列表）
    emit('update:modelValue', {
      ...props.modelValue,
      photoSrc: photoUrl
    })

    if (bookInfo.value) {
      bookInfo.value.capturedPhoto = photoUrl
    }

    // 更新已扫描列表中的拍摄图片
    const goosListRef_tmp = injectedGoosListRef?.value as { goodsList?: any[] } | null | undefined
    if (goosListRef_tmp?.goodsList) {
      const item = goosListRef_tmp.goodsList.find((g: any) => g.isbn === isbn)
      if (item) {
        item.capturedPhoto = photoUrl
      }
    }

    barcodeStatus.value = '重新拍照完成'
  } else {
    console.warn('[重新拍照] product ID 为空,跳过图片上传')
  }

  barcodeSuccess.value = true
  barcodeError.value = false
  isProcessing.value = false

  // 恢复扫码输入框焦点
  setTimeout(() => {
    const inputElement = document.querySelector('.hidden-scanner-input') as HTMLInputElement
    if (inputElement && !isOtherInputFocused()) {
      inputElement.focus()
    }
  }, 100)
}

/**
 * 拍照预览功能
 * 拍照并确认添加到已扫描书籍列表
 */
const takePhotoPreview = async () => {
  const video = videoRef.value
  const canvas = canvasRef.value
  if (!video || !canvas) {
    barcodeStatus.value = '摄像头未就绪'
    return
  }
  if (video.videoWidth === 0 || video.videoHeight === 0) {
    barcodeStatus.value = '视频流未就绪,请稍后重试'
    return
  }

  isProcessing.value = true
  barcodeStatus.value = '正在拍照预览...'
  barcodeSuccess.value = false
  barcodeError.value = false

  const ctx = canvas.getContext('2d')
  if (!ctx) {
    isProcessing.value = false
    return
  }

  canvas.width = OUTPUT_SIZE
  canvas.height = OUTPUT_SIZE

  const videoW = video.videoWidth
  const videoH = video.videoHeight
  let sx, sy, sWidth, sHeight
  if (videoW > videoH) {
    sHeight = videoH
    sWidth = videoH
    sx = (videoW - sWidth) / 2
    sy = 0
  } else {
    sWidth = videoW
    sHeight = videoW
    sx = 0
    sy = (videoH - sHeight) / 2
  }

  ctx.drawImage(video, sx, sy, sWidth, sHeight, 0, 0, OUTPUT_SIZE, OUTPUT_SIZE)

  const base64Data = canvas.toDataURL('image/jpeg', 0.9)

  // 上传封面图片到图片服务(使用最后一次扫码的ISBN)
  const isbn = lastScannedIsbn.value
  const bookPicS = lastBookPicS.value

  console.log('=============测试实拍图片:', isbn, '| lastBookPicS:', lastBookPicS.value)

  console.log('[上传实拍] ISBN:', isbn, '| lastScannedIsbn:', lastScannedIsbn.value)

  if (!isbn) {
    console.warn('[上传实拍] ISBN 为空,跳过上传')
    barcodeSuccess.value = true
    barcodeError.value = false
    isProcessing.value = false
    return
  }

  // 价格来源:用户通过 priceFormatted 输入或 OCR/查询结果（已转为分）
  const productPrice = bookInfo.value?.price ?? 0
  console.log('[上传实拍] 价格字段 - bookInfo price:', bookInfo.value?.price, '| 最终取值:', productPrice)

  // 第一步:调用 /api/product/save 保存商品,获取 product ID
  const { saveProduct, updateProductLiveImage, updateProductLiveImageForTest, updateProductLiveImageAsPddUrl } = await import('@/api/product')
  const productId = await saveProductBeforeUpload({
    isbn,
    name: bookInfo.value?.bookName || '',
    price: productPrice, // 单位:分,整数类型
    bookPicS,
    appearance: injectedQuality.value as number | undefined,
    binding: bookInfo.value?.binding,
    pageCount: bookInfo.value?.pageCount,
    wordCount: bookInfo.value?.wordCount
  })

  // 第二步:上传实拍图片,文件名为 {id}-{isbn}-{5位随机数}.jpg
  const random5 = String(Math.floor(Math.random() * 100000)).padStart(5, '0')
  const pictureName = productId ? `${productId}-${isbn}-${random5}.jpg` : null
  if (pictureName) {
    lastLivingPictureName.value = pictureName
    await uploadLivingPicture(base64Data, pictureName, isbn)
    console.log('[实拍] 图片名:', pictureName, '| 地址: https://shxy.image.yushutx.com/living-picture/' + pictureName)

    // 第三步半:将视频帧图片上传拼多多图片空间,获取 pddUrl
    let pddUrl = ''
    try {
      const CryptoJS = (await import('crypto-js')).default
      const timestamp = String(Math.floor(Date.now() / 1000))
      const signParams: Record<string, string> = {
        type: 'pdd.goods.filespace.image.upload',
        access_token: '5f7dcc92211549f3b8b05451288a92fa9546732d',
        timestamp,
        client_id: '203c5a7ba8bd4b8488d5e26f93052642',
        data_type: 'JSON'
      }
      // 生成签名
      const sortedKeys = Object.keys(signParams).sort()
      let signStr = '892ffaa86e12b7a3d8d2942b669d9aa520ad8179'
      for (const key of sortedKeys) {
        signStr += key + signParams[key]
      }
      signStr += '892ffaa86e12b7a3d8d2942b669d9aa520ad8179'
      const sign = CryptoJS.MD5(signStr).toString().toUpperCase()

      // 构造 FormData
      const pddFormData = new FormData()
      pddFormData.append('type', signParams.type)
      pddFormData.append('access_token', signParams.access_token)
      pddFormData.append('timestamp', signParams.timestamp)
      pddFormData.append('client_id', signParams.client_id)
      pddFormData.append('data_type', signParams.data_type)
      pddFormData.append('sign', sign)

      // base64 转 Blob
      const base64 = base64Data.replace(/^data:image\/\w+;base64,/, '')
      const binaryStr = atob(base64)
      const bytes = new Uint8Array(binaryStr.length)
      for (let i = 0; i < binaryStr.length; i++) {
        bytes[i] = binaryStr.charCodeAt(i)
      }
      const blob = new Blob([bytes], { type: 'image/jpeg' })
      pddFormData.append('file', blob, `${isbn}.jpg`)

      const pddRes = await axios.post('https://gw-upload.pinduoduo.com/api/upload', pddFormData, {
        headers: { 'Content-Type': 'multipart/form-data' },
        timeout: 30000
      })
      const pddResult = pddRes.data
      console.log('[PDD上传] 响应:', JSON.stringify(pddResult))
      if (pddResult?.goods_filespace_image_upload_response?.image_url) {
        pddUrl = pddResult.goods_filespace_image_upload_response.image_url
        console.log('[PDD上传] 成功,图片地址:', pddUrl)
      } else if (pddResult?.goods_filespace_image_upload_response?.url) {
        pddUrl = pddResult.goods_filespace_image_upload_response.url
        console.log('[PDD上传] 成功,图片地址(url字段):', pddUrl)
      } else if (pddResult?.error_response) {
        errorBlocker?.block(`拍照失败`)
        console.warn('[PDD上传] 失败:', pddResult.error_response.error_msg || JSON.stringify(pddResult.error_response))
      }
    } catch (pddErr) {
      errorBlocker?.block(`拍照失败`)
      console.warn('[PDD上传] 异常:', pddErr instanceof Error ? pddErr.message : String(pddErr))
    }

    console.log('[实拍] 上传完成,开始更新商品图片 - productId:', productId, '| bookPicS:', pddUrl)
    // 第四步:用第一步拿到的 productId 更新商品的 live_image 为 pdd 图片地址（测试阶段使用bookPicS）
    await updateProductLiveImageAsPddUrl(productId, pddUrl || bookPicS  , {
      barcode: isbn,
      name: bookInfo.value?.bookName || '',
      price: productPrice, // 单位:分,整数类型
      appearance: injectedQuality.value
    })

    // 第五步:第二次调用 /api/product/save 后,调用 goods/query
    // 套装书（isSuit=true）需传递 bookName/author/publisher 走套装书专用核价
    const isSuitBook = !!(bookInfo.value?.isSuit)
    if (hasActiveWave.value) {
      if (isSuitBook) {
        await queryGoodsApi(isbn, productId, String(injectedQuality.value), {
          bookName: bookInfo.value?.bookName || '',
          author: bookInfo.value?.author || '',
          publisher: bookInfo.value?.publisher || ''
        })
      } else {
        await queryGoodsApi(isbn, productId, String(injectedQuality.value))
      }
    } else {
      if (isSuitBook) {
        pendingGoodsQueryQueue.push({
          isbn,
          outId: productId.toString(),
          quality: String(injectedQuality.value),
          productId,
          bookName: bookInfo.value?.bookName || '',
          author: bookInfo.value?.author || '',
          publisher: bookInfo.value?.publisher || '',
          isSuit: true,
        })
      } else {
        pendingGoodsQueryQueue.push({
          isbn,
          outId: productId.toString(),
          quality: String(injectedQuality.value),
          productId
        })
      }
      console.log(`[goods/query] 无活跃波次,暂存队列(当前数量: ${pendingGoodsQueryQueue.length})`)
    }

    console.log('=======实拍图片地址: ', pddUrl)
    // 第四步:上传完成后再 emit,使用网络 URL 替代 base64
    const photoUrl = pddUrl

    // 更新 imageCut 预览(网络地址)
    emit('update:modelValue', {
      ...props.modelValue,
      photoSrc: photoUrl
    })

    // // 把 URL 回填到 bookInfo 并通知已扫描书籍列表(只 emit photo-preview-confirm,不 emit book-info-update,避免触发 handleBookInfoUpdate 再次 +1)
    if (bookInfo.value) {
      bookInfo.value.capturedPhoto = photoUrl

      // 获取商品售价
      let salePrice = null
      if (productId) {
        try {
          const req = (await import('@/utils/request')).default
          const listRes = await req.get('/product/list', {
            params: { status: 1, page: 1, page_size: 20, 'ids[0]': productId }
          })
          console.log('[条码扫描] /product/list 响应:', listRes)
          const list = listRes?.data?.list || listRes?.data?.data || listRes?.data?.rows || []
          console.log('[条码扫描] 提取的列表:', list, '第一条:', list[0])
          if (list.length > 0) {
            salePrice = list[0].sale_price
            console.log('[条码扫描] salePrice:', salePrice)
          }
        } catch (e) {
          console.warn('[条码扫描] 获取 sale_price 失败:', e)
        }
      }

      // 强制 ownPrice 为 0，防止被意外修改
      if (bookInfo.value) {
        bookInfo.value.ownPrice = 0
      }


      emit('photo-preview-confirm', {
        bookInfo: bookInfo.value,
        photoSrc: photoUrl,
        productId,
        salePrice
      })
      barcodeStatus.value = '已添加到已扫描书籍'

      // 自动追加到当前波次(如果有活跃波次)
      // 必须等待 nextTick,确保 GoosList 的 watch 已执行并将新书加入列表
      if (hasActiveWave.value) {
        await nextTick()
        await handleAppendToWave()
      }
    } else {
      barcodeStatus.value = '拍照预览成功(无书籍信息)'
    }
  } else {
    console.warn('[实拍] product ID 为空,跳过图片上传')
  }

  barcodeSuccess.value = true
  barcodeError.value = false
  isProcessing.value = false

  ElMessage.success({ message: `拍照成功`, duration: 1000, customClass: 'scan-success-message' })
  // 恢复扫码输入框焦点,准备下一次扫描
  setTimeout(() => {
    const inputElement = document.querySelector('.hidden-scanner-input') as HTMLInputElement
    if (inputElement && !isOtherInputFocused()) {
      inputElement.focus()
    }
  }, 100)
}
/**
 * 根据当前模式执行拍照动作
 * scan 模式: 调用 takePhotoPreview (原有逻辑)
 * recognize 模式: 调用 takePhotoOcr (OCR识别)
 */
const handlePhotoAction = async () => {
  if (isPageLocked.value) {
    return
  }
  if (!checkCarSelected()) return

  // 检查波次状态：有活跃波次且已开始拣货时阻止拍照
  if (hasActiveWave.value && currentWaveId.value) {
    try {
      const res = await getWaveStatusById(currentWaveId.value)
      const status = res?.data?.status
      if (status !== undefined && status !== 1 && status !== 2) {
        errorBlocker?.block('该波次已开始拣货不能继续追加商品，请扫描波次码清空当前波次，重新扫描书籍')
        return
      }
    } catch (err) {
      console.error('[拍照] 波次状态查询失败:', err)
    }
  }

  // 检查小车容量：当前已扫描数 >= 容量时阻止拍照
  const capacity = injectedCarCapacity.value
  if (capacity !== null && scannedCount.value >= capacity) {
    errorBlocker?.block('小车容量已满，请创建波次上书或者创建新波次')
    return
  }

  if (scanMode.value === 'recognize') {
    await takePhotoOcr()
  } else {
    await takePhotoPreview()
  }
}

/**
 * 拍照并调用 OCR 识别
 */
const takePhotoOcr = async () => {
  const video = videoRef.value
  const canvas = canvasRef.value
  if (!video || !canvas) {
    barcodeStatus.value = '摄像头未就绪'
    return
  }
  if (video.videoWidth === 0 || video.videoHeight === 0) {
    barcodeStatus.value = '视频流未就绪,请稍后重试'
    return
  }

  isProcessing.value = true
  barcodeStatus.value = '正在拍照识别...'

  const ctx = canvas.getContext('2d')
  if (!ctx) {
    isProcessing.value = false
    return
  }

  // 绘制视频帧到 canvas
  canvas.width = OUTPUT_SIZE
  canvas.height = OUTPUT_SIZE

  const videoW = video.videoWidth
  const videoH = video.videoHeight
  let sx, sy, sWidth, sHeight
  if (videoW > videoH) {
    sHeight = videoH
    sWidth = videoH
    sx = (videoW - sWidth) / 2
    sy = 0
  } else {
    sWidth = videoW
    sHeight = videoW
    sx = 0
    sy = (videoH - sHeight) / 2
  }

  ctx.drawImage(video, sx, sy, sWidth, sHeight, 0, 0, OUTPUT_SIZE, OUTPUT_SIZE)

  // 保存捕获的 base64 图片（用于后续上传和预览）
  const base64Data = canvas.toDataURL('image/jpeg', 0.9)
  ocrCapturedBase64.value = base64Data

  // 转换为 Blob 并调用 OCR API
  try {
    const blob = await new Promise<Blob>((resolve, reject) => {
      canvas.toBlob((b) => {
        if (b) resolve(b)
        else reject(new Error('Canvas toBlob failed'))
      }, 'image/jpeg', 0.9)
    })

    ocrLoading.value = true
    const result = await ocrImage(blob)
    ocrLoading.value = false

    if (result.code === 200 && result.data?.success) {
      ocrResult.value = result.data
      ocrDialogVisible.value = true
      barcodeStatus.value = 'OCR识别成功,请选择字段分配'
    } else {
      barcodeStatus.value = 'OCR识别失败: ' + (result.data?.texts?.join(', ') || '未知错误')
      barcodeError.value = true
    }
  } catch (err) {
    // console.error('[OCR] 识别失败:', err)
    barcodeStatus.value = 'OCR识别失败'
    barcodeError.value = true
    ocrLoading.value = false
  } finally {
    isProcessing.value = false
  }
}

/**
 * 处理 OCR 结果分配
 * @param assignments - 字段分配对象 { field: value }
 */
const handleOcrAssign = async (assignments: Record<string, string>) => {
  if (!bookInfo.value) {
    // 如果 bookInfo 为空,先创建一个基础对象
    bookInfo.value = {
      bookName: '',
      author: '',
      publisher: '',
      publishDate: '',
      binding: '',
      price: 0,
      pageCount: 0,
      wordCount: 0,
      totalBook: 1,
      ownPrice: 0
    }
  }

  // 映射字段名
  const fieldMap: Record<string, keyof NonNullable<typeof bookInfo.value>> = {
    bookName: 'bookName',
    author: 'author',
    publisher: 'publisher',
    publishDate: 'publishDate',
    binding: 'binding',
    price: 'price',
    pageCount: 'pageCount',
    wordCount: 'wordCount'
  }

  // 应用所有分配
  const assignedFields: string[] = []
  for (const [field, value] of Object.entries(assignments)) {
    const targetField = fieldMap[field]
    if (targetField && bookInfo.value) {
      // 特殊处理 price 字段（需要转换为数字）
      if (field === 'price') {
        // 价格可能包含 '元' 或其他字符，提取数字
        const priceNum = parseFloat(value.replace(/[^\d.]/g, ''))
        if (!isNaN(priceNum)) {
          ; (bookInfo.value as any)[targetField] = Math.round(priceNum * 100) // 转换为分
        }
      } else {
        ; (bookInfo.value as any)[targetField] = value
      }
      assignedFields.push(field)
    }
  }

  // 关闭弹窗
  ocrDialogVisible.value = false

  // ========== 后续逻辑与 takePhotoPreview 相同 ==========
  const base64Data = ocrCapturedBase64.value
  if (!base64Data) {
    barcodeStatus.value = 'OCR 图片丢失，请重新拍照'
    barcodeError.value = true
    return
  }

  isProcessing.value = true
  barcodeStatus.value = '正在获取商品编码...'

  // OCR 模式：先调用 /api/getProCode 获取商品编码
  const { getProCode } = await import('@/api/product')
  const proCode = await getProCode({
    book_name: bookInfo.value?.bookName || '',
    author: bookInfo.value?.author || '',
    publisher: bookInfo.value?.publisher || ''
  })
  // 使用 getProCode 返回的编码作为 barcode，失败时回退到占位符
  const isbn = proCode || lastScannedIsbn.value || `OCR-${Date.now()}`
  console.log('[OCR] getProCode 返回:', proCode, '→ 使用 barcode:', isbn)

  // OCR 模式：使用 OCR 识别的定价（已转为分）
  const productPrice = bookInfo.value?.price ?? 0

  // 第一步：先上传截取的视频帧到图片服务，文件名为 {barcode}.jpg
  const coverPictureName = `${isbn}.jpg`
  console.log('[OCR上传封面] 开始上传, 文件名:', coverPictureName)
  await uploadLivingPicture(base64Data, coverPictureName, isbn)
  const coverImageUrl = `https://shxy.image.yushutx.com/living-picture/${coverPictureName}`
  console.log('[OCR上传封面] 完成, URL:', coverImageUrl)

  // 第二步：调用 /api/product/save 保存商品，使用上传后的 URL 作为 live_image[0]
  barcodeStatus.value = '正在保存商品...'
  const { saveProduct, updateProductLiveImage } = await import('@/api/product')
  const productId = await saveProductBeforeUpload({
    isbn,
    name: bookInfo.value?.bookName || '',
    price: productPrice,
    bookPicS: coverImageUrl,
    appearance: injectedQuality.value as number | undefined,
    binding: bookInfo.value?.binding,
    pageCount: bookInfo.value?.pageCount,
    wordCount: bookInfo.value?.wordCount
  })

  // 第三步：上传实拍图片
  const random5 = String(Math.floor(Math.random() * 100000)).padStart(5, '0')
  const pictureName = productId ? `${productId}-${isbn}-${random5}.jpg` : null

  if (pictureName) {
    lastLivingPictureName.value = pictureName
    await uploadLivingPicture(base64Data, pictureName, isbn)

    // 第四步：更新商品的 live_image 为实拍图片地址
    await updateProductLiveImage(productId, pictureName, {
      barcode: isbn,
      name: bookInfo.value?.bookName || '',
      price: productPrice,
      appearance: injectedQuality.value
    })

    // 第四步半:第二次调用 /api/product/save 后,调用 goods/query
    if (hasActiveWave.value) {
      await queryGoodsApi(isbn, productId, String(injectedQuality.value))
    } else {
      pendingGoodsQueryQueue.push({
        isbn,
        outId: productId.toString(),
        quality: String(injectedQuality.value),
        productId
      })
      console.log(`[goods/query] 无活跃波次,暂存队列(当前数量: ${pendingGoodsQueryQueue.length})`)
    }

    // 第五步：上传完成后 emit，使用网络 URL
    const photoUrl = `https://shxy.image.yushutx.com/living-picture/${pictureName}`

    // 更新照片预览
    emit('update:modelValue', {
      ...props.modelValue,
      photoSrc: photoUrl
    })

    // 通知已扫描书籍列表
    if (bookInfo.value) {
      bookInfo.value.capturedPhoto = photoUrl

      // 获取商品售价
      let salePrice = null
      if (productId) {
        try {
          const req = (await import('@/utils/request')).default
          const listRes = await req.get('/product/list', {
            params: { status: 1, page: 1, page_size: 20, 'ids[0]': productId }
          })
          console.log('[OCR] /product/list 响应:', listRes)
          const list = listRes?.data?.list || listRes?.data?.data || listRes?.data?.rows || []
          console.log('[OCR] 提取的列表:', list, '第一条:', list[0])
          if (list.length > 0) {
            salePrice = list[0].sale_price
            console.log('[OCR] salePrice:', salePrice)
          }
        } catch (e) {
          // console.error('[OCR] 获取商品售价失败:', e)
        }
      }

      emit('photo-preview-confirm', {
        bookInfo: bookInfo.value,
        photoSrc: photoUrl,
        productId,
        salePrice
      })
      barcodeStatus.value = '已添加到已扫描书籍'

      // 自动追加到当前波次（如果有活跃波次）
      if (hasActiveWave.value) {
        await nextTick()
        await handleAppendToWave()
      }
    } else {
      barcodeStatus.value = 'OCR 识别成功（无书籍信息）'
    }
  } else {
    barcodeStatus.value = '商品保存失败，无法上传图片'
    barcodeError.value = true
  }

  barcodeSuccess.value = true
  barcodeError.value = false
  isProcessing.value = false

  // 清空 OCR 捕获的图片
  ocrCapturedBase64.value = ''

  // 恢复扫码输入框焦点
  setTimeout(() => {
    const inputElement = document.querySelector('.hidden-scanner-input') as HTMLInputElement
    if (inputElement && !isOtherInputFocused()) {
      inputElement.focus()
    }
  }, 100)
}

/**
 * 保存商品(/api/product/save)
 * 参数:name, barcode, price, live_image[0] = book_pic_s
 * 返回:product ID(成功时),null(失败时)
 */
async function saveProductBeforeUpload({ isbn, name, price, bookPicS, appearance, binding, pageCount, wordCount, productId: existingId = 0 }) {
  try {
    const { saveProduct } = await import('@/api/product')
    // 如有已有 productId 则更新，否则新建（id=0）
    const id = existingId
    const res = await saveProduct({
      id,
      category_id: 1,
      standard_product_id: 1,
      name,
      barcode: isbn,
      price,
      is_batch_managed: 0,
      is_shelf_life_managed: 0,
      status: 1,
      appearance: appearance ?? props.quality ?? 85,
      live_image: bookPicS ? [bookPicS] : [],
      binding: binding ?? undefined,
      page_count: pageCount ? parseInt(String(pageCount)) : undefined,
      word_count: wordCount ? parseInt(String(wordCount)) : undefined
    })
    const productId = res?.data?.id ?? null
    console.log(res.data)
    console.log('[保存商品] 成功, ID:', productId, '| ISBN:', isbn)
    return productId
  } catch (err) {
    console.warn('[保存商品] 失败:', err instanceof Error ? err.message : String(err))
    return null
  }
}

/**
 * 创建波次(第一步:调用 /api/purchase-order/create-with-wave)
 */
async function handleCreateWave() {
  if (!checkCarSelected()) return

  try {
    // 获取仓库ID:优先从 props(Wave.vue 传入的 computed 已 unwrap)
    let warehouseId: number | null = props.warehouseId ?? null

    // 备用方案:从 carRef inject 获取
    if (!warehouseId && injectedCarRef?.value) {
      const wh = injectedCarRef.value.selectedWarehouseId
      warehouseId = wh
      console.log('[创建波次] 从 injectedCarRef 获取 warehouseId:', warehouseId)
    }

    console.log('[创建波次] 最终 warehouseId:', warehouseId)
    if (!warehouseId) {
      ElMessage.warning({ message: '请先选择仓库', duration: 1000, customClass: 'scan-warning-message' })
      return
    }

    // 构建 items 数组:遍历 goosList 每一条记录,直接使用每条记录的 productId
    const allGoods = getGoosListInstance()?.getAllGoods?.() ?? []
    console.log('[创建波次] allGoods from getAllGoods:', JSON.stringify(allGoods))
    const items: { product_id: number; quantity: number; unit_price: number }[] = []
    for (const g of allGoods) {
      const productId = g.id
      if (!productId) continue
      console.log(`[创建波次] item: productId=${productId}, totalBook=${g.totalBook}, ownPrice=${g.ownPrice}`)
      items.push({
        product_id: productId,
        quantity: g.totalBook,
        unit_price: g.ownPrice
      })
    }
    console.log('[创建波次] final items:', JSON.stringify(items))

    if (items.length === 0) {
      ElMessage.warning({
        message: '没有已扫描的书籍<br>请先扫描一本书籍拍照扫描后再创建波次',
        duration: 1000,
        customClass: 'scan-warning-message',
        dangerouslyUseHTMLString: true   // 添加这一行
      })
      return
    }


    const carId = injectedCarId.value
    const carCode = injectedCarCode.value
    const carCapacity = injectedCarCapacity.value
    console.log('[创建波次] carId:', carId, '| carCode:', carCode, '| carCapacity:', carCapacity)

    // 调用创建波次接口
    const res = await createPurchaseOrderWithWave({

      warehouse_id: warehouseId,
      supplier_id: 1,
      expected_arrival_date: '',
      remark: '',
      items,
      direction: 1,
      car_id: carId,
      car_code: carCode
    })

    console.log('[创建波次] 响应:', res.data)

    // 第二步:释放波次 /api/wave/release
    const waveId = res.data?.wave_id
    const purchaseOrderId = res.data?.order_id
    if (waveId && purchaseOrderId) {
      try {
        // 获取小车信息
        const carId = injectedCarId.value
        const carCode = injectedCarCode.value
        const carCapacity = injectedCarCapacity.value
        console.log('[创建波次] carId:', carId, '| carCode:', carCode, '| carCapacity:', carCapacity)

        const releaseRes = await releaseWave({
          wave_id: waveId,
          related_order_id: purchaseOrderId,
          items,
          car_id: carId,
          car_code: carCode
        })
        console.log('[释放波次] 响应:', releaseRes.data)

        // 保存波次号用于生成条形码
        const releasedWaveNo = releaseRes.data?.wave_no || releaseRes.wave_no
        if (releasedWaveNo) {
          waveNo.value = releasedWaveNo
          // 保存当前活跃波次状态,后续可追加
          currentWaveId.value = waveId
          currentOrderId.value = purchaseOrderId
          currentWarehouseId.value = warehouseId
          // 处理堆积的 goods/query 队列
          await processPendingGoodsQueryQueue()
          // 标记所有书籍为已提交到波次
          const goosListInstance = getGoosListInstance()
          goosListInstance?.markAllCommitted?.()
          ElMessage.success({ message: `波次创建并释放成功,波次号:${releasedWaveNo}`, duration: 1000, customClass: 'scan-success-message' })
        } else {
          ElMessage.success({ message: '波次创建并释放成功', duration: 1000, customClass: 'scan-success-message' })
        }
      } catch (releaseErr) {
        console.warn('[释放波次] 失败:', releaseErr instanceof Error ? releaseErr.message : String(releaseErr))
        ElMessage.warning({ message: '波次已创建,但释放失败: ' + (releaseErr instanceof Error ? releaseErr.message : String(releaseErr)), duration: 1000, customClass: 'scan-warning-message' })
      }
    } else {
      ElMessage.success({ message: '波次创建成功,但响应中缺少 wave_id 或 order_id,无法自动释放', duration: 1000, customClass: 'scan-success-message' })
    }
  } catch (err) {
    console.warn('[创建波次] 失败:', err instanceof Error ? err.message : String(err))
    errorBlocker?.block('波次创建失败: ' + (err instanceof Error ? err.message : String(err)))
  }
}

/**
 * 向当前活跃波次追加商品
 * 调用 /api/wave/release,传递 currentWaveId 和 currentOrderId,后端会追加到同一波次
 * 只传递尚未提交到波次的书籍(通过 _committed 标记追踪)
 */
async function handleAppendToWave() {
  if (!checkCarSelected()) return
  if (!currentWaveId.value || !currentOrderId.value) {
    ElMessage.warning({ message: '当前没有活跃波次,请先创建波次', duration: 1000, customClass: 'scan-warning-message' })
    return
  }

  // 检查波次状态：已开始拣货时阻止追加
  try {
    const res = await getWaveStatusById(currentWaveId.value)
    const status = res?.data?.status
    if (status !== undefined && status !== 1 && status !== 2) {
      errorBlocker?.block('该波次已开始拣货不能继续追加商品，请扫描波次码清空当前波次，重新扫描书籍')
      return
    }
  } catch (err) {
    console.error('[追加波次] 波次状态查询失败:', err)
  }

  try {
    // 获取未提交到波次的书籍
    const goosListInstance = getGoosListInstance()
    const uncommittedGoods = goosListInstance?.getUncommittedGoods?.() ?? []

    if (uncommittedGoods.length === 0) {
      ElMessage.warning({ message: '没有新扫描的书籍可追加', duration: 1000, customClass: 'scan-warning-message' })
      return
    }

    // 构建 items 数组:每个未提交的书籍都作为独立条目,直接使用每条记录的 productId
    console.log('[追加波次] uncommittedGoods:', JSON.stringify(uncommittedGoods))
    const newItems: { product_id: number; quantity: number; unit_price: number }[] = []
    for (const g of uncommittedGoods) {
      const productId = g.id
      if (!productId) continue
      console.log(`[追加波次] item: productId=${productId}, totalBook=${g.totalBook}, ownPrice=${g.ownPrice}`)
      newItems.push({
        product_id: productId,
        quantity: g.totalBook,
        unit_price: g.ownPrice
      })
    }
    console.log('[追加波次] final newItems:', JSON.stringify(newItems))

    if (newItems.length === 0) {
      ElMessage.warning({ message: '没有可追加的书籍(商品未保存)', duration: 1000, customClass: 'scan-warning-message' })
      return
    }

    // 获取小车信息
    const carId = injectedCarId.value
    const carCode = injectedCarCode.value
    const carCapacity = injectedCarCapacity.value
    console.log('[追加波次] carId:', carId, '| carCode:', carCode, '| carCapacity:', carCapacity)

    const releaseRes = await releaseWave({
      wave_id: currentWaveId.value,
      related_order_id: currentOrderId.value,
      items: newItems,
      car_id: carId,
      car_code: carCode
    })
    console.log('[追加波次] 响应:', releaseRes.data)

    // 标记所有书籍为已提交
    goosListInstance?.markAllCommitted?.()

    const releasedWaveNo = releaseRes.data?.wave_no || releaseRes.wave_no
    if (releasedWaveNo) {
      waveNo.value = releasedWaveNo
    }

    ElMessage.success({ message: `已追加到波次 ${releasedWaveNo || ''},新增 ${newItems.length} 本书籍`, duration: 1000, customClass: 'scan-success-message' })
  } catch (err) {
    console.warn('[追加波次] 失败:', err instanceof Error ? err.message : String(err))
    errorBlocker?.block('追加波次失败: ' + (err instanceof Error ? err.message : String(err)))
  }
}

/**
 * 创建新波次:清空所有数据,重新开始
 * 点击后会清空扫描列表、波次状态,用户需要重新扫描后创建新波次
 */
async function handleCreateNewWave() {
  if (!checkCarSelected()) return

  // 1. 清空书籍列表(通过 waveComponent.goosListRef,注意 goosListRef 是 Vue ref,需 .value)
  const goosListInstance = getGoosListInstance()
  if (goosListInstance && typeof goosListInstance.clearAll === 'function') {
    goosListInstance.clearAll()
  }

  // 2. 清空 isbnProductIdMap
  isbnProductIdMap.clear()

  // 3. 清空波次状态
  currentWaveId.value = null
  currentOrderId.value = null
  currentWarehouseId.value = null
  waveNo.value = ''

  // 4. 清空 bookInfo
  bookInfo.value = null

  // 5. 清空 modelValue(photoSrc/isbn/firstPagePhoto/barcode)
  emit('update:modelValue', {
    photoSrc: '',
    isbn: '',
    firstPagePhoto: '',
    barcode: ''
  })

  // 6. 清空 Wave.vue 中的状态(currentGoods 等)
  if (waveComponent.value) {
    ; (waveComponent.value as any).currentGoods = null
  }

  // 7. 清空条形码显示
  barcodeImageBase64.value = ''
  barcodeLoading.value = false

  ElMessage.success({ message: '已清空,请重新扫描图书', duration: 1000, customClass: 'scan-success-message' })
}

/**
 * 检测到波次号扫码时的处理
 * 1. 调用接口验证波次号是否存在
 * 2. 存在则弹窗确认是否切换
 * 3. 确认则切换到该波次，取消则静默略过
 */
async function handleWaveNoDetected(waveNoStr: string) {
  try {
    const { fetchWaveTaskByNo } = await import('@/api/waveTask')
    const waveData = await fetchWaveTaskByNo(waveNoStr)

    if (!waveData) {
      // 不是有效的波次号，静默略过
      console.log('[波次号识别] 未找到波次:', waveNoStr)
      return
    }

    // 弹窗确认
    try {
      await ElMessageBox.confirm(
        `检测到扫描波次号 ${waveNoStr}，是否切换到该波次，未追加部分书籍信息会丢失。`,
        '切换波次',
        {
          confirmButtonText: '切换',
          cancelButtonText: '取消',
          type: 'warning'
        }
      )

      // 用户确认，切换波次
      await switchToWave(waveData)
    } catch {
      // 用户取消，静默略过
    }
  } catch (err) {
    console.warn('[波次号查询] 失败:', err instanceof Error ? err.message : String(err))
    // API错误，静默略过
  }
}

/**
 * 切换到指定的波次
 * 清空当前扫描状态，更新波次和仓库信息
 */
async function switchToWave(waveData: any) {
  // 1. 清空书籍列表
  const goosListInstance = getGoosListInstance()
  goosListInstance?.clearAll?.()

  // 2. 清空 isbnProductIdMap
  isbnProductIdMap.clear()

  // 3. 更新波次状态
  currentWaveId.value = waveData.wave_id || waveData.id
  currentOrderId.value = waveData.related_order_id || waveData.order_id
  currentWarehouseId.value = waveData.warehouse_id
  waveNo.value = waveData.wave_no

  // 4. 清空书籍信息预览
  bookInfo.value = null

  // 5. 清空 modelValue
  emit('update:modelValue', {
    photoSrc: '',
    isbn: '',
    firstPagePhoto: '',
    barcode: ''
  })

  // 6. 清空 Wave.vue 中的 currentGoods
  if (waveComponent.value) {
    ; (waveComponent.value as any).currentGoods = null
  }

  // 7. 清空条形码显示
  barcodeImageBase64.value = ''
  barcodeLoading.value = false

  // 8. 切换仓库选择器到该波次的仓库
  if (waveData.warehouse_id && waveComponent.value && typeof (waveComponent.value as any).switchWarehouse === 'function') {
    ; (waveComponent.value as any).switchWarehouse(waveData.warehouse_id)
  }

  ElMessage.success({ message: `已切换到波次 ${waveData.wave_no}`, duration: 1000, customClass: 'scan-success-message' })
}

/**
 * 波次选择器下拉展开时加载波次列表
 */
async function handleWaveDropdownVisible(visible: boolean) {
  if (!visible) return
  waveSearchLoading.value = true
  try {
    const { fetchWaveTaskList } = await import('@/api/waveTask')
    const res = await fetchWaveTaskList({
      page: 1,
      pageSize: 100
    })
    // 从波次任务列表中提取不重复的波次号
    const taskList = res.list || []
    const seen = new Set<string>()
    const waves: any[] = []
    for (const task of taskList) {
      const wn = task.wave_no
      if (wn && !seen.has(wn)) {
        seen.add(wn)
        waves.push({ wave_no: wn, id: task.id, wave_id: task.wave_id })
      }
    }
    waveSearchResults.value = waves
    console.log('[波次选择] 接收到的数据:', JSON.parse(JSON.stringify(waves)))
  } catch {
    waveSearchResults.value = []
  } finally {
    waveSearchLoading.value = false
  }
}

/**
 * 波次选择器选中波次
 */
async function handleWaveSelect(waveId: string | number) {
  if (!waveId) return
  try {
    const { fetchWaveById } = await import('@/api/waveTask')
    const waveData = await fetchWaveById(waveId)
    if (!waveData) {
      ElMessage.warning({ message: '未找到该波次', duration: 1000, customClass: 'scan-warning-message' })
      return
    }

    try {
      await ElMessageBox.confirm(
        `确定切换到波次 ${waveData.wave_no || waveId}，未追加部分书籍信息会丢失。`,
        '切换波次',
        {
          confirmButtonText: '切换',
          cancelButtonText: '取消',
          type: 'warning'
        }
      )

      await switchToWave(waveData)
    } catch {
      // 用户取消
    }
  } catch (err) {
    console.warn('[波次选择器] 查询失败:', err instanceof Error ? err.message : String(err))
  } finally {
    // 清空选择器显示
    selectedWaveNo.value = ''
  }
}

/**
 * 生成波次条形码
 * 调用 /api/barcode/generate 接口,使用 wave_no 生成条形码图片
 */
async function generateWaveBarcode() {
  if (!checkCarSelected()) return
  if (!waveNo.value) {
    ElMessage.warning({ message: '波次号为空,无法生成条形码', duration: 1000, customClass: 'scan-warning-message' })
    return
  }

  barcodeLoading.value = true
  try {
    const res = await generateBarcode(waveNo.value)

    const resData = res as any
    const imageBase64 = resData.data?.image_base64
    if (resData.code === 200 && imageBase64) {
      barcodeImageBase64.value = imageBase64
      ElMessage.success({ message: '条形码生成成功', duration: 1000, customClass: 'scan-success-message' })
    } else {
      errorBlocker?.block('条形码生成失败:' + (resData.msg || resData.message || '未知错误'))
    }
  } catch (err) {
    console.warn('[生成条形码] 失败:', err instanceof Error ? err.message : String(err))
    errorBlocker?.block('条形码生成失败: ' + (err instanceof Error ? err.message : String(err)))
  } finally {
    barcodeLoading.value = false
  }
}



/**
 * 处理扫码输入变化
 * 兼容不带 Enter 的扫码枪：输入变化时立即检测快捷键或长输入自动触发。
 * 注意：带 Enter 的扫码枪仍会触发 @keyup.enter 的 handleScan，handleScan 内部
 * 会清空输入框防重入，因此重复触发时第二次检测到空值直接 return。
 */
const handleScanInput = () => {
  // 页面锁定时不处理任何扫码输入
  if (isPageLocked.value) return

  const code = scannedCode.value.trim().toLowerCase()

  // ---- 1. 优先检测快捷键（短文本，立即触发） ----
  const shortcutAction = SCAN_SHORTCUT_MAP[code]
  if (shortcutAction) {
    console.log('[handleScanInput] 识别到快捷键:', code)
    scannedCode.value = '' // 立即清空，防重入
    shortcutAction()
    return
  }

  // ---- 2. 检测到过长的输入（ISBN/常规条码），自动触发 handleScan ----
  // 有些扫码枪不发送 Enter，当输入超过一定长度时自动处理
  if (code.length >= 8) {
    // 防抖：清除之前的定时器，确保最后一次输入停顿后再处理，避免扫码枪字符间隙导致截断
    if (scanInputTimer) {
      clearTimeout(scanInputTimer)
      scanInputTimer = null
    }
    scanInputTimer = setTimeout(() => {
      scanInputTimer = null
      if (scannedCode.value.trim().length >= 8) {
        handleScan()
      }
    }, 200)
  }
}

/**
 * 扫码快捷键映射表
 * 当扫码枪扫到这些文本时，执行对应的快捷键动作
 */
const SCAN_SHORTCUT_MAP: Record<string, () => void> = {
  'alt+a': () => handlePhotoAction(),
  'alt+b': () => generateWaveBarcode(),
  'alt+c': () => { scanMode.value = scanMode.value === 'scan' ? 'recognize' : 'scan' },
  'alt+x': () => hasActiveWave.value ? handleCreateNewWave() : handleCreateWave(),
}

/**
 * 处理扫码完成
 * 注意：handleScanInput 中长输入（>=8字符）的 50ms 延迟兜底也可能触发此函数，
 * 因此内部先立即清空 scannedCode 防止重复处理（防重入）。
 */
async function handleScan() {
  // 页面锁定时不处理任何扫码操作
  if (isPageLocked.value) {
    scannedCode.value = ''
    return
  }

  const code = scannedCode.value.trim()
  if (!code) return

  // 扫码操作解除错误阻断状态
  errorBlocker?.unblock()

  // 没选择小车的情况下，提示并阻止扫码操作
  if (!checkCarSelected()) {
    scannedCode.value = ''
    return
  }

  // 清理防抖定时器，防止与 Enter 触发产生竞态
  if (scanInputTimer) {
    clearTimeout(scanInputTimer)
    scanInputTimer = null
  }

  // 立即清空输入框，防止 handleScanInput 的 setTimeout 兜底再次触发 handleScan
  scannedCode.value = ''

  // 检测扫码快捷键：如果扫描内容匹配快捷键文本，执行对应动作
  const lowerCode = code.toLowerCase().trim()
  console.log('进入监听'), console.log('扫码输入:', code, '| 规范化后:', lowerCode)
  const shortcutAction = SCAN_SHORTCUT_MAP[lowerCode]
  if (shortcutAction) {
    console.log('[扫码快捷键] 识别到快捷键:', lowerCode)
    shortcutAction()
    return
  }

  // 优先检测波次号：如果不是有效ISBN且匹配波次号格式，尝试切换波次
  const normalizedCode = normalizeBarcode(code)
  const isISBN = isValidISBN13(normalizedCode) || isValidISBN10(normalizedCode)
  if (!isISBN && isWaveNoPattern(code)) {
    await handleWaveNoDetected(code)
    return
  }

  // 原有ISBN处理逻辑
  const extractedIsbn = extractISBN(code)
  await handleScannedCode(extractedIsbn || code)
}

/**
 * 从扫码数据中提取ISBN
 */
function extractISBN(data: string): string {
  // 尝试提取13位数字的ISBN
  const isbn13Match = data.match(/\d{13}/)
  if (isbn13Match) {
    return isbn13Match[0]
  }

  // 尝试提取10位数字的ISBN
  const isbn10Match = data.match(/\d{9}[\dXx]/)
  if (isbn10Match) {
    return isbn10Match[0]
  }

  // 尝试提取包含连字符的ISBN
  const isbnWithDashesMatch = data.match(/\d{1,5}[- ]\d{1,7}[- ]\d{1,6}[- ]\d{1,3}/)
  if (isbnWithDashesMatch) {
    return isbnWithDashesMatch[0]
  }

  return ''
}

/**
 * 处理扫码结果
 * 扫码枪只用于获取二维码数据,不自动拍照
 */
async function handleScannedCode(code: string) {
  // 扫码操作解除错误阻断状态
  errorBlocker?.unblock()

  const normalizedCode = normalizeBarcode(code)
  console.log('规范化后的条码:', normalizedCode)
  console.log('条码长度:', normalizedCode.length)

  // 清空照片预览(扫码只获取数据,不拍照)
  const newFormData = {
    ...props.modelValue,
    photoSrc: '',  // 清空照片预览
    isbn: '',
    barcode: ''
  }
  emit('update:modelValue', newFormData)

  // 有活跃波次时，先查询波次状态
  if (hasActiveWave.value && currentWaveId.value) {
    try {
      const res = await getWaveStatusById(currentWaveId.value)
      console.log('[WaveStatus] 波次状态:', res)
      const status = res?.data?.status
      // status 不为 1 或 2 时，说明波次已开始拣货，阻止后续操作
      if (status !== undefined && status !== 1 && status !== 2) {
        errorBlocker?.block('该波次已开始拣货不能继续追加商品，请扫描波次码清空当前波次，重新扫描书籍')
        return
      }
    } catch (err) {
      console.error('[WaveStatus] 查询失败:', err)
    }
  }

  // 尝试多种ISBN格式验证
  if (isValidISBN13(normalizedCode) || isValidISBN10(normalizedCode)) {
    // 使用规范化后的条码
    const finalIsbn = isValidISBN13(normalizedCode) ? normalizedCode : convertISBN10to13(normalizedCode)

    // 更新条码数据
    emit('update:modelValue', {
      ...newFormData,
      isbn: finalIsbn,
      barcode: finalIsbn
    })

    // 加载书籍信息
    loadBookInfo(finalIsbn, true)

    // 保存ISBN供拍照上传使用
    lastScannedIsbn.value = finalIsbn

    // 显示扫码成功状态
    barcodeStatus.value = `扫码成功:${finalIsbn}`
    barcodeSuccess.value = true
    barcodeError.value = false
    ElMessage.success({ message: `扫码成功:${finalIsbn}`, duration: 1000, customClass: 'scan-success-message' })
  } else {
    console.log('ISBN验证失败,尝试直接使用原始条码')
    // 即使验证失败,也尝试使用原始条码

    // 更新条码数据
    emit('update:modelValue', {
      ...newFormData,
      isbn: normalizedCode,
      barcode: normalizedCode
    })

    // 尝试加载书籍信息
    loadBookInfo(normalizedCode, true)

    // 保存ISBN供拍照上传使用
    lastScannedIsbn.value = normalizedCode

    // 显示扫码失败状态并阻断后续操作
    barcodeStatus.value = `扫码失败:${normalizedCode}`
    barcodeSuccess.value = false
    barcodeError.value = true
    errorBlocker?.block(`扫码失败:${normalizedCode}`)
    return
  }
}

/**
 * 校验 ISBN-10
 */
function isValidISBN10(barcode: string): boolean {
  const digits = normalizeBarcode(barcode)
  if (!/^\d{9}[\dXx]$/.test(digits)) return false

  let sum = 0
  for (let i = 0; i < 9; i++) {
    const n = Number(digits[i])
    sum += n * (10 - i)
  }

  const checkDigit = digits[9]
  const calculatedCheckDigit = (11 - (sum % 11)) % 11
  const checkDigitValue = checkDigit === 'X' || checkDigit === 'x' ? 10 : Number(checkDigit)

  return checkDigitValue === calculatedCheckDigit
}

/**
 * 将 ISBN-10 转换为 ISBN-13
 */
function convertISBN10to13(isbn10: string): string {
  const digits = normalizeBarcode(isbn10).slice(0, 9)
  const isbn13WithoutCheck = '978' + digits

  let sum = 0
  for (let i = 0; i < 12; i++) {
    const n = Number(isbn13WithoutCheck[i])
    sum += i % 2 === 0 ? n : n * 3
  }

  const checkDigit = (10 - (sum % 10)) % 10
  return isbn13WithoutCheck + checkDigit
}

/**
 * 检查当前是否有其他可交互元素正在获得焦点
 * 如果有,则不应该抢夺焦点给扫码输入框
 *
 * 覆盖场景:
 * - 原生控件:input / textarea / select / contentEditable
 * - Element Plus 组件:el-select(role=combobox)、el-cascader、el-date-picker、
 *   el-input-number、el-radio-group、el-checkbox-group、el-switch、el-slider 等
 * - 任何带有 role=textbox/combobox/listbox/searchbox/slider 的 ARIA 控件
 */
function isOtherInputFocused(): boolean {
  const activeEl = document.activeElement as HTMLElement
  if (!activeEl) return false

  // 1. 扫码输入框自身 → 不算"其他"
  if (activeEl.classList.contains('hidden-scanner-input')) return false

  // 2. 原生表单控件
  const tagName = activeEl.tagName.toLowerCase()
  if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') return true

  // 3. contentEditable 元素
  if (activeEl.isContentEditable) return true

  // 4. ARIA 交互角色(Element Plus 下拉框、输入框等都带这些 role)
  const role = activeEl.getAttribute('role')
  const interactiveRoles = ['textbox', 'combobox', 'listbox', 'searchbox', 'slider', 'spinbutton', 'switch', 'checkbox', 'radio', 'treeitem', 'option']
  if (role && interactiveRoles.includes(role)) return true

  // 5. Element Plus 组件:焦点可能落在 .el-input__inner / .el-textarea__inner 等内部元素上
  if (activeEl.classList.contains('el-input__inner') ||
    activeEl.classList.contains('el-textarea__inner') ||
    activeEl.classList.contains('el-input-number__decrease') ||
    activeEl.classList.contains('el-input-number__increase')) {
    return true
  }

  // 6. Element Plus 下拉弹层中的元素(el-select / el-cascader / el-date-picker 打开后的选项)
  //    弹层挂在 body 下,焦点可能落在 .el-select-dropdown / .el-cascader-panel 等
  const elPopupParent = activeEl.closest('.el-select-dropdown, .el-cascader-panel, .el-date-picker, .el-picker-panel, .el-table__body')
  if (elPopupParent) return true

  // 7. 任何 .el-input / .el-textarea 容器内的焦点(兜底)
  const elInputParent = activeEl.closest('.el-input, .el-textarea, .el-input-number, .el-radio-group, .el-checkbox-group, .el-switch, .el-slider')
  if (elInputParent) return true

  return false
}

// 全局点击事件处理函数
const handleGlobalClick = (event: MouseEvent) => {
  // 只有在扫描模式为'scan'时才聚焦输入框
  if (scanMode.value !== 'scan' || isProcessing.value) return

  const target = event.target as HTMLElement

  // 点击了这些元素,不抢焦点(用户可能在复制/操作这些内容)
  if (isInteractiveTarget(target)) return
  if (target.tagName.toLowerCase() === 'img') return
  if (target.tagName.toLowerCase() === 'video') return
  if (target.tagName.toLowerCase() === 'canvas') return
  if (target.classList.contains('wave-barcode-section')) return
  if (target.classList.contains('barcode-display-section')) return
  if (target.classList.contains('barcode-image-container')) return
  if (target.classList.contains('wave-no-display')) return
  if (target.tagName.toLowerCase() === 'label') return

  // 延迟检查,确保点击后焦点已转移
  setTimeout(() => {
    if (isOtherInputFocused()) return
    const inputElement = document.querySelector('.hidden-scanner-input') as HTMLInputElement
    if (inputElement) {
      inputElement.focus()
    }
  }, 100)
}

/**
 * 判断点击目标是否为可交互元素(不需要抢焦点的目标)
 * 用于 click 事件快速判断
 */
function isInteractiveTarget(target: HTMLElement): boolean {
  const tagName = target.tagName.toLowerCase()
  if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') return true
  if (target.isContentEditable) return true

  // Element Plus 按钮、下拉、开关等
  if (target.closest('.el-input, .el-textarea, .el-select, .el-cascader, .el-date-picker, .el-picker-panel, .el-input-number, .el-radio-group, .el-checkbox-group, .el-switch, .el-slider, .el-button, .el-dropdown, .el-tooltip__popper, .el-select-dropdown, .el-cascader-panel, .el-table')) {
    return true
  }

  // 通用可交互属性
  const role = target.getAttribute('role')
  const interactiveRoles = ['textbox', 'combobox', 'listbox', 'searchbox', 'slider', 'spinbutton', 'switch', 'checkbox', 'radio', 'button', 'treeitem', 'option', 'tab', 'menuitem']
  if (role && interactiveRoles.includes(role)) return true

  return false
}

// 键盘事件处理函数
const handleKeyDown = (event: KeyboardEvent) => {
  // 页面锁定状态下拦截所有快捷键操作
  if (isPageLocked.value) {
    event.preventDefault()
    event.stopImmediatePropagation()
    return
  }
  // 检测 alt+a 快捷键
  if (event.altKey && event.key === 'a') {
    handlePhotoAction()
    event.preventDefault()
  }
  // 检测 alt+c 快捷键:切换扫描模式
  if (event.altKey && event.key === 'c') {
    scanMode.value = scanMode.value === 'scan' ? 'recognize' : 'scan'
    event.preventDefault()
  }
  // 检测 alt+x 快捷键:创建波次(无活跃波次时创建新波次,有活跃波次时清空重新开始)
  if (event.altKey && event.key === 'x') {
    hasActiveWave.value ? handleCreateNewWave() : handleCreateWave()
    event.preventDefault()
  }
  // 检测 alt+b 快捷键:生成波次条形码
  if (event.altKey && event.key === 'b') {
    generateWaveBarcode()
    event.preventDefault()
  }
}

/**
 * 全局 focusin 事件:当焦点从其他输入框离开后,自动聚焦扫码输入框
 * 注意:扫码输入框自身获得焦点时也会触发,需排除
 */
const handleGlobalFocusIn = (event: FocusEvent) => {
  if (scanMode.value !== 'scan' || isProcessing.value) return
  const target = event.target as HTMLElement
  // 扫码输入框自身获得焦点,无需处理
  if (target.classList.contains('hidden-scanner-input')) return
  // 其他可交互元素获得焦点,不抢夺
  if (isOtherInputFocused()) return
  // 延迟聚焦,等焦点稳定
  setTimeout(() => {
    if (isOtherInputFocused()) return
    const inputElement = document.querySelector('.hidden-scanner-input') as HTMLInputElement
    if (inputElement && document.activeElement !== inputElement) {
      inputElement.focus()
    }
  }, 200)
}

/**
 * 页面关闭/刷新前释放摄像头资源，防止浏览器缓存流占用摄像头
 */
function handleBeforeUnload() {
  closeCamera()
}

onMounted(() => {
  openCamera()
  // 添加 beforeunload 事件，确保页面刷新/关闭时释放摄像头
  window.addEventListener('beforeunload', handleBeforeUnload)
  // 聚焦输入框,准备接收扫码枪输入(仅当当前无其他输入获得焦点时)
  setTimeout(() => {
    if (!isOtherInputFocused()) {
      const inputElement = document.querySelector('.hidden-scanner-input') as HTMLInputElement
      if (inputElement) {
        inputElement.focus()
      }
    }
  }, 500)

  // 添加全局事件监听
  document.addEventListener('click', handleGlobalClick)
  document.addEventListener('keydown', handleKeyDown)
  document.addEventListener('focusin', handleGlobalFocusIn)
  document.addEventListener('selectstart', onSelectStart)
  document.addEventListener('mouseup', onMouseUp)
})

onBeforeUnmount(() => {
  closeCamera()
  window.removeEventListener('beforeunload', handleBeforeUnload)
  // 移除全局事件监听
  document.removeEventListener('click', handleGlobalClick)
  document.removeEventListener('keydown', handleKeyDown)
  document.removeEventListener('focusin', handleGlobalFocusIn)
  document.removeEventListener('selectstart', onSelectStart)
  document.removeEventListener('mouseup', onMouseUp)
})

// 监听文本选择开始/结束,临时隐藏隐藏输入框防止抢焦点
function onSelectStart() {
  const inputElement = document.querySelector('.hidden-scanner-input') as HTMLInputElement
  if (inputElement) {
    inputElement.classList.add('selection-active')
  }
}

function onMouseUp() {
  // 延迟清除,恢复扫码枪焦点功能
  setTimeout(() => {
    const inputElement = document.querySelector('.hidden-scanner-input') as HTMLInputElement
    if (inputElement) {
      inputElement.classList.remove('selection-active')
    }
  }, 300)
}

/**
 * 智能格式化出版日期
 * - 已经是 YYYY-MM 格式 → 直接使用
 * - Unix 时间戳(秒/毫秒)→ 转换为 YYYY-MM
 * - 其他格式 → 原样返回
 */
function formatPublishDate(value: any): string {
  if (!value) return ''

  // 已经是 YYYY-MM 格式,直接返回
  if (typeof value === 'string' && /^\d{4}-\d{2}$/.test(value)) {
    return value
  }

  // 时间戳转换
  let date: Date | null = null
  if (typeof value === 'number') {
    date = new Date(value > 1e12 ? value : value * 1000)
  } else if (typeof value === 'string' && /^\d+$/.test(value)) {
    // 纯数字字符串,视为时间戳
    const num = Number(value)
    date = new Date(num > 1e12 ? num : num * 1000)
  }

  if (date && !isNaN(date.getTime())) {
    const year = date.getFullYear()
    const month = String(date.getMonth() + 1).padStart(2, '0')
    return `${year}-${month}`
  }

  return String(value)
}

// 查询书籍信息
async function loadBookInfo(isbn: string, previewOnly = false) {
  if (!isbn) {
    bookInfo.value = null
    return
  }

  // 同步保存 ISBN，供拍照上传使用（Car 搜索查询流程不走 handleScannedCode，需在此处设置）
  lastScannedIsbn.value = isbn

  try {
    const payload = await getBookInfo(isbn)
    const data = payload?.data
    if (!data) {
      bookInfo.value = null
      ElMessage.warning({ message: '数据库中暂无该书数据，请使用OCR识别', duration: 1000, customClass: 'scan-warning-message' })
      return
    }

    // 检测是否为套装书 (is_suit === 1)
    if (data.is_suit === 1) {
      currentSuitIsbn.value = isbn

      // 保存 book_pic_s.pddResponse
      lastBookPicS.value = data.book_pic_s?.pddResponse || ''

      // 先构建书籍数据并更新预览，再弹窗
      const suitPreviewBookPic = data.live_image?.[0]
        ? { localPath: data.live_image[0], pddPath: data.live_image[0] }
        : (data.book_pic || undefined)
      const bookData = {
        bookName: data.book_name || '',
        author: data.author || '',
        publisher: data.publisher || '',
        publishDate: formatPublishDate(data.publication_time),
        binding: data.binding_layout || '',
        price: typeof data.fix_price === 'number' ? data.fix_price : 0,
        pageCount: Number(data.page_count) || 0,
        wordCount: Number(data.word_count) || 0,
        book_pic: suitPreviewBookPic,
        totalBook: 1,
        ownPrice: 0,
        salePrice: 0,
        isbn: isbn,
        isSuit: true
      }
      bookInfo.value = bookData
      emit('book-info-update', bookData)

      // 保存书籍信息，用于套装书弹窗自定义添加项的预填
      currentSuitBookInfo.value = {
        bookName: data.book_name || '',
        author: data.author || '',
        publisher: data.publisher || '',
        publishDate: formatPublishDate(data.publication_time),
        binding: data.binding_layout || '',
        price: typeof data.fix_price === 'number' ? data.fix_price : 0,
        pageCount: Number(data.page_count) || 0,
        wordCount: Number(data.word_count) || 0,
        book_pic: suitPreviewBookPic,
      }
      // 套装书弹窗打开时，立即同步数据到第三方平台（f_isbn=000，f_book_name为空串）
      const liveImage = data.book_pic?.pddPath || data.book_pic?.localPath || data.book_pic_s?.pddResponse || ''
      const syncRes = await callSyncBookApi({
        book_name: data.book_name || '',
        author: data.author || '',
        publisher: data.publisher || '',
        publication_time: toPublishTimestamp(String(data.publication_time ?? '')),
        binding_layout: data.binding_layout || '',
        fix_price: typeof data.fix_price === 'number' ? data.fix_price : 0,
        isbn: isbn,
        page_count: String(data.page_count || ''),
        word_count: String(data.word_count || ''),
        book_format: String(data.book_format ?? ''),
        fid: 0,
        f_isbn: '000',
        live_image: liveImage,
        type: '2',
      })
      // 保存返回的 id，供后续同步作为 f_id 使用
      if (syncRes) {
        const r = syncRes as any
        currentSuitFid.value = r.id ?? r.data?.id ?? null
      }
      // 预览模式时，套装书选择后仅更新预览，不执行保存/加入列表
      suitPreviewOnly.value = previewOnly
      suitBookDialogVisible.value = true
      return
    }

    // 保存 book_pic_s.pddResponse,供保存商品时使用
    lastBookPicS.value = data.book_pic_s?.pddResponse || ''

    const bookData = {
      bookName: data.book_name || '',
      author: data.author || '',
      publisher: data.publisher || '',
      publishDate: formatPublishDate(data.publication_time),
      binding: data.binding_layout || '',
      price: typeof data.fix_price === 'number' ? data.fix_price : 0,
      pageCount: Number(data.page_count) || 0,
      wordCount: Number(data.word_count) || 0,
      book_pic: data.live_image?.[0]
        ? { localPath: data.live_image[0], pddPath: data.live_image[0] }
        : (data.book_pic || undefined),
      totalBook: 1,
      ownPrice: 0,
      salePrice: 0,
      isbn: isbn
    }
    bookInfo.value = bookData
    emit('book-info-update', bookData)
  } catch (error) {
    console.warn('书籍信息查询失败:', error instanceof Error ? error.message : String(error))
    bookInfo.value = null
    emit('book-info-update', null)
  }
}

/**
 * 处理套装书选择
 * 当用户从套装书列表中选择一本书后,将该书信息更新到书籍预览
 * 自定义项如果有拍摄的照片,则执行完整的上传+保存流程
 */
async function handleSuitBookSelect(book: any, photoSrc?: string, customTotalBook?: number, customOwnPrice?: number) {
  // ---- 自定义项且有拍摄的照片：执行上传保存流程 ----
  if (photoSrc && photoSrc.startsWith('data:')) {
    await processCustomItemUpload(book, photoSrc, customTotalBook, customOwnPrice)
    currentSuitBookInfo.value = null
    return
  }

  // ---- 自定义项无照片（仅编辑文本字段）：保存商品并添加到已扫描列表 ----
  // customTotalBook 或 customOwnPrice 有值即表示为自定义项（API 书籍不会传这些参数）
  if (customTotalBook !== undefined || customOwnPrice !== undefined) {
    await processCustomItemWithoutPhoto(book, photoSrc, customTotalBook, customOwnPrice)
    currentSuitBookInfo.value = null
    return
  }

  // ---- API 选择的书籍：保存商品并添加到已扫描列表 ----
  lastBookPicS.value = book.book_pic_s?.pddResponse || ''
  const isbn = book.isbn || currentSuitIsbn.value
  const bookName = book.book_name || ''
  const price = typeof book.fix_price === 'number' ? book.fix_price : 0
  const binding = book.binding_layout || ''
  const pageCountValue = Number(book.page_count) || 0
  const wordCountValue = Number(book.word_count) || 0
  const originalBookPic = currentSuitBookInfo.value?.book_pic || book.book_pic
  // 拍照预览和已扫描列表使用 /api/getSuitBook 的 live_image（套装书列表中映射为 book.book_pic）
  const liveImgUrl = book.book_pic?.pddPath || book.book_pic?.localPath || ''
  const appearance = injectedQuality.value as number | undefined

  // 预览模式（来自 Car 搜索/扫码枪预览）：仅更新预览，不执行保存/加入列表
  if (suitPreviewOnly.value) {
    const displayIsbn = isbn + (book.f_isbn ? '-' + book.f_isbn : '')
    const displayBookName = bookName + (book.f_book_name ? '-' + book.f_book_name : '')
    const bookData = {
      bookName: displayBookName,
      author: book.author || '',
      publisher: book.publisher || '',
      publishDate: formatPublishDate(book.publication_time),
      binding,
      price,
      pageCount: pageCountValue,
      wordCount: wordCountValue,
      totalBook: customTotalBook ?? 1,
      ownPrice: customOwnPrice ? Number(customOwnPrice) : 0,
      isbn: displayIsbn,
      book_pic: book.book_pic || originalBookPic,
      isSuit: true, // 标记为套装书，后续拍照核价时走套装书专用逻辑
    }
    bookInfo.value = bookData as BookInfo
    emit('book-info-update', bookData)
    currentSuitBookInfo.value = null
    suitPreviewOnly.value = false
    return
  }

  barcodeStatus.value = '正在保存商品...'

  // 保存商品（使用 API 返回的 live_img 作为实拍图）
  const productId = await saveProductBeforeUpload({
    isbn, name: bookName, price, bookPicS: liveImgUrl, appearance, binding,
    pageCount: pageCountValue, wordCount: wordCountValue,
  })
  if (!productId) {
    ElMessage.warning({ message: '商品保存失败', duration: 1000, customClass: 'scan-warning-message' })
    barcodeStatus.value = '商品保存失败'
    return
  }

  // 调用 goods/query（套装书不传 isbn，传 book_name、author、publisher）
  const queryBookName = bookName + (book.f_book_name ? '-' + book.f_book_name : '')
  if (hasActiveWave.value) {
    await queryGoodsApi(isbn, productId, String(injectedQuality.value), {
      bookName: queryBookName,
      author: book.author || '',
      publisher: book.publisher || ''
    })
  } else {
    pendingGoodsQueryQueue.push({
      isbn, outId: productId.toString(), quality: String(injectedQuality.value), productId,
      bookName: queryBookName,
      author: book.author || '',
      publisher: book.publisher || '',
      isSuit: true,
    })
  }

  // 获取 salePrice
  let salePrice = null
  try {
    const req = (await import('@/utils/request')).default
    const listRes = await req.get('/product/list', {
      params: { status: 1, page: 1, page_size: 20, 'ids[0]': productId },
    })
    const list = listRes?.data?.list || listRes?.data?.data || listRes?.data?.rows || []
    if (list.length > 0) salePrice = list[0].sale_price
  } catch (e) {
    console.warn('[套装书] 获取 salePrice 失败:', e)
  }

  // 构建 bookData：书籍预览使用封面图，拍照预览使用 API 返回的 live_img
  const displayIsbn = isbn + (book.f_isbn ? '-' + book.f_isbn : '')
  const displayBookName = bookName + (book.f_book_name ? '-' + book.f_book_name : '')
  const bookData = {
    bookName: displayBookName,
    author: book.author || '',
    publisher: book.publisher || '',
    publishDate: formatPublishDate(book.publication_time),
    binding,
    price,
    pageCount: pageCountValue,
    wordCount: wordCountValue,
    totalBook: customTotalBook ?? 1,
    ownPrice: customOwnPrice ? Number(customOwnPrice) : 0,
    isbn: displayIsbn,
    book_pic: originalBookPic,
    capturedPhoto: liveImgUrl,
    isSuit: true,
  }
  bookInfo.value = bookData as BookInfo
  emit('book-info-update', bookData)

  // 拍照预览展示 API 返回的 live_img
  emit('update:modelValue', { ...props.modelValue, photoSrc: liveImgUrl })

  await nextTick()
  emit('photo-preview-confirm', {
    bookInfo: bookData, photoSrc: liveImgUrl, productId, salePrice, originalBookPic,
  } as any)
  barcodeStatus.value = '套装书商品已添加'

  if (hasActiveWave.value) {
    await nextTick()
    await handleAppendToWave()
  }
  currentSuitBookInfo.value = null
}

/**
 * 自定义项照片的上传+保存全流程（与 takePhotoPreview 的拍照后逻辑一致）
 */
async function processCustomItemUpload(
  customBook: any,
  base64Data: string,
  customTotalBook?: number,
  customOwnPrice?: number
) {
  const isbn = customBook.isbn || currentSuitIsbn.value
  const bookName = customBook.book_name || ''
  const price = typeof customBook.fix_price === 'number' ? customBook.fix_price : 0
  const binding = customBook.binding_layout || ''
  const pageCountValue = Number(customBook.page_count) || 0
  const wordCountValue = Number(customBook.word_count) || 0
  const bookPicS = lastBookPicS.value || ''
  const appearance = injectedQuality.value as number | undefined

  barcodeStatus.value = '正在保存自定义商品...'

  // 第1步:调用 /api/product/save 保存商品,获取 product ID
  const { updateProductLiveImageAsPddUrl } = await import('@/api/product')
  const productId = await saveProductBeforeUpload({
    isbn,
    name: bookName,
    price,
    bookPicS,
    appearance,
    binding,
    pageCount: pageCountValue,
    wordCount: wordCountValue
  })

  if (!productId) {
    ElMessage.warning({ message: '自定义商品保存失败', duration: 1000, customClass: 'scan-warning-message' })
    barcodeStatus.value = '自定义商品保存失败'
    return
  }

  // 第2步:上传实拍图片到图片服务
  const random5 = String(Math.floor(Math.random() * 100000)).padStart(5, '0')
  const pictureName = `${productId}-${isbn}-${random5}.jpg`
  await uploadLivingPicture(base64Data, pictureName, isbn)

  // 第3步:上传到拼多多图片空间
  let pddUrl = ''
  try {
    const CryptoJS = (await import('crypto-js')).default
    const timestamp = String(Math.floor(Date.now() / 1000))
    const signParams: Record<string, string> = {
      type: 'pdd.goods.filespace.image.upload',
      access_token: '5f7dcc92211549f3b8b05451288a92fa9546732d',
      timestamp,
      client_id: '203c5a7ba8bd4b8488d5e26f93052642',
      data_type: 'JSON'
    }
    const sortedKeys = Object.keys(signParams).sort()
    let signStr = '892ffaa86e12b7a3d8d2942b669d9aa520ad8179'
    for (const key of sortedKeys) {
      signStr += key + signParams[key]
    }
    signStr += '892ffaa86e12b7a3d8d2942b669d9aa520ad8179'
    const sign = CryptoJS.MD5(signStr).toString().toUpperCase()

    const pddFormData = new FormData()
    pddFormData.append('type', signParams.type)
    pddFormData.append('access_token', signParams.access_token)
    pddFormData.append('timestamp', signParams.timestamp)
    pddFormData.append('client_id', signParams.client_id)
    pddFormData.append('data_type', signParams.data_type)
    pddFormData.append('sign', sign)

    const base64 = base64Data.replace(/^data:image\/\w+;base64,/, '')
    const binaryStr = atob(base64)
    const bytes = new Uint8Array(binaryStr.length)
    for (let i = 0; i < binaryStr.length; i++) {
      bytes[i] = binaryStr.charCodeAt(i)
    }
    const blob = new Blob([bytes], { type: 'image/jpeg' })
    pddFormData.append('file', blob, `${isbn}.jpg`)

    const pddRes = await axios.post('https://gw-upload.pinduoduo.com/api/upload', pddFormData, {
      headers: { 'Content-Type': 'multipart/form-data' },
      timeout: 30000
    })
    const pddResult = pddRes.data
    if (pddResult?.goods_filespace_image_upload_response?.image_url) {
      pddUrl = pddResult.goods_filespace_image_upload_response.image_url
    } else if (pddResult?.goods_filespace_image_upload_response?.url) {
      pddUrl = pddResult.goods_filespace_image_upload_response.url
    } else if (pddResult?.error_response) {
      console.warn('[自定义PDD] 失败:', pddResult.error_response.error_msg)
    }
  } catch (pddErr) {
    console.warn('[自定义PDD] 异常:', pddErr instanceof Error ? pddErr.message : String(pddErr))
  }

  // 第4步:更新商品 live_image
  const photoUrl = pddUrl || bookPicS
  await updateProductLiveImageAsPddUrl(productId, photoUrl, {
    barcode: isbn,
    name: bookName,
    price,
    appearance: injectedQuality.value
  })

  // 第5步:调用 goods/query（套装书不传 isbn，传 book_name、author、publisher）
  const queryCustomBookName = bookName + (customBook.subTitle ? '-' + customBook.subTitle : '')
  if (hasActiveWave.value) {
    await queryGoodsApi(isbn, productId, String(injectedQuality.value), {
      bookName: queryCustomBookName,
      author: customBook.author || '',
      publisher: customBook.publisher || ''
    })
  } else {
    pendingGoodsQueryQueue.push({
      isbn,
      outId: productId.toString(),
      quality: String(injectedQuality.value),
      productId,
      bookName: queryCustomBookName,
      author: customBook.author || '',
      publisher: customBook.publisher || '',
      isSuit: true,
    })
  }

  // 第7步:获取 salePrice（update:modelValue 移至 book-info-update 之后，避免被 handleBookInfoUpdate 清空）
  let salePrice = null
  try {
    const req = (await import('@/utils/request')).default
    const listRes = await req.get('/product/list', {
      params: { status: 1, page: 1, page_size: 20, 'ids[0]': productId }
    })
    const list = listRes?.data?.list || listRes?.data?.data || listRes?.data?.rows || []
    if (list.length > 0) {
      salePrice = list[0].sale_price
    }
  } catch (e) {
    console.warn('[自定义] 获取 salePrice 失败:', e)
  }

  // 保存原始接口 book_pic 数据（供已扫描列表使用，不覆盖接口封面）
  const originalBookPic = currentSuitBookInfo.value?.book_pic

  // 第8步:构建 bookData 并推送到预览和已扫描列表
  // book_pic 使用接口原始封面（书籍信息预览展示接口封面，照片预览单独通过 update:modelValue 展示拍摄照片）
  const bookData = {
    bookName,
    author: customBook.author || '',
    publisher: customBook.publisher || '',
    publishDate: customBook.publication_time || '',
    binding,
    price,
    pageCount: pageCountValue,
    wordCount: wordCountValue,
    totalBook: customTotalBook ?? 1,
    ownPrice: customOwnPrice ? Number(customOwnPrice) : 0,
    isbn,
    book_pic: originalBookPic,
    capturedPhoto: photoUrl
  }
  bookInfo.value = bookData as BookInfo
  emit('book-info-update', bookData)

  // book-info-update 后重新设置照片预览（handleBookInfoUpdate 会清空 photoSrc）
  emit('update:modelValue', {
    ...props.modelValue,
    photoSrc: photoUrl
  })

  // 等待父组件更新 currentGoods 后，再 emit photo-preview-confirm
  // 携带 originalBookPic 让父组件在已扫描列表中使用接口原始封面
  await nextTick()
  emit('photo-preview-confirm', {
    bookInfo: bookData,
    photoSrc: photoUrl,
    productId,
    salePrice,
    originalBookPic
  } as any)
  barcodeStatus.value = '自定义商品已添加'

  // 自动追加到当前波次
  if (hasActiveWave.value) {
    await nextTick()
    await handleAppendToWave()
  }

  // 同步书本数据到第三方平台（使用用户拍摄的 PDD 图片 URL）
  callSyncBookApi({
    book_name: bookName,
    author: customBook.author || '',
    publisher: customBook.publisher || '',
    publication_time: toPublishTimestamp(customBook.publication_time),
    binding_layout: binding,
    fix_price: price,
    isbn,
    page_count: String(pageCountValue || ''),
    word_count: String(wordCountValue || ''),
    fid: currentSuitFid.value ?? 0,
    f_isbn: customBook.subIsbn || '',
    f_book_name: customBook.subTitle || '',
    live_image: photoUrl,
    type: '2',
  })

  console.log('[自定义] 上传+保存完成, 商品ID:', productId, '| 图片:', photoUrl)
}

/**
 * 自定义项无照片时的保存+添加流程
 * 跳过图片上传步骤，但同样保存商品并添加到已扫描列表
 */
async function processCustomItemWithoutPhoto(
  customBook: any,
  coverUrl?: string,
  customTotalBook?: number,
  customOwnPrice?: number
) {
  const isbn = customBook.isbn || currentSuitIsbn.value
  const bookName = customBook.book_name || ''
  const price = typeof customBook.fix_price === 'number' ? customBook.fix_price : 0
  const binding = customBook.binding_layout || ''
  const pageCountValue = Number(customBook.page_count) || 0
  const wordCountValue = Number(customBook.word_count) || 0
  const bookPicS = lastBookPicS.value || coverUrl || ''
  const appearance = injectedQuality.value as number | undefined

  barcodeStatus.value = '正在保存自定义商品...'

  // 第1步: 调用 /api/product/save 保存商品（使用封面图作为 live_image）
  const productId = await saveProductBeforeUpload({
    isbn,
    name: bookName,
    price,
    bookPicS,
    appearance,
    binding,
    pageCount: pageCountValue,
    wordCount: wordCountValue
  })

  if (!productId) {
    ElMessage.warning({ message: '自定义商品保存失败', duration: 1000, customClass: 'scan-warning-message' })
    barcodeStatus.value = '自定义商品保存失败'
    return
  }

  // 第2步: 调用 goods/query（套装书不传 isbn，传 book_name、author、publisher）
  const queryCustomNoPhotoBookName = bookName + (customBook.subTitle ? '-' + customBook.subTitle : '')
  if (hasActiveWave.value) {
    await queryGoodsApi(isbn, productId, String(injectedQuality.value), {
      bookName: queryCustomNoPhotoBookName,
      author: customBook.author || '',
      publisher: customBook.publisher || ''
    })
  } else {
    pendingGoodsQueryQueue.push({
      isbn,
      outId: productId.toString(),
      quality: String(injectedQuality.value),
      productId,
      bookName: queryCustomNoPhotoBookName,
      author: customBook.author || '',
      publisher: customBook.publisher || '',
      isSuit: true,
    })
  }

  // 第3步: 获取 salePrice
  let salePrice = null
  try {
    const req = (await import('@/utils/request')).default
    const listRes = await req.get('/product/list', {
      params: { status: 1, page: 1, page_size: 20, 'ids[0]': productId }
    })
    const list = listRes?.data?.list || listRes?.data?.data || listRes?.data?.rows || []
    if (list.length > 0) {
      salePrice = list[0].sale_price
    }
  } catch (e) {
    console.warn('[自定义无照片] 获取 salePrice 失败:', e)
  }

  // 第5步: 构建 bookData（将封面 URL 包装为 book_pic 对象供书籍预览展示）
  const previewUrl = bookPicS || ''
  const bookData = {
    bookName,
    author: customBook.author || '',
    publisher: customBook.publisher || '',
    publishDate: customBook.publication_time || '',
    binding,
    price,
    pageCount: pageCountValue,
    wordCount: wordCountValue,
    totalBook: customTotalBook ?? 1,
    ownPrice: customOwnPrice ? Number(customOwnPrice) : 0,
    isbn,
    book_pic: coverUrl ? { localPath: coverUrl, pddPath: coverUrl } : undefined,
    capturedPhoto: previewUrl
  }
  bookInfo.value = bookData as BookInfo

  // 先 emit book-info-update 更新书籍预览，再 emit update:modelValue 更新照片预览
  // 注意顺序：handleBookInfoUpdate 会清空 image.photoSrc，所以 update:modelValue 必须在其之后
  emit('book-info-update', bookData)
  emit('update:modelValue', {
    ...props.modelValue,
    photoSrc: previewUrl
  })

  // 等待父组件更新 currentGoods 后 emit photo-preview-confirm
  await nextTick()
  emit('photo-preview-confirm', {
    bookInfo: bookData,
    photoSrc: previewUrl,
    productId,
    salePrice
  })
  barcodeStatus.value = '自定义商品已添加'

  // 自动追加到当前波次
  if (hasActiveWave.value) {
    await nextTick()
    await handleAppendToWave()
  }

  // 同步书本数据到第三方平台（无照片时使用封面图 URL）
  callSyncBookApi({
    book_name: bookName,
    author: customBook.author || '',
    publisher: customBook.publisher || '',
    publication_time: toPublishTimestamp(customBook.publication_time),
    binding_layout: binding,
    fix_price: price,
    isbn,
    page_count: String(pageCountValue || ''),
    word_count: String(wordCountValue || ''),
    fid: currentSuitFid.value ?? 0,
    f_isbn: customBook.subIsbn || '',
    f_book_name: customBook.subTitle || '',
    live_image: bookPicS,
    type: '2',
  })

  console.log('[自定义无照片] 保存完成, 商品ID:', productId)
}

/**
 * 出版时间转为 10 位 Unix 时间戳
 * "YYYY-MM" → 当月 1 日 0 时的时间戳，其他格式原样返回
 */
function toPublishTimestamp(value: string): string {
  if (!value) return ''
  if (/^\d{10}$/.test(value)) return value
  if (/^\d{4}-\d{2}$/.test(value)) {
    return String(Math.floor(new Date(value + '-01T00:00:00+08:00').getTime() / 1000))
  }
  if (/^\d{4}$/.test(value)) {
    return String(Math.floor(new Date(value + '-01-01T00:00:00+08:00').getTime() / 1000))
  }
  return value
}

/**
 * 调用 /api/syncBook 同步书本数据到第三方平台
 * POST formdata 请求
 */
async function callSyncBookApi(params: {
  book_name?: string
  author?: string
  publisher?: string
  publication_time?: string
  binding_layout?: string
  fix_price?: number
  isbn?: string
  page_count?: string
  word_count?: string
  book_format?: string
  fid?: number
  f_isbn?: string
  f_book_name?: string
  live_image?: string
  type?: string
}) {
  try {
    const res = await syncBook(params)
    console.log('[syncBook] 同步成功:', res)
    return res
  } catch (err) {
    console.warn('[syncBook] 失败:', err instanceof Error ? err.message : String(err))
    return null
  }
}

defineExpose({ hasActiveWave, handlePhotoAction, retakePhoto, loadBookInfo, currentWaveId, waveNo })
</script>

<style scoped>
.camera-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
}

.camera-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  width: 100%;
}

.video-section {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}

.video-wrapper {
  width: 400px;
  height: 400px;
  border-radius: 8px;
  overflow: hidden;
  background-color: #000;
  position: relative;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.camera-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.controls {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  width: 100%;
}

.hidden-scanner-input {
  position: fixed;
  /* 用 fixed 而非 absolute,避免受父容器影响 */
  width: 1px;
  height: 1px;
  padding: 0;
  margin: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
  opacity: 0;
  z-index: 9999;
  /* 确保在最上层,能接收焦点 */
  /* 不使用 pointer-events: none,否则无法接收键盘输入 */
  left: 0;
  top: 0;
}

/* 当用户正在操作文本(复制/选择)时,临时禁用隐藏输入框的聚焦 */
.hidden-scanner-input.selection-active {
  z-index: -1;
}

.button-group {
  display: flex;
  gap: 12px;
  margin-top: 4px;
}

.status {
  font-size: 14px;
  color: #666;
  text-align: center;
  min-height: 20px;
}

.status-success {
  color: #67c23a;
}

.status-error {
  color: #f56c6c;
}

.info-section {
  /* width: 450px; */
  min-width: 550px;
  padding: 20px;
  border-radius: 12px;
  background-color: #ffffff;
  /* box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); */
  /* border: 1px solid #e5e7eb; */
  /* transition: all 0.3s ease; */
}

.book-detail {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.book-title {
  font-weight: 600;
  font-size: 18px;
  color: #1a1a1a;
  margin: 0;
  padding-bottom: 12px;
  border-bottom: 1px solid #f0f0f0;
}

/* 新的书籍内容布局 - 使用 Flex 布局替代浮动 */
.book-content {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}

.book-image-container {
  width: 120px;
  flex-shrink: 0;
}

.book-image {
  width: 120px;
  height: 160px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.book-image img {
  max-width: 100%;
  max-height: 100%;
  object-fit: cover;
}

.book-image-placeholder {
  width: 120px;
  height: 160px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed #ddd;
  border-radius: 8px;
  background-color: #f5f5f5;
}

.book-image-placeholder p {
  color: #888;
  text-align: center;
  margin: 0;
  font-size: 12px;
}

/* 右侧区域:书名和作者 */
.book-info-header {
  flex: 1;
  min-width: 200px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* 下方其他字段网格布局:一行两个 */
.book-other-fields {
  width: 100%;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin-top: 8px;
}

/* 表单项通用样式 */
.form-group {
  margin-bottom: 0;
}

.form-group label {
  display: block;
  font-size: 14px;
  font-weight: 500;
  color: #666;
  margin-bottom: 4px;
}

.form-input,
.form-textarea {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
  transition: border-color 0.3s ease;
  box-sizing: border-box;
}

.form-textarea {
  resize: vertical;
  min-height: 60px;
}

.form-input:focus,
.form-textarea:focus {
  outline: none;
  border-color: #409eff;
  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}

.no-info {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
  color: #999;
  text-align: center;
}

.no-info p {
  margin: 0;
  font-size: 14px;
}

.no-info::before {
  content: "📚";
  font-size: 32px;
  margin-bottom: 12px;
  opacity: 0.5;
}

.wave-create-section {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 12px;
}

.wave-buttons-row {
  display: flex;
  gap: 6px;
}

.wave-hint {
  font-size: 12px;
  color: #909399;
}

.wave-active-hint {
  color: #67c23a;
  font-weight: 600;
}

.wave-switch-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
  padding: 4px 0;
}

.wave-selector-label {
  font-size: 13px;
  color: #606266;
  white-space: nowrap;
}

/* 波次条形码展示区域 */
.wave-barcode-section {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  margin-top: 16px;
  padding: 16px;
  background-color: #f5f7fa;
  border-radius: 8px;
  border: 1px solid #e4e7ed;
}

.wave-no-display {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
}

.wave-no-label {
  color: #606266;
  font-weight: 500;
}

.wave-no-value {
  color: #409eff;
  font-weight: 600;
  font-size: 16px;
  font-family: 'Courier New', monospace;
}

.barcode-image-container {
  margin-top: 8px;
  padding: 12px;
  background-color: #ffffff;
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.barcode-image {
  max-width: 300px;
  max-height: 150px;
  display: block;
}

/* 新的波次条形码展示样式 */
.barcode-display-section {
  margin-top: 12px;
  padding: 16px;
  background-color: #f0f9eb;
  border: 1px solid #e1f3d8;
  border-radius: 8px;
  text-align: center;
}

.barcode-wave-no {
  margin-bottom: 12px;
  font-size: 14px;
  color: #606266;
}

.barcode-wave-no strong {
  color: #409eff;
  font-family: 'Courier New', monospace;
  font-size: 16px;
}

.barcode-img-wrapper {
  background: #ffffff;
  padding: 12px;
  border-radius: 4px;
  display: inline-block;
}

.barcode-img-wrapper img {
  max-width: 300px;
  max-height: 150px;
  display: block;
}
</style>

<style>
/** 成功提示样式 */
.el-message.scan-success-message {
  top: 15% !important;
  left: 50% !important;
  transform: translate(-50%, -50%) !important;
  display: flex;
  flex-direction: row;
  align-items: center;
  min-width: 600px;
  min-height: 160px;
  padding: 12px 20px;
  font-size: 15px;
  border-radius: 10px;
  background-color: #67C23A;
  opacity: 0.9;
  border: 1px solid #b7eb8f;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  justify-content: center;
}

.el-message.scan-success-message .el-message__icon {
  color: #ffffff;
  font-size: 18px;
}

.el-message.scan-success-message .el-message__content {
  font-size: 30px;
  color: #ffffff;
  font-weight: bold;
}

/** 错误提示样式 */
.el-message.scan-error-message {
  top: 15% !important;
  left: 50% !important;
  transform: translate(-50%, -50%) !important;
  display: flex;
  flex-direction: row;
  align-items: center;
  min-width: 600px;
  min-height: 160px;
  padding: 12px 20px;
  font-size: 15px;
  border-radius: 10px;
  background-color: #F56C6C;
  opacity: 0.9;
  border: 1px solid #fbc4c4;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  justify-content: center;
}

.el-message.scan-error-message .el-message__icon {
  color: #ffffff;
  font-size: 18px;
}

.el-message.scan-error-message .el-message__content {
  font-size: 30px;
  color: #ffffff;
  font-weight: bold;
}

/** 警告提示样式 */
.el-message.scan-warning-message {
  top: 15% !important;
  left: 50% !important;
  transform: translate(-50%, -50%) !important;
  display: flex;
  flex-direction: row;
  align-items: center;
  min-width: 600px;
  min-height: 160px;
  padding: 12px 20px;
  font-size: 15px;
  border-radius: 10px;
  background-color: #E6A23C;
  opacity: 0.9;
  border: 1px solid #f5c6a5;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  justify-content: center;
}

.el-message.scan-warning-message .el-message__icon {
  color: #ffffff;
  font-size: 18px;
}

.el-message.scan-warning-message .el-message__content {
  font-size: 30px;
  color: #ffffff;
  font-weight: bold;
}
</style>