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

This commit is contained in:
easy 2023-02-01 18:11:27 +08:00
commit 58c6d1aaf3
24 changed files with 1224 additions and 134 deletions

View File

@ -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

View File

@ -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`),
}

9
src/api/media/home.ts Normal file
View File

@ -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`),
}

View File

@ -45,6 +45,7 @@ const iconKeys = [
'InfoCircleOutlined',
'SearchOutlined',
'EllipsisOutlined',
'ClockCircleOutlined'
]
const Icon = (props: {type: string}) => {

View File

@ -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;

View File

@ -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;

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,14 @@
<template>
<div>
{{data.value || '--'}}
</div>
</template>
<script lang="ts" setup>
const _data = defineProps({
data: {
type: Object,
default: () => {},
},
});
</script>

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -0,0 +1,7 @@
<template>
<div class="page-container"></div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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>

16
src/views/media/DashBoard/typings.d.ts vendored Normal file
View File

@ -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"
}

View File

@ -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>

View File

@ -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>

24
src/views/media/Home/typings.d.ts vendored Normal file
View File

@ -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;

View File

@ -47,6 +47,7 @@
<template #img>
<slot name="img">
<img
style="width: 80px; height: 80px"
:src="
getLogo(slotProps.type, slotProps.provider)
"