发布 v1.10 #5

Merged
czm merged 147 commits from develop into main 2026-08-20 11:36:27 +08:00
3 changed files with 260 additions and 51 deletions
Showing only changes of commit a051dbacba - Show all commits

View File

@@ -17,13 +17,17 @@ import {
ElIcon, ElIcon,
ElTag, ElTag,
ElText, ElText,
ElTooltip,
} from 'element-plus'; } from 'element-plus';
import { $t } from '#/locales'; import { $t } from '#/locales';
export type ActionPlacement = 'inline' | 'menu'; export type ActionPlacement = 'inline' | 'menu';
export type ActionTone = 'danger' | 'default'; export type ActionTone = 'danger' | 'default';
export type CardDensity = 'compact' | 'default';
export type MetaLayout = 'compact' | 'stacked';
export type TagLayout = 'inline' | 'stacked'; export type TagLayout = 'inline' | 'stacked';
export type TagPlacement = 'details' | 'title';
export interface ActionButton { export interface ActionButton {
icon?: any; icon?: any;
@@ -40,6 +44,8 @@ export interface ActionButton {
export interface CardPrimaryAction { export interface CardPrimaryAction {
icon?: any; icon?: any;
iconOnly?: boolean;
showIndicator?: boolean;
text: string; text: string;
permission?: string; permission?: string;
onClick: (row: any) => void; onClick: (row: any) => void;
@@ -58,12 +64,16 @@ export interface CardListProps {
cornerTagField?: string; cornerTagField?: string;
cornerTagMap?: Record<string, string>; cornerTagMap?: Record<string, string>;
cornerTagTypeMap?: Record<string, TagProps['type']>; cornerTagTypeMap?: Record<string, TagProps['type']>;
density?: CardDensity;
defaultIcon: any; defaultIcon: any;
data: any[]; data: any[];
metaLayout?: MetaLayout;
primaryAction?: CardPrimaryAction; primaryAction?: CardPrimaryAction;
tagField?: string; tagField?: string;
tagLayout?: TagLayout; tagLayout?: TagLayout;
tagMap?: Record<string, string>; tagMap?: Record<string, string>;
tagPlacement?: TagPlacement;
titleLineClamp?: number;
} }
const props = withDefaults(defineProps<CardListProps>(), { const props = withDefaults(defineProps<CardListProps>(), {
@@ -74,10 +84,14 @@ const props = withDefaults(defineProps<CardListProps>(), {
cornerTagField: '', cornerTagField: '',
cornerTagMap: () => ({}), cornerTagMap: () => ({}),
cornerTagTypeMap: () => ({}), cornerTagTypeMap: () => ({}),
density: 'default',
metaLayout: 'stacked',
primaryAction: undefined, primaryAction: undefined,
tagField: '', tagField: '',
tagLayout: 'inline', tagLayout: 'inline',
tagMap: () => ({}), tagMap: () => ({}),
tagPlacement: 'title',
titleLineClamp: 1,
}); });
const { hasAccessByCodes } = useAccess(); const { hasAccessByCodes } = useAccess();
@@ -107,6 +121,10 @@ const resolvedPrimaryAction = computed(() => {
return props.primaryAction; return props.primaryAction;
}); });
const showPrimaryActionIndicator = computed(
() => resolvedPrimaryAction.value?.showIndicator !== false,
);
function resolveActionPlacement( function resolveActionPlacement(
action: ActionButton, action: ActionButton,
index: number, index: number,
@@ -185,6 +203,12 @@ function resolveMetaItems(item: any) {
} }
return metaItems; return metaItems;
} }
function resolveMetaAriaLabel(item: any) {
return resolveMetaItems(item)
.map((meta) => `${meta.label}${meta.value}`)
.join('');
}
</script> </script>
<template> <template>
@@ -194,7 +218,10 @@ function resolveMetaItems(item: any) {
:key="item.id ?? index" :key="item.id ?? index"
shadow="never" shadow="never"
class="card-item" class="card-item"
:class="[{ 'card-item--interactive': resolvedPrimaryAction }]" :class="[
`card-item--${density}`,
{ 'card-item--interactive': resolvedPrimaryAction },
]"
:role="resolvedPrimaryAction ? 'button' : undefined" :role="resolvedPrimaryAction ? 'button' : undefined"
:tabindex="resolvedPrimaryAction ? 0 : undefined" :tabindex="resolvedPrimaryAction ? 0 : undefined"
@click="handlePrimaryAction(item)" @click="handlePrimaryAction(item)"
@@ -213,11 +240,17 @@ function resolveMetaItems(item: any) {
class="title-row" class="title-row"
:class="{ 'title-row--stacked': tagLayout === 'stacked' }" :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] }} {{ item[titleField] }}
</ElText> </ElText>
<ElTag <ElTag
v-if="tagField && item[tagField]" v-if="tagPlacement === 'title' && tagField && item[tagField]"
size="small" size="small"
effect="plain" effect="plain"
type="info" type="info"
@@ -228,6 +261,24 @@ function resolveMetaItems(item: any) {
<ElText line-clamp="2" class="item-desc w-full"> <ElText line-clamp="2" class="item-desc w-full">
{{ item[descField] }} {{ item[descField] }}
</ElText> </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>
<div <div
v-if="$slots.corner || (cornerTagField && item[cornerTagField])" v-if="$slots.corner || (cornerTagField && item[cornerTagField])"
@@ -244,7 +295,31 @@ function resolveMetaItems(item: any) {
</ElTag> </ElTag>
</slot> </slot>
</div> </div>
<div v-if="resolveMetaItems(item).length > 0" class="card-meta-row"> <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 <span
v-for="meta in resolveMetaItems(item)" v-for="meta in resolveMetaItems(item)"
:key="meta.label" :key="meta.label"
@@ -253,14 +328,52 @@ function resolveMetaItems(item: any) {
<span class="card-meta-label">{{ meta.label }}</span> <span class="card-meta-label">{{ meta.label }}</span>
<span class="card-meta-value">{{ meta.value }}</span> <span class="card-meta-value">{{ meta.value }}</span>
</span> </span>
</template>
</slot>
</div> </div>
</div> </div>
</div> </div>
<template v-if="resolvedPrimaryAction || hasVisibleActions(item)" #footer> <template
<div class="card-footer"> v-if="
<div v-if="resolvedPrimaryAction" class="card-primary-hint"> (resolvedPrimaryAction && showPrimaryActionIndicator) ||
<div class="primary-label"> 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"> <ElIcon v-if="resolvedPrimaryAction.icon" class="primary-icon">
<IconifyIcon <IconifyIcon
v-if="typeof resolvedPrimaryAction.icon === 'string'" v-if="typeof resolvedPrimaryAction.icon === 'string'"
@@ -401,6 +514,14 @@ function resolveMetaItems(item: any) {
var(--shadow-subtle); 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) { :deep(.el-card__body) {
flex: 1; flex: 1;
padding: 18px; padding: 18px;
@@ -413,9 +534,14 @@ function resolveMetaItems(item: any) {
.card-content { .card-content {
display: flex; display: flex;
flex-direction: column;
min-height: 116px; min-height: 116px;
} }
.card-item--compact .card-content {
min-height: 0;
}
.card-header { .card-header {
display: grid; display: grid;
grid-template-columns: 44px minmax(0, 1fr) auto; grid-template-columns: 44px minmax(0, 1fr) auto;
@@ -442,6 +568,10 @@ function resolveMetaItems(item: any) {
min-width: 0; min-width: 0;
} }
.card-item--compact .card-meta {
gap: var(--space-2);
}
.title-row { .title-row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -465,6 +595,13 @@ function resolveMetaItems(item: any) {
color: hsl(var(--text-strong)); color: hsl(var(--text-strong));
} }
.card-title--wrap {
overflow: visible;
overflow-wrap: anywhere;
text-overflow: clip;
white-space: normal;
}
.item-desc { .item-desc {
min-height: 44px; min-height: 44px;
font-size: 13px; font-size: 13px;
@@ -472,6 +609,31 @@ function resolveMetaItems(item: any) {
color: hsl(var(--text-muted)); 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 { .card-meta-row {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -493,6 +655,18 @@ function resolveMetaItems(item: any) {
min-width: 0; 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 { .card-meta-label {
flex-shrink: 0; flex-shrink: 0;
} }
@@ -534,12 +708,45 @@ function resolveMetaItems(item: any) {
border-top: 1px solid hsl(var(--divider-faint) / 85%); border-top: 1px solid hsl(var(--divider-faint) / 85%);
} }
.card-footer--actions-only {
justify-content: flex-end;
}
.card-primary-hint { .card-primary-hint {
display: flex; display: flex;
flex: 1; flex: 1;
align-items: center;
min-width: 0; 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 { .primary-label {
display: flex; display: flex;
gap: 8px; gap: 8px;

View File

@@ -19,7 +19,6 @@ import {
Delete, Delete,
Edit, Edit,
Lock, Lock,
Notebook,
OfficeBuilding, OfficeBuilding,
Plus, Plus,
Promotion, Promotion,
@@ -188,7 +187,7 @@ interface FieldDefinition {
placeholder?: string; placeholder?: string;
} }
const primaryAction: CardPrimaryAction = { const primaryAction: CardPrimaryAction = {
icon: Notebook, showIndicator: false,
text: $t('documentCollection.actions.knowledge'), text: $t('documentCollection.actions.knowledge'),
permission: '/api/v1/documentCollection/query', permission: '/api/v1/documentCollection/query',
onClick(row) { onClick(row) {
@@ -702,12 +701,15 @@ function changeCategory(category: any) {
:data="pageList" :data="pageList"
:primary-action="primaryAction" :primary-action="primaryAction"
:actions="actions" :actions="actions"
density="compact"
meta-layout="compact"
tag-field="collectionType" tag-field="collectionType"
tag-layout="stacked" tag-placement="details"
:tag-map="collectionTypeLabelMap" :tag-map="collectionTypeLabelMap"
:title-line-clamp="0"
> >
<template #corner="{ item }"> <template #details="{ item }">
<AiResourceCornerMeta publish-placement="below"> <AiResourceCornerMeta>
<template #publish> <template #publish>
<div <div
class="knowledge-publish-chip" class="knowledge-publish-chip"
@@ -715,7 +717,10 @@ function changeCategory(category: any) {
> >
<span class="knowledge-publish-chip__dot"></span> <span class="knowledge-publish-chip__dot"></span>
<span>{{ <span>{{
resolvePublishStatusMeta(item.displayPublishStatus, item.publishStatus).label resolvePublishStatusMeta(
item.displayPublishStatus,
item.publishStatus,
).label
}}</span> }}</span>
</div> </div>
</template> </template>
@@ -874,10 +879,9 @@ function changeCategory(category: any) {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 22px; min-height: 22px;
min-width: 70px; padding: 0 8px;
padding: 0 9px;
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 500;
line-height: 1; line-height: 1;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 999px; border-radius: 999px;
@@ -928,10 +932,9 @@ h1 {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 22px; min-height: 22px;
min-width: 70px; padding: 0 8px;
padding: 0 9px;
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 500;
line-height: 1; line-height: 1;
color: hsl(var(--text-strong)); color: hsl(var(--text-strong));
background: transparent; background: transparent;

View File

@@ -57,7 +57,6 @@ import { api } from '#/api/request';
import workflowIcon from '#/assets/ai/workflow/workflowIcon.png'; import workflowIcon from '#/assets/ai/workflow/workflowIcon.png';
// import workflowSvg from '#/assets/workflow.svg'; // import workflowSvg from '#/assets/workflow.svg';
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue'; import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
import DesignIcon from '#/components/icons/DesignIcon.vue';
import CardList from '#/components/page/CardList.vue'; import CardList from '#/components/page/CardList.vue';
import PageData from '#/components/page/PageData.vue'; import PageData from '#/components/page/PageData.vue';
import PageSide from '#/components/page/PageSide.vue'; import PageSide from '#/components/page/PageSide.vue';
@@ -118,7 +117,7 @@ interface ApiFieldDoc {
} }
const primaryAction: CardPrimaryAction = { const primaryAction: CardPrimaryAction = {
icon: DesignIcon, showIndicator: false,
text: $t('button.design'), text: $t('button.design'),
permission: '/api/v1/workflow/save', permission: '/api/v1/workflow/save',
onClick: (row: any) => { 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, icon: Tickets,
text: $t('aiWorkflowExecRecord.moduleName'), text: $t('aiWorkflowExecRecord.moduleName'),
@@ -207,19 +218,6 @@ const actions: ActionButton[] = [
showApiInstruction(row); 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, icon: Download,
text: $t('button.export'), text: $t('button.export'),
@@ -1204,9 +1202,12 @@ function handleHeaderButtonClick(data: any) {
:data="pageList" :data="pageList"
:primary-action="primaryAction" :primary-action="primaryAction"
:actions="actions" :actions="actions"
density="compact"
meta-layout="compact"
:title-line-clamp="0"
> >
<template #corner="{ item }"> <template #details="{ item }">
<AiResourceCornerMeta publish-placement="below"> <AiResourceCornerMeta>
<template #publish> <template #publish>
<div <div
class="workflow-publish-chip" class="workflow-publish-chip"
@@ -1371,10 +1372,9 @@ function handleHeaderButtonClick(data: any) {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 22px; min-height: 22px;
min-width: 70px; padding: 0 8px;
padding: 0 9px;
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 500;
line-height: 1; line-height: 1;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 999px; border-radius: 999px;
@@ -1419,10 +1419,9 @@ function handleHeaderButtonClick(data: any) {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 22px; min-height: 22px;
min-width: 70px; padding: 0 8px;
padding: 0 9px;
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 500;
line-height: 1; line-height: 1;
color: hsl(var(--text-strong)); color: hsl(var(--text-strong));
background: transparent; background: transparent;