fix: 修复管理端前端 lint 与构建问题

- 收敛 easyflow-ui-admin 的 lint、格式和类型问题

- 修正 demo 页面与管理端前端构建失败点

- 验证 pnpm lint 与 pnpm build 均已通过
This commit is contained in:
2026-04-05 21:39:13 +08:00
parent bb72e19c84
commit 7e7c236c2a
240 changed files with 5151 additions and 4701 deletions

View File

@@ -144,143 +144,8 @@ const filteredActions = computed(() => {
</template>
<style scoped>
.card-list-container {
width: 100%;
height: 100%;
}
.card-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
:deep(.el-card__body) {
padding: 20px 0 0 0;
}
.card-item {
transition: all 0.3s ease;
border-radius: 8px;
flex-shrink: 0;
height: 165px;
min-width: 0;
}
:deep(.delete-dropdown-item) {
color: #ff4d4f !important;
}
:deep(.delete-dropdown-item:hover) {
color: #ff7875 !important;
}
:deep(.delete-dropdown-item .el-icon) {
color: inherit !important;
}
.card-item:hover {
transform: translateY(-5px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.card-content {
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--el-bg-color);
border-radius: 8px;
}
.card-header {
display: flex;
align-items: flex-start;
gap: 15px;
margin-bottom: 15px;
flex: 1;
padding: 0 20px 0 20px;
}
.card-info {
flex: 1;
min-width: 0;
max-width: 100%;
}
.card-title {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
line-height: 1.4;
}
.card-desc {
margin: 0;
width: 100%;
font-size: 14px;
color: #606266;
line-height: 1.5;
height: 42px;
min-height: 42px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.card-actions {
display: flex;
align-items: center;
height: 48px;
background:
linear-gradient(
180deg,
var(--el-color-primary-light-9),
var(--el-bg-color)
),
var(--el-bg-color);
}
.action-btn {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
min-width: 0;
color: #acb7c6;
position: relative;
&:hover {
color: var(--el-color-primary);
}
}
.more-btn {
width: 100%;
justify-content: center;
}
.divider {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 1px;
height: 20px;
background-color: #e0e0e0;
}
/* 确保按钮内容不换行 */
.action-btn .el-button {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
justify-content: center;
}
.empty-state {
padding: 40px 0;
text-align: center;
}
/* 响应式设计 */
/* 超大屏幕 */
@media (min-width: 1920px) {
.card-list {
@@ -305,8 +170,8 @@ const filteredActions = computed(() => {
}
.card-header {
padding: 0 16px 0 16px;
gap: 12px;
padding: 0 16px;
}
.card-title {
@@ -314,9 +179,9 @@ const filteredActions = computed(() => {
}
.card-desc {
font-size: 13px;
height: 36px;
min-height: 36px;
font-size: 13px;
}
}
@@ -326,21 +191,22 @@ const filteredActions = computed(() => {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
}
.card-header {
padding: 0 12px 0 12px;
gap: 10px;
padding: 0 12px;
}
.card-title {
font-size: 14px;
margin-bottom: 6px;
font-size: 14px;
}
.card-desc {
font-size: 12px;
height: 32px;
min-height: 32px;
-webkit-line-clamp: 2;
font-size: 12px;
}
.card-actions {
@@ -351,4 +217,147 @@ const filteredActions = computed(() => {
font-size: 11px;
}
}
.card-list-container {
width: 100%;
height: 100%;
}
.card-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
:deep(.el-card__body) {
padding: 20px 0 0;
}
.card-item {
flex-shrink: 0;
min-width: 0;
height: 165px;
border-radius: 8px;
transition: all 0.3s ease;
}
:deep(.delete-dropdown-item) {
color: #ff4d4f !important;
}
:deep(.delete-dropdown-item:hover) {
color: #ff7875 !important;
}
:deep(.delete-dropdown-item .el-icon) {
color: inherit !important;
}
.card-item:hover {
box-shadow: 0 4px 20px rgb(0 0 0 / 10%);
transform: translateY(-5px);
}
.card-content {
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--el-bg-color);
border-radius: 8px;
}
.card-header {
display: flex;
flex: 1;
gap: 15px;
align-items: flex-start;
padding: 0 20px;
margin-bottom: 15px;
}
.card-info {
flex: 1;
min-width: 0;
max-width: 100%;
}
.card-title {
margin: 0 0 8px;
font-size: 16px;
font-weight: 600;
line-height: 1.4;
color: var(--el-text-color-primary);
}
.card-desc {
display: -webkit-box;
width: 100%;
height: 42px;
min-height: 42px;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
font-size: 14px;
line-height: 1.5;
color: #606266;
-webkit-box-orient: vertical;
}
.card-actions {
display: flex;
align-items: center;
height: 48px;
background:
linear-gradient(
180deg,
var(--el-color-primary-light-9),
var(--el-bg-color)
),
var(--el-bg-color);
}
.action-btn {
position: relative;
display: flex;
flex: 1;
align-items: center;
justify-content: center;
min-width: 0;
color: #acb7c6;
&:hover {
color: var(--el-color-primary);
}
}
.more-btn {
justify-content: center;
width: 100%;
}
.divider {
position: absolute;
top: 50%;
right: 0;
width: 1px;
height: 20px;
background-color: #e0e0e0;
transform: translateY(-50%);
}
/* 确保按钮内容不换行 */
.action-btn .el-button {
justify-content: center;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.empty-state {
padding: 40px 0;
text-align: center;
}
</style>

View File

@@ -283,28 +283,28 @@ const handleDeleteClick = (event: any, item: any) => {
<style scoped>
.crud-category-item:hover {
background-color: var(--el-color-primary-light-9);
cursor: pointer;
background-color: var(--el-color-primary-light-9);
}
.crud-category-item.selected {
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
.no-data {
text-align: center;
padding: 40px 0;
text-align: center;
}
.crud-category-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
height: 40px;
border-radius: 8px;
padding: 8px 12px;
font-size: 14px;
border-radius: 8px;
}
.crud-category-item :deep(.el-icon) {
@@ -316,8 +316,8 @@ const handleDeleteClick = (event: any, item: any) => {
align-items: center;
justify-content: center;
padding: 4px;
border-radius: 4px;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.3s;
}

View File

@@ -10,7 +10,6 @@ const props = defineProps({
categories: {
type: Array,
default: () => [],
required: true,
},
valueKey: {
type: String,
@@ -125,6 +124,14 @@ const panelWidth = computed(() => {
}
});
const categoryIconStyle = computed(() => ({
'--category-icon-color': props.iconColor,
'--category-icon-size':
typeof props.iconSize === 'number'
? `${props.iconSize}px`
: `${props.iconSize}`,
}));
// 切换面板收缩状态
const togglePanel = () => {
isCollapsed.value = !isCollapsed.value;
@@ -177,18 +184,24 @@ onMounted(() => {
@click="handleCategoryClick(category)"
>
<!-- 图标 -->
<div v-if="category[iconKey]" class="category-icon">
<div
v-if="category[iconKey]"
class="category-icon"
:style="categoryIconStyle"
>
<!-- 1. 组件类型图标Element Plus / 自定义 SVG 组件 -->
<ElIcon v-if="isComponent(category[iconKey])">
<component :is="category[iconKey]" />
</ElIcon>
<!-- 2. SVG 字符串支持 v-html img 两种渲染方式 -->
<template v-else-if="isSvgString(category[iconKey])">
<!-- eslint-disable vue/no-v-html -->
<div
v-if="!useImgForSvg"
v-html="category[iconKey]"
class="custom-svg"
></div>
<!-- eslint-enable vue/no-v-html -->
<img
v-else
:src="svgToDataUrl(category[iconKey])"
@@ -221,14 +234,14 @@ onMounted(() => {
<style scoped>
.category-panel {
position: relative; /* 相对定位,用于按钮绝对定位 */
overflow: hidden;
height: 100%;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); /* 平滑宽度过渡 */
box-sizing: border-box;
background: #ffffff;
border-radius: 8px;
border: 1px solid #f0f0f0;
height: 100%;
padding: 20px;
overflow: hidden;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); /* 平滑宽度过渡 */
}
/* 右上角收缩/展开按钮 */
@@ -242,18 +255,19 @@ onMounted(() => {
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
background-color: var(--el-color-white);
border: 1px solid #e5e7eb;
cursor: pointer;
transition: all 0.2s;
border-radius: 50%;
/* 按钮不随面板收缩移动 */
transform: translateX(0);
transition: all 0.2s;
}
.toggle-panel-btn:hover {
background-color: #f3f4f6;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px rgb(0 0 0 / 10%);
}
.toggle-panel-btn .el-icon {
@@ -264,15 +278,12 @@ onMounted(() => {
/* 分类列表容器 */
.category-list {
overflow: hidden;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 1px;
}
.category-item {
height: 100%;
overflow: hidden;
}
.category-item:last-child {
@@ -281,15 +292,15 @@ onMounted(() => {
.category-item-content {
display: flex;
gap: 12px;
align-items: center;
padding: 6px 0 6px 14px;
font-size: 14px;
font-weight: 500;
color: #333;
cursor: pointer;
transition: background-color 0.2s;
gap: 12px;
padding: 6px 0 6px 14px;
border-radius: 4px;
transition: background-color 0.2s;
}
.category-item-content:hover {
@@ -297,49 +308,36 @@ onMounted(() => {
}
.category-icon {
width: v-bind(iconSize);
height: v-bind(iconSize);
color: v-bind(iconColor);
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--category-icon-size);
height: var(--category-icon-size);
vertical-align: middle;
color: var(--category-icon-color);
}
.category-icon .el-icon {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.category-item-content {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 500;
color: #333;
cursor: pointer;
transition: background-color 0.2s;
gap: 12px;
width: 100%;
height: 100%;
}
.category-name {
overflow: hidden;
text-overflow: ellipsis;
font-family: PingFangSC, 'PingFang SC';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
text-align: left;
white-space: nowrap;
transition:
opacity 0.2s,
transform 0.2s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-family:
PingFangSC,
PingFang SC;
font-weight: 400;
font-size: 14px;
line-height: 20px;
text-align: left;
font-style: normal;
}
/* 收缩状态样式 */
@@ -360,11 +358,11 @@ onMounted(() => {
/* 新增:选中态样式 */
.category-item-content.selected {
font-weight: 600;
background: rgba(0, 102, 255, 0.06);
color: #0066ff;
color: #06f;
background: rgb(0 102 255 / 6%);
}
.category-item-content.selected:hover {
background: rgba(0, 102, 255, 0.06);
background: rgb(0 102 255 / 6%);
}
</style>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ChatThinkingBlock } from '@easyflow/common-ui';
import ElXMarkdown from 'vue-element-plus-x/es/XMarkdown/index.js';
import { ChatThinkingBlock } from '@easyflow/common-ui';
import { IconifyIcon } from '@easyflow/icons';
import { CircleCheck, Close } from '@element-plus/icons-vue';
@@ -22,7 +22,7 @@ interface ChatHistoryDetailDrawerProps {
session?: any;
messages?: any[];
hasMore?: boolean;
onLoadMore?: (() => void | Promise<void>) | undefined;
onLoadMore?: (() => Promise<void> | void) | undefined;
}
const props = withDefaults(defineProps<ChatHistoryDetailDrawerProps>(), {
@@ -128,12 +128,7 @@ async function handleLoadMore() {
<div class="chat-history-detail__stream">
<div class="chat-history-detail__stream-toolbar">
<ElButton
v-if="hasMore"
text
type="primary"
@click="handleLoadMore"
>
<ElButton v-if="hasMore" text type="primary" @click="handleLoadMore">
加载更早消息
</ElButton>
</div>
@@ -233,48 +228,48 @@ async function handleLoadMore() {
<style scoped>
.chat-history-detail {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
flex-direction: column;
background:
radial-gradient(
circle at top right,
hsl(var(--nav-ambient) / 0.1),
hsl(var(--nav-ambient) / 10%),
transparent 26%
),
linear-gradient(
180deg,
hsl(var(--glass-border) / 0.18) 0%,
hsl(var(--glass-tint) / 0.34) 10%,
hsl(var(--surface-panel) / 0.88) 28%,
hsl(var(--surface-panel) / 0.96) 100%
hsl(var(--glass-border) / 18%) 0%,
hsl(var(--glass-tint) / 34%) 10%,
hsl(var(--surface-panel) / 88%) 28%,
hsl(var(--surface-panel) / 96%) 100%
);
}
.chat-history-detail__summary {
display: flex;
gap: 16px;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding: 20px 24px 12px;
border-bottom: 1px solid hsl(var(--divider-faint) / 0.16);
border-bottom: 1px solid hsl(var(--divider-faint) / 16%);
}
.chat-history-detail__summary-main {
min-width: 0;
flex: 1;
min-width: 0;
}
.chat-history-detail__title-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
align-items: center;
}
.chat-history-detail__title {
margin: 0;
min-width: 0;
margin: 0;
font-size: 18px;
font-weight: 600;
color: hsl(var(--text-strong));
@@ -285,19 +280,19 @@ async function handleLoadMore() {
align-items: center;
min-height: 26px;
padding: 0 10px;
border: 1px solid hsl(var(--glass-border) / 0.38);
border-radius: 999px;
background: hsl(var(--glass-tint) / 0.54);
font-size: 12px;
font-weight: 600;
color: hsl(var(--nav-item-active-foreground));
background: hsl(var(--glass-tint) / 54%);
border: 1px solid hsl(var(--glass-border) / 38%);
border-radius: 999px;
}
.chat-history-detail__meta-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
align-items: center;
margin-top: 10px;
}
@@ -316,28 +311,28 @@ async function handleLoadMore() {
.chat-history-detail__meta-inline {
display: inline-flex;
min-width: 0;
align-items: center;
gap: 6px;
align-items: center;
min-width: 0;
}
.chat-history-detail__meta-divider {
display: inline-flex;
height: 12px;
width: 1px;
background: hsl(var(--divider-faint) / 0.42);
height: 12px;
background: hsl(var(--divider-faint) / 42%);
}
.chat-history-detail__close {
display: inline-flex;
height: 36px;
width: 36px;
align-items: center;
justify-content: center;
border: 1px solid hsl(var(--divider-faint) / 0.24);
border-radius: 999px;
background: hsl(var(--glass-tint) / 0.46);
width: 36px;
height: 36px;
color: hsl(var(--text-muted));
background: hsl(var(--glass-tint) / 46%);
border: 1px solid hsl(var(--divider-faint) / 24%);
border-radius: 999px;
transition:
transform 0.18s ease,
background-color 0.18s ease,
@@ -346,15 +341,15 @@ async function handleLoadMore() {
.chat-history-detail__close:hover {
color: hsl(var(--text-strong));
background: hsl(var(--surface-contrast-soft) / 0.92);
background: hsl(var(--surface-contrast-soft) / 92%);
transform: translateY(-1px);
}
.chat-history-detail__stream {
display: flex;
min-height: 0;
flex: 1;
flex-direction: column;
min-height: 0;
padding: 14px 24px 20px;
}
@@ -365,8 +360,8 @@ async function handleLoadMore() {
}
.chat-history-detail__scrollbar {
min-height: 0;
flex: 1;
min-height: 0;
}
.chat-history-detail__message-list {
@@ -391,9 +386,9 @@ async function handleLoadMore() {
.chat-history-detail__message-meta {
display: inline-flex;
max-width: 88%;
align-items: center;
gap: 10px;
align-items: center;
max-width: 88%;
font-size: 11px;
color: hsl(var(--text-muted));
}
@@ -409,30 +404,30 @@ async function handleLoadMore() {
.chat-history-detail__message-bubble {
width: min(88%, 760px);
border: 1px solid hsl(var(--divider-faint) / 0.34);
border-radius: 22px;
padding: 14px 16px;
font-size: 14px;
line-height: 1.72;
border: 1px solid hsl(var(--divider-faint) / 34%);
border-radius: 22px;
backdrop-filter: blur(10px);
}
.chat-history-detail__message-bubble.is-assistant {
background: hsl(var(--glass-tint) / 0.88);
background: hsl(var(--glass-tint) / 88%);
box-shadow:
inset 0 1px 0 hsl(var(--glass-border) / 0.48),
0 12px 26px -24px hsl(var(--foreground) / 0.18);
inset 0 1px 0 hsl(var(--glass-border) / 48%),
0 12px 26px -24px hsl(var(--foreground) / 18%);
}
.chat-history-detail__message-bubble.is-user {
background: linear-gradient(
180deg,
hsl(var(--primary) / 0.14) 0%,
hsl(var(--surface-panel) / 0.96) 100%
hsl(var(--primary) / 14%) 0%,
hsl(var(--surface-panel) / 96%) 100%
);
box-shadow:
inset 0 1px 0 hsl(var(--glass-border) / 0.42),
0 16px 30px -26px hsl(var(--primary) / 0.24);
inset 0 1px 0 hsl(var(--glass-border) / 42%),
0 16px 30px -26px hsl(var(--primary) / 24%);
}
.chat-history-detail__message-chains {
@@ -448,23 +443,23 @@ async function handleLoadMore() {
.chat-history-detail__tool-panel {
overflow: hidden;
border: 1px solid hsl(var(--divider-faint) / 0.24);
background: hsl(var(--surface-panel) / 68%);
border: 1px solid hsl(var(--divider-faint) / 24%);
border-radius: 14px;
background: hsl(var(--surface-panel) / 0.68);
box-shadow: inset 0 1px 0 hsl(var(--glass-border) / 0.18);
box-shadow: inset 0 1px 0 hsl(var(--glass-border) / 18%);
}
.chat-history-detail__tool-title {
display: flex;
min-width: 0;
align-items: center;
gap: 8px;
align-items: center;
min-width: 0;
padding-left: 6px;
}
.chat-history-detail__tool-name {
min-width: 0;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -472,13 +467,13 @@ async function handleLoadMore() {
.chat-history-detail__tool-status {
display: inline-flex;
align-items: center;
gap: 6px;
align-items: center;
padding: 4px 8px;
border-radius: 999px;
background: hsl(var(--surface-contrast-soft) / 0.92);
font-size: 12px;
color: hsl(var(--text-muted));
background: hsl(var(--surface-contrast-soft) / 92%);
border-radius: 999px;
}
.chat-history-detail__markdown {
@@ -488,9 +483,9 @@ async function handleLoadMore() {
}
.chat-history-detail__markdown :deep(.markdown-body) {
background: transparent;
font-size: inherit;
line-height: inherit;
background: transparent;
}
.chat-history-detail__markdown :deep(.markdown-body > :first-child) {
@@ -508,8 +503,8 @@ async function handleLoadMore() {
.chat-history-detail__markdown :deep(pre) {
max-width: 100%;
overflow: auto;
margin-top: 0;
overflow: auto;
}
.chat-history-detail__tool-panel :deep(.el-collapse-item__wrap) {
@@ -520,7 +515,7 @@ async function handleLoadMore() {
min-height: 44px;
padding-right: 14px;
background: transparent;
border-bottom-color: hsl(var(--divider-faint) / 0.16);
border-bottom-color: hsl(var(--divider-faint) / 16%);
}
.chat-history-detail__message-bubble :deep(.el-collapse-item__content) {
@@ -550,8 +545,8 @@ async function handleLoadMore() {
.chat-history-detail__message-bubble,
.chat-history-detail__message-meta {
max-width: 100%;
width: 100%;
max-width: 100%;
}
}
</style>

View File

@@ -5,18 +5,23 @@ import type {
} from 'vue-element-plus-x/types/BubbleList';
import type { TypewriterInstance } from 'vue-element-plus-x/types/Typewriter';
import {
ChatThinkingBlock,
type ChatThinkingBlockStatus,
} from '@easyflow/common-ui';
import type { ChatThinkingBlockStatus } from '@easyflow/common-ui';
import type { BotInfo, ChatMessage } from '@easyflow/types';
import { nextTick, onBeforeUnmount, onMounted, ref, watch, watchEffect } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import {
nextTick,
onBeforeUnmount,
onMounted,
ref,
watch,
watchEffect,
} from 'vue';
import ElBubbleList from 'vue-element-plus-x/es/BubbleList/index.js';
import ElSender from 'vue-element-plus-x/es/Sender/index.js';
import ElXMarkdown from 'vue-element-plus-x/es/XMarkdown/index.js';
import { useRoute, useRouter } from 'vue-router';
import { ChatThinkingBlock } from '@easyflow/common-ui';
import { IconifyIcon } from '@easyflow/icons';
import { $t } from '@easyflow/locales';
import { useBotStore } from '@easyflow/stores';
@@ -51,8 +56,8 @@ import SendingIcon from '../icons/SendingIcon.vue';
type Think = {
reasoning_content?: string;
thinkingStatus?: ChatThinkingBlockStatus;
thinkingExpanded?: boolean;
thinkingStatus?: ChatThinkingBlockStatus;
};
type Tool = {
@@ -168,7 +173,8 @@ const updateBackToBottomButtonVisible = () => {
showBackToBottomButton.value = false;
return;
}
const { scrollTop, scrollHeight, clientHeight } = bubbleListScrollElement.value;
const { scrollTop, scrollHeight, clientHeight } =
bubbleListScrollElement.value;
showBackToBottomButton.value =
scrollHeight - (scrollTop + clientHeight) > BACK_TO_BOTTOM_THRESHOLD;
};
@@ -571,7 +577,6 @@ onBeforeUnmount(() => {
</ElCollapse>
</template>
</template>
</div>
</template>
<!-- 自定义头像 -->
@@ -725,7 +730,7 @@ onBeforeUnmount(() => {
background: var(--el-bg-color-overlay);
border: 1px solid var(--el-border-color-light);
border-radius: 999px;
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.08);
box-shadow: 0 6px 10px rgb(0 0 0 / 8%);
transition: all 0.2s ease;
}
@@ -775,10 +780,10 @@ onBeforeUnmount(() => {
}
:deep(.chat-tool-panel.el-collapse) {
border: 1px solid hsl(var(--divider-faint) / 0.26);
background: hsl(var(--surface-panel) / 70%);
border: 1px solid hsl(var(--divider-faint) / 26%);
border-radius: 14px;
background: hsl(var(--surface-panel) / 0.7);
box-shadow: inset 0 1px 0 hsl(var(--glass-border) / 0.2);
box-shadow: inset 0 1px 0 hsl(var(--glass-border) / 20%);
}
:deep(.chat-tool-panel .el-collapse-item) {
@@ -794,7 +799,7 @@ onBeforeUnmount(() => {
min-height: 44px;
padding-right: 14px;
background: transparent;
border-bottom-color: hsl(var(--divider-faint) / 0.16);
border-bottom-color: hsl(var(--divider-faint) / 16%);
}
:deep(.chat-tool-panel .el-collapse-item__content) {

View File

@@ -122,162 +122,6 @@ const collapseAll = () => {
</template>
<style scoped>
.accordion-container {
max-width: 100%;
margin: 0 auto;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.title {
text-align: center;
color: var(--el-text-color-secondary);
margin-bottom: 8px;
font-size: 2rem;
font-weight: 600;
}
.subtitle {
text-align: center;
color: var(--el-text-color-secondary);
margin-bottom: 30px;
font-size: 1.1rem;
}
/* 控制面板样式 */
.controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding: 20px;
background: var(--el-bg-color);
border-radius: 8px;
border: 1px solid #e9ecef;
}
.control-group {
display: flex;
align-items: center;
gap: 15px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--el-text-color-secondary);
cursor: pointer;
}
.checkbox {
width: 16px;
height: 16px;
}
.control-btn {
padding: 8px 16px;
border: 1px solid #3498db;
background: var(--el-bg-color);
color: var(--el-text-color-secondary);
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.control-btn:hover {
background: #3498db;
background: var(--el-color-primary-light-9);
}
/* 折叠面板列表 */
.accordion-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.accordion-item {
border: 1px solid #e1e5e9;
border-radius: 8px;
overflow: hidden;
background: var(--el-bg-color);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.accordion-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.accordion-item--active {
border-color: #3498db;
}
/* 面板头部 */
.accordion-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: var(--el-bg-color);
cursor: pointer;
transition: background-color 0.3s ease;
user-select: none;
}
.accordion-header:hover {
background: var(--el-color-primary-light-9);
}
.accordion-title {
margin: 0;
font-size: 1.1rem;
font-weight: 500;
color: var(--el-text-color-secondary);
padding-left: 12px;
}
.accordion-icon {
transition: transform 0.3s ease;
color: #7f8c8d;
font-size: 12px;
}
.accordion-icon--rotated {
transform: rotate(180deg);
color: #3498db;
}
.accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.4s ease;
background: var(--el-bg-color);
}
.accordion-content--open {
max-height: 200px;
}
.accordion-content-inner {
padding: 20px;
border-top: 1px solid #e1e5e9;
}
.accordion-content-inner p {
margin: 0;
line-height: 1.6;
color: var(--el-text-color-secondary);
font-size: 14px;
}
.column-header-container {
display: flex;
justify-content: space-between;
align-items: center;
}
/* 响应式设计 */
@media (max-width: 768px) {
.accordion-container {
@@ -306,4 +150,162 @@ const collapseAll = () => {
font-size: 1rem;
}
}
.accordion-container {
max-width: 100%;
padding: 20px;
margin: 0 auto;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.title {
margin-bottom: 8px;
font-size: 2rem;
font-weight: 600;
color: var(--el-text-color-secondary);
text-align: center;
}
.subtitle {
margin-bottom: 30px;
font-size: 1.1rem;
color: var(--el-text-color-secondary);
text-align: center;
}
/* 控制面板样式 */
.controls {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
margin-bottom: 30px;
background: var(--el-bg-color);
border: 1px solid #e9ecef;
border-radius: 8px;
}
.control-group {
display: flex;
gap: 15px;
align-items: center;
}
.checkbox-label {
display: flex;
gap: 8px;
align-items: center;
font-size: 14px;
color: var(--el-text-color-secondary);
cursor: pointer;
}
.checkbox {
width: 16px;
height: 16px;
}
.control-btn {
padding: 8px 16px;
font-size: 14px;
color: var(--el-text-color-secondary);
cursor: pointer;
background: var(--el-bg-color);
border: 1px solid #3498db;
border-radius: 4px;
transition: all 0.3s ease;
}
.control-btn:hover {
background: #3498db;
background: var(--el-color-primary-light-9);
}
/* 折叠面板列表 */
.accordion-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.accordion-item {
overflow: hidden;
background: var(--el-bg-color);
border: 1px solid #e1e5e9;
border-radius: 8px;
box-shadow: 0 2px 8px rgb(0 0 0 / 5%);
transition: all 0.3s ease;
}
.accordion-item:hover {
box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
}
.accordion-item--active {
border-color: #3498db;
}
/* 面板头部 */
.accordion-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
cursor: pointer;
user-select: none;
background: var(--el-bg-color);
transition: background-color 0.3s ease;
}
.accordion-header:hover {
background: var(--el-color-primary-light-9);
}
.accordion-title {
padding-left: 12px;
margin: 0;
font-size: 1.1rem;
font-weight: 500;
color: var(--el-text-color-secondary);
}
.accordion-icon {
font-size: 12px;
color: #7f8c8d;
transition: transform 0.3s ease;
}
.accordion-icon--rotated {
color: #3498db;
transform: rotate(180deg);
}
.accordion-content {
max-height: 0;
overflow: hidden;
background: var(--el-bg-color);
transition: max-height 0.4s ease;
}
.accordion-content--open {
max-height: 200px;
}
.accordion-content-inner {
padding: 20px;
border-top: 1px solid #e1e5e9;
}
.accordion-content-inner p {
margin: 0;
font-size: 14px;
line-height: 1.6;
color: var(--el-text-color-secondary);
}
.column-header-container {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>

View File

@@ -171,9 +171,9 @@ const handleDropdownClick = (button) => {
<style scoped>
.custom-header {
display: flex;
gap: 16px;
align-items: center;
justify-content: space-between;
gap: 16px;
width: 100%;
}
@@ -192,8 +192,8 @@ const handleDropdownClick = (button) => {
display: flex;
flex: 1;
flex-wrap: wrap;
align-items: center;
gap: 12px;
align-items: center;
}
.search-middle {
@@ -208,20 +208,20 @@ const handleDropdownClick = (button) => {
.search-input :deep(.el-input__wrapper) {
min-height: 40px;
background: hsl(var(--surface-contrast-soft) / 0.92);
background: hsl(var(--surface-contrast-soft) / 92%);
border-radius: 14px;
box-shadow:
inset 0 1px 0 hsl(var(--glass-border) / 0.44),
0 10px 24px -24px hsl(var(--foreground) / 0.24);
inset 0 1px 0 hsl(var(--glass-border) / 44%),
0 10px 24px -24px hsl(var(--foreground) / 24%);
}
.search-input :deep(.el-input__wrapper:hover),
.search-input :deep(.el-input__wrapper.is-focus) {
background: hsl(var(--surface-subtle) / 0.98);
background: hsl(var(--surface-subtle) / 98%);
box-shadow:
inset 0 1px 0 hsl(var(--glass-border) / 0.56),
0 0 0 3px hsl(var(--primary) / 0.08),
0 12px 24px -24px hsl(var(--foreground) / 0.24);
inset 0 1px 0 hsl(var(--glass-border) / 56%),
0 0 0 3px hsl(var(--primary) / 8%),
0 12px 24px -24px hsl(var(--foreground) / 24%);
}
.search-prefix {
@@ -231,8 +231,8 @@ const handleDropdownClick = (button) => {
.header-right {
flex-shrink: 0;
flex-wrap: wrap;
justify-content: flex-end;
gap: 12px;
justify-content: flex-end;
}
.header-right :deep(.el-button),
@@ -241,24 +241,24 @@ const handleDropdownClick = (button) => {
height: 32px;
min-height: 32px;
padding-inline: 18px;
border-radius: 10px;
border-color: transparent;
border-radius: 10px;
box-shadow: none;
}
.header-right :deep(.el-button:not(.el-button--primary)),
.search-group :deep(.el-button:not(.el-button--primary)) {
color: hsl(var(--text-strong));
background: hsl(var(--surface-contrast-soft) / 0.86);
background: hsl(var(--surface-contrast-soft) / 86%);
border-color: transparent;
box-shadow:
inset 0 1px 0 hsl(var(--glass-border) / 0.34),
0 10px 24px -24px hsl(var(--foreground) / 0.2);
inset 0 1px 0 hsl(var(--glass-border) / 34%),
0 10px 24px -24px hsl(var(--foreground) / 20%);
}
.header-right :deep(.el-button--primary),
.search-group :deep(.el-button--primary) {
box-shadow: 0 16px 28px -22px hsl(var(--primary) / 0.48);
box-shadow: 0 16px 28px -22px hsl(var(--primary) / 48%);
}
.dropdown-label {
@@ -268,8 +268,8 @@ const handleDropdownClick = (button) => {
@media (max-width: 768px) {
.custom-header {
flex-direction: column;
align-items: stretch;
gap: 12px;
align-items: stretch;
}
.header-right {

View File

@@ -6,9 +6,10 @@ import { preferences } from '@easyflow/preferences';
import { ElEmpty } from 'element-plus';
import { JsonViewer } from 'vue3-json-viewer';
import 'vue3-json-viewer/dist/vue3-json-viewer.css';
import { getEmptyStateImageUrl } from '#/utils/assets';
import 'vue3-json-viewer/dist/vue3-json-viewer.css';
defineProps({
value: {
required: true,

View File

@@ -84,14 +84,25 @@ const resolvedPrimaryAction = computed(() => {
return props.primaryAction;
});
function resolveActionPlacement(
action: ActionButton,
index: number,
): ActionPlacement {
if (action.placement) {
return action.placement;
}
if (resolvedPrimaryAction.value) {
return 'menu';
}
return index < 3 ? 'inline' : 'menu';
}
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'),
placement: resolveActionPlacement(action, index),
tone:
action.tone ||
(action.className?.includes('danger') ? 'danger' : 'default'),
@@ -111,8 +122,8 @@ const menuActions = computed(() => {
const showFooter = computed(() => {
return Boolean(
resolvedPrimaryAction.value ||
inlineActions.value.length ||
menuActions.value.length,
inlineActions.value.length > 0 ||
menuActions.value.length > 0,
);
});
@@ -132,10 +143,8 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
v-for="(item, index) in props.data"
:key="item.id ?? index"
shadow="never"
:class="[
'card-item',
{ 'card-item--interactive': resolvedPrimaryAction },
]"
class="card-item"
:class="[{ 'card-item--interactive': resolvedPrimaryAction }]"
:role="resolvedPrimaryAction ? 'button' : undefined"
:tabindex="resolvedPrimaryAction ? 0 : undefined"
@click="handlePrimaryAction(item)"
@@ -201,7 +210,7 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
</div>
<div
v-if="inlineActions.length || menuActions.length"
v-if="inlineActions.length > 0 || menuActions.length > 0"
class="card-actions"
@click.stop
>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import type {CSSProperties} from 'vue';
import {computed, useSlots} from 'vue';
import type { CSSProperties } from 'vue';
import { computed, useSlots } from 'vue';
interface Props {
contentPadding?: number | string;
@@ -67,8 +68,8 @@ const contentStyle = computed((): CSSProperties => {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
gap: 16px;
min-height: 0;
}
.list-page-shell.is-dense {
@@ -78,9 +79,9 @@ const contentStyle = computed((): CSSProperties => {
.list-page-shell__toolbar {
position: relative;
display: flex;
gap: 16px;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding: 8px 6px 2px;
background: transparent;
border: none;
@@ -92,8 +93,8 @@ const contentStyle = computed((): CSSProperties => {
top: 12px;
z-index: 5;
padding: 10px 10px 6px;
background: hsl(var(--glass-tint) / 0.7);
border: 1px solid hsl(var(--glass-border) / 0.44);
background: hsl(var(--glass-tint) / 70%);
border: 1px solid hsl(var(--glass-border) / 44%);
border-radius: 20px;
box-shadow: var(--shadow-toolbar);
backdrop-filter: blur(var(--glass-blur)) saturate(155%);
@@ -110,40 +111,41 @@ const contentStyle = computed((): CSSProperties => {
.list-page-shell__content {
--list-page-shell-content-padding: 0px;
position: relative;
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
position: relative;
overflow: hidden;
padding: var(--list-page-shell-content-padding);
overflow: hidden;
background: linear-gradient(
180deg,
hsl(var(--glass-border) / 0.84) 0%,
hsl(var(--surface-panel) / 0.94) 28%,
hsl(var(--surface-panel) / 0.97) 100%
hsl(var(--glass-border) / 84%) 0%,
hsl(var(--surface-panel) / 94%) 28%,
hsl(var(--surface-panel) / 97%) 100%
);
border: 1px solid hsl(var(--glass-border) / 0.58);
border: 1px solid hsl(var(--glass-border) / 58%);
border-radius: 24px;
box-shadow:
inset 0 1px 0 hsl(var(--glass-border) / 0.72),
0 24px 54px -42px hsl(var(--primary) / 0.22),
0 18px 42px -34px hsl(var(--foreground) / 0.1);
inset 0 1px 0 hsl(var(--glass-border) / 72%),
0 24px 54px -42px hsl(var(--primary) / 22%),
0 18px 42px -34px hsl(var(--foreground) / 10%);
backdrop-filter: blur(calc(var(--glass-blur) * 0.7)) saturate(145%);
}
.list-page-shell.is-subtle .list-page-shell__content {
background: linear-gradient(
180deg,
hsl(var(--glass-tint) / 0.84) 0%,
hsl(var(--surface-contrast-soft) / 0.94) 100%
hsl(var(--glass-tint) / 84%) 0%,
hsl(var(--surface-contrast-soft) / 94%) 100%
);
box-shadow: none;
}
.list-page-shell.is-plain .list-page-shell__content {
background: hsl(var(--surface-panel) / 0.96);
border: 1px solid hsl(var(--divider-faint) / 0.58);
background: hsl(var(--surface-panel) / 96%);
border: 1px solid hsl(var(--divider-faint) / 58%);
border-radius: 20px;
box-shadow: none;
backdrop-filter: none;
@@ -162,14 +164,10 @@ const contentStyle = computed((): CSSProperties => {
background:
radial-gradient(
ellipse 84% 72% at 10% 0%,
hsl(var(--nav-ambient) / 0.18) 0%,
hsl(var(--nav-ambient) / 18%) 0%,
transparent 68%
),
linear-gradient(
180deg,
hsl(var(--nav-flow-trace) / 0.08) 0%,
transparent 78%
);
linear-gradient(180deg, hsl(var(--nav-flow-trace) / 8%) 0%, transparent 78%);
}
.list-page-shell__content > * {
@@ -195,6 +193,7 @@ const contentStyle = computed((): CSSProperties => {
--el-table-border-color: hsl(var(--table-row-border));
--el-table-current-row-bg-color: hsl(var(--table-row-hover));
--el-fill-color-blank: transparent;
background: transparent;
}
@@ -216,8 +215,8 @@ const contentStyle = computed((): CSSProperties => {
font-size: 13px;
font-weight: 600;
color: hsl(var(--text-strong));
background: hsl(var(--table-header-bg) / 0.86);
border-bottom: 1px solid hsl(var(--divider-faint) / 0.38);
background: hsl(var(--table-header-bg) / 86%);
border-bottom: 1px solid hsl(var(--divider-faint) / 38%);
}
.list-page-shell__content :deep(.el-table th.el-table__cell:first-child) {
@@ -232,7 +231,7 @@ const contentStyle = computed((): CSSProperties => {
.list-page-shell__content :deep(.el-table--border .el-table__cell) {
padding: 14px 0;
border-right: none;
border-bottom: 1px solid hsl(var(--divider-faint) / 0.46);
border-bottom: 1px solid hsl(var(--divider-faint) / 46%);
}
.list-page-shell__content :deep(.el-table__row:hover > td.el-table__cell) {

View File

@@ -138,7 +138,7 @@ onMounted(() => {
flex: 1;
flex-direction: column;
min-height: 0;
padding: 18px 20px 18px;
padding: 18px 20px;
}
.page-data-container__body {

View File

@@ -114,6 +114,7 @@ const isSvgString = (icon: any) => {
v-if="item.icon"
class="ml-[-3px] flex items-center justify-center"
>
<!-- eslint-disable vue/no-v-html -->
<div
v-if="isSvgString(item.icon)"
v-html="item.icon"
@@ -127,6 +128,7 @@ const isSvgString = (icon: any) => {
}"
class="svg-container"
></div>
<!-- eslint-enable vue/no-v-html -->
<img
v-else-if="
typeof item.icon === 'string' && !isComponent(item.icon)

View File

@@ -1,6 +1,7 @@
import {mount} from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import { describe, expect, it, vi } from 'vitest';
import {describe, expect, it, vi} from 'vitest';
import CardList from '../CardList.vue';
const { hasAccessByCodes } = vi.hoisted(() => ({
@@ -13,7 +14,7 @@ vi.mock('@easyflow/access', () => ({
}),
}));
describe('CardList', () => {
describe('cardList', () => {
function mountCardList(props: Record<string, unknown>) {
return mount(CardList, {
props: {

View File

@@ -233,14 +233,14 @@ onMounted(() => {
v-loading="loading"
:element-loading-text="loadingText"
>
<template #default="{ node, data }">
<template #default="{ node, data: itemData }">
<span class="tree-node">
<span class="node-label">{{ $t(node.label) }}</span>
<span
v-if="showCount && data[defaultProps.children]"
v-if="showCount && itemData[defaultProps.children]"
class="node-count"
>
({{ data[defaultProps.children].length }})
({{ itemData[defaultProps.children].length }})
</span>
</span>
</template>
@@ -278,17 +278,17 @@ onMounted(() => {
.tree-node {
display: flex;
align-items: center;
gap: 8px;
align-items: center;
min-width: 0;
}
.node-label {
overflow: hidden;
color: var(--el-text-color-primary);
text-overflow: ellipsis;
font-size: 14px;
font-weight: 500;
text-overflow: ellipsis;
color: var(--el-text-color-primary);
white-space: nowrap;
}
@@ -299,22 +299,22 @@ onMounted(() => {
min-width: 22px;
height: 20px;
padding: 0 6px;
color: var(--el-text-color-secondary);
font-size: 12px;
line-height: 1;
color: var(--el-text-color-secondary);
background: var(--el-fill-color);
border-radius: 999px;
}
:deep(.el-tree) {
background: transparent;
color: inherit;
background: transparent;
}
:deep(.el-tree-node__content) {
height: 38px;
margin-bottom: 4px;
padding: 0 10px;
margin-bottom: 4px;
border-radius: 10px;
transition:
background-color 0.18s ease,

View File

@@ -9,7 +9,10 @@ import { useAccessStore } from '@easyflow/stores';
import { UploadFilled } from '@element-plus/icons-vue';
import { ElIcon, ElMessage, ElUpload } from 'element-plus';
import { normalizeUploadError, resolveUploadPath } from '#/utils/upload-response';
import {
normalizeUploadError,
resolveUploadPath,
} from '#/utils/upload-response';
const props = defineProps({
action: {

View File

@@ -8,8 +8,11 @@ import { useAccessStore } from '@easyflow/stores';
import { ElButton, ElMessage, ElUpload } from 'element-plus';
import { normalizeUploadError, resolveUploadPath } from '#/utils/upload-response';
import { $t } from '#/locales';
import {
normalizeUploadError,
resolveUploadPath,
} from '#/utils/upload-response';
const props = defineProps({
action: {

View File

@@ -10,7 +10,10 @@ import { Plus } from '@element-plus/icons-vue';
import { ElIcon, ElImage, ElMessage, ElUpload } from 'element-plus';
import { $t } from '#/locales';
import { normalizeUploadError, resolveUploadPath } from '#/utils/upload-response';
import {
normalizeUploadError,
resolveUploadPath,
} from '#/utils/upload-response';
const props = defineProps({
action: {
@@ -134,20 +137,20 @@ const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
<style>
.avatar-uploader .el-upload {
width: var(--avatar-size);
height: var(--avatar-size);
position: relative;
display: flex;
align-items: center;
justify-content: center;
position: relative;
width: var(--avatar-size);
height: var(--avatar-size);
overflow: hidden;
cursor: pointer;
border: 1px solid hsl(var(--input) / 0.92);
background: hsl(var(--modal-surface-strong) / 92%);
border: 1px solid hsl(var(--input) / 92%);
border-radius: 50%;
background: hsl(var(--modal-surface-strong) / 0.92);
box-shadow:
inset 0 1px 0 hsl(0 0% 100% / 0.72),
0 16px 28px -24px hsl(var(--foreground) / 0.24);
inset 0 1px 0 hsl(0deg 0% 100% / 72%),
0 16px 28px -24px hsl(var(--foreground) / 24%);
transition:
transform var(--el-transition-duration-fast),
border-color var(--el-transition-duration-fast),
@@ -155,26 +158,26 @@ const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
}
.avatar-uploader .el-upload:hover {
border-color: hsl(var(--primary) / 0.52);
transform: translateY(-1px);
border-color: hsl(var(--primary) / 52%);
box-shadow:
inset 0 1px 0 hsl(0 0% 100% / 0.76),
0 20px 36px -26px hsl(var(--modal-preview-glow) / 0.3);
inset 0 1px 0 hsl(0deg 0% 100% / 76%),
0 20px 36px -26px hsl(var(--modal-preview-glow) / 30%);
transform: translateY(-1px);
}
.avatar-uploader--soft-panel .el-upload {
background:
radial-gradient(
circle at top,
hsl(var(--modal-preview-glow) / 0.14),
hsl(var(--modal-preview-glow) / 14%),
transparent 60%
),
linear-gradient(
145deg,
hsl(var(--modal-preview-surface) / 0.98),
hsl(var(--modal-preview-surface-strong) / 0.9)
hsl(var(--modal-preview-surface) / 98%),
hsl(var(--modal-preview-surface-strong) / 90%)
);
border-color: hsl(var(--modal-preview-border) / 0.9);
border-color: hsl(var(--modal-preview-border) / 90%);
}
.avatar-uploader--rounded .el-upload {