Files
MrleeAdmin/frontend/app/pages/exception/index.vue
T

1454 lines
35 KiB
Vue
Raw Normal View History

2026-08-31 11:33:59 +08:00
<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>