@ -0,0 +1,2 @@
|
||||
/mvnw text eol=lf
|
||||
*.cmd text eol=crlf
|
||||
@ -0,0 +1,33 @@
|
||||
HELP.md
|
||||
target/
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
@ -0,0 +1,3 @@
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
|
||||
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@ -0,0 +1,20 @@
|
||||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/drive/1l5dsPVkxm5nl4yN-ckKSgRGs0nlklvz9
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { Message, QueryResultData } from '../types';
|
||||
import { QueryResult } from './QueryResult';
|
||||
|
||||
interface ChatMessageProps {
|
||||
message: Message;
|
||||
onSaveQuery: (query: QueryResultData) => void;
|
||||
onShareQuery: (queryId: string, friendId: string) => void;
|
||||
savedQueries: QueryResultData[];
|
||||
}
|
||||
|
||||
export const ChatMessage: React.FC<ChatMessageProps> = ({ message, onSaveQuery, onShareQuery, savedQueries }) => {
|
||||
const { role, content } = message;
|
||||
|
||||
const isUser = role === 'user';
|
||||
const wrapperClass = `flex items-start gap-3 ${isUser ? 'flex-row-reverse' : ''}`;
|
||||
const avatarClass = `w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 text-lg ${isUser ? 'bg-primary text-white' : 'bg-primary/10 text-primary'}`;
|
||||
const iconClass = `fa ${isUser ? 'fa-user' : 'fa-robot'}`;
|
||||
const bubbleClass = `p-4 shadow-sm max-w-[85%] md:max-w-[80%] text-sm rounded-lg ${isUser ? 'bg-primary text-white rounded-tr-none' : 'bg-white border border-gray-200 rounded-tl-none'}`;
|
||||
|
||||
return (
|
||||
<div className={wrapperClass}>
|
||||
<div className={avatarClass}>
|
||||
<i className={iconClass}></i>
|
||||
</div>
|
||||
<div className={bubbleClass}>
|
||||
{typeof content === 'string' ? (
|
||||
<p>{content}</p>
|
||||
) : (
|
||||
<QueryResult
|
||||
result={content}
|
||||
onSaveQuery={onSaveQuery}
|
||||
onShareQuery={onShareQuery}
|
||||
savedQueries={savedQueries}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,123 @@
|
||||
import React from 'react';
|
||||
import { DataAdminPageType, Page, Conversation, QueryResultData, MessageRole, QueryShare } from '../types';
|
||||
import { DataSourceManagementPage } from './data-admin/DataSourceManagementPage';
|
||||
import { UserPermissionPage } from './data-admin/UserPermissionPage';
|
||||
import { ConnectionLogPage } from './data-admin/ConnectionLogPage';
|
||||
import { QueryPage } from './QueryPage';
|
||||
import { HistoryPage } from './HistoryPage';
|
||||
import { AccountPage } from './AccountPage';
|
||||
import { FriendsPageWithRealAPI } from './FriendsPageWithRealAPI';
|
||||
import { NotificationsPage } from './NotificationsPage';
|
||||
import { DataAdminNotificationPage } from './data-admin/DataAdminNotificationPage';
|
||||
import { ComparisonModal } from './ComparisonModal';
|
||||
import { DataAdminDashboardPage } from './data-admin/DataAdminDashboardPage';
|
||||
|
||||
interface DataAdminPageProps {
|
||||
activePage: DataAdminPageType;
|
||||
setActivePage: (page: DataAdminPageType) => void;
|
||||
// Props for QueryPage and others
|
||||
currentConversation: Conversation | undefined;
|
||||
onToggleHistory: () => void;
|
||||
isHistoryOpen: boolean;
|
||||
onAddMessage: (role: MessageRole, content: string | QueryResultData) => void;
|
||||
onSaveQuery: (query: QueryResultData) => void;
|
||||
onShareQuery: (queryId: string, friendId: string) => void;
|
||||
savedQueries: QueryResultData[];
|
||||
initialPrompt?: string;
|
||||
onClearInitialPrompt: () => void;
|
||||
// Props for HistoryPage
|
||||
conversations: Conversation[];
|
||||
currentConversationId: string;
|
||||
onSwitchConversation: (id: string) => void;
|
||||
onNewConversation: () => void;
|
||||
onDelete: (id: string) => void;
|
||||
onViewInChat: (conversationId: string) => void;
|
||||
onRerun: (prompt: string) => void;
|
||||
onCompare: (queryId1: string, queryId2: string) => void;
|
||||
// Props for FriendsPage
|
||||
shares: QueryShare[];
|
||||
onMarkShareAsRead: (shareId: string) => void;
|
||||
onDeleteShare: (shareId: string) => void;
|
||||
onDeleteConversation: (id: string) => void;
|
||||
}
|
||||
|
||||
export const DataAdminPage: React.FC<DataAdminPageProps> = (props) => {
|
||||
const { activePage } = props;
|
||||
|
||||
// Duplicated from App.tsx to render shared pages
|
||||
const renderNormalUserPage = (page: Page) => {
|
||||
switch (page) {
|
||||
case 'query':
|
||||
return (
|
||||
<QueryPage
|
||||
currentConversation={props.currentConversation}
|
||||
onToggleHistory={props.onToggleHistory}
|
||||
isHistoryOpen={props.isHistoryOpen}
|
||||
onAddMessage={props.onAddMessage}
|
||||
onSaveQuery={props.onSaveQuery}
|
||||
onShareQuery={props.onShareQuery}
|
||||
savedQueries={props.savedQueries}
|
||||
initialPrompt={props.initialPrompt}
|
||||
onClearInitialPrompt={props.onClearInitialPrompt}
|
||||
conversations={props.conversations}
|
||||
currentConversationId={props.currentConversationId}
|
||||
onSwitchConversation={props.onSwitchConversation}
|
||||
onNewConversation={props.onNewConversation}
|
||||
onDeleteConversation={props.onDeleteConversation}
|
||||
/>
|
||||
);
|
||||
case 'history':
|
||||
return (
|
||||
<HistoryPage
|
||||
savedQueries={props.savedQueries}
|
||||
conversations={props.conversations}
|
||||
onDelete={props.onDelete}
|
||||
onViewInChat={props.onViewInChat}
|
||||
onRerun={props.onRerun}
|
||||
onCompare={props.onCompare}
|
||||
/>
|
||||
);
|
||||
case 'notifications':
|
||||
return <NotificationsPage />;
|
||||
case 'account':
|
||||
return <AccountPage />;
|
||||
case 'friends':
|
||||
return (
|
||||
<FriendsPageWithRealAPI
|
||||
savedQueries={props.savedQueries}
|
||||
shares={props.shares}
|
||||
onMarkShareAsRead={props.onMarkShareAsRead}
|
||||
onDeleteShare={props.onDeleteShare}
|
||||
onRerunQuery={props.onRerun}
|
||||
onSaveQuery={props.onSaveQuery}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
// This case handles 'comparison', which is rendered by the parent switch
|
||||
return <div>Page not found</div>;
|
||||
}
|
||||
};
|
||||
|
||||
switch (activePage) {
|
||||
// Admin specific pages
|
||||
case 'dashboard':
|
||||
return <DataAdminDashboardPage setActivePage={props.setActivePage} />;
|
||||
case 'datasource':
|
||||
return <DataSourceManagementPage />;
|
||||
case 'user-permission':
|
||||
return <UserPermissionPage />;
|
||||
case 'notification-management':
|
||||
return <DataAdminNotificationPage />;
|
||||
case 'connection-log':
|
||||
return <ConnectionLogPage />;
|
||||
case 'query':
|
||||
case 'history':
|
||||
case 'notifications':
|
||||
case 'account':
|
||||
case 'friends':
|
||||
return renderNormalUserPage(activePage);
|
||||
default:
|
||||
const exhaustiveCheck: never = activePage;
|
||||
return <div>Page not found</div>;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import { DataAdminPageType } from '../types';
|
||||
|
||||
interface SidebarItemProps {
|
||||
href: DataAdminPageType;
|
||||
icon: string;
|
||||
label: string;
|
||||
isActive: boolean;
|
||||
onClick: (page: DataAdminPageType) => void;
|
||||
}
|
||||
|
||||
const SidebarItem: React.FC<SidebarItemProps> = ({ href, icon, label, isActive, onClick }) => {
|
||||
const activeClass = 'bg-primary/10 text-primary border-l-4 border-primary';
|
||||
const inactiveClass = 'text-gray-600 hover:bg-gray-50';
|
||||
|
||||
return (
|
||||
<li>
|
||||
<a
|
||||
href={`#${href}`}
|
||||
className={`flex items-center px-6 py-3 text-sm transition-colors duration-200 ${isActive ? activeClass : inactiveClass}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick(href);
|
||||
}}
|
||||
>
|
||||
<i className={`fa ${icon} w-6`}></i>
|
||||
<span>{label}</span>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
interface SidebarProps {
|
||||
activePage: DataAdminPageType;
|
||||
setActivePage: (page: DataAdminPageType) => void;
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
export const DataAdminSidebar: React.FC<SidebarProps> = ({ activePage, setActivePage, onLogout }) => {
|
||||
const queryItems = [
|
||||
{ href: 'query', icon: 'fa-search', label: '数据查询' },
|
||||
{ href: 'history', icon: 'fa-star', label: '收藏夹' },
|
||||
] as const;
|
||||
|
||||
const managementItems = [
|
||||
{ href: 'dashboard', icon: 'fa-tachometer', label: '仪表盘' },
|
||||
{ href: 'datasource', icon: 'fa-plug', label: '数据源管理' },
|
||||
{ href: 'user-permission', icon: 'fa-key', label: '用户权限管理' },
|
||||
{ href: 'notification-management', icon: 'fa-bullhorn', label: '通知管理' },
|
||||
{ href: 'connection-log', icon: 'fa-link', label: '连接日志' },
|
||||
] as const;
|
||||
|
||||
const personalItems = [
|
||||
{ href: 'notifications', icon: 'fa-bell', label: '通知中心' },
|
||||
{ href: 'account', icon: 'fa-user', label: '账户管理' },
|
||||
{ href: 'friends', icon: 'fa-users', label: '好友管理' },
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<aside className="w-64 bg-white shadow-md h-screen flex-shrink-0 flex flex-col hidden lg:flex">
|
||||
<div className="p-4 border-b flex items-center space-x-2">
|
||||
<i className="fa fa-database text-primary text-2xl"></i>
|
||||
<h1 className="text-lg font-bold">数据管理中心</h1>
|
||||
</div>
|
||||
<nav className="py-4 flex-grow">
|
||||
<ul>
|
||||
<li className="px-6 py-2 text-xs text-gray-400 font-bold uppercase">查询功能</li>
|
||||
{queryItems.map(item => (
|
||||
<SidebarItem key={item.href} {...item} isActive={activePage === item.href} onClick={setActivePage} />
|
||||
))}
|
||||
|
||||
<li className="px-6 py-2 text-xs text-gray-400 font-bold uppercase mt-4">管理功能</li>
|
||||
{managementItems.map(item => (
|
||||
<SidebarItem key={item.href} {...item} isActive={activePage === item.href} onClick={setActivePage} />
|
||||
))}
|
||||
|
||||
<li className="px-6 py-2 text-xs text-gray-400 font-bold uppercase mt-4">个人设置</li>
|
||||
{personalItems.map(item => (
|
||||
<SidebarItem key={item.href} {...item} isActive={activePage === item.href} onClick={setActivePage} />
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="border-t p-4">
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="flex items-center text-gray-600 hover:text-danger transition-colors w-full">
|
||||
<i className="fa fa-sign-out w-6"></i>
|
||||
<span>退出登录</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,63 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { ModelOption } from '../types';
|
||||
|
||||
interface DropdownProps {
|
||||
options: ModelOption[];
|
||||
selected: string;
|
||||
setSelected: (option: string) => void;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export const Dropdown: React.FC<DropdownProps> = ({ options, selected, setSelected, icon }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleSelect = (option: ModelOption) => {
|
||||
if (option.disabled) return;
|
||||
setSelected(option.name);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="inline-flex items-center justify-center px-4 py-2 bg-primary text-white rounded-lg text-sm w-40 hover:shadow-md hover:-translate-y-0.5 transition-all duration-200"
|
||||
>
|
||||
<i className={`fa ${icon} mr-2`}></i>
|
||||
<span className="truncate">{selected}</span>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="absolute left-0 bottom-full mb-1 w-48 bg-white border border-gray-300 rounded-lg shadow-lg z-10 max-h-60 overflow-y-auto">
|
||||
{options.map(option => (
|
||||
<button
|
||||
key={option.name}
|
||||
onClick={() => handleSelect(option)}
|
||||
disabled={option.disabled}
|
||||
title={option.description}
|
||||
className={`w-full text-left px-4 py-2 text-sm transition-colors
|
||||
${selected === option.name ? 'bg-primary/10 text-primary' : ''}
|
||||
${option.disabled
|
||||
? 'text-gray-400 cursor-not-allowed bg-gray-100'
|
||||
: 'hover:bg-gray-100'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{option.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,477 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { Conversation, QueryResultData } from '../types';
|
||||
import { Modal } from './Modal';
|
||||
import { QueryResult } from './QueryResult';
|
||||
import { MODEL_OPTIONS, DATABASE_OPTIONS } from '../constants';
|
||||
import { queryLogApi } from '../services/api';
|
||||
|
||||
type FilterType = 'all' | 'today' | '7days' | '30days';
|
||||
type ConfirmAction = 'delete' | 'rerun' | 'bulkDelete';
|
||||
|
||||
interface HistoryPageProps {
|
||||
savedQueries: QueryResultData[];
|
||||
conversations: Conversation[];
|
||||
onDelete: (id: string) => void;
|
||||
onViewInChat: (conversationId: string) => void;
|
||||
onRerun: (prompt: string) => void;
|
||||
onCompare: (queryId1: string, queryId2: string) => void;
|
||||
}
|
||||
|
||||
const isDateInRage = (date: Date, days: number): boolean => {
|
||||
const today = new Date();
|
||||
const pastDate = new Date();
|
||||
if (days > 0) {
|
||||
pastDate.setDate(today.getDate() - (days - 1));
|
||||
}
|
||||
today.setHours(23, 59, 59, 999);
|
||||
pastDate.setHours(0, 0, 0, 0);
|
||||
return date >= pastDate && date <= today;
|
||||
};
|
||||
|
||||
export const HistoryPageWithAPI: React.FC<HistoryPageProps> = ({
|
||||
savedQueries, conversations, onDelete, onViewInChat, onRerun, onCompare
|
||||
}) => {
|
||||
const userId = Number(sessionStorage.getItem('userId') || '1');
|
||||
const [queryLogs, setQueryLogs] = useState<QueryResultData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [activeFilters, setActiveFilters] = useState({
|
||||
date: 'all' as FilterType,
|
||||
model: 'all',
|
||||
database: 'all'
|
||||
});
|
||||
const [expandedGroup, setExpandedGroup] = useState<string | null>(null);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [viewingQuery, setViewingQuery] = useState<QueryResultData | null>(null);
|
||||
|
||||
const [confirmModalState, setConfirmModalState] = useState<{
|
||||
isOpen: boolean;
|
||||
action: ConfirmAction | null;
|
||||
targetId: string | null;
|
||||
targetPrompt: string | null;
|
||||
targetIds: string[] | null;
|
||||
}>({
|
||||
isOpen: false,
|
||||
action: null,
|
||||
targetId: null,
|
||||
targetPrompt: null,
|
||||
targetIds: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadQueryHistory();
|
||||
}, []);
|
||||
|
||||
const loadQueryHistory = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const logs = await queryLogApi.getByUser(userId);
|
||||
const queryData: QueryResultData[] = logs.map(log => {
|
||||
const result = JSON.parse(log.queryResult || '{}');
|
||||
return {
|
||||
id: String(log.id),
|
||||
userPrompt: log.userPrompt,
|
||||
sqlQuery: log.sqlQuery,
|
||||
conversationId: log.dialogId,
|
||||
queryTime: log.queryTime,
|
||||
executionTime: log.executionTime,
|
||||
database: String(log.dbConnectionId),
|
||||
model: String(log.llmConfigId),
|
||||
tableData: result.tableData || { headers: [], rows: [] },
|
||||
chartData: result.chartData,
|
||||
};
|
||||
});
|
||||
setQueryLogs(queryData);
|
||||
} catch (error) {
|
||||
console.error('加载查询历史失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getConversationTitle = (id: string) => {
|
||||
return conversations.find(c => c.id === id)?.title || '未知对话';
|
||||
};
|
||||
|
||||
const handleToggleGroup = (prompt: string) => {
|
||||
setExpandedGroup(prev => (prev === prompt ? null : prompt));
|
||||
setSelectedIds(new Set());
|
||||
};
|
||||
|
||||
const handleSelectSnapshot = (id: string) => {
|
||||
setSelectedIds(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(id)) {
|
||||
newSet.delete(id);
|
||||
} else if (newSet.size < 100) {
|
||||
newSet.add(id);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const handleCompare = () => {
|
||||
if (selectedIds.size !== 2) return;
|
||||
const [id1, id2] = Array.from(selectedIds);
|
||||
onCompare(id1, id2);
|
||||
setSelectedIds(new Set());
|
||||
};
|
||||
|
||||
const openDeleteConfirm = (id: string) => {
|
||||
setConfirmModalState({
|
||||
isOpen: true,
|
||||
action: 'delete',
|
||||
targetId: id,
|
||||
targetPrompt: null,
|
||||
targetIds: null,
|
||||
});
|
||||
};
|
||||
|
||||
const openRerunConfirm = (prompt: string) => {
|
||||
setConfirmModalState({
|
||||
isOpen: true,
|
||||
action: 'rerun',
|
||||
targetId: null,
|
||||
targetPrompt: prompt,
|
||||
targetIds: null,
|
||||
});
|
||||
};
|
||||
|
||||
const openBulkDeleteConfirm = () => {
|
||||
setConfirmModalState({
|
||||
isOpen: true,
|
||||
action: 'bulkDelete',
|
||||
targetId: null,
|
||||
targetPrompt: null,
|
||||
targetIds: Array.from(selectedIds),
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirmAction = async () => {
|
||||
try {
|
||||
if (confirmModalState.action === 'delete' && confirmModalState.targetId) {
|
||||
await queryLogApi.delete(Number(confirmModalState.targetId));
|
||||
setQueryLogs(prev => prev.filter(q => q.id !== confirmModalState.targetId));
|
||||
onDelete(confirmModalState.targetId);
|
||||
} else if (confirmModalState.action === 'rerun' && confirmModalState.targetPrompt) {
|
||||
onRerun(confirmModalState.targetPrompt);
|
||||
} else if (confirmModalState.action === 'bulkDelete' && confirmModalState.targetIds) {
|
||||
await Promise.all(
|
||||
confirmModalState.targetIds.map(id => queryLogApi.delete(Number(id)))
|
||||
);
|
||||
setQueryLogs(prev => prev.filter(q => !confirmModalState.targetIds!.includes(q.id)));
|
||||
confirmModalState.targetIds.forEach(id => onDelete(id));
|
||||
setSelectedIds(new Set());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error);
|
||||
alert('操作失败,请稍后重试');
|
||||
}
|
||||
setConfirmModalState({ isOpen: false, action: null, targetId: null, targetPrompt: null, targetIds: null });
|
||||
};
|
||||
|
||||
const handleCancelConfirm = () => {
|
||||
setConfirmModalState({ isOpen: false, action: null, targetId: null, targetPrompt: null, targetIds: null });
|
||||
};
|
||||
|
||||
const handleFilterChange = (type: 'date' | 'model' | 'database', value: string) => {
|
||||
setActiveFilters(prev => ({
|
||||
...prev,
|
||||
[type]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleResetFilters = () => {
|
||||
setActiveFilters({
|
||||
date: 'all',
|
||||
model: 'all',
|
||||
database: 'all'
|
||||
});
|
||||
setSearchTerm('');
|
||||
};
|
||||
|
||||
const queryGroups = useMemo(() => {
|
||||
const groups: Record<string, QueryResultData[]> = {};
|
||||
|
||||
queryLogs.forEach(query => {
|
||||
if (!groups[query.userPrompt]) {
|
||||
groups[query.userPrompt] = [];
|
||||
}
|
||||
groups[query.userPrompt].push(query);
|
||||
});
|
||||
|
||||
Object.values(groups).forEach(snapshots => {
|
||||
snapshots.sort((a, b) => new Date(b.queryTime).getTime() - new Date(a.queryTime).getTime());
|
||||
});
|
||||
return groups;
|
||||
}, [queryLogs]);
|
||||
|
||||
const filteredGroups = useMemo(() => {
|
||||
return Object.entries(queryGroups)
|
||||
.filter(([prompt, snapshots]) => {
|
||||
const matchesSearch = prompt.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
if (!matchesSearch) return false;
|
||||
|
||||
const filteredSnapshots = snapshots.filter(query => {
|
||||
if (activeFilters.date !== 'all') {
|
||||
const queryDate = new Date(query.queryTime);
|
||||
const dateMatch =
|
||||
activeFilters.date === 'today' ? isDateInRage(queryDate, 1) :
|
||||
activeFilters.date === '7days' ? isDateInRage(queryDate, 7) :
|
||||
activeFilters.date === '30days' ? isDateInRage(queryDate, 30) :
|
||||
false;
|
||||
if (!dateMatch) return false;
|
||||
}
|
||||
|
||||
if (activeFilters.model !== 'all' && query.model !== activeFilters.model) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activeFilters.database !== 'all' && query.database !== activeFilters.database) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return filteredSnapshots.length > 0;
|
||||
});
|
||||
}, [queryGroups, searchTerm, activeFilters]);
|
||||
|
||||
const renderFilterDropdown = (
|
||||
label: string,
|
||||
type: 'date' | 'model' | 'database',
|
||||
options: { value: string; label: string }[]
|
||||
) => (
|
||||
<div className="relative">
|
||||
<select
|
||||
value={activeFilters[type]}
|
||||
onChange={(e) => handleFilterChange(type, e.target.value)}
|
||||
className="px-3 py-1.5 text-sm rounded-md border border-gray-300 bg-white font-bold focus:ring-2 focus:ring-primary/30 pr-6 appearance-none"
|
||||
style={{
|
||||
backgroundImage: 'url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'%23333\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\'%3e%3cpolyline points=\'6 9 12 15 18 9\'%3e%3c/polyline%3e%3c/svg%3e")',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'right 0.5rem center',
|
||||
backgroundSize: '1em'
|
||||
}}
|
||||
>
|
||||
{options.map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
|
||||
const modelOptions = useMemo(() => {
|
||||
const uniqueModels = Array.from(new Set(MODEL_OPTIONS.map(option => option.name)));
|
||||
return [
|
||||
{ value: 'all', label: '全部大模型' },
|
||||
...uniqueModels.map(model => ({ value: model, label: model }))
|
||||
];
|
||||
}, []);
|
||||
|
||||
const databaseOptions = useMemo(() => {
|
||||
const uniqueDatabases = Array.from(new Set(DATABASE_OPTIONS.map(option => option.name)));
|
||||
return [
|
||||
{ value: 'all', label: '全部数据库' },
|
||||
...uniqueDatabases.map(db => ({ value: db, label: db }))
|
||||
];
|
||||
}, []);
|
||||
|
||||
const dateOptions = [
|
||||
{ value: 'all', label: '全部日期' },
|
||||
{ value: 'today', label: '今天' },
|
||||
{ value: '7days', label: '近7天' },
|
||||
{ value: '30days', label: '近30天' }
|
||||
];
|
||||
|
||||
const confirmModalContent = useMemo(() => {
|
||||
if (confirmModalState.action === 'delete') {
|
||||
return {
|
||||
title: '确认删除查询记录?',
|
||||
message: '此操作将永久删除该条查询快照。请确认是否继续?',
|
||||
buttonText: '确认删除',
|
||||
buttonClass: 'bg-red-600 hover:bg-red-700',
|
||||
};
|
||||
}
|
||||
if (confirmModalState.action === 'rerun') {
|
||||
return {
|
||||
title: '确认重新执行查询?',
|
||||
message: `您确定要重新执行查询:"${confirmModalState.targetPrompt}" 吗? 这将消耗新的计算资源。`,
|
||||
buttonText: '重新执行',
|
||||
buttonClass: 'bg-primary hover:bg-primary/90',
|
||||
};
|
||||
}
|
||||
if (confirmModalState.action === 'bulkDelete' && confirmModalState.targetIds) {
|
||||
return {
|
||||
title: '确认批量删除?',
|
||||
message: `您确定要永久删除选中的 ${confirmModalState.targetIds.length} 条查询记录吗?此操作不可恢复。`,
|
||||
buttonText: '批量删除',
|
||||
buttonClass: 'bg-red-600 hover:bg-red-700',
|
||||
};
|
||||
}
|
||||
return { title: '', message: '', buttonText: '', buttonClass: '' };
|
||||
}, [confirmModalState.action, confirmModalState.targetPrompt, confirmModalState.targetIds]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="p-6 space-y-6 overflow-y-auto h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="dot-flashing"></div>
|
||||
<p className="mt-4 text-gray-500">加载查询历史中...</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="p-6 space-y-6 overflow-y-auto h-full">
|
||||
|
||||
<div className="bg-white p-4 rounded-xl shadow-sm border sticky top-0 z-10">
|
||||
<div className="flex flex-col md:flex-row gap-4 items-center">
|
||||
<div className="relative w-full md:w-1/3">
|
||||
<i className="fa fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="按查询内容搜索...(右侧下拉框可筛选)"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4 flex-grow justify-end">
|
||||
{renderFilterDropdown('大模型', 'model', modelOptions)}
|
||||
{renderFilterDropdown('数据库', 'database', databaseOptions)}
|
||||
{renderFilterDropdown('日期', 'date', dateOptions)}
|
||||
|
||||
<button
|
||||
onClick={handleResetFilters}
|
||||
className="px-3 py-1.5 text-sm rounded-md border border-gray-300 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<i className="fa fa-refresh mr-1"></i> 重置
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={openBulkDeleteConfirm}
|
||||
disabled={selectedIds.size === 0}
|
||||
className="px-4 py-1.5 text-sm rounded-md transition-colors
|
||||
disabled:bg-gray-200 disabled:text-gray-400 disabled:cursor-not-allowed
|
||||
enabled:bg-red-600 enabled:text-white enabled:hover:bg-red-700"
|
||||
>
|
||||
<i className="fa fa-trash-o mr-1"></i> 批量删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{filteredGroups.length > 0 ? (
|
||||
filteredGroups.map(([prompt, snapshots]) => (
|
||||
<div key={prompt} className="bg-white rounded-xl shadow-sm overflow-hidden border border-gray-200">
|
||||
<div className="p-4 cursor-pointer hover:bg-gray-50 flex justify-between items-center" onClick={() => handleToggleGroup(prompt)}>
|
||||
<div>
|
||||
<p className="font-semibold text-dark truncate max-w-lg" title={prompt}>{prompt}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">{snapshots.length} 次执行</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{snapshots.length > 1 && expandedGroup === prompt && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleCompare(); }}
|
||||
disabled={selectedIds.size !== 2}
|
||||
className="px-3 py-1 bg-primary text-white rounded-md text-xs disabled:bg-primary/50 disabled:cursor-not-allowed"
|
||||
>
|
||||
对比差异 ({selectedIds.size}/2)
|
||||
</button>
|
||||
)}
|
||||
<button className="text-gray-500"><i className={`fa fa-chevron-down transition-transform ${expandedGroup === prompt ? 'rotate-180' : ''}`}></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{expandedGroup === prompt && (
|
||||
<div className="p-4 border-t border-gray-200 bg-neutral space-y-3">
|
||||
{snapshots.map(query => (
|
||||
<div key={query.id} className={`p-3 rounded-lg flex items-center justify-between ${selectedIds.has(query.id) ? 'bg-primary/10' : 'bg-white'}`}>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.has(query.id)}
|
||||
onChange={() => handleSelectSnapshot(query.id)}
|
||||
className="mr-4 h-4 w-4 text-primary focus:ring-primary/50 border-gray-300 rounded"
|
||||
/>
|
||||
<p className="text-sm font-medium">执行于: {new Date(query.queryTime).toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-6 gap-y-1 mt-1 text-xs text-gray-500 ml-8">
|
||||
<span>耗时: {query.executionTime}</span>
|
||||
<span>大模型: {query.model}</span>
|
||||
<span>数据库: {query.database}</span>
|
||||
<span>所属对话: "{query.conversationId}"</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-3 text-xs">
|
||||
<button onClick={(e) => { e.stopPropagation(); setViewingQuery(query); }} className="text-primary hover:underline">查看详情</button>
|
||||
<button onClick={(e) => { e.stopPropagation(); onViewInChat(query.conversationId); }} className="text-primary hover:underline">查看对话</button>
|
||||
<button onClick={(e) => { e.stopPropagation(); openRerunConfirm(query.userPrompt); }} className="text-primary hover:underline">重新执行</button>
|
||||
<button onClick={(e) => { e.stopPropagation(); openDeleteConfirm(query.id); }} className="text-danger hover:underline">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center text-gray-500 py-16 bg-white rounded-xl shadow-sm">
|
||||
<i className="fa fa-search-minus text-4xl mb-3 text-gray-400"></i>
|
||||
<p>未找到匹配的查询记录</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
isOpen={!!viewingQuery}
|
||||
onClose={() => setViewingQuery(null)}
|
||||
>
|
||||
{viewingQuery && (
|
||||
<div className="max-h-[70vh] overflow-y-auto -m-6 p-6 pt-0">
|
||||
<QueryResult
|
||||
result={viewingQuery}
|
||||
showActions={{ save: false, share: true, export: true }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
isOpen={confirmModalState.isOpen}
|
||||
onClose={handleCancelConfirm}
|
||||
title={confirmModalContent.title}
|
||||
>
|
||||
<p className="text-gray-700 mb-6">{confirmModalContent.message}</p>
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={handleCancelConfirm}
|
||||
className="px-4 py-2 border border-gray-300 rounded-lg text-sm hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirmAction}
|
||||
className={`px-4 py-2 text-white rounded-lg text-sm hover:shadow-md transition-all duration-200 ${confirmModalContent.buttonClass}`}
|
||||
>
|
||||
{confirmModalContent.buttonText}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface PlaceholderPageProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const PlaceholderPage: React.FC<PlaceholderPageProps> = ({ title }) => {
|
||||
return (
|
||||
<section className="p-6 space-y-6 overflow-y-auto flex flex-col items-center justify-center h-full text-gray-500">
|
||||
<div className="text-center">
|
||||
<i className="fa fa-cogs text-6xl mb-4"></i>
|
||||
<h2 className="text-2xl font-bold mb-2">{title}</h2>
|
||||
<p>此页面正在建设中。</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { Conversation, Message, QueryResultData } from '../types';
|
||||
import { COMMON_RECOMMENDATIONS, MOCK_FAILURE_SUGGESTIONS, MOCK_SUCCESS_SUGGESTIONS } from '../constants';
|
||||
|
||||
interface RightSidebarProps {
|
||||
currentConversation: Conversation | undefined;
|
||||
onRecommendationClick: (prompt: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const isQueryResult = (content: any): content is QueryResultData => {
|
||||
return content && typeof content === 'object' && 'sqlQuery' in content;
|
||||
}
|
||||
|
||||
export const RightSidebar: React.FC<RightSidebarProps> = ({ currentConversation, onRecommendationClick, className = '' }) => {
|
||||
|
||||
const lastMessage: Message | undefined = currentConversation?.messages[currentConversation.messages.length - 1];
|
||||
|
||||
let relatedSearches: string[] = [];
|
||||
let queryFailed = false;
|
||||
let showSuggestions = false;
|
||||
|
||||
// We only want to show suggestions after the first user query and an AI response
|
||||
if (currentConversation && currentConversation.messages.length > 1 && lastMessage && lastMessage.role === 'ai') {
|
||||
showSuggestions = true;
|
||||
if (isQueryResult(lastMessage.content)) {
|
||||
queryFailed = false;
|
||||
relatedSearches = MOCK_SUCCESS_SUGGESTIONS;
|
||||
} else {
|
||||
// Use centralized mock suggestions for failed query
|
||||
queryFailed = true;
|
||||
relatedSearches = MOCK_FAILURE_SUGGESTIONS;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<aside className={`w-80 bg-white border-l border-gray-200 p-4 space-y-6 overflow-y-auto flex-shrink-0 flex flex-col ${className}`}>
|
||||
{/* Common Searches */}
|
||||
<div className="p-4 bg-neutral rounded-lg">
|
||||
<h3 className="font-bold text-lg mb-4 flex items-center">
|
||||
<span className="text-xl mr-2">⭐</span>
|
||||
常用搜索
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{COMMON_RECOMMENDATIONS.map(query => (
|
||||
<button
|
||||
key={query}
|
||||
onClick={() => onRecommendationClick(query)}
|
||||
className="w-full text-left p-3 border border-gray-300 bg-white rounded-lg hover:bg-gray-50 transition-colors text-sm shadow-sm"
|
||||
>
|
||||
{query}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Thinking / Related Searches */}
|
||||
{showSuggestions && (
|
||||
<div className="p-4 bg-neutral rounded-lg">
|
||||
<h3 className="font-bold text-lg mb-4 flex items-center">
|
||||
<i className="fa fa-magic text-secondary text-xl mr-2"></i>
|
||||
大模型思考
|
||||
</h3>
|
||||
<div className={`p-3 rounded-md text-sm mb-3 ${queryFailed ? 'bg-danger/10 text-danger' : 'bg-success/10 text-success'}`}>
|
||||
{queryFailed ? '查询似乎遇到了问题,您可以尝试:' : '基于当前结果,您可以继续探索:'}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{relatedSearches.map((query, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => onRecommendationClick(query)}
|
||||
className="w-full text-left p-3 border border-gray-300 bg-white rounded-lg hover:bg-gray-50 transition-colors text-sm shadow-sm"
|
||||
>
|
||||
{query}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { Page } from '../types';
|
||||
|
||||
interface SidebarItemProps {
|
||||
href: Page;
|
||||
icon: string;
|
||||
label: string;
|
||||
isActive: boolean;
|
||||
onClick: (page: Page) => void;
|
||||
}
|
||||
|
||||
const SidebarItem: React.FC<SidebarItemProps> = ({ href, icon, label, isActive, onClick }) => {
|
||||
const activeClass = 'bg-primary/10 text-primary border-l-4 border-primary';
|
||||
const inactiveClass = 'text-gray-600 hover:bg-gray-50';
|
||||
|
||||
return (
|
||||
<li>
|
||||
<a
|
||||
href={`#${href}`}
|
||||
className={`flex items-center px-6 py-3 text-sm transition-colors duration-200 ${isActive ? activeClass : inactiveClass}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick(href);
|
||||
}}
|
||||
>
|
||||
<i className={`fa ${icon} w-6`}></i>
|
||||
<span>{label}</span>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
interface SidebarProps {
|
||||
activePage: Page;
|
||||
setActivePage: (page: Page) => void;
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
export const Sidebar: React.FC<SidebarProps> = ({ activePage, setActivePage, onLogout }) => {
|
||||
const queryItems = [
|
||||
{ href: 'query', icon: 'fa-search', label: '数据查询' },
|
||||
{ href: 'history', icon: 'fa-star', label: '收藏夹' },
|
||||
] as const;
|
||||
|
||||
const personalItems = [
|
||||
{ href: 'notifications', icon: 'fa-bell', label: '通知中心' },
|
||||
{ href: 'friends', icon: 'fa-users', label: '好友管理' },
|
||||
{ href: 'account', icon: 'fa-user', label: '账户管理' },
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<aside className="w-64 bg-white shadow-md h-screen flex-shrink-0 flex flex-col hidden lg:flex">
|
||||
<div className="p-4 border-b flex items-center space-x-2">
|
||||
<i className="fa fa-database text-primary text-2xl"></i>
|
||||
<h1 className="text-lg font-bold">数据查询中心</h1>
|
||||
</div>
|
||||
<nav className="py-4 flex-grow pt-8">
|
||||
<ul>
|
||||
<li className="px-6 py-2 text-xs text-gray-400 font-bold uppercase">查询中心</li>
|
||||
{queryItems.map(item => (
|
||||
<SidebarItem
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
icon={item.icon}
|
||||
label={item.label}
|
||||
isActive={activePage === item.href}
|
||||
onClick={setActivePage}
|
||||
/>
|
||||
))}
|
||||
|
||||
<li className="px-6 py-2 text-xs text-gray-400 font-bold uppercase mt-4">个人中心</li>
|
||||
{personalItems.map(item => (
|
||||
<SidebarItem
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
icon={item.icon}
|
||||
label={item.label}
|
||||
isActive={activePage === item.href}
|
||||
onClick={setActivePage}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="border-t p-4">
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="flex items-center text-gray-600 hover:text-danger transition-colors w-full">
|
||||
<i className="fa fa-sign-out w-6"></i>
|
||||
<span>退出登录</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,43 @@
|
||||
import React, { useState } from 'react';
|
||||
import { SysAdminPageType } from '../types';
|
||||
import { DashboardPage } from './admin/DashboardPage';
|
||||
import { UserManagementPage } from './admin/UserManagementPage';
|
||||
import { NotificationManagementPage } from './admin/NotificationManagementPage';
|
||||
import { SystemLogPage } from './admin/SystemLogPage';
|
||||
import { LLMConfigPage } from './admin/LLMConfigPage';
|
||||
import { AdminAccountPage } from './admin/AdminAccountPage';
|
||||
|
||||
interface SysAdminPageProps {
|
||||
activePage: SysAdminPageType;
|
||||
setActivePage: (page: SysAdminPageType) => void;
|
||||
}
|
||||
|
||||
export const SysAdminPage: React.FC<SysAdminPageProps> = ({ activePage, setActivePage }) => {
|
||||
const [initialLogStatusFilter, setInitialLogStatusFilter] = useState('');
|
||||
|
||||
const handleViewAbnormalLogs = () => {
|
||||
setInitialLogStatusFilter('failure');
|
||||
setActivePage('system-log');
|
||||
};
|
||||
|
||||
switch (activePage) {
|
||||
case 'dashboard':
|
||||
return <DashboardPage onViewAbnormalLogs={handleViewAbnormalLogs} />;
|
||||
case 'user-management':
|
||||
return <UserManagementPage />;
|
||||
case 'notification-management':
|
||||
return <NotificationManagementPage />;
|
||||
case 'system-log':
|
||||
return <SystemLogPage
|
||||
initialStatusFilter={initialLogStatusFilter}
|
||||
clearInitialFilter={() => setInitialLogStatusFilter('')}
|
||||
/>;
|
||||
case 'llm-config':
|
||||
return <LLMConfigPage />;
|
||||
case 'account':
|
||||
return <AdminAccountPage />;
|
||||
default:
|
||||
const exhaustiveCheck: never = activePage;
|
||||
return <div>Unknown page: {exhaustiveCheck}</div>;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { SysAdminPageType } from '../types';
|
||||
|
||||
interface SidebarItemProps {
|
||||
href: SysAdminPageType;
|
||||
icon: string;
|
||||
label: string;
|
||||
isActive: boolean;
|
||||
onClick: (page: SysAdminPageType) => void;
|
||||
}
|
||||
|
||||
const SidebarItem: React.FC<SidebarItemProps> = ({ href, icon, label, isActive, onClick }) => {
|
||||
const activeClass = 'bg-primary/10 text-primary border-l-4 border-primary';
|
||||
const inactiveClass = 'text-gray-600 hover:bg-gray-50';
|
||||
|
||||
return (
|
||||
<li>
|
||||
<a
|
||||
href={`#${href}`}
|
||||
className={`flex items-center px-6 py-3 text-sm transition-colors duration-200 ${isActive ? activeClass : inactiveClass}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick(href);
|
||||
}}
|
||||
>
|
||||
<i className={`fa ${icon} w-6`}></i>
|
||||
<span>{label}</span>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
interface SysAdminSidebarProps {
|
||||
activePage: SysAdminPageType;
|
||||
setActivePage: (page: SysAdminPageType) => void;
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
export const SysAdminSidebar: React.FC<SysAdminSidebarProps> = ({ activePage, setActivePage, onLogout }) => {
|
||||
const monitoringItems = [
|
||||
{ href: 'dashboard', icon: 'fa-tachometer', label: '仪表盘' },
|
||||
{ href: 'system-log', icon: 'fa-history', label: '系统日志' },
|
||||
] as const;
|
||||
|
||||
const managementItems = [
|
||||
{ href: 'user-management', icon: 'fa-users', label: '用户管理' },
|
||||
{ href: 'llm-config', icon: 'fa-cogs', label: '大模型配置' },
|
||||
{ href: 'notification-management', icon: 'fa-bullhorn', label: '通知管理' },
|
||||
] as const;
|
||||
|
||||
const personalItems = [
|
||||
{ href: 'account', icon: 'fa-user-circle-o', label: '我的账户' },
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<aside className="w-64 bg-white shadow-md h-screen flex-shrink-0 flex flex-col hidden lg:flex">
|
||||
<div className="p-4 border-b flex items-center space-x-2">
|
||||
<i className="fa fa-shield text-primary text-2xl"></i>
|
||||
<h1 className="text-lg font-bold">系统管理中心</h1>
|
||||
</div>
|
||||
<nav className="py-4 flex-grow pt-8">
|
||||
<ul>
|
||||
<li className="px-6 py-2 text-xs text-gray-400 font-bold uppercase">系统监控</li>
|
||||
{monitoringItems.map(item => (
|
||||
<SidebarItem key={item.href} {...item} isActive={activePage === item.href} onClick={setActivePage} />
|
||||
))}
|
||||
|
||||
<li className="px-6 py-2 text-xs text-gray-400 font-bold uppercase mt-4">核心管理</li>
|
||||
{managementItems.map(item => (
|
||||
<SidebarItem key={item.href} {...item} isActive={activePage === item.href} onClick={setActivePage} />
|
||||
))}
|
||||
|
||||
<li className="px-6 py-2 text-xs text-gray-400 font-bold uppercase mt-4">个人设置</li>
|
||||
{personalItems.map(item => (
|
||||
<SidebarItem key={item.href} {...item} isActive={activePage === item.href} onClick={setActivePage} />
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="border-t p-4">
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="flex items-center text-gray-600 hover:text-danger transition-colors w-full">
|
||||
<i className="fa fa-sign-out w-6"></i>
|
||||
<span>退出登录</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
interface AdminModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AdminModal: React.FC<AdminModalProps> = ({ isOpen, onClose, title, children }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={onClose}>
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg mx-4 fade-in" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="p-4 border-b flex justify-between items-center">
|
||||
<h3 className="font-bold text-lg">{title}</h3>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||
<i className="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,243 @@
|
||||
import React, { useState } from 'react';
|
||||
import { SysAdminPageType } from '../../types';
|
||||
import { AdminModal } from './AdminModal';
|
||||
import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, PointElement, LineElement, Title } from 'chart.js';
|
||||
import { Doughnut, Line, Bar } from 'react-chartjs-2';
|
||||
|
||||
ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, PointElement, LineElement, Title);
|
||||
|
||||
type StatCardColor = 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
|
||||
|
||||
interface StatCardProps {
|
||||
icon: string;
|
||||
title: string;
|
||||
value: string;
|
||||
change: { value: string; type: 'positive' | 'negative' | 'neutral' };
|
||||
color: StatCardColor;
|
||||
}
|
||||
|
||||
const StatCard: React.FC<StatCardProps> = ({ icon, title, value, change, color }) => {
|
||||
const changeColor = change.type === 'positive' ? 'text-success' : change.type === 'negative' ? 'text-danger' : 'text-gray-500';
|
||||
const changeIcon = change.type === 'positive' ? 'fa-arrow-up' : change.type === 'negative' ? 'fa-arrow-down' : 'fa-minus';
|
||||
|
||||
const colorMapping: Record<StatCardColor, { bg: string; text: string }> = {
|
||||
primary: { bg: 'bg-primary/10', text: 'text-primary' },
|
||||
secondary: { bg: 'bg-secondary/10', text: 'text-secondary' },
|
||||
success: { bg: 'bg-success/10', text: 'text-success' },
|
||||
warning: { bg: 'bg-warning/10', text: 'text-warning' },
|
||||
danger: { bg: 'bg-danger/10', text: 'text-danger' },
|
||||
};
|
||||
|
||||
const { bg, text } = colorMapping[color];
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl p-6 shadow-sm card-hover">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm">{title}</p>
|
||||
<h3 className="text-3xl font-bold mt-2">{value}</h3>
|
||||
<p className={`${changeColor} text-sm mt-2`}><i className={`fa ${changeIcon}`}></i> {change.value}</p>
|
||||
</div>
|
||||
<div className={`w-12 h-12 rounded-full ${bg} flex items-center justify-center ${text}`}>
|
||||
<i className={`fa ${icon} text-xl`}></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const showToast = (message: string, type: 'success' | 'error' = 'success') => {
|
||||
const toast = document.createElement('div');
|
||||
const colors = {
|
||||
success: 'bg-green-500 text-white',
|
||||
error: 'bg-red-500 text-white'
|
||||
};
|
||||
toast.className = `fixed top-5 right-5 px-6 py-3 rounded-lg shadow-lg z-50 transition-all duration-300 transform translate-x-full ${colors[type]}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-x-full');
|
||||
toast.classList.add('translate-x-0');
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-x-0');
|
||||
toast.classList.add('translate-x-full');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
|
||||
export const DashboardPage: React.FC<{ onViewAbnormalLogs: () => void }> = ({ onViewAbnormalLogs }) => {
|
||||
const [isExportModalOpen, setExportModalOpen] = useState(false);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
const handleRefresh = () => {
|
||||
setIsRefreshing(true);
|
||||
setTimeout(() => {
|
||||
setIsRefreshing(false);
|
||||
showToast('仪表盘数据已刷新', 'success');
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const handleExport = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const reportType = formData.get('report-type');
|
||||
const format = formData.get('export-format');
|
||||
|
||||
showToast(`正在导出 ${reportType} 报告 (${format})...`, 'success');
|
||||
|
||||
setTimeout(() => {
|
||||
const fakeFile = new Blob(['This is a mock report file.'], { type: 'text/plain' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(fakeFile);
|
||||
link.download = `report-${reportType}.${format}`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}, 1000);
|
||||
|
||||
setExportModalOpen(false);
|
||||
};
|
||||
|
||||
// --- MOCK DATA FOR NEW CHARTS ---
|
||||
|
||||
const queryVolumeData = {
|
||||
labels: Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, '0')}:00`),
|
||||
datasets: [{
|
||||
label: '查询量',
|
||||
data: [12, 10, 8, 15, 25, 30, 45, 60, 80, 110, 150, 120, 100, 90, 85, 95, 120, 180, 220, 160, 110, 80, 50, 30],
|
||||
borderColor: '#165DFF',
|
||||
backgroundColor: 'rgba(22, 93, 255, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
}],
|
||||
};
|
||||
|
||||
const responseTimeData = {
|
||||
labels: ['7天前', '6天前', '5天前', '4天前', '3天前', '昨天', '今天'],
|
||||
datasets: [{
|
||||
label: '平均响应时间 (ms)',
|
||||
data: [850, 860, 840, 900, 880, 920, 950],
|
||||
borderColor: '#00B42A',
|
||||
backgroundColor: 'rgba(0, 180, 42, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
}],
|
||||
};
|
||||
|
||||
const errorChartData = {
|
||||
labels: ['模型调用超时', '数据库连接错误', '用户认证失败', '其他'],
|
||||
datasets: [{
|
||||
data: [5, 2, 8, 3],
|
||||
backgroundColor: ['#FF7D00', '#F53F3F', '#FADC19', '#86909C'],
|
||||
borderWidth: 0,
|
||||
}]
|
||||
};
|
||||
|
||||
const costChartData = {
|
||||
labels: ['gemini-2.5-pro', 'GPT-4', 'GLM-4.6', 'qwen3-max'],
|
||||
datasets: [{
|
||||
label: '今日预估成本 (USD)',
|
||||
data: [12.5, 8.2, 0, 0],
|
||||
backgroundColor: ['#165DFF', '#00B42A', '#86909C', '#36BFFA'],
|
||||
borderRadius: 4,
|
||||
}],
|
||||
};
|
||||
|
||||
const commonLineChartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: { y: { beginAtZero: false }, x: { grid: { display: false } } }
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex space-x-2">
|
||||
<button onClick={handleRefresh} disabled={isRefreshing} className="bg-white border border-gray-300 rounded-lg px-4 py-2 text-sm btn-effect disabled:opacity-50">
|
||||
<i className={`fa ${isRefreshing ? 'fa-refresh fa-spin' : 'fa-refresh'} mr-1`}></i> {isRefreshing ? '刷新中...' : '刷新'}
|
||||
</button>
|
||||
<button onClick={() => setExportModalOpen(true)} className="bg-primary text-white rounded-lg px-4 py-2 text-sm btn-effect">
|
||||
<i className="fa fa-download mr-1"></i> 导出报告
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-6 shadow-sm card-hover">
|
||||
<h3 className="font-bold mb-4">系统健康状态</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="flex items-center p-4 border rounded-lg"><span className="health-status-indicator health-status-healthy"></span><div><p className="font-medium">数据库服务</p><p className="text-sm text-gray-500">延迟: 45ms</p></div></div>
|
||||
<div className="flex items-center p-4 border rounded-lg"><span className="health-status-indicator health-status-warning"></span><div><p className="font-medium">缓存服务</p><p className="text-sm text-gray-500">延迟: 120ms</p></div></div>
|
||||
<div className="flex items-center p-4 border rounded-lg"><span className="health-status-indicator health-status-healthy"></span><div><p className="font-medium">大模型服务</p><p className="text-sm text-gray-500">延迟: 200ms</p></div></div>
|
||||
<div className="flex items-center p-4 border rounded-lg"><span className="health-status-indicator health-status-danger"></span><div><p className="font-medium">存储服务</p><p className="text-sm text-gray-500">使用率: 95%</p></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-6">
|
||||
<StatCard icon="fa-user-circle-o" title="总用户数" value="1,284" change={{ value: '12%', type: 'positive' }} color="primary" />
|
||||
<StatCard icon="fa-plug" title="数据源" value="8" change={{ value: '', type: 'neutral' }} color="secondary" />
|
||||
<StatCard icon="fa-search" title="今日查询" value="3,280" change={{ value: '8%', type: 'positive' }} color="success" />
|
||||
<StatCard icon="fa-bolt" title="今日Token消耗" value="1.2M" change={{ value: '15%', type: 'negative' }} color="warning" />
|
||||
<div className="cursor-pointer" onClick={onViewAbnormalLogs}>
|
||||
<StatCard icon="fa-exclamation-triangle" title="异常日志" value="18" change={{ value: '2', type: 'negative' }} color="danger" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-6 shadow-sm card-hover">
|
||||
<h3 className="font-bold mb-4">性能趋势</h3>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm text-center mb-2">过去24小时查询量</h4>
|
||||
<div className="h-64"><Line data={queryVolumeData} options={commonLineChartOptions} /></div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm text-center mb-2">平均查询响应时间 (近7日)</h4>
|
||||
<div className="h-64"><Line data={responseTimeData} options={commonLineChartOptions} /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="bg-white rounded-xl p-6 shadow-sm card-hover">
|
||||
<h3 className="font-bold mb-4">关键错误分类</h3>
|
||||
<div className="h-64 relative"><Doughnut data={errorChartData} options={{ responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right' } } }} /></div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-6 shadow-sm card-hover">
|
||||
<h3 className="font-bold mb-4">模型成本预估</h3>
|
||||
<div className="h-64 relative">
|
||||
<Bar data={costChartData} options={{ responsive: true, maintainAspectRatio: false, indexAxis: 'y' as const, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false } } } }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AdminModal isOpen={isExportModalOpen} onClose={() => setExportModalOpen(false)} title="导出系统报告">
|
||||
<form onSubmit={handleExport} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-gray-700 text-sm mb-2">报告类型</label>
|
||||
<select name="report-type" className="w-full px-4 py-2 border border-gray-300 rounded-lg"><option value="system-overview">系统概览报告</option><option value="user-statistics">用户统计报告</option></select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-gray-700 text-sm mb-2">时间范围</label>
|
||||
<div className="grid grid-cols-2 gap-2"><input name="start-date" type="date" className="px-3 py-2 border rounded-lg" /><input name="end-date" type="date" className="px-3 py-2 border rounded-lg" /></div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-gray-700 text-sm mb-2">导出格式</label>
|
||||
<div className="flex space-x-4"><label className="flex items-center"><input type="radio" name="export-format" value="csv" defaultChecked className="mr-2" /><span>CSV</span></label><label className="flex items-center"><input type="radio" name="export-format" value="json" className="mr-2" /><span>JSON</span></label></div>
|
||||
</div>
|
||||
<div className="pt-4 flex justify-end space-x-2">
|
||||
<button type="button" onClick={() => setExportModalOpen(false)} className="px-4 py-2 border rounded-lg">取消</button>
|
||||
<button type="submit" className="px-4 py-2 bg-primary text-white rounded-lg">导出</button>
|
||||
</div>
|
||||
</form>
|
||||
</AdminModal>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import { DataAdminPageType } from '../../types';
|
||||
import { MOCK_DATASOURCES, MOCK_CONNECTION_LOGS, MOCK_PERMISSION_LOGS, MOCK_QUERY_LOAD } from '../../constants';
|
||||
import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement } from 'chart.js';
|
||||
import { Pie, Bar } from 'react-chartjs-2';
|
||||
|
||||
ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement);
|
||||
|
||||
// Reusable StatCard component specific for this dashboard
|
||||
const StatCard: React.FC<{ title: string; value: string; icon: string; color: string; onClick: () => void; }> = ({ title, value, icon, color, onClick }) => (
|
||||
<div onClick={onClick} className="bg-white p-6 rounded-xl shadow-sm cursor-pointer transition-all duration-200 hover:shadow-md hover:-translate-y-1">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">{title}</p>
|
||||
<h3 className="text-3xl font-bold mt-2">{value}</h3>
|
||||
</div>
|
||||
<div className={`w-12 h-12 rounded-full ${color} flex items-center justify-center text-white`}>
|
||||
<i className={`fa ${icon} text-xl`}></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Helper to format timestamp into a relative string
|
||||
const formatTimeAgo = (timestamp: string): string => {
|
||||
const now = new Date();
|
||||
const then = new Date(timestamp);
|
||||
const diffInSeconds = Math.round((now.getTime() - then.getTime()) / 1000);
|
||||
|
||||
if (diffInSeconds < 60) return `${diffInSeconds}秒前`;
|
||||
const diffInMinutes = Math.round(diffInSeconds / 60);
|
||||
if (diffInMinutes < 60) return `${diffInMinutes}分钟前`;
|
||||
const diffInHours = Math.round(diffInMinutes / 60);
|
||||
if (diffInHours < 24) return `${diffInHours}小时前`;
|
||||
const diffInDays = Math.round(diffInHours / 24);
|
||||
return `${diffInDays}天前`;
|
||||
};
|
||||
|
||||
export const DataAdminDashboardPage: React.FC<{ setActivePage: (page: DataAdminPageType) => void; }> = ({ setActivePage }) => {
|
||||
// Data processing for cards and charts
|
||||
const connectedCount = MOCK_DATASOURCES.filter(ds => ds.status === 'connected').length;
|
||||
const errorCount = MOCK_CONNECTION_LOGS.filter(log => log.status === '失败').length;
|
||||
const pendingRequests = 3; // Mock value
|
||||
|
||||
const healthStatusCounts = MOCK_DATASOURCES.reduce((acc, ds) => {
|
||||
const statusMap = { connected: '已连接', disconnected: '未连接', error: '错误', testing: '测试中', disabled: '已禁用' };
|
||||
const status = statusMap[ds.status];
|
||||
acc[status] = (acc[status] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
const healthStatusData = {
|
||||
labels: Object.keys(healthStatusCounts),
|
||||
datasets: [{
|
||||
data: Object.values(healthStatusCounts),
|
||||
backgroundColor: ['#00B42A', '#86909C', '#F53F3F', '#36BFFA', '#C9CDD4'],
|
||||
borderWidth: 0,
|
||||
}],
|
||||
};
|
||||
|
||||
const queryLoadData = {
|
||||
labels: MOCK_QUERY_LOAD.labels,
|
||||
datasets: [{
|
||||
label: '查询量',
|
||||
data: MOCK_QUERY_LOAD.data,
|
||||
backgroundColor: '#165DFF',
|
||||
borderRadius: 4,
|
||||
}],
|
||||
};
|
||||
|
||||
const recentFailures = MOCK_CONNECTION_LOGS.filter(log => log.status === '失败').slice(0, 5);
|
||||
|
||||
return (
|
||||
<main className="flex-1 overflow-y-auto p-6 space-y-6 bg-neutral">
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatCard title="数据源总数" value={`${MOCK_DATASOURCES.length}`} icon="fa-database" color="bg-primary" onClick={() => setActivePage('datasource')} />
|
||||
<StatCard title="当前连接数" value={`${connectedCount}`} icon="fa-check-circle" color="bg-success" onClick={() => setActivePage('datasource')} />
|
||||
<StatCard title="连接错误数" value={`${errorCount}`} icon="fa-exclamation-triangle" color="bg-danger" onClick={() => setActivePage('connection-log')} />
|
||||
<StatCard title="待处理权限请求" value={`${pendingRequests}`} icon="fa-key" color="bg-warning" onClick={() => setActivePage('user-permission')} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||||
<div className="lg:col-span-2 bg-white rounded-xl p-6 shadow-sm">
|
||||
<h3 className="font-bold mb-4">数据源健康状态</h3>
|
||||
<div className="h-64 relative"><Pie data={healthStatusData} options={{ responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right' } } }} /></div>
|
||||
</div>
|
||||
<div className="lg:col-span-3 bg-white rounded-xl p-6 shadow-sm">
|
||||
<h3 className="font-bold mb-4">数据源查询量 Top 5</h3>
|
||||
<div className="h-64 relative"><Bar data={queryLoadData} options={{ responsive: true, maintainAspectRatio: false, indexAxis: 'y' as const, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false } } } }} /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="bg-white rounded-xl p-6 shadow-sm">
|
||||
<h3 className="font-bold mb-4">近期连接失败日志</h3>
|
||||
<div className="space-y-3">
|
||||
{recentFailures.length > 0 ? recentFailures.map(log => (
|
||||
<div key={log.id} className="flex justify-between items-center text-sm p-2 rounded-lg hover:bg-gray-50">
|
||||
<div>
|
||||
<p className="font-medium text-danger">{log.datasource}: {log.note}</p>
|
||||
<p className="text-xs text-gray-500">{log.time}</p>
|
||||
</div>
|
||||
<button onClick={() => setActivePage('connection-log')} className="text-primary text-xs hover:underline">查看详情</button>
|
||||
</div>
|
||||
)) : <p className="text-sm text-gray-500 text-center py-4">无失败记录</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-6 shadow-sm">
|
||||
<h3 className="font-bold mb-4">近期权限变更动态</h3>
|
||||
<div className="space-y-4">
|
||||
{MOCK_PERMISSION_LOGS.slice(0, 4).map(log => (
|
||||
<div key={log.id} className="flex items-start space-x-3 text-sm">
|
||||
<i className="fa fa-user-secret text-gray-400 mt-1"></i>
|
||||
<div className="flex-1">
|
||||
<p dangerouslySetInnerHTML={{ __html: log.text }}></p>
|
||||
<p className="text-xs text-gray-400">{formatTimeAgo(log.timestamp)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,434 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { AdminModal } from '../admin/AdminModal';
|
||||
import { UserPermissionAssignment, UnassignedUser, DataSourcePermission } from '../../types';
|
||||
import { userDbPermissionApi, UserDbPermission, userApi, User, dbConnectionApi, DbConnection } from '../../services/api';
|
||||
|
||||
export const UserPermissionPage: React.FC = () => {
|
||||
const [unassignedUsers, setUnassignedUsers] = useState<UnassignedUser[]>([]);
|
||||
const [assignedPermissions, setAssignedPermissions] = useState<UserPermissionAssignment[]>([]);
|
||||
const [dataSources, setDataSources] = useState<DbConnection[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 定义支持的搜索类别
|
||||
type SearchCategory = 'all' | 'username' | 'email' | 'datasource' | 'table';
|
||||
|
||||
// 加载数据
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 加载所有用户
|
||||
const allUsers = await userApi.getList();
|
||||
// 加载已分配权限
|
||||
const assignedPerms = await userDbPermissionApi.getAssigned();
|
||||
// 加载数据源
|
||||
const connections = await dbConnectionApi.getList();
|
||||
setDataSources(connections);
|
||||
|
||||
// 解析已分配权限
|
||||
const parsedAssigned: UserPermissionAssignment[] = assignedPerms.map(perm => {
|
||||
const user = allUsers.find(u => u.id === perm.userId);
|
||||
let permissions: DataSourcePermission[] = [];
|
||||
try {
|
||||
const details = JSON.parse(perm.permissionDetails || '[]');
|
||||
permissions = details.map((detail: any) => {
|
||||
const conn = connections.find(c => c.id === detail.db_connection_id);
|
||||
return {
|
||||
dataSourceId: String(detail.db_connection_id),
|
||||
dataSourceName: conn?.name || '未知数据源',
|
||||
tables: detail.table_ids?.map((tid: number) => `table_${tid}`) || [],
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('解析权限详情失败:', e);
|
||||
}
|
||||
return {
|
||||
id: String(perm.id),
|
||||
userId: String(perm.userId),
|
||||
username: user?.username || '未知用户',
|
||||
permissions,
|
||||
};
|
||||
});
|
||||
|
||||
// 找出未分配权限的用户(所有用户中不在已分配列表中的)
|
||||
const assignedUserIds = new Set(assignedPerms.map(p => p.userId));
|
||||
const unassigned: UnassignedUser[] = allUsers
|
||||
.filter(u => !assignedUserIds.has(u.id))
|
||||
.map(u => ({
|
||||
id: String(u.id),
|
||||
username: u.username,
|
||||
email: u.email,
|
||||
regTime: new Date().toISOString().split('T')[0],
|
||||
}));
|
||||
|
||||
setAssignedPermissions(parsedAssigned);
|
||||
setUnassignedUsers(unassigned);
|
||||
} catch (error) {
|
||||
console.error('加载权限数据失败:', error);
|
||||
alert('加载权限数据失败: ' + (error instanceof Error ? error.message : '未知错误'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const [selectedUserIds, setSelectedUserIds] = useState<Set<string>>(new Set());
|
||||
const [modal, setModal] = useState<'assign' | 'manage' | null>(null);
|
||||
const [currentItem, setCurrentItem] = useState<UserPermissionAssignment | null>(null);
|
||||
const [usersToAssign, setUsersToAssign] = useState<UnassignedUser[]>([]);
|
||||
const [searchKeyword, setSearchKeyword] = useState<string>('');
|
||||
const [searchCategory, setSearchCategory] = useState<SearchCategory>('all');
|
||||
|
||||
// 2. 优化:按「搜索类别+关键词」过滤待分配用户
|
||||
const filteredUnassignedUsers = useMemo(() => {
|
||||
const keyword = searchKeyword.toLowerCase().trim();
|
||||
if (!keyword) return unassignedUsers;
|
||||
|
||||
return unassignedUsers.filter(user => {
|
||||
switch (searchCategory) {
|
||||
case 'username':
|
||||
return user.username.toLowerCase().includes(keyword);
|
||||
case 'email':
|
||||
return user.email.toLowerCase().includes(keyword);
|
||||
case 'all':
|
||||
return user.username.toLowerCase().includes(keyword) || user.email.toLowerCase().includes(keyword);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}, [unassignedUsers, searchKeyword, searchCategory]);
|
||||
|
||||
// 3. 优化:按「搜索类别+关键词」过滤已分配用户
|
||||
const filteredAssignedPermissions = useMemo(() => {
|
||||
const keyword = searchKeyword.toLowerCase().trim();
|
||||
if (!keyword) return assignedPermissions;
|
||||
|
||||
return assignedPermissions.filter(assignment => {
|
||||
switch (searchCategory) {
|
||||
case 'username':
|
||||
return assignment.username.toLowerCase().includes(keyword);
|
||||
case 'email': // 已分配用户无邮箱字段,不匹配
|
||||
return false;
|
||||
case 'datasource':
|
||||
return assignment.permissions.some(perm => perm.dataSourceName.toLowerCase().includes(keyword));
|
||||
case 'table':
|
||||
return assignment.permissions.some(perm => perm.tables.some(table => table.toLowerCase().includes(keyword)));
|
||||
case 'all': // 全部:匹配用户名/数据源/表名
|
||||
return assignment.username.toLowerCase().includes(keyword) ||
|
||||
assignment.permissions.some(perm => perm.dataSourceName.toLowerCase().includes(keyword)) ||
|
||||
assignment.permissions.some(perm => perm.tables.some(table => table.toLowerCase().includes(keyword)));
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}, [assignedPermissions, searchKeyword, searchCategory]);
|
||||
|
||||
const handleSelectAll = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedUserIds(new Set(filteredUnassignedUsers.map(u => u.id)));
|
||||
} else {
|
||||
setSelectedUserIds(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectUser = (id: string, checked: boolean) => {
|
||||
const newSet = new Set(selectedUserIds);
|
||||
if (checked) newSet.add(id);
|
||||
else newSet.delete(id);
|
||||
setSelectedUserIds(newSet);
|
||||
};
|
||||
|
||||
const openAssignModal = (users: UnassignedUser[]) => {
|
||||
if (users.length === 0) return;
|
||||
setUsersToAssign(users);
|
||||
setCurrentItem(null);
|
||||
setModal('assign');
|
||||
};
|
||||
|
||||
const openManageModal = (permission: UserPermissionAssignment) => {
|
||||
setCurrentItem(permission);
|
||||
setModal('manage');
|
||||
};
|
||||
|
||||
const handleSavePermissions = async (userIds: string[], permissions: DataSourcePermission[]) => {
|
||||
try {
|
||||
const filteredPerms = permissions.filter(p => p.tables.length > 0);
|
||||
|
||||
if (modal === 'assign') {
|
||||
// 为多个用户分配权限
|
||||
const currentUserId = Number(sessionStorage.getItem('userId') || '1');
|
||||
const permissionDetails = filteredPerms.map(p => ({
|
||||
db_connection_id: Number(p.dataSourceId),
|
||||
table_ids: p.tables.map(t => Number(t.replace('table_', ''))),
|
||||
}));
|
||||
|
||||
for (const userId of userIds) {
|
||||
await userDbPermissionApi.create({
|
||||
userId: Number(userId),
|
||||
permissionDetails: JSON.stringify(permissionDetails),
|
||||
isAssigned: 1,
|
||||
lastGrantUserId: currentUserId,
|
||||
});
|
||||
}
|
||||
alert('分配权限成功');
|
||||
await loadData();
|
||||
} else if (modal === 'manage' && currentItem) {
|
||||
// 更新权限
|
||||
const permissionDetails = filteredPerms.map(p => ({
|
||||
db_connection_id: Number(p.dataSourceId),
|
||||
table_ids: p.tables.map(t => Number(t.replace('table_', ''))),
|
||||
}));
|
||||
await userDbPermissionApi.update({
|
||||
id: Number(currentItem.id),
|
||||
permissionDetails: JSON.stringify(permissionDetails),
|
||||
lastGrantUserId: Number(sessionStorage.getItem('userId') || '1'),
|
||||
});
|
||||
alert('更新权限成功');
|
||||
await loadData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存权限失败:', error);
|
||||
alert('保存权限失败: ' + (error instanceof Error ? error.message : '未知错误'));
|
||||
return;
|
||||
}
|
||||
setModal(null);
|
||||
};
|
||||
|
||||
const PermissionModal: React.FC<{
|
||||
users: { id: string, username: string }[];
|
||||
existingPermissions?: DataSourcePermission[];
|
||||
onSave: (userIds: string[], permissions: DataSourcePermission[]) => void;
|
||||
onClose: () => void;
|
||||
}> = ({ users, existingPermissions = [], onSave, onClose }) => {
|
||||
const [perms, setPerms] = useState<DataSourcePermission[]>(
|
||||
dataSources.map(ds => {
|
||||
const existing = existingPermissions.find(p => p.dataSourceId === String(ds.id));
|
||||
return {
|
||||
dataSourceId: String(ds.id),
|
||||
dataSourceName: ds.name,
|
||||
tables: existing ? [...existing.tables] : []
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const handleTableToggle = (dsId: string, table: string, checked: boolean) => {
|
||||
setPerms(prev => prev.map(p => {
|
||||
if (p.dataSourceId === dsId) {
|
||||
const newTables = new Set(p.tables);
|
||||
if (checked) newTables.add(table);
|
||||
else newTables.delete(table);
|
||||
return { ...p, tables: Array.from(newTables) };
|
||||
}
|
||||
return p;
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSelectAllTables = (dsId: string, checked: boolean) => {
|
||||
// 简化处理:假设每个数据源有默认的表列表
|
||||
// 实际应该从后端获取表列表
|
||||
const defaultTables = ['table_1', 'table_2', 'table_3']; // 临时处理
|
||||
setPerms(prev => prev.map(p => p.dataSourceId === dsId ? { ...p, tables: checked ? defaultTables : [] } : p));
|
||||
};
|
||||
|
||||
const title = users.length > 1 ? `为 ${users.length} 位用户分配权限` : `为 ${users[0].username} 分配权限`;
|
||||
const isEditing = existingPermissions.length > 0;
|
||||
|
||||
return (
|
||||
<AdminModal isOpen={true} onClose={onClose} title={isEditing ? `管理 ${users[0].username} 的权限` : title}>
|
||||
<div className="space-y-4 max-h-[60vh] overflow-y-auto pr-2">
|
||||
{dataSources.map(ds => {
|
||||
const currentPerm = perms.find(p => p.dataSourceId === String(ds.id));
|
||||
// 简化处理:使用默认表列表,实际应该从后端获取
|
||||
const allTablesForDs = ['table_1', 'table_2', 'table_3'];
|
||||
const allSelected = currentPerm ? currentPerm.tables.length === allTablesForDs.length : false;
|
||||
|
||||
return (
|
||||
<div key={ds.id} className="p-3 border rounded-lg">
|
||||
<h4 className="font-semibold mb-2">{ds.name}</h4>
|
||||
<div className="border-t pt-2">
|
||||
<label className="flex items-center mb-2 font-medium text-sm">
|
||||
<input type="checkbox" onChange={(e) => handleSelectAllTables(String(ds.id), e.target.checked)} checked={allSelected} className="mr-2 h-4 w-4" />
|
||||
全选
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
{allTablesForDs.map(table => (
|
||||
<label key={table} className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={currentPerm?.tables.includes(table) || false}
|
||||
onChange={(e) => handleTableToggle(String(ds.id), table, e.target.checked)}
|
||||
className="mr-2 h-4 w-4"
|
||||
/>
|
||||
{table}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2 mt-6">
|
||||
<button onClick={onClose} className="px-4 py-2 border rounded-lg">取消</button>
|
||||
<button onClick={() => onSave(users.map(u => u.id), perms)} className="px-4 py-2 bg-primary text-white rounded-lg">保存</button>
|
||||
</div>
|
||||
</AdminModal>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<main className="flex-1 overflow-y-auto p-6 space-y-8">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-center">
|
||||
<i className="fa fa-spinner fa-spin text-3xl text-primary mb-4"></i>
|
||||
<p className="text-gray-500">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex-1 overflow-y-auto p-6 space-y-8">
|
||||
<div className="w-full max-w-2xl flex items-center gap-3">
|
||||
<select
|
||||
value={searchCategory}
|
||||
onChange={(e) => setSearchCategory(e.target.value as SearchCategory)}
|
||||
className="px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50 bg-white font-bold"
|
||||
>
|
||||
<option value="all">全部</option>
|
||||
<option value="username">用户名</option>
|
||||
<option value="email">邮箱</option>
|
||||
<option value="datasource">数据源名</option>
|
||||
<option value="table">表名</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={`搜索${
|
||||
searchCategory === 'all' ? '(用户名/数据源/表名)' :
|
||||
searchCategory === 'username' ? '用户名' :
|
||||
searchCategory === 'email' ? '邮箱' :
|
||||
searchCategory === 'datasource' ? '数据源名' : '表名'
|
||||
}`}
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-bold">待分配权限用户 ({filteredUnassignedUsers.length})</h2>
|
||||
<button
|
||||
onClick={() => openAssignModal(filteredUnassignedUsers.filter(u => selectedUserIds.has(u.id)))}
|
||||
disabled={selectedUserIds.size === 0}
|
||||
className="bg-primary text-white rounded-lg px-4 py-2 text-sm btn-effect disabled:bg-primary/50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<i className="fa fa-key mr-1"></i> 批量分配权限
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th className="px-6 py-3 text-left w-12">
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={handleSelectAll}
|
||||
checked={filteredUnassignedUsers.length > 0 && selectedUserIds.size === filteredUnassignedUsers.length}
|
||||
/>
|
||||
</th>
|
||||
<th className="text-left px-6 py-3">用户名</th>
|
||||
<th className="text-left px-6 py-3">邮箱</th>
|
||||
<th className="text-left px-6 py-3">注册时间</th>
|
||||
<th className="text-left px-6 py-3">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredUnassignedUsers.map(user => (
|
||||
<tr key={user.id} className="border-b hover:bg-gray-50">
|
||||
<td className="px-6 py-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedUserIds.has(user.id)}
|
||||
onChange={(e) => handleSelectUser(user.id, e.target.checked)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-6 py-4">{user.username}</td>
|
||||
<td className="px-6 py-4">{user.email}</td>
|
||||
<td className="px-6 py-4">{user.regTime}</td>
|
||||
<td className="px-6 py-4">
|
||||
<button onClick={() => openAssignModal([user])} className="text-primary hover:underline">分配权限</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{filteredUnassignedUsers.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-6 py-8 text-center text-gray-500">未找到匹配的待分配用户</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold mb-4">已分配权限用户 ({filteredAssignedPermissions.length})</h2>
|
||||
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th className="text-left px-6 py-3">用户名</th>
|
||||
<th className="text-left px-6 py-3">数据源权限</th>
|
||||
<th className="text-left px-6 py-3">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredAssignedPermissions.map(p => (
|
||||
<tr key={p.id} className="border-b hover:bg-gray-50">
|
||||
<td className="px-6 py-4 font-medium">{p.username}</td>
|
||||
<td className="px-6 py-4">
|
||||
{p.permissions.map(perm => (
|
||||
<div key={perm.dataSourceId} className="mb-1">
|
||||
<span className="font-semibold">{perm.dataSourceName}:</span>
|
||||
<span className="text-gray-600">{perm.tables.join(', ')}</span>
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<button onClick={() => openManageModal(p)} className="text-primary hover:underline">管理权限</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{filteredAssignedPermissions.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={3} className="px-6 py-8 text-center text-gray-500">未找到匹配的已分配权限用户</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(modal === 'assign' && usersToAssign.length > 0) && (
|
||||
<PermissionModal
|
||||
users={usersToAssign}
|
||||
onSave={handleSavePermissions}
|
||||
onClose={() => setModal(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(modal === 'manage' && currentItem) && (
|
||||
<PermissionModal
|
||||
users={[currentItem]}
|
||||
existingPermissions={currentItem.permissions}
|
||||
onSave={handleSavePermissions}
|
||||
onClose={() => setModal(null)}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,14 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
// 与 .env.local 中的变量一一对应
|
||||
readonly VITE_GEMINI_API_KEY: string;
|
||||
readonly VITE_OPENAI_API_KEY: string;
|
||||
readonly VITE_GLM_API_KEY: string;
|
||||
readonly VITE_QWEN_API_KEY: string;
|
||||
readonly VITE_KIMI_API_KEY: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@ -0,0 +1,158 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { queryCollectionApi, collectionRecordApi } from '../services/api';
|
||||
|
||||
export interface QueryCollection {
|
||||
id: number;
|
||||
userId: number;
|
||||
collectionName: string;
|
||||
description?: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
export interface CollectionRecord {
|
||||
id: string;
|
||||
collectionId: number;
|
||||
queryLogId: number;
|
||||
addTime: string;
|
||||
}
|
||||
|
||||
export const useQueryCollection = (userId: number) => {
|
||||
const [collections, setCollections] = useState<QueryCollection[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadCollections = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await queryCollectionApi.getByUser(userId);
|
||||
setCollections(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '加载收藏夹失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
loadCollections();
|
||||
}
|
||||
}, [userId]);
|
||||
|
||||
const createCollection = async (name: string, description?: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const newCollection = await queryCollectionApi.create({
|
||||
userId,
|
||||
collectionName: name,
|
||||
description,
|
||||
});
|
||||
setCollections(prev => [...prev, newCollection]);
|
||||
return newCollection;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '创建收藏夹失败');
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateCollection = async (id: number, name: string, description?: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const updated = await queryCollectionApi.update({
|
||||
id,
|
||||
collectionName: name,
|
||||
description,
|
||||
});
|
||||
setCollections(prev => prev.map(c => c.id === id ? updated : c));
|
||||
return updated;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '更新收藏夹失败');
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteCollection = async (id: number) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await queryCollectionApi.delete(id);
|
||||
setCollections(prev => prev.filter(c => c.id !== id));
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '删除收藏夹失败');
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addQueryToCollection = async (collectionId: number, queryLogId: number) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await collectionRecordApi.create({
|
||||
collectionId,
|
||||
queryLogId,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '添加到收藏夹失败');
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const removeQueryFromCollection = async (recordId: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await collectionRecordApi.delete(recordId);
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '从收藏夹移除失败');
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getCollectionRecords = async (collectionId: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const records = await collectionRecordApi.getByCollection(collectionId);
|
||||
return records;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '获取收藏记录失败');
|
||||
return [];
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
collections,
|
||||
loading,
|
||||
error,
|
||||
loadCollections,
|
||||
createCollection,
|
||||
updateCollection,
|
||||
deleteCollection,
|
||||
addQueryToCollection,
|
||||
removeQueryFromCollection,
|
||||
getCollectionRecords,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
import { useState } from 'react';
|
||||
import { queryShareApi, queryLogApi } from '../services/api';
|
||||
|
||||
export const useQueryShare = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const shareQuery = async (queryLogId: number, receiveUserId: number) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await queryShareApi.create({
|
||||
shareUserId: Number(sessionStorage.getItem('userId') || '1'),
|
||||
receiveUserId,
|
||||
queryLogId,
|
||||
receiveStatus: 0,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '分享失败');
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const markAsRead = async (shareId: number) => {
|
||||
try {
|
||||
await queryShareApi.update({
|
||||
id: shareId,
|
||||
receiveStatus: 1,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '标记失败');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteShare = async (shareId: number) => {
|
||||
try {
|
||||
await queryShareApi.delete(shareId);
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '删除失败');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
shareQuery,
|
||||
markAsRead,
|
||||
deleteShare,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>自然语言数据库查询系统</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#165DFF',
|
||||
secondary: '#36BFFA',
|
||||
neutral: '#F5F7FA',
|
||||
dark: '#1D2129',
|
||||
success: '#00B42A',
|
||||
warning: '#FF7D00',
|
||||
danger: '#F53F3F',
|
||||
},
|
||||
fontFamily: {
|
||||
inter: ['Inter', 'sans-serif']
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
/* For Chart.js responsiveness */
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 256px; /* h-64 */
|
||||
width: 100%;
|
||||
}
|
||||
.animated-gradient {
|
||||
background: linear-gradient(-45deg, #eef2ff, #f3e8ff, #dbeafe, #e0f2fe);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient-animation 15s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradient-animation {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
</style>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react": "https://aistudiocdn.com/react@^19.2.0",
|
||||
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
|
||||
"react/": "https://aistudiocdn.com/react@^19.2.0/",
|
||||
"@google/genai": "https://aistudiocdn.com/@google/genai@^1.28.0",
|
||||
"chart.js": "https://aistudiocdn.com/chart.js@^4.5.1",
|
||||
"react-chartjs-2": "https://aistudiocdn.com/react-chartjs-2@^5.3.1"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
<body class="bg-neutral font-inter text-dark">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,16 @@
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
throw new Error("Could not find root element to mount to");
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Natural Language Database Query System",
|
||||
"description": "A web application that allows users to query databases using natural language. It provides a chat-based interface, displays results in tables and charts, and manages conversation history.",
|
||||
"requestFramePermissions": []
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "natural-language-database-query-system",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.28.0",
|
||||
"chart.js": "^4.5.1",
|
||||
"openai": "^6.8.1",
|
||||
"react": "^19.2.0",
|
||||
"react-chartjs-2": "^5.3.1",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["*.d.ts", "**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
@ -0,0 +1,193 @@
|
||||
// Basic types
|
||||
export type UserRole = 'sys-admin' | 'data-admin' | 'normal-user';
|
||||
export type MessageRole = 'user' | 'ai';
|
||||
|
||||
// Page navigation types
|
||||
export type Page = 'query' | 'history' | 'notifications' | 'account' | 'friends' | 'comparison';
|
||||
export type SysAdminPageType = 'dashboard' | 'user-management' | 'notification-management' | 'system-log' | 'llm-config' | 'account';
|
||||
export type DataAdminPageType = 'dashboard' | 'query' | 'history' | 'datasource' | 'user-permission' | 'notification-management' | 'connection-log' | 'notifications' | 'account' | 'friends' | 'comparison';
|
||||
|
||||
// Data structure types
|
||||
export interface ModelOption {
|
||||
name: string;
|
||||
disabled: boolean;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ChartData {
|
||||
type: 'bar' | 'line' | 'pie';
|
||||
labels: string[];
|
||||
datasets: {
|
||||
label: string;
|
||||
data: number[];
|
||||
backgroundColor: string | string[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface TableData {
|
||||
headers: string[];
|
||||
rows: string[][];
|
||||
}
|
||||
|
||||
export interface QueryResultData {
|
||||
id: string;
|
||||
userPrompt: string;
|
||||
sqlQuery: string;
|
||||
conversationId: string;
|
||||
queryTime: string;
|
||||
executionTime: string;
|
||||
tableData: TableData;
|
||||
chartData: ChartData;
|
||||
database: string;
|
||||
model:string;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
role: MessageRole;
|
||||
content: string | QueryResultData;
|
||||
}
|
||||
|
||||
export interface Conversation {
|
||||
id: string;
|
||||
title: string;
|
||||
messages: Message[];
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
type: 'system' | 'share';
|
||||
title: string;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
isRead: boolean;
|
||||
isPinned: boolean;
|
||||
fromUser?: { name: string, avatarUrl: string };
|
||||
relatedShareId?: string;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
email: string;
|
||||
phoneNumber: string;
|
||||
avatarUrl: string;
|
||||
registrationDate: string;
|
||||
accountStatus: 'normal' | 'disabled';
|
||||
preferences: {
|
||||
defaultModel: string;
|
||||
defaultDatabase: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Friend {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
isOnline: boolean;
|
||||
email: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface FriendRequest {
|
||||
id: string;
|
||||
fromUser: { name: string; avatarUrl: string };
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface QueryShare {
|
||||
id: string;
|
||||
sender: Friend;
|
||||
recipientId: string; // The ID of the user receiving the share
|
||||
querySnapshot: QueryResultData;
|
||||
timestamp: string;
|
||||
status: 'unread' | 'read';
|
||||
}
|
||||
|
||||
|
||||
// Admin Panel Types
|
||||
export interface AdminNotification {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
role: 'all' | UserRole;
|
||||
priority: 'urgent' | 'important' | 'normal';
|
||||
pinned: boolean;
|
||||
publisher: string;
|
||||
publishTime: string;
|
||||
status: 'published' | 'draft';
|
||||
dataSourceTopic?: string;
|
||||
}
|
||||
|
||||
export interface AdminUser {
|
||||
id: number;
|
||||
username: string;
|
||||
role: UserRole;
|
||||
email: string;
|
||||
regTime: string;
|
||||
status: 'active' | 'disabled';
|
||||
}
|
||||
|
||||
export interface SystemLog {
|
||||
id: string;
|
||||
time: string;
|
||||
user: string;
|
||||
action: string;
|
||||
model: string;
|
||||
ip: string;
|
||||
status: 'success' | 'failure';
|
||||
details?: string;
|
||||
}
|
||||
|
||||
export interface LLMConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
apiKey: string;
|
||||
endpoint: string;
|
||||
status: 'available' | 'unstable' | 'unavailable' | 'testing' | 'disabled';
|
||||
}
|
||||
|
||||
// Data Admin Types
|
||||
export interface DataSource {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'MySQL' | 'PostgreSQL' | 'Oracle' | 'SQL Server';
|
||||
address: string;
|
||||
status: 'connected' | 'disconnected' | 'error' | 'testing' | 'disabled';
|
||||
}
|
||||
|
||||
export interface DataSourcePermission {
|
||||
dataSourceId: string;
|
||||
dataSourceName: string;
|
||||
tables: string[];
|
||||
}
|
||||
|
||||
export interface UserPermissionAssignment {
|
||||
id: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
permissions: DataSourcePermission[];
|
||||
}
|
||||
|
||||
export interface UnassignedUser {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
regTime: string;
|
||||
}
|
||||
|
||||
export interface ConnectionLog {
|
||||
id: string;
|
||||
time: string;
|
||||
datasource: string;
|
||||
status: '成功' | '失败';
|
||||
details?: string;
|
||||
}
|
||||
|
||||
export interface PermissionLog {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
text: string;
|
||||
}
|
||||
@ -0,0 +1,295 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
scriptDir="$(dirname "$0")"
|
||||
scriptName="$(basename "$0")"
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
actualDistributionDir=""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$distributionUrlNameMain"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
# enable globbing to iterate over items
|
||||
set +f
|
||||
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$(basename "$dir")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
set -f
|
||||
fi
|
||||
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||
die "Could not find Maven distribution directory in extracted archive"
|
||||
fi
|
||||
|
||||
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
@ -0,0 +1,27 @@
|
||||
@echo off
|
||||
REM Windows 版本的数据导出脚本
|
||||
echo 开始导出数据...
|
||||
|
||||
REM 创建导出目录
|
||||
if not exist "data-backup" mkdir data-backup
|
||||
|
||||
REM 导出 MySQL 数据
|
||||
echo 导出 MySQL 数据...
|
||||
docker exec nlq_mysql mysqldump -uroot -proot123456 --no-create-info --skip-triggers --complete-insert natural_language_query_system > data-backup\mysql-data.sql
|
||||
|
||||
REM 导出 MongoDB 数据
|
||||
echo 导出 MongoDB 数据...
|
||||
docker exec nlq_mongodb mongodump --username=admin --password=admin123456 --authenticationDatabase=admin --db=natural_language_query_system --out=/tmp/mongodb-backup
|
||||
docker cp nlq_mongodb:/tmp/mongodb-backup/natural_language_query_system data-backup\mongodb-data
|
||||
|
||||
echo.
|
||||
echo 数据导出完成!
|
||||
echo 文件位置:
|
||||
echo - MySQL: data-backup\mysql-data.sql
|
||||
echo - MongoDB: data-backup\mongodb-data\
|
||||
echo.
|
||||
echo 请将 data-backup 文件夹分享给团队成员
|
||||
pause
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
# 导出 Docker 容器中的数据到本地文件
|
||||
# 用于团队成员之间共享测试数据
|
||||
|
||||
echo "开始导出数据..."
|
||||
|
||||
# 创建导出目录
|
||||
mkdir -p ./data-backup
|
||||
|
||||
# 导出 MySQL 数据(仅数据,不含表结构)
|
||||
echo "导出 MySQL 数据..."
|
||||
docker exec nlq_mysql mysqldump \
|
||||
-uroot -proot123456 \
|
||||
--no-create-info \
|
||||
--skip-triggers \
|
||||
--complete-insert \
|
||||
natural_language_query_system \
|
||||
> ./data-backup/mysql-data.sql
|
||||
|
||||
# 导出 MongoDB 数据
|
||||
echo "导出 MongoDB 数据..."
|
||||
docker exec nlq_mongodb mongodump \
|
||||
--username=admin \
|
||||
--password=admin123456 \
|
||||
--authenticationDatabase=admin \
|
||||
--db=natural_language_query_system \
|
||||
--out=/tmp/mongodb-backup
|
||||
|
||||
docker cp nlq_mongodb:/tmp/mongodb-backup/natural_language_query_system ./data-backup/mongodb-data
|
||||
|
||||
echo "数据导出完成!"
|
||||
echo "文件位置:"
|
||||
echo " - MySQL: ./data-backup/mysql-data.sql"
|
||||
echo " - MongoDB: ./data-backup/mongodb-data/"
|
||||
echo ""
|
||||
echo "请将 data-backup 文件夹分享给团队成员"
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
@echo off
|
||||
REM Windows 版本的数据导入脚本
|
||||
echo 开始导入数据...
|
||||
|
||||
REM 检查备份文件
|
||||
if not exist "data-backup\mysql-data.sql" (
|
||||
echo 错误:找不到 MySQL 备份文件 data-backup\mysql-data.sql
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if not exist "data-backup\mongodb-data" (
|
||||
echo 错误:找不到 MongoDB 备份目录 data-backup\mongodb-data
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 导入 MySQL 数据
|
||||
echo 导入 MySQL 数据...
|
||||
docker exec -i nlq_mysql mysql -uroot -proot123456 natural_language_query_system < data-backup\mysql-data.sql
|
||||
|
||||
REM 导入 MongoDB 数据
|
||||
echo 导入 MongoDB 数据...
|
||||
docker cp data-backup\mongodb-data nlq_mongodb:/tmp/mongodb-restore
|
||||
docker exec nlq_mongodb mongorestore --username=admin --password=admin123456 --authenticationDatabase=admin --db=natural_language_query_system /tmp/mongodb-restore
|
||||
|
||||
echo.
|
||||
echo 数据导入完成!
|
||||
pause
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
# 导入团队共享的测试数据到 Docker 容器
|
||||
# 使用前确保已经运行 docker compose up -d
|
||||
|
||||
echo "开始导入数据..."
|
||||
|
||||
# 检查备份文件是否存在
|
||||
if [ ! -f "./data-backup/mysql-data.sql" ]; then
|
||||
echo "错误:找不到 MySQL 备份文件 ./data-backup/mysql-data.sql"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "./data-backup/mongodb-data" ]; then
|
||||
echo "错误:找不到 MongoDB 备份目录 ./data-backup/mongodb-data"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 导入 MySQL 数据
|
||||
echo "导入 MySQL 数据..."
|
||||
docker exec -i nlq_mysql mysql \
|
||||
-uroot -proot123456 \
|
||||
natural_language_query_system \
|
||||
< ./data-backup/mysql-data.sql
|
||||
|
||||
# 导入 MongoDB 数据
|
||||
echo "导入 MongoDB 数据..."
|
||||
docker cp ./data-backup/mongodb-data nlq_mongodb:/tmp/mongodb-restore
|
||||
docker exec nlq_mongodb mongorestore \
|
||||
--username=admin \
|
||||
--password=admin123456 \
|
||||
--authenticationDatabase=admin \
|
||||
--db=natural_language_query_system \
|
||||
/tmp/mongodb-restore
|
||||
|
||||
echo "数据导入完成!"
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
package com.example.springboot_demo;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SpringbootDemoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SpringbootDemoApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package com.example.springboot_demo.common;
|
||||
|
||||
public class Result<T> {
|
||||
private Integer code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
public Integer getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(Integer code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public T getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(T data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static <T> Result<T> success() {
|
||||
return success(null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> success(T data) {
|
||||
Result<T> result = new Result<>();
|
||||
result.setCode(200);
|
||||
result.setMessage("success");
|
||||
result.setData(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(String message) {
|
||||
Result<T> result = new Result<>();
|
||||
result.setCode(500);
|
||||
result.setMessage(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(Integer code, String message) {
|
||||
Result<T> result = new Result<>();
|
||||
result.setCode(code);
|
||||
result.setMessage(message);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
package com.example.springboot_demo.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
|
||||
@Configuration
|
||||
public class CorsConfig {
|
||||
|
||||
@Bean
|
||||
public CorsFilter corsFilter() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.addAllowedOriginPattern("*");
|
||||
config.setAllowCredentials(true);
|
||||
config.addAllowedMethod("*");
|
||||
config.addAllowedHeader("*");
|
||||
config.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
return new CorsFilter(source);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mongodb.AiInteractionLog;
|
||||
import com.example.springboot_demo.service.AiInteractionLogService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/ai-interaction-log")
|
||||
public class AiInteractionLogController {
|
||||
|
||||
@Autowired
|
||||
private AiInteractionLogService aiInteractionLogService;
|
||||
|
||||
@GetMapping("/list/user/{userId}")
|
||||
public Result<List<AiInteractionLog>> listByUserId(@PathVariable Long userId) {
|
||||
return Result.success(aiInteractionLogService.listByUserId(userId));
|
||||
}
|
||||
|
||||
@GetMapping("/list/llm/{llmName}")
|
||||
public Result<List<AiInteractionLog>> listByLlmName(@PathVariable String llmName) {
|
||||
return Result.success(aiInteractionLogService.listByLlmName(llmName));
|
||||
}
|
||||
|
||||
@GetMapping("/list/llm/{llmName}/{status}")
|
||||
public Result<List<AiInteractionLog>> listByLlmNameAndStatus(@PathVariable String llmName, @PathVariable String status) {
|
||||
return Result.success(aiInteractionLogService.listByLlmNameAndStatus(llmName, status));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<AiInteractionLog> save(@RequestBody AiInteractionLog aiInteractionLog) {
|
||||
aiInteractionLog.setCreateTime(LocalDateTime.now());
|
||||
AiInteractionLog saved = aiInteractionLogService.save(aiInteractionLog);
|
||||
return Result.success(saved);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.dto.LoginDTO;
|
||||
import com.example.springboot_demo.service.AuthService;
|
||||
import com.example.springboot_demo.vo.LoginVO;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
public class AuthController {
|
||||
|
||||
@Autowired
|
||||
private AuthService authService;
|
||||
|
||||
@PostMapping("/login")
|
||||
public Result<LoginVO> login(@RequestBody LoginDTO loginDTO) {
|
||||
try {
|
||||
LoginVO loginVO = authService.login(loginDTO);
|
||||
return Result.success(loginVO);
|
||||
} catch (Exception e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mongodb.CollectionRecord;
|
||||
import com.example.springboot_demo.service.CollectionRecordService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/collection-record")
|
||||
public class CollectionRecordController {
|
||||
|
||||
@Autowired
|
||||
private CollectionRecordService collectionRecordService;
|
||||
|
||||
@GetMapping("/list/query/{queryId}")
|
||||
public Result<List<CollectionRecord>> listByQueryId(@PathVariable String queryId) {
|
||||
return Result.success(collectionRecordService.listByQueryId(queryId));
|
||||
}
|
||||
|
||||
@GetMapping("/list/user/{userId}")
|
||||
public Result<List<CollectionRecord>> listByUserId(@PathVariable Long userId) {
|
||||
return Result.success(collectionRecordService.listByUserId(userId));
|
||||
}
|
||||
|
||||
@GetMapping("/list/db/{dbConnectionId}")
|
||||
public Result<List<CollectionRecord>> listByDbConnectionId(@PathVariable Long dbConnectionId) {
|
||||
return Result.success(collectionRecordService.listByDbConnectionId(dbConnectionId));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Result<CollectionRecord> getById(@PathVariable String id) {
|
||||
return Result.success(collectionRecordService.getById(id));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<CollectionRecord> save(@RequestBody CollectionRecord collectionRecord) {
|
||||
collectionRecord.setCreateTime(LocalDateTime.now());
|
||||
CollectionRecord saved = collectionRecordService.save(collectionRecord);
|
||||
return Result.success(saved);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable String id) {
|
||||
collectionRecordService.deleteById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mysql.ColumnMetadata;
|
||||
import com.example.springboot_demo.service.ColumnMetadataService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/column-metadata")
|
||||
public class ColumnMetadataController {
|
||||
|
||||
@Autowired
|
||||
private ColumnMetadataService columnMetadataService;
|
||||
|
||||
/**
|
||||
* 查询所有字段元数据
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public Result<List<ColumnMetadata>> list() {
|
||||
return Result.success(columnMetadataService.list());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据表ID查询字段元数据
|
||||
*/
|
||||
@GetMapping("/list/{tableId}")
|
||||
public Result<List<ColumnMetadata>> listByTable(@PathVariable Long tableId) {
|
||||
return Result.success(columnMetadataService.listByTableId(tableId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询字段元数据
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public Result<ColumnMetadata> getById(@PathVariable Long id) {
|
||||
return Result.success(columnMetadataService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加字段元数据
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<ColumnMetadata> save(@RequestBody ColumnMetadata columnMetadata) {
|
||||
columnMetadata.setCreateTime(LocalDateTime.now());
|
||||
if (columnMetadata.getIsPrimary() == null) {
|
||||
columnMetadata.setIsPrimary(0);
|
||||
}
|
||||
columnMetadataService.save(columnMetadata);
|
||||
return Result.success(columnMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字段元数据
|
||||
*/
|
||||
@PutMapping
|
||||
public Result<ColumnMetadata> update(@RequestBody ColumnMetadata columnMetadata) {
|
||||
columnMetadataService.updateById(columnMetadata);
|
||||
return Result.success(columnMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除字段元数据
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
columnMetadataService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mysql.DbConnectionLog;
|
||||
import com.example.springboot_demo.service.DbConnectionLogService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/db-connection-log")
|
||||
public class DbConnectionLogController {
|
||||
|
||||
@Autowired
|
||||
private DbConnectionLogService dbConnectionLogService;
|
||||
|
||||
@GetMapping("/list")
|
||||
public Result<List<DbConnectionLog>> list() {
|
||||
return Result.success(dbConnectionLogService.list());
|
||||
}
|
||||
|
||||
@GetMapping("/list/connection/{dbConnectionId}")
|
||||
public Result<List<DbConnectionLog>> listByDbConnectionId(@PathVariable Long dbConnectionId) {
|
||||
return Result.success(dbConnectionLogService.listByDbConnectionId(dbConnectionId));
|
||||
}
|
||||
|
||||
@GetMapping("/list/status/{status}")
|
||||
public Result<List<DbConnectionLog>> listByStatus(@PathVariable String status) {
|
||||
return Result.success(dbConnectionLogService.listByStatus(status));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Result<DbConnectionLog> getById(@PathVariable Long id) {
|
||||
return Result.success(dbConnectionLogService.getById(id));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<DbConnectionLog> save(@RequestBody DbConnectionLog dbConnectionLog) {
|
||||
if (dbConnectionLog.getConnectTime() == null) {
|
||||
dbConnectionLog.setConnectTime(LocalDateTime.now());
|
||||
}
|
||||
dbConnectionLogService.save(dbConnectionLog);
|
||||
return Result.success(dbConnectionLog);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mysql.DbType;
|
||||
import com.example.springboot_demo.service.DbTypeService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/db-type")
|
||||
public class DbTypeController {
|
||||
|
||||
@Autowired
|
||||
private DbTypeService dbTypeService;
|
||||
|
||||
/**
|
||||
* 查询所有数据库类型
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public Result<List<DbType>> list() {
|
||||
return Result.success(dbTypeService.list());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public Result<DbType> getById(@PathVariable Integer id) {
|
||||
return Result.success(dbTypeService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类型编码查询
|
||||
*/
|
||||
@GetMapping("/code/{typeCode}")
|
||||
public Result<DbType> getByTypeCode(@PathVariable String typeCode) {
|
||||
return Result.success(dbTypeService.getByTypeCode(typeCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加数据库类型
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<DbType> save(@RequestBody DbType dbType) {
|
||||
dbTypeService.save(dbType);
|
||||
return Result.success(dbType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新数据库类型
|
||||
*/
|
||||
@PutMapping
|
||||
public Result<DbType> update(@RequestBody DbType dbType) {
|
||||
dbTypeService.updateById(dbType);
|
||||
return Result.success(dbType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据库类型
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Integer id) {
|
||||
dbTypeService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mongodb.DialogRecord;
|
||||
import com.example.springboot_demo.service.DialogService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/dialog")
|
||||
public class DialogController {
|
||||
|
||||
@Autowired
|
||||
private DialogService dialogService;
|
||||
|
||||
@GetMapping("/list")
|
||||
public Result<List<DialogRecord>> getUserDialogs(@RequestHeader(value = "userId", defaultValue = "1") Long userId) {
|
||||
List<DialogRecord> dialogs = dialogService.getUserDialogs(userId);
|
||||
return Result.success(dialogs);
|
||||
}
|
||||
|
||||
@GetMapping("/{dialogId}")
|
||||
public Result<DialogRecord> getDialogById(@PathVariable String dialogId) {
|
||||
DialogRecord dialog = dialogService.getDialogById(dialogId);
|
||||
if (dialog != null) {
|
||||
return Result.success(dialog);
|
||||
}
|
||||
return Result.error("对话不存在");
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<DialogRecord> createDialog(@RequestBody DialogRecord dialogRecord,
|
||||
@RequestHeader(value = "userId", defaultValue = "1") Long userId) {
|
||||
dialogRecord.setUserId(userId);
|
||||
DialogRecord created = dialogService.createDialog(dialogRecord);
|
||||
return Result.success(created);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{dialogId}")
|
||||
public Result<Void> deleteDialog(@PathVariable String dialogId,
|
||||
@RequestHeader(value = "userId", defaultValue = "1") Long userId) {
|
||||
dialogService.deleteDialog(dialogId, userId);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PutMapping("/{dialogId}")
|
||||
public Result<DialogRecord> updateDialog(@PathVariable String dialogId,
|
||||
@RequestBody DialogRecord dialogRecord,
|
||||
@RequestHeader(value = "userId", defaultValue = "1") Long userId) {
|
||||
dialogRecord.setDialogId(dialogId);
|
||||
dialogRecord.setUserId(userId);
|
||||
DialogRecord updated = dialogService.updateDialog(dialogRecord);
|
||||
return Result.success(updated);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mongodb.DialogDetail;
|
||||
import com.example.springboot_demo.service.DialogDetailService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/dialog-detail")
|
||||
public class DialogDetailController {
|
||||
|
||||
@Autowired
|
||||
private DialogDetailService dialogDetailService;
|
||||
|
||||
@GetMapping("/{dialogId}")
|
||||
public Result<DialogDetail> getByDialogId(@PathVariable String dialogId) {
|
||||
return Result.success(dialogDetailService.getByDialogId(dialogId));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<DialogDetail> save(@RequestBody DialogDetail dialogDetail) {
|
||||
DialogDetail saved = dialogDetailService.save(dialogDetail);
|
||||
return Result.success(saved);
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
public Result<DialogDetail> update(@RequestBody DialogDetail dialogDetail) {
|
||||
DialogDetail saved = dialogDetailService.save(dialogDetail);
|
||||
return Result.success(saved);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable String id) {
|
||||
dialogDetailService.deleteById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,86 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mysql.ErrorLog;
|
||||
import com.example.springboot_demo.service.ErrorLogService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/error-log")
|
||||
public class ErrorLogController {
|
||||
|
||||
@Autowired
|
||||
private ErrorLogService errorLogService;
|
||||
|
||||
/**
|
||||
* 查询所有错误日志
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public Result<List<ErrorLog>> list() {
|
||||
return Result.success(errorLogService.list());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据错误类型查询
|
||||
*/
|
||||
@GetMapping("/list/type/{errorTypeId}")
|
||||
public Result<List<ErrorLog>> listByErrorType(@PathVariable Integer errorTypeId) {
|
||||
return Result.success(errorLogService.listByErrorTypeId(errorTypeId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据统计周期查询
|
||||
*/
|
||||
@GetMapping("/list/period/{period}")
|
||||
public Result<List<ErrorLog>> listByPeriod(@PathVariable String period) {
|
||||
return Result.success(errorLogService.listByPeriod(period));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public Result<ErrorLog> getById(@PathVariable Long id) {
|
||||
return Result.success(errorLogService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加错误日志
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<ErrorLog> save(@RequestBody ErrorLog errorLog) {
|
||||
if (errorLog.getStatTime() == null) {
|
||||
errorLog.setStatTime(LocalDateTime.now());
|
||||
}
|
||||
errorLogService.save(errorLog);
|
||||
return Result.success(errorLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新错误日志
|
||||
*/
|
||||
@PutMapping
|
||||
public Result<ErrorLog> update(@RequestBody ErrorLog errorLog) {
|
||||
errorLogService.updateById(errorLog);
|
||||
return Result.success(errorLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除错误日志
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
errorLogService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mysql.ErrorType;
|
||||
import com.example.springboot_demo.service.ErrorTypeService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/error-type")
|
||||
public class ErrorTypeController {
|
||||
|
||||
@Autowired
|
||||
private ErrorTypeService errorTypeService;
|
||||
|
||||
/**
|
||||
* 查询所有错误类型
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public Result<List<ErrorType>> list() {
|
||||
return Result.success(errorTypeService.list());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public Result<ErrorType> getById(@PathVariable Integer id) {
|
||||
return Result.success(errorTypeService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据错误编码查询
|
||||
*/
|
||||
@GetMapping("/code/{errorCode}")
|
||||
public Result<ErrorType> getByErrorCode(@PathVariable String errorCode) {
|
||||
return Result.success(errorTypeService.getByErrorCode(errorCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加错误类型
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<ErrorType> save(@RequestBody ErrorType errorType) {
|
||||
errorTypeService.save(errorType);
|
||||
return Result.success(errorType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新错误类型
|
||||
*/
|
||||
@PutMapping
|
||||
public Result<ErrorType> update(@RequestBody ErrorType errorType) {
|
||||
errorTypeService.updateById(errorType);
|
||||
return Result.success(errorType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除错误类型
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Integer id) {
|
||||
errorTypeService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mongodb.FriendChat;
|
||||
import com.example.springboot_demo.service.FriendChatService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/friend-chat")
|
||||
public class FriendChatController {
|
||||
|
||||
@Autowired
|
||||
private FriendChatService friendChatService;
|
||||
|
||||
@GetMapping("/list/{userId}/{friendId}")
|
||||
public Result<List<FriendChat>> listByUserIdAndFriendId(@PathVariable Long userId, @PathVariable Long friendId) {
|
||||
return Result.success(friendChatService.listByUserIdAndFriendId(userId, friendId));
|
||||
}
|
||||
|
||||
@GetMapping("/unread/{friendId}")
|
||||
public Result<List<FriendChat>> listUnreadByFriendId(@PathVariable Long friendId) {
|
||||
return Result.success(friendChatService.listUnreadByFriendId(friendId));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<FriendChat> save(@RequestBody FriendChat friendChat) {
|
||||
friendChat.setSendTime(LocalDateTime.now());
|
||||
if (friendChat.getIsRead() == null) {
|
||||
friendChat.setIsRead(false);
|
||||
}
|
||||
FriendChat saved = friendChatService.save(friendChat);
|
||||
return Result.success(saved);
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
public Result<FriendChat> update(@RequestBody FriendChat friendChat) {
|
||||
FriendChat saved = friendChatService.save(friendChat);
|
||||
return Result.success(saved);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable String id) {
|
||||
friendChatService.deleteById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mysql.FriendRelation;
|
||||
import com.example.springboot_demo.service.FriendRelationService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/friend-relation")
|
||||
public class FriendRelationController {
|
||||
|
||||
@Autowired
|
||||
private FriendRelationService friendRelationService;
|
||||
|
||||
@GetMapping("/list/{userId}")
|
||||
public Result<List<FriendRelation>> listByUserId(@PathVariable Long userId) {
|
||||
return Result.success(friendRelationService.listByUserId(userId));
|
||||
}
|
||||
|
||||
@GetMapping("/{userId}/{friendId}")
|
||||
public Result<FriendRelation> getByUserIdAndFriendId(@PathVariable Long userId, @PathVariable Long friendId) {
|
||||
return Result.success(friendRelationService.getByUserIdAndFriendId(userId, friendId));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<FriendRelation> save(@RequestBody FriendRelation friendRelation) {
|
||||
friendRelation.setCreateTime(LocalDateTime.now());
|
||||
if (friendRelation.getOnlineStatus() == null) {
|
||||
friendRelation.setOnlineStatus(0);
|
||||
}
|
||||
friendRelationService.save(friendRelation);
|
||||
return Result.success(friendRelation);
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
public Result<FriendRelation> update(@RequestBody FriendRelation friendRelation) {
|
||||
friendRelationService.updateById(friendRelation);
|
||||
return Result.success(friendRelation);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
friendRelationService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{userId}/{friendId}")
|
||||
public Result<Void> deleteByUserIdAndFriendId(@PathVariable Long userId, @PathVariable Long friendId) {
|
||||
friendRelationService.removeByUserIdAndFriendId(userId, friendId);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,86 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mysql.FriendRequest;
|
||||
import com.example.springboot_demo.service.FriendRequestService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/friend-request")
|
||||
public class FriendRequestController {
|
||||
|
||||
@Autowired
|
||||
private FriendRequestService friendRequestService;
|
||||
|
||||
@GetMapping("/list/{recipientId}")
|
||||
public Result<List<FriendRequest>> listByRecipientId(@PathVariable Long recipientId) {
|
||||
return Result.success(friendRequestService.listByRecipientId(recipientId));
|
||||
}
|
||||
|
||||
@GetMapping("/list/{recipientId}/{status}")
|
||||
public Result<List<FriendRequest>> listByRecipientIdAndStatus(@PathVariable Long recipientId, @PathVariable Integer status) {
|
||||
return Result.success(friendRequestService.listByRecipientIdAndStatus(recipientId, status));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Result<FriendRequest> getById(@PathVariable Long id) {
|
||||
return Result.success(friendRequestService.getById(id));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<FriendRequest> save(@RequestBody FriendRequest friendRequest) {
|
||||
friendRequest.setCreateTime(LocalDateTime.now());
|
||||
if (friendRequest.getStatus() == null) {
|
||||
friendRequest.setStatus(0);
|
||||
}
|
||||
friendRequestService.save(friendRequest);
|
||||
return Result.success(friendRequest);
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
public Result<FriendRequest> update(@RequestBody FriendRequest friendRequest) {
|
||||
if (friendRequest.getStatus() != null && friendRequest.getStatus() != 0) {
|
||||
friendRequest.setHandleTime(LocalDateTime.now());
|
||||
}
|
||||
friendRequestService.updateById(friendRequest);
|
||||
return Result.success(friendRequest);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
friendRequestService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/accept")
|
||||
public Result<String> acceptRequest(@PathVariable Long id) {
|
||||
try {
|
||||
boolean success = friendRequestService.acceptRequest(id);
|
||||
if (success) {
|
||||
return Result.success("已接受好友请求");
|
||||
}
|
||||
return Result.error("接受好友请求失败");
|
||||
} catch (RuntimeException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/reject")
|
||||
public Result<String> rejectRequest(@PathVariable Long id) {
|
||||
try {
|
||||
boolean success = friendRequestService.rejectRequest(id);
|
||||
if (success) {
|
||||
return Result.success("已拒绝好友请求");
|
||||
}
|
||||
return Result.error("拒绝好友请求失败");
|
||||
} catch (RuntimeException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mysql.LlmStatus;
|
||||
import com.example.springboot_demo.service.LlmStatusService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/llm-status")
|
||||
public class LlmStatusController {
|
||||
|
||||
@Autowired
|
||||
private LlmStatusService llmStatusService;
|
||||
|
||||
/**
|
||||
* 查询所有大模型状态
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public Result<List<LlmStatus>> list() {
|
||||
return Result.success(llmStatusService.list());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public Result<LlmStatus> getById(@PathVariable Integer id) {
|
||||
return Result.success(llmStatusService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据状态编码查询
|
||||
*/
|
||||
@GetMapping("/code/{statusCode}")
|
||||
public Result<LlmStatus> getByStatusCode(@PathVariable String statusCode) {
|
||||
return Result.success(llmStatusService.getByStatusCode(statusCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加大模型状态
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<LlmStatus> save(@RequestBody LlmStatus llmStatus) {
|
||||
llmStatusService.save(llmStatus);
|
||||
return Result.success(llmStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新大模型状态
|
||||
*/
|
||||
@PutMapping
|
||||
public Result<LlmStatus> update(@RequestBody LlmStatus llmStatus) {
|
||||
llmStatusService.updateById(llmStatus);
|
||||
return Result.success(llmStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除大模型状态
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Integer id) {
|
||||
llmStatusService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mysql.NotificationTarget;
|
||||
import com.example.springboot_demo.service.NotificationTargetService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/notification-target")
|
||||
public class NotificationTargetController {
|
||||
|
||||
@Autowired
|
||||
private NotificationTargetService notificationTargetService;
|
||||
|
||||
/**
|
||||
* 查询所有通知目标
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public Result<List<NotificationTarget>> list() {
|
||||
return Result.success(notificationTargetService.list());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public Result<NotificationTarget> getById(@PathVariable Integer id) {
|
||||
return Result.success(notificationTargetService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据目标编码查询
|
||||
*/
|
||||
@GetMapping("/code/{targetCode}")
|
||||
public Result<NotificationTarget> getByTargetCode(@PathVariable String targetCode) {
|
||||
return Result.success(notificationTargetService.getByTargetCode(targetCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加通知目标
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<NotificationTarget> save(@RequestBody NotificationTarget notificationTarget) {
|
||||
notificationTargetService.save(notificationTarget);
|
||||
return Result.success(notificationTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新通知目标
|
||||
*/
|
||||
@PutMapping
|
||||
public Result<NotificationTarget> update(@RequestBody NotificationTarget notificationTarget) {
|
||||
notificationTargetService.updateById(notificationTarget);
|
||||
return Result.success(notificationTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除通知目标
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Integer id) {
|
||||
notificationTargetService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,82 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mysql.OperationLog;
|
||||
import com.example.springboot_demo.service.OperationLogService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/operation-log")
|
||||
public class OperationLogController {
|
||||
|
||||
@Autowired
|
||||
private OperationLogService operationLogService;
|
||||
|
||||
/**
|
||||
* 查询所有操作日志
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public Result<List<OperationLog>> list() {
|
||||
return Result.success(operationLogService.list());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID查询操作日志
|
||||
*/
|
||||
@GetMapping("/list/user/{userId}")
|
||||
public Result<List<OperationLog>> listByUser(@PathVariable Long userId) {
|
||||
return Result.success(operationLogService.listByUserId(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模块查询操作日志
|
||||
*/
|
||||
@GetMapping("/list/module/{module}")
|
||||
public Result<List<OperationLog>> listByModule(@PathVariable String module) {
|
||||
return Result.success(operationLogService.listByModule(module));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询失败的操作日志
|
||||
*/
|
||||
@GetMapping("/list/failed")
|
||||
public Result<List<OperationLog>> listFailed() {
|
||||
return Result.success(operationLogService.listFailed());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public Result<OperationLog> getById(@PathVariable Long id) {
|
||||
return Result.success(operationLogService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加操作日志
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<OperationLog> save(@RequestBody OperationLog operationLog) {
|
||||
if (operationLog.getOperateTime() == null) {
|
||||
operationLog.setOperateTime(LocalDateTime.now());
|
||||
}
|
||||
operationLogService.save(operationLog);
|
||||
return Result.success(operationLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除操作日志
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
operationLogService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mysql.PerformanceMetric;
|
||||
import com.example.springboot_demo.service.PerformanceMetricService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/performance-metric")
|
||||
public class PerformanceMetricController {
|
||||
|
||||
@Autowired
|
||||
private PerformanceMetricService performanceMetricService;
|
||||
|
||||
@GetMapping("/list")
|
||||
public Result<List<PerformanceMetric>> list() {
|
||||
return Result.success(performanceMetricService.list());
|
||||
}
|
||||
|
||||
@GetMapping("/list/{metricType}")
|
||||
public Result<List<PerformanceMetric>> listByMetricType(@PathVariable String metricType) {
|
||||
return Result.success(performanceMetricService.listByMetricType(metricType));
|
||||
}
|
||||
|
||||
@GetMapping("/range")
|
||||
public Result<List<PerformanceMetric>> listByTimeRange(@RequestParam String startTime, @RequestParam String endTime) {
|
||||
LocalDateTime start = LocalDateTime.parse(startTime);
|
||||
LocalDateTime end = LocalDateTime.parse(endTime);
|
||||
return Result.success(performanceMetricService.listByTimeRange(start, end));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<PerformanceMetric> save(@RequestBody PerformanceMetric performanceMetric) {
|
||||
performanceMetricService.save(performanceMetric);
|
||||
return Result.success(performanceMetric);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mongodb.QueryCollection;
|
||||
import com.example.springboot_demo.service.QueryCollectionService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/query-collection")
|
||||
public class QueryCollectionController {
|
||||
|
||||
@Autowired
|
||||
private QueryCollectionService queryCollectionService;
|
||||
|
||||
@GetMapping("/list/{userId}")
|
||||
public Result<List<QueryCollection>> listByUserId(@PathVariable Long userId) {
|
||||
return Result.success(queryCollectionService.listByUserId(userId));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Result<QueryCollection> getById(@PathVariable String id) {
|
||||
return Result.success(queryCollectionService.getById(id));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<QueryCollection> save(@RequestBody QueryCollection queryCollection) {
|
||||
queryCollection.setCreateTime(LocalDateTime.now());
|
||||
QueryCollection saved = queryCollectionService.save(queryCollection);
|
||||
return Result.success(saved);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable String id) {
|
||||
queryCollectionService.deleteById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.dto.QueryRequestDTO;
|
||||
import com.example.springboot_demo.service.QueryService;
|
||||
import com.example.springboot_demo.vo.QueryResponseVO;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/query")
|
||||
public class QueryController {
|
||||
|
||||
@Autowired
|
||||
private QueryService queryService;
|
||||
|
||||
@PostMapping("/execute")
|
||||
public Result<QueryResponseVO> executeQuery(@RequestBody QueryRequestDTO request,
|
||||
@RequestHeader(value = "userId", defaultValue = "1") Long userId) {
|
||||
try {
|
||||
QueryResponseVO response = queryService.executeQuery(request, userId);
|
||||
return Result.success(response);
|
||||
} catch (Exception e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
package com.example.springboot_demo.controller;
|
||||
|
||||
import com.example.springboot_demo.common.Result;
|
||||
import com.example.springboot_demo.entity.mysql.QueryLog;
|
||||
import com.example.springboot_demo.service.QueryLogService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/query-log")
|
||||
public class QueryLogController {
|
||||
|
||||
@Autowired
|
||||
private QueryLogService queryLogService;
|
||||
|
||||
/**
|
||||
* 查询所有查询日志
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public Result<List<QueryLog>> list() {
|
||||
return Result.success(queryLogService.list());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID查询查询日志
|
||||
*/
|
||||
@GetMapping("/list/user/{userId}")
|
||||
public Result<List<QueryLog>> listByUser(@PathVariable Long userId) {
|
||||
return Result.success(queryLogService.listByUserId(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据对话ID查询查询日志
|
||||
*/
|
||||
@GetMapping("/list/dialog/{dialogId}")
|
||||
public Result<List<QueryLog>> listByDialog(@PathVariable String dialogId) {
|
||||
return Result.success(queryLogService.listByDialogId(dialogId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询查询日志
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public Result<QueryLog> getById(@PathVariable Long id) {
|
||||
return Result.success(queryLogService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加查询日志
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<QueryLog> save(@RequestBody QueryLog queryLog) {
|
||||
if (queryLog.getQueryDate() == null) {
|
||||
queryLog.setQueryDate(LocalDate.now());
|
||||
}
|
||||
if (queryLog.getQueryTime() == null) {
|
||||
queryLog.setQueryTime(LocalDateTime.now());
|
||||
}
|
||||
queryLogService.save(queryLog);
|
||||
return Result.success(queryLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除查询日志
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
queryLogService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue