diff --git a/.eslintrc.js b/.eslintrc.js index 3da2f8038e830ccdbd9d559118c4e74b0baf58b9..95881caded69522f82da79944aa6219b673ac879 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,4 @@ + module.exports = { parser: 'vue-eslint-parser', parserOptions: { diff --git a/devui/loading/src/loading.scss b/devui/loading/src/loading.scss index 2e9fbef5101c3d6df424b8f95e2dbcac79357de1..025dc9e6569013bb2d6fbf6364f80ba99d2f7614 100644 --- a/devui/loading/src/loading.scss +++ b/devui/loading/src/loading.scss @@ -48,7 +48,6 @@ } .devui-loading-text { - vertical-align: super; margin-left: 10px; } diff --git a/devui/overlay/index.ts b/devui/overlay/index.ts index 0e363ac8cdff8c34abd987d93223208b80c695bf..e5a97c08ab92f1925f228ee0fd33079862f07f5e 100644 --- a/devui/overlay/index.ts +++ b/devui/overlay/index.ts @@ -13,7 +13,7 @@ FixedOverlay.install = function(app: App) { export { FlexibleOverlay, FixedOverlay } export default { - title: 'Overlay 浮层', + title: 'Overlay 遮罩层', category: '通用', install(app: App): void { app.use(FixedOverlay as any); diff --git a/devui/overlay/src/flexible-overlay.tsx b/devui/overlay/src/flexible-overlay.tsx index 1bc253676a9b8dbf2d8040520296dec1d04f73e5..cc1a5cdc971a15f94640cdf144bb357e33a33a10 100644 --- a/devui/overlay/src/flexible-overlay.tsx +++ b/devui/overlay/src/flexible-overlay.tsx @@ -274,7 +274,7 @@ function getOriginRect(origin: Origin): ClientRect { } /** - * 获取浮层的左上角坐标 + * 获取遮罩层的左上角坐标 * @param {Point} originPoint * @param {DOMRect} rect * @param {ConnectionPosition} position diff --git a/devui/pagination/src/components/jump-page.tsx b/devui/pagination/src/components/jump-page.tsx index 096a56c2cea758573acafd05431c0051641a4bf7..a4e7e2f8bf85c806f0085a3551d168a61a81a99e 100644 --- a/devui/pagination/src/components/jump-page.tsx +++ b/devui/pagination/src/components/jump-page.tsx @@ -1,22 +1,69 @@ -import { defineComponent, PropType } from 'vue'; +import { defineComponent, PropType, ref, watch, toRefs, ExtractPropTypes } from 'vue'; + +const jumpPageProps = { + goToText: String, + size: { + type: String as PropType<'lg' | '' | 'sm'>, + default: '' + }, + pageIndex: Number, + showJumpButton: Boolean, + totalPages: Number, + cursor: Number, + onChangeCursorEmit: Function as PropType<(v: number) => void> +} + +type JumpPageProps = ExtractPropTypes export default defineComponent({ - props: { - goToText: String, - size: { - type: String as PropType<'lg' | '' | 'sm'>, - default: '' - }, - inputPageNum: Number, - jump: Function, - jumpPageChange: Function, - showJumpButton: Boolean - } as const, + props: jumpPageProps, + emits: ['changeCursorEmit'], + setup(props: JumpPageProps, { emit }) { + const { + pageIndex, + totalPages, + cursor + } = toRefs(props) + + // 输入跳转页码 + const inputNum = ref(pageIndex.value) + watch( + () => pageIndex.value, + (val: number) => { + inputNum.value = val + } + ) + let curPage = pageIndex.value + const jumpPageChange = (currentPage: number) => { + curPage = +currentPage + inputNum.value = currentPage + if (isNaN(currentPage)) { + setTimeout(() => { + inputNum.value = pageIndex.value + }, 300) + } + } + // 跳转指定页码 + const jump = (e: KeyboardEvent | 'btn') => { + if (curPage > totalPages.value) { + return + } + if ((e === 'btn' || e.key === 'Enter') && cursor.value !== curPage) { + emit('changeCursorEmit', curPage) + } + } + + return { + inputNum, + jumpPageChange, + jump + } + }, render() { const { goToText, size, - inputPageNum, + inputNum, jumpPageChange, jump, showJumpButton @@ -24,14 +71,15 @@ export default defineComponent({ return (
- {goToText} + {goToText} + /> { // TODO 加入国际化后,替换为当前语言为中文的时候加上 '页' @@ -42,7 +90,7 @@ export default defineComponent({
diff --git a/devui/pagination/src/components/page-nums.tsx b/devui/pagination/src/components/page-nums.tsx index 8ebb51d1121d2c7ab2683756b27ab058c61da6c6..eaccee591e463c646433af9d79c8d9f3d8fd30d9 100644 --- a/devui/pagination/src/components/page-nums.tsx +++ b/devui/pagination/src/components/page-nums.tsx @@ -12,7 +12,7 @@ const pageNumBtnProps = { cursor: Number, maxItems: Number, totalPages: Number, - onChangeCursorEmit: Function, + onChangeCursorEmit: Function as PropType<(v: number) => void>, showTruePageIndex: Boolean } as const diff --git a/devui/pagination/src/pagination.scss b/devui/pagination/src/pagination.scss index fa28bfe66b7422528df77cd18e214ea120fd3648..c950daef8cf0c4e0bbe9c26f569fb3d6ac4eed35 100644 --- a/devui/pagination/src/pagination.scss +++ b/devui/pagination/src/pagination.scss @@ -25,11 +25,11 @@ max-width: 100px; .devui-select-input { - height: 44px; + height: 46px; } .devui-select-item { - height: 44px; + height: 46px; } } } @@ -124,25 +124,15 @@ vertical-align: middle; align-items: center; - .devui-input { + .devui-pagination-input { display: inline-block; width: 42px; - height: 28px; vertical-align: middle; margin: 0 3px; + } - &.devui-input-sm { - height: 24px; - padding: 4px 4px; - font-size: $devui-font-size; - line-height: 1.5; - border-radius: $devui-border-radius; - } - - &.devui-input-lg { - width: 56px; - height: 46px; - } + .devui-pagination-input-lg { + width: 56px; } } diff --git a/devui/pagination/src/pagination.tsx b/devui/pagination/src/pagination.tsx index 6e7fd2e83fe6586a284934b854cae55e261ee266..a27c319bc5db3d4101f368119f9a7608d99b2b35 100644 --- a/devui/pagination/src/pagination.tsx +++ b/devui/pagination/src/pagination.tsx @@ -1,9 +1,7 @@ -import { defineComponent, computed, ref, nextTick } from 'vue' +import { defineComponent, computed, nextTick } from 'vue' import { ComponentProps, componentProps } from './use-pagination' import { liteSelectOptions } from './utils' -import clickoutsideDirective from '../../shared/devui-directive/clickoutside' - import ConfigMenu from './components/config-menu' import JumpPage from './components/jump-page' import PageNumBtn from './components/page-nums' @@ -12,9 +10,6 @@ import './pagination.scss' export default defineComponent({ name: 'DPagination', - directives: { - clickoutside: clickoutsideDirective - }, components: { ConfigMenu, JumpPage, @@ -41,16 +36,6 @@ export default defineComponent({ emit('update:pageIndex', val) } }) - const changePageNo = ref(props.pageIndex) - // 输入框显示的页码 - const inputPageNum = computed({ - get() { - return props.pageIndex - }, - set(val: number) { - changePageNo.value = val - } - }) // 每页显示最大条目数量 const currentPageSize = computed({ get() { @@ -65,27 +50,12 @@ export default defineComponent({ const changeCursorEmit = (val: number) => { cursor.value = val - changePageNo.value = val emit('pageIndexChange', val) } - // 输入跳转页码 - const jumpPageChange = (currentPage: string) => { - const curPage = +currentPage - if (isNaN(curPage) || curPage < 1 || curPage > totalPages.value) { - inputPageNum.value = props.pageIndex - return - } - inputPageNum.value = curPage - } - // 跳转指定页码 - const jump = (e: KeyboardEvent | 'btn') => { - if ((e === 'btn' || e.key === 'Enter') && cursor.value !== changePageNo.value) { - cursor.value = changePageNo.value - } - } + // 每页条数改变 - const pageSizeChange = (value: number) => { - currentPageSize.value = value + const pageSizeChange = (val: Record) => { + currentPageSize.value = val.value as number // 页数改变后,如果当前页码超出最大页码时修正 if (props.autoFixPageIndex) { nextTick(() => { @@ -94,7 +64,7 @@ export default defineComponent({ } }) } - emit('pageSizeChange', value) + emit('pageSizeChange', val.value as number) } // 极简模式下的跳转页码 const litePageIndexChange = (page: {name: string; value: number;}) => { @@ -104,10 +74,7 @@ export default defineComponent({ return { cursor, totalPages, - jump, changeCursorEmit, - inputPageNum, - jumpPageChange, currentPageSize, pageSizeChange, litePageOptions, @@ -118,6 +85,7 @@ export default defineComponent({ const { total, + pageIndex, pageSizeOptions, pageSizeDirection, preLink, @@ -139,9 +107,6 @@ export default defineComponent({ cursor, totalPages, - jump, - inputPageNum, - jumpPageChange, currentPageSize, pageSizeChange, changeCursorEmit, @@ -206,11 +171,12 @@ export default defineComponent({ {...{ goToText, size, - inputPageNum, - jumpPageChange, - jump, + pageIndex, + totalPages, + cursor, showJumpButton }} + onChangeCursorEmit={changeCursorEmit} /> } { diff --git a/devui/quadrant-diagram/config.ts b/devui/quadrant-diagram/config.ts index f5902f086711dbf95db7f14e9dcd2342e327990b..c89a9443fabb944eb60aed1846432de4825d9540 100644 --- a/devui/quadrant-diagram/config.ts +++ b/devui/quadrant-diagram/config.ts @@ -29,6 +29,10 @@ export const DEFAULT_QUADRANT_CONFIGS = [ { title: '不重要不紧急' }, { title: '不重要紧急' } ]; +export const DEFAULT_VIEW_CONFIGS = { + height: 900, + width: 950, +} export const AXIS_TITLE_SPACE = 15; export const SMALL_LABEL_SIZE_CENTER_POINT = { x: 6, y: 6 diff --git a/devui/quadrant-diagram/src/components/axis/index.tsx b/devui/quadrant-diagram/src/components/axis/index.tsx index f7353d000e7d83f8eb240a2b59036e853f2fc75d..0e0d7d398df2b988f0d6885d3c12bf07f024d518 100644 --- a/devui/quadrant-diagram/src/components/axis/index.tsx +++ b/devui/quadrant-diagram/src/components/axis/index.tsx @@ -1,22 +1,22 @@ -import { defineComponent, toRefs, onMounted, ExtractPropTypes, reactive, ref } from 'vue' -import { IViewConfigs, IAxisConfigs } from '../../../type'; -import { AXIS_TITLE_SPACE } from '../../../config'; -import { quadrantDiagramAxisProps } from './types' -import { debounce } from 'lodash'; +import { defineComponent, toRefs, onMounted, ExtractPropTypes, reactive, ref, watch } from 'vue' +import { IViewConfigs, IAxisConfigs } from '../../../type' +import { AXIS_TITLE_SPACE } from '../../../config' +import { quadrantDiagramAxisProps, QuadrantDiagramAxisProps } from './types' +import { debounce } from 'lodash' import './index.scss' -const Axis = defineComponent({ +export default defineComponent({ name: 'DQuadrantDiagramAxis', props: quadrantDiagramAxisProps, - setup(props: ExtractPropTypes) { + setup(props: QuadrantDiagramAxisProps) { - const { diagramId, view, axisConfigs } = toRefs(props); - const AXIS_COLOR = ref('#0000ff'); - const AXIS_LABEL_COLOR = ref('#ff0000'); + const { diagramId, view, axisConfigs } = toRefs(props) + const AXIS_COLOR = ref('#0000ff') + const AXIS_LABEL_COLOR = ref('#ff0000') - const quadrantAxis = ref(); - const context = ref(); + const quadrantAxis = ref() + const context = ref() const axisInnerAttr = reactive({ axisOrigin: { x: 0, @@ -32,179 +32,185 @@ const Axis = defineComponent({ yTickSpacing: 0, }) - const axisConfigsVal: IAxisConfigs = axisConfigs.value; - const viewVal: IViewConfigs = view.value; + const axisConfigsVal: IAxisConfigs = axisConfigs.value + const viewVal: IViewConfigs = view.value onMounted(() => { - resetAxis(); - }); + resetAxis() + }) + + watch(viewVal, () => { + resetAxis() + }) const resetAxis = debounce(() => { - initAxisData(); - setAxisData(); - drawAxis(); - drawAxisLabels(); - }, 200); + initAxisData() + setAxisData() + drawAxis() + drawAxisLabels() + }, 200) /** * 获取 canvas 并赋值宽高 */ const initAxisData = () => { - quadrantAxis.value = document.querySelector('#devui-quadrant-axis-' + diagramId.value); - quadrantAxis.value.width = viewVal.width; - quadrantAxis.value.height = viewVal.height; + quadrantAxis.value = document.querySelector('#devui-quadrant-axis-' + diagramId.value) + quadrantAxis.value.width = viewVal.width + quadrantAxis.value.height = viewVal.height } const setAxisData = () => { - context.value = quadrantAxis.value.getContext('2d'); - axisInnerAttr.axisOrigin = axisConfigsVal.axisOrigin; - axisInnerAttr.axisTop = axisConfigsVal.axisTop; - axisInnerAttr.axisRight = axisConfigsVal.axisRight; - axisInnerAttr.axisWidth = axisConfigsVal.axisWidth; - axisInnerAttr.axisHeight = axisConfigsVal.axisHeight; - axisInnerAttr.yAxisTicksNum = axisConfigsVal.yAxisTicksNum; - axisInnerAttr.xAxisTicksNum = axisConfigsVal.xAxisTicksNum; - axisInnerAttr.xTickSpacing = axisConfigsVal.xTickSpacing; - axisInnerAttr.yTickSpacing = axisConfigsVal.yTickSpacing; + context.value = quadrantAxis.value.getContext('2d') + axisInnerAttr.axisOrigin = axisConfigsVal.axisOrigin + axisInnerAttr.axisTop = axisConfigsVal.axisTop + axisInnerAttr.axisRight = axisConfigsVal.axisRight + axisInnerAttr.axisWidth = axisConfigsVal.axisWidth + axisInnerAttr.axisHeight = axisConfigsVal.axisHeight + axisInnerAttr.yAxisTicksNum = axisConfigsVal.yAxisTicksNum + axisInnerAttr.xAxisTicksNum = axisConfigsVal.xAxisTicksNum + axisInnerAttr.xTickSpacing = axisConfigsVal.xTickSpacing + axisInnerAttr.yTickSpacing = axisConfigsVal.yTickSpacing } /** * 执行绘制 */ const drawAxis = () => { - context.value.save(); - context.value.fillStyle = AXIS_COLOR.value; - context.value.strokeStyle = AXIS_COLOR.value; - drawXAxis(); - drawYAxis(); - context.value.lineWidth = 0.5; - drawXAxisTicks(); - drawYAxisTicks(); - context.value.restore(); + context.value.save() + context.value.fillStyle = AXIS_COLOR.value + context.value.strokeStyle = AXIS_COLOR.value + drawXAxis() + drawYAxis() + context.value.lineWidth = 0.5 + drawXAxisTicks() + drawYAxisTicks() + context.value.restore() } /** * 绘制 XY 轴 */ const drawYAxis = () => { - context.value.beginPath(); - context.value.moveTo(axisInnerAttr.axisOrigin.x, axisInnerAttr.axisOrigin.y); - context.value.lineTo(axisInnerAttr.axisOrigin.x, axisInnerAttr.axisTop - axisConfigsVal.axisMargin); - context.value.stroke(); - context.value.moveTo(axisInnerAttr.axisOrigin.x, axisInnerAttr.axisTop - axisConfigsVal.axisMargin); - context.value.lineTo(axisInnerAttr.axisOrigin.x + 5, axisInnerAttr.axisTop - axisConfigsVal.axisMargin + 10); - context.value.lineTo(axisInnerAttr.axisOrigin.x - 5, axisInnerAttr.axisTop - axisConfigsVal.axisMargin + 10); - context.value.fill(); + context.value.beginPath() + context.value.moveTo(axisInnerAttr.axisOrigin.x, axisInnerAttr.axisOrigin.y) + context.value.lineTo(axisInnerAttr.axisOrigin.x, axisInnerAttr.axisTop - axisConfigsVal.axisMargin) + context.value.stroke() + context.value.moveTo(axisInnerAttr.axisOrigin.x, axisInnerAttr.axisTop - axisConfigsVal.axisMargin) + context.value.lineTo(axisInnerAttr.axisOrigin.x + 5, axisInnerAttr.axisTop - axisConfigsVal.axisMargin + 10) + context.value.lineTo(axisInnerAttr.axisOrigin.x - 5, axisInnerAttr.axisTop - axisConfigsVal.axisMargin + 10) + context.value.fill() } const drawXAxis = () => { - context.value.beginPath(); - context.value.moveTo(axisInnerAttr.axisOrigin.x, axisInnerAttr.axisOrigin.y); - context.value.lineTo(axisInnerAttr.axisRight + axisConfigsVal.axisMargin - 10, axisInnerAttr.axisOrigin.y); - context.value.stroke(); + context.value.beginPath() + context.value.moveTo(axisInnerAttr.axisOrigin.x, axisInnerAttr.axisOrigin.y) + context.value.lineTo(axisInnerAttr.axisRight + axisConfigsVal.axisMargin - 10, axisInnerAttr.axisOrigin.y) + context.value.stroke() // 绘制坐标轴三角形 - context.value.moveTo(axisInnerAttr.axisRight + axisConfigsVal.axisMargin, axisInnerAttr.axisOrigin.y); - context.value.lineTo(axisInnerAttr.axisRight + axisConfigsVal.axisMargin - 10, axisInnerAttr.axisOrigin.y + 5); - context.value.lineTo(axisInnerAttr.axisRight + axisConfigsVal.axisMargin - 10, axisInnerAttr.axisOrigin.y - 5); - context.value.fill(); + context.value.moveTo(axisInnerAttr.axisRight + axisConfigsVal.axisMargin, axisInnerAttr.axisOrigin.y) + context.value.lineTo(axisInnerAttr.axisRight + axisConfigsVal.axisMargin - 10, axisInnerAttr.axisOrigin.y + 5) + context.value.lineTo(axisInnerAttr.axisRight + axisConfigsVal.axisMargin - 10, axisInnerAttr.axisOrigin.y - 5) + context.value.fill() } /** * 绘制轴线刻度 */ const drawXAxisTicks = () => { - let deltaY; + let deltaY: number for (let i = 1; i < axisInnerAttr.xAxisTicksNum; i++) { - context.value.beginPath(); + context.value.beginPath() // 判断显示长刻度还是短刻度 if (i % axisConfigsVal.xAxisRange.step === 0) { - deltaY = axisConfigsVal.tickWidth; + deltaY = axisConfigsVal.tickWidth } else { - deltaY = axisConfigsVal.tickWidth / 2; + deltaY = axisConfigsVal.tickWidth / 2 } context.value.moveTo(axisInnerAttr.axisOrigin.x + i * axisInnerAttr.xTickSpacing, - axisInnerAttr.axisOrigin.y - deltaY); + axisInnerAttr.axisOrigin.y - deltaY) context.value.lineTo(axisInnerAttr.axisOrigin.x + i * axisInnerAttr.xTickSpacing, - axisInnerAttr.axisOrigin.y + deltaY); - context.value.stroke(); + axisInnerAttr.axisOrigin.y + deltaY) + context.value.stroke() } } const drawYAxisTicks = () => { - let deltaX; + let deltaX: number for (let i = 1; i < axisInnerAttr.yAxisTicksNum; i++) { - context.value.beginPath(); + context.value.beginPath() if (i % axisConfigsVal.yAxisRange.step === 0) { - deltaX = axisConfigsVal.tickWidth; + deltaX = axisConfigsVal.tickWidth } else { - deltaX = axisConfigsVal.tickWidth / 2; + deltaX = axisConfigsVal.tickWidth / 2 } context.value.moveTo(axisInnerAttr.axisOrigin.x - deltaX, - axisInnerAttr.axisOrigin.y - i * axisInnerAttr.yTickSpacing); + axisInnerAttr.axisOrigin.y - i * axisInnerAttr.yTickSpacing) context.value.lineTo(axisInnerAttr.axisOrigin.x + deltaX, - axisInnerAttr.axisOrigin.y - i * axisInnerAttr.yTickSpacing); - context.value.stroke(); + axisInnerAttr.axisOrigin.y - i * axisInnerAttr.yTickSpacing) + context.value.stroke() } } - + /** + * 绘制轴线标签 + */ const drawAxisLabels = () => { - context.value.save(); - context.value.fillStyle = AXIS_LABEL_COLOR.value; - drawXTicksLabels(); - drawYTicksLabels(); - context.value.restore(); - drawAxisTitle(); + context.value.save() + context.value.fillStyle = AXIS_LABEL_COLOR.value + drawXTicksLabels() + drawYTicksLabels() + context.value.restore() + drawAxisTitle() } const drawXTicksLabels = () => { - context.value.textAlign = 'center'; - context.value.textBaseline = 'top'; + context.value.textAlign = 'center' + context.value.textBaseline = 'top' for (let i = 0; i <= axisInnerAttr.xAxisTicksNum; i++) { if (i % axisConfigsVal.xAxisRange.step === 0) { context.value.fillText(i, axisInnerAttr.axisOrigin.x + i * axisInnerAttr.xTickSpacing, - axisInnerAttr.axisOrigin.y + axisConfigsVal.spaceBetweenLabelsAxis); + axisInnerAttr.axisOrigin.y + axisConfigsVal.spaceBetweenLabelsAxis) } } - }; + } const drawYTicksLabels = () => { - context.value.textAlign = 'center'; - context.value.textBaseline = 'middle'; + context.value.textAlign = 'center' + context.value.textBaseline = 'middle' for (let i = 0; i <= axisInnerAttr.yAxisTicksNum; i++) { if (i % axisConfigsVal.yAxisRange.step === 0) { context.value.fillText(i, axisInnerAttr.axisOrigin.x - axisConfigsVal.spaceBetweenLabelsAxis, - axisInnerAttr.axisOrigin.y - i * axisInnerAttr.yTickSpacing); + axisInnerAttr.axisOrigin.y - i * axisInnerAttr.yTickSpacing) } } - }; + } const drawAxisTitle = () => { - context.value.font = '12px Microsoft YaHei'; - context.value.textAlign = 'left'; - context.value.fillStyle = AXIS_LABEL_COLOR.value; - const xLabelWidth = context.value.measureText(axisConfigsVal.xAxisLabel).width; + context.value.font = '12px Microsoft YaHei' + context.value.textAlign = 'left' + context.value.fillStyle = AXIS_LABEL_COLOR.value + const xLabelWidth = context.value.measureText(axisConfigsVal.xAxisLabel).width rotateLabel(axisConfigsVal.xAxisLabel, axisInnerAttr.axisRight + axisConfigsVal.axisMargin / 2, - axisInnerAttr.axisOrigin.y - xLabelWidth - AXIS_TITLE_SPACE); + axisInnerAttr.axisOrigin.y - xLabelWidth - AXIS_TITLE_SPACE) context.value.fillText(axisConfigsVal.yAxisLabel, - axisInnerAttr.axisOrigin.x + AXIS_TITLE_SPACE, axisInnerAttr.axisTop - axisConfigsVal.axisMargin / 2); - }; + axisInnerAttr.axisOrigin.x + AXIS_TITLE_SPACE, axisInnerAttr.axisTop - axisConfigsVal.axisMargin / 2) + } const rotateLabel = (name: string, x: number, y: number) => { for (let i = 0; i < name.length; i++) { - const str = name.slice(i, i + 1).toString(); + const str = name.slice(i, i + 1).toString() if (str.match(/[A-Za-z0-9]/)) { - context.value.save(); - context.value.translate(x, y); - context.value.rotate(Math.PI / 180 * 90); - context.value.textBaseline = 'bottom'; - context.value.fillText(str, 0, 0); - context.value.restore(); - y += context.value.measureText(str).width; + context.value.save() + context.value.translate(x, y) + context.value.rotate(Math.PI / 180 * 90) + context.value.textBaseline = 'bottom' + context.value.fillText(str, 0, 0) + context.value.restore() + y += context.value.measureText(str).width } else if (str.match(/[\u4E00-\u9FA5]/)) { - context.value.save(); - context.value.textBaseline = 'top'; - context.value.fillText(str, x, y); - context.value.restore(); - y += context.value.measureText(str).width; + context.value.save() + context.value.textBaseline = 'top' + context.value.fillText(str, x, y) + context.value.restore() + y += context.value.measureText(str).width } } } @@ -212,13 +218,11 @@ const Axis = defineComponent({ }, render() { - const { diagramId } = this; + const { diagramId } = this return (
- ); + ) } -}) - -export default Axis; \ No newline at end of file +}) \ No newline at end of file diff --git a/devui/quadrant-diagram/src/components/axis/types.ts b/devui/quadrant-diagram/src/components/axis/types.ts index e22f059ed952e1fea802bb2a6fb6d1492aed35a6..c6c84fdd4d1ebfa8f5bd75c5cec6e3da77ec2341 100644 --- a/devui/quadrant-diagram/src/components/axis/types.ts +++ b/devui/quadrant-diagram/src/components/axis/types.ts @@ -1,5 +1,5 @@ import type { ExtractPropTypes, PropType } from 'vue' -import { IViewConfigs, IAxisConfigs } from '../../../type'; +import { IViewConfigs, IAxisConfigs } from '../../../type' export const quadrantDiagramAxisProps = { diagramId: { diff --git a/devui/quadrant-diagram/src/quadrant-diagram-types.ts b/devui/quadrant-diagram/src/quadrant-diagram-types.ts index 3513e64f531df83a44387dd77b6b03e976b3f135..26f9e8df2362547839327b3ae32aaf92ee4d45b6 100644 --- a/devui/quadrant-diagram/src/quadrant-diagram-types.ts +++ b/devui/quadrant-diagram/src/quadrant-diagram-types.ts @@ -1,6 +1,6 @@ import type { ExtractPropTypes, PropType } from 'vue' -import { DEFAULT_AXIS_CONFIGS } from '../config'; -import { IViewConfigs, IAxisConfigs } from '../type'; +import { DEFAULT_AXIS_CONFIGS, DEFAULT_VIEW_CONFIGS } from '../config' +import { IViewConfigs, IAxisConfigs } from '../type' export const quadrantDiagramProps = { diagramId: { @@ -13,7 +13,7 @@ export const quadrantDiagramProps = { }, view: { type: Object as PropType, - default: { height: 720, width: 720 }, + default: DEFAULT_VIEW_CONFIGS, }, } as const diff --git a/devui/quadrant-diagram/src/quadrant-diagram.tsx b/devui/quadrant-diagram/src/quadrant-diagram.tsx index d5506a13465643435576d2673782ec8980542766..27b01404ce9247110fd8e54b69a3200f33133ac5 100644 --- a/devui/quadrant-diagram/src/quadrant-diagram.tsx +++ b/devui/quadrant-diagram/src/quadrant-diagram.tsx @@ -1,17 +1,17 @@ -import { defineComponent, toRefs, ref, onMounted, reactive } from 'vue' +import { defineComponent, toRefs, reactive, watch } from 'vue' import { quadrantDiagramProps, QuadrantDiagramProps } from './quadrant-diagram-types' -import DQuadrantDiagramAxis from './components/axis'; -import { DEFAULT_AXIS_CONFIGS } from '../config'; +import DQuadrantDiagramAxis from './components/axis' +import { DEFAULT_AXIS_CONFIGS } from '../config' export default defineComponent({ name: 'DQuadrantDiagram', props: quadrantDiagramProps, emits: [], - setup(props, ctx) { - const { diagramId, axisConfigs, view } = toRefs(props); + setup(props: QuadrantDiagramProps) { + const { diagramId, axisConfigs, view } = toRefs(props) - const axisConfigsVal = axisConfigs.value; - const viewVal = view.value; + const axisConfigsVal = axisConfigs.value + const viewVal = view.value const calAxisConfig = reactive({ axisOrigin: { x: null, y: null }, @@ -26,36 +26,40 @@ export default defineComponent({ }) const initAxisData = () => { - const axisConfigKeys = Object.keys(DEFAULT_AXIS_CONFIGS); + const axisConfigKeys = Object.keys(DEFAULT_AXIS_CONFIGS) for (let i = 0; i < axisConfigKeys.length; i++) { if (calAxisConfig[axisConfigKeys[i]] === undefined) { - calAxisConfig[axisConfigKeys[i]] = DEFAULT_AXIS_CONFIGS[axisConfigKeys[i]]; + calAxisConfig[axisConfigKeys[i]] = DEFAULT_AXIS_CONFIGS[axisConfigKeys[i]] } } calAxisConfig.axisOrigin = { x: axisConfigsVal.originPosition.left, y: viewVal.height - axisConfigsVal.originPosition.bottom - }; - calAxisConfig.axisTop = axisConfigsVal.axisMargin; - calAxisConfig.axisRight = viewVal.width - axisConfigsVal.axisMargin; - calAxisConfig.axisWidth = calAxisConfig.axisRight - calAxisConfig.axisOrigin.x; - calAxisConfig.axisHeight = calAxisConfig.axisOrigin.y - calAxisConfig.axisTop; - calAxisConfig.yAxisTicksNum = axisConfigsVal.yAxisRange.max - axisConfigsVal.yAxisRange.min; - calAxisConfig.xAxisTicksNum = axisConfigsVal.xAxisRange.max - axisConfigsVal.xAxisRange.min; - calAxisConfig.xTickSpacing = calAxisConfig.axisWidth / calAxisConfig.xAxisTicksNum; - calAxisConfig.yTickSpacing = calAxisConfig.axisHeight / calAxisConfig.yAxisTicksNum; + } + calAxisConfig.axisTop = axisConfigsVal.axisMargin + calAxisConfig.axisRight = viewVal.width - axisConfigsVal.axisMargin + calAxisConfig.axisWidth = calAxisConfig.axisRight - calAxisConfig.axisOrigin.x + calAxisConfig.axisHeight = calAxisConfig.axisOrigin.y - calAxisConfig.axisTop + calAxisConfig.yAxisTicksNum = axisConfigsVal.yAxisRange.max - axisConfigsVal.yAxisRange.min + calAxisConfig.xAxisTicksNum = axisConfigsVal.xAxisRange.max - axisConfigsVal.xAxisRange.min + calAxisConfig.xTickSpacing = calAxisConfig.axisWidth / calAxisConfig.xAxisTicksNum + calAxisConfig.yTickSpacing = calAxisConfig.axisHeight / calAxisConfig.yAxisTicksNum } - initAxisData(); + initAxisData() + + watch(viewVal, () => { + initAxisData() + }) - return { diagramId, calAxisConfig, viewVal }; + return { diagramId, calAxisConfig, } }, render() { - const { diagramId, calAxisConfig, viewVal } = this; + const { diagramId, calAxisConfig, view } = this return (
- +
) } diff --git a/devui/shared/util/class.ts b/devui/shared/util/class.ts new file mode 100644 index 0000000000000000000000000000000000000000..046f6ce669bdd1fb103f41f94fa88a47a854be9e --- /dev/null +++ b/devui/shared/util/class.ts @@ -0,0 +1,44 @@ +/** + * 判断 DOM 中的元素是否含有某个类 + * @param el 元素 + * @param className 类名 + * @returns + */ +export function hasClass(el: HTMLElement, className: string): boolean { + if (el.classList) { + return el.classList.contains(className); + } + const originClass = el.className; + return ` ${originClass} `.indexOf(` ${className} `) > -1; +} + +/** + * 向 DOM 中的元素添加一个类 + * @param el 元素 + * @param className 类名 + */ +export function addClass(el: HTMLElement, className: string): void { + if (el.classList) { + el.classList.add(className); + } else { + if (!hasClass(el, className)) { + el.className = `${el.className} ${className}`; + } + } +} + +/** + * 从 DOM 中的元素移除一个类 + * @param el 元素 + * @param className 类名 + */ +export function removeClass(el: HTMLElement, className: string): void { + if (el.classList) { + el.classList.remove(className); + } else { + if (hasClass(el, className)) { + const originClass = el.className; + el.className = ` ${originClass} `.replace(` ${className} `, ' '); + } + } +} diff --git a/devui/shared/util/set-style.ts b/devui/shared/util/set-style.ts new file mode 100644 index 0000000000000000000000000000000000000000..8ce5065bc54f92085bf349b504be3c41cfa29b1c --- /dev/null +++ b/devui/shared/util/set-style.ts @@ -0,0 +1,26 @@ +import type { CSSProperties } from 'vue'; + +/** + * 设置元素的样式,返回上一次的样式 + * @param element + * @param style + * @returns + */ +export function setStyle( + element: HTMLElement, + style: CSSProperties, +): CSSProperties { + const oldStyle: CSSProperties = {}; + + const styleKeys = Object.keys(style); + + styleKeys.forEach((key) => { + oldStyle[key] = element.style[key]; + }); + + styleKeys.forEach((key) => { + element.style[key] = style[key]; + }); + + return oldStyle; +} \ No newline at end of file diff --git a/devui/splitter/index.ts b/devui/splitter/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..6c473534b4971b077f25fc0ed09be6d80465b275 --- /dev/null +++ b/devui/splitter/index.ts @@ -0,0 +1,18 @@ +import type { App } from 'vue' +import Splitter from './src/splitter' +import SplitterPane from './src/splitter-pane' + +Splitter.install = function (app: App): void { + app.component(Splitter.name, Splitter) + app.component(SplitterPane.name, SplitterPane) +} + +export { Splitter } + +export default { + title: 'Splitter 分割器', + category: '布局', + install(app: App) { + app.use(Splitter as any) + }, +} diff --git a/devui/splitter/src/splitter-bar-type.tsx b/devui/splitter/src/splitter-bar-type.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e7ac1e231322a5c47e9e25bc859f6b169f90a2aa --- /dev/null +++ b/devui/splitter/src/splitter-bar-type.tsx @@ -0,0 +1,34 @@ +import { PropType, ExtractPropTypes } from 'vue'; +import { SplitterOrientation } from './splitter-types'; + + +export const splitterBarProps = { + /** + * 当前 pane 的索引 + */ + index: { + type: Number, + }, + /** + * 必选,指定 SplitterBar 的方向 + */ + orientation: { + type: String as PropType, + required: true, + }, + /** + * 分隔条大小 + */ + splitBarSize: { + type: String, + required: true, + }, + /** + * 是否显示展开/收缩按钮 + */ + showCollapseButton: { + type: Boolean, + }, +} as const; + +export type SplitterBarProps = ExtractPropTypes; diff --git a/devui/splitter/src/splitter-bar.scss b/devui/splitter/src/splitter-bar.scss new file mode 100644 index 0000000000000000000000000000000000000000..bc6495b11999a53cdb4cd99272773cd135f47f95 --- /dev/null +++ b/devui/splitter/src/splitter-bar.scss @@ -0,0 +1,115 @@ +@import '../../style/theme/color'; +@import '../../style/theme/corner'; + +.devui-splitter-bar { + background-color: $devui-dividing-line; + display: flex; + position: relative; + align-items: center; + justify-content: center; + flex-grow: 0; + flex-shrink: 0; + + .devui-collapse { + background-color: $devui-dividing-line; + position: absolute; + z-index: 15; + cursor: pointer; + + &::before, + &::after { + content: ''; + width: 10px; + height: 2px; + background: #ffffff; + display: block; + position: absolute; + } + + &:hover { + background-color: $devui-brand-hover; + } + } + + &-horizontal { + .devui-collapse { + width: 12px; + height: 30px; + + &.prev { + border-radius: + 0 $devui-border-radius-feedback + $devui-border-radius-feedback 0; + left: 100%; + + &::before, + &.collapsed::before { + top: 9px; + left: 1px; + } + + // 设置线条 + &::before { + transform: rotate(-70deg); + } + + &.collapsed::before { + transform: rotate(70deg); + } + + &::after { + transform: rotate(70deg); + } + + &.collapsed::after { + transform: rotate(-70deg); + } + + &::after { + top: 18px; + left: 1px; + } + } + } + + &.resizable { + // 修正 IE 浏览器,css 伪元素中鼠标手型无效 + cursor: col-resize; + + &::after { + // content 由下面的 :not(.none-resizable) 控制显示 + cursor: col-resize; + height: 100%; + width: 10px; + top: 0; + } + + &:not(.none-resizable) { + // 非折叠的情况下 + &:hover, + &:focus, + &:active { + background-color: $devui-brand-hover; + } + + &::after { + content: ''; + display: block; + position: absolute; + z-index: 10; + } + } + } + } + + &-vertical.resizable { + cursor: row-resize; + + &::after { + cursor: row-resize; + width: 100%; + height: 10px; + left: 0; + } + } +} diff --git a/devui/splitter/src/splitter-bar.tsx b/devui/splitter/src/splitter-bar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..51b05f18a103415003dda7fd1d46e6c404470010 --- /dev/null +++ b/devui/splitter/src/splitter-bar.tsx @@ -0,0 +1,184 @@ +import { + defineComponent, + ref, + watch, + nextTick, + reactive, + computed, + withDirectives, + onMounted, +} from 'vue' +import { useSplitterStore } from './splitter-store' +import { setStyle } from '../../shared/util/set-style' +import { addClass, removeClass } from '../../shared/util/class' +import dresize, { ResizeDirectiveProp } from './util/d-resize-directive' +import './splitter-bar.scss' +import { splitterBarProps, SplitterBarProps } from './splitter-bar-type' + +export default defineComponent({ + name: 'DSplitterBar', + props: splitterBarProps, + setup(props: SplitterBarProps) { + const { + splitterState, + getPane, + isStaticBar, + isResizable, + dragState, + setSize, + tooglePane, + } = useSplitterStore() + const state = reactive({ + wrapperClass: `devui-splitter-bar devui-splitter-bar-${props.orientation}`, + }) + const domRef = ref() + + watch( + () => props.splitBarSize, + (curSplitBarSize) => { + nextTick(() => { + const ele = domRef?.value + setStyle(ele, { flexBasis: curSplitBarSize }) + }) + }, + { immediate: true } + ) + + watch( + () => splitterState.panes, + () => { + if (!isStaticBar(props.index)) { + state.wrapperClass += ' resizable' + } else { + // TODO 禁用的样式处理 + // console.log(666); + } + }, + { deep: true } + ) + + // 指令输入值 + const coordinate = { pageX: 0, pageY: 0, originalX: 0, originalY: 0 } + let initState + // TODO 待优化,如何像 angular rxjs 操作一样优雅 + const resizeProp: ResizeDirectiveProp = { + enableResize: true, + onPressEvent: ({ originalEvent }) => { + originalEvent.stopPropagation() // 按下的时候,阻止事件冒泡 + if (!isResizable(props.index)) return + initState = dragState(props.index) + coordinate.originalX = originalEvent.pageX + coordinate.originalY = originalEvent.pageX + }, + onDragEvent: function ({ originalEvent }) { + originalEvent.stopPropagation() // 移动的时候,阻止事件冒泡 + if (!isResizable(props.index)) return + coordinate.pageX = originalEvent.pageX + coordinate.pageY = originalEvent.pageX + let distance + if (props.orientation === 'vertical') { + distance = coordinate.pageY - coordinate.originalY + } else { + distance = coordinate.pageX - coordinate.originalX + } + setSize(initState, distance) + }, + onReleaseEvent: function ({ originalEvent }) { + originalEvent.stopPropagation() // 释放的时候,阻止事件冒泡 + if (!isResizable(props.index)) return + coordinate.pageX = originalEvent.pageX + coordinate.pageY = originalEvent.pageX + let distance + if (props.orientation === 'vertical') { + distance = coordinate.pageY - coordinate.originalY + } else { + distance = coordinate.pageX - coordinate.originalX + } + setSize(initState, distance) + }, + } + + const queryPanes = (index, nearIndex) => { + const pane = getPane(index) + const nearPane = getPane(nearIndex) + return { + pane, + nearPane, + } + } + + // 根据当前状态生成收起按钮样式 + const generateCollapseClass = (pane, nearPane, showIcon) => { + // 是否允许收起 + const isCollapsible = pane?.component?.props?.collapsible && showIcon + // 当前收起状态 + const isCollapsed = pane?.component?.props?.collapsed + // 一个 pane 收起的时候,隐藏相邻 pane 的收起按钮 + const isNearPaneCollapsed = nearPane.collapsed + return { + 'devui-collapse': isCollapsible, + collapsed: isCollapsed, + hidden: isNearPaneCollapsed, + } + } + + // 计算前面板收起操作样式 + const prevClass = computed(() => { + const { pane, nearPane } = queryPanes(props.index, props.index + 1) + // 第一个面板或者其它面板折叠方向不是向后的, 显示操作按钮 + const showIcon = + pane?.component?.props?.collapseDirection !== 'after' || + props.index === 0 + return generateCollapseClass(pane, nearPane, showIcon) + }) + + // 切换是否允许拖拽,收起时不能拖拽 + const toggleResize = () => { + const { pane, nearPane } = queryPanes(props.index, props.index + 1) + const isCollapsed = + pane?.component?.props?.collapsed || + nearPane?.component?.props?.collapsed + if (isCollapsed) { + addClass(domRef.value, 'none-resizable') + } else { + removeClass(domRef.value, 'none-resizable') + } + } + + const handleCollapsePrePane = (lockStatus?) => { + tooglePane(props.index, props.index + 1, lockStatus) + toggleResize() + } + + const handleCollapseNextPane = () => { + /**TODO */ + } + + const initialCollapseStatus = () => { + handleCollapsePrePane(true) + // collapseNextPane(true); + } + + onMounted(() => { + initialCollapseStatus() + }) + + return () => { + return withDirectives( +
+ {props.showCollapseButton ? ( +
handleCollapsePrePane()} + >
+ ) : null} +
+ {props.showCollapseButton ? ( + + ) : null} +
, + [[dresize, resizeProp]] + ) + } + }, +}) diff --git a/devui/splitter/src/splitter-pane-type.tsx b/devui/splitter/src/splitter-pane-type.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9e366021881c4576495bd2041d9ff4ad65d50711 --- /dev/null +++ b/devui/splitter/src/splitter-pane-type.tsx @@ -0,0 +1,74 @@ +import { ExtractPropTypes, PropType } from 'vue'; +import { CollapseDirection } from './splitter-types'; + +export const splitterPaneProps = { + /** + * 可选,指定 pane 宽度,设置像素值或者百分比 + * pane初始化大小 + */ + size: { + type: String, + }, + /** + * 可选,指定 pane 最小宽度,设置像素值或者百分比 + */ + minSize: { + type: String, + }, + /** + * 可选,指定 pane 最大宽度,设置像素值或者百分比 + */ + maxSize: { + type: String, + }, + /** + * 可选,指定 pane 是否可调整大小,会影响相邻 pane + */ + resizable: { + type: Boolean, + default: true, + }, + /** + * 可选,指定 pane 是否可折叠收起 + */ + collapsible: { + type: Boolean, + default: false, + }, + /** + * 可选,指定 pane 初始化是否收起,配合 collapsible 使用 + */ + collapsed: { + type: Boolean, + default: false, + }, + /** + * 非边缘面板折叠方向,before 只生成向前折叠的按钮,after 生成向后折叠按钮,both 生成两个 + */ + collapseDirection: { + type: String as PropType, + default: 'both', + }, + /** + * 可选,是否在 pane 进行折叠后收缩 pane 宽度而非收起 + */ + shrink: { + type: Boolean, + default: false, + }, + /** + * 可选,折叠后收缩的 pane 宽度 (单位:px) + */ + shrinkWidth: { + type: Number, + default: 36, + }, + /** + * 内部排版使用,不对外提供,TODO 待优化 + */ + order: { + type: Number, + }, +} as const; + +export type SplitterPaneProps = ExtractPropTypes; diff --git a/devui/splitter/src/splitter-pane.scss b/devui/splitter/src/splitter-pane.scss new file mode 100644 index 0000000000000000000000000000000000000000..c8cf77007b063cb9015713205905661aaf40bfc3 --- /dev/null +++ b/devui/splitter/src/splitter-pane.scss @@ -0,0 +1,23 @@ +.devui-splitter-pane { + position: relative; + flex: 1 1 auto; + display: block; + min-width: 0; + max-width: 100%; + min-height: 0; + max-height: 100%; + + &-fixed { + flex-grow: 0; + flex-shrink: 0; + } + + &-hidden { + flex: 0 !important; + overflow: hidden !important; + } + + &-grow { + flex-grow: 1 !important; + } +} diff --git a/devui/splitter/src/splitter-pane.tsx b/devui/splitter/src/splitter-pane.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3da126e776fdcf116a9f012d507c7fc14dd0b4b8 --- /dev/null +++ b/devui/splitter/src/splitter-pane.tsx @@ -0,0 +1,137 @@ +import { + defineComponent, + ref, + watch, + nextTick, + inject, + onMounted, + onUpdated, +} from 'vue' +import { + addClass, + hasClass, + removeClass, +} from '../../shared/util/class' +import { setStyle } from '../../shared/util/set-style' +import { useSplitterStore } from './splitter-store' +import './splitter-pane.scss' +import { splitterPaneProps, SplitterPaneProps } from './splitter-pane-type' + +export default defineComponent({ + name: 'DSplitterPane', + props: splitterPaneProps, + emits: ['sizeChange', 'collapsedChange'], + setup(props: SplitterPaneProps, { slots, expose, emit }) { + const { setPanes } = useSplitterStore() + + const domRef = ref() + watch( + () => props.order, + (order) => { + nextTick(() => { + const ele = domRef.value + setStyle(ele, { order }) + }) + }, + { immediate: true } + ) + + // pane 初始化大小 + const setSizeStyle = (curSize) => { + const ele = domRef.value + ele.style.flexBasis = curSize + const paneFixedClass = 'devui-splitter-pane-fixed' + if (curSize) { + // 设置 flex-grow 和 flex-shrink + addClass(ele, paneFixedClass) + } else { + removeClass(ele, paneFixedClass) + } + } + + watch( + () => props.size, + (newSize) => { + nextTick(() => { + setSizeStyle(newSize) + }) + }, + { immediate: true } + ) + + const panes = inject('panes') + const orientation = inject('orientation') + let initialSize = '' // 记录初始化挂载传入的大小 + onMounted(() => { + initialSize = props.size + setPanes({ panes }) + }) + + onUpdated(() => { + setPanes({ panes }) + }) + + // 获取当前 pane大小 + const getPaneSize = (): number => { + const el = domRef?.value + if (orientation === 'vertical') { + return el.offsetHeight + } else { + return el.offsetWidth + } + } + + const toggleCollapseClass = () => { + const paneHiddenClass = 'devui-splitter-pane-hidden' + nextTick(() => { + const el = domRef.value + if (!props.collapsed) { + removeClass(el, paneHiddenClass) + } else { + addClass(el, paneHiddenClass) + } + + if (props.collapsed && props.shrink) { + removeClass(el, paneHiddenClass) + setStyle(el, { flexBasis: `${props.shrinkWidth}px` }) + } else { + setStyle(el, { flexBasis: initialSize }) + } + }) + } + watch( + () => props.collapsed, + () => { + nextTick(() => { + toggleCollapseClass() + }) + } + ) + + // 收起时用于改变相邻 pane 的 flex-grow 属性来改变非自适应 pane 的 size + const toggleNearPaneFlexGrow = (collapsed) => { + nextTick(() => { + const flexGrowClass = 'devui-splitter-pane-grow' + if (hasClass(domRef.value, flexGrowClass)) { + removeClass(domRef.value, flexGrowClass) + } else if (collapsed) { + addClass(domRef.value, flexGrowClass) + } + }) + } + + // 暴露给外部使用 + expose({ + getPaneSize, + toggleNearPaneFlexGrow, + }) + + return () => { + return ( +
+ {slots.default?.()} +
+ ) + } + }, +}) diff --git a/devui/splitter/src/splitter-store.ts b/devui/splitter/src/splitter-store.ts new file mode 100644 index 0000000000000000000000000000000000000000..1112ea91e7744e0818530775fcef9c9adbb43315 --- /dev/null +++ b/devui/splitter/src/splitter-store.ts @@ -0,0 +1,200 @@ +import SplitterPane from './splitter-pane' +import { reactive, readonly } from 'vue' + +export interface Pane { + getPaneSize: () => number +} + +export interface PaneState { + index: number + initialSize: number + minSize: number + maxSize: number +} + +export interface DragState { + prev: PaneState + next: PaneState +} + +type SplitterPane = typeof SplitterPane & Pane +export interface splitterState { + panes: Array // 所有 pane 对象的一些关键信息 + splitterContainerSize: number +} + +const state: splitterState = reactive({ + panes: [], + splitterContainerSize: 0, +}) + +export function useSplitterStore() { + // 配置 pane 信息,panes 列表,方便后续计算使用 + const setPanes = ({ panes }): void => { + state.panes = panes.map((pane: SplitterPane) => { + pane.getPaneSize = pane?.component?.exposed.getPaneSize + return pane + }) + } + const setSplitter = ({ containerSize }: { containerSize: number; }): void => { + state.splitterContainerSize = containerSize + } + + // 获取 pane,防止没有初始化的时候调用内部方法取值 + const getPane = (index: number): SplitterPane => { + if (!state.panes || index < 0 || index >= state.panes.length) { + throw new Error('no pane can return.') + } + return state.panes[index] + } + + // 按下的时候计算 pane 的 size 信息 + const dragState = (splitbarIndex: number): DragState => { + const prev = getPane(splitbarIndex) + const next = getPane(splitbarIndex + 1) + const total = prev.getPaneSize() + next.getPaneSize() + return { + prev: { + index: splitbarIndex, + initialSize: prev.getPaneSize(), + // 设置有最小值,直接取值,如果没有设置就用两个 pane 总和减去相邻 pane 的最大值,都没设置(NaN)再取0 + minSize: + toPixels(prev.component.props.minSize) || + total - toPixels(next.component.props.maxSize) || + 0, + // 设置最大值,直接取值,如果没有设置就用两个 pane 总和减去相邻 pane 的最小值,都没设置(NaN)再取两个 pane 总和 + maxSize: + toPixels(prev.component.props.maxSize) || + total - toPixels(next.component.props.minSize) || + total, + }, + next: { + index: splitbarIndex + 1, + initialSize: next.getPaneSize(), + minSize: + toPixels(next.component.props.minSize) || + total - toPixels(prev.component.props.maxSize) || + 0, + maxSize: + toPixels(next.component.props.maxSize) || + total - toPixels(prev.component.props.minSize) || + total, + }, + } + } + + // 大小限制函数,(max)小于最小值时取最小值,(min)大于最大值时取最大值 + const clamp = ( + minSize: number, + maxSize: number, + initialSize: number + ): number => { + return Math.min(maxSize, Math.max(minSize, initialSize)) + } + + // resize pane的大小 + const resize = (paneState: PaneState, moveSize: number): void => { + const pane = getPane(paneState.index) + const splitterSize = state.splitterContainerSize + const newSize = clamp( + paneState.minSize, + paneState.maxSize, + paneState.initialSize + moveSize + ) + let size = '' + if (isPercent(pane.component.props.size)) { + size = (100 * newSize) / splitterSize + '%' + } else { + size = newSize + 'px' + } + pane.component.props.size = size + pane.component.emit('sizeChange', size) + } + + // 判断 pane 是否可以调整大小,只要有一边设置了不可调整或者收起,相邻 pane 调整就失效 + const isResizable = (splitBarIndex: number): boolean => { + const prevPane = getPane(splitBarIndex) + const nextPane = getPane(splitBarIndex + 1) + const paneCollapsed = + prevPane?.component?.props?.collapsed || nextPane?.component?.props?.collapsed + return ( + prevPane?.component?.props?.resizable && + nextPane?.component?.props?.resizable && + !paneCollapsed + ) + } + + // 判断分割条是否是固定的,只要有一边不能调整, 就是禁用状态固定 bar + const isStaticBar = (splitBarIndex: number): boolean => { + const prevPane = getPane(splitBarIndex) + const nextPane = getPane(splitBarIndex + 1) + return !( + prevPane?.component?.props?.resizable && nextPane?.component?.props?.resizable + ) + } + + // 判断是不是百分比设置宽度 + const isPercent = (size: string) => { + return /%$/.test(size) + } + + // 计算时把百分比转换为像素 + const toPixels = (size: string): number => { + // 值不满足转换时,result 为 NaN,方便计算最小、最大宽度判断 + let result = parseFloat(size) + if (isPercent(size)) { + result = (state.splitterContainerSize * result) / 100 + } + return result + } + + // 切换 pane 展开,收起 + const tooglePane = ( + paneIndex: number, + nearPaneIndex: number, + lockStatus?: boolean + ): void => { + const pane = getPane(paneIndex) + const nearPane = getPane(nearPaneIndex) + if (pane?.component?.props?.collapsible) { + pane.component.props.collapsed = lockStatus + ? pane?.component?.props?.collapsed + : !pane?.component?.props?.collapsed + nearPane?.component?.exposed?.toggleNearPaneFlexGrow( + pane?.component?.props?.collapsed + ) + pane?.component?.emit('collapsedChange', pane?.component?.props?.collapsed) + } + } + + // 设置 pane 大小 + const setSize = (state: DragState, distance: number): void => { + const prev = getPane(state.prev.index) + const next = getPane(state.next.index) + if (prev.component.props.size && next.component.props.size) { + // 相邻的两个 pane 都指定了 size,需要同时修改 size + resize(state.prev, distance) + resize(state.next, distance) + } else if (next.component.props.size) { + // 只有 next pane指定了 size,直接修改 next pane + resize(state.next, -distance) + } else { + // 最后都没有指定 size,直接修改 pre pane + resize(state.prev, distance) + } + } + + const readonlyState = readonly(state) + + return { + setPanes, + getPane, + isStaticBar, + setSize, + setSplitter, + dragState, + isResizable, + tooglePane, + splitterState: readonlyState, + } +} diff --git a/devui/splitter/src/splitter-types.ts b/devui/splitter/src/splitter-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..73467eb868270c6849f052e010a10ded034c235b --- /dev/null +++ b/devui/splitter/src/splitter-types.ts @@ -0,0 +1,29 @@ +import type { PropType, ExtractPropTypes } from 'vue'; +export type SplitterOrientation = 'vertical' | 'horizontal'; +export type CollapseDirection = 'before' | 'after' | 'both'; + +export const splitterProps = { + /** + * 可选,指定 Splitter 分割方向,可选值'vertical'|'horizontal' + */ + orientation: { + type: String as PropType, + default: 'horizontal', + }, + /** + * 可选,分隔条大小,默认 2px + */ + splitBarSize: { + type: String, + default: '2px', + }, + /** + * 是否显示展开/收缩按钮 + */ + showCollapseButton: { + type: Boolean, + default: true, + }, +} as const; + +export type SplitterProps = ExtractPropTypes; diff --git a/devui/splitter/src/splitter.scss b/devui/splitter/src/splitter.scss new file mode 100644 index 0000000000000000000000000000000000000000..a2f5eb8136132ec4a94d5ab93982429f5e070f68 --- /dev/null +++ b/devui/splitter/src/splitter.scss @@ -0,0 +1,18 @@ +@import '../../style/theme/color'; +@import '../../style/theme/corner'; + +.devui-splitter { + display: flex; + width: 100%; + height: auto; + position: relative; + border-radius: $devui-border-radius; + + &.devui-splitter-horizontal { + flex-direction: row; + } + + &.devui-splitter-vertical { + flex-direction: column; + } +} diff --git a/devui/splitter/src/splitter.tsx b/devui/splitter/src/splitter.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cfa77eaaea10d9af35d12e3a5086ffd0ed1bbaef --- /dev/null +++ b/devui/splitter/src/splitter.tsx @@ -0,0 +1,69 @@ +import { defineComponent, reactive, ref, provide, onMounted } from 'vue' +import { splitterProps, SplitterProps } from './splitter-types' +import DSplitterBar from './splitter-bar' +import { useSplitterStore } from './splitter-store' +import './splitter.scss' + +export default defineComponent({ + name: 'DSplitter', + components: { + DSplitterBar, + }, + props: splitterProps, + emits: [], + setup(props: SplitterProps, ctx) { + const { setPanes, setSplitter } = useSplitterStore() + const state = reactive({ + panes: [], // 内嵌面板 + }) + + state.panes = ctx.slots.DSplitterPane?.() || [] + setPanes({ panes: state.panes }) + + const domRef = ref() + + provide('orientation', props.orientation) + provide('panes', state.panes) + + onMounted(() => { + let containerSize = 0 + if (props.orientation === 'vertical') { + containerSize = domRef.value.clientHeight + } else { + containerSize = domRef.value.clientWidth + } + setSplitter({ containerSize }) + }) + + return () => { + const { splitBarSize, orientation, showCollapseButton } = props + const wrapperClass = ['devui-splitter', `devui-splitter-${orientation}`] + + return ( +
+ {state.panes.map((pane, index) => { + // pane.props = pane.props || reactive({}) + if (pane.props) { + pane.props.order = index * 2 // props 有可能为空,如何处理 + } + return pane + })} + {state.panes + .filter((pane, index, arr) => index !== arr.length - 1) + .map((pane, index) => { + return ( + + ) + })} +
+ ) + } + }, +}) diff --git a/devui/splitter/src/util/d-resize-directive.ts b/devui/splitter/src/util/d-resize-directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..80747166cbd9eca009b4f8edfeb478524609d8d9 --- /dev/null +++ b/devui/splitter/src/util/d-resize-directive.ts @@ -0,0 +1,82 @@ +import type { Directive, DirectiveBinding } from 'vue' + +export class ResizeDirectiveProp { + enableResize = true // 是否允许拖动 + onPressEvent = function (...args: any[]): void { + /** */ + } + onDragEvent = function (...args: any[]): void { + /** */ + } + onReleaseEvent = function (...args: any[]): void { + /** */ + } +} + +let resizeDirectiveProp: ResizeDirectiveProp +const resize: Directive = { + mounted( + el, + { value = new ResizeDirectiveProp() }: DirectiveBinding + ) { + resizeDirectiveProp = value + // 是否允许拖动 + if (value.enableResize) { + bindEvent(el) + } + }, + unmounted( + el, + { value = new ResizeDirectiveProp() }: DirectiveBinding + ) { + if (value.enableResize) { + unbind(el, 'mousedown', onMousedown) + } + }, +} + +function bindEvent(el) { + // 绑定 mousedown 事件 + bind(el, 'mousedown', onMousedown) + // TODO 绑定触屏事件 +} + +function bind(el, event, callback) { + el.addEventListener && el.addEventListener(event, callback) +} + +function unbind(el, event, callback) { + el.removeEventListener && el.removeEventListener(event, callback) +} + +function onMousedown(e) { + bind(document, 'mousemove', onMousemove) + bind(document, 'mouseup', onMouseup) + resizeDirectiveProp.onPressEvent(normalizeEvent(e)) +} + +function onMousemove(e) { + resizeDirectiveProp.onDragEvent(normalizeEvent(e)) +} + +function onMouseup(e) { + unbind(document, 'mousemove', onMousemove) + unbind(document, 'mouseup', onMouseup) + resizeDirectiveProp.onReleaseEvent(normalizeEvent(e)) +} + +// 返回常用位置信息 +function normalizeEvent(e) { + return { + pageX: e.pageX, + pageY: e.pageY, + clientX: e.clientX, + clientY: e.clientY, + offsetX: e.offsetX, + offsetY: e.offsetY, + type: e.type, + originalEvent: e, + } +} + +export default resize diff --git a/devui/sticky/index.ts b/devui/sticky/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..71c064d776c9f6780a4a8ef2192107216d640be5 --- /dev/null +++ b/devui/sticky/index.ts @@ -0,0 +1,18 @@ +import type { App } from 'vue' +import Sticky from './src/sticky' + +Sticky.install = function(app: App): void { + app.component(Sticky.name, Sticky) +} + +export { Sticky } + +export default { + title: 'Sticky 便贴', + category: '通用', + status: undefined, // TODO: 组件若开发完成则填入"已完成",并删除该注释 + install(app: App): void { + + app.use(Sticky as any) + } +} diff --git a/devui/sticky/src/sticky.tsx b/devui/sticky/src/sticky.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bdd0fd45d14fe01fab498353cd78aea0520ccf7a --- /dev/null +++ b/devui/sticky/src/sticky.tsx @@ -0,0 +1,254 @@ + +import { defineComponent, onMounted, reactive, ref, watch } from 'vue' + +export default defineComponent({ + name: 'DSticky', + props: { + zIndex: { + type: Number, + }, + container: { + type: Element, + default: '', + }, + view: { + type: Object, + default: ()=>{return {top:0,bottom:0}}, + }, + scrollTarget: { + type: Element, + default: '', + }, + }, + emits: ['statusChange'], + setup(props, ctx) { + const { slots } = ctx + let container: Element + let scrollTarget: Element | Window + + let scrollTimer: any + let scrollPreStart: number | null + + const THROTTLE_DELAY = 16; + const THROTTLE_TRIGGER = 100; + + let parentNode: Element + let containerLeft = 0 + + const state = reactive({ + status: 'normal' + }) + + watch( + () => props.zIndex, + () => { + init() + } + ); + watch( + () => props.container, + () => { + init() + } + ); + watch( + () => props.scrollTarget, + () => { + init() + } + ); + watch( + () => state.status, + () => { + ctx.emit('statusChange', state.status) + }, + { immediate: true } + ); + + const init = () => { + parentNode = stickyRef.value.parentElement + if (!props.container) { + container = parentNode; + } else { + container = props.container + } + + stickyRef.value.style.zIndex = props.zIndex + + scrollTarget = props.scrollTarget || window; + scrollTarget.addEventListener('scroll',throttle); + + initScrollStatus(scrollTarget); + } + + // 初始化,判断位置,如果有滚用动则用handler处理 + const initScrollStatus = ( target: any)=>{ + const scrollTargets = target === window ? + [document.documentElement, document.body] : [target]; + let flag = false; + scrollTargets.forEach((scrollTarget) => { + if (scrollTarget.scrollTop && scrollTarget.scrollTop > 0) { + flag = true; + } + }); + if (flag) { + setTimeout(scrollHandler); + } + } + + + const statusProcess = (status: any) => { + const wrapper = stickyRef.value|| document.createElement('div') + switch (status) { + case 'normal': + wrapper.style.top = 'auto'; + wrapper.style.left = 'auto'; + wrapper.style.position = 'static'; + break; + case 'follow': + const scrollTargetElement:any = scrollTarget + const viewOffset = scrollTarget && scrollTarget !== window ? + scrollTargetElement.getBoundingClientRect().top : 0; + wrapper.style.top = +viewOffset + ((props.view && props.view.top) || 0) + 'px'; + wrapper.style.left = wrapper.getBoundingClientRect().left + 'px'; + wrapper.style.position = 'fixed'; + break; + case 'stay': + wrapper.style.top = calculateRelativePosition(wrapper, parentNode, 'top') + 'px'; + wrapper.style.left = 'auto'; + wrapper.style.position = 'relative'; + break; + case 'remain': + if (wrapper.style.position !== 'fixed' && wrapper.style.position !== 'absolute') { + wrapper.style.top = calculateRelativePosition(wrapper, parentNode, 'top') + 'px'; + wrapper.style.left = 'auto'; + wrapper.style.position = 'absolute'; + } + wrapper.style.top = + calculateRemainPosition(wrapper, parentNode, container) + 'px'; + wrapper.style.left = calculateRelativePosition(wrapper, parentNode, 'left') + 'px'; + wrapper.style.position = 'relative'; + break; + default: + break; + } + } + + const throttle = () => { + const fn = scrollAndResizeHock; + const time = Date.now(); + if (scrollTimer) { + clearTimeout(scrollTimer); + } + if (!scrollPreStart) { + scrollPreStart = time; + } + if (time - scrollPreStart > THROTTLE_TRIGGER) { + fn(); + scrollPreStart = null; + scrollTimer = null; + } else { + scrollTimer = setTimeout(() => { + fn(); + scrollPreStart = null; + scrollTimer = null; + }, THROTTLE_DELAY); + } + } + + const scrollAndResizeHock = () => { + if (container.getBoundingClientRect().left - (containerLeft || 0) !== 0) { + state.status = 'stay'; + containerLeft = container.getBoundingClientRect().left; + } else { + scrollHandler(); + } + } + + const scrollHandler = () => { + const scrollTargetElement:any = scrollTarget + const wrapper = stickyRef.value || document.createElement('div') + const viewOffsetTop = scrollTarget && scrollTarget !== window ? + scrollTargetElement.getBoundingClientRect().top : 0; + const computedStyle = window.getComputedStyle(container); + if (parentNode.getBoundingClientRect().top - viewOffsetTop > ((props.view && props.view.top) || 0)) { + state.status = 'normal'; + statusProcess(state.status); + } else if ( + container.getBoundingClientRect().top + + parseInt(computedStyle.paddingTop, 10) + + parseInt(computedStyle.borderTopWidth, 10) - + viewOffsetTop >= + ((props.view && props.view.top) || 0) + ) { + state.status = 'normal'; + statusProcess(state.status); + } else if ( + container.getBoundingClientRect().bottom - + parseInt(computedStyle.paddingBottom, 10) - + parseInt(computedStyle.borderBottomWidth, 10) < + viewOffsetTop + + ((props.view && props.view.top) || 0) + + wrapper.getBoundingClientRect().height + + ((props.view && props.view.bottom) || 0) + ) { + state.status = 'remain'; + statusProcess(state.status); + } else if ( + container.getBoundingClientRect().top + parseInt(computedStyle.paddingTop, 10) - viewOffsetTop < + ((props.view && props.view.top) || 0) + ) { + state.status = 'follow'; + statusProcess(state.status); + } + } + + + const calculateRelativePosition =(element:any, relativeElement:any, direction:'left' | 'top') => { + const key = { + left: ['left', 'Left'], + top: ['top', 'Top'], + }; + if (window && window.getComputedStyle) { + const computedStyle = window.getComputedStyle(relativeElement); + return ( + element.getBoundingClientRect()[key[direction][0]] - + relativeElement.getBoundingClientRect()[key[direction][0]] - + parseInt(computedStyle[ direction === 'left' ? 'paddingLeft' : 'paddingTop' ], 10) - + parseInt(computedStyle[ direction === 'left' ? 'borderLeftWidth' : 'borderTopWidth' ], 10) + ); + } + } + + const calculateRemainPosition = (element: any, relativeElement: any, container: any) => { + if (window && window.getComputedStyle) { + const computedStyle = window.getComputedStyle(container); + const result = + container.getBoundingClientRect().height - + element.getBoundingClientRect().height + + container.getBoundingClientRect().top - + relativeElement.getBoundingClientRect().top - + parseInt(computedStyle['paddingTop'], 10) - + parseInt(computedStyle['borderTopWidth'], 10) - + parseInt(computedStyle['paddingBottom'], 10) - + parseInt(computedStyle['borderBottomWidth'], 10); + return result < 0 ? 0 : result; + } + } + + onMounted(() => { + init() + }) + + const stickyRef = ref() + + + return () => { + return ( +
+ { slots.default ? slots.default() : '' } +
+ ) + } + } +}) diff --git a/devui/toast/src/toast.tsx b/devui/toast/src/toast.tsx index 5f925c361d101e215315cfc73b9511178e522edf..a056b7c4d947c05fedff5ca79f97ba2df458692d 100644 --- a/devui/toast/src/toast.tsx +++ b/devui/toast/src/toast.tsx @@ -1,3 +1,4 @@ + import './toast.scss' import { computed, defineComponent, nextTick, onUnmounted, ref, watch } from 'vue' diff --git a/devui/tooltip/index.ts b/devui/tooltip/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b5b67564f8f0347b69a4785e20508267d36946b6 --- /dev/null +++ b/devui/tooltip/index.ts @@ -0,0 +1,16 @@ +import type { App } from 'vue' +import Tooltip from './src/tooltip' + +Tooltip.install = function(app: App) { + app.component(Tooltip.name, Tooltip) +} + +export { Tooltip } + +export default { + title: 'Tooltip tooltip提示', + category: '反馈', + install(app: App): void { + app.use(Tooltip as any) + } +} diff --git a/devui/tooltip/src/tooltip-types.ts b/devui/tooltip/src/tooltip-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..6c9f8d35930b30b3bc68d2e158d0d660e7eae712 --- /dev/null +++ b/devui/tooltip/src/tooltip-types.ts @@ -0,0 +1,30 @@ +import type { ExtractPropTypes } from 'vue' + +export type TTooltip = 'top' | 'right' | 'bottom' | 'left'; + +export const tooltipProps = { + /* test: { + type: Object as PropType<{ xxx: xxx }> + } */ + position: { + type: String, + default: 'top' + }, + showAnimation: { + type: Boolean, + default: true + }, + content: { + type: String + }, + mouseLeaveDelay: { + type: String, + default: '150' + }, + mouseEnterDelay: { + type: String, + default: '100' + } +} as const + +export type TooltipProps = ExtractPropTypes diff --git a/devui/tooltip/src/tooltip.scss b/devui/tooltip/src/tooltip.scss new file mode 100644 index 0000000000000000000000000000000000000000..d123bbe24214df918249b2dc1d90f3bbce82ca17 --- /dev/null +++ b/devui/tooltip/src/tooltip.scss @@ -0,0 +1,23 @@ +.d-tooltip { + box-sizing: border-box; + // position: relative; + .tooltip { + box-sizing: border-box; + position: absolute; + width: fit-content; + transition: all 0.5s; + .arrow { + width: 0; + height: 0; + position: absolute; + } + .tooltipcontent { + box-sizing: border-box; + padding: 10px; + margin-left: 10px; + width: fit-content; + background-color: rgb(51, 51, 51); + color: #fff; + } + } +} diff --git a/devui/tooltip/src/tooltip.tsx b/devui/tooltip/src/tooltip.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0f4231cd656adcaedcfde203aff428ee875c6a6e --- /dev/null +++ b/devui/tooltip/src/tooltip.tsx @@ -0,0 +1,165 @@ +import './tooltip.scss' +import { defineComponent, reactive, ref, watch, onMounted, getCurrentInstance, onBeforeUnmount, renderSlot, useSlots} from 'vue' +import { tooltipProps } from './tooltip-types' +import EventListener from '../utils/EventListener' + +/** + * 使用: + * + * + */ +export default defineComponent({ + name: 'DTooltip', + props: tooltipProps, + setup(props, ctx){ + let position = reactive({ + left: 0, + top: 0 + }) + let show = ref(false) + + // slotElement元素的ref + let slotElement = ref(null) + // tooltip元素的引用 + let tooltip = ref(null) + // arrow元素的引用 + let arrow = ref(null) + // tooltipcontent的引用 + let tooltipcontent = ref(null) + + let enterEvent + let leaveEvent + + const arrowStyle = (attr, value)=>{ + arrow.value.style[attr] = value + } + + // 延迟显示 + const delayShowTrue = function (fn, delay=props.mouseEnterDelay){ + let start + if (parseInt(delay)>=0){ + return function (){ + if (start){ + clearTimeout(start) + } + start = setTimeout(fn, parseInt(delay)) + } + } else{ + console.error('the value of delay is bigger than 0 and the type of delay must be string!') + return + } + } + // 延迟消失 + const delayShowFalse = function (fn, delay=props.mouseLeaveDelay){ + if (show.value && parseInt(delay) >= 0){ + setTimeout(fn, parseInt(delay)) + } + } + + onMounted(()=>{ + // 组件初始化不渲染tooltip + if (!show.value){ + tooltip.value.style.opacity = '0' + } + + // 注册鼠标引入事件 + /*enterEvent = EventListener.listen(slotElement.value.children[0], 'mouseenter', function (){ + show.value = true + })*/ + enterEvent = EventListener.listen(slotElement.value.children[0], 'mouseenter', delayShowTrue(function (){ + show.value = true + }, props.mouseEnterDelay)) + + // 注册鼠标移除事件 + leaveEvent = EventListener.listen(slotElement.value.children[0], 'mouseleave', function (){ + // show.value = false + setTimeout(function (){ + show.value = false; + }, props.mouseLeaveDelay) + }) + }) + + watch(show, function (newValue, oldValue){ + if (newValue){ + // 鼠标悬浮为true,显示提示框 + tooltip.value.style.opacity = '1' + tooltip.value.style.zIndex = '999' + arrow.value.style.border = '10px solid transparent' + // 具体的判定规则 + switch (props.position){ + case 'top': + // 设置 tooltip 内容的样式 + position.left = slotElement.value.children[0].offsetLeft - tooltip.value.offsetWidth / 2 + slotElement.value.children[0].offsetWidth / 2 + position.top = slotElement.value.children[0].offsetTop - 10 - tooltipcontent.value.offsetHeight + // 设置箭头的样式 + arrowStyle('borderTop', '10px solid cornflowerblue') + arrow.value.style.top = `${tooltipcontent.value.offsetHeight}px` + arrow.value.style.left = `${tooltipcontent.value.offsetWidth/2 - 5}px` + break + + case 'right': + // 设置tooltip 内容的样式 + position.left = slotElement.value.children[0].offsetLeft + slotElement.value.children[0].offsetWidth + position.top = slotElement.value.children[0].offsetTop + slotElement.value.children[0].offsetHeight/2 - tooltipcontent.value.offsetHeight/2 + // 设置箭头的样式 + arrowStyle('borderRight', '10px solid cornflowerblue') + arrow.value.style.top = `${tooltipcontent.value.offsetHeight/2 - 10}px` + arrow.value.style.left = '-10px' + break + + case 'bottom': + // 设置tooltip的样式 + position.top = slotElement.value.children[0].offsetHeight + slotElement.value.children[0].offsetTop + 10 + position.left = slotElement.value.children[0].offsetLeft + slotElement.value.children[0].offsetWidth/2 - tooltipcontent.value.offsetWidth/2 + // 设置arrow.value的样式 + arrowStyle('borderBottom', '10px solid cornflowerblue') + arrow.value.style.top = '-20px' + arrow.value.style.left = `${tooltipcontent.value.offsetWidth/2 - 10}px` + break + + case 'left': + position.top = slotElement.value.children[0].offsetTop + slotElement.value.children[0].offsetHeight/2 - tooltipcontent.value.offsetHeight/2 + position.left = slotElement.value.children[0].offsetLeft - 20 - tooltipcontent.value.offsetWidth + // 设置arrow.value的样式 + arrowStyle('borderLeft', '10px solid cornflowerblue') + arrow.value.style.left = `${tooltipcontent.value.offsetWidth + 10}px` + arrow.value.style.top = `${tooltipcontent.value.offsetHeight/2 - 10}px` + break + + default: + console.error('The attribute position value is wrong, the value is one of top、right、left、bottom') + break + } + tooltip.value.style.top = position.top + 'px' + tooltip.value.style.left = position.left + 'px' + } else { + position.top = 0 + position.left = 0 + // 鼠标移走为false,隐藏提示框 + tooltip.value.style.opacity = '0' + } + }) + + onBeforeUnmount (()=>{ + enterEvent.remove() + leaveEvent.remove() + }) + + return ()=>{ + const defaultSlot = renderSlot(useSlots(), 'default') + return ( +
+
+ {defaultSlot} +
+
{/** tooltip 提示框 */} +
{/** tooltip 提示框箭头 */} +
{/** tooltip提示的内容 */} + {props.content} +
+
+
+ ) + } + } +}) \ No newline at end of file diff --git a/devui/tooltip/utils/EventListener.js b/devui/tooltip/utils/EventListener.js new file mode 100644 index 0000000000000000000000000000000000000000..73f6f68c535deb2a2001bca79a2c0916460fd1f2 --- /dev/null +++ b/devui/tooltip/utils/EventListener.js @@ -0,0 +1,21 @@ +const EventListener = { + listen: function (target, eventType, callback) { + if (target.addEventListener){ + target.addEventListener(eventType, callback, false); + return { + remove (){ + target.removeEventListener(target, callback, false); + } + } + } else { + target.attchEvent(eventType, callback); + return { + remove (){ + target.detachEvent(eventType, callback); + } + }; + } + } +}; + +export default EventListener; \ No newline at end of file diff --git a/devui/transfer/common/use-transfer-base.ts b/devui/transfer/common/use-transfer-base.ts new file mode 100644 index 0000000000000000000000000000000000000000..ff58c15297aea7accbffc91caa9a348455be3817 --- /dev/null +++ b/devui/transfer/common/use-transfer-base.ts @@ -0,0 +1,128 @@ +import { computed, ExtractPropTypes, PropType, ComputedRef } from 'vue' +import { IItem, TState, TResult } from '../types' +import { TransferProps } from './use-transfer' + +export type TransferOperationProps = ExtractPropTypes + +export const transferBaseProps = { + sourceOption: { + type: Array as () => IItem[], + default(): Array { + return [] + } + }, + targetOption: { + type: Array as () => IItem[], + default(): Array { + return [] + } + }, + type: { + type: String, + default: (): string => 'source' + }, + title: { + type: String, + default: (): string => 'Source' + }, + search: { + type: Boolean, + default: (): boolean => false + }, + allChecked: { + type: Boolean, + default: (): boolean => false + }, + query: { + type: String, + default: (): string => '' + }, + alltargetState: { + type: Boolean, + default: (): boolean => false + }, + checkedNum: { + type: Number, + default: (): number => 0 + }, + checkedValues: { + type: Array, + default: (): string[] => [] + }, + allCount: { + type: Number, + default: (): number => 0 + }, + scopedSlots: { + type: Object + }, + onChangeAllSource: { + type: Function as unknown as () => ((val: boolean) => void) + }, + onChangeQuery: { + type: Function as PropType<(val: string) => void> + }, + onUpdateCheckeds: { + type: Function as PropType<(val: string[]) => void> + } +} + +export type TransferBaseProps = ExtractPropTypes + +export const transferOperationProps = { + sourceDisabled: { + type: Boolean, + default: (): boolean => true + }, + targetDisabled: { + type: Boolean, + default: (): boolean => true + }, + onUpdateSourceData: { + type: Function as unknown as () => (() => void) + }, + onUpdateTargetData: { + type: Function as unknown as () => (() => void) + } +} + +const getFilterData = (props, type: string): TResult => { + const newModel: string[] = []; + const data: IItem[] = type === 'source' ? props.sourceOption : props.targetOption + const resultData: IItem[] = data.map((item: IItem) => { + const checked = props.modelValue.some(cur => cur === item.value) + checked && newModel.push(item.value) + return item + }) + return { + model: newModel, + data: resultData + } +} + + + +export const initState = (props: TransferProps, type: string): TState => { + const initModel: TResult = getFilterData(props, type); + const state: TState = { + data: initModel.data, + allChecked: false, + disabled: false, + checkedNum: initModel.model.length, + query: '', + checkedValues: initModel.model, + filterData: initModel.data + } + return state +} + +export const TransferBaseClass = (props: TransferOperationProps): ComputedRef => { + return computed(() => { + return `devui-transfer-panel devui-transfer-${props.type}` + }) +} + +export const Query = ((props: TransferOperationProps): ComputedRef => { + return computed(() => props.query) +}) + diff --git a/devui/transfer/common/use-transfer-operation.ts b/devui/transfer/common/use-transfer-operation.ts new file mode 100644 index 0000000000000000000000000000000000000000..c4eb000f664dd5542a18903ffc0972bc8a2c8d5c --- /dev/null +++ b/devui/transfer/common/use-transfer-operation.ts @@ -0,0 +1,22 @@ + + +export const transferOperationProps = { + sourceDisabled: { + type: Boolean, + default: (): boolean => true + }, + targetDisabled: { + type: Boolean, + default: (): boolean => true + }, + disabled: { + type: Boolean, + default: (): boolean => false + }, + onUpdateSourceData: { + type: Function as unknown as () => (() => void) + }, + onUpdateTargetData: { + type: Function as unknown as () => (() => void) + } +} diff --git a/devui/transfer/common/use-transfer.ts b/devui/transfer/common/use-transfer.ts new file mode 100644 index 0000000000000000000000000000000000000000..6eaa39f3f619e8ed63c1dc84d1fcff7ce00a7a71 --- /dev/null +++ b/devui/transfer/common/use-transfer.ts @@ -0,0 +1,58 @@ +import { ExtractPropTypes, PropType } from 'vue' +import { IItem, ITitles, IModel } from '../types' + +export const transferProps = { + sourceOption: { + type: Array as () => IItem[], + require: true, + default(): IItem[] { + return [] + } + }, + targetOption: { + type: Array as () => IItem[], + require: true, + default(): IItem[] { + return [] + } + }, + titles: { + type: Array as PropType, + default: () => (): ITitles[] => ['Source', 'Target'] + }, + modelValue: { + type: Array as PropType, + default: () => (): IModel[] => [], + }, + height: { + type: String, + default: '320px' + }, + isSearch: { + type: Boolean, + default: false + }, + isSourceDroppable: { + type: Boolean, + default: false + }, + isTargetDroppable: { + type: Boolean, + default: false + }, + disabled: { + type: Boolean, + default: false + }, + showOptionTitle: { + type: Boolean, + default: false + }, + slots: { + type: Object + } +} + +export type TransferProps = ExtractPropTypes; + + diff --git a/devui/transfer/index.ts b/devui/transfer/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..dfc390d671d49e450f7e5d9a0cc6bb122b980b1c --- /dev/null +++ b/devui/transfer/index.ts @@ -0,0 +1,16 @@ +import type { App } from 'vue' +import Transfer from './src/transfer' + +Transfer.install = function (app: App) { + app.component(Transfer.name, Transfer) +} + +export { Transfer } + +export default { + title: 'Transfer 穿梭框', + category: '数据录入', + install(app: App): void { + app.use(Transfer as any) + } +} diff --git a/devui/transfer/src/transfer-base.tsx b/devui/transfer/src/transfer-base.tsx new file mode 100644 index 0000000000000000000000000000000000000000..be6c197ac0c44ca783318240aeabb718e39212a5 --- /dev/null +++ b/devui/transfer/src/transfer-base.tsx @@ -0,0 +1,96 @@ +import { defineComponent, computed } from 'vue' +import { transferBaseProps, TransferBaseClass } from '../common/use-transfer-base' +import { TransferBaseProps } from '../common/use-transfer-base' +import DCheckbox from '../../checkbox/src/checkbox' +import DCheckboxGroup from '../../checkbox/src/checkbox-group' +import DSearch from '../../search/src/search' +export default defineComponent({ + name: 'DTransferBase', + components: { + DSearch, + DCheckboxGroup, + DCheckbox + }, + props: transferBaseProps, + setup(props: TransferBaseProps, ctx) { + /** data start **/ + const modelValues = computed(() => props.checkedValues) + const searchQuery = computed(() => props.query) + const baseClass = TransferBaseClass(props) + /** data end **/ + + /** watch start **/ + /** watch start **/ + + /** methods start **/ + const updateSearchQuery = (val: string): void => ctx.emit('changeQuery', val) + /** methods start **/ + + return { + baseClass, + searchQuery, + modelValues, + updateSearchQuery + } + }, + render() { + const { + title, + baseClass, + checkedNum, + allChecked, + sourceOption, + allCount, + updateSearchQuery, + search, + searchQuery, + modelValues + } = this + + return ( +
+ { + this.$slots.Header ? this.$slots.Header() : (
+
+ this.$emit('changeAllSource', value)}> + {title} + +
+
{checkedNum}/{allCount}
+
) + } + { + this.$slots.Body ? this.$slots.Body() :
+ {search && } +
+ { + sourceOption.length ? this.$emit('updateCheckeds', values)}> + { + sourceOption.map((item, idx) => { + return + + }) + } + : +
无数据
+ } +
+
+ } +
+ ) + } +}) \ No newline at end of file diff --git a/devui/transfer/src/transfer-operation.tsx b/devui/transfer/src/transfer-operation.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9718e3ce08c877200a73d03528636787f83c3d10 --- /dev/null +++ b/devui/transfer/src/transfer-operation.tsx @@ -0,0 +1,25 @@ +import { defineComponent, computed } from 'vue'; +import DButton from '../../button/src/button' +import { transferOperationProps } from '../common/use-transfer-operation' + +export default defineComponent({ + name: 'DTransferOperation', + components: { + DButton + }, + props: transferOperationProps, + setup(props, ctx) { + return () => { + return
+
+ ctx.emit('updateSourceData')}> + ctx.emit('updateTargetData')}> +
+
+ } + }, + data() { + return {} + } +}) \ No newline at end of file diff --git a/devui/transfer/src/transfer.scss b/devui/transfer/src/transfer.scss new file mode 100644 index 0000000000000000000000000000000000000000..c24d4f81c134083e098d55a1071b36bba77be19b --- /dev/null +++ b/devui/transfer/src/transfer.scss @@ -0,0 +1,152 @@ +// @import '../../style/theme/color'; +@import '../../style/theme/color'; + +$devui-transfer-border-color: #adb0b8; +$devui-transfer-border-radius: 2px; +$devui-transfer-header-height: 40px; +$devui-transfer-header-border-line-color:#dfe1e6; +$devui-transfer-body-search-height: 42px; +$devui-transfer-body-list-item-height: 36px; + +.devui-transfer { + display: flex; + + &-panel { + width: 300px; + border: 1px solid $devui-transfer-border-color; + border-radius: $devui-transfer-border-radius; + + &-header { + display: flex; + justify-content: space-between; + height: $devui-transfer-header-height; + line-height: $devui-transfer-header-height; + border-bottom: 1px solid$devui-transfer-header-border-line-color; + + &-allChecked { + display: flex; + margin-left: 20px; + } + + &-num { + margin-right: 12px; + } + } + + &-body { + height: 100%; + + &-search { + height: $devui-transfer-body-search-height; + display: flex; + justify-content: center; + width: calc(100% - 40px); + align-items: center; + margin: 0 auto; + + .devui-search, + .devui-input__wrap { + width: 100%; + } + } + + &-list { + overflow: auto; + height: calc(100% - #{$devui-transfer-header-height} - #{$devui-transfer-body-search-height}); + width: 100%; + + &-item { + margin: 0 20px; + height: $devui-transfer-body-list-item-height; + line-height: $devui-transfer-body-list-item-height; + } + + &-empty { + height: 100%; + // width: 100%; + display: flex; + justify-content: center; + align-items: center; + color: $devui-disabled-text; + } + } + } + + &-operation { + width: 10%; + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + + &-group { + width: 36px; + + &-left, + &-right { + width: 36px; + height: 36px; + color: $devui-light-text; + background: $devui-primary; + border-radius: 50%; + border: none; + cursor: pointer; + padding: 0; + min-width: 36px !important; + } + + &-right { + margin-top: 12px; + } + + &-left:hover, + &-left span:hover, + &-right:hover, + &-right span:hover { + background: $devui-primary-hover; + } + + &-left:active, + &-left span:active, + &-right:active, + &-right span:active { + background: $devui-primary-active; + } + + &-left:disabled, + &-left span:disabled, + &-right:disabled, + &-right span:disabled { + color: $devui-disabled-text; + background: $devui-disabled-bg; + cursor: not-allowed; + } + } + } + } + + .custom { + &-body { + &-header, + &-list { + display: flex; + justify-content: space-around; + line-height: $devui-transfer-body-list-item-height; + flex-wrap: wrap; + margin: 0; + } + + &-header { + border-bottom: 1px solid $devui-transfer-border-color; + width: 100%; + } + + &-header > div, + &-list > div { + width: 25%; + display: flex; + justify-content: center; + } + } + } +} diff --git a/devui/transfer/src/transfer.tsx b/devui/transfer/src/transfer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..dac3cc92473f0734f4a03d377052833dd4a62808 --- /dev/null +++ b/devui/transfer/src/transfer.tsx @@ -0,0 +1,160 @@ +import { defineComponent, reactive, watch, ref } from 'vue' +import { TState } from '../types' +import DTransferBase from './transfer-base' +import DTransferOperation from './transfer-operation' +import { initState } from '../common/use-transfer-base' +import { transferProps, TransferProps } from '../common/use-transfer' +import DCheckbox from '../..//checkbox/src/checkbox' +import './transfer.scss' + +export default defineComponent({ + name: 'DTransfer', + components: { + DTransferBase, + DTransferOperation, + DCheckbox + }, + props: transferProps, + setup(props: TransferProps) { + /** data start **/ + const leftOptions = reactive(initState(props, 'source')) + const rightOptions = reactive(initState(props, 'target')) + const origin = ref(null); + /** data end **/ + + /** watch start **/ + watch( + () => leftOptions.query, + (nVal: string): void => { + searchFilterData(leftOptions) + } + ) + + watch( + () => leftOptions.checkedValues, + (values: string[]): void => { + leftOptions.checkedNum = values.length + setAllCheckedState(leftOptions, values) + }, + { + deep: true + } + ) + + watch( + () => rightOptions.query, + (nVal: string): void => { + searchFilterData(rightOptions) + }, + ) + + watch( + () => rightOptions.checkedValues, + (values: string[]): void => { + rightOptions.checkedNum = values.length; + setAllCheckedState(rightOptions, values) + }, + { + deep: true + } + ) + + /** watch end **/ + + /** methods start **/ + const setAllCheckedState = (source: TState, value: string[]): void => { + if (origin.value === 'click') { + source.allChecked = false + } else { + source.allChecked = value.length === source.data.filter(item => !item.disabled).length ? true : false + } + } + + const updateFilterData = (source: TState, target: TState): void => { + const newData = [] + source.data = source.data.filter(item => { + const hasInclues = source.checkedValues.includes(item.value) + hasInclues && newData.push(item) + return !hasInclues + }) + target.data = target.data.concat(newData) + source.checkedValues = [] + target.disabled = !target.disabled + searchFilterData(source) + searchFilterData(target) + setOrigin('click') + } + const changeAllSource = (source: TState, value: boolean): void => { + if (source.filterData.every(item => item.disabled)) return + source.allChecked = value + if (value) { + source.checkedValues = source.filterData.filter(item => !item.disabled) + .map(item => item.value) + } else { + source.checkedValues = [] + } + setOrigin('change') + } + const updateLeftCheckeds = (values: string[]): void => { + leftOptions.checkedValues = values + setOrigin('change') + } + const updateRightCheckeds = (values: string[]): void => { + rightOptions.checkedValues = values + setOrigin('change') + } + const searchFilterData = (source: TState): void => { + source.filterData = source.data.filter(item => item.key.indexOf(source.query) !== -1) + } + const setOrigin = (value: string): void => { + origin.value = value + } + /** methods end **/ + + return () => { + return
+ changeAllSource(leftOptions, value)} + onUpdateCheckeds={updateLeftCheckeds} + onChangeQuery={(value) => leftOptions.query = value} + /> + 0 ? false : true} + targetDisabled={leftOptions.checkedNum > 0 ? false : true} + onUpdateSourceData={() => { updateFilterData(rightOptions, leftOptions) }} + onUpdateTargetData={() => { updateFilterData(leftOptions, rightOptions) }} + /> + changeAllSource(rightOptions, value)} + onUpdateCheckeds={updateRightCheckeds} + onChangeQuery={(value) => rightOptions.query = value} + /> +
+ } + } +}) \ No newline at end of file diff --git a/devui/transfer/types.ts b/devui/transfer/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..c527d74e6661facaf96f8b1c9ec507305194df46 --- /dev/null +++ b/devui/transfer/types.ts @@ -0,0 +1,54 @@ + +export interface IItem { + key: string + value: string + disabled: boolean +} + +export interface ITitles { + [index: number]: string +} + +export interface IModel { + [index: number]: string | number +} + +export interface TState { + data: IItem[] + allChecked: boolean + checkedNum: number + query: string + checkedValues: string[] + filterData: IItem[] + disabled: boolean +} + +export interface TResult { + model: string[] + data: IItem[] +} + + + +// import { ComputedRef } from 'vue' +// export type TItem = { +// key: string +// value: string +// disabled: boolean +// checked?: boolean +// } + +// export type TState = { +// data: TItem[] +// allChecked: boolean +// // disabled: boolean // ComputedRef// +// checkedNum: number +// query: string +// checkedValues: string[] +// filterData: TItem[] +// } + +// export type TResult = { +// model: string[] +// data: TItem[] +// } diff --git a/devui/upload/src/multiple-upload.tsx b/devui/upload/src/multiple-upload.tsx index da3a23157352bf820610386afb6b0361ca6b9015..0928fe4106b23878bb278236f0a9062e1de07173 100644 --- a/devui/upload/src/multiple-upload.tsx +++ b/devui/upload/src/multiple-upload.tsx @@ -1,8 +1,4 @@ -import './upload.scss' - import { defineComponent, toRefs, ref } from 'vue' -import { Observable } from 'rxjs' -import { last, map, debounceTime } from 'rxjs/operators' import { ToastService } from '../../toast' import { UploadStatus, multiUploadProps } from './upload-types' import { useSelectFiles } from './use-select-files' @@ -15,6 +11,7 @@ import { getExistSameNameFilesMsg, } from './i18n-upload' import { FileUploader } from './file-uploader' +import './upload.scss' export default defineComponent({ name: 'DMultipleUpload', @@ -98,38 +95,32 @@ export default defineComponent({ } } - const _dealFiles = (observale) => { + const _dealFiles = (promise: Promise) => { resetSameNameFiles() - observale - .pipe( - map((file) => { + promise + .then((files) => { + files.forEach((file) => { addFile(file, uploadOptions.value) - return file - }), - debounceTime(100) - ) - .subscribe( - () => { - checkValid() - const sameNameFiles = getSameNameFiles() - if (uploadOptions.value.checkSameName && sameNameFiles.length) { - alertMsg(getExistSameNameFilesMsg(sameNameFiles)) - } - // TODO: onChange事件 - const selectedFiles = fileUploaders.value - .filter( - (fileUploader) => fileUploader.status === UploadStatus.preLoad - ) - .map((fileUploader) => fileUploader.file) - ctx.emit('fileSelect', selectedFiles) - if (autoUpload.value) { - upload() - } - }, - (error: Error) => { - alertMsg(error.message) + // debounceTime(100) + }) + checkValid() + const sameNameFiles = getSameNameFiles() + if (uploadOptions.value.checkSameName && sameNameFiles.length) { + alertMsg(getExistSameNameFilesMsg(sameNameFiles)) } - ) + const selectedFiles = fileUploaders.value + .filter( + (fileUploader) => fileUploader.status === UploadStatus.preLoad + ) + .map((fileUploader) => fileUploader.file) + ctx.emit('fileSelect', selectedFiles) + if (autoUpload.value) { + upload() + } + }) + .catch((error: Error) => { + alertMsg(error.message) + }) } const handleClick = () => { @@ -141,9 +132,7 @@ export default defineComponent({ const onFileDrop = (files: File[]) => { isDropOVer.value = false - _dealFiles( - triggerDropFiles(fileOptions.value, uploadOptions.value, files) - ) + _dealFiles(triggerDropFiles(files)) ctx.emit('fileDrop', files) } const onFileOver = (event: boolean) => { @@ -169,8 +158,6 @@ export default defineComponent({ if (typeof result !== 'undefined') { if (result.then) { uploadResult = result - } else if (result.subscribe) { - uploadResult = (result as Observable).toPromise() } else { uploadResult = Promise.resolve(result) } @@ -190,17 +177,17 @@ export default defineComponent({ const uploadObservable = oneTimeUpload.value ? _oneTimeUpload() : upload(fileUploader) - uploadObservable.pipe(last()).subscribe( - (results: Array<{ file: File; response: any; }>) => { + + uploadObservable + .then((results: Array<{ file: File; response: any; }>) => { ctx.emit('successEvent', results) const newFiles = results.map((result) => result.file) const newUploadedFiles = [...newFiles, ...uploadedFiles.value] ctx.emit('update:uploadedFiles', newUploadedFiles) - }, - (error) => { + }) + .catch((error) => { ctx.emit('errorEvent', error) - } - ) + }) }) } @@ -301,82 +288,78 @@ export default defineComponent({ v-file-drop={{ enableDrop, isSingle: false, onFileDrop, onFileOver }} style={`border: ${isDropOVer ? '1px solid #15bf15' : '0'}`} > -
- {fileUploaders.length === 0 && ( -
- {placeholderText} -
- )} - {fileUploaders.length > 0 && ( -
    - {fileUploaders.map((fileUploader, index) => ( -
  • - {this.$slots.default()}
+ ) : ( +
+ {fileUploaders.length === 0 && ( +
+ {placeholderText} +
+ )} + {fileUploaders.length > 0 && ( +
    + {fileUploaders.map((fileUploader, index) => ( +
  • - {fileUploader.file.name} - - - onDeleteFile(event, fileUploader.file) - } - /> - {fileUploader.status === UploadStatus.uploading && ( -
    - -
    - )} - {fileUploader.status === UploadStatus.failed && ( - - )} - {fileUploader.status === UploadStatus.uploaded && ( - - )} -
  • - ))} -
- )} - - - - - -
+ + {fileUploader.file.name} + + + onDeleteFile(event, fileUploader.file) + } + /> + {fileUploader.status === UploadStatus.uploading && ( +
+ +
+ )} + {fileUploader.status === UploadStatus.failed && ( + + )} + {fileUploader.status === UploadStatus.uploaded && ( + + )} + + ))} + + )} + + + +
+ )} {!autoUpload && !withoutBtn && ( )} - {showTip && ( -
- {getStatus() === 'selected' && ( - {uploadTips} - )} - {getStatus() === 'uploading' && ( - - {uploadTips} - {i18nText.cancelUpload} - - )} - {getStatus() === 'uploaded' && ( -
- - - {i18nText.uploadSuccess} - -
- )} - {getStatus() === 'failed' && ( -
- - - {uploadTips} - {i18nText.reUpload} - -
- )} -
- )} + {}
{this.$slots.preloadFiles?.({ fileUploaders, diff --git a/devui/upload/src/single-upload-view.tsx b/devui/upload/src/single-upload-view.tsx deleted file mode 100644 index 33d4f981a6d4d9a19e3feff00da7b20cf01cffb8..0000000000000000000000000000000000000000 --- a/devui/upload/src/single-upload-view.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import './upload.scss' - -import { defineComponent } from 'vue' -import { singleUploadViewProps } from './upload-types' - -export default defineComponent({ - name: 'DSingleUploadView', - props: singleUploadViewProps, - // setup(props) {}, - render() { - const {} = this - return
- }, -}) diff --git a/devui/upload/src/single-upload.tsx b/devui/upload/src/single-upload.tsx index 9ceb1f5db15568daaa9e0c309e5865d135ef2631..23c84d49f3acb7b58a4bb3ea54e48a5790d5753b 100644 --- a/devui/upload/src/single-upload.tsx +++ b/devui/upload/src/single-upload.tsx @@ -1,13 +1,11 @@ -import './upload.scss' - import { defineComponent, toRefs, computed, ref } from 'vue' -import { Observable } from 'rxjs' -import { last, map } from 'rxjs/operators' import { ToastService } from '../../toast' import { uploadProps, UploadProps, UploadStatus } from './upload-types' import { useUpload } from './use-upload' import { useSelectFiles } from './use-select-files' import { i18nText } from './i18n-upload' +import './upload.scss' + export default defineComponent({ name: 'DSingleUpload', props: uploadProps, @@ -17,6 +15,7 @@ export default defineComponent({ 'fileSelect', 'successEvent', 'errorEvent', + 'deleteUploadedFileEvent', 'update:uploadedFiles', ], setup(props: UploadProps, ctx) { @@ -41,6 +40,7 @@ export default defineComponent({ getFullFiles, deleteFile, upload, + removeFiles, } = useUpload() const { triggerSelectFiles, _validateFiles, triggerDropFiles } = useSelectFiles() @@ -61,8 +61,6 @@ export default defineComponent({ if (typeof result !== 'undefined') { if (result.then) { uploadResult = result - } else if (result.subscribe) { - uploadResult = (result as Observable).toPromise() } else { uploadResult = Promise.resolve(result) } @@ -77,22 +75,19 @@ export default defineComponent({ return } upload() - .pipe(last()) - .subscribe( - (results: Array<{ file: File; response: any; }>) => { - ctx.emit('successEvent', results) - const newFiles = results.map((result) => result.file) - const newUploadedFiles = [...newFiles, ...uploadedFiles.value] - ctx.emit('update:uploadedFiles', newUploadedFiles) - }, - (error) => { - console.error(error) - if (fileUploaders.value[0]) { - fileUploaders.value[0].percentage = 0 - } - ctx.emit('errorEvent', error) + .then((results: { file: File; response: any; }[]) => { + ctx.emit('successEvent', results) + const newFiles = results.map((result) => result.file) + const newUploadedFiles = [...newFiles, ...uploadedFiles.value] + ctx.emit('update:uploadedFiles', newUploadedFiles) + }) + .catch((error) => { + console.error(error) + if (fileUploaders.value[0]) { + fileUploaders.value[0].percentage = 0 } - ) + ctx.emit('errorEvent', error) + }) }) } @@ -110,32 +105,29 @@ export default defineComponent({ }) } - const _dealFiles = (observale) => { - observale - .pipe( - map((file) => { + const _dealFiles = (promise: Promise) => { + promise + .then((files) => { + files.forEach((file) => { + // 单文件上传前先清空数组 + removeFiles() addFile(file, uploadOptions.value) - return file }) - ) - .subscribe( - () => { - checkValid() - const file = fileUploaders[0]?.file - if (props.onChange) { - props.onChange(file) - } - if (file) { - ctx.emit('fileSelect', file) - } - if (autoUpload.value) { - fileUpload() - } - }, - (error: Error) => { - alertMsg(error.message) + checkValid() + const file = fileUploaders.value[0]?.file + if (props.onChange) { + props.onChange(file) } - ) + if (file) { + ctx.emit('fileSelect', file) + } + if (autoUpload.value) { + fileUpload() + } + }) + .catch((error: Error) => { + alertMsg(error.message) + }) } const handleClick = () => { @@ -154,11 +146,17 @@ export default defineComponent({ const files = getFiles() deleteFile(files[0]) } + // 删除已上传文件 + const deleteUploadedFile = (file: File) => { + const newUploadedFiles = uploadedFiles.value.filter((uploadedFile) => { + return uploadedFile.name !== file.name + }) + ctx.emit('deleteUploadedFileEvent', file) + ctx.emit('update:uploadedFiles', newUploadedFiles) + } const onFileDrop = (files: File[]) => { isDropOVer.value = false - _dealFiles( - triggerDropFiles(fileOptions.value, uploadOptions.value, files) - ) + _dealFiles(triggerDropFiles(files)) ctx.emit('fileDrop', files[0]) } const onFileOver = (event: boolean) => { @@ -180,6 +178,8 @@ export default defineComponent({ onFileOver, isDropOVer, showTip, + uploadedFiles, + deleteUploadedFile, } }, render() { @@ -199,91 +199,87 @@ export default defineComponent({ isDropOVer, disabled, showTip, + uploadedFiles, + deleteUploadedFile, } = this - return ( - <> +
-
-
- {!filename && ( -
- {placeholderText} -
- )} - {!!filename && ( -
- {this.$slots.default()}
+ ) : ( +
+
+ {!filename && ( +
+ {placeholderText} +
+ )} + {!!filename && ( +
- {filename} - - onDeleteFile(event)} - /> - {fileUploaders[0]?.status === UploadStatus.uploading && ( -
- -
- )} - {fileUploaders[0].status === UploadStatus.failed && ( - - )} - {fileUploaders[0].status === UploadStatus.uploaded && ( - - )} -
- )} + + {filename} + + onDeleteFile(event)} + /> + {fileUploaders[0]?.status === UploadStatus.uploading && ( +
+ +
+ )} + {fileUploaders[0].status === UploadStatus.failed && ( + + )} + {fileUploaders[0].status === UploadStatus.uploaded && ( + + )} +
+ )} +
+ + +
- - - - - -
- + )} {!autoUpload && !withoutBtn && ( )} - +
+ {this.$slots.preloadFiles?.({ + fileUploaders, + deleteFile: onDeleteFile, + })} +
+
+ {this.$slots.uploadedFiles?.({ + uploadedFiles, + deleteFile: deleteUploadedFile, + })} +
+
) }, }) diff --git a/devui/upload/src/upload-types.ts b/devui/upload/src/upload-types.ts index 8820d2387f7a0df48461a8bb971f2a076a6e5932..cc5ad91580ad011c1da666b74eb0bd755f5cd25b 100644 --- a/devui/upload/src/upload-types.ts +++ b/devui/upload/src/upload-types.ts @@ -1,6 +1,4 @@ import type { PropType, ExtractPropTypes } from 'vue' -import { Observable } from 'rxjs' -import { FileUploader } from './file-uploader' export class IUploadOptions { // 上传接口地址 uri: string @@ -45,9 +43,7 @@ export enum UploadStatus { type DynamicUploadOptionsFn = (files, uploadOptions) => IUploadOptions type ChangeFn = (_: any) => void -type BeforeUploadFn = ( - file: File -) => boolean | Promise | Observable +type BeforeUploadFn = (file: File) => boolean | Promise export const uploadProps = { uploadOptions: { type: Object as PropType, @@ -69,22 +65,14 @@ export const uploadProps = { type: String, default: '选择文件', }, - // TODO - preloadFilesRef: { - type: Object, - }, uploadText: { type: String, default: '上传', }, uploadedFiles: { - type: Array, + type: Array as PropType, default: () => [], }, - // TODO - uploadedFilesRef: { - type: Object, - }, withoutBtn: { type: Boolean, default: false, @@ -135,33 +123,7 @@ export const uploadProps = { default: undefined, }, } as const -export const singleUploadViewProps = { - uploadOptions: { - type: Object as PropType, - }, - // TODO - preloadFilesRef: { - type: Object, - }, - uploadedFiles: { - type: Array, - }, - // TODO - uploadedFilesRef: { - type: Object, - }, - filePath: { - type: String, - required: true, - }, - dynamicUploadOptionsFn: { - type: Function as PropType, - }, -} export type UploadProps = ExtractPropTypes -export type singleUploadViewProps = ExtractPropTypes< - typeof singleUploadViewProps -> export const multiUploadProps = { uploadOptions: { @@ -174,7 +136,6 @@ export const multiUploadProps = { }, filePath: { type: String, - required: true, }, autoUpload: { type: Boolean, @@ -196,14 +157,6 @@ export const multiUploadProps = { type: Boolean, default: false, }, - // TODO - uploadedFilesRef: { - type: Object, - }, - // TODO - preloadFilesRef: { - type: Object, - }, placeholderText: { type: String, default: '选择文件', @@ -221,9 +174,7 @@ export const multiUploadProps = { default: false, }, beforeUpload: { - type: Function as PropType< - (files: any) => boolean | Promise | Observable - >, + type: Function as PropType<(files: any) => boolean | Promise>, }, fileDrop: { type: Function as PropType<(v: any) => void>, diff --git a/devui/upload/src/upload.scss b/devui/upload/src/upload.scss index edabc30117fab9d0a1f5d9544b9737fcdfadd479..287179cd863a6343b2f838f9c0e30067b8a88172 100644 --- a/devui/upload/src/upload.scss +++ b/devui/upload/src/upload.scss @@ -50,14 +50,10 @@ cursor: pointer; height: 100%; position: relative; - display: inline-block; - - svg { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - } + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; } .devui-input-group .devui-form-control { @@ -176,10 +172,6 @@ color: $devui-disabled-text; } -svg.svg-icon-dot > path { - fill: $devui-icon-text; -} - .devui-form-control { outline: none; } diff --git a/devui/upload/src/use-select-files.ts b/devui/upload/src/use-select-files.ts index cce6f208165cd676353f4dc2db19c24a3ec5607e..75caa61b0edeeb61d1718c44fa3bef0f777b4deb 100644 --- a/devui/upload/src/use-select-files.ts +++ b/devui/upload/src/use-select-files.ts @@ -1,6 +1,4 @@ import { ref } from 'vue' -import { from, Observable } from 'rxjs' -import { mergeMap } from 'rxjs/operators' import { IFileOptions, IUploadOptions } from './upload-types' import { getNotAllowedFileTypeMsg, @@ -128,18 +126,10 @@ export const useSelectFiles = () => { const triggerSelectFiles = (fileOptions: IFileOptions) => { const { multiple, accept, webkitdirectory } = fileOptions - return from(selectFiles({ multiple, accept, webkitdirectory })).pipe( - mergeMap((file) => file) - ) + return selectFiles({ multiple, accept, webkitdirectory }) } - const triggerDropFiles = ( - fileOptions: IFileOptions, - uploadOptions: IUploadOptions, - files: any - ) => { - return new Observable((observer) => observer.next(files)).pipe( - mergeMap((file) => file) - ) + const triggerDropFiles = (files: File[]) => { + return Promise.resolve(files) } const checkAllFilesSize = (fileSize, maximumSize) => { if (beyondMaximalSize(fileSize, maximumSize)) { diff --git a/devui/upload/src/use-upload.ts b/devui/upload/src/use-upload.ts index 48427ba15c20b4f74e11e30a767d093b8816bb49..72aa7a48d655743b091c0ad1445955aec4dddc83 100644 --- a/devui/upload/src/use-upload.ts +++ b/devui/upload/src/use-upload.ts @@ -1,6 +1,4 @@ import { ref } from 'vue' -import { from, merge } from 'rxjs' -import { toArray } from 'rxjs/operators' import { FileUploader } from './file-uploader' import { UploadStatus } from './upload-types' @@ -64,11 +62,20 @@ export const useUpload = () => { return finalUploads } - const upload = (oneFile?) => { + const upload = async ( + oneFile? + ): Promise< + | never + | { + file: File + response: any + }[] + > => { let uploads: any[] = [] if (oneFile) { oneFile.percentage = 0 - uploads.push(from(oneFile.send())) + const uploadedFile = await oneFile.send() + uploads.push(uploadedFile) } else { const preFiles = fileUploaders.value.filter( (fileUploader) => fileUploader.status === UploadStatus.preLoad @@ -77,28 +84,26 @@ export const useUpload = () => { (fileUploader) => fileUploader.status === UploadStatus.failed ) const uploadFiles = preFiles.length > 0 ? preFiles : failedFiles - uploads = uploadFiles.map((fileUploader) => { - fileUploader.percentage = 0 - return from(fileUploader.send()) - }) + uploads = await Promise.all( + uploadFiles.map(async (fileUploader) => { + fileUploader.percentage = 0 + const uploadedFile = await fileUploader.send() + return uploadedFile + }) + ) } if (uploads.length > 0) { - return merge< - { - file: File - response: any - }[] - >(...uploads).pipe(toArray()) + return Promise.resolve(uploads) } - return from(Promise.reject('no files')) + return Promise.reject('no files') } const _oneTimeUpload = () => { const uploads = fileUploaders.value.filter( (fileUploader) => fileUploader.status !== UploadStatus.uploaded ) - return from(dealOneTimeUploadFiles(uploads)) + return dealOneTimeUploadFiles(uploads) } const deleteFile = (file) => { diff --git a/sites/.vitepress/config/sidebar.ts b/sites/.vitepress/config/sidebar.ts index 7c9f0f769a5d71664eea48998892d7ab61e698e1..92d91f756db06f53e2cb5ad22411ff38531300f8 100644 --- a/sites/.vitepress/config/sidebar.ts +++ b/sites/.vitepress/config/sidebar.ts @@ -12,7 +12,7 @@ const sidebar = { { text: 'Search 搜索框', link: '/components/search/', status: '已完成' }, { text: 'Status 状态', link: '/components/status/', status: '已完成' }, { text: 'Sticky 便贴', link: '/components/sticky/' }, - { text: 'Overlay 浮层', link: '/components/overlay/'} + { text: 'Overlay 遮罩层', link: '/components/overlay/'} ] }, { @@ -90,7 +90,7 @@ const sidebar = { text: '布局', children: [ { text: 'Layout 布局', link: '/components/layout/' }, - { text: 'Splitter 分割器', link: '/components/splitter/' } + { text: 'Splitter 分割器', link: '/components/splitter/', status: '开发中' } ] }, ] diff --git a/sites/components/loading/customStyle.scss b/sites/components/loading/customStyle.scss deleted file mode 100644 index 5f89cd60d57fd6073dd21b39d204dce4dbb3d192..0000000000000000000000000000000000000000 --- a/sites/components/loading/customStyle.scss +++ /dev/null @@ -1,20 +0,0 @@ -.devui-infinity-loading { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); -} - -.devui-circle-loading-container-2 { - position: absolute; - left: 5px; - top: 50%; - transform: translateY(-50%); -} - -.devui-circle-loading-container-3 { - position: absolute; - right: -20px; - top: 50%; - transform: translateY(-50%); -} \ No newline at end of file diff --git a/sites/components/loading/index.md b/sites/components/loading/index.md index 69e75b83bfe632ebef6a9336e821f65e9dafe692..2a8521d152bed88eaa9ca1bff29a442e17fde25c 100644 --- a/sites/components/loading/index.md +++ b/sites/components/loading/index.md @@ -7,32 +7,11 @@ 当执行指令时间较长(需要数秒以上)时,向用户展示正在执行的状态。 - ### 基本用法 展示加载表格数据的场景中的基本使用方法。 +:::demo -click me! - - - - - - - - - - - -
序号姓名队伍操作
{{index}}张家齐跳水跳水队
- -```html +```vue - ``` +::: + ### 多promise 支持多个promise。 +:::demo -click me! - -
loading will show here2
- -```html +```vue - ``` +::: ### 自定义样式 通过 templateRef 自定义loading样式。 +:::demo -Loading Style 1 - -Loading Style 2 - -Loading Style 3 - -
loading will show here1
- -```html +```vue - -``` -```scss -// ./customStyle.scss + + ``` +::: ### 服务方式调用 使用服务的方式全屏加载loading组件或者在指定宿主上加载loading组件。 +:::demo -click me show full screen loading! - -click me show loading in target! - -click me close loading in target! - -
loading will show here3
- -```html +```vue - ``` +::: ### 参数 +dLoading 参数 | **参数** | **类型** | **默认** | **说明** | **跳转 Demo** | | ------------------ | ------------------------------------------------------------ | ------------------------- | ------------------------------------------------------------ | ---------------------------- | -| v-dLoading | Promise\ / Array\\> / Boolean / undefined | -- | 可选,指令方式,控制 loading 状态 | [基本用法](#基本用法) | -| target | Element | document.body | 可选,服务方式,Loading 需要覆盖的 DOM 节点 | [服务方式调用](#服务方式调用) | -| message | String | -- | 可选,loading 时的提示信息 | [多promise](#多promise) | -| loadingTemplateRef | VNode | -- | 可选,自定义 loading 模板 | [自定义样式](#自定义样式) | -| backdrop | Boolean | true | 可选,loading 时是否显示遮罩 | [基本用法](#基本用法) | -| positionType | String | relative | 可选,指定`dLoading`宿主元素的定位类型,取值与 css position 属性一致。 | [基本用法](#基本用法) | -| view | {top?:string,left?:string} | {top: '50%', left: '50%'} | 可选,调整 loading 的显示位置,相对于宿主元素的顶部距离与左侧距离 | [基本用法](#基本用法) | -| zIndex | Number | -- | 可选,loading加载提示的 z-index 值 | [基本用法](#基本用法) | - - - \ No newline at end of file diff --git a/sites/components/overlay/index.md b/sites/components/overlay/index.md index 9e62a2379031c9cc9fa47fb585a7fede582bf07e..31ab9274042e7a217a9f31358ff73444d4b96879 100644 --- a/sites/components/overlay/index.md +++ b/sites/components/overlay/index.md @@ -1,8 +1,8 @@ -## 浮层 -浮层属于基础组件,用于构建独立于当前页面布局的组件。 +## 遮罩层 +遮罩层属于基础组件,用于构建独立于当前页面布局的组件。 ### 何时使用 当你需要全局弹窗,或者需要元素跟随功能,便可以使用该组件。 -### 固定浮层 +### 固定遮罩层 :::demo 使用`sm`,`''`,`lg`来定义`Search`基本类型 @@ -24,7 +24,7 @@ export default defineComponent({ const origin = ref(); const visible = ref(false); const handleClick = () => visible.value = !visible.value; - const title = computed(() => visible.value ? '隐藏' : '显示固定浮层' ); + const title = computed(() => visible.value ? '隐藏' : '显示固定遮罩层' ); return { visible, handleClick, @@ -37,7 +37,7 @@ export default defineComponent({ ::: -### 弹性浮层 +### 弹性遮罩层 :::demo ```vue @@ -172,27 +172,27 @@ export default defineComponent({ ### API d-fixed-overlay 参数 -| 参数 | 类型 | 默认 | 说明 | -| :--------------: | :------------------------: | :---: | :---------------------------------------------------------------- | -| visible | `boolean` | false | 可选,浮层是否可见 | -| onUpdate:visible | `(value: boolean) => void` | -- | 可选,浮层取消可见事件 | -| backgroundBlock | `boolean` | false | 可选,如果为 true,背景不能滚动 | -| backgroundClass | `string` | -- | 可选,背景的样式类 | -| hasBackdrop | `boolean` | true | 可选,如果为false,背景后下的内容将可以触发 | -| backdropClick | `() => void` | -- | 可选,点击背景触发的事件 | -| backdropClose | `boolean` | false | 可选,如果为true,点击背景将触发 `onUpdate:visible`,参数是 false | -| overlayStyle | `CSSProperties` | -- | 可选,浮层的样式 | +| 参数 | 类型 | 默认 | 说明 | +| :--------------: | :------------------------: | :---: | :-------------------------------------------------------------------- | +| visible | `boolean` | false | 可选,遮罩层是否可见 | +| onUpdate:visible | `(value: boolean) => void` | -- | 可选,遮罩层取消可见事件 | +| backgroundBlock | `boolean` | false | 可选,如果为 true,背景不能滚动 | +| backgroundClass | `string` | -- | 可选,背景的样式类 | +| hasBackdrop | `boolean` | true | 可选,如果为false,背景后下的内容将可以触发 | +| backdropClick | `() => void` | -- | 可选,点击背景触发的事件 | +| backdropClose | `boolean` | false | 可选,如果为true,点击背景将触发 `onUpdate:visible`,默认参数是 false | +| overlayStyle | `CSSProperties` | -- | 可选,遮罩层的样式 | d-flexible-overlay 参数 | 参数 | 类型 | 默认 | 说明 | | :--------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---: | :---------------------------------------------------------------- | -| visible | `boolean` | false | 可选,浮层是否可见 | -| onUpdate:visible | `(value: boolean) => void` | -- | 可选,浮层取消可见事件 | +| visible | `boolean` | false | 可选,遮罩层是否可见 | +| onUpdate:visible | `(value: boolean) => void` | -- | 可选,遮罩层取消可见事件 | | backgroundBlock | `boolean` | false | 可选,如果为 true,背景不能滚动 | | backgroundClass | `string` | -- | 可选,背景的样式类 | | hasBackdrop | `boolean` | true | 可选,如果为false,背景后下的内容将可以触发 | | backdropClick | `() => void` | -- | 可选,点击背景触发的事件 | | backdropClose | `boolean` | false | 可选,如果为true,点击背景将触发 `onUpdate:visible`,参数是 false | -| origin | `Element \| ComponentPublicInstance \| { x: number, y: number, width?: number, height?: number }` | false | 必选,你必须指定起点元素才能让浮层与该元素连接在一起 | -| position | `{originX: HorizontalPos, originY: VerticalPos, overlayX: HorizontalPos, overlayY: VerticalPos } (type HorizontalPos = 'left' \| 'center' \| 'right') ( type VerticalPos = 'top' \| 'center' \| 'bottom')` | false | 可选,指定浮层与原点的连接点 | +| origin | `Element \| ComponentPublicInstance \| { x: number, y: number, width?: number, height?: number }` | false | 必选,你必须指定起点元素才能让遮罩层与该元素连接在一起 | +| position | `{originX: HorizontalPos, originY: VerticalPos, overlayX: HorizontalPos, overlayY: VerticalPos } (type HorizontalPos = 'left' \| 'center' \| 'right') ( type VerticalPos = 'top' \| 'center' \| 'bottom')` | false | 可选,指定遮罩层与原点的连接点 | diff --git a/sites/components/pagination/index.md b/sites/components/pagination/index.md index b21a10e54af279e5c7e04d13d42697304fc5adf4..dad91b8a72be43a9b2158af8a8de15e14752e51a 100644 --- a/sites/components/pagination/index.md +++ b/sites/components/pagination/index.md @@ -9,214 +9,158 @@ ### 基本用法 -**size = 'sm'** - - - - -**size = 'md'** - - - - -**size = 'lg'** - - - - -**Custom Style** - - - -```html -size = 'sm' - - -size = 'md' - - -size = 'lg' - - -Custom Style - +:::demo + +```vue + + ``` +::: ### 极简模式 极简模式适用于一些有大量信息的页面,可以简化页面的复杂度。 - -**Simple Mode** - - - - -**Super Simple Mode** - - - - -**haveConfigMenu = "true"** - - -
-
show field
-
setting
-
-
-
display method
-
- - +:::demo + + +```vue + + + + ``` - +::: ### 多种配置 支持设置输入跳转、显示跳转按钮;设置pageSize等功能。 - -
- - - -
- - - -
- - - -```html - - - - - -``` - - -### 特殊情况 -特殊场景下分页器的显示。 - -
-When the value of pageIndex exceeds the maximum page number, enable showTruePageIndex to display the value of pageIndex -
- - - - -
-When the value of pageIndex exceeds the maximum page number, the showTruePageIndex function is disabled and only the maximum page number is displayed. -
- - - - -
Default Mode
- - - - -
- total = 0 - total = 5 - total = 15 -
- -
Simple Mode
- - - -
- total = 0 - total = 20 - total = 30000 - total = 100000 - index = 2 - index = 3 -
- -```html -
-When the value of pageIndex exceeds the maximum page number, enable showTruePageIndex to display the value of pageIndex -
- - -
-When the value of pageIndex exceeds the maximum page number, the showTruePageIndex function is disabled and only the maximum page number is displayed. -
- - -
Default Mode
- -
- total = 0 - total = 5 - total = 15 -
- -
Simple Mode
- -
- total = 0 - total = 20 - total = 30000 - total = 100000 - index = 2 - index = 3 -
-``` - - +``` +::: + +### 特殊情况 +特殊场景下分页器的显示。 +:::demo + + +```vue + + \ No newline at end of file + +``` +::: + +### 参数 + +d-pagination 参数 + +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | +| ----------------- | ------------------------------------------------------------ | -------------------------- | ------------------------------------------------------------ | --------------------- | +| pageSize | `number` | 10 | 可选,每页显示最大条目数量 | [基本用法](#基本用法) | +| total | `number` | 0 | 可选,显示的总条目数 | [基本用法](#基本用法) | +| pageSizeOptions | `number[] ` | 10 | 可选,分页每页最大条目数量的下拉框的数据源,默认有四种选择 5, 10, 20, 50 | [多种配置](#多种配置) | +| pageSizeDirection | `Array<`[`AppendToBodyDirection`](#appendtobodydirection) `\|` [`ConnectedPosition`](#connectedposition)`>` | ['centerDown', 'centerUp'] | 可选,设置分页每页条目的下拉框展示的方向 | [多种配置](#多种配置) | +| pageIndex | `number` | 1 | 可选,初始化页码 | [基本用法](#基本用法) | +| maxItems | `number` | 10 | 可选,分页最多显示几个按钮 | [基本用法](#基本用法) | +| preLink | `string` | -- | 可选,上一页按钮显示图标,默认设置为左箭头图标 | [基本用法](#基本用法) | +| nextLink | `string` | -- | 可选, 下一页按钮显示图标,默认设置为右箭头图标 | [基本用法](#基本用法) | +| size | `number` | '' | 可选,分页组件尺寸,有三种选择 lg,``,sm,分别代表大,中,小 | [基本用法](#基本用法) | +| canJumpPage | `boolean` | false | 可选,是否显示分页输入跳转 | [基本用法](#基本用法) | +| canChangePageSize | `boolean` | false | 可选,是否显示用于选择更改分页每页最大条目数量的下拉框 | [基本用法](#基本用法) | +| canViewTotal | `boolean` | false | 可选,是否显示总条目 | [基本用法](#基本用法) | +| totalItemText | `string` | '所有条目' | 可选,总条目文本 | [极简模式](#极简模式) | +| goToText | `string` | '跳至' | 可选,总条目文本 | [基本用法](#基本用法) | +| showJumpButton | `boolean` | false | 可选,是否显示跳转按钮 | [多种配置](#多种配置) | +| showTruePageIndex | `boolean` | false | 可选,页码超出分页范围时候也显示当前页码的开关 | [多种配置](#多种配置) | +| lite | `boolean` | false | 可选,是否切换为极简模式 | [极简模式](#极简模式) | +| showPageSelector | `boolean` | true | 可选,`极简模式`下是否显示页码下拉 | [极简模式](#极简模式) | +| haveConfigMenu | `boolean` | false | 可选,`极简模式`下是否显示配置 | [极简模式](#极简模式) | +| autoFixPageIndex | `boolean` | true | 可选,改变 pageSize 时是否自动修正页码,若`pageSizeChange`事件中会对`pageIndex`做处理,建议设置为`false` | [极简模式](#极简模式) | +| autoHide | `boolean` | false | 可选,是否自动隐藏, autoHide为 true 并且 pageSizeOptions最小值 > total 不展示分页 | [极简模式](#极简模式) | + +d-pagination 事件 + +| 参数 | 类型 | 说明 | 跳转 Demo | +| --------------- | -------------------- | ---------------------------------------------------------- | --------------------- | +| pageIndexChange | `EventEmitter` | 可选,页码变化的回调,返回当前页码值 | [多种配置](#多种配置) | +| pageSizeChange | `EventEmitter` | 可选,每页最大条目数量变更时的回调,返回当前每页显示条目数 | [多种配置](#多种配置) | + +**接口 & 类型定义** + +##### AppendToBodyDirection + +```ts +export type AppendToBodyDirection = 'rightDown' | 'rightUp' | 'leftUp' | 'leftDown' | 'centerDown' | 'centerUp'; +``` + +##### ConnectedPosition + +```ts +export interface ConnectedPosition { + originX: 'start' | 'center' | 'end'; + originY: 'top' | 'center' | 'bottom'; + + overlayX: 'start' | 'center' | 'end'; + overlayY: 'top' | 'center' | 'bottom'; + + weight?: number; + offsetX?: number; + offsetY?: number; + panelClass?: string | string[]; +} +``` + diff --git a/sites/components/quadrant-diagram/index.md b/sites/components/quadrant-diagram/index.md index 8a3e8f62cc821e67d3ce387c8bc575ec314e4f9b..34ae985470b2620e74d31cd7bf261ffb2dea0008 100644 --- a/sites/components/quadrant-diagram/index.md +++ b/sites/components/quadrant-diagram/index.md @@ -13,7 +13,32 @@ :::demo ```vue - + + + + ``` ::: diff --git a/sites/components/splitter/index.md b/sites/components/splitter/index.md new file mode 100644 index 0000000000000000000000000000000000000000..957995982b3c9fbd5808a500da0568df24c791b4 --- /dev/null +++ b/sites/components/splitter/index.md @@ -0,0 +1,98 @@ +# Splitter 分割器 + +页面分割器。 + +**何时使用** + +需要动态调整不同页面布局区域大小的时候选择使用。 + +## 基本用法 + +:::demo + +```vue + + + + + +``` +::: + + + +## 组合布局用法【TODO】 + +## 指定折叠收起方向【TODO】 + +## 折叠收缩显示菜单【TODO】 + diff --git a/sites/components/splitter/indexBak.md b/sites/components/splitter/indexBak.md new file mode 100644 index 0000000000000000000000000000000000000000..c753c9c964ba354bdb14daf42eec798ab459e6c9 --- /dev/null +++ b/sites/components/splitter/indexBak.md @@ -0,0 +1,188 @@ +# Splitter 分割器 + +页面分割器。 + +**何时使用** + +需要动态调整不同页面布局区域大小的时候选择使用。 + +## 基本用法 + +:::demo + +```vue + + + + + +``` +::: + + + + + + + + + + +## 组合布局用法【TODO】 + +## 指定折叠收起方向【TODO】 + +## 折叠收缩显示菜单【TODO】 + diff --git a/sites/components/sticky/index.md b/sites/components/sticky/index.md new file mode 100644 index 0000000000000000000000000000000000000000..e3acf7c0334a6204896fccae94b1efbc6182c017 --- /dev/null +++ b/sites/components/sticky/index.md @@ -0,0 +1,139 @@ +# sticky 便贴 + +### 何时使用 +当用户在滚动屏幕时,需要某个区域内容在段落或者浏览器可视区域可见时。 + +### 基本用法 + +:::demo 默认容器为父元素 + +```vue + + + + +``` +::: + +### 指定容器 + +:::demo + +```vue + + + +``` +::: + + +### 指定滚动容器 + +:::demo + +```vue + + + + +``` +::: + + +### API + +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | 全局配置项 | +| :---------: | :------: | :-------: | :----------------------- | --------------------------------- | --------- | +| zIndex | `number` | -- | 可选,指定包裹层的 z-index,用于浮动的时候控制 z 轴的叠放 | [基本用法](#基本用法) || +| container | `Element` | 父容器 | 可选,触发的容器,可不同于父容器 | [指定容器](#指定容器) || +| view | `{top?:number,bottom?:number}` | {top:0,bottom:0} | 可选,用于可视区域的调整,比如顶部有固定位置的头部等,数值对应被遮挡的顶部或底部的高度 | [基本用法](#基本用法) || +| scrollTarget | `Element` | document.documentElement | 可选,设置要发生滚动的容器,一般为滚动条所在容器,为主页面的滚动条时候可以不设置 | [指定滚动容器](#指定滚动容器) || + + +### d-sticky 事件 + +| 事件 | 类型 | 说明 | 跳转 Demo | +| :---------: | :------: | :--------------------: | :---------: | +| statusChange | `string` | 可选,状态变化的时候触发,值为变化后的状态值 | [基本用法](#基本用法) | + diff --git a/sites/components/tabs/index.md b/sites/components/tabs/index.md index 1fe147671caabaffff282bdff74f4af2dce92a39..f318f6d26e93bf658622729f447360e622ac6f74 100644 --- a/sites/components/tabs/index.md +++ b/sites/components/tabs/index.md @@ -5,3 +5,4 @@ ### 何时使用 用户需要通过平级的区域将大块内容进行收纳和展现,保持界面整洁。 + diff --git a/sites/components/tooltip/index.md b/sites/components/tooltip/index.md new file mode 100644 index 0000000000000000000000000000000000000000..770d55d477ee96e8f24c5299ea014aca9dedb7a9 --- /dev/null +++ b/sites/components/tooltip/index.md @@ -0,0 +1,90 @@ +# Tooltip 提示 + +文字提示组件。 + +### 何时使用 + +用户鼠标移动到文字上,需要进一步的提示时使用。 + +### 基本用法 + +:::demo我们可以通过控制属性`position`来控制tooltip的显示位置,`position`取值有4个,分别是`top`、`right`、`bottom`、`left`。通过属性`content`控制tooltip提示框的内容。 + + + + + + + + + +```vue + +``` + +```css +.example { + height: 50px; + width: 60px; + background: cornflowerblue; + margin-top: 30px; +} +``` +::: + +### 延时触发 + +鼠标移入的时长超过 [mouseEnterDelay] 毫秒之后才会触发,以防止用户无意划过导致的闪现,默认值是150毫秒;鼠标移出之后,再经过[mouseLeaveDelay]毫秒后,toolTip组件才会隐藏,默认值是100毫秒。 + +:::demo 通过`mouseEnterDelay`属性来控制tooltip提示框的`延迟显示`(默认是100ms),`mouseLeaveDelay`属性来控制tooltip提示框的`延迟消失`(默认是150ms) +
MouseEnter delay 500ms
+ +
MouseLeave delay 1000ms
+```vue + +``` + +```css +.customCss { + width: fit-content; + height: 30px; + padding: 10px; + display: flex; + justify-content: center; + align-items: center; + color: #fff; + background: cornflowerblue; +} +.customCss-leave { + width: fit-content; + height: 30px; + padding: 10px; + display: flex; + justify-content: center; + align-items: center; + color: #252b3a; + background: #fff; +} +``` +::: \ No newline at end of file diff --git a/sites/components/transfer/index.md b/sites/components/transfer/index.md new file mode 100644 index 0000000000000000000000000000000000000000..988204fe24b2687ba999593adb86bae70ce5053b --- /dev/null +++ b/sites/components/transfer/index.md @@ -0,0 +1,224 @@ +# Transfer 穿梭框 + +双栏穿梭选择框。 + +### 何时使用 + +需要在多个可选项中进行多选时。穿梭选择框可用只管的方式在两栏中移动数据,完成选择行为。其中左边一栏为source,右边一栏为target。最终返回两栏的数据,提供给开发者使用。 + +### 基本用法 + +穿梭框基本用法。 + +
+ + +
+ + + +```html + + +``` + +```ts +import {defineComponent, reactive} from 'vue' + type TData = { + id: number + age: number + value: string + disabled?: boolean + } + export default defineComponent({ + setup() { + const options = reactive({ + titles: ['sourceHeader', 'targetHeader'], + source: [ + { + key: '北京', + value: '北京', + disabled: false + }, + { + key: '上海', + value: '上海', + disabled: true + }, + { + key: '广州', + value: '广州', + disabled: false + }, + { + key: '深圳', + value: '深圳', + disabled: false + }, + { + key: '成都', + value: '成都', + disabled: false + }, + { + key: '武汉', + value: '武汉', + disabled: false + }, + { + key: '西安', + value: '西安', + disabled: false + }, + { + key: '福建', + value: '福建', + disabled: false + }, + { + key: '大连', + value: '大连', + disabled: false + }, + { + key: '重庆', + value: '重庆', + disabled: false + } + ], + target: [ + { + key: '南充', + value: '南充', + disabled: false + }, + { + key: '广元', + value: '广元', + disabled: true + }, + { + key: '绵阳', + value: '绵阳', + disabled: false + } + ], + isSearch: true, + modelValues: ['深圳', '成都', '绵阳'] + }) + return { + options + } + } + }) +``` + +### 参数 + +| **参数** | **类型** | **默认** | **说明** | **跳转 Demo** | +| ------------------ | ------------------------------------------------------------ | ------------------------- | ------------------------------------------------------------ | ---------------------------- | +| sourceOption | Array | [] | 可选参数,穿梭框源数据 | [基本用法](#基本用法) | +| targetOption | Array | [] | 可选参数,穿梭框目标数据 | [基本用法](#基本用法) | +| titles | Array | [] | 可选参数,穿梭框标题 | [基本用法](#基本用法) | +| height | string | 320px | 可选参数,穿梭框高度 | [基本用法](#基本用法) | +| isSearch | boolean | true | 可选参数,是否可以搜索 | [基本用法](#基本用法) | +| disabled | boolean | false | 可选参数 穿梭框禁止使用 | [基本用法](#基本用法) | diff --git a/sites/components/upload/index.md b/sites/components/upload/index.md index 30ac446efd382b7d8f8b379b726f0028e4409601..6300ab68fb1b8789e32e91a372c6fd051dbe73cb 100644 --- a/sites/components/upload/index.md +++ b/sites/components/upload/index.md @@ -19,7 +19,7 @@ +``` + +::: + +自定义默认 slot,初始显示已上传文件。 + +:::demo + +```vue + + + +``` + +::: + ### API d-single-upload 参数 -| **参数** | **类型** | **默认** | 说明 | **跳转 Demo** | -| ---------------------- | ---------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------- | --------------------- | -| fileOptions | [IFileOptions](#ifileoptions) | -- | 必选,待上传文件配置 | [基本用法](#基本用法) | -| filePath | `string` | -- | 必选,文件路径 | [基本用法](#基本用法) | -| uploadOptions | [IUploadOptions](#iuploadoptions) | \-- | 必选,上传配置 | [基本用法](#基本用法) | -| autoUpload | `boolean` | false | 可选,是否自动上传 | [基本用法](#基本用法) | -| placeholderText | `string` | '选择文件' | 可选,上传输入框中的 Placeholder 文字 | [基本用法](#基本用法) | -| uploadText | `string` | '上传' | 可选,上传按钮文字 | [基本用法](#基本用法) | -| uploadedFiles | `Array` | [] | 可选,获取已上传的文件列表 | [基本用法](#基本用法) | -| withoutBtn | `boolean` | false | 可选,是否舍弃按钮 | [基本用法](#基本用法) | -| enableDrop | `boolean` | false | 可选,是否支持拖拽 | [基本用法](#基本用法) | -| beforeUpload | `boolean Promise Observable` | \-- | 可选,上传前的回调,通过返回`true` or `false` ,控制文件是否上传,参数为文件信息及上传配置 | [基本用法](#基本用法) | -| dynamicUploadOptionsFn | [IUploadOptions](#iuploadoptions) | \-- | 为文件动态设置自定义的上传参数, 参数为当前选中文件及`uploadOptions`的值 | [基本用法](#基本用法) | -| disabled | `boolean` | false | 可选,是否禁用上传组件 | [基本用法](#基本用法) | -| showTip | `boolean` | false | 可选,是否显示上传提示信息 | [自动上传](#自动上传) | +| **参数** | **类型** | **默认** | 说明 | **跳转 Demo** | +| ---------------------- | --------------------------------- | ---------- | ---------------------------------------------------------------------------------------- | --------------------- | +| fileOptions | [IFileOptions](#ifileoptions) | -- | 必选,待上传文件配置 | [基本用法](#基本用法) | +| filePath | `string` | -- | 文件路径 | [基本用法](#基本用法) | +| uploadOptions | [IUploadOptions](#iuploadoptions) | \-- | 必选,上传配置 | [基本用法](#基本用法) | +| autoUpload | `boolean` | false | 可选,是否自动上传 | [基本用法](#基本用法) | +| placeholderText | `string` | '选择文件' | 可选,上传输入框中的 Placeholder 文字 | [基本用法](#基本用法) | +| uploadText | `string` | '上传' | 可选,上传按钮文字 | [基本用法](#基本用法) | +| uploadedFiles | `Array` | [] | 可选,获取已上传的文件列表 | [基本用法](#基本用法) | +| withoutBtn | `boolean` | false | 可选,是否舍弃按钮 | [基本用法](#基本用法) | +| enableDrop | `boolean` | false | 可选,是否支持拖拽 | [基本用法](#基本用法) | +| beforeUpload | `boolean Promise ` | \-- | 可选,上传前的回调,通过返回`true` or `false` ,控制文件是否上传,参数为文件信息及上传配置 | [基本用法](#基本用法) | +| dynamicUploadOptionsFn | [IUploadOptions](#iuploadoptions) | \-- | 为文件动态设置自定义的上传参数, 参数为当前选中文件及`uploadOptions`的值 | [基本用法](#基本用法) | +| disabled | `boolean` | false | 可选,是否禁用上传组件 | [基本用法](#基本用法) | +| showTip | `boolean` | false | 可选,是否显示上传提示信息 | [自动上传](#自动上传) | d-single-upload 事件 @@ -885,22 +1217,22 @@ d-single-upload 事件 d-multiple-upload 参数 -| **参数** | **类型** | **默认** | 说明 | **跳转 Demo** | -| ---------------------- | ---------------------------------------------- | -------------- | ---------------------------------------------------------------------------------------- | ------------------------- | -| fileOptions | [IFileOptions](#ifileoptions) | -- | 必选,待上传文件配置 | [多文件上传](#多文件上传) | -| filePath | `string` | -- | 必选,文件路径 | [多文件上传](#多文件上传) | -| uploadOptions | [IUploadOptions](#iuploadoptions) | \-- | 必选,上传配置 | [多文件上传](#多文件上传) | -| autoUpload | `boolean` | false | 可选,是否自动上传 | [自动上传](#自动上传) | -| placeholderText | `string` | '选择多个文件' | 可选,上传输入框中的 Placeholder 文字 | [基本用法](#基本用法) | -| uploadText | `string` | '上传' | 可选,上传按钮文字 | [基本用法](#基本用法) | -| uploadedFiles | `Array` | [] | 可选,获取已上传的文件列表 | [多文件上传](#多文件上传) | -| withoutBtn | `boolean` | false | 可选,是否舍弃按钮 | [自定义](#自定义) | -| enableDrop | `boolean` | false | 可选,是否支持拖拽 | [多文件上传](#多文件上传) | -| beforeUpload | `boolean Promise Observable` | \-- | 可选,上传前的回调,通过返回`true` or `false` ,控制文件是否上传,参数为文件信息及上传配置 | [多文件上传](#多文件上传) | -| dynamicUploadOptionsFn | [IUploadOptions](#iuploadoptions) | \-- | 为文件动态设置自定义的上传参数, 参数为当前选中文件及`uploadOptions`的值 | [多文件上传](#多文件上传) | -| disabled | `boolean` | false | 可选,是否禁用上传组件 | [多文件上传](#多文件上传) | -| showTip | `boolean` | false | 可选,是否显示上传提示信息 | [多文件上传](#多文件上传) | -| setCustomUploadOptions | [IUploadOptions](#iuploadoptions) | -- | 为每个文件设置自定义的上传参数, 参数为当前选中文件及`uploadOptions`的值 | [自定义](#自定义) | +| **参数** | **类型** | **默认** | 说明 | **跳转 Demo** | +| ---------------------- | --------------------------------- | -------------- | ---------------------------------------------------------------------------------------- | ------------------------- | +| fileOptions | [IFileOptions](#ifileoptions) | -- | 必选,待上传文件配置 | [多文件上传](#多文件上传) | +| filePath | `string` | -- | 文件路径 | [多文件上传](#多文件上传) | +| uploadOptions | [IUploadOptions](#iuploadoptions) | \-- | 必选,上传配置 | [多文件上传](#多文件上传) | +| autoUpload | `boolean` | false | 可选,是否自动上传 | [自动上传](#自动上传) | +| placeholderText | `string` | '选择多个文件' | 可选,上传输入框中的 Placeholder 文字 | [基本用法](#基本用法) | +| uploadText | `string` | '上传' | 可选,上传按钮文字 | [基本用法](#基本用法) | +| uploadedFiles | `Array` | [] | 可选,获取已上传的文件列表 | [多文件上传](#多文件上传) | +| withoutBtn | `boolean` | false | 可选,是否舍弃按钮 | [自定义](#自定义) | +| enableDrop | `boolean` | false | 可选,是否支持拖拽 | [多文件上传](#多文件上传) | +| beforeUpload | `boolean Promise` | \-- | 可选,上传前的回调,通过返回`true` or `false` ,控制文件是否上传,参数为文件信息及上传配置 | [多文件上传](#多文件上传) | +| dynamicUploadOptionsFn | [IUploadOptions](#iuploadoptions) | \-- | 为文件动态设置自定义的上传参数, 参数为当前选中文件及`uploadOptions`的值 | [多文件上传](#多文件上传) | +| disabled | `boolean` | false | 可选,是否禁用上传组件 | [多文件上传](#多文件上传) | +| showTip | `boolean` | false | 可选,是否显示上传提示信息 | [多文件上传](#多文件上传) | +| setCustomUploadOptions | [IUploadOptions](#iuploadoptions) | -- | 为每个文件设置自定义的上传参数, 参数为当前选中文件及`uploadOptions`的值 | [自定义](#自定义) | d-multiple-upload 事件 diff --git a/sites/vite.config.ts b/sites/vite.config.ts index 59f87bb3f7a6184993fea9bd97114e066961cf00..3758c975f65216efb8909de2f335461a9888426c 100644 --- a/sites/vite.config.ts +++ b/sites/vite.config.ts @@ -1,8 +1,10 @@ -import { defineConfig } from 'vite'; -import vueJsx from '@vitejs/plugin-vue-jsx'; +import path from 'path' +import { defineConfig } from 'vite' +import vueJsx from '@vitejs/plugin-vue-jsx' export default defineConfig({ - plugins: [ - vueJsx({}), - ], + resolve: { + alias: [{ find: '@devui', replacement: path.resolve(__dirname, '../devui') }], + }, + plugins: [vueJsx({})], })