7.29 修改前端页面展示
This commit is contained in:
parent
1ea68cb74f
commit
81b71d0d07
@ -9,7 +9,7 @@ public interface IRAGService {
|
|||||||
|
|
||||||
Response<List<String>> queryRagTagList();
|
Response<List<String>> queryRagTagList();
|
||||||
|
|
||||||
Response<String> uploadFile(String ragTag, List<MultipartFile> files);
|
Response<String> uploadFile(String ragTag, List<MultipartFile> files, List<String> filePaths);
|
||||||
|
|
||||||
Response<String> analyzeGitRepository(String repoUrl, String userName, String token) throws Exception;
|
Response<String> analyzeGitRepository(String repoUrl, String userName, String token) throws Exception;
|
||||||
|
|
||||||
|
@ -80,18 +80,42 @@ public class RAGController implements IRAGService {
|
|||||||
@Override
|
@Override
|
||||||
public Response<String> uploadFile(
|
public Response<String> uploadFile(
|
||||||
@RequestParam("ragTag") String ragTag,
|
@RequestParam("ragTag") String ragTag,
|
||||||
@RequestParam("file") List<MultipartFile> files) {
|
@RequestParam("file") List<MultipartFile> files,
|
||||||
|
@RequestParam(value = "filePath", required = false) List<String> filePaths ) {
|
||||||
|
|
||||||
log.info("上传知识库开始:{}", ragTag);
|
log.info("上传知识库开始:{}", ragTag);
|
||||||
for (MultipartFile file : files) {
|
|
||||||
|
// 使用带索引的 for 循环,保证与 filePath 一一对应
|
||||||
|
for (int i = 0; i < files.size(); i++) {
|
||||||
|
MultipartFile file = files.get(i);
|
||||||
|
|
||||||
|
// ===== 新增:相对路径(与前端传入顺序一致;缺失时回退到原始文件名)=====
|
||||||
|
final String relPath =
|
||||||
|
(filePaths != null && i < filePaths.size() && filePaths.get(i) != null && !filePaths.get(i).isBlank())
|
||||||
|
? filePaths.get(i)
|
||||||
|
: (file.getOriginalFilename() != null ? file.getOriginalFilename() : "");
|
||||||
|
|
||||||
|
// 可选:调试日志
|
||||||
|
log.debug("接收文件:{},相对路径:{}", file.getOriginalFilename(), relPath);
|
||||||
|
|
||||||
// 读取上传文件,提取文档内容
|
// 读取上传文件,提取文档内容
|
||||||
TikaDocumentReader documentReader = new TikaDocumentReader(file.getResource());
|
TikaDocumentReader documentReader = new TikaDocumentReader(file.getResource());
|
||||||
List<Document> documents = documentReader.get();
|
List<Document> documents = documentReader.get();
|
||||||
|
|
||||||
// 对文档进行 Token 拆分
|
// 对文档进行 Token 拆分
|
||||||
List<Document> documentSplitterList = tokenTextSplitter.apply(documents);
|
List<Document> documentSplitterList = tokenTextSplitter.apply(documents);
|
||||||
|
|
||||||
// 为原文档和拆分文档设置 ragTag 元数据
|
// 为原文档和拆分文档设置 ragTag 元数据
|
||||||
documents.forEach(doc -> doc.getMetadata().put("knowledge", ragTag));
|
documents.forEach(doc -> {
|
||||||
documentSplitterList.forEach(doc -> doc.getMetadata().put("knowledge", ragTag));
|
doc.getMetadata().put("knowledge", ragTag);
|
||||||
|
// ===== 新增:写入相对路径 =====
|
||||||
|
doc.getMetadata().put("path", relPath);
|
||||||
|
});
|
||||||
|
documentSplitterList.forEach(doc -> {
|
||||||
|
doc.getMetadata().put("knowledge", ragTag);
|
||||||
|
// ===== 新增:写入相对路径 =====
|
||||||
|
doc.getMetadata().put("path", relPath);
|
||||||
|
});
|
||||||
|
|
||||||
// 存储拆分后的文档到 pgVectorStore
|
// 存储拆分后的文档到 pgVectorStore
|
||||||
pgVectorStore.accept(documentSplitterList);
|
pgVectorStore.accept(documentSplitterList);
|
||||||
@ -102,10 +126,13 @@ public class RAGController implements IRAGService {
|
|||||||
elements.add(ragTag);
|
elements.add(ragTag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("上传知识库完成:{}", ragTag);
|
log.info("上传知识库完成:{}", ragTag);
|
||||||
return Response.<String>builder().code("0000").info("调用成功").build();
|
return Response.<String>builder().code("0000").info("调用成功").build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 克隆并分析 Git 仓库:
|
* 克隆并分析 Git 仓库:
|
||||||
* - 克隆指定仓库到本地
|
* - 克隆指定仓库到本地
|
||||||
|
@ -164,3 +164,66 @@
|
|||||||
25-07-29.09:53:05.853 [http-nio-8095-exec-5] INFO RAGController - 上传知识库开始:草稿1
|
25-07-29.09:53:05.853 [http-nio-8095-exec-5] INFO RAGController - 上传知识库开始:草稿1
|
||||||
25-07-29.09:53:06.647 [http-nio-8095-exec-5] INFO RAGController - 上传知识库完成:草稿1
|
25-07-29.09:53:06.647 [http-nio-8095-exec-5] INFO RAGController - 上传知识库完成:草稿1
|
||||||
25-07-29.09:53:30.890 [http-nio-8095-exec-7] INFO OllamaController - generate_stream_rag called!
|
25-07-29.09:53:30.890 [http-nio-8095-exec-7] INFO OllamaController - generate_stream_rag called!
|
||||||
|
25-07-29.10:26:15.821 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown initiated...
|
||||||
|
25-07-29.10:26:15.826 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown completed.
|
||||||
|
25-07-29.10:27:07.258 [main ] INFO Application - Starting Application v1.0 using Java 17.0.2 with PID 1 (/app.jar started by root in /)
|
||||||
|
25-07-29.10:27:07.263 [main ] INFO Application - The following 1 profile is active: "dev"
|
||||||
|
25-07-29.10:27:08.167 [main ] INFO RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||||
|
25-07-29.10:27:08.171 [main ] INFO RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
||||||
|
25-07-29.10:27:08.202 [main ] INFO RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 14 ms. Found 0 Redis repository interfaces.
|
||||||
|
25-07-29.10:27:08.986 [main ] INFO TomcatWebServer - Tomcat initialized with port 8095 (http)
|
||||||
|
25-07-29.10:27:08.998 [main ] INFO Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8095"]
|
||||||
|
25-07-29.10:27:09.000 [main ] INFO StandardService - Starting service [Tomcat]
|
||||||
|
25-07-29.10:27:09.000 [main ] INFO StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.19]
|
||||||
|
25-07-29.10:27:09.047 [main ] INFO [/] - Initializing Spring embedded WebApplicationContext
|
||||||
|
25-07-29.10:27:09.048 [main ] INFO ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 1672 ms
|
||||||
|
25-07-29.10:27:09.446 [main ] INFO HikariDataSource - HikariCP - Starting...
|
||||||
|
25-07-29.10:27:09.630 [main ] INFO HikariPool - HikariCP - Added connection org.postgresql.jdbc.PgConnection@374b6e33
|
||||||
|
25-07-29.10:27:09.632 [main ] INFO HikariDataSource - HikariCP - Start completed.
|
||||||
|
25-07-29.10:27:10.270 [main ] INFO Version - Redisson 3.44.0
|
||||||
|
25-07-29.10:27:10.505 [redisson-netty-1-4] INFO ConnectionsHolder - 1 connections initialized for 192.168.10.218/192.168.10.218:26379
|
||||||
|
25-07-29.10:27:10.546 [redisson-netty-1-13] INFO ConnectionsHolder - 5 connections initialized for 192.168.10.218/192.168.10.218:26379
|
||||||
|
25-07-29.10:27:11.294 [main ] INFO Http11NioProtocol - Starting ProtocolHandler ["http-nio-8095"]
|
||||||
|
25-07-29.10:27:11.308 [main ] INFO TomcatWebServer - Tomcat started on port 8095 (http) with context path ''
|
||||||
|
25-07-29.10:27:11.324 [main ] INFO Application - Started Application in 4.858 seconds (process running for 5.691)
|
||||||
|
25-07-29.10:27:27.925 [http-nio-8095-exec-1] INFO [/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
|
||||||
|
25-07-29.10:27:27.925 [http-nio-8095-exec-1] INFO DispatcherServlet - Initializing Servlet 'dispatcherServlet'
|
||||||
|
25-07-29.10:27:27.927 [http-nio-8095-exec-1] INFO DispatcherServlet - Completed initialization in 1 ms
|
||||||
|
25-07-29.10:27:27.977 [http-nio-8095-exec-1] INFO RAGController - 上传知识库开始:测试01
|
||||||
|
25-07-29.10:27:28.578 [http-nio-8095-exec-1] INFO TextSplitter - Splitting up document into 2 chunks.
|
||||||
|
25-07-29.10:27:29.993 [http-nio-8095-exec-1] INFO TextSplitter - Splitting up document into 15 chunks.
|
||||||
|
25-07-29.10:27:43.442 [http-nio-8095-exec-1] INFO TextSplitter - Splitting up document into 6 chunks.
|
||||||
|
25-07-29.10:27:48.303 [http-nio-8095-exec-1] INFO TextSplitter - Splitting up document into 13 chunks.
|
||||||
|
25-07-29.10:27:59.867 [http-nio-8095-exec-1] INFO TextSplitter - Splitting up document into 5 chunks.
|
||||||
|
25-07-29.10:28:03.851 [http-nio-8095-exec-1] INFO TextSplitter - Splitting up document into 18 chunks.
|
||||||
|
25-07-29.10:28:20.184 [http-nio-8095-exec-1] INFO TextSplitter - Splitting up document into 2 chunks.
|
||||||
|
25-07-29.10:28:22.057 [http-nio-8095-exec-1] INFO RAGController - 上传知识库完成:测试01
|
||||||
|
25-07-29.10:37:27.151 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown initiated...
|
||||||
|
25-07-29.10:37:27.153 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown completed.
|
||||||
|
25-07-29.10:37:37.830 [main ] INFO Application - Starting Application v1.0 using Java 17.0.2 with PID 1 (/app.jar started by root in /)
|
||||||
|
25-07-29.10:37:37.835 [main ] INFO Application - The following 1 profile is active: "dev"
|
||||||
|
25-07-29.10:37:38.660 [main ] INFO RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||||
|
25-07-29.10:37:38.662 [main ] INFO RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
||||||
|
25-07-29.10:37:38.689 [main ] INFO RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 11 ms. Found 0 Redis repository interfaces.
|
||||||
|
25-07-29.10:37:39.369 [main ] INFO TomcatWebServer - Tomcat initialized with port 8095 (http)
|
||||||
|
25-07-29.10:37:39.378 [main ] INFO Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8095"]
|
||||||
|
25-07-29.10:37:39.380 [main ] INFO StandardService - Starting service [Tomcat]
|
||||||
|
25-07-29.10:37:39.380 [main ] INFO StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.19]
|
||||||
|
25-07-29.10:37:39.416 [main ] INFO [/] - Initializing Spring embedded WebApplicationContext
|
||||||
|
25-07-29.10:37:39.416 [main ] INFO ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 1477 ms
|
||||||
|
25-07-29.10:37:39.774 [main ] INFO HikariDataSource - HikariCP - Starting...
|
||||||
|
25-07-29.10:37:39.967 [main ] INFO HikariPool - HikariCP - Added connection org.postgresql.jdbc.PgConnection@cdbe995
|
||||||
|
25-07-29.10:37:39.969 [main ] INFO HikariDataSource - HikariCP - Start completed.
|
||||||
|
25-07-29.10:37:42.629 [main ] INFO Version - Redisson 3.44.0
|
||||||
|
25-07-29.10:37:42.905 [redisson-netty-1-4] INFO ConnectionsHolder - 1 connections initialized for 192.168.10.218/192.168.10.218:26379
|
||||||
|
25-07-29.10:37:42.940 [redisson-netty-1-13] INFO ConnectionsHolder - 5 connections initialized for 192.168.10.218/192.168.10.218:26379
|
||||||
|
25-07-29.10:37:43.452 [main ] INFO Http11NioProtocol - Starting ProtocolHandler ["http-nio-8095"]
|
||||||
|
25-07-29.10:37:43.460 [main ] INFO TomcatWebServer - Tomcat started on port 8095 (http) with context path ''
|
||||||
|
25-07-29.10:37:43.468 [main ] INFO Application - Started Application in 6.441 seconds (process running for 7.065)
|
||||||
|
25-07-29.10:37:55.291 [http-nio-8095-exec-1] INFO [/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
|
||||||
|
25-07-29.10:37:55.291 [http-nio-8095-exec-1] INFO DispatcherServlet - Initializing Servlet 'dispatcherServlet'
|
||||||
|
25-07-29.10:37:55.292 [http-nio-8095-exec-1] INFO DispatcherServlet - Completed initialization in 1 ms
|
||||||
|
25-07-29.10:37:55.337 [http-nio-8095-exec-1] INFO RAGController - 上传知识库开始:测试02
|
||||||
|
25-07-29.10:37:55.992 [http-nio-8095-exec-1] INFO RAGController - 上传知识库完成:测试02
|
||||||
|
25-07-29.10:49:25.780 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown initiated...
|
||||||
|
25-07-29.10:49:25.783 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown completed.
|
||||||
|
52
docs/tag/v1.0/nginx/html/css/upload.css
Normal file
52
docs/tag/v1.0/nginx/html/css/upload.css
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/* 旋转动画(loading 图标) */
|
||||||
|
.loader {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg) }
|
||||||
|
100% { transform: rotate(360deg) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖拽高亮:与 Tailwind 共存,覆盖边框与背景 */
|
||||||
|
.drag-zone.drag-active {
|
||||||
|
border-color: #2563eb; /* blue-600 */
|
||||||
|
background-color: #eff6ff; /* blue-50 */
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文件列表样式微调 */
|
||||||
|
#fileList li {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#fileList .file-name {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #111827; /* gray-900 */
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
#fileList .file-meta {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #6b7280; /* gray-500 */
|
||||||
|
}
|
||||||
|
#fileList .remove-btn {
|
||||||
|
color: #ef4444; /* red-500 */
|
||||||
|
}
|
||||||
|
#fileList .remove-btn:hover {
|
||||||
|
color: #b91c1c; /* red-700 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏但保留可访问性的类(配合HTML中的 sr-only)*/
|
||||||
|
.sr-only {
|
||||||
|
position: absolute !important;
|
||||||
|
width: 1px !important;
|
||||||
|
height: 1px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: -1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
clip: rect(0,0,0,0) !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
// ===== index.js (modified to auto-refresh RAG list after upload) =====
|
||||||
|
|
||||||
const chatArea = document.getElementById('chatArea');
|
const chatArea = document.getElementById('chatArea');
|
||||||
const messageInput = document.getElementById('messageInput');
|
const messageInput = document.getElementById('messageInput');
|
||||||
const submitBtn = document.getElementById('submitBtn');
|
const submitBtn = document.getElementById('submitBtn');
|
||||||
@ -9,37 +11,82 @@ const sidebar = document.getElementById('sidebar');
|
|||||||
let currentEventSource = null;
|
let currentEventSource = null;
|
||||||
let currentChatId = null;
|
let currentChatId = null;
|
||||||
|
|
||||||
// 获取知识库列表
|
/** -------------------------
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
* RAG 列表加载(提升为顶层可复用)
|
||||||
// 获取知识库列表
|
* 支持可选预选值 preselectTag
|
||||||
const loadRagOptions = () => {
|
* ------------------------- */
|
||||||
const ragSelect = document.getElementById('ragSelect');
|
function loadRagOptions(preselectTag) {
|
||||||
|
const ragSelect = document.getElementById('ragSelect');
|
||||||
|
|
||||||
fetch('/api/v1/rag/query_rag_tag_list')
|
return fetch('/api/v1/rag/query_rag_tag_list')
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.code === '0000' && data.data) {
|
if (data.code === '0000' && data.data) {
|
||||||
// 清空现有选项(保留第一个默认选项)
|
// 清空现有选项(保留第一个默认选项)
|
||||||
while (ragSelect.options.length > 1) {
|
while (ragSelect.options.length > 1) {
|
||||||
ragSelect.remove(1);
|
ragSelect.remove(1);
|
||||||
}
|
|
||||||
|
|
||||||
// 添加新选项
|
|
||||||
data.data.forEach(tag => {
|
|
||||||
const option = new Option(`Rag:${tag}`, tag);
|
|
||||||
ragSelect.add(option);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
// 添加新选项
|
||||||
.catch(error => {
|
data.data.forEach(tag => {
|
||||||
console.error('获取知识库列表失败:', error);
|
const option = new Option(`Rag:${tag}`, tag);
|
||||||
});
|
ragSelect.add(option);
|
||||||
};
|
});
|
||||||
|
|
||||||
// 初始化加载
|
// 可选:预选刚创建的 ragTag
|
||||||
|
if (preselectTag) {
|
||||||
|
const exists = Array.from(ragSelect.options).some(o => o.value === preselectTag);
|
||||||
|
if (!exists) {
|
||||||
|
ragSelect.add(new Option(`Rag:${preselectTag}`, preselectTag));
|
||||||
|
}
|
||||||
|
ragSelect.value = preselectTag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('获取知识库列表失败:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取知识库列表(首屏)
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
loadRagOptions();
|
loadRagOptions();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** -------------------------
|
||||||
|
* 接收来自 upload.html 的刷新通知
|
||||||
|
* 方式 A:postMessage
|
||||||
|
* 方式 B:BroadcastChannel
|
||||||
|
* 兜底:页面从后台切回前台自动刷新
|
||||||
|
* ------------------------- */
|
||||||
|
// A. postMessage(当 upload.html 有 opener 时)
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
if (event.origin !== window.location.origin) return;
|
||||||
|
const msg = event.data;
|
||||||
|
if (msg && msg.type === 'RAG_LIST_REFRESH') {
|
||||||
|
loadRagOptions(msg.ragTag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// B. BroadcastChannel(不依赖 opener,现代浏览器有效)
|
||||||
|
if ('BroadcastChannel' in window) {
|
||||||
|
const bc = new BroadcastChannel('rag-updates');
|
||||||
|
bc.addEventListener('message', (event) => {
|
||||||
|
const msg = event.data;
|
||||||
|
if (msg && msg.type === 'rag:updated') {
|
||||||
|
loadRagOptions(msg.ragTag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// C. 兜底:页面重新获得焦点/可见时刷新一次
|
||||||
|
window.addEventListener('focus', () => loadRagOptions());
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (!document.hidden) loadRagOptions();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** -------------------------
|
||||||
|
* 聊天逻辑
|
||||||
|
* ------------------------- */
|
||||||
function createNewChat() {
|
function createNewChat() {
|
||||||
const chatId = Date.now().toString();
|
const chatId = Date.now().toString();
|
||||||
currentChatId = chatId;
|
currentChatId = chatId;
|
||||||
@ -55,21 +102,20 @@ function createNewChat() {
|
|||||||
|
|
||||||
function deleteChat(chatId) {
|
function deleteChat(chatId) {
|
||||||
if (confirm('确定要删除这个聊天记录吗?')) {
|
if (confirm('确定要删除这个聊天记录吗?')) {
|
||||||
localStorage.removeItem(`chat_${chatId}`); // Remove the chat from localStorage
|
localStorage.removeItem(`chat_${chatId}`);
|
||||||
if (currentChatId === chatId) { // If the current chat is being deleted
|
if (currentChatId === chatId) {
|
||||||
createNewChat(); // Create a new chat
|
createNewChat();
|
||||||
}
|
}
|
||||||
updateChatList(); // Update the chat list to reflect changes
|
updateChatList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateChatList() {
|
function updateChatList() {
|
||||||
chatList.innerHTML = '';
|
chatList.innerHTML = '';
|
||||||
const chats = Object.keys(localStorage)
|
const chats = Object.keys(localStorage).filter(key => key.startsWith('chat_'));
|
||||||
.filter(key => key.startsWith('chat_'));
|
|
||||||
|
|
||||||
const currentChatIndex = chats.findIndex(key => key.split('_')[1] === currentChatId);
|
const currentChatIndex = chats.findIndex(key => key.split('_')[1] === currentChatId);
|
||||||
if (currentChatIndex!== -1) {
|
if (currentChatIndex !== -1) {
|
||||||
const currentChat = chats[currentChatIndex];
|
const currentChat = chats[currentChatIndex];
|
||||||
chats.splice(currentChatIndex, 1);
|
chats.splice(currentChatIndex, 1);
|
||||||
chats.unshift(currentChat);
|
chats.unshift(currentChat);
|
||||||
@ -89,17 +135,17 @@ function updateChatList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = `chat-item flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg cursor-pointer transition-colors ${chatId === currentChatId? 'bg-blue-50' : ''}`;
|
li.className = `chat-item flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg cursor-pointer transition-colors ${chatId === currentChatId ? 'bg-blue-50' : ''}`;
|
||||||
li.innerHTML = `
|
li.innerHTML = `
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-sm font-medium">${chatData.name}</div>
|
<div class="text-sm font-medium">${chatData.name}</div>
|
||||||
<div class="text-xs text-gray-400">${new Date(parseInt(chatId)).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })}</div>
|
<div class="text-xs text-gray-400">${new Date(parseInt(chatId)).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-actions flex items-center gap-1 opacity-0 transition-opacity duration-200">
|
<div class="chat-actions flex items-center gap-1 opacity-0 transition-opacity duration-200">
|
||||||
<button class="p-1 hover:bg-gray-200 rounded text-gray-500" onclick="renameChat('${chatId}')">重命名</button>
|
<button class="p-1 hover:bg-gray-200 rounded text-gray-500" onclick="renameChat('${chatId}')">重命名</button>
|
||||||
<button class="p-1 hover:bg-red-200 rounded text-red-500" onclick="deleteChat('${chatId}')">删除</button>
|
<button class="p-1 hover:bg-red-200 rounded text-red-500" onclick="deleteChat('${chatId}')">删除</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
li.addEventListener('click', (e) => {
|
li.addEventListener('click', (e) => {
|
||||||
if (!e.target.closest('.chat-actions')) {
|
if (!e.target.closest('.chat-actions')) {
|
||||||
loadChat(chatId);
|
loadChat(chatId);
|
||||||
@ -129,19 +175,19 @@ function showChatContextMenu(event, chatId) {
|
|||||||
menu.style.top = `${buttonRect.bottom + 4}px`;
|
menu.style.top = `${buttonRect.bottom + 4}px`;
|
||||||
|
|
||||||
menu.innerHTML = `
|
menu.innerHTML = `
|
||||||
<div class="context-menu-item" onclick="renameChat('${chatId}')">
|
<div class="context-menu-item" onclick="renameChat('${chatId}')">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||||
</svg>
|
</svg>
|
||||||
重命名
|
重命名
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-item text-red-500" onclick="deleteChat('${chatId}')">
|
<div class="context-menu-item text-red-500" onclick="deleteChat('${chatId}')">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||||
</svg>
|
</svg>
|
||||||
删除
|
删除
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.body.appendChild(menu);
|
document.body.appendChild(menu);
|
||||||
currentContextMenu = menu;
|
currentContextMenu = menu;
|
||||||
@ -176,11 +222,11 @@ function loadChat(chatId) {
|
|||||||
currentChatId = chatId;
|
currentChatId = chatId;
|
||||||
localStorage.setItem('currentChatId', chatId);
|
localStorage.setItem('currentChatId', chatId);
|
||||||
clearChatArea();
|
clearChatArea();
|
||||||
const chatData = JSON.parse(localStorage.getItem(`chat_${chatId}`) || { messages: [] });
|
const chatData = JSON.parse(localStorage.getItem(`chat_${chatId}`) || '{ "messages": [] }');
|
||||||
chatData.messages.forEach(msg => {
|
(chatData.messages || []).forEach(msg => {
|
||||||
appendMessage(msg.content, msg.isAssistant, false);
|
appendMessage(msg.content, msg.isAssistant, false);
|
||||||
});
|
});
|
||||||
updateChatList()
|
updateChatList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearChatArea() {
|
function clearChatArea() {
|
||||||
@ -203,7 +249,7 @@ function appendMessage(content, isAssistant = false, saveToStorage = true) {
|
|||||||
copyBtn.onclick = () => {
|
copyBtn.onclick = () => {
|
||||||
navigator.clipboard.writeText(content).then(() => {
|
navigator.clipboard.writeText(content).then(() => {
|
||||||
copyBtn.textContent = '已复制';
|
copyBtn.textContent = '已复制';
|
||||||
setTimeout(() => copyBtn.textContent = '复制', 2000);
|
setTimeout(() => (copyBtn.textContent = '复制'), 2000);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
messageDiv.appendChild(copyBtn);
|
messageDiv.appendChild(copyBtn);
|
||||||
@ -213,33 +259,26 @@ function appendMessage(content, isAssistant = false, saveToStorage = true) {
|
|||||||
|
|
||||||
// 仅在需要时保存到本地存储
|
// 仅在需要时保存到本地存储
|
||||||
if (saveToStorage && currentChatId) {
|
if (saveToStorage && currentChatId) {
|
||||||
// 确保读取和保存完整的数据结构
|
|
||||||
const chatData = JSON.parse(localStorage.getItem(`chat_${currentChatId}`) || '{"name": "新聊天", "messages": []}');
|
const chatData = JSON.parse(localStorage.getItem(`chat_${currentChatId}`) || '{"name": "新聊天", "messages": []}');
|
||||||
chatData.messages.push({ content, isAssistant });
|
chatData.messages.push({ content, isAssistant });
|
||||||
localStorage.setItem(`chat_${currentChatId}`, JSON.stringify(chatData));
|
localStorage.setItem(`chat_${currentChatId}`, JSON.stringify(chatData));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startEventStream(message) {
|
function startEventStream(message) {
|
||||||
if (currentEventSource) {
|
if (currentEventSource) {
|
||||||
currentEventSource.close();
|
currentEventSource.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选项值,
|
// 组装流式接口
|
||||||
// 组装01;/api/v1/ollama/generate_stream?message=Hello&model=deepseek-r1:1.5b
|
|
||||||
// 组装02;/api/v1/openai/generate_stream?message=Hello&model=gpt-4o
|
|
||||||
const ragTag = document.getElementById('ragSelect').value;
|
const ragTag = document.getElementById('ragSelect').value;
|
||||||
const aiModelSelect = document.getElementById('aiModel');
|
const aiModelSelect = document.getElementById('aiModel');
|
||||||
const aiModelValue = aiModelSelect.value; // 获取选中的 aiModel 的 value
|
const aiModelValue = aiModelSelect.value; // openai / ollama
|
||||||
const aiModelModel = aiModelSelect.options[aiModelSelect.selectedIndex].getAttribute('model'); // 获取选中的 aiModel 的 model 属性
|
const aiModelModel = aiModelSelect.options[aiModelSelect.selectedIndex].getAttribute('model');
|
||||||
|
|
||||||
let url;
|
let url;
|
||||||
|
|
||||||
const base = `/api/v1/${aiModelValue}`;
|
const base = `/api/v1/${aiModelValue}`;
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({ message, model: aiModelModel });
|
||||||
message,
|
|
||||||
model: aiModelModel
|
|
||||||
});
|
|
||||||
if (ragTag) {
|
if (ragTag) {
|
||||||
params.append('ragTag', ragTag);
|
params.append('ragTag', ragTag);
|
||||||
url = `${base}/generate_stream_rag?${params.toString()}`;
|
url = `${base}/generate_stream_rag?${params.toString()}`;
|
||||||
@ -251,7 +290,7 @@ function appendMessage(content, isAssistant = false, saveToStorage = true) {
|
|||||||
let accumulatedContent = '';
|
let accumulatedContent = '';
|
||||||
let tempMessageDiv = null;
|
let tempMessageDiv = null;
|
||||||
|
|
||||||
currentEventSource.onmessage = function(event) {
|
currentEventSource.onmessage = function (event) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
@ -267,7 +306,7 @@ function appendMessage(content, isAssistant = false, saveToStorage = true) {
|
|||||||
welcomeMessage.style.display = 'none';
|
welcomeMessage.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接更新文本内容(先不解析Markdown)
|
// 直接更新文本内容(先不解析 Markdown)
|
||||||
tempMessageDiv.textContent = accumulatedContent;
|
tempMessageDiv.textContent = accumulatedContent;
|
||||||
chatArea.scrollTop = chatArea.scrollHeight;
|
chatArea.scrollTop = chatArea.scrollHeight;
|
||||||
}
|
}
|
||||||
@ -286,14 +325,13 @@ function appendMessage(content, isAssistant = false, saveToStorage = true) {
|
|||||||
copyBtn.onclick = () => {
|
copyBtn.onclick = () => {
|
||||||
navigator.clipboard.writeText(finalContent).then(() => {
|
navigator.clipboard.writeText(finalContent).then(() => {
|
||||||
copyBtn.textContent = '已复制';
|
copyBtn.textContent = '已复制';
|
||||||
setTimeout(() => copyBtn.textContent = '复制', 2000);
|
setTimeout(() => (copyBtn.textContent = '复制'), 2000);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
tempMessageDiv.appendChild(copyBtn);
|
tempMessageDiv.appendChild(copyBtn);
|
||||||
|
|
||||||
// 保存到本地存储
|
// 保存到本地存储
|
||||||
if (currentChatId) {
|
if (currentChatId) {
|
||||||
// 正确的数据结构应该是对象包含messages数组
|
|
||||||
const chatData = JSON.parse(localStorage.getItem(`chat_${currentChatId}`) || '{"name": "新聊天", "messages": []}');
|
const chatData = JSON.parse(localStorage.getItem(`chat_${currentChatId}`) || '{"name": "新聊天", "messages": []}');
|
||||||
chatData.messages.push({ content: finalContent, isAssistant: true });
|
chatData.messages.push({ content: finalContent, isAssistant: true });
|
||||||
localStorage.setItem(`chat_${currentChatId}`, JSON.stringify(chatData));
|
localStorage.setItem(`chat_${currentChatId}`, JSON.stringify(chatData));
|
||||||
@ -304,7 +342,7 @@ function appendMessage(content, isAssistant = false, saveToStorage = true) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
currentEventSource.onerror = function(error) {
|
currentEventSource.onerror = function (error) {
|
||||||
console.error('EventSource error:', error);
|
console.error('EventSource error:', error);
|
||||||
currentEventSource.close();
|
currentEventSource.close();
|
||||||
};
|
};
|
||||||
|
383
docs/tag/v1.0/nginx/html/js/upload.js
Normal file
383
docs/tag/v1.0/nginx/html/js/upload.js
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
// ====== 配置 ======
|
||||||
|
const ENDPOINT = "/api/v1/rag/file/upload";
|
||||||
|
const ALLOWED_EXTS = [".pdf", ".csv", ".txt", ".md", ".sql", ".java"];
|
||||||
|
const MAX_SCAN_DEPTH = 20; // 防止极端深层目录卡住(可自行调整)
|
||||||
|
|
||||||
|
// ====== 工具函数 ======
|
||||||
|
const $ = (sel) => document.querySelector(sel);
|
||||||
|
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (bytes === 0) return "0 B";
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extOfName(name) {
|
||||||
|
return (name.match(/\.[^.]+$/) || [""])[0].toLowerCase();
|
||||||
|
}
|
||||||
|
function isAllowedName(name) {
|
||||||
|
return ALLOWED_EXTS.includes(extOfName(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于渲染与去重时的“相对路径”,优先使用浏览器提供的相对路径
|
||||||
|
function displayPath(file) {
|
||||||
|
return file.webkitRelativePath || file._relativePath || file.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以路径+大小+mtime 作为去重 key(避免不同子目录下同名文件冲突)
|
||||||
|
function uniqueKey(file) {
|
||||||
|
return `${displayPath(file)}__${file.size}__${file.lastModified}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 FileList + 新增 files 合并为去重后的 DataTransfer.files
|
||||||
|
function mergeFiles(existingList, newFiles) {
|
||||||
|
const map = new Map();
|
||||||
|
Array.from(existingList || []).forEach((f) => map.set(uniqueKey(f), f));
|
||||||
|
Array.from(newFiles || []).forEach((f) => map.set(uniqueKey(f), f));
|
||||||
|
|
||||||
|
const dt = new DataTransfer();
|
||||||
|
for (const f of map.values()) dt.items.add(f);
|
||||||
|
return dt.files;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== DOM 引用 ======
|
||||||
|
const form = $("#uploadForm");
|
||||||
|
const dropZone = $("#dropZone");
|
||||||
|
const fileInput = $("#fileInput");
|
||||||
|
const dirInput = $("#dirInput");
|
||||||
|
const chooseDirBtn = $("#chooseDirBtn");
|
||||||
|
const fileListWrap = $("#fileList");
|
||||||
|
const fileListUl = $("#fileList ul");
|
||||||
|
const clearAllBtn = $("#clearAllBtn");
|
||||||
|
const loadingOverlay = $("#loadingOverlay");
|
||||||
|
const submitBtn = $("#submitBtn");
|
||||||
|
const progressWrap = $("#progressWrap");
|
||||||
|
const progressBar = $("#progressBar");
|
||||||
|
const progressText = $("#progressText");
|
||||||
|
|
||||||
|
// ====== 阻止默认拖拽打开新窗口(关键) ======
|
||||||
|
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
|
||||||
|
document.addEventListener(eventName, (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ====== 支持:点击选择文件 / 选择目录 ======
|
||||||
|
dropZone.addEventListener("click", () => fileInput.click());
|
||||||
|
dropZone.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
fileInput.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
chooseDirBtn?.addEventListener("click", () => dirInput?.click());
|
||||||
|
|
||||||
|
// ====== 拖拽区交互状态 ======
|
||||||
|
dropZone.addEventListener("dragenter", () => dropZone.classList.add("drag-active"));
|
||||||
|
dropZone.addEventListener("dragover", () => dropZone.classList.add("drag-active"));
|
||||||
|
dropZone.addEventListener("dragleave", (e) => {
|
||||||
|
if (!dropZone.contains(e.relatedTarget)) dropZone.classList.remove("drag-active");
|
||||||
|
});
|
||||||
|
|
||||||
|
// ====== 拖拽接收(支持文件夹递归) ======
|
||||||
|
dropZone.addEventListener("drop", async (e) => {
|
||||||
|
dropZone.classList.remove("drag-active");
|
||||||
|
|
||||||
|
const items = e.dataTransfer?.items;
|
||||||
|
const plainFiles = e.dataTransfer?.files;
|
||||||
|
|
||||||
|
let collected = [];
|
||||||
|
try {
|
||||||
|
if (items && items.length) {
|
||||||
|
collected = await collectFromDataTransferItems(items);
|
||||||
|
} else if (plainFiles && plainFiles.length) {
|
||||||
|
collected = Array.from(plainFiles);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("解析拖拽内容失败,回退到 files:", err);
|
||||||
|
collected = Array.from(plainFiles || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accepted, rejectedCount } = filterAllowed(collected);
|
||||||
|
if (rejectedCount > 0) {
|
||||||
|
alert(`已忽略 ${rejectedCount} 个不支持的文件类型。\n支持:${ALLOWED_EXTS.join(" ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accepted.length) {
|
||||||
|
fileInput.files = mergeFiles(fileInput.files, accepted);
|
||||||
|
renderFileList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ====== 目录选择(webkitdirectory) ======
|
||||||
|
dirInput?.addEventListener("change", () => {
|
||||||
|
const all = Array.from(dirInput.files || []); // 目录选择会包含 webkitRelativePath
|
||||||
|
const { accepted, rejectedCount } = filterAllowed(all, (f) => f.webkitRelativePath || f.name);
|
||||||
|
|
||||||
|
if (rejectedCount > 0) {
|
||||||
|
alert(`已忽略 ${rejectedCount} 个不支持的文件类型。\n支持:${ALLOWED_EXTS.join(" ")}`);
|
||||||
|
}
|
||||||
|
if (accepted.length) {
|
||||||
|
fileInput.files = mergeFiles(fileInput.files, accepted);
|
||||||
|
renderFileList();
|
||||||
|
}
|
||||||
|
// 允许连续两次选择同一文件夹
|
||||||
|
dirInput.value = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// ====== 文件选择变化(普通文件选择) ======
|
||||||
|
fileInput.addEventListener("change", renderFileList);
|
||||||
|
|
||||||
|
clearAllBtn.addEventListener("click", () => {
|
||||||
|
fileInput.value = "";
|
||||||
|
renderFileList();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ====== 渲染文件列表(显示相对路径) ======
|
||||||
|
function renderFileList() {
|
||||||
|
const files = Array.from(fileInput.files || []);
|
||||||
|
fileListUl.innerHTML = "";
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
fileListWrap.classList.add("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fileListWrap.classList.remove("hidden");
|
||||||
|
|
||||||
|
files.forEach((file, idx) => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
|
||||||
|
const leftWrap = document.createElement("div");
|
||||||
|
const nameEl = document.createElement("div");
|
||||||
|
const metaEl = document.createElement("div");
|
||||||
|
nameEl.className = "file-name";
|
||||||
|
metaEl.className = "file-meta";
|
||||||
|
|
||||||
|
const rel = displayPath(file);
|
||||||
|
nameEl.textContent = rel;
|
||||||
|
metaEl.textContent = `${file.type || "未知类型"} · ${formatBytes(file.size)}`;
|
||||||
|
|
||||||
|
leftWrap.appendChild(nameEl);
|
||||||
|
leftWrap.appendChild(metaEl);
|
||||||
|
|
||||||
|
const removeBtn = document.createElement("button");
|
||||||
|
removeBtn.type = "button";
|
||||||
|
removeBtn.className = "remove-btn text-sm";
|
||||||
|
removeBtn.textContent = "删除";
|
||||||
|
removeBtn.addEventListener("click", () => removeFile(idx));
|
||||||
|
|
||||||
|
li.appendChild(leftWrap);
|
||||||
|
li.appendChild(removeBtn);
|
||||||
|
fileListUl.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 删除单个文件 ======
|
||||||
|
function removeFile(index) {
|
||||||
|
const files = Array.from(fileInput.files);
|
||||||
|
files.splice(index, 1);
|
||||||
|
|
||||||
|
const dt = new DataTransfer();
|
||||||
|
files.forEach((f) => dt.items.add(f));
|
||||||
|
fileInput.files = dt.files;
|
||||||
|
|
||||||
|
renderFileList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 过滤后缀白名单 ======
|
||||||
|
function filterAllowed(files, pathGetter) {
|
||||||
|
let rejectedCount = 0;
|
||||||
|
const accepted = [];
|
||||||
|
|
||||||
|
for (const f of files) {
|
||||||
|
const p = pathGetter ? pathGetter(f) : displayPath(f) || f.name;
|
||||||
|
if (isAllowedName(p)) {
|
||||||
|
accepted.push(f);
|
||||||
|
} else {
|
||||||
|
rejectedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { accepted, rejectedCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 拖拽目录解析(两套方案 + 回退) ======
|
||||||
|
async function collectFromDataTransferItems(items) {
|
||||||
|
const supportsFSH =
|
||||||
|
typeof DataTransferItem !== "undefined" &&
|
||||||
|
DataTransferItem.prototype &&
|
||||||
|
"getAsFileSystemHandle" in DataTransferItem.prototype;
|
||||||
|
|
||||||
|
const collected = [];
|
||||||
|
|
||||||
|
// 方案 1:File System Access API(Chromium 新方案)
|
||||||
|
if (supportsFSH) {
|
||||||
|
const jobs = [];
|
||||||
|
for (const item of items) {
|
||||||
|
jobs.push(handleItemWithFSH(item, collected));
|
||||||
|
}
|
||||||
|
await Promise.all(jobs);
|
||||||
|
return collected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方案 2:webkitGetAsEntry(Safari/老版 Chromium)
|
||||||
|
const supportsWebkitEntry =
|
||||||
|
items[0] && typeof items[0].webkitGetAsEntry === "function";
|
||||||
|
if (supportsWebkitEntry) {
|
||||||
|
const jobs = [];
|
||||||
|
for (const item of items) {
|
||||||
|
const entry = item.webkitGetAsEntry && item.webkitGetAsEntry();
|
||||||
|
if (!entry) continue;
|
||||||
|
jobs.push(walkWebkitEntry(entry, collected, "", 0));
|
||||||
|
}
|
||||||
|
await Promise.all(jobs);
|
||||||
|
return collected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退:只能拿到平铺的 files,无法识别目录
|
||||||
|
for (const item of items) {
|
||||||
|
const f = item.getAsFile && item.getAsFile();
|
||||||
|
if (f) collected.push(f);
|
||||||
|
}
|
||||||
|
return collected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------ 方案 1:FS Access API ------
|
||||||
|
async function handleItemWithFSH(item, out) {
|
||||||
|
const handle = await item.getAsFileSystemHandle();
|
||||||
|
if (!handle) return;
|
||||||
|
|
||||||
|
if (handle.kind === "file") {
|
||||||
|
const file = await handle.getFile();
|
||||||
|
// 无法直接获取相对路径,使用文件名作为相对路径
|
||||||
|
file._relativePath = file.name;
|
||||||
|
out.push(file);
|
||||||
|
} else if (handle.kind === "directory") {
|
||||||
|
await walkDirectoryHandle(handle, out, `${handle.name}/`, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function walkDirectoryHandle(dirHandle, out, prefix = "", depth = 0) {
|
||||||
|
if (depth > MAX_SCAN_DEPTH) return;
|
||||||
|
for await (const [name, handle] of dirHandle.entries()) {
|
||||||
|
if (handle.kind === "file") {
|
||||||
|
const file = await handle.getFile();
|
||||||
|
file._relativePath = `${prefix}${name}`;
|
||||||
|
out.push(file);
|
||||||
|
} else if (handle.kind === "directory") {
|
||||||
|
await walkDirectoryHandle(handle, out, `${prefix}${name}/`, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------ 方案 2:webkitGetAsEntry ------
|
||||||
|
function readEntriesAsync(dirReader) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
dirReader.readEntries(resolve, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function walkWebkitEntry(entry, out, prefix = "", depth = 0) {
|
||||||
|
if (depth > MAX_SCAN_DEPTH) return;
|
||||||
|
|
||||||
|
if (entry.isFile) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
entry.file((file) => {
|
||||||
|
file._relativePath = `${prefix}${entry.name}`;
|
||||||
|
resolve(out.push(file));
|
||||||
|
}, () => resolve());
|
||||||
|
});
|
||||||
|
} else if (entry.isDirectory) {
|
||||||
|
const dirReader = entry.createReader();
|
||||||
|
let entries = [];
|
||||||
|
do {
|
||||||
|
entries = await readEntriesAsync(dirReader);
|
||||||
|
for (const ent of entries) {
|
||||||
|
await walkWebkitEntry(ent, out, `${prefix}${entry.name}/`, depth + 1);
|
||||||
|
}
|
||||||
|
} while (entries.length > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 表单提交 ======
|
||||||
|
form.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const title = $("#title").value.trim();
|
||||||
|
const files = Array.from(fileInput.files || []);
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
alert("请先填写知识库名称");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (files.length === 0) {
|
||||||
|
alert("请先选择至少一个文件或文件夹");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("ragTag", title);
|
||||||
|
|
||||||
|
// 将文件按顺序追加
|
||||||
|
// 可选:若你想把“相对路径”也传给后端,请同时 append 一个 filePath(顺序与 file 对应)
|
||||||
|
files.forEach((f) => {
|
||||||
|
fd.append("file", f);
|
||||||
|
fd.append("filePath", displayPath(f)); // 需要后端额外接收
|
||||||
|
});
|
||||||
|
|
||||||
|
// UI 状态
|
||||||
|
loadingOverlay.classList.remove("hidden");
|
||||||
|
progressWrap.classList.remove("hidden");
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await axios.post(ENDPOINT, fd, {
|
||||||
|
onUploadProgress: (evt) => {
|
||||||
|
if (!evt.total) return;
|
||||||
|
const pct = Math.round((evt.loaded / evt.total) * 100);
|
||||||
|
progressBar.style.width = `${pct}%`;
|
||||||
|
progressText.textContent = `${pct}%`;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res?.data?.code === "0000") {
|
||||||
|
// === 通知 index.html 刷新 Rag 列表(两种方式都发,保证健壮性) ===
|
||||||
|
const ragTagJustCreated = title; // 你提交的 ragTag 标题
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1) postMessage:同源且 opener 存在时生效
|
||||||
|
if (window.opener && !window.opener.closed) {
|
||||||
|
window.opener.postMessage(
|
||||||
|
{ type: "RAG_LIST_REFRESH", ragTag: ragTagJustCreated },
|
||||||
|
window.location.origin
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2) BroadcastChannel:同源标签页广播
|
||||||
|
if ("BroadcastChannel" in window) {
|
||||||
|
const bc = new BroadcastChannel("rag-updates");
|
||||||
|
bc.postMessage({ type: "rag:updated", ragTag: ragTagJustCreated });
|
||||||
|
bc.close();
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
alert("上传成功,窗口即将关闭");
|
||||||
|
setTimeout(() => window.close(), 500);
|
||||||
|
} else {
|
||||||
|
throw new Error(res?.data?.info || "上传失败");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert(err?.message || "上传过程中出现错误");
|
||||||
|
} finally {
|
||||||
|
loadingOverlay.classList.add("hidden");
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
progressWrap.classList.add("hidden");
|
||||||
|
progressBar.style.width = "0%";
|
||||||
|
progressText.textContent = "0%";
|
||||||
|
renderFileList();
|
||||||
|
}
|
||||||
|
});
|
@ -1,158 +1,162 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>文件上传</title>
|
<title>文件上传</title>
|
||||||
<script src="js/axios.min.js"></script>
|
|
||||||
|
<!-- Tailwind CDN(可保留,也可改为自己构建的CSS) -->
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<style>
|
|
||||||
/* 加载动画 */
|
<!-- 自定义样式 -->
|
||||||
.loader {
|
<link rel="stylesheet" href="css/upload.css" />
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
<!-- Axios(CDN 版本;也可改回你本地的 js/axios.min.js) -->
|
||||||
@keyframes spin {
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js" defer></script>
|
||||||
0% { transform: rotate(0deg); }
|
<!-- 交互逻辑 -->
|
||||||
100% { transform: rotate(360deg); }
|
<script src="js/upload.js" defer></script>
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="flex justify-center items-center min-h-screen bg-gray-100">
|
|
||||||
|
|
||||||
<!-- 上传文件模态框 -->
|
<body class="min-h-screen min-h-[100dvh] bg-gradient-to-br from-gray-50 to-gray-100 grid place-items-center">
|
||||||
<div class="bg-white p-6 rounded-lg shadow-lg w-96 relative">
|
<div class="container mx-auto px-4 py-10">
|
||||||
<!-- 加载遮罩层 -->
|
<div class="mx-auto max-w-lg relative">
|
||||||
<div id="loadingOverlay" class="hidden absolute inset-0 bg-white bg-opacity-90 flex flex-col items-center justify-center rounded-lg">
|
<!-- 卡片 -->
|
||||||
<div class="loader mb-4">
|
<div class="bg-white/90 backdrop-blur p-6 rounded-2xl shadow-xl ring-1 ring-gray-100">
|
||||||
<svg class="h-8 w-8 text-blue-600" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<h2 class="text-2xl font-semibold text-center mb-2">添加知识</h2>
|
||||||
<path d="M12 2V6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
<p class="text-center text-gray-500 mb-6">支持拖拽或点击上传多个文件</p>
|
||||||
<path d="M12 18V22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
||||||
<path d="M4.93 4.93L7.76 7.76" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
||||||
<path d="M16.24 16.24L19.07 19.07" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
||||||
<path d="M2 12H6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
||||||
<path d="M18 12H22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
||||||
<path d="M4.93 19.07L7.76 16.24" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
||||||
<path d="M16.24 7.76L19.07 4.93" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p class="text-gray-600">文件上传中,请稍候...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold text-center mb-4">添加知识</h2>
|
<!-- 表单 -->
|
||||||
<form id="uploadForm" enctype="multipart/form-data">
|
<form id="uploadForm" enctype="multipart/form-data" class="space-y-5">
|
||||||
<!-- 知识标题输入 -->
|
<!-- 标题 -->
|
||||||
<div class="mb-4">
|
<div>
|
||||||
<label for="title" class="block text-sm font-medium text-gray-700">知识标题</label>
|
<label for="title" class="block text-sm font-medium text-gray-700"
|
||||||
<input type="text" id="title" name="title" class="mt-1 block w-full p-2 border border-gray-300 rounded-md" placeholder="输入知识标题" required />
|
>知识库名称</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="title"
|
||||||
|
name="title"
|
||||||
|
class="mt-1 block w-full rounded-lg border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="输入知识库名称"
|
||||||
|
required
|
||||||
|
aria-describedby="titleHelp"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 拖拽/点击区域 -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">上传文件</label>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="dropZone"
|
||||||
|
class="mt-2 drag-zone group border-2 border-dashed border-gray-300 rounded-xl p-6 text-center text-gray-500 transition-all"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="将文件或文件夹拖拽到此处,或点击选择文件"
|
||||||
|
>
|
||||||
|
<!-- 隐藏但可访问的原生文件输入(选择文件) -->
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="fileInput"
|
||||||
|
class="sr-only"
|
||||||
|
accept=".pdf,.csv,.txt,.md,.sql,.java"
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center justify-center gap-2 pointer-events-none">
|
||||||
|
<svg
|
||||||
|
class="h-10 w-10 opacity-80"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 16V4m0 0l-4 4m4-4l4 4M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div class="text-base">
|
||||||
|
将<strong>文件或文件夹</strong>拖到此处 <span class="text-gray-400">或</span>
|
||||||
|
<span class="text-blue-600 underline decoration-dotted">点击选择文件</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400">
|
||||||
|
支持文件后缀:.pdf .csv .txt .md .sql .java(可多选)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 选择文件夹按钮 + 隐藏目录输入 -->
|
||||||
|
<div class="mt-2 text-center">
|
||||||
|
<button type="button" id="chooseDirBtn" class="text-sm text-blue-600 hover:text-blue-700 underline">
|
||||||
|
或选择一个文件夹上传
|
||||||
|
</button>
|
||||||
|
<!-- 选择目录(Chrome/Edge/Safari 支持;Firefox 暂不支持) -->
|
||||||
|
<input type="file" id="dirInput" class="sr-only" webkitdirectory directory multiple />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件列表 -->
|
||||||
|
<div id="fileList" class="hidden">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="text-sm font-medium text-gray-700">待上传文件</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="clearAllBtn"
|
||||||
|
class="text-sm text-gray-500 hover:text-red-600"
|
||||||
|
>
|
||||||
|
清空
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ul class="list-none divide-y divide-gray-100 rounded-lg border border-gray-100" aria-live="polite"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 进度条 -->
|
||||||
|
<div id="progressWrap" class="hidden">
|
||||||
|
<div class="flex justify-between text-xs text-gray-500 mb-1">
|
||||||
|
<span>上传进度</span><span id="progressText">0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-2 bg-gray-100 rounded-full overflow-hidden">
|
||||||
|
<div id="progressBar" class="h-full bg-blue-600 transition-all" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提交 -->
|
||||||
|
<div class="flex justify-center pt-2">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="bg-blue-600 text-white py-2 px-5 rounded-lg hover:bg-blue-700 active:scale-[0.99] disabled:opacity-60"
|
||||||
|
id="submitBtn"
|
||||||
|
>
|
||||||
|
提交
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 上传文件区域 -->
|
<!-- 加载遮罩层 -->
|
||||||
<div class="mb-4">
|
<div
|
||||||
<label for="file" class="block text-sm font-medium text-gray-700">上传文件</label>
|
id="loadingOverlay"
|
||||||
<div class="mt-2 border-dashed border-2 border-gray-300 p-4 text-center text-gray-500">
|
class="hidden absolute inset-0 bg-white/80 backdrop-blur flex flex-col items-center justify-center rounded-2xl"
|
||||||
<input type="file" id="file" name="file" accept=".pdf,.csv,.txt,.md,.sql,.java" class="hidden" multiple />
|
aria-hidden="true"
|
||||||
<label for="file" class="cursor-pointer">
|
>
|
||||||
<div>将文件拖到此处或点击上传</div>
|
<div class="loader mb-3">
|
||||||
<div class="mt-2 text-sm text-gray-400">支持的文件类型:.pdf, .csv, .txt, .md, .sql, .java</div>
|
<svg class="h-8 w-8 text-blue-600" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
</label>
|
<path d="M12 2V6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M12 18V22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M4.93 4.93L7.76 7.76" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M16.24 16.24L19.07 19.07" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M2 12H6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M18 12H22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M4.93 19.07L7.76 16.24" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M16.24 7.76L19.07 4.93" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-gray-600 text-sm">文件上传中,请稍候...</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- 待上传文件列表 -->
|
|
||||||
<div class="mb-4" id="fileList">
|
|
||||||
<ul class="list-disc pl-5 text-gray-700"></ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 提交按钮 -->
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<button type="submit" class="bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700">
|
|
||||||
提交
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
const fileListElement = document.querySelector('#fileList ul');
|
|
||||||
|
|
||||||
// 文件选择变更处理
|
|
||||||
document.getElementById('file').addEventListener('change', function (e) {
|
|
||||||
const files = Array.from(e.target.files);
|
|
||||||
fileListElement.innerHTML = ''; // 清空列表
|
|
||||||
files.forEach((file, index) => {
|
|
||||||
const listItem = document.createElement('li');
|
|
||||||
listItem.className = 'flex justify-between items-center';
|
|
||||||
listItem.innerHTML = `
|
|
||||||
<span>${file.name}</span>
|
|
||||||
<button type="button" class="text-red-500 hover:text-red-700" onclick="removeFile(${index})">删除</button>
|
|
||||||
`;
|
|
||||||
fileListElement.appendChild(listItem);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 移除文件
|
|
||||||
function removeFile(index) {
|
|
||||||
const input = document.getElementById('file');
|
|
||||||
let files = Array.from(input.files);
|
|
||||||
files.splice(index, 1);
|
|
||||||
|
|
||||||
// 创建一个新的DataTransfer对象
|
|
||||||
const dataTransfer = new DataTransfer();
|
|
||||||
files.forEach(file => dataTransfer.items.add(file));
|
|
||||||
|
|
||||||
// 更新文件输入对象的文件列表
|
|
||||||
input.files = dataTransfer.files;
|
|
||||||
|
|
||||||
// 更新文件列表UI
|
|
||||||
const fileListItems = fileListElement.children;
|
|
||||||
fileListItems[index].remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交事件处理
|
|
||||||
document.getElementById('uploadForm').addEventListener('submit', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const loadingOverlay = document.getElementById('loadingOverlay');
|
|
||||||
const input = document.getElementById('file');
|
|
||||||
const files = Array.from(input.files);
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
|
||||||
alert('请先选择一个文件');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示加载状态
|
|
||||||
loadingOverlay.classList.remove('hidden');
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('ragTag', document.getElementById('title').value);
|
|
||||||
files.forEach(file => formData.append('file', file));
|
|
||||||
|
|
||||||
axios.post('/api/v1/rag/file/upload', formData)
|
|
||||||
.then(response => {
|
|
||||||
if (response.data.code === '0000') {
|
|
||||||
// 成功提示并关闭窗口
|
|
||||||
setTimeout(() => {
|
|
||||||
alert('上传成功,窗口即将关闭');
|
|
||||||
window.close();
|
|
||||||
}, 500);
|
|
||||||
} else {
|
|
||||||
throw new Error(response.data.info || '上传失败');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
alert(error.message);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
// 隐藏加载状态
|
|
||||||
loadingOverlay.classList.add('hidden');
|
|
||||||
|
|
||||||
// 清空表单(无论成功与否)
|
|
||||||
input.value = '';
|
|
||||||
document.getElementById('title').value = '';
|
|
||||||
fileListElement.innerHTML = '';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user