feat: 优化知识库与工作流卡片布局
- 统一紧凑卡片、完整标题与状态元信息展示 - 移除多余入口指示并将工作流分享操作前置
This commit is contained in:
@@ -17,13 +17,17 @@ import {
|
||||
ElIcon,
|
||||
ElTag,
|
||||
ElText,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
export type ActionPlacement = 'inline' | 'menu';
|
||||
export type ActionTone = 'danger' | 'default';
|
||||
export type CardDensity = 'compact' | 'default';
|
||||
export type MetaLayout = 'compact' | 'stacked';
|
||||
export type TagLayout = 'inline' | 'stacked';
|
||||
export type TagPlacement = 'details' | 'title';
|
||||
|
||||
export interface ActionButton {
|
||||
icon?: any;
|
||||
@@ -40,6 +44,8 @@ export interface ActionButton {
|
||||
|
||||
export interface CardPrimaryAction {
|
||||
icon?: any;
|
||||
iconOnly?: boolean;
|
||||
showIndicator?: boolean;
|
||||
text: string;
|
||||
permission?: string;
|
||||
onClick: (row: any) => void;
|
||||
@@ -58,12 +64,16 @@ export interface CardListProps {
|
||||
cornerTagField?: string;
|
||||
cornerTagMap?: Record<string, string>;
|
||||
cornerTagTypeMap?: Record<string, TagProps['type']>;
|
||||
density?: CardDensity;
|
||||
defaultIcon: any;
|
||||
data: any[];
|
||||
metaLayout?: MetaLayout;
|
||||
primaryAction?: CardPrimaryAction;
|
||||
tagField?: string;
|
||||
tagLayout?: TagLayout;
|
||||
tagMap?: Record<string, string>;
|
||||
tagPlacement?: TagPlacement;
|
||||
titleLineClamp?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<CardListProps>(), {
|
||||
@@ -74,10 +84,14 @@ const props = withDefaults(defineProps<CardListProps>(), {
|
||||
cornerTagField: '',
|
||||
cornerTagMap: () => ({}),
|
||||
cornerTagTypeMap: () => ({}),
|
||||
density: 'default',
|
||||
metaLayout: 'stacked',
|
||||
primaryAction: undefined,
|
||||
tagField: '',
|
||||
tagLayout: 'inline',
|
||||
tagMap: () => ({}),
|
||||
tagPlacement: 'title',
|
||||
titleLineClamp: 1,
|
||||
});
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
@@ -107,6 +121,10 @@ const resolvedPrimaryAction = computed(() => {
|
||||
return props.primaryAction;
|
||||
});
|
||||
|
||||
const showPrimaryActionIndicator = computed(
|
||||
() => resolvedPrimaryAction.value?.showIndicator !== false,
|
||||
);
|
||||
|
||||
function resolveActionPlacement(
|
||||
action: ActionButton,
|
||||
index: number,
|
||||
@@ -185,6 +203,12 @@ function resolveMetaItems(item: any) {
|
||||
}
|
||||
return metaItems;
|
||||
}
|
||||
|
||||
function resolveMetaAriaLabel(item: any) {
|
||||
return resolveMetaItems(item)
|
||||
.map((meta) => `${meta.label}:${meta.value}`)
|
||||
.join(',');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -194,7 +218,10 @@ function resolveMetaItems(item: any) {
|
||||
:key="item.id ?? index"
|
||||
shadow="never"
|
||||
class="card-item"
|
||||
:class="[{ 'card-item--interactive': resolvedPrimaryAction }]"
|
||||
:class="[
|
||||
`card-item--${density}`,
|
||||
{ 'card-item--interactive': resolvedPrimaryAction },
|
||||
]"
|
||||
:role="resolvedPrimaryAction ? 'button' : undefined"
|
||||
:tabindex="resolvedPrimaryAction ? 0 : undefined"
|
||||
@click="handlePrimaryAction(item)"
|
||||
@@ -213,11 +240,17 @@ function resolveMetaItems(item: any) {
|
||||
class="title-row"
|
||||
:class="{ 'title-row--stacked': tagLayout === 'stacked' }"
|
||||
>
|
||||
<ElText truncated size="large" class="card-title">
|
||||
<ElText
|
||||
:truncated="titleLineClamp === 1"
|
||||
:line-clamp="titleLineClamp > 1 ? titleLineClamp : undefined"
|
||||
size="large"
|
||||
class="card-title"
|
||||
:class="{ 'card-title--wrap': titleLineClamp === 0 }"
|
||||
>
|
||||
{{ item[titleField] }}
|
||||
</ElText>
|
||||
<ElTag
|
||||
v-if="tagField && item[tagField]"
|
||||
v-if="tagPlacement === 'title' && tagField && item[tagField]"
|
||||
size="small"
|
||||
effect="plain"
|
||||
type="info"
|
||||
@@ -228,6 +261,24 @@ function resolveMetaItems(item: any) {
|
||||
<ElText line-clamp="2" class="item-desc w-full">
|
||||
{{ item[descField] }}
|
||||
</ElText>
|
||||
<div
|
||||
v-if="
|
||||
(tagPlacement === 'details' && tagField && item[tagField]) ||
|
||||
$slots.details
|
||||
"
|
||||
class="card-detail-row"
|
||||
>
|
||||
<ElTag
|
||||
v-if="tagPlacement === 'details' && tagField && item[tagField]"
|
||||
size="small"
|
||||
effect="plain"
|
||||
type="info"
|
||||
round
|
||||
>
|
||||
{{ tagMap[item[tagField]] || item[tagField] }}
|
||||
</ElTag>
|
||||
<slot name="details" :item="item"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="$slots.corner || (cornerTagField && item[cornerTagField])"
|
||||
@@ -244,23 +295,85 @@ function resolveMetaItems(item: any) {
|
||||
</ElTag>
|
||||
</slot>
|
||||
</div>
|
||||
<div v-if="resolveMetaItems(item).length > 0" class="card-meta-row">
|
||||
<span
|
||||
v-for="meta in resolveMetaItems(item)"
|
||||
:key="meta.label"
|
||||
class="card-meta-item"
|
||||
>
|
||||
<span class="card-meta-label">{{ meta.label }}:</span>
|
||||
<span class="card-meta-value">{{ meta.value }}</span>
|
||||
</span>
|
||||
<div
|
||||
v-if="$slots.meta || resolveMetaItems(item).length > 0"
|
||||
class="card-meta-row"
|
||||
>
|
||||
<slot name="meta" :item="item">
|
||||
<div
|
||||
v-if="metaLayout === 'compact'"
|
||||
class="card-meta-compact"
|
||||
:aria-label="resolveMetaAriaLabel(item)"
|
||||
>
|
||||
<template
|
||||
v-for="(meta, metaIndex) in resolveMetaItems(item)"
|
||||
:key="meta.label"
|
||||
>
|
||||
<span
|
||||
v-if="metaIndex > 0"
|
||||
class="card-meta-separator"
|
||||
aria-hidden="true"
|
||||
>
|
||||
·
|
||||
</span>
|
||||
<span class="card-meta-value">{{ meta.value }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<template v-else>
|
||||
<span
|
||||
v-for="meta in resolveMetaItems(item)"
|
||||
:key="meta.label"
|
||||
class="card-meta-item"
|
||||
>
|
||||
<span class="card-meta-label">{{ meta.label }}:</span>
|
||||
<span class="card-meta-value">{{ meta.value }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="resolvedPrimaryAction || hasVisibleActions(item)" #footer>
|
||||
<div class="card-footer">
|
||||
<div v-if="resolvedPrimaryAction" class="card-primary-hint">
|
||||
<div class="primary-label">
|
||||
<template
|
||||
v-if="
|
||||
(resolvedPrimaryAction && showPrimaryActionIndicator) ||
|
||||
hasVisibleActions(item)
|
||||
"
|
||||
#footer
|
||||
>
|
||||
<div
|
||||
class="card-footer"
|
||||
:class="{
|
||||
'card-footer--actions-only':
|
||||
!resolvedPrimaryAction || !showPrimaryActionIndicator,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="resolvedPrimaryAction && showPrimaryActionIndicator"
|
||||
class="card-primary-hint"
|
||||
>
|
||||
<ElTooltip
|
||||
v-if="resolvedPrimaryAction.iconOnly"
|
||||
:content="resolvedPrimaryAction.text"
|
||||
placement="top"
|
||||
:show-after="300"
|
||||
>
|
||||
<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
|
||||
v-if="typeof resolvedPrimaryAction.icon === 'string'"
|
||||
@@ -401,6 +514,14 @@ function resolveMetaItems(item: any) {
|
||||
var(--shadow-subtle);
|
||||
}
|
||||
|
||||
.card-item--compact :deep(.el-card__body) {
|
||||
padding: var(--space-4) var(--space-5);
|
||||
}
|
||||
|
||||
.card-item--compact :deep(.el-card__footer) {
|
||||
padding: 0 var(--space-5) var(--space-4);
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
flex: 1;
|
||||
padding: 18px;
|
||||
@@ -413,9 +534,14 @@ function resolveMetaItems(item: any) {
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 116px;
|
||||
}
|
||||
|
||||
.card-item--compact .card-content {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(0, 1fr) auto;
|
||||
@@ -442,6 +568,10 @@ function resolveMetaItems(item: any) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-item--compact .card-meta {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.title-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -465,6 +595,13 @@ function resolveMetaItems(item: any) {
|
||||
color: hsl(var(--text-strong));
|
||||
}
|
||||
|
||||
.card-title--wrap {
|
||||
overflow: visible;
|
||||
overflow-wrap: anywhere;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
min-height: 44px;
|
||||
font-size: 13px;
|
||||
@@ -472,6 +609,31 @@ function resolveMetaItems(item: any) {
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.card-item--compact .item-desc {
|
||||
min-height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.card-detail-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
}
|
||||
|
||||
.card-detail-row :deep(.el-tag) {
|
||||
--el-tag-bg-color: hsl(var(--surface-subtle));
|
||||
--el-tag-border-color: hsl(var(--line-subtle));
|
||||
--el-tag-text-color: hsl(var(--text-muted));
|
||||
--el-tag-border-radius: var(--radius-pill);
|
||||
--el-tag-font-size: 11px;
|
||||
|
||||
height: 22px;
|
||||
padding: 0 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-meta-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -493,6 +655,18 @@ function resolveMetaItems(item: any) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-meta-compact {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-meta-separator {
|
||||
color: hsl(var(--divider-faint));
|
||||
}
|
||||
|
||||
.card-meta-label {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -534,12 +708,45 @@ function resolveMetaItems(item: any) {
|
||||
border-top: 1px solid hsl(var(--divider-faint) / 85%);
|
||||
}
|
||||
|
||||
.card-footer--actions-only {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.card-primary-hint {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-primary-button {
|
||||
--el-button-bg-color: hsl(var(--primary) / 8%);
|
||||
--el-button-border-color: hsl(var(--primary) / 14%);
|
||||
--el-button-hover-bg-color: hsl(var(--primary) / 14%);
|
||||
--el-button-hover-border-color: hsl(var(--primary) / 20%);
|
||||
--el-button-active-bg-color: hsl(var(--primary) / 18%);
|
||||
--el-button-active-border-color: hsl(var(--primary) / 24%);
|
||||
--el-button-text-color: hsl(var(--primary));
|
||||
--el-button-hover-text-color: hsl(var(--primary));
|
||||
--el-button-active-text-color: hsl(var(--primary));
|
||||
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
transition:
|
||||
color var(--motion-duration-base) var(--motion-ease-standard),
|
||||
background-color var(--motion-duration-base) var(--motion-ease-standard),
|
||||
transform var(--motion-duration-fast) var(--motion-ease-standard);
|
||||
}
|
||||
|
||||
.card-primary-button:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.card-primary-button:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 4px hsl(var(--primary) / 12%);
|
||||
}
|
||||
|
||||
.primary-label {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
Delete,
|
||||
Edit,
|
||||
Lock,
|
||||
Notebook,
|
||||
OfficeBuilding,
|
||||
Plus,
|
||||
Promotion,
|
||||
@@ -188,7 +187,7 @@ interface FieldDefinition {
|
||||
placeholder?: string;
|
||||
}
|
||||
const primaryAction: CardPrimaryAction = {
|
||||
icon: Notebook,
|
||||
showIndicator: false,
|
||||
text: $t('documentCollection.actions.knowledge'),
|
||||
permission: '/api/v1/documentCollection/query',
|
||||
onClick(row) {
|
||||
@@ -702,12 +701,15 @@ function changeCategory(category: any) {
|
||||
:data="pageList"
|
||||
:primary-action="primaryAction"
|
||||
:actions="actions"
|
||||
density="compact"
|
||||
meta-layout="compact"
|
||||
tag-field="collectionType"
|
||||
tag-layout="stacked"
|
||||
tag-placement="details"
|
||||
:tag-map="collectionTypeLabelMap"
|
||||
:title-line-clamp="0"
|
||||
>
|
||||
<template #corner="{ item }">
|
||||
<AiResourceCornerMeta publish-placement="below">
|
||||
<template #details="{ item }">
|
||||
<AiResourceCornerMeta>
|
||||
<template #publish>
|
||||
<div
|
||||
class="knowledge-publish-chip"
|
||||
@@ -715,7 +717,10 @@ function changeCategory(category: any) {
|
||||
>
|
||||
<span class="knowledge-publish-chip__dot"></span>
|
||||
<span>{{
|
||||
resolvePublishStatusMeta(item.displayPublishStatus, item.publishStatus).label
|
||||
resolvePublishStatusMeta(
|
||||
item.displayPublishStatus,
|
||||
item.publishStatus,
|
||||
).label
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -874,10 +879,9 @@ function changeCategory(category: any) {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 22px;
|
||||
min-width: 70px;
|
||||
padding: 0 9px;
|
||||
padding: 0 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 999px;
|
||||
@@ -928,10 +932,9 @@ h1 {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 22px;
|
||||
min-width: 70px;
|
||||
padding: 0 9px;
|
||||
padding: 0 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
color: hsl(var(--text-strong));
|
||||
background: transparent;
|
||||
|
||||
@@ -57,7 +57,6 @@ import { api } from '#/api/request';
|
||||
import workflowIcon from '#/assets/ai/workflow/workflowIcon.png';
|
||||
// import workflowSvg from '#/assets/workflow.svg';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import DesignIcon from '#/components/icons/DesignIcon.vue';
|
||||
import CardList from '#/components/page/CardList.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
@@ -118,7 +117,7 @@ interface ApiFieldDoc {
|
||||
}
|
||||
|
||||
const primaryAction: CardPrimaryAction = {
|
||||
icon: DesignIcon,
|
||||
showIndicator: false,
|
||||
text: $t('button.design'),
|
||||
permission: '/api/v1/workflow/save',
|
||||
onClick: (row: any) => {
|
||||
@@ -185,6 +184,18 @@ const actions: ActionButton[] = [
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Share,
|
||||
text: $t('button.share'),
|
||||
permission: '/api/v1/workflow/save',
|
||||
placement: 'inline',
|
||||
disabled: (row: any) =>
|
||||
row.publishStatus !== 'PUBLISHED' || sharingWorkflowId.value === row.id,
|
||||
loading: (row: any) => sharingWorkflowId.value === row.id,
|
||||
onClick: (row: any) => {
|
||||
shareWorkflow(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Tickets,
|
||||
text: $t('aiWorkflowExecRecord.moduleName'),
|
||||
@@ -207,19 +218,6 @@ const actions: ActionButton[] = [
|
||||
showApiInstruction(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Share,
|
||||
text: $t('button.share'),
|
||||
permission: '/api/v1/workflow/save',
|
||||
placement: 'menu',
|
||||
disabled: (row: any) =>
|
||||
row.publishStatus !== 'PUBLISHED' ||
|
||||
sharingWorkflowId.value === row.id,
|
||||
loading: (row: any) => sharingWorkflowId.value === row.id,
|
||||
onClick: (row: any) => {
|
||||
shareWorkflow(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Download,
|
||||
text: $t('button.export'),
|
||||
@@ -1204,9 +1202,12 @@ function handleHeaderButtonClick(data: any) {
|
||||
:data="pageList"
|
||||
:primary-action="primaryAction"
|
||||
:actions="actions"
|
||||
density="compact"
|
||||
meta-layout="compact"
|
||||
:title-line-clamp="0"
|
||||
>
|
||||
<template #corner="{ item }">
|
||||
<AiResourceCornerMeta publish-placement="below">
|
||||
<template #details="{ item }">
|
||||
<AiResourceCornerMeta>
|
||||
<template #publish>
|
||||
<div
|
||||
class="workflow-publish-chip"
|
||||
@@ -1371,10 +1372,9 @@ function handleHeaderButtonClick(data: any) {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 22px;
|
||||
min-width: 70px;
|
||||
padding: 0 9px;
|
||||
padding: 0 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 999px;
|
||||
@@ -1419,10 +1419,9 @@ function handleHeaderButtonClick(data: any) {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 22px;
|
||||
min-width: 70px;
|
||||
padding: 0 9px;
|
||||
padding: 0 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
color: hsl(var(--text-strong));
|
||||
background: transparent;
|
||||
|
||||
Reference in New Issue
Block a user