Просмотр исходного кода

feat(app): 3.2.1 品控巡检列表 - 独立页面,独立 API,独立类型

新增文件:
- src/types/qualityInspection.ts: 品控任务类型(字段名对齐后端 MobileInspectionTaskVo)
- src/api/qualityInspection.ts: 品控列表 API + 后端 → 前端字段映射
- src/pages/qualityInspection/list/index.vue: 独立品控巡检列表页面,9 个字段全部展示

修改文件:
- src/pages.json: 注册品控巡检页面
- src/pages/index/index.vue: 添加「品控巡检」菜单入口

特性:
- 实际打卡时间/打卡结果 仅在 已打卡 状态显示
- 打卡状态:已打卡=绿,未打卡=橙
- 打卡结果:正常=绿,异常=橙

Created: 2026-06-21
yangjingjing 1 день назад
Родитель
Сommit
77891a82e7

+ 90 - 0
src/api/qualityInspection.ts

@@ -0,0 +1,90 @@
+// ===== 3.2.1 品控巡检列表 - API =====
+// 独立于 src/api/inspection.ts,调用 /quality/inspectionTask/* 路径
+
+import type { QualityListQuery, QualityInspectionTask } from '@/types/qualityInspection'
+import { request } from '@/utils/request'
+
+// ===== 列表分页响应(对应后端 TableDataInfo) =====
+export interface PageResult<T> {
+  total: number
+  rows: T[]
+  pageNum: number
+  pageSize: number
+}
+
+// ===== 后端 → 前端 punchStatus 规整 =====
+// 后端返回 PENDING/PUNCHED/OVERDUE/FINISHED
+// 前端 UI 仅展示 PENDING/DONE 两态(与 inspection 列表保持一致):
+//   PENDING/OVERDUE → PENDING
+//   PUNCHED/FINISHED → DONE
+function normalizePunchStatus(s: string): QualityInspectionTask['punchStatus'] {
+  if (s === 'PUNCHED' || s === 'FINISHED' || s === 'DONE') return 'DONE'
+  return 'PENDING'
+}
+
+// ===== 打卡方式文字 =====
+function punchTypeName(t: string): string {
+  return t === 'SCAN' ? '扫码打卡' : '常规打卡'
+}
+
+// ===== 打卡结果文字 =====
+function resultLabel(r?: string | null): string {
+  if (r === '0') return '正常'
+  if (r === '1') return '异常'
+  return ''
+}
+
+// ===== 后端原始 → 前端 QualityInspectionTask =====
+function mapRow(raw: any): QualityInspectionTask {
+  const punchStatus = normalizePunchStatus(raw.punchStatus || 'PENDING')
+  return {
+    id: Number(raw.id),
+    taskCode: raw.taskCode || '',
+    taskName: raw.taskName || '',
+    taskType: raw.taskType || '',
+    storeName: raw.storeName || '',
+    inspectorName: raw.inspectorName,
+    punchType: (raw.punchType || 'NORMAL') as QualityInspectionTask['punchType'],
+    location: raw.location || '',
+    lng: raw.lng != null ? Number(raw.lng) : undefined,
+    lat: raw.lat != null ? Number(raw.lat) : undefined,
+    planStartTime: raw.planStartTime,
+    planEndTime: raw.planEndTime || '',
+    punchTime: raw.punchTime,
+    punchStatus,
+    punchResult: raw.punchResult ?? null,
+    qrCode: raw.qrCode,
+    itemCount: raw.itemCount,
+    completedCount: raw.completedCount
+  }
+}
+
+// ===== 后端返回的 punchStatus 字符串 → 后端入参 status =====
+function statusToBackendParam(s: QualityListQuery['status']): string | undefined {
+  if (s === 'ALL' || !s) return undefined
+  if (s === 'PENDING') return 'PENDING'
+  if (s === 'DONE') return 'PUNCHED'
+  return s
+}
+
+// ===== 品控巡检任务列表 =====
+// 后端从 Sa-Token 取当前登录用户作为 inspectorId(已移除前端传参)
+export function listQualityInspectionTasks(query: QualityListQuery): Promise<PageResult<QualityInspectionTask>> {
+  return request<PageResult<QualityInspectionTask>>({
+    url: '/quality/inspectionTask/mobileList',
+    method: 'GET',
+    data: {
+      ...query,
+      status: statusToBackendParam(query.status)
+    }
+  }).then((res: any) => {
+    const rows = (res?.rows ?? []) as any[]
+    return {
+      ...res,
+      rows: rows.map(mapRow)
+    } as PageResult<QualityInspectionTask>
+  })
+}
+
+// ===== 工具导出 =====
+export { punchTypeName, resultLabel }

