You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
besure_app/src/pages_function/pages/moldoperate/deviceSelect.vue

402 lines
10 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="page-container">
<NavBar :title="t('moldOperate.selectDevice')" />
<!-- 搜索区 -->
<view class="search-bar">
<view class="search-input-wrap">
<text class="search-icon iconfont icon-search"></text>
<input class="search-input" v-model="searchText" :placeholder="t('equipmentLedger.placeholderDeviceName')" @input="onSearch" placeholder-class="search-placeholder" />
<text v-if="searchText" class="search-clear" @click="clearSearch">✕</text>
</view>
</view>
<!-- 设备列表 -->
<scroll-view scroll-y class="device-list" v-if="filteredList.length > 0">
<view
v-for="device in filteredList"
:key="device.id"
class="device-card"
:class="{ active: selectedId === device.id }"
@click="selectedId = device.id"
>
<view class="device-card-header">
<text class="device-name">{{ textValue(device.deviceName) }}</text>
<view class="device-status-tag" :class="getStatusClass(device.deviceStatus)">
{{ getStatusLabel(device) }}
</view>
</view>
<view class="device-card-body">
<view class="device-info-row">
<text class="info-label">{{ t('moldOperate.deviceCode') }}</text>
<text class="info-value">{{ textValue(device.deviceCode) }}</text>
</view>
<view class="device-info-row">
<text class="info-label">{{ t('moldOperate.productionLine') }}</text>
<text class="info-value">{{ getTopLineName(device.deviceLine) }}</text>
</view>
<view class="device-info-row">
<text class="info-label">{{ t('moldOperate.currentMold') }}</text>
<text class="info-value">{{ getCurrentMold(device) }}</text>
</view>
<view class="device-info-row">
<text class="info-label">{{ t('moldOperate.deviceStatus') }}</text>
<text class="info-value">{{ getStatusLabel(device) }}</text>
</view>
</view>
</view>
</scroll-view>
<!-- 空状态 -->
<view v-else class="empty-wrap">
<text v-if="loading" class="empty-text">{{ t('functionCommon.loading') }}</text>
<text v-else class="empty-text">{{ t('moldOperate.noDeviceData') }}</text>
</view>
<!-- 底部确认按钮 -->
<view class="bottom-actions">
<view class="bottom-btn confirm-btn" @click="handleConfirm">
{{ t('functionCommon.confirm') }}
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getDeviceLedgerList } from '@/api/mes/moldoperate'
import { getDeviceLineTree } from '@/api/mes/deviceLine'
import { getMoldBrandPage } from '@/api/mes/mold'
const { t } = useI18n()
const deviceList = ref([])
const selectedId = ref(null)
const searchText = ref('')
const loading = ref(false)
const lineInfoMap = ref(new Map())
function textValue(v) {
if (v === 0) return '0'
if (v == null) return '-'
const s = String(v).trim()
return s || '-'
}
function getStatusClass(status) {
const s = Number(status)
if (s === 0) return 'running-tag'
if (s === 1) return 'stop-tag'
if (s >= 2) return 'fault-tag'
return ''
}
function getStatusLabel(device) {
const status = Number(device.deviceStatus)
const map = {
0: t('moldOperate.statusRunning'),
1: t('moldOperate.statusStop'),
2: t('moldOperate.statusFault'),
3: t('moldOperate.statusFault')
}
return map[status] || textValue(device.deviceStatus) || '-'
}
// 设备名 → 在机模具名 映射表
const deviceMoldMap = ref(new Map())
async function loadDeviceMolds() {
try {
const res = await getMoldBrandPage({ pageSize: 100 })
const root = res && res.data !== undefined ? res.data : res
const pageData = root?.pageResult || root
const list = Array.isArray(pageData) ? pageData : (Array.isArray(pageData?.list) ? pageData.list : [])
const map = new Map()
for (const mold of list) {
const deviceName = mold.deviceName
if (deviceName) {
if (!map.has(deviceName)) map.set(deviceName, [])
map.get(deviceName).push(mold.name || '')
}
}
deviceMoldMap.value = map
} catch (e) {
console.error('loadDeviceMolds error', e)
}
}
// 当前在机模具 - 从模具型号接口获取
function getCurrentMold(device) {
if (!device) return t('moldOperate.noMoldOnDevice')
const deviceName = device.deviceName
if (deviceName && deviceMoldMap.value.has(deviceName)) {
const names = deviceMoldMap.value.get(deviceName) || []
return names.join('')
}
// fallback 设备台账静态字段
return textValue(device.currentMold || device.moldName) || t('moldOperate.noMoldOnDevice')
}
function flattenLineTree(nodes, parentId) {
if (!Array.isArray(nodes)) return
for (const node of nodes) {
if (node.id != null && node.name != null) {
lineInfoMap.value.set(String(node.id), {
id: node.id,
name: node.name,
parentId: node.parentId != null ? node.parentId : (parentId || null),
parentChain: node.parentChain || ''
})
}
if (Array.isArray(node.children)) {
flattenLineTree(node.children, node.id)
}
}
}
function getTopLineName(deviceLineId) {
if (deviceLineId == null) return '-'
const node = lineInfoMap.value.get(String(deviceLineId))
if (!node) return '-'
if (node.parentChain) {
const firstId = node.parentChain.split(',')[0]?.trim()
if (firstId) {
const topNode = lineInfoMap.value.get(firstId)
if (topNode) return topNode.name
}
}
let current = node
const visited = new Set()
while (current.parentId != null && current.parentId > 0 && !visited.has(current.id)) {
visited.add(current.id)
const parent = lineInfoMap.value.get(String(current.parentId))
if (!parent) break
current = parent
}
return current.name || '-'
}
async function loadLineTree() {
if (lineInfoMap.value.size > 0) return
try {
const res = await getDeviceLineTree()
const tree = (res && res.data !== undefined) ? res.data : res
const nodes = Array.isArray(tree) ? tree : (tree?.list || tree?.children || [])
flattenLineTree(nodes)
} catch (e) {
console.error('load line tree error', e)
}
}
const filteredList = computed(() => {
const keyword = searchText.value.trim().toLowerCase()
if (!keyword) return deviceList.value
return deviceList.value.filter((d) => {
return (d.deviceName || '').toLowerCase().includes(keyword) ||
(d.deviceCode || '').toLowerCase().includes(keyword) ||
(getTopLineName(d.deviceLine) || '').toLowerCase().includes(keyword)
})
})
function onSearch() {}
function clearSearch() {
searchText.value = ''
}
async function loadDevices() {
loading.value = true
try {
const res = await getDeviceLedgerList({ pageNo: 1, pageSize: 100 })
const root = res && res.data !== undefined ? res.data : res
deviceList.value = Array.isArray(root)
? root
: Array.isArray(root?.list) ? root.list
: Array.isArray(root?.rows) ? root.rows
: Array.isArray(root?.records) ? root.records
: []
} catch (e) {
console.error('loadDevices error', e)
} finally {
loading.value = false
}
}
function handleConfirm() {
if (!selectedId.value) {
uni.showToast({ title: t('moldOperate.validatorDeviceRequired'), icon: 'none' })
return
}
const device = deviceList.value.find((d) => d.id === selectedId.value)
if (device) {
const lineName = getTopLineName(device.deviceLine)
if (lineName && lineName !== '-') {
device.workshopName = lineName
}
}
// 存入 globalData 后再返回,目标页 onShow 中读取
getApp().globalData._deviceSelectResult = device || null
uni.navigateBack()
}
onShow(async () => {
await Promise.allSettled([loadDevices(), loadLineTree(), loadDeviceMolds()])
})
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: #f5f6f8;
padding-bottom: 140rpx;
}
/* 搜索栏 */
.search-bar {
padding: 16rpx 24rpx;
background: #fff;
}
.search-input-wrap {
display: flex;
align-items: center;
height: 72rpx;
padding: 0 16rpx;
background: #f5f6f8;
border-radius: 36rpx;
}
.search-icon {
font-size: 32rpx;
color: #999;
margin-right: 12rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: #333;
}
.search-placeholder {
color: #bbb;
}
.search-clear {
font-size: 28rpx;
color: #999;
padding: 8rpx;
}
/* 设备列表 */
.device-list {
padding: 16rpx 24rpx;
}
.device-card {
background: #fff;
border-radius: 14rpx;
padding: 24rpx;
margin-bottom: 16rpx;
border: 2rpx solid transparent;
&.active {
border-color: #2563eb;
background: #f0f5ff;
}
}
.device-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.device-name {
font-size: 30rpx;
font-weight: 700;
color: #1a1a1a;
}
.device-status-tag {
padding: 4rpx 16rpx;
border-radius: 6rpx;
font-size: 22rpx;
font-weight: 500;
&.running-tag { color: #059669; background: #ecfdf5; border: 1rpx solid #a7f3d0; }
&.stop-tag { color: #d97706; background: #fffbeb; border: 1rpx solid #fde68a; }
&.fault-tag { color: #dc2626; background: #fef2f2; border: 1rpx solid #fecaca; }
}
.device-card-body {
border-top: 1rpx solid #f0f0f0;
padding-top: 16rpx;
}
.device-info-row {
display: flex;
align-items: center;
margin-bottom: 10rpx;
&:last-child {
margin-bottom: 0;
}
}
.info-label {
width: 140rpx;
font-size: 24rpx;
color: #999;
flex-shrink: 0;
}
.info-value {
font-size: 26rpx;
color: #333;
}
/* 空状态 */
.empty-wrap {
padding: 120rpx 0;
text-align: center;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
/* 底部确认按钮 */
.bottom-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 18rpx 24rpx calc(18rpx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
z-index: 99;
}
.bottom-btn {
width: 100%;
height: 84rpx;
line-height: 84rpx;
text-align: center;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 600;
}
.confirm-btn {
background: #1f4b79;
color: #fff;
}
</style>