From 27031c8ee49833ee7fa22ae3956e78dfedb754ea Mon Sep 17 00:00:00 2001 From: jianglinjun Date: Thu, 8 Aug 2024 22:48:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E8=BD=AE=E6=92=AD?= =?UTF-8?q?=E5=88=97=E8=A1=A8=EF=BC=8C=E8=BD=AE=E6=92=AD=E8=A1=A8=E6=A0=BC?= =?UTF-8?q?=EF=BC=8C=E8=BF=9B=E5=BA=A6=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +- pnpm-lock.yaml | 8 +- src/carousel-grid/carousel-grid.controller.ts | 42 + src/carousel-grid/carousel-grid.provider.ts | 5 + src/carousel-grid/carousel-grid.scss | 53 ++ src/carousel-grid/carousel-grid.tsx | 418 ++++++++++ src/carousel-grid/grid-control.util.ts | 776 ++++++++++++++++++ src/carousel-grid/index.ts | 13 + src/carousel-list/carousel-list.controller.ts | 57 ++ src/carousel-list/carousel-list.provider.ts | 5 + src/carousel-list/carousel-list.scss | 37 + src/carousel-list/carousel-list.tsx | 452 ++++++++++ src/carousel-list/index.ts | 13 + src/index.ts | 6 + src/rawitem-slider/index.ts | 13 + .../rawitem-slider.controller.ts | 26 + src/rawitem-slider/rawitem-slider.provider.ts | 21 + src/rawitem-slider/rawitem-slider.scss | 76 ++ src/rawitem-slider/rawitem-slider.tsx | 59 ++ vite.config.ts | 1 + 20 files changed, 2082 insertions(+), 3 deletions(-) create mode 100644 src/carousel-grid/carousel-grid.controller.ts create mode 100644 src/carousel-grid/carousel-grid.provider.ts create mode 100644 src/carousel-grid/carousel-grid.scss create mode 100644 src/carousel-grid/carousel-grid.tsx create mode 100644 src/carousel-grid/grid-control.util.ts create mode 100644 src/carousel-grid/index.ts create mode 100644 src/carousel-list/carousel-list.controller.ts create mode 100644 src/carousel-list/carousel-list.provider.ts create mode 100644 src/carousel-list/carousel-list.scss create mode 100644 src/carousel-list/carousel-list.tsx create mode 100644 src/carousel-list/index.ts create mode 100644 src/rawitem-slider/index.ts create mode 100644 src/rawitem-slider/rawitem-slider.controller.ts create mode 100644 src/rawitem-slider/rawitem-slider.provider.ts create mode 100644 src/rawitem-slider/rawitem-slider.scss create mode 100644 src/rawitem-slider/rawitem-slider.tsx diff --git a/package.json b/package.json index 063d9f6..e6e72ae 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "qx-util": "^0.4.8", "snabbdom": "^3.6.2", "vue": "^3.3.8", + "lodash-es": "^4.17.21", "vuedraggable": "^4.1.0" }, "peerDependencies": { @@ -66,6 +67,7 @@ "@typescript-eslint/parser": "^6.13.2", "@vitejs/plugin-vue": "^4.5.1", "@vitejs/plugin-vue-jsx": "^3.1.0", + "@types/lodash-es": "^4.17.12", "eslint": "^8.55.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-prettier": "^9.1.0", @@ -98,4 +100,4 @@ "*.{ts,vue}": "eslint --fix", "*.less": "stylelint --syntax=scss" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a4fa15..78a7175 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ dependencies: element-plus: specifier: ^2.4.2 version: 2.7.8(vue@3.4.35) + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 pluralize: specifier: ^8.0.0 version: 8.0.0 @@ -61,6 +64,9 @@ devDependencies: '@qx-chitanda/vite-plugin-lib-legacy': specifier: ^5.2.1 version: 5.2.1(terser@5.31.3)(vite@5.3.5) + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 '@types/pluralize': specifier: ^0.0.33 version: 0.0.33 @@ -2385,11 +2391,9 @@ packages: resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} dependencies: '@types/lodash': 4.17.7 - dev: false /@types/lodash@4.17.7: resolution: {integrity: sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==} - dev: false /@types/minimist@1.2.5: resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} diff --git a/src/carousel-grid/carousel-grid.controller.ts b/src/carousel-grid/carousel-grid.controller.ts new file mode 100644 index 0000000..994f478 --- /dev/null +++ b/src/carousel-grid/carousel-grid.controller.ts @@ -0,0 +1,42 @@ +import { CTX, GridController } from '@ibiz-template/runtime'; +import { IControl } from '@ibiz/model-core'; + +export class CarouselGridController extends GridController { + /** + * 移动速度 + * DEFAULT时,表示多少秒内完整轮播一次全部数据,鼠标移上去时暂停 + * STEP: 表示每隔多少秒移动一行,不会根据鼠标是否悬浮而暂停 + * + * @type {number} + * @memberof CarouselGridController + */ + public speed: number = 2; + + /** + * 滚动方式 + * + * @type {('DEFAULT' | 'STEP')} + * @memberof CarouselGridController + */ + public rollMode: 'DEFAULT' | 'STEP' = 'DEFAULT'; + + constructor(model: IControl, context: IContext, params: IParams, ctx: CTX) { + super(model, context, params, ctx); + this.init(); + } + + public init() { + if (this.controlParams) { + if (this.controlParams.rollmode) { + this.rollMode = this.controlParams.rollmode; + } + if (this.controlParams.speed) { + this.speed = this.controlParams.speed; + } else if (this.rollMode === 'DEFAULT') { + this.speed = 12; + } else { + this.speed = 2; + } + } + } +} diff --git a/src/carousel-grid/carousel-grid.provider.ts b/src/carousel-grid/carousel-grid.provider.ts new file mode 100644 index 0000000..7385e1f --- /dev/null +++ b/src/carousel-grid/carousel-grid.provider.ts @@ -0,0 +1,5 @@ +import { IControlProvider } from '@ibiz-template/runtime'; + +export class CarouselGridProvider implements IControlProvider { + component: string = 'CarouselGrid'; +} diff --git a/src/carousel-grid/carousel-grid.scss b/src/carousel-grid/carousel-grid.scss new file mode 100644 index 0000000..2d742d7 --- /dev/null +++ b/src/carousel-grid/carousel-grid.scss @@ -0,0 +1,53 @@ +// 左右移动 +@keyframes scroll-left-right { + 0% { + transform: translateX(0); + } + + 100% { + transform: translateX(-100%); + } +} + +// 上下移动 +@keyframes scroll-top { + 0% { + transform: translateY(0); + } + + 100% { + transform: translateY(-50%); + } +} + +@include b('carousel-grid-content'){ + &:hover{ + animation-play-state: paused!important; + } +} + +@include b(control-grid) { + @include e('table'){ + @include when('allow-roll'){ + tbody{ + flex: none; + height: auto; + animation: scroll-top var(--speed) linear infinite; + + &:hover{ + animation-play-state: paused!important; + } + } + } + } +} + + + + + +// @include b('carousel-list-item'){ +// &:hover{ +// animation: scroll-left-right 15s linear infinite; +// } +// } diff --git a/src/carousel-grid/carousel-grid.tsx b/src/carousel-grid/carousel-grid.tsx new file mode 100644 index 0000000..ed4559c --- /dev/null +++ b/src/carousel-grid/carousel-grid.tsx @@ -0,0 +1,418 @@ +import { + useControlController, + useNamespace, + useUIStore, +} from '@ibiz-template/vue3-util'; +import { + defineComponent, + PropType, + computed, + VNode, + ref, + watch, + VNodeArrayChildren, + resolveComponent, + h, + renderSlot, + onUnmounted, +} from 'vue'; +import { IDEGrid, IDEGridColumn, IDEGridGroupColumn } from '@ibiz/model-core'; +import { createUUID } from 'qx-util'; +import { + GridController, + IControlProvider, + ScriptFactory, +} from '@ibiz-template/runtime'; +import { NOOP } from '@ibiz-template/core'; +import './carousel-grid.scss'; +// import { usePagination } from '@ibiz-template/vue3-components'; +import { + IGridProps, + useAppGridBase, + useGridDraggable, + useGridHeaderStyle, + useITableEvent, +} from './grid-control.util'; +import { CarouselGridController } from './carousel-grid.controller'; + +/** + * 绘制成员的attrs + * @author lxm + * @date 2024-03-19 03:48:00 + * @param {IDEFormDetail} model + * @return {*} {IParams} + */ +function renderAttrs(model: IDEGridColumn, params: IParams): IParams { + const attrs: IParams = {}; + model.controlAttributes?.forEach(item => { + if (item.attrName && item.attrValue) { + attrs[item.attrName!] = ScriptFactory.execSingleLine(item.attrValue!, { + ...params, + }); + } + }); + return attrs; +} + +// 绘制除分组列之外的表格列 +export function renderColumn( + c: GridController, + model: IDEGridColumn, + renderColumns: IDEGridColumn[], + index: number, +): VNode | null { + const { codeName: columnName, width } = model; + const columnC = c.columns[columnName!]; + const columnState = c.state.columnStates.find( + item => item.key === columnName, + )!; + + // 如果没有配置自适应列,则最后一列变为自适应列 + const widthFlexGrow = + columnC.isAdaptiveColumn || + (!c.hasAdaptiveColumn && index === renderColumns.length - 1); + + const widthName = widthFlexGrow ? 'min-width' : 'width'; + // 表格列自定义 + return ( + + {{ + default: ({ row }: IData): VNode | null => { + let elRow = row; // element表格数据 + if (row.isGroupData) { + // 有第一条数据时,分组那一行绘制第一条数据 + elRow = row.first; + } + + const rowState = c.findRowState(elRow); + if (rowState) { + const comp = resolveComponent(c.providers[columnName!].component); + return h(comp, { + controller: columnC, + row: rowState, + key: elRow.tempsrfkey + columnName, + attrs: renderAttrs(model, { + ...c.getEventArgs(), + data: rowState.data, + }), + }); + } + return null; + }, + }} + + ); +} + +// 绘制表格列 +export function renderChildColumn( + c: GridController, + model: IDEGridColumn, + renderColumns: IDEGridColumn[], + index: number, +): VNode | null { + if (model.columnType === 'GROUPGRIDCOLUMN') { + const childColumns = + (model as IDEGridGroupColumn).degridColumns?.filter( + item => !item.hideDefault && !item.hiddenDataItem, + ) || []; + const { width } = model; + const align = model.align?.toLowerCase() || 'center'; + return ( + + {{ + default: (): VNodeArrayChildren => { + return childColumns.map((column, index2) => { + return renderChildColumn(c, column, renderColumns, index2); + }); + }, + }} + + ); + } + return renderColumn(c, model, renderColumns, index); +} + +export const CarouselGrid = defineComponent({ + name: 'CarouselGrid', + props: { + modelData: { type: Object as PropType, required: true }, + context: { type: Object as PropType, required: true }, + params: { type: Object as PropType, default: () => ({}) }, + provider: { type: Object as PropType }, + /** + * 部件行数据默认激活模式 + * - 0 不激活 + * - 1 单击激活 + * - 2 双击激活(默认值) + * + * @type {(number | 0 | 1 | 2)} + */ + mdctrlActiveMode: { type: Number, default: undefined }, + singleSelect: { type: Boolean, default: undefined }, + rowEditOpen: { type: Boolean, default: undefined }, + isSimple: { type: Boolean, required: false }, + data: { type: Array, required: false }, + loadDefault: { type: Boolean, default: true }, + }, + setup(props, { slots }) { + const c = useControlController( + (...args) => new CarouselGridController(...args), + ); + const ns = useNamespace(`control-${c.model.controlType!.toLowerCase()}`); + const ns1 = useNamespace('carousel-grid'); + const timer = ref(); + const rollStyle = ref({}); + + const allowRoll = ref(false); + + const { zIndex } = useUIStore(); + c.state.zIndex = zIndex.increment(); + + const rowHeight = ref(48); + + const { + tableRef, + onRowClick, + onDbRowClick, + onSelectionChange, + onSortChange, + handleRowClassName, + handleHeaderCellClassName, + } = useITableEvent(c); + // const { onPageChange, onPageRefresh, onPageSizeChange } = usePagination(c); + + const { headerCssVars } = useGridHeaderStyle(tableRef, ns); + const { cleanup = NOOP } = useGridDraggable(tableRef, ns, c); + + const renderNoData = (): VNode | null => { + return
; + }; + const { + tableData, + renderColumns, + defaultSort, + summaryMethod, + spanMethod, + headerDragend, + } = useAppGridBase(c, props as IGridProps); + + // 绘制表格列 + const renderTableColumn = ( + model: IDEGridColumn, + index: number, + ): VNode | null => { + if (slots[model.id!]) { + return renderSlot(slots, model.id!, { + model, + data: c.state.items, + }); + } + return renderChildColumn(c, model, renderColumns.value, index); + }; + + const handleResize = () => { + if (tableRef.value?.$el) { + const scrollers = + tableRef.value.$el.getElementsByClassName('el-scrollbar__wrap'); + if (scrollers && scrollers.length) { + const target = scrollers[0]; + const totalHeight = tableData.value.length * rowHeight.value; + if (target.clientHeight && totalHeight > target.clientHeight) { + allowRoll.value = true; + if (allowRoll.value) { + if (c.rollMode === 'STEP') { + clearInterval(timer.value); + let length = 1; + timer.value = setInterval(() => { + target.scrollTo({ + top: rowHeight.value * length, + behavior: 'smooth', + }); + if (length >= tableData.value.length) { + setTimeout(() => { + target.scrollTo({ + top: 0, + behavior: 'instant', + }); + }, 500); + length = 1; + } else { + length += 1; + } + }, c.speed * 1000); + } else { + rollStyle.value = { + '--speed': `${c.speed}s`, + }; + } + } + } + } + } + }; + + onUnmounted(() => { + zIndex.decrement(); + if (cleanup !== NOOP) { + cleanup(); + } + }); + + // 是否可以加载更多 + const isLodeMoreDisabled = computed(() => { + if (c.model.pagingMode !== 2) { + return true; + } + return ( + c.state.items.length >= c.state.total || + c.state.isLoading || + c.state.total <= c.state.size + ); + }); + + // 计算表格显示数据 + const conputedGridData = (items: IData[]) => { + if (allowRoll.value) { + return [...items, ...items]; + } + return items; + }; + + // 无限滚动元素 + const infiniteScroll = ref(); + // 无限滚动元素标识 + const infiniteScrollKey = ref(createUUID()); + + watch( + () => c.state.curPage, + () => { + if ( + c.state.curPage === 1 && + (c.model.pagingMode === 2 || c.model.pagingMode === 3) + ) { + infiniteScrollKey.value = createUUID(); + const containerEl = + infiniteScroll.value?.ElInfiniteScroll?.containerEl; + if (containerEl) { + containerEl.lastScrollTop = 0; + containerEl.scrollTop = 0; + } + } + }, + ); + + return { + c, + ns, + ns1, + tableRef, + tableData, + renderColumns, + allowRoll, + rollStyle, + renderTableColumn, + onDbRowClick, + onRowClick, + onSelectionChange, + onSortChange, + // onPageChange, + // onPageSizeChange, + // onPageRefresh, + handleRowClassName, + handleHeaderCellClassName, + renderNoData, + summaryMethod, + spanMethod, + headerDragend, + handleResize, + conputedGridData, + defaultSort, + headerCssVars, + isLodeMoreDisabled, + infiniteScroll, + infiniteScrollKey, + }; + }, + render() { + if (!this.c.state.isCreated) { + return; + } + this.handleResize(); + const state = this.c.state; + const defaultExpandAll = this.c.controlParams.defaultexpandall === 'true'; + + return ( + + { + + {{ + empty: this.renderNoData, + default: (): VNodeArrayChildren => { + return [ + this.renderColumns.map((model, index) => { + return this.renderTableColumn(model, index); + }), + ]; + }, + }} + + } + + ); + }, +}); diff --git a/src/carousel-grid/grid-control.util.ts b/src/carousel-grid/grid-control.util.ts new file mode 100644 index 0000000..2404fb1 --- /dev/null +++ b/src/carousel-grid/grid-control.util.ts @@ -0,0 +1,776 @@ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable no-param-reassign */ +/* eslint-disable no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Namespace, listenJSEvent } from '@ibiz-template/core'; +import { + ControlVO, + GridController, + GridRowState, + IControlProvider, + IGridRowState, + Srfuf, +} from '@ibiz-template/runtime'; +import { IDEGrid, IDEGridColumn } from '@ibiz/model-core'; +import { TableColumnCtx } from 'element-plus'; +import { + computed, + nextTick, + onUnmounted, + Ref, + ref, + watch, + watchEffect, +} from 'vue'; + +/** + * 表格部件props接口 + * + * @author zk + * @date 2023-09-26 06:09:51 + * @export + * @interface IGridProps + */ +export interface IGridProps { + // 模型 + modelData: IDEGrid; + // 上下文 + context: IContext; + // 视图参数 + params: IParams; + // 适配器 + provider: IControlProvider; + // 部件行数据默认激活模式 + mdctrlActiveMode?: number; + // 是否单选 + singleSelect?: boolean; + // 是否开启行编辑 + rowEditOpen?: boolean; + // 是否是本地数据模式 + isSimple: boolean; + // 本地数据模式data + data: Array; + // 默认加载 + loadDefault: boolean; +} + +/** + * 排序合并数据 + * + * @param {string[]} rowSpanKeys + * @param {IData[]} items + * @return {*} + */ +function sortMergeData(rowSpanKeys: string[], items: IData[]) { + const otherItems: IData[] = []; + const firstKey: string = rowSpanKeys[0] || ''; + const sortedItems = items.filter((item: IData) => { + if (!item[firstKey]) { + otherItems.push(item); + } + return item[firstKey]; + }); + sortedItems.sort((a: IData, b: IData) => { + for (const key of rowSpanKeys) { + if (a[key] !== b[key]) { + return a[key] > b[key] ? 1 : -1; + } + } + return 0; + }); + sortedItems.push(...otherItems); + return sortedItems; +} + +/** + * 适配element的table的事件 + * + * @author lxm + * @date 2022-09-05 21:09:42 + * @export + * @param {GridController} c + * @returns {*} + */ +export function useITableEvent(c: GridController): { + tableRef: Ref; + onRowClick: ( + data: ControlVO, + _column: IData, + event: MouseEvent, + ) => Promise; + onDbRowClick: (data: ControlVO) => void; + onSelectionChange: (selection: ControlVO[]) => void; + onSortChange: (opts: { + _column: IData; + prop: string; + order: 'ascending' | 'descending'; + }) => void; + handleRowClassName: ({ row }: { row: IData }) => string; + handleHeaderCellClassName: ({ + _row, + column, + _rowIndex, + _columnIndex, + }: { + _row: IData; + column: IData; + _rowIndex: number; + _columnIndex: number; + }) => string; +} { + const tableRef = ref(); + let forbidChange = false; + + // 是否正在设置elementPlus表格排序回显效果 + let isGridUISort = false; + + async function onRowClick( + data: ControlVO, + _column: IData, + _event: MouseEvent, + ): Promise { + // 新建行拦截行点击事件 + if (data.srfuf === Srfuf.CREATE) { + return; + } + // 单行编辑模式下,行点击会触发 + if ((c as any).editShowMode === 'row' && c.model.enableRowEdit) { + const row = c.findRowState(data); + if (row && row.showRowEdit !== true) { + // 开启行编辑 + await c.switchRowEdit(row, true); + } + } else { + c.onRowClick(data as ControlVO); + } + } + + function onDbRowClick(data: ControlVO): void { + // 新建行拦截行双击事件 + if (data.srfuf === Srfuf.CREATE) { + return; + } + c.onDbRowClick(data); + } + + function onSelectionChange(selection: ControlVO[]): void { + // 选中数据在回显的时候屏蔽值变更事件,否则会递归。 + if (!forbidChange) { + c.setSelection(selection); + } + } + + // 监听选中数据,操作表格来界面回显选中效果。 + watch( + [ + () => tableRef.value, + (): boolean => c.state.isLoaded, + (): IData[] => c.state.selectedData, + ], + ([table, isLoaded, newVal]) => { + if (!isLoaded || !table) { + return; + } + if (c.state.singleSelect) { + // 单选,选中效果回显。 + if (newVal[0]) { + tableRef.value!.setCurrentRow(newVal[0], true); + } else { + tableRef.value!.setCurrentRow(); + } + } else { + forbidChange = true; + tableRef.value!.clearSelection(); + newVal.forEach(item => tableRef.value!.toggleRowSelection(item, true)); + forbidChange = false; + } + }, + ); + + // 排序变更回调。 + function onSortChange(opts: { + _column: IData; + prop: string; + order: 'ascending' | 'descending'; + }): void { + if (isGridUISort) { + isGridUISort = false; + return; + } + const { prop, order } = opts; + const fieldName = c.fieldColumns[prop].model.appDEFieldId; + let order1: 'asc' | 'desc' | undefined; + if (order === 'ascending') { + order1 = 'asc'; + } else if (order === 'descending') { + order1 = 'desc'; + } + // 如果排序条件相同,不触发查询。 + const sortQuery = `${fieldName},${order1}`; + if (sortQuery === c.state.sortQuery) { + return; + } + c.setSort(fieldName, order1); + c.load({ + isInitialLoad: c.model.pagingMode === 2 || c.model.pagingMode === 3, + }); + } + + // todo 用自己的ns类名去压制,把element原来的样式清除 + function handleRowClassName({ row }: { row: IData }): string { + let activeClassName = ''; + if (c.state.selectedData.length > 0) { + c.state.selectedData.forEach((data: IData) => { + if (data === row) { + // current-row用于多选激活样式与单选保持一致,有背景色 + activeClassName = 'current-row'; + } + }); + } + const rowState = c.findRowState(row); + if (rowState?.showRowEdit) { + activeClassName += ' editing-row'; + } + if (row.srfkey) { + activeClassName += ` id-${row.srfkey}`; + } + if (c.enableRowEditOrder) { + activeClassName += ` enable-order`; + } + return activeClassName; + } + + // 表头单元格的 className 的回调方法 + function handleHeaderCellClassName({ + _row, + column, + _rowIndex, + _columnIndex, + }: { + _row: IData; + column: IData; + _rowIndex: number; + _columnIndex: number; + }): string { + const columnModel = c.model.degridColumns?.find(gridColumn => { + return gridColumn.codeName === column.property; + }); + if ( + columnModel && + columnModel.headerSysCss && + columnModel.headerSysCss.cssName + ) { + return columnModel.headerSysCss.cssName; + } + return ''; + } + + watch( + () => c.state.sortQuery, + newVal => { + // 监听排序查询条件,手动触发element-plus表格的排序以回显样式 + if (newVal) { + const prop = c.state.sortQuery.split(',')[0]; + const sortDir = c.state.sortQuery.split(',')[1]; + if (prop && sortDir) { + const order = sortDir === 'desc' ? 'descending' : 'ascending'; + const sortTable = () => { + if (tableRef.value) { + nextTick(() => { + isGridUISort = true; + tableRef.value!.sort(prop, order); + }); + } else { + setTimeout(sortTable, 500); + } + }; + sortTable(); + } + } + }, + ); + + return { + tableRef, + onRowClick, + onDbRowClick, + onSelectionChange, + onSortChange, + handleRowClassName, + handleHeaderCellClassName, + }; +} + +/** + * 使用表格分页组件 + * + * @author lxm + * @date 2022-09-06 17:09:09 + * @export + * @param {GridController} c + * @returns {*} + */ +export function useAppGridPagination(c: GridController): { + onPageChange: (page: number) => void; + onPageSizeChange: (size: number) => void; + onPageRefresh: () => void; +} { + function onPageChange(page: number): void { + if (!page || page === c.state.curPage) { + return; + } + c.state.curPage = page; + c.load(); + } + + function onPageSizeChange(size: number): void { + if (!size || size === c.state.size) { + return; + } + c.state.size = size; + + // 当page为第一页的时候切换size不会触发pageChange,需要自己触发加载 + if (c.state.curPage === 1) { + c.load(); + } + } + + function onPageRefresh(): void { + c.load(); + } + return { onPageChange, onPageSizeChange, onPageRefresh }; +} + +export function useAppGridBase( + c: GridController, + props: IGridProps, +): { + tableData: Ref; + renderColumns: Ref; + defaultSort: Ref; + summaryMethod: ({ + columns, + }: { + columns: TableColumnCtx[]; + data: IData[]; + }) => string[]; + spanMethod: ({ + row, + column, + rowIndex, + columnIndex, + }: { + row: IData; + column: IData; + rowIndex: number; + columnIndex: number; + }) => IData | void; + headerDragend: (newWidth: number, oldWidth: number, column: IData) => void; +} { + const initSimpleData = (): void => { + if (!props.data) { + return; + } + c.state.items = props.data; + c.state.rows = props.data.map(item => { + const row = new GridRowState(new ControlVO(item), c); + return row; + }); + c.calcAggResult(c.state.items); + c.calcTotalData(); + }; + + const defaultSort = computed(() => { + const fieldColumn = Object.values(c.fieldColumns).find( + item => item.model.appDEFieldId === c.model.minorSortAppDEFieldId, + ); + return { + prop: fieldColumn?.model.codeName, + order: + c.model.minorSortDir?.toLowerCase() === 'desc' + ? 'descending' + : 'ascending', + }; + }); + + // 选中数据回显 + c.evt.on('onCreated', async () => { + if (props.isSimple) { + initSimpleData(); + c.state.isLoaded = true; + } + }); + + watch( + () => props.data, + () => { + if (props.isSimple) { + initSimpleData(); + } + }, + { + deep: true, + }, + ); + + // 表格数据,items和rows更新有时间差,用rows来获取items + const tableData = computed(() => { + const state = c.state; + if (c.model.enableGroup) { + const result: IData[] = []; + state.groups.forEach(item => { + if (!item.children.length) { + return; + } + const children = [...item.children]; + const first = children.shift(); + result.push({ + tempsrfkey: first?.tempsrfkey || item.caption, + srfkey: first?.srfkey || item.caption, + isGroupData: true, + caption: item.caption, + first, + children, + }); + }); + return result; + } + const { rowspankeys = [] } = c.controlParams; + // 合并行单元格时计算合并数据,合并列则不用 + if (rowspankeys.length > 0) { + return sortMergeData( + rowspankeys, + state.rows.map(row => row.data), + ); + } + return state.rows.map(row => row.data); + }); + + // 实际绘制的表格列 + const renderColumns = computed(() => { + if (c.isMultistageHeader) { + return c.model.degridColumns || []; + } + const columns: IDEGridColumn[] = []; + c.state.columnStates.forEach(item => { + if (item.hidden) { + return; + } + const columnModel = + c.fieldColumns[item.key]?.model || c.uaColumns[item.key]?.model; + if (columnModel) { + columns.push(columnModel); + } + }); + return columns; + }); + + /** + * 求和计算回调 + * @author lxm + * @date 2023-08-07 05:21:31 + * @return {*} {string[]} + */ + const summaryMethod = ({ + columns, + }: { + columns: TableColumnCtx[]; + data: IData[]; + }): string[] => { + return columns.map((item, index) => { + if (index === 0) { + return c.aggTitle; + } + return c.state.aggResult[item.property]; + }); + }; + + /** + * 表格合并单元格方法 + * + * @return {*} + */ + const spanMethod = ({ + row, + column, + rowIndex, + columnIndex, + }: { + row: IData; + column: IData; + rowIndex: number; + columnIndex: number; + }) => { + const { property } = column; + const { rowspankeys = [], colspankeys = [] } = c.controlParams; + // 合并行 + if (rowspankeys.length > 0 && rowspankeys.includes(property)) { + // 只有第一列或有值时才和并 + const allow = columnIndex === 0 || row[property]; + // 第二行开始合并 + if ( + rowIndex > 0 && + allow && + row[property] === tableData.value[rowIndex - 1][property] + ) { + return { + rowspan: 0, + colspan: 0, + }; + } + // 第一行计算出合并长度 + let rowspan = 1; + for (let i = rowIndex + 1; i < tableData.value.length; i++) { + if (allow && tableData.value[i][property] === row[property]) { + rowspan += 1; + } else { + break; + } + } + return { + rowspan, + colspan: 1, + }; + } + // 合并列 + if (colspankeys.length > 0 && colspankeys.includes(property)) { + // 第二列开始合并 + const preProperty = renderColumns.value[columnIndex - 1].codeName; + if ( + columnIndex > 0 && + colspankeys.includes(preProperty) && + row[property] === row[preProperty!] + ) { + return { + rowspan: 0, + colspan: 0, + }; + } + // 第一列计算出合并长度 + let colspan = 1; + for (let i = columnIndex + 1; i < renderColumns.value.length; i++) { + const nextProperty = renderColumns.value[i].codeName!; + if ( + colspankeys.includes(nextProperty) && + row[nextProperty] === row[property] + ) { + colspan += 1; + } else { + break; + } + } + return { + rowspan: 1, + colspan, + }; + } + }; + + /** + * 表格列拖动 + * + * @return {*} + */ + const headerDragend = ( + newWidth: number, + oldWidth: number, + column: IData, + ): void => { + const { property } = column; + const columnC = c.columns[property!]; + if (columnC.isAdaptiveColumn) { + columnC.isAdaptiveColumn = false; + columnC.model.width = newWidth; + const index = renderColumns.value.findIndex(renderColumn => { + const renderColumnC = c.columns[renderColumn.codeName!]; + return renderColumnC.isAdaptiveColumn; + }); + c.hasAdaptiveColumn = index !== -1; + } + }; + + return { + tableData, + renderColumns, + defaultSort, + summaryMethod, + spanMethod, + headerDragend, + }; +} + +/** + * 监听表格头部高度变化,计算css变量 + */ +export function useGridHeaderStyle( + tableRef: IData, + ns: Namespace, +): { + headerCssVars: IData; +} { + // 浏览器ResizeObserver对象 + let resizeObserver: ResizeObserver | null = null; + + // 上次表格头高度 + let lastGridHeaderHeight = 0; + + // 样式变量 + const headerCssVars = ref({}); + + const calcGridHeaderHeight = () => { + if (window.ResizeObserver) { + const gridHeaderDom = tableRef.value!.$el.querySelector( + '.el-table__header-wrapper', + ); + if (gridHeaderDom) { + // 监听表格头高度变化动态去算css + resizeObserver = new ResizeObserver(entries => { + const height = entries[0].contentRect.height; + if (height !== lastGridHeaderHeight) { + const tempCssVars = { + 'now-header-height': `${height}px`, + }; + headerCssVars.value = ns.cssVarBlock(tempCssVars); + lastGridHeaderHeight = height; + } + }); + resizeObserver.observe(gridHeaderDom); + } + } + }; + + const stop = watchEffect(() => { + if (tableRef.value) { + calcGridHeaderHeight(); + } + }); + + onUnmounted(() => { + if (resizeObserver) { + resizeObserver.disconnect(); + } + stop(); + }); + + return { + headerCssVars, + }; +} + +export function useGridDraggable( + tableRef: IData, + ns: Namespace, + c: GridController, +): { + cleanup?: () => void; +} { + // 表格不启用次序调整 + if (!c.enableRowEditOrder) { + return {}; + } + + // 拖拽下标 + let dragIndex = 0; + + // 目标下标 + let dropIndex = 0; + + // 拖拽数据 + let draggingData: IGridRowState | null = null; + + // 目标数据 + let dropData: IGridRowState | null = null; + + // eslint-disable-next-line @typescript-eslint/ban-types + const cleanups: Function[] = []; + + const calcSrfKeyByClass = (classList: DOMTokenList): string => { + let result = ''; + classList.forEach((className: string) => { + if (className.startsWith('id-')) { + result = className.replace('id-', ''); + } + }); + return result; + }; + + const setRowDragEvent = (item: HTMLTableRowElement) => { + item.setAttribute('draggable', 'true'); + const cleanDragStart = listenJSEvent( + item, + 'dragstart', + (event: DragEvent) => { + if (event.target) { + const draggingDom = event.target as HTMLElement; + event.dataTransfer!.effectAllowed = 'move'; + const draggingKey = calcSrfKeyByClass(draggingDom.classList); + dragIndex = c.state.rows.findIndex( + row => row.data.srfkey === draggingKey, + ); + draggingData = c.state.rows[dragIndex]; + } + }, + ); + const cleanDragEnter = listenJSEvent( + item, + 'dragenter', + (event: DragEvent) => { + event.preventDefault(); + const targetDom = event.currentTarget as HTMLElement; + const targetKey = calcSrfKeyByClass(targetDom.classList); + dropIndex = c.state.rows.findIndex( + row => row.data.srfkey === targetKey, + ); + if (draggingData?.data.srfkey === targetKey || dropIndex === -1) { + return; + } + dropData = c.state.rows[dropIndex]; + }, + ); + const cleanDragOver = listenJSEvent( + item, + 'dragover', + (event: DragEvent) => { + event.preventDefault(); + }, + ); + const cleanDragEnd = listenJSEvent(item, 'dragend', (event: DragEvent) => { + event.preventDefault(); + if (draggingData && dropData) { + c.onDragChange( + draggingData, + dropData, + dropIndex > dragIndex ? 'next' : 'prev', + ); + } + }); + cleanups.push(cleanDragStart); + cleanups.push(cleanDragEnter); + cleanups.push(cleanDragOver); + cleanups.push(cleanDragEnd); + }; + + watch( + [() => tableRef.value, (): boolean => c.state.isLoaded], + (table, isLoaded) => { + if (!isLoaded || !table) { + return; + } + const grid = tableRef.value!.$el; + if (grid) { + const rows = grid.getElementsByClassName('el-table__row'); + rows.forEach((item: HTMLTableRowElement) => { + setRowDragEvent(item); + }); + } + }, + ); + + return { + cleanup: () => { + cleanups.forEach(cleanup => { + cleanup(); + }); + }, + }; +} diff --git a/src/carousel-grid/index.ts b/src/carousel-grid/index.ts new file mode 100644 index 0000000..2bc02e8 --- /dev/null +++ b/src/carousel-grid/index.ts @@ -0,0 +1,13 @@ +import { App } from 'vue'; +import { registerControlProvider } from '@ibiz-template/runtime'; +import { withInstall } from '@ibiz-template/vue3-util'; +import { CarouselGrid } from './carousel-grid'; +import { CarouselGridProvider } from './carousel-grid.provider'; + +export const IBizCarouselGrid = withInstall(CarouselGrid, function (v: App) { + v.component(CarouselGrid.name, CarouselGrid); + registerControlProvider( + 'GRID_RENDER_CAROUSEL_GRID', + () => new CarouselGridProvider(), + ); +}); diff --git a/src/carousel-list/carousel-list.controller.ts b/src/carousel-list/carousel-list.controller.ts new file mode 100644 index 0000000..c3da75c --- /dev/null +++ b/src/carousel-list/carousel-list.controller.ts @@ -0,0 +1,57 @@ +import { CTX, ListController } from '@ibiz-template/runtime'; +import { IControl } from '@ibiz/model-core'; + +export class CarouselListController extends ListController { + /** + * 滚动模式 + * DEFAULT是连续滚动 STEP是一行一行滚动 + * + * @type {('DEFAULT' | 'STEP')} + * @memberof CarouselListController + */ + public rollMode: 'DEFAULT' | 'STEP' = 'DEFAULT'; + + /** + * 滚动速度 + * + * @type {number} + * @memberof CarouselListController + */ + public moveSpeed: number = 0; + + /** + * 列表项边框 + * + * @type {string} + * @memberof CarouselListController + */ + public borderStyle: string = ''; + + constructor(model: IControl, context: IContext, params: IParams, ctx: CTX) { + super(model, context, params, ctx); + this.init(); + } + + /** + * 初始化控件参数 + * + * @memberof CarouselListController + */ + public init() { + if (this.controlParams) { + if (this.controlParams.rollmode) { + this.rollMode = this.controlParams.rollmode; + } + if (this.controlParams.speed) { + this.moveSpeed = Number(this.controlParams.speed); + } else if (this.rollMode === 'DEFAULT') { + this.moveSpeed = 20; + } else { + this.moveSpeed = 2; + } + if (this.controlParams.borderstyle) { + this.borderStyle = this.controlParams.borderstyle; + } + } + } +} diff --git a/src/carousel-list/carousel-list.provider.ts b/src/carousel-list/carousel-list.provider.ts new file mode 100644 index 0000000..9553b4c --- /dev/null +++ b/src/carousel-list/carousel-list.provider.ts @@ -0,0 +1,5 @@ +import { IControlProvider } from '@ibiz-template/runtime'; + +export class CarouselListProvider implements IControlProvider { + component: string = 'CarouselList'; +} diff --git a/src/carousel-list/carousel-list.scss b/src/carousel-list/carousel-list.scss new file mode 100644 index 0000000..4f1126d --- /dev/null +++ b/src/carousel-list/carousel-list.scss @@ -0,0 +1,37 @@ +// 左右移动 +@keyframes scroll-left-right { + 0% { + transform: translateX(0); + } + + 100% { + transform: translateX(-100%); + } +} + +// 上下移动 +@keyframes scroll-top { + 0% { + transform: translateY(0); + } + + 100% { + transform: translateY(-50%); + } +} + +@include b('carousel-list-content'){ + height: 100%; + overflow: auto; + @include when('allow-roll'){ + &:hover{ + animation-play-state: paused!important; + } + } +} + +// @include b('carousel-list-item'){ +// &:hover{ +// animation: scroll-left-right 15s linear infinite; +// } +// } diff --git a/src/carousel-list/carousel-list.tsx b/src/carousel-list/carousel-list.tsx new file mode 100644 index 0000000..12f9edc --- /dev/null +++ b/src/carousel-list/carousel-list.tsx @@ -0,0 +1,452 @@ +import { useControlController, useNamespace } from '@ibiz-template/vue3-util'; +import { + defineComponent, + PropType, + computed, + VNode, + ref, + watch, + Ref, + resolveComponent, + h, + nextTick, +} from 'vue'; +import { IDEList, ILayoutPanel, IPanel } from '@ibiz/model-core'; +import { isNil } from 'lodash-es'; +import { createUUID } from 'qx-util'; +import { IControlProvider, IMDControlGroupState } from '@ibiz-template/runtime'; +import { showTitle } from '@ibiz-template/core'; +// import { usePagination } from '@ibiz-template/vue3-components'; +import './carousel-list.scss'; +import { CarouselListController } from './carousel-list.controller'; + +export const CarouselList = defineComponent({ + name: 'CarouselList', + props: { + modelData: { type: Object as PropType, required: true }, + context: { type: Object as PropType, required: true }, + params: { type: Object as PropType, default: () => ({}) }, + provider: { type: Object as PropType }, + /** + * 部件行数据默认激活模式 + * - 0 不激活 + * - 1 单击激活 + * - 2 双击激活(默认值) + * + * @type {(number | 0 | 1 | 2)} + */ + mdctrlActiveMode: { type: Number, default: undefined }, + + /** + * 是否为单选 + * - true 单选 + * - false 多选 + * + * @type {(Boolean)} + */ + singleSelect: { type: Boolean, default: undefined }, + isSimple: { type: Boolean, required: false }, + loadDefault: { type: Boolean, default: true }, + }, + setup(props) { + const c = useControlController( + (...args) => new CarouselListController(...args), + ); + const ns = useNamespace('carousel-list'); + const moveSpeed = ref(c.moveSpeed); // 指列表在默认滚动模式下多少秒内整体完成一次上下滚动,默认20秒,或者在步骤模式下多少秒滚动一次,默认2秒 + const allowRoll = ref(false); // 是否允许滚动 + const rollMode = ref(c.rollMode); + const carouselContainer = ref(); + const timer: Ref = ref(); + // const { onPageChange, onPageRefresh, onPageSizeChange } = usePagination(c); + + // 是否可以加载更多 + const isLodeMoreDisabled = computed(() => { + if (c.model.enablePagingBar === true) { + return true; + } + if (c.model.pagingMode !== 2) { + return true; + } + return ( + c.state.items.length >= c.state.total || + c.state.isLoading || + c.state.total <= c.state.size + ); + }); + + // 无限滚动元素 + const infiniteScroll = ref(); + // 无限滚动元素标识 + const infiniteScrollKey = ref(createUUID()); + + // 分组默认展开项 + const defaultOpens = ref(); + if (c.controlParams.defaultexpandall === 'true') { + watch( + () => c.state.groups, + () => { + if (c.state.groups.length > 0) { + defaultOpens.value = c.state.groups.map((x: IData) => x.key); + } + }, + ); + defaultOpens.value = c.state.groups.map((x: IData) => x.key); + } + + // 整体滚动样式 + const moveStyle = computed(() => { + if (!allowRoll.value || rollMode.value !== 'DEFAULT') { + return {}; + } + const style = { + flex: 'none', + height: 'auto', + }; + Object.assign(style, { + animation: `scroll-top ${moveSpeed.value}s linear infinite`, + }); + return style; + }); + + watch( + () => c.state.curPage, + () => { + if ( + c.state.curPage === 1 && + (c.model.pagingMode === 2 || c.model.pagingMode === 3) + ) { + infiniteScrollKey.value = createUUID(); + const containerEl = + infiniteScroll.value?.ElInfiniteScroll?.containerEl; + if (containerEl) { + containerEl.lastScrollTop = 0; + containerEl.scrollTop = 0; + } + } + }, + ); + + // 绘制项布局面板 + const renderPanelItem = (item: IData, modelData: ILayoutPanel): VNode => { + const { context, params } = c; + // 是否选中数据 + const findIndex = c.state.selectedData.findIndex((data: IData) => { + return data.srfkey === item.srfkey; + }); + const itemClass = [ns.b('item'), ns.is('active', findIndex !== -1)]; + return ( + => c.onRowClick(item)} + onDblclick={(): Promise => c.onDbRowClick(item)} + > + ); + }; + + // 绘制默认列表项 + const renderDefaultItem = (item: IData): VNode => { + // 是否选中数据 + const findIndex = c.state.selectedData.findIndex((data: IData) => { + return data.srfkey === item.srfkey; + }); + const itemClass = [ns.b('item'), ns.is('active', findIndex !== -1)]; + return ( +
=> c.onRowClick(item)} + onDblclick={(): Promise => c.onDbRowClick(item)} + > + {`${isNil(item.srfmajortext) ? '' : item.srfmajortext}`} +
+ ); + }; + + // 绘制分组 + const renderGroup = (group: IMDControlGroupState): VNode => { + const panel = props.modelData.itemLayoutPanel; + return ( + + {group.children.length > 0 ? ( + group.children.map(item => { + return panel + ? renderPanelItem(item, panel) + : renderDefaultItem(item); + }) + ) : ( +
+ {ibiz.i18n.t('app.noData')} +
+ )} +
+ ); + }; + + // 绘制项边框 + const renderItemBorder = (item: IData, panel: IPanel | undefined) => { + const content = panel + ? renderPanelItem(item, panel) + : renderDefaultItem(item); + if (c.borderStyle) { + const borderDiv = resolveComponent(c.borderStyle); + return h(borderDiv, {}, content); + } + return content; + }; + + // 绘制卡片内容 + const renderListContent = (): VNode => { + if (c.model.enableGroup && !c.state.isSimple) { + return ( + + {c.state.groups?.map((group: IMDControlGroupState) => { + return ( +
{renderGroup(group)}
+ ); + })} +
+ ); + } + const panel = props.modelData.itemLayoutPanel; + + return ( +
=> c.loadMore()} + infinite-scroll-distance={10} + infinite-scroll-disabled={isLodeMoreDisabled.value} + ref={'infiniteScroll'} + key={infiniteScrollKey.value} + > + {c.state.items.map((item: IData, index: number) => { + return ( +
+ {renderItemBorder(item, panel)} +
+ ); + })} + {allowRoll.value && + c.state.items.map((item: IData, index: number) => { + return ( +
+ {renderItemBorder(item, panel)} +
+ ); + })} + + {c.model.pagingMode === 3 && + !( + c.state.items.length >= c.state.total || + c.state.isLoading || + c.state.total <= c.state.size + ) && ( +
+ c.loadMore()}> + {ibiz.i18n.t('control.common.loadMore')} + +
+ )} +
+ ); + }; + + // 绘制项行为 + const renderQuickToolBar = (): VNode | undefined => { + const ctrlModel = c.model.controls?.find((item: IData) => { + return item.name === `${c.model.name!}_quicktoolbar`; + }); + if (!ctrlModel) { + return; + } + return ( + + ); + }; + + const renderBatchToolBar = (): VNode | undefined => { + const ctrlModel = c.model.controls?.find((item: IData) => { + return item.name === `${c.model.name!}_batchtoolbar`; + }); + if (!ctrlModel) { + return; + } + return ( +
+ +
+ ); + }; + + const renderNoData = (): VNode | undefined => { + // 未加载不显示无数据 + const { isLoaded } = c.state; + if (!isLoaded) { + return; + } + return ( + isLoaded && ( + + {renderQuickToolBar()} + + ) + ); + }; + + // 处理列表步骤模式滚动 + const handleListRoll = () => { + clearInterval(timer.value); + + let length = 1; + timer.value = setInterval(() => { + const els = carouselContainer.value?.$el?.getElementsByClassName( + ns.b('scroll-item'), + ); + if (allowRoll.value && rollMode.value === 'STEP') { + const height = (els[0] as HTMLElement).offsetHeight; + (infiniteScroll.value as HTMLElement).scrollTo({ + top: height * length, + behavior: 'smooth', + }); + if (length >= c.state.items.length) { + setTimeout(() => { + (infiniteScroll.value as HTMLElement).scrollTo({ + top: 0, + behavior: 'instant', + }); + }, 500); + length = 1; + } else { + length += 1; + } + } else { + clearInterval(timer.value); + } + }, moveSpeed.value * 1000); + }; + + // 计算是否允许滚动 + const computeAllowRoll = () => { + const els = carouselContainer.value?.$el?.getElementsByClassName( + ns.b('scroll-item'), + ); + if (!allowRoll.value && infiniteScroll.value && els && els.length) { + const height = (els[0] as HTMLElement).offsetHeight; + if (!height) { + // 此时布局面板没加载完 + const tempTimer = setInterval(() => { + if ( + (els[0] as HTMLElement).offsetHeight > 0 && + c.state.items.length * (els[0] as HTMLElement).offsetHeight > + infiniteScroll.value!.clientHeight + ) { + allowRoll.value = true; + handleListRoll(); + clearInterval(tempTimer); + } else { + clearInterval(tempTimer); + clearInterval(timer.value); + } + }, 10); + } + } + }; + + watch( + () => infiniteScroll.value, + newVal => { + if (newVal) { + computeAllowRoll(); + } + }, + { + immediate: true, + deep: true, + }, + ); + + return { + c, + ns, + carouselContainer, + infiniteScroll, + renderListContent, + renderNoData, + renderBatchToolBar, + // onPageChange, + // onPageRefresh, + // onPageSizeChange, + }; + }, + render() { + let content = null; + if (this.c.state.isCreated) { + content = [ + this.c.state.items.length > 0 + ? this.renderListContent() + : this.renderNoData(), + this.renderBatchToolBar(), + this.c.state.enablePagingBar && this.c.model.pagingMode === 1 ? ( + + ) : null, + ]; + } + + return ( + + {content} + + ); + }, +}); diff --git a/src/carousel-list/index.ts b/src/carousel-list/index.ts new file mode 100644 index 0000000..16605c2 --- /dev/null +++ b/src/carousel-list/index.ts @@ -0,0 +1,13 @@ +import { App } from 'vue'; +import { registerControlProvider } from '@ibiz-template/runtime'; +import { withInstall } from '@ibiz-template/vue3-util'; +import { CarouselList } from './carousel-list'; +import { CarouselListProvider } from './carousel-list.provider'; + +export const IBizCarouselList = withInstall(CarouselList, function (v: App) { + v.component(CarouselList.name, CarouselList); + registerControlProvider( + 'LIST_RENDER_CAROUSEL_LIST', + () => new CarouselListProvider(), + ); +}); diff --git a/src/index.ts b/src/index.ts index e5c8d04..dee0f00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,9 @@ import { IBizScreenPortlet } from './screen-portlet/index'; import { IBizScreenRadioList } from './screen-radio-list/index'; import { IBizScreenRealTime } from './screen-real-time/index'; import { IBizScreenPanelContainer } from './screen-panel-container/index'; +import { IBizCarouselList } from './carousel-list'; +import { IBizCarouselGrid } from './carousel-grid'; +import { IBizRawItemSlider } from './rawitem-slider'; // 自定义边框 import { @@ -37,6 +40,9 @@ export default { _app.use(IBizScreenRadioList); _app.use(IBizScreenRealTime); _app.use(IBizScreenPanelContainer); + _app.use(IBizCarouselList); + _app.use(IBizCarouselGrid); + _app.use(IBizRawItemSlider); // 自定义边框 _app.use(IBizCustomDV1); diff --git a/src/rawitem-slider/index.ts b/src/rawitem-slider/index.ts new file mode 100644 index 0000000..538a6cd --- /dev/null +++ b/src/rawitem-slider/index.ts @@ -0,0 +1,13 @@ +import { App } from 'vue'; +import { withInstall } from '@ibiz-template/vue3-util'; +import { registerEditorProvider } from '@ibiz-template/runtime'; +import { RawItemSlider } from './rawitem-slider'; +import { RawItemSliderProvider } from './rawitem-slider.provider'; + +export const IBizRawItemSlider = withInstall(RawItemSlider, function (v: App) { + v.component(RawItemSlider.name, RawItemSlider); + registerEditorProvider( + 'EDITOR_CUSTOMSTYLE_SCREEN_PROGRESS', + () => new RawItemSliderProvider(), + ); +}); diff --git a/src/rawitem-slider/rawitem-slider.controller.ts b/src/rawitem-slider/rawitem-slider.controller.ts new file mode 100644 index 0000000..2a1692b --- /dev/null +++ b/src/rawitem-slider/rawitem-slider.controller.ts @@ -0,0 +1,26 @@ +import { EditorController } from '@ibiz-template/runtime'; +import { ISlider } from '@ibiz/model-core'; + +export class RawItemSliderController extends EditorController { + /** + * 总数属性 + * + * @type {string} + * @memberof RawItemSliderController + */ + public totalField: string = ''; + + /** + * 初始化 + * + * @protected + * @return {*} {Promise} + * @memberof RawItemSliderController + */ + protected async onInit(): Promise { + super.onInit(); + if (this.editorParams && this.editorParams.TOTALFIELD) { + this.totalField = this.editorParams.TOTALFIELD.toLowerCase(); + } + } +} diff --git a/src/rawitem-slider/rawitem-slider.provider.ts b/src/rawitem-slider/rawitem-slider.provider.ts new file mode 100644 index 0000000..9276c8b --- /dev/null +++ b/src/rawitem-slider/rawitem-slider.provider.ts @@ -0,0 +1,21 @@ +import { + IEditorContainerController, + IEditorProvider, +} from '@ibiz-template/runtime'; +import { ISlider } from '@ibiz/model-core'; +import { RawItemSliderController } from './rawitem-slider.controller'; + +export class RawItemSliderProvider implements IEditorProvider { + formEditor: string = 'RawItemSlider'; + + gridEditor: string = 'RawItemSlider'; + + async createController( + editorModel: ISlider, + parentController: IEditorContainerController, + ): Promise { + const c = new RawItemSliderController(editorModel, parentController); + await c.init(); + return c; + } +} diff --git a/src/rawitem-slider/rawitem-slider.scss b/src/rawitem-slider/rawitem-slider.scss new file mode 100644 index 0000000..4196c56 --- /dev/null +++ b/src/rawitem-slider/rawitem-slider.scss @@ -0,0 +1,76 @@ +@include b('rawitem-slider'){ + display: flex; + gap: 10px; + align-items: end; + width: 100%; + @include e('slider'){ + flex: 1; + @include m('line'){ + position: relative; + width: 100%; + height: 1px; + overflow: visible; + background-color: gray; + opacity: 0.6; + + &::before { + position: absolute; + top: -2px; + left: 0; + width: 4px; + height: 4px; + content: ""; + background-color: getCssVar(screen-dashboard, primary-color); + } + + &::after { + position: absolute; + top: -2px; + right: 0; + width: 4px; + height: 4px; + content: ""; + background-color: getCssVar(screen-dashboard, primary-color); + } + } + @include m('cover-slider'){ + position: relative; + width: 100%; + height: 5px; + margin-top: 16px; + background-color: getCssVar(color,fill,0); + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAT4AAAAGCAYAAABaZvJEAAAA20lEQVRYhe3YvyrGcRQG8A+9G4MdxawMb8kV2Bis/sRiklyDa2BSeheTxSiTiYhJJhvlzw2w6dGvvjfxvp3p1OnznPnpjCXZwTqucIsPzGAL0zjBN96xXbZs2bLDbsexj0k84wVz2MMKPnHfwhtly5YtOwq2h1+c4RV97GIZdzhvR9ZwiJ+yZcuWHXbbNb7TVgtnG1rCYwt31XEVB/grW7Zs2ZGwSSaSdPMoyUOS4yT9tltIcpPkKclm2bJly46C7bWH3zymcI1LvLV6uIgvDHDRXNmyZcsOr2XwD/WvZ19mO61bAAAAAElFTkSuQmCC"); + } + @include m('use-cover'){ + position: absolute; + left: 0; + width: 0; + height: 100%; + background-color: getCssVar(screen-dashboard, primary-color); + opacity: 0.5; + } + @include m('slider-arrow'){ + position: absolute; + top: -28px; + left: 0; + color: getCssVar(screen-dashboard, primary-color); + opacity: 0.5; + } + } + @include e('value'){ + display: flex; + flex: 0; + align-items: end; + color: getCssVar(color,primary,light,active); + @include m('current'){ + display: inline-block; + font-size: 32px; + line-height: 1; + } + @include m('total'){ + display: inline-block; + font-size: 16px; + font-weight: 600; + } + } +} \ No newline at end of file diff --git a/src/rawitem-slider/rawitem-slider.tsx b/src/rawitem-slider/rawitem-slider.tsx new file mode 100644 index 0000000..4fbc229 --- /dev/null +++ b/src/rawitem-slider/rawitem-slider.tsx @@ -0,0 +1,59 @@ +import { defineComponent, ref, watch } from 'vue'; +import { useNamespace, getSliderProps } from '@ibiz-template/vue3-util'; +import './rawitem-slider.scss'; +import { RawItemSliderController } from './rawitem-slider.controller'; + +export const RawItemSlider = defineComponent({ + name: 'RawItemSlider', + props: getSliderProps(), + setup(props) { + const ns = useNamespace('rawitem-slider'); + const c = props.controller; + const total = ref(0); + + const useCover = () => { + const tempValue = Number(props.value) || 0; + return `${total.value === 0 ? 0 : Math.round((tempValue / total.value) * 100) || 0}%`; + }; + + watch( + () => props.data[c.totalField], + newVal => { + if (newVal || newVal === 0) { + total.value = newVal; + } else { + total.value = 0; + } + }, + { + immediate: true, + }, + ); + + return { ns, useCover, total }; + }, + render() { + return ( +
+
+
+
+
+ +
+
+
+
{this.value}
+
/{this.total}
+
+
+ ); + }, +}); diff --git a/vite.config.ts b/vite.config.ts index b7c8c43..f8768cb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -25,6 +25,7 @@ export default defineConfig({ 'vuedraggable', 'axios', 'ramda', + 'lodash-es', 'echarts', 'dayjs', ], -- Gitee