<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted, onBeforeUnmount, nextTick, type Ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { Modal, Dropdown } from 'bootstrap'
import { getApiBase, normalizePublicAssetUrl, TOKEN_KEY } from '../config/api'

/** @types/bootstrap 未完全对齐 5.x 运行时 API */
type BootstrapDropdownInstance = { toggle: () => void; hide: () => void }

const DropdownCompat = Dropdown as unknown as {
  getOrCreateInstance: (
    element: HTMLElement,
    options?: Record<string, unknown>,
  ) => BootstrapDropdownInstance
  getInstance: (element: HTMLElement) => BootstrapDropdownInstance | null
}
import { cityMap, provinceOrder } from '../data/chinaRegion'
import {
  brandingFormFromApi,
  emptyBrandingForm,
  hasVisibleText,
  participantApplyPageTitle,
  type BrandingForm,
} from '../utils/competitionBranding'
import { normalizeSignupSchema, type SignupFormSchemaField } from '../utils/defaultSignupFormSchema'

const router = useRouter()
const route = useRoute()

const slug = computed(() => String(route.params.slug ?? '').trim())

const allowedExt = ['pdf', 'ppt', 'pptx', 'doc', 'docx', 'wps', 'rar', 'zip']
const maxFileSize = 20 * 1024 * 1024
const MOBILE_PATTERN = '^1[3-9]\\d{9}$'

interface FileItem {
  id: string | number
  file?: File
  fromServer?: boolean
  original_name?: string
  size?: number
  url?: string
  previewUrl?: string
}

interface PublicTrackRow {
  track_code: string
  title: string
  description: string | null
  sort: number
}

function templateRefEl<T extends HTMLElement>(r: Ref<T | T[] | null>): T | null {
  const v = r.value
  if (v == null) return null
  return Array.isArray(v) ? v[0] ?? null : v
}

/** 过滤后台 schema 里遗留的开发者说明，不在选手端展示（规则尽量窄，避免误伤运营填写的正常说明） */
function participantFieldHelp(help?: string): string {
  const t = (help ?? '').trim()
  if (!t) return ''
  if (
    /applications\.|config\/contest|与提交校验|\^1\[3-9\]|kind=\s*plan|kind=\s*supporting|\\d\{9\}/i.test(t)
  ) {
    return ''
  }
  return t
}

function apiBase() {
  return getApiBase()
}

function competitionQuery(): string {
  const s = slug.value
  return s ? `?competition_slug=${encodeURIComponent(s)}` : ''
}

function authHeaders(isJson: boolean): HeadersInit {
  const t = localStorage.getItem(TOKEN_KEY) || ''
  const h: Record<string, string> = { Authorization: `Bearer ${t}`, Accept: 'application/json' }
  if (isJson) h['Content-Type'] = 'application/json'
  return h
}

function goLogin() {
  localStorage.removeItem(TOKEN_KEY)
  const s = slug.value
  if (s) {
    void router.push({
      name: 'participant-login',
      params: { slug: s },
      query: { redirect: route.fullPath },
    })
  } else {
    void router.push('/c')
  }
}

const schemaFields = ref<SignupFormSchemaField[]>(normalizeSignupSchema([]))
const competitionTracks = ref<PublicTrackRow[]>([])
/** 赛事 /public API 已成功解析后为 true，避免 tracks 尚未返回时出现黄色提示闪烁 */
const competitionHydrated = ref(false)
const competitionName = ref('')
/** 赛事配置的参赛承诺书正文（HTML）；空则选手端用默认文案 */
const pledgeContentHtml = ref('')

const DEFAULT_PLEDGE_BODY_HTML =
  '<p>本项目申报信息中所填写的各栏目内容真实、准确。本项目负责人（团队）对申报材料的真实性负完全责任。</p><p>若申报信息中存在虚假、伪造等不实情况，本项目负责人（团队）将积极配合调查，并按照有关规定接受处理。</p>'

const pledgeBodyDisplayHtml = computed(() => {
  const h = pledgeContentHtml.value.trim()
  return h || DEFAULT_PLEDGE_BODY_HTML
})
const brand = ref<BrandingForm>(emptyBrandingForm())
const loadError = ref('')

const formModel = reactive<Record<string, string>>({})

function ensureFormKeys(fields: SignupFormSchemaField[]) {
  for (const f of fields) {
    if (f.type === 'file') continue
    if (!(f.key in formModel)) formModel[f.key] = ''
  }
  if (fields.some((f) => f.key === 'commitment_accepted') && !('promise_signature' in formModel)) {
    formModel.promise_signature = ''
  }
}

const STORAGE_KEY = computed(() => `jscc_signup_form_draft_v2_${slug.value || 'x'}`)

const applicationStatus = ref<'draft' | 'submitted'>('draft')
/** 与后端 participant_may_edit 对齐：有评审记录后为 false，表单整表禁用 */
const participantMayEdit = ref(true)

const trackInvalid = ref(false)
const trackHiddenSelect = ref<HTMLSelectElement | HTMLSelectElement[] | null>(null)
const trackDropdownBtn = ref<HTMLButtonElement | HTMLButtonElement[] | null>(null)

/** 与原型一致：主文案 + 简介（赛事 tracks API 的 description） */
const trackMainText = computed(() => {
  const code = formModel.track
  const t = competitionTracks.value.find((x) => x.track_code === code)
  return t ? t.title : code ? code : '请选择'
})
const trackMainClass = computed(() => (formModel.track ? 'text-body' : 'text-secondary'))
const trackNoteText = computed(() => {
  const code = formModel.track
  const t = competitionTracks.value.find((x) => x.track_code === code)
  return (t?.description ?? '').trim()
})

function trackPickDescription(desc: string | null | undefined): string {
  return (desc ?? '').trim()
}

function pickTrack(value: string) {
  formModel.track = value
  trackInvalid.value = false
  const btn = templateRefEl(trackDropdownBtn)
  if (btn) DropdownCompat.getInstance(btn)?.hide()
}

const isChina = computed(() => formModel.location_country === '中国')
const isOversea = computed(() => formModel.location_country === '海外')

const provinceOptions = computed(() =>
  provinceOrder.map((p) => ({ label: p, value: p })),
)

const citySelectOptions = computed(() => {
  const p = formModel.location_province
  if (!p) return []
  return (cityMap[p] || []).map((c) => ({ label: c, value: c }))
})

function syncLocationFields() {
  if (isOversea.value) {
    formModel.location_province = ''
    formModel.location_city = ''
  } else if (!isChina.value) {
    formModel.location_province = ''
    formModel.location_city = ''
    formModel.oversea_country = ''
  }
}

watch(
  () => formModel.location_country,
  () => {
    syncLocationFields()
  },
)

