diff --git a/config/config.ts b/config/config.ts index 108726d5..9a3c3b8a 100644 --- a/config/config.ts +++ b/config/config.ts @@ -2,6 +2,15 @@ export default { theme: { 'primary-color': '#1d39c4', }, - logo: '/favicon.ico', - title: 'Jetlinks' + logo: '/favicon.ico', // 浏览器标签页logo + title: 'Jetlinks', // 浏览器标签页title + layout: { + title: '物联网平台', // 平台title + logo: '/icons/icon-192x192.png', // 平台logo + siderWidth: 208, // 左侧菜单栏宽度 + headerHeight: 48, // 头部高度 + collapsedWidth: 48, + mode: 'inline', + theme: 'light', // 'dark' 'light' + } } \ No newline at end of file diff --git a/public/images/network/01.jpg b/public/images/network/01.jpg deleted file mode 100644 index 950b64d7..00000000 Binary files a/public/images/network/01.jpg and /dev/null differ diff --git a/public/images/network/01.png b/public/images/network/01.png new file mode 100644 index 00000000..9ec5cdc8 Binary files /dev/null and b/public/images/network/01.png differ diff --git a/public/images/network/02.jpg b/public/images/network/02.jpg index 745ccacc..950b64d7 100644 Binary files a/public/images/network/02.jpg and b/public/images/network/02.jpg differ diff --git a/public/images/network/03.png b/public/images/network/03.png new file mode 100644 index 00000000..a9e8e8d3 Binary files /dev/null and b/public/images/network/03.png differ diff --git a/public/images/network/03.jpg b/public/images/network/04.jpg similarity index 100% rename from public/images/network/03.jpg rename to public/images/network/04.jpg diff --git a/public/images/network/05.jpg b/public/images/network/05.jpg new file mode 100644 index 00000000..e15d4962 Binary files /dev/null and b/public/images/network/05.jpg differ diff --git a/public/images/network/06.jpg b/public/images/network/06.jpg new file mode 100644 index 00000000..e5a2f45a Binary files /dev/null and b/public/images/network/06.jpg differ diff --git a/src/App.vue b/src/App.vue index 1299fb94..02bc5677 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,9 +1,9 @@ - - + + diff --git a/src/api/comm.ts b/src/api/comm.ts index 71d6aa82..29cf4f11 100644 --- a/src/api/comm.ts +++ b/src/api/comm.ts @@ -23,3 +23,8 @@ export const getSearchHistory = (target:string) => server.get server.remove(`/user/settings/${target}/${id}`) + +/** + * 获取当前系统版本 + */ +export const systemVersion = () => server.get<{edition?: string}>('/system/version') \ No newline at end of file diff --git a/src/api/device/instance.ts b/src/api/device/instance.ts index 9f051caf..19df86c2 100644 --- a/src/api/device/instance.ts +++ b/src/api/device/instance.ts @@ -1,6 +1,7 @@ +import { LocalStore } from '@/utils/comm' import server from '@/utils/request' -import { BASE_API_PATH } from '@/utils/variable' -import { DeviceInstance } from '@/views/device/instance/typings' +import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable' +import { DeviceInstance } from '@/views/device/Instance/typings' /** * 删除设备物模型 @@ -97,5 +98,5 @@ export const batchDeleteDevice = (data: string[]) => server.put(`/device-instanc * @param type 文件类型 * @returns */ - export const deviceExport = (productId: string, type: string) => `${BASE_API_PATH}/device-instance${!!productId ? '/' + productId : ''}/export.${type}` +export const deviceExport = (productId: string, type: string) => `${BASE_API_PATH}/device-instance${!!productId ? '/' + productId : ''}/export.${type}` diff --git a/src/api/device/product.ts b/src/api/device/product.ts index 17100b3f..6ae11030 100644 --- a/src/api/device/product.ts +++ b/src/api/device/product.ts @@ -109,4 +109,9 @@ export const deleteProduct = (id: string) => server.patch(`/device-product/${id} * @param id 产品ID */ export const queryProductId = (id: string) => server.post(`/device-product/${id}/exists`) - \ No newline at end of file +/** + * 保存产品 + * @param data 产品信息 + * @returns + */ +export const saveProductMetadata = (data: Record) => server.patch('/device-product', data) diff --git a/src/api/home.ts b/src/api/home.ts index 2164e2f5..a37c0f3d 100644 --- a/src/api/home.ts +++ b/src/api/home.ts @@ -12,4 +12,6 @@ export const getDeviceCount_api = () => server.get(`/device/instance/_count`); // 产品数量 export const getProductCount_api = (data:object) => server.post(`/device-product/_count`, data); // 查询产品列表 -export const getProductList_api = (data:object) => server.get(`/device/product/_query/no-paging?paging=false`, data); +export const getProductList_api = (data:object={}) => server.get(`/device/product/_query/no-paging?paging=false`, data); +// 查询设备列表 +export const getDeviceList_api = (data:object) => server.post(`/device-instance/_query/`, data); diff --git a/src/api/iot-card/cardManagement.ts b/src/api/iot-card/cardManagement.ts new file mode 100644 index 00000000..390685e0 --- /dev/null +++ b/src/api/iot-card/cardManagement.ts @@ -0,0 +1,67 @@ +import server from '@/utils/request' + +/** + * 不分页查询平台对接 + * @param data + */ +export const queryPlatformNoPage = (data: any) => server.post(`/network/card/platform/_query/no-paging`, data) + +/** + * 分页查询物联卡管理列表 + * @param data + */ +export const query = (data: any) => server.post(`/network/card/_query`, data) + +/** + * 激活待激活物联卡 + * @param cardId + */ +export const changeDeploy = (cardId: string) => server.get(`/network/card/${cardId}/_activation`); + +/** + * 停用已激活物联卡 + * @param cardId + */ +export const unDeploy = (cardId: string) => server.get(`/network/card/${cardId}/_deactivate`); + +/** + * 复机已停机物联卡 + * @param cardId + */ +export const resumption = (cardId: string) => server.get(`/network/card/${cardId}/_resumption`); + +/** + * 删除物联卡 + * @param id + */ +export const del = (id: string) => server.remove(`/network/card/${id}`); + + +/** + * 激活待激活物联卡(批量) + * @param data + */ +export const changeDeployBatch = (data: any) => server.get(`/network/card/_activation/_bitch`, data); + +/** + * 停用已激活物联卡(批量) + * @param data + */ +export const unDeployBatch = (data: any) => server.get(`/network/card/_deactivate/_bitch`, data); + +/** + * 复机已停机物联卡(批量) + * @param data + */ +export const resumptionBatch = (data: any) => server.get(`/network/card/_resumption/_bitch`, data); + +/** + * 同步物联卡状态 + */ +export const sync = () => server.get(`/network/card/state/_sync`); + +/** + * 批量删除物联卡 + * @param data + */ +export const removeCards = (data: any) => server.post(`/network/card/batch/_delete`, data); \ No newline at end of file diff --git a/src/api/login.ts b/src/api/login.ts index 33684901..6391c6b8 100644 --- a/src/api/login.ts +++ b/src/api/login.ts @@ -46,4 +46,9 @@ export const bindInfo = () => server.get(`/application/sso/_all`) * 查询配置信息 * @returns */ -export const settingDetail = (scopes: string) => server.get(`/system/config/${scopes}`) \ No newline at end of file +export const settingDetail = (scopes: string) => server.get(`/system/config/${scopes}`) + +/** + * 获取当前登录用户信息 + */ +export const userDetail = () => server.get('/user/detail') \ No newline at end of file diff --git a/src/api/notice/config.ts b/src/api/notice/config.ts index d6e29cc5..c75aa1f5 100644 --- a/src/api/notice/config.ts +++ b/src/api/notice/config.ts @@ -1,4 +1,5 @@ import { patch, post, get, remove } from '@/utils/request' +import { TemplateFormData } from '@/views/notice/Template/types' export default { // 列表 @@ -10,14 +11,14 @@ export default { // 修改 update: (data: any) => patch(`/notifier/config`, data), del: (id: string) => remove(`/notifier/config/${id}`), - getTemplate: (data: any, id: string) => post(`/notifier/template/${id}/_query`, data), - getTemplateDetail: (id: string) => get(`/notifier/template/${id}/detail`), + getTemplate: (data: any, id: string) => post(`/notifier/template/${id}/_query`, data), + getTemplateDetail: (id: string) => get(`/notifier/template/${id}/detail`), debug: (data: any, configId: string, templateId: string) => post(`/notifier/${configId}/${templateId}/_send`, data), getHistory: (data: any, id: string) => post(`/notify/history/config/${id}/_query`, data), // 获取所有平台用户 - getPlatformUsers: () => post(`/user/_query/no-paging`, { paging: false }), + getPlatformUsers: () => post(`/user/_query/no-paging`, { paging: false }), // 钉钉部门 - dingTalkDept: (id: string) => get(`/notifier/dingtalk/corp/${id}/departments/tree`), + dingTalkDept: (id: string) => get(`/notifier/dingtalk/corp/${id}/departments/tree`), // 钉钉部门人员 getDingTalkUsers: (configId: string, deptId: string) => get(`/notifier/dingtalk/corp/${configId}/${deptId}/users`), // 钉钉已经绑定的人员 @@ -25,7 +26,7 @@ export default { // 钉钉绑定用户 dingTalkBindUser: (data: any, id: string) => patch(`/user/third-party/dingTalk_dingTalkMessage/${id}`, data), // 微信部门 - weChatDept: (id: string) => get(`/notifier/wechat/corp/${id}/departments`), + weChatDept: (id: string) => get(`/notifier/wechat/corp/${id}/departments`), // 微信部门人员 getWeChatUsers: (configId: string, deptId: string) => get(`/notifier/wechat/corp/${configId}/${deptId}/users`), // 微信已经绑定的人员 diff --git a/src/api/notice/template.ts b/src/api/notice/template.ts index 7281501f..45c90010 100644 --- a/src/api/notice/template.ts +++ b/src/api/notice/template.ts @@ -16,12 +16,12 @@ export default { debug: (data: any, configId: string, templateId: string) => post(`/notifier/${configId}/${templateId}/_send`, data), getHistory: (data: any, id: string) => post(`/notify/history/template/${id}/_query`, data), // 钉钉/微信, 根据配置获取部门和用户 - getDept: (type: string, id: string) => get(`/notifier/${type}/corp/${id}/departments`), - getUser: (type: string, id: string) => get(`/notifier/${type}/corp/${id}/users`), + getDept: (type: string, id: string) => get(`/notifier/${type}/corp/${id}/departments`), + getUser: (type: string, id: string) => get(`/notifier/${type}/corp/${id}/users`), // 微信获取标签推送 - getTags: (id: string) => get(`/notifier/wechat/corp/${id}/tags`), + getTags: (id: string) => get(`/notifier/wechat/corp/${id}/tags`), // 语音/短信获取阿里云模板 - getAliTemplate: (id: string) => get(`/notifier/sms/aliyun/${id}/templates`), + getAliTemplate: (id: any) => get(`/notifier/sms/aliyun/${id}/templates`), // 短信获取签名 - getSigns: (id: string) => get(`/notifier/sms/aliyun/${id}/signs`) + getSigns: (id: any) => get(`/notifier/sms/aliyun/${id}/signs`) } \ No newline at end of file diff --git a/src/api/system/menu.ts b/src/api/system/menu.ts new file mode 100644 index 00000000..09c9ca8c --- /dev/null +++ b/src/api/system/menu.ts @@ -0,0 +1,3 @@ +import server from '@/utils/request' + +export const queryOwnThree = (data: any) => server.post('/menu/user-own/tree', data) \ No newline at end of file diff --git a/src/api/system/permission.ts b/src/api/system/permission.ts index 978e4b26..659abbed 100644 --- a/src/api/system/permission.ts +++ b/src/api/system/permission.ts @@ -1,6 +1,15 @@ import server from '@/utils/request'; // 获取权限列表 -export const getPermission_api = (data:object) => server.post(`/permission/_query/`,data); -// 修改权限信息 -export const editPermission_api = (data:object) => server.patch(`/permission`,data); \ No newline at end of file +export const getPermission_api = (data: object) => server.post(`/permission/_query/`, data); +// 新增时校验标识id是否可用 +export const checkId_api = (data: object) => server.get(`/permission/id/_validate`, data); +// 修改权限 | 导入文件内容 +export const editPermission_api = (data: object) => server.patch(`/permission`, data); +// 添加权限 +export const addPermission_api = (data: object) => server.post(`/permission`, data); +// 删除权限 +export const delPermission_api = (id: string) => server.remove(`/permission/${id}`); + +// 导出权限数据 +export const exportPermission_api = (data: object) => server.post(`/permission/_query/no-paging`, data); diff --git a/src/components/AIcon/index.tsx b/src/components/AIcon/index.tsx index ab50cf8c..c580070d 100644 --- a/src/components/AIcon/index.tsx +++ b/src/components/AIcon/index.tsx @@ -27,8 +27,23 @@ const iconKeys = [ 'SyncOutlined', 'ExclamationCircleOutlined', 'UploadOutlined', + 'LoadingOutlined', 'PlusCircleOutlined', - 'QuestionCircleOutlined' + 'QuestionCircleOutlined', + 'DisconnectOutlined', + 'LinkOutlined', + 'PoweroffOutlined', + 'SwapOutlined', + 'BugOutlined', + 'BarsOutlined', + 'ArrowDownOutlined', + 'SmallDashOutlined', + 'TeamOutlined', + 'MenuUnfoldOutlined', + 'MenuFoldOutlined', + 'QuestionCircleOutlined', + 'InfoCircleOutlined', + 'SearchOutlined', ] const Icon = (props: {type: string}) => { diff --git a/src/components/JUpload/index.vue b/src/components/JUpload/index.vue index c7f612de..94cfe30b 100644 --- a/src/components/JUpload/index.vue +++ b/src/components/JUpload/index.vue @@ -1,70 +1,213 @@ \ No newline at end of file diff --git a/src/components/Layout/BasicLayout.tsx b/src/components/Layout/BasicLayout.tsx new file mode 100644 index 00000000..d091510a --- /dev/null +++ b/src/components/Layout/BasicLayout.tsx @@ -0,0 +1,226 @@ +import { + computed, + reactive, + unref, + defineComponent, + toRefs, + provide +} from 'vue' + +import type { DefineComponent, ExtractPropTypes, PropType, CSSProperties, Plugin, App } from 'vue' +import { Layout } from 'ant-design-vue' +import useConfigInject from 'ant-design-vue/es/_util/hooks/useConfigInject' +import { defaultSettingProps, defaultSettings } from './defaultSetting' +import type { PureSettings } from './defaultSetting' +import type { BreadcrumbProps, RouteContextProps } from './RouteContext' +import type { + BreadcrumbRender, + CollapsedButtonRender, CustomRender, + FooterRender, + HeaderContentRender, + HeaderRender, + MenuContentRender, + MenuExtraRender, + MenuFooterRender, + MenuHeaderRender, + MenuItemRender, + RightContentRender, + SubMenuItemRender +} from './typings' +import SiderMenuWrapper, { siderMenuProps } from 'components/Layout/components/SiderMenu/SiderMenu' +import { getSlot } from '@/utils/comm' +import { getMenuFirstChildren } from 'components/Layout/utils' +import { pick } from 'lodash-es' +import { routeContextInjectKey } from './RouteContext' +import { HeaderView, headerViewProps } from './components/Header' + +export const basicLayoutProps = { + ...defaultSettingProps, + ...siderMenuProps, + ...headerViewProps, + + breadcrumb: { + type: [Object, Function] as PropType, + default: () => null + }, + breadcrumbRender: { + type: [Object, Function, Boolean] as PropType, + default() { + return null + } + }, + contentStyle: { + type: [String, Object] as PropType, + default: () => { + return null + } + }, + pure: { + type: Boolean, + default: () => false + } +} + +export type BasicLayoutProps = Partial>; + +export default defineComponent({ + name: 'ProLayout', + inheritAttrs: false, + props: basicLayoutProps, + emits: [ + 'update:collapsed', + 'update:open-keys', + 'update:selected-keys', + 'collapse', + 'openKeys', + 'select', + 'menuHeaderClick', + 'menuClick' + ], + setup(props, { emit, attrs, slots }) { + const siderWidth = computed(() => (props.collapsed ? props.collapsedWidth : props.siderWidth)) + + const onCollapse = (collapsed: boolean) => { + emit('update:collapsed', collapsed) + emit('collapse', collapsed) + } + const onOpenKeys = (openKeys: string[] | false) => { + emit('update:open-keys', openKeys) + emit('openKeys', openKeys) + } + const onSelect = (selectedKeys: string[] | false) => { + emit('update:selected-keys', selectedKeys) + emit('select', selectedKeys) + } + const onMenuHeaderClick = (e: MouseEvent) => { + emit('menuHeaderClick', e) + } + const onMenuClick = (args: any) => { + emit('menuClick', args) + } + const headerRender = ( + p: BasicLayoutProps & { + hasSiderMenu: boolean; + headerRender: HeaderRender; + rightContentRender: RightContentRender; + }, + matchMenuKeys?: string[] + ): CustomRender | null => { + if (p.headerRender === false) { + return null + } + return + } + + const breadcrumb = computed(() => ({ + ...props.breadcrumb, + itemRender: getSlot(slots, props, 'breadcrumbRender') as BreadcrumbRender + })) + + const flatMenuData = computed( + () => (props.selectedKeys && getMenuFirstChildren(props.menuData, props.selectedKeys[0])) || []) + + const routeContext = reactive({ + ...(pick(toRefs(props), [ + 'menuData', + 'openKeys', + 'selectedKeys', + 'contentWidth', + 'headerHeight' + ]) as any), + siderWidth, + breadcrumb, + flatMenuData + }) + + provide(routeContextInjectKey, routeContext) + + return () => { + const { + pure, + onCollapse: propsOnCollapse, + onOpenKeys: propsOnOpenKeys, + onSelect: propsOnSelect, + onMenuClick: propsOnMenuClick, + ...restProps + } = props + + const collapsedButtonRender = getSlot(slots, props, 'collapsedButtonRender') + const rightContentRender = getSlot(slots, props, 'rightContentRender') + const customHeaderRender = getSlot(slots, props, 'headerRender') + + // menu + const menuHeaderRender = getSlot(slots, props, 'menuHeaderRender') + const menuExtraRender = getSlot(slots, props, 'menuExtraRender') + const menuContentRender = getSlot(slots, props, 'menuContentRender') + const menuItemRender = getSlot(slots, props, 'menuItemRender') + const subMenuItemRender = getSlot(slots, props, 'subMenuItemRender') + + const headerDom = computed(() => + headerRender( + { + ...props, + hasSiderMenu: true, + menuItemRender, + subMenuItemRender, + menuData: props.menuData, + onCollapse, + onOpenKeys, + onSelect, + onMenuHeaderClick, + rightContentRender, + collapsedButtonRender, + menuExtraRender, + menuContentRender, + headerRender: customHeaderRender, + theme: props.navTheme + }, + [] + ) + ) + + return ( + <> + { + pure ? ( + slots.default?.() + ) : ( + + + + + {headerDom.value} + + {slots.default?.()} + + + + ) + } + + ) + } + } +}) + + + + diff --git a/src/components/Layout/BasicLayoutPage.vue b/src/components/Layout/BasicLayoutPage.vue new file mode 100644 index 00000000..054f54ad --- /dev/null +++ b/src/components/Layout/BasicLayoutPage.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/src/components/Layout/BlankLayoutPage.vue b/src/components/Layout/BlankLayoutPage.vue new file mode 100644 index 00000000..69d6fa16 --- /dev/null +++ b/src/components/Layout/BlankLayoutPage.vue @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/src/components/Layout/PageContainer.tsx b/src/components/Layout/PageContainer.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/Layout/RouteContext.tsx b/src/components/Layout/RouteContext.tsx new file mode 100644 index 00000000..78f995b4 --- /dev/null +++ b/src/components/Layout/RouteContext.tsx @@ -0,0 +1,45 @@ +import type { ComputedRef, VNodeChild, InjectionKey } from 'vue' +import type { PureSettings } from 'components/Layout/defaultSetting' +import type { MenuDataItem } from 'components/Layout/typings' +import { useContext } from 'components/Layout/hooks/context' + +export interface Route { + path: string; + breadcrumbName: string; + children?: Omit[]; +} + +export interface BreadcrumbProps { + prefixCls?: string; + routes?: Route[]; + params?: any; + separator?: VNodeChild; + itemRender?: (opts: { route: Route; params: any; routes: Array; paths: Array }) => VNodeChild; +} + +export type BreadcrumbListReturn = Pick>; + +export interface MenuState { + selectedKeys: string[]; + openKeys: string[]; +} + +export interface RouteContextProps extends Partial, MenuState { + menuData: MenuDataItem[]; + flatMenuData?: MenuDataItem[]; + + getPrefixCls?: (suffixCls?: string, customizePrefixCls?: string) => string; + breadcrumb?: BreadcrumbListReturn | ComputedRef; + collapsed?: boolean; + hasSideMenu?: boolean; + siderWidth?: number; + headerHeight?: number; + /* 附加属性 */ + [key: string]: any; +} + +export const routeContextInjectKey: InjectionKey = Symbol('route-context'); + +export const useRouteContext = () => + useContext>(routeContextInjectKey, {}); + diff --git a/src/components/Layout/components/Header/Header.tsx b/src/components/Layout/components/Header/Header.tsx new file mode 100644 index 00000000..f3ff3654 --- /dev/null +++ b/src/components/Layout/components/Header/Header.tsx @@ -0,0 +1,128 @@ +import type { ExtractPropTypes, FunctionalComponent } from 'vue' +import { defineComponent, PropType } from 'vue' +import { defaultRenderLogo, siderMenuProps } from 'components/Layout/components/SiderMenu/SiderMenu' +import BaseMenu from '../SiderMenu/BaseMenu' +import { useRouteContext } from 'components/Layout/RouteContext' +import { default as ResizeObserver } from 'ant-design-vue/es/vc-resize-observer'; +import { defaultSettingProps } from 'components/Layout/defaultSetting' +import PropTypes from 'ant-design-vue/es/_util/vue-types' +import { CustomRender, MenuDataItem, ProProps, RightContentRender, WithFalse } from 'components/Layout/typings' +import './index.less' +import { omit } from 'lodash-es' +import { RouteRecordRaw } from 'vue-router' +import { clearMenuItem } from 'components/Layout/utils' + +export const headerProps = { + ...defaultSettingProps, + collapsed: PropTypes.looseBool, + menuData: { + type: Array as PropType, + default: () => [], + }, + logo: siderMenuProps.logo, + logoStyle: siderMenuProps.logoStyle, + menuRender: { + type: [Object, Function] as PropType< + WithFalse<(props: ProProps, defaultDom: CustomRender) => CustomRender> + >, + default: () => undefined, + }, + menuItemRender: siderMenuProps.menuItemRender, + subMenuItemRender: siderMenuProps.subMenuItemRender, + rightContentRender: { + type: [Object, Function] as PropType, + default: () => undefined, + }, + siderWidth: PropTypes.number.def(208), + // events + onMenuHeaderClick: PropTypes.func, + onCollapse: siderMenuProps.onCollapse, + onOpenKeys: siderMenuProps.onOpenKeys, + onSelect: siderMenuProps.onSelect, +} + +export type HeaderProps = ExtractPropTypes; + +export default defineComponent({ + name: 'Header', + emits: [ + 'menuHeaderClick', 'collapse', 'openKeys', 'select' + ], + inheritAttrs: false, + props: headerProps, + setup(props, { slots, emit}) { + + const { + onSelect, + onMenuHeaderClick, + onOpenKeys, + logo, + logoStyle, + menuData, + navTheme, + contentWidth, + title, + rightContentRender + } = props; + + const rightSize = ref('auto') + + const context = useRouteContext(); + + const noChildrenMenuData = (menuData || []).map((item) => ({ + ...item, + children: undefined, + })) as RouteRecordRaw[]; + + const clearMenuData = clearMenuItem(noChildrenMenuData); + + return () => ( + <> +
+
+ +
+ onOpenKeys && onOpenKeys($event), + 'onUpdate:selectedKeys': ($event: string[]) => onSelect && onSelect($event), + }} + /> +
+
+
+ { + rightSize.value = width + }} + > + { + rightContentRender && typeof rightContentRender === 'function' ? ( +
{rightContentRender({...props})}
+ ) : rightContentRender + } +
+
+
+
+
+ + ) + } +}) \ No newline at end of file diff --git a/src/components/Layout/components/Header/index.less b/src/components/Layout/components/Header/index.less new file mode 100644 index 00000000..d9734f2f --- /dev/null +++ b/src/components/Layout/components/Header/index.less @@ -0,0 +1,64 @@ +.header-content { + position: relative; + width: 100%; + height: 100%; + box-shadow: 0 1px 4px 0 rgb(0 21 41 / 12%); + transition: background .3s,width .2s; + + &.light { + background-color: #fff; + } + + &.dark { + .header-logo { + >a { + > h1 { + color: #fff !important; + } + } + } + } + + .header-main { + padding-left: 16px; + display: flex; + height: 100%; + + .header-main-left { + display: flex; + min-width: 192px; + + .header-logo { + height: 100%; + position: relative; + display: flex; + justify-content: center; + min-width: 165px; + overflow: hidden; + + > a { + color: @primary-color; + text-decoration: none; + background-color: transparent; + outline: none; + cursor: pointer; + transition: color .3s; + + > h1 { + vertical-align: top; + display: inline-block; + margin: 0 0 0 12px; + font-size: 16px; + color: rgba(0,0,0,.85); + } + } + + img { + display: inline-block; + height: 32px; + vertical-align: middle; + } + } + } + } +} \ No newline at end of file diff --git a/src/components/Layout/components/Header/index.tsx b/src/components/Layout/components/Header/index.tsx new file mode 100644 index 00000000..ad2a8461 --- /dev/null +++ b/src/components/Layout/components/Header/index.tsx @@ -0,0 +1,64 @@ +import type { ExtractPropTypes, PropType } from 'vue' +import PropTypes from 'ant-design-vue/es/_util/vue-types'; +import type { MenuDataItem, WithFalse, ProProps, CustomRender, RightContentRender } from '../../typings' +import { siderMenuProps } from '../SiderMenu/SiderMenu' +import { defaultSettingProps } from 'components/Layout/defaultSetting' +import Header, { headerProps } from './Header' +import { useRouteContext } from 'components/Layout/RouteContext' +import type { RouteRecordRaw } from 'vue-router' +import { clearMenuItem } from 'components/Layout/utils' +import DefaultSetting from '../../../../../config/config' +import { Layout } from 'ant-design-vue' + +export const headerViewProps = { + ...headerProps +} + +export type HeaderViewProps = Partial>; + +export const HeaderView = defineComponent({ + name: 'HeaderView', + inheritAttrs: false, + props: headerViewProps, + setup(props) { + const { headerHeight, onCollapse } = toRefs(props); + + const context = useRouteContext(); + + const clearMenuData = computed( + () => (context.menuData && clearMenuItem(context.menuData as RouteRecordRaw[])) || [] + ); + + return () => ( + <> + + +
+ + + ) +} +}) \ No newline at end of file diff --git a/src/components/Layout/components/PageContainer/index.less b/src/components/Layout/components/PageContainer/index.less new file mode 100644 index 00000000..fd4e709a --- /dev/null +++ b/src/components/Layout/components/PageContainer/index.less @@ -0,0 +1,23 @@ +.page-container { + min-height: 100%; + display: flex; + flex-direction: column; + + .page-container-grid-content { + padding: 24px; + flex: 1 1 auto; + display: flex; + flex-direction: column; + > div, .page-container-children-content, .page-container-full-height { + min-height: 100%; + display: flex; + flex-direction: column; + } + + .children-full-height { + > :nth-child(1) { + min-height: 100%; + } + } + } +} \ No newline at end of file diff --git a/src/components/Layout/components/PageContainer/index.tsx b/src/components/Layout/components/PageContainer/index.tsx new file mode 100644 index 00000000..e2763276 --- /dev/null +++ b/src/components/Layout/components/PageContainer/index.tsx @@ -0,0 +1,248 @@ +import { TabPaneProps } from 'ant-design-vue' +import type { ExtractPropTypes, FunctionalComponent, PropType, VNodeChild } from 'vue' +import { pageHeaderProps } from 'ant-design-vue/es/page-header'; +import type { DefaultPropRender, PageHeaderRender } from 'components/Layout/typings' +import type { AffixProps, TabBarExtraContent } from 'components/Layout/components/PageContainer/types' +import { useRouteContext } from 'components/Layout/RouteContext' +import { getSlotVNode } from '@/utils/comm' +import { Affix, Spin, PageHeader, Tabs } from 'ant-design-vue'; +import './index.less' + +export const pageHeaderTabConfig = { + /** + * @name tabs 的列表 + */ + tabList: { + type: [Object, Function, Array] as PropType<(Omit & { key?: string })[]>, + default: () => undefined, + }, + /** + * @name 当前选中 tab 的 key + */ + tabActiveKey: String, //PropTypes.string, + /** + * @name tab 上多余的区域 + */ + tabBarExtraContent: { + type: [Object, Function] as PropType, + default: () => undefined, + }, + /** + * @name tabs 的其他配置 + */ + tabProps: { + type: Object, //as PropType, + default: () => undefined, + }, + /** + * @name 固定 PageHeader 到页面顶部 + */ + fixedHeader: Boolean, //PropTypes.looseBool, + // events + onTabChange: Function, //PropTypes.func, +}; +export type PageHeaderTabConfig = Partial>; + + +export const pageContainerProps = { + ...pageHeaderTabConfig, + ...pageHeaderProps, + prefixCls: { + type: String, + default: 'ant-pro', + }, //PropTypes.string.def('ant-pro'), + title: { + type: [Object, String, Boolean, Function] as PropType, + default: () => null, + }, + subTitle: { + type: [Object, String, Boolean, Function] as PropType, + default: () => null, + }, + content: { + type: [Object, String, Boolean, Function] as PropType, + default: () => null, + }, + extra: { + type: [Object, String, Boolean, Function] as PropType, + default: () => null, + }, + extraContent: { + type: [Object, String, Boolean, Function] as PropType, + default: () => null, + }, + header: { + type: [Object, String, Boolean, Function] as PropType, + default: () => null, + }, + pageHeaderRender: { + type: [Object, Function, Boolean] as PropType, + default: () => undefined, + }, + affixProps: { + type: [Object, Function] as PropType, + }, + ghost: { + type: Boolean, + default: () => false, + }, //PropTypes.looseBool, + loading: { + type: Boolean, + default: () => undefined, + }, //PropTypes.looseBool, + childrenFullHeight: { + type: Boolean, + default: () => true, + } +}; + +export type PageContainerProps = Partial>; + +const renderFooter = ( + props: Omit< PageContainerProps, 'title' > +): VNodeChild | JSX.Element => { + const { tabList, tabActiveKey, onTabChange, tabBarExtraContent, tabProps } = props; + if (tabList && tabList.length) { + return ( + { + if (onTabChange) { + onTabChange(key); + } + }} + tabBarExtraContent={tabBarExtraContent} + {...tabProps} + > + {tabList.map((item) => ( + + ))} + + ); + } + return null; +} + +const ProPageHeader: FunctionalComponent = (props) => { + const { + title, + tabList, + tabActiveKey, + content, + pageHeaderRender, + header, + extraContent, + prefixCls, + fixedHeader: _, + ...restProps + } = props; + + const value = useRouteContext() + + if (pageHeaderRender === false) { + return null; + } + + if (pageHeaderRender) { + return pageHeaderRender({ ...props }); + } + let pageHeaderTitle = title; + if (!title && title !== false) { + pageHeaderTitle = value.title; + } + + const unrefBreadcrumb = unref(value.breadcrumb || {}); + const breadcrumb = (props as any).breadcrumb || { + ...unrefBreadcrumb, + routes: unrefBreadcrumb.routes, + itemRender: unrefBreadcrumb.itemRender, + }; + + return ( +
+ + {/*{header || renderPageHeader(content, extraContent)}*/} + { header } + +
+ ); +} + +const PageContainer = defineComponent({ + name: 'PageContainer', + inheritAttrs: false, + props: pageContainerProps, + setup(props, { slots }) { + const { loading, affixProps, ghost, childrenFullHeight } = toRefs(props); + + const value = useRouteContext(); + + const headerDom = computed(() => { + // const tags = getSlotVNode(slots, props, 'tags'); + const headerContent = getSlotVNode(slots, props, 'content'); + const extra = getSlotVNode(slots, props, 'extra'); + const extraContent = getSlotVNode(slots, props, 'extraContent'); + const subTitle = getSlotVNode(slots, props, 'subTitle'); + + // @ts-ignore + return ( + + ); + }) + + return () => { + const { fixedHeader } = props; + return ( +
+ {fixedHeader && headerDom.value ? ( + + {headerDom.value} + + ) : ( + headerDom.value + )} +
+ {loading.value ? ( + + ) : slots.default ? ( +
+
{slots.default()}
+ {value.hasFooterToolbar && ( +
+ )} +
+ ) : null} +
+
+ ); + }; + } +}) + +export default PageContainer \ No newline at end of file diff --git a/src/components/Layout/components/PageContainer/types.d.ts b/src/components/Layout/components/PageContainer/types.d.ts new file mode 100644 index 00000000..f189788a --- /dev/null +++ b/src/components/Layout/components/PageContainer/types.d.ts @@ -0,0 +1,56 @@ +import type { VNodeChild, CSSProperties, VNode } from 'vue'; + +export interface Tab { + key: string; + tab: string | VNode | JSX.Element; +} + +export type TabBarType = 'line' | 'card' | 'editable-card'; +export type TabSize = 'default' | 'large' | 'small'; +export type TabPosition = 'left' | 'right'; +export type TabBarExtraPosition = TabPosition; + +export type TabBarExtraMap = Partial>; + +export type TabBarExtraContent = VNodeChild | TabBarExtraMap; + +export interface TabsProps { + prefixCls?: string; + class?: string | string[]; + style?: CSSProperties; + id?: string; + + activeKey?: string; + hideAdd?: boolean; + // Unchangeable + // size?: TabSize; + tabBarStyle?: CSSProperties; + tabPosition?: TabPosition; + type?: TabBarType; + tabBarGutter?: number; +} + +export interface AffixProps { + offsetBottom: number; + offsetTop: number; + target?: () => HTMLElement; + + onChange?: (affixed: boolean) => void; +} + +export interface TabPaneProps { + tab?: string | VNodeChild | JSX.Element; + class?: string | string[]; + style?: CSSProperties; + disabled?: boolean; + forceRender?: boolean; + closable?: boolean; + closeIcon?: VNodeChild | JSX.Element; + + prefixCls?: string; + tabKey?: string; + id: string; + animated?: boolean; + active?: boolean; + destroyInactiveTabPane?: boolean; +} \ No newline at end of file diff --git a/src/components/Layout/components/SiderMenu/BaseMenu.tsx b/src/components/Layout/components/SiderMenu/BaseMenu.tsx new file mode 100644 index 00000000..c73f5002 --- /dev/null +++ b/src/components/Layout/components/SiderMenu/BaseMenu.tsx @@ -0,0 +1,217 @@ +import { isVNode, defineComponent, getCurrentInstance, withCtx } from 'vue' +import type { FunctionalComponent, PropType, VNodeChild, ComponentInternalInstance, ConcreteComponent, ExtractPropTypes, VNode } from 'vue' +import type { + MenuDataItem, + WithFalse, + MenuItemRender, + LayoutType, + MenuTheme, + SubMenuItemRender, + MenuMode +} from '../../typings' +import type { + SelectEventHandler, + MenuClickEventHandler, + SelectInfo, + MenuInfo, +} from 'ant-design-vue/es/menu/src/interface'; +import type { Key } from 'ant-design-vue/es/_util/type'; +import IconFont from '@/components/AIcon' +import { Menu } from 'ant-design-vue'; +import { isUrl } from '@/utils/regular' + +export const baseMenuProps = { + mode: { + type: String as PropType, + default: 'inline', + }, + menuData: { + type: Array as PropType, + default: () => [], + }, + layout: { + type: String as PropType, + default: 'side', + }, + theme: { + type: String as PropType, + default: 'dark', + }, + collapsed: { + type: Boolean as PropType, + default: () => false, + }, + openKeys: { + type: Array as PropType>, + default: () => undefined, + }, + selectedKeys: { + type: Array as PropType>, + default: () => undefined, + }, + menuProps: { + type: Object as PropType>, + default: () => null, + }, + menuItemRender: { + type: [Object, Function, Boolean] as PropType, + default: () => undefined, + }, + subMenuItemRender: { + type: [Object, Function, Boolean] as PropType, + default: () => undefined, + }, + onClick: [Function, Object] as PropType<(...args: any) => void>, +} + +export type BaseMenuProps = ExtractPropTypes; + +const LazyIcon: FunctionalComponent<{ icon: VNodeChild | string;}> = (props) => { + const {icon} = props + if (!icon) return null + if (typeof icon === 'string' && icon !== '') { + return ; + } + if (isVNode(icon)) { + return icon; + } +} + +class MenuUtil { + props: BaseMenuProps; + ctx: ComponentInternalInstance | null; + RouterLink: ConcreteComponent; + + constructor(props: BaseMenuProps, ctx: ComponentInternalInstance | null) { + this.props = props + this.ctx = ctx + this.RouterLink = resolveComponent('router-link') as ConcreteComponent + } + + getNavMenuItems = (menusData: MenuDataItem[] = []) => { + return menusData.map((item) => this.getSubMenuOrItem(item)).filter((item) => item); + }; + + getSubMenuOrItem = (item: MenuDataItem): VNode => { + if ( + Array.isArray(item.children) && + item.children.length > 0 && + !item?.meta?.hideInMenu && + !item?.meta?.hideChildrenInMenu + ) { + if (this.props.subMenuItemRender) { + const subMenuItemRender = withCtx(this.props.subMenuItemRender, this.ctx); + return subMenuItemRender({ + item, + children: this.getNavMenuItems(item.children), + }) as VNode; + } + const menuTitle = item.meta?.title + + + const defaultTitle = item.meta?.icon ? ( + + {menuTitle} + + ) : ( + {menuTitle} + ); + + return ( + } + > + {this.getNavMenuItems(item.children)} + + ) + } + + const menuItemRender = this.props.menuItemRender && withCtx(this.props.menuItemRender, this.ctx); + + const [title, icon] = this.getMenuItem(item); + + return ( + (menuItemRender && (menuItemRender({ item, title, icon }) as VNode)) || ( + + {title} + + ) + ) + } + + getMenuItem = (item: MenuDataItem) => { + const meta = { ...item.meta }; + const target = (meta.target || null) as string | null; + const hasUrl = isUrl(item.path); + const CustomTag: any = (target && 'a') || this.RouterLink; + const props = { to: { path: item.path, ...item.meta } }; + const attrs = hasUrl || target ? { ...item.meta, href: item.path, target } : {}; + + const icon = (item.meta?.icon && ) || undefined; + const menuTitle = item.meta?.title; + const defaultTitle = item.meta?.icon ? ( + + {icon} + {menuTitle} + + ) : ( + + {menuTitle} + + ); + + return [defaultTitle, icon]; + } + + conversionPath = (path: string) => { + if (path && path.indexOf('http') === 0) { + return path; + } + return `/${path || ''}`.replace(/\/+/g, '/'); + } +} + +export default defineComponent({ + name: 'BaseMenu', + props: baseMenuProps, + emits: ['update:openKeys', 'update:selectedKeys', 'click'], + setup(props, { emit }) { + const ctx = getCurrentInstance() + + const menuUtil = new MenuUtil(props, ctx); + + const handleOpenChange = (openKeys: Key[]): void => { + emit('update:openKeys', openKeys); + }; + + const handleSelect: SelectEventHandler = (args: SelectInfo): void => { + // ignore https? link handle selectkeys + if (isUrl(args.key as string)) { + return; + } + emit('update:selectedKeys', args.selectedKeys); + }; + + const handleClick: MenuClickEventHandler = (args: MenuInfo) => { + emit('click', args); + }; + + return () => ( + + {menuUtil.getNavMenuItems(props.menuData)} + + ) + } +}) \ No newline at end of file diff --git a/src/components/Layout/components/SiderMenu/SiderMenu.less b/src/components/Layout/components/SiderMenu/SiderMenu.less new file mode 100644 index 00000000..6b230073 --- /dev/null +++ b/src/components/Layout/components/SiderMenu/SiderMenu.less @@ -0,0 +1,16 @@ +.pro-layout-sider { + position: fixed; + top: 0; + left: 0; + z-index: 100; + height: 100%; + overflow: auto; + overflow-x: hidden; + box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%); + + .ant-layout-sider-children { + display: flex; + flex-direction: column; + height: 100%; + } +} diff --git a/src/components/Layout/components/SiderMenu/SiderMenu.tsx b/src/components/Layout/components/SiderMenu/SiderMenu.tsx new file mode 100644 index 00000000..29587b4a --- /dev/null +++ b/src/components/Layout/components/SiderMenu/SiderMenu.tsx @@ -0,0 +1,192 @@ +import { Layout, Menu } from 'ant-design-vue'; +import type { CSSProperties, ExtractPropTypes, FunctionalComponent, PropType } from 'vue' +import type { + LogoRender, + MenuHeaderRender, + MenuExtraRender, + MenuContentRender, + CollapsedButtonRender, + WithFalse, + CustomRender +} from '../../typings' +import PropTypes from 'ant-design-vue/es/_util/vue-types'; +import { baseMenuProps } from 'components/Layout/components/SiderMenu/BaseMenu' +import AIcon from '@/components/AIcon' +import { useRouteContext } from 'components/Layout/RouteContext' +import BaseMenu from './BaseMenu' +import './SiderMenu.less' +import { computed } from 'vue' +import { omit } from 'lodash-es' + +export type PrivateSiderMenuProps = { + matchMenuKeys?: string[]; +} + +const { Sider } = Layout + +export const defaultRenderLogo = (logo?: CustomRender, logoStyle?: CSSProperties): CustomRender => { + if (!logo) { + return null; + } + if (typeof logo === 'string') { + return logo; + } + if (typeof logo === 'function') { + // @ts-ignore + return logo(); + } + return logo; +}; + +export const siderMenuProps = { + ...baseMenuProps, + logo: { + type: [Object, String, Function] as PropType, + default: () => '', + }, + logoStyle: { + type: Object as PropType, + default: () => undefined, + }, + siderWidth: PropTypes.number.def(208), + headerHeight: PropTypes.number.def(48), + collapsedWidth: PropTypes.number.def(48), + menuHeaderRender: { + type: [Function, Object, Boolean] as PropType, + default: () => undefined, + }, + menuContentRender: { + type: [Function, Object, Boolean] as PropType, + default: () => undefined, + }, + menuExtraRender: { + type: [Function, Object, Boolean] as PropType, + default: () => undefined, + }, + collapsedButtonRender: { + type: [Function, Object, Boolean] as PropType, + default: () => undefined, + }, + onMenuHeaderClick: PropTypes.func, + onMenuClick: PropTypes.func, + onCollapse: { + type: Function as PropType<(collapsed: boolean) => void>, + }, + onOpenKeys: { + type: Function as PropType<(openKeys: WithFalse) => void>, + }, + onSelect: { + type: Function as PropType<(selectedKeys: WithFalse) => void>, + }, +} + +export type SiderMenuProps = Partial>; + +export const defaultRenderCollapsedButton = (collapsed?: boolean): CustomRender => + collapsed ? : ; + +const SiderMenu: FunctionalComponent = (props, { slots, emit}) => { + const { + collapsed, + collapsedWidth = 48, + menuExtraRender = false, + menuContentRender = false, + collapsedButtonRender = defaultRenderCollapsedButton, + } = props; + + const context = useRouteContext(); + const sSideWidth = computed(() => (props.collapsed ? props.collapsedWidth : props.siderWidth)); + + const extraDom = menuExtraRender && menuExtraRender(props); + + const handleSelect = ($event: string[]) => { + if (props.onSelect) { + props.onSelect([context.selectedKeys[0], ...$event]); + } + } + + const defaultMenuDom = ( + props.onOpenKeys && props.onOpenKeys($event), + 'onUpdate:selectedKeys': handleSelect, + }} + /> + ) + + const Style = computed(() => { + return { + overflow: 'hidden', + height: '100vh', + zIndex: 18, + paddingTop: `${props.headerHeight}px`, + flex: `0 0 ${sSideWidth.value}px`, + minWidth: `${sSideWidth.value}px`, + maxWidth: `${sSideWidth.value}px`, + width: `${sSideWidth.value}px`, + transition: 'background-color 0.3s ease 0s, min-width 0.3s ease 0s, max-width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) 0s' + } + }) + + return ( + <> +
+ { + props.onCollapse?.(collapse); + }} + collapsedWidth={collapsedWidth} + style={omit(Style.value, ['transition'])} + width={sSideWidth.value} + theme={props.theme as 'dark' | 'light'} + class={'pro-layout-sider'} + > +
+ {(menuContentRender && menuContentRender(props, defaultMenuDom)) || defaultMenuDom} +
+
+ {collapsedButtonRender !== false ? ( + { + if (props.onCollapse) { + props.onCollapse(!props.collapsed); + } + }} + > + + {collapsedButtonRender && typeof collapsedButtonRender === 'function' + ? collapsedButtonRender(collapsed) + : collapsedButtonRender} + + + ) : null} +
+
+ + ) +} + +export default SiderMenu \ No newline at end of file diff --git a/src/components/Layout/defaultSetting.ts b/src/components/Layout/defaultSetting.ts new file mode 100644 index 00000000..741ceda0 --- /dev/null +++ b/src/components/Layout/defaultSetting.ts @@ -0,0 +1,68 @@ +import type { PropType } from 'vue' +import config from '../../../config/config' + +export interface PureSettings { + title: string + /** + * theme for nav menu + */ + navTheme: 'dark' | 'light' | 'realDark' | undefined; + /** + * nav menu position: `side` or `top` + */ + headerHeight?: number; + /** + * customize header height + */ + layout: 'side' | 'top' | 'mix'; + /** + * layout of content: `Fluid` or `Fixed`, only works when layout is top + */ + contentWidth: 'Fluid' | 'Fixed'; + menu: { locale?: boolean; defaultOpenAll?: boolean }; + splitMenus?: boolean; +} + +export const defaultSettings = { + navTheme: 'dark', + layout: 'side', + contentWidth: 'Fluid', + fixedHeader: false, + fixSiderbar: false, + menu: {}, + headerHeight: 48, + iconfontUrl: '', + title: config.title +}; + +export const defaultSettingProps = { + navTheme: { + type: String as PropType, + default: defaultSettings.navTheme, + }, + title: { + type: String as PropType, + default: () => defaultSettings.title, + }, + layout: { + type: String as PropType, + default: defaultSettings.layout, + }, + contentWidth: { + type: String as PropType, + default: defaultSettings.contentWidth, + }, + menu: { + type: Object as PropType, + default: () => { + return { + locale: true, + }; + }, + }, + headerHeight: { + type: Number as PropType, + default: defaultSettings.headerHeight, + }, +} + diff --git a/src/components/Layout/hooks/context.ts b/src/components/Layout/hooks/context.ts new file mode 100644 index 00000000..c02aba9e --- /dev/null +++ b/src/components/Layout/hooks/context.ts @@ -0,0 +1,10 @@ +import { InjectionKey } from 'vue' + +export type ContextType = any; + +export const useContext = ( + contextInjectKey: string | InjectionKey> = Symbol(), + defaultValue?: ContextType +): T => { + return inject(contextInjectKey, defaultValue || ({} as T)); +}; \ No newline at end of file diff --git a/src/components/Layout/index.ts b/src/components/Layout/index.ts new file mode 100644 index 00000000..66fb03d7 --- /dev/null +++ b/src/components/Layout/index.ts @@ -0,0 +1,4 @@ +export { default as ProLayout } from './BasicLayout'; +export { default as BasicLayoutPage } from './BasicLayoutPage.vue' +export { default as BlankLayoutPage } from './BlankLayoutPage.vue' +export { default as PageContainer } from './components/PageContainer' \ No newline at end of file diff --git a/src/components/Layout/typings.ts b/src/components/Layout/typings.ts new file mode 100644 index 00000000..b4e90976 --- /dev/null +++ b/src/components/Layout/typings.ts @@ -0,0 +1,68 @@ +import type { VNode, Slots } from 'vue'; +import { BreadcrumbProps } from 'components/Layout/RouteContext' +import { VueNode } from 'ant-design-vue/es/_util/type' + +export interface MetaRecord { + /** + * @name 菜单的icon + */ + icon?: string | VNode; + /** + * @name 自定义菜单的国际化 key,如果没有则返回自身 + */ + title?: string; + /** + * @name 在菜单中隐藏子节点 + */ + hideChildInMenu?: boolean; + /** + * @name 在菜单中隐藏自己和子节点 + */ + hideInMenu?: boolean; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +} +export interface MenuDataItem { + /** + * @name 用于标定选中的值,默认是 path + */ + path: string; + name?: string | symbol; + meta?: MetaRecord; + /** + * @name 子菜单 + */ + children?: MenuDataItem[]; +} +export type Theme = 'dark' | 'light'; + +export type MenuTheme = Theme; +export type MenuMode = 'horizontal' | 'vertical' | 'inline'; +export type LayoutType = 'side' | 'top' | 'mix'; +export type ProProps = Record; +export type TargetType = '_blank' | '_self' | unknown; +export type BreadcrumbRender = BreadcrumbProps['itemRender']; + +export type CustomRender = VueNode; +export type WithFalse = T | false; +export type LogoRender = WithFalse; + +export type DefaultPropRender = WithFalse; +export type HeaderContentRender = WithFalse<() => CustomRender>; +export type HeaderRender = WithFalse<(props: ProProps) => CustomRender>; +export type FooterRender = WithFalse<(props: ProProps) => CustomRender>; +export type RightContentRender = WithFalse<(props: ProProps) => CustomRender>; +export type MenuItemRender = WithFalse< + (args: { item: MenuDataItem; title?: JSX.Element; icon?: JSX.Element }) => CustomRender + >; +export type SubMenuItemRender = WithFalse<(args: { item: MenuDataItem; children?: CustomRender[] }) => CustomRender>; +export type MenuHeaderRender = WithFalse<(logo: CustomRender, title: CustomRender, props?: ProProps) => CustomRender>; +export type MenuContentRender = WithFalse<(props: ProProps, defaultDom: CustomRender) => CustomRender>; +export type MenuFooterRender = WithFalse<(props?: ProProps) => CustomRender>; +export type MenuExtraRender = WithFalse<(props?: ProProps) => CustomRender>; + +export type CollapsedButtonRender = WithFalse<(collapsed?: boolean) => CustomRender>; + +export type PageHeaderRender = WithFalse<(props?: ProProps) => CustomRender>; + diff --git a/src/components/Layout/utils.ts b/src/components/Layout/utils.ts new file mode 100644 index 00000000..2422b857 --- /dev/null +++ b/src/components/Layout/utils.ts @@ -0,0 +1,48 @@ +import type { MenuDataItem } from 'components/Layout/typings' +import type { RouteRecord, RouteRecordRaw } from 'vue-router' + +export function getMenuFirstChildren(menus: MenuDataItem[], key?: string) { + return key === undefined ? [] : (menus[menus.findIndex((menu) => menu.path === key)] || {}).children || []; +} + +export function clearMenuItem(menusData: RouteRecord[] | RouteRecordRaw[]): RouteRecordRaw[] { + return menusData + .map((item: RouteRecord | RouteRecordRaw) => { + const finalItem = { ...item }; + if (finalItem.meta?.hideInMenu) { + return null; + } + + if (finalItem && finalItem?.children) { + if ( + !finalItem.meta?.hideChildInMenu && + finalItem.children.some( + (child: RouteRecord | RouteRecordRaw) => child && !child.meta?.hideInMenu + ) + ) { + return { + ...item, + children: clearMenuItem(finalItem.children), + }; + } + delete finalItem.children; + } + return finalItem; + }) + .filter((item) => item) as RouteRecordRaw[]; +} + +export function flatMap(menusData: RouteRecord[]): MenuDataItem[] { + return menusData + .map((item) => { + const finalItem = { ...item } as MenuDataItem; + if (!finalItem.name || finalItem.meta?.hideInMenu) { + return null; + } + if (finalItem.children) { + delete finalItem.children; + } + return finalItem; + }) + .filter((item) => item) as MenuDataItem[]; +} diff --git a/src/components/PermissionButton/index.vue b/src/components/PermissionButton/index.vue index d211e2ee..735fbf69 100644 --- a/src/components/PermissionButton/index.vue +++ b/src/components/PermissionButton/index.vue @@ -4,25 +4,50 @@ - + + + + - + + + + - + + + + - \ No newline at end of file diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index 92069199..7c43d1f7 100644 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -54,7 +54,7 @@ export interface JTableProps extends TableProps{ rowSelection?: TableProps['rowSelection']; cardProps?: Record; dataSource?: Record[]; - gridColumn: number; + gridColumn?: number; /** * 用于不同分辨率 * gridColumns[0] 1366 ~ 1440 分辨率; diff --git a/src/components/ValueItem/index.vue b/src/components/ValueItem/index.vue index 7a01b89e..3e7528ae 100644 --- a/src/components/ValueItem/index.vue +++ b/src/components/ValueItem/index.vue @@ -109,7 +109,7 @@ const props = defineProps({ // 组件类型 itemType: { type: String, - default: () => 'geoPoint', + default: () => 'string', }, // 下拉选择框下拉数据 options: { diff --git a/src/components/index.ts b/src/components/index.ts index ed95c26d..49f9d91d 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -9,6 +9,7 @@ import Search from './Search' import NormalUpload from './NormalUpload/index.vue' import FileFormat from './FileFormat/index.vue' import JUpload from './JUpload/index.vue' +import { BasicLayoutPage, BlankLayoutPage, PageContainer } from './Layout' export default { install(app: App) { @@ -22,5 +23,8 @@ export default { .component('NormalUpload', NormalUpload) .component('FileFormat', FileFormat) .component('JUpload', JUpload) + .component('BasicLayoutPage', BasicLayoutPage) + .component('BlankLayoutPage', BlankLayoutPage) + .component('PageContainer', PageContainer) } } diff --git a/src/router/index.ts b/src/router/index.ts index 11ffce4c..af006514 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,11 +1,12 @@ -import { createRouter, createWebHashHistory } from 'vue-router'; +import { createRouter, createWebHashHistory } from 'vue-router' import menus, { LoginPath } from './menu' -import { LocalStore } from "@/utils/comm"; -import { TOKEN_KEY } from "@/utils/variable"; +import { getToken } from '@/utils/comm' +import { useUserInfo } from '@/store/userInfo' +import { useSystem } from '@/store/system' const router = createRouter({ - history: createWebHashHistory(), - routes: menus + history: createWebHashHistory(), + routes: menus }) const filterPath = [ @@ -14,17 +15,45 @@ const filterPath = [ ] router.beforeEach((to, from, next) => { - const token = LocalStore.get(TOKEN_KEY) - // TODO 切换路由取消请求 - if (token || filterPath.includes(to.path)) { - next() - } else { - if (to.path === LoginPath) { - next() + // TODO 切换路由取消请求 + const isFilterPath = filterPath.includes(to.path) + if (isFilterPath) { + next() + } else { + const token = getToken() + if (token) { + if (to.path === LoginPath) { + next({ path: '/' }) + } else { + const userInfo = useUserInfo() + const system = useSystem() + if (!userInfo.$state.userInfos.username) { + userInfo.getUserInfo() + system.getSystemVersion().then((menuData: any[]) => { + menuData.forEach(r => { + router.addRoute('main', r) + }) + const redirect = decodeURIComponent((from.query.redirect as string) || to.path) + if(to.path === redirect) { + next({ ...to, replace: true }) + } else { + next({ path: redirect }) + } + }) + } else { - next({ path: LoginPath }) + next() } + } + + } else { + if (to.path === LoginPath) { + next() + } else { + next({ path: LoginPath }) + } } + } }) export default router \ No newline at end of file diff --git a/src/router/menu.ts b/src/router/menu.ts index fbc5fc7e..d086ddf5 100644 --- a/src/router/menu.ts +++ b/src/router/menu.ts @@ -127,6 +127,10 @@ export default [ path: '/iot-card/Dashboard', component: () => import('@/views/iot-card/Dashboard/index.vue') }, + { + path: '/iot-card/CardManagement', + component: () => import('@/views/iot-card/CardManagement/index.vue') + }, // 北向输出 { path: '/northbound/DuerOS', diff --git a/src/store/instance.ts b/src/store/instance.ts index f4d6e145..eb51df72 100644 --- a/src/store/instance.ts +++ b/src/store/instance.ts @@ -1,4 +1,4 @@ -import { DeviceInstance, InstanceModel } from "@/views/device/instance/typings"; +import { DeviceInstance, InstanceModel } from "@/views/device/Instance/typings" import { defineStore } from "pinia"; export const useInstanceStore = defineStore({ @@ -7,6 +7,7 @@ export const useInstanceStore = defineStore({ actions: { setCurrent(current: Partial) { this.current = current + this.detail = current } } }) \ No newline at end of file diff --git a/src/store/menu.ts b/src/store/menu.ts index b03e4202..42f1946d 100644 --- a/src/store/menu.ts +++ b/src/store/menu.ts @@ -1,9 +1,12 @@ import { defineStore } from "pinia"; +import { queryOwnThree } from '@/api/system/menu' +import { filterAsnycRouter } from '@/utils/menu' export const useMenuStore = defineStore({ id: 'menu', state: () => ({ - menus: {} as {[key: string]: string}, + menus: {}, + menusKey: [] }), getters: { hasPermission(state) { @@ -19,6 +22,47 @@ export const useMenuStore = defineStore({ } return false } + }, + }, + actions: { + queryMenuTree(isCommunity = false): Promise { + return new Promise(async (res) => { + //过滤非集成的菜单 + const params = [ + { + terms: [ + { + terms: [ + { + column: 'owner', + termType: 'eq', + value: 'iot', + }, + { + column: 'owner', + termType: 'isnull', + value: '1', + type: 'or', + }, + ], + }, + ], + }, + ]; + const resp = await queryOwnThree({ paging: false, terms: params }) + if (resp.success) { + const menus = filterAsnycRouter(resp.result) + menus.push({ + path: '/', + redirect: menus[0]?.path, + meta: { + hideInMenu: true + } + }) + this.menus = menus + res(menus) + } + }) } } }) \ No newline at end of file diff --git a/src/store/metadata.ts b/src/store/metadata.ts new file mode 100644 index 00000000..c793f233 --- /dev/null +++ b/src/store/metadata.ts @@ -0,0 +1,31 @@ +import { DeviceInstance, InstanceModel } from "@/views/device/Instance/typings" +import { defineStore } from "pinia"; +import type { MetadataItem, MetadataType } from '@/views/device/Product/typings' + +type MetadataModelType = { + item: MetadataItem | unknown; + edit: boolean; + type: MetadataType; + action: 'edit' | 'add'; + import: boolean; + importMetadata: boolean; +}; + +export const useMetadataStore = defineStore({ + id: 'metadata', + state: () => ({ + model: { + item: undefined, + edit: false, + type: 'events', + action: 'add', + import: false, + importMetadata: false, + } as MetadataModelType + }), + actions: { + set(key: string, value: any) { + this.model[key] = value + } + } +}) \ No newline at end of file diff --git a/src/store/system.ts b/src/store/system.ts new file mode 100644 index 00000000..e4d37681 --- /dev/null +++ b/src/store/system.ts @@ -0,0 +1,24 @@ +import { defineStore } from 'pinia'; +import { systemVersion } from '@/api/comm' +import { useMenuStore } from './menu' + +export const useSystem = defineStore('system', { + state: () => ({ + isCommunity: false + }), + actions: { + getSystemVersion(): Promise { + return new Promise(async(res, rej) => { + const resp = await systemVersion() + if (resp.success && resp.result) { + const isCommunity = resp.result.edition === 'community' + this.isCommunity = isCommunity + // 获取菜单 + const menu = useMenuStore() + const menuData: any[] = await menu.queryMenuTree(isCommunity) + res(menuData) + } + }) + } + } +}) \ No newline at end of file diff --git a/src/store/userInfo.ts b/src/store/userInfo.ts index 4cf56cc6..3a6de12e 100644 --- a/src/store/userInfo.ts +++ b/src/store/userInfo.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia'; -import { authLogin } from '@/api/login'; +import { authLogin, userDetail } from '@/api/login'; import { LocalStore } from '@/utils/comm'; import { TOKEN_KEY } from '@/utils/variable'; @@ -38,5 +38,17 @@ export const useUserInfo = defineStore('userInfo', { }); }); }, + getUserInfo() { + return new Promise((res, rej) => { + userDetail().then(resp => { + if (resp.success) { + res(true) + this.userInfos = resp.result + } else { + rej() + } + }).catch(() => rej()) + }) + } }, }); diff --git a/src/utils/comm.ts b/src/utils/comm.ts index a2bb3385..addc6a11 100644 --- a/src/utils/comm.ts +++ b/src/utils/comm.ts @@ -1,41 +1,43 @@ +import type { Slots } from 'vue' import { TOKEN_KEY } from '@/utils/variable' import { Terms } from 'components/Search/types' +import { urlReg } from '@/utils/regular' /** * 静态图片资源处理 * @param path {String} 路径 */ export const getImage = (path: string) => { - return new URL('/images'+path, import.meta.url).href + return new URL('/images' + path, import.meta.url).href } export const LocalStore = { - set(key: string, data: any) { - localStorage.setItem(key, typeof data === 'string' ? data : JSON.stringify(data)) - }, - get(key: string) { - const dataStr = localStorage.getItem(key) - try { - if (dataStr) { - const data = JSON.parse(dataStr) - return data && typeof data === 'object' ? data : dataStr - } else { - return dataStr - } - } catch (e) { - return dataStr - } - }, - remove(key: string) { - localStorage.removeItem(key) - }, - removeAll() { - localStorage.clear() + set(key: string, data: any) { + localStorage.setItem(key, typeof data === 'string' ? data : JSON.stringify(data)) + }, + get(key: string) { + const dataStr = localStorage.getItem(key) + try { + if (dataStr) { + const data = JSON.parse(dataStr) + return data && typeof data === 'object' ? data : dataStr + } else { + return dataStr + } + } catch (e) { + return dataStr } + }, + remove(key: string) { + localStorage.removeItem(key) + }, + removeAll() { + localStorage.clear() + } } export const getToken = () => { - return LocalStore.get(TOKEN_KEY) + return LocalStore.get(TOKEN_KEY) } /** @@ -45,7 +47,7 @@ export const getToken = () => { * @param key */ export const filterTreeSelectNode = (value: string, treeNode: any, key: string = 'name'): boolean => { - return treeNode[key]?.includes(value) + return treeNode[key]?.includes(value) } /** @@ -55,5 +57,47 @@ export const filterTreeSelectNode = (value: string, treeNode: any, key: string = * @param key */ export const filterSelectNode = (value: string, option: any, key: string = 'label'): boolean => { - return option[key]?.includes(value) + return option[key]?.includes(value) +} + +export function getSlot(slots: Slots, props: Record, prop = 'default'): T | false { + if (props[prop] === false) { + // force not render + return false + } + return (props[prop] || slots[prop]) as T +} + +export function getSlotVNode(slots: Slots, props: Record, prop = 'default'): T | false { + if (props[prop] === false) { + return false; + } + return (props[prop] || slots[prop]?.()) as T; +} + +/** + * 时间转换为'2022-01-02 14:03:05' + * @param date 时间对象 + * @returns + */ +export const dateFormat = (dateSouce:any):string|Error => { + let date = null + try { + date = new Date(dateSouce) + } catch (error) { + return new Error('请传入日期格式数据') + } + let year = date.getFullYear(); + let month: number | string = date.getMonth() + 1; + let day: number | string = date.getDate(); + let hour: number | string = date.getHours(); + let minutes: number | string = date.getMinutes(); + let seconds: number | string = date.getSeconds(); + month = (month < 10) ? '0' + month : month; + day = (day < 10) ? '0' + day : day; + hour = (hour < 10) ? '0' + hour : hour; + minutes = (minutes < 10) ? '0' + minutes : minutes; + seconds = (seconds < 10) ? '0' + seconds : seconds; + return year + "-" + month + "-" + day + + " " + hour + ":" + minutes + ":" + seconds; } diff --git a/src/utils/encodeQuery.ts b/src/utils/encodeQuery.ts index 19d11640..748bba26 100644 --- a/src/utils/encodeQuery.ts +++ b/src/utils/encodeQuery.ts @@ -1,3 +1,5 @@ +import { isObject } from 'lodash-es' + export default function encodeQuery(params: any) { if (!params) return {}; const queryParam = { @@ -15,7 +17,7 @@ export default function encodeQuery(params: any) { terms[k] === '' || terms[k] === undefined || terms[k].length === 0 || - terms[k] === {} || + (isObject(terms[k]) && Object.keys(terms[k]).length === 0) || terms[k] === null ) ) { diff --git a/src/utils/menu.ts b/src/utils/menu.ts new file mode 100644 index 00000000..22230e84 --- /dev/null +++ b/src/utils/menu.ts @@ -0,0 +1,111 @@ +const pagesComponent = import.meta.glob('../views/system/**/*.vue', { eager: true }); +import { BlankLayoutPage, BasicLayoutPage } from 'components/Layout' + +type ExtraRouteItem = { + code: string + name: string + url?: string +} +// 额外子级路由 +const extraRouteObj = { + 'media/Cascade': { + children: [ + { code: 'Save', name: '新增' }, + { code: 'Channel', name: '选择通道' }, + ], + }, + 'media/Device': { + children: [ + { code: 'Save', name: '详情' }, + { code: 'Channel', name: '通道列表' }, + { code: 'Playback', name: '回放' }, + ], + }, + 'rule-engine/Scene': { + children: [ + { code: 'Save', name: '详情' }, + { code: 'Save2', name: '测试详情' }, + ], + }, + 'rule-engine/Alarm/Configuration': { + children: [{ code: 'Save', name: '详情' }], + }, + 'device/Firmware': { + children: [{ code: 'Task', name: '升级任务' }], + }, + demo: { + children: [{ code: 'AMap', name: '地图' }], + }, + 'system/Platforms': { + children: [ + { code: 'Api', name: '赋权' }, + { code: 'View', name: 'Api详情' }, + ], + }, + 'system/DataSource': { + children: [{ code: 'Management', name: '管理' }], + }, + 'system/Menu': { + children: [{ code: 'Setting', name: '菜单配置' }], + }, + 'system/Apply': { + children: [ + { code: 'Api', name: '赋权' }, + { code: 'View', name: 'Api详情' }, + { code: 'Save', name: '详情' }, + ], + }, +}; + + +const resolveComponent = (name: any) => { + // TODO 暂时用system进行测试 + const importPage = pagesComponent[`../views/${name}/index.vue`]; + // if (!importPage) { + // throw new Error(`Unknown page ${name}. Is it located under Pages with a .vue extension?`); + // } + + //@ts-ignore + return !importPage ? BlankLayoutPage : importPage.default + // return importPage.default +} + +const findChildrenRoute = (code: string, url: string): ExtraRouteItem[] => { + if (extraRouteObj[code]) { + return extraRouteObj[code].children.map((route: ExtraRouteItem) => { + return { + url: `${url}/${route.code}`, + code: route.code, + name: route.name + } + }) + } + return [] +} + +export function filterAsnycRouter(asyncRouterMap: any, parentCode = '', level = 1) { + return asyncRouterMap.map((route: any) => { + + route.path = `${route.url}` + route.meta = { + icon: route.icon, + title: route.name + } + + // 查看是否有隐藏子路由 + route.children = route.children && route.children.length ? [...route.children, ...findChildrenRoute(route.code, route.url)] : findChildrenRoute(route.code, route.url) + + // TODO 查看是否具有详情页 + // route.children = [...route.children, ] + + if (route.children && route.children.length) { + route.component = () => level === 1 ? BasicLayoutPage : BlankLayoutPage + route.children = filterAsnycRouter(route.children, `${parentCode}/${route.code}`, level + 1) + route.redirect = route.children[0].url + } else { + route.component = resolveComponent(route.code); + } + console.log(route.code, route) + return route + }) +} \ No newline at end of file diff --git a/src/utils/regular.ts b/src/utils/regular.ts new file mode 100644 index 00000000..9d88336f --- /dev/null +++ b/src/utils/regular.ts @@ -0,0 +1,4 @@ +// 用于校验 url +export const urlReg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/; + +export const isUrl = (path: string): boolean => urlReg.test(path) \ No newline at end of file diff --git a/src/utils/request.ts b/src/utils/request.ts index cc606fea..9e43cb5b 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -118,6 +118,7 @@ const errorHandler = (error: any) => { const status = error.response.status if (status === 403) { Notification.error({ + key: '403', message: 'Forbidden', description: (data.message + '').substr(0, 90) }) @@ -129,22 +130,25 @@ const errorHandler = (error: any) => { }, 0) } else if (status === 500) { Notification.error({ + key: '500', message: 'Server Side Error', description: (data.message + '').substr(0, 90) }) } else if (status === 400) { Notification.error({ + key: '400', message: 'Request Error', description: (data.message + '').substr(0, 90) }) } else if (status === 401) { Notification.error({ + key: '401', message: 'Unauthorized', description: 'Authorization verification failed' }) setTimeout(() => { router.replace({ - name: 'login' + path: LoginPath }) }, 0) } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 30e21216..239ee90f 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -55,4 +55,21 @@ export const downloadObject = (record: Record, fileName: string, fo document.body.removeChild(formElement); }; // 是否不是community版本 -export const isNoCommunity = !(localStorage.getItem(SystemConst.VERSION_CODE) === 'community'); \ No newline at end of file +export const isNoCommunity = !(localStorage.getItem(SystemConst.VERSION_CODE) === 'community'); + + +/** + * 生成随机数 + * @param length + * @returns + */ +export const randomString = (length?: number) => { + const tempLength = length || 32; + const chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'; + const maxPos = chars.length; + let pwd = ''; + for (let i = 0; i < tempLength; i += 1) { + pwd += chars.charAt(Math.floor(Math.random() * maxPos)); + } + return pwd; +}; diff --git a/src/views/device/Instance/Save/index.vue b/src/views/device/Instance/Save/index.vue index ff691bd1..1301e493 100644 --- a/src/views/device/Instance/Save/index.vue +++ b/src/views/device/Instance/Save/index.vue @@ -4,8 +4,8 @@ - - + + @@ -32,6 +32,7 @@ + \ No newline at end of file diff --git a/src/views/device/components/Metadata/Base/columns.ts b/src/views/device/components/Metadata/Base/columns.ts new file mode 100644 index 00000000..7c4f0f63 --- /dev/null +++ b/src/views/device/components/Metadata/Base/columns.ts @@ -0,0 +1,91 @@ +import { JColumnProps } from "@/components/Table"; + +const SourceMap = { + device: '设备', + manual: '手动', + rule: '规则', +}; + +const type = { + read: '读', + write: '写', + report: '上报', +}; + +const BaseColumns: JColumnProps[] = [ + { + title: '标识', + dataIndex: 'id', + ellipsis: true, + }, + { + title: '名称', + dataIndex: 'name', + ellipsis: true, + }, + { + title: '说明', + dataIndex: 'description', + ellipsis: true, + }, +]; + +const EventColumns: JColumnProps[] = BaseColumns.concat([ + { + title: '事件级别', + dataIndex: 'expands', + scopedSlots: true, + }, +]); + +const FunctionColumns: JColumnProps[] = BaseColumns.concat([ + { + title: '是否异步', + dataIndex: 'async', + scopedSlots: true, + }, + // { + // title: '读写类型', + // dataIndex: 'expands', + // render: (text: any) => (text?.type || []).map((item: string | number) => {type[item]}), + // }, +]); + +const PropertyColumns: JColumnProps[] = BaseColumns.concat([ + { + title: '数据类型', + dataIndex: 'valueType', + scopedSlots: true, + }, + { + title: '属性来源', + dataIndex: 'expands', + scopedSlots: true, + }, + { + title: '读写类型', + dataIndex: 'expands', + scopedSlots: true, + }, +]); + +const TagColumns: JColumnProps[] = BaseColumns.concat([ + { + title: '数据类型', + dataIndex: 'valueType', + scopedSlots: true, + }, + { + title: '读写类型', + dataIndex: 'expands', + scopedSlots: true, + }, +]); + +const MetadataMapping = new Map(); +MetadataMapping.set('properties', PropertyColumns); +MetadataMapping.set('events', EventColumns); +MetadataMapping.set('tags', TagColumns); +MetadataMapping.set('functions', FunctionColumns); + +export default MetadataMapping; \ No newline at end of file diff --git a/src/views/device/components/Metadata/Base/index.vue b/src/views/device/components/Metadata/Base/index.vue new file mode 100644 index 00000000..35e9dfed --- /dev/null +++ b/src/views/device/components/Metadata/Base/index.vue @@ -0,0 +1,100 @@ + + + \ No newline at end of file diff --git a/src/views/device/components/Metadata/Import/index.vue b/src/views/device/components/Metadata/Import/index.vue index eda57092..fb6028bd 100644 --- a/src/views/device/components/Metadata/Import/index.vue +++ b/src/views/device/components/Metadata/Import/index.vue @@ -47,14 +47,16 @@ import { saveMetadata } from '@/api/device/instance' import { queryNoPagingPost, convertMetadata, modify } from '@/api/device/product' import type { DefaultOptionType } from 'ant-design-vue/es/select'; import { UploadProps } from 'ant-design-vue/es'; -import type { DeviceMetadata } from '@/views/device/Product/typings' +import type { DeviceMetadata, ProductItem } from '@/views/device/Product/typings' import { message } from 'ant-design-vue/es'; import { Store } from 'jetlinks-store'; import { SystemConst } from '@/utils/consts'; import { useInstanceStore } from '@/store/instance' +import { useProductStore } from '@/store/product'; const route = useRoute() const instanceStore = useInstanceStore() +const productStore = useProductStore() interface Props { visible: boolean, @@ -191,8 +193,10 @@ const handleImport = async () => { const { id } = route.params || {} if (props?.type === 'device') { await saveMetadata(id as string, metadata) + instanceStore.setCurrent(JSON.parse(metadata || '{}')) } else { await modify(id as string, { metadata: metadata }) + productStore.setCurrent(JSON.parse(metadata || '{}')) } loading.value = false // MetadataAction.insert(JSON.parse(metadata || '{}')); @@ -231,10 +235,12 @@ const handleImport = async () => { if (props?.type === 'device') { const metadata: DeviceMetadata = JSON.parse(paramsDevice || '{}') // MetadataAction.insert(metadata); + instanceStore.setCurrent(metadata) message.success('导入成功') } else { - const metadata: DeviceMetadata = JSON.parse(params?.metadata || '{}') + const metadata: ProductItem = JSON.parse(params?.metadata || '{}') // MetadataAction.insert(metadata); + productStore.setCurrent(metadata) message.success('导入成功') } } diff --git a/src/views/device/components/Metadata/metadata.ts b/src/views/device/components/Metadata/metadata.ts new file mode 100644 index 00000000..134a67c0 --- /dev/null +++ b/src/views/device/components/Metadata/metadata.ts @@ -0,0 +1,61 @@ +import { saveProductMetadata } from "@/api/device/product"; +import { saveMetadata } from "@/api/device/instance"; +import type { DeviceInstance } from "../../Instance/typings"; +import type { DeviceMetadata, MetadataItem, MetadataType, ProductItem } from "../../Product/typings"; + +/** + * 更新物模型 + * @param type 物模型类型 events + * @param item 物模型数据 【{a},{b},{c}】 + // * @param target product、device + * @param data product 、device [{event:[1,2,3]] + * @param onEvent 数据更新回调:更新数据库、发送事件等操作 + * + */ + export const updateMetadata = ( + type: MetadataType, + item: MetadataItem[], + // target: 'product' | 'device', + data: ProductItem | DeviceInstance, + onEvent?: (item: string) => void, +): ProductItem | DeviceInstance => { + if (!data) return data; + const metadata = JSON.parse(data.metadata || '{}') as DeviceMetadata; + const config = (metadata[type] || []) as MetadataItem[]; + if (item.length > 0) { + item.forEach((i) => { + const index = config.findIndex((c) => c.id === i.id); + if (index > -1) { + config[index] = i; + // onEvent?.('update', i); + } else { + config.push(i); + // onEvent?.('add', i); + } + }); + } else { + console.warn('未触发物模型修改'); + } + // @ts-ignore + metadata[type] = config.sort((a, b) => b?.sortsIndex - a?.sortsIndex); + data.metadata = JSON.stringify(metadata); + onEvent?.(data.metadata) + return data; +}; + +/** + * 保存物模型数据到服务器 + * @param type 类型 + * @param data 数据 + */ +export const asyncUpdateMetadata = ( + type: 'product' | 'device', + data: ProductItem | DeviceInstance, +): Promise => { + switch (type) { + case 'product': + return saveProductMetadata(data); + case 'device': + return saveMetadata(data.id, JSON.parse(data.metadata || '{}')); + } +}; \ No newline at end of file diff --git a/src/views/home/components/StatusLabel.vue b/src/views/home/components/StatusLabel.vue new file mode 100644 index 00000000..59a720f6 --- /dev/null +++ b/src/views/home/components/StatusLabel.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/src/views/home/components/StepCard.vue b/src/views/home/components/StepCard.vue index ccc87a5d..661de226 100644 --- a/src/views/home/components/StepCard.vue +++ b/src/views/home/components/StepCard.vue @@ -21,11 +21,11 @@
- - @@ -38,8 +38,8 @@ import { PropType } from 'vue'; import { QuestionCircleOutlined } from '@ant-design/icons-vue'; import { message } from 'ant-design-vue'; -import AccessMethodDialog from './dialogs/AccessMethodDialog.vue'; -import FuncTestDialog from './dialogs/FuncTestDialog.vue'; +import ProductChooseDialog from './dialogs/ProductChooseDialog.vue'; +import DeviceChooseDialog from './dialogs/DeviceChooseDialog.vue'; import { recommendList } from '../index'; @@ -73,9 +73,8 @@ const jumpPage = (row: recommendList) => { } }; // 弹窗返回后的二次跳转 -const againJumpPage = (paramsSource: object) => { - const params = { ...(selectRow.params || {}), ...paramsSource }; - router.push(`${selectRow.linkUrl}${objToParams(params || {})}`); +const againJumpPage = (params: string) => { + router.push(`${selectRow.linkUrl}/${params}`); }; const objToParams = (source: object): string => { diff --git a/src/views/home/components/dialogs/DeviceChooseDialog.vue b/src/views/home/components/dialogs/DeviceChooseDialog.vue new file mode 100644 index 00000000..18a03947 --- /dev/null +++ b/src/views/home/components/dialogs/DeviceChooseDialog.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/src/views/home/components/dialogs/FuncTestDialog.vue b/src/views/home/components/dialogs/FuncTestDialog.vue deleted file mode 100644 index f04db868..00000000 --- a/src/views/home/components/dialogs/FuncTestDialog.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - - - diff --git a/src/views/home/components/dialogs/AccessMethodDialog.vue b/src/views/home/components/dialogs/ProductChooseDialog.vue similarity index 96% rename from src/views/home/components/dialogs/AccessMethodDialog.vue rename to src/views/home/components/dialogs/ProductChooseDialog.vue index 6bf83d48..07cb6e95 100644 --- a/src/views/home/components/dialogs/AccessMethodDialog.vue +++ b/src/views/home/components/dialogs/ProductChooseDialog.vue @@ -53,7 +53,7 @@ const productList = ref<[productItem] | []>([]); const getContainer = () => proxy?.$refs.modal as HTMLElement; const getOptions = () => { - getProductList_api().then((resp) => { + getProductList_api().then((resp:any) => { productList.value = resp.result .filter((i: any) => !i?.accessId) .map((item: { name: any; id: any }) => ({ @@ -63,7 +63,7 @@ const getOptions = () => { }); }; const handleOk = () => { - emits('confirm', form.value); + emits('confirm', form.value.productId); visible.value = false; }; const filterOption = (input: string, option: any) => { diff --git a/src/views/home/index.d.ts b/src/views/home/index.d.ts index 6b8a22bb..19b3c366 100644 --- a/src/views/home/index.d.ts +++ b/src/views/home/index.d.ts @@ -8,7 +8,11 @@ export interface recommendList { auth: boolean; dialogTag?: 'accessMethod' | 'funcTest'; } - +// 产品列表里的每项 +export interface productItem { + label: string; + value: string +} export interface deviceInfo { deviceId: string, deviceName: string, diff --git a/src/views/home/modules/config.ts b/src/views/home/modules/config.ts index 5857b8d0..377db512 100644 --- a/src/views/home/modules/config.ts +++ b/src/views/home/modules/config.ts @@ -1,9 +1,10 @@ // import {getImage} from '@/utils/comm' +import { useMenuStore } from "@/store/menu"; import { usePermissionStore } from "@/store/permission"; import { recommendList, bootConfig } from "../index"; -// 权限控制 +// 按钮权限控制 const hasPermission = usePermissionStore().hasPermission; const productPermission = (action: string) => hasPermission(`device/Product:${action}`); @@ -11,6 +12,8 @@ const devicePermission = (action: string) => hasPermission(`device/Instance:${action}`); const rulePermission = (action: string) => hasPermission(`rule-engine/Instance:${action}`); +// 页面权限控制 +const menuPermission = useMenuStore().hasPermission // 物联网引导-数据 @@ -18,7 +21,7 @@ export const deviceBootConfig: bootConfig[] = [ { english: 'STEP1', label: '创建产品', - link: '/a', + link: '/iot/device/Product', auth: productPermission('add'), params: { save: true, @@ -27,7 +30,7 @@ export const deviceBootConfig: bootConfig[] = [ { english: 'STEP2', label: '创建设备', - link: '/b', + link: '/iot/device/Instance', auth: devicePermission('add'), params: { save: true, @@ -36,7 +39,7 @@ export const deviceBootConfig: bootConfig[] = [ { english: 'STEP3', label: '规则引擎', - link: '/c', + link: '/iot/rule-engine/Instance', auth: rulePermission('add'), params: { save: true, @@ -50,7 +53,7 @@ export const deviceStepDetails: recommendList[] = [ details: '产品是设备的集合,通常指一组具有相同功能的设备。物联设备必须通过产品进行接入方式配置。', iconUrl: '/images/home/bottom-4.png', - linkUrl: '/a', + linkUrl: '/iot/device/Product', auth: productPermission('add'), params: { save: true, @@ -61,7 +64,7 @@ export const deviceStepDetails: recommendList[] = [ details: '通过产品对同一类型的设备进行统一的接入方式配置。请参照设备铭牌说明选择匹配的接入方式。', iconUrl: '/images/home/bottom-1.png', - linkUrl: '/a', + linkUrl: '/iot/device/Product/detail', auth: productPermission('update'), dialogTag: 'accessMethod', }, @@ -69,7 +72,7 @@ export const deviceStepDetails: recommendList[] = [ title: '添加测试设备', details: '添加单个设备,用于验证产品模型是否配置正确。', iconUrl: '/images/home/bottom-5.png', - linkUrl: '/a', + linkUrl: '/iot/device/Instance', auth: devicePermission('add'), params: { save: true, @@ -80,15 +83,16 @@ export const deviceStepDetails: recommendList[] = [ details: '对添加的测试设备进行功能调试,验证能否连接到平台,设备功能是否配置正确。', iconUrl: '/images/home/bottom-2.png', - linkUrl: '/a', - auth: devicePermission('update'), + linkUrl: '/iot/device/Instance/detail', + // auth: devicePermission('update'), + auth: true, dialogTag: 'funcTest', }, { title: '批量添加设备', details: '批量添加同一产品下的设备', iconUrl: '/images/home/bottom-3.png', - linkUrl: '/a', + linkUrl: '/iot/device/Instance', auth: devicePermission('import'), params: { import: true, @@ -102,14 +106,14 @@ export const opsBootConfig: bootConfig[] = [ { english: 'STEP1', label: '设备接入配置', - link: '/a', - auth: true, + link: '/iot/link/accessConfig', + auth: menuPermission('link/accessConfig'), }, { english: 'STEP2', label: '日志排查', - link: '/b', - auth: true, + link: '/iot/link/Log', + auth: menuPermission('link/Log'), params: { key: 'system', }, @@ -117,8 +121,8 @@ export const opsBootConfig: bootConfig[] = [ { english: 'STEP3', label: '实时监控', - link: '/c', - auth: false, + link: '/iot/link/dashboard', + auth: menuPermission('link/dashboard'), params: { save: true, }, @@ -131,44 +135,38 @@ export const opsStepDetails: recommendList[] = [ details: '根据业务需求自定义开发对应的产品(设备模型)接入协议,并上传到平台。', iconUrl: '/images/home/bottom-1.png', - linkUrl: '/a', - auth: true, - params: { - a: 1, - save: true, - }, + linkUrl: '/iot/link/protocol', + auth: menuPermission('link/Protocol'), + }, { title: '证书管理', details: '统一维护平台内的证书,用于数据通信加密。', iconUrl: '/images/home/bottom-6.png', - linkUrl: '/a', - auth: true, - params: { - a: 1, - save: false, - }, + linkUrl: '/iot/link/Certificate', + auth: menuPermission('link/Certificate'), + }, { title: '网络组件', details: '根据不同的传输类型配置平台底层网络组件相关参数。', iconUrl: '/images/home/bottom-3.png', - linkUrl: '/a', - auth: true, + linkUrl: '/iot/link/type', + auth: menuPermission('link/Type'), }, { title: '设备接入网关', details: '根据不同的传输类型,关联消息协议,配置设备接入网关相关参数。', iconUrl: '/images/home/bottom-4.png', - linkUrl: '/a', - auth: true, + linkUrl: '/iot/link/accessConfig', + auth: menuPermission('link/AccessConfig'), }, { title: '日志管理', details: '监控系统日志,及时处理系统异常。', iconUrl: '/images/home/bottom-5.png', - linkUrl: '/a', - auth: false, + linkUrl: '/iot/link/Log', + auth: menuPermission('Log'), params: { key: 'system', } diff --git a/src/views/iot-card/CardManagement/index.vue b/src/views/iot-card/CardManagement/index.vue new file mode 100644 index 00000000..6ab563d4 --- /dev/null +++ b/src/views/iot-card/CardManagement/index.vue @@ -0,0 +1,571 @@ + + + + + + diff --git a/src/views/link/AccessConfig/Detail/index.vue b/src/views/link/AccessConfig/Detail/index.vue index e8c820a4..f84b13fb 100644 --- a/src/views/link/AccessConfig/Detail/index.vue +++ b/src/views/link/AccessConfig/Detail/index.vue @@ -16,6 +16,12 @@ /> + +
@@ -28,6 +34,8 @@ import Provider from '../components/Provider/index.vue'; import { getProviders, detail } from '@/api/link/accessConfig'; import Media from '../components/Media/index.vue'; import Channel from '../components/Channel/index.vue'; +import Edge from '../components/Edge/index.vue'; +import Cloud from '../components/Cloud/index.vue'; // const router = useRouter(); const route = useRoute(); diff --git a/src/views/link/AccessConfig/components/Channel/index.vue b/src/views/link/AccessConfig/components/Channel/index.vue index 658c940d..15b30253 100644 --- a/src/views/link/AccessConfig/components/Channel/index.vue +++ b/src/views/link/AccessConfig/components/Channel/index.vue @@ -82,10 +82,11 @@ - + + diff --git a/src/views/link/AccessConfig/components/Cloud/OneNet.vue b/src/views/link/AccessConfig/components/Cloud/OneNet.vue new file mode 100644 index 00000000..79f91f71 --- /dev/null +++ b/src/views/link/AccessConfig/components/Cloud/OneNet.vue @@ -0,0 +1,736 @@ + + + + + diff --git a/src/views/link/AccessConfig/components/Cloud/index.vue b/src/views/link/AccessConfig/components/Cloud/index.vue new file mode 100644 index 00000000..dfa6dc39 --- /dev/null +++ b/src/views/link/AccessConfig/components/Cloud/index.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/src/views/link/AccessConfig/components/Edge/index.vue b/src/views/link/AccessConfig/components/Edge/index.vue new file mode 100644 index 00000000..6798e357 --- /dev/null +++ b/src/views/link/AccessConfig/components/Edge/index.vue @@ -0,0 +1,492 @@ + + + + + diff --git a/src/views/link/AccessConfig/components/Media/GB28181.vue b/src/views/link/AccessConfig/components/Media/GB28181.vue index 239486ec..77540348 100644 --- a/src/views/link/AccessConfig/components/Media/GB28181.vue +++ b/src/views/link/AccessConfig/components/Media/GB28181.vue @@ -6,7 +6,7 @@
- + 配置设备信令参数
@@ -511,7 +511,12 @@ import { message, Form } from 'ant-design-vue'; import type { FormInstance } from 'ant-design-vue'; import { getResourcesCurrent, getClusters } from '@/api/link/accessConfig'; -import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue'; +import { + DeleteOutlined, + PlusOutlined, + QuestionCircleOutlined, + InfoCircleOutlined, +} from '@ant-design/icons-vue'; import { update, save } from '@/api/link/accessConfig'; interface Form2 { diff --git a/src/views/link/AccessConfig/components/Network.vue b/src/views/link/AccessConfig/components/Network.vue index edd2c5c1..04bebf34 100644 --- a/src/views/link/AccessConfig/components/Network.vue +++ b/src/views/link/AccessConfig/components/Network.vue @@ -6,7 +6,7 @@
- + 选择与设备通信的网络组件
- + 使用选择的消息协议,对网络组件通信数据进行编解码、认证等操作