diff --git a/backend/app/http/super/dto/user.go b/backend/app/http/super/dto/user.go index 1b87139..3f1dbf4 100644 --- a/backend/app/http/super/dto/user.go +++ b/backend/app/http/super/dto/user.go @@ -1,24 +1,57 @@ package dto import ( + "strings" + "time" + "quyun/v2/app/requests" - "quyun/v2/database/models" "quyun/v2/pkg/consts" + + "go.ipao.vip/gen/types" ) type UserPageFilter struct { - requests.Pagination - requests.SortQueryFilter + requests.Pagination `json:",inline" query:",inline"` + requests.SortQueryFilter `json:",inline" query:",inline"` - Username *string `query:"username"` - Status *consts.UserStatus `query:"status"` - TenantID *int64 `query:"tenant_id"` + ID *int64 `json:"id,omitempty" query:"id"` + Username *string `json:"username,omitempty" query:"username"` + Status *consts.UserStatus `json:"status,omitempty" query:"status"` + + // TenantID filters users by membership in the given tenant. + TenantID *int64 `json:"tenant_id,omitempty" query:"tenant_id"` + + // Role filters users containing a role (user/super_admin). + Role *consts.Role `json:"role,omitempty" query:"role"` + + CreatedAtFrom *time.Time `json:"created_at_from,omitempty" query:"created_at_from"` + CreatedAtTo *time.Time `json:"created_at_to,omitempty" query:"created_at_to"` + VerifiedAtFrom *time.Time `json:"verified_at_from,omitempty" query:"verified_at_from"` + VerifiedAtTo *time.Time `json:"verified_at_to,omitempty" query:"verified_at_to"` +} + +func (f *UserPageFilter) UsernameTrimmed() string { + if f == nil || f.Username == nil { + return "" + } + return strings.TrimSpace(*f.Username) } type UserItem struct { - *models.User + ID int64 `json:"id"` + Username string `json:"username"` + Roles types.Array[consts.Role] `json:"roles"` + Status consts.UserStatus `json:"status"` + StatusDescription string `json:"status_description,omitempty"` - StatusDescription string `json:"status_description,omitempty"` + Balance int64 `json:"balance"` + BalanceFrozen int64 `json:"balance_frozen"` + VerifiedAt time.Time `json:"verified_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + + OwnedTenantCount int64 `json:"owned_tenant_count"` + JoinedTenantCount int64 `json:"joined_tenant_count"` } type UserStatusUpdateForm struct { diff --git a/backend/app/http/super/dto/user_tenant.go b/backend/app/http/super/dto/user_tenant.go new file mode 100644 index 0000000..9af44f5 --- /dev/null +++ b/backend/app/http/super/dto/user_tenant.go @@ -0,0 +1,60 @@ +package dto + +import ( + "strings" + "time" + + "quyun/v2/app/requests" + "quyun/v2/pkg/consts" + + "go.ipao.vip/gen/types" +) + +type UserTenantPageFilter struct { + requests.Pagination `json:",inline" query:",inline"` + + TenantID *int64 `json:"tenant_id,omitempty" query:"tenant_id"` + Code *string `json:"code,omitempty" query:"code"` + Name *string `json:"name,omitempty" query:"name"` + + // Role filters tenant_users.role containing a role (tenant_admin/member). + Role *consts.TenantUserRole `json:"role,omitempty" query:"role"` + // Status filters tenant_users.status. + Status *consts.UserStatus `json:"status,omitempty" query:"status"` + + CreatedAtFrom *time.Time `json:"created_at_from,omitempty" query:"created_at_from"` + CreatedAtTo *time.Time `json:"created_at_to,omitempty" query:"created_at_to"` +} + +func (f *UserTenantPageFilter) CodeTrimmed() string { + if f == nil || f.Code == nil { + return "" + } + return strings.ToLower(strings.TrimSpace(*f.Code)) +} + +func (f *UserTenantPageFilter) NameTrimmed() string { + if f == nil || f.Name == nil { + return "" + } + return strings.TrimSpace(*f.Name) +} + +type UserTenantItem struct { + TenantID int64 `json:"tenant_id"` + Code string `json:"code"` + Name string `json:"name"` + + TenantStatus consts.TenantStatus `json:"tenant_status"` + TenantStatusDescription string `json:"tenant_status_description,omitempty"` + ExpiredAt time.Time `json:"expired_at"` + + Owner *TenantOwnerUserLite `json:"owner,omitempty"` + + Role types.Array[consts.TenantUserRole] `json:"role"` + + MemberStatus consts.UserStatus `json:"member_status"` + MemberStatusDescription string `json:"member_status_description,omitempty"` + + JoinedAt time.Time `json:"joined_at"` +} diff --git a/backend/app/http/super/routes.gen.go b/backend/app/http/super/routes.gen.go index 470d049..c76ae20 100644 --- a/backend/app/http/super/routes.gen.go +++ b/backend/app/http/super/routes.gen.go @@ -99,6 +99,12 @@ func (r *Routes) Register(router fiber.Router) { r.user.list, Query[dto.UserPageFilter]("filter"), )) + r.log.Debugf("Registering route: Get /super/v1/users/:userID/tenants -> user.tenants") + router.Get("/super/v1/users/:userID/tenants"[len(r.Path()):], DataFunc2( + r.user.tenants, + PathParam[int64]("userID"), + Query[dto.UserTenantPageFilter]("filter"), + )) r.log.Debugf("Registering route: Get /super/v1/users/statistics -> user.statistics") router.Get("/super/v1/users/statistics"[len(r.Path()):], DataFunc0( r.user.statistics, diff --git a/backend/app/http/super/user.go b/backend/app/http/super/user.go index e54b3b6..3bff175 100644 --- a/backend/app/http/super/user.go +++ b/backend/app/http/super/user.go @@ -15,7 +15,7 @@ type user struct{} // list // -// @Summary 租户列表 +// @Summary 用户列表 // @Tags Super // @Accept json // @Produce json @@ -28,6 +28,23 @@ func (*user) list(ctx fiber.Ctx, filter *dto.UserPageFilter) (*requests.Pager, e return services.User.Page(ctx, filter) } +// tenants +// +// @Summary 用户加入的租户列表 +// @Tags Super +// @Accept json +// @Produce json +// @Param userID path int64 true "UserID" +// @Param filter query dto.UserTenantPageFilter true "Filter" +// @Success 200 {object} requests.Pager{items=dto.UserTenantItem} +// +// @Router /super/v1/users/:userID/tenants [get] +// @Bind userID path +// @Bind filter query +func (*user) tenants(ctx fiber.Ctx, userID int64, filter *dto.UserTenantPageFilter) (*requests.Pager, error) { + return services.User.TenantsPage(ctx, userID, filter) +} + // updateStatus // // @Summary 更新用户状态 diff --git a/backend/app/services/user.go b/backend/app/services/user.go index 984713c..22fac35 100644 --- a/backend/app/services/user.go +++ b/backend/app/services/user.go @@ -2,9 +2,11 @@ package services import ( "context" + "strings" "quyun/v2/app/http/super/dto" "quyun/v2/app/requests" + "quyun/v2/database" "quyun/v2/database/models" "quyun/v2/pkg/consts" @@ -12,6 +14,8 @@ import ( "github.com/samber/lo" "github.com/sirupsen/logrus" "go.ipao.vip/gen" + "go.ipao.vip/gen/field" + "go.ipao.vip/gen/types" ) // @provider @@ -59,9 +63,19 @@ func (t *user) SetStatus(ctx context.Context, userID int64, status consts.UserSt func (t *user) Page(ctx context.Context, filter *dto.UserPageFilter) (*requests.Pager, error) { tbl, query := models.UserQuery.QueryContext(ctx) + if filter == nil { + filter = &dto.UserPageFilter{} + } + conds := []gen.Condition{} - if filter.Username != nil { - conds = append(conds, tbl.Username.Like("%"+*filter.Username+"%")) + if filter.ID != nil && *filter.ID > 0 { + conds = append(conds, tbl.ID.Eq(*filter.ID)) + } + if username := filter.UsernameTrimmed(); username != "" { + conds = append(conds, tbl.Username.Like(database.WrapLike(username))) + } + if filter.Role != nil && *filter.Role != "" { + conds = append(conds, tbl.Roles.Contains(types.NewArray([]consts.Role{*filter.Role}))) } if filter.TenantID != nil { @@ -75,15 +89,103 @@ func (t *user) Page(ctx context.Context, filter *dto.UserPageFilter) (*requests. } filter.Pagination.Format() - users, total, err := query.Where(conds...).Order(tbl.ID.Desc()).FindByPage(int(filter.Offset()), int(filter.Limit)) + + if filter.CreatedAtFrom != nil { + conds = append(conds, tbl.CreatedAt.Gte(*filter.CreatedAtFrom)) + } + if filter.CreatedAtTo != nil { + conds = append(conds, tbl.CreatedAt.Lte(*filter.CreatedAtTo)) + } + if filter.VerifiedAtFrom != nil { + conds = append(conds, tbl.VerifiedAt.Gte(*filter.VerifiedAtFrom)) + } + if filter.VerifiedAtTo != nil { + conds = append(conds, tbl.VerifiedAt.Lte(*filter.VerifiedAtTo)) + } + + // 排序白名单:避免把任意字段拼进 SQL 导致注入或慢查询。 + orderBys := make([]field.Expr, 0, 6) + allowedAsc := map[string]field.Expr{ + "id": tbl.ID.Asc(), + "username": tbl.Username.Asc(), + "status": tbl.Status.Asc(), + "balance": tbl.Balance.Asc(), + "verified_at": tbl.VerifiedAt.Asc(), + "created_at": tbl.CreatedAt.Asc(), + "updated_at": tbl.UpdatedAt.Asc(), + } + allowedDesc := map[string]field.Expr{ + "id": tbl.ID.Desc(), + "username": tbl.Username.Desc(), + "status": tbl.Status.Desc(), + "balance": tbl.Balance.Desc(), + "verified_at": tbl.VerifiedAt.Desc(), + "created_at": tbl.CreatedAt.Desc(), + "updated_at": tbl.UpdatedAt.Desc(), + } + for _, f := range filter.AscFields() { + f = strings.TrimSpace(f) + if f == "" { + continue + } + if ob, ok := allowedAsc[f]; ok { + orderBys = append(orderBys, ob) + } + } + for _, f := range filter.DescFields() { + f = strings.TrimSpace(f) + if f == "" { + continue + } + if ob, ok := allowedDesc[f]; ok { + orderBys = append(orderBys, ob) + } + } + if len(orderBys) == 0 { + orderBys = append(orderBys, tbl.ID.Desc()) + } else { + orderBys = append(orderBys, tbl.ID.Desc()) + } + + users, total, err := query.Where(conds...).Order(orderBys...).FindByPage(int(filter.Offset()), int(filter.Limit)) + if err != nil { + return nil, err + } + + userIDs := make([]int64, 0, len(users)) + for _, u := range users { + if u == nil { + continue + } + userIDs = append(userIDs, u.ID) + } + + ownedTenantCounts, err := t.UserOwnedTenantCountMapping(ctx, userIDs) + if err != nil { + return nil, err + } + joinedTenantCounts, err := t.UserJoinedTenantCountMapping(ctx, userIDs) if err != nil { return nil, err } items := lo.Map(users, func(model *models.User, _ int) *dto.UserItem { + if model == nil { + return &dto.UserItem{} + } return &dto.UserItem{ - User: model, + ID: model.ID, + Username: model.Username, + Roles: model.Roles, + Status: model.Status, StatusDescription: model.Status.Description(), + Balance: model.Balance, + BalanceFrozen: model.BalanceFrozen, + VerifiedAt: model.VerifiedAt, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + OwnedTenantCount: ownedTenantCounts[model.ID], + JoinedTenantCount: joinedTenantCounts[model.ID], } }) @@ -94,6 +196,68 @@ func (t *user) Page(ctx context.Context, filter *dto.UserPageFilter) (*requests. }, nil } +func (t *user) UserOwnedTenantCountMapping(ctx context.Context, userIDs []int64) (map[int64]int64, error) { + result := make(map[int64]int64, len(userIDs)) + for _, id := range userIDs { + if id <= 0 { + continue + } + result[id] = 0 + } + if len(result) == 0 { + return result, nil + } + + ttbl, tquery := models.TenantQuery.QueryContext(ctx) + var rows []struct { + UserID int64 + Count int64 + } + err := tquery. + Select(ttbl.UserID, ttbl.ID.Count().As("count")). + Where(ttbl.UserID.In(userIDs...)). + Group(ttbl.UserID). + Scan(&rows) + if err != nil { + return nil, err + } + for _, row := range rows { + result[row.UserID] = row.Count + } + return result, nil +} + +func (t *user) UserJoinedTenantCountMapping(ctx context.Context, userIDs []int64) (map[int64]int64, error) { + result := make(map[int64]int64, len(userIDs)) + for _, id := range userIDs { + if id <= 0 { + continue + } + result[id] = 0 + } + if len(result) == 0 { + return result, nil + } + + tutbl, tuquery := models.TenantUserQuery.QueryContext(ctx) + var rows []struct { + UserID int64 + Count int64 + } + err := tuquery. + Select(tutbl.UserID, tutbl.TenantID.Count().As("count")). + Where(tutbl.UserID.In(userIDs...)). + Group(tutbl.UserID). + Scan(&rows) + if err != nil { + return nil, err + } + for _, row := range rows { + result[row.UserID] = row.Count + } + return result, nil +} + // UpdateStatus 更新用户状态(超级管理员侧)。 func (t *user) UpdateStatus(ctx context.Context, userID int64, status consts.UserStatus) error { logrus.WithField("user_id", userID).WithField("status", status).Info("update user status") @@ -127,3 +291,114 @@ func (t *user) Statistics(ctx context.Context) ([]*dto.UserStatistics, error) { return item }), nil } + +// TenantsPage 分页查询“用户加入的租户”(通过 tenant_users 关联)。 +func (t *user) TenantsPage(ctx context.Context, userID int64, filter *dto.UserTenantPageFilter) (*requests.Pager, error) { + if userID <= 0 { + return nil, errors.New("user_id must be > 0") + } + if filter == nil { + filter = &dto.UserTenantPageFilter{} + } + + filter.Pagination.Format() + + tuTbl, query := models.TenantUserQuery.QueryContext(ctx) + conds := []gen.Condition{tuTbl.UserID.Eq(userID)} + + if filter.TenantID != nil && *filter.TenantID > 0 { + conds = append(conds, tuTbl.TenantID.Eq(*filter.TenantID)) + } + if filter.Role != nil && *filter.Role != "" { + conds = append(conds, tuTbl.Role.Contains(types.NewArray([]consts.TenantUserRole{*filter.Role}))) + } + if filter.Status != nil && *filter.Status != "" { + conds = append(conds, tuTbl.Status.Eq(*filter.Status)) + } + if filter.CreatedAtFrom != nil { + conds = append(conds, tuTbl.CreatedAt.Gte(*filter.CreatedAtFrom)) + } + if filter.CreatedAtTo != nil { + conds = append(conds, tuTbl.CreatedAt.Lte(*filter.CreatedAtTo)) + } + + code := filter.CodeTrimmed() + name := filter.NameTrimmed() + if code != "" || name != "" { + tTbl, _ := models.TenantQuery.QueryContext(ctx) + query = query.LeftJoin(tTbl, tTbl.ID.EqCol(tuTbl.TenantID)) + if code != "" { + conds = append(conds, tTbl.Code.Like(database.WrapLike(code))) + } + if name != "" { + conds = append(conds, tTbl.Name.Like(database.WrapLike(name))) + } + } + + rows, total, err := query.Where(conds...).Order(tuTbl.ID.Desc()).FindByPage(int(filter.Offset()), int(filter.Limit)) + if err != nil { + return nil, err + } + + tenantIDs := make([]int64, 0, len(rows)) + for _, tu := range rows { + if tu == nil { + continue + } + tenantIDs = append(tenantIDs, tu.TenantID) + } + tenantIDs = lo.Uniq(tenantIDs) + + tenants := make(map[int64]*models.Tenant, len(tenantIDs)) + tenantList := make([]*models.Tenant, 0, len(tenantIDs)) + if len(tenantIDs) > 0 { + tTbl, tQuery := models.TenantQuery.QueryContext(ctx) + ts, err := tQuery.Where(tTbl.ID.In(tenantIDs...)).Find() + if err != nil { + return nil, err + } + for _, te := range ts { + if te == nil { + continue + } + tenants[te.ID] = te + tenantList = append(tenantList, te) + } + } + + ownerMap, err := Tenant.TenantOwnerUserMapping(ctx, tenantList) + if err != nil { + return nil, err + } + + items := make([]*dto.UserTenantItem, 0, len(rows)) + for _, tu := range rows { + if tu == nil { + continue + } + te := tenants[tu.TenantID] + if te == nil { + continue + } + + items = append(items, &dto.UserTenantItem{ + TenantID: te.ID, + Code: te.Code, + Name: te.Name, + TenantStatus: te.Status, + TenantStatusDescription: te.Status.Description(), + ExpiredAt: te.ExpiredAt, + Owner: ownerMap[te.ID], + Role: tu.Role, + MemberStatus: tu.Status, + MemberStatusDescription: tu.Status.Description(), + JoinedAt: tu.CreatedAt, + }) + } + + return &requests.Pager{ + Pagination: filter.Pagination, + Total: total, + Items: items, + }, nil +} diff --git a/frontend/superadmin/dist/index.html b/frontend/superadmin/dist/index.html index 0ab5f2b..b26c0ab 100644 --- a/frontend/superadmin/dist/index.html +++ b/frontend/superadmin/dist/index.html @@ -7,8 +7,8 @@ Sakai Vue - - + + diff --git a/frontend/superadmin/src/service/UserService.js b/frontend/superadmin/src/service/UserService.js index 5e21af9..7a774b4 100644 --- a/frontend/superadmin/src/service/UserService.js +++ b/frontend/superadmin/src/service/UserService.js @@ -7,8 +7,41 @@ function normalizeItems(items) { } export const UserService = { - async listUsers({ page, limit, tenantID, username, status, sortField, sortOrder } = {}) { - const query = { page, limit, tenantID, username, status }; + async listUsers({ + page, + limit, + id, + tenant_id, + username, + status, + role, + created_at_from, + created_at_to, + verified_at_from, + verified_at_to, + sortField, + sortOrder + } = {}) { + const iso = (d) => { + if (!d) return undefined; + const date = d instanceof Date ? d : new Date(d); + if (Number.isNaN(date.getTime())) return undefined; + return date.toISOString(); + }; + + const query = { + page, + limit, + id, + tenant_id, + username, + status, + role, + created_at_from: iso(created_at_from), + created_at_to: iso(created_at_to), + verified_at_from: iso(verified_at_from), + verified_at_to: iso(verified_at_to) + }; if (sortField && sortOrder) { if (sortOrder === 1) query.asc = sortField; if (sortOrder === -1) query.desc = sortField; @@ -55,5 +88,38 @@ export const UserService = { } throw error; } + }, + async listUserTenants( + userID, + { page, limit, tenant_id, code, name, role, status, created_at_from, created_at_to } = {} + ) { + if (!userID) throw new Error('userID is required'); + + const iso = (d) => { + if (!d) return undefined; + const date = d instanceof Date ? d : new Date(d); + if (Number.isNaN(date.getTime())) return undefined; + return date.toISOString(); + }; + + const query = { + page, + limit, + tenant_id, + code, + name, + role, + status, + created_at_from: iso(created_at_from), + created_at_to: iso(created_at_to) + }; + + const data = await requestJson(`/super/v1/users/${userID}/tenants`, { query }); + return { + page: data?.page ?? page ?? 1, + limit: data?.limit ?? limit ?? 10, + total: data?.total ?? 0, + items: normalizeItems(data?.items) + }; } }; diff --git a/frontend/superadmin/src/views/superadmin/Users.vue b/frontend/superadmin/src/views/superadmin/Users.vue index 4856c11..7f0884d 100644 --- a/frontend/superadmin/src/views/superadmin/Users.vue +++ b/frontend/superadmin/src/views/superadmin/Users.vue @@ -2,6 +2,7 @@ import SearchField from '@/components/SearchField.vue'; import SearchPanel from '@/components/SearchPanel.vue'; import StatisticsStrip from '@/components/StatisticsStrip.vue'; +import { TenantService } from '@/service/TenantService'; import { UserService } from '@/service/UserService'; import { useToast } from 'primevue/usetoast'; import { computed, onMounted, ref } from 'vue'; @@ -15,8 +16,15 @@ const totalRecords = ref(0); const page = ref(1); const rows = ref(10); +const userID = ref(null); +const tenantID = ref(null); const username = ref(''); const status = ref(''); +const role = ref(''); +const createdAtFrom = ref(null); +const createdAtTo = ref(null); +const verifiedAtFrom = ref(null); +const verifiedAtTo = ref(null); const sortField = ref('id'); const sortOrder = ref(-1); @@ -28,6 +36,12 @@ function formatDate(value) { return date.toLocaleString(); } +function formatCny(amountInCents) { + const amount = Number(amountInCents) / 100; + if (!Number.isFinite(amount)) return '-'; + return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount); +} + function getStatusSeverity(status) { switch (status) { case 'active': @@ -51,6 +65,31 @@ const statusOptions = ref([]); const statusUser = ref(null); const statusValue = ref(null); +const statusFilterOptions = computed(() => [{ label: '全部', value: '' }, ...(statusOptions.value || [])]); + +const ownedTenantsDialogVisible = ref(false); +const ownedTenantsLoading = ref(false); +const ownedTenantsUser = ref(null); +const ownedTenants = ref([]); +const ownedTenantsTotal = ref(0); +const ownedTenantsPage = ref(1); +const ownedTenantsRows = ref(10); + +const joinedTenantsDialogVisible = ref(false); +const joinedTenantsLoading = ref(false); +const joinedTenantsUser = ref(null); +const joinedTenants = ref([]); +const joinedTenantsTotal = ref(0); +const joinedTenantsPage = ref(1); +const joinedTenantsRows = ref(10); +const joinedTenantsTenantID = ref(null); +const joinedTenantsCode = ref(''); +const joinedTenantsName = ref(''); +const joinedTenantsRole = ref(''); +const joinedTenantsStatus = ref(''); +const joinedTenantsJoinedAtFrom = ref(null); +const joinedTenantsJoinedAtTo = ref(null); + const statistics = ref([]); const statisticsLoading = ref(false); @@ -154,8 +193,15 @@ async function loadUsers() { const result = await UserService.listUsers({ page: page.value, limit: rows.value, + id: userID.value || undefined, + tenant_id: tenantID.value || undefined, username: username.value, status: status.value, + role: role.value || undefined, + created_at_from: createdAtFrom.value || undefined, + created_at_to: createdAtTo.value || undefined, + verified_at_from: verifiedAtFrom.value || undefined, + verified_at_to: verifiedAtTo.value || undefined, sortField: sortField.value, sortOrder: sortOrder.value }); @@ -179,8 +225,15 @@ function onSearch() { } function onReset() { + userID.value = null; + tenantID.value = null; username.value = ''; status.value = ''; + role.value = ''; + createdAtFrom.value = null; + createdAtTo.value = null; + verifiedAtFrom.value = null; + verifiedAtTo.value = null; sortField.value = 'id'; sortOrder.value = -1; page.value = 1; @@ -200,6 +253,107 @@ function onSort(event) { loadUsers(); } +function openOwnedTenantsDialog(user) { + ownedTenantsUser.value = user; + ownedTenantsDialogVisible.value = true; + ownedTenantsPage.value = 1; + ownedTenantsRows.value = 10; + loadOwnedTenants(); +} + +function openJoinedTenantsDialog(user) { + joinedTenantsUser.value = user; + joinedTenantsDialogVisible.value = true; + joinedTenantsPage.value = 1; + joinedTenantsRows.value = 10; + joinedTenantsTenantID.value = null; + joinedTenantsCode.value = ''; + joinedTenantsName.value = ''; + joinedTenantsRole.value = ''; + joinedTenantsStatus.value = ''; + joinedTenantsJoinedAtFrom.value = null; + joinedTenantsJoinedAtTo.value = null; + loadJoinedTenants(); +} + +async function loadOwnedTenants() { + const uid = ownedTenantsUser.value?.id; + if (!uid) return; + + ownedTenantsLoading.value = true; + try { + const result = await TenantService.listTenants({ + page: ownedTenantsPage.value, + limit: ownedTenantsRows.value, + user_id: uid, + sortField: 'id', + sortOrder: -1 + }); + ownedTenants.value = result.items; + ownedTenantsTotal.value = result.total; + } catch (error) { + toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载该用户拥有的租户', life: 4000 }); + } finally { + ownedTenantsLoading.value = false; + } +} + +function onOwnedTenantsPage(event) { + ownedTenantsPage.value = (event.page ?? 0) + 1; + ownedTenantsRows.value = event.rows ?? ownedTenantsRows.value; + loadOwnedTenants(); +} + +async function loadJoinedTenants() { + const uid = joinedTenantsUser.value?.id; + if (!uid) return; + + joinedTenantsLoading.value = true; + try { + const result = await UserService.listUserTenants(uid, { + page: joinedTenantsPage.value, + limit: joinedTenantsRows.value, + tenant_id: joinedTenantsTenantID.value || undefined, + code: joinedTenantsCode.value, + name: joinedTenantsName.value, + role: joinedTenantsRole.value || undefined, + status: joinedTenantsStatus.value || undefined, + created_at_from: joinedTenantsJoinedAtFrom.value || undefined, + created_at_to: joinedTenantsJoinedAtTo.value || undefined + }); + joinedTenants.value = result.items; + joinedTenantsTotal.value = result.total; + } catch (error) { + toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载该用户加入的租户', life: 4000 }); + } finally { + joinedTenantsLoading.value = false; + } +} + +function onJoinedTenantsSearch() { + joinedTenantsPage.value = 1; + loadJoinedTenants(); +} + +function onJoinedTenantsReset() { + joinedTenantsTenantID.value = null; + joinedTenantsCode.value = ''; + joinedTenantsName.value = ''; + joinedTenantsRole.value = ''; + joinedTenantsStatus.value = ''; + joinedTenantsJoinedAtFrom.value = null; + joinedTenantsJoinedAtTo.value = null; + joinedTenantsPage.value = 1; + joinedTenantsRows.value = 10; + loadJoinedTenants(); +} + +function onJoinedTenantsPage(event) { + joinedTenantsPage.value = (event.page ?? 0) + 1; + joinedTenantsRows.value = event.rows ?? joinedTenantsRows.value; + loadJoinedTenants(); +} + onMounted(() => { loadUsers(); loadStatistics(); @@ -216,6 +370,12 @@ onMounted(() => { + + + + + + @@ -225,7 +385,33 @@ onMounted(() => { - + + + + + +