feat: 增加物联卡列表卡片、导出、导入、绑定设备

This commit is contained in:
blp 2023-01-30 18:12:42 +08:00
parent 2b6048707b
commit ac62778a2f
9 changed files with 657 additions and 7 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -65,3 +65,36 @@ export const sync = () => server.get(`/network/card/state/_sync`);
* @param data * @param data
*/ */
export const removeCards = (data: any) => server.post(`/network/card/batch/_delete`, data); export const removeCards = (data: any) => server.post(`/network/card/batch/_delete`, data);
/**
*
* @param cardId
*/
export const unbind = (cardId: string) => server.get(`/network/card/${cardId}/_unbind`);
/**
*
* @param data
*/
export const queryUnbounded = (data: any) => server.post(`/network/card/unbounded/device/_query`, data);
/**
*
* @param cardId
* @param deviceId id
*/
export const bind = (cardId: string | any, deviceId: string) => server.get(`/network/card/${cardId}/${deviceId}/_bind`);
/**
*
* @param configId id
* @param params
*/
export const _import = (configId: any, params: any) => server.get(`/network/card/${configId}/_import`, params);
/**
* id批量导出
* @param format xlsxcsv
* @param params
*/
export const _export = (format: string, data: any) => server.post(`/network/card/download.${format}/_query`, data, 'blob');

View File

@ -232,6 +232,17 @@ const handleClick = () => {
:deep(.card-item-content-title) { :deep(.card-item-content-title) {
cursor: pointer; cursor: pointer;
} }
:deep(.card-item-heard-name) {
font-weight: 700;
font-size: 16px;
margin-bottom: 12px;
}
:deep(.card-item-content-text) {
color: rgba(0, 0, 0, 0.75);
font-size: 12px;
}
} }
.card-mask { .card-mask {

View File

@ -54,6 +54,17 @@ export const downloadObject = (record: Record<string, any>, fileName: string, fo
formElement.submit(); formElement.submit();
document.body.removeChild(formElement); document.body.removeChild(formElement);
}; };
export const downloadFileByUrl = (url: string, name: string, type: string) => {
const downNode = document.createElement('a');
downNode.style.display = 'none';
downNode.download = `${name}.${type}`;
downNode.href = url;
document.body.appendChild(downNode);
downNode.click();
document.body.removeChild(downNode);
};
// 是否不是community版本 // 是否不是community版本
export const isNoCommunity = !(localStorage.getItem(SystemConst.VERSION_CODE) === 'community'); export const isNoCommunity = !(localStorage.getItem(SystemConst.VERSION_CODE) === 'community');

View File

@ -0,0 +1,155 @@
<!-- 绑定设备 -->
<template>
<a-modal
:maskClosable="false"
width="1100px"
:visible="true"
title="选择设备"
@ok="handleOk"
@cancel="handleCancel"
:confirmLoading="btnLoading"
>
<div style="margin-top: 10px">
<Search
:columns="columns"
target="iot-card-management-search"
@search="handleSearch"
/>
<JTable
ref="bindDeviceRef"
:columns="columns"
:request="queryUnbounded"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
:rowSelection="{
type: 'radio',
selectedRowKeys: _selectedRowKeys,
onSelect: onSelectChange,
}"
@cancelSelect="cancelSelect"
:params="params"
>
<template #registryTime="slotProps">
{{
slotProps.registryTime
? moment(slotProps.registryTime).format(
'YYYY-MM-DD HH:mm:ss',
)
: ''
}}
</template>
<template #state="slotProps">
<a-badge
:text="slotProps.state.text"
:status="statusMap.get(slotProps.state.value)"
/>
</template>
</JTable>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { queryUnbounded, bind } from '@/api/iot-card/cardManagement';
import moment from 'moment';
import { message } from 'ant-design-vue';
const emit = defineEmits(['change']);
const props = defineProps({
cardId: {
type: String,
},
});
const bindDeviceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const _selectedRowKeys = ref<string[]>([]);
const btnLoading = ref<boolean>(false);
const statusMap = new Map();
statusMap.set('online', 'processing');
statusMap.set('offline', 'error');
statusMap.set('notActive', 'warning');
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '设备名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '注册时间',
dataIndex: 'registryTime',
key: 'registryTime',
scopedSlots: true,
search: {
type: 'date',
},
// sorter: true,
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '禁用', value: 'notActive' },
{ label: '离线', value: 'offline' },
{ label: '在线', value: 'online' },
],
},
// filterMultiple: false,
},
];
const handleSearch = (params: any) => {
console.log(params);
params.value = params;
};
const onSelectChange = (record: any) => {
_selectedRowKeys.value = [record.id];
};
const cancelSelect = () => {
_selectedRowKeys.value = [];
};
const handleOk = () => {
btnLoading.value = true;
bind(props.cardId, _selectedRowKeys.value[0])
.then((resp: any) => {
if (resp.status === 200) {
message.success('操作成功');
emit('change', true);
}
})
.finally(() => {
btnLoading.value = false;
});
};
const handleCancel = () => {
emit('change', false);
};
</script>
<style scoped lang="less"></style>

