You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

485 regels
11 KiB

  1. <template>
  2. <view class="page">
  3. <scroll-view class="scroll" scroll-y>
  4. <view class="wrap">
  5. <view v-if="!loaded || !isSuccess" class="state-card">
  6. <view v-if="!loaded || isPending" class="spinner"></view>
  7. <text class="state-title">{{ stateTitle }}</text>
  8. <text v-if="isPending && countDown > 0" class="count">{{ countDown }} S 后刷新</text>
  9. <text v-if="loaded && !isPending" class="error">{{ detail.error_msg || '暂时无法读取证件照成品,请稍后重试' }}</text>
  10. <view v-if="loaded && !isPending" class="retry-btn" hover-class="button-pressed" @click="createAgain">重新制作</view>
  11. </view>
  12. <template v-else>
  13. <view class="result-card">
  14. <view class="result-head">
  15. <view>
  16. <text class="eyebrow">FINISHED PHOTO</text>
  17. <text class="result-title">{{ detail.spec_name || '证件照' }}</text>
  18. </view>
  19. <view class="dpi-chip">{{ detail.dpi || 300 }} DPI</view>
  20. </view>
  21. <view class="result-stage" @click="preview">
  22. <image class="result-img" :src="detail.result_url" mode="widthFix" />
  23. </view>
  24. <text class="preview-tip">点击照片可查看大图</text>
  25. </view>
  26. <view class="info-card">
  27. <text class="info-title">成品信息</text>
  28. <view class="cell">
  29. <text class="cell-label">证件照规格</text>
  30. <text class="cell-content">{{ detail.spec_name || '-' }}</text>
  31. </view>
  32. <view class="cell">
  33. <text class="cell-label">像素尺寸</text>
  34. <text class="cell-content">{{ sizeText }}</text>
  35. </view>
  36. <view class="cell">
  37. <text class="cell-label">背景颜色</text>
  38. <view class="color-value">
  39. <view class="color-dot" :style="{ backgroundColor: detail.background || '#438edb' }"></view>
  40. <text>{{ String(detail.background || '#438EDB').toUpperCase() }}</text>
  41. </view>
  42. </view>
  43. <view class="cell">
  44. <text class="cell-label">画面优化</text>
  45. <text class="cell-content">{{ beautyText }}</text>
  46. </view>
  47. <view class="cell">
  48. <text class="cell-label">创建时间</text>
  49. <text class="cell-content">{{ formatTime(detail.add_time) }}</text>
  50. </view>
  51. </view>
  52. </template>
  53. </view>
  54. </scroll-view>
  55. <view v-if="isSuccess" class="bottom-bar">
  56. <view class="delete-btn" hover-class="button-pressed" @click="deleteRecord">删除</view>
  57. <view class="save-btn" hover-class="button-pressed" @click="download">
  58. <up-icon name="download" color="#1f8cff" size="18" />
  59. <text>保存照片</text>
  60. </view>
  61. <view class="main-btn" hover-class="button-pressed" @click="createAgain">再做一张</view>
  62. </view>
  63. </view>
  64. </template>
  65. <script setup>
  66. import { computed, ref } from 'vue'
  67. import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
  68. import dayjs from 'dayjs'
  69. import { idPhotoApi } from '@/api/index.js'
  70. import { useApp } from '@/utils/useApp.js'
  71. const { toast } = useApp()
  72. const id = ref('')
  73. const detail = ref({ status: 0 })
  74. const loaded = ref(false)
  75. const countDown = ref(0)
  76. let timer = 0
  77. let countTimer = 0
  78. const isPending = computed(() => loaded.value && Number(detail.value.status) === 0)
  79. const isSuccess = computed(() => loaded.value && Number(detail.value.status) === 1 && Boolean(detail.value.result_url))
  80. const stateTitle = computed(() => {
  81. if (!loaded.value) return '正在读取证件照'
  82. if (isPending.value) return '证件照正在制作中'
  83. if (Number(detail.value.status) < 0) return '本次制作失败'
  84. return '证件照成品暂不可用'
  85. })
  86. const sizeText = computed(() => {
  87. const width = Number(detail.value.width || 0)
  88. const height = Number(detail.value.height || 0)
  89. return width && height ? `${width} × ${height}px` : '-'
  90. })
  91. const beautyText = computed(() => detail.value.beauty === 'natural' ? '自然优化' : '关闭美化')
  92. onLoad((options = {}) => {
  93. id.value = String(options.id || options.work || '')
  94. })
  95. onShow(() => loadDetail())
  96. onUnload(() => clearPolling())
  97. async function loadDetail() {
  98. if (!id.value) {
  99. detail.value = { status: -1, error_msg: '缺少证件照记录编号' }
  100. loaded.value = true
  101. return
  102. }
  103. clearPolling()
  104. try {
  105. const res = await idPhotoApi.getDetail({ photo_open_id: id.value })
  106. detail.value = res.data || {}
  107. loaded.value = true
  108. if (Number(detail.value.status) === 0) waitNext()
  109. } catch (e) {
  110. detail.value = {
  111. status: -1,
  112. error_msg: (e && e.msg) || '记录加载失败'
  113. }
  114. loaded.value = true
  115. toast((e && e.msg) || '记录加载失败')
  116. }
  117. }
  118. function waitNext() {
  119. clearPolling()
  120. countDown.value = 3
  121. countTimer = setInterval(() => {
  122. countDown.value -= 1
  123. if (countDown.value <= 0) clearInterval(countTimer)
  124. }, 1000)
  125. timer = setTimeout(loadDetail, 3000)
  126. }
  127. function clearPolling() {
  128. clearTimeout(timer)
  129. clearInterval(countTimer)
  130. timer = 0
  131. countTimer = 0
  132. }
  133. function formatTime(value) {
  134. if (!value) return '-'
  135. return dayjs(Number(value) * 1000).format('YYYY-MM-DD HH:mm')
  136. }
  137. function preview() {
  138. if (!detail.value.result_url) return
  139. uni.previewImage({ urls: [detail.value.result_url], current: detail.value.result_url })
  140. }
  141. function download() {
  142. if (!detail.value.result_url) return
  143. uni.showLoading({ title: '保存中...', mask: true })
  144. uni.downloadFile({
  145. url: detail.value.result_url,
  146. success: (res) => {
  147. if (res.statusCode !== 200) {
  148. toast('图片下载失败')
  149. return
  150. }
  151. uni.saveImageToPhotosAlbum({
  152. filePath: res.tempFilePath,
  153. success: () => toast('已保存到相册'),
  154. fail: () => toast('保存失败,请检查相册权限')
  155. })
  156. },
  157. fail: () => toast('图片下载失败'),
  158. complete: () => uni.hideLoading()
  159. })
  160. }
  161. function deleteRecord() {
  162. if (!id.value) return
  163. uni.showModal({
  164. title: '删除记录',
  165. content: '删除后将不再展示该证件照记录,是否继续?',
  166. confirmText: '删除',
  167. confirmColor: '#f04438',
  168. success: async (res) => {
  169. if (!res.confirm) return
  170. try {
  171. await idPhotoApi.delete({ photo_open_id: id.value })
  172. toast('删除成功')
  173. const pages = getCurrentPages()
  174. if (pages.length > 1) {
  175. uni.navigateBack()
  176. } else {
  177. uni.redirectTo({ url: '/pages/works/works' })
  178. }
  179. } catch (e) {
  180. toast((e && e.msg) || '删除失败')
  181. }
  182. }
  183. })
  184. }
  185. function createAgain() {
  186. const delta = getPageStackDelta('pages/tool/id-photo')
  187. if (delta > 0) {
  188. uni.navigateBack({ delta })
  189. return
  190. }
  191. uni.navigateTo({ url: '/pages/tool/id-photo' })
  192. }
  193. function getPageStackDelta(routeName) {
  194. const pages = getCurrentPages()
  195. const index = pages.findIndex((item) => item.route === routeName)
  196. return index >= 0 ? pages.length - 1 - index : -1
  197. }
  198. </script>
  199. <style lang="scss" scoped>
  200. .page {
  201. height: 100vh;
  202. display: flex;
  203. flex-direction: column;
  204. overflow: hidden;
  205. background: #f4f7fb;
  206. }
  207. .scroll {
  208. flex: 1;
  209. height: 0;
  210. }
  211. .wrap {
  212. padding: 28rpx 28rpx calc(150rpx + env(safe-area-inset-bottom));
  213. }
  214. .state-card,
  215. .result-card,
  216. .info-card {
  217. border-radius: 24rpx;
  218. background: #ffffff;
  219. box-shadow: 0 10rpx 28rpx rgba(31, 52, 86, 0.05);
  220. overflow: hidden;
  221. }
  222. .state-card {
  223. min-height: 660rpx;
  224. padding: 50rpx;
  225. display: flex;
  226. flex-direction: column;
  227. align-items: center;
  228. justify-content: center;
  229. box-sizing: border-box;
  230. text-align: center;
  231. }
  232. .spinner {
  233. width: 68rpx;
  234. height: 68rpx;
  235. margin-bottom: 28rpx;
  236. border: 8rpx solid #e9edf5;
  237. border-top-color: #1f8cff;
  238. border-radius: 50%;
  239. animation: spin 0.8s linear infinite;
  240. }
  241. .state-title {
  242. color: #172033;
  243. font-size: 32rpx;
  244. font-weight: 800;
  245. }
  246. .count,
  247. .error {
  248. margin-top: 18rpx;
  249. color: #7f8896;
  250. font-size: 25rpx;
  251. line-height: 1.5;
  252. }
  253. .error {
  254. color: #e74b43;
  255. }
  256. .retry-btn {
  257. min-width: 210rpx;
  258. height: 72rpx;
  259. margin-top: 30rpx;
  260. padding: 0 28rpx;
  261. border-radius: 999rpx;
  262. color: #ffffff;
  263. background: #1f8cff;
  264. font-size: 27rpx;
  265. font-weight: 800;
  266. line-height: 72rpx;
  267. box-sizing: border-box;
  268. }
  269. .result-head {
  270. padding: 26rpx 28rpx 22rpx;
  271. display: flex;
  272. align-items: center;
  273. justify-content: space-between;
  274. gap: 20rpx;
  275. }
  276. .eyebrow {
  277. display: block;
  278. color: #1f8cff;
  279. font-size: 19rpx;
  280. font-weight: 800;
  281. }
  282. .result-title {
  283. display: block;
  284. margin-top: 8rpx;
  285. color: #172033;
  286. font-size: 34rpx;
  287. font-weight: 900;
  288. }
  289. .dpi-chip {
  290. height: 48rpx;
  291. padding: 0 18rpx;
  292. border-radius: 999rpx;
  293. color: #1f8cff;
  294. background: #edf7ff;
  295. font-size: 21rpx;
  296. font-weight: 800;
  297. line-height: 48rpx;
  298. flex-shrink: 0;
  299. }
  300. .result-stage {
  301. min-height: 520rpx;
  302. padding: 30rpx;
  303. background: repeating-linear-gradient(135deg, #edf3f9 0, #edf3f9 20rpx, #f8fbfd 20rpx, #f8fbfd 40rpx);
  304. display: flex;
  305. align-items: center;
  306. justify-content: center;
  307. box-sizing: border-box;
  308. }
  309. .result-img {
  310. width: 520rpx;
  311. max-width: 100%;
  312. border-radius: 4rpx;
  313. display: block;
  314. box-shadow: 0 16rpx 34rpx rgba(23, 32, 51, 0.16);
  315. }
  316. .preview-tip {
  317. display: block;
  318. padding: 18rpx 28rpx 22rpx;
  319. color: #8a95a6;
  320. font-size: 22rpx;
  321. text-align: center;
  322. }
  323. .info-card {
  324. margin-top: 24rpx;
  325. padding: 28rpx;
  326. }
  327. .info-title {
  328. display: block;
  329. margin-bottom: 14rpx;
  330. color: #172033;
  331. font-size: 31rpx;
  332. font-weight: 800;
  333. }
  334. .cell {
  335. min-height: 70rpx;
  336. padding: 8rpx 0;
  337. border-bottom: 1rpx solid #edf1f6;
  338. display: flex;
  339. align-items: center;
  340. gap: 24rpx;
  341. box-sizing: border-box;
  342. font-size: 26rpx;
  343. }
  344. .cell:last-child {
  345. border-bottom: 0;
  346. }
  347. .cell-label {
  348. width: 160rpx;
  349. color: #7f8896;
  350. flex-shrink: 0;
  351. }
  352. .cell-content,
  353. .color-value {
  354. flex: 1;
  355. min-width: 0;
  356. color: #172033;
  357. text-align: right;
  358. }
  359. .color-value {
  360. display: flex;
  361. align-items: center;
  362. justify-content: flex-end;
  363. gap: 12rpx;
  364. }
  365. .color-dot {
  366. width: 30rpx;
  367. height: 30rpx;
  368. border-radius: 50%;
  369. border: 2rpx solid rgba(23, 32, 51, 0.12);
  370. box-sizing: border-box;
  371. }
  372. .bottom-bar {
  373. position: fixed;
  374. z-index: 80;
  375. left: 0;
  376. right: 0;
  377. bottom: 0;
  378. padding: 18rpx 28rpx calc(18rpx + env(safe-area-inset-bottom));
  379. display: flex;
  380. gap: 18rpx;
  381. background: rgba(255, 255, 255, 0.97);
  382. box-shadow: 0 -8rpx 24rpx rgba(35, 55, 90, 0.08);
  383. box-sizing: border-box;
  384. }
  385. .save-btn,
  386. .delete-btn,
  387. .main-btn {
  388. height: 82rpx;
  389. border-radius: 42rpx;
  390. display: flex;
  391. align-items: center;
  392. justify-content: center;
  393. font-size: 29rpx;
  394. font-weight: 800;
  395. }
  396. .delete-btn {
  397. width: 150rpx;
  398. color: #f04438;
  399. background: #fff1f0;
  400. }
  401. .save-btn {
  402. width: 220rpx;
  403. gap: 10rpx;
  404. color: #1f8cff;
  405. background: #eef6ff;
  406. }
  407. .main-btn {
  408. flex: 1;
  409. color: #ffffff;
  410. background: #1f8cff;
  411. box-shadow: 0 12rpx 26rpx rgba(31, 140, 255, 0.2);
  412. }
  413. .button-pressed {
  414. opacity: 0.82;
  415. }
  416. @keyframes spin {
  417. to { transform: rotate(360deg); }
  418. }
  419. @media (min-width: 768px) {
  420. .wrap {
  421. width: 720px;
  422. margin: 0 auto;
  423. box-sizing: border-box;
  424. }
  425. .bottom-bar {
  426. left: 50%;
  427. width: 720px;
  428. transform: translateX(-50%);
  429. border-left: 1px solid #e6edf5;
  430. border-right: 1px solid #e6edf5;
  431. }
  432. }
  433. </style>