feat: 设备管理批量操作

This commit is contained in:
100011797 2023-01-17 18:04:42 +08:00
parent 063602078a
commit 5e5087602c
14 changed files with 481 additions and 46 deletions

View File

@ -1,3 +1,6 @@
import { BASE_API_PATH } from "@/utils/variable";
export const FILE_UPLOAD = `${BASE_API_PATH}/file/static`;
/**
*
*/
export const FILE_UPLOAD = `${BASE_API_PATH}/file/static`;

View File

@ -1,4 +1,5 @@
import server from '@/utils/request'
import { BASE_API_PATH } from '@/utils/variable'
import { DeviceInstance } from '@/views/device/instance/typings'
/**
@ -73,3 +74,28 @@ export const batchUndeployDevice = (data: string[]) => server.put(`/device-insta
* @returns
*/
export const batchDeleteDevice = (data: string[]) => server.put(`/device-instance/batch/_delete`, data)
/**
*
* @param productId id
* @param type
* @returns
*/
export const deviceTemplateDownload = (productId: string, type: string) => `${BASE_API_PATH}/device-instance/${productId}/template.${type}`
/**
*
* @param productId id
* @param type
* @returns
*/
export const deviceImport = (productId: string, fileUrl: string, autoDeploy: boolean) => `${BASE_API_PATH}/device-instance/${productId}/import?fileUrl=${fileUrl}&autoDeploy=${autoDeploy}&:X_Access_Token=${LocalStore.get(TOKEN_KEY)}`
/**
*
* @param productId id
* @param type
* @returns
*/
export const deviceExport = (productId: string, type: string) => `${BASE_API_PATH}/device-instance${!!productId ? '/' + productId : ''}/export.${type}`

View File

