fix(iot): 修复产品、设备、规则与首页对标差异
- 对齐产品卡片默认图标和图片资源,修正产品导出文件名 - 对齐设备导入、属性历史、分组校验和物模型编辑行为 - 对齐首页统计空态、设备地图图例和快捷日期范围实现 - 对齐数据规则 source/sink 配置、Redis Stream 字段契约和场景联动选择器 - 补充空值判断工具测试,并将剩余 IoT 对标项迁入 donepull/348/head
parent
fab333fbb7
commit
272757995e
|
|
@ -5,7 +5,7 @@ import { requestClient } from '#/api/request';
|
||||||
export namespace MesProProcessApi {
|
export namespace MesProProcessApi {
|
||||||
/** MES 生产工序 */
|
/** MES 生产工序 */
|
||||||
export interface Process {
|
export interface Process {
|
||||||
id: number;
|
id?: number;
|
||||||
code?: string;
|
code?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
attention?: string;
|
attention?: string;
|
||||||
|
|
@ -36,3 +36,23 @@ export function getProcess(id: number) {
|
||||||
`/mes/pro/process/get?id=${id}`,
|
`/mes/pro/process/get?id=${id}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 新增生产工序 */
|
||||||
|
export function createProcess(data: MesProProcessApi.Process) {
|
||||||
|
return requestClient.post('/mes/pro/process/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改生产工序 */
|
||||||
|
export function updateProcess(data: MesProProcessApi.Process) {
|
||||||
|
return requestClient.put('/mes/pro/process/update', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除生产工序 */
|
||||||
|
export function deleteProcess(id: number) {
|
||||||
|
return requestClient.delete(`/mes/pro/process/delete?id=${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出生产工序 Excel */
|
||||||
|
export function exportProcess(params: any) {
|
||||||
|
return requestClient.download('/mes/pro/process/export-excel', { params });
|
||||||
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 12 12"><g clip-path="url(#a)"><path fill="url(#b)" fill-rule="evenodd" d="M6.958.42C6.444.216 5.61.216 5.098.42L1.15 1.975c-.77.304-.77.797 0 1.1l3.947 1.558c.514.202 1.347.202 1.86 0l3.948-1.557c.77-.304.77-.797 0-1.1L6.958.418ZM4.715 11.788a.857.857 0 0 0 .3.056c.383 0 .671-.295.671-.7V6.404c0-.49-.364-1.007-.817-1.177L1.09 3.805a.808.808 0 0 0-.284-.056c-.353 0-.581.275-.581.7V9.19c0 .508.33 1.014.763 1.177l3.726 1.422Zm2.229-.024h-.02l.073.003c.074.004.154.009.227-.019L11 10.367c.45-.168.83-.686.83-1.177V4.45c0-.413-.29-.7-.673-.7a.965.965 0 0 0-.317.055l-3.72 1.422c-.44.165-.75.67-.75 1.177v4.74c0 .42.218.621.575.621Z" clip-rule="evenodd"/></g><defs><linearGradient id="b" x1=".226" x2="11.803" y1=".267" y2="11.871" gradientUnits="userSpaceOnUse"><stop stop-color="#1B3149"/><stop offset="1" stop-color="#717D8A"/></linearGradient><clipPath id="a"><path fill="#fff" d="M0 0h12v12H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1011 B |
|
|
@ -137,16 +137,6 @@ const tableColumns = computed(() => [
|
||||||
},
|
},
|
||||||
]); // 表格列配置
|
]); // 表格列配置
|
||||||
|
|
||||||
const paginationConfig = computed(() => ({
|
|
||||||
current: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
total: total.value,
|
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
pageSizeOptions: ['10', '20', '50', '100'],
|
|
||||||
showTotal: (total: number) => `共 ${total} 条数据`,
|
|
||||||
})); // 分页配置
|
|
||||||
|
|
||||||
/** 获得设备历史数据 */
|
/** 获得设备历史数据 */
|
||||||
async function getList() {
|
async function getList() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
@ -438,7 +428,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||||
<Table
|
<Table
|
||||||
:columns="tableColumns"
|
:columns="tableColumns"
|
||||||
:data-source="list"
|
:data-source="list"
|
||||||
:pagination="paginationConfig"
|
:pagination="false"
|
||||||
:scroll="{ y: 500 }"
|
:scroll="{ y: 500 }"
|
||||||
row-key="_rowKey"
|
row-key="_rowKey"
|
||||||
size="small"
|
size="small"
|
||||||
|
|
|
||||||
|
|
@ -75,11 +75,6 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
|
|
||||||
/** 上传前 */
|
/** 上传前 */
|
||||||
function beforeUpload(file: FileType) {
|
function beforeUpload(file: FileType) {
|
||||||
const fileName = file.name?.toLowerCase() ?? '';
|
|
||||||
if (!fileName.endsWith('.xls') && !fileName.endsWith('.xlsx')) {
|
|
||||||
message.error('只能上传 Excel 文件(.xls / .xlsx)');
|
|
||||||
return Upload.LIST_IGNORE;
|
|
||||||
}
|
|
||||||
formApi.setFieldValue('file', file);
|
formApi.setFieldValue('file', file);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -97,18 +92,18 @@ async function handleDownload() {
|
||||||
<template #file>
|
<template #file>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<Upload
|
<Upload
|
||||||
:before-upload="beforeUpload"
|
|
||||||
:max-count="1"
|
:max-count="1"
|
||||||
accept=".xls,.xlsx"
|
accept=".xls,.xlsx"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
>
|
>
|
||||||
<Button type="primary"> 选择 Excel 文件</Button>
|
<Button type="primary"> 选择 Excel 文件 </Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Form>
|
</Form>
|
||||||
<template #prepend-footer>
|
<template #prepend-footer>
|
||||||
<div class="flex flex-auto items-center">
|
<div class="flex flex-auto items-center">
|
||||||
<Button @click="handleDownload"> 下载导入模板</Button>
|
<Button @click="handleDownload"> 下载导入模板 </Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请输入分组名称',
|
placeholder: '请输入分组名称',
|
||||||
},
|
},
|
||||||
rules: z
|
rules: z.string().min(1, '分组名称不能为空'),
|
||||||
.string()
|
|
||||||
.min(1, '分组名称不能为空')
|
|
||||||
.max(64, '分组名称长度不能超过 64 个字符'),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'status',
|
fieldName: 'status',
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,12 @@ export const defaultStatsData: StatsData = {
|
||||||
productCount: -1,
|
productCount: -1,
|
||||||
deviceCount: -1,
|
deviceCount: -1,
|
||||||
deviceMessageCount: -1,
|
deviceMessageCount: -1,
|
||||||
productCategoryTodayCount: 0,
|
productCategoryTodayCount: -1,
|
||||||
productTodayCount: 0,
|
productTodayCount: -1,
|
||||||
deviceTodayCount: 0,
|
deviceTodayCount: -1,
|
||||||
deviceMessageTodayCount: 0,
|
deviceMessageTodayCount: -1,
|
||||||
deviceOnlineCount: 0,
|
deviceOnlineCount: -1,
|
||||||
deviceOfflineCount: 0,
|
deviceOfflineCount: -1,
|
||||||
deviceInactiveCount: 0,
|
deviceInactiveCount: -1,
|
||||||
productCategoryDeviceCounts: {},
|
productCategoryDeviceCounts: {},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { NumberDictDataType } from '@vben/hooks';
|
||||||
|
|
||||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||||
|
|
||||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
|
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { DeviceStateEnum, DICT_TYPE } from '@vben/constants';
|
import { DeviceStateEnum, DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictLabel } from '@vben/hooks';
|
import { getDictLabel, getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import { Card, Empty, Spin } from 'ant-design-vue';
|
import { Card, Empty, Spin } from 'ant-design-vue';
|
||||||
|
|
||||||
|
|
@ -20,15 +22,20 @@ let mapInstance: any = null; // 百度地图实例
|
||||||
const loading = ref(true); // 加载状态
|
const loading = ref(true); // 加载状态
|
||||||
const deviceList = ref<IotDeviceApi.Device[]>([]); // 设备分布列表
|
const deviceList = ref<IotDeviceApi.Device[]>([]); // 设备分布列表
|
||||||
|
|
||||||
/** 是否有数据 */
|
const hasData = computed(() => deviceList.value.length > 0); // 是否有数据
|
||||||
const hasData = computed(() => deviceList.value.length > 0);
|
|
||||||
|
const stateOptions = computed(() =>
|
||||||
|
getDictOptions(
|
||||||
|
DICT_TYPE.IOT_DEVICE_STATE,
|
||||||
|
'number',
|
||||||
|
) as NumberDictDataType[],
|
||||||
|
); // 状态图例列表(从字典获取)
|
||||||
|
|
||||||
/** 设备状态颜色映射 */
|
|
||||||
const stateColorMap: Record<number, string> = {
|
const stateColorMap: Record<number, string> = {
|
||||||
[DeviceStateEnum.INACTIVE]: '#EAB308', // 待激活 - 黄色
|
[DeviceStateEnum.INACTIVE]: '#EAB308', // 待激活 - 黄色
|
||||||
[DeviceStateEnum.ONLINE]: '#22C55E', // 在线 - 绿色
|
[DeviceStateEnum.ONLINE]: '#22C55E', // 在线 - 绿色
|
||||||
[DeviceStateEnum.OFFLINE]: '#9CA3AF', // 离线 - 灰色
|
[DeviceStateEnum.OFFLINE]: '#9CA3AF', // 离线 - 灰色
|
||||||
};
|
}; // 设备状态颜色映射
|
||||||
|
|
||||||
/** 获取设备状态配置;名称走字典,颜色用本地映射 */
|
/** 获取设备状态配置;名称走字典,颜色用本地映射 */
|
||||||
function getStateConfig(state: number): { color: string; name: string } {
|
function getStateConfig(state: number): { color: string; name: string } {
|
||||||
|
|
@ -177,28 +184,18 @@ onUnmounted(() => {
|
||||||
<Card class="h-full" title="设备分布地图">
|
<Card class="h-full" title="设备分布地图">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<div class="flex items-center gap-4 text-sm">
|
<div class="flex items-center gap-4 text-sm">
|
||||||
<span class="flex items-center gap-1">
|
<span
|
||||||
<span
|
v-for="item in stateOptions"
|
||||||
class="inline-block h-3 w-3 rounded-full"
|
:key="item.value"
|
||||||
:style="{ backgroundColor: stateColorMap[DeviceStateEnum.ONLINE] }"
|
class="flex items-center gap-1"
|
||||||
></span>
|
>
|
||||||
<span class="text-gray-500">在线</span>
|
|
||||||
</span>
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<span
|
|
||||||
class="inline-block h-3 w-3 rounded-full"
|
|
||||||
:style="{ backgroundColor: stateColorMap[DeviceStateEnum.OFFLINE] }"
|
|
||||||
></span>
|
|
||||||
<span class="text-gray-500">离线</span>
|
|
||||||
</span>
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<span
|
<span
|
||||||
class="inline-block h-3 w-3 rounded-full"
|
class="inline-block h-3 w-3 rounded-full"
|
||||||
:style="{
|
:style="{
|
||||||
backgroundColor: stateColorMap[DeviceStateEnum.INACTIVE],
|
backgroundColor: stateColorMap[item.value],
|
||||||
}"
|
}"
|
||||||
></span>
|
></span>
|
||||||
<span class="text-gray-500">待激活</span>
|
<span class="text-gray-500">{{ item.label }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ async function handleViewModeChange(mode: 'card' | 'list') {
|
||||||
/** 导出表格 */
|
/** 导出表格 */
|
||||||
async function handleExport() {
|
async function handleExport() {
|
||||||
const data = await exportProduct(queryParams.value);
|
const data = await exportProduct(queryParams.value);
|
||||||
downloadFileFromBlobPart({ fileName: '产品列表.xls', source: data });
|
downloadFileFromBlobPart({ fileName: '物联网产品.xls', source: data });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 打开产品详情 */
|
/** 打开产品详情 */
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ import {
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
import { getProductPage } from '#/api/iot/product/product';
|
import { getProductPage } from '#/api/iot/product/product';
|
||||||
|
import defaultPicUrl from '#/assets/imgs/iot/device.png';
|
||||||
|
import defaultIconUrl from '#/assets/svgs/iot/cube.svg';
|
||||||
import { DictTag } from '#/components/dict-tag';
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -55,6 +57,24 @@ function getCategoryName(item: any) {
|
||||||
return item.categoryName || category?.name || '未分类';
|
return item.categoryName || category?.name || '未分类';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 是否按图片 URL 渲染产品图标 */
|
||||||
|
function isImageIcon(icon?: string) {
|
||||||
|
if (!icon) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return isHttpUrl(icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 产品图标 fallback */
|
||||||
|
function getProductIcon(icon?: string) {
|
||||||
|
return icon || defaultIconUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 产品图片 fallback */
|
||||||
|
function getProductPic(picUrl?: string) {
|
||||||
|
return picUrl || defaultPicUrl;
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取产品列表 */
|
/** 获取产品列表 */
|
||||||
async function getList() {
|
async function getList() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
@ -119,14 +139,14 @@ onMounted(() => {
|
||||||
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff] to-[#1890ff] text-white"
|
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff] to-[#1890ff] text-white"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="isHttpUrl(item.icon)"
|
v-if="isImageIcon(item.icon)"
|
||||||
:src="item.icon"
|
:src="getProductIcon(item.icon)"
|
||||||
alt=""
|
alt=""
|
||||||
class="size-6 object-contain"
|
class="size-6 object-contain"
|
||||||
/>
|
/>
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
v-else
|
v-else
|
||||||
:icon="item.icon || 'lucide:box'"
|
:icon="item.icon"
|
||||||
class="text-xl"
|
class="text-xl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -177,16 +197,10 @@ onMounted(() => {
|
||||||
class="flex size-20 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff15] to-[#1890ff15] text-[#1890ff] dark:from-[#40a9ff25] dark:to-[#1890ff25] dark:text-[#69c0ff]"
|
class="flex size-20 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff15] to-[#1890ff15] text-[#1890ff] dark:from-[#40a9ff25] dark:to-[#1890ff25] dark:text-[#69c0ff]"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
v-if="item.picUrl"
|
:src="getProductPic(item.picUrl)"
|
||||||
:src="item.picUrl"
|
|
||||||
:preview="true"
|
:preview="true"
|
||||||
class="size-full rounded object-cover"
|
class="size-full rounded object-cover"
|
||||||
/>
|
/>
|
||||||
<IconifyIcon
|
|
||||||
v-else
|
|
||||||
icon="lucide:image"
|
|
||||||
class="text-2xl opacity-50"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 按钮组 -->
|
<!-- 按钮组 -->
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,7 @@ defineExpose({ validate, getData, setData });
|
||||||
v-model:value="formData[rowIndex].productId"
|
v-model:value="formData[rowIndex].productId"
|
||||||
placeholder="请选择产品"
|
placeholder="请选择产品"
|
||||||
show-search
|
show-search
|
||||||
|
allow-clear
|
||||||
:filter-option="
|
:filter-option="
|
||||||
(input: string, option: any) =>
|
(input: string, option: any) =>
|
||||||
option.label.toLowerCase().includes(input.toLowerCase())
|
option.label.toLowerCase().includes(input.toLowerCase())
|
||||||
|
|
@ -232,6 +233,7 @@ defineExpose({ validate, getData, setData });
|
||||||
v-model:value="formData[rowIndex].deviceId"
|
v-model:value="formData[rowIndex].deviceId"
|
||||||
placeholder="请选择设备"
|
placeholder="请选择设备"
|
||||||
show-search
|
show-search
|
||||||
|
allow-clear
|
||||||
:filter-option="
|
:filter-option="
|
||||||
(input: string, option: any) =>
|
(input: string, option: any) =>
|
||||||
option.label.toLowerCase().includes(input.toLowerCase())
|
option.label.toLowerCase().includes(input.toLowerCase())
|
||||||
|
|
@ -253,6 +255,7 @@ defineExpose({ validate, getData, setData });
|
||||||
v-model:value="formData[rowIndex].method"
|
v-model:value="formData[rowIndex].method"
|
||||||
placeholder="请选择消息"
|
placeholder="请选择消息"
|
||||||
show-search
|
show-search
|
||||||
|
allow-clear
|
||||||
:filter-option="
|
:filter-option="
|
||||||
(input: string, option: any) =>
|
(input: string, option: any) =>
|
||||||
option.label.toLowerCase().includes(input.toLowerCase())
|
option.label.toLowerCase().includes(input.toLowerCase())
|
||||||
|
|
@ -273,6 +276,7 @@ defineExpose({ validate, getData, setData });
|
||||||
v-model:value="formData[rowIndex].identifier"
|
v-model:value="formData[rowIndex].identifier"
|
||||||
placeholder="请选择标识符"
|
placeholder="请选择标识符"
|
||||||
show-search
|
show-search
|
||||||
|
allow-clear
|
||||||
:loading="formData[rowIndex].identifierLoading"
|
:loading="formData[rowIndex].identifierLoading"
|
||||||
:filter-option="
|
:filter-option="
|
||||||
(input: string, option: any) =>
|
(input: string, option: any) =>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
import { Form, Input, InputNumber, Select } from 'ant-design-vue';
|
import { Form, Input, InputNumber } from 'ant-design-vue';
|
||||||
|
|
||||||
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
|
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
|
||||||
|
|
||||||
|
|
@ -12,23 +12,16 @@ const props = defineProps<{ modelValue: any }>();
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
const config = useVModel(props, 'modelValue', emit);
|
const config = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
const REDIS_DATA_STRUCTURE_OPTIONS = [
|
/** 移除当前 Redis Stream API 类型未声明的旧扩展字段 */
|
||||||
{ label: 'Stream', value: 1 },
|
function removeUnsupportedFields() {
|
||||||
{ label: 'Hash', value: 2 },
|
delete config.value.dataStructure;
|
||||||
{ label: 'List', value: 3 },
|
delete config.value.hashField;
|
||||||
{ label: 'Set', value: 4 },
|
delete config.value.scoreField;
|
||||||
{ label: 'ZSet', value: 5 },
|
}
|
||||||
{ label: 'String', value: 6 },
|
|
||||||
]; // Redis 数据结构枚举(与后端 IotRedisDataStructureEnum 对应)
|
|
||||||
|
|
||||||
const isHash = computed(() => Number(config.value?.dataStructure) === 2);
|
|
||||||
const isZSet = computed(() => Number(config.value?.dataStructure) === 5);
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!isEmpty(config.value)) {
|
if (!isEmpty(config.value)) {
|
||||||
if (config.value.dataStructure == null) {
|
removeUnsupportedFields();
|
||||||
config.value.dataStructure = 1;
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
config.value = {
|
config.value = {
|
||||||
|
|
@ -38,7 +31,6 @@ onMounted(() => {
|
||||||
password: '',
|
password: '',
|
||||||
database: 0,
|
database: 0,
|
||||||
topic: '',
|
topic: '',
|
||||||
dataStructure: 1,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -111,37 +103,4 @@ onMounted(() => {
|
||||||
>
|
>
|
||||||
<Input v-model:value="config.topic" placeholder="请输入主题" />
|
<Input v-model:value="config.topic" placeholder="请输入主题" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
|
||||||
:name="['config', 'dataStructure']"
|
|
||||||
:rules="[
|
|
||||||
{ required: true, message: 'Redis 数据结构不能为空', trigger: 'change' },
|
|
||||||
]"
|
|
||||||
label="数据结构"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
v-model:value="config.dataStructure"
|
|
||||||
:options="REDIS_DATA_STRUCTURE_OPTIONS"
|
|
||||||
placeholder="请选择 Redis 数据结构"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
v-if="isHash"
|
|
||||||
:name="['config', 'hashField']"
|
|
||||||
label="Hash 字段"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
v-model:value="config.hashField"
|
|
||||||
placeholder="留空使用 deviceId 作为 Hash 字段"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
v-if="isZSet"
|
|
||||||
:name="['config', 'scoreField']"
|
|
||||||
label="Score 字段"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
v-model:value="config.scoreField"
|
|
||||||
placeholder="留空使用当前时间戳作为 Score"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { ThingModelApi } from '#/api/iot/thingmodel';
|
import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getEventTypeLabel,
|
getEventTypeLabel,
|
||||||
getThingModelServiceCallTypeLabel,
|
getThingModelServiceCallTypeLabel,
|
||||||
|
|
@ -10,9 +8,7 @@ import {
|
||||||
IoTThingModelTypeEnum,
|
IoTThingModelTypeEnum,
|
||||||
} from '@vben/constants';
|
} from '@vben/constants';
|
||||||
|
|
||||||
import { Tooltip } from 'ant-design-vue';
|
defineProps<{ data: ThingModelApi.ThingModel }>();
|
||||||
|
|
||||||
const props = defineProps<{ data: ThingModelApi.ThingModel }>();
|
|
||||||
const NUMBER_TYPES = new Set<string>([
|
const NUMBER_TYPES = new Set<string>([
|
||||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||||
|
|
@ -27,26 +23,6 @@ const LIST_TYPES = new Set<string>([
|
||||||
IoTDataSpecsDataTypeEnum.BOOL,
|
IoTDataSpecsDataTypeEnum.BOOL,
|
||||||
IoTDataSpecsDataTypeEnum.ENUM,
|
IoTDataSpecsDataTypeEnum.ENUM,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const formattedDataSpecsList = computed(() => {
|
|
||||||
if (!props.data.property?.dataSpecsList?.length) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return props.data.property.dataSpecsList
|
|
||||||
.map((item) => `${item.name}-${item.value}`)
|
|
||||||
.join('、');
|
|
||||||
});
|
|
||||||
|
|
||||||
const shortText = computed(() => {
|
|
||||||
const list = props.data.property?.dataSpecsList;
|
|
||||||
if (!list?.length) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
const first = list[0];
|
|
||||||
return list.length > 1
|
|
||||||
? `${first.name}-${first.value} 等 ${list.length} 项`
|
|
||||||
: `${first.name}-${first.value}`;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -62,17 +38,19 @@ const shortText = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
<div v-if="PLACEHOLDER_TYPES.has(data.property?.dataType as any)">-</div>
|
<div v-if="PLACEHOLDER_TYPES.has(data.property?.dataType as any)">-</div>
|
||||||
<div v-if="LIST_TYPES.has(data.property?.dataType as any)">
|
<div v-if="LIST_TYPES.has(data.property?.dataType as any)">
|
||||||
<Tooltip :title="formattedDataSpecsList" placement="topLeft">
|
<div>
|
||||||
<span
|
{{
|
||||||
class="cursor-help border-b border-dashed border-gray-300 hover:border-blue-500 hover:text-blue-500"
|
data.property?.dataType === IoTDataSpecsDataTypeEnum.BOOL
|
||||||
>
|
? '布尔值'
|
||||||
{{
|
: '枚举值'
|
||||||
data.property?.dataType === IoTDataSpecsDataTypeEnum.BOOL
|
}}:
|
||||||
? '布尔值'
|
</div>
|
||||||
: '枚举值'
|
<div
|
||||||
}}:{{ shortText }}
|
v-for="item in data.property?.dataSpecsList || []"
|
||||||
</span>
|
:key="String(item.value)"
|
||||||
</Tooltip>
|
>
|
||||||
|
{{ item.name }}-{{ item.value }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- 服务 -->
|
<!-- 服务 -->
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,6 @@ function removeDataSpecs(val: any) {
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
v-model:value="formData.identifier"
|
v-model:value="formData.identifier"
|
||||||
:disabled="formData.id != null"
|
|
||||||
placeholder="请输入标识符"
|
placeholder="请输入标识符"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
|
||||||
|
|
@ -133,10 +133,70 @@ export const MesAutoCodeRuleCode = {
|
||||||
MD_VENDOR_CODE: 'MD_VENDOR_CODE',
|
MD_VENDOR_CODE: 'MD_VENDOR_CODE',
|
||||||
MD_WORKSTATION_CODE: 'MD_WORKSTATION_CODE',
|
MD_WORKSTATION_CODE: 'MD_WORKSTATION_CODE',
|
||||||
MD_WORKSHOP_CODE: 'MD_WORKSHOP_CODE',
|
MD_WORKSHOP_CODE: 'MD_WORKSHOP_CODE',
|
||||||
|
PRO_CARD_CODE: 'PRO_CARD_CODE',
|
||||||
|
PRO_FEEDBACK_CODE: 'PRO_FEEDBACK_CODE',
|
||||||
|
PRO_PROCESS_CODE: 'PRO_PROCESS_CODE',
|
||||||
|
PRO_ROUTE_CODE: 'PRO_ROUTE_CODE',
|
||||||
|
PRO_TASK_CODE: 'PRO_TASK_CODE',
|
||||||
|
PRO_WORK_ORDER_CODE: 'PRO_WORK_ORDER_CODE',
|
||||||
TM_TOOL_TYPE_CODE: 'TM_TOOL_TYPE_CODE',
|
TM_TOOL_TYPE_CODE: 'TM_TOOL_TYPE_CODE',
|
||||||
TM_TOOL_CODE: 'TM_TOOL_CODE',
|
TM_TOOL_CODE: 'TM_TOOL_CODE',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/** MES 生产工单状态枚举 */
|
||||||
|
export const MesProWorkOrderStatusEnum = {
|
||||||
|
PREPARE: MesOrderStatusConstants.DRAFT,
|
||||||
|
CONFIRMED: MesOrderStatusConstants.CONFIRMED,
|
||||||
|
APPROVING: MesOrderStatusConstants.APPROVING,
|
||||||
|
PRODUCING: MesOrderStatusConstants.APPROVED,
|
||||||
|
FINISHED: MesOrderStatusConstants.FINISHED,
|
||||||
|
CANCELLED: MesOrderStatusConstants.CANCELLED,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** MES 生产任务状态枚举 */
|
||||||
|
export const MesProTaskStatusEnum = {
|
||||||
|
PREPARE: MesOrderStatusConstants.DRAFT,
|
||||||
|
CONFIRMED: MesOrderStatusConstants.CONFIRMED,
|
||||||
|
APPROVING: MesOrderStatusConstants.APPROVING,
|
||||||
|
PRODUCING: MesOrderStatusConstants.APPROVED,
|
||||||
|
FINISHED: MesOrderStatusConstants.FINISHED,
|
||||||
|
CANCELLED: MesOrderStatusConstants.CANCELLED,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** MES 生产报工状态枚举 */
|
||||||
|
export const MesProFeedbackStatusEnum = {
|
||||||
|
PREPARE: MesOrderStatusConstants.DRAFT,
|
||||||
|
CONFIRMED: MesOrderStatusConstants.CONFIRMED,
|
||||||
|
APPROVING: MesOrderStatusConstants.APPROVING,
|
||||||
|
FINISHED: MesOrderStatusConstants.FINISHED,
|
||||||
|
CANCELLED: MesOrderStatusConstants.CANCELLED,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** MES 流转卡状态枚举 */
|
||||||
|
export const MesProCardStatusEnum = {
|
||||||
|
PREPARE: MesOrderStatusConstants.DRAFT,
|
||||||
|
ISSUED: MesOrderStatusConstants.CONFIRMED,
|
||||||
|
PRODUCING: MesOrderStatusConstants.APPROVED,
|
||||||
|
FINISHED: MesOrderStatusConstants.FINISHED,
|
||||||
|
CANCELLED: MesOrderStatusConstants.CANCELLED,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** MES 安灯类型枚举 */
|
||||||
|
export const MesProAndonTypeEnum = {
|
||||||
|
QUALITY: 1,
|
||||||
|
EQUIPMENT: 2,
|
||||||
|
MATERIAL: 3,
|
||||||
|
PROCESS: 4,
|
||||||
|
OTHER: 9,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** MES 安灯状态枚举 */
|
||||||
|
export const MesProAndonStatusEnum = {
|
||||||
|
TRIGGERED: 1,
|
||||||
|
HANDLING: 2,
|
||||||
|
CLOSED: 3,
|
||||||
|
} as const;
|
||||||
|
|
||||||
/** MES 编码规则分段类型枚举 */
|
/** MES 编码规则分段类型枚举 */
|
||||||
export const MesAutoCodePartTypeEnum = {
|
export const MesAutoCodePartTypeEnum = {
|
||||||
INPUT: 1,
|
INPUT: 1,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { requestClient } from '#/api/request';
|
||||||
export namespace MesProProcessApi {
|
export namespace MesProProcessApi {
|
||||||
/** MES 生产工序 */
|
/** MES 生产工序 */
|
||||||
export interface Process {
|
export interface Process {
|
||||||
id: number;
|
id?: number;
|
||||||
code?: string;
|
code?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
attention?: string;
|
attention?: string;
|
||||||
|
|
@ -36,3 +36,23 @@ export function getProcess(id: number) {
|
||||||
`/mes/pro/process/get?id=${id}`,
|
`/mes/pro/process/get?id=${id}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 新增生产工序 */
|
||||||
|
export function createProcess(data: MesProProcessApi.Process) {
|
||||||
|
return requestClient.post('/mes/pro/process/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改生产工序 */
|
||||||
|
export function updateProcess(data: MesProProcessApi.Process) {
|
||||||
|
return requestClient.put('/mes/pro/process/update', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除生产工序 */
|
||||||
|
export function deleteProcess(id: number) {
|
||||||
|
return requestClient.delete(`/mes/pro/process/delete?id=${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出生产工序 Excel */
|
||||||
|
export function exportProcess(params: any) {
|
||||||
|
return requestClient.download('/mes/pro/process/export-excel', { params });
|
||||||
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 12 12"><g clip-path="url(#a)"><path fill="url(#b)" fill-rule="evenodd" d="M6.958.42C6.444.216 5.61.216 5.098.42L1.15 1.975c-.77.304-.77.797 0 1.1l3.947 1.558c.514.202 1.347.202 1.86 0l3.948-1.557c.77-.304.77-.797 0-1.1L6.958.418ZM4.715 11.788a.857.857 0 0 0 .3.056c.383 0 .671-.295.671-.7V6.404c0-.49-.364-1.007-.817-1.177L1.09 3.805a.808.808 0 0 0-.284-.056c-.353 0-.581.275-.581.7V9.19c0 .508.33 1.014.763 1.177l3.726 1.422Zm2.229-.024h-.02l.073.003c.074.004.154.009.227-.019L11 10.367c.45-.168.83-.686.83-1.177V4.45c0-.413-.29-.7-.673-.7a.965.965 0 0 0-.317.055l-3.72 1.422c-.44.165-.75.67-.75 1.177v4.74c0 .42.218.621.575.621Z" clip-rule="evenodd"/></g><defs><linearGradient id="b" x1=".226" x2="11.803" y1=".267" y2="11.871" gradientUnits="userSpaceOnUse"><stop stop-color="#1B3149"/><stop offset="1" stop-color="#717D8A"/></linearGradient><clipPath id="a"><path fill="#fff" d="M0 0h12v12H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1011 B |
|
|
@ -18,24 +18,20 @@ const emits = defineEmits<{
|
||||||
const times = ref<[Dayjs, Dayjs]>(); // 日期范围
|
const times = ref<[Dayjs, Dayjs]>(); // 日期范围
|
||||||
|
|
||||||
const rangePickerProps = getRangePickerDefaultProps();
|
const rangePickerProps = getRangePickerDefaultProps();
|
||||||
|
|
||||||
const timeRangeOptions = [
|
const timeRangeOptions = [
|
||||||
{
|
{
|
||||||
label: '昨天',
|
label: rangePickerProps.shortcuts[1]!.text,
|
||||||
value: () => [
|
value: () => rangePickerProps.shortcuts[1]!.value() as [Dayjs, Dayjs],
|
||||||
dayjs().subtract(1, 'day').startOf('day'),
|
|
||||||
dayjs().subtract(1, 'day').endOf('day'),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '最近 7 天',
|
label: rangePickerProps.shortcuts[2]!.text,
|
||||||
value: () => [
|
value: () => [
|
||||||
dayjs().subtract(7, 'day').startOf('day'),
|
dayjs().subtract(7, 'day').startOf('day'),
|
||||||
dayjs().subtract(1, 'day').endOf('day'),
|
dayjs().subtract(1, 'day').endOf('day'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '最近 30 天',
|
label: rangePickerProps.shortcuts[3]!.text,
|
||||||
value: () => [
|
value: () => [
|
||||||
dayjs().subtract(30, 'day').startOf('day'),
|
dayjs().subtract(30, 'day').startOf('day'),
|
||||||
dayjs().subtract(1, 'day').endOf('day'),
|
dayjs().subtract(1, 'day').endOf('day'),
|
||||||
|
|
|
||||||
|
|
@ -438,7 +438,6 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
/** 同别的地方,将 style 改成 unocss 的诉求。如果不好改,就注释说明; */
|
|
||||||
.property-history-container {
|
.property-history-container {
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
|
||||||
|
|
@ -71,15 +71,11 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 上传前 */
|
/** 文件改变时 */
|
||||||
function beforeUpload(file: File) {
|
function handleChange(file: any) {
|
||||||
const fileName = file.name?.toLowerCase() ?? '';
|
if (file.raw) {
|
||||||
if (!fileName.endsWith('.xls') && !fileName.endsWith('.xlsx')) {
|
formApi.setFieldValue('file', file.raw);
|
||||||
ElMessage.error('只能上传 Excel 文件(.xls / .xlsx)');
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
formApi.setFieldValue('file', file);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 下载模版 */
|
/** 下载模版 */
|
||||||
|
|
@ -95,19 +91,19 @@ async function handleDownload() {
|
||||||
<template #file>
|
<template #file>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<ElUpload
|
<ElUpload
|
||||||
:before-upload="beforeUpload"
|
|
||||||
:limit="1"
|
:limit="1"
|
||||||
accept=".xls,.xlsx"
|
accept=".xls,.xlsx"
|
||||||
:show-file-list="false"
|
:on-change="handleChange"
|
||||||
|
:auto-upload="false"
|
||||||
>
|
>
|
||||||
<ElButton type="primary">选择 Excel 文件</ElButton>
|
<ElButton type="primary"> 选择 Excel 文件 </ElButton>
|
||||||
</ElUpload>
|
</ElUpload>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Form>
|
</Form>
|
||||||
<template #prepend-footer>
|
<template #prepend-footer>
|
||||||
<div class="flex flex-auto items-center">
|
<div class="flex flex-auto items-center">
|
||||||
<ElButton @click="handleDownload">下载导入模板</ElButton>
|
<ElButton @click="handleDownload"> 下载导入模板 </ElButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请输入分组名称',
|
placeholder: '请输入分组名称',
|
||||||
},
|
},
|
||||||
rules: z
|
rules: z.string().min(1, '分组名称不能为空'),
|
||||||
.string()
|
|
||||||
.min(1, '分组名称不能为空')
|
|
||||||
.max(64, '分组名称长度不能超过 64 个字符'),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'status',
|
fieldName: 'status',
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,12 @@ export const defaultStatsData: StatsData = {
|
||||||
productCount: -1,
|
productCount: -1,
|
||||||
deviceCount: -1,
|
deviceCount: -1,
|
||||||
deviceMessageCount: -1,
|
deviceMessageCount: -1,
|
||||||
productCategoryTodayCount: 0,
|
productCategoryTodayCount: -1,
|
||||||
productTodayCount: 0,
|
productTodayCount: -1,
|
||||||
deviceTodayCount: 0,
|
deviceTodayCount: -1,
|
||||||
deviceMessageTodayCount: 0,
|
deviceMessageTodayCount: -1,
|
||||||
deviceOnlineCount: 0,
|
deviceOnlineCount: -1,
|
||||||
deviceOfflineCount: 0,
|
deviceOfflineCount: -1,
|
||||||
deviceInactiveCount: 0,
|
deviceInactiveCount: -1,
|
||||||
productCategoryDeviceCounts: {},
|
productCategoryDeviceCounts: {},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { NumberDictDataType } from '@vben/hooks';
|
||||||
|
|
||||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||||
|
|
||||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
|
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { DeviceStateEnum, DICT_TYPE } from '@vben/constants';
|
import { DeviceStateEnum, DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictLabel } from '@vben/hooks';
|
import { getDictLabel, getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import { ElCard, ElEmpty } from 'element-plus';
|
import { ElCard, ElEmpty } from 'element-plus';
|
||||||
|
|
||||||
|
|
@ -20,15 +22,20 @@ let mapInstance: any = null; // 百度地图实例
|
||||||
const loading = ref(true); // 加载状态
|
const loading = ref(true); // 加载状态
|
||||||
const deviceList = ref<IotDeviceApi.Device[]>([]); // 设备分布列表
|
const deviceList = ref<IotDeviceApi.Device[]>([]); // 设备分布列表
|
||||||
|
|
||||||
/** 是否有数据 */
|
const hasData = computed(() => deviceList.value.length > 0); // 是否有数据
|
||||||
const hasData = computed(() => deviceList.value.length > 0);
|
|
||||||
|
const stateOptions = computed(() =>
|
||||||
|
getDictOptions(
|
||||||
|
DICT_TYPE.IOT_DEVICE_STATE,
|
||||||
|
'number',
|
||||||
|
) as NumberDictDataType[],
|
||||||
|
); // 状态图例列表(从字典获取)
|
||||||
|
|
||||||
/** 设备状态颜色映射 */
|
|
||||||
const stateColorMap: Record<number, string> = {
|
const stateColorMap: Record<number, string> = {
|
||||||
[DeviceStateEnum.INACTIVE]: '#EAB308', // 待激活 - 黄色
|
[DeviceStateEnum.INACTIVE]: '#EAB308', // 待激活 - 黄色
|
||||||
[DeviceStateEnum.ONLINE]: '#22C55E', // 在线 - 绿色
|
[DeviceStateEnum.ONLINE]: '#22C55E', // 在线 - 绿色
|
||||||
[DeviceStateEnum.OFFLINE]: '#9CA3AF', // 离线 - 灰色
|
[DeviceStateEnum.OFFLINE]: '#9CA3AF', // 离线 - 灰色
|
||||||
};
|
}; // 设备状态颜色映射
|
||||||
|
|
||||||
/** 获取设备状态配置;名称走字典,颜色用本地映射 */
|
/** 获取设备状态配置;名称走字典,颜色用本地映射 */
|
||||||
function getStateConfig(state: number): { color: string; name: string } {
|
function getStateConfig(state: number): { color: string; name: string } {
|
||||||
|
|
@ -177,32 +184,18 @@ onUnmounted(() => {
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span>设备分布地图</span>
|
<span>设备分布地图</span>
|
||||||
<div class="flex items-center gap-4 text-sm">
|
<div class="flex items-center gap-4 text-sm">
|
||||||
<span class="flex items-center gap-1">
|
<span
|
||||||
|
v-for="item in stateOptions"
|
||||||
|
:key="item.value"
|
||||||
|
class="flex items-center gap-1"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
class="inline-block h-3 w-3 rounded-full"
|
class="inline-block h-3 w-3 rounded-full"
|
||||||
:style="{
|
:style="{
|
||||||
backgroundColor: stateColorMap[DeviceStateEnum.ONLINE],
|
backgroundColor: stateColorMap[item.value],
|
||||||
}"
|
}"
|
||||||
></span>
|
></span>
|
||||||
<span class="text-gray-500">在线</span>
|
<span class="text-gray-500">{{ item.label }}</span>
|
||||||
</span>
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<span
|
|
||||||
class="inline-block h-3 w-3 rounded-full"
|
|
||||||
:style="{
|
|
||||||
backgroundColor: stateColorMap[DeviceStateEnum.OFFLINE],
|
|
||||||
}"
|
|
||||||
></span>
|
|
||||||
<span class="text-gray-500">离线</span>
|
|
||||||
</span>
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<span
|
|
||||||
class="inline-block h-3 w-3 rounded-full"
|
|
||||||
:style="{
|
|
||||||
backgroundColor: stateColorMap[DeviceStateEnum.INACTIVE],
|
|
||||||
}"
|
|
||||||
></span>
|
|
||||||
<span class="text-gray-500">待激活</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,11 @@ export function getProductName(productId?: number): string {
|
||||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||||
return [
|
return [
|
||||||
{ field: 'name', label: '固件名称' },
|
{ field: 'name', label: '固件名称' },
|
||||||
{ field: 'productName', label: '所属产品' },
|
{
|
||||||
|
field: 'productName',
|
||||||
|
label: '所属产品',
|
||||||
|
render: (val) => val || '-',
|
||||||
|
},
|
||||||
{ field: 'version', label: '固件版本' },
|
{ field: 'version', label: '固件版本' },
|
||||||
{
|
{
|
||||||
field: 'createTime',
|
field: 'createTime',
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||||
component: 'InputNumber',
|
component: 'InputNumber',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
class: '!w-full',
|
class: '!w-full',
|
||||||
|
controlsPosition: 'right',
|
||||||
placeholder: '请输入分类排序',
|
placeholder: '请输入分类排序',
|
||||||
min: 0,
|
min: 0,
|
||||||
precision: 0,
|
precision: 0,
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ async function handleViewModeChange(mode: 'card' | 'list') {
|
||||||
/** 导出表格 */
|
/** 导出表格 */
|
||||||
async function handleExport() {
|
async function handleExport() {
|
||||||
const data = await exportProduct(queryParams.value);
|
const data = await exportProduct(queryParams.value);
|
||||||
downloadFileFromBlobPart({ fileName: '产品列表.xls', source: data });
|
downloadFileFromBlobPart({ fileName: '物联网产品.xls', source: data });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 打开产品详情 */
|
/** 打开产品详情 */
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ import {
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
|
||||||
import { getProductPage } from '#/api/iot/product/product';
|
import { getProductPage } from '#/api/iot/product/product';
|
||||||
|
import defaultPicUrl from '#/assets/imgs/iot/device.png';
|
||||||
|
import defaultIconUrl from '#/assets/svgs/iot/cube.svg';
|
||||||
import { DictTag } from '#/components/dict-tag';
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -55,6 +57,24 @@ function getCategoryName(item: any) {
|
||||||
return item.categoryName || category?.name || '未分类';
|
return item.categoryName || category?.name || '未分类';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 是否按图片 URL 渲染产品图标 */
|
||||||
|
function isImageIcon(icon?: string) {
|
||||||
|
if (!icon) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return isHttpUrl(icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 产品图标 fallback */
|
||||||
|
function getProductIcon(icon?: string) {
|
||||||
|
return icon || defaultIconUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 产品图片 fallback */
|
||||||
|
function getProductPic(picUrl?: string) {
|
||||||
|
return picUrl || defaultPicUrl;
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取产品列表 */
|
/** 获取产品列表 */
|
||||||
async function getList() {
|
async function getList() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
@ -113,14 +133,14 @@ onMounted(() => {
|
||||||
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff] to-[#1890ff] text-white"
|
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff] to-[#1890ff] text-white"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="isHttpUrl(item.icon)"
|
v-if="isImageIcon(item.icon)"
|
||||||
:src="item.icon"
|
:src="getProductIcon(item.icon)"
|
||||||
alt=""
|
alt=""
|
||||||
class="size-6 object-contain"
|
class="size-6 object-contain"
|
||||||
/>
|
/>
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
v-else
|
v-else
|
||||||
:icon="item.icon || 'lucide:box'"
|
:icon="item.icon"
|
||||||
class="text-xl"
|
class="text-xl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -174,16 +194,10 @@ onMounted(() => {
|
||||||
class="flex size-20 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff15] to-[#1890ff15] text-[#1890ff] dark:from-[#40a9ff25] dark:to-[#1890ff25] dark:text-[#69c0ff]"
|
class="flex size-20 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff15] to-[#1890ff15] text-[#1890ff] dark:from-[#40a9ff25] dark:to-[#1890ff25] dark:text-[#69c0ff]"
|
||||||
>
|
>
|
||||||
<ElImage
|
<ElImage
|
||||||
v-if="item.picUrl"
|
:src="getProductPic(item.picUrl)"
|
||||||
:src="item.picUrl"
|
:preview-src-list="[getProductPic(item.picUrl)]"
|
||||||
:preview-src-list="[item.picUrl]"
|
|
||||||
class="size-full rounded object-cover"
|
class="size-full rounded object-cover"
|
||||||
/>
|
/>
|
||||||
<IconifyIcon
|
|
||||||
v-else
|
|
||||||
icon="lucide:image"
|
|
||||||
class="text-2xl opacity-50"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 按钮组 -->
|
<!-- 按钮组 -->
|
||||||
|
|
@ -216,6 +230,9 @@ onMounted(() => {
|
||||||
物模型
|
物模型
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<template v-if="hasAccessByCodes(['iot:product:delete'])">
|
<template v-if="hasAccessByCodes(['iot:product:delete'])">
|
||||||
|
<div
|
||||||
|
class="h-5 w-px self-center bg-[#dcdfe6] dark:bg-[#3a3a3a]"
|
||||||
|
></div>
|
||||||
<ElTooltip
|
<ElTooltip
|
||||||
v-if="item.status === ProductStatusEnum.PUBLISHED"
|
v-if="item.status === ProductStatusEnum.PUBLISHED"
|
||||||
content="已发布的产品不能删除"
|
content="已发布的产品不能删除"
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const activeTabName = ref('rule');
|
||||||
<ElTabPane name="rule" label="规则">
|
<ElTabPane name="rule" label="规则">
|
||||||
<DataRuleList />
|
<DataRuleList />
|
||||||
</ElTabPane>
|
</ElTabPane>
|
||||||
<ElTabPane name="sink" label="目的">
|
<ElTabPane name="sink" label="目的" lazy>
|
||||||
<DataSinkList />
|
<DataSinkList />
|
||||||
</ElTabPane>
|
</ElTabPane>
|
||||||
</ElTabs>
|
</ElTabs>
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,7 @@ defineExpose({ validate, getData, setData });
|
||||||
v-model="formData[rowIndex].productId"
|
v-model="formData[rowIndex].productId"
|
||||||
placeholder="请选择产品"
|
placeholder="请选择产品"
|
||||||
filterable
|
filterable
|
||||||
|
clearable
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@change="() => handleProductChange(rowIndex)"
|
@change="() => handleProductChange(rowIndex)"
|
||||||
>
|
>
|
||||||
|
|
@ -232,6 +233,7 @@ defineExpose({ validate, getData, setData });
|
||||||
v-model="formData[rowIndex].deviceId"
|
v-model="formData[rowIndex].deviceId"
|
||||||
placeholder="请选择设备"
|
placeholder="请选择设备"
|
||||||
filterable
|
filterable
|
||||||
|
clearable
|
||||||
class="w-full"
|
class="w-full"
|
||||||
>
|
>
|
||||||
<ElOption label="全部设备" :value="0" />
|
<ElOption label="全部设备" :value="0" />
|
||||||
|
|
@ -248,6 +250,7 @@ defineExpose({ validate, getData, setData });
|
||||||
v-model="formData[rowIndex].method"
|
v-model="formData[rowIndex].method"
|
||||||
placeholder="请选择消息"
|
placeholder="请选择消息"
|
||||||
filterable
|
filterable
|
||||||
|
clearable
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@change="() => handleMethodChange(rowIndex)"
|
@change="() => handleMethodChange(rowIndex)"
|
||||||
>
|
>
|
||||||
|
|
@ -265,6 +268,7 @@ defineExpose({ validate, getData, setData });
|
||||||
v-model="formData[rowIndex].identifier"
|
v-model="formData[rowIndex].identifier"
|
||||||
placeholder="请选择标识符"
|
placeholder="请选择标识符"
|
||||||
filterable
|
filterable
|
||||||
|
clearable
|
||||||
:loading="formData[rowIndex].identifierLoading"
|
:loading="formData[rowIndex].identifierLoading"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,10 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
import {
|
import { ElFormItem, ElInput, ElInputNumber } from 'element-plus';
|
||||||
ElFormItem,
|
|
||||||
ElInput,
|
|
||||||
ElInputNumber,
|
|
||||||
ElOption,
|
|
||||||
ElSelect,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
|
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
|
||||||
|
|
||||||
|
|
@ -18,23 +12,16 @@ const props = defineProps<{ modelValue: any }>();
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
const config = useVModel(props, 'modelValue', emit);
|
const config = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
const REDIS_DATA_STRUCTURE_OPTIONS = [
|
/** 移除当前 Redis Stream API 类型未声明的旧扩展字段 */
|
||||||
{ label: 'Stream', value: 1 },
|
function removeUnsupportedFields() {
|
||||||
{ label: 'Hash', value: 2 },
|
delete config.value.dataStructure;
|
||||||
{ label: 'List', value: 3 },
|
delete config.value.hashField;
|
||||||
{ label: 'Set', value: 4 },
|
delete config.value.scoreField;
|
||||||
{ label: 'ZSet', value: 5 },
|
}
|
||||||
{ label: 'String', value: 6 },
|
|
||||||
]; // Redis 数据结构枚举(与后端 IotRedisDataStructureEnum 对应)
|
|
||||||
|
|
||||||
const isHash = computed(() => Number(config.value?.dataStructure) === 2);
|
|
||||||
const isZSet = computed(() => Number(config.value?.dataStructure) === 5);
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!isEmpty(config.value)) {
|
if (!isEmpty(config.value)) {
|
||||||
if (config.value.dataStructure == null) {
|
removeUnsupportedFields();
|
||||||
config.value.dataStructure = 1;
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
config.value = {
|
config.value = {
|
||||||
|
|
@ -44,7 +31,6 @@ onMounted(() => {
|
||||||
password: '',
|
password: '',
|
||||||
database: 0,
|
database: 0,
|
||||||
topic: '',
|
topic: '',
|
||||||
dataStructure: 1,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -122,44 +108,4 @@ onMounted(() => {
|
||||||
>
|
>
|
||||||
<ElInput v-model="config.topic" placeholder="请输入主题" />
|
<ElInput v-model="config.topic" placeholder="请输入主题" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem
|
|
||||||
prop="config.dataStructure"
|
|
||||||
:rules="[
|
|
||||||
{ required: true, message: 'Redis 数据结构不能为空', trigger: 'change' },
|
|
||||||
]"
|
|
||||||
label="数据结构"
|
|
||||||
>
|
|
||||||
<ElSelect
|
|
||||||
v-model="config.dataStructure"
|
|
||||||
placeholder="请选择 Redis 数据结构"
|
|
||||||
class="w-full"
|
|
||||||
>
|
|
||||||
<ElOption
|
|
||||||
v-for="item in REDIS_DATA_STRUCTURE_OPTIONS"
|
|
||||||
:key="item.value"
|
|
||||||
:label="item.label"
|
|
||||||
:value="item.value"
|
|
||||||
/>
|
|
||||||
</ElSelect>
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem
|
|
||||||
v-if="isHash"
|
|
||||||
prop="config.hashField"
|
|
||||||
label="Hash 字段"
|
|
||||||
>
|
|
||||||
<ElInput
|
|
||||||
v-model="config.hashField"
|
|
||||||
placeholder="留空使用 deviceId 作为 Hash 字段"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem
|
|
||||||
v-if="isZSet"
|
|
||||||
prop="config.scoreField"
|
|
||||||
label="Score 字段"
|
|
||||||
>
|
|
||||||
<ElInput
|
|
||||||
v-model="config.scoreField"
|
|
||||||
placeholder="留空使用当前时间戳作为 Score"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ const emit = defineEmits<{
|
||||||
(e: 'change', value: { config: any; type: string }): void;
|
(e: 'change', value: { config: any; type: string }): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// TODO 芋艿
|
|
||||||
/** 属性选择器内部使用的统一数据结构 */
|
/** 属性选择器内部使用的统一数据结构 */
|
||||||
interface PropertySelectorItem {
|
interface PropertySelectorItem {
|
||||||
identifier: string;
|
identifier: string;
|
||||||
|
|
@ -65,89 +64,67 @@ interface PropertySelectorItem {
|
||||||
|
|
||||||
const localValue = useVModel(props, 'modelValue', emit);
|
const localValue = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
const loading = ref(false); // 加载状态
|
const loading = ref(false);
|
||||||
const propertyList = ref<PropertySelectorItem[]>([]); // 属性列表
|
const propertyList = ref<PropertySelectorItem[]>([]);
|
||||||
const thingModelTSL = ref<null | ThingModelApi.ThingModelTSL>(null); // 物模型 TSL 数据
|
|
||||||
|
|
||||||
// 计算属性:属性分组
|
/** 触发类型 → 物模型类型 + 分组标签 */
|
||||||
|
const TRIGGER_TYPE_TO_GROUP: Record<
|
||||||
|
number,
|
||||||
|
{ label: string; modelType: number }
|
||||||
|
> = {
|
||||||
|
[IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST]: {
|
||||||
|
label: THING_MODEL_GROUP_LABELS.PROPERTY,
|
||||||
|
modelType: IoTThingModelTypeEnum.PROPERTY,
|
||||||
|
},
|
||||||
|
[IotRuleSceneTriggerTypeEnum.TIMER]: {
|
||||||
|
label: THING_MODEL_GROUP_LABELS.PROPERTY,
|
||||||
|
modelType: IoTThingModelTypeEnum.PROPERTY,
|
||||||
|
},
|
||||||
|
[IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST]: {
|
||||||
|
label: THING_MODEL_GROUP_LABELS.EVENT,
|
||||||
|
modelType: IoTThingModelTypeEnum.EVENT,
|
||||||
|
},
|
||||||
|
[IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE]: {
|
||||||
|
label: THING_MODEL_GROUP_LABELS.SERVICE,
|
||||||
|
modelType: IoTThingModelTypeEnum.SERVICE,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 属性分组:按触发类型筛选属性 / 事件 / 服务 */
|
||||||
const propertyGroups = computed(() => {
|
const propertyGroups = computed(() => {
|
||||||
const groups: { label: string; options: any[] }[] = [];
|
const config = TRIGGER_TYPE_TO_GROUP[props.triggerType];
|
||||||
|
if (!config) return [];
|
||||||
if (
|
const options = propertyList.value.filter(
|
||||||
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST ||
|
(item) => item.type === config.modelType,
|
||||||
props.triggerType === IotRuleSceneTriggerTypeEnum.TIMER
|
|
||||||
) {
|
|
||||||
groups.push({
|
|
||||||
label: THING_MODEL_GROUP_LABELS.PROPERTY,
|
|
||||||
options: propertyList.value.filter(
|
|
||||||
(p) => p.type === IoTThingModelTypeEnum.PROPERTY,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) {
|
|
||||||
groups.push({
|
|
||||||
label: THING_MODEL_GROUP_LABELS.EVENT,
|
|
||||||
options: propertyList.value.filter(
|
|
||||||
(p) => p.type === IoTThingModelTypeEnum.EVENT,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE) {
|
|
||||||
groups.push({
|
|
||||||
label: THING_MODEL_GROUP_LABELS.SERVICE,
|
|
||||||
options: propertyList.value.filter(
|
|
||||||
(p) => p.type === IoTThingModelTypeEnum.SERVICE,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups.filter((group) => group.options.length > 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 计算属性:当前选中的属性
|
|
||||||
const selectedProperty = computed(() => {
|
|
||||||
return propertyList.value.find(
|
|
||||||
(property) => property.identifier === localValue.value,
|
|
||||||
);
|
);
|
||||||
|
return options.length > 0 ? [{ label: config.label, options }] : [];
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/** 当前选中的属性 */
|
||||||
* 处理选择变化事件
|
const selectedProperty = computed(() =>
|
||||||
* @param value 选中的属性标识符
|
propertyList.value.find(
|
||||||
*/
|
(property) => property.identifier === localValue.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 处理选择变化事件 */
|
||||||
function handleChange(value: any) {
|
function handleChange(value: any) {
|
||||||
const property = propertyList.value.find((item) => item.identifier === value);
|
const property = propertyList.value.find((item) => item.identifier === value);
|
||||||
if (property) {
|
if (property) {
|
||||||
emit('change', {
|
emit('change', { type: property.dataType, config: property });
|
||||||
type: property.dataType,
|
|
||||||
config: property,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 获取物模型 TSL 数据 */
|
||||||
* 获取物模型 TSL 数据
|
async function loadThingModelTSL() {
|
||||||
*/
|
|
||||||
async function fetchThingModelTSL() {
|
|
||||||
if (!props.productId) {
|
if (!props.productId) {
|
||||||
thingModelTSL.value = null;
|
|
||||||
propertyList.value = [];
|
propertyList.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const tslData = await getThingModelTSLByProductId(props.productId);
|
const tsl = await getThingModelTSLByProductId(props.productId);
|
||||||
|
propertyList.value = parseThingModelData(tsl);
|
||||||
if (tslData) {
|
|
||||||
thingModelTSL.value = tslData;
|
|
||||||
parseThingModelData();
|
|
||||||
} else {
|
|
||||||
console.error('获取物模型 TSL 失败 :返回数据为空');
|
|
||||||
propertyList.value = [];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取物模型 TSL 失败 :', error);
|
console.error('获取物模型 TSL 失败 :', error);
|
||||||
propertyList.value = [];
|
propertyList.value = [];
|
||||||
|
|
@ -156,104 +133,62 @@ async function fetchThingModelTSL() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 解析物模型 TSL 数据 */
|
/** 把 TSL 树展平为 PropertySelectorItem[] */
|
||||||
function parseThingModelData() {
|
function parseThingModelData(
|
||||||
const tsl = thingModelTSL.value;
|
tsl?: null | ThingModelApi.ThingModelTSL,
|
||||||
const properties: PropertySelectorItem[] = [];
|
): PropertySelectorItem[] {
|
||||||
|
if (!tsl) return [];
|
||||||
if (!tsl) {
|
const properties = (tsl.properties ?? []).map<PropertySelectorItem>(
|
||||||
propertyList.value = properties;
|
(prop) => ({
|
||||||
return;
|
identifier: prop.identifier!,
|
||||||
}
|
name: prop.name!,
|
||||||
// 解析属性
|
description: prop.description,
|
||||||
if (tsl.properties && Array.isArray(tsl.properties)) {
|
dataType: prop.dataType!,
|
||||||
tsl.properties.forEach((prop) => {
|
type: IoTThingModelTypeEnum.PROPERTY,
|
||||||
properties.push({
|
accessMode: prop.accessMode,
|
||||||
identifier: prop.identifier!,
|
required: prop.required,
|
||||||
name: prop.name!,
|
unit: prop.dataSpecs?.unit,
|
||||||
description: prop.description,
|
range: getPropertyRange(prop),
|
||||||
dataType: prop.dataType!,
|
property: prop,
|
||||||
type: IoTThingModelTypeEnum.PROPERTY,
|
}),
|
||||||
accessMode: prop.accessMode,
|
);
|
||||||
required: prop.required,
|
const events = (tsl.events ?? []).map<PropertySelectorItem>((event) => ({
|
||||||
unit: getPropertyUnit(prop),
|
identifier: event.identifier!,
|
||||||
range: getPropertyRange(prop),
|
name: event.name!,
|
||||||
property: prop,
|
description: event.description,
|
||||||
});
|
dataType: 'struct',
|
||||||
});
|
type: IoTThingModelTypeEnum.EVENT,
|
||||||
}
|
eventType: event.type,
|
||||||
|
required: event.required,
|
||||||
// 解析事件
|
outputParams: event.outputParams,
|
||||||
if (tsl.events && Array.isArray(tsl.events)) {
|
event,
|
||||||
tsl.events.forEach((event) => {
|
}));
|
||||||
properties.push({
|
const services = (tsl.services ?? []).map<PropertySelectorItem>(
|
||||||
identifier: event.identifier!,
|
(service) => ({
|
||||||
name: event.name!,
|
identifier: service.identifier!,
|
||||||
description: event.description,
|
name: service.name!,
|
||||||
dataType: 'struct',
|
description: service.description,
|
||||||
type: IoTThingModelTypeEnum.EVENT,
|
dataType: 'struct',
|
||||||
eventType: event.type,
|
type: IoTThingModelTypeEnum.SERVICE,
|
||||||
required: event.required,
|
callType: service.callType,
|
||||||
outputParams: event.outputParams,
|
required: service.required,
|
||||||
event,
|
inputParams: service.inputParams,
|
||||||
});
|
outputParams: service.outputParams,
|
||||||
});
|
service,
|
||||||
}
|
}),
|
||||||
|
);
|
||||||
// 解析服务
|
return [...properties, ...events, ...services];
|
||||||
if (tsl.services && Array.isArray(tsl.services)) {
|
|
||||||
tsl.services.forEach((service) => {
|
|
||||||
properties.push({
|
|
||||||
identifier: service.identifier!,
|
|
||||||
name: service.name!,
|
|
||||||
description: service.description,
|
|
||||||
dataType: 'struct',
|
|
||||||
type: IoTThingModelTypeEnum.SERVICE,
|
|
||||||
callType: service.callType,
|
|
||||||
required: service.required,
|
|
||||||
inputParams: service.inputParams,
|
|
||||||
outputParams: service.outputParams,
|
|
||||||
service,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
propertyList.value = properties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 获取属性取值范围:数值型给 min~max;枚举 / 布尔给选项列表 */
|
||||||
* 获取属性单位
|
function getPropertyRange(
|
||||||
* @param property 属性对象
|
property: ThingModelApi.Property,
|
||||||
* @returns 属性单位
|
): string | undefined {
|
||||||
*/
|
const specs = property.dataSpecs;
|
||||||
function getPropertyUnit(property: any) {
|
if (specs && specs.min !== undefined && specs.max !== undefined) {
|
||||||
if (!property) return undefined;
|
return `${specs.min}~${specs.max}`;
|
||||||
|
|
||||||
// 数值型数据的单位
|
|
||||||
if (property.dataSpecs && property.dataSpecs.unit) {
|
|
||||||
return property.dataSpecs.unit;
|
|
||||||
}
|
}
|
||||||
|
if (property.dataSpecsList?.length) {
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取属性范围描述
|
|
||||||
* @param property 属性对象
|
|
||||||
* @returns 属性范围描述
|
|
||||||
*/
|
|
||||||
function getPropertyRange(property: any) {
|
|
||||||
if (!property) return undefined;
|
|
||||||
|
|
||||||
// 数值型数据的范围
|
|
||||||
if (property.dataSpecs) {
|
|
||||||
const specs = property.dataSpecs;
|
|
||||||
if (specs.min !== undefined && specs.max !== undefined) {
|
|
||||||
return `${specs.min}~${specs.max}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 枚举型和布尔型数据的选项
|
|
||||||
if (property.dataSpecsList && Array.isArray(property.dataSpecsList)) {
|
|
||||||
return property.dataSpecsList
|
return property.dataSpecsList
|
||||||
.map((item: any) => `${item.name}(${item.value})`)
|
.map((item: any) => `${item.name}(${item.value})`)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
@ -266,7 +201,7 @@ function getPropertyRange(property: any) {
|
||||||
watch(
|
watch(
|
||||||
() => props.productId,
|
() => props.productId,
|
||||||
() => {
|
() => {
|
||||||
fetchThingModelTSL();
|
loadThingModelTSL();
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { ThingModelApi } from '#/api/iot/thingmodel';
|
import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getEventTypeLabel,
|
getEventTypeLabel,
|
||||||
getThingModelServiceCallTypeLabel,
|
getThingModelServiceCallTypeLabel,
|
||||||
|
|
@ -10,9 +8,7 @@ import {
|
||||||
IoTThingModelTypeEnum,
|
IoTThingModelTypeEnum,
|
||||||
} from '@vben/constants';
|
} from '@vben/constants';
|
||||||
|
|
||||||
import { ElTooltip } from 'element-plus';
|
defineProps<{ data: ThingModelApi.ThingModel }>();
|
||||||
|
|
||||||
const props = defineProps<{ data: ThingModelApi.ThingModel }>();
|
|
||||||
const NUMBER_TYPES = new Set<string>([
|
const NUMBER_TYPES = new Set<string>([
|
||||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||||
|
|
@ -27,26 +23,6 @@ const LIST_TYPES = new Set<string>([
|
||||||
IoTDataSpecsDataTypeEnum.BOOL,
|
IoTDataSpecsDataTypeEnum.BOOL,
|
||||||
IoTDataSpecsDataTypeEnum.ENUM,
|
IoTDataSpecsDataTypeEnum.ENUM,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const formattedDataSpecsList = computed(() => {
|
|
||||||
if (!props.data.property?.dataSpecsList?.length) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return props.data.property.dataSpecsList
|
|
||||||
.map((item) => `${item.name}-${item.value}`)
|
|
||||||
.join('、');
|
|
||||||
});
|
|
||||||
|
|
||||||
const shortText = computed(() => {
|
|
||||||
const list = props.data.property?.dataSpecsList;
|
|
||||||
if (!list?.length) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
const first = list[0];
|
|
||||||
return list.length > 1
|
|
||||||
? `${first.name}-${first.value} 等 ${list.length} 项`
|
|
||||||
: `${first.name}-${first.value}`;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -62,17 +38,19 @@ const shortText = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
<div v-if="PLACEHOLDER_TYPES.has(data.property?.dataType as any)">-</div>
|
<div v-if="PLACEHOLDER_TYPES.has(data.property?.dataType as any)">-</div>
|
||||||
<div v-if="LIST_TYPES.has(data.property?.dataType as any)">
|
<div v-if="LIST_TYPES.has(data.property?.dataType as any)">
|
||||||
<ElTooltip :content="formattedDataSpecsList" placement="top-start">
|
<div>
|
||||||
<span
|
{{
|
||||||
class="cursor-help border-b border-dashed border-gray-300 hover:border-blue-500 hover:text-blue-500"
|
data.property?.dataType === IoTDataSpecsDataTypeEnum.BOOL
|
||||||
>
|
? '布尔值'
|
||||||
{{
|
: '枚举值'
|
||||||
data.property?.dataType === IoTDataSpecsDataTypeEnum.BOOL
|
}}:
|
||||||
? '布尔值'
|
</div>
|
||||||
: '枚举值'
|
<div
|
||||||
}}:{{ shortText }}
|
v-for="item in data.property?.dataSpecsList || []"
|
||||||
</span>
|
:key="String(item.value)"
|
||||||
</ElTooltip>
|
>
|
||||||
|
{{ item.name }}-{{ item.value }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- 服务 -->
|
<!-- 服务 -->
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,6 @@ function removeDataSpecs(val: any) {
|
||||||
>
|
>
|
||||||
<ElInput
|
<ElInput
|
||||||
v-model="formData.identifier"
|
v-model="formData.identifier"
|
||||||
:disabled="formData.id != null"
|
|
||||||
placeholder="请输入标识符"
|
placeholder="请输入标识符"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ onMounted(async () => {
|
||||||
v-for="item in filteredList"
|
v-for="item in filteredList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:label="item.name"
|
:label="item.name"
|
||||||
:value="item.id"
|
:value="item.id!"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span>{{ item.name }}</span>
|
<span>{{ item.name }}</span>
|
||||||
|
|
|
||||||
|
|
@ -133,10 +133,70 @@ export const MesAutoCodeRuleCode = {
|
||||||
MD_VENDOR_CODE: 'MD_VENDOR_CODE',
|
MD_VENDOR_CODE: 'MD_VENDOR_CODE',
|
||||||
MD_WORKSTATION_CODE: 'MD_WORKSTATION_CODE',
|
MD_WORKSTATION_CODE: 'MD_WORKSTATION_CODE',
|
||||||
MD_WORKSHOP_CODE: 'MD_WORKSHOP_CODE',
|
MD_WORKSHOP_CODE: 'MD_WORKSHOP_CODE',
|
||||||
|
PRO_CARD_CODE: 'PRO_CARD_CODE',
|
||||||
|
PRO_FEEDBACK_CODE: 'PRO_FEEDBACK_CODE',
|
||||||
|
PRO_PROCESS_CODE: 'PRO_PROCESS_CODE',
|
||||||
|
PRO_ROUTE_CODE: 'PRO_ROUTE_CODE',
|
||||||
|
PRO_TASK_CODE: 'PRO_TASK_CODE',
|
||||||
|
PRO_WORK_ORDER_CODE: 'PRO_WORK_ORDER_CODE',
|
||||||
TM_TOOL_TYPE_CODE: 'TM_TOOL_TYPE_CODE',
|
TM_TOOL_TYPE_CODE: 'TM_TOOL_TYPE_CODE',
|
||||||
TM_TOOL_CODE: 'TM_TOOL_CODE',
|
TM_TOOL_CODE: 'TM_TOOL_CODE',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/** MES 生产工单状态枚举 */
|
||||||
|
export const MesProWorkOrderStatusEnum = {
|
||||||
|
PREPARE: MesOrderStatusConstants.DRAFT,
|
||||||
|
CONFIRMED: MesOrderStatusConstants.CONFIRMED,
|
||||||
|
APPROVING: MesOrderStatusConstants.APPROVING,
|
||||||
|
PRODUCING: MesOrderStatusConstants.APPROVED,
|
||||||
|
FINISHED: MesOrderStatusConstants.FINISHED,
|
||||||
|
CANCELLED: MesOrderStatusConstants.CANCELLED,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** MES 生产任务状态枚举 */
|
||||||
|
export const MesProTaskStatusEnum = {
|
||||||
|
PREPARE: MesOrderStatusConstants.DRAFT,
|
||||||
|
CONFIRMED: MesOrderStatusConstants.CONFIRMED,
|
||||||
|
APPROVING: MesOrderStatusConstants.APPROVING,
|
||||||
|
PRODUCING: MesOrderStatusConstants.APPROVED,
|
||||||
|
FINISHED: MesOrderStatusConstants.FINISHED,
|
||||||
|
CANCELLED: MesOrderStatusConstants.CANCELLED,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** MES 生产报工状态枚举 */
|
||||||
|
export const MesProFeedbackStatusEnum = {
|
||||||
|
PREPARE: MesOrderStatusConstants.DRAFT,
|
||||||
|
CONFIRMED: MesOrderStatusConstants.CONFIRMED,
|
||||||
|
APPROVING: MesOrderStatusConstants.APPROVING,
|
||||||
|
FINISHED: MesOrderStatusConstants.FINISHED,
|
||||||
|
CANCELLED: MesOrderStatusConstants.CANCELLED,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** MES 流转卡状态枚举 */
|
||||||
|
export const MesProCardStatusEnum = {
|
||||||
|
PREPARE: MesOrderStatusConstants.DRAFT,
|
||||||
|
ISSUED: MesOrderStatusConstants.CONFIRMED,
|
||||||
|
PRODUCING: MesOrderStatusConstants.APPROVED,
|
||||||
|
FINISHED: MesOrderStatusConstants.FINISHED,
|
||||||
|
CANCELLED: MesOrderStatusConstants.CANCELLED,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** MES 安灯类型枚举 */
|
||||||
|
export const MesProAndonTypeEnum = {
|
||||||
|
QUALITY: 1,
|
||||||
|
EQUIPMENT: 2,
|
||||||
|
MATERIAL: 3,
|
||||||
|
PROCESS: 4,
|
||||||
|
OTHER: 9,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** MES 安灯状态枚举 */
|
||||||
|
export const MesProAndonStatusEnum = {
|
||||||
|
TRIGGERED: 1,
|
||||||
|
HANDLING: 2,
|
||||||
|
CLOSED: 3,
|
||||||
|
} as const;
|
||||||
|
|
||||||
/** MES 编码规则分段类型枚举 */
|
/** MES 编码规则分段类型枚举 */
|
||||||
export const MesAutoCodePartTypeEnum = {
|
export const MesAutoCodePartTypeEnum = {
|
||||||
INPUT: 1,
|
INPUT: 1,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
getFirstNonNullOrUndefined,
|
getFirstNonNullOrUndefined,
|
||||||
isBoolean,
|
isBoolean,
|
||||||
isEmpty,
|
isEmpty,
|
||||||
|
isEmptyVal,
|
||||||
isHttpUrl,
|
isHttpUrl,
|
||||||
isObject,
|
isObject,
|
||||||
isUndefined,
|
isUndefined,
|
||||||
|
|
@ -85,6 +86,21 @@ describe('isEmpty', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isEmptyVal', () => {
|
||||||
|
it('should return true for empty value', () => {
|
||||||
|
expect(isEmptyVal('')).toBe(true);
|
||||||
|
expect(isEmptyVal(null)).toBe(true);
|
||||||
|
expect(isEmptyVal()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for valid falsy and collection values', () => {
|
||||||
|
expect(isEmptyVal(0)).toBe(false);
|
||||||
|
expect(isEmptyVal(false)).toBe(false);
|
||||||
|
expect(isEmptyVal([])).toBe(false);
|
||||||
|
expect(isEmptyVal({})).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('isWindow', () => {
|
describe('isWindow', () => {
|
||||||
it('should return true for the window object', () => {
|
it('should return true for the window object', () => {
|
||||||
expect(isWindow(window)).toBe(true);
|
expect(isWindow(window)).toBe(true);
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,21 @@ function isEmpty<T = unknown>(value?: T): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查传入的值是否为空值。
|
||||||
|
*
|
||||||
|
* 仅以下情况将被认为是空值:
|
||||||
|
* - 值为 null。
|
||||||
|
* - 值为 undefined。
|
||||||
|
* - 值为空字符串。
|
||||||
|
*
|
||||||
|
* @param value 要检查的值。
|
||||||
|
* @returns 如果值为空值,返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
function isEmptyVal(value?: unknown): value is '' | null | undefined {
|
||||||
|
return value === '' || value === null || value === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查传入的字符串是否为有效的HTTP或HTTPS URL。
|
* 检查传入的字符串是否为有效的HTTP或HTTPS URL。
|
||||||
*
|
*
|
||||||
|
|
@ -152,6 +167,7 @@ export {
|
||||||
getFirstNonNullOrUndefined,
|
getFirstNonNullOrUndefined,
|
||||||
isBoolean,
|
isBoolean,
|
||||||
isEmpty,
|
isEmpty,
|
||||||
|
isEmptyVal,
|
||||||
isFunction,
|
isFunction,
|
||||||
isHttpUrl,
|
isHttpUrl,
|
||||||
isMacOs,
|
isMacOs,
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,19 @@ const MES_DICT = {
|
||||||
MES_DV_REPAIR_RESULT: 'mes_dv_repair_result', // MES 维修结果
|
MES_DV_REPAIR_RESULT: 'mes_dv_repair_result', // MES 维修结果
|
||||||
MES_DV_CHECK_RECORD_STATUS: 'mes_dv_check_record_status', // MES 点检记录状态
|
MES_DV_CHECK_RECORD_STATUS: 'mes_dv_check_record_status', // MES 点检记录状态
|
||||||
MES_DV_CHECK_RESULT: 'mes_dv_check_result', // MES 点检结果
|
MES_DV_CHECK_RESULT: 'mes_dv_check_result', // MES 点检结果
|
||||||
|
MES_PRO_LINK_TYPE: 'mes_pro_link_type', // MES 工序关系类型
|
||||||
|
MES_PRO_WORK_ORDER_STATUS: 'mes_pro_work_order_status', // MES 生产工单状态
|
||||||
|
MES_PRO_WORK_ORDER_TYPE: 'mes_pro_work_order_type', // MES 工单类型
|
||||||
|
MES_PRO_WORK_ORDER_SOURCE_TYPE: 'mes_pro_work_order_source_type', // MES 工单来源类型
|
||||||
|
MES_PRO_TASK_STATUS: 'mes_pro_task_status', // MES 生产任务状态
|
||||||
|
MES_PRO_FEEDBACK_STATUS: 'mes_pro_feedback_status', // MES 生产报工状态
|
||||||
|
MES_PRO_FEEDBACK_TYPE: 'mes_pro_feedback_type', // MES 生产报工类型
|
||||||
|
MES_PRO_FEEDBACK_CHANNEL: 'mes_pro_feedback_channel', // MES 生产报工途径
|
||||||
|
MES_PRO_ANDON_STATUS: 'mes_pro_andon_status', // MES 安灯处置状态
|
||||||
|
MES_PRO_ANDON_LEVEL: 'mes_pro_andon_level', // MES 安灯级别
|
||||||
|
MES_PRO_WORK_RECORD_TYPE: 'mes_pro_work_record_type', // MES 上下工状态类型
|
||||||
|
MES_TIME_UNIT_TYPE: 'mes_time_unit_type', // MES 时间单位
|
||||||
|
MES_ORDER_STATUS: 'mes_order_status', // MES 单据状态
|
||||||
MES_WM_BARCODE_BIZ_TYPE: 'mes_wm_barcode_biz_type', // MES 条码业务类型
|
MES_WM_BARCODE_BIZ_TYPE: 'mes_wm_barcode_biz_type', // MES 条码业务类型
|
||||||
MES_WM_BARCODE_FORMAT: 'mes_wm_barcode_format', // MES 条码格式
|
MES_WM_BARCODE_FORMAT: 'mes_wm_barcode_format', // MES 条码格式
|
||||||
MES_WM_PRODUCT_SALES_STATUS: 'mes_wm_product_sales_status', // MES 销售出库单状态
|
MES_WM_PRODUCT_SALES_STATUS: 'mes_wm_product_sales_status', // MES 销售出库单状态
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue