diff --git a/config/routes.ts b/config/routes.ts
index 4f7b9d31..7c108fc7 100644
--- a/config/routes.ts
+++ b/config/routes.ts
@@ -196,6 +196,16 @@
},
]
},
+ {
+ path: '/dataQuery',
+ component: '../layouts/HomeLayout',
+ routes: [
+ {
+ path: '/dataQuery',
+ component: './dataQuery',
+ },
+ ]
+ },
{
path: '/home',
component: '../layouts/HomeLayout',
diff --git a/images/image.png b/images/image.png
new file mode 100644
index 00000000..5bd6401f
Binary files /dev/null and b/images/image.png differ
diff --git a/src/layouts/HomeLayout/_defaultProps.tsx b/src/layouts/HomeLayout/_defaultProps.tsx
index ba7814bf..ea30203c 100644
--- a/src/layouts/HomeLayout/_defaultProps.tsx
+++ b/src/layouts/HomeLayout/_defaultProps.tsx
@@ -1,4 +1,4 @@
-import {DatabaseNetwork, EveryUser, HomeTwo, Sphere, Table, Timeline, User} from "@icon-park/react";
+import {DatabaseNetwork, EveryUser, HomeTwo, Sphere, Table, Timeline, User, Search} from "@icon-park/react";
export default {
route: {
@@ -14,6 +14,11 @@ export default {
name: '数据模型',
icon:
,
},
+ {
+ path: '/dataQuery',
+ name: '数据查询',
+ icon: ,
+ },
{
path: '/databaseConfig',
diff --git a/src/pages/dataQuery/component/ExplainResult.tsx b/src/pages/dataQuery/component/ExplainResult.tsx
new file mode 100644
index 00000000..658b9b71
--- /dev/null
+++ b/src/pages/dataQuery/component/ExplainResult.tsx
@@ -0,0 +1,41 @@
+import {ProTable} from "@ant-design/pro-components";
+import React from "react";
+
+export type ExplainResultProps = {
+ tableResult: { columns: any, dataSource: any, total: number };
+};
+
+
+const ExplainResult: React.FC = (props) => {
+
+
+ const getColumns = () => {
+ return props.tableResult.columns.map((k: any) => ({
+ title: k,
+ key: k,
+ dataIndex: k,
+ ellipsis: true,
+ width: 150,
+ render: (text: any) => text === null ? {""} : text
+ }))
+ }
+
+ return (<>
+
+ >);
+};
+
+export default React.memo(ExplainResult)
diff --git a/src/pages/dataQuery/component/QueryHistory.tsx b/src/pages/dataQuery/component/QueryHistory.tsx
new file mode 100644
index 00000000..90b5a19d
--- /dev/null
+++ b/src/pages/dataQuery/component/QueryHistory.tsx
@@ -0,0 +1,91 @@
+import {ProColumns, ProTable} from "@ant-design/pro-components";
+import React, {useEffect} from "react";
+import {GET} from "@/services/crud";
+import {useSearchParams} from "@@/exports";
+import * as cache from "@/utils/cache";
+import {CONSTANT} from "@/utils/constant";
+
+export type QueryHistoryProps = {
+ queryId: string | number;
+ key: string;
+};
+
+type QueryHistoryItem = {
+ id: number | string;
+ title: string;
+ sqlInfo: string;
+ dbName: string;
+ createTime: string;
+ creator: string;
+ duration: number;
+};
+
+
+const QueryHistory: React.FC = (props) => {
+ useEffect(() => {
+ }, [props.key])
+
+ const columns: ProColumns[] = [
+ {
+ title: 'SQL',
+ dataIndex: 'sqlInfo',
+ copyable: true,
+ ellipsis: true,
+ },
+ {
+ title: '执行数据库',
+ dataIndex: 'dbName',
+ },
+ {
+ title: '耗时',
+ dataIndex: 'duration',
+ },
+ {
+ title: '执行时间',
+ dataIndex: 'createTime',
+ },
+ {
+ title: '执行人',
+ dataIndex: 'creator',
+ },
+ ]
+
+
+ const [searchParams] = useSearchParams();
+ let projectId = searchParams.get("projectId") || '';
+ if (!projectId || projectId === '') {
+ projectId = cache.getItem(CONSTANT.PROJECT_ID) || '';
+ }
+
+
+ return (<>
+ {
+ const result = await GET('/ncnb/queryHistory', {
+ ...params,
+ size: params.pageSize,
+ queryId: props.queryId,
+ });
+ return {
+ data: result?.data?.records,
+ total: result?.data?.total,
+ success: result.code === 200
+ }
+ }
+ }
+ pagination={{
+ pageSize: 6,
+ }}
+ columns={columns}
+ search={false}
+ options={false}
+ dateFormatter="string"
+ />
+ >);
+};
+
+export default React.memo(QueryHistory)
diff --git a/src/pages/dataQuery/component/QueryResult.tsx b/src/pages/dataQuery/component/QueryResult.tsx
new file mode 100644
index 00000000..b3a6369e
--- /dev/null
+++ b/src/pages/dataQuery/component/QueryResult.tsx
@@ -0,0 +1,41 @@
+import {ProTable} from "@ant-design/pro-components";
+import React from "react";
+
+export type QueryResultProps = {
+ tableResult: { columns: any, dataSource: any, total: number };
+};
+
+
+const QueryResult: React.FC = (props) => {
+
+
+ const getColumns = () => {
+ return props.tableResult.columns.map((k: any) => ({
+ title: k,
+ key: k,
+ dataIndex: k,
+ ellipsis: true,
+ width: 150,
+ render: (text: any) => text === null ? {""} : text
+ }))
+ }
+
+ return (<>
+
+ >);
+};
+
+export default React.memo(QueryResult)
diff --git a/src/pages/dataQuery/index.tsx b/src/pages/dataQuery/index.tsx
new file mode 100644
index 00000000..56c71df2
--- /dev/null
+++ b/src/pages/dataQuery/index.tsx
@@ -0,0 +1,696 @@
+import React, {useEffect, useRef, useState} from "react";
+import {Button, message, Select, Space, Input, FloatButton, List, Badge, Typography, Spin, Layout, Menu, Tree, Empty, Modal, Form, Space as AntSpace, Dropdown, Checkbox, TreeSelect} from "antd";
+import {ProCard} from "@ant-design/pro-components";
+import {Data, HistoryQuery, Plan} from "@icon-park/react";
+import CodeEditor from "@/components/CodeEditor";
+import QueryResult from "@/pages/design/query/component/QueryResult";
+import {BarsOutlined, EyeOutlined, PlayCircleOutlined, SaveOutlined, PlusOutlined, EditOutlined, DeleteOutlined, FolderOutlined, FileOutlined, MoreOutlined} from "@ant-design/icons";
+import useQueryStore from "@/store/query/useQueryStore";
+import shallow from "zustand/shallow";
+import useVersionStore from "@/store/version/useVersionStore";
+import _ from "lodash";
+import {useSearchParams} from "@@/exports";
+import * as cache from "@/utils/cache";
+import {CONSTANT} from "@/utils/constant";
+import {format} from "sql-formatter";
+import useProjectStore from "@/store/project/useProjectStore";
+import ExplainResult from "@/pages/design/query/component/ExplainResult";
+import QueryHistory from "@/pages/design/query/component/QueryHistory";
+import {POST} from "@/services/crud";
+import {uuid} from "@/utils/uuid";
+import moment from "moment";
+import { DataSourceSelect } from '@/components/DataSourceSelect';
+import type { DataNode } from 'antd/es/tree';
+import { ModalForm, ProFormText } from '@ant-design/pro-components';
+import './style.less';
+
+const {Text} = Typography;
+const {Search} = Input;
+const { Sider, Content } = Layout;
+
+const {Option, OptGroup} = Select;
+export type QueryProps = {
+ id: string | number;
+};
+
+const DataQuery: React.FC = (props) => {
+ const {dbs, versionDispatch} = useVersionStore(state => ({
+ dbs: state.dbs,
+ versionDispatch: state.dispatch,
+ }), shallow);
+
+
+ const {tables, modules} = useProjectStore(state => ({
+ tables: state.tables,
+ modules: state.project?.projectJSON?.modules || [],
+
+ }), shallow);
+
+ console.log(130, tables);
+
+ const [searchParams] = useSearchParams();
+ let projectId = searchParams.get("projectId") || '';
+ if (!projectId || projectId === '') {
+ projectId = cache.getItem(CONSTANT.PROJECT_ID) || '';
+ }
+
+ const groupDb = _.groupBy(dbs, g => g.select);
+ const currentDB = versionDispatch.getCurrentDB();
+
+
+ const [tableResult, setTableResult] = useState({
+ columns: [],
+ dataSource: [],
+ total: 0
+ });
+ const [explainTable, setExplainTable] = useState({
+ columns: [],
+ dataSource: [],
+ total: 0
+ });
+ const [tab, setTab] = useState('result');
+ const [selectDB, setSelectDB] = useState(currentDB);
+ const [sqlMode, setSqlMode] = useState('mysql');
+ const [theme, setTheme] = useState('xcode');
+
+ const {queryDispatch, treeData} = useQueryStore(state => ({
+ queryDispatch: state.dispatch,
+ treeData: state.treeData,
+ }), shallow);
+
+ const [queryInfo, setQueryInfo] = useState({
+ sqlInfo: ''
+ });
+
+ const editorRef = useRef(null);
+
+
+ useEffect(() => {
+ queryDispatch.fetchQueryInfo(props.id).then(r => {
+ if (r.code === 200) {
+ setQueryInfo(r.data);
+ }
+ });
+ console.log(26, queryInfo);
+ }, [])
+
+ useEffect(() => {
+
+ }, [tableResult])
+
+ const EDITOR_THEME = ['xcode', 'terminal',];
+
+ const [selectedTable, setSelectedTable] = useState([]);
+ const [chatId, setChatId] = useState(uuid());
+ const [open, setOpen] = useState(false);
+ const [aiLoading, setAiLoading] = useState(false);
+
+ const [savedQueries, setSavedQueries] = useState([]);
+ const [selectedQuery, setSelectedQuery] = useState(null);
+ const [siderCollapsed, setSiderCollapsed] = useState(false);
+ const [isAddingQuery, setIsAddingQuery] = useState(false);
+ const [newQueryName, setNewQueryName] = useState('');
+ const [editingQuery, setEditingQuery] = useState(null);
+ const [isEditModalVisible, setIsEditModalVisible] = useState(false);
+ const [editQueryName, setEditQueryName] = useState('');
+ const [isModalVisible, setIsModalVisible] = useState(false);
+
+ const [form] = Form.useForm();
+ const [modalVisible, setModalVisible] = useState(false);
+ const [modalType, setModalType] = useState<'add' | 'edit'>('add');
+ const [selectedNode, setSelectedNode] = useState(null);
+
+ const renderTreeSelectNodes = (data: DataNode[]): DataNode[] =>
+ data.map((item) => ({
+ title: item.title,
+ key: item.key,
+ value: item.key,
+ disabled: item.isLeaf,
+ children: item.children ? renderTreeSelectNodes(item.children) : undefined,
+ }));
+
+ useEffect(() => {
+ queryDispatch.fetchTreeData({ projectId });
+ }, [projectId, queryDispatch]);
+
+
+
+ const showModal = (type: 'add' | 'edit', node?: DataNode) => {
+ setModalType(type);
+ setSelectedNode(node || null);
+ setModalVisible(true);
+ if (type === 'edit' && node) {
+ form.setFieldsValue({
+ name: node.title,
+ isLeaf: node.isLeaf
+ });
+ } else {
+ form.resetFields();
+ if (node) {
+ form.setFieldsValue({ parentId: node.key });
+ }
+ // 设置默认查询类型为 SQL
+ form.setFieldsValue({ type: 'sql', isLeaf: true });
+ }
+ };
+
+ const handleModalOk = async () => {
+ try {
+ const values = await form.validateFields();
+ if (modalType === 'add') {
+ await queryDispatch.addQuery({
+ title: values.name,
+ projectId,
+ parentId: values.parentId || '0',
+ type: values.type,
+ isLeaf: values.isLeaf,
+ });
+ } else {
+ await queryDispatch.renameQuery({
+ id: selectedNode!.key,
+ title: values.name,
+ projectId,
+ isLeaf: values.isLeaf,
+ });
+ }
+ setModalVisible(false);
+ queryDispatch.fetchTreeData({ projectId });
+ } catch (error) {
+ console.error("handleModalOk 中的错误:", error);
+ }
+ };
+
+ const handleDelete = async (node: DataNode) => {
+ Modal.confirm({
+ title: '确定要删除此项吗?',
+ content: '此操作无法撤销。',
+ onOk: async () => {
+ try {
+ await queryDispatch.removeQuery({
+ id: node.key,
+ projectId,
+ });
+ queryDispatch.fetchTreeData({ projectId });
+ } catch (error) {
+ console.error("handleDelete 中的错误:", error);
+ }
+ },
+ });
+ };
+
+ const renderTreeNodes = (data: DataNode[] | undefined): DataNode[] => {
+ if (!data || !Array.isArray(data)) {
+ return [];
+ }
+
+ return data.map((item) => ({
+ ...item,
+ title: (
+
+
+ {item.isLeaf ? : } {item.title}
+
+
+ {!item.isLeaf && (
+ showModal('add', item)}>
+ 添加子项
+
+ )}
+ showModal('edit', item)}>
+ 编辑
+
+ handleDelete(item)}>
+ 删除
+
+
+ }
+ trigger={['click']}
+ >
+ }
+ size="small"
+ onClick={(e) => e.stopPropagation()}
+ className="tree-node-action"
+ />
+
+
+ ),
+ children: item.children ? renderTreeNodes(item.children) : undefined,
+ }));
+ };
+
+ const handleOpen = () => {
+ setOpen(true);
+ };
+
+ const handleClose = () => {
+ setOpen(false);
+ };
+
+ const handleClear = () => {
+ setSelectedTable([]);
+ setOpen(false);
+ };
+
+ const actions =
+
+ 已选表
+
+ 数据源
+ setSelectDB(value?.value)}
+ style={{ width: 200 }}
+ size="small"
+ onDbChange={(db) => {
+ // You can handle additional logic here if needed when the database changes
+ }}
+ />
+ 模式
+
+ 主题
+
+
+
+
+ const [prefix, setPrefix] = useState('select'); // 初始选择第一个前缀
+
+
+ const aiSearch = (command: string) => {
+ const sqlInfo = (queryInfo?.sqlInfo || '') + '\n' + '-- ' + cache.getItem('username') + ':' + command + moment().format('YYYY-MM-DD HH:mm:ss');
+ setQueryInfo({
+ ...queryInfo,
+ sqlInfo: sqlInfo
+ });
+ setAiLoading(true);
+ POST('/ncnb/ai/sql', {
+ chatId,
+ command: prefix + ":" + command,
+ "tables": selectedTable,
+ "schema": "Mysql",
+ }
+ ).then((result) => {
+ console.log(151, result)
+ console.log(152, queryInfo?.sqlInfo)
+ if (result && result.code === 200) {
+ setQueryInfo({
+ ...queryInfo,
+ sqlInfo: sqlInfo + '\n' + result.data
+ });
+ } else {
+ if (result && result?.msg) {
+ message.error(result?.msg);
+ }
+ }
+ setAiLoading(false);
+ });
+
+ }
+
+ const onDrop = (e: any) => {
+ e.preventDefault();
+ const data = e.dataTransfer.getData('Text');
+ console.log(283, data)
+ if (data.startsWith('entity&')) {
+ let moduleName = data.split('&')[1];
+ let tableName = data.split('&')[2];
+ const tmpModule = _.filter(modules, {'name': moduleName});
+ console.log(283, tmpModule);
+ const table = _.filter(tmpModule[0]?.entities, {'title': tableName});
+ console.log(283, table);
+ const map = _.map(table[0]?.fields, 'name');
+ console.log(283, map);
+ const fields = map?.join(",");
+ console.log(283, fields);
+ const template = '{tableName}({fields})';
+ // @ts-ignore
+ const aiKey = template.render({
+ tableName,
+ fields
+ });
+ console.log(283, aiKey);
+ if (_.includes(selectedTable, aiKey)) {
+ message.warning(`表「${tableName}」已经添加!`);
+ return;
+ }
+ if (selectedTable.length >= 10) {
+ message.warning('最多只能同时分析10张表!');
+ return;
+ }
+ // @ts-ignore
+ setSelectedTable([...selectedTable, aiKey]);
+ message.success('添加成功');
+ } else {
+ message.error('移动无效,该内容不是数据表,无法参与AI分析!')
+ }
+ };
+
+ const onDragOver = (e: any) => {
+ e.preventDefault();
+ };
+
+
+ const selectBefore = (
+
+ );
+
+ const [isContentDisabled, setIsContentDisabled] = useState(true);
+
+ const handleSelectQuery = (selectedKeys: React.Key[], info: any) => {
+ setIsContentDisabled(!info.node.isLeaf);
+ if (info.node.isLeaf) {
+ setSelectedNode(info.node);
+ queryDispatch.fetchQueryInfo(info.node.key).then(r => {
+ if (r.code === 200) {
+ setQueryInfo(r.data);
+ }
+ });
+ } else {
+ setSelectedNode(null);
+ }
+ };
+
+ return (<>
+
+
+
+
+ {
+ queryDispatch.setQuerySearchKey(value);
+ queryDispatch.fetchTreeData({ projectId });
+ }}
+ style={{ width: 'calc(100% - 40px)' }}
+ />
+ }
+ onClick={() => showModal('add')}
+ style={{ width: '40px' }}
+ />
+
+
+
+
+
+
+
+
+
+ {isContentDisabled && (
+
+ 请选择一个查询
+
+ )}
+
{
+ aiSearch(value)
+ }}
+ />
+
+
+
+ {
+ setQueryInfo({
+ ...queryInfo,
+ sqlInfo: value
+ });
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 执行结果,
+ key: 'result',
+ children: ,
+ },
+ {
+ label: 执行计划,
+ key: 'plan',
+ children: ,
+ },
+ {
+ label: 历史记录,
+ key: 'history',
+ children: ,
+ },
+ ],
+ onChange: (key) => {
+ setTab(key);
+ },
+ }}
+ >
+ Auto
+
+
+ 清空
+ ,
+ ,
+ ]}
+ >
+ (
+ {
+ let tmp = [...selectedTable];
+ _.pull(tmp, item);
+ console.log(283, tmp);
+ setSelectedTable(tmp);
+ }}>删除]}
+ >
+
+ {item}
+
+
+ )}
+ />
+
+
+
+
+
+
+ setModalVisible(false)}
+ >
+
+
+
+ {modalType === 'add' && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+ 是叶子节点
+
+
+
+ >);
+};
+
+export default React.memo(DataQuery)
diff --git a/src/pages/dataQuery/style.less b/src/pages/dataQuery/style.less
new file mode 100644
index 00000000..a9598a1c
--- /dev/null
+++ b/src/pages/dataQuery/style.less
@@ -0,0 +1,25 @@
+.tree-container {
+ position: relative;
+ padding-right: 16px; /* 与 Sider 的右边缘保持一致的距离 */
+}
+
+.custom-tree-node {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+}
+
+.tree-node-action {
+ visibility: hidden;
+}
+
+.ant-tree-node-content-wrapper:hover .tree-node-action {
+ visibility: visible;
+}
+
+.ant-tree-node-content-wrapper {
+ display: flex !important;
+ align-items: center;
+ width: 100%;
+}
\ No newline at end of file
diff --git a/src/store/query/useQueryStore.tsx b/src/store/query/useQueryStore.tsx
index a94decd2..2ebef206 100644
--- a/src/store/query/useQueryStore.tsx
+++ b/src/store/query/useQueryStore.tsx
@@ -54,51 +54,77 @@ const useQueryStore = create(
}
});
},
- renameQuery: (model) => {
- EDIT('/ncnb/queryInfo/' + model.id, model).then(r => {
- if (r?.code === 200) {
- message.success('修改成功');
- get().dispatch.fetchTreeData({
+ renameQuery: async (model) => {
+ try {
+ const response = await EDIT('/ncnb/queryInfo/' + model.id, model);
+ if (response?.code === 200) {
+ message.success('Query renamed successfully');
+ await get().dispatch.fetchTreeData({
projectId: model.projectId
});
+ return response;
+ } else {
+ throw new Error(response?.msg || 'Failed to rename query');
}
- });
+ } catch (error) {
+ console.error("Error in renameQuery:", error);
+ message.error('Failed to rename query');
+ throw error;
+ }
},
- removeQuery: (model) => {
- DEL('/ncnb/queryInfo/' + model.id, {}).then(r => {
- if (r?.code === 200) {
- message.success('删除成功');
- get().dispatch.fetchTreeData({
- projectId: model.id
+ removeQuery: async (model) => {
+ try {
+ const response = await DEL('/ncnb/queryInfo/' + model.id, {});
+ if (response?.code === 200) {
+ message.success('Query deleted successfully');
+ await get().dispatch.fetchTreeData({
+ projectId: model.projectId
});
+ return response;
+ } else {
+ throw new Error(response?.msg || 'Failed to delete query');
}
- });
+ } catch (error) {
+ console.error("Error in removeQuery:", error);
+ message.error('Failed to delete query');
+ throw error;
+ }
},
- addQuery: (model) => {
- ADD('/ncnb/queryInfo', model).then(r => {
- if (r?.code === 200) {
- message.success('新增成功');
- get().dispatch.fetchTreeData({
- projectId: model.projectId
- });
+ addQuery: async (model) => {
+ try {
+ const response = await ADD('/ncnb/queryInfo', model);
+ if (response?.code === 200) {
+ message.success(model.isLeaf ? 'Query added successfully' : 'Folder added successfully');
+ await get().dispatch.fetchTreeData({ projectId: model.projectId });
+ return response;
+ } else {
+ throw new Error(response?.msg || 'Failed to add item');
}
- });
+ } catch (error) {
+ console.error("Error in addQuery:", error);
+ message.error('Failed to add item');
+ throw error;
+ }
},
fetchQueryInfo: (id) => {
return GET('/ncnb/queryInfo/' + id, {});
},
- fetchTreeData: (params) => {
+ fetchTreeData: async (params) => {
const title = get().querySearchKey;
if (title) {
- _.set(params, 'title', title);
+ params.title = title;
}
- TREE('/ncnb/queryInfo/tree', params).then(r => {
- if (r?.code === 200) {
- set({
- treeData: r?.data || []
- })
+ try {
+ const response = await TREE('/ncnb/queryInfo/tree', params);
+ if (response?.code === 200) {
+ set({ treeData: response?.data || [] });
+ } else {
+ throw new Error(response?.msg || 'Failed to fetch tree data');
}
- });
+ } catch (error) {
+ console.error("Error in fetchTreeData:", error);
+ message.error('Failed to fetch tree data');
+ }
},
setQuerySearchKey: (querySearchKey: string) => set(produce(state => {
state.querySearchKey = querySearchKey;