-
-
-
Upload
+
+
+
+
+
+
+
+ 点击修改
+
+
+
+
+
+
+
+
+
-
+
\ 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 @@
+
+
+
+ {{slotProps.route.breadcrumbName}}
+ {{slotProps.route.breadcrumbName}}
+
+
+
+
+
+
+
+
+
+
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 () => (
+ <>
+
+ >
+ )
+ }
+})
\ 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 ? (
+
+ ) : (
+
+ );
+
+ 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 ? (
+
+ ) : (
+
+ );
+
+ 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 () => (
+
+ )
+ }
+})
\ 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
;
+ }
+ 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}
+
+
+
+ >
+ )
+}
+
+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 @@
+
+
+
+ {{ props.statusLabel }}
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ {{ dateFormat(slotProps.modifyTime) }}
+
+
+
+
+
+
+
+ 取消
+ 确认
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+ 新增
+
+
+
+ 批量操作
+
+
+
+
+
+
+
+ 批量导出
+
+
+
+ 批量导入
+
+
+
+
+
+ 批量激活
+
+
+
+
+
+
+
+ 批量停用
+
+
+
+
+
+
+
+ 批量复机
+
+
+
+
+
+
+
+ 同步状态
+
+
+
+
+
+
+
+ 批量删除
+
+
+
+
+
+
+
+
+
+
+ {{
+ slotProps.totalFlow
+ ? slotProps.totalFlow.toFixed(2) + ' M'
+ : ''
+ }}
+
+
+
+
+ {{
+ slotProps.usedFlow
+ ? slotProps.usedFlow.toFixed(2) + ' M'
+ : ''
+ }}
+
+
+
+
+ {{
+ slotProps.residualFlow
+ ? slotProps.residualFlow.toFixed(2) + ' M'
+ : ''
+ }}
+
+
+
+ {{ slotProps.cardType.text }}
+
+
+ {{ slotProps.cardStateType.text }}
+
+
+ {{
+ slotProps.activationDate
+ ? moment(slotProps.activationDate).format(
+ 'YYYY-MM-DD HH:mm:ss',
+ )
+ : ''
+ }}
+
+
+ {{
+ slotProps.updateTime
+ ? moment(slotProps.updateTime).format(
+ 'YYYY-MM-DD HH:mm:ss',
+ )
+ : ''
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@