Browse Source

chore: cleanup

main
刘凯 1 year ago
parent
commit
5ee4b64126
  1. 32
      src/api/device-manage/device/cloud-command.ts
  2. 78
      src/api/device-manage/device/index.ts
  3. 46
      src/api/device-manage/device/types.ts
  4. 64
      src/api/device-manage/group/index.ts
  5. 12
      src/api/device-manage/group/types.ts
  6. 18
      src/api/monitor-ops/log/index.ts
  7. 25
      src/api/monitor-ops/log/types.ts
  8. 42
      src/api/product/index.ts
  9. 68
      src/api/product/model.ts
  10. 29
      src/api/product/topic.ts
  11. 120
      src/api/product/types.ts
  12. 62
      src/api/subscription/consumer/index.ts
  13. 18
      src/api/subscription/consumer/types.ts
  14. 44
      src/api/subscription/list/index.ts
  15. 11
      src/api/subscription/list/types.ts
  16. 55
      src/views/device-manage/device/DeviceFormModal.vue
  17. 104
      src/views/device-manage/device/components/CloudCommand.vue
  18. 114
      src/views/device-manage/device/components/DeviceInfo.vue
  19. 59
      src/views/device-manage/device/components/MessageModal.vue
  20. 77
      src/views/device-manage/device/components/MqttParamsModal.vue
  21. 185
      src/views/device-manage/device/components/SendCommandModal.vue
  22. 53
      src/views/device-manage/device/components/TopicList.vue
  23. 84
      src/views/device-manage/device/components/composables/useDeviceInfo.ts
  24. 27
      src/views/device-manage/device/components/composables/useDeviceProperties.ts
  25. 4
      src/views/device-manage/device/components/index.ts
  26. 123
      src/views/device-manage/device/data.ts
  27. 39
      src/views/device-manage/device/detail.vue
  28. 109
      src/views/device-manage/device/index.vue
  29. 101
      src/views/device-manage/group/components/BindingDeviceDrawer.vue
  30. 86
      src/views/device-manage/group/components/GroupFormModal.vue
  31. 112
      src/views/device-manage/group/components/GroupList.vue
  32. 2
      src/views/device-manage/group/components/index.ts
  33. 60
      src/views/device-manage/group/data.ts
  34. 126
      src/views/device-manage/group/index.vue
  35. 62
      src/views/monitor-ops/log/MessageContentModal.vue
  36. 127
      src/views/monitor-ops/log/data.ts
  37. 44
      src/views/monitor-ops/log/index.vue
  38. 55
      src/views/product/ProductFormModal.vue
  39. 138
      src/views/product/components/Model.vue
  40. 234
      src/views/product/components/ModelAttributeFormModal.vue
  41. 79
      src/views/product/components/ModelServiceFormModal.vue
  42. 56
      src/views/product/components/Subscription.vue
  43. 93
      src/views/product/components/TopicFormModal.vue
  44. 145
      src/views/product/components/TopicManage.vue
  45. 78
      src/views/product/components/composables/useModelAttribute.ts
  46. 41
      src/views/product/components/composables/useModelService.ts
  47. 3
      src/views/product/components/index.ts
  48. 188
      src/views/product/data.ts
  49. 96
      src/views/product/detail.vue
  50. 92
      src/views/product/index.vue
  51. 65
      src/views/subscription/consumer/ConsumerFormModal.vue
  52. 34
      src/views/subscription/consumer/components/OnlineClient.vue
  53. 92
      src/views/subscription/consumer/components/Product.vue
  54. 2
      src/views/subscription/consumer/components/index.ts
  55. 50
      src/views/subscription/consumer/data.ts
  56. 99
      src/views/subscription/consumer/detail.vue
  57. 91
      src/views/subscription/consumer/index.vue
  58. 59
      src/views/subscription/list/SubscriptionFormModal.vue
  59. 104
      src/views/subscription/list/data.ts
  60. 86
      src/views/subscription/list/index.vue

32
src/api/device-manage/device/cloud-command.ts

@ -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,
})
}

78
src/api/device-manage/device/index.ts

@ -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,
},
})
}

46
src/api/device-manage/device/types.ts

@ -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
}

64
src/api/device-manage/group/index.ts

@ -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,
},
})
}

12
src/api/device-manage/group/types.ts

@ -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
}

18
src/api/monitor-ops/log/index.ts