watch(
  () => formModel.location_province,
  (newProv, oldProv) => {
    if (!isChina.value) return
    if (newProv === oldProv) return
    // 从「未选省」变为某省：不清空城市（服务端/草稿一次性回填省、市时避免把已回填的城市抹掉）
    if (oldProv == null || String(oldProv).trim() === '') return
    formModel.location_city = ''
  },
)

const planFileItems = ref<FileItem[]>([])
const supportingFileItems = ref<FileItem[]>([])
const planFileSavedInfo = ref('')
const supportingFilesSavedInfo = ref('')

const planFileFeedback = ref('')
const supportingFilesFeedback = ref('')

const wasValidated = ref(false)
const formDisabled = computed(() => !participantMayEdit.value)

const applyFormEl = ref<HTMLFormElement | null>(null)
/** 在 v-for 中绑定时 Vue 可能将 ref 设为元素数组 */
const planFileInput = ref<HTMLInputElement | HTMLInputElement[] | null>(null)
const supportingFileInput = ref<HTMLInputElement | HTMLInputElement[] | null>(null)

const introCount = computed(() => (formModel.intro || '').length)

const formPageTitle = computed(() => participantApplyPageTitle(brand.value, competitionName.value))

/** 后台「报名填报页 → 副标题」：整页填报说明（与单项 field.help 不同） */
const applyPageSubtitle = computed(() => {
  const s = brand.value.apply?.headerSubtitle
  return hasVisibleText(s) ? String(s).trim() : ''
})

/** 原生校验失败时提示：空值与格式错误区分用语 */
function nativeInputInvalidFeedback(field: SignupFormSchemaField): string {
  const v = String(formModel[field.key] ?? '').trim()
  if (field.type === 'email' || field.key === 'contact_email') {
    if (!v) return `请填写${field.label}`
    return '请输入正确的邮箱格式'
  }
  if (field.type === 'tel' || field.key === 'contact_mobile') {
    if (!v) return `请填写${field.label}`
    return '请输入正确的11位中国大陆手机号'
  }
  return `请填写${field.label}`
}

function fieldColClass(field: SignupFormSchemaField): string {
  if (field.type === 'textarea' || field.type === 'file' || field.type === 'checkbox') return 'col-12'
  return 'col-md-4'
}

/** 多行文本：配置项 placeholder 改在输入框下方展示 */
function textareaFieldPlaceholder(field: SignupFormSchemaField): string {
  if (field.type !== 'textarea') return ''
  return String(field.placeholder ?? '').trim()
}

function isFieldVisible(field: SignupFormSchemaField): boolean {
  if (field.type === 'file') return true
  if (field.key === 'location_province' || field.key === 'location_city') {
    return isChina.value
  }
  if (field.key === 'oversea_country') return isOversea.value
  return true
}

function effectiveRequired(field: SignupFormSchemaField): boolean {
  if (field.key === 'location_province' || field.key === 'location_city') {
    return isChina.value
  }
  if (field.key === 'oversea_country') {
    return isOversea.value
  }
  return field.required
}

/** 与 schema 顺序一致：含文件上传等非隐藏字段 */
const orderedSignupFields = computed(() => schemaFields.value.filter((f) => isFieldVisible(f)))

function optionsForField(field: SignupFormSchemaField): { label: string; value: string }[] {
  if (field.key === 'track') {
    return competitionTracks.value.map((t) => ({
      label: t.title,
      value: t.track_code,
    }))
  }
  if (field.key === 'location_province') {
    return provinceOptions.value
  }
  if (field.key === 'location_city' && isChina.value) {
    return citySelectOptions.value
  }
  return field.options ?? []
}

function isCitySelectField(field: SignupFormSchemaField): boolean {
  return field.key === 'location_city' && isChina.value
}

function formatFileSize(bytes: number) {
  if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1).replace(/\.0$/, '')}M`
  if (bytes >= 1024) return `${Math.ceil(bytes / 1024)}K`
  return `${bytes}B`
}

function fileItemDisplayName(item: FileItem) {
  return item.file ? item.file.name : item.original_name || ''
}

function formatApiErrSafe(data: Record<string, unknown>) {
  const msg = (data.message as string) || ''
  const errObj = data.errors as Record<string, string[]> | undefined
  if (errObj && Object.keys(errObj).length) return `${msg}\n${JSON.stringify(errObj)}`
  return msg || '操作失败'
}

const noticeModalEl = ref<HTMLElement | null>(null)
const noticeTitle = ref('提示')
const noticeBody = ref('')
const noticeIsSuccess = ref(false)

function showNotice(message: string, title = '提示', type: 'success' | 'warning' = 'warning') {
  noticeTitle.value = title
  noticeBody.value = message
  noticeIsSuccess.value = type === 'success'
  void nextTick(() => {
    const el = noticeModalEl.value
    if (el) Modal.getOrCreateInstance(el).show()
  })
}

/** 与后端 `ApplicationController` 校验上限一致（base64 图片） */
const PROMISE_SIG_MAX_CHARS = 2_097_152

const promiseSignModalRef = ref<HTMLElement | null>(null)
const promiseCanvasRef = ref<HTMLCanvasElement | null>(null)
const promiseModalDateText = ref('')
const promiseSigHintVisible = ref(true)

let promiseSigCtx: CanvasRenderingContext2D | null = null
let promiseSigCssW = 520
let promiseSigCssH = 140
let promiseSigDrawing = false
let promiseSigHasInk = false
let promiseSigListenersAbort: AbortController | null = null
let promiseModalBootstrapCleanup: (() => void) | null = null

function teardownPromiseSigListeners() {
  promiseSigListenersAbort?.abort()
  promiseSigListenersAbort = null
}

const commitmentSigned = computed(
  () => formModel.commitment_accepted === '1' && String(formModel.promise_signature ?? '').trim() !== '',
)

const promiseModalHeading = computed(() => {
  const n = competitionName.value.trim()
  return n ? `${n} 赛事承诺书` : '赛事承诺书'
})

function formatCnDate(d: Date): string {
  return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`
}

function promiseSigPos(
  ev: MouseEvent | TouchEvent,
  canvas: HTMLCanvasElement,
): { x: number; y: number } {
  const rect = canvas.getBoundingClientRect()
  if ('touches' in ev && ev.touches[0]) {
    const t = ev.touches[0]
    return { x: t.clientX - rect.left, y: t.clientY - rect.top }
  }
  const m = ev as MouseEvent
  return { x: m.clientX - rect.left, y: m.clientY - rect.top }
}

