diff --git a/devui/vue-devui.ts b/devui/vue-devui.ts new file mode 100644 index 0000000000000000000000000000000000000000..9392e32e14cf633637a9cfe130b04e5965428b19 --- /dev/null +++ b/devui/vue-devui.ts @@ -0,0 +1,181 @@ +import type { App } from 'vue' + +import AccordionInstall, { Accordion } from './accordion' +import AlertInstall, { Alert } from './alert' +import AnchorInstall, { Anchor } from './anchor' +import AvatarInstall, { Avatar } from './avatar' +import BadgeInstall, { Badge } from './badge' +import BreadcrumbInstall, { Breadcrumb } from './breadcrumb' +import ButtonInstall, { Button } from './button' +import CardInstall, { Card } from './card' +import CarouselInstall, { Carousel } from './carousel' +import CascaderInstall, { Cascader } from './cascader' +import CheckboxInstall, { Checkbox } from './checkbox' +import DatePickerInstall, { DatePicker, StickSlider } from './date-picker' +import DrawerInstall, { Drawer } from './drawer' +import EditableSelectInstall, { EditableSelect, EditableSelectOption } from './editable-select' +import FullscreenInstall, { Fullscreen } from './fullscreen' +import IconInstall, { Icon } from './icon' +import ImagePreviewInstall, { ImagePreviewDirective, ImagePreviewService } from './image-preview' +import InputInstall, { Input } from './input' +import InputNumberInstall, { InputNumber } from './input-number' +import LayoutInstall, { Layout, Content, Header, Footer, Aside } from './layout' +import LoadingInstall, { LoadingService, Loading } from './loading' +import ModalInstall, { Modal } from './modal' +import NavSpriteInstall, { NavSprite } from './nav-sprite' +import OverlayInstall, { FlexibleOverlay, FixedOverlay } from './overlay' +import PaginationInstall, { Pagination } from './pagination' +import PanelInstall, { Panel } from './panel' +import PopoverInstall, { Popover } from './popover' +import ProgressInstall, { Progress } from './progress' +import QuadrantDiagramInstall, { QuadrantDiagram } from './quadrant-diagram' +import RadioInstall, { Radio, RadioGroup } from './radio' +import RateInstall, { Rate } from './rate' +import RippleInstall, { RippleDirective } from './ripple' +import SearchInstall, { Search } from './search' +import SelectInstall, { Select } from './select' +import SkeletonInstall, { Skeleton } from './skeleton' +import SliderInstall, { Slider } from './slider' +import SplitterInstall, { Splitter } from './splitter' +import StatusInstall, { Status } from './status' +import StepsGuideInstall, { StepsGuide } from './steps-guide' +import StickyInstall, { Sticky } from './sticky' +import SwitchInstall, { Switch } from './switch' +import TableInstall, { Table, Column } from './table' +import TabsInstall, { Tabs } from './tabs' +import TagInputInstall, { TagInput } from './tag-input' +import TimeAxisInstall, { TimeAxis } from './time-axis' +import TimePickerInstall, { TimePicker } from './time-picker' +import ToastInstall, { Toast, ToastService } from './toast' +import TooltipInstall, { Tooltip } from './tooltip' +import TransferInstall, { Transfer } from './transfer' +import TreeInstall, { Tree } from './tree' +import UploadInstall, { Upload, MultiUpload } from './upload' + +const installs = [ + AccordionInstall, + AlertInstall, + AnchorInstall, + AvatarInstall, + BadgeInstall, + BreadcrumbInstall, + ButtonInstall, + CardInstall, + CarouselInstall, + CascaderInstall, + CheckboxInstall, + DatePickerInstall, + DrawerInstall, + EditableSelectInstall, + FullscreenInstall, + IconInstall, + ImagePreviewInstall, + InputInstall, + InputNumberInstall, + LayoutInstall, + LoadingInstall, + ModalInstall, + NavSpriteInstall, + OverlayInstall, + PaginationInstall, + PanelInstall, + PopoverInstall, + ProgressInstall, + QuadrantDiagramInstall, + RadioInstall, + RateInstall, + RippleInstall, + SearchInstall, + SelectInstall, + SkeletonInstall, + SliderInstall, + SplitterInstall, + StatusInstall, + StepsGuideInstall, + StickyInstall, + SwitchInstall, + TableInstall, + TabsInstall, + TagInputInstall, + TimeAxisInstall, + TimePickerInstall, + ToastInstall, + TooltipInstall, + TransferInstall, + TreeInstall, + UploadInstall +] + +export { + Accordion, + Alert, + Anchor, + Avatar, + Badge, + Breadcrumb, + Button, + Card, + Carousel, + Cascader, + Checkbox, + DatePicker, + StickSlider, + Drawer, + EditableSelect, + EditableSelectOption, + Fullscreen, + Icon, + ImagePreviewDirective, + ImagePreviewService, + Input, + InputNumber, + Layout, + Content, + Header, + Footer, + Aside, + LoadingService, + Loading, + Modal, + NavSprite, + FlexibleOverlay, + FixedOverlay, + Pagination, + Panel, + Popover, + Progress, + QuadrantDiagram, + Radio, + RadioGroup, + Rate, + RippleDirective, + Search, + Select, + Skeleton, + Slider, + Splitter, + Status, + StepsGuide, + Sticky, + Switch, + Table, + Column, + Tabs, + TagInput, + TimeAxis, + TimePicker, + Toast, + ToastService, + Tooltip, + Transfer, + Tree, + Upload, + MultiUpload +} + +export default { + version: '0.0.1', + install(app: App): void { + installs.forEach((p) => app.use(p as any)) + } +} diff --git a/docs/.vitepress/config/sidebar.ts b/docs/.vitepress/config/sidebar.ts new file mode 100644 index 0000000000000000000000000000000000000000..89cbe87058faf9088e18113ac048dcaff5949675 --- /dev/null +++ b/docs/.vitepress/config/sidebar.ts @@ -0,0 +1,293 @@ +export default { + '/': [ + { + "text": "快速开始", + "link": "/" + }, + { + "text": "通用", + "children": [ + { + "text": "Button 按钮", + "link": "/components/button/", + "status": "已完成" + }, + { + "text": "Fullscreen 全屏", + "link": "/components/fullscreen/", + "status": "已完成" + }, + { + "text": "Icon 图标", + "link": "/components/icon/", + "status": "已完成" + }, + { + "text": "Overlay 遮罩层", + "link": "/components/overlay/", + "status": "已完成" + }, + { + "text": "Panel 面板", + "link": "/components/panel/", + "status": "100%" + }, + { + "text": "Ripple 水波纹", + "link": "/components/ripple/", + "status": "已完成" + }, + { + "text": "Search 搜索框", + "link": "/components/search/", + "status": "已完成" + }, + { + "text": "Status 状态", + "link": "/components/status/", + "status": "已完成" + }, + { + "text": "Sticky 便贴", + "link": "/components/sticky/", + "status": "50%" + } + ] + }, + { + "text": "导航", + "children": [ + { + "text": "Accordion 手风琴", + "link": "/components/accordion/", + "status": "10%" + }, + { + "text": "Anchor 锚点", + "link": "/components/anchor/", + "status": "50%" + }, + { + "text": "Breadcrumb 面包屑", + "link": "/components/breadcrumb/", + "status": "50%" + }, + { + "text": "NavSprite 导航精灵", + "link": "/components/nav-sprite/", + "status": "10%" + }, + { + "text": "Pagination 分页", + "link": "/components/pagination/", + "status": "已完成" + }, + { + "text": "StepsGuide 操作指引", + "link": "/components/steps-guide/", + "status": "50%" + }, + { + "text": "Tabs 选项卡", + "link": "/components/tabs/", + "status": "60%" + } + ] + }, + { + "text": "反馈", + "children": [ + { + "text": "Alert 警告", + "link": "/components/alert/", + "status": "已完成" + }, + { + "text": "Drawer 抽屉板", + "link": "/components/drawer/", + "status": "10%" + }, + { + "text": "Loading 加载提示", + "link": "/components/loading/", + "status": "已完成" + }, + { + "text": "Modal 弹窗", + "link": "/components/modal/", + "status": "已完成" + }, + { + "text": "Popover 悬浮提示", + "link": "/components/popover/", + "status": "已完成" + }, + { + "text": "Toast 全局提示", + "link": "/components/toast/", + "status": "已完成" + }, + { + "text": "Tooltip提示", + "link": "/components/tooltip/", + "status": "50%" + } + ] + }, + { + "text": "数据录入", + "children": [ + { + "text": "Cascader 级联菜单", + "link": "/components/cascader/", + "status": "10%" + }, + { + "text": "Checkbox 复选框", + "link": "/components/checkbox/", + "status": "已完成" + }, + { + "text": "DatePicker 日期选择器", + "link": "/components/date-picker/", + "status": "50%" + }, + { + "text": "EditableSelect 可输入下拉选择框", + "link": "/components/editable-select/", + "status": "10%" + }, + { + "text": "Input 输入框", + "link": "/components/input/", + "status": "已完成" + }, + { + "text": "InputNumber 数字输入框", + "link": "/components/input-number/", + "status": "50%" + }, + { + "text": "Radio 单选框", + "link": "/components/radio/", + "status": "已完成" + }, + { + "text": "Select 下拉框", + "link": "/components/select/", + "status": "10%" + }, + { + "text": "Slider 滑块", + "link": "/components/slider/", + "status": "80%" + }, + { + "text": "Switch 开关", + "link": "/components/switch/", + "status": "已完成" + }, + { + "text": "TagInput 标签输入框", + "link": "/components/tag-input/", + "status": "已完成" + }, + { + "text": "TimePicker 时间选择器", + "link": "/components/time-picker/", + "status": "80%" + }, + { + "text": "Transfer 穿梭框", + "link": "/components/transfer/", + "status": "10%" + }, + { + "text": "Upload 上传", + "link": "/components/upload/", + "status": "80%" + } + ] + }, + { + "text": "数据展示", + "children": [ + { + "text": "Avatar 头像", + "link": "/components/avatar/", + "status": "已完成" + }, + { + "text": "Badge 徽标", + "link": "/components/badge/", + "status": "已完成" + }, + { + "text": "Card 卡片", + "link": "/components/card/", + "status": "已完成" + }, + { + "text": "Carousel 走马灯", + "link": "/components/carousel/", + "status": "80%" + }, + { + "text": "ImagePreview 图片预览", + "link": "/components/image-preview/", + "status": "已完成" + }, + { + "text": "Progress 进度条", + "link": "/components/progress/", + "status": "已完成" + }, + { + "text": "QuadrantDiagram 象限图", + "link": "/components/quadrant-diagram/", + "status": "10%" + }, + { + "text": "Rate 评分", + "link": "/components/rate/", + "status": "已完成" + }, + { + "text": "Skeleton 骨架屏", + "link": "/components/skeleton/", + "status": "已完成" + }, + { + "text": "Table 表格", + "link": "/components/table/", + "status": "10%" + }, + { + "text": "TimeAxis 时间轴", + "link": "/components/time-axis/", + "status": "10%" + }, + { + "text": "Tree 树", + "link": "/components/tree/", + "status": "20%" + } + ] + }, + { + "text": "布局", + "children": [ + { + "text": "Layout 布局", + "link": "/components/layout/", + "status": "已完成" + }, + { + "text": "Splitter 分割器", + "link": "/components/splitter/", + "status": "已完成" + } + ] + } + ] +} diff --git a/packages/devui-vue/.yarnrc b/packages/devui-vue/.yarnrc new file mode 100644 index 0000000000000000000000000000000000000000..f4074f57d780e95aa353cc4b0bbb5a36708fa35f --- /dev/null +++ b/packages/devui-vue/.yarnrc @@ -0,0 +1 @@ +registry "https://registry.npm.taobao.org" \ No newline at end of file diff --git a/packages/devui-vue/devui/dragdrop/index.ts b/packages/devui-vue/devui/dragdrop/index.ts index 6ab571518cb6e0f43c4eb096d632cc91a9b6c1b4..b203021a07b8a3b55d17569751e945027deaa54f 100644 --- a/packages/devui-vue/devui/dragdrop/index.ts +++ b/packages/devui-vue/devui/dragdrop/index.ts @@ -1,6 +1,6 @@ import type { App } from 'vue' -import DraggableDirective from './src/draggable.directive' -import DroppableDirective from './src/droppable.directive' +import DraggableDirective from './src/draggable-directive' +import DroppableDirective from './src/droppable-directive' export { DraggableDirective, DroppableDirective } diff --git a/packages/devui-vue/devui/dragdrop/src/draggable.directive.ts b/packages/devui-vue/devui/dragdrop/src/draggable-directive.ts similarity index 100% rename from packages/devui-vue/devui/dragdrop/src/draggable.directive.ts rename to packages/devui-vue/devui/dragdrop/src/draggable-directive.ts diff --git a/packages/devui-vue/devui/dragdrop/src/droppable.directive.ts b/packages/devui-vue/devui/dragdrop/src/droppable-directive.ts similarity index 100% rename from packages/devui-vue/devui/dragdrop/src/droppable.directive.ts rename to packages/devui-vue/devui/dragdrop/src/droppable-directive.ts diff --git a/packages/devui-vue/devui/grid/__tests__/grid.spec.ts b/packages/devui-vue/devui/grid/__tests__/grid.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..fa33b9765e7ec5a8972716e9ccc95f1c53e2e097 --- /dev/null +++ b/packages/devui-vue/devui/grid/__tests__/grid.spec.ts @@ -0,0 +1,8 @@ +import { mount } from '@vue/test-utils'; +import { Row } from '../index'; + +describe('grid test', () => { + it('grid init render', async () => { + // todo + }) +}) diff --git a/packages/devui-vue/devui/grid/index.ts b/packages/devui-vue/devui/grid/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb9577156f351ec9585a16ba63a0d0d18063409c --- /dev/null +++ b/packages/devui-vue/devui/grid/index.ts @@ -0,0 +1,22 @@ +import type { App } from 'vue' +import Row from './src/row' +import Col from './src/col' + +Row.install = function(app: App): void { + app.component(Row.name, Row) +} + +Col.install = function(app: App): void { + app.component(Col.name, Col) +} +export { Row, Col } + +export default { + title: 'Grid 栅格', + category: '布局', + status: '已完成', + install(app: App): void { + app.use(Col as any) + app.use(Row as any) + } +} diff --git a/packages/devui-vue/devui/grid/src/col.scss b/packages/devui-vue/devui/grid/src/col.scss new file mode 100644 index 0000000000000000000000000000000000000000..2381135f094e193d8635eaf954fb55a366739469 --- /dev/null +++ b/packages/devui-vue/devui/grid/src/col.scss @@ -0,0 +1,64 @@ +.devui-col { + position: relative; + max-width: 100%; + min-height: 1px; +} + +@function percentage ($i, $sum: 24) { + @return $i / $sum * 100%; } + +.devui-col-span-0 { + display: none; +} + +@for $i from 1 to 24 { + .devui-col-offset-#{$i} { + margin-left: percentage($i); + } + .devui-col-pull-#{$i} { + right: percentage($i); + } + .devui-col-push-#{$i} { + left: percentage($i); + } + .devui-col-span-#{$i} { + display: block; + flex: 0 0 percentage($i); + width: percentage($i); + } + .devui-col-xs-offset-#{$i} { + margin-left: percentage($i); + } + .devui-col-xs-pull-#{$i} { + right: percentage($i); + } + .devui-col-xs-push-#{$i} { + left: percentage($i); + } + .devui-col-xs-span-#{$i} { + display: block; + flex: 0 0 percentage($i); + width: percentage($i); + } +} + +@each $size, $value in (sm, 576), (md, 768), (lg, 992), (xl, 1200), (xxl, 1600) { + @media screen and (min-width: #{$value}px) { + @for $i from 1 to 24 { + .devui-col-#{$size}-offset-#{$i} { + margin-left: percentage($i); + } + .devui-col-#{$size}-pull-#{$i} { + right: percentage($i); + } + .devui-col-#{$size}-push-#{$i} { + left: percentage($i); + } + .devui-col-#{$size}-span-#{$i} { + display: block; + flex: 0 0 percentage($i); + width: percentage($i); + } + } + } +} diff --git a/packages/devui-vue/devui/grid/src/col.tsx b/packages/devui-vue/devui/grid/src/col.tsx new file mode 100644 index 0000000000000000000000000000000000000000..596cb8a73c4dc9d6dc86d1dea8f42357a2619690 --- /dev/null +++ b/packages/devui-vue/devui/grid/src/col.tsx @@ -0,0 +1,35 @@ +import { defineComponent, computed, CSSProperties, Ref, inject } from 'vue' +import { colProps, ColProps } from './grid-types' +import { useSize, CLASS_PREFIX, useColClassNames } from './use-grid' +import './col.scss' + + +export default defineComponent({ + name: 'DCol', + props: colProps, + setup (props: ColProps, { slots }) { + + const formatFlex = (flex: typeof props.flex) => { + if (typeof flex === 'number') { + return `${flex} ${flex} auto` + } + if (/^\d+(\.\d+)?(px|rem|em|%)$/.test(flex)) { + return `0 0 ${flex}` + } + return flex + } + + const colClassNames= useColClassNames(props) + + const sizeClassNames = useSize(props) + + const colStyle = computed(() => ({ + flex: formatFlex(props.flex), + order: props.order + })) + + const gutterStyle = inject>('gutterStyle') + + return () =>
{slots.default?.()}
+ } +}) diff --git a/packages/devui-vue/devui/grid/src/grid-types.ts b/packages/devui-vue/devui/grid/src/grid-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..a6491cf262525c3f8184abac3e93dd9c568abb29 --- /dev/null +++ b/packages/devui-vue/devui/grid/src/grid-types.ts @@ -0,0 +1,70 @@ +import type { PropType, ExtractPropTypes } from 'vue' + +export type Align = 'top' | 'middle' | 'bottom' + +export type Justify = 'start' | 'end' | 'center' | 'around' | 'between' + +export interface GutterScreenSizes { + xs?: number | number[] + sm: number | number[] + md: number | number[] + lg: number | number[] + xl: number | number[] + xxl: number | number[] +} + +export const rowProps = { + align: { + type: String as PropType, + default: 'top' + }, + gutter: { + type: [Number, Object, Array] as PropType, + default: 0 + }, + justify: { + type: String as PropType, + default: 'start' + }, + wrap: { + type: Boolean as PropType, + default: false + } +} as const + +export type RowProps = ExtractPropTypes + +const screenSizesProp = [Number, Object] as PropType + +export const screenSizes = { + xs: screenSizesProp, + sm: screenSizesProp, + md: screenSizesProp, + lg: screenSizesProp, + xl: screenSizesProp, + xxl: screenSizesProp, +} as const + +const numberProp = Number as PropType + +export const colPropsBaseStyle = { + flex: [String, Number] as PropType, + order: numberProp, +} as const + +export const colPropsBaseClass = { + offset: numberProp, + pull: numberProp, + push: numberProp, + span: numberProp +} as const + +export type ColPropsBaseStyle = ExtractPropTypes + +export type ColPropsBaseClass = ExtractPropTypes + +export type ScreenSizes = ExtractPropTypes + +export const colProps = { ...colPropsBaseStyle, ...colPropsBaseClass, ...screenSizes} + +export type ColProps = ExtractPropTypes diff --git a/packages/devui-vue/devui/grid/src/row.scss b/packages/devui-vue/devui/grid/src/row.scss new file mode 100644 index 0000000000000000000000000000000000000000..b5b3df4ad0830f343e0323418894c159fe702c4f --- /dev/null +++ b/packages/devui-vue/devui/grid/src/row.scss @@ -0,0 +1,19 @@ +.devui-row { + display: flex; +} + +.devui-row-wrap { + flex-wrap: wrap; +} + +@each $prefix, $value in (top, flex-start), (middle, center), (bottom, flex-end) { + .devui-row-align-#{$prefix} { + align-items: $value; + } +} + +@each $prefix, $value in (start, flex-start), (center, center), (end, flex-end), (around, space-around), (between, space-between) { + .devui-row-justify-#{$prefix} { + justify-content: $value; + } +} diff --git a/packages/devui-vue/devui/grid/src/row.tsx b/packages/devui-vue/devui/grid/src/row.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6e6bb5b35c4ec28dea954674b458e25f99c2bed6 --- /dev/null +++ b/packages/devui-vue/devui/grid/src/row.tsx @@ -0,0 +1,69 @@ +import { defineComponent, computed, ref, Ref, CSSProperties, onMounted, onUnmounted, provide } from 'vue' +import { rowProps, RowProps } from './grid-types' +import { formatClass } from './use-grid' +import { responesScreen, Screen, RESULT_SCREEN, removeSubscribeCb } from './use-screen' +import './row.scss' + +const CLASS_PREFIX = 'devui-row' + +export default defineComponent({ + name: 'DRow', + props: rowProps, + emits: [], + setup(props: RowProps, { slots }) { + const gutterScreenSize = ref({}) + + const rowClass = computed(() => { + const alignClass = formatClass(`${CLASS_PREFIX}-align`, props.align) + const justifyClass = formatClass(`${CLASS_PREFIX}-justify`, props.justify) + const wrapClass = props.wrap ? ` ${CLASS_PREFIX}-wrap` : '' + return `${alignClass}${justifyClass}${wrapClass}` + }) + + let token + + onMounted(() => { + token = responesScreen(screen => { + gutterScreenSize.value = screen + }) + }) + + onUnmounted(() => { + removeSubscribeCb(token) + }) + + const gutterStyle = computed(() => { + if (!props.gutter) { + return {} + } + let currentGutter = [0, 0] + if (Array.isArray(props.gutter)) { + currentGutter = props.gutter as number[] + } else if (typeof props.gutter === 'number') { + currentGutter = [props.gutter as number, 0] + } else { + RESULT_SCREEN.some(size => { + const gzs = props.gutter[size] + if (gutterScreenSize.value[size] && gzs) { + if (typeof gzs === 'number') { + currentGutter = [gzs, 0] + } else { + currentGutter = gzs + } + return true + } + return false + }) + } + const paddingLeft = `${(currentGutter[0] || 0) / 2}px` + const paddingRight = `${(currentGutter[0] || 0) / 2}px` + const paddingTop = `${(currentGutter[1] || 0) / 2}px` + const paddingBottom = `${(currentGutter[1] || 0) / 2}px` + return { paddingLeft, paddingRight, paddingTop, paddingBottom } + }) + + provide>('gutterStyle', gutterStyle) + + return () =>
{slots.default?.()}
+ } +}) diff --git a/packages/devui-vue/devui/grid/src/use-grid.ts b/packages/devui-vue/devui/grid/src/use-grid.ts new file mode 100644 index 0000000000000000000000000000000000000000..a303a3624638d7767d3190ce7c4b89468c682f58 --- /dev/null +++ b/packages/devui-vue/devui/grid/src/use-grid.ts @@ -0,0 +1,47 @@ +import { computed } from 'vue' +import { ScreenSizes, ColPropsBaseClass, screenSizes, colPropsBaseClass } from './grid-types' + +export const CLASS_PREFIX = 'devui-col' + +export function formatClass (prefix: string, val: number | string | undefined) { + return val !== undefined ? ` ${prefix}-${val}` : '' +} + +export function useColClassNames (props: ColPropsBaseClass) { + return computed(() => { + const spanClass = formatClass(`${CLASS_PREFIX}-span`, props.span) + const offsetClass = formatClass(`${CLASS_PREFIX}-offset`, props.offset) + const pullClass = formatClass(`${CLASS_PREFIX}-pull`, props.pull) + const pushClass = formatClass(`${CLASS_PREFIX}-push`, props.push) + return `${spanClass}${offsetClass}${pullClass}${pushClass}` + }) +} + +function setSpace (val:string) { + return val && ` ${val.trim()} ` +} + +export function useSize (colSizes: ScreenSizes) { + const keys = Object.keys(colSizes).filter(key => key in screenSizes) as (keyof ScreenSizes)[] + return computed(() => { + return keys.reduce((total, key) => { + const valueType = typeof colSizes[key] + + if (valueType === 'number') { + total = `${setSpace(total)}${CLASS_PREFIX}-${key}-span-${colSizes[key]}` + } else if (valueType === 'object') { + const colSizesKeys = Object.keys(colSizes[key]) as (keyof ColPropsBaseClass)[] + const sum = colSizesKeys.filter(item => item in colPropsBaseClass).reduce((tot, k) => { + if (typeof colSizes[key][k] !== 'number') { + return '' + } else { + tot = `${setSpace(tot)}${CLASS_PREFIX}-${key}-${k}-${colSizes[key][k]}` + } + return tot + }, '') + total = `${setSpace(total)}${sum}` + } + return total + }, '') + }) +} diff --git a/packages/devui-vue/devui/grid/src/use-screen.ts b/packages/devui-vue/devui/grid/src/use-screen.ts new file mode 100644 index 0000000000000000000000000000000000000000..98d86f99acbe02fea94eb585b07ba9ec490b8a70 --- /dev/null +++ b/packages/devui-vue/devui/grid/src/use-screen.ts @@ -0,0 +1,84 @@ + +export const RESULT_SCREEN = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs'] + +const screenMedias = { + xs: 'screen and (max-width: 575px)', + sm: 'screen and (min-width: 576px)', + md: 'screen and (min-width: 768px)', + lg: 'screen and (min-width: 992px)', + xl: 'screen and (min-width: 1200px)', + xxl: 'screen and (min-width: 1600px)', +} as const + +export interface Screen { + xs?: boolean + sm?: boolean + md?: boolean + lg?: boolean + xl?: boolean + xxl?: boolean +} + +export type ScreenMediasKey = keyof typeof screenMedias +type SubscribeCb = (screen: Screen) => void + +const subscribers = new Map() +let subUid = -1 +const screen: Screen = {} +const results: { + [key: string]: { + res: MediaQueryList + listener: (this: MediaQueryList, ev: MediaQueryListEvent) => void + } + } = {}; + +export function responesScreen (func: SubscribeCb) { + if (!subscribers.size) { + register() + } + subUid += 1 + subscribers.set(subUid, func) + func({ ...screen }) + return subUid +} + +export function removeSubscribeCb (id: number) { + subscribers.delete(id) + if (subscribers.size === 0) { + unRegister() + } +} + +function register () { + Object.keys(screenMedias).forEach(key => { + const result = window.matchMedia(screenMedias[key]) + if (result.matches) { + screen[key as ScreenMediasKey] = true + dispatch() + } + const listener = e => { + screen[key as ScreenMediasKey] = e.matches + dispatch() + } + result.addEventListener('change', listener) + + results[key] = { + res: result, + listener + } + }) +} + +function unRegister () { + Object.keys(screenMedias).forEach(key => { + const handler = results[key] + handler.res.removeEventListener('change', handler.listener) + }) + subscribers.clear() +} + +function dispatch () { + subscribers.forEach(value => { + value({ ...screen }) + }) +} diff --git a/packages/devui-vue/devui/popover/src/popover.tsx b/packages/devui-vue/devui/popover/src/popover.tsx index f467f8b8808b3ff36751ea7b45d8df554e10d338..d0b367c3efb2e16373162adb9ded6e6084ff8e13 100644 --- a/packages/devui-vue/devui/popover/src/popover.tsx +++ b/packages/devui-vue/devui/popover/src/popover.tsx @@ -16,7 +16,7 @@ const popTypeClass = { export default defineComponent({ name: 'DPopover', - + directives: { clickoutside: clickoutsideDirective }, @@ -81,7 +81,11 @@ export default defineComponent({ setup(props, ctx) { const { slots } = ctx; const visible = ref(props.visible); - const { position, content, zIndex, trigger, popType, popoverStyle, mouseEnterDelay, mouseLeaveDelay, showAnimation, popMaxWidth } = toRefs(props); + const { + position, content, zIndex, trigger, popType, + popoverStyle, mouseEnterDelay, mouseLeaveDelay, + showAnimation, popMaxWidth + } = toRefs(props); const style: CSSProperties = { zIndex: zIndex.value, ...popoverStyle.value } const isClick = trigger.value === 'click' const iconType = reactive(popTypeClass[popType.value]) @@ -110,7 +114,12 @@ export default defineComponent({ 'devui-popover-isVisible': visible.value } ]} > -
+
{slots.reference?.()}
diff --git a/packages/devui-vue/devui/read-tip/__tests__/read-tip.spec.ts b/packages/devui-vue/devui/read-tip/__tests__/read-tip.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..fae43c0e63a45a3f7dca07b4e5b4a02c22c6f84c --- /dev/null +++ b/packages/devui-vue/devui/read-tip/__tests__/read-tip.spec.ts @@ -0,0 +1,8 @@ +import { mount } from '@vue/test-utils'; +import { ReadTip, ReadTipDirective } from '../index'; + +describe('read-tip test', () => { + it('read-tip init render', async () => { + // todo + }) +}) diff --git a/packages/devui-vue/devui/read-tip/index.ts b/packages/devui-vue/devui/read-tip/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..db1b532c6fda5e7fcce0288f9baa6f3d312885f1 --- /dev/null +++ b/packages/devui-vue/devui/read-tip/index.ts @@ -0,0 +1,17 @@ +import type { App } from 'vue' +import ReadTip from './src/read-tip' + +ReadTip.install = function(app: App): void { + app.component(ReadTip.name, ReadTip) +} + +export { ReadTip, } + +export default { + title: 'ReadTip 阅读提示', + category: '反馈', + status: '30%', // TODO: 组件若开发完成则填入"已完成",并删除该注释 + install(app: App): void { + app.use(ReadTip as any) + } +} diff --git a/packages/devui-vue/devui/read-tip/src/read-tip-directive.ts b/packages/devui-vue/devui/read-tip/src/read-tip-directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..788dde30eedb905e475ad731757230cb936f4d3a --- /dev/null +++ b/packages/devui-vue/devui/read-tip/src/read-tip-directive.ts @@ -0,0 +1,10 @@ +// can export function. +export default { + // created() { }, + // beforeMount() { }, + // mounted() { }, + // beforeUpdate() { }, + // updated() { }, + // beforeUnmount() { }, + // unmounted() { } +} diff --git a/packages/devui-vue/devui/read-tip/src/read-tip-template.tsx b/packages/devui-vue/devui/read-tip/src/read-tip-template.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9ef7afb9187703e179d1c1f64622da87df0a0db7 --- /dev/null +++ b/packages/devui-vue/devui/read-tip/src/read-tip-template.tsx @@ -0,0 +1,52 @@ +import { defineComponent, ref, onMounted, Teleport } from 'vue' +import { readTipProps, ReadTipProps, } from './read-tip-types' +import './read-tip.scss' + +export default defineComponent({ + name: 'DReadTipTemplate', + props: readTipProps, + emits: [], + setup(props: ReadTipProps, ctx) { + const query = props.defaultTemplateProps?.id ? `#${props.defaultTemplateProps.id}` : props.defaultTemplateProps.selector + + const temp = ref(null) + onMounted(() => { + // 当定位为top展示时 元素定位高度需要计算弹窗的高度 + if(props.defaultTemplateProps.position == 'top') { + temp.value.style.top = (- temp.value.offsetHeight - 10) + 'px' + } + }) + return () => { + return ( + +
+ + { + props.defaultTemplateProps.contentTemplate ? ctx.slots?.default() : + ( + <> +
+ {props.defaultTemplateProps.title} +
+
+ {props.defaultTemplateProps.content} +
+ + ) + } + + {/* { + ctx.slots?.default() + } */} + +
+
+ + + ) + } + } +}) diff --git a/packages/devui-vue/devui/read-tip/src/read-tip-types.ts b/packages/devui-vue/devui/read-tip/src/read-tip-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..422d29909a14880fc5d8dd19b8061a773d07ca69 --- /dev/null +++ b/packages/devui-vue/devui/read-tip/src/read-tip-types.ts @@ -0,0 +1,59 @@ +import type { PropType, ExtractPropTypes } from 'vue' + +export const readTipProps = { + readTipOptions: { + type: Object as PropType + }, + defaultTemplateProps: { + type: Object as PropType + } +} as const + +export type Position = 'top' | 'left' | 'right' | 'bottom' +export type Trigger = 'hover' | 'click' + +export type DefaultTemplateProps = { + title?: string + content?: string + top?: number + selector?: string + position?: string + id? : string + temp: string + contentTemplate: boolean +} + +export interface ReadTipOptions { + trigger?: Trigger + showAnimate?: boolean + mouseenterTime?: number + mouseleaveTime?: number + position?: Position + overlayClassName?: string + appendToBody?: boolean + rules: ReadTipRules +} + +export type ReadTipRules = ReadTipRule | ReadTipRule[]; + +export interface ReadTipRule { + key?: string + selector: string + trigger?: Trigger + title?: string + content?: string + showAnimate?: boolean + mouseenterTime?: number + mouseleaveTime?: number + position?: Position + overlayClassName?: string + appendToBody?: boolean + //customData与template搭配使用,customData为传入模板的上下文,可以自定义模板内容 + // dataFn?: ({ + // element, + // rule: ReadTipRule, + // }) => Observable<{ title?: string; content?: string; template?: TemplateRef; customData?: any }> +} + + +export type ReadTipProps = ExtractPropTypes diff --git a/packages/devui-vue/devui/read-tip/src/read-tip.scss b/packages/devui-vue/devui/read-tip/src/read-tip.scss new file mode 100644 index 0000000000000000000000000000000000000000..e113de163cc787ef65f0dcbe8cee85c856fa6dc5 --- /dev/null +++ b/packages/devui-vue/devui/read-tip/src/read-tip.scss @@ -0,0 +1,79 @@ +@import '../../style/theme/color'; +@import '../../style/theme/variables'; +@import '../../style/mixins/index'; +@import '../../style/theme/shadow'; +@import '../../style/theme/corner'; +@import '../../style/core/_font'; +// @import '../../style/core/z-index'; +.devui-read-tip { + position: relative; +} + +.source { + overflow: initial; +} + +.read-tip-container { + font-size: $devui-font-size; + position: absolute; + width: max-content; + height: max-content; + // top: 0; + // left: 0; + line-height: 1.5; + border: none; + border-radius: $devui-border-radius-feedback; + // z-index: $devui-z-index-pop-up; + background-color: $devui-feedback-overlay-bg; + color: $devui-feedback-overlay-text; + overflow-wrap: break-word; + padding: 10px; + z-index: 50; + + .after { + content: ''; + width: 12px; + height: 12px; + transform: rotate(45deg); + position: absolute; + background-color: $devui-feedback-overlay-bg; + } + // 定位 top + &.top { + top: -60px; + left: 0; + + .after { + bottom: -4px; + } + } + + // 定位 left + &.left { + right: calc(100% + 10px); + top: -2px; + + .after { + right: -4px; + } + } + + // 定位 right + &.right { + left: calc(100% + 10px); + top: -2px; + + .after { + left: -4px; + } + } + + // 定位 bottom + &.bottom { + top: calc(100% + 10px); + + .after { + top: -4px; + } + } +} diff --git a/packages/devui-vue/devui/read-tip/src/read-tip.tsx b/packages/devui-vue/devui/read-tip/src/read-tip.tsx new file mode 100644 index 0000000000000000000000000000000000000000..74333c4dbd8d756fb44aaf932a2a8c4ff4adfc1b --- /dev/null +++ b/packages/devui-vue/devui/read-tip/src/read-tip.tsx @@ -0,0 +1,163 @@ +import { defineComponent, ref, onMounted, reactive, Teleport, onUnmounted } from 'vue' +import { readTipProps, ReadTipProps, ReadTipOptions } from './read-tip-types' +import './read-tip.scss' +import TipsTemplate from './read-tip-template'; + +export default defineComponent({ + name: 'DReadTip', + props: readTipProps, + emits: [], + setup(props: ReadTipProps, ctx) { + // 默认配置 + const defaultOptions: ReadTipOptions = { + trigger: 'hover', + showAnimate: false, + mouseenterTime: 100, + mouseleaveTime: 100, + position: 'top', + overlayClassName: '', + appendToBody: true, + rules: { selector: null }, + }; + const tempTop = ref(0) + // 合并基础配置 + const options = { ...defaultOptions, ...props.readTipOptions } + const defaultSlot = ref(null) + const onMouseenter = (rule) => () => { + setTimeout(() => { + if (rule.id) { + const a = refRules.find(u => u.id === rule.id) + a.status = true + } + rule.status = true + }, rule.mouseenterTime || options.mouseenterTime); + } + const onMouseleave = (rule) => () => { + setTimeout(() => { + if (rule.id) { + const a = refRules.find(u => u.id === rule.id) + a.status = false + } + rule.status = false + + }, rule.mouseleaveTime || options.mouseleaveTime); + } + + const init = (rules, trigger = 'hover') => { + rules.map(rule => { + rule.status = false + trigger = rule.trigger || trigger + rule.contentTemplate = !!(ctx.slots.contentTemplate) + const doms = defaultSlot.value.querySelectorAll(rule.selector); + [...doms].map((dom, index) => { + dom.style.position = 'relative' + + let newRule = reactive({ + id: null + }) + if (index > 0) { + newRule = { ...rule } + dom.id = rule.selector.slice(1) + index + newRule.id = rule.selector.slice(1) + index + rules.push(newRule) + } + + if (trigger === 'hover') { + dom.addEventListener('mouseenter', onMouseenter(newRule.id ? newRule : rule,)) + dom.addEventListener('mouseleave', onMouseleave(newRule.id ? newRule : rule)) + } + }) + + }) + return rules + } + function show(dom, rule) { + const top = dom.offsetTop + rule.status = true + } + // 把传入的props.rules统一转为数组对象格式 + const rules = (rules) => { + if (rules === null) return + if (typeof rules === 'object' && !Array.isArray(rules)) { + rules = [rules] + } + rules = [...rules] + Array.isArray(rules) && rules.map(rule => { + rule.status = false + }) + return rules + } + const refRules = reactive(rules(options.rules)) + const clickFn = () => { + refRules.forEach(element => { + element.status = false + }) + } + onMounted(() => { + init(refRules, options.trigger) + // 点击其他位置 关闭弹框 + document.addEventListener('click', clickFn, true) + + }) + + onUnmounted(() => { + // 取消事件 + document.removeEventListener('click', clickFn) + }) + + // 添加点击事件 当前元素是click事件目标则弹框展示 + const onClick = (e: Event) => { + for (const rule of refRules) { + const doms = defaultSlot.value.querySelectorAll(rule.selector); + for (const dom of doms) { + if (doms.length > 1) { + if (dom === e.target && rule.id) { + show(dom, rule) + return + + } else if (dom === e.target && !rule.id && !dom.id) { + show(dom, rule) + return + } + + } else + + if (dom === e.target) { + show(dom, rule) + return + } else { + rule.status = false + } + } + + } + + } + return () => { + return (
+
+ { + ctx.slots?.default() + } +
+ + {(refRules).map(rule => ( +
+ {rule.status && ( + { + rule.contentTemplate && ctx.slots?.contentTemplate() + } + ) + + } +
+ ) + )} +
) + } + } +}) diff --git a/packages/devui-vue/devui/slider/index.ts b/packages/devui-vue/devui/slider/index.ts index 8138b2bac28521d81f60eb1197a83846aae2e1b5..f4596b7734eec02a4e6729d10d96a3013edc3f4b 100644 --- a/packages/devui-vue/devui/slider/index.ts +++ b/packages/devui-vue/devui/slider/index.ts @@ -10,7 +10,7 @@ export { Slider } export default { title: 'Slider 滑块', category: '数据录入', - status: '80%', + status: '已完成', install(app: App): void { app.use(Slider as any) } diff --git a/packages/devui-vue/devui/slider/src/slider.tsx b/packages/devui-vue/devui/slider/src/slider.tsx index 0e4fc5f38d21de3710fcaaa55966c1e15c564d1b..3619794833444f51fc55bf6c2b6726a9b1b9744a 100644 --- a/packages/devui-vue/devui/slider/src/slider.tsx +++ b/packages/devui-vue/devui/slider/src/slider.tsx @@ -40,10 +40,10 @@ export default defineComponent({ const sliderWidth = sliderRunway.value.clientWidth; currentPosition.value = (sliderWidth * (inputValue.value - props.min)) / (props.max - props.min); }); - function handleonMousedown(event: MouseEvent) { + function handleButtonMousedown(event: MouseEvent) { popoverShow.value = true; //props.disabled状态是不能点击拖拽的 - if (props.disabled || props.disabled) return; + if (props.disabled) return; //阻止默认事件 event.preventDefault(); dragStart(event); @@ -98,7 +98,6 @@ export default defineComponent({ const steps = Math.round(newPosition / LengthPerStep); //实际的偏移像素 const value: number = steps * LengthPerStep; - //要是刚好划过半段切刚好超出最大长度的情况进行限定 if (Math.round(value) >= sliderWidth) { currentPosition.value = sliderWidth; @@ -115,12 +114,13 @@ export default defineComponent({ currentPosition.value = newPosition; ctx.emit('update:modelValue', inputValue.value); } - //当点击滑动条时, - function handleClick(event) { + //当在滑动条触发鼠标事件时处理, + function handleRunwayMousedown(event) { if (!props.disabled && isClick) { startX = event.target.getBoundingClientRect().left; const currentX = event.clientX; setPostion(currentX - startX); + handleButtonMousedown(event); } else { return; } @@ -160,13 +160,17 @@ export default defineComponent({ return () => (
{/* 整个的长度 */} -
+
(popoverShow.value = false)} + > {/* 滑动后左边的进度条 */}
(popoverShow.value = true)} onMouseout={() => (popoverShow.value = false)} >
diff --git a/packages/devui-vue/devui/tag/__tests__/tag.spec.ts b/packages/devui-vue/devui/tag/__tests__/tag.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..9db389e4d2fa8c9e9040fc451832c6ddb99a3f26 --- /dev/null +++ b/packages/devui-vue/devui/tag/__tests__/tag.spec.ts @@ -0,0 +1,8 @@ +import { mount } from '@vue/test-utils' +import { Tag } from '../index' + +describe('tag test', () => { + it('tag init render', async () => { + // todo + }) +}) diff --git a/packages/devui-vue/devui/tag/index.ts b/packages/devui-vue/devui/tag/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..43310285f5c37a45fdbee30011f4ca0b4bb64b92 --- /dev/null +++ b/packages/devui-vue/devui/tag/index.ts @@ -0,0 +1,17 @@ +import type { App } from 'vue' +import Tag from './src/tag' + +Tag.install = function (app: App): void { + app.component(Tag.name, Tag) +} + +export { Tag } + +export default { + title: 'Tag 标签', + category: '数据展示', + status: '20%', // TODO: 组件若开发完成则填入"已完成",并删除该注释 + install(app: App): void { + app.use(Tag as any) + } +} diff --git a/packages/devui-vue/devui/tag/src/hooks/index.ts b/packages/devui-vue/devui/tag/src/hooks/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..8931ccc412a0f807e375da1330bdb8b66af15299 --- /dev/null +++ b/packages/devui-vue/devui/tag/src/hooks/index.ts @@ -0,0 +1,3 @@ +import useStyle from './useStyle' + +export { useStyle } diff --git a/packages/devui-vue/devui/tag/src/hooks/useStyle.ts b/packages/devui-vue/devui/tag/src/hooks/useStyle.ts new file mode 100644 index 0000000000000000000000000000000000000000..2dbb29d72cc7711ee2481484387b90a5ae9d0a01 --- /dev/null +++ b/packages/devui-vue/devui/tag/src/hooks/useStyle.ts @@ -0,0 +1,10 @@ +import { computed } from 'vue' +import { tagProps, TagProps } from '../tag-types' + +export default function (props: TagProps) { + return computed(() => { + const { type } = props + + return `devui-tag devui-tag-${type || 'default'}` + }) +} diff --git a/packages/devui-vue/devui/tag/src/tag-types.ts b/packages/devui-vue/devui/tag/src/tag-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..f8fd95a57f4f119dcc6f936a55362e4121b9a462 --- /dev/null +++ b/packages/devui-vue/devui/tag/src/tag-types.ts @@ -0,0 +1,16 @@ +import type { PropType, ExtractPropTypes } from 'vue' + +export type tagType = 'primary' | 'success' | 'warning' | 'danger' + +export const tagProps = { + type: { + type: String as PropType, + default: '' + }, + color: { + type: String as PropType, + default: '' + } +} as const + +export type TagProps = ExtractPropTypes diff --git a/packages/devui-vue/devui/tag/src/tag.scss b/packages/devui-vue/devui/tag/src/tag.scss new file mode 100644 index 0000000000000000000000000000000000000000..bd3e3c7a3f55afe0603b34768a4b44812f59ded4 --- /dev/null +++ b/packages/devui-vue/devui/tag/src/tag.scss @@ -0,0 +1,56 @@ +@import '@devui/styles-var/devui-var'; + +$devui-tag-normal-config: ( + default: ( + border: 0, + color: $devui-text, + background-color: $devui-default-bg + ), + primary: ( + color: $devui-primary, + background-color: $devui-primary-bg, + border-color: $devui-primary-line + ), + success: ( + color: $devui-success, + background-color: $devui-success-bg, + border-color: $devui-success-line + ), + warning: ( + color: $devui-warning, + background-color: $devui-warning-bg, + border-color: $devui-warning-line + ), + danger: ( + color: $devui-danger, + background-color: $devui-danger-bg, + border-color: $devui-danger-line + ) +); + +.devui-tag { + display: inline-block; + margin: 4px; + + .devui-tag { + padding: 0 8px; + min-height: 20px; + font-size: 12px; + font-size: var(--devui-font-size, 12px); + line-height: 20px; + border: 1px solid; + border-radius: $devui-border-radius; + display: block; + align-items: center; + position: relative; + cursor: default; + + @each $type in default, primary, success, warning, danger { + &.devui-tag-#{$type} { + @each $key, $value in map-get($devui-tag-normal-config, $type) { + #{$key}: $value; + } + } + } + } +} diff --git a/packages/devui-vue/devui/tag/src/tag.tsx b/packages/devui-vue/devui/tag/src/tag.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a04ac737d1eb5f53d3084b739b3f55ef58f28d6b --- /dev/null +++ b/packages/devui-vue/devui/tag/src/tag.tsx @@ -0,0 +1,23 @@ +import { defineComponent, ref, toRefs, computed } from 'vue' +import { tagProps, TagProps } from './tag-types' +import { useStyle } from './hooks' +import './tag.scss' +// 类型声明 + +export default defineComponent({ + name: 'DTag', + props: tagProps, + emits: [], + setup(props: TagProps, { slots }) { + //获取type对应样式 + const tagClass = useStyle(props) + + return () => ( +
+ + {slots.default?.()} + +
+ ) + } +}) diff --git a/packages/devui-vue/devui/time-picker/__tests__/time-picker.spec.ts b/packages/devui-vue/devui/time-picker/__tests__/time-picker.spec.ts index 26f607222f33c227a8688a7b17638a415812c7d0..865b5c9d457cc1ced0481fa41b81c1120e2450e2 100644 --- a/packages/devui-vue/devui/time-picker/__tests__/time-picker.spec.ts +++ b/packages/devui-vue/devui/time-picker/__tests__/time-picker.spec.ts @@ -1,8 +1,204 @@ import { mount } from '@vue/test-utils'; -import { TimePicker } from '../index'; +import DTimePicker from '../src/time-picker'; +import { nextTick, ref } from 'vue'; describe('time-picker test', () => { it('time-picker init render', async () => { - // todo + + const wrapper = mount({ + components: { DTimePicker }, + template: ``, + + setup() { + return + } + }); + + const container = wrapper.find('.devui-time-picker'); + const timeUl = wrapper.findAll('.time-ul') + await nextTick() + expect(timeUl[0].element.childElementCount).toBe(24) + expect(timeUl[1].element.childElementCount).toBe(60) + expect(timeUl[2].element.childElementCount).toBe(60) + expect(container.classes()).toContain('devui-time-picker') + }) + + it('time-picker default open work', async () => { + + const wrapper = mount({ + components: { DTimePicker }, + template: ``, + setup() { + + const vModelValue = ref('12:30:40') + const autoOpen = ref(true) + + return { + vModelValue, + autoOpen + } + } + }); + + const timeInput = wrapper.find('.time-input') + const tiemPopup = wrapper.find('.devui-time-popup') + + await nextTick() + expect(timeInput.element.value).toBe('12:30:40') + expect(tiemPopup.classes()).toContain('devui-show-time-popup') + expect(tiemPopup.attributes('style')).toMatch('width: 300px'); + + }); + + it('time-picker disabled work', async () => { + const wrapper = mount(DTimePicker, { + props: { + disabled: true, + }, + }); + + expect(wrapper.find('.devui-time-picker').classes()).toContain('picker-disabled') + expect(wrapper.find('.time-input').element.disabled).toBe(true) + }) + + it('time-picker min-time work', async () => { + const wrapper = mount({ + components: { DTimePicker }, + template: ``, + setup() { + const vModelValue = ref('01:03:00') + return { + vModelValue + } + } + }) + + const timeInput = wrapper.find('.time-input') + await nextTick() + // 如果 v-mode 的时间超出 限制范围,将返回最小时间值 + expect(timeInput.element.value).toBe('01:04:30') + }) + + it('time-picker max-time work', async () => { + const wrapper = mount({ + components: { DTimePicker }, + template: ``, + setup() { + const vModelValue = ref('23:30:00') + return { + vModelValue + } + } + }) + + const timeInput = wrapper.find('.time-input') + await nextTick() + expect(timeInput.element.value).toBe('22:46:20') + }) + + it('time-picker format mm:HH:ss work', async () => { + const wrapper = mount({ + components: { DTimePicker }, + template: ``, + setup() { + const vModelValue = ref('23:30:00') + return { + vModelValue + } + } + }) + + const timeInput = wrapper.find('.time-input') + await nextTick() + expect(timeInput.element.value).toBe('30:23:00') + }) + + it('time-picker format mm:ss work', async () => { + const wrapper = mount({ + components: { DTimePicker }, + template: ``, + setup() { + const vModelValue = ref('23:30:00') + return { + vModelValue + } + } + }) + + const timeInput = wrapper.find('.time-input') + await nextTick() + expect(timeInput.element.value).toBe('30:00') + }) + + it('time-picker format hh:mm work', async () => { + const wrapper = mount({ + components: { DTimePicker }, + template: ``, + setup() { + const vModelValue = ref('23:30:00') + return { + vModelValue + } + } + }) + + const timeInput = wrapper.find('.time-input') + await nextTick() + expect(timeInput.element.value).toBe('23:30') + }) + + it('time-picker slot customViewTemplate work', async () => { + + const slotDemo = ref() + + const chooseTime = () => { + const timeObj = { + time: '21', + type: 'mm' + } + slotDemo.value.chooseTime(timeObj) + } + // 插槽内方法 -- 选择当前时间 + const chooseNowFun = () => { + const date = new Date() + const hour = date.getHours() > 9 ? date.getHours() : '0' + date.getHours() + const minute = date.getMinutes() > 9 ? date.getMinutes() : '0' + date.getMinutes() + const second = date.getSeconds() > 9 ? date.getSeconds() : '0' + date.getSeconds() + const timeObj = { + time: hour + ':' + minute + ':' + second + } + slotDemo.value.chooseTime(timeObj) + + return timeObj.time + } + + const wrapper = mount({ + components: { DTimePicker }, + template: ` + + + `, + setup() { + return { + chooseNowFun, + chooseTime, + slotDemo + } + } + }) + + const timeInput = wrapper.find('.time-input') + const slotBottomNow = wrapper.find('.slot-bottom-now') + const slotBottomOne = wrapper.find('.slot-bottom-one') + await slotBottomNow.trigger('click') + expect(timeInput.element.value).toBe(chooseNowFun()) + + await slotBottomOne.trigger('click') + expect(timeInput.element.value).toMatch(/21/) }) }) diff --git a/packages/devui-vue/devui/time-picker/index.ts b/packages/devui-vue/devui/time-picker/index.ts index 7535762542f21f511e69c6dcd2f056a6c5ba224f..85c682ab4d4724704f772c4c0ab14652024dd2c9 100644 --- a/packages/devui-vue/devui/time-picker/index.ts +++ b/packages/devui-vue/devui/time-picker/index.ts @@ -10,7 +10,7 @@ export { TimePicker } export default { title: 'TimePicker 时间选择器', category: '数据录入', - status: '80%', // TODO: 组件若开发完成则填入"已完成",并删除该注释 + status: '90%', // TODO: 组件若开发完成则填入"已完成",并删除该注释 install(app: App): void { app.use(TimePicker as any) } diff --git a/packages/devui-vue/devui/time-picker/src/composables/use-time-picker.ts b/packages/devui-vue/devui/time-picker/src/composables/use-time-picker.ts index f80fdedb43b5463499f00facb82bcce7e2a01c1d..9b2f58b9dfcb0998870e73930f57ee2ec1a6e027 100644 --- a/packages/devui-vue/devui/time-picker/src/composables/use-time-picker.ts +++ b/packages/devui-vue/devui/time-picker/src/composables/use-time-picker.ts @@ -3,7 +3,7 @@ import { TimeObj } from '../types' import { getPositionFun } from '../utils' export default function useTimePicker( - hh:Ref,mm:Ref,ss:Ref,minTime:string,format:string, + hh:Ref,mm:Ref,ss:Ref,minTime:string,maxTime:string,format:string, autoOpen:boolean,disabled:boolean,value:string ):any{ const isActive = ref(false) @@ -37,19 +37,42 @@ export default function useTimePicker( const mouseInIputFun = ()=>{ if(firsthandActiveTime.value == '00:00:00'){ + const vModelValueArr = vModeValue.value.split(':') + const minTimeValueArr = minTime.split(':') + vModeValue.value == '' ? vModeValue.value = '00:00:00' : '' - - vModeValue.value > minTime - ? firsthandActiveTime.value = vModeValue.value - : firsthandActiveTime.value = minTime + + if( vModeValue.value > minTime ){ + firsthandActiveTime.value = vModeValue.value + setInputValue(vModelValueArr[0],vModelValueArr[1],vModelValueArr[2]) + }else{ + firsthandActiveTime.value = minTime + setInputValue(minTimeValueArr[0],minTimeValueArr[1],minTimeValueArr[2]) + } } - setInputValue() + isActive.value = true showPopup.value = true } + /** + * 判断v-model 绑定的时间是否超出 最大值 最小值 范围 + * 如果带有格式化 , 将执行格式化 + * */ + const vModelIsBeyond = ()=>{ + if(vModeValue.value != '' && vModeValue.value < minTime){ + vModeValue.value = minTime + }else if( vModeValue.value != '' && vModeValue.value > maxTime ){ + vModeValue.value = maxTime + } + + const vModelValueArr = vModeValue.value.split(':') + vModeValue.value && setInputValue(vModelValueArr[0],vModelValueArr[1],vModelValueArr[2]) + } + + const getTimeValue = (e:MouseEvent)=>{ e.stopPropagation() if(showPopup.value){ @@ -57,19 +80,19 @@ export default function useTimePicker( mm.value = timePopupDom.value.changTimeData().activeMinute.value ss.value = timePopupDom.value.changTimeData().activeSecond.value firsthandActiveTime.value = `${hh.value}:${mm.value}:${ss.value}` - setInputValue() + setInputValue(hh.value,mm.value,ss.value) } } - const setInputValue = ()=> { + const setInputValue = (hh:string,mm:string,ss:string)=> { if(format == 'hh:mm:ss'){ - vModeValue.value = `${hh.value}:${mm.value}:${ss.value}` + vModeValue.value = `${hh}:${mm}:${ss}` }else if(format == 'mm:hh:ss'){ - vModeValue.value = `${mm.value}:${hh.value}:${ss.value}` + vModeValue.value = `${mm}:${hh}:${ss}` }else if(format == 'hh:mm'){ - vModeValue.value = `${hh.value}:${mm.value}` + vModeValue.value = `${hh}:${mm}` }else if(format == 'mm:ss'){ - vModeValue.value = `${mm.value}:${ss.value}` + vModeValue.value = `${mm}:${ss}` } } @@ -88,7 +111,7 @@ export default function useTimePicker( ss.value = '00' } firsthandActiveTime.value = `${hh.value}:${mm.value}:${ss.value}` - setInputValue() + setInputValue(hh.value,mm.value,ss.value) } const isOutOpen =()=>{ @@ -100,7 +123,9 @@ export default function useTimePicker( ss.value = timeArr[2] firsthandActiveTime.value = vModeValue.value - setInputValue() + + setInputValue(hh.value,mm.value,ss.value) + isActive.value = true showPopup.value = autoOpen } @@ -117,14 +142,14 @@ export default function useTimePicker( ss.value = slotTime.time } firsthandActiveTime.value = `${hh.value}:${mm.value}:${ss.value}` - setInputValue() + setInputValue(hh.value,mm.value,ss.value) } else { const timeArr = slotTime.time.split(':') hh.value = timeArr[0] mm.value = timeArr[1] ss.value = timeArr[2] firsthandActiveTime.value = `${hh.value}:${mm.value}:${ss.value}` - setInputValue() + setInputValue(hh.value,mm.value,ss.value) } } @@ -140,10 +165,10 @@ export default function useTimePicker( firsthandActiveTime, vModeValue, getPopupPosition, - setInputValue, getTimeValue, clickVerifyFun, isOutOpen, + vModelIsBeyond, clearAll, chooseTime } diff --git a/packages/devui-vue/devui/time-picker/src/time-picker.tsx b/packages/devui-vue/devui/time-picker/src/time-picker.tsx index 857ca20e7f249e538172abdd084ee66a0c146c95..15be03e08c1ec8b8f58aff5dd79aabb5a4f48158 100644 --- a/packages/devui-vue/devui/time-picker/src/time-picker.tsx +++ b/packages/devui-vue/devui/time-picker/src/time-picker.tsx @@ -30,11 +30,12 @@ export default defineComponent({ getTimeValue, clickVerifyFun, isOutOpen, + vModelIsBeyond, clearAll, timePopupDom, vModeValue, getPopupPosition - } = useTimePicker(activeHour,activeMinute,activeSecond,props.minTime,format,props.autoOpen,props.disabled,props.modelValue) + } = useTimePicker(activeHour,activeMinute,activeSecond,props.minTime,props.maxTime,format,props.autoOpen,props.disabled,props.modelValue) const selectedTimeChage = (e:MouseEvent) => { @@ -46,6 +47,7 @@ export default defineComponent({ onMounted(() => { getPopupPosition() isOutOpen() + vModelIsBeyond() document.addEventListener('click', clickVerifyFun) document.addEventListener('click',getTimeValue) document.addEventListener('scroll',getPopupPosition) @@ -60,7 +62,7 @@ export default defineComponent({ watch(vModeValue,(newValue:string)=>{ ctx.emit('update:modelValue',vModeValue.value) - if(newValue != props.minTime){ + if(newValue != props.minTime && newValue != '00:00'){ showClearIcon.value = true }else{ showClearIcon.value = false diff --git a/packages/devui-vue/docs/components/grid/index.md b/packages/devui-vue/docs/components/grid/index.md new file mode 100644 index 0000000000000000000000000000000000000000..2a67551b3e7570f72b8bff6ff2f0fa15a06f8c87 --- /dev/null +++ b/packages/devui-vue/docs/components/grid/index.md @@ -0,0 +1,316 @@ +# Grid 栅格 + +24栅格系统。 +### 何时使用 + +需要使用弹性布局时,并且需要适配不同的屏幕时,使用grid组件。 + + +### 基本用法 + +基础栅格 + +:::demo 使用 Row 和 Col组件,可以创建一个基本的栅格系统,Col必须放在Row里面。 + +```vue + + + +``` + +::: + +### 对齐 + +垂直对齐和水平对齐 + +:::demo 使用Row的align属性和justify属性子元素垂直对齐和水平对齐。 + +```vue + + +``` + +::: + + +### 子元素的间隔 + +栅格之间的间隔可以用Row的gutter属性 + +:::demo :gutter="10" 子元素左右间隔为 5px;:gutter="[10, 20]" 子元素左右间隔为5px,上下间隔为10px;需要适配屏幕宽度的情况,:gutter="{ xs: 10, sm: 20, md: [20, 10], lg: [30, 20], xl: [40, 30], xxl: [50, 40] }" + +```vue + + + +``` + +::: + +### flex填充 + +Col的flex属性支持flex填充。 + +:::demo + +```vue + +``` + +::: + +### 左右偏移 + +使用Col的offset、pull和push来使子元素左右偏移。 + +:::demo 列偏移。使用 offset 可以将列向右侧偏。例如,offset={4} 将元素向右侧偏移了 4 个列(column)的宽度;offset、pull、push也可以内嵌到。 + +```vue + +``` + +::: + +### 响应式布局 + +预设六个响应尺寸:xs sm md lg xl xxl。 + +:::demo 参照 Bootstrap 的 [响应式设计](https://getbootstrap.com/docs/3.4/css/)。 + +```vue + +``` + +::: + +### 栅格排序 + +列排序。通过使用order、 push 和 pull 类就可以改变列(column)的顺。 + +:::demo 参照 Bootstrap 的 [响应式设计](https://getbootstrap.com/docs/3.4/css/)。 + +```vue + +``` + +::: + +### d-grid + +d-row 参数 + +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | 全局配置项 | +| ---- | ---- | ---- | ---- | --------- | --------- | +| align | `string` | `'top'` | flex 布局下的垂直对齐方式:`'top'`,`'middle'`,`'bottom'` | [垂直对齐](#垂直对齐) | | +| justify | `string` | `'start'` | flex 布局下的垂直对齐方式:`'start'`,`'end'`,`'center'`,`'space-around'`,`'space-between'` | [垂直对齐](#垂直对齐) | | +| gutter | `number\|array\|object` | 0 | 栅格间隔,数值形式:水平间距。对象形式支持响应式: { xs: 8, sm: 16, md: 24}。数组形式:[水平间距, 垂直间距]。 | [对齐](#对齐) | | +| wrap | `boolean` | false | 是否自动换行 | | | + +d-col 参数 + +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | 全局配置项 | +| ---- | ---- | ---- | ---- | --------- | --------- | +| span | `number` | - | 栅格占位格数,为 0 时相当于 display: none | [基本用法](#基本用法) | | +| flex | `string\|number` | - | flex 布局填充 | [flex填充](#flex填充) | | +| offset | `number` | - | 栅格左侧的间隔格数,间隔内不可以有栅格 | [左右偏移](#左右偏移) | +| pull | `number` | - | 栅格向左移动格数 | [左右偏移](#左右偏移)、[栅格排序](#栅格排序) | +| push | `number` | - | 栅格向右移动格数 | [左右偏移](#左右偏移)、[栅格排序](#栅格排序) | +| order | `number` | - | 栅格顺序,flex 布局模式下有效 | [栅格排序](#栅格排序) | +| xs | `number\|object` | - | <576px 响应式栅格,可为栅格数或一个包含其他属性的对象 | [栅格排序](#栅格排序) | +| sm | `number\|object` | - | >=576px 响应式栅格,可为栅格数或一个包含其他属性的对象 | [响应式布局](#响应式布局) | +| md | `number\|object` | - | >=768px 响应式栅格,可为栅格数或一个包含其他属性的对象 | [响应式布局](#响应式布局) | +| lg | `number\|object` | - | >=992px 响应式栅格,可为栅格数或一个包含其他属性的对象 | [响应式布局](#响应式布局) | +| xl | `number\|object` | - | >=1200px 响应式栅格,可为栅格数或一个包含其他属性的对象 | [响应式布局](#响应式布局) | +| xxl | `number\|object` | - | >=1600px 响应式栅格,可为栅格数或一个包含其他属性的对象 | [响应式布局](#响应式布局) | \ No newline at end of file diff --git a/packages/devui-vue/docs/components/read-tip/index.md b/packages/devui-vue/docs/components/read-tip/index.md new file mode 100644 index 0000000000000000000000000000000000000000..d3c4c955d939359bd24083daa106f6cb5c54c3ce --- /dev/null +++ b/packages/devui-vue/docs/components/read-tip/index.md @@ -0,0 +1,194 @@ +# ReadTip 阅读提示 + +阅读提示组件。 + +### 何时使用 + +当html文档中需要对特定内容进行提示时使用。 + + +### 基本用法 +通过设置selector选择需要显示readtip的元素,传入title和content设置显示的内容。 +:::demo + +```vue + + + + + +``` + +::: + +### 包括多个提示的readtip +传入多个rule,设置不同元素的readtip显示模式。 +:::demo + +```vue + + + + + +``` + +::: + +### d-read-tip + +d-read-tip 参数 + +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | 全局配置项 | +| -------------------- | ------------------ | ---- | ------------------------------- | ---------------------- | ---------- | +| readTipOptions | ReadTipOptions | -- | 必选,配置提示选项 | 基本用法 | -- | +| readTipOptions.rules | ReadTipRules | -- | 必选,配置 readtip 内容 | 包括多个提示的 readtip | -- | +| contentTemplate | `TemplateRef` | -- | 可选,传入模板显示 readtip 内容 | 传入模板显示内容 | -- | + + + +``` +export interface ReadTipOptions { + trigger?: 'hover' | 'click'; // 默认值是 hover + showAnimate?: boolean; // 默认值是 false + mouseenterTime?: number; // 默认值是 100 + mouseleaveTime?: number; // 默认值是 100 + position?: PositionType | PositionType[]; // 默认值是 'top' + overlayClassName?: string; // 默认值为空字符串 + appendToBody?: boolean; // 默认值为true + rules: ReadTipRules; +} +export type ReadTipRules = ReadTipRule | ReadTipRule[]; + +export interface ReadTipRule { + key?: string; + selector: string; + trigger?: 'hover' | 'click'; // 可以继承自 ReadTipOptions + title?: string; + content?: string; + showAnimate?: boolean; // 可以继承自 ReadTipOptions + mouseenterTime?: number; // 可以继承自 ReadTipOptions + mouseleaveTime?: number; // 可以继承自 ReadTipOptions + position?: PositionType | PositionType[]; // 可以继承自 ReadTipOptions + overlayClassName?: string; // 可以继承自 ReadTipOptions + appendToBody?: boolean; //可以继承自 ReadTipOtions + //customData与template搭配使用,customData为传入模板的上下文,可以自定义模板内容 + dataFn?: ({ + element, + rule: ReadTipRule, + }) => Observable<{ title?: string; content?: string; template?: TemplateRef; customData?: any }>; +} +``` + diff --git a/packages/devui-vue/docs/components/slider/index.md b/packages/devui-vue/docs/components/slider/index.md index e8af1c7278744b4917ac8c447fa3ffd26fbe07a6..98cf1b23d7b18336cc7378d7bb6fb8964471e9dd 100644 --- a/packages/devui-vue/docs/components/slider/index.md +++ b/packages/devui-vue/docs/components/slider/index.md @@ -123,19 +123,20 @@ export default defineComponent({ ::: -### 异定制 Popover 的显示内容 +### 定制 Popover 的显示内容 -通过 tipsRenderer 参数传入函数定制 Popover 内的显示内容。 +通过 tipsRenderer 参数传入定制 Popover 内的显示内容。 :::demo ```vue + + +``` + +::: + +## API + +### Props + +| 参数 | 类型 | 默认值 | 说明 | 可选值 | 跳转至 Demo | +| :---: | :------: | :-----: | :----------------: | :------------------------------: | :---------------: | +| type | `string` | defalut | 可选,标签的类型 | `success\|info\|warning\|danger` | [示例](#基本用法) | +| color | `string` | '' | 可选,标签的主题色 | | | + +### Event + +| 名称 | 说明 | +| :------------ | --------------------------------- | +| tagDelete | 删除 tag 的时候触发的事件 | +| checkedChange | tag 的 check 状态改变时触发的事件 | diff --git a/packages/devui-vue/docs/components/time-picker/index.md b/packages/devui-vue/docs/components/time-picker/index.md index dc3f1f37e8f7fb7248c52dbb36ad9d26521d3dd0..858f7c11c6e1702936dd27c100bf5a8c6a2d39e5 100644 --- a/packages/devui-vue/docs/components/time-picker/index.md +++ b/packages/devui-vue/docs/components/time-picker/index.md @@ -48,18 +48,31 @@ + + ``` ::: @@ -73,17 +86,33 @@

mm:HH:ss

- +

hh:mm

- +

MM:ss

- +
+ ``` ::: @@ -111,11 +140,6 @@ export default defineComponent({ setup(props,ctx){ let slotDemo = ref(null) - // 返回选中的时间 - const selectedTimeChage =(timeValue)=>{ - console.log(timeValue) - } - const chooseTime = ()=>{ let timeObj ={ time:'23', diff --git a/packages/devui-vue/docs/components/tooltip/index.md b/packages/devui-vue/docs/components/tooltip/index.md index 489df085278157c752b51adf6e793d3c5746ec58..8ae9d4829c80d14482a1e6a33bcf4676c3dc89f8 100644 --- a/packages/devui-vue/docs/components/tooltip/index.md +++ b/packages/devui-vue/docs/components/tooltip/index.md @@ -8,93 +8,74 @@ ### 基本用法 -:::demo我们可以通过控制属性`position`来控制tooltip的显示位置,`position`取值有4个,分别是`top`、`right`、`bottom`、`left`。通过属性`content`控制tooltip提示框的内容。 - - - - - - - - +:::demo ```vue -``` - -```css -.example { - height: 50px; - width: 60px; - background: cornflowerblue; - margin-top: 30px; + ``` + ::: ### 延时触发 -鼠标移入的时长超过 [mouseEnterDelay] 毫秒之后才会触发,以防止用户无意划过导致的闪现,默认值是150毫秒;鼠标移出之后,再经过[mouseLeaveDelay]毫秒后,toolTip组件才会隐藏,默认值是100毫秒。 +鼠标移入的时长超过 [mouseEnterDelay] 毫秒之后才会触发,以防止用户无意划过导致的闪现,默认值是 150 毫秒;鼠标移出之后,再经过[mouseLeaveDelay]毫秒后,toolTip 组件才会隐藏,默认值是 100 毫秒。 -:::demo 通过`mouseEnterDelay`属性来控制tooltip提示框的`延迟显示`(默认是100ms),`mouseLeaveDelay`属性来控制tooltip提示框的`延迟消失`(默认是150ms) -
MouseEnter delay 500ms
+:::demo -
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; -} -``` ::: ### Tooltip Api -| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | 全局配置项 | -| :---------: | :------: | :-------: | :----------------------- | --------------------------------- | --------- | -| content | `string|DOMString` | -- | 必选,tooltip显示内容 | [基本用法](#基本用法) || -| position | `PositionType | PositionType[]` | ['top', 'right', 'bottom', 'left'] | 可选,tooltip显示位置 | [基本用法](#基本用法) || -| showAnimation | `boolean` | true | 可选,是否显示划出动画 | |✔| -| mouseEnterDelay | `number` | 150 | 可选,鼠标移入后延时多少才显示Tooltip,单位是ms | [基本用法](#延时触发) || -| mouseLeaveDelay | `number` | 100 | 可选,鼠标移出后延时多少才隐藏Tooltip,单位是ms | [基本用法](#延时触发) || +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | 全局配置项 | +| :-------------: | :----------------------------: | :--------------------------------: | :------------------------------------------------ | --------------------- | ---------- | +| content | `string\|DOMString` | -- | 必选,tooltip 显示内容 | [基本用法](#基本用法) | | +| position | `PositionType\|PositionType[]` | ['top', 'right', 'bottom', 'left'] | 可选,tooltip 显示位置 | [基本用法](#基本用法) | | +| showAnimation | `boolean` | true | 可选,是否显示划出动画 | | ✔ | +| mouseEnterDelay | `number` | 150 | 可选,鼠标移入后延时多少才显示 Tooltip,单位是 ms | [延时触发](#延时触发) | +| mouseLeaveDelay | `number` | 100 | 可选,鼠标移出后延时多少才隐藏 Tooltip,单位是 ms | [延时触发](#延时触发) | diff --git a/packages/devui-vue/package.json b/packages/devui-vue/package.json index 991512f692cde0eefdf39f67d004a08557ba627f..46bc909102f149df661a2032339db2de7cf69ff2 100644 --- a/packages/devui-vue/package.json +++ b/packages/devui-vue/package.json @@ -94,8 +94,8 @@ "vite": "^2.4.4", "vite-plugin-md": "^0.6.0", "vite-svg-loader": "^2.2.0", - "vitepress": "^0.15.6", - "vitepress-theme-demoblock": "^1.2.2", + "vitepress": "0.20.1", + "vitepress-theme-demoblock": "1.3.2", "vue-tsc": "^0.2.2", "yarn": "^1.22.11" },