@ -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,
},
})
}

25
src/api/monitor-ops/log/types.ts

@ -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
}

42
src/api/product/index.ts

@ -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',
})
}

68
src/api/product/model.ts

@ -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,
},
})
}

29
src/api/product/topic.ts

@ -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}`,
})
}

120
src/api/product/types.ts

@ -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
}
}

62
src/api/subscription/consumer/index.ts

@ -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}`,
})
}

18
src/api/subscription/consumer/types.ts

@ -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
}

44
src/api/subscription/list/index.ts

@ -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,
},
})
}

11
src/api/subscription/list/types.ts

@ -1,11 +0,0 @@
export interface GetSubscriptionListParams extends PageParam {
productId?: string
}
export interface SubScription {
id: string
messageType: string
productId: string
tenantId: string
createTime: string
}

55
src/views/device-manage/device/DeviceFormModal.vue

@ -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>

104
src/views/device-manage/device/components/CloudCommand.vue

@ -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>

114
src/views/device-manage/device/components/DeviceInfo.vue

@ -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>

59
src/views/device-manage/device/components/MessageModal.vue

@ -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>

77
src/views/device-manage/device/components/MqttParamsModal.vue

@ -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>

185
src/views/device-manage/device/components/SendCommandModal.vue

@ -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>

53
src/views/device-manage/device/components/TopicList.vue

@ -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>

84
src/views/device-manage/device/components/composables/useDeviceInfo.ts

@ -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,
}
}

27
src/views/device-manage/device/components/composables/useDeviceProperties.ts

@ -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,
}
}

4
src/views/device-manage/device/components/index.ts

@ -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'

123
src/views/device-manage/device/data.ts

@ -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',
},
]
}

39
src/views/device-manage/device/detail.vue

@ -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>

109
src/views/device-manage/device/index.vue

@ -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>

101
src/views/device-manage/group/components/BindingDeviceDrawer.vue

@ -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>

86
src/views/device-manage/group/components/GroupFormModal.vue

@ -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>

112
src/views/device-manage/group/components/GroupList.vue

@ -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>

2
src/views/device-manage/group/components/index.ts

@ -1,2 +0,0 @@
export { default as GroupList } from './GroupList.vue'
export { default as BindingDeviceDrawer } from './BindingDeviceDrawer.vue'

60
src/views/device-manage/group/data.ts

@ -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,
},
},
]
}

126
src/views/device-manage/group/index.vue

@ -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>

62
src/views/monitor-ops/log/MessageContentModal.vue

@ -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>

127
src/views/monitor-ops/log/data.ts

@ -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,
},
},
]
}

44
src/views/monitor-ops/log/index.vue

@ -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>

55
src/views/product/ProductFormModal.vue

@ -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>

138
src/views/product/components/Model.vue

@ -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>

234
src/views/product/components/ModelAttributeFormModal.vue

@ -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>

79
src/views/product/components/ModelServiceFormModal.vue

@ -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>

56
src/views/product/components/Subscription.vue

@ -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>

93
src/views/product/components/TopicFormModal.vue

@ -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>

145
src/views/product/components/TopicManage.vue

@ -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>

78
src/views/product/components/composables/useModelAttribute.ts

@ -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,
}
}

41
src/views/product/components/composables/useModelService.ts

@ -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,
}
}

3
src/views/product/components/index.ts

@ -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'

188
src/views/product/data.ts

@ -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',
}

96
src/views/product/detail.vue

@ -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>

92
src/views/product/index.vue

@ -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>

65
src/views/subscription/consumer/ConsumerFormModal.vue

@ -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>

34
src/views/subscription/consumer/components/OnlineClient.vue

@ -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>

92
src/views/subscription/consumer/components/Product.vue

@ -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>

2
src/views/subscription/consumer/components/index.ts

@ -1,2 +0,0 @@
export { default as OnlineClient } from './OnlineClient.vue'
export { default as Product } from './Product.vue'

50
src/views/subscription/consumer/data.ts

@ -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',
},
},
]

99
src/views/subscription/consumer/detail.vue

@ -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>

91
src/views/subscription/consumer/index.vue

@ -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>

59
src/views/subscription/list/SubscriptionFormModal.vue

@ -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>

104
src/views/subscription/list/data.ts

@ -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'),
},
},
]
}

86
src/views/subscription/list/index.vue

@ -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…
Cancel
Save