<template>
|
<view class="nonconforming-form-page">
|
<PageHeader :title="pageTitle" @back="goBack" />
|
|
<scroll-view scroll-y class="content-scroll">
|
<view class="form-section">
|
<view class="form-row">
|
<text class="form-label required">产品名称</text>
|
<view class="selector-trigger" @click="openProductSelector" :class="{ disabled: isEdit }">
|
<text class="selector-text" :class="{ placeholder: !form.productName }">
|
{{ form.productName || '请选择' }}
|
</text>
|
<up-icon name="arrow-right" size="16" color="#999"></up-icon>
|
</view>
|
</view>
|
<view class="form-row">
|
<text class="form-label required">规格型号</text>
|
<view class="selector-trigger" @click="openModelSelector" :class="{ disabled: !form.productId || isEdit }">
|
<text class="selector-text" :class="{ placeholder: !form.model }">
|
{{ form.model || '请选择' }}
|
</text>
|
<up-icon name="arrow-right" size="16" color="#999"></up-icon>
|
</view>
|
</view>
|
<view class="form-row">
|
<text class="form-label">单位</text>
|
<up-input v-model="form.unit" disabled placeholder="自动带出" />
|
</view>
|
<view class="form-row">
|
<text class="form-label required">批号</text>
|
<up-input v-model="form.batchNo" placeholder="请输入" />
|
</view>
|
<view class="form-row">
|
<text class="form-label required">检验类型</text>
|
<view class="selector-trigger" @click="showTypeSelect = true">
|
<text class="selector-text" :class="{ placeholder: form.checkType === undefined || form.checkType === '' }">
|
{{ checkTypeLabel || '请选择' }}
|
</text>
|
<up-icon name="arrow-down" size="14" color="#999"></up-icon>
|
</view>
|
</view>
|
<view class="form-row">
|
<text class="form-label required">检验员</text>
|
<view class="selector-trigger" @click="showUserSelect = true">
|
<text class="selector-text" :class="{ placeholder: !form.checkName }">
|
{{ form.checkName || '请选择' }}
|
</text>
|
<up-icon name="arrow-down" size="14" color="#999"></up-icon>
|
</view>
|
</view>
|
<view class="form-row">
|
<text class="form-label required">检测日期</text>
|
<view class="selector-trigger" @click="openCheckTimePicker">
|
<text class="selector-text" :class="{ placeholder: !form.checkTime }">
|
{{ form.checkTime || '请选择' }}
|
</text>
|
<up-icon name="calendar" size="16" color="#999"></up-icon>
|
</view>
|
</view>
|
<view class="form-row">
|
<text class="form-label required">不合格现象</text>
|
<up-textarea v-model="form.defectivePhenomena" placeholder="请输入" count border="surround" />
|
</view>
|
</view>
|
|
<view class="form-section">
|
<view class="section-title">默认处理信息</view>
|
<view class="form-row">
|
<text class="form-label required">处理结果</text>
|
<up-input :modelValue="dealResultLabel || '报废'" disabled />
|
</view>
|
<view class="form-row">
|
<text class="form-label">处理人</text>
|
<up-input v-model="form.dealName" placeholder="选填" />
|
</view>
|
<view class="form-row">
|
<text class="form-label">处理日期</text>
|
<view class="selector-trigger" @click="openDealTimePicker">
|
<text class="selector-text" :class="{ placeholder: !form.dealTime }">
|
{{ form.dealTime || '请选择' }}
|
</text>
|
<up-icon name="calendar" size="16" color="#999"></up-icon>
|
</view>
|
</view>
|
</view>
|
</scroll-view>
|
|
<view class="bottom-bar">
|
<view class="btn-submit" @click="handleSubmit" :class="{ disabled: submitting }">
|
{{ submitting ? '提交中...' : '提交' }}
|
</view>
|
</view>
|
|
<!-- 产品选择 -->
|
<up-popup :show="showProductPopup" mode="bottom" @close="showProductPopup = false">
|
<view class="popup">
|
<view class="popup-header">
|
<text class="popup-title">选择产品</text>
|
</view>
|
<scroll-view scroll-y class="popup-list">
|
<view
|
v-for="(item, idx) in productOptions"
|
:key="item.value || idx"
|
class="popup-item"
|
@click="selectProduct(item)"
|
>
|
<text class="popup-item-title">{{ item.label }}</text>
|
</view>
|
<view v-if="!productLoading && productOptions.length === 0" class="no-data">暂无数据</view>
|
</scroll-view>
|
</view>
|
</up-popup>
|
|
<!-- 型号选择 -->
|
<up-popup :show="showModelPopup" mode="bottom" @close="showModelPopup = false">
|
<view class="popup">
|
<view class="popup-header">
|
<text class="popup-title">选择规格型号</text>
|
</view>
|
<scroll-view scroll-y class="popup-list">
|
<view
|
v-for="(item, idx) in modelOptions"
|
:key="item.id || idx"
|
class="popup-item"
|
@click="selectModel(item)"
|
>
|
<view class="popup-item-row">
|
<text class="popup-item-title">{{ item.model }}</text>
|
<text class="popup-item-sub">{{ item.unit }}</text>
|
</view>
|
</view>
|
<view v-if="!modelLoading && modelOptions.length === 0" class="no-data">暂无数据</view>
|
</scroll-view>
|
</view>
|
</up-popup>
|
|
<!-- 检验类型 -->
|
<up-action-sheet
|
:actions="checkTypeActions"
|
:show="showTypeSelect"
|
@close="showTypeSelect = false"
|
@select="selectCheckType"
|
title="请选择检验类型"
|
/>
|
|
<!-- 检验员 -->
|
<up-action-sheet
|
:actions="userActions"
|
:show="showUserSelect"
|
@close="showUserSelect = false"
|
@select="selectUser"
|
title="请选择检验员"
|
/>
|
|
<!-- 日期选择器:检测日期 -->
|
<up-datetime-picker
|
:show="showCheckTimePicker"
|
v-model="checkTimeValue"
|
mode="date"
|
@confirm="confirmCheckTime"
|
@cancel="showCheckTimePicker = false"
|
/>
|
<!-- 日期选择器:处理日期 -->
|
<up-datetime-picker
|
:show="showDealTimePicker"
|
v-model="dealTimeValue"
|
mode="date"
|
@confirm="confirmDealTime"
|
@cancel="showDealTimePicker = false"
|
/>
|
</view>
|
</template>
|
|
<script setup>
|
import { computed, reactive, ref } from 'vue'
|
import { onLoad } from '@dcloudio/uni-app'
|
import dayjs from 'dayjs'
|
import PageHeader from '@/components/PageHeader.vue'
|
import { useDict } from '@/utils/dict'
|
import { toast } from '@/utils/common'
|
import { productTreeList, modelList } from '@/api/basicData/product.js'
|
import { userListNoPage } from '@/api/system/user.js'
|
import {
|
getQualityUnqualifiedInfo,
|
qualityUnqualifiedAdd,
|
qualityUnqualifiedUpdate
|
} from '@/api/qualityManagement/nonconformingManagement.js'
|
|
const pageType = ref('add') // add | edit
|
const id = ref('')
|
const submitting = ref(false)
|
|
const isEdit = computed(() => pageType.value === 'edit')
|
const pageTitle = computed(() => (isEdit.value ? '编辑不合格管理' : '新增不合格管理'))
|
|
const { rejection_handling } = useDict('rejection_handling')
|
const dealResultLabel = computed(() => {
|
const list = rejection_handling?.value || []
|
const v = form.dealResult
|
return (list || []).find(it => String(it.value) === String(v))?.label || ''
|
})
|
const getScrapDealResultValue = () => {
|
const list = rejection_handling?.value || []
|
const scrap = (list || []).find(it => String(it?.label ?? '') === '报废')
|
return scrap?.value ?? ''
|
}
|
|
const form = reactive({
|
id: undefined,
|
productId: '',
|
productName: '',
|
productModelId: '',
|
model: '',
|
unit: '',
|
batchNo: '',
|
checkType: undefined,
|
checkName: '',
|
checkTime: '',
|
defectivePhenomena: '',
|
dealResult: '',
|
dealName: '',
|
dealTime: ''
|
})
|
|
// 选择器数据
|
const showProductPopup = ref(false)
|
const productLoading = ref(false)
|
const productOptions = ref([])
|
|
const showModelPopup = ref(false)
|
const modelLoading = ref(false)
|
const modelOptions = ref([])
|
|
const showTypeSelect = ref(false)
|
const checkTypeActions = [
|
{ name: '入厂检', value: 0 },
|
{ name: '车间检', value: 1 },
|
{ name: '出厂检', value: 2 }
|
]
|
const checkTypeLabel = computed(() => {
|
const v = form.checkType
|
return checkTypeActions.find(it => String(it.value) === String(v))?.name || ''
|
})
|
|
const showUserSelect = ref(false)
|
const userActions = ref([])
|
|
const showCheckTimePicker = ref(false)
|
const checkTimeValue = ref(Date.now())
|
const showDealTimePicker = ref(false)
|
const dealTimeValue = ref(Date.now())
|
|
const loadProducts = async () => {
|
productLoading.value = true
|
try {
|
const res = await productTreeList()
|
const list =
|
(Array.isArray(res) ? res : null) ||
|
(Array.isArray(res?.data) ? res.data : null) ||
|
(Array.isArray(res?.records) ? res.records : null) ||
|
(Array.isArray(res?.data?.records) ? res.data.records : null) ||
|
[]
|
// 仅取叶子节点(可选产品);并转为 action 列表
|
const flat = []
|
const walk = (nodes) => {
|
;(nodes || []).forEach(n => {
|
const children = Array.isArray(n?.children) ? n.children : []
|
if (children.length > 0) walk(children)
|
else {
|
const value = n?.value ?? n?.id
|
const label = n?.label || n?.productName || n?.name || ''
|
flat.push({ label, value })
|
}
|
})
|
}
|
walk(list)
|
productOptions.value = flat.filter(it => it.value)
|
} finally {
|
productLoading.value = false
|
}
|
}
|
|
const openProductSelector = async () => {
|
if (isEdit.value) return
|
showProductPopup.value = true
|
if (productOptions.value.length === 0) {
|
await loadProducts()
|
}
|
}
|
const selectProduct = (item) => {
|
if (isEdit.value) return
|
form.productId = item.value
|
form.productName = item.label
|
form.productModelId = ''
|
form.model = ''
|
form.unit = ''
|
modelOptions.value = []
|
showProductPopup.value = false
|
}
|
|
const loadModels = async () => {
|
if (!form.productId) return
|
modelLoading.value = true
|
try {
|
const res = await modelList({ id: form.productId })
|
modelOptions.value = Array.isArray(res) ? res : (res?.data || [])
|
} finally {
|
modelLoading.value = false
|
}
|
}
|
const openModelSelector = async () => {
|
if (isEdit.value) return
|
if (!form.productId) {
|
toast('请先选择产品名称')
|
return
|
}
|
showModelPopup.value = true
|
if (modelOptions.value.length === 0) {
|
await loadModels()
|
}
|
}
|
const selectModel = (item) => {
|
if (isEdit.value) return
|
form.productModelId = item?.id
|
form.model = item?.model || ''
|
form.unit = item?.unit || ''
|
showModelPopup.value = false
|
}
|
|
const selectCheckType = (e) => {
|
form.checkType = e.value
|
showTypeSelect.value = false
|
}
|
|
const loadUsers = async () => {
|
const res = await userListNoPage()
|
const list = res?.data || []
|
userActions.value = (list || []).map(u => ({ name: u.nickName, value: u.nickName })).filter(it => it.value)
|
}
|
const selectUser = (e) => {
|
form.checkName = e.value
|
showUserSelect.value = false
|
}
|
|
const openCheckTimePicker = () => {
|
checkTimeValue.value = form.checkTime ? dayjs(form.checkTime, 'YYYY-MM-DD').valueOf() : Date.now()
|
showCheckTimePicker.value = true
|
}
|
const confirmCheckTime = (e) => {
|
const ts = e?.value ?? checkTimeValue.value
|
form.checkTime = dayjs(ts).format('YYYY-MM-DD')
|
showCheckTimePicker.value = false
|
}
|
|
const openDealTimePicker = () => {
|
dealTimeValue.value = form.dealTime ? dayjs(form.dealTime, 'YYYY-MM-DD').valueOf() : Date.now()
|
showDealTimePicker.value = true
|
}
|
const confirmDealTime = (e) => {
|
const ts = e?.value ?? dealTimeValue.value
|
form.dealTime = dayjs(ts).format('YYYY-MM-DD')
|
showDealTimePicker.value = false
|
}
|
|
const loadDetail = async () => {
|
if (!id.value) return
|
const res = await getQualityUnqualifiedInfo(id.value)
|
const d = res?.data || {}
|
Object.assign(form, {
|
id: d.id,
|
productId: d.productId,
|
productName: d.productName,
|
productModelId: d.productModelId,
|
model: d.model,
|
unit: d.unit,
|
batchNo: d.batchNo,
|
// 兼容后端返回字段:优先 checkType,其次 inspectType
|
checkType: d.checkType ?? d.inspectType,
|
checkName: d.checkName,
|
checkTime: d.checkTime,
|
defectivePhenomena: d.defectivePhenomena,
|
dealResult: d.dealResult,
|
dealName: d.dealName,
|
dealTime: d.dealTime
|
})
|
}
|
|
const validate = () => {
|
if (!form.productId) return toast('请选择产品名称'), false
|
if (!form.productModelId) return toast('请选择规格型号'), false
|
if (!form.batchNo) return toast('请输入批号'), false
|
if (form.checkType === undefined || form.checkType === '') return toast('请选择检验类型'), false
|
if (!form.checkName) return toast('请选择检验员'), false
|
if (!form.checkTime) return toast('请选择检测日期'), false
|
if (!form.defectivePhenomena) return toast('请输入不合格现象'), false
|
return true
|
}
|
|
const handleSubmit = async () => {
|
if (submitting.value) return
|
if (!validate()) return
|
submitting.value = true
|
try {
|
// 处理结果默认报废
|
if (!form.dealResult) form.dealResult = getScrapDealResultValue()
|
const payload = { ...form }
|
delete payload.inspectState
|
const api = isEdit.value ? qualityUnqualifiedUpdate : qualityUnqualifiedAdd
|
await api(payload)
|
toast('提交成功')
|
setTimeout(() => uni.navigateBack(), 400)
|
} catch (e) {
|
toast('提交失败')
|
} finally {
|
submitting.value = false
|
}
|
}
|
|
const goBack = () => uni.navigateBack()
|
|
onLoad(async (options) => {
|
pageType.value = options?.type || 'add'
|
id.value = options?.id || ''
|
form.dealResult = getScrapDealResultValue()
|
await loadUsers()
|
if (isEdit.value) {
|
await loadDetail()
|
}
|
})
|
</script>
|
|
<style lang="scss" scoped>
|
.nonconforming-form-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 120rpx; }
|
.content-scroll { height: calc(100vh - 100rpx); }
|
.form-section { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
|
.section-title { font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; }
|
.form-row { margin-bottom: 24rpx; }
|
.form-label { display: block; font-size: 26rpx; color: #666; margin-bottom: 12rpx; }
|
.form-label.required:before { content: "*"; color: #f56c6c; margin-right: 6rpx; }
|
.selector-trigger { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; background: #f5f5f5; border-radius: 12rpx; }
|
.selector-trigger.disabled { opacity: 0.6; }
|
.selector-text { font-size: 28rpx; color: #333; }
|
.selector-text.placeholder { color: #999; }
|
.bottom-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); background: #fff; box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.04); }
|
.btn-submit { height: 88rpx; border-radius: 999rpx; background: #2979ff; color: #fff; font-size: 30rpx; display: flex; align-items: center; justify-content: center; }
|
.btn-submit.disabled { opacity: 0.6; }
|
|
.popup { background: #fff; border-top-left-radius: 16rpx; border-top-right-radius: 16rpx; overflow: hidden; }
|
.popup-header { padding: 24rpx; border-bottom: 1rpx solid #eee; }
|
.popup-title { font-size: 30rpx; font-weight: 500; color: #333; }
|
.popup-list { max-height: 60vh; padding: 12rpx 0; }
|
.popup-item { padding: 22rpx 24rpx; border-bottom: 1rpx solid #f0f0f0; }
|
.popup-item-title { font-size: 28rpx; color: #333; }
|
.popup-item-row { display: flex; justify-content: space-between; gap: 16rpx; }
|
.popup-item-sub { font-size: 24rpx; color: #999; }
|
.no-data { text-align: center; padding: 60rpx 0; color: #999; font-size: 28rpx; }
|
</style>
|