function layoutPromiseCanvas() {
  const canvas = promiseCanvasRef.value
  if (!canvas) return
  const wrap = canvas.closest('.promise-sig-wrap') as HTMLElement | null
  const dpr = window.devicePixelRatio || 1
  const canvasStyle = window.getComputedStyle(canvas)
  promiseSigCssW = Math.max(280, Math.floor(wrap?.clientWidth || 520))
  promiseSigCssH = Math.max(76, Math.floor(parseFloat(canvasStyle.height) || 110))
  canvas.style.width = `${promiseSigCssW}px`
  canvas.style.height = `${promiseSigCssH}px`
  canvas.width = Math.floor(promiseSigCssW * dpr)
  canvas.height = Math.floor(promiseSigCssH * dpr)
  promiseSigCtx = canvas.getContext('2d')
  if (!promiseSigCtx) return
  promiseSigCtx.setTransform(1, 0, 0, 1, 0, 0)
  promiseSigCtx.scale(dpr, dpr)
  promiseSigCtx.fillStyle = '#ffffff'
  promiseSigCtx.fillRect(0, 0, promiseSigCssW, promiseSigCssH)
  promiseSigCtx.strokeStyle = '#111111'
  promiseSigCtx.lineWidth = 2
  promiseSigCtx.lineCap = 'round'
  promiseSigCtx.lineJoin = 'round'
  promiseSigHasInk = false
  promiseSigHintVisible.value = true
}

function paintExistingPromiseSignature() {
  const url = String(formModel.promise_signature ?? '').trim()
  if (!url || !promiseSigCtx) return
  const img = new Image()
  img.onload = () => {
    if (!promiseSigCtx) return
    promiseSigCtx.drawImage(img, 0, 0, promiseSigCssW, promiseSigCssH)
    promiseSigHasInk = true
    promiseSigHintVisible.value = false
  }
  img.src = url
}

function bindPromiseSigCanvas() {
  const canvas = promiseCanvasRef.value
  if (!canvas || !promiseSigCtx) return
  teardownPromiseSigListeners()
  promiseSigListenersAbort = new AbortController()
  const { signal } = promiseSigListenersAbort

  const start = (ev: MouseEvent | TouchEvent) => {
    if ('touches' in ev) ev.preventDefault()
    if (!promiseSigCtx) return
    promiseSigDrawing = true
    const p = promiseSigPos(ev, canvas)
    promiseSigCtx.beginPath()
    promiseSigCtx.moveTo(p.x, p.y)
  }
  const move = (ev: MouseEvent | TouchEvent) => {
    if (!promiseSigDrawing || !promiseSigCtx) return
    if ('touches' in ev) ev.preventDefault()
    const p = promiseSigPos(ev, canvas)
    promiseSigCtx.lineTo(p.x, p.y)
    promiseSigCtx.stroke()
    promiseSigHasInk = true
    promiseSigHintVisible.value = false
  }
  const end = () => {
    promiseSigDrawing = false
  }

  canvas.addEventListener('mousedown', start, { signal })
  canvas.addEventListener('mousemove', move, { signal })
  window.addEventListener('mouseup', end, { signal })
  canvas.addEventListener('touchstart', start, { passive: false, signal })
  canvas.addEventListener('touchmove', move, { passive: false, signal })
  canvas.addEventListener('touchend', end, { signal })
  canvas.addEventListener('touchcancel', end, { signal })
}

/** Bootstrap 事件名含点号，勿用模板 @shown.bs.modal（Vue 会当成事件修饰符解析坏） */
function initPromiseCanvasAfterModalOpen() {
  promiseModalDateText.value = formatCnDate(new Date())
  const run = (attempt: number) => {
    const canvas = promiseCanvasRef.value
    if (!canvas) {
      if (attempt < 40) requestAnimationFrame(() => run(attempt + 1))
      return
    }
    layoutPromiseCanvas()
    bindPromiseSigCanvas()
    const existing = String(formModel.promise_signature ?? '').trim()
    if (existing && formModel.commitment_accepted === '1') paintExistingPromiseSignature()
  }
  void nextTick(() => {
    requestAnimationFrame(() => run(0))
  })
}

function attachPromiseModalBootstrapListeners(el: HTMLElement) {
  promiseModalBootstrapCleanup?.()
  const onShown = () => {
    initPromiseCanvasAfterModalOpen()
  }
  const onHidden = () => {
    teardownPromiseSigListeners()
  }
  el.addEventListener('shown.bs.modal', onShown)
  el.addEventListener('hidden.bs.modal', onHidden)
  promiseModalBootstrapCleanup = () => {
    el.removeEventListener('shown.bs.modal', onShown)
    el.removeEventListener('hidden.bs.modal', onHidden)
    promiseModalBootstrapCleanup = null
  }
}

watch(
  promiseSignModalRef,
  (el) => {
    promiseModalBootstrapCleanup?.()
    if (el) attachPromiseModalBootstrapListeners(el)
  },
  { flush: 'post' },
)

function openPromiseSignModal() {
  if (formDisabled.value) return
  const el = promiseSignModalRef.value
  if (el) Modal.getOrCreateInstance(el).show()
}

function clearPromiseSignatureCanvas() {
  layoutPromiseCanvas()
  bindPromiseSigCanvas()
}

function confirmPromiseSignature() {
  const canvas = promiseCanvasRef.value
  if (!promiseSigHasInk || !canvas) {
    showNotice('请先在签名区域内手写签名后再确认。', '请确认', 'warning')
    return
  }
  const dataUrl = canvas.toDataURL('image/png')
  if (dataUrl.length > PROMISE_SIG_MAX_CHARS) {
    showNotice('签名数据过大，请清除后重新签名或联系管理员。', '提示', 'warning')
    return
  }
  formModel.promise_signature = dataUrl
  formModel.commitment_accepted = '1'
  const el = promiseSignModalRef.value
  if (el) Modal.getInstance(el)?.hide()
}

function buildApiPayload(): Record<string, unknown> {
  const o: Record<string, unknown> = {}
  for (const f of schemaFields.value) {
    if (f.type === 'file') continue
    if (f.type === 'checkbox') {
      o[f.key] = formModel[f.key] === '1'
      continue
    }
    o[f.key] = formModel[f.key] ?? ''
  }
  if (schemaFields.value.some((f) => f.key === 'commitment_accepted')) {
    o.promise_signature = formModel.promise_signature ?? ''
  }
  return o
}

function validateFileItems(
  items: FileItem[],
  missingMessage: string,
  optional: boolean,
): { ok: boolean; feedback: string } {
  const realFiles = items.filter((i) => i.file)
  const serverOnly = items.filter((i) => i.fromServer && !i.file)
  if (!realFiles.length && !serverOnly.length) {
    return { ok: optional, feedback: optional ? '' : missingMessage }
  }
  const invalidExtFile = realFiles.map((i) => i.file!).find((file) => {
    const ext = (file.name.split('.').pop() || '').toLowerCase()
    return !allowedExt.includes(ext)
  })
  if (invalidExtFile) {
    return {
      ok: false,
      feedback: `“${invalidExtFile.name}”格式不支持，请上传 PDF/PPT/PPTX/DOC/DOCX/WPS/RAR/ZIP`,
    }
  }
  const oversizedFile = realFiles.map((i) => i.file!).find((file) => file.size > maxFileSize)
  if (oversizedFile) {
    return { ok: false, feedback: `“${oversizedFile.name}”大小不能超过20M` }
  }
  return { ok: true, feedback: '' }
}