@ -25,7 +25,8 @@ const iconKeys = [
'ImportOutlined',
'ExportOutlined',
'SyncOutlined',
'ExclamationCircleOutlined'
'ExclamationCircleOutlined',
'UploadOutlined'
]
const Icon = (props: {type: string}) => {

View File

@ -228,6 +228,10 @@ const handleClick = () => {
transform: skewX(-45deg);
}
}
:deep(.card-item-content-title) {
cursor: pointer;
}
}
.card-mask {

View File

@ -0,0 +1,36 @@
<template>
<a-space align="end">
<a-radio-group button-style="solid" v-model:value="modelValue.fileType" placeholder="请选择文件格式">
<a-radio-button value="xlsx">xlsx</a-radio-button>
<a-radio-button value="csv">csv</a-radio-button>
</a-radio-group>
<a-checkbox v-model:checked="modelValue.autoDeploy">自动启用</a-checkbox>
</a-space>
</template>
<script lang="ts" setup>
import { PropType } from 'vue'
type Props = {
autoDeploy: boolean,
fileType: 'xlsx' | 'csv'
}
type Emits = {
(e: 'update:modelValue', data: Partial<Props>): void;
};
const emit = defineEmits<Emits>();
const props = defineProps({
//
modelValue: {
type: Object as PropType<Props>,
default: () => {
return {
fileType: 'xlsx',
autoDeploy: false
}
}
},
})
</script>

View File

@ -0,0 +1,70 @@
<template>
<a-upload
v-model:file-list="fileList"
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
:before-upload="beforeUpload"
@change="handleChange"
>
<img v-if="imageUrl" :src="imageUrl" alt="avatar" />
<div v-else>
<loading-outlined v-if="loading"></loading-outlined>
<plus-outlined v-else></plus-outlined>
<div class="ant-upload-text">Upload</div>
</div>
</a-upload>
</template>
<script lang="ts" setup>
import { message, UploadChangeParam, UploadProps } from 'ant-design-vue';
const handleChange = (info: UploadChangeParam) => {
// if (info.file.status === 'uploading') {
// loading.value = true;
// return;
// }
// if (info.file.status === 'done') {
// // Get this url from response in real world.
// getBase64(info.file.originFileObj, (base64Url: string) => {
// imageUrl.value = base64Url;
// loading.value = false;
// });
// }
// if (info.file.status === 'error') {
// loading.value = false;
// message.error('upload error');
// }
};
const beforeUpload = (file: UploadProps['fileList'][number]) => {
// const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
// if (!isJpgOrPng) {
// message.error('You can only upload JPG file!');
// }
// const isLt2M = file.size / 1024 / 1024 < 2;
// if (!isLt2M) {
// message.error('Image must smaller than 2MB!');
// }
// return isJpgOrPng && isLt2M;
};
</script>
<style lang="less" scoped>
.avatar-uploader {
width: 160px;
height: 160px;
padding: 8px;
background-color: rgba(0,0,0,.06);
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
:deep(.ant-upload.ant-upload-select-picture-card) {
width: 100%;
height: 100%;
}
}
</style>

View File

@ -0,0 +1,116 @@
<template>
<a-space align="end">
<a-upload
v-model:fileList="modelValue.upload"
name="file"
:action="FILE_UPLOAD"
:headers="{
'X-Access-Token': LocalStore.get(TOKEN_KEY)
}"
accept=".xlsx,.csv"
:maxCount="1"
:showUploadList="false"
@change="uploadChange"
>
<a-button>
<template #icon><AIcon type="UploadOutlined" /></template>
文件上传
</a-button>
</a-upload>
<div style="margin-left: 20px">
<a-space>
<a @click="downFile('xlsx')">.xlsx</a>
<a @click="downFile('csv')">.csv</a>
</a-space>
</div>
</a-space>
<div style="margin-top: 20px" v-if="importLoading">
<a-badge v-if="flag" status="processing" text="进行中" />
<a-badge v-else status="success" text="已完成" />
<span>总数量{{count}}</span>
<p style="color: red">{{errMessage}}</p>
</div>
</template>
<script lang="ts" setup>
import { FILE_UPLOAD } from '@/api/comm'
import { TOKEN_KEY } from '@/utils/variable';
import { LocalStore } from '@/utils/comm';
import { downloadFile } from '@/utils/utils';
import { deviceImport, deviceTemplateDownload } from '@/api/device/instance'
import { EventSourcePolyfill } from 'event-source-polyfill'
import { message } from 'ant-design-vue';
type Emits = {
(e: 'update:modelValue', data: string[]): void;
};
const emit = defineEmits<Emits>();
const props = defineProps({
//
modelValue: {
type: Array,
default: () => []
},
product: {
type: String,
default: ''
},
file: {
type: Object,
default: () => {
return {
fileType: 'xlsx',
autoDeploy: false,
}
}
}
})
const importLoading = ref<boolean>(false)
const flag = ref<boolean>(false)
const count = ref<number>(0)
const errMessage = ref<string>('')
const downFile = (type: string) => {
downloadFile(deviceTemplateDownload(props.product, type));
}
const submitData = async (fileUrl: string) => {
if (!!fileUrl) {
count.value = 0
errMessage.value = ''
flag.value = true
const autoDeploy = !!props?.file?.autoDeploy || false;
importLoading.value = true
let dt = 0;
const source = new EventSourcePolyfill(deviceImport(props.product, fileUrl, autoDeploy));
source.onmessage = (e: any) => {
const res = JSON.parse(e.data);
if (res.success) {
const temp = res.result.total;
dt += temp;
count.value = dt
} else {
errMessage.value = res.message || '失败'
}
};
source.onerror = (e: { status: number; }) => {
if (e.status === 403) errMessage.value = '暂无权限,请联系管理员'
flag.value = false
source.close();
};
source.onopen = () => {};
} else {
message.error('请先上传文件')
}
}
const uploadChange = async (info: Record<string, any>) => {
if (info.file.status === 'done') {
const resp: any = info.file.response || { result: '' };
await submitData(resp?.result || '');
}
}
</script>

View File

