981 lines
43 KiB
Vue
981 lines
43 KiB
Vue
<template>
|
||||
|
|
<view class="repair-page" :class="`theme-${themeMode}`">
|
|||
|
|
<view class="repair-hero">
|
|||
|
|
<view class="hero-copy">
|
|||
|
|
<text class="hero-tag">维修工作台</text>
|
|||
|
|
<text class="hero-title">维修处理</text>
|
|||
|
|
<text class="hero-subtitle">扫描设备,快速完成故障判断与维修记录</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="hero-icon">
|
|||
|
|
<u-icon name="setting-fill" color="#ffffff" size="46" />
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="summary-grid">
|
|||
|
|
<view v-for="item in summaries" :key="item.label" class="summary-item" @tap="handleSummaryTap(item)">
|
|||
|
|
<text class="summary-value" :class="item.type">{{ item.value }}</text>
|
|||
|
|
<text class="summary-label">{{ item.label }}</text>
|
|||
|
|
<view v-if="item.action === 'pending'" class="summary-arrow">
|
|||
|
|
<u-icon name="arrow-right" color="#1677ff" size="16" />
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="section-card scan-card">
|
|||
|
|
<view class="section-header">
|
|||
|
|
<view>
|
|||
|
|
<text class="section-title">1. 扫描或录入设备</text>
|
|||
|
|
<text class="section-desc">支持订单号、整机编码或设备条码</text>
|
|||
|
|
</view>
|
|||
|
|
<text class="required-tag">必填</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="scan-input-row">
|
|||
|
|
<view class="scan-input">
|
|||
|
|
<u-icon name="search" color="#9aa4b4" size="32" />
|
|||
|
|
<u-input v-model="form.deviceNo" :input-mode="pdaMode ? 'none' : 'text'" placeholder="请输入订单号 / 整机编码" placeholder-style="color: #a0a7b5;" @focus="handleScanInputFocus" @blur="loadRepairTask" />
|
|||
|
|
</view>
|
|||
|
|
<view class="scan-button" @tap="scanDevice">
|
|||
|
|
<u-icon name="scan" color="#ffffff" size="30" />
|
|||
|
|
<text>扫码</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
<view v-if="repairTask" class="device-found">
|
|||
|
|
<view class="device-found-icon"><u-icon name="checkmark" color="#ffffff" size="28" /></view>
|
|||
|
|
<view class="device-found-info">
|
|||
|
|
<text class="device-found-title">已识别维修对象</text>
|
|||
|
|
<text class="device-found-value">{{ repairTask.deviceNo }}</text>
|
|||
|
|
</view>
|
|||
|
|
<text class="device-found-status">{{ repairTask.statusName }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="section-card">
|
|||
|
|
<view class="section-header">
|
|||
|
|
<view>
|
|||
|
|
<text class="section-title">2. 异常信息</text>
|
|||
|
|
<text class="section-desc">来自异常提报的维修任务</text>
|
|||
|
|
</view>
|
|||
|
|
<text class="order-time">{{ repairTask ? repairTask.reportedDate : '待扫描设备' }}</text>
|
|||
|
|
</view>
|
|||
|
|
<view v-if="repairTask" class="exception-info">
|
|||
|
|
<view class="info-row"><text class="info-label">异常类型</text><text class="info-value">{{ repairTask.typeName || '-' }}</text></view>
|
|||
|
|
<view class="info-row"><text class="info-label">异常原因</text><text class="info-value">{{ repairTask.reasonText || '-' }}</text></view>
|
|||
|
|
<view class="info-row"><text class="info-label">提报人员</text><text class="info-value">{{ repairTask.reporterText || '-' }}</text></view>
|
|||
|
|
</view>
|
|||
|
|
<view v-else class="exception-empty">扫描或录入设备后加载异常任务</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="section-card">
|
|||
|
|
<view class="section-header">
|
|||
|
|
<view>
|
|||
|
|
<text class="section-title">3. 维修判定</text>
|
|||
|
|
<text class="section-desc">选择本次维修的不良类型</text>
|
|||
|
|
</view>
|
|||
|
|
<text class="required-tag">必选</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="result-grid">
|
|||
|
|
<view v-for="item in resultOptions" :key="item.value" class="result-item" :class="{ active: form.result === item.value }" @tap="selectRepairResult(item.value)">
|
|||
|
|
<view class="result-icon" :style="{ backgroundColor: form.result === item.value ? item.color : '#f2f5fa' }">
|
|||
|
|
<u-icon :name="item.icon" :color="form.result === item.value ? '#ffffff' : item.color" size="32" />
|
|||
|
|
</view>
|
|||
|
|
<text class="result-label">{{ item.label }}</text>
|
|||
|
|
<text class="result-desc">{{ item.desc }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view v-if="form.result === 'assembly_mismatch'" class="section-card">
|
|||
|
|
<view class="section-header">
|
|||
|
|
<view>
|
|||
|
|
<text class="section-title">4. 搭配错误分类</text>
|
|||
|
|
<text class="section-desc">选择与本次异常对应的物料分类</text>
|
|||
|
|
</view>
|
|||
|
|
<text class="required-tag">必选</text>
|
|||
|
|
</view>
|
|||
|
|
<view v-if="loadingMaterialCategories" class="repair-actions-tip">正在加载物料分类...</view>
|
|||
|
|
<view v-else-if="!mismatchCategoryOptions.length" class="repair-actions-tip">暂无可用物料分类</view>
|
|||
|
|
<view v-else class="repair-action-list">
|
|||
|
|
<view
|
|||
|
|
v-for="item in mismatchCategoryOptions"
|
|||
|
|
:key="item.id"
|
|||
|
|
class="repair-action-item"
|
|||
|
|
:class="{ active: isMismatchCategorySelected(item.id) }"
|
|||
|
|
@tap="toggleMismatchCategory(item)"
|
|||
|
|
>
|
|||
|
|
<u-icon :name="isMismatchCategorySelected(item.id) ? 'checkmark-circle-fill' : 'plus-circle'" :color="isMismatchCategorySelected(item.id) ? '#1677ff' : '#9aa4b4'" size="28" />
|
|||
|
|
<text>{{ item.category_name }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view v-if="form.result === 'operation_defect'" class="section-card responsibility-card">
|
|||
|
|
<view class="section-header">
|
|||
|
|
<view>
|
|||
|
|
<text class="section-title">4. 作业不良责任</text>
|
|||
|
|
<text class="section-desc">选择责任人员,工序步骤随人员记录一并保存</text>
|
|||
|
|
</view>
|
|||
|
|
<text class="required-tag">必选</text>
|
|||
|
|
</view>
|
|||
|
|
<view v-if="loadingOrderRecords" class="repair-actions-tip">正在加载历史扫码记录...</view>
|
|||
|
|
<view v-else-if="!orderScanRecords.length" class="repair-actions-tip">当前订单暂无可定责的历史扫码记录</view>
|
|||
|
|
<view v-else>
|
|||
|
|
<text class="responsibility-title">责任人员</text>
|
|||
|
|
<view class="responsibility-list">
|
|||
|
|
<view v-for="item in orderScanRecords" :key="`operator-${item.id}`" class="responsibility-item" :class="{ active: isResponsibleOperatorSelected(item.id) }" @tap="toggleResponsibleOperator(item.id)">
|
|||
|
|
<u-icon :name="isResponsibleOperatorSelected(item.id) ? 'checkmark-circle-fill' : 'plus-circle'" :color="isResponsibleOperatorSelected(item.id) ? '#1677ff' : '#9aa4b4'" size="28" />
|
|||
|
|
<view><text class="responsibility-name">{{ item.operator || item.userCode || '未记录人员' }}</text><text class="responsibility-meta">{{ item.processName || '未设置工序' }} · {{ item.stepName || '未设置步骤' }}</text></view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="section-card">
|
|||
|
|
<view class="section-header repair-record-header">
|
|||
|
|
<view>
|
|||
|
|
<text class="section-title">{{ form.result === 'material_defect' ? '4. 维修记录' : '5. 维修记录' }}</text>
|
|||
|
|
<text class="section-desc">记录处理过程,便于后续追溯</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="parts-actions">
|
|||
|
|
<text class="parts-action scan-part-action" @tap="scanPart">扫码</text>
|
|||
|
|
<text class="parts-action input-part-action" @tap="inputPart">输入</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
<view class="parts-header">
|
|||
|
|
<text class="parts-title">更换备件</text>
|
|||
|
|
</view>
|
|||
|
|
<view v-for="(part, index) in form.parts" :key="part.id" class="part-row">
|
|||
|
|
<view class="part-index">{{ index + 1 }}</view>
|
|||
|
|
<view class="part-info">
|
|||
|
|
<text class="part-name">{{ part.name }}</text>
|
|||
|
|
<text class="part-code">{{ part.code }}</text>
|
|||
|
|
<text v-if="part.categoryName" class="part-category">{{ part.categoryName }}</text>
|
|||
|
|
<view class="part-source-options">
|
|||
|
|
<view
|
|||
|
|
v-for="item in partSourceOptions"
|
|||
|
|
:key="item.value"
|
|||
|
|
class="part-source-option"
|
|||
|
|
:class="{ active: part.source === item.value }"
|
|||
|
|
@tap="part.source = item.value"
|
|||
|
|
>
|
|||
|
|
{{ item.label }}
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
<view class="part-fault-entry" @tap="openFaultPointPopup(part)">
|
|||
|
|
<text class="part-fault-entry-label">故障点</text>
|
|||
|
|
<text class="part-fault-entry-value">{{ getPartFaultSummary(part) || '请选择' }}</text>
|
|||
|
|
<u-icon name="arrow-right" color="#9aa4b4" size="20" />
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
<view class="part-stepper">
|
|||
|
|
<view class="stepper-button" @tap.stop="changePartQuantity(part, -1)">
|
|||
|
|
<u-icon name="minus" color="#56647a" size="22" />
|
|||
|
|
</view>
|
|||
|
|
<text class="stepper-value">{{ part.quantity }}</text>
|
|||
|
|
<view class="stepper-button" @tap.stop="changePartQuantity(part, 1)">
|
|||
|
|
<u-icon name="plus" color="#1677ff" size="22" />
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
<view class="part-remove" @tap.stop="removePart(index)">
|
|||
|
|
<u-icon name="trash" color="#ef625f" size="30" />
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
<view class="repair-actions-tip">每个备件分别选择对应分类的故障点</view>
|
|||
|
|
<view v-if="form.remark" class="repair-action-summary">{{ form.remark }}</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<u-popup v-model="partInputVisible" mode="center" border-radius="20">
|
|||
|
|
<view class="part-input-dialog">
|
|||
|
|
<text class="part-input-title">输入商品编码</text>
|
|||
|
|
<u-input v-model="partInputCode" :input-mode="pdaMode ? 'none' : 'text'" placeholder="请输入商品编码" border @focus="handleScanInputFocus" />
|
|||
|
|
<view class="part-input-actions">
|
|||
|
|
<view class="part-input-cancel" @tap="closePartInput">取消</view>
|
|||
|
|
<view class="part-input-confirm" @tap="confirmPartInput">确定</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</u-popup>
|
|||
|
|
|
|||
|
|
<u-popup v-model="faultPointPopupVisible" mode="bottom" border-radius="24">
|
|||
|
|
<view class="fault-popup">
|
|||
|
|
<view class="fault-popup-header">
|
|||
|
|
<view>
|
|||
|
|
<text class="fault-popup-title">选择故障点</text>
|
|||
|
|
<text class="fault-popup-subtitle">{{ currentFaultPart ? currentFaultPart.name : '' }}</text>
|
|||
|
|
</view>
|
|||
|
|
<text class="fault-popup-close" @tap="closeFaultPointPopup">关闭</text>
|
|||
|
|
</view>
|
|||
|
|
<view v-if="loadingFaultPoints" class="repair-actions-tip">正在加载故障描述...</view>
|
|||
|
|
<view v-else-if="!currentFaultPartOptions.length" class="repair-actions-tip">当前备件分类暂无故障描述</view>
|
|||
|
|
<scroll-view v-else scroll-y class="fault-popup-scroll">
|
|||
|
|
<view class="repair-action-list fault-popup-list">
|
|||
|
|
<view
|
|||
|
|
v-for="item in currentFaultPartOptions"
|
|||
|
|
:key="item.id"
|
|||
|
|
class="repair-action-item"
|
|||
|
|
:class="{ active: isFaultPointSelected(item.id) }"
|
|||
|
|
@tap="toggleFaultPoint(item)"
|
|||
|
|
>
|
|||
|
|
<u-icon
|
|||
|
|
:name="isFaultPointSelected(item.id) ? 'checkmark-circle-fill' : 'plus-circle'"
|
|||
|
|
:color="isFaultPointSelected(item.id) ? '#1677ff' : '#9aa4b4'"
|
|||
|
|
size="28"
|
|||
|
|
/>
|
|||
|
|
<text>{{ item.description }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</scroll-view>
|
|||
|
|
<view class="fault-popup-footer">
|
|||
|
|
<view class="fault-popup-button fault-popup-clear" @tap="clearCurrentPartFaults">清空</view>
|
|||
|
|
<view class="fault-popup-button fault-popup-confirm" @tap="closeFaultPointPopup">完成</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</u-popup>
|
|||
|
|
|
|||
|
|
<view class="section-card evidence-card">
|
|||
|
|
<view class="section-header">
|
|||
|
|
<view>
|
|||
|
|
<text class="section-title">{{ form.result === 'material_defect' ? '5. 维修凭证' : '6. 维修凭证' }}</text>
|
|||
|
|
<text class="section-desc">可上传维修完成后的现场照片</text>
|
|||
|
|
</view>
|
|||
|
|
<text class="optional-tag">选填</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="evidence-row">
|
|||
|
|
<view v-if="form.images.length < 4" class="upload-box" @tap="chooseImages">
|
|||
|
|
<u-icon name="camera-fill" color="#ff8a3d" size="38" />
|
|||
|
|
<text>拍照上传</text>
|
|||
|
|
</view>
|
|||
|
|
<view v-for="(image, index) in form.images" :key="image" class="evidence-image">
|
|||
|
|
<image :src="image" mode="aspectFill" />
|
|||
|
|
<text class="upload-label">图片{{ index + 1 }}</text>
|
|||
|
|
<view class="upload-delete" @tap.stop="removeImage(index)">
|
|||
|
|
<u-icon name="close" color="#ffffff" size="20" />
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="bottom-actions">
|
|||
|
|
<view class="draft-button" @tap="saveDraft">保存草稿</view>
|
|||
|
|
<view class="complete-button" @tap="completeRepair">完成维修</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
// UTS 原生扫码插件仅在 App-Plus 真机/自定义基座中解析,Web 环境不加载。
|
|||
|
|
// #ifdef APP-PLUS
|
|||
|
|
import { openCodeScan } from '@/uni_modules/mas-code-scan'
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
import { getHardwareFaultDescriptionOptions, getOrderRecentRecords, getScanAppMaterialCategories, lookupMaterial } from '@/api/produce/data-entry.js'
|
|||
|
|
import { getRepairStatistics, getRepairTask, submitRepairRecord } from '@/api/produce/exception.js'
|
|||
|
|
import { uploadImage } from '@/api/upload.js'
|
|||
|
|
import { getScanRegionSetting } from '@/utils/scan-settings.js'
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
partInputVisible: false,
|
|||
|
|
partInputCode: '',
|
|||
|
|
scanning: false,
|
|||
|
|
pdaMode: false,
|
|||
|
|
loadingRepairTask: false,
|
|||
|
|
loadingOrderRecords: false,
|
|||
|
|
submitting: false,
|
|||
|
|
repairTask: null,
|
|||
|
|
orderScanRecords: [],
|
|||
|
|
loadingFaultPoints: false,
|
|||
|
|
loadingMaterialCategories: false,
|
|||
|
|
faultPointRequestKey: '',
|
|||
|
|
faultPointPopupVisible: false,
|
|||
|
|
currentFaultPartId: '',
|
|||
|
|
faultPointOptions: [],
|
|||
|
|
summaries: [
|
|||
|
|
{ label: '待维修', value: 0, type: 'warning', action: 'pending' },
|
|||
|
|
{ label: '处理中', value: 0, type: 'primary' },
|
|||
|
|
{ label: '今日完成', value: 0, type: 'success' }
|
|||
|
|
],
|
|||
|
|
resultOptions: [
|
|||
|
|
{ label: '物料不良', value: 'material_defect', desc: '物料本身存在异常', icon: 'bag-fill', color: '#ef625f' },
|
|||
|
|
{ label: '作业不良', value: 'operation_defect', desc: '作业过程导致异常', icon: 'edit-pen-fill', color: '#f2a93b' },
|
|||
|
|
{ label: '搭配错误', value: 'assembly_mismatch', desc: '物料搭配或装配错误', icon: 'grid-fill', color: '#2f6cf6' }
|
|||
|
|
],
|
|||
|
|
mismatchCategoryOptions: [],
|
|||
|
|
partSourceOptions: [
|
|||
|
|
{ label: '新料', value: 'new' },
|
|||
|
|
{ label: '售后', value: 'after_sales' },
|
|||
|
|
{ label: '返修', value: 'refurbished' }
|
|||
|
|
],
|
|||
|
|
form: {
|
|||
|
|
deviceNo: '',
|
|||
|
|
result: 'material_defect',
|
|||
|
|
remark: '',
|
|||
|
|
parts: [],
|
|||
|
|
responsibleOperatorIds: [],
|
|||
|
|
mismatchCategoryIds: [],
|
|||
|
|
images: [],
|
|||
|
|
status: ''
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
onLoad(options) {
|
|||
|
|
this.loadPdaMode()
|
|||
|
|
this.loadMaterialCategories()
|
|||
|
|
this.loadRepairStatistics()
|
|||
|
|
const deviceNo = String(options?.device_no || '').trim()
|
|||
|
|
if (deviceNo) {
|
|||
|
|
this.form.deviceNo = deviceNo
|
|||
|
|
this.loadRepairTask()
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
onShow() {
|
|||
|
|
this.loadPdaMode()
|
|||
|
|
},
|
|||
|
|
computed: {
|
|||
|
|
currentFaultPart() {
|
|||
|
|
return this.form.parts.find((part) => part.id === this.currentFaultPartId) || null
|
|||
|
|
},
|
|||
|
|
currentFaultPartOptions() {
|
|||
|
|
if (!this.currentFaultPart) return []
|
|||
|
|
return this.faultPointOptions.filter((item) => item.categoryId === String(this.currentFaultPart.categoryId || ''))
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
getCurrentUserCode() {
|
|||
|
|
const user = uni.getStorageSync('scanapp_user') || {}
|
|||
|
|
return user.username || user.nickname || 'U001'
|
|||
|
|
},
|
|||
|
|
loadPdaMode() {
|
|||
|
|
const settings = uni.getStorageSync(`scanapp_settings_${this.getCurrentUserCode()}`)
|
|||
|
|
this.pdaMode = settings?.pdaMode === true
|
|||
|
|
},
|
|||
|
|
handleScanInputFocus() {
|
|||
|
|
// #ifdef APP-PLUS
|
|||
|
|
try {
|
|||
|
|
const activity = plus.android.runtimeMainActivity()
|
|||
|
|
const focusedView = activity.getWindow().getDecorView().findFocus()
|
|||
|
|
if (focusedView) plus.android.invoke(focusedView, 'setShowSoftInputOnFocus', !this.pdaMode)
|
|||
|
|
if (this.pdaMode) uni.hideKeyboard()
|
|||
|
|
} catch (error) {
|
|||
|
|
if (this.pdaMode) uni.hideKeyboard()
|
|||
|
|
}
|
|||
|
|
// #endif
|
|||
|
|
},
|
|||
|
|
handleSummaryTap(item) {
|
|||
|
|
if (item.action !== 'pending') return
|
|||
|
|
uni.navigateTo({ url: '/pages/repair/tasks' })
|
|||
|
|
},
|
|||
|
|
async loadRepairStatistics() {
|
|||
|
|
try {
|
|||
|
|
const user = uni.getStorageSync('scanapp_user') || {}
|
|||
|
|
const userCode = String(user.username || user.user_code || '').trim()
|
|||
|
|
const statistics = await getRepairStatistics({ user_code: userCode })
|
|||
|
|
const data = statistics?.data || statistics || {}
|
|||
|
|
this.summaries = [
|
|||
|
|
{ label: '待维修', value: Number(data.pending || 0), type: 'warning', action: 'pending' },
|
|||
|
|
{ label: '处理中', value: Number(data.processing || 0), type: 'primary' },
|
|||
|
|
{ label: '今日完成', value: Number(data.completed_today || 0), type: 'success' }
|
|||
|
|
]
|
|||
|
|
} catch (error) {
|
|||
|
|
this.summaries = [
|
|||
|
|
{ label: '待维修', value: 0, type: 'warning', action: 'pending' },
|
|||
|
|
{ label: '处理中', value: 0, type: 'primary' },
|
|||
|
|
{ label: '今日完成', value: 0, type: 'success' }
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
async loadMaterialCategories() {
|
|||
|
|
this.loadingMaterialCategories = true
|
|||
|
|
try {
|
|||
|
|
const items = await getScanAppMaterialCategories()
|
|||
|
|
this.mismatchCategoryOptions = (Array.isArray(items) ? items : []).map((item) => ({
|
|||
|
|
id: String(item.id || ''),
|
|||
|
|
category_name: String(item.category_name || item.name || '')
|
|||
|
|
})).filter((item) => item.id && item.category_name)
|
|||
|
|
const availableIds = new Set(this.mismatchCategoryOptions.map((item) => item.id))
|
|||
|
|
this.form.mismatchCategoryIds = this.form.mismatchCategoryIds.filter((id) => availableIds.has(id))
|
|||
|
|
} catch (error) {
|
|||
|
|
this.mismatchCategoryOptions = []
|
|||
|
|
this.form.mismatchCategoryIds = []
|
|||
|
|
uni.showToast({ title: '物料分类加载失败,请稍后重试', icon: 'none' })
|
|||
|
|
} finally {
|
|||
|
|
this.loadingMaterialCategories = false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
async loadRepairTask() {
|
|||
|
|
const deviceNo = this.form.deviceNo.trim()
|
|||
|
|
this.repairTask = null
|
|||
|
|
this.orderScanRecords = []
|
|||
|
|
this.form.responsibleOperatorIds = []
|
|||
|
|
if (!deviceNo || this.loadingRepairTask) return
|
|||
|
|
this.loadingRepairTask = true
|
|||
|
|
try {
|
|||
|
|
const task = await getRepairTask(deviceNo)
|
|||
|
|
if (this.form.deviceNo.trim() !== deviceNo) return
|
|||
|
|
this.form.deviceNo = String(task.device_no || deviceNo)
|
|||
|
|
this.repairTask = {
|
|||
|
|
id: String(task.id || ''),
|
|||
|
|
deviceNo: String(task.device_no || deviceNo),
|
|||
|
|
typeName: String(task.type_name || ''),
|
|||
|
|
reasonText: [task.reason_name, task.issue_name].filter(Boolean).join(' · '),
|
|||
|
|
reporterText: task.reporter ? `${task.reporter}${task.user_code ? `(${task.user_code})` : ''}` : String(task.user_code || ''),
|
|||
|
|
reportedDate: String(task.reported_date || ''),
|
|||
|
|
statusName: String(task.status_name || '待维修')
|
|||
|
|
}
|
|||
|
|
await this.loadOrderScanRecords(this.repairTask.deviceNo)
|
|||
|
|
} catch (error) {
|
|||
|
|
if (this.form.deviceNo.trim() === deviceNo) this.repairTask = null
|
|||
|
|
} finally {
|
|||
|
|
this.loadingRepairTask = false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
async loadOrderScanRecords(orderNo) {
|
|||
|
|
this.loadingOrderRecords = true
|
|||
|
|
try {
|
|||
|
|
const result = await getOrderRecentRecords({ order_no: String(orderNo || '').trim(), limit: 50 })
|
|||
|
|
const items = Array.isArray(result?.items) ? result.items : []
|
|||
|
|
this.orderScanRecords = items.map((item) => ({
|
|||
|
|
id: String(item.id || ''),
|
|||
|
|
processName: String(item.process_name || item.process?.process_name || ''),
|
|||
|
|
stepName: String(item.step_name || item.step?.step_name || ''),
|
|||
|
|
operator: String(item.operator || ''),
|
|||
|
|
userCode: String(item.user_code || '')
|
|||
|
|
})).filter((item) => item.id)
|
|||
|
|
} catch (error) {
|
|||
|
|
this.orderScanRecords = []
|
|||
|
|
} finally {
|
|||
|
|
this.loadingOrderRecords = false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
selectRepairResult(result) {
|
|||
|
|
this.form.result = result
|
|||
|
|
if (result !== 'operation_defect') this.form.responsibleOperatorIds = []
|
|||
|
|
if (result !== 'assembly_mismatch') this.form.mismatchCategoryIds = []
|
|||
|
|
},
|
|||
|
|
isMismatchCategorySelected(categoryId) {
|
|||
|
|
return this.form.mismatchCategoryIds.includes(String(categoryId))
|
|||
|
|
},
|
|||
|
|
toggleMismatchCategory(category) {
|
|||
|
|
const id = String(category.id)
|
|||
|
|
const ids = this.form.mismatchCategoryIds
|
|||
|
|
this.form.mismatchCategoryIds = ids.includes(id) ? ids.filter((item) => item !== id) : [...ids, id]
|
|||
|
|
},
|
|||
|
|
isResponsibleOperatorSelected(scanId) {
|
|||
|
|
return this.form.responsibleOperatorIds.includes(String(scanId))
|
|||
|
|
},
|
|||
|
|
toggleResponsibleOperator(scanId) {
|
|||
|
|
const id = String(scanId)
|
|||
|
|
const ids = this.form.responsibleOperatorIds
|
|||
|
|
this.form.responsibleOperatorIds = ids.includes(id) ? ids.filter((item) => item !== id) : [...ids, id]
|
|||
|
|
},
|
|||
|
|
scanDevice() {
|
|||
|
|
if (this.scanning || this.submitting) return
|
|||
|
|
// #ifdef APP-PLUS
|
|||
|
|
this.scanning = true
|
|||
|
|
openCodeScan({
|
|||
|
|
smartZoom: true,
|
|||
|
|
vibrate: true,
|
|||
|
|
scanRegion: getScanRegionSetting(),
|
|||
|
|
success: async (result) => {
|
|||
|
|
const value = String(result?.content || '').trim()
|
|||
|
|
if (!value) return
|
|||
|
|
this.form.deviceNo = value
|
|||
|
|
await this.loadRepairTask()
|
|||
|
|
},
|
|||
|
|
fail: (error) => this.handleScanFail(error),
|
|||
|
|
complete: () => {
|
|||
|
|
this.scanning = false
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
// #endif
|
|||
|
|
// #ifndef APP-PLUS
|
|||
|
|
uni.showToast({ title: '请在 Android App 中使用扫码功能', icon: 'none' })
|
|||
|
|
// #endif
|
|||
|
|
},
|
|||
|
|
handleScanFail(error) {
|
|||
|
|
const code = Number(error?.errCode)
|
|||
|
|
if (code === 9021003) return
|
|||
|
|
const messageMap = {
|
|||
|
|
9021001: '当前环境不支持扫码,请在 Android 自定义调试基座或正式安装包中使用',
|
|||
|
|
9021002: '相机权限被拒绝,请到系统设置中开启相机权限',
|
|||
|
|
9021004: '未获取到有效扫码结果,请重试'
|
|||
|
|
}
|
|||
|
|
uni.showToast({ title: messageMap[code] || String(error?.errMsg || '扫码插件调用异常'), icon: 'none' })
|
|||
|
|
},
|
|||
|
|
chooseImages() {
|
|||
|
|
if (this.submitting || this.form.images.length >= 4) return
|
|||
|
|
uni.chooseImage({
|
|||
|
|
count: 4 - this.form.images.length,
|
|||
|
|
sizeType: ['compressed'],
|
|||
|
|
sourceType: ['camera', 'album'],
|
|||
|
|
success: (res) => {
|
|||
|
|
this.form.images = [...this.form.images, ...res.tempFilePaths].slice(0, 4)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
removeImage(index) {
|
|||
|
|
if (this.submitting) return
|
|||
|
|
this.form.images.splice(index, 1)
|
|||
|
|
},
|
|||
|
|
getFaultDescriptionById(id) {
|
|||
|
|
return this.faultPointOptions.find((item) => item.id === id)?.description || ''
|
|||
|
|
},
|
|||
|
|
getPartFaultSummary(part) {
|
|||
|
|
return (part.selectedFaultPointIds || [])
|
|||
|
|
.map((id) => this.getFaultDescriptionById(id))
|
|||
|
|
.filter(Boolean)
|
|||
|
|
.join(';')
|
|||
|
|
},
|
|||
|
|
openFaultPointPopup(part) {
|
|||
|
|
if (!part.categoryId) {
|
|||
|
|
uni.showToast({ title: '当前备件缺少分类信息', icon: 'none' })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
this.currentFaultPartId = part.id
|
|||
|
|
this.faultPointPopupVisible = true
|
|||
|
|
},
|
|||
|
|
closeFaultPointPopup() {
|
|||
|
|
this.faultPointPopupVisible = false
|
|||
|
|
this.currentFaultPartId = ''
|
|||
|
|
},
|
|||
|
|
isFaultPointSelected(faultPointId) {
|
|||
|
|
return !!this.currentFaultPart && (this.currentFaultPart.selectedFaultPointIds || []).includes(faultPointId)
|
|||
|
|
},
|
|||
|
|
toggleFaultPoint(faultPoint) {
|
|||
|
|
if (!this.currentFaultPart) return
|
|||
|
|
const selectedFaultPointIds = this.currentFaultPart.selectedFaultPointIds || []
|
|||
|
|
const index = selectedFaultPointIds.indexOf(faultPoint.id)
|
|||
|
|
if (index === -1) {
|
|||
|
|
selectedFaultPointIds.push(faultPoint.id)
|
|||
|
|
} else {
|
|||
|
|
selectedFaultPointIds.splice(index, 1)
|
|||
|
|
}
|
|||
|
|
this.currentFaultPart.selectedFaultPointIds = [...selectedFaultPointIds]
|
|||
|
|
this.syncFaultPointRemark()
|
|||
|
|
},
|
|||
|
|
clearCurrentPartFaults() {
|
|||
|
|
if (!this.currentFaultPart) return
|
|||
|
|
this.currentFaultPart.selectedFaultPointIds = []
|
|||
|
|
this.syncFaultPointRemark()
|
|||
|
|
},
|
|||
|
|
syncFaultPointRemark() {
|
|||
|
|
const summaries = this.form.parts
|
|||
|
|
.map((part, index) => {
|
|||
|
|
const summary = this.getPartFaultSummary(part)
|
|||
|
|
if (!summary) return ''
|
|||
|
|
return `备件${index + 1}:${summary}`
|
|||
|
|
})
|
|||
|
|
.filter(Boolean)
|
|||
|
|
this.form.remark = summaries.join(';')
|
|||
|
|
},
|
|||
|
|
async reloadFaultPointOptions() {
|
|||
|
|
const categoryIds = [...new Set(this.form.parts.map((part) => String(part.categoryId || '')).filter(Boolean))].sort()
|
|||
|
|
const requestKey = categoryIds.join(',')
|
|||
|
|
this.faultPointRequestKey = requestKey
|
|||
|
|
if (!categoryIds.length) {
|
|||
|
|
this.faultPointOptions = []
|
|||
|
|
this.form.parts = this.form.parts.map((part) => ({
|
|||
|
|
...part,
|
|||
|
|
selectedFaultPointIds: []
|
|||
|
|
}))
|
|||
|
|
this.syncFaultPointRemark()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.loadingFaultPoints = true
|
|||
|
|
try {
|
|||
|
|
const items = await getHardwareFaultDescriptionOptions(categoryIds)
|
|||
|
|
if (this.faultPointRequestKey !== requestKey) return
|
|||
|
|
this.faultPointOptions = (Array.isArray(items) ? items : []).map((item) => ({
|
|||
|
|
id: String(item.id),
|
|||
|
|
categoryId: String(item.category_id),
|
|||
|
|
description: String(item.description || ''),
|
|||
|
|
useCount: Number(item.use_count || 0)
|
|||
|
|
}))
|
|||
|
|
const availableIds = new Set(this.faultPointOptions.map((item) => item.id))
|
|||
|
|
this.form.parts = this.form.parts.map((part) => ({
|
|||
|
|
...part,
|
|||
|
|
selectedFaultPointIds: (part.selectedFaultPointIds || []).filter((id) => availableIds.has(id))
|
|||
|
|
}))
|
|||
|
|
this.syncFaultPointRemark()
|
|||
|
|
} catch (error) {
|
|||
|
|
if (this.faultPointRequestKey === requestKey) this.faultPointOptions = []
|
|||
|
|
} finally {
|
|||
|
|
if (this.faultPointRequestKey === requestKey) this.loadingFaultPoints = false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
async removePart(index) {
|
|||
|
|
if (this.form.parts[index] && this.form.parts[index].id === this.currentFaultPartId) {
|
|||
|
|
this.closeFaultPointPopup()
|
|||
|
|
}
|
|||
|
|
this.form.parts.splice(index, 1)
|
|||
|
|
await this.reloadFaultPointOptions()
|
|||
|
|
},
|
|||
|
|
changePartQuantity(part, delta) {
|
|||
|
|
const current = Math.max(1, Number.parseInt(part.quantity, 10) || 1)
|
|||
|
|
part.quantity = String(Math.max(1, current + delta))
|
|||
|
|
},
|
|||
|
|
async addPartByCode(code) {
|
|||
|
|
const productCode = String(code || '').trim()
|
|||
|
|
if (!productCode) return
|
|||
|
|
if (this.form.parts.some((part) => part.code === productCode)) {
|
|||
|
|
uni.showToast({ title: '该物料已添加', icon: 'none' })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
const material = await lookupMaterial(productCode)
|
|||
|
|
this.form.parts.push({
|
|||
|
|
id: `${Date.now()}-${productCode}`,
|
|||
|
|
name: material.product_name,
|
|||
|
|
code: material.product_code || productCode,
|
|||
|
|
categoryId: String(material.category_id || ''),
|
|||
|
|
categoryName: String(material.category_name || ''),
|
|||
|
|
selectedFaultPointIds: [],
|
|||
|
|
source: 'new',
|
|||
|
|
quantity: '1'
|
|||
|
|
})
|
|||
|
|
await this.reloadFaultPointOptions()
|
|||
|
|
} catch (error) {
|
|||
|
|
// 请求层已显示服务端错误提示,避免重复弹出。
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
scanPart() {
|
|||
|
|
if (this.scanning || this.submitting) return
|
|||
|
|
// #ifdef APP-PLUS
|
|||
|
|
this.scanning = true
|
|||
|
|
openCodeScan({
|
|||
|
|
smartZoom: true,
|
|||
|
|
vibrate: true,
|
|||
|
|
scanRegion: getScanRegionSetting(),
|
|||
|
|
success: (result) => {
|
|||
|
|
const value = String(result?.content || '').trim()
|
|||
|
|
if (value) this.addPartByCode(value)
|
|||
|
|
},
|
|||
|
|
fail: (error) => this.handleScanFail(error),
|
|||
|
|
complete: () => {
|
|||
|
|
this.scanning = false
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
// #endif
|
|||
|
|
// #ifndef APP-PLUS
|
|||
|
|
uni.showToast({ title: '请在 Android App 中使用扫码功能', icon: 'none' })
|
|||
|
|
// #endif
|
|||
|
|
},
|
|||
|
|
inputPart() {
|
|||
|
|
this.partInputCode = ''
|
|||
|
|
this.partInputVisible = true
|
|||
|
|
},
|
|||
|
|
closePartInput() {
|
|||
|
|
this.partInputVisible = false
|
|||
|
|
},
|
|||
|
|
async confirmPartInput() {
|
|||
|
|
const productCode = this.partInputCode.trim()
|
|||
|
|
if (!productCode) {
|
|||
|
|
uni.showToast({ title: '请输入商品编码', icon: 'none' })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
this.partInputVisible = false
|
|||
|
|
await this.addPartByCode(productCode)
|
|||
|
|
},
|
|||
|
|
saveDraft() {
|
|||
|
|
uni.showToast({ title: '维修草稿已保存', icon: 'success' })
|
|||
|
|
},
|
|||
|
|
completeRepair() {
|
|||
|
|
if (this.submitting) return
|
|||
|
|
if (!this.form.deviceNo.trim()) {
|
|||
|
|
uni.showToast({ title: '请先扫描或录入设备', icon: 'none' })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (!this.repairTask?.id) {
|
|||
|
|
uni.showToast({ title: '请先加载有效的维修任务', icon: 'none' })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (!this.form.result) {
|
|||
|
|
uni.showToast({ title: '请选择维修判定', icon: 'none' })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (this.form.result === 'operation_defect' && !this.form.responsibleOperatorIds.length) {
|
|||
|
|
uni.showToast({ title: '请选择责任人员', icon: 'none' })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (this.form.result === 'assembly_mismatch' && !this.form.mismatchCategoryIds.length) {
|
|||
|
|
uni.showToast({ title: '请选择搭配错误分类', icon: 'none' })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const user = uni.getStorageSync('scanapp_user') || {}
|
|||
|
|
const userCode = String(user.username || user.user_code || '').trim()
|
|||
|
|
if (!userCode) {
|
|||
|
|
uni.showToast({ title: '登录信息已失效,请重新登录', icon: 'none' })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '确认完成维修',
|
|||
|
|
content: `确定提交设备 ${this.form.deviceNo} 的维修记录吗?`,
|
|||
|
|
confirmText: '确认完成',
|
|||
|
|
success: async (res) => {
|
|||
|
|
if (!res.confirm || this.submitting) return
|
|||
|
|
this.submitting = true
|
|||
|
|
try {
|
|||
|
|
const uploadedImages = []
|
|||
|
|
for (const filePath of this.form.images) {
|
|||
|
|
uploadedImages.push(await uploadImage(filePath, 'repair'))
|
|||
|
|
}
|
|||
|
|
const payload = {
|
|||
|
|
exception_id: this.repairTask.id,
|
|||
|
|
user_code: userCode,
|
|||
|
|
repair_result: this.form.result,
|
|||
|
|
images: uploadedImages,
|
|||
|
|
parts: this.form.parts.map((part) => ({
|
|||
|
|
product_code: String(part.code || '').trim(),
|
|||
|
|
source: part.source || 'new',
|
|||
|
|
quantity: Math.max(1, Number.parseInt(part.quantity, 10) || 1),
|
|||
|
|
fault_description_ids: (part.selectedFaultPointIds || []).map((id) => String(id))
|
|||
|
|
})),
|
|||
|
|
operation_fault_steps: this.form.responsibleOperatorIds,
|
|||
|
|
responsible_operators: this.form.responsibleOperatorIds,
|
|||
|
|
remark: this.form.remark.trim()
|
|||
|
|
}
|
|||
|
|
if (this.form.result === 'assembly_mismatch') {
|
|||
|
|
payload.category_ids = this.form.mismatchCategoryIds
|
|||
|
|
}
|
|||
|
|
await submitRepairRecord(payload)
|
|||
|
|
uni.showToast({ title: '维修记录已完成', icon: 'success' })
|
|||
|
|
this.repairTask = null
|
|||
|
|
this.orderScanRecords = []
|
|||
|
|
this.faultPointOptions = []
|
|||
|
|
this.form.mismatchCategoryIds = []
|
|||
|
|
this.form = {
|
|||
|
|
deviceNo: '',
|
|||
|
|
result: 'material_defect',
|
|||
|
|
remark: '',
|
|||
|
|
parts: [],
|
|||
|
|
responsibleOperatorIds: [],
|
|||
|
|
mismatchCategoryIds: [],
|
|||
|
|
images: []
|
|||
|
|
}
|
|||
|
|
this.loadRepairStatistics()
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('[scanapp] 维修记录提交失败', error)
|
|||
|
|
} finally {
|
|||
|
|
this.submitting = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.repair-page {
|
|||
|
|
min-height: 100vh;
|
|||
|
|
padding: 24rpx 24rpx 210rpx;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
background: linear-gradient(180deg, #eaf4ff 0%, #f4f6fb 24%, #f4f6fb 100%);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.repair-hero {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
padding: 20rpx 8rpx 28rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.hero-copy text { display: block; }
|
|||
|
|
.hero-tag { color: #1677ff; font-size: 20rpx; line-height: 1.35; }
|
|||
|
|
.hero-title { margin-top: 6rpx; color: #17233d; font-size: 38rpx; font-weight: 800; line-height: 1.35; }
|
|||
|
|
.hero-subtitle { margin-top: 8rpx; color: #7d899d; font-size: 20rpx; line-height: 1.4; }
|
|||
|
|
.hero-icon { display: flex; align-items: center; justify-content: center; width: 88rpx; height: 88rpx; border-radius: 28rpx; background: linear-gradient(135deg, #2f6cf6 0%, #35bfa3 100%); box-shadow: 0 16rpx 28rpx rgba(47, 108, 246, 0.2); }
|
|||
|
|
|
|||
|
|
.summary-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12rpx; margin-bottom: 18rpx; }
|
|||
|
|
.summary-item { position: relative; padding: 18rpx 12rpx; border-radius: 20rpx; background: #ffffff; text-align: center; box-shadow: 0 8rpx 24rpx rgba(36, 59, 92, 0.05); }
|
|||
|
|
.summary-arrow { position: absolute; top: 50%; right: 12rpx; display: flex; align-items: center; justify-content: center; width: 34rpx; height: 34rpx; border-radius: 50%; background: #edf4ff; transform: translateY(-50%); }
|
|||
|
|
.summary-value { display: block; font-size: 32rpx; font-weight: 800; line-height: 1.2; }
|
|||
|
|
.summary-value.warning { color: #f2a93b; }
|
|||
|
|
.summary-value.primary { color: #1677ff; }
|
|||
|
|
.summary-value.success { color: #26b36c; }
|
|||
|
|
.summary-label { display: block; margin-top: 8rpx; color: #8a94a6; font-size: 18rpx; line-height: 1.35; }
|
|||
|
|
|
|||
|
|
.section-card { margin-bottom: 18rpx; padding: 24rpx; border-radius: 24rpx; background: #ffffff; box-shadow: 0 8rpx 24rpx rgba(36, 59, 92, 0.05); }
|
|||
|
|
.section-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 14rpx; }
|
|||
|
|
.section-title, .section-desc { display: block; }
|
|||
|
|
.section-title { color: #17233d; font-size: 28rpx; font-weight: 700; line-height: 1.35; }
|
|||
|
|
.section-desc { margin-top: 8rpx; color: #9aa4b4; font-size: 20rpx; line-height: 1.35; }
|
|||
|
|
.required-tag, .optional-tag, .order-time { flex: none; font-size: 18rpx; line-height: 1.35; }
|
|||
|
|
.required-tag { padding: 6rpx 12rpx; border-radius: 999rpx; color: #1677ff; background: #edf4ff; }
|
|||
|
|
.optional-tag, .order-time { color: #9aa4b4; }
|
|||
|
|
|
|||
|
|
.scan-input-row { display: flex; align-items: center; margin-top: 18rpx; }
|
|||
|
|
.scan-input { display: flex; align-items: center; flex: 1; min-width: 0; padding: 0 14rpx; border: 1rpx solid #e7edf5; border-radius: 18rpx; background: #f8fbff; }
|
|||
|
|
.scan-button { display: flex; align-items: center; justify-content: center; gap: 8rpx; width: 116rpx; height: 78rpx; margin-left: 14rpx; border-radius: 18rpx; color: #ffffff; background: linear-gradient(135deg, #2f6cf6 0%, #35bfa3 100%); font-size: 22rpx; flex: none; }
|
|||
|
|
.device-found { display: flex; align-items: center; margin-top: 16rpx; padding: 16rpx; border-radius: 18rpx; background: #f0fbf6; }
|
|||
|
|
.device-found-icon { display: flex; align-items: center; justify-content: center; width: 46rpx; height: 46rpx; margin-right: 12rpx; border-radius: 50%; background: #26b36c; }
|
|||
|
|
.device-found-info { flex: 1; min-width: 0; }
|
|||
|
|
.device-found-title, .device-found-value { display: block; }
|
|||
|
|
.device-found-title { color: #26b36c; font-size: 18rpx; line-height: 1.35; }
|
|||
|
|
.device-found-value { margin-top: 4rpx; color: #17233d; font-size: 24rpx; font-weight: 700; word-break: break-all; }
|
|||
|
|
.device-found-status { color: #f2a93b; font-size: 18rpx; }
|
|||
|
|
|
|||
|
|
.exception-info { margin-top: 18rpx; padding: 4rpx 18rpx; border-radius: 18rpx; background: #f8fafc; }
|
|||
|
|
.exception-empty { margin-top: 18rpx; padding: 24rpx 18rpx; border-radius: 18rpx; color: #9aa4b4; background: #f8fafc; font-size: 20rpx; text-align: center; }
|
|||
|
|
.info-row { display: flex; align-items: center; justify-content: space-between; padding: 16rpx 0; border-bottom: 1rpx solid #edf0f5; }
|
|||
|
|
.info-row:last-child { border-bottom: 0; }
|
|||
|
|
.info-label { color: #8a94a6; font-size: 20rpx; }
|
|||
|
|
.info-value { max-width: 62%; color: #2b3853; font-size: 22rpx; font-weight: 600; text-align: right; word-break: break-all; }
|
|||
|
|
|
|||
|
|
.result-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12rpx; margin-top: 18rpx; }
|
|||
|
|
.result-item { padding: 16rpx 8rpx; border: 2rpx solid #edf0f5; border-radius: 18rpx; text-align: center; }
|
|||
|
|
.result-item.active { border-color: #2f6cf6; background: #f4f8ff; }
|
|||
|
|
.result-icon { display: flex; align-items: center; justify-content: center; width: 56rpx; height: 56rpx; margin: 0 auto 10rpx; border-radius: 18rpx; }
|
|||
|
|
.result-label { display: block; color: #2b3853; font-size: 22rpx; font-weight: 700; line-height: 1.35; }
|
|||
|
|
.result-desc { display: block; margin-top: 6rpx; color: #9aa4b4; font-size: 18rpx; line-height: 1.35; }
|
|||
|
|
|
|||
|
|
.repair-actions-tip { margin-top: 18rpx; color: #9aa4b4; font-size: 20rpx; }
|
|||
|
|
.responsibility-title { display: block; margin-top: 20rpx; color: #2b3853; font-size: 22rpx; font-weight: 700; }
|
|||
|
|
.responsibility-list { display: flex; flex-wrap: wrap; margin-top: 12rpx; }
|
|||
|
|
.responsibility-item { display: flex; align-items: center; min-width: 44%; margin: 0 12rpx 12rpx 0; padding: 14rpx; border: 2rpx solid #e7edf5; border-radius: 14rpx; color: #56647a; background: #f8fafc; font-size: 20rpx; }
|
|||
|
|
.responsibility-item.active { border-color: #1677ff; color: #1677ff; background: #edf4ff; }
|
|||
|
|
.responsibility-item > view { margin-left: 8rpx; min-width: 0; }
|
|||
|
|
.responsibility-name, .responsibility-meta { display: block; }
|
|||
|
|
.responsibility-name { color: #2b3853; font-size: 20rpx; font-weight: 600; line-height: 1.35; }
|
|||
|
|
.responsibility-meta { margin-top: 4rpx; color: #9aa4b4; font-size: 17rpx; line-height: 1.35; }
|
|||
|
|
.repair-action-list { display: flex; flex-wrap: wrap; margin-top: 12rpx; }
|
|||
|
|
.repair-action-item { display: flex; align-items: center; min-height: 64rpx; margin: 0 12rpx 12rpx 0; padding: 0 18rpx; border: 2rpx solid #e7edf5; border-radius: 14rpx; color: #56647a; background: #f8fafc; font-size: 22rpx; flex-shrink: 0; }
|
|||
|
|
.repair-action-item text { margin-left: 8rpx; }
|
|||
|
|
.repair-action-item.active { border-color: #1677ff; color: #1677ff; background: #edf4ff; }
|
|||
|
|
.repair-action-summary { margin-top: 2rpx; padding: 16rpx; border-radius: 14rpx; color: #2b3853; background: #f8fbff; font-size: 20rpx; line-height: 1.5; word-break: break-all; }
|
|||
|
|
.fault-popup { max-height: 78vh; padding: 28rpx 24rpx calc(24rpx + env(safe-area-inset-bottom)); box-sizing: border-box; background: #ffffff; }
|
|||
|
|
.fault-popup-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16rpx; }
|
|||
|
|
.fault-popup-title, .fault-popup-subtitle { display: block; }
|
|||
|
|
.fault-popup-title { color: #17233d; font-size: 28rpx; font-weight: 700; line-height: 1.35; }
|
|||
|
|
.fault-popup-subtitle { margin-top: 8rpx; color: #7d899d; font-size: 20rpx; line-height: 1.35; }
|
|||
|
|
.fault-popup-close { color: #1677ff; font-size: 22rpx; line-height: 1.35; flex-shrink: 0; }
|
|||
|
|
.fault-popup-scroll { max-height: 48vh; margin-top: 20rpx; }
|
|||
|
|
.fault-popup-list { margin-top: 0; }
|
|||
|
|
.fault-popup-footer { display: flex; align-items: center; margin-top: 12rpx; }
|
|||
|
|
.fault-popup-button { display: flex; align-items: center; justify-content: center; height: 78rpx; border-radius: 18rpx; font-size: 24rpx; font-weight: 700; }
|
|||
|
|
.fault-popup-clear { width: 30%; color: #56647a; background: #f0f3f8; }
|
|||
|
|
.fault-popup-confirm { flex: 1; margin-left: 14rpx; color: #ffffff; background: #1677ff; }
|
|||
|
|
.repair-record-header { align-items: center; }
|
|||
|
|
.parts-header { margin-top: 24rpx; }
|
|||
|
|
.parts-title { color: #2b3853; font-size: 24rpx; font-weight: 700; }
|
|||
|
|
.part-source-options { display: flex; align-items: center; width: fit-content; margin-top: 10rpx; overflow: hidden; border: 1rpx solid #dce6f3; border-radius: 12rpx; }
|
|||
|
|
.part-source-option { min-width: 88rpx; padding: 8rpx 14rpx; color: #7d899d; background: #ffffff; font-size: 18rpx; line-height: 1.35; text-align: center; }
|
|||
|
|
.part-source-option + .part-source-option { border-left: 1rpx solid #dce6f3; }
|
|||
|
|
.part-source-option.active { color: #ffffff; background: #1677ff; }
|
|||
|
|
.parts-actions { display: flex; align-items: center; flex: none; }
|
|||
|
|
.parts-action { display: flex; align-items: center; justify-content: center; width: 100rpx; height: 64rpx; border-radius: 16rpx; font-size: 24rpx; flex-shrink: 0; }
|
|||
|
|
.scan-part-action { color: #ffffff; background: #1677ff; }
|
|||
|
|
.input-part-action { margin-left: 16rpx; color: #1677ff; background: #edf4ff; }
|
|||
|
|
.part-row { display: flex; align-items: center; margin-top: 12rpx; padding: 12rpx; border-radius: 16rpx; background: #f8fafc; }
|
|||
|
|
.part-index { display: flex; align-items: center; justify-content: center; width: 42rpx; height: 42rpx; margin-right: 12rpx; border-radius: 50%; color: #1677ff; background: #eaf2ff; font-size: 20rpx; }
|
|||
|
|
.part-info { flex: 1; min-width: 0; }
|
|||
|
|
.part-name, .part-code, .part-category { display: block; }
|
|||
|
|
.part-name { color: #2b3853; font-size: 22rpx; font-weight: 600; }
|
|||
|
|
.part-code { margin-top: 4rpx; color: #9aa4b4; font-size: 18rpx; }
|
|||
|
|
.part-category { margin-top: 4rpx; color: #1677ff; font-size: 18rpx; }
|
|||
|
|
.part-fault-entry { display: flex; align-items: center; margin-top: 12rpx; padding: 14rpx 16rpx; border-radius: 14rpx; background: #ffffff; }
|
|||
|
|
.part-fault-entry-label { color: #7d899d; font-size: 20rpx; line-height: 1.35; flex-shrink: 0; }
|
|||
|
|
.part-fault-entry-value { margin: 0 12rpx; color: #2b3853; font-size: 20rpx; line-height: 1.4; flex: 1; min-width: 0; word-break: break-all; }
|
|||
|
|
.part-stepper { display: flex; align-items: center; width: 172rpx; height: 52rpx; overflow: hidden; border: 1rpx solid #dce6f3; border-radius: 12rpx; background: #ffffff; flex: none; }
|
|||
|
|
.stepper-button { display: flex; align-items: center; justify-content: center; width: 52rpx; height: 52rpx; background: #f8fafc; flex: none; }
|
|||
|
|
.stepper-button + .stepper-value { border-left: 1rpx solid #dce6f3; }
|
|||
|
|
.stepper-value { display: flex; align-items: center; justify-content: center; height: 52rpx; color: #2b3853; font-size: 22rpx; font-weight: 600; flex: 1; }
|
|||
|
|
.stepper-value + .stepper-button { border-left: 1rpx solid #dce6f3; }
|
|||
|
|
.part-remove { display: flex; align-items: center; justify-content: center; width: 52rpx; height: 52rpx; margin-left: 10rpx; flex: none; }
|
|||
|
|
.part-input-dialog { width: 580rpx; padding: 32rpx; box-sizing: border-box; background: #ffffff; }
|
|||
|
|
.part-input-title { display: block; margin-bottom: 24rpx; color: #17233d; font-size: 28rpx; font-weight: 700; }
|
|||
|
|
.part-input-actions { display: flex; justify-content: flex-end; margin-top: 28rpx; }
|
|||
|
|
.part-input-cancel, .part-input-confirm { padding: 14rpx 28rpx; border-radius: 12rpx; font-size: 24rpx; flex-shrink: 0; }
|
|||
|
|
.part-input-cancel { color: #56647a; background: #f0f3f8; }
|
|||
|
|
.part-input-confirm { margin-left: 16rpx; color: #ffffff; background: #1677ff; }
|
|||
|
|
|
|||
|
|
.evidence-row { display: flex; flex-wrap: nowrap; align-items: flex-start; overflow-x: auto; margin-top: 18rpx; padding-bottom: 4rpx; }
|
|||
|
|
.evidence-row > * { margin-right: 14rpx; margin-bottom: 14rpx; }
|
|||
|
|
.upload-box, .evidence-image { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 144rpx; height: 144rpx; border-radius: 20rpx; font-size: 20rpx; }
|
|||
|
|
.upload-box { border: 2rpx dashed #ffb47e; color: #ff8a3d; background: #fffaf6; }
|
|||
|
|
.evidence-image { position: relative; overflow: hidden; color: #ffffff; background: #b7c6d8; }
|
|||
|
|
.evidence-image image { width: 100%; height: 100%; }
|
|||
|
|
.upload-label { position: absolute; right: 8rpx; bottom: 8rpx; padding: 4rpx 8rpx; border-radius: 8rpx; background: rgba(0, 0, 0, 0.45); font-size: 18rpx; line-height: 1.2; }
|
|||
|
|
.upload-delete { position: absolute; top: 8rpx; right: 8rpx; display: flex; align-items: center; justify-content: center; width: 34rpx; height: 34rpx; border-radius: 50%; background: rgba(0, 0, 0, 0.55); }
|
|||
|
|
|
|||
|
|
.bottom-actions { position: fixed; right: 0; bottom: 0; left: 0; z-index: 10; display: flex; padding: 22rpx 24rpx calc(22rpx + env(safe-area-inset-bottom)); background: rgba(255, 255, 255, 0.98); box-shadow: 0 -8rpx 24rpx rgba(36, 59, 92, 0.08); }
|
|||
|
|
.draft-button, .complete-button { display: flex; align-items: center; justify-content: center; height: 82rpx; border-radius: 20rpx; font-size: 24rpx; font-weight: 700; flex-shrink: 0; }
|
|||
|
|
.draft-button { width: 30%; color: #56647a; background: #f0f3f8; }
|
|||
|
|
.complete-button { flex: 1; margin-left: 14rpx; color: #ffffff; background: linear-gradient(135deg, #2f6cf6 0%, #35bfa3 100%); }
|
|||
|
|
.theme-dark.repair-page {
|
|||
|
|
background: #151923;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.theme-dark .summary-item,
|
|||
|
|
.theme-dark .section-card,
|
|||
|
|
.theme-dark .evidence-panel,
|
|||
|
|
.theme-dark .remark-panel,
|
|||
|
|
.theme-dark .fault-popup,
|
|||
|
|
.theme-dark .part-input-dialog,
|
|||
|
|
.theme-dark .bottom-actions {
|
|||
|
|
border-color: #364152;
|
|||
|
|
background: #222936;
|
|||
|
|
box-shadow: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.theme-dark .scan-input,
|
|||
|
|
.theme-dark .device-found,
|
|||
|
|
.theme-dark .exception-info,
|
|||
|
|
.theme-dark .exception-empty,
|
|||
|
|
.theme-dark .result-item,
|
|||
|
|
.theme-dark .responsibility-item,
|
|||
|
|
.theme-dark .repair-action-item,
|
|||
|
|
.theme-dark .repair-action-summary,
|
|||
|
|
.theme-dark .part-row,
|
|||
|
|
.theme-dark .part-fault-entry,
|
|||
|
|
.theme-dark .part-source-option,
|
|||
|
|
.theme-dark .upload-box {
|
|||
|
|
border-color: #364152;
|
|||
|
|
background: #1c2431;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.theme-dark .section-title,
|
|||
|
|
.theme-dark .device-found-value,
|
|||
|
|
.theme-dark .info-value,
|
|||
|
|
.theme-dark .result-label,
|
|||
|
|
.theme-dark .responsibility-title,
|
|||
|
|
.theme-dark .responsibility-name,
|
|||
|
|
.theme-dark .part-name,
|
|||
|
|
.theme-dark .part-fault-entry-value,
|
|||
|
|
.theme-dark .fault-popup-title,
|
|||
|
|
.theme-dark .part-input-title {
|
|||
|
|
color: #f2f5fa;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.theme-dark .section-desc,
|
|||
|
|
.theme-dark .info-label,
|
|||
|
|
.theme-dark .result-desc,
|
|||
|
|
.theme-dark .responsibility-meta,
|
|||
|
|
.theme-dark .repair-actions-tip,
|
|||
|
|
.theme-dark .part-code,
|
|||
|
|
.theme-dark .part-category,
|
|||
|
|
.theme-dark .part-fault-entry-label,
|
|||
|
|
.theme-dark .fault-popup-subtitle {
|
|||
|
|
color: #aeb9ca;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.theme-dark .info-row,
|
|||
|
|
.theme-dark .record-item,
|
|||
|
|
.theme-dark .part-source-option + .part-source-option,
|
|||
|
|
.theme-dark .stepper-button + .stepper-value,
|
|||
|
|
.theme-dark .stepper-value + .stepper-button {
|
|||
|
|
border-color: #364152;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.theme-dark .part-stepper,
|
|||
|
|
.theme-dark .stepper-button {
|
|||
|
|
border-color: #364152;
|
|||
|
|
background: #1c2431;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.theme-dark :deep(.u-input__input),
|
|||
|
|
.theme-dark :deep(textarea) {
|
|||
|
|
color: #f2f5fa !important;
|
|||
|
|
}
|
|||
|
|
</style>
|