feat(config): 新增配置回收站与还原功能

- 配置列表支持 deleted=1 查询已删除配置
- 新增 POST /admin/config/restore/:id,还原配置并写入 restore 历史
- 前端新增回收站页面,配置列表「新建配置」右侧增加红色入口按钮
This commit is contained in:
2026-09-23 21:13:30 +08:00
parent 185e458b45
commit bb90f9cf79
7 changed files with 190 additions and 1 deletions
+1
View File
@@ -66,6 +66,7 @@ func main() {
admin.POST("/config/add", configHandler.Add)
admin.POST("/config/update", configHandler.Update)
admin.POST("/config/delete/:id", configHandler.Delete)
admin.POST("/config/restore/:id", configHandler.Restore)
admin.GET("/config/configHistories/:id", configHandler.GetHistory)
admin.POST("/config/history/delete/:id", configHandler.DeleteHistory)
admin.POST("/config/rollback/:id", configHandler.Rollback)
+37 -1
View File
@@ -28,6 +28,7 @@ type ConfigListRequest struct {
PageSize int `form:"pageSize" binding:"required"`
Env string `form:"env"`
AppId *int `form:"appId"`
Deleted bool `form:"deleted"`
}
func (h *ConfigHandler) List(c *gin.Context) {
@@ -48,7 +49,11 @@ func (h *ConfigHandler) List(c *gin.Context) {
}
// 构建查询条件
conditions := []string{"enabled = 1"}
enabledCond := "enabled = 1"
if req.Deleted {
enabledCond = "enabled = 0"
}
conditions := []string{enabledCond}
args := make([]interface{}, 0, 4)
if req.Env != "" {
conditions = append(conditions, "env = ?")
@@ -320,6 +325,37 @@ func (h *ConfigHandler) Delete(c *gin.Context) {
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0})
}
// Restore 从回收站还原配置
func (h *ConfigHandler) Restore(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var config model.AppConfig
has, _ := dao.Engine.ID(id).Get(&config)
if !has {
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "配置不存在"})
return
}
userInfo := middleware.GetUserInfo(c)
if err := h.permService.RequirePermission(userInfo, config.GroupId, config.AppId); err != nil {
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()})
return
}
if config.Enabled {
c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "配置未被删除"})
return
}
now := time.Now()
h.saveHistory(&config, userInfo.Name, "restore", now)
config.Enabled = true
config.UpdatedBy = userInfo.Name
config.UpdatedAt = &now
dao.Engine.ID(config.Id).Cols("enabled", "updated_by", "updated_at").Update(&config)
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: true})
}
func (h *ConfigHandler) GetHistory(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
+7
View File
@@ -38,6 +38,13 @@ export function deleteConfig(id) {
})
}
export function restoreConfig(id) {
return request({
url: `/admin/config/restore/${id}`,
method: 'post'
})
}
export function getConfigHistory(id, params) {
return request({
url: `/admin/config/configHistories/${id}`,
+5
View File
@@ -35,6 +35,11 @@ const routes = [
name: 'ConfigEdit',
component: () => import('../views/config/ConfigEdit.vue')
},
{
path: '/config/recycle',
name: 'RecycleBin',
component: () => import('../views/config/RecycleBin.vue')
},
{
path: '/app/list',
name: 'AppList',
+2
View File
@@ -22,6 +22,7 @@
<el-card>
<div slot="header" style="display: flex; justify-content: flex-end">
<el-button type="primary" @click="$router.push('/config/add')">新建配置</el-button>
<el-button type="danger" style="margin-left: 10px" @click="$router.push('/config/recycle')">回收站</el-button>
</div>
<el-table :data="list" v-loading="loading" border stripe>
<el-table-column prop="id" label="ID" width="60"></el-table-column>
@@ -70,6 +71,7 @@
<span v-if="scope.row.operate === 'delete'" style="color: #F56C6C">删除</span>
<span v-else-if="scope.row.operate === 'rollback'">回滚</span>
<span v-else-if="scope.row.operate === 'update'">更新</span>
<span v-else-if="scope.row.operate === 'restore'">还原</span>
<span v-else>-</span>
</template>
</el-table-column>
+2
View File
@@ -17,6 +17,7 @@
<el-card>
<div slot="header" style="display: flex; justify-content: flex-end">
<el-button type="primary" @click="$router.push('/config/add')">新建配置</el-button>
<el-button type="danger" style="margin-left: 10px" @click="$router.push('/config/recycle')">回收站</el-button>
</div>
<el-table :data="list" v-loading="loading" border stripe>
<el-table-column prop="id" label="ID" width="60"></el-table-column>
@@ -65,6 +66,7 @@
<span v-if="scope.row.operate === 'delete'" style="color: #F56C6C">删除</span>
<span v-else-if="scope.row.operate === 'rollback'">回滚</span>
<span v-else-if="scope.row.operate === 'update'">更新</span>
<span v-else-if="scope.row.operate === 'restore'">还原</span>
<span v-else>-</span>
</template>
</el-table-column>
+136
View File
@@ -0,0 +1,136 @@
<template>
<div>
<el-card style="margin-bottom: 16px">
<el-form :inline="true" @submit.native.prevent="handleSearch">
<el-form-item label="环境">
<el-select v-model="query.env" placeholder="请选择" clearable style="width: 160px">
<el-option v-for="p in profiles" :key="p.value" :label="p.label" :value="p.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="应用">
<el-select v-model="query.appId" placeholder="请选择" clearable filterable style="width: 200px">
<el-option v-for="app in apps" :key="app.value" :label="app.label" :value="parseInt(app.value)"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card>
<div slot="header">回收站(仅显示已删除配置)</div>
<el-table :data="list" v-loading="loading" border stripe>
<el-table-column prop="id" label="ID" width="60"></el-table-column>
<el-table-column prop="name" label="配置名"></el-table-column>
<el-table-column prop="env" label="环境" width="80"></el-table-column>
<el-table-column prop="appNames" label="所属应用"></el-table-column>
<el-table-column prop="version" label="版本" width="100"></el-table-column>
<el-table-column prop="updatedBy" label="删除人" width="120"></el-table-column>
<el-table-column label="删除时间" width="180">
<template slot-scope="scope">
{{ formatTime(scope.row.updatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template slot-scope="scope">
<el-button type="text" @click="handleRestore(scope.row)">还原</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
style="margin-top: 15px; text-align: right"
@current-change="handlePageChange"
:current-page="query.pageNo"
:page-size="query.pageSize"
:total="total"
layout="total, prev, pager, next"
></el-pagination>
</el-card>
</div>
</template>
<script>
import { getConfigList, restoreConfig } from '../../api/config'
import request from '../../api/request'
export default {
data() {
return {
list: [],
loading: false,
total: 0,
query: { pageNo: 1, pageSize: 10, env: '', appId: null },
profiles: [],
apps: []
}
},
created() {
this.loadData()
this.loadProfiles()
this.loadApps()
},
methods: {
handleSearch() {
this.query.pageNo = 1
this.loadData()
},
handleReset() {
this.query = { pageNo: 1, pageSize: 10, env: '', appId: null }
this.loadData()
},
async loadData() {
this.loading = true
try {
const params = {
pageNo: this.query.pageNo,
pageSize: this.query.pageSize,
deleted: 1
}
if (this.query.env) {
params.env = this.query.env
}
if (this.query.appId) {
params.appId = this.query.appId
}
const res = await getConfigList(params)
this.list = res.data || []
this.total = res.total || 0
} finally {
this.loading = false
}
},
async loadProfiles() {
try {
const res = await request({ url: '/admin/profile/options', method: 'get' })
this.profiles = res.data || []
} catch (e) {}
},
async loadApps() {
try {
const res = await request({ url: '/admin/app/options', method: 'get' })
this.apps = res.data || []
} catch (e) {}
},
handlePageChange(page) {
this.query.pageNo = page
this.loadData()
},
async handleRestore(row) {
try {
await this.$confirm(`确定还原配置「${row.name}」?`)
await restoreConfig(row.id)
this.$message.success('还原成功')
this.loadData()
} catch (e) {}
},
formatTime(time) {
if (!time) return ''
const d = new Date(time)
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
}
}
}
</script>