Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev
This commit is contained in:
commit
58c6d1aaf3
|
@ -114,6 +114,14 @@ export const isExists = (id: string) => server.get(`/device-instance/${id}/exist
|
|||
*/
|
||||
export const update = (data: Partial<DeviceInstance>) => data.id ? server.patch(`/device-instance`, data) : server.post(`/device-instance`, data)
|
||||
|
||||
/**
|
||||
* 修改设备信息
|
||||
* @param id 设备id
|
||||
* @param data 设备信息
|
||||
* @returns
|
||||
*/
|
||||
export const modify = (id: string, data: Partial<DeviceInstance>) => server.put(`/device-instance/${id}`, data)
|
||||
|
||||
/**
|
||||
* 获取配置信息
|
||||
* @param id 设备id
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import server from '@/utils/request'
|
||||
import type { Agg, AggPlaying } from '@/views/media/DashBoard/typings'
|
||||
|
||||
export default {
|
||||
// 录像数量
|
||||
agg: () => server.get<Agg>(`/media/record/file/agg`),
|
||||
// 播放中数量
|
||||
aggPlaying: () => server.get<AggPlaying>(`/media/channel/playing/agg`),
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import server from '@/utils/request'
|
||||
|
||||
export default {
|
||||
// 设备数量
|
||||
deviceCount: () => server.get<number>(`/media/device/_count`),
|
||||
// 通道数量
|
||||
channelCount: () => server.post<number>(`/media/channel/_count`),
|
||||
|
||||
}
|
|
@ -45,6 +45,7 @@ const iconKeys = [
|
|||
'InfoCircleOutlined',
|
||||
'SearchOutlined',
|
||||
'EllipsisOutlined',
|
||||
'ClockCircleOutlined'
|
||||
]
|
||||
|
||||
const Icon = (props: {type: string}) => {
|
||||
|
|
|
@ -2,22 +2,21 @@
|
|||
<div class="card">
|
||||
<div
|
||||
class="card-warp"
|
||||
:class="{ active: active ? 'active' : ''}"
|
||||
:class="{ active: active ? 'active' : '' }"
|
||||
@click="handleClick"
|
||||
>
|
||||
<div class="card-content">
|
||||
<a-row>
|
||||
<a-col :span="6">
|
||||
<!-- 图片 -->
|
||||
<div class="card-item-avatar">
|
||||
<slot name="img"> </slot>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="18">
|
||||
<!-- 内容 -->
|
||||
<div style="display: flex">
|
||||
<!-- 图片 -->
|
||||
<div class="card-item-avatar">
|
||||
<slot name="img"> </slot>
|
||||
</div>
|
||||
|
||||
<!-- 内容 -->
|
||||
<div class="card-item-body">
|
||||
<slot name="content"></slot>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 勾选 -->
|
||||
<div v-if="active" class="checked-icon">
|
||||
|
@ -201,6 +200,13 @@ const handleClick = () => {
|
|||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.card-item-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.card-state {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
.jtable-body {
|
||||
width: 100%;
|
||||
padding: 0 24px 24px;
|
||||
padding: 16px 24px 24px;
|
||||
background-color: white;
|
||||
.jtable-body-header {
|
||||
padding: 16px 0;
|
||||
padding-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
|
|
@ -40,11 +40,11 @@ export interface ActionsType {
|
|||
children?: ActionsType[];
|
||||
}
|
||||
|
||||
export interface JColumnProps extends ColumnProps{
|
||||
export interface JColumnProps extends ColumnProps {
|
||||
scopedSlots?: boolean; // 是否为插槽 true: 是 false: 否
|
||||
}
|
||||
|
||||
export interface JTableProps extends TableProps{
|
||||
export interface JTableProps extends TableProps {
|
||||
request?: (params?: Record<string, any>) => Promise<Partial<RequestData>>;
|
||||
cardBodyClass?: string;
|
||||
columns: JColumnProps[];
|
||||
|
@ -53,8 +53,8 @@ export interface JTableProps extends TableProps{
|
|||
// actions?: ActionsType[];
|
||||
noPagination?: boolean;
|
||||
rowSelection?: TableProps['rowSelection'];
|
||||
cardProps?: Record<string, any>;
|
||||
dataSource?: Record<string, any>[];
|
||||
cardProps?: Record<string, any>;
|
||||
dataSource?: Record<string, any>[];
|
||||
gridColumn?: number;
|
||||
/**
|
||||
* 用于不同分辨率
|
||||
|
@ -62,10 +62,11 @@ export interface JTableProps extends TableProps{
|
|||
* gridColumns[1] 1440 ~ 1600 分辨率;
|
||||
* gridColumns[2] > 1600 分辨率;
|
||||
*/
|
||||
gridColumns?: number[];
|
||||
alertRender?: boolean;
|
||||
type?: keyof typeof TypeEnum;
|
||||
defaultParams?: Record<string, any>;
|
||||
gridColumns?: number[];
|
||||
alertRender?: boolean;
|
||||
type?: keyof typeof TypeEnum;
|
||||
defaultParams?: Record<string, any>;
|
||||
bodyStyle?: Record<string, any>;
|
||||
}
|
||||
|
||||
const JTable = defineComponent<JTableProps>({
|
||||
|
@ -88,13 +89,17 @@ const JTable = defineComponent<JTableProps>({
|
|||
type: String,
|
||||
default: ''
|
||||
},
|
||||
bodyStyle: {
|
||||
type: Object,
|
||||
default: {}
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
params: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
default: () => { }
|
||||
},
|
||||
model: {
|
||||
type: [String, undefined],
|
||||
|
@ -142,7 +147,7 @@ const JTable = defineComponent<JTableProps>({
|
|||
}
|
||||
}
|
||||
} as any,
|
||||
setup(props: JTableProps ,{ slots, emit, expose }){
|
||||
setup(props: JTableProps, { slots, emit, expose }) {
|
||||
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE
|
||||
const _model = ref<keyof typeof ModelEnum>(props.model ? props.model : ModelEnum.CARD); // 模式切换
|
||||
const column = ref<number>(props.gridColumn || 4);
|
||||
|
@ -174,9 +179,9 @@ const JTable = defineComponent<JTableProps>({
|
|||
*/
|
||||
const handleSearch = async (_params?: Record<string, any>) => {
|
||||
loading.value = true
|
||||
if(props.request) {
|
||||
if (props.request) {
|
||||
const resp = await props.request({
|
||||
pageIndex: 0,
|
||||
pageIndex: 0,
|
||||
pageSize: 12,
|
||||
...props.defaultParams,
|
||||
..._params,
|
||||
|
@ -185,14 +190,14 @@ const JTable = defineComponent<JTableProps>({
|
|||
...(_params?.terms || [])
|
||||
]
|
||||
})
|
||||
if(resp.status === 200){
|
||||
if(props.type === 'PAGE'){
|
||||
if (resp.status === 200) {
|
||||
if (props.type === 'PAGE') {
|
||||
// 判断如果是最后一页且最后一页为空,就跳转到前一页
|
||||
if(resp.result.total && resp.result.pageSize && resp.result.pageIndex && resp.result?.data?.length === 0) {
|
||||
if (resp.result.total && resp.result.pageSize && resp.result.pageIndex && resp.result?.data?.length === 0) {
|
||||
handleSearch({
|
||||
..._params,
|
||||
pageSize: pageSize.value,
|
||||
pageIndex: pageIndex.value > 0 ? pageIndex.value - 1 : 0,
|
||||
pageIndex: pageIndex.value > 0 ? pageIndex.value - 1 : 0,
|
||||
})
|
||||
} else {
|
||||
_dataSource.value = resp.result?.data || []
|
||||
|
@ -204,31 +209,30 @@ const JTable = defineComponent<JTableProps>({
|
|||
_dataSource.value = resp?.result || []
|
||||
}
|
||||
} else {
|
||||
_dataSource.value = []
|
||||
_dataSource.value = []
|
||||
}
|
||||
} else {
|
||||
console.log(props?.dataSource)
|
||||
_dataSource.value = props?.dataSource || []
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.params,
|
||||
() => props.params,
|
||||
(newValue) => {
|
||||
handleSearch(newValue)
|
||||
},
|
||||
{deep: true, immediate: true}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.dataSource,
|
||||
(newValue) => {
|
||||
if(props.dataSource){
|
||||
() => props.dataSource,
|
||||
() => {
|
||||
if (props.dataSource && !props.request) {
|
||||
handleSearch(props.params)
|
||||
}
|
||||
},
|
||||
{deep: true, immediate: true}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -241,6 +245,10 @@ const JTable = defineComponent<JTableProps>({
|
|||
window.onresize = null
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
// console.log(props.bodyStyle)
|
||||
})
|
||||
|
||||
/**
|
||||
* 刷新数据
|
||||
* @param _params
|
||||
|
@ -256,10 +264,10 @@ const JTable = defineComponent<JTableProps>({
|
|||
/**
|
||||
* 导出方法
|
||||
*/
|
||||
expose({ reload })
|
||||
|
||||
expose({ reload })
|
||||
|
||||
return () => <Spin spinning={loading.value}>
|
||||
<div class={styles["jtable-body"]}>
|
||||
<div class={styles["jtable-body"]} style={{ ...props.bodyStyle }}>
|
||||
<div class={styles["jtable-body-header"]}>
|
||||
<div class={styles["jtable-body-header-left"]}>
|
||||
{/* 顶部左边插槽 */}
|
||||
|
@ -278,7 +286,7 @@ const JTable = defineComponent<JTableProps>({
|
|||
<div class={[styles["jtable-setting-item"], ModelEnum.TABLE === _model.value ? styles['active'] : '']} onClick={() => {
|
||||
_model.value = ModelEnum.TABLE
|
||||
}}>
|
||||
<UnorderedListOutlined />
|
||||
<UnorderedListOutlined />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
@ -288,66 +296,66 @@ const JTable = defineComponent<JTableProps>({
|
|||
<div class={styles['jtable-content']}>
|
||||
{
|
||||
props.alertRender && props?.rowSelection && props?.rowSelection?.selectedRowKeys && props.rowSelection.selectedRowKeys?.length ?
|
||||
<div class={styles['jtable-alert']}>
|
||||
<Alert
|
||||
message={'已选择' + props?.rowSelection?.selectedRowKeys?.length + '项'}
|
||||
type="info"
|
||||
onClose={() => {
|
||||
emit('cancelSelect')
|
||||
}}
|
||||
closeText={<a-button type="link">取消选择</a-button>}
|
||||
/>
|
||||
</div> : null
|
||||
<div class={styles['jtable-alert']}>
|
||||
<Alert
|
||||
message={'已选择' + props?.rowSelection?.selectedRowKeys?.length + '项'}
|
||||
type="info"
|
||||
onClose={() => {
|
||||
emit('cancelSelect')
|
||||
}}
|
||||
closeText={<a-button type="link">取消选择</a-button>}
|
||||
/>
|
||||
</div> : null
|
||||
}
|
||||
{
|
||||
_model.value === ModelEnum.CARD ?
|
||||
<div class={styles['jtable-card']}>
|
||||
{
|
||||
_dataSource.value.length ?
|
||||
<div
|
||||
class={styles['jtable-card-items']}
|
||||
style={{gridTemplateColumns: `repeat(${column.value}, 1fr)`}}
|
||||
>
|
||||
{
|
||||
_dataSource.value.map(item => slots.card ?
|
||||
<div class={[styles['jtable-card-item'], props.cardBodyClass]}>
|
||||
{slots.card(item)}
|
||||
</div> : null
|
||||
)
|
||||
}
|
||||
</div> :
|
||||
<div><Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /></div>
|
||||
}
|
||||
</div> :
|
||||
<div>
|
||||
<Table
|
||||
dataSource={_dataSource.value}
|
||||
columns={_columns.value}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
rowSelection={props.rowSelection}
|
||||
scroll={{x: 1366}}
|
||||
v-slots={{
|
||||
bodyCell: (dt: Record<string, any>) => {
|
||||
const {column, record} = dt;
|
||||
if((column?.key || column?.dataIndex) && column?.scopedSlots && (slots?.[column?.dataIndex] || slots?.[column?.key])) {
|
||||
const _key = column?.key || column?.dataIndex
|
||||
return slots?.[_key]!(record)
|
||||
} else {
|
||||
return record?.[column?.dataIndex] || ''
|
||||
<div class={styles['jtable-card']}>
|
||||
{
|
||||
_dataSource.value.length ?
|
||||
<div
|
||||
class={styles['jtable-card-items']}
|
||||
style={{ gridTemplateColumns: `repeat(${column.value}, 1fr)` }}
|
||||
>
|
||||
{
|
||||
_dataSource.value.map(item => slots.card ?
|
||||
<div class={[styles['jtable-card-item'], props.cardBodyClass]}>
|
||||
{slots.card(item)}
|
||||
</div> : null
|
||||
)
|
||||
}
|
||||
</div> :
|
||||
<div><Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /></div>
|
||||
}
|
||||
</div> :
|
||||
<div>
|
||||
<Table
|
||||
dataSource={_dataSource.value}
|
||||
columns={_columns.value}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
rowSelection={props.rowSelection}
|
||||
scroll={{ x: 1366 }}
|
||||
v-slots={{
|
||||
bodyCell: (dt: Record<string, any>) => {
|
||||
const { column, record } = dt;
|
||||
if ((column?.key || column?.dataIndex) && column?.scopedSlots && (slots?.[column?.dataIndex] || slots?.[column?.key])) {
|
||||
const _key = column?.key || column?.dataIndex
|
||||
return slots?.[_key]!(record)
|
||||
} else {
|
||||
return record?.[column?.dataIndex] || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{/* 分页 */}
|
||||
{
|
||||
(!!_dataSource.value.length) && !props.noPagination && props.type === 'PAGE' &&
|
||||
<div class={styles['jtable-pagination']}>
|
||||
<Pagination
|
||||
size="small"
|
||||
<Pagination
|
||||
size="small"
|
||||
total={total.value}
|
||||
showQuickJumper={false}
|
||||
showSizeChanger={true}
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
<template>
|
||||
<a-drawer placement="right" :closable="false" :visible="true">
|
||||
<template #title>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<span
|
||||
><AIcon
|
||||
type="CloseOutlined"
|
||||
style="margin-right: 5px"
|
||||
@click="onClose"
|
||||
/>编辑配置</span
|
||||
>
|
||||
<a-button type="primary" @click="saveBtn">保存</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<a-form layout="vertical" ref="formRef" :model="modelRef">
|
||||
<template v-for="(item, index) in props.config" :key="index">
|
||||
<a-form-item
|
||||
:name="item.property"
|
||||
v-for="i in item.properties"
|
||||
:key="i.property"
|
||||
>
|
||||
<template #label>
|
||||
<span style="margin-right: 5px">{{ i.name }}</span>
|
||||
<a-tooltip v-if="i.description" :title="i.description"
|
||||
><AIcon type="QuestionCircleOutlined"
|
||||
/></a-tooltip>
|
||||
</template>
|
||||
<ValueItem
|
||||
v-model:modelValue="modelRef[i.property]"
|
||||
:itemType="i.type.type"
|
||||
:options="
|
||||
i.type.type === 'enum'
|
||||
? (i.type?.elements || []).map((item) => {
|
||||
return {
|
||||
label: item?.text,
|
||||
value: item?.value,
|
||||
};
|
||||
})
|
||||
: undefined
|
||||
"
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { modify } from '@/api/device/instance';
|
||||
import { useInstanceStore } from '@/store/instance';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const emit = defineEmits(['close', 'save']);
|
||||
|
||||
const formRef = ref();
|
||||
const modelRef = reactive({});
|
||||
|
||||
const instanceStore = useInstanceStore();
|
||||
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Array,
|
||||
default: []
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const obj = instanceStore.current?.configuration
|
||||
if(obj && Object.keys(obj).length) {
|
||||
(props?.config || []).map((item: any) => {
|
||||
if(Array.isArray(item.properties) && item?.properties.length){
|
||||
item.properties.map((i: any) => {
|
||||
modelRef[i.property] = obj[i.property]
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
const onClose = () => {
|
||||
emit('close');
|
||||
formRef.value.resetFields();
|
||||
};
|
||||
|
||||
const saveBtn = () => {
|
||||
formRef.value.validate().then(async () => {
|
||||
const values = toRaw(modelRef);
|
||||
const resp = await modify(instanceStore.current?.id || '', {
|
||||
id: instanceStore.current?.id,
|
||||
configuration: { ...values }
|
||||
})
|
||||
if(resp.status === 200){
|
||||
message.success('操作成功!')
|
||||
emit('save');
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div style="margin-top: 20px" v-if="config.length">
|
||||
<div style="display: flex;">
|
||||
<div style="display: flex; margin-bottom: 20px; align-items: center;">
|
||||
<div style="font-size: 16px; font-weight: 700">配置</div>
|
||||
<a-space>
|
||||
<a-button type="link" @click="visible = true"><AIcon type="EditOutlined" />编辑</a-button>
|
||||
|
@ -13,11 +13,11 @@
|
|||
</a-space>
|
||||
</div>
|
||||
<a-descriptions bordered size="small" v-for="i in config" :key="i.name">
|
||||
<template #title><h4>{{i.name}}</h4></template>
|
||||
<template #title><h4 style="font-size: 15px">{{i.name}}</h4></template>
|
||||
<a-descriptions-item v-for="item in i.properties" :key="item.property">
|
||||
<template #label>
|
||||
<span style="margin-right: 5px">{{item.name}}</span>
|
||||
<a-tooltip v-if="item.description" :title="item.description"><AIcon type="QuestionCircleOutlined" /></a-tooltip>
|
||||
<span>{{item.name}}</span>
|
||||
</template>
|
||||
<span v-if="item.type.type === 'password' && instanceStore.current?.configuration?.[item.property]?.length > 0">******</span>
|
||||
<span v-else>
|
||||
|
@ -26,6 +26,7 @@
|
|||
</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<Save v-if="visible" @save="saveBtn" @close="visible = false" :config="config" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -34,6 +35,7 @@ import { useInstanceStore } from "@/store/instance"
|
|||
import { ConfigMetadata } from "@/views/device/Product/typings"
|
||||
import { getConfigMetadata, _deploy, configurationReset } from '@/api/device/instance'
|
||||
import { message } from "ant-design-vue"
|
||||
import Save from './Save.vue'
|
||||
|
||||
const instanceStore = useInstanceStore()
|
||||
const visible = ref<boolean>(false)
|
||||
|
@ -78,4 +80,11 @@ const resetBtn = async () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const saveBtn = () => {
|
||||
visible.value = false
|
||||
if(instanceStore.current.id){
|
||||
instanceStore.refresh(instanceStore.current.id)
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<Search :columns="columns" target="device-instance-running-events" />
|
||||
<JTable
|
||||
ref="eventsRef"
|
||||
:columns="columns"
|
||||
:dataSource="dataSource"
|
||||
model="TABLE"
|
||||
:bodyStyle="{padding: '0 24px'}"
|
||||
>
|
||||
<template #timestamp="slotProps">
|
||||
{{ moment(slotProps.timestamp).format('YYYY-MM-DD HH:mm:ss') }}
|
||||
</template>
|
||||
<template #action="slotProps">
|
||||
<a-button type="link" @click="detail(slotProps)">
|
||||
<AIcon type="SearchOutlined" />
|
||||
</a-button>
|
||||
</template>
|
||||
</JTable>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import moment from 'moment'
|
||||
const events = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
|
||||
const columns = ref<Record<string, any>>([
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
scopedSlots: true,
|
||||
}
|
||||
])
|
||||
|
||||
const dataSource = ref<Record<string, any>[]>([])
|
||||
|
||||
watchEffect(() => {
|
||||
if(events.data?.valueType?.type === 'object'){
|
||||
(events.data.valueType?.properties || []).map((i: any) => {
|
||||
columns.value.splice(0, 0, {
|
||||
key: i.id,
|
||||
title: i.name,
|
||||
dataIndex: `${i.id}_format`,
|
||||
// renderText: (text) => (typeof text === 'object' ? JSON.stringify(text) : text),
|
||||
})
|
||||
})
|
||||
} else {
|
||||
columns.value.splice(0, 0, {
|
||||
title: '数据',
|
||||
dataIndex: 'value',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const detail = () => {
|
||||
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,114 @@
|
|||
<template>
|
||||
<a-card :hoverable="true" class="card-box">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="card-container">
|
||||
<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-tooltip>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
<div class="value">
|
||||
<ValueRender :data="data" />
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div style="color: rgba(0, 0, 0, .65); font-size: 12px">更新时间</div>
|
||||
<div class="time-value">{{data?.time || '--'}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ValueRender from './ValueRender.vue'
|
||||
const _props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
const loading = ref<boolean>(true);
|
||||
|
||||
watchEffect(() => {
|
||||
if (_props.data.name) {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.card-box {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
width: 100%;
|
||||
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 154px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.title {
|
||||
width: 60%;
|
||||
margin-right: 10px;
|
||||
overflow: hidden;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
color: #323130;
|
||||
font-weight: 700;
|
||||
font-size: 24px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
.time-value {
|
||||
margin-top: 5px;
|
||||
font-size: 16px;
|
||||
min-height: 25px;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<div>
|
||||
{{data.value || '--'}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const _data = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,166 @@
|
|||
<template>
|
||||
<JTable
|
||||
ref="metadataRef"
|
||||
:columns="columns"
|
||||
:dataSource="dataSource"
|
||||
:bodyStyle="{padding: 0}"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<a-input-search
|
||||
placeholder="请输入名称"
|
||||
style="width: 300px; margin-bottom: 10px"
|
||||
@search="onSearch"
|
||||
/>
|
||||
</template>
|
||||
<template #card="slotProps">
|
||||
<PropertyCard :data="slotProps" />
|
||||
</template>
|
||||
<template #value="slotProps">
|
||||
<ValueRender :data="slotProps" />
|
||||
</template>
|
||||
<template #time="slotProps">
|
||||
{{slotProps.time || '--'}}
|
||||
</template>
|
||||
<template #action="slotProps">
|
||||
<a-space :size="16">
|
||||
<a-tooltip
|
||||
v-for="i in getActions(slotProps)"
|
||||
: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
|
||||
@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>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropertyData } from "../../../typings"
|
||||
import PropertyCard from './PropertyCard.vue'
|
||||
import ValueRender from './ValueRender.vue'
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '值',
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
scopedSlots: true
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'time',
|
||||
key: 'time',
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
scopedSlots: true,
|
||||
},
|
||||
]
|
||||
|
||||
const _data = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const dataSource = ref<PropertyData[]>([])
|
||||
|
||||
const getActions = (data: Partial<Record<string, any>>) => {
|
||||
const arr = []
|
||||
if(data.expands?.type?.includes('write')){
|
||||
arr.push({
|
||||
key: 'edit',
|
||||
tooltip: {
|
||||
title: '设置属性至设备',
|
||||
},
|
||||
icon: 'EditOutlined',
|
||||
onClick: () => {
|
||||
|
||||
},
|
||||
})
|
||||
}
|
||||
if((data.expands?.metrics || []).length && ['int', 'long', 'float', 'double', 'string', 'boolean', 'date'].includes(
|
||||
data.valueType?.type || '',
|
||||
)){
|
||||
arr.push({
|
||||
key: 'metrics',
|
||||
tooltip: {
|
||||
title: '指标',
|
||||
},
|
||||
icon: 'ClockCircleOutlined',
|
||||
onClick: () => {
|
||||
|
||||
},
|
||||
})
|
||||
}
|
||||
if(data.expands?.type?.includes('read')){
|
||||
arr.push({
|
||||
key: 'read',
|
||||
tooltip: {
|
||||
title: '获取最新属性值',
|
||||
},
|
||||
icon: 'SyncOutlined',
|
||||
onClick: () => {
|
||||
|
||||
},
|
||||
})
|
||||
}
|
||||
arr.push({
|
||||
key: 'detail',
|
||||
text: '详情',
|
||||
tooltip: {
|
||||
title: '详情',
|
||||
},
|
||||
icon: 'BarsOutlined',
|
||||
onClick: () => {
|
||||
|
||||
},
|
||||
})
|
||||
return arr
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
dataSource.value = _data.data as PropertyData[]
|
||||
})
|
||||
|
||||
const onSearch = () => {};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
|
@ -1,46 +1,120 @@
|
|||
<template>
|
||||
<a-card>
|
||||
<a-row type="flex">
|
||||
<a-col flex="200px">
|
||||
<div>
|
||||
<a-input-search
|
||||
v-model:value="value"
|
||||
placeholder="请输入事件名称"
|
||||
style="width: 200px; margin-bottom: 10px"
|
||||
@search="onSearch"
|
||||
<div class="property-box">
|
||||
<div class="property-box-left">
|
||||
<a-input-search
|
||||
v-model:value="value"
|
||||
placeholder="请输入事件名称"
|
||||
style="width: 200px; margin-bottom: 10px"
|
||||
@search="onSearch"
|
||||
:allowClear="true"
|
||||
/>
|
||||
<a-tabs
|
||||
tab-position="left"
|
||||
style="height: 600px"
|
||||
v-model:activeKey="activeKey"
|
||||
:tabBarStyle="{ width: '200px' }"
|
||||
@change="tabChange"
|
||||
>
|
||||
<a-tab-pane
|
||||
v-for="i in tabList"
|
||||
:key="i.key"
|
||||
:tab="i.tab"
|
||||
/>
|
||||
<a-tabs
|
||||
tab-position="left"
|
||||
:style="{ height: '600px' }"
|
||||
v-model:activeKey="activeKey"
|
||||
tabBarStyle="width: 200px"
|
||||
>
|
||||
<a-tab-pane v-for="i in tabList" :key="i.key" :tab="i.tab" />
|
||||
</a-tabs>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col flex="auto">
|
||||
<!-- <component :is="tabs[activeKey]" /> -->
|
||||
123
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tabs>
|
||||
</div>
|
||||
<div class="property-box-right">
|
||||
<Event v-if="type === 'event'" :data="data" />
|
||||
<Property v-else :data="properties" />
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const activeKey = ref<string>('property')
|
||||
const tabList = ref<{key: string, tab: string}[]>([
|
||||
{
|
||||
key: 'property',
|
||||
tab: '属性'
|
||||
},
|
||||
{
|
||||
key: 'event1',
|
||||
tab: '事件1'
|
||||
import { useInstanceStore } from '@/store/instance';
|
||||
import _ from 'lodash';
|
||||
import Event from './Event/index.vue';
|
||||
import Property from './Property/index.vue';
|
||||
|
||||
const activeKey = ref<string>('property');
|
||||
const tabList = ref<{ key: string; tab: string; type: 'property' | 'event' }[]>(
|
||||
[
|
||||
{
|
||||
key: 'property',
|
||||
tab: '属性',
|
||||
type: 'property',
|
||||
},
|
||||
],
|
||||
);
|
||||
const type = ref<string>('property');
|
||||
const data = ref<Record<string, any>>({})
|
||||
const value = ref<string>('');
|
||||
const instanceStore = useInstanceStore()
|
||||
const metadata = JSON.parse(instanceStore.current?.metadata || '{}')
|
||||
const properties = metadata.properties
|
||||
const events = metadata.events
|
||||
|
||||
watch(() => events, (newVal) => {
|
||||
if(events && newVal.length){
|
||||
newVal.map((item: any) => {
|
||||
tabList.value.push({
|
||||
...item,
|
||||
key: item.id,
|
||||
tab: item.name,
|
||||
type: 'event',
|
||||
})
|
||||
})
|
||||
}
|
||||
])
|
||||
}, {
|
||||
deep: true,
|
||||
immediate: true
|
||||
})
|
||||
|
||||
const onSearch = () => {
|
||||
const arr = [
|
||||
{
|
||||
key: 'property',
|
||||
tab: '属性',
|
||||
type: 'property',
|
||||
},
|
||||
...events.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
key: item.id,
|
||||
tab: item.name,
|
||||
type: 'event',
|
||||
}
|
||||
})
|
||||
]
|
||||
if(value.value){
|
||||
const li = arr.filter((i: any) => {
|
||||
return i?.tab.indexOf(value.value) !== -1;
|
||||
})
|
||||
tabList.value = _.cloneDeep(li)
|
||||
} else {
|
||||
tabList.value = _.cloneDeep(arr)
|
||||
}
|
||||
};
|
||||
const tabChange = (key: string) => {
|
||||
const dt = tabList.value.find((i) => i.key === key);
|
||||
if (dt) {
|
||||
data.value = dt
|
||||
type.value = dt.type;
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.property-box {
|
||||
display: flex;
|
||||
.property-box-left {
|
||||
width: 200px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.property-box-right {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</style>
|
|
@ -117,11 +117,14 @@ const objToParams = (source: object): string => {
|
|||
}
|
||||
|
||||
.box-list {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
display: grid;
|
||||
grid-column-gap: 66px;
|
||||
// grid-template-columns: repeat(5, 1fr);
|
||||
// display: grid;
|
||||
// grid-column-gap: 66px;
|
||||
display: flex;
|
||||
gap: 66px;
|
||||
|
||||
.list-item {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
.box-top {
|
||||
position: relative;
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div class="page-container"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,94 @@
|
|||
<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">
|
||||
<a-badge :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 '@/views/media/DashBoard/typings';
|
||||
|
||||
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;
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.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;
|
||||
|
||||
.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,93 @@
|
|||
<template>
|
||||
<div class="page-container">
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="6">
|
||||
<TopCard
|
||||
title="设备数量"
|
||||
:img="getImage('/media/dashboard-1.png')"
|
||||
:footer="deviceFooter"
|
||||
:value="deviceTotal"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<TopCard
|
||||
title="通道数量"
|
||||
:img="getImage('/media/dashboard-2.png')"
|
||||
:footer="channelFooter"
|
||||
:value="channelTotal"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<TopCard
|
||||
title="录像数量"
|
||||
:img="getImage('/media/dashboard-3.png')"
|
||||
:footer="aggFooter"
|
||||
:value="aggTotal"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<TopCard
|
||||
title="播放中数量"
|
||||
tooltip="当前正在播放的通道数量之和"
|
||||
:img="getImage('/media/dashboard-4.png')"
|
||||
:footer="aggPlayingFooter"
|
||||
:value="aggPlayingTotal"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TopCard from '@/views/media/DashBoard/components/TopCard.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';
|
||||
|
||||
// 设备
|
||||
const deviceFooter = ref<Footer[]>([]);
|
||||
const deviceTotal = ref(0);
|
||||
const getDeviceData = () => {
|
||||
homeApi.deviceCount().then((res) => {
|
||||
deviceTotal.value = res.result;
|
||||
});
|
||||
};
|
||||
getDeviceData();
|
||||
|
||||
// 通道
|
||||
const channelFooter = ref<Footer[]>([]);
|
||||
const channelTotal = ref(0);
|
||||
const getChannelData = () => {
|
||||
homeApi.channelCount().then((res) => {
|
||||
channelTotal.value = res.result;
|
||||
});
|
||||
};
|
||||
getChannelData();
|
||||
|
||||
// 录像
|
||||
const aggFooter = ref<Footer[]>([]);
|
||||
const aggTotal = ref(0);
|
||||
const getAggData = () => {
|
||||
dashboardApi.agg().then((res) => {
|
||||
aggTotal.value = res.result.total;
|
||||
});
|
||||
};
|
||||
getAggData();
|
||||
|
||||
// 播放中
|
||||
const aggPlayingFooter = ref<Footer[]>([]);
|
||||
const aggPlayingTotal = ref(0);
|
||||
const getAggPlayingData = () => {
|
||||
dashboardApi.aggPlaying().then((res) => {
|
||||
aggTotal.value = res.result.playingTotal;
|
||||
});
|
||||
};
|
||||
getAggPlayingData();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.page-container {
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,16 @@
|
|||
export type Agg = {
|
||||
duration: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type AggPlaying = {
|
||||
playerTotal: number
|
||||
playingTotal: number
|
||||
}
|
||||
|
||||
export type Footer = {
|
||||
title: string;
|
||||
value: number;
|
||||
status?: "default" | "error" | "success" | "warning" | "processing"
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
<template>
|
||||
<a-card class="device-count-container">
|
||||
<template #title>
|
||||
<h5 class="title">基础统计</h5>
|
||||
</template>
|
||||
<template #extra>
|
||||
<span style="color: #1d39c4; cursor: pointer" @click="jumpPage"
|
||||
>详情</span
|
||||
>
|
||||
</template>
|
||||
|
||||
<div class="box-list">
|
||||
<div class="box-item">
|
||||
<div class="label">设备数量</div>
|
||||
<div class="value">{{ deviceCount }}</div>
|
||||
<img :src="getImage('/home/top-1.png')" alt="" />
|
||||
</div>
|
||||
<div class="box-item">
|
||||
<div class="label">通道数量</div>
|
||||
<div class="value">{{ channelCount }}</div>
|
||||
<img :src="getImage('/home/top-2.png')" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import homeApi from '@/api/media/home';
|
||||
import { getImage } from '@/utils/comm';
|
||||
|
||||
const channelCount = ref(0);
|
||||
const deviceCount = ref(0);
|
||||
|
||||
onMounted(() => {
|
||||
getData();
|
||||
});
|
||||
|
||||
const getData = () => {
|
||||
homeApi.deviceCount().then((resp) => {
|
||||
deviceCount.value = resp.result;
|
||||
});
|
||||
homeApi.channelCount().then((resp) => {
|
||||
channelCount.value = resp.result;
|
||||
});
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const jumpPage = () => {
|
||||
router.push('/media/dashboard');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.device-count-container {
|
||||
:deep(.ant-card-body) {
|
||||
padding-top: 0;
|
||||
}
|
||||
.title {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 18px;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #1d39c4;
|
||||
border: 1px solid #b4c0da;
|
||||
transform: translateY(-50%);
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
|
||||
.box-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-gap: 24px;
|
||||
gap: 24px;
|
||||
|
||||
.box-item {
|
||||
position: relative;
|
||||
padding: 16px;
|
||||
background: linear-gradient(
|
||||
135.62deg,
|
||||
#f6f7fd 22.27%,
|
||||
hsla(0, 0%, 100%, 0.86) 91.82%
|
||||
);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 4px 18px #efefef;
|
||||
|
||||
.label {
|
||||
color: #4f4f4f;
|
||||
}
|
||||
.value {
|
||||
margin: 20px 0;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
right: 10%;
|
||||
bottom: 0;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,104 @@
|
|||
<template>
|
||||
<div class="page-container">
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="14">
|
||||
<BootCard
|
||||
:cardData="deviceBootConfig"
|
||||
cardTitle="视频中心引导"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="10">
|
||||
<BasicCountCard />
|
||||
</a-col>
|
||||
<a-col :span="24" style="margin: 20px 0">
|
||||
<PlatformPicCard />
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<StepCard
|
||||
cardTitle="设备接入推荐步骤"
|
||||
tooltip="不同的设备因为通信协议的不同,存在接入步骤的差异"
|
||||
:dataList="deviceStepDetails"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BootCard from '@/views/home/components/BootCard.vue';
|
||||
import PlatformPicCard from '@/views/home/components/PlatformPicCard.vue';
|
||||
import StepCard from '@/views/home/components/StepCard.vue';
|
||||
import BasicCountCard from '@/views/media/Home/components/BasicCountCard.vue';
|
||||
|
||||
import { usePermissionStore } from '@/store/permission';
|
||||
import type { bootConfig, recommendList } from '@/views/home/index';
|
||||
|
||||
// 权限控制
|
||||
const hasPermission = usePermissionStore().hasPermission;
|
||||
|
||||
const deviceBootConfig: bootConfig[] = [
|
||||
{
|
||||
english: 'STEP1',
|
||||
label: '添加视频设备',
|
||||
link: '/media/device/Save',
|
||||
auth: hasPermission('/media/device/Save'),
|
||||
params: {
|
||||
save: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
english: 'STEP2',
|
||||
label: '分屏展示',
|
||||
link: '/media/SplitScreen',
|
||||
auth: hasPermission('/media/SplitScreen'),
|
||||
params: {
|
||||
save: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
english: 'STEP3',
|
||||
label: '国标级联',
|
||||
link: '/media/Cascade',
|
||||
auth: hasPermission('/media/Cascade'),
|
||||
params: {
|
||||
save: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
const deviceStepDetails: recommendList[] = [
|
||||
{
|
||||
title: '添加视频设备',
|
||||
details: '根据视频设备的传输协议,在已创建的产品下添加对应的设备。',
|
||||
iconUrl: '/images/home/bottom-6.png',
|
||||
linkUrl: '/media/device/Save',
|
||||
auth: hasPermission('/media/device/Save'),
|
||||
params: {
|
||||
save: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '查看通道',
|
||||
details: '查看设备下的通道数据,可以进行直播、录制等操作。',
|
||||
iconUrl: '/images/home/bottom-7.png',
|
||||
linkUrl: '/media/device/Channel',
|
||||
auth: hasPermission('/media/device/Save'),
|
||||
dialogTag: 'accessMethod',
|
||||
},
|
||||
{
|
||||
title: '分屏展示',
|
||||
details: '对多个通道的视频流数据进行分屏展示。',
|
||||
iconUrl: '/images/home/bottom-8.png',
|
||||
linkUrl: '/media/SplitScreen',
|
||||
auth: hasPermission('/media/SplitScreen'),
|
||||
params: {
|
||||
save: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.page-container {
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,24 @@
|
|||
type BaseItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
type State = {
|
||||
value: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type DeviceItem = {
|
||||
photoUrl?: string;
|
||||
channelNumber: number;
|
||||
createTime: number;
|
||||
firmware: string;
|
||||
gatewayId: string;
|
||||
host: string;
|
||||
manufacturer: string;
|
||||
model: string;
|
||||
port: number;
|
||||
provider: string;
|
||||
state: State;
|
||||
streamMode: string;
|
||||
transport: string;
|
||||
} & BaseItem;
|
|
@ -47,6 +47,7 @@
|
|||
<template #img>
|
||||
<slot name="img">
|
||||
<img
|
||||
style="width: 80px; height: 80px"
|
||||
:src="
|
||||
getLogo(slotProps.type, slotProps.provider)
|
||||
"
|
||||
|
|
Loading…
Reference in New Issue