From 102974b667a2e460f49f6951c57162f2a914699c Mon Sep 17 00:00:00 2001 From: XIE7654 <765462425@qq.com> Date: Sat, 1 Nov 2025 20:35:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9chat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/ai/views/chat_conversation.py | 30 ++++++- web/apps/web-antd/package.json | 1 + .../src/components/useModelForm/index.ts | 79 +++++++++++++++++++ web/apps/web-antd/src/models/base.ts | 35 +++++++- .../web-antd/src/views/ai/ai_model/list.vue | 39 ++------- web/apps/web-antd/src/views/ai/chat/index.vue | 20 ++++- web/pnpm-lock.yaml | 4 + 7 files changed, 166 insertions(+), 42 deletions(-) create mode 100644 web/apps/web-antd/src/components/useModelForm/index.ts diff --git a/backend/ai/views/chat_conversation.py b/backend/ai/views/chat_conversation.py index 3a41b5b..a54904d 100644 --- a/backend/ai/views/chat_conversation.py +++ b/backend/ai/views/chat_conversation.py @@ -1,4 +1,6 @@ from rest_framework import serializers +from rest_framework.decorators import action +from rest_framework.serializers import ModelSerializer from ai.models import ChatConversation from utils.serializers import CustomModelSerializer @@ -25,6 +27,14 @@ class ChatConversationFilter(filters.FilterSet): 'system_message', 'max_tokens', 'max_contexts'] +class ConversationsSerializer(ModelSerializer): + """ + AI 聊天对话 列表序列化器 + """ + class Meta: + model = ChatConversation + fields = ['id', 'title', 'update_time'] + class ChatConversationViewSet(CustomModelViewSet): """ AI 聊天对话 视图集 @@ -36,4 +46,22 @@ class ChatConversationViewSet(CustomModelViewSet): ordering_fields = ['create_time', 'id'] ordering = ['-create_time'] -# 移入urls中 + def create(self, request, *args, **kwargs): + request.data['max_tokens'] = 2048 + request.data['max_contexts'] = 10 + if request.data['platform'] == 'tongyi': + model = 'qwen-plus' + else: + model = 'deepseek-chat' + request.data['model'] = model + request.data['temperature'] = 0.7 + return super().create(request, *args, **kwargs) + + @action(methods=['get'], detail=False) + def conversations(self): + queryset = self.get_queryset().filter(creator=self.request.user.username).values('id', 'title', 'update_time') + serializer = ConversationsSerializer(queryset, many=True) + return self._build_response( + data=serializer.data, + message="ok" + ) diff --git a/web/apps/web-antd/package.json b/web/apps/web-antd/package.json index f9c1506..187b0ae 100644 --- a/web/apps/web-antd/package.json +++ b/web/apps/web-antd/package.json @@ -42,6 +42,7 @@ "@vben/utils": "workspace:*", "@vueuse/core": "catalog:", "@vben-core/menu-ui": "workspace:*", + "@vben-core/popup-ui": "workspace:*", "ant-design-vue": "catalog:", "dayjs": "catalog:", "pinia": "catalog:", diff --git a/web/apps/web-antd/src/components/useModelForm/index.ts b/web/apps/web-antd/src/components/useModelForm/index.ts new file mode 100644 index 0000000..8dc667e --- /dev/null +++ b/web/apps/web-antd/src/components/useModelForm/index.ts @@ -0,0 +1,79 @@ +import { computed, ref } from 'vue'; + +import { $t } from '@vben/locales'; + +import { useVbenModal } from '@vben-core/popup-ui'; + +import { useVbenForm } from '#/adapter/form'; +import { BaseModel } from '#/models'; + +export interface BaseEntity { + id?: number; + // 其他公共字段... +} + +export function useModelForm(options: { + model: BaseModel; + schema: any; + titleKey: string; +}) { + const formData = ref(); + const emit = defineEmits(['success']); + + const getTitle = computed(() => { + return formData.value?.id + ? $t('ui.actionTitle.edit', [$t(options.titleKey)]) + : $t('ui.actionTitle.create', [$t(options.titleKey)]); + }); + + const [Form, formApi] = useVbenForm({ + layout: 'horizontal', + schema: options.schema, + showDefaultActions: false, + }); + + const [Modal, modalApi] = useVbenModal({ + async onConfirm() { + const { valid } = await formApi.validate(); + if (valid) { + modalApi.lock(); + const rawFormData = await formApi.getValues(); + const formattedData = rawFormData as Partial; // 关键:类型断言 + + try { + await (formData.value?.id + ? options.model.update(formData.value.id, formattedData) + : options.model.create(formattedData as Omit)); + await modalApi.close(); + emit('success'); + } finally { + modalApi.lock(false); + } + } + }, + onOpenChange(isOpen) { + if (isOpen) { + const data = modalApi.getData(); + if (data) { + formData.value = data; + formApi.setValues(formData.value); + } + } + }, + }); + + function resetForm() { + formApi.resetForm(); + formApi.setValues(formData.value || {}); + } + + return { + Form, + Modal, + formData, + getTitle, + resetForm, + formApi, + modalApi, + }; +} diff --git a/web/apps/web-antd/src/models/base.ts b/web/apps/web-antd/src/models/base.ts index a679174..4afcd33 100644 --- a/web/apps/web-antd/src/models/base.ts +++ b/web/apps/web-antd/src/models/base.ts @@ -1,5 +1,9 @@ import type { Recordable } from '@vben/types'; +import type { ExtendedModalApi } from '@vben-core/popup-ui'; + +import { message } from 'ant-design-vue'; + import { requestClient } from '#/api/request'; export interface CoreModel { @@ -18,6 +22,7 @@ export class BaseModel< UpdateData = Partial, > { protected baseUrl: string; + protected formModalApi: ExtendedModalApi | undefined; constructor(baseUrl: string) { this.baseUrl = baseUrl; @@ -70,9 +75,6 @@ export class BaseModel< return requestClient.delete(`${this.baseUrl}${id}/`); } - /** - * 导出数据 - */ /** * 导出数据 */ @@ -90,6 +92,29 @@ export class BaseModel< return requestClient.get>(this.baseUrl, { params }); } + onDelete(row: T, refreshGrid: () => void) { + const hideLoading = message.loading({ + content: '删除中...', + duration: 0, + key: 'action_process_msg', + }); + this.delete(row.id) + .then(() => { + message.success({ + content: '删除成功', + key: 'action_process_msg', + }); + refreshGrid(); + }) + .catch(() => { + hideLoading(); + }); + } + + onEdit(row: T) { + this.formModalApi?.setData(row).open(); + } + /** * 部分更新记录 */ @@ -103,6 +128,10 @@ export class BaseModel< async retrieve(id: number) { return requestClient.get(`${this.baseUrl}${id}/`); } + + setFormModalApi(api: ExtendedModalApi) { + this.formModalApi = api; + } /** * 全量更新记录 */ diff --git a/web/apps/web-antd/src/views/ai/ai_model/list.vue b/web/apps/web-antd/src/views/ai/ai_model/list.vue index 56fa5de..291c1c3 100644 --- a/web/apps/web-antd/src/views/ai/ai_model/list.vue +++ b/web/apps/web-antd/src/views/ai/ai_model/list.vue @@ -8,7 +8,7 @@ import type { AiAIModelApi } from '#/models/ai/ai_model'; import { Page, useVbenModal } from '@vben/common-ui'; import { Plus } from '@vben/icons'; -import { Button, message } from 'ant-design-vue'; +import { Button } from 'ant-design-vue'; import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { $t } from '#/locales'; @@ -17,19 +17,13 @@ import { AiAIModelModel } from '#/models/ai/ai_model'; import { useColumns, useGridFormSchema } from './data'; import Form from './modules/form.vue'; -const formModel = new AiAIModelModel(); - const [FormModal, formModalApi] = useVbenModal({ connectedComponent: Form, destroyOnClose: true, }); -/** - * 编辑AI 模型 - */ -function onEdit(row: AiAIModelApi.AiAIModel) { - formModalApi.setData(row).open(); -} +const formModel = new AiAIModelModel(); +formModel.setFormModalApi(formModalApi); /** * 创建新AI 模型 @@ -38,29 +32,6 @@ function onCreate() { formModalApi.setData(null).open(); } -/** - * 删除AI 模型 - */ -function onDelete(row: AiAIModelApi.AiAIModel) { - const hideLoading = message.loading({ - content: '删除AI 模型', - duration: 0, - key: 'action_process_msg', - }); - formModel - .delete(row.id) - .then(() => { - message.success({ - content: '删除成功', - key: 'action_process_msg', - }); - refreshGrid(); - }) - .catch(() => { - hideLoading(); - }); -} - /** * 表格操作按钮的回调函数 */ @@ -70,11 +41,11 @@ function onActionClick({ }: OnActionClickParams) { switch (code) { case 'delete': { - onDelete(row); + formModel.onDelete(row, refreshGrid); break; } case 'edit': { - onEdit(row); + formModel.onEdit(row); break; } } diff --git a/web/apps/web-antd/src/views/ai/chat/index.vue b/web/apps/web-antd/src/views/ai/chat/index.vue index 068491a..c2fd688 100644 --- a/web/apps/web-antd/src/views/ai/chat/index.vue +++ b/web/apps/web-antd/src/views/ai/chat/index.vue @@ -13,14 +13,17 @@ import { Row, Select, } from 'ant-design-vue'; - import { createConversation, fetchAIStream, getConversations, getMessages, } from '#/api/ai/chat'; +import {AiChatConversationModel} from "#/models/ai/chat_conversation"; +import {AiChatMessageModel} from "#/models/ai/chat_message"; +const aiChatConversation = new AiChatConversationModel(); +const aiChatMessageModel = new AiChatMessageModel(); interface Message { id: number; type: 'assistant' | 'user'; @@ -60,14 +63,21 @@ const filteredChats = computed(() => { async function selectChat(id: number) { selectedChatId.value = id; - const { data } = await getMessages(id); + const data = await aiChatMessageModel.list({ + conversation_id: id, + }); + console.log('history', data); messages.value = data; nextTick(scrollToBottom); } async function handleNewChat() { // 调用后端新建对话 - const { data } = await createConversation(selectedPlatform.value!); + // const { data } = await createConversation(selectedPlatform.value!); + const data = await aiChatConversation.create({ + platform: selectedPlatform.value!, + title: '新对话', + }); // 刷新对话列表 await fetchConversations(); // 选中新建的对话 @@ -127,7 +137,9 @@ function scrollToBottom() { // 获取历史对话 async function fetchConversations() { - const { data } = await getConversations(); + // const { data } = await getConversations(); + const data = await aiChatConversation.list(); + console.log('history', data); chatList.value = data.map((item: any) => ({ id: item.id, title: item.title, diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 40300e1..c33daa1 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -656,6 +656,9 @@ importers: '@vben-core/menu-ui': specifier: workspace:* version: link:../../packages/@core/ui-kit/menu-ui + '@vben-core/popup-ui': + specifier: workspace:* + version: link:../../packages/@core/ui-kit/popup-ui '@vben/access': specifier: workspace:* version: link:../../packages/effects/access @@ -9994,6 +9997,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}