perf: 卡片入口视觉效果重做

This commit is contained in:
2026-03-11 20:30:36 +08:00
parent 0a8a7c8046
commit 219fa566ef
6 changed files with 507 additions and 259 deletions

View File

@@ -8,7 +8,6 @@ import {
ElAvatar, ElAvatar,
ElButton, ElButton,
ElCard, ElCard,
ElDivider,
ElDropdown, ElDropdown,
ElDropdownItem, ElDropdownItem,
ElDropdownMenu, ElDropdownMenu,
@@ -17,14 +16,31 @@ import {
ElText, ElText,
} from 'element-plus'; } from 'element-plus';
export type ActionPlacement = 'inline' | 'menu';
export type ActionTone = 'danger' | 'default';
export interface ActionButton { export interface ActionButton {
icon: any; icon?: any;
text: string; text: string;
className: string; className?: string;
permission: string; permission?: string;
placement?: ActionPlacement;
tone?: ActionTone;
onClick: (row: any) => void; onClick: (row: any) => void;
} }
export interface CardPrimaryAction {
icon?: any;
text: string;
permission?: string;
onClick: (row: any) => void;
}
interface ResolvedActionButton extends ActionButton {
placement: ActionPlacement;
tone: ActionTone;
}
export interface CardListProps { export interface CardListProps {
iconField?: string; iconField?: string;
titleField?: string; titleField?: string;
@@ -32,53 +48,97 @@ export interface CardListProps {
actions?: ActionButton[]; actions?: ActionButton[];
defaultIcon: any; defaultIcon: any;
data: any[]; data: any[];
primaryAction?: CardPrimaryAction;
tagField?: string; tagField?: string;
tagMap?: Record<string, string>; tagMap?: Record<string, string>;
} }
const props = withDefaults(defineProps<CardListProps>(), { const props = withDefaults(defineProps<CardListProps>(), {
iconField: 'icon', iconField: 'icon',
titleField: 'title', titleField: 'title',
descField: 'description', descField: 'description',
actions: () => [], actions: () => [],
primaryAction: undefined,
tagField: '', tagField: '',
tagMap: () => ({}), tagMap: () => ({}),
}); });
const { hasAccessByCodes } = useAccess(); const { hasAccessByCodes } = useAccess();
const filterActions = computed(() => {
return props.actions.filter((action) => { function hasPermission(permission?: string) {
return hasAccessByCodes([action.permission]); return !permission || hasAccessByCodes([permission]);
}
const resolvedPrimaryAction = computed(() => {
if (!props.primaryAction || !hasPermission(props.primaryAction.permission)) {
return undefined;
}
return props.primaryAction;
}); });
const resolvedActions = computed<ResolvedActionButton[]>(() => {
return props.actions
.filter((action) => hasPermission(action.permission))
.map((action, index) => ({
...action,
placement:
action.placement ||
(resolvedPrimaryAction.value ? 'menu' : index < 3 ? 'inline' : 'menu'),
tone:
action.tone ||
(action.className?.includes('danger') ? 'danger' : 'default'),
}));
}); });
const visibleActions = computed(() => {
return filterActions.value.length <= 3 const inlineActions = computed(() => {
? filterActions.value return resolvedActions.value.filter((action) => action.placement === 'inline');
: filterActions.value.slice(0, 3);
}); });
const hiddenActions = computed(() => {
return filterActions.value.length > 3 ? filterActions.value.slice(3) : []; const menuActions = computed(() => {
return resolvedActions.value.filter((action) => action.placement === 'menu');
}); });
const showFooter = computed(() => {
return Boolean(
resolvedPrimaryAction.value ||
inlineActions.value.length ||
menuActions.value.length,
);
});
function handlePrimaryAction(item: any) {
resolvedPrimaryAction.value?.onClick(item);
}
function handleActionClick(event: Event, action: ActionButton, item: any) {
event.stopPropagation();
action.onClick(item);
}
</script> </script>
<template> <template>
<div class="card-grid"> <div class="card-grid">
<ElCard <ElCard
v-for="(item, index) in props.data" v-for="(item, index) in props.data"
:key="index" :key="item.id ?? index"
shadow="hover" shadow="never"
footer-class="foot-c" :class="['card-item', { 'card-item--interactive': resolvedPrimaryAction }]"
:style="{ :role="resolvedPrimaryAction ? 'button' : undefined"
'--el-box-shadow-light': '0px 2px 12px 0px rgb(100 121 153 10%)', :tabindex="resolvedPrimaryAction ? 0 : undefined"
}" @click="handlePrimaryAction(item)"
@keydown.enter.prevent="handlePrimaryAction(item)"
@keydown.space.prevent="handlePrimaryAction(item)"
> >
<div class="flex flex-col gap-3"> <div class="card-content">
<div class="flex items-center gap-3"> <div class="card-header">
<ElAvatar <ElAvatar
class="shrink-0" class="card-avatar shrink-0"
:src="item[iconField] || defaultIcon" :src="item[iconField] || defaultIcon"
:size="36" :size="44"
/> />
<div class="card-meta">
<div class="title-row"> <div class="title-row">
<ElText truncated size="large" class="font-medium"> <ElText truncated size="large" class="card-title">
{{ item[titleField] }} {{ item[titleField] }}
</ElText> </ElText>
<ElTag <ElTag
@@ -90,136 +150,268 @@ const hiddenActions = computed(() => {
{{ tagMap[item[tagField]] || item[tagField] }} {{ tagMap[item[tagField]] || item[tagField] }}
</ElTag> </ElTag>
</div> </div>
</div>
<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> </div>
<template #footer> </div>
<div :class="visibleActions.length > 2 ? 'footer-div' : ''"> </div>
<template v-for="(action, idx) in visibleActions" :key="idx">
<template v-if="showFooter" #footer>
<div class="card-footer">
<div v-if="resolvedPrimaryAction" class="card-primary-hint">
<div 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
v-if="inlineActions.length || menuActions.length"
class="card-actions"
@click.stop
>
<ElButton <ElButton
v-for="action in inlineActions"
:key="action.text"
:icon="typeof action.icon === 'string' ? undefined : action.icon" :icon="typeof action.icon === 'string' ? undefined : action.icon"
size="small" size="small"
:style="{ class="card-action-btn"
'--el-button-text-color': 'hsl(220deg 9.68% 63.53%)', :class="{ 'card-action-btn--danger': action.tone === 'danger' }"
'--el-button-font-weight': 400,
}"
link link
@click="action.onClick(item)" @click.stop="handleActionClick($event, action, item)"
> >
<template v-if="typeof action.icon === 'string'" #icon> <template v-if="typeof action.icon === 'string'" #icon>
<IconifyIcon :icon="action.icon" /> <IconifyIcon :icon="action.icon" />
</template> </template>
{{ action.text }} {{ action.text }}
</ElButton> </ElButton>
<ElDivider
v-if="
filterActions.length <= 3
? idx < filterActions.length - 1
: true
"
direction="vertical"
/>
</template>
<ElDropdown v-if="hiddenActions.length > 0" trigger="click"> <ElDropdown
v-if="menuActions.length > 0"
trigger="click"
placement="bottom-end"
>
<ElButton <ElButton
:style="{ class="card-action-btn card-action-btn--menu"
'--el-button-text-color': 'hsl(220deg 9.68% 63.53%)',
'--el-button-font-weight': 400,
}"
:icon="MoreFilled" :icon="MoreFilled"
link link
@click.stop
/> />
<template #dropdown> <template #dropdown>
<ElDropdownMenu> <ElDropdownMenu>
<ElDropdownItem <ElDropdownItem
v-for="(action, idx) in hiddenActions" v-for="action in menuActions"
:key="idx" :key="action.text"
:class="{ 'card-menu-item--danger': action.tone === 'danger' }"
@click="action.onClick(item)" @click="action.onClick(item)"
> >
<template #default> <div class="menu-action-content">
<div :class="`${action.className} handle-div`">
<ElIcon v-if="action.icon"> <ElIcon v-if="action.icon">
<component :is="action.icon" /> <IconifyIcon
v-if="typeof action.icon === 'string'"
:icon="action.icon"
/>
<component v-else :is="action.icon" />
</ElIcon> </ElIcon>
{{ action.text }} <span>{{ action.text }}</span>
</div> </div>
</template>
</ElDropdownItem> </ElDropdownItem>
</ElDropdownMenu> </ElDropdownMenu>
</template> </template>
</ElDropdown> </ElDropdown>
</div> </div>
</div>
</template> </template>
</ElCard> </ElCard>
</div> </div>
</template> </template>
<style scoped> <style scoped>
/* 响应式调整 */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.card-grid { .card-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
} }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.card-grid { .card-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
min-width: 100%;
} }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.card-grid { .card-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); grid-template-columns: minmax(0, 1fr);
} }
} }
:deep(.el-card__footer) {
border-top: none;
}
.footer-div {
display: flex;
justify-content: space-between;
padding: 8px 20px;
background-color: hsl(var(--background-deep));
border-radius: 8px;
}
.handle-div {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0;
}
.card-grid { .card-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px; gap: 20px;
min-width: max(100%, 600px); /* 确保至少显示2个卡片 */ min-width: max(100%, 600px);
} }
.item-desc { .card-item {
height: 40px; background: hsl(var(--card));
font-size: clamp(8px, 1vw, 14px); border: 1px solid hsl(var(--border));
line-height: 20px; border-radius: 18px;
color: #75808d; transition:
transform 0.18s ease,
box-shadow 0.18s ease,
border-color 0.18s ease,
background-color 0.18s ease;
} }
.item-danger { .card-item--interactive {
color: var(--el-color-danger); cursor: pointer;
}
.card-item--interactive:hover {
border-color: hsl(var(--primary) / 20%);
box-shadow: var(--shadow-subtle);
transform: translateY(-2px);
}
.card-item--interactive:focus-visible {
outline: none;
border-color: hsl(var(--primary) / 32%);
box-shadow:
0 0 0 4px hsl(var(--primary) / 12%),
var(--shadow-subtle);
}
:deep(.el-card__body) {
padding: 18px;
}
:deep(.el-card__footer) {
padding: 0 18px 18px;
border-top: none;
}
.card-content {
display: flex;
min-height: 116px;
}
.card-header {
display: flex;
gap: 14px;
align-items: flex-start;
width: 100%;
}
.card-avatar {
background: hsl(var(--surface-subtle));
border: 1px solid hsl(var(--line-subtle));
}
.card-meta {
display: flex;
flex: 1;
flex-direction: column;
gap: 10px;
min-width: 0;
} }
.title-row { .title-row {
display: flex; display: flex;
flex: 1; flex-wrap: wrap;
gap: 8px; gap: 8px;
align-items: center; align-items: center;
justify-content: space-between;
min-width: 0; min-width: 0;
} }
.card-title {
font-weight: 600;
color: hsl(var(--text-strong));
}
.item-desc {
min-height: 44px;
font-size: 13px;
line-height: 22px;
color: hsl(var(--text-muted));
}
.card-footer {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
padding-top: 14px;
border-top: 1px solid hsl(var(--divider-faint) / 85%);
}
.card-primary-hint {
display: flex;
flex: 1;
min-width: 0;
}
.primary-label {
display: flex;
gap: 8px;
align-items: center;
min-width: 0;
}
.primary-icon {
color: hsl(var(--primary));
}
.primary-text {
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
font-weight: 500;
color: hsl(var(--text-strong));
white-space: nowrap;
}
.card-actions {
display: flex;
flex-shrink: 0;
gap: 4px;
align-items: center;
margin-left: auto;
}
.card-action-btn {
--el-button-font-weight: 500;
--el-button-text-color: hsl(var(--text-muted));
--el-button-hover-text-color: hsl(var(--text-strong));
--el-button-active-text-color: hsl(var(--text-strong));
padding: 0 4px;
}
.card-action-btn--danger {
--el-button-text-color: hsl(var(--destructive));
--el-button-hover-text-color: hsl(var(--destructive));
--el-button-active-text-color: hsl(var(--destructive));
}
.card-action-btn--menu {
padding: 0;
}
.menu-action-content {
display: flex;
gap: 8px;
align-items: center;
}
:deep(.card-menu-item--danger) {
color: hsl(var(--destructive));
}
</style> </style>

