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']} + > + + + + + + + + + 执行结果, + 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;