function schemaHasPlan() {
  return schemaFields.value.some((f) => f.key === 'plan' && f.type === 'file')
}

function schemaHasSupporting() {
  return schemaFields.value.some((f) => f.key === 'supporting' && f.type === 'file')
}

function validatePlanFile() {
  if (!schemaHasPlan()) {
    templateRefEl(planFileInput)?.setCustomValidity('')
    return true
  }
  const r = validateFileItems(planFileItems.value, '请上传商业计划书', false)
  planFileFeedback.value = r.feedback
  templateRefEl(planFileInput)?.setCustomValidity(r.ok ? '' : 'missing')
  return r.ok
}

function validateSupportingFiles() {
  if (!schemaHasSupporting()) {
    templateRefEl(supportingFileInput)?.setCustomValidity('')
    return true
  }
  const r = validateFileItems(supportingFileItems.value, '文件格式或大小不符合要求', true)
  supportingFilesFeedback.value = r.feedback
  templateRefEl(supportingFileInput)?.setCustomValidity(r.ok ? '' : 'bad')
  return r.ok
}

function validateForm(): boolean {
  wasValidated.value = true
  validatePlanFile()
  validateSupportingFiles()

  const trackField = schemaFields.value.find((f) => f.key === 'track')
  if (trackField && isFieldVisible(trackField)) {
    trackInvalid.value = !formModel.track
    const trackEl = templateRefEl(trackHiddenSelect)
    if (trackEl) trackEl.setCustomValidity(formModel.track ? '' : '请选择主题赛道')
  } else {
    trackInvalid.value = false
    templateRefEl(trackHiddenSelect)?.setCustomValidity('')
  }

  const native = applyFormEl.value?.checkValidity() ?? false
  if (!native) return false
  if (trackField && effectiveRequired(trackField) && !formModel.track) return false
  if (!validatePlanFile() || !validateSupportingFiles()) return false

  const commitmentField = schemaFields.value.find((f) => f.key === 'commitment_accepted')
  if (
    commitmentField &&
    (formModel.commitment_accepted !== '1' || !String(formModel.promise_signature ?? '').trim())
  ) {
    showNotice('请先阅读并完成参赛承诺书的手写签署。', '提示', 'warning')
    return false
  }
  return true
}

async function uploadNewFiles(target: 'plan' | 'supporting') {
  const token = localStorage.getItem(TOKEN_KEY)
  if (!token) return
  const items = target === 'plan' ? planFileItems.value : supportingFileItems.value
  const pending = items.filter((i) => i.file && !i.fromServer)
  if (pending.length === 0) return
  for (const item of pending) {
    if (!item.file) continue
    const fd = new FormData()
    fd.append('kind', target === 'plan' ? 'plan' : 'supporting')
    fd.append('file', item.file)
    const r = await fetch(`${apiBase()}/api/applications/current/files${competitionQuery()}`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
      body: fd,
    })
    const data = (await r.json().catch(() => ({}))) as Record<string, unknown>
    if (r.status === 401) {
      goLogin()
      return
    }
    if (!r.ok) {
      showNotice(formatApiErrSafe(data), '提示', 'warning')
      return
    }
    const blobUrl = item.previewUrl
    item.id = data.id as number
    item.fromServer = true
    item.original_name = data.original_name as string
    item.size = data.size as number
    const fileUrl = normalizePublicAssetUrl(String(data.url ?? ''))
    item.url = fileUrl
    item.previewUrl = fileUrl
    if (blobUrl && String(blobUrl).startsWith('blob:')) URL.revokeObjectURL(blobUrl)
    delete item.file
  }
  showNotice('文件已上传。', '上传成功', 'success')
  if (target === 'plan') validatePlanFile()
  else validateSupportingFiles()
}

async function removeFile(id: string | number, target: 'plan' | 'supporting') {
  let items = target === 'plan' ? [...planFileItems.value] : [...supportingFileItems.value]
  const item = items.find((e) => String(e.id) === String(id))
  if (!item) return
  if (item.fromServer && typeof item.id === 'number') {
    const token = localStorage.getItem(TOKEN_KEY)
    if (!token) {
      goLogin()
      return
    }
    const r = await fetch(
      `${apiBase()}/api/applications/current/files/${item.id}${competitionQuery()}`,
      {
        method: 'DELETE',
        headers: authHeaders(false),
      },
    )
    if (r.status === 401) {
      goLogin()
      return
    }
    if (!r.ok) {
      showNotice('删除失败', '提示', 'warning')
      return
    }
  }
  if (item.previewUrl && String(item.previewUrl).startsWith('blob:')) {
    URL.revokeObjectURL(item.previewUrl as string)
  }
  items = items.filter((e) => String(e.id) !== String(id))
  if (target === 'plan') planFileItems.value = items
  else supportingFileItems.value = items
  wasValidated.value = true
  if (target === 'plan') validatePlanFile()
  else validateSupportingFiles()
}

function addFiles(fileList: FileList | null, target: 'plan' | 'supporting') {
  if (!fileList?.length) return
  const items = target === 'plan' ? planFileItems.value : supportingFileItems.value
  Array.from(fileList).forEach((file) => {
    items.push({
      id: `tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`,
      file,
      previewUrl: URL.createObjectURL(file),
      fromServer: false,
    })
  })
  if (target === 'plan') {
    planFileItems.value = [...items]
    planFileSavedInfo.value = ''
    const p = templateRefEl(planFileInput)
    if (p) p.value = ''
    wasValidated.value = true
    validatePlanFile()
  } else {
    supportingFileItems.value = [...items]
    supportingFilesSavedInfo.value = ''
    const s = templateRefEl(supportingFileInput)
    if (s) s.value = ''
    wasValidated.value = true
    validateSupportingFiles()
  }
  void uploadNewFiles(target)
}

function getDraftData() {
  return {
    formModel: { ...formModel },
    planFileNames: planFileItems.value.map(fileItemDisplayName).filter(Boolean),
    supportingFileNames: supportingFileItems.value.map(fileItemDisplayName).filter(Boolean),
  }
}

