diff --git a/devui/grid/__tests__/grid.spec.ts b/devui/grid/__tests__/grid.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..fa33b9765e7ec5a8972716e9ccc95f1c53e2e097 --- /dev/null +++ b/devui/grid/__tests__/grid.spec.ts @@ -0,0 +1,8 @@ +import { mount } from '@vue/test-utils'; +import { Row } from '../index'; + +describe('grid test', () => { + it('grid init render', async () => { + // todo + }) +}) diff --git a/devui/grid/index.ts b/devui/grid/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb9577156f351ec9585a16ba63a0d0d18063409c --- /dev/null +++ b/devui/grid/index.ts @@ -0,0 +1,22 @@ +import type { App } from 'vue' +import Row from './src/row' +import Col from './src/col' + +Row.install = function(app: App): void { + app.component(Row.name, Row) +} + +Col.install = function(app: App): void { + app.component(Col.name, Col) +} +export { Row, Col } + +export default { + title: 'Grid 栅格', + category: '布局', + status: '已完成', + install(app: App): void { + app.use(Col as any) + app.use(Row as any) + } +} diff --git a/devui/grid/src/col.scss b/devui/grid/src/col.scss new file mode 100644 index 0000000000000000000000000000000000000000..2381135f094e193d8635eaf954fb55a366739469 --- /dev/null +++ b/devui/grid/src/col.scss @@ -0,0 +1,64 @@ +.devui-col { + position: relative; + max-width: 100%; + min-height: 1px; +} + +@function percentage ($i, $sum: 24) { + @return $i / $sum * 100%; } + +.devui-col-span-0 { + display: none; +} + +@for $i from 1 to 24 { + .devui-col-offset-#{$i} { + margin-left: percentage($i); + } + .devui-col-pull-#{$i} { + right: percentage($i); + } + .devui-col-push-#{$i} { + left: percentage($i); + } + .devui-col-span-#{$i} { + display: block; + flex: 0 0 percentage($i); + width: percentage($i); + } + .devui-col-xs-offset-#{$i} { + margin-left: percentage($i); + } + .devui-col-xs-pull-#{$i} { + right: percentage($i); + } + .devui-col-xs-push-#{$i} { + left: percentage($i); + } + .devui-col-xs-span-#{$i} { + display: block; + flex: 0 0 percentage($i); + width: percentage($i); + } +} + +@each $size, $value in (sm, 576), (md, 768), (lg, 992), (xl, 1200), (xxl, 1600) { + @media screen and (min-width: #{$value}px) { + @for $i from 1 to 24 { + .devui-col-#{$size}-offset-#{$i} { + margin-left: percentage($i); + } + .devui-col-#{$size}-pull-#{$i} { + right: percentage($i); + } + .devui-col-#{$size}-push-#{$i} { + left: percentage($i); + } + .devui-col-#{$size}-span-#{$i} { + display: block; + flex: 0 0 percentage($i); + width: percentage($i); + } + } + } +} diff --git a/devui/grid/src/col.tsx b/devui/grid/src/col.tsx new file mode 100644 index 0000000000000000000000000000000000000000..596cb8a73c4dc9d6dc86d1dea8f42357a2619690 --- /dev/null +++ b/devui/grid/src/col.tsx @@ -0,0 +1,35 @@ +import { defineComponent, computed, CSSProperties, Ref, inject } from 'vue' +import { colProps, ColProps } from './grid-types' +import { useSize, CLASS_PREFIX, useColClassNames } from './use-grid' +import './col.scss' + + +export default defineComponent({ + name: 'DCol', + props: colProps, + setup (props: ColProps, { slots }) { + + const formatFlex = (flex: typeof props.flex) => { + if (typeof flex === 'number') { + return `${flex} ${flex} auto` + } + if (/^\d+(\.\d+)?(px|rem|em|%)$/.test(flex)) { + return `0 0 ${flex}` + } + return flex + } + + const colClassNames= useColClassNames(props) + + const sizeClassNames = useSize(props) + + const colStyle = computed(() => ({ + flex: formatFlex(props.flex), + order: props.order + })) + + const gutterStyle = inject>('gutterStyle') + + return () =>
{slots.default?.()}
+ } +}) diff --git a/devui/grid/src/grid-types.ts b/devui/grid/src/grid-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..a6491cf262525c3f8184abac3e93dd9c568abb29 --- /dev/null +++ b/devui/grid/src/grid-types.ts @@ -0,0 +1,70 @@ +import type { PropType, ExtractPropTypes } from 'vue' + +export type Align = 'top' | 'middle' | 'bottom' + +export type Justify = 'start' | 'end' | 'center' | 'around' | 'between' + +export interface GutterScreenSizes { + xs?: number | number[] + sm: number | number[] + md: number | number[] + lg: number | number[] + xl: number | number[] + xxl: number | number[] +} + +export const rowProps = { + align: { + type: String as PropType, + default: 'top' + }, + gutter: { + type: [Number, Object, Array] as PropType, + default: 0 + }, + justify: { + type: String as PropType, + default: 'start' + }, + wrap: { + type: Boolean as PropType, + default: false + } +} as const + +export type RowProps = ExtractPropTypes + +const screenSizesProp = [Number, Object] as PropType + +export const screenSizes = { + xs: screenSizesProp, + sm: screenSizesProp, + md: screenSizesProp, + lg: screenSizesProp, + xl: screenSizesProp, + xxl: screenSizesProp, +} as const + +const numberProp = Number as PropType + +export const colPropsBaseStyle = { + flex: [String, Number] as PropType, + order: numberProp, +} as const + +export const colPropsBaseClass = { + offset: numberProp, + pull: numberProp, + push: numberProp, + span: numberProp +} as const + +export type ColPropsBaseStyle = ExtractPropTypes + +export type ColPropsBaseClass = ExtractPropTypes + +export type ScreenSizes = ExtractPropTypes + +export const colProps = { ...colPropsBaseStyle, ...colPropsBaseClass, ...screenSizes} + +export type ColProps = ExtractPropTypes diff --git a/devui/grid/src/row.scss b/devui/grid/src/row.scss new file mode 100644 index 0000000000000000000000000000000000000000..b5b3df4ad0830f343e0323418894c159fe702c4f --- /dev/null +++ b/devui/grid/src/row.scss @@ -0,0 +1,19 @@ +.devui-row { + display: flex; +} + +.devui-row-wrap { + flex-wrap: wrap; +} + +@each $prefix, $value in (top, flex-start), (middle, center), (bottom, flex-end) { + .devui-row-align-#{$prefix} { + align-items: $value; + } +} + +@each $prefix, $value in (start, flex-start), (center, center), (end, flex-end), (around, space-around), (between, space-between) { + .devui-row-justify-#{$prefix} { + justify-content: $value; + } +} diff --git a/devui/grid/src/row.tsx b/devui/grid/src/row.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6e6bb5b35c4ec28dea954674b458e25f99c2bed6 --- /dev/null +++ b/devui/grid/src/row.tsx @@ -0,0 +1,69 @@ +import { defineComponent, computed, ref, Ref, CSSProperties, onMounted, onUnmounted, provide } from 'vue' +import { rowProps, RowProps } from './grid-types' +import { formatClass } from './use-grid' +import { responesScreen, Screen, RESULT_SCREEN, removeSubscribeCb } from './use-screen' +import './row.scss' + +const CLASS_PREFIX = 'devui-row' + +export default defineComponent({ + name: 'DRow', + props: rowProps, + emits: [], + setup(props: RowProps, { slots }) { + const gutterScreenSize = ref({}) + + const rowClass = computed(() => { + const alignClass = formatClass(`${CLASS_PREFIX}-align`, props.align) + const justifyClass = formatClass(`${CLASS_PREFIX}-justify`, props.justify) + const wrapClass = props.wrap ? ` ${CLASS_PREFIX}-wrap` : '' + return `${alignClass}${justifyClass}${wrapClass}` + }) + + let token + + onMounted(() => { + token = responesScreen(screen => { + gutterScreenSize.value = screen + }) + }) + + onUnmounted(() => { + removeSubscribeCb(token) + }) + + const gutterStyle = computed(() => { + if (!props.gutter) { + return {} + } + let currentGutter = [0, 0] + if (Array.isArray(props.gutter)) { + currentGutter = props.gutter as number[] + } else if (typeof props.gutter === 'number') { + currentGutter = [props.gutter as number, 0] + } else { + RESULT_SCREEN.some(size => { + const gzs = props.gutter[size] + if (gutterScreenSize.value[size] && gzs) { + if (typeof gzs === 'number') { + currentGutter = [gzs, 0] + } else { + currentGutter = gzs + } + return true + } + return false + }) + } + const paddingLeft = `${(currentGutter[0] || 0) / 2}px` + const paddingRight = `${(currentGutter[0] || 0) / 2}px` + const paddingTop = `${(currentGutter[1] || 0) / 2}px` + const paddingBottom = `${(currentGutter[1] || 0) / 2}px` + return { paddingLeft, paddingRight, paddingTop, paddingBottom } + }) + + provide>('gutterStyle', gutterStyle) + + return () =>
{slots.default?.()}
+ } +}) diff --git a/devui/grid/src/use-grid.ts b/devui/grid/src/use-grid.ts new file mode 100644 index 0000000000000000000000000000000000000000..a303a3624638d7767d3190ce7c4b89468c682f58 --- /dev/null +++ b/devui/grid/src/use-grid.ts @@ -0,0 +1,47 @@ +import { computed } from 'vue' +import { ScreenSizes, ColPropsBaseClass, screenSizes, colPropsBaseClass } from './grid-types' + +export const CLASS_PREFIX = 'devui-col' + +export function formatClass (prefix: string, val: number | string | undefined) { + return val !== undefined ? ` ${prefix}-${val}` : '' +} + +export function useColClassNames (props: ColPropsBaseClass) { + return computed(() => { + const spanClass = formatClass(`${CLASS_PREFIX}-span`, props.span) + const offsetClass = formatClass(`${CLASS_PREFIX}-offset`, props.offset) + const pullClass = formatClass(`${CLASS_PREFIX}-pull`, props.pull) + const pushClass = formatClass(`${CLASS_PREFIX}-push`, props.push) + return `${spanClass}${offsetClass}${pullClass}${pushClass}` + }) +} + +function setSpace (val:string) { + return val && ` ${val.trim()} ` +} + +export function useSize (colSizes: ScreenSizes) { + const keys = Object.keys(colSizes).filter(key => key in screenSizes) as (keyof ScreenSizes)[] + return computed(() => { + return keys.reduce((total, key) => { + const valueType = typeof colSizes[key] + + if (valueType === 'number') { + total = `${setSpace(total)}${CLASS_PREFIX}-${key}-span-${colSizes[key]}` + } else if (valueType === 'object') { + const colSizesKeys = Object.keys(colSizes[key]) as (keyof ColPropsBaseClass)[] + const sum = colSizesKeys.filter(item => item in colPropsBaseClass).reduce((tot, k) => { + if (typeof colSizes[key][k] !== 'number') { + return '' + } else { + tot = `${setSpace(tot)}${CLASS_PREFIX}-${key}-${k}-${colSizes[key][k]}` + } + return tot + }, '') + total = `${setSpace(total)}${sum}` + } + return total + }, '') + }) +} diff --git a/devui/grid/src/use-screen.ts b/devui/grid/src/use-screen.ts new file mode 100644 index 0000000000000000000000000000000000000000..98d86f99acbe02fea94eb585b07ba9ec490b8a70 --- /dev/null +++ b/devui/grid/src/use-screen.ts @@ -0,0 +1,84 @@ + +export const RESULT_SCREEN = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs'] + +const screenMedias = { + xs: 'screen and (max-width: 575px)', + sm: 'screen and (min-width: 576px)', + md: 'screen and (min-width: 768px)', + lg: 'screen and (min-width: 992px)', + xl: 'screen and (min-width: 1200px)', + xxl: 'screen and (min-width: 1600px)', +} as const + +export interface Screen { + xs?: boolean + sm?: boolean + md?: boolean + lg?: boolean + xl?: boolean + xxl?: boolean +} + +export type ScreenMediasKey = keyof typeof screenMedias +type SubscribeCb = (screen: Screen) => void + +const subscribers = new Map() +let subUid = -1 +const screen: Screen = {} +const results: { + [key: string]: { + res: MediaQueryList + listener: (this: MediaQueryList, ev: MediaQueryListEvent) => void + } + } = {}; + +export function responesScreen (func: SubscribeCb) { + if (!subscribers.size) { + register() + } + subUid += 1 + subscribers.set(subUid, func) + func({ ...screen }) + return subUid +} + +export function removeSubscribeCb (id: number) { + subscribers.delete(id) + if (subscribers.size === 0) { + unRegister() + } +} + +function register () { + Object.keys(screenMedias).forEach(key => { + const result = window.matchMedia(screenMedias[key]) + if (result.matches) { + screen[key as ScreenMediasKey] = true + dispatch() + } + const listener = e => { + screen[key as ScreenMediasKey] = e.matches + dispatch() + } + result.addEventListener('change', listener) + + results[key] = { + res: result, + listener + } + }) +} + +function unRegister () { + Object.keys(screenMedias).forEach(key => { + const handler = results[key] + handler.res.removeEventListener('change', handler.listener) + }) + subscribers.clear() +} + +function dispatch () { + subscribers.forEach(value => { + value({ ...screen }) + }) +} diff --git a/docs/components/grid/index.md b/docs/components/grid/index.md new file mode 100644 index 0000000000000000000000000000000000000000..2a67551b3e7570f72b8bff6ff2f0fa15a06f8c87 --- /dev/null +++ b/docs/components/grid/index.md @@ -0,0 +1,316 @@ +# Grid 栅格 + +24栅格系统。 +### 何时使用 + +需要使用弹性布局时,并且需要适配不同的屏幕时,使用grid组件。 + + +### 基本用法 + +基础栅格 + +:::demo 使用 Row 和 Col组件,可以创建一个基本的栅格系统,Col必须放在Row里面。 + +```vue + + + +``` + +::: + +### 对齐 + +垂直对齐和水平对齐 + +:::demo 使用Row的align属性和justify属性子元素垂直对齐和水平对齐。 + +```vue + + +``` + +::: + + +### 子元素的间隔 + +栅格之间的间隔可以用Row的gutter属性 + +:::demo :gutter="10" 子元素左右间隔为 5px;:gutter="[10, 20]" 子元素左右间隔为5px,上下间隔为10px;需要适配屏幕宽度的情况,:gutter="{ xs: 10, sm: 20, md: [20, 10], lg: [30, 20], xl: [40, 30], xxl: [50, 40] }" + +```vue + + + +``` + +::: + +### flex填充 + +Col的flex属性支持flex填充。 + +:::demo + +```vue + +``` + +::: + +### 左右偏移 + +使用Col的offset、pull和push来使子元素左右偏移。 + +:::demo 列偏移。使用 offset 可以将列向右侧偏。例如,offset={4} 将元素向右侧偏移了 4 个列(column)的宽度;offset、pull、push也可以内嵌到。 + +```vue + +``` + +::: + +### 响应式布局 + +预设六个响应尺寸:xs sm md lg xl xxl。 + +:::demo 参照 Bootstrap 的 [响应式设计](https://getbootstrap.com/docs/3.4/css/)。 + +```vue + +``` + +::: + +### 栅格排序 + +列排序。通过使用order、 push 和 pull 类就可以改变列(column)的顺。 + +:::demo 参照 Bootstrap 的 [响应式设计](https://getbootstrap.com/docs/3.4/css/)。 + +```vue + +``` + +::: + +### d-grid + +d-row 参数 + +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | 全局配置项 | +| ---- | ---- | ---- | ---- | --------- | --------- | +| align | `string` | `'top'` | flex 布局下的垂直对齐方式:`'top'`,`'middle'`,`'bottom'` | [垂直对齐](#垂直对齐) | | +| justify | `string` | `'start'` | flex 布局下的垂直对齐方式:`'start'`,`'end'`,`'center'`,`'space-around'`,`'space-between'` | [垂直对齐](#垂直对齐) | | +| gutter | `number\|array\|object` | 0 | 栅格间隔,数值形式:水平间距。对象形式支持响应式: { xs: 8, sm: 16, md: 24}。数组形式:[水平间距, 垂直间距]。 | [对齐](#对齐) | | +| wrap | `boolean` | false | 是否自动换行 | | | + +d-col 参数 + +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | 全局配置项 | +| ---- | ---- | ---- | ---- | --------- | --------- | +| span | `number` | - | 栅格占位格数,为 0 时相当于 display: none | [基本用法](#基本用法) | | +| flex | `string\|number` | - | flex 布局填充 | [flex填充](#flex填充) | | +| offset | `number` | - | 栅格左侧的间隔格数,间隔内不可以有栅格 | [左右偏移](#左右偏移) | +| pull | `number` | - | 栅格向左移动格数 | [左右偏移](#左右偏移)、[栅格排序](#栅格排序) | +| push | `number` | - | 栅格向右移动格数 | [左右偏移](#左右偏移)、[栅格排序](#栅格排序) | +| order | `number` | - | 栅格顺序,flex 布局模式下有效 | [栅格排序](#栅格排序) | +| xs | `number\|object` | - | <576px 响应式栅格,可为栅格数或一个包含其他属性的对象 | [栅格排序](#栅格排序) | +| sm | `number\|object` | - | >=576px 响应式栅格,可为栅格数或一个包含其他属性的对象 | [响应式布局](#响应式布局) | +| md | `number\|object` | - | >=768px 响应式栅格,可为栅格数或一个包含其他属性的对象 | [响应式布局](#响应式布局) | +| lg | `number\|object` | - | >=992px 响应式栅格,可为栅格数或一个包含其他属性的对象 | [响应式布局](#响应式布局) | +| xl | `number\|object` | - | >=1200px 响应式栅格,可为栅格数或一个包含其他属性的对象 | [响应式布局](#响应式布局) | +| xxl | `number\|object` | - | >=1600px 响应式栅格,可为栅格数或一个包含其他属性的对象 | [响应式布局](#响应式布局) | \ No newline at end of file