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 {
|
||||
/** MES 生产工序 */
|
||||
export interface Process {
|
||||
id: number;
|
||||
id?: number;
|
||||
code?: string;
|
||||
name?: string;
|
||||
attention?: string;
|
||||
|
|
@ -36,3 +36,23 @@ export function getProcess(id: number) {
|
|||
`/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() {
|
||||
loading.value = true;
|
||||
|
|
@ -438,7 +428,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
|||
<Table
|
||||
:columns="tableColumns"
|
||||
:data-source="list"
|
||||
:pagination="paginationConfig"
|
||||
:pagination="false"
|
||||
:scroll="{ y: 500 }"
|
||||
row-key="_rowKey"
|
||||
size="small"
|
||||
|
|
|
|||
|
|
@ -75,11 +75,6 @@ const [Modal, modalApi] = useVbenModal({
|
|||
|
||||
/** 上传前 */
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -97,18 +92,18 @@ async function handleDownload() {
|
|||
<template #file>
|
||||
<div class="w-full">
|
||||
<Upload
|
||||
:before-upload="beforeUpload"
|
||||
:max-count="1"
|
||||
accept=".xls,.xlsx"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<Button type="primary"> 选择 Excel 文件</Button>
|
||||
<Button type="primary"> 选择 Excel 文件 </Button>
|
||||
</Upload>
|
||||
</div>
|
||||
</template>
|
||||
</Form>
|
||||
<template #prepend-footer>
|
||||
<div class="flex flex-auto items-center">
|
||||
<Button @click="handleDownload"> 下载导入模板</Button>
|
||||
<Button @click="handleDownload"> 下载导入模板 </Button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -26,10 +26,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
componentProps: {
|
||||
placeholder: '请输入分组名称',
|
||||
},
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, '分组名称不能为空')
|
||||
.max(64, '分组名称长度不能超过 64 个字符'),
|
||||
rules: z.string().min(1, '分组名称不能为空'),
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ export const defaultStatsData: StatsData = {
|
|||
productCount: -1,
|
||||
deviceCount: -1,
|
||||
deviceMessageCount: -1,
|
||||
productCategoryTodayCount: 0,
|
||||
productTodayCount: 0,
|
||||
deviceTodayCount: 0,
|
||||
deviceMessageTodayCount: 0,
|
||||
deviceOnlineCount: 0,
|
||||
deviceOfflineCount: 0,
|
||||
deviceInactiveCount: 0,
|
||||
productCategoryTodayCount: -1,
|
||||
productTodayCount: -1,
|
||||
deviceTodayCount: -1,
|
||||
deviceMessageTodayCount: -1,
|
||||
deviceOnlineCount: -1,
|
||||
deviceOfflineCount: -1,
|
||||
deviceInactiveCount: -1,
|
||||
productCategoryDeviceCounts: {},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
<script lang="ts" setup>
|
||||
import type { NumberDictDataType } from '@vben/hooks';
|
||||
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
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';
|
||||
|
||||
|
|
@ -20,15 +22,20 @@ let mapInstance: any = null; // 百度地图实例
|
|||
const loading = ref(true); // 加载状态
|
||||
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> = {
|
||||
[DeviceStateEnum.INACTIVE]: '#EAB308', // 待激活 - 黄色
|
||||
[DeviceStateEnum.ONLINE]: '#22C55E', // 在线 - 绿色
|
||||
[DeviceStateEnum.OFFLINE]: '#9CA3AF', // 离线 - 灰色
|
||||
};
|
||||
}; // 设备状态颜色映射
|
||||
|
||||
/** 获取设备状态配置;名称走字典,颜色用本地映射 */
|
||||
function getStateConfig(state: number): { color: string; name: string } {
|
||||
|
|
@ -177,28 +184,18 @@ onUnmounted(() => {
|
|||
<Card class="h-full" title="设备分布地图">
|
||||
<template #extra>
|
||||
<div class="flex items-center gap-4 text-sm">
|
||||
<span class="flex items-center gap-1">
|
||||
<span
|
||||
class="inline-block h-3 w-3 rounded-full"
|
||||
:style="{ backgroundColor: stateColorMap[DeviceStateEnum.ONLINE] }"
|
||||
></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
|
||||
v-for="item in stateOptions"
|
||||
:key="item.value"
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<span
|
||||
class="inline-block h-3 w-3 rounded-full"
|
||||
:style="{
|
||||
backgroundColor: stateColorMap[DeviceStateEnum.INACTIVE],
|
||||
backgroundColor: stateColorMap[item.value],
|
||||
}"
|
||||
></span>
|
||||
<span class="text-gray-500">待激活</span>
|
||||
<span class="text-gray-500">{{ item.label }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ async function handleViewModeChange(mode: 'card' | 'list') {
|
|||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
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';
|
||||
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -55,6 +57,24 @@ function getCategoryName(item: any) {
|
|||
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() {
|
||||
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"
|
||||
>
|
||||
<img
|
||||
v-if="isHttpUrl(item.icon)"
|
||||
:src="item.icon"
|
||||
v-if="isImageIcon(item.icon)"
|
||||
:src="getProductIcon(item.icon)"
|
||||
alt=""
|
||||
class="size-6 object-contain"
|
||||
/>
|
||||
<IconifyIcon
|
||||
v-else
|
||||
:icon="item.icon || 'lucide:box'"
|
||||
:icon="item.icon"
|
||||
class="text-xl"
|
||||
/>
|
||||
</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]"
|
||||
>
|
||||
<Image
|
||||
v-if="item.picUrl"
|
||||
:src="item.picUrl"
|
||||
:src="getProductPic(item.picUrl)"
|
||||
:preview="true"
|
||||
class="size-full rounded object-cover"
|
||||
/>
|
||||
<IconifyIcon
|
||||
v-else
|
||||
icon="lucide:image"
|
||||
class="text-2xl opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 按钮组 -->
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ defineExpose({ validate, getData, setData });
|
|||
v-model:value="formData[rowIndex].productId"
|
||||
placeholder="请选择产品"
|
||||
show-search
|
||||
allow-clear
|
||||
:filter-option="
|
||||
(input: string, option: any) =>
|
||||
option.label.toLowerCase().includes(input.toLowerCase())
|
||||
|
|
@ -232,6 +233,7 @@ defineExpose({ validate, getData, setData });
|
|||
v-model:value="formData[rowIndex].deviceId"
|
||||
placeholder="请选择设备"
|
||||
show-search
|
||||
allow-clear
|
||||
:filter-option="
|
||||
(input: string, option: any) =>
|
||||
option.label.toLowerCase().includes(input.toLowerCase())
|
||||
|
|
@ -253,6 +255,7 @@ defineExpose({ validate, getData, setData });
|
|||
v-model:value="formData[rowIndex].method"
|
||||
placeholder="请选择消息"
|
||||
show-search
|
||||
allow-clear
|
||||
:filter-option="
|
||||
(input: string, option: any) =>
|
||||
option.label.toLowerCase().includes(input.toLowerCase())
|
||||
|
|
@ -273,6 +276,7 @@ defineExpose({ validate, getData, setData });
|
|||
v-model:value="formData[rowIndex].identifier"
|
||||
placeholder="请选择标识符"
|
||||
show-search
|
||||
allow-clear
|
||||
:loading="formData[rowIndex].identifierLoading"
|
||||
:filter-option="
|
||||
(input: string, option: any) =>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
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';
|
||||
|
||||
|
|
@ -12,23 +12,16 @@ const props = defineProps<{ modelValue: any }>();
|
|||
const emit = defineEmits(['update:modelValue']);
|
||||
const config = useVModel(props, 'modelValue', emit);
|
||||
|
||||
const REDIS_DATA_STRUCTURE_OPTIONS = [
|
||||
{ label: 'Stream', value: 1 },
|
||||
{ label: 'Hash', value: 2 },
|
||||
{ label: 'List', value: 3 },
|
||||
{ label: 'Set', value: 4 },
|
||||
{ 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);
|
||||
/** 移除当前 Redis Stream API 类型未声明的旧扩展字段 */
|
||||
function removeUnsupportedFields() {
|
||||
delete config.value.dataStructure;
|
||||
delete config.value.hashField;
|
||||
delete config.value.scoreField;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!isEmpty(config.value)) {
|
||||
if (config.value.dataStructure == null) {
|
||||
config.value.dataStructure = 1;
|
||||
}
|
||||
removeUnsupportedFields();
|
||||
return;
|
||||
}
|
||||
config.value = {
|
||||
|
|
@ -38,7 +31,6 @@ onMounted(() => {
|
|||
password: '',
|
||||
database: 0,
|
||||
topic: '',
|
||||
dataStructure: 1,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
|
@ -111,37 +103,4 @@ onMounted(() => {
|
|||
>
|
||||
<Input v-model:value="config.topic" placeholder="请输入主题" />
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import {
|
||||
getEventTypeLabel,
|
||||
getThingModelServiceCallTypeLabel,
|
||||
|
|
@ -10,9 +8,7 @@ import {
|
|||
IoTThingModelTypeEnum,
|
||||
} from '@vben/constants';
|
||||
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
|
||||
const props = defineProps<{ data: ThingModelApi.ThingModel }>();
|
||||
defineProps<{ data: ThingModelApi.ThingModel }>();
|
||||
const NUMBER_TYPES = new Set<string>([
|
||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||
|
|
@ -27,26 +23,6 @@ const LIST_TYPES = new Set<string>([
|
|||
IoTDataSpecsDataTypeEnum.BOOL,
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
|
@ -62,17 +38,19 @@ const shortText = computed(() => {
|
|||
</div>
|
||||
<div v-if="PLACEHOLDER_TYPES.has(data.property?.dataType as any)">-</div>
|
||||
<div v-if="LIST_TYPES.has(data.property?.dataType as any)">
|
||||
<Tooltip :title="formattedDataSpecsList" placement="topLeft">
|
||||
<span
|
||||
class="cursor-help border-b border-dashed border-gray-300 hover:border-blue-500 hover:text-blue-500"
|
||||
>
|
||||
{{
|
||||
data.property?.dataType === IoTDataSpecsDataTypeEnum.BOOL
|
||||
? '布尔值'
|
||||
: '枚举值'
|
||||
}}:{{ shortText }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<div>
|
||||
{{
|
||||
data.property?.dataType === IoTDataSpecsDataTypeEnum.BOOL
|
||||
? '布尔值'
|
||||
: '枚举值'
|
||||
}}:
|
||||
</div>
|
||||
<div
|
||||
v-for="item in data.property?.dataSpecsList || []"
|
||||
:key="String(item.value)"
|
||||
>
|
||||
{{ item.name }}-{{ item.value }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 服务 -->
|
||||
|
|
|
|||
|
|
@ -218,7 +218,6 @@ function removeDataSpecs(val: any) {
|
|||
>
|
||||
<Input
|
||||
v-model:value="formData.identifier"
|
||||
:disabled="formData.id != null"
|
||||
placeholder="请输入标识符"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
|
|
|||
|
|
@ -133,10 +133,70 @@ export const MesAutoCodeRuleCode = {
|
|||
MD_VENDOR_CODE: 'MD_VENDOR_CODE',
|
||||
MD_WORKSTATION_CODE: 'MD_WORKSTATION_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_CODE: 'TM_TOOL_CODE',
|
||||
} 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 编码规则分段类型枚举 */
|
||||
export const MesAutoCodePartTypeEnum = {
|
||||
INPUT: 1,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { requestClient } from '#/api/request';
|
|||
export namespace MesProProcessApi {
|
||||
/** MES 生产工序 */
|
||||
export interface Process {
|
||||
id: number;
|
||||
id?: number;
|
||||
code?: string;
|
||||
name?: string;
|
||||
attention?: string;
|
||||
|
|
@ -36,3 +36,23 @@ export function getProcess(id: number) {
|
|||
`/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 rangePickerProps = getRangePickerDefaultProps();
|
||||
|
||||
const timeRangeOptions = [
|
||||
{
|
||||
label: '昨天',
|
||||
value: () => [
|
||||
dayjs().subtract(1, 'day').startOf('day'),
|
||||
dayjs().subtract(1, 'day').endOf('day'),
|
||||
],
|
||||
label: rangePickerProps.shortcuts[1]!.text,
|
||||
value: () => rangePickerProps.shortcuts[1]!.value() as [Dayjs, Dayjs],
|
||||
},
|
||||
{
|
||||
label: '最近 7 天',
|
||||
label: rangePickerProps.shortcuts[2]!.text,
|
||||
value: () => [
|
||||
dayjs().subtract(7, 'day').startOf('day'),
|
||||
dayjs().subtract(1, 'day').endOf('day'),
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '最近 30 天',
|
||||
label: rangePickerProps.shortcuts[3]!.text,
|
||||
value: () => [
|
||||
dayjs().subtract(30, 'day').startOf('day'),
|
||||
dayjs().subtract(1, 'day').endOf('day'),
|
||||
|
|
|
|||
|
|
@ -438,7 +438,6 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/** 同别的地方,将 style 改成 unocss 的诉求。如果不好改,就注释说明; */
|
||||
.property-history-container {
|
||||
max-height: 70vh;
|
||||
overflow: auto;
|
||||
|
|
|
|||
|
|
@ -71,15 +71,11 @@ const [Modal, modalApi] = useVbenModal({
|
|||
},
|
||||
});
|
||||
|
||||
/** 上传前 */
|
||||
function beforeUpload(file: File) {
|
||||
const fileName = file.name?.toLowerCase() ?? '';
|
||||
if (!fileName.endsWith('.xls') && !fileName.endsWith('.xlsx')) {
|
||||
ElMessage.error('只能上传 Excel 文件(.xls / .xlsx)');
|
||||
return false;
|
||||
/** 文件改变时 */
|
||||
function handleChange(file: any) {
|
||||
if (file.raw) {
|
||||
formApi.setFieldValue('file', file.raw);
|
||||
}
|
||||
formApi.setFieldValue('file', file);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 下载模版 */
|
||||
|
|
@ -95,19 +91,19 @@ async function handleDownload() {
|
|||
<template #file>
|
||||
<div class="w-full">
|
||||
<ElUpload
|
||||
:before-upload="beforeUpload"
|
||||
:limit="1"
|
||||
accept=".xls,.xlsx"
|
||||
:show-file-list="false"
|
||||
:on-change="handleChange"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<ElButton type="primary">选择 Excel 文件</ElButton>
|
||||
<ElButton type="primary"> 选择 Excel 文件 </ElButton>
|
||||
</ElUpload>
|
||||
</div>
|
||||
</template>
|
||||
</Form>
|
||||
<template #prepend-footer>
|
||||
<div class="flex flex-auto items-center">
|
||||
<ElButton @click="handleDownload">下载导入模板</ElButton>
|
||||
<ElButton @click="handleDownload"> 下载导入模板 </ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -26,10 +26,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
componentProps: {
|
||||
placeholder: '请输入分组名称',
|
||||
},
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, '分组名称不能为空')
|
||||
.max(64, '分组名称长度不能超过 64 个字符'),
|
||||
rules: z.string().min(1, '分组名称不能为空'),
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ export const defaultStatsData: StatsData = {
|
|||
productCount: -1,
|
||||
deviceCount: -1,
|
||||
deviceMessageCount: -1,
|
||||
productCategoryTodayCount: 0,
|
||||
productTodayCount: 0,
|
||||
deviceTodayCount: 0,
|
||||
deviceMessageTodayCount: 0,
|
||||
deviceOnlineCount: 0,
|
||||
deviceOfflineCount: 0,
|
||||
deviceInactiveCount: 0,
|
||||
productCategoryTodayCount: -1,
|
||||
productTodayCount: -1,
|
||||
deviceTodayCount: -1,
|
||||
deviceMessageTodayCount: -1,
|
||||
deviceOnlineCount: -1,
|
||||
deviceOfflineCount: -1,
|
||||
deviceInactiveCount: -1,
|
||||
productCategoryDeviceCounts: {},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
<script lang="ts" setup>
|
||||
import type { NumberDictDataType } from '@vben/hooks';
|
||||
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { DeviceStateEnum, DICT_TYPE } from '@vben/constants';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
import { getDictLabel, getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { ElCard, ElEmpty } from 'element-plus';
|
||||
|
||||
|
|
@ -20,15 +22,20 @@ let mapInstance: any = null; // 百度地图实例
|
|||
const loading = ref(true); // 加载状态
|
||||
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> = {
|
||||
[DeviceStateEnum.INACTIVE]: '#EAB308', // 待激活 - 黄色
|
||||
[DeviceStateEnum.ONLINE]: '#22C55E', // 在线 - 绿色
|
||||
[DeviceStateEnum.OFFLINE]: '#9CA3AF', // 离线 - 灰色
|
||||
};
|
||||
}; // 设备状态颜色映射
|
||||
|
||||
/** 获取设备状态配置;名称走字典,颜色用本地映射 */
|
||||
function getStateConfig(state: number): { color: string; name: string } {
|
||||
|
|
@ -177,32 +184,18 @@ onUnmounted(() => {
|
|||
<div class="flex items-center justify-between">
|
||||
<span>设备分布地图</span>
|
||||
<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
|
||||
class="inline-block h-3 w-3 rounded-full"
|
||||
:style="{
|
||||
backgroundColor: stateColorMap[DeviceStateEnum.ONLINE],
|
||||
backgroundColor: stateColorMap[item.value],
|
||||
}"
|
||||
></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
|
||||
class="inline-block h-3 w-3 rounded-full"
|
||||
:style="{
|
||||
backgroundColor: stateColorMap[DeviceStateEnum.INACTIVE],
|
||||
}"
|
||||
></span>
|
||||
<span class="text-gray-500">待激活</span>
|
||||
<span class="text-gray-500">{{ item.label }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ export function getProductName(productId?: number): string {
|
|||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{ field: 'name', label: '固件名称' },
|
||||
{ field: 'productName', label: '所属产品' },
|
||||
{
|
||||
field: 'productName',
|
||||
label: '所属产品',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{ field: 'version', label: '固件版本' },
|
||||
{
|
||||
field: 'createTime',
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
class: '!w-full',
|
||||
controlsPosition: 'right',
|
||||
placeholder: '请输入分类排序',
|
||||
min: 0,
|
||||
precision: 0,
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ async function handleViewModeChange(mode: 'card' | 'list') {
|
|||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportProduct(queryParams.value);
|
||||
downloadFileFromBlobPart({ fileName: '产品列表.xls', source: data });
|
||||
downloadFileFromBlobPart({ fileName: '物联网产品.xls', source: data });
|
||||
}
|
||||
|
||||
/** 打开产品详情 */
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import {
|
|||
} from 'element-plus';
|
||||
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -55,6 +57,24 @@ function getCategoryName(item: any) {
|
|||
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() {
|
||||
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"
|
||||
>
|
||||
<img
|
||||
v-if="isHttpUrl(item.icon)"
|
||||
:src="item.icon"
|
||||
v-if="isImageIcon(item.icon)"
|
||||
:src="getProductIcon(item.icon)"
|
||||
alt=""
|
||||
class="size-6 object-contain"
|
||||
/>
|
||||
<IconifyIcon
|
||||
v-else
|
||||
:icon="item.icon || 'lucide:box'"
|
||||
:icon="item.icon"
|
||||
class="text-xl"
|
||||
/>
|
||||
</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]"
|
||||
>
|
||||
<ElImage
|
||||
v-if="item.picUrl"
|
||||
:src="item.picUrl"
|
||||
:preview-src-list="[item.picUrl]"
|
||||
:src="getProductPic(item.picUrl)"
|
||||
:preview-src-list="[getProductPic(item.picUrl)]"
|
||||
class="size-full rounded object-cover"
|
||||
/>
|
||||
<IconifyIcon
|
||||
v-else
|
||||
icon="lucide:image"
|
||||
class="text-2xl opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 按钮组 -->
|
||||
|
|
@ -216,6 +230,9 @@ onMounted(() => {
|
|||
物模型
|
||||
</ElButton>
|
||||
<template v-if="hasAccessByCodes(['iot:product:delete'])">
|
||||
<div
|
||||
class="h-5 w-px self-center bg-[#dcdfe6] dark:bg-[#3a3a3a]"
|
||||
></div>
|
||||
<ElTooltip
|
||||
v-if="item.status === ProductStatusEnum.PUBLISHED"
|
||||
content="已发布的产品不能删除"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const activeTabName = ref('rule');
|
|||
<ElTabPane name="rule" label="规则">
|
||||
<DataRuleList />
|
||||
</ElTabPane>
|
||||
<ElTabPane name="sink" label="目的">
|
||||
<ElTabPane name="sink" label="目的" lazy>
|
||||
<DataSinkList />
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ defineExpose({ validate, getData, setData });
|
|||
v-model="formData[rowIndex].productId"
|
||||
placeholder="请选择产品"
|
||||
filterable
|
||||
clearable
|
||||
class="w-full"
|
||||
@change="() => handleProductChange(rowIndex)"
|
||||
>
|
||||
|
|
@ -232,6 +233,7 @@ defineExpose({ validate, getData, setData });
|
|||
v-model="formData[rowIndex].deviceId"
|
||||
placeholder="请选择设备"
|
||||
filterable
|
||||
clearable
|
||||
class="w-full"
|
||||
>
|
||||
<ElOption label="全部设备" :value="0" />
|
||||
|
|
@ -248,6 +250,7 @@ defineExpose({ validate, getData, setData });
|
|||
v-model="formData[rowIndex].method"
|
||||
placeholder="请选择消息"
|
||||
filterable
|
||||
clearable
|
||||
class="w-full"
|
||||
@change="() => handleMethodChange(rowIndex)"
|
||||
>
|
||||
|
|
@ -265,6 +268,7 @@ defineExpose({ validate, getData, setData });
|
|||
v-model="formData[rowIndex].identifier"
|
||||
placeholder="请选择标识符"
|
||||
filterable
|
||||
clearable
|
||||
:loading="formData[rowIndex].identifierLoading"
|
||||
class="w-full"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,10 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
import { ElFormItem, ElInput, ElInputNumber } from 'element-plus';
|
||||
|
||||
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
|
||||
|
||||
|
|
@ -18,23 +12,16 @@ const props = defineProps<{ modelValue: any }>();
|
|||
const emit = defineEmits(['update:modelValue']);
|
||||
const config = useVModel(props, 'modelValue', emit);
|
||||
|
||||
const REDIS_DATA_STRUCTURE_OPTIONS = [
|
||||
{ label: 'Stream', value: 1 },
|
||||
{ label: 'Hash', value: 2 },
|
||||
{ label: 'List', value: 3 },
|
||||
{ label: 'Set', value: 4 },
|
||||
{ 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);
|
||||
/** 移除当前 Redis Stream API 类型未声明的旧扩展字段 */
|
||||
function removeUnsupportedFields() {
|
||||
delete config.value.dataStructure;
|
||||
delete config.value.hashField;
|
||||
delete config.value.scoreField;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!isEmpty(config.value)) {
|
||||
if (config.value.dataStructure == null) {
|
||||
config.value.dataStructure = 1;
|
||||
}
|
||||
removeUnsupportedFields();
|
||||
return;
|
||||
}
|
||||
config.value = {
|
||||
|
|
@ -44,7 +31,6 @@ onMounted(() => {
|
|||
password: '',
|
||||
database: 0,
|
||||
topic: '',
|
||||
dataStructure: 1,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
|
@ -122,44 +108,4 @@ onMounted(() => {
|
|||
>
|
||||
<ElInput v-model="config.topic" placeholder="请输入主题" />
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ const emit = defineEmits<{
|
|||
(e: 'change', value: { config: any; type: string }): void;
|
||||
}>();
|
||||
|
||||
// TODO 芋艿
|
||||
/** 属性选择器内部使用的统一数据结构 */
|
||||
interface PropertySelectorItem {
|
||||
identifier: string;
|
||||
|
|
@ -65,89 +64,67 @@ interface PropertySelectorItem {
|
|||
|
||||
const localValue = useVModel(props, 'modelValue', emit);
|
||||
|
||||
const loading = ref(false); // 加载状态
|
||||
const propertyList = ref<PropertySelectorItem[]>([]); // 属性列表
|
||||
const thingModelTSL = ref<null | ThingModelApi.ThingModelTSL>(null); // 物模型 TSL 数据
|
||||
const loading = ref(false);
|
||||
const propertyList = ref<PropertySelectorItem[]>([]);
|
||||
|
||||
// 计算属性:属性分组
|
||||
/** 触发类型 → 物模型类型 + 分组标签 */
|
||||
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 groups: { label: string; options: any[] }[] = [];
|
||||
|
||||
if (
|
||||
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST ||
|
||||
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,
|
||||
const config = TRIGGER_TYPE_TO_GROUP[props.triggerType];
|
||||
if (!config) return [];
|
||||
const options = propertyList.value.filter(
|
||||
(item) => item.type === config.modelType,
|
||||
);
|
||||
return options.length > 0 ? [{ label: config.label, options }] : [];
|
||||
});
|
||||
|
||||
/**
|
||||
* 处理选择变化事件
|
||||
* @param value 选中的属性标识符
|
||||
*/
|
||||
/** 当前选中的属性 */
|
||||
const selectedProperty = computed(() =>
|
||||
propertyList.value.find(
|
||||
(property) => property.identifier === localValue.value,
|
||||
),
|
||||
);
|
||||
|
||||
/** 处理选择变化事件 */
|
||||
function handleChange(value: any) {
|
||||
const property = propertyList.value.find((item) => item.identifier === value);
|
||||
if (property) {
|
||||
emit('change', {
|
||||
type: property.dataType,
|
||||
config: property,
|
||||
});
|
||||
emit('change', { type: property.dataType, config: property });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取物模型 TSL 数据
|
||||
*/
|
||||
async function fetchThingModelTSL() {
|
||||
/** 获取物模型 TSL 数据 */
|
||||
async function loadThingModelTSL() {
|
||||
if (!props.productId) {
|
||||
thingModelTSL.value = null;
|
||||
propertyList.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const tslData = await getThingModelTSLByProductId(props.productId);
|
||||
|
||||
if (tslData) {
|
||||
thingModelTSL.value = tslData;
|
||||
parseThingModelData();
|
||||
} else {
|
||||
console.error('获取物模型 TSL 失败 :返回数据为空');
|
||||
propertyList.value = [];
|
||||
}
|
||||
const tsl = await getThingModelTSLByProductId(props.productId);
|
||||
propertyList.value = parseThingModelData(tsl);
|
||||
} catch (error) {
|
||||
console.error('获取物模型 TSL 失败 :', error);
|
||||
propertyList.value = [];
|
||||
|
|
@ -156,104 +133,62 @@ async function fetchThingModelTSL() {
|
|||
}
|
||||
}
|
||||
|
||||
/** 解析物模型 TSL 数据 */
|
||||
function parseThingModelData() {
|
||||
const tsl = thingModelTSL.value;
|
||||
const properties: PropertySelectorItem[] = [];
|
||||
|
||||
if (!tsl) {
|
||||
propertyList.value = properties;
|
||||
return;
|
||||
}
|
||||
// 解析属性
|
||||
if (tsl.properties && Array.isArray(tsl.properties)) {
|
||||
tsl.properties.forEach((prop) => {
|
||||
properties.push({
|
||||
identifier: prop.identifier!,
|
||||
name: prop.name!,
|
||||
description: prop.description,
|
||||
dataType: prop.dataType!,
|
||||
type: IoTThingModelTypeEnum.PROPERTY,
|
||||
accessMode: prop.accessMode,
|
||||
required: prop.required,
|
||||
unit: getPropertyUnit(prop),
|
||||
range: getPropertyRange(prop),
|
||||
property: prop,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 解析事件
|
||||
if (tsl.events && Array.isArray(tsl.events)) {
|
||||
tsl.events.forEach((event) => {
|
||||
properties.push({
|
||||
identifier: event.identifier!,
|
||||
name: event.name!,
|
||||
description: event.description,
|
||||
dataType: 'struct',
|
||||
type: IoTThingModelTypeEnum.EVENT,
|
||||
eventType: event.type,
|
||||
required: event.required,
|
||||
outputParams: event.outputParams,
|
||||
event,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 解析服务
|
||||
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;
|
||||
/** 把 TSL 树展平为 PropertySelectorItem[] */
|
||||
function parseThingModelData(
|
||||
tsl?: null | ThingModelApi.ThingModelTSL,
|
||||
): PropertySelectorItem[] {
|
||||
if (!tsl) return [];
|
||||
const properties = (tsl.properties ?? []).map<PropertySelectorItem>(
|
||||
(prop) => ({
|
||||
identifier: prop.identifier!,
|
||||
name: prop.name!,
|
||||
description: prop.description,
|
||||
dataType: prop.dataType!,
|
||||
type: IoTThingModelTypeEnum.PROPERTY,
|
||||
accessMode: prop.accessMode,
|
||||
required: prop.required,
|
||||
unit: prop.dataSpecs?.unit,
|
||||
range: getPropertyRange(prop),
|
||||
property: prop,
|
||||
}),
|
||||
);
|
||||
const events = (tsl.events ?? []).map<PropertySelectorItem>((event) => ({
|
||||
identifier: event.identifier!,
|
||||
name: event.name!,
|
||||
description: event.description,
|
||||
dataType: 'struct',
|
||||
type: IoTThingModelTypeEnum.EVENT,
|
||||
eventType: event.type,
|
||||
required: event.required,
|
||||
outputParams: event.outputParams,
|
||||
event,
|
||||
}));
|
||||
const services = (tsl.services ?? []).map<PropertySelectorItem>(
|
||||
(service) => ({
|
||||
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,
|
||||
}),
|
||||
);
|
||||
return [...properties, ...events, ...services];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性单位
|
||||
* @param property 属性对象
|
||||
* @returns 属性单位
|
||||
*/
|
||||
function getPropertyUnit(property: any) {
|
||||
if (!property) return undefined;
|
||||
|
||||
// 数值型数据的单位
|
||||
if (property.dataSpecs && property.dataSpecs.unit) {
|
||||
return property.dataSpecs.unit;
|
||||
/** 获取属性取值范围:数值型给 min~max;枚举 / 布尔给选项列表 */
|
||||
function getPropertyRange(
|
||||
property: ThingModelApi.Property,
|
||||
): string | undefined {
|
||||
const specs = property.dataSpecs;
|
||||
if (specs && specs.min !== undefined && specs.max !== undefined) {
|
||||
return `${specs.min}~${specs.max}`;
|
||||
}
|
||||
|
||||
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)) {
|
||||
if (property.dataSpecsList?.length) {
|
||||
return property.dataSpecsList
|
||||
.map((item: any) => `${item.name}(${item.value})`)
|
||||
.join(', ');
|
||||
|
|
@ -266,7 +201,7 @@ function getPropertyRange(property: any) {
|
|||
watch(
|
||||
() => props.productId,
|
||||
() => {
|
||||
fetchThingModelTSL();
|
||||
loadThingModelTSL();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import {
|
||||
getEventTypeLabel,
|
||||
getThingModelServiceCallTypeLabel,
|
||||
|
|
@ -10,9 +8,7 @@ import {
|
|||
IoTThingModelTypeEnum,
|
||||
} from '@vben/constants';
|
||||
|
||||
import { ElTooltip } from 'element-plus';
|
||||
|
||||
const props = defineProps<{ data: ThingModelApi.ThingModel }>();
|
||||
defineProps<{ data: ThingModelApi.ThingModel }>();
|
||||
const NUMBER_TYPES = new Set<string>([
|
||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||
|
|
@ -27,26 +23,6 @@ const LIST_TYPES = new Set<string>([
|
|||
IoTDataSpecsDataTypeEnum.BOOL,
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
|
@ -62,17 +38,19 @@ const shortText = computed(() => {
|
|||
</div>
|
||||
<div v-if="PLACEHOLDER_TYPES.has(data.property?.dataType as any)">-</div>
|
||||
<div v-if="LIST_TYPES.has(data.property?.dataType as any)">
|
||||
<ElTooltip :content="formattedDataSpecsList" placement="top-start">
|
||||
<span
|
||||
class="cursor-help border-b border-dashed border-gray-300 hover:border-blue-500 hover:text-blue-500"
|
||||
>
|
||||
{{
|
||||
data.property?.dataType === IoTDataSpecsDataTypeEnum.BOOL
|
||||
? '布尔值'
|
||||
: '枚举值'
|
||||
}}:{{ shortText }}
|
||||
</span>
|
||||
</ElTooltip>
|
||||
<div>
|
||||
{{
|
||||
data.property?.dataType === IoTDataSpecsDataTypeEnum.BOOL
|
||||
? '布尔值'
|
||||
: '枚举值'
|
||||
}}:
|
||||
</div>
|
||||
<div
|
||||
v-for="item in data.property?.dataSpecsList || []"
|
||||
:key="String(item.value)"
|
||||
>
|
||||
{{ item.name }}-{{ item.value }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 服务 -->
|
||||
|
|
|
|||
|
|
@ -227,7 +227,6 @@ function removeDataSpecs(val: any) {
|
|||
>
|
||||
<ElInput
|
||||
v-model="formData.identifier"
|
||||
:disabled="formData.id != null"
|
||||
placeholder="请输入标识符"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ onMounted(async () => {
|
|||
v-for="item in filteredList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
:value="item.id!"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ item.name }}</span>
|
||||
|
|
|
|||
|
|
@ -133,10 +133,70 @@ export const MesAutoCodeRuleCode = {
|
|||
MD_VENDOR_CODE: 'MD_VENDOR_CODE',
|
||||
MD_WORKSTATION_CODE: 'MD_WORKSTATION_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_CODE: 'TM_TOOL_CODE',
|
||||
} 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 编码规则分段类型枚举 */
|
||||
export const MesAutoCodePartTypeEnum = {
|
||||
INPUT: 1,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
getFirstNonNullOrUndefined,
|
||||
isBoolean,
|
||||
isEmpty,
|
||||
isEmptyVal,
|
||||
isHttpUrl,
|
||||
isObject,
|
||||
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', () => {
|
||||
it('should return true for the window object', () => {
|
||||
expect(isWindow(window)).toBe(true);
|
||||
|
|
|
|||
|
|
@ -53,6 +53,21 @@ function isEmpty<T = unknown>(value?: T): boolean {
|
|||
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。
|
||||
*
|
||||
|
|
@ -152,6 +167,7 @@ export {
|
|||
getFirstNonNullOrUndefined,
|
||||
isBoolean,
|
||||
isEmpty,
|
||||
isEmptyVal,
|
||||
isFunction,
|
||||
isHttpUrl,
|
||||
isMacOs,
|
||||
|
|
|
|||
|
|
@ -200,6 +200,19 @@ const MES_DICT = {
|
|||
MES_DV_REPAIR_RESULT: 'mes_dv_repair_result', // MES 维修结果
|
||||
MES_DV_CHECK_RECORD_STATUS: 'mes_dv_check_record_status', // 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_FORMAT: 'mes_wm_barcode_format', // MES 条码格式
|
||||
MES_WM_PRODUCT_SALES_STATUS: 'mes_wm_product_sales_status', // MES 销售出库单状态
|
||||
|
|
|
|||
Loading…
Reference in New Issue