index.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998
  1. <template>
  2. <view class="scan-page">
  3. <!-- 顶部导航栏 -->
  4. <view class="header-bg" :style="{ paddingTop: statusBarHeight + 'px' }">
  5. <view class="header-content">
  6. <view class="back-btn" @click="handleBack">
  7. <text class="back-icon">‹</text>
  8. </view>
  9. <text class="header-title">{{ t('scan.title') }}</text>
  10. <view class="placeholder"></view>
  11. </view>
  12. </view>
  13. <!-- 扫描区域 -->
  14. <view class="scan-area">
  15. <camera
  16. device-position="back"
  17. flash="off"
  18. class="camera"
  19. @error="handleCameraError"
  20. >
  21. </camera>
  22. <!-- 模式切换按钮 - 在底部操作栏上方 -->
  23. <view class="mode-switch-wrapper">
  24. <view class="mode-switch">
  25. <view
  26. class="mode-btn"
  27. :class="{ active: scanMode === 'single' }"
  28. @click="switchToSingle"
  29. >
  30. <text class="mode-text">{{ t('scan.singlePage') }}</text>
  31. </view>
  32. <view
  33. class="mode-btn"
  34. :class="{ active: scanMode === 'multiple' }"
  35. @click="switchToMultiple"
  36. >
  37. <text class="mode-text">{{ t('scan.multiplePage') }}</text>
  38. </view>
  39. </view>
  40. </view>
  41. <!-- 多页模式缩略图列表 -->
  42. <view v-if="scanMode === 'multiple' && pdfDataList.length > 0" class="thumbnail-list">
  43. <scroll-view scroll-x class="thumbnail-scroll">
  44. <view class="thumbnail-wrapper">
  45. <view
  46. v-for="(item, index) in pdfDataList"
  47. :key="index"
  48. class="thumbnail-item"
  49. @click="handleViewAllPdf"
  50. >
  51. <view class="pdf-thumbnail-icon">
  52. <image src="/static/icon/pdf.svg" mode="aspectFit" class="pdf-icon-img" />
  53. </view>
  54. <view class="thumbnail-delete" @click.stop="handleDeletePdf(index)">
  55. <text class="delete-icon">×</text>
  56. </view>
  57. <view class="thumbnail-index">{{ index + 1 }}</view>
  58. </view>
  59. </view>
  60. </scroll-view>
  61. </view>
  62. <!-- 上传按钮(多页模式且有图片时显示�?-->
  63. <view
  64. v-if="scanMode === 'multiple' && imageBase64List.length > 0"
  65. class="upload-btn"
  66. @click="handleMultipleUpload"
  67. >
  68. <text class="upload-text">{{ t('scan.upload') }}</text>
  69. </view>
  70. <!-- PDF缩略图预览(右下角) -->
  71. <view v-if="showPdfThumbnail" class="pdf-thumbnail" @click="handleOpenPdf">
  72. <view class="pdf-close" @click.stop="handleClosePdf">
  73. <text class="close-icon">×</text>
  74. </view>
  75. <image class="pdf-icon" src="/static/icon/pdf.svg" mode="aspectFit" />
  76. <text class="pdf-text">PDF</text>
  77. </view>
  78. <!-- 底部操作按钮 -->
  79. <view class="action-buttons">
  80. <view class="action-btn" @click="handleSelectImage">
  81. <image class="btn-icon" src="/static/pages/scan/import-pic.svg" mode="aspectFit" />
  82. <text class="btn-text">{{ t('scan.importImage') }}</text>
  83. </view>
  84. <view class="capture-btn" @click="handleCapture">
  85. <view class="capture-inner"></view>
  86. </view>
  87. <view class="action-btn" @click="handleSelectFile">
  88. <image class="btn-icon" src="/static/pages/scan/import-doc.svg" mode="aspectFit" />
  89. <text class="btn-text">{{ t('scan.importDocument') }}</text>
  90. </view>
  91. </view>
  92. </view>
  93. </view>
  94. </template>
  95. <script setup>
  96. import { ref, onMounted, onUnmounted } from 'vue'
  97. import { useI18n } from 'vue-i18n'
  98. import { scanUpload } from '@/apis/scan'
  99. const { t } = useI18n()
  100. // 状态栏高度
  101. const statusBarHeight = ref(0)
  102. // 扫描模式:single-单页,multiple-多页
  103. const scanMode = ref('single')
  104. // 图片base64数组(用于多页模式)
  105. const imageBase64List = ref([])
  106. // 图片临时路径数组(用于显示缩略图�?
  107. const imageTempPaths = ref([])
  108. // PDF数据数组(多页模式,包含路径和base64�?
  109. const pdfDataList = ref([])
  110. // 缓存的ossId
  111. const cachedOssId = ref(null)
  112. // PDF文件路径(用于预览)
  113. const pdfFilePath = ref('')
  114. // 是否显示PDF缩略图
  115. const showPdfThumbnail = ref(false)
  116. // 暂存扫描的PDF base64(用于单页模式)
  117. const scannedPdfBase64 = ref('')
  118. // 相机上下文
  119. let cameraContext = null
  120. onMounted(() => {
  121. // 获取系统信息
  122. const windowInfo = uni.getWindowInfo()
  123. statusBarHeight.value = windowInfo.statusBarHeight || 0
  124. // 初始化相机上下文
  125. cameraContext = uni.createCameraContext()
  126. })
  127. onUnmounted(() => {
  128. // 清理资源
  129. })
  130. // 返回上一页
  131. const handleBack = () => {
  132. uni.reLaunch({
  133. url: '/pages/home/index'
  134. })
  135. }
  136. // 切换扫描模式
  137. const toggleScanMode = () => {
  138. scanMode.value = scanMode.value === 'single' ? 'multiple' : 'single'
  139. uni.showToast({
  140. title: scanMode.value === 'single' ? '已切换到单页模式' : '已切换到多页模式',
  141. icon: 'none',
  142. duration: 1500
  143. })
  144. }
  145. // 切换到单页模式
  146. const switchToSingle = () => {
  147. if (scanMode.value === 'single') return
  148. scanMode.value = 'single'
  149. uni.showToast({
  150. title: t('scan.switchToSingle'),
  151. icon: 'none',
  152. duration: 1500
  153. })
  154. }
  155. // 切换到多页模式
  156. const switchToMultiple = () => {
  157. if (scanMode.value === 'multiple') return
  158. scanMode.value = 'multiple'
  159. uni.showToast({
  160. title: t('scan.switchToMultiple'),
  161. icon: 'none',
  162. duration: 1500
  163. })
  164. }
  165. // 相机错误处理
  166. const handleCameraError = (e) => {
  167. console.error('相机错误:', e)
  168. uni.showToast({
  169. title: t('scan.cameraError'),
  170. icon: 'none'
  171. })
  172. }
  173. // 拍照
  174. const handleCapture = () => {
  175. if (!cameraContext) return
  176. cameraContext.takePhoto({
  177. quality: 'high',
  178. success: async (res) => {
  179. try {
  180. uni.showLoading({
  181. title: t('scan.processing'),
  182. mask: true
  183. })
  184. // 将图片转换为base64
  185. const base64Data = await imageToBase64(res.tempImagePath)
  186. if (scanMode.value === 'single') {
  187. // 单页模式:直接上传
  188. await uploadSingleImage(base64Data)
  189. } else {
  190. // 多页模式:立即上传并扫描
  191. await uploadAndScanImage(base64Data)
  192. }
  193. } catch (error) {
  194. uni.hideLoading()
  195. console.error('处理图片失败:', error)
  196. uni.showToast({
  197. title: t('scan.processFailed'),
  198. icon: 'none'
  199. })
  200. }
  201. },
  202. fail: (err) => {
  203. console.error('拍照失败:', err)
  204. uni.showToast({
  205. title: t('scan.captureFailed'),
  206. icon: 'none'
  207. })
  208. }
  209. })
  210. }
  211. // 导入图片
  212. const handleSelectImage = async () => {
  213. uni.chooseImage({
  214. count: 1,
  215. sizeType: ['original', 'compressed'],
  216. sourceType: ['album'],
  217. success: async (res) => {
  218. try {
  219. uni.showLoading({
  220. title: t('scan.processing'),
  221. mask: true
  222. })
  223. // 将图片转换为base64
  224. const base64Data = await imageToBase64(res.tempFilePaths[0])
  225. if (scanMode.value === 'single') {
  226. // 单页模式:直接上传
  227. await uploadSingleImage(base64Data)
  228. } else {
  229. // 多页模式:立即上传并扫描
  230. await uploadAndScanImage(base64Data)
  231. }
  232. } catch (error) {
  233. uni.hideLoading()
  234. console.error('处理图片失败:', error)
  235. uni.showToast({
  236. title: t('scan.processFailed'),
  237. icon: 'none'
  238. })
  239. }
  240. }
  241. })
  242. }
  243. // 导入文档
  244. const handleSelectFile = () => {
  245. uni.chooseMessageFile({
  246. count: 1,
  247. type: 'file',
  248. extension: ['.pdf'],
  249. success: async (res) => {
  250. try {
  251. uni.showLoading({
  252. title: t('scan.processing'),
  253. mask: true
  254. })
  255. const file = res.tempFiles[0]
  256. // 将文件转换为base64
  257. const base64Data = await fileToBase64(file.path)
  258. if (scanMode.value === 'single') {
  259. // 单页模式:直接处理文档
  260. await handleDocumentUpload(base64Data)
  261. } else {
  262. // 多页模式:添加到列表
  263. await handleDocumentInMultipleMode(base64Data, file.name)
  264. }
  265. } catch (error) {
  266. uni.hideLoading()
  267. console.error('处理文档失败:', error)
  268. uni.showToast({
  269. title: t('scan.processFailed'),
  270. icon: 'none'
  271. })
  272. }
  273. },
  274. fail: (err) => {
  275. console.error('选择文档失败:', err)
  276. uni.showToast({
  277. title: t('scan.selectDocumentFailed'),
  278. icon: 'none'
  279. })
  280. }
  281. })
  282. }
  283. // 将文件转换为base64
  284. const fileToBase64 = (filePath) => {
  285. return new Promise((resolve, reject) => {
  286. uni.getFileSystemManager().readFile({
  287. filePath: filePath,
  288. encoding: 'base64',
  289. success: (res) => {
  290. resolve(res.data)
  291. },
  292. fail: (err) => {
  293. reject(err)
  294. }
  295. })
  296. })
  297. }
  298. // 单页模式处理文档
  299. const handleDocumentUpload = async (base64Data) => {
  300. try {
  301. // 直接将文档base64存储到全局数据中(作为数组)
  302. getApp().globalData.scannedFileBase64List = [base64Data]
  303. uni.hideLoading()
  304. uni.showToast({
  305. title: t('scan.documentImportSuccess'),
  306. icon: 'success',
  307. duration: 1500
  308. })
  309. // 延迟跳转到文件选择页面
  310. setTimeout(() => {
  311. uni.navigateTo({
  312. url: '/pages/scan/fileSelect/index'
  313. })
  314. }, 1500)
  315. } catch (error) {
  316. uni.hideLoading()
  317. console.error('处理文档失败:', error)
  318. uni.showToast({
  319. title: error.message || t('scan.processFailed'),
  320. icon: 'none'
  321. })
  322. }
  323. }
  324. // 多页模式处理文档
  325. const handleDocumentInMultipleMode = async (base64Data, fileName) => {
  326. try {
  327. // 生成临时文件路径用于显示
  328. const tempPath = await base64ToTempFile(base64Data)
  329. // 添加到PDF数据列表
  330. pdfDataList.value.push({
  331. path: tempPath,
  332. base64: base64Data,
  333. name: fileName || `文档_${Date.now()}`
  334. })
  335. uni.hideLoading()
  336. uni.showToast({
  337. title: t('scan.added', { count: pdfDataList.value.length }),
  338. icon: 'success',
  339. duration: 1500
  340. })
  341. } catch (error) {
  342. uni.hideLoading()
  343. console.error('处理文档失败:', error)
  344. uni.showToast({
  345. title: error.message || t('scan.processFailed'),
  346. icon: 'none'
  347. })
  348. }
  349. }
  350. // 将图片转换为base64
  351. const imageToBase64 = (filePath) => {
  352. return new Promise((resolve, reject) => {
  353. uni.getFileSystemManager().readFile({
  354. filePath: filePath,
  355. encoding: 'base64',
  356. success: (res) => {
  357. resolve(res.data)
  358. },
  359. fail: (err) => {
  360. reject(err)
  361. }
  362. })
  363. })
  364. }
  365. // 单页模式上传
  366. const uploadSingleImage = async (base64Data) => {
  367. try {
  368. // 调用上传接口(单个文件)
  369. const response = await scanUpload({
  370. file: base64Data
  371. })
  372. uni.hideLoading()
  373. if (response.code === 200 && response.data) {
  374. // 缓存ossId
  375. cachedOssId.value = response.data.ossId
  376. // 暂存扫描的PDF base64
  377. if (response.data.fileBase64) {
  378. scannedPdfBase64.value = response.data.fileBase64
  379. // 将fileBase64存储到全局数据中(作为数组)
  380. getApp().globalData.scannedFileBase64List = [response.data.fileBase64]
  381. // 跳转到文件选择页面
  382. uni.navigateTo({
  383. url: '/pages/scan/fileSelect/index'
  384. })
  385. } else {
  386. uni.showToast({
  387. title: t('scan.uploadSuccess'),
  388. icon: 'success',
  389. duration: 2000
  390. })
  391. }
  392. } else {
  393. throw new Error(response.msg || t('scan.uploadFailed'))
  394. }
  395. } catch (error) {
  396. uni.hideLoading()
  397. console.error('上传失败:', error)
  398. uni.showToast({
  399. title: error.message || t('scan.uploadFailed'),
  400. icon: 'none'
  401. })
  402. }
  403. }
  404. // 多页模式批量上传
  405. const handleMultipleUpload = async () => {
  406. if (imageBase64List.value.length === 0) {
  407. uni.showToast({
  408. title: t('scan.addImage'),
  409. icon: 'none'
  410. })
  411. return
  412. }
  413. try {
  414. uni.showLoading({
  415. title: t('scan.uploading'),
  416. mask: true
  417. })
  418. // 调用上传接口
  419. const response = await scanUpload({
  420. files: imageBase64List.value
  421. })
  422. uni.hideLoading()
  423. if (response.code === 200 && response.data) {
  424. // 缓存ossId
  425. cachedOssId.value = response.data.ossId
  426. // 清空数组
  427. imageBase64List.value = []
  428. imageTempPaths.value = []
  429. // 处理PDF预览
  430. if (response.data.fileBase64) {
  431. await handlePdfPreview(response.data.fileBase64)
  432. } else {
  433. uni.showToast({
  434. title: t('scan.uploadSuccess'),
  435. icon: 'success',
  436. duration: 2000
  437. })
  438. }
  439. } else {
  440. throw new Error(response.msg || t('scan.uploadFailed'))
  441. }
  442. } catch (error) {
  443. uni.hideLoading()
  444. console.error('上传失败:', error)
  445. uni.showToast({
  446. title: error.message || t('scan.uploadFailed'),
  447. icon: 'none'
  448. })
  449. }
  450. }
  451. // 多页模式:上传并扫描单张图片
  452. const uploadAndScanImage = async (base64Data) => {
  453. try {
  454. // 调用上传接口(单个文件)
  455. const response = await scanUpload({
  456. file: base64Data
  457. })
  458. uni.hideLoading()
  459. if (response.code === 200 && response.data && response.data.fileBase64) {
  460. // 将PDF转换为临时文件
  461. const pdfPath = await base64ToTempFile(response.data.fileBase64)
  462. // 生成文件名(使用时间戳或从服务器返回的文件名
  463. const fileName = response.data.fileName || response.data.originalName || `扫描文档_${Date.now()}`
  464. // 添加到PDF数据列表(包含路径、base64和文件名
  465. pdfDataList.value.push({
  466. path: pdfPath,
  467. base64: response.data.fileBase64,
  468. name: fileName
  469. })
  470. uni.showToast({
  471. title: t('scan.scanned', { count: pdfDataList.value.length }),
  472. icon: 'success',
  473. duration: 1500
  474. })
  475. } else {
  476. throw new Error(response.msg || '扫描失败')
  477. }
  478. } catch (error) {
  479. uni.hideLoading()
  480. console.error('扫描失败:', error)
  481. uni.showToast({
  482. title: t('scan.scanFailed'),
  483. icon: 'none'
  484. })
  485. }
  486. }
  487. // 删除指定图片
  488. const handleDeleteImage = (index) => {
  489. imageBase64List.value.splice(index, 1)
  490. imageTempPaths.value.splice(index, 1)
  491. uni.showToast({
  492. title: t('scan.deleted'),
  493. icon: 'success',
  494. duration: 1000
  495. })
  496. }
  497. // 删除指定PDF
  498. const handleDeletePdf = (index) => {
  499. pdfDataList.value.splice(index, 1)
  500. uni.showToast({
  501. title: t('scan.deleted'),
  502. icon: 'success',
  503. duration: 1000
  504. })
  505. }
  506. // 查看所有PDF
  507. const handleViewAllPdf = () => {
  508. if (pdfDataList.value.length === 0) {
  509. uni.showToast({
  510. title: t('scan.noPdf'),
  511. icon: 'none'
  512. })
  513. return
  514. }
  515. // 使用全局数据存储PDF数据(包含路径和base64�?
  516. getApp().globalData.pdfData = pdfDataList.value
  517. uni.navigateTo({
  518. url: '/pages/scan/pdfViewer/index'
  519. })
  520. }
  521. // 处理PDF预览
  522. const handlePdfPreview = async (base64Data) => {
  523. try {
  524. // 将base64转换为临时文件
  525. const filePath = await base64ToTempFile(base64Data)
  526. pdfFilePath.value = filePath
  527. showPdfThumbnail.value = true
  528. uni.showToast({
  529. title: t('scan.uploadSuccess'),
  530. icon: 'success',
  531. duration: 2000
  532. })
  533. } catch (error) {
  534. console.error('处理PDF失败:', error)
  535. uni.showToast({
  536. title: t('scan.processPdfFailed'),
  537. icon: 'none'
  538. })
  539. }
  540. }
  541. // 将base64转换为临时文件
  542. const base64ToTempFile = (base64Data) => {
  543. return new Promise((resolve, reject) => {
  544. const fs = uni.getFileSystemManager()
  545. const fileName = `scan_${Date.now()}.pdf`
  546. const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`
  547. fs.writeFile({
  548. filePath: filePath,
  549. data: base64Data,
  550. encoding: 'base64',
  551. success: () => {
  552. resolve(filePath)
  553. },
  554. fail: (err) => {
  555. reject(err)
  556. }
  557. })
  558. })
  559. }
  560. // 打开PDF预览
  561. const handleOpenPdf = () => {
  562. if (!pdfFilePath.value) return
  563. uni.openDocument({
  564. filePath: pdfFilePath.value,
  565. fileType: 'pdf',
  566. showMenu: true,
  567. success: () => {
  568. // 预览成功
  569. },
  570. fail: (err) => {
  571. console.error('打开文档失败:', err)
  572. uni.showToast({
  573. title: t('scan.openDocumentFailed'),
  574. icon: 'none'
  575. })
  576. }
  577. })
  578. }
  579. // 关闭PDF缩略图
  580. const handleClosePdf = () => {
  581. showPdfThumbnail.value = false
  582. pdfFilePath.value = ''
  583. }
  584. </script>
  585. <style lang="scss" scoped>
  586. .scan-page {
  587. width: 100%;
  588. height: 100vh;
  589. display: flex;
  590. flex-direction: column;
  591. background-color: #000000;
  592. // 顶部导航栏
  593. .header-bg {
  594. background: linear-gradient(180deg, #1ec9c9 0%, #1eb8b8 100%);
  595. position: relative;
  596. z-index: 100;
  597. .header-content {
  598. height: 88rpx;
  599. display: flex;
  600. align-items: center;
  601. justify-content: space-between;
  602. padding: 0 24rpx;
  603. .back-btn {
  604. width: 60rpx;
  605. height: 60rpx;
  606. display: flex;
  607. align-items: center;
  608. justify-content: flex-start;
  609. .back-icon {
  610. font-size: 56rpx;
  611. color: #ffffff;
  612. font-weight: 300;
  613. }
  614. }
  615. .header-title {
  616. flex: 1;
  617. text-align: center;
  618. font-size: 32rpx;
  619. font-weight: 600;
  620. color: #ffffff;
  621. }
  622. .placeholder {
  623. width: 60rpx;
  624. }
  625. }
  626. }
  627. // 扫描区域
  628. .scan-area {
  629. flex: 1;
  630. position: relative;
  631. display: flex;
  632. flex-direction: column;
  633. .camera {
  634. flex: 1;
  635. width: 100%;
  636. position: relative;
  637. }
  638. .camera-placeholder {
  639. flex: 1;
  640. width: 100%;
  641. background: #000000;
  642. }
  643. // 模式切换按钮 - 在底部操作栏上方
  644. .mode-switch-wrapper {
  645. position: absolute;
  646. bottom: 280rpx;
  647. left: 50%;
  648. transform: translateX(-50%);
  649. z-index: 10;
  650. .mode-switch {
  651. display: flex;
  652. background: rgba(0, 0, 0, 0.6);
  653. border-radius: 40rpx;
  654. padding: 6rpx;
  655. backdrop-filter: blur(10rpx);
  656. .mode-btn {
  657. padding: 12rpx 32rpx;
  658. border-radius: 34rpx;
  659. transition: all 0.3s;
  660. .mode-text {
  661. font-size: 26rpx;
  662. color: rgba(255, 255, 255, 0.6);
  663. font-weight: 500;
  664. transition: all 0.3s;
  665. }
  666. &.active {
  667. background: rgba(255, 255, 255, 0.9);
  668. .mode-text {
  669. color: #333333;
  670. font-weight: 600;
  671. }
  672. }
  673. &:active {
  674. transform: scale(0.95);
  675. }
  676. }
  677. }
  678. }
  679. // 多页模式缩略图列�?
  680. .thumbnail-list {
  681. position: absolute;
  682. bottom: 220rpx;
  683. left: 0;
  684. right: 0;
  685. height: 160rpx;
  686. background: rgba(0, 0, 0, 0.6);
  687. backdrop-filter: blur(10rpx);
  688. .thumbnail-scroll {
  689. height: 100%;
  690. white-space: nowrap;
  691. .thumbnail-wrapper {
  692. display: inline-flex;
  693. padding: 20rpx;
  694. gap: 16rpx;
  695. .thumbnail-item {
  696. position: relative;
  697. width: 120rpx;
  698. height: 120rpx;
  699. border-radius: 12rpx;
  700. overflow: hidden;
  701. background: #ffffff;
  702. border: 2rpx solid #e0e0e0;
  703. .thumbnail-image {
  704. width: 100%;
  705. height: 100%;
  706. }
  707. .pdf-thumbnail-icon {
  708. width: 100%;
  709. height: 100%;
  710. display: flex;
  711. align-items: center;
  712. justify-content: center;
  713. background: #f5f5f5;
  714. .pdf-icon-img {
  715. width: 60rpx;
  716. height: 60rpx;
  717. }
  718. }
  719. .thumbnail-delete {
  720. position: absolute;
  721. top: 4rpx;
  722. right: 4rpx;
  723. width: 36rpx;
  724. height: 36rpx;
  725. border-radius: 50%;
  726. background: rgba(0, 0, 0, 0.7);
  727. display: flex;
  728. align-items: center;
  729. justify-content: center;
  730. .delete-icon {
  731. font-size: 32rpx;
  732. color: #ffffff;
  733. line-height: 1;
  734. }
  735. }
  736. .thumbnail-index {
  737. position: absolute;
  738. bottom: 4rpx;
  739. left: 4rpx;
  740. padding: 2rpx 8rpx;
  741. background: rgba(0, 0, 0, 0.7);
  742. border-radius: 8rpx;
  743. font-size: 20rpx;
  744. color: #ffffff;
  745. }
  746. }
  747. }
  748. }
  749. }
  750. // 上传按钮
  751. .upload-btn {
  752. position: absolute;
  753. bottom: 240rpx;
  754. right: 32rpx;
  755. width: 120rpx;
  756. height: 80rpx;
  757. background: #1ec9c9;
  758. border-radius: 40rpx;
  759. display: flex;
  760. align-items: center;
  761. justify-content: center;
  762. box-shadow: 0 4rpx 16rpx rgba(30, 201, 201, 0.4);
  763. z-index: 20;
  764. &:active {
  765. transform: scale(0.95);
  766. }
  767. .upload-text {
  768. font-size: 28rpx;
  769. font-weight: 600;
  770. color: #ffffff;
  771. }
  772. }
  773. // PDF缩略图
  774. .pdf-thumbnail {
  775. position: absolute;
  776. bottom: 240rpx;
  777. right: 32rpx;
  778. width: 140rpx;
  779. height: 140rpx;
  780. background: #ffffff;
  781. border-radius: 16rpx;
  782. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);
  783. display: flex;
  784. flex-direction: column;
  785. align-items: center;
  786. justify-content: center;
  787. z-index: 20;
  788. &:active {
  789. transform: scale(0.95);
  790. }
  791. .pdf-close {
  792. position: absolute;
  793. top: -8rpx;
  794. right: -8rpx;
  795. width: 40rpx;
  796. height: 40rpx;
  797. border-radius: 50%;
  798. background: #ff4444;
  799. display: flex;
  800. align-items: center;
  801. justify-content: center;
  802. box-shadow: 0 2rpx 8rpx rgba(255, 68, 68, 0.3);
  803. .close-icon {
  804. font-size: 36rpx;
  805. color: #ffffff;
  806. line-height: 1;
  807. font-weight: 300;
  808. }
  809. }
  810. .pdf-icon {
  811. width: 60rpx;
  812. height: 60rpx;
  813. margin-bottom: 8rpx;
  814. }
  815. .pdf-text {
  816. font-size: 24rpx;
  817. color: #666666;
  818. font-weight: 500;
  819. }
  820. }
  821. // 底部操作按钮
  822. .action-buttons {
  823. position: absolute;
  824. bottom: 0;
  825. left: 0;
  826. right: 0;
  827. background: #ffffff;
  828. padding: 32rpx 40rpx;
  829. padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
  830. display: flex;
  831. align-items: center;
  832. justify-content: space-between;
  833. .action-btn {
  834. display: flex;
  835. flex-direction: column;
  836. align-items: center;
  837. gap: 12rpx;
  838. .btn-icon {
  839. width: 48rpx;
  840. height: 48rpx;
  841. }
  842. .btn-text {
  843. font-size: 24rpx;
  844. color: #666666;
  845. }
  846. }
  847. .capture-btn {
  848. width: 120rpx;
  849. height: 120rpx;
  850. border-radius: 50%;
  851. background: #1ec9c9;
  852. display: flex;
  853. align-items: center;
  854. justify-content: center;
  855. box-shadow: 0 4rpx 16rpx rgba(30, 201, 201, 0.4);
  856. &:active {
  857. transform: scale(0.95);
  858. }
  859. .capture-inner {
  860. width: 100rpx;
  861. height: 100rpx;
  862. border-radius: 50%;
  863. background: #ffffff;
  864. }
  865. }
  866. }
  867. }
  868. }
  869. </style>