发布 v1.10 #5

Merged
czm merged 147 commits from develop into main 2026-08-20 11:36:27 +08:00
7 changed files with 193 additions and 107 deletions
Showing only changes of commit 4b52d85512 - Show all commits

View File

@@ -341,115 +341,129 @@ function resolveMetaAriaLabel(item: any) {
" "
#footer #footer
> >
<div <div class="card-footer">
class="card-footer"
:class="{
'card-footer--actions-only':
!resolvedPrimaryAction || !showPrimaryActionIndicator,
}"
>
<div <div
v-if="resolvedPrimaryAction && showPrimaryActionIndicator" v-if="
class="card-primary-hint" (resolvedPrimaryAction && showPrimaryActionIndicator) ||
resolveInlineActions(item).length > 0
"
class="card-main-actions"
> >
<ElTooltip <div
v-if="resolvedPrimaryAction.iconOnly" v-if="resolvedPrimaryAction && showPrimaryActionIndicator"
:content="resolvedPrimaryAction.text" class="card-primary-hint"
placement="top"
:show-after="300"
> >
<ElButton <ElTooltip
class="card-primary-button" v-if="resolvedPrimaryAction.iconOnly"
:aria-label="resolvedPrimaryAction.text" :content="resolvedPrimaryAction.text"
circle placement="top"
@click.stop="handlePrimaryAction(item)" :show-after="300"
> >
<ElIcon v-if="resolvedPrimaryAction.icon"> <ElButton
class="card-primary-button"
:aria-label="resolvedPrimaryAction.text"
circle
@click.stop="handlePrimaryAction(item)"
>
<ElIcon v-if="resolvedPrimaryAction.icon">
<IconifyIcon
v-if="typeof resolvedPrimaryAction.icon === 'string'"
:icon="resolvedPrimaryAction.icon"
/>
<component v-else :is="resolvedPrimaryAction.icon" />
</ElIcon>
</ElButton>
</ElTooltip>
<div v-else class="primary-label">
<ElIcon v-if="resolvedPrimaryAction.icon" class="primary-icon">
<IconifyIcon <IconifyIcon
v-if="typeof resolvedPrimaryAction.icon === 'string'" v-if="typeof resolvedPrimaryAction.icon === 'string'"
:icon="resolvedPrimaryAction.icon" :icon="resolvedPrimaryAction.icon"
/> />
<component v-else :is="resolvedPrimaryAction.icon" /> <component v-else :is="resolvedPrimaryAction.icon" />
</ElIcon> </ElIcon>
<span class="primary-text">
{{ resolvedPrimaryAction.text }}
</span>
</div>
</div>
<div
v-if="resolveInlineActions(item).length > 0"
class="card-actions"
@click.stop
>
<ElButton
v-for="(action, actionIndex) in resolveInlineActions(item)"
:key="`${item.id ?? index}-inline-${actionIndex}`"
:icon="
typeof action.icon === 'string' ? undefined : action.icon
"
size="small"
class="card-action-btn"
:class="{
'card-action-btn--danger': action.tone === 'danger',
}"
:disabled="resolveActionState(action.disabled, item)"
:loading="resolveActionState(action.loading, item)"
link
@click.stop="handleActionClick($event, action, item)"
>
<template v-if="typeof action.icon === 'string'" #icon>
<IconifyIcon :icon="action.icon" />
</template>
{{ resolveActionText(action, item) }}
</ElButton> </ElButton>
</ElTooltip>
<div v-else class="primary-label">
<ElIcon v-if="resolvedPrimaryAction.icon" class="primary-icon">
<IconifyIcon
v-if="typeof resolvedPrimaryAction.icon === 'string'"
:icon="resolvedPrimaryAction.icon"
/>
<component v-else :is="resolvedPrimaryAction.icon" />
</ElIcon>
<span class="primary-text">{{ resolvedPrimaryAction.text }}</span>
</div> </div>
</div> </div>
<div v-if="hasVisibleActions(item)" class="card-actions" @click.stop> <ElDropdown
v-if="resolveMenuActions(item).length > 0"
class="card-overflow-action"
trigger="click"
placement="bottom-end"
>
<ElButton <ElButton
v-for="(action, actionIndex) in resolveInlineActions(item)" class="card-action-btn card-action-btn--menu"
:key="`${item.id ?? index}-inline-${actionIndex}`" :icon="MoreFilled"
:icon="typeof action.icon === 'string' ? undefined : action.icon" :aria-label="$t('button.more')"
size="small"
class="card-action-btn"
:class="{ 'card-action-btn--danger': action.tone === 'danger' }"
:disabled="resolveActionState(action.disabled, item)"
:loading="resolveActionState(action.loading, item)"
link link
@click.stop="handleActionClick($event, action, item)" @click.stop
> />
<template v-if="typeof action.icon === 'string'" #icon> <template #dropdown>
<IconifyIcon :icon="action.icon" /> <ElDropdownMenu>
</template> <ElDropdownItem
{{ resolveActionText(action, item) }} v-for="(action, actionIndex) in resolveMenuActions(item)"
</ElButton> :key="`${item.id ?? index}-menu-${actionIndex}`"
:class="{
<ElDropdown 'card-menu-item--danger': action.tone === 'danger',
v-if="resolveMenuActions(item).length > 0" }"
trigger="click" :disabled="
placement="bottom-end" resolveActionState(action.disabled, item) ||
> resolveActionState(action.loading, item)
<ElButton "
class="card-action-btn card-action-btn--menu" @click="handleActionClick($event, action, item)"
:icon="MoreFilled" >
link <div class="menu-action-content">
@click.stop <ElIcon
/> v-if="resolveActionState(action.loading, item)"
<template #dropdown> class="is-loading"
<ElDropdownMenu> >
<ElDropdownItem <Loading />
v-for="(action, actionIndex) in resolveMenuActions(item)" </ElIcon>
:key="`${item.id ?? index}-menu-${actionIndex}`" <ElIcon v-else-if="action.icon">
:class="{ <IconifyIcon
'card-menu-item--danger': action.tone === 'danger', v-if="typeof action.icon === 'string'"
}" :icon="action.icon"
:disabled=" />
resolveActionState(action.disabled, item) || <component v-else :is="action.icon" />
resolveActionState(action.loading, item) </ElIcon>
" <span>{{ resolveActionText(action, item) }}</span>
@click="handleActionClick($event, action, item)" </div>
> </ElDropdownItem>
<div class="menu-action-content"> </ElDropdownMenu>
<ElIcon </template>
v-if="resolveActionState(action.loading, item)" </ElDropdown>
class="is-loading"
>
<Loading />
</ElIcon>
<ElIcon v-else-if="action.icon">
<IconifyIcon
v-if="typeof action.icon === 'string'"
:icon="action.icon"
/>
<component v-else :is="action.icon" />
</ElIcon>
<span>{{ resolveActionText(action, item) }}</span>
</div>
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</div>
</div> </div>
</template> </template>
</ElCard> </ElCard>
@@ -488,7 +502,7 @@ function resolveMetaAriaLabel(item: any) {
flex-direction: column; flex-direction: column;
background: hsl(var(--card)); background: hsl(var(--card));
border: 1px solid hsl(var(--border)); border: 1px solid hsl(var(--border));
border-radius: 18px; border-radius: var(--radius-panel);
transition: transition:
transform 0.18s ease, transform 0.18s ease,
box-shadow 0.18s ease, box-shadow 0.18s ease,
@@ -701,20 +715,24 @@ function resolveMetaAriaLabel(item: any) {
.card-footer { .card-footer {
display: flex; display: flex;
gap: 12px; gap: var(--space-2);
align-items: center; align-items: center;
justify-content: space-between; justify-content: flex-start;
padding-top: 14px; min-height: var(--space-8);
padding-top: var(--space-4);
border-top: 1px solid hsl(var(--divider-faint) / 85%); border-top: 1px solid hsl(var(--divider-faint) / 85%);
} }
.card-footer--actions-only { .card-main-actions {
justify-content: flex-end; display: flex;
gap: var(--space-2);
align-items: center;
min-width: 0;
} }
.card-primary-hint { .card-primary-hint {
display: flex; display: flex;
flex: 1; flex: 0 0 auto;
align-items: center; align-items: center;
min-width: 0; min-width: 0;
} }
@@ -751,7 +769,9 @@ function resolveMetaAriaLabel(item: any) {
display: flex; display: flex;
gap: 8px; gap: 8px;
align-items: center; align-items: center;
height: var(--space-8);
min-width: 0; min-width: 0;
padding: 0 var(--space-1);
} }
.primary-icon { .primary-icon {
@@ -769,9 +789,18 @@ function resolveMetaAriaLabel(item: any) {
.card-actions { .card-actions {
display: flex; display: flex;
flex-shrink: 0; flex: 0 1 auto;
gap: 4px; gap: var(--space-2);
align-items: center; align-items: center;
min-width: 0;
}
.card-actions :deep(.el-button + .el-button) {
margin-left: 0;
}
.card-overflow-action {
flex-shrink: 0;
margin-left: auto; margin-left: auto;
} }
@@ -781,7 +810,9 @@ function resolveMetaAriaLabel(item: any) {
--el-button-hover-text-color: hsl(var(--text-strong)); --el-button-hover-text-color: hsl(var(--text-strong));
--el-button-active-text-color: hsl(var(--text-strong)); --el-button-active-text-color: hsl(var(--text-strong));
padding: 0 4px; height: var(--space-8);
padding: 0 var(--space-1);
white-space: nowrap;
} }
.card-action-btn--danger { .card-action-btn--danger {
@@ -791,6 +822,7 @@ function resolveMetaAriaLabel(item: any) {
} }
.card-action-btn--menu { .card-action-btn--menu {
width: var(--space-8);
padding: 0; padding: 0;
} }

View File

@@ -89,6 +89,58 @@ describe('cardList', () => {
expect(primaryAction).not.toHaveBeenCalled(); expect(primaryAction).not.toHaveBeenCalled();
}); });
it('将可见主操作与行内操作紧凑放在左侧,并将更多菜单独立放在右侧', () => {
const wrapper = mountCardList({
primaryAction: {
text: '编排',
onClick: vi.fn(),
},
actions: [
{
text: '发布',
placement: 'inline',
onClick: vi.fn(),
},
{
text: '删除',
placement: 'menu',
onClick: vi.fn(),
},
],
});
const mainActions = wrapper.get('.card-main-actions');
expect(mainActions.get('.card-primary-hint').text()).toContain('编排');
expect(mainActions.get('.card-actions').text()).toContain('发布');
expect(mainActions.find('.card-action-btn--menu').exists()).toBe(false);
expect(wrapper.find('.card-overflow-action').exists()).toBe(true);
});
it('仅渲染当前可见的行内操作且不保留空占位', () => {
const wrapper = mountCardList({
actions: [
{
text: '编辑',
placement: 'inline',
visible: false,
onClick: vi.fn(),
},
{
text: '运行',
placement: 'inline',
onClick: vi.fn(),
},
],
});
const inlineButtons = wrapper.findAll('.card-actions .card-action-btn');
expect(inlineButtons).toHaveLength(1);
expect(inlineButtons[0]?.text()).toContain('运行');
expect(wrapper.text()).not.toContain('编辑');
});
it('异步次级操作执行中会禁用重复点击并展示加载态', async () => { it('异步次级操作执行中会禁用重复点击并展示加载态', async () => {
const inlineAction = vi.fn(); const inlineAction = vi.fn();
const wrapper = mountCardList({ const wrapper = mountCardList({

View File

@@ -46,7 +46,7 @@
"republish": "Republish", "republish": "Republish",
"offline": "Offline", "offline": "Offline",
"hide": "Hide", "hide": "Hide",
"more": "Mode", "more": "More",
"submitDeleteApproval": "Submit Delete Approval", "submitDeleteApproval": "Submit Delete Approval",
"submitPublishApproval": "Submit Publish Approval", "submitPublishApproval": "Submit Publish Approval",
"viewSegmentation": "View Segments", "viewSegmentation": "View Segments",

View File

@@ -358,6 +358,8 @@ async function handleDeleteAction(row: AgentInfo) {
:data="pageList" :data="pageList"
:primary-action="primaryAction" :primary-action="primaryAction"
:actions="actions" :actions="actions"
density="compact"
meta-layout="compact"
> >
<template #details="{ item }"> <template #details="{ item }">
<AiResourceCornerMeta> <AiResourceCornerMeta>

View File

@@ -730,7 +730,6 @@ function changeCategory(category: any) {
tag-field="collectionType" tag-field="collectionType"
tag-placement="details" tag-placement="details"
:tag-map="collectionTypeLabelMap" :tag-map="collectionTypeLabelMap"
:title-line-clamp="0"
> >
<template #details="{ item }"> <template #details="{ item }">
<AiResourceCornerMeta> <AiResourceCornerMeta>

View File

@@ -124,7 +124,7 @@ const actions: ActionButton[] = [
text: $t('button.delete'), text: $t('button.delete'),
tone: 'danger', tone: 'danger',
permission: '/api/v1/plugin/remove', permission: '/api/v1/plugin/remove',
placement: 'inline', placement: 'menu',
onClick(item) { onClick(item) {
handleDelete(item); handleDelete(item);
}, },
@@ -382,6 +382,8 @@ const handleClickCategory = (item: PluginCategory) => {
:primary-action="primaryAction" :primary-action="primaryAction"
:actions="actions" :actions="actions"
:default-icon="defaultPluginIcon" :default-icon="defaultPluginIcon"
density="compact"
meta-layout="compact"
> >
<template #corner="{ item }"> <template #corner="{ item }">
<ElTag <ElTag

View File

@@ -1245,7 +1245,6 @@ function handleHeaderButtonClick(data: any) {
:actions="actions" :actions="actions"
density="compact" density="compact"
meta-layout="compact" meta-layout="compact"
:title-line-clamp="0"
> >
<template #details="{ item }"> <template #details="{ item }">
<AiResourceCornerMeta> <AiResourceCornerMeta>