From 1c83d0a1b4f5e876718fce8da144ce93cc0b9c8e Mon Sep 17 00:00:00 2001 From: Wang Jason Date: Mon, 1 Dec 2025 10:27:55 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=BB=9A=E5=8A=A8=E5=AF=BC=E8=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/mobile-ui-vue/components/index.ts | 4 +- .../combo-list/src/schema/schema-resolver.ts | 2 +- .../designer-canvas/src/components/maps.ts | 3 +- packages/ui-vue/components/index.ts | 2 + .../ui-vue/components/scroll-navbar/index.ts | 24 ++ .../src/components/tab-content.component.tsx | 53 ++++ .../src/components/tab-panel.component.tsx | 63 +++++ .../src/components/tab-panel.props.ts | 20 ++ .../scroll-navbar/src/components/tab.props.ts | 53 ++++ .../scroll-navbar/src/composition/types.ts | 74 ++++++ .../src/composition/use-items.ts | 99 +++++++ .../src/composition/use-navbar.ts | 245 ++++++++++++++++++ .../src/composition/use-scroll.ts | 98 +++++++ .../scroll-navbar.design.component.tsx | 45 ++++ .../src/designer/use-designer-rules.ts | 204 +++++++++++++++ .../scroll-navbar/src/schema/schema-mapper.ts | 7 + .../src/schema/schema-resolver.ts | 75 ++++++ .../src/schema/scroll-navbar.schema.json | 72 +++++ .../src/scroll-navbar.component.tsx | 98 +++++++ .../scroll-navbar/src/scroll-navbar.props.ts | 40 +++ .../scroll-navbar/src/scroll-navbar.scss | 71 +++++ 21 files changed, 1349 insertions(+), 3 deletions(-) create mode 100644 packages/ui-vue/components/scroll-navbar/index.ts create mode 100644 packages/ui-vue/components/scroll-navbar/src/components/tab-content.component.tsx create mode 100644 packages/ui-vue/components/scroll-navbar/src/components/tab-panel.component.tsx create mode 100644 packages/ui-vue/components/scroll-navbar/src/components/tab-panel.props.ts create mode 100644 packages/ui-vue/components/scroll-navbar/src/components/tab.props.ts create mode 100644 packages/ui-vue/components/scroll-navbar/src/composition/types.ts create mode 100644 packages/ui-vue/components/scroll-navbar/src/composition/use-items.ts create mode 100644 packages/ui-vue/components/scroll-navbar/src/composition/use-navbar.ts create mode 100644 packages/ui-vue/components/scroll-navbar/src/composition/use-scroll.ts create mode 100644 packages/ui-vue/components/scroll-navbar/src/designer/scroll-navbar.design.component.tsx create mode 100644 packages/ui-vue/components/scroll-navbar/src/designer/use-designer-rules.ts create mode 100644 packages/ui-vue/components/scroll-navbar/src/schema/schema-mapper.ts create mode 100644 packages/ui-vue/components/scroll-navbar/src/schema/schema-resolver.ts create mode 100644 packages/ui-vue/components/scroll-navbar/src/schema/scroll-navbar.schema.json create mode 100644 packages/ui-vue/components/scroll-navbar/src/scroll-navbar.component.tsx create mode 100644 packages/ui-vue/components/scroll-navbar/src/scroll-navbar.props.ts create mode 100644 packages/ui-vue/components/scroll-navbar/src/scroll-navbar.scss diff --git a/packages/mobile-ui-vue/components/index.ts b/packages/mobile-ui-vue/components/index.ts index 48c39ab0ff..dda835cb0a 100644 --- a/packages/mobile-ui-vue/components/index.ts +++ b/packages/mobile-ui-vue/components/index.ts @@ -38,6 +38,7 @@ import TabBar from "./tab-bar"; import SwipeCell from './swipe-cell'; import ActionSheet from './action-sheet'; import Tab from './tab'; +// import ScrollNavbar from './scroll-navbar'; import Tabs from './tabs'; import Tag from './tag'; import PageContainer from './page-container'; @@ -108,7 +109,7 @@ export const components = [ Component, ContentContainer, FloatContainer, - + // ScrollNavbar, Toast, Notify, Loading, @@ -151,6 +152,7 @@ export { FormItem, Filter, Popup, + // ScrollNavbar, // Popover, Picker, PickerGroup, diff --git a/packages/ui-vue/components/combo-list/src/schema/schema-resolver.ts b/packages/ui-vue/components/combo-list/src/schema/schema-resolver.ts index d36ae74574..08dbffa30c 100644 --- a/packages/ui-vue/components/combo-list/src/schema/schema-resolver.ts +++ b/packages/ui-vue/components/combo-list/src/schema/schema-resolver.ts @@ -1,5 +1,5 @@ import { DynamicResolver } from "@farris/ui-vue/components/dynamic-resolver"; -export function schemaResolver(resolver: DynamicResolver, schema: Record, context: Record): Record { +export function schemaResolver(resolver: DynamicResolver, schema: Record, context: Record): Record { return schema; } diff --git a/packages/ui-vue/components/designer-canvas/src/components/maps.ts b/packages/ui-vue/components/designer-canvas/src/components/maps.ts index 44cf29dcd8..b07bf2d877 100644 --- a/packages/ui-vue/components/designer-canvas/src/components/maps.ts +++ b/packages/ui-vue/components/designer-canvas/src/components/maps.ts @@ -59,6 +59,7 @@ import FDrawer from '@farris/ui-vue/components/drawer/designer'; import FHtmlTemplate from '@farris/ui-vue/components/html-template'; import FImage from '@farris/ui-vue/components/image'; import FComment from '@farris/ui-vue/components/comment'; +import FScrollNavbar from '@farris/ui-vue/components/scroll-navbar'; import { RegisterContext, useThirdComponent } from '@farris/ui-vue/components/common'; import { createPropsResolver, propertyConfigSchemaMapForDesigner, propertyEffectMapForDesigner } from '@farris/ui-vue/components/dynamic-resolver'; import { schemaMapForDesigner, schemaResolverMapForDesigner } from '@farris/ui-vue/components/dynamic-resolver'; @@ -140,7 +141,7 @@ function loadDesignerRegister() { FImage.registerDesigner(componentMap, componentPropsConverter, componentPropertyConfigConverter); FComment.registerDesigner(componentMap, componentPropsConverter, componentPropertyConfigConverter); FHtmlTemplate.registerDesigner(componentMap, componentPropsConverter, componentPropertyConfigConverter); - + FScrollNavbar.registerDesigner(componentMap, componentPropsConverter); // FHtmlEditor.createPropsResolver = createPropsResolver; // FHtmlEditor.registerDesigner(componentMap, componentPropsConverter, componentPropertyConfigConverter); const thirdComponents = window[globalStorageKey]; diff --git a/packages/ui-vue/components/index.ts b/packages/ui-vue/components/index.ts index 234bf0c16a..f1a1f57fa8 100644 --- a/packages/ui-vue/components/index.ts +++ b/packages/ui-vue/components/index.ts @@ -44,6 +44,7 @@ import Layout from './layout'; import ListNav from './list-nav'; import Loading from './loading'; import Lookup from './lookup'; +import ScrollNavbar from './scroll-navbar'; import Modal from './modal'; import MessageBox from './message-box'; import Nav from './nav'; @@ -114,6 +115,7 @@ export default { .use(Accordion) .use(Avatar) .use(Image) + .use(ScrollNavbar) .use(BorderEditor) .use(Button) .use(ButtonGroup) diff --git a/packages/ui-vue/components/scroll-navbar/index.ts b/packages/ui-vue/components/scroll-navbar/index.ts new file mode 100644 index 0000000000..dd90157faf --- /dev/null +++ b/packages/ui-vue/components/scroll-navbar/index.ts @@ -0,0 +1,24 @@ +import type { App } from 'vue'; +//import ScrollNavbar from './src/scroll-navbar.component'; +import { withInstall } from '@farris/ui-vue/components/common'; +import ScrollNavbarInstallless from './src/scroll-navbar.component'; +import {propsResolverGenerator} from './src/scroll-navbar.props' +import ScrollNavbarDesign from './src/designer/scroll-navbar.design.component' +export * from './src/scroll-navbar.props'; + +const ScrollNavbar = withInstall(ScrollNavbarInstallless); +export { ScrollNavbar }; + +export default { + install(app: App): void { + app.component(ScrollNavbarInstallless.name as string, ScrollNavbarInstallless); + }, + register(componentMap: Record, propsResolverMap: Record): void { + componentMap['scroll-navbar'] = ScrollNavbarInstallless; + propsResolverMap['scroll-navbar'] = propsResolverGenerator; + }, + registerDesigner(componentMap: Record, propsResolverMap: Record): void { + componentMap['scroll-navbar'] = ScrollNavbarDesign; + propsResolverMap['scroll-navbar'] = propsResolverGenerator; + } +}; \ No newline at end of file diff --git a/packages/ui-vue/components/scroll-navbar/src/components/tab-content.component.tsx b/packages/ui-vue/components/scroll-navbar/src/components/tab-content.component.tsx new file mode 100644 index 0000000000..36f06eeee5 --- /dev/null +++ b/packages/ui-vue/components/scroll-navbar/src/components/tab-content.component.tsx @@ -0,0 +1,53 @@ +import { defineComponent, computed } from 'vue'; +import { NavbarItem } from '../composition/types'; +import { ScrollNavbarProps } from '../scroll-navbar.props'; + +/** + * Tab Content 组件属性 + */ +interface TabContentProps { + items: NavbarItem[]; + currentValue: string | number; + type?: string; +} + +/** + * Tab Content 组件 - 主要标签内容显示 + */ +export default defineComponent({ + name: 'FTabContent', + props: { + items: { type: Array as () => NavbarItem[], required: true }, + currentValue: { type: [String, Number], required: true }, + type: { type: String, default: 'line' } + }, + + emits: ['tabChange'], + + setup(props: TabContentProps, { emit }) { + /** 处理标签切换 */ + const handlertabChange = (item: NavbarItem) => { + emit('tabChange', item); + }; + + /** 标签项样式类 */ + const tabItemClass = computed(() => ({ + 'f-tab-bar-item': true, + 'f-tab-bar-item-line': props.type === 'line', + 'f-tab-bar-item-nav': props.type === 'nav' + })); + + return () => ( + + {/* 支持默认插槽用于自定义内容 */} + + + ); + } +}); \ No newline at end of file diff --git a/packages/ui-vue/components/scroll-navbar/src/components/tab-panel.component.tsx b/packages/ui-vue/components/scroll-navbar/src/components/tab-panel.component.tsx new file mode 100644 index 0000000000..3cd8842843 --- /dev/null +++ b/packages/ui-vue/components/scroll-navbar/src/components/tab-panel.component.tsx @@ -0,0 +1,63 @@ +import { defineComponent, computed } from 'vue'; +import type { PropType } from 'vue'; +import { NavbarItem } from '../composition/types'; +import { tabPanelProps } from './tab-panel.props'; + +/** + * Tab Panel 组件 - 用于扩展面板显示 + * 修正了 defineComponent 的重载匹配问题 + */ +export default defineComponent({ + name: 'FTabPanel', + + props: { + title: { + type: String as PropType, + default: '' + }, + active: { + type: [String, Number] as PropType, + default: '' + }, + items: { + type: Array as PropType, + default: () => [] as NavbarItem[] + } + }, + + emits: ['itemClick'] as const, + + setup(props, { emit }) { + /** 处理项目点击事件 */ + const handleItemClick = (item: NavbarItem, index: number): void => { + emit('itemClick', { item, index }); + }; + + return () => ( +
+ {/* 面板头部 */} + {props.title && ( +
+ {props.title} +
+ )} + + {/* 面板内容 */} +
+ {props.items.map((item: NavbarItem, index: number) => ( +
handleItemClick(item, index)} + > + {item.title} +
+ ))} +
+
+ ); + } +}); \ No newline at end of file diff --git a/packages/ui-vue/components/scroll-navbar/src/components/tab-panel.props.ts b/packages/ui-vue/components/scroll-navbar/src/components/tab-panel.props.ts new file mode 100644 index 0000000000..ede3d2f420 --- /dev/null +++ b/packages/ui-vue/components/scroll-navbar/src/components/tab-panel.props.ts @@ -0,0 +1,20 @@ +import { PropType } from 'vue'; +import { NavbarItem } from '../composition/types'; + +export const tabPanelProps = { + /** 面板标题 */ + title: { + type: String as PropType, + default: '' + }, + /** 当前激活的项 */ + active: { + type: [String, Number] as PropType, + default: '' + }, + /** 导航项列表 */ + items: { + type: Array as PropType, + default: () => [] as NavbarItem[] + } +} as const; \ No newline at end of file diff --git a/packages/ui-vue/components/scroll-navbar/src/components/tab.props.ts b/packages/ui-vue/components/scroll-navbar/src/components/tab.props.ts new file mode 100644 index 0000000000..586c9d5d3c --- /dev/null +++ b/packages/ui-vue/components/scroll-navbar/src/components/tab.props.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2020 - present, Inspur Genersoft Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ExtractPropTypes } from 'vue'; + +export const TAB_NAME = 'fm-tab'; + +export const tabProps = { + + /** 标签的名称,作为匹配的唯一标识 */ + name: { type: [String, Number], default: '' }, + + /** 显示的标题 */ + title: { type: String, default: '' }, + + /** 图标,如果非空则显示在标题的左侧 */ + icon: { type: String, default: undefined }, + + /** 图标的颜色 */ + iconColor: { type: String, default: undefined }, + + /** 选中状态下的图标 */ + activeIcon: { type: String, default: undefined }, + + /** 选中状态下的图标的颜色 */ + activeIconColor: { type: String, default: undefined }, + + /** 是否在标题右上角显示一个小圆点 */ + dot: { type: Boolean, default: undefined }, + + /** 徽标的内容,如果非空则在标题的右上角显示一个徽标 */ + badge: { type: [String, Number], default: undefined }, + + /** 是否禁用当前标签页 */ + disabled: { type: Boolean, default: false }, + + /** 是否在隐藏时销毁内容 */ + destroyOnHide: { type: Boolean, default: false }, +}; + +export type TabProps = ExtractPropTypes; diff --git a/packages/ui-vue/components/scroll-navbar/src/composition/types.ts b/packages/ui-vue/components/scroll-navbar/src/composition/types.ts new file mode 100644 index 0000000000..4ea5632b56 --- /dev/null +++ b/packages/ui-vue/components/scroll-navbar/src/composition/types.ts @@ -0,0 +1,74 @@ +// composition/types.ts +import { Ref } from 'vue'; + +export interface NavbarItem { + name: string; + title: string; + offsetTop: number; + clientHeight: number; + element: Element; +} + +export interface ScrollState { + active: boolean; + start: number; + end: number; + timer: number; +} + +export interface UseItemsReturn { + items: Ref; + getItems: () => NavbarItem[]; + getTopElementInView: () => NavbarItem; +} + +export interface UseNavbarReturn { + shouldShowNavbar: Ref; + navbarFixedClass: Ref>; + handlertabChange: (item: NavbarItem) => void; + checkContentStyle: () => void; + handlerScroll: () => void; + checkNavbarShow: () => void; + navbarShow: Ref; + checkContentContainsNavbar: () => boolean; +} +export interface NavbarItem { + name: string; + title: string; + offsetTop: number; + clientHeight: number; + element: Element; +} + +export interface TabPanelProps { + title?: string; + active?: string | number; + items: NavbarItem[]; +} + +export interface TabPanelEmits { + (e: 'itemClick', payload: { item: NavbarItem; index: number }): void; +} + +export interface ScrollState { + active: boolean; + start: number; + end: number; + timer: number; +} + +export interface UseItemsReturn { + items: Ref; + getItems: () => NavbarItem[]; + getTopElementInView: () => NavbarItem; +} + +export interface UseNavbarReturn { + shouldShowNavbar: Ref; + navbarFixedClass: Ref>; + handlertabChange: (item: NavbarItem) => void; + handlePanelItemClick: (event: { item: NavbarItem; index: number }) => void; + checkContentStyle: () => void; + handlerScroll: () => void; + checkNavbarShow: () => void; +} \ No newline at end of file diff --git a/packages/ui-vue/components/scroll-navbar/src/composition/use-items.ts b/packages/ui-vue/components/scroll-navbar/src/composition/use-items.ts new file mode 100644 index 0000000000..5bc5bc1682 --- /dev/null +++ b/packages/ui-vue/components/scroll-navbar/src/composition/use-items.ts @@ -0,0 +1,99 @@ +import { Ref, ref } from 'vue'; +import { NavbarItem, UseItemsReturn } from './types'; + +export const useItems = ( + content: Ref, + itemRef: string | any[] +): UseItemsReturn => { + const items = ref([]); + + const getItemsByProps = (configItems: any[]): NavbarItem[] => { + return configItems + .map(item => { + const { id = '', title = '', selector = '', visible = true } = item; + if (!visible) return null; + + const itemEl = content.value.querySelector(selector); + if (!itemEl) return null; + + const { offsetTop, clientHeight } = itemEl as HTMLElement; + return { + name: id, + title: title, + offsetTop, + clientHeight, + element: itemEl, + }; + }) + .filter((item): item is NavbarItem => item !== null); + }; + + const getItemsBySelector = (selectors: string[]): NavbarItem[] => { + const itemEls: Element[] = []; + + selectors.forEach((selector: string) => { + const foundElements = Array.from(content.value.querySelectorAll(selector)); + itemEls.push(...foundElements); + }); + + return itemEls.map(itemEl => { + const { dataset, offsetTop, clientHeight } = itemEl as HTMLElement; + return { + name: dataset.id || '', + title: dataset.title || '', + offsetTop, + clientHeight, + element: itemEl, + }; + }); + }; + + const getItems = (): NavbarItem[] => { + items.value = []; + + if (!itemRef || (Array.isArray(itemRef) && itemRef.length === 0)) { + return items.value; + } + + try { + const innerValue = typeof itemRef === 'string' ? [itemRef] : itemRef; + + if (typeof innerValue[0] === 'string') { + items.value = getItemsBySelector(innerValue as string[]); + } else { + items.value = getItemsByProps(innerValue); + } + + // 按位置排序确保导航顺序正确 + items.value.sort((prev, next) => prev.offsetTop - next.offsetTop); + } catch (error) { + console.warn('获取导航项时出错:', error); + } + + return items.value; + }; + + const getTopElementInView = (): NavbarItem => { + const scrollTop = content.value.scrollTop || 0; + const currentItems = items.value; + + if (currentItems.length === 0) { + return { name: '', title: '', offsetTop: 0, clientHeight: 0, element: null as any }; + } + + for (let i = 0; i < currentItems.length; i++) { + const item = currentItems[i]; + if (item.offsetTop + item.clientHeight / 2 > scrollTop) { + return item; + } + } + + return currentItems[currentItems.length - 1]; + }; + + return { + items: ref(items.value), + getItems, + getTopElementInView + }; +}; \ No newline at end of file diff --git a/packages/ui-vue/components/scroll-navbar/src/composition/use-navbar.ts b/packages/ui-vue/components/scroll-navbar/src/composition/use-navbar.ts new file mode 100644 index 0000000000..00b1c972af --- /dev/null +++ b/packages/ui-vue/components/scroll-navbar/src/composition/use-navbar.ts @@ -0,0 +1,245 @@ +import { Ref, computed, onMounted, onUnmounted } from 'vue'; +import { SetupContext } from 'vue'; +import { ScrollNavbarProps } from '../scroll-navbar.props'; + +/** + * 导航项接口定义(移至当前文件避免导入问题) + */ +interface NavbarItem { + name: string; + title: string; + offsetTop: number; + clientHeight: number; + element: Element; +} + +/** + * 使用导航栏的组合式函数 + */ +export const useNavbar = ( + props: ScrollNavbarProps, + context: SetupContext, + navbarElement: Ref, + contentEl: Ref, + items: Ref, // 使用内部定义的NavbarItem接口 + navbarShow: Ref, + currentValue: Ref, + +) => { + let navbarElementTop = 0; + + /** 滚动状态管理 */ + const scrollState = { + active: false, + start: 0, + end: 0, + timer: 0 + }; + + /** 是否显示导航栏 */ + const shouldShowNavbar = computed(() => navbarShow.value); + + /** 导航栏固定定位的类名计算 */ + const navbarFixedClass = computed(() => { + const isFixed = contentEl.value.scrollTop > navbarElementTop; + return { + 'f-scroll-navbar': true, + 'f-scroll-navbar-fixed': isFixed, + 'f-scroll-navbar-visible': navbarShow.value + } as Record; + }); + + /** + * 检查内容区域是否包含导航栏 + */ + const checkContentContainsNavbar = (): boolean => { + if (!contentEl.value || !navbarElement.value) return false; + return contentEl.value.contains(navbarElement.value); + }; + + /** + * 检查导航栏显示状态 + */ + const checkNavbarShow = (): void => { + if (props.alwaysShow || checkContentContainsNavbar()) { + navbarShow.value = true; + return; + } + + const startTop = items.value[0] ? items.value[0].offsetTop : 0; + navbarShow.value = contentEl.value.scrollTop > startTop; + }; + + /** + * 检查并确保内容区域有定位样式 + */ + const checkContentStyle = (): void => { + const content = contentEl.value as HTMLElement; + if (!content || !(content instanceof HTMLElement)) { + console.warn('Content element is not available for style checking'); + return; + } + + const styles = getComputedStyle(content); + const allowedPositions = ['fixed', 'absolute', 'relative']; + + if (!styles.position || !allowedPositions.includes(styles.position)) { + content.style.position = 'relative'; + } + }; + + const handlePanelItemClick = (event: { item: NavbarItem; index: number }): void => { + + // 处理标签切换 + handlertabChange(event.item); + + // 可以在这里添加其他逻辑 + console.log(`点击了导航项: ${event.item.title}, 索引: ${event.index}`); + }; + + /** + * 标签切换处理函数 + */ + const handlertabChange = (item: NavbarItem): void => { + scrollState.active = true; + + // 平滑滚动到对应位置 + if (contentEl.value && 'scrollTo' in contentEl.value) { + contentEl.value.scrollTo({ + top: Math.max(0, item.offsetTop - 44), + behavior: 'smooth' + }); + } + + // 更新当前值 + currentValue.value = item.name; + + // 发射change事件 + context.emit('change', item); + + // 滚动结束检测 + const initialScrollTop = contentEl.value.scrollTop; + setTimeout(() => { + if (initialScrollTop === contentEl.value.scrollTop) { + scrollState.active = false; + } + }, 500); + }; + + /** + * 滚动结束检测 + */ + const checkScrollEnd = (afterScrollEnd: () => void): void => { + scrollState.end = contentEl.value.scrollTop; + if (scrollState.start === scrollState.end) { + scrollState.active = false; + afterScrollEnd(); + } + }; + + /** + * 滚动事件处理函数 + */ + const handlerScroll = (): void => { + if (!contentEl.value || !navbarElement.value) return; + + // 导航栏固定定位逻辑 + if (checkContentContainsNavbar()) { + if (!navbarElement.value.classList.contains('f-scroll-navbar-fixed')) { + navbarElementTop = navbarElement.value.offsetTop; + } + + const shouldFix = contentEl.value.scrollTop > navbarElementTop; + if (shouldFix) { + navbarElement.value.classList.add('f-scroll-navbar-fixed'); + } else { + navbarElement.value.classList.remove('f-scroll-navbar-fixed'); + } + } + + // 滚动状态处理 + if (scrollState.active) { + clearTimeout(scrollState.timer); + scrollState.timer = window.setTimeout(() => { + checkScrollEnd(checkNavbarShow); + }, 100); + scrollState.start = contentEl.value.scrollTop; + return; + } + + // 查找当前可视区域顶部的元素 + const scrollTop = contentEl.value.scrollTop; + let topElement: NavbarItem | undefined; + + for (const item of items.value) { + if (item.offsetTop + item.clientHeight / 2 > scrollTop) { + topElement = item; + break; + } + } + + // 更新当前激活的导航项 + if (topElement) { + currentValue.value = topElement.name; + } else if (items.value.length > 0) { + currentValue.value = items.value[items.value.length - 1].name; + } + + checkNavbarShow(); + }; + + /** + * 初始化滚动监听 + */ + const initScrollListener = (): void => { + if (contentEl.value) { + contentEl.value.addEventListener('scroll', handlerScroll); + } + }; + + /** + * 清理滚动监听 + */ + const cleanupScrollListener = (): void => { + if (contentEl.value) { + contentEl.value.removeEventListener('scroll', handlerScroll); + } + }; + + // 生命周期钩子 + onMounted(() => { + initScrollListener(); + checkContentStyle(); + checkNavbarShow(); + }); + + onUnmounted(() => { + cleanupScrollListener(); + }); + + return { + /** 是否显示导航栏 */ + shouldShowNavbar, + /** 导航栏样式类 */ + navbarFixedClass, + /** 标签切换处理 */ + handlertabChange, + /** 面板项目点击处理 */ + handlePanelItemClick, + /** 检查内容样式 */ + checkContentStyle, + /** 滚动处理 */ + handlerScroll, + /** 检查导航栏显示状态 */ + checkNavbarShow, + /** 导航栏显示状态引用 */ + navbarShow, + /** 检查内容包含 */ + checkContentContainsNavbar + }; +}; + +/** + * 导出类型定义供其他文件使用 + */ +export type { NavbarItem }; \ No newline at end of file diff --git a/packages/ui-vue/components/scroll-navbar/src/composition/use-scroll.ts b/packages/ui-vue/components/scroll-navbar/src/composition/use-scroll.ts new file mode 100644 index 0000000000..efd3df2a66 --- /dev/null +++ b/packages/ui-vue/components/scroll-navbar/src/composition/use-scroll.ts @@ -0,0 +1,98 @@ +// import { Ref, nextTick, ref } from "vue"; +// import { ScrollNavbarProps } from '../scroll-navbar.props'; +// import { Item, ScrollState, UseScrollReturn } from './types'; +// import { UseItemsReturn } from './use-items'; + +// export const useScroll = ( +// props: ScrollNavbarProps, +// contentEl: Ref, +// navbarElement: Ref, +// useItemsComposition: UseItemsReturn, +// useNavbarComposition: any +// ): UseScrollReturn => { +// const currentValue = ref(''); +// let navbarElementTop = 0; + +// const scrollState: ScrollState = { +// active: false, +// start: 0, +// end: 0, +// timer: 0 +// }; + +// const { getItems, getTopElementInView } = useItemsComposition; +// const { checkNavbarShow, checkContentContainsNavbar } = useNavbarComposition; + +// const checkContentStyle = (): void => { +// const content = contentEl.value as HTMLElement; +// const styles = getComputedStyle(content); +// const allowPosition = ['fixed', 'absolute', 'relative']; + +// if (!styles.position || !allowPosition.includes(styles.position)) { +// content.style.position = 'relative'; +// } +// }; + +// const handlertabChange = (item: Item): void => { +// const items = getItems(); +// const targetItem = items.find(el => el.name === item.name) || item; + +// scrollState.active = true; +// contentEl.value.scrollTo({ +// top: targetItem.offsetTop - 44, +// behavior: 'smooth' +// }); + +// const fixScrollTop = contentEl.value.scrollTop; +// setTimeout(() => { +// if (fixScrollTop === contentEl.value.scrollTop) { +// scrollState.active = false; +// } +// }, 500); +// }; + +// const checkScrollEnd = (afterScrollEnd: () => void): void => { +// scrollState.end = contentEl.value.scrollTop; +// if (scrollState.start === scrollState.end) { +// scrollState.active = false; +// afterScrollEnd(); +// } +// }; + +// const handlerScroll = (): void => { +// if (checkContentContainsNavbar() && navbarElement.value) { +// if (!navbarElement.value.classList.contains('fm-scroll-navbar-fixed')) { +// navbarElementTop = navbarElement.value.offsetTop; +// } + +// if (contentEl.value.scrollTop > navbarElementTop) { +// navbarElement.value.classList.add('fm-scroll-navbar-fixed'); +// } else { +// navbarElement.value.classList.remove('fm-scroll-navbar-fixed'); +// } +// } + +// if (scrollState.active) { +// clearTimeout(scrollState.timer); +// scrollState.timer = window.setTimeout(() => checkScrollEnd(checkNavbarShow), 100); +// scrollState.start = contentEl.value.scrollTop; +// return; +// } + +// getItems(); +// const topElement = getTopElementInView(); +// currentValue.value = topElement.name; +// checkNavbarShow(); +// }; + +// const initScrollHandler = (): void => { +// contentEl.value.addEventListener('scroll', handlerScroll); +// }; + +// return { +// currentValue, +// handlertabChange, +// handlerScroll: initScrollHandler, +// checkContentStyle +// }; +// }; \ No newline at end of file diff --git a/packages/ui-vue/components/scroll-navbar/src/designer/scroll-navbar.design.component.tsx b/packages/ui-vue/components/scroll-navbar/src/designer/scroll-navbar.design.component.tsx new file mode 100644 index 0000000000..273e16d98a --- /dev/null +++ b/packages/ui-vue/components/scroll-navbar/src/designer/scroll-navbar.design.component.tsx @@ -0,0 +1,45 @@ +import { defineComponent, inject, onMounted, ref } from 'vue'; +import { scrollNavbarProps } from '../scroll-navbar.props'; +import { useDesignerRules } from './use-designer-rules'; +import { DesignerItemContext } from '../../../designer-canvas/src/types'; +import { useDesignerComponent } from '../../../designer-canvas/src/composition/function/use-designer-component'; +export default defineComponent({ + name: 'FScrollNavbarDesign', + props: scrollNavbarProps, + setup(props, context) { + const elementRef = ref(); + const designItemContext = inject('design-item-context') as DesignerItemContext; + //const designerRulesComposition = useDesignerRules(designItemContext.schema, designItemContext.parent?.schema); + const componentInstance = useDesignerComponent(elementRef, designItemContext); + onMounted(() => { + elementRef.value.componentInstance = componentInstance; + }); + + + context.expose(componentInstance.value); + + return () => ( +
+
+ 滚动导航栏组件 - {designItemContext.schema.id} +
+ +
+ {context.slots.default && context.slots.default()} + +
+
+ ); + } +}); \ No newline at end of file diff --git a/packages/ui-vue/components/scroll-navbar/src/designer/use-designer-rules.ts b/packages/ui-vue/components/scroll-navbar/src/designer/use-designer-rules.ts new file mode 100644 index 0000000000..92fc5a8e1b --- /dev/null +++ b/packages/ui-vue/components/scroll-navbar/src/designer/use-designer-rules.ts @@ -0,0 +1,204 @@ +import { DesignerHostService, DraggingResolveContext, + UseDesignerRules, useDragulaCommonRule, + ComponentSchema, DesignerItemContext, + UseTemplateDragAndDropRules, DgControl } from "@farris/ui-vue/components/designer-canvas"; + export function useDesignerRules(designItemContext: DesignerItemContext, designerHostService?: DesignerHostService): UseDesignerRules { + const schema = designItemContext.schema as ComponentSchema; + + const dragAndDropRules = new UseTemplateDragAndDropRules(); + /** + * 判断当前移动的控件是否为筛选方案或者筛选方案外层的分组面板 + */ + function checkIfDraggingQuerySolution(draggingContext: DraggingResolveContext) { + const { componentType, sourceType, sourceElement } = draggingContext; + if (sourceType === 'control' && componentType === DgControl['query-solution'].type) { + return true; + } + if (sourceType === 'move' && componentType === DgControl['section'].type && sourceElement?.className?.includes('f-section-scheme')) { + return true; + } + return false; + } + /** + * 判断树表格绑定的分级码是否分级码分级信息 + * @param treeGridViewModelId 树表格所在的模型id + * @param udtField 树表格绑定的分级码字段编号 + * @returns + */ + function checkPathHierarchyType(treeGridViewModelId: string, udtField: string) { + const schemaService = designerHostService?.schemaService; + const udtFields = schemaService.getTreeGridUdtFields(treeGridViewModelId); + + if (udtFields) { + const bindingUdtFieldInfo = udtFields.find(fieldInfo => fieldInfo.key === udtField); + const bindingUdtField = bindingUdtFieldInfo && bindingUdtFieldInfo.field; + if (bindingUdtField?.type?.name?.includes('PathHierarchyInfo')) { + return true; + } + } + } + /** + * 判断页面是否包含表格,并且要求: + * 1、表格或树表格绑定主表 + * 2、树表格绑定的分级码udt字段必须为分级码分级信息,不能为父节点分级信息 + */ + function checkDataGridExisted() { + const formSchemaUtils = designerHostService?.formSchemaUtils; + const rootComponent = formSchemaUtils.getComponentById('root-component'); + const matchedDataGridComponentRef = formSchemaUtils.selectNode(rootComponent, (item) => { + if (item.type === 'component-ref') { + const childComponent = formSchemaUtils.getComponentById(item.component); + const childViewModel = formSchemaUtils.getViewModelById(childComponent?.viewModel); + if (childComponent?.componentType === 'data-grid' && childViewModel?.bindTo === '/') { + const treeGrid = formSchemaUtils.selectNode(childComponent, item => item.type === 'tree-grid'); + if (treeGrid) { + // return checkPathHierarchyType(childComponent?.viewModel, treeGrid.udtField); + // 暂时不支持树表格启用筛选方案 + return false; + } else { + return true; + } + + } + } + }); + return matchedDataGridComponentRef; + } + /** + * 判断当前页面是否已启用筛选条 + */ + function checkFilterBarExisted() { + const formSchemaUtils = designerHostService?.formSchemaUtils; + const matchedDataGridComponentRef = formSchemaUtils.selectNode(schema, (item) => { + if (item.type === 'component-ref') { + const childComponent = formSchemaUtils.getComponentById(item.component); + if (childComponent?.componentType === 'data-grid') { + const filterBar = formSchemaUtils.selectNode(childComponent, childItem => { + return childItem.contents && childItem.contents.find(content => content.type === DgControl['filter-bar'].type); + }); + if (filterBar) { + return true; + } + } + } + }); + return matchedDataGridComponentRef; + } + /** + * 判断当前页面是否已启用筛选方案 + */ + function checkQuerySolutionExisted() { + const formSchemaUtils = designerHostService?.formSchemaUtils; + const rootComponent = formSchemaUtils.getComponentById('root-component'); + + const solution = formSchemaUtils.selectNode(rootComponent, (item) => item.type === DgControl['query-solution'].type); + return !!solution; + } + /** + * 判断当前容器是否可接收筛选方案 + */ + function checkCanAcceptQuerySolution(draggingContext: DraggingResolveContext) { + const containerClass = schema.appearance?.class; + const { sourceType } = draggingContext; + const isPageLayer = containerClass && containerClass.split(' ').includes('f-page'); + if (!isPageLayer) { + return false; + } + if (sourceType === 'control') { + // 要求:当前页面不存在筛选方案、当前页面不存在筛选条、当前页面存在绑定主表的表格控件 + return !checkQuerySolutionExisted() && !checkFilterBarExisted() && checkDataGridExisted(); + } + if (sourceType === 'move') { + return true; + } + return false; + } + /** + * 判断当前移动的控件是否为抽屉 + */ + function checkIfDraggingDrawer(draggingContext: DraggingResolveContext) { + const { componentType, sourceType } = draggingContext; + if (sourceType === 'control' && componentType === DgControl['drawer'].type) { + return true; + } + return false; + } + /** + * 判断当前容器是否可接收抽屉 + */ + function checkCanAcceptDrawer(draggingContext: DraggingResolveContext) { + const containerClass = schema.appearance?.class; + const { sourceType } = draggingContext; + const isPageLayer = containerClass && containerClass.split(' ').includes('f-page'); + if (!isPageLayer) { + return false; + } + + if (sourceType === 'control') { + // 要求:一个页面只存在一个抽屉 + const drawerExisted = schema.contents && schema.contents.find(content => content.type === DgControl['drawer'].type); + return !drawerExisted; + } + if (sourceType === 'move') { + return true; + } + return false; + } + /** + * 判断是否可以接收拖拽新增的子级控件 + */ + function canAccepts(draggingContext: DraggingResolveContext): boolean { + + if (checkIfDraggingQuerySolution(draggingContext)) { + return checkCanAcceptQuerySolution(draggingContext); + } + if (checkIfDraggingDrawer(draggingContext)) { + return checkCanAcceptDrawer(draggingContext); + } + const basalRule = useDragulaCommonRule().basalDragulaRuleForContainer(draggingContext, designerHostService); + if (!basalRule) { + return false; + } + const { canAccept } = dragAndDropRules.getTemplateRule(designItemContext, designerHostService); + + return canAccept; + } + + function getStyles() { + const component = schema; + if (component.componentType) { + return 'display:inherit;flex-direction:inherit;margin-bottom:10px'; + } + return ''; + } + + function checkCanMoveComponent() { + const { canMove } = dragAndDropRules.getTemplateRule(designItemContext, designerHostService); + return canMove; + } + function checkCanDeleteComponent() { + const { canDelete } = dragAndDropRules.getTemplateRule(designItemContext, designerHostService); + return canDelete; + } + + function hideNestedPaddingInDesginerView() { + const { canMove, canDelete } = dragAndDropRules.getTemplateRule(designItemContext, designerHostService); + return !canMove && !canDelete; + } + /** + * 获取属性配置 + */ + // function getPropsConfig(componentId: string) { + // const componentProp = new ContentContainerProperty(componentId, designerHostService); + // const { schema } = designItemContext; + // return componentProp.getPropertyConfig(schema); + // } + return { + canAccepts, + getStyles, + checkCanMoveComponent, + checkCanDeleteComponent, + hideNestedPaddingInDesginerView, + //getPropsConfig + }; + } \ No newline at end of file diff --git a/packages/ui-vue/components/scroll-navbar/src/schema/schema-mapper.ts b/packages/ui-vue/components/scroll-navbar/src/schema/schema-mapper.ts new file mode 100644 index 0000000000..fcf6389264 --- /dev/null +++ b/packages/ui-vue/components/scroll-navbar/src/schema/schema-mapper.ts @@ -0,0 +1,7 @@ +import { MapperFunction, resolveAppearance } from '../../../dynamic-resolver'; + +export const schemaMapper = new Map([ + ['appearance', resolveAppearance], + ['scrollItems', 'itemRef'], + ['title', 'extendTitle'] +]); \ No newline at end of file diff --git a/packages/ui-vue/components/scroll-navbar/src/schema/schema-resolver.ts b/packages/ui-vue/components/scroll-navbar/src/schema/schema-resolver.ts new file mode 100644 index 0000000000..29511930bb --- /dev/null +++ b/packages/ui-vue/components/scroll-navbar/src/schema/schema-resolver.ts @@ -0,0 +1,75 @@ +import { DynamicResolver } from "../../../dynamic-resolver"; +import { DesignerHostService, DesignerComponentInstance } from "@farris/ui-vue/components/designer-canvas"; +export function schemaResolver(resolver: DynamicResolver, schema: Record, context: Record, designerHostService: DesignerHostService): Record { + let a = 'abcdefghijk'; + a=a+'j'; + if(isString(a)){ + let nodes = extractNodesByLevel(designerHostService.formSchemaUtils.getFormSchema()); + schema.scrollItems=nodes; + return schema; + } + +} +export function itemCollectionEditorSchemaResolver(resolver: DynamicResolver, schema: Record, context: Record): Record { + return schema; +} +export function isString(val: unknown): val is string { + return typeof val === 'string' +} +interface NodeInfo { + id: string; + type: string; + title: string; + originalTitle: string; + selector: string; + visible: Boolean, + +} + +function extractNodesByLevel(moduleData: any): NodeInfo[] { + const result: NodeInfo[] = []; + + // 直接遍历components数组 + if (moduleData.module?.components) { + moduleData.module.components.forEach((component: any) => { + // 搜索组件的contents中的section + if (component.contents) { + searchInContents(component.contents, result); + } + }); + } + + return result; +} + +function searchInContents(contents: any[], result: NodeInfo[]) { + if (!Array.isArray(contents)) return; + + contents.forEach(item => { + if (!item) return; + + // 检查当前项目是否符合条件 + if (item.type === 'section' || item.type === 'html-template') { + const title = item.title || item.mainTitle || ''; + result.push({ + id: item.id || '', + type: item.type, + title: title, + originalTitle:title, + selector :"#"+item.id || '', + visible:true, + }); + } + + // 只递归遍历contents属性,避免深度遍历 + if (item.contents) { + searchInContents(item.contents, result); + } + + // 如果是component-ref,可以进一步处理引用的组件 + if (item.type === 'component-ref' && item.component) { + // 这里可以根据需要查找对应的组件定义 + } + }); +} + diff --git a/packages/ui-vue/components/scroll-navbar/src/schema/scroll-navbar.schema.json b/packages/ui-vue/components/scroll-navbar/src/schema/scroll-navbar.schema.json new file mode 100644 index 0000000000..4ee6b8fb45 --- /dev/null +++ b/packages/ui-vue/components/scroll-navbar/src/schema/scroll-navbar.schema.json @@ -0,0 +1,72 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://farris-design.gitee.io/scroll-navbar.schema.json", + "title": "scroll-navbar", + "description": "滚动导航", + "type": "object", + "properties": { + "id": { + "description": "标识", + "type": "string" + }, + "type": { + "description": "组件类型", + "type": "string", + "default": "scroll-navbar" + }, + "visible": { + "description": "是否可见", + "type": "boolean", + "default": true + }, + "title": { + "description": "标题", + "type": "string", + "default": "导航栏" + }, + "scrollItems": { + "description": "导航内容", + "type": "array", + "default":[] + }, + "contentRef": { + "description": "", + "type": "string", + "default": ".fm-page-main" + }, + "isManualEditMode": { + "description": "", + "type": "boolean", + "default": false + }, + "contents": { + "description": "", + "type": "array", + "default":[] + }, + "alwaysShow": { + "description": "Whether the navbar is always visible", + "type": "boolean", + "default": false + }, + "extendable": { + "description": "Whether the tab bar is extendable", + "type": "boolean", + "default": false + }, + "extendTitle": { + "description": "Title for extendable functionality", + "type": "string" + }, + "appearance": { + "description": "Component appearance styling", + "type": "object", + "properties": { + "class": { "type": "string" }, + "style": { "type": "string" } + }, + "default": {} + } + }, + "required": ["id", "type","visible","scrollItems"] + } \ No newline at end of file diff --git a/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.component.tsx b/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.component.tsx new file mode 100644 index 0000000000..0e4666e17e --- /dev/null +++ b/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.component.tsx @@ -0,0 +1,98 @@ +import { defineComponent, ref, computed, onMounted, watch, nextTick } from 'vue'; +import { ScrollNavbarProps, scrollNavbarProps } from './scroll-navbar.props'; +import { useItems } from './composition/use-items'; +import { useNavbar } from './composition/use-navbar'; +import TabContent from './components/tab-content.component'; +import TabPanel from './components/tab-panel.component'; +import { SetupContext } from 'vue'; +export default defineComponent({ + name: 'FScrollNavbar', + props: scrollNavbarProps, + emits: ['change', 'update:modelValue'], + + setup(props: ScrollNavbarProps, context) { + const navbarElement = ref(); + const currentValue = ref(props.modelValue); + const navbarShow = ref(true); + const extendState = ref(false); + + const contentEl = computed(() => { + try { + const content = document.querySelector(props.contentRef); + return content || document.body; + } catch { + return document.body; + } + }); + + // 初始化组合式 API + const { items, getItems } = useItems(contentEl, props.itemRef); + const navbarLogic = useNavbar( + props, + context as unknown as SetupContext, + navbarElement, + contentEl, + items, + navbarShow, + currentValue + ); + + // 监视模型值变化 + watch(() => props.modelValue, (newValue) => { + currentValue.value = newValue; + }); + + // 处理扩展点击 + const handleExtendClick = () => { + extendState.value = !extendState.value; + }; + + onMounted(() => { + nextTick(() => { + getItems(); + navbarLogic.checkContentStyle(); + navbarLogic.checkNavbarShow(); + }); + }); + + return () => ( +
+ +
+ + + {props.extendable && ( +
+ +
+ )} +
+
+ +
+ + + + +
+ ); + } +}); \ No newline at end of file diff --git a/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.props.ts b/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.props.ts new file mode 100644 index 0000000000..5bc480732c --- /dev/null +++ b/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.props.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2020 - present, Inspur Genersoft Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { PropType, ExtractPropTypes } from 'vue'; +import { schemaMapper } from './schema/schema-mapper'; +import { schemaResolver } from './schema/schema-resolver'; +import scrollNavbarSchema from './schema/scroll-navbar.schema.json'; +import { createPropsResolver } from '../../dynamic-resolver'; +export const scrollNavbarProps = { + /** 是否始终显示导航栏 */ + alwaysShow: { type: Boolean, default: false }, + /** 内容区域选择器 */ + contentRef: { type: String, default: '.fm-page-main' }, + /** 是否可扩展 */ + extendable: { type: Boolean, default: false }, + /** 扩展标题 */ + extendTitle: { type: String, default: '' }, + /** 导航项引用配置 */ + itemRef: { type: [String, Array] as PropType, default: '' } +} as Record; + +export const propsResolverGenerator = createPropsResolver( + scrollNavbarProps, + scrollNavbarSchema, + schemaMapper, + schemaResolver + ); +export type ScrollNavbarProps = ExtractPropTypes; \ No newline at end of file diff --git a/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.scss b/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.scss new file mode 100644 index 0000000000..167f446568 --- /dev/null +++ b/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.scss @@ -0,0 +1,71 @@ +:root { + --fm-scroll-navbar-zindex: var(--fm-zindex-1); +} +.fm-scroll-navbar-fixed { + position: fixed; + z-index: var(--fm-scroll-navbar-zindex); + width: 100%; +} +.fm-scroll-navbar-placeholder { + display: none; + padding-top: 44px +} +.fm-scroll-navbar-fixed + .fm-scroll-navbar-placeholder { + display: block; +} +.app-scroll-navbar { + display: block; + position: relative; + height: 45px; + width: 100%; + overflow: hidden; + user-select: none; + border-bottom: 1px solid #dddddd; + background-color: #ffffff; +} + +.app-tab-bar-list { + display: flex; + justify-content: space-between; + min-width: 100%; +} + +.app-tab-bar-item.is-active { + color: #3a90ff; +} + +.app-tab-bar-item { + flex: auto; + flex-shrink: 0; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 44px; + padding: 0 12px; + margin: 0 auto; + box-sizing: border-box; + line-height: 16px; + font-size: 16px; +} + +.app-tab-bar-item-content { + flex: 0 0 auto; + display: flex; + height: 100%; + justify-content: center; + align-items: center; + position: relative; +} + +.app-tab-bar-item.is-active .app-tab-bar-item-content::after { + content: " "; + display: block; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 4px; + background-color: #3a90ff; + border-radius: 2px; +} -- Gitee From 77ea6be63a89d25ddd9632b0dcbb182da748427f Mon Sep 17 00:00:00 2001 From: Wang Jason Date: Mon, 1 Dec 2025 21:19:23 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E9=80=82?= =?UTF-8?q?=E9=85=8D=EF=BC=8C=E7=BC=BA=E5=9B=BD=E9=99=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designer/combo-list.design.component.tsx | 2 +- .../ui-vue/components/scroll-navbar/index.ts | 1 - .../scroll-navbar/src/composition/types.ts | 14 +- .../src/composition/use-items.ts | 6 +- .../src/composition/use-navbar.ts | 34 +-- .../scroll-navbar.design.component.tsx | 112 +++++-- .../src/designer/use-designer-rules.ts | 1 - .../src/schema/schema-resolver.ts | 70 ++--- .../src/scroll-navbar.component.tsx | 175 ++++++++--- .../scroll-navbar/src/scroll-navbar.props.ts | 3 +- .../scroll-navbar/src/scroll-navbar.scss | 278 ++++++++++++++++++ 11 files changed, 568 insertions(+), 128 deletions(-) diff --git a/packages/ui-vue/components/combo-list/src/designer/combo-list.design.component.tsx b/packages/ui-vue/components/combo-list/src/designer/combo-list.design.component.tsx index e4f60e5e3a..d0411bc2e7 100644 --- a/packages/ui-vue/components/combo-list/src/designer/combo-list.design.component.tsx +++ b/packages/ui-vue/components/combo-list/src/designer/combo-list.design.component.tsx @@ -35,7 +35,7 @@ export default defineComponent({ }); context.expose(componentInstance.value); - + return () => { return ( void; export interface ScrollState { active: boolean; diff --git a/packages/ui-vue/components/scroll-navbar/src/composition/use-items.ts b/packages/ui-vue/components/scroll-navbar/src/composition/use-items.ts index 5bc5bc1682..ba071c04e2 100644 --- a/packages/ui-vue/components/scroll-navbar/src/composition/use-items.ts +++ b/packages/ui-vue/components/scroll-navbar/src/composition/use-items.ts @@ -11,10 +11,12 @@ export const useItems = ( return configItems .map(item => { const { id = '', title = '', selector = '', visible = true } = item; - if (!visible) return null; + if (!visible) {return null;} const itemEl = content.value.querySelector(selector); - if (!itemEl) return null; + if (!itemEl){ + return null; + } const { offsetTop, clientHeight } = itemEl as HTMLElement; return { diff --git a/packages/ui-vue/components/scroll-navbar/src/composition/use-navbar.ts b/packages/ui-vue/components/scroll-navbar/src/composition/use-navbar.ts index 00b1c972af..a35612bd2a 100644 --- a/packages/ui-vue/components/scroll-navbar/src/composition/use-navbar.ts +++ b/packages/ui-vue/components/scroll-navbar/src/composition/use-navbar.ts @@ -53,7 +53,7 @@ export const useNavbar = ( * 检查内容区域是否包含导航栏 */ const checkContentContainsNavbar = (): boolean => { - if (!contentEl.value || !navbarElement.value) return false; + if (!contentEl.value || !navbarElement.value) {return false}; return contentEl.value.contains(navbarElement.value); }; @@ -87,19 +87,6 @@ export const useNavbar = ( content.style.position = 'relative'; } }; - - const handlePanelItemClick = (event: { item: NavbarItem; index: number }): void => { - - // 处理标签切换 - handlertabChange(event.item); - - // 可以在这里添加其他逻辑 - console.log(`点击了导航项: ${event.item.title}, 索引: ${event.index}`); - }; - - /** - * 标签切换处理函数 - */ const handlertabChange = (item: NavbarItem): void => { scrollState.active = true; @@ -125,6 +112,20 @@ export const useNavbar = ( } }, 500); }; + const handlePanelItemClick = (event: { item: NavbarItem; index: number }): void => { + + // 处理标签切换 + + handlertabChange(event.item); + + // 可以在这里添加其他逻辑 + console.log(`点击了导航项: ${event.item.title}, 索引: ${event.index}`); + }; + + /** + * 标签切换处理函数 + */ + /** * 滚动结束检测 @@ -141,7 +142,7 @@ export const useNavbar = ( * 滚动事件处理函数 */ const handlerScroll = (): void => { - if (!contentEl.value || !navbarElement.value) return; + if (!contentEl.value || !navbarElement.value) {return}; // 导航栏固定定位逻辑 if (checkContentContainsNavbar()) { @@ -168,7 +169,7 @@ export const useNavbar = ( } // 查找当前可视区域顶部的元素 - const scrollTop = contentEl.value.scrollTop; + const { scrollTop } = contentEl.value; let topElement: NavbarItem | undefined; for (const item of items.value) { @@ -184,7 +185,6 @@ export const useNavbar = ( } else if (items.value.length > 0) { currentValue.value = items.value[items.value.length - 1].name; } - checkNavbarShow(); }; diff --git a/packages/ui-vue/components/scroll-navbar/src/designer/scroll-navbar.design.component.tsx b/packages/ui-vue/components/scroll-navbar/src/designer/scroll-navbar.design.component.tsx index 273e16d98a..e0c3c7bbba 100644 --- a/packages/ui-vue/components/scroll-navbar/src/designer/scroll-navbar.design.component.tsx +++ b/packages/ui-vue/components/scroll-navbar/src/designer/scroll-navbar.design.component.tsx @@ -1,41 +1,109 @@ -import { defineComponent, inject, onMounted, ref } from 'vue'; -import { scrollNavbarProps } from '../scroll-navbar.props'; +import { defineComponent, inject, onMounted, ref,computed,Ref } from 'vue'; +import { ScrollNavbarProps,scrollNavbarProps } from '../scroll-navbar.props'; import { useDesignerRules } from './use-designer-rules'; +import { useItems } from '../composition/use-items'; +import{ NodeInfo}from "../composition/types" import { DesignerItemContext } from '../../../designer-canvas/src/types'; import { useDesignerComponent } from '../../../designer-canvas/src/composition/function/use-designer-component'; +import '../scroll-navbar.scss'; export default defineComponent({ name: 'FScrollNavbarDesign', props: scrollNavbarProps, - setup(props, context) { + emits: ['clear', 'update:modelValue', 'change'], + setup(props :ScrollNavbarProps, context) { const elementRef = ref(); - const designItemContext = inject('design-item-context') as DesignerItemContext; - //const designerRulesComposition = useDesignerRules(designItemContext.schema, designItemContext.parent?.schema); - const componentInstance = useDesignerComponent(elementRef, designItemContext); + const designerHostService = inject('designer-host-service'); + const designItemContext = inject('design-item-context') as DesignerItemContext; + const componentInstance = useDesignerComponent(elementRef, designItemContext); + const enumData: Ref = ref(props.itemRef); onMounted(() => { elementRef.value.componentInstance = componentInstance; - }); + }); context.expose(componentInstance.value); - + const realEnumData = computed(() => { + if (!enumData.value || enumData.value.length === 0) { + const result = [] as any; + return [ + { name: 'section1', title: '基本信息', active: true, disabled: false }, + { name: 'section2', title: '详细信息', active: false, disabled: false }, + { name: 'section3', title: '附件信息', active: false, disabled: false }, + { name: 'section4', title: '审批流程', active: false, disabled: true } + ]; + }else{ + const result = [] as any; + for(let i = 0 ; i ( -
- 滚动导航栏组件 - {designItemContext.schema.id} -
- -
+
+ +
+
{context.slots.default && context.slots.default()}
diff --git a/packages/ui-vue/components/scroll-navbar/src/designer/use-designer-rules.ts b/packages/ui-vue/components/scroll-navbar/src/designer/use-designer-rules.ts index 92fc5a8e1b..3e27694f8c 100644 --- a/packages/ui-vue/components/scroll-navbar/src/designer/use-designer-rules.ts +++ b/packages/ui-vue/components/scroll-navbar/src/designer/use-designer-rules.ts @@ -199,6 +199,5 @@ import { DesignerHostService, DraggingResolveContext, checkCanMoveComponent, checkCanDeleteComponent, hideNestedPaddingInDesginerView, - //getPropsConfig }; } \ No newline at end of file diff --git a/packages/ui-vue/components/scroll-navbar/src/schema/schema-resolver.ts b/packages/ui-vue/components/scroll-navbar/src/schema/schema-resolver.ts index 29511930bb..ae89a8408e 100644 --- a/packages/ui-vue/components/scroll-navbar/src/schema/schema-resolver.ts +++ b/packages/ui-vue/components/scroll-navbar/src/schema/schema-resolver.ts @@ -1,56 +1,18 @@ import { DynamicResolver } from "../../../dynamic-resolver"; +import{ NodeInfo}from "../composition/types" import { DesignerHostService, DesignerComponentInstance } from "@farris/ui-vue/components/designer-canvas"; -export function schemaResolver(resolver: DynamicResolver, schema: Record, context: Record, designerHostService: DesignerHostService): Record { - let a = 'abcdefghijk'; - a=a+'j'; - if(isString(a)){ - let nodes = extractNodesByLevel(designerHostService.formSchemaUtils.getFormSchema()); - schema.scrollItems=nodes; - return schema; - } - -} -export function itemCollectionEditorSchemaResolver(resolver: DynamicResolver, schema: Record, context: Record): Record { - return schema; -} export function isString(val: unknown): val is string { return typeof val === 'string' } -interface NodeInfo { - id: string; - type: string; - title: string; - originalTitle: string; - selector: string; - visible: Boolean, - -} - -function extractNodesByLevel(moduleData: any): NodeInfo[] { - const result: NodeInfo[] = []; - - // 直接遍历components数组 - if (moduleData.module?.components) { - moduleData.module.components.forEach((component: any) => { - // 搜索组件的contents中的section - if (component.contents) { - searchInContents(component.contents, result); - } - }); - } - - return result; -} - function searchInContents(contents: any[], result: NodeInfo[]) { - if (!Array.isArray(contents)) return; + if (!Array.isArray(contents)) {return}; contents.forEach(item => { - if (!item) return; + if (!item) {return}; // 检查当前项目是否符合条件 if (item.type === 'section' || item.type === 'html-template') { - const title = item.title || item.mainTitle || ''; + const title = item.title || item.mainTitle ||item.id|| ''; result.push({ id: item.id || '', type: item.type, @@ -72,4 +34,26 @@ function searchInContents(contents: any[], result: NodeInfo[]) { } }); } - +function extractNodesByLevel(moduleData: any): NodeInfo[] { + const result: NodeInfo[] = []; + + // 直接遍历components数组 + if (moduleData.module?.components) { + moduleData.module.components.forEach((component: any) => { + // 搜索组件的contents中的section + if (component.contents) { + searchInContents(component.contents, result); + } + }); + } + + return result; +} +export function schemaResolver(resolver: DynamicResolver, schema: Record, context: Record, designerHostService: DesignerHostService): Record { + const nodes = extractNodesByLevel(designerHostService.formSchemaUtils.getFormSchema()); + schema.scrollItems=nodes; + return schema; +} +export function itemCollectionEditorSchemaResolver(resolver: DynamicResolver, schema: Record, context: Record): Record { + return schema; +} diff --git a/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.component.tsx b/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.component.tsx index 0e4666e17e..a50753c99b 100644 --- a/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.component.tsx +++ b/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.component.tsx @@ -2,9 +2,9 @@ import { defineComponent, ref, computed, onMounted, watch, nextTick } from 'vue' import { ScrollNavbarProps, scrollNavbarProps } from './scroll-navbar.props'; import { useItems } from './composition/use-items'; import { useNavbar } from './composition/use-navbar'; -import TabContent from './components/tab-content.component'; -import TabPanel from './components/tab-panel.component'; import { SetupContext } from 'vue'; +import '../scroll-navbar.scss'; + export default defineComponent({ name: 'FScrollNavbar', props: scrollNavbarProps, @@ -47,6 +47,25 @@ export default defineComponent({ extendState.value = !extendState.value; }; + // 处理标签点击 + const handleTabClick = (item: any, index: number) => { + // 更新当前激活状态 + currentValue.value = item.name; + + // 调用导航逻辑处理滚动 + navbarLogic.handlertabChange(item); + + // 发射事件 + context.emit('change', item, index); + context.emit('update:modelValue', item.name); + }; + + // 处理面板项点击 + const handlePanelItemClick = (event: { item: any; index: number }) => { + handleTabClick(event.item, event.index); + extendState.value = false; // 关闭扩展面板 + }; + onMounted(() => { nextTick(() => { getItems(); @@ -55,43 +74,127 @@ export default defineComponent({ }); }); + // 计算标签数据 - 与设计器相同的逻辑 + const realEnumData = computed(() => { + if (!items.value || items.value.length === 0) { + return [ + { name: 'section1', title: '基本信息', active: true, disabled: false }, + { name: 'section2', title: '详细信息', active: false, disabled: false }, + { name: 'section3', title: '附件信息', active: false, disabled: false }, + { name: 'section4', title: '审批流程', active: false, disabled: true } + ]; + } else { + return items.value.map((item, index) => ({ + ...item, + active: item.name === currentValue.value, + disabled: false + })); + } + }); + + // 计算下划线位置 + const inkStyle = computed(() => { + const activeIndex = realEnumData.value.findIndex(item => item.active); + const itemWidth = 100; // 与设计器保持一致 + const inkWidth = 60; + const inkPos = activeIndex * itemWidth + (itemWidth - inkWidth) / 2; + + return { + width: `${inkWidth}px`, + transform: `translateX(${inkPos}px)` + }; + }); + return () => ( -
- -
- - - {props.extendable && ( -
- +
+
+
+ {/* 使用与设计器完全相同的结构 */} + +
- - -
- - - - + + {/* 扩展面板 */} + +
+
+ {props.extendTitle || '更多'} +
+
+ {realEnumData.value.map((item, index) => ( +
!item.disabled && handlePanelItemClick({ item, index })} + > + {item.title} +
+ ))} +
+
+
+
); } diff --git a/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.props.ts b/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.props.ts index 5bc480732c..07a7c0ef85 100644 --- a/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.props.ts +++ b/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.props.ts @@ -18,6 +18,7 @@ import { schemaMapper } from './schema/schema-mapper'; import { schemaResolver } from './schema/schema-resolver'; import scrollNavbarSchema from './schema/scroll-navbar.schema.json'; import { createPropsResolver } from '../../dynamic-resolver'; +import{ NodeInfo}from "./composition/types" export const scrollNavbarProps = { /** 是否始终显示导航栏 */ alwaysShow: { type: Boolean, default: false }, @@ -28,7 +29,7 @@ export const scrollNavbarProps = { /** 扩展标题 */ extendTitle: { type: String, default: '' }, /** 导航项引用配置 */ - itemRef: { type: [String, Array] as PropType, default: '' } + itemRef: {type: Array, default: [] } } as Record; export const propsResolverGenerator = createPropsResolver( diff --git a/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.scss b/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.scss index 167f446568..7904d4cc40 100644 --- a/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.scss +++ b/packages/ui-vue/components/scroll-navbar/src/scroll-navbar.scss @@ -69,3 +69,281 @@ background-color: #3a90ff; border-radius: 2px; } +// 设计器专用的样式变量 +.fm-scroll-navbar-design { + --fm-tab-bar-height: 44px; + --fm-tab-bar-background: #ffffff; + --fm-tab-bar-color: #666666; + --fm-tab-bar-active-color: #1890ff; + --fm-tab-bar-font-size: 14px; + --fm-primary-color: #1890ff; + --fm-disabled-color: #c0c4cc; + --fm-text-color: #333333; + --fm-text-color-2: #666666; + --fm-text-color-3: #999999; + --fm-gradient-blue: #1890ff; + --fm-background-2: #f5f5f5; + --fm-danger-color: #ff4d4f; + --fm-box-shadow-color: rgba(0, 0, 0, 0.1); + --fm-duration-base: 0.3s; +} + +.fm-scroll-navbar-design { + width: 100%; + box-sizing: border-box; +} + +.fm-tab-bar--designer { + position: relative; + background: var(--fm-tab-bar-background); + color: var(--fm-tab-bar-color); + font-size: var(--fm-tab-bar-font-size); + border-bottom: 1px solid #ebedf0; // 替代 hairline mixin + + // 设计器内的悬停效果 + &:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + outline: 2px solid #1890ff; + outline-offset: -1px; + } + + &.fm-tab-bar--selected { + outline: 2px solid #52c41a !important; + } +} + +.fm-tab-bar.fm-tab-bar--designer { + height: var(--fm-tab-bar-height); + display: flex; + align-items: center; + padding: 0 16px; + z-index: 1000; +} + +.fm-tab-bar-inner { + position: relative; + width: 100%; + display: flex; + align-items: center; +} + +.fm-tab-bar-extendable { + width: calc(100% - 30px); +} + +// 滚动遮罩 +.fm-tab-bar-start, +.fm-tab-bar-end { + position: absolute; + top: 0; + bottom: 0; + width: 14px; + overflow: hidden; + z-index: 1; +} + +.fm-tab-bar-start { + left: 0; + &::after { + content: ''; + display: block; + position: absolute; + left: -14px; + top: 50%; + width: 14px; + margin-top: -22px; + height: var(--fm-tab-bar-height); + border-radius: 50%; + box-shadow: -1px 0 12px 0 var(--fm-box-shadow-color); + } +} + +.fm-tab-bar-end { + left: auto; + right: 0; + transform: rotate(180deg); + &::after { + content: ''; + display: block; + position: absolute; + left: -14px; + top: 50%; + width: 14px; + margin-top: -22px; + height: var(--fm-tab-bar-height); + border-radius: 50%; + box-shadow: -1px 0 12px 0 var(--fm-box-shadow-color); + } +} + +// 扩展按钮 +.fm-tab-bar-extend { + position: absolute; + top: 0; + right: -30px; + width: 42px; + height: calc(var(--fm-tab-bar-height) - 8px); + margin: 4px 0; + display: flex; + align-items: center; + justify-content: center; + background-image: linear-gradient( + 270deg, + #ffffff 74%, + rgba(255, 255, 255, 0) 100% + ); + border-radius: 2px; + z-index: 2; + cursor: pointer; + + .fm-icon-simulate { + font-size: 16px; + color: var(--fm-text-color-2); + } +} + +// 滚动容器模拟 +.fm-scroll-view-simulate { + position: relative; + width: 100%; + overflow: hidden; +} + +.fm-tab-bar-list { + display: flex; + justify-content: space-between; + min-width: 100%; + gap: 24px; +} + +.fm-tab-bar-item { + flex: auto; + flex-shrink: 0; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + min-height: var(--fm-tab-bar-height); + padding: 0 12px; + margin: 0 auto; + box-sizing: border-box; + cursor: pointer; + transition: all var(--fm-duration-base); + white-space: nowrap; + + &:hover { + color: var(--fm-tab-bar-active-color); + } + + &.is-active { + color: var(--fm-tab-bar-active-color); + font-weight: 500; + } + + &.is-disabled { + color: var(--fm-disabled-color); + cursor: not-allowed; + + &:hover { + color: var(--fm-disabled-color); + } + } +} + +.fm-tab-bar-item-content { + display: flex; + align-items: center; + justify-content: center; + + .text { + padding: 4px 8px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +// 下划线指示器 +.fm-tab-bar-ink { + position: absolute; + bottom: 0; + left: 0; + display: block; + height: 4px; + background-color: var(--fm-tab-bar-active-color); + transition: all var(--fm-duration-base); + border-radius: 2px; + + &.is-disabled { + background-color: var(--fm-disabled-color); + } +} + +// Nav 类型样式 +.fm-tab-bar-nav { + border-top: 1px solid #ebedf0; + border-bottom: none; + + .fm-tab-bar-item { + flex-direction: column; + min-height: 49px; + + &.is-active { + color: var(--fm-tab-bar-active-color); + + .text { + color: var(--fm-primary-color); + } + } + + .text { + line-height: 13px; + font-size: 10px; + color: var(--fm-text-color-2); + } + } +} + +// Tab 类型样式 +.fm-tab-bar-tab { + .fm-tab-bar-item-content { + display: flex; + align-items: center; + justify-content: center; + + .fm-tab-bar-icon { + margin-right: 6px; + } + } +} + +// 占位符 +.fm-scroll-navbar-placeholder { + height: var(--fm-tab-bar-height); + visibility: hidden; +} + +// 设计器交互状态 +.fm-tab-bar--designer.fm-tab-bar--dragging { + opacity: 0.7; + transform: scale(0.98); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); +} + +// 固定定位样式(设计器内预览用) +.fm-tab-bar--fixed { + position: fixed !important; + top: 0; + left: 0; + right: 0; + animation: slideDown 0.3s ease; +} + +@keyframes slideDown { + from { + transform: translateY(-100%); + } + to { + transform: translateY(0); + } +} \ No newline at end of file -- Gitee