DemandPathPanel.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820
  1. <script setup lang="ts">
  2. import { computed, ref, watch } from 'vue'
  3. import { fetchDemandGradeVideos } from '../api/demand'
  4. import type { DemandExpansionPoint, DemandGradeItem, DemandVideoItem } from '../types/demand'
  5. import { gradeRank, parseStrategies } from '../types/demand'
  6. const props = defineProps<{
  7. open: boolean
  8. categoryName: string
  9. items: DemandGradeItem[]
  10. /** When set, auto-select and load videos for this demand_grade id. */
  11. highlightDemandId?: number | null
  12. /** When set, prepend a "当前节点" column. */
  13. nodeName?: string | null
  14. nodePath?: string | null
  15. /** Hide the small inline × close button (when parent provides its own). */
  16. hideInlineClose?: boolean
  17. }>()
  18. const emit = defineEmits<{
  19. close: []
  20. }>()
  21. const selectedDemandId = ref<number | null>(null)
  22. const videos = ref<DemandVideoItem[]>([])
  23. const videosLoading = ref(false)
  24. const videosError = ref<string | null>(null)
  25. const selectedVid = ref<string | null>(null)
  26. const selectedDemand = computed(
  27. () => props.items.find((item) => item.id === selectedDemandId.value) ?? null,
  28. )
  29. const selectedStrategies = computed(() => parseStrategies(selectedDemand.value?.strategies))
  30. const sortedItems = computed(() =>
  31. [...props.items].sort((a, b) => gradeRank(a.grade) - gradeRank(b.grade)),
  32. )
  33. const selectedVideo = computed(
  34. () => videos.value.find((v) => v.vid === selectedVid.value) ?? null,
  35. )
  36. const hasVideoInstances = computed(() => videos.value.length > 0)
  37. interface TopicPoint {
  38. title: string
  39. pointDesc: string
  40. reason: string
  41. }
  42. interface TopicSection {
  43. key: string
  44. label: string
  45. accent: string
  46. items: TopicPoint[]
  47. }
  48. const POINT_TYPE_LABELS: Record<string, string> = {
  49. purpose: '目的点',
  50. key: '关键点',
  51. inspiration: '灵感点',
  52. }
  53. const POINT_TYPE_ORDER = ['purpose', 'key', 'inspiration'] as const
  54. function buildTopicSections(points: DemandExpansionPoint[]): TopicSection[] | null {
  55. if (!points.length) return null
  56. const grouped = new Map<string, TopicPoint[]>()
  57. for (const point of points) {
  58. const key = point.point_type || 'other'
  59. const items = grouped.get(key) ?? []
  60. items.push({
  61. title: point.expanded_text?.trim() || '未命名',
  62. pointDesc: point.point_desc?.trim() || '',
  63. reason: point.reason?.trim() || '',
  64. })
  65. grouped.set(key, items)
  66. }
  67. const sections: TopicSection[] = []
  68. for (const key of POINT_TYPE_ORDER) {
  69. const items = grouped.get(key)
  70. if (!items?.length) continue
  71. sections.push({
  72. key,
  73. label: POINT_TYPE_LABELS[key] ?? key,
  74. accent: key === 'purpose' ? 'purpose' : key === 'key' ? 'key' : 'inspiration',
  75. items,
  76. })
  77. }
  78. if (!sections.length) return null
  79. return sections
  80. }
  81. const topicSections = computed((): TopicSection[] | null => {
  82. const video = selectedVideo.value
  83. if (!video) return null
  84. return buildTopicSections(video.expansion_points ?? [])
  85. })
  86. watch(
  87. () => props.open,
  88. (open) => {
  89. if (open) return
  90. selectedDemandId.value = null
  91. videos.value = []
  92. videosError.value = null
  93. selectedVid.value = null
  94. },
  95. )
  96. watch(
  97. () => [props.highlightDemandId, props.items] as const,
  98. ([nextId, items]) => {
  99. if (!props.open) return
  100. if (!nextId) {
  101. selectedDemandId.value = null
  102. videos.value = []
  103. videosError.value = null
  104. selectedVid.value = null
  105. return
  106. }
  107. const item = items.find((i) => i.id === nextId)
  108. if (!item) return
  109. void selectDemand(item)
  110. },
  111. { immediate: true },
  112. )
  113. function gradeClass(grade: string | null | undefined): string {
  114. const g = (grade || '').toUpperCase()
  115. return g ? `grade-${g}` : 'grade-none'
  116. }
  117. async function selectDemand(item: DemandGradeItem) {
  118. if (selectedDemandId.value === item.id) return
  119. selectedDemandId.value = item.id
  120. selectedVid.value = null
  121. videos.value = []
  122. videosError.value = null
  123. videosLoading.value = true
  124. try {
  125. const res = await fetchDemandGradeVideos(item.id)
  126. videos.value = res.videos ?? []
  127. } catch (e) {
  128. videosError.value = e instanceof Error ? e.message : String(e)
  129. } finally {
  130. videosLoading.value = false
  131. }
  132. }
  133. function selectVideo(video: DemandVideoItem) {
  134. selectedVid.value = video.vid
  135. }
  136. </script>
  137. <template>
  138. <div v-if="open" class="path-rail" role="region" :aria-label="`${categoryName} 路径展开`">
  139. <!-- 当前节点 -->
  140. <template v-if="nodeName">
  141. <section class="col col-node">
  142. <div class="col-head">
  143. <h3 class="col-title">当前节点</h3>
  144. <button
  145. v-if="!hideInlineClose"
  146. type="button"
  147. class="close-btn"
  148. title="收起路径"
  149. @click="emit('close')"
  150. >
  151. ×
  152. </button>
  153. </div>
  154. <div class="card node-card">
  155. <span class="card-label">分类节点</span>
  156. <span class="card-value">{{ nodeName }}</span>
  157. <span v-if="nodePath" class="card-path">{{ nodePath }}</span>
  158. </div>
  159. </section>
  160. <div class="arrow-col" aria-hidden="true">
  161. <div class="arrow arrow-slate">
  162. <span class="arrow-line" />
  163. <span class="arrow-head">▶</span>
  164. </div>
  165. </div>
  166. </template>
  167. <!-- 节点 → 需求词 -->
  168. <div class="arrow-col" aria-hidden="true">
  169. <div class="arrow arrow-blue">
  170. <span class="arrow-line" />
  171. <span class="arrow-head">▶</span>
  172. </div>
  173. </div>
  174. <section class="col col-demand">
  175. <div class="col-head">
  176. <h3 class="col-title">细节元素 · 需求词</h3>
  177. <button
  178. v-if="!nodeName && !hideInlineClose"
  179. type="button"
  180. class="close-btn"
  181. title="收起路径"
  182. @click="emit('close')"
  183. >
  184. ×
  185. </button>
  186. </div>
  187. <div v-if="sortedItems.length" class="card-list">
  188. <button
  189. v-for="item in sortedItems"
  190. :key="item.id"
  191. type="button"
  192. class="card demand-card"
  193. :class="{ active: selectedDemandId === item.id }"
  194. @click="selectDemand(item)"
  195. >
  196. <div class="demand-card-head">
  197. <span class="card-label">需求词</span>
  198. <span class="grade-badge" :class="gradeClass(item.grade)">{{ item.grade || '—' }}</span>
  199. </div>
  200. <span class="card-value">{{ item.demand_name || '—' }}</span>
  201. <div v-if="parseStrategies(item.strategies).length" class="strategy-tags">
  202. <span
  203. v-for="s in parseStrategies(item.strategies)"
  204. :key="s"
  205. class="strategy-tag"
  206. >{{ s }}</span>
  207. </div>
  208. </button>
  209. </div>
  210. <div v-else class="empty">暂无需求词</div>
  211. </section>
  212. <!-- 需求词 → 视频 -->
  213. <template v-if="hasVideoInstances">
  214. <div class="arrow-col" aria-hidden="true">
  215. <div v-if="selectedDemand" class="arrow arrow-green">
  216. <span class="arrow-line" />
  217. <span class="arrow-head">▶</span>
  218. </div>
  219. </div>
  220. <section class="col col-video">
  221. <h3 class="col-title">真实视频实例</h3>
  222. <template v-if="selectedDemand">
  223. <div class="detail-meta">
  224. <span class="grade-badge" :class="gradeClass(selectedDemand.grade)">
  225. {{ selectedDemand.grade || '—' }}
  226. </span>
  227. <div v-if="selectedStrategies.length" class="strategy-tags">
  228. <span v-for="s in selectedStrategies" :key="s" class="strategy-tag">{{ s }}</span>
  229. </div>
  230. </div>
  231. <div v-if="videosLoading" class="empty">加载中…</div>
  232. <div v-else-if="videosError" class="empty error">{{ videosError }}</div>
  233. <div v-else class="card-list">
  234. <button
  235. v-for="video in videos"
  236. :key="video.vid"
  237. type="button"
  238. class="card video-card"
  239. :class="{ active: selectedVid === video.vid }"
  240. @click="selectVideo(video)"
  241. >
  242. <span class="card-label">Video {{ video.vid }}</span>
  243. <span class="card-value">{{ video.title || '(无标题)' }}</span>
  244. </button>
  245. </div>
  246. </template>
  247. </section>
  248. <!-- 视频 → 选题 -->
  249. <div class="arrow-col" aria-hidden="true">
  250. <div v-if="selectedVideo" class="arrow arrow-purple">
  251. <span class="arrow-line" />
  252. <span class="arrow-head">▶</span>
  253. </div>
  254. </div>
  255. <section class="col col-topic">
  256. <h3 class="col-title">选题结果</h3>
  257. <template v-if="selectedVideo">
  258. <div v-if="topicSections" class="topic-card">
  259. <div class="topic-meta">Video {{ selectedVideo.vid }}</div>
  260. <div class="topic-body">
  261. <section
  262. v-for="section in topicSections"
  263. :key="section.key"
  264. class="topic-section"
  265. :class="`accent-${section.accent}`"
  266. >
  267. <div class="section-head">
  268. <span class="section-label">{{ section.label }}</span>
  269. <span class="section-count">{{ section.items.length }}</span>
  270. </div>
  271. <div v-if="section.items.length" class="point-list">
  272. <article v-for="(item, idx) in section.items" :key="idx" class="point-item">
  273. <div class="point-title-row">
  274. <span class="point-index">{{ idx + 1 }}</span>
  275. <h4 class="point-title">{{ item.title || '未命名' }}</h4>
  276. </div>
  277. <p v-if="item.pointDesc" class="point-desc">{{ item.pointDesc }}</p>
  278. <p v-if="item.reason" class="point-reason">{{ item.reason }}</p>
  279. </article>
  280. </div>
  281. <div v-else class="section-empty">暂无</div>
  282. </section>
  283. </div>
  284. </div>
  285. <div v-else class="empty">暂无选题结果</div>
  286. </template>
  287. <div v-else class="empty hint">点击视频展开</div>
  288. </section>
  289. </template>
  290. </div>
  291. </template>
  292. <style scoped>
  293. .path-rail {
  294. display: flex;
  295. flex-direction: row;
  296. align-items: stretch;
  297. gap: 0;
  298. padding-left: 0;
  299. min-height: 0;
  300. height: 100%;
  301. width: max-content;
  302. min-width: 100%;
  303. max-width: 100%;
  304. }
  305. .col {
  306. width: 220px;
  307. flex-shrink: 0;
  308. display: flex;
  309. flex-direction: column;
  310. gap: 10px;
  311. min-height: 0;
  312. height: 100%;
  313. }
  314. .col-topic {
  315. width: 380px;
  316. }
  317. .col-head {
  318. display: flex;
  319. align-items: center;
  320. justify-content: space-between;
  321. gap: 6px;
  322. }
  323. .col-title {
  324. margin: 0;
  325. font-size: 12px;
  326. font-weight: 700;
  327. letter-spacing: 0.03em;
  328. color: #64748b;
  329. }
  330. .col-demand .col-title {
  331. color: #1d4ed8;
  332. }
  333. .col-node .col-title {
  334. color: #334155;
  335. }
  336. .col-video .col-title {
  337. color: #15803d;
  338. }
  339. .col-topic .col-title {
  340. color: #7e22ce;
  341. }
  342. .close-btn {
  343. width: 22px;
  344. height: 22px;
  345. border: none;
  346. border-radius: 4px;
  347. background: transparent;
  348. color: #94a3b8;
  349. font-size: 16px;
  350. line-height: 1;
  351. cursor: pointer;
  352. flex-shrink: 0;
  353. padding: 0;
  354. }
  355. .close-btn:hover {
  356. background: #f1f5f9;
  357. color: #0f172a;
  358. }
  359. .card-list {
  360. display: flex;
  361. flex-direction: column;
  362. gap: 8px;
  363. flex: 1;
  364. min-height: 80px;
  365. overflow: auto;
  366. overscroll-behavior: contain;
  367. padding-right: 2px;
  368. }
  369. .card {
  370. display: flex;
  371. flex-direction: column;
  372. align-items: flex-start;
  373. gap: 3px;
  374. width: 100%;
  375. padding: 10px 12px;
  376. border-radius: 10px;
  377. border: 1.5px solid transparent;
  378. background: #fff;
  379. text-align: left;
  380. cursor: pointer;
  381. box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
  382. transition: border-color 0.15s ease, box-shadow 0.15s ease;
  383. }
  384. .card:hover {
  385. box-shadow: 0 3px 10px rgba(15, 23, 42, 0.08);
  386. }
  387. .card-label {
  388. font-size: 11px;
  389. font-weight: 600;
  390. opacity: 0.8;
  391. }
  392. .card-value {
  393. font-size: 13px;
  394. font-weight: 600;
  395. line-height: 1.35;
  396. word-break: break-word;
  397. color: #0f172a;
  398. }
  399. .card-path {
  400. margin-top: 4px;
  401. font-size: 11px;
  402. color: #94a3b8;
  403. line-height: 1.35;
  404. word-break: break-word;
  405. }
  406. .node-card {
  407. border-color: #cbd5e1;
  408. background: linear-gradient(180deg, #f8fafc 0%, #fff 100%);
  409. cursor: default;
  410. }
  411. .node-card .card-label {
  412. color: #475569;
  413. }
  414. .demand-card {
  415. border-color: #93c5fd;
  416. background: linear-gradient(180deg, #eff6ff 0%, #fff 100%);
  417. }
  418. .demand-card-head {
  419. display: flex;
  420. align-items: center;
  421. justify-content: space-between;
  422. width: 100%;
  423. gap: 8px;
  424. }
  425. .grade-badge {
  426. display: inline-flex;
  427. align-items: center;
  428. justify-content: center;
  429. min-width: 22px;
  430. height: 20px;
  431. padding: 0 6px;
  432. border-radius: 6px;
  433. font-size: 11px;
  434. font-weight: 700;
  435. letter-spacing: 0.02em;
  436. flex-shrink: 0;
  437. }
  438. .grade-badge.grade-S {
  439. background: #fee2e2;
  440. color: #b91c1c;
  441. }
  442. .grade-badge.grade-A {
  443. background: #ffedd5;
  444. color: #c2410c;
  445. }
  446. .grade-badge.grade-B {
  447. background: #fef9c3;
  448. color: #a16207;
  449. }
  450. .grade-badge.grade-C {
  451. background: #e0f2fe;
  452. color: #0369a1;
  453. }
  454. .grade-badge.grade-D {
  455. background: #f1f5f9;
  456. color: #64748b;
  457. }
  458. .grade-badge.grade-none {
  459. background: #f1f5f9;
  460. color: #94a3b8;
  461. }
  462. .strategy-tags {
  463. display: flex;
  464. flex-wrap: wrap;
  465. gap: 4px;
  466. margin-top: 2px;
  467. }
  468. .strategy-tag {
  469. display: inline-flex;
  470. align-items: center;
  471. height: 18px;
  472. padding: 0 6px;
  473. border-radius: 999px;
  474. background: rgba(99, 102, 241, 0.12);
  475. color: #4338ca;
  476. font-size: 10px;
  477. font-weight: 600;
  478. white-space: nowrap;
  479. }
  480. .detail-meta {
  481. display: flex;
  482. flex-wrap: wrap;
  483. align-items: center;
  484. gap: 8px;
  485. margin-bottom: 6px;
  486. }
  487. .demand-card .card-label {
  488. color: #1d4ed8;
  489. }
  490. .demand-card.active {
  491. border-color: #2563eb;
  492. box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.16);
  493. }
  494. .video-card {
  495. border-color: #86efac;
  496. background: linear-gradient(180deg, #f0fdf4 0%, #fff 100%);
  497. }
  498. .video-card .card-label {
  499. color: #15803d;
  500. font-variant-numeric: tabular-nums;
  501. }
  502. .video-card.active {
  503. border-color: #16a34a;
  504. box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.16);
  505. }
  506. .arrow-col {
  507. width: 40px;
  508. flex-shrink: 0;
  509. display: flex;
  510. align-items: flex-start;
  511. justify-content: center;
  512. padding-top: 42px;
  513. }
  514. .arrow {
  515. display: flex;
  516. align-items: center;
  517. width: 100%;
  518. }
  519. .arrow-line {
  520. flex: 1;
  521. height: 3px;
  522. border-radius: 999px;
  523. }
  524. .arrow-head {
  525. font-size: 11px;
  526. line-height: 1;
  527. margin-left: -1px;
  528. }
  529. .arrow-blue .arrow-line {
  530. background: #3b82f6;
  531. }
  532. .arrow-blue .arrow-head {
  533. color: #3b82f6;
  534. }
  535. .arrow-slate .arrow-line {
  536. background: #94a3b8;
  537. }
  538. .arrow-slate .arrow-head {
  539. color: #94a3b8;
  540. }
  541. .arrow-green .arrow-line {
  542. background: #22c55e;
  543. }
  544. .arrow-green .arrow-head {
  545. color: #22c55e;
  546. }
  547. .arrow-purple .arrow-line {
  548. background: #a855f7;
  549. }
  550. .arrow-purple .arrow-head {
  551. color: #a855f7;
  552. }
  553. .topic-card {
  554. border: 1.5px solid #d8b4fe;
  555. border-radius: 12px;
  556. background: linear-gradient(180deg, #faf5ff 0%, #fff 45%);
  557. overflow: hidden;
  558. flex: 1;
  559. min-height: 80px;
  560. display: flex;
  561. flex-direction: column;
  562. }
  563. .topic-meta {
  564. padding: 8px 12px;
  565. border-bottom: 1px solid #f3e8ff;
  566. font-size: 11px;
  567. font-weight: 600;
  568. color: #7e22ce;
  569. font-variant-numeric: tabular-nums;
  570. flex-shrink: 0;
  571. }
  572. .topic-body {
  573. padding: 10px 12px 12px;
  574. overflow: auto;
  575. display: flex;
  576. flex-direction: column;
  577. gap: 14px;
  578. }
  579. .topic-section {
  580. display: flex;
  581. flex-direction: column;
  582. gap: 8px;
  583. }
  584. .section-head {
  585. display: flex;
  586. align-items: center;
  587. gap: 8px;
  588. }
  589. .section-label {
  590. font-size: 12px;
  591. font-weight: 700;
  592. letter-spacing: 0.02em;
  593. }
  594. .section-count {
  595. min-width: 18px;
  596. height: 18px;
  597. padding: 0 5px;
  598. border-radius: 999px;
  599. font-size: 10px;
  600. font-weight: 700;
  601. line-height: 18px;
  602. text-align: center;
  603. font-variant-numeric: tabular-nums;
  604. }
  605. .accent-inspiration .section-label {
  606. color: #c2410c;
  607. }
  608. .accent-inspiration .section-count {
  609. color: #c2410c;
  610. background: #ffedd5;
  611. }
  612. .accent-inspiration .point-index {
  613. background: #ffedd5;
  614. color: #c2410c;
  615. }
  616. .accent-purpose .section-label {
  617. color: #1d4ed8;
  618. }
  619. .accent-purpose .section-count {
  620. color: #1d4ed8;
  621. background: #dbeafe;
  622. }
  623. .accent-purpose .point-index {
  624. background: #dbeafe;
  625. color: #1d4ed8;
  626. }
  627. .accent-key .section-label {
  628. color: #7e22ce;
  629. }
  630. .accent-key .section-count {
  631. color: #7e22ce;
  632. background: #f3e8ff;
  633. }
  634. .accent-key .point-index {
  635. background: #f3e8ff;
  636. color: #7e22ce;
  637. }
  638. .point-list {
  639. display: flex;
  640. flex-direction: column;
  641. gap: 8px;
  642. }
  643. .point-item {
  644. padding: 10px 11px;
  645. border-radius: 10px;
  646. background: #fff;
  647. border: 1px solid #e2e8f0;
  648. box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
  649. }
  650. .point-title-row {
  651. display: flex;
  652. align-items: flex-start;
  653. gap: 8px;
  654. }
  655. .point-index {
  656. flex-shrink: 0;
  657. width: 18px;
  658. height: 18px;
  659. margin-top: 1px;
  660. border-radius: 5px;
  661. font-size: 10px;
  662. font-weight: 700;
  663. line-height: 18px;
  664. text-align: center;
  665. font-variant-numeric: tabular-nums;
  666. }
  667. .point-title {
  668. margin: 0;
  669. font-size: 13px;
  670. font-weight: 700;
  671. line-height: 1.35;
  672. color: #0f172a;
  673. word-break: break-word;
  674. }
  675. .point-desc {
  676. margin: 6px 0 0;
  677. padding-left: 26px;
  678. font-size: 12px;
  679. line-height: 1.55;
  680. color: #475569;
  681. white-space: pre-wrap;
  682. word-break: break-word;
  683. }
  684. .point-reason {
  685. margin: 4px 0 0;
  686. padding-left: 26px;
  687. font-size: 11px;
  688. line-height: 1.5;
  689. color: #64748b;
  690. white-space: pre-wrap;
  691. word-break: break-word;
  692. }
  693. .section-empty {
  694. padding: 8px 10px;
  695. font-size: 12px;
  696. color: #94a3b8;
  697. border: 1px dashed #e2e8f0;
  698. border-radius: 8px;
  699. background: rgba(255, 255, 255, 0.6);
  700. }
  701. .empty {
  702. padding: 20px 10px;
  703. text-align: center;
  704. color: #94a3b8;
  705. font-size: 12px;
  706. border: 1px dashed #e2e8f0;
  707. border-radius: 10px;
  708. background: rgba(255, 255, 255, 0.7);
  709. }
  710. .empty.hint {
  711. border-style: solid;
  712. background: transparent;
  713. }
  714. .empty.error {
  715. color: #b91c1c;
  716. border-color: #fecaca;
  717. background: #fef2f2;
  718. }
  719. @media (max-height: 800px) {
  720. .arrow-col {
  721. padding-top: 32px;
  722. }
  723. }
  724. </style>