diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fc239ea5d23fef262f952bfbf0a7383addbf6bf..289296ec1ae3e5a0a05f873022e9ec1e494ee6f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ - 新增树导航视图的视图刷新能力 - 新增树面包屑绘制模式(头部样式),当配置树控件动态参数crumbshowmode=HEADERSTYLE或处于树导航栏视图时,将绘制该样式 - modal工具补充extendConfirm弹出确认操作方法 +- 移动端数据看板支持自定义布局和导航栏 +- 看板部件支持锚点配置 ### Change diff --git a/src/control/dashboard/custom-dashboard-design/custom-dashboard-design.scss b/src/control/dashboard/custom-dashboard-design/custom-dashboard-design.scss new file mode 100644 index 0000000000000000000000000000000000000000..48dae347f821a7f54158749d7fc2d073caba38e8 --- /dev/null +++ b/src/control/dashboard/custom-dashboard-design/custom-dashboard-design.scss @@ -0,0 +1,135 @@ +$custom-dashboard-design: ( + // 颜色 + color: getCssVar(color, text, 0), + bg-color: getCssVar(color, bg, 0), + save-btn-color: getCssVar(color, primary), + group-header-color: getCssVar(color, text, 1), + group-content-bg-color: getCssVar(color, bg, 1), + remove-btn-color: getCssVar(color, danger), + add-btn-color: getCssVar(color, primary), + + // 间距 + header-padding: getCssVar(spacing, base), + content-padding: 0 getCssVar(spacing, base) getCssVar(spacing, base), + group-margin-bottom: getCssVar(spacing, base), + group-header-margin-bottom: getCssVar(spacing, base), + group-content-padding: getCssVar(spacing, base) getCssVar(spacing, base-tight), + item-padding: getCssVar(spacing, base-tight) 0, + btn-margin-right: getCssVar(spacing, tight), + + // 圆角 + group-content-border-radius: getCssVar(border, radius, medium), + + // 字体大小 + btn-font-size: getCssVar(font-size, header-3), + + // 字体粗细 + header-title-font-weight: getCssVar(font-weight, bold), + + // 边框 + item-border-bottom: 1px solid getCssVar(color, border), +); + +@include b(custom-dashboard-design) { + @include set-component-css-var('custom-dashboard-design', $custom-dashboard-design); + + display: flex; + flex-direction: column; + height: 100%; + color: getCssVar(custom-dashboard-design, color); + background-color: getCssVar(custom-dashboard-design, bg-color); +} + +@include b(custom-dashboard-design-header) { + display: flex; + flex: 0 0 auto; + align-items: center; + justify-content: space-between; + padding: getCssVar(custom-dashboard-design, header-padding); + + @include e(close-btn) { + flex-shrink: 0; + font-size: getCssVar(custom-dashboard-design, btn-font-size); + cursor: pointer; + } + + @include e(title) { + overflow: hidden; + font-weight: getCssVar(custom-dashboard-design, header-title-font-weight); + text-overflow: ellipsis; + white-space: nowrap; + } + + @include e(save-btn) { + flex-shrink: 0; + color: getCssVar(custom-dashboard-design, save-btn-color); + cursor: pointer; + } +} + +@include b(custom-dashboard-design-content) { + flex: 1 1 0; + padding: getCssVar(custom-dashboard-design, content-padding); + overflow: auto; +} + +@include b(custom-dashboard-design-group) { + margin-bottom: getCssVar(custom-dashboard-design, group-margin-bottom); + + &:last-child { + margin-bottom: 0; + } +} + +@include b(custom-dashboard-design-group-header) { + margin-bottom: getCssVar(custom-dashboard-design, group-header-margin-bottom); + color: getCssVar(custom-dashboard-design, group-header-color); +} + +@include b(custom-dashboard-design-group-content) { + padding: getCssVar(custom-dashboard-design, group-content-padding); + background-color: getCssVar(custom-dashboard-design, group-content-bg-color); + border-radius: getCssVar(custom-dashboard-design, group-content-border-radius); + + .van-empty { + padding: 0; + } + + .van-empty__bottom { + margin-top: 0; + } +} + +@include b(custom-dashboard-design-item) { + display: flex; + padding: getCssVar(custom-dashboard-design, item-padding); + border-bottom: getCssVar(custom-dashboard-design, item-border-bottom); + + @include e(remove-btn) { + flex-shrink: 0; + margin-right: getCssVar(custom-dashboard-design, btn-margin-right); + font-size: getCssVar(custom-dashboard-design, btn-font-size); + color: getCssVar(custom-dashboard-design, remove-btn-color); + cursor: pointer; + } + + @include e(add-btn) { + flex-shrink: 0; + margin-right: getCssVar(custom-dashboard-design, btn-margin-right); + font-size: getCssVar(custom-dashboard-design, btn-font-size); + color: getCssVar(custom-dashboard-design, add-btn-color); + cursor: pointer; + } + + @include e(text) { + flex: 1 1 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &:first-child { + padding-top: 0; + } +} + diff --git a/src/control/dashboard/custom-dashboard-design/custom-dashboard-design.tsx b/src/control/dashboard/custom-dashboard-design/custom-dashboard-design.tsx new file mode 100644 index 0000000000000000000000000000000000000000..652ac625c1d9c95703d9d5d056059c51f40f8bae --- /dev/null +++ b/src/control/dashboard/custom-dashboard-design/custom-dashboard-design.tsx @@ -0,0 +1,151 @@ +import { DashboardController, IModal } from '@ibiz-template/runtime'; +import { useNamespace } from '@ibiz-template/vue3-util'; +import { PropType, defineComponent, ref } from 'vue'; +import './custom-dashboard-design.scss'; + +export const CustomDashboardDesign = defineComponent({ + name: 'IBizCustomDashboardDesign', + props: { + controller: { + type: Object as PropType, + required: true, + }, + modal: { + type: Object as PropType, + required: true, + }, + }, + setup(props) { + const ns = useNamespace('custom-dashboard-design'); + + // 自定义看板控制器 + const customDashboard = props.controller.getMobCustomDashboard()!; + + // 门户部件 + const portlets = Object.values(props.controller.portlets).filter( + item => !customDashboard.ignoreType.includes(item.model.portletType!), + ); + + // 门户部件列表 + const items = portlets.map(item => { + return { + id: item.model.id!, + title: item.model.title, + }; + }); + + // 默认门户部件列表 + const defaultItems = ref( + items.filter(item => { + if (!Array.isArray(customDashboard.portlets)) { + return true; + } + return customDashboard.portlets.includes(item.id); + }), + ); + + // 隐藏门户部件列表 + const hiddenItems = ref( + items.filter(item => !defaultItems.value.includes(item)), + ); + + // 处理移除门户部件 + const handleRemove = (index: number) => { + hiddenItems.value.push(...defaultItems.value.splice(index, 1)); + }; + + // 处理添加门户部件 + const handleAdd = (index: number) => { + defaultItems.value.push(...hiddenItems.value.splice(index, 1)); + }; + + // 处理关闭 + const handleClose = () => { + props.modal.dismiss(); + }; + + // 处理保存 + const handleSave = async () => { + await customDashboard.save(defaultItems.value.map(item => item.id)); + handleClose(); + }; + + return { + ns, + defaultItems, + hiddenItems, + handleRemove, + handleAdd, + handleClose, + handleSave, + }; + }, + render() { + return ( +
+
+ +
+ {ibiz.i18n.t('control.dashboard.customLayout')} +
+
+ {ibiz.i18n.t('control.dashboard.save')} +
+
+
+
+
+ {ibiz.i18n.t('control.dashboard.showList')} +
+
+ {this.defaultItems.map((item, index) => { + return ( +
+ this.handleRemove(index)} + > +
+ {item.title || item.id} +
+
+ ); + })} + {this.defaultItems.length === 0 ? : null} +
+
+
+
+ {ibiz.i18n.t('control.dashboard.hiddenList')} +
+
+ {this.hiddenItems.map((item, index) => { + return ( +
+ this.handleAdd(index)} + > +
+ {item.title || item.id} +
+
+ ); + })} + {this.hiddenItems.length === 0 ? : null} +
+
+
+
+ ); + }, +}); diff --git a/src/control/dashboard/dashboard.scss b/src/control/dashboard/dashboard.scss index 00307a81f0824de311678c0869b8d5839cd41f86..13eb0cc33332ca130ad03fe05e77e0436b3491f3 100644 --- a/src/control/dashboard/dashboard.scss +++ b/src/control/dashboard/dashboard.scss @@ -1,6 +1,20 @@ $control-dashboard: ( // 高度 portlet-header-height: rem(50px), + + // 边距 + sidebar-topleft-top: calc(#{getCssVar(view-page-caption, line-height)} + 2 * #{getCssVar(spacing, tight)}), + sidebar-topleft-left: rem(0px), + sidebar-topright-top: calc(#{getCssVar(view-page-caption, line-height)} + 2 * #{getCssVar(spacing, tight)}), + sidebar-topright-right: rem(0px), + sidebar-bottomleft-bottom: rem(0px), + sidebar-bottomleft-left: rem(0px), + sidebar-bottomright-bottom: rem(0px), + sidebar-bottomright-right: rem(0px), + sidebar-middleleft-top: 50%, + sidebar-middleleft-left: rem(0px), + sidebar-middleright-top: 50%, + sidebar-middleright-right: rem(0px), ); @include b(control-dashboard) { @@ -9,6 +23,58 @@ $control-dashboard: ( height: 100%; overflow: getCssVar(control, overflow); + + @include e(nav-bar) { + @include m(topleft) { + .van-index-bar__sidebar { + top: getCssVar('control-dashboard', 'sidebar-topleft-top'); + right: auto; + left: getCssVar('control-dashboard', 'sidebar-topleft-left'); + transform: none; + } + } + + @include m(topright) { + .van-index-bar__sidebar { + top: getCssVar('control-dashboard', 'sidebar-topright-top'); + right: getCssVar('control-dashboard', 'sidebar-topright-right'); + transform: none; + } + } + + @include m(bottomleft) { + .van-index-bar__sidebar { + inset: auto auto getCssVar('control-dashboard', 'sidebar-bottomleft-bottom') getCssVar('control-dashboard', 'sidebar-bottomleft-left'); + transform: none; + } + } + + @include m(bottomright) { + .van-index-bar__sidebar { + top: auto; + right: getCssVar('control-dashboard', 'sidebar-bottomright-right'); + bottom: getCssVar('control-dashboard', 'sidebar-bottomright-bottom'); + transform: none; + } + } + + @include m(middleleft) { + .van-index-bar__sidebar { + top: getCssVar('control-dashboard', 'sidebar-middleleft-top'); + right: auto; + left: getCssVar('control-dashboard', 'sidebar-middleleft-left'); + } + } + + @include m(middleright) { + .van-index-bar__sidebar { + top: getCssVar('control-dashboard', 'sidebar-middleright-top'); + right: getCssVar('control-dashboard', 'sidebar-middleright-right'); + } + } + + } + @include b(control-dashboard-row) { gap: getCssVar('control-dashboard', 'dashboard-margin'); } diff --git a/src/control/dashboard/dashboard.tsx b/src/control/dashboard/dashboard.tsx index 81379ad6efa736175f3309dde32907ccdb6bb614..3aeea5c9d0adb0fa0e357fde40d31320e9e11a6f 100644 --- a/src/control/dashboard/dashboard.tsx +++ b/src/control/dashboard/dashboard.tsx @@ -3,7 +3,10 @@ import { ConcreteComponent, defineComponent, h, + isReactive, PropType, + reactive, + ref, resolveComponent, VNode, } from 'vue'; @@ -15,8 +18,11 @@ import { import { DashboardController, IControlProvider, + IModal, IPortletController, + MobCustomDashboardController, } from '@ibiz-template/runtime'; +import { CustomDashboardDesign } from './custom-dashboard-design/custom-dashboard-design'; import './dashboard.scss'; /** @@ -31,7 +37,7 @@ function renderPortletByType( model: IDBPortletPart, c: DashboardController, opts?: IData, -): VNode { +): VNode | null { const provider = c.providers[model.id!]; const controller = c.portlets[model.id!]; const commonProps = { @@ -39,6 +45,10 @@ function renderPortletByType( controller, }; + if (!controller.state.visible) { + return null; + } + if (!provider) { return (
@@ -117,26 +127,113 @@ export const DashboardControl = defineComponent({ return part.state; }; - return { c, ns, calcLayoutHeightWidth }; + c.evt.on('onInitPortlets', () => { + // 数据看板成员state响应式 + Object.values(c.portlets).forEach(portlet => { + if (!isReactive(portlet.state)) { + portlet.state = reactive(portlet.state); + } + }); + }); + + // 是否已初始化 + const initialized = ref(false); + + // 是否显示浮动按钮 + const isShowFloatingButton = ref(!!c.model.enableCustomized); + + // 初始化 + const init = async () => { + try { + if (c.model.enableCustomized) { + const customDashboard = new MobCustomDashboardController(c.model, c); + c.setMobCustomDashboard(customDashboard); + await customDashboard.load(); + } + } finally { + initialized.value = true; + } + }; + + init(); + + // 处理浮动按钮点击 + const handleFloatingButtonClick = async () => { + await ibiz.overlay.drawer( + (modal: IModal) => { + return h(CustomDashboardDesign, { + modal, + controller: c, + }); + }, + {}, + { + attrs: { + position: 'bottom', + closeable: false, + }, + }, + ); + }; + + return { + c, + ns, + initialized, + isShowFloatingButton, + calcLayoutHeightWidth, + handleFloatingButtonClick, + }; }, render() { const { state, model } = this.c; + + let content = ( + + {model.controls?.map((child: IDBPortletPart) => { + return ( + + {renderPortletByType(child, this.c)} + + ); + })} + + ); + + if (model.showDashboardNavBar) { + const anchorList = this.c.enableAnchorCtrls.filter(ctrl => { + const controller = this.c.portlets[ctrl.id!]; + return controller.state.visible; + }); + const list = anchorList.map(ctrl => ctrl.title || ctrl.id); + content = ( + + {content} + + ); + } + return ( - {state.isCreated && ( - - {model.controls?.map((child: IDBPortletPart) => { - return ( - - {renderPortletByType(child, this.c)} - - ); - })} - + {state.isCreated && this.initialized && content} + {state.isCreated && this.initialized && this.isShowFloatingButton && ( + )} ); diff --git a/src/control/dashboard/portlet/portlet-layout/portlet-layout.scss b/src/control/dashboard/portlet/portlet-layout/portlet-layout.scss index 6d5181daf41d65606d1c747acc129641279ec848..952c07a61f6967eabd2c7034ea50488e5838a20c 100644 --- a/src/control/dashboard/portlet/portlet-layout/portlet-layout.scss +++ b/src/control/dashboard/portlet/portlet-layout/portlet-layout.scss @@ -51,6 +51,15 @@ $portlet-layout: ( padding: 0; } } + + .van-index-anchor { + padding: 0; + font-size: inherit; + font-weight: inherit; + line-height:inherit; + color: inherit; + background: inherit; + } } // 头部样式 diff --git a/src/control/dashboard/portlet/portlet-layout/portlet-layout.tsx b/src/control/dashboard/portlet/portlet-layout/portlet-layout.tsx index 6920d49609aee3a1719c8ffe1be946f97159a0fb..81cb1e354130ff3095dd0e9758346f432b5d630b 100644 --- a/src/control/dashboard/portlet/portlet-layout/portlet-layout.tsx +++ b/src/control/dashboard/portlet/portlet-layout/portlet-layout.tsx @@ -155,6 +155,16 @@ export const PortletLayout = defineComponent({ ); } } + if ( + model.enableAnchor && + this.controller.dashboard.model.showDashboardNavBar + ) { + headerRender = ( + + {headerRender} + + ); + } return (
{headerRender} diff --git a/src/locale/en/index.ts b/src/locale/en/index.ts index 5054b6107912a68e6e4596683618c3f6a9af041d..bb505c22cca96b3ade1c8e11130243bd7f7f4a64 100644 --- a/src/locale/en/index.ts +++ b/src/locale/en/index.ts @@ -164,6 +164,12 @@ export default { pickerDate: 'Picker Date', customPicker: 'Please select a date', }, + dashboard: { + customLayout: 'Custom Layout', + save: 'Save', + showList: 'Show List', + hiddenList: 'Hidden List', + }, }, // 编辑器 editor: { diff --git a/src/locale/zh-CN/index.ts b/src/locale/zh-CN/index.ts index 162e4dfa0233ff9b51d2c126d256dbc2630b761b..f01e76ede2f19741e3e4b40c52384690ec028a9e 100644 --- a/src/locale/zh-CN/index.ts +++ b/src/locale/zh-CN/index.ts @@ -145,6 +145,12 @@ export default { pickerDate: '选择日期', customPicker: '请选择日期', }, + dashboard: { + customLayout: '自定义布局', + save: '保存', + showList: '显示清单', + hiddenList: '隐藏清单', + }, }, // 编辑器 editor: {