+ 11 - 0
src/pages.json

@@ -39,6 +39,13 @@
         "navigationBarTitleText": "扫码打卡",
         "navigationBarBackgroundColor": "#000000"
       }
+    },
+    {
+      "path": "pages/qualityInspection/list/index",
+      "style": {
+        "navigationBarTitleText": "品控巡检",
+        "enablePullDownRefresh": true
+      }
     }
   ],
   "globalStyle": {
@@ -53,6 +60,10 @@
       {
         "name": "巡检列表",
         "path": "pages/inspection/list/index"
+      },
+      {
+        "name": "品控巡检列表",
+        "path": "pages/qualityInspection/list/index"
       }
     ]
   }

+ 25 - 0
src/pages/index/index.vue

@@ -28,6 +28,14 @@
         </view>
         <text class="menu-arrow">›</text>
       </view>
+      <view class="menu-item" @tap="goQualityList">
+        <view class="menu-icon icon-quality"></view>
+        <view class="menu-info">
+          <text class="menu-title">品控巡检</text>
+          <text class="menu-desc">品控检查、巡检打卡</text>
+        </view>
+        <text class="menu-arrow">›</text>
+      </view>
     </view>
   </view>
 </template>
@@ -68,6 +76,10 @@ function goList() {
   uni.navigateTo({ url: '/pages/inspection/list/index' })
 }
 
+function goQualityList() {
+  uni.navigateTo({ url: '/pages/qualityInspection/list/index' })
+}
+
 function onLogout() {
   uni.showModal({
     title: '提示',
@@ -189,6 +201,19 @@ function onLogout() {
     color: $primary;
     font-weight: 600;
   }
+  &.icon-quality {
+    background: #FFF1E5;
+    &::after {
+      content: '品';
+      position: absolute;
+      inset: 0;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: $warning;
+      font-weight: 600;
+    }
+  }
 }
 .menu-info {
   flex: 1;

+ 351 - 0
src/pages/qualityInspection/list/index.vue

@@ -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>

+ 47 - 0
src/types/qualityInspection.ts

@@ -0,0 +1,47 @@
+// ===== 3.2.1 品控巡检列表 - 类型定义 =====
+// 独立于 src/types/inspection.ts,字段名直接对齐后端 MobileInspectionTaskVo
+
+// ===== 字典 =====
+export type DictCode = 'quality_inspection_type' | 'punch_type' | 'punch_status' | 'punch_result'
+
+// ===== 打卡方式 =====
+export type PunchType = 'NORMAL' | 'SCAN'   // 常规 / 扫码
+
+// ===== 打卡状态 =====
+// 与现有 inspection 列表一致,前端 status tab 仅展示 PENDING/DONE(后端额外有 PUNCHED/OVERDUE/FINISHED)
+export type PunchStatus = 'PENDING' | 'DONE' | 'PUNCHED' | 'OVERDUE' | 'FINISHED'
+
+// ===== 打卡结果 =====
+export type PunchResult = '0' | '1' | null  // 0 正常 / 1 异常 / null 未打卡
+
+// ===== 品控巡检任务(列表项) =====
+// 字段名直接对齐后端返回,避免后端/前端字段名不一致问题
+export interface QualityInspectionTask {
+  id: number
+  taskCode: string                // 任务编号
+  taskName: string                // 巡检任务名称
+  taskType: string                // 巡检类型(后端字段名)
+  storeName: string               // 门店
+  inspectorName?: string          // 巡检人
+  punchType: PunchType            // 打卡方式 NORMAL/SCAN
+  location: string                // 打卡地点(后端字段名)
+  lng?: number                    // 经度
+  lat?: number                    // 纬度
+  planStartTime?: string          // 计划开始时间
+  planEndTime: string             // 任务完成时间
+  punchTime?: string              // 实际打卡时间
+  punchStatus: PunchStatus        // 打卡状态
+  punchResult?: PunchResult       // 打卡结果
+  qrCode?: string                 // 巡检点二维码
+  itemCount?: number              // 项目总数
+  completedCount?: number         // 已完成项数
+}
+
+// ===== 列表查询参数 =====
+export interface QualityListQuery {
+  status?: 'ALL' | 'PENDING' | 'DONE'   // 全部/未打卡/已打卡
+  keyword?: string                       // 任务名称模糊
+  typeCode?: string                      // 巡检类型字典
+  pageNo?: number
+  pageSize?: number
+}