Pre Merge pull request !359 from XuZhiqiang/feat-antdv-next

pull/359/MERGE
XuZhiqiang 2026-06-06 16:52:13 +00:00 committed by Gitee
commit e12b19a90e
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
99 changed files with 1381 additions and 1088 deletions

View File

@ -3,7 +3,7 @@ import type { Dayjs } from 'dayjs';
import { onMounted, ref } from 'vue';
import { DatePicker, Radio, RadioGroup } from 'antdv-next';
import { DateRangePicker, Radio, RadioGroup } from 'antdv-next';
import dayjs from 'dayjs';
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
@ -88,7 +88,7 @@ onMounted(() => {
{{ option.label }}
</Radio>
</RadioGroup>
<DatePicker.RangePicker
<DateRangePicker
v-model:value="times"
:format="rangePickerProps.format"
:value-format="rangePickerProps.valueFormat"

View File

@ -11,7 +11,15 @@ import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { isBoolean, isFunction } from '@vben/utils';
import { Button, Dropdown, Menu, Popconfirm, Space, Tooltip } from 'antdv-next';
import {
Button,
Dropdown,
Menu,
MenuItem,
Popconfirm,
Space,
Tooltip,
} from 'antdv-next';
const props = defineProps({
actions: {
@ -188,7 +196,7 @@ watch(
</slot>
<template #popupRender>
<Menu>
<Menu.Item
<MenuItem
v-for="(action, index) in getDropdownList"
:key="index"
:disabled="action.disabled"
@ -225,7 +233,7 @@ watch(
{{ action.label }}
</div>
</template>
</Menu.Item>
</MenuItem>
</Menu>
</template>
</Dropdown>

View File

@ -78,7 +78,7 @@ function handleClick(doc: any) {
</div>
</div>
</div>
<Tooltip placement="topLeft" trigger="click">
<Tooltip placement="topLeft" :trigger="['click']">
<div ref="documentRef"></div>
<template #title>
<div class="mb-3 text-base font-bold">{{ document?.title }}</div>

View File

@ -7,7 +7,7 @@ import { ref } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { Avatar, Button, Card, Dropdown, Menu } from 'antdv-next';
import { Avatar, Button, Card, Dropdown, Menu, MenuItem } from 'antdv-next';
const props = defineProps({
loading: {
@ -103,18 +103,18 @@ async function handleTabsScroll() {
</Button>
<template #popupRender>
<Menu>
<Menu.Item @click="handleMoreClick(['delete', role])">
<MenuItem @click="handleMoreClick(['delete', role])">
<div class="flex items-center">
<IconifyIcon icon="lucide:trash" color="red" />
<span class="ml-2 text-red-500">删除</span>
</div>
</Menu.Item>
<Menu.Item @click="handleMoreClick(['edit', role])">
</MenuItem>
<MenuItem @click="handleMoreClick(['edit', role])">
<div class="flex items-center">
<IconifyIcon icon="lucide:edit" color="#787878" />
<span class="ml-2 text-primary">编辑</span>
</div>
</Menu.Item>
</MenuItem>
</Menu>
</template>
</Dropdown>

View File

@ -12,6 +12,7 @@ import {
Form,
InputNumber,
Menu,
MenuItem,
message,
Tooltip,
} from 'antdv-next';
@ -207,7 +208,7 @@ onMounted(async () => {
<div class="mb-2.5">
<Dropdown
v-if="modelData.list && modelData.list.length > 0"
trigger="click"
:trigger="['click']"
>
<div class="flex cursor-pointer items-center">
<IconifyIcon icon="lucide:file-text" class="mr-1" />
@ -222,7 +223,7 @@ onMounted(async () => {
</div>
<template #popupRender>
<Menu>
<Menu.Item
<MenuItem
v-for="(file, index) in modelData.list"
:key="index"
@click="selectFile(index)"
@ -231,7 +232,7 @@ onMounted(async () => {
<span v-if="file.segments" class="ml-1 text-sm text-gray-500">
({{ file.segments.length }} 个分片)
</span>
</Menu.Item>
</MenuItem>
</Menu>
</template>
</Dropdown>

View File

@ -4,7 +4,7 @@ import { ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Form, Input, Select } from 'antdv-next';
import { Form, Input, Select, TextArea } from 'antdv-next';
type Rule = any;
const modelData = defineModel<any>(); //
@ -64,7 +64,7 @@ defineExpose({ validate });
</Select>
</FormItem>
<FormItem label="流程描述" name="description" class="mb-5">
<Input.TextArea v-model:value="modelData.description" allow-clear />
<TextArea v-model:value="modelData.description" allow-clear />
</FormItem>
</Form>
</template>

View File

@ -1,11 +1,21 @@
<script lang="ts" setup>
import { h, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import type { TableColumnType } from 'antdv-next';
import {
computed,
h,
nextTick,
onBeforeUnmount,
onMounted,
ref,
watch,
} from 'vue';
import { BpmProcessInstanceStatus, DICT_TYPE } from '@vben/constants';
import { UndoOutlined, ZoomInOutlined, ZoomOutOutlined } from '@vben/icons';
import { formatDate, formatPast2 } from '@vben/utils';
import { Button, Modal, Row, Table } from 'antdv-next';
import { Button, Modal, Row, SpaceCompact, Table } from 'antdv-next';
import BpmnViewer from 'bpmn-js/lib/Viewer';
import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas';
@ -13,8 +23,6 @@ import { DictTag } from '#/components/dict-tag';
import '../theme/index.scss';
const ButtonGroup = Button.Group;
const props = defineProps({
xml: {
type: String,
@ -40,6 +48,80 @@ const dialogVisible = ref(false); // 弹窗可见性
const dialogTitle = ref<string | undefined>(undefined); //
const selectActivityType = ref<string | undefined>(undefined); // Task
const selectTasks = ref<any[]>([]); //
const approvalColumns = computed<TableColumnType[]>(() => {
const userColumn: TableColumnType =
selectActivityType.value === 'bpmn:UserTask'
? {
align: 'center',
key: 'approver',
title: '审批人',
width: 100,
}
: {
align: 'center',
dataIndex: ['assigneeUser', 'nickname'],
key: 'starter',
title: '发起人',
width: 100,
};
return [
{
align: 'center',
key: 'index',
title: '序号',
width: 50,
},
userColumn,
{
align: 'center',
key: 'dept',
title: '部门',
width: 100,
},
{
align: 'center',
dataIndex: 'createTime',
key: 'createTime',
render: (val) => formatDate(val),
title: '开始时间',
width: 140,
},
{
align: 'center',
dataIndex: 'endTime',
key: 'endTime',
render: (val) => formatDate(val),
title: '结束时间',
width: 140,
},
{
align: 'center',
dataIndex: 'status',
key: 'status',
title: '审批状态',
width: 90,
},
...(selectActivityType.value === 'bpmn:UserTask'
? [
{
align: 'center',
dataIndex: 'reason',
key: 'reason',
title: '审批建议',
width: 120,
} as TableColumnType,
]
: []),
{
align: 'center',
dataIndex: 'durationInMillis',
key: 'durationInMillis',
title: '耗时',
width: 100,
},
];
});
/** Zoom恢复 */
const processReZoom = () => {
@ -360,78 +442,32 @@ onBeforeUnmount(() => {
:width="1000"
>
<Row>
<Table :data-source="selectTasks" size="small" :bordered="true">
<Table.Column title="序号" align="center" width="50">
<template #default="{ index }">
<Table
:columns="approvalColumns"
:data-source="selectTasks"
size="small"
:bordered="true"
>
<template #bodyCell="{ column, index, record }">
<template v-if="column.key === 'index'">
{{ index + 1 }}
</template>
</Table.Column>
<Table.Column
title="审批人"
width="100"
align="center"
v-if="selectActivityType === 'bpmn:UserTask'"
>
<template #default="{ record }">
<template v-else-if="column.key === 'approver'">
{{ record.assigneeUser?.nickname || record.ownerUser?.nickname }}
</template>
</Table.Column>
<Table.Column
title="发起人"
data-index="assigneeUser.nickname"
width="100"
align="center"
v-else
/>
<Table.Column title="部门" width="100" align="center">
<template #default="{ record }">
<template v-else-if="column.key === 'dept'">
{{ record.assigneeUser?.deptName || record.ownerUser?.deptName }}
</template>
</Table.Column>
<Table.Column
:custom-render="({ text }) => formatDate(text)"
align="center"
title="开始时间"
data-index="createTime"
width="140"
/>
<Table.Column
:custom-render="({ text }) => formatDate(text)"
align="center"
title="结束时间"
data-index="endTime"
width="140"
/>
<Table.Column
align="center"
title="审批状态"
data-index="status"
width="90"
>
<template #default="{ record }">
<template v-else-if="column.key === 'status'">
<DictTag
:type="DICT_TYPE.BPM_TASK_STATUS"
:value="record.status"
/>
</template>
</Table.Column>
<Table.Column
align="center"
title="审批建议"
data-index="reason"
width="120"
v-if="selectActivityType === 'bpmn:UserTask'"
/>
<Table.Column
align="center"
title="耗时"
data-index="durationInMillis"
width="100"
>
<template #default="{ record }">
<template v-else-if="column.key === 'durationInMillis'">
{{ formatPast2(record.durationInMillis) }}
</template>
</Table.Column>
</template>
</Table>
</Row>
</Modal>
@ -439,7 +475,7 @@ onBeforeUnmount(() => {
<!-- Zoom放大缩小 -->
<div style="position: absolute; top: 0; left: 0; width: 100%">
<Row justify="end">
<ButtonGroup key="scale-control">
<SpaceCompact key="scale-control">
<Button
:disabled="defaultZoom <= 0.3"
:icon="h(ZoomOutOutlined)"
@ -454,7 +490,7 @@ onBeforeUnmount(() => {
@click="processZoomIn()"
/>
<Button :icon="h(UndoOutlined)" @click="processReZoom()" />
</ButtonGroup>
</SpaceCompact>
</Row>
</div>
</div>

View File

@ -4,6 +4,7 @@ import { ref, watch } from 'vue';
import {
Button,
Checkbox,
CheckboxGroup,
DatePicker,
Input,
InputNumber,
@ -309,7 +310,7 @@ watch(
</RadioGroup>
</div>
<div v-if="cronMode[f.key] === 'appoint'">
<Checkbox.Group
<CheckboxGroup
v-model:value="cronAppoint[f.key]"
:key="`group-${f.key}`"
>
@ -320,7 +321,7 @@ watch(
>
{{ pad(n - 1) }}
</Checkbox>
</Checkbox.Group>
</CheckboxGroup>
</div>
</TabPane>
</Tabs>

View File

@ -248,7 +248,7 @@ function addNode(type: number) {
<template>
<div class="node-handler-wrapper">
<div class="node-handler">
<Popover trigger="hover" placement="right" width="auto" v-if="!readonly">
<Popover :trigger="['hover']" placement="right" width="auto" v-if="!readonly">
<template #content>
<div class="handler-item-wrapper">
<div

View File

@ -21,6 +21,7 @@ import {
Input,
Radio,
Select,
TextArea,
Tooltip,
} from 'antdv-next';
@ -295,7 +296,7 @@ defineExpose({ validate });
<ImageUpload v-model:value="modelData.icon" />
</FormItem>
<FormItem label="流程描述" name="description">
<Input.TextArea v-model:value="modelData.description" allow-clear />
<TextArea v-model:value="modelData.description" allow-clear />
</FormItem>
<FormItem label="流程类型" name="type">
<RadioGroup v-model:value="modelData.type">

View File

@ -8,7 +8,15 @@ import { IconifyIcon } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import { useSortable } from '@vueuse/integrations/useSortable';
import { Button, Card, Dropdown, Input, Menu, message } from 'antdv-next';
import {
Button,
Card,
Dropdown,
Input,
Menu,
MenuItem,
message,
} from 'antdv-next';
import {
getCategorySimpleList,
@ -171,18 +179,18 @@ async function handleCategorySortSubmit() {
</Button>
<template #popupRender>
<Menu @click="(e) => handleCommand(e.key as string)">
<Menu.Item key="handleCategoryAdd">
<MenuItem key="handleCategoryAdd">
<div class="flex items-center gap-1">
<IconifyIcon icon="lucide:plus" />
新建分类
</div>
</Menu.Item>
<Menu.Item key="handleCategorySort">
</MenuItem>
<MenuItem key="handleCategorySort">
<div class="flex items-center gap-1">
<IconifyIcon icon="lucide:align-start-vertical" />
分类排序
</div>
</Menu.Item>
</MenuItem>
</Menu>
</template>
</Dropdown>

View File

@ -20,6 +20,7 @@ import {
Collapse,
Dropdown,
Menu,
MenuItem,
message,
Tag,
Tooltip,
@ -524,8 +525,8 @@ function handleRenameSuccess() {
</Button>
<template #popupRender>
<Menu @click="(e) => handleCommand(e.key as string)">
<Menu.Item key="renameCategory"> 重命名 </Menu.Item>
<Menu.Item key="deleteCategory"> 删除分类 </Menu.Item>
<MenuItem key="renameCategory"> 重命名 </MenuItem>
<MenuItem key="deleteCategory"> 删除分类 </MenuItem>
</Menu>
</template>
</Dropdown>
@ -687,16 +688,16 @@ function handleRenameSuccess() {
<Menu
@click="(e) => handleModelCommand(e.key as string, row)"
>
<Menu.Item key="handleCopy"> 复制 </Menu.Item>
<Menu.Item key="handleDefinitionList"> 历史 </Menu.Item>
<MenuItem key="handleCopy"> 复制 </MenuItem>
<MenuItem key="handleDefinitionList"> 历史 </MenuItem>
<Menu.Item
<MenuItem
key="handleReport"
:disabled="!isManagerUser(row)"
>
报表
</Menu.Item>
<Menu.Item
</MenuItem>
<MenuItem
key="handleChangeState"
v-if="row.processDefinition"
:disabled="!isManagerUser(row)"
@ -706,21 +707,21 @@ function handleRenameSuccess() {
? '停用'
: '启用'
}}
</Menu.Item>
<Menu.Item
</MenuItem>
<MenuItem
danger
key="handleClean"
:disabled="!isManagerUser(row)"
>
清理
</Menu.Item>
<Menu.Item
</MenuItem>
<MenuItem
danger
key="handleDelete"
:disabled="!isManagerUser(row) && !hasPermiDelete"
>
删除
</Menu.Item>
</MenuItem>
</Menu>
</template>
</Dropdown>

View File

@ -767,7 +767,7 @@ defineExpose({ loadTodoTask });
v-model:open="popOverVisible.approve"
placement="top"
:styles="{ root: { minWidth: '400px', zIndex: 300 } }"
trigger="click"
:trigger="['click']"
@open-change="handlePopoverVisible"
v-if="
runningTask &&
@ -871,7 +871,7 @@ defineExpose({ loadTodoTask });
v-model:open="popOverVisible.reject"
placement="top"
:styles="{ root: { minWidth: '400px' } }"
trigger="click"
:trigger="['click']"
v-if="
runningTask &&
isHandleTaskStatus() &&
@ -928,7 +928,7 @@ defineExpose({ loadTodoTask });
v-model:open="popOverVisible.copy"
placement="top"
:styles="{ root: { width: '400px' } }"
trigger="click"
:trigger="['click']"
v-if="
runningTask &&
isHandleTaskStatus() &&
@ -1000,7 +1000,7 @@ defineExpose({ loadTodoTask });
v-model:open="popOverVisible.transfer"
placement="top"
:styles="{ root: { width: '400px' } }"
trigger="click"
:trigger="['click']"
v-if="
runningTask &&
isHandleTaskStatus() &&
@ -1073,7 +1073,7 @@ defineExpose({ loadTodoTask });
v-model:open="popOverVisible.delegate"
placement="top"
:styles="{ root: { width: '400px' } }"
trigger="click"
:trigger="['click']"
v-if="
runningTask &&
isHandleTaskStatus() &&
@ -1146,7 +1146,7 @@ defineExpose({ loadTodoTask });
v-model:open="popOverVisible.addSign"
placement="top"
:styles="{ root: { width: '400px' } }"
trigger="click"
:trigger="['click']"
v-if="
runningTask &&
isHandleTaskStatus() &&
@ -1231,7 +1231,7 @@ defineExpose({ loadTodoTask });
v-model:open="popOverVisible.deleteSign"
placement="top"
:styles="{ root: { width: '400px' } }"
trigger="click"
:trigger="['click']"
v-if="runningTask?.children.length > 0"
>
<Button type="dashed" @click="openPopover('deleteSign')">
@ -1297,7 +1297,7 @@ defineExpose({ loadTodoTask });
v-model:open="popOverVisible.return"
placement="top"
:styles="{ root: { width: '400px' } }"
trigger="click"
:trigger="['click']"
v-if="
runningTask &&
isHandleTaskStatus() &&
@ -1370,7 +1370,7 @@ defineExpose({ loadTodoTask });
v-model:open="popOverVisible.cancel"
placement="top"
:width="500"
trigger="click"
:trigger="['click']"
v-if="
userId === processInstance?.startUser?.id &&
!isEndProcessStatus(processInstance?.status)

View File

@ -32,6 +32,12 @@ const clueId = ref(0); // 线索编号
const clue = ref<CrmClueApi.Clue>({} as CrmClueApi.Clue); // 线
const logList = ref<SystemOperateLogApi.OperateLog[]>([]); //
const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // Ref
const clueTabItems = [
{ key: '1', label: '跟进记录', forceRender: true },
{ key: '2', label: '基本信息', forceRender: true },
{ key: '3', label: '团队成员', forceRender: true },
{ key: '4', label: '操作日志', forceRender: true },
];
const [Descriptions] = useDescription({
bordered: false,
@ -143,25 +149,24 @@ onMounted(() => {
<Descriptions :data="clue" />
</Card>
<Card class="mt-4 min-h-[60%]">
<Tabs :tab-bar-gutter="16">
<Tabs.TabPane tab="跟进记录" key="1" :force-render="true">
<FollowUp :biz-id="clueId" :biz-type="BizTypeEnum.CRM_CLUE" />
</Tabs.TabPane>
<Tabs.TabPane tab="基本信息" key="2" :force-render="true">
<Info :clue="clue" />
</Tabs.TabPane>
<Tabs.TabPane tab="团队成员" key="3" :force-render="true">
<Tabs :items="clueTabItems" :tab-bar-gutter="16">
<template #contentRender="{ item }">
<FollowUp
v-if="item.key === '1'"
:biz-id="clueId"
:biz-type="BizTypeEnum.CRM_CLUE"
/>
<Info v-else-if="item.key === '2'" :clue="clue" />
<PermissionList
v-else-if="item.key === '3'"
ref="permissionListRef"
:biz-id="clueId"
:biz-type="BizTypeEnum.CRM_CLUE"
:show-action="true"
@quit-team="handleBack"
/>
</Tabs.TabPane>
<Tabs.TabPane tab="操作日志" key="4" :force-render="true">
<OperateLog :log-list="logList" />
</Tabs.TabPane>
<OperateLog v-else-if="item.key === '4'" :log-list="logList" />
</template>
</Tabs>
</Card>
</Page>

View File

@ -45,6 +45,16 @@ const customerId = ref(0); // 客户编号
const customer = ref<CrmCustomerApi.Customer>({} as CrmCustomerApi.Customer); //
const logList = ref<SystemOperateLogApi.OperateLog[]>([]); //
const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // Ref
const customerTabItems = [
{ key: '1', label: '跟进记录', forceRender: true },
{ key: '2', label: '基本信息', forceRender: true },
{ key: '3', label: '联系人', forceRender: true },
{ key: '4', label: '团队成员', forceRender: true },
{ key: '5', label: '商机', forceRender: true },
{ key: '6', label: '合同', forceRender: true },
{ key: '7', label: '回款', forceRender: true },
{ key: '8', label: '操作日志', forceRender: true },
];
const [Descriptions] = useDescription({
bordered: false,
@ -241,49 +251,45 @@ onMounted(() => {
<Descriptions :data="customer" />
</Card>
<Card class="mt-4 min-h-[60%]">
<Tabs>
<Tabs.TabPane tab="跟进记录" key="1" :force-render="true">
<FollowUp :biz-id="customerId" :biz-type="BizTypeEnum.CRM_CUSTOMER" />
</Tabs.TabPane>
<Tabs.TabPane tab="基本信息" key="2" :force-render="true">
<Info :customer="customer" />
</Tabs.TabPane>
<Tabs.TabPane tab="联系人" key="3" :force-render="true">
<Tabs :items="customerTabItems">
<template #contentRender="{ item }">
<FollowUp
v-if="item.key === '1'"
:biz-id="customerId"
:biz-type="BizTypeEnum.CRM_CUSTOMER"
/>
<Info v-else-if="item.key === '2'" :customer="customer" />
<ContactDetailsList
v-else-if="item.key === '3'"
:biz-id="customerId"
:biz-type="BizTypeEnum.CRM_CUSTOMER"
:customer-id="customerId"
/>
</Tabs.TabPane>
<Tabs.TabPane tab="团队成员" key="4" :force-render="true">
<PermissionList
v-else-if="item.key === '4'"
ref="permissionListRef"
:biz-id="customerId"
:biz-type="BizTypeEnum.CRM_CUSTOMER"
:show-action="true"
@quit-team="handleBack"
/>
</Tabs.TabPane>
<Tabs.TabPane tab="商机" key="5" :force-render="true">
<BusinessDetailsList
v-else-if="item.key === '5'"
:biz-id="customerId"
:biz-type="BizTypeEnum.CRM_CUSTOMER"
:customer-id="customerId"
/>
</Tabs.TabPane>
<Tabs.TabPane tab="合同" key="6" :force-render="true">
<ContractDetailsList
v-else-if="item.key === '6'"
:biz-id="customerId"
:biz-type="BizTypeEnum.CRM_CUSTOMER"
/>
</Tabs.TabPane>
<Tabs.TabPane tab="回款" key="7" :force-render="true">
<ReceivablePlanDetailsList :customer-id="customerId" />
<ReceivableDetailsList :customer-id="customerId" />
</Tabs.TabPane>
<Tabs.TabPane tab="操作日志" key="8" :force-render="true">
<OperateLog :log-list="logList" />
</Tabs.TabPane>
<template v-else-if="item.key === '7'">
<ReceivablePlanDetailsList :customer-id="customerId" />
<ReceivableDetailsList :customer-id="customerId" />
</template>
<OperateLog v-else-if="item.key === '8'" :log-list="logList" />
</template>
</Tabs>
</Card>
</Page>

View File

@ -8,7 +8,7 @@ import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { formatDateTime } from '@vben/utils';
import { Timeline } from 'antdv-next';
import { Timeline, TimelineItem } from 'antdv-next';
import { CronTab } from '#/components/cron-tab';
import { DictTag } from '#/components/dict-tag';
@ -239,7 +239,7 @@ export function useDetailSchema(): DescriptionItemSchema[] {
}
return h(Timeline, {}, () =>
val?.map((time: Date) =>
h(Timeline.Item, {}, () => formatDateTime(time)),
h(TimelineItem, {}, () => formatDateTime(time)),
),
);
},

View File

@ -20,6 +20,7 @@ import {
message,
Select,
Tag,
TextArea,
} from 'antdv-next';
import { getSimpleUserList } from '#/api/system/user';
@ -249,7 +250,7 @@ onMounted(async () => {
</SelectOption>
</Select>
<Input.TextArea
<TextArea
v-model:value="sendText"
:auto-size="{ minRows: 3, maxRows: 6 }"
:disabled="!getIsOpen"

View File

@ -7,7 +7,7 @@ import { h, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Button, Input, message, Modal, Popover } from 'antdv-next';
import { Button, message, Modal, Popover, TextArea } from 'antdv-next';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAlertRecordPage, processAlertRecord } from '#/api/iot/alert/record';
@ -37,7 +37,7 @@ function handleProcess(row: AlertRecordApi.AlertRecord) {
content: () =>
h('div', { class: 'space-y-2' }, [
h('p', '请输入处理原因:'),
h(Input.TextArea, {
h(TextArea, {
value: processRemark.value,
'onUpdate:value': (val: string) => (processRemark.value = val),
rows: 3,
@ -98,7 +98,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
<Popover
v-if="row.deviceMessage"
placement="topLeft"
trigger="hover"
:trigger="['hover']"
:overlay-style="{ maxWidth: '600px' }"
>
<template #content>

View File

@ -3,7 +3,7 @@ import type { IotDeviceApi } from '#/api/iot/device/device';
import type { IotProductApi } from '#/api/iot/product/product';
import type { ThingModelApi } from '#/api/iot/thingmodel';
import { onMounted, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
@ -33,6 +33,22 @@ const product = ref<IotProductApi.Product>({} as IotProductApi.Product);
const device = ref<IotDeviceApi.Device>({} as IotDeviceApi.Device);
const activeTab = ref('info');
const thingModelList = ref<ThingModelApi.ThingModel[]>([]);
const deviceTabItems = computed(() => [
{ key: 'info', label: '设备信息' },
{ key: 'model', label: '物模型数据' },
...(product.value.deviceType === DeviceTypeEnum.GATEWAY
? [{ key: 'subDevice', label: '子设备管理' }]
: []),
{ key: 'log', label: '设备消息' },
{ key: 'simulator', label: '模拟设备' },
{ key: 'config', label: '设备配置' },
...([
ProtocolTypeEnum.MODBUS_TCP_CLIENT,
ProtocolTypeEnum.MODBUS_TCP_SERVER,
].includes(product.value.protocolType as ProtocolTypeEnum)
? [{ key: 'modbus', label: 'Modbus 配置' }]
: []),
]);
/** 获取设备详情 */
async function getDeviceData(deviceId: number) {
@ -94,69 +110,52 @@ onMounted(async () => {
@refresh="() => getDeviceData(id)"
/>
<Tabs v-model:active-key="activeTab" class="mt-4">
<Tabs.TabPane key="info" tab="设备信息">
<Tabs v-model:active-key="activeTab" :items="deviceTabItems" class="mt-4">
<template #contentRender="{ item }">
<DeviceDetailsInfo
v-if="activeTab === 'info' && device.id"
v-if="item.key === 'info' && activeTab === 'info' && device.id"
:device="device"
:product="product"
/>
</Tabs.TabPane>
<Tabs.TabPane key="model" tab="物模型数据">
<DeviceDetailsThingModel
v-if="activeTab === 'model' && device.id"
v-else-if="item.key === 'model' && activeTab === 'model' && device.id"
:device-id="device.id"
:thing-model-list="thingModelList"
/>
</Tabs.TabPane>
<Tabs.TabPane
v-if="product.deviceType === DeviceTypeEnum.GATEWAY"
key="subDevice"
tab="子设备管理"
>
<DeviceDetailsSubDevice
v-if="activeTab === 'subDevice' && device.id"
v-else-if="
item.key === 'subDevice' && activeTab === 'subDevice' && device.id
"
:device-id="device.id"
/>
</Tabs.TabPane>
<Tabs.TabPane key="log" tab="设备消息">
<DeviceDetailsMessage
v-if="activeTab === 'log' && device.id"
v-else-if="item.key === 'log' && activeTab === 'log' && device.id"
:device-id="device.id"
/>
</Tabs.TabPane>
<Tabs.TabPane key="simulator" tab="模拟设备">
<DeviceDetailsSimulator
v-if="activeTab === 'simulator' && device.id"
v-else-if="
item.key === 'simulator' && activeTab === 'simulator' && device.id
"
:device="device"
:product="product"
:thing-model-list="thingModelList"
/>
</Tabs.TabPane>
<Tabs.TabPane key="config" tab="设备配置">
<DeviceDetailConfig
v-if="activeTab === 'config' && device.id"
v-else-if="
item.key === 'config' && activeTab === 'config' && device.id
"
:device="device"
@success="() => getDeviceData(id)"
/>
</Tabs.TabPane>
<Tabs.TabPane
v-if="
[
ProtocolTypeEnum.MODBUS_TCP_CLIENT,
ProtocolTypeEnum.MODBUS_TCP_SERVER,
].includes(product.protocolType as ProtocolTypeEnum)
"
key="modbus"
tab="Modbus 配置"
>
<DeviceModbusConfig
v-if="activeTab === 'modbus' && device.id"
v-else-if="
item.key === 'modbus' && activeTab === 'modbus' && device.id
"
:device="device"
:product="product"
:thing-model-list="thingModelList"
/>
</Tabs.TabPane>
</template>
</Tabs>
</Page>
</template>

View File

@ -6,7 +6,7 @@ import { useRouter } from 'vue-router';
import { useVbenModal } from '@vben/common-ui';
import { Button, Card, Descriptions, message } from 'antdv-next';
import { Button, Card, Descriptions, DescriptionsItem, message } from 'antdv-next';
import DeviceForm from '../../modules/form.vue';
@ -75,15 +75,15 @@ function openEditForm(row: IotDeviceApi.Device) {
<Card class="mt-4">
<Descriptions :column="2">
<Descriptions.Item label="产品">
<DescriptionsItem label="产品">
<a
class="cursor-pointer text-blue-600"
@click="goToProductDetail(product.id)"
>
{{ product.name }}
</a>
</Descriptions.Item>
<Descriptions.Item label="ProductKey">
</DescriptionsItem>
<DescriptionsItem label="ProductKey">
{{ product.productKey }}
<Button
class="ml-2"
@ -92,7 +92,7 @@ function openEditForm(row: IotDeviceApi.Device) {
>
复制
</Button>
</Descriptions.Item>
</DescriptionsItem>
</Descriptions>
</Card>
</div>

View File

@ -12,10 +12,13 @@ import {
Button,
Card,
Descriptions,
DescriptionsItem,
Form,
FormItem,
Input,
message,
Modal,
SpaceCompact,
} from 'antdv-next';
import { getDeviceAuthInfo } from '#/api/iot/device/device';
@ -38,7 +41,7 @@ const mapDialogRef = ref<InstanceType<typeof MapDialog>>();
/** 是否有位置信息(合法经纬度 0 不应视为空) */
const hasLocation = computed(() => {
return props.device.longitude != null && props.device.latitude != null;
return props.device.longitude !== null && props.device.latitude !== null;
});
/** 打开地图弹窗 */
@ -77,40 +80,40 @@ function handleAuthInfoDialogClose() {
<div>
<Card title="设备信息">
<Descriptions :column="3" bordered size="small">
<Descriptions.Item label="产品名称">
<DescriptionsItem label="产品名称">
{{ product.name }}
</Descriptions.Item>
<Descriptions.Item label="ProductKey">
</DescriptionsItem>
<DescriptionsItem label="ProductKey">
{{ product.productKey }}
</Descriptions.Item>
<Descriptions.Item label="设备类型">
</DescriptionsItem>
<DescriptionsItem label="设备类型">
<DictTag
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
:value="product.deviceType"
/>
</Descriptions.Item>
<Descriptions.Item label="DeviceName">
</DescriptionsItem>
<DescriptionsItem label="DeviceName">
{{ device.deviceName }}
</Descriptions.Item>
<Descriptions.Item label="备注名称">
</DescriptionsItem>
<DescriptionsItem label="备注名称">
{{ device.nickname || '--' }}
</Descriptions.Item>
<Descriptions.Item label="当前状态">
</DescriptionsItem>
<DescriptionsItem label="当前状态">
<DictTag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="device.state" />
</Descriptions.Item>
<Descriptions.Item label="创建时间">
</DescriptionsItem>
<DescriptionsItem label="创建时间">
{{ formatDateTime(device.createTime) }}
</Descriptions.Item>
<Descriptions.Item label="激活时间">
</DescriptionsItem>
<DescriptionsItem label="激活时间">
{{ formatDateTime(device.activeTime) }}
</Descriptions.Item>
<Descriptions.Item label="最后上线时间">
</DescriptionsItem>
<DescriptionsItem label="最后上线时间">
{{ formatDateTime(device.onlineTime) }}
</Descriptions.Item>
<Descriptions.Item label="最后离线时间">
</DescriptionsItem>
<DescriptionsItem label="最后离线时间">
{{ formatDateTime(device.offlineTime) }}
</Descriptions.Item>
<Descriptions.Item label="设备位置">
</DescriptionsItem>
<DescriptionsItem label="设备位置">
<template v-if="hasLocation">
<span class="mr-2">
{{ device.longitude }}, {{ device.latitude }}
@ -121,12 +124,12 @@ function handleAuthInfoDialogClose() {
</Button>
</template>
<span v-else class="text-gray-400">暂无位置信息</span>
</Descriptions.Item>
<Descriptions.Item label="MQTT 连接参数">
</DescriptionsItem>
<DescriptionsItem label="MQTT 连接参数">
<Button size="small" type="link" @click="handleAuthInfoDialogOpen">
查看
</Button>
</Descriptions.Item>
</DescriptionsItem>
</Descriptions>
</Card>
@ -138,8 +141,8 @@ function handleAuthInfoDialogClose() {
width="640px"
>
<Form :label-col="{ span: 6 }">
<Form.Item label="clientId">
<Input.Group compact>
<FormItem label="clientId">
<SpaceCompact>
<Input
v-model:value="authInfo.clientId"
readonly
@ -148,10 +151,10 @@ function handleAuthInfoDialogClose() {
<Button type="primary" @click="copyToClipboard(authInfo.clientId)">
<IconifyIcon icon="lucide:copy" />
</Button>
</Input.Group>
</Form.Item>
<Form.Item label="username">
<Input.Group compact>
</SpaceCompact>
</FormItem>
<FormItem label="username">
<SpaceCompact>
<Input
v-model:value="authInfo.username"
readonly
@ -160,10 +163,10 @@ function handleAuthInfoDialogClose() {
<Button type="primary" @click="copyToClipboard(authInfo.username)">
<IconifyIcon icon="lucide:copy" />
</Button>
</Input.Group>
</Form.Item>
<Form.Item label="password">
<Input.Group compact>
</SpaceCompact>
</FormItem>
<FormItem label="password">
<SpaceCompact>
<Input
v-model:value="authInfo.password"
:type="authPasswordVisible ? 'text' : 'password'"
@ -181,8 +184,8 @@ function handleAuthInfoDialogClose() {
<Button type="primary" @click="copyToClipboard(authInfo.password)">
<IconifyIcon icon="lucide:copy" />
</Button>
</Input.Group>
</Form.Item>
</SpaceCompact>
</FormItem>
</Form>
<div class="mt-4 text-right">
<Button @click="handleAuthInfoDialogClose"></Button>

View File

@ -15,7 +15,7 @@ import { DICT_TYPE, IotDeviceMessageMethodEnum } from '@vben/constants';
import { IconifyIcon } from '@vben/icons';
import { formatDateTime } from '@vben/utils';
import { Button, Select, Space, Switch, Tag } from 'antdv-next';
import { Button, Select, SelectOption, Space, Switch, Tag } from 'antdv-next';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getDeviceMessagePage } from '#/api/iot/device/device';
@ -196,14 +196,14 @@ defineExpose({
placeholder="所有方法"
style="width: 160px"
>
<Select.Option
<SelectOption
v-for="item in methodOptions"
:key="item.value"
:label="item.label"
:value="item.value"
>
{{ item.label }}
</Select.Option>
</SelectOption>
</Select>
<Select
v-model:value="queryParams.upstream"
@ -211,8 +211,8 @@ defineExpose({
placeholder="上行/下行"
style="width: 160px"
>
<Select.Option label="上行" value="true">上行</Select.Option>
<Select.Option label="下行" value="false">下行</Select.Option>
<SelectOption label="上行" value="true">上行</SelectOption>
<SelectOption label="下行" value="false">下行</SelectOption>
</Select>
<Space>
<Button type="primary" @click="handleQuery">

View File

@ -48,6 +48,34 @@ const upstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_POST.method); // 上
const downstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_SET.method); //
const deviceMessageRef = ref(); //
const deviceMessageRefreshDelay = 2000; // N
const simulatorTabItems = [
{ key: 'upstream', label: '上行指令调试' },
{ key: 'downstream', label: '下行指令调试' },
];
const upstreamTabItems = [
{
key: IotDeviceMessageMethodEnum.PROPERTY_POST.method,
label: '属性上报',
},
{
key: IotDeviceMessageMethodEnum.EVENT_POST.method,
label: '事件上报',
},
{
key: IotDeviceMessageMethodEnum.STATE_UPDATE.method,
label: '状态变更',
},
];
const downstreamTabItems = [
{
key: IotDeviceMessageMethodEnum.PROPERTY_SET.method,
label: '属性设置',
},
{
key: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method,
label: '设备服务调用',
},
];
//
const debugCollapsed = ref(false); //
@ -465,87 +493,91 @@ watch([activeTab, upstreamTab, downstreamTab], () => {
</div>
</template>
<div v-show="!debugCollapsed">
<Tabs v-model:active-key="activeTab" size="small">
<!-- 上行指令调试 -->
<Tabs.TabPane key="upstream" tab="上行指令调试">
<Tabs
v-model:active-key="activeTab"
:items="simulatorTabItems"
size="small"
>
<template #contentRender="{ item }">
<Tabs
v-if="activeTab === 'upstream'"
v-if="item.key === 'upstream' && activeTab === 'upstream'"
v-model:active-key="upstreamTab"
:items="upstreamTabItems"
size="small"
>
<!-- 属性上报 -->
<Tabs.TabPane
:key="IotDeviceMessageMethodEnum.PROPERTY_POST.method"
tab="属性上报"
>
<template #contentRender="{ item: upstreamItem }">
<ContentWrap>
<Table
:columns="propertyColumns"
:data-source="propertyList"
:pagination="false"
:scroll="{ x: 'max-content', y: 300 }"
align="center"
size="small"
<template
v-if="
upstreamItem.key ===
IotDeviceMessageMethodEnum.PROPERTY_POST.method
"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataType'">
{{ record.property?.dataType ?? '-' }}
<Table
:columns="propertyColumns"
:data-source="propertyList"
:pagination="false"
:scroll="{ x: 'max-content', y: 300 }"
align="center"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataType'">
{{ record.property?.dataType ?? '-' }}
</template>
<template
v-else-if="column.key === 'dataDefinition'"
>
<DataDefinition :data="record" />
</template>
<template v-else-if="column.key === 'value'">
<InputNumber
v-if="isNumberProperty(record)"
:value="getFormValue(record.identifier)"
placeholder="输入值"
size="small"
class="w-full"
@update:value="
setFormValue(record.identifier, $event)
"
/>
<Select
v-else-if="isSelectProperty(record)"
:value="getFormValue(record.identifier)"
:options="getPropertyOptions(record)"
placeholder="请选择值"
size="small"
class="w-full"
@update:value="
setFormValue(record.identifier, $event)
"
/>
<Input
v-else
:value="getFormValue(record.identifier)"
placeholder="输入值"
size="small"
@update:value="
setFormValue(record.identifier, $event)
"
/>
</template>
</template>
<template v-else-if="column.key === 'dataDefinition'">
<DataDefinition :data="record" />
</template>
<template v-else-if="column.key === 'value'">
<InputNumber
v-if="isNumberProperty(record)"
:value="getFormValue(record.identifier)"
placeholder="输入值"
size="small"
class="w-full"
@update:value="
setFormValue(record.identifier, $event)
"
/>
<Select
v-else-if="isSelectProperty(record)"
:value="getFormValue(record.identifier)"
:options="getPropertyOptions(record)"
placeholder="请选择值"
size="small"
class="w-full"
@update:value="
setFormValue(record.identifier, $event)
"
/>
<Input
v-else
:value="getFormValue(record.identifier)"
placeholder="输入值"
size="small"
@update:value="
setFormValue(record.identifier, $event)
"
/>
</template>
</template>
</Table>
<div class="mt-4 flex items-center justify-between">
<span class="text-sm text-gray-600">
设置属性值后点击发送属性上报按钮
</span>
<Button type="primary" @click="handlePropertyPost">
发送属性上报
</Button>
</div>
</ContentWrap>
</Tabs.TabPane>
<!-- 事件上报 -->
<Tabs.TabPane
:key="IotDeviceMessageMethodEnum.EVENT_POST.method"
tab="事件上报"
>
<ContentWrap>
</Table>
<div class="mt-4 flex items-center justify-between">
<span class="text-sm text-gray-600">
设置属性值后点击发送属性上报按钮
</span>
<Button type="primary" @click="handlePropertyPost">
发送属性上报
</Button>
</div>
</template>
<Table
v-else-if="
upstreamItem.key ===
IotDeviceMessageMethodEnum.EVENT_POST.method
"
:columns="eventColumns"
:data-source="eventList"
:pagination="false"
@ -555,7 +587,7 @@ watch([activeTab, upstreamTab, downstreamTab], () => {
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataType'">
{{ record.event?.dataType ?? '-' }}
{{ record.dataType ?? '-' }}
</template>
<template v-else-if="column.key === 'dataDefinition'">
<DataDefinition :data="record" />
@ -563,11 +595,11 @@ watch([activeTab, upstreamTab, downstreamTab], () => {
<template v-else-if="column.key === 'value'">
<TextArea
:rows="3"
:value="getFormValue(record.identifier)"
:value="getFormValue(record.identifier!)"
placeholder="输入事件参数JSON格式"
size="small"
@update:value="
setFormValue(record.identifier, $event)
setFormValue(record.identifier!, $event)
"
/>
</template>
@ -582,16 +614,13 @@ watch([activeTab, upstreamTab, downstreamTab], () => {
</template>
</template>
</Table>
</ContentWrap>
</Tabs.TabPane>
<!-- 状态变更 -->
<Tabs.TabPane
:key="IotDeviceMessageMethodEnum.STATE_UPDATE.method"
tab="状态变更"
>
<ContentWrap>
<div class="flex gap-4">
<div
v-else-if="
upstreamItem.key ===
IotDeviceMessageMethodEnum.STATE_UPDATE.method
"
class="flex gap-4"
>
<Button
type="primary"
@click="handleDeviceState(DeviceStateEnum.ONLINE)"
@ -606,90 +635,89 @@ watch([activeTab, upstreamTab, downstreamTab], () => {
</Button>
</div>
</ContentWrap>
</Tabs.TabPane>
</template>
</Tabs>
</Tabs.TabPane>
<!-- 下行指令调试 -->
<Tabs.TabPane key="downstream" tab="下行指令调试">
<Tabs
v-if="activeTab === 'downstream'"
v-else-if="
item.key === 'downstream' && activeTab === 'downstream'
"
v-model:active-key="downstreamTab"
:items="downstreamTabItems"
size="small"
>
<!-- 属性调试 -->
<Tabs.TabPane
:key="IotDeviceMessageMethodEnum.PROPERTY_SET.method"
tab="属性设置"
>
<template #contentRender="{ item: downstreamItem }">
<ContentWrap>
<Table
:columns="propertyColumns"
:data-source="propertyList"
:pagination="false"
:scroll="{ x: 'max-content', y: 300 }"
align="center"
size="small"
<template
v-if="
downstreamItem.key ===
IotDeviceMessageMethodEnum.PROPERTY_SET.method
"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataType'">
{{ record.property?.dataType ?? '-' }}
<Table
:columns="propertyColumns"
:data-source="propertyList"
:pagination="false"
:scroll="{ x: 'max-content', y: 300 }"
align="center"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataType'">
{{ record.property?.dataType ?? '-' }}
</template>
<template
v-else-if="column.key === 'dataDefinition'"
>
<DataDefinition :data="record" />
</template>
<template v-else-if="column.key === 'value'">
<InputNumber
v-if="isNumberProperty(record)"
:value="getFormValue(record.identifier)"
placeholder="输入值"
size="small"
class="w-full"
@update:value="
setFormValue(record.identifier, $event)
"
/>
<Select
v-else-if="isSelectProperty(record)"
:value="getFormValue(record.identifier)"
:options="getPropertyOptions(record)"
placeholder="请选择值"
size="small"
class="w-full"
@update:value="
setFormValue(record.identifier, $event)
"
/>
<Input
v-else
:value="getFormValue(record.identifier)"
placeholder="输入值"
size="small"
@update:value="
setFormValue(record.identifier, $event)
"
/>
</template>
</template>
<template v-else-if="column.key === 'dataDefinition'">
<DataDefinition :data="record" />
</template>
<template v-else-if="column.key === 'value'">
<InputNumber
v-if="isNumberProperty(record)"
:value="getFormValue(record.identifier)"
placeholder="输入值"
size="small"
class="w-full"
@update:value="
setFormValue(record.identifier, $event)
"
/>
<Select
v-else-if="isSelectProperty(record)"
:value="getFormValue(record.identifier)"
:options="getPropertyOptions(record)"
placeholder="请选择值"
size="small"
class="w-full"
@update:value="
setFormValue(record.identifier, $event)
"
/>
<Input
v-else
:value="getFormValue(record.identifier)"
placeholder="输入值"
size="small"
@update:value="
setFormValue(record.identifier, $event)
"
/>
</template>
</template>
</Table>
<div class="mt-4 flex items-center justify-between">
<span class="text-sm text-gray-600">
设置属性值后点击发送属性设置按钮
</span>
<Button type="primary" @click="handlePropertySet">
发送属性设置
</Button>
</div>
</ContentWrap>
</Tabs.TabPane>
<!-- 服务调用 -->
<Tabs.TabPane
:key="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method"
tab="设备服务调用"
>
<ContentWrap>
</Table>
<div class="mt-4 flex items-center justify-between">
<span class="text-sm text-gray-600">
设置属性值后点击发送属性设置按钮
</span>
<Button type="primary" @click="handlePropertySet">
发送属性设置
</Button>
</div>
</template>
<Table
v-else-if="
downstreamItem.key ===
IotDeviceMessageMethodEnum.SERVICE_INVOKE.method
"
:columns="serviceColumns"
:data-source="serviceList"
:pagination="false"
@ -704,11 +732,11 @@ watch([activeTab, upstreamTab, downstreamTab], () => {
<template v-else-if="column.key === 'value'">
<TextArea
:rows="3"
:value="getFormValue(record.identifier)"
:value="getFormValue(record.identifier!)"
placeholder="输入服务参数JSON格式"
size="small"
@update:value="
setFormValue(record.identifier, $event)
setFormValue(record.identifier!, $event)
"
/>
</template>
@ -724,9 +752,9 @@ watch([activeTab, upstreamTab, downstreamTab], () => {
</template>
</Table>
</ContentWrap>
</Tabs.TabPane>
</template>
</Tabs>
</Tabs.TabPane>
</template>
</Tabs>
</div>
</Card>

View File

@ -14,7 +14,14 @@ import {
import { IconifyIcon } from '@vben/icons';
import { formatDateTime } from '@vben/utils';
import { Button, DatePicker, Select, Space, Tag } from 'antdv-next';
import {
Button,
DateRangePicker,
Select,
SelectOption,
Space,
Tag,
} from 'antdv-next';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
@ -24,8 +31,6 @@ const props = defineProps<{
thingModelList: ThingModelApi.ThingModel[];
}>();
const RangePicker = DatePicker.RangePicker;
/** 查询参数 */
const queryParams = reactive({
identifier: '',
@ -213,18 +218,18 @@ defineExpose({
placeholder="请选择事件标识符"
style="width: 240px"
>
<Select.Option
<SelectOption
v-for="event in eventThingModels"
:key="event.identifier"
:value="event.identifier!"
>
{{ event.name }}({{ event.identifier }})
</Select.Option>
</SelectOption>
</Select>
</div>
<div class="flex items-center gap-2">
<span>时间范围</span>
<RangePicker
<DateRangePicker
v-model:value="queryParams.times"
format="YYYY-MM-DD HH:mm:ss"
show-time

View File

@ -19,6 +19,7 @@ import {
message,
Modal,
Space,
SpaceCompact,
Spin,
Table,
Tag,
@ -120,7 +121,7 @@ const tableColumns = computed(() => [
key: 'index',
width: 80,
align: 'center' as const,
customRender: ({ index }: { index: number }) => index + 1,
render: (_value: unknown, _record: unknown, index: number) => index + 1,
},
{
title: '时间',
@ -373,7 +374,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
</Button>
<!-- 视图切换 -->
<Button.Group class="ml-auto">
<SpaceCompact class="ml-auto">
<Button
:disabled="!canShowChart"
:type="viewMode === 'chart' ? 'primary' : 'default'"
@ -393,7 +394,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
</template>
列表
</Button>
</Button.Group>
</SpaceCompact>
</Space>
<!-- 数据统计信息 -->

View File

@ -23,6 +23,7 @@ import {
Divider,
Input,
Row,
SpaceCompact,
Switch,
Tag,
} from 'antdv-next';
@ -298,7 +299,7 @@ onBeforeUnmount(() => {
un-checked-children="定时刷新"
/>
</div>
<Button.Group>
<SpaceCompact>
<Button
:type="viewMode === 'card' ? 'primary' : 'default'"
@click="handleViewModeChange('card')"
@ -311,7 +312,7 @@ onBeforeUnmount(() => {
>
<IconifyIcon icon="ep:list" />
</Button>
</Button.Group>
</SpaceCompact>
</div>
<!-- 分隔线 -->

View File

@ -14,7 +14,14 @@ import {
import { IconifyIcon } from '@vben/icons';
import { formatDateTime } from '@vben/utils';
import { Button, DatePicker, Select, Space, Tag } from 'antdv-next';
import {
Button,
DateRangePicker,
Select,
SelectOption,
Space,
Tag,
} from 'antdv-next';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
@ -24,8 +31,6 @@ const props = defineProps<{
thingModelList: ThingModelApi.ThingModel[];
}>();
const RangePicker = DatePicker.RangePicker;
/** 查询参数 */
const queryParams = reactive({
identifier: '',
@ -227,18 +232,18 @@ defineExpose({
placeholder="请选择服务标识符"
style="width: 240px"
>
<Select.Option
<SelectOption
v-for="service in serviceThingModels"
:key="service.identifier"
:value="service.identifier!"
>
{{ service.name }}({{ service.identifier }})
</Select.Option>
</SelectOption>
</Select>
</div>
<div class="flex items-center gap-2">
<span>时间范围</span>
<RangePicker
<DateRangePicker
v-model:value="queryParams.times"
format="YYYY-MM-DD HH:mm:ss"
show-time

View File

@ -18,30 +18,35 @@ const props = defineProps<{
}>();
const activeTab = ref('property'); //
const thingModelTabItems = [
{ key: 'property', label: '设备属性(运行状态)' },
{ key: 'event', label: '设备事件上报' },
{ key: 'service', label: '设备服务调用' },
];
</script>
<template>
<ContentWrap>
<Tabs v-model:active-key="activeTab" class="!h-auto !p-0">
<Tabs.TabPane key="property" tab="设备属性(运行状态)">
<Tabs
v-model:active-key="activeTab"
:items="thingModelTabItems"
class="!h-auto !p-0"
>
<template #contentRender="{ item }">
<DeviceDetailsThingModelProperty
v-if="activeTab === 'property'"
v-if="item.key === 'property' && activeTab === 'property'"
:device-id="deviceId"
/>
</Tabs.TabPane>
<Tabs.TabPane key="event" tab="设备事件上报">
<DeviceDetailsThingModelEvent
v-if="activeTab === 'event'"
v-else-if="item.key === 'event' && activeTab === 'event'"
:device-id="props.deviceId"
:thing-model-list="props.thingModelList"
/>
</Tabs.TabPane>
<Tabs.TabPane key="service" tab="设备服务调用">
<DeviceDetailsThingModelService
v-if="activeTab === 'service'"
v-else-if="item.key === 'service' && activeTab === 'service'"
:device-id="deviceId"
:thing-model-list="props.thingModelList"
/>
</Tabs.TabPane>
</template>
</Tabs>
</ContentWrap>
</template>

View File

@ -21,6 +21,7 @@ import {
Input,
message,
Select,
SelectOption,
Space,
Tag,
} from 'antdv-next';
@ -292,13 +293,13 @@ onMounted(async () => {
allow-clear
style="width: 200px"
>
<Select.Option
<SelectOption
v-for="product in products"
:key="product.id"
:value="product.id"
>
{{ product.name }}
</Select.Option>
</SelectOption>
</Select>
<Input
v-model:value="queryParams.deviceName"
@ -320,7 +321,7 @@ onMounted(async () => {
allow-clear
style="width: 200px"
>
<Select.Option
<SelectOption
v-for="dict in getDictOptions(
DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE,
'number',
@ -329,7 +330,7 @@ onMounted(async () => {
:value="dict.value"
>
{{ dict.label }}
</Select.Option>
</SelectOption>
</Select>
<Select
v-model:value="queryParams.status"
@ -337,13 +338,13 @@ onMounted(async () => {
allow-clear
style="width: 200px"
>
<Select.Option
<SelectOption
v-for="dict in getDictOptions(DICT_TYPE.IOT_DEVICE_STATE, 'number')"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</Select.Option>
</SelectOption>
</Select>
<Select
v-model:value="queryParams.groupId"
@ -351,13 +352,13 @@ onMounted(async () => {
allow-clear
style="width: 200px"
>
<Select.Option
<SelectOption
v-for="group in deviceGroups"
:key="group.id"
:value="group.id"
>
{{ group.name }}
</Select.Option>
</SelectOption>
</Select>
<Button type="primary" @click="handleSearch">
<IconifyIcon icon="ant-design:search-outlined" class="mr-1" />

View File

@ -6,7 +6,7 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, Collapse, message, Space } from 'antdv-next';
import { Button, Collapse, CollapsePanel, message, Space } from 'antdv-next';
import { useVbenForm } from '#/adapter/form';
import { createDevice, getDevice, updateDevice } from '#/api/iot/device/device';
@ -208,12 +208,12 @@ onMounted(async () => {
<div class="mx-4">
<Form />
<Collapse v-model:active-key="activeKey" class="mt-4">
<Collapse.Panel key="advanced" header="更多设置">
<CollapsePanel key="advanced" header="更多设置">
<AdvancedForm />
<Space class="mt-2">
<Button type="primary" @click="openMapDialog"></Button>
</Space>
</Collapse.Panel>
</CollapsePanel>
</Collapse>
</div>
</Modal>

View File

@ -98,9 +98,12 @@ watch(
<template>
<Card title="升级设备记录">
<Tabs v-model:active-key="activeTab" @change="handleTabChange" class="mb-4">
<Tabs.TabPane v-for="tab in statusTabs" :key="tab.key" :tab="tab.label" />
</Tabs>
<Tabs
v-model:active-key="activeTab"
:items="statusTabs"
@change="handleTabChange"
class="mb-4"
/>
<Grid>
<template #actions="{ row }">
<TableAction

View File

@ -23,6 +23,10 @@ const id = Number(route.params.id);
const loading = ref(true);
const product = ref<IotProductApi.Product>({} as IotProductApi.Product);
const activeTab = ref('info');
const productTabItems = [
{ key: 'info', label: '产品信息' },
{ key: 'thingModel', label: '物模型(功能定义)' },
];
/** 向子组件提供产品信息 */
provide(IOT_PROVIDE_KEY.PRODUCT, product);
@ -77,13 +81,16 @@ onMounted(async () => {
:product="product"
@refresh="() => getProductData(id)"
/>
<Tabs v-model:active-key="activeTab" class="mt-4">
<Tabs.TabPane key="info" tab="产品信息">
<ProductDetailsInfo v-if="activeTab === 'info'" :product="product" />
</Tabs.TabPane>
<Tabs.TabPane key="thingModel" tab="物模型(功能定义)">
<IoTProductThingModel v-if="activeTab === 'thingModel'" />
</Tabs.TabPane>
<Tabs v-model:active-key="activeTab" :items="productTabItems" class="mt-4">
<template #contentRender="{ item }">
<ProductDetailsInfo
v-if="item.key === 'info' && activeTab === 'info'"
:product="product"
/>
<IoTProductThingModel
v-else-if="item.key === 'thingModel' && activeTab === 'thingModel'"
/>
</template>
</Tabs>
</Page>
</template>

View File

@ -11,6 +11,7 @@ import {
Button,
Card,
Descriptions,
DescriptionsItem,
message,
Popconfirm,
} from 'antdv-next';
@ -135,7 +136,7 @@ async function handleSyncPropertyTable(product: IotProductApi.Product) {
<Card class="mt-4">
<Descriptions :column="1">
<Descriptions.Item label="ProductKey">
<DescriptionsItem label="ProductKey">
{{ product.productKey }}
<Button
class="ml-2"
@ -144,15 +145,15 @@ async function handleSyncPropertyTable(product: IotProductApi.Product) {
>
复制
</Button>
</Descriptions.Item>
<Descriptions.Item label="设备总数">
</DescriptionsItem>
<DescriptionsItem label="设备总数">
<span class="ml-5 mr-2">
{{ product.deviceCount ?? '加载中...' }}
</span>
<Button size="small" @click="goToDeviceList(product.id!)">
前往管理
</Button>
</Descriptions.Item>
</DescriptionsItem>
</Descriptions>
</Card>
</div>

View File

@ -5,7 +5,7 @@ import { ref } from 'vue';
import { DeviceTypeEnum, DICT_TYPE } from '@vben/constants';
import { Button, Card, Descriptions, message } from 'antdv-next';
import { Button, Card, Descriptions, DescriptionsItem, message } from 'antdv-next';
import { DictTag } from '#/components/dict-tag';
@ -44,37 +44,37 @@ async function copyToClipboard(text: string) {
<template>
<Card title="产品信息">
<Descriptions :column="3" bordered size="small">
<Descriptions.Item label="产品名称">
<DescriptionsItem label="产品名称">
{{ product.name }}
</Descriptions.Item>
<Descriptions.Item label="所属分类">
</DescriptionsItem>
<DescriptionsItem label="所属分类">
{{ product.categoryName || '-' }}
</Descriptions.Item>
<Descriptions.Item label="设备类型">
</DescriptionsItem>
<DescriptionsItem label="设备类型">
<DictTag
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
:value="product.deviceType"
/>
</Descriptions.Item>
<Descriptions.Item label="创建时间">
</DescriptionsItem>
<DescriptionsItem label="创建时间">
{{ formatDate(product.createTime) }}
</Descriptions.Item>
<Descriptions.Item label="协议类型">
</DescriptionsItem>
<DescriptionsItem label="协议类型">
<DictTag
:type="DICT_TYPE.IOT_PROTOCOL_TYPE"
:value="product.protocolType"
/>
</Descriptions.Item>
<Descriptions.Item label="序列化类型">
</DescriptionsItem>
<DescriptionsItem label="序列化类型">
<DictTag
:type="DICT_TYPE.IOT_SERIALIZE_TYPE"
:value="product.serializeType"
/>
</Descriptions.Item>
<Descriptions.Item label="产品状态">
</DescriptionsItem>
<DescriptionsItem label="产品状态">
<DictTag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" />
</Descriptions.Item>
<Descriptions.Item
</DescriptionsItem>
<DescriptionsItem
v-if="
(
[DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY] as number[]
@ -83,8 +83,8 @@ async function copyToClipboard(text: string) {
label="联网方式"
>
<DictTag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" />
</Descriptions.Item>
<Descriptions.Item label="产品密钥">
</DescriptionsItem>
<DescriptionsItem label="产品密钥">
<span v-if="showProductSecret">{{ product.productSecret }}</span>
<span v-else>********</span>
<Button class="ml-2" size="small" @click="toggleProductSecretVisible">
@ -98,13 +98,13 @@ async function copyToClipboard(text: string) {
>
复制
</Button>
</Descriptions.Item>
<Descriptions.Item label="动态注册">
</DescriptionsItem>
<DescriptionsItem label="动态注册">
{{ product.registerEnabled ? '已开启' : '未开启' }}
</Descriptions.Item>
<Descriptions.Item :span="3" label="产品描述">
</DescriptionsItem>
<DescriptionsItem :span="3" label="产品描述">
{{ product.description || '-' }}
</Descriptions.Item>
</DescriptionsItem>
</Descriptions>
</Card>
</template>

View File

@ -5,7 +5,7 @@ import { computed, nextTick, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Collapse, message } from 'antdv-next';
import { Collapse, CollapsePanel, message } from 'antdv-next';
import { useVbenForm } from '#/adapter/form';
import {
@ -148,9 +148,9 @@ const [Modal, modalApi] = useVbenModal({
<div class="mx-4">
<Form />
<Collapse v-model:active-key="activeKey" class="mt-4">
<Collapse.Panel key="advanced" header="更多设置">
<CollapsePanel key="advanced" header="更多设置">
<AdvancedForm />
</Collapse.Panel>
</CollapsePanel>
</Collapse>
</div>
</Modal>

View File

@ -9,17 +9,19 @@ import DataRuleList from './rule/index.vue';
import DataSinkList from './sink/index.vue';
const activeTabName = ref('rule');
const ruleDataTabItems = [
{ key: 'rule', label: '规则' },
{ key: 'sink', label: '目的' },
];
</script>
<template>
<Page auto-content-height>
<Tabs v-model:active-key="activeTabName">
<Tabs.TabPane key="rule" tab="规则">
<DataRuleList />
</Tabs.TabPane>
<Tabs.TabPane key="sink" tab="目的">
<DataSinkList />
</Tabs.TabPane>
<Tabs v-model:active-key="activeTabName" :items="ruleDataTabItems">
<template #contentRender="{ item }">
<DataRuleList v-if="item.key === 'rule'" />
<DataSinkList v-else-if="item.key === 'sink'" />
</template>
</Tabs>
</Page>
</template>

View File

@ -5,7 +5,7 @@ import { IconifyIcon } from '@vben/icons';
import { isEmpty } from '@vben/utils';
import { useClipboard, useVModel } from '@vueuse/core';
import { Button, Form, Input, message } from 'antdv-next';
import { Button, FormItem, Input, InputPassword, message } from 'antdv-next';
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
@ -67,7 +67,7 @@ onMounted(() => {
</script>
<template>
<Form.Item
<FormItem
:name="['config', 'jdbcUrl']"
:rules="[
{ required: true, message: 'JDBC 连接地址不能为空', trigger: 'blur' },
@ -78,25 +78,25 @@ onMounted(() => {
v-model:value="config.jdbcUrl"
placeholder="请输入 JDBC 连接地址jdbc:mysql://localhost:3306/iot_data"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'username']"
:rules="[{ required: true, message: '用户名不能为空', trigger: 'blur' }]"
label="用户名"
>
<Input v-model:value="config.username" placeholder="请输入数据库用户名" />
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'password']"
:rules="[{ required: true, message: '密码不能为空', trigger: 'blur' }]"
label="密码"
>
<Input.Password
<InputPassword
v-model:value="config.password"
placeholder="请输入数据库密码"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'tableName']"
:rules="[{ required: true, message: '目标表名不能为空', trigger: 'blur' }]"
label="目标表名"
@ -115,7 +115,7 @@ onMounted(() => {
{{ showSqlTip ? '收起表结构提示' : '查看表结构提示' }}
</Button>
</div>
</Form.Item>
</FormItem>
<div
v-if="showSqlTip"
class="mt-2 overflow-hidden rounded border border-gray-200 dark:border-gray-700"

View File

@ -5,7 +5,7 @@ import { computed, onMounted, ref, watch } from 'vue';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Form, Input, Select } from 'antdv-next';
import { FormItem, Input, Select, SelectOption, TextArea } from 'antdv-next';
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
@ -60,7 +60,7 @@ onMounted(() => {
</script>
<template>
<Form.Item
<FormItem
:name="['config', 'url']"
:rules="[{ required: true, message: '请求地址不能为空', trigger: 'blur' }]"
label="请求地址"
@ -68,13 +68,13 @@ onMounted(() => {
<Input v-model:value="urlPath" placeholder="请输入请求地址">
<template #addonBefore>
<Select v-model:value="urlPrefix" class="w-[100px]">
<Select.Option value="http://">http://</Select.Option>
<Select.Option value="https://">https://</Select.Option>
<SelectOption value="http://">http://</SelectOption>
<SelectOption value="https://">https://</SelectOption>
</Select>
</template>
</Input>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'method']"
:rules="[
{ required: true, message: '请求方法不能为空', trigger: 'change' },
@ -82,23 +82,23 @@ onMounted(() => {
label="请求方法"
>
<Select v-model:value="config.method" placeholder="请选择请求方法">
<Select.Option value="GET">GET</Select.Option>
<Select.Option value="POST">POST</Select.Option>
<Select.Option value="PUT">PUT</Select.Option>
<Select.Option value="DELETE">DELETE</Select.Option>
<SelectOption value="GET">GET</SelectOption>
<SelectOption value="POST">POST</SelectOption>
<SelectOption value="PUT">PUT</SelectOption>
<SelectOption value="DELETE">DELETE</SelectOption>
</Select>
</Form.Item>
<Form.Item label="请求头">
</FormItem>
<FormItem label="请求头">
<KeyValueEditor v-model="config.headers" add-button-text="" />
</Form.Item>
<Form.Item label="请求参数">
</FormItem>
<FormItem label="请求参数">
<KeyValueEditor v-model="config.query" add-button-text="" />
</Form.Item>
<Form.Item label="请求体">
<Input.TextArea
</FormItem>
<FormItem label="请求体">
<TextArea
v-model:value="config.body"
placeholder="请输入内容"
:rows="4"
/>
</Form.Item>
</FormItem>
</template>

View File

@ -4,7 +4,7 @@ import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Form, Input, Switch } from 'antdv-next';
import { FormItem, Input, InputPassword, Switch } from 'antdv-next';
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
@ -28,7 +28,7 @@ onMounted(() => {
</script>
<template>
<Form.Item
<FormItem
:name="['config', 'bootstrapServers']"
:rules="[{ required: true, message: '服务地址不能为空', trigger: 'blur' }]"
label="服务地址"
@ -37,29 +37,29 @@ onMounted(() => {
v-model:value="config.bootstrapServers"
placeholder="请输入服务地址localhost:9092"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'username']"
:rules="[{ required: true, message: '用户名不能为空', trigger: 'blur' }]"
label="用户名"
>
<Input v-model:value="config.username" placeholder="请输入用户名" />
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'password']"
:rules="[{ required: true, message: '密码不能为空', trigger: 'blur' }]"
label="密码"
>
<Input.Password v-model:value="config.password" placeholder="请输入密码" />
</Form.Item>
<Form.Item :name="['config', 'ssl']" label="启用 SSL">
<InputPassword v-model:value="config.password" placeholder="请输入密码" />
</FormItem>
<FormItem :name="['config', 'ssl']" label="启用 SSL">
<Switch v-model:checked="config.ssl" />
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'topic']"
:rules="[{ required: true, message: '主题不能为空', trigger: 'blur' }]"
label="主题"
>
<Input v-model:value="config.topic" placeholder="请输入主题" />
</Form.Item>
</FormItem>
</template>

View File

@ -4,7 +4,7 @@ import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Form, Input } from 'antdv-next';
import { FormItem, Input, InputPassword } from 'antdv-next';
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
@ -28,7 +28,7 @@ onMounted(() => {
</script>
<template>
<Form.Item
<FormItem
:name="['config', 'url']"
:rules="[{ required: true, message: '服务地址不能为空', trigger: 'blur' }]"
label="服务地址"
@ -37,22 +37,22 @@ onMounted(() => {
v-model:value="config.url"
placeholder="请输入 MQTT 服务地址mqtt://localhost:1883"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'username']"
:rules="[{ required: true, message: '用户名不能为空', trigger: 'blur' }]"
label="用户名"
>
<Input v-model:value="config.username" placeholder="请输入用户名" />
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'password']"
:rules="[{ required: true, message: '密码不能为空', trigger: 'blur' }]"
label="密码"
>
<Input.Password v-model:value="config.password" placeholder="请输入密码" />
</Form.Item>
<Form.Item
<InputPassword v-model:value="config.password" placeholder="请输入密码" />
</FormItem>
<FormItem
:name="['config', 'clientId']"
:rules="[
{ required: true, message: '客户端 ID 不能为空', trigger: 'blur' },
@ -60,12 +60,12 @@ onMounted(() => {
label="客户端 ID"
>
<Input v-model:value="config.clientId" placeholder="请输入客户端 ID" />
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'topic']"
:rules="[{ required: true, message: '主题不能为空', trigger: 'blur' }]"
label="主题"
>
<Input v-model:value="config.topic" placeholder="请输入主题" />
</Form.Item>
</FormItem>
</template>

View File

@ -4,7 +4,7 @@ import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Form, Input, InputNumber } from 'antdv-next';
import { FormItem, Input, InputNumber, InputPassword } from 'antdv-next';
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
@ -31,7 +31,7 @@ onMounted(() => {
</script>
<template>
<Form.Item
<FormItem
:name="['config', 'host']"
:rules="[{ required: true, message: '主机地址不能为空', trigger: 'blur' }]"
label="主机地址"
@ -40,8 +40,8 @@ onMounted(() => {
v-model:value="config.host"
placeholder="请输入主机地址localhost"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'port']"
:rules="[
{ required: true, message: '端口不能为空', trigger: 'blur' },
@ -62,47 +62,47 @@ onMounted(() => {
placeholder="请输入端口"
class="w-full"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'virtualHost']"
:rules="[{ required: true, message: '虚拟主机不能为空', trigger: 'blur' }]"
label="虚拟主机"
>
<Input v-model:value="config.virtualHost" placeholder="请输入虚拟主机" />
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'username']"
:rules="[{ required: true, message: '用户名不能为空', trigger: 'blur' }]"
label="用户名"
>
<Input v-model:value="config.username" placeholder="请输入用户名" />
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'password']"
:rules="[{ required: true, message: '密码不能为空', trigger: 'blur' }]"
label="密码"
>
<Input.Password v-model:value="config.password" placeholder="请输入密码" />
</Form.Item>
<Form.Item
<InputPassword v-model:value="config.password" placeholder="请输入密码" />
</FormItem>
<FormItem
:name="['config', 'exchange']"
:rules="[{ required: true, message: '交换机不能为空', trigger: 'blur' }]"
label="交换机"
>
<Input v-model:value="config.exchange" placeholder="请输入交换机" />
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'routingKey']"
:rules="[{ required: true, message: '路由键不能为空', trigger: 'blur' }]"
label="路由键"
>
<Input v-model:value="config.routingKey" placeholder="请输入路由键" />
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'queue']"
:rules="[{ required: true, message: '队列不能为空', trigger: 'blur' }]"
label="队列"
>
<Input v-model:value="config.queue" placeholder="请输入队列" />
</Form.Item>
</FormItem>
</template>

View File

@ -4,7 +4,7 @@ import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Form, Input, InputNumber } from 'antdv-next';
import { FormItem, Input, InputNumber, InputPassword } from 'antdv-next';
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
@ -36,7 +36,7 @@ onMounted(() => {
</script>
<template>
<Form.Item
<FormItem
:name="['config', 'host']"
:rules="[{ required: true, message: '主机地址不能为空', trigger: 'blur' }]"
label="主机地址"
@ -45,8 +45,8 @@ onMounted(() => {
v-model:value="config.host"
placeholder="请输入主机地址localhost"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'port']"
:rules="[
{ required: true, message: '端口不能为空', trigger: 'blur' },
@ -67,15 +67,15 @@ onMounted(() => {
placeholder="请输入端口"
class="w-full"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'password']"
:rules="[{ required: true, message: '密码不能为空', trigger: 'blur' }]"
label="密码"
>
<Input.Password v-model:value="config.password" placeholder="请输入密码" />
</Form.Item>
<Form.Item
<InputPassword v-model:value="config.password" placeholder="请输入密码" />
</FormItem>
<FormItem
:name="['config', 'database']"
:rules="[
{ required: true, message: '数据库索引不能为空', trigger: 'blur' },
@ -95,12 +95,12 @@ onMounted(() => {
placeholder="请输入数据库索引"
class="w-full"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'topic']"
:rules="[{ required: true, message: '主题不能为空', trigger: 'blur' }]"
label="主题"
>
<Input v-model:value="config.topic" placeholder="请输入主题" />
</Form.Item>
</FormItem>
</template>

View File

@ -4,7 +4,7 @@ import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Form, Input } from 'antdv-next';
import { FormItem, Input, InputPassword } from 'antdv-next';
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
@ -29,7 +29,7 @@ onMounted(() => {
</script>
<template>
<Form.Item
<FormItem
:name="['config', 'nameServer']"
:rules="[
{ required: true, message: 'NameServer 地址不能为空', trigger: 'blur' },
@ -40,8 +40,8 @@ onMounted(() => {
v-model:value="config.nameServer"
placeholder="请输入 NameServer 地址127.0.0.1:9876"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'accessKey']"
:rules="[
{ required: true, message: 'AccessKey 不能为空', trigger: 'blur' },
@ -49,34 +49,34 @@ onMounted(() => {
label="AccessKey"
>
<Input v-model:value="config.accessKey" placeholder="请输入 AccessKey" />
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'secretKey']"
:rules="[
{ required: true, message: 'SecretKey 不能为空', trigger: 'blur' },
]"
label="SecretKey"
>
<Input.Password
<InputPassword
v-model:value="config.secretKey"
placeholder="请输入 SecretKey"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'group']"
:rules="[{ required: true, message: '消费组不能为空', trigger: 'blur' }]"
label="消费组"
>
<Input v-model:value="config.group" placeholder="请输入消费组" />
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'topic']"
:rules="[{ required: true, message: '主题不能为空', trigger: 'blur' }]"
label="主题"
>
<Input v-model:value="config.topic" placeholder="请输入主题" />
</Form.Item>
<Form.Item :name="['config', 'tags']" label="标签">
</FormItem>
<FormItem :name="['config', 'tags']" label="标签">
<Input v-model:value="config.tags" placeholder="请输入标签" />
</Form.Item>
</FormItem>
</template>

View File

@ -4,7 +4,7 @@ import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Form, Input, InputNumber, Select, Switch } from 'antdv-next';
import { FormItem, Input, InputNumber, Select, SelectOption, Switch } from 'antdv-next';
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
@ -33,7 +33,7 @@ onMounted(() => {
</script>
<template>
<Form.Item
<FormItem
:name="['config', 'host']"
:rules="[{ required: true, message: '主机地址不能为空', trigger: 'blur' }]"
label="服务器地址"
@ -42,8 +42,8 @@ onMounted(() => {
v-model:value="config.host"
placeholder="请输入 TCP 服务器地址localhost"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'port']"
:rules="[
{ required: true, message: '端口不能为空', trigger: 'blur' },
@ -64,8 +64,8 @@ onMounted(() => {
placeholder="请输入端口"
class="w-full"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'connectTimeoutMs']"
:rules="[
{ required: true, message: '连接超时时间不能为空', trigger: 'blur' },
@ -78,8 +78,8 @@ onMounted(() => {
:step="1000"
class="w-full"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'readTimeoutMs']"
:rules="[
{ required: true, message: '读取超时时间不能为空', trigger: 'blur' },
@ -92,11 +92,11 @@ onMounted(() => {
:step="1000"
class="w-full"
/>
</Form.Item>
<Form.Item :name="['config', 'ssl']" label="启用 SSL">
</FormItem>
<FormItem :name="['config', 'ssl']" label="启用 SSL">
<Switch v-model:checked="config.ssl" />
</Form.Item>
<Form.Item
</FormItem>
<FormItem
v-if="config.ssl"
:name="['config', 'sslCertPath']"
label="SSL 证书路径"
@ -105,8 +105,8 @@ onMounted(() => {
v-model:value="config.sslCertPath"
placeholder="请输入 SSL 证书路径"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'dataFormat']"
:rules="[
{ required: true, message: '数据格式不能为空', trigger: 'change' },
@ -114,11 +114,11 @@ onMounted(() => {
label="数据格式"
>
<Select v-model:value="config.dataFormat" placeholder="请选择数据格式">
<Select.Option value="JSON">JSON</Select.Option>
<Select.Option value="BINARY">BINARY</Select.Option>
<SelectOption value="JSON">JSON</SelectOption>
<SelectOption value="BINARY">BINARY</SelectOption>
</Select>
</Form.Item>
<Form.Item :name="['config', 'heartbeatIntervalMs']" label="心跳间隔(ms)">
</FormItem>
<FormItem :name="['config', 'heartbeatIntervalMs']" label="心跳间隔(ms)">
<InputNumber
v-model:value="config.heartbeatIntervalMs"
:min="0"
@ -126,20 +126,20 @@ onMounted(() => {
placeholder="0 表示不启用心跳"
class="w-full"
/>
</Form.Item>
<Form.Item :name="['config', 'reconnectIntervalMs']" label="重连间隔(ms)">
</FormItem>
<FormItem :name="['config', 'reconnectIntervalMs']" label="重连间隔(ms)">
<InputNumber
v-model:value="config.reconnectIntervalMs"
:min="1000"
:step="1000"
class="w-full"
/>
</Form.Item>
<Form.Item :name="['config', 'maxReconnectAttempts']" label="最大重连次数">
</FormItem>
<FormItem :name="['config', 'maxReconnectAttempts']" label="最大重连次数">
<InputNumber
v-model:value="config.maxReconnectAttempts"
:min="0"
class="w-full"
/>
</Form.Item>
</FormItem>
</template>

View File

@ -4,7 +4,7 @@ import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Form, Input, InputNumber, Select, Switch } from 'antdv-next';
import { FormItem, Input, InputNumber, Select, SelectOption, Switch, TextArea } from 'antdv-next';
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
@ -37,7 +37,7 @@ onMounted(() => {
</script>
<template>
<Form.Item
<FormItem
:name="['config', 'serverUrl']"
:rules="[
{
@ -52,8 +52,8 @@ onMounted(() => {
v-model:value="config.serverUrl"
placeholder="请输入 WebSocket 地址ws://localhost:8080/ws"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'connectTimeoutMs']"
:rules="[
{ required: true, message: '连接超时时间不能为空', trigger: 'blur' },
@ -66,8 +66,8 @@ onMounted(() => {
:step="1000"
class="w-full"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'sendTimeoutMs']"
:rules="[
{ required: true, message: '发送超时时间不能为空', trigger: 'blur' },
@ -80,8 +80,8 @@ onMounted(() => {
:step="1000"
class="w-full"
/>
</Form.Item>
<Form.Item :name="['config', 'heartbeatIntervalMs']" label="心跳间隔(ms)">
</FormItem>
<FormItem :name="['config', 'heartbeatIntervalMs']" label="心跳间隔(ms)">
<InputNumber
v-model:value="config.heartbeatIntervalMs"
:min="0"
@ -89,30 +89,30 @@ onMounted(() => {
placeholder="0 表示不启用心跳"
class="w-full"
/>
</Form.Item>
<Form.Item :name="['config', 'heartbeatMessage']" label="心跳消息">
</FormItem>
<FormItem :name="['config', 'heartbeatMessage']" label="心跳消息">
<Input
v-model:value="config.heartbeatMessage"
placeholder="请输入心跳消息内容JSON 格式)"
/>
</Form.Item>
<Form.Item :name="['config', 'subprotocols']" label="子协议">
</FormItem>
<FormItem :name="['config', 'subprotocols']" label="子协议">
<Input
v-model:value="config.subprotocols"
placeholder="请输入子协议列表,多个用逗号分隔"
/>
</Form.Item>
<Form.Item :name="['config', 'customHeaders']" label="自定义请求头">
<Input.TextArea
</FormItem>
<FormItem :name="['config', 'customHeaders']" label="自定义请求头">
<TextArea
v-model:value="config.customHeaders"
placeholder="请输入自定义请求头JSON 格式)"
:rows="3"
/>
</Form.Item>
<Form.Item :name="['config', 'verifySslCert']" label="验证 SSL 证书">
</FormItem>
<FormItem :name="['config', 'verifySslCert']" label="验证 SSL 证书">
<Switch v-model:checked="config.verifySslCert" />
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['config', 'dataFormat']"
:rules="[
{ required: true, message: '数据格式不能为空', trigger: 'change' },
@ -120,41 +120,41 @@ onMounted(() => {
label="数据格式"
>
<Select v-model:value="config.dataFormat" placeholder="请选择数据格式">
<Select.Option value="JSON">JSON</Select.Option>
<Select.Option value="TEXT">TEXT</Select.Option>
<SelectOption value="JSON">JSON</SelectOption>
<SelectOption value="TEXT">TEXT</SelectOption>
</Select>
</Form.Item>
<Form.Item :name="['config', 'reconnectIntervalMs']" label="重连间隔(ms)">
</FormItem>
<FormItem :name="['config', 'reconnectIntervalMs']" label="重连间隔(ms)">
<InputNumber
v-model:value="config.reconnectIntervalMs"
:min="1000"
:step="1000"
class="w-full"
/>
</Form.Item>
<Form.Item :name="['config', 'maxReconnectAttempts']" label="最大重连次数">
</FormItem>
<FormItem :name="['config', 'maxReconnectAttempts']" label="最大重连次数">
<InputNumber
v-model:value="config.maxReconnectAttempts"
:min="0"
class="w-full"
/>
</Form.Item>
<Form.Item :name="['config', 'enableCompression']" label="启用压缩">
</FormItem>
<FormItem :name="['config', 'enableCompression']" label="启用压缩">
<Switch v-model:checked="config.enableCompression" />
</Form.Item>
<Form.Item :name="['config', 'sendRetryCount']" label="发送重试次数">
</FormItem>
<FormItem :name="['config', 'sendRetryCount']" label="发送重试次数">
<InputNumber
v-model:value="config.sendRetryCount"
:min="0"
class="w-full"
/>
</Form.Item>
<Form.Item :name="['config', 'sendRetryIntervalMs']" label="重试间隔(ms)">
</FormItem>
<FormItem :name="['config', 'sendRetryIntervalMs']" label="重试间隔(ms)">
<InputNumber
v-model:value="config.sendRetryIntervalMs"
:min="100"
:step="500"
class="w-full"
/>
</Form.Item>
</FormItem>
</template>

View File

@ -7,7 +7,7 @@ import { useVbenModal } from '@vben/common-ui';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Form, Input, message, Radio, Select } from 'antdv-next';
import { Form, FormItem, Input, message, Radio, RadioGroup, Select, TextArea } from 'antdv-next';
import {
createDataSink,
@ -102,7 +102,7 @@ function handleTypeChange(type: number) {
:wrapper-col="{ span: 18 }"
class="mx-4"
>
<Form.Item
<FormItem
:rules="[
{ required: true, message: '目的名称不能为空', trigger: 'blur' },
]"
@ -110,15 +110,15 @@ function handleTypeChange(type: number) {
name="name"
>
<Input v-model:value="formData.name" placeholder="请输入目的名称" />
</Form.Item>
<Form.Item label="目的描述" name="description">
<Input.TextArea
</FormItem>
<FormItem label="目的描述" name="description">
<TextArea
v-model:value="formData.description"
placeholder="请输入目的描述"
:rows="3"
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:rules="[
{ required: true, message: '目的类型不能为空', trigger: 'change' },
]"
@ -133,7 +133,7 @@ function handleTypeChange(type: number) {
placeholder="请选择目的类型"
@change="(value: any) => handleTypeChange(value as number)"
/>
</Form.Item>
</FormItem>
<!-- 配置项按目的类型分支 -->
<HttpConfigForm
v-if="formData.type === IotDataSinkTypeEnum.HTTP"
@ -171,14 +171,14 @@ function handleTypeChange(type: number) {
v-if="formData.type === IotDataSinkTypeEnum.REDIS_STREAM"
v-model="formData.config"
/>
<Form.Item
<FormItem
:rules="[
{ required: true, message: '目的状态不能为空', trigger: 'change' },
]"
label="目的状态"
name="status"
>
<Radio.Group v-model:value="formData.status">
<RadioGroup v-model:value="formData.status">
<Radio
v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS, 'number')"
:key="String(dict.value)"
@ -186,8 +186,8 @@ function handleTypeChange(type: number) {
>
{{ dict.label }}
</Radio>
</Radio.Group>
</Form.Item>
</RadioGroup>
</FormItem>
</Form>
</Modal>
</template>

View File

@ -3,7 +3,7 @@
import { onMounted, ref } from 'vue';
import { useVModel } from '@vueuse/core';
import { Form, Select } from 'antdv-next';
import { FormItem, Select, SelectOption } from 'antdv-next';
import { getSimpleAlertConfigList } from '#/api/iot/alert/config';
@ -46,7 +46,7 @@ onMounted(() => {
<template>
<div class="w-full">
<Form.Item label="告警配置" required>
<FormItem label="告警配置" required>
<Select
v-model:value="localValue"
placeholder="请选择告警配置"
@ -56,13 +56,13 @@ onMounted(() => {
class="w-full"
:loading="loading"
>
<Select.Option
<SelectOption
v-for="config in alertConfigs"
:key="config.id"
:label="config.name"
:value="config.id"
/>
</Select>
</Form.Item>
</FormItem>
</div>
</template>

View File

@ -12,7 +12,7 @@ import {
} from '@vben/constants';
import { useVModel } from '@vueuse/core';
import { Col, Form, Row, Select } from 'antdv-next';
import { Col, FormItem, Row, Select, SelectOption } from 'antdv-next';
import ValueInput from '../inputs/value-input.vue';
import DeviceSelector from '../selectors/device-selector.vue';
@ -160,7 +160,7 @@ function handleOperatorChange() {
<!-- 条件类型选择 -->
<Row :gutter="16">
<Col :span="8">
<Form.Item label="条件类型" required>
<FormItem label="条件类型" required>
<Select
:value="condition.type"
@change="
@ -172,22 +172,22 @@ function handleOperatorChange() {
placeholder="请选择条件类型"
class="w-full"
>
<Select.Option
<SelectOption
v-for="option in getConditionTypeOptions()"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</Select.Option>
</SelectOption>
</Select>
</Form.Item>
</FormItem>
</Col>
</Row>
<!-- 产品设备选择 - 设备相关条件的公共部分 -->
<Row v-if="isDeviceCondition" :gutter="16">
<Col :span="12">
<Form.Item label="产品" required>
<FormItem label="产品" required>
<ProductSelector
:model-value="condition.productId"
@update:model-value="
@ -195,10 +195,10 @@ function handleOperatorChange() {
"
@change="handleProductChange"
/>
</Form.Item>
</FormItem>
</Col>
<Col :span="12">
<Form.Item label="设备" required>
<FormItem label="设备" required>
<DeviceSelector
:model-value="condition.deviceId"
@update:model-value="
@ -207,7 +207,7 @@ function handleOperatorChange() {
:product-id="condition.productId"
@change="handleDeviceChange"
/>
</Form.Item>
</FormItem>
</Col>
</Row>
@ -222,42 +222,42 @@ function handleOperatorChange() {
<Row :gutter="16">
<!-- 操作符选择 -->
<Col :span="12">
<Form.Item label="操作符" required>
<FormItem label="操作符" required>
<Select
:value="condition.operator"
@change="(value: any) => updateConditionField('operator', value)"
placeholder="请选择操作符"
class="w-full"
>
<Select.Option
<SelectOption
v-for="option in statusOperatorOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</Select.Option>
</SelectOption>
</Select>
</Form.Item>
</FormItem>
</Col>
<!-- 状态选择 -->
<Col :span="12">
<Form.Item label="设备状态" required>
<FormItem label="设备状态" required>
<Select
:value="condition.param"
@change="(value: any) => updateConditionField('param', value)"
placeholder="请选择设备状态"
class="w-full"
>
<Select.Option
<SelectOption
v-for="option in deviceStatusOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</Select.Option>
</SelectOption>
</Select>
</Form.Item>
</FormItem>
</Col>
</Row>
</div>
@ -273,7 +273,7 @@ function handleOperatorChange() {
<Row :gutter="16">
<!-- 属性/事件/服务选择 -->
<Col :span="6">
<Form.Item label="监控项" required>
<FormItem label="监控项" required>
<PropertySelector
:model-value="condition.identifier"
@update:model-value="
@ -284,12 +284,12 @@ function handleOperatorChange() {
:device-id="condition.deviceId"
@change="handlePropertyChange"
/>
</Form.Item>
</FormItem>
</Col>
<!-- 操作符选择 -->
<Col :span="6">
<Form.Item label="操作符" required>
<FormItem label="操作符" required>
<OperatorSelector
:model-value="condition.operator"
@update:model-value="
@ -298,12 +298,12 @@ function handleOperatorChange() {
:property-type="propertyType"
@change="handleOperatorChange"
/>
</Form.Item>
</FormItem>
</Col>
<!-- 值输入 -->
<Col :span="12">
<Form.Item label="比较值" required>
<FormItem label="比较值" required>
<ValueInput
:model-value="condition.param"
@update:model-value="
@ -313,7 +313,7 @@ function handleOperatorChange() {
:operator="condition.operator"
:property-config="propertyConfig"
/>
</Form.Item>
</FormItem>
</Col>
</Row>
</div>

View File

@ -14,9 +14,10 @@ import { useVModel } from '@vueuse/core';
import {
Col,
DatePicker,
Form,
FormItem,
Row,
Select,
SelectOption,
Tag,
TimePicker,
} from 'antdv-next';
@ -178,7 +179,7 @@ watch(
<Row :gutter="16">
<!-- 时间操作符选择 -->
<Col :span="8">
<Form.Item label="时间条件" required>
<FormItem label="时间条件" required>
<Select
:value="condition.operator"
@update:value="
@ -187,7 +188,7 @@ watch(
placeholder="请选择时间条件"
class="w-full"
>
<Select.Option
<SelectOption
v-for="option in timeOperatorOptions"
:key="option.value"
:label="option.label"
@ -202,14 +203,14 @@ watch(
{{ option.category }}
</Tag>
</div>
</Select.Option>
</SelectOption>
</Select>
</Form.Item>
</FormItem>
</Col>
<!-- 时间值输入 -->
<Col :span="8">
<Form.Item label="时间值" required>
<FormItem label="时间值" required>
<TimePicker
v-if="needsTimeInput"
:value="timeValue"
@ -230,12 +231,12 @@ watch(
class="w-full"
/>
<div v-else class="text-sm text-muted-foreground">无需设置时间值</div>
</Form.Item>
</FormItem>
</Col>
<!-- 第二个时间值范围条件 -->
<Col :span="8" v-if="needsSecondTimeInput">
<Form.Item label="结束时间" required>
<FormItem label="结束时间" required>
<TimePicker
v-if="needsTimeInput"
:value="timeValue2"
@ -255,7 +256,7 @@ watch(
value-format="YYYY-MM-DD HH:mm:ss"
class="w-full"
/>
</Form.Item>
</FormItem>
</Col>
</Row>
</div>

View File

@ -13,7 +13,7 @@ import {
import { isObject } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Col, Form, Row, Select, Tag } from 'antdv-next';
import { Col, FormItem, Row, Select, SelectOption, Tag } from 'antdv-next';
import { getThingModelTSLByProductId } from '#/api/iot/thingmodel';
@ -315,21 +315,21 @@ watch(
<!-- 产品和设备选择 - 与触发器保持一致的分离式选择器 -->
<Row :gutter="16">
<Col :span="12">
<Form.Item label="产品" required>
<FormItem label="产品" required>
<ProductSelector
v-model="action.productId"
@change="handleProductChange"
/>
</Form.Item>
</FormItem>
</Col>
<Col :span="12">
<Form.Item label="设备" required>
<FormItem label="设备" required>
<DeviceSelector
v-model="action.deviceId"
:product-id="action.productId"
@change="handleDeviceChange"
/>
</Form.Item>
</FormItem>
</Col>
</Row>
@ -338,7 +338,7 @@ watch(
v-if="action.productId && isServiceInvokeAction"
class="space-y-[16px]"
>
<Form.Item label="服务" required>
<FormItem label="服务" required>
<Select
v-model:value="action.identifier"
placeholder="请选择服务"
@ -348,7 +348,7 @@ watch(
:loading="loadingServices"
@change="handleServiceChange"
>
<Select.Option
<SelectOption
v-for="service in serviceList"
:key="service.identifier"
:label="service.name"
@ -362,34 +362,34 @@ watch(
{{ service.callType === 'sync' ? '同步' : '异步' }}
</Tag>
</div>
</Select.Option>
</SelectOption>
</Select>
</Form.Item>
</FormItem>
<!-- 服务参数配置 -->
<div v-if="action.identifier" class="space-y-[16px]">
<Form.Item label="服务参数" required>
<FormItem label="服务参数" required>
<JsonParamsInput
v-model="paramsValue"
type="service"
:config="{ service: selectedService } as any"
placeholder="请输入 JSON 格式的服务参数"
/>
</Form.Item>
</FormItem>
</div>
</div>
<!-- 控制参数配置 - 属性设置类型时显示 -->
<div v-if="action.productId && isPropertySetAction" class="space-y-[16px]">
<!-- 参数配置 -->
<Form.Item label="参数" required>
<FormItem label="参数" required>
<JsonParamsInput
v-model="paramsValue"
type="property"
:config="{ properties: thingModelProperties }"
placeholder="请输入 JSON 格式的控制参数"
/>
</Form.Item>
</FormItem>
</div>
</div>
</template>

View File

@ -12,7 +12,7 @@ import {
} from '@vben/constants';
import { useVModel } from '@vueuse/core';
import { Col, Form, Input, Row, Select } from 'antdv-next';
import { Col, FormItem, Input, Row, Select, SelectOption } from 'antdv-next';
import JsonParamsInput from '../inputs/json-params-input.vue';
import ValueInput from '../inputs/value-input.vue';
@ -168,29 +168,29 @@ function handlePropertyChange(propertyInfo: any) {
<template>
<div class="space-y-4">
<!-- 触发事件类型选择 -->
<Form.Item label="触发事件类型" required>
<FormItem label="触发事件类型" required>
<Select
:value="triggerType"
@change="(value: any) => handleTriggerTypeChange(value)"
placeholder="请选择触发事件类型"
class="w-full"
>
<Select.Option
<SelectOption
v-for="option in triggerTypeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</Select.Option>
</SelectOption>
</Select>
</Form.Item>
</FormItem>
<!-- 设备属性条件配置 -->
<div v-if="isDevicePropertyTrigger" class="space-y-4">
<!-- 产品设备选择 -->
<Row :gutter="16">
<Col :span="12">
<Form.Item label="产品" required>
<FormItem label="产品" required>
<ProductSelector
:model-value="condition.productId"
@update:model-value="
@ -198,10 +198,10 @@ function handlePropertyChange(propertyInfo: any) {
"
@change="handleProductChange"
/>
</Form.Item>
</FormItem>
</Col>
<Col :span="12">
<Form.Item label="设备" required>
<FormItem label="设备" required>
<DeviceSelector
:model-value="condition.deviceId"
@update:model-value="
@ -210,7 +210,7 @@ function handlePropertyChange(propertyInfo: any) {
:product-id="condition.productId"
@change="handleDeviceChange"
/>
</Form.Item>
</FormItem>
</Col>
</Row>
@ -218,7 +218,7 @@ function handlePropertyChange(propertyInfo: any) {
<Row :gutter="16">
<!-- 属性/事件/服务选择 -->
<Col :span="6">
<Form.Item label="监控项" required>
<FormItem label="监控项" required>
<PropertySelector
:model-value="condition.identifier"
@update:model-value="
@ -229,12 +229,12 @@ function handlePropertyChange(propertyInfo: any) {
:device-id="condition.deviceId"
@change="handlePropertyChange"
/>
</Form.Item>
</FormItem>
</Col>
<!-- 操作符选择 - 服务调用和事件上报不需要操作符 -->
<Col v-if="needsOperatorSelector" :span="6">
<Form.Item label="操作符" required>
<FormItem label="操作符" required>
<OperatorSelector
:model-value="condition.operator"
@update:model-value="
@ -242,12 +242,12 @@ function handlePropertyChange(propertyInfo: any) {
"
:property-type="propertyType"
/>
</Form.Item>
</FormItem>
</Col>
<!-- 值输入 -->
<Col :span="isWideValueColumn ? 18 : 12">
<Form.Item :label="valueInputLabel" required>
<FormItem :label="valueInputLabel" required>
<!-- 服务调用参数配置 -->
<JsonParamsInput
v-if="
@ -265,9 +265,7 @@ function handlePropertyChange(propertyInfo: any) {
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
"
:value="condition.value"
@update:value="
(value) => updateConditionField('value', value)
"
@update:value="(value) => updateConditionField('value', value)"
placeholder="留空则事件发生即匹配"
/>
<!-- 普通值输入 -->
@ -281,7 +279,7 @@ function handlePropertyChange(propertyInfo: any) {
:operator="condition.operator"
:property-config="propertyConfig"
/>
</Form.Item>
</FormItem>
</Col>
</Row>
</div>
@ -291,7 +289,7 @@ function handlePropertyChange(propertyInfo: any) {
<!-- 设备状态触发器使用简化的配置 -->
<Row :gutter="16">
<Col :span="12">
<Form.Item label="产品" required>
<FormItem label="产品" required>
<ProductSelector
:model-value="condition.productId"
@update:model-value="
@ -299,10 +297,10 @@ function handlePropertyChange(propertyInfo: any) {
"
@change="handleProductChange"
/>
</Form.Item>
</FormItem>
</Col>
<Col :span="12">
<Form.Item label="设备" required>
<FormItem label="设备" required>
<DeviceSelector
:model-value="condition.deviceId"
@update:model-value="
@ -311,19 +309,19 @@ function handlePropertyChange(propertyInfo: any) {
:product-id="condition.productId"
@change="handleDeviceChange"
/>
</Form.Item>
</FormItem>
</Col>
</Row>
<Row :gutter="16">
<Col :span="6">
<Form.Item label="操作符" required>
<FormItem label="操作符" required>
<Select
:value="condition.operator"
@change="(value: any) => updateConditionField('operator', value)"
placeholder="请选择操作符"
class="w-full"
>
<Select.Option
<SelectOption
:value="
IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
"
@ -331,27 +329,27 @@ function handlePropertyChange(propertyInfo: any) {
{{
IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.name
}}
</Select.Option>
</SelectOption>
</Select>
</Form.Item>
</FormItem>
</Col>
<Col :span="6">
<Form.Item label="参数" required>
<FormItem label="参数" required>
<Select
:value="condition.value"
@change="(value: any) => updateConditionField('value', value)"
placeholder="请选择操作符"
class="w-full"
>
<Select.Option
<SelectOption
v-for="option in deviceStatusChangeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</Select.Option>
</SelectOption>
</Select>
</Form.Item>
</FormItem>
</Col>
</Row>
</div>

View File

@ -15,7 +15,7 @@ import { IconifyIcon } from '@vben/icons';
import { isEmptyVal } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Button, Input, Popover, Tag } from 'antdv-next';
import { Button, Popover, Tag, TextArea } from 'antdv-next';
/** JSON参数输入组件 - 通用版本 */
defineOptions({ name: 'JsonParamsInput' });
@ -422,7 +422,7 @@ watch(
<div class="w-full space-y-3">
<!-- JSON 输入框 -->
<div class="relative">
<Input.TextArea
<TextArea
v-model:value="paramsJson"
:rows="4"
:placeholder="placeholder"
@ -434,7 +434,7 @@ watch(
<Popover
placement="leftTop"
:overlay-style="{ width: '450px' }"
trigger="click"
:trigger="['click']"
:arrow="true"
overlay-class-name="json-params-detail-popover"
>

View File

@ -14,6 +14,7 @@ import {
Input,
InputNumber,
Select,
SelectOption,
Tag,
Tooltip,
} from 'antdv-next';
@ -209,8 +210,8 @@ watch(
placeholder="请选择布尔值"
class="w-full!"
>
<Select.Option value="true"> (true)</Select.Option>
<Select.Option value="false"> (false)</Select.Option>
<SelectOption value="true"> (true)</SelectOption>
<SelectOption value="false"> (false)</SelectOption>
</Select>
<!-- 枚举值选择 -->
@ -222,13 +223,13 @@ watch(
placeholder="请选择枚举值"
class="w-full!"
>
<Select.Option
<SelectOption
v-for="option in enumOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</Select.Option>
</SelectOption>
</Select>
<!-- 范围输入 (between 操作符) -->

View File

@ -11,7 +11,7 @@ import { IconifyIcon } from '@vben/icons';
import { getStableObjectKey } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Button, Card, Empty, Form, Select, Tag } from 'antdv-next';
import { Button, Card, Empty, FormItem, Select, SelectOption, Tag } from 'antdv-next';
import AlertConfig from '../configs/alert-config.vue';
import DeviceControlConfig from '../configs/device-control-config.vue';
@ -228,22 +228,22 @@ function onActionTypeChange(action: RuleSceneApi.Action, type: number) {
<div class="p-[16px] space-y-[16px]">
<!-- 执行类型选择 -->
<div class="w-full">
<Form.Item label="执行类型" required>
<FormItem label="执行类型" required>
<Select
:value="action.type"
@change="(value) => updateActionType(index, value as number)"
placeholder="请选择执行类型"
class="w-full"
>
<Select.Option
<SelectOption
v-for="option in getActionTypeOptions()"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</Select.Option>
</SelectOption>
</Select>
</Form.Item>
</FormItem>
</div>
<!-- 设备控制配置 -->

View File

@ -7,7 +7,7 @@ import { getDictOptions } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons';
import { useVModel } from '@vueuse/core';
import { Card, Col, Form, Input, Radio, Row } from 'antdv-next';
import { Card, Col, FormItem, Input, Radio, RadioGroup, Row, TextArea } from 'antdv-next';
import { DictTag } from '#/components/dict-tag';
@ -44,7 +44,7 @@ const formData = useVModel(props, 'modelValue', emit); // 表单数据
<div class="p-0">
<Row :gutter="24" class="mb-[24px]">
<Col :span="12">
<Form.Item label="场景名称" name="name" required>
<FormItem label="场景名称" name="name" required>
<Input
v-model:value="formData.name"
placeholder="请输入场景名称"
@ -52,11 +52,11 @@ const formData = useVModel(props, 'modelValue', emit); // 表单数据
show-word-limit
allow-clear
/>
</Form.Item>
</FormItem>
</Col>
<Col :span="12">
<Form.Item label="场景状态" name="status" required>
<Radio.Group v-model:value="formData.status">
<FormItem label="场景状态" name="status" required>
<RadioGroup v-model:value="formData.status">
<Radio
v-for="(dict, index) in getDictOptions(
DICT_TYPE.COMMON_STATUS,
@ -67,12 +67,12 @@ const formData = useVModel(props, 'modelValue', emit); // 表单数据
>
{{ dict.label }}
</Radio>
</Radio.Group>
</Form.Item>
</RadioGroup>
</FormItem>
</Col>
</Row>
<Form.Item label="场景描述" name="description">
<Input.TextArea
<FormItem label="场景描述" name="description">
<TextArea
v-model:value="formData.description"
placeholder="请输入场景描述(可选)"
:rows="3"
@ -80,7 +80,7 @@ const formData = useVModel(props, 'modelValue', emit); // 表单数据
show-word-limit
resize="none"
/>
</Form.Item>
</FormItem>
</div>
</Card>
</template>

View File

@ -12,7 +12,7 @@ import { IconifyIcon } from '@vben/icons';
import { getStableObjectKey } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Button, Card, Empty, Form, Tag } from 'antdv-next';
import { Button, Card, Empty, FormItem, Tag } from 'antdv-next';
import { CronTab } from '#/components/cron-tab';
@ -208,14 +208,14 @@ onMounted(() => {
<div
class="p-[16px] rounded-[6px] border border-primary bg-background"
>
<Form.Item label="CRON 表达式" required>
<FormItem label="CRON 表达式" required>
<CronTab
:model-value="triggerItem.cronExpression || '0 0 12 * * ?'"
@update:model-value="
(value) => updateTriggerCronConfig(index, value)
"
/>
</Form.Item>
</FormItem>
</div>
<!-- 附加条件组配置 -->

View File

@ -4,7 +4,7 @@ import { ref, watch } from 'vue';
import { DEVICE_SELECTOR_OPTIONS, DICT_TYPE } from '@vben/constants';
import { Select } from 'antdv-next';
import { Select, SelectOption } from 'antdv-next';
import { getDeviceListByProductId } from '#/api/iot/device/device';
import { DictTag } from '#/components/dict-tag';
@ -93,7 +93,7 @@ watch(
:loading="deviceLoading"
:disabled="!productId"
>
<Select.Option
<SelectOption
v-for="device in deviceList"
:key="device.id"
:label="device.deviceName"
@ -112,6 +112,6 @@ watch(
<DictTag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="device.state" />
</div>
</div>
</Select.Option>
</SelectOption>
</Select>
</template>

View File

@ -8,7 +8,7 @@ import {
} from '@vben/constants';
import { useVModel } from '@vueuse/core';
import { Select } from 'antdv-next';
import { Select, SelectOption } from 'antdv-next';
/** 操作符选择器组件 */
defineOptions({ name: 'OperatorSelector' });
@ -245,7 +245,7 @@ watch(
class="w-full"
option-label-prop="label"
>
<Select.Option
<SelectOption
v-for="operator in availableOperators"
:key="operator.value"
:label="operator.label"
@ -266,7 +266,7 @@ watch(
{{ operator.description }}
</div>
</div>
</Select.Option>
</SelectOption>
</Select>
</div>
</template>

View File

@ -4,7 +4,7 @@ import { onMounted, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { Select } from 'antdv-next';
import { Select, SelectOption } from 'antdv-next';
import { getSimpleProductList } from '#/api/iot/product/product';
import { DictTag } from '#/components/dict-tag';
@ -64,7 +64,7 @@ onMounted(() => {
option-label-prop="label"
:loading="productLoading"
>
<Select.Option
<SelectOption
v-for="product in productList"
:key="product.id"
:label="product.name"
@ -81,6 +81,6 @@ onMounted(() => {
</div>
<DictTag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" />
</div>
</Select.Option>
</SelectOption>
</Select>
</template>

View File

@ -16,7 +16,7 @@ import {
import { IconifyIcon } from '@vben/icons';
import { useVModel } from '@vueuse/core';
import { Button, Popover, Select, Tag } from 'antdv-next';
import { Button, Popover, Select, SelectOptGroup, SelectOption, Tag } from 'antdv-next';
import { getThingModelTSLByProductId } from '#/api/iot/thingmodel';
@ -219,12 +219,12 @@ watch(
option-label-prop="label"
:loading="loading"
>
<Select.OptGroup
<SelectOptGroup
v-for="group in propertyGroups"
:key="group.label"
:label="group.label"
>
<Select.Option
<SelectOption
v-for="property in group.options"
:key="property.identifier"
:label="property.name"
@ -240,8 +240,8 @@ watch(
{{ property.identifier }}
</Tag>
</div>
</Select.Option>
</Select.OptGroup>
</SelectOption>
</SelectOptGroup>
</Select>
<!-- 属性详情弹出层 -->
@ -249,7 +249,7 @@ watch(
v-if="selectedProperty"
placement="rightTop"
:overlay-style="{ width: '350px' }"
trigger="click"
:trigger="['click']"
:arrow="true"
overlay-class-name="property-detail-popover"
>

View File

@ -5,7 +5,7 @@ import type { Ref } from 'vue';
import { getDataTypeOptions, IoTDataSpecsDataTypeEnum } from '@vben/constants';
import { useVModel } from '@vueuse/core';
import { Form, Input, Radio } from 'antdv-next';
import { FormItem, Input, Radio, RadioGroup } from 'antdv-next';
import { ThingModelFormRules } from '#/api/iot/thingmodel';
@ -34,12 +34,12 @@ function handleChange(e: any) {
</script>
<template>
<Form.Item
<FormItem
:name="['property', 'dataSpecs', 'childDataType']"
:rules="ThingModelFormRules.childDataType"
label="元素类型"
>
<Radio.Group v-model:value="dataSpecs.childDataType" @change="handleChange">
<RadioGroup v-model:value="dataSpecs.childDataType" @change="handleChange">
<Radio
v-for="item in childDataTypeOptions"
:key="item.value"
@ -48,9 +48,9 @@ function handleChange(e: any) {
>
{{ `${item.value}(${item.label})` }}
</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
</RadioGroup>
</FormItem>
<FormItem
:name="['property', 'dataSpecs', 'size']"
:rules="ThingModelFormRules.size"
label="元素个数"
@ -59,7 +59,7 @@ function handleChange(e: any) {
v-model:value="dataSpecs.size"
placeholder="请输入数组中的元素个数"
/>
</Form.Item>
</FormItem>
<!-- Struct 型配置-->
<ThingModelStructDataSpecs
v-if="dataSpecs.childDataType === IoTDataSpecsDataTypeEnum.STRUCT"

View File

@ -6,7 +6,7 @@ import { IoTDataSpecsDataTypeEnum } from '@vben/constants';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Button, Form, Input, message } from 'antdv-next';
import { Button, FormItem, Input, message } from 'antdv-next';
import { buildIdentifierLikeNameValidator } from '#/api/iot/thingmodel';
@ -84,7 +84,7 @@ function validateEnumList(_rule: any, _value: any, callback: any) {
</script>
<template>
<Form.Item
<FormItem
:rules="[{ validator: validateEnumList, trigger: 'change' }]"
label="枚举项"
>
@ -98,7 +98,7 @@ function validateEnumList(_rule: any, _value: any, callback: any) {
:key="index"
class="mb-[5px] flex items-center justify-between"
>
<Form.Item
<FormItem
:name="['property', 'dataSpecsList', index, 'value']"
:rules="[
{ required: true, message: '枚举值不能为空', trigger: 'blur' },
@ -110,9 +110,9 @@ function validateEnumList(_rule: any, _value: any, callback: any) {
v-model:value="item.value"
placeholder="请输入枚举值如「0」"
/>
</Form.Item>
</FormItem>
<span class="mx-2">~</span>
<Form.Item
<FormItem
:name="['property', 'dataSpecsList', index, 'name']"
:rules="[
{ required: true, message: '枚举描述不能为空', trigger: 'blur' },
@ -121,14 +121,14 @@ function validateEnumList(_rule: any, _value: any, callback: any) {
class="mb-0 flex-1"
>
<Input v-model:value="item.name" placeholder="对该枚举项的描述" />
</Form.Item>
</FormItem>
<Button class="ml-2.5" type="link" @click="deleteEnum(index)">
删除
</Button>
</div>
<Button type="link" @click="addEnum">+ </Button>
</div>
</Form.Item>
</FormItem>
</template>
<style lang="scss" scoped>

View File

@ -10,7 +10,7 @@ import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { useVModel } from '@vueuse/core';
import { Form, Input, Select } from 'antdv-next';
import { FormItem, Input, Select, SelectOption } from 'antdv-next';
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
@ -87,9 +87,9 @@ function validateStep(_rule: any, _value: any, callback: any) {
</script>
<template>
<Form.Item label="取值范围">
<FormItem label="取值范围">
<div class="flex items-center justify-between">
<Form.Item
<FormItem
:name="['property', 'dataSpecs', 'min']"
:rules="[
{ required: true, message: '最小值不能为空', trigger: 'blur' },
@ -98,9 +98,9 @@ function validateStep(_rule: any, _value: any, callback: any) {
class="mb-0 flex-1"
>
<Input v-model:value="dataSpecs.min" placeholder="请输入最小值" />
</Form.Item>
</FormItem>
<span class="mx-2">~</span>
<Form.Item
<FormItem
:name="['property', 'dataSpecs', 'max']"
:rules="[
{ required: true, message: '最大值不能为空', trigger: 'blur' },
@ -109,10 +109,10 @@ function validateStep(_rule: any, _value: any, callback: any) {
class="mb-0 flex-1"
>
<Input v-model:value="dataSpecs.max" placeholder="请输入最大值" />
</Form.Item>
</FormItem>
</div>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['property', 'dataSpecs', 'step']"
:rules="[
{ required: true, message: '步长不能为空', trigger: 'blur' },
@ -121,8 +121,8 @@ function validateStep(_rule: any, _value: any, callback: any) {
label="步长"
>
<Input v-model:value="dataSpecs.step" placeholder="请输入步长" />
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:name="['property', 'dataSpecs', 'unit']"
:rules="[{ required: true, message: '请选择单位', trigger: 'change' }]"
label="单位"
@ -134,15 +134,15 @@ function validateStep(_rule: any, _value: any, callback: any) {
class="w-full"
@change="unitChange"
>
<Select.Option
<SelectOption
v-for="(item, index) in unitOptions"
:key="index"
:value="`${item.label}-${item.value}`"
>
{{ `${item.label}-${item.value}` }}
</Select.Option>
</SelectOption>
</Select>
</Form.Item>
</FormItem>
</template>
<style lang="scss" scoped>

View File

@ -8,7 +8,7 @@ import { IoTDataSpecsDataTypeEnum } from '@vben/constants';
import { cloneDeep, isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Button, Divider, Form, Input } from 'antdv-next';
import { Button, Divider, Form, FormItem, Input } from 'antdv-next';
import { ThingModelFormRules } from '#/api/iot/thingmodel';
@ -130,7 +130,7 @@ onMounted(() => {
</script>
<template>
<Form.Item
<FormItem
:name="fieldPath"
:rules="[{ validator: validateStructSpecsList, trigger: 'change' }]"
label="属性对象"
@ -150,7 +150,7 @@ onMounted(() => {
</div>
</div>
<Button type="link" @click="openStructForm(null)">+ </Button>
</Form.Item>
</FormItem>
<!-- 结构体参数表单 -->
<Modal class="w-2/5" title="结构体参数">
@ -161,16 +161,16 @@ onMounted(() => {
:wrapper-col="{ span: 18 }"
class="mx-4"
>
<Form.Item :rules="ThingModelFormRules.name" label="参数名称" name="name">
<FormItem :rules="ThingModelFormRules.name" label="参数名称" name="name">
<Input v-model:value="formData.name" placeholder="请输入参数名称" />
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:rules="ThingModelFormRules.identifier"
label="标识符"
name="identifier"
>
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
</Form.Item>
</FormItem>
<!-- 属性配置 -->
<ThingModelProperty v-model="formData.property" is-struct-data-specs />
</Form>

View File

@ -11,7 +11,7 @@ import {
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Form, Radio } from 'antdv-next';
import { FormItem, Radio, RadioGroup } from 'antdv-next';
import { ThingModelFormRules } from '#/api/iot/thingmodel';
@ -32,12 +32,12 @@ watch(
</script>
<template>
<Form.Item
<FormItem
:name="['event', 'type']"
:rules="ThingModelFormRules.eventType"
label="事件类型"
>
<Radio.Group v-model:value="thingModelEvent.type">
<RadioGroup v-model:value="thingModelEvent.type">
<Radio
v-for="eventType in Object.values(IoTThingModelEventTypeEnum)"
:key="eventType.value"
@ -45,14 +45,14 @@ watch(
>
{{ eventType.label }}
</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="输出参数">
</RadioGroup>
</FormItem>
<FormItem label="输出参数">
<ThingModelInputOutputParam
v-model="thingModelEvent.outputParams"
:direction="IoTThingModelParamDirectionEnum.OUTPUT"
/>
</Form.Item>
</FormItem>
</template>
<style lang="scss" scoped>

View File

@ -17,7 +17,15 @@ import { getDictOptions } from '@vben/hooks';
import { $t } from '@vben/locales';
import { cloneDeep, isEmpty } from '@vben/utils';
import { Form, Input, message, Radio } from 'antdv-next';
import {
Form,
FormItem,
Input,
message,
RadioButton,
RadioGroup,
TextArea,
} from 'antdv-next';
import {
createThingModel,
@ -197,30 +205,27 @@ function removeDataSpecs(val: any) {
:wrapper-col="{ span: 18 }"
class="mx-4"
>
<Form.Item :rules="ThingModelFormRules.type" label="功能类型" name="type">
<Radio.Group v-model:value="formData.type">
<Radio.Button
<FormItem :rules="ThingModelFormRules.type" label="功能类型" name="type">
<RadioGroup v-model:value="formData.type">
<RadioButton
v-for="dict in getDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)"
:key="String(dict.value)"
:value="Number(dict.value)"
>
{{ dict.label }}
</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item :rules="ThingModelFormRules.name" label="功能名称" name="name">
</RadioButton>
</RadioGroup>
</FormItem>
<FormItem :rules="ThingModelFormRules.name" label="功能名称" name="name">
<Input v-model:value="formData.name" placeholder="请输入功能名称" />
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:rules="ThingModelFormRules.identifier"
label="标识符"
name="identifier"
>
<Input
v-model:value="formData.identifier"
placeholder="请输入标识符"
/>
</Form.Item>
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
</FormItem>
<!-- 属性配置 -->
<ThingModelProperty
v-if="formData.type === IoTThingModelTypeEnum.PROPERTY"
@ -236,14 +241,14 @@ function removeDataSpecs(val: any) {
v-if="formData.type === IoTThingModelTypeEnum.EVENT"
v-model="formData.event"
/>
<Form.Item label="描述" name="description">
<Input.TextArea
<FormItem label="描述" name="description">
<TextArea
v-model:value="formData.description"
:maxlength="200"
:rows="3"
placeholder="请输入物模型描述"
/>
</Form.Item>
</FormItem>
</Form>
</Modal>
</template>

View File

@ -8,7 +8,7 @@ import { IoTDataSpecsDataTypeEnum } from '@vben/constants';
import { cloneDeep, isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Button, Divider, Form, Input, message } from 'antdv-next';
import { Button, Divider, Form, FormItem, Input, message } from 'antdv-next';
import { ThingModelFormRules } from '#/api/iot/thingmodel';
@ -146,16 +146,16 @@ function deleteParamItem(index: number) {
:wrapper-col="{ span: 18 }"
class="mx-4"
>
<Form.Item :rules="ThingModelFormRules.name" label="参数名称" name="name">
<FormItem :rules="ThingModelFormRules.name" label="参数名称" name="name">
<Input v-model:value="formData.name" placeholder="请输入参数名称" />
</Form.Item>
<Form.Item
</FormItem>
<FormItem
:rules="ThingModelFormRules.identifier"
label="标识符"
name="identifier"
>
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
</Form.Item>
</FormItem>
<!-- 属性配置 -->
<ThingModelProperty v-model="formData.property" is-params />
</Form>

View File

@ -14,7 +14,7 @@ import {
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Form, Input, Radio, Select } from 'antdv-next';
import { FormItem, Input, Radio, RadioGroup, Select, SelectOption } from 'antdv-next';
import { ThingModelFormRules, validateBoolName } from '#/api/iot/thingmodel';
@ -102,7 +102,7 @@ if (!props.isStructDataSpecs && !props.isParams) {
</script>
<template>
<Form.Item
<FormItem
:name="['property', 'dataType']"
:rules="[{ required: true, message: '请选择数据类型', trigger: 'change' }]"
label="数据类型"
@ -113,15 +113,15 @@ if (!props.isStructDataSpecs && !props.isParams) {
@change="handleChange"
>
<!-- ARRAY STRUCT 类型数据相互嵌套时最多支持递归嵌套 2 父和子 -->
<Select.Option
<SelectOption
v-for="option in dataTypeOptions"
:key="option.value"
:value="option.value"
>
{{ `${option.value}(${option.label})` }}
</Select.Option>
</SelectOption>
</Select>
</Form.Item>
</FormItem>
<!-- 数值型配置 -->
<ThingModelNumberDataSpecs
v-if="NUMERIC_TYPES.has(property.dataType || '')"
@ -133,7 +133,7 @@ if (!props.isStructDataSpecs && !props.isParams) {
v-model="property.dataSpecsList"
/>
<!-- 布尔型配置 -->
<Form.Item
<FormItem
v-if="property.dataType === IoTDataSpecsDataTypeEnum.BOOL"
label="布尔值"
>
@ -141,7 +141,7 @@ if (!props.isStructDataSpecs && !props.isParams) {
<div class="mb-[5px] flex w-full items-center justify-start">
<span>{{ item.value }}</span>
<span class="mx-2">-</span>
<Form.Item
<FormItem
:name="['property', 'dataSpecsList', index, 'name']"
:rules="[
{ required: true, message: '布尔描述不能为空', trigger: 'blur' },
@ -154,12 +154,12 @@ if (!props.isStructDataSpecs && !props.isParams) {
:placeholder="`如:${item.value === 0 ? '关' : '开'}`"
class="!w-[255px]"
/>
</Form.Item>
</FormItem>
</div>
</template>
</Form.Item>
</FormItem>
<!-- 文本型配置 -->
<Form.Item
<FormItem
v-if="property.dataType === IoTDataSpecsDataTypeEnum.TEXT"
:name="['property', 'dataSpecs', 'length']"
:rules="ThingModelFormRules.length"
@ -172,9 +172,9 @@ if (!props.isStructDataSpecs && !props.isParams) {
>
<template #addonAfter>字节</template>
</Input>
</Form.Item>
</FormItem>
<!-- 时间型配置 -->
<Form.Item
<FormItem
v-if="property.dataType === IoTDataSpecsDataTypeEnum.DATE"
label="时间格式"
name="date"
@ -184,7 +184,7 @@ if (!props.isStructDataSpecs && !props.isParams) {
disabled
placeholder="String 类型的 UTC 时间戳(毫秒)"
/>
</Form.Item>
</FormItem>
<!-- 数组型配置-->
<ThingModelArrayDataSpecs
v-if="property.dataType === IoTDataSpecsDataTypeEnum.ARRAY"
@ -195,13 +195,13 @@ if (!props.isStructDataSpecs && !props.isParams) {
v-if="property.dataType === IoTDataSpecsDataTypeEnum.STRUCT"
v-model="property.dataSpecsList"
/>
<Form.Item
<FormItem
v-if="!isStructDataSpecs && !isParams"
:name="['property', 'accessMode']"
:rules="ThingModelFormRules.accessMode"
label="读写类型"
>
<Radio.Group v-model:value="property.accessMode">
<RadioGroup v-model:value="property.accessMode">
<Radio
v-for="accessMode in Object.values(IoTThingModelAccessModeEnum)"
:key="accessMode.value"
@ -209,8 +209,8 @@ if (!props.isStructDataSpecs && !props.isParams) {
>
{{ accessMode.label }}
</Radio>
</Radio.Group>
</Form.Item>
</RadioGroup>
</FormItem>
</template>
<style lang="scss" scoped>

View File

@ -11,7 +11,7 @@ import {
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Form, Radio } from 'antdv-next';
import { FormItem, Radio, RadioGroup } from 'antdv-next';
import { ThingModelFormRules } from '#/api/iot/thingmodel';
@ -43,12 +43,12 @@ function getParamIdentifiers(params?: any[]) {
</script>
<template>
<Form.Item
<FormItem
:name="['service', 'callType']"
:rules="ThingModelFormRules.callType"
label="调用方式"
>
<Radio.Group v-model:value="service.callType">
<RadioGroup v-model:value="service.callType">
<Radio
v-for="callType in Object.values(IoTThingModelServiceCallTypeEnum)"
:key="callType.value"
@ -56,22 +56,22 @@ function getParamIdentifiers(params?: any[]) {
>
{{ callType.label }}
</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="输入参数">
</RadioGroup>
</FormItem>
<FormItem label="输入参数">
<ThingModelInputOutputParam
v-model="service.inputParams"
:direction="IoTThingModelParamDirectionEnum.INPUT"
:existing-identifiers="getParamIdentifiers(service.outputParams)"
/>
</Form.Item>
<Form.Item label="输出参数">
</FormItem>
<FormItem label="输出参数">
<ThingModelInputOutputParam
v-model="service.outputParams"
:direction="IoTThingModelParamDirectionEnum.OUTPUT"
:existing-identifiers="getParamIdentifiers(service.inputParams)"
/>
</Form.Item>
</FormItem>
</template>
<style lang="scss" scoped>

View File

@ -8,7 +8,7 @@ import { computed, inject, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { IOT_PROVIDE_KEY } from '@vben/constants';
import { Radio, TextArea } from 'antdv-next';
import { Radio, RadioButton, RadioGroup, TextArea } from 'antdv-next';
import { getThingModelTSLByProductId } from '#/api/iot/thingmodel';
@ -56,10 +56,10 @@ watch(tslString, (newValue) => {
<Modal :footer="false" class="w-3/5" title="物模型 TSL">
<div class="mx-4">
<div class="mb-4">
<Radio.Group v-model:value="viewMode" size="small">
<Radio.Button value="view">代码视图</Radio.Button>
<Radio.Button value="editor">编辑器视图</Radio.Button>
</Radio.Group>
<RadioGroup v-model:value="viewMode" size="small">
<RadioButton value="view">代码视图</RadioButton>
<RadioButton value="editor">编辑器视图</RadioButton>
</RadioGroup>
</div>
<!-- 代码视图只读展示pre / code 必须紧贴避免显示出空白 -->
<div

View File

@ -2,7 +2,7 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MallSpuApi } from '#/api/mall/product/spu';
import { onMounted, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { confirm, DocAlert, Page } from '@vben/common-ui';
@ -53,6 +53,12 @@ const tabsData = ref([
count: 0,
},
]);
const spuTabItems = computed(() =>
tabsData.value.map((item) => ({
key: String(item.type),
label: `${item.name} (${item.count})`,
})),
);
/** 刷新表格 */
async function handleRefresh() {
@ -208,13 +214,7 @@ onMounted(async () => {
<Grid>
<template #toolbar-actions>
<Tabs @change="onChangeTab" class="w-full">
<Tabs.TabPane
v-for="item in tabsData"
:key="item.type"
:tab="`${item.name} (${item.count})`"
/>
</Tabs>
<Tabs :items="spuTabItems" @change="onChangeTab" class="w-full" />
</template>
<template #toolbar-tools>
<TableAction

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { Space } from 'antdv-next';
import { SpaceCompact } from 'antdv-next';
/**
* 垂直按钮组
@ -10,8 +10,8 @@ defineOptions({ name: 'VerticalSpace' });
<template>
<Space v-bind="$attrs">
<Space.Compact orientation="vertical">
<SpaceCompact orientation="vertical">
<slot></slot>
</Space.Compact>
</SpaceCompact>
</Space>
</template>

View File

@ -24,7 +24,7 @@ function handleSelect(item: Emoji) {
</script>
<template>
<Popover placement="top" trigger="click">
<Popover placement="top" :trigger="['click']">
<template #content>
<!-- TODO: antdv-next 暂不支持 List 组件后续组件库会新增 Listy 组件替代 List -->
<div class="h-[300px] w-[500px] overflow-auto">

View File

@ -15,6 +15,11 @@ import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
const activeTab = ref<string>('type');
const calendarTabItems = [
{ key: 'type', label: '按分类', forceRender: true },
{ key: 'team', label: '按班组', forceRender: true },
{ key: 'user', label: '按个人', forceRender: true },
];
</script>
<template>
@ -26,16 +31,12 @@ const activeTab = ref<string>('type');
/>
</template>
<div class="bg-card rounded-md p-3">
<Tabs v-model:active-key="activeTab" type="card">
<Tabs.TabPane key="type" tab="按分类" force-render>
<TypeView />
</Tabs.TabPane>
<Tabs.TabPane key="team" tab="按班组" force-render>
<TeamView />
</Tabs.TabPane>
<Tabs.TabPane key="user" tab="按个人" force-render>
<UserView />
</Tabs.TabPane>
<Tabs v-model:active-key="activeTab" :items="calendarTabItems" type="card">
<template #contentRender="{ item }">
<TypeView v-if="item.key === 'type'" />
<TeamView v-else-if="item.key === 'team'" />
<UserView v-else-if="item.key === 'user'" />
</template>
</Tabs>
</div>
</Page>

View File

@ -21,6 +21,10 @@ import PlanTeamList from './team-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create'); //
const subTabsName = ref('shift'); //
const planTabItems = [
{ key: 'shift', label: '班次' },
{ key: 'team', label: '班组' },
];
const formData = ref<MesCalPlanApi.Plan>();
const isDetail = computed(() => formType.value === 'detail'); //
const canConfirm = computed(
@ -131,14 +135,21 @@ const [Modal, modalApi] = useVbenModal({
<Tabs
v-if="formType !== 'create' && formData?.id"
v-model:active-key="subTabsName"
:items="planTabItems"
class="mx-4 mt-4"
>
<Tabs.TabPane key="shift" tab="班次">
<ShiftList :form-type="formType" :plan-id="formData.id" />
</Tabs.TabPane>
<Tabs.TabPane key="team" tab="班组">
<PlanTeamList :form-type="formType" :plan-id="formData.id" />
</Tabs.TabPane>
<template #contentRender="{ item }">
<ShiftList
v-if="item.key === 'shift'"
:form-type="formType"
:plan-id="formData.id"
/>
<PlanTeamList
v-else-if="item.key === 'team'"
:form-type="formType"
:plan-id="formData.id"
/>
</template>
</Tabs>
<template #prepend-footer>
<div class="flex flex-auto items-center">

View File

@ -20,6 +20,7 @@ import MemberList from './member-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create'); //
const subTabsName = ref('member'); //
const teamTabItems = [{ key: 'member', label: '班组成员' }];
const formData = ref<MesCalTeamApi.Team>();
const isDetail = computed(() => formType.value === 'detail'); //
const getTitle = computed(() => {
@ -107,11 +108,16 @@ const [Modal, modalApi] = useVbenModal({
<Tabs
v-if="formType !== 'create' && formData?.id"
v-model:active-key="subTabsName"
:items="teamTabItems"
class="mx-4 mt-4"
>
<Tabs.TabPane key="member" tab="班组成员">
<MemberList :form-type="formType" :team-id="formData.id" />
</Tabs.TabPane>
<template #contentRender="{ item }">
<MemberList
v-if="item.key === 'member'"
:form-type="formType"
:team-id="formData.id"
/>
</template>
</Tabs>
</Modal>
</template>

View File

@ -21,6 +21,10 @@ import SubjectList from './subject-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const subTabsName = ref('machinery');
const checkPlanTabItems = [
{ key: 'machinery', label: '设备' },
{ key: 'subject', label: '项目' },
];
const formData = ref<MesDvCheckPlanApi.CheckPlan>();
const isDetail = computed(() => formType.value === 'detail');
const getTitle = computed(() => {
@ -105,14 +109,22 @@ const [Modal, modalApi] = useVbenModal({
<Tabs
v-if="formType !== 'create' && formData?.id"
v-model:active-key="subTabsName"
:items="checkPlanTabItems"
class="mx-4 mt-4"
>
<Tabs.TabPane key="machinery" tab="设备">
<MachineryList :form-type="formType" :plan-id="formData.id" />
</Tabs.TabPane>
<Tabs.TabPane key="subject" tab="项目">
<SubjectList :form-type="formType" :plan-id="formData.id" :plan-type="formData.type" />
</Tabs.TabPane>
<template #contentRender="{ item }">
<MachineryList
v-if="item.key === 'machinery'"
:form-type="formType"
:plan-id="formData.id"
/>
<SubjectList
v-else-if="item.key === 'subject'"
:form-type="formType"
:plan-id="formData.id"
:plan-type="formData.type"
/>
</template>
</Tabs>
</Modal>
</template>

View File

@ -23,6 +23,11 @@ import RepairList from './repair-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create'); //
const subTabsName = ref('check'); //
const machineryTabItems = [
{ key: 'check', label: '点检记录' },
{ key: 'mainten', label: '保养记录' },
{ key: 'repair', label: '维修记录' },
];
const formData = ref<MesDvMachineryApi.Machinery>();
const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); //
const isDetail = computed(() => formType.value === 'detail'); //
@ -116,17 +121,23 @@ const [Modal, modalApi] = useVbenModal({
<Tabs
v-if="formType !== 'create' && formData?.id"
v-model:active-key="subTabsName"
:items="machineryTabItems"
class="mx-4 mt-4"
>
<Tabs.TabPane key="check" tab="点检记录">
<CheckRecordList :machinery-id="formData.id" />
</Tabs.TabPane>
<Tabs.TabPane key="mainten" tab="保养记录">
<MaintenRecordList :machinery-id="formData.id" />
</Tabs.TabPane>
<Tabs.TabPane key="repair" tab="维修记录">
<RepairList :machinery-id="formData.id" />
</Tabs.TabPane>
<template #contentRender="{ item }">
<CheckRecordList
v-if="item.key === 'check'"
:machinery-id="formData.id"
/>
<MaintenRecordList
v-else-if="item.key === 'mainten'"
:machinery-id="formData.id"
/>
<RepairList
v-else-if="item.key === 'repair'"
:machinery-id="formData.id"
/>
</template>
</Tabs>
<template #prepend-footer>
<div class="flex flex-auto items-center">

View File

@ -20,6 +20,10 @@ import ClientProductSalesList from './product-sales-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create'); //
const subTabsName = ref('productSalesLine'); //
const clientTabItems = [
{ key: 'productSalesLine', label: '产品清单' },
{ key: 'productSales', label: '销售记录' },
];
const formData = ref<MesMdClientApi.Client>();
const isDetail = computed(() => formType.value === 'detail'); //
@ -100,14 +104,19 @@ const [Modal, modalApi] = useVbenModal({
<Tabs
v-if="formType !== 'create' && formData?.id"
v-model:active-key="subTabsName"
:items="clientTabItems"
class="mx-4 mt-4"
>
<Tabs.TabPane key="productSalesLine" tab="产品清单">
<ClientProductSalesLineList :client-id="formData.id" />
</Tabs.TabPane>
<Tabs.TabPane key="productSales" tab="销售记录">
<ClientProductSalesList :client-id="formData.id" />
</Tabs.TabPane>
<template #contentRender="{ item }">
<ClientProductSalesLineList
v-if="item.key === 'productSalesLine'"
:client-id="formData.id"
/>
<ClientProductSalesList
v-else-if="item.key === 'productSales'"
:client-id="formData.id"
/>
</template>
</Tabs>
</Modal>
</template>

View File

@ -37,6 +37,13 @@ const getTitle = computed(() => {
const currentItemOrProduct = computed(
() => formData.value?.itemOrProduct || '',
); // /
const itemTabItems = computed(() => [
{ key: 'bom', label: 'BOM 组成' },
...(formData.value?.batchFlag ? [{ key: 'batch', label: '批次属性' }] : []),
{ key: 'substitute', label: '替代品' },
{ key: 'sip', label: 'SIP' },
{ key: 'sop', label: 'SOP' },
]);
const [Form, formApi] = useVbenForm({
commonConfig: {
@ -128,27 +135,36 @@ const [Modal, modalApi] = useVbenModal({
<Tabs
v-if="formType !== 'create' && formData?.id"
v-model:active-key="subTabsName"
:items="itemTabItems"
class="mx-4 mt-4"
>
<Tabs.TabPane key="bom" tab="BOM 组成">
<ProductBomForm :form-type="formType" :item-id="formData.id" />
</Tabs.TabPane>
<Tabs.TabPane v-if="formData.batchFlag" key="batch" tab="批次属性">
<template #contentRender="{ item }">
<ProductBomForm
v-if="item.key === 'bom'"
:form-type="formType"
:item-id="formData.id"
/>
<ItemBatchConfigForm
v-else-if="item.key === 'batch'"
:form-type="formType"
:item-id="formData.id"
:item-or-product="currentItemOrProduct"
/>
</Tabs.TabPane>
<Tabs.TabPane key="substitute" tab="替代品">
<Empty description="替代品(待实现)" />
</Tabs.TabPane>
<Tabs.TabPane key="sip" tab="SIP">
<ProductSipForm :form-type="formType" :item-id="formData.id" />
</Tabs.TabPane>
<Tabs.TabPane key="sop" tab="SOP">
<ProductSopForm :form-type="formType" :item-id="formData.id" />
</Tabs.TabPane>
<Empty
v-else-if="item.key === 'substitute'"
description="替代品(待实现)"
/>
<ProductSipForm
v-else-if="item.key === 'sip'"
:form-type="formType"
:item-id="formData.id"
/>
<ProductSopForm
v-else-if="item.key === 'sop'"
:form-type="formType"
:item-id="formData.id"
/>
</template>
</Tabs>
<template #prepend-footer>
<div class="flex flex-auto items-center">

View File

@ -3,7 +3,7 @@ import type { MesMdUnitMeasureApi } from '#/api/mes/md/unitmeasure';
import { computed, onMounted, ref, watch } from 'vue';
import { Select, Tag, Tooltip } from 'antdv-next';
import { Select, SelectOption, Tag, Tooltip } from 'antdv-next';
import { getUnitMeasureSimpleList } from '#/api/mes/md/unitmeasure';
@ -106,7 +106,7 @@ onMounted(async () => {
show-search
@change="handleChange"
>
<Select.Option
<SelectOption
v-for="item in filteredList"
:key="item.id"
:item="item"
@ -116,7 +116,7 @@ onMounted(async () => {
<span>{{ item.name }}</span>
<Tag v-if="item.code" color="default">: {{ item.code }}</Tag>
</div>
</Select.Option>
</SelectOption>
</Select>
</Tooltip>
</template>

View File

@ -20,6 +20,10 @@ import VendorItemReceiptList from './item-receipt-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create'); //
const subTabsName = ref('itemReceiptLine'); //
const vendorTabItems = [
{ key: 'itemReceiptLine', label: '物料清单' },
{ key: 'itemReceipt', label: '采购记录' },
];
const formData = ref<MesMdVendorApi.Vendor>();
const isDetail = computed(() => formType.value === 'detail'); //
@ -102,14 +106,19 @@ const [Modal, modalApi] = useVbenModal({
<Tabs
v-if="formType !== 'create' && formData?.id"
v-model:active-key="subTabsName"
:items="vendorTabItems"
class="mx-4 mt-4"
>
<Tabs.TabPane key="itemReceiptLine" tab="物料清单">
<VendorItemReceiptLineList :vendor-id="formData.id" />
</Tabs.TabPane>
<Tabs.TabPane key="itemReceipt" tab="采购记录">
<VendorItemReceiptList :vendor-id="formData.id" />
</Tabs.TabPane>
<template #contentRender="{ item }">
<VendorItemReceiptLineList
v-if="item.key === 'itemReceiptLine'"
:vendor-id="formData.id"
/>
<VendorItemReceiptList
v-else-if="item.key === 'itemReceipt'"
:vendor-id="formData.id"
/>
</template>
</Tabs>
</Modal>
</template>

View File

@ -8,7 +8,7 @@ import { nextTick, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { formatDateTime } from '@vben/utils';
import { Descriptions, Spin } from 'antdv-next';
import { Descriptions, DescriptionsItem, Spin } from 'antdv-next';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getItemReceipt } from '#/api/mes/wm/itemreceipt';
@ -123,36 +123,36 @@ const [Modal, modalApi] = useVbenModal({
>
<Spin :spinning="loading">
<Descriptions bordered size="small" :column="3">
<Descriptions.Item label="入库单编号">
<DescriptionsItem label="入库单编号">
{{ formatEmpty(receipt?.code) }}
</Descriptions.Item>
<Descriptions.Item label="入库单名称">
</DescriptionsItem>
<DescriptionsItem label="入库单名称">
{{ formatEmpty(receipt?.name) }}
</Descriptions.Item>
<Descriptions.Item label="入库日期">
</DescriptionsItem>
<DescriptionsItem label="入库日期">
{{ formatDate(receipt?.receiptDate) }}
</Descriptions.Item>
<Descriptions.Item label="到货通知单">
</DescriptionsItem>
<DescriptionsItem label="到货通知单">
{{ formatEmpty(receipt?.noticeCode) }}
</Descriptions.Item>
<Descriptions.Item label="供应商">
</DescriptionsItem>
<DescriptionsItem label="供应商">
{{ formatEmpty(receipt?.vendorName) }}
</Descriptions.Item>
<Descriptions.Item label="采购订单号">
</DescriptionsItem>
<DescriptionsItem label="采购订单号">
{{ formatEmpty(receipt?.purchaseOrderCode) }}
</Descriptions.Item>
<Descriptions.Item label="仓库">
</DescriptionsItem>
<DescriptionsItem label="仓库">
{{ formatEmpty(receipt?.warehouseName) }}
</Descriptions.Item>
<Descriptions.Item label="库区">
</DescriptionsItem>
<DescriptionsItem label="库区">
{{ formatEmpty(receipt?.locationName) }}
</Descriptions.Item>
<Descriptions.Item label="库位">
</DescriptionsItem>
<DescriptionsItem label="库位">
{{ formatEmpty(receipt?.areaName) }}
</Descriptions.Item>
<Descriptions.Item label="备注" :span="3">
</DescriptionsItem>
<DescriptionsItem label="备注" :span="3">
{{ formatEmpty(receipt?.remark) }}
</Descriptions.Item>
</DescriptionsItem>
</Descriptions>
<div class="mt-4">
<Grid table-title="" />

View File

@ -3,7 +3,7 @@ import type { MesMdWorkshopApi } from '#/api/mes/md/workstation/workshop';
import { computed, onMounted, ref, watch } from 'vue';
import { Select, Tag, Tooltip } from 'antdv-next';
import { Select, SelectOption, Tag, Tooltip } from 'antdv-next';
import { getWorkshopSimpleList } from '#/api/mes/md/workstation/workshop';
@ -101,7 +101,7 @@ onMounted(async () => {
show-search
@change="handleChange"
>
<Select.Option
<SelectOption
v-for="item in allList"
:key="item.id"
:item="item"
@ -111,7 +111,7 @@ onMounted(async () => {
<span>{{ item.name }}</span>
<Tag v-if="item.code" color="default">{{ item.code }}</Tag>
</div>
</Select.Option>
</SelectOption>
</Select>
</Tooltip>
</template>

View File

@ -27,6 +27,11 @@ import WorkerList from './worker-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create'); //
const subTabsName = ref('machine'); //
const workstationTabItems = [
{ key: 'machine', label: '设备资源' },
{ key: 'tool', label: '工装夹具' },
{ key: 'worker', label: '人力资源' },
];
const formData = ref<MesMdWorkstationApi.Workstation>();
const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); //
@ -127,17 +132,26 @@ const [Modal, modalApi] = useVbenModal({
<Tabs
v-if="formType !== 'create' && formData?.id"
v-model:active-key="subTabsName"
:items="workstationTabItems"
class="mx-4 mt-4"
>
<Tabs.TabPane key="machine" tab="设备资源">
<MachineList :form-type="formType" :workstation-id="formData.id" />
</Tabs.TabPane>
<Tabs.TabPane key="tool" tab="工装夹具">
<ToolList :form-type="formType" :workstation-id="formData.id" />
</Tabs.TabPane>
<Tabs.TabPane key="worker" tab="人力资源">
<WorkerList :form-type="formType" :workstation-id="formData.id" />
</Tabs.TabPane>
<template #contentRender="{ item }">
<MachineList
v-if="item.key === 'machine'"
:form-type="formType"
:workstation-id="formData.id"
/>
<ToolList
v-else-if="item.key === 'tool'"
:form-type="formType"
:workstation-id="formData.id"
/>
<WorkerList
v-else-if="item.key === 'worker'"
:form-type="formType"
:workstation-id="formData.id"
/>
</template>
</Tabs>
<template #prepend-footer>
<div class="flex flex-auto items-center">

View File

@ -5,7 +5,7 @@ import { computed, onMounted, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { Select } from 'antdv-next';
import { Select, SelectOption } from 'antdv-next';
import { getAndonConfigList } from '#/api/mes/pro/andon/config';
import DictTag from '#/components/dict-tag/dict-tag.vue';
@ -73,7 +73,7 @@ onMounted(async () => {
show-search
@change="handleChange"
>
<Select.Option
<SelectOption
v-for="item in allList"
:key="item.id"
:item="item"
@ -83,6 +83,6 @@ onMounted(async () => {
<span>{{ item.reason }}</span>
<DictTag :type="DICT_TYPE.MES_PRO_ANDON_LEVEL" :value="item.level" />
</div>
</Select.Option>
</SelectOption>
</Select>
</template>

View File

@ -53,6 +53,10 @@ const showSubTabs = computed(
formData.value?.status !== MesProFeedbackStatusEnum.PREPARE &&
formData.value?.status !== MesProFeedbackStatusEnum.APPROVING,
);
const feedbackTabItems = [
{ key: 'itemConsume', label: 'BOM 物资消耗' },
{ key: 'productProduce', label: '产品产出' },
];
const getTitle = computed(() => {
if (formType.value === 'detail') {
return $t('ui.actionTitle.view', ['生产报工']);
@ -274,15 +278,20 @@ const [Modal, modalApi] = useVbenModal({
<Tabs
v-if="showSubTabs"
v-model:active-key="subTabsName"
:items="feedbackTabItems"
type="card"
class="mx-4 mt-2"
>
<Tabs.TabPane key="itemConsume" tab="BOM 物资消耗">
<ItemConsumeList :feedback-id="formData!.id!" />
</Tabs.TabPane>
<Tabs.TabPane key="productProduce" tab="产品产出">
<ProductProduceList :feedback-id="formData!.id!" />
</Tabs.TabPane>
<template #contentRender="{ item }">
<ItemConsumeList
v-if="item.key === 'itemConsume'"
:feedback-id="formData!.id!"
/>
<ProductProduceList
v-else-if="item.key === 'productProduce'"
:feedback-id="formData!.id!"
/>
</template>
</Tabs>
<template #prepend-footer>
<div class="flex flex-auto items-center justify-end gap-2">

View File

@ -3,7 +3,7 @@ import type { MesProProcessApi } from '#/api/mes/pro/process';
import { computed, onMounted, ref, watch } from 'vue';
import { Select, Tag, Tooltip } from 'antdv-next';
import { Select, SelectOption, Tag, Tooltip } from 'antdv-next';
import { getProcessSimpleList } from '#/api/mes/pro/process';
@ -98,7 +98,7 @@ onMounted(async () => {
show-search
@change="handleChange"
>
<Select.Option
<SelectOption
v-for="item in allList"
:key="item.id"
:item="item"
@ -108,7 +108,7 @@ onMounted(async () => {
<span>{{ item.name }}</span>
<Tag v-if="item.code" color="default">{{ item.code }}</Tag>
</div>
</Select.Option>
</SelectOption>
</Select>
</Tooltip>
</template>

View File

@ -34,6 +34,10 @@ const formType = ref<FormType>('create');
const formData = ref<MesProWorkOrderApi.WorkOrder>();
const originalSnapshot = ref(''); //
const subTabsName = ref('bom'); // Tab
const workOrderTabItems = [
{ key: 'bom', label: '工单 BOM' },
{ key: 'item', label: '物料需求' },
];
const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); //
const isEditable = computed(() => //
@ -252,18 +256,24 @@ const [Modal, modalApi] = useVbenModal({
<Form class="mx-4" />
<!-- BOM / 物料需求 Tab非新建态展示 -->
<template v-if="formData?.id">
<Tabs v-model:active-key="subTabsName" class="mx-4">
<Tabs.TabPane key="bom" tab="工单 BOM">
<Tabs
v-model:active-key="subTabsName"
:items="workOrderTabItems"
class="mx-4"
>
<template #contentRender="{ item }">
<BomList
v-if="item.key === 'bom'"
:form-type="formType"
:work-order="formData"
:work-order-id="formData.id"
@generate-work-order="handleGenerateWorkOrder"
/>
</Tabs.TabPane>
<Tabs.TabPane key="item" tab="物料需求">
<ItemList :work-order-id="formData.id" />
</Tabs.TabPane>
<ItemList
v-else-if="item.key === 'item'"
:work-order-id="formData.id"
/>
</template>
</Tabs>
</template>
<template #prepend-footer>

View File

@ -14,6 +14,10 @@ import TraceList from './trace-list.vue';
const detailData = ref<MesWmBatchApi.Batch>(); //
const subTabsName = ref<'backward' | 'forward'>('forward'); //
const traceTabItems = [
{ key: 'forward', label: '向前追溯' },
{ key: 'backward', label: '向后追溯' },
];
const [Descriptions] = useDescription({
bordered: true,
@ -44,15 +48,22 @@ const [Modal, modalApi] = useVbenModal({
<Tabs
v-if="detailData?.code"
v-model:active-key="subTabsName"
:items="traceTabItems"
class="mx-4 mt-4"
type="card"
>
<Tabs.TabPane key="forward" tab="向前追溯">
<TraceList :batch-code="detailData.code" direction="forward" />
</Tabs.TabPane>
<Tabs.TabPane key="backward" tab="向后追溯">
<TraceList :batch-code="detailData.code" direction="backward" />
</Tabs.TabPane>
<template #contentRender="{ item }">
<TraceList
v-if="item.key === 'forward'"
:batch-code="detailData.code"
direction="forward"
/>
<TraceList
v-else-if="item.key === 'backward'"
:batch-code="detailData.code"
direction="backward"
/>
</template>
</Tabs>
</Modal>
</template>

View File

@ -12,6 +12,7 @@ import { getDictOptions } from '@vben/hooks';
import {
Form as AForm,
Divider,
FormItem,
Input,
InputNumber,
message,
@ -97,7 +98,7 @@ const [Modal, modalApi] = useVbenModal({
item.valueType === MesQcResultValueType.INTEGER
) {
submit.value =
item.valueNumber == null ? undefined : String(item.valueNumber);
item.valueNumber === null ? undefined : String(item.valueNumber);
} else {
submit.value = item.value;
}
@ -148,7 +149,7 @@ const [Modal, modalApi] = useVbenModal({
valueNumber:
(item.valueType === MesQcResultValueType.FLOAT ||
item.valueType === MesQcResultValueType.INTEGER) &&
item.value != null
item.value !== null
? Number(item.value)
: undefined,
}));
@ -178,9 +179,7 @@ const [Modal, modalApi] = useVbenModal({
:key="item.indicatorId ?? index"
class="mb-2"
>
<AForm.Item
:label="`检测项${index + 1}${item.indicatorName ?? ''}`"
>
<FormItem :label="`检测项${index + 1}${item.indicatorName ?? ''}`">
<InputNumber
v-if="
item.valueType === MesQcResultValueType.FLOAT ||
@ -210,7 +209,7 @@ const [Modal, modalApi] = useVbenModal({
placeholder="请输入文件地址"
/>
<Input v-else v-model:value="item.value" placeholder="请输入" />
</AForm.Item>
</FormItem>
</div>
</AForm>
</div>

View File

@ -8,7 +8,7 @@ import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { Button, Descriptions, message, Tabs } from 'antdv-next';
import { Button, Descriptions, DescriptionsItem, message, Tabs } from 'antdv-next';
import { useVbenForm } from '#/adapter/form';
import {
@ -27,6 +27,10 @@ const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesQcIpqcApi.Ipqc>();
const subTabsName = ref('line');
const qcTabItems = [
{ key: 'line', label: '检验项' },
{ key: 'result', label: '检测结果' },
];
const originalSnapshot = ref(''); // finish
const isDetail = computed(() => formType.value === 'detail');
const canFinish = computed(
@ -188,45 +192,46 @@ const [Modal, modalApi] = useVbenModal({
<!-- 缺陷统计只读 -->
<div v-if="formData?.id" class="mx-4 mt-4">
<Descriptions title="缺陷情况" :column="3" bordered size="small">
<Descriptions.Item label="致命缺陷数">
<DescriptionsItem label="致命缺陷数">
{{ formData.criticalQuantity ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="严重缺陷数">
</DescriptionsItem>
<DescriptionsItem label="严重缺陷数">
{{ formData.majorQuantity ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="轻微缺陷数">
</DescriptionsItem>
<DescriptionsItem label="轻微缺陷数">
{{ formData.minorQuantity ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="致命缺陷率">
</DescriptionsItem>
<DescriptionsItem label="致命缺陷率">
{{ formData.criticalRate ?? 0 }}%
</Descriptions.Item>
<Descriptions.Item label="严重缺陷率">
</DescriptionsItem>
<DescriptionsItem label="严重缺陷率">
{{ formData.majorRate ?? 0 }}%
</Descriptions.Item>
<Descriptions.Item label="轻微缺陷率">
</DescriptionsItem>
<DescriptionsItem label="轻微缺陷率">
{{ formData.minorRate ?? 0 }}%
</Descriptions.Item>
</DescriptionsItem>
</Descriptions>
</div>
<Tabs
v-if="formData?.id"
v-model:active-key="subTabsName"
:items="qcTabItems"
class="mx-4 mt-4"
>
<Tabs.TabPane key="line" tab="检验项">
<template #contentRender="{ item }">
<LineList
v-if="item.key === 'line'"
:form-type="formType"
:ipqc-id="formData.id"
@refresh="handleRefresh"
/>
</Tabs.TabPane>
<Tabs.TabPane key="result" tab="检测结果">
<QcIndicatorResultList
v-else-if="item.key === 'result'"
:qc-id="formData.id"
:qc-type="MesQcTypeEnum.IPQC"
:readonly="isDetail"
/>
</Tabs.TabPane>
</template>
</Tabs>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">

View File

@ -11,17 +11,13 @@ import { MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import {
Button,
Descriptions,
DescriptionsItem,
message,
Tabs,
} from 'antdv-next';
import { useVbenForm } from '#/adapter/form';
import {
createIqc,
finishIqc,
getIqc,
updateIqc,
} from '#/api/mes/qc/iqc';
import { createIqc, finishIqc, getIqc, updateIqc } from '#/api/mes/qc/iqc';
import { $t } from '#/locales';
import { QcIndicatorResultList } from '../../indicatorresult/components';
@ -32,6 +28,10 @@ const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesQcIqcApi.Iqc>();
const subTabsName = ref('line');
const qcTabItems = [
{ key: 'line', label: '检验项' },
{ key: 'result', label: '检测结果' },
];
const originalSnapshot = ref(''); // finish
const isDetail = computed(() => formType.value === 'detail');
const canFinish = computed(
@ -194,45 +194,46 @@ const [Modal, modalApi] = useVbenModal({
<!-- 缺陷统计只读 -->
<div v-if="formData?.id" class="mx-4 mt-4">
<Descriptions title="缺陷情况" :column="3" bordered size="small">
<Descriptions.Item label="致命缺陷数">
<DescriptionsItem label="致命缺陷数">
{{ formData.criticalQuantity ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="严重缺陷数">
</DescriptionsItem>
<DescriptionsItem label="严重缺陷数">
{{ formData.majorQuantity ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="轻微缺陷数">
</DescriptionsItem>
<DescriptionsItem label="轻微缺陷数">
{{ formData.minorQuantity ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="致命缺陷率">
</DescriptionsItem>
<DescriptionsItem label="致命缺陷率">
{{ formData.criticalRate ?? 0 }}%
</Descriptions.Item>
<Descriptions.Item label="严重缺陷率">
</DescriptionsItem>
<DescriptionsItem label="严重缺陷率">
{{ formData.majorRate ?? 0 }}%
</Descriptions.Item>
<Descriptions.Item label="轻微缺陷率">
</DescriptionsItem>
<DescriptionsItem label="轻微缺陷率">
{{ formData.minorRate ?? 0 }}%
</Descriptions.Item>
</DescriptionsItem>
</Descriptions>
</div>
<Tabs
v-if="formData?.id"
v-model:active-key="subTabsName"
:items="qcTabItems"
class="mx-4 mt-4"
>
<Tabs.TabPane key="line" tab="检验项">
<template #contentRender="{ item }">
<LineList
v-if="item.key === 'line'"
:form-type="formType"
:iqc-id="formData.id"
@refresh="handleRefresh"
/>
</Tabs.TabPane>
<Tabs.TabPane key="result" tab="检测结果">
<QcIndicatorResultList
v-else-if="item.key === 'result'"
:qc-id="formData.id"
:qc-type="MesQcTypeEnum.IQC"
:readonly="isDetail"
/>
</Tabs.TabPane>
</template>
</Tabs>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">

View File

@ -8,7 +8,7 @@ import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { Button, Descriptions, message, Tabs } from 'antdv-next';
import { Button, Descriptions, DescriptionsItem, message, Tabs } from 'antdv-next';
import { useVbenForm } from '#/adapter/form';
import {
@ -27,6 +27,10 @@ const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesQcOqcApi.Oqc>();
const subTabsName = ref('line');
const qcTabItems = [
{ key: 'line', label: '检验项' },
{ key: 'result', label: '检测结果' },
];
const originalSnapshot = ref(''); // finish
const isDetail = computed(() => formType.value === 'detail');
const canFinish = computed(
@ -192,45 +196,46 @@ const [Modal, modalApi] = useVbenModal({
<!-- 缺陷统计只读 -->
<div v-if="formData?.id" class="mx-4 mt-4">
<Descriptions title="缺陷情况" :column="3" bordered size="small">
<Descriptions.Item label="致命缺陷数">
<DescriptionsItem label="致命缺陷数">
{{ formData.criticalQuantity ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="严重缺陷数">
</DescriptionsItem>
<DescriptionsItem label="严重缺陷数">
{{ formData.majorQuantity ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="轻微缺陷数">
</DescriptionsItem>
<DescriptionsItem label="轻微缺陷数">
{{ formData.minorQuantity ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="致命缺陷率">
</DescriptionsItem>
<DescriptionsItem label="致命缺陷率">
{{ formData.criticalRate ?? 0 }}%
</Descriptions.Item>
<Descriptions.Item label="严重缺陷率">
</DescriptionsItem>
<DescriptionsItem label="严重缺陷率">
{{ formData.majorRate ?? 0 }}%
</Descriptions.Item>
<Descriptions.Item label="轻微缺陷率">
</DescriptionsItem>
<DescriptionsItem label="轻微缺陷率">
{{ formData.minorRate ?? 0 }}%
</Descriptions.Item>
</DescriptionsItem>
</Descriptions>
</div>
<Tabs
v-if="formData?.id"
v-model:active-key="subTabsName"
:items="qcTabItems"
class="mx-4 mt-4"
>
<Tabs.TabPane key="line" tab="检验项">
<template #contentRender="{ item }">
<LineList
v-if="item.key === 'line'"
:form-type="formType"
:oqc-id="formData.id"
@refresh="handleRefresh"
/>
</Tabs.TabPane>
<Tabs.TabPane key="result" tab="检测结果">
<QcIndicatorResultList
v-else-if="item.key === 'result'"
:qc-id="formData.id"
:qc-type="MesQcTypeEnum.OQC"
:readonly="isDetail"
/>
</Tabs.TabPane>
</template>
</Tabs>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">

View File

@ -8,7 +8,7 @@ import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { Button, Descriptions, message, Tabs } from 'antdv-next';
import { Button, Descriptions, DescriptionsItem, message, Tabs } from 'antdv-next';
import { useVbenForm } from '#/adapter/form';
import {
@ -27,6 +27,10 @@ const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesQcRqcApi.Rqc>();
const subTabsName = ref('line');
const qcTabItems = [
{ key: 'line', label: '检验项' },
{ key: 'result', label: '检测结果' },
];
const originalSnapshot = ref(''); // finish
const isDetail = computed(() => formType.value === 'detail');
const canFinish = computed(
@ -188,45 +192,46 @@ const [Modal, modalApi] = useVbenModal({
<!-- 缺陷统计只读 -->
<div v-if="formData?.id" class="mx-4 mt-4">
<Descriptions title="缺陷情况" :column="3" bordered size="small">
<Descriptions.Item label="致命缺陷数">
<DescriptionsItem label="致命缺陷数">
{{ formData.criticalQuantity ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="严重缺陷数">
</DescriptionsItem>
<DescriptionsItem label="严重缺陷数">
{{ formData.majorQuantity ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="轻微缺陷数">
</DescriptionsItem>
<DescriptionsItem label="轻微缺陷数">
{{ formData.minorQuantity ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="致命缺陷率">
</DescriptionsItem>
<DescriptionsItem label="致命缺陷率">
{{ formData.criticalRate ?? 0 }}%
</Descriptions.Item>
<Descriptions.Item label="严重缺陷率">
</DescriptionsItem>
<DescriptionsItem label="严重缺陷率">
{{ formData.majorRate ?? 0 }}%
</Descriptions.Item>
<Descriptions.Item label="轻微缺陷率">
</DescriptionsItem>
<DescriptionsItem label="轻微缺陷率">
{{ formData.minorRate ?? 0 }}%
</Descriptions.Item>
</DescriptionsItem>
</Descriptions>
</div>
<Tabs
v-if="formData?.id"
v-model:active-key="subTabsName"
:items="qcTabItems"
class="mx-4 mt-4"
>
<Tabs.TabPane key="line" tab="检验项">
<template #contentRender="{ item }">
<LineList
v-if="item.key === 'line'"
:form-type="formType"
:rqc-id="formData.id"
@refresh="handleRefresh"
/>
</Tabs.TabPane>
<Tabs.TabPane key="result" tab="检测结果">
<QcIndicatorResultList
v-else-if="item.key === 'result'"
:qc-id="formData.id"
:qc-type="MesQcTypeEnum.RQC"
:readonly="isDetail"
/>
</Tabs.TabPane>
</template>
</Tabs>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">

View File

@ -24,6 +24,10 @@ import ItemList from './item-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create'); // //
const subTabsName = ref('indicator'); // tab
const templateTabItems = [
{ key: 'indicator', label: '检测指标项' },
{ key: 'item', label: '产品关联' },
];
const formData = ref<MesQcTemplateApi.Template>();
const getTitle = computed(() => {
@ -105,20 +109,21 @@ const [Modal, modalApi] = useVbenModal({
<Tabs
v-if="formType !== 'create' && formData?.id"
v-model:active-key="subTabsName"
:items="templateTabItems"
class="mx-4 mt-4"
>
<Tabs.TabPane key="indicator" tab="检测指标项">
<template #contentRender="{ item }">
<IndicatorList
v-if="item.key === 'indicator'"
:readonly="formType === 'detail'"
:template-id="formData.id"
/>
</Tabs.TabPane>
<Tabs.TabPane key="item" tab="产品关联">
<ItemList
v-else-if="item.key === 'item'"
:readonly="formType === 'detail'"
:template-id="formData.id"
/>
</Tabs.TabPane>
</template>
</Tabs>
</Modal>
</template>

View File

@ -27,6 +27,10 @@ const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesWmPackageApi.Package>();
const subTabsName = ref('subPackage'); // tab
const packageTabItems = [
{ key: 'subPackage', label: '子箱' },
{ key: 'packageLine', label: '装箱清单' },
];
const originalSnapshot = ref(''); // finish
const isEditable = computed(() =>
@ -173,14 +177,21 @@ const [Modal, modalApi] = useVbenModal({
<Tabs
v-if="formData?.id"
v-model:active-key="subTabsName"
:items="packageTabItems"
class="mx-4 mt-4"
>
<Tabs.TabPane key="subPackage" tab="子箱">
<SubPackageList :editable="isEditable" :package-id="formData.id" />
</Tabs.TabPane>
<Tabs.TabPane key="packageLine" tab="装箱清单">
<PackageLineList :editable="isEditable" :package-id="formData.id" />
</Tabs.TabPane>
<template #contentRender="{ item }">
<SubPackageList
v-if="item.key === 'subPackage'"
:editable="isEditable"
:package-id="formData.id"
/>
<PackageLineList
v-else-if="item.key === 'packageLine'"
:editable="isEditable"
:package-id="formData.id"
/>
</template>
</Tabs>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">

View File

@ -49,6 +49,10 @@ const showResultTab = computed(
(!!formData.value?.status &&
formData.value.status !== MesWmStockTakingTaskStatusEnum.PREPARE),
);
const stockTakingTabItems = computed(() => [
...(showLineTab.value ? [{ key: 'lines', label: '盘点清单' }] : []),
...(showResultTab.value ? [{ key: 'results', label: '盘点结果' }] : []),
]);
const getTitle = computed(() => {
if (formType.value === 'detail') {
return $t('ui.actionTitle.view', ['盘点任务']);
@ -191,18 +195,22 @@ const [Modal, modalApi] = useVbenModal({
<Tabs
v-if="formData?.id"
v-model:active-key="subTabsName"
:items="stockTakingTabItems"
class="mx-4 mt-4"
type="card"
>
<Tabs.TabPane v-if="showLineTab" key="lines" tab="盘点清单">
<LineList :form-type="formType" :task-id="formData.id" />
</Tabs.TabPane>
<Tabs.TabPane v-if="showResultTab" key="results" tab="盘点结果">
<template #contentRender="{ item }">
<LineList
v-if="item.key === 'lines'"
:form-type="formType"
:task-id="formData.id"
/>
<ResultList
v-else-if="item.key === 'results'"
:form-type="isExecute ? 'execute' : 'detail'"
:task-id="formData.id"
/>
</Tabs.TabPane>
</template>
</Tabs>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">

View File

@ -14,6 +14,7 @@ import {
Input,
message,
Modal,
TextArea,
Upload,
} from 'antdv-next';
@ -149,7 +150,7 @@ const customRequest: UploadProps['customRequest'] = async function (options) {
/>
</FormItem>
<FormItem label="描述" name="introduction">
<Input.TextArea
<TextArea
v-model:value="uploadData.introduction"
:rows="3"
placeholder="介绍语将展示在相关播放页面,建议填写简洁明确、有信息量的内容"