function saveDraftLocal() {
  const draft = getDraftData()
  localStorage.setItem(STORAGE_KEY.value, JSON.stringify(draft))
  if (draft.planFileNames.length) {
    planFileSavedInfo.value = `已保存商业计划书名称：${draft.planFileNames.join('、')}`
  } else planFileSavedInfo.value = ''
  if (draft.supportingFileNames.length) {
    supportingFilesSavedInfo.value = `已保存其他佐证材料名称：${draft.supportingFileNames.join('、')}`
  } else supportingFilesSavedInfo.value = ''
}

function restoreDraft() {
  const raw = localStorage.getItem(STORAGE_KEY.value)
  if (!raw) return
  try {
    const draft = JSON.parse(raw) as { formModel?: Record<string, string>; planFileNames?: string[]; supportingFileNames?: string[] }
    if (draft.formModel && typeof draft.formModel === 'object') {
      for (const k of Object.keys(draft.formModel)) {
        formModel[k] = draft.formModel[k] ?? ''
      }
    }
    const restoredPlan = draft.planFileNames || []
    if (restoredPlan.length) {
      planFileSavedInfo.value = `上次保存商业计划书名称：${restoredPlan.join('、')}（浏览器限制，需重新选择后预览或删除）`
    }
    const sup = draft.supportingFileNames
    if (sup?.length) {
      supportingFilesSavedInfo.value = `上次保存其他佐证材料名称：${sup.join('、')}（浏览器限制，需重新选择后预览或删除）`
    }
    syncLocationFields()
  } catch {
    localStorage.removeItem(STORAGE_KEY.value)
  }
}

async function loadPublicCompetition() {
  competitionHydrated.value = false
  const s = slug.value
  if (!s) {
    loadError.value = '缺少赛事地址'
    return
  }
  loadError.value = ''
  try {
    const r = await fetch(`${apiBase()}/api/v1/public/competitions/by-slug/${encodeURIComponent(s)}`, {
      headers: { Accept: 'application/json' },
    })
    if (!r.ok) {
      loadError.value = r.status === 404 ? '赛事不存在或未发布' : '无法加载赛事配置'
      return
    }
    const raw = (await r.json()) as Record<string, unknown>
    const data =
      raw.data != null && typeof raw.data === 'object' && !Array.isArray(raw.data)
        ? (raw.data as Record<string, unknown>)
        : raw

    competitionName.value = String(data.name ?? '')
    pledgeContentHtml.value =
      typeof data.pledge_content_html === 'string' ? data.pledge_content_html : ''
    const tr = data.tracks
    competitionTracks.value = Array.isArray(tr) ? (tr as PublicTrackRow[]) : []

    let schemaRaw: unknown = data.signup_form_schema
    if (typeof schemaRaw === 'string') {
      try {
        schemaRaw = JSON.parse(schemaRaw) as unknown
      } catch {
        schemaRaw = []
      }
    }
    schemaFields.value = normalizeSignupSchema(schemaRaw)
    ensureFormKeys(schemaFields.value)
    brand.value = brandingFormFromApi(data.branding_json ?? null)
    const dt = brand.value.documentTitle
    if (hasVisibleText(dt) && typeof document !== 'undefined') {
      document.title = dt.trim()
    }
    competitionHydrated.value = true
  } catch {
    loadError.value = '网络错误，无法加载赛事'
  }
}

function applyServerPayload(d: {
  status?: string
  participant_may_edit?: boolean
  player_name?: string
  school?: string
  degree?: string
  contact_email?: string
  contact_mobile?: string
  company_name?: string
  project_name?: string
  track?: string
  location_country?: string
  location_province?: string
  location_city?: string
  oversea_country?: string
  intro?: string
  promise_signed_at?: string | null
  promise_signature?: string | null
  files?: { id: number; kind: string; original_name: string; size: number; url: string }[]
}) {
  applicationStatus.value = (d.status as 'draft' | 'submitted') || 'draft'
  participantMayEdit.value = d.participant_may_edit !== false
  formModel.player_name = d.player_name || ''
  formModel.school = d.school || ''
  formModel.degree = d.degree || ''
  formModel.contact_email = d.contact_email || ''
  formModel.contact_mobile = d.contact_mobile || ''
  formModel.company_name = d.company_name || ''
  formModel.project_name = d.project_name || ''
  formModel.track = d.track || ''
  formModel.location_country = d.location_country || ''
  formModel.location_province = d.location_province || ''
  formModel.location_city = d.location_city || ''
  formModel.oversea_country = d.oversea_country || ''
  formModel.intro = d.intro || ''
  const sig = typeof d.promise_signature === 'string' ? d.promise_signature : ''
  formModel.promise_signature = sig
  formModel.commitment_accepted = sig.trim() && d.promise_signed_at ? '1' : ''
  syncLocationFields()
  planFileItems.value = (d.files || [])
    .filter((f) => f.kind === 'plan')
    .map((f) => {
      const u = normalizePublicAssetUrl(f.url)
      return {
        id: f.id,
        fromServer: true,
        original_name: f.original_name,
        size: f.size,
        url: u,
        previewUrl: u,
      }
    })
  supportingFileItems.value = (d.files || [])
    .filter((f) => f.kind === 'supporting')
    .map((f) => {
      const u = normalizePublicAssetUrl(f.url)
      return {
        id: f.id,
        fromServer: true,
        original_name: f.original_name,
        size: f.size,
        url: u,
        previewUrl: u,
      }
    })
  validatePlanFile()
  validateSupportingFiles()
}

async function loadApplicationFromServer() {
  const token = localStorage.getItem(TOKEN_KEY)
  if (!token) return false
  const r = await fetch(`${apiBase()}/api/applications/current${competitionQuery()}`, {
    headers: authHeaders(false),
  })
  if (r.status === 401) {
    localStorage.removeItem(TOKEN_KEY)
    goLogin()
    return false
  }
  if (!r.ok) return false
  const d = (await r.json()) as Parameters<typeof applyServerPayload>[0]
  applyServerPayload(d)
  return true
}

async function saveDraftToServer() {
  const r = await fetch(`${apiBase()}/api/applications/current${competitionQuery()}`, {
    method: 'PUT',
    headers: authHeaders(true),
    body: JSON.stringify(buildApiPayload()),
  })
  if (r.status === 401) {
    goLogin()
    return false
  }
  if (!r.ok) {
    void r.json().catch(() => ({}))
    showNotice('草稿未能保存，请检查网络或稍后重试。', '保存失败', 'warning')
    return false
  }
  try {
    const d = (await r.json()) as Parameters<typeof applyServerPayload>[0]
    applyServerPayload(d)
  } catch {
    /* 非 JSON 时保持本地状态 */
  }
  return true
}

