From ed472c0071c51195b6dddf75f69722c9baa4eab6 Mon Sep 17 00:00:00 2001 From: mereith Date: Sun, 8 Dec 2024 18:34:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8E=E5=8F=B0=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=8B=96=E6=8B=BD=E6=8E=92=E5=BA=8F=E5=8A=9F=E8=83=BD=20#48?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api-website/openapi.yaml | 37 +++ handler/handlers.go | 27 ++ main.go | 1 + service/tools.go | 24 ++ types/dto.go | 4 + ui/package.json | 4 + ui/pnpm-lock.yaml | 72 ++++++ ui/src/pages/admin/tabs/Tools.tsx | 415 +++++++++++++++++++----------- ui/src/utils/api.tsx | 4 + 9 files changed, 443 insertions(+), 145 deletions(-) diff --git a/api-website/openapi.yaml b/api-website/openapi.yaml index 8824817..bb7ad6b 100644 --- a/api-website/openapi.yaml +++ b/api-website/openapi.yaml @@ -585,5 +585,42 @@ paths: schema: $ref: "#/components/schemas/StandardResponse" + /api/admin/tools/sort: + put: + summary: 批量更新工具排序 + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer + sort: + type: integer + responses: + "200": + description: 更新成功 + content: + application/json: + schema: + $ref: "#/components/schemas/StandardResponse" + "400": + description: 请求参数错误 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "500": + description: 服务器内部错误 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" security: - BearerAuth: [] diff --git a/handler/handlers.go b/handler/handlers.go index ce55a29..7cdae41 100644 --- a/handler/handlers.go +++ b/handler/handlers.go @@ -425,3 +425,30 @@ func ManifastHanlder(c *gin.Context) { "background_color": "#ffffff", }) } + +func UpdateToolsSortHandler(c *gin.Context) { + var updates []types.UpdateToolsSortDto + if err := c.ShouldBindJSON(&updates); err != nil { + utils.CheckErr(err) + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "errorMessage": err.Error(), + }) + return + } + + err := service.UpdateToolsSort(updates) + if err != nil { + utils.CheckErr(err) + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "errorMessage": err.Error(), + }) + return + } + + c.JSON(200, gin.H{ + "success": true, + "message": "更新排序成功", + }) +} diff --git a/main.go b/main.go index 687b94b..782e3e5 100644 --- a/main.go +++ b/main.go @@ -96,6 +96,7 @@ func main() { admin.POST("/tool", handler.AddToolHandler) admin.DELETE("/tool/:id", handler.DeleteToolHandler) admin.PUT("/tool/:id", handler.UpdateToolHandler) + admin.PUT("/tools/sort", handler.UpdateToolsSortHandler) admin.POST("/catelog", handler.AddCatelogHandler) admin.DELETE("/catelog/:id", handler.DeleteCatelogHandler) diff --git a/service/tools.go b/service/tools.go index 61063de..0fb51f3 100644 --- a/service/tools.go +++ b/service/tools.go @@ -127,3 +127,27 @@ func UpdateToolIcon(id int64, logo string) { utils.CheckErr(err) UpdateImg(logo) } +func UpdateToolsSort(updates []types.UpdateToolsSortDto) error { + tx, err := database.DB.Begin() + if err != nil { + return err + } + + sql := `UPDATE nav_table SET sort = ? WHERE id = ?` + stmt, err := tx.Prepare(sql) + if err != nil { + tx.Rollback() + return err + } + defer stmt.Close() + + for _, update := range updates { + _, err = stmt.Exec(update.Sort, update.Id) + if err != nil { + tx.Rollback() + return err + } + } + + return tx.Commit() +} diff --git a/types/dto.go b/types/dto.go index 8b54cc2..6ae048c 100644 --- a/types/dto.go +++ b/types/dto.go @@ -47,3 +47,7 @@ type AddToolDto struct { Sort int `json:"sort"` Hide bool `json:"hide"` } +type UpdateToolsSortDto struct { + Id int `json:"id"` + Sort int `json:"sort"` +} diff --git a/ui/package.json b/ui/package.json index 2ee0109..a02a11e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -5,6 +5,10 @@ "private": true, "dependencies": { "@ant-design/icons": "^5.5.2", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@radix-ui/react-icons": "^1.3.2", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 9a35fa2..5ece281 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -11,6 +11,18 @@ importers: '@ant-design/icons': specifier: ^5.5.2 version: 5.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/modifiers': + specifier: ^9.0.0 + version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@18.3.1) '@radix-ui/react-icons': specifier: ^1.3.2 version: 1.3.2(react@18.3.1) @@ -929,6 +941,34 @@ packages: resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} engines: {node: '>=10'} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/modifiers@9.0.0': + resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@emotion/hash@0.8.0': resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} @@ -7507,6 +7547,38 @@ snapshots: '@ctrl/tinycolor@3.6.1': {} + '@dnd-kit/accessibility@3.1.1(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + + '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + '@emotion/hash@0.8.0': {} '@emotion/unitless@0.7.5': {} diff --git a/ui/src/pages/admin/tabs/Tools.tsx b/ui/src/pages/admin/tabs/Tools.tsx index 5fd5707..b47c493 100644 --- a/ui/src/pages/admin/tabs/Tools.tsx +++ b/ui/src/pages/admin/tabs/Tools.tsx @@ -15,8 +15,8 @@ import { Tooltip, Switch } from "antd"; -import { QuestionCircleOutlined } from '@ant-design/icons'; -import { useCallback, useState } from "react"; +import { QuestionCircleOutlined, HolderOutlined } from '@ant-design/icons'; +import React, { useCallback, useState, useEffect, useContext, useMemo } from "react"; import { getFilter, getOptions, mutiSearch } from "../../../utils/admin"; import { fetchAddTool, @@ -24,8 +24,82 @@ import { fetchExportTools, fetchImportTools, fetchUpdateTool, + fetchUpdateToolsSort, } from "../../../utils/api"; import { useData } from "../hooks/useData"; +import type { DragEndEvent } from '@dnd-kit/core'; +import { DndContext } from '@dnd-kit/core'; +import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities'; +import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; +import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; + +interface DataType { + id: number; + name: string; + sort: number; + [key: string]: any; +} + +interface RowContextProps { + setActivatorNodeRef?: (element: HTMLElement | null) => void; + listeners?: SyntheticListenerMap; +} + +const RowContext = React.createContext({}); + +const DragHandle: React.FC = () => { + const { setActivatorNodeRef, listeners } = useContext(RowContext); + return ( + + )} @@ -262,145 +387,145 @@ export const Tools: React.FC = (props) => { } > - { - let show = false; - // 过滤名称或描述 - if (searchString === "") { - show = true; - } else { - show = mutiSearch(item.name, searchString) || mutiSearch(item.desc, searchString); - } - // 过滤分类 - if (!catelogName || catelogName === "") { - show = show && true; - } else { - show = show && mutiSearch(item.catelog, catelogName); - } - return show; - }) || [] - } - size="small" - rowKey="id" - rowSelection={{ - type: "checkbox", - onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => { - setSelectRows(selectedRows); - }, - }} - pagination={{ - showSizeChanger: true, - pageSizeOptions: ['10', '20', '50', '100'], - defaultPageSize: 10, - showTotal: (total) => `共 ${total} 条` - }} - > - - { - return ( -
- {" "} - {record.logo.split(".").pop().includes("svg") ? ( - - ) : ( - - )} - {record.name} -
- ); - }} - /> - { - return value === record["catelog"]; - }} - /> - ( -
- {url} -
- )} - /> - 排序 - - - - - } - dataIndex="sort" - width={50} - /> - 隐藏 - - - - - } dataIndex={"hide"} width={50} render={(val) => { - return Boolean(val) ? "是" : "否" - }} /> - - { - return ( - - - { - handleDelete(record.id); - }} - title={`确定要删除 ${record.name} 吗?`} - > - - - - ); - }} - /> -
+ + i.id.toString())} + strategy={verticalListSortingStrategy} + > + { + setSelectRows(selectedRows); + }, + }} + pagination={{ + showSizeChanger: true, + pageSizeOptions: ['10', '20', '50', '100'], + defaultPageSize: 10, + showTotal: (total) => `共 ${total} 条` + }} + > + } + /> + + { + return ( +
+ {" "} + {record.logo.split(".").pop().includes("svg") ? ( + + ) : ( + + )} + {record.name} +
+ ); + }} + /> + { + return value === record["catelog"]; + }} + /> + ( +
+ {url} +
+ )} + /> + {/* 排序 + + + + + } + dataIndex="sort" + width={50} + /> */} + 隐藏 + + + + + } dataIndex={"hide"} width={50} render={(val) => { + return Boolean(val) ? "是" : "否" + }} /> + { + return ( + + + { + handleDelete(record.id); + }} + title={`确定要删除 ${record.name} 吗?`} + > + + + + ); + }} + /> +
+
+
{ = (props) => { rules={[{ required: true, message: "请填写描述" }]} name="desc" required - label="描述" + label="描���" labelCol={{ span: 4 }} > diff --git a/ui/src/utils/api.tsx b/ui/src/utils/api.tsx index 0729faf..e574674 100644 --- a/ui/src/utils/api.tsx +++ b/ui/src/utils/api.tsx @@ -141,3 +141,7 @@ export const fetchDeleteApiToken = async (id: number) => { const { data } = await axios.delete(`/api/admin/apiToken/${id}`); return data?.data || {}; }; +export const fetchUpdateToolsSort = async (updates: { id: number; sort: number }[]) => { + const { data } = await axios.put(`/api/admin/tools/sort`, updates); + return data?.data || {}; +}; \ No newline at end of file