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

This commit is contained in:
xieyonghong 2023-01-12 09:34:31 +08:00
commit 0890350cf2
34 changed files with 22626 additions and 4475 deletions

7
components.d.ts vendored
View File

@ -7,24 +7,29 @@ export {}
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
export interface GlobalComponents { export interface GlobalComponents {
AAlert: typeof import('ant-design-vue/es')['Alert']
ABadge: typeof import('ant-design-vue/es')['Badge'] ABadge: typeof import('ant-design-vue/es')['Badge']
AButton: typeof import('ant-design-vue/es')['Button'] AButton: typeof import('ant-design-vue/es')['Button']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox'] ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup'] ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
ACol: typeof import('ant-design-vue/es')['Col'] ACol: typeof import('ant-design-vue/es')['Col']
ACollapse: typeof import('ant-design-vue/es')['Collapse']
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker'] ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADivider: typeof import('ant-design-vue/es')['Divider'] ADivider: typeof import('ant-design-vue/es')['Divider']
AEmpty: typeof import('ant-design-vue/es')['Empty'] AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form'] AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem'] AFormItem: typeof import('ant-design-vue/es')['FormItem']
AInput: typeof import('ant-design-vue/es')['Input'] AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AModal: typeof import('ant-design-vue/es')['Modal']
APagination: typeof import('ant-design-vue/es')['Pagination'] APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm'] APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARow: typeof import('ant-design-vue/es')['Row'] ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select'] ASelect: typeof import('ant-design-vue/es')['Select']
ASpace: typeof import('ant-design-vue/es')['Space'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASpin: typeof import('ant-design-vue/es')['Spin'] ASpin: typeof import('ant-design-vue/es')['Spin']
ASwitch: typeof import('ant-design-vue/es')['Switch'] ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table'] ATable: typeof import('ant-design-vue/es')['Table']

12783
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,6 @@
import { get } from '@/utils/request' import { get, post } from '@/utils/request'
// 三方应用账户信息 // 三方应用账户信息
export const applicationInfo = (code: string) => get(`/application/sso/bind-code/${code}`) export const applicationInfo = (code: string): any => get(`/application/sso/bind-code/${code}`)
// 立即绑定
export const bindAccount = (code: string): any => post(`/application/sso/me/bind/${code}`)

25
src/api/initHome.ts Normal file
View File

@ -0,0 +1,25 @@
import server from '@/utils/request';
// 更新全部菜单
export const updateMenus = (data: any) => server
// 添加角色
export const addRole = (data: any) => server.post(`/role`)
//更新权限菜单
export const getRoleMenu = (id: string) => server.get(`/menu/role/${id}/_grant/tree`)
//更新权限菜单
export const updateRoleMenu = (id: string, data: any) => server.put(`/menu/role/${id}/_grant`)
// 记录初始化
export const saveInit = () => server.post(`/user/settings/init`,{ init: true },)
//获取初始化
export const getInit = () => server.get(`/user/settings/init`)
// 获取当前系统权限信息
export const getSystemPermission = () =>server.get(`/system/resources/permission`)
// 保存基础信息
export const save = (data?: any) => server.post('/system/config/scope/_save')

View File

@ -0,0 +1,33 @@
import server from '@/utils/request';
export const getProviders = () => server.get(`/gateway/device/providers`);
export const detail = (id) => server.get(`/gateway/device/${id}`);
export const getNetworkList = (networkType, data, params) =>
server.get(
`/network/config/${networkType}/_alive?include=${params.include}`,
data,
);
export const getProtocolList = (transport, params) =>
server.get(`/protocol/supports/${transport ? transport : ''}`, params);
export const getConfigView = (id, transport) =>
server.get(`/protocol/${id}/transport/${transport}`);
export const getChildConfigView = (id) =>
server.get(`/protocol/${id}/transports`);
export const save = (data) => server.post(`/gateway/device`, data);
export const update = (data) => server.patch(`/gateway/device`, data);
export const list = (data) =>
server.post(`/gateway/device/detail/_query`, data);
export const undeploy = (id) => server.post(`/gateway/device/${id}/_shutdown`);
export const deploy = (id) => server.post(`/gateway/device/${id}/_startup`);
export const del = (id) => server.remove(`/gateway/device/${id}`);

View File

@ -2,17 +2,10 @@
<div class="card"> <div class="card">
<div <div
class="card-warp" class="card-warp"
:class="{ :class="{ active: active ? 'active' : ''}"
hover: maskShow ? 'hover' : '',
active: actived ? 'active' : '',
}"
@click="handleClick" @click="handleClick"
> >
<div <div class="card-content">
class="card-content"
@mouseenter="setMaskShow(true)"
@mouseleave="setMaskShow(false)"
>
<a-row> <a-row>
<a-col :span="6"> <a-col :span="6">
<!-- 图片 --> <!-- 图片 -->
@ -27,7 +20,7 @@
</a-row> </a-row>
<!-- 勾选 --> <!-- 勾选 -->
<div v-if="actived" class="checked-icon"> <div v-if="active" class="checked-icon">
<div> <div>
<CheckOutlined /> <CheckOutlined />
</div> </div>
@ -47,21 +40,12 @@
></BadgeStatus> ></BadgeStatus>
</div> </div>
</div> </div>
<!-- 遮罩层 -->
<div
v-if="showMask"
class="card-mask"
:class="maskShow ? 'show' : ''"
>
<slot name="mask"></slot>
</div>
</div> </div>
</div> </div>
<!-- 按钮 --> <!-- 按钮 -->
<slot name="bottom-tool"> <slot name="bottom-tool">
<div v-if="showTool" class="card-tools"> <div v-if="showTool && actions && actions.length" class="card-tools">
<div <div
v-for="item in actions" v-for="item in actions"
:key="item.key" :key="item.key"
@ -70,18 +54,32 @@
delete: item.key === 'delete', delete: item.key === 'delete',
}" }"
> >
<a-tooltip v-if="item.disabled === true"> <a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
<template #title>{{ item.message }}</template> <template v-if="item.key === 'delete'">
<a-button :disabled="item.disabled"> <a-button :disabled="item.disabled">
<template #icon><SearchOutlined /></template> <DeleteOutlined />
<span>{{ item.label }}</span>
</a-button> </a-button>
</a-tooltip> </template>
<template v-else>
<a-button v-else :disabled="item.disabled"> <a-button :disabled="item.disabled">
<template #icon><SearchOutlined /></template> <AIcon :type="item.icon" />
<span>{{ item.label }}</span> <span>{{ item.text }}</span>
</a-button> </a-button>
</template>
</a-popconfirm>
<template v-else>
<template v-if="item.key === 'delete'">
<a-button :disabled="item.disabled">
<DeleteOutlined />
</a-button>
</template>
<template v-else>
<a-button :disabled="item.disabled">
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</a-button>
</template>
</template>
</div> </div>
</div> </div>
</slot> </slot>
@ -89,18 +87,26 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SearchOutlined, CheckOutlined } from '@ant-design/icons-vue'; import { SearchOutlined, CheckOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import BadgeStatus from '@/components/BadgeStatus/index.vue'; import BadgeStatus from '@/components/BadgeStatus/index.vue';
import { StatusColorEnum } from '@/utils/consts.ts'; import { StatusColorEnum } from '@/utils/consts.ts';
import type { ActionsType } from '@/components/Table/index.vue'
import { PropType } from 'vue';
type EmitProps = { type EmitProps = {
(e: 'update:modelvalue', data: string | number): void; // (e: 'update:modelValue', data: Record<string, any>): void;
(e: 'actived', data: boolean): void; (e: 'click', data: Record<string, any>): void;
}; };
type TableActionsType = Partial<ActionsType>
const emit = defineEmits<EmitProps>(); const emit = defineEmits<EmitProps>();
const props = defineProps({ const props = defineProps({
value: {
type: Object as PropType<Record<string, any>>,
default: () => {}
},
showStatus: { showStatus: {
type: Boolean, type: Boolean,
default: true, default: true,
@ -109,10 +115,7 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
showMask: {
type: Boolean,
default: true,
},
statusText: { statusText: {
type: String, type: String,
default: '正常', default: '正常',
@ -125,21 +128,17 @@ const props = defineProps({
type: Object, type: Object,
}, },
actions: { actions: {
type: Array as any, type: Array as PropType<TableActionsType[]>,
default: () => [], default: () => [],
}, },
active: {
type: Boolean,
default: false
}
}); });
const maskShow = ref(false);
const actived = ref(false);
const setMaskShow = (val: boolean) => {
maskShow.value = val;
};
const handleClick = () => { const handleClick = () => {
actived.value = !actived.value; emit('click', props.value);
emit('actived', actived.value);
}; };
</script> </script>

View File

@ -15,9 +15,13 @@
</div> </div>
</div> </div>
<div class="jtable-content"> <div class="jtable-content">
<!-- <div class="jtable-alert"> <div class="jtable-alert" v-if="rowSelection.selectedRowKeys && rowSelection.selectedRowKeys.length">
<a-alert message="Info Text" type="info" /> <a-alert :message="'已选择' + rowSelection.selectedRowKeys.length + '项'" type="info" :afterClose="handleAlertClose">
</div> --> <template #closeText>
<a>取消选择</a>
</template>
</a-alert>
</div>
<div v-if="_model === ModelEnum.CARD" class="jtable-card"> <div v-if="_model === ModelEnum.CARD" class="jtable-card">
<div <div
v-if="_dataSource.length" v-if="_dataSource.length"
@ -29,16 +33,7 @@
v-for="(item, index) in _dataSource" v-for="(item, index) in _dataSource"
:key="index" :key="index"
> >
<CardBox :actions="actions" v-bind="cardProps"> <slot name="card" v-bind="item" :index="index"></slot>
<template #img>
<slot name="img">
<img :src="getImage('/device-product.png')" />
</slot>
</template>
<template #content>
<slot name="cardContent" :item="item" :index="index"></slot>
</template>
</CardBox>
</div> </div>
</div> </div>
<div v-else> <div v-else>
@ -46,23 +41,21 @@
</div> </div>
</div> </div>
<div v-else> <div v-else>
<a-table :rowSelection="rowSelection" :columns="[..._columns]" :dataSource="_dataSource" :pagination="false" :scroll="{ x: 1366 }"> <a-table rowKey="id" :rowSelection="rowSelection" :columns="[..._columns]" :dataSource="_dataSource" :pagination="false" :scroll="{ x: 1366 }">
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'"> <!-- <template v-if="column.key === 'action'">
<a-space> <a-space>
<a-tooltip v-for="i in actions" :key="i.key" v-bind="i.tooltip"> <a-tooltip v-for="i in actions" :key="i.key" v-bind="i.tooltip">
<a-popconfirm v-if="i.popConfirm" v-bind="i.popConfirm"> <a-popconfirm v-if="i.popConfirm" v-bind="i.popConfirm">
<a> <a><AIcon :type="i.icon" /></a>
{{i.text}}
</a>
</a-popconfirm> </a-popconfirm>
<a v-else @click="i.onClick && i.onClick(record)"> <a v-else @click="i.onClick && i.onClick(record)">
{{i.text}} <AIcon :type="i.icon" />
</a> </a>
</a-tooltip> </a-tooltip>
</a-space> </a-space>
</template> </template> -->
<template v-else-if="column.scopedSlots"> <template v-if="column.scopedSlots">
<slot :name="column.key" :row="record"></slot> <slot :name="column.key" :row="record"></slot>
</template> </template>
</template> </template>
@ -88,12 +81,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { UnorderedListOutlined, AppstoreOutlined } from '@ant-design/icons-vue' import { UnorderedListOutlined, AppstoreOutlined } from '@ant-design/icons-vue'
import type { TableProps } from 'ant-design-vue/es/table' import type { TableProps, ColumnsType } from 'ant-design-vue/es/table'
import type { TooltipProps } from 'ant-design-vue/es/tooltip' import type { TooltipProps } from 'ant-design-vue/es/tooltip'
import type { PopconfirmProps } from 'ant-design-vue/es/popconfirm' import type { PopconfirmProps } from 'ant-design-vue/es/popconfirm'
import { Empty } from 'ant-design-vue' import { Empty } from 'ant-design-vue'
import { CSSProperties } from 'vue'; import { CSSProperties } from 'vue';
import { getImage } from '@/utils/comm';
enum ModelEnum { enum ModelEnum {
TABLE = 'TABLE', TABLE = 'TABLE',
@ -123,13 +115,17 @@ export interface ActionsType {
icon?: string; icon?: string;
} }
interface JTableProps extends TableProps{ export interface JColumnsProps extends ColumnsType{
scopedSlots?: boolean; // true: false:
}
export interface JTableProps extends TableProps{
request?: (params: Record<string, any> & { request?: (params: Record<string, any> & {
pageSize: number; pageSize: number;
pageIndex: number; pageIndex: number;
}) => Promise<Partial<RequestData>>; }) => Promise<Partial<RequestData>>;
cardBodyClass?: string; cardBodyClass?: string;
columns: Record<string, any>[]; columns: JColumnsProps;
params?: Record<string, any> & { params?: Record<string, any> & {
pageSize: number; pageSize: number;
pageIndex: number; pageIndex: number;
@ -147,6 +143,11 @@ const props = withDefaults(defineProps<JTableProps>(), {
request: undefined, request: undefined,
}) })
// emit
const emit = defineEmits<{
(e: 'cancelSelect'): void
}>()
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); //
@ -155,16 +156,17 @@ const _dataSource = ref<Record<string, any>[]>([])
const pageIndex = ref<number>(0) const pageIndex = ref<number>(0)
const pageSize = ref<number>(6) const pageSize = ref<number>(6)
const total = ref<number>(0) const total = ref<number>(0)
const _columns = ref<Record<string, any>[]>([]) const _columns = ref<Record<string, any>[]>([...props.columns])
const loading = ref<boolean>(true) const loading = ref<boolean>(true)
//
// const slotColumns = computed(() => props.columns.filter((item) => item.scopedSlots))
// //
// //
const modelChange = (type: keyof typeof ModelEnum) => { const modelChange = (type: keyof typeof ModelEnum) => {
_model.value = type _model.value = type
} }
// /**
* 请求数据
*/
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) {
@ -194,7 +196,9 @@ const handleSearch = async (_params?: Record<string, any>) => {
loading.value = false loading.value = false
} }
/**
* 页码变化
*/
const pageChange = (page: number, size: number) => { const pageChange = (page: number, size: number) => {
handleSearch({ handleSearch({
...props.params, ...props.params,
@ -203,22 +207,30 @@ const pageChange = (page: number, size: number) => {
}) })
} }
// alert
const handleAlertClose = () => {
emit('cancelSelect')
}
// watchEffect(() => {
// if(Array.isArray(props.actions) && props.actions.length) {
// _columns.value = [...props.columns,
// {
// title: '',
// key: 'action',
// fixed: 'right',
// width: 250
// }
// ]
// } else {
// _columns.value = [...props.columns]
// }
// })
watchEffect(() => { watchEffect(() => {
if(Array.isArray(props.actions) && props.actions.length) {
_columns.value = [...props.columns,
{
title: '操作',
key: 'action',
fixed: 'right',
width: 250
}
]
} else {
_columns.value = [...props.columns]
}
handleSearch(props.params) handleSearch(props.params)
}) })
// TODO
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@ -250,11 +262,13 @@ watchEffect(() => {
} }
} }
.jtable-content { .jtable-content {
.jtable-alert {
margin-bottom: 16px;
}
.jtable-card { .jtable-card {
.jtable-card-items { .jtable-card-items {
display: grid; display: grid;
grid-gap: 26px; grid-gap: 26px;
// grid-template-columns: repeat(4, 1fr);
.jtable-card-item { .jtable-card-item {
display: flex; display: flex;
} }

View File

@ -1,45 +0,0 @@
.jtable-body {
width: 100%;
padding: 0 24px 24px;
background-color: white;
.jtable-body-header {
padding: 16px 0;
display: flex;
justify-content: space-between;
align-items: center;
.jtable-body-header-right {
display: flex;
gap: 8px;
.jtable-setting-item {
color: rgba(0, 0, 0, 0.75);
font-size: 16px;
cursor: pointer;
&:hover {
color: @primary-color-hover;
}
&.active {
color: @primary-color-active;
}
}
}
}
.jtable-content {
.jtable-card {
.jtable-card-items {
display: grid;
grid-gap: 26px;
// grid-template-columns: repeat(4, 1fr);
.jtable-card-item {
display: flex;
}
}
}
}
.jtable-pagination {
position: absolute;
right: 24px;
bottom: 24px;
}
}

View File

@ -1,168 +0,0 @@
import { UnorderedListOutlined, AppstoreOutlined } from '@ant-design/icons-vue'
import styles from './index.module.less'
import { Pagination, Table, Empty } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue/es/table'
enum ModelEnum {
TABLE = 'TABLE',
CARD = 'CARD',
}
export declare type RequestData = {
code: string;
result: {
data: any[] | undefined;
pageIndex: number;
pageSize: number;
total: number;
};
status: number;
} & Record<string, any>;
interface JTableProps extends TableProps{
request: (params: Record<string, any> & {
pageSize: number;
pageIndex: number;
}) => Promise<Partial<RequestData>>;
cardBodyClass: string;
}
const JTable = defineComponent<JTableProps>({
name: 'JTable',
slots: [
'headerTitle', // 顶部左边插槽
'cardRender', // 卡片内容
],
emits: [
'modelChange', // 切换卡片和表格
],
props: {
cardBodyClass: '',
request: undefined,
columns: []
} as any,
setup(props ,{ slots, emit }){
const model = ref<keyof typeof ModelEnum>(ModelEnum.CARD); // 模式切换
const column = ref<number>(3);
console.log(props.columns, props.request)
const dataSource = ref<any[]>([
{
key: '1',
name: '胡彦斌',
age: 32,
address: '西湖区湖底公园1号',
},
{
key: '2',
name: '胡彦祖1',
age: 42,
address: '西湖区湖底公园1号',
},
{
key: '3',
name: '胡彦斌',
age: 32,
address: '西湖区湖底公园1号',
},
{
key: '4',
name: '胡彦祖1',
age: 42,
address: '西湖区湖底公园1号',
},
{
key: '5',
name: '胡彦斌',
age: 32,
address: '西湖区湖底公园1号',
},
{
key: '6',
name: '胡彦祖1',
age: 42,
address: '西湖区湖底公园1号',
},
])
// 请求数据
onMounted(() => {
})
return () => <div class={styles["jtable-body"]}>
<div class={styles["jtable-body-header"]}>
<div class={styles["jtable-body-header-left"]}>
{/* 顶部左边插槽 */}
{slots.headerTitle && slots.headerTitle()}
</div>
<div class={styles["jtable-body-header-right"]}>
{/* <Space> */}
<div class={[styles["jtable-setting-item"], ModelEnum.CARD === model.value ? styles['active'] : '']} onClick={() => {
model.value = ModelEnum.CARD
}}>
<AppstoreOutlined />
</div>
<div class={[styles["jtable-setting-item"], ModelEnum.TABLE === model.value ? styles['active'] : '']} onClick={() => {
model.value = ModelEnum.TABLE
}}>
<UnorderedListOutlined />
</div>
{/* </Space> */}
</div>
</div>
{/* content */}
<div class={styles['jtable-content']}>
{
model.value === ModelEnum.CARD ?
<div class={styles['jtable-card']}>
{
dataSource.value.length ?
<div
class={styles['jtable-card-items']}
style={{gridTemplateColumns: `repeat(${column.value}, 1fr)`}}
>
{
dataSource.value.map(item => slots.cardRender ?
<div class={[styles['jtable-card-item'], props.cardBodyClass]}>{slots.cardRender(item)}</div>
: null)
}
</div> :
<div><Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /></div>
}
</div> :
<div>
<Table
dataSource={dataSource.value}
columns={props.columns}
pagination={false}
/>
</div>
}
</div>
{/* 分页 */}
{
dataSource.value.length &&
<div class={styles['jtable-pagination']}>
<Pagination
size="small"
total={50}
showTotal={(total) => {
const min = 1
const max = 1
return `${min} - ${max} 条/总共 ${total}`
}}
onChange={() => {
}}
onShowSizeChange={() => {
}}
/>
</div>
}
</div>
}
})
export default JTable

View File

@ -63,8 +63,17 @@ export default [
path: '/link/certificate/detail/add', path: '/link/certificate/detail/add',
component: () => import('@/views/link/Certificate/Detail/index.vue') component: () => import('@/views/link/Certificate/Detail/index.vue')
}, },
{
path: '/link/accessConfig',
component: () => import('@/views/link/AccessConfig/index.vue')
},
{ {
path: '/link/accessConfig/detail/add', path: '/link/accessConfig/detail/add',
component: () => import('@/views/link/AccessConfig/Detail/index.vue') component: () => import('@/views/link/AccessConfig/Detail/index.vue')
}, },
// 初始化
{
path: '/init-home',
component: () => import('@/views/init-home/index.vue')
},
] ]

View File

@ -4,7 +4,7 @@
<div class="content"> <div class="content">
<div class="title">第三方账户绑定</div> <div class="title">第三方账户绑定</div>
<!-- 已登录-绑定三方账号 --> <!-- 已登录-绑定三方账号 -->
<template v-if="false"> <template v-if="!token">
<div class="info"> <div class="info">
<a-card style="width: 280px"> <a-card style="width: 280px">
<template #title> <template #title>
@ -28,14 +28,21 @@
</div> </div>
</template> </template>
<div class="info-body"> <div class="info-body">
<img :src="getImage('/bind/wechat-webapp.png')" /> <img
:src="
accountInfo?.avatar ||
getImage('/bind/wechat-webapp.png')
"
/>
<p>用户名-</p> <p>用户名-</p>
<p>名称微信昵称</p> <p>名称{{ accountInfo?.name || '-' }}</p>
</div> </div>
</a-card> </a-card>
</div> </div>
<div class="btn"> <div class="btn">
<a-button type="primary">立即绑定</a-button> <a-button type="primary" @click="handleBind"
>立即绑定</a-button
>
</div> </div>
</template> </template>
<!-- 未登录-绑定三方账号 --> <!-- 未登录-绑定三方账号 -->
@ -74,23 +81,25 @@
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="验证码" label="验证码"
v-bind="validateInfos.captcha" v-bind="validateInfos.verifyCode"
> >
<a-input <a-input
v-model:value="formData.captcha" v-model:value="formData.verifyCode"
placeholder="请输入验证码" placeholder="请输入验证码"
> >
<template #addonAfter> <template #addonAfter>
<span style="cursor: pointer"> <img
图形验证码 :src="captcha.base64"
</span> @click="getCode"
style="cursor: pointer"
/>
</template> </template>
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-button <a-button
type="primary" type="primary"
@click="handleSubmit" @click="handleLoginBind"
style="width: 100%" style="width: 100%"
> >
登录并绑定账户 登录并绑定账户
@ -105,32 +114,58 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { getImage } from '@/utils/comm'; import { getImage, LocalStore } from '@/utils/comm';
import { TOKEN_KEY } from '@/utils/variable';
import { Form } from 'ant-design-vue'; import { Form } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { applicationInfo } from '@/api/bind'; import { applicationInfo, bindAccount } from '@/api/bind';
import { code, authLogin } from '@/api/login';
const useForm = Form.useForm; const useForm = Form.useForm;
interface formData { interface formData {
username: string; username: string;
password: string; password: string;
captcha: string; verifyCode: string;
} }
const token = computed(() => LocalStore.get(TOKEN_KEY));
//
const getUrlCode = () => {
const url = new URLSearchParams(window.location.href);
return url.get('code') as string;
};
// //
const accountInfo = ref({
avatar: '',
name: '',
});
const getAppInfo = async () => { const getAppInfo = async () => {
const code: string = '73ab60c88979a1475963a5dde31e374b'; const code = getUrlCode();
const res = await applicationInfo(code); const res = await applicationInfo(code);
console.log('getAppInfo: ', res); accountInfo.value = res?.result?.result;
}; };
getAppInfo(); getAppInfo();
// /**
* 立即绑定
*/
const handleBind = async () => {
const code = getUrlCode();
const res = await bindAccount(code);
console.log('bindAccount: ', res);
message.success('绑定成功');
goRedirect();
setTimeout(() => window.close(), 1000);
};
// -
const formData = ref<formData>({ const formData = ref<formData>({
username: '', username: '',
password: '', password: '',
captcha: '', verifyCode: '',
}); });
const formRules = ref({ const formRules = ref({
username: [ username: [
@ -145,7 +180,7 @@ const formRules = ref({
message: '请输入密码', message: '请输入密码',
}, },
], ],
captcha: [ verifyCode: [
{ {
required: true, required: true,
message: '请输入验证码', message: '请输入验证码',
@ -158,17 +193,40 @@ const { resetFields, validate, validateInfos } = useForm(
formRules.value, formRules.value,
); );
/**
* 获取图形验证码
*/
const captcha = ref({
base64: '',
key: '',
});
const getCode = async () => {
const res: any = await code();
captcha.value = res.result;
};
getCode();
/** /**
* 登录并绑定账户 * 登录并绑定账户
*/ */
const handleSubmit = () => { const handleLoginBind = () => {
validate() validate()
.then(() => { .then(async () => {
console.log('toRaw:', toRaw(formData.value)); const code = getUrlCode();
console.log('formData.value:', formData.value); const params = {
...formData.value,
verifyKey: captcha.value.key,
bindCode: code,
expires: 3600000,
};
const res = await authLogin(params);
console.log('res: ', res);
message.success('登录成功');
goRedirect();
setTimeout(() => window.close(), 1000);
}) })
.catch((err) => { .catch((err) => {
console.log('error', err); getCode();
}); });
}; };

View File

@ -11,8 +11,8 @@
status="disable" status="disable"
:statusNames="{ disable: StatusColorEnum.error }" :statusNames="{ disable: StatusColorEnum.error }"
statusText="正常" statusText="正常"
:showMask="false"
:actions="actions" :actions="actions"
v-model="data"
> >
<template #img> <template #img>
<img :src="getImage('/device-product.png')" /> <img :src="getImage('/device-product.png')" />
@ -63,6 +63,10 @@ const actions = ref([
label: '删除', label: '删除',
}, },
]); ]);
const data = ref({
id: 123
})
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -18,34 +18,66 @@
dataIndex: 'classifiedName', dataIndex: 'classifiedName',
key: 'classifiedName', key: 'classifiedName',
}, },
{
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
scopedSlots: true
}
]" ]"
:actions="actions" :actions="actions"
:request="request" :request="request"
:rowSelection="rowSelection" :rowSelection="{
selectedRowKeys: _selectedRowKeys,
onChange: onSelectChange
}"
@cancelSelect="cancelSelect"
> >
<template #headerTitle> <template #headerTitle>
<a-button type="primary">新增</a-button> <a-button type="primary">新增</a-button>
</template> </template>
<template #cardContent="slotProps"> <template #card="slotProps">
<h3>{{slotProps.item.name}}</h3> <CardBox :value="slotProps" @click="handleClick" :actions="actions" v-bind="slotProps" :active="_selectedRowKeys.includes(slotProps.id)">
<a-row> <template #img>
<a-col :span="12"> <slot name="img">
<div class="card-item-content-text"> <img :src="getImage('/device-product.png')" />
设备类型 </slot>
</div> </template>
<div>直连设备</div> <template #content>
</a-col> <h3>{{slotProps.name}}</h3>
<a-col :span="12"> <a-row>
<div class="card-item-content-text"> <a-col :span="12">
产品名称 <div class="card-item-content-text">
</div> 设备类型
<div>测试固定地址</div> </div>
</a-col> <div>直连设备</div>
</a-row> </a-col>
<a-col :span="12">
<div class="card-item-content-text">
产品名称
</div>
<div>测试固定地址</div>
</a-col>
</a-row>
</template>
</CardBox>
</template> </template>
<template #id="slotProps"> <template #id="slotProps">
<a>{{slotProps.row.id}}</a> <a>{{slotProps.row.id}}</a>
</template> </template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip v-for="i in actions" :key="i.key" v-bind="i.tooltip">
<a-popconfirm v-if="i.popConfirm" v-bind="i.popConfirm">
<a-button 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)">
<AIcon :type="i.icon" />
</a-button>
</a-tooltip>
</a-space>
</template>
</JTable> </JTable>
</div> </div>
</template> </template>
@ -54,7 +86,6 @@
import server from "@/utils/request"; import server from "@/utils/request";
import type { ActionsType } from '@/components/Table/index.vue' import type { ActionsType } from '@/components/Table/index.vue'
import { getImage } from '@/utils/comm'; import { getImage } from '@/utils/comm';
import type { TableProps, TableColumnType } from 'ant-design-vue';
const request = (data: any) => server.post(`/device-product/_query`, data) const request = (data: any) => server.post(`/device-product/_query`, data)
const actions: ActionsType[] = [ const actions: ActionsType[] = [
@ -65,30 +96,50 @@ const actions: ActionsType[] = [
tooltip: { tooltip: {
title: '编辑' title: '编辑'
}, },
// component: <UnorderedListOutlined /> icon: 'icon-rizhifuwu'
},
{
key: 'import',
// disabled: true,
text: "导入",
tooltip: {
title: '导入'
},
icon: 'icon-xiazai'
}, },
{ {
key: 'delete', key: 'delete',
disabled: true, // disabled: true,
text: "删除", text: "删除",
tooltip: { tooltip: {
title: '删除' title: '删除'
}, },
popConfirm: { popConfirm: {
title: '确认删除?' title: '确认删除?'
} },
} }
] ]
const rowSelection: TableProps['rowSelection'] = { const _selectedRowKeys = ref<string[]>([])
onChange: (selectedRowKeys: string[], selectedRows: any[]) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); const onSelectChange = (keys: string[]) => {
}, _selectedRowKeys.value = [...keys]
getCheckboxProps: (record: any) => ({ }
disabled: record.name === 'Disabled User',
name: record.name, const cancelSelect = () => {
}), _selectedRowKeys.value = []
}; }
const handleClick = (dt: any) => {
// _selectedRowKeys.value = [dt.id] //
// _selectedRowKeys.value = [..._selectedRowKeys.value, dt.id] //
if(_selectedRowKeys.value.includes(dt.id)) {
const _index = _selectedRowKeys.value.findIndex(i => i === dt.id)
_selectedRowKeys.value.splice(_index, 1)
} else {
_selectedRowKeys.value = [..._selectedRowKeys.value, dt.id]
}
}
</script> </script>

View File

@ -30,18 +30,10 @@
重置 重置
</a-button> </a-button>
</div> </div>
<a-table <JTable :columns="columns">
:columns="columns"
:data-source="tableData" </JTable>
:row-selection="{
onChange: (selectedRowKeys, selectedRows) =>
(selectItem = selectedRows),
type: 'radio',
}"
>
</a-table>
<template #footer> <template #footer>
<a-button key="back" @click="visible = false">取消</a-button> <a-button key="back" @click="visible = false">取消</a-button>
<a-button key="submit" type="primary" @click="handleOk" <a-button key="submit" type="primary" @click="handleOk"
@ -90,9 +82,7 @@ const productList = ref<[productItem] | []>([]);
const getOptions = () => { const getOptions = () => {
productList.value = []; productList.value = [];
}; };
const clickSearch = ()=>{ const clickSearch = () => {};
}
const clickReset = () => { const clickReset = () => {
Object.entries(form.value).forEach(([prop]) => { Object.entries(form.value).forEach(([prop]) => {
form.value[prop] = ''; form.value[prop] = '';
@ -102,27 +92,27 @@ const clickReset = () => {
// //
const columns = [ const columns = [
{ {
name: 'deviceId', title: '设备Id',
dataIndex: 'deviceId', dataIndex: 'deviceId',
key: 'deviceId', key: 'deviceId',
}, },
{ {
name: 'deviceName', title: '设备名称',
dataIndex: 'deviceName', dataIndex: 'deviceName',
key: 'deviceName', key: 'deviceName',
}, },
{ {
name: 'productName', title: '产品名称',
dataIndex: 'productName', dataIndex: 'productName',
key: 'productName', key: 'productName',
}, },
{ {
name: 'createTime', title: '注册时间',
dataIndex: 'createTime', dataIndex: 'createTime',
key: 'createTime', key: 'createTime',
}, },
{ {
name: 'status', title: '状态',
dataIndex: 'status', dataIndex: 'status',
key: 'status', key: 'status',
}, },

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,5 @@
import type { Rule } from 'ant-design-vue/es/form';
import type { UploadChangeParam, UploadProps } from 'ant-design-vue';
/** 初始化数据提交表单 */ /** 初始化数据提交表单 */
export interface modalState { export interface modalState {
host: string; // 本地地址 host: string; // 本地地址
@ -19,7 +21,7 @@ export interface formState {
} }
/** /**
* logo上传表 * logo上传表
*/ */
export interface logoState { export interface logoState {
logoValue: string; logoValue: string;
@ -29,5 +31,7 @@ export interface logoState {
inBackground: boolean; inBackground: boolean;
iconValue: string; iconValue: string;
backValue: string; backValue: string;
handleChangeLogo:(url: string) => void handleChangeLogo:(info: UploadChangeParam ) => void
} beforeBackUpload:(file: UploadProps['beforeUpload']) => void
changeBackUpload:(info: UploadChangeParam ) => void
}

View File

@ -30,6 +30,8 @@
> >
<a-input <a-input
v-model:value="form.title" v-model:value="form.title"
:maxlength="64"
placeholder="请输入系统名称"
/> />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
@ -69,6 +71,7 @@
</template> </template>
<a-input <a-input
v-model:value="form.apikey" v-model:value="form.apikey"
placeholder="请输入高德API Key"
/> />
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
@ -91,6 +94,7 @@
v-model:value=" v-model:value="
form.basePath form.basePath
" "
placeholder="请输入高德API Key"
/> />
</a-form-item> </a-form-item>
<a-row :gutter="24" :span="24"> <a-row :gutter="24" :span="24">
@ -224,7 +228,7 @@
class="upload-image-content-logo" class="upload-image-content-logo"
> >
<div <div
class="upload-image" class="upload-image-icon"
v-if=" v-if="
iconValue iconValue
" "
@ -293,7 +297,14 @@
<div <div
class="upload-image-border-back" class="upload-image-border-back"
> >
<a-upload> <a-upload
@beforeUpload="
beforeBackUpload
"
@change="
changeBackUpload
"
>
<div <div
class="upload-image-content-back" class="upload-image-content-back"
> >
@ -338,26 +349,6 @@
</div> </div>
</div> </div>
</a-upload> </a-upload>
<!-- <div
v-if="
logoValue &&
logoLoading
"
>
<div
class="upload-loading-mask"
>
<LoadingOutlined
v-if="
loading
"
style="
font-size: 28px;
"
/>
</div>
</div> -->
</div> </div>
</div> </div>
<div class="upload-tips"> <div class="upload-tips">
@ -387,7 +378,7 @@
/> />
</div> </div>
<div class="menu-info"> <div class="menu-info">
<b>系统初始化xxx个菜单</b> <b>系统初始化{{ count }}个菜单</b>
<div> <div>
初始化后的菜单可在菜单管理页面进行维护管理 初始化后的菜单可在菜单管理页面进行维护管理
</div> </div>
@ -645,7 +636,12 @@
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</a-spin> </a-spin>
<a-button type="primary" class="btn-style">确定</a-button> <a-button
type="primary"
class="btn-style"
@click="submitData"
>确定</a-button
>
</div> </div>
</div> </div>
</div> </div>
@ -659,12 +655,17 @@ import {
LoadingOutlined, LoadingOutlined,
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { ROLEKEYS, RoleData } from './data/RoleData'; import { ROLEKEYS, RoleData } from './data/RoleData';
import type { FormInstance } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form'; import type { Rule } from 'ant-design-vue/es/form';
import { Form } from 'ant-design-vue'; import type {
import type { UploadChangeParam } from 'antd/lib/upload/interface'; FormInstance,
UploadChangeParam,
UploadProps,
} from 'ant-design-vue';
import { modalState, formState, logoState } from './data/interface'; import { modalState, formState, logoState } from './data/interface';
const formRef = ref<FormInstance>(); import BaseMenu from './data/baseMenu';
import { getSystemPermission, save } from '@/api/initHome';
const formRef = ref();
const menuRef = ref();
const formBasicRef = ref<FormInstance>(); const formBasicRef = ref<FormInstance>();
/** /**
* 表单数据 * 表单数据
@ -673,7 +674,7 @@ const form = reactive<formState>({
title: '', title: '',
headerTheme: 'light', headerTheme: 'light',
apikey: '', apikey: '',
basePath: '', basePath: `${window.location.origin}/api`,
logo: '', logo: '',
icon: '', icon: '',
rulesFrom: { rulesFrom: {
@ -688,14 +689,14 @@ const form = reactive<formState>({
{ {
required: true, required: true,
message: '请选择主题色', message: '请选择主题色',
trigger: '[blur, change]', trigger: 'blur',
}, },
], ],
basePath: [ basePath: [
{ {
required: true, required: true,
message: '请输入base-path', message: '请输入base-path',
trigger: 'blur, change', trigger: 'blur',
}, },
], ],
}, },
@ -762,7 +763,6 @@ const ModalForm = reactive<modalState>({
{ {
required: true, required: true,
validator: validateNumber, validator: validateNumber,
trigger: 'change', trigger: 'change',
}, },
], ],
@ -815,17 +815,17 @@ const cancel = () => {
* 提交图片 * 提交图片
*/ */
const logoData = reactive<logoState>({ const logoData = reactive<logoState>({
logoValue: '', logoValue: '/public/logo.png',
logoLoading: false, logoLoading: false,
inLogo: false, inLogo: false,
inIcon: false, inIcon: false,
inBackground: false, inBackground: false,
iconValue: '', iconValue: '/public/favicon.ico',
backValue: '', backValue: '/public/images/login.png',
/** /**
* 图片上传改变事件 * 图片上传改变事件
*/ */
handleChangeLogo: (info: UploadChangeParam) => { handleChangeLogo: (info) => {
if (info.file.status === 'uploading') { if (info.file.status === 'uploading') {
logoData.logoLoading = true; logoData.logoLoading = true;
} }
@ -835,6 +835,14 @@ const logoData = reactive<logoState>({
logoData.logoValue = info.file.response?.result; logoData.logoValue = info.file.response?.result;
} }
}, },
/**
* 背景图片上传之前
*/
beforeBackUpload: (file) => {},
/**
* 背景图片发生改变
*/
changeBackUpload: (info) => {},
}); });
const { const {
@ -847,6 +855,79 @@ const {
backValue, backValue,
handleChangeLogo, handleChangeLogo,
} = toRefs(logoData); } = toRefs(logoData);
/**
* 提交基础表单
*/
const basicData = reactive({
/**
* 提交基础表单数据
*/
saveBasicInfo: async () => {},
});
/**
* 获取菜单数据
*/
const menuDatas = reactive({
count: 0,
/**
* 获取当前系统权限信息
*/
getSystemPermissionData: async () => {
const resp = await getSystemPermission();
if (resp.status === 200) {
const newTree = menuDatas.filterMenu(
resp.result.map((item: any) => JSON.parse(item).id),
BaseMenu,
);
const _count = menuDatas.menuCount(newTree);
menuDatas.count = _count;
console.log(menuDatas.count, 'menuDatas.count');
}
},
/**
* 过滤菜单
*/
filterMenu: (permissions: string[], menus: any[]) => {
return menus.filter((item) => {
let isShow = false;
if (item.showPage && item.showPage.length) {
isShow = item.showPage.every((pItem: any) => {
return permissions.includes(pItem);
});
}
if (item.children) {
item.children = menuDatas.filterMenu(
permissions,
item.children,
);
}
return isShow || !!item.children?.length;
});
},
/**
* 计算菜单数量
*/
menuCount: (menus: any[]) => {
return menus.reduce((pre, next) => {
let _count = 1;
if (next.children) {
_count = menuDatas.menuCount(next.children);
}
return pre + _count;
}, 0);
},
});
const { count } = toRefs(menuDatas);
/**
* 初始化
*/
menuDatas.getSystemPermissionData();
/**
* 提交所有数据
*/
const submit = () => {};
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.page-container { .page-container {
@ -999,6 +1080,13 @@ const {
background-position: 50%; background-position: 50%;
background-size: cover; background-size: cover;
} }
.upload-image-icon {
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-position: 50%;
background-size: inherit;
}
.upload-image-mask { .upload-image-mask {
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@ -1,21 +1,30 @@
const MetworkTypeMapping = new Map();
MetworkTypeMapping.set('websocket-server', 'WEB_SOCKET_SERVER');
MetworkTypeMapping.set('http-server-gateway', 'HTTP_SERVER');
MetworkTypeMapping.set('udp-device-gateway', 'UDP');
MetworkTypeMapping.set('coap-server-gateway', 'COAP_SERVER');
MetworkTypeMapping.set('mqtt-client-gateway', 'MQTT_CLIENT');
MetworkTypeMapping.set('mqtt-server-gateway', 'MQTT_SERVER');
MetworkTypeMapping.set('tcp-server-gateway', 'TCP_SERVER');
const ProcotoleMapping = new Map(); const ProtocolMapping = new Map();
ProcotoleMapping.set('websocket-server', 'WebSocket'); ProtocolMapping.set('websocket-server', 'WebSocket');
ProcotoleMapping.set('http-server-gateway', 'HTTP'); ProtocolMapping.set('http-server-gateway', 'HTTP');
ProcotoleMapping.set('udp-device-gateway', 'UDP'); ProtocolMapping.set('udp-device-gateway', 'UDP');
ProcotoleMapping.set('coap-server-gateway', 'CoAP'); ProtocolMapping.set('coap-server-gateway', 'CoAP');
ProcotoleMapping.set('mqtt-client-gateway', 'MQTT'); ProtocolMapping.set('mqtt-client-gateway', 'MQTT');
ProcotoleMapping.set('mqtt-server-gateway', 'MQTT'); ProtocolMapping.set('mqtt-server-gateway', 'MQTT');
ProcotoleMapping.set('tcp-server-gateway', 'TCP'); ProtocolMapping.set('tcp-server-gateway', 'TCP');
ProcotoleMapping.set('child-device', ''); ProtocolMapping.set('child-device', '');
ProtocolMapping.set('OneNet', 'HTTP');
ProtocolMapping.set('Ctwing', 'HTTP');
ProtocolMapping.set('modbus-tcp', 'MODBUS_TCP');
ProtocolMapping.set('opc-ua', 'OPC_UA');
ProtocolMapping.set('edge-child-device', 'EdgeGateway');
ProtocolMapping.set('official-edge-gateway', 'MQTT');
const NetworkTypeMapping = new Map();
NetworkTypeMapping.set('websocket-server', 'WEB_SOCKET_SERVER');
NetworkTypeMapping.set('http-server-gateway', 'HTTP_SERVER');
NetworkTypeMapping.set('udp-device-gateway', 'UDP');
NetworkTypeMapping.set('coap-server-gateway', 'COAP_SERVER');
NetworkTypeMapping.set('mqtt-client-gateway', 'MQTT_CLIENT');
NetworkTypeMapping.set('mqtt-server-gateway', 'MQTT_SERVER');
NetworkTypeMapping.set('tcp-server-gateway', 'TCP_SERVER');
NetworkTypeMapping.set('official-edge-gateway', 'MQTT_SERVER');
const descriptionList = { const descriptionList = {
'udp-device-gateway': 'udp-device-gateway':
@ -96,4 +105,4 @@ const columnsHTTP = [
}, },
] ]
export { MetworkTypeMapping, ProcotoleMapping, descriptionList, columnsMQTT, columnsHTTP }; export { NetworkTypeMapping, ProtocolMapping, descriptionList, columnsMQTT, columnsHTTP };

View File

@ -1,62 +1,93 @@
<template> <template>
<a-card :bordered="false"> <a-spin :spinning="loading">
<TitleComponent data="自定义设备接入"></TitleComponent> <a-card :bordered="false">
<div> <div v-if="type">
<a-row :gutter="[24, 24]"> <Provider
<a-col :span="12" v-for="item in items" :key="item.id"> @onClick="goProviders"
<div class="provider"> :dataSource="dataSource"
<div class="box"> ></Provider>
<div class="left"> </div>
<div class="images"> <div v-else>
<img :src="backMap.get(item.id)" /> <div v-if="!id"><a @click="goBack">返回</a></div>
</div> <AccessNetwork :provider="provider" :data="data" />
<div class="context"> </div>
<div class="title">{{ item.name }}</div> </a-card>
<div class="desc"> </a-spin>
<a-tooltip :title="item.description">
{{ item.description || '' }}
</a-tooltip>
</div>
</div>
</div>
<div class="right">
<a-button
type="primary"
@click="goProviders(item)"
>接入</a-button
>
</div>
</div>
</div>
</a-col>
</a-row>
</div>
</a-card>
</template> </template>
<script lang="ts" setup name="AccessConfigDetail"> <script lang="ts" setup name="AccessConfigDetail">
import { getImage } from '@/utils/comm'; import { getImage } from '@/utils/comm';
import TitleComponent from '@/components/TitleComponent/index.vue'; import TitleComponent from '@/components/TitleComponent/index.vue';
import AccessNetwork from '../components/Network.vue';
import Provider from '../components/Provider/index.vue';
import { getProviders, detail } from '@/api/link/accessConfig';
const items = [ // const router = useRouter();
{ id: 'mqtt-server-gateway', name: '测试1', description: '测试1' }, const route = useRoute();
{ id: 'websocket-server', name: '测试2', description: '测试' },
{ id: 'coap-server-gateway', name: '测试3', description: '测试' },
];
const backMap = new Map(); const id = route.query.id;
backMap.set('mqtt-server-gateway', getImage('/access/mqtt.png'));
backMap.set('websocket-server', getImage('/access/websocket.png'));
backMap.set('coap-server-gateway', getImage('/access/coap.png'));
backMap.set('tcp-server-gateway', getImage('/access/tcp.png'));
backMap.set('child-device', getImage('/access/child-device.png'));
backMap.set('http-server-gateway', getImage('/access/http.png'));
backMap.set('udp-device-gateway', getImage('/access/udp.png'));
backMap.set('mqtt-client-gateway', getImage('/access/mqtt-broke.png'));
const goProviders = (value: object) => { const dataSource = ref([]);
console.log(111, value); const type = ref(false);
const loading = ref(true);
const provider = ref({});
const data = ref({});
const goProviders = (param: object) => {
provider.value = param;
type.value = false;
}; };
const goBack = () => {
provider.value = {};
type.value = true;
};
const queryProviders = async () => {
const resp = await getProviders();
if (resp.status === 200) {
dataSource.value = resp.result.filter(
(item) =>
item.channel === 'network' || item.channel === 'child-device',
);
}
};
const getProvidersData = async () => {
if (id) {
getProviders().then((response) => {
if (response.status === 200) {
dataSource.value = response.result.filter(
(item) =>
item.channel === 'network' ||
item.channel === 'child-device',
);
detail(id).then((resp) => {
if (resp.status === 200) {
const dt = response.result.find(
(item) => item?.id === resp.result.provider,
);
provider.value = dt;
data.value = resp.result;
type.value = false;
}
});
loading.value = false;
} else {
loading.value = false;
}
});
} else {
type.value = true;
queryProviders();
loading.value = false;
}
};
onMounted(() => {
loading.value = true;
getProvidersData();
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -0,0 +1,86 @@
<template>
<a-card hoverable :class="['card-render', checked === data.id ? 'checked' : '']" @click="checkedChange(data.id)">
<div class="title">
<a-tooltip placement="topLeft" :title="data.name">{{ data.name }}</a-tooltip>
</div>
<slot name="other"></slot>
<div class="desc">
<a-tooltip placement="topLeft" :title="data.description">{{ data.description }}</a-tooltip>
</div>
<div class="checked-icon">
<div><a-icon type="check" /></div>
</div>
</a-card>
</template>
<script>
export default {
name: "AccessCard",
props: ['data', 'checked'],
methods: {
checkedChange(id){
this.$emit('checkedChange', id)
}
}
};
</script>
<style lang="less" scoped>
.card-render {
width: 100%;
overflow: hidden;
background: url("/public/images/access/access.png") no-repeat;
background-size: 100% 100%;
min-height: 105px;
.title {
width: calc(100% - 88px);
overflow: hidden;
font-weight: 800;
white-space: nowrap;
text-overflow: ellipsis;
}
.desc {
width: 100%;
margin-top: 10px;
color: rgba(0, 0, 0, 0.55);
font-weight: 400;
font-size: 13px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.checked-icon {
position: absolute;
right: -22px;
bottom: -22px;
z-index: 2;
display: none;
width: 44px;
height: 44px;
color: #fff;
background-color: red;
background-color: #2f54eb;
transform: rotate(-45deg);
> div {
position: relative;
height: 100%;
transform: rotate(45deg);
font-size: 12px;
padding: 4px 0 0 6px;
}
}
&.checked {
position: relative;
color: #2f54eb;
border-color: #2f54eb;
.checked-icon {
display: block;
}
}
}
</style>

View File

@ -0,0 +1,733 @@
<template>
<div style="margin-top: 10px">
<a-steps :current="stepCurrent">
<a-step v-for="item in steps" :key="item" :title="item" />
</a-steps>
<div class="steps-content">
<div class="steps-box" v-if="current === 0">
<div class="alert">
<a-icon type="info-circle" style="margin-right: 10px" />
选择与设备通信的网络组件
</div>
<div class="search">
<a-input-search
allowClear
placeholder="请输入"
style="width: 300px"
@search="networkSearch"
/>
<a-button type="primary" @click="addNetwork">新增</a-button>
</div>
<div class="card-item">
<a-row :gutter="[24, 24]" v-if="networkList.length > 0">
<a-col
:span="8"
v-for="item in networkList"
:key="item.id"
>
<access-card
@checkedChange="checkedChange"
:checked="networkCurrent"
:data="{
...item,
description: item.description
? item.description
: descriptionList[provider.id],
}"
>
<div slot="other" class="other">
<a-tooltip placement="topLeft">
<div
slot="title"
v-if="
(item.addresses || []).length >
1
"
>
<div
v-for="i in item.addresses ||
[]"
:key="i.address"
class="item"
>
<a-badge
:color="
i.health === -1
? 'red'
: 'green'
"
/>{{ i.address }}
</div>
</div>
<div
v-for="i in (
item.addresses || []
).slice(0, 1)"
:key="i.address"
class="item"
>
<a-badge
:color="
i.health === -1
? 'red'
: 'green'
"
:text="i.address"
/>
<span
v-if="
(item.addresses || [])
.length > 1
"
>...</span
>
</div>
</a-tooltip>
</div>
</access-card>
</a-col>
</a-row>
<a-empty v-else description="暂无数据" />
</div>
</div>
<div class="steps-box" v-else-if="current === 1">
<div class="alert">
<a-icon type="info-circle" style="margin-right: 10px" />
使用选择的消息协议对网络组件通信数据进行编解码认证等操作
</div>
<div class="search">
<a-input-search
allowClear
placeholder="请输入"
style="width: 300px"
@search="procotolSearch"
/>
<a-button type="primary" @click="addProcotol"
>新增</a-button
>
</div>
<div class="card-item">
<a-row :gutter="[24, 24]" v-if="procotolList.length > 0">
<a-col
:span="8"
v-for="item in procotolList"
:key="item.id"
>
<access-card
@checkedChange="procotolChange"
:checked="procotolCurrent"
:data="item"
>
</access-card>
</a-col>
</a-row>
<a-empty v-else description="暂无数据" />
</div>
</div>
<div class="steps-box" v-else>
<div class="card-last">
<a-row :gutter="[24, 24]">
<a-col :span="12">
<title-component data="基本信息" />
<div>
<a-form :form="form" layout="vertical">
<a-form-item label="名称">
<a-input
allowClear
placeholder="请输入名称"
v-decorator="[
'name',
{
initialValue: data.name,
rules: [
{
required: true,
message:
'请输入名称!',
},
],
},
]"
/>
</a-form-item>
<a-form-item label="说明">
<a-textarea
placeholder="请输入说明"
:rows="4"
v-decorator="[
'description',
{
initialValue:
data.description,
},
]"
/>
</a-form-item>
</a-form>
</div>
</a-col>
<a-col :span="12">
<div class="config-right">
<div class="config-right-item">
<div class="config-right-item-title">
接入方式
</div>
<div class="config-right-item-context">
{{ provider.name }}
</div>
<div class="config-right-item-context">
{{ provider.description }}
</div>
</div>
<div class="config-right-item">
<div class="config-right-item-title">
消息协议
</div>
<div class="config-right-item-context">
{{
procotolList.find(
(i) => i.id === procotolCurrent,
).name
}}
</div>
<div
class="config-right-item-context"
v-if="config.document"
>
{{ config.document }}
</div>
</div>
<div
class="config-right-item"
v-if="
networkList.find(
(i) => i.id === networkCurrent,
) &&
(
networkList.find(
(i) => i.id === networkCurrent,
).addresses || []
).length > 0
"
>
<div class="config-right-item-title">
网络组件
</div>
<div
v-for="i in (networkList.find(
(i) => i.id === networkCurrent,
) &&
networkList.find(
(i) => i.id === networkCurrent,
).addresses) ||
[]"
:key="i.address"
>
<a-badge
:color="
i.health === -1
? 'red'
: 'green'
"
:text="i.address"
/>
</div>
</div>
<div
class="config-right-item"
v-if="
config.routes &&
config.routes.length > 0
"
>
<div class="config-right-item-title">
{{
data.provider ===
'mqtt-server-gateway' ||
data.provider ===
'mqtt-client-gateway'
? 'topic'
: 'URL信息'
}}
</div>
<a-table
:pagination="false"
:rowKey="generateUUID()"
:data-source="config.routes || []"
bordered
:columns="columnsMQTT"
:scroll="{ y: 300 }"
>
<template
slot="stream"
slot-scope="text, record"
>
<span
v-if="
record.upstream &&
record.downstream
"
>上行下行</span
>
<span v-else-if="record.upstream"
>上行</span
>
<span v-else-if="record.downstream"
>下行</span
>
</template>
</a-table>
</div>
</div>
</a-col>
</a-row>
</div>
</div>
</div>
<div class="steps-action">
<a-button
v-if="[0, 1].includes(current)"
type="primary"
@click="next"
>
下一步
</a-button>
<a-button v-if="current === 2" type="primary" @click="save">
保存
</a-button>
<a-button v-if="current > 0" style="margin-left: 8px" @click="prev">
上一步
</a-button>
</div>
</div>
</template>
<script lang="ts" setup name="AccessNetwork">
import {
getNetworkList,
getProtocolList,
getConfigView,
save,
update,
getChildConfigView,
} from '@/api/link/accessConfig';
import {
descriptionList,
NetworkTypeMapping,
ProtocolMapping,
} from '../Detail/data';
import AccessCard from './AccessCard/index.vue';
import TitleComponent from '@/components/TitleComponent/index.vue';
import { message, Form } from 'ant-design-vue';
function generateUUID() {
var d = new Date().getTime();
if (
typeof performance !== 'undefined' &&
typeof performance.now === 'function'
) {
d += performance.now(); //use high-precision timer if available
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
/[xy]/g,
function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
},
);
}
const props = defineProps({
provider: {
type: Object,
default: () => {},
},
data: {
type: Object,
default: () => {},
},
});
const current = ref(0);
const stepCurrent = ref(0);
const steps = ref(['网络组件', '消息协议', '完成']);
const networkList = ref([]);
const procotolList = ref([]);
const allProcotolList = ref([]);
const networkCurrent = ref('');
const procotolCurrent = ref('');
let config = ref({});
let columnsMQTT = ref([]);
const form = reactive({
name: 'access',
description: '',
});
const queryNetworkList = async (id: string, params: object, data = {}) => {
console.log('queryNetworkList',NetworkTypeMapping.get(id), data, params);
const resp = await getNetworkList(NetworkTypeMapping.get(id), data, params);
if (resp.status === 200) {
networkList.value = resp.result;
}
};
// const queryProcotolList=async(id:string, params:object) =>{
const queryProcotolList = async (id: string, params = {}) => {
const resp = await getProtocolList(ProtocolMapping.get(id), {
...params,
'sorts[0].name': 'createTime',
'sorts[0].order': 'desc',
});
if (resp.status === 200) {
procotolList.value = resp.result;
allProcotolList.value = resp.result;
}
};
const addNetwork = () => {
// const url = this.$store.state.permission.routes['Link/Type/Detail']
const url = '/demo';
const tab = window.open(
`${window.location.origin + window.location.pathname}#${url}?type=${
NetworkTypeMapping.get(props.provider?.id) || ''
}`,
);
tab.onTabSaveSuccess = (value) => {
if (value.success) {
networkCurrent.value = value.result.id;
queryNetworkList(props.provider?.id, {
include: networkCurrent.value || '',
});
}
};
};
const addProcotol = () => {
// const url = this.$store.state.permission.routes['Link/Protocol']
const url = '/demo';
const tab = window.open(
`${window.location.origin + window.location.pathname}#${url}?save=true`,
);
tab.onTabSaveSuccess = (value) => {
if (value.success) {
procotolCurrent.value = value.result?.id;
queryProcotolList(props.provider?.id);
}
};
};
const checkedChange = (id: string) => {
networkCurrent.value = id;
};
const networkSearch = (value: string) => {
console.log('networkSearch',
props.provider.id,
{
include: networkCurrent.value || '',
},
{
terms: [
{
column: 'name$LIKE',
value: `%${value}%`,
},
],
},
);
queryNetworkList(
props.provider.id,
{
include: networkCurrent.value || '',
},
{
terms: [
{
column: 'name$LIKE',
value: `%${value}%`,
},
],
},
);
};
const procotolChange = (id: string) => {
if (!props.data.id) {
procotolCurrent.value = id;
}
};
const procotolSearch = (value: string) => {
if (value) {
const list = allProcotolList.value.filter((i) => {
return (
i.name &&
i.name.toLocaleLowerCase().includes(value.toLocaleLowerCase())
);
});
procotolList.value = list;
} else {
procotolList.value = allProcotolList.value;
}
};
const saveData = () => {
form.validateFields(async (err, values) => {
if (!err) {
let resp = undefined;
if (props.data && props.data.id) {
resp = await update({
...props.data,
name: values.name,
description: values.description,
protocol: procotolCurrent.value,
channel: 'network', //
channelId: networkCurrent.value,
});
} else {
resp = await save({
name: values.name,
description: values.description,
provider: props.provider.id,
protocol: procotolCurrent.value,
transport:
props.provider?.id === 'child-device'
? 'Gateway'
: ProtocolMapping.get(props.provider.id),
channel: 'network', //
channelId: networkCurrent.value,
});
}
if (resp.status === 200) {
message.success('操作成功!');
//
if (window.onTabSaveSuccess) {
window.onTabSaveSuccess(resp);
setTimeout(() => window.close(), 300);
} else {
// this.$store.dispatch('jumpPathByKey', { key: MenuKeys['Link/AccessConfig'] })
}
}
}
});
};
const next = async () => {
if (current.value === 0) {
if (!networkCurrent.value) {
message.error('请选择网络组件!');
} else {
queryProcotolList(props.provider.id);
current.value -= current.value;
}
} else if (current.value === 1) {
if (!procotolCurrent.value) {
message.error('请选择消息协议!');
} else {
const resp =
props.provider.channel !== 'child-device'
? await getConfigView(
procotolCurrent.value,
ProtocolMapping.get(props.provider.id),
)
: await getChildConfigView(procotolCurrent.value);
if (resp.status === 200) {
config.value = resp.result;
current.value += current.value;
columnsMQTT = [
{
title: '分组',
dataIndex: 'group',
key: 'group',
ellipsis: true,
align: 'center',
width: 100,
customRender: (value, row, index) => {
const obj = {
children: value,
attrs: {},
};
const list = (config && config.routes) || [];
const arr = list.filter((res) => {
return res.group == row.group;
});
if (
index == 0 ||
list[index - 1].group !== row.group
) {
obj.attrs.rowSpan = arr.length;
} else {
obj.attrs.rowSpan = 0;
}
return obj;
},
},
{
title: 'topic',
dataIndex: 'topic',
key: 'topic',
ellipsis: true,
},
{
title: '上下行',
dataIndex: 'stream',
key: 'stream',
ellipsis: true,
align: 'center',
width: 100,
scopedSlots: { customRender: 'stream' },
},
{
title: '说明',
dataIndex: 'description',
key: 'description',
ellipsis: true,
},
];
}
}
}
};
const prev = () => {
const currentValue = current.value;
current.value -= currentValue;
};
onMounted(() => {
if (props.data && props.data.id) {
if (props.data.provider !== 'child-device') {
procotolCurrent.value = props.data.protocol;
current.value = 0;
networkCurrent.value = props.data.channelId;
console.log(11111111,props.provider.id, {
include: networkCurrent.value,
});
queryNetworkList(props.provider.id, {
include: networkCurrent.value,
});
procotolCurrent.value = props.data.protocol;
steps.value = ['网络组件', '消息协议', '完成'];
} else {
steps.value = ['消息协议', '完成'];
current.value = 1;
queryProcotolList(props.provider.id);
}
} else {
if (props.provider?.id) {
if (props.provider.channel !== 'child-device') {
console.log(3333333, props.provider.id, {
include: '',
});
queryNetworkList(props.provider.id, {
include: '',
});
steps.value = ['网络组件', '消息协议', '完成'];
current.value = 0;
} else {
console.log(444444,props.provider.id);
steps.value = ['消息协议', '完成'];
current.value = 1;
queryProcotolList(props.provider.id);
}
}
}
});
// watch(
// () => props.modelValue,
// (v) => {
// keystoreBase64.value = v;
// },
// {
// deep: true,
// immediate: true,
// },
// );
// watch: {
// current(val) {
// if (this.provider.channel !== 'child-device') {
// this.stepCurrent = val
// } else {
// this.stepCurrent = val - 1
// }
// },
// },
</script>
<style lang="less" scoped>
.steps-content {
margin: 20px;
}
.steps-box {
min-height: 400px;
.card-item {
padding-right: 5px;
max-height: 480px;
overflow-y: auto;
overflow-x: hidden;
}
.card-last {
padding-right: 5px;
max-height: 580px;
overflow-y: auto;
overflow-x: hidden;
}
}
.steps-action {
width: 100%;
margin-top: 24px;
margin-left: 20px;
}
.alert {
height: 40px;
padding-left: 10px;
color: rgba(0, 0, 0, 0.55);
line-height: 40px;
background-color: #f6f6f6;
}
.search {
display: flex;
margin: 15px 0;
justify-content: space-between;
}
.other {
width: 100%;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
.item {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.config-right {
padding: 20px;
color: rgba(0, 0, 0, 0.8);
background: rgba(0, 0, 0, 0.04);
.config-right-item {
margin-bottom: 10px;
.config-right-item-title {
width: 100%;
margin-bottom: 10px;
font-weight: 600;
}
.config-right-item-context {
margin: 5px 0;
color: rgba(0, 0, 0, 0.8);
}
}
}
</style>

View File

@ -0,0 +1,145 @@
<template>
<TitleComponent data="自定义设备接入"></TitleComponent>
<div>
<a-row :gutter="[24, 24]">
<a-col :span="12" v-for="item in dataSource" :key="item.id">
<div class="provider">
<div class="box">
<div class="left">
<div class="images">
<img :src="backMap.get(item.id)" />
</div>
<div class="context">
<div class="title">
{{ item.name }}
</div>
<div class="desc">
<a-tooltip :title="item.description">
{{ item.description || '' }}
</a-tooltip>
</div>
</div>
</div>
<div class="right">
<a-button type="primary" @click="click(item)"
>接入</a-button
>
</div>
</div>
</div>
</a-col>
</a-row>
</div>
</template>
<script lang="ts" setup name="AccessConfigProvider">
import TitleComponent from '@/components/TitleComponent/index.vue';
import { getImage } from '@/utils/comm';
const props = defineProps({
dataSource: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['onClick']);
const backMap = new Map();
backMap.set('mqtt-server-gateway', getImage('/access/mqtt.png'));
backMap.set('websocket-server', getImage('/access/websocket.png'));
backMap.set('modbus-tcp', getImage('/access/modbus.png'));
backMap.set('coap-server-gateway', getImage('/access/coap.png'));
backMap.set('tcp-server-gateway', getImage('/access/tcp.png'));
backMap.set('Ctwing', getImage('/access/ctwing.png'));
backMap.set('child-device', getImage('/access/child-device.png'));
backMap.set('opc-ua', getImage('/access/opc-ua.png'));
backMap.set('http-server-gateway', getImage('/access/http.png'));
backMap.set('fixed-media', getImage('/access/video-device.png'));
backMap.set('udp-device-gateway', getImage('/access/udp.png'));
backMap.set('OneNet', getImage('/access/onenet.png'));
backMap.set('gb28181-2016', getImage('/access/gb28181.png'));
backMap.set('mqtt-client-gateway', getImage('/access/mqtt-broke.png'));
backMap.set('edge-child-device', getImage('/access/child-device.png'));
backMap.set('official-edge-gateway', getImage('/access/edge.png'));
const click = (value: object) => {
emit('onClick', value);
};
</script>
<style lang="less" scoped>
.provider {
position: relative;
width: 100%;
padding: 20px;
background: url('/public/images/access/background.png') no-repeat;
background-size: 100% 100%;
border: 1px solid #e6e6e6;
&::before {
position: absolute;
top: 0;
left: 40px;
display: block;
width: 15%;
min-width: 64px;
height: 2px;
background-image: url('/public/images/access/rectangle.png');
background-repeat: no-repeat;
background-size: 100% 100%;
// border: 1px #8da1f4 solid;
// border-bottom-left-radius: 10%;
// border-bottom-right-radius: 10%;
content: ' ';
}
&:hover {
box-shadow: 0 0 24px rgba(#000, 0.1);
}
}
.box {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.left {
display: flex;
width: calc(100% - 70px);
.images {
width: 64px;
height: 64px;
img {
width: 100%;
}
}
.context {
width: calc(100% - 84px);
margin: 10px;
.title {
font-weight: 600;
}
.desc {
width: 100%;
margin-top: 10px;
overflow: hidden;
color: rgba(0, 0, 0, 0.55);
font-weight: 400;
font-size: 13px;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.right {
width: 70px;
}
}
</style>

View File

@ -1,11 +1,18 @@
<template> <template>
<a-button type="primary" @click="handlAdd">新增</a-button> <a-button type="primary" @click="handlAdd">新增</a-button>
</template> </template>
<script lang="ts" setup name="AccessConfigPage"> <script lang="ts" setup name="AccessConfigPage">
const router = useRouter();
const handlAdd = (e: any) => { // const handlAdd = () => {
console.log(111,e); // router.push({
// path: '/link/accessConfig/detail/add',
// query: {
// id: '1610475400026861568',
// },
// });
// };
const handlAdd = () => {
router.push('/link/accessConfig/detail/add');
} }
</script> </script>

View File

@ -64,16 +64,14 @@ const handleChange = (info: UploadChangeParam) => {
message.success('上传成功!'); message.success('上传成功!');
const result = info.file.response?.result; const result = info.file.response?.result;
keystoreBase64.value = result; keystoreBase64.value = result;
console.log(1114, result);
loading.value = false; loading.value = false;
emit('change', result); emit('change', result);
emit('update:modelValue', result); emit('update:modelValue', result);
} }
}; };
const textChange = (val: any) => { const textChange = (val: any) => {
val.name = props.name; emit('change', keystoreBase64.value);
emit('change', val); emit('update:modelValue', keystoreBase64.value);
// emit('update:modelValue', val);
}; };
watch( watch(

View File

@ -10,10 +10,8 @@
:label-col="{ span: 8 }" :label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }" :wrapper-col="{ span: 16 }"
autocomplete="off" autocomplete="off"
@finish="onFinish"
:rules="formRules"
> >
<a-form-item label="证书标准" name="type"> <a-form-item label="证书标准" v-bind="validateInfos.type">
<a-radio-group v-model:value="formData.type"> <a-radio-group v-model:value="formData.type">
<a-radio-button <a-radio-button
class="form-radio-button" class="form-radio-button"
@ -24,25 +22,29 @@
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item label="证书名称" name="name"> <a-form-item label="证书名称" v-bind="validateInfos.name">
<a-input <a-input
placeholder="请输入证书名称" placeholder="请输入证书名称"
v-model:value="formData.name" v-model:value="formData.name"
/> />
</a-form-item> </a-form-item>
<a-form-item label="证书文件" name="cert"> <a-form-item
label="证书文件"
v-bind="validateInfos['configs.cert']"
>
<CertificateFile <CertificateFile
name="cert" name="cert"
v-model:modelValue="formData.cert" v-model:modelValue="formData.configs.cert"
@change="changeFileValue"
placeholder='证书格式以"-----BEGIN CERTIFICATE-----"开头,以"-----END CERTIFICATE-----"结尾"' placeholder='证书格式以"-----BEGIN CERTIFICATE-----"开头,以"-----END CERTIFICATE-----"结尾"'
/> />
</a-form-item> </a-form-item>
<a-form-item label="证书私钥" name="key"> <a-form-item
label="证书私钥"
v-bind="validateInfos['configs.key']"
>
<CertificateFile <CertificateFile
name="key" name="key"
v-model:modelValue="formData.key" v-model:modelValue="formData.configs.key"
@change="changeFileValue"
placeholder='证书私钥格式以"-----BEGIN (RSA|EC) PRIVATE KEY-----"开头,以"-----END(RSA|EC) PRIVATE KEY-----"结尾。' placeholder='证书私钥格式以"-----BEGIN (RSA|EC) PRIVATE KEY-----"开头,以"-----END(RSA|EC) PRIVATE KEY-----"结尾。'
/> />
</a-form-item> </a-form-item>
@ -61,6 +63,8 @@
class="form-submit" class="form-submit"
html-type="submit" html-type="submit"
type="primary" type="primary"
@click.prevent="onSubmit"
:loading="loading"
>保存</a-button >保存</a-button
> >
</a-form-item> </a-form-item>
@ -89,7 +93,7 @@
</template> </template>
<script lang="ts" setup name="CertificateDetail"> <script lang="ts" setup name="CertificateDetail">
import { message } from 'ant-design-vue'; import { message, Form } from 'ant-design-vue';
import { getImage } from '@/utils/comm'; import { getImage } from '@/utils/comm';
import CertificateFile from './CertificateFile.vue'; import CertificateFile from './CertificateFile.vue';
import type { UploadChangeParam } from 'ant-design-vue'; import type { UploadChangeParam } from 'ant-design-vue';
@ -101,57 +105,65 @@ import {
} from '@/utils/variable'; } from '@/utils/variable';
import { save } from '@/api/link/certificate'; import { save } from '@/api/link/certificate';
const router = useRouter();
const useForm = Form.useForm;
const fileLoading = ref(false);
const loading = ref(false); const loading = ref(false);
const formData = reactive({ const formData = reactive({
type: 'common', type: 'common',
name: '', name: '',
cert: '', configs: {
key: '', cert: '',
// configs: { key: '',
// cert: '', },
// key: '',
// },
description: '', description: '',
}); });
const formRules = { const { resetFields, validate, validateInfos } = useForm(
type: [{ required: true, message: '请选择证书标准', trigger: 'blur' }], formData,
name: [ reactive({
{ required: true, message: '请输入证书名称', trigger: 'blur' }, type: [{ required: true, message: '请选择证书标准', trigger: 'blur' }],
{ max: 64, message: '最多可输入64个字符' }, name: [
], { required: true, message: '请输入证书名称', trigger: 'blur' },
cert: [{ required: true, message: '请输入或上传文件', trigger: 'blur' }], { max: 64, message: '最多可输入64个字符' },
key: [{ required: true, message: '请输入或上传文件', trigger: 'blur' }], ],
description: [{ max: 200, message: '最多可输入200个字符' }], 'configs.cert': [
}; { required: true, message: '请输入或上传文件', trigger: 'blur' },
],
'configs.key': [
{ required: true, message: '请输入或上传文件', trigger: 'blur' },
],
description: [{ max: 200, message: '最多可输入200个字符' }],
}),
);
const onFinish = async (values: any) => { const onSubmit = () => {
values.configs = { validate()
cert: formData.cert, .then(async (res) => {
key: formData.key, const params = toRaw(formData);
}; loading.value = true;
delete values.cert; const response = await save(params);
delete values.key; if (response.status === 200) {
message.success('操作成功');
const response = await save(values) router.push('/link/certificate');
if (response.status === 200) { }
message.success('操作成功') loading.value = false;
} })
.catch((err) => {
}; loading.value = false;
});
const changeFileValue = (v: any) => {
formData[v.name] = v.data;
}; };
const handleChange = (info: UploadChangeParam) => { const handleChange = (info: UploadChangeParam) => {
loading.value = true; fileLoading.value = true;
if (info.file.status === 'done') { if (info.file.status === 'done') {
message.success('上传成功!'); message.success('上传成功!');
const result = info.file.response?.result; const result = info.file.response?.result;
formData.cert = result; formData.configs.cert = result;
loading.value = false; fileLoading.value = false;
} }
}; };
</script> </script>

View File

@ -3,8 +3,11 @@
</template> </template>
<script lang="ts" setup name="CertificatePage"> <script lang="ts" setup name="CertificatePage">
const handlAdd = (e: any) => { const router = useRouter();
console.log(111,e);
const handlAdd = () => {
router.push('/link/certificate/detail/add');
} }

View File

@ -0,0 +1,139 @@
<template>
<div class="api-does-container">
<div class="top">
<h5>{{ selectApi.summary }}</h5>
<div class="input">
<InputCard :value="selectApi.method" />
<a-input :value="selectApi?.url" disabled />
</div>
</div>
<p style="display: flex; justify-content: space-between">
<span class="label">请求数据类型</span>
<span class="value">{{
getContent(selectApi.requestBody) ||
'application/x-www-form-urlencoded'
}}</span>
<span class="label">响应数据类型</span>
<span class="value">{{ `["/"]` }}</span>
</p>
<div class="api-card">
<h5>请求参数</h5>
<div class="content">
<JTable
:columns="columns.request"
:dataSource="selectApi.parameters"
noPagination
model="TABLE"
>
<template #required="slotProps">
<span>{{ slotProps.row.required + '' }}</span>
</template>
<template #type="slotProps">
<span>{{ slotProps.row.schema.type }}</span>
</template>
</JTable>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { apiDetailsType } from '../index';
import InputCard from './InputCard.vue';
import { PropType } from 'vue';
const props = defineProps({
selectApi: {
type: Object as PropType<apiDetailsType>,
required: true,
},
});
const { selectApi } = toRefs(props);
const columns = {
request: [
{
title: '参数名',
dataIndex: 'name',
key: 'name',
},
{
title: '参数说明',
dataIndex: 'description',
key: 'description',
},
{
title: '请求类型',
dataIndex: 'in',
key: 'in',
},
{
title: '是否必须',
dataIndex: 'required',
key: 'required',
scopedSlots: true,
},
{
title: '参数类型',
dataIndex: 'type',
key: 'type',
scopedSlots: true,
},
],
};
console.log(selectApi.value);
const getContent = (data: any) => {
if (!data) return '';
return Object.keys(data.content)[0];
};
</script>
<style lang="less" scoped>
.api-does-container {
.top {
width: 100%;
h5 {
font-weight: bold;
font-size: 16px;
}
.input {
display: flex;
}
}
.api-card {
h5 {
position: relative;
padding-left: 10px;
&::before {
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background-color: #1d39c4;
border-radius: 0 3px 3px 0;
content: ' ';
}
}
.content {
padding-left: 10px;
:deep(.jtable-body) {
padding: 0;
.jtable-body-header {
display: none;
}
}
}
}
}
</style>

View File

@ -0,0 +1,42 @@
<template>
<div class="api-test-container">
<div class="top">
<h5>{{ selectApi.summary }}</h5>
<div class="input">
<InputCard :value="selectApi.method" />
<a-input :value="selectApi?.url" disabled />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { apiDetailsType } from '../index';
import InputCard from './InputCard.vue';
import { PropType } from 'vue';
const props = defineProps({
selectApi: {
type: Object as PropType<apiDetailsType>,
required: true,
},
});
const { selectApi } = toRefs(props);
</script>
<style lang="less" scoped>
.api-test-container {
.top {
width: 100%;
h5 {
font-weight: bold;
font-size: 16px;
}
.input {
display: flex;
}
}
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<div class="choose-api-container">
<JTable
:columns="columns"
:dataSource="props.tableData"
:rowSelection="rowSelection"
noPagination
model="TABLE"
>
<template #url="slotProps">
<span
style="color: #1d39c4; cursor: pointer"
@click="jump(slotProps.row)"
>{{ slotProps.row.url }}</span
>
</template>
</JTable>
<a-button type="primary">保存</a-button>
</div>
</template>
<script setup lang="ts">
import { TableProps } from 'ant-design-vue';
const emits = defineEmits(['update:clickApi'])
const props = defineProps({
tableData: Array,
clickApi: Object
});
const columns = [
{
title: 'API',
dataIndex: 'url',
key: 'url',
scopedSlots: true,
},
{
title: '说明',
dataIndex: 'summary',
key: 'summary',
},
];
const rowSelection: TableProps['rowSelection'] = {
// onChange: (selectedRowKeys, selectedRows) => {
// console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
// },
};
const jump = (row:object) => {
emits('update:clickApi',row)
};
</script>
<style lang="less" scoped>
.choose-api-container {
height: 100%;
:deep(.jtable-body-header) {
display: none !important;
}
}
</style>

View File

@ -0,0 +1,35 @@
<template>
<span class="input-card-container" :class="props.value">
{{ props.value?.toLocaleUpperCase() }}
</span>
</template>
<script setup lang="ts">
const props = defineProps({
value: String,
});
</script>
<style lang="less" scoped>
.input-card-container {
padding: 4px 15px;
font-size: 14px;
color: #fff;
&.get {
background-color: #1890ff;
}
&.put {
background-color: #fa8c16;
}
&.post {
background-color: #52c41a;
}
&.delete {
background-color: #f5222d;
}
&.patch {
background-color: #a0d911;
}
}
</style>

View File

@ -15,51 +15,62 @@
import { TreeProps } from 'ant-design-vue'; import { TreeProps } from 'ant-design-vue';
import { getTreeOne_api, getTreeTwo_api } from '@/api/system/apiPage'; import { getTreeOne_api, getTreeTwo_api } from '@/api/system/apiPage';
import { treeNodeTpye } from '../index';
type treeNodeTpye = {
name: string;
url: string;
children?: treeNodeTpye[];
};
const emits = defineEmits(['select']); const emits = defineEmits(['select']);
const treeData: TreeProps['treeData'] = ref([]); const treeData = ref<TreeProps['treeData']>([]);
const getTreeData = () => { const getTreeData = () => {
let tree: treeNodeTpye[] = []; let tree: treeNodeTpye[] = [];
getTreeOne_api().then((resp) => { getTreeOne_api().then((resp: any) => {
tree = resp.urls.map((item) => ({ tree = resp.urls.map((item: any) => ({
...item, ...item,
key: item.url, key: item.url,
})); }));
const allPromise = tree.map((item) => getTreeTwo_api(item.name)); const allPromise = tree.map((item) => getTreeTwo_api(item.name));
Promise.all(allPromise).then((values) => { Promise.all(allPromise).then((values) => {
values.forEach((item, i) => { values.forEach((item: any, i) => {
tree[i].children = combData(item.paths); tree[i].children = combData(item?.paths);
}); });
console.log(tree); treeData.value = tree;
treeData.value = tree
}); });
}); });
}; };
const clickSelectItem = (key, { node }) => { const clickSelectItem: TreeProps['onSelect'] = (key, node: any) => {
emits('select', node); emits('select', node.node.dataRef);
}; };
onMounted(() => { onMounted(() => {
getTreeData(); getTreeData();
}); });
const combData = (dataSource: object): object[] => { const combData = (dataSource: object) => {
const apiList: object[] = []; const apiList: treeNodeTpye[] = [];
const keys = Object.keys(dataSource); const keys = Object.keys(dataSource);
keys.forEach((key) => { keys.forEach((key) => {
const method = Object.keys(dataSource[key] || {})[0]; const method = Object.keys(dataSource[key] || {})[0];
const name = dataSource[key][method].tags[0]; const name = dataSource[key][method].tags[0];
let apiObj = apiList.find((item) => item.name === name); let apiObj: treeNodeTpye | undefined = apiList.find(
if (!apiObj) { (item) => item.name === name,
apiObj = { name, link: key, methods: dataSource[key], key }; );
if (apiObj) {
apiObj.apiList?.push({
url: key,
method: dataSource[key],
});
} else {
apiObj = {
name,
key: name,
apiList: [
{
url: key,
method: dataSource[key],
},
],
};
apiList.push(apiObj); apiList.push(apiObj);
} }
}); });
@ -68,4 +79,17 @@ const combData = (dataSource: object): object[] => {
}; };
</script> </script>
<style lang="less" scoped></style> <style lang="less">
.left-tree-container {
border-right: 1px solid #e9e9e9;
height: calc(100vh - 150px);
overflow-y: auto;
.ant-tree-list {
.ant-tree-list-holder-inner {
.ant-tree-switcher-noop {
display: none !important;
}
}
}
}
</style>

22
src/views/system/apiPage/index.d.ts vendored Normal file
View File

@ -0,0 +1,22 @@
export type treeNodeTpye = {
name: string;
key: string;
link?: string;
apiList?: object[];
children?: treeNodeTpye[];
};
export type methodType = {
[key: string]: object
}
export type apiObjType = {
url: string,
method: methodType
}
export type apiDetailsType = {
url: string;
method: string;
summary: string;
parameters: [];
requestBody?: any;
}

View File

@ -1,15 +1,78 @@
<template> <template>
<a-card class="api-page-container" > <a-card class="api-page-container">
<LeftTree /> <a-row :gutter="24">
<a-col :span="5">
<LeftTree @select="treeSelect" />
</a-col>
<a-col :span="19">
<ChooseApi
v-show="!selectedApi.url"
v-model:click-api="selectedApi"
:table-data="tableData"
/>
<div
class="api-details"
v-show="selectedApi.url && tableData.length > 0"
>
<a-button @click="selectedApi = initSelectedApi"
>返回</a-button
>
<a-tabs v-model:activeKey="activeKey" type="card">
<a-tab-pane key="does" tab="文档">
<ApiDoes :select-api="selectedApi" />
</a-tab-pane>
<a-tab-pane key="test" tab="调试">
<ApiTest :select-api="selectedApi" />
</a-tab-pane>
</a-tabs>
</div>
</a-col>
</a-row>
</a-card> </a-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts" name="apiPage">
import { treeNodeTpye, apiObjType, apiDetailsType } from './index';
import LeftTree from './components/LeftTree.vue'; import LeftTree from './components/LeftTree.vue';
import ChooseApi from './components/ChooseApi.vue';
import ApiDoes from './components/ApiDoes.vue';
import ApiTest from './components/ApiTest.vue';
const tableData = ref([]);
const treeSelect = (node: treeNodeTpye) => {
if (!node.apiList) return;
const apiList: apiObjType[] = node.apiList as apiObjType[];
const table: any = [];
//
apiList?.forEach((apiItem) => {
const { method, url } = apiItem;
for (const key in method) {
if (Object.prototype.hasOwnProperty.call(method, key)) {
table.push({
...method[key],
url,
method: key,
});
}
}
});
tableData.value = table;
};
const activeKey = ref('does');
const initSelectedApi = {
url: '',
method: '',
summary: '',
};
const selectedApi = ref<apiDetailsType>(initSelectedApi);
watch(tableData, () => (selectedApi.value = initSelectedApi));
</script> </script>
<style scoped> <style scoped>
.api-page-container { .api-page-container {
height: 100%; height: 100%;
} }
</style> </style>

7656
yarn.lock

File diff suppressed because it is too large Load Diff