Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
xieyonghong 2023-02-01 18:21:44 +08:00
commit 00d57cc44a
61 changed files with 2699 additions and 500 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) 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 * @param id id

7
src/api/link/log.ts Normal file
View File

@ -0,0 +1,7 @@
import server from '@/utils/request';
export const queryAccess = (data: object) =>
server.post(`/logger/access/_query`, data);
export const querySystem = (data: object) =>
server.post(`/logger/system/_query`, data);

10
src/api/link/protocol.ts Normal file
View File

@ -0,0 +1,10 @@
import server from '@/utils/request';
export const detail = (id: string) => server.get(`/gateway/device/${id}`);
export const save = (data: Object) => server.post(`/gateway/device`, data);
export const list = (data: Object) =>
server.post(`/protocol/_query`, data);
export const remove = (id: string) => server.remove(`/gateway/device/${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

@ -15,3 +15,5 @@ export const getMenuInfo_api = (id:string) => server.get(`/menu/${id}`);
export const saveMenuInfo_api = (data: object) => server.patch(`/menu`, data); export const saveMenuInfo_api = (data: object) => server.patch(`/menu`, data);
// 新增菜单信息 // 新增菜单信息
export const addMenuInfo_api = (data: object) => server.post(`/menu`, data); export const addMenuInfo_api = (data: object) => server.post(`/menu`, data);
// 删除菜单信息
export const delMenuInfo_api = (id: string) => server.remove(`/menu/${id}`);

View File

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

View File

@ -2,22 +2,21 @@
<div class="card"> <div class="card">
<div <div
class="card-warp" class="card-warp"
:class="{ active: active ? 'active' : ''}" :class="{ active: active ? 'active' : '' }"
@click="handleClick" @click="handleClick"
> >
<div class="card-content"> <div class="card-content">
<a-row> <div style="display: flex">
<a-col :span="6"> <!-- 图片 -->
<!-- 图片 --> <div class="card-item-avatar">
<div class="card-item-avatar"> <slot name="img"> </slot>
<slot name="img"> </slot> </div>
</div>
</a-col> <!-- 内容 -->
<a-col :span="18"> <div class="card-item-body">
<!-- 内容 -->
<slot name="content"></slot> <slot name="content"></slot>
</a-col> </div>
</a-row> </div>
<!-- 勾选 --> <!-- 勾选 -->
<div v-if="active" class="checked-icon"> <div v-if="active" class="checked-icon">
@ -201,6 +200,13 @@ const handleClick = () => {
margin-right: 16px; margin-right: 16px;
} }
.card-item-body {
display: flex;
flex-direction: column;
flex-grow: 1;
width: 0;
}
.card-state { .card-state {
position: absolute; position: absolute;
top: 30px; top: 30px;

View File

@ -1,9 +1,9 @@
.jtable-body { .jtable-body {
width: 100%; width: 100%;
padding: 0 24px 24px; padding: 16px 24px 24px;
background-color: white; background-color: white;
.jtable-body-header { .jtable-body-header {
padding: 16px 0; padding-bottom: 16px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;

View File

@ -40,11 +40,11 @@ export interface ActionsType {
children?: ActionsType[]; children?: ActionsType[];
} }
export interface JColumnProps extends ColumnProps{ export interface JColumnProps extends ColumnProps {
scopedSlots?: boolean; // 是否为插槽 true: 是 false: 否 scopedSlots?: boolean; // 是否为插槽 true: 是 false: 否
} }
export interface JTableProps extends TableProps{ export interface JTableProps extends TableProps {
request?: (params?: Record<string, any>) => Promise<Partial<RequestData>>; request?: (params?: Record<string, any>) => Promise<Partial<RequestData>>;
cardBodyClass?: string; cardBodyClass?: string;
columns: JColumnProps[]; columns: JColumnProps[];
@ -53,8 +53,8 @@ export interface JTableProps extends TableProps{
// actions?: ActionsType[]; // actions?: ActionsType[];
noPagination?: boolean; noPagination?: boolean;
rowSelection?: TableProps['rowSelection']; rowSelection?: TableProps['rowSelection'];
cardProps?: Record<string, any>; cardProps?: Record<string, any>;
dataSource?: Record<string, any>[]; dataSource?: Record<string, any>[];
gridColumn?: number; gridColumn?: number;
/** /**
* *
@ -62,10 +62,11 @@ export interface JTableProps extends TableProps{
* gridColumns[1] 1440 ~ 1600 * gridColumns[1] 1440 ~ 1600
* gridColumns[2] > 1600 * gridColumns[2] > 1600
*/ */
gridColumns?: number[]; gridColumns?: number[];
alertRender?: boolean; alertRender?: boolean;
type?: keyof typeof TypeEnum; type?: keyof typeof TypeEnum;
defaultParams?: Record<string, any>; defaultParams?: Record<string, any>;
bodyStyle?: Record<string, any>;
} }
const JTable = defineComponent<JTableProps>({ const JTable = defineComponent<JTableProps>({
@ -88,13 +89,17 @@ const JTable = defineComponent<JTableProps>({
type: String, type: String,
default: '' default: ''
}, },
bodyStyle: {
type: Object,
default: {}
},
columns: { columns: {
type: Array, type: Array,
default: () => [] default: () => []
}, },
params: { params: {
type: Object, type: Object,
default: () => {} default: () => { }
}, },
model: { model: {
type: [String, undefined], type: [String, undefined],
@ -142,7 +147,7 @@ const JTable = defineComponent<JTableProps>({
} }
} }
} as any, } as any,
setup(props: JTableProps ,{ slots, emit, expose }){ setup(props: JTableProps, { slots, emit, expose }) {
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE
const _model = ref<keyof typeof ModelEnum>(props.model ? props.model : ModelEnum.CARD); // 模式切换 const _model = ref<keyof typeof ModelEnum>(props.model ? props.model : ModelEnum.CARD); // 模式切换
const column = ref<number>(props.gridColumn || 4); const column = ref<number>(props.gridColumn || 4);
@ -174,7 +179,7 @@ const JTable = defineComponent<JTableProps>({
*/ */
const handleSearch = async (_params?: Record<string, any>) => { const handleSearch = async (_params?: Record<string, any>) => {
loading.value = true loading.value = true
if(props.request) { if (props.request) {
const resp = await props.request({ const resp = await props.request({
pageIndex: 0, pageIndex: 0,
pageSize: 12, pageSize: 12,
@ -185,14 +190,14 @@ const JTable = defineComponent<JTableProps>({
...(_params?.terms || []) ...(_params?.terms || [])
] ]
}) })
if(resp.status === 200){ if (resp.status === 200) {
if(props.type === 'PAGE'){ 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({ handleSearch({
..._params, ..._params,
pageSize: pageSize.value, pageSize: pageSize.value,
pageIndex: pageIndex.value > 0 ? pageIndex.value - 1 : 0, pageIndex: pageIndex.value > 0 ? pageIndex.value - 1 : 0,
}) })
} else { } else {
_dataSource.value = resp.result?.data || [] _dataSource.value = resp.result?.data || []
@ -204,10 +209,9 @@ const JTable = defineComponent<JTableProps>({
_dataSource.value = resp?.result || [] _dataSource.value = resp?.result || []
} }
} else { } else {
_dataSource.value = [] _dataSource.value = []
} }
} else { } else {
console.log(props?.dataSource)
_dataSource.value = props?.dataSource || [] _dataSource.value = props?.dataSource || []
} }
loading.value = false loading.value = false
@ -218,17 +222,17 @@ const JTable = defineComponent<JTableProps>({
(newValue) => { (newValue) => {
handleSearch(newValue) handleSearch(newValue)
}, },
{deep: true, immediate: true} { deep: true, immediate: true }
) )
watch( watch(
() => props.dataSource, () => props.dataSource,
(newValue) => { () => {
if(props.dataSource){ if (props.dataSource && !props.request) {
handleSearch(props.params) handleSearch(props.params)
} }
}, },
{deep: true, immediate: true} { deep: true, immediate: true }
) )
onMounted(() => { onMounted(() => {
@ -241,6 +245,10 @@ const JTable = defineComponent<JTableProps>({
window.onresize = null window.onresize = null
}) })
watchEffect(() => {
// console.log(props.bodyStyle)
})
/** /**
* *
* @param _params * @param _params
@ -256,10 +264,10 @@ const JTable = defineComponent<JTableProps>({
/** /**
* *
*/ */
expose({ reload }) expose({ reload })
return () => <Spin spinning={loading.value}> 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"]}>
<div class={styles["jtable-body-header-left"]}> <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={() => { <div class={[styles["jtable-setting-item"], ModelEnum.TABLE === _model.value ? styles['active'] : '']} onClick={() => {
_model.value = ModelEnum.TABLE _model.value = ModelEnum.TABLE
}}> }}>
<UnorderedListOutlined /> <UnorderedListOutlined />
</div> </div>
</div> </div>
} }
@ -288,58 +296,58 @@ const JTable = defineComponent<JTableProps>({
<div class={styles['jtable-content']}> <div class={styles['jtable-content']}>
{ {
props.alertRender && props?.rowSelection && props?.rowSelection?.selectedRowKeys && props.rowSelection.selectedRowKeys?.length ? props.alertRender && props?.rowSelection && props?.rowSelection?.selectedRowKeys && props.rowSelection.selectedRowKeys?.length ?
<div class={styles['jtable-alert']}> <div class={styles['jtable-alert']}>
<Alert <Alert
message={'已选择' + props?.rowSelection?.selectedRowKeys?.length + '项'} message={'已选择' + props?.rowSelection?.selectedRowKeys?.length + '项'}
type="info" type="info"
onClose={() => { onClose={() => {
emit('cancelSelect') emit('cancelSelect')
}} }}
closeText={<a-button type="link"></a-button>} closeText={<a-button type="link"></a-button>}
/> />
</div> : null </div> : null
} }
{ {
_model.value === ModelEnum.CARD ? _model.value === ModelEnum.CARD ?
<div class={styles['jtable-card']}> <div class={styles['jtable-card']}>
{ {
_dataSource.value.length ? _dataSource.value.length ?
<div <div
class={styles['jtable-card-items']} class={styles['jtable-card-items']}
style={{gridTemplateColumns: `repeat(${column.value}, 1fr)`}} style={{ gridTemplateColumns: `repeat(${column.value}, 1fr)` }}
> >
{ {
_dataSource.value.map(item => slots.card ? _dataSource.value.map(item => slots.card ?
<div class={[styles['jtable-card-item'], props.cardBodyClass]}> <div class={[styles['jtable-card-item'], props.cardBodyClass]}>
{slots.card(item)} {slots.card(item)}
</div> : null </div> : null
) )
} }
</div> : </div> :
<div><Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /></div> <div><Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /></div>
} }
</div> : </div> :
<div> <div>
<Table <Table
dataSource={_dataSource.value} dataSource={_dataSource.value}
columns={_columns.value} columns={_columns.value}
pagination={false} pagination={false}
rowKey="id" rowKey="id"
rowSelection={props.rowSelection} rowSelection={props.rowSelection}
scroll={{x: 1366}} scroll={{ x: 1366 }}
v-slots={{ v-slots={{
bodyCell: (dt: Record<string, any>) => { bodyCell: (dt: Record<string, any>) => {
const {column, record} = dt; const { column, record } = dt;
if((column?.key || column?.dataIndex) && column?.scopedSlots && (slots?.[column?.dataIndex] || slots?.[column?.key])) { if ((column?.key || column?.dataIndex) && column?.scopedSlots && (slots?.[column?.dataIndex] || slots?.[column?.key])) {
const _key = column?.key || column?.dataIndex const _key = column?.key || column?.dataIndex
return slots?.[_key]!(record) return slots?.[_key]!(record)
} else { } else {
return record?.[column?.dataIndex] || '' return record?.[column?.dataIndex] || ''
}
} }
} }}
}} />
/> </div>
</div>
} }
</div> </div>
{/* 分页 */} {/* 分页 */}

View File

@ -76,3 +76,23 @@ export function getSlotVNode<T>(slots: Slots, props: Record<string, unknown>, pr
} }
return (props[prop] || slots[prop]?.()) as T; return (props[prop] || slots[prop]?.()) as T;
} }
/**
* Select参数column的值
* @param e // 查询参数 e
* @param column {Object} {需要修改的值: 修改后的值}
* {
username: 'context.username',
}
*/
export const modifySearchColumnValue = (e: any, column: object) => {
e.terms.forEach((item: any) => {
item.terms.forEach((t: any) => {
if (column[t.column]) {
t.column = column[t.column];
}
});
});
return e;
};

View File

@ -0,0 +1,255 @@
<template>
<div>
<Search :columns="columns" target="search" @search="handleSearch" />
<JTable
ref="tableRef"
model="TABLE"
:columns="columns"
:request="queryAccess"
:defaultParams="{
sorts: [{ name: 'responseTime', order: 'desc' }],
}"
:params="params"
>
<template #requestTime="slotProps">
{{
moment(slotProps.requestTime).format('YYYY-MM-DD HH:mm:ss')
}}
</template>
<template #responseTime="slotProps">
<a-tag color="purple">
{{ slotProps.responseTime - slotProps.requestTime }} ms
</a-tag>
</template>
<template #username="slotProps">
<a-tag color="geekblue">
{{ slotProps.context.userName }}
</a-tag>
</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">
<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>
</div>
<a-modal
:width="1100"
v-model:visible="visible"
title="详情"
@ok="handleOk"
>
<a-descriptions :data="descriptionsData" title="" bordered :column="2">
<a-descriptions-item label="URL">
{{ descriptionsData?.url }}
</a-descriptions-item>
<a-descriptions-item label="请求方法">
{{ descriptionsData?.httpMethod }}
</a-descriptions-item>
<a-descriptions-item label="动作">
{{ descriptionsData?.action }}
</a-descriptions-item>
<a-descriptions-item label="类名">
{{ descriptionsData?.target }}
</a-descriptions-item>
<a-descriptions-item label="方法名">
{{ descriptionsData?.method }}
</a-descriptions-item>
<a-descriptions-item label="IP">
{{ descriptionsData?.ip }}
</a-descriptions-item>
<a-descriptions-item label="请求时间">
{{
moment(descriptionsData?.requestTime).format(
'YYYY-MM-DD HH:mm:ss',
)
}}
</a-descriptions-item>
<a-descriptions-item label="请求耗时">
{{
descriptionsData?.responseTime -
descriptionsData?.requestTime +
'ms'
}}
</a-descriptions-item>
<a-descriptions-item label="请求头" :span="2">
{{ descriptionsData?.httpHeaders }}
</a-descriptions-item>
<a-descriptions-item label="请求参数" :span="2">
{{ descriptionsData?.parameters }}
</a-descriptions-item>
<a-descriptions-item label="异常信息" :span="2">
<a-textarea
v-model:value="descriptionsData.exception"
placeholder="暂无数据"
:auto-size="{ minRows: 3, maxRows: 20 }"
/>
</a-descriptions-item>
</a-descriptions>
</a-modal>
</template>
<script lang="ts" setup name="AccessLog">
import type { ActionsType } from '@/components/Table/index.vue';
import type { AccessLogItem } from '../typings';
import { queryAccess } from '@/api/link/log';
import moment from 'moment';
import { modifySearchColumnValue } from '@/utils/comm';
const tableRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const columns = [
{
title: 'IP',
dataIndex: 'ip',
key: 'ip',
search: {
type: 'string',
},
scopedSlots: true,
width: 150,
fixed: 'left',
},
{
title: '请求路径',
dataIndex: 'url',
key: 'url',
search: {
type: 'string',
},
// width: 200,
},
{
title: '请求方法',
dataIndex: 'httpMethod',
key: 'httpMethod',
search: {
type: 'select',
options: [
{
label: 'POST',
value: 'POST',
},
{
label: 'GET',
value: 'GET',
},
{
label: 'PATCH',
value: 'PATCH',
},
{
label: 'DELETE',
value: 'DELETE',
},
{
label: 'PUT',
value: 'PUT',
},
],
},
scopedSlots: true,
width: 100,
},
{
title: '请求时间',
dataIndex: 'requestTime',
key: 'requestTime',
scopedSlots: true,
search: {
type: 'date',
},
width: 200,
},
{
title: '请求耗时',
dataIndex: 'responseTime',
key: 'responseTime',
scopedSlots: true,
width: 100,
},
{
title: '请求用户',
dataIndex: 'username',
key: 'username',
search: {
type: 'string',
},
width: 150,
scopedSlots: true,
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 150,
scopedSlots: true,
},
];
const descriptionsData = ref<AccessLogItem>();
const visible = ref<boolean>(false);
const handleOk = (e: MouseEvent) => {
visible.value = false;
};
const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
if (!data) {
return [];
}
return [
{
key: 'eye',
text: '查看',
tooltip: {
title: '查看',
},
icon: 'EyeOutlined',
onClick: (data: AccessLogItem) => {
descriptionsData.value = data;
visible.value = true;
},
},
];
};
const column = {
username: 'context.username',
};
/**
* 搜索
* @param params
*/
const handleSearch = (e: any) => {
params.value = modifySearchColumnValue(e, column);
};
</script>

View File

@ -0,0 +1,246 @@
<template>
<div>
<Search :columns="columns" target="search" @search="handleSearch" />
<JTable
ref="tableRef"
model="TABLE"
:columns="columns"
:request="querySystem"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
:params="params"
>
<template #level="slotProps">
<a-tag
:color="
slotProps.level === 'WARN'
? 'orange'
: slotProps.level === 'ERROR'
? 'red'
: slotProps.level === 'DEBUG'
? 'blue'
: 'green'
"
>
{{ slotProps.level }}
</a-tag>
</template>
<template #createTime="slotProps">
{{ moment(slotProps.createTime).format('YYYY-MM-DD HH:mm:ss') }}
</template>
<template #server="slotProps">
{{ slotProps.context.server }}
</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">
<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>
</div>
<a-modal
:width="1100"
v-model:visible="visible"
title="详情"
@ok="handleOk"
>
<div>
<span class="mr-10">[{{ descriptionsData?.threadName }}]</span>
<span class="mr-10">{{
moment(descriptionsData?.createTime).format(
'YYYY-MM-DD HH:mm:ss',
)
}}</span>
<span>{{ descriptionsData?.className }}</span>
</div>
<div class="mb-10">
<a-tag
:color="
descriptionsData?.level === 'WARN'
? 'orange'
: descriptionsData?.level === 'ERROR'
? 'red'
: descriptionsData?.level === 'DEBUG'
? 'blue'
: 'green'
"
>
{{ descriptionsData?.level }}
</a-tag>
<span>{{ descriptionsData?.message }}</span>
</div>
<a-textarea
v-model:value="descriptionsData.exceptionStack"
placeholder="暂无数据"
:auto-size="{ minRows: 24, maxRows: 28 }"
/>
</a-modal>
</template>
<script lang="ts" setup name="SystemLog">
import type { ActionsType } from '@/components/Table/index.vue';
import type { SystemLogItem } from '../typings';
import { querySystem } from '@/api/link/log';
import moment from 'moment';
import { modifySearchColumnValue } from '@/utils/comm';
const tableRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
search: {
type: 'string',
},
scopedSlots: true,
width: 400,
fixed: 'left',
ellipsis: true,
},
{
title: '日志级别',
dataIndex: 'level',
key: 'level',
search: {
type: 'select',
options: [
{
label: 'ERROR',
value: 'ERROR',
},
{
label: 'INFO',
value: 'INFO',
},
{
label: 'DEBUG',
value: 'DEBUG',
},
{
label: 'WARN',
value: 'WARN',
},
],
},
scopedSlots: true,
width: 100,
},
{
title: '日志内容',
dataIndex: 'message',
key: 'message',
search: {
type: 'string',
},
scopedSlots: true,
ellipsis: true,
},
{
title: '服务名',
dataIndex: 'server',
key: 'server',
scopedSlots: true,
search: {
type: 'string',
},
width: 200,
ellipsis: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
search: {
type: 'date',
},
scopedSlots: true,
width: 200,
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 150,
scopedSlots: true,
},
];
const descriptionsData = ref<SystemLogItem>();
const visible = ref<boolean>(false);
const handleOk = (e: MouseEvent) => {
visible.value = false;
};
const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
if (!data) {
return [];
}
return [
{
key: 'eye',
text: '查看',
tooltip: {
title: '查看',
},
icon: 'EyeOutlined',
onClick: (data: SystemLogItem) => {
descriptionsData.value = data;
visible.value = true;
},
},
];
};
const column = {
server: 'context.server',
};
/**
* 搜索
* @param params
*/
const handleSearch = (e: any) => {
params.value = modifySearchColumnValue(e, column);
};
</script>
<style scoped lang="less">
.mr-10 {
margin-right: 10px;
}
.mb-10 {
margin-bottom: 10px;
}
</style>

28
src/views/Log/index.vue Normal file
View File

@ -0,0 +1,28 @@
<template>
<page-container :tabList="list" @tabChange="onTabChange">
<AccessLog v-if="activeKey === '1'" />
<SystemLog v-else />
</page-container>
</template>
<script lang="ts" setup name="LogPage">
import { defineComponent, ref } from 'vue';
import AccessLog from './Access/index.vue';
import SystemLog from './System/index.vue';
const activeKey = ref('1');
const list = [
{
key: '1',
tab: '访问日志',
},
{
key: '2',
tab: '系统日志',
},
];
const onTabChange = (e: string) => {
activeKey.value = e;
};
</script>

31
src/views/Log/typings.d.ts vendored Normal file
View File

@ -0,0 +1,31 @@
export type AccessLogItem = {
id: string;
context: any;
describe: string;
exception: string;
httpHeaders: any;
httpMethod: string;
ip: string;
method: string;
parameters: any;
requestTime: number;
responseTime: number;
target: string;
url: string;
action: string;
};
export type SystemLogItem = {
id: string;
className: string;
context: any;
createTime: number;
exceptionStack: string;
level: string;
lineNumber: number;
message: string;
methodName: string;
name: string;
threadId: string;
threadName: string;
};

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> <template>
<div style="margin-top: 20px" v-if="config.length"> <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> <div style="font-size: 16px; font-weight: 700">配置</div>
<a-space> <a-space>
<a-button type="link" @click="visible = true"><AIcon type="EditOutlined" />编辑</a-button> <a-button type="link" @click="visible = true"><AIcon type="EditOutlined" />编辑</a-button>
@ -13,11 +13,11 @@
</a-space> </a-space>
</div> </div>
<a-descriptions bordered size="small" v-for="i in config" :key="i.name"> <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"> <a-descriptions-item v-for="item in i.properties" :key="item.property">
<template #label> <template #label>
<span style="margin-right: 5px">{{item.name}}</span>
<a-tooltip v-if="item.description" :title="item.description"><AIcon type="QuestionCircleOutlined" /></a-tooltip> <a-tooltip v-if="item.description" :title="item.description"><AIcon type="QuestionCircleOutlined" /></a-tooltip>
<span>{{item.name}}</span>
</template> </template>
<span v-if="item.type.type === 'password' && instanceStore.current?.configuration?.[item.property]?.length > 0">******</span> <span v-if="item.type.type === 'password' && instanceStore.current?.configuration?.[item.property]?.length > 0">******</span>
<span v-else> <span v-else>
@ -26,6 +26,7 @@
</span> </span>
</a-descriptions-item> </a-descriptions-item>
</a-descriptions> </a-descriptions>
<Save v-if="visible" @save="saveBtn" @close="visible = false" :config="config" />
</div> </div>
</template> </template>
@ -34,6 +35,7 @@ import { useInstanceStore } from "@/store/instance"
import { ConfigMetadata } from "@/views/device/Product/typings" import { ConfigMetadata } from "@/views/device/Product/typings"
import { getConfigMetadata, _deploy, configurationReset } from '@/api/device/instance' import { getConfigMetadata, _deploy, configurationReset } from '@/api/device/instance'
import { message } from "ant-design-vue" import { message } from "ant-design-vue"
import Save from './Save.vue'
const instanceStore = useInstanceStore() const instanceStore = useInstanceStore()
const visible = ref<boolean>(false) 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> </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> <template>
<a-card> <a-card>
<a-row type="flex"> <div class="property-box">
<a-col flex="200px"> <div class="property-box-left">
<div> <a-input-search
<a-input-search v-model:value="value"
v-model:value="value" placeholder="请输入事件名称"
placeholder="请输入事件名称" style="width: 200px; margin-bottom: 10px"
style="width: 200px; margin-bottom: 10px" @search="onSearch"
@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 </a-tabs>
tab-position="left" </div>
:style="{ height: '600px' }" <div class="property-box-right">
v-model:activeKey="activeKey" <Event v-if="type === 'event'" :data="data" />
tabBarStyle="width: 200px" <Property v-else :data="properties" />
> </div>
<a-tab-pane v-for="i in tabList" :key="i.key" :tab="i.tab" /> </div>
</a-tabs>
</div>
</a-col>
<a-col flex="auto">
<!-- <component :is="tabs[activeKey]" /> -->
123
</a-col>
</a-row>
</a-card> </a-card>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const activeKey = ref<string>('property') import { useInstanceStore } from '@/store/instance';
const tabList = ref<{key: string, tab: string}[]>([ import _ from 'lodash';
{ import Event from './Event/index.vue';
key: 'property', import Property from './Property/index.vue';
tab: '属性'
}, const activeKey = ref<string>('property');
{ const tabList = ref<{ key: string; tab: string; type: 'property' | 'event' }[]>(
key: 'event1', [
tab: '事件1' {
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 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> </script>
<style lang="less" scoped>
.property-box {
display: flex;
.property-box-left {
width: 200px;
margin-right: 20px;
}
.property-box-right {
flex: 1;
}
}
</style>

View File

@ -117,11 +117,14 @@ const objToParams = (source: object): string => {
} }
.box-list { .box-list {
grid-template-columns: repeat(5, 1fr); // grid-template-columns: repeat(5, 1fr);
display: grid; // display: grid;
grid-column-gap: 66px; // grid-column-gap: 66px;
display: flex;
gap: 66px;
.list-item { .list-item {
flex: 1;
position: relative; position: relative;
.box-top { .box-top {
position: relative; position: relative;

View File

@ -1,6 +1,6 @@
<!-- 物联卡查看 --> <!-- 物联卡查看 -->
<template> <template>
<div class="page-container"> <page-container>
<!-- 新增编辑 --> <!-- 新增编辑 -->
<Save <Save
v-if="visible" v-if="visible"
@ -117,7 +117,7 @@
</a-row> </a-row>
</a-col> </a-col>
</a-row> </a-row>
</div> </page-container>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -138,7 +138,7 @@ const detail = ref<any>({});
const flowData = ref<any[]>([]); const flowData = ref<any[]>([]);
const getDetail = () => { const getDetail = () => {
queryDetail(route.query.id).then((resp: any) => { queryDetail(route.params.id).then((resp: any) => {
if (resp.status === 200) { if (resp.status === 200) {
detail.value = resp.result; detail.value = resp.result;
} }

View File

@ -1,6 +1,6 @@
<!-- 物联卡管理 --> <!-- 物联卡管理 -->
<template> <template>
<div class="page-container"> <page-container class="container">
<Search <Search
:columns="columns" :columns="columns"
target="iot-card-management-search" target="iot-card-management-search"
@ -323,7 +323,7 @@
:data="current" :data="current"
@change="saveChange" @change="saveChange"
/> />
</div> </page-container>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -539,10 +539,7 @@ const getActions = (
icon: 'EyeOutlined', icon: 'EyeOutlined',
onClick: () => { onClick: () => {
router.push({ router.push({
path: '/iot-card/CardManagement/Detail', path: `/iot-card/CardManagement/detail/${data.id}`,
query: {
id: data.id,
},
}); });
}, },
}, },
@ -795,7 +792,7 @@ const handelRemove = async () => {
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.page-container { .container {
.search { .search {
width: calc(100% - 330px); width: calc(100% - 330px);
} }

View File

@ -1,6 +1,6 @@
<!-- 物联卡-仪表盘 --> <!-- 物联卡-仪表盘 -->
<template> <template>
<div class="page-container"> <page-container class="container">
<a-card> <a-card>
<a-row :gutter="20" :style="{ marginBottom: '20px' }"> <a-row :gutter="20" :style="{ marginBottom: '20px' }">
<a-col :span="24"><Guide title="数据统计" /></a-col> <a-col :span="24"><Guide title="数据统计" /></a-col>
@ -104,7 +104,7 @@
</a-col> </a-col>
</a-row> </a-row>
</a-card> </a-card>
</div> </page-container>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -229,7 +229,7 @@ const dTime = [
getTopRang(dTime[0], dTime[1]); getTopRang(dTime[0], dTime[1]);
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.page-container { .container {
.data-statistics-item { .data-statistics-item {
height: 140px; height: 140px;
background: #fcfcfc; background: #fcfcfc;

View File

@ -1,6 +1,6 @@
<!-- 物联卡-首页 --> <!-- 物联卡-首页 -->
<template> <template>
<div class="page-container"> <page-container>
<a-row :gutter="24"> <a-row :gutter="24">
<a-col :span="14"> <a-col :span="14">
<div class="home-guide"> <div class="home-guide">
@ -79,7 +79,7 @@
</div> </div>
</a-col> </a-col>
</a-row> </a-row>
</div> </page-container>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@ -1,9 +1,9 @@
<!-- 充值管理 --> <!-- 充值管理 -->
<template> <template>
<div class="page-container"> <page-container>
<Search <Search
:columns="columns" :columns="columns"
target="iot-card-management-search" target="recharge-search"
@search="handleSearch" @search="handleSearch"
/> />
<JTable <JTable
@ -71,7 +71,7 @@
<!-- 充值 --> <!-- 充值 -->
<Save v-if="visible" @change="saveChange" /> <Save v-if="visible" @change="saveChange" />
<Detail v-if="detailVisible" :data="current" @close="close" /> <Detail v-if="detailVisible" :data="current" @close="close" />
</div> </page-container>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@ -1,42 +1,44 @@
<template> <template>
<a-spin :spinning="loading"> <page-container>
<a-card :bordered="false"> <a-spin :spinning="loading">
<div v-if="type && modeType === 'add'"> <a-card :bordered="false">
<Provider <div v-if="type && id === ':id'">
@onClick="goProviders" <Provider
:dataSource="dataSource" @onClick="goProviders"
></Provider> :dataSource="dataSource"
</div> ></Provider>
<div v-else> </div>
<div v-if="!id"><a @click="goBack">返回</a></div> <div v-else>
<AccessNetwork <div v-if="!id"><a @click="goBack">返回</a></div>
v-if="showType === 'network'" <AccessNetwork
:provider="provider" v-if="showType === 'network'"
:data="data" :provider="provider"
/> :data="data"
<Media />
v-if="showType === 'media'" <Media
:provider="provider" v-if="showType === 'media'"
:data="data" :provider="provider"
/> :data="data"
<Channel />
v-if="showType === 'channel'" <Channel
:provider="provider" v-if="showType === 'channel'"
:data="data" :provider="provider"
/> :data="data"
<Edge />
v-if="showType === 'edge'" <Edge
:provider="provider" v-if="showType === 'edge'"
:data="data" :provider="provider"
/> :data="data"
<Cloud />
v-if="showType === 'cloud'" <Cloud
:provider="provider" v-if="showType === 'cloud'"
:data="data" :provider="provider"
/> :data="data"
</div> />
</a-card> </div>
</a-spin> </a-card>
</a-spin>
</page-container>
</template> </template>
<script lang="ts" setup name="AccessConfigDetail"> <script lang="ts" setup name="AccessConfigDetail">
@ -51,7 +53,7 @@ import Cloud from '../components/Cloud/index.vue';
const route = useRoute(); const route = useRoute();
const modeType = route.params.type as string; const view = route.query.view as string;
const id = route.params.id as string; const id = route.params.id as string;
const dataSource = ref([]); const dataSource = ref([]);
@ -138,7 +140,7 @@ const queryProviders = async () => {
}; };
const getProvidersData = async () => { const getProvidersData = async () => {
if (id && modeType !== 'add') { if (id !== ':id') {
getProviders().then((response) => { getProviders().then((response) => {
if (response.status === 200) { if (response.status === 200) {
const list = getTypeList(response.result); const list = getTypeList(response.result);

View File

@ -40,7 +40,7 @@
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-button <a-button
v-if="modeType !== 'view'" v-if="view === 'false'"
type="primary" type="primary"
html-type="submit" html-type="submit"
>保存</a-button >保存</a-button
@ -96,7 +96,7 @@ interface FormState {
description: string; description: string;
} }
const route = useRoute(); const route = useRoute();
const modeType = route.params.type as string; const view = route.query.view as string;
const id = route.params.id as string; const id = route.params.id as string;
const props = defineProps({ const props = defineProps({
@ -126,9 +126,7 @@ const onFinish = async (values: any) => {
channel: providerId === 'modbus-tcp' ? 'modbus' : 'opc-ua', channel: providerId === 'modbus-tcp' ? 'modbus' : 'opc-ua',
}; };
const resp = const resp =
!!id && modeType !== 'add' id === ':id' ? await save(params) : await update({ ...params, id });
? await update({ ...params, id })
: await save(params);
if (resp.status === 200) { if (resp.status === 200) {
message.success('操作成功!'); message.success('操作成功!');
// if (params.get('save')) { // if (params.get('save')) {
@ -145,7 +143,7 @@ const onFinish = async (values: any) => {
}; };
onMounted(() => { onMounted(() => {
if (modeType !== 'add') { if (id === ':id') {
formState.value = { formState.value = {
name: props.data.name, name: props.data.name,
description: props.data?.description || '', description: props.data?.description || '',

View File

@ -283,7 +283,7 @@
下一步 下一步
</a-button> </a-button>
<a-button <a-button
v-if="current === 2 && modeType !== 'view'" v-if="current === 2 && view === 'false'"
type="primary" type="primary"
style="margin-right: 8px" style="margin-right: 8px"
@click="saveData" @click="saveData"
@ -383,7 +383,7 @@ interface Form {
description: string; description: string;
} }
const route = useRoute(); const route = useRoute();
const modeType = route.params.type as string; const view = route.query.view as string;
const id = route.params.id as string; const id = route.params.id as string;
const props = defineProps({ const props = defineProps({
@ -450,13 +450,13 @@ const saveData = async () => {
transport: 'HTTP_SERVER', transport: 'HTTP_SERVER',
}; };
const resp = const resp =
!!id && modeType !== 'add' id === ':id'
? await update({ ? await save(params)
: await update({
...props.data, ...props.data,
...params, ...params,
}) id,
: await save(params); });
if (resp.status === 200) { if (resp.status === 200) {
message.success('操作成功!'); message.success('操作成功!');
// //
@ -489,7 +489,7 @@ const queryProcotolList = async (id: string, params = {}) => {
const addProcotol = () => { const addProcotol = () => {
// const url = this.$store.state.permission.routes['Link/Protocol'] // const url = this.$store.state.permission.routes['Link/Protocol']
const url = '/demo'; const url = '/iot/link/protocol';
const tab = window.open( const tab = window.open(
`${window.location.origin + window.location.pathname}#${url}?save=true`, `${window.location.origin + window.location.pathname}#${url}?save=true`,
); );
@ -518,7 +518,7 @@ const prev = () => {
current.value = current.value - 1; current.value = current.value - 1;
}; };
onMounted(() => { onMounted(() => {
if (modeType !== 'add') { if (id !== ':id') {
formState.value = props.data.configuration; formState.value = props.data.configuration;
procotolCurrent.value = props.data.protocol; procotolCurrent.value = props.data.protocol;
formData.value = { formData.value = {

View File

@ -377,7 +377,7 @@
</a-button> </a-button>
<a-button <a-button
style="margin-right: 8px" style="margin-right: 8px"
v-if="current === 2 && modeType !== 'view'" v-if="current === 2 && view === 'false'"
type="primary" type="primary"
@click="saveData" @click="saveData"
> >
@ -479,7 +479,7 @@ interface Form {
description: string; description: string;
} }
const route = useRoute(); const route = useRoute();
const modeType = route.params.type as string; const view = route.query.view as string;
const id = route.params.id as string; const id = route.params.id as string;
const props = defineProps({ const props = defineProps({
@ -547,12 +547,13 @@ const saveData = async () => {
transport: 'HTTP_SERVER', transport: 'HTTP_SERVER',
}; };
const resp = const resp =
!!id && modeType !== 'add' id === ':id'
? await update({ ? await save(params)
: await update({
...props.data, ...props.data,
...params, ...params,
}) id,
: await save(params); });
if (resp.status === 200) { if (resp.status === 200) {
message.success('操作成功!'); message.success('操作成功!');
@ -585,7 +586,7 @@ const queryProcotolList = async (id: string, params = {}) => {
const addProcotol = () => { const addProcotol = () => {
// const url = this.$store.state.permission.routes['Link/Protocol'] // const url = this.$store.state.permission.routes['Link/Protocol']
const url = '/demo'; const url = '/iot/link/protocol';
const tab = window.open( const tab = window.open(
`${window.location.origin + window.location.pathname}#${url}?save=true`, `${window.location.origin + window.location.pathname}#${url}?save=true`,
); );
@ -615,7 +616,7 @@ const prev = () => {
}; };
onMounted(() => { onMounted(() => {
if (modeType !== 'add') { if (id !== ':id') {
formState.value = props.data.configuration; formState.value = props.data.configuration;
procotolCurrent.value = props.data.protocol; procotolCurrent.value = props.data.protocol;
formData.value = { formData.value = {

View File

@ -140,7 +140,7 @@
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-button <a-button
v-if="current !== 1 && modeType !== 'view'" v-if="current !== 1 && view === 'false'"
type="primary" type="primary"
html-type="submit" html-type="submit"
>保存</a-button >保存</a-button
@ -179,7 +179,7 @@
下一步 下一步
</a-button> </a-button>
<a-button <a-button
v-if="current === 1 && modeType !== 'view'" v-if="current === 1 && view === 'false'"
type="primary" type="primary"
style="margin-right: 8px" style="margin-right: 8px"
@click="saveData" @click="saveData"
@ -297,7 +297,7 @@ interface FormState {
description: string; description: string;
} }
const route = useRoute(); const route = useRoute();
const modeType = route.params.type as string; const view = route.query.view as string;
const id = route.params.id as string; const id = route.params.id as string;
const props = defineProps({ const props = defineProps({
@ -338,9 +338,7 @@ const onFinish = async (values: any) => {
}; };
if (networkCurrent.value) params.channelId = networkCurrent.value; if (networkCurrent.value) params.channelId = networkCurrent.value;
const resp = const resp =
!!id && modeType !== 'add' id === ':id' ? await save(params) : await update({ ...params, id });
? await update({ ...params, id })
: await save(params);
if (resp.status === 200) { if (resp.status === 200) {
message.success('操作成功!'); message.success('操作成功!');
// if (params.get('save')) { // if (params.get('save')) {
@ -392,7 +390,7 @@ const saveData = async () => {
const addNetwork = () => { const addNetwork = () => {
// const url = this.$store.state.permission.routes['Link/Type/Detail'] // const url = this.$store.state.permission.routes['Link/Type/Detail']
const url = '/demo'; const url = '/iot/link/type/detail/:id';
const tab = window.open( const tab = window.open(
`${window.location.origin + window.location.pathname}#${url}?type=${ `${window.location.origin + window.location.pathname}#${url}?type=${
NetworkTypeMapping.get(props.provider?.id) || '' NetworkTypeMapping.get(props.provider?.id) || ''
@ -421,7 +419,7 @@ onMounted(() => {
if (props.provider.id === 'official-edge-gateway') { if (props.provider.id === 'official-edge-gateway') {
queryNetworkList(props.provider.id, ''); queryNetworkList(props.provider.id, '');
} }
if (modeType !== 'add') { if (id !== ':id') {
formState.value = { formState.value = {
name: props.data.name, name: props.data.name,
description: props.data?.description || '', description: props.data?.description || '',

View File

@ -503,7 +503,7 @@
下一步 下一步
</a-button> </a-button>
<a-button <a-button
v-if="current === 1 && modeType !== 'view'" v-if="current === 1 && view === 'false'"
type="primary" type="primary"
style="margin-right: 8px" style="margin-right: 8px"
@click="saveData" @click="saveData"
@ -559,7 +559,7 @@ const props = defineProps({
}); });
const route = useRoute(); const route = useRoute();
const modeType = route.params.type as string; const view = route.query.view as string;
const id = route.params.id as string; const id = route.params.id as string;
const activeKey: any = ref([]); const activeKey: any = ref([]);
@ -663,9 +663,7 @@ const saveData = () => {
}; };
const resp = const resp =
!!id && modeType !== 'add' id === ':id' ? await save(params) : await update({ ...params, id });
? await update({ ...params, id })
: await save(params);
if (resp.status === 200) { if (resp.status === 200) {
message.success('操作成功!'); message.success('操作成功!');
// if (params.get('save')) { // if (params.get('save')) {
@ -741,7 +739,7 @@ onMounted(() => {
} }
}); });
if (modeType !== 'add') { if (id !== ':id') {
formState.value = props.data.configuration; formState.value = props.data.configuration;
formData.value = { formData.value = {
name: props.data.name, name: props.data.name,

View File

@ -40,7 +40,7 @@
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-button <a-button
v-if="modeType !== 'view'" v-if="view === 'false'"
type="primary" type="primary"
html-type="submit" html-type="submit"
>保存</a-button >保存</a-button
@ -91,7 +91,7 @@ interface FormState {
description: string; description: string;
} }
const route = useRoute(); const route = useRoute();
const modeType = route.params.type as string; const view = route.query.view as string;
const id = route.params.id as string; const id = route.params.id as string;
const props = defineProps({ const props = defineProps({
@ -119,9 +119,7 @@ const onFinish = async (values: any) => {
channel: 'fixed-media', channel: 'fixed-media',
}; };
const resp = const resp =
!!id && modeType !== 'add' id === ':id' ? await save(params) : await update({ ...params, id });
? await update({ ...params, id })
: await save(params);
if (resp.status === 200) { if (resp.status === 200) {
message.success('操作成功!'); message.success('操作成功!');
// if (params.get('save')) { // if (params.get('save')) {
@ -138,7 +136,7 @@ const onFinish = async (values: any) => {
}; };
onMounted(() => { onMounted(() => {
if (modeType !== 'add') { if (id !== ':id') {
formState.value = { formState.value = {
name: props.data.name, name: props.data.name,
description: props.data?.description || '', description: props.data?.description || '',

View File

@ -1,5 +1,5 @@
<template> <template>
<div style="margin-top: 10px"> <div>
<a-steps :current="stepCurrent"> <a-steps :current="stepCurrent">
<a-step v-for="item in steps" :key="item" :title="item" /> <a-step v-for="item in steps" :key="item" :title="item" />
</a-steps> </a-steps>
@ -304,7 +304,7 @@
下一步 下一步
</a-button> </a-button>
<a-button <a-button
v-if="current === 2 && modeType !== 'view'" v-if="current === 2 && view === 'false'"
type="primary" type="primary"
style="margin-right: 8px" style="margin-right: 8px"
@click="saveData" @click="saveData"
@ -767,7 +767,7 @@ const props = defineProps({
const clientHeight = document.body.clientHeight; const clientHeight = document.body.clientHeight;
const type = props.provider.channel; const type = props.provider.channel;
const route = useRoute(); const route = useRoute();
const modeType = route.params.type as string; const view = route.query.view as string;
const id = route.params.id as string; const id = route.params.id as string;
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
@ -839,7 +839,7 @@ const queryProcotolList = async (id: string, params = {}) => {
const addNetwork = () => { const addNetwork = () => {
// const url = this.$store.state.permission.routes['Link/Type/Detail'] // const url = this.$store.state.permission.routes['Link/Type/Detail']
const url = '/demo'; const url = '/iot/link/type/detail/:id';
const tab = window.open( const tab = window.open(
`${window.location.origin + window.location.pathname}#${url}?type=${ `${window.location.origin + window.location.pathname}#${url}?type=${
NetworkTypeMapping.get(props.provider?.id) || '' NetworkTypeMapping.get(props.provider?.id) || ''
@ -854,7 +854,7 @@ const addNetwork = () => {
}; };
const addProcotol = () => { const addProcotol = () => {
// const url = this.$store.state.permission.routes['Link/Protocol'] // const url = this.$store.state.permission.routes['Link/Protocol']
const url = '/demo'; const url = '/iot/link/protocol';
const tab = window.open( const tab = window.open(
`${window.location.origin + window.location.pathname}#${url}?save=true`, `${window.location.origin + window.location.pathname}#${url}?save=true`,
); );
@ -903,27 +903,25 @@ const procotolSearch = (value: string) => {
const saveData = () => { const saveData = () => {
validate() validate()
.then(async (values) => { .then(async (values) => {
let resp = undefined; const params = {
let params = {
...props.data, ...props.data,
...values, ...values,
protocol: procotolCurrent.value, protocol: procotolCurrent.value,
channel: 'network', // channel: 'network', //
channelId: networkCurrent.value, channelId: networkCurrent.value,
}; };
if (!!id && modeType !== 'add') { const resp =
resp = await update(params); id === ':id'
} else { ? await save(params)
params = { : await update({
...params, ...params,
provider: props.provider.id, id,
transport: provider: props.provider.id,
props.provider?.id === 'child-device' transport:
? 'Gateway' props.provider?.id === 'child-device'
: ProtocolMapping.get(props.provider.id), ? 'Gateway'
}; : ProtocolMapping.get(props.provider.id),
resp = await save(params); });
}
if (resp.status === 200) { if (resp.status === 200) {
message.success('操作成功!'); message.success('操作成功!');
// //
@ -1104,7 +1102,7 @@ onMounted(() => {
}); });
onMounted(() => { onMounted(() => {
if (modeType !== 'add') { if (id !== ':id') {
procotolCurrent.value = props.data.protocol; procotolCurrent.value = props.data.protocol;
formData.value = { formData.value = {
name: props.data.name, name: props.data.name,

View File

@ -1,9 +1,8 @@
<template> <template>
<div class="page-container"> <page-container>
<a-card style="margin-bottom: 20px"> <div>
<Search :columns="columns" target="search" @search="handleSearch" /> <Search :columns="columns" target="search" @search="handleSearch" />
</a-card>
<a-card>
<JTable <JTable
ref="tableRef" ref="tableRef"
model="CARD" model="CARD"
@ -187,8 +186,8 @@
/> />
</template> </template>
</JTable> </JTable>
</a-card> </div>
</div> </page-container>
</template> </template>
<script lang="ts" setup name="AccessConfigPage"> <script lang="ts" setup name="AccessConfigPage">
import type { ActionsType } from '@/components/Table/index.vue'; import type { ActionsType } from '@/components/Table/index.vue';
@ -345,13 +344,25 @@ const getProvidersList = async () => {
getProvidersList(); getProvidersList();
const handlAdd = () => { const handlAdd = () => {
router.push('/link/accessConfig/detail/add/new'); // router.push('/link/accessConfig/detail/add/new');
router.push({
path: `/iot/link/accessConfig/detail/:id`,
query: { view: false },
});
}; };
const handlEdit = (id: string) => { const handlEdit = (id: string) => {
router.push(`/link/accessConfig/detail/edit/${id}`); // router.push(`/link/accessConfig/detail/edit/${id}`);
router.push({
path: `/iot/link/accessConfig/detail/${id}`,
query: { view: false },
});
}; };
const handlEye = (id: string) => { const handlEye = (id: string) => {
router.push(`/link/accessConfig/detail/view/${id}`); // router.push(`/link/accessConfig/detail/view/${id}`);
router.push({
path: `/iot/link/accessConfig/detail/${id}`,
query: { view: true },
});
}; };
/** /**
@ -375,10 +386,6 @@ const handleSearch = (e: any) => {
// } // }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.page-container {
background: #f0f2f5;
padding: 24px;
}
.tableCardDisabled { .tableCardDisabled {
width: 100%; width: 100%;
background: url('/images/access-config-diaabled.png') no-repeat; background: url('/images/access-config-diaabled.png') no-repeat;

View File

@ -1,96 +1,104 @@
<template> <template>
<a-card> <page-container>
<a-row :gutter="[24, 24]" style="padding: 24px"> <a-card>
<a-col :span="12"> <a-row :gutter="[24, 24]" style="padding: 24px">
<a-form <a-col :span="12">
class="form" <a-form
layout="vertical" class="form"
:model="formData" layout="vertical"
name="basic" :model="formData"
:label-col="{ span: 8 }" name="basic"
:wrapper-col="{ span: 16 }" :label-col="{ span: 8 }"
autocomplete="off" :wrapper-col="{ span: 16 }"
> autocomplete="off"
<a-form-item label="证书标准" v-bind="validateInfos.type">
<a-radio-group v-model:value="formData.type">
<a-radio-button
class="form-radio-button"
value="common"
>
<img :src="getImage('/certificate.png')" />
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="证书名称" v-bind="validateInfos.name">
<a-input
placeholder="请输入证书名称"
v-model:value="formData.name"
/>
</a-form-item>
<a-form-item
label="证书文件"
v-bind="validateInfos['configs.cert']"
> >
<CertificateFile <a-form-item
name="cert" label="证书标准"
v-model:modelValue="formData.configs.cert" v-bind="validateInfos.type"
placeholder='证书格式以"-----BEGIN CERTIFICATE-----"开头,以"-----END CERTIFICATE-----"结尾"'
/>
</a-form-item>
<a-form-item
label="证书私钥"
v-bind="validateInfos['configs.key']"
>
<CertificateFile
name="key"
v-model:modelValue="formData.configs.key"
placeholder='证书私钥格式以"-----BEGIN (RSA|EC) PRIVATE KEY-----"开头,以"-----END(RSA|EC) PRIVATE KEY-----"结尾。'
/>
</a-form-item>
<a-form-item label="说明" name="description">
<a-textarea
placeholder="请输入说明"
v-model:value="formData.description"
:maxlength="200"
:rows="3"
showCount
/>
</a-form-item>
<a-form-item>
<a-button
v-if="modeType !== 'view'"
class="form-submit"
html-type="submit"
type="primary"
@click.prevent="onSubmit"
:loading="loading"
>保存</a-button
> >
</a-form-item> <a-radio-group v-model:value="formData.type">
</a-form> <a-radio-button
</a-col> class="form-radio-button"
<a-col :span="12"> value="common"
<div class="doc"> >
<h1>1. 概述</h1> <img :src="getImage('/certificate.png')" />
<div> </a-radio-button>
证书由受信任的数字证书颁发机构CA在验证服务器身份后颁发具有服务器身份验证和数据传输加密功能保障设备与平台间的数据传输安全配置后可被网络组件引用 </a-radio-group>
</a-form-item>
<a-form-item
label="证书名称"
v-bind="validateInfos.name"
>
<a-input
placeholder="请输入证书名称"
v-model:value="formData.name"
/>
</a-form-item>
<a-form-item
label="证书文件"
v-bind="validateInfos['configs.cert']"
>
<CertificateFile
name="cert"
v-model:modelValue="formData.configs.cert"
placeholder='证书格式以"-----BEGIN CERTIFICATE-----"开头,以"-----END CERTIFICATE-----"结尾"'
/>
</a-form-item>
<a-form-item
label="证书私钥"
v-bind="validateInfos['configs.key']"
>
<CertificateFile
name="key"
v-model:modelValue="formData.configs.key"
placeholder='证书私钥格式以"-----BEGIN (RSA|EC) PRIVATE KEY-----"开头,以"-----END(RSA|EC) PRIVATE KEY-----"结尾。'
/>
</a-form-item>
<a-form-item label="说明" name="description">
<a-textarea
placeholder="请输入说明"
v-model:value="formData.description"
:maxlength="200"
:rows="3"
showCount
/>
</a-form-item>
<a-form-item>
<a-button
v-if="view === 'false'"
class="form-submit"
html-type="submit"
type="primary"
@click.prevent="onSubmit"
:loading="loading"
>保存</a-button
>
</a-form-item>
</a-form>
</a-col>
<a-col :span="12">
<div class="doc">
<h1>1. 概述</h1>
<div>
证书由受信任的数字证书颁发机构CA在验证服务器身份后颁发具有服务器身份验证和数据传输加密功能保障设备与平台间的数据传输安全配置后可被网络组件引用
</div>
<h1>2. 配置说明</h1>
<h2>1证书文件</h2>
<div>
您可以使用文本编辑工具打开PEM或者CRT格式的证书文件复制其中的内容并粘贴到该文本框或者单击该文本框下的上传并选择存储在本地计算机的证书文件将文件内容上传到文本框
</div>
<h2>2证书私钥</h2>
<div>
填写证书私钥内容的PEM编码
您可以使用文本编辑工具打开KEY格式的证书私钥文件复制其中的内容并粘贴到该文本框或者单击该文本框下的上传并选择存储在本地计算机的证书私钥文件将文件内容上传到文本框
</div>
</div> </div>
<h1>2. 配置说明</h1> </a-col>
<h2>1证书文件</h2> </a-row>
<div> </a-card>
您可以使用文本编辑工具打开PEM或者CRT格式的证书文件复制其中的内容并粘贴到该文本框或者单击该文本框下的上传并选择存储在本地计算机的证书文件将文件内容上传到文本框 </page-container>
</div>
<h2>2证书私钥</h2>
<div>
填写证书私钥内容的PEM编码
您可以使用文本编辑工具打开KEY格式的证书私钥文件复制其中的内容并粘贴到该文本框或者单击该文本框下的上传并选择存储在本地计算机的证书私钥文件将文件内容上传到文本框
</div>
</div>
</a-col>
</a-row>
</a-card>
</template> </template>
<script lang="ts" setup name="CertificateDetail"> <script lang="ts" setup name="CertificateDetail">
@ -103,7 +111,7 @@ import { FormDataType, TypeObjType } from '../type';
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const modeType = route.params.type as string; const view = route.query.view as string;
const id = route.params.id as string; const id = route.params.id as string;
const useForm = Form.useForm; const useForm = Form.useForm;
@ -145,10 +153,12 @@ const onSubmit = () => {
const params = toRaw(formData.value); const params = toRaw(formData.value);
loading.value = true; loading.value = true;
const response = const response =
modeType === 'edit' ? await update(params) : await save(params); id === ':id'
? await save(params)
: await update({ ...params, id });
if (response.status === 200) { if (response.status === 200) {
message.success('操作成功'); message.success('操作成功');
router.push('/link/certificate'); router.push('/iot/link/certificate');
} }
loading.value = false; loading.value = false;
}) })
@ -168,7 +178,7 @@ const handleChange = (info: UploadChangeParam) => {
}; };
const detail = async (id: string) => { const detail = async (id: string) => {
if (modeType !== 'add') { if (id !== ':id') {
loading.value = true; loading.value = true;
const res = await queryDetail(id); const res = await queryDetail(id);
if (res.success) { if (res.success) {

View File

@ -1,13 +1,7 @@
<template> <template>
<div class="page-container"> <page-container>
<a-card style="margin-bottom: 20px"> <div>
<Search <Search :columns="columns" target="search" @search="handleSearch" />
:columns="columns"
target="search"
@search="handleSearch"
/>
</a-card>
<a-card>
<JTable <JTable
ref="tableRef" ref="tableRef"
model="TABLE" model="TABLE"
@ -61,8 +55,8 @@
</a-space> </a-space>
</template> </template>
</JTable> </JTable>
</a-card> </div>
</div> </page-container>
</template> </template>
<script lang="ts" setup name="CertificatePage"> <script lang="ts" setup name="CertificatePage">
import type { ActionsType } from '@/components/Table/index.vue'; import type { ActionsType } from '@/components/Table/index.vue';
@ -158,15 +152,24 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
}; };
const handlAdd = () => { const handlAdd = () => {
router.push('/link/certificate/detail/add/new'); router.push({
path: `/iot/link/certificate/detail/:id`,
query: { view: false },
});
}; };
const handlEye = (id: string) => { const handlEye = (id: string) => {
router.push(`/link/certificate/detail/view/${id}`); router.push({
path: `/iot/link/certificate/detail/${id}`,
query: { view: true },
});
}; };
const handlEdit = (id: string) => { const handlEdit = (id: string) => {
router.push(`/link/certificate/detail/edit/${id}`); router.push({
path: `/iot/link/certificate/detail/${id}`,
query: { view: false },
});
}; };
const handlDelete = async (id: string) => { const handlDelete = async (id: string) => {
@ -182,15 +185,8 @@ const handlDelete = async (id: string) => {
* @param params * @param params
*/ */
const handleSearch = (e: any) => { const handleSearch = (e: any) => {
console.log(1211, e);
params.value = e; params.value = e;
}; };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped></style>
.page-container {
background: #f0f2f5;
padding: 24px;
}
</style>

View File

@ -1,6 +0,0 @@
<template>
<div>访问日志</div>
</template>
<script lang="ts" setup name="SystemLog">
</script>

View File

@ -1,6 +0,0 @@
<template>
<div>系统日志</div>
</template>
<script lang="ts" setup name="AccessLog">
</script>

View File

@ -1,17 +0,0 @@
<template>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1" tab="访问日志">
<AccessLog />
</a-tab-pane>
<a-tab-pane key="2" tab="系统日志" force-render>
<SystemLog />
</a-tab-pane>
</a-tabs>
</template>
<script lang="ts" setup name="LogPage">
import { defineComponent, ref } from 'vue';
import AccessLog from './Access/index.vue';
import SystemLog from './System/index.vue';
const activeKey = ref('1');
</script>

View File

@ -0,0 +1,37 @@
<template lang="">
<a-modal
:title="data.id ? '编辑' : '新增'"
ok-text="确认"
cancel-text="取消"
:visible="true"
width="600px"
:confirm-loading="loading"
@cancel="handleCancel"
@ok="handleOk"
>
123
</a-modal>
</template>
<script lang="ts" setup>
const loading = ref(false);
const props = defineProps({
data: {
type: Object,
default: () => {},
},
});
const emit = defineEmits(['change']);
const handleOk = () => {
console.log(2);
emit('change', true);
};
const handleCancel = () => {
console.log(1);
emit('change', false);
};
</script>
<style lang=""></style>

View File

@ -0,0 +1,327 @@
<template>
<page-container>
<div>
<Search :columns="columns" target="search" @search="handleSearch" />
<JTable
ref="tableRef"
:columns="columns"
:request="list"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
:params="params"
>
<template #headerTitle>
<a-button type="primary" @click="handlAdd"
><plus-outlined />新增</a-button
>
</template>
<template #card="slotProps">
<CardBox
:showStatus="false"
:value="slotProps"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
>
<template #img>
<slot name="img">
<img :src="getImage('/device-access.png')" />
</slot>
</template>
<template #content>
<div class="card-item-content">
<h3 class="card-item-content-title-a">
{{ slotProps.name }}
</h3>
<a-row class="card-item-content-box">
<a-col
:span="12"
class="card-item-content-text"
>
<div class="card-item-content-text">
ID
</div>
<div class="card-item-content-text">
<a-tooltip>
<template #title>{{
slotProps.id
}}</template>
{{ slotProps.id }}
</a-tooltip>
</div>
</a-col>
<a-col :span="12">
<div class="card-item-content-text">
类型
</div>
<div class="card-item-content-text">
<a-tooltip>
<template #title>{{
slotProps.type
}}</template>
{{ slotProps.type }}
</a-tooltip>
</div>
</a-col>
</a-row>
</div>
</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 #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"
: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>
</div>
<Save v-if="visible" :data="current" @change="saveChange" />
</page-container>
</template>
<script lang="ts" setup name="AccessConfigPage">
import type { ActionsType } from '@/components/Table/index.vue';
import { getImage } from '@/utils/comm';
import { list, remove } from '@/api/link/protocol';
import { message } from 'ant-design-vue';
import Save from './Save/index.vue';
const tableRef = ref<Record<string, any>>({});
const router = useRouter();
const params = ref<Record<string, any>>({});
const visible = ref(false);
const current = ref({});
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
search: {
type: 'string',
},
width: 200,
fixed: 'left',
// scopedSlots: true,
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
search: {
type: 'string',
},
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
search: {
type: 'select',
options: [
{
label: 'jar',
value: 'jar',
},
{
label: 'local',
value: 'local',
},
],
},
scopedSlots: true,
},
{
title: '说明',
dataIndex: 'description',
key: 'description',
search: {
type: 'string',
},
ellipsis: true,
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 200,
scopedSlots: true,
},
];
const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return [];
const actions = [
{
key: 'edit',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
handlEdit(data.id);
},
},
{
key: 'delete',
text: '删除',
popConfirm: {
title: '确认删除?',
onConfirm: async () => {
console.log(11, data.id);
// const res = await remove(data.id);
// if (res.success) {
// message.success('');
// tableRef.value.reload();
// } else {
// message.error('');
// }
},
},
icon: 'DeleteOutlined',
},
];
return actions;
};
const handlAdd = () => {
console.log(11);
visible.value = true;
};
const handlEdit = (id: string) => {
// router.push(`/link/accessConfig/detail/edit/${id}`);
// router.push({
// path: `/iot/link/accessConfig/detail/${id}`,
// query: { view: false },
// });
console.log(id);
visible.value = true;
};
const saveChange = (value: object) => {
visible.value = false;
current.value = {};
if (value) {
message.success('操作成功');
tableRef.value.reload();
}
};
/**
* 搜索
* @param params
*/
const handleSearch = (e: any) => {
params.value = e;
};
</script>
<style lang="less" scoped>
.tableCardDisabled {
width: 100%;
background: url('/images/access-config-diaabled.png') no-repeat;
background-size: 100% 100%;
}
.tableCardEnabled {
width: 100%;
background: url('/images/access-config-enabled.png') no-repeat;
background-size: 100% 100%;
}
.card-item-content {
min-height: 100px;
.card-item-content-title-a {
// color: #000 !important;
font-weight: 700;
font-size: 18px;
overflow: hidden; //
text-overflow: ellipsis; //
white-space: nowrap; //
}
.card-item-content-box {
min-height: 50px;
}
.card-item-content-text {
color: rgba(0, 0, 0, 0.75);
font-size: 12px;
overflow: hidden; //
text-overflow: ellipsis; //
white-space: nowrap; //
}
}
</style>

7
src/views/link/Protocol/typings.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
import type { BaseItem } from '@/utils/typings';
type ProtocolItem = {
state: number;
type: string;
configuration: Record<string, any>;
} & BaseItem;

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> <template #img>
<slot name="img"> <slot name="img">
<img <img
style="width: 80px; height: 80px"
:src=" :src="
getLogo(slotProps.type, slotProps.provider) getLogo(slotProps.type, slotProps.provider)
" "

View File

@ -181,11 +181,18 @@
</a-form-item> </a-form-item>
</a-form-item> </a-form-item>
<a-form-item label="权限"> <a-form-item label="权限">
<PermissChoose :first-width="3" max-height="350px" v-model:value="form.data.permissions" /> <PermissChoose
:first-width="3"
max-height="350px"
v-model:value="form.data.permissions"
/>
</a-form-item> </a-form-item>
</a-form> </a-form>
<a-button type="primary" @click="clickSave" v-loading="saveLoading" <a-button
type="primary"
@click="form.clickSave"
v-loading="form.saveLoading"
>保存</a-button >保存</a-button
> >
</a-card> </a-card>
@ -215,14 +222,17 @@ import {
addMenuInfo_api, addMenuInfo_api,
} from '@/api/system/menu'; } from '@/api/system/menu';
//
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const routeParams = { const routeParams = {
id: route.params.id === ':id' ? undefined : (route.params.id as string), id: route.params.id === ':id' ? undefined : (route.params.id as string),
...route.query, ...route.query,
url: route.query.basePath, url: route.query.basePath,
parentId: route.query.pid,
}; };
//
const basicFormRef = ref<FormInstance>(); const basicFormRef = ref<FormInstance>();
const permissFormRef = ref<FormInstance>(); const permissFormRef = ref<FormInstance>();
const form = reactive({ const form = reactive({
@ -238,15 +248,18 @@ const form = reactive({
indirectMenus: [], indirectMenus: [],
...routeParams, ...routeParams,
} as formType, } as formType,
treeData: [], // treeData: [], //
assetsType: [] as assetType[], // assetsType: [] as assetType[], //
saveLoading: false,
init: () => { init: () => {
// //
routeParams.id && routeParams.id &&
getMenuInfo_api(routeParams.id).then((resp) => { getMenuInfo_api(routeParams.id).then((resp: any) => {
form.data = resp.result as formType form.data = {
...(resp.result as formType),
accessSupport: resp.result.accessSupport.value,
};
}); });
// //
getMenuTree_api({ paging: false }).then((resp: any) => { getMenuTree_api({ paging: false }).then((resp: any) => {
@ -260,9 +273,52 @@ const form = reactive({
})); }));
}); });
}, },
clickSave: () => {
if (!basicFormRef || !permissFormRef) return;
Promise.all([
basicFormRef.value?.validate(),
permissFormRef.value?.validate(),
])
.then(() => {
const api = routeParams.id ? saveMenuInfo_api : addMenuInfo_api;
form.saveLoading = true;
const accessSupportValue = form.data.accessSupport;
const params = {
...form.data,
accessSupport: {
value: accessSupportValue,
label:
accessSupportValue === 'unsupported'
? '不支持'
: accessSupportValue === 'support'
? '支持'
: '间接控制',
},
};
api(params)
.then((resp: any) => {
if (resp.status === 200) {
message.success('操作成功!');
//
if (!routeParams.id) {
router.push(
`/system/Menu/detail/${resp.result.id}`,
);
routeParams.id = resp.result.id;
form.init();
}
} else {
message.error('操作失败!');
}
})
.finally(() => (form.saveLoading = false));
})
.catch((err) => {});
},
}); });
form.init(); form.init();
//
const ChooseIconRef = ref<any>(null); const ChooseIconRef = ref<any>(null);
const dialog = { const dialog = {
openDialog: () => { openDialog: () => {
@ -272,36 +328,6 @@ const dialog = {
form.data.icon = typeStr || form.data.icon; form.data.icon = typeStr || form.data.icon;
}, },
}; };
const saveLoading = ref<boolean>(false);
const clickSave = () => {
if (!basicFormRef || !permissFormRef) return;
Promise.all([
basicFormRef.value?.validate(),
permissFormRef.value?.validate(),
])
.then(() => {
const api = routeParams.id ? saveMenuInfo_api : addMenuInfo_api;
saveLoading.value = true;
api(form.data)
.then((resp: any) => {
if (resp.status === 200) {
message.success('操作成功!');
//
if (!routeParams.id) {
router.push(
`/system/Menu/detail/${resp.result.id}`,
);
routeParams.id = resp.result.id;
form.init();
}
} else {
message.error('操作失败!');
}
})
.finally(() => (saveLoading.value = false));
})
.catch((err) => {});
};
type formType = { type formType = {
name: string; name: string;
@ -314,6 +340,7 @@ type formType = {
accessSupport: string; accessSupport: string;
assetType: string | undefined; assetType: string | undefined;
indirectMenus: any[]; indirectMenus: any[];
parentId?: string;
}; };
type assetType = { type assetType = {

View File

@ -4,13 +4,14 @@
ref="tableRef" ref="tableRef"
:columns="table.columns" :columns="table.columns"
model="TABLE" model="TABLE"
:dataSource="table.data" :dataSource="table.tableData"
noPagination
> >
<template #headerTitle> <template #headerTitle>
<a-button <a-button
type="primary" type="primary"
style="margin-right: 10px" style="margin-right: 10px"
@click="() => dialog.openDialog()" @click="() => dialog.openDialog('新增')"
><plus-outlined />新增</a-button ><plus-outlined />新增</a-button
> >
</template> </template>
@ -21,7 +22,7 @@
<a-button <a-button
style="padding: 0" style="padding: 0"
type="link" type="link"
@click="() => dialog.openDialog(slotProps)" @click="() => dialog.openDialog('编辑', slotProps)"
> >
<edit-outlined /> <edit-outlined />
</a-button> </a-button>
@ -31,9 +32,9 @@
<a-button <a-button
style="padding: 0" style="padding: 0"
type="link" type="link"
@click="() => dialog.openDialog(slotProps)" @click="() => dialog.openDialog('查看', slotProps)"
> >
<edit-outlined /> <search-outlined />
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@ -41,7 +42,7 @@
title="确认删除" title="确认删除"
ok-text="确定" ok-text="确定"
cancel-text="取消" cancel-text="取消"
:disabled="slotProps.status" @confirm="table.clickDel(slotProps)"
> >
<a-tooltip> <a-tooltip>
<template #title>删除</template> <template #title>删除</template>
@ -55,7 +56,11 @@
</JTable> </JTable>
<div class="dialog"> <div class="dialog">
<ButtonAddDialog ref="dialogRef" @confirm="dialog.confirm" /> <ButtonAddDialog
ref="dialogRef"
@confirm="dialog.confirm"
:menu-info="menuInfo"
/>
</div> </div>
</div> </div>
</template> </template>
@ -63,12 +68,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
EditOutlined, EditOutlined,
SearchOutlined,
DeleteOutlined, DeleteOutlined,
PlusOutlined, PlusOutlined,
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import ButtonAddDialog from '../components/ButtonAddDialog.vue'; import ButtonAddDialog from '../components/ButtonAddDialog.vue';
import { getMenuInfo_api } from '@/api/system/menu'; import { getMenuInfo_api, saveMenuInfo_api } from '@/api/system/menu';
import { message } from 'ant-design-vue';
// //
const route = useRoute(); const route = useRoute();
@ -81,11 +88,15 @@ const routeParams = {
const dialogRef = ref<any>(null); const dialogRef = ref<any>(null);
const dialog = { const dialog = {
// //
openDialog: (row?: object) => { openDialog: (mode: string, row?: object) => {
dialogRef.value && dialogRef.value.openDialog(row); dialogRef.value && dialogRef.value.openDialog(mode, { ...row });
},
confirm: () => {
table.getList();
}, },
confirm: () => {},
}; };
// -
const menuInfo = ref<any>({});
// //
const table = reactive({ const table = reactive({
columns: [ columns: [
@ -114,13 +125,27 @@ const table = reactive({
width: 240, width: 240,
}, },
], ],
data: [] as tableDataItem[], tableData: [] as tableDataItem[],
getList: () => { getList: () => {
routeParams.id && routeParams.id &&
getMenuInfo_api(routeParams.id).then((resp: any) => { getMenuInfo_api(routeParams.id).then((resp: any) => {
table.data = resp.result.buttons as tableDataItem[]; menuInfo.value = resp.result;
table.tableData = resp.result.buttons as tableDataItem[];
}); });
}, },
clickDel: (row: tableDataItem) => {
const buttons = menuInfo.value.buttons.filter(
(item: tableDataItem) => item.id !== row.id,
);
const params = {
...menuInfo.value,
buttons,
};
saveMenuInfo_api(params).then(() => {
message.success('操作成功');
table.getList();
});
},
}); });
table.getList(); table.getList();
type tableDataItem = { type tableDataItem = {

View File

@ -17,6 +17,11 @@ const activeKey = ref('basic');
<style lang="less" scoped> <style lang="less" scoped>
.menu-detail-container { .menu-detail-container {
:deep(.ant-tabs-nav) {
background-color: #fff;
padding-left: 24px;
margin-bottom: 0;
}
.ant-tabs-tabpane { .ant-tabs-tabpane {
background-color: #f0f2f5; background-color: #f0f2f5;
padding: 24px; padding: 24px;

View File

@ -0,0 +1,16 @@
<template>
<div class="setting-container">
<a-card>
<h5>
<info-circle-outlined />
<span>基于系统源代码中的菜单数据配置系统菜单</span>
</h5>
</a-card>
</div>
</template>
<script setup lang="ts">
import { InfoCircleOutlined } from '@ant-design/icons-vue';
</script>
<style scoped></style>

View File

@ -1,11 +1,15 @@
<template> <template>
<a-modal <a-modal
v-model:visible="dialog.visible" v-model:visible="dialog.visible"
title="新增" :title="form.mode"
width="660px" width="660px"
@ok="dialog.handleOk" @ok="dialog.handleOk"
:maskClosable="false"
cancelText="取消"
okText="确定"
:confirmLoading="dialog.loading"
> >
<a-form :model="form.data" class="basic-form"> <a-form :model="form.data" class="basic-form" ref="formRef">
<a-form-item <a-form-item
label="编码" label="编码"
name="id" name="id"
@ -14,7 +18,10 @@
{ max: 64, message: '最多可输入64个字符' }, { max: 64, message: '最多可输入64个字符' },
]" ]"
> >
<a-input v-model:value="form.data.id" /> <a-input
v-model:value="form.data.id"
:disabled="form.mode !== '新增'"
/>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="名称" label="名称"
@ -24,20 +31,43 @@
{ max: 64, message: '最多可输入64个字符' }, { max: 64, message: '最多可输入64个字符' },
]" ]"
> >
<a-input v-model:value="form.data.name" /> <a-input
</a-form-item> v-model:value="form.data.name"
<a-form-item label="权限"> :disabled="form.mode === '查看'"
<PermissChoose
:first-width="8"
max-height="350px"
v-model:value="form.data.permissions"
/> />
</a-form-item> </a-form-item>
<a-form-item
label="权限"
name="permissions"
:rules="[
{
required: true,
message: '请选择权限',
validator: form.checkPermission,
},
]"
>
<!-- <a-form-item-rest>
<PermissChoose
:first-width="8"
max-height="350px"
v-model:value="form.data.permissions"
:disabled="form.mode === '查看'"
/>
</a-form-item-rest> -->
<PermissChoose
:first-width="8"
max-height="350px"
v-model:value="form.data.permissions"
:disabled="form.mode === '查看'"
/>
</a-form-item>
<a-form-item label="说明" name="describe"> <a-form-item label="说明" name="describe">
<a-textarea <a-textarea
v-model:value="form.data.describe" v-model:value="form.data.describe"
:rows="4" :rows="4"
placeholder="请输入说明" placeholder="请输入说明"
:disabled="form.mode === '查看'"
/> />
</a-form-item> </a-form-item>
</a-form> </a-form>
@ -45,18 +75,56 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { FormInstance, message } from 'ant-design-vue';
import { Rule } from 'ant-design-vue/es/form';
import PermissChoose from '../components/PermissChoose.vue'; import PermissChoose from '../components/PermissChoose.vue';
import { saveMenuInfo_api } from '@/api/system/menu';
const props = defineProps<{
menuInfo: {
buttons: formType[];
id: string;
};
}>();
const emits = defineEmits(['confirm']); const emits = defineEmits(['confirm']);
const dialog = reactive({ const dialog = reactive({
visible: false, visible: false,
loading: false,
handleOk: () => { handleOk: () => {
dialog.changeVisible(); props.menuInfo.id && formRef.value &&
formRef.value
.validate()
.then(() => {
const buttons = toRaw(props.menuInfo.buttons);
const button = buttons.find(
(item) => item.id === form.data.id,
);
if (button) {
Object.entries(form.data).forEach(([key, value]) => {
button[key] = value;
});
} else buttons.push({ ...form.data });
const params = {
...props.menuInfo,
buttons,
};
dialog.loading = true;
saveMenuInfo_api(params)
.then((resp) => {
dialog.changeVisible();
message.success('操作成功')
emits('confirm');
})
.finally(() => (dialog.loading = false));
})
.catch(() => {});
}, },
changeVisible: (formValue?: formType, show?: boolean) => { changeVisible: (mode?: string, formValue?: formType) => {
dialog.visible = show === undefined ? !dialog.visible : show; dialog.visible = !dialog.visible;
form.data = formValue || { ...initForm }; form.data = { ...initForm, ...formValue };
console.log(1111111111, form.data); form.mode = mode || '';
formRef.value && formRef.value.clearValidate();
}, },
}); });
const initForm = { const initForm = {
@ -65,8 +133,14 @@ const initForm = {
permissions: [], permissions: [],
describe: '', describe: '',
} as formType; } as formType;
const formRef = ref<FormInstance>();
const form = reactive({ const form = reactive({
data: { ...initForm }, data: { ...initForm },
mode: '',
checkPermission: async (_rule: Rule, value: string[]) => {
if (!value || value.length < 1) return Promise.reject('请选择权限');
return Promise.resolve();
},
}); });
// //

View File

@ -6,6 +6,7 @@
allowClear allowClear
placeholder="请输入权限名称" placeholder="请输入权限名称"
@input="search.search" @input="search.search"
:disabled="props.disabled"
/> />
<div class="permission-table"> <div class="permission-table">
@ -24,6 +25,7 @@
v-model:checked="rowItem.checkAll" v-model:checked="rowItem.checkAll"
:indeterminate="rowItem.indeterminate" :indeterminate="rowItem.indeterminate"
@change="() => permission.selectAllOpions(rowItem)" @change="() => permission.selectAllOpions(rowItem)"
:disabled="props.disabled"
> >
{{ rowItem.name }} {{ rowItem.name }}
</a-checkbox> </a-checkbox>
@ -33,6 +35,7 @@
v-model:value="rowItem.checkedList" v-model:value="rowItem.checkedList"
:options="rowItem.options" :options="rowItem.options"
@change="((checkValue:string[])=>permission.selectOption(rowItem, checkValue))" @change="((checkValue:string[])=>permission.selectOption(rowItem, checkValue))"
:disabled="props.disabled"
/> />
</a-col> </a-col>
</a-row> </a-row>
@ -43,11 +46,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { exportPermission_api } from '@/api/system/permission'; import { exportPermission_api } from '@/api/system/permission';
import { Form } from 'ant-design-vue';
Form.useInjectFormItemContext()
const props = defineProps<{ const props = defineProps<{
value: any[]; value: any[];
firstWidth: number; firstWidth: number;
maxHeight: string; maxHeight: string;
disabled?: boolean;
}>(); }>();
const emits = defineEmits(['update:value']); const emits = defineEmits(['update:value']);
const searchValue = ref<string>(''); const searchValue = ref<string>('');
@ -127,18 +132,14 @@ const permission = reactive({
emits('update:value', newProp); emits('update:value', newProp);
}, },
makeList: (checkedValue: any[], sourceList: any[]): permissionType[] => { makeList: (checkedValue: any[], sourceList: any[]): permissionType[] => {
console.log(checkedValue);
const result = sourceList.map((item) => { const result = sourceList.map((item) => {
const checked = checkedValue.find( const checked = checkedValue?.find(
(checkedItem) => checkedItem.permission === item.id, (checkedItem) => checkedItem.permission === item.id,
); );
const options = item.actions.map((actionItem: any) => ({ const options = item.actions.map((actionItem: any) => ({
label: actionItem.name, label: actionItem.name,
value: actionItem.action, value: actionItem.action,
})); }));
console.log(item, checked);
return { return {
id: item.id, id: item.id,
name: item.name, name: item.name,
@ -146,7 +147,7 @@ const permission = reactive({
checkAll: checkAll:
(checked && (checked &&
item.actions && item.actions &&
checked?.actions.length === item.actions.length) || checked.actions.length === item.actions.length) ||
false, false,
indeterminate: indeterminate:
(checked && (checked &&
@ -179,6 +180,9 @@ type paramsType = {
<style lang="less" scoped> <style lang="less" scoped>
.permission-choose-container { .permission-choose-container {
.ant-input-affix-wrapper {
border-color: #d9d9d9 !important;
}
.permission-table { .permission-table {
margin-top: 12px; margin-top: 12px;
font-size: 14px; font-size: 14px;

View File

@ -16,7 +16,7 @@
style="margin-right: 10px" style="margin-right: 10px"
><plus-outlined />新增</a-button ><plus-outlined />新增</a-button
> >
<a-button>菜单实例</a-button> <a-button @click="router.push('/system/Menu/Setting')">菜单实例</a-button>
</template> </template>
<template #createTime="slotProps"> <template #createTime="slotProps">
{{ moment(slotProps.createTime).format('YYYY-MM-DD HH:mm:ss') }} {{ moment(slotProps.createTime).format('YYYY-MM-DD HH:mm:ss') }}
@ -30,7 +30,7 @@
type="link" type="link"
@click="table.toDetails(slotProps)" @click="table.toDetails(slotProps)"
> >
<search-outlined /> <search-outlined />
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip>
@ -38,9 +38,9 @@
<a-button <a-button
style="padding: 0" style="padding: 0"
type="link" type="link"
@click="table.toDetails(slotProps)" @click="table.addChildren(slotProps)"
> >
<plus-circle-outlined /> <plus-circle-outlined />
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@ -49,7 +49,6 @@
ok-text="确定" ok-text="确定"
cancel-text="取消" cancel-text="取消"
@confirm="table.clickDel(slotProps)" @confirm="table.clickDel(slotProps)"
:disabled="slotProps.status"
> >
<a-tooltip> <a-tooltip>
<template #title>删除</template> <template #title>删除</template>
@ -65,13 +64,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { getMenuTree_api } from '@/api/system/menu'; import { getMenuTree_api, delMenuInfo_api } from '@/api/system/menu';
import { import {
SearchOutlined, SearchOutlined,
DeleteOutlined, DeleteOutlined,
PlusOutlined, PlusOutlined,
PlusCircleOutlined PlusCircleOutlined,
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import moment from 'moment'; import moment from 'moment';
const router = useRouter(); const router = useRouter();
@ -228,22 +228,32 @@ const table = reactive({
status: resp.status, status: resp.status,
}; };
}, },
addChildren: (row: any) => {
console.log(row);
router.push(
`/system/Menu/detail/:id?pid=${row.id}&basePath=${
row.url || ''
}&sortIndex=${row.children.length + 1}`,
);
},
// //
toDetails: (row: any) => { toDetails: (row: any) => {
router.push( router.push(
`/system/Menu/detail/${row.id || ':id'}?pid=${ `/system/Menu/detail/${row.id || ':id'}?pid=${
row.pid || '' row.pid || ''
}&basePath=${row.basePath || ''}&sortIndex=${table.total}`, }&basePath=${row.url|| ''}&sortIndex=${table.total}`,
); );
}, },
// //
clickDel: (row: any) => { clickDel: (row: any) => {
// delPermission_api(row.id).then((resp: any) => { console.log(row.id);
// if (resp.status === 200) {
// tableRef.value?.reload(); delMenuInfo_api(row.id).then((resp: any) => {
// message.success('!'); if (resp.status === 200) {
// } tableRef.value?.reload();
// }); message.success('操作成功!');
}
});
}, },
// //
refresh: () => { refresh: () => {
@ -252,4 +262,8 @@ const table = reactive({
}); });
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped>
.menu-container {
padding: 24px;
}
</style>