9 changed files with 543 additions and 1 deletions
@ -0,0 +1,64 @@
|
||||
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, |
||||
}, |
||||
}) |
||||
} |
@ -0,0 +1,12 @@
|
||||
export interface DeviceGroup { |
||||
id: string |
||||
parentId: string |
||||
groupName: string |
||||
remark: string |
||||
} |
||||
|
||||
export interface GetdeviceListByGroupParams extends PageParam { |
||||
groupId?: string |
||||
productId?: string |
||||
deviceSn?: string |
||||
} |
@ -0,0 +1,101 @@
|
||||
<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> |
@ -0,0 +1,86 @@
|
||||
<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> |
@ -0,0 +1,96 @@
|
||||
<script lang="ts" setup> |
||||
import { ref, watch } from 'vue' |
||||
import { PlusOutlined, SyncOutlined } from '@ant-design/icons-vue' |
||||
import { useAsyncState } from '@vueuse/core' |
||||
import { Popconfirm, Space, Tree } from 'ant-design-vue' |
||||
import GroupFormModal from './GroupFormModal.vue' |
||||
import { useModal } from '@/components/Modal' |
||||
import { deleteDevicegroup, getDeviceGroupTree } from '@/api/device-manage/group' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
|
||||
defineProps<{ selectedGroupId: string | undefined }>() |
||||
const emit = defineEmits(['update:selectedGroupId']) |
||||
|
||||
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[]>([]) |
||||
watch(selectedKeys, (keys, oldKeys) => { |
||||
const value = keys[0] |
||||
if (!value) { |
||||
// can't unselect |
||||
selectedKeys.value = oldKeys |
||||
return |
||||
} |
||||
|
||||
emit('update:selectedGroupId', value) |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div rounded="6px" px="10px" pt="12px" pb="6px" border-box bg-white min-w="360px"> |
||||
<div> |
||||
<div flex="~ items-center justify-between" h="35px"> |
||||
<a-button size="small" @click="openModal()"> |
||||
<PlusOutlined /> |
||||
添加根分组 |
||||
</a-button> |
||||
|
||||
<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' }"> |
||||
<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 class="i-ant-design:plus-outlined" title="添加子分组" @click="openModal(true, { parentId: data.id })" /> |
||||
<span class="i-ant-design:edit-outlined" title="编辑分组" @click="openModal(true, { id: data.id })" /> |
||||
<Popconfirm |
||||
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> |
||||
</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> |
@ -0,0 +1,2 @@
|
||||
export { default as GroupList } from './GroupList.vue' |
||||
export { default as BindingDeviceDrawer } from './BindingDeviceDrawer.vue' |
@ -0,0 +1,60 @@
|
||||
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, |
||||
}, |
||||
}, |
||||
] |
||||
} |
@ -0,0 +1,121 @@
|
||||
<script lang="ts" setup> |
||||
import { ref, watch } 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' |
||||
|
||||
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', |
||||
}, |
||||
}) |
||||
|
||||
const [registerBindingDeviceDrawer, { openDrawer }] = useDrawer() |
||||
|
||||
const selectedGroupId = ref<string | undefined>() |
||||
watch(selectedGroupId, reload) |
||||
|
||||
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" /> |
||||
<BasicTable flex="1" @register="register"> |
||||
<template #tableTitle> |
||||
<Space> |
||||
<a-button @click="handleBindingDivice"> |
||||
<LinkOutlined /> |
||||
批量绑定设备 |
||||
</a-button> |
||||
<a-button @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: '设备详情', |
||||
onClick: () => $router.push(`/device-manage/device/detail/${record.id}`), |
||||
}, |
||||
{ |
||||
icon: 'i-ant-design:edit-outlined', |
||||
label: '解绑设备', |
||||
popConfirm: { |
||||
title: '是否要解绑该设备?', |
||||
placement: 'left', |
||||
confirm: () => handleUnbindingDivice(record.id), |
||||
}, |
||||
}, |
||||
]" |
||||
/> |
||||
</template> |
||||
</template> |
||||
</BasicTable> |
||||
|
||||
<BindingDeviceDrawer @register="registerBindingDeviceDrawer" @success="reload" /> |
||||
</div> |
||||
</template> |
Loading…
Reference in new issue