feat:总览-能源模块

master
黄伟杰 14 hours ago
parent c4ff5f807c
commit ed30527d2d

@ -0,0 +1,33 @@
import request from '@/utils/request'
export function getEnergyTypeList(params = {}) {
return request({
url: '/admin-api/mes/energy-type/list',
method: 'get',
params
})
}
export function getOrganizationList(params = {}) {
return request({
url: '/admin-api/mes/organization/list',
method: 'get',
params
})
}
export function getEnergyDeviceList(params = {}) {
return request({
url: '/admin-api/mes/energy-device/getList',
method: 'get',
params
})
}
export function queryEnergyOverviewData(params = {}) {
return request({
url: '/admin-api/mes/energy-device/queryOverviewData',
method: 'get',
params
})
}

@ -367,6 +367,14 @@
"enablePullDownRefresh": true
}
},
{
"path": "overview/energy/index",
"style": {
"navigationBarTitleText": "\u80fd\u6e90\u603b\u89c8",
"navigationStyle": "custom",
"enablePullDownRefresh": true
}
},
{
"path": "spare/index",
"style": {

@ -0,0 +1,939 @@
<template>
<view class="page-container">
<NavBar title="能源总览" />
<view class="page-body">
<view class="filter-bar">
<view class="filter-row quick-row">
<picker :range="orgLabels" :value="orgIndex" @change="onOrgChange">
<view class="picker-field">
<text class="picker-text">{{ selectedOrgLabel }}</text>
<uni-icons type="bottom" size="14" color="#a8adb7"></uni-icons>
</view>
</picker>
<picker :range="energyTypeLabels" :value="energyTypeIndex" @change="onEnergyTypeChange">
<view class="picker-field">
<text class="picker-text">{{ selectedEnergyTypeName }}</text>
<uni-icons type="bottom" size="14" color="#a8adb7"></uni-icons>
</view>
</picker>
<view class="icon-filter-btn" @click="refreshData">
<uni-icons type="refresh" size="24" color="#7b8491"></uni-icons>
</view>
<view class="icon-filter-btn" @click="openFilterDrawer">
<uni-icons type="settings" size="24" color="#7b8491"></uni-icons>
</view>
</view>
</view>
<view class="content-wrap">
<view v-if="!hasEnergyTypes && !loading" class="empty-card"></view>
<template v-else>
<view class="metric-grid">
<view v-for="item in metricCards" :key="item.key" class="metric-card">
<view class="metric-icon" :class="item.theme">
<uni-icons :type="item.icon" size="24" :color="item.color"></uni-icons>
</view>
<view class="metric-content">
<text class="metric-label">{{ item.label }}</text>
<view class="metric-value-row">
<text class="metric-value">{{ item.value }}</text>
<text v-if="item.unit" class="metric-unit">{{ item.unit }}</text>
</view>
<view class="metric-sub">
<text class="sub-label">{{ item.subLabel }}</text>
<text v-if="item.change" :class="['metric-change', item.down ? 'down' : 'up']">
{{ item.down ? '↓' : '↑' }} {{ item.change }}
</text>
<text v-else class="sub-value">{{ item.subValue }}</text>
</view>
</view>
</view>
</view>
<view class="panel-card">
<view class="panel-title">能源用量趋势</view>
<view class="chart-box">
<qiun-data-charts type="line" :chartData="trendChartData" :canvas2d="false" :opts="trendChartOpts" />
</view>
</view>
<view class="panel-card">
<view class="panel-title">区域能耗占比</view>
<view class="region-layout">
<view class="ring-box">
<qiun-data-charts type="ring" :chartData="regionChartData" :canvas2d="false" :opts="regionChartOpts" />
</view>
<view class="region-legend">
<view v-for="item in regionItems" :key="item.name" class="region-row">
<view class="region-left">
<view class="region-dot" :style="{ background: item.color }"></view>
<text class="region-name">{{ item.name }}</text>
</view>
<text class="region-value">{{ item.value }} {{ selectedEnergyUnit }} ({{ item.percent }}%)</text>
</view>
</view>
</view>
</view>
<view class="panel-card">
<view class="panel-title">能耗排行 TOP5</view>
<view v-if="!rankList.length" class="empty-text"></view>
<view v-for="(item, index) in rankList" :key="item.id || item.name" class="rank-row">
<view :class="['rank-index', `rank-${index + 1}`]">{{ index + 1 }}</view>
<view class="rank-main">
<text class="rank-name">{{ textValue(item.name) }}</text>
<text class="rank-region">{{ textValue(item.region) }}</text>
</view>
<text class="rank-value">{{ textValue(item.value) }} {{ selectedEnergyUnit }}</text>
</view>
</view>
</template>
<view v-if="loading" class="loading-mask">{{ t('functionCommon.loading') }}</view>
</view>
</view>
<uni-popup ref="filterPopupRef" class="energy-filter-popup" type="right" background-color="transparent" :animation="false">
<view class="filter-drawer">
<view class="drawer-header">
<text class="drawer-title">更多筛选</text>
</view>
<scroll-view scroll-y class="drawer-body">
<view class="drawer-section drawer-fields">
<view class="drawer-field drawer-field-wide">
<text class="drawer-label">时间范围</text>
<view class="drawer-date">
<uni-datetime-picker
v-model="timeRange"
type="datetimerange"
:clear-icon="true"
start-placeholder="开始时间"
end-placeholder="结束时间"
/>
</view>
</view>
</view>
</scroll-view>
<view class="drawer-actions">
<view class="drawer-action reset" @click="resetFilters"></view>
<view class="drawer-action confirm" @click="confirmFilterDrawer"></view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import {
getEnergyDeviceList,
getEnergyTypeList,
getOrganizationList,
queryEnergyOverviewData
} from '@/api/mes/energyOverview'
const { t } = useI18n()
const loading = ref(false)
const organizationOptions = ref([])
const selectedOrgId = ref('')
const timeRange = ref(getTodayTimeRange())
const filterPopupRef = ref(null)
const energyTypeOptions = ref([])
const energyDeviceList = ref([])
const selectedEnergyTypeId = ref('')
const metricCards = ref([])
const rankList = ref([])
const regionItems = ref([])
const trendChartData = reactive({
categories: [],
series: [{ name: '用量', data: [] }]
})
const regionChartData = reactive({
series: [{ data: [] }]
})
const trendChartOpts = {
color: ['#2f7df6'],
dataLabel: false,
dataPointShape: true,
legend: { show: false },
xAxis: { disableGrid: true, labelCount: 5 },
yAxis: { gridType: 'dash', dashLength: 2 },
extra: { line: { type: 'curve', width: 2, activeType: 'hollow' } }
}
const regionChartOpts = {
color: ['#2f7df6', '#52c41a', '#faad14', '#13c2c2', '#fa8c16'],
dataLabel: false,
legend: { show: false },
title: { name: '总用量', fontSize: 12, color: '#64748b' },
subtitle: { name: '', fontSize: 13, color: '#0f172a' },
extra: {
ring: {
ringWidth: 22,
activeOpacity: 0.5,
activeRadius: 6,
offsetAngle: 0,
labelWidth: 15,
border: false
}
}
}
const metricConfig = {
total: { icon: 'fire-filled', theme: 'blue', color: '#2f7df6' },
deviceCount: { icon: 'gear-filled', theme: 'green', color: '#22c55e' },
topDevice: { icon: 'flag-filled', theme: 'purple', color: '#8b5cf6' },
topRegion: { icon: 'map-filled', theme: 'cyan', color: '#06b6d4' },
range: { icon: 'calendar-filled', theme: 'orange', color: '#f59e0b' }
}
const orgPickerOptions = computed(() => [
{ id: '', name: '\u5168\u90e8\u533a\u57df' },
...organizationOptions.value
])
const orgLabels = computed(() => orgPickerOptions.value.map((item) => item.name || '-'))
const orgIndex = computed(() => {
const idx = orgPickerOptions.value.findIndex((item) => String(item.id) === String(selectedOrgId.value))
return idx >= 0 ? idx : 0
})
const selectedOrgLabel = computed(() => {
const found = orgPickerOptions.value.find((item) => String(item.id) === String(selectedOrgId.value))
return found?.name || '\u5168\u90e8\u533a\u57df'
})
const hasEnergyTypes = computed(() => energyTypeOptions.value.length > 0)
const selectedEnergyType = computed(() =>
energyTypeOptions.value.find((item) => String(item.id) === String(selectedEnergyTypeId.value))
)
const selectedEnergyUnit = computed(() => selectedEnergyType.value?.unit || 'kWh')
const selectedEnergyTypeName = computed(() => selectedEnergyType.value?.name || '请选择能源类型')
const energyTypeLabels = computed(() => energyTypeOptions.value.map((item) => item.name || '-'))
const energyTypeIndex = computed(() => {
const idx = energyTypeOptions.value.findIndex((item) => String(item.id) === String(selectedEnergyTypeId.value))
return idx >= 0 ? idx : 0
})
onLoad(async () => {
await initPage()
})
onPullDownRefresh(async () => {
await refreshData()
uni.stopPullDownRefresh()
})
async function initPage() {
loading.value = true
try {
await loadOrganizations()
await loadEnergyTypes()
await refreshData(false)
} finally {
loading.value = false
}
}
async function refreshData(showLoading = true) {
if (showLoading) loading.value = true
try {
if (!hasEnergyTypes.value) {
resetOverview()
return
}
await loadEnergyDevices()
const range = normalizeTimeRange(timeRange.value)
const res = await queryEnergyOverviewData({
orgId: selectedOrgId.value || undefined,
energyTypeId: selectedEnergyTypeId.value,
startTime: range[0],
endTime: range[1],
pageNo: 1,
pageSize: 10
})
const data = normalizeOverviewData(res)
setMetricCards(data.metrics)
updateTrendChart(data.trendChart)
updateRegionChart(data.regionChart)
rankList.value = (data.rankList || []).slice(0, 5)
} catch (error) {
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
setFallbackOverview()
} finally {
loading.value = false
}
}
async function loadOrganizations() {
try {
const res = await getOrganizationList({})
organizationOptions.value = normalizeList(res)
} catch (error) {
organizationOptions.value = []
}
}
async function loadEnergyTypes() {
try {
const res = await getEnergyTypeList({ orgId: selectedOrgId.value || undefined })
const list = normalizeList(res)
energyTypeOptions.value = list
const currentExists = list.some((item) => String(item.id) === String(selectedEnergyTypeId.value))
if (!currentExists) {
selectedEnergyTypeId.value = list[0]?.id || ''
}
} catch (error) {
energyTypeOptions.value = []
selectedEnergyTypeId.value = ''
}
}
async function loadEnergyDevices() {
try {
const res = await getEnergyDeviceList({ orgId: selectedOrgId.value || undefined })
energyDeviceList.value = normalizeList(res)
} catch (error) {
energyDeviceList.value = []
}
}
function normalizeOverviewData(res) {
const root = res && res.data !== undefined ? res.data : res
return {
metrics: Array.isArray(root?.metrics) ? root.metrics : [],
trendChart: root?.trendChart || {},
regionChart: root?.regionChart || {},
rankList: Array.isArray(root?.rankList) ? root.rankList : []
}
}
function normalizeList(res) {
const root = res && res.data !== undefined ? res.data : res
if (Array.isArray(root)) return root
if (Array.isArray(root?.data)) return root.data
if (Array.isArray(root?.list)) return root.list
if (Array.isArray(root?.records)) return root.records
return []
}
function setMetricCards(metrics = []) {
if (metrics.length) {
metricCards.value = metrics
.filter((item) => ['total', 'deviceCount', 'topDevice', 'topRegion'].includes(item.key))
.map((item) => normalizeMetricCard(item))
return
}
metricCards.value = buildFallbackMetricCards()
}
function normalizeMetricCard(item) {
const config = metricConfig[item.key] || metricConfig.total
return {
key: item.key,
label: item.label || metricLabel(item.key),
value: textValue(item.value),
unit: item.unit || '',
icon: config.icon,
theme: config.theme,
color: config.color,
subLabel: item.subLabel || '',
subValue: item.subValue || '',
change: item.change || '',
down: Boolean(item.down)
}
}
function buildFallbackMetricCards() {
const unit = selectedEnergyUnit.value
const deviceCount = energyDeviceList.value.length
return [
normalizeMetricCard({ key: 'total', label: '总用量', value: '0', unit, subLabel: '今日累计', subValue: '0' }),
normalizeMetricCard({ key: 'deviceCount', label: '统计设备', value: String(deviceCount), unit: '台', subLabel: '当前能源类型', subValue: selectedEnergyTypeName.value }),
normalizeMetricCard({ key: 'topDevice', label: '最高能耗设备', value: '-', unit: '', subLabel: '设备', subValue: '-' }),
normalizeMetricCard({ key: 'topRegion', label: '最高能耗区域', value: '-', unit: '', subLabel: '区域', subValue: '-' })
]
}
function updateTrendChart(trend = {}) {
const categories = Array.isArray(trend.xAxis) && trend.xAxis.length ? trend.xAxis : buildTodayHours()
const data = Array.isArray(trend.data) && trend.data.length
? trend.data.map((item) => Number(item) || 0)
: categories.map(() => 0)
trendChartData.categories = categories
trendChartData.series = [{ name: '用量', data }]
}
function updateRegionChart(region = {}) {
const colors = regionChartOpts.color
const items = Array.isArray(region.items) ? region.items : []
regionItems.value = items.map((item, index) => ({
name: item.name || '-',
value: textValue(item.value),
percent: textValue(item.percent),
color: colors[index % colors.length]
}))
regionChartData.series = [{
data: regionItems.value.map((item) => ({
name: item.name,
value: Number(item.value) || 0
}))
}]
regionChartOpts.subtitle.name = `${textValue(region.totalValue || 0)} ${selectedEnergyUnit.value}`
}
function setFallbackOverview() {
metricCards.value = buildFallbackMetricCards()
updateTrendChart({})
updateRegionChart({})
rankList.value = []
}
function resetOverview() {
metricCards.value = []
rankList.value = []
regionItems.value = []
trendChartData.categories = []
trendChartData.series = [{ name: '用量', data: [] }]
regionChartData.series = [{ data: [] }]
}
async function onOrgChange(e) {
const item = orgPickerOptions.value[Number(e?.detail?.value || 0)]
if (!item) return
selectedOrgId.value = item.id || ''
selectedEnergyTypeId.value = ''
await loadEnergyTypes()
await refreshData()
}
async function onEnergyTypeChange(e) {
const item = energyTypeOptions.value[Number(e?.detail?.value || 0)]
if (!item) return
selectedEnergyTypeId.value = item.id
await refreshData()
}
function openFilterDrawer() {
filterPopupRef.value?.open()
}
function closeFilterDrawer() {
filterPopupRef.value?.close()
}
async function confirmFilterDrawer() {
closeFilterDrawer()
await refreshData()
}
async function resetFilters() {
selectedOrgId.value = ''
selectedEnergyTypeId.value = ''
timeRange.value = getTodayTimeRange()
await loadEnergyTypes()
closeFilterDrawer()
await refreshData()
}
function getTodayTimeRange() {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const date = `${year}-${month}-${day}`
return [`${date} 00:00:00`, `${date} 23:59:59`]
}
function normalizeTimeRange(value) {
return Array.isArray(value) && value.length === 2 && value[0] && value[1]
? value
: getTodayTimeRange()
}
function buildTodayHours() {
return Array.from({ length: 24 }).map((_, index) => `${String(index).padStart(2, '0')}:00`)
}
function metricLabel(key) {
const map = {
total: '总用量',
deviceCount: '统计设备',
topDevice: '最高能耗设备',
topRegion: '最高能耗区域'
}
return map[key] || '-'
}
function textValue(value) {
if (value === 0) return '0'
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: #f3f4f6;
}
.filter-bar {
padding: 18rpx 20rpx;
background: #f3f4f6;
}
.filter-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.quick-row>picker {
min-width: 0;
flex: 1;
}
.picker-field {
height: 66rpx;
padding: 0 18rpx 0 24rpx;
border: 1rpx solid #d9dde5;
background: #ffffff;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
}
.picker-text {
min-width: 0;
flex: 1;
font-size: 26rpx;
color: #374151;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.icon-filter-btn {
width: 66rpx;
height: 66rpx;
flex: 0 0 66rpx;
display: flex;
align-items: center;
justify-content: center;
border: 1rpx solid transparent;
background: transparent;
box-sizing: border-box;
}
.page-body {
padding-bottom: 32rpx;
}
.content-wrap {
padding: 0 20rpx;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18rpx;
}
.metric-card,
.panel-card,
.empty-card {
border-radius: 8rpx;
background: #ffffff;
box-shadow: 0 6rpx 20rpx rgba(15, 23, 42, 0.06);
box-sizing: border-box;
}
.metric-card {
min-width: 0;
min-height: 178rpx;
padding: 20rpx;
display: flex;
gap: 16rpx;
}
.metric-icon {
width: 62rpx;
height: 62rpx;
border-radius: 8rpx;
flex: 0 0 62rpx;
display: flex;
align-items: center;
justify-content: center;
}
.metric-icon.blue {
background: #eff6ff;
}
.metric-icon.green {
background: #ecfdf3;
}
.metric-icon.purple {
background: #f5f3ff;
}
.metric-icon.cyan {
background: #ecfeff;
}
.metric-icon.orange {
background: #fffbeb;
}
.metric-content {
min-width: 0;
flex: 1;
}
.metric-label {
display: block;
font-size: 24rpx;
line-height: 1.25;
color: #475569;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.metric-value-row {
margin-top: 10rpx;
display: flex;
align-items: baseline;
min-width: 0;
}
.metric-value {
min-width: 0;
font-size: 34rpx;
line-height: 1;
color: #0f172a;
font-weight: 800;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.metric-unit {
margin-left: 5rpx;
font-size: 22rpx;
color: #475569;
white-space: nowrap;
}
.metric-sub {
margin-top: 14rpx;
display: flex;
align-items: center;
gap: 8rpx;
min-width: 0;
}
.sub-label,
.sub-value,
.metric-change {
font-size: 22rpx;
line-height: 1.2;
color: #64748b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sub-value {
min-width: 0;
}
.metric-change.up {
color: #ef4444;
}
.metric-change.down {
color: #16a34a;
}
.panel-card {
margin-top: 18rpx;
padding: 24rpx;
}
.panel-title {
font-size: 30rpx;
line-height: 1.3;
color: #0f172a;
font-weight: 800;
}
.chart-box {
height: 360rpx;
margin-top: 18rpx;
}
.region-layout {
margin-top: 18rpx;
display: flex;
gap: 18rpx;
align-items: center;
}
.ring-box {
width: 260rpx;
height: 260rpx;
flex: 0 0 260rpx;
}
.region-legend {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 14rpx;
}
.region-row {
min-width: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10rpx;
}
.region-left {
min-width: 0;
display: flex;
align-items: center;
gap: 9rpx;
}
.region-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
flex: 0 0 16rpx;
}
.region-name {
font-size: 24rpx;
color: #64748b;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.region-value {
min-width: 0;
flex: 1;
text-align: right;
font-size: 23rpx;
color: #111827;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rank-row {
min-width: 0;
min-height: 88rpx;
display: flex;
align-items: center;
gap: 18rpx;
border-bottom: 1rpx solid #edf0f5;
}
.rank-row:last-child {
border-bottom: 0;
}
.rank-index {
width: 42rpx;
height: 42rpx;
border-radius: 50%;
background: #e5e7eb;
color: #475569;
font-size: 23rpx;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 42rpx;
}
.rank-1 {
background: #fef3c7;
color: #d97706;
}
.rank-2 {
background: #e0f2fe;
color: #0284c7;
}
.rank-3 {
background: #fee2e2;
color: #dc2626;
}
.rank-main {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.rank-name {
font-size: 26rpx;
color: #111827;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rank-region {
font-size: 22rpx;
color: #64748b;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rank-value {
max-width: 210rpx;
text-align: right;
font-size: 25rpx;
color: #0f172a;
font-weight: 800;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.empty-card,
.empty-text,
.loading-mask {
padding: 34rpx 0;
text-align: center;
color: #64748b;
font-size: 26rpx;
}
.filter-drawer {
width: 630rpx;
height: calc(100vh - var(--status-bar-height));
margin-top: var(--status-bar-height);
background: #f5f5f7;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 28rpx 0 0 28rpx;
}
:deep(.energy-filter-popup.right .uni-popup__content-transition) {
transform: none !important;
}
.drawer-header {
height: 104rpx;
padding: 18rpx 34rpx 0;
background: #ffffff;
display: flex;
align-items: center;
box-sizing: border-box;
}
.drawer-title {
color: #1f2937;
font-size: 34rpx;
line-height: 1.3;
font-weight: 700;
}
.drawer-body {
flex: 1;
min-height: 0;
padding-bottom: 40rpx;
box-sizing: border-box;
}
.drawer-section {
margin-bottom: 18rpx;
padding: 8rpx 28rpx;
border-radius: 24rpx;
background: #ffffff;
box-sizing: border-box;
}
.drawer-field {
min-width: 0;
min-height: 118rpx;
display: flex;
align-items: center;
gap: 20rpx;
border-bottom: 1rpx solid #eceff3;
box-sizing: border-box;
}
.drawer-field:last-child {
border-bottom: 0;
}
.drawer-label {
width: 150rpx;
flex: 0 0 150rpx;
font-size: 24rpx;
line-height: 1.3;
color: #4b5563;
font-weight: 500;
}
.drawer-date {
min-width: 0;
flex: 1;
width: 100%;
}
.drawer-actions {
padding: 18rpx 28rpx 34rpx;
display: flex;
gap: 18rpx;
background: #ffffff;
box-sizing: border-box;
}
.drawer-action {
flex: 1;
height: 78rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 700;
}
.drawer-action.reset {
background: #f3f4f6;
color: #475569;
}
.drawer-action.confirm {
background: #22486e;
color: #ffffff;
}
</style>

@ -90,6 +90,9 @@ const MENU_ROUTE_MAP = {
'\u8bbe\u5907\u603b\u89c8': '/pages_function/pages/overview/device/index',
deviceoverview: '/pages_function/pages/overview/device/index',
overviewdevice: '/pages_function/pages/overview/device/index',
'\u80fd\u6e90\u603b\u89c8': '/pages_function/pages/overview/energy/index',
energyoverview: '/pages_function/pages/overview/energy/index',
overviewenergy: '/pages_function/pages/overview/energy/index',
spare: '/pages_function/pages/spare/index',
sparepartInbound: '/pages_function/pages/sparepartInbound/index',
productInbound: '/pages_function/pages/productInbound/index',

Loading…
Cancel
Save