From 093d86040fc03950dcd0d8205cfd8635915f3281 Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Sun, 22 Jun 2025 16:35:30 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20feat(token):=20impl?= =?UTF-8?q?ement=20batch=20token=20deletion=20API=20&=20front-end=20integr?= =?UTF-8?q?ation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Back-end • model/token.go • Add `BatchDeleteTokens(ids []int, userId int)` – transactional DB removal + async Redis cache cleanup. • controller/token.go • Introduce `TokenBatch` DTO and `DeleteTokenBatch` handler calling the model layer; returns amount deleted. • router/api-router.go • Register `POST /api/token/batch` route (user-scoped). • Front-end (TokensTable.js) • Replace per-token deletion loops with single request to `/api/token/batch`. • Display dynamic i18n message: “Deleted {{count}} tokens!”. • Add modal confirmation: • Title “Batch delete token”. • Content “Are you sure you want to delete the selected {{count}} tokens?”. • UI/UX tweaks • Responsive button group (flex-wrap, mobile line-break). • Clear `selectedKeys` after refresh / successful deletion to avoid ghost selections. • i18n • Ensure placeholder style matches translation keys (`{{count}}`). This commit delivers efficient, scalable token management and an improved user experience across devices. --- controller/token.go | 29 ++++++++++++++ model/token.go | 34 ++++++++++++++++ router/api-router.go | 1 + web/src/components/table/TokensTable.js | 53 +++++++++++++++++++++++-- web/src/i18n/locales/en.json | 6 +++ 5 files changed, 120 insertions(+), 3 deletions(-) diff --git a/controller/token.go b/controller/token.go index c57552c0..173fc22e 100644 --- a/controller/token.go +++ b/controller/token.go @@ -258,3 +258,32 @@ func UpdateToken(c *gin.Context) { }) return } + +type TokenBatch struct { + Ids []int `json:"ids"` +} + +func DeleteTokenBatch(c *gin.Context) { + tokenBatch := TokenBatch{} + if err := c.ShouldBindJSON(&tokenBatch); err != nil || len(tokenBatch.Ids) == 0 { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "参数错误", + }) + return + } + userId := c.GetInt("id") + count, err := model.BatchDeleteTokens(tokenBatch.Ids, userId) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": count, + }) +} diff --git a/model/token.go b/model/token.go index 2ed2c09a..7e68f185 100644 --- a/model/token.go +++ b/model/token.go @@ -327,3 +327,37 @@ func CountUserTokens(userId int) (int64, error) { err := DB.Model(&Token{}).Where("user_id = ?", userId).Count(&total).Error return total, err } + +// BatchDeleteTokens 删除指定用户的一组令牌,返回成功删除数量 +func BatchDeleteTokens(ids []int, userId int) (int, error) { + if len(ids) == 0 { + return 0, errors.New("ids 不能为空!") + } + + tx := DB.Begin() + + var tokens []Token + if err := tx.Where("user_id = ? AND id IN (?)", userId, ids).Find(&tokens).Error; err != nil { + tx.Rollback() + return 0, err + } + + if err := tx.Where("user_id = ? AND id IN (?)", userId, ids).Delete(&Token{}).Error; err != nil { + tx.Rollback() + return 0, err + } + + if err := tx.Commit().Error; err != nil { + return 0, err + } + + if common.RedisEnabled { + gopool.Go(func() { + for _, t := range tokens { + _ = cacheDeleteToken(t.Key) + } + }) + } + + return len(tokens), nil +} diff --git a/router/api-router.go b/router/api-router.go index badfa7bf..db4c3898 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -125,6 +125,7 @@ func SetApiRouter(router *gin.Engine) { tokenRoute.POST("/", controller.AddToken) tokenRoute.PUT("/", controller.UpdateToken) tokenRoute.DELETE("/:id", controller.DeleteToken) + tokenRoute.POST("/batch", controller.DeleteTokenBatch) } redemptionRoute := apiRouter.Group("/redemption") redemptionRoute.Use(middleware.AdminAuth()) diff --git a/web/src/components/table/TokensTable.js b/web/src/components/table/TokensTable.js index bc6c7607..d49a344d 100644 --- a/web/src/components/table/TokensTable.js +++ b/web/src/components/table/TokensTable.js @@ -435,6 +435,7 @@ const TokensTable = () => { const refresh = async () => { await loadTokens(1); + setSelectedKeys([]); }; const copyText = async (text) => { @@ -583,6 +584,29 @@ const TokensTable = () => { } }; + const batchDeleteTokens = async () => { + if (selectedKeys.length === 0) { + showError(t('请先选择要删除的令牌!')); + return; + } + setLoading(true); + try { + const ids = selectedKeys.map((token) => token.id); + const res = await API.post('/api/token/batch', { ids }); + if (res?.data?.success) { + const count = res.data.data || 0; + showSuccess(t('已删除 {{count}} 个令牌!', { count })); + await refresh(); + } else { + showError(res?.data?.message || t('删除失败')); + } + } catch (error) { + showError(error.message); + } finally { + setLoading(false); + } + }; + const renderHeader = () => (
@@ -595,12 +619,12 @@ const TokensTable = () => {
-
+
+
+
Date: Sun, 22 Jun 2025 16:49:44 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8=20feat(tokens-table):=20add=20sel?= =?UTF-8?q?ectable=20copy=20modes=20for=20bulk=20token=20copy=20action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit enhances the “Copy Selected Tokens to Clipboard” feature in `TokensTable.js` by introducing a user-friendly modal that lets users choose how they want to copy tokens. Changes made • Replaced direct copy logic with a `Modal.info` dialog. • Modal displays the prompt “Please choose your copy mode”. • Added two buttons in a custom footer: – **Name + Secret**: copies `tokenName sk-tokenKey`. – **Secret Only**: copies `sk-tokenKey`. • Each button triggers the copy operation and closes the dialog. • Maintains existing validations (e.g., selection check, clipboard feedback). Benefits • Gives users clear control over copy format, reducing manual editing. • Aligns UI with Semi UI’s best practices via custom modal footer. No backend/API changes are involved; all updates are limited to the front-end UI logic. --- web/src/components/table/TokensTable.js | 49 +++++++++++++++++++++---- web/src/i18n/locales/en.json | 5 ++- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/web/src/components/table/TokensTable.js b/web/src/components/table/TokensTable.js index d49a344d..0a049c39 100644 --- a/web/src/components/table/TokensTable.js +++ b/web/src/components/table/TokensTable.js @@ -639,20 +639,53 @@ const TokensTable = () => { type="warning" icon={} className="!rounded-full flex-1 md:flex-initial" - onClick={async () => { + onClick={() => { if (selectedKeys.length === 0) { showError(t('请至少选择一个令牌!')); return; } - let keys = ''; - for (let i = 0; i < selectedKeys.length; i++) { - keys += - selectedKeys[i].name + ' sk-' + selectedKeys[i].key + '\n'; - } - await copyText(keys); + Modal.info({ + title: t('复制令牌'), + icon: null, + content: t('请选择你的复制方式'), + footer: ( + + + + + ), + }); }} > - {t('复制所选令牌到剪贴板')} + {t('复制所选令牌')}