async function submitApplicationToServer() {
  const r = await fetch(`${apiBase()}/api/applications/current/submit${competitionQuery()}`, {
    method: 'POST',
    headers: authHeaders(true),
    body: JSON.stringify(buildApiPayload()),
  })
  if (r.status === 401) {
    goLogin()
    return false
  }
  if (!r.ok) {
    void r.json().catch(() => ({}))
    showNotice('报名未能提交，请检查网络或稍后重试。', '提交失败', 'warning')
    return false
  }
  const d = (await r.json()) as Parameters<typeof applyServerPayload>[0]
  applyServerPayload(d)
  return true
}

async function onSaveClick() {
  if (!localStorage.getItem(TOKEN_KEY)) {
    goLogin()
    return
  }
  const ok = await saveDraftToServer()
  if (!ok) return
  saveDraftLocal()
  showNotice('已保存草稿（按当前已填写信息暂存）。', '保存成功', 'success')
}

async function onSubmitClick() {
  if (!localStorage.getItem(TOKEN_KEY)) {
    goLogin()
    return
  }
  if (!validateForm()) return
  const ok = await submitApplicationToServer()
  if (!ok) return
  showNotice('已提交报名', '提交成功', 'success')
}

onBeforeUnmount(() => {
  promiseModalBootstrapCleanup?.()
  teardownPromiseSigListeners()
})

onMounted(() => {
  void (async () => {
    await loadPublicCompetition()
    if (loadError.value) return
    const loaded = await loadApplicationFromServer()
    if (!loaded) restoreDraft()
    syncLocationFields()
  })()
})
</script>

