cwg-file-picker.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851
  1. <template>
  2. <view class="common-file-uploader" :class="customClass">
  3. <!-- 编辑状态:显示可上传的文件选择器 -->
  4. <view v-if="editable" class="upload-wrapper">
  5. <uni-file-picker
  6. :limit="multiple ? limit : 1"
  7. :title="title"
  8. :file-mediatype="fileMediatype"
  9. :mode="mode"
  10. :auto-upload="false"
  11. :value="fileList"
  12. :disabled="disabled"
  13. :readonly="readonly"
  14. :image-styles="imageStyles"
  15. :list-styles="listStyles"
  16. :del-icon="showPreviewDelete"
  17. :disablePreview="disablePreview"
  18. :canChoose="canChoose"
  19. @select="handleSelect"
  20. @delete="handleDelete">
  21. <!-- 自定义上传按钮(单张图片且已有图片时显示替换) -->
  22. <template v-if="$slots.default || showCustomButton">
  23. <slot name="default">
  24. <view class="custom-upload-btn" :class="{ 'replace-btn': !multiple && fileList.length > 0 }"
  25. :style="customButtonStyle">
  26. <template v-if="!multiple && fileList.length > 0">
  27. <text class="replace-icon">↻</text>
  28. <text class="tip">{{ replaceText || t('Common.Replace') }}</text>
  29. </template>
  30. <template v-else>
  31. <text class="plus">+</text>
  32. <!-- <text class="tip">{{ uploadText || t('common.upload') }}</text>-->
  33. </template>
  34. </view>
  35. </slot>
  36. </template>
  37. </uni-file-picker>
  38. <!-- 上传进度 -->
  39. <view v-if="showProgress && uploadProgress > 0 && uploadProgress < 100" class="upload-progress"
  40. :style="progressStyle">
  41. <view class="progress-bar" :style="{ width: uploadProgress + '%' }"></view>
  42. <text class="progress-text">{{ uploadProgress }}%</text>
  43. </view>
  44. <!-- 上传错误提示 -->
  45. <view v-if="uploadError && showError" class="upload-error">
  46. <text class="error-text">{{ uploadError }}</text>
  47. </view>
  48. </view>
  49. <!-- 非编辑状态:显示图片预览 -->
  50. <view v-else class="image-preview" :class="previewClass">
  51. <!-- 单张图片预览 -->
  52. <template v-if="!multiple">
  53. <view class="single-preview">
  54. <image v-if="getImageUrl(modelValue)" :src="getImageUrl(modelValue)" :mode="imageMode"
  55. class="preview-image" :style="previewImageStyle" @tap="handlePreview(getImageUrl(modelValue))"
  56. @error="handleImageError" :lazy-load="true" />
  57. <view v-else class="no-image" :style="noImageStyle">
  58. <text :style="noImageTextStyle">{{ noImageText || t('Common.NoImage') }}</text>
  59. </view>
  60. </view>
  61. </template>
  62. <!-- 多张图片预览 -->
  63. <template v-else>
  64. <view v-if="getFileList(modelValue).length" class="image-list" :style="listContainerStyle">
  65. <view v-for="(file, idx) in getFileList(modelValue)" :key="idx" class="image-item"
  66. :style="imageItemStyle">
  67. <image :src="getFileUrl(file)" :mode="imageMode" class="preview-image"
  68. :style="previewImageStyle" @tap="handlePreview(getFilePath(file))" @error="handleImageError"
  69. :lazy-load="true" />
  70. <!-- 预览时的删除按钮(如果有权限) -->
  71. <view v-if="showPreviewDelete && canDelete" class="preview-delete-btn" :style="deleteBtnStyle"
  72. @tap.stop="handlePreviewDelete(file, idx)">
  73. <text class="delete-icon">×</text>
  74. </view>
  75. </view>
  76. </view>
  77. <view v-else class="no-image" :style="noImageStyle">
  78. <text :style="noImageTextStyle">{{ noImageText || t('Common.NoImage') }}</text>
  79. </view>
  80. </template>
  81. </view>
  82. </view>
  83. </template>
  84. <script setup>
  85. import { computed, ref, watch } from 'vue'
  86. import { useI18n } from 'vue-i18n'
  87. import config from '@/config';
  88. import { userToken } from '@/composables/config'
  89. const { t } = useI18n()
  90. const props = defineProps({
  91. // 基础配置
  92. modelValue: {
  93. type: [String, Array, Object],
  94. default: null
  95. },
  96. editable: {
  97. type: Boolean,
  98. default: false
  99. },
  100. multiple: {
  101. type: Boolean,
  102. default: false
  103. },
  104. limit: {
  105. type: Number,
  106. default: 1
  107. },
  108. // 上传配置
  109. title: {
  110. type: String,
  111. default: ''
  112. },
  113. fileMediatype: {
  114. type: String,
  115. default: 'image'
  116. },
  117. mode: {
  118. type: String,
  119. default: 'grid'
  120. },
  121. disabled: {
  122. type: Boolean,
  123. default: false
  124. },
  125. disablePreview: {
  126. type: Boolean,
  127. default: false
  128. },
  129. canChoose: {
  130. type: Boolean,
  131. default: false
  132. },
  133. readonly: {
  134. type: Boolean,
  135. default: false
  136. },
  137. // 上传API配置
  138. uploadUrl: {
  139. type: String,
  140. required: true,
  141. default: '/custom/bank/upload'
  142. },
  143. uploadHeaders: {
  144. type: Object,
  145. default: () => ({})
  146. },
  147. uploadName: {
  148. type: String,
  149. default: 'file'
  150. },
  151. uploadData: {
  152. type: Object,
  153. default: () => ({})
  154. },
  155. // 图片服务配置
  156. baseUrl: {
  157. type: String,
  158. default: config.Host80
  159. },
  160. imagePathPrefix: {
  161. type: String,
  162. default: ''
  163. },
  164. // 样式配置
  165. customClass: {
  166. type: String,
  167. default: ''
  168. },
  169. previewClass: {
  170. type: String,
  171. default: ''
  172. },
  173. // 尺寸配置
  174. imageWidth: {
  175. type: [String, Number],
  176. default: 160
  177. },
  178. imageHeight: {
  179. type: [String, Number],
  180. default: 160
  181. },
  182. imageGap: {
  183. type: [String, Number],
  184. default: 16
  185. },
  186. imageBorderRadius: {
  187. type: [String, Number],
  188. default: 8
  189. },
  190. // 图片配置
  191. imageMode: {
  192. type: String,
  193. default: 'aspectFill'
  194. },
  195. // 上传按钮配置
  196. showCustomButton: {
  197. type: Boolean,
  198. default: true
  199. },
  200. uploadText: {
  201. type: String,
  202. default: ''
  203. },
  204. replaceText: {
  205. type: String,
  206. default: ''
  207. },
  208. // 暂无图片配置
  209. noImageText: {
  210. type: String,
  211. default: ''
  212. },
  213. // 预览配置
  214. showPreviewDelete: {
  215. type: Boolean,
  216. default: false
  217. },
  218. canDelete: {
  219. type: Boolean,
  220. default: true
  221. },
  222. // 进度显示
  223. showProgress: {
  224. type: Boolean,
  225. default: true
  226. },
  227. // 是否自动上传
  228. autoUpload: {
  229. type: Boolean,
  230. default: true
  231. },
  232. // 响应数据处理函数
  233. responseHandler: {
  234. type: Function,
  235. default: (res) => {
  236. try {
  237. const data = typeof res === 'string' ? JSON.parse(res) : res
  238. return {
  239. success: data.code === 200,
  240. path: data.data?.path || data.data,
  241. message: data.msg || '上传成功',
  242. data: data
  243. }
  244. } catch (e) {
  245. return {
  246. success: false,
  247. path: null,
  248. message: '解析响应失败',
  249. data: null
  250. }
  251. }
  252. }
  253. },
  254. // uni-file-picker 的图片样式
  255. imageStyles: {
  256. type: Object,
  257. default: () => ({})
  258. },
  259. // uni-file-picker 的列表样式
  260. listStyles: {
  261. type: Object,
  262. default: () => ({})
  263. },
  264. // 自定义样式对象
  265. customStyles: {
  266. type: Object,
  267. default: () => ({})
  268. },
  269. // 展示错误信息
  270. showError: {
  271. type: Object,
  272. default: true
  273. }
  274. })
  275. const emit = defineEmits([
  276. 'update:modelValue',
  277. 'select',
  278. 'delete',
  279. 'progress',
  280. 'success',
  281. 'fail',
  282. 'preview',
  283. 'image-error',
  284. 'preview-delete',
  285. 'upload-start',
  286. 'upload-complete'
  287. ])
  288. // 上传进度
  289. const uploadProgress = ref(0)
  290. const uploadError = ref('')
  291. const isUploading = ref(false)
  292. // 文件列表(用于uni-file-picker回显)
  293. const fileList = computed(() => {
  294. return getFileValue(props.modelValue)
  295. })
  296. // 计算样式
  297. const imageWidthPx = computed(() => {
  298. return typeof props.imageWidth === 'number' ? px2rpx(props.imageWidth) : props.imageWidth
  299. })
  300. const imageHeightPx = computed(() => {
  301. return typeof props.imageHeight === 'number' ? px2rpx(props.imageHeight) : props.imageHeight
  302. })
  303. const imageGapPx = computed(() => {
  304. return typeof props.imageGap === 'number' ? px2rpx(props.imageGap) : props.imageGap
  305. })
  306. const imageBorderRadiusPx = computed(() => {
  307. return typeof props.imageBorderRadius === 'number' ? px2rpx(props.imageBorderRadius) : props.imageBorderRadius
  308. })
  309. // 自定义按钮样式
  310. const customButtonStyle = computed(() => ({
  311. width: imageWidthPx.value,
  312. height: imageHeightPx.value,
  313. ...props.customStyles.customButton
  314. }))
  315. // 预览图片样式
  316. const previewImageStyle = computed(() => ({
  317. width: imageWidthPx.value,
  318. height: imageHeightPx.value,
  319. borderRadius: imageBorderRadiusPx.value,
  320. ...props.customStyles.previewImage
  321. }))
  322. // 图片项样式
  323. const imageItemStyle = computed(() => ({
  324. width: imageWidthPx.value,
  325. height: imageHeightPx.value,
  326. borderRadius: imageBorderRadiusPx.value,
  327. ...props.customStyles.imageItem
  328. }))
  329. // 列表容器样式
  330. const listContainerStyle = computed(() => ({
  331. gap: imageGapPx.value,
  332. ...props.customStyles.listContainer
  333. }))
  334. // 暂无图片样式
  335. const noImageStyle = computed(() => ({
  336. width: imageWidthPx.value,
  337. height: imageHeightPx.value,
  338. borderRadius: imageBorderRadiusPx.value,
  339. ...props.customStyles.noImage
  340. }))
  341. const noImageTextStyle = computed(() => props.customStyles.noImageText || {})
  342. const progressStyle = computed(() => props.customStyles.progress || {})
  343. const deleteBtnStyle = computed(() => props.customStyles.deleteBtn || {})
  344. // 获取文件值,用于回显
  345. const getFileValue = (value) => {
  346. if (!value) return props.multiple ? [] : []
  347. if (props.multiple) {
  348. if (Array.isArray(value)) {
  349. return value.map(item => ({
  350. url: getFullUrl(item.path || item),
  351. path: item.path || item,
  352. name: item.name || '图片',
  353. uuid: item.id || item.path || Date.now() + Math.random()
  354. }))
  355. }
  356. return []
  357. } else {
  358. console.log(value, [{
  359. url: getFullUrl(value),
  360. path: value,
  361. name: '图片',
  362. uuid: value
  363. }], 121212121);
  364. if (typeof value === 'string') {
  365. return value ? [{
  366. url: getFullUrl(value),
  367. path: value,
  368. name: '图片',
  369. uuid: value
  370. }] : []
  371. }
  372. if (value && value.path) {
  373. return [{
  374. url: getFullUrl(value.path),
  375. path: value.path,
  376. name: value.name || '图片',
  377. uuid: value.id || value.path
  378. }]
  379. }
  380. return []
  381. }
  382. }
  383. // 获取文件列表
  384. const getFileList = (value) => {
  385. if (!value) return []
  386. if (props.multiple) {
  387. return Array.isArray(value) ? value : []
  388. } else {
  389. if (typeof value === 'string') {
  390. return value ? [{ path: value }] : []
  391. }
  392. return value ? [value] : []
  393. }
  394. }
  395. // 获取完整URL
  396. const getFullUrl = (path) => {
  397. if (!path) return ''
  398. if (path.startsWith('http')) return path
  399. return (props.baseUrl || config.Host80) + (props.imagePathPrefix || '') + path
  400. }
  401. // 获取图片URL
  402. const getImageUrl = (value) => {
  403. if (!value) return ''
  404. if (typeof value === 'string') return getFullUrl(value)
  405. if (value.url) return value.url
  406. if (value.path) return getFullUrl(value.path)
  407. return ''
  408. }
  409. // 获取文件路径
  410. const getFilePath = (file) => {
  411. if (!file) return ''
  412. if (typeof file === 'string') return file
  413. return file.path || file.url || ''
  414. }
  415. // 获取文件URL(用于预览)
  416. const getFileUrl = (file) => {
  417. if (!file) return ''
  418. if (typeof file === 'string') return getFullUrl(file)
  419. return getFullUrl(file.path || file.url || '')
  420. }
  421. // 处理选择文件
  422. const handleSelect = async (e) => {
  423. uploadError.value = ''
  424. emit('select', e)
  425. if (props.autoUpload) {
  426. await uploadFiles(e.tempFiles)
  427. }
  428. }
  429. // 上传文件
  430. const uploadFiles = async (tempFiles) => {
  431. if (!tempFiles || tempFiles.length === 0) return
  432. isUploading.value = true
  433. emit('upload-start')
  434. try {
  435. const uploadedPaths = []
  436. for (let i = 0; i < tempFiles.length; i++) {
  437. const file = tempFiles[i]
  438. uploadProgress.value = Math.round(((i) / tempFiles.length) * 100)
  439. const result = await uploadSingleFile(file)
  440. if (result.success) {
  441. uploadedPaths.push(result.path)
  442. } else {
  443. throw new Error(result.message)
  444. }
  445. uploadProgress.value = Math.round(((i + 1) / tempFiles.length) * 100)
  446. }
  447. // 更新modelValue
  448. if (props.multiple) {
  449. // 多张图片:合并新旧数据
  450. const currentValue = Array.isArray(props.modelValue) ? [...props.modelValue] : []
  451. const newValue = [...currentValue, ...uploadedPaths.map(path => ({ path }))]
  452. emit('update:modelValue', newValue)
  453. } else {
  454. // 单张图片:直接替换
  455. const newPath = uploadedPaths[0]
  456. console.log(3333, props.modelValue);
  457. emit('update:modelValue', newPath)
  458. }
  459. uploadProgress.value = 100
  460. setTimeout(() => {
  461. uploadProgress.value = 0
  462. }, 500)
  463. emit('upload-complete', { success: true, paths: uploadedPaths })
  464. } catch (error) {
  465. uploadError.value = error.message || '上传失败'
  466. uploadProgress.value = 0
  467. emit('upload-complete', { success: false, error: error.message })
  468. uni.showToast({
  469. title: error.message || '上传失败',
  470. icon: 'none'
  471. })
  472. } finally {
  473. isUploading.value = false
  474. }
  475. }
  476. // 上传单个文件
  477. const uploadSingleFile = (file) => {
  478. return new Promise((resolve, reject) => {
  479. const uploadTask = uni.uploadFile({
  480. url: config.Host80 + props.uploadUrl,
  481. filePath: file.path,
  482. name: props.uploadName,
  483. header: props.uploadHeaders,
  484. name: 'file',
  485. header: {
  486. 'Access-Token': userToken.value
  487. },
  488. formData: props.uploadData,
  489. success: (res) => {
  490. const result = props.responseHandler(res.data)
  491. if (result.success) {
  492. resolve(result)
  493. } else {
  494. reject(new Error(result.message))
  495. }
  496. },
  497. fail: (err) => {
  498. console.error('上传失败:', err)
  499. reject(new Error('网络错误,请重试'))
  500. }
  501. })
  502. // 监听上传进度
  503. uploadTask.onProgressUpdate((res) => {
  504. // 这里可以处理单个文件的上传进度
  505. console.log('单文件上传进度:', res.progress)
  506. })
  507. })
  508. }
  509. // 处理删除文件
  510. const handleDelete = (e) => {
  511. emit('delete', e)
  512. // 更新modelValue
  513. if (props.multiple) {
  514. const currentValue = Array.isArray(props.modelValue) ? [...props.modelValue] : []
  515. const newValue = currentValue.filter((_, index) => index !== e.index)
  516. emit('update:modelValue', newValue)
  517. } else {
  518. emit('update:modelValue', '')
  519. }
  520. }
  521. // 处理预览
  522. const handlePreview = (path) => {
  523. if (!path) return
  524. const urls = getFileList(props.modelValue).map(f => getFullUrl(getFilePath(f)))
  525. uni.previewImage({
  526. current: getFullUrl(path),
  527. urls: urls.length ? urls : [getFullUrl(path)]
  528. })
  529. emit('preview', path)
  530. }
  531. // 处理图片加载错误
  532. const handleImageError = (e) => {
  533. console.error('图片加载失败:', e)
  534. emit('image-error', e)
  535. }
  536. // 处理预览时的删除
  537. const handlePreviewDelete = (file, index) => {
  538. emit('preview-delete', { file, index })
  539. // 更新modelValue
  540. if (props.multiple) {
  541. const currentValue = Array.isArray(props.modelValue) ? [...props.modelValue] : []
  542. currentValue.splice(index, 1)
  543. emit('update:modelValue', currentValue)
  544. }
  545. }
  546. // 单位转换函数
  547. const px2rpx = (px) => {
  548. // 假设设计稿750px,1px = 2rpx
  549. return px * 2 + 'rpx'
  550. }
  551. // 对外暴露方法
  552. defineExpose({
  553. uploadFiles,
  554. clearError: () => uploadError.value = ''
  555. })
  556. </script>
  557. <style lang="scss" scoped>
  558. .common-file-uploader {
  559. width: 100%;
  560. .upload-wrapper {
  561. width: 100%;
  562. height: 100%;
  563. :deep(.uni-file-picker) {
  564. .file-picker__box {
  565. border: none;
  566. background: transparent;
  567. }
  568. .uni-file-picker__files{
  569. height: 100%;
  570. }
  571. .files-button{
  572. height: 100%;
  573. }
  574. .custom-upload-btn {
  575. width: 100%!important;
  576. height: 100%!important;
  577. display: flex;
  578. flex-direction: column;
  579. align-items: center;
  580. justify-content: center;
  581. &.replace-btn {
  582. background: rgba(0, 0, 0, 0.03);
  583. .replace-icon {
  584. font-size: 40px;
  585. color: #666;
  586. margin-bottom: 4px;
  587. transform: rotate(90deg);
  588. }
  589. .tip {
  590. color: #666;
  591. }
  592. &:hover {
  593. background: rgba(234, 32, 39, 0.05);
  594. .replace-icon,
  595. .tip {
  596. color: #ea2027;
  597. }
  598. }
  599. }
  600. .plus {
  601. font-size: 48px;
  602. color: #9ca3af;
  603. //line-height: 1;
  604. }
  605. .tip {
  606. font-size: 24px;
  607. color: #6b7280;
  608. }
  609. }
  610. .file-picker__box-list {
  611. display: flex;
  612. gap: v-bind(imageGapPx);
  613. flex-wrap: wrap;
  614. .file-picker__box-item {
  615. position: relative;
  616. border: 1px solid #e5e7eb;
  617. overflow: hidden;
  618. .file-picker__box-item-image {
  619. width: 100%;
  620. height: 100%;
  621. object-fit: cover;
  622. }
  623. .file-picker__box-item-close {
  624. position: absolute;
  625. top: 4px;
  626. right: 4px;
  627. width: 24px;
  628. height: 24px;
  629. background: rgba(0, 0, 0, 0.5);
  630. border-radius: 50%;
  631. display: flex;
  632. align-items: center;
  633. justify-content: center;
  634. &::before {
  635. content: '×';
  636. color: #fff;
  637. font-size: 28px;
  638. line-height: 1;
  639. }
  640. .close-icon {
  641. display: none;
  642. }
  643. }
  644. }
  645. }
  646. .file-picker__box-add {
  647. margin: 0;
  648. border: 2rpx dashed #d1d5db;
  649. background: #f9fafb;
  650. transition: all 0.3s;
  651. cursor: pointer;
  652. &:hover {
  653. border-color: #ea2027;
  654. background: #fef2f2;
  655. }
  656. .add-icon {
  657. display: none;
  658. }
  659. }
  660. .file-picker__box-progress {
  661. border-radius: v-bind(imageBorderRadiusPx);
  662. background: rgba(0, 0, 0, 0.5);
  663. color: #fff;
  664. }
  665. }
  666. .upload-progress {
  667. margin-top: 8px;
  668. padding: 8px;
  669. background: #f5f5f5;
  670. border-radius: 4px;
  671. position: relative;
  672. .progress-bar {
  673. height: 4px;
  674. background: #ea2027;
  675. border-radius: 2px;
  676. transition: width 0.3s;
  677. }
  678. .progress-text {
  679. position: absolute;
  680. right: 8px;
  681. top: 8px;
  682. font-size: 24px;
  683. color: #666;
  684. }
  685. }
  686. .upload-error {
  687. margin-top: 8px;
  688. padding: 8px;
  689. background: #fee2e2;
  690. border-radius: 4px;
  691. .error-text {
  692. color: #dc2626;
  693. font-size: 24px;
  694. }
  695. }
  696. }
  697. .image-preview {
  698. .image-list {
  699. display: flex;
  700. gap: v-bind(imageGapPx);
  701. flex-wrap: wrap;
  702. .image-item {
  703. position: relative;
  704. overflow: hidden;
  705. border: 1px solid #e5e7eb;
  706. .preview-image {
  707. width: 100%;
  708. height: 100%;
  709. cursor: pointer;
  710. }
  711. .preview-delete-btn {
  712. position: absolute;
  713. top: 4px;
  714. right: 4px;
  715. width: 24px;
  716. height: 24px;
  717. background: rgba(0, 0, 0, 0.5);
  718. border-radius: 50%;
  719. display: flex;
  720. align-items: center;
  721. justify-content: center;
  722. cursor: pointer;
  723. .delete-icon {
  724. color: #fff;
  725. font-size: 28px;
  726. line-height: 1;
  727. }
  728. }
  729. }
  730. }
  731. .single-preview {
  732. .preview-image {
  733. cursor: pointer;
  734. border: 1px solid #e5e7eb;
  735. }
  736. }
  737. .no-image {
  738. display: flex;
  739. align-items: center;
  740. justify-content: center;
  741. background: #f5f5f5;
  742. border: 1px solid #e5e7eb;
  743. text {
  744. color: #999;
  745. font-size: 24px;
  746. }
  747. }
  748. }
  749. }
  750. </style>