Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 

522 rader
22 KiB

  1. <template>
  2. <Teleport to="body">
  3. <Transition name="wd-modal">
  4. <div v-if="modelValue" class="fixed inset-0 z-[120] flex items-center justify-center overflow-hidden p-3 sm:p-4">
  5. <div class="absolute inset-0 bg-[var(--c-0f172a)]/55 backdrop-blur-sm" @click="close" />
  6. <div class="relative flex max-h-[calc(100dvh-1.5rem)] w-full max-w-3xl flex-col overflow-hidden rounded-2xl bg-[var(--c-ffffff)] shadow-2xl sm:max-h-[88dvh]">
  7. <!-- 头部 -->
  8. <div class="flex shrink-0 items-center justify-between gap-3 border-b border-[var(--c-eef2f7)] px-5 py-4">
  9. <div class="min-w-0">
  10. <h3 class="truncate text-base font-extrabold text-[var(--c-172033)]">{{ headerTitle }}</h3>
  11. <div v-if="addTimeText" class="mt-0.5 text-xs text-[var(--c-8a97a8)]">{{ addTimeText }}</div>
  12. </div>
  13. <button
  14. class="flex size-8 shrink-0 items-center justify-center rounded-full text-[var(--c-a0aabb)] transition-colors hover:bg-[var(--c-f1f5fa)] hover:text-[var(--c-172033)]"
  15. @click="close"
  16. >
  17. <UIcon name="i-lucide-x" class="size-5" />
  18. </button>
  19. </div>
  20. <!-- 内容 -->
  21. <div class="detail-scroll min-h-[220px] flex-1 overflow-y-auto overscroll-contain px-5 py-5">
  22. <!-- 加载中 -->
  23. <div v-if="loading" class="flex flex-col items-center justify-center gap-3 py-16">
  24. <span class="size-8 animate-spin rounded-full border-[3px] border-[var(--c-0b8cff)]/20 border-t-[var(--c-0b8cff)]" />
  25. <span class="text-sm text-[var(--c-8a95a6)]">加载详情中...</span>
  26. </div>
  27. <!-- 出错 -->
  28. <div v-else-if="errorMsg" class="py-16 text-center">
  29. <UIcon name="i-lucide-circle-alert" class="mx-auto mb-3 size-10 text-[var(--c-f04438)]" />
  30. <div class="text-sm text-[var(--c-f04438)]">{{ errorMsg }}</div>
  31. <button
  32. class="mt-4 rounded-full bg-[var(--c-eef7ff)] px-5 py-2 text-xs font-bold text-[var(--c-0b8cff)]"
  33. @click="load"
  34. >
  35. 重试
  36. </button>
  37. </div>
  38. <template v-else>
  39. <!-- ============ AI 取名详情 ============ -->
  40. <template v-if="isName">
  41. <div v-if="detail.question_content" class="mb-4 rounded-2xl bg-[var(--c-f7f9fd)] p-4">
  42. <div class="mb-1 text-xs font-bold text-[var(--c-8a97a8)]">取名条件</div>
  43. <div class="whitespace-pre-wrap text-sm leading-relaxed text-[var(--c-4b5b70)]">{{ detail.question_content }}</div>
  44. </div>
  45. <div v-if="nameResult.summary" class="mb-4 rounded-2xl bg-[var(--c-eef7ff)] p-4 text-sm leading-relaxed text-[var(--c-3a5878)]">
  46. {{ nameResult.summary }}
  47. </div>
  48. <div v-if="nameResult.names.length" class="space-y-3">
  49. <div
  50. v-for="(item, idx) in nameResult.names"
  51. :key="idx"
  52. class="rounded-2xl border border-[var(--c-eef2f7)] p-4"
  53. >
  54. <div class="mb-2 flex items-center justify-between">
  55. <span class="text-2xl font-extrabold text-[var(--c-0b8cff)]">{{ item.name }}</span>
  56. <span v-if="item.score" class="text-base font-extrabold text-[var(--c-ff8a00)]">{{ item.score }}分</span>
  57. </div>
  58. <div v-if="item.pinyin" class="mb-3 text-xs text-[var(--c-8a97a8)]">{{ item.pinyin }}</div>
  59. <div class="space-y-2">
  60. <div v-for="f in NAME_FIELDS" :key="f.key">
  61. <div v-if="item[f.key]" class="flex items-start gap-2.5">
  62. <span class="inline-flex h-[24px] shrink-0 items-center rounded-full bg-[var(--c-eef6ff)] px-2 text-[11px] font-bold text-[var(--c-0b8cff)]">
  63. {{ f.label }}
  64. </span>
  65. <span class="pt-0.5 text-sm leading-relaxed text-[var(--c-172033)]">{{ item[f.key] }}</span>
  66. </div>
  67. </div>
  68. </div>
  69. </div>
  70. </div>
  71. <!-- 生成中 / 无结果 -->
  72. <div v-else class="py-14 text-center">
  73. <template v-if="isPending">
  74. <span class="mx-auto mb-3 block size-8 animate-spin rounded-full border-[3px] border-[var(--c-0b8cff)]/20 border-t-[var(--c-0b8cff)]" />
  75. <div class="text-sm font-bold text-[var(--c-172033)]">名字整理中</div>
  76. <div class="mt-1 text-xs text-[var(--c-8a95a6)]">稍后可再次打开查看</div>
  77. </template>
  78. <template v-else>
  79. <UIcon name="i-lucide-search-x" class="mx-auto mb-3 size-10 text-[var(--c-a0aabb)]" />
  80. <div class="text-sm text-[var(--c-8a95a6)]">暂无名字结果</div>
  81. </template>
  82. </div>
  83. </template>
  84. <!-- ============ 图片类详情(动漫头像 / 证件照等) ============ -->
  85. <template v-else>
  86. <div
  87. v-if="imageUrl"
  88. class="grid min-h-0 gap-3 sm:gap-4"
  89. :class="isPhoto ? 'mx-auto w-full max-w-md grid-cols-1' : 'grid-cols-2'"
  90. >
  91. <!-- 原图 -->
  92. <div v-if="originalUrl && !isPhoto" class="min-w-0">
  93. <div class="mb-2 text-xs font-bold text-[var(--c-8a97a8)]">原图</div>
  94. <div
  95. class="flex max-h-[calc(100dvh-13rem)] items-center justify-center overflow-hidden rounded-2xl bg-[var(--c-f7f9fd)] sm:max-h-[calc(88dvh-13rem)]"
  96. :class="{ 'aspect-square': !isPhoto }"
  97. :style="imageAspectStyle"
  98. >
  99. <img :src="originalUrl" class="size-full" :class="isPhoto ? 'object-contain' : 'object-cover'" alt="原图">
  100. </div>
  101. </div>
  102. <!-- 结果图 -->
  103. <div
  104. class="min-w-0"
  105. :class="{ 'col-span-2 mx-auto w-full max-w-md': !originalUrl && !isPhoto }"
  106. >
  107. <div class="mb-2 text-xs font-bold text-[var(--c-8a97a8)]">{{ resultLabel }}</div>
  108. <div
  109. class="flex max-h-[calc(100dvh-13rem)] items-center justify-center overflow-hidden rounded-2xl border-2 border-[var(--c-0b8cff)] bg-[var(--c-f7f9fd)] sm:max-h-[calc(88dvh-13rem)]"
  110. :class="{ 'aspect-square': !isPhoto }"
  111. :style="imageAspectStyle"
  112. >
  113. <img :src="imageUrl" class="size-full" :class="isPhoto ? 'object-contain' : 'object-cover'" :alt="resultLabel">
  114. </div>
  115. </div>
  116. </div>
  117. <!-- 形象照一单多张:缩略图切换 -->
  118. <div v-if="imageUrl && portraitUrls.length > 1" class="mt-4 flex flex-wrap gap-2.5">
  119. <button
  120. v-for="(url, index) in portraitUrls"
  121. :key="url"
  122. type="button"
  123. class="size-14 cursor-pointer overflow-hidden rounded-xl border-2 transition-colors"
  124. :class="activePortraitIndex === index ? 'border-[var(--c-0b8cff)]' : 'border-transparent hover:border-[var(--c-e5edf7)]'"
  125. :aria-label="`查看第 ${index + 1} 张`"
  126. :aria-pressed="activePortraitIndex === index"
  127. @click="activePortraitIndex = index"
  128. >
  129. <img :src="url" class="size-full object-cover" :alt="`第 ${index + 1} 张`">
  130. </button>
  131. </div>
  132. <div v-if="!imageUrl" class="py-14 text-center">
  133. <template v-if="isPending">
  134. <span class="mx-auto mb-3 block size-8 animate-spin rounded-full border-[3px] border-[var(--c-0b8cff)]/20 border-t-[var(--c-0b8cff)]" />
  135. <div class="text-sm font-bold text-[var(--c-172033)]">正在创作</div>
  136. <div class="mt-1 text-xs text-[var(--c-8a95a6)]">完成后可在此查看结果</div>
  137. </template>
  138. <template v-else-if="isFailed">
  139. <UIcon name="i-lucide-circle-alert" class="mx-auto mb-3 size-10 text-[var(--c-f04438)]" />
  140. <div class="text-sm text-[var(--c-f04438)]">{{ detail.err_msg || '生成失败' }}</div>
  141. </template>
  142. <template v-else>
  143. <UIcon name="i-lucide-image-off" class="mx-auto mb-3 size-10 text-[var(--c-a0aabb)]" />
  144. <div class="text-sm text-[var(--c-8a95a6)]">暂无图片</div>
  145. </template>
  146. </div>
  147. </template>
  148. </template>
  149. </div>
  150. <!-- 底部操作 -->
  151. <div v-if="!loading && !errorMsg" class="flex shrink-0 flex-wrap justify-end gap-3 border-t border-[var(--c-eef2f7)] px-5 py-4">
  152. <button
  153. v-if="canCompare"
  154. type="button"
  155. class="inline-flex items-center gap-1.5 rounded-full border border-[var(--c-0b8cff)] px-5 py-2.5 text-sm font-bold text-[var(--c-0b8cff)] transition-colors hover:bg-[var(--c-eef7ff)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--c-0b8cff)] focus-visible:ring-offset-2"
  156. @click="openCompare"
  157. >
  158. <UIcon name="i-lucide-split-square-horizontal" class="size-4" />
  159. 对比效果
  160. </button>
  161. <button
  162. class="rounded-full border border-[var(--c-e5edf7)] px-5 py-2.5 text-sm font-bold text-[var(--c-5b6b80)] transition-colors hover:bg-[var(--c-f7f9fd)]"
  163. @click="close"
  164. >
  165. 关闭
  166. </button>
  167. <button
  168. v-if="isName && nameResult.names.length"
  169. class="rounded-full bg-[var(--c-0b8cff)] px-5 py-2.5 text-sm font-bold text-white transition-colors hover:bg-[var(--c-0a7ce0)]"
  170. @click="copyNames"
  171. >
  172. 复制结果
  173. </button>
  174. <a
  175. v-if="!isName && imageUrl"
  176. :href="imageUrl"
  177. target="_blank"
  178. rel="noopener"
  179. download
  180. class="rounded-full bg-[var(--c-0b8cff)] px-5 py-2.5 text-sm font-bold text-white transition-colors hover:bg-[var(--c-0a7ce0)]"
  181. >
  182. {{ isPhoto ? '下载证件照' : '下载图片' }}
  183. </a>
  184. </div>
  185. </div>
  186. </div>
  187. </Transition>
  188. <Transition name="wd-modal">
  189. <div v-if="modelValue && compareOpen" class="fixed inset-0 z-[140] flex items-center justify-center overflow-hidden p-3 sm:p-4">
  190. <div class="absolute inset-0 bg-[var(--c-0f172a)]/70 backdrop-blur-sm" @click="closeCompare" />
  191. <div class="relative flex max-h-[calc(100dvh-1.5rem)] w-full max-w-2xl flex-col overflow-hidden rounded-2xl bg-[var(--c-ffffff)] shadow-2xl sm:max-h-[90dvh]">
  192. <div class="flex shrink-0 items-center justify-between border-b border-[var(--c-eef2f7)] px-5 py-4">
  193. <div>
  194. <h3 class="text-base font-extrabold text-[var(--c-172033)]">{{ compareTitle }}</h3>
  195. <p class="mt-0.5 text-xs text-[var(--c-8a97a8)]">左右拖动分割线查看生成前后变化</p>
  196. </div>
  197. <button
  198. type="button"
  199. aria-label="关闭对比"
  200. class="flex size-8 shrink-0 items-center justify-center rounded-full text-[var(--c-8a97a8)] transition-colors hover:bg-[var(--c-f1f5fa)] hover:text-[var(--c-172033)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--c-0b8cff)]"
  201. @click="closeCompare"
  202. >
  203. <UIcon name="i-lucide-x" class="size-5" />
  204. </button>
  205. </div>
  206. <div class="min-h-0 flex-1 p-4 sm:p-5">
  207. <div
  208. ref="compareSurface"
  209. class="relative mx-auto max-h-[calc(100dvh-8.5rem)] w-full max-w-[min(100%,640px)] touch-none select-none overflow-hidden rounded-2xl bg-[var(--c-e8eef6)] cursor-ew-resize sm:max-h-[calc(90dvh-8.5rem)]"
  210. :class="{ 'aspect-square': !isPhoto }"
  211. :style="imageAspectStyle"
  212. @pointerdown="startCompareDrag"
  213. >
  214. <img :src="imageUrl" class="pointer-events-none absolute inset-0 size-full" :class="isPhoto ? 'object-contain' : 'object-cover'" :alt="resultLabel">
  215. <img
  216. :src="originalUrl"
  217. class="pointer-events-none absolute inset-0 size-full"
  218. :class="isPhoto ? 'object-contain' : 'object-cover'"
  219. :style="{ clipPath: `inset(0 ${100 - comparePercent}% 0 0)` }"
  220. alt="原图"
  221. >
  222. <div
  223. class="pointer-events-none absolute bottom-0 top-0 w-1 -ml-px bg-[var(--c-ffffff)] shadow-[0_0_14px_var(--sh-15-23-42-280)]"
  224. :style="{ left: `${comparePercent}%` }"
  225. >
  226. <div class="absolute left-1/2 top-1/2 flex h-14 w-9 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-[var(--c-ffffff)] text-lg font-extrabold text-[var(--c-0b8cff)] shadow-[0_8px_22px_var(--sh-15-23-42-240)]">
  227. <span>‹</span><span>›</span>
  228. </div>
  229. </div>
  230. <span class="pointer-events-none absolute left-3 top-3 rounded-full bg-[var(--c-0f172a)]/55 px-2.5 py-1 text-[11px] font-bold text-white">原图</span>
  231. <span class="pointer-events-none absolute right-3 top-3 rounded-full bg-[var(--c-0f172a)]/55 px-2.5 py-1 text-[11px] font-bold text-white">{{ resultLabel }}</span>
  232. </div>
  233. </div>
  234. </div>
  235. </div>
  236. </Transition>
  237. </Teleport>
  238. </template>
  239. <script setup>
  240. import dayjs from 'dayjs'
  241. import { parseNameResult, buildNameCopyText, NAME_FIELDS } from '~/utils/nameParse'
  242. const props = defineProps({
  243. modelValue: { type: Boolean, default: false },
  244. /** 作品项:需含 work_type 与 chat_open_id(或 id) */
  245. work: { type: Object, default: () => ({}) },
  246. })
  247. const emit = defineEmits(['update:modelValue'])
  248. const { post } = useApi()
  249. const toast = useToast()
  250. const loading = ref(false)
  251. const errorMsg = ref('')
  252. const detail = ref({})
  253. const compareOpen = ref(false)
  254. const comparePercent = ref(50)
  255. const compareSurface = ref(null)
  256. let compareDragging = false
  257. const isName = computed(() => props.work?.work_type === 'ainame')
  258. const isPhoto = computed(() => props.work?.work_type === 'photo')
  259. const isOldPhoto = computed(() => props.work?.work_type === 'oldphoto')
  260. const isPortrait = computed(() => props.work?.work_type === 'portrait')
  261. /**
  262. * 状态归一。
  263. * 老照片与形象照任务用的是自己的一套状态(10排队 20处理中 30成功 -10失败 -20审核不通过),
  264. * 这里换算成作品列表通用的 0生成中 / 1完成 / -1失败,避免详情加载完还一直显示「生成中」。
  265. */
  266. const statusValue = computed(() => {
  267. if ((isOldPhoto.value || isPortrait.value) && detail.value.status !== undefined) {
  268. const status = Number(detail.value.status)
  269. if (status === 30) return 1
  270. return status < 0 ? -1 : 0
  271. }
  272. return Number(detail.value.is_suc ?? props.work?.status ?? 0)
  273. })
  274. const isPending = computed(() => statusValue.value === 0)
  275. const isFailed = computed(() => statusValue.value < 0)
  276. const resultLabel = computed(() => {
  277. if (isPhoto.value) return '证件照'
  278. if (isOldPhoto.value) return '修复结果'
  279. return '生成结果'
  280. })
  281. const compareTitle = computed(() => {
  282. if (isPhoto.value) return '证件照效果对比'
  283. if (isOldPhoto.value) return '老照片修复对比'
  284. if (isPortrait.value) return '形象照效果对比'
  285. return '动漫头像效果对比'
  286. })
  287. /** 形象照一单可能有多张图,详情里支持切换,切换后对比与下载都跟着走 */
  288. const portraitUrls = computed(() => {
  289. if (!isPortrait.value) return []
  290. const urls = detail.value.result_urls || props.work?.pic_list || []
  291. return Array.isArray(urls) ? urls.filter(Boolean) : []
  292. })
  293. const activePortraitIndex = ref(0)
  294. const activePortraitUrl = computed(
  295. () => portraitUrls.value[activePortraitIndex.value] || portraitUrls.value[0] || '',
  296. )
  297. const imageAspectStyle = computed(() => {
  298. if (!isPhoto.value) return {}
  299. const width = Number(detail.value.width || props.work?.width || 0)
  300. const height = Number(detail.value.height || props.work?.height || 0)
  301. return width > 0 && height > 0
  302. ? { aspectRatio: `${width} / ${height}` }
  303. : { aspectRatio: '5 / 7' }
  304. })
  305. const headerTitle = computed(() => {
  306. const map = {
  307. ainame: 'AI 取名详情',
  308. anime: '动漫头像详情',
  309. photo: '证件照详情',
  310. oldphoto: '老照片详情',
  311. portrait: '形象照详情',
  312. image: '作品详情',
  313. }
  314. return map[props.work?.work_type] || '作品详情'
  315. })
  316. const addTimeText = computed(() => {
  317. const t = detail.value.add_time || props.work?.add_time
  318. return t ? dayjs(Number(t) * 1000).format('YYYY-MM-DD HH:mm') : ''
  319. })
  320. const nameResult = computed(() => {
  321. if (!isName.value) return { summary: '', names: [] }
  322. // 优先用后端结构化的 detail,其次解析 content 文本
  323. const d = detail.value.detail
  324. if (d && Array.isArray(d.names) && d.names.length) {
  325. return { summary: d.summary || '', names: d.names }
  326. }
  327. return parseNameResult(detail.value.content || '')
  328. })
  329. const imageUrl = computed(() =>
  330. (isPortrait.value ? activePortraitUrl.value : '')
  331. || detail.value.answer
  332. || detail.value.result_url
  333. || detail.value.url
  334. || (!isName.value ? detail.value.content : '')
  335. || props.work?.cover
  336. || (!isName.value ? props.work?.content : '')
  337. || '')
  338. const originalUrl = computed(() => {
  339. const imgs = detail.value.original
  340. || detail.value.original_url
  341. || detail.value.original_imgs
  342. || detail.value.imgs
  343. || detail.value.img
  344. || props.work?.original
  345. || props.work?.original_url
  346. || props.work?.imgs
  347. || props.work?.img
  348. || ''
  349. if (Array.isArray(imgs)) return imgs[0] || ''
  350. return typeof imgs === 'string' && imgs ? imgs.split(',')[0] : ''
  351. })
  352. const canCompare = computed(() =>
  353. !isName.value
  354. && !isPhoto.value
  355. && Boolean(originalUrl.value && imageUrl.value)
  356. && !isPending.value
  357. && !isFailed.value)
  358. async function load() {
  359. const id = props.work?.photo_open_id || props.work?.chat_open_id || props.work?.id
  360. if (!id) {
  361. errorMsg.value = '缺少作品标识'
  362. return
  363. }
  364. loading.value = true
  365. errorMsg.value = ''
  366. try {
  367. let path = isName.value ? '/ainame/getdetail' : '/anime/getdetail'
  368. let body = { chat_open_id: id }
  369. if (isPhoto.value) {
  370. path = '/idphoto/getdetail'
  371. body = { photo_open_id: id }
  372. }
  373. if (isOldPhoto.value) {
  374. path = '/oldphoto/getdetail'
  375. body = { photo_open_id: id }
  376. }
  377. if (isPortrait.value) {
  378. path = '/portrait/getdetail'
  379. body = { photo_open_id: id }
  380. }
  381. const res = await post(path, body)
  382. detail.value = res.data || {}
  383. // 还在处理中就继续拉:老照片是异步任务,弹窗里也要能等到结果
  384. schedulePendingReload()
  385. } catch (e) {
  386. errorMsg.value = e.message || '详情加载失败'
  387. } finally {
  388. loading.value = false
  389. }
  390. }
  391. let pendingTimer = null
  392. function schedulePendingReload() {
  393. clearPendingReload()
  394. if (!props.modelValue || !isPending.value) return
  395. pendingTimer = setTimeout(() => load(), 3000)
  396. }
  397. function clearPendingReload() {
  398. if (pendingTimer) clearTimeout(pendingTimer)
  399. pendingTimer = null
  400. }
  401. onUnmounted(() => clearPendingReload())
  402. function close() {
  403. closeCompare()
  404. clearPendingReload()
  405. emit('update:modelValue', false)
  406. }
  407. function openCompare() {
  408. if (!canCompare.value) {
  409. toast.add({ title: '当前作品缺少原图或结果图', color: 'error' })
  410. return
  411. }
  412. comparePercent.value = 50
  413. compareOpen.value = true
  414. }
  415. function closeCompare() {
  416. compareOpen.value = false
  417. stopCompareDrag()
  418. }
  419. function startCompareDrag(event) {
  420. if (!compareSurface.value) return
  421. compareDragging = true
  422. updateComparePercent(event)
  423. window.addEventListener('pointermove', moveCompareDrag)
  424. window.addEventListener('pointerup', stopCompareDrag)
  425. window.addEventListener('pointercancel', stopCompareDrag)
  426. }
  427. function moveCompareDrag(event) {
  428. if (!compareDragging) return
  429. if (event.cancelable) event.preventDefault()
  430. updateComparePercent(event)
  431. }
  432. function stopCompareDrag() {
  433. compareDragging = false
  434. window.removeEventListener('pointermove', moveCompareDrag)
  435. window.removeEventListener('pointerup', stopCompareDrag)
  436. window.removeEventListener('pointercancel', stopCompareDrag)
  437. }
  438. function updateComparePercent(event) {
  439. if (!compareSurface.value) return
  440. const rect = compareSurface.value.getBoundingClientRect()
  441. const percent = ((Number(event.clientX) - rect.left) / rect.width) * 100
  442. comparePercent.value = Math.max(2, Math.min(98, Number(percent.toFixed(1))))
  443. }
  444. function copyNames() {
  445. const text = buildNameCopyText(nameResult.value)
  446. if (!text) {
  447. toast.add({ title: '暂无可复制内容', color: 'error' })
  448. return
  449. }
  450. navigator.clipboard.writeText(text)
  451. .then(() => toast.add({ title: '复制成功', color: 'success' }))
  452. .catch(() => toast.add({ title: '复制失败', color: 'error' }))
  453. }
  454. // 打开时加载详情
  455. watch(() => props.modelValue, (open) => {
  456. if (open) {
  457. detail.value = {}
  458. compareOpen.value = false
  459. activePortraitIndex.value = 0
  460. load()
  461. } else {
  462. closeCompare()
  463. clearPendingReload()
  464. }
  465. })
  466. onBeforeUnmount(stopCompareDrag)
  467. </script>
  468. <style scoped>
  469. .wd-modal-enter-active,
  470. .wd-modal-leave-active { transition: opacity 0.2s ease; }
  471. .wd-modal-enter-from,
  472. .wd-modal-leave-to { opacity: 0; }
  473. .detail-scroll {
  474. scrollbar-width: none;
  475. -ms-overflow-style: none;
  476. }
  477. .detail-scroll::-webkit-scrollbar { display: none; }
  478. @media (prefers-reduced-motion: reduce) {
  479. .wd-modal-enter-active,
  480. .wd-modal-leave-active { transition: none; }
  481. }
  482. </style>