60 changed files with 0 additions and 4402 deletions
@ -1,32 +0,0 @@
|
||||
import type { CommandToDeviceData, GetCloudCommandLogsParams, MessageToDeviceData } from './types' |
||||
import { defHttp } from '@/utils/http/axios' |
||||
|
||||
export function getCloudCommandLogs(params: GetCloudCommandLogsParams) { |
||||
return defHttp.get({ |
||||
url: '/cloud/logPage', |
||||
params, |
||||
}) |
||||
} |
||||
|
||||
export function getMessageContent(id: string) { |
||||
return defHttp.get({ |
||||
url: '/cloud/message', |
||||
params: { |
||||
id, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
export function sendMessageToDevice(data: MessageToDeviceData) { |
||||
return defHttp.post({ |
||||
url: '/cloud/sendMessage', |
||||
data, |
||||
}) |
||||
} |
||||
|
||||
export function sendCommandToDevice(data: CommandToDeviceData) { |
||||
return defHttp.post({ |
||||
url: '/cloud/sendCommand', |
||||
data, |
||||
}) |
||||
} |
@ -1,78 +0,0 @@
|
||||
import type { Device, DevicePropertie, GetDeviceListParams } from './types' |
||||
import type { Topic } from '@/api/product/types' |
||||
import { defHttp } from '@/utils/http/axios' |
||||
|
||||
export function getDeviceList(params: GetDeviceListParams) { |
||||
return defHttp.get<PageResult<Device>>({ |
||||
url: '/device/page', |
||||
params, |
||||
}) |
||||
} |
||||
|
||||
export function createDevice(data: Partial<Device>) { |
||||
return defHttp.post({ |
||||
url: '/device/save', |
||||
data, |
||||
}) |
||||
} |
||||
|
||||
export function updateDevice(data: Partial<Device>) { |
||||
return defHttp.post({ |
||||
url: '/device/update', |
||||
data, |
||||
}) |
||||
} |
||||
|
||||
export function deleteDevice(id: string) { |
||||
return defHttp.post({ |
||||
url: `/device/remove?id=${id}`, |
||||
}) |
||||
} |
||||
|
||||
export function getDeviceDetail(id: string) { |
||||
return defHttp.get<Device>({ |
||||
url: '/device/detail', |
||||
params: { |
||||
id, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
export function getDeviceProperties(modelId: string, deviceSn: string) { |
||||
return defHttp.get<{ |
||||
properties?: DevicePropertie[] |
||||
updateTime?: string |
||||
}>({ |
||||
url: '/device/properties', |
||||
params: { |
||||
deviceSn, |
||||
modelId, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
export function getDeviceTopicList(params: PageParam & { deviceId: string }) { |
||||
return defHttp.get<PageResult<Topic>>({ |
||||
url: '/device/topic/page', |
||||
params, |
||||
}) |
||||
} |
||||
|
||||
export function getMqttConnectParams(deviceId: string) { |
||||
return defHttp.get({ |
||||
url: '/device/mqttLinkInfo', |
||||
params: { |
||||
deviceId, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
export function getReportExample(productId: string, deviceSn: string) { |
||||
return defHttp.get({ |
||||
url: '/device/messageExample', |
||||
params: { |
||||
productId, |
||||
deviceSn, |
||||
}, |
||||
}) |
||||
} |
@ -1,46 +0,0 @@
|
||||
export interface GetDeviceListParams extends PageParam { |
||||
productId?: string |
||||
deviceSn?: string |
||||
deviceName?: string |
||||
isOnline?: BooleanFlag |
||||
} |
||||
|
||||
export interface Device { |
||||
id: string |
||||
productId: string |
||||
deviceSn: string |
||||
deviceName: string |
||||
deviceDesc: string |
||||
isOnline: BooleanFlag |
||||
} |
||||
|
||||
export interface DevicePropertie { |
||||
identifier: string |
||||
name: string |
||||
unit: string |
||||
value: string |
||||
sort: number |
||||
} |
||||
|
||||
export interface GetCloudCommandLogsParams extends PageParam { |
||||
messageType?: number |
||||
} |
||||
|
||||
export enum CloudCommandType { |
||||
Attribute = 1, |
||||
Command = 2, |
||||
Message = 3, |
||||
} |
||||
|
||||
export interface MessageToDeviceData { |
||||
deviceId: string |
||||
topic: string |
||||
message: string |
||||
} |
||||
|
||||
export interface CommandToDeviceData { |
||||
deviceId: string |
||||
modelId: string |
||||
itemId: string |
||||
value: string | number |
||||
} |
@ -1,64 +0,0 @@
|
||||
import type { DeviceGroup, GetdeviceListByGroupParams } from './types' |
||||
import { defHttp } from '@/utils/http/axios' |
||||
|
||||
export function getDeviceGroupTree() { |
||||
return defHttp.get({ |
||||
url: '/deviceGroup/tree', |
||||
}) |
||||
} |
||||
|
||||
export function getDevicegroupDetail(id: string) { |
||||
return defHttp.get({ |
||||
url: '/deviceGroup/detail', |
||||
params: { |
||||
id, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
export function getDeviceListByGroup(params: GetdeviceListByGroupParams) { |
||||
return defHttp.get({ |
||||
url: '/device/pageByGroup', |
||||
params, |
||||
}) |
||||
} |
||||
|
||||
export function createDeviceGroup(data: Partial<DeviceGroup>) { |
||||
return defHttp.post({ |
||||
url: '/deviceGroup/save', |
||||
data, |
||||
}) |
||||
} |
||||
|
||||
export function updateDeviceGroup(data: Partial<DeviceGroup>) { |
||||
return defHttp.post({ |
||||
url: '/deviceGroup/update', |
||||
data, |
||||
}) |
||||
} |
||||
|
||||
export function deleteDevicegroup(id: string) { |
||||
return defHttp.post({ |
||||
url: `/deviceGroup/remove?id=${id}`, |
||||
}) |
||||
} |
||||
|
||||
export function bindingDeviceToGroup(deviceGroupId: string, deviceIds: string) { |
||||
return defHttp.post({ |
||||
url: '/deviceGroup/bindDevice', |
||||
data: { |
||||
deviceGroupId, |
||||
deviceIds, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
export function unbindingDeviceFromGroup(deviceGroupId: string, deviceIds: string) { |
||||
return defHttp.post({ |
||||
url: '/deviceGroup/unbindDevice', |
||||
data: { |
||||
deviceGroupId, |
||||
deviceIds, |
||||
}, |
||||
}) |
||||
} |
@ -1,12 +0,0 @@
|
||||
export interface DeviceGroup { |
||||
id: string |
||||
parentId: string |
||||
groupName: string |
||||
remark: string |
||||
} |
||||
|
||||
export interface GetdeviceListByGroupParams extends PageParam { |
||||
groupId?: string |
||||
productId?: string |
||||
deviceSn?: string |
||||
} |
@ -1,18 +0,0 @@
|
||||
import type { GetLogListParams, Log, MessageContent } from './types' |
||||
import { defHttp } from '@/utils/http/axios' |
||||
|
||||
export function getLogList(params: GetLogListParams) { |
||||
return defHttp.get<PageResult<Log>>({ |
||||
url: '/device/log/page', |
||||
params, |
||||
}) |
||||
} |
||||
|
||||
export function getMessageContent(id: string) { |
||||
return defHttp.get<MessageContent>({ |
||||
url: '/device/log/message', |
||||
params: { |
||||
id, |
||||
}, |
||||
}) |
||||
} |
@ -1,25 +0,0 @@
|
||||
export interface GetLogListParams extends PageParam { |
||||
productId: string |
||||
deviceSn: string |
||||
traceId: string |
||||
bizType: string |
||||
queryStartTime: string |
||||
queryEndTime: string |
||||
} |
||||
|
||||
export interface Log { |
||||
productId: string |
||||
deviceSn: string |
||||
traceId: string |
||||
messageId?: string |
||||
bizType: number |
||||
operation: string |
||||
createTime: string |
||||
code: number |
||||
} |
||||
|
||||
export interface MessageContent { |
||||
message: string |
||||
topic: string |
||||
createTime: string |
||||
} |
@ -1,42 +0,0 @@
|
||||
import type { SubScription } from '../subscription/list/types' |
||||
import type { GetProductListParmas, Product } from './types' |
||||
import { defHttp } from '@/utils/http/axios' |
||||
|
||||
export function getProductList(params: GetProductListParmas) { |
||||
return defHttp.get<PageResult<Product>>({ |
||||
url: '/product/page', |
||||
params, |
||||
}) |
||||
} |
||||
|
||||
export function getProductDetail(id: string) { |
||||
return defHttp.get<Product & { subscribe: SubScription }>({ |
||||
url: `/product/detail?id=${id}`, |
||||
}) |
||||
} |
||||
|
||||
export function createProduct(data: Partial<Product>) { |
||||
return defHttp.post({ |
||||
url: '/product/save', |
||||
data, |
||||
}) |
||||
} |
||||
|
||||
export function updateProduct(data: Partial<Product>) { |
||||
return defHttp.post({ |
||||
url: '/product/update', |
||||
data, |
||||
}) |
||||
} |
||||
|
||||
export function deleteProduct(id: string) { |
||||
return defHttp.post({ |
||||
url: `/product/remove?id=${id}`, |
||||
}) |
||||
} |
||||
|
||||
export function getAllProducts() { |
||||
return defHttp.get<Pick<Product, 'id' | 'productName'>[]>({ |
||||
url: '/product/select', |
||||
}) |
||||
} |
@ -1,68 +0,0 @@
|
||||
import type { GetModelAttributeListParams, ModelAttribute, ModelAttributeWithForm, ModelService, SimpleAttribute } from './types' |
||||
import { defHttp } from '@/utils/http/axios' |
||||
|
||||
export function getAllModelServices(productId: string) { |
||||
return defHttp.get<ModelService[]>({ |
||||
url: '/thingModel/select', |
||||
params: { |
||||
productId, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
export function createModelService(data: Partial<ModelService>) { |
||||
return defHttp.post({ |
||||
url: '/thingModel/save', |
||||
data, |
||||
}) |
||||
} |
||||
|
||||
export function updateModelService(data: Partial<ModelService>) { |
||||
return defHttp.post({ |
||||
url: '/thingModel/update', |
||||
data, |
||||
}) |
||||
} |
||||
|
||||
export function deleteModelService(id: string) { |
||||
return defHttp.post({ |
||||
url: `/thingModel/remove?id=${id}`, |
||||
}) |
||||
} |
||||
|
||||
export function getModelAttributeList(params: GetModelAttributeListParams) { |
||||
return defHttp.get<PageResult<ModelAttribute>>({ |
||||
url: '/thingModel/item/page', |
||||
params, |
||||
}) |
||||
} |
||||
|
||||
export function createModelAttribute(data: ModelAttributeWithForm) { |
||||
return defHttp.post({ |
||||
url: '/thingModel/item/save', |
||||
data, |
||||
}) |
||||
} |
||||
|
||||
export function updateModelAttribute(data: ModelAttributeWithForm) { |
||||
return defHttp.post({ |
||||
url: '/thingModel/item/update', |
||||
data, |
||||
}) |
||||
} |
||||
|
||||
export function deleteModelAttribute(id: string) { |
||||
return defHttp.post({ |
||||
url: `/thingModel/item/remove?id=${id}`, |
||||
}) |
||||
} |
||||
|
||||
export function getAllModelAttributes(productId: string, itemType: number) { |
||||
return defHttp.get<SimpleAttribute[]>({ |
||||
url: '/thingModel/item/queryList', |
||||
params: { |
||||
productId, |
||||
itemType, |
||||
}, |
||||
}) |
||||
} |
@ -1,29 +0,0 @@
|
||||
import type { GetTopicListPrams, Topic } from './types' |
||||
import { defHttp } from '@/utils/http/axios' |
||||
|
||||
export function getTopicList(params: GetTopicListPrams) { |
||||
return defHttp.get<PageResult<Topic>>({ |
||||
url: '/product/topic/page', |
||||
params, |
||||
}) |
||||
} |
||||
|
||||
export function createTopic(data: Partial<Topic>) { |
||||
return defHttp.post({ |
||||
url: '/product/topic/save', |
||||
data, |
||||
}) |
||||
} |
||||
|
||||
export function updateTopic(data: Partial<Topic>) { |
||||
return defHttp.post({ |
||||
url: '/product/topic/update', |
||||
data, |
||||
}) |
||||
} |
||||
|
||||
export function deleteTopic(id: string) { |
||||
return defHttp.post({ |
||||
url: `/product/topic/remove?id=${id}`, |
||||
}) |
||||
} |
@ -1,120 +0,0 @@
|
||||
export interface GetProductListParmas extends PageParam { |
||||
productName?: string |
||||
networkType?: number |
||||
networkProtocol?: number |
||||
nodeType?: number |
||||
securityType?: number |
||||
dataType?: number |
||||
} |
||||
|
||||
export interface Product { |
||||
id: string |
||||
tenantId: string |
||||
uuid: string |
||||
productName: string |
||||
productDesc: string |
||||
productKey: string |
||||
productSecret: string |
||||
nodeType: number |
||||
networkType: number |
||||
networkProtocol: number |
||||
authType: number |
||||
securityType: number |
||||
dataType: number |
||||
tsl: string |
||||
isRelease: number |
||||
} |
||||
|
||||
export enum TopicType { |
||||
System = 1, |
||||
Custom = 2, |
||||
} |
||||
|
||||
export interface GetTopicListPrams extends PageParam { |
||||
productId?: string |
||||
topicCategory?: TopicType |
||||
} |
||||
|
||||
export interface Topic { |
||||
id: string |
||||
topic: string |
||||
topicType: number |
||||
enableScript: BooleanFlag |
||||
topicDesc: string |
||||
productId: string |
||||
} |
||||
|
||||
export interface ModelService { |
||||
id: string |
||||
productId: string |
||||
serviceId: string |
||||
tenantId: string |
||||
description: string |
||||
} |
||||
|
||||
export interface GetModelAttributeListParams extends PageParam { |
||||
productId?: string |
||||
modelId?: string |
||||
} |
||||
|
||||
export enum ModelAttributeDataTypesEnum { |
||||
Int32 = 'int32', |
||||
Float = 'float', |
||||
Bool = 'bool', |
||||
Text = 'text', |
||||
} |
||||
|
||||
export interface ModelAttribute { |
||||
id: string |
||||
name: string |
||||
itemType: number |
||||
identifier: string |
||||
modelId: string |
||||
modelName?: string |
||||
tenantId: string |
||||
method: string |
||||
sort: number |
||||
dataType: ModelAttributeDataTypesEnum |
||||
dataSpecs?: { |
||||
min?: number |
||||
max?: number |
||||
maxLength?: number |
||||
trueDesc?: string |
||||
falseDesc?: string |
||||
scale?: string |
||||
} |
||||
} |
||||
|
||||
export interface ModelAttributeWithForm { |
||||
id: string |
||||
name: string |
||||
itemType: number |
||||
identifier: string |
||||
dataType: ModelAttributeDataTypesEnum |
||||
min: number |
||||
max: number |
||||
maxLength: number |
||||
scale: number |
||||
unit: string |
||||
trueDesc: string |
||||
falseDesc: string |
||||
method: string |
||||
sort: number |
||||
modelId: string |
||||
} |
||||
|
||||
export interface SimpleAttribute { |
||||
dataType: ModelAttributeDataTypesEnum |
||||
itemType: number |
||||
modelId: string |
||||
modelName: string |
||||
name: string |
||||
dataSpecs: { |
||||
min: number |
||||
max: number |
||||
maxLength: number |
||||
trueDesc: string |
||||
falseDesc: string |
||||
scale: string |
||||
} |
||||
} |
@ -1,62 +0,0 @@
|
||||
import type { Consumer, GetConsumerListParams, Product } from './types' |
||||
import { defHttp } from '@/utils/http/axios' |
||||
|
||||
export function getConsumerList(params: GetConsumerListParams) { |
||||
return defHttp.get<PageResult<Consumer>>({ |
||||
url: '/server/consumer/page', |
||||
params, |
||||
}) |
||||
} |
||||
|
||||
export function createConsumer(data: Partial<Consumer>) { |
||||
return defHttp.post({ |
||||
url: '/server/consumer/save', |
||||
data, |
||||
}) |
||||
} |
||||
|
||||
export function updateConsumer(data: Partial<Consumer>) { |
||||
return defHttp.post({ |
||||
url: '/server/consumer/update', |
||||
data, |
||||
}) |
||||
} |
||||
|
||||
export function deleteConsumer(id: string) { |
||||
return defHttp.post({ |
||||
url: `/server/consumer/remove?id=${id}`, |
||||
}) |
||||
} |
||||
|
||||
export function getConsumerDetail(id: string) { |
||||
return defHttp.get<Consumer>({ |
||||
url: '/server/consumer/detail', |
||||
params: { |
||||
id, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
export function getOnlineClientList(consumerToken: string) { |
||||
return defHttp.get({ |
||||
url: '/server/client/list', |
||||
params: { |
||||
consumerToken, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
export function getSubscribeList(consumerId: string) { |
||||
return defHttp.get<Product[]>({ |
||||
url: '/server/consumer/subscribeList', |
||||
params: { |
||||
consumerId, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
export function disSubscription(consumerId: string, serverSubscribeId: string) { |
||||
return defHttp.post({ |
||||
url: `/server/consumer/unsubscription?consumerId=${consumerId}&serverSubscribeId=${serverSubscribeId}`, |
||||
}) |
||||
} |
@ -1,18 +0,0 @@
|
||||
export interface GetConsumerListParams extends PageParam { |
||||
consumerName?: string |
||||
} |
||||
|
||||
export interface Consumer { |
||||
id: string |
||||
consumerName: string |
||||
consumerToken: string |
||||
createTime: string |
||||
subscribes: string |
||||
} |
||||
|
||||
export interface Product { |
||||
id: string |
||||
messageType: string |
||||
productId: string |
||||
tenantId: string |
||||
} |
@ -1,44 +0,0 @@
|
||||
import type { GetSubscriptionListParams, SubScription } from './types' |
||||
import { defHttp } from '@/utils/http/axios' |
||||
|
||||
export function getSubscriptionList(params: GetSubscriptionListParams) { |
||||
return defHttp.get<PageResult<SubScription>>({ |
||||
url: '/server/subscribe/page', |
||||
params, |
||||
}) |
||||
} |
||||
|
||||
export function createSubscription(data: Pick<SubScription, 'productId' | 'messageType'>) { |
||||
return defHttp.post({ |
||||
url: '/server/subscribe/save', |
||||
data, |
||||
}) |
||||
} |
||||
|
||||
export function updateSubscription(data: Pick<SubScription, 'productId' | 'messageType' | 'id'>) { |
||||
return defHttp.post({ |
||||
url: '/server/subscribe/update', |
||||
data, |
||||
}) |
||||
} |
||||
|
||||
export function deleteSubscription(id: string) { |
||||
return defHttp.post({ |
||||
url: `/server/subscribe/remove?id=${id}`, |
||||
}) |
||||
} |
||||
|
||||
export function getAllSubscription() { |
||||
return defHttp.get<SubScription[]>({ |
||||
url: '/server/subscribe/select', |
||||
}) |
||||
} |
||||
|
||||
export function getSubscriptionDetail(id: string) { |
||||
return defHttp.get({ |
||||
url: '/server/consumer/detail', |
||||
params: { |
||||
id, |
||||
}, |
||||
}) |
||||
} |
@ -1,11 +0,0 @@
|
||||
export interface GetSubscriptionListParams extends PageParam { |
||||
productId?: string |
||||
} |
||||
|
||||
export interface SubScription { |
||||
id: string |
||||
messageType: string |
||||
productId: string |
||||
tenantId: string |
||||
createTime: string |
||||
} |
@ -1,55 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { ref } from 'vue' |
||||
import { getFormSchema } from './data' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { BasicForm, useForm } from '@/components/Form' |
||||
import { BasicModal, useModalInner } from '@/components/Modal' |
||||
import { createDevice, updateDevice } from '@/api/device-manage/device' |
||||
import type { Device } from '@/api/device-manage/device/types' |
||||
|
||||
defineOptions({ name: 'DeviceFormModal' }) |
||||
|
||||
const emit = defineEmits(['success', 'register']) |
||||
|
||||
const isUpdate = ref(false) |
||||
const [registerForm, { setFieldsValue, validate }] = useForm({ |
||||
labelWidth: 100, |
||||
baseColProps: { span: 24 }, |
||||
schemas: getFormSchema(isUpdate), |
||||
showActionButtonGroup: false, |
||||
actionColOptions: { span: 23 }, |
||||
}) |
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data: Device) => { |
||||
isUpdate.value = true |
||||
setFieldsValue({ ...data }) |
||||
}) |
||||
|
||||
async function handleSubmit() { |
||||
try { |
||||
const values = await validate<Device>() |
||||
setModalProps({ confirmLoading: true }) |
||||
await (isUpdate.value ? updateDevice(values) : createDevice(values)) |
||||
closeModal() |
||||
emit('success') |
||||
useMessage().createMessage.success('保存成功') |
||||
} |
||||
catch {} |
||||
finally { |
||||
setModalProps({ confirmLoading: false }) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<BasicModal |
||||
width="30%" |
||||
:min-height="100" |
||||
:title="isUpdate ? '编辑' : '新增'" |
||||
:after-close="() => isUpdate = false" |
||||
@register="registerModal" |
||||
@ok="handleSubmit" |
||||
> |
||||
<BasicForm @register="registerForm" /> |
||||
</BasicModal> |
||||
</template> |
@ -1,104 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { computed, h, ref, unref } from 'vue' |
||||
import { CloudSyncOutlined } from '@ant-design/icons-vue' |
||||
import { Alert, Segmented } from 'ant-design-vue' |
||||
import MessageModal from './MessageModal.vue' |
||||
import SendCommandModal from './SendCommandModal.vue' |
||||
import { BasicTable, useTable } from '@/components/Table' |
||||
import { useModal } from '@/components/Modal' |
||||
import { getCloudCommandLogs, getMessageContent } from '@/api/device-manage/device/cloud-command' |
||||
import type { Device } from '@/api/device-manage/device/types' |
||||
import { CloudCommandType } from '@/api/device-manage/device/types' |
||||
import { usePermission } from '@/hooks/web/usePermission' |
||||
|
||||
defineProps<{ device?: Device }>() |
||||
|
||||
const [registerModal, { openModal }] = useModal<{ type: CloudCommandType, productId: string, deviceId: string }>() |
||||
|
||||
const commandTypes = [ |
||||
{ |
||||
label: '命令下发', |
||||
value: CloudCommandType.Command, |
||||
desc: '如果设备所属产品定义了命令功能,则您可以通过应用调用平台接口或者操作 “下发” 按钮下发命令,当前MQTT设备仅支持同步命令下发,NB设备仅支持异步命令下发。', |
||||
}, |
||||
{ |
||||
label: '属性下发', |
||||
value: CloudCommandType.Attribute, |
||||
desc: '属性下发依赖产品模型,平台会以异步方式(属性下发后无需等待设备侧回复相应)下发消息给设备,当前仅MQTT设备支持属性下发。', |
||||
}, |
||||
{ |
||||
label: '消息下发', |
||||
value: CloudCommandType.Message, |
||||
desc: '消息下发不依赖产品模型,平台会以异步方式(消息下发后无需等待设备侧回复相应)下发消息给设备,当前仅MQTT设备支持消息下发。', |
||||
}, |
||||
] |
||||
const selectedCommonType = ref(CloudCommandType.Command) |
||||
const selectedCommon = computed(() => commandTypes.find(item => item.value === selectedCommonType.value)) |
||||
|
||||
const [register, { reload }] = useTable({ |
||||
api: params => getCloudCommandLogs({ ...params, messageType: unref(selectedCommon)?.value }), |
||||
columns: [ |
||||
{ |
||||
title: '追踪 ID', |
||||
dataIndex: 'traceId', |
||||
}, |
||||
{ |
||||
title: '内容', |
||||
width: 200, |
||||
dataIndex: 'messageId', |
||||
customRender({ value }) { |
||||
return value && h(MessageModal, { |
||||
buttonText: '查看', |
||||
request: () => getMessageContent(value), |
||||
}) |
||||
}, |
||||
}, |
||||
{ |
||||
title: '主题', |
||||
dataIndex: 'operation', |
||||
}, |
||||
{ |
||||
title: '创建时间', |
||||
dataIndex: 'createTime', |
||||
}, |
||||
], |
||||
bordered: true, |
||||
inset: true, |
||||
canResize: false, |
||||
}) |
||||
|
||||
const { hasPermission } = usePermission() |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<div flex="~ items-center gap-12px" mb="12px"> |
||||
<Segmented v-model:value="selectedCommonType" :options="commandTypes" @change="() => reload()" /> |
||||
<a-button |
||||
v-if="hasPermission('device_cloud_command_action')" |
||||
type="primary" |
||||
@click="openModal(true, { |
||||
deviceId: device!.id, |
||||
type: selectedCommon!.value, |
||||
productId: device!.productId, |
||||
})" |
||||
> |
||||
<CloudSyncOutlined />下发 |
||||
</a-button> |
||||
<Alert |
||||
type="info" |
||||
show-icon |
||||
class="py-4px text-13px" |
||||
:message="selectedCommon?.desc" |
||||
/> |
||||
</div> |
||||
|
||||
<BasicTable @register="register" /> |
||||
|
||||
<SendCommandModal |
||||
:title="selectedCommon?.label" |
||||
@register="registerModal" |
||||
@success="reload" |
||||
/> |
||||
</div> |
||||
</template> |
@ -1,114 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { toRef } from 'vue' |
||||
import { Card, Empty } from 'ant-design-vue' |
||||
import { FieldTimeOutlined, SyncOutlined } from '@ant-design/icons-vue' |
||||
import { useDeviceInfo } from './composables/useDeviceInfo' |
||||
import { useDeviceProperties } from './composables/useDeviceProperties' |
||||
import { Description } from '@/components/Description' |
||||
import { useModelService } from '@/views/product/components/composables/useModelService' |
||||
import { usePermission } from '@/hooks/web/usePermission' |
||||
import { ProductTabEnums } from '@/views/product/data' |
||||
import type { Device } from '@/api/device-manage/device/types' |
||||
|
||||
const props = defineProps<{ data: Device }>() |
||||
|
||||
const { scheam } = useDeviceInfo(toRef(props, 'data')) |
||||
|
||||
const { |
||||
modelServiceList, |
||||
selectedModelId, |
||||
setSelectedModelId, |
||||
} = useModelService(props.data.productId) |
||||
|
||||
const { |
||||
isLoading, |
||||
deviceProperties, |
||||
reloadReviceProperties, |
||||
} = useDeviceProperties(() => selectedModelId.value, () => props.data?.deviceSn) |
||||
|
||||
const { hasPermission } = usePermission() |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<Description :schema="scheam" :data="data" :column="2" :label-style="{ width: '130px' }" /> |
||||
|
||||
<Card v-if="hasPermission('device_report_view')" mt="15px"> |
||||
<div flex="~ items-center justify-between"> |
||||
<div font-bold> |
||||
最新上报数据 |
||||
</div> |
||||
<div flex="~ items-center"> |
||||
<span v-if="deviceProperties?.updateTime" text="gray-400 center" mr="12px"> |
||||
更新时间: {{ deviceProperties.updateTime }} |
||||
</span> |
||||
<a-button size="small" @click="reloadReviceProperties"> |
||||
<SyncOutlined /> |
||||
刷新 |
||||
</a-button> |
||||
</div> |
||||
</div> |
||||
|
||||
<div v-loading="isLoading" flex="~ gap-12px" mt="12px"> |
||||
<div w="20%" border="0 r-1 solid gray-50 dark:white dark:opacity-5"> |
||||
<div |
||||
v-for="item in modelServiceList" :key="item.id" |
||||
flex="~ items-center justify-between" |
||||
class="box-border h-60px cursor-pointer pl-10px hover:bg-gray-100 hover:dark:bg-white hover:dark:bg-opacity-5" |
||||
border="0 b-1 solid gray-50 dark:white dark:opacity-5" |
||||
:class="selectedModelId === item.id ? 'bg-gray-100 dark:bg-white dark:bg-opacity-5' : ''" |
||||
@click="setSelectedModelId(item.id)" |
||||
> |
||||
<div> |
||||
<div truncate> |
||||
{{ item.serviceId }} |
||||
</div> |
||||
<div mt="5px" text="12px gray-500" truncate :title="item.description"> |
||||
{{ item.description }} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div |
||||
v-if="deviceProperties?.properties" |
||||
w-0 flex-1 |
||||
grid="~ cols-4 rows-[160px] auto-rows-[160px] gap-12px" |
||||
> |
||||
<div |
||||
v-for="item in deviceProperties.properties" |
||||
:key="item.identifier" |
||||
p="15px" box-border rounded |
||||
shadow="hover:lg" transition="shadow" |
||||
flex="~ col justify-between" |
||||
style="background: linear-gradient(to right, rgba(243, 244, 246, 1), rgba(209, 216, 224, 1));" |
||||
> |
||||
<div> |
||||
<div font-500 text="16px"> |
||||
{{ item.name }} |
||||
</div> |
||||
<div text="gray-500"> |
||||
{{ item.identifier }} |
||||
</div> |
||||
</div> |
||||
<div text="22px center"> |
||||
"{{ item.unit || 'null' }}" |
||||
</div> |
||||
<div text="right gray-500 hover:gray-800"> |
||||
<span v-if="hasPermission('device_report_history')" class="cursor-pointer"> |
||||
<FieldTimeOutlined /> |
||||
历史 |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div v-else text="center" flex-1> |
||||
<Empty :image="Empty.PRESENTED_IMAGE_SIMPLE" /> |
||||
<a-button size="small" @click="$router.push(`/product/detail/${data?.productId}/${ProductTabEnums.Model}/${selectedModelId}`)"> |
||||
点击添加属性 |
||||
</a-button> |
||||
</div> |
||||
</div> |
||||
</Card> |
||||
</div> |
||||
</template> |
@ -1,59 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { h, ref } from 'vue' |
||||
import { Modal } from 'ant-design-vue' |
||||
import { EyeOutlined } from '@ant-design/icons-vue' |
||||
import { useAsyncState } from '@vueuse/core' |
||||
import type { DescItem } from '@/components/Description' |
||||
import { Description } from '@/components/Description' |
||||
import { JsonPreview } from '@/components/CodeEditor' |
||||
import { noop } from '@/utils' |
||||
|
||||
const props = defineProps<{ |
||||
buttonText?: string |
||||
request: (...args: any[]) => Promise<{ topic: string, message: string }> |
||||
}>() |
||||
|
||||
const { state, execute, isLoading } = useAsyncState(props.request, undefined, { immediate: false }) |
||||
|
||||
const open = ref(false) |
||||
function handleOpen() { |
||||
if (state.value) |
||||
return open.value = true |
||||
|
||||
execute() |
||||
.then(() => open.value = true) |
||||
.catch(noop) |
||||
} |
||||
|
||||
const schema: DescItem[] = [ |
||||
{ |
||||
label: 'Topic', |
||||
field: 'topic', |
||||
}, |
||||
{ |
||||
label: '内容', |
||||
field: 'message', |
||||
render(value) { |
||||
let content = value |
||||
try { |
||||
content = JSON.parse(value) |
||||
} |
||||
catch {} |
||||
return h(JsonPreview, { |
||||
data: content, |
||||
}) |
||||
}, |
||||
}, |
||||
] |
||||
</script> |
||||
|
||||
<template> |
||||
<a-button size="small" :loading="isLoading" @click="handleOpen"> |
||||
<EyeOutlined /> |
||||
{{ buttonText || '查看参数' }} |
||||
</a-button> |
||||
|
||||
<Modal v-model:open="open" title="上报示例" :footer="null" width="50%"> |
||||
<Description :data="state" :schema="schema" :column="1" /> |
||||
</Modal> |
||||
</template> |
@ -1,77 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { ref } from 'vue' |
||||
import { Modal } from 'ant-design-vue' |
||||
import { EyeOutlined } from '@ant-design/icons-vue' |
||||
import { useAsyncState } from '@vueuse/core' |
||||
import { getMqttConnectParams } from '@/api/device-manage/device' |
||||
import type { DescItem } from '@/components/Description' |
||||
import { Description } from '@/components/Description' |
||||
import { noop } from '@/utils' |
||||
import { copyText } from '@/utils/copyTextToClipboard' |
||||
|
||||
const props = defineProps<{ deviceId: string }>() |
||||
|
||||
const { state, execute, isLoading } = useAsyncState(() => getMqttConnectParams(props.deviceId), undefined, { immediate: false }) |
||||
|
||||
const open = ref(false) |
||||
function handleOpen() { |
||||
if (state.value) |
||||
return open.value = true |
||||
|
||||
execute() |
||||
.then(() => { |
||||
open.value = true |
||||
}) |
||||
.catch(noop) |
||||
} |
||||
|
||||
const schema: DescItem[] = [ |
||||
{ |
||||
label: '服务器地址 (hostUrl)', |
||||
field: 'hostUrl', |
||||
}, |
||||
{ |
||||
label: '端口 (port)', |
||||
field: 'port', |
||||
}, |
||||
{ |
||||
label: 'ClientID (clientId)', |
||||
field: 'clientId', |
||||
}, |
||||
{ |
||||
label: '用户名 (username)', |
||||
field: 'username', |
||||
}, |
||||
{ |
||||
label: '密码 (password)', |
||||
field: 'password', |
||||
}, |
||||
] |
||||
|
||||
function handleCopy() { |
||||
const data = schema.map((item) => { |
||||
return { |
||||
key: item.field, |
||||
keyName: item.label, |
||||
value: state.value[item.field], |
||||
} |
||||
}) |
||||
copyText(JSON.stringify(data)) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<a-button size="small" :loading="isLoading" @click="handleOpen"> |
||||
<EyeOutlined /> |
||||
查看参数 |
||||
</a-button> |
||||
|
||||
<Modal v-model:open="open" title="MQTT 连接参数" :footer="null"> |
||||
<Description :data="state" :schema="schema" :column="1" /> |
||||
<div text="center" mt="10px"> |
||||
<a-button size="small" @click="handleCopy"> |
||||
一键复制 |
||||
</a-button> |
||||
</div> |
||||
</Modal> |
||||
</template> |
@ -1,185 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { BasicModal, useModalInner } from '@/components/Modal' |
||||
import type { FormSchema, FormSchemaInner, Rule } from '@/components/Form' |
||||
import { BasicForm, useForm } from '@/components/Form' |
||||
import { CloudCommandType } from '@/api/device-manage/device/types' |
||||
import { sendCommandToDevice, sendMessageToDevice } from '@/api/device-manage/device/cloud-command' |
||||
import CodeEditor from '@/components/CodeEditor/src/CodeEditor.vue' |
||||
import { getAllModelAttributes } from '@/api/product/model' |
||||
import { getDeviceTopicList } from '@/api/device-manage/device' |
||||
import { ModelAttributeDataTypesEnum, type SimpleAttribute } from '@/api/product/types' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
|
||||
const emit = defineEmits(['register', 'success']) |
||||
|
||||
const [registerForm, { updateSchema, resetSchema, validate, setFieldsValue, clearValidate }] = useForm({ |
||||
schemas: [], |
||||
labelWidth: 100, |
||||
baseColProps: { span: 24 }, |
||||
showActionButtonGroup: false, |
||||
actionColOptions: { span: 23 }, |
||||
}) |
||||
|
||||
let productId: string |
||||
let deviceId: string |
||||
let commandType: CloudCommandType |
||||
const CommandAndAttributeSchemas: FormSchema[] = [ |
||||
{ |
||||
field: 'itemId', |
||||
label: '选择属性', |
||||
required: true, |
||||
component: 'ApiSelect', |
||||
componentProps: { |
||||
api: () => getAllModelAttributes(productId!, commandType), |
||||
valueField: 'id', |
||||
labelField: 'name', |
||||
allowClear: false, |
||||
onChange(_, option: SimpleAttribute & { label: string }) { // label is SimpleAttribute.name |
||||
if (!option) |
||||
return |
||||
|
||||
const { dataType, label, dataSpecs, modelId } = option |
||||
setFieldsValue({ modelId, value: undefined }) |
||||
// avoid non-null check |
||||
clearValidate() |
||||
|
||||
let schema: Partial<FormSchemaInner> = {} |
||||
if (dataType === ModelAttributeDataTypesEnum.Bool) { |
||||
schema = { |
||||
rules: [], // remove rules cache |
||||
component: 'RadioGroup', |
||||
componentProps: { |
||||
options: [ |
||||
{ label: dataSpecs.falseDesc, value: 0 }, |
||||
{ label: dataSpecs.trueDesc, value: 1 }, |
||||
], |
||||
}, |
||||
} |
||||
} |
||||
else { |
||||
let rules: Rule[] = [] |
||||
switch (dataType) { |
||||
case ModelAttributeDataTypesEnum.Text: |
||||
rules = [{ max: dataSpecs.maxLength, message: `限制最大长度为 ${dataSpecs.maxLength}` }] |
||||
break |
||||
case ModelAttributeDataTypesEnum.Float: |
||||
case ModelAttributeDataTypesEnum.Int32: |
||||
rules = [ |
||||
{ type: 'number', min: +dataSpecs.min, max: +dataSpecs.max, message: `数值范围为: ${dataSpecs.min}-${dataSpecs.max}` }, |
||||
] |
||||
break |
||||
default: { |
||||
const _exhaustiveCheck: never = dataType |
||||
return _exhaustiveCheck |
||||
} |
||||
} |
||||
|
||||
schema = { |
||||
rules, |
||||
// if it isn't text, then it's a float or int32 |
||||
component: dataType === ModelAttributeDataTypesEnum.Text ? 'InputTextArea' : 'InputNumber', |
||||
componentProps: dataType !== ModelAttributeDataTypesEnum.Text |
||||
? { |
||||
precision: dataType === ModelAttributeDataTypesEnum.Float ? dataSpecs.scale : 0, |
||||
} |
||||
: { rows: 3 }, |
||||
} |
||||
} |
||||
updateSchema({ |
||||
label, |
||||
field: 'value', |
||||
ifShow: true, |
||||
...schema, |
||||
}) |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
field: 'value', |
||||
fields: ['modelId'], |
||||
required: true, |
||||
component: 'Input', |
||||
ifShow: false, |
||||
}, |
||||
] |
||||
const MessageSchemas: FormSchema[] = [ |
||||
{ |
||||
field: 'topic', |
||||
label: 'Topic', |
||||
required: true, |
||||
component: 'ApiSelect', |
||||
componentProps: { |
||||
api: async () => (await getDeviceTopicList({ deviceId, current: 1, size: 500 })).records, |
||||
valueField: 'topic', |
||||
labelField: 'topic', |
||||
}, |
||||
}, |
||||
{ |
||||
field: 'message', |
||||
label: '指令内容', |
||||
rules: [ |
||||
{ |
||||
async validator(_, value) { |
||||
try { |
||||
const code = JSON.stringify(JSON.parse(value)) |
||||
if (code === '{}') |
||||
// eslint-disable-next-line unicorn/error-message |
||||
throw new Error() |
||||
} |
||||
catch { |
||||
// eslint-disable-next-line prefer-promise-reject-errors |
||||
return Promise.reject('指令内容必须是 JSON 格式, 且不能为空') |
||||
} |
||||
}, |
||||
required: true, |
||||
}, |
||||
], |
||||
slot: 'Message', |
||||
helpMessage: '指令内容必须是 JSON 格式', |
||||
defaultValue: {}, |
||||
componentProps: { |
||||
rows: 10, |
||||
}, |
||||
}, |
||||
] |
||||
const [register, { closeModal, setModalProps }] = useModalInner((params: { type: CloudCommandType, productId: string, deviceId: string }) => { |
||||
productId = params.productId |
||||
deviceId = params.deviceId |
||||
commandType = params.type |
||||
resetSchema(commandType === CloudCommandType.Message ? MessageSchemas : CommandAndAttributeSchemas) |
||||
}) |
||||
|
||||
async function handleSubmit() { |
||||
try { |
||||
const values = await validate<any>() |
||||
setModalProps({ confirmLoading: true }) |
||||
values.deviceId = deviceId |
||||
const isSendMessage = commandType === CloudCommandType.Message |
||||
await (isSendMessage ? sendMessageToDevice(values) : sendCommandToDevice(values)) |
||||
useMessage().createMessage.success('下发成功') |
||||
emit('success') |
||||
closeModal() |
||||
} |
||||
catch {} |
||||
finally { |
||||
setModalProps({ confirmLoading: false }) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<BasicModal |
||||
v-bind="$attrs" |
||||
:min-height="100" |
||||
@register="register" |
||||
@ok="handleSubmit" |
||||
> |
||||
<BasicForm @register="registerForm"> |
||||
<template #Message="{ model, field }"> |
||||
<div h="200px" border="1px solid gray-200 dark:#424242" w-full overflow-hidden rounded> |
||||
<CodeEditor v-model:value="model[field]" /> |
||||
</div> |
||||
</template> |
||||
</BasicForm> |
||||
</BasicModal> |
||||
</template> |
@ -1,53 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { h } from 'vue' |
||||
import { Alert, Tag } from 'ant-design-vue' |
||||
import { SyncOutlined } from '@ant-design/icons-vue' |
||||
import { useRoute } from 'vue-router' |
||||
import { BasicTable, useTable } from '@/components/Table' |
||||
import { getDeviceTopicList } from '@/api/device-manage/device' |
||||
import { useSystemEnumStore } from '@/store/modules/systemEnum' |
||||
|
||||
const route = useRoute() |
||||
const { getSystemEnumLabel } = useSystemEnumStore() |
||||
const [register, { reload }] = useTable({ |
||||
api(params) { |
||||
return getDeviceTopicList({ ...params, deviceId: route.params.id as string }) |
||||
}, |
||||
columns: [ |
||||
{ |
||||
title: 'Topic', |
||||
dataIndex: 'topic', |
||||
}, |
||||
{ |
||||
title: '操作权限', |
||||
dataIndex: 'topicType', |
||||
customRender({ value }) { |
||||
return h(Tag, () => getSystemEnumLabel('eProductTopicType', value)) |
||||
}, |
||||
}, |
||||
{ |
||||
title: '描述', |
||||
dataIndex: 'topicDesc', |
||||
}, |
||||
], |
||||
bordered: true, |
||||
inset: true, |
||||
canResize: false, |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div flex="~ items-center justify-between"> |
||||
<Alert |
||||
type="info" |
||||
show-icon |
||||
class="py-4px text-13px" |
||||
message="Topic用以将设备数据分类上报,进而分别进行处理。除了系统预置的Topic,您也可以自定义Topic,然后在设备侧开发时选择数据上报的Topic。" |
||||
/> |
||||
<a-button size="small" @click="reload"> |
||||
<SyncOutlined /> |
||||
刷新 |
||||
</a-button> |
||||
</div> |
||||
<BasicTable mt="12px" @register="register" /> |
||||
</template> |
@ -1,84 +0,0 @@
|
||||
import type { Ref } from 'vue' |
||||
import { h } from 'vue' |
||||
import { Tag } from 'ant-design-vue' |
||||
import MqttParamsModal from '../MqttParamsModal.vue' |
||||
import MessageModal from '../MessageModal.vue' |
||||
import type { DescItem } from '@/components/Description' |
||||
import { getReportExample } from '@/api/device-manage/device' |
||||
import { usePermission } from '@/hooks/web/usePermission' |
||||
import type { Device } from '@/api/device-manage/device/types' |
||||
|
||||
export function useDeviceInfo(data: Ref<Device | undefined>) { |
||||
const { hasPermission } = usePermission() |
||||
|
||||
const scheam: DescItem[] = [ |
||||
{ |
||||
field: 'productName', |
||||
label: '所属产品', |
||||
}, |
||||
{ |
||||
field: 'deviceName', |
||||
label: '设备名称', |
||||
}, |
||||
{ |
||||
field: 'deviceSn', |
||||
label: '设备序列号', |
||||
}, |
||||
{ |
||||
field: 'deviceSecret', |
||||
label: '设备密钥', |
||||
}, |
||||
{ |
||||
field: 'isActive', |
||||
label: '设备状态', |
||||
render(value) { |
||||
return h(Tag, { color: value ? 'green' : 'default' }, () => value ? '已激活' : '未激活') |
||||
}, |
||||
}, |
||||
{ |
||||
field: 'isOnline', |
||||
label: '在线状态', |
||||
render(value) { |
||||
return h(Tag, { color: value ? 'green' : 'default' }, () => value ? '在线' : '离线') |
||||
}, |
||||
}, |
||||
{ |
||||
field: 'createTime', |
||||
label: '创建时间', |
||||
}, |
||||
{ |
||||
field: 'activeTime', |
||||
label: '激活时间', |
||||
}, |
||||
{ |
||||
field: 'lastOnlineTime', |
||||
label: '最后上线时间', |
||||
}, |
||||
{ |
||||
field: 'lastOfflineTime', |
||||
label: '最后离线时间', |
||||
}, |
||||
{ |
||||
field: 'mqtt', |
||||
label: 'MQTT连接参数', |
||||
show: () => hasPermission('device_mqtt_params'), |
||||
render: () => h(MqttParamsModal, { deviceId: data.value!.id }), |
||||
}, |
||||
{ |
||||
field: 'report', |
||||
label: '上报示例', |
||||
show: () => hasPermission('device_report_example'), |
||||
render: () => h(MessageModal, { |
||||
request: () => getReportExample(data.value!.productId, data.value!.deviceSn), |
||||
}), |
||||
}, |
||||
{ |
||||
field: 'deviceDesc', |
||||
label: '设备描述', |
||||
}, |
||||
] |
||||
|
||||
return { |
||||
scheam, |
||||
} |
||||
} |
@ -1,27 +0,0 @@
|
||||
import { useAsyncState } from '@vueuse/core' |
||||
import { type MaybeRefOrGetter, toValue, watchEffect } from 'vue' |
||||
import { getDeviceProperties } from '@/api/device-manage/device' |
||||
|
||||
export function useDeviceProperties(modelId: MaybeRefOrGetter<string | undefined>, deviceSn: MaybeRefOrGetter<string | undefined>) { |
||||
const { state, execute, isLoading } = useAsyncState( |
||||
() => getDeviceProperties(toValue(modelId)!, toValue(deviceSn)!), |
||||
undefined, |
||||
{ |
||||
immediate: false, |
||||
resetOnExecute: false, |
||||
}, |
||||
) |
||||
|
||||
watchEffect(() => { |
||||
if (!toValue(modelId) || !toValue(deviceSn)) |
||||
return |
||||
|
||||
execute() |
||||
}) |
||||
|
||||
return { |
||||
isLoading, |
||||
deviceProperties: state, |
||||
reloadReviceProperties: execute, |
||||
} |
||||
} |
@ -1,4 +0,0 @@
|
||||
export { default as DeviceInfo } from './DeviceInfo.vue' |
||||
export { default as TopicList } from './TopicList.vue' |
||||
export { default as CloudCommand } from './CloudCommand.vue' |
||||
export { default as SendCommandModal } from './SendCommandModal.vue' |
@ -1,123 +0,0 @@
|
||||
import { Tag } from 'ant-design-vue' |
||||
import type { Ref } from 'vue' |
||||
import { h } from 'vue' |
||||
import type { BasicColumn, FormSchema } from '@/components/Table' |
||||
import { getAllProducts } from '@/api/product' |
||||
import { getAllTenants } from '@/api/system/tenant' |
||||
|
||||
export const columns: BasicColumn[] = [ |
||||
{ |
||||
title: '所属产品', |
||||
dataIndex: 'productName', |
||||
}, |
||||
{ |
||||
title: '设备名称', |
||||
dataIndex: 'deviceName', |
||||
}, |
||||
{ |
||||
title: '设备序列号', |
||||
dataIndex: 'deviceSn', |
||||
}, |
||||
{ |
||||
title: '设备描述', |
||||
dataIndex: 'deviceDesc', |
||||
}, |
||||
{ |
||||
title: '是否在线', |
||||
dataIndex: 'isOnline', |
||||
customRender({ value }) { |
||||
return h(Tag, { |
||||
color: value ? 'green' : 'default', |
||||
}, () => value ? '在线' : '离线') |
||||
}, |
||||
}, |
||||
] |
||||
|
||||
export const searchFormSchemas: FormSchema[] = [ |
||||
{ |
||||
label: '所属租户', |
||||
field: 'tenantId', |
||||
component: 'ApiSelect', |
||||
componentProps: { |
||||
api: getAllTenants, |
||||
valueField: 'tenantId', |
||||
labelField: 'tenantName', |
||||
}, |
||||
colProps: { span: 6 }, |
||||
}, |
||||
{ |
||||
label: '所属产品', |
||||
field: 'productId', |
||||
component: 'ApiSelect', |
||||
componentProps: { |
||||
api: getAllProducts, |
||||
valueField: 'id', |
||||
labelField: 'productName', |
||||
}, |
||||
colProps: { span: 6 }, |
||||
}, |
||||
{ |
||||
label: '设备序列号', |
||||
field: 'deviceSn', |
||||
component: 'Input', |
||||
colProps: { span: 6 }, |
||||
}, |
||||
{ |
||||
label: '设备名称', |
||||
field: 'deviceName', |
||||
component: 'Input', |
||||
colProps: { span: 6 }, |
||||
}, |
||||
{ |
||||
label: '是否在线', |
||||
field: 'isOnline', |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: [ |
||||
{ label: '在线', value: 1 }, |
||||
{ label: '离线', value: 0 }, |
||||
], |
||||
}, |
||||
colProps: { span: 6 }, |
||||
}, |
||||
] |
||||
|
||||
export function getFormSchema(isUpdate: Ref<boolean>): FormSchema[] { |
||||
return [ |
||||
{ |
||||
field: 'id', |
||||
show: false, |
||||
component: 'Input', |
||||
}, |
||||
{ |
||||
label: '设备名称', |
||||
field: 'deviceName', |
||||
component: 'Input', |
||||
required: true, |
||||
}, |
||||
{ |
||||
label: '设备序列号', |
||||
field: 'deviceSn', |
||||
component: 'Input', |
||||
required: true, |
||||
ifShow: () => !isUpdate.value, |
||||
}, |
||||
{ |
||||
label: '所属产品', |
||||
field: 'productId', |
||||
component: 'ApiSelect', |
||||
required: true, |
||||
ifShow: () => !isUpdate.value, |
||||
componentProps: { |
||||
api: getAllProducts, |
||||
valueField: 'id', |
||||
labelField: 'productName', |
||||
}, |
||||
}, |
||||
{ |
||||
label: '设备描述', |
||||
field: 'deviceDesc', |
||||
component: 'InputTextArea', |
||||
}, |
||||
] |
||||
} |
@ -1,39 +0,0 @@
|
||||
<script lang='ts' setup> |
||||
import { Card, Tabs } from 'ant-design-vue' |
||||
import { useAsyncState } from '@vueuse/core' |
||||
import { useRoute } from 'vue-router' |
||||
import { CloudCommand, DeviceInfo, TopicList } from './components' |
||||
import { usePermission } from '@/hooks/web/usePermission' |
||||
import { getDeviceDetail } from '@/api/device-manage/device' |
||||
|
||||
defineOptions({ name: 'DeviceDetail' }) |
||||
|
||||
const route = useRoute() |
||||
const { state: data } = useAsyncState(() => getDeviceDetail(route.params.id as string), undefined) |
||||
|
||||
const { hasPermission } = usePermission() |
||||
</script> |
||||
|
||||
<template> |
||||
<div p="12px"> |
||||
<Card title="设备详情"> |
||||
<Tabs> |
||||
<Tabs.TabPane key="1" tab="设备信息"> |
||||
<DeviceInfo v-if="data" :data="data" /> |
||||
</Tabs.TabPane> |
||||
<Tabs.TabPane v-if="hasPermission('device_topic_view')" key="2" tab="已订阅 Topic"> |
||||
<TopicList /> |
||||
</Tabs.TabPane> |
||||
<Tabs.TabPane v-if="hasPermission('device_cloud_command_view')" key="3" tab="云端下发"> |
||||
<CloudCommand :device="data" /> |
||||
</Tabs.TabPane> |
||||
</Tabs> |
||||
</Card> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped lang="less"> |
||||
:deep(.ant-card-body) { |
||||
padding-top: 10px; |
||||
} |
||||
</style> |
@ -1,109 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { PlusOutlined } from '@ant-design/icons-vue' |
||||
import { columns, searchFormSchemas } from './data' |
||||
import DeviceFormModal from './DeviceFormModal.vue' |
||||
import { SendCommandModal } from './components' |
||||
import { useModal } from '@/components/Modal' |
||||
import { BasicTable, TableAction, useTable } from '@/components/Table' |
||||
import { deleteDevice, getDeviceList } from '@/api/device-manage/device' |
||||
import { CloudCommandType } from '@/api/device-manage/device/types' |
||||
import type { Device } from '@/api/device-manage/device/types' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { usePermission } from '@/hooks/web/usePermission' |
||||
|
||||
defineOptions({ name: 'Device' }) |
||||
|
||||
const [registerModal, { openModal }] = useModal<Device>() |
||||
const [ |
||||
registerSendCommandModal, |
||||
{ openModal: openSendCommandModal }, |
||||
] = useModal<{ type: CloudCommandType, productId: string, deviceId: string }>() |
||||
|
||||
const { hasPermission } = usePermission() |
||||
|
||||
const [register, { reload }] = useTable({ |
||||
api: getDeviceList, |
||||
columns, |
||||
formConfig: { |
||||
schemas: searchFormSchemas, |
||||
labelWidth: 80, |
||||
}, |
||||
bordered: true, |
||||
canResize: false, |
||||
useSearchForm: true, |
||||
actionColumn: { |
||||
width: 320, |
||||
title: '操作', |
||||
dataIndex: 'action', |
||||
fixed: 'right', |
||||
auth: ['view', 'edit', 'delete', 'cmd_send'].map(code => `device_${code}`), |
||||
}, |
||||
}) |
||||
|
||||
async function handleDelete(id: string) { |
||||
try { |
||||
await deleteDevice(id) |
||||
useMessage().createMessage.success('删除成功') |
||||
reload() |
||||
} |
||||
catch {} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<BasicTable :api="async () => ([] as Device[])" @register="register"> |
||||
<template v-if="hasPermission('device_add')" #tableTitle> |
||||
<a-button type="primary" @click="openModal(true)"> |
||||
<PlusOutlined /> |
||||
新建 |
||||
</a-button> |
||||
</template> |
||||
|
||||
<template #bodyCell="{ column, record }"> |
||||
<template v-if="column.key === 'action'"> |
||||
<TableAction |
||||
:actions="[ |
||||
{ |
||||
icon: 'i-ant-design:file-search-outlined', |
||||
label: '详情', |
||||
auth: 'device_view', |
||||
onClick: () => $router.push(`/device-manage/device/detail/${record.id}`), |
||||
}, |
||||
{ |
||||
icon: 'i-ant-design:edit-outlined', |
||||
label: '编辑', |
||||
auth: 'device_edit', |
||||
onClick: () => openModal(true, record), |
||||
}, |
||||
{ |
||||
icon: 'i-ant-design:cloud-sync-outlined', |
||||
label: '指令下发', |
||||
auth: 'device_cmd_send', |
||||
onClick: () => openSendCommandModal(true, { |
||||
deviceId: record.id, |
||||
productId: record.productId, |
||||
type: CloudCommandType.Message, |
||||
}), |
||||
}, |
||||
{ |
||||
icon: 'i-ant-design:delete-outlined', |
||||
danger: true, |
||||
label: '删除', |
||||
auth: 'device_delete', |
||||
popConfirm: { |
||||
title: '是否要删除数据?', |
||||
placement: 'left', |
||||
confirm: () => handleDelete(record.id), |
||||
}, |
||||
}, |
||||
]" |
||||
/> |
||||
</template> |
||||
</template> |
||||
</BasicTable> |
||||
|
||||
<DeviceFormModal @register="registerModal" @success="reload" /> |
||||
<SendCommandModal title="指令下发" @register="registerSendCommandModal" /> |
||||
</div> |
||||
</template> |
@ -1,101 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { BasicDrawer, useDrawerInner } from '@/components/Drawer' |
||||
import { BasicTable, useTable } from '@/components/Table' |
||||
import { getDeviceList } from '@/api/device-manage/device' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { bindingDeviceToGroup } from '@/api/device-manage/group' |
||||
import { useSharedProducts } from '@/views/subscription/list/data' |
||||
import { noop } from '@/utils' |
||||
|
||||
const emit = defineEmits(['register', 'success']) |
||||
|
||||
let deviceGroupId: string |
||||
const [register, { closeDrawer, setDrawerProps }] = useDrawerInner((id: string) => { |
||||
deviceGroupId = id |
||||
}) |
||||
|
||||
const { products } = useSharedProducts() |
||||
|
||||
const [registerTable, { getSelectRowKeys, clearSelectedRowKeys }] = useTable({ |
||||
api: getDeviceList, |
||||
rowKey: 'id', |
||||
columns: [ |
||||
{ |
||||
title: '所属产品', |
||||
dataIndex: 'productId', |
||||
customRender({ value }) { |
||||
return products.value.find(item => item.id === value)?.productName |
||||
}, |
||||
}, |
||||
{ |
||||
title: '产品名称', |
||||
dataIndex: 'deviceName', |
||||
}, |
||||
{ |
||||
title: '设备序列号', |
||||
dataIndex: 'deviceSn', |
||||
}, |
||||
], |
||||
formConfig: { |
||||
schemas: [ |
||||
{ |
||||
field: 'productId', |
||||
label: '所属产品', |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: products as any, |
||||
showSearch: true, |
||||
fieldNames: { |
||||
label: 'productName', |
||||
value: 'id', |
||||
}, |
||||
}, |
||||
colProps: { span: 8 }, |
||||
}, |
||||
{ |
||||
field: 'deviceSn', |
||||
label: '设备序列号', |
||||
component: 'Input', |
||||
colProps: { span: 8 }, |
||||
}, |
||||
], |
||||
labelWidth: 90, |
||||
}, |
||||
bordered: true, |
||||
canResize: false, |
||||
useSearchForm: true, |
||||
inset: true, |
||||
rowSelection: {}, |
||||
}) |
||||
|
||||
function handleSubmit() { |
||||
const keys = getSelectRowKeys() |
||||
if (!keys.length) |
||||
return useMessage().createMessage.warn('请先选择设备') |
||||
|
||||
setDrawerProps({ confirmLoading: true }) |
||||
bindingDeviceToGroup(deviceGroupId, keys.join(',')) |
||||
.then(() => { |
||||
useMessage().createMessage.success('绑定成功') |
||||
clearSelectedRowKeys() |
||||
closeDrawer() |
||||
emit('success') |
||||
}) |
||||
.catch(noop) |
||||
.finally(() => { |
||||
setDrawerProps({ confirmLoading: false }) |
||||
}) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<BasicDrawer |
||||
title="绑定设备" |
||||
width="900px" |
||||
show-footer |
||||
@register="register" |
||||
@ok="handleSubmit" |
||||
> |
||||
<BasicTable @register="registerTable" /> |
||||
</BasicDrawer> |
||||
</template> |
@ -1,86 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { ref } from 'vue' |
||||
import { BasicModal, useModalInner } from '@/components/Modal' |
||||
import { BasicForm, useForm } from '@/components/Form' |
||||
import { createDeviceGroup, getDevicegroupDetail, updateDeviceGroup } from '@/api/device-manage/group' |
||||
import type { DeviceGroup } from '@/api/device-manage/group/types' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { noop } from '@/utils' |
||||
|
||||
const emit = defineEmits(['register', 'success']) |
||||
|
||||
const [registerForm, { validate, setFieldsValue }] = useForm({ |
||||
schemas: [ |
||||
{ |
||||
field: 'groupName', |
||||
fields: ['parentId', 'id'], |
||||
label: '群组名称', |
||||
required: true, |
||||
component: 'Input', |
||||
componentProps: { |
||||
maxlength: 30, |
||||
showCount: true, |
||||
}, |
||||
}, |
||||
{ |
||||
field: 'remark', |
||||
label: '群组描述', |
||||
component: 'InputTextArea', |
||||
componentProps: { |
||||
rows: 5, |
||||
}, |
||||
}, |
||||
], |
||||
labelWidth: 80, |
||||
baseColProps: { span: 24 }, |
||||
showActionButtonGroup: false, |
||||
actionColOptions: { span: 23 }, |
||||
}) |
||||
|
||||
const modalTitle = ref('新增根群组') |
||||
|
||||
const [register, { setModalProps, closeModal }] = useModalInner((data: { parentId: string } | { id: string }) => { |
||||
if ('id' in data) { |
||||
modalTitle.value = '编辑群组' |
||||
setModalProps({ loading: true, confirmLoading: true }) |
||||
getDevicegroupDetail(data.id) |
||||
.then((res) => { |
||||
setFieldsValue(res) |
||||
}) |
||||
.catch(noop) |
||||
.finally(() => { |
||||
setModalProps({ loading: false, confirmLoading: false }) |
||||
}) |
||||
} |
||||
else { |
||||
modalTitle.value = '新增子群组' |
||||
setFieldsValue(data) |
||||
} |
||||
}) |
||||
|
||||
async function handleSubmit() { |
||||
try { |
||||
const values = await validate<DeviceGroup>() |
||||
setModalProps({ confirmLoading: true }) |
||||
await (values.id ? updateDeviceGroup(values) : createDeviceGroup(values)) |
||||
closeModal() |
||||
emit('success') |
||||
useMessage().createMessage.success('保存成功') |
||||
} |
||||
catch {} |
||||
finally { |
||||
setModalProps({ confirmLoading: false }) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<BasicModal |
||||
:title="modalTitle" |
||||
:after-close="() => modalTitle = '新增根群组'" |
||||
@register="register" |
||||
@ok="handleSubmit" |
||||
> |
||||
<BasicForm @register="registerForm" /> |
||||
</BasicModal> |
||||
</template> |
@ -1,112 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { ref } from 'vue' |
||||
import { PlusOutlined, SyncOutlined } from '@ant-design/icons-vue' |
||||
import { useAsyncState } from '@vueuse/core' |
||||
import { Empty, Popconfirm, Space, Tree } from 'ant-design-vue' |
||||
import type { EventDataNode } from 'ant-design-vue/es/tree' |
||||
import GroupFormModal from './GroupFormModal.vue' |
||||
import { useModal } from '@/components/Modal' |
||||
import { deleteDevicegroup, getDeviceGroupTree } from '@/api/device-manage/group' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { usePermission } from '@/hooks/web/usePermission' |
||||
|
||||
defineProps<{ selectedGroupId: string | undefined }>() |
||||
const emit = defineEmits(['update:selectedGroupId', 'change']) |
||||
|
||||
const [registerModal, { openModal }] = useModal<{ id: string } | { parentId: string }>() |
||||
|
||||
const { state, execute } = useAsyncState(getDeviceGroupTree, [], { resetOnExecute: false }) |
||||
|
||||
async function handleDelete(id: string) { |
||||
try { |
||||
await deleteDevicegroup(id) |
||||
useMessage().createMessage.success('删除成功') |
||||
execute() |
||||
} |
||||
catch {} |
||||
} |
||||
|
||||
const selectedKeys = ref<string[]>([]) |
||||
|
||||
function onSelectNode(_, { selected, node }: { selected: boolean, node: EventDataNode }) { |
||||
// can't unselect |
||||
if (!selected) |
||||
return selectedKeys.value = [node.key as string] |
||||
|
||||
emit('update:selectedGroupId', node.key) |
||||
emit('change') |
||||
} |
||||
|
||||
const { hasPermission } = usePermission() |
||||
</script> |
||||
|
||||
<template> |
||||
<div rounded="6px" px="10px" pt="12px" pb="6px" border-box bg="white dark:[var(--component-background)]" min-w="360px"> |
||||
<div> |
||||
<div flex="~ items-center justify-between" h="35px"> |
||||
<a-button v-if="hasPermission('device_group_add')" size="small" @click="openModal()"> |
||||
<PlusOutlined /> |
||||
添加根分组 |
||||
</a-button> |
||||
<span v-else /> <!-- ghost --> |
||||
|
||||
<a-button size="small" @click="execute"> |
||||
<SyncOutlined /> |
||||
刷新 |
||||
</a-button> |
||||
</div> |
||||
|
||||
<div mt="20px"> |
||||
<Tree v-model:selected-keys="selectedKeys" :tree-data="state" :field-names="{ key: 'id' }" @select="onSelectNode"> |
||||
<template #title="{ title, data }"> |
||||
<div flex="~ items-center justify-between" py="8px" px="5px" box-border> |
||||
<div flex="1" width="0" truncate :title="title"> |
||||
{{ title }} |
||||
</div> |
||||
|
||||
<Space> |
||||
<span |
||||
v-if="hasPermission('device_group_add')" |
||||
class="i-ant-design:plus-outlined" |
||||
title="添加子分组" |
||||
@click="openModal(true, { parentId: data.id })" |
||||
/> |
||||
<span |
||||
v-if="hasPermission('device_group_edit')" |
||||
class="i-ant-design:edit-outlined" |
||||
title="编辑分组" |
||||
@click="openModal(true, { id: data.id })" |
||||
/> |
||||
<Popconfirm |
||||
v-if="hasPermission('device_group_delete')" |
||||
title="是否要删除数据?" |
||||
:class="[data.hasChildren ? 'text-gray-300 cursor-not-allowed' : '']" |
||||
:disabled="data.hasChildren" |
||||
@confirm="handleDelete(data.id)" |
||||
> |
||||
<span class="i-ant-design:delete-outlined" :title="data.hasChildren ? '存在子分组, 无法删除' : '删除分组'" /> |
||||
</Popconfirm> |
||||
</Space> |
||||
</div> |
||||
</template> |
||||
</Tree> |
||||
<Empty v-if="!state.length" :image="Empty.PRESENTED_IMAGE_SIMPLE" /> |
||||
</div> |
||||
</div> |
||||
|
||||
<GroupFormModal @register="registerModal" @success="execute" /> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped lang="less"> |
||||
:deep(.ant-tree-treenode) { |
||||
width: 100%; |
||||
} |
||||
:deep(.ant-tree-node-content-wrapper) { |
||||
flex: 1; |
||||
width: 0; |
||||
} |
||||
:deep(.ant-tree-switcher-icon) { |
||||
margin-top: 15px; |
||||
} |
||||
</style> |
@ -1,2 +0,0 @@
|
||||
export { default as GroupList } from './GroupList.vue' |
||||
export { default as BindingDeviceDrawer } from './BindingDeviceDrawer.vue' |
@ -1,60 +0,0 @@
|
||||
import { h } from 'vue' |
||||
import { Tag } from 'ant-design-vue' |
||||
import type { BasicColumn, FormSchema } from '@/components/Table' |
||||
import { useSharedProducts } from '@/views/subscription/list/data' |
||||
|
||||
export function useColumns(): BasicColumn[] { |
||||
const { products } = useSharedProducts() |
||||
return [ |
||||
{ |
||||
title: '所属产品', |
||||
dataIndex: 'productId', |
||||
customRender({ value }) { |
||||
return products.value.find(item => item.id === value)?.productName |
||||
}, |
||||
}, |
||||
{ |
||||
title: '设备序列号', |
||||
dataIndex: 'deviceSn', |
||||
}, |
||||
{ |
||||
title: '是否在线', |
||||
dataIndex: 'isOnline', |
||||
customRender({ value }) { |
||||
return h(Tag, { |
||||
color: value ? 'green' : 'default', |
||||
}, () => value ? '在线' : '离线') |
||||
}, |
||||
}, |
||||
] |
||||
} |
||||
|
||||
export function useSearchFormSchemas(): FormSchema[] { |
||||
const { products } = useSharedProducts() |
||||
return [ |
||||
{ |
||||
label: '所属产品', |
||||
field: 'productId', |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: products as any, |
||||
showSearch: true, |
||||
fieldNames: { |
||||
label: 'productName', |
||||
value: 'id', |
||||
}, |
||||
}, |
||||
colProps: { |
||||
span: 8, |
||||
}, |
||||
}, |
||||
{ |
||||
label: '设备序列号', |
||||
field: 'deviceSn', |
||||
component: 'Input', |
||||
colProps: { |
||||
span: 8, |
||||
}, |
||||
}, |
||||
] |
||||
} |
@ -1,126 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { ref } from 'vue' |
||||
import { Space } from 'ant-design-vue' |
||||
import { DisconnectOutlined, LinkOutlined } from '@ant-design/icons-vue' |
||||
import { BindingDeviceDrawer, GroupList } from './components' |
||||
import { useColumns, useSearchFormSchemas } from './data' |
||||
import { BasicTable, TableAction, useTable } from '@/components/Table' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { getDeviceListByGroup, unbindingDeviceFromGroup } from '@/api/device-manage/group' |
||||
import { useDrawer } from '@/components/Drawer' |
||||
import { noop } from '@/utils' |
||||
import { usePermission } from '@/hooks/web/usePermission' |
||||
|
||||
const { hasPermission } = usePermission() |
||||
|
||||
const [register, { getSelectRowKeys, reload }] = useTable({ |
||||
async api(params) { |
||||
if (!selectedGroupId.value) |
||||
return [] |
||||
|
||||
return getDeviceListByGroup({ |
||||
...params, |
||||
groupId: selectedGroupId.value, |
||||
}) |
||||
}, |
||||
rowKey: 'id', |
||||
columns: useColumns(), |
||||
formConfig: { |
||||
schemas: useSearchFormSchemas(), |
||||
labelWidth: 90, |
||||
}, |
||||
rowSelection: {}, |
||||
bordered: true, |
||||
canResize: false, |
||||
useSearchForm: true, |
||||
actionColumn: { |
||||
width: 210, |
||||
title: '操作', |
||||
dataIndex: 'action', |
||||
auth: ['device_group_unbinding', 'device_group_device_view'], |
||||
}, |
||||
}) |
||||
|
||||
const [registerBindingDeviceDrawer, { openDrawer }] = useDrawer() |
||||
|
||||
const selectedGroupId = ref<string | undefined>() |
||||
|
||||
const message = useMessage() |
||||
function handleBindingDivice() { |
||||
if (!selectedGroupId.value) |
||||
return message.createMessage.warn('请先选择分组') |
||||
|
||||
openDrawer(true, selectedGroupId.value) |
||||
} |
||||
function handleUnbindingDivice(id?: string) { |
||||
const selectionKeys = id ? [id] : getSelectRowKeys() |
||||
if (!selectionKeys.length) |
||||
return message.createMessage.warn('请先选择设备') |
||||
|
||||
function execute() { |
||||
unbindingDeviceFromGroup(selectedGroupId.value!, selectionKeys.join(',')) |
||||
.then(() => { |
||||
message.createMessage.success('解绑成功') |
||||
reload() |
||||
}) |
||||
.catch(noop) |
||||
} |
||||
|
||||
if (id) |
||||
return execute() |
||||
|
||||
// 批量绑定设备二次确认 |
||||
message.createConfirm({ |
||||
iconType: 'warning', |
||||
title: '是否要解绑选择的设备?', |
||||
onOk: execute, |
||||
}) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div flex="~"> |
||||
<GroupList v-model:selectedGroupId="selectedGroupId" my="12px" ml="12px" @change="reload" /> |
||||
<BasicTable flex="1" @register="register"> |
||||
<template v-if="hasPermission(['device_group_binding', 'device_group_unbinding'])" #tableTitle> |
||||
<Space> |
||||
<a-button v-if="hasPermission('device_group_binding')" @click="handleBindingDivice"> |
||||
<LinkOutlined /> |
||||
批量绑定设备 |
||||
</a-button> |
||||
<a-button v-if="hasPermission('device_group_unbinding')" @click="handleUnbindingDivice()"> |
||||
<DisconnectOutlined /> |
||||
批量解绑设备 |
||||
</a-button> |
||||
</Space> |
||||
</template> |
||||
|
||||
<template #bodyCell="{ column, record }"> |
||||
<template v-if="column.key === 'action'"> |
||||
<TableAction |
||||
:actions="[ |
||||
{ |
||||
icon: 'i-ant-design:file-search-outlined', |
||||
label: '设备详情', |
||||
auth: 'device_group_device_view', |
||||
onClick: () => $router.push(`/device-manage/device/detail/${record.id}`), |
||||
}, |
||||
{ |
||||
icon: 'i-ant-design:edit-outlined', |
||||
label: '解绑设备', |
||||
auth: 'device_group_unbinding', |
||||
popConfirm: { |
||||
title: '是否要解绑该设备?', |
||||
placement: 'left', |
||||
confirm: () => handleUnbindingDivice(record.id), |
||||
}, |
||||
}, |
||||
]" |
||||
/> |
||||
</template> |
||||
</template> |
||||
</BasicTable> |
||||
|
||||
<BindingDeviceDrawer @register="registerBindingDeviceDrawer" @success="reload" /> |
||||
</div> |
||||
</template> |
@ -1,62 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { h, ref } from 'vue' |
||||
import { Button } from 'ant-design-vue' |
||||
import { CopyOutlined } from '@ant-design/icons-vue' |
||||
import { getMessageContent } from '@/api/monitor-ops/log' |
||||
import { BasicModal, useModalInner } from '@/components/Modal' |
||||
import { noop } from '@/utils' |
||||
import type { DescItem } from '@/components/Description' |
||||
import { Description } from '@/components/Description' |
||||
import { copyText } from '@/utils/copyTextToClipboard' |
||||
import type { MessageContent } from '@/api/monitor-ops/log/types' |
||||
|
||||
defineEmits(['register']) |
||||
|
||||
const data = ref<MessageContent>() |
||||
const [register] = useModalInner((id: string) => { |
||||
getMessageContent(id) |
||||
.then((res) => { |
||||
data.value = res |
||||
}) |
||||
.catch(noop) |
||||
}) |
||||
|
||||
const schema: DescItem[] = [ |
||||
{ |
||||
field: 'topic', |
||||
label: 'Topic', |
||||
}, |
||||
{ |
||||
field: 'createTime', |
||||
label: '时间', |
||||
}, |
||||
{ |
||||
field: 'message', |
||||
label: '内容', |
||||
render(val) { |
||||
let content = val |
||||
try { |
||||
content = JSON.stringify(JSON.parse(val), null, 2) |
||||
} |
||||
catch {} |
||||
return h('div', [ |
||||
h(Button, { |
||||
size: 'small', |
||||
onClick: () => copyText(content), |
||||
}, () => [h(CopyOutlined), '复制']), |
||||
h('pre', { |
||||
style: { |
||||
marginTop: '10px', |
||||
}, |
||||
}, content), |
||||
]) |
||||
}, |
||||
}, |
||||
] |
||||
</script> |
||||
|
||||
<template> |
||||
<BasicModal title="消息内容" :footer="null" @register="register"> |
||||
<Description :data="data" :schema="schema" :column="1" /> |
||||
</BasicModal> |
||||
</template> |
@ -1,127 +0,0 @@
|
||||
import { h } from 'vue' |
||||
import { Tag } from 'ant-design-vue' |
||||
import dayjs from 'dayjs' |
||||
import type { BasicColumn, FormSchema } from '@/components/Table' |
||||
import { useSystemEnumStore } from '@/store/modules/systemEnum' |
||||
import { useSharedProducts } from '@/views/subscription/list/data' |
||||
|
||||
export function useColumns(): BasicColumn[] { |
||||
const { products } = useSharedProducts() |
||||
const { getSystemEnumLabel } = useSystemEnumStore() |
||||
|
||||
return [ |
||||
{ |
||||
title: '所属产品', |
||||
dataIndex: 'productId', |
||||
customRender({ value }) { |
||||
return products.value.find(item => item.id === value)?.productName |
||||
}, |
||||
}, |
||||
{ |
||||
title: '设备序列号', |
||||
dataIndex: 'deviceSn', |
||||
}, |
||||
{ |
||||
title: 'TraceID', |
||||
dataIndex: 'traceId', |
||||
}, |
||||
{ |
||||
title: '消息内容', |
||||
dataIndex: 'messageId', |
||||
width: 120, |
||||
}, |
||||
{ |
||||
title: '业务类型', |
||||
dataIndex: 'bizType', |
||||
width: 150, |
||||
customRender({ value }) { |
||||
return h(Tag, () => getSystemEnumLabel('eDeviceLogBizType', value)) |
||||
}, |
||||
}, |
||||
{ |
||||
title: '操作', |
||||
dataIndex: 'operation', |
||||
}, |
||||
{ |
||||
title: '时间', |
||||
dataIndex: 'createTime', |
||||
}, |
||||
{ |
||||
title: '状态', |
||||
dataIndex: 'code', |
||||
width: 120, |
||||
customRender({ value }) { |
||||
return h('b', { |
||||
style: { |
||||
color: value === 200 ? '#16a34a' : '#dc2626', |
||||
}, |
||||
}, value) |
||||
}, |
||||
}, |
||||
] |
||||
} |
||||
|
||||
export function useSearchFormSchema(): FormSchema[] { |
||||
const { products } = useSharedProducts() |
||||
const { getSystemEnums } = useSystemEnumStore() |
||||
|
||||
return [ |
||||
{ |
||||
field: 'productId', |
||||
label: '所属产品', |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: products as any, |
||||
showSearch: true, |
||||
fieldNames: { |
||||
label: 'productName', |
||||
value: 'id', |
||||
}, |
||||
}, |
||||
colProps: { |
||||
span: 6, |
||||
}, |
||||
}, |
||||
{ |
||||
field: 'deviceSn', |
||||
label: '设备序列号', |
||||
component: 'Input', |
||||
colProps: { |
||||
span: 6, |
||||
}, |
||||
}, |
||||
{ |
||||
field: 'traceId', |
||||
label: 'TraceId', |
||||
component: 'Input', |
||||
colProps: { |
||||
span: 6, |
||||
}, |
||||
}, |
||||
{ |
||||
field: 'bizType', |
||||
label: '业务类型', |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: getSystemEnums('eDeviceLogBizType'), |
||||
}, |
||||
colProps: { |
||||
span: 6, |
||||
}, |
||||
}, |
||||
{ |
||||
field: 'time', |
||||
label: '日志时间', |
||||
component: 'RangePicker', |
||||
componentProps: { |
||||
showTime: true, |
||||
disabledDate(current) { |
||||
return current && current > dayjs().endOf('day') |
||||
}, |
||||
}, |
||||
colProps: { |
||||
span: 6, |
||||
}, |
||||
}, |
||||
] |
||||
} |
@ -1,44 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { useColumns, useSearchFormSchema } from './data' |
||||
import MessageContentModal from './MessageContentModal.vue' |
||||
import { getLogList } from '@/api/monitor-ops/log' |
||||
import { BasicTable, useTable } from '@/components/Table' |
||||
import { useModal } from '@/components/Modal' |
||||
import type { GetLogListParams, Log } from '@/api/monitor-ops/log/types' |
||||
|
||||
const [registerModal, { openModal }] = useModal<string>() |
||||
|
||||
const [register] = useTable({ |
||||
api(params: GetLogListParams & { time: [string, string] }) { |
||||
return getLogList({ |
||||
...params, |
||||
queryStartTime: params.time && params.time[0], |
||||
queryEndTime: params.time && params.time[1], |
||||
}) |
||||
}, |
||||
columns: useColumns(), |
||||
formConfig: { |
||||
labelWidth: 90, |
||||
schemas: useSearchFormSchema(), |
||||
}, |
||||
bordered: true, |
||||
canResize: false, |
||||
useSearchForm: true, |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<BasicTable :api="async () => ([] as Log[])" @register="register"> |
||||
<template #bodyCell="{ column, record }"> |
||||
<template v-if="column.key === 'messageId'"> |
||||
<a-button v-if="record.messageId" type="link" size="small" @click="openModal(true, record.messageId)"> |
||||
查看 |
||||
</a-button> |
||||
</template> |
||||
</template> |
||||
</BasicTable> |
||||
|
||||
<MessageContentModal @register="registerModal" /> |
||||
</div> |
||||
</template> |
@ -1,55 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { ref } from 'vue' |
||||
import { getFormSchema } from './data' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { BasicForm, useForm } from '@/components/Form' |
||||
import { BasicModal, useModalInner } from '@/components/Modal' |
||||
import { createProduct, updateProduct } from '@/api/product' |
||||
import type { Product } from '@/api/product/types' |
||||
|
||||
defineOptions({ name: 'ProductFormModal' }) |
||||
|
||||
const emit = defineEmits(['success', 'register']) |
||||
|
||||
const isUpdate = ref(false) |
||||
const [registerForm, { setFieldsValue, validate }] = useForm({ |
||||
labelWidth: 100, |
||||
baseColProps: { span: 24 }, |
||||
schemas: getFormSchema(isUpdate), |
||||
showActionButtonGroup: false, |
||||
actionColOptions: { span: 23 }, |
||||
}) |
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data: Product) => { |
||||
isUpdate.value = true |
||||
setFieldsValue({ ...data }) |
||||
}) |
||||
|
||||
async function handleSubmit() { |
||||
try { |
||||
const values = await validate<Product>() |
||||
setModalProps({ confirmLoading: true }) |
||||
await (isUpdate.value ? updateProduct(values) : createProduct(values)) |
||||
closeModal() |
||||
emit('success') |
||||
useMessage().createMessage.success('保存成功') |
||||
} |
||||
catch {} |
||||
finally { |
||||
setModalProps({ confirmLoading: false }) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<BasicModal |
||||
width="30%" |
||||
:min-height="100" |
||||
:title="isUpdate ? '编辑' : '新增'" |
||||
:after-close="() => isUpdate = false" |
||||
@register="registerModal" |
||||
@ok="handleSubmit" |
||||
> |
||||
<BasicForm @register="registerForm" /> |
||||
</BasicModal> |
||||
</template> |
@ -1,138 +0,0 @@
|
||||
<script setup lang='ts'> |
||||
import { Alert, Popconfirm } from 'ant-design-vue' |
||||
import { useRoute } from 'vue-router' |
||||
import { EyeOutlined, PlusOutlined, SyncOutlined } from '@ant-design/icons-vue' |
||||
import { useModelService } from './composables/useModelService' |
||||
import { useModelAttribute } from './composables/useModelAttribute' |
||||
import ModelServiceFormModal from './ModelServiceFormModal.vue' |
||||
import ModelAttributeFormModal from './ModelAttributeFormModal.vue' |
||||
import { BasicTable, TableAction } from '@/components/Table' |
||||
import type { ModelAttribute } from '@/api/product/types' |
||||
import JsonPreviewModal from '@/components/JsonPreviewModal' |
||||
import { usePermission } from '@/hooks/web/usePermission' |
||||
|
||||
defineProps<{ tsl?: string }>() |
||||
|
||||
const route = useRoute() |
||||
const { |
||||
selectedModelId, |
||||
setSelectedModelId, |
||||
modelServiceList, |
||||
handleDeleteModelService, |
||||
registerModelServiceModal, |
||||
openModelServiceModal, |
||||
reloadModelService, |
||||
} = useModelService(route.params.id as string, route.params.modelId as string) |
||||
|
||||
const { |
||||
registerModelAttributeTable, |
||||
registerModelAttributeModal, |
||||
openModelAttributeModal, |
||||
reloadModalAttribute, |
||||
handleDeleteModelAttribute, |
||||
} = useModelAttribute(route.params.id as string, selectedModelId) |
||||
|
||||
const { hasPermission } = usePermission() |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<div mb="12px"> |
||||
<JsonPreviewModal title="物模型 TSL" :data="tsl || ''"> |
||||
<a-button type="primary"> |
||||
<EyeOutlined /> |
||||
物模型 TSL |
||||
</a-button> |
||||
</JsonPreviewModal> |
||||
</div> |
||||
<Alert type="info" show-icon class="py-4px text-13px" message="产品模型用于描述设备具备的能力和特性。通过定义产品模型,在平台构建一款设备的抽象模型,使平台理解该款设备支持的服务、属性、命令等信息,如颜色、开关等。当定义完一款产品模型后,在进行注册设备时,就可以使用在控制台上定义的产品模型。" /> |
||||
|
||||
<div flex="~ gap-12px" mt="12px"> |
||||
<div w="20%" border="0 r-1 solid gray-50 dark:white dark:opacity-5"> |
||||
<div flex="~ items-center justify-between" border="0 b-1 solid gray-200 dark:white dark:opacity-5" class="mb-12px box-border h-40px px-10px"> |
||||
<div font-bold> |
||||
服务列表 |
||||
</div> |
||||
<div v-if="hasPermission('product_model_service_add')" class="i-ant-design:plus-outlined cursor-pointer text-20px" @click="openModelServiceModal()" /> |
||||
</div> |
||||
|
||||
<div |
||||
v-for="item in modelServiceList" :key="item.id" |
||||
flex="~ items-center justify-between" |
||||
class="box-border h-60px cursor-pointer pl-10px hover:bg-gray-100 hover:dark:bg-white hover:dark:bg-opacity-5" |
||||
border="0 b-1 solid gray-50 dark:white dark:opacity-5" |
||||
:class="selectedModelId === item.id ? 'bg-gray-100 dark:bg-white dark:bg-opacity-5' : ''" |
||||
@click="setSelectedModelId(item.id); reloadModalAttribute(true)" |
||||
> |
||||
<div w-0 flex-1> |
||||
<div truncate> |
||||
{{ item.serviceId }} |
||||
</div> |
||||
<div mt="5px" text="12px gray-500" truncate :title="item.description"> |
||||
{{ item.description }} |
||||
</div> |
||||
</div> |
||||
<div v-if="item.serviceId !== 'DEFAULT'"> |
||||
<a-button v-if="hasPermission('product_model_service_edit')" type="link" size="small" @click="openModelServiceModal(true, item)"> |
||||
<span class="i-ant-design:edit-outlined" /> |
||||
</a-button> |
||||
<Popconfirm v-if="hasPermission('product_model_service_delete')" title="是否要删除数据?" @confirm="handleDeleteModelService(item.id)"> |
||||
<a-button type="link" size="small" danger> |
||||
<span class="i-ant-design:delete-outlined" /> |
||||
</a-button> |
||||
</Popconfirm> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div w-0 flex-1> |
||||
<BasicTable :api="async () => ([] as ModelAttribute[])" @register="registerModelAttributeTable"> |
||||
<template v-if="hasPermission('product_model_attr_add')" #tableTitle> |
||||
<a-button type="primary" @click="openModelAttributeModal"> |
||||
<PlusOutlined /> |
||||
新增属性 |
||||
</a-button> |
||||
</template> |
||||
|
||||
<template #toolbar> |
||||
<a-button size="small" @click="reloadModalAttribute()"> |
||||
<template #icon> |
||||
<SyncOutlined /> |
||||
</template> |
||||
刷新 |
||||
</a-button> |
||||
</template> |
||||
|
||||
<template #bodyCell="{ column, record }"> |
||||
<template v-if="column.key === 'action'"> |
||||
<TableAction |
||||
:actions="[ |
||||
{ |
||||
icon: 'i-ant-design:edit-outlined', |
||||
label: '编辑', |
||||
auth: 'product_model_attr_edit', |
||||
onClick: () => openModelAttributeModal(true, record), |
||||
}, |
||||
{ |
||||
icon: 'i-ant-design:delete-outlined', |
||||
danger: true, |
||||
label: '删除', |
||||
auth: 'product_model_attr_delete', |
||||
popConfirm: { |
||||
title: '是否要删除数据?', |
||||
placement: 'left', |
||||
confirm: () => handleDeleteModelAttribute(record.id), |
||||
}, |
||||
}, |
||||
]" |
||||
/> |
||||
</template> |
||||
</template> |
||||
</BasicTable> |
||||
</div> |
||||
</div> |
||||
|
||||
<ModelServiceFormModal @register="registerModelServiceModal" @success="reloadModelService" /> |
||||
<ModelAttributeFormModal :model-id="selectedModelId || ''" @register="registerModelAttributeModal" @success="reloadModalAttribute()" /> |
||||
</div> |
||||
</template> |
@ -1,234 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { ref } from 'vue' |
||||
import { FormItem, Input, InputNumber } from 'ant-design-vue' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { BasicForm, useForm } from '@/components/Form' |
||||
import { BasicModal, useModalInner } from '@/components/Modal' |
||||
import { createModelAttribute, updateModelAttribute } from '@/api/product/model' |
||||
import { ModelAttributeDataTypesEnum } from '@/api/product/types' |
||||
import type { ModelAttribute, ModelAttributeWithForm } from '@/api/product/types' |
||||
|
||||
defineOptions({ name: 'ModelAttributeFormModal' }) |
||||
const props = defineProps<{ modelId: string }>() |
||||
|
||||
const emit = defineEmits(['success', 'register']) |
||||
|
||||
const isUpdate = ref(false) |
||||
const [registerForm, { setFieldsValue, validate }] = useForm({ |
||||
labelWidth: 80, |
||||
baseColProps: { span: 24 }, |
||||
schemas: [ |
||||
{ |
||||
field: 'id', |
||||
show: false, |
||||
component: 'Input', |
||||
}, |
||||
{ |
||||
field: 'itemType', |
||||
label: '功能类型', |
||||
required: true, |
||||
component: 'Select', |
||||
defaultValue: 1, |
||||
componentProps: { |
||||
options: [ |
||||
{ label: '属性', value: 1 }, |
||||
{ label: '命令', value: 2 }, |
||||
], |
||||
}, |
||||
dynamicDisabled: () => isUpdate.value, |
||||
}, |
||||
{ |
||||
field: 'name', |
||||
label: '功能名称', |
||||
required: true, |
||||
component: 'Input', |
||||
}, |
||||
{ |
||||
field: 'identifier', |
||||
label: '标识符', |
||||
required: true, |
||||
component: 'Input', |
||||
}, |
||||
{ |
||||
field: 'dataType', |
||||
label: '数据类型', |
||||
required: true, |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: Object.values(ModelAttributeDataTypesEnum).map(value => ({ label: value, value })), |
||||
}, |
||||
dynamicDisabled: () => isUpdate.value, |
||||
}, |
||||
|
||||
// int32 / float schemas |
||||
{ |
||||
field: 'valueScope', |
||||
fields: ['min', 'max'], |
||||
label: '取值范围', |
||||
slot: 'ValueScope', |
||||
required: true, |
||||
itemProps: { |
||||
autoLink: false, |
||||
}, |
||||
defaultValue: '_', // skip the required check |
||||
ifShow: ({ values }) => [ModelAttributeDataTypesEnum.Int32, ModelAttributeDataTypesEnum.Float].includes(values.dataType), |
||||
}, |
||||
{ |
||||
field: 'scale', |
||||
label: '精度', |
||||
required: true, |
||||
helpMessage: '小数位', |
||||
component: 'InputNumber', |
||||
componentProps: { |
||||
precision: 0, |
||||
}, |
||||
ifShow: ({ values }) => values.dataType === ModelAttributeDataTypesEnum.Float, |
||||
}, |
||||
{ |
||||
field: 'unit', |
||||
label: '单位', |
||||
component: 'Input', |
||||
ifShow: ({ values }) => [ModelAttributeDataTypesEnum.Int32, ModelAttributeDataTypesEnum.Float].includes(values.dataType), |
||||
}, |
||||
|
||||
// bool schemas |
||||
{ |
||||
field: 'bool', |
||||
fields: ['trueDesc', 'falseDesc'], |
||||
label: '布尔值', |
||||
slot: 'Bool', |
||||
required: true, |
||||
itemProps: { |
||||
autoLink: false, |
||||
}, |
||||
defaultValue: '_', // skip the required check |
||||
ifShow: ({ values }) => values.dataType === ModelAttributeDataTypesEnum.Bool, |
||||
}, |
||||
|
||||
// text schemas |
||||
{ |
||||
field: 'maxLength', |
||||
label: '数据长度', |
||||
required: true, |
||||
component: 'InputNumber', |
||||
componentProps: { |
||||
precision: 0, |
||||
}, |
||||
ifShow: ({ values }) => values.dataType === ModelAttributeDataTypesEnum.Text, |
||||
}, |
||||
|
||||
{ |
||||
field: 'method', |
||||
label: '读写', |
||||
component: 'Select', |
||||
defaultValue: 'r', |
||||
componentProps: { |
||||
options: [ |
||||
{ label: '只读', value: 'r' }, |
||||
{ label: '读写', value: 'rw' }, |
||||
], |
||||
}, |
||||
}, |
||||
{ |
||||
field: 'sort', |
||||
label: '排序', |
||||
component: 'InputNumber', |
||||
componentProps: { |
||||
precision: 0, |
||||
}, |
||||
}, |
||||
], |
||||
showActionButtonGroup: false, |
||||
actionColOptions: { span: 23 }, |
||||
}) |
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data: ModelAttribute) => { |
||||
isUpdate.value = true |
||||
setFieldsValue({ ...data, ...(data.dataSpecs || {}) }) |
||||
}) |
||||
|
||||
async function handleSubmit() { |
||||
try { |
||||
const values = await validate<ModelAttributeWithForm>() |
||||
setModalProps({ confirmLoading: true }) |
||||
values.modelId = props.modelId |
||||
await (isUpdate.value ? updateModelAttribute(values) : createModelAttribute(values)) |
||||
closeModal() |
||||
emit('success') |
||||
useMessage().createMessage.success('保存成功') |
||||
} |
||||
catch {} |
||||
finally { |
||||
setModalProps({ confirmLoading: false }) |
||||
} |
||||
} |
||||
|
||||
function validatorValueScope(value1: number, value2: number) { |
||||
// eslint-disable-next-line prefer-promise-reject-errors |
||||
return value1 > value2 ? Promise.reject() : Promise.resolve() |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<BasicModal |
||||
width="40%" |
||||
:min-height="100" |
||||
:title="isUpdate ? '编辑' : '新增'" |
||||
:after-close="() => isUpdate = false" |
||||
@register="registerModal" |
||||
@ok="handleSubmit" |
||||
> |
||||
<BasicForm @register="registerForm"> |
||||
<template #ValueScope="{ model }"> |
||||
<div flex="~ justify-between gap-12px" class="custom-form-slot"> |
||||
<FormItem |
||||
name="min" |
||||
:rules="[ |
||||
{ required: true, message: '请填写最小值' }, |
||||
{ validator: (_, min) => validatorValueScope(min, model.max), message: '最小值不能大于最大值' }, |
||||
]" |
||||
> |
||||
<InputNumber v-model:value="model.min" placeholder="最小值" /> |
||||
</FormItem> |
||||
|
||||
<span mt="5px">-</span> |
||||
|
||||
<FormItem |
||||
name="max" |
||||
:rules="[ |
||||
{ required: true, message: '请填写最大值' }, |
||||
{ validator: (_, max) => validatorValueScope(model.min, max), message: '最大值不能小于最小值' }, |
||||
]" |
||||
> |
||||
<InputNumber v-model:value="model.max" placeholder="最大值" /> |
||||
</FormItem> |
||||
</div> |
||||
</template> |
||||
|
||||
<template #Bool="{ model }"> |
||||
<div flex="~ justify-between gap-12px" class="custom-form-slot"> |
||||
<FormItem name="trueDesc" :rules="[{ required: true, message: '请填写布尔值' }]"> |
||||
<Input v-model:value="model.trueDesc" addon-before="0" placeholder="如:是" /> |
||||
</FormItem> |
||||
|
||||
<span mt="5px">-</span> |
||||
|
||||
<FormItem name="falseDesc" :rules="[{ required: true, message: '请填写布尔值' }]"> |
||||
<Input v-model:value="model.falseDesc" addon-before="1" placeholder="如:否" /> |
||||
</FormItem> |
||||
</div> |
||||
</template> |
||||
</BasicForm> |
||||
</BasicModal> |
||||
</template> |
||||
|
||||
<style scoped lang="less"> |
||||
.custom-form-slot .ant-form-item { |
||||
margin-bottom: 0; |
||||
flex: 1; |
||||
|
||||
:deep(.ant-form-item-explain-error) { |
||||
position: absolute; |
||||
} |
||||
} |
||||
</style> |
@ -1,79 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { ref } from 'vue' |
||||
import { useRoute } from 'vue-router' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { BasicForm, useForm } from '@/components/Form' |
||||
import { BasicModal, useModalInner } from '@/components/Modal' |
||||
import { createModelService, updateModelService } from '@/api/product/model' |
||||
import type { ModelService } from '@/api/product/types' |
||||
|
||||
defineOptions({ name: 'ModelServiceFormModal' }) |
||||
|
||||
const emit = defineEmits(['success', 'register']) |
||||
|
||||
const isUpdate = ref(false) |
||||
const [registerForm, { setFieldsValue, validate }] = useForm({ |
||||
labelWidth: 80, |
||||
baseColProps: { span: 24 }, |
||||
schemas: [ |
||||
{ |
||||
field: 'id', |
||||
show: false, |
||||
component: 'Input', |
||||
}, |
||||
{ |
||||
field: 'serviceId', |
||||
label: '服务 ID', |
||||
required: true, |
||||
component: 'Input', |
||||
componentProps: { |
||||
maxlength: 30, |
||||
showCount: true, |
||||
}, |
||||
}, |
||||
{ |
||||
field: 'description', |
||||
label: '服务描述', |
||||
required: true, |
||||
component: 'InputTextArea', |
||||
}, |
||||
], |
||||
showActionButtonGroup: false, |
||||
actionColOptions: { span: 23 }, |
||||
}) |
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data: ModelService) => { |
||||
isUpdate.value = true |
||||
setFieldsValue({ ...data }) |
||||
}) |
||||
|
||||
const route = useRoute() |
||||
async function handleSubmit() { |
||||
try { |
||||
const values = await validate<ModelService>() |
||||
setModalProps({ confirmLoading: true }) |
||||
values.productId = route.params.id as string |
||||
await (isUpdate.value ? updateModelService(values) : createModelService(values)) |
||||
closeModal() |
||||
emit('success') |
||||
useMessage().createMessage.success('保存成功') |
||||
} |
||||
catch {} |
||||
finally { |
||||
setModalProps({ confirmLoading: false }) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<BasicModal |
||||
width="30%" |
||||
:min-height="100" |
||||
:title="isUpdate ? '编辑' : '新增'" |
||||
:after-close="() => isUpdate = false" |
||||
@register="registerModal" |
||||
@ok="handleSubmit" |
||||
> |
||||
<BasicForm @register="registerForm" /> |
||||
</BasicModal> |
||||
</template> |
@ -1,56 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
import { h } from 'vue' |
||||
import { Alert, Tag } from 'ant-design-vue' |
||||
import { LinkOutlined } from '@ant-design/icons-vue' |
||||
import type { DescItem } from '@/components/Description' |
||||
import { Description } from '@/components/Description' |
||||
import { useSystemEnumStore } from '@/store/modules/systemEnum' |
||||
import type { SubScription } from '@/api/subscription/list/types' |
||||
|
||||
defineProps<{ |
||||
data?: SubScription |
||||
}>() |
||||
|
||||
const { getSystemEnums } = useSystemEnumStore() |
||||
|
||||
const schema: DescItem[] = [ |
||||
{ |
||||
label: '订阅类型', |
||||
field: 'type', |
||||
render: () => 'mqtt', |
||||
}, |
||||
{ |
||||
label: '订阅消息', |
||||
field: 'messageType', |
||||
render: (val: string) => { |
||||
const values = val.split(',') |
||||
const types = getSystemEnums('eSubscribeMessageType') |
||||
return h( |
||||
'div', |
||||
types |
||||
.map(item => values.includes(item.value.toString()) ? item.label : null) |
||||
.filter(Boolean) |
||||
.map(name => h(Tag, () => name)), |
||||
) |
||||
}, |
||||
}, |
||||
{ |
||||
label: '创建时间', |
||||
field: 'createTime', |
||||
}, |
||||
] |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<Alert |
||||
type="info" show-icon class="py-4px" |
||||
message="服务端订阅:服务端可以直接订阅产品下多种类型的消息,例如设备上报属性、设备上报消息、设备状态变化通知、设备生命周期变更等。配置服务端订阅后配合使用【消费组】,平台会将产品下所有设备中已订阅类型的消息进行转发。" |
||||
/> |
||||
<a-button type="link" size="small" my="12px" @click="$router.push({ name: 'SubscriptionList', state: { productId: data?.productId } })"> |
||||
<LinkOutlined /> |
||||
管理订阅 |
||||
</a-button> |
||||
<Description :data="data" :schema="schema" :column="1" :label-style="{ width: '100px' }" /> |
||||
</div> |
||||
</template> |
@ -1,93 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { h, ref } from 'vue' |
||||
import { useRoute } from 'vue-router' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { BasicForm, useForm } from '@/components/Form' |
||||
import { BasicModal, useModalInner } from '@/components/Modal' |
||||
import { createTopic, updateTopic } from '@/api/product/topic' |
||||
import type { Topic } from '@/api/product/types' |
||||
import { useSystemEnumStore } from '@/store/modules/systemEnum' |
||||
|
||||
defineOptions({ name: 'CustomTopicFormModal' }) |
||||
|
||||
const emit = defineEmits(['success', 'register']) |
||||
|
||||
const isUpdate = ref(false) |
||||
const { getSystemEnums } = useSystemEnumStore() |
||||
const [registerForm, { setFieldsValue, validate }] = useForm({ |
||||
labelWidth: 100, |
||||
baseColProps: { span: 24 }, |
||||
schemas: [ |
||||
{ |
||||
field: 'id', |
||||
show: false, |
||||
component: 'Input', |
||||
}, |
||||
{ |
||||
field: 'topic', |
||||
label: 'Topic', |
||||
required: true, |
||||
// eslint-disable-next-line no-template-curly-in-string |
||||
helpMessage: 'Topic必须包含/${deviceSn}/占位符', |
||||
component: 'Input', |
||||
rules: [ |
||||
// eslint-disable-next-line no-template-curly-in-string |
||||
{ pattern: /\/\$\{deviceSn}\//, message: h('span', 'Topic必须包含/${deviceSn}/占位符') }, |
||||
], |
||||
dynamicDisabled: () => isUpdate.value, |
||||
}, |
||||
{ |
||||
field: 'topicType', |
||||
label: '操作权限', |
||||
required: true, |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: getSystemEnums('eProductTopicType'), |
||||
}, |
||||
dynamicDisabled: () => isUpdate.value, |
||||
}, |
||||
{ |
||||
field: 'topicDesc', |
||||
label: '描述', |
||||
component: 'InputTextArea', |
||||
}, |
||||
], |
||||
showActionButtonGroup: false, |
||||
actionColOptions: { span: 23 }, |
||||
}) |
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data: Topic) => { |
||||
isUpdate.value = true |
||||
setFieldsValue({ ...data }) |
||||
}) |
||||
|
||||
const route = useRoute() |
||||
async function handleSubmit() { |
||||
try { |
||||
const values = await validate<Topic>() |
||||
setModalProps({ confirmLoading: true }) |
||||
values.productId = route.params.id as string |
||||
await (isUpdate.value ? updateTopic(values) : createTopic(values)) |
||||
closeModal() |
||||
emit('success') |
||||
useMessage().createMessage.success('保存成功') |
||||
} |
||||
catch {} |
||||
finally { |
||||
setModalProps({ confirmLoading: false }) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<BasicModal |
||||
width="30%" |
||||
:min-height="100" |
||||
:title="isUpdate ? '编辑' : '新增'" |
||||
:after-close="() => isUpdate = false" |
||||
@register="registerModal" |
||||
@ok="handleSubmit" |
||||
> |
||||
<BasicForm @register="registerForm" /> |
||||
</BasicModal> |
||||
</template> |
@ -1,145 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
import { Alert, Segmented } from 'ant-design-vue' |
||||
import { computed, ref } from 'vue' |
||||
import { useRoute } from 'vue-router' |
||||
import { PlusOutlined, SyncOutlined } from '@ant-design/icons-vue' |
||||
import TopicFormModal from './TopicFormModal.vue' |
||||
import type { Topic } from '@/api/product/types' |
||||
import { TopicType } from '@/api/product/types' |
||||
import { BasicTable, TableAction, useTable } from '@/components/Table' |
||||
import { deleteTopic, getTopicList } from '@/api/product/topic' |
||||
import { useSystemEnumStore } from '@/store/modules/systemEnum' |
||||
import { useModal } from '@/components/Modal' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { usePermission } from '@/hooks/web/usePermission' |
||||
|
||||
const route = useRoute() |
||||
const { getSystemEnumLabel } = useSystemEnumStore() |
||||
|
||||
const [registerModal, { openModal }] = useModal<Topic>() |
||||
|
||||
const topicType = ref<TopicType>(TopicType.System) |
||||
const isCustomTopic = computed(() => topicType.value === TopicType.Custom) |
||||
const alertMessage = computed( |
||||
() => isCustomTopic.value |
||||
? 'Topic用以将设备数据分类上报,进而分别进行处理。除了系统预置的Topic,您也可以自定义Topic,然后在设备侧开发时选择数据上报的Topic。' |
||||
: '以下是系统Topic,设备侧通过系统预设的topic接收数据时,无需提前订阅。', |
||||
) |
||||
|
||||
const { hasPermission } = usePermission() |
||||
|
||||
const [register, { reload, setTableData }] = useTable({ |
||||
api(params) { |
||||
return getTopicList({ |
||||
...params, |
||||
productId: route.params.id, |
||||
topicCategory: topicType.value, |
||||
}) |
||||
}, |
||||
columns: [ |
||||
{ |
||||
title: 'Topic', |
||||
dataIndex: 'topic', |
||||
}, |
||||
{ |
||||
title: '操作权限', |
||||
dataIndex: 'topicType', |
||||
customRender: ({ value }) => getSystemEnumLabel('eProductTopicType', value), |
||||
}, |
||||
{ |
||||
title: '开启解析脚本', |
||||
dataIndex: 'enableScript', |
||||
ifShow: () => isCustomTopic.value, |
||||
}, |
||||
{ |
||||
title: '描述', |
||||
dataIndex: 'topicDesc', |
||||
}, |
||||
], |
||||
bordered: true, |
||||
inset: true, |
||||
canResize: false, |
||||
actionColumn: { |
||||
width: 150, |
||||
title: '操作', |
||||
dataIndex: 'action', |
||||
fixed: 'right', |
||||
ifShow: () => isCustomTopic.value, |
||||
auth: ['product_topic_edit', 'product_topic_delete'], |
||||
}, |
||||
}) |
||||
|
||||
function onChangeTopicType() { |
||||
setTableData([]) |
||||
reload() |
||||
} |
||||
|
||||
async function handleDelete(id: string) { |
||||
try { |
||||
await deleteTopic(id) |
||||
useMessage().createMessage.success('删除成功') |
||||
reload() |
||||
} |
||||
catch {} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<div flex="~ items-center justify-between gap-12px" mb="12px"> |
||||
<div flex="~ wrap gap-12px"> |
||||
<Segmented |
||||
v-model:value="topicType" |
||||
:options="[{ label: '系统 Topic', value: TopicType.System }, { label: '自定义 Topic', value: TopicType.Custom }]" |
||||
@change="onChangeTopicType" |
||||
/> |
||||
|
||||
<a-button v-if="isCustomTopic && hasPermission('product_topic_add')" type="primary" @click="openModal"> |
||||
<PlusOutlined /> |
||||
新增 Topic |
||||
</a-button> |
||||
|
||||
<Alert :message="alertMessage" type="info" show-icon class="py-4px" /> |
||||
</div> |
||||
|
||||
<div class="mr-10px flex cursor-pointer items-center"> |
||||
<a-button size="small" @click="reload"> |
||||
<template #icon> |
||||
<SyncOutlined /> |
||||
</template> |
||||
刷新 |
||||
</a-button> |
||||
</div> |
||||
</div> |
||||
|
||||
<BasicTable :api="async () => ([] as Topic[])" @register="register"> |
||||
<template #bodyCell="{ column, record }"> |
||||
<template v-if="column.key === 'action'"> |
||||
<TableAction |
||||
:actions="[ |
||||
{ |
||||
icon: 'i-ant-design:edit-outlined', |
||||
label: '编辑', |
||||
auth: 'product_topic_edit', |
||||
onClick: () => openModal(true, record), |
||||
}, |
||||
{ |
||||
icon: 'i-ant-design:delete-outlined', |
||||
danger: true, |
||||
label: '删除', |
||||
auth: 'product_topic_delete', |
||||
popConfirm: { |
||||
title: '是否要删除数据?', |
||||
placement: 'left', |
||||
confirm: () => handleDelete(record.id), |
||||
}, |
||||
}, |
||||
]" |
||||
/> |
||||
</template> |
||||
</template> |
||||
</BasicTable> |
||||
|
||||
<TopicFormModal @register="registerModal" @success="reload" /> |
||||
</div> |
||||
</template> |
@ -1,78 +0,0 @@
|
||||
import { unref } from 'vue' |
||||
import type { MaybeRef, Ref } from 'vue' |
||||
import { useTable } from '@/components/Table' |
||||
import { deleteModelAttribute, getModelAttributeList } from '@/api/product/model' |
||||
import { useModal } from '@/components/Modal' |
||||
import type { ModelAttribute } from '@/api/product/types' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
|
||||
export function useModelAttribute(productId: MaybeRef<string>, modelId: Ref<string | undefined>) { |
||||
const [registerModelAttributeTable, { reload, setPagination }] = useTable({ |
||||
async api(params) { |
||||
if (!unref(modelId)) |
||||
return [] |
||||
|
||||
return getModelAttributeList({ |
||||
...params, |
||||
productId: unref(productId), |
||||
modelId: unref(modelId), |
||||
}) |
||||
}, |
||||
columns: [ |
||||
{ |
||||
title: '功能类型', |
||||
dataIndex: 'itemType', |
||||
customRender: ({ value }) => ({ 1: '属性', 2: '命令' }[value]), |
||||
}, |
||||
{ |
||||
title: '功能名称', |
||||
dataIndex: 'name', |
||||
}, |
||||
{ |
||||
title: '标识符', |
||||
dataIndex: 'identifier', |
||||
}, |
||||
{ |
||||
title: '数据类型', |
||||
dataIndex: 'dataType', |
||||
}, |
||||
], |
||||
bordered: true, |
||||
inset: true, |
||||
canResize: false, |
||||
actionColumn: { |
||||
width: 150, |
||||
title: '操作', |
||||
dataIndex: 'action', |
||||
fixed: 'right', |
||||
auth: ['product_model_attr_delete', 'product_model_attr_edit'], |
||||
}, |
||||
}) |
||||
|
||||
const [registerModelAttributeModal, { openModal: openModelAttributeModal }] = useModal<ModelAttribute>() |
||||
|
||||
async function handleDeleteModelAttribute(id: string) { |
||||
try { |
||||
await deleteModelAttribute(id) |
||||
useMessage().createMessage.success('删除成功') |
||||
reload() |
||||
} |
||||
catch {} |
||||
} |
||||
|
||||
return { |
||||
registerModelAttributeTable, |
||||
registerModelAttributeModal, |
||||
openModelAttributeModal, |
||||
/** |
||||
* @param resetPageCurrent 是否重置分页 |
||||
*/ |
||||
reloadModalAttribute(resetPageCurrent = false) { |
||||
if (resetPageCurrent) |
||||
setPagination({ current: 1 }) |
||||
|
||||
reload() |
||||
}, |
||||
handleDeleteModelAttribute, |
||||
} |
||||
} |
@ -1,41 +0,0 @@
|
||||
import { ref } from 'vue' |
||||
import { useAsyncState, watchOnce } from '@vueuse/core' |
||||
import { deleteModelService, getAllModelServices } from '@/api/product/model' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { useModal } from '@/components/Modal' |
||||
import type { ModelService } from '@/api/product/types' |
||||
|
||||
export function useModelService(productId: string, defaultModelId?: string) { |
||||
const selectedModelId = ref<string>() |
||||
|
||||
const { state, execute } = useAsyncState(() => getAllModelServices(productId), [], { |
||||
resetOnExecute: false, |
||||
}) |
||||
|
||||
// 默认选择的 ModelId, 如果没有 defaultModelId 则是第一个元素的 Id
|
||||
watchOnce(state, () => { |
||||
if (state.value.length > 0) |
||||
selectedModelId.value = defaultModelId || state.value[0].id |
||||
}) |
||||
|
||||
const [registerModelServiceModal, { openModal }] = useModal<ModelService>() |
||||
|
||||
async function handleDeleteModelService(id: string) { |
||||
try { |
||||
await deleteModelService(id) |
||||
useMessage().createMessage.success('删除成功') |
||||
execute() |
||||
} |
||||
catch {} |
||||
} |
||||
|
||||
return { |
||||
selectedModelId, |
||||
setSelectedModelId: (id: string) => selectedModelId.value = id, |
||||
reloadModelService: execute, |
||||
modelServiceList: state, |
||||
handleDeleteModelService, |
||||
registerModelServiceModal, |
||||
openModelServiceModal: openModal, |
||||
} |
||||
} |
@ -1,3 +0,0 @@
|
||||
export { default as TopicManage } from './TopicManage.vue' |
||||
export { default as Model } from './Model.vue' |
||||
export { default as Subscription } from './Subscription.vue' |
@ -1,188 +0,0 @@
|
||||
import type { Ref } from 'vue' |
||||
import type { BasicColumn, FormSchema } from '@/components/Table' |
||||
import { useSystemEnumStoreWithOut } from '@/store/modules/systemEnum' |
||||
|
||||
const { getSystemEnumLabel, getSystemEnums } = useSystemEnumStoreWithOut() |
||||
|
||||
export const columns: BasicColumn[] = [ |
||||
{ |
||||
title: '产品名称', |
||||
dataIndex: 'productName', |
||||
}, |
||||
{ |
||||
title: '产品标识', |
||||
dataIndex: 'productKey', |
||||
}, |
||||
{ |
||||
title: '节点类型', |
||||
dataIndex: 'nodeType', |
||||
customRender: ({ value }) => getSystemEnumLabel('eProductNodeType', value), |
||||
}, |
||||
{ |
||||
title: '联网方式', |
||||
dataIndex: 'networkType', |
||||
customRender: ({ value }) => getSystemEnumLabel('eNetworkType', value), |
||||
}, |
||||
{ |
||||
title: '鉴权方式', |
||||
dataIndex: 'authType', |
||||
customRender: ({ value }) => getSystemEnumLabel('eAuthType', value), |
||||
}, |
||||
{ |
||||
title: '安全类型', |
||||
dataIndex: 'securityType', |
||||
customRender: ({ value }) => getSystemEnumLabel('eProductSecurityType', value), |
||||
}, |
||||
{ |
||||
title: '通信协议', |
||||
dataIndex: 'networkProtocol', |
||||
customRender: ({ value }) => getSystemEnumLabel('eNetworkProtocol', value), |
||||
}, |
||||
{ |
||||
title: '数据格式', |
||||
dataIndex: 'dataType', |
||||
customRender: ({ value }) => getSystemEnumLabel('eDataType', value), |
||||
}, |
||||
{ |
||||
title: '创建时间', |
||||
dataIndex: 'createTime', |
||||
width: 200, |
||||
}, |
||||
] |
||||
|
||||
export const searchFormSchemas: FormSchema[] = [ |
||||
{ |
||||
label: '产品名称', |
||||
field: 'productName', |
||||
component: 'Input', |
||||
colProps: { span: 6 }, |
||||
}, |
||||
{ |
||||
label: '联网方式', |
||||
field: 'networkType', |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: getSystemEnums('eNetworkType'), |
||||
}, |
||||
colProps: { span: 6 }, |
||||
}, |
||||
{ |
||||
label: '通信协议', |
||||
field: 'networkProtocol', |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: getSystemEnums('eNetworkProtocol'), |
||||
}, |
||||
colProps: { span: 6 }, |
||||
}, |
||||
{ |
||||
label: '节点类型', |
||||
field: 'nodeType', |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: getSystemEnums('eProductNodeType'), |
||||
}, |
||||
colProps: { span: 6 }, |
||||
}, |
||||
{ |
||||
label: '安全类型', |
||||
field: 'securityType', |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: getSystemEnums('eProductSecurityType'), |
||||
}, |
||||
colProps: { span: 6 }, |
||||
}, |
||||
{ |
||||
label: '数据格式', |
||||
field: 'dataType', |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: getSystemEnums('eDataType'), |
||||
}, |
||||
colProps: { span: 6 }, |
||||
}, |
||||
] |
||||
|
||||
export function getFormSchema(isUpdate: Ref<boolean>): FormSchema[] { |
||||
return [ |
||||
{ |
||||
field: 'id', |
||||
show: false, |
||||
component: 'Input', |
||||
}, |
||||
{ |
||||
label: '产品名称', |
||||
field: 'productName', |
||||
required: true, |
||||
component: 'Input', |
||||
}, |
||||
{ |
||||
label: '联网方式', |
||||
field: 'networkType', |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: getSystemEnums('eNetworkType'), |
||||
}, |
||||
}, |
||||
{ |
||||
label: '通信协议', |
||||
field: 'networkProtocol', |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: getSystemEnums('eNetworkProtocol'), |
||||
}, |
||||
ifShow: () => !isUpdate.value, |
||||
}, |
||||
{ |
||||
label: '节点类型', |
||||
field: 'nodeType', |
||||
required: true, |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: getSystemEnums('eProductNodeType'), |
||||
}, |
||||
ifShow: () => !isUpdate.value, |
||||
}, |
||||
{ |
||||
label: '安全类型', |
||||
field: 'securityType', |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: getSystemEnums('eProductSecurityType'), |
||||
}, |
||||
ifShow: () => !isUpdate.value, |
||||
}, |
||||
{ |
||||
label: '鉴权方式', |
||||
field: 'authType', |
||||
required: true, |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: getSystemEnums('eAuthType'), |
||||
}, |
||||
ifShow: () => !isUpdate.value, |
||||
}, |
||||
{ |
||||
label: '数据格式', |
||||
field: 'dataType', |
||||
required: true, |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: getSystemEnums('eDataType'), |
||||
}, |
||||
ifShow: () => !isUpdate.value, |
||||
}, |
||||
{ |
||||
label: '产品描述', |
||||
field: 'productDesc', |
||||
component: 'InputTextArea', |
||||
}, |
||||
] |
||||
} |
||||
|
||||
export enum ProductTabEnums { |
||||
TopicManage = '1', |
||||
Model = '2', |
||||
Subscription = '3', |
||||
} |
@ -1,96 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
import { Card, Tabs } from 'ant-design-vue' |
||||
import { useAsyncState } from '@vueuse/core' |
||||
import { useRoute } from 'vue-router' |
||||
import { ref } from 'vue' |
||||
import { Model, Subscription, TopicManage } from './components' |
||||
import { ProductTabEnums } from './data' |
||||
import { Description } from '@/components/Description' |
||||
import { getProductDetail } from '@/api/product' |
||||
import type { DescItem } from '@/components/Description' |
||||
import { useSystemEnumStore } from '@/store/modules/systemEnum' |
||||
import { usePermission } from '@/hooks/web/usePermission' |
||||
|
||||
const route = useRoute() |
||||
const { getSystemEnumLabel } = useSystemEnumStore() |
||||
|
||||
const { state } = useAsyncState(() => getProductDetail(route.params.id as string), undefined) |
||||
const schema: DescItem[] = [ |
||||
{ |
||||
label: '产品名称', |
||||
field: 'productName', |
||||
}, |
||||
{ |
||||
label: '产品标识(ProductKey)', |
||||
field: 'productKey', |
||||
}, |
||||
{ |
||||
label: 'ProductSecret', |
||||
field: 'productSecret', |
||||
}, |
||||
{ |
||||
label: '联网方式', |
||||
field: 'networkType', |
||||
render: value => getSystemEnumLabel('eNetworkType', value), |
||||
}, |
||||
{ |
||||
label: '通信协议', |
||||
field: 'networkProtocol', |
||||
render: value => getSystemEnumLabel('eNetworkProtocol', value), |
||||
}, |
||||
{ |
||||
label: '节点类型', |
||||
field: 'nodeType', |
||||
render: value => getSystemEnumLabel('eProductNodeType', value), |
||||
}, |
||||
{ |
||||
label: '安全类型', |
||||
field: 'securityType', |
||||
render: value => getSystemEnumLabel('eProductSecurityType', value), |
||||
}, |
||||
{ |
||||
label: '鉴权方式', |
||||
field: 'authType', |
||||
render: value => getSystemEnumLabel('eAuthType', value), |
||||
}, |
||||
{ |
||||
label: '数据格式', |
||||
field: 'dataType', |
||||
render: value => getSystemEnumLabel('eDataType', value), |
||||
}, |
||||
{ |
||||
label: '创建时间', |
||||
field: 'createTime', |
||||
}, |
||||
{ |
||||
label: '产品描述', |
||||
field: 'productDesc', |
||||
}, |
||||
] |
||||
|
||||
const activeTab = ref<ProductTabEnums>(route.params.activeTab as unknown as ProductTabEnums || ProductTabEnums.TopicManage) |
||||
|
||||
const { hasPermission } = usePermission() |
||||
</script> |
||||
|
||||
<template> |
||||
<div p="12px"> |
||||
<Card title="基础信息"> |
||||
<Description :data="state" :schema="schema" :column="2" /> |
||||
</Card> |
||||
|
||||
<Card mt="12px"> |
||||
<Tabs v-model:active-key="activeTab"> |
||||
<Tabs.TabPane v-if="hasPermission('product_topic_view')" :key="ProductTabEnums.TopicManage" tab="Topic 管理"> |
||||
<TopicManage /> |
||||
</Tabs.TabPane> |
||||
<Tabs.TabPane v-if="hasPermission('product_model_view')" :key="ProductTabEnums.Model" tab="物模型"> |
||||
<Model :tsl="state?.tsl" /> |
||||
</Tabs.TabPane> |
||||
<Tabs.TabPane v-if="hasPermission('product_subscription_view')" :key="ProductTabEnums.Subscription" tab="服务端订阅"> |
||||
<Subscription :data="state?.subscribe" /> |
||||
</Tabs.TabPane> |
||||
</Tabs> |
||||
</Card> |
||||
</div> |
||||
</template> |
@ -1,92 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
import { PlusOutlined } from '@ant-design/icons-vue' |
||||
import { columns, searchFormSchemas } from './data' |
||||
import ProductFormModal from './ProductFormModal.vue' |
||||
import { BasicTable, TableAction, useTable } from '@/components/Table' |
||||
import { deleteProduct, getProductList } from '@/api/product' |
||||
import { useModal } from '@/components/Modal' |
||||
import type { Product } from '@/api/product/types' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { usePermission } from '@/hooks/web/usePermission' |
||||
|
||||
defineOptions({ name: 'Product' }) |
||||
|
||||
const [registerModal, { openModal }] = useModal<Product>() |
||||
|
||||
const { hasPermission } = usePermission() |
||||
|
||||
const [register, { reload }] = useTable({ |
||||
api: getProductList, |
||||
columns, |
||||
formConfig: { |
||||
schemas: searchFormSchemas, |
||||
labelWidth: 80, |
||||
}, |
||||
bordered: true, |
||||
canResize: false, |
||||
useSearchForm: true, |
||||
actionColumn: { |
||||
width: 220, |
||||
title: '操作', |
||||
dataIndex: 'action', |
||||
fixed: 'right', |
||||
auth: ['view', 'edit', 'delete'].map(code => `product_${code}`), |
||||
}, |
||||
}) |
||||
|
||||
async function handleDelete(id: string) { |
||||
try { |
||||
await deleteProduct(id) |
||||
useMessage().createMessage.success('删除成功') |
||||
reload() |
||||
} |
||||
catch {} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<BasicTable :api="async () => ([] as Product[])" @register="register"> |
||||
<template v-if="hasPermission('product_add')" #tableTitle> |
||||
<a-button type="primary" @click="openModal"> |
||||
<PlusOutlined /> |
||||
新建 |
||||
</a-button> |
||||
</template> |
||||
|
||||
<template #bodyCell="{ column, record }"> |
||||
<template v-if="column.key === 'action'"> |
||||
<TableAction |
||||
:actions="[ |
||||
{ |
||||
icon: 'i-ant-design:file-search-outlined', |
||||
label: '详情', |
||||
auth: 'product_view', |
||||
onClick: () => $router.push(`/product/detail/${record.id}`), |
||||
}, |
||||
{ |
||||
icon: 'i-ant-design:edit-outlined', |
||||
label: '编辑', |
||||
auth: 'product_edit', |
||||
onClick: () => openModal(true, record), |
||||
}, |
||||
{ |
||||
icon: 'i-ant-design:delete-outlined', |
||||
danger: true, |
||||
label: '删除', |
||||
auth: 'product_delete', |
||||
popConfirm: { |
||||
title: '是否要删除数据?', |
||||
placement: 'left', |
||||
confirm: () => handleDelete(record.id), |
||||
}, |
||||
}, |
||||
]" |
||||
/> |
||||
</template> |
||||
</template> |
||||
</BasicTable> |
||||
|
||||
<ProductFormModal @register="registerModal" @success="reload" /> |
||||
</div> |
||||
</template> |
@ -1,65 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { ref } from 'vue' |
||||
import { formSchema } from './data' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { BasicForm, useForm } from '@/components/Form' |
||||
import { BasicModal, useModalInner } from '@/components/Modal' |
||||
import { createConsumer, updateConsumer } from '@/api/subscription/consumer' |
||||
import type { Consumer } from '@/api/subscription/consumer/types' |
||||
import { getSubscriptionDetail } from '@/api/subscription/list' |
||||
import { noop } from '@/utils' |
||||
|
||||
defineOptions({ name: 'ConsumerFormModal' }) |
||||
|
||||
const emit = defineEmits(['success', 'register']) |
||||
|
||||
const isUpdate = ref(false) |
||||
const [registerForm, { setFieldsValue, validate }] = useForm({ |
||||
labelWidth: 100, |
||||
baseColProps: { span: 24 }, |
||||
schemas: formSchema, |
||||
showActionButtonGroup: false, |
||||
actionColOptions: { span: 23 }, |
||||
}) |
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (id: string) => { |
||||
isUpdate.value = true |
||||
setModalProps({ confirmLoading: true, loading: true }) |
||||
getSubscriptionDetail(id) |
||||
.then((data) => { |
||||
setFieldsValue({ ...data, subscribeIds: data.subscribes ? data.subscribes.split(',') : [] }) |
||||
}) |
||||
.catch(noop) |
||||
.finally(() => { |
||||
setModalProps({ confirmLoading: false, loading: false }) |
||||
}) |
||||
}) |
||||
|
||||
async function handleSubmit() { |
||||
try { |
||||
const values = await validate<Consumer & { subscribeIds: string[] | string }>() |
||||
setModalProps({ confirmLoading: true }) |
||||
await (isUpdate.value ? updateConsumer(values) : createConsumer(values)) |
||||
closeModal() |
||||
emit('success') |
||||
useMessage().createMessage.success('保存成功') |
||||
} |
||||
catch {} |
||||
finally { |
||||
setModalProps({ confirmLoading: false }) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<BasicModal |
||||
width="30%" |
||||
:min-height="100" |
||||
:title="isUpdate ? '编辑' : '新增'" |
||||
:after-close="() => isUpdate = false" |
||||
@register="registerModal" |
||||
@ok="handleSubmit" |
||||
> |
||||
<BasicForm @register="registerForm" /> |
||||
</BasicModal> |
||||
</template> |
@ -1,34 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { getOnlineClientList } from '@/api/subscription/consumer' |
||||
import { BasicTable, useTable } from '@/components/Table' |
||||
|
||||
const props = defineProps<{ |
||||
consumerToken: string |
||||
}>() |
||||
|
||||
const [register] = useTable({ |
||||
api: () => getOnlineClientList(props.consumerToken), |
||||
columns: [ |
||||
{ |
||||
title: '客户端 ID', |
||||
dataIndex: 'uuid', |
||||
}, |
||||
{ |
||||
title: '客户端 IP', |
||||
dataIndex: 'address', |
||||
}, |
||||
{ |
||||
title: '最后上线时间', |
||||
dataIndex: 'onlineDate', |
||||
}, |
||||
], |
||||
bordered: true, |
||||
inset: true, |
||||
canResize: false, |
||||
pagination: false, |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<BasicTable @register="register" /> |
||||
</template> |
@ -1,92 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { h, ref } from 'vue' |
||||
import { Popconfirm, Space, Tag } from 'ant-design-vue' |
||||
import { DisconnectOutlined } from '@ant-design/icons-vue' |
||||
import { useAsyncState } from '@vueuse/core' |
||||
import { disSubscription, getSubscribeList } from '@/api/subscription/consumer' |
||||
import { BasicTable, useTable } from '@/components/Table' |
||||
import { useSystemEnumStore } from '@/store/modules/systemEnum' |
||||
import { getAllProducts } from '@/api/product' |
||||
import type { Product } from '@/api/subscription/consumer/types' |
||||
import { noop } from '@/utils' |
||||
|
||||
const props = defineProps<{ consumerId: string }>() |
||||
|
||||
const { getSystemEnums } = useSystemEnumStore() |
||||
|
||||
const { state: products } = useAsyncState(getAllProducts, []) |
||||
|
||||
const [register, { reload }] = useTable({ |
||||
api: () => getSubscribeList(props.consumerId), |
||||
columns: [ |
||||
{ |
||||
title: '产品名称', |
||||
dataIndex: 'productId', |
||||
customRender({ value }) { |
||||
const product = products.value.find(item => item.id === value) |
||||
return product && product.productName |
||||
}, |
||||
}, |
||||
{ |
||||
title: '推送消息类型', |
||||
dataIndex: 'messageType', |
||||
customRender({ value }) { |
||||
const values = value.split(',') |
||||
const types = getSystemEnums('eSubscribeMessageType') |
||||
return h( |
||||
Space, |
||||
() => types |
||||
.map(item => values.includes(item.value.toString()) ? item.label : null) |
||||
.filter(Boolean) |
||||
.map(name => h(Tag, () => name)), |
||||
) |
||||
}, |
||||
}, |
||||
], |
||||
bordered: true, |
||||
inset: true, |
||||
canResize: false, |
||||
pagination: false, |
||||
actionColumn: { |
||||
width: 200, |
||||
title: '操作', |
||||
dataIndex: 'action', |
||||
fixed: 'right', |
||||
auth: ['consumer_disconnect'], |
||||
}, |
||||
}) |
||||
|
||||
const loading = ref(false) |
||||
function handleDisconnect(serverSubscribeId: string) { |
||||
loading.value = true |
||||
disSubscription(props.consumerId, serverSubscribeId) |
||||
.then(() => { |
||||
reload() |
||||
}) |
||||
.catch(noop) |
||||
.finally(() => { |
||||
loading.value = false |
||||
}) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<BasicTable :api="async () => ([] as Product[])" @register="register"> |
||||
<template #bodyCell="{ column, record }"> |
||||
<template v-if="column.key === 'action'"> |
||||
<Popconfirm |
||||
title="删除消费组后,该组内的消费者都将立即停止接收消息,并会清除所有相关资源,您确定要删除吗?" |
||||
ok-text="确定" |
||||
cancel-text="取消" |
||||
placement="left" |
||||
@confirm="() => handleDisconnect(record.id)" |
||||
> |
||||
<a-button type="link" size="small" danger :loading="loading"> |
||||
<DisconnectOutlined /> |
||||
解除订阅 |
||||
</a-button> |
||||
</Popconfirm> |
||||
</template> |
||||
</template> |
||||
</BasicTable> |
||||
</template> |
@ -1,2 +0,0 @@
|
||||
export { default as OnlineClient } from './OnlineClient.vue' |
||||
export { default as Product } from './Product.vue' |
@ -1,50 +0,0 @@
|
||||
import type { BasicColumn, FormSchema } from '@/components/Table' |
||||
import { getAllSubscription } from '@/api/subscription/list' |
||||
|
||||
export const columns: BasicColumn[] = [ |
||||
{ |
||||
title: '消费组名称', |
||||
dataIndex: 'consumerName', |
||||
}, |
||||
{ |
||||
title: '消费组 Token', |
||||
dataIndex: 'consumerToken', |
||||
}, |
||||
{ |
||||
title: '创建时间', |
||||
dataIndex: 'createTime', |
||||
}, |
||||
] |
||||
|
||||
export const searchFormSchema: FormSchema[] = [ |
||||
{ |
||||
field: 'consumerName', |
||||
label: '消费组名称', |
||||
component: 'Input', |
||||
colProps: { |
||||
span: 6, |
||||
}, |
||||
}, |
||||
] |
||||
|
||||
export const formSchema: FormSchema[] = [ |
||||
{ |
||||
field: 'consumerName', |
||||
fields: ['id'], |
||||
label: '消费组名称', |
||||
required: true, |
||||
component: 'Input', |
||||
}, |
||||
{ |
||||
field: 'subscribeIds', |
||||
label: '订阅', |
||||
required: true, |
||||
component: 'ApiSelect', |
||||
componentProps: { |
||||
api: getAllSubscription, |
||||
mode: 'multiple', |
||||
valueField: 'id', |
||||
labelField: 'productName', |
||||
}, |
||||
}, |
||||
] |
@ -1,99 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { useAsyncState } from '@vueuse/core' |
||||
import { Button, Card, Tabs } from 'ant-design-vue' |
||||
import { CopyOutlined } from '@ant-design/icons-vue' |
||||
import { useRoute } from 'vue-router' |
||||
import { h } from 'vue' |
||||
import { OnlineClient, Product } from './components' |
||||
import { getConsumerDetail } from '@/api/subscription/consumer' |
||||
import type { DescItem } from '@/components/Description' |
||||
import { Description } from '@/components/Description' |
||||
import { copyText } from '@/utils/copyTextToClipboard' |
||||
import { usePermission } from '@/hooks/web/usePermission' |
||||
|
||||
const route = useRoute() |
||||
const { state } = useAsyncState(() => getConsumerDetail(route.params.id as string), undefined) |
||||
const schema: DescItem[] = [ |
||||
{ |
||||
field: 'consumerToken', |
||||
label: '消费组 Token', |
||||
render(value) { |
||||
return h('div', [ |
||||
value, |
||||
h(Button, { |
||||
size: 'small', |
||||
type: 'link', |
||||
style: { |
||||
marginLeft: '5px', |
||||
}, |
||||
onClick: () => copyText(value), |
||||
}, () => [h(CopyOutlined), '复制']), |
||||
]) |
||||
}, |
||||
}, |
||||
{ |
||||
field: 'consumerName', |
||||
label: '消费组名称', |
||||
}, |
||||
{ |
||||
field: 'tenantId', |
||||
label: '企业编号', |
||||
render(value) { |
||||
return h('div', [ |
||||
value, |
||||
h(Button, { |
||||
size: 'small', |
||||
type: 'link', |
||||
style: { |
||||
marginLeft: '5px', |
||||
}, |
||||
onClick: () => copyText(value), |
||||
}, () => [h(CopyOutlined), '复制']), |
||||
]) |
||||
}, |
||||
}, |
||||
{ |
||||
field: 'Topic', |
||||
label: 'Topic', |
||||
render(_, data) { |
||||
const value = `$SYS/${data.tenantId}/${data.consumerToken}/status ` |
||||
return h('div', [ |
||||
value, |
||||
h(Button, { |
||||
size: 'small', |
||||
type: 'link', |
||||
style: { |
||||
marginLeft: '5px', |
||||
}, |
||||
onClick: () => copyText(value), |
||||
}, () => [h(CopyOutlined), '复制']), |
||||
]) |
||||
}, |
||||
}, |
||||
{ |
||||
field: 'createTime', |
||||
label: '创建时间', |
||||
}, |
||||
] |
||||
|
||||
const { hasPermission } = usePermission() |
||||
</script> |
||||
|
||||
<template> |
||||
<div p="12px"> |
||||
<Card title="基础信息"> |
||||
<Description :data="state" :schema="schema" :column="1" :label-style="{ width: '140px' }" /> |
||||
</Card> |
||||
|
||||
<Card v-if="hasPermission(['consumer_client_view', 'consumer_product_view'])" mt="12px"> |
||||
<Tabs> |
||||
<Tabs.TabPane v-if="hasPermission('consumer_client_view')" key="1" tab="在线客户端"> |
||||
<OnlineClient v-if="state" :consumer-token="state.consumerToken" /> |
||||
</Tabs.TabPane> |
||||
<Tabs.TabPane v-if="hasPermission('consumer_product_view')" key="2" tab="订阅产品"> |
||||
<Product v-if="state" :consumer-id="state.id" /> |
||||
</Tabs.TabPane> |
||||
</Tabs> |
||||
</Card> |
||||
</div> |
||||
</template> |
@ -1,91 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { PlusOutlined } from '@ant-design/icons-vue' |
||||
import { columns, searchFormSchema } from './data' |
||||
import ConsumerFormModal from './ConsumerFormModal.vue' |
||||
import { BasicTable, TableAction, useTable } from '@/components/Table' |
||||
import { deleteConsumer, getConsumerList } from '@/api/subscription/consumer' |
||||
import { useModal } from '@/components/Modal' |
||||
import type { Consumer } from '@/api/subscription/consumer/types' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { usePermission } from '@/hooks/web/usePermission' |
||||
|
||||
defineOptions({ name: 'Consumer' }) |
||||
|
||||
const [registerModal, { openModal }] = useModal<string>() |
||||
|
||||
const { hasPermission } = usePermission() |
||||
|
||||
const [register, { reload }] = useTable({ |
||||
api: getConsumerList, |
||||
columns, |
||||
formConfig: { |
||||
labelWidth: 80, |
||||
schemas: searchFormSchema, |
||||
}, |
||||
bordered: true, |
||||
canResize: false, |
||||
useSearchForm: true, |
||||
actionColumn: { |
||||
width: 220, |
||||
title: '操作', |
||||
dataIndex: 'action', |
||||
fixed: 'right', |
||||
}, |
||||
}) |
||||
|
||||
async function handleDelete(id: string) { |
||||
try { |
||||
await deleteConsumer(id) |
||||
useMessage().createMessage.success('删除成功') |
||||
reload() |
||||
} |
||||
catch {} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<BasicTable :api="async () => ([] as Consumer[])" @register="register"> |
||||
<template v-if="hasPermission('consumer_add')" #tableTitle> |
||||
<a-button type="primary" @click="openModal()"> |
||||
<PlusOutlined /> |
||||
创建 |
||||
</a-button> |
||||
</template> |
||||
|
||||
<template #bodyCell="{ column, record }"> |
||||
<template v-if="column.key === 'action'"> |
||||
<TableAction |
||||
:actions="[ |
||||
{ |
||||
icon: 'i-ant-design:file-search-outlined', |
||||
label: '详情', |
||||
auth: 'consumer_view', |
||||
onClick: () => $router.push(`/subscription/consumer/detail/${record.id}`), |
||||
}, |
||||
{ |
||||
icon: 'i-ant-design:edit-outlined', |
||||
label: '修改', |
||||
auth: 'consumer_edit', |
||||
onClick: () => openModal(true, record.id), |
||||
}, |
||||
{ |
||||
icon: 'i-ant-design:delete-outlined', |
||||
danger: true, |
||||
label: '删除', |
||||
auth: 'consumer_delete', |
||||
popConfirm: { |
||||
title: '确定要删除数据吗?', |
||||
placement: 'left', |
||||
confirm: () => handleDelete(record.id), |
||||
}, |
||||
}, |
||||
]" |
||||
/> |
||||
</template> |
||||
</template> |
||||
</BasicTable> |
||||
|
||||
<ConsumerFormModal @register="registerModal" @success="reload" /> |
||||
</div> |
||||
</template> |
@ -1,59 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { ref } from 'vue' |
||||
import { useFormSchema } from './data' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { BasicForm, useForm } from '@/components/Form' |
||||
import { BasicModal, useModalInner } from '@/components/Modal' |
||||
import { createSubscription, updateSubscription } from '@/api/subscription/list' |
||||
import type { SubScription } from '@/api/subscription/list/types' |
||||
|
||||
defineOptions({ name: 'ProductFormModal' }) |
||||
|
||||
const emit = defineEmits(['success', 'register']) |
||||
|
||||
const isUpdate = ref(false) |
||||
const [registerForm, { setFieldsValue, validate }] = useForm({ |
||||
labelWidth: 100, |
||||
baseColProps: { span: 24 }, |
||||
schemas: useFormSchema(isUpdate), |
||||
showActionButtonGroup: false, |
||||
actionColOptions: { span: 23 }, |
||||
}) |
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data: SubScription) => { |
||||
isUpdate.value = true |
||||
setFieldsValue({ |
||||
...data, |
||||
messageType: data.messageType.split(',').map(Number), |
||||
}) |
||||
}) |
||||
|
||||
async function handleSubmit() { |
||||
try { |
||||
const values = await validate<Pick<SubScription, 'productId' | 'id' | 'messageType'>>() |
||||
setModalProps({ confirmLoading: true }) |
||||
values.messageType = (values.messageType as unknown as string[]).join(',') |
||||
await (isUpdate.value ? updateSubscription(values) : createSubscription(values)) |
||||
closeModal() |
||||
emit('success') |
||||
useMessage().createMessage.success('保存成功') |
||||
} |
||||
catch {} |
||||
finally { |
||||
setModalProps({ confirmLoading: false }) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<BasicModal |
||||
width="30%" |
||||
:min-height="100" |
||||
:title="isUpdate ? '编辑' : '新增'" |
||||
:after-close="() => isUpdate = false" |
||||
@register="registerModal" |
||||
@ok="handleSubmit" |
||||
> |
||||
<BasicForm @register="registerForm" /> |
||||
</BasicModal> |
||||
</template> |
@ -1,104 +0,0 @@
|
||||
import { h } from 'vue' |
||||
import type { Ref } from 'vue' |
||||
import { Space, Tag } from 'ant-design-vue' |
||||
import { createSharedComposable, useAsyncState } from '@vueuse/core' |
||||
import type { BasicColumn, FormSchema } from '@/components/Table' |
||||
import { useSystemEnumStore } from '@/store/modules/systemEnum' |
||||
import { getAllProducts } from '@/api/product' |
||||
|
||||
export const useSharedProducts = createSharedComposable(() => { |
||||
const { state } = useAsyncState(getAllProducts, []) |
||||
return { products: state } |
||||
}) |
||||
|
||||
export function useColumns(): BasicColumn[] { |
||||
const { getSystemEnums } = useSystemEnumStore() |
||||
const { products } = useSharedProducts() |
||||
|
||||
return [ |
||||
{ |
||||
title: '产品名称', |
||||
dataIndex: 'productId', |
||||
customRender({ value }) { |
||||
return products.value.find(item => item.id === value)?.productName |
||||
}, |
||||
}, |
||||
{ |
||||
title: '推送消息类型', |
||||
dataIndex: 'messageType', |
||||
customRender({ value }) { |
||||
const values = value.split(',') |
||||
const types = getSystemEnums('eSubscribeMessageType') |
||||
return h( |
||||
Space, |
||||
() => types |
||||
.map(item => values.includes(item.value.toString()) ? item.label : null) |
||||
.filter(Boolean) |
||||
.map(name => h(Tag, () => name)), |
||||
) |
||||
}, |
||||
}, |
||||
{ |
||||
title: '创建时间', |
||||
dataIndex: 'createTime', |
||||
}, |
||||
] |
||||
} |
||||
|
||||
export function useSearchFormSchema(productId?: string): FormSchema[] { |
||||
const { products } = useSharedProducts() |
||||
|
||||
return [ |
||||
{ |
||||
field: 'productId', |
||||
label: '产品名称', |
||||
component: 'Select', |
||||
componentProps: { |
||||
options: products as any, |
||||
showSearch: true, |
||||
fieldNames: { |
||||
label: 'productName', |
||||
value: 'id', |
||||
}, |
||||
}, |
||||
defaultValue: productId || undefined, |
||||
colProps: { |
||||
span: 6, |
||||
}, |
||||
}, |
||||
] |
||||
} |
||||
|
||||
export function useFormSchema(isUpload: Ref<boolean>): FormSchema[] { |
||||
const { products } = useSharedProducts() |
||||
const { getSystemEnums } = useSystemEnumStore() |
||||
|
||||
return [ |
||||
{ |
||||
field: 'productId', |
||||
fields: ['id'], |
||||
label: '产品名称', |
||||
required: true, |
||||
component: 'ApiSelect', |
||||
componentProps: { |
||||
options: products as any, |
||||
showSearch: true, |
||||
fieldNames: { |
||||
label: 'productName', |
||||
value: 'id', |
||||
}, |
||||
}, |
||||
dynamicDisabled: () => isUpload.value, |
||||
}, |
||||
{ |
||||
field: 'messageType', |
||||
label: '推送消息类型', |
||||
required: true, |
||||
component: 'Select', |
||||
componentProps: { |
||||
mode: 'multiple', |
||||
options: getSystemEnums('eSubscribeMessageType'), |
||||
}, |
||||
}, |
||||
] |
||||
} |
@ -1,86 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { PlusOutlined } from '@ant-design/icons-vue' |
||||
import { useColumns, useSearchFormSchema } from './data' |
||||
import SubscriptionFormModal from './SubscriptionFormModal.vue' |
||||
import { BasicTable, TableAction, useTable } from '@/components/Table' |
||||
import { deleteSubscription, getSubscriptionList } from '@/api/subscription/list' |
||||
import { useModal } from '@/components/Modal' |
||||
import type { SubScription } from '@/api/subscription/list/types' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { usePermission } from '@/hooks/web/usePermission' |
||||
|
||||
defineOptions({ name: 'Subscription' }) |
||||
|
||||
const [registerModal, { openModal }] = useModal<SubScription>() |
||||
|
||||
const { hasPermission } = usePermission() |
||||
|
||||
const [register, { reload }] = useTable({ |
||||
api: getSubscriptionList, |
||||
columns: useColumns(), |
||||
formConfig: { |
||||
labelWidth: 80, |
||||
schemas: useSearchFormSchema(history.state.productId), |
||||
}, |
||||
bordered: true, |
||||
canResize: false, |
||||
useSearchForm: true, |
||||
actionColumn: { |
||||
width: 160, |
||||
title: '操作', |
||||
dataIndex: 'action', |
||||
fixed: 'right', |
||||
auth: ['subscribe_edit', 'subscribe_delete'], |
||||
}, |
||||
}) |
||||
|
||||
async function handleDelete(id: string) { |
||||
try { |
||||
await deleteSubscription(id) |
||||
useMessage().createMessage.success('删除成功') |
||||
reload() |
||||
} |
||||
catch {} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<BasicTable :api="async () => ([] as SubScription[])" @register="register"> |
||||
<template v-if="hasPermission('subscribe_add')" #tableTitle> |
||||
<a-button type="primary" @click="openModal()"> |
||||
<PlusOutlined /> |
||||
创建 |
||||
</a-button> |
||||
</template> |
||||
|
||||
<template #bodyCell="{ column, record }"> |
||||
<template v-if="column.key === 'action'"> |
||||
<TableAction |
||||
:actions="[ |
||||
{ |
||||
icon: 'i-ant-design:edit-outlined', |
||||
label: '修改', |
||||
auth: 'subscribe_edit', |
||||
onClick: () => openModal(true, record), |
||||
}, |
||||
{ |
||||
icon: 'i-ant-design:delete-outlined', |
||||
danger: true, |
||||
label: '删除', |
||||
auth: 'subscribe_delete', |
||||
popConfirm: { |
||||
title: '确定要删除数据吗?', |
||||
placement: 'left', |
||||
confirm: () => handleDelete(record.id), |
||||
}, |
||||
}, |
||||
]" |
||||
/> |
||||
</template> |
||||
</template> |
||||
</BasicTable> |
||||
|
||||
<SubscriptionFormModal @register="registerModal" @success="reload" /> |
||||
</div> |
||||
</template> |
Loading…
Reference in new issue