detail.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. <!--
  2. * @Author: wanglj wanglijie@dashoo.cn
  3. * @Date: 2025-03-24 09:17:15
  4. * @LastEditors: wanglj wanglijie@dashoo.cn
  5. * @LastEditTime: 2025-03-28 11:39:21
  6. * @FilePath: \labsop_h5\src\view\instr\detail.vue
  7. * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  8. -->
  9. <template>
  10. <div class="instr-detail">
  11. <van-swipe v-if="noticeInfo.noticeTitle" class="my-swipe" :autoplay="5000" :show-indicators="false" vertical
  12. height="30">
  13. <van-swipe-item @click="state.popupShow = true">
  14. <div class="flex">
  15. <van-icon name="volume-o" class="mr4" :size="20" />
  16. {{ noticeInfo.noticeTitle }}
  17. </div>
  18. </van-swipe-item>
  19. </van-swipe>
  20. <header class="flex">
  21. <div class="h100">
  22. <!-- <img :showLoading="true" :src="state.instDetail.instPicture" width="80px" height="80px" /> -->
  23. <van-image width="80px" height="80px" :src="getImageUrl(state.instDetail.instPicture)" />
  24. </div>
  25. <div class="i-right ml10">
  26. <div class="h100 flex flex-top flex-column flex-between">
  27. <div class="flex flex-top mb4 ml2">
  28. <div class="detailTxt name">{{ state.instDetail.instName }}({{ state.instDetail.instCode }})</div>
  29. </div>
  30. <footer>
  31. <div class="flex flex-top mb4 mt-auto">
  32. <img class="i-r-icon" src="../../assets/img/user.png" v-if="state.instDetail.instHeadName" />
  33. <div class="detailTxt">{{ state.instDetail.instHeadName }}</div>
  34. </div>
  35. <div class="flex flex-top">
  36. <img class="i-r-icon" src="../../assets/img/address.png" v-if="state.instDetail.placeAddress" />
  37. <div class="detailTxt">
  38. {{ state.instDetail.placeAddress + setLaboratoryName(state.instDetail.laboratoryName) }}
  39. </div>
  40. </div>
  41. </footer>
  42. </div>
  43. </div>
  44. </header>
  45. <van-tabs v-model:active="active" @change="tabChange">
  46. <van-tab title="仪器信息" name="info"></van-tab>
  47. <van-tab title="待审核" name="approval"></van-tab>
  48. <van-tab title="历史申请" name="history"></van-tab>
  49. </van-tabs>
  50. <div v-if="active === 'info'" class="content">
  51. <div class="card">
  52. <h4>仪器信息</h4>
  53. <ul>
  54. <li>
  55. <label>名称</label>
  56. <span>{{ state.instDetail.instName }}</span>
  57. </li>
  58. <li>
  59. <label>编号</label>
  60. <span>{{ state.instDetail.instCode }}</span>
  61. </li>
  62. <li>
  63. <label>仪器型号</label>
  64. <span>{{ state.instDetail.instNameEn }}</span>
  65. </li>
  66. <li>
  67. <label>当前状态</label>
  68. <span>{{ state.instStatus[state.instDetail.instStatus] }}</span>
  69. </li>
  70. <li>
  71. <label>所属组织</label>
  72. <span>{{ state.instDetail.belongOrgName }}</span>
  73. </li>
  74. <li>
  75. <label>位置</label>
  76. <span>{{ state.instDetail.placeAddress }}</span>
  77. </li>
  78. <li>
  79. <label>负责人</label>
  80. <span>{{ state.instDetail.instHeadName }}</span>
  81. </li>
  82. <li>
  83. <label>联系方式</label>
  84. <span>{{ state.instDetail.instHeadTel }}</span>
  85. </li>
  86. </ul>
  87. </div>
  88. <div class="card">
  89. <h4>申请须知</h4>
  90. <div class="text">{{ state.instDetail.applicationNotes }}</div>
  91. </div>
  92. <div class="card">
  93. <h4>主要功能</h4>
  94. <div class="text">{{ state.instDetail.instFunctFeat }}</div>
  95. </div>
  96. <div class="card" v-if="isNeedGrant" @click="applicationAuth">
  97. <h4>资质申请</h4>
  98. </div>
  99. <div class="card" v-if="isNeedGrant" @click="applyTraining">
  100. <h4>培训申请</h4>
  101. </div>
  102. <!-- <div class="card">
  103. <h4>相关附件</h4>
  104. <template v-for="item in state.instFiles">
  105. <div class="file-item">
  106. <a href="javascript: void(0);" @click="realDown(item.docName, item.docUrl)">{{ item.docName }}</a>
  107. </div>
  108. </template>
  109. </div> -->
  110. </div>
  111. <van-list v-else v-model:loading="state.loading" :finished="state.finished" finished-text="没有更多了" @load="onLoad">
  112. <van-cell v-for="item in state.list" :key="item.id">
  113. <template #default>
  114. <div class="list">
  115. <header class="flex justify-between">
  116. <strong class="title">{{ item.userName }}的预约</strong>
  117. <van-tag v-if="item.appointStatus == '10'" type="default">
  118. 待审核
  119. </van-tag>
  120. <van-tag v-else-if="item.appointStatus == '11'" type="warning">
  121. 已退回
  122. </van-tag>
  123. <van-tag v-else-if="item.appointStatus == '20'" type="success">
  124. 已通过
  125. </van-tag>
  126. <van-tag v-else-if="item.appointStatus == '30'" type="danger">
  127. 已驳回
  128. </van-tag>
  129. <van-tag v-else-if="item.appointStatus == '40'" type="warning">
  130. 已取消
  131. </van-tag>
  132. <van-tag v-else-if="item.appointStatus == '50'" type="default">
  133. 已上机
  134. </van-tag>
  135. <van-tag v-else-if="item.appointStatus == '60'" type="primary">
  136. 已完成
  137. </van-tag>
  138. <van-tag v-else-if="item.appointStatus == '70'" type="warning">
  139. 审核超时
  140. </van-tag>
  141. <van-tag v-else-if="item.appointStatus == '80'" type="danger">
  142. 超时取消
  143. </van-tag>
  144. <van-tag v-else-if="item.appointStatus == '90'" type="danger">
  145. 超时未上机
  146. </van-tag>
  147. </header>
  148. <p class="inst-title">
  149. <span>预约仪器</span>
  150. <span class="title ml8">{{ item.instName }}({{ item.instCode }})</span>
  151. </p>
  152. <p class="inst-title">
  153. <span>预约时间</span>
  154. <span class="title ml8">
  155. {{ formatDate(new Date(item.startTime), 'YYYY-mm-dd HH:MM') }}~{{
  156. formatDate(new Date(item.endTime), 'YYYY-mm-dd HH:MM')
  157. }}
  158. </span>
  159. </p>
  160. <p class="inst-title">
  161. <span>预约时长</span>
  162. <span class="title ml8">{{ getAppointTime(item) }}</span>
  163. </p>
  164. <p class="inst-title">
  165. <span>违约情况</span>
  166. <span class="title ml8">{{ getBreachTypes(item) }}</span>
  167. </p>
  168. <p class="inst-title">
  169. <span>扣分明细</span>
  170. <span class="title ml8">{{ item.breachScore }}分</span>
  171. </p>
  172. <p class="inst-title">
  173. <span>备注</span>
  174. <span class="title ml8">{{ item.remark }}</span>
  175. </p>
  176. <footer class="flex justify-between mt4">
  177. <span class="title">{{ item.userName }}</span>
  178. <span class="time">{{ formatDate(new Date(item.createdTime), 'mm-dd HH:MM') }}</span>
  179. </footer>
  180. </div>
  181. </template>
  182. </van-cell>
  183. </van-list>
  184. <van-back-top target=".instr-detail" bottom="10vh" />
  185. </div>
  186. <van-action-bar placeholder>
  187. <van-action-bar-icon icon="wap-home-o" text="首页" @click="onRouterPush('/home')" />
  188. <van-action-bar-icon :icon="state.instDetail.following ? 'star' : 'star-o'"
  189. :class="{ follow: state.instDetail.following }" :text="state.instDetail.following ? '取消收藏' : '收藏'"
  190. @click="handleFollowInst" />
  191. <van-action-bar-icon icon="revoke" text="返回" @click="onRouterPush('/instr-list')" />
  192. <van-action-bar-button v-if="state.instDetail.instStatus == '10' && state.instDetail.isAppointment == '10'"
  193. v-auth="'instr_appoint_btn'" type="primary" text="使用预约" @click="onAppoint('use')" />
  194. <van-action-bar-button v-if="state.instDetail.instStatus == '10' && state.instDetail.isSampleDelivery == '10'"
  195. v-auth="'instr_sampleDelivery_btn'" type="warning" text="送样预约" @click="onAppoint('sample')" />
  196. </van-action-bar>
  197. <!-- 通知 -->
  198. <van-popup v-model:show="state.popupShow" round :closeable="true" position="top" :style="{ padding: '20px' }">
  199. <h4>{{ noticeInfo.noticeTitle }}</h4>
  200. <div class="notice-container" v-html="noticeInfo.noticeContent"></div>
  201. </van-popup>
  202. <!-- 申请须知 -->
  203. <van-popup v-model:show="state.needToKnowShow" round :closeable="true" position="bottom" :style="{ height: '90vh' }">
  204. <div class="need-to-know">
  205. <h4 class="mt8 mb8">申请须知</h4>
  206. <p>{{ state.instDetail.applicationNotes }}</p>
  207. <footer>
  208. <van-button class="w100" type="primary" round @click="confirmAppoint">
  209. 我知道了
  210. </van-button>
  211. </footer>
  212. </div>
  213. </van-popup>
  214. <AddAuthDialog ref="addAuthDialogRef" />
  215. </template>
  216. <script lang="ts" setup>
  217. import to from 'await-to-js'
  218. import { useRoute, useRouter } from 'vue-router'
  219. import { ElMessageBox, ElMessage } from 'element-plus'
  220. import { useInstrApi } from '/@/api/instr'
  221. import { useInstDocApi } from '/@/api/instr/document'
  222. import { onMounted, reactive, ref } from 'vue'
  223. import { formatDate } from '/@/utils/formatTime'
  224. import { showNotify } from 'vant'
  225. import download from 'downloadjs'
  226. import { useNoticeApi } from '/@/api/instr/notice'
  227. import { useUseAppointApi } from '/@/api/instr/useAppoint'
  228. import { useBlackApi } from '/@/api/blacklist'
  229. import AddAuthDialog from './addAuthorization/index.vue'
  230. import { useUserInfos } from '/@/hooks/useUserInfos'
  231. import { useTrainingApi } from '/@/api/instr/inst/training'
  232. import { getImageUrl } from '/@/utils/url'
  233. const route = useRoute()
  234. const router = useRouter()
  235. const instApi = useInstrApi()
  236. const instDocApi = useInstDocApi()
  237. const noticeApi = useNoticeApi()
  238. const useAppointApi = useUseAppointApi()
  239. const blacklistApi = useBlackApi()
  240. const trainingApi = useTrainingApi()
  241. const active = ref('info')
  242. const state = reactive({
  243. detailsLoading: false,
  244. instStatus: {
  245. 10: '正常',
  246. 20: '故障',
  247. 30: '报废',
  248. },
  249. instDetail: {} as any,
  250. instFiles: [] as any[],
  251. loading: false,
  252. finished: false,
  253. queryParams: {
  254. pageNum: 1,
  255. pageSize: 10,
  256. instId: 0,
  257. appointStatus: [],
  258. },
  259. list: [] as any[],
  260. popupShow: false,
  261. needToKnowShow: false,
  262. appointType: '',
  263. })
  264. const noticeInfo = ref({ noticeTitle: '', noticeContent: '' })
  265. const isNeedGrant = ref(false)
  266. const addAuthDialogRef = ref()
  267. const { userInfos } = useUserInfos()
  268. const getNeedGrant = async (instId: number) => {
  269. const [err, res]: ToResponse = await to(useAppointApi.getNeedGrant({ instId }))
  270. if (err) return
  271. isNeedGrant.value = res?.data
  272. }
  273. // 获取仪器详情
  274. const getDetail = async (id: number) => {
  275. state.detailsLoading = true
  276. const [err, res]: ToResponse = await to(instApi.getDetail({ id }))
  277. state.detailsLoading = false
  278. if (err) return
  279. if (res?.code === 200) {
  280. state.instDetail = res.data
  281. getDocs()
  282. getNotice()
  283. }
  284. }
  285. const getNotice = async () => {
  286. const param = {
  287. pageNum: 1,
  288. pageSize: 1,
  289. instId: state.instDetail.instId,
  290. }
  291. const [err, res]: ToResponse = await to(noticeApi.list({ ...param }))
  292. if (err) return
  293. noticeInfo.value = res?.data?.list.length > 0 ? res?.data?.list[0] : {}
  294. }
  295. // 附件列表
  296. const getDocs = async () => {
  297. const [err, res]: ToResponse = await to(
  298. instDocApi.list({ noPage: true, instId: state.instDetail.instId, docType: '' }),
  299. )
  300. if (err) return
  301. state.instFiles = res?.data.list || []
  302. }
  303. const realDown = (filename: string, fileurl: string) => {
  304. let ua = navigator.userAgent.toLowerCase()
  305. if (ua.includes('mac')) {
  306. //iOS 将文件url转换为文件流 在下载
  307. downloadFun(fileurl + '?response-content-type=application/octet-stream', filename)
  308. } else {
  309. //android 直接用插件的方法下载即可
  310. download(fileurl, filename)
  311. }
  312. }
  313. // 创建a标签 实现下载
  314. const downloadFun = async (blobFile, fileName) => {
  315. let blob = new Blob([blobFile], {
  316. type: 'application/pdf;charset=UTF-8',
  317. })
  318. // @ts-ignore
  319. if (window.navigator.msSaveOrOpenBlob) {
  320. // @ts-ignore
  321. navigator.msSaveBlob(blob, fileName)
  322. } else {
  323. let link = document.createElement('a')
  324. link.href = window.URL.createObjectURL(blob)
  325. link.download = fileName
  326. link.click()
  327. window.URL.revokeObjectURL(link.href) //释放内存
  328. }
  329. }
  330. const setLaboratoryName = (name) => {
  331. return name ? `(${name})` : ''
  332. }
  333. const tabChange = (name: string) => {
  334. if (name === 'history' || name === 'approval') {
  335. state.finished = false
  336. state.list = []
  337. state.queryParams = {
  338. pageNum: 1,
  339. pageSize: 10,
  340. instId: state.instDetail.id,
  341. appointStatus: name === 'approval' ? ['10'] : [],
  342. }
  343. onLoad()
  344. }
  345. }
  346. const onLoad = async () => {
  347. state.loading = true
  348. const [err, res]: ToResponse = await to(useAppointApi.getListByPermission(state.queryParams))
  349. if (err) return
  350. const list = res?.data?.list || []
  351. for (const item of list) {
  352. state.list.push(item)
  353. }
  354. state.loading = false
  355. state.queryParams.pageNum++
  356. if (list.length < state.queryParams.pageSize) {
  357. state.finished = true
  358. }
  359. }
  360. const getBreachTypes = (row: any) => {
  361. let breachTypes = <string[]>[]
  362. if (row.isLate) breachTypes.push('迟到')
  363. if (row.isOvertime) breachTypes.push('超时')
  364. if (row.isLeaveEarly) breachTypes.push('早退')
  365. if (row.isAbsence) breachTypes.push('爽约')
  366. return breachTypes.join('、') || '-'
  367. }
  368. const getAppointTime = (row: any) => {
  369. const startDate = new Date(row.startTime)
  370. const endDate = new Date(row.endTime)
  371. // 计算两个日期之间的时间差(以毫秒为单位)
  372. const timeDifference = endDate.getTime() - startDate.getTime()
  373. // 计算天数
  374. const days = Math.floor(timeDifference / (1000 * 60 * 60 * 24))
  375. // 计算剩余的毫秒数
  376. const remainingMilliseconds = timeDifference % (1000 * 60 * 60 * 24)
  377. // 计算小时数
  378. const hours = Math.floor(remainingMilliseconds / (1000 * 60 * 60))
  379. // 计算剩余的毫秒数
  380. const remainingMillisecondsAfterHours = remainingMilliseconds % (1000 * 60 * 60)
  381. // 计算分钟数
  382. const minutes = Math.floor(remainingMillisecondsAfterHours / (1000 * 60))
  383. return `${days}天${hours}小时${minutes}分`
  384. }
  385. // 关注/取关
  386. const handleFollowInst = async () => {
  387. const [err] = state.instDetail.following
  388. ? await to(instApi.unfollow({ ids: [state.instDetail.id] }))
  389. : await to(instApi.follow({ ids: [state.instDetail.id] }))
  390. if (err) return
  391. showNotify({ type: 'success', message: !state.instDetail.following ? '收藏成功' : '已取消收藏' })
  392. getDetail(state.instDetail.id)
  393. }
  394. const onAppoint = async (type: string) => {
  395. state.appointType = type
  396. state.needToKnowShow = true
  397. }
  398. const confirmAppoint = async () => {
  399. const [err, res]: ToResponse = await to(blacklistApi.checkInBlacklist())
  400. if (err) return
  401. if (res.data) {
  402. showNotify({ type: 'danger', message: '您已被拉入黑名单,无法预约,请联系管理员' })
  403. return
  404. }
  405. if (state.appointType === 'sample') {
  406. onRouterPush('/sample-appoint', { id: state.instDetail.id })
  407. } else {
  408. onRouterPush('/instr-appoint', { id: state.instDetail.id })
  409. }
  410. }
  411. const onRouterPush = (val: string, params?: any) => {
  412. router.push({
  413. path: val,
  414. query: { ...params },
  415. })
  416. }
  417. const applicationAuth = async () => {
  418. console.log('applicationAuth')
  419. addAuthDialogRef.value.openDialog('personal', state.instDetail)
  420. }
  421. const applyTraining = () => {
  422. console.log('applyTraining')
  423. ElMessageBox.confirm('确认发起培训申请?', '提示', {
  424. confirmButtonText: '确认',
  425. cancelButtonText: '取消',
  426. type: 'warning',
  427. })
  428. .then(async () => {
  429. const params = {
  430. instCode: state.instDetail.instCode,
  431. instId: state.instDetail.id,
  432. instName: state.instDetail.instName,
  433. userId: userInfos.value.id,
  434. userName: userInfos.value.nickName,
  435. }
  436. const [err]: ToResponse = await to(trainingApi.add({ ...params }))
  437. if (err) return
  438. ElMessage.success('培训申请提交成功')
  439. })
  440. .catch(() => { })
  441. }
  442. onMounted(() => {
  443. const id = route.query.id ? +route.query.id : 0
  444. getDetail(id)
  445. getNeedGrant(id)
  446. })
  447. </script>
  448. <style lang="scss" scoped>
  449. .instr-detail {
  450. flex: 1;
  451. overflow-y: auto;
  452. background-color: #f7f8fa;
  453. .my-swipe {
  454. background-color: #fff;
  455. height: 30px !important;
  456. line-height: 30px !important;
  457. :deep(.flex) {
  458. height: 30px;
  459. overflow: hidden;
  460. padding: 0 12px;
  461. span {
  462. display: inline-block;
  463. height: 30px;
  464. line-height: 30px;
  465. }
  466. span:first-child {
  467. flex: 1;
  468. white-space: nowrap;
  469. overflow: hidden;
  470. text-overflow: ellipsis;
  471. }
  472. }
  473. }
  474. >header {
  475. height: auto;
  476. min-height: 80px;
  477. background-color: #fff;
  478. padding: 12px;
  479. }
  480. .inst-info {
  481. display: flex;
  482. }
  483. .i-right {
  484. flex: 1;
  485. font-size: 14px;
  486. height: auto;
  487. min-height: 80px;
  488. .i-r-icon {
  489. width: 15px;
  490. height: 15px;
  491. margin-right: 10px;
  492. }
  493. }
  494. .detailTxt {
  495. font-size: 12px;
  496. color: #333333;
  497. white-space: normal;
  498. overflow: visible;
  499. text-overflow: unset;
  500. word-break: break-all;
  501. &.name {
  502. font-weight: bold;
  503. font-size: 16px;
  504. }
  505. }
  506. .content {
  507. padding: 10px;
  508. }
  509. .card {
  510. border-radius: 4px;
  511. background-color: #fff;
  512. padding: 10px;
  513. box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
  514. &+.card {
  515. margin-top: 10px;
  516. }
  517. h4 {
  518. height: 18px;
  519. line-height: 18px;
  520. display: flex;
  521. margin-bottom: 10px;
  522. span {
  523. font-weight: normal;
  524. margin-left: auto;
  525. }
  526. &::before {
  527. display: inline-block;
  528. content: '';
  529. width: 3px;
  530. height: 18px;
  531. background-color: #1c9bfd;
  532. margin-right: 4px;
  533. vertical-align: middle;
  534. }
  535. }
  536. >ul {
  537. li {
  538. display: flex;
  539. padding: 6px 0;
  540. label {
  541. width: 80px;
  542. min-width: 80px;
  543. color: #969799;
  544. }
  545. span {
  546. word-break: break-all;
  547. }
  548. }
  549. }
  550. .text {
  551. white-space: pre-wrap;
  552. }
  553. }
  554. .van-list {
  555. padding: 10px;
  556. border-radius: 4px;
  557. flex: 1;
  558. .van-cell {
  559. background-color: #fff;
  560. +.van-cell {
  561. margin-top: 10px;
  562. }
  563. header,
  564. footer {
  565. color: #333;
  566. }
  567. .title {
  568. flex: 1;
  569. white-space: nowrap;
  570. overflow: hidden;
  571. text-overflow: ellipsis;
  572. text-align: left;
  573. }
  574. .inst-title {
  575. color: #333;
  576. text-align: left;
  577. flex: 1;
  578. overflow: hidden;
  579. white-space: nowrap;
  580. text-overflow: ellipsis;
  581. margin-top: 4px;
  582. span:first-child {
  583. display: inline-block;
  584. width: 80px;
  585. min-width: 80px;
  586. color: rgb(120, 120, 120);
  587. }
  588. }
  589. .time {
  590. color: #f69a4d;
  591. }
  592. }
  593. }
  594. }
  595. .btns {
  596. flex: 1;
  597. display: flex;
  598. li {
  599. display: flex;
  600. flex-direction: column;
  601. align-items: center;
  602. justify-content: center;
  603. padding: 0 8px;
  604. font-size: 12px;
  605. i {
  606. margin-bottom: 4px;
  607. }
  608. }
  609. }
  610. :deep(.follow .van-icon) {
  611. color: #fdc33e;
  612. }
  613. .need-to-know {
  614. height: calc(100% - 20px);
  615. overflow: hidden;
  616. display: flex;
  617. flex-direction: column;
  618. padding: 10px 20px;
  619. white-space: pre-wrap;
  620. p {
  621. flex: 1;
  622. overflow-y: auto;
  623. }
  624. footer {
  625. flex: 0 0 45px;
  626. margin-top: 4px;
  627. border-top: 1px solid #f7f8fa;
  628. }
  629. }
  630. </style>