From c208683d80d64823aff30e4721bbf7f50115811d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BC=80=E7=A4=BE?= <2568429394@qq.com> Date: Wed, 13 Aug 2025 19:35:59 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=94=B9=E6=AD=A3=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E7=9A=84=E4=B8=A4=E4=B8=AA=E8=AD=A6=E5=91=8A=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E8=B7=A8=E5=9F=9F=E7=9A=84=E5=89=8D=E7=AB=AF=E7=AB=AF?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/UniversalAdmin.Api/Program.cs | 12 ++++++------ .../Services/ChatService.cs | 10 +++++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/backend/UniversalAdmin.Api/Program.cs b/backend/UniversalAdmin.Api/Program.cs index 1772a52..5e8c568 100644 --- a/backend/UniversalAdmin.Api/Program.cs +++ b/backend/UniversalAdmin.Api/Program.cs @@ -216,7 +216,7 @@ builder.Services.ConfigureSwagger(); builder.Services.AddCors(options => options.AddPolicy("AllowFrontend", policy => { - policy.WithOrigins( "http://localhost:5173", + policy.WithOrigins("http://localhost:5173", "http://localhost:3000", "http://localhost:8080", "http://localhost:4173", @@ -225,11 +225,11 @@ builder.Services.AddCors(options => "http://admin-zjp.beiweijierui.xyz:5173", "http://admin-yzy.wudkmao.top:5173", "http://47.122.49.186:5173", - "http://admin-zjp-two.beiweijierui.xyz", - "http://admin-yzy-two.wudkmao.top", - "http://admin-hcx-two.axuege.xyz", - "http://rag.zzaisx.top" - ) + "http://admin-zjp-two.beiweijierui.xyz:3000", + "http://admin-yzy-two.wudkmao.top:3000", + "http://admin-hcx-two.axuege.xyz:3000", + "http://rag.zzaisx.top:3000" + ) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); diff --git a/backend/UniversalAdmin.Infrastructure/Services/ChatService.cs b/backend/UniversalAdmin.Infrastructure/Services/ChatService.cs index 1ff5355..be0c095 100644 --- a/backend/UniversalAdmin.Infrastructure/Services/ChatService.cs +++ b/backend/UniversalAdmin.Infrastructure/Services/ChatService.cs @@ -141,13 +141,13 @@ public class ChatService : IChatService { var messages = await _messageRepository.GetByConversationIdOrderedAsync(conversationId); var messageDtos = _mapper.Map>(messages); - + // 为每个消息添加空的sources列表(历史消息可能没有保存sources信息) foreach (var messageDto in messageDtos) { messageDto.Sources = new List(); } - + return messageDtos; } @@ -391,7 +391,7 @@ public class ChatService : IChatService // 使用AI服务基于上下文生成智能回答 var aiAnswer = await CallAIWithContextAsync(question, context); Console.WriteLine($"✅ AI服务返回答案: {aiAnswer?.Substring(0, Math.Min(100, aiAnswer?.Length ?? 0))}..."); - return aiAnswer; + return aiAnswer ?? "抱歉,我现在无法基于文档内容回答这个问题。"; } catch (Exception ex) { @@ -487,6 +487,10 @@ public class ChatService : IChatService if (response.IsSuccessStatusCode) { Console.WriteLine("✅ AI服务调用成功,解析响应..."); + if (string.IsNullOrEmpty(responseContent)) + { + throw new Exception("AI服务返回了空的响应内容"); + } var jsonResponse = System.Text.Json.JsonDocument.Parse(responseContent); var aiAnswer = jsonResponse.RootElement .GetProperty("choices")[0] -- Gitee From f1f8dc4043c9c95051b1dd19f3893d6175c38729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BC=80=E7=A4=BE?= <2568429394@qq.com> Date: Wed, 13 Aug 2025 20:05:18 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E5=AE=8C=E6=88=90ai=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E7=9A=84=E6=A0=B7=E5=BC=8F=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/uniapp/src/App.vue | 53 ++- frontend/uniapp/src/main.js | 14 +- frontend/uniapp/src/pages.json | 14 - frontend/uniapp/src/pages/index/index.vue | 405 +++++++++++----------- 4 files changed, 273 insertions(+), 213 deletions(-) diff --git a/frontend/uniapp/src/App.vue b/frontend/uniapp/src/App.vue index 49dad18..7bc3d8d 100644 --- a/frontend/uniapp/src/App.vue +++ b/frontend/uniapp/src/App.vue @@ -2,7 +2,7 @@ - + @@ -43,6 +43,12 @@ export default { onHide: function () { console.log("App Hide"); }, + onError: function (err) { + console.error("App Error:", err); + }, + onUnhandledRejection: function (res) { + console.error("App Unhandled Rejection:", res); + } }; @@ -55,6 +61,51 @@ page { sans-serif; } +#app { + width: 100%; + height: 100%; +} + +.app-container { + width: 100%; + height: 100%; +} + +/* 全局加载样式 */ +.global-loading { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.loading-spinner { + width: 60rpx; + height: 60rpx; + border: 4rpx solid rgba(255, 255, 255, 0.3); + border-top: 4rpx solid white; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 20rpx; +} + +.loading-text { + color: white; + font-size: 28rpx; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + /* 通用样式类 */ .flex-center { display: flex; diff --git a/frontend/uniapp/src/main.js b/frontend/uniapp/src/main.js index 3f29620..c1eb17a 100644 --- a/frontend/uniapp/src/main.js +++ b/frontend/uniapp/src/main.js @@ -6,12 +6,22 @@ export function createApp() { // 全局错误处理 app.config.errorHandler = (err, vm, info) => { - console.error('Vue Error:', err, info) + console.error('Vue Error:', err) + console.error('Error Info:', info) + console.error('Component:', vm) } // 全局警告处理 app.config.warnHandler = (msg, vm, trace) => { - console.warn('Vue Warning:', msg, trace) + console.warn('Vue Warning:', msg) + console.warn('Warning Trace:', trace) + } + + // 添加全局属性 + app.config.globalProperties.$platform = { + isH5: process.env.VUE_APP_PLATFORM === 'h5', + isWeixin: process.env.VUE_APP_PLATFORM === 'mp-weixin', + isApp: process.env.VUE_APP_PLATFORM === 'app-plus' } return { diff --git a/frontend/uniapp/src/pages.json b/frontend/uniapp/src/pages.json index da10044..1187c25 100644 --- a/frontend/uniapp/src/pages.json +++ b/frontend/uniapp/src/pages.json @@ -55,19 +55,5 @@ "navigationBarTitleText": "VKG AI", "navigationBarBackgroundColor": "#000000", "backgroundColor": "#f8f8f8" - }, - "tabBar": { - "color": "#7A7E83", - "selectedColor": "#3cc51f", - "borderStyle": "black", - "backgroundColor": "#ffffff", - "list": [ - { - "pagePath": "pages/index/index", - "iconPath": "static/VK_MDY_lightmode.png", - "selectedIconPath": "static/VK_MDY_lightmode.png", - "text": "AI对话" - } - ] } } diff --git a/frontend/uniapp/src/pages/index/index.vue b/frontend/uniapp/src/pages/index/index.vue index 70c8f77..7bb871f 100644 --- a/frontend/uniapp/src/pages/index/index.vue +++ b/frontend/uniapp/src/pages/index/index.vue @@ -4,16 +4,22 @@ - + + + VKG AI @@ -30,19 +36,29 @@ - 💬 + + + 开始对话 与AI助手进行智能问答 - 📚 + + + + + + + 文档管理 管理您的文档和查看分析结果 - 🗂️ + + + 对话历史 查看之前的对话记录 @@ -259,26 +275,26 @@ onMounted(() => { }); // 检查认证状态 - const checkAuth = () => { - // 检查是否为游客模式 - if (userStore.isGuestMode()) { - const guestUser = userStore.getGuestInfo(); - if (guestUser) { - userInfo.value = { - username: guestUser.username, - isGuest: true, - }; - - // 显示游客模式提示框 - uni.showModal({ - title: '游客模式', - content: '当前为游客模式,如果要使用对话功能,请点击右上角退出注册账号', - showCancel: false, - confirmText: '我知道了' - }); - } - return; +const checkAuth = () => { + // 检查是否为游客模式 + if (userStore.isGuestMode()) { + const guestUser = userStore.getGuestInfo(); + if (guestUser) { + userInfo.value = { + username: guestUser.username, + isGuest: true, + }; + + // 显示游客模式提示框 + uni.showModal({ + title: "游客模式", + content: "当前为游客模式,如果要使用对话功能,请点击右上角退出注册账号", + showCancel: false, + confirmText: "我知道了", + }); } + return; + } // 检查正式登录状态 if (!userStore.isLoggedIn()) { @@ -536,157 +552,6 @@ html { overflow: hidden; } -/* 隐藏底部导航栏 */ -.uni-tabbar { - display: none !important; - visibility: hidden !important; - opacity: 0 !important; - height: 0 !important; - width: 0 !important; - overflow: hidden !important; - position: absolute !important; - top: -9999px !important; - left: -9999px !important; -} - -.uni-tabbar__item { - display: none !important; - visibility: hidden !important; - opacity: 0 !important; - height: 0 !important; - width: 0 !important; - overflow: hidden !important; - position: absolute !important; - top: -9999px !important; - left: -9999px !important; -} - -/* 隐藏tabBar容器 */ -.uni-tabbar__container { - display: none !important; - visibility: hidden !important; - opacity: 0 !important; - height: 0 !important; - width: 0 !important; - overflow: hidden !important; - position: absolute !important; - top: -9999px !important; - left: -9999px !important; -} - -/* 隐藏tabBar背景 */ -.uni-tabbar__background { - display: none !important; - visibility: hidden !important; - opacity: 0 !important; - height: 0 !important; - width: 0 !important; - overflow: hidden !important; - position: absolute !important; - top: -9999px !important; - left: -9999px !important; -} - -/* 隐藏AI对话元素 */ -.uni-tabbar__bd { - display: none !important; - visibility: hidden !important; - opacity: 0 !important; - height: 0 !important; - width: 0 !important; - overflow: hidden !important; - position: absolute !important; - top: -9999px !important; - left: -9999px !important; -} - -.uni-tabbar__icon { - display: none !important; - visibility: hidden !important; - opacity: 0 !important; - height: 0 !important; - width: 0 !important; - overflow: hidden !important; - position: absolute !important; - top: -9999px !important; - left: -9999px !important; -} - -.uni-tabbar__label { - display: none !important; - visibility: hidden !important; - opacity: 0 !important; - height: 0 !important; - width: 0 !important; - overflow: hidden !important; - position: absolute !important; - top: -9999px !important; - left: -9999px !important; -} - -/* 隐藏整个tabBar项目内容 */ -.uni-tabbar__item .uni-tabbar__bd { - display: none !important; - visibility: hidden !important; - opacity: 0 !important; - height: 0 !important; - width: 0 !important; - overflow: hidden !important; - position: absolute !important; - top: -9999px !important; - left: -9999px !important; -} - -.uni-tabbar__item .uni-tabbar__icon { - display: none !important; - visibility: hidden !important; - opacity: 0 !important; - height: 0 !important; - width: 0 !important; - overflow: hidden !important; - position: absolute !important; - top: -9999px !important; - left: -9999px !important; -} - -.uni-tabbar__item .uni-tabbar__label { - display: none !important; - visibility: hidden !important; - opacity: 0 !important; - height: 0 !important; - width: 0 !important; - overflow: hidden !important; - position: absolute !important; - top: -9999px !important; - left: -9999px !important; -} - -/* 额外的隐藏规则,确保完全隐藏 */ -.uni-tabbar__icon__diff { - display: none !important; - visibility: hidden !important; - opacity: 0 !important; - height: 0 !important; - width: 0 !important; - overflow: hidden !important; - position: absolute !important; - top: -9999px !important; - left: -9999px !important; -} - -/* 隐藏所有可能的tabBar相关元素 */ -.uni-tabbar * { - display: none !important; - visibility: hidden !important; - opacity: 0 !important; - height: 0 !important; - width: 0 !important; - overflow: hidden !important; - position: absolute !important; - top: -9999px !important; - left: -9999px !important; -} - /* 确保页面底部没有空白 */ body { padding-bottom: 0 !important; @@ -713,6 +578,32 @@ page { overflow: hidden; } +/* 添加背景装饰 */ +.container::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient( + circle at 20% 80%, + rgba(120, 119, 198, 0.3) 0%, + transparent 50% + ), + radial-gradient( + circle at 80% 20%, + rgba(255, 119, 198, 0.3) 0%, + transparent 50% + ), + radial-gradient( + circle at 40% 40%, + rgba(120, 219, 255, 0.2) 0%, + transparent 50% + ); + pointer-events: none; +} + /* 确保页面填满整个视口 */ page { height: 100vh; @@ -733,8 +624,11 @@ page { .header { padding: 40rpx 40rpx 20rpx; - background: rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.15); backdrop-filter: blur(20rpx); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + position: relative; + z-index: 10; } .header-content { @@ -748,10 +642,21 @@ page { align-items: center; } -.logo { +.logo-icon { width: 80rpx; height: 80rpx; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + display: flex; + align-items: center; + justify-content: center; margin-right: 20rpx; + overflow: hidden; +} + +.logo-image { + width: 60rpx; + height: 60rpx; } .logo-text { @@ -777,14 +682,17 @@ page { backdrop-filter: blur(10rpx); } -.btn-icon { - font-size: 32rpx; - color: white; +.user-avatar-image { + width: 40rpx; + height: 40rpx; + border-radius: 50%; } .main-content { padding: 40rpx; flex: 1; + position: relative; + z-index: 5; } .welcome-section { @@ -814,30 +722,135 @@ page { } .feature-card { - background: rgba(128, 128, 128, 0.25); + background: rgba(255, 255, 255, 0.15); backdrop-filter: blur(20rpx); border-radius: 20rpx; padding: 50rpx 40rpx; text-align: center; cursor: pointer; transition: all 0.3s ease; - border: 1rpx solid rgba(200, 200, 200, 0.3); + border: 1px solid rgba(255, 255, 255, 0.2); box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1); &:hover { transform: translateY(-8rpx); - background: rgba(150, 150, 150, 0.35); + background: rgba(255, 255, 255, 0.25); box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.15); - border-color: rgba(220, 220, 220, 0.4); + border-color: rgba(255, 255, 255, 0.3); } } .feature-icon { - font-size: 60rpx; - margin-bottom: 25rpx; + width: 60rpx; + height: 60rpx; + margin: 0 auto 25rpx; + display: flex; + align-items: center; + justify-content: center; opacity: 0.9; } +.icon-bubble { + width: 60rpx; + height: 60rpx; + background: rgba(255, 255, 255, 0.9); + border-radius: 50%; + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.icon-bubble::before { + content: ""; + width: 40rpx; + height: 30rpx; + background: rgba(255, 255, 255, 0.9); + border-radius: 20rpx 20rpx 20rpx 5rpx; + position: relative; +} + +.icon-bubble::after { + content: ""; + position: absolute; + bottom: -8rpx; + right: 10rpx; + width: 0; + height: 0; + border-left: 8rpx solid rgba(255, 255, 255, 0.9); + border-top: 8rpx solid transparent; + border-bottom: 8rpx solid transparent; +} + +.icon-documents { + width: 60rpx; + height: 60rpx; + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.doc { + position: absolute; + width: 24rpx; + height: 32rpx; + background: rgba(255, 255, 255, 0.9); + border-radius: 4rpx; + box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1); +} + +.doc-1 { + top: 5rpx; + left: 5rpx; + transform: rotate(-5deg); +} + +.doc-2 { + bottom: 5rpx; + right: 5rpx; + transform: rotate(5deg); +} + +.doc-3 { + top: 15rpx; + right: 15rpx; + transform: rotate(-10deg); +} + +.icon-folder { + width: 60rpx; + height: 60rpx; + background: rgba(255, 255, 255, 0.9); + border-radius: 8rpx; + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.icon-folder::before { + content: ""; + position: absolute; + top: 8rpx; + left: 8rpx; + right: 8rpx; + height: 8rpx; + background: rgba(255, 255, 255, 0.9); + border-radius: 4rpx 4rpx 0 0; +} + +.icon-folder::after { + content: ""; + position: absolute; + top: 16rpx; + left: 8rpx; + right: 8rpx; + bottom: 8rpx; + background: rgba(255, 255, 255, 0.9); + border-radius: 0 0 4rpx 4rpx; +} + .feature-title { font-size: 32rpx; font-weight: 600; @@ -855,11 +868,11 @@ page { } .quick-start-section { - background: rgba(128, 128, 128, 0.25); + background: rgba(255, 255, 255, 0.15); backdrop-filter: blur(20rpx); border-radius: 20rpx; padding: 50rpx 40rpx; - border: 1rpx solid rgba(200, 200, 200, 0.3); + border: 1px solid rgba(255, 255, 255, 0.2); box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1); } @@ -881,8 +894,8 @@ page { } .quick-btn { - background: rgba(150, 150, 150, 0.3); - border: 1rpx solid rgba(200, 200, 200, 0.4); + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 15rpx; padding: 25rpx 35rpx; color: white; @@ -893,10 +906,10 @@ page { backdrop-filter: blur(10rpx); &:hover { - background: rgba(170, 170, 170, 0.4); + background: rgba(255, 255, 255, 0.3); transform: scale(1.05); box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.15); - border-color: rgba(220, 220, 220, 0.5); + border-color: rgba(255, 255, 255, 0.4); } &:active { -- Gitee From 7c5410bf49e54d0a01b03bd674e5bf35b43ead90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BC=80=E7=A4=BE?= <2568429394@qq.com> Date: Wed, 13 Aug 2025 20:18:51 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=C2=94=C2=A8=E6=88=B7?= =?UTF-8?q?=E5=A4=B4=E5=83=8F=E6=98=BE=E7=A4=BA=E4=B8=8D=E5=85=A8=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/uniapp/src/pages/index/index.vue | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/uniapp/src/pages/index/index.vue b/frontend/uniapp/src/pages/index/index.vue index 7bb871f..be4d05a 100644 --- a/frontend/uniapp/src/pages/index/index.vue +++ b/frontend/uniapp/src/pages/index/index.vue @@ -937,9 +937,9 @@ page { } .chat-container { - width: 90%; - max-width: 800rpx; - height: 80%; + width: 100%; + max-width: 1000rpx; + height: 85%; background: white; border-radius: 30rpx; display: flex; @@ -972,7 +972,7 @@ page { .chat-messages { flex: 1; - padding: 20rpx; + padding: 10rpx 10rpx 10rpx 10rpx; overflow-y: auto; } @@ -989,6 +989,11 @@ page { align-items: flex-end; margin-right: 20rpx; } + + .message-avatar { + margin-left: 40rpx; + margin-right: 20rpx; + } } .message-ai { @@ -1007,6 +1012,7 @@ page { justify-content: center; flex-shrink: 0; margin: 0 16rpx; + overflow: visible; } .avatar-icon { @@ -1018,7 +1024,7 @@ page { flex: 1; display: flex; flex-direction: column; - max-width: 70%; + max-width: 65%; } .message-text { -- Gitee From 7431c71f0c679210a53154abcd6e5cd1370678fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BC=80=E7=A4=BE?= <2568429394@qq.com> Date: Wed, 13 Aug 2025 23:17:07 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=BA=86h5=E7=AB=AF?= =?UTF-8?q?=E5=92=8C=E5=BE=AE=E4=BF=A1=E5=B0=8F=E7=A8=8B=E5=BA=8F=E7=AB=AF?= =?UTF-8?q?=E7=9A=84=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/DocumentController.cs | 62 +-- .../Services/IDocumentService.cs | 2 +- .../Repositories/DocumentChunkRepository.cs | 2 +- .../Repositories/DocumentRepository.cs | 6 +- .../Services/DocumentService.cs | 115 +++-- ...12\344\274\240\347\273\204\344\273\266.md" | 165 +++++++ frontend/uniapp/src/api/index.js | 195 ++++++--- ...ocumentUpload.vue => DocumentUploadH5.vue} | 267 ++++++++---- .../src/components/DocumentUploadSmart.vue | 153 +++++++ .../src/components/DocumentUploadWeixin.vue | 410 ++++++++++++++++++ frontend/uniapp/src/manifest.json | 51 ++- .../uniapp/src/pages/documents/detail.vue | 21 +- frontend/uniapp/src/pages/documents/index.vue | 147 ++----- frontend/uniapp/src/pages/index/index.vue | 230 +++++++++- frontend/uniapp/src/utils/platform.js | 371 +++++++++++----- 15 files changed, 1700 insertions(+), 497 deletions(-) create mode 100644 "frontend/uniapp/README_\344\270\212\344\274\240\347\273\204\344\273\266.md" rename frontend/uniapp/src/components/{DocumentUpload.vue => DocumentUploadH5.vue} (49%) create mode 100644 frontend/uniapp/src/components/DocumentUploadSmart.vue create mode 100644 frontend/uniapp/src/components/DocumentUploadWeixin.vue diff --git a/backend/UniversalAdmin.Api/Controllers/DocumentController.cs b/backend/UniversalAdmin.Api/Controllers/DocumentController.cs index 4d75cb8..c1c632c 100644 --- a/backend/UniversalAdmin.Api/Controllers/DocumentController.cs +++ b/backend/UniversalAdmin.Api/Controllers/DocumentController.cs @@ -51,12 +51,12 @@ public class DocumentController : ControllerBase } [HttpPost("upload")] - public async Task UploadDocument([FromForm] IFormFile file) + public async Task UploadDocument([FromForm] IFormFile file, [FromForm] string? fileName = null) { try { var userId = GetCurrentUserId(); - var document = await _documentService.UploadDocumentAsync(userId, file); + var document = await _documentService.UploadDocumentAsync(userId, file, fileName); return CreatedAtAction(nameof(GetDocumentById), new { id = document.Id }, document); } catch (Exception ex) @@ -73,16 +73,6 @@ public class DocumentController : ControllerBase var userId = GetCurrentUserId(); Console.WriteLine($"用户 {userId} 尝试删除文档 {id}"); - // 先检查文档是否存在 - var document = await _documentService.GetDocumentByIdAsync(userId, id); - if (document == null) - { - Console.WriteLine($"文档 {id} 不存在或用户 {userId} 无权限访问"); - return NotFound(new { message = "文档不存在或无权限删除" }); - } - - Console.WriteLine($"找到文档: {document.Title}, 开始删除..."); - var success = await _documentService.DeleteDocumentAsync(userId, id); if (!success) { @@ -96,7 +86,16 @@ public class DocumentController : ControllerBase catch (Exception ex) { Console.WriteLine($"删除文档时发生异常: {ex.Message}"); + Console.WriteLine($"异常类型: {ex.GetType().Name}"); Console.WriteLine($"异常堆栈: {ex.StackTrace}"); + + // 如果是数据库相关异常,提供更具体的错误信息 + if (ex.InnerException != null) + { + Console.WriteLine($"内部异常: {ex.InnerException.Message}"); + return BadRequest(new { message = $"删除失败: {ex.InnerException.Message}" }); + } + return BadRequest(new { message = $"删除失败: {ex.Message}" }); } } @@ -133,46 +132,7 @@ public class DocumentController : ControllerBase } } - [HttpPost("{id}/test-delete")] - public async Task TestDeleteDocument(Guid id) - { - try - { - var userId = GetCurrentUserId(); - Console.WriteLine($"测试删除文档 {id},用户 {userId}"); - // 检查文档是否存在 - var document = await _documentService.GetDocumentByIdAsync(userId, id); - if (document == null) - { - return NotFound(new { message = "文档不存在", documentId = id, userId = userId }); - } - - // 检查文档块数量 - var chunks = await _documentService.GetDocumentChunksAsync(userId, id, 1, 1000); - var chunkCount = chunks.TotalCount; - - Console.WriteLine($"文档信息: ID={id}, 标题={document.Title}, 块数量={chunkCount}"); - - return Ok(new - { - message = "文档存在,可以删除", - document = new - { - id = document.Id, - title = document.Title, - uploadedBy = document.UploadedBy - }, - chunkCount = chunkCount, - userId = userId - }); - } - catch (Exception ex) - { - Console.WriteLine($"测试删除时发生异常: {ex.Message}"); - return BadRequest(new { message = ex.Message, documentId = id }); - } - } private Guid GetCurrentUserId() { diff --git a/backend/UniversalAdmin.Application/Services/IDocumentService.cs b/backend/UniversalAdmin.Application/Services/IDocumentService.cs index 88ba8d4..6535763 100644 --- a/backend/UniversalAdmin.Application/Services/IDocumentService.cs +++ b/backend/UniversalAdmin.Application/Services/IDocumentService.cs @@ -6,7 +6,7 @@ namespace UniversalAdmin.Application.Services; public interface IDocumentService { - Task UploadDocumentAsync(Guid userId, IFormFile file); + Task UploadDocumentAsync(Guid userId, IFormFile file, string? fileName = null); Task> GetAllDocumentsAsync(Guid userId, int page = 1, int pageSize = 10, string? keyword = null); Task GetDocumentByIdAsync(Guid userId, Guid id); Task DeleteDocumentAsync(Guid userId, Guid id); diff --git a/backend/UniversalAdmin.Infrastructure/Repositories/DocumentChunkRepository.cs b/backend/UniversalAdmin.Infrastructure/Repositories/DocumentChunkRepository.cs index 3e42bde..ba01269 100644 --- a/backend/UniversalAdmin.Infrastructure/Repositories/DocumentChunkRepository.cs +++ b/backend/UniversalAdmin.Infrastructure/Repositories/DocumentChunkRepository.cs @@ -14,7 +14,7 @@ public class DocumentChunkRepository : Repository, IDocumentChunk public async Task> GetByDocumentIdAsync(Guid documentId) { return await _context.DocumentChunks - .Where(dc => dc.DocumentId == documentId && !dc.IsDeleted) + .Where(dc => dc.DocumentId == documentId) .ToListAsync(); } diff --git a/backend/UniversalAdmin.Infrastructure/Repositories/DocumentRepository.cs b/backend/UniversalAdmin.Infrastructure/Repositories/DocumentRepository.cs index ba3d171..813d232 100644 --- a/backend/UniversalAdmin.Infrastructure/Repositories/DocumentRepository.cs +++ b/backend/UniversalAdmin.Infrastructure/Repositories/DocumentRepository.cs @@ -14,8 +14,8 @@ public class DocumentRepository : Repository, IDocumentRepository public async Task GetByIdWithChunksAsync(Guid id) { return await _context.Documents - .Include(d => d.Chunks.Where(c => !c.IsDeleted)) - .FirstOrDefaultAsync(d => d.Id == id && !d.IsDeleted); + .Include(d => d.Chunks) + .FirstOrDefaultAsync(d => d.Id == id); } public async Task> GetAllWithChunksAsync() @@ -39,6 +39,6 @@ public class DocumentRepository : Repository, IDocumentRepository public IQueryable GetQueryable() { - return _context.Documents.Where(d => !d.IsDeleted).AsQueryable(); + return _context.Documents.AsQueryable(); } } \ No newline at end of file diff --git a/backend/UniversalAdmin.Infrastructure/Services/DocumentService.cs b/backend/UniversalAdmin.Infrastructure/Services/DocumentService.cs index 28f8f9e..92f5a25 100644 --- a/backend/UniversalAdmin.Infrastructure/Services/DocumentService.cs +++ b/backend/UniversalAdmin.Infrastructure/Services/DocumentService.cs @@ -29,15 +29,22 @@ public class DocumentService : IDocumentService _mapper = mapper; } - public async Task UploadDocumentAsync(Guid userId, IFormFile file) + public async Task UploadDocumentAsync(Guid userId, IFormFile file, string? fileName = null) { try { + // 优先使用传入的fileName,如果没有则使用file.FileName + var documentTitle = !string.IsNullOrEmpty(fileName) + ? Path.GetFileNameWithoutExtension(fileName) + : Path.GetFileNameWithoutExtension(file.FileName); + + var documentSource = !string.IsNullOrEmpty(fileName) ? fileName : file.FileName; + var document = new Document { - Title = Path.GetFileNameWithoutExtension(file.FileName), + Title = documentTitle, Content = await ReadFileContentAsync(file), - Source = file.FileName, + Source = documentSource, UploadedBy = userId, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, @@ -76,8 +83,10 @@ public class DocumentService : IDocumentService public async Task> GetAllDocumentsAsync(Guid userId, int page = 1, int pageSize = 10, string? keyword = null) { - var query = _documentRepository.GetQueryable() - .Where(d => d.UploadedBy == userId && !d.IsDeleted); + // 直接查询数据库,不检查软删除状态 + var context = (ApplicationDbContext)_documentRepository.GetContext(); + var query = context.Documents + .Where(d => d.UploadedBy == userId); if (!string.IsNullOrWhiteSpace(keyword)) { @@ -103,7 +112,7 @@ public class DocumentService : IDocumentService public async Task GetDocumentByIdAsync(Guid userId, Guid id) { var document = await _documentRepository.GetByIdWithChunksAsync(id); - if (document == null || document.UploadedBy != userId || document.IsDeleted) + if (document == null || document.UploadedBy != userId) return null; return _mapper.Map(document); @@ -113,53 +122,86 @@ public class DocumentService : IDocumentService { try { - var document = await _documentRepository.GetByIdAsync(id); - if (document == null || document.UploadedBy != userId) - return false; + Console.WriteLine($"开始删除文档,用户ID: {userId}, 文档ID: {id}"); - // 获取数据库上下文以使用事务 + // 直接查询数据库,不检查软删除状态 var context = (ApplicationDbContext)_documentRepository.GetContext(); + var document = await context.Documents + .FirstOrDefaultAsync(d => d.Id == id); + + if (document == null) + { + Console.WriteLine($"文档 {id} 不存在"); + return false; + } + + if (document.UploadedBy != userId) + { + Console.WriteLine($"用户 {userId} 无权限删除文档 {id},文档属于用户 {document.UploadedBy}"); + return false; + } + + Console.WriteLine($"找到文档: {document.Title}, 开始删除..."); - using var transaction = await context.Database.BeginTransactionAsync(); try { - // 软删除相关的文档块 - var chunks = await _documentChunkRepository.GetByDocumentIdAsync(id); - if (chunks.Any()) + // 先检查文档块数量 + var chunkCount = await context.DocumentChunks + .Where(dc => dc.DocumentId == id) + .CountAsync(); + Console.WriteLine($"文档 {id} 有 {chunkCount} 个文档块"); + + // 使用原生SQL删除,避免EF Core的复杂性 + using var transaction = await context.Database.BeginTransactionAsync(); + try { - foreach (var chunk in chunks) - { - chunk.IsDeleted = true; - chunk.UpdatedAt = DateTime.UtcNow; - chunk.UpdateBy = userId.ToString(); - } - context.DocumentChunks.UpdateRange(chunks); - } + // 先删除文档块 + var chunksDeleted = await context.Database.ExecuteSqlRawAsync( + "DELETE FROM document_chunks WHERE \"DocumentId\" = {0}", id); + Console.WriteLine($"删除了 {chunksDeleted} 个文档块"); - // 软删除文档 - document.IsDeleted = true; - document.UpdatedAt = DateTime.UtcNow; - document.UpdateBy = userId.ToString(); - context.Documents.Update(document); + // 再删除文档 + var documentsDeleted = await context.Database.ExecuteSqlRawAsync( + "DELETE FROM documents WHERE \"Id\" = {0} AND \"UploadedBy\" = {1}", id, userId); + Console.WriteLine($"删除了 {documentsDeleted} 个文档"); - // 保存更改 - await context.SaveChangesAsync(); + if (documentsDeleted == 0) + { + Console.WriteLine("没有删除任何文档,可能是权限问题"); + await transaction.RollbackAsync(); + return false; + } - // 提交事务 - await transaction.CommitAsync(); - return true; + await transaction.CommitAsync(); + Console.WriteLine($"文档 {id} 删除成功"); + return true; + } + catch (Exception) + { + await transaction.RollbackAsync(); + throw; + } } catch (Exception ex) { - // 回滚事务 - await transaction.RollbackAsync(); Console.WriteLine($"删除文档失败: {ex.Message}"); + Console.WriteLine($"异常类型: {ex.GetType().Name}"); + Console.WriteLine($"异常堆栈: {ex.StackTrace}"); + + // 如果是数据库约束错误,提供更详细的信息 + if (ex.InnerException != null) + { + Console.WriteLine($"内部异常: {ex.InnerException.Message}"); + } + return false; } } catch (Exception ex) { Console.WriteLine($"删除文档时发生异常: {ex.Message}"); + Console.WriteLine($"异常类型: {ex.GetType().Name}"); + Console.WriteLine($"异常堆栈: {ex.StackTrace}"); return false; } } @@ -172,7 +214,10 @@ public class DocumentService : IDocumentService var chunks = await _documentChunkRepository.GetByDocumentIdAsync(documentId); var totalItems = chunks.Count(); - var pagedChunks = chunks.Skip((page - 1) * pageSize).Take(pageSize); + var pagedChunks = chunks + .OrderBy(c => c.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize); return new PagedResultDto { diff --git "a/frontend/uniapp/README_\344\270\212\344\274\240\347\273\204\344\273\266.md" "b/frontend/uniapp/README_\344\270\212\344\274\240\347\273\204\344\273\266.md" new file mode 100644 index 0000000..b5cd4e7 --- /dev/null +++ "b/frontend/uniapp/README_\344\270\212\344\274\240\347\273\204\344\273\266.md" @@ -0,0 +1,165 @@ +# 文档上传组件使用说明 + +## 概述 + +为了解决 H5 端和微信小程序端代码冲突的问题,我们创建了三个专门的上传组件: + +1. **DocumentUploadH5.vue** - 专门用于 H5 端的文档上传 +2. **DocumentUploadWeixin.vue** - 专门用于微信小程序端的文档上传 +3. **DocumentUploadSmart.vue** - 智能组件,自动检测平台并选择合适的组件 + +## 组件特点 + +### DocumentUploadH5.vue + +- ✅ **专门为 H5 端优化**:使用原生 HTML5 File API +- ✅ **严格按照后端接口**:完全匹配 `DocumentController.cs` 的上传接口 +- ✅ **FormData 正确构建**:确保文件大小和内容正确传递 +- ✅ **详细调试信息**:完整的日志记录,便于问题排查 +- ✅ **文件验证增强**:严格防止 0KB 文件上传 + +### DocumentUploadWeixin.vue + +- ✅ **专门为微信小程序端优化**:使用 `uni.chooseMessageFile` 和 `uni.uploadFile` +- ✅ **严格按照后端接口**:完全匹配 `DocumentController.cs` 的上传接口 +- ✅ **微信小程序兼容性**:支持多种文件选择方式 +- ✅ **错误处理完善**:详细的错误信息和用户提示 + +### DocumentUploadSmart.vue + +- ✅ **智能平台检测**:自动识别 H5、微信小程序、APP 等平台 +- ✅ **动态组件渲染**:根据平台自动选择合适的上传组件 +- ✅ **统一接口**:提供一致的 API 接口,便于父组件使用 +- ✅ **避免代码冲突**:完全分离不同平台的实现逻辑 + +## 使用方法 + +### 1. 在文档页面中使用智能组件 + +```vue + + + +``` + +### 2. 直接使用特定平台组件 + +如果需要针对特定平台进行定制,可以直接使用对应的组件: + +```vue + + + + + +``` + +## 后端接口对应 + +所有组件都严格按照后端 `DocumentController.cs` 的接口实现: + +```csharp +[HttpPost("upload")] +public async Task UploadDocument([FromForm] IFormFile file, [FromForm] string? fileName = null) +``` + +### H5 端实现 + +```javascript +const formData = new FormData(); +formData.append("file", file.originalFile, file.name); // 对应 [FromForm] IFormFile file +formData.append("fileName", file.name); // 对应 [FromForm] string? fileName = null +``` + +### 微信小程序端实现 + +```javascript +uni.uploadFile({ + url: `${API_BASE_URL}/Document/upload`, + filePath: file.path, + name: "file", // 对应 [FromForm] IFormFile file + formData: { + fileName: file.name, // 对应 [FromForm] string? fileName = null + }, +}); +``` + +## 主要优势 + +1. **代码分离**:H5 端和微信小程序端完全独立,避免冲突 +2. **接口一致**:所有组件都严格按照后端接口实现 +3. **平台优化**:针对不同平台使用最适合的 API +4. **错误处理**:完善的错误处理和用户提示 +5. **调试友好**:详细的日志记录,便于问题排查 +6. **易于维护**:模块化设计,便于后续维护和扩展 + +## 注意事项 + +1. **H5 端**:确保浏览器支持 File API,文件大小验证更严格 +2. **微信小程序端**:支持的文件类型和大小可能受平台限制 +3. **API 地址**:确保 `VITE_API_BASE_URL` 环境变量正确配置 +4. **认证 Token**:确保用户已登录,token 有效 +5. **文件类型**:支持 PDF、Word、TXT、MD 等文档格式 + +## 故障排除 + +### H5 端文件大小为 0KB + +- 检查 FormData 构建是否正确 +- 确认文件对象是否完整 +- 查看浏览器控制台的详细日志 + +### 微信小程序端上传失败 + +- 检查文件路径是否有效 +- 确认 API 地址是否正确 +- 验证 token 是否有效 + +### 通用问题 + +- 查看控制台日志信息 +- 确认网络连接正常 +- 验证后端服务是否正常运行 diff --git a/frontend/uniapp/src/api/index.js b/frontend/uniapp/src/api/index.js index 5803433..0a977fa 100644 --- a/frontend/uniapp/src/api/index.js +++ b/frontend/uniapp/src/api/index.js @@ -16,6 +16,17 @@ const clearToken = () => { uni.removeStorageSync('token'); }; +// 构建查询字符串(兼容小程序) +const buildQueryString = (params) => { + const queryParts = []; + for (const key in params) { + if (params[key] !== undefined && params[key] !== null && params[key] !== '') { + queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`); + } + } + return queryParts.length > 0 ? `?${queryParts.join('&')}` : ''; +}; + // 通用请求方法 const request = async (url, options = {}) => { const token = getToken(); @@ -88,102 +99,162 @@ export const authAPI = { // 文档管理API export const documentAPI = { - // 上传文档 - upload: (file, onProgress) => { + // 上传文档 - 完全按照后端DocumentController接口设计 + upload: (file, progressCallback = null, fileName = null) => { return new Promise((resolve, reject) => { - // H5环境使用FormData直接上传 - if (process.env.UNI_PLATFORM === 'h5') { - // 获取token - const token = getToken(); + const token = getToken(); + + if (!token) { + reject(new Error('未找到认证token,请先登录')); + return; + } + + // #ifdef H5 + // H5端:使用FormData和fetch + if (file.originalFile && file.originalFile instanceof File) { + // 验证文件 + if (file.originalFile.size === 0) { + reject(new Error('文件大小为0,请选择有效的文件')); + return; + } + + // 验证是否为文件夹 + if (file.originalFile.size === 0 && !file.originalFile.name.includes('.')) { + reject(new Error('不能上传文件夹,请选择文件')); + return; + } - // 创建FormData const formData = new FormData(); - formData.append('file', file.file || file, file.name); + formData.append('file', file.originalFile); - // 使用fetch上传 + // 如果提供了fileName,添加到FormData中 + if (fileName) { + formData.append('fileName', fileName); + } + + console.log('H5端上传 - 文件:', file.originalFile); + console.log('H5端上传 - 文件名:', fileName || file.name); + console.log('H5端上传 - FormData内容:'); + for (let [key, value] of formData.entries()) { + console.log(`${key}:`, value); + } + + // H5端模拟进度(因为fetch不支持进度) + if (progressCallback) { + let progress = 0; + const progressInterval = setInterval(() => { + progress += Math.random() * 20; + if (progress >= 100) { + progress = 100; + clearInterval(progressInterval); + } + progressCallback(progress); + }, 100); + } + fetch(`${API_BASE_URL}/document/upload`, { method: 'POST', headers: { - 'Authorization': `Bearer ${token}` + 'Authorization': `Bearer ${token}`, + // 不要手动设置Content-Type,让浏览器自动设置multipart/form-data的boundary }, - body: formData + body: formData, }) - .then(response => response.json()) - .then(data => { - if (data.success || data.code === 200 || data.code === 201 || data.id) { - resolve(data); + .then(response => { + console.log('上传响应状态:', response.status); + console.log('上传响应头:', response.headers); + + if (response.status === 201) { + // 成功:201 Created + return response.json(); + } else if (response.ok) { + // 其他成功状态码 + return response.json(); } else { - reject(new Error(data.message || data.error || '上传失败')); + // 失败状态码 + return response.text().then(text => { + throw new Error(`上传失败 (${response.status}): ${text}`); + }); } }) + .then(data => { + console.log('上传成功,返回数据:', data); + if (progressCallback) progressCallback(100); + resolve(data); + }) .catch(error => { - reject(new Error(error.message || '网络错误')); + console.error('H5端上传失败:', error); + reject(error); }); return; } + // #endif - const token = getToken(); - - // 获取文件路径(兼容不同平台) + // 非H5端或没有originalFile的情况:使用uni.uploadFile let filePath = file.path || file.tempFilePath || file.tempPath; - // 调试日志 - console.log('上传文件信息:', { - file, - filePath, - hasPath: !!file.path, - hasTempFilePath: !!file.tempFilePath, - hasTempPath: !!file.tempPath, - platform: process.env.UNI_PLATFORM - }); - if (!filePath) { - // 尝试从其他属性获取路径 - if (file.filePath) { - filePath = file.filePath; - } else if (typeof file === 'string') { - filePath = file; - } else { - reject(new Error(`文件路径无效,请检查文件选择方式。平台:${process.env.UNI_PLATFORM}`)); - return; - } + reject(new Error(`文件路径无效,请检查文件选择方式。平台:${process.env.UNI_PLATFORM}`)); + return; } + console.log('非H5端上传 - 文件路径:', filePath); + console.log('非H5端上传 - 文件名:', fileName || file.name); + const uploadTask = uni.uploadFile({ url: `${API_BASE_URL}/document/upload`, filePath: filePath, name: 'file', + formData: { + // 如果提供了fileName,添加到formData中 + ...(fileName && { fileName: fileName }) + }, header: { 'Authorization': `Bearer ${token}`, - 'Content-Type': 'multipart/form-data' + // 不要手动设置Content-Type,让uni-app自动设置 }, success: (res) => { - if (res.statusCode === 200 || res.statusCode === 201) { + console.log('uni.uploadFile响应:', res); + + if (res.statusCode === 201) { + // 成功:201 Created + try { + const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data; + if (progressCallback) progressCallback(100); + resolve(data); + } catch (e) { + reject(new Error('服务器响应格式错误')); + } + } else if (res.statusCode >= 200 && res.statusCode < 300) { + // 其他成功状态码 try { const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data; + if (progressCallback) progressCallback(100); resolve(data); } catch (e) { reject(new Error('服务器响应格式错误')); } } else { + // 失败状态码 try { const errorData = JSON.parse(res.data); - reject(new Error(errorData.message || '上传失败')); + reject(new Error(errorData.message || `上传失败 (${res.statusCode})`)); } catch (e) { - reject(new Error('上传失败')); + reject(new Error(`上传失败 (${res.statusCode}): ${res.data}`)); } } }, fail: (error) => { - console.error('上传失败:', error); + console.error('uni.uploadFile失败:', error); reject(new Error(error.errMsg || '网络错误')); } }); - // 监听上传进度 - if (onProgress && uploadTask) { + // 监听上传进度(微信小程序端) + if (uploadTask && uploadTask.onProgressUpdate && progressCallback) { uploadTask.onProgressUpdate((res) => { - onProgress(res.progress); + console.log('上传进度:', res.progress + '%'); + progressCallback(res.progress); }); } }); @@ -191,13 +262,13 @@ export const documentAPI = { // 获取文档列表 getList: (page = 1, pageSize = 10, keyword = '') => { - const params = new URLSearchParams({ + const params = { page: page.toString(), pageSize: pageSize.toString() - }); - if (keyword) params.append('keyword', keyword); + }; + if (keyword) params.keyword = keyword; - return request(`/document?${params.toString()}`); + return request(`/document${buildQueryString(params)}`); }, // 获取文档详情 @@ -221,6 +292,11 @@ export const documentAPI = { if (res.statusCode === 204) { // 204 No Content 表示删除成功 resolve({ success: true }); + } else if (res.statusCode === 401) { + // token过期,跳转登录 + clearToken(); + uni.reLaunch({ url: '/pages/login/index' }); + reject(new Error('认证失败,请重新登录')); } else if (res.statusCode === 404) { reject(new Error('文档不存在或无权限删除')); } else if (res.statusCode === 400) { @@ -240,12 +316,12 @@ export const documentAPI = { // 获取文档分块信息 getChunks: (id, page = 1, pageSize = 10) => { - const params = new URLSearchParams({ + const params = { page: page.toString(), pageSize: pageSize.toString() - }); + }; - return request(`/document/${id}/chunks?${params.toString()}`); + return request(`/document/${id}/chunks${buildQueryString(params)}`); }, // 重新索引文档 @@ -266,12 +342,12 @@ export const chatAPI = { // 获取对话列表 getConversations: (page = 1, pageSize = 10) => { - const params = new URLSearchParams({ + const params = { page: page.toString(), pageSize: pageSize.toString() - }); + }; - return request(`/chat/conversations?${params.toString()}`); + return request(`/chat/conversations${buildQueryString(params)}`); }, // 发送问题 @@ -303,6 +379,11 @@ export const chatAPI = { if (res.statusCode === 204 || res.statusCode === 200) { // 204 No Content 或 200 OK 都表示删除成功 resolve({ success: true }); + } else if (res.statusCode === 401) { + // token过期,跳转登录 + clearToken(); + uni.reLaunch({ url: '/pages/login/index' }); + reject(new Error('认证失败,请重新登录')); } else if (res.statusCode === 404) { reject(new Error('对话不存在或无权限删除')); } else if (res.statusCode === 400) { diff --git a/frontend/uniapp/src/components/DocumentUpload.vue b/frontend/uniapp/src/components/DocumentUploadH5.vue similarity index 49% rename from frontend/uniapp/src/components/DocumentUpload.vue rename to frontend/uniapp/src/components/DocumentUploadH5.vue index 7ff8f43..752f255 100644 --- a/frontend/uniapp/src/components/DocumentUpload.vue +++ b/frontend/uniapp/src/components/DocumentUploadH5.vue @@ -31,9 +31,8 @@ + + diff --git a/frontend/uniapp/src/components/DocumentUploadWeixin.vue b/frontend/uniapp/src/components/DocumentUploadWeixin.vue new file mode 100644 index 0000000..fce8c73 --- /dev/null +++ b/frontend/uniapp/src/components/DocumentUploadWeixin.vue @@ -0,0 +1,410 @@ + + + + + diff --git a/frontend/uniapp/src/manifest.json b/frontend/uniapp/src/manifest.json index ecbdcc9..ea9c353 100644 --- a/frontend/uniapp/src/manifest.json +++ b/frontend/uniapp/src/manifest.json @@ -15,7 +15,12 @@ "autoclose": true, "delay": 0 }, - "modules": {}, + "modules": { + "VideoPlayer": {}, + "OAuth": {}, + "Payment": {}, + "Share": {} + }, "distribute": { "android": { "permissions": [ @@ -31,16 +36,40 @@ "", "", "", - "" - ] + "", + "", + "", + "", + "", + "", + "" + ], + "abiFilters": ["armeabi-v7a", "arm64-v8a"], + "minSdkVersion": 21, + "targetSdkVersion": 30 + }, + "ios": { + "privacyDescription": { + "NSPhotoLibraryUsageDescription": "此应用需要访问相册来选择图片", + "NSCameraUsageDescription": "此应用需要访问相机来拍照", + "NSMicrophoneUsageDescription": "此应用需要访问麦克风来录音", + "NSLocationWhenInUseUsageDescription": "此应用需要访问位置信息" + } }, - "ios": {}, - "sdkConfigs": {} + "sdkConfigs": { + "ad": {}, + "maps": {}, + "oauth": {}, + "payment": {}, + "share": {}, + "speech": {}, + "video": {} + } } }, "quickapp": {}, "mp-weixin": { - "appid": "", + "appid": "wxb2217ad686192c30", "setting": { "urlCheck": false, "es6": true, @@ -79,7 +108,15 @@ "showES6CompileOption": false, "useCompilerPlugins": false }, - "usingComponents": true + "usingComponents": true, + "permission": { + "scope.userLocation": { + "desc": "你的位置信息将用于小程序位置接口的效果展示" + } + }, + "requiredPrivateInfos": [ + "chooseLocation" + ] }, "mp-alipay": { "usingComponents": true diff --git a/frontend/uniapp/src/pages/documents/detail.vue b/frontend/uniapp/src/pages/documents/detail.vue index a466c60..c4f04cb 100644 --- a/frontend/uniapp/src/pages/documents/detail.vue +++ b/frontend/uniapp/src/pages/documents/detail.vue @@ -189,20 +189,25 @@ const loadDocumentDetail = async () => { uni.showLoading({ title: '加载中...' }) const response = await documentAPI.getDetail(props.id) + console.log('文档详情API响应:', response) + // 检查不同的响应格式 const documentData = response.data || response if (documentData && documentData.id) { - document.value = { + const processedDocument = { ...documentData, - fileName: documentData.title, + fileName: documentData.title || documentData.Title, fileSize: documentData.content ? documentData.content.length : 0, - uploadedAt: documentData.createdAt, + uploadedAt: documentData.createdAt || documentData.CreatedAt, status: documentData.errorMessage ? 'error' : 'success', - chunkCount: documentData.chunksCount || 0, + chunkCount: documentData.chunksCount || documentData.ChunksCount || 0, formattedSize: formatFileSize(documentData.content ? documentData.content.length : 0), - formattedDate: formatDateTime(documentData.createdAt) + formattedDate: formatDateTime(documentData.createdAt || documentData.CreatedAt) } + + console.log('处理后的文档数据:', processedDocument) + document.value = processedDocument } } catch (error) { console.error('加载文档详情失败:', error) @@ -228,10 +233,12 @@ const loadChunks = async (refresh = false) => { loading.value = true const response = await documentAPI.getChunks(props.id, page.value, pageSize.value) + console.log('分块API响应:', response) + // 检查不同的响应格式 const chunksData = response.data || response - const items = chunksData.items || chunksData.Items || [] - const totalCount = chunksData.totalCount || chunksData.TotalCount || 0 + const items = chunksData.items || chunksData.Items || chunksData.data || [] + const totalCount = chunksData.totalCount || chunksData.TotalCount || chunksData.total || 0 if (items.length > 0 || totalCount > 0) { const formattedItems = items.map(chunk => ({ diff --git a/frontend/uniapp/src/pages/documents/index.vue b/frontend/uniapp/src/pages/documents/index.vue index 2724bae..76fc5ea 100644 --- a/frontend/uniapp/src/pages/documents/index.vue +++ b/frontend/uniapp/src/pages/documents/index.vue @@ -106,21 +106,15 @@ - - {{ - selectedFile.name - }} - + + @@ -205,6 +199,7 @@ import { formatDateTime, truncateText, } from "@/utils/document.js"; +import DocumentUploadSmart from "@/components/DocumentUploadSmart.vue"; // 响应式数据 const documents = ref([]); @@ -337,114 +332,26 @@ const hideUploadDialog = () => { uploadProgress.value = 0; }; -const selectFile = async () => { - try { - // #ifdef H5 - const input = document.createElement("input"); - input.type = "file"; - input.accept = ".pdf,.doc,.docx,.txt,.md"; - input.onchange = (e) => { - if (e.target.files && e.target.files.length > 0) { - const file = e.target.files[0]; - // H5环境下需要创建兼容的对象 - const compatibleFile = { - name: file.name, - size: file.size, - type: file.type, - path: file.name, // H5使用文件名作为标识 - file: file, // 保存原始文件对象,供API使用 - }; - console.log("H5文件选择结果:", compatibleFile); - selectedFile.value = compatibleFile; - } - }; - input.click(); - // #endif - - // #ifdef MP-WEIXIN || APP-PLUS - const res = await uni.chooseFile({ - count: 1, - type: "all", - extension: [".pdf", ".doc", ".docx", ".txt", ".md"], - }); - if (res.tempFiles && res.tempFiles.length > 0) { - selectedFile.value = res.tempFiles[0]; - } - // #endif - } catch (error) { - console.error("选择文件失败:", error); - } +const handleUploadSuccess = (file) => { + console.log("上传成功:", file); + uni.showToast({ + title: "上传成功", + icon: "success", + }); + hideUploadDialog(); + loadDocuments(true); }; -const uploadFile = async () => { - if (!selectedFile.value) return; - - try { - uploading.value = true; - uploadProgress.value = 0; - - console.log("开始上传文件:", selectedFile.value); - - // #ifdef H5 - // H5环境使用FormData直接上传 - const formData = new FormData(); - formData.append( - "file", - selectedFile.value.file || selectedFile.value, - selectedFile.value.name - ); - - const token = uni.getStorageSync("token"); - const API_BASE_URL = - import.meta.env.VITE_API_BASE_URL || "http://localhost:5292/api/v1"; - - const fetchResponse = await fetch(`${API_BASE_URL}/document/upload`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, - body: formData, - }); +const handleUploadError = (error) => { + console.error("上传失败:", error); + uni.showToast({ + title: `上传失败: ${error.message}`, + icon: "none", + }); +}; - const response = await fetchResponse.json(); - if (!fetchResponse.ok) { - throw new Error(response.message || response.error || "上传失败"); - } - // #endif - - // #ifndef H5 - // 小程序/APP使用原生上传 - const response = await documentAPI.upload(selectedFile.value); - // #endif - - console.log("上传响应:", response); - - // 检查不同的成功响应格式 - // 如果响应包含文档ID,说明上传成功 - if ( - response.success || - response.code === 200 || - response.code === 201 || - response.id - ) { - uni.showToast({ - title: "上传成功", - icon: "success", - }); - hideUploadDialog(); - loadDocuments(true); - } else { - throw new Error(response.message || response.error || "上传失败"); - } - } catch (error) { - console.error("上传失败:", error); - uni.showToast({ - title: `上传失败: ${error.message}`, - icon: "none", - }); - } finally { - uploading.value = false; - } +const handleUploadProgress = (progress) => { + uploadProgress.value = progress; }; // 文档操作 diff --git a/frontend/uniapp/src/pages/index/index.vue b/frontend/uniapp/src/pages/index/index.vue index be4d05a..2b3b4aa 100644 --- a/frontend/uniapp/src/pages/index/index.vue +++ b/frontend/uniapp/src/pages/index/index.vue @@ -62,6 +62,17 @@ 对话历史 查看之前的对话记录 + + + + + + + + + 上传文档 + 上传文档进行智能分析 + @@ -251,6 +262,13 @@ import { ref, reactive, onMounted, nextTick } from "vue"; import { chatAPI, documentAPI } from "@/api/index"; import { useUserStore } from "@/stores/user"; +// API基础URL - 确保URL格式完全正确 +const API_BASE_URL = "http://localhost:5292/api/v1"; + +// 调试:打印API基础URL +console.log("API_BASE_URL:", API_BASE_URL); +console.log("环境变量:", import.meta.env); + // 响应式数据 const showChat = ref(false); const loading = ref(false); @@ -467,50 +485,204 @@ const selectFile = async () => { input.accept = ".pdf,.doc,.docx,.txt,.md"; input.onchange = (e) => { if (e.target.files && e.target.files.length > 0) { - selectedFile.value = e.target.files[0]; + const file = e.target.files[0]; + console.log("H5文件选择结果:", { + name: file.name, + size: file.size, + type: file.type, + // 不要使用file作为path,这可能导致错误的URL生成 + path: file.name, // 只使用文件名作为路径 + tempFilePath: file.name, + // 添加原始File对象引用 + originalFile: file, + }); + + // 为H5端创建兼容的文件对象 + selectedFile.value = { + name: file.name, + size: file.size, + type: file.type, + // 使用文件名作为路径,避免生成错误的URL + path: file.name, + tempFilePath: file.name, + // 添加原始File对象引用 + originalFile: file, + }; } }; input.click(); // #endif // #ifdef MP-WEIXIN || APP-PLUS - const res = await uni.chooseFile({ - count: 1, - type: "all", - extension: [".pdf", ".doc", ".docx", ".txt", ".md"], - }); - if (res.tempFiles && res.tempFiles.length > 0) { - selectedFile.value = res.tempFiles[0]; + try { + const res = await uni.chooseMessageFile({ + count: 1, + type: "file", + extension: ["pdf", "doc", "docx", "txt", "md"], + }); + if (res.tempFiles && res.tempFiles.length > 0) { + const file = res.tempFiles[0]; + console.log("微信小程序文件选择结果:", file); + + // 为微信小程序端创建兼容的文件对象 + // 微信小程序的文件对象需要特殊处理,确保有正确的文件名 + selectedFile.value = { + name: + file.name || file.path?.split("/").pop() || `文件_${Date.now()}`, + size: file.size, + type: file.type || "application/octet-stream", + path: file.path, + tempFilePath: file.path, + // 微信小程序特有的属性 + originalFile: file, + }; + + console.log("处理后的文件对象:", selectedFile.value); + } + } catch (chooseError) { + console.log("chooseMessageFile失败,尝试chooseFile:", chooseError); + // 如果chooseMessageFile失败,尝试使用chooseFile + const res = await uni.chooseFile({ + count: 1, + type: "all", + extension: [".pdf", ".doc", ".docx", ".txt", ".md"], + }); + if (res.tempFiles && res.tempFiles.length > 0) { + const file = res.tempFiles[0]; + console.log("微信小程序chooseFile结果:", file); + + // 为微信小程序端创建兼容的文件对象 + selectedFile.value = { + name: + file.name || file.path?.split("/").pop() || `文件_${Date.now()}`, + size: file.size, + type: file.type || "application/octet-stream", + path: file.path, + tempFilePath: file.path, + // 微信小程序特有的属性 + originalFile: file, + }; + + console.log("处理后的文件对象:", selectedFile.value); + } } // #endif } catch (error) { console.error("选择文件失败:", error); + uni.showToast({ + title: "选择文件失败", + icon: "none", + }); } }; -// 上传文件 +// 上传文件 - 完全按照后端DocumentController接口设计 const uploadFile = async () => { if (!selectedFile.value) return; try { uploading.value = true; + console.log("开始上传文件:", selectedFile.value); - const response = await documentAPI.upload(selectedFile.value); + // 检查用户登录状态 + if (!userStore.isLoggedIn()) { + uploading.value = false; + uni.showModal({ + title: "需要登录", + content: "上传文档需要登录账号,请先登录", + confirmText: "去登录", + cancelText: "取消", + success: (res) => { + if (res.confirm) { + uni.navigateTo({ + url: "/pages/login/index", + }); + } + }, + }); + return; + } + + // 检查token是否存在 + const token = uni.getStorageSync("token"); + if (!token) { + uploading.value = false; + uni.showToast({ + title: "认证失败,请重新登录", + icon: "none", + }); + return; + } + + console.log("用户已登录,Token:", token); + console.log("用户信息:", userInfo.value); + console.log("准备上传文件:", { + name: selectedFile.value.name, + size: selectedFile.value.size, + type: selectedFile.value.type, + hasOriginalFile: !!selectedFile.value.originalFile, + }); + + // 调用documentAPI.upload方法 + // 传递文件名作为第二个参数(对应后端的fileName参数) + // 确保文件名正确,微信小程序端可能需要特殊处理 + const fileName = + selectedFile.value.name || + selectedFile.value.path?.split("/").pop() || + `文件_${Date.now()}`; + console.log("最终使用的文件名:", fileName); + + const response = await documentAPI.upload(selectedFile.value, fileName); + + console.log("上传成功,返回结果:", response); + + // 检查返回结果 - 后端返回的是文档对象,包含id字段 + if (response && (response.id || response.Id)) { + uni.showToast({ + title: "上传成功", + icon: "success", + duration: 2000, + }); - if (response.success) { + // 延迟关闭对话框,让用户看到成功提示 + setTimeout(() => { + hideUploadDialog(); + selectedFile.value = null; + }, 1500); + } else { + console.warn("返回数据结构不符合预期:", response); + // 即使数据结构不符合预期,如果API调用成功,仍然认为上传成功 uni.showToast({ title: "上传成功", icon: "success", + duration: 2000, }); - // 关闭对话框 - hideUploadDialog(); + setTimeout(() => { + hideUploadDialog(); + selectedFile.value = null; + }, 1500); } } catch (error) { console.error("上传失败:", error); + + let errorTitle = "上传失败,请重试"; + + if (typeof error === "string") { + errorTitle = error; + } else if (error && error.message) { + errorTitle = error.message.replace(/^Error:\s*/i, ""); + } + + // 限制错误信息长度 + if (errorTitle.length > 50) { + errorTitle = errorTitle.substring(0, 50) + "..."; + } + uni.showToast({ - title: "上传失败,请重试", + title: errorTitle, icon: "none", + duration: 3000, }); } finally { uploading.value = false; @@ -532,6 +704,7 @@ const formatTime = (time) => { diff --git a/frontend/uniapp/src/utils/document.js b/frontend/uniapp/src/utils/document.js index e1aa953..3e06262 100644 --- a/frontend/uniapp/src/utils/document.js +++ b/frontend/uniapp/src/utils/document.js @@ -167,6 +167,20 @@ export function formatRelativeTime(date) { */ export function truncateText(text, maxLength = 50) { if (!text) return '' + + // 对于微信小程序,优先保留文件扩展名 + if (text.includes('.')) { + const lastDotIndex = text.lastIndexOf('.') + const name = text.substring(0, lastDotIndex) + const ext = text.substring(lastDotIndex) + + if (name.length > maxLength - 3) { // 为扩展名和省略号留出空间 + return name.substring(0, maxLength - 3) + '...' + ext + } + return text + } + + // 没有扩展名的情况 return text.length > maxLength ? text.substring(0, maxLength) + '...' : text } -- Gitee