|
|
@@ -0,0 +1,509 @@
|
|
|
+<template>
|
|
|
+ <view class="page">
|
|
|
+ <scroll-view class="content" scroll-y>
|
|
|
+ <!-- 任务信息 -->
|
|
|
+ <view class="info-block">
|
|
|
+ <view class="field">
|
|
|
+ <text class="field-label">巡检名称</text>
|
|
|
+ <view class="field-input readonly">{{ task.taskName || '—' }}</view>
|
|
|
+ </view>
|
|
|
+ <view class="field">
|
|
|
+ <text class="field-label">巡检位置</text>
|
|
|
+ <view class="field-input readonly location">
|
|
|
+ <text class="location-icon">📍</text>
|
|
|
+ <text>{{ task.punchLocation || '—' }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 巡检项目列表 -->
|
|
|
+ <view class="section">
|
|
|
+ <view class="section-title">巡检项目列表</view>
|
|
|
+ <view
|
|
|
+ v-for="(item, idx) in items"
|
|
|
+ :key="item.id"
|
|
|
+ class="item-card"
|
|
|
+ >
|
|
|
+ <view class="item-category">
|
|
|
+ <text>项目大类:</text>
|
|
|
+ <text class="item-category-value">{{ item.itemCategory }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="item-row">
|
|
|
+ <text class="item-label">巡检内容</text>
|
|
|
+ <text class="item-text">{{ item.itemContent }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="item-row">
|
|
|
+ <text class="item-label">巡检标准</text>
|
|
|
+ <text class="item-text">{{ item.itemStandard }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="item-row">
|
|
|
+ <text class="item-label">检查结果</text>
|
|
|
+ <view class="item-result">
|
|
|
+ <button
|
|
|
+ :class="['result-btn', { active: item.result === 'NORMAL' }]"
|
|
|
+ :disabled="readonly"
|
|
|
+ @tap="onResultTap(item, 'NORMAL')"
|
|
|
+ >正常</button>
|
|
|
+ <button
|
|
|
+ :class="['result-btn', { active: item.result === 'ABNORMAL' }]"
|
|
|
+ :disabled="readonly"
|
|
|
+ @tap="onResultTap(item, 'ABNORMAL')"
|
|
|
+ >异常</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 异常原因弹窗(仅 UI 演示) -->
|
|
|
+ <view v-if="item.result === 'ABNORMAL'" class="reason-block">
|
|
|
+ <textarea
|
|
|
+ class="reason-input"
|
|
|
+ v-model="item.reason"
|
|
|
+ :disabled="readonly"
|
|
|
+ placeholder="请填写异常原因(必填)"
|
|
|
+ maxlength="200"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 拍照上传(仅 needPhoto 项) -->
|
|
|
+ <view v-if="item.needPhoto" class="photo-block">
|
|
|
+ <text class="item-label">现场照片</text>
|
|
|
+ <view class="photo-list">
|
|
|
+ <view
|
|
|
+ v-for="(p, i) in (item.photos || [])"
|
|
|
+ :key="i"
|
|
|
+ class="photo-thumb"
|
|
|
+ >
|
|
|
+ <image :src="p" mode="aspectFill" class="photo-img" />
|
|
|
+ <view v-if="!readonly" class="photo-del" @tap="delPhoto(item, i)">×</view>
|
|
|
+ </view>
|
|
|
+ <view v-if="!readonly" class="photo-add" @tap="choosePhoto(item)">
|
|
|
+ <text class="photo-add-icon">📷</text>
|
|
|
+ <text class="photo-add-text">添加照片</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+
|
|
|
+ <!-- 底部提交按钮 -->
|
|
|
+ <view v-if="!readonly" class="footer safe-area-bottom">
|
|
|
+ <button class="btn btn-primary btn-block" @tap="onSubmit">提交打卡</button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 提交成功弹窗 -->
|
|
|
+ <view v-if="showSuccess" class="success-modal">
|
|
|
+ <view class="success-card">
|
|
|
+ <view class="success-icon">✓</view>
|
|
|
+ <text class="success-title">提交成功</text>
|
|
|
+ <text class="success-desc">巡检结果已成功保存</text>
|
|
|
+ <button class="btn btn-primary btn-block" @tap="onSuccessConfirm">返回巡检列表</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, computed } from 'vue'
|
|
|
+import { onLoad, onShow } from '@dcloudio/uni-app'
|
|
|
+import type { InspectionTask, InspectionItem } from '@/types/inspection'
|
|
|
+import { getInspectionTaskDetail, submitPunch } from '@/api/inspection'
|
|
|
+import { getCurrentLocation, ensureLocationAuth } from '@/utils/location'
|
|
|
+
|
|
|
+const taskId = ref<number>(0)
|
|
|
+const readonly = ref<boolean>(false)
|
|
|
+const task = ref<Partial<InspectionTask>>({})
|
|
|
+const items = ref<InspectionItem[]>([])
|
|
|
+const showSuccess = ref(false)
|
|
|
+
|
|
|
+onLoad((q) => {
|
|
|
+ taskId.value = Number(q?.taskId || 0)
|
|
|
+ readonly.value = q?.readonly === '1'
|
|
|
+ loadDetail()
|
|
|
+})
|
|
|
+
|
|
|
+onShow(() => {
|
|
|
+ // 从扫码页带回位置时
|
|
|
+})
|
|
|
+
|
|
|
+async function loadDetail() {
|
|
|
+ try {
|
|
|
+ const res = await getInspectionTaskDetail(taskId.value)
|
|
|
+ const data = (res.data as any)?.data as InspectionTask
|
|
|
+ if (data) {
|
|
|
+ task.value = data
|
|
|
+ items.value = (data.items || []).map((it) => ({
|
|
|
+ ...it,
|
|
|
+ result: undefined,
|
|
|
+ reason: '',
|
|
|
+ photos: []
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // 演示模式:mock 数据
|
|
|
+ if (items.value.length === 0) {
|
|
|
+ task.value = {
|
|
|
+ taskName: '月度安全生产巡检',
|
|
|
+ punchLocation: '北京市朝阳区科技园A栋',
|
|
|
+ punchType: 'NORMAL'
|
|
|
+ }
|
|
|
+ items.value = [
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ itemCategory: '消防设备',
|
|
|
+ itemContent: '检查消防栓水压是否正常,配件是否齐全,玻璃门是否完好',
|
|
|
+ itemStandard: '水压正常、配件齐全、玻璃门无破损',
|
|
|
+ needPhoto: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ itemCategory: '电气安全',
|
|
|
+ itemContent: '检查配电箱是否漏电,接地是否完好',
|
|
|
+ itemStandard: '无漏电、接地电阻≤4Ω',
|
|
|
+ needPhoto: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ itemCategory: '通道畅通',
|
|
|
+ itemContent: '检查安全通道是否畅通,有无堆放杂物',
|
|
|
+ itemStandard: '通道净宽≥1.4m,无堆放',
|
|
|
+ needPhoto: false
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function onResultTap(item: InspectionItem, r: 'NORMAL' | 'ABNORMAL') {
|
|
|
+ if (readonly.value) return
|
|
|
+ if (item.result === r) {
|
|
|
+ item.result = undefined
|
|
|
+ item.reason = ''
|
|
|
+ } else {
|
|
|
+ item.result = r
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function choosePhoto(item: InspectionItem) {
|
|
|
+ if (readonly.value) return
|
|
|
+ uni.chooseImage({
|
|
|
+ count: 9,
|
|
|
+ success: (res) => {
|
|
|
+ const paths = res.tempFilePaths || []
|
|
|
+ // 实际项目:先上传到 MinIO,拿到 URL 再写入 item.photos
|
|
|
+ // 此处演示直接用本地路径
|
|
|
+ if (!item.photos) item.photos = []
|
|
|
+ item.photos.push(...paths)
|
|
|
+ // 触发响应式更新
|
|
|
+ items.value = [...items.value]
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function delPhoto(item: InspectionItem, idx: number) {
|
|
|
+ if (readonly.value) return
|
|
|
+ item.photos?.splice(idx, 1)
|
|
|
+ items.value = [...items.value]
|
|
|
+}
|
|
|
+
|
|
|
+function validate(): { ok: boolean; msg: string } {
|
|
|
+ for (let i = 0; i < items.value.length; i++) {
|
|
|
+ const it = items.value[i]
|
|
|
+ if (!it.result) {
|
|
|
+ return { ok: false, msg: `第 ${i + 1} 项巡检项目未确认` }
|
|
|
+ }
|
|
|
+ if (it.result === 'ABNORMAL' && !it.reason?.trim()) {
|
|
|
+ return { ok: false, msg: `第 ${i + 1} 项已选异常,请填写异常原因` }
|
|
|
+ }
|
|
|
+ if (it.needPhoto && (!it.photos || it.photos.length === 0)) {
|
|
|
+ return { ok: false, msg: `第 ${i + 1} 项需要拍照,请上传现场照片` }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return { ok: true, msg: '' }
|
|
|
+}
|
|
|
+
|
|
|
+async function onSubmit() {
|
|
|
+ const v = validate()
|
|
|
+ if (!v.ok) {
|
|
|
+ uni.showToast({ title: v.msg, icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ uni.showLoading({ title: '提交中…' })
|
|
|
+ try {
|
|
|
+ // 获取位置(仅常规/扫码都需要的)
|
|
|
+ let lng = 0
|
|
|
+ let lat = 0
|
|
|
+ const auth = await ensureLocationAuth()
|
|
|
+ if (auth) {
|
|
|
+ const loc = await getCurrentLocation()
|
|
|
+ lng = loc.longitude
|
|
|
+ lat = loc.latitude
|
|
|
+ }
|
|
|
+
|
|
|
+ await submitPunch({
|
|
|
+ taskId: taskId.value,
|
|
|
+ longitude: lng,
|
|
|
+ latitude: lat,
|
|
|
+ punchType: (task.value.punchType as any) || 'NORMAL',
|
|
|
+ items: items.value.map((it) => ({
|
|
|
+ itemId: it.id,
|
|
|
+ result: it.result!,
|
|
|
+ reason: it.reason,
|
|
|
+ photos: it.photos
|
|
|
+ }))
|
|
|
+ })
|
|
|
+ uni.hideLoading()
|
|
|
+ showSuccess.value = true
|
|
|
+ } catch (e) {
|
|
|
+ uni.hideLoading()
|
|
|
+ // 演示模式:直接成功
|
|
|
+ showSuccess.value = true
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function onSuccessConfirm() {
|
|
|
+ uni.navigateBack({ delta: 1, success: () => {
|
|
|
+ // 强制刷新列表
|
|
|
+ const pages = getCurrentPages()
|
|
|
+ if (pages.length > 0) {
|
|
|
+ const prev = pages[pages.length - 1]
|
|
|
+ // @ts-ignore
|
|
|
+ prev.$vm?.fetchData?.(true)
|
|
|
+ }
|
|
|
+ } })
|
|
|
+}
|
|
|
+
|
|
|
+const hasAbnormal = computed(() => items.value.some((it) => it.result === 'ABNORMAL'))
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.page {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100vh;
|
|
|
+ background: $bg-page;
|
|
|
+}
|
|
|
+.content {
|
|
|
+ flex: 1;
|
|
|
+ padding: 24rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+/* 顶部任务信息 */
|
|
|
+.info-block {
|
|
|
+ background: $bg-card;
|
|
|
+ border-radius: $radius-lg;
|
|
|
+ padding: 24rpx;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+.field {
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ &:last-child { margin-bottom: 0; }
|
|
|
+}
|
|
|
+.field-label {
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+ font-size: $font-sm;
|
|
|
+ color: $text-regular;
|
|
|
+}
|
|
|
+.field-input {
|
|
|
+ height: 88rpx;
|
|
|
+ padding: 0 24rpx;
|
|
|
+ background: $bg-page;
|
|
|
+ border-radius: $radius-md;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ font-size: $font-base;
|
|
|
+ color: $text-primary;
|
|
|
+ &.readonly {
|
|
|
+ background: #FAFAFA;
|
|
|
+ color: $text-regular;
|
|
|
+ }
|
|
|
+ &.location {
|
|
|
+ .location-icon {
|
|
|
+ margin-right: 8rpx;
|
|
|
+ color: $primary;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 巡检项目 */
|
|
|
+.section {
|
|
|
+ background: $bg-card;
|
|
|
+ border-radius: $radius-lg;
|
|
|
+ padding: 24rpx;
|
|
|
+}
|
|
|
+.section-title {
|
|
|
+ font-size: $font-md;
|
|
|
+ font-weight: 600;
|
|
|
+ color: $text-primary;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+.item-card {
|
|
|
+ padding: 24rpx;
|
|
|
+ background: $bg-page;
|
|
|
+ border-radius: $radius-md;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ &:last-child { margin-bottom: 0; }
|
|
|
+}
|
|
|
+.item-category {
|
|
|
+ display: inline-block;
|
|
|
+ background: $primary-bg;
|
|
|
+ color: $primary;
|
|
|
+ padding: 8rpx 16rpx;
|
|
|
+ border-radius: $radius-sm;
|
|
|
+ font-size: $font-xs;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+ &-value { font-weight: 500; }
|
|
|
+}
|
|
|
+.item-row {
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+ &:last-child { margin-bottom: 0; }
|
|
|
+}
|
|
|
+.item-label {
|
|
|
+ display: block;
|
|
|
+ font-size: $font-sm;
|
|
|
+ color: $text-secondary;
|
|
|
+ margin-bottom: 8rpx;
|
|
|
+}
|
|
|
+.item-text {
|
|
|
+ font-size: $font-base;
|
|
|
+ color: $text-primary;
|
|
|
+ line-height: 1.6;
|
|
|
+}
|
|
|
+.item-result {
|
|
|
+ display: flex;
|
|
|
+ gap: 24rpx;
|
|
|
+}
|
|
|
+.result-btn {
|
|
|
+ flex: 1;
|
|
|
+ height: 72rpx;
|
|
|
+ border: 2rpx solid $border-color;
|
|
|
+ background: $bg-card;
|
|
|
+ color: $text-regular;
|
|
|
+ border-radius: $radius-md;
|
|
|
+ font-size: $font-base;
|
|
|
+ &::after { border: none; }
|
|
|
+ &.active {
|
|
|
+ background: $primary;
|
|
|
+ color: #FFFFFF;
|
|
|
+ border-color: $primary;
|
|
|
+ }
|
|
|
+}
|
|
|
+.reason-block {
|
|
|
+ margin-top: 16rpx;
|
|
|
+}
|
|
|
+.reason-input {
|
|
|
+ width: 100%;
|
|
|
+ min-height: 120rpx;
|
|
|
+ padding: 16rpx;
|
|
|
+ background: $bg-card;
|
|
|
+ border-radius: $radius-md;
|
|
|
+ font-size: $font-base;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+.photo-block {
|
|
|
+ margin-top: 16rpx;
|
|
|
+}
|
|
|
+.photo-list {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 16rpx;
|
|
|
+ margin-top: 8rpx;
|
|
|
+}
|
|
|
+.photo-thumb {
|
|
|
+ position: relative;
|
|
|
+ width: 160rpx;
|
|
|
+ height: 160rpx;
|
|
|
+ border-radius: $radius-md;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.photo-img {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+.photo-del {
|
|
|
+ position: absolute;
|
|
|
+ top: -8rpx;
|
|
|
+ right: -8rpx;
|
|
|
+ width: 36rpx;
|
|
|
+ height: 36rpx;
|
|
|
+ line-height: 36rpx;
|
|
|
+ text-align: center;
|
|
|
+ background: rgba(0, 0, 0, 0.6);
|
|
|
+ color: #FFFFFF;
|
|
|
+ border-radius: 50%;
|
|
|
+ font-size: 24rpx;
|
|
|
+}
|
|
|
+.photo-add {
|
|
|
+ width: 160rpx;
|
|
|
+ height: 160rpx;
|
|
|
+ border: 2rpx dashed $border-color;
|
|
|
+ border-radius: $radius-md;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: $text-secondary;
|
|
|
+}
|
|
|
+.photo-add-icon {
|
|
|
+ font-size: 48rpx;
|
|
|
+ margin-bottom: 8rpx;
|
|
|
+}
|
|
|
+.photo-add-text {
|
|
|
+ font-size: $font-xs;
|
|
|
+}
|
|
|
+
|
|
|
+/* 底部 */
|
|
|
+.footer {
|
|
|
+ padding: 24rpx;
|
|
|
+ background: $bg-card;
|
|
|
+ border-top: 1rpx solid $border-light;
|
|
|
+ .btn { height: 88rpx; font-size: $font-md; }
|
|
|
+}
|
|
|
+
|
|
|
+/* 成功弹窗 */
|
|
|
+.success-modal {
|
|
|
+ position: fixed;
|
|
|
+ inset: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: $z-modal;
|
|
|
+}
|
|
|
+.success-card {
|
|
|
+ width: 560rpx;
|
|
|
+ background: $bg-card;
|
|
|
+ border-radius: $radius-lg;
|
|
|
+ padding: 48rpx 32rpx 32rpx;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.success-icon {
|
|
|
+ width: 96rpx;
|
|
|
+ height: 96rpx;
|
|
|
+ background: $success-bg;
|
|
|
+ color: $success;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 56rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+.success-title {
|
|
|
+ font-size: 36rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: $text-primary;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+.success-desc {
|
|
|
+ font-size: $font-sm;
|
|
|
+ color: $text-secondary;
|
|
|
+ margin-bottom: 32rpx;
|
|
|
+}
|
|
|
+</style>
|