Files
shj-app/scanpages/hj/material_sampling.vue
2026-08-20 10:51:21 +08:00

746 lines
22 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="detail-page">
<!-- 焊口主标识 -->
<view class="joint-hero">
<text class="joint-hero__code">{{ info.WeldJointCode || '--' }}</text>
<view class="joint-hero__meta">
<view class="joint-hero__pill">
<text class="pill-label">管线编码</text>
<text class="pill-value">{{ info.PipelineCode || '--' }}</text>
</view>
<view class="joint-hero__pill">
<text class="pill-label">项目名称</text>
<text class="pill-value">{{ info.ProjectName || info.ProjectId || '--' }}</text>
</view>
</view>
</view>
<!-- 下料填报 -->
<view class="section">
<view class="section__header">
<view class="section__bar"></view>
<text class="section__title">下料填报</text>
</view>
<view class="card">
<!-- 下料长度 -->
<view class="card__row" @click="showPaintCodePicker = true">
<text class="card__label">下料长度(mm)</text>
<view class="card__value-wrap">
<input class="card__input" v-model="layingOffForm.CuttingLength" type="digit"
placeholder="请输入下料长度" />
</view>
</view>
<!-- 坡口形式 -->
<view class="card__row">
<text class="card__label">坡口形式</text>
<view class="card__value-wrap">
<input class="card__input" v-model="layingOffForm.GrooveForm" type="text"
placeholder="请输入坡口形式" />
</view>
</view>
<!-- 坡口加工类型 -->
<view class="card__row">
<text class="card__label">坡口加工类型</text>
<view class="card__value-wrap">
<input class="card__input" v-model="layingOffForm.GrooveProcessType" type="text"
placeholder="请输入坡口加工类型" />
</view>
</view>
<!-- 坡口角度 -->
<view class="card__row">
<text class="card__label">坡口角度</text>
<view class="card__value-wrap">
<input class="card__input" v-model="layingOffForm.GrooveAngle" type="digit"
placeholder="请输入坡口角度" />
</view>
</view>
<!-- 材料编码批次是否准确 -->
<view class="card__row">
<text class="card__label">材料编码批次是否准确</text>
<view class="card__value-wrap">
<nbd-check-btn v-model="layingOffForm.IsMaterialCodeBatchNoAccurate" passKey="准确" failKey="不准确" passText="准确"
failText="不准确" />
</view>
</view>
<!-- 材料数量是否准确 -->
<view class="card__row">
<text class="card__label">材料数量是否准确</text>
<view class="card__value-wrap">
<nbd-check-btn v-model="layingOffForm.IsMaterialQuantityAccurate" passKey="准确" failKey="不准确" passText="准确"
failText="不准确" />
</view>
</view>
<!-- 专项审核办理人:材料编码批次或材料数量不准确时必填 -->
<view v-if="needSpecialReviewer" class="card__row" @click="handleShowReviewerSelect">
<text class="card__label required">质检员</text>
<view class="card__value-wrap card__value-wrap--clickable">
<text class="card__value" :class="{ 'card__value--placeholder': !specialReviewerName }">
{{ specialReviewerName || '请选择' }}
</text>
<u-icon name="arrow-right" :size="24" color="#bbb" />
</view>
</view>
<!-- 两侧主编码 -->
<!-- <view class="card__row card__row--stack">
<text class="card__label">两侧主编码</text>
<view v-if="sideMaterials.length" class="side-codes">
<view v-for="(m, idx) in sideMaterials" :key="m.side || idx" class="side-codes__item">
<text class="side-codes__title">侧{{ m.side || (idx + 1) }}</text>
<text class="side-codes__line">上报主编码:{{ m.reported || '--' }}</text>
<text class="side-codes__line">原主编码:{{ m.original || '--' }}</text>
<text v-if="m.batchChanged" class="side-codes__changed">批次已变更</text>
</view>
</view>
<text v-else class="card__value">暂无主编码信息</text>
</view> -->
</view>
</view>
<!-- 检查信息 -->
<view class="section">
<view class="section__header">
<view class="section__bar"></view>
<text class="section__title">检查信息</text>
</view>
<view class="card">
<!-- 检查结果 -->
<view class="card__row">
<text class="card__label">检查结果</text>
<view class="card__value-wrap card__value-wrap--clickable">
<nbd-check-btn v-model="layingOffForm.CheckResult" passKey="合格" failKey="不合格" passText="合格"
failText="不合格" />
</view>
</view>
<view class="fail-section"
:class="{ 'is-visible': failVisible, 'is-enter': failAnimating, 'is-leave': failLeaving }">
<!-- 不合格原因 -->
<view class="card__row card__row--stack">
<text class="card__label">不合格原因</text>
<view class="card__value-wrap">
<textarea class="remark-input" v-model="layingOffForm.UnqualifiedReason" placeholder="请输入"
:maxlength="200" auto-height />
</view>
</view>
<!-- 整改要求 -->
<view class="card__row card__row--stack">
<text class="card__label">整改要求</text>
<view class="card__value-wrap">
<textarea class="remark-input" v-model="layingOffForm.RectifyRequirement" placeholder="请输入"
:maxlength="200" auto-height />
</view>
</view>
</view>
<!-- 备注 -->
<view class="card__row card__row--stack">
<text class="card__label">备注</text>
<view class="card__value-wrap">
<textarea class="remark-input" v-model="layingOffForm.Remark" placeholder="请输入备注信息(选填)"
:maxlength="200" auto-height />
</view>
</view>
<!-- 照片 -->
<view class="card__row card__row--stack">
<text class="card__label">照片</text>
<view class="card__value-wrap">
<nbd-photo-upload ref="photoRef" v-model:attachUrl="formAttachUrl" typeName="AntiRust" />
</view>
</view>
</view>
</view>
<!-- 下料记录 -->
<view class="section">
<nbd-record-card title="下料" :list="cuttingList" :fields="cuttingFields" status-key="CheckResult" status-match="合格" />
</view>
<block v-for="(item, index) in info.preWeldMaterialItems" :key="item.MaterialCode || index">
<!-- 材料信息 -->
<view class="section">
<view class="section__header">
<view class="section__bar"></view>
<text class="section__title">{{ item.MaterialName || item.MaterialCode || '--' }}</text>
</view>
<view class="card">
<view class="card__row">
<text class="card__label">炉号</text>
<text class="card__value">
{{ item.HeatNo || '--' }}
</text>
</view>
<view class="card__row">
<text class="card__label">批号</text>
<text class="card__value">{{ item.BatchNo || '--' }}</text>
</view>
<view class="card__row">
<text class="card__label">规格</text>
<text class="card__value">{{ item.MaterialSpec || '--' }}</text>
</view>
<view class="card__row">
<text class="card__label">单位</text>
<text class="card__value">{{ item.MaterialUnit || '--' }}</text>
</view>
<view class="card__row card__row--stack">
<text class="card__label">材料描述</text>
<text class="card__value">{{ item.MaterialDef || '--' }}</text>
</view>
</view>
<view class="nav-row" @click="handleMutualCheck(item.MaterialCode)">
<view class="nav-row__left">
<view class="nav-row__dot"></view>
<text class="nav-row__label">互检上一工序</text>
</view>
<text class="nav-row__arrow">›</text>
</view>
<!-- 防腐记录 -->
<nbd-record-card title="防腐" :list="antiListMap[item.MaterialCode] || []" :fields="antiFields" :custom-style="{ margin: '0rpx'}" status-key="CheckResult" status-match="合格" />
</view>
</block>
<!-- 提交按钮 -->
<view v-if="canEdit" class="submit-bar">
<view class="btn-primary-gradient" :class="{ 'btn-primary-gradient--disabled': submitting }"
@click="handleSubmit">
<text class="btn-primary-gradient__text">{{ submitting ? '提交中...' : '提交结果' }}</text>
</view>
</view>
<!-- 专项审核办理人选择 -->
<nbd-select v-model="showReviewerPicker" :show-search="true" title="选择质检员" :list="reviewerList"
label-key="BaseInfoName" value-key="BaseInfoId" @confirm="handleConfirmReviewer" />
</view>
</template>
<script setup>
import { ref, watch, nextTick, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { storeToRefs } from 'pinia'
import { useUserStore, useMenuStore } from '@/store'
import { reqPreWeldJointByWeldJointId, reqSaveCuttingCheck, reqCuttingCheckByWeldJointId, reqGetAntiCorrosionCheck, reqGetSpecialReviewers } from '@/api/hj'
const cuttingFields = [
{ label: '下料长度', key: 'CuttingLength' },
{ label: '坡口形式', key: 'GrooveForm' },
{ label: '坡口加工类型', key: 'GrooveProcessType' },
{ label: '坡口角度', key: 'GrooveAngle' },
{ label: '侧1上报主编码', key: 'Side1ReportedMainCode' },
{ label: '侧1原主编码', key: 'Side1OriginalMainCode' },
{ label: '侧2上报主编码', key: 'Side2ReportedMainCode' },
{ label: '侧2原主编码', key: 'Side2OriginalMainCode' },
{ label: '材料编码批次', key: 'IsMaterialCodeBatchNoAccurate', type: 'tag', tagMatch: true, tagPassText: '准确', tagFailText: '不准确' },
{ label: '材料数量', key: 'IsMaterialQuantityAccurate', type: 'tag', tagMatch: true, tagPassText: '准确', tagFailText: '不准确' },
{ label: '质检员', key: 'SpecialReviewerName' },
{ label: '检查人', key: 'CheckPersonName' },
{ label: '检查时间', key: 'CheckTime' },
{ label: '检查结果', key: 'CheckResult', type: 'tag', tagMatch: '合格' },
{ label: '不合格原因', key: 'UnqualifiedReason', showWhen: { key: 'CheckResult', notEqual: '合格' } },
{ label: '整改要求', key: 'RectifyRequirement', showWhen: { key: 'CheckResult', notEqual: '合格' } },
{ label: '照片', key: 'AttachUrl1', type: 'photo', cols: 4 },
]
const antiFields = [
{ label: '防腐等级', key: 'AnticorrosionLevel' },
{ label: '中间漆', key: 'IntermediatePaint' },
{ label: '面漆', key: 'Topcoat' },
{ label: '漆膜厚度', key: 'FilmThickness' },
{ label: '喷砂情况', key: 'SandBlasting' },
{ label: '除锈喷砂情况', key: 'RustSandBlasting' },
{ label: '检查人', key: 'CheckPersonName' },
{ label: '检查时间', key: 'CheckTime' },
{ label: '检查结果', key: 'CheckResult', type: 'tag', tagMatch: '合格' },
{ label: '不合格原因', key: 'UnqualifiedReason', showWhen: { key: 'CheckResult', notEqual: '合格' } },
{ label: '整改要求', key: 'RectifyRequirement', showWhen: { key: 'CheckResult', notEqual: '合格' } },
{ label: '照片', key: 'AttachUrl1', type: 'photo', cols: 4 },
]
const cuttingList = ref([])
const antiListMap = ref({})
const userStore = useUserStore()
const menuStore = useMenuStore()
const { userInfo, currentProject } = storeToRefs(userStore)
const canEdit = computed(() => menuStore.canEditFunc('sampling'))
const info = ref({})
const remark = ref('')
const formAttachUrl = ref('')
const layingOffForm = ref({
CuttingLength: '',
GrooveForm: '',
GrooveProcessType: '',
GrooveAngle: '',
CheckResult: '合格',
IsMaterialCodeBatchNoAccurate: '准确',
IsMaterialQuantityAccurate: '准确',
SpecialReviewerId: '',
UnqualifiedReason: '',
RectifyRequirement: '',
Remark: ''
})
const showReviewerPicker = ref(false)
const reviewerList = ref([])
const specialReviewerName = ref('')
// 材料编码批次或材料数量不准确时,专项审核办理人必填
const needSpecialReviewer = computed(() =>
layingOffForm.value.IsMaterialCodeBatchNoAccurate === '不准确' ||
layingOffForm.value.IsMaterialQuantityAccurate === '不准确'
)
const pickMaterials = (list) => Array.isArray(list) ? list : []
const flattenCuttingRecord = (item) => {
const extra = {}
pickMaterials(item.Materials).forEach((m) => {
const side = m.MaterialSide
if (side === undefined || side === null || side === '') return
extra[`Side${side}ReportedMainCode`] = m.ReportedMainCode || ''
extra[`Side${side}OriginalMainCode`] = m.OriginalMainCode || ''
})
return { ...item, ...extra }
}
const sideMaterials = computed(() => {
const fromCheck = pickMaterials(cuttingList.value[0]?.Materials)
if (fromCheck.length) {
return fromCheck.map((m) => ({
side: m.MaterialSide ?? '',
reported: m.ReportedMainCode || '',
original: m.OriginalMainCode || '',
batchChanged: !!m.IsBatchChanged,
}))
}
const items = info.value.preWeldMaterialItems || []
return items.map((item, i) => ({
side: i + 1,
reported: item.ReportedMainCode || item.MainCode || item.MaterialCode || '',
original: item.OriginalMainCode || '',
batchChanged: !!item.IsBatchChanged,
}))
})
watch(needSpecialReviewer, (need) => {
if (!need) {
layingOffForm.value.SpecialReviewerId = ''
specialReviewerName.value = ''
}
})
// 照片组件引用
const photoRef = ref(null)
const submitting = ref(false)
const failVisible = ref(layingOffForm.value.CheckResult === '不合格')
const failAnimating = ref(false)
const failLeaving = ref(false)
watch(() => layingOffForm.value.CheckResult, async (val) => {
if (val === '不合格') {
failVisible.value = true
await nextTick()
failAnimating.value = true
failLeaving.value = false
} else {
failLeaving.value = true
failAnimating.value = false
setTimeout(() => {
failVisible.value = false
failLeaving.value = false
}, 250)
}
})
const handleShowReviewerSelect = () => {
const projectId = currentProject.value?.ProjectId || info.value?.ProjectId
if (!projectId) {
uni.showToast({ title: '缺少项目信息', icon: 'none' })
return
}
reqGetSpecialReviewers(projectId).then(res => {
reviewerList.value = Array.isArray(res.data) ? res.data : []
showReviewerPicker.value = true
})
}
const handleConfirmReviewer = (item) => {
if (!item) return
layingOffForm.value.SpecialReviewerId = item.BaseInfoId || item.PersonId || ''
specialReviewerName.value = item.BaseInfoName || item.PersonName || ''
showReviewerPicker.value = false
}
const validateSpecialReviewer = () => {
if (!needSpecialReviewer.value) return true
if (!layingOffForm.value.SpecialReviewerId) {
uni.showToast({ title: '请选择质检员', icon: 'none' })
return false
}
return true
}
const handleSubmit = () => {
if (!canEdit.value) {
uni.showToast({ title: '当前岗位仅可查看', icon: 'none' })
return
}
if (submitting.value) return
if (!validateSpecialReviewer()) return
uni.showModal({
title: '确认提交',
content: '确定提交吗?',
success: async (res) => {
if (!res.confirm) return
submitting.value = true
try {
if (photoRef.value?.hasPending()) {
uni.showLoading({ title: '上传照片中...', mask: true })
await photoRef.value.uploadAll()
uni.hideLoading()
}
const CuttingAttachUrl1 = formAttachUrl.value || info.value.CuttingAttachUrl1 || ''
const params = {
CuttingCheckId: null,
ProjectId: currentProject.value?.ProjectId,
WeldJointId: info.value?.WeldJointId,
CuttingLength: layingOffForm.value.CuttingLength,
GrooveForm: layingOffForm.value.GrooveForm,
GrooveProcessType: layingOffForm.value.GrooveProcessType,
GrooveAngle: layingOffForm.value.GrooveAngle,
IsMaterialCodeBatchNoAccurate: layingOffForm.value.IsMaterialCodeBatchNoAccurate === '准确',
IsMaterialQuantityAccurate: layingOffForm.value.IsMaterialQuantityAccurate === '准确',
SpecialReviewerId: needSpecialReviewer.value ? (layingOffForm.value.SpecialReviewerId || null) : null,
CheckPerson: userInfo.value?.PersonId,
CheckTime: uni.formatTime(new Date().getTime(), 'YYYY-MM-DD HH:mm'),
CreateUser: userInfo.value?.PersonId,
CheckResult: layingOffForm.value.CheckResult,
Remark: layingOffForm.value.Remark || '',
UnqualifiedReason: layingOffForm.value.UnqualifiedReason,
RectifyRequirement: layingOffForm.value.RectifyRequirement,
AttachUrl1: CuttingAttachUrl1,
}
await reqSaveCuttingCheck(params)
uni.showToast({ title: '提交成功', icon: 'none' })
setTimeout(() => { uni.navigateBack() }, 1500)
} catch (error) {
console.error('保存下料抽检失败:', error)
} finally {
submitting.value = false
}
}
})
}
onLoad((opt) => {
if (opt.id) {
getInfo(opt.id)
}
})
const getInfo = async (id) => {
try {
uni.showLoading({ title: '加载中...', mask: true })
const res = await reqPreWeldJointByWeldJointId(id)
if (res.code === 1) {
info.value = res.data
// 获取下料记录
if (info.value.WeldJointId) {
const cuttingRes = await reqCuttingCheckByWeldJointId(info.value.WeldJointId)
if (cuttingRes.code === 1 && cuttingRes.data) {
cuttingList.value = (Array.isArray(cuttingRes.data) ? cuttingRes.data : []).map(flattenCuttingRecord)
if (cuttingList.value.length > 0) {
const latest = cuttingList.value[0]
layingOffForm.value.CuttingLength = latest.CuttingLength
layingOffForm.value.GrooveForm = latest.GrooveForm
layingOffForm.value.GrooveProcessType = latest.GrooveProcessType || ''
layingOffForm.value.GrooveAngle = latest.GrooveAngle
layingOffForm.value.IsMaterialCodeBatchNoAccurate = latest.IsMaterialCodeBatchNoAccurate?'准确' : '不准确'
layingOffForm.value.IsMaterialQuantityAccurate = latest.IsMaterialQuantityAccurate?'准确' : '不准确'
if (latest.SpecialReviewerId) {
layingOffForm.value.SpecialReviewerId = latest.SpecialReviewerId
specialReviewerName.value = latest.SpecialReviewerName || ''
}
}
}
}
// 获取防腐记录(每个材料一条)
const materials = info.value.preWeldMaterialItems || []
if (materials.length > 0) {
const antiResults = await Promise.all(
materials.map(m => reqGetAntiCorrosionCheck({
materialCode: m.MaterialCode,
projectId: currentProject.value.ProjectId
}))
)
const map = {}
antiResults.forEach((r, i) => {
if (r.code === 1 && r.data) map[materials[i].MaterialCode] = r.data
})
antiListMap.value = map
}
}
} catch (error) {
console.error('获取焊口详情失败:', error)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
const handleMutualCheck = (id) => {
if (!id) {
uni.showToast({ title: '缺少材料信息', icon: 'none' })
return
}
uni.navigateTo({ url: `/scanpages/hj/do_anti_rust?id=${id}` })
}
</script>
<style lang="scss" scoped>
// 继承全局变量(定义在 uni.scss)
$primary: $app-primary;
$primary-light: $app-primary-light;
$primary-mid: $app-primary-mid;
$primary-dark: $app-primary-dark;
$text: $app-text;
$text-secondary: $app-text-secondary;
$border: $app-border;
$radius: $app-radius;
// 页面专属
$bg: #f0f2f5;
$card-bg: #ffffff;
$shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.detail-page {
background: $bg;
}
// ===== 焊口主标识 =====
.joint-hero {
position: relative;
margin: 24rpx;
padding: 36rpx 32rpx 28rpx;
background: $card-bg;
border-radius: $radius;
box-shadow: $shadow-md;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4rpx;
background: linear-gradient(90deg, $primary, #6ba3ff, $primary);
background-size: 200% 100%;
animation: shimmer 3s ease-in-out infinite;
}
@keyframes shimmer {
0%,
100% {
background-position: 0% 0;
}
50% {
background-position: 100% 0;
}
}
&__code {
display: block;
font-size: 52rpx;
font-weight: 800;
color: $text;
line-height: 1.2;
word-break: break-all;
font-family: monospace;
letter-spacing: 2rpx;
margin-bottom: 28rpx;
position: relative;
padding-left: 24rpx;
&::before {
content: '';
position: absolute;
left: 0;
top: 6rpx;
bottom: 6rpx;
width: 4rpx;
background: $primary;
border-radius: 2rpx;
}
}
&__meta {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
&__pill {
display: inline-flex;
width: 100%;
align-items: center;
gap: 8rpx;
background: $primary-light;
padding: 10rpx 18rpx;
border-radius: 10rpx;
border: 1rpx solid $primary-mid;
.pill-label {
font-size: 22rpx;
color: $text-secondary;
width: 60rpx;
}
.pill-value {
font-size: 24rpx;
font-weight: 600;
color: $primary-dark;
font-family: monospace;
}
}
}
// ===== 提交栏(悬浮底部) =====
.submit-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 24rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: linear-gradient(180deg, rgba(240, 242, 245, 0) 0%, $bg 20%);
}
// ================================================================
// 材料质检区块 —— 三合一 section
// ================================================================
// section 容器:微背景 + 内边距聚合三个子元素
.section {
background: #fafbfc;
border-radius: 20rpx;
padding: 24rpx 0 0;
margin-bottom: 28rpx;
box-shadow: $shadow-md;
}
// ---- 材料详情卡片 ----
// 底部圆角清零,与下方 nav-row 无缝衔接
.card {
border-radius: $radius $radius 0 0;
box-shadow: none;
}
// 编码类字段(炉号 / 批号 / 规格 / 单位)→ monospace
.card__value {
font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
font-size: 28rpx;
font-weight: 500;
color: $text;
letter-spacing: 0.3rpx;
}
// 材料描述 → 回退系统字体
.card__row--stack .card__value {
font-family: inherit;
font-size: 26rpx;
font-weight: 400;
letter-spacing: normal;
color: $text-secondary;
}
// ---- 互检导航按钮 ----
.nav-row {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0;
padding: 24rpx 28rpx;
background: $card-bg;
border-radius: 0;
border-top: 1rpx solid $border;
box-shadow: none;
transition: background 0.2s ease;
margin-bottom: 28rpx;
&:active {
background: #f5f7fa;
}
&__left {
display: flex;
align-items: center;
gap: 14rpx;
}
&__dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background: $primary;
flex-shrink: 0;
}
&__label {
font-size: 28rpx;
font-weight: 600;
color: $text;
letter-spacing: 1rpx;
}
&__arrow {
font-size: 36rpx;
color: $text-secondary;
font-weight: 300;
line-height: 1;
margin-right: -4rpx;
}
}
.side-codes {
width: 100%;
display: flex;
flex-direction: column;
gap: 16rpx;
padding: 8rpx 0 12rpx;
&__item {
display: flex;
flex-direction: column;
gap: 6rpx;
padding: 16rpx 20rpx;
background: #f5f7fa;
border-radius: 12rpx;
}
&__title {
font-size: 26rpx;
font-weight: 600;
color: $text;
}
&__line {
font-size: 24rpx;
color: $text-secondary;
font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
}
&__changed {
font-size: 22rpx;
color: #dc2626;
}
}
</style>