|
|
@@ -0,0 +1,351 @@
|
|
|
+<template>
|
|
|
+ <view class="page">
|
|
|
+ <!-- 顶部 Tab -->
|
|
|
+ <view class="tabs">
|
|
|
+ <view
|
|
|
+ v-for="t in tabs"
|
|
|
+ :key="t.value"
|
|
|
+ :class="['tab-item', { active: currentTab === t.value }]"
|
|
|
+ @tap="onTabChange(t.value)"
|
|
|
+ >
|
|
|
+ <text>{{ t.label }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="filter-btn" @tap="onFilterTap">
|
|
|
+ <text class="filter-icon">▼</text>
|
|
|
+ <text>筛选</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 列表 -->
|
|
|
+ <scroll-view
|
|
|
+ class="list"
|
|
|
+ scroll-y
|
|
|
+ @scrolltolower="onReachBottom"
|
|
|
+ :refresher-enabled="true"
|
|
|
+ :refresher-triggered="refreshing"
|
|
|
+ @refresherrefresh="onRefresh"
|
|
|
+ >
|
|
|
+ <view v-if="loading && list.length === 0" class="empty">加载中…</view>
|
|
|
+ <view v-else-if="!loading && list.length === 0" class="empty">暂无品控巡检任务</view>
|
|
|
+
|
|
|
+ <view
|
|
|
+ v-for="item in list"
|
|
|
+ :key="item.id"
|
|
|
+ class="task-card"
|
|
|
+ >
|
|
|
+ <!-- 头部:任务名称 + 打卡状态 -->
|
|
|
+ <view class="card-head">
|
|
|
+ <text class="task-name text-ellipsis">{{ item.taskName }}</text>
|
|
|
+ <text :class="['tag', statusTagClass(item.punchStatus)]">
|
|
|
+ {{ statusLabel(item.punchStatus) }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 字段列表(9 个字段 + 2 个条件显示) -->
|
|
|
+ <view class="info">
|
|
|
+ <view class="info-row">
|
|
|
+ <text class="info-label">巡检类型:</text>
|
|
|
+ <text class="info-value">{{ item.taskType || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-row">
|
|
|
+ <text class="info-label">打卡方式:</text>
|
|
|
+ <text class="info-value">{{ punchTypeName(item.punchType) }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-row">
|
|
|
+ <text class="info-label">门店:</text>
|
|
|
+ <text class="info-value">{{ item.storeName || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-row">
|
|
|
+ <text class="info-label">打卡地点:</text>
|
|
|
+ <text class="info-value">{{ item.location || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-row">
|
|
|
+ <text class="info-label">任务完成时间:</text>
|
|
|
+ <text class="info-value">{{ item.planEndTime || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <!-- 实际打卡时间:仅已打卡状态显示 -->
|
|
|
+ <view v-if="item.punchStatus === 'DONE' && item.punchTime" class="info-row">
|
|
|
+ <text class="info-label">实际打卡时间:</text>
|
|
|
+ <text class="info-value">{{ item.punchTime }}</text>
|
|
|
+ </view>
|
|
|
+ <!-- 打卡结果:仅已打卡状态显示 -->
|
|
|
+ <view v-if="item.punchStatus === 'DONE'" class="info-row">
|
|
|
+ <text class="info-label">打卡结果:</text>
|
|
|
+ <text :class="['tag', resultTagClass(item.punchResult)]">
|
|
|
+ {{ resultLabel(item.punchResult) || '-' }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 操作按钮 -->
|
|
|
+ <view class="card-foot">
|
|
|
+ <button
|
|
|
+ v-if="item.punchStatus === 'PENDING'"
|
|
|
+ class="btn btn-primary"
|
|
|
+ @tap="onPunchTap(item)"
|
|
|
+ >前去打卡</button>
|
|
|
+ <button
|
|
|
+ v-else
|
|
|
+ class="btn btn-default"
|
|
|
+ @tap="onDetailTap(item)"
|
|
|
+ >查看详情</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="noMore" class="nomore">— 没有更多了 —</view>
|
|
|
+ </scroll-view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, onMounted } from 'vue'
|
|
|
+import { onShow } from '@dcloudio/uni-app'
|
|
|
+import type { QualityInspectionTask, QualityListQuery } from '@/types/qualityInspection'
|
|
|
+import { listQualityInspectionTasks, punchTypeName, resultLabel } from '@/api/qualityInspection'
|
|
|
+
|
|
|
+// ===== Tabs:全部/未打卡/已打卡 =====
|
|
|
+const tabs = [
|
|
|
+ { label: '全部', value: 'ALL' },
|
|
|
+ { label: '未打卡', value: 'PENDING' },
|
|
|
+ { label: '已打卡', value: 'DONE' }
|
|
|
+] as const
|
|
|
+
|
|
|
+const currentTab = ref<QualityListQuery['status']>('ALL')
|
|
|
+const list = ref<QualityInspectionTask[]>([])
|
|
|
+const loading = ref(false)
|
|
|
+const refreshing = ref(false)
|
|
|
+const noMore = ref(false)
|
|
|
+const pageNo = ref(1)
|
|
|
+const pageSize = 10
|
|
|
+
|
|
|
+// ===== 加载数据 =====
|
|
|
+async function fetchData(reset = false) {
|
|
|
+ if (loading.value) return
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const data = await listQualityInspectionTasks({
|
|
|
+ status: currentTab.value,
|
|
|
+ pageNo: reset ? 1 : pageNo.value,
|
|
|
+ pageSize
|
|
|
+ })
|
|
|
+ const rows = (data?.rows ?? []) as QualityInspectionTask[]
|
|
|
+ if (reset) {
|
|
|
+ list.value = rows
|
|
|
+ } else {
|
|
|
+ list.value.push(...rows)
|
|
|
+ }
|
|
|
+ noMore.value = rows.length < pageSize
|
|
|
+ pageNo.value = (reset ? 1 : pageNo.value) + 1
|
|
|
+ } catch (e: any) {
|
|
|
+ if (reset) {
|
|
|
+ list.value = []
|
|
|
+ }
|
|
|
+ uni.showToast({
|
|
|
+ title: e?.message || '加载失败,请检查后端服务',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 2000
|
|
|
+ })
|
|
|
+ console.error('[品控巡检列表] 加载失败', e)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ refreshing.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function onTabChange(v: QualityListQuery['status']) {
|
|
|
+ if (v === currentTab.value) return
|
|
|
+ currentTab.value = v
|
|
|
+ pageNo.value = 1
|
|
|
+ list.value = []
|
|
|
+ noMore.value = false
|
|
|
+ fetchData(true)
|
|
|
+}
|
|
|
+
|
|
|
+function onFilterTap() {
|
|
|
+ // 简单弹窗(独立功能,占位即可)
|
|
|
+ uni.showActionSheet({
|
|
|
+ itemList: ['全部类型', '品控巡检', '质量检查', '卫生检查', '工艺检查'],
|
|
|
+ success: (r) => {
|
|
|
+ console.log('筛选', r.tapIndex)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function onPunchTap(item: QualityInspectionTask) {
|
|
|
+ // 品控暂时复用风控打卡页(暂不区分 NORMAL/SCAN 路由差异,跳统一详情/打卡页)
|
|
|
+ uni.navigateTo({
|
|
|
+ url: `/pages/inspection/check/index?taskId=${item.id}`
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function onDetailTap(item: QualityInspectionTask) {
|
|
|
+ uni.navigateTo({
|
|
|
+ url: `/pages/inspection/check/index?taskId=${item.id}&readonly=1`
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function onReachBottom() {
|
|
|
+ if (noMore.value) return
|
|
|
+ fetchData()
|
|
|
+}
|
|
|
+
|
|
|
+async function onRefresh() {
|
|
|
+ refreshing.value = true
|
|
|
+ pageNo.value = 1
|
|
|
+ await fetchData(true)
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ fetchData(true)
|
|
|
+})
|
|
|
+onShow(() => {
|
|
|
+ if (list.value.length > 0) {
|
|
|
+ pageNo.value = 1
|
|
|
+ fetchData(true)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// ===== 模板用工具方法 =====
|
|
|
+function statusLabel(s: string) {
|
|
|
+ return { PENDING: '未打卡', DONE: '已打卡', OVERDUE: '已逾期' }[s] ?? s
|
|
|
+}
|
|
|
+// 不同颜色:已打卡=success(绿),未打卡=warning(橙)
|
|
|
+function statusTagClass(s: string) {
|
|
|
+ return {
|
|
|
+ PENDING: 'tag-warning',
|
|
|
+ DONE: 'tag-success',
|
|
|
+ OVERDUE: 'tag-danger',
|
|
|
+ PUNCHED: 'tag-success',
|
|
|
+ FINISHED: 'tag-success'
|
|
|
+ }[s] ?? 'tag-info'
|
|
|
+}
|
|
|
+// 不同颜色:正常=success(绿),异常=warning(橙)
|
|
|
+function resultTagClass(r?: string | null) {
|
|
|
+ if (r === '0') return 'tag-success' // 正常
|
|
|
+ if (r === '1') return 'tag-warning' // 异常
|
|
|
+ return 'tag-info'
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.page {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100vh;
|
|
|
+ background: $bg-page;
|
|
|
+}
|
|
|
+
|
|
|
+/* Tabs */
|
|
|
+.tabs {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background: $bg-card;
|
|
|
+ padding: 0 24rpx;
|
|
|
+ border-bottom: 1rpx solid $border-light;
|
|
|
+ position: sticky;
|
|
|
+ top: 0;
|
|
|
+ z-index: $z-nav;
|
|
|
+}
|
|
|
+.tab-item {
|
|
|
+ padding: 28rpx 24rpx;
|
|
|
+ font-size: $font-md;
|
|
|
+ color: $text-regular;
|
|
|
+ position: relative;
|
|
|
+ &.active {
|
|
|
+ color: $primary;
|
|
|
+ font-weight: 600;
|
|
|
+ &::after {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ left: 50%;
|
|
|
+ bottom: 0;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ width: 48rpx;
|
|
|
+ height: 6rpx;
|
|
|
+ background: $primary;
|
|
|
+ border-radius: 3rpx;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+.filter-btn {
|
|
|
+ margin-left: auto;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ font-size: $font-sm;
|
|
|
+ color: $text-regular;
|
|
|
+ .filter-icon {
|
|
|
+ font-size: 20rpx;
|
|
|
+ margin-right: 8rpx;
|
|
|
+ color: $text-secondary;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 列表 */
|
|
|
+.list {
|
|
|
+ flex: 1;
|
|
|
+ padding: 24rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+.empty {
|
|
|
+ padding: 96rpx 0;
|
|
|
+ text-align: center;
|
|
|
+ color: $text-secondary;
|
|
|
+ font-size: $font-sm;
|
|
|
+}
|
|
|
+.task-card {
|
|
|
+ background: $bg-card;
|
|
|
+ border-radius: $radius-lg;
|
|
|
+ padding: 32rpx;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ box-shadow: $shadow-sm;
|
|
|
+}
|
|
|
+.card-head {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+.task-name {
|
|
|
+ flex: 1;
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: $text-primary;
|
|
|
+ margin-right: 16rpx;
|
|
|
+}
|
|
|
+.info {
|
|
|
+ padding: 8rpx 0;
|
|
|
+}
|
|
|
+.info-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ font-size: $font-sm;
|
|
|
+ line-height: 1.8;
|
|
|
+ & + & { margin-top: 4rpx; }
|
|
|
+}
|
|
|
+.info-label {
|
|
|
+ color: $text-secondary;
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 168rpx;
|
|
|
+}
|
|
|
+.info-value {
|
|
|
+ color: $text-primary;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+.card-foot {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ margin-top: 16rpx;
|
|
|
+ .btn {
|
|
|
+ width: 200rpx;
|
|
|
+ height: 64rpx;
|
|
|
+ font-size: $font-sm;
|
|
|
+ border-radius: $radius-md;
|
|
|
+ }
|
|
|
+}
|
|
|
+.nomore {
|
|
|
+ text-align: center;
|
|
|
+ padding: 32rpx 0;
|
|
|
+ color: $text-placeholder;
|
|
|
+ font-size: $font-xs;
|
|
|
+}
|
|
|
+</style>
|