View File

@ -0,0 +1,65 @@
<template>
<!-- 导入 -->
<a-modal
:maskClosable="false"
:visible="true"
title="导出"
@ok="handleOk"
@cancel="handleCancel"
>
<div style="margin-top: 10px">
<a-space>
<span>文件格式</span>
<a-radio-group
v-model:value="type"
placeholder="请选择文件格式"
button-style="solid"
>
<a-radio-button value="xlsx">xlsx</a-radio-button>
<a-radio-button value="csv">csv</a-radio-button>
</a-radio-group>
</a-space>
</div>
</a-modal>
</template>
<script setup lang="ts">
import moment from 'moment';
import { _export } from '@/api/iot-card/cardManagement';
import { downloadFileByUrl } from '@/utils/utils';
const emit = defineEmits(['close']);
const props = defineProps({
data: {
type: Object,
default: undefined,
},
});
const type = ref<string>('xlsx');
const handleOk = () => {
console.log(props.data);
_export(type.value, props.data).then((res: any) => {
if (res) {
const blob = new Blob([res.data], { type: type.value });
const url = URL.createObjectURL(blob);
downloadFileByUrl(
url,
`物联卡管理-${moment(new Date()).format(
'YYYY/MM/DD HH:mm:ss',
)}`,
type.value,
);
emit('close');
}
});
};
const handleCancel = () => {
emit('close');
};
</script>
<style scoped lang="less"></style>

View File

@ -0,0 +1,157 @@
<template>
<!-- 导入 -->
<a-modal
:maskClosable="false"
:visible="true"
title="导入"
@ok="handleCancel"
@cancel="handleCancel"
>
<div style="margin-top: 10px">
<a-form :layout="'vertical'">
<a-form-item label="平台对接" required>
<a-select
showSearch
v-model:value="modelRef.configId"
:options="configList"
placeholder="请选择平台对接"
>
</a-select>
</a-form-item>
<a-form-item v-if="modelRef.configId" label="文件格式">
<a-radio-group
button-style="solid"
v-model:value="modelRef.fileType"
placeholder="请选择文件格式"
>
<a-radio-button value="xlsx">xlsx</a-radio-button>
<a-radio-button value="csv">csv</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="文件上传" v-if="modelRef.configId">
<a-upload
v-model:fileList="modelRef.upload"
name="file"
:action="FILE_UPLOAD"
:headers="{
'X-Access-Token': LocalStore.get(TOKEN_KEY),
}"
:accept="`.${modelRef.fileType || 'xlsx'}`"
:showUploadList="false"
@change="fileChange"
>
<a-button :loading="loading">
<template #icon>
<AIcon type="UploadOutlined" />
</template>
文件上传
</a-button>
</a-upload>
</a-form-item>
<a-form-item v-if="modelRef.configId" label="下载模板">
<a-space>
<a-button icon="file" @click="downFileFn('xlsx')">
.xlsx
</a-button>
<a-button icon="file" @click="downFileFn('csv')">
.csv
</a-button>
</a-space>
</a-form-item>
<div v-if="totalCount">
<a-icon class="check-num" type="check" /> 已完成 总数量
<span class="check-num">{{ totalCount }}</span>
</div>
<div v-if="errCount">
<a-icon class="check-num" style="color: red" type="close" />
失败 总数量
<span class="check-num">{{ errCount }}</span>
</div>
</a-form>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { FILE_UPLOAD } from '@/api/comm';
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import { LocalStore } from '@/utils/comm';
import { downloadFile } from '@/utils/utils';
import { queryPlatformNoPage, _import } from '@/api/iot-card/cardManagement';
import { message } from 'ant-design-vue';
const emit = defineEmits(['close']);
const configList = ref<Record<string, any>[]>([]);
const loading = ref<boolean>(false);
const totalCount = ref<number>(0);
const errCount = ref<number>(0);
const modelRef = reactive({
configId: undefined,
upload: [],
fileType: 'xlsx',
});
const getConfig = async () => {
const resp: any = await queryPlatformNoPage({
paging: false,
terms: [
{
terms: [
{
column: 'state',
termType: 'eq',
value: 'enabled',
type: 'and',
},
],
},
],
});
configList.value = resp.result.map((item: any) => {
return { key: item.id, label: item.name, value: item.id };
});
};
const fileChange = (info: any) => {
loading.value = true;
if (info.file.status === 'done') {
const r = info.file.response || { result: '' };
_import(modelRef.configId, { fileUrl: r.result })
.then((resp: any) => {
totalCount.value = resp.result.total;
message.success('导入成功');
})
.catch((err) => {
message.error(err.response.data.message || '导入失败');
})
.finally(() => {
loading.value = false;
});
}
};
const downFileFn = (type: string) => {
const url = `${BASE_API_PATH}/network/card/template.${type}`;
downloadFile(url);
};
const handleCancel = () => {
totalCount.value = 0;
errCount.value = 0;
modelRef.configId = undefined;
emit('close', true);
};
getConfig();
</script>
<style scoped lang="less">
.check-num {
margin: 6px;
color: @primary-color;
}
</style>

