diff --git a/components/index.js b/components/index.js index a69da8c32..d39c17f90 100644 --- a/components/index.js +++ b/components/index.js @@ -58,7 +58,7 @@ import { default as Input } from './input' import { default as InputNumber } from './input-number' -// import { default as Layout } from './layout' +import { default as Layout } from './layout' // import { default as List } from './list' @@ -154,6 +154,11 @@ const components = [ Input.Search, Input.TextArea, InputNumber, + Layout, + Layout.Header, + Layout.Footer, + Layout.Sider, + Layout.Content, LocaleProvider, Menu, Menu.Item, @@ -235,6 +240,7 @@ export { Icon, Input, InputNumber, + Layout, LocaleProvider, Menu, Modal, diff --git a/components/layout/Sider.jsx b/components/layout/Sider.jsx new file mode 100644 index 000000000..f422f40a8 --- /dev/null +++ b/components/layout/Sider.jsx @@ -0,0 +1,239 @@ +// matchMedia polyfill for +// https://github.com/WickyNilliams/enquire.js/issues/82 +if (typeof window !== 'undefined') { + const matchMediaPolyfill = (mediaQuery) => { + return { + media: mediaQuery, + matches: false, + addListener () { + }, + removeListener () { + }, + } + } + window.matchMedia = window.matchMedia || matchMediaPolyfill +} + +import classNames from 'classnames' +import PropTypes from '../_util/vue-types' +import Icon from '../icon' +import { initDefaultProps, getOptionProps, hasProp, getComponentFromProp } from '../_util/props-util' +import BaseMixin from '../_util/BaseMixin' + +const dimensionMap = { + xs: '480px', + sm: '576px', + md: '768px', + lg: '992px', + xl: '1200px', + xxl: '1600px', +} + +// export type CollapseType = 'clickTrigger' | 'responsive'; + +export const SiderProps = { + prefixCls: PropTypes.string, + collapsible: PropTypes.bool, + collapsed: PropTypes.bool, + defaultCollapsed: PropTypes.bool, + reverseArrow: PropTypes.bool, + // onCollapse?: (collapsed: boolean, type: CollapseType) => void; + trigger: PropTypes.any, + width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + collapsedWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + breakpoint: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl', 'xxl']), +} + +// export interface SiderState { +// collapsed?: boolean; +// below: boolean; +// belowShow?: boolean; +// } + +// export interface SiderContext { +// siderCollapsed: boolean; +// } + +const generateId = (() => { + let i = 0 + return (prefix = '') => { + i += 1 + return `${prefix}${i}` + } +})() + +export default { + name: 'ALayoutSider', + __ANT_LAYOUT_SIDER: true, + mixins: [BaseMixin], + props: initDefaultProps(SiderProps, { + prefixCls: 'ant-layout-sider', + collapsible: false, + defaultCollapsed: false, + reverseArrow: false, + width: 200, + collapsedWidth: 80, + }), + + // static childContextTypes = { + // siderCollapsed: PropTypes.bool, + // collapsedWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + // }; + + // static contextTypes = { + // siderHook: PropTypes.object, + // }; + + // private mql: MediaQueryList; + // private uniqueId: string; + + data () { + this.uniqueId = generateId('ant-sider-') + let matchMedia + if (typeof window !== 'undefined') { + matchMedia = window.matchMedia + } + const props = getOptionProps(this) + if (matchMedia && props.breakpoint && props.breakpoint in dimensionMap) { + this.mql = matchMedia(`(max-width: ${dimensionMap[props.breakpoint]})`) + } + let sCollapsed + if ('collapsed' in props) { + sCollapsed = props.collapsed + } else { + sCollapsed = props.defaultCollapsed + } + return { + sCollapsed, + below: false, + belowShow: false, + } + }, + provide () { + return { + layoutSiderContext: this, // menu组件中使用 + } + }, + inject: { + siderHook: { default: {}}, + }, + // getChildContext() { + // return { + // siderCollapsed: this.state.collapsed, + // collapsedWidth: this.props.collapsedWidth, + // }; + // } + watch: { + collapsed (val) { + this.setState({ + sCollapsed: val, + }) + }, + }, + + mounted () { + this.$nextTick(() => { + if (this.mql) { + this.mql.addListener(this.responsiveHandler) + this.responsiveHandler(this.mql) + } + + if (this.siderHook.addSider) { + this.siderHook.addSider(this.uniqueId) + } + }) + }, + + beforeDestroy () { + if (this.mql) { + this.mql.removeListener(this.responsiveHandler) + } + + if (this.siderHook.removeSider) { + this.siderHook.removeSider(this.uniqueId) + } + }, + model: { + prop: 'collapsed', + event: 'collapse', + }, + methods: { + responsiveHandler (mql) { + this.setState({ below: mql.matches }) + if (this.sCollapsed !== mql.matches) { + this.setCollapsed(mql.matches, 'responsive') + } + }, + + setCollapsed (collapsed, type) { + if (!hasProp(this, 'collapsed')) { + this.setState({ + sCollapsed: collapsed, + }) + } + this.$emit('collapse', collapsed, type) + }, + + toggle () { + const collapsed = !this.sCollapsed + this.setCollapsed(collapsed, 'clickTrigger') + }, + + belowShowChange () { + this.setState({ belowShow: !this.belowShow }) + }, + }, + + render () { + const { prefixCls, + collapsible, reverseArrow, width, collapsedWidth, + } = getOptionProps(this) + const trigger = getComponentFromProp(this, 'trigger') + let siderWidth = this.sCollapsed ? collapsedWidth : width + siderWidth = typeof siderWidth === 'string' ? siderWidth.replace('px', '') : siderWidth + // special trigger when collapsedWidth == 0 + const zeroWidthTrigger = collapsedWidth === 0 || collapsedWidth === '0' || collapsedWidth === '0px' ? ( + + + + ) : null + const iconObj = { + 'expanded': reverseArrow ? : , + 'collapsed': reverseArrow ? : , + } + const status = this.sCollapsed ? 'collapsed' : 'expanded' + const defaultTrigger = iconObj[status] + const triggerDom = ( + trigger !== null + ? zeroWidthTrigger || ( +
+ {trigger || defaultTrigger} +
+ ) : null + ) + const divStyle = { + // ...style, + flex: `0 0 ${siderWidth}px`, + maxWidth: `${siderWidth}px`, // Fix width transition bug in IE11 + minWidth: `${siderWidth}px`, // https://github.com/ant-design/ant-design/issues/6349 + width: `${siderWidth}px`, + } + const siderCls = classNames(prefixCls, { + [`${prefixCls}-collapsed`]: !!this.sCollapsed, + [`${prefixCls}-has-trigger`]: collapsible && trigger !== null && !zeroWidthTrigger, + [`${prefixCls}-below`]: !!this.below, + [`${prefixCls}-zero-width`]: siderWidth === 0 || siderWidth === '0' || siderWidth === '0px', + }) + const divProps = { + on: this.$listeners, + class: siderCls, + style: divStyle, + } + return ( +
+
{this.$slots.default}
+ {collapsible || (this.below && zeroWidthTrigger) ? triggerDom : null} +
+ ) + }, +} diff --git a/components/layout/demo/basic.md b/components/layout/demo/basic.md new file mode 100644 index 000000000..5cb9907d9 --- /dev/null +++ b/components/layout/demo/basic.md @@ -0,0 +1,79 @@ + +#### 基本结构 +典型的页面布局。 + + + +#### Basic Structure +Classic page layouts. + + +```html + + + +``` diff --git a/components/layout/demo/custom-trigger.md b/components/layout/demo/custom-trigger.md new file mode 100644 index 000000000..8882fe689 --- /dev/null +++ b/components/layout/demo/custom-trigger.md @@ -0,0 +1,77 @@ + +#### 自定义触发器 +要使用自定义触发器,可以设置 `:trigger="null"` 来隐藏默认设定。 + + + +#### Custom trigger +If you want to use a customized trigger, you can hide the default one by setting `:trigger="null"`. + + +```html + + + +``` diff --git a/components/layout/index.en-US.md b/components/layout/index.en-US.md new file mode 100644 index 000000000..b04925f8f --- /dev/null +++ b/components/layout/index.en-US.md @@ -0,0 +1,113 @@ +--- +category: Components +type: Layout +cols: 1 +title: Layout +--- + +Handling the overall layout of a page. + +## Specification + +### Size + +The first level navigation is inclined left near a logo, and the secondary menu is inclined right. + +- Top Navigation (almost systems): the height of the first level navigation `64px`, the second level navigation `48px`. +- Top Navigation(contents page): the height of the first level navigation `80px`, the second level navigation `56px`. +- Calculation formula of a top navigation: `48+8n`. +- Calculation formula of an aside navigation: `200+8n`. + +### Interaction rules + +- The first level navigation and the last level navigation should be distincted by visualization; +- The current item should have the highest priority of visualization; +- When the current navigation item is collapsed, the stlye of the current navigation item will be applied to its parent level; +- The left side navigation bar has support for both the accordion and expanding styles, you can choose the one that fits your case best. + +## Visualization rules + + Style of a navigation should conform to its level. + +- **Emphasis by colorblock** + + When background color is a deep color, you can use this pattern for the parent level navigation item of current page. + +- **The highlight match stick** + + When background color is a light color, you can use this pattern for the current page navigation item, we recommed using it for the last item of the navigation path. + +- **Hightlighted font** + + From the visualization aspect, hightlighted font is stronger than colorblock, this pattern is often used for the parent level of the current item. + +- **Enlarge the size of the font** + + `12px`、`14px` is a standard font size of navigations,`14px` is used for the first and the second level of the navigation. You can choose a appropriate font size in terms of the level of your navigation. + +## Component Overview + +- `Layout`: The layout wrapper, in which `Header` `Sider` `Content` `Footer` or `Layout` itself can be nested, and can be placed in any parent container. +- `Header`: The top layout with default style, in which any element can be nested, and must be placed in `Layout`. +- `Sider`: The sidebar with default style and basic functions, in which any element can be nested, and must be placed in `Layout`. +- `Content`: The content layout with default style, in which any element can be nested, and must be placed in `Layout`. +- `Footer`: The bottom layout with default style, in which any element can be nested, and must be placed in `Layout`. + +> Based on `flex layout`, please pay attention to the [compatibility](http://caniuse.com/#search=flex). + +## API + +```jsx + +
header
+ + left sidebar + main content + right sidebar + +
footer
+
+``` + +### Layout + +The wrapper. + +| Property | Description | Type | Default | +| -------- | ----------- | ---- | ------- | +| className | container className | string | - | +| style | to customize the styles | object | - | +| hasSider | whether contain Sider in children, don't have to assign it normally. Useful in ssr avoid style flickering | boolean | - | + +> APIs of `Layout.Header` `Layout.Footer` `Layout.Content` are the same as that of `Layout`. + +### Layout.Sider + +The sidebar. + +| Property | Description | Type | Default | +| -------- | ----------- | ---- | ------- | +| breakpoint | [breakpoints](/components/grid#api) of the responsive layout | Enum { 'xs', 'sm', 'md', 'lg', 'xl', 'xxl' } | - | +| className | container className | string | - | +| collapsed | to set the current status | boolean | - | +| collapsedWidth | width of the collapsed sidebar, by setting to `0` a special trigger will appear | number | 64 | +| collapsible | whether can be collapsed | boolean | false | +| defaultCollapsed | to set the initial status | boolean | false | +| reverseArrow | reverse direction of arrow, for a sider that expands from the right | boolean | false | +| style | to customize the styles | object | - | +| trigger | specify the customized trigger, set to null to hide the trigger | string\|ReactNode | - | +| width | width of the sidebar | number\|string | 200 | +| onCollapse | the callback function, executed by clicking the trigger or activating the responsive layout | (collapsed, type) => {} | - | + +#### breakpoint width + +```js +{ + xs: '480px', + sm: '576px', + md: '768px', + lg: '992px', + xl: '1200px', + xxl: '1600px', +} +``` diff --git a/components/layout/index.js b/components/layout/index.js new file mode 100644 index 000000000..d4a0328d7 --- /dev/null +++ b/components/layout/index.js @@ -0,0 +1,5 @@ +import Layout from './layout' +import Sider from './Sider' + +Layout.Sider = Sider +export default Layout diff --git a/components/layout/index.zh-CN.md b/components/layout/index.zh-CN.md new file mode 100644 index 000000000..21fb73d40 --- /dev/null +++ b/components/layout/index.zh-CN.md @@ -0,0 +1,114 @@ +--- +category: Components +subtitle: 布局 +type: Layout +cols: 1 +title: Layout +--- + +协助进行页面级整体布局。 + +## 设计规则 + +### 尺寸 + +一级导航项偏左靠近 logo 放置,辅助菜单偏右放置。 + +- 顶部导航(大部分系统):一级导航高度 `64px`,二级导航 `48px`。 +- 顶部导航(展示类页面):一级导航高度 `80px`,二级导航 `56px`。 +- 顶部导航高度的范围计算公式为:`48+8n`。 +- 侧边导航宽度的范围计算公式:`200+8n`。 + +### 交互 + +- 一级导航和末级的导航需要在可视化的层面被强调出来; +- 当前项应该在呈现上优先级最高; +- 当导航收起的时候,当前项的样式自动赋予给它的上一个层级; +- 左侧导航栏的收放交互同时支持手风琴和全展开的样式,根据业务的要求进行适当的选择。 + +### 视觉 + +导航样式上需要根据信息层级合理的选择样式: + +- **大色块强调** + + 建议用于底色为深色系时,当前页面父级的导航项。 + +- **高亮火柴棍** + + 当导航栏底色为浅色系时使用,可用于当前页面对应导航项,建议尽量在导航路径的最终项使用。 + +- **字体高亮变色** + + 从可视化层面,字体高亮的视觉强化力度低于大色块,通常在当前项的上一级使用。 + +- **字体放大** + + `12px`、`14px` 是导航的标准字号,14 号字体用在一、二级导航中。字号可以考虑导航项的等级做相应选择。 + +## 组件概述 + +- `Layout`:布局容器,其下可嵌套 `Header` `Sider` `Content` `Footer` 或 `Layout` 本身,可以放在任何父容器中。 +- `Header`:顶部布局,自带默认样式,其下可嵌套任何元素,只能放在 `Layout` 中。 +- `Sider`:侧边栏,自带默认样式及基本功能,其下可嵌套任何元素,只能放在 `Layout` 中。 +- `Content`:内容部分,自带默认样式,其下可嵌套任何元素,只能放在 `Layout` 中。 +- `Footer`:底部布局,自带默认样式,其下可嵌套任何元素,只能放在 `Layout` 中。 + +> 注意:采用 flex 布局实现,请注意[浏览器兼容性](http://caniuse.com/#search=flex)问题。 + +## API + +```jsx + +
header
+ + left sidebar + main content + right sidebar + +
footer
+
+``` + +### Layout + +布局容器。 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| class | 容器 class | string | - | +| style | 指定样式 | object | - | +| hasSider | 表示子元素里有 Sider,一般不用指定。可用于服务端渲染时避免样式闪动 | boolean | - | + +> `Layout.Header` `Layout.Footer` `Layout.Content` API 与 `Layout` 相同 + +### Layout.Sider + +侧边栏。 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| breakpoint | 触发响应式布局的[断点](/components/grid#api) | Enum { 'xs', 'sm', 'md', 'lg', 'xl', 'xxl' } | - | +| class | 容器 class | string | - | +| collapsed | 当前收起状态 | boolean | - | +| collapsedWidth | 收缩宽度,设置为 0 会出现特殊 trigger | number | 64 | +| collapsible | 是否可收起 | boolean | false | +| defaultCollapsed | 是否默认收起 | boolean | false | +| reverseArrow | 翻转折叠提示箭头的方向,当 Sider 在右边时可以使用 | boolean | false | +| style | 指定样式 | object | - | +| trigger | 自定义 trigger,设置为 null 时隐藏 trigger | string\|slot | - | +| width | 宽度 | number\|string | 200 | +| onCollapse | 展开-收起时的回调函数,有点击 trigger 以及响应式反馈两种方式可以触发 | (collapsed, type) => {} | - | + +#### breakpoint width + +```js +{ + xs: '480px', + sm: '576px', + md: '768px', + lg: '992px', + xl: '1200px', + xxl: '1600px', +} +``` diff --git a/components/layout/layout.jsx b/components/layout/layout.jsx new file mode 100644 index 000000000..0aae2a37b --- /dev/null +++ b/components/layout/layout.jsx @@ -0,0 +1,98 @@ +import PropTypes from '../_util/vue-types' +import classNames from 'classnames' +import { getOptionProps } from '../_util/props-util' + +export const BasicProps = { + prefixCls: PropTypes.string, + hasSider: PropTypes.boolean, +} + +function generator (props, name) { + return (BasicComponent) => { + return { + name, + props: BasicComponent.props, + render () { + const { prefixCls } = props + const basicComponentProps = { + props: { + prefixCls, + ...getOptionProps(this), + }, + on: this.$listeners, + } + return {this.$slots.default} + }, + } + } +} + +const Basic = { + props: BasicProps, + render () { + const { prefixCls, $slots, $listeners } = this + const divProps = { + class: prefixCls, + on: $listeners, + } + return ( +
{$slots.default}
+ ) + }, +} + +const BasicLayout = { + props: BasicProps, + data () { + return { + siders: [], + } + }, + provide () { + return { + siderHook: { + addSider: (id) => { + this.siders = [...this.siders, id] + }, + removeSider: (id) => { + this.siders = this.siders.filter(currentId => currentId !== id) + }, + }, + } + }, + render () { + const { prefixCls, $slots, hasSider, $listeners } = this + const divCls = classNames(prefixCls, { + [`${prefixCls}-has-sider`]: hasSider || this.siders.length > 0, + }) + const divProps = { + class: divCls, + on: $listeners, + } + return ( +
{$slots.default}
+ ) + }, +} + +const Layout = generator({ + prefixCls: 'ant-layout', +}, 'ALayout')(BasicLayout) + +const Header = generator({ + prefixCls: 'ant-layout-header', +}, 'ALayoutHeader')(Basic) + +const Footer = generator({ + prefixCls: 'ant-layout-footer', +}, 'ALayoutFooter')(Basic) + +const Content = generator({ + prefixCls: 'ant-layout-content', +}, 'ALayoutContent')(Basic) + +Layout.Header = Header +Layout.Footer = Footer +Layout.Content = Content + +export default Layout diff --git a/components/layout/style/index.js b/components/layout/style/index.js new file mode 100644 index 000000000..cf31ed80f --- /dev/null +++ b/components/layout/style/index.js @@ -0,0 +1,2 @@ +import '../../style/index.less' +import './index.less' diff --git a/components/layout/style/index.less b/components/layout/style/index.less new file mode 100644 index 000000000..4ad899d78 --- /dev/null +++ b/components/layout/style/index.less @@ -0,0 +1,112 @@ +@import "../../style/themes/default"; +@import "../../style/mixins/index"; + +@layout-prefix-cls: ~"@{ant-prefix}-layout"; + +.@{layout-prefix-cls} { + display: flex; + flex-direction: column; + flex: auto; + background: @layout-body-background; + + &, + * { + box-sizing: border-box; + } + + &&-has-sider { + flex-direction: row; + > .@{layout-prefix-cls}, + > .@{layout-prefix-cls}-content { + overflow-x: hidden; + } + } + + &-header, + &-footer { + flex: 0 0 auto; + } + + &-header { + background: @layout-header-background; + padding: @layout-header-padding; + height: @layout-header-height; + line-height: @layout-header-height; + } + + &-footer { + background: @layout-footer-background; + padding: @layout-footer-padding; + color: @text-color; + font-size: @font-size-base; + } + + &-content { + flex: auto; + } + + &-sider { + transition: all .2s; + position: relative; + background: @layout-sider-background; + + /* fix firefox can't set width smaller than content on flex item */ + min-width: 0; + + &-children { + height: 100%; + // Hack for fixing margin collaspe bug + // https://github.com/ant-design/ant-design/issues/7967 + // solution from https://stackoverflow.com/a/33132624/3040605 + padding-top: 0.1px; + margin-top: -0.1px; + } + + &-has-trigger { + padding-bottom: @layout-trigger-height; + } + + &-right { + order: 1; + } + + &-trigger { + position: fixed; + text-align: center; + bottom: 0; + cursor: pointer; + height: @layout-trigger-height; + line-height: @layout-trigger-height; + color: @layout-trigger-color; + background: @layout-trigger-background; + z-index: 1; + transition: all .2s; + } + + &-zero-width { + & > * { + overflow: hidden; + } + + &-trigger { + position: absolute; + top: @layout-header-height; + right: -@layout-zero-trigger-width; + text-align: center; + width: @layout-zero-trigger-width; + height: @layout-zero-trigger-height; + line-height: @layout-zero-trigger-height; + background: @layout-sider-background; + color: @layout-trigger-color; + font-size: @layout-zero-trigger-width / 2; + border-radius: 0 @border-radius-base @border-radius-base 0; + cursor: pointer; + transition: background .3s ease; + + &:hover { + background: tint(@layout-sider-background, 10%); + } + } + } + } +} diff --git a/components/menu/index.jsx b/components/menu/index.jsx index 6b2c134f2..ac7471036 100644 --- a/components/menu/index.jsx +++ b/components/menu/index.jsx @@ -38,20 +38,19 @@ export default { ItemGroup: { ...ItemGroup, name: 'AMenuItemGroup' }, provide () { return { - inlineCollapsed: this.getInlineCollapsed(), getInlineCollapsed: this.getInlineCollapsed, } }, mixins: [BaseMixin], inject: { - layoutContext: { default: {}}, + layoutSiderContext: { default: {}}, }, model: { prop: 'selectedKeys', event: 'selectChange', }, mounted () { - this.preProps = { ...this.props } + this.preProps = { ...this.$props } }, watch: { '$props': { @@ -80,7 +79,7 @@ export default { }, deep: true, }, - 'layoutContext.siderCollapsed': function (val) { + 'layoutSiderContext.sCollapsed': function (val) { const { openKeys, sOpenKeys, prefixCls } = this if (hasProp(this, 'openKeys')) { this.setState({ sOpenKeys: openKeys }) @@ -150,8 +149,8 @@ export default { }, getInlineCollapsed () { const { inlineCollapsed } = this.$props - if (this.layoutContext.siderCollapsed !== undefined) { - return this.layoutContext.siderCollapsed + if (this.layoutSiderContext.sCollapsed !== undefined) { + return this.layoutSiderContext.sCollapsed } return inlineCollapsed }, @@ -199,12 +198,8 @@ export default { }, }, render () { - const { layoutContext, $slots, $listeners } = this - const { collapsedWidth, siderCollapsed } = layoutContext - this.preLayoutContext = { - siderCollapsed, - collapsedWidth, - } + const { layoutSiderContext, $slots, $listeners } = this + const { collapsedWidth } = layoutSiderContext const { prefixCls, theme } = this.$props const menuMode = this.getRealMenuMode() const menuOpenAnimation = this.getMenuOpenAnimation(menuMode) diff --git a/components/style.js b/components/style.js index 02468dccd..e900685b8 100644 --- a/components/style.js +++ b/components/style.js @@ -42,3 +42,4 @@ import './input-number/style' import './transfer/style' import './tree/style' import './upload/style' +import './layout/style' diff --git a/site/components.js b/site/components.js index 8ecc6330e..4b20bfb63 100644 --- a/site/components.js +++ b/site/components.js @@ -23,7 +23,7 @@ import { Icon, Input, InputNumber, - // Layout, + Layout, // List, LocaleProvider, message, @@ -92,7 +92,11 @@ Vue.component(Input.Group.name, Input.Group) Vue.component(Input.Search.name, Input.Search) Vue.component(Input.TextArea.name, Input.TextArea) Vue.component(InputNumber.name, InputNumber) -// Vue.component(Layout.name, Layout) +Vue.component(Layout.name, Layout) +Vue.component(Layout.Header.name, Layout.Header) +Vue.component(Layout.Footer.name, Layout.Footer) +Vue.component(Layout.Sider.name, Layout.Sider) +Vue.component(Layout.Content.name, Layout.Content) // Vue.component(List.name, List) Vue.component(LocaleProvider.name, LocaleProvider) Vue.component(Menu.name, Menu) diff --git a/site/routes.js b/site/routes.js index 2b214efe2..e75bd2c35 100644 --- a/site/routes.js +++ b/site/routes.js @@ -3,7 +3,7 @@ import Layout from './components/layout.vue' const AsyncTestComp = () => { const d = window.location.hash.replace('#', '') return { - component: import(`../components/vc-upload/demo/${d}`), + component: import(`../components/layout/demo/${d}`), } }