mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-04-08 14:37:38 +00:00
新增 CardBase 组件,重构知识文档卡片、知识库卡片和分段卡片以使用新组件,优化按钮组件并更新相关页面以提升用户体验。
This commit is contained in:
44
frontend/src/components/common/CardBase.vue
Normal file
44
frontend/src/components/common/CardBase.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
|
||||
<div class="p-4">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<div class="flex-1">
|
||||
<h5 :class="[titleClass || 'text-xl font-semibold tracking-tight text-gray-900 mb-1 truncate']">
|
||||
<slot name="title"></slot>
|
||||
</h5>
|
||||
<div v-if="$slots.subtitle" class="flex items-center mb-2">
|
||||
<slot name="subtitle"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$slots['header-actions']" class="flex space-x-2">
|
||||
<slot name="header-actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.content" class="text-sm text-gray-600 mb-3 space-y-2">
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<slot name="footer-left">
|
||||
<span v-if="$slots.timestamp" class="text-xs text-gray-500">
|
||||
<slot name="timestamp"></slot>
|
||||
</span>
|
||||
</slot>
|
||||
<div v-if="$slots['footer-actions']" class="flex space-x-2">
|
||||
<slot name="footer-actions"></slot>
|
||||
</div>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
titleClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
</script>
|
||||
4
frontend/src/components/common/index.ts
Normal file
4
frontend/src/components/common/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import CardBase from "./CardBase.vue";
|
||||
import PromotionBanner from "./PromotionBanner.vue";
|
||||
|
||||
export { CardBase, PromotionBanner };
|
||||
@@ -1,31 +1,26 @@
|
||||
<template>
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
|
||||
<div class="p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h5 class="text-xl font-semibold tracking-tight text-gray-900 mb-1 truncate">{{ doc.name }}</h5>
|
||||
<div class="flex items-center mb-2">
|
||||
<KnowledgeStatusBadge :status="doc.status" type="status" class="mr-2" />
|
||||
<KnowledgeStatusBadge :enabled="doc.enable" type="enabled" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<slot name="toggle-switch"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500">
|
||||
上传时间: {{ formatDateString(doc.createTime) }}
|
||||
</span>
|
||||
<div class="flex space-x-2">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CardBase>
|
||||
<template #title>{{ doc.name }}</template>
|
||||
<template #subtitle>
|
||||
<KnowledgeStatusBadge :status="doc.status" type="status" class="mr-2" />
|
||||
<KnowledgeStatusBadge :enabled="doc.enable" type="enabled" />
|
||||
</template>
|
||||
<template #header-actions>
|
||||
<slot name="toggle-switch"></slot>
|
||||
</template>
|
||||
<template #footer-left>
|
||||
<span class="text-xs text-gray-500">
|
||||
上传时间: {{ formatDateString(doc.createTime) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #footer-actions>
|
||||
<slot name="actions"></slot>
|
||||
</template>
|
||||
</CardBase>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CardBase from '@/components/common/CardBase.vue';
|
||||
import { KnowledgeStatusBadge } from '@/components/common/knowledge';
|
||||
import type { LibraryDoc } from "@/types/KnowledgeTypes";
|
||||
import { formatDateString } from '@/utils/dateUtil';
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
<template>
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
|
||||
<div class="p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<h5 class="text-xl font-semibold tracking-tight text-gray-900 mb-1 truncate">{{ library.name }}</h5>
|
||||
<div class="flex space-x-2">
|
||||
<slot name="actions-top"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 mb-3 line-clamp-2">
|
||||
<CardBase>
|
||||
<template #title>{{ library.name }}</template>
|
||||
<template #header-actions>
|
||||
<slot name="actions-top"></slot>
|
||||
</template>
|
||||
<template #content>
|
||||
<p class="text-sm text-gray-600 line-clamp-2">
|
||||
{{ library.description || '暂无描述' }}
|
||||
</p>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500">
|
||||
创建时间: {{ formatDateString(library.createTime) }}
|
||||
</span>
|
||||
<slot name="actions-bottom"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer-left>
|
||||
<span class="text-xs text-gray-500">
|
||||
创建时间: {{ formatDateString(library.createTime) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #footer-actions>
|
||||
<slot name="actions-bottom"></slot>
|
||||
</template>
|
||||
</CardBase>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CardBase from '@/components/common/CardBase.vue';
|
||||
import type { Library } from "@/types/KnowledgeTypes";
|
||||
import { formatDateString } from '@/utils/dateUtil';
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-4">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<h5 class="text-lg font-semibold text-gray-900">分段 #{{ index + 1 }}</h5>
|
||||
<CardBase>
|
||||
<template #title>分段 #{{ index + 1 }}</template>
|
||||
<template #header-actions>
|
||||
<div class="text-xs text-gray-500">
|
||||
ID: {{ segment.id }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 mb-2">
|
||||
</template>
|
||||
<template #subtitle>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800">
|
||||
Embedding ID: {{ segment.embeddingId || '无' }}
|
||||
@@ -15,16 +15,19 @@
|
||||
Token 使用量: {{ segment.tokenUsage || 0 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 pt-3 mt-3">
|
||||
<h6 class="text-sm font-medium text-gray-900 mb-2">内容:</h6>
|
||||
<pre
|
||||
class="text-sm text-gray-700 whitespace-pre-wrap bg-gray-50 p-3 rounded-lg max-h-60 overflow-y-auto">{{ segment.content }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="border-t border-gray-200 pt-3">
|
||||
<h6 class="text-sm font-medium text-gray-900 mb-2">内容:</h6>
|
||||
<pre
|
||||
class="text-sm text-gray-700 whitespace-pre-wrap bg-gray-50 p-3 rounded-lg max-h-60 overflow-y-auto">{{ segment.content }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
</CardBase>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CardBase from '@/components/common/CardBase.vue';
|
||||
import type { LibraryDocSegment } from "@/types/KnowledgeTypes";
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
122
frontend/src/components/ui/Button.vue
Normal file
122
frontend/src/components/ui/Button.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<button :class="[
|
||||
'flex items-center justify-center gap-x-1 whitespace-nowrap font-medium rounded-lg focus:ring-4 focus:outline-none',
|
||||
sizeClasses,
|
||||
colorClasses,
|
||||
(disabled || (isLoading && !abortable)) ? 'opacity-50 cursor-not-allowed' : '',
|
||||
fullWidth ? 'w-full' : '',
|
||||
className
|
||||
]" :disabled="disabled || (isLoading && !abortable)" @click="handleClick" :type="type">
|
||||
<div v-if="isLoading && !abortable" class="animate-spin mr-1" :class="iconSizeClasses">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
<StopIcon v-else-if="isLoading && abortable" :class="iconSizeClasses" />
|
||||
<slot v-else name="icon"></slot>
|
||||
<span v-if="$slots.default">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { StopIcon } from "@/components/icons";
|
||||
import { computed } from "vue";
|
||||
|
||||
export type ButtonVariant =
|
||||
| "primary"
|
||||
| "secondary"
|
||||
| "success"
|
||||
| "danger"
|
||||
| "warning"
|
||||
| "info";
|
||||
export type ButtonSize = "xs" | "sm" | "md" | "lg";
|
||||
export type ButtonType = "button" | "submit" | "reset";
|
||||
|
||||
const props = defineProps<{
|
||||
/** 按钮变体类型 */
|
||||
variant?: ButtonVariant;
|
||||
/** 按钮尺寸 */
|
||||
size?: ButtonSize;
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean;
|
||||
/** 自定义CSS类名 */
|
||||
className?: string;
|
||||
/** 是否为移动端尺寸 */
|
||||
isMobile?: boolean;
|
||||
/** 是否处于加载状态 */
|
||||
isLoading?: boolean;
|
||||
/** 是否可中止 */
|
||||
abortable?: boolean;
|
||||
/** 按钮类型 */
|
||||
type?: ButtonType;
|
||||
/** 是否占满宽度 */
|
||||
fullWidth?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [event: MouseEvent];
|
||||
}>();
|
||||
|
||||
/** 按钮颜色样式映射 */
|
||||
const colorClasses = computed(() => {
|
||||
const variants: Record<ButtonVariant, string> = {
|
||||
primary: "text-white bg-blue-700 hover:bg-blue-800 focus:ring-blue-300",
|
||||
secondary:
|
||||
"text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 focus:ring-gray-100",
|
||||
success: "text-white bg-green-700 hover:bg-green-800 focus:ring-green-300",
|
||||
danger: "text-white bg-red-700 hover:bg-red-800 focus:ring-red-300",
|
||||
warning:
|
||||
"text-gray-900 bg-yellow-400 hover:bg-yellow-500 focus:ring-yellow-300",
|
||||
info: "text-white bg-cyan-700 hover:bg-cyan-800 focus:ring-cyan-300",
|
||||
};
|
||||
|
||||
return variants[props.variant || "primary"];
|
||||
});
|
||||
|
||||
/** 按钮尺寸样式映射 */
|
||||
const sizeClasses = computed(() => {
|
||||
// 移动端尺寸
|
||||
if (props.isMobile) {
|
||||
const sizes: Record<ButtonSize, string> = {
|
||||
xs: "text-xs px-2 py-1",
|
||||
sm: "text-xs px-3 py-1.5",
|
||||
md: "text-sm px-3 py-2",
|
||||
lg: "text-sm px-4 py-2.5",
|
||||
};
|
||||
return sizes[props.size || "sm"];
|
||||
}
|
||||
|
||||
// PC端尺寸
|
||||
const sizes: Record<ButtonSize, string> = {
|
||||
xs: "text-xs px-3 py-1.5",
|
||||
sm: "text-sm px-3 py-2",
|
||||
md: "text-sm px-4 py-2.5",
|
||||
lg: "text-base px-5 py-3",
|
||||
};
|
||||
|
||||
return sizes[props.size || "md"];
|
||||
});
|
||||
|
||||
/** 图标尺寸样式映射 */
|
||||
const iconSizeClasses = computed(() => {
|
||||
const sizes: Record<ButtonSize, string> = {
|
||||
xs: "w-3.5 h-3.5",
|
||||
sm: "w-4 h-4",
|
||||
md: "w-4.5 h-4.5",
|
||||
lg: "w-5 h-5",
|
||||
};
|
||||
return sizes[props.size || "md"];
|
||||
});
|
||||
|
||||
/** 处理点击事件 */
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (!props.disabled && !(props.isLoading && !props.abortable)) {
|
||||
emit("click", event);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
7
frontend/src/components/ui/index.ts
Normal file
7
frontend/src/components/ui/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Alert from "./Alert.vue";
|
||||
import Avatar from "./Avatar.vue";
|
||||
import Button from "./Button.vue";
|
||||
import InputButton from "./InputButton.vue";
|
||||
import SortIcon from "./SortIcon.vue";
|
||||
|
||||
export { Alert, Avatar, Button, InputButton, SortIcon };
|
||||
Reference in New Issue
Block a user