25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 

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