forked from opentiny/tiny-vue
feat(action-menu): [action-menu] add XDesign theme (#1514)
* feat(action-menu): [action-menu] add XDesign theme * feat(action-menu): [action-menu] add XDesign theme
This commit is contained in:
parent
33f32aec46
commit
fedf1cb1fe
|
@ -17,6 +17,17 @@ export default {
|
|||
mode: ['pc'],
|
||||
pcDemo: 'max-show-num'
|
||||
},
|
||||
{
|
||||
name: 'mode',
|
||||
type: '"default" | "card"',
|
||||
defaultValue: '"default"',
|
||||
desc: {
|
||||
'zh-CN': '菜单按钮模式',
|
||||
'en-US': 'Card mode'
|
||||
},
|
||||
mode: ['pc'],
|
||||
pcDemo: 'card-mode'
|
||||
},
|
||||
{
|
||||
name: 'more-text',
|
||||
type: 'string',
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<tiny-action-menu :options="options" mode="card"> </tiny-action-menu>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ActionMenu as TinyActionMenu } from '@opentiny/vue'
|
||||
import { iconWebPlus, iconSuccessful, iconCloseSquare } from '@opentiny/vue-icon'
|
||||
|
||||
const options = ref([
|
||||
{
|
||||
label: '远程登陆',
|
||||
icon: iconWebPlus()
|
||||
},
|
||||
{
|
||||
label: '开机',
|
||||
icon: iconSuccessful()
|
||||
},
|
||||
{
|
||||
label: '关机',
|
||||
icon: iconCloseSquare()
|
||||
},
|
||||
{
|
||||
label: '重启'
|
||||
},
|
||||
{
|
||||
label: '网络设置',
|
||||
children: [{ label: '更改安全组' }, { label: '切换VPC', divided: true }]
|
||||
}
|
||||
])
|
||||
</script>
|
|
@ -0,0 +1,16 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('基本用法', async ({ page }) => {
|
||||
page.on('pageerror', (exception) => expect(exception).toBeNull())
|
||||
await page.goto('action-menu#card-mode')
|
||||
|
||||
const wrap = page.locator('#card-mode')
|
||||
const actionMenu = wrap.locator('.tiny-action-menu')
|
||||
const visibleItem = actionMenu.locator('.tiny-action-menu__item')
|
||||
const moreItem = visibleItem.last()
|
||||
|
||||
await expect(visibleItem).toHaveCount(4)
|
||||
await expect(moreItem).not.toHaveText(/更多/)
|
||||
// 三点图标
|
||||
await expect(moreItem.locator('circle')).toHaveCount(3)
|
||||
})
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<tiny-action-menu :options="options" mode="card"> </tiny-action-menu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ActionMenu } from '@opentiny/vue'
|
||||
import { iconWebPlus, iconSuccessful, iconCloseSquare } from '@opentiny/vue-icon'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TinyActionMenu: ActionMenu
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
options: [
|
||||
{
|
||||
label: '远程登陆',
|
||||
icon: iconWebPlus()
|
||||
},
|
||||
{
|
||||
label: '开机',
|
||||
icon: iconSuccessful()
|
||||
},
|
||||
{
|
||||
label: '关机',
|
||||
icon: iconCloseSquare()
|
||||
},
|
||||
{
|
||||
label: '重启'
|
||||
},
|
||||
{
|
||||
label: '网络设置',
|
||||
children: [{ label: '更改安全组' }, { label: '切换VPC', divided: true }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -80,6 +80,20 @@ export default {
|
|||
},
|
||||
codeFiles: ['spacing.vue']
|
||||
},
|
||||
{
|
||||
demoId: 'card-mode',
|
||||
name: {
|
||||
'zh-CN': '菜单模式',
|
||||
'en-US': 'Mode'
|
||||
},
|
||||
desc: {
|
||||
'zh-CN':
|
||||
'<p>通过 <code>mode</code> 属性设置菜单模式以适配在不同场景中能够使用,例如:菜单按钮在卡片中使用,可以配置为 <code>card</code>,卡片模式字体为黑色,间距为10px。 <code>mode</code> 默认为值<code>default</code>。</p>',
|
||||
'en-US':
|
||||
'<p>Use the <code>mode</code> attribute to set the menu mode so that the vehicle can be used in different scenarios. For example, if the menu button is used in configuration, it can be configured as <code>card</code>, and the auxiliary mode font is Black with 10px spacing. <code>mode</code> defaults to <code>default</code>. </p>'
|
||||
},
|
||||
codeFiles: ['card-mode.vue']
|
||||
},
|
||||
{
|
||||
demoId: 'popper-class',
|
||||
name: {
|
||||
|
|
|
@ -12,6 +12,58 @@
|
|||
|
||||
import type { IActionMenuRenderlessParams, IActionMenuItemData } from '@/types'
|
||||
|
||||
export const computedMaxShowNum =
|
||||
({ props, state }: Pick<IActionMenuRenderlessParams, 'props' | 'state'>) =>
|
||||
(): number => {
|
||||
if (props.maxShowNum !== undefined) {
|
||||
return props.maxShowNum
|
||||
}
|
||||
if (state.isCardMode) {
|
||||
return 3
|
||||
} else {
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
export const computedSpacing =
|
||||
({ props, state }: Pick<IActionMenuRenderlessParams, 'props' | 'state'>) =>
|
||||
(): string => {
|
||||
if (props.spacing !== undefined) {
|
||||
return String(props.spacing).includes('px') ? props.spacing : props.spacing + 'px'
|
||||
}
|
||||
if (state.isCardMode) {
|
||||
return '10px'
|
||||
} else {
|
||||
return '5px'
|
||||
}
|
||||
}
|
||||
|
||||
export const computedMoreText =
|
||||
({ props, state, t }: Pick<IActionMenuRenderlessParams, 'props' | 'state', 't'>) =>
|
||||
(): string => {
|
||||
if (props.moreText !== undefined) {
|
||||
return props.moreText
|
||||
}
|
||||
if (state.isCardMode) {
|
||||
return ''
|
||||
} else {
|
||||
return t('ui.actionMenu.moreText')
|
||||
}
|
||||
}
|
||||
|
||||
export const computedSuffixIcon =
|
||||
({ props, state }: Pick<IActionMenuRenderlessParams, 'props' | 'state'>) =>
|
||||
(): string | Object => {
|
||||
if (props.suffixIcon) {
|
||||
return props.suffixIcon
|
||||
}
|
||||
if (state.isCardMode) {
|
||||
return 'tiny-icon-ellipsis'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export const handleMoreClick = (emit: IActionMenuRenderlessParams['emit']) => () => {
|
||||
emit('more-click')
|
||||
}
|
||||
|
|
|
@ -17,27 +17,45 @@ import type {
|
|||
ISharedRenderlessParamHooks,
|
||||
IActionMenuRenderlessParamUtils
|
||||
} from '@/types'
|
||||
import { handleMoreClick, handleItemClick, visibleChange } from './index'
|
||||
import {
|
||||
handleMoreClick,
|
||||
handleItemClick,
|
||||
visibleChange,
|
||||
computedMaxShowNum,
|
||||
computedSpacing,
|
||||
computedMoreText,
|
||||
computedSuffixIcon
|
||||
} from './index'
|
||||
|
||||
export const api = ['state', 'handleMoreClick', 'handleItemClick', 'visibleChange']
|
||||
|
||||
export const renderless = (
|
||||
props: IActionMenuProps,
|
||||
{ computed, reactive }: ISharedRenderlessParamHooks,
|
||||
{ emit }: IActionMenuRenderlessParamUtils
|
||||
{ emit, t }: IActionMenuRenderlessParamUtils
|
||||
): IActionMenuApi => {
|
||||
const api = {} as IActionMenuApi
|
||||
|
||||
const state: IActionMenuState = reactive({
|
||||
visibleOptions: computed(() => props.options.slice(0, props.maxShowNum)),
|
||||
moreOptions: computed(() => props.options.slice(props.maxShowNum)),
|
||||
spacing: computed(() => (String(props.spacing).includes('px') ? props.spacing : props.spacing + 'px'))
|
||||
visibleOptions: computed(() => props.options.slice(0, state.maxShowNum)),
|
||||
isCardMode: computed(() => props.mode === 'card'),
|
||||
moreOptions: computed(() => props.options.slice(state.maxShowNum)),
|
||||
spacing: computed(() => api.computedSpacing()),
|
||||
maxShowNum: computed(() => api.computedMaxShowNum()),
|
||||
moreText: computed(() => api.computedMoreText()),
|
||||
suffixIcon: computed(() => api.computedSuffixIcon())
|
||||
})
|
||||
|
||||
const api: IActionMenuApi = {
|
||||
Object.assign(api, {
|
||||
state,
|
||||
handleMoreClick: handleMoreClick(emit),
|
||||
handleItemClick: handleItemClick(emit),
|
||||
visibleChange: visibleChange(emit),
|
||||
state
|
||||
}
|
||||
computedMaxShowNum: computedMaxShowNum({ props, state }),
|
||||
computedSpacing: computedSpacing({ props, state }),
|
||||
computedMoreText: computedMoreText({ props, state, t }),
|
||||
computedSuffixIcon: computedSuffixIcon({ props, state })
|
||||
})
|
||||
|
||||
return api
|
||||
}
|
||||
|
|
|
@ -10,14 +10,27 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import type { ComputedRef, ExtractPropTypes, ComponentPublicInstance } from 'vue'
|
||||
import type { ExtractPropTypes, ComponentPublicInstance } from 'vue'
|
||||
import type { actionMenuProps } from '@/action-menu/src'
|
||||
import type { ISharedRenderlessFunctionParams, ISharedRenderlessParamUtils } from './shared.type'
|
||||
import type {
|
||||
handleMoreClick,
|
||||
handleItemClick,
|
||||
visibleChange,
|
||||
computedMaxShowNum,
|
||||
computedSpacing,
|
||||
computedMoreText,
|
||||
computedSuffixIcon
|
||||
} from '../src/action-menu'
|
||||
|
||||
export interface IActionMenuState {
|
||||
visibleOptions: ComputedRef<object>
|
||||
moreOptions: ComputedRef<object>
|
||||
spacing: ComputedRef<string | number>
|
||||
visibleOptions: object
|
||||
moreOptions: object
|
||||
isCardMode: boolean
|
||||
spacing: string | number
|
||||
maxShowNum: number
|
||||
moreText: string
|
||||
suffixIcon: string | Object
|
||||
}
|
||||
|
||||
export type IActionMenuProps = ExtractPropTypes<typeof actionMenuProps>
|
||||
|
@ -34,9 +47,13 @@ export interface IActionMenuItemData {
|
|||
}
|
||||
|
||||
export interface IActionMenuApi {
|
||||
handleMoreClick: () => void
|
||||
handleItemClick: (data: IActionMenuItemData) => void
|
||||
visibleChange: (status: boolean) => void
|
||||
handleMoreClick: ReturnType<typeof handleMoreClick>
|
||||
handleItemClick: ReturnType<typeof handleItemClick>
|
||||
visibleChange: ReturnType<typeof visibleChange>
|
||||
computedMaxShowNum: ReturnType<typeof computedMaxShowNum>
|
||||
computedSpacing: ReturnType<typeof computedSpacing>
|
||||
computedMoreText: ReturnType<typeof computedMoreText>
|
||||
computedSuffixIcon: ReturnType<typeof computedSuffixIcon>
|
||||
state: IActionMenuState
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,24 @@
|
|||
&__wrap {
|
||||
display: flex;
|
||||
|
||||
&.@{action-menu-prefix-cls}__card-mode {
|
||||
.@{action-menu-prefix-cls}__item {
|
||||
.tiny-svg {
|
||||
fill: var(--ti-action-menu-item-card-text-color);
|
||||
}
|
||||
|
||||
.@{dropdown-prefix-cls} {
|
||||
.@{dropdown-prefix-cls}-trigger {
|
||||
&:hover {
|
||||
.tiny-svg {
|
||||
fill: var(--ti-action-menu-item-card-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{action-menu-prefix-cls}__item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -54,15 +72,9 @@
|
|||
}
|
||||
|
||||
&:not(.is-disabled):hover {
|
||||
color: var(--ti-action-menu-item-hover-text-color);
|
||||
|
||||
&.@{action-menu-prefix-cls}__item-visable {
|
||||
.@{dropdown-item-prefix-cls} {
|
||||
background-color: var(--ti-action-menu-item-hover-bg-color);
|
||||
|
||||
&__wrap {
|
||||
color: var(--ti-action-menu-item-hover-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,6 +92,21 @@
|
|||
&:hover {
|
||||
background-color: var(--ti-action-menu-item-hover-bg-color);
|
||||
text-decoration: var(--ti-action-menu-hover-text-decoratio);
|
||||
color: var(--ti-action-menu-item-hover-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.@{action-menu-prefix-cls}__card-item {
|
||||
.@{dropdown-item-prefix-cls}__wrap {
|
||||
color: var(--ti-action-menu-item-card-text-color);
|
||||
|
||||
.tiny-svg {
|
||||
fill: var(--ti-action-menu-item-card-text-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--ti-action-menu-item-card-hover-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,5 +9,7 @@ export const tinyActionMenuSmbTheme = {
|
|||
'ti-action-menu-hover-text-decoratio': 'underline',
|
||||
'ti-dropdown-line-height': 'calc(var(--ti-common-line-height-4) + 2px)',
|
||||
'ti-action-menu-more-icon-width': 'var(--ti-common-size-5x)',
|
||||
'ti-action-menu-more-icon-height': 'var(--ti-common-size-5x)'
|
||||
'ti-action-menu-more-icon-height': 'var(--ti-common-size-5x)',
|
||||
'ti-action-menu-item-card-text-color': 'var(--ti-common-color-text-primary)',
|
||||
'ti-action-menu-item-card-hover-text-color': 'var(--ti-common-color-text-primary)'
|
||||
}
|
||||
|
|
|
@ -51,4 +51,8 @@
|
|||
--ti-action-menu-item-svg-margin-bottom: var(--ti-common-space-0, 0px);
|
||||
// 下拉菜单项图标左侧内边距
|
||||
--ti-action-menu-item-svg-margin-left: var(--ti-common-space-0, 0px);
|
||||
// 下拉菜单卡片模式字体颜色
|
||||
--ti-action-menu-item-card-text-color: var(--ti-common-color-text-link, #526ecc);
|
||||
// 下拉菜单卡片模式字体颜色
|
||||
--ti-action-menu-item-card-hover-text-color: var(--ti-common-color-text-link-hover, #344899);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ export const tinyDropdownItemSmbTheme = {
|
|||
'ti-dropdown-item-padding-vertical': 'var(--ti-common-space-0)',
|
||||
'ti-dropdown-item-padding-horizontal': 'var(--ti-common-space-4x)',
|
||||
'ti-dropdown-item-hover-text-color': 'var(--ti-common-color-text-primary)',
|
||||
'ti-dropdown-item-icon-color-hover': 'var(--ti-common-color-icon-graybg-hover)',
|
||||
'ti-dropdown-item-icon-color-hover': 'var(--ti-common-color-text-link-hover)',
|
||||
'ti-dropdown-item-expand-svg-margin-left': 'var(--ti-common-space-0)',
|
||||
'ti-dropdown-item-expand-svg-margin-right': 'var(--ti-common-space-2x)',
|
||||
'ti-dropdown-item-content-margin-left': 'calc(var(--ti-dropdown-item-expand-icon-size) + var(--ti-common-space-2x))',
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
export const tinyDropdownSmbTheme = {
|
||||
'ti-dropdown-icon-size': 'var(--ti-common-font-size-2)',
|
||||
'ti-dropdown-text-color-hover': 'var(--ti-common-color-text-primary)',
|
||||
'ti-dropdown-icon-color': 'var(--ti-common-color-placeholder)', // TINY-TODO:缺少#808080图标色变量
|
||||
'ti-dropdown-icon-color-hover': 'var(--ti-common-color-icon-graybg-hover)',
|
||||
'ti-dropdown-text-color-hover': 'var(--ti-common-color-text-link)',
|
||||
'ti-dropdown-icon-color': 'var(--ti-common-color-text-link)',
|
||||
'ti-dropdown-icon-color-hover': 'var(--ti-common-color-text-link)',
|
||||
'ti-dropdown-caret-svg-margin-horizontal': 'var(--ti-common-space-0)',
|
||||
'ti-dropdown-caret-button-padding-right': 'var(--ti-common-space-6x)',
|
||||
'ti-dropdown-caret-line-width': 'var(--ti-common-size-0)',
|
||||
|
|
|
@ -19,14 +19,10 @@ export const actionMenuProps = {
|
|||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
maxShowNum: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
maxShowNum: Number,
|
||||
moreText: String,
|
||||
spacing: {
|
||||
type: [String, Number],
|
||||
default: '5px'
|
||||
type: [String, Number]
|
||||
},
|
||||
textField: {
|
||||
type: String,
|
||||
|
@ -48,6 +44,10 @@ export const actionMenuProps = {
|
|||
showIcon: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
<template>
|
||||
<div class="tiny-action-menu">
|
||||
<ul class="tiny-action-menu__wrap">
|
||||
<ul :class="{ 'tiny-action-menu__wrap': true, 'tiny-action-menu__card-mode': state.isCardMode }">
|
||||
<li
|
||||
v-for="(visableItem, index) in state.visibleOptions"
|
||||
:key="index"
|
||||
:class="['tiny-action-menu__item', 'tiny-action-menu__item-visable', { 'is-disabled': visableItem.disabled }]"
|
||||
:class="[
|
||||
'tiny-action-menu__item',
|
||||
'tiny-action-menu__item-visable',
|
||||
{ 'is-disabled': visableItem.disabled, 'tiny-action-menu__card-item': state.isCardMode }
|
||||
]"
|
||||
>
|
||||
<tiny-dropdown-item
|
||||
:item-data="visableItem"
|
||||
|
@ -23,14 +27,16 @@
|
|||
|
||||
<li v-if="state.moreOptions.length" class="tiny-action-menu__item">
|
||||
<tiny-dropdown
|
||||
:title="moreText"
|
||||
:title="state.moreText"
|
||||
:trigger="trigger"
|
||||
:suffix-icon="suffixIcon"
|
||||
:show-icon="showIcon"
|
||||
@item-click="handleItemClick"
|
||||
@handle-click="handleMoreClick"
|
||||
@visible-change="visibleChange"
|
||||
>
|
||||
<template v-if="state.suffixIcon" #suffix-icon>
|
||||
<component :is="state.suffixIcon"></component>
|
||||
</template>
|
||||
<template #dropdown>
|
||||
<tiny-dropdown-menu :text-field="textField" :popper-class="popperClass">
|
||||
<tiny-dropdown-item
|
||||
|
@ -53,20 +59,22 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { IActionMenuApi } from '@opentiny/vue-renderless/types/action-menu.type'
|
||||
import { setup, $prefix, defineComponent } from '@opentiny/vue-common'
|
||||
import { renderless, api } from '@opentiny/vue-renderless/action-menu/vue'
|
||||
import { iconEllipsis } from '@opentiny/vue-icon'
|
||||
import '@opentiny/vue-theme/action-menu/index.less'
|
||||
import Dropdown from '@opentiny/vue-dropdown'
|
||||
import DropdownMenu from '@opentiny/vue-dropdown-menu'
|
||||
import DropdownItem from '@opentiny/vue-dropdown-item'
|
||||
import { t } from '@opentiny/vue-locale'
|
||||
|
||||
export default defineComponent({
|
||||
name: $prefix + 'ActionMenu',
|
||||
components: {
|
||||
TinyDropdown: Dropdown,
|
||||
TinyDropdownMenu: DropdownMenu,
|
||||
TinyDropdownItem: DropdownItem
|
||||
TinyDropdownItem: DropdownItem,
|
||||
TinyIconEllipsis: iconEllipsis()
|
||||
},
|
||||
emits: ['more-click', 'item-click', 'visible-change'],
|
||||
props: {
|
||||
|
@ -74,17 +82,12 @@ export default defineComponent({
|
|||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
maxShowNum: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
maxShowNum: Number,
|
||||
moreText: {
|
||||
type: String,
|
||||
default: t('ui.actionMenu.moreText')
|
||||
type: String
|
||||
},
|
||||
spacing: {
|
||||
type: [String, Number],
|
||||
default: '5px'
|
||||
type: [String, Number]
|
||||
},
|
||||
textField: {
|
||||
type: String,
|
||||
|
@ -106,10 +109,14 @@ export default defineComponent({
|
|||
showIcon: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
}
|
||||
},
|
||||
setup(props, context) {
|
||||
return setup({ props, context, renderless, api })
|
||||
return setup({ props, context, renderless, api }) as unknown as IActionMenuApi
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
Loading…
Reference in New Issue