feat:修改设备维修-设备和关键件的选择方式,增加弹窗以及表单默认当天日期。

master
ck-chenkang 4 days ago
parent fb9c54fa0e
commit 691e0cec97

@ -108,16 +108,24 @@
<u-icon name="scan" size="18" color="#1f4b79"></u-icon>
</view>
</view>
<picker :range="deviceLabels" :value="deviceIndex" :disabled="readonlyBase" @change="onDeviceChange">
<view :class="['picker-field', !formData.deviceId ? 'is-placeholder' : '']">{{ selectedDeviceLabel }}</view>
</picker>
<view
:class="['picker-field', !formData.deviceId ? 'is-placeholder' : '', readonlyBase ? 'is-disabled' : '']"
@click="openDevicePicker"
>
<text class="picker-field-text">{{ selectedDeviceLabel }}</text>
<u-icon v-if="!readonlyBase" name="arrow-down" size="16" color="#64748b"></u-icon>
</view>
</view>
<view v-if="showComponentSelect" class="form-item">
<text class="form-label required">{{ t('equipmentMaintenance.component') }}</text>
<picker :range="componentLabels" :value="componentIndex" :disabled="readonlyBase" @change="onComponentChange">
<view :class="['picker-field', !formData.componentId ? 'is-placeholder' : '']">{{ selectedComponentLabel }}</view>
</picker>
<view
:class="['picker-field', !formData.componentId ? 'is-placeholder' : '', readonlyBase ? 'is-disabled' : '']"
@click="openComponentPicker"
>
<text class="picker-field-text">{{ selectedComponentLabel }}</text>
<u-icon v-if="!readonlyBase" name="arrow-down" size="16" color="#64748b"></u-icon>
</view>
</view>
</view>
@ -232,6 +240,86 @@
<view class="footer-btn cancel" @click="goBack">{{ t('dashboard.back') }}</view>
<view v-if="!isReadonly" class="footer-btn confirm" @click="submitForm">{{ submitButtonText }}</view>
</view>
<uni-popup ref="devicePickerRef" type="bottom">
<view class="device-picker-popup">
<view class="device-picker-header">
<text class="device-picker-title">{{ t('equipmentMaintenance.placeholderDevice') }}</text>
<text class="device-picker-close" @click="closeDevicePicker">×</text>
</view>
<view class="device-picker-search">
<view class="device-search-box">
<u-icon name="search" size="17" color="#94a3b8"></u-icon>
<input
v-model="deviceSearchKeyword"
class="device-search-input"
type="text"
confirm-type="search"
:placeholder="t('equipmentLedger.placeholderDeviceCode') + ' / ' + t('equipmentLedger.placeholderDeviceName')"
/>
<text v-if="deviceSearchKeyword" class="device-search-clear" @click="clearDeviceSearch">×</text>
</view>
</view>
<scroll-view scroll-y class="device-picker-scroll">
<view
v-for="item in filteredDeviceOptions"
:key="String(item.value)"
:class="['device-picker-item', String(item.value) === String(formData.deviceId || '') ? 'is-selected' : '']"
@click="selectDeviceOption(item)"
>
<view class="device-item-main">
<view class="device-item-title">
<text class="device-item-code">{{ item.raw?.deviceCode || String(item.value) }}</text>
<text class="device-item-name">{{ item.raw?.deviceName || '-' }}</text>
</view>
<view v-if="deviceMetaText(item)" class="device-item-meta">{{ deviceMetaText(item) }}</view>
</view>
<u-icon v-if="String(item.value) === String(formData.deviceId || '')" name="checkmark" size="18" color="#1f4b79"></u-icon>
</view>
<view v-if="!filteredDeviceOptions.length" class="device-picker-empty">{{ t('equipmentMaintenance.deviceNotFound') }}</view>
</scroll-view>
</view>
</uni-popup>
<uni-popup ref="componentPickerRef" type="bottom">
<view class="device-picker-popup">
<view class="device-picker-header">
<text class="device-picker-title">{{ t('equipmentMaintenance.placeholderComponent') }}</text>
<text class="device-picker-close" @click="closeComponentPicker">×</text>
</view>
<view class="device-picker-search">
<view class="device-search-box">
<u-icon name="search" size="17" color="#94a3b8"></u-icon>
<input
v-model="componentSearchKeyword"
class="device-search-input"
type="text"
confirm-type="search"
:placeholder="t('criticalComponent.placeholderCode') + ' / ' + t('criticalComponent.placeholderName')"
/>
<text v-if="componentSearchKeyword" class="device-search-clear" @click="clearComponentSearch">×</text>
</view>
</view>
<scroll-view scroll-y class="device-picker-scroll">
<view
v-for="item in searchableComponentOptions"
:key="String(item.value)"
:class="['device-picker-item', String(item.value) === String(formData.componentId || '') ? 'is-selected' : '']"
@click="selectComponentOption(item)"
>
<view class="device-item-main">
<view class="device-item-title">
<text class="device-item-code">{{ item.raw?.code || item.raw?.componentCode || '-' }}</text>
<text class="device-item-name">{{ item.raw?.name || item.raw?.componentName || '-' }}</text>
</view>
<view v-if="componentMetaText(item)" class="device-item-meta">{{ componentMetaText(item) }}</view>
</view>
<u-icon v-if="String(item.value) === String(formData.componentId || '')" name="checkmark" size="18" color="#1f4b79"></u-icon>
</view>
<view v-if="!searchableComponentOptions.length" class="device-picker-empty">{{ t('equipmentMaintenance.deviceNotFound') }}</view>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
@ -261,6 +349,10 @@ const users = ref([])
const deviceOptions = ref([])
const componentOptions = ref([])
const loading = ref(false)
const devicePickerRef = ref(null)
const deviceSearchKeyword = ref('')
const componentPickerRef = ref(null)
const componentSearchKeyword = ref('')
const formData = reactive({
id: undefined,
@ -275,9 +367,9 @@ const formData = reactive({
machineryBrand: '',
machinerySpec: '',
machineryTypeId: 1,
requireDate: '',
finishDate: '',
confirmDate: '',
requireDate: todayPickerDate(),
finishDate: todayPickerDate(),
confirmDate: todayPickerDate(),
repairStatus: '0',
repairResult: '0',
acceptedBy: '',
@ -318,7 +410,6 @@ const statusLabel = computed(() => {
return t('equipmentMaintenance.statusPending')
})
const userLabels = computed(() => users.value.map((item) => item.nickname || item.name || String(item.id || '')))
const deviceLabels = computed(() => deviceOptions.value.map((item) => item.label))
const faultLevelOptions = computed(() => {
const dicts = dictStore.getDict(DICT_TYPE.FAILURE_LEVEL) || []
return dicts
@ -333,14 +424,32 @@ const confirmByIndex = computed(() => findUserIndex(formData.confirmBy))
const acceptedByLabel = computed(() => resolveUserLabel(formData.acceptedBy, t('equipmentMaintenance.placeholderAcceptedBy')))
const confirmByLabel = computed(() => resolveUserLabel(formData.confirmBy, t('equipmentMaintenance.placeholderConfirmBy')))
const showComponentSelect = computed(() => Number(formData.machineryTypeId || 1) === 2)
const deviceIndex = computed(() => {
const index = deviceOptions.value.findIndex((item) => String(item.value) === String(formData.deviceId || ''))
return index >= 0 ? index : 0
})
const selectedDeviceLabel = computed(() => {
const current = deviceOptions.value.find((item) => String(item.value) === String(formData.deviceId || ''))
return current?.label || t('equipmentMaintenance.placeholderDevice')
})
const filteredDeviceOptions = computed(() => {
const keyword = deviceSearchKeyword.value.trim().toLowerCase()
if (!keyword) return deviceOptions.value
const keywordId = extractEquipmentKeywordId(keyword)
return deviceOptions.value.filter((item) => {
const raw = item.raw || {}
const fields = [
item.label,
item.value,
raw.id,
raw.deviceId,
raw.machineryId,
raw.machineryid,
raw.deviceCode,
raw.deviceName,
raw.deviceBrand,
raw.deviceSpec
]
return fields.some((field) => String(field || '').toLowerCase().includes(keyword))
|| (keywordId && matchesDeviceOptionId(item, keywordId))
})
})
const filteredComponentOptions = computed(() => {
const source = Array.isArray(componentOptions.value) ? componentOptions.value : []
const currentDeviceId = String(formData.deviceId || '').trim()
@ -348,10 +457,22 @@ const filteredComponentOptions = computed(() => {
const matched = source.filter((item) => matchesComponentDevice(item?.raw, currentDeviceId))
return matched.length ? matched : source
})
const componentLabels = computed(() => filteredComponentOptions.value.map((item) => item.label))
const componentIndex = computed(() => {
const index = filteredComponentOptions.value.findIndex((item) => String(item.value) === String(formData.componentId || ''))
return index >= 0 ? index : 0
const searchableComponentOptions = computed(() => {
const keyword = componentSearchKeyword.value.trim().toLowerCase()
if (!keyword) return filteredComponentOptions.value
return filteredComponentOptions.value.filter((item) => {
const raw = item.raw || {}
const fields = [
item.label,
raw.code,
raw.componentCode,
raw.name,
raw.componentName,
raw.deviceName,
raw.deviceCode
]
return fields.some((field) => String(field || '').toLowerCase().includes(keyword))
})
})
const selectedComponentLabel = computed(() => {
const current = filteredComponentOptions.value.find((item) => String(item.value) === String(formData.componentId || ''))
@ -433,9 +554,9 @@ async function fetchDetail(id) {
formData.machineryBrand = inputValue(detail?.machineryBrand)
formData.machinerySpec = inputValue(detail?.machinerySpec)
formData.machineryTypeId = Number(detail?.machineryTypeId || 1) || 1
formData.requireDate = formatPickerDate(detail?.requireDate)
formData.finishDate = formatPickerDate(detail?.finishDate)
formData.confirmDate = formatPickerDate(detail?.confirmDate)
formData.requireDate = formatPickerDate(detail?.requireDate) || todayPickerDate()
formData.finishDate = formatPickerDate(detail?.finishDate) || todayPickerDate()
formData.confirmDate = formatPickerDate(detail?.confirmDate) || todayPickerDate()
formData.repairStatus = normalizedRepairStatus
formData.repairResult = normalizedRepairStatus
formData.acceptedBy = normalizeUserId(detail?.acceptedBy)
@ -513,17 +634,61 @@ function onUserChange(field, event) {
formData[field] = current ? String(current.id) : ''
}
function onDeviceChange(event) {
const index = Number(event?.detail?.value || 0)
const current = deviceOptions.value[index]
if (!current) return
function openDevicePicker() {
if (readonlyBase.value) return
devicePickerRef.value?.open?.()
}
function closeDevicePicker() {
devicePickerRef.value?.close?.()
}
function clearDeviceSearch() {
deviceSearchKeyword.value = ''
}
function selectDeviceOption(current) {
if (!current || readonlyBase.value) return
applyDeviceOption(current)
clearDeviceSearch()
closeDevicePicker()
}
function onComponentChange(event) {
const index = Number(event?.detail?.value || 0)
const current = filteredComponentOptions.value[index]
formData.componentId = current ? current.value : undefined
function deviceMetaText(item) {
const raw = item?.raw || {}
return [raw.deviceBrand, raw.deviceSpec].map(inputValue).filter(Boolean).join(' / ')
}
function openComponentPicker() {
if (readonlyBase.value) return
componentPickerRef.value?.open?.()
}
function closeComponentPicker() {
componentPickerRef.value?.close?.()
}
function clearComponentSearch() {
componentSearchKeyword.value = ''
}
function selectComponentOption(current) {
if (!current || readonlyBase.value) return
formData.componentId = current.value
clearComponentSearch()
closeComponentPicker()
}
function componentMetaText(item) {
const raw = item?.raw || {}
return [
raw.deviceName,
raw.deviceCode,
raw.spec,
raw.componentSpec,
raw.model,
raw.componentModel
].map(inputValue).filter(Boolean).join(' / ')
}
function selectMachineryType(value) {
@ -552,7 +717,7 @@ async function handleDeviceScan() {
uni.showToast({ title: t('equipmentMaintenance.scanEquipmentRequired'), icon: 'none' })
return
}
const matched = deviceOptions.value.find((item) => String(item.value) === String(scan.id))
const matched = deviceOptions.value.find((item) => matchesDeviceOptionId(item, scan.id))
if (!matched) {
uni.showToast({ title: t('equipmentMaintenance.deviceNotFound'), icon: 'none' })
return
@ -587,6 +752,28 @@ function parseEquipmentScanResult(result) {
}
}
function extractEquipmentKeywordId(keyword) {
const match = String(keyword || '').trim().match(/-(\d+)$/)
return match ? match[1] : ''
}
function getDeviceOptionIds(item) {
const raw = item?.raw || {}
return [
item?.value,
raw.id,
raw.deviceId,
raw.machineryId,
raw.machineryid
].filter((value) => value !== undefined && value !== null && value !== '')
}
function matchesDeviceOptionId(item, id) {
const target = String(id || '').trim()
if (!target) return false
return getDeviceOptionIds(item).some((value) => String(value) === target)
}
function applyDeviceOption(current) {
formData.deviceId = current.value
formData.machineryId = current.value
@ -671,6 +858,10 @@ function datePickerValue(value) {
return value || formatPickerDate(Date.now())
}
function todayPickerDate() {
return formatPickerDate(Date.now())
}
function formatPickerDate(value) {
if (value === undefined || value === null || value === '') return ''
const date = parseDate(value)
@ -1002,6 +1193,23 @@ function goBack() {
color: #9ca3af;
}
.picker-field {
justify-content: space-between;
gap: 12rpx;
}
.picker-field.is-disabled {
color: #64748b;
}
.picker-field-text {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.code-row {
display: flex;
align-items: center;
@ -1161,4 +1369,146 @@ function goBack() {
background: #1f4b79;
color: #ffffff;
}
.device-picker-popup {
height: 72vh;
background: #ffffff;
border-radius: 22rpx 22rpx 0 0;
overflow: hidden;
}
.device-picker-header {
position: relative;
height: 92rpx;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1rpx solid #eef2f7;
}
.device-picker-title {
font-size: 30rpx;
font-weight: 700;
color: #111827;
}
.device-picker-close {
position: absolute;
right: 24rpx;
top: 14rpx;
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
color: #64748b;
font-size: 42rpx;
line-height: 1;
}
.device-picker-search {
padding: 18rpx 24rpx 14rpx;
}
.device-search-box {
height: 74rpx;
padding: 0 20rpx;
border-radius: 14rpx;
background: #f8fafc;
display: flex;
align-items: center;
gap: 12rpx;
}
.device-search-input {
flex: 1;
height: 74rpx;
min-width: 0;
font-size: 28rpx;
color: #111827;
}
.device-search-clear {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background: #e2e8f0;
color: #64748b;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
line-height: 1;
}
.device-picker-scroll {
height: calc(72vh - 184rpx);
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.device-picker-item {
margin: 0 24rpx 14rpx;
padding: 20rpx;
border-radius: 16rpx;
background: #f8fafc;
border: 2rpx solid transparent;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.device-picker-item.is-selected {
background: #eef6ff;
border-color: #bfdbfe;
}
.device-item-main {
flex: 1;
min-width: 0;
}
.device-item-title {
display: flex;
align-items: center;
gap: 12rpx;
min-width: 0;
}
.device-item-code {
flex-shrink: 0;
max-width: 52%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 28rpx;
font-weight: 700;
color: #111827;
}
.device-item-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 28rpx;
color: #334155;
}
.device-item-meta {
margin-top: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 24rpx;
color: #64748b;
}
.device-picker-empty {
padding: 72rpx 24rpx;
text-align: center;
color: #94a3b8;
font-size: 28rpx;
}
</style>

Loading…
Cancel
Save