Commit 31db5a3f authored by Sendya's avatar Sendya

fix: layout props inject

parent a7b4a123
......@@ -20,9 +20,8 @@
"lint-fix": "eslint --fix src/ -c .eslintrc.js --ext .tsx,.ts",
"compile": "vc-tools run compile",
"test": "cross-env NODE_ENV=test jest --config .jest.js",
"prepublishOnly": "npm run lint && npm run generate && npm run compile && npm run test",
"generate": "rimraf src/icons && cross-env TS_NODE_PROJECT=scripts/tsconfig.json node -r ts-node/register scripts/generate.ts --target=icon",
"postcompile": "npm run clean && cross-env TS_NODE_PROJECT=scripts/tsconfig.json node -r ts-node/register scripts/generate.ts --target=entry"
"prepublishOnly": "npm run lint && npm run compile && npm run test",
"postcompile": "npm run clean"
},
"peerDependencies": {
"ant-design-vue": ">=2.0.0",
......
const { camelCase, upperFirst } = require('lodash');
const manifest = require('@ant-design/icons-svg/lib/manifest').default;
const themeMap = {
fill: 'filled',
outline: 'outlined', // default theme
twotone: 'twoTone',
};
exports.getComponentNameList = () => {
const icons = [];
Object.keys(manifest).forEach(theme => {
manifest[theme].forEach(name => {
const baseName = upperFirst(camelCase(name));
icons.push({
theme,
componentName: baseName + upperFirst(themeMap[theme]),
svgName: baseName + upperFirst(theme === 'twotone' ? 'twoTone' : theme),
});
});
});
return icons;
};
import './BasicLayoutTest.less';
import './BasicLayout.less';
import { App, FunctionalComponent, Plugin, CSSProperties } from 'vue';
import { Layout } from 'ant-design-vue';
import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue';
import { computed, App, FunctionalComponent, Plugin, CSSProperties } from 'vue';
import 'ant-design-vue/es/layout/style';
import Layout from 'ant-design-vue/es/layout';
import { default as ProProvider, ProProviderData } from './ProProvider';
import { default as GlobalFooter } from './GlobalFooter';
import { default as SiderMenuWrapper, SiderMenuWrapperProps } from './SiderMenu';
import { WrapContent } from './WrapContent';
import { RenderVNodeType, WithFalse } from './typings';
import './BasicLayout.less';
const defaultI18nRender = (key: string) => key;
......@@ -31,11 +28,15 @@ export interface BasicLayoutProps {
>;
headerRender?: WithFalse<(props: any /* HeaderProps */) => RenderVNodeType>;
colSize?: string,
/**
* 是否禁用移动端模式,有的管理系统不需要移动端模式,此属性设置为true即可
*/
disableMobile?: boolean;
isChildrenLayout?: boolean;
contentStyle?: CSSProperties;
/**
* 兼用 content的 margin
......@@ -46,80 +47,108 @@ export interface BasicLayoutProps {
export type ProLayoutProps = BasicLayoutProps &
SiderMenuWrapperProps /* & HeaderProps & FooterProps */;
const ProLayout: FunctionalComponent<ProLayoutProps> = (props, { emit, slots }) => {
const handleClick = () => {
emit('update:collapsed', !props.collapsed);
const ProLayout: FunctionalComponent<ProLayoutProps> = (props, { emit, slots, attrs }) => {
const handleCollapse = (collapsed: boolean) => {
emit('update:collapsed', collapsed);
};
const handleOpenChange = (openKeys): void => {
emit('update:openKeys', openKeys);
const handleOpenChange = (openKeys: string[] | false): void => {
openKeys && emit('update:openKeys', openKeys);
};
const handleSelect = (selectedKeys: string[]): void => {
emit('update:selectedKeys', selectedKeys);
const handleSelect = (selectedKeys: string[] | false): void => {
selectedKeys && emit('update:selectedKeys', selectedKeys);
};
const baseClassName = computed(() => `${props.prefixCls}-basicLayout`);
// gen className
const className = computed(() => {
return {
[baseClassName.value]: true,
[`screen-${props.colSize}`]: props.colSize,
[`${baseClassName.value}-top-menu`]: props.layout === 'top',
[`${baseClassName.value}-is-children`]: props.isChildrenLayout,
[`${baseClassName.value}-fix-siderbar`]: props.fixSiderbar,
[`${baseClassName.value}-${props.layout}`]: props.layout,
};
});
return (
<ProProvider {...props} i18n={defaultI18nRender}>
<Layout class="ant-pro-basicLayout">
<SiderMenuWrapper
{...props}
onCollapse={(collapsed: boolean) => emit('update:collapsed', collapsed)}
/>
<Layout>
<Layout.Header style="background: #fff; padding: 0; height: 48px; line-height: 48px;">
{props.collapsed ? (
<MenuUnfoldOutlined class="trigger" onClick={handleClick} />
) : (
<MenuFoldOutlined class="trigger" onClick={handleClick} />
)}
</Layout.Header>
<WrapContent
style={{
margin: '24px 16px',
padding: '24px',
background: '#fff',
minHeight: '280px',
}}
>
{slots.default?.()}
</WrapContent>
<GlobalFooter
links={[
{
key: '1',
title: 'Pro Layout',
href: 'https://www.github.com/vueComponent/pro-layout',
blankTarget: true,
},
{
key: '2',
title: 'Github',
href: 'https://www.github.com/vueComponent/ant-design-vue-pro',
blankTarget: true,
},
{
key: '3',
title: '@Sendya',
href: 'https://www.github.com/sendya/',
blankTarget: true,
},
]}
copyright={
<a href="https://github.com/vueComponent" target="_blank">
vueComponent
</a>
}
<ProProvider i18n={defaultI18nRender}>
<div class={className.value}>
<Layout class={baseClassName.value}>
<SiderMenuWrapper
{...props}
onSelect={handleSelect}
onOpenChange={handleOpenChange}
onCollapse={handleCollapse}
/>
<Layout>
<Layout.Header style="background: #fff; padding: 0; height: 48px; line-height: 48px;">
</Layout.Header>
<WrapContent
style={{
margin: '24px 16px',
padding: '24px',
background: '#fff',
minHeight: '280px',
}}
>
{slots.default?.()}
</WrapContent>
<GlobalFooter
links={[
{
key: '1',
title: 'Pro Layout',
href: 'https://www.github.com/vueComponent/pro-layout',
blankTarget: true,
},
{
key: '2',
title: 'Github',
href: 'https://www.github.com/vueComponent/ant-design-vue-pro',
blankTarget: true,
},
{
key: '3',
title: '@Sendya',
href: 'https://www.github.com/sendya/',
blankTarget: true,
},
]}
copyright={
<a href="https://github.com/vueComponent" target="_blank">
vueComponent
</a>
}
/>
</Layout>
</Layout>
</Layout>
</div>
</ProProvider>
);
};
ProLayout.inheritAttrs = false;
ProLayout.displayName = 'ProLayout';
ProLayout.emits = ['update:collapsed', 'update:openKeys', 'update:selectedKeys'];
ProLayout.props = {
prefixCls: {
type: String,
default: 'ant-pro',
},
'title': String,
'colSize': String,
'isChildrenLayout': Boolean,
'fixSiderbar': Boolean,
'layout': String,
'openKeys': Array,
'selectedKeys': Array,
'collapsed': Boolean,
'menuData': Array,
} as any;
// @ts-ignore
ProLayout.install = function (app: App) {
app.component('pro-layout', ProLayout);
}
};
export default ProLayout as typeof ProLayout & Plugin;
.ant-pro-basicLayout {
.logo {
height: 32px;
background: rgba(255, 255, 255, 0.2);
margin: 16px;
}
.trigger {
font-size: 18px;
line-height: 48px;
padding: 0 12px;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: #1890ff;
}
}
}
import './GridContent.less';
import { FunctionalComponent, SetupContext, CSSProperties } from 'vue';
import { FunctionalComponent, CSSProperties, toRefs } from 'vue';
import { useProProvider } from '../ProProvider';
import { PureSettings } from '../defaultSettings';
import './GridContent.less';
interface GridContentProps {
contentWidth?: PureSettings['contentWidth'];
......@@ -10,17 +10,21 @@ interface GridContentProps {
}
const GridContent: FunctionalComponent<GridContentProps> = (
{ prefixCls = 'ant-pro', contentWidth },
{ slots }: SetupContext,
props,
{ slots },
) => {
const proConfig = useProProvider();
const { contentWidth, getPrefixCls } = toRefs(proConfig);
const customPrefixCls = props.prefixCls || getPrefixCls.value();
const customContentWidth = props.contentWidth || contentWidth.value;
return (
<div
class={{
[`${prefixCls}-grid-content`]: true,
wide: contentWidth === 'Fixed',
[`${customPrefixCls}-grid-content`]: true,
wide: customContentWidth === 'Fixed',
}}
>
<div class={`${prefixCls}-grid-content-children`}>{slots.default?.()}</div>
<div class={`${customPrefixCls}-grid-content-children`}>{slots.default?.()}</div>
</div>
);
};
......
......@@ -7,7 +7,14 @@ import { AffixProps } from './interfaces/Affix';
/* replace antd ts define end */
import { useRouteContext, RouteContextProps } from '../RouteContext';
import { useProProvider } from '../ProProvider';
import { Affix, PageHeader, Tabs, Spin } from 'ant-design-vue';
import 'ant-design-vue/es/affix/style';
import Affix from 'ant-design-vue/es/affix';
import 'ant-design-vue/es/page-header/style';
import PageHeader from 'ant-design-vue/es/page-header';
import 'ant-design-vue/es/tabs/style';
import Tabs from 'ant-design-vue/es/tabs';
import 'ant-design-vue/es/spin/style';
import Spin from 'ant-design-vue/es/spin';
import GridContent from '../GridContent';
import FooterToolbar from '../FooterToolbar';
import './index.less';
......
......@@ -15,9 +15,9 @@ import { ContentWidth } from '../typings';
export const defaultPrefixCls = 'ant-pro';
export interface ProProviderData {
getPrefixCls?: (suffixCls?: string, customizePrefixCls?: string) => string;
i18n?: (t: string) => string;
contentWidth?: ContentWidth;
getPrefixCls: (suffixCls?: string, customizePrefixCls?: string) => string;
i18n: (t: string) => string;
contentWidth: ContentWidth;
}
export const defaultProProviderProps: ProProviderData = {
......
import './index.less';
import {
defineComponent,
resolveComponent,
......@@ -17,16 +15,13 @@ import {
toRefs,
} from 'vue';
import { createFromIconfontCN } from '@ant-design/icons-vue';
// import 'ant-design-vue/es/menu/style'
// import Menu from 'ant-design-vue/es/menu'
import { Menu } from 'ant-design-vue';
import 'ant-design-vue/es/menu/style';
import Menu from 'ant-design-vue/es/menu';
import defaultSettings, { PureSettings } from '../defaultSettings';
import { isImg, isUrl } from '../utils';
import { MenuMode, SelectInfo, OpenEventHandler } from './typings';
import { RouteProps, MenuTheme, WithFalse } from '../typings';
import { RouteProps, MenuTheme, FormatMessage, WithFalse } from '../typings';
import './index.less';
export { MenuMode, SelectInfo, OpenEventHandler };
......@@ -88,7 +83,7 @@ export interface BaseMenuProps extends Partial<PureSettings> {
selectedKeys?: WithFalse<string[]> | undefined;
handleOpenChange?: (openKeys: string[]) => void;
theme?: MenuTheme | 'realDark';
i18n?: (t: string) => string | VNodeChild;
i18n?: FormatMessage;
}
// vue props
......@@ -118,16 +113,16 @@ export const VueBaseMenuProps = {
},
};
const renderTitle = (title, i18nRender) => {
return <span>{(i18nRender && i18nRender(title)) || title}</span>;
const renderTitle = (title: string | undefined, i18nRender: FormatMessage) => {
return <span>{(i18nRender && title && i18nRender(title)) || title}</span>;
};
const renderMenuItem = (item, i18nRender) => {
const renderMenuItem = (item: RouteProps, i18nRender: FormatMessage) => {
const meta = Object.assign({}, item.meta);
const target = meta.target || null;
const CustomTag = (target && 'a') || 'router-link';
const CustomTag: any = (target && 'a') || 'router-link';
const props = { to: { name: item.name }, href: item.path, target: target };
if (item.children && item.hideChildrenInMenu) {
if (item.children && item.meta?.hideChildInMenu) {
// 把有子菜单的 并且 父菜单是要隐藏子菜单的
// 都给子菜单增加一个 hidden 属性
// 用来给刷新页面时, selectedKeys 做控制用
......@@ -135,34 +130,35 @@ const renderMenuItem = (item, i18nRender) => {
cd.meta = Object.assign(cd.meta || {}, { hidden: true });
});
}
// @ts-nocheck
return (
<Menu.Item key={item.path}>
<CustomTag {...props}>
<LazyIcon icon={meta.icon} />
{renderTitle(meta.title, i18nRender)}
{renderTitle(meta.title!, i18nRender)}
</CustomTag>
</Menu.Item>
);
};
const renderSubMenu = (item, i18nRender) => {
const renderSubMenu = (item: RouteProps, i18nRender: FormatMessage) => {
const renderMenuContent = (
<span>
<LazyIcon icon={item.meta.icon} />
<span>{renderTitle(item.meta.title, i18nRender)}</span>
<LazyIcon icon={item.meta?.icon} />
<span>{renderTitle(item.meta?.title, i18nRender)}</span>
</span>
) as string & VNode;
return (
<Menu.SubMenu key={item.path} title={renderMenuContent}>
{/* eslint-disable-next-line @typescript-eslint/no-use-before-define */}
{!item.hideChildrenInMenu && item.children.map(cd => renderMenu(cd, i18nRender))}
{!item.meta?.hideChildInMenu && item.children!.map(cd => renderMenu(cd, i18nRender))}
</Menu.SubMenu>
);
};
const renderMenu = (item, i18nRender) => {
const renderMenu = (item: RouteProps, i18nRender: FormatMessage) => {
if (item && !item.hidden) {
const hasChild = item.children && !item.hideChildrenInMenu;
const hasChild = item.children && !item.meta?.hideChildInMenu;
return hasChild ? renderSubMenu(item, i18nRender) : renderMenuItem(item, i18nRender);
}
return null;
......@@ -172,8 +168,11 @@ const IconFont = createFromIconfontCN({
scriptUrl: defaultSettings.iconfontUrl,
});
const LazyIcon = props => {
const LazyIcon = (props: any) => {
const { icon, prefixCls } = props;
if (!icon) {
return null;
}
if (typeof icon === 'string' && icon !== '') {
if (isUrl(icon) || isImg(icon)) {
return <img src={icon} alt="icon" class={`${prefixCls}-sider-menu-icon`} />;
......@@ -199,7 +198,7 @@ export default defineComponent({
{},
{
i18n: {
type: Function,
type: Function as PropType<FormatMessage>,
default: (t: string): string => t,
},
},
......@@ -207,7 +206,7 @@ export default defineComponent({
),
emits: ['update:openKeys', 'update:selectedKeys'],
setup(props, { emit }) {
const { mode } = toRefs(props);
const { mode, i18n } = toRefs(props);
const isInline = computed(() => mode.value === 'inline');
const handleOpenChange: OpenEventHandler = (openKeys): void => {
emit('update:openKeys', openKeys);
......@@ -237,7 +236,7 @@ export default defineComponent({
if (menu.hidden) {
return null;
}
return renderMenu(menu, props.i18n);
return renderMenu(menu, i18n.value);
})}
</Menu>
);
......
import './index.less';
import { FunctionalComponent, computed, ref } from 'vue';
// import 'ant-design-vue/es/layout/style';
// import Layout from 'ant-design-vue/es/layout';
import { Layout, Menu } from 'ant-design-vue';
import 'ant-design-vue/es/layout/style';
import Layout from 'ant-design-vue/es/layout';
import 'ant-design-vue/es/menu/style';
import Menu from 'ant-design-vue/es/menu';
import BaseMenu, { BaseMenuProps } from './BaseMenu';
import { WithFalse, RenderVNodeType } from '../typings';
import { SiderProps } from './typings';
import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue';
import { useProProvider } from '../ProProvider';
import './index.less';
const { Sider } = Layout;
......@@ -50,14 +50,14 @@ export const defaultRenderLogo = (logo: RenderVNodeType): RenderVNodeType => {
export const defaultRenderLogoAndTitle = (
props: SiderMenuProps,
renderKey: string = 'menuHeaderRender',
renderKey = 'menuHeaderRender',
): RenderVNodeType => {
const {
logo = 'https://gw.alipayobjects.com/zos/antfincdn/PmY%24TNNDBI/logo.svg',
title,
layout,
} = props;
const renderFunction = props[renderKey || ''];
const renderFunction = (props as any)[renderKey || ''];
if (renderFunction === false) {
return null;
}
......@@ -129,11 +129,11 @@ const SiderMenu: FunctionalComponent<SiderMenuProps> = (props: SiderMenuProps) =
}}
class={`${baseClassName}-menu`}
{...{
'onUpdate:openKeys': $event => {
onOpenChange($event);
'onUpdate:openKeys': ($event: any) => {
onOpenChange && onOpenChange($event);
},
'onUpdate:selectedKeys': $event => {
onSelect($event);
'onUpdate:selectedKeys': ($event: any) => {
onSelect && onSelect($event);
},
}}
/>
......
import { FunctionalComponent, reactive, toRefs, CSSProperties } from 'vue';
import { Layout } from 'ant-design-vue';
import 'ant-design-vue/es/layout/style';
import Layout from 'ant-design-vue/es/layout';
import { useProProvider } from './ProProvider';
import { ContentWidth } from './typings';
......
import { App, Plugin } from 'vue';
export * from './RouteContext';
export { createContext, useContext, ContextType, CreateContext } from './hooks/context';
export { default as FooterToolbar } from './FooterToolbar';
export { default as GlobalFooter } from './GlobalFooter';
......
// https://github.com/substack/insert-css
const containers = []; // will store container HTMLElement references
const styleElements = []; // will store {prepend: HTMLElement, append: HTMLElement}
const usage =
'insert-css: You need to provide a CSS string. Usage: insertCss(cssString[, options]).';
function createStyleElement() {
const styleElement = document.createElement('style');
styleElement.setAttribute('type', 'text/css');
return styleElement;
}
function insertCss(css: any, options: any) {
options = options || {};
if (css === undefined) {
throw new Error(usage);
}
const position = options.prepend === true ? 'prepend' : 'append';
const container =
options.container !== undefined ? options.container : document.querySelector('head');
let containerId = containers.indexOf(container);
// first time we see this container, create the necessary entries
if (containerId === -1) {
containerId = containers.push(container) - 1;
styleElements[containerId] = {};
}
// try to get the correponding container + position styleElement, create it otherwise
let styleElement;
if (
styleElements[containerId] !== undefined &&
styleElements[containerId][position] !== undefined
) {
styleElement = styleElements[containerId][position];
} else {
styleElement = styleElements[containerId][position] = createStyleElement();
if (position === 'prepend') {
container.insertBefore(styleElement, container.childNodes[0]);
} else {
container.appendChild(styleElement);
}
}
// strip potential UTF-8 BOM if css was read from a file
if (css.charCodeAt(0) === 0xfeff) {
css = css.substr(1, css.length);
}
// actually add the stylesheet
if (styleElement.styleSheet) {
styleElement.styleSheet.cssText += css;
} else {
styleElement.textContent += css;
}
return styleElement;
}
export default insertCss;
......@@ -15,6 +15,7 @@ export interface MetaRecord {
icon?: string | VNodeChild | JSX.Element;
title?: string;
authority?: string | string[];
target?: '_blank' | '_self' | string;
[key: string]: any;
}
......@@ -22,10 +23,12 @@ export interface RouteProps {
key?: string | symbol;
path: string;
name?: string | symbol;
meta?: MetaRecord | {};
meta?: MetaRecord;
target?: TargetType;
hidden?: boolean;
children?: RouteProps[];
}
export type WithFalse<T> = T | false;
export type FormatMessage = (message: string) => string;
import { nextTick, h } from 'vue';
import { AbstractNode, IconDefinition } from '@ant-design/icons-svg/lib/types';
import { generate as generateColor } from '@ant-design/colors';
import { default as insertCss } from '../insert-css';
export { getComponent } from 'ant-design-vue/es/_util/props-util';
export { default as isUrl } from './isUrl';
......@@ -19,149 +15,12 @@ export function warning(valid: boolean, message: string) {
warn(valid, `[@ant-design-vue/pro-layout] ${message}`);
}
export function isIconDefinition(target: any): target is IconDefinition {
return (
typeof target === 'object' &&
typeof target.name === 'string' &&
typeof target.theme === 'string' &&
(typeof target.icon === 'object' || typeof target.icon === 'function')
);
}
export function normalizeAttrs(attrs: Attrs = {}): Attrs {
return Object.keys(attrs).reduce((acc: Attrs, key) => {
const val = attrs[key];
switch (key) {
case 'class':
acc.className = val;
delete acc.class;
break;
default:
acc[key] = val;
}
return acc;
}, {});
}
export interface Attrs {
[key: string]: string;
}
export type StringKeyOf<T> = Extract<keyof T, string>;
export type EventHandlers<E> = {
[K in StringKeyOf<E>]?: E[K] extends Function ? E[K] : (payload: E[K]) => void;
};
export function generate(
node: AbstractNode,
key: string,
rootProps?: { [key: string]: any } | false,
): any {
if (!rootProps) {
return h(
node.tag,
{ key, ...node.attrs },
(node.children || []).map((child, index) => generate(child, `${key}-${node.tag}-${index}`)),
);
}
return h(
node.tag,
{
key,
...rootProps,
...node.attrs,
},
(node.children || []).map((child, index) => generate(child, `${key}-${node.tag}-${index}`)),
);
}
export function getSecondaryColor(primaryColor: string): string {
// choose the second color
return generateColor(primaryColor)[0];
}
export function normalizeTwoToneColors(
twoToneColor: string | [string, string] | undefined,
): string[] {
if (!twoToneColor) {
return [];
}
return Array.isArray(twoToneColor) ? twoToneColor : [twoToneColor];
}
// These props make sure that the SVG behaviours like general text.
// Reference: https://blog.prototypr.io/align-svg-icons-to-text-and-say-goodbye-to-font-icons-d44b3d7b26b4
export const svgBaseProps = {
width: '1em',
height: '1em',
fill: 'currentColor',
'aria-hidden': 'true',
focusable: 'false',
} as any;
export const iconStyles = `
.anticon {
display: inline-block;
color: inherit;
font-style: normal;
line-height: 0;
text-align: center;
text-transform: none;
vertical-align: -0.125em;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.anticon > * {
line-height: 1;
}
.anticon svg {
display: inline-block;
}
.anticon::before {
display: none;
}
.anticon .anticon-icon {
display: block;
}
.anticon[tabindex] {
cursor: pointer;
}
.anticon-spin::before,
.anticon-spin {
display: inline-block;
-webkit-animation: loadingCircle 1s infinite linear;
animation: loadingCircle 1s infinite linear;
}
@-webkit-keyframes loadingCircle {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loadingCircle {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
`;
let cssInjectedFlag = false;
export const useInsertStyles = (styleStr: string = iconStyles) => {
nextTick(() => {
if (!cssInjectedFlag) {
insertCss(styleStr, {
prepend: true,
});
cssInjectedFlag = true;
}
});
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment