Compare commits
19 Commits
main
...
besure_bit
| Author | SHA1 | Date |
|---|---|---|
|
|
6f0510aac5 | 2 weeks ago |
|
|
7d760f03ea | 2 weeks ago |
|
|
b86ee3d2e8 | 2 weeks ago |
|
|
8d3a0129a5 | 2 weeks ago |
|
|
7e415b432b | 2 weeks ago |
|
|
c1a59ca833 | 2 weeks ago |
|
|
307a02a809 | 2 weeks ago |
|
|
60925b5ee6 | 2 weeks ago |
|
|
6c47d072b8 | 2 weeks ago |
|
|
3ad05a459b | 2 weeks ago |
|
|
a8aff70afc | 2 weeks ago |
|
|
b701a9a923 | 2 weeks ago |
|
|
8a7bcd1f31 | 2 weeks ago |
|
|
ff0b41820e | 3 weeks ago |
|
|
9a589d531a | 3 weeks ago |
|
|
9954bceaa8 | 3 weeks ago |
|
|
a4c2ccac74 | 3 weeks ago |
|
|
86058e36d1 | 3 weeks ago |
|
|
62d26cdc12 | 3 weeks ago |
@ -0,0 +1,76 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface BitWmsInboundReqVO {
|
||||
productId: number
|
||||
warehouseId?: number
|
||||
areaId?: number
|
||||
locationId: number
|
||||
count: number
|
||||
unitId: number
|
||||
supplierId?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export interface BitWmsOutboundReqVO {
|
||||
productId: number
|
||||
warehouseId?: number
|
||||
areaId?: number
|
||||
locationId: number
|
||||
count: number
|
||||
outReason: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export interface BitWmsMoveReqVO {
|
||||
productId: number
|
||||
fromLocationId: number
|
||||
toLocationId: number
|
||||
fromWarehouseId?: number
|
||||
fromAreaId?: number
|
||||
toWarehouseId?: number
|
||||
toAreaId?: number
|
||||
count: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export interface BitWmsLabelRespVO {
|
||||
title?: string
|
||||
printData: Record<string, any>
|
||||
templateJson?: Record<string, any>
|
||||
}
|
||||
|
||||
export const BitWmsApi = {
|
||||
getSamplePage: async (params: any) => {
|
||||
return await request.get({ url: '/erp/product/sample-page', params })
|
||||
},
|
||||
getSampleSimpleList: async (params?: any) => {
|
||||
return await request.get({ url: '/erp/product/sample-simple-list', params })
|
||||
},
|
||||
getLocationStockPage: async (params: any) => {
|
||||
return await request.get({ url: '/erp/bit-wms/location-stock/page', params })
|
||||
},
|
||||
getLocationStockCount: async (productId: number, locationId: number) => {
|
||||
return await request.get({ url: '/erp/bit-wms/location-stock/count', params: { productId, locationId } })
|
||||
},
|
||||
getLocationStockRecordPage: async (params: any) => {
|
||||
return await request.get({ url: '/erp/bit-wms/location-stock-record/page', params })
|
||||
},
|
||||
inbound: async (data: BitWmsInboundReqVO) => {
|
||||
return await request.post({ url: '/erp/bit-wms/inbound', data })
|
||||
},
|
||||
outbound: async (data: BitWmsOutboundReqVO) => {
|
||||
return await request.post({ url: '/erp/bit-wms/outbound', data })
|
||||
},
|
||||
move: async (data: BitWmsMoveReqVO) => {
|
||||
return await request.post({ url: '/erp/bit-wms/move', data })
|
||||
},
|
||||
scan: async (code: string) => {
|
||||
return await request.get({ url: '/erp/bit-wms/scan', params: { code } })
|
||||
},
|
||||
getSampleLabel: async (productId: number) => {
|
||||
return await request.get({ url: '/erp/bit-wms/sample-label', params: { productId } })
|
||||
},
|
||||
getLocationLabel: async (locationId: number) => {
|
||||
return await request.get({ url: '/erp/bit-wms/location-label', params: { locationId } })
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<el-button size="small" type="primary" :loading="loading" @click="openPrint">
|
||||
<Icon icon="ep:printer" class="mr-4px" />
|
||||
{{ buttonText }}
|
||||
</el-button>
|
||||
<HiprintPreviewDialog ref="printDialogRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import HiprintPreviewDialog from '@/components/HiprintPreviewDialog/index.vue'
|
||||
import { ConfigApi } from '@/api/mes/printconfig'
|
||||
import { defaultElementTypeProvider, hiwebSocket, hiprint } from 'vue-plugin-hiprint'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title: string
|
||||
buttonText?: string
|
||||
businessScene?: string
|
||||
printData?: Record<string, any>
|
||||
templateJson?: Record<string, any> | string
|
||||
loadLabel?: () => Promise<{ printData: Record<string, any>; templateJson?: Record<string, any> | string }>
|
||||
}>(),
|
||||
{
|
||||
buttonText: '打印'
|
||||
}
|
||||
)
|
||||
|
||||
const message = useMessage()
|
||||
const printDialogRef = ref()
|
||||
const loading = ref(false)
|
||||
let hiprintInited = false
|
||||
const clientHostStorageKey = 'hiprint-client-host'
|
||||
const defaultClientHost = 'http://192.168.2.58:17521'
|
||||
|
||||
const getHiwebSocket = () => {
|
||||
return ((hiwebSocket as any) || (window as any).hiwebSocket) as any
|
||||
}
|
||||
|
||||
const ensureHiprintInit = () => {
|
||||
if (hiprintInited) {
|
||||
return
|
||||
}
|
||||
hiprint.init({
|
||||
host: localStorage.getItem(clientHostStorageKey) || defaultClientHost,
|
||||
providers: [defaultElementTypeProvider()]
|
||||
})
|
||||
hiprintInited = true
|
||||
}
|
||||
|
||||
const normalizeTemplateJson = (templateJson?: Record<string, any> | string) => {
|
||||
if (!templateJson) {
|
||||
return undefined
|
||||
}
|
||||
if (typeof templateJson !== 'string') {
|
||||
return templateJson
|
||||
}
|
||||
try {
|
||||
return JSON.parse(templateJson)
|
||||
} catch (error) {
|
||||
message.error('打印模板格式异常')
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const getPaperSize = (templateJson?: Record<string, any>) => {
|
||||
const panel = templateJson?.panels?.[0]
|
||||
if (panel?.width && panel?.height) {
|
||||
return {
|
||||
width: Number(panel.width),
|
||||
height: Number(panel.height)
|
||||
}
|
||||
}
|
||||
return { width: 80, height: 60 }
|
||||
}
|
||||
|
||||
const ensureClientConnected = async () => {
|
||||
ensureHiprintInit()
|
||||
const socket = getHiwebSocket()
|
||||
if (!socket) {
|
||||
message.warning('打印客户端未初始化')
|
||||
return false
|
||||
}
|
||||
if (socket.opened) {
|
||||
return true
|
||||
}
|
||||
if (typeof socket.start !== 'function') {
|
||||
message.warning('打印客户端未提供连接方法')
|
||||
return false
|
||||
}
|
||||
return await new Promise<boolean>((resolve) => {
|
||||
socket.stop?.()
|
||||
socket.start((status: boolean) => {
|
||||
if (!status) {
|
||||
message.warning('未连接到本地打印客户端,请先确认 electron-hiprint 已启动')
|
||||
}
|
||||
resolve(Boolean(status))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const silentPrint = async (printData: Record<string, any>, templateJson?: Record<string, any>) => {
|
||||
if (!templateJson) {
|
||||
message.warning('未找到可用打印模板')
|
||||
return
|
||||
}
|
||||
const pageRes = await ConfigApi.getConfigPage({
|
||||
pageNo: 1,
|
||||
pageSize: 100,
|
||||
businessScenario: props.businessScene
|
||||
})
|
||||
const configs = (pageRes?.list || []).filter((item: any) => item.isEnabled !== false)
|
||||
if (!configs.length) {
|
||||
message.warning('未找到匹配业务场景「' + props.businessScene + '」的打印机配置')
|
||||
return
|
||||
}
|
||||
const printerName = configs[0].systemPrinterName
|
||||
if (!printerName) {
|
||||
message.warning('匹配到的打印机配置未设置系统打印机')
|
||||
return
|
||||
}
|
||||
const connected = await ensureClientConnected()
|
||||
if (!connected) {
|
||||
return
|
||||
}
|
||||
const hiprintTmpl = new hiprint.PrintTemplate({ template: templateJson })
|
||||
hiprintTmpl.on('printSuccess', () => {
|
||||
message.success('打印已发送到: ' + printerName)
|
||||
})
|
||||
hiprintTmpl.on('printError', (error: any) => {
|
||||
message.error(error?.msg || error?.message || '打印失败')
|
||||
})
|
||||
try {
|
||||
hiprintTmpl.print2(printData, {
|
||||
printer: printerName,
|
||||
title: props.title
|
||||
})
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '打印失败')
|
||||
}
|
||||
}
|
||||
|
||||
const openPrint = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const label = props.loadLabel ? await props.loadLabel() : undefined
|
||||
const printData = label?.printData || props.printData || {}
|
||||
const templateJson = normalizeTemplateJson(label?.templateJson || props.templateJson)
|
||||
if (props.businessScene) {
|
||||
await silentPrint(printData, templateJson)
|
||||
return
|
||||
}
|
||||
printDialogRef.value?.open({
|
||||
title: props.title,
|
||||
printData,
|
||||
templateJson,
|
||||
withDefaultQrcodeLayout: !templateJson,
|
||||
paperSize: getPaperSize(templateJson)
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="110px" class="bit-wms-form">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="样品" prop="productId">
|
||||
<el-select v-model="formData.productId" placeholder="请选择样品" filterable clearable class="!w-full">
|
||||
<el-option v-for="item in sampleList" :key="item.id" :label="formatSampleLabel(item)" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="库位" prop="locationId">
|
||||
<el-select
|
||||
v-model="formData.locationId"
|
||||
placeholder="请选择库位"
|
||||
filterable
|
||||
clearable
|
||||
class="!w-full"
|
||||
@change="handleLocationChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in filteredLocationList"
|
||||
:key="item.id"
|
||||
:label="formatLocationLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="数量" prop="count">
|
||||
<el-input-number v-model="formData.count" :min="1" :precision="0" class="!w-full" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="单位" prop="unitId">
|
||||
<el-select v-model="formData.unitId" placeholder="请选择单位" filterable class="!w-full">
|
||||
<el-option v-for="item in sampleUnitOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="submitLoading" v-hasPermi="['erp:bit-wms-in:create']" @click="submitForm">
|
||||
<Icon icon="ep:check" class="mr-5px" />确认入库
|
||||
</el-button>
|
||||
<el-button @click="resetForm"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BitWmsApi } from '@/api/erp/bitwms'
|
||||
import { WarehouseApi } from '@/api/erp/stock/warehouse'
|
||||
import { WarehouseAreaApi } from '@/api/erp/stock/warehousearea'
|
||||
import { WarehouseLocationApi } from '@/api/erp/stock/warehouselocation'
|
||||
|
||||
defineOptions({ name: 'ErpBitWmsIn' })
|
||||
|
||||
const DEFAULT_WAREHOUSE_NAME = '样品仓'
|
||||
const DEFAULT_AREA_NAME = '必硕三楼'
|
||||
const message = useMessage()
|
||||
const formRef = ref()
|
||||
const submitLoading = ref(false)
|
||||
const sampleList = ref<any[]>([])
|
||||
const warehouseList = ref<any[]>([])
|
||||
const areaList = ref<any[]>([])
|
||||
const locationList = ref<any[]>([])
|
||||
const formData = reactive<any>({
|
||||
productId: undefined,
|
||||
warehouseId: undefined,
|
||||
areaId: undefined,
|
||||
locationId: undefined,
|
||||
count: 1,
|
||||
unitId: undefined,
|
||||
remark: undefined
|
||||
})
|
||||
const formRules = reactive({
|
||||
productId: [{ required: true, message: '请选择样品', trigger: 'change' }],
|
||||
locationId: [{ required: true, message: '请选择库位', trigger: 'change' }],
|
||||
count: [{ required: true, message: '请输入数量', trigger: 'blur' }],
|
||||
unitId: [{ required: true, message: '请选择单位', trigger: 'change' }]
|
||||
})
|
||||
const selectedSample = computed(() => sampleList.value.find((item) => item.id === formData.productId))
|
||||
const sampleUnitOptions = computed(() => {
|
||||
const sample = selectedSample.value
|
||||
if (!sample?.unitId) return []
|
||||
return [{ id: sample.unitId, name: sample.unitName || '默认单位' }]
|
||||
})
|
||||
const filteredLocationList = computed(() => {
|
||||
if (!formData.warehouseId || !formData.areaId) return []
|
||||
return locationList.value.filter((item) => {
|
||||
const matchedWarehouse = item.warehouseId === formData.warehouseId
|
||||
const matchedArea = item.areaId === formData.areaId
|
||||
return matchedWarehouse && matchedArea
|
||||
})
|
||||
})
|
||||
|
||||
const formatSampleLabel = (item: any) => `${item.barCode || item.code || '-'} / ${item.name || '-'}`
|
||||
const formatLocationLabel = (item: any) => `${item.code || '-'} / ${item.name || '-'}`
|
||||
const applyDefaultWarehouseArea = () => {
|
||||
const warehouse = warehouseList.value.find((item) => item.name === DEFAULT_WAREHOUSE_NAME)
|
||||
if (!warehouse) {
|
||||
formData.warehouseId = undefined
|
||||
formData.areaId = undefined
|
||||
return
|
||||
}
|
||||
formData.warehouseId = warehouse.id
|
||||
const area = areaList.value.find(
|
||||
(item) => item.warehouseId === warehouse.id && item.areaName === DEFAULT_AREA_NAME
|
||||
)
|
||||
formData.areaId = area?.id
|
||||
}
|
||||
const handleLocationChange = (locationId?: number) => {
|
||||
const location = locationList.value.find((item) => item.id === locationId)
|
||||
if (!location) return
|
||||
formData.warehouseId = location.warehouseId
|
||||
formData.areaId = location.areaId
|
||||
}
|
||||
const handleSampleChange = () => {
|
||||
formData.unitId = selectedSample.value?.unitId
|
||||
}
|
||||
const resetInnerFormData = () => {
|
||||
formData.productId = undefined
|
||||
formData.locationId = undefined
|
||||
formData.count = 1
|
||||
formData.unitId = undefined
|
||||
formData.remark = undefined
|
||||
applyDefaultWarehouseArea()
|
||||
}
|
||||
const resetForm = () => {
|
||||
formRef.value?.resetFields()
|
||||
resetInnerFormData()
|
||||
}
|
||||
const buildSubmitData = () => ({
|
||||
productId: formData.productId,
|
||||
locationId: formData.locationId,
|
||||
warehouseId: formData.warehouseId,
|
||||
areaId: formData.areaId,
|
||||
count: formData.count,
|
||||
unitId: formData.unitId,
|
||||
remark: formData.remark
|
||||
})
|
||||
|
||||
watch(
|
||||
() => formData.productId,
|
||||
() => handleSampleChange()
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [formData.warehouseId, formData.areaId],
|
||||
() => {
|
||||
if (
|
||||
formData.locationId &&
|
||||
!filteredLocationList.value.some((item) => item.id === formData.locationId)
|
||||
) {
|
||||
formData.locationId = undefined
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const submitForm = async () => {
|
||||
await formRef.value.validate()
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await BitWmsApi.inbound(buildSubmitData())
|
||||
message.success('入库成功')
|
||||
resetForm()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const [samples, warehouses, areas, locations] = await Promise.all([
|
||||
BitWmsApi.getSampleSimpleList(),
|
||||
WarehouseApi.getWarehouseSimpleList(),
|
||||
WarehouseAreaApi.getWarehouseAreaPage({ pageNo: 1, pageSize: 100 }),
|
||||
WarehouseLocationApi.getWarehouseLocationPage({ pageNo: 1, pageSize: 100 })
|
||||
])
|
||||
sampleList.value = samples || []
|
||||
warehouseList.value = warehouses || []
|
||||
areaList.value = areas.list || []
|
||||
locationList.value = locations.list || []
|
||||
resetInnerFormData()
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="!formVisible">
|
||||
<ContentWrap>
|
||||
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="auto">
|
||||
<el-form-item label="仓库" prop="warehouseId">
|
||||
<el-select v-model="queryParams.warehouseId" placeholder="请选择仓库" clearable filterable class="!w-220px" @change="handleWarehouseChange">
|
||||
<el-option v-for="item in warehouseList" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="库区" prop="areaId">
|
||||
<el-select v-model="queryParams.areaId" placeholder="请选择库区" clearable filterable class="!w-220px">
|
||||
<el-option v-for="item in filteredAreaList" :key="item.id" :label="item.areaName" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="库位码" prop="code">
|
||||
<el-input v-model="queryParams.code" placeholder="请输入库位码" clearable class="!w-220px" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="库位名称" prop="name">
|
||||
<el-input v-model="queryParams.name" placeholder="请输入库位名称" clearable class="!w-220px" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />查询</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
|
||||
<el-button type="primary" plain v-hasPermi="['erp:bit-wms-location:create']" @click="openForm('create')">
|
||||
<Icon icon="ep:plus" class="mr-5px" />新增库位
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" row-key="id">
|
||||
<el-table-column label="仓库" align="center" min-width="140">
|
||||
<template #default="{ row }">{{ getWarehouseName(row.warehouseId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库区" align="center" min-width="120">
|
||||
<template #default="{ row }">{{ getAreaName(row.areaId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库位码" align="center" prop="code" min-width="140" />
|
||||
<el-table-column label="库位名称" align="center" prop="name" min-width="140" />
|
||||
<el-table-column label="面积" align="center" prop="areaSize" width="100" />
|
||||
<el-table-column label="最大承重" align="center" prop="maxLoadWeight" width="110" />
|
||||
<el-table-column label="状态" align="center" prop="status" width="100">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="210" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" v-hasPermi="['erp:bit-wms-location:update']" @click="openForm('update', row.id)">编辑</el-button>
|
||||
<BitWmsPrintButton
|
||||
v-hasPermi="['erp:bit-wms-location:print']"
|
||||
title="库位标签"
|
||||
:load-label="() => loadLocationLabel(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<WarehouseLocationForm v-else ref="formRef" @success="getList" @closed="formVisible = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { WarehouseLocationApi } from '@/api/erp/stock/warehouselocation'
|
||||
import { WarehouseApi } from '@/api/erp/stock/warehouse'
|
||||
import { WarehouseAreaApi } from '@/api/erp/stock/warehousearea'
|
||||
import { BitWmsApi } from '@/api/erp/bitwms'
|
||||
import WarehouseLocationForm from '@/views/erp/warehouselocation/WarehouseLocationForm.vue'
|
||||
import BitWmsPrintButton from '../components/BitWmsPrintButton.vue'
|
||||
|
||||
defineOptions({ name: 'ErpBitWmsLocation' })
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const queryFormRef = ref()
|
||||
const formRef = ref()
|
||||
const formVisible = ref(false)
|
||||
const warehouseList = ref<any[]>([])
|
||||
const areaList = ref<any[]>([])
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
warehouseId: undefined,
|
||||
areaId: undefined,
|
||||
code: undefined,
|
||||
name: undefined
|
||||
})
|
||||
|
||||
const filteredAreaList = computed(() => {
|
||||
if (!queryParams.warehouseId) return areaList.value
|
||||
return areaList.value.filter((item) => item.warehouseId === queryParams.warehouseId)
|
||||
})
|
||||
|
||||
const buildQueryParams = () =>
|
||||
Object.fromEntries(Object.entries(queryParams).filter(([, value]) => value !== undefined && value !== null && value !== ''))
|
||||
|
||||
const getWarehouseName = (warehouseId: number) => warehouseList.value.find((item) => item.id === warehouseId)?.name || '-'
|
||||
const getAreaName = (areaId: number) => areaList.value.find((item) => item.id === areaId)?.areaName || '-'
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await WarehouseLocationApi.getWarehouseLocationPage(buildQueryParams())
|
||||
list.value = data.list || []
|
||||
total.value = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadBaseData = async () => {
|
||||
warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
|
||||
const areaData = await WarehouseAreaApi.getWarehouseAreaPage({ pageNo: 1, pageSize: 100 })
|
||||
areaList.value = areaData.list || []
|
||||
}
|
||||
|
||||
const handleWarehouseChange = () => {
|
||||
queryParams.areaId = undefined
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const openForm = async (type: string, id?: number) => {
|
||||
formVisible.value = true
|
||||
await nextTick()
|
||||
formRef.value.open(type, id, type === 'create'
|
||||
? {
|
||||
defaultWarehouseName: '样品仓',
|
||||
defaultAreaName: '必硕三楼',
|
||||
defaultData: {
|
||||
isCode: false,
|
||||
allowProductMix: true,
|
||||
allowBatchMix: true
|
||||
},
|
||||
hideAutoCodeSwitch: true,
|
||||
hiddenFields: [
|
||||
'areaSize',
|
||||
'maxLoadWeight',
|
||||
'positionX',
|
||||
'positionY',
|
||||
'positionZ',
|
||||
'allowProductMix',
|
||||
'allowBatchMix'
|
||||
]
|
||||
}
|
||||
: undefined)
|
||||
}
|
||||
|
||||
const loadLocationLabel = async (id: number) => await BitWmsApi.getLocationLabel(id)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadBaseData()
|
||||
await getList()
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px" class="bit-wms-form">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="样品" prop="productId">
|
||||
<el-select
|
||||
v-model="formData.productId"
|
||||
placeholder="请选择样品"
|
||||
filterable
|
||||
clearable
|
||||
class="!w-full"
|
||||
@change="loadCurrentStock"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in sampleList"
|
||||
:key="item.id"
|
||||
:label="formatSampleLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="来源库存">
|
||||
<el-input :model-value="currentStockText" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="来源库位" prop="fromLocationId">
|
||||
<el-select
|
||||
v-model="formData.fromLocationId"
|
||||
placeholder="请选择来源库位"
|
||||
filterable
|
||||
clearable
|
||||
class="!w-full"
|
||||
@change="handleFromLocationChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in filteredLocationList"
|
||||
:key="item.id"
|
||||
:label="formatLocationLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="目标库位" prop="toLocationId">
|
||||
<el-select
|
||||
v-model="formData.toLocationId"
|
||||
placeholder="请选择目标库位"
|
||||
filterable
|
||||
clearable
|
||||
class="!w-full"
|
||||
@change="handleToLocationChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in filteredLocationList"
|
||||
:key="item.id"
|
||||
:label="formatLocationLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="数量" prop="count">
|
||||
<el-input-number v-model="formData.count" :min="1" :precision="0" class="!w-240px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="submitLoading" v-hasPermi="['erp:bit-wms-move:create']" @click="submitForm">
|
||||
<Icon icon="ep:check" class="mr-5px" />确认移库
|
||||
</el-button>
|
||||
<el-button @click="resetForm"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BitWmsApi } from '@/api/erp/bitwms'
|
||||
import { WarehouseApi } from '@/api/erp/stock/warehouse'
|
||||
import { WarehouseAreaApi } from '@/api/erp/stock/warehousearea'
|
||||
import { WarehouseLocationApi } from '@/api/erp/stock/warehouselocation'
|
||||
|
||||
defineOptions({ name: 'ErpBitWmsMove' })
|
||||
|
||||
const DEFAULT_WAREHOUSE_NAME = '样品仓'
|
||||
const DEFAULT_AREA_NAME = '必硕三楼'
|
||||
const message = useMessage()
|
||||
const formRef = ref()
|
||||
const submitLoading = ref(false)
|
||||
const stockLoading = ref(false)
|
||||
const currentStock = ref<number | undefined>(undefined)
|
||||
const sampleList = ref<any[]>([])
|
||||
const warehouseList = ref<any[]>([])
|
||||
const areaList = ref<any[]>([])
|
||||
const locationList = ref<any[]>([])
|
||||
const formData = reactive<any>({
|
||||
productId: undefined,
|
||||
fromLocationId: undefined,
|
||||
fromWarehouseId: undefined,
|
||||
fromAreaId: undefined,
|
||||
toLocationId: undefined,
|
||||
toWarehouseId: undefined,
|
||||
toAreaId: undefined,
|
||||
count: 1,
|
||||
remark: undefined
|
||||
})
|
||||
const formRules = reactive({
|
||||
productId: [{ required: true, message: '请选择样品', trigger: 'change' }],
|
||||
fromLocationId: [{ required: true, message: '请选择来源库位', trigger: 'change' }],
|
||||
toLocationId: [{ required: true, message: '请选择目标库位', trigger: 'change' }],
|
||||
count: [{ required: true, message: '请输入数量', trigger: 'blur' }]
|
||||
})
|
||||
const defaultWarehouseId = computed(
|
||||
() => warehouseList.value.find((item) => item.name === DEFAULT_WAREHOUSE_NAME)?.id
|
||||
)
|
||||
const defaultAreaId = computed(() => {
|
||||
if (!defaultWarehouseId.value) return undefined
|
||||
return areaList.value.find(
|
||||
(item) => item.warehouseId === defaultWarehouseId.value && item.areaName === DEFAULT_AREA_NAME
|
||||
)?.id
|
||||
})
|
||||
const filteredLocationList = computed(() =>
|
||||
locationList.value.filter((item) => {
|
||||
if (defaultWarehouseId.value && item.warehouseId !== defaultWarehouseId.value) return false
|
||||
if (defaultAreaId.value && item.areaId !== defaultAreaId.value) return false
|
||||
return true
|
||||
})
|
||||
)
|
||||
const currentStockText = computed(() => {
|
||||
if (stockLoading.value) return '查询中...'
|
||||
return currentStock.value === undefined ? '-' : `${currentStock.value}`
|
||||
})
|
||||
|
||||
const formatSampleLabel = (item: any) => `${item.barCode || item.code || '-'} / ${item.name || '-'}`
|
||||
const formatLocationLabel = (item: any) => `${item.code || '-'} / ${item.name || '-'}`
|
||||
const fillLocationFields = (prefix: 'from' | 'to', locationId?: number) => {
|
||||
const location = locationList.value.find((item) => item.id === locationId)
|
||||
formData[`${prefix}WarehouseId`] = location?.warehouseId
|
||||
formData[`${prefix}AreaId`] = location?.areaId
|
||||
}
|
||||
const handleFromLocationChange = (locationId?: number) => {
|
||||
fillLocationFields('from', locationId)
|
||||
loadCurrentStock()
|
||||
}
|
||||
const handleToLocationChange = (locationId?: number) => {
|
||||
fillLocationFields('to', locationId)
|
||||
}
|
||||
const loadCurrentStock = async () => {
|
||||
if (!formData.productId || !formData.fromLocationId) {
|
||||
currentStock.value = undefined
|
||||
return
|
||||
}
|
||||
stockLoading.value = true
|
||||
try {
|
||||
currentStock.value = await BitWmsApi.getLocationStockCount(
|
||||
formData.productId,
|
||||
formData.fromLocationId
|
||||
)
|
||||
} finally {
|
||||
stockLoading.value = false
|
||||
}
|
||||
}
|
||||
const resetInnerFormData = () => {
|
||||
formData.productId = undefined
|
||||
formData.fromLocationId = undefined
|
||||
formData.fromWarehouseId = undefined
|
||||
formData.fromAreaId = undefined
|
||||
formData.toLocationId = undefined
|
||||
formData.toWarehouseId = undefined
|
||||
formData.toAreaId = undefined
|
||||
formData.count = 1
|
||||
formData.remark = undefined
|
||||
currentStock.value = undefined
|
||||
}
|
||||
const resetForm = () => {
|
||||
formRef.value?.resetFields()
|
||||
resetInnerFormData()
|
||||
}
|
||||
const submitForm = async () => {
|
||||
await formRef.value.validate()
|
||||
if (formData.fromLocationId === formData.toLocationId) {
|
||||
message.warning('来源库位和目标库位不能相同')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await BitWmsApi.move(formData)
|
||||
message.success('移库成功')
|
||||
await loadCurrentStock()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const [samples, warehouses, areas, locations] = await Promise.all([
|
||||
BitWmsApi.getSampleSimpleList(),
|
||||
WarehouseApi.getWarehouseSimpleList(),
|
||||
WarehouseAreaApi.getWarehouseAreaPage({ pageNo: 1, pageSize: 100 }),
|
||||
WarehouseLocationApi.getWarehouseLocationPage({ pageNo: 1, pageSize: 100 })
|
||||
])
|
||||
sampleList.value = samples || []
|
||||
warehouseList.value = warehouses || []
|
||||
areaList.value = areas.list || []
|
||||
locationList.value = locations.list || []
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="110px" class="bit-wms-form">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="样品" prop="productId">
|
||||
<el-select v-model="formData.productId" placeholder="请选择样品" filterable clearable class="!w-full" @change="loadCurrentStock">
|
||||
<el-option v-for="item in sampleList" :key="item.id" :label="formatSampleLabel(item)" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="当前库存">
|
||||
<el-input :model-value="currentStockText" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="库位" prop="locationId">
|
||||
<el-select
|
||||
v-model="formData.locationId"
|
||||
placeholder="请选择库位"
|
||||
filterable
|
||||
clearable
|
||||
class="!w-full"
|
||||
@change="handleLocationChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in filteredLocationList"
|
||||
:key="item.id"
|
||||
:label="formatLocationLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="数量" prop="count">
|
||||
<el-input-number v-model="formData.count" :min="1" :precision="0" class="!w-full" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="出库原因" prop="outReason">
|
||||
<el-select v-model="formData.outReason" placeholder="请选择出库原因" class="!w-full">
|
||||
<el-option v-for="dict in getStrDictOptions(DICT_TYPE.BIT_SAMPLE_OUT_REASON)" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="submitLoading" v-hasPermi="['erp:bit-wms-out:create']" @click="submitForm">
|
||||
<Icon icon="ep:check" class="mr-5px" />确认出库
|
||||
</el-button>
|
||||
<el-button @click="resetForm"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
|
||||
import { BitWmsApi } from '@/api/erp/bitwms'
|
||||
import { WarehouseApi } from '@/api/erp/stock/warehouse'
|
||||
import { WarehouseAreaApi } from '@/api/erp/stock/warehousearea'
|
||||
import { WarehouseLocationApi } from '@/api/erp/stock/warehouselocation'
|
||||
|
||||
defineOptions({ name: 'ErpBitWmsOut' })
|
||||
|
||||
const DEFAULT_WAREHOUSE_NAME = '样品仓'
|
||||
const DEFAULT_AREA_NAME = '必硕三楼'
|
||||
const message = useMessage()
|
||||
const formRef = ref()
|
||||
const submitLoading = ref(false)
|
||||
const stockLoading = ref(false)
|
||||
const currentStock = ref<number | undefined>(undefined)
|
||||
const sampleList = ref<any[]>([])
|
||||
const warehouseList = ref<any[]>([])
|
||||
const areaList = ref<any[]>([])
|
||||
const locationList = ref<any[]>([])
|
||||
const formData = reactive<any>({
|
||||
productId: undefined,
|
||||
warehouseId: undefined,
|
||||
areaId: undefined,
|
||||
locationId: undefined,
|
||||
count: 1,
|
||||
outReason: undefined,
|
||||
remark: undefined
|
||||
})
|
||||
const formRules = reactive({
|
||||
productId: [{ required: true, message: '请选择样品', trigger: 'change' }],
|
||||
locationId: [{ required: true, message: '请选择库位', trigger: 'change' }],
|
||||
count: [{ required: true, message: '请输入数量', trigger: 'blur' }],
|
||||
outReason: [{ required: true, message: '请选择出库原因', trigger: 'change' }]
|
||||
})
|
||||
const filteredLocationList = computed(() => {
|
||||
if (!formData.warehouseId || !formData.areaId) return []
|
||||
return locationList.value.filter((item) => item.warehouseId === formData.warehouseId && item.areaId === formData.areaId)
|
||||
})
|
||||
const currentStockText = computed(() => {
|
||||
if (stockLoading.value) return '查询中...'
|
||||
return currentStock.value === undefined ? '-' : `${currentStock.value}`
|
||||
})
|
||||
|
||||
const formatSampleLabel = (item: any) => `${item.barCode || item.code || '-'} / ${item.name || '-'}`
|
||||
const formatLocationLabel = (item: any) => `${item.code || '-'} / ${item.name || '-'}`
|
||||
const applyDefaultWarehouseArea = () => {
|
||||
const warehouse = warehouseList.value.find((item) => item.name === DEFAULT_WAREHOUSE_NAME)
|
||||
if (!warehouse) {
|
||||
formData.warehouseId = undefined
|
||||
formData.areaId = undefined
|
||||
return
|
||||
}
|
||||
formData.warehouseId = warehouse.id
|
||||
const area = areaList.value.find(
|
||||
(item) => item.warehouseId === warehouse.id && item.areaName === DEFAULT_AREA_NAME
|
||||
)
|
||||
formData.areaId = area?.id
|
||||
}
|
||||
const handleLocationChange = (locationId?: number) => {
|
||||
const location = locationList.value.find((item) => item.id === locationId)
|
||||
if (!location) {
|
||||
currentStock.value = undefined
|
||||
return
|
||||
}
|
||||
formData.warehouseId = location.warehouseId
|
||||
formData.areaId = location.areaId
|
||||
loadCurrentStock()
|
||||
}
|
||||
const loadCurrentStock = async () => {
|
||||
if (!formData.productId || !formData.locationId) {
|
||||
currentStock.value = undefined
|
||||
return
|
||||
}
|
||||
stockLoading.value = true
|
||||
try {
|
||||
currentStock.value = await BitWmsApi.getLocationStockCount(
|
||||
formData.productId,
|
||||
formData.locationId
|
||||
)
|
||||
} finally {
|
||||
stockLoading.value = false
|
||||
}
|
||||
}
|
||||
const resetInnerFormData = () => {
|
||||
formData.productId = undefined
|
||||
formData.locationId = undefined
|
||||
formData.count = 1
|
||||
formData.outReason = undefined
|
||||
formData.remark = undefined
|
||||
currentStock.value = undefined
|
||||
applyDefaultWarehouseArea()
|
||||
}
|
||||
const resetForm = () => {
|
||||
formRef.value?.resetFields()
|
||||
resetInnerFormData()
|
||||
}
|
||||
const submitForm = async () => {
|
||||
await formRef.value.validate()
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await BitWmsApi.outbound(formData)
|
||||
message.success('出库成功')
|
||||
await loadCurrentStock()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const [samples, warehouses, areas, locations] = await Promise.all([
|
||||
BitWmsApi.getSampleSimpleList(),
|
||||
WarehouseApi.getWarehouseSimpleList(),
|
||||
WarehouseAreaApi.getWarehouseAreaPage({ pageNo: 1, pageSize: 100 }),
|
||||
WarehouseLocationApi.getWarehouseLocationPage({ pageNo: 1, pageSize: 100 })
|
||||
])
|
||||
sampleList.value = samples || []
|
||||
warehouseList.value = warehouses || []
|
||||
areaList.value = areas.list || []
|
||||
locationList.value = locations.list || []
|
||||
resetInnerFormData()
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="auto"
|
||||
>
|
||||
<el-form-item label="样品" prop="productId">
|
||||
<el-select
|
||||
v-model="queryParams.productId"
|
||||
placeholder="请选择样品"
|
||||
clearable
|
||||
filterable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in sampleList"
|
||||
:key="item.id"
|
||||
:label="formatSampleLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="仓库" prop="warehouseId">
|
||||
<el-select
|
||||
v-model="queryParams.warehouseId"
|
||||
placeholder="请选择仓库"
|
||||
clearable
|
||||
filterable
|
||||
class="!w-220px"
|
||||
@change="handleWarehouseChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in warehouseList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="库区" prop="areaId">
|
||||
<el-select
|
||||
v-model="queryParams.areaId"
|
||||
placeholder="请选择库区"
|
||||
clearable
|
||||
filterable
|
||||
class="!w-220px"
|
||||
@change="handleAreaChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in filteredAreaList"
|
||||
:key="item.id"
|
||||
:label="item.areaName"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="库位" prop="locationId">
|
||||
<el-select
|
||||
v-model="queryParams.locationId"
|
||||
placeholder="请选择库位"
|
||||
clearable
|
||||
filterable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in filteredLocationList"
|
||||
:key="item.id"
|
||||
:label="formatLocationLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务单号" prop="bizNo">
|
||||
<el-input
|
||||
v-model="queryParams.bizNo"
|
||||
placeholder="请输入业务单号"
|
||||
clearable
|
||||
class="!w-220px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />查询</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<ContentWrap>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
:stripe="true"
|
||||
:show-overflow-tooltip="true"
|
||||
row-key="id"
|
||||
>
|
||||
<el-table-column label="业务单号" align="center" prop="bizNo" min-width="170" />
|
||||
<el-table-column label="方向" align="center" prop="bizDirection" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.bizDirection" :type="getDirectionTagType(row.bizDirection)">{{
|
||||
row.bizDirection
|
||||
}}</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="样品名称" align="left" prop="productName" min-width="180" />
|
||||
<!-- <el-table-column label="分类" align="center" prop="categoryName" min-width="120" /> -->
|
||||
<el-table-column label="仓库" align="center" prop="warehouseName" min-width="140" />
|
||||
<el-table-column label="库区" align="center" prop="areaName" min-width="120" />
|
||||
<el-table-column label="库位编码" align="center" prop="locationCode" min-width="130" />
|
||||
<el-table-column label="库位名称" align="center" prop="locationName" min-width="140" />
|
||||
<el-table-column label="变动数量" align="center" prop="count" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span :class="getDirectionClass(row.bizDirection)">{{
|
||||
formatCount(row.count, row.unitName, true, row.bizDirection)
|
||||
}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结存" align="center" prop="totalCount" min-width="120">
|
||||
<template #default="{ row }">{{ formatCount(row.totalCount, row.unitName) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作人" align="center" prop="creatorName" min-width="110" />
|
||||
<el-table-column
|
||||
label="操作时间"
|
||||
align="center"
|
||||
prop="recordTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180"
|
||||
/>
|
||||
</el-table>
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { BitWmsApi } from '@/api/erp/bitwms'
|
||||
import { WarehouseApi } from '@/api/erp/stock/warehouse'
|
||||
import { WarehouseAreaApi } from '@/api/erp/stock/warehousearea'
|
||||
import { WarehouseLocationApi } from '@/api/erp/stock/warehouselocation'
|
||||
|
||||
defineOptions({ name: 'ErpBitWmsRecord' })
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const queryFormRef = ref()
|
||||
const sampleList = ref<any[]>([])
|
||||
const warehouseList = ref<any[]>([])
|
||||
const areaList = ref<any[]>([])
|
||||
const locationList = ref<any[]>([])
|
||||
const queryParams = reactive<any>({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
productId: undefined,
|
||||
warehouseId: undefined,
|
||||
areaId: undefined,
|
||||
locationId: undefined,
|
||||
bizNo: undefined
|
||||
})
|
||||
const filteredAreaList = computed(() => {
|
||||
if (!queryParams.warehouseId) return areaList.value
|
||||
return areaList.value.filter((item) => item.warehouseId === queryParams.warehouseId)
|
||||
})
|
||||
const filteredLocationList = computed(() =>
|
||||
locationList.value.filter((item) => {
|
||||
if (queryParams.warehouseId && item.warehouseId !== queryParams.warehouseId) return false
|
||||
if (queryParams.areaId && item.areaId !== queryParams.areaId) return false
|
||||
return true
|
||||
})
|
||||
)
|
||||
|
||||
const formatSampleLabel = (item: any) => `${item.barCode || item.code || '-'} / ${item.name || '-'}`
|
||||
const formatLocationLabel = (item: any) => `${item.code || '-'} / ${item.name || '-'}`
|
||||
const isStockIn = (direction?: string) => direction === '入库'
|
||||
const isStockOut = (direction?: string) => direction === '出库'
|
||||
const getDirectionTagType = (direction?: string) => {
|
||||
if (isStockIn(direction)) return 'success'
|
||||
if (isStockOut(direction)) return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
const getDirectionClass = (direction?: string) => {
|
||||
if (isStockIn(direction)) return 'bit-wms-record-in'
|
||||
if (isStockOut(direction)) return 'bit-wms-record-out'
|
||||
return ''
|
||||
}
|
||||
const formatNumber = (value: number | string | undefined) => {
|
||||
if (value === undefined || value === null || value === '') return '-'
|
||||
const num = Number(value)
|
||||
return Number.isFinite(num) ? num.toLocaleString() : String(value)
|
||||
}
|
||||
const formatCount = (
|
||||
value: number | string | undefined,
|
||||
unit?: string,
|
||||
withSign = false,
|
||||
direction?: string
|
||||
) => {
|
||||
const unitText = unit ? ` ${unit}` : ''
|
||||
if (!withSign) return `${formatNumber(value)}${unitText}`
|
||||
const num = Number(value)
|
||||
if (!Number.isFinite(num)) return `${value ?? '-'}${unitText}`
|
||||
const absValue = Math.abs(num).toLocaleString()
|
||||
if (isStockIn(direction)) return `+${absValue}${unitText}`
|
||||
if (isStockOut(direction)) return `-${absValue}${unitText}`
|
||||
return `${formatNumber(value)}${unitText}`
|
||||
}
|
||||
const buildQueryParams = () =>
|
||||
Object.fromEntries(
|
||||
Object.entries(queryParams).filter(
|
||||
([, value]) => value !== undefined && value !== null && value !== ''
|
||||
)
|
||||
)
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await BitWmsApi.getLocationStockRecordPage(buildQueryParams())
|
||||
list.value = data.list || []
|
||||
total.value = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const handleWarehouseChange = () => {
|
||||
queryParams.areaId = undefined
|
||||
queryParams.locationId = undefined
|
||||
}
|
||||
const handleAreaChange = () => {
|
||||
queryParams.locationId = undefined
|
||||
}
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const [samples, warehouses, areas, locations] = await Promise.all([
|
||||
BitWmsApi.getSampleSimpleList(),
|
||||
WarehouseApi.getWarehouseSimpleList(),
|
||||
WarehouseAreaApi.getWarehouseAreaPage({ pageNo: 1, pageSize: 100 }),
|
||||
WarehouseLocationApi.getWarehouseLocationPage({ pageNo: 1, pageSize: 100 })
|
||||
])
|
||||
sampleList.value = samples || []
|
||||
warehouseList.value = warehouses || []
|
||||
areaList.value = areas.list || []
|
||||
locationList.value = locations.list || []
|
||||
await getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bit-wms-record-in {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.bit-wms-record-out {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<template v-if="!formVisible">
|
||||
<ContentWrap>
|
||||
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="auto">
|
||||
<el-form-item label="样品码" prop="barCode">
|
||||
<el-input v-model="queryParams.barCode" placeholder="请输入样品码" clearable class="!w-220px" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="样品名称" prop="name">
|
||||
<el-input v-model="queryParams.name" placeholder="请输入样品名称" clearable class="!w-220px" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="样品类型" prop="categoryType">
|
||||
<el-select v-model="queryParams.categoryType" placeholder="请选择样品类型" clearable class="!w-180px">
|
||||
<el-option v-for="dict in getIntDictOptions(DICT_TYPE.MATERIAL_CLASSIFICATION_TYPE)" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="样品分类" prop="sampleCategory">
|
||||
<el-select v-model="queryParams.sampleCategory" placeholder="请选择样品分类" clearable class="!w-180px">
|
||||
<el-option v-for="dict in getStrDictOptions(DICT_TYPE.BIT_SAMPLE_CATEGORY)" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户名称" prop="customerName">
|
||||
<el-input v-model="queryParams.customerName" placeholder="请输入客户名称" clearable class="!w-220px" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />查询</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
|
||||
<el-button type="primary" plain v-hasPermi="['erp:bit-wms-sample:create']" @click="openProductCreate">
|
||||
<Icon icon="ep:plus" class="mr-5px" />新增样品
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" row-key="id">
|
||||
<el-table-column label="样品码" align="center" prop="barCode" min-width="160" />
|
||||
<el-table-column label="样品名称" align="left" prop="name" min-width="180" />
|
||||
<el-table-column label="样品类型" align="center" prop="categoryType" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :type="DICT_TYPE.MATERIAL_CLASSIFICATION_TYPE" :value="row.categoryType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="样品分类" align="center" prop="sampleCategory" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :type="DICT_TYPE.BIT_SAMPLE_CATEGORY" :value="row.sampleCategory" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="规格" align="center" prop="standard" min-width="140" />
|
||||
<el-table-column label="客户名称" align="center" prop="customerName" min-width="140" />
|
||||
<el-table-column label="默认仓库" align="center" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ row.defaultWarehouseName || row.warehouseName || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status" width="100">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="210" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openProductDetail(row.id)">详情</el-button>
|
||||
<el-button link type="primary" v-hasPermi="['erp:bit-wms-sample:update']" @click="openProductEdit(row.id)">编辑</el-button>
|
||||
<BitWmsPrintButton
|
||||
v-hasPermi="['erp:bit-wms-sample:print']"
|
||||
title="样品标签"
|
||||
:load-label="() => loadSampleLabel(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<ProductForm v-else ref="formRef" @success="handleFormSuccess" @closed="handleFormClosed" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
|
||||
import { BitWmsApi } from '@/api/erp/bitwms'
|
||||
import BitWmsPrintButton from '../components/BitWmsPrintButton.vue'
|
||||
import ProductForm from '@/views/erp/product/product/ProductForm.vue'
|
||||
|
||||
defineOptions({ name: 'ErpBitWmsSample' })
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const queryFormRef = ref()
|
||||
const formRef = ref()
|
||||
const formVisible = ref(false)
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
barCode: undefined,
|
||||
name: undefined,
|
||||
categoryType: undefined,
|
||||
sampleCategory: undefined,
|
||||
customerName: undefined
|
||||
})
|
||||
|
||||
const buildQueryParams = () =>
|
||||
Object.fromEntries(Object.entries(queryParams).filter(([, value]) => value !== undefined && value !== null && value !== ''))
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await BitWmsApi.getSamplePage(buildQueryParams())
|
||||
list.value = data.list || []
|
||||
total.value = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const loadSampleLabel = async (id: number) => await BitWmsApi.getSampleLabel(id)
|
||||
|
||||
const openProductForm = async (type: 'create' | 'update' | 'detail', id?: number) => {
|
||||
formVisible.value = true
|
||||
await nextTick()
|
||||
const defaultData = {
|
||||
sampleMode: true,
|
||||
...(type === 'create'
|
||||
? {
|
||||
isSample: true,
|
||||
bizUnit: 'BIT'
|
||||
}
|
||||
: {})
|
||||
}
|
||||
formRef.value.open(type, id, defaultData)
|
||||
}
|
||||
const openProductCreate = () => openProductForm('create')
|
||||
const openProductEdit = (id: number) => openProductForm('update', id)
|
||||
const openProductDetail = (id: number) => openProductForm('detail', id)
|
||||
const handleFormSuccess = async () => {
|
||||
formVisible.value = false
|
||||
await getList()
|
||||
}
|
||||
const handleFormClosed = () => {
|
||||
formVisible.value = false
|
||||
}
|
||||
|
||||
onMounted(getList)
|
||||
</script>
|
||||
@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="auto"
|
||||
>
|
||||
<el-form-item label="样品" prop="productId">
|
||||
<el-select
|
||||
v-model="queryParams.productId"
|
||||
placeholder="请选择样品"
|
||||
clearable
|
||||
filterable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in sampleList"
|
||||
:key="item.id"
|
||||
:label="formatSampleLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="仓库" prop="warehouseId">
|
||||
<el-select
|
||||
v-model="queryParams.warehouseId"
|
||||
placeholder="请选择仓库"
|
||||
clearable
|
||||
filterable
|
||||
class="!w-220px"
|
||||
@change="handleWarehouseChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in warehouseList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="库区" prop="areaId">
|
||||
<el-select
|
||||
v-model="queryParams.areaId"
|
||||
placeholder="请选择库区"
|
||||
clearable
|
||||
filterable
|
||||
class="!w-220px"
|
||||
@change="handleAreaChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in filteredAreaList"
|
||||
:key="item.id"
|
||||
:label="item.areaName"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="库位" prop="locationId">
|
||||
<el-select
|
||||
v-model="queryParams.locationId"
|
||||
placeholder="请选择库位"
|
||||
clearable
|
||||
filterable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in filteredLocationList"
|
||||
:key="item.id"
|
||||
:label="formatLocationLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />查询</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<ContentWrap>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
:stripe="true"
|
||||
:show-overflow-tooltip="true"
|
||||
row-key="id"
|
||||
>
|
||||
<el-table-column label="样品码" align="center" prop="barCode" min-width="150" />
|
||||
<el-table-column label="样品名称" align="left" min-width="180">
|
||||
<template #default="{ row }">{{ row.name || row.productName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column label="样品分类" align="center" prop="categoryName" min-width="130" /> -->
|
||||
<el-table-column label="仓库" align="center" prop="warehouseName" min-width="140" />
|
||||
<el-table-column label="库区" align="center" prop="areaName" min-width="120" />
|
||||
<el-table-column label="库位编码" align="center" prop="locationCode" min-width="130" />
|
||||
<el-table-column label="库位名称" align="center" prop="locationName" min-width="140" />
|
||||
<el-table-column label="库存" align="center" prop="count" min-width="120">
|
||||
<template #default="{ row }">{{ formatCount(row.count) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" align="center" prop="unitName" width="90" />
|
||||
<!-- <el-table-column label="库存展示" align="center" prop="stockDisplay" min-width="200">
|
||||
<template #default="{ row }">{{ row.stockDisplay || '-' }}</template>
|
||||
</el-table-column> -->
|
||||
</el-table>
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BitWmsApi } from '@/api/erp/bitwms'
|
||||
import { WarehouseApi } from '@/api/erp/stock/warehouse'
|
||||
import { WarehouseAreaApi } from '@/api/erp/stock/warehousearea'
|
||||
import { WarehouseLocationApi } from '@/api/erp/stock/warehouselocation'
|
||||
|
||||
defineOptions({ name: 'ErpBitWmsStock' })
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const queryFormRef = ref()
|
||||
const sampleList = ref<any[]>([])
|
||||
const warehouseList = ref<any[]>([])
|
||||
const areaList = ref<any[]>([])
|
||||
const locationList = ref<any[]>([])
|
||||
const queryParams = reactive<any>({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
productId: undefined,
|
||||
warehouseId: undefined,
|
||||
areaId: undefined,
|
||||
locationId: undefined
|
||||
})
|
||||
const filteredAreaList = computed(() => {
|
||||
if (!queryParams.warehouseId) return areaList.value
|
||||
return areaList.value.filter((item) => item.warehouseId === queryParams.warehouseId)
|
||||
})
|
||||
const filteredLocationList = computed(() =>
|
||||
locationList.value.filter((item) => {
|
||||
if (queryParams.warehouseId && item.warehouseId !== queryParams.warehouseId) return false
|
||||
if (queryParams.areaId && item.areaId !== queryParams.areaId) return false
|
||||
return true
|
||||
})
|
||||
)
|
||||
|
||||
const formatSampleLabel = (item: any) => `${item.barCode || item.code || '-'} / ${item.name || '-'}`
|
||||
const formatLocationLabel = (item: any) => `${item.code || '-'} / ${item.name || '-'}`
|
||||
const formatCount = (value: number | string | undefined) => {
|
||||
if (value === undefined || value === null || value === '') return '-'
|
||||
const num = Number(value)
|
||||
return Number.isFinite(num) ? num.toLocaleString() : String(value)
|
||||
}
|
||||
const buildQueryParams = () =>
|
||||
Object.fromEntries(
|
||||
Object.entries(queryParams).filter(
|
||||
([, value]) => value !== undefined && value !== null && value !== ''
|
||||
)
|
||||
)
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await BitWmsApi.getLocationStockPage(buildQueryParams())
|
||||
list.value = data.list || []
|
||||
total.value = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const handleWarehouseChange = () => {
|
||||
queryParams.areaId = undefined
|
||||
queryParams.locationId = undefined
|
||||
}
|
||||
const handleAreaChange = () => {
|
||||
queryParams.locationId = undefined
|
||||
}
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const [samples, warehouses, areas, locations] = await Promise.all([
|
||||
BitWmsApi.getSampleSimpleList(),
|
||||
WarehouseApi.getWarehouseSimpleList(),
|
||||
WarehouseAreaApi.getWarehouseAreaPage({ pageNo: 1, pageSize: 100 }),
|
||||
WarehouseLocationApi.getWarehouseLocationPage({ pageNo: 1, pageSize: 100 })
|
||||
])
|
||||
sampleList.value = samples || []
|
||||
warehouseList.value = warehouses || []
|
||||
areaList.value = areas.list || []
|
||||
locationList.value = locations.list || []
|
||||
await getList()
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,939 @@
|
||||
<template>
|
||||
<div ref="dashboardRef" class="run-dashboard">
|
||||
<div class="bg-grid"></div>
|
||||
|
||||
<header class="dashboard-header">
|
||||
<div class="brand">
|
||||
<div class="brand-icon">
|
||||
<Icon icon="ep:cpu" />
|
||||
</div>
|
||||
<div class="brand-text">{{ t('Header.subTitle') }}</div>
|
||||
</div>
|
||||
<h1>{{ t('Header.title') }}</h1>
|
||||
<div class="header-meta">
|
||||
<div><span>{{ t('Header.currentTime') }}</span>{{ currentTime }}</div>
|
||||
<div><span>{{ t('Header.lastUpdate') }}</span>{{ lastUpdateTime || '-' }}</div>
|
||||
<div class="refresh-row">
|
||||
<span>{{ t('Header.refreshRate') }}</span>{{ t('Header.refreshRateValue', { seconds: 15 }) }}
|
||||
<button type="button" class="screen-btn" @click="toggleFullscreen">
|
||||
<Icon :icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="dashboard-main">
|
||||
<section class="top-row">
|
||||
<div
|
||||
v-for="card in statusCards"
|
||||
:key="card.key"
|
||||
class="metric-card"
|
||||
:class="`metric-card--${card.tone}`"
|
||||
>
|
||||
<div class="metric-title">
|
||||
<Icon :icon="card.icon" />
|
||||
<span>{{ card.label }}</span>
|
||||
</div>
|
||||
<div class="metric-value">
|
||||
<strong>{{ card.value }}</strong>
|
||||
<span v-if="card.percent !== undefined">{{ formatPercent(card.percent) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alarm-board panel">
|
||||
<div class="alarm-head">
|
||||
<span>{{ t('AlarmBoard.currentDevice') }}</span>
|
||||
<span>{{ t('AlarmBoard.todayAlarmDuration') }}</span>
|
||||
<span>{{ t('AlarmBoard.lastReportTime') }}</span>
|
||||
</div>
|
||||
<div v-if="alarmDevices.length" class="alarm-list">
|
||||
<div v-for="item in alarmDevices" :key="item.id" class="alarm-item">
|
||||
<strong :title="item.name">{{ item.name }}</strong>
|
||||
<span>{{ t('AlarmBoard.alarmPrefix') }} {{ formatMinutes(item.alarmMinutes) }}</span>
|
||||
<span>{{ item.lastReportTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-line">{{ t('AlarmBoard.empty') }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel detail-panel">
|
||||
<div class="panel-title">
|
||||
<h2>{{ t('Detail.title') }}</h2>
|
||||
<span>{{ t('Detail.carouselText', { page: detailPageText }) }}</span>
|
||||
</div>
|
||||
<div class="detail-table">
|
||||
<div class="table-header table-row">
|
||||
<span>{{ t('Detail.columns.deviceName') }}</span>
|
||||
<span>{{ t('Detail.columns.currentStatus') }}</span>
|
||||
<span>{{ t('Detail.columns.runningTime') }}</span>
|
||||
<span>{{ t('Detail.columns.standbyTime') }}</span>
|
||||
<span>{{ t('Detail.columns.alarmTime') }}</span>
|
||||
<span>{{ t('Detail.columns.efficiency') }}</span>
|
||||
</div>
|
||||
<div v-if="pagedRows.length" class="table-body">
|
||||
<div
|
||||
v-for="row in pagedRows"
|
||||
:key="row.id"
|
||||
class="table-row"
|
||||
:class="`table-row--${row.status}`"
|
||||
>
|
||||
<strong :title="row.name">{{ row.name }}</strong>
|
||||
<span>
|
||||
<em class="status-pill" :class="`status-pill--${row.status}`">{{ statusText[row.status] }}</em>
|
||||
</span>
|
||||
<span>{{ formatMinutes(row.runningMinutes) }}</span>
|
||||
<span>{{ formatMinutes(row.standbyMinutes) }}</span>
|
||||
<span>{{ formatMinutes(row.alarmMinutes) }}</span>
|
||||
<span class="rate">{{ formatPercent(row.utilizationRate) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-table">{{ t('Detail.empty') }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="chart-row">
|
||||
<div class="panel chart-panel">
|
||||
<div class="panel-title">
|
||||
<h2>{{ t('Charts.last24Hours') }}</h2>
|
||||
<span>{{ t('Charts.averageRate', { value: formatPercent(todayAverageRate) }) }}</span>
|
||||
</div>
|
||||
<div ref="hourChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="panel chart-panel">
|
||||
<div class="panel-title">
|
||||
<h2>{{ t('Charts.last7Days') }}</h2>
|
||||
<span>{{ t('Charts.averageRate', { value: formatPercent(sevenDayAverageRate) }) }}</span>
|
||||
</div>
|
||||
<div ref="weekChartRef" class="chart"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import * as echarts from 'echarts'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import { DeviceOperationOverviewApi } from '@/api/iot/deviceOperationOverview'
|
||||
import type { DeviceOperationOverviewRespVO, DeviceOperationOverviewTimelineRowVO } from '@/api/iot/deviceOperationOverview'
|
||||
import { OrganizationApi } from '@/api/mes/organization'
|
||||
|
||||
defineOptions({ name: 'IotReportDashboard3' })
|
||||
|
||||
const { t } = useI18n('Dashboard3')
|
||||
|
||||
type RunStatus = 'running' | 'standby' | 'fault' | 'offline'
|
||||
|
||||
interface OrganizationFilterItem {
|
||||
id: string
|
||||
parentId?: string | null
|
||||
dvId?: number | string | null
|
||||
machineName?: string
|
||||
}
|
||||
|
||||
interface DetailRow {
|
||||
id: string
|
||||
name: string
|
||||
status: RunStatus
|
||||
runningMinutes: number
|
||||
standbyMinutes: number
|
||||
alarmMinutes: number
|
||||
utilizationRate: number
|
||||
lastReportTime: string
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const dashboardRef = ref<HTMLElement>()
|
||||
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen(dashboardRef)
|
||||
const currentTime = ref('')
|
||||
const lastUpdateTime = ref('')
|
||||
const overviewData = ref<DeviceOperationOverviewRespVO>({
|
||||
metrics: [],
|
||||
hourlyStatus: [],
|
||||
summary: [],
|
||||
summaryTotalHours: 0,
|
||||
timelineRows: [],
|
||||
totalDevices: 0
|
||||
})
|
||||
const weekTrend = ref<{ date: string; rate: number }[]>([])
|
||||
const detailPage = ref(1)
|
||||
const detailPageSize = 8
|
||||
const organizationList = ref<OrganizationFilterItem[]>([])
|
||||
const hourChartRef = ref<HTMLElement | null>(null)
|
||||
const weekChartRef = ref<HTMLElement | null>(null)
|
||||
let hourChart: echarts.ECharts | null = null
|
||||
let weekChart: echarts.ECharts | null = null
|
||||
let clockTimer: number | undefined
|
||||
let refreshTimer: number | undefined
|
||||
let carouselTimer: number | undefined
|
||||
|
||||
const statusText = computed<Record<RunStatus, string>>(() => ({
|
||||
running: t('Status.running'),
|
||||
standby: t('Status.standby'),
|
||||
fault: t('Status.fault'),
|
||||
offline: t('Status.offline')
|
||||
}))
|
||||
|
||||
const metricValueMap = computed(() => {
|
||||
return overviewData.value.metrics.reduce<Record<string, number>>((acc, item) => {
|
||||
acc[item.key] = Number(item.value) || 0
|
||||
return acc
|
||||
}, {})
|
||||
})
|
||||
|
||||
const getMetricValue = (keys: string[]) => {
|
||||
const foundKey = keys.find((key) => metricValueMap.value[key] !== undefined)
|
||||
return foundKey ? metricValueMap.value[foundKey] : undefined
|
||||
}
|
||||
|
||||
const summaryMap = computed(() => {
|
||||
return overviewData.value.summary.reduce<Record<string, number>>((acc, item) => {
|
||||
acc[item.status] = Number(item.percent) || 0
|
||||
return acc
|
||||
}, {})
|
||||
})
|
||||
|
||||
const statusCards = computed(() => [
|
||||
{
|
||||
key: 'total',
|
||||
label: t('Metrics.total'),
|
||||
icon: 'ep:connection',
|
||||
value:
|
||||
overviewData.value.totalDevices ||
|
||||
getMetricValue(['total', 'deviceTotal', 'totalDevices']) ||
|
||||
overviewData.value.timelineRows.length,
|
||||
tone: 'total'
|
||||
},
|
||||
{
|
||||
key: 'online',
|
||||
label: t('Metrics.online'),
|
||||
icon: 'ep:cpu',
|
||||
value: Math.max(0, totalDeviceCount.value - offlineDeviceCount.value),
|
||||
percent: 100 - (summaryMap.value.offline || 0),
|
||||
tone: 'online'
|
||||
},
|
||||
{
|
||||
key: 'offline',
|
||||
label: t('Metrics.offline'),
|
||||
icon: 'ep:link',
|
||||
value: offlineDeviceCount.value,
|
||||
percent: summaryMap.value.offline || 0,
|
||||
tone: 'offline'
|
||||
},
|
||||
{
|
||||
key: 'running',
|
||||
label: t('Metrics.running'),
|
||||
icon: 'ep:video-play',
|
||||
value: getMetricValue(['running', 'run', 'runningDevices']) ?? getRowsByStatus('running').length,
|
||||
percent: summaryMap.value.running || 0,
|
||||
tone: 'running'
|
||||
},
|
||||
{
|
||||
key: 'standby',
|
||||
label: t('Metrics.standby'),
|
||||
icon: 'ep:video-pause',
|
||||
value: getMetricValue(['standby', 'idle', 'standbyDevices']) ?? getRowsByStatus('standby').length,
|
||||
percent: summaryMap.value.standby || 0,
|
||||
tone: 'standby'
|
||||
},
|
||||
{
|
||||
key: 'fault',
|
||||
label: t('Metrics.fault'),
|
||||
icon: 'ep:bell',
|
||||
value: getMetricValue(['fault', 'alarm', 'faultDevices']) ?? getRowsByStatus('fault').length,
|
||||
percent: summaryMap.value.fault || 0,
|
||||
tone: 'fault'
|
||||
}
|
||||
])
|
||||
|
||||
const detailRows = computed<DetailRow[]>(() => {
|
||||
return overviewData.value.timelineRows.map((row) => {
|
||||
const minutes = calcMinutes(row)
|
||||
const status = getCurrentStatus(row)
|
||||
return {
|
||||
id: String(row.id),
|
||||
name: row.name || '-',
|
||||
status,
|
||||
runningMinutes: minutes.running,
|
||||
standbyMinutes: minutes.standby,
|
||||
alarmMinutes: minutes.fault,
|
||||
utilizationRate: Number(row.utilizationRate) || 0,
|
||||
lastReportTime: status === 'fault' || minutes.fault > 0 ? lastUpdateTime.value || '-' : '-'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const totalDetailPages = computed(() => Math.max(1, Math.ceil(detailRows.value.length / detailPageSize)))
|
||||
const totalDeviceCount = computed(
|
||||
() =>
|
||||
overviewData.value.totalDevices ||
|
||||
getMetricValue(['total', 'deviceTotal', 'totalDevices']) ||
|
||||
detailRows.value.length
|
||||
)
|
||||
const offlineDeviceCount = computed(
|
||||
() => getMetricValue(['offline', 'offlineDevices']) ?? getRowsByStatus('offline').length
|
||||
)
|
||||
const pagedRows = computed(() => {
|
||||
const start = (detailPage.value - 1) * detailPageSize
|
||||
return detailRows.value.slice(start, start + detailPageSize)
|
||||
})
|
||||
const detailPageText = computed(() => `${detailPage.value} / ${totalDetailPages.value}`)
|
||||
const alarmDevices = computed(() => {
|
||||
return detailRows.value
|
||||
.filter((item) => item.status === 'fault' || item.alarmMinutes > 0)
|
||||
.sort((a, b) => b.alarmMinutes - a.alarmMinutes)
|
||||
.slice(0, 2)
|
||||
})
|
||||
const hourlyRates = computed(() =>
|
||||
overviewData.value.hourlyStatus.map((item) => {
|
||||
const total = item.running + item.standby + item.fault + item.offline
|
||||
return {
|
||||
hour: item.hour,
|
||||
rate: total > 0 ? Number(((item.running / total) * 100).toFixed(1)) : 0
|
||||
}
|
||||
})
|
||||
)
|
||||
const todayAverageRate = computed(() => average(hourlyRates.value.map((item) => item.rate)))
|
||||
const sevenDayAverageRate = computed(() => average(weekTrend.value.map((item) => item.rate)))
|
||||
|
||||
const formatPercent = (value?: number) => `${Number(value || 0).toFixed(1)}%`
|
||||
|
||||
const formatMinutes = (minutes: number) => {
|
||||
const safeMinutes = Math.max(0, Math.round(minutes || 0))
|
||||
const hours = Math.floor(safeMinutes / 60)
|
||||
const rest = safeMinutes % 60
|
||||
if (!hours) return t('Time.minute', { minute: rest })
|
||||
if (!rest) return t('Time.hour', { hour: hours })
|
||||
return t('Time.hourMinute', { hour: hours, minute: rest })
|
||||
}
|
||||
|
||||
const average = (values: number[]) => {
|
||||
const valid = values.filter((item) => Number.isFinite(item))
|
||||
if (!valid.length) return 0
|
||||
return Number((valid.reduce((sum, item) => sum + item, 0) / valid.length).toFixed(1))
|
||||
}
|
||||
|
||||
const getRowsByStatus = (status: RunStatus) => detailRows.value.filter((row) => row.status === status)
|
||||
|
||||
const getCurrentStatus = (row: DeviceOperationOverviewTimelineRowVO): RunStatus => {
|
||||
const currentHour = dayjs().hour() + dayjs().minute() / 60
|
||||
const currentSegment = row.segments?.find((item) => currentHour >= item.startHour && currentHour < item.endHour)
|
||||
return currentSegment?.status || row.segments?.[row.segments.length - 1]?.status || 'offline'
|
||||
}
|
||||
|
||||
const calcMinutes = (row: DeviceOperationOverviewTimelineRowVO) => {
|
||||
return (row.segments || []).reduce<Record<RunStatus, number>>(
|
||||
(acc, segment) => {
|
||||
const minutes = Math.max(0, (Number(segment.endHour) - Number(segment.startHour)) * 60)
|
||||
acc[segment.status] += minutes
|
||||
return acc
|
||||
},
|
||||
{ running: 0, standby: 0, fault: 0, offline: 0 }
|
||||
)
|
||||
}
|
||||
|
||||
const updateClock = () => {
|
||||
currentTime.value = dayjs().format('YYYY/M/D HH:mm:ss')
|
||||
}
|
||||
|
||||
const childrenByParentId = computed(() => {
|
||||
return organizationList.value.reduce<Record<string, OrganizationFilterItem[]>>((acc, item) => {
|
||||
const parentKey = String(item.parentId ?? 0)
|
||||
if (!acc[parentKey]) acc[parentKey] = []
|
||||
acc[parentKey].push(item)
|
||||
return acc
|
||||
}, {})
|
||||
})
|
||||
|
||||
const collectDeviceIdsByOrg = (orgId?: string) => {
|
||||
const deviceIds = new Set<string>()
|
||||
const pushDevice = (node?: OrganizationFilterItem) => {
|
||||
if (node?.dvId) deviceIds.add(String(node.dvId))
|
||||
}
|
||||
if (!orgId) {
|
||||
organizationList.value.forEach(pushDevice)
|
||||
return Array.from(deviceIds)
|
||||
}
|
||||
const queue = [orgId]
|
||||
while (queue.length) {
|
||||
const currentId = queue.shift() as string
|
||||
const current = organizationList.value.find((item) => item.id === currentId)
|
||||
pushDevice(current)
|
||||
;(childrenByParentId.value[currentId] || []).forEach((child) => queue.push(child.id))
|
||||
}
|
||||
return Array.from(deviceIds)
|
||||
}
|
||||
|
||||
const getDeviceIds = () => {
|
||||
const routeIds = String(route.query.ids || route.query.deviceIds || '')
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
if (routeIds.length) return routeIds
|
||||
return collectDeviceIdsByOrg(route.query.orgId ? String(route.query.orgId) : undefined)
|
||||
}
|
||||
|
||||
const buildParams = (startTime: string, endTime: string, pageSize = 1000) => ({
|
||||
ids: getDeviceIds().join(','),
|
||||
startTime,
|
||||
endTime,
|
||||
timelinePageNo: 1,
|
||||
timelinePageSize: pageSize
|
||||
})
|
||||
|
||||
const loadOrganizationList = async () => {
|
||||
const data = await OrganizationApi.getOrganizationList()
|
||||
organizationList.value = Array.isArray(data)
|
||||
? data.map((item: any) => ({
|
||||
id: String(item.id),
|
||||
parentId: item.parentId != null ? String(item.parentId) : item.parentId,
|
||||
dvId: item.dvId,
|
||||
machineName: item.machineName
|
||||
}))
|
||||
: []
|
||||
}
|
||||
|
||||
const refreshTodayData = async () => {
|
||||
const start = dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss')
|
||||
const end = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss')
|
||||
const response = await DeviceOperationOverviewApi.getRunOverview(buildParams(start, end))
|
||||
overviewData.value = {
|
||||
metrics: response?.metrics || [],
|
||||
hourlyStatus: response?.hourlyStatus || [],
|
||||
summary: response?.summary || [],
|
||||
summaryTotalHours: response?.summaryTotalHours || 0,
|
||||
timelineRows: response?.timelineRows || [],
|
||||
totalDevices: response?.totalDevices || 0
|
||||
}
|
||||
lastUpdateTime.value = dayjs().format('YYYY/M/D HH:mm:ss')
|
||||
}
|
||||
|
||||
const refreshWeekTrend = async () => {
|
||||
const requests = Array.from({ length: 7 }).map((_, index) => {
|
||||
const date = dayjs().subtract(6 - index, 'day')
|
||||
return DeviceOperationOverviewApi.getRunOverview(
|
||||
buildParams(date.startOf('day').format('YYYY-MM-DD HH:mm:ss'), date.endOf('day').format('YYYY-MM-DD HH:mm:ss'), 1)
|
||||
).then((res) => {
|
||||
const running = res?.summary?.find((item) => item.status === 'running')?.percent || 0
|
||||
return {
|
||||
date: date.format('MM-DD'),
|
||||
rate: Number(Number(running).toFixed(1))
|
||||
}
|
||||
})
|
||||
})
|
||||
weekTrend.value = await Promise.all(requests)
|
||||
}
|
||||
|
||||
const refreshData = async () => {
|
||||
await refreshTodayData()
|
||||
await refreshWeekTrend()
|
||||
renderCharts()
|
||||
}
|
||||
|
||||
const getLineChartOption = (labels: string[], values: number[]): EChartsOption => ({
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
valueFormatter: (value) => `${value}%`
|
||||
},
|
||||
grid: { left: 42, right: 24, top: 26, bottom: 32 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: labels,
|
||||
axisLine: { lineStyle: { color: 'rgba(105, 165, 235, 0.55)' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#a9c8ef', fontSize: 12 }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 100,
|
||||
axisLabel: { color: '#a9c8ef', formatter: '{value}%' },
|
||||
splitLine: { lineStyle: { color: 'rgba(76, 144, 219, 0.18)' } }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 5,
|
||||
data: values,
|
||||
lineStyle: { width: 3, color: '#4692ff' },
|
||||
itemStyle: { color: '#54a7ff' },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(70, 146, 255, 0.46)' },
|
||||
{ offset: 1, color: 'rgba(70, 146, 255, 0.04)' }
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const renderCharts = () => {
|
||||
if (hourChartRef.value && !hourChart) hourChart = echarts.init(hourChartRef.value, 'dark', { renderer: 'canvas' })
|
||||
if (weekChartRef.value && !weekChart) weekChart = echarts.init(weekChartRef.value, 'dark', { renderer: 'canvas' })
|
||||
hourChart?.setOption(
|
||||
getLineChartOption(
|
||||
hourlyRates.value.map((item) => item.hour),
|
||||
hourlyRates.value.map((item) => item.rate)
|
||||
)
|
||||
)
|
||||
weekChart?.setOption(
|
||||
getLineChartOption(
|
||||
weekTrend.value.map((item) => item.date),
|
||||
weekTrend.value.map((item) => item.rate)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const resizeCharts = () => {
|
||||
hourChart?.resize()
|
||||
weekChart?.resize()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
updateClock()
|
||||
clockTimer = window.setInterval(updateClock, 1000)
|
||||
await loadOrganizationList()
|
||||
await refreshData()
|
||||
refreshTimer = window.setInterval(() => {
|
||||
void refreshData()
|
||||
}, 15000)
|
||||
carouselTimer = window.setInterval(() => {
|
||||
detailPage.value = detailPage.value >= totalDetailPages.value ? 1 : detailPage.value + 1
|
||||
}, 5000)
|
||||
window.addEventListener('resize', resizeCharts)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (clockTimer) window.clearInterval(clockTimer)
|
||||
if (refreshTimer) window.clearInterval(refreshTimer)
|
||||
if (carouselTimer) window.clearInterval(carouselTimer)
|
||||
window.removeEventListener('resize', resizeCharts)
|
||||
hourChart?.dispose()
|
||||
weekChart?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.run-dashboard {
|
||||
--bg: #03142d;
|
||||
--panel: rgb(10 44 88 / 82%);
|
||||
--panel-deep: rgb(6 27 61 / 88%);
|
||||
--line: rgb(47 129 222 / 62%);
|
||||
--text: #eef6ff;
|
||||
--muted: #91b7e5;
|
||||
--blue: #49a0ff;
|
||||
--green: #40f06d;
|
||||
--red: #ff4d5b;
|
||||
--gray: #aec0d7;
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 1366px;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
font-family: "Microsoft Yahei", "PingFang SC", Arial, sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(circle at 50% 0, rgb(21 88 157 / 38%), transparent 34%),
|
||||
linear-gradient(180deg, #061a38 0%, #041329 48%, #031026 100%);
|
||||
}
|
||||
|
||||
.bg-grid {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(rgb(42 122 204 / 16%) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgb(42 122 204 / 16%) 1px, transparent 1px);
|
||||
background-size: 96px 40px;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
height: 86px;
|
||||
padding: 12px 24px 8px;
|
||||
grid-template-columns: 360px 1fr 360px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #c7dcf6;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
display: grid;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
color: #8cd3ff;
|
||||
background: rgb(78 157 255 / 16%);
|
||||
border: 1px solid rgb(93 177 255 / 40%);
|
||||
border-radius: 6px;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-size: 42px;
|
||||
font-weight: 800;
|
||||
line-height: 1.15;
|
||||
text-align: center;
|
||||
text-shadow: 0 0 12px rgb(105 178 255 / 86%);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.header-meta {
|
||||
justify-self: end;
|
||||
color: #b6d0ef;
|
||||
font-size: 14px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.header-meta span {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.refresh-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.screen-btn {
|
||||
display: inline-grid;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
color: #8fc7ff;
|
||||
cursor: pointer;
|
||||
background: rgb(16 62 116 / 80%);
|
||||
border: 1px solid rgb(77 151 232 / 70%);
|
||||
border-radius: 4px;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.dashboard-main {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
height: calc(100vh - 86px);
|
||||
min-height: 820px;
|
||||
padding: 8px 24px 16px;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(150px, 1fr)) 2fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.panel,
|
||||
.metric-card {
|
||||
background: linear-gradient(135deg, var(--panel), var(--panel-deep));
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 5px;
|
||||
box-shadow: inset 0 0 24px rgb(34 113 203 / 18%);
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
height: 120px;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.metric-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #d7e9ff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.metric-title .iconify {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
display: flex;
|
||||
margin-top: 16px;
|
||||
align-items: baseline;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.metric-value strong {
|
||||
font-size: 40px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.metric-value span {
|
||||
color: #a8bfdd;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.metric-card--online .metric-value strong,
|
||||
.metric-card--running .metric-value strong,
|
||||
.metric-card--online .metric-title,
|
||||
.metric-card--running .metric-title {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.metric-card--offline .metric-value strong,
|
||||
.metric-card--offline .metric-title {
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.metric-card--standby .metric-value strong,
|
||||
.metric-card--standby .metric-title {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.metric-card--fault {
|
||||
border-color: rgb(255 67 84 / 86%);
|
||||
}
|
||||
|
||||
.metric-card--fault .metric-value strong,
|
||||
.metric-card--fault .metric-title {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.alarm-board {
|
||||
height: 120px;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.alarm-head,
|
||||
.alarm-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1.1fr 1fr 1fr;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.alarm-head {
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.alarm-list {
|
||||
display: flex;
|
||||
margin-top: 8px;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.alarm-item {
|
||||
height: 30px;
|
||||
padding: 0 8px;
|
||||
overflow: hidden;
|
||||
color: #e8f2ff;
|
||||
background: linear-gradient(90deg, rgb(102 25 56 / 76%), rgb(33 31 69 / 52%));
|
||||
border: 1px solid rgb(255 64 82 / 52%);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.alarm-item strong {
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.alarm-item span:nth-child(2) {
|
||||
color: #ff5365;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.alarm-item span:nth-child(3) {
|
||||
color: #d7aab4;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.empty-line,
|
||||
.empty-table {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
color: var(--muted);
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
min-height: 0;
|
||||
padding: 14px 16px;
|
||||
flex: 1.25;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: flex;
|
||||
height: 30px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.panel-title h2 {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.panel-title span {
|
||||
color: #89bdf5;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.detail-table {
|
||||
display: flex;
|
||||
height: calc(100% - 34px);
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 1.4fr 1.4fr 1.4fr 1.4fr;
|
||||
min-height: 43px;
|
||||
padding: 0 14px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid rgb(60 134 212 / 35%);
|
||||
}
|
||||
|
||||
.table-header {
|
||||
min-height: 44px;
|
||||
color: #98bee8;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.table-body .table-row {
|
||||
color: #dcecff;
|
||||
background: rgb(12 52 98 / 42%);
|
||||
border: 1px solid rgb(60 134 212 / 48%);
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.table-row strong {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table-row--fault {
|
||||
color: #ff5768;
|
||||
background: linear-gradient(90deg, rgb(108 27 58 / 82%), rgb(11 39 83 / 46%)) !important;
|
||||
border-color: rgb(255 64 82 / 80%) !important;
|
||||
}
|
||||
|
||||
.table-row--running strong,
|
||||
.table-row--running .rate {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.table-row--offline strong {
|
||||
color: #96adca;
|
||||
}
|
||||
|
||||
.rate {
|
||||
color: #54a7ff;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
min-width: 42px;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
font-style: normal;
|
||||
border: 1px solid currentcolor;
|
||||
border-radius: 13px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.status-pill--running {
|
||||
color: var(--green);
|
||||
background: rgb(34 197 94 / 12%);
|
||||
}
|
||||
|
||||
.status-pill--standby {
|
||||
color: var(--blue);
|
||||
background: rgb(59 130 246 / 12%);
|
||||
}
|
||||
|
||||
.status-pill--fault {
|
||||
color: var(--red);
|
||||
background: rgb(239 68 68 / 14%);
|
||||
}
|
||||
|
||||
.status-pill--offline {
|
||||
color: #b5c4d9;
|
||||
background: rgb(148 163 184 / 14%);
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
display: grid;
|
||||
min-height: 280px;
|
||||
flex: 0.9;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.chart-panel {
|
||||
min-height: 0;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: calc(100% - 34px);
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
@media (width <= 1600px) {
|
||||
.dashboard-header h1 {
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
grid-template-columns: 310px 1fr 320px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
padding: 14px 12px;
|
||||
}
|
||||
|
||||
.metric-value strong {
|
||||
font-size: 34px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue