Files
MrleeAdmin/frontend/scanapp/pages/exception/index.vue
T
2026-08-31 11:33:59 +08:00

1454 lines
35 KiB
Vue
Raw 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="exception-page" :class="`theme-${themeMode}`">
<view class="scene-hero">
<view class="hero-content">
<text class="scene-tag">现场提报</text>
<text class="hero-title">异常处理</text>
<text class="hero-subtitle">先扫码,再选处理类型,补充说明后立即提交</text>
</view>
<view class="hero-user">
<text class="hero-user-name">{{ currentUser.name }}</text>
<text class="hero-user-code">{{ currentUser.userCode }}</text>
</view>
</view>
<view class="status-strip">
<view class="status-item">
<text class="status-value danger">{{ summary.pending }}</text>
<text class="status-label">待处理</text>
</view>
<view class="status-item">
<text class="status-value warning">{{ summary.review }}</text>
<text class="status-label">待复核</text>
</view>
<view class="status-item">
<text class="status-value success">{{ summary.done }}</text>
<text class="status-label">已处理</text>
</view>
</view>
<view class="quick-card">
<view class="quick-header">
<text class="section-title">1. 扫描或录入单号</text>
<text class="quick-time">{{ submitTime }}</text>
</view>
<view class="scan-box" @tap="scanOrderNo">
<view class="scan-icon-wrap">
<u-icon name="scan" color="#ffffff" size="38" />
</view>
<view class="scan-text-group">
<text class="scan-text-label">当前单号</text>
<text class="scan-text-value" :class="{ 'is-empty': !form.orderNo }">{{ form.orderNo || '点击扫码,或在下方手工输入单号' }}</text>
</view>
<text v-if="orderValidation.message" class="order-validation" :class="orderValidation.valid ? 'is-valid' : 'is-invalid'">{{ orderValidation.message }}</text>
</view>
<view class="manual-row">
<view class="manual-input-wrap">
<u-input v-model="form.orderNo" :input-mode="pdaMode ? 'none' : 'text'" clearable placeholder="请输入单号 / 条码" placeholder-style="color: #a0a7b5;" @focus="handleOrderInputFocus" @blur="checkOrderNo" @input="resetOrderValidation" />
</view>
<view class="manual-action ghost" @tap="clearOrderNo">清空</view>
<view class="manual-action primary" @tap="scanOrderNo">扫码</view>
</view>
</view>
<view class="form-card">
<view class="field-header">
<text class="section-title">2. 选择处理类型</text>
<text class="field-tip">必选</text>
</view>
<view class="type-grid">
<view v-for="item in typeOptions" :key="item.value" class="type-card" :class="{ 'is-active': form.type === item.value }" @tap="selectType(item.value)">
<text class="type-card-title">{{ item.label }}</text>
<text class="type-card-desc">{{ item.desc || '按异常设置执行处理' }}</text>
</view>
</view>
<view class="field-header section-gap">
<text class="section-title">3. 选择异常原因</text>
<text class="field-tip">快速点选</text>
</view>
<view class="reason-grid">
<view v-for="item in activeReasonOptions" :key="item.value" class="reason-card" :class="{ 'is-active': form.reason === item.value }" @tap="selectReason(item.value)">
{{ item.label }}
</view>
</view>
<view v-if="activeSubReasonOptions.length" class="sub-reason-panel">
<view class="field-header compact-header">
<text class="section-title">3.1 选择具体问题</text>
<text class="field-tip">{{ getLabel(activeReasonOptions, form.reason) }}</text>
</view>
<view class="sub-reason-grid">
<view v-for="item in frequentSubReasonOptions" :key="item.value" class="sub-reason-card" :class="{ 'is-active': form.subReason === item.value }" @tap="selectSubReason(item.value)">
{{ item.label }}
</view>
<view class="sub-reason-card more-sub-reason" @tap="openSubReasonPopup">
更多问题
</view>
</view>
<view class="selected-sub-reason" @tap="openSubReasonPopup">
<text>当前选择:{{ getLabel(activeSubReasonOptions, form.subReason) || '请选择具体问题' }}</text>
<u-icon name="arrow-right" color="#e97020" size="22" />
</view>
</view>
<view class="evidence-panel">
<view class="field-header compact-header">
<text class="section-title">4. 现场取证</text>
<text class="field-link" @tap="fillVoiceRemark">语音速记</text>
</view>
<view class="evidence-grid">
<view v-if="form.images.length < 4" class="shot-box" @tap="chooseImages">
<u-icon name="camera-fill" color="#ff8b38" size="42" />
<text class="shot-title">拍照上传</text>
<text class="shot-desc">最多 4 张</text>
</view>
<view v-for="(item, index) in form.images" :key="item" class="upload-preview">
<image :src="item" 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="remark-panel">
<view class="field-header compact-header">
<text class="section-title">补充说明</text>
<text class="field-tip optional">选填</text>
</view>
<u-input v-model="form.remark" type="textarea" :auto-height="true" maxlength="200" placeholder="补充现场情况、处理建议、责任工位等" placeholder-style="color: #a0a7b5;" />
</view>
</view>
<view class="operator-card">
<view class="operator-row">
<text class="operator-label">提报人</text>
<text class="operator-value">{{ currentUser.name }} / {{ currentUser.userCode }}</text>
</view>
<view class="operator-row">
<text class="operator-label">岗位</text>
<text class="operator-value">{{ currentUser.position }}</text>
</view>
<view class="operator-row keep-row" @tap="keepReporting = !keepReporting">
<text class="operator-label">提交后继续提报</text>
<view class="keep-switch" :class="{ 'is-active': keepReporting }">
<view class="keep-dot"></view>
</view>
</view>
</view>
<view class="record-card">
<view class="field-header compact-header">
<text class="section-title">最近异常</text>
<text class="field-tip optional">现场回看</text>
</view>
<view v-for="item in recentExceptions" :key="item.id" class="record-item">
<view class="record-top">
<view>
<text class="record-main">{{ item.orderNo }}</text>
<text class="record-sub">{{ item.typeLabel }} · {{ item.reasonLabel }}</text>
</view>
<view class="status-pill" :class="item.statusClass">{{ item.statusLabel }}</view>
</view>
<view class="record-bottom">
<text>{{ item.time }}</text>
<text>{{ item.operator }}</text>
</view>
</view>
</view>
<view class="submit-bar">
<view class="submit-tip">{{ keepReporting ? '提交后将保留当前页面,便于继续提报' : '提交后返回当前空白表单' }}</view>
<view class="submit-actions">
<view class="submit-secondary" @tap="resetForm">重置</view>
<view class="submit-primary" :class="{ 'is-loading': submitting }" @tap="submitException">{{ submitting ? '提交中...' : '提交异常' }}</view>
</view>
</view>
<u-popup v-model="subReasonPopupVisible" mode="bottom" :border-radius="20" @close="closeSubReasonPopup">
<view class="sub-reason-popup">
<view class="popup-header">
<view>
<text class="popup-title">选择具体问题</text>
<text class="popup-subtitle">{{ getLabel(activeReasonOptions, form.reason) }} · 共 {{ activeSubReasonOptions.length }} 项</text>
</view>
<view class="popup-close" @tap="closeSubReasonPopup">关闭</view>
</view>
<u-input v-model="subReasonKeyword" clearable prefixIcon="search" placeholder="搜索具体问题" placeholder-style="color: #a0a7b5;" />
<scroll-view scroll-y class="popup-list">
<view v-for="item in filteredSubReasonOptions" :key="item.value" class="popup-option" :class="{ 'is-active': form.subReason === item.value }" @tap="selectSubReason(item.value, true)">
<text>{{ item.label }}</text>
<u-icon v-if="form.subReason === item.value" name="checkmark" color="#e97020" size="28" />
</view>
<view v-if="!filteredSubReasonOptions.length" class="popup-empty">未找到匹配的具体问题</view>
</scroll-view>
</view>
</u-popup>
</view>
</template>
<script>
// UTS 原生扫码插件仅在 App-Plus 真机/自定义基座中解析,Web 环境不加载。
// #ifdef APP-PLUS
import { openCodeScan } from '@/uni_modules/mas-code-scan'
// #endif
import { checkExceptionOrder, getExceptionSettings, getExceptionStatistics, getRecentExceptions, submitExceptionRecord } from '@/api/produce/exception.js'
import { uploadImage } from '@/api/upload.js'
import { getScanRegionSetting } from '@/utils/scan-settings.js'
export default {
data() {
return {
submitting: false,
scanning: false,
pdaMode: false,
checkingOrder: false,
orderValidation: { valid: false, value: '', message: '' },
keepReporting: true,
subReasonPopupVisible: false,
subReasonKeyword: '',
summary: { pending: 0, review: 0, done: 0 },
currentUser: { name: '', userCode: '', position: '' },
submitTime: '',
typeOptions: [],
reasonOptions: {},
subReasonOptions: {},
form: { orderNo: '', type: '', reason: '', subReason: '', remark: '', images: [] },
recentExceptions: []
}
},
computed: {
activeReasonOptions() {
return this.reasonOptions[this.form.type] || []
},
activeSubReasonOptions() {
return this.subReasonOptions[this.form.reason] || []
},
frequentSubReasonOptions() {
return this.activeSubReasonOptions.slice(0, 6)
},
filteredSubReasonOptions() {
const keyword = this.subReasonKeyword.trim().toLowerCase()
return keyword ? this.activeSubReasonOptions.filter((item) => item.label.toLowerCase().includes(keyword)) : this.activeSubReasonOptions
}
},
methods: {
formatNow() {
const date = new Date()
const pad = (value) => String(value).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
},
updateSubmitTime() {
this.submitTime = this.formatNow()
},
getCurrentUser() {
const user = uni.getStorageSync('scanapp_user') || {}
return {
name: user.nickname || user.real_name || user.username || '',
userCode: user.username || '',
position: user.position || user.role_name || ''
}
},
loadPdaMode() {
const settings = uni.getStorageSync(`scanapp_settings_${this.getCurrentUser().userCode}`)
this.pdaMode = settings?.pdaMode === true
},
handleOrderInputFocus() {
// #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
},
formatSettings(items) {
const settings = Array.isArray(items) ? items : []
const toOption = (item) => ({
value: String(item.id),
label: String(item.name || ''),
code: String(item.code || ''),
desc: String(item.remark || ''),
frequency: Number(item.frequency || 0),
sort: Number(item.sort || 0)
})
const types = settings.filter((item) => item.setting_type === 'type')
const reasons = settings.filter((item) => item.setting_type === 'reason')
const issues = settings.filter((item) => item.setting_type === 'issue')
this.typeOptions = types.map(toOption)
this.reasonOptions = reasons.reduce((result, item) => {
const parentId = String(item.parent_id || '')
if (!result[parentId]) result[parentId] = []
result[parentId].push(toOption(item))
return result
}, {})
this.subReasonOptions = issues.reduce((result, item) => {
const parentId = String(item.parent_id || '')
if (!result[parentId]) result[parentId] = []
result[parentId].push(toOption(item))
return result
}, {})
Object.keys(this.subReasonOptions).forEach((reasonId) => {
this.subReasonOptions[reasonId].sort((a, b) => {
return b.frequency - a.frequency
|| a.sort - b.sort
|| a.label.localeCompare(b.label, 'zh-CN')
})
})
},
resetSelection() {
const firstType = this.typeOptions[0]
this.form.type = firstType?.value || ''
const firstReason = this.reasonOptions[this.form.type]?.[0]
this.form.reason = firstReason?.value || ''
this.form.subReason = this.subReasonOptions[this.form.reason]?.[0]?.value || ''
},
async loadDashboard() {
const [statistics, recent] = await Promise.all([
getExceptionStatistics({ user_code: this.currentUser.userCode }),
getRecentExceptions({ user_code: this.currentUser.userCode, limit: 4 })
])
const statisticsData = statistics?.data || statistics || {}
this.summary = {
pending: Number(statisticsData.pending || 0),
review: Number(statisticsData.review || 0),
done: Number(statisticsData.done || 0)
}
const recentData = Array.isArray(recent) ? recent : (Array.isArray(recent?.data) ? recent.data : [])
this.recentExceptions = recentData.map((item) => ({
id: item.id,
orderNo: item.order_no || '-',
typeLabel: item.type_name || '-',
reasonLabel: [item.reason_name, item.issue_name].filter(Boolean).join(' · '),
statusLabel: item.status_name || '待处理',
statusClass: Number(item.status) === 2 ? 'done' : Number(item.status) === 1 ? 'review' : 'pending',
time: item.reported_date ? String(item.reported_date).slice(5, 16) : '',
operator: item.reporter || item.user_code || ''
}))
},
async loadPageData() {
this.currentUser = this.getCurrentUser()
if (!this.currentUser.userCode) {
uni.showToast({ title: '请先登录后再提报异常', icon: 'none' })
return
}
try {
const settings = await getExceptionSettings()
this.formatSettings(settings)
this.resetSelection()
await this.loadDashboard()
} catch (error) {
console.error('[scanapp] 异常页面数据加载失败', error)
}
},
scanOrderNo() {
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.orderNo = value
this.updateSubmitTime()
await this.checkOrderNo()
},
fail: (error) => {
if (Number(error?.errCode) === 9021003) return
uni.showToast({
title: Number(error?.errCode) === 9021002 ? '未获得扫码权限' : '未识别到有效条码',
icon: 'none'
})
},
complete: () => {
this.scanning = false
}
})
// #endif
// #ifndef APP-PLUS
uni.showToast({ title: '请在 Android App 中使用扫码功能', icon: 'none' })
// #endif
},
resetOrderValidation() {
this.orderValidation = { valid: false, value: '', message: '' }
},
clearOrderNo() {
this.form.orderNo = ''
this.resetOrderValidation()
},
async checkOrderNo() {
const orderNo = this.form.orderNo.trim()
if (!orderNo || this.checkingOrder) return false
if (this.orderValidation.valid && this.orderValidation.value === orderNo) return true
this.checkingOrder = true
try {
const reference = await checkExceptionOrder(orderNo)
if (this.form.orderNo.trim() !== orderNo) return false
this.orderValidation = {
valid: true,
value: orderNo,
message: `已匹配${reference.reference_name || '记录'}`
}
return true
} catch (error) {
if (this.form.orderNo.trim() === orderNo) {
this.orderValidation = { valid: false, value: orderNo, message: '未找到对应订单或整机记录' }
}
return false
} finally {
this.checkingOrder = false
}
},
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)
},
fillVoiceRemark() {
const voiceText = '【语音速记】现场发现异常,请优先复核。'
this.form.remark = this.form.remark ? `${this.form.remark}\n${voiceText}` : voiceText
uni.showToast({ title: '已写入语音速记', icon: 'none' })
},
getLabel(options, value) {
const matched = options.find((item) => String(item.value) === String(value))
return matched ? matched.label : ''
},
selectType(type) {
this.form.type = String(type)
this.form.reason = this.reasonOptions[this.form.type]?.[0]?.value || ''
this.form.subReason = this.subReasonOptions[this.form.reason]?.[0]?.value || ''
},
selectReason(reason) {
this.form.reason = String(reason)
this.form.subReason = this.subReasonOptions[this.form.reason]?.[0]?.value || ''
},
openSubReasonPopup() {
this.subReasonKeyword = ''
this.subReasonPopupVisible = true
},
closeSubReasonPopup() {
this.subReasonPopupVisible = false
},
selectSubReason(subReason, closePopup = false) {
this.form.subReason = String(subReason)
if (closePopup) this.closeSubReasonPopup()
},
getReasonLabel() {
const reasonLabel = this.getLabel(this.activeReasonOptions, this.form.reason)
const issueLabel = this.getLabel(this.activeSubReasonOptions, this.form.subReason)
return [reasonLabel, issueLabel].filter(Boolean).join(' · ')
},
validateForm() {
if (!this.form.orderNo.trim()) {
uni.showToast({ title: '请先输入单号', icon: 'none' })
return false
}
if (!this.orderValidation.valid || this.orderValidation.value !== this.form.orderNo.trim()) {
uni.showToast({ title: '请先校验有效的订单或整机条码', icon: 'none' })
return false
}
if (!this.form.type || !this.form.reason) {
uni.showToast({ title: '请选择处理类型和异常原因', icon: 'none' })
return false
}
if (this.activeSubReasonOptions.length && !this.form.subReason) {
uni.showToast({ title: '请选择具体问题', icon: 'none' })
return false
}
return true
},
async submitException() {
if (this.submitting) return
const isOrderValid = await this.checkOrderNo()
if (!isOrderValid || !this.validateForm()) return
this.submitting = true
this.updateSubmitTime()
try {
const uploadedImages = []
for (const filePath of this.form.images) {
uploadedImages.push(await uploadImage(filePath, 'exception'))
}
await submitExceptionRecord({
order_no: this.form.orderNo.trim(),
user_code: this.currentUser.userCode,
type_id: this.form.type,
reason_id: this.form.reason,
issue_id: this.form.subReason || '',
images: uploadedImages,
remark: this.form.remark.trim()
})
uni.showToast({ title: '异常已提交', icon: 'success' })
this.resetForm()
await this.loadDashboard()
} catch (error) {
console.error('[scanapp] 异常提交失败', error)
} finally {
this.submitting = false
}
},
resetForm() {
this.form.orderNo = ''
this.resetOrderValidation()
this.form.remark = ''
this.form.images = []
this.resetSelection()
this.updateSubmitTime()
}
},
onLoad() {
this.loadPdaMode()
this.updateSubmitTime()
this.loadPageData()
},
onShow() {
this.loadPdaMode()
}
}
</script>
<style lang="scss" scoped>
.exception-page {
min-height: 100vh;
padding: 24rpx 22rpx calc(184rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
background: linear-gradient(180deg, #fff6ef 0%, #f7f9fc 26%, #f5f7fb 100%);
}
.scene-hero,
.status-strip,
.quick-card,
.form-card,
.operator-card,
.record-card,
.submit-bar {
margin-top: 18rpx;
border-radius: 24rpx;
background: #ffffff;
box-shadow: 0 10rpx 26rpx rgba(23, 35, 61, 0.05);
}
.scene-hero {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18rpx;
margin-top: 0;
padding: 28rpx 24rpx;
background: linear-gradient(135deg, #ff9d42 0%, #ff7048 100%);
}
.hero-content,
.hero-user {
min-width: 0;
}
.scene-tag,
.hero-title,
.hero-subtitle,
.hero-user-name,
.hero-user-code,
.status-value,
.status-label,
.section-title,
.quick-time,
.scan-text-label,
.scan-text-value,
.type-card-title,
.type-card-desc,
.field-tip,
.field-link,
.record-main,
.record-sub,
.record-bottom,
.operator-label,
.operator-value,
.submit-tip {
display: block;
}
.scene-tag {
width: fit-content;
padding: 6rpx 14rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.18);
color: #ffffff;
font-size: 18rpx;
font-weight: 600;
line-height: 1.35;
}
.hero-title {
margin-top: 12rpx;
color: #ffffff;
font-size: 32rpx;
font-weight: 800;
line-height: 1.3;
}
.hero-subtitle {
margin-top: 8rpx;
color: rgba(255, 255, 255, 0.88);
font-size: 20rpx;
line-height: 1.45;
}
.hero-user {
padding: 16rpx 18rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.14);
text-align: right;
flex: none;
}
.hero-user-name {
color: #ffffff;
font-size: 24rpx;
font-weight: 700;
line-height: 1.35;
}
.hero-user-code {
margin-top: 6rpx;
color: rgba(255, 255, 255, 0.84);
font-size: 18rpx;
line-height: 1.35;
}
.status-strip {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12rpx;
padding: 18rpx;
}
.status-item {
padding: 14rpx 12rpx;
border-radius: 18rpx;
background: #f8fafc;
text-align: center;
}
.status-value {
font-size: 28rpx;
font-weight: 800;
line-height: 1.2;
}
.status-value.danger {
color: #ef4f4f;
}
.status-value.warning {
color: #ff9a3c;
}
.status-value.success {
color: #26b36c;
}
.status-label {
margin-top: 6rpx;
color: #8a94a6;
font-size: 18rpx;
line-height: 1.35;
}
.quick-card,
.form-card,
.operator-card,
.record-card {
padding: 24rpx;
}
.quick-header,
.field-header,
.operator-row,
.record-top,
.record-bottom,
.submit-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14rpx;
}
.submit-actions {
gap: 0;
}
.section-title {
color: #17233d;
font-size: 26rpx;
font-weight: 700;
line-height: 1.35;
}
.quick-time,
.field-tip,
.record-bottom,
.operator-label,
.submit-tip {
color: #9aa4b4;
font-size: 18rpx;
line-height: 1.35;
}
.field-tip {
padding: 4rpx 12rpx;
border-radius: 999rpx;
background: #f2f6ff;
color: #4e7fff;
}
.field-tip.optional {
background: #fff4ea;
color: #ff8b38;
}
.field-link {
color: #ff8b38;
font-size: 20rpx;
font-weight: 600;
line-height: 1.35;
}
.scan-box {
display: flex;
align-items: center;
gap: 0;
margin-top: 18rpx;
padding: 20rpx;
border-radius: 22rpx;
background: linear-gradient(135deg, #fff6ef 0%, #fff2e8 100%);
}
.scan-icon-wrap {
display: flex;
align-items: center;
justify-content: center;
width: 92rpx;
height: 92rpx;
margin-right: 24rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, #ff9d42 0%, #ff7048 100%);
box-shadow: 0 14rpx 24rpx rgba(255, 132, 68, 0.2);
flex: none;
}
.scan-text-group {
flex: 1;
min-width: 0;
}
.scan-text-label {
color: #8a94a6;
font-size: 20rpx;
line-height: 1.35;
}
.scan-text-value {
margin-top: 6rpx;
color: #17233d;
font-size: 26rpx;
font-weight: 700;
line-height: 1.4;
word-break: break-all;
}
.scan-text-value.is-empty {
color: #9aa4b4;
font-weight: 500;
}
.manual-row {
display: flex;
align-items: center;
gap: 0;
margin-top: 16rpx;
}
.manual-input-wrap {
flex: 1;
min-width: 0;
padding: 0 16rpx;
border: 1rpx solid #ebeef4;
border-radius: 18rpx;
background: #f8fbff;
}
.manual-action {
display: flex;
align-items: center;
justify-content: center;
height: 80rpx;
padding: 0 24rpx;
border-radius: 18rpx;
font-size: 20rpx;
font-weight: 600;
line-height: 1;
flex: none;
}
.manual-action.ghost {
margin-left: 16rpx;
background: #f2f4f8;
color: #6c7788;
}
.manual-action.primary {
margin-left: 16rpx;
background: linear-gradient(135deg, #2f6cf6 0%, #35bfa3 100%);
color: #ffffff;
}
.order-validation {
margin-left: 16rpx;
font-size: 18rpx;
line-height: 1.35;
text-align: right;
flex: none;
}
.order-validation.is-valid {
color: #26b36c;
}
.order-validation.is-invalid {
color: #ef4f4f;
}
.section-gap {
margin-top: 24rpx;
}
.type-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14rpx;
margin-top: 16rpx;
}
.type-card {
padding: 18rpx;
border: 2rpx solid transparent;
border-radius: 20rpx;
background: #f8fafc;
}
.type-card.is-active {
border-color: #ff9a3c;
background: #fff7ef;
box-shadow: 0 10rpx 18rpx rgba(255, 154, 60, 0.12);
}
.type-card-title {
color: #17233d;
font-size: 22rpx;
font-weight: 700;
line-height: 1.35;
}
.type-card-desc {
margin-top: 8rpx;
color: #8a94a6;
font-size: 18rpx;
line-height: 1.45;
}
.reason-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12rpx;
margin-top: 16rpx;
}
.reason-card {
display: flex;
align-items: center;
justify-content: center;
min-height: 82rpx;
padding: 0 12rpx;
border-radius: 18rpx;
background: #f2f4f8;
color: #5f6b7a;
font-size: 20rpx;
font-weight: 600;
line-height: 1.35;
text-align: center;
}
.reason-card.is-active {
background: linear-gradient(135deg, #ff9d42 0%, #ff7d47 100%);
color: #ffffff;
box-shadow: 0 12rpx 20rpx rgba(255, 132, 68, 0.18);
}
.sub-reason-panel {
margin-top: 18rpx;
padding: 20rpx;
border-radius: 22rpx;
background: #fff8f2;
}
.sub-reason-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12rpx;
}
.sub-reason-card {
display: flex;
align-items: center;
justify-content: center;
min-height: 72rpx;
padding: 8rpx 10rpx;
border: 1rpx solid #f2dac7;
border-radius: 16rpx;
background: #ffffff;
color: #5f6b7a;
font-size: 18rpx;
font-weight: 600;
line-height: 1.35;
text-align: center;
}
.sub-reason-card.is-active {
border-color: #ff9a3c;
background: #fff0e2;
color: #e97020;
}
.more-sub-reason {
border-style: dashed;
border-color: #ffbd8e;
background: #fffaf5;
color: #e97020;
}
.selected-sub-reason {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
margin-top: 16rpx;
padding: 16rpx;
border-radius: 16rpx;
background: #fff1e4;
color: #bf5d1e;
font-size: 20rpx;
font-weight: 600;
}
.sub-reason-popup {
padding: 28rpx 24rpx calc(28rpx + env(safe-area-inset-bottom));
background: #ffffff;
}
.popup-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20rpx;
margin-bottom: 22rpx;
}
.popup-title,
.popup-subtitle {
display: block;
}
.popup-title {
color: #17233d;
font-size: 30rpx;
font-weight: 700;
}
.popup-subtitle {
margin-top: 6rpx;
color: #8a94a6;
font-size: 20rpx;
}
.popup-close {
padding: 8rpx 0 8rpx 20rpx;
color: #e97020;
font-size: 22rpx;
font-weight: 600;
}
.popup-list {
height: 680rpx;
margin-top: 18rpx;
}
.popup-option {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 86rpx;
padding: 0 16rpx;
border-bottom: 1rpx solid #f0f2f6;
color: #4e5969;
font-size: 24rpx;
}
.popup-option.is-active {
color: #e97020;
font-weight: 700;
}
.popup-empty {
padding: 60rpx 0;
color: #9aa4b4;
font-size: 22rpx;
text-align: center;
}
.evidence-panel,
.remark-panel {
margin-top: 24rpx;
padding: 20rpx;
border-radius: 22rpx;
background: #fafbfd;
}
.compact-header {
margin-bottom: 16rpx;
}
.evidence-grid {
display: flex;
flex-wrap: nowrap;
align-items: flex-start;
overflow-x: auto;
padding-bottom: 4rpx;
}
.evidence-grid > * {
margin-right: 14rpx;
margin-bottom: 14rpx;
}
.shot-box,
.upload-preview {
display: flex;
align-items: center;
justify-content: center;
width: 144rpx;
height: 144rpx;
border-radius: 20rpx;
background: #ffffff;
}
.shot-box {
flex-direction: column;
gap: 8rpx;
border: 2rpx dashed #ffd3b4;
}
.shot-title {
color: #ff8b38;
font-size: 22rpx;
font-weight: 600;
line-height: 1.35;
}
.shot-desc {
color: #a6afbd;
font-size: 18rpx;
line-height: 1.35;
}
.upload-preview {
position: relative;
overflow: hidden;
}
.upload-preview image {
width: 100%;
height: 100%;
}
.upload-label {
position: absolute;
right: 8rpx;
bottom: 8rpx;
padding: 4rpx 8rpx;
border-radius: 8rpx;
color: #ffffff;
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);
}
.operator-card {
padding-top: 18rpx;
padding-bottom: 18rpx;
}
.operator-row {
padding: 10rpx 0;
}
.operator-value {
color: #17233d;
font-size: 20rpx;
font-weight: 600;
line-height: 1.35;
}
.keep-row {
cursor: pointer;
}
.keep-switch {
position: relative;
width: 92rpx;
height: 50rpx;
margin-left: 22rpx;
padding: 4rpx;
border-radius: 999rpx;
background: #d9dee8;
box-sizing: border-box;
transition: all 0.2s ease;
flex: none;
}
.keep-switch.is-active {
background: linear-gradient(135deg, #ff9d42 0%, #ff7048 100%);
}
.keep-dot {
width: 42rpx;
height: 42rpx;
border-radius: 50%;
background: #ffffff;
transition: transform 0.2s ease;
}
.keep-switch.is-active .keep-dot {
transform: translateX(42rpx);
}
.record-item {
padding: 18rpx 0;
border-bottom: 1rpx solid #eef1f6;
}
.record-item:last-child {
padding-bottom: 0;
border-bottom: 0;
}
.record-top {
align-items: flex-start;
}
.record-main {
color: #17233d;
font-size: 24rpx;
font-weight: 700;
line-height: 1.35;
}
.record-sub {
margin-top: 6rpx;
color: #7d8798;
font-size: 20rpx;
line-height: 1.35;
}
.status-pill {
padding: 6rpx 16rpx;
border-radius: 999rpx;
font-size: 18rpx;
line-height: 1.35;
flex: none;
}
.status-pill.pending {
background: rgba(239, 79, 79, 0.12);
color: #ef4f4f;
}
.status-pill.review {
background: rgba(255, 154, 60, 0.14);
color: #ff8b38;
}
.status-pill.done {
background: rgba(38, 179, 108, 0.12);
color: #26b36c;
}
.record-bottom {
margin-top: 10rpx;
}
.submit-bar {
position: fixed;
left: 22rpx;
right: 22rpx;
bottom: calc(22rpx + env(safe-area-inset-bottom));
padding: 18rpx 20rpx;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(12rpx);
z-index: 20;
}
.submit-tip {
margin-bottom: 14rpx;
}
.submit-secondary,
.submit-primary {
display: flex;
align-items: center;
justify-content: center;
height: 86rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: 700;
line-height: 1;
}
.submit-secondary {
width: 176rpx;
background: #f2f4f8;
color: #5f6b7a;
flex: none;
}
.submit-primary {
margin-left: 14rpx;
flex: 1;
background: linear-gradient(135deg, #ff9d42 0%, #ff7048 100%);
color: #ffffff;
box-shadow: 0 14rpx 24rpx rgba(255, 132, 68, 0.2);
}
.submit-primary.is-loading {
opacity: 0.8;
}
.theme-dark.exception-page {
background: #151923;
}
.theme-dark .scene-hero,
.theme-dark .status-strip,
.theme-dark .quick-card,
.theme-dark .form-card,
.theme-dark .evidence-panel,
.theme-dark .remark-panel,
.theme-dark .operator-card,
.theme-dark .record-card {
border-color: #364152;
background: #222936;
box-shadow: none;
}
.theme-dark .status-item,
.theme-dark .scan-box,
.theme-dark .exception-info,
.theme-dark .exception-empty,
.theme-dark .responsibility-item,
.theme-dark .repair-action-item,
.theme-dark .part-row,
.theme-dark .part-fault-entry,
.theme-dark .upload-box,
.theme-dark .scan-input,
.theme-dark .manual-input-wrap,
.theme-dark .selected-sub-reason,
.theme-dark .sub-reason-panel,
.theme-dark .sub-reason-card,
.theme-dark .device-found,
.theme-dark .result-item,
.theme-dark .stepper-button,
.theme-dark .part-stepper {
border-color: #364152;
background: #1c2431;
}
.theme-dark .result-item,
.theme-dark .info-row,
.theme-dark .record-item,
.theme-dark .part-source-option + .part-source-option,
.theme-dark .responsibility-item + .responsibility-item,
.theme-dark .repair-action-item + .repair-action-item,
.theme-dark .part-row + .part-row,
.theme-dark .part-fault-entry + .part-fault-entry {
border-color: #364152;
}
.theme-dark .section-title,
.theme-dark .scan-text-value,
.theme-dark .type-card-title,
.theme-dark .operator-value,
.theme-dark .record-main,
.theme-dark .info-value,
.theme-dark .device-found-value,
.theme-dark .result-label,
.theme-dark .responsibility-title,
.theme-dark .responsibility-name,
.theme-dark .repair-action-summary,
.theme-dark .part-name,
.theme-dark .part-fault-entry-value,
.theme-dark .record-main,
.theme-dark .operator-value,
.theme-dark .fault-popup-title,
.theme-dark .popup-title,
.theme-dark .part-input-title {
color: #f2f5fa;
}
.theme-dark .quick-time,
.theme-dark .status-label,
.theme-dark .scan-text-label,
.theme-dark .type-card-desc,
.theme-dark .operator-label,
.theme-dark .submit-tip,
.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 .machine-hint,
.theme-dark .record-sub,
.theme-dark .record-bottom,
.theme-dark .operator-label,
.theme-dark .fault-popup-subtitle,
.theme-dark .popup-subtitle,
.theme-dark .scan-text-label {
color: #aeb9ca;
}
.theme-dark .sub-reason-panel,
.theme-dark .selected-sub-reason,
.theme-dark .fault-popup,
.theme-dark .part-input-dialog,
.theme-dark .sub-reason-card,
.theme-dark .detail-sheet,
.theme-dark .submit-bar,
.theme-dark .bottom-actions {
border-color: #364152;
background: #222936;
}
.theme-dark .scan-input,
.theme-dark .manual-input-wrap,
.theme-dark .stepper-button,
.theme-dark .part-stepper {
color: #f2f5fa;
}
.theme-dark .scan-input::placeholder,
.theme-dark .manual-input-wrap::placeholder,
.theme-dark .part-input::placeholder {
color: #8996aa;
}
.theme-dark .manual-action.ghost,
.theme-dark .submit-secondary {
background: #293344;
color: #d7deea;
}
.theme-dark .type-card,
.theme-dark .reason-card,
.theme-dark .sub-reason-card,
.theme-dark .popup-option {
border: 1rpx solid #2e394a;
border-color: #364152;
background: #1c2431;
color: #d7deea;
box-shadow: none;
}
.theme-dark .reason-card {
min-height: 82rpx;
background: #1a2230;
color: #c9d2df;
}
.theme-dark .reason-card:active,
.theme-dark .sub-reason-card:active {
background: #243044;
}
.theme-dark .type-card.is-active,
.theme-dark .reason-card.is-active,
.theme-dark .sub-reason-card.is-active,
.theme-dark .popup-option.is-active {
border-color: #ff9d42;
background: rgba(255, 157, 66, 0.16);
color: #ffd1ad;
box-shadow: 0 8rpx 16rpx rgba(255, 157, 66, 0.12);
}
.theme-dark .reason-card.is-active {
background: linear-gradient(135deg, #a85d2f 0%, #8b4d35 100%);
color: #ffffff;
box-shadow: 0 10rpx 18rpx rgba(255, 132, 68, 0.2);
}
.theme-dark .sub-reason-popup {
background: #222936;
}
.theme-dark .submit-primary,
.theme-dark .repair-action-item.is-selected,
.theme-dark .responsibility-item.is-selected,
.theme-dark .part-source-option.is-selected {
color: #ffffff;
}
.theme-dark .manual-input-wrap,
.theme-dark .sub-reason-card,
.theme-dark .shot-box,
.theme-dark .upload-preview,
.theme-dark .part-stepper,
.theme-dark .stepper-button,
.theme-dark .part-fault-entry {
border-color: #364152;
background: #1c2431;
}
.theme-dark :deep(.u-input__input),
.theme-dark :deep(textarea) {
color: #f2f5fa !important;
}
</style>