diff --git a/packages/docs/menu.md b/packages/docs/menu.md new file mode 100644 index 0000000000000000000000000000000000000000..3f78af70401f587bf4da15e9791fa2bc8439fd3c --- /dev/null +++ b/packages/docs/menu.md @@ -0,0 +1,50 @@ +# Menu 菜单 + +## OMenu + +### props + +| name | type | 默认值 | 说明 | +| :------------------ | :-------------- | :----- | -------------------------------------- | +| modelValue(v-model) | string | - | 可选,双向绑定值 | +| defaultValue | boolean | false | 可选,非受控状态时,默认选中值 | +| expanded(v-model) | Array | - | 可选,双向展开的子菜单值 | +| defaultExpanded | Array | false | 可选,非受控状态时,默认展开的子菜单值 | +| accordion | boolean | false | 可选,是否开启手风琴模式 | +| levelIndent | number | 24 | 可选,层级缩进值 | + +### event + +| name | 参数 | 说明 | +| :-------------- | :------------------- | :--------------------- | +| change | val: string | 选中值切换后触发 | +| expanded-change | val: Array | 展开子菜单值切换后触发 | + +## OSubMenu + +### props + +| name | type | 默认值 | 说明 | +| :---- | :----- | :----- | -------------- | +| value | string | - | 必选,子菜单值 | + +## slot + +| name | 参数 | 说明 | +| :--- | :--- | :--------- | +| icon | | 自定义图标 | + +## OMenuItem + +### props + +| name | type | 默认值 | 说明 | +| :------- | :------ | :----- | ---------------- | +| value | string | - | 必选,菜单选项值 | +| disabled | boolean | false | 可选,是否禁用 | + +## event + +| name | 参数 | 说明 | +| :---- | :--- | :------- | +| click | | 点击触发 | diff --git a/packages/opendesign/src/components/_shared/tree.ts b/packages/opendesign/src/components/_shared/tree.ts new file mode 100644 index 0000000000000000000000000000000000000000..0b160f7d9fe293d25cde45eb5356ac9a37766426 --- /dev/null +++ b/packages/opendesign/src/components/_shared/tree.ts @@ -0,0 +1,71 @@ +export interface TreeNodeT { + value: string | number; + parent: TreeNodeT | null; + children: Array; +} + +export interface TreeT { + root: TreeNodeT; + getNode(node: TreeNodeT, val: string | number): TreeNodeT | undefined; + getPath(node: TreeNodeT, val: string | number, path: Array): Array | undefined; +} + +export class VTree implements TreeT { + root: TreeNodeT; + constructor(value: string | number, parent: TreeNodeT | null, children: Array = []) { + this.root = { + value, + parent, + children, + }; + } + + getNode(node: TreeNodeT, val: string | number): TreeNodeT | undefined { + if (node.value === val) { + return node; + } + + const children: Array = node.children; + for (let i = 0, len = children.length; i < len; i++) { + const rlt = this.getNode(children[i], val); + if (rlt) { + return rlt; + } + } + } + + getPath(node: TreeNodeT, val: string | number, path: Array): Array | undefined { + const children: Array = node.children; + for (let i = 0, len = children.length; i < len; i++) { + const child = children[i]; + if (child.value === val) { + return [...path, child]; + } + + const rlt = this.getPath(child, val, [...path, child]); + if (rlt) { + return rlt; + } + } + } + + hasSameNode(nodes: Array, val: string | number) { + return nodes.some((item) => item.value === val); + } + + addNode(node: TreeNodeT) { + const parent = node.parent; + if (!parent) { + if (!this.hasSameNode(this.root.children, node.value)) { + node.parent = this.root; + this.root.children.push(node); + } + } else { + const parentNode = this.getNode(this.root, parent.value); + if (parentNode && !this.hasSameNode(parentNode.children, node.value)) { + node.parent = parentNode; + parentNode.children.push(node); + } + } + } +} diff --git a/packages/opendesign/src/components/menu/OMenu.vue b/packages/opendesign/src/components/menu/OMenu.vue new file mode 100644 index 0000000000000000000000000000000000000000..a058cb7bb7d59f9e77f813c32995142428079695 --- /dev/null +++ b/packages/opendesign/src/components/menu/OMenu.vue @@ -0,0 +1,83 @@ + + + diff --git a/packages/opendesign/src/components/menu/OMenuItem.vue b/packages/opendesign/src/components/menu/OMenuItem.vue new file mode 100644 index 0000000000000000000000000000000000000000..2fc28b7084d6dad82c2f56cf9067c7a29b23e132 --- /dev/null +++ b/packages/opendesign/src/components/menu/OMenuItem.vue @@ -0,0 +1,57 @@ + + + diff --git a/packages/opendesign/src/components/menu/OSubMenu.vue b/packages/opendesign/src/components/menu/OSubMenu.vue new file mode 100644 index 0000000000000000000000000000000000000000..c1740c64b811bb7f2d825fc1c2a26be52ba56c91 --- /dev/null +++ b/packages/opendesign/src/components/menu/OSubMenu.vue @@ -0,0 +1,116 @@ + + + diff --git a/packages/opendesign/src/components/menu/__demo__/IndexMenu.vue b/packages/opendesign/src/components/menu/__demo__/IndexMenu.vue new file mode 100644 index 0000000000000000000000000000000000000000..e5334052a14c12e99684894b93231fb63a98cdde --- /dev/null +++ b/packages/opendesign/src/components/menu/__demo__/IndexMenu.vue @@ -0,0 +1,12 @@ + + + diff --git a/packages/opendesign/src/components/menu/__demo__/MenuAccordion.vue b/packages/opendesign/src/components/menu/__demo__/MenuAccordion.vue new file mode 100644 index 0000000000000000000000000000000000000000..72787147519d249cca24e507be329cb6d48a489c --- /dev/null +++ b/packages/opendesign/src/components/menu/__demo__/MenuAccordion.vue @@ -0,0 +1,48 @@ + + + diff --git a/packages/opendesign/src/components/menu/__demo__/MenuBasic.vue b/packages/opendesign/src/components/menu/__demo__/MenuBasic.vue new file mode 100644 index 0000000000000000000000000000000000000000..1d8f778db0c653efc31d605ee834089b18809f1f --- /dev/null +++ b/packages/opendesign/src/components/menu/__demo__/MenuBasic.vue @@ -0,0 +1,73 @@ + + + diff --git a/packages/opendesign/src/components/menu/index.ts b/packages/opendesign/src/components/menu/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..7202a587b56ea81e6ed4049dbfe3e78f2040c72b --- /dev/null +++ b/packages/opendesign/src/components/menu/index.ts @@ -0,0 +1,24 @@ +import type { App } from 'vue'; + +import _OMenu from './OMenu.vue'; +import _OSubMenu from './OSubMenu.vue'; +import _OMenuItem from './OMenuItem.vue'; + +const OMenu = Object.assign(_OMenu, { + install(app: App) { + app.component(_OMenu.name, _OMenu); + }, +}); + +const OSubMenu = Object.assign(_OSubMenu, { + install(app: App) { + app.component(_OSubMenu.name, _OSubMenu); + }, +}); + +const OMenuItem = Object.assign(_OMenuItem, { + install(app: App) { + app.component(_OMenuItem.name, _OMenuItem); + }, +}); +export { OMenu, OSubMenu, OMenuItem }; diff --git a/packages/opendesign/src/components/menu/menu.ts b/packages/opendesign/src/components/menu/menu.ts new file mode 100644 index 0000000000000000000000000000000000000000..cfc4aac98d2696077119bf73af20db80dfe3605c --- /dev/null +++ b/packages/opendesign/src/components/menu/menu.ts @@ -0,0 +1,54 @@ +import { VTree } from '../_shared/tree'; +import type { TreeNodeT } from '../_shared/tree'; +import { isString, isUndefined } from '../_shared/is'; + +interface MenuNodeT extends TreeNodeT {} + +export default class MenuTree extends VTree { + constructor(value: string | number, parent: MenuNodeT | null, children: Array = []) { + super(value, parent, children); + } + + addChild(opitons: { value: string; parentVal: string | undefined }) { + const { value, parentVal } = opitons; + const node: MenuNodeT = { + value, + parent: null, + children: [], + }; + + if (isUndefined(parentVal)) { + if (!this.hasSameNode(this.root.children, node.value)) { + node.parent = this.root; + this.root.children.push(node); + } + } else { + const parentNode = this.getNode(this.root, parentVal); + if (parentNode && !this.hasSameNode(parentNode.children, node.value)) { + node.parent = parentNode; + parentNode.children.push(node); + } + } + } + + selectNode(val: string) { + const path = this.getPath(this.root, val, []) || []; + return path.map((node) => { + if (isString(node.value)) { + return node.value; + } + }); + } + + getSiblings(val: string) { + const node = this.getNode(this.root, val); + if (!node || !node.parent) { + return []; + } + return node.parent.children.map((item) => { + if (item.value !== val) { + return item.value; + } + }); + } +} diff --git a/packages/opendesign/src/components/menu/provide.ts b/packages/opendesign/src/components/menu/provide.ts new file mode 100644 index 0000000000000000000000000000000000000000..99811e06642f178642f246b7b976f88b0762d50e --- /dev/null +++ b/packages/opendesign/src/components/menu/provide.ts @@ -0,0 +1,19 @@ +import { InjectionKey, Ref } from 'vue'; +import MenuTree from './menu'; + +export const menuInjectKey: InjectionKey<{ + accordion: Ref; + levelIndent: Ref; + realValue: Ref; + activeNodes: Ref>; + realExpanded: Ref>; + menuTree: MenuTree; + depth: number; + updateModelValue: (val: string) => void; + updateExpanded: (val: Array) => void; +}> = Symbol('provide-menu'); + +export const subMenuInjectKey: InjectionKey<{ + value: string; + depth: number; +}> = Symbol('provide-sub-menu'); diff --git a/packages/opendesign/src/components/menu/style/index.scss b/packages/opendesign/src/components/menu/style/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..f4fe0d6200ddd2a3d763857f44c0a26ffce8b241 --- /dev/null +++ b/packages/opendesign/src/components/menu/style/index.scss @@ -0,0 +1,103 @@ +@use './var.scss'; + +.o-menu { + list-style: none; + width: var(--o-menu-width); + color: var(--o-menu-color); + background-color: var(--o-menu-bg); + margin: 0; + padding: 0; +} + +// submenu +.o-sub-menu { + font-size: var(--o-sub-menu-text-size); + line-height: var(--o-sub-menu-text-height); + font-weight: 400; +} + +.o-sub-menu-active { + & > .o-sub-menu-title { + font-weight: 700; + } +} + +.o-sub-menu-expanded { + & > .o-sub-menu-title { + .o-sub-menu-title-arrow { + transform: rotate(90deg); + } + } +} + +.o-sub-menu-title { + position: relative; + display: flex; + align-items: center; + padding: 16px 48px 16px 24px; + border-radius: 8px; + margin-bottom: 4px; + cursor: pointer; + transition: background-color var(--o-duration-m2) var(--o-easing-standard); + + &:hover { + background-color: var(--o-sub-menu-bg-hover); + } +} + +.o-sub-menu-title-icon { + margin-right: 4px; + font-size: var(--o-sub-menu-icon-height); +} + +.o-sub-menu-title-content { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.o-sub-menu-title-arrow { + position: absolute; + right: 16px; + transform: rotate(0); + font-size: var(--o-sub-menu-icon-height); + transition: transform var(--o-duration-m2) var(--o-easing-standard); +} + +.o-sub-menu-children { + padding: 0; + margin: 0; + list-style: none; + overflow: hidden; + transition: height var(--o-duration-m2) var(--o-easing-standard); +} + +//menu-item +.o-menu-item { + font-size: var(--o-menu-item-text-size); + line-height: var(--o-menu-item-text-height); + border-radius: 8px; + padding: 12px 16px 12px 36px; + cursor: pointer; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + transition: background-color var(--o-duration-s) var(--o-easing-standard); + + &:not(.o-menu-item-disabled):hover { + background-color: var(--o-menu-item-bg-hover); + } + + & + .o-menu-item { + margin-top: 4px; + } +} + +.o-menu-item-active { + background-color: var(--o-menu-item-bg-active); +} + +.o-menu-item-disabled { + cursor: not-allowed; + color: var(--o-menu-item-color-disabled); +} diff --git a/packages/opendesign/src/components/menu/style/index.ts b/packages/opendesign/src/components/menu/style/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..591b2fbd72dcd5e987362d645b561bd2fac42716 --- /dev/null +++ b/packages/opendesign/src/components/menu/style/index.ts @@ -0,0 +1,2 @@ +import '../../style'; +import './index.scss'; diff --git a/packages/opendesign/src/components/menu/style/var.scss b/packages/opendesign/src/components/menu/style/var.scss new file mode 100644 index 0000000000000000000000000000000000000000..b97c4874532fb9d87787ba2ae71383224caafb6b --- /dev/null +++ b/packages/opendesign/src/components/menu/style/var.scss @@ -0,0 +1,20 @@ +.o-menu { + --o-menu-width: 260px; + --o-menu-bg: var(--o-color-fill2); + --o-menu-color: var(--o-color-info1); +} + +.o-sub-menu { + --o-sub-menu-bg-hover: var(--o-color-control2-light); + --o-sub-menu-text-size: var(--o-font_size-h4); + --o-sub-menu-text-height: var(--o-line_height-h4); + --o-sub-menu-icon-height: var(--o-font_size-h1); +} + +.o-menu-item { + --o-menu-item-text-size: var(--o-font_size-text); + --o-menu-item-text-height: var(--o-line_height-text); + --o-menu-item-bg-active: var(--o-color-control2); + --o-menu-item-bg-hover: var(--o-color-control2-light); + --o-menu-item-color-disabled: var(--o-color-info4); +} diff --git a/packages/opendesign/src/components/menu/types.ts b/packages/opendesign/src/components/menu/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..21a0004d6de3df4f8f779b2283e2c3625ae7b3d9 --- /dev/null +++ b/packages/opendesign/src/components/menu/types.ts @@ -0,0 +1,63 @@ +import type { ExtractPropTypes, PropType } from 'vue'; + +// MenuProps +export const menuProps = { + /** + * 是否开启手风琴模式 + */ + accordion: { + type: Boolean, + default: false, + }, + // 层级缩进 + levelIndent: { + type: Number, + default: 24, + }, + /** + * 默认选中值 + */ + modelValue: { + type: String, + }, + // 非受控模式时,默认选中值 + defaultValue: { + type: String, + default: '', + }, + /** + * 默认展开的子菜单值 + */ + expanded: { + type: Array as PropType>, + }, + // 非受控模式时,默认展开的子菜单值 + defaultExpanded: { + type: Array as PropType>, + default: () => [], + }, +}; + +export type MenuPropsT = ExtractPropTypes; + +// SubMenuProps +export const subMenuProps = { + value: { + type: String, + required: true, + }, +}; +export type SubMenuPropsT = ExtractPropTypes; + +// MenuItemProps +export const menuItemProps = { + value: { + type: String, + required: true, + }, + disabled: { + type: Boolean, + default: false, + }, +}; +export type MenuItemPropsT = ExtractPropTypes; diff --git a/packages/portal/src/router.ts b/packages/portal/src/router.ts index e64b38e4685e1b82fdce709e43c15b45f930bc7b..99d7b1e8df468eda819fbb9f322bcfbcfb6ee121 100644 --- a/packages/portal/src/router.ts +++ b/packages/portal/src/router.ts @@ -147,6 +147,12 @@ export const routes = [ label: '面包屑', component: () => import('@components/breadcrumb/__demo__/IndexBreadcrumb.vue'), }, + { + path: '/menu', + name: 'OMenu ', + label: '菜单', + component: () => import('@components/menu/__demo__/IndexMenu.vue'), + }, ]; export const router = createRouter({