View File

@@ -3,7 +3,10 @@ import type { FormInstance } from 'element-plus';
import type { BotInfo } from '@easyflow/types'; import type { BotInfo } from '@easyflow/types';
import type { ActionButton } from '#/components/page/CardList.vue'; import type {
ActionButton,
CardPrimaryAction,
} from '#/components/page/CardList.vue';
import { computed, markRaw, onMounted, ref } from 'vue'; import { computed, markRaw, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@@ -70,36 +73,35 @@ const headerButtons = [
function resolveNavTitle(row: BotInfo) { function resolveNavTitle(row: BotInfo) {
return (row as Record<string, any>)?.title || row?.name || ''; return (row as Record<string, any>)?.title || row?.name || '';
} }
const actions: ActionButton[] = [ const primaryAction: CardPrimaryAction = {
{
icon: Edit,
text: $t('button.edit'),
className: '',
permission: '',
onClick(row: BotInfo) {
modalRef.value?.open('edit', row);
},
},
{
icon: Setting, icon: Setting,
text: $t('button.setting'), text: $t('button.setting'),
className: '',
permission: '',
onClick(row: BotInfo) { onClick(row: BotInfo) {
router.push({ router.push({
path: '/ai/bots/setting/' + row.id, path: `/ai/bots/setting/${row.id}`,
query: { query: {
pageKey: '/ai/bots', pageKey: '/ai/bots',
navTitle: resolveNavTitle(row), navTitle: resolveNavTitle(row),
}, },
}); });
}, },
};
const actions: ActionButton[] = [
{
icon: Edit,
text: $t('button.edit'),
placement: 'inline',
onClick(row: BotInfo) {
modalRef.value?.open('edit', row);
},
}, },
{ {
icon: Delete, icon: Delete,
text: $t('button.delete'), text: $t('button.delete'),
className: 'item-danger', tone: 'danger',
permission: '/api/v1/bot/remove', permission: '/api/v1/bot/remove',
placement: 'inline',
onClick(row: BotInfo) { onClick(row: BotInfo) {
removeBot(row); removeBot(row);
}, },
@@ -298,6 +300,7 @@ const getSideList = async () => {
<CardList <CardList
:default-icon="defaultAvatar" :default-icon="defaultAvatar"
:data="pageList" :data="pageList"
:primary-action="primaryAction"
:actions="actions" :actions="actions"
/> />
</template> </template>

View File

@@ -1,7 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type {FormInstance} from 'element-plus'; import type {FormInstance} from 'element-plus';
import {ElForm, ElFormItem, ElInput, ElInputNumber, ElMessage, ElMessageBox,} from 'element-plus';
import type { ActionButton } from '#/components/page/CardList.vue'; import type {ActionButton, CardPrimaryAction,} from '#/components/page/CardList.vue';
import CardPage from '#/components/page/CardList.vue';
import {computed, onMounted, ref} from 'vue'; import {computed, onMounted, ref} from 'vue';
import {useRouter} from 'vue-router'; import {useRouter} from 'vue-router';
@@ -10,20 +12,11 @@ import { EasyFlowFormModal } from '@easyflow/common-ui';
import {$t} from '@easyflow/locales'; import {$t} from '@easyflow/locales';
import {Delete, Edit, Notebook, Plus, Search} from '@element-plus/icons-vue'; import {Delete, Edit, Notebook, Plus, Search} from '@element-plus/icons-vue';
import {
ElForm,
ElFormItem,
ElInput,
ElInputNumber,
ElMessage,
ElMessageBox,
} from 'element-plus';
import {tryit} from 'radash'; import {tryit} from 'radash';
import {api} from '#/api/request'; import {api} from '#/api/request';
import defaultIcon from '#/assets/ai/knowledge/book.svg'; import defaultIcon from '#/assets/ai/knowledge/book.svg';
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue'; import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
import CardPage 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';
import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue'; import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue';
@@ -37,6 +30,17 @@ const collectionTypeLabelMap = {
function resolveNavTitle(row: Record<string, any>) { function resolveNavTitle(row: Record<string, any>) {
return row?.title || row?.name || ''; return row?.title || row?.name || '';
} }
function openKnowledgeDetail(row: { id: string; name?: string; title?: string }) {
router.push({
path: '/ai/documentCollection/document',
query: {
id: row.id,
pageKey: '/ai/documentCollection',
navTitle: resolveNavTitle(row),
},
});
}
interface FieldDefinition { interface FieldDefinition {
// 字段名称 // 字段名称
prop: string; prop: string;
@@ -49,38 +53,29 @@ interface FieldDefinition {
// 占位符 // 占位符
placeholder?: string; placeholder?: string;
} }
// 操作按钮配置 const primaryAction: CardPrimaryAction = {
icon: Notebook,
text: $t('documentCollection.actions.knowledge'),
permission: '/api/v1/documentCollection/save',
onClick(row) {
openKnowledgeDetail(row);
},
};
const actions: ActionButton[] = [ const actions: ActionButton[] = [
{ {
icon: Edit, icon: Edit,
text: $t('button.edit'), text: $t('button.edit'),
className: '',
permission: '/api/v1/documentCollection/save', permission: '/api/v1/documentCollection/save',
placement: 'inline',
onClick(row) { onClick(row) {
aiKnowledgeModalRef.value.openDialog(row); aiKnowledgeModalRef.value.openDialog(row);
}, },
}, },
{
icon: Notebook,
text: $t('documentCollection.actions.knowledge'),
className: '',
permission: '/api/v1/documentCollection/save',
onClick(row) {
router.push({
path: '/ai/documentCollection/document',
query: {
id: row.id,
pageKey: '/ai/documentCollection',
navTitle: resolveNavTitle(row),
},
});
},
},
{ {
icon: Search, icon: Search,
text: $t('documentCollection.actions.retrieve'), text: $t('documentCollection.actions.retrieve'),
className: '', placement: 'inline',
permission: '',
onClick(row) { onClick(row) {
router.push({ router.push({
path: '/ai/documentCollection/document', path: '/ai/documentCollection/document',
@@ -96,8 +91,9 @@ const actions: ActionButton[] = [
{ {
text: $t('button.delete'), text: $t('button.delete'),
icon: Delete, icon: Delete,
className: 'item-danger', tone: 'danger',
permission: '/api/v1/documentCollection/remove', permission: '/api/v1/documentCollection/remove',
placement: 'inline',
onClick(row) { onClick(row) {
handleDelete(row); handleDelete(row);
}, },
@@ -314,10 +310,11 @@ function changeCategory(category: any) {
<template #default="{ pageList }"> <template #default="{ pageList }">
<CardPage <CardPage
:default-icon="defaultIcon" :default-icon="defaultIcon"
title-key="title" title-field="title"
avatar-key="icon" icon-field="icon"
description-key="description" desc-field="description"
:data="pageList" :data="pageList"
:primary-action="primaryAction"
:actions="actions" :actions="actions"
tag-field="collectionType" tag-field="collectionType"
:tag-map="collectionTypeLabelMap" :tag-map="collectionTypeLabelMap"

View File

@@ -1,11 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type {FormInstance} from 'element-plus'; import type {FormInstance} from 'element-plus';
import { onMounted, ref } from 'vue';
import { EasyFlowFormModal } from '@easyflow/common-ui';
import { Plus, Remove } from '@element-plus/icons-vue';
import { import {
ElForm, ElForm,
ElFormItem, ElFormItem,
@@ -18,6 +12,12 @@ import {
ElSelect, ElSelect,
} from 'element-plus'; } from 'element-plus';
import {onMounted, ref} from 'vue';
import {EasyFlowFormModal} from '@easyflow/common-ui';
import {Plus, Remove} from '@element-plus/icons-vue';
import {api} from '#/api/request'; import {api} from '#/api/request';
import UploadAvatar from '#/components/upload/UploadAvatar.vue'; import UploadAvatar from '#/components/upload/UploadAvatar.vue';
import {$t} from '#/locales'; import {$t} from '#/locales';
@@ -25,6 +25,7 @@ import { $t } from '#/locales';
const emit = defineEmits(['reload']); const emit = defineEmits(['reload']);
const embeddingLlmList = ref<any>([]); const embeddingLlmList = ref<any>([]);
const rerankerLlmList = ref<any>([]); const rerankerLlmList = ref<any>([]);
const categoryList = ref<any[]>([]);
interface headersType { interface headersType {
label: string; label: string;
value: string; value: string;
@@ -46,6 +47,11 @@ onMounted(() => {
api.get('/api/v1/model/list?supportRerankerLlmList=true').then((res) => { api.get('/api/v1/model/list?supportRerankerLlmList=true').then((res) => {
rerankerLlmList.value = res.data; rerankerLlmList.value = res.data;
}); });
api.get('/api/v1/pluginCategory/list').then((res) => {
if (res.errorCode === 0) {
categoryList.value = res.data;
}
});
}); });
defineExpose({ defineExpose({
openDialog, openDialog,
@@ -57,6 +63,7 @@ const isAdd = ref(true);
const tempAddHeaders = ref<headersType[]>([]); const tempAddHeaders = ref<headersType[]>([]);
const entity = ref<any>({ const entity = ref<any>({
alias: '', alias: '',
categoryIds: [],
deptId: '', deptId: '',
icon: '', icon: '',
title: '', title: '',
@@ -91,6 +98,7 @@ const rules = ref({
// functions // functions
function openDialog(row: any) { function openDialog(row: any) {
tempAddHeaders.value = [];
if (row.id) { if (row.id) {
isAdd.value = false; isAdd.value = false;
if (row.headers) { if (row.headers) {
@@ -100,26 +108,62 @@ function openDialog(row: any) {
entity.value = { entity.value = {
...row, ...row,
authType: row.authType || 'none', authType: row.authType || 'none',
categoryIds: row.categoryIds?.map((item: any) => item.id) || [],
}; };
dialogVisible.value = true; dialogVisible.value = true;
} }
async function syncPluginCategories(pluginId: string, categoryIds: string[]) {
if (!pluginId) {
return;
}
const relationRes = await api.post(
'/api/v1/pluginCategoryMapping/updateRelation',
{
pluginId,
categoryIds,
},
);
if (relationRes.errorCode !== 0) {
throw new Error(relationRes.message || 'sync categories failed');
}
}
function save() { function save() {
saveForm.value?.validate((valid) => { saveForm.value?.validate((valid) => {
if (valid) { if (valid) {
btnLoading.value = true;
const plainEntity = { ...entity.value }; const plainEntity = { ...entity.value };
const plainHeaders = [...tempAddHeaders.value]; const plainHeaders = [...tempAddHeaders.value];
const categoryIds = [...(plainEntity.categoryIds || [])];
delete plainEntity.categoryIds;
if (isAdd.value) { if (isAdd.value) {
api api
.post('/api/v1/plugin/plugin/save', { .post('/api/v1/plugin/plugin/save', {
...plainEntity, ...plainEntity,
headers: plainHeaders, headers: plainHeaders,
}) })
.then((res) => { .then(async (res) => {
if (res.errorCode === 0) { if (res.errorCode === 0) {
const pluginId =
res.data?.id ||
res.data ||
plainEntity.id ||
entity.value.id ||
'';
await syncPluginCategories(pluginId, categoryIds);
dialogVisible.value = false; dialogVisible.value = false;
ElMessage.success($t('message.saveOkMessage')); ElMessage.success($t('message.saveOkMessage'));
emit('reload'); emit('reload');
} else {
ElMessage.error(res.message);
} }
})
.catch((error) => {
ElMessage.error(error?.message || $t('message.saveFailMessage'));
})
.finally(() => {
btnLoading.value = false;
}); });
} else { } else {
api api
@@ -127,12 +171,21 @@ function save() {
...plainEntity, ...plainEntity,
headers: plainHeaders, headers: plainHeaders,
}) })
.then((res) => { .then(async (res) => {
if (res.errorCode === 0) { if (res.errorCode === 0) {
await syncPluginCategories(entity.value.id, categoryIds);
dialogVisible.value = false; dialogVisible.value = false;
ElMessage.success($t('message.updateOkMessage')); ElMessage.success($t('message.updateOkMessage'));
emit('reload'); emit('reload');
} else {
ElMessage.error(res.message);
} }
})
.catch((error) => {
ElMessage.error(error?.message || $t('message.saveFailMessage'));
})
.finally(() => {
btnLoading.value = false;
}); });
} }
} }
@@ -141,6 +194,7 @@ function save() {
function closeDialog() { function closeDialog() {
saveForm.value?.resetFields(); saveForm.value?.resetFields();
isAdd.value = true; isAdd.value = true;
tempAddHeaders.value = [];
entity.value = {}; entity.value = {};
dialogVisible.value = false; dialogVisible.value = false;
} }
@@ -199,6 +253,22 @@ function removeHeader(index: number) {
:placeholder="$t('plugin.placeholder.description')" :placeholder="$t('plugin.placeholder.description')"
/> />
</ElFormItem> </ElFormItem>
<ElFormItem prop="categoryIds" :label="$t('plugin.category')">
<ElSelect
v-model="entity.categoryIds"
multiple
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="3"
>
<ElOption
v-for="item in categoryList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem prop="Headers" label="Headers"> <ElFormItem prop="Headers" label="Headers">
<div <div
class="headers-container-reduce flex flex-row gap-4" class="headers-container-reduce flex flex-row gap-4"

View File

@@ -1,5 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ActionButton } from '#/components/page/CardList.vue'; import type {
ActionButton,
CardPrimaryAction,
} from '#/components/page/CardList.vue';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@@ -19,13 +22,11 @@ import {
import { api } from '#/api/request'; import { api } from '#/api/request';
import defaultPluginIcon from '#/assets/ai/plugin/defaultPluginIcon.png'; import defaultPluginIcon from '#/assets/ai/plugin/defaultPluginIcon.png';
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue'; import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
import CategorizeIcon from '#/components/icons/CategorizeIcon.vue';
import PluginToolIcon from '#/components/icons/PluginToolIcon.vue'; import PluginToolIcon from '#/components/icons/PluginToolIcon.vue';
import CardPage from '#/components/page/CardList.vue'; import CardPage 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';
import AddPluginModal from '#/views/ai/plugin/AddPluginModal.vue'; import AddPluginModal from '#/views/ai/plugin/AddPluginModal.vue';
import CategoryPluginModal from '#/views/ai/plugin/CategoryPluginModal.vue';
const router = useRouter(); const router = useRouter();
@@ -52,23 +53,7 @@ function resolveNavTitle(item: PluginRecord) {
return (item.title as string) || (item.name as string) || ''; return (item.title as string) || (item.name as string) || '';
} }
// 操作按钮配置 function openPluginTools(item: PluginRecord) {
const actions: ActionButton[] = [
{
icon: Edit,
text: $t('button.edit'),
className: '',
permission: '/api/v1/plugin/save',
onClick(item) {
aiPluginModalRef.value.openDialog(item);
},
},
{
icon: PluginToolIcon,
text: $t('plugin.button.tools'),
className: '',
permission: '/api/v1/plugin/save',
onClick(item) {
router.push({ router.push({
path: '/ai/plugin/tools', path: '/ai/plugin/tools',
query: { query: {
@@ -77,22 +62,33 @@ const actions: ActionButton[] = [
navTitle: resolveNavTitle(item), navTitle: resolveNavTitle(item),
}, },
}); });
}, }
},
{ const primaryAction: CardPrimaryAction = {
icon: CategorizeIcon, icon: PluginToolIcon,
text: $t('plugin.button.categorize'), text: $t('plugin.button.tools'),
className: '',
permission: '/api/v1/plugin/save', permission: '/api/v1/plugin/save',
onClick(item) { onClick(item) {
categoryCategoryModal.value.openDialog(item); openPluginTools(item);
},
};
const actions: ActionButton[] = [
{
icon: Edit,
text: $t('button.edit'),
permission: '/api/v1/plugin/save',
placement: 'inline',
onClick(item) {
aiPluginModalRef.value.openDialog(item);
}, },
}, },
{ {
icon: Delete, icon: Delete,
text: $t('button.delete'), text: $t('button.delete'),
className: 'item-danger', tone: 'danger',
permission: '/api/v1/plugin/remove', permission: '/api/v1/plugin/remove',
placement: 'inline',
onClick(item) { onClick(item) {
handleDelete(item); handleDelete(item);
}, },
@@ -162,7 +158,6 @@ const handleDelete = (item: PluginRecord) => {
const pageDataRef = ref(); const pageDataRef = ref();
const aiPluginModalRef = ref(); const aiPluginModalRef = ref();
const categoryCategoryModal = ref();
const headerButtons = [ const headerButtons = [
{ {
key: 'add', key: 'add',
@@ -267,10 +262,11 @@ const handleClickCategory = (item: PluginCategory) => {
> >
<template #default="{ pageList }"> <template #default="{ pageList }">
<CardPage <CardPage
title-key="title" title-field="title"
avatar-key="icon" icon-field="icon"
description-key="description" desc-field="description"
:data="pageList" :data="pageList"
:primary-action="primaryAction"
:actions="actions" :actions="actions"
:default-icon="defaultPluginIcon" :default-icon="defaultPluginIcon"
/> />
@@ -279,7 +275,6 @@ const handleClickCategory = (item: PluginCategory) => {
</div> </div>
</div> </div>
<AddPluginModal ref="aiPluginModalRef" @reload="handleSearch" /> <AddPluginModal ref="aiPluginModalRef" @reload="handleSearch" />
<CategoryPluginModal ref="categoryCategoryModal" @reload="handleSearch" />
<EasyFlowFormModal <EasyFlowFormModal
:title="isEdit ? `${$t('button.edit')}` : `${$t('button.add')}`" :title="isEdit ? `${$t('button.edit')}` : `${$t('button.add')}`"
v-model:open="dialogVisible" v-model:open="dialogVisible"

View File

@@ -1,7 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type {FormInstance} from 'element-plus'; import type {FormInstance} from 'element-plus';
import {ElForm, ElFormItem, ElInput, ElInputNumber, ElMessage, ElMessageBox,} from 'element-plus';
import type { ActionButton } from '#/components/page/CardList.vue'; import type {ActionButton, CardPrimaryAction,} from '#/components/page/CardList.vue';
import CardList from '#/components/page/CardList.vue';
import {computed, markRaw, onMounted, ref} from 'vue'; import {computed, markRaw, onMounted, ref} from 'vue';
@@ -17,14 +19,6 @@ import {
Upload, Upload,
VideoPlay, VideoPlay,
} from '@element-plus/icons-vue'; } from '@element-plus/icons-vue';
import {
ElForm,
ElFormItem,
ElInput,
ElInputNumber,
ElMessage,
ElMessageBox,
} from 'element-plus';
import {tryit} from 'radash'; import {tryit} from 'radash';
import {api} from '#/api/request'; import {api} from '#/api/request';
@@ -32,7 +26,6 @@ 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 DesignIcon from '#/components/icons/DesignIcon.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';
import {$t} from '#/locales'; import {$t} from '#/locales';
@@ -54,30 +47,29 @@ interface FieldDefinition {
placeholder?: string; placeholder?: string;
} }
const primaryAction: CardPrimaryAction = {
icon: DesignIcon,
text: $t('button.design'),
permission: '/api/v1/workflow/save',
onClick: (row: any) => {
toDesignPage(row);
},
};
const actions: ActionButton[] = [ const actions: ActionButton[] = [
{ {
icon: Edit, icon: Edit,
text: $t('button.edit'), text: $t('button.edit'),
className: '',
permission: '/api/v1/workflow/save', permission: '/api/v1/workflow/save',
placement: 'inline',
onClick: (row: any) => { onClick: (row: any) => {
showDialog(row); showDialog(row);
}, },
}, },
{
icon: DesignIcon,
text: $t('button.design'),
className: '',
permission: '/api/v1/workflow/save',
onClick: (row: any) => {
toDesignPage(row);
},
},
{ {
icon: VideoPlay, icon: VideoPlay,
text: $t('button.run'), text: $t('button.run'),
className: '', placement: 'inline',
permission: '',
onClick: (row: any) => { onClick: (row: any) => {
router.push({ router.push({
name: 'RunPage', name: 'RunPage',
@@ -90,8 +82,8 @@ const actions: ActionButton[] = [
{ {
icon: Tickets, icon: Tickets,
text: $t('aiWorkflowExecRecord.moduleName'), text: $t('aiWorkflowExecRecord.moduleName'),
className: '',
permission: '/api/v1/workflow/save', permission: '/api/v1/workflow/save',
placement: 'menu',
onClick: (row: any) => { onClick: (row: any) => {
router.push({ router.push({
name: 'ExecRecord', name: 'ExecRecord',
@@ -104,8 +96,7 @@ const actions: ActionButton[] = [
{ {
icon: Download, icon: Download,
text: $t('button.export'), text: $t('button.export'),
className: '', placement: 'menu',
permission: '',
onClick: (row: any) => { onClick: (row: any) => {
exportJson(row); exportJson(row);
}, },
@@ -113,8 +104,7 @@ const actions: ActionButton[] = [
{ {
icon: CopyDocument, icon: CopyDocument,
text: $t('button.copy'), text: $t('button.copy'),
className: '', placement: 'menu',
permission: '',
onClick: (row: any) => { onClick: (row: any) => {
showDialog({ showDialog({
title: `${row.title}Copy`, title: `${row.title}Copy`,
@@ -125,8 +115,8 @@ const actions: ActionButton[] = [
{ {
icon: Delete, icon: Delete,
text: $t('button.delete'), text: $t('button.delete'),
className: 'item-danger', tone: 'danger',
permission: '', placement: 'inline',
onClick: (row: any) => { onClick: (row: any) => {
remove(row); remove(row);
}, },
@@ -402,6 +392,7 @@ function handleHeaderButtonClick(data: any) {
<CardList <CardList
:default-icon="workflowIcon" :default-icon="workflowIcon"
:data="pageList" :data="pageList"
:primary-action="primaryAction"
:actions="actions" :actions="actions"
/> />
</template> </template>