commit
9b8ce9e586
|
@ -0,0 +1,27 @@
|
||||||
|
import server from '@/utils/request';
|
||||||
|
|
||||||
|
export const query = (data: any) =>
|
||||||
|
server.post(`/data-collect/channel/_query`, data);
|
||||||
|
|
||||||
|
export const remove = (id: string) =>
|
||||||
|
server.remove(`/data-collect/channel/${id}`);
|
||||||
|
|
||||||
|
export const save = (data: any) => server.post(`/data-collect/channel`, data);
|
||||||
|
|
||||||
|
export const update = (id: string, data: any) =>
|
||||||
|
server.put(`/data-collect/channel/${id}`, data);
|
||||||
|
|
||||||
|
export const getProviders = () => server.get(`/gateway/device/providers`);
|
||||||
|
|
||||||
|
export const queryOptionsList = (type: strimg) =>
|
||||||
|
server.get(`/data-collect/opc/${type}`);
|
||||||
|
|
||||||
|
export const validateField = (data: any) =>
|
||||||
|
server.post(`/data-collect/opc/endpoint/_validate`, data, null, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain;charset=UTF-8',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const queryCertificateList = () =>
|
||||||
|
server.get(`/network/certificate/_query/no-paging?paging=false`, {});
|
|
@ -0,0 +1,6 @@
|
||||||
|
import server from '@/utils/request';
|
||||||
|
|
||||||
|
export const queryCount = (type: string, data: any) =>
|
||||||
|
server.post(`/data-collect/${type}/_count`, data);
|
||||||
|
|
||||||
|
export const dashboard = (data: any) => server.post(`/dashboard/_multi`, data);
|
|
@ -24,12 +24,17 @@ export const request = axios.create({
|
||||||
* @param {String} url
|
* @param {String} url
|
||||||
* @param {Object} [data]
|
* @param {Object} [data]
|
||||||
* @param {String} responseType 如果接口是需要导出文件流,那么responseType = 'blob'
|
* @param {String} responseType 如果接口是需要导出文件流,那么responseType = 'blob'
|
||||||
|
* @param {Object|String} [ext] 扩展参数,如果是配置headers,ext对象内包含headers对象,如下
|
||||||
|
* {
|
||||||
|
headers: {'Content-Type': 'text/plain;charset=UTF-8'},
|
||||||
|
}
|
||||||
* @returns {AxiosInstance}
|
* @returns {AxiosInstance}
|
||||||
*/
|
*/
|
||||||
export const post = function<T>(url: string, data = {}, params = {}) {
|
export const post = function<T>(url: string, data = {}, params = {}, ext={}) {
|
||||||
params = typeof params === 'string' ? { responseType: params } : params
|
ext = typeof ext === 'string' ? { responseType: ext } : ext
|
||||||
return request<any, AxiosResponseRewrite<T>>({
|
return request<any, AxiosResponseRewrite<T>>({
|
||||||
...params,
|
...ext,
|
||||||
|
params,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url,
|
url,
|
||||||
data
|
data
|
||||||
|
|
|
@ -0,0 +1,315 @@
|
||||||
|
<template lang="">
|
||||||
|
<a-modal
|
||||||
|
:title="data.id ? '编辑' : '新增'"
|
||||||
|
:visible="true"
|
||||||
|
width="700px"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
>
|
||||||
|
<a-form
|
||||||
|
class="form"
|
||||||
|
layout="vertical"
|
||||||
|
:model="formData"
|
||||||
|
name="basic"
|
||||||
|
autocomplete="off"
|
||||||
|
:rules="FormValidate"
|
||||||
|
ref="formRef"
|
||||||
|
>
|
||||||
|
<a-form-item label="通道名称" name="name">
|
||||||
|
<a-input
|
||||||
|
placeholder="请输入通道名称"
|
||||||
|
v-model:value="formData.name"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="通讯协议" name="provider">
|
||||||
|
<a-select
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.provider"
|
||||||
|
:options="providersList"
|
||||||
|
placeholder="请选择通讯协议"
|
||||||
|
allowClear
|
||||||
|
show-search
|
||||||
|
:filter-option="filterOption"
|
||||||
|
:disabled="!!id"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
v-if="formData.provider === 'MODBUS_TCP'"
|
||||||
|
:name="['configuration', 'host']"
|
||||||
|
:rules="FormValidate.host"
|
||||||
|
>
|
||||||
|
<div class="form-label">
|
||||||
|
Modbus主机IP
|
||||||
|
<span class="form-label-required">*</span>
|
||||||
|
<a-tooltip>
|
||||||
|
<template #title>
|
||||||
|
<p>支持ipv4、ipv6、域名</p>
|
||||||
|
</template>
|
||||||
|
<AIcon type="QuestionCircleOutlined" />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<a-input
|
||||||
|
placeholder="请输入Modbus主机IP"
|
||||||
|
v-model:value="formData.configuration.host"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
v-if="formData.provider === 'MODBUS_TCP'"
|
||||||
|
label="端口"
|
||||||
|
:name="['configuration', 'port']"
|
||||||
|
:rules="FormValidate.port"
|
||||||
|
>
|
||||||
|
<a-input-number
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="请输入端口"
|
||||||
|
v-model:value="formData.configuration.port"
|
||||||
|
:min="1"
|
||||||
|
:max="65535"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
v-if="formData.provider === 'OPC_UA'"
|
||||||
|
label="端点url"
|
||||||
|
:name="['configuration', 'endpoint']"
|
||||||
|
:rules="FormValidate.endpoint"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
placeholder="请输入端点url"
|
||||||
|
v-model:value="formData.configuration.endpoint"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
v-if="formData.provider === 'OPC_UA'"
|
||||||
|
label="安全策略"
|
||||||
|
:name="['configuration.securityPolicy']"
|
||||||
|
:rules="FormValidate.securityPolicy"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.configuration.securityPolicy"
|
||||||
|
:options="Options['security-policies']"
|
||||||
|
placeholder="请选择安全策略"
|
||||||
|
allowClear
|
||||||
|
show-search
|
||||||
|
:filter-option="filterOption"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
v-if="formData.provider === 'OPC_UA'"
|
||||||
|
label="安全模式"
|
||||||
|
:name="['configuration.securityMode']"
|
||||||
|
:rules="FormValidate.securityMode"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.configuration.securityMode"
|
||||||
|
:options="Options['security-modes']"
|
||||||
|
placeholder="请选择安全模式"
|
||||||
|
allowClear
|
||||||
|
show-search
|
||||||
|
:filter-option="filterOption"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
v-if="
|
||||||
|
formData.configuration.securityMode === 'SignAndEncrypt' ||
|
||||||
|
formData.configuration.securityMode === 'Sign'
|
||||||
|
"
|
||||||
|
label="证书"
|
||||||
|
:name="['configuration.certificate']"
|
||||||
|
:rules="FormValidate.certificate"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.configuration.certificate"
|
||||||
|
:options="certificateList"
|
||||||
|
placeholder="请选择证书"
|
||||||
|
allowClear
|
||||||
|
show-search
|
||||||
|
:filter-option="filterOption"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
v-if="formData.provider === 'OPC_UA'"
|
||||||
|
label="权限认证"
|
||||||
|
:name="['configuration.authType']"
|
||||||
|
:rules="FormValidate.authType"
|
||||||
|
>
|
||||||
|
<RadioCard
|
||||||
|
layout="horizontal"
|
||||||
|
:checkStyle="true"
|
||||||
|
:options="Options['auth-types']"
|
||||||
|
v-model="formData.configuration.authType"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
v-if="formData.configuration.authType === 'username'"
|
||||||
|
label="用户名"
|
||||||
|
:name="['configuration.username']"
|
||||||
|
:rules="FormValidate.username"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
v-model:value="formData.configuration.username"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
v-if="formData.configuration.authType === 'username'"
|
||||||
|
label="密码"
|
||||||
|
:name="['configuration.password']"
|
||||||
|
:rules="FormValidate.password"
|
||||||
|
>
|
||||||
|
<a-input-password
|
||||||
|
placeholder="请输入密码"
|
||||||
|
v-model:value="formData.configuration.password"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="说明" name="description">
|
||||||
|
<a-textarea
|
||||||
|
placeholder="请输入说明"
|
||||||
|
v-model:value="formData.description"
|
||||||
|
:maxlength="200"
|
||||||
|
:rows="3"
|
||||||
|
showCount
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
<template #footer>
|
||||||
|
<a-button key="back" @click="handleCancel">取消</a-button>
|
||||||
|
<PermissionButton
|
||||||
|
key="submit"
|
||||||
|
type="primary"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleOk"
|
||||||
|
style="margin-left: 8px"
|
||||||
|
:hasPermission="`DataCollect/Channel:${id ? 'update' : 'add'}`"
|
||||||
|
>
|
||||||
|
确认
|
||||||
|
</PermissionButton>
|
||||||
|
</template>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
save,
|
||||||
|
update,
|
||||||
|
queryOptionsList,
|
||||||
|
queryCertificateList,
|
||||||
|
getProviders,
|
||||||
|
} from '@/api/data-collect/channel';
|
||||||
|
import { FormValidate, FormState } from '../data';
|
||||||
|
import type { FormInstance } from 'ant-design-vue';
|
||||||
|
import type { FormDataType } from '../type.d';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['change']);
|
||||||
|
const loading = ref(false);
|
||||||
|
const id = props.data.id;
|
||||||
|
const formRef = ref<FormInstance>();
|
||||||
|
|
||||||
|
const certificateList = ref([]);
|
||||||
|
const providersList = ref([]);
|
||||||
|
const Options = ref({
|
||||||
|
'auth-types': [],
|
||||||
|
'security-modes': [],
|
||||||
|
'security-policies': [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const formData = ref<FormDataType>(FormState);
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
const params = await formRef.value?.validate();
|
||||||
|
loading.value = true;
|
||||||
|
const response = !id
|
||||||
|
? await save(params)
|
||||||
|
: await update(id, { ...props.data, ...params });
|
||||||
|
if (response.status === 200) {
|
||||||
|
emit('change', true);
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
formRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit('change', false);
|
||||||
|
formRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterOption = (input: string, option: any) => {
|
||||||
|
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOptionsList = async () => {
|
||||||
|
for (let key in Options.value) {
|
||||||
|
const res = await queryOptionsList(key);
|
||||||
|
Options.value[key] = res.result.map((item) => ({
|
||||||
|
label: item?.text || item,
|
||||||
|
value: item?.value || item,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getCertificateList = async () => {
|
||||||
|
const res = await queryCertificateList();
|
||||||
|
certificateList.value = res.result.map((item) => ({
|
||||||
|
value: item.id,
|
||||||
|
label: item.name,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProvidersList = async () => {
|
||||||
|
const resp = await getProviders();
|
||||||
|
if (resp.status === 200) {
|
||||||
|
const list = [
|
||||||
|
{ label: 'OPC UA', value: 'OPC_UA' },
|
||||||
|
{ label: 'Modbus TCP', value: 'MODBUS_TCP' },
|
||||||
|
];
|
||||||
|
const arr = resp.result
|
||||||
|
.filter(
|
||||||
|
(item: any) => item.id === 'modbus-tcp' || item.id === 'opc-ua',
|
||||||
|
)
|
||||||
|
.map((it: any) => (it?.id === 'opc-ua' ? 'OPC_UA' : 'MODBUS_TCP'));
|
||||||
|
const providers = list.filter((item: any) => arr.includes(item.value));
|
||||||
|
providersList.value = providers;
|
||||||
|
if (arr.includes('OPC_UA')) {
|
||||||
|
getOptionsList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getProvidersList();
|
||||||
|
getCertificateList();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
(value) => {
|
||||||
|
if (value.id) formData.value = value;
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.form {
|
||||||
|
.form-radio-button {
|
||||||
|
width: 148px;
|
||||||
|
height: 80px;
|
||||||
|
padding: 0;
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.form-label {
|
||||||
|
height: 30px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
.form-label-required {
|
||||||
|
color: red;
|
||||||
|
margin: 0 4px 0 -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { validateField } from '@/api/data-collect/channel';
|
||||||
|
import { FormDataType } from './type.d';
|
||||||
|
|
||||||
|
export const FormState: FormDataType = {
|
||||||
|
name: '',
|
||||||
|
provider: undefined,
|
||||||
|
configuration: {
|
||||||
|
host: '',
|
||||||
|
port: '502',
|
||||||
|
endpoint: '',
|
||||||
|
securityPolicy: undefined,
|
||||||
|
securityMode: undefined,
|
||||||
|
certificate: undefined,
|
||||||
|
authType: undefined,
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
},
|
||||||
|
description: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StatusColorEnum = {
|
||||||
|
running: 'success',
|
||||||
|
disabled: 'error',
|
||||||
|
partialError: 'processing',
|
||||||
|
failed: 'warning',
|
||||||
|
stopped: 'default',
|
||||||
|
};
|
||||||
|
export const updateStatus = {
|
||||||
|
disabled: {
|
||||||
|
state: 'enabled',
|
||||||
|
runningState: 'running',
|
||||||
|
},
|
||||||
|
enabled: {
|
||||||
|
state: 'disabled',
|
||||||
|
runningState: 'stopped',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TiTlePermissionButtonStyle = {
|
||||||
|
padding: 0,
|
||||||
|
color: ' #1890ff !important',
|
||||||
|
'font-weight': 700,
|
||||||
|
'font-size': '16px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
'text-overflow': 'ellipsis',
|
||||||
|
'white-space': 'nowrap',
|
||||||
|
width: 'calc(100%-100px)',
|
||||||
|
// width: '60%',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const regOnlyNumber = new RegExp(/^\d+$/);
|
||||||
|
|
||||||
|
export const regIP = new RegExp(
|
||||||
|
/^([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])$/,
|
||||||
|
);
|
||||||
|
export const regIPv6 = new RegExp(
|
||||||
|
/^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/,
|
||||||
|
);
|
||||||
|
export const regDomain = new RegExp(
|
||||||
|
/([0-9a-z-]{2,}\.[0-9a-z-]{2,3}\.[0-9a-z-]{2,3}|[0-9a-z-]{2,}\.[0-9a-z-]{2,3})$/i,
|
||||||
|
);
|
||||||
|
export const checkEndpoint = (_rule: Rule, value: string): Promise<any> =>
|
||||||
|
new Promise(async (resolve, reject) => {
|
||||||
|
if (value) {
|
||||||
|
const res = await validateField(value);
|
||||||
|
return res.result.passed ? resolve('') : reject(res.result.reason);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
export const FormValidate = {
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '请输入名称', trigger: 'blur' },
|
||||||
|
{ max: 64, message: '最多可输入64个字符' },
|
||||||
|
],
|
||||||
|
provider: [{ required: true, message: '请选择通讯协议' }],
|
||||||
|
host: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入Modbus主机IP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: regIP || regIPv6 || regDomain,
|
||||||
|
message: '请输入正确格式的Modbus主机IP地址',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
port: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入端口',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: regOnlyNumber,
|
||||||
|
message: '请输入1-65535之间的正整数',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
endpoint: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入端点url',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: checkEndpoint,
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
securityPolicy: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择安全策略',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
securityMode: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择安全模式',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
certificate: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择证书',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
authType: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择权限认证',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
username: [
|
||||||
|
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||||
|
{ max: 64, message: '最多可输入64个字符' },
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ max: 64, message: '最多可输入64个字符' },
|
||||||
|
],
|
||||||
|
|
||||||
|
description: [{ max: 200, message: '最多可输入200个字符' }],
|
||||||
|
};
|
|
@ -0,0 +1,341 @@
|
||||||
|
<template>
|
||||||
|
<page-container>
|
||||||
|
<div>
|
||||||
|
<Search :columns="columns" target="search" @search="handleSearch" />
|
||||||
|
|
||||||
|
<j-pro-table
|
||||||
|
ref="tableRef"
|
||||||
|
:columns="columns"
|
||||||
|
model="CARD"
|
||||||
|
:gridColumn="3"
|
||||||
|
:request="query"
|
||||||
|
:defaultParams="{
|
||||||
|
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||||
|
}"
|
||||||
|
:params="params"
|
||||||
|
>
|
||||||
|
<template #headerTitle>
|
||||||
|
<PermissionButton
|
||||||
|
type="primary"
|
||||||
|
@click="handlAdd"
|
||||||
|
hasPermission="DataCollect/Channel:add"
|
||||||
|
>
|
||||||
|
<template #icon><AIcon type="PlusOutlined" /></template>
|
||||||
|
新增通道
|
||||||
|
</PermissionButton>
|
||||||
|
</template>
|
||||||
|
<template #card="slotProps">
|
||||||
|
<CardBox
|
||||||
|
:showStatus="true"
|
||||||
|
:value="slotProps"
|
||||||
|
:actions="getActions(slotProps, 'card')"
|
||||||
|
v-bind="slotProps"
|
||||||
|
:status="getState(slotProps).value"
|
||||||
|
:statusText="getState(slotProps).text"
|
||||||
|
:statusNames="StatusColorEnum"
|
||||||
|
>
|
||||||
|
<template #img>
|
||||||
|
<slot name="img">
|
||||||
|
<img :src="getImage('/network.png')" />
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="card-item-content">
|
||||||
|
<PermissionButton
|
||||||
|
type="link"
|
||||||
|
@click="handlEye(slotProps.id)"
|
||||||
|
hasPermission="DataCollect/Collector:view"
|
||||||
|
:style="TiTlePermissionButtonStyle"
|
||||||
|
>
|
||||||
|
{{ slotProps.name }}
|
||||||
|
</PermissionButton>
|
||||||
|
|
||||||
|
<a-row class="card-item-content-box">
|
||||||
|
<a-col :span="12">
|
||||||
|
<div class="card-item-content-text">
|
||||||
|
协议
|
||||||
|
</div>
|
||||||
|
<div class="card-item-content-text">
|
||||||
|
<a-tooltip>
|
||||||
|
<template #title>{{
|
||||||
|
slotProps.provider
|
||||||
|
}}</template>
|
||||||
|
{{ slotProps.provider }}
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<div class="card-item-content-text">
|
||||||
|
地址
|
||||||
|
</div>
|
||||||
|
<div class="card-item-content-text">
|
||||||
|
<a-tooltip>
|
||||||
|
<template #title>{{
|
||||||
|
slotProps.configuration
|
||||||
|
.host ||
|
||||||
|
slotProps.configuration
|
||||||
|
.endpoint
|
||||||
|
}}</template>
|
||||||
|
<span class="details-text">{{
|
||||||
|
slotProps.configuration
|
||||||
|
.host ||
|
||||||
|
slotProps.configuration
|
||||||
|
.endpoint
|
||||||
|
}}</span>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #actions="item">
|
||||||
|
<PermissionButton
|
||||||
|
:disabled="item.disabled"
|
||||||
|
:popConfirm="item.popConfirm"
|
||||||
|
:tooltip="{
|
||||||
|
...item.tooltip,
|
||||||
|
}"
|
||||||
|
@click="item.onClick"
|
||||||
|
:hasPermission="
|
||||||
|
'DataCollect/Channel:' + item.key
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<AIcon
|
||||||
|
type="DeleteOutlined"
|
||||||
|
v-if="item.key === 'delete'"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
<AIcon :type="item.icon" />
|
||||||
|
<span>{{ item?.text }}</span>
|
||||||
|
</template>
|
||||||
|
</PermissionButton>
|
||||||
|
</template>
|
||||||
|
</CardBox>
|
||||||
|
</template>
|
||||||
|
</j-pro-table>
|
||||||
|
<Save v-if="visible" :data="current" @change="saveChange" />
|
||||||
|
</div>
|
||||||
|
</page-container>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup name="TypePage">
|
||||||
|
import type { ActionsType } from '@/components/Table/index';
|
||||||
|
import { getImage } from '@/utils/comm';
|
||||||
|
import { query, remove, update } from '@/api/data-collect/channel';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import {
|
||||||
|
TiTlePermissionButtonStyle,
|
||||||
|
StatusColorEnum,
|
||||||
|
updateStatus,
|
||||||
|
} from './data';
|
||||||
|
import { useMenuStore } from 'store/menu';
|
||||||
|
import Save from './Save/index.vue';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
const menuStory = useMenuStore();
|
||||||
|
const tableRef = ref<Record<string, any>>({});
|
||||||
|
const params = ref<Record<string, any>>({});
|
||||||
|
const options = ref([]);
|
||||||
|
const visible = ref(false);
|
||||||
|
const current = ref({});
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '通道名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
ellipsis: true,
|
||||||
|
fixed: 'left',
|
||||||
|
search: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '通讯协议',
|
||||||
|
dataIndex: 'provider',
|
||||||
|
key: 'provider',
|
||||||
|
ellipsis: true,
|
||||||
|
search: {
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'OPC_UA', value: 'OPC_UA' },
|
||||||
|
{ label: 'MODBUS_TCP', value: 'MODBUS_TCP' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'state',
|
||||||
|
key: 'state',
|
||||||
|
ellipsis: true,
|
||||||
|
scopedSlots: true,
|
||||||
|
search: {
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '正常', value: 'enabled' },
|
||||||
|
{ label: '禁用', value: 'disabled' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '运行状态',
|
||||||
|
dataIndex: 'runningState',
|
||||||
|
key: 'runningState',
|
||||||
|
ellipsis: true,
|
||||||
|
scopedSlots: true,
|
||||||
|
search: {
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '运行中', value: 'running' },
|
||||||
|
{ label: '部分错误', value: 'partialError' },
|
||||||
|
{ label: '错误', value: 'failed' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '说明',
|
||||||
|
dataIndex: 'description',
|
||||||
|
key: 'description',
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 200,
|
||||||
|
scopedSlots: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const getActions = (
|
||||||
|
data: Partial<Record<string, any>>,
|
||||||
|
type: 'card' | 'table',
|
||||||
|
): ActionsType[] => {
|
||||||
|
if (!data) return [];
|
||||||
|
const state = data.state.value;
|
||||||
|
const stateText = state === 'enabled' ? '禁用' : '启用';
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
key: 'update',
|
||||||
|
text: '编辑',
|
||||||
|
tooltip: {
|
||||||
|
title: '编辑',
|
||||||
|
},
|
||||||
|
icon: 'EditOutlined',
|
||||||
|
onClick: () => {
|
||||||
|
handlEdit(data);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'action',
|
||||||
|
text: stateText,
|
||||||
|
tooltip: {
|
||||||
|
title: stateText,
|
||||||
|
},
|
||||||
|
icon: state === 'enabled' ? 'StopOutlined' : 'CheckCircleOutlined',
|
||||||
|
popConfirm: {
|
||||||
|
title: `确认${stateText}?`,
|
||||||
|
onConfirm: async () => {
|
||||||
|
const res = await update(data.id, updateStatus[state]);
|
||||||
|
if (res.success) {
|
||||||
|
message.success('操作成功');
|
||||||
|
tableRef.value?.reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
text: '删除',
|
||||||
|
disabled: state === 'enabled',
|
||||||
|
tooltip: {
|
||||||
|
title:
|
||||||
|
state === 'enabled' ? '请先禁用该组件,再删除。' : '删除',
|
||||||
|
},
|
||||||
|
popConfirm: {
|
||||||
|
title: '确认删除?',
|
||||||
|
onConfirm: async () => {
|
||||||
|
const res = await remove(data.id);
|
||||||
|
if (res.success) {
|
||||||
|
message.success('操作成功');
|
||||||
|
tableRef.value.reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icon: 'DeleteOutlined',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return actions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlAdd = () => {
|
||||||
|
current.value = {};
|
||||||
|
visible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlEdit = (data: object) => {
|
||||||
|
current.value = _.cloneDeep(data);
|
||||||
|
visible.value = true;
|
||||||
|
};
|
||||||
|
const handlEye = (id: string) => {
|
||||||
|
console.log(id);
|
||||||
|
};
|
||||||
|
const saveChange = (value: object) => {
|
||||||
|
visible.value = false;
|
||||||
|
current.value = {};
|
||||||
|
if (value) {
|
||||||
|
message.success('操作成功');
|
||||||
|
tableRef.value.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getState = (record: Partial<Record<string, any>>) => {
|
||||||
|
if (record) {
|
||||||
|
if (record?.state?.value === 'enabled') {
|
||||||
|
return { ...record?.runningState };
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
text: '禁用',
|
||||||
|
value: 'disabled',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索
|
||||||
|
* @param params
|
||||||
|
*/
|
||||||
|
const handleSearch = (e: any) => {
|
||||||
|
params.value = e;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.card-item-content {
|
||||||
|
min-height: 100px;
|
||||||
|
|
||||||
|
.card-item-content-title-a {
|
||||||
|
// color: #000 !important;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
overflow: hidden; //超出的文本隐藏
|
||||||
|
text-overflow: ellipsis; //溢出用省略号显示
|
||||||
|
white-space: nowrap; //溢出不换行
|
||||||
|
}
|
||||||
|
.card-item-content-box {
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
||||||
|
.card-item-content-text {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: rgba(0, 0, 0, 0.75);
|
||||||
|
font-size: 12px;
|
||||||
|
overflow: hidden; //超出的文本隐藏
|
||||||
|
text-overflow: ellipsis; //溢出用省略号显示
|
||||||
|
white-space: nowrap; //溢出不换行
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.details-text {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,19 @@
|
||||||
|
export interface ConfigurationType {
|
||||||
|
port: string | undefined;
|
||||||
|
host: string | undefined;;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
endpoint: string,
|
||||||
|
securityPolicy: string | undefined,
|
||||||
|
securityMode: string | undefined,
|
||||||
|
certificate: string | undefined,
|
||||||
|
authType: string | undefined,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormDataType {
|
||||||
|
name: string;
|
||||||
|
provider: string | undefined,
|
||||||
|
configuration: ConfigurationType;
|
||||||
|
description?: string;
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
<template>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div class="dash-board">
|
||||||
|
<div class="header">
|
||||||
|
<div class="left">
|
||||||
|
<h3 style="width: 100px">点位数据量</h3>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<a-radio-group
|
||||||
|
default-value="a"
|
||||||
|
button-style="solid"
|
||||||
|
style="margin-right: 10px"
|
||||||
|
v-model:value="data.time.type"
|
||||||
|
>
|
||||||
|
<a-radio-button value="hour">
|
||||||
|
最近1小时
|
||||||
|
</a-radio-button>
|
||||||
|
<a-radio-button value="today"> 今日 </a-radio-button>
|
||||||
|
<a-radio-button value="week"> 近一周 </a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
<a-range-picker
|
||||||
|
:allowClear="false"
|
||||||
|
:show-time="{ format: 'HH:mm:ss' }"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
v-model="data.time"
|
||||||
|
@change="pickerTimeChange"
|
||||||
|
>
|
||||||
|
<template #suffixIcon
|
||||||
|
><AIcon type="CalendarOutlined"
|
||||||
|
/></template>
|
||||||
|
</a-range-picker>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div ref="chartRef" style="width: 100%; height: 350px"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { dashboard } from '@/api/data-collect/dashboard';
|
||||||
|
import { getTimeByType, pointParams, pointOptionsSeries } from '../tool.ts';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
import { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
const chartRef = ref<Record<string, any>>({});
|
||||||
|
const loading = ref(false);
|
||||||
|
const data = ref({
|
||||||
|
time: {
|
||||||
|
type: 'hour',
|
||||||
|
end: 0,
|
||||||
|
start: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pickerTimeChange = (
|
||||||
|
value: [Dayjs, Dayjs],
|
||||||
|
dateString: [string, string],
|
||||||
|
) => {
|
||||||
|
data.value.time.start = Date.parse(dateString[0]);
|
||||||
|
data.value.time.end = Date.parse(dateString[1]);
|
||||||
|
data.value.time.type = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEcharts = async (val) => {
|
||||||
|
loading.value = true;
|
||||||
|
const resp = await dashboard(pointParams(val));
|
||||||
|
if (resp.success) {
|
||||||
|
const x = resp.result
|
||||||
|
.map((item: any) => item.data.timeString)
|
||||||
|
.reverse();
|
||||||
|
const y = resp.result.map((item: any) => item.data.value).reverse();
|
||||||
|
handleOptions(x, y);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false;
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOptions = (x = [], y = []) => {
|
||||||
|
const chart = chartRef.value;
|
||||||
|
if (chart) {
|
||||||
|
const myChart = echarts.init(chart);
|
||||||
|
const options = {
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data: x,
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '80px',
|
||||||
|
right: '50px',
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
},
|
||||||
|
color: ['#979AFF'],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '消息量',
|
||||||
|
data: y,
|
||||||
|
...pointOptionsSeries,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
myChart.setOption(options);
|
||||||
|
window.addEventListener('resize', function () {
|
||||||
|
myChart.resize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => data.value.time.type,
|
||||||
|
(value) => {
|
||||||
|
data.value.time.end = Date.parse(new Date());
|
||||||
|
data.value.time.start = Date.parse(getTimeByType(value));
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => data.value,
|
||||||
|
(value) => {
|
||||||
|
const { time } = value;
|
||||||
|
if (time.type || (time.end && time.start)) {
|
||||||
|
getEcharts(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.dash-board {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
padding: 24px;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0px 2.73036px 5.46071px rgba(31, 89, 245, 0.2);
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
.left h3 {
|
||||||
|
width: 200px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.left,
|
||||||
|
.right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,98 @@
|
||||||
|
<template>
|
||||||
|
<div class="top-card">
|
||||||
|
<div class="top-card-content">
|
||||||
|
<div class="content-left">
|
||||||
|
<div class="content-left-title">
|
||||||
|
<span>{{ title }}</span>
|
||||||
|
<a-tooltip placement="top" v-if="tooltip">
|
||||||
|
<template #title>
|
||||||
|
<span>{{ tooltip }}</span>
|
||||||
|
</template>
|
||||||
|
<AIcon type="QuestionCircleOutlined" />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="content-left-value">{{ value }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="content-right">
|
||||||
|
<img :src="img" alt="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="top-card-footer">
|
||||||
|
<template v-for="(item, index) in footer" :key="index">
|
||||||
|
<span v-if="!item.status">{{ item.title }}</span>
|
||||||
|
<a-badge v-else :text="item.title" :status="item.status" />
|
||||||
|
<div class="footer-item-value">{{ item.value }}</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
import type { Footer } from '../typings.d';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: { type: String, default: '' },
|
||||||
|
tooltip: { type: String, default: '' },
|
||||||
|
img: { type: String, default: '' },
|
||||||
|
footer: { type: Array as PropType<Footer[]>, default: '' },
|
||||||
|
value: { type: Number, default: 0 },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.top-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
// height: 200px;
|
||||||
|
padding: 24px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #e0e4e8;
|
||||||
|
border-radius: 2px;
|
||||||
|
max-height: 215px;
|
||||||
|
.top-card-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-grow: 1;
|
||||||
|
.content-left {
|
||||||
|
height: 100%;
|
||||||
|
width: 50%;
|
||||||
|
&-title {
|
||||||
|
color: rgba(0, 0, 0, 0.64);
|
||||||
|
}
|
||||||
|
&-value {
|
||||||
|
padding: 12px 0;
|
||||||
|
color: #323130;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.content-right {
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 123px;
|
||||||
|
max-width: 140px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.top-card-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
.footer-item-value {
|
||||||
|
color: #323130;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,38 @@
|
||||||
|
.media-dash-board {
|
||||||
|
.top-card-items {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
height: 100px;
|
||||||
|
.top-card-item {
|
||||||
|
width: 25%;
|
||||||
|
padding: 6px 24px;
|
||||||
|
border: 1px solid #e3e3e3;
|
||||||
|
|
||||||
|
.top-card-top {
|
||||||
|
display: flex;
|
||||||
|
padding: 12px 0;
|
||||||
|
|
||||||
|
.top-card-top-left {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-card-top-right {
|
||||||
|
.top-card-total {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-card-bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-top: 1px solid #e3e3e3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-dash-board-body {
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
<template>
|
||||||
|
<page-container>
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="8" v-for="item in statusData" :key="item[0].type">
|
||||||
|
<TopCard
|
||||||
|
:title="item[0].label"
|
||||||
|
:img="
|
||||||
|
getImage(`/DataCollect/dashboard/${item[0].type}.png`)
|
||||||
|
"
|
||||||
|
:footer="item"
|
||||||
|
:value="item[0].total"
|
||||||
|
/>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="24">
|
||||||
|
<Card />
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</page-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import TopCard from './components/TopCard.vue';
|
||||||
|
import Card from './components/Card.vue';
|
||||||
|
import { getImage } from '@/utils/comm';
|
||||||
|
import { queryCount } from '@/api/data-collect/dashboard';
|
||||||
|
import { defaultParams, statusData } from './tool';
|
||||||
|
|
||||||
|
const getNumberData = () => {
|
||||||
|
statusData.value.forEach(async (item) => {
|
||||||
|
const res = await queryCount(item[0].type, {});
|
||||||
|
const resp = await queryCount(item[0].type, defaultParams);
|
||||||
|
item[0].total = res.result || 0;
|
||||||
|
item[0].value = resp.result || 0;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
getNumberData();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
|
@ -0,0 +1,155 @@
|
||||||
|
import moment from 'moment';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
const getParams = (dt: any) => {
|
||||||
|
switch (dt.type) {
|
||||||
|
case 'today':
|
||||||
|
return {
|
||||||
|
limit: 24,
|
||||||
|
interval: '1h',
|
||||||
|
format: 'HH:mm',
|
||||||
|
};
|
||||||
|
case 'week':
|
||||||
|
return {
|
||||||
|
limit: 7,
|
||||||
|
interval: '1d',
|
||||||
|
format: 'MM-dd',
|
||||||
|
};
|
||||||
|
case 'hour':
|
||||||
|
return {
|
||||||
|
limit: 60,
|
||||||
|
interval: '1m',
|
||||||
|
format: 'HH:mm',
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
const time = dt.end - dt.start;
|
||||||
|
const hour = 60 * 60 * 1000;
|
||||||
|
const days = hour * 24;
|
||||||
|
const year = days * 365;
|
||||||
|
if (time <= hour) {
|
||||||
|
return {
|
||||||
|
limit: Math.abs(Math.ceil(time / (60 * 60))),
|
||||||
|
interval: '1m',
|
||||||
|
format: 'HH:mm',
|
||||||
|
};
|
||||||
|
} else if (time > hour && time <= days) {
|
||||||
|
return {
|
||||||
|
limit: Math.abs(Math.ceil(time / hour)),
|
||||||
|
interval: '1h',
|
||||||
|
format: 'HH:mm',
|
||||||
|
};
|
||||||
|
} else if (time >= year) {
|
||||||
|
return {
|
||||||
|
limit: Math.abs(Math.ceil(time / days / 31)) + 1,
|
||||||
|
interval: '1M',
|
||||||
|
format: 'yyyy年-M月',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
limit: Math.abs(Math.ceil(time / days)) + 1,
|
||||||
|
interval: '1d',
|
||||||
|
format: 'MM-dd',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTimeByType = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'hour':
|
||||||
|
return moment().subtract(1, 'hours');
|
||||||
|
case 'week':
|
||||||
|
return moment().subtract(6, 'days');
|
||||||
|
case 'month':
|
||||||
|
return moment().subtract(29, 'days');
|
||||||
|
case 'year':
|
||||||
|
return moment().subtract(365, 'days');
|
||||||
|
default:
|
||||||
|
return moment().startOf('day');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pointParams = (data) => [
|
||||||
|
{
|
||||||
|
dashboard: 'collector',
|
||||||
|
object: 'pointData',
|
||||||
|
measurement: 'quantity',
|
||||||
|
dimension: 'agg',
|
||||||
|
params: {
|
||||||
|
limit: getParams(data.time).limit,
|
||||||
|
from: data.time.start,
|
||||||
|
to: data.time.end,
|
||||||
|
interval: getParams(data.time).interval,
|
||||||
|
format: getParams(data.time).format,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const pointOptionsSeries = {
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
color: '#60DFC7',
|
||||||
|
areaStyle: {
|
||||||
|
color: {
|
||||||
|
type: 'linear',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
color: '#60DFC7', // 100% 处的颜色
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: '#FFFFFF', // 0% 处的颜色
|
||||||
|
},
|
||||||
|
],
|
||||||
|
global: false, // 缺省为 false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultParams = {
|
||||||
|
terms: [
|
||||||
|
{
|
||||||
|
column: 'runningState',
|
||||||
|
termType: 'not',
|
||||||
|
value: 'running',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const statusData = ref([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'channel',
|
||||||
|
title: '异常通道',
|
||||||
|
status: 'error',
|
||||||
|
label: '通道数量',
|
||||||
|
value: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'collector',
|
||||||
|
title: '异常采集器',
|
||||||
|
status: 'error',
|
||||||
|
label: '采集器数量',
|
||||||
|
value: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'point',
|
||||||
|
title: '异常点位',
|
||||||
|
status: 'error',
|
||||||
|
label: '采集点位',
|
||||||
|
value: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
|
@ -0,0 +1,18 @@
|
||||||
|
export type Agg = {
|
||||||
|
duration: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AggPlaying = {
|
||||||
|
playerTotal: number;
|
||||||
|
playingTotal: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Footer = {
|
||||||
|
title: string;
|
||||||
|
value: number | string;
|
||||||
|
total: number | string;
|
||||||
|
status?: 'default' | 'error' | 'success' | 'warning' | 'processing' | '';
|
||||||
|
type: string;
|
||||||
|
label: string;
|
||||||
|
};
|
|
@ -316,7 +316,7 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
|
||||||
|
|
||||||
const getProvidersList = async () => {
|
const getProvidersList = async () => {
|
||||||
const res = await getProviders();
|
const res = await getProviders();
|
||||||
providersList = res.result;
|
providersList.value = res.result;
|
||||||
};
|
};
|
||||||
getProvidersList();
|
getProvidersList();
|
||||||
|
|
||||||
|
@ -337,7 +337,7 @@ const handlEye = (id: string) => {
|
||||||
const getDescription = (slotProps: Record<string, any>) =>
|
const getDescription = (slotProps: Record<string, any>) =>
|
||||||
slotProps.description
|
slotProps.description
|
||||||
? slotProps.description
|
? slotProps.description
|
||||||
: providersList?.find(
|
: providersList.value?.find(
|
||||||
(item: Record<string, any>) => item.id === slotProps.provider,
|
(item: Record<string, any>) => item.id === slotProps.provider,
|
||||||
)?.description;
|
)?.description;
|
||||||
|
|
||||||
|
|
|
@ -33,8 +33,7 @@
|
||||||
>
|
>
|
||||||
</a-range-picker>
|
</a-range-picker>
|
||||||
</div>
|
</div>
|
||||||
<a-empty v-if="empty" class="empty" />
|
<div ref="chartRef" style="width: 100%; height: 300px"></div>
|
||||||
<div v-else ref="chartRef" style="width: 100%; height: 300px"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</template>
|
</template>
|
||||||
|
@ -54,7 +53,6 @@ import {
|
||||||
|
|
||||||
const chartRef = ref<Record<string, any>>({});
|
const chartRef = ref<Record<string, any>>({});
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const empty = ref(false);
|
|
||||||
const data = ref({
|
const data = ref({
|
||||||
type: 'hour',
|
type: 'hour',
|
||||||
time: [null, null],
|
time: [null, null],
|
||||||
|
@ -108,7 +106,6 @@ const handleCpuOptions = (optionsData, xAxis) => {
|
||||||
if (chart) {
|
if (chart) {
|
||||||
const myChart = echarts.init(chart);
|
const myChart = echarts.init(chart);
|
||||||
const dataKeys = Object.keys(optionsData);
|
const dataKeys = Object.keys(optionsData);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
|
@ -143,7 +140,6 @@ const handleCpuOptions = (optionsData, xAxis) => {
|
||||||
: typeDataLine,
|
: typeDataLine,
|
||||||
};
|
};
|
||||||
myChart.setOption(options);
|
myChart.setOption(options);
|
||||||
xAxis.length === 0 && (empty.value = true);
|
|
||||||
window.addEventListener('resize', function () {
|
window.addEventListener('resize', function () {
|
||||||
myChart.resize();
|
myChart.resize();
|
||||||
});
|
});
|
||||||
|
@ -190,7 +186,4 @@ watch(
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.empty {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -33,8 +33,7 @@
|
||||||
>
|
>
|
||||||
</a-range-picker>
|
</a-range-picker>
|
||||||
</div>
|
</div>
|
||||||
<a-empty v-if="empty" class="empty" />
|
<div ref="chartRef" style="width: 100%; height: 300px"></div>
|
||||||
<div v-else ref="chartRef" style="width: 100%; height: 300px"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</template>
|
</template>
|
||||||
|
@ -53,7 +52,6 @@ import {
|
||||||
} from './tool.ts';
|
} from './tool.ts';
|
||||||
|
|
||||||
const chartRef = ref<Record<string, any>>({});
|
const chartRef = ref<Record<string, any>>({});
|
||||||
const empty = ref(false);
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const data = ref({
|
const data = ref({
|
||||||
type: 'hour',
|
type: 'hour',
|
||||||
|
@ -147,7 +145,6 @@ const handleJVMOptions = (optionsData, xAxis) => {
|
||||||
: typeDataLine,
|
: typeDataLine,
|
||||||
};
|
};
|
||||||
myChart.setOption(options);
|
myChart.setOption(options);
|
||||||
xAxis.length === 0 && (empty.value = true);
|
|
||||||
window.addEventListener('resize', function () {
|
window.addEventListener('resize', function () {
|
||||||
myChart.resize();
|
myChart.resize();
|
||||||
});
|
});
|
||||||
|
@ -194,7 +191,4 @@ watch(
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.empty {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -37,18 +37,13 @@
|
||||||
@change="pickerTimeChange"
|
@change="pickerTimeChange"
|
||||||
>
|
>
|
||||||
<template #suffixIcon
|
<template #suffixIcon
|
||||||
><a-icon type="calendar"
|
><AIcon type="CalendarOutlined"
|
||||||
/></template>
|
/></template>
|
||||||
</a-range-picker>
|
</a-range-picker>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a-empty v-if="empty" class="empty" />
|
<div ref="chartRef" style="width: 100%; height: 350px"></div>
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
ref="chartRef"
|
|
||||||
style="width: 100%; height: 350px"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
|
@ -61,13 +56,11 @@ import {
|
||||||
typeDataLine,
|
typeDataLine,
|
||||||
areaStyle,
|
areaStyle,
|
||||||
networkParams,
|
networkParams,
|
||||||
arrayReverse,
|
|
||||||
} from './tool.ts';
|
} from './tool.ts';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
const chartRef = ref<Record<string, any>>({});
|
const chartRef = ref<Record<string, any>>({});
|
||||||
const empty = ref(false);
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const data = ref({
|
const data = ref({
|
||||||
type: 'bytesRead',
|
type: 'bytesRead',
|
||||||
|
@ -134,7 +127,6 @@ const setOptions = (data, key) => ({
|
||||||
|
|
||||||
const handleNetworkOptions = (optionsData, xAxis) => {
|
const handleNetworkOptions = (optionsData, xAxis) => {
|
||||||
const chart = chartRef.value;
|
const chart = chartRef.value;
|
||||||
|
|
||||||
if (chart) {
|
if (chart) {
|
||||||
const myChart = echarts.init(chart);
|
const myChart = echarts.init(chart);
|
||||||
const dataKeys = Object.keys(optionsData);
|
const dataKeys = Object.keys(optionsData);
|
||||||
|
@ -148,7 +140,7 @@ const handleNetworkOptions = (optionsData, xAxis) => {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
left: '80px',
|
left: '100px',
|
||||||
right: '50px',
|
right: '50px',
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
@ -161,7 +153,6 @@ const handleNetworkOptions = (optionsData, xAxis) => {
|
||||||
: typeDataLine,
|
: typeDataLine,
|
||||||
};
|
};
|
||||||
myChart.setOption(options);
|
myChart.setOption(options);
|
||||||
// xAxis.length === 0 && (empty.value = true);
|
|
||||||
window.addEventListener('resize', function () {
|
window.addEventListener('resize', function () {
|
||||||
myChart.resize();
|
myChart.resize();
|
||||||
});
|
});
|
||||||
|
@ -215,7 +206,4 @@ watch(
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.empty {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in New Issue