@ -174,8 +174,14 @@ const JTable = defineComponent<JTableProps>({
loading.value = true
if(props.request) {
const resp = await props.request({
pageIndex: 0,
pageSize: 12,
...props.defaultParams,
..._params
..._params,
terms: [
...(props.defaultParams?.terms || []),
...(_params?.terms || [])
]
})
if(resp.status === 200){
if(props.type === 'PAGE'){

View File

@ -6,6 +6,9 @@ import TitleComponent from "./TitleComponent/index.vue";
import Form from './Form';
import CardBox from './CardBox/index.vue';
import Search from './Search'
import NormalUpload from './NormalUpload/index.vue'
import FileFormat from './FileFormat/index.vue'
import JUpload from './JUpload/index.vue'
export default {
install(app: App) {
@ -16,5 +19,8 @@ export default {
.component('Form', Form)
.component('CardBox', CardBox)
.component('Search', Search)
.component('NormalUpload', NormalUpload)
.component('FileFormat', FileFormat)
.component('JUpload', JUpload)
}
}

View File

@ -29,6 +29,7 @@ import { queryNoPagingPost } from '@/api/device/product'
import { downloadFile } from '@/utils/utils'
import encodeQuery from '@/utils/encodeQuery'
import { BASE_API_PATH } from '@/utils/variable'
import { deviceExport } from '@/api/device/instance'
const emit = defineEmits(['close'])
const props = defineProps({
@ -58,15 +59,8 @@ watch(
const handleOk = () => {
const params = encodeQuery(props.data);
if(modelRef.product){
downloadFile(
`${BASE_API_PATH}/device/instance/${modelRef.product}/export.${modelRef.fileType}`,
params
);
} else {
downloadFile(`${BASE_API_PATH}/device/instance/export.${modelRef.fileType}`, params);
}
emit('close')
downloadFile(deviceExport(modelRef.product || "", modelRef.fileType),params);
emit('close')
}
const handleCancel = () => {

View File

@ -1,14 +1,67 @@
<template>
<a-modal :maskClosable="false" width="800px" :visible="true" title="导入" @ok="handleOk" @cancel="handleCancel">
123
<a-modal :maskClosable="false" width="800px" :visible="true" title="导入" @ok="handleCancel" @cancel="handleCancel">
<div style="margin-top: 10px">
<a-form :layout="'vertical'">
<a-row>
<a-col span="24">
<a-form-item label="产品" required>
<a-select showSearch v-model:value="modelRef.product" placeholder="请选择产品">
<a-select-option :value="item.id" v-for="item in productList" :key="item.id" :title="item.name"></a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col span="24">
<a-form-item label="文件格式" v-if="modelRef.product">
<FileFormat v-model="modelRef.file" />
</a-form-item>
</a-col>
<a-col span="12">
<a-form-item label="文件上传" v-if="modelRef.product">
<NormalUpload :product="modelRef.product" v-model="modelRef.upload" :file="modelRef.file" />
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
</a-modal>
</template>
<script lang="ts" setup>
const handleOk = () => {
import { queryNoPagingPost } from '@/api/device/product'
import { Form } from 'ant-design-vue';
const emit = defineEmits(['close'])
const props = defineProps({
data: {
type: Object,
default: undefined
}
})
const productList = ref<Record<string, any>[]>([])
const useForm = Form.useForm;
const modelRef = reactive({
product: undefined,
upload: [],
file: {
fileType: 'xlsx',
autoDeploy: false,
}
});
watch(
() => props.data,
() => {
queryNoPagingPost({paging: false}).then(resp => {
if(resp.status === 200){
productList.value = resp.result as Record<string, any>[]
}
})
},
{immediate: true, deep: true}
)
}
const handleCancel = () => {
emit('close')
}
</script>

View File

@ -1,16 +1,16 @@
<template>
<a-modal :maskClosable="false" width="800px" :visible="true" title="当前进度" @ok="handleOk" @cancel="handleCancel">
<a-modal :maskClosable="false" width="800px" :visible="true" title="当前进度" @ok="handleCancel" @cancel="handleCancel">
<div>
<a-badge v-if="flag" status="processing" text="进行中" />
<a-badge v-else status="success" text="已完成" />
</div>
<p>总数量{{count}}</p>
<a></a>
<a style="color: red">{{errMessage}}</a>
</a-modal>
</template>
<script lang="ts" setup>
import { downloadFile } from '@/utils/utils'
import { EventSourcePolyfill } from 'event-source-polyfill'
const emit = defineEmits(['close'])
const props = defineProps({
@ -29,22 +29,63 @@ const flag = ref<boolean>(false)
const errMessage = ref<string>('')
const isSource = ref<boolean>(false)
const id = ref<string>('')
const handleOk = () => {
emit('close')
}
const source = ref<Record<string, any>>({})
const handleCancel = () => {
emit('close')
}
const getData = () => {
const getData = (api: string) => {
let dt = 0
const _source = new EventSourcePolyfill(api)
source.value = _source
_source.onmessage = (e: any) => {
const res = JSON.parse(e.data);
switch (props.type) {
case 'active':
if (res.success) {
dt += res.total;
count.value = dt
} else {
if (res.source) {
const msg = `${res.source.name}: ${res.message}`;
errMessage.value = msg
id.value = res.source.id
isSource.value = true
} else {
errMessage.value = res.message
}
}
break;
case 'sync':
dt += res;
count.value = dt
break;
case 'import':
if (res.success) {
const temp = res.result.total;
dt += temp;
count.value = dt
} else {
errMessage.value = res.message
}
break;
default:
break;
}
};
_source.onerror = () => {
flag.value = false
_source.close();
};
_source.onopen = () => {};
}
watch(() => props.api,
() => {
getData()
(newValue) => {
if(newValue) {
getData(newValue)
}
},
{deep: true, immediate: true}
)

View File

@ -0,0 +1,70 @@
<template>
<a-modal :maskClosable="false" width="650px" :visible="true" title="新增" @ok="handleCancel" @cancel="handleCancel">
<div style="margin-top: 10px">
<a-form :layout="'vertical'">
<a-row type="flex">
<a-col flex="180px">
<a-form-item required>
<JUpload />
</a-form-item>
</a-col>
<a-col flex="auto">
<a-form-item label="ID">
<a-input v-model:value="modelRef.id" placeholder="请输入ID" />
</a-form-item>
<a-form-item label="名称" required>
<a-input v-model:value="modelRef.name" placeholder="请输入名称" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="产品" required>
<a-select showSearch v-model:value="modelRef.productId" placeholder="请选择产品">
<a-select-option :value="item.id" v-for="item in productList" :key="item.id" :title="item.name"></a-select-option>
</a-select>
</a-form-item>
<a-form-item label="说明">
<a-textarea v-model:value="modelRef.describe" placeholder="请输入说明" />
</a-form-item>
</a-form>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { queryNoPagingPost } from '@/api/device/product'
import { Form } from 'ant-design-vue';
const emit = defineEmits(['close', 'save'])
const props = defineProps({
data: {
type: Object,
default: undefined
}
})
const productList = ref<Record<string, any>[]>([])
const useForm = Form.useForm;
const modelRef = reactive({
productId: undefined,
id: '',
name: '',
describe: '',
photoUrl: ''
});
watch(
() => props.data,
() => {
queryNoPagingPost({paging: false}).then(resp => {
if(resp.status === 200){
productList.value = resp.result as Record<string, any>[]
}
})
},
{immediate: true, deep: true}
)
const handleCancel = () => {
emit('close')
}
</script>

View File

@ -73,7 +73,7 @@
</slot>
</template>
<template #content>
<h3 @click="handleView(slotProps.id)">{{ slotProps.name }}</h3>
<h3 class="card-item-content-title" @click.stop="handleView(slotProps.id)">{{ slotProps.name }}</h3>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">设备类型</div>
@ -151,6 +151,7 @@
<Import v-if="importVisible" @close="importVisible = false" />
<Export v-if="exportVisible" @close="exportVisible = false" :data="params" />
<Process v-if="operationVisible" @close="operationVisible = false" :api="api" :type="type" />
<Save v-if="visible" :data="current" />
</template>
<script setup lang="ts">
@ -161,13 +162,15 @@ import { message } from "ant-design-vue";
import Import from './Import/index.vue'
import Export from './Export/index.vue'
import Process from './Process/index.vue'
import Save from './Save/index.vue'
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
const instanceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({pageIndex: 0, pageSize: 12})
const params = ref<Record<string, any>>({})
const _selectedRowKeys = ref<string[]>([])
const importVisible = ref<boolean>(false)
const exportVisible = ref<boolean>(false)
const visible = ref<boolean>(false)
const current = ref<Record<string, any>>({})
const operationVisible = ref<boolean>(false)
const api = ref<string>('')
@ -220,9 +223,9 @@ const columns = [
}
]
const paramsFormat = (config: any, _terms: any, name?: string) => {
const paramsFormat = (config: Record<string, any>, _terms: Record<string, any>, name?: string) => {
if (config?.terms && Array.isArray(config.terms) && config?.terms.length > 0) {
(config?.terms || []).map((item: any, index: number) => {
(config?.terms || []).map((item: Record<string, any>, index: number) => {
if (item?.type) {
_terms[`${name ? `${name}.` : ''}terms[${index}].type`] = item.type;
}
@ -237,28 +240,33 @@ const paramsFormat = (config: any, _terms: any, name?: string) => {
}
}
const handleParams = (config: any) => {
const _terms: any = {};
const handleParams = (config: Record<string, any>) => {
const _terms: Record<string, any> = {};
paramsFormat(config, _terms);
const url = new URLSearchParams();
Object.keys(_terms).forEach((key) => {
url.append(key, _terms[key]);
});
return url.toString();
if(Object.keys(_terms._value).length && Object.keys(_terms).length) {
const url = new URLSearchParams();
Object.keys(_terms).forEach((key) => {
url.append(key, _terms[key]);
});
return url.toString();
} else {
return ''
}
}
/**
* 新增
*/
const handleAdd = () => {
message.warn('新增')
visible.value = true
current.value = {}
}
/**
* 查看
*/
const handleView = (dt: any) => {
// message.warn('')
const handleView = (id: string) => {
message.warn(id + '暂未开发')
}
const getActions = (data: Partial<Record<string, any>>, type: 'card' | 'table'): ActionsType[] => {
@ -272,7 +280,7 @@ const getActions = (data: Partial<Record<string, any>>, type: 'card' | 'table'):
},
icon: 'EyeOutlined',
onClick: () => {
handleView(data)
handleView(data.id)
}
},
{
@ -283,7 +291,8 @@ const getActions = (data: Partial<Record<string, any>>, type: 'card' | 'table'):
},
icon: 'EditOutlined',
onClick: () => {
message.warn('edit')
visible.value = true
current.value = data
}
},
{
@ -356,14 +365,14 @@ const handleClick = (dt: any) => {
const activeAllDevice = () => {
type.value = 'active'
const activeAPI = `/${BASE_API_PATH}/device-instance/deploy?:X_Access_Token=${LocalStore.get(TOKEN_KEY)}&${handleParams(params)}`;
const activeAPI = `${BASE_API_PATH}/device-instance/deploy?:X_Access_Token=${LocalStore.get(TOKEN_KEY)}&${handleParams(params)}`;
api.value = activeAPI
operationVisible.value = true
}
const syncDeviceStatus = () => {
type.value = 'sync'
const syncAPI = `/${BASE_API_PATH}/device-instance/state/_sync?:X_Access_Token=${LocalStore.get(TOKEN_KEY)}&${handleParams(params)}`;
const syncAPI = `${BASE_API_PATH}/device-instance/state/_sync?:X_Access_Token=${LocalStore.get(TOKEN_KEY)}&${handleParams(params)}`;
api.value = syncAPI
operationVisible.value = true
}