|
- <template>
- <view class="page">
- <scroll-view class="scroll" scroll-y>
- <view class="wrap">
- <view v-if="!loaded || !isSuccess" class="state-card">
- <view v-if="!loaded || isPending" class="spinner"></view>
- <text class="state-title">{{ stateTitle }}</text>
- <text v-if="isPending && countDown > 0" class="count">{{ countDown }} S 后刷新</text>
- <text v-if="loaded && !isPending" class="error">{{ detail.error_msg || '暂时无法读取证件照成品,请稍后重试' }}</text>
- <view v-if="loaded && !isPending" class="retry-btn" hover-class="button-pressed" @click="createAgain">重新制作</view>
- </view>
-
- <template v-else>
- <view class="result-card">
- <view class="result-head">
- <view>
- <text class="eyebrow">FINISHED PHOTO</text>
- <text class="result-title">{{ detail.spec_name || '证件照' }}</text>
- </view>
- <view class="dpi-chip">{{ detail.dpi || 300 }} DPI</view>
- </view>
- <view class="result-stage" @click="preview">
- <image class="result-img" :src="detail.result_url" mode="widthFix" />
- </view>
- <text class="preview-tip">点击照片可查看大图</text>
- </view>
-
- <view class="info-card">
- <text class="info-title">成品信息</text>
- <view class="cell">
- <text class="cell-label">证件照规格</text>
- <text class="cell-content">{{ detail.spec_name || '-' }}</text>
- </view>
- <view class="cell">
- <text class="cell-label">像素尺寸</text>
- <text class="cell-content">{{ sizeText }}</text>
- </view>
- <view class="cell">
- <text class="cell-label">背景颜色</text>
- <view class="color-value">
- <view class="color-dot" :style="{ backgroundColor: detail.background || '#438edb' }"></view>
- <text>{{ String(detail.background || '#438EDB').toUpperCase() }}</text>
- </view>
- </view>
- <view class="cell">
- <text class="cell-label">画面优化</text>
- <text class="cell-content">{{ beautyText }}</text>
- </view>
- <view class="cell">
- <text class="cell-label">创建时间</text>
- <text class="cell-content">{{ formatTime(detail.add_time) }}</text>
- </view>
- </view>
- </template>
- </view>
- </scroll-view>
-
- <view v-if="isSuccess" class="bottom-bar">
- <view class="save-btn" hover-class="button-pressed" @click="download">
- <up-icon name="download" color="#1f8cff" size="18" />
- <text>保存照片</text>
- </view>
- <view class="main-btn" hover-class="button-pressed" @click="createAgain">再做一张</view>
- </view>
- </view>
- </template>
-
- <script setup>
- import { computed, ref } from 'vue'
- import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
- import dayjs from 'dayjs'
- import { idPhotoApi } from '@/api/index.js'
- import { useApp } from '@/utils/useApp.js'
-
- const { toast } = useApp()
- const id = ref('')
- const detail = ref({ status: 0 })
- const loaded = ref(false)
- const countDown = ref(0)
- let timer = 0
- let countTimer = 0
-
- const isPending = computed(() => loaded.value && Number(detail.value.status) === 0)
- const isSuccess = computed(() => loaded.value && Number(detail.value.status) === 1 && Boolean(detail.value.result_url))
- const stateTitle = computed(() => {
- if (!loaded.value) return '正在读取证件照'
- if (isPending.value) return '证件照正在制作中'
- if (Number(detail.value.status) < 0) return '本次制作失败'
- return '证件照成品暂不可用'
- })
- const sizeText = computed(() => {
- const width = Number(detail.value.width || 0)
- const height = Number(detail.value.height || 0)
- return width && height ? `${width} × ${height}px` : '-'
- })
- const beautyText = computed(() => detail.value.beauty === 'natural' ? '自然优化' : '关闭美化')
-
- onLoad((options = {}) => {
- id.value = String(options.id || options.work || '')
- })
-
- onShow(() => loadDetail())
- onUnload(() => clearPolling())
-
- async function loadDetail() {
- if (!id.value) {
- detail.value = { status: -1, error_msg: '缺少证件照记录编号' }
- loaded.value = true
- return
- }
- clearPolling()
- try {
- const res = await idPhotoApi.getDetail({ photo_open_id: id.value })
- detail.value = res.data || {}
- loaded.value = true
- if (Number(detail.value.status) === 0) waitNext()
- } catch (e) {
- detail.value = {
- status: -1,
- error_msg: (e && e.msg) || '记录加载失败'
- }
- loaded.value = true
- toast((e && e.msg) || '记录加载失败')
- }
- }
-
- function waitNext() {
- clearPolling()
- countDown.value = 3
- countTimer = setInterval(() => {
- countDown.value -= 1
- if (countDown.value <= 0) clearInterval(countTimer)
- }, 1000)
- timer = setTimeout(loadDetail, 3000)
- }
-
- function clearPolling() {
- clearTimeout(timer)
- clearInterval(countTimer)
- timer = 0
- countTimer = 0
- }
-
- function formatTime(value) {
- if (!value) return '-'
- return dayjs(Number(value) * 1000).format('YYYY-MM-DD HH:mm')
- }
-
- function preview() {
- if (!detail.value.result_url) return
- uni.previewImage({ urls: [detail.value.result_url], current: detail.value.result_url })
- }
-
- function download() {
- if (!detail.value.result_url) return
- uni.showLoading({ title: '保存中...', mask: true })
- uni.downloadFile({
- url: detail.value.result_url,
- success: (res) => {
- if (res.statusCode !== 200) {
- toast('图片下载失败')
- return
- }
- uni.saveImageToPhotosAlbum({
- filePath: res.tempFilePath,
- success: () => toast('已保存到相册'),
- fail: () => toast('保存失败,请检查相册权限')
- })
- },
- fail: () => toast('图片下载失败'),
- complete: () => uni.hideLoading()
- })
- }
-
- function createAgain() {
- const delta = getPageStackDelta('pages/tool/id-photo')
- if (delta > 0) {
- uni.navigateBack({ delta })
- return
- }
- uni.navigateTo({ url: '/pages/tool/id-photo' })
- }
-
- function getPageStackDelta(routeName) {
- const pages = getCurrentPages()
- const index = pages.findIndex((item) => item.route === routeName)
- return index >= 0 ? pages.length - 1 - index : -1
- }
- </script>
-
- <style lang="scss" scoped>
- .page {
- height: 100vh;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- background: #f4f7fb;
- }
-
- .scroll {
- flex: 1;
- height: 0;
- }
-
- .wrap {
- padding: 28rpx 28rpx calc(150rpx + env(safe-area-inset-bottom));
- }
-
- .state-card,
- .result-card,
- .info-card {
- border-radius: 24rpx;
- background: #ffffff;
- box-shadow: 0 10rpx 28rpx rgba(31, 52, 86, 0.05);
- overflow: hidden;
- }
-
- .state-card {
- min-height: 660rpx;
- padding: 50rpx;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- box-sizing: border-box;
- text-align: center;
- }
-
- .spinner {
- width: 68rpx;
- height: 68rpx;
- margin-bottom: 28rpx;
- border: 8rpx solid #e9edf5;
- border-top-color: #1f8cff;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- }
-
- .state-title {
- color: #172033;
- font-size: 32rpx;
- font-weight: 800;
- }
-
- .count,
- .error {
- margin-top: 18rpx;
- color: #7f8896;
- font-size: 25rpx;
- line-height: 1.5;
- }
-
- .error {
- color: #e74b43;
- }
-
- .retry-btn {
- min-width: 210rpx;
- height: 72rpx;
- margin-top: 30rpx;
- padding: 0 28rpx;
- border-radius: 999rpx;
- color: #ffffff;
- background: #1f8cff;
- font-size: 27rpx;
- font-weight: 800;
- line-height: 72rpx;
- box-sizing: border-box;
- }
-
- .result-head {
- padding: 26rpx 28rpx 22rpx;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 20rpx;
- }
-
- .eyebrow {
- display: block;
- color: #1f8cff;
- font-size: 19rpx;
- font-weight: 800;
- }
-
- .result-title {
- display: block;
- margin-top: 8rpx;
- color: #172033;
- font-size: 34rpx;
- font-weight: 900;
- }
-
- .dpi-chip {
- height: 48rpx;
- padding: 0 18rpx;
- border-radius: 999rpx;
- color: #1f8cff;
- background: #edf7ff;
- font-size: 21rpx;
- font-weight: 800;
- line-height: 48rpx;
- flex-shrink: 0;
- }
-
- .result-stage {
- min-height: 520rpx;
- padding: 30rpx;
- background: repeating-linear-gradient(135deg, #edf3f9 0, #edf3f9 20rpx, #f8fbfd 20rpx, #f8fbfd 40rpx);
- display: flex;
- align-items: center;
- justify-content: center;
- box-sizing: border-box;
- }
-
- .result-img {
- width: 520rpx;
- max-width: 100%;
- border-radius: 4rpx;
- display: block;
- box-shadow: 0 16rpx 34rpx rgba(23, 32, 51, 0.16);
- }
-
- .preview-tip {
- display: block;
- padding: 18rpx 28rpx 22rpx;
- color: #8a95a6;
- font-size: 22rpx;
- text-align: center;
- }
-
- .info-card {
- margin-top: 24rpx;
- padding: 28rpx;
- }
-
- .info-title {
- display: block;
- margin-bottom: 14rpx;
- color: #172033;
- font-size: 31rpx;
- font-weight: 800;
- }
-
- .cell {
- min-height: 70rpx;
- padding: 8rpx 0;
- border-bottom: 1rpx solid #edf1f6;
- display: flex;
- align-items: center;
- gap: 24rpx;
- box-sizing: border-box;
- font-size: 26rpx;
- }
-
- .cell:last-child {
- border-bottom: 0;
- }
-
- .cell-label {
- width: 160rpx;
- color: #7f8896;
- flex-shrink: 0;
- }
-
- .cell-content,
- .color-value {
- flex: 1;
- min-width: 0;
- color: #172033;
- text-align: right;
- }
-
- .color-value {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- gap: 12rpx;
- }
-
- .color-dot {
- width: 30rpx;
- height: 30rpx;
- border-radius: 50%;
- border: 2rpx solid rgba(23, 32, 51, 0.12);
- box-sizing: border-box;
- }
-
- .bottom-bar {
- position: fixed;
- z-index: 80;
- left: 0;
- right: 0;
- bottom: 0;
- padding: 18rpx 28rpx calc(18rpx + env(safe-area-inset-bottom));
- display: flex;
- gap: 18rpx;
- background: rgba(255, 255, 255, 0.97);
- box-shadow: 0 -8rpx 24rpx rgba(35, 55, 90, 0.08);
- box-sizing: border-box;
- }
-
- .save-btn,
- .main-btn {
- height: 82rpx;
- border-radius: 42rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 29rpx;
- font-weight: 800;
- }
-
- .save-btn {
- width: 240rpx;
- gap: 10rpx;
- color: #1f8cff;
- background: #eef6ff;
- }
-
- .main-btn {
- flex: 1;
- color: #ffffff;
- background: #1f8cff;
- box-shadow: 0 12rpx 26rpx rgba(31, 140, 255, 0.2);
- }
-
- .button-pressed {
- opacity: 0.82;
- }
-
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
-
- @media (min-width: 768px) {
- .wrap {
- width: 720px;
- margin: 0 auto;
- box-sizing: border-box;
- }
-
- .bottom-bar {
- left: 50%;
- width: 720px;
- transform: translateX(-50%);
- border-left: 1px solid #e6edf5;
- border-right: 1px solid #e6edf5;
- }
- }
- </style>
|