You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

426 lines
19 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="page-container">
<view class="fixed-header">
<NavBar :title="t('moldCheck.detailTitle')" />
</view>
<scroll-view scroll-y class="detail-scroll">
<view class="content-section">
<view class="hero-card">
<view class="hero-body">
<text class="hero-title">{{ textValue(detailData.moldName) }}</text>
<view class="hero-meta-row">
<view class="hero-meta-item">
<text class="hero-meta-label">{{ t('moldCheck.taskType') }}</text>
<text class="hero-meta-value">{{ planTypeText }}</text>
</view>
<view class="hero-meta-item hero-meta-item-right">
<text class="hero-meta-label">{{ t('moldCheck.planNo') }}</text>
<text class="hero-meta-value">{{ textValue(detailData.planNo) }}</text>
</view>
</view>
</view>
</view>
<view class="progress-sticky-wrap">
<view class="progress-card">
<view class="progress-header">
<text class="progress-title">{{ t('moldCheck.progressTitle') }}</text>
<text class="progress-count">{{ completedCount }}/{{ totalCount }}</text>
</view>
<view class="progress-track">
<view class="progress-bar" :style="{ width: `${progressPercent}%` }"></view>
</view>
</view>
</view>
<view v-if="resultLoading" class="hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!resultList.length" class="hint">{{ t('moldCheck.noResultData') }}</view>
<view v-else class="result-list">
<view v-for="(item, index) in resultList" :key="item.id || index" class="result-card">
<view class="result-header">
<view class="result-index">{{ index + 1 }}</view>
<text class="result-title">{{ textValue(item.inspectionItemName) }}</text>
<view v-if="isRequiredItem(item)" class="required-tag">{{ t('moldCheck.requiredText') }}</view>
</view>
<view class="result-fields">
<view class="field-row">
<text class="field-label">{{ t('moldCheck.inspectionMethod') }}</text>
<text class="field-value">{{ inspectionMethodText(item.inspectionMethod) }}</text>
</view>
<view class="field-row">
<text class="field-label">{{ t('moldCheck.judgmentCriteria') }}</text>
<text class="field-value">{{ textValue(item.judgmentCriteria) }}</text>
</view>
<view v-if="shouldShowInput(item) && isEditableItem(item) && isNumericValueType(item.valueType)" class="field-row">
<text class="field-label">{{ t('moldCheck.textInput') }}</text>
<view class="number-box-wrap number-box-wrap-inline">
<uni-number-box
:modelValue="numberInputValue(item)"
:min="-999999999"
:max="999999999"
:step="1"
@change="onNumberInputChange(item, $event)"
/>
</view>
</view>
<view v-else-if="shouldShowInput(item)" class="field-block">
<text class="field-label block-label">{{ t('moldCheck.textInput') }}</text>
<textarea
v-if="isEditableItem(item)"
v-model="item.textInput"
class="form-textarea"
:placeholder="t('moldCheck.inputPlaceholder')"
maxlength="500"
/>
<view v-else class="readonly-box">{{ textInputDisplay(item) }}</view>
</view>
<view class="field-block">
<text class="field-label block-label">{{ t('moldCheck.images') }}</text>
<view class="image-list">
<view v-for="(img, imgIndex) in parseImages(item.images)" :key="imgIndex" class="image-item">
<image
:src="encodeURI(img)"
class="result-image"
mode="aspectFill"
@click="previewImage(encodeURI(img), parseImages(item.images).map(i => encodeURI(i)))"
/>
<view v-if="isEditableItem(item)" class="image-remove" @click="removeImage(item, imgIndex)">×</view>
</view>
<view v-if="isEditableItem(item) && parseImages(item.images).length < 3" class="image-upload" @click="chooseImages(item)">
<text class="image-upload-icon">+</text>
</view>
</view>
</view>
<view v-if="item.remark" class="field-row field-row-top">
<text class="field-label">{{ t('moldCheck.remark') }}</text>
<text class="field-value multiline">{{ textValue(item.remark) }}</text>
</view>
<view class="field-block">
<text class="field-label block-label">{{ t('moldCheck.resultText') }}</text>
<view class="result-option-group">
<view
:class="['result-option', resultOptionClass(item, '1')]"
@click="setDecision(item, '1')"
>
{{ t('moldCheck.inspectionResultPass') }}
</view>
<view
:class="['result-option', resultOptionClass(item, '2')]"
@click="setDecision(item, '2')"
>
{{ t('moldCheck.inspectionResultFail') }}
</view>
<view v-if="isEditableItem(item) && String(item?.inspectionResult || '') === '0'" class="pending-tag">{{ t('moldCheck.inspectionResultPending') }}</view>
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="action-bar">
<view class="action-btn back-btn" @click="goBack">{{ t('dashboard.back') }}</view>
<view v-if="!isExecuted" :class="['action-btn', 'save-btn', saveLoading ? 'action-btn-disabled' : '']" @click="handleSave">{{ t('functionCommon.save') }}</view>
</view>
</view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { batchUpdateMoldCheckResults, getMoldCheckResultsPage, uploadMoldCheckImage } from '@/api/mes/moldCheck'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const { t } = useI18n()
const detailData = reactive({})
const resultList = ref([])
const resultLoading = ref(false)
const managementId = ref('')
const saveLoading = ref(false)
const isExecuted = computed(() => Number(detailData.jobStatus) === 1)
const planTypeText = computed(() => {
const value = String(detailData.planType || '')
if (value === '1') return t('moldCheck.taskTypeInspect')
if (value === '2') return t('moldCheck.taskTypeMaintain')
return textValue(detailData.planType)
})
const totalCount = computed(() => resultList.value.length)
const completedCount = computed(() => resultList.value.filter((item) => isItemCompleted(item)).length)
const progressPercent = computed(() => {
if (!totalCount.value) return 0
return Math.min(100, Math.round((completedCount.value / totalCount.value) * 100))
})
onLoad(async (query) => {
managementId.value = String(query?.id || '')
if (!managementId.value) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
await initAllDict()
try {
const cached = uni.getStorageSync('moldCheckDetail')
if (cached) {
Object.assign(detailData, JSON.parse(cached))
uni.removeStorageSync('moldCheckDetail')
}
} catch (error) {}
await fetchResults()
})
async function fetchResults() {
resultLoading.value = true
try {
const res = await getMoldCheckResultsPage({ managementId: managementId.value })
const root = res && res.data !== undefined ? res.data : res
const items = root?.list || root?.rows || root?.records || root?.data?.list || root?.data?.rows || root?.data?.records || root?.data || root || []
resultList.value = (Array.isArray(items) ? items : []).map((item) => ({
...item,
__editable: !isExecuted.value && String(item?.inspectionResult ?? '0') === '0'
}))
} catch (error) {
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
} finally {
resultLoading.value = false
}
}
function isRequiredItem(item) {
const value = item?.required ?? item?.isRequired ?? item?.needCheck ?? item?.mustCheck
return value === true || value === 1 || value === '1'
}
function isPendingItem(item) {
return String(item?.inspectionResult ?? '') === '0'
}
function isEditableItem(item) {
return !isExecuted.value && item?.__editable === true
}
function shouldShowInput(item) {
const valueType = String(item?.valueType ?? '')
return valueType === '0' || valueType === '2' || !!String(item?.textInput || '').trim()
}
function isNumericValueType(value) {
const normalized = String(value ?? '')
const label = String(valueTypeText(value) || '').toLowerCase()
return normalized === '2' || /数值|数字|number|numeric|digit|decimal/.test(label)
}
function textInputDisplay(item) {
const text = String(item?.textInput || '').trim()
return text || t('moldCheck.inspectionResultPending')
}
function numberInputValue(item) {
const value = Number(item?.textInput)
return Number.isFinite(value) ? value : 0
}
function onNumberInputChange(item, value) {
item.textInput = String(value)
}
function isItemCompleted(item) {
const result = String(item?.inspectionResult ?? '').trim()
return !!result && result !== '0'
}
function textValue(value) {
if (value === 0) return '0'
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
function inspectionMethodText(value) {
return getDictLabel(DICT_TYPE.INSPECTION_METHOD, value, textValue(value))
}
function valueTypeText(value) {
return getDictLabel(DICT_TYPE.VALUE_TYPES, value, textValue(value))
}
function parseImages(value) {
if (!value) return []
if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean)
return String(value).split(',').map((item) => item.trim()).filter(Boolean)
}
function joinImages(value) {
return parseImages(value).join(',')
}
function resultOptionClass(item, value) {
const current = String(item?.inspectionResult || '')
if (current !== String(value)) return ''
return String(value) === '1' ? 'result-option-active' : 'result-option-danger'
}
function setDecision(item, value) {
if (!isEditableItem(item)) return
item.inspectionResult = String(value)
}
async function chooseImages(item) {
try {
const currentCount = parseImages(item.images).length
const remain = Math.max(0, 3 - currentCount)
if (!remain) {
uni.showToast({ title: t('moldCheck.maxUploadCount'), icon: 'none' })
return
}
const res = await uni.chooseImage({ count: remain, sizeType: ['compressed'] })
const files = Array.isArray(res?.tempFilePaths) ? res.tempFilePaths : []
if (!files.length) return
uni.showLoading({ title: t('functionCommon.uploading'), mask: true })
const uploaded = []
for (const filePath of files) {
const uploadRes = await uploadMoldCheckImage(filePath)
const url = String(uploadRes?.data?.fileUrl || uploadRes?.data?.url || uploadRes?.data || uploadRes?.url || '').trim()
if (url) uploaded.push(url)
}
item.images = joinImages([...parseImages(item.images), ...uploaded])
} catch (error) {
const message = String(error?.errMsg || '')
if (!message.includes('cancel')) {
uni.showToast({ title: t('functionCommon.uploadImageFailed'), icon: 'none' })
}
} finally {
uni.hideLoading()
}
}
function removeImage(item, index) {
if (!isEditableItem(item)) return
const next = parseImages(item.images)
next.splice(index, 1)
item.images = next.join(',')
}
function previewImage(current, urls) {
uni.previewImage({ current, urls })
}
function goBack() {
uni.navigateBack()
}
async function handleSave() {
if (saveLoading.value || isExecuted.value) return
const pendingItems = resultList.value.filter((item) => isEditableItem(item))
if (!pendingItems.length) {
uni.navigateBack()
return
}
const hasUnselected = pendingItems.some((item) => String(item?.inspectionResult || '') === '0')
if (hasUnselected) {
uni.showToast({ title: t('moldCheck.selectAllDecisionError'), icon: 'none' })
return
}
const payload = pendingItems.map((item) => ({
...item,
managementId: item.managementId || managementId.value,
inspectionResult: item.inspectionResult,
images: joinImages(item.images),
textInput: normalizeTextInput(item.textInput)
}))
saveLoading.value = true
try {
await batchUpdateMoldCheckResults(payload)
detailData.jobStatus = 1
uni.setStorageSync('moldCheckListNeedRefresh', '1')
uni.showToast({ title: t('functionCommon.saveSuccess'), icon: 'success' })
await fetchResults()
} catch (error) {
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' })
} finally {
saveLoading.value = false
}
}
function normalizeTextInput(value) {
if (value === 0 || value === '0') return '0'
const text = String(value ?? '').trim()
return text || undefined
}
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f5f7fb; }
.fixed-header { position: sticky; top: 0; z-index: 20; }
.detail-scroll { height: calc(100vh - 212rpx); }
.content-section { padding: 20rpx 24rpx 28rpx; }
.hero-card { display: flex; gap: 20rpx; background: #ffffff; border-radius: 24rpx; padding: 24rpx; box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.05); }
.hero-body { flex: 1; min-width: 0; }
.hero-title { display: block; font-size: 42rpx; line-height: 1.2; font-weight: 700; color: #1f2937; }
.hero-meta-row { display: flex; align-items: center; justify-content: space-between; gap: 24rpx; margin-top: 16rpx; }
.hero-meta-item { display: flex; align-items: center; min-width: 0; }
.hero-meta-item-right { justify-content: flex-end; flex-shrink: 0; }
.hero-meta-label { font-size: 25rpx; color: #9ca3af; }
.hero-meta-value { font-size: 25rpx; color: #6b7280; }
.progress-sticky-wrap { position: sticky; top: 0; z-index: 15; padding-top: 20rpx; padding-bottom: 8rpx; background: #f5f7fb; }
.progress-card { background: #ffffff; border-radius: 24rpx; padding: 24rpx; box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.05); }
.progress-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16rpx; }
.progress-title { font-size: 30rpx; font-weight: 600; color: #374151; }
.progress-count { font-size: 40rpx; font-weight: 700; color: #1f7cff; }
.progress-track { height: 16rpx; border-radius: 999rpx; background: #edf2f7; overflow: hidden; }
.progress-bar { height: 100%; border-radius: 999rpx; background: linear-gradient(90deg, #1f7cff, #4da3ff); }
.result-list { margin-top: 20rpx; display: flex; flex-direction: column; gap: 20rpx; }
.result-card { background: #ffffff; border-radius: 24rpx; padding: 24rpx; box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.05); }
.result-header { display: flex; align-items: center; gap: 16rpx; }
.result-index { width: 44rpx; height: 44rpx; border-radius: 50%; background: #1f7cff; color: #ffffff; font-size: 24rpx; font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.result-title { flex: 1; min-width: 0; font-size: 40rpx; font-weight: 700; color: #1f2937; }
.required-tag { padding: 8rpx 16rpx; border-radius: 10rpx; background: #e8fff2; color: #22c55e; font-size: 24rpx; }
.result-fields { margin-top: 16rpx; display: flex; flex-direction: column; gap: 18rpx; }
.field-row { display: flex; justify-content: space-between; align-items: flex-start; gap: 20rpx; }
.field-row-top { align-items: flex-start; }
.field-label { width: 170rpx; font-size: 27rpx; color: #9ca3af; flex-shrink: 0; }
.field-value { flex: 1; text-align: right; font-size: 28rpx; color: #374151; line-height: 1.5; }
.field-value.multiline { text-align: left; }
.field-block { display: flex; flex-direction: column; gap: 14rpx; }
.block-label { width: auto; }
.result-option-group { display: flex; align-items: center; gap: 16rpx; flex-wrap: wrap; }
.result-option { min-width: 160rpx; height: 72rpx; padding: 0 24rpx; border-radius: 14rpx; border: 1rpx solid #d1d5db; color: #6b7280; font-size: 30rpx; display: flex; align-items: center; justify-content: center; background: #ffffff; }
.result-option-active { border-color: #60a5fa; background: #eff6ff; color: #1f7cff; }
.result-option-danger { border-color: #fca5a5; background: #fef2f2; color: #ef4444; }
.pending-tag { padding: 8rpx 14rpx; border-radius: 10rpx; background: #f3f4f6; color: #9ca3af; font-size: 24rpx; }
.readonly-box { min-height: 84rpx; padding: 18rpx 20rpx; border-radius: 14rpx; background: #f8fafc; font-size: 28rpx; color: #374151; line-height: 1.5; }
.form-textarea { width: 100%; min-height: 76rpx; max-height: 108rpx; background: #f8fafc; border-radius: 12rpx; padding: 10rpx 16rpx; font-size: 26rpx; color: #374151; box-sizing: border-box; }
.number-box-wrap { width: 100%; }
.number-box-wrap-inline { width: 320rpx; }
.number-box-wrap :deep(.uni-numbox) { width: 100%; }
.number-box-wrap :deep(.uni-numbox__value) { flex: 1; height: 60rpx; font-size: 24rpx; }
.number-box-wrap :deep(.uni-numbox-btns) { min-width: 60rpx; justify-content: center; }
.number-box-wrap :deep(.uni-numbox--text) { font-size: 18px; }
.image-list { display: flex; flex-wrap: wrap; gap: 12rpx; }
.image-item { position: relative; }
.result-image { width: 140rpx; height: 140rpx; border-radius: 16rpx; background: #f3f4f6; }
.image-remove { position: absolute; top: -12rpx; right: -12rpx; width: 36rpx; height: 36rpx; border-radius: 18rpx; background: rgba(15, 23, 42, 0.72); color: #ffffff; display: flex; align-items: center; justify-content: center; font-size: 24rpx; }
.image-upload { width: 140rpx; height: 140rpx; border-radius: 16rpx; border: 2rpx dashed #cbd5e1; background: #f8fafc; display: flex; align-items: center; justify-content: center; }
.image-upload-icon { font-size: 60rpx; color: #94a3b8; line-height: 1; }
.hint { padding: 48rpx 0; text-align: center; color: #9ca3af; font-size: 26rpx; }
.action-bar { position: fixed; left: 0; right: 0; bottom: 0; display: flex; gap: 18rpx; padding: 18rpx 24rpx calc(18rpx + env(safe-area-inset-bottom)); background: #ffffff; box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.06); }
.action-btn { flex: 1; height: 84rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; font-size: 30rpx; font-weight: 600; }
.back-btn { background: #eef2f7; color: #475569; }
.save-btn { background: #1f4b79; color: #ffffff; }
.action-btn-disabled { background: #94a3b8; }
</style>