|
- <template>
- <view class="page">
- <scroll-view
- class="scroll"
- scroll-y
- refresher-enabled
- :refresher-triggered="refreshing"
- @refresherrefresh="refresh"
- @scrolltolower="loadMore"
- >
- <view class="wrap">
- <view class="studio-head">
- <view class="head-copy">
- <text class="eyebrow">ID PHOTO RECORDS</text>
- <text class="page-title">我的证件照</text>
- <text class="page-desc">制作记录会自动保存,可随时查看成品、下载照片或继续制作。</text>
- </view>
- <view class="create-btn" hover-class="button-pressed" @click="goCreate">制作</view>
- </view>
-
- <view class="stats-row">
- <view class="stat-card">
- <text class="stat-num">{{ total || list.length }}</text>
- <text class="stat-label">全部记录</text>
- </view>
- <view class="stat-card">
- <text class="stat-num">{{ pendingCount }}</text>
- <text class="stat-label">制作中</text>
- </view>
- <view class="stat-card">
- <text class="stat-num">{{ successCount }}</text>
- <text class="stat-label">已完成</text>
- </view>
- </view>
-
- <view v-if="list.length" class="gallery">
- <view v-for="item in list" :key="item.photo_open_id || item.id" class="work-card">
- <view class="poster" hover-class="card-pressed" @click="openItem(item)">
- <image v-if="item.cover" class="poster-img" :src="item.cover" mode="aspectFit" />
- <view v-else class="poster-empty">
- <up-icon name="photo" color="#8abff2" size="30" />
- </view>
-
- <view class="status-chip" :class="statusClass(item)">{{ item.status_text || statusText(item) }}</view>
- <view v-if="isPending(item)" class="progress-mask">
- <view class="spinner"></view>
- <text>正在制作</text>
- </view>
- <view v-else-if="Number(item.status) < 0" class="failed-mask">
- <text>{{ item.summary || '制作失败' }}</text>
- </view>
- </view>
-
- <view class="card-info">
- <text class="spec-name">{{ item.title || '证件照' }}</text>
- <text class="spec-size">{{ item.subtitle || '标准证件照' }}</text>
- <view class="time-line">
- <text>{{ formatTime(item.add_time) }}</text>
- <view class="delete-btn" hover-class="button-pressed" @click.stop="deleteItem(item)">删除</view>
- </view>
- </view>
- </view>
- </view>
-
- <view v-if="!list.length && !loading" class="empty-state">
- <view class="empty-preview">
- <view class="empty-photo">
- <view class="empty-head"></view>
- <view class="empty-body"></view>
- </view>
- </view>
- <text class="empty-title">还没有证件照</text>
- <text class="empty-desc">上传一张清晰正面照,制作符合规格的证件照。</text>
- <view class="empty-action" hover-class="button-pressed" @click="goCreate">去制作</view>
- </view>
-
- <view v-if="loading" class="loading">{{ list.length ? '加载更多...' : '加载中...' }}</view>
- <view v-if="list.length && list.length >= total && !loading" class="end-text">已经到底了</view>
- </view>
- </scroll-view>
- </view>
- </template>
-
- <script setup>
- import { computed, ref } from 'vue'
- import { onHide, 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 list = ref([])
- const page = ref(1)
- const pageSize = 10
- const total = ref(0)
- const loading = ref(false)
- const refreshing = ref(false)
- let fetching = false
- let firstShow = true
- let timer = 0
-
- const pendingCount = computed(() => list.value.filter((item) => isPending(item)).length)
- const successCount = computed(() => list.value.filter((item) => Number(item.status) === 1).length)
-
- onShow(() => {
- if (firstShow) {
- firstShow = false
- fetchList({ reset: true })
- return
- }
- refreshLoaded(true)
- })
- onHide(() => clearTimer())
- onUnload(() => clearTimer())
-
- function isPending(item) {
- return Number(item.status) === 0
- }
-
- function formatTime(value) {
- if (!value) return ''
- return dayjs(Number(value) * 1000).format('YYYY-MM-DD HH:mm')
- }
-
- async function fetchList(options = {}) {
- const {
- reset = false,
- silent = false,
- replace = reset,
- size = pageSize
- } = options
- let queryPage = options.pageNo || page.value
-
- if (fetching) {
- finishRefresh()
- return
- }
- fetching = true
- if (!silent) loading.value = true
- if (reset) {
- page.value = 1
- queryPage = 1
- }
- try {
- const res = await idPhotoApi.getList({ page_no: queryPage, page_size: size })
- const rows = res.list || []
- total.value = Number((res.data && res.data.total) || rows.length)
- list.value = replace ? rows : list.value.concat(rows)
- scheduleRefresh()
- } catch (e) {
- if (!silent) toast((e && e.msg) || '记录加载失败')
- } finally {
- fetching = false
- loading.value = false
- finishRefresh()
- }
- }
-
- function scheduleRefresh() {
- clearTimer()
- if (list.value.some((item) => isPending(item))) {
- timer = setTimeout(() => refreshLoaded(true), 3000)
- }
- }
-
- function clearTimer() {
- clearTimeout(timer)
- timer = 0
- }
-
- function finishRefresh() {
- refreshing.value = false
- uni.stopPullDownRefresh()
- }
-
- function refresh() {
- refreshing.value = true
- fetchList({ reset: true })
- }
-
- function refreshLoaded(silent = true) {
- const currentPage = Math.max(1, Number(page.value) || 1)
- fetchList({
- pageNo: 1,
- size: currentPage * pageSize,
- replace: true,
- silent
- })
- }
-
- function loadMore() {
- if (fetching || list.value.length >= total.value) return
- page.value += 1
- fetchList()
- }
-
- function openItem(item) {
- const id = item.photo_open_id || item.id
- if (!id) return
- uni.navigateTo({ url: `/pages/tool/id-photo-detail?id=${id}` })
- }
-
- function statusClass(item) {
- if (isPending(item)) return 'pending'
- if (Number(item.status) < 0) return 'failed'
- return 'done'
- }
-
- function statusText(item) {
- if (isPending(item)) return '制作中'
- if (Number(item.status) < 0) return '失败'
- return '完成'
- }
-
- function goCreate() {
- 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
- }
-
- function deleteItem(item) {
- const id = item.photo_open_id || item.id
- if (!id) return
- uni.showModal({
- title: '温馨提示',
- content: '是否确认删除该证件照记录?',
- confirmText: '确认删除',
- success: async (res) => {
- if (!res.confirm) return
- try {
- await idPhotoApi.delete({ photo_open_id: id })
- toast('删除成功')
- refreshLoaded(true)
- } catch (e) {
- toast((e && e.msg) || '删除失败')
- }
- }
- })
- }
- </script>
-
- <style lang="scss" scoped>
- .page,
- .scroll {
- height: 100vh;
- background: #f6f8fc;
- }
-
- .wrap {
- padding: 24rpx 28rpx calc(70rpx + env(safe-area-inset-bottom));
- }
-
- .studio-head {
- padding: 30rpx;
- border-radius: 26rpx;
- background: #ffffff;
- display: flex;
- align-items: center;
- gap: 24rpx;
- box-shadow: 0 10rpx 28rpx rgba(31, 52, 86, 0.05);
- }
-
- .head-copy {
- flex: 1;
- min-width: 0;
- }
-
- .eyebrow {
- display: block;
- color: #1f8cff;
- font-size: 20rpx;
- font-weight: 800;
- }
-
- .page-title {
- display: block;
- margin-top: 10rpx;
- color: #172033;
- font-size: 40rpx;
- line-height: 1.2;
- font-weight: 900;
- }
-
- .page-desc {
- display: block;
- margin-top: 12rpx;
- color: #64748b;
- font-size: 24rpx;
- line-height: 1.5;
- }
-
- .create-btn {
- width: 112rpx;
- height: 68rpx;
- border-radius: 999rpx;
- color: #ffffff;
- background: #1f8cff;
- font-size: 26rpx;
- line-height: 68rpx;
- text-align: center;
- font-weight: 800;
- box-shadow: 0 12rpx 24rpx rgba(31, 140, 255, 0.22);
- flex-shrink: 0;
- }
-
- .stats-row {
- margin-top: 18rpx;
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 14rpx;
- }
-
- .stat-card {
- padding: 20rpx 8rpx;
- border-radius: 20rpx;
- background: #ffffff;
- text-align: center;
- box-shadow: 0 8rpx 20rpx rgba(31, 52, 86, 0.04);
- }
-
- .stat-num,
- .stat-label,
- .spec-name,
- .spec-size,
- .empty-title,
- .empty-desc {
- display: block;
- }
-
- .stat-num {
- color: #172033;
- font-size: 34rpx;
- line-height: 1;
- font-weight: 900;
- }
-
- .stat-label {
- margin-top: 10rpx;
- color: #7c8a9b;
- font-size: 22rpx;
- }
-
- .gallery {
- margin-top: 24rpx;
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 22rpx;
- }
-
- .work-card {
- min-width: 0;
- border-radius: 24rpx;
- background: #ffffff;
- overflow: hidden;
- box-shadow: 0 10rpx 28rpx rgba(31, 52, 86, 0.06);
- }
-
- .poster {
- position: relative;
- width: 100%;
- height: 440rpx;
- background: repeating-linear-gradient(135deg, #edf3f9 0, #edf3f9 18rpx, #f8fbfd 18rpx, #f8fbfd 36rpx);
- overflow: hidden;
- }
-
- .poster-img,
- .poster-empty {
- width: 100%;
- height: 100%;
- }
-
- .poster-empty {
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .status-chip {
- position: absolute;
- left: 14rpx;
- top: 14rpx;
- height: 42rpx;
- padding: 0 16rpx;
- border-radius: 999rpx;
- color: #ffffff;
- font-size: 22rpx;
- line-height: 42rpx;
- font-weight: 800;
- background: rgba(15, 23, 42, 0.48);
- }
-
- .status-chip.done { background: rgba(31, 140, 255, 0.92); }
- .status-chip.pending { background: rgba(249, 115, 22, 0.92); }
- .status-chip.failed { background: rgba(239, 68, 68, 0.92); }
-
- .progress-mask,
- .failed-mask {
- position: absolute;
- inset: 0;
- padding: 24rpx;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- color: #ffffff;
- font-size: 26rpx;
- text-align: center;
- background: rgba(15, 23, 42, 0.52);
- }
-
- .failed-mask {
- background: rgba(127, 29, 29, 0.72);
- font-size: 24rpx;
- }
-
- .spinner {
- width: 48rpx;
- height: 48rpx;
- margin-bottom: 18rpx;
- border: 6rpx solid rgba(255, 255, 255, 0.45);
- border-top-color: #ffffff;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- }
-
- .card-info {
- padding: 18rpx;
- }
-
- .spec-name {
- color: #172033;
- font-size: 26rpx;
- font-weight: 800;
- line-height: 1.3;
- }
-
- .spec-size {
- margin-top: 6rpx;
- color: #8994a5;
- font-size: 21rpx;
- line-height: 1.3;
- }
-
- .time-line {
- margin-top: 12rpx;
- display: flex;
- align-items: center;
- gap: 10rpx;
- color: #64748b;
- font-size: 21rpx;
- }
-
- .time-line text {
- flex: 1;
- min-width: 0;
- }
-
- .delete-btn {
- width: 74rpx;
- height: 44rpx;
- border-radius: 999rpx;
- color: #ef4444;
- background: #fff1f2;
- font-size: 22rpx;
- line-height: 44rpx;
- text-align: center;
- flex-shrink: 0;
- }
-
- .empty-state {
- margin-top: 86rpx;
- padding: 56rpx 34rpx;
- border-radius: 28rpx;
- background: #ffffff;
- display: flex;
- flex-direction: column;
- align-items: center;
- text-align: center;
- box-shadow: 0 10rpx 28rpx rgba(31, 52, 86, 0.05);
- }
-
- .empty-preview {
- width: 150rpx;
- height: 200rpx;
- border-radius: 30rpx;
- background: repeating-linear-gradient(135deg, #edf5fd 0, #edf5fd 14rpx, #f8fbff 14rpx, #f8fbff 28rpx);
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .empty-photo {
- position: relative;
- width: 88rpx;
- height: 122rpx;
- border-radius: 8rpx;
- background: #438edb;
- overflow: hidden;
- }
-
- .empty-head {
- position: absolute;
- left: 50%;
- top: 24rpx;
- width: 34rpx;
- height: 34rpx;
- margin-left: -17rpx;
- border-radius: 50%;
- background: #ffffff;
- }
-
- .empty-body {
- position: absolute;
- left: 50%;
- bottom: -18rpx;
- width: 78rpx;
- height: 72rpx;
- margin-left: -39rpx;
- border-radius: 50% 50% 0 0;
- background: #ffffff;
- }
-
- .empty-title {
- margin-top: 28rpx;
- color: #172033;
- font-size: 32rpx;
- font-weight: 900;
- }
-
- .empty-desc {
- margin-top: 12rpx;
- color: #64748b;
- font-size: 25rpx;
- line-height: 1.5;
- }
-
- .empty-action {
- margin-top: 28rpx;
- width: 220rpx;
- height: 72rpx;
- border-radius: 999rpx;
- color: #ffffff;
- background: #1f8cff;
- font-size: 28rpx;
- line-height: 72rpx;
- font-weight: 800;
- }
-
- .loading,
- .end-text {
- padding: 46rpx 0 20rpx;
- color: #7c8a9b;
- font-size: 24rpx;
- text-align: center;
- }
-
- .button-pressed,
- .card-pressed {
- opacity: 0.82;
- }
-
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
-
- @media (min-width: 768px) {
- .wrap {
- width: 720px;
- margin: 0 auto;
- box-sizing: border-box;
- }
- }
- </style>
|