View File

@ -103,6 +103,113 @@
</a-dropdown> </a-dropdown>
</a-space> </a-space>
</template> </template>
<template #card="slotProps">
<CardBox
:value="slotProps"
@click="handleClick"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
:active="_selectedRowKeys.includes(slotProps.id)"
:status="slotProps.cardStateType.value"
:statusText="slotProps.cardStateType.text"
:statusNames="{
using: 'success',
toBeActivated: 'default',
deactivate: 'error',
}"
>
<template #img>
<slot name="img">
<img :src="getImage('/iot-card/iot-card-bg.png')" />
</slot>
</template>
<template #content>
<h3
class="card-item-content-title"
@click.stop="handleView(slotProps.id)"
>
{{ slotProps.id }}
</h3>
<a-row>
<a-col :span="8">
<div class="card-item-content-text">
平台对接
</div>
<div>{{ slotProps.platformConfigName }}</div>
</a-col>
<a-col :span="6">
<div class="card-item-content-text">类型</div>
<div>{{ slotProps.cardType.text }}</div>
</a-col>
<a-col :span="6">
<div class="card-item-content-text">提醒</div>
<!-- <div>{{ slotProps.cardType.text }}</div> -->
</a-col>
</a-row>
<a-divider style="margin: 12px 0" />
<div v-if="slotProps.usedFlow === 0">
<span class="flow-text">
{{ slotProps.totalFlow }}
</span>
<span class="card-item-content-text"> M 使用流量</span>
</div>
<div v-else>
<div class="progress-text">
<div>{{ slotProps.totalFlow - slotProps.usedFlow }} %</div>
<div class="card-item-content-text">
总共 {{ slotProps.totalFlow }} M
</div>
</div>
<a-progress
:strokeColor="'#ADC6FF'"
:showInfo="false"
:percent="slotProps.totalFlow - slotProps.usedFlow"
/>
</div>
</template>
<template #actions="item">
<a-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
>
<a-popconfirm
v-if="item.popConfirm"
v-bind="item.popConfirm"
:disabled="item.disabled"
>
<a-button :disabled="item.disabled">
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</template>
</a-button>
</a-popconfirm>
<template v-else>
<a-button
:disabled="item.disabled"
@click="item.onClick"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</template>
</a-button>
</template>
</a-tooltip>
</template>
</CardBox>
</template>
<template #deviceId="slotProps">
{{ slotProps.deviceName }}
</template>
<template #totalFlow="slotProps"> <template #totalFlow="slotProps">
<div> <div>
{{ {{
@ -157,7 +264,7 @@
<template #action="slotProps"> <template #action="slotProps">
<a-space :size="16"> <a-space :size="16">
<a-tooltip <a-tooltip
v-for="i in getActions(slotProps)" v-for="i in getActions(slotProps, 'table')"
:key="i.key" :key="i.key"
v-bind="i.tooltip" v-bind="i.tooltip"
> >
@ -186,6 +293,20 @@
</a-space> </a-space>
</template> </template>
</JTable> </JTable>
<!-- 批量导入 -->
<Import v-if="importVisible" @close="importVisible = false" />
<!-- 批量导出 -->
<Export
v-if="exportVisible"
@close="exportVisible = false"
:data="_selectedRowKeys"
/>
<!-- 绑定设备 -->
<BindDevice
v-if="bindDeviceVisible"
:cardId="cardId"
@change="bindDevice"
/>
</div> </div>
</template> </template>
@ -204,16 +325,25 @@ import {
resumptionBatch, resumptionBatch,
sync, sync,
removeCards, removeCards,
unbind,
} from '@/api/iot-card/cardManagement'; } from '@/api/iot-card/cardManagement';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import type { CardManagement } from './typing';
import { getImage } from '@/utils/comm';
import BindDevice from './BindDevice.vue';
import Import from './Import.vue';
import Export from './Export.vue';
const cardManageRef = ref<Record<string, any>>({}); const cardManageRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({}); const params = ref<Record<string, any>>({});
const _selectedRowKeys = ref<string[]>([]); const _selectedRowKeys = ref<string[]>([]);
const _selectedRow = ref<any[]>([]); const _selectedRow = ref<any[]>([]);
const bindDeviceVisible = ref<boolean>(false);
const visible = ref<boolean>(false); const visible = ref<boolean>(false);
const exportVisible = ref<boolean>(false); const exportVisible = ref<boolean>(false);
const importVisible = ref<boolean>(false); const importVisible = ref<boolean>(false);
const cardId = ref<any>();
const current = ref<Partial<CardManagement>>({});
const columns = [ const columns = [
{ {
@ -239,9 +369,10 @@ const columns = [
}, },
{ {
title: '绑定设备', title: '绑定设备',
dataIndex: 'deviceName', dataIndex: 'deviceId',
key: 'deviceName', key: 'deviceId',
ellipsis: true, ellipsis: true,
scopedSlots: true,
width: 200, width: 200,
}, },
{ {
@ -360,7 +491,10 @@ const columns = [
}, },
]; ];
const getActions = (data: Partial<Record<string, any>>): ActionsType[] => { const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return []; if (!data) return [];
return [ return [
{ {
@ -386,6 +520,25 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
title: data.deviceId ? '解绑设备' : '绑定设备', title: data.deviceId ? '解绑设备' : '绑定设备',
}, },
icon: data.deviceId ? 'DisconnectOutlined' : 'LinkOutlined', icon: data.deviceId ? 'DisconnectOutlined' : 'LinkOutlined',
popConfirm: data.deviceId
? {
title: '确认解绑设备?',
onConfirm: async () => {
unbind(data.id).then((resp: any) => {
if (resp.status === 200) {
message.success('操作成功');
cardManageRef.value?.reload();
}
});
},
}
: undefined,
onClick: () => {
if (!data.deviceId) {
bindDeviceVisible.value = true;
cardId.value = data.id;
}
},
}, },
{ {
key: 'activation', key: 'activation',
@ -479,11 +632,38 @@ const cancelSelect = () => {
_selectedRowKeys.value = []; _selectedRowKeys.value = [];
}; };
const handleClick = (dt: any) => {
if (_selectedRowKeys.value.includes(dt.id)) {
const _index = _selectedRowKeys.value.findIndex((i) => i === dt.id);
_selectedRowKeys.value.splice(_index, 1);
} else {
_selectedRowKeys.value = [..._selectedRowKeys.value, dt.id];
}
};
/**
* 查看
*/
const handleView = (id: string) => {
message.warn(id + '暂未开发');
};
/** /**
* 新增 * 新增
*/ */
const handleAdd = () => {}; const handleAdd = () => {};
/**
* 绑定设备关闭窗口
*/
const bindDevice = (val: boolean) => {
bindDeviceVisible.value = false;
cardId.value = '';
if (val) {
cardManageRef.value?.reload();
}
};
/** /**
* 批量激活 * 批量激活
*/ */
@ -565,7 +745,25 @@ const handelRemove = async () => {
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.page-container {
.search { .search {
width: calc(100% - 330px); width: calc(100% - 330px);
} }
.flow-text {
font-size: 20px;
font-weight: 600;
}
.progress-text {
display: flex;
justify-content: space-between;
align-items: center;
}
:deep(.ant-progress-inner) {
border-radius: 0px;
}
:deep(.ant-progress-bg) {
border-radius: 0px;
}
}
</style> </style>

View File

@ -0,0 +1,20 @@
export type CardManagement = {
id: string;
name: string;
iccId: string;
deviceId: string;
deviceName: string;
platformConfigId: string;
operatorName: string;
cardType: any;
totalFlow: number;
usedFlow: number;
residualFlow: number;
activationDate: string;
updateTime: string;
cardStateType: any;
cardState: any;
describe: string;
platformConfigName: string;
operatorPlatformType: any;
};