黄伟杰 6 days ago
commit c251b49322

@ -1,203 +0,0 @@
import { onHide, onShow, onUnload } from '@dcloudio/uni-app'
const androidKeyCodeMap = {
7: '0',
8: '1',
9: '2',
10: '3',
11: '4',
12: '5',
13: '6',
14: '7',
15: '8',
16: '9',
29: 'A',
30: 'B',
31: 'C',
32: 'D',
33: 'E',
34: 'F',
35: 'G',
36: 'H',
37: 'I',
38: 'J',
39: 'K',
40: 'L',
41: 'M',
42: 'N',
43: 'O',
44: 'P',
45: 'Q',
46: 'R',
47: 'S',
48: 'T',
49: 'U',
50: 'V',
51: 'W',
52: 'X',
53: 'Y',
54: 'Z',
55: ',',
56: '.',
69: '-',
70: '=',
71: '[',
72: ']',
73: '\\',
74: ';',
75: "'",
76: '/',
81: '+'
}
export function useScannerInput(options = {}) {
const minLength = Number(options.minLength ?? 3)
const commitDelay = Number(options.commitDelay ?? 120)
const uppercase = options.uppercase !== false
const ignoreInputTarget = options.ignoreInputTarget !== false
const hideKeyboardOnScan = options.hideKeyboardOnScan !== false
const hideKeyboardOnLeave = options.hideKeyboardOnLeave !== false
const onScan = typeof options.onScan === 'function' ? options.onScan : () => {}
let scannerBuffer = ''
let scannerTimer = null
let scannerListening = false
let plusKeyListening = false
function getScannerEventTarget() {
if (typeof document !== 'undefined') return document
if (typeof window !== 'undefined') return window
return null
}
function registerScannerListener() {
if (scannerListening) return
const target = getScannerEventTarget()
if (target?.addEventListener) {
target.addEventListener('keydown', handleScannerKeydown)
scannerListening = true
}
registerPlusKeyListener()
}
function unregisterScannerListener() {
const target = getScannerEventTarget()
if (scannerListening && target?.removeEventListener) {
target.removeEventListener('keydown', handleScannerKeydown)
}
scannerListening = false
unregisterPlusKeyListener()
clearScannerBuffer()
if (hideKeyboardOnLeave) hideSoftKeyboard()
}
function registerPlusKeyListener() {
if (plusKeyListening) return
if (typeof plus === 'undefined' || !plus?.key?.addEventListener) return
plus.key.addEventListener('keydown', handlePlusScannerKeydown)
plusKeyListening = true
}
function unregisterPlusKeyListener() {
if (!plusKeyListening) return
if (typeof plus !== 'undefined' && plus?.key?.removeEventListener) {
plus.key.removeEventListener('keydown', handlePlusScannerKeydown)
}
plusKeyListening = false
}
function handlePlusScannerKeydown(event) {
handleScannerKeydown({ ...event, isPlusKeyEvent: true })
}
function handleScannerKeydown(event) {
if (ignoreInputTarget) {
const targetTag = String(event?.target?.tagName || '').toLowerCase()
if (targetTag === 'input' || targetTag === 'textarea') return
}
const key = normalizeScannerKey(event)
if (!key) return
if (key === 'Enter') {
commitScannerBuffer()
return
}
scannerBuffer += key
if (scannerTimer) clearTimeout(scannerTimer)
scannerTimer = setTimeout(() => {
commitScannerBuffer()
}, commitDelay)
}
function normalizeScannerKey(event) {
if (event?.ctrlKey || event?.altKey || event?.metaKey) return ''
if (isAndroidPlusKeyEvent(event)) return normalizeAndroidKeyCode(event)
const key = event?.key
if (key === 'Enter' || key === 'Tab') return 'Enter'
if (typeof key === 'string' && key.length === 1) return uppercase ? key.toUpperCase() : key
const code = Number(event?.keyCode || event?.which || 0)
if (code === 13 || code === 9) return 'Enter'
if (code >= 48 && code <= 90) {
const value = String.fromCharCode(code)
return uppercase ? value.toUpperCase() : value
}
if (code >= 96 && code <= 105) return String(code - 96)
if (code === 189 || code === 109) return '-'
if (code === 190 || code === 110) return '.'
return ''
}
function isAndroidPlusKeyEvent(event) {
return Boolean(event?.isPlusKeyEvent)
}
function normalizeAndroidKeyCode(event) {
const code = Number(event?.keyCode || event?.which || 0)
if (code === 66 || code === 61 || code === 160) return 'Enter'
const value = androidKeyCodeMap[code] || ''
return uppercase ? value.toUpperCase() : value.toLowerCase()
}
function clearScannerBuffer() {
scannerBuffer = ''
if (scannerTimer) {
clearTimeout(scannerTimer)
scannerTimer = null
}
}
async function commitScannerBuffer() {
const value = scannerBuffer.trim()
clearScannerBuffer()
if (value.length < minLength) return
if (hideKeyboardOnScan) hideSoftKeyboard()
await onScan(value)
}
function hideSoftKeyboard() {
try {
uni.hideKeyboard()
} catch (e) {}
}
onShow(() => {
registerScannerListener()
})
onHide(() => {
unregisterScannerListener()
})
onUnload(() => {
unregisterScannerListener()
})
return {
clearScannerBuffer,
registerScannerListener,
unregisterScannerListener
}
}

