This commit is contained in:
2026-04-28 20:25:20 +08:00
parent a3331a1def
commit 7db64658f9
7 changed files with 517 additions and 172 deletions
+80 -82
View File
@@ -2,8 +2,8 @@ package routers
import (
"encoding/json"
"ops/models"
parsefmt "fmt"
"ops/models"
"slices"
"strings"
"time"
@@ -18,7 +18,7 @@ var (
)
// 更新管理员成员缓存
func updatePurchaseAdminsCash() {
func PurchaseUpdateAdminsCash() {
purchaseAdmins = nil
// id 1 是系统管理员
purchaseAdmins = append(purchaseAdmins, 1)
@@ -92,8 +92,6 @@ type TabPurchaseCosts struct {
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"`
}
// TabPurchaseCommit 记录订单状态变更及评论
type TabPurchaseCommit struct {
ID uint `gorm:"primarykey"`
@@ -132,7 +130,7 @@ func ApiPurchaseInit() {
//先检查用户组有没有这个key purchase
purchaseUserGroup.Name = "purchase_admin"
if models.DB.Where(&purchaseUserGroup).First(&purchaseUserGroup).Error == nil {
updatePurchaseAdminsCash()
PurchaseUpdateAdminsCash()
} else {
purchaseUserGroup.Type = "usergroup"
models.DB.Create(&purchaseUserGroup)
@@ -229,49 +227,49 @@ func ApiPurchase(r *gin.RouterGroup) {
commitResps = append(commitResps, resp)
}
// 查询关联工单(通过 TabWorkOrderPurchaseOrderBind 表)
type WorkOrderInfo struct {
ID uint `json:"id"`
Title string `json:"title"`
CurrentStatus string `json:"status"`
}
var linkedWorkOrders []WorkOrderInfo
var woBinds []TabWorkOrderPurchaseOrderBind
models.DB.Where("purchase_order_id = ?", from.ID).Find(&woBinds)
if len(woBinds) > 0 {
woIDSet := make(map[uint]bool)
var woIDs []uint
for _, wb := range woBinds {
if !woIDSet[wb.WorkOrderID] {
woIDSet[wb.WorkOrderID] = true
woIDs = append(woIDs, wb.WorkOrderID)
// 查询关联工单(通过 TabWorkOrderPurchaseOrderBind 表)
type WorkOrderInfo struct {
ID uint `json:"id"`
Title string `json:"title"`
CurrentStatus string `json:"status"`
}
var linkedWorkOrders []WorkOrderInfo
var woBinds []TabWorkOrderPurchaseOrderBind
models.DB.Where("purchase_order_id = ?", from.ID).Find(&woBinds)
if len(woBinds) > 0 {
woIDSet := make(map[uint]bool)
var woIDs []uint
for _, wb := range woBinds {
if !woIDSet[wb.WorkOrderID] {
woIDSet[wb.WorkOrderID] = true
woIDs = append(woIDs, wb.WorkOrderID)
}
}
var workOrders []TabWorkOrder
models.DB.Where("id IN ?", woIDs).Find(&workOrders)
for _, wo := range workOrders {
linkedWorkOrders = append(linkedWorkOrders, WorkOrderInfo{
ID: wo.ID,
Title: wo.Title,
CurrentStatus: wo.CurrentStatus,
})
}
}
var workOrders []TabWorkOrder
models.DB.Where("id IN ?", woIDs).Find(&workOrders)
for _, wo := range workOrders {
linkedWorkOrders = append(linkedWorkOrders, WorkOrderInfo{
ID: wo.ID,
Title: wo.Title,
CurrentStatus: wo.CurrentStatus,
})
if linkedWorkOrders == nil {
linkedWorkOrders = []WorkOrderInfo{}
}
}
if linkedWorkOrders == nil {
linkedWorkOrders = []WorkOrderInfo{}
}
// 判断当前用户是否可以修改
canModify := canModifyPurchase(user.ID, order.UserID)
// 判断当前用户是否可以修改
canModify := canModifyPurchase(user.ID, order.UserID)
ReturnJson(ctx, "apiOK", gin.H{
"order": order,
"canModify": canModify,
"costs": costs,
"photos": files,
"commits": commitResps,
"workOrders": linkedWorkOrders,
})
ReturnJson(ctx, "apiOK", gin.H{
"order": order,
"canModify": canModify,
"costs": costs,
"photos": files,
"commits": commitResps,
"workOrders": linkedWorkOrders,
})
})
// 更新订单状态(可附带评论)
@@ -432,46 +430,46 @@ func ApiPurchase(r *gin.RouterGroup) {
//fmt.Println(user)
// DebugPrintJson(data)
type From_purchase_getorders struct {
Search string
Status string
Entries int
Page int
type From_purchase_getorders struct {
Search string
Status string
Entries int
Page int
}
var jsondata From_purchase_getorders
if err := decodeJSON(data, &jsondata); err == nil {
//fmt.Println(jsondata)
is_data_ok := true
if jsondata.Entries <= 0 || jsondata.Entries > 300 {
is_data_ok = false
}
if jsondata.Page <= 0 {
is_data_ok = false
}
var jsondata From_purchase_getorders
if err := decodeJSON(data, &jsondata); err == nil {
//fmt.Println(jsondata)
if is_data_ok {
is_data_ok := true
if jsondata.Entries <= 0 || jsondata.Entries > 300 {
is_data_ok = false
}
if jsondata.Page <= 0 {
is_data_ok = false
}
if is_data_ok {
//读取有多少条目
var count int64
query := models.DB.Model(TabPurchaseOrder{})
if jsondata.Search != "" {
// 精确匹配订单 ID
var id uint
if _, err := parsefmt.Sscanf(jsondata.Search, "%d", &id); err == nil && id > 0 {
query = query.Where("id = ?", id)
} else {
// 模糊匹配标题和用途(Remark)
query = query.Where("title LIKE ? OR remark LIKE ?",
"%"+jsondata.Search+"%", "%"+jsondata.Search+"%")
}
//读取有多少条目
var count int64
query := models.DB.Model(TabPurchaseOrder{})
if jsondata.Search != "" {
// 精确匹配订单 ID
var id uint
if _, err := parsefmt.Sscanf(jsondata.Search, "%d", &id); err == nil && id > 0 {
query = query.Where("id = ?", id)
} else {
// 模糊匹配标题和用途(Remark)
query = query.Where("title LIKE ? OR remark LIKE ?",
"%"+jsondata.Search+"%", "%"+jsondata.Search+"%")
}
if jsondata.Status != "" {
query = query.Where("order_status = ?", jsondata.Status)
}
query.Count(&count)
}
if jsondata.Status != "" {
query = query.Where("order_status = ?", jsondata.Status)
}
query.Count(&count)
//读取条目
var getorders []TabPurchaseOrder
@@ -820,11 +818,11 @@ func ApiPurchase(r *gin.RouterGroup) {
ReturnJson(ctx, "apiOK", map[string]interface{}{
"pending": count.Pending,
"ordered": count.Ordered,
"arrived": count.Arrived,
"arrived": count.Arrived,
"received": count.Received,
"lost": count.Lost,
"lost": count.Lost,
"returned": count.Returned,
"total": count.Total,
"total": count.Total,
})
})
+40 -44
View File
@@ -40,7 +40,7 @@ type TabScheduleLog struct {
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime;comment:操作时间"`
}
type fromAddEvent struct {
ID uint `json:"id"`
ID uint `json:"id"`
Title string `json:"title" binding:"required"` // 日程标题
Start string `json:"start" binding:"required"` // 开始日期
End string `json:"end" binding:"required"` // 结束日期
@@ -53,27 +53,27 @@ type fromGetEvents struct {
}
var (
userGroup TabUserGroups
userGroup TabUserGroups
scheduleAdmins []uint
)
//更新管理员成员缓存
func updateAdminsCash(){
//先清空切片
scheduleAdmins=nil
// 更新管理员成员缓存
func ScheduleUpdateAdminsCash() {
//先清空切片
scheduleAdmins = nil
//获取管理员用户组id
//id 1是系统管理员 直接appen
scheduleAdmins=append(scheduleAdmins, 1)
//获取管理员用户组id
//id 1是系统管理员 直接appen
scheduleAdmins = append(scheduleAdmins, 1)
//读取所有绑定了这个用户组的用户id
var usergroupbind []TabUserGroupBinds
usergroupbindfind:= TabUserGroupBinds{
usergroupbindfind := TabUserGroupBinds{
GroupID: userGroup.ID,
}
models.DB.Where(&usergroupbindfind).Find(&usergroupbind)
for _ , item:= range usergroupbind{
if !slices.Contains(scheduleAdmins,item.UserID){
scheduleAdmins=append(scheduleAdmins, item.UserID)
for _, item := range usergroupbind {
if !slices.Contains(scheduleAdmins, item.UserID) {
scheduleAdmins = append(scheduleAdmins, item.UserID)
}
}
@@ -87,7 +87,7 @@ func ApiScheduleInit() {
//先检查用户组有没有这个key
userGroup.Name = "schedule_admin"
if models.DB.Where(&userGroup).First(&userGroup).Error == nil {
updateAdminsCash()
ScheduleUpdateAdminsCash()
} else {
userGroup.Type = "usergroup"
models.DB.Create(&userGroup)
@@ -115,7 +115,7 @@ func ApiSchedule(r *gin.RouterGroup) {
//已登录 进一步判断编辑权限
temp["edit"] = false
if slices.Contains(scheduleAdmins,user.ID){
if slices.Contains(scheduleAdmins, user.ID) {
temp["edit"] = true
}
if item.UserID == user.ID {
@@ -149,29 +149,29 @@ func ApiSchedule(r *gin.RouterGroup) {
var from fromAddEvent
if err := mapstructure.Decode(data, &from); err == nil {
//先从数据库拉取原始event数据
oldEvent:=TabSchedule{}
if models.DB.Where("id = ?", from.ID).First(&oldEvent).Error==nil{
oldEvent := TabSchedule{}
if models.DB.Where("id = ?", from.ID).First(&oldEvent).Error == nil {
//需要先判断修改权限
var isCanEdit=false
if slices.Contains(scheduleAdmins,user.ID){ //用户id是管理员
var isCanEdit = false
if slices.Contains(scheduleAdmins, user.ID) { //用户id是管理员
isCanEdit = true
}
if oldEvent.UserID==user.ID{//event是用户创建的
if oldEvent.UserID == user.ID { //event是用户创建的
isCanEdit = true
}
if isCanEdit{
if isCanEdit {
tosql := TabSchedule{}
tosql.DeletedAt.Scan(time.Now())
//fmt.Println(tosql)
findEvent:=TabSchedule{
findEvent := TabSchedule{
ID: oldEvent.ID,
}
if models.DB.Where(&findEvent).Updates(&tosql).Error==nil{
if models.DB.Where(&findEvent).Updates(&tosql).Error == nil {
//应该修改完了 写日志
//把最新数据再读出来
models.DB.Where(&findEvent).First(&findEvent)
newContent, _ := json.Marshal(findEvent) //转 JSON
oldContent, _ := json.Marshal(oldEvent) //转 JSON
oldContent, _ := json.Marshal(oldEvent) //转 JSON
tosqllog := TabScheduleLog{
UserID: user.ID,
ScheduleID: oldEvent.ID,
@@ -182,13 +182,13 @@ func ApiSchedule(r *gin.RouterGroup) {
}
models.DB.Create(&tosqllog)
ReturnJson(ctx, "apiOK", nil)
}else{
} else {
ReturnJson(ctx, "apiErr", nil)
}
}else{
} else {
ReturnJson(ctx, "schedule_permission_denied", nil)
}
}else{
} else {
ReturnJson(ctx, "schedule_event_not_find", nil)
}
} else {
@@ -201,24 +201,24 @@ func ApiSchedule(r *gin.RouterGroup) {
})
r.POST("/editevent", func(ctx *gin.Context) {
isAuth, user, data := AuthenticationAuthority(ctx)
isAuth, user, data := AuthenticationAuthority(ctx)
if isAuth {
var from fromAddEvent
if err := mapstructure.Decode(data, &from); err == nil {
//先从数据库拉取原始event数据
oldEvent:=TabSchedule{}
if models.DB.Where("id = ?", from.ID).First(&oldEvent).Error==nil{
oldEvent := TabSchedule{}
if models.DB.Where("id = ?", from.ID).First(&oldEvent).Error == nil {
//需要先判断修改权限
var isCanEdit=false
if slices.Contains(scheduleAdmins,user.ID){ //用户id是管理员
var isCanEdit = false
if slices.Contains(scheduleAdmins, user.ID) { //用户id是管理员
isCanEdit = true
}
if oldEvent.UserID==user.ID{//event是用户创建的
if oldEvent.UserID == user.ID { //event是用户创建的
isCanEdit = true
}
if isCanEdit{
if isCanEdit {
tosql := TabSchedule{
// UserID: user.ID, //如果是管理员修改的话会覆盖掉创建者的id
Title: from.Title,
@@ -228,16 +228,16 @@ func ApiSchedule(r *gin.RouterGroup) {
}
//fmt.Println(tosql)
findEvent:=TabSchedule{
findEvent := TabSchedule{
ID: oldEvent.ID,
}
if models.DB.Where(&findEvent).Updates(&tosql).Error==nil{
if models.DB.Where(&findEvent).Updates(&tosql).Error == nil {
//应该修改完了 写日志
//把最新数据再读出来
models.DB.Where(&findEvent).First(&findEvent)
newContent, _ := json.Marshal(findEvent) //转 JSON
oldContent, _ := json.Marshal(oldEvent) //转 JSON
oldContent, _ := json.Marshal(oldEvent) //转 JSON
tosqllog := TabScheduleLog{
UserID: user.ID,
ScheduleID: oldEvent.ID,
@@ -249,22 +249,18 @@ func ApiSchedule(r *gin.RouterGroup) {
models.DB.Create(&tosqllog)
ReturnJson(ctx, "apiOK", nil)
}else{
} else {
ReturnJson(ctx, "apiErr", nil)
}
}else{
} else {
ReturnJson(ctx, "schedule_permission_denied", nil)
}
}else{
} else {
ReturnJson(ctx, "schedule_event_not_find", nil)
}
} else {
ReturnJson(ctx, "jsonErr", nil)
}
+149 -16
View File
@@ -156,15 +156,15 @@ func ApiSysAdmin(r *gin.RouterGroup) {
return
}
var params struct {
GroupID uint `json:"group_id"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
if err := mapstructure.Decode(data, &params); err != nil {
params.Page = 1
params.PageSize = 20
}
var params struct {
GroupID float64 `json:"group_id" mapstructure:"group_id"`
Page float64 `json:"page" mapstructure:"page"`
PageSize float64 `json:"page_size" mapstructure:"page_size"`
}
if err := mapstructure.Decode(data, &params); err != nil {
params.Page = 1
params.PageSize = 20
}
if params.Page < 1 {
params.Page = 1
}
@@ -179,12 +179,21 @@ func ApiSysAdmin(r *gin.RouterGroup) {
return
}
offset := (params.Page - 1) * params.PageSize
groupID := uint(params.GroupID)
page := int(params.Page)
pageSize := int(params.PageSize)
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
offset := (page - 1) * pageSize
var binds []TabUserGroupBinds
var total int64
models.DB.Model(&TabUserGroupBinds{}).Where("group_id = ?", params.GroupID).Count(&total)
models.DB.Where("group_id = ?", params.GroupID).Order("id ASC").Offset(offset).Limit(params.PageSize).Find(&binds)
models.DB.Model(&TabUserGroupBinds{}).Where("group_id = ?", groupID).Count(&total)
models.DB.Where("group_id = ?", groupID).Order("id ASC").Offset(offset).Limit(pageSize).Find(&binds)
// 获取成员用户信息
var members []map[string]interface{}
@@ -209,12 +218,12 @@ func ApiSysAdmin(r *gin.RouterGroup) {
}
var redata map[string]interface{} = make(map[string]interface{})
redata["group_id"] = params.GroupID
redata["group_id"] = groupID
redata["group_name"] = group.Name
redata["members"] = members
redata["total"] = total
redata["page"] = params.Page
redata["page_size"] = params.PageSize
redata["page"] = page
redata["page_size"] = pageSize
ReturnJson(ctx, "apiOK", redata)
})
@@ -316,7 +325,131 @@ func ApiSysAdmin(r *gin.RouterGroup) {
ReturnJson(ctx, "apiOK", nil)
})
// 获取登录失败日志(仅系统管理员可访问)
// 添加用户组成员(仅系统管理员可访问)
r.POST("/add_group_member", func(ctx *gin.Context) {
isAuth, adminUser, data := AuthenticationAuthority(ctx)
if !isAuth {
ReturnJson(ctx, "userNoLogin", nil)
return
}
// 检查是否为系统管理员
if !SysAdminCheck(adminUser.ID) {
ReturnJson(ctx, "permission_denied", nil)
return
}
var params struct {
GroupID float64 `json:"group_id" mapstructure:"group_id"`
UserID float64 `json:"user_id" mapstructure:"user_id"`
}
if err := mapstructure.Decode(data, &params); err != nil || params.GroupID == 0 || params.UserID == 0 {
ReturnJson(ctx, "parameErr", nil)
return
}
// 验证用户组是否存在
var group TabUserGroups
if models.DB.First(&group, uint(params.GroupID)).Error != nil {
ReturnJson(ctx, "groupNotFound", nil)
return
}
// 验证用户是否存在
var user TabUser
if models.DB.First(&user, uint(params.UserID)).Error != nil {
ReturnJson(ctx, "userNotFound", nil)
return
}
// 检查绑定是否已存在
var existingBind TabUserGroupBinds
if models.DB.Where("group_id = ? AND user_id = ?", uint(params.GroupID), uint(params.UserID)).First(&existingBind).Error == nil {
ReturnJson(ctx, "userAlreadyInGroup", nil)
return
}
// 创建绑定
newBind := TabUserGroupBinds{
UserID: uint(params.UserID),
GroupID: uint(params.GroupID),
}
if err := models.DB.Create(&newBind).Error; err != nil {
ReturnJson(ctx, "dbErr", nil)
return
}
// 根据组名刷新对应的权限缓存
switch group.Name {
case "admins":
updateSysAdminsCash()
case "schedule_admin":
ScheduleUpdateAdminsCash()
case "purchase_admin":
PurchaseUpdateAdminsCash()
case "work_order_admin":
WorkOrderUpdateAdminsCash()
case "warehouse_admin":
WarehouseUpdateAdminsCash()
}
ReturnJson(ctx, "apiOK", nil)
})
// 移除用户组成员(仅系统管理员可访问)
r.POST("/remove_group_member", func(ctx *gin.Context) {
isAuth, adminUser, data := AuthenticationAuthority(ctx)
if !isAuth {
ReturnJson(ctx, "userNoLogin", nil)
return
}
// 检查是否为系统管理员
if !SysAdminCheck(adminUser.ID) {
ReturnJson(ctx, "permission_denied", nil)
return
}
var params struct {
GroupID float64 `json:"group_id" mapstructure:"group_id"`
UserID float64 `json:"user_id" mapstructure:"user_id"`
}
if err := mapstructure.Decode(data, &params); err != nil || params.GroupID == 0 || params.UserID == 0 {
ReturnJson(ctx, "parameErr", nil)
return
}
// 验证用户组是否存在
var group TabUserGroups
if models.DB.First(&group, uint(params.GroupID)).Error != nil {
ReturnJson(ctx, "groupNotFound", nil)
return
}
// 删除绑定
if err := models.DB.Where("group_id = ? AND user_id = ?", uint(params.GroupID), uint(params.UserID)).Delete(&TabUserGroupBinds{}).Error; err != nil {
ReturnJson(ctx, "dbErr", nil)
return
}
// 根据组名刷新对应的权限缓存
switch group.Name {
case "admins":
updateSysAdminsCash()
case "schedule_admin":
ScheduleUpdateAdminsCash()
case "purchase_admin":
PurchaseUpdateAdminsCash()
case "work_order_admin":
WorkOrderUpdateAdminsCash()
case "warehouse_admin":
WarehouseUpdateAdminsCash()
}
ReturnJson(ctx, "apiOK", nil)
})
// 获取登录失败日志(仅系统管理员可访问)
r.POST("/login_fail_logs", func(ctx *gin.Context) {
isAuth, _, data := AuthenticationAuthority(ctx)
if !isAuth {
+2 -2
View File
@@ -71,7 +71,7 @@ var (
)
// updateWarehouseAdminsCash 刷新仓库管理员缓存
func updateWarehouseAdminsCash() {
func WarehouseUpdateAdminsCash() {
warehouseAdmins = nil
warehouseAdmins = append(warehouseAdmins, 1) // id=1 超级管理员
var binds []TabUserGroupBinds
@@ -144,7 +144,7 @@ func ApiWarehouseInit() {
warehouseUserGroup.Name = "warehouse_admin"
if models.DB.Where(&warehouseUserGroup).First(&warehouseUserGroup).Error == nil {
updateWarehouseAdminsCash()
WarehouseUpdateAdminsCash()
} else {
warehouseUserGroup.Type = "usergroup"
models.DB.Create(&warehouseUserGroup)
+3 -8
View File
@@ -17,7 +17,7 @@ var (
)
// updateWorkOrderAdminsCash 刷新工单管理员缓存
func updateWorkOrderAdminsCash() {
func WorkOrderUpdateAdminsCash() {
workOrderAdmins = nil
workOrderAdmins = append(workOrderAdmins, 1) // id=1 超级管理员
var binds []TabUserGroupBinds
@@ -50,8 +50,6 @@ type TabWorkOrder struct {
DeletedAt gorm.DeletedAt `gorm:"index"`
}
type TabWorkOrderCommit struct {
ID uint `gorm:"primarykey"`
WorkOrderID uint `gorm:"not null;index;comment:关联工单ID"`
@@ -76,8 +74,6 @@ type TabWorkOrderLog struct {
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"`
}
// PurchaseOrderInfo 采购订单简要信息
type PurchaseOrderInfo struct {
ID uint `json:"id"`
@@ -93,10 +89,9 @@ func ApiWorkOrderInit() {
models.DB.AutoMigrate(&TabWorkOrderCommit{})
models.DB.AutoMigrate(&TabWorkOrderLog{})
workOrderUserGroup.Name = "work_order_admin"
if models.DB.Where(&workOrderUserGroup).First(&workOrderUserGroup).Error == nil {
updateWorkOrderAdminsCash()
WorkOrderUpdateAdminsCash()
} else {
workOrderUserGroup.Type = "usergroup"
models.DB.Create(&workOrderUserGroup)
@@ -348,7 +343,7 @@ func ApiWorkOrder(r *gin.RouterGroup) {
// 为每条 commit 附加图片和采购订单
type CommitWithPhotos struct {
TabWorkOrderCommit
Photos []TabFileInfo `json:"photos"`
Photos []TabFileInfo `json:"photos"`
PurchaseOrders []PurchaseOrderInfo `json:"purchaseOrders"`
}
var commitsWithPhotos []CommitWithPhotos
+10
View File
@@ -36,6 +36,16 @@ export const authApi = {
return api.post('/admin/group_members', { group_id: groupId, ...params })
},
/** 添加用户组成员(仅管理员可访问) */
addGroupMember(groupId, userId) {
return api.post('/admin/add_group_member', { group_id: groupId, user_id: userId })
},
/** 移除用户组成员(仅管理员可访问) */
removeGroupMember(groupId, userId) {
return api.post('/admin/remove_group_member', { group_id: groupId, user_id: userId })
},
/** 获取用户详细信息(仅管理员可访问) */
getUserDetail(userId) {
return api.post('/admin/user_detail', { user_id: userId })
+229 -16
View File
@@ -4,7 +4,8 @@ import { useUserStore } from '@/stores/user'
import { useUsersStore } from '@/stores/users'
import { useToastStore } from '@/stores/toast'
import { authApi } from '@/api/auth'
import { IconSearch, IconRefresh, IconChevronLeft, IconChevronRight } from '@tabler/icons-vue'
import ConfirmDialog from '@/components/ConfirmDialog.vue'
import { IconSearch, IconRefresh, IconChevronLeft, IconChevronRight, IconPlus, IconX } from '@tabler/icons-vue'
const toast = useToastStore()
@@ -49,6 +50,17 @@ const userDetailLoading = ref(false)
const newPassword = ref('')
const resetPasswordLoading = ref(false)
// 确认弹窗相关
const showConfirmDialog = ref(false)
const confirmDialogConfig = ref({
title: '确认',
message: '',
confirmText: '确认',
cancelText: '取消',
danger: false,
onConfirm: null,
})
const tabs = [
{ id: 'users', label: '用户管理' },
{ id: 'groups', label: '用户组' },
@@ -106,6 +118,13 @@ function onPageChange(page) {
const totalPages = computed(() => Math.ceil(userTotal.value / userPageSize.value))
const groupMemberTotalPages = computed(() => Math.ceil(groupMemberTotal.value / groupMemberPageSize.value))
// 添加成员弹窗相关
const showAddMemberDialog = ref(false)
const addMemberSearch = ref('')
const addMemberSearchResults = ref([])
const addMemberLoading = ref(false)
const addMemberSearchLoading = ref(false)
const loginFailLogTotalPages = computed(() => Math.ceil(loginFailLogTotal.value / loginFailLogPageSize.value))
async function fetchGroups() {
@@ -160,6 +179,97 @@ function onGroupMemberPageChange(page) {
fetchGroupMembers()
}
async function openAddMemberDialog() {
showAddMemberDialog.value = true
addMemberSearch.value = ''
addMemberSearchResults.value = []
}
function closeAddMemberDialog() {
showAddMemberDialog.value = false
addMemberSearch.value = ''
addMemberSearchResults.value = []
}
async function searchUsersToAdd() {
if (!addMemberSearch.value.trim()) {
addMemberSearchResults.value = []
return
}
addMemberSearchLoading.value = true
try {
const res = await authApi.getUsers({
page: 1,
page_size: 10,
search: addMemberSearch.value,
})
if (res.errCode === 0) {
// 过滤掉已经在组中的用户
const existingMemberIds = new Set(groupMembers.value.map(m => m.id))
addMemberSearchResults.value = (res.data.users || []).filter(u => !existingMemberIds.has(u.id))
}
} catch {
// 错误已由拦截器处理
} finally {
addMemberSearchLoading.value = false
}
}
async function addGroupMember(userId) {
if (!selectedGroup.value) return
addMemberLoading.value = true
try {
const res = await authApi.addGroupMember(selectedGroup.value.id, userId)
if (res.errCode === 0) {
toast.success('成员添加成功')
fetchGroupMembers()
// 从搜索结果中移除已添加的用户
addMemberSearchResults.value = addMemberSearchResults.value.filter(u => u.id !== userId)
} else {
toast.error(res.raw?.err_msg || '添加失败')
}
} catch {
// 错误已由拦截器处理
} finally {
addMemberLoading.value = false
}
}
function openConfirmDialog(config) {
confirmDialogConfig.value = { ...confirmDialogConfig.value, ...config }
showConfirmDialog.value = true
}
function handleConfirm() {
if (confirmDialogConfig.value.onConfirm) {
confirmDialogConfig.value.onConfirm()
}
showConfirmDialog.value = false
}
async function removeGroupMember(userId) {
if (!selectedGroup.value) return
openConfirmDialog({
title: '移除成员',
message: '确定要移除该成员吗?',
confirmText: '移除',
danger: true,
onConfirm: async () => {
try {
const res = await authApi.removeGroupMember(selectedGroup.value.id, userId)
if (res.errCode === 0) {
toast.success('成员移除成功')
fetchGroupMembers()
} else {
toast.error(res.raw?.err_msg || '移除失败')
}
} catch {
// 错误已由拦截器处理
}
},
})
}
async function fetchLoginFailLogs() {
loginFailLogsLoading.value = true
try {
@@ -482,21 +592,9 @@ onMounted(() => {
: 'hover:bg-gray-50 dark:hover:bg-dk-base'
]"
>
<div class="flex items-center justify-between">
<div class="flex items-center">
<div>
<div class="font-medium text-gray-900 dark:text-dk-text">{{ group.name }}</div>
<div class="text-xs text-gray-500 dark:text-dk-subtle">
{{ group.memberCount }} 位成员
</div>
</div>
<div class="flex -space-x-1">
<img
v-for="memberId in group.memberIDs?.slice(0, 3)"
:key="memberId"
:src="usersStore.getAvatarUrlFromUserID(memberId)"
class="h-6 w-6 rounded-full border-2 border-white object-cover dark:border-dk-card"
:title="usersStore.getUsernameFromUserID(memberId)"
/>
</div>
</div>
</button>
@@ -515,6 +613,12 @@ onMounted(() => {
<h3 class="font-semibold text-gray-900 dark:text-dk-text">{{ selectedGroup.name }}</h3>
<p class="text-sm text-gray-500 dark:text-dk-subtle"> {{ groupMemberTotal }} 位成员</p>
</div>
<button
@click="openAddMemberDialog"
class="flex items-center gap-1 rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"
>
<IconPlus :size="16" /> 添加成员
</button>
</div>
<div class="overflow-hidden rounded-md border border-gray-200 dark:border-dk-muted">
@@ -524,14 +628,15 @@ onMounted(() => {
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">用户</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">邮箱</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">类型</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-dk-muted dark:bg-dk-card">
<tr v-if="groupMembersLoading" class="text-center">
<td colspan="3" class="py-8 text-gray-500 dark:text-dk-subtle">加载中...</td>
<td colspan="4" class="py-8 text-gray-500 dark:text-dk-subtle">加载中...</td>
</tr>
<tr v-else-if="groupMembers.length === 0" class="text-center">
<td colspan="3" class="py-8 text-gray-500 dark:text-dk-subtle">暂无成员</td>
<td colspan="4" class="py-8 text-gray-500 dark:text-dk-subtle">暂无成员</td>
</tr>
<tr v-for="member in groupMembers" :key="member.id" class="hover:bg-gray-50 dark:hover:bg-dk-base">
<td class="whitespace-nowrap px-4 py-3">
@@ -557,6 +662,14 @@ onMounted(() => {
{{ member.type }}
</span>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm">
<button
@click="removeGroupMember(member.id)"
class="text-red-600 hover:text-red-700 dark:text-red-400"
>
移除
</button>
</td>
</tr>
</tbody>
</table>
@@ -873,4 +986,104 @@ onMounted(() => {
</div>
</div>
</div>
<!-- 添加成员弹窗 -->
<div
v-if="showAddMemberDialog"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
@click.self="closeAddMemberDialog"
>
<div class="w-full max-w-md rounded-lg bg-white p-6 shadow-xl dark:bg-dk-card">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900 dark:text-dk-text">添加成员到 {{ selectedGroup?.name }}</h3>
<button
@click="closeAddMemberDialog"
class="text-gray-400 hover:text-gray-600 dark:text-dk-subtle dark:hover:text-dk-text"
>
<IconX :size="20" />
</button>
</div>
<!-- 搜索框 -->
<div class="mb-4">
<div class="flex gap-2">
<input
v-model="addMemberSearch"
type="text"
placeholder="搜索用户名或邮箱..."
class="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none dark:border-dk-muted dark:bg-dk-base dark:text-dk-text"
@keyup.enter="searchUsersToAdd"
/>
<button
@click="searchUsersToAdd"
:disabled="addMemberSearchLoading"
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
>
{{ addMemberSearchLoading ? '搜索中...' : '搜索' }}
</button>
</div>
</div>
<!-- 搜索结果 -->
<div class="max-h-64 overflow-y-auto">
<div v-if="addMemberSearchLoading" class="py-4 text-center text-gray-500 dark:text-dk-subtle">
搜索中...
</div>
<div v-else-if="addMemberSearchResults.length === 0 && addMemberSearch" class="py-4 text-center text-gray-500 dark:text-dk-subtle">
未找到匹配的用户
</div>
<div v-else-if="!addMemberSearch" class="py-4 text-center text-gray-500 dark:text-dk-subtle">
输入关键词搜索用户
</div>
<div v-else class="space-y-2">
<div
v-for="user in addMemberSearchResults"
:key="user.id"
class="flex items-center justify-between rounded-md border border-gray-200 p-3 dark:border-dk-muted"
>
<div class="flex items-center gap-3">
<img
:src="usersStore.getAvatarUrlFromUserID(user.id)"
class="h-8 w-8 rounded-full object-cover"
alt="avatar"
/>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-dk-text">
{{ usersStore.getUsernameFromUserID(user.id) || user.name }}
</div>
<div class="text-xs text-gray-500 dark:text-dk-subtle">{{ user.email }}</div>
</div>
</div>
<button
@click="addGroupMember(user.id)"
:disabled="addMemberLoading"
class="rounded-md bg-green-600 px-3 py-1 text-xs font-medium text-white hover:bg-green-700 disabled:opacity-50"
>
添加
</button>
</div>
</div>
</div>
<div class="mt-4 flex justify-end">
<button
@click="closeAddMemberDialog"
class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-dk-muted dark:text-dk-text dark:hover:bg-dk-base"
>
关闭
</button>
</div>
</div>
</div>
<!-- 确认弹窗 -->
<ConfirmDialog
v-model="showConfirmDialog"
:title="confirmDialogConfig.title"
:message="confirmDialogConfig.message"
:confirm-text="confirmDialogConfig.confirmText"
:cancel-text="confirmDialogConfig.cancelText"
:danger="confirmDialogConfig.danger"
@confirm="handleConfirm"
/>
</template>