<template>
  <div class="apply-form-page">
    <div class="form-page-header flex-shrink-0 mb-3">
      <h5 class="mb-0 form-page-title">{{ formPageTitle }}</h5>
      <p v-if="!loadError && applyPageSubtitle" class="text-secondary prototype-subtitle mb-0 mt-2">{{ applyPageSubtitle }}</p>
      <p v-if="loadError" class="text-danger small mb-0 mt-2">{{ loadError }}</p>
      <p v-else-if="competitionHydrated && competitionTracks.length === 0 && schemaFields.some((f) => f.key === 'track')" class="text-warning small mb-0 mt-2">
        本场赛事尚未在「赛道管理」中配置可用赛道，提交时可能会失败，请联系管理员。
      </p>
    </div>

    <div v-if="!loadError" class="apply-form-scroll">
    <div class="card mb-0">
      <div class="card-body">
        <form
          id="applyForm"
          ref="applyFormEl"
          class="row g-3"
          :class="{ 'was-validated': wasValidated }"
          @submit.prevent
        >
          <template v-for="field in orderedSignupFields" :key="field.type === 'file' ? 'f-' + field.key : field.key">
            <div v-if="field.key === 'track'" class="col-md-4">
              <label class="form-label" id="trackFieldLabel"
                >{{ field.label }} <span v-if="effectiveRequired(field)" class="text-danger">*</span></label
              >
              <div class="track-custom-select w-100">
                <select
                  id="track"
                  ref="trackHiddenSelect"
                  v-model="formModel.track"
                  class="visually-hidden"
                  :required="effectiveRequired(field)"
                  tabindex="-1"
                  aria-hidden="true"
                  :disabled="formDisabled"
                >
                  <option value="">请选择</option>
                  <option v-for="t in competitionTracks" :key="t.track_code" :value="t.track_code">
                    {{ t.title }}
                  </option>
                </select>
                <div class="dropdown w-100">
                  <button
                    ref="trackDropdownBtn"
                    type="button"
                    class="dropdown-toggle form-select track-custom-toggle editable w-100 text-start"
                    aria-labelledby="trackFieldLabel"
                    aria-expanded="false"
                    data-bs-toggle="dropdown"
                    data-bs-display="static"
                    data-bs-auto-close="true"
                    :disabled="formDisabled"
                    :class="{ 'is-invalid': trackInvalid && wasValidated }"
                  >
                    <span class="track-toggle-label text-truncate">
                      <span :class="trackMainClass">{{ trackMainText }}</span>
                      <span v-if="trackNoteText" class="track-custom-note">{{ trackNoteText }}</span>
                    </span>
                  </button>
                  <ul class="dropdown-menu w-100 border shadow-sm rounded-2 py-0 my-1 track-custom-menu">
                    <li>
                      <button
                        type="button"
                        class="dropdown-item track-pick border-0 w-100 text-start rounded-0"
                        @click="pickTrack('')"
                      >
                        <span class="text-secondary">请选择</span>
                      </button>
                    </li>
                    <li v-for="t in competitionTracks" :key="t.track_code">
                      <button
                        type="button"
                        class="dropdown-item track-pick border-0 w-100 text-start rounded-0"
                        @click="pickTrack(t.track_code)"
                      >
                        <span class="track-pick-inline">
                          <span class="text-body">{{ t.title }}</span>
                          <span v-if="trackPickDescription(t.description)" class="track-custom-note">{{
                            trackPickDescription(t.description)
                          }}</span>
                        </span>
                      </button>
                    </li>
                  </ul>
                  <div class="invalid-feedback">请选择主题赛道</div>
                </div>
              </div>
              <small
                v-if="participantFieldHelp(field.help)"
                class="text-secondary prototype-subtitle d-block"
                >{{ participantFieldHelp(field.help) }}</small
              >
            </div>

            <div v-else-if="field.type === 'file' && field.key === 'plan'" class="col-12">
              <label class="form-label"
                >{{ field.label }} <span v-if="field.required" class="text-danger">*</span></label
              >
              <input
                ref="planFileInput"
                type="file"
                class="form-control editable"
                :class="{ 'is-invalid': wasValidated && !!planFileFeedback }"
                accept=".pdf,.ppt,.pptx,.doc,.docx,.wps,.rar,.zip"
                multiple
                :disabled="formDisabled"
                @change="addFiles(($event.target as HTMLInputElement).files, 'plan')"
              />
              <template v-if="participantFieldHelp(field.help)">
                <small class="text-secondary prototype-subtitle d-block mt-1">{{
                  participantFieldHelp(field.help)
                }}</small>
              </template>
              <div v-if="planFileItems.length" class="supporting-file-list mt-2" aria-live="polite">
                <div
                  v-for="item in planFileItems"
                  :key="String(item.id)"
                  class="supporting-file-item"
                >
                  <div class="supporting-file-meta">
                    <span class="supporting-file-name" :title="fileItemDisplayName(item)">{{
                      fileItemDisplayName(item)
                    }}</span>
                    <span class="supporting-file-size">{{
                      formatFileSize(item.file ? item.file.size : item.size || 0)
                    }}</span>
                  </div>
                  <div class="supporting-file-actions">
                    <a
                      class="btn btn-sm btn-outline-primary"
                      :href="item.previewUrl || item.url || '#'"
                      target="_blank"
                      rel="noopener"
                      >预览</a
                    >
                    <button
                      type="button"
                      class="btn btn-sm btn-outline-secondary"
                      :disabled="formDisabled"
                      @click="removeFile(item.id, 'plan')"
                    >
                      删除
                    </button>
                  </div>
                </div>
              </div>
              <small class="text-secondary prototype-subtitle d-block">{{ planFileSavedInfo }}</small>
              <div v-if="wasValidated && planFileFeedback" class="invalid-feedback d-block">{{ planFileFeedback }}</div>
            </div>

            <div v-else-if="field.type === 'file' && field.key === 'supporting'" class="col-12">
              <label class="form-label">{{ field.label }}</label>
              <input
                ref="supportingFileInput"
                type="file"
                class="form-control editable"
                :class="{ 'is-invalid': wasValidated && !!supportingFilesFeedback }"
                accept=".pdf,.ppt,.pptx,.doc,.docx,.wps,.rar,.zip"
                multiple
                :disabled="formDisabled"
                @change="addFiles(($event.target as HTMLInputElement).files, 'supporting')"
              />
              <template v-if="participantFieldHelp(field.help)">
                <small class="text-secondary prototype-subtitle d-block mt-1">{{
                  participantFieldHelp(field.help)
                }}</small>
              </template>
              <div v-if="supportingFileItems.length" class="supporting-file-list mt-2" aria-live="polite">
                <div
                  v-for="item in supportingFileItems"
                  :key="String(item.id)"
                  class="supporting-file-item"
                >
                  <div class="supporting-file-meta">
                    <span class="supporting-file-name" :title="fileItemDisplayName(item)">{{
                      fileItemDisplayName(item)
                    }}</span>
                    <span class="supporting-file-size">{{
                      formatFileSize(item.file ? item.file.size : item.size || 0)
                    }}</span>
                  </div>
                  <div class="supporting-file-actions">
                    <a
                      class="btn btn-sm btn-outline-primary"
                      :href="item.previewUrl || item.url || '#'"
                      target="_blank"
                      rel="noopener"
                      >预览</a
                    >
                    <button
                      type="button"
                      class="btn btn-sm btn-outline-secondary"
                      :disabled="formDisabled"
                      @click="removeFile(item.id, 'supporting')"
                    >
                      删除
                    </button>
                  </div>
                </div>
              </div>
              <small class="text-secondary prototype-subtitle d-block">{{ supportingFilesSavedInfo }}</small>
              <div v-if="wasValidated && supportingFilesFeedback" class="invalid-feedback d-block">{{
                supportingFilesFeedback
              }}</div>
            </div>

            <div v-else-if="field.type === 'select' && !isCitySelectField(field)" :class="fieldColClass(field)">
              <label class="form-label"
                >{{ field.label }} <span v-if="effectiveRequired(field)" class="text-danger">*</span></label
              >
              <select
                v-model="formModel[field.key]"
                class="form-select editable"
                :required="effectiveRequired(field)"
                :disabled="formDisabled"
              >
                <option value="">请选择</option>
                <option v-for="opt in optionsForField(field)" :key="`${field.key}-${opt.value}`" :value="opt.value">
                  {{ opt.label }}
                </option>
              </select>
              <div class="invalid-feedback">请选择{{ field.label }}</div>
              <small
                v-if="participantFieldHelp(field.help)"
                class="text-secondary prototype-subtitle d-block"
                >{{ participantFieldHelp(field.help) }}</small
              >
            </div>

            <div v-else-if="isCitySelectField(field)" :class="fieldColClass(field)">
              <label class="form-label"
                >{{ field.label }} <span v-if="effectiveRequired(field)" class="text-danger">*</span></label
              >
              <select
                v-model="formModel.location_city"
                class="form-select editable"
                :required="effectiveRequired(field)"
                :disabled="formDisabled || !isChina"
              >
                <option value="">请选择</option>
                <option v-for="opt in optionsForField(field)" :key="opt.value" :value="opt.value">
                  {{ opt.label }}
                </option>
              </select>
              <div class="invalid-feedback">请选择{{ field.label }}</div>
              <small
                v-if="participantFieldHelp(field.help)"
                class="text-secondary prototype-subtitle d-block"
                >{{ participantFieldHelp(field.help) }}</small
              >
            </div>

            <div
              v-else-if="field.type === 'checkbox' && field.key === 'commitment_accepted'"
              :class="fieldColClass(field)"
            >
              <label class="form-label"
                >{{ field.label }} <span v-if="effectiveRequired(field)" class="text-danger">*</span></label
              >
              <p class="small text-secondary mb-2">
                请点击按钮打开承诺书全文，阅读后在文末手写签名并点击「确认签署」。
              </p>
              <div class="d-flex flex-wrap align-items-center gap-2 mb-1">
                <button
                  type="button"
                  class="btn btn-outline-primary btn-sm editable"
                  :disabled="formDisabled"
                  @click="openPromiseSignModal"
                >
                  {{ commitmentSigned ? '重新查看承诺书' : '查看并签署参赛承诺书' }}
                </button>
                <span v-if="commitmentSigned" class="badge rounded-pill px-3 py-2 promise-signed-badge"
                  >已完成签署</span
                >
              </div>
              <small
                v-if="participantFieldHelp(field.help)"
                class="text-secondary prototype-subtitle d-block mt-1"
                >{{ participantFieldHelp(field.help) }}</small
              >
            </div>

            <div v-else-if="field.type === 'checkbox'" :class="fieldColClass(field)">
              <div class="form-check">
                <input
                  :id="'signup-field-' + field.key"
                  v-model="formModel[field.key]"
                  type="checkbox"
                  class="form-check-input editable"
                  true-value="1"
                  false-value=""
                  :required="effectiveRequired(field)"
                  :disabled="formDisabled"
                />
                <label class="form-check-label" :for="'signup-field-' + field.key">
                  {{ field.label }}<span v-if="effectiveRequired(field)" class="text-danger"> *</span>
                </label>
                <div class="invalid-feedback">请勾选此项以继续</div>
              </div>
              <small
                v-if="participantFieldHelp(field.help)"
                class="text-secondary prototype-subtitle d-block mt-1"
                >{{ participantFieldHelp(field.help) }}</small
              >
            </div>

            <div v-else-if="field.type === 'textarea'" :class="fieldColClass(field)">
              <label class="form-label"
                >{{ field.label }} <span v-if="effectiveRequired(field)" class="text-danger">*</span></label
              >
              <textarea
                v-model="formModel[field.key]"
                class="form-control editable"
                rows="4"
                placeholder=""
                :required="effectiveRequired(field)"
                :disabled="formDisabled"
              />
              <div
                v-if="textareaFieldPlaceholder(field) || field.key === 'intro'"
                class="d-flex flex-wrap align-items-baseline gap-2 mt-1 w-100"
              >
                <small v-if="textareaFieldPlaceholder(field)" class="apply-textarea-placeholder-hint mb-0">{{
                  textareaFieldPlaceholder(field)
                }}</small>
                <small
                  v-if="field.key === 'intro'"
                  class="text-secondary prototype-subtitle intro-char-count mb-0 ms-auto"
                  >{{ introCount }} 字</small
                >
              </div>
              <small
                v-if="participantFieldHelp(field.help)"
                class="text-secondary prototype-subtitle d-block"
                :class="{ 'mt-1': field.key === 'intro' || textareaFieldPlaceholder(field) }"
                >{{ participantFieldHelp(field.help) }}</small
              >
            </div>

            <div v-else :class="fieldColClass(field)">
              <label class="form-label"
                >{{ field.label }} <span v-if="effectiveRequired(field)" class="text-danger">*</span></label
              >
              <input
                v-model="formModel[field.key]"
                :type="
                  field.type === 'email'
                    ? 'email'
                    : field.key === 'contact_mobile'
                      ? 'tel'
                      : 'text'
                "
                class="form-control editable"
                :class="{ 'company-name-input': field.key === 'company_name' }"
                :required="effectiveRequired(field)"
                :disabled="formDisabled"
                :placeholder="(field.placeholder ?? '').trim()"
                :pattern="field.key === 'contact_mobile' ? MOBILE_PATTERN : undefined"
                :autocomplete="field.key === 'company_name' ? 'organization' : undefined"
              />
              <div class="invalid-feedback">{{ nativeInputInvalidFeedback(field) }}</div>
              <small
                v-if="participantFieldHelp(field.help)"
                class="text-secondary prototype-subtitle d-block"
                >{{ participantFieldHelp(field.help) }}</small
              >
            </div>
          </template>
          <div class="col-12 apply-form-actions mt-3 pt-3 border-top d-flex gap-2 flex-wrap">
            <button type="button" class="btn btn-outline-primary" :disabled="formDisabled" @click="onSaveClick">保存</button>
            <button type="button" class="btn btn-success" :disabled="formDisabled" @click="onSubmitClick">提交报名</button>
          </div>
        </form>
      </div>
    </div>
    </div>

    <div
      v-if="!loadError && schemaFields.some((f) => f.key === 'commitment_accepted')"
      id="promiseSignModal"
      ref="promiseSignModalRef"
      class="modal fade"
      tabindex="-1"
      aria-hidden="true"
      data-bs-backdrop="static"
    >
      <div class="modal-dialog modal-dialog-centered promise-sign-modal">
        <div class="modal-content promise-sign-sheet">
          <div class="modal-body pt-2">
            <div class="promise-doc-paper">
              <h1 class="promise-doc-heading text-center">{{ promiseModalHeading }}</h1>
              <div class="promise-doc-scroll">
                <div class="promise-doc-body promise-doc-body--rich" v-html="pledgeBodyDisplayHtml"></div>
              </div>
              <div class="promise-doc-signblock">
                <div class="promise-doc-signrow">
                  <span class="promise-doc-signlabel">参赛人签名：</span>
                  <div class="promise-sig-wrap">
                    <div class="promise-sig-box">
                      <canvas
                        ref="promiseCanvasRef"
                        class="promise-sig-canvas"
                        width="520"
                        height="140"
                        aria-label="手写签名区域"
                      />
                      <span class="promise-sig-hint" :class="{ 'd-none': !promiseSigHintVisible }"
                        >请在框内手写签名</span
                      >
                    </div>
                    <div class="promise-sig-tools">
                      <button type="button" class="btn btn-sm btn-outline-secondary" @click="clearPromiseSignatureCanvas">
                        清除签名
                      </button>
                    </div>
                  </div>
                </div>
                <div class="promise-doc-daterow">
                  <span class="promise-doc-dateline"
                    >日期：<strong>{{ promiseModalDateText }}</strong></span
                  >
                </div>
              </div>
              <div class="promise-sign-actions">
                <button type="button" class="btn promise-close-btn" data-bs-dismiss="modal">关闭</button>
                <button type="button" class="btn btn-primary" @click="confirmPromiseSignature">确认签署</button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <div ref="noticeModalEl" id="applyNoticeModal" class="modal fade" tabindex="-1" aria-hidden="true">
      <div class="modal-dialog modal-dialog-centered notice-modal-dialog">
        <div
          class="modal-content notice-modal-content"
          :class="noticeIsSuccess ? 'notice-success' : 'notice-warning'"
        >
          <div class="modal-header border-0 pb-0">
            <button type="button" class="btn-close ms-auto" data-bs-dismiss="modal" aria-label="关闭" />
          </div>
          <div class="modal-body notice-modal-body">
            <div class="notice-icon" aria-hidden="true">
              <svg
                v-if="noticeIsSuccess"
                viewBox="0 0 24 24"
                width="36"
                height="36"
                fill="none"
                aria-hidden="true"
              >
                <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.8" />
                <path
                  d="M7 12.2l3.1 3.1L17.2 8.8"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
              </svg>
              <svg v-else viewBox="0 0 24 24" width="36" height="36" fill="none" aria-hidden="true">
                <path
                  d="M12 3.6L2.9 19.2a1 1 0 0 0 .86 1.5h16.48a1 1 0 0 0 .86-1.5L12 3.6z"
                  stroke="currentColor"
                  stroke-width="1.8"
                />
                <path d="M12 9v5" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
                <circle cx="12" cy="16.8" r="1.1" fill="currentColor" />
              </svg>
            </div>
            <h5 class="notice-title">{{ noticeTitle }}</h5>
            <p v-if="noticeBody.trim()" class="notice-text text-break" style="white-space: pre-wrap">
              {{ noticeBody }}
            </p>
          </div>
          <div class="modal-footer notice-modal-footer border-0">
            <button type="button" class="btn btn-light notice-cancel-btn" data-bs-dismiss="modal">取消</button>
            <button type="button" class="btn btn-primary notice-ok-btn" data-bs-dismiss="modal">确定</button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.promise-doc-body--rich {
  font-size: 0.95rem;
  line-height: 1.65;
  color: var(--bs-body-color, #212529);
  text-align: justify;
}

.promise-doc-body--rich :deep(p) {
  margin-bottom: 0.75rem;
}

.promise-doc-body--rich :deep(p:last-child) {
  margin-bottom: 0;
}

.promise-doc-body--rich :deep(ul),
.promise-doc-body--rich :deep(ol) {
  padding-left: 1.25rem;
  margin-bottom: 0.75rem;
}

.promise-doc-body--rich :deep(h1),
.promise-doc-body--rich :deep(h2),
.promise-doc-body--rich :deep(h3) {
  font-size: 1rem;
  font-weight: 600;
  margin: 0.75rem 0 0.5rem;
}
</style>