@ -7,14 +7,14 @@
<text :class="['line-filter-text', selectedLineId === '' ? 'placeholder' : '']">{{ selectedLineLabel }}</text>
<uni-icons type="bottom" size="14" color="#a8adb7"></uni-icons>
</view>
<view :class="['keyword-wrap', isKeywordInputActive ? 'active' : '']">
<view class="keyword-wrap">
<input
v-model="searchKeyword"
class="keyword-input"
:placeholder="t('equipmentLedger.searchPlaceholder')"
:focus="keywordFocus"
confirm-type="search"
@blur="handleKeywordBlur"
@tap="handleKeywordTap"
@blur="keywordFocus = false"
@confirm="handleSearch"
/>
</view>
@ -82,7 +82,7 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, nextTick } from 'vue'
import { onLoad, onPageScroll, onReachBottom, onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
@ -90,13 +90,11 @@ import { getDeviceLedgerPage, updateDeviceLedger } from '@/api/mes/deviceLedger'
import { getDeviceLineTree } from '@/api/mes/deviceLine'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
import useDictStore from '@/store/modules/dict'
import { useScannerInput } from '@/hooks/useScannerInput'
const { t } = useI18n()
const dictStore = useDictStore()
const resetFilterText = computed(() => t('functionCommon.reset'))
const searchKeyword = ref('')
const isKeywordInputActive = ref(true)
const selectedStatus = ref('')
const selectedLineId = ref('')
const lineTree = ref([])
@ -112,19 +110,7 @@ const pageSize = ref(10)
const total = ref(0)
const showGoTop = ref(false)
const statusUpdatingMap = ref({})
const { clearScannerBuffer } = useScannerInput({
minLength: 3,
commitDelay: 120,
uppercase: true,
hideKeyboardOnScan: true,
hideKeyboardOnLeave: true,
onScan: async (value) => {
searchKeyword.value = value
isKeywordInputActive.value = true
await fetchList(true)
}
})
const keywordFocus = ref(false)
const statusOptions = computed(() => {
const dicts = dictStore.getDict(DICT_TYPE.MES_TZ_STATUS) || []
@ -156,13 +142,14 @@ const selectedLineLabel = computed(() => {
})
onLoad(async () => {
activateKeywordFocus()
await initAllDict()
await fetchLineTree()
await fetchList(true)
})
onShow(() => {
isKeywordInputActive.value = true
activateKeywordFocus()
})
onReachBottom(() => {
@ -239,13 +226,11 @@ function findLinePath(nodes, id, parents = []) {
return []
}
function handleKeywordTap() {
isKeywordInputActive.value = true
clearScannerBuffer()
}
function handleKeywordBlur() {
isKeywordInputActive.value = true
function activateKeywordFocus() {
keywordFocus.value = false
nextTick(() => {
keywordFocus.value = true
})
}
async function fetchList(reset) {
@ -314,6 +299,7 @@ async function resetFilters() {
lineCascaderValue.value = []
lineCascaderShow.value = false
lineCascaderKey.value += 1
activateKeywordFocus()
await fetchList(true)
}
@ -322,7 +308,6 @@ async function resetLineFilter() {
}
async function handleSearch() {
uni.hideKeyboard()
await fetchList(true)
}
@ -481,11 +466,6 @@ function formatDateValue(value) {
align-items: center;
}
.keyword-wrap.active {
border-color: #2f7dff;
box-shadow: 0 0 0 2rpx rgba(47, 125, 255, 0.12);
}
.keyword-input {
width: 100%;
height: 64rpx;

@ -4,16 +4,17 @@
<!-- 操作按钮区 -->
<view class="action-row">
<view class="action-btn scan-btn" @click="handleScan">
<view class="btn-icon-wrap">
<text class="iconfont icon-scan btn-icon"></text>
</view>
<text class="btn-text">{{ t('sparepartInbound.scanSparepart') }}</text>
<view class="scan-input-row">
<input
class="scan-input"
v-model="scanCodeInput"
placeholder="红外扫码或输入备件码"
confirm-type="done"
:focus="true"
@confirm="onScanInputConfirm"
/>
</view>
<view class="action-btn select-btn" @click="handleSelectSparepart">
<view class="btn-icon-wrap">
<text class="iconfont icon-device btn-icon"></text>
</view>
<text class="btn-text">{{ t('sparepartInbound.selectSparepart') }}</text>
</view>
</view>
@ -43,7 +44,7 @@
</text>
<text class="form-row-arrow"></text>
<view v-if="showOperatorDropdown" class="dropdown-panel">
<scroll-view scroll-y class="dropdown-scroll">
<view class="dropdown-scroll">
<view
v-for="item in operatorOptions"
:key="item.value"
@ -55,7 +56,7 @@
<text v-if="selectedOperator?.value === item.value" class="dropdown-check"></text>
</view>
<view v-if="!operatorOptions.length" class="dropdown-empty"></view>
</scroll-view>
</view>
</view>
</view>
@ -160,6 +161,44 @@ import { getSimpleUserList } from '@/api/mes/moldget'
const { t } = useI18n()
const itemList = ref([])
const scanCodeInput = ref('') // /
//
function onScanInputConfirm() {
const code = scanCodeInput.value.trim()
if (!code) return
handleScanCode(code)
}
async function handleScanCode(code) {
let sparepartId = null
if (code.toUpperCase().startsWith('SPARE-')) {
sparepartId = code.replace(/SPARE-/i, '')
} else {
const idMatch = code.match(/(\d+)$/)
if (idMatch) sparepartId = idMatch[1]
}
if (!sparepartId) {
uni.showToast({ title: '无法识别备件码', icon: 'none' })
return
}
try {
const apiRes = await getSparepartDetail(sparepartId)
const detail = apiRes?.data || apiRes
if (detail && detail.id) {
getApp().globalData._sparepartFromScan = true
getApp().globalData._sparepartBeforeConfirm = detail
uni.navigateTo({
url: '/pages_function/pages/sparepartInbound/sparepartConfirm'
})
} else {
uni.showToast({ title: '未找到备件: ' + sparepartId, icon: 'none' })
}
} catch (e) {
console.error('[备件入库] 扫码查询备件失败:', e)
uni.showToast({ title: '扫码失败', icon: 'none' })
}
}
//
const inboundDate = ref(formatDate(new Date()))
@ -209,36 +248,11 @@ function handleScan() {
uni.scanCode({
onlyFromCamera: false,
scanType: ['barCode', 'qrCode'],
success: async (res) => {
success: (res) => {
const code = (res.result || '').trim()
if (!code) return
let sparepartId = null
if (code.toUpperCase().startsWith('SPARE-')) {
sparepartId = code.replace(/SPARE-/i, '')
} else {
const idMatch = code.match(/(\d+)$/)
if (idMatch) sparepartId = idMatch[1]
}
if (!sparepartId) {
uni.showToast({ title: '无法识别备件码', icon: 'none' })
return
}
try {
const apiRes = await getSparepartDetail(sparepartId)
const detail = apiRes?.data || apiRes
if (detail && detail.id) {
getApp().globalData._sparepartFromScan = true
getApp().globalData._sparepartBeforeConfirm = detail
uni.navigateTo({
url: '/pages_function/pages/sparepartInbound/sparepartConfirm'
})
} else {
uni.showToast({ title: '未找到备件: ' + sparepartId, icon: 'none' })
}
} catch (e) {
console.error('[备件入库] 扫码查询备件失败:', e)
uni.showToast({ title: '扫码失败', icon: 'none' })
}
scanCodeInput.value = code
handleScanCode(code)
},
fail: () => {
uni.showToast({ title: '扫码失败', icon: 'none' })
@ -418,26 +432,46 @@ onHide(() => {
/* 操作按钮区 */
.action-row {
display: flex;
gap: 20rpx;
align-items: center;
gap: 16rpx;
padding: 20rpx 24rpx;
}
.action-btn {
.scan-input-row {
flex: 1;
display: flex;
align-items: center;
height: 72rpx;
border-radius: 10rpx;
overflow: hidden;
}
.scan-input {
flex: 1;
height: 72rpx;
padding: 0 20rpx;
font-size: 26rpx;
color: #333;
background: #fff;
border: 1rpx solid #d0d5dd;
border-radius: 10rpx;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
height: 96rpx;
border-radius: 12rpx;
height: 72rpx;
padding: 0 24rpx;
border-radius: 10rpx;
background: #1f4b79;
color: #fff;
font-size: 28rpx;
font-size: 26rpx;
font-weight: 600;
white-space: nowrap;
.btn-icon-wrap { width: 44rpx; height: 44rpx; display: flex; align-items: center; justify-content: center; }
.btn-icon { font-size: 36rpx; color: #fff; }
.btn-text { font-size: 28rpx; font-weight: 600; color: #fff; }
.btn-text { font-size: 26rpx; font-weight: 600; color: #fff; }
}
/* 备件列表 */
@ -505,7 +539,7 @@ onHide(() => {
background: #fff; border: 1rpx solid #e0e0e0; border-radius: 12rpx;
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.1); overflow: hidden;
}
.dropdown-scroll { height: 360rpx; }
.dropdown-scroll { max-height: 360rpx; overflow-y: auto; }
.dropdown-item { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; border-bottom: 1rpx solid #f0f0f0;
&:last-child { border-bottom: 0; }
&.active { background: #f0f5ff; }

@ -188,14 +188,14 @@ function textValue(v) {
function formatDateTime(value) {
if (!value) return '-'
if (Array.isArray(value) && value.length >= 3) {
const [year, month, day, hour = 0, minute = 0, second = 0] = value
const [year, month, day] = value
const pad = (n) => String(n).padStart(2, '0')
return `${year}-${pad(month)}-${pad(day)} ${pad(hour)}:${pad(minute)}:${pad(second)}`
return `${year}-${pad(month)}-${pad(day)}`
}
const date = new Date(Number(value))
if (Number.isNaN(date.getTime())) return String(value)
const pad = (n) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
}
const STATUS_MAP = { 0: '待入库', 10: '待审核', 20: '已入库', 1: '已驳回' }

@ -110,7 +110,7 @@
<text class="dropdown-arrow"></text>
</view>
<view v-if="showWarehouseDropdown" class="dropdown-panel">
<scroll-view scroll-y class="dropdown-scroll">
<view class="dropdown-scroll">
<view
v-for="item in warehouseOptions"
:key="item.value"
@ -122,7 +122,7 @@
<text v-if="selectedWarehouse?.value === item.value" class="dropdown-check"></text>
</view>
<view v-if="!warehouseOptions.length" class="dropdown-empty"></view>
</scroll-view>
</view>
</view>
</view>
</view>
@ -136,7 +136,7 @@
<text class="dropdown-arrow"></text>
</view>
<view v-if="showAreaDropdown" class="dropdown-panel">
<scroll-view scroll-y class="dropdown-scroll">
<view class="dropdown-scroll">
<view
v-for="item in areaOptions"
:key="item.value"
@ -148,7 +148,7 @@
<text v-if="selectedArea?.value === item.value" class="dropdown-check"></text>
</view>
<view v-if="!areaOptions.length" class="dropdown-empty"></view>
</scroll-view>
</view>
</view>
</view>
</view>
@ -470,7 +470,7 @@ onHide(() => {
.dropdown-value { flex: 1; font-size: 27rpx; color: #333; &.placeholder { color: #bbb; } }
.dropdown-arrow { font-size: 20rpx; color: #999; flex-shrink: 0; }
.dropdown-panel { position: absolute; top: 68rpx; left: 0; right: 0; z-index: 200; background: #fff; border: 1rpx solid #e0e0e0; border-radius: 12rpx; box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.1); overflow: hidden; }
.dropdown-scroll { height: 360rpx; }
.dropdown-scroll { max-height: 360rpx; overflow-y: auto; }
.dropdown-item { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; border-bottom: 1rpx solid #f0f0f0;
&:last-child { border-bottom: 0; }
&.active { background: #f0f5ff; }

@ -4,12 +4,17 @@
<!-- 操作按钮区 -->
<view class="action-row">
<view class="action-btn scan-btn" @click="handleScan">
<view class="btn-icon-wrap"><text class="iconfont icon-scan btn-icon"></text></view>
<text class="btn-text">扫备件码</text>
<view class="scan-input-row">
<input
class="scan-input"
v-model="scanCodeInput"
placeholder="红外扫码或输入备件码"
confirm-type="done"
:focus="true"
@confirm="onScanInputConfirm"
/>
</view>
<view class="action-btn select-btn" @click="handleSelectSparepart">
<view class="btn-icon-wrap"><text class="iconfont icon-device btn-icon"></text></view>
<text class="btn-text">选择备件</text>
</view>
</view>
@ -37,13 +42,13 @@
<text :class="{ placeholder: !selectedOperator }">{{ selectedOperator ? selectedOperator.label : '请选择经办人' }}</text>
<text class="form-row-arrow"></text>
<view v-if="showOperatorDropdown" class="dropdown-panel">
<scroll-view scroll-y class="dropdown-scroll">
<view class="dropdown-scroll">
<view v-for="item in operatorOptions" :key="item.value" class="dropdown-item" :class="{ active: selectedOperator?.value === item.value }" @click.stop="handleSelectOperator(item)">
<text class="dropdown-item-text">{{ item.label }}</text>
<text v-if="selectedOperator?.value === item.value" class="dropdown-check"></text>
</view>
<view v-if="!operatorOptions.length" class="dropdown-empty"></view>
</scroll-view>
</view>
</view>
</view>
@ -146,25 +151,38 @@ function handleSelectSparepart() {
uni.navigateTo({ url: '/pages_function/pages/sparepartInbound/sparepartSelect' })
}
const scanCodeInput = ref('')
function onScanInputConfirm() {
const code = scanCodeInput.value.trim()
if (!code) return
handleScanCode(code)
}
async function handleScanCode(code) {
let sparepartId = null
if (code.toUpperCase().startsWith('SPARE-')) sparepartId = code.replace(/SPARE-/i, '')
else { const idMatch = code.match(/(\d+)$/); if (idMatch) sparepartId = idMatch[1] }
if (!sparepartId) { uni.showToast({ title: '无法识别备件码', icon: 'none' }); return }
try {
const apiRes = await getSparepartDetail(sparepartId)
const detail = apiRes?.data || apiRes
if (detail && detail.id) {
getApp().globalData._sparepartFromScan = true
getApp().globalData._sparepartBeforeConfirm = detail
uni.navigateTo({ url: '/pages_function/pages/sparepartOutbound/sparepartConfirm' })
} else { uni.showToast({ title: '未找到备件: ' + sparepartId, icon: 'none' }) }
} catch (e) { console.error(e); uni.showToast({ title: '扫码失败', icon: 'none' }) }
}
function handleScan() {
uni.scanCode({
onlyFromCamera: false, scanType: ['barCode', 'qrCode'],
success: async (res) => {
success: (res) => {
const code = (res.result || '').trim()
if (!code) return
let sparepartId = null
if (code.toUpperCase().startsWith('SPARE-')) sparepartId = code.replace(/SPARE-/i, '')
else { const idMatch = code.match(/(\d+)$/); if (idMatch) sparepartId = idMatch[1] }
if (!sparepartId) { uni.showToast({ title: '无法识别备件码', icon: 'none' }); return }
try {
const apiRes = await getSparepartDetail(sparepartId)
const detail = apiRes?.data || apiRes
if (detail && detail.id) {
getApp().globalData._sparepartFromScan = true
getApp().globalData._sparepartBeforeConfirm = detail
uni.navigateTo({ url: '/pages_function/pages/sparepartOutbound/sparepartConfirm' })
} else { uni.showToast({ title: '未找到备件: ' + sparepartId, icon: 'none' }) }
} catch (e) { console.error(e); uni.showToast({ title: '扫码失败', icon: 'none' }) }
scanCodeInput.value = code
handleScanCode(code)
},
fail: () => { uni.showToast({ title: '扫码失败', icon: 'none' }) }
})
@ -251,8 +269,10 @@ onHide(() => { showOperatorDropdown.value = false })
<style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f5f6f8; padding-bottom: calc(120rpx + env(safe-area-inset-bottom)); }
.action-row { display: flex; gap: 20rpx; padding: 20rpx 24rpx; }
.action-btn { flex: 1; display: flex; align-items: center; justify-content: center; gap: 12rpx; height: 96rpx; border-radius: 12rpx; background: #1f4b79; color: #fff; font-size: 28rpx; font-weight: 600; .btn-icon-wrap { width: 44rpx; height: 44rpx; display: flex; align-items: center; justify-content: center; } .btn-icon { font-size: 36rpx; color: #fff; } .btn-text { font-size: 28rpx; font-weight: 600; color: #fff; } }
.action-row { display: flex; align-items: center; gap: 16rpx; padding: 20rpx 24rpx; }
.scan-input-row { flex: 1; display: flex; align-items: center; height: 72rpx; border-radius: 10rpx; overflow: hidden; }
.scan-input { flex: 1; height: 72rpx; padding: 0 20rpx; font-size: 26rpx; color: #333; background: #fff; border: 1rpx solid #d0d5dd; border-radius: 10rpx; }
.action-btn { display: flex; align-items: center; justify-content: center; height: 72rpx; padding: 0 24rpx; border-radius: 10rpx; background: #1f4b79; color: #fff; font-size: 26rpx; font-weight: 600; white-space: nowrap; .btn-icon-wrap { width: 44rpx; height: 44rpx; display: flex; align-items: center; justify-content: center; } .btn-icon { font-size: 36rpx; color: #fff; } .btn-text { font-size: 26rpx; font-weight: 600; color: #fff; } }
.form-section { padding-bottom: 16rpx; }
.section-title-bar { display: flex; align-items: center; gap: 10rpx; padding: 24rpx 24rpx 18rpx; }
.section-bar-line { width: 6rpx; height: 32rpx; border-radius: 3rpx; background: #2563eb; flex-shrink: 0; }
@ -265,7 +285,7 @@ onHide(() => { showOperatorDropdown.value = false })
.purpose-item { flex: 1; display: flex; align-items: center; justify-content: center; height: 92rpx; background: #fff; border: 2rpx solid #e5e7eb; border-radius: 14rpx; font-size: 28rpx; color: #6b7280; &.active { border-color: #2563eb; background: #eff6ff; color: #2563eb; font-weight: 600; } }
.dropdown-panel { position: absolute; top: 100%; left: 0; right: 0; z-index: 200; margin-top: 4rpx; background: #fff; border: 1rpx solid #e0e0e0; border-radius: 12rpx; box-shadow: 0 8rpx 30rpx rgba(0,0,0,0.1); overflow: hidden; }
.dropdown-scroll { height: 360rpx; }
.dropdown-scroll { max-height: 360rpx; overflow-y: auto; }
.dropdown-item { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; border-bottom: 1rpx solid #f0f0f0; &:last-child { border-bottom: 0; } &.active { background: #f0f5ff; } }
.dropdown-item-text { font-size: 27rpx; color: #333; }
.dropdown-check { font-size: 28rpx; color: #2563eb; font-weight: 700; }

@ -115,9 +115,9 @@ let searchTimer = null
function textValue(v) { if (v === 0) return '0'; if (v == null) return '-'; const s = String(v).trim(); return s || '-' }
function formatDateTime(value) {
if (!value) return '-'
if (Array.isArray(value) && value.length >= 3) { const [y,m,d,h=0,mi=0,s=0]=value; const p=n=>String(n).padStart(2,'0'); return `${y}-${p(m)}-${p(d)} ${p(h)}:${p(mi)}:${p(s)}` }
if (Array.isArray(value) && value.length >= 3) { const [y,m,d]=value; const p=n=>String(n).padStart(2,'0'); return `${y}-${p(m)}-${p(d)}` }
const date = new Date(Number(value)); if (Number.isNaN(date.getTime())) return String(value)
const p=n=>String(n).padStart(2,'0'); return `${date.getFullYear()}-${p(date.getMonth()+1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}`
const p=n=>String(n).padStart(2,'0'); return `${date.getFullYear()}-${p(date.getMonth()+1)}-${p(date.getDate())}`
}
const STATUS_MAP = { 0: '待出库', 10: '待审核', 20: '已出库', 1: '已驳回' }

@ -5,7 +5,7 @@
<view class="sparepart-section">
<view class="section-title-bar">
<view class="section-bar-line"></view>
<text class="section-title">已选备件</text>
<text class="section-title">å·²éå¤ä»?/text>
</view>
<view class="sparepart-card">
<view class="card-top">
@ -14,24 +14,24 @@
<view v-else class="card-image-empty"><text class="empty-img-icon">📦</text></view>
</view>
<view class="card-info-right">
<view class="info-item"><text class="info-label">备件名称</text><text class="info-name">{{ textValue(sparepart.name) }}</text></view>
<view class="info-item"><text class="info-label">规格</text><text class="info-value">{{ textValue(sparepart.standard || sparepart.deviceSpec) }}</text></view>
<view class="info-item"><text class="info-label">当前库存</text><text class="info-value stock-highlight">{{ sparepart.count != null ? sparepart.count : 0 }}{{ textUnit(sparepart.unitName || sparepart.minStockUnitName || '个') }}</text></view>
<view class="info-item"><text class="info-label">å¤ä»å<EFBFBD><EFBFBD>ç§°ï¼?/text><text class="info-name">{{ textValue(sparepart.name) }}</text></view>
<view class="info-item"><text class="info-label">è§æ ¼ï¼?/text><text class="info-value">{{ textValue(sparepart.standard || sparepart.deviceSpec) }}</text></view>
<view class="info-item"><text class="info-label">å½å<EFBFBD>åºå­˜ï¼?/text><text class="info-value stock-highlight">{{ sparepart.count != null ? sparepart.count : 0 }}{{ textUnit(sparepart.unitName || sparepart.minStockUnitName || 'ä¸?) }}</text></view>
</view>
</view>
<view class="card-bottom">
<view class="detail-row two-col">
<view class="detail-col"><text class="detail-label">采购单位</text><text class="detail-value">{{ textValue(sparepart.purchaseUnitName) }}</text></view>
<view class="detail-col"><text class="detail-label">库存单位</text><text class="detail-value">{{ textValue(sparepart.unitName || sparepart.minStockUnitName || '个') }}</text></view>
<view class="detail-col"><text class="detail-label">éè´­å<EFBFBD>ä½<EFBFBD>ï¼?/text><text class="detail-value">{{ textValue(sparepart.purchaseUnitName) }}</text></view>
<view class="detail-col"><text class="detail-label">åºå­˜å<EFBFBD>ä½<EFBFBD>ï¼?/text><text class="detail-value">{{ textValue(sparepart.unitName || sparepart.minStockUnitName || 'ä¸?) }}</text></view>
</view>
<view class="detail-row"><text class="detail-label">换算关系</text><text class="detail-value">1{{ textValue(sparepart.purchaseUnitName) }}={{ textValue(sparepart.purchaseUnitConvertQuantity) }}{{ stockUnitLabel(sparepart) }}</text></view>
<view class="detail-row"><text class="detail-label">æ<EFBFBD>¢ç®å³ç³»ï¼?/text><text class="detail-value">1{{ textValue(sparepart.purchaseUnitName) }}={{ textValue(sparepart.purchaseUnitConvertQuantity) }}{{ stockUnitLabel(sparepart) }}</text></view>
</view>
</view>
<!-- 出库用途 -->
<!-- åºåºç¨é?-->
<view class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view>
<text class="section-title">出库用途</text>
<text class="section-title">åºåºç¨é?/text>
</view>
<view class="purpose-card">
<view class="purpose-item" :class="{ active: selectedPurpose === 'repair' }" @click="setPurpose('repair')">
@ -59,17 +59,17 @@
<view class="warehouse-area-dropdown" @click="toggleDeviceDropdown">
<view class="dropdown-input">
<text :class="['dropdown-value', { placeholder: !selectedDevice }]">{{ selectedDevice ? selectedDevice.label : '请选择' }}</text>
<text class="dropdown-arrow"></text>
<text class="dropdown-arrow">â?/text>
</view>
<view v-if="showDeviceDropdown" class="dropdown-panel">
<scroll-view scroll-y class="dropdown-scroll">
<view class="dropdown-scroll">
<view v-for="item in filteredDeviceOptions" :key="item.value" class="dropdown-item" :class="{ active: selectedDevice?.value === item.value }" @click.stop="handleSelectDevice(item)">
<text class="dropdown-item-text">{{ item.label }}</text>
<text v-if="selectedDevice?.value === item.value" class="dropdown-check"></text>
<text v-if="selectedDevice?.value === item.value" class="dropdown-check">�/text>
</view>
<view v-if="deviceLoading" class="dropdown-empty">...</view>
<view v-if="deviceLoading" class="dropdown-empty">加载�..</view>
<view v-else-if="!filteredDeviceOptions.length" class="dropdown-empty">{{ selectedPurpose === 'maintain' ? '暂无保养设备' : '暂无设备数据' }}</view>
</scroll-view>
</view>
</view>
</view>
</view>
@ -78,16 +78,16 @@
<view class="warehouse-area-dropdown" @click="toggleOrderDropdown">
<view class="dropdown-input">
<text :class="['dropdown-value', { placeholder: !currentOrder }]">{{ currentOrder ? currentOrder.label : '请选择' }}</text>
<text class="dropdown-arrow"></text>
<text class="dropdown-arrow">â?/text>
</view>
<view v-if="showOrderDropdown" class="dropdown-panel">
<scroll-view scroll-y class="dropdown-scroll">
<view class="dropdown-scroll">
<view v-for="item in currentOrderOptions" :key="item.value" class="dropdown-item" :class="{ active: currentOrder?.value === item.value }" @click.stop="handleSelectOrder(item)">
<text class="dropdown-item-text">{{ item.label }}</text>
<text v-if="currentOrder?.value === item.value" class="dropdown-check"></text>
<text v-if="currentOrder?.value === item.value" class="dropdown-check">�/text>
</view>
<view v-if="!currentOrderOptions.length" class="dropdown-empty">{{ selectedPurpose === 'repair' ? '' : '' }}</view>
</scroll-view>
<view v-if="!currentOrderOptions.length" class="dropdown-empty">{{ selectedPurpose === 'repair' ? 'æšæ ç»´ä¿®å<EFBFBD>? : 'æšæ ä¿<EFBFBD>å»å<EFBFBD>? }}</view>
</view>
</view>
</view>
</view>
@ -101,7 +101,7 @@
<view class="qty-input-card">
<view class="form-field">
<text class="form-label">出库数量</text>
<input v-model="outboundQty" class="form-input" placeholder="请输入" confirm-type="done" />
<input v-model="outboundQty" class="form-input" placeholder="请输� confirm-type="done" />
<text class="form-suffix-text">单位{{ textValue(sparepart.purchaseUnitName) }}</text>
</view>
<view class="convert-row">
@ -113,18 +113,18 @@
<view class="convert-formula-inline">{{ outboundQty || 0 }}{{ textValue(sparepart.purchaseUnitName) }} × {{ textValue(sparepart.purchaseUnitConvertQuantity) }}{{ stockUnitLabel(sparepart) }} = {{ calculatedStock }}{{ stockUnitLabel(sparepart) }}</view>
<!-- 库存不足提示 -->
<view v-if="stockExceeded" class="stock-warning">
<text class="warning-icon"></text>
<text class="warning-icon">âš?/text>
<view class="warning-text">
<text class="warning-line">当前库存不足不能出库</text>
<text class="warning-line">当前库存{{ stockCount }}{{ stockUnitLabel(sparepart) }}{{ stockPackText }}</text>
<text class="warning-line">本次出库{{ calculatedStock }}{{ stockUnitLabel(sparepart) }}{{ outboundPackText }}</text>
<text class="warning-line">å½å<EFBFBD>åºå­˜ä¸<EFBFBD>è³ï¼Œä¸<EFBFBD>能åºåº?/text>
<text class="warning-line">å½å<EFBFBD>åºå­˜ï¼š{{ stockCount }}{{ stockUnitLabel(sparepart) }}({{ stockPackText }}ï¼?/text>
<text class="warning-line">本次åºåºï¼š{{ calculatedStock }}{{ stockUnitLabel(sparepart) }}({{ outboundPackText }}ï¼?/text>
</view>
</view>
</view>
<!-- 供应商 -->
<!-- ä¾åºå?-->
<view class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view><text class="section-title">供应商</text>
<view class="section-bar-line"></view><text class="section-title">ä¾åºå?/text>
</view>
<view class="select-row-card">
<view class="full-dropdown">
@ -143,15 +143,15 @@
<view class="warehouse-area-dropdown" @click="toggleWarehouseDropdown">
<view class="dropdown-input">
<text :class="['dropdown-value', { placeholder: !selectedWarehouse }]">{{ selectedWarehouse ? selectedWarehouse.label : '请选择' }}</text>
<text class="dropdown-arrow"></text>
<text class="dropdown-arrow">â?/text>
</view>
<view v-if="showWarehouseDropdown" class="dropdown-panel">
<scroll-view scroll-y class="dropdown-scroll">
<view class="dropdown-scroll">
<view v-for="item in warehouseOptions" :key="item.value" class="dropdown-item" :class="{ active: selectedWarehouse?.value === item.value }" @click.stop="handleSelectWarehouse(item)">
<text class="dropdown-item-text">{{ item.label }}</text>
<text v-if="selectedWarehouse?.value === item.value" class="dropdown-check"></text>
<text v-if="selectedWarehouse?.value === item.value" class="dropdown-check">�/text>
</view>
</scroll-view>
</view>
</view>
</view>
</view>
@ -159,16 +159,16 @@
<text class="warehouse-area-label">库区</text>
<view class="warehouse-area-dropdown" @click="toggleAreaDropdown">
<view class="dropdown-input">
<text :class="['dropdown-value', { placeholder: !selectedArea }]">{{ selectedArea ? selectedArea.label : (loadingAreas ? '加载中...' : '请选择') }}</text>
<text class="dropdown-arrow"></text>
<text :class="['dropdown-value', { placeholder: !selectedArea }]">{{ selectedArea ? selectedArea.label : (loadingAreas ? '加载�..' : '请选择') }}</text>
<text class="dropdown-arrow">â?/text>
</view>
<view v-if="showAreaDropdown" class="dropdown-panel">
<scroll-view scroll-y class="dropdown-scroll">
<view class="dropdown-scroll">
<view v-for="item in areaOptions" :key="item.value" class="dropdown-item" :class="{ active: selectedArea?.value === item.value }" @click.stop="handleSelectArea(item)">
<text class="dropdown-item-text">{{ item.label }}</text>
<text v-if="selectedArea?.value === item.value" class="dropdown-check"></text>
<text v-if="selectedArea?.value === item.value" class="dropdown-check">�/text>
</view>
</scroll-view>
</view>
</view>
</view>
</view>
@ -266,7 +266,7 @@ function formatStockPack(count) {
const currentStockText = computed(() => {
const text = formatStockPack(stockCount.value)
return text ? `当前库存:${text}` : ''
return text ? `当å‰<EFBFBD>库存ï¼?{text}` : ''
})
const stockPackText = computed(() => {
@ -290,23 +290,23 @@ const defaultSupplierName = computed(() => {
function textValue(v) { if (v === 0) return '0'; if (v == null) return '-'; const s = String(v).trim(); return s || '-' }
function textUnit(v) { if (v === 0) return '0'; if (v == null) return ''; return String(v).trim() }
function stockUnitLabel(item) { return item.unitName || item.minStockUnitName || '个' }
function stockUnitLabel(item) { return item.unitName || item.minStockUnitName || '� }
function handleCancel() { uni.navigateBack() }
// repair1, maintain2, other3
// ç¨é映å°ï¼šrepairâ?, maintainâ?, otherâ?
const PURPOSE_MAP = { repair: 1, maintain: 2, other: 3 }
function handleConfirm() {
if (!outboundQty.value || Number(outboundQty.value) <= 0) {
uni.showToast({ title: '请输入出库数量', icon: 'none' }); return
uni.showToast({ title: '请输入出库数�, icon: 'none' }); return
}
if (stockExceeded.value) {
uni.showModal({
title: '库存不足',
content: `当前库存:${stockCount.value}${stockUnitLabel(sparepart)}${stockPackText.value}\n本次出库${calculatedStock.value}${stockUnitLabel(sparepart)}${outboundPackText.value}`,
content: `当å‰<EFBFBD>库存ï¼?{stockCount.value}${stockUnitLabel(sparepart)}ï¼?{stockPackText.value})\n本次出库ï¼?{calculatedStock.value}${stockUnitLabel(sparepart)}ï¼?{outboundPackText.value})`,
showCancel: false,
confirmText: '知道了'
confirmText: '知é<EFBFBD>äº?
})
return
}
@ -355,7 +355,7 @@ function handleConfirm() {
}
getApp().globalData._sparepartOutboundItems.push(item)
uni.showToast({ title: '已添加: ' + (sparepart.value.name || ''), icon: 'success', duration: 1200 })
uni.showToast({ title: '已添� ' + (sparepart.value.name || ''), icon: 'success', duration: 1200 })
setTimeout(() => {
const fromScan = getApp().globalData._sparepartFromScan
getApp().globalData._sparepartFromScan = false
@ -466,8 +466,8 @@ async function loadWarehouses() {
const res = await getWarehouseSimpleList()
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
warehouseOptions.value = data.map(w => ({ value: w.id, label: w.name || String(w.id || '') }))
// ""
const defaultWh = warehouseOptions.value.find(w => (w.label || '').includes('备件仓'))
// 默认é中"å¤ä»ä»?
const defaultWh = warehouseOptions.value.find(w => (w.label || '').includes('å¤ä»ä»?))
if (defaultWh) { selectedWarehouse.value = defaultWh; loadAreas(defaultWh.value) }
} catch (e) {}
}
@ -483,8 +483,8 @@ async function loadAreas(warehouseId) {
const res = await getWarehouseAreaSimpleList(warehouseId)
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
areaOptions.value = data.map(a => ({ value: a.id, label: a.name || a.areaName || String(a.id || '') }))
// ""
const defaultArea = areaOptions.value.find(a => (a.label || '').includes('备件库'))
// 默认é中"å¤ä»åº?
const defaultArea = areaOptions.value.find(a => (a.label || '').includes('å¤ä»åº?))
if (defaultArea) selectedArea.value = defaultArea
} catch (e) {} finally { loadingAreas.value = false }
}
@ -577,7 +577,7 @@ onHide(() => { showDeviceDropdown.value = false; showOrderDropdown.value = false
.dropdown-value { flex: 1; font-size: 27rpx; color: #333; &.placeholder { color: #bbb; } }
.dropdown-arrow { font-size: 20rpx; color: #999; flex-shrink: 0; }
.dropdown-panel { position: absolute; top: 68rpx; left: 0; right: 0; z-index: 200; background: #fff; border: 1rpx solid #e0e0e0; border-radius: 12rpx; box-shadow: 0 8rpx 30rpx rgba(0,0,0,0.1); overflow: hidden; }
.dropdown-scroll { height: 360rpx; }
.dropdown-scroll { max-height: 360rpx; overflow-y: auto; }
.dropdown-item { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; border-bottom: 1rpx solid #f0f0f0; &:last-child { border-bottom: 0; } &.active { background: #f0f5ff; } }
.dropdown-item-text { font-size: 27rpx; color: #333; }
.dropdown-check { font-size: 28rpx; color: #2563eb; font-weight: 700; }

Loading…
Cancel
Save