浏览代码

提示词管理功能

kagg886 1 月之前
父节点
当前提交
0be689c764

+ 239 - 220
src/api/assist/index.ts

@@ -11,6 +11,12 @@ import {
 	SessionMessagesListParams,
 	SessionMessagesSaveReq,
 	LmDashboard,
+	Prompt,
+	PromptListParams,
+	PromptAddReq,
+	PromptEditReq,
+	PromptDeleteParams,
+	PromptGetParams,
 } from '/@/api/assist/type'
 import { get, post, del, put } from '/@/utils/request'
 import getOrigin from '/@/utils/origin'
@@ -70,284 +76,297 @@ export default {
 		},
 	},
 
-	struct: (uuid: string, refresh?: boolean) => post('/ai/chat/structdata', { uuid,refresh }),
-
-	// SSE聊天方法
-	chat: (data: {
-		chatRequest: ChatRequest
-		// eslint-disable-next-line no-unused-vars
-		onReceive: (resp: ChatResponse) => void
-
-		// eslint-disable-next-line no-unused-vars
-		onComplete?: (error: Error | undefined) => void
-	}) => {
-		const { chatRequest, onReceive, onComplete } = data
-
-		//FIXME 需要抹掉
-		chatRequest.modelMcpId = 1 as unknown as number[]
-		chatRequest['UserId'] = 10
-
-		// 构建SSE URL
-		const baseURL = getOrigin()
-		const url = `${baseURL}/ai/chat`
-
-		// 使用fetch API实现SSE POST请求(EventSource不支持POST和自定义headers)
-		const controller = new AbortController()
-
-		// 使用fetch API实现SSE POST请求
-		const startSSE = async () => {
-			try {
-				const headers: Record<string, string> = {
-					'Content-Type': 'application/json',
-					Accept: 'text/event-stream',
-					'Cache-Control': 'no-cache',
-				}
-
-				// 添加认证token
-				const token = getToken()
-				if (token) {
-					headers['Authorization'] = `Bearer ${token}`
-				}
-
-				const response = await fetch(url, {
-					method: 'POST',
-					headers,
-					body: JSON.stringify(chatRequest),
-					signal: controller.signal,
-				}).catch((e: Error) => e)
-
-				if (response instanceof Error) {
-					throw new Error('无法连接到服务器')
-				}
 
-				if (!response.ok) {
-					throw new Error(`HTTP error! status: ${response.status}`)
-				}
-
-				const reader = response.body?.getReader()
-				const decoder = new TextDecoder()
-
-				if (!reader) {
-					throw new Error('Response body is not readable')
-				}
+	chat: {
+		prompt: {
+			// Prompt列表
+			list: (params?: PromptListParams) => get('/system/lmprompt/list', params),
+			// 获取Prompt详情
+			detail: (params: PromptGetParams) => get('/system/lmprompt/get', params),
+			// 添加Prompt
+			add: (data: PromptAddReq) => post('/system/lmprompt/add', data),
+			// 编辑Prompt
+			edit: (data: PromptEditReq) => put('/system/lmprompt/edit', data),
+			// 删除Prompt
+			del: (params: PromptDeleteParams) => del('/system/lmprompt/delete', params),
+		},
+		struct: (uuid: string, refresh?: boolean) => post('/ai/chat/structdata', { uuid,refresh }),
+		sse: (data: {
+			chatRequest: ChatRequest
+			// eslint-disable-next-line no-unused-vars
+			onReceive: (resp: ChatResponse) => void
+
+			// eslint-disable-next-line no-unused-vars
+			onComplete?: (error: Error | undefined) => void
+		}) => {
+			const { chatRequest, onReceive, onComplete } = data
+
+			//FIXME 需要抹掉
+			chatRequest.modelMcpId = 1 as unknown as number[]
+			chatRequest['UserId'] = 10
+
+			// 构建SSE URL
+			const baseURL = getOrigin()
+			const url = `${baseURL}/ai/chat`
+
+			// 使用fetch API实现SSE POST请求(EventSource不支持POST和自定义headers)
+			const controller = new AbortController()
+
+			// 使用fetch API实现SSE POST请求
+			const startSSE = async () => {
+				try {
+					const headers: Record<string, string> = {
+						'Content-Type': 'application/json',
+						Accept: 'text/event-stream',
+						'Cache-Control': 'no-cache',
+					}
 
-				// 读取SSE流
-				let currentEvent = ''
-				let buffer = ''
+					// 添加认证token
+					const token = getToken()
+					if (token) {
+						headers['Authorization'] = `Bearer ${token}`
+					}
 
-				while (true) {
-					const { done, value } = await reader.read()
+					const response = await fetch(url, {
+						method: 'POST',
+						headers,
+						body: JSON.stringify(chatRequest),
+						signal: controller.signal,
+					}).catch((e: Error) => e)
 
-					if (done) {
-						break
+					if (response instanceof Error) {
+						throw new Error('无法连接到服务器')
 					}
 
-					buffer += decoder.decode(value, { stream: true })
-
-					// 按双换行符分割事件块
-					const eventBlocks = buffer.split('\n\n')
+					if (!response.ok) {
+						throw new Error(`HTTP error! status: ${response.status}`)
+					}
 
-					// 保留最后一个可能不完整的事件块
-					buffer = eventBlocks.pop() || ''
+					const reader = response.body?.getReader()
+					const decoder = new TextDecoder()
 
-					for (const eventBlock of eventBlocks) {
-						if (!eventBlock.trim()) continue
+					if (!reader) {
+						throw new Error('Response body is not readable')
+					}
 
-						const lines = eventBlock.split('\n')
-						let eventType = ''
-						let dataLines: string[] = []
+					// 读取SSE流
+					let currentEvent = ''
+					let buffer = ''
 
-						// 解析事件块中的每一行
-						for (const line of lines) {
-							const trimmedLine = line.trim()
+					while (true) {
+						const { done, value } = await reader.read()
 
-							if (trimmedLine.startsWith('event:')) {
-								eventType = trimmedLine.substring(6).trim()
-							} else if (trimmedLine.startsWith('data:')) {
-								// 提取data后的内容,保留原始格式(包括可能的空格)
-								const dataContent = line.substring(line.indexOf('data:') + 5)
-								dataLines.push(dataContent)
-							}
+						if (done) {
+							break
 						}
 
-						// 如果有事件类型,更新当前事件
-						if (eventType) {
-							currentEvent = eventType
-						}
+						buffer += decoder.decode(value, { stream: true })
 
-						// 如果有数据行,处理数据
-						if (dataLines.length > 0) {
-							// 将多行data合并,用换行符连接
-							const data = dataLines.join('\n')
-
-							// 根据事件类型处理数据
-							switch (currentEvent) {
-								case 'message': {
-									const messageResponse: ChatResponse = {
-										type: 'message',
-										message: data,
-									}
-									onReceive(messageResponse)
-									break
-								}
+						// 按双换行符分割事件块
+						const eventBlocks = buffer.split('\n\n')
 
-								case 'toolcall': {
-									const tools = JSON.parse(data)
-									const toolcallResponse: ChatResponse = {
-										type: 'toolcall',
-										request: {
-											id: tools['tool_call_id'],
-											name: tools['name'],
-											data: tools['arguments'],
-										},
-									}
-									onReceive(toolcallResponse)
-									break
-								}
-
-								case 'structdata': {
-									const structDataResponse: ChatResponse = {
-										type: 'structdata',
-										uuid: data,
-									}
-									onReceive(structDataResponse)
-									break
-								}
+						// 保留最后一个可能不完整的事件块
+						buffer = eventBlocks.pop() || ''
 
-								case 'toolres': {
-									const tools = JSON.parse(data)
-									const toolresResponse: ChatResponse = {
-										type: 'toolres',
-										response: {
-											id: tools['tool_call_id'],
-											name: tools['name'],
-											data: tools['response'],
-										},
-									}
-									onReceive(toolresResponse)
-									break
-								}
+						for (const eventBlock of eventBlocks) {
+							if (!eventBlock.trim()) continue
 
-								case 'error': {
-									const errorResponse: ErrorResponse = {
-										type: 'error',
-										error: data,
-									}
-									onReceive(errorResponse)
-									break
-								}
+							const lines = eventBlock.split('\n')
+							let eventType = ''
+							let dataLines: string[] = []
 
-								case 'meta': {
-									break
-								}
+							// 解析事件块中的每一行
+							for (const line of lines) {
+								const trimmedLine = line.trim()
 
-								default: {
-									// 如果没有明确的事件类型,默认作为消息处理
-									const defaultResponse: ChatResponse = {
-										type: 'message',
-										message: data,
-									}
-									onReceive(defaultResponse)
-									break
+								if (trimmedLine.startsWith('event:')) {
+									eventType = trimmedLine.substring(6).trim()
+								} else if (trimmedLine.startsWith('data:')) {
+									// 提取data后的内容,保留原始格式(包括可能的空格)
+									const dataContent = line.substring(line.indexOf('data:') + 5)
+									dataLines.push(dataContent)
 								}
 							}
-						}
-					}
-
-					// 处理buffer中剩余的可能完整的单行事件
-					const remainingLines = buffer.split('\n')
-					const completeLines = remainingLines.slice(0, -1)
-					buffer = remainingLines[remainingLines.length - 1] || ''
-
-					for (const line of completeLines) {
-						const trimmedLine = line.trim()
-
-						if (trimmedLine === '') {
-							continue
-						}
-
-						if (trimmedLine.startsWith('event:')) {
-							currentEvent = trimmedLine.substring(6).trim()
-							continue
-						}
 
-						if (trimmedLine.startsWith('data:')) {
-							const data = line.substring(line.indexOf('data:') + 5)
+							// 如果有事件类型,更新当前事件
+							if (eventType) {
+								currentEvent = eventType
+							}
 
-							// 根据事件类型处理数据
-							switch (currentEvent) {
-								case 'message': {
-									const messageResponse: ChatResponse = {
-										type: 'message',
-										message: data,
+							// 如果有数据行,处理数据
+							if (dataLines.length > 0) {
+								// 将多行data合并,用换行符连接
+								const data = dataLines.join('\n')
+
+								// 根据事件类型处理数据
+								switch (currentEvent) {
+									case 'message': {
+										const messageResponse: ChatResponse = {
+											type: 'message',
+											message: data,
+										}
+										onReceive(messageResponse)
+										break
 									}
-									onReceive(messageResponse)
-									break
-								}
 
-								case 'toolcall': {
-									try {
+									case 'toolcall': {
 										const tools = JSON.parse(data)
 										const toolcallResponse: ChatResponse = {
 											type: 'toolcall',
 											request: {
+												id: tools['tool_call_id'],
 												name: tools['name'],
 												data: tools['arguments'],
 											},
 										}
 										onReceive(toolcallResponse)
-									} catch (error) {
-										console.error('解析toolcall数据失败:', error, '原始数据:', data)
+										break
+									}
+
+									case 'structdata': {
+										const structDataResponse: ChatResponse = {
+											type: 'structdata',
+											uuid: data,
+										}
+										onReceive(structDataResponse)
+										break
 									}
-									break
-								}
 
-								case 'toolres': {
-									try {
+									case 'toolres': {
 										const tools = JSON.parse(data)
 										const toolresResponse: ChatResponse = {
 											type: 'toolres',
 											response: {
+												id: tools['tool_call_id'],
 												name: tools['name'],
 												data: tools['response'],
 											},
 										}
 										onReceive(toolresResponse)
-									} catch (error) {
-										console.error('解析toolres数据失败:', error, '原始数据:', data)
+										break
+									}
+
+									case 'error': {
+										const errorResponse: ErrorResponse = {
+											type: 'error',
+											error: data,
+										}
+										onReceive(errorResponse)
+										break
+									}
+
+									case 'meta': {
+										break
+									}
+
+									default: {
+										// 如果没有明确的事件类型,默认作为消息处理
+										const defaultResponse: ChatResponse = {
+											type: 'message',
+											message: data,
+										}
+										onReceive(defaultResponse)
+										break
 									}
-									break
 								}
+							}
+						}
+
+						// 处理buffer中剩余的可能完整的单行事件
+						const remainingLines = buffer.split('\n')
+						const completeLines = remainingLines.slice(0, -1)
+						buffer = remainingLines[remainingLines.length - 1] || ''
+
+						for (const line of completeLines) {
+							const trimmedLine = line.trim()
 
-								default: {
-									// 如果没有明确的事件类型,默认作为消息处理
-									const defaultResponse: ChatResponse = {
-										type: 'message',
-										message: data,
+							if (trimmedLine === '') {
+								continue
+							}
+
+							if (trimmedLine.startsWith('event:')) {
+								currentEvent = trimmedLine.substring(6).trim()
+								continue
+							}
+
+							if (trimmedLine.startsWith('data:')) {
+								const data = line.substring(line.indexOf('data:') + 5)
+
+								// 根据事件类型处理数据
+								switch (currentEvent) {
+									case 'message': {
+										const messageResponse: ChatResponse = {
+											type: 'message',
+											message: data,
+										}
+										onReceive(messageResponse)
+										break
+									}
+
+									case 'toolcall': {
+										try {
+											const tools = JSON.parse(data)
+											const toolcallResponse: ChatResponse = {
+												type: 'toolcall',
+												request: {
+													name: tools['name'],
+													data: tools['arguments'],
+												},
+											}
+											onReceive(toolcallResponse)
+										} catch (error) {
+											console.error('解析toolcall数据失败:', error, '原始数据:', data)
+										}
+										break
+									}
+
+									case 'toolres': {
+										try {
+											const tools = JSON.parse(data)
+											const toolresResponse: ChatResponse = {
+												type: 'toolres',
+												response: {
+													name: tools['name'],
+													data: tools['response'],
+												},
+											}
+											onReceive(toolresResponse)
+										} catch (error) {
+											console.error('解析toolres数据失败:', error, '原始数据:', data)
+										}
+										break
+									}
+
+									default: {
+										// 如果没有明确的事件类型,默认作为消息处理
+										const defaultResponse: ChatResponse = {
+											type: 'message',
+											message: data,
+										}
+										onReceive(defaultResponse)
+										break
 									}
-									onReceive(defaultResponse)
-									break
 								}
 							}
 						}
 					}
-				}
 
-				onComplete?.(undefined)
-			} catch (error: any) {
-				if (error.name !== 'AbortError') {
-					console.error('SSE连接错误:', error)
+					onComplete?.(undefined)
+				} catch (error: any) {
+					if (error.name !== 'AbortError') {
+						console.error('SSE连接错误:', error)
+					}
+					onComplete?.(error)
 				}
-				onComplete?.(error)
 			}
-		}
 
-		// 启动SSE连接
-		startSSE()
+			// 启动SSE连接
+			startSSE()
 
-		// 返回关闭连接的函数
-		return () => {
-			controller.abort()
+			// 返回关闭连接的函数
+			return () => {
+				controller.abort()
+			}
 		}
 	},
 }

+ 39 - 0
src/api/assist/type.ts

@@ -180,3 +180,42 @@ export type LmDashboard = {
 	createdBy?: number // 创建者ID
 	updatedBy?: number // 更新者ID
 }
+
+export type Prompt = {
+	id: number
+	title: string
+	prompt: string
+	createdAt?: string // 创建时间
+	updatedAt?: string // 更新时间
+	createdBy?: number // 创建者ID
+	updatedBy?: number // 更新者ID
+}
+
+// Prompt列表查询参数
+export type PromptListParams = {
+	keyWord?: string // 搜索关键字
+	dateRange?: string[] // 日期范围
+	OrderBy?: string // 排序
+	pageNum?: number // 分页号码,默认1
+	pageSize?: number // 分页数量,最大500,默认10
+	title?: string // 标题
+	createdAt?: string // 创建时间
+}
+
+// Prompt添加请求
+export type PromptAddReq = Omit<Prompt, 'id' | 'createdAt' | 'updatedAt'>
+
+// Prompt编辑请求
+export type PromptEditReq = Prompt & {
+	id: number // 编辑时ID必须
+}
+
+// Prompt删除请求参数
+export type PromptDeleteParams = {
+	ids: number[]
+}
+
+// 获取单个Prompt参数
+export type PromptGetParams = {
+	id: number
+}

+ 1 - 1
src/components/markdown/plugins/impl/VueStructData.vue

@@ -24,7 +24,7 @@ interface StructData {
 const data = ref<StructData>()
 
 const { loading: loadingStruct, doLoading: doLoadingStruct } = useLoading(async () => {
-	data.value = await assist.struct(props.uuid,props.refresh)
+	data.value = await assist.chat.struct(props.uuid,props.refresh)
 })
 
 // 导出数据为CSV格式

+ 239 - 4
src/views/assistant/index.vue

@@ -26,7 +26,7 @@ import ToolsLoadingPlugin from '/@/components/markdown/plugins/tools-loading'
 import TablePlugin from '/@/components/markdown/plugins/table'
 import Markdown from '/@/components/markdown/Markdown.vue'
 import assist from '/@/api/assist'
-import { ChatResponse, LmConfigInfo, LmSession, Message } from '/@/api/assist/type'
+import { ChatResponse, LmConfigInfo, LmSession, Message, Prompt, PromptListParams } from '/@/api/assist/type'
 import { useLoading } from '/@/utils/loading-util'
 import { Setting as EleSetting } from '@element-plus/icons-vue'
 import { useRouter } from 'vue-router'
@@ -102,6 +102,15 @@ const messagesContainer = ref<HTMLElement>()
 const prompt = ref<string>('')
 const openPromptDialog = ref(false)
 
+// 提示词列表相关状态
+const promptList = ref<Prompt[]>([])
+const promptListParams = ref<PromptListParams>({
+	pageNum: 1,
+	pageSize: 10,
+	keyWord: ''
+})
+const promptListTotal = ref(0)
+
 // 模型选择
 const modelOptions = ref<LmConfigInfo[]>([])
 const modelLabel = computed(() => {
@@ -127,6 +136,26 @@ const { loading: loadingModels, doLoading: loadModel } = useLoading(async () =>
 
 onMounted(loadModel)
 
+// 加载提示词列表
+const { loading: loadingPromptList, doLoading: loadPromptList } = useLoading(async () => {
+	const data: { list: Prompt[]; total: number } = await assist.chat.prompt.list(promptListParams.value).catch(() => {
+		return {
+			list: [],
+			total: 0,
+		}
+	})
+
+	promptList.value = data.list
+	promptListTotal.value = data.total
+})
+
+// 监听对话框打开状态,打开时加载提示词列表
+watch(openPromptDialog, (newVal) => {
+	if (newVal) {
+		loadPromptList()
+	}
+})
+
 const selectedModel = ref<number | undefined>(undefined)
 
 const chatInstance = ref<(() => void) | undefined>(undefined)
@@ -254,7 +283,7 @@ const replaceMessage = async (index: number) => {
 }
 
 const chatInternal = (rtn: Message, context: Message[] = messages.value) => {
-	chatInstance.value = assist.chat({
+	chatInstance.value = assist.chat.sse({
 		chatRequest: {
 			message: prompt.value
 				? [
@@ -730,6 +759,38 @@ const { loading: exportConversationLoading, doLoading: exportConversation } = us
 const isBlank = (str: string) => {
 	return str == null || str.trim().length === 0
 }
+
+// 提示词列表相关函数
+// 选择提示词
+const selectPrompt = (selectedPrompt: Prompt) => {
+	prompt.value = selectedPrompt.prompt
+}
+
+// 提示词分页变化
+const handlePromptPageChange = async (page: number) => {
+	promptListParams.value.pageNum = page
+	await loadPromptList()
+}
+
+// 提示词每页数目变化
+const handlePromptPageSizeChange = async (pageSize: number) => {
+	promptListParams.value.pageSize = pageSize
+	promptListParams.value.pageNum = 1 // 重置到第一页
+	await loadPromptList()
+}
+
+// 提示词搜索
+const handlePromptSearch = async () => {
+	promptListParams.value.pageNum = 1
+	await loadPromptList()
+}
+
+// 重置提示词搜索
+const handlePromptReset = async () => {
+	promptListParams.value.keyWord = ''
+	promptListParams.value.pageNum = 1
+	await loadPromptList()
+}
 </script>
 
 <template>
@@ -1100,14 +1161,79 @@ const isBlank = (str: string) => {
 		<el-dialog
 			v-model="openPromptDialog"
 			title="设置提示词"
-			width="600px"
+			width="800px"
 			:before-close="
 				() => {
 					openPromptDialog = false
 				}
 			"
 		>
-			<el-input v-model="prompt" type="textarea" placeholder="请输入提示词..." :rows="8" resize="none" />
+			<div class="prompt-dialog-content">
+				<!-- 提示词输入框 -->
+				<div class="prompt-input-section">
+					<h4>提示词内容</h4>
+					<el-input v-model="prompt" type="textarea" placeholder="请输入提示词..." :rows="8" resize="none" />
+				</div>
+
+				<!-- 提示词列表 -->
+				<div class="prompt-list-section">
+					<div class="prompt-list-header">
+						<h4>选择提示词模板</h4>
+						<div class="prompt-search">
+							<el-input
+								v-model="promptListParams.keyWord"
+								placeholder="搜索提示词..."
+								:prefix-icon="Search"
+								clearable
+								@keydown.enter="handlePromptSearch"
+								@clear="handlePromptReset"
+								style="width: 200px"
+							/>
+							<el-button type="primary" :icon="Search" @click="handlePromptSearch" :loading="loadingPromptList" size="small">
+								搜索
+							</el-button>
+						</div>
+					</div>
+
+					<!-- 提示词列表 -->
+					<div class="prompt-list-container" v-loading="loadingPromptList">
+						<div v-if="promptList.length === 0" class="prompt-list-empty">
+							<el-empty description="暂无提示词模板" />
+						</div>
+						<div v-else class="prompt-list">
+							<div
+								v-for="item in promptList"
+								:key="item.id"
+								class="prompt-item"
+								@click="selectPrompt(item)"
+							>
+								<div class="prompt-item-header">
+									<span class="prompt-title">{{ item.title }}</span>
+								</div>
+								<div class="prompt-preview">
+									{{ item.prompt.length > 60 ? item.prompt.substring(0, 60) + '...' : item.prompt }}
+								</div>
+							</div>
+						</div>
+					</div>
+
+					<!-- 分页 -->
+					<div class="prompt-pagination" v-if="promptListTotal > 0">
+						<el-pagination
+							v-model:current-page="promptListParams.pageNum"
+							v-model:page-size="promptListParams.pageSize"
+							:total="promptListTotal"
+							:page-sizes="[5, 10, 20, 30]"
+							layout="total, sizes, prev, pager, next"
+							@current-change="handlePromptPageChange"
+							@size-change="handlePromptPageSizeChange"
+							:disabled="loadingPromptList"
+							small
+						/>
+					</div>
+				</div>
+			</div>
+
 			<template #footer>
 				<div class="dialog-footer">
 					<el-button @click="openPromptDialog = false">取消</el-button>
@@ -1825,6 +1951,115 @@ const isBlank = (str: string) => {
 	text-align: right;
 }
 
+/* 提示词对话框样式 */
+.prompt-dialog-content {
+	display: flex;
+	flex-direction: column;
+	gap: 24px;
+	max-height: 600px;
+}
+
+.prompt-input-section {
+	h4 {
+		margin: 0 0 12px 0;
+		color: var(--el-text-color-primary);
+		font-size: 14px;
+		font-weight: 600;
+	}
+}
+
+.prompt-list-section {
+	border-top: 1px solid var(--el-border-color-lighter);
+	padding-top: 20px;
+
+	h4 {
+		margin: 0 0 16px 0;
+		color: var(--el-text-color-primary);
+		font-size: 14px;
+		font-weight: 600;
+	}
+}
+
+.prompt-list-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	margin-bottom: 16px;
+
+	.prompt-search {
+		display: flex;
+		align-items: center;
+		gap: 8px;
+	}
+}
+
+.prompt-list-container {
+	min-height: 200px;
+	max-height: 300px;
+	overflow-y: auto;
+	border: 1px solid var(--el-border-color-light);
+	border-radius: 6px;
+	background: var(--el-fill-color-extra-light);
+}
+
+.prompt-list-empty {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	height: 200px;
+}
+
+.prompt-list {
+	padding: 8px;
+}
+
+.prompt-item {
+	padding: 12px;
+	margin-bottom: 8px;
+	border: 1px solid var(--el-border-color-lighter);
+	border-radius: 6px;
+	background: var(--el-bg-color);
+	cursor: pointer;
+	transition: all 0.2s ease;
+
+	&:hover {
+		border-color: var(--el-color-primary-light-7);
+		background: var(--el-color-primary-light-9);
+		transform: translateY(-1px);
+		box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+	}
+
+	&:last-child {
+		margin-bottom: 0;
+	}
+}
+
+.prompt-item-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	margin-bottom: 8px;
+}
+
+.prompt-title {
+	font-weight: 600;
+	color: var(--el-text-color-primary);
+	font-size: 14px;
+}
+
+.prompt-preview {
+	color: var(--el-text-color-regular);
+	font-size: 12px;
+	line-height: 1.4;
+	word-break: break-all;
+}
+
+.prompt-pagination {
+	margin-top: 16px;
+	display: flex;
+	justify-content: center;
+}
+
 /* 响应式调整 */
 /* 空状态页面样式 */
 .empty-content {

+ 369 - 0
src/views/assistant/prompt.vue

@@ -0,0 +1,369 @@
+<script setup lang="ts">
+import { onMounted, reactive, ref } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Search as EleSearch, Refresh as EleRefresh, Plus as ElePlus, Delete as EleDelete, Edit as EleEdit } from '@element-plus/icons-vue'
+import { useLoading } from '/@/utils/loading-util'
+import api from '/@/api/assist'
+import type { Prompt, PromptListParams } from '/@/api/assist/type'
+
+// 数据搜索部分
+const searchParam = reactive<PromptListParams>({
+	pageNum: 1,
+	pageSize: 10,
+	keyWord: '',
+	title: '',
+	dateRange: [],
+})
+
+const total = ref<number>(0)
+const data = ref<Array<Prompt>>([])
+const ids = ref<number[]>([])
+
+// 加载列表数据
+const { loading, doLoading: doListLoad } = useLoading(async () => {
+	try {
+		const res: {
+			list: Prompt[]
+			total: number
+		} = await api.chat.prompt.list(searchParam)
+		total.value = res.total
+		data.value = res.list
+	} catch (error) {
+		console.error('获取提示词列表失败:', error)
+		data.value = []
+		total.value = 0
+	}
+})
+
+// 重置搜索条件
+const reset = () => {
+	Object.assign(searchParam, {
+		pageNum: 1,
+		pageSize: 10,
+		keyWord: '',
+		title: '',
+		dateRange: [],
+	})
+	doListLoad()
+}
+
+// 选择删除项
+const onDeleteItemSelected = (selection: Prompt[]) => {
+	ids.value = selection.map((item) => item.id).filter(Boolean)
+}
+
+// 批量删除
+const del = async () => {
+	if (ids.value.length === 0) {
+		ElMessage.error('请选择要删除的数据')
+		return
+	}
+
+	try {
+		await ElMessageBox.confirm('您确定要删除所选数据吗?', '提示', {
+			confirmButtonText: '确认',
+			cancelButtonText: '取消',
+			type: 'warning',
+		})
+
+		await api.chat.prompt.del({ ids: ids.value })
+		ElMessage.success('删除成功')
+		await doListLoad()
+	} catch (error) {
+		if (error !== 'cancel') {
+			console.error('删除失败:', error)
+			ElMessage.error('删除失败')
+		}
+	}
+}
+
+// 单个删除
+const delSingle = async (id: number) => {
+	try {
+		await ElMessageBox.confirm('您确定要删除这条数据吗?', '提示', {
+			confirmButtonText: '确认',
+			cancelButtonText: '取消',
+			type: 'warning',
+		})
+
+		await api.chat.prompt.del({ ids: [id] })
+		ElMessage.success('删除成功')
+		await doListLoad()
+	} catch (error) {
+		if (error !== 'cancel') {
+			console.error('删除失败:', error)
+			ElMessage.error('删除失败')
+		}
+	}
+}
+
+// 编辑/新增对话框相关
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const isEdit = ref(false)
+const formRef = ref()
+
+const formData = reactive<Prompt>({
+	id: 0,
+	title: '',
+	prompt: '',
+})
+
+// 表单验证规则
+const formRules = {
+	title: [{ required: true, message: '请输入提示词标题', trigger: 'blur' }],
+	prompt: [{ required: true, message: '请输入提示词内容', trigger: 'blur' }],
+}
+
+// 重置表单
+const resetForm = () => {
+	Object.assign(formData, {
+		id: 0,
+		title: '',
+		prompt: '',
+	})
+	formRef.value?.clearValidate()
+}
+
+// 打开新增对话框
+const openAddDialog = () => {
+	resetForm()
+	dialogTitle.value = '新增提示词'
+	isEdit.value = false
+	dialogVisible.value = true
+}
+
+const loadingNum = ref<number>(-1)
+// 打开编辑对话框
+const { doLoading: openEditDialog, loading: loadingOpenEditDialog } = useLoading(async (row: Prompt) => {
+	try {
+		loadingNum.value = row.id ?? -1
+		const res = await api.chat.prompt.detail({ id: row.id })
+		Object.assign(formData, res)
+		dialogTitle.value = '编辑提示词'
+		isEdit.value = true
+		dialogVisible.value = true
+	} catch (error) {
+		console.error('获取提示词详情失败:', error)
+		ElMessage.error('获取提示词详情失败')
+	}
+})
+
+// 保存表单
+const { loading: saveLoading, doLoading: doSave } = useLoading(async () => {
+	try {
+		await formRef.value?.validate()
+
+		if (isEdit.value) {
+			await api.chat.prompt.edit(formData as any)
+			ElMessage.success('编辑成功')
+		} else {
+			await api.chat.prompt.add(formData as any)
+			ElMessage.success('新增成功')
+		}
+
+		dialogVisible.value = false
+		await doListLoad()
+	} catch (error) {
+		console.error('保存失败:', error)
+		if (error !== false) {
+			// 表单验证失败时不显示错误消息
+			ElMessage.error('保存失败')
+		}
+	}
+})
+
+// 组件挂载时加载数据
+onMounted(() => {
+	doListLoad()
+})
+</script>
+
+<template>
+	<el-card shadow="never" class="page">
+		<!-- 搜索表单 -->
+		<el-form :model="searchParam" inline>
+			<el-form-item label="" prop="keyWord">
+				<el-input style="width: 200px" v-model="searchParam.keyWord" placeholder="搜索关键字" clearable />
+			</el-form-item>
+			<el-form-item label="" prop="title">
+				<el-input style="width: 150px" v-model="searchParam.title" placeholder="提示词标题" clearable />
+			</el-form-item>
+			<el-form-item label="" prop="dateRange">
+				<el-date-picker
+					v-model="searchParam.dateRange"
+					style="width: 220px"
+					value-format="YYYY-MM-DD"
+					type="daterange"
+					range-separator="-"
+					start-placeholder="开始时间"
+					end-placeholder="结束时间"
+				/>
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="doListLoad">
+					<el-icon>
+						<EleSearch />
+					</el-icon>
+					查询
+				</el-button>
+				<el-button @click="reset">
+					<el-icon>
+						<EleRefresh />
+					</el-icon>
+					重置
+				</el-button>
+				<el-button type="primary" @click="openAddDialog">
+					<el-icon>
+						<ElePlus />
+					</el-icon>
+					新增提示词
+				</el-button>
+				<el-button type="danger" @click="del" :disabled="ids.length === 0">
+					<el-icon>
+						<EleDelete />
+					</el-icon>
+					批量删除
+				</el-button>
+			</el-form-item>
+		</el-form>
+
+		<!-- 数据表格 -->
+		<el-table :data="data" style="width: 100%" v-loading="loading" @selection-change="onDeleteItemSelected">
+			<el-table-column type="selection" width="50" align="center" />
+			<el-table-column label="ID" prop="id" width="80" align="center" />
+			<el-table-column label="标题" prop="title" align="center" show-overflow-tooltip />
+			<el-table-column label="提示词内容" prop="prompt" align="center" show-overflow-tooltip>
+				<template #default="scope">
+					<div class="prompt-content">
+						{{ scope.row.prompt.length > 100 ? scope.row.prompt.substring(0, 100) + '...' : scope.row.prompt }}
+					</div>
+				</template>
+			</el-table-column>
+			<el-table-column label="创建时间" prop="createdAt" width="180" align="center" />
+			<el-table-column label="更新时间" prop="updatedAt" width="180" align="center" />
+			<el-table-column label="操作" width="150" align="center" fixed="right">
+				<template #default="scope">
+					<el-button text type="primary" size="small" @click="openEditDialog(scope.row)" :loading="loadingOpenEditDialog && loadingNum === scope.row.id"> 编辑 </el-button>
+					<el-button text type="danger" size="small" @click="delSingle(scope.row.id)"> 删除 </el-button>
+				</template>
+			</el-table-column>
+		</el-table>
+
+		<!-- 分页 -->
+		<div class="pagination-container">
+			<el-pagination
+				v-show="total > 0"
+				:current-page="searchParam.pageNum"
+				:page-size="searchParam.pageSize"
+				:page-sizes="[10, 20, 50, 100]"
+				:total="total"
+				layout="total, sizes, prev, pager, next, jumper"
+				@size-change="(size: number) => { searchParam.pageSize = size; doListLoad(); }"
+				@current-change="(page: number) => { searchParam.pageNum = page; doListLoad(); }"
+			/>
+		</div>
+
+		<!-- 编辑/新增对话框 -->
+		<el-dialog :title="dialogTitle" v-model="dialogVisible" width="800px" destroy-on-close @close="resetForm">
+			<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
+				<el-form-item label="提示词标题" prop="title">
+					<el-input v-model="formData.title" placeholder="请输入提示词标题" clearable />
+				</el-form-item>
+				<el-form-item label="提示词内容" prop="prompt">
+					<el-input
+						v-model="formData.prompt"
+						type="textarea"
+						:rows="10"
+						placeholder="请输入提示词内容"
+						clearable
+						show-word-limit
+						maxlength="2000"
+					/>
+				</el-form-item>
+			</el-form>
+
+			<template #footer>
+				<div class="dialog-footer">
+					<el-button @click="dialogVisible = false">取消</el-button>
+					<el-button type="primary" @click="doSave" :loading="saveLoading"> 确定 </el-button>
+				</div>
+			</template>
+		</el-dialog>
+	</el-card>
+</template>
+
+<style scoped lang="scss">
+.page {
+	margin: 20px;
+
+	.el-form {
+		margin-bottom: 20px;
+
+		.el-form-item {
+			margin-bottom: 20px;
+		}
+	}
+
+	.pagination-container {
+		margin-top: 20px;
+		display: flex;
+		justify-content: center;
+	}
+
+	.dialog-footer {
+		text-align: right;
+
+		.el-button {
+			margin-left: 10px;
+		}
+	}
+}
+
+// 提示词内容样式
+.prompt-content {
+	max-width: 300px;
+	word-break: break-all;
+	line-height: 1.4;
+}
+
+// 表格样式优化
+.el-table {
+	.el-table__header {
+		th {
+			background-color: var(--el-bg-color-page);
+			color: var(--el-text-color-primary);
+			font-weight: 600;
+		}
+	}
+
+	.el-table__row {
+		&:hover {
+			background-color: var(--el-bg-color-page);
+		}
+	}
+}
+
+// 按钮组样式
+.el-form-item:last-child {
+	.el-button {
+		margin-right: 10px;
+
+		&:last-child {
+			margin-right: 0;
+		}
+	}
+}
+
+// 操作按钮样式
+.el-table__fixed-right {
+	.el-button {
+		margin: 0 2px;
+
+		&.el-button--small {
+			padding: 5px 8px;
+			font-size: 12px;
+		}
+	}
+}
+</style>