index.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999
  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">小程序扫描</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">单页</text>
  31. </view>
  32. <view
  33. class="mode-btn"
  34. :class="{ active: scanMode === 'multiple' }"
  35. @click="switchToMultiple"
  36. >
  37. <text class="mode-text">多页</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">上传</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">导入图片</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">导入文档</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: '已切换到单页模式',
  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: '已切换到多页模式',
  161. icon: 'none',
  162. duration: 1500
  163. })
  164. }
  165. // 相机错误处理
  166. const handleCameraError = (e) => {
  167. console.error('相机错误:', e)
  168. uni.showToast({
  169. title: '相机启动失败',
  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: '处理中..',
  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: '处理失败',
  198. icon: 'none'
  199. })
  200. }
  201. },
  202. fail: (err) => {
  203. console.error('拍照失败:', err)
  204. uni.showToast({
  205. title: '拍照失败',
  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: '处理�?..',
  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: '处理失败',
  237. icon: 'none'
  238. })
  239. }
  240. }
  241. })
  242. }
  243. // 导入文档
  244. // 导入文档
  245. const handleSelectFile = () => {
  246. uni.chooseMessageFile({
  247. count: 1,
  248. type: 'file',
  249. extension: ['.pdf', '.doc', '.docx'],
  250. success: async (res) => {
  251. try {
  252. uni.showLoading({
  253. title: '处理中...',
  254. mask: true
  255. })
  256. const file = res.tempFiles[0]
  257. // 将文件转换为base64
  258. const base64Data = await fileToBase64(file.path)
  259. if (scanMode.value === 'single') {
  260. // 单页模式:直接处理文档
  261. await handleDocumentUpload(base64Data)
  262. } else {
  263. // 多页模式:添加到列表
  264. await handleDocumentInMultipleMode(base64Data, file.name)
  265. }
  266. } catch (error) {
  267. uni.hideLoading()
  268. console.error('处理文档失败:', error)
  269. uni.showToast({
  270. title: '处理失败',
  271. icon: 'none'
  272. })
  273. }
  274. },
  275. fail: (err) => {
  276. console.error('选择文档失败:', err)
  277. uni.showToast({
  278. title: '选择文档失败',
  279. icon: 'none'
  280. })
  281. }
  282. })
  283. }
  284. // 将文件转换为base64
  285. const fileToBase64 = (filePath) => {
  286. return new Promise((resolve, reject) => {
  287. uni.getFileSystemManager().readFile({
  288. filePath: filePath,
  289. encoding: 'base64',
  290. success: (res) => {
  291. resolve(res.data)
  292. },
  293. fail: (err) => {
  294. reject(err)
  295. }
  296. })
  297. })
  298. }
  299. // 单页模式处理文档
  300. const handleDocumentUpload = async (base64Data) => {
  301. try {
  302. // 直接将文档base64存储到全局数据中(作为数组)
  303. getApp().globalData.scannedFileBase64List = [base64Data]
  304. uni.hideLoading()
  305. uni.showToast({
  306. title: '文档导入成功',
  307. icon: 'success',
  308. duration: 1500
  309. })
  310. // 延迟跳转到文件选择页面
  311. setTimeout(() => {
  312. uni.navigateTo({
  313. url: '/pages/scan/fileSelect/index'
  314. })
  315. }, 1500)
  316. } catch (error) {
  317. uni.hideLoading()
  318. console.error('处理文档失败:', error)
  319. uni.showToast({
  320. title: error.message || '处理失败',
  321. icon: 'none'
  322. })
  323. }
  324. }
  325. // 多页模式处理文档
  326. const handleDocumentInMultipleMode = async (base64Data, fileName) => {
  327. try {
  328. // 生成临时文件路径用于显示
  329. const tempPath = await base64ToTempFile(base64Data)
  330. // 添加到PDF数据列表
  331. pdfDataList.value.push({
  332. path: tempPath,
  333. base64: base64Data,
  334. name: fileName || `文档_${Date.now()}`
  335. })
  336. uni.hideLoading()
  337. uni.showToast({
  338. title: `已添加第${pdfDataList.value.length}个文档`,
  339. icon: 'success',
  340. duration: 1500
  341. })
  342. } catch (error) {
  343. uni.hideLoading()
  344. console.error('处理文档失败:', error)
  345. uni.showToast({
  346. title: error.message || '处理失败',
  347. icon: 'none'
  348. })
  349. }
  350. }
  351. // 将图片转换为base64
  352. const imageToBase64 = (filePath) => {
  353. return new Promise((resolve, reject) => {
  354. uni.getFileSystemManager().readFile({
  355. filePath: filePath,
  356. encoding: 'base64',
  357. success: (res) => {
  358. resolve(res.data)
  359. },
  360. fail: (err) => {
  361. reject(err)
  362. }
  363. })
  364. })
  365. }
  366. // 单页模式上传
  367. const uploadSingleImage = async (base64Data) => {
  368. try {
  369. // 调用上传接口(单个文件)
  370. const response = await scanUpload({
  371. file: base64Data
  372. })
  373. uni.hideLoading()
  374. if (response.code === 200 && response.data) {
  375. // 缓存ossId
  376. cachedOssId.value = response.data.ossId
  377. // 暂存扫描的PDF base64
  378. if (response.data.fileBase64) {
  379. scannedPdfBase64.value = response.data.fileBase64
  380. // 将fileBase64存储到全局数据中(作为数组)
  381. getApp().globalData.scannedFileBase64List = [response.data.fileBase64]
  382. // 跳转到文件选择页面
  383. uni.navigateTo({
  384. url: '/pages/scan/fileSelect/index'
  385. })
  386. } else {
  387. uni.showToast({
  388. title: '上传成功',
  389. icon: 'success',
  390. duration: 2000
  391. })
  392. }
  393. } else {
  394. throw new Error(response.msg || '上传失败')
  395. }
  396. } catch (error) {
  397. uni.hideLoading()
  398. console.error('上传失败:', error)
  399. uni.showToast({
  400. title: error.message || '上传失败',
  401. icon: 'none'
  402. })
  403. }
  404. }
  405. // 多页模式批量上传
  406. const handleMultipleUpload = async () => {
  407. if (imageBase64List.value.length === 0) {
  408. uni.showToast({
  409. title: '请先添加图片',
  410. icon: 'none'
  411. })
  412. return
  413. }
  414. try {
  415. uni.showLoading({
  416. title: '上传中...',
  417. mask: true
  418. })
  419. // 调用上传接口
  420. const response = await scanUpload({
  421. files: imageBase64List.value
  422. })
  423. uni.hideLoading()
  424. if (response.code === 200 && response.data) {
  425. // 缓存ossId
  426. cachedOssId.value = response.data.ossId
  427. // 清空数组
  428. imageBase64List.value = []
  429. imageTempPaths.value = []
  430. // 处理PDF预览
  431. if (response.data.fileBase64) {
  432. await handlePdfPreview(response.data.fileBase64)
  433. } else {
  434. uni.showToast({
  435. title: '上传成功',
  436. icon: 'success',
  437. duration: 2000
  438. })
  439. }
  440. } else {
  441. throw new Error(response.msg || '上传失败')
  442. }
  443. } catch (error) {
  444. uni.hideLoading()
  445. console.error('上传失败:', error)
  446. uni.showToast({
  447. title: error.message || '上传失败',
  448. icon: 'none'
  449. })
  450. }
  451. }
  452. // 多页模式:上传并扫描单张图片
  453. const uploadAndScanImage = async (base64Data) => {
  454. try {
  455. // 调用上传接口(单个文件)
  456. const response = await scanUpload({
  457. file: base64Data
  458. })
  459. uni.hideLoading()
  460. if (response.code === 200 && response.data && response.data.fileBase64) {
  461. // 将PDF转换为临时文件
  462. const pdfPath = await base64ToTempFile(response.data.fileBase64)
  463. // 生成文件名(使用时间戳或从服务器返回的文件名
  464. const fileName = response.data.fileName || response.data.originalName || `扫描文档_${Date.now()}`
  465. // 添加到PDF数据列表(包含路径、base64和文件名
  466. pdfDataList.value.push({
  467. path: pdfPath,
  468. base64: response.data.fileBase64,
  469. name: fileName
  470. })
  471. uni.showToast({
  472. title: `已扫描第${pdfDataList.value.length}页`,
  473. icon: 'success',
  474. duration: 1500
  475. })
  476. } else {
  477. throw new Error(response.msg || '扫描失败')
  478. }
  479. } catch (error) {
  480. uni.hideLoading()
  481. console.error('扫描失败:', error)
  482. uni.showToast({
  483. title: error.message || '扫描失败',
  484. icon: 'none'
  485. })
  486. }
  487. }
  488. // 删除指定图片
  489. const handleDeleteImage = (index) => {
  490. imageBase64List.value.splice(index, 1)
  491. imageTempPaths.value.splice(index, 1)
  492. uni.showToast({
  493. title: '已删除',
  494. icon: 'success',
  495. duration: 1000
  496. })
  497. }
  498. // 删除指定PDF
  499. const handleDeletePdf = (index) => {
  500. pdfDataList.value.splice(index, 1)
  501. uni.showToast({
  502. title: '已删除',
  503. icon: 'success',
  504. duration: 1000
  505. })
  506. }
  507. // 查看所有PDF
  508. const handleViewAllPdf = () => {
  509. if (pdfDataList.value.length === 0) {
  510. uni.showToast({
  511. title: '暂无PDF',
  512. icon: 'none'
  513. })
  514. return
  515. }
  516. // 使用全局数据存储PDF数据(包含路径和base64�?
  517. getApp().globalData.pdfData = pdfDataList.value
  518. uni.navigateTo({
  519. url: '/pages/scan/pdfViewer/index'
  520. })
  521. }
  522. // 处理PDF预览
  523. const handlePdfPreview = async (base64Data) => {
  524. try {
  525. // 将base64转换为临时文件
  526. const filePath = await base64ToTempFile(base64Data)
  527. pdfFilePath.value = filePath
  528. showPdfThumbnail.value = true
  529. uni.showToast({
  530. title: '上传成功',
  531. icon: 'success',
  532. duration: 2000
  533. })
  534. } catch (error) {
  535. console.error('处理PDF失败:', error)
  536. uni.showToast({
  537. title: '处理PDF失败',
  538. icon: 'none'
  539. })
  540. }
  541. }
  542. // 将base64转换为临时文件
  543. const base64ToTempFile = (base64Data) => {
  544. return new Promise((resolve, reject) => {
  545. const fs = uni.getFileSystemManager()
  546. const fileName = `scan_${Date.now()}.pdf`
  547. const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`
  548. fs.writeFile({
  549. filePath: filePath,
  550. data: base64Data,
  551. encoding: 'base64',
  552. success: () => {
  553. resolve(filePath)
  554. },
  555. fail: (err) => {
  556. reject(err)
  557. }
  558. })
  559. })
  560. }
  561. // 打开PDF预览
  562. const handleOpenPdf = () => {
  563. if (!pdfFilePath.value) return
  564. uni.openDocument({
  565. filePath: pdfFilePath.value,
  566. fileType: 'pdf',
  567. showMenu: true,
  568. success: () => {
  569. // 预览成功
  570. },
  571. fail: (err) => {
  572. console.error('打开文档失败:', err)
  573. uni.showToast({
  574. title: '打开文档失败',
  575. icon: 'none'
  576. })
  577. }
  578. })
  579. }
  580. // 关闭PDF缩略图
  581. const handleClosePdf = () => {
  582. showPdfThumbnail.value = false
  583. pdfFilePath.value = ''
  584. }
  585. </script>
  586. <style lang="scss" scoped>
  587. .scan-page {
  588. width: 100%;
  589. height: 100vh;
  590. display: flex;
  591. flex-direction: column;
  592. background-color: #000000;
  593. // 顶部导航栏
  594. .header-bg {
  595. background: linear-gradient(180deg, #1ec9c9 0%, #1eb8b8 100%);
  596. position: relative;
  597. z-index: 100;
  598. .header-content {
  599. height: 88rpx;
  600. display: flex;
  601. align-items: center;
  602. justify-content: space-between;
  603. padding: 0 24rpx;
  604. .back-btn {
  605. width: 60rpx;
  606. height: 60rpx;
  607. display: flex;
  608. align-items: center;
  609. justify-content: flex-start;
  610. .back-icon {
  611. font-size: 56rpx;
  612. color: #ffffff;
  613. font-weight: 300;
  614. }
  615. }
  616. .header-title {
  617. flex: 1;
  618. text-align: center;
  619. font-size: 32rpx;
  620. font-weight: 600;
  621. color: #ffffff;
  622. }
  623. .placeholder {
  624. width: 60rpx;
  625. }
  626. }
  627. }
  628. // 扫描区域
  629. .scan-area {
  630. flex: 1;
  631. position: relative;
  632. display: flex;
  633. flex-direction: column;
  634. .camera {
  635. flex: 1;
  636. width: 100%;
  637. position: relative;
  638. }
  639. .camera-placeholder {
  640. flex: 1;
  641. width: 100%;
  642. background: #000000;
  643. }
  644. // 模式切换按钮 - 在底部操作栏上方
  645. .mode-switch-wrapper {
  646. position: absolute;
  647. bottom: 280rpx;
  648. left: 50%;
  649. transform: translateX(-50%);
  650. z-index: 10;
  651. .mode-switch {
  652. display: flex;
  653. background: rgba(0, 0, 0, 0.6);
  654. border-radius: 40rpx;
  655. padding: 6rpx;
  656. backdrop-filter: blur(10rpx);
  657. .mode-btn {
  658. padding: 12rpx 32rpx;
  659. border-radius: 34rpx;
  660. transition: all 0.3s;
  661. .mode-text {
  662. font-size: 26rpx;
  663. color: rgba(255, 255, 255, 0.6);
  664. font-weight: 500;
  665. transition: all 0.3s;
  666. }
  667. &.active {
  668. background: rgba(255, 255, 255, 0.9);
  669. .mode-text {
  670. color: #333333;
  671. font-weight: 600;
  672. }
  673. }
  674. &:active {
  675. transform: scale(0.95);
  676. }
  677. }
  678. }
  679. }
  680. // 多页模式缩略图列�?
  681. .thumbnail-list {
  682. position: absolute;
  683. bottom: 220rpx;
  684. left: 0;
  685. right: 0;
  686. height: 160rpx;
  687. background: rgba(0, 0, 0, 0.6);
  688. backdrop-filter: blur(10rpx);
  689. .thumbnail-scroll {
  690. height: 100%;
  691. white-space: nowrap;
  692. .thumbnail-wrapper {
  693. display: inline-flex;
  694. padding: 20rpx;
  695. gap: 16rpx;
  696. .thumbnail-item {
  697. position: relative;
  698. width: 120rpx;
  699. height: 120rpx;
  700. border-radius: 12rpx;
  701. overflow: hidden;
  702. background: #ffffff;
  703. border: 2rpx solid #e0e0e0;
  704. .thumbnail-image {
  705. width: 100%;
  706. height: 100%;
  707. }
  708. .pdf-thumbnail-icon {
  709. width: 100%;
  710. height: 100%;
  711. display: flex;
  712. align-items: center;
  713. justify-content: center;
  714. background: #f5f5f5;
  715. .pdf-icon-img {
  716. width: 60rpx;
  717. height: 60rpx;
  718. }
  719. }
  720. .thumbnail-delete {
  721. position: absolute;
  722. top: 4rpx;
  723. right: 4rpx;
  724. width: 36rpx;
  725. height: 36rpx;
  726. border-radius: 50%;
  727. background: rgba(0, 0, 0, 0.7);
  728. display: flex;
  729. align-items: center;
  730. justify-content: center;
  731. .delete-icon {
  732. font-size: 32rpx;
  733. color: #ffffff;
  734. line-height: 1;
  735. }
  736. }
  737. .thumbnail-index {
  738. position: absolute;
  739. bottom: 4rpx;
  740. left: 4rpx;
  741. padding: 2rpx 8rpx;
  742. background: rgba(0, 0, 0, 0.7);
  743. border-radius: 8rpx;
  744. font-size: 20rpx;
  745. color: #ffffff;
  746. }
  747. }
  748. }
  749. }
  750. }
  751. // 上传按钮
  752. .upload-btn {
  753. position: absolute;
  754. bottom: 240rpx;
  755. right: 32rpx;
  756. width: 120rpx;
  757. height: 80rpx;
  758. background: #1ec9c9;
  759. border-radius: 40rpx;
  760. display: flex;
  761. align-items: center;
  762. justify-content: center;
  763. box-shadow: 0 4rpx 16rpx rgba(30, 201, 201, 0.4);
  764. z-index: 20;
  765. &:active {
  766. transform: scale(0.95);
  767. }
  768. .upload-text {
  769. font-size: 28rpx;
  770. font-weight: 600;
  771. color: #ffffff;
  772. }
  773. }
  774. // PDF缩略图
  775. .pdf-thumbnail {
  776. position: absolute;
  777. bottom: 240rpx;
  778. right: 32rpx;
  779. width: 140rpx;
  780. height: 140rpx;
  781. background: #ffffff;
  782. border-radius: 16rpx;
  783. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);
  784. display: flex;
  785. flex-direction: column;
  786. align-items: center;
  787. justify-content: center;
  788. z-index: 20;
  789. &:active {
  790. transform: scale(0.95);
  791. }
  792. .pdf-close {
  793. position: absolute;
  794. top: -8rpx;
  795. right: -8rpx;
  796. width: 40rpx;
  797. height: 40rpx;
  798. border-radius: 50%;
  799. background: #ff4444;
  800. display: flex;
  801. align-items: center;
  802. justify-content: center;
  803. box-shadow: 0 2rpx 8rpx rgba(255, 68, 68, 0.3);
  804. .close-icon {
  805. font-size: 36rpx;
  806. color: #ffffff;
  807. line-height: 1;
  808. font-weight: 300;
  809. }
  810. }
  811. .pdf-icon {
  812. width: 60rpx;
  813. height: 60rpx;
  814. margin-bottom: 8rpx;
  815. }
  816. .pdf-text {
  817. font-size: 24rpx;
  818. color: #666666;
  819. font-weight: 500;
  820. }
  821. }
  822. // 底部操作按钮
  823. .action-buttons {
  824. position: absolute;
  825. bottom: 0;
  826. left: 0;
  827. right: 0;
  828. background: #ffffff;
  829. padding: 32rpx 40rpx;
  830. padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
  831. display: flex;
  832. align-items: center;
  833. justify-content: space-between;
  834. .action-btn {
  835. display: flex;
  836. flex-direction: column;
  837. align-items: center;
  838. gap: 12rpx;
  839. .btn-icon {
  840. width: 48rpx;
  841. height: 48rpx;
  842. }
  843. .btn-text {
  844. font-size: 24rpx;
  845. color: #666666;
  846. }
  847. }
  848. .capture-btn {
  849. width: 120rpx;
  850. height: 120rpx;
  851. border-radius: 50%;
  852. background: #1ec9c9;
  853. display: flex;
  854. align-items: center;
  855. justify-content: center;
  856. box-shadow: 0 4rpx 16rpx rgba(30, 201, 201, 0.4);
  857. &:active {
  858. transform: scale(0.95);
  859. }
  860. .capture-inner {
  861. width: 100rpx;
  862. height: 100rpx;
  863. border-radius: 50%;
  864. background: #ffffff;
  865. }
  866. }
  867. }
  868. }
  869. }
  870. </style>