Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev

This commit is contained in:
easy 2023-02-02 18:10:16 +08:00
commit 519042d019
31 changed files with 1899 additions and 159 deletions

View File

@ -175,3 +175,45 @@ export const delTags = (deviceId: string, id: string) => server.remove(`/device/
* @returns
*/
export const configurationReset = (deviceId: string) => server.put(`/device-instance/${deviceId}/configuration/_reset`)
/**
*
* @param deviceId id
* @param eventId id
* @param data
* @returns
*/
export const getEventList = (deviceId: string, eventId: string, data: Record<string, any>) => server.post(`/device-instance/${deviceId}/event/${eventId}?format=true`, data)
/**
*
* @param deviceId id
* @param data
* @returns
*/
export const setProperty = (deviceId: string, data: Record<string, any>) => server.put(`/device-instance/${deviceId}/property`, data)
/**
*
* @param deviceId id
* @param type id
* @returns
*/
export const getProperty = (deviceId: string, type: string) => server.get(`/device/standard/${deviceId}/property/${type}`)
/**
*
* @param deviceId id
* @param propertyId id
* @returns
*/
export const queryMetric = (deviceId: string, propertyId: string) => server.get(`/device-instance/${deviceId}/metric/property/${propertyId}`)
/**
*
* @param deviceId id
* @param propertyId id
* @param data
* @returns
*/
export const saveMetric = (deviceId: string, propertyId: string, data: Record<string, any>) => server.patch(`/device-instance/${deviceId}/metric/property/${propertyId}`, data)

View File

@ -0,0 +1,31 @@
import server from '@/utils/request'
/**
*
* @param data
*/
export const queryList = (data: any) => server.post(`/network/card/platform/_query`, data)
/**
* id查询详情
* @param id
*/
export const queryById = (id: any) => server.get(`/network/card/platform/${id}`)
/**
*
* @param data
*/
export const save = (data: any) => server.post(`/network/card/platform`, data)
/**
*
* @param data
*/
export const update = (data: any) => server.patch(`/network/card/platform`, data)
/**
*
* @param id
*/
export const del = (id: string) => server.remove(`/network/card/platform/${id}`)

View File

@ -0,0 +1,7 @@
import server from '@/utils/request'
/**
*
* @param data
*/
export const queryList = (data: any) => server.post(`/network/card/stateOperate/_log`, data)

View File

@ -6,5 +6,6 @@ export default {
agg: () => server.get<Agg>(`/media/record/file/agg`),
// 播放中数量
aggPlaying: () => server.get<AggPlaying>(`/media/channel/playing/agg`),
// 获取播放数量(人次)
getPlayCount: (data: any) => server.post<any>(`/dashboard/_multi`, data),
}

View File

@ -2,8 +2,8 @@ import server from '@/utils/request'
export default {
// 设备数量
deviceCount: () => server.get<number>(`/media/device/_count`),
deviceCount: (params: any) => server.get<number>(`/media/device/_count`, params),
// 通道数量
channelCount: () => server.post<number>(`/media/channel/_count`),
channelCount: (data: any) => server.post<number>(`/media/channel/_count`, data),
}

View File

@ -4,14 +4,14 @@
<a-popconfirm v-bind="popConfirm" :disabled="!isPermission || props.disabled">
<a-tooltip v-if="tooltip" v-bind="tooltip">
<slot v-if="noButton"></slot>
<a-button v-else v-bind="buttonProps" :disabled="_isPermission" @click="handleClick">
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" @click="handleClick">
<slot></slot>
<template #icon>
<slot name="icon"></slot>
</template>
</a-button>
</a-tooltip>
<a-button v-else v-bind="buttonProps" :disabled="_isPermission" @click="handleClick">
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" @click="handleClick">
<slot></slot>
<template #icon>
<slot name="icon"></slot>
@ -22,7 +22,7 @@
<template v-else-if="tooltip">
<a-tooltip v-bind="tooltip">
<slot v-if="noButton"></slot>
<a-button v-else v-bind="buttonProps" :disabled="_isPermission" @click="handleClick">
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" @click="handleClick">
<slot></slot>
<template #icon>
<slot name="icon"></slot>
@ -32,7 +32,7 @@
</template>
<template v-else>
<slot v-if="noButton"></slot>
<a-button v-else v-bind="buttonProps" :disabled="_isPermission" @click="handleClick">
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" @click="handleClick">
<slot></slot>
<template #icon>
<slot name="icon"></slot>
@ -42,7 +42,7 @@
</template>
<a-tooltip v-else title="没有权限">
<slot v-if="noButton"></slot>
<a-button v-else v-bind="buttonProps" :disabled="_isPermission" @click="handleClick">
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" @click="handleClick">
<slot></slot>
<template #icon>
<slot name="icon"></slot>
@ -51,7 +51,9 @@
</a-tooltip>
</template>
<script setup lang="ts" name="PermissionButton">
import type { ButtonProps, TooltipProps, PopconfirmProps } from 'ant-design-vue'
import { PropType } from 'vue'
import { TooltipProps, PopconfirmProps } from 'ant-design-vue/es'
import { buttonProps } from 'ant-design-vue/es/button/button'
import { usePermissionStore } from '@/store/permission';
interface PermissionButtonEmits {
@ -60,16 +62,33 @@ interface PermissionButtonEmits {
const emits = defineEmits<PermissionButtonEmits>()
interface PermissionButtonProps extends ButtonProps {
tooltip?: TooltipProps;
popConfirm?: PopconfirmProps;
hasPermission?: string | Array<string>;
noButton?: boolean;
}
const props = withDefaults(defineProps<PermissionButtonProps>(), {
noButton: false
// interface PermissionButtonProps extends ButtonProps {
// tooltip?: TooltipProps;
// popConfirm?: PopconfirmProps;
// hasPermission?: string | Array<string>;
// noButton?: boolean;
// }
// const props = withDefaults(defineProps<PermissionButtonProps>(), {
// noButton: false,
// })
const props = defineProps({
noButton: {
type: Boolean,
default: () => false
},
tooltip: {
type: Object as PropType<TooltipProps>,
},
popConfirm: {
type: Object as PropType<PopconfirmProps>,
},
hasPermission: {
type: String || Array,
},
...buttonProps()
})
const { tooltip, popConfirm, hasPermission, noButton, ...buttonProps } = props;
const { tooltip, popConfirm, hasPermission, noButton, ..._buttonProps } = props;
const permissionStore = usePermissionStore()
@ -82,7 +101,7 @@ const isPermission = computed(() => {
const _isPermission = computed(() =>
'hasPermission' in props && isPermission.value
? 'disabled' in buttonProps
? buttonProps.disabled
? buttonProps.disabled as boolean
: false
: true
)

View File

@ -74,7 +74,8 @@ const JTable = defineComponent<JTableProps>({
slots: [
'headerTitle', // 顶部左边插槽
'card', // 卡片内容
'rightExtraRender'
'rightExtraRender',
'paginationRender' // 分页
],
emits: [
'modelChange', // 切换卡片和表格
@ -354,6 +355,9 @@ const JTable = defineComponent<JTableProps>({
{
(!!_dataSource.value.length) && !props.noPagination && props.type === 'PAGE' &&
<div class={styles['jtable-pagination']}>
{
slots?.paginationRender ?
slots.paginationRender() :
<Pagination
size="small"
total={total.value}
@ -375,6 +379,7 @@ const JTable = defineComponent<JTableProps>({
})
}}
/>
}
</div>
}
</div>

View File

@ -84,3 +84,34 @@ export const randomString = (length?: number) => {
}
return pwd;
};
/**
*
* @param time
* @returns
*/
export const timestampFormat = (time: number) => {
let hour = 0;
let minute = 0;
let second = 0;
const timeStr = 'hh小时mm分钟ss秒';
if (time) {
if (time >= 60 * 60 * 1000) {
hour = Math.trunc(time / (60 * 60 * 1000));
}
if (time >= 60 * 1000) {
minute = Math.trunc((time - hour * 60 * 60 * 1000) / (60 * 1000));
}
second = Math.trunc(
(time - hour * (60 * 60 * 1000) - minute * 60 * 1000) / 1000,
);
}
return timeStr
.replace('hh', hour.toString())
.replace('mm', minute.toString())
.replace('ss', second.toString());
};

View File

@ -3,7 +3,7 @@
<JTable
ref="eventsRef"
:columns="columns"
:dataSource="dataSource"
:request="_getEventList"
model="TABLE"
:bodyStyle="{padding: '0 24px'}"
>
@ -16,16 +16,24 @@
</a-button>
</template>
</JTable>
<a-button type="link" @click="detail(slotProps)">
<AIcon type="SearchOutlined" />
</a-button>
</template>
<script lang="ts" setup>
import moment from 'moment'
import { getEventList } from '@/api/device/instance'
import { useInstanceStore } from '@/store/instance'
import { Modal } from 'ant-design-vue'
const events = defineProps({
data: {
type: Object,
default: () => {}
}
})
const instanceStore = useInstanceStore()
const columns = ref<Record<string, any>>([
{
@ -41,8 +49,9 @@ const columns = ref<Record<string, any>>([
scopedSlots: true,
}
])
const params = ref<Record<string, any>>({})
const dataSource = ref<Record<string, any>[]>([])
const _getEventList = () => getEventList(instanceStore.current.id || '', events.data.id || '', params.value)
watchEffect(() => {
if(events.data?.valueType?.type === 'object'){
@ -50,8 +59,7 @@ watchEffect(() => {
columns.value.splice(0, 0, {
key: i.id,
title: i.name,
dataIndex: `${i.id}_format`,
// renderText: (text) => (typeof text === 'object' ? JSON.stringify(text) : text),
dataIndex: `${i.id}_format`
})
})
} else {
@ -63,6 +71,13 @@ watchEffect(() => {
})
const detail = () => {
Modal.info({
title: () => '详情',
width: 850,
content: () => h('div', {}, [
h('p', '暂未开发'),
]),
okText: '关闭'
});
}
</script>

View File

@ -0,0 +1,156 @@
<template>
<a-modal
:maskClosable="false"
:visible="true"
title="编辑指标"
@ok="handleSave"
@cancel="handleCancel"
:confirmLoading="loading"
>
<a-alert message="场景联动页面可引用指标配置触发条件" type="warning" showIcon />
<a-form layout="vertical" ref="formRef" :model="modelRef" style="margin-top: 20px">
<template v-for="(item, index) in modelRef.metrics" :key="index">
<a-row type="flex" justify="space-between" align="bottom">
<a-col :span="11">
<a-form-item
:rules="{
required: true,
message: `${['date', 'boolean'].includes(data?.valueType?.type)? '选择': '输入'}指标值`,
}"
:name="['metrics', index, 'value', 0]"
:label="item?.name || '指标值'"
>
<ValueItem
v-model:modelValue="item.value[0]"
:itemType="data.valueType?.type"
:options="
data.valueType?.type === 'boolean'
? [
{
label: data.valueType?.trueText,
value: String(data.valueType?.trueValue),
},
{
label: data.valueType?.falseText,
value: String(data.valueType?.falseValue),
},
]
: undefined
"
/>
</a-form-item>
</a-col>
<template v-if="item.range">
<a-col><div class="center-icon">~</div></a-col>
<a-col :span="11">
<a-form-item
:name="['metrics', index, 'value', 1]"
:rules="{
required: true,
message: `${['date', 'boolean'].includes(data?.valueType?.type)? '选择': '输入'}指标值`,
}"
>
<ValueItem
v-model:modelValue="item.value[1]"
:itemType="data.valueType?.type"
/>
</a-form-item>
</a-col>
</template>
</a-row>
</template>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { queryMetric, saveMetric } from '@/api/device/instance'
const emit = defineEmits(['close']);
import { useInstanceStore } from "@/store/instance"
import { message } from 'ant-design-vue';
const props = defineProps({
data: {
type: Object,
default: () => {}
}
})
const loading = ref<boolean>(false)
const instanceStore = useInstanceStore()
const formRef = ref();
const modelRef = reactive({
metrics: []
});
const handleCancel = () => {
emit('close')
}
watch(() => props.data.id, (newVal) => {
if(newVal && instanceStore.current.id){
queryMetric(instanceStore.current.id, props.data.id).then(resp => {
if (resp.status === 200) {
if (Array.isArray(resp?.result) && resp?.result.length) {
const list = resp?.result.map((item: any) => {
const val = Array.isArray(item?.value) ? [item?.value] : item?.value?.split(',')
return {
...item,
value: val
};
});
modelRef.metrics = list as any
} else {
const type = props.data.valueType?.type;
if (type === 'boolean') {
const list = props.data.expands?.metrics.map((item: any) => {
const value = (item?.value || {}).map((i: any) => String(i)) || {};
return {
...item,
value,
};
});
modelRef.metrics = list || []
} else {
modelRef.metrics = props.data.expands?.metrics || []
}
}
}
})
}
}, {immediate: true, deep: true})
const handleSave = () => {
formRef.value
.validate()
.then(async () => {
loading.value = true;
const list = (toRaw(modelRef)?.metrics || []).map((item: any) => {
return {
...item,
value: item.value.join(','),
};
});
const resp = await saveMetric(instanceStore.current.id || '', props.data.id || '', list).finally(() => {
loading.value = false
})
if (resp.status === 200) {
message.success('操作成功!');
emit('close')
formRef.value.resetFields();
}
})
.catch((err: any) => {
console.log('error', err);
});
}
</script>
<style lang="less" scoped>
.center-icon {
height: 86px;
display: flex;
align-items: center;
}
</style>

View File

@ -5,33 +5,20 @@
<div class="header">
<div class="title">{{ _props.data.name }}</div>
<div class="extra">
<a-space>
<a-tooltip title="设置属性至设备" v-if="data.expands?.type?.includes('write')">
<AIcon
type="EditOutlined"
style="font-size: 12px"
/>
</a-tooltip>
<a-tooltip title="指标" v-if="(data.expands?.metrics || []).length > 0 &&
['int', 'long', 'float', 'double', 'string', 'boolean', 'date'].includes(
data.valueType?.type || '',
)">
<AIcon
type="ClockCircleOutlined"
style="font-size: 12px"
/>
</a-tooltip>
<a-tooltip title="获取最新属性值" v-if="data.expands?.type?.includes('read')">
<AIcon
type="SyncOutlined"
style="font-size: 12px"
/>
</a-tooltip>
<a-tooltip title="详情">
<AIcon
type="BarsOutlined"
style="font-size: 12px"
/>
<a-space :size="16">
<a-tooltip
v-for="i in actions"
:key="i.key"
v-bind="i.tooltip"
>
<a-button
style="padding: 0; margin: 0"
type="link"
:disabled="i.disabled"
@click="i.onClick && i.onClick(data)"
>
<AIcon :type="i.icon" style="color: #323130; font-size: 12px" />
</a-button>
</a-tooltip>
</a-space>
</div>
@ -55,6 +42,10 @@ const _props = defineProps({
type: Object,
default: () => {},
},
actions: {
type: Array,
default: () => []
},
});
const loading = ref<boolean>(true);

View File

@ -0,0 +1,85 @@
<template>
<a-modal
:maskClosable="false"
:visible="true"
title="编辑"
@ok="handleSave"
@cancel="handleCancel"
:confirmLoading="loading"
>
<a-alert message="当数据来源为设备时,填写的值将下发到设备" type="warning" showIcon />
<a-form :rules="rules" layout="vertical" ref="formRef" :model="modelRef" style="margin-top: 20px">
<a-form-item name="propertyValue" :label="data?.name || '自定义属性'">
<ValueItem
v-model:modelValue="modelRef.propertyValue"
:itemType="data?.valueType?.type || data?.dataType"
:options="
(data?.valueType?.type || data?.dataType) === 'enum'
? (data?.valueType?.elements || []).map((item) => {
return {
label: item?.text,
value: item?.value
};
})
: undefined
"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { setProperty } from '@/api/device/instance'
const emit = defineEmits(['close']);
import { useInstanceStore } from "@/store/instance"
import { message } from 'ant-design-vue';
const props = defineProps({
data: {
type: Object,
default: () => {}
}
})
const loading = ref<boolean>(false)
const instanceStore = useInstanceStore()
const formRef = ref();
const modelRef = reactive({
propertyValue: undefined
});
const handleCancel = () => {
emit('close')
}
const rules = {
propertyValue: [
{
required: true,
message: '该字段是必填字段',
}
],
}
const handleSave = () => {
formRef.value
.validate()
.then(async () => {
loading.value = true;
const resp = await setProperty(instanceStore.current?.id || '', {[props.data?.id]: toRaw(modelRef)?.propertyValue}).finally(() => {
loading.value = false
})
if (resp.status === 200) {
message.success('操作成功!');
emit('close')
formRef.value.resetFields();
}
})
.catch((err: any) => {
console.log('error', err);
});
}
</script>

View File

@ -1,14 +1,74 @@
<template>
<div>
{{data.value || '--'}}
<div class="value">
{{value}}
</div>
</template>
<script lang="ts" setup>
import { getImage } from "@/utils/comm";
const _data = defineProps({
data: {
type: Object,
default: () => {},
},
value: {
type: [Object, String, Number],
default: '--'
},
type: {
type: String,
default: 'card'
}
});
const imgMap = new Map<any, any>();
imgMap.set('txt', getImage('/running/txt.png'));
imgMap.set('doc', getImage('/running/doc.png'));
imgMap.set('xls', getImage('/running/xls.png'));
imgMap.set('ppt', getImage('/running/ppt.png'));
imgMap.set('docx', getImage('/running/docx.png'));
imgMap.set('xlsx', getImage('/running/xlsx.png'));
imgMap.set('pptx', getImage('/running/pptx.png'));
imgMap.set('pdf', getImage('/running/pdf.png'));
imgMap.set('img', getImage('/running/img.png'));
imgMap.set('error', getImage('/running/error.png'));
imgMap.set('video', getImage('/running/video.png'));
imgMap.set('other', getImage('/running/other.png'));
imgMap.set('obj', getImage('/running/obj.png'));
const imgList = ['.jpg', '.png', '.swf', '.tiff'];
const videoList = ['.m3u8', '.flv', '.mp4', '.rmvb', '.mvb'];
const fileList = ['.txt', '.doc', '.xls', '.pdf', '.ppt', '.docx', '.xlsx', '.pptx'];
</script>
<style lang="less" scoped>
.value {
display: flex;
align-items: center;
width: 100%;
.cardValue {
display: flex;
align-items: center;
width: 100%;
height: 60px;
overflow: hidden;
color: #323130;
font-weight: 700;
font-size: 24px;
white-space: nowrap;
text-overflow: ellipsis;
img {
width: 60px;
}
}
.otherValue {
img {
width: 40px;
}
}
}
</style>

View File

@ -1,19 +1,20 @@
<template>
<JTable
ref="metadataRef"
:columns="columns"
:dataSource="dataSource"
:bodyStyle="{padding: 0}"
:bodyStyle="{padding: '0 0 0 20px'}"
>
<template #headerTitle>
<a-input-search
placeholder="请输入名称"
style="width: 300px; margin-bottom: 10px"
@search="onSearch"
v-model:value="value"
:allowClear="true"
/>
</template>
<template #card="slotProps">
<PropertyCard :data="slotProps" />
<PropertyCard :data="slotProps" :actions="getActions(slotProps)" />
</template>
<template #value="slotProps">
<ValueRender :data="slotProps" />
@ -28,41 +29,45 @@
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
:disabled="i.disabled"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button
style="padding: 0"
type="link"
v-else
:disabled="i.disabled"
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
<AIcon :type="i.icon" />
</a-button>
</a-tooltip>
</a-space>
</template>
<template #paginationRender>
<a-pagination
size="small"
:total="total"
:showQuickJumper="false"
:showSizeChanger="true"
:current="pageIndex + 1"
:pageSize="pageSize"
:pageSizeOptions="['8', '12', '24', '60', '100']"
:show-total="(num) => `第 ${pageIndex * pageSize + 1} - ${(pageIndex + 1) * pageSize > num ? num : (pageIndex + 1) * pageSize} 条/总共 ${num} 条`"
@change="pageChange"
/>
</template>
</JTable>
<Save v-if="editVisible" @close="editVisible = false" :data="currentInfo" />
<Indicators v-if="indicatorVisible" @close="indicatorVisible = false" :data="currentInfo" />
</template>
<script lang="ts" setup>
import _ from "lodash"
import { PropertyData } from "../../../typings"
import PropertyCard from './PropertyCard.vue'
import ValueRender from './ValueRender.vue'
import Save from './Save.vue'
import Indicators from './Indicators.vue'
import { getProperty } from '@/api/device/instance'
import { useInstanceStore } from "@/store/instance"
import { message } from "ant-design-vue"
const columns = [
{
@ -96,8 +101,17 @@ const _data = defineProps({
default: () => []
}
})
const value = ref<string>('')
const dataSource = ref<PropertyData[]>([])
const _dataSource = ref<PropertyData[]>([])
const pageIndex = ref<number>(0)
const pageSize = ref<number>(8)
const total = ref<number>(0)
const editVisible = ref<boolean>(false) //
const detailVisible = ref<boolean>(false) //
const currentInfo = ref<Record<string, any>>({})
const instanceStore = useInstanceStore()
const indicatorVisible = ref<boolean>(false) //
const getActions = (data: Partial<Record<string, any>>) => {
const arr = []
@ -109,7 +123,8 @@ const getActions = (data: Partial<Record<string, any>>) => {
},
icon: 'EditOutlined',
onClick: () => {
editVisible.value = true
currentInfo.value = data
},
})
}
@ -123,7 +138,8 @@ const getActions = (data: Partial<Record<string, any>>) => {
},
icon: 'ClockCircleOutlined',
onClick: () => {
indicatorVisible.value = true
currentInfo.value = data
},
})
}
@ -134,8 +150,13 @@ const getActions = (data: Partial<Record<string, any>>) => {
title: '获取最新属性值',
},
icon: 'SyncOutlined',
onClick: () => {
onClick: async () => {
if(instanceStore.current.id && data.id){
const resp = await getProperty(instanceStore.current.id, data.id)
if(resp.status === 200){
message.success('操作成功!')
}
}
},
})
}
@ -147,17 +168,53 @@ const getActions = (data: Partial<Record<string, any>>) => {
},
icon: 'BarsOutlined',
onClick: () => {
detailVisible.value = true
currentInfo.value = data
},
})
return arr
}
watchEffect(() => {
dataSource.value = _data.data as PropertyData[]
const query = (page: number, size: number, value: string) => {
pageIndex.value = page || 0
pageSize.value = size || 8
const _from = pageIndex.value * pageSize.value
const _to = (pageIndex.value + 1) * pageSize.value
const arr = _.cloneDeep(_dataSource.value)
if(value){
const li = arr.filter((i: any) => {
return i?.name.indexOf(value) !== -1;
})
dataSource.value = li.slice(_from, _to)
total.value = li.length
} else {
dataSource.value = arr.slice(_from, _to)
total.value = arr.length
}
}
const pageChange = (page: number, size: number) => {
if(size === pageSize.value) {
query(page - 1, size, value.value)
} else {
query(0, size, value.value)
}
}
watch(() => _data.data,
(newVal) => {
if(newVal.length) {
_dataSource.value = newVal as PropertyData[]
query(0, 8, value.value)
}
}, {
deep: true,
immediate: true
})
const onSearch = () => {};
const onSearch = () => {
query(0, 8, value.value)
};
</script>

View File

@ -111,7 +111,6 @@ const tabChange = (key: string) => {
display: flex;
.property-box-left {
width: 200px;
margin-right: 20px;
}
.property-box-right {
flex: 1;

View File

@ -5,6 +5,8 @@
width="1100px"
:visible="true"
title="选择设备"
okText="确定"
cancelText="取消"
@ok="handleOk"
@cancel="handleCancel"
:confirmLoading="btnLoading"
@ -12,8 +14,9 @@
<div style="margin-top: 10px">
<Search
:columns="columns"
target="iot-card-management-search"
target="iot-card-bind-device"
@search="handleSearch"
type="simple"
/>
<JTable
ref="bindDeviceRef"
@ -121,9 +124,8 @@ const columns = [
},
];
const handleSearch = (params: any) => {
console.log(params);
params.value = params;
const handleSearch = (e: any) => {
params.value = e;
};
const onSelectChange = (record: any) => {

View File

@ -4,6 +4,8 @@
:maskClosable="false"
:visible="true"
title="导出"
okText="确定"
cancelText="取消"
@ok="handleOk"
@cancel="handleCancel"
>

View File

@ -4,6 +4,8 @@
:maskClosable="false"
:visible="true"
title="导入"
okText="确定"
cancelText="取消"
@ok="handleCancel"
@cancel="handleCancel"
>

View File

@ -4,6 +4,8 @@
width="600px"
:visible="true"
:title="type === 'add' ? '新增' : '编辑'"
okText="确定"
cancelText="取消"
@ok="handleOk"
@cancel="handleCancel"
:confirmLoading="btnLoading"

View File

@ -1,6 +1,6 @@
<!-- 物联卡管理 -->
<template>
<page-container class="container">
<page-container>
<Search
:columns="columns"
target="iot-card-management-search"
@ -124,10 +124,7 @@
</slot>
</template>
<template #content>
<h3
class="card-item-content-title"
@click.stop="handleView(slotProps.id)"
>
<h3 class="card-item-content-title">
{{ slotProps.id }}
</h3>
<a-row>
@ -553,6 +550,8 @@ const getActions = (
popConfirm: data.deviceId
? {
title: '确认解绑设备?',
okText: '确定',
cancelText: '取消',
onConfirm: async () => {
unbind(data.id).then((resp: any) => {
if (resp.status === 200) {
@ -599,6 +598,8 @@ const getActions = (
: data.cardStateType?.value === 'deactivate'
? '确认复机?'
: '确认停用?',
okText: '确定',
cancelText: '取消',
onConfirm: async () => {
if (data.cardStateType?.value === 'toBeActivated') {
changeDeploy(data.id).then((resp) => {
@ -633,6 +634,8 @@ const getActions = (
},
popConfirm: {
title: '确认删除?',
okText: '确定',
cancelText: '取消',
onConfirm: async () => {
const resp: any = await del(data.id);
if (resp.status === 200) {
@ -648,9 +651,8 @@ const getActions = (
];
};
const handleSearch = (params: any) => {
console.log(params);
params.value = params;
const handleSearch = (e: any) => {
params.value = e;
};
const onSelectChange = (keys: string[], rows: []) => {
@ -671,13 +673,6 @@ const handleClick = (dt: any) => {
}
};
/**
* 查看
*/
const handleView = (id: string) => {
message.warn(id + '暂未开发');
};
/**
* 新增
*/
@ -792,25 +787,19 @@ const handelRemove = async () => {
</script>
<style scoped lang="less">
.container {
.search {
width: calc(100% - 330px);
}
.flow-text {
.flow-text {
font-size: 20px;
font-weight: 600;
}
.progress-text {
}
.progress-text {
display: flex;
justify-content: space-between;
align-items: center;
}
:deep(.ant-progress-inner) {
}
:deep(.ant-progress-inner) {
border-radius: 0px;
}
:deep(.ant-progress-bg) {
}
:deep(.ant-progress-bg) {
border-radius: 0px;
}
}
</style>

View File

@ -0,0 +1,249 @@
<template>
<page-container>
<a-card>
<a-row :gutter="24">
<a-col :span="14">
<TitleComponent data="详情" />
<a-form
:layout="'vertical'"
ref="formRef"
:rules="rules"
:model="form"
>
<a-form-item
label="平台类型"
name="operatorName"
required
>
<PlatformType
:disabled="false"
:model="'singular'"
:itemStyle="{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-around',
minWidth: '130px',
}"
:options="platformTypeList"
v-model:value="form.operatorName"
@change="typeChange"
></PlatformType
></a-form-item>
<a-form-item label="名称" name="name">
<a-input
v-model:value="form.name"
placeholder="请输入名称"
/>
</a-form-item>
<!-- onelink -->
<div v-if="form.operatorName === 'onelink'">
<a-form-item label="App ID" name="appId">
<a-input
v-model:value="form.appId"
placeholder="请输入App ID"
/>
</a-form-item>
<a-form-item label="Password" name="passWord">
<a-input-password
v-model:value="form.passWord"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item label="接口地址" name="apiAddr">
<a-input
v-model:value="form.apiAddr"
placeholder="请输入接口地址"
/>
</a-form-item>
</div>
<!-- ctwing -->
<div v-if="form.operatorName === 'ctwing'">
<a-form-item label="用户id" name="userId">
<a-input
v-model:value="form.userId"
placeholder="请输入用户id"
/>
</a-form-item>
<a-form-item label="密码" name="passWord">
<a-input-password
v-model:value="form.passWord"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item label="secretKey" name="secretKey">
<a-input
v-model:value="form.secretKey"
placeholder="请输入secretKey"
/>
</a-form-item>
</div>
<!-- unicom -->
<div v-if="form.operatorName === 'unicom'">
<a-form-item label="App ID" name="appId">
<a-input
v-model:value="form.appId"
placeholder="请输入App ID"
/>
</a-form-item>
<a-form-item label="App Secret" name="appSecret">
<a-input
v-model:value="form.appSecret"
placeholder="请输入App Secret"
/>
</a-form-item>
<a-form-item label="创建者ID" name="openId">
<a-input
v-model:value="form.openId"
placeholder="请输入创建者ID"
/>
</a-form-item>
</div>
<a-form-item label="说明" name="explain">
<a-textarea
v-model:value="form.explain"
placeholder="请输入说明"
showCount
:rows="3"
:maxlength="200"
/>
</a-form-item>
<a-form-item>
<a-divider />
<a-button
:loading="saveBtnLoading"
type="primary"
@click="handleSave"
>
保存
</a-button>
</a-form-item>
</a-form>
</a-col>
<a-col :span="10">
<Doc :type="form.operatorName" />
</a-col>
</a-row>
</a-card>
</page-container>
</template>
<script lang="ts" setup>
import { getImage } from '@/utils/comm';
import PlatformType from '@/views/iot-card/components/PlatformType.vue';
import { queryById, save, update } from '@/api/iot-card/platform';
import { message } from 'ant-design-vue';
import Doc from '../doc/index.vue';
const router = useRouter();
const route = useRoute();
const formRef = ref();
const saveBtnLoading = ref<boolean>(false);
const form = reactive({
operatorName: 'onelink',
name: undefined,
// onelink
appId: undefined,
passWord: undefined,
apiAddr: undefined,
// ctwing
userId: undefined,
secretKey: undefined,
// unicom
appSecret: undefined,
openId: undefined,
explain: undefined,
});
const platformTypeList = [
{
label: '移动OneLink',
value: 'onelink',
imgUrl: getImage('/iot-card/onelink.png'),
imgSize: ['78px', '20px'],
},
{
label: '电信Ctwing',
value: 'ctwing',
imgUrl: getImage('/iot-card/ctwingcmp.png'),
imgSize: ['52px', '25px'],
},
{
label: '联通Unicom',
value: 'unicom',
imgUrl: getImage('/iot-card/unicom.png'),
imgSize: ['56px', '41px'],
},
];
const rules = {
name: [
{ required: true, message: '请输入名称' },
{ max: 64, message: '最多可输入64个字符' },
],
appId: [
{ required: true, message: '请输入App ID' },
{ max: 64, message: '最多可输入64个字符' },
],
passWord: [
{ required: true, message: '请输入密码' },
{ max: 64, message: '最多可输入64个字符' },
],
apiAddr: [{ required: true, message: '请输入接口地址' }],
userId: [
{ required: true, message: '请输入用户 ID' },
{ max: 64, message: '最多可输入64个字符' },
],
secretKey: [{ required: true, message: '请输入secretKey' }],
appSecret: [{ required: true, message: '请输入App Secret' }],
openId: [{ required: true, message: '请输入创建者ID' }],
explain: [{ required: false, max: 200, message: '最多可输入200个字符' }],
};
const getDetail = async () => {
if (route.params.id === ':id') return;
const resp: any = await queryById(route.params.id);
if (resp.status === 200) {
Object.assign(form, resp.result, { ...resp.result.config });
}
};
const typeChange = (val: any) => {
formRef.value.resetFields();
form.operatorName = val;
};
const handleSave = async () => {
const data: any = await formRef.value.validate();
const formData = {
operatorName: data.operatorName,
name: data.name,
config: {
appId: data.appId,
passWord: data.passWord,
apiAddr: data.apiAddr,
userId: data.userId,
secretKey: data.secretKey,
appSecret: data.appSecret,
openId: data.openId,
},
explain: data.explain,
};
saveBtnLoading.value = true;
const res: any =
route.params.id === ':id'
? await save(formData)
: await update({ id: route.params.id, ...formData });
if (res.status === 200) {
message.success('保存成功!');
router.back();
}
saveBtnLoading.value = false;
};
getDetail();
</script>

View File

@ -0,0 +1,220 @@
<template>
<div v-if="type === 'onelink'" class="doc">
<div class="url">
中国移动物联卡能力开放平台
<a
style="word-break: break-all"
href="https://api.iot.10086.cn/api/index.html#/login"
target="_blank"
rel="noreferrer"
>
https://api.iot.10086.cn/api/index.html#/login
</a>
</div>
<h1>1.概述</h1>
<p>
平台对接通过API的方式与三方系统进行数据对接为物联卡的管理提供数据交互支持
</p>
<h1>2.配置说明</h1>
<h2>1APP ID</h2>
<p>
第三方应用唯一标识中国移动物联网全网管理员在 OneLink
能力开放平台上分配并展示给集团客户
<br />
获取路径中移物联卡能力开放平台--个人中心--客户信息--接入信息
</p>
<div class="image">
<a-image
width="100%"
:src="getImage('/iot-card/onelink-appid.png')"
/>
</div>
<h2>2Password</h2>
<p>
API 接入秘钥,由中国移动物联网提供集团客户从OneLink
能力开放平台获取
<br />
获取路径中移物联卡能力开放平台--个人中心--客户信息--接入信息
</p>
<div class="image">
<a-image
width="100%"
:src="getImage('/iot-card/onelink-pass.png')"
/>
</div>
<h2>3接口地址</h2>
<p>
https://api.iot.10086.cn/v5/ec/get/token
<br />
token后缀请根据实际情况填写
<br />
示例https://api.iot.10086.cn/v5/authService?appid=xxx&password=xxx&transid=xxx
</p>
</div>
<div v-if="type === 'ctwing'" class="doc">
<div class="url">
5G连接管理平台
<a
style="word-break: break-all"
href="https://cmp.ctwing.cn:4821/login"
target="_blank"
rel="noreferrer"
>
https://cmp.ctwing.cn:4821/login
</a>
</div>
<div>
<h1>1.概述</h1>
<p>
平台对接通过API的方式与三方系统进行数据对接为物联卡的管理提供数据交互支持
</p>
<h1>2.配置说明</h1>
<h2>1用户 id</h2>
<p>
5G连接管理平台用户的唯一标识用于身份识别
<br />
获取路径5G连接管理平台--能力开放--API网关账号管理
</p>
<div class="image">
<a-image
width="100%"
:src="getImage('/iot-card/ctwing-id.png')"
/>
</div>
<h2>2密码</h2>
<p>
用户id经加密之后的密码
<br />
获取路径5G连接管理平台--能力开放--API网关账号管理
</p>
<div class="image">
<a-image
width="100%"
:src="getImage('/iot-card/ctwing-pass.png')"
/>
</div>
<h2>3secretKey</h2>
<p>
APP secret唯一秘钥
<br />
获取路径5G连接管理平台--能力开放--API网关账号管理
</p>
<div class="image">
<a-image
width="100%"
:src="getImage('/iot-card/ctwing-secret.png')"
/>
</div>
</div>
</div>
<div v-if="type === 'unicom'" class="doc">
<div class="url">
雁飞智连CMP平台
<a
style="word-break: break-all"
href=" https://cmp.10646.cn/webframe/login"
target="_blank"
rel="noreferrer"
>
https://cmp.10646.cn/webframe/login
</a>
</div>
<div>
<h1>1.概述</h1>
<p>
平台对接通过API的方式与三方系统进行数据对接为物联卡的管理提供数据交互支持
</p>
<h1>2.配置说明</h1>
<h2>1APP ID</h2>
<p>
第三方应用唯一标识
<br />
获取路径雁飞智连CMP平台--我的应用--应用列表
</p>
<div class="image">
<a-image
width="100%"
:src="getImage('/iot-card/unicom-id.png')"
/>
</div>
<h2>2App Secret</h2>
<p>
API 接入秘钥
<br />
获取路径雁飞智连CMP平台--我的应用--应用列表
</p>
<div class="image">
<a-image
width="100%"
:src="getImage('/iot-card/unicom-secret.png')"
/>
</div>
<h2>3创建者ID</h2>
<p>
接口参数中的 OpenId
<br />
获取路径雁飞智连CMP平台--我的应用--应用列表
<br />
</p>
<div class="image">
<a-image
width="100%"
:src="getImage('/iot-card/unicom-openid.png')"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { getImage } from '@/utils/comm';
const props = defineProps({
type: { type: String, default: 'onelink' },
});
</script>
<style scoped lang="less">
.doc {
height: 800px;
padding: 24px;
overflow-y: auto;
color: rgba(#000, 0.8);
font-size: 14px;
background-color: #fafafa;
.url {
padding: 8px 16px;
color: #2f54eb;
background-color: rgba(#a7bdf7, 0.2);
}
h1 {
margin: 16px 0;
color: rgba(#000, 0.85);
font-weight: bold;
font-size: 14px;
// &:first-child {
// margin-top: 0;
// }
}
h2 {
margin: 6px 0;
color: rgba(0, 0, 0, 0.8);
font-size: 14px;
}
.image {
margin: 16px 0;
}
}
</style>

View File

@ -0,0 +1,304 @@
<!-- 平台对接 -->
<template>
<page-container>
<Search
:columns="columns"
target="platform-search"
@search="handleSearch"
/>
<JTable
ref="platformRef"
:columns="columns"
:request="queryList"
:defaultParams="{ sorts: [{ name: 'createTime', order: 'desc' }] }"
:params="params"
>
<template #headerTitle>
<a-space>
<a-button type="primary" @click="handleAdd">
<AIcon type="PlusOutlined" />新增
</a-button>
</a-space>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
:status="slotProps.state.value"
:statusText="slotProps.state.text"
:statusNames="{
enabled: 'success',
disabled: '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">
{{ slotProps.name }}
</h3>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">
平台类型
</div>
<div>{{ slotProps.operatorName }}</div>
</a-col>
<a-col :span="12">
<div class="card-item-content-text">说明</div>
<div>{{ slotProps.explain }}</div>
</a-col>
</a-row>
</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 #state="slotProps">
<a-badge
:text="slotProps.state.text"
:status="
slotProps.state.value === 'disabled'
? 'error'
: 'success'
"
/>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm v-if="i.popConfirm" v-bind="i.popConfirm">
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
</a-space>
</template>
</JTable>
</page-container>
</template>
<script setup lang="ts">
import { getImage } from '@/utils/comm';
import type { ActionsType } from '@/components/Table';
import { message } from 'ant-design-vue';
import { queryList, update, del } from '@/api/iot-card/platform';
const router = useRouter()
const platformRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '平台类型',
dataIndex: 'operatorName',
key: 'operatorName',
search: {
type: 'select',
options: [
{ label: '移动OneLink', value: 'onelink' },
{ label: '电信Ctwing', value: 'ctwing' },
{ label: '联通Unicom', value: 'unicom' },
],
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
width: 120,
search: {
type: 'select',
options: [
{ label: '启用', value: 'enabled' },
{ label: '禁用', value: 'disabled' },
],
},
},
{
title: '说明',
dataIndex: 'explain',
key: 'explain',
ellipsis: true,
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
scopedSlots: true,
},
];
const statusUpdate = async (data: any) => {
const res = await update(data);
if (res.status === 200) {
message.success('操作成功');
platformRef.value?.reload();
}
};
const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return [];
return [
{
key: 'edit',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
router.push(`/iot-card/Platform/detail/${data.id}`);
},
},
{
key: 'action',
text: data.state.value === 'enabled' ? '禁用' : '启用',
tooltip: {
title: data.state.value === 'enabled' ? '禁用' : '启用',
},
icon:
data.state.value === 'enabled'
? 'StopOutlined'
: 'PlayCircleOutlined',
popConfirm: {
title: `确认${
data.state.value === 'enabled' ? '禁用' : '启用'
}`,
okText: ' 确定',
cancelText: '取消',
onConfirm: () => {
if (data.state.value === 'enabled') {
statusUpdate({
id: data.id,
config: { ...data.config },
state: 'disabled',
operatorName: data.operatorName,
});
} else {
statusUpdate({
id: data.id,
config: { ...data.config },
state: 'enabled',
operatorName: data.operatorName,
});
}
},
},
},
{
key: 'delete',
text: '删除',
tooltip: {
title:
data.state.value !== 'enabled' ? '删除' : '请先禁用再删除',
},
disabled: data.state.value === 'enabled',
popConfirm: {
title: '确认删除?',
okText: ' 确定',
cancelText: '取消',
onConfirm: async () => {
const resp: any = await del(data.id);
if (resp.status === 200) {
message.success('操作成功!');
platformRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
icon: 'DeleteOutlined',
},
];
};
const handleSearch = (e: any) => {
params.value = e;
};
/**
* 新增
*/
const handleAdd = () => {
router.push(`/iot-card/Platform/detail/:id`)
};
</script>
<style scoped lang="less"></style>

View File

@ -4,6 +4,8 @@
width="600px"
:visible="true"
title="充值"
okText="确定"
cancelText="取消"
@ok="handleOk"
@cancel="handleCancel"
:confirmLoading="btnLoading"

View File

@ -151,9 +151,8 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
];
};
const handleSearch = (params: any) => {
console.log(params);
params.value = params;
const handleSearch = (e: any) => {
params.value = e;
};
/**

View File

@ -0,0 +1,77 @@
<!-- 操作记录 -->
<template>
<page-container>
<Search
:columns="columns"
target="record-search"
@search="handleSearch"
/>
<JTable
ref="RecordRef"
:columns="columns"
:request="queryList"
:defaultParams="{ sorts: [{ name: 'time', order: 'desc' }] }"
:params="params"
:model="'TABLE'"
>
<template #time="slotProps">
{{
slotProps.time
? moment(slotProps.time).format('YYYY-MM-DD HH:mm:ss')
: ''
}}
</template>
</JTable>
</page-container>
</template>
<script setup lang="ts">
import { queryList } from '@/api/iot-card/record';
import moment from 'moment';
const params = ref<Record<string, any>>({});
const columns = [
{
title: '卡号',
dataIndex: 'cardId',
key: 'cardId',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '操作类型',
dataIndex: 'type',
key: 'type',
search: {
type: 'string',
},
},
{
title: '操作时间',
dataIndex: 'time',
key: 'time',
scopedSlots: true,
search: {
type: 'date',
},
},
{
title: '操作人',
dataIndex: 'operator',
key: 'operator',
ellipsis: true,
search: {
type: 'string',
},
},
];
const handleSearch = (e: any) => {
params.value = e;
};
</script>
<style scoped lang="less"></style>

View File

@ -0,0 +1,205 @@
<!-- 平台类型 -->
<template>
<div :class="['radio-card-items', className, disabled ? 'disabled' : '']">
<div
v-for="item in options"
:key="item.value"
:style="itemStyle"
:class="[
'radio-card-item',
keys.includes(item.value) ? 'checked' : '',
]"
@click="toggleOption(item.value)"
>
<div class="card-list">
<div>
<img
:style="`width: ${item.imgSize?.[0]}; height: ${item.imgSize?.[1]}`"
v-if="item.imgUrl"
:src="item.imgUrl"
alt=""
/>
</div>
<div>{{ item.label }}</div>
</div>
<div class="checked-icon">
<div><AIcon type="CheckOutlined" /></div>
</div>
</div>
</div>
</template>
<script setup>
const emit = defineEmits(['update:value', 'change'], );
const props = defineProps({
options: {
type: Array,
default: () => [],
required: true,
},
model: {
validator: function (value) {
return ['multiple', 'singular'].includes(value);
},
default: () => 'singular',
},
value: {
type: String,
default: () => '',
},
disabled: {
type: Boolean,
default: () => true,
},
className: {
type: String,
},
itemStyle: {
type: Object,
default: () => {},
},
});
const keys = ref(
!(props.model && props.model === 'singular') ? props.value : [props.value],
);
const toggleOption = (key) => {
if (props.disabled) {
return;
} else {
const optionIndex = keys.value.includes(key);
const newKeys = [...keys.value];
const singular = props.model && props.model === 'singular';
if (!optionIndex) {
if (!(props.model && props.model === 'singular')) {
newKeys.push(key);
} else {
newKeys[0] = key;
}
} else {
newKeys.splice(optionIndex, 1);
}
emit('update:value', singular ? newKeys[0] : newKeys);
emit('change', singular ? newKeys[0] : newKeys);
}
};
watch(
() => props.value,
(newVal) => {
keys.value = !(props.model && props.model === 'singular')
? newVal
: [newVal];
},
);
</script>
<style lang="less" scoped>
@border: 1px solid @border-color-base;
.radio-card-items {
display: flex;
.radio-card-item {
display: flex;
align-items: center;
min-width: 180px;
padding: 22px 28px;
overflow: hidden;
font-size: 14px;
border: @border;
border-radius: @border-radius-base;
.card-list {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
div {
text-align: center;
}
}
> img {
width: 32px;
height: 32px;
margin-right: 24px;
}
> span {
cursor: default;
}
&:not(:last-child) {
margin-right: 24px;
}
&:hover,
&:focus {
color: @primary-color-hover;
border-color: @primary-color-hover;
}
.checked-icon {
position: absolute;
right: -22px;
bottom: -22px;
z-index: 2;
display: none;
width: 44px;
height: 44px;
color: #fff;
background-color: @primary-color-active;
transform: rotate(-45deg);
> div {
position: relative;
height: 100%;
transform: rotate(45deg);
> span {
position: absolute;
top: 6px;
left: 6px;
font-size: 12px;
}
}
}
&.checked {
position: relative;
color: @primary-color-active;
border-color: @primary-color-active;
> .checked-icon {
display: block;
}
}
}
&.disabled {
.radio-card-item {
color: @disabled-color;
border-color: @disabled-bg;
cursor: not-allowed;
.checked-icon {
background-color: @disabled-active-bg;
}
&:hover,
&:focus {
color: @disabled-color;
border-color: @disabled-active-bg;
}
&.checked {
color: @disabled-color;
border-color: @disabled-active-bg;
}
}
}
}
</style>

View File

@ -1,7 +1,124 @@
<template>
<div class="page-container"></div>
<div class="page-container">
<div class="card-header">
<div class="title">{{ title }}</div>
<div class="tools">
<a-space>
<a-radio-group
v-model:value="dimension"
button-style="solid"
>
<a-radio-button value="today">今日</a-radio-button>
<a-radio-button value="week">近一周</a-radio-button>
<a-radio-button value="month">近一月</a-radio-button>
<a-radio-button value="year">近一年</a-radio-button>
</a-radio-group>
<a-range-picker v-model:value="dateRange" />
</a-space>
</div>
</div>
<div class="chart" ref="chartRef"></div>
</div>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
import * as echarts from 'echarts';
<style lang="less" scoped></style>
// const { proxy } = <any>getCurrentInstance();
const props = defineProps({
title: { type: String, default: '' },
//
chartData: { type: Array, default: () => [] },
});
//
const dimension = ref('week');
const dateRange = ref<any>([]);
/**
* 绘制图表
*/
const chartRef = ref();
const createChart = () => {
nextTick(() => {
const myChart = echarts.init(chartRef.value as HTMLElement);
const options = {
grid: {
left: '7%',
right: '5%',
top: '5%',
bottom: '5%',
},
tooltip: {
trigger: 'axis',
// formatter: '{a}<br>{b}: {c}',
axisPointer: {
type: 'shadow',
},
},
xAxis: [
{
data: props.chartData.map((m: any) => m.x),
},
],
yAxis: [
{
show: false,
axisTick: {
show: false,
},
axisLine: {
show: false,
},
splitLine: {
lineStyle: {
type: 'solid',
},
},
},
],
series: [
{
name: '播放数量(人次)',
type: 'line',
symbol: 'circle',
showSymbol: false,
smooth: true,
data: props.chartData.map(
(m: any) => m.value && m.value.toFixed(2),
),
},
],
};
myChart.setOption(options);
window.addEventListener('resize', function () {
myChart.resize();
});
});
};
watch(
() => props.chartData,
() => createChart(),
{ immediate: true, deep: true },
);
</script>
<style scoped lang="less">
.page-container {
.card-header {
display: flex;
justify-content: space-between;
.title {
font-weight: 700;
font-size: 16px;
}
}
.chart {
width: 100%;
height: 100%;
}
}
</style>

View File

@ -19,7 +19,8 @@
</div>
<div class="top-card-footer">
<template v-for="(item, index) in footer" :key="index">
<a-badge :text="item.title" :status="item.status" />
<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>

View File

@ -34,24 +34,49 @@
:value="aggPlayingTotal"
/>
</a-col>
<a-col :span="24">
<Card title="播放数量(人次)" :chartData="chartData" />
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import TopCard from '@/views/media/DashBoard/components/TopCard.vue'
import TopCard from '@/views/media/DashBoard/components/TopCard.vue';
import Card from '@/views/media/DashBoard/components/Card.vue';
import { getImage } from '@/utils/comm';
import homeApi from '@/api/media/home';
import dashboardApi from '@/api/media/dashboard';
import type { Footer } from '@/views/media/DashBoard/typings';
import encodeQuery from '@/utils/encodeQuery';
import { timestampFormat } from '@/utils/utils';
//
const deviceFooter = ref<Footer[]>([]);
const deviceTotal = ref(0);
const getDeviceData = () => {
homeApi.deviceCount().then((res) => {
homeApi.deviceCount({}).then((res) => {
deviceTotal.value = res.result;
});
homeApi
.deviceCount(encodeQuery({ terms: { state: 'online' } }))
.then((res) => {
deviceFooter.value[0] = {
title: '在线',
value: res.result,
status: 'success',
};
});
homeApi
.deviceCount(encodeQuery({ terms: { state: 'offline' } }))
.then((res) => {
deviceFooter.value[1] = {
title: '离线',
value: res.result,
status: 'error',
};
});
};
getDeviceData();
@ -59,9 +84,27 @@ getDeviceData();
const channelFooter = ref<Footer[]>([]);
const channelTotal = ref(0);
const getChannelData = () => {
homeApi.channelCount().then((res) => {
homeApi.channelCount({}).then((res) => {
channelTotal.value = res.result;
});
homeApi
.channelCount({ terms: [{ column: 'status', value: 'online' }] })
.then((res) => {
channelFooter.value[0] = {
title: '在线',
value: res.result,
status: 'success',
};
});
homeApi
.channelCount({ terms: [{ column: 'status$not', value: 'online' }] })
.then((res) => {
channelFooter.value[1] = {
title: '离线',
value: res.result,
status: 'error',
};
});
};
getChannelData();
@ -71,6 +114,10 @@ const aggTotal = ref(0);
const getAggData = () => {
dashboardApi.agg().then((res) => {
aggTotal.value = res.result.total;
aggFooter.value.push({
title: '总时长',
value: timestampFormat(res.result.duration),
});
});
};
getAggData();
@ -81,9 +128,32 @@ const aggPlayingTotal = ref(0);
const getAggPlayingData = () => {
dashboardApi.aggPlaying().then((res) => {
aggTotal.value = res.result.playingTotal;
aggPlayingFooter.value.push({
title: '播放人数',
value: res.result.playerTotal,
});
});
};
getAggPlayingData();
/**
* 获取播放数量(人次)
*/
const chartData = ref([]);
const getPlayCount = async () => {
const params = {};
dashboardApi.getPlayCount(params).then((res) => {
let result: any = [];
res.result.forEach((item: any) => {
result = [...result, ...item.data];
});
chartData.value = result.map((m: any) => ({
x: m.timeString,
value: m.value,
}));
});
};
getPlayCount();
</script>
<style lang="less" scoped>

View File

@ -10,7 +10,7 @@ export type AggPlaying = {
export type Footer = {
title: string;
value: number;
value: number | string;
status?: "default" | "error" | "success" | "warning" | "processing"
}