diff --git a/contracts/admin-openapi.json b/contracts/admin-openapi.json index c1bf04f..1673ecc 100644 --- a/contracts/admin-openapi.json +++ b/contracts/admin-openapi.json @@ -1,3277 +1,3010 @@ { - "openapi": "3.1.0", - "info": { - "title": "HYApp Admin API", - "version": "0.1.0" - }, - "servers": [ - { - "url": "/api/v1" - } - ], - "paths": { - "/admin/agencies": { - "get": { - "operationId": "listAgencies", - "parameters": [ - { - "$ref": "#/components/parameters/Page" - }, - { - "$ref": "#/components/parameters/PageSize" - }, - { - "$ref": "#/components/parameters/Keyword" - }, - { - "$ref": "#/components/parameters/Status" - }, - { - "$ref": "#/components/parameters/RegionId" - }, - { - "$ref": "#/components/parameters/ParentBdUserId" - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/AgencyPageResponse" - } - }, - "x-permission": "agency:view", - "x-permissions": [ - "agency:view" - ] - }, - "post": { - "operationId": "createAgency", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "agency:create", - "x-permissions": [ - "agency:create" - ] - } + "openapi": "3.1.0", + "info": { + "title": "HYApp Admin API", + "version": "0.1.0" }, - "/admin/agencies/{agency_id}/close": { - "post": { - "operationId": "closeAgency", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "agency_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "agency:status", - "x-permissions": [ - "agency:status" - ] - } - }, - "/admin/agencies/{agency_id}/join-enabled": { - "post": { - "operationId": "setAgencyJoinEnabled", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "agency_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "agency:status", - "x-permissions": [ - "agency:status" - ] - } - }, - "/admin/app-config/banners": { - "get": { - "operationId": "listBanners", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "app-config:view", - "x-permissions": [ - "app-config:view" - ] - }, - "post": { - "operationId": "createBanner", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "app-config:update", - "x-permissions": [ - "app-config:update" - ] - } - }, - "/admin/app-config/banners/{banner_id}": { - "put": { - "operationId": "updateBanner", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "banner_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "app-config:update", - "x-permissions": [ - "app-config:update" - ] - }, - "delete": { - "operationId": "deleteBanner", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "banner_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "app-config:update", - "x-permissions": [ - "app-config:update" - ] - } - }, - "/admin/app-config/h5-links": { - "get": { - "operationId": "listH5Links", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "app-config:view", - "x-permissions": [ - "app-config:view" - ] - }, - "put": { - "operationId": "updateH5Links", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "app-config:update", - "x-permissions": [ - "app-config:update" - ] - } - }, - "/admin/apps": { - "get": { - "operationId": "listApps", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } + "servers": [ + { + "url": "/api/v1" } - } - }, - "/admin/bd-leaders": { - "get": { - "operationId": "listBDLeaders", - "parameters": [ - { - "$ref": "#/components/parameters/Page" - }, - { - "$ref": "#/components/parameters/PageSize" - }, - { - "$ref": "#/components/parameters/Keyword" - }, - { - "$ref": "#/components/parameters/Status" - }, - { - "$ref": "#/components/parameters/RegionId" - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/BDProfilePageResponse" - } - }, - "x-permission": "bd:view", - "x-permissions": [ - "bd:view" - ] - }, - "post": { - "operationId": "createBDLeader", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "bd:create", - "x-permissions": [ - "bd:create" - ] - } - }, - "/admin/bd-leaders/{user_id}/status": { - "patch": { - "operationId": "setBDLeaderStatus", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "user_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "bd:update", - "x-permissions": [ - "bd:update" - ] - } - }, - "/admin/bds": { - "get": { - "operationId": "listBDs", - "parameters": [ - { - "$ref": "#/components/parameters/Page" - }, - { - "$ref": "#/components/parameters/PageSize" - }, - { - "$ref": "#/components/parameters/Keyword" - }, - { - "$ref": "#/components/parameters/Status" - }, - { - "$ref": "#/components/parameters/RegionId" - }, - { - "$ref": "#/components/parameters/ParentLeaderUserId" - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/BDProfilePageResponse" - } - }, - "x-permission": "bd:view", - "x-permissions": [ - "bd:view" - ] - }, - "post": { - "operationId": "createBD", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "bd:create", - "x-permissions": [ - "bd:create" - ] - } - }, - "/admin/bds/{user_id}/status": { - "patch": { - "operationId": "setBDStatus", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "user_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "bd:update", - "x-permissions": [ - "bd:update" - ] - } - }, - "/admin/coin-sellers": { - "get": { - "operationId": "listCoinSellers", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "coin-seller:view", - "x-permissions": [ - "coin-seller:view" - ] - }, - "post": { - "operationId": "createCoinSeller", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "coin-seller:create", - "x-permissions": [ - "coin-seller:create" - ] - } - }, - "/admin/coin-sellers/{user_id}/status": { - "patch": { - "operationId": "setCoinSellerStatus", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "user_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "coin-seller:update", - "x-permissions": [ - "coin-seller:update" - ] - } - }, - "/admin/countries": { - "get": { - "operationId": "listCountries", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "country:view", - "x-permissions": [ - "country:view" - ] - }, - "post": { - "operationId": "createCountry", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "country:create", - "x-permissions": [ - "country:create" - ] - } - }, - "/admin/countries/{country_id}": { - "get": { - "operationId": "getCountry", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "country_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "country:view", - "x-permissions": [ - "country:view" - ] - }, - "patch": { - "operationId": "updateCountry", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "country_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "country:update", - "x-permissions": [ - "country:update" - ] - }, - "delete": { - "operationId": "deleteCountry", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "country_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "country:status", - "x-permissions": [ - "country:status" - ] - } - }, - "/admin/countries/{country_id}/disable": { - "post": { - "operationId": "disableCountry", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "country_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "country:status", - "x-permissions": [ - "country:status" - ] - } - }, - "/admin/countries/{country_id}/enable": { - "post": { - "operationId": "enableCountry", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "country_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "country:status", - "x-permissions": [ - "country:status" - ] - } - }, - "/admin/files/image/upload": { - "post": { - "operationId": "uploadImage", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "upload:create", - "x-permissions": [ - "upload:create" - ] - } - }, - "/admin/files/upload": { - "post": { - "operationId": "uploadFile", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "upload:create", - "x-permissions": [ - "upload:create" - ] - } - }, - "/admin/gifts": { - "get": { - "operationId": "listGifts", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "gift:view", - "x-permissions": [ - "gift:view" - ] - }, - "post": { - "operationId": "createGift", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "gift:create", - "x-permissions": [ - "gift:create" - ] - } - }, - "/admin/gifts/{gift_id}": { - "put": { - "operationId": "updateGift", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "gift_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "gift:update", - "x-permissions": [ - "gift:update" - ] - } - }, - "/admin/gifts/{gift_id}/disable": { - "post": { - "operationId": "disableGift", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "gift_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "gift:status", - "x-permissions": [ - "gift:status" - ] - } - }, - "/admin/gifts/{gift_id}/enable": { - "post": { - "operationId": "enableGift", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "gift_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "gift:status", - "x-permissions": [ - "gift:status" - ] - } - }, - "/admin/hosts": { - "get": { - "operationId": "listHosts", - "parameters": [ - { - "$ref": "#/components/parameters/Page" - }, - { - "$ref": "#/components/parameters/PageSize" - }, - { - "$ref": "#/components/parameters/Keyword" - }, - { - "$ref": "#/components/parameters/Status" - }, - { - "$ref": "#/components/parameters/RegionId" - }, - { - "$ref": "#/components/parameters/AgencyId" - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/HostProfilePageResponse" - } - }, - "x-permission": "host:view", - "x-permissions": [ - "host:view" - ] - } - }, - "/admin/payment/recharge-bills": { - "get": { - "operationId": "listRechargeBills", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "payment-bill:view", - "x-permissions": [ - "payment-bill:view" - ] - } - }, - "/admin/regions": { - "get": { - "operationId": "listRegions", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "region:view", - "x-permissions": [ - "region:view" - ] - }, - "post": { - "operationId": "createRegion", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "region:create", - "x-permissions": [ - "region:create" - ] - } - }, - "/admin/regions/{region_id}": { - "get": { - "operationId": "getRegion", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "region_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "region:view", - "x-permissions": [ - "region:view" - ] - }, - "patch": { - "operationId": "updateRegion", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "region_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "region:update", - "x-permissions": [ - "region:update" - ] - }, - "delete": { - "operationId": "disableRegion", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "region_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "region:status", - "x-permissions": [ - "region:status" - ] - } - }, - "/admin/regions/{region_id}/countries": { - "put": { - "operationId": "replaceRegionCountries", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "region_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "region:update", - "x-permissions": [ - "region:update" - ] - } - }, - "/admin/regions/{region_id}/enable": { - "post": { - "operationId": "enableRegion", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "region_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "region:status", - "x-permissions": [ - "region:status" - ] - } - }, - "/admin/resource-grants": { - "get": { - "operationId": "listResourceGrants", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "resource-grant:view", - "x-permissions": [ - "resource-grant:view" - ] - } - }, - "/admin/resource-grants/group": { - "post": { - "operationId": "grantResourceGroup", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "resource-grant:create", - "x-permissions": [ - "resource-grant:create" - ] - } - }, - "/admin/resource-grants/resource": { - "post": { - "operationId": "grantResource", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "resource-grant:create", - "x-permissions": [ - "resource-grant:create" - ] - } - }, - "/admin/resource-groups": { - "get": { - "operationId": "listResourceGroups", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "resource-group:view", - "x-permissions": [ - "resource-group:view" - ] - }, - "post": { - "operationId": "createResourceGroup", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "resource-group:create", - "x-permissions": [ - "resource-group:create" - ] - } - }, - "/admin/resource-groups/{group_id}": { - "get": { - "operationId": "getResourceGroup", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "group_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "resource-group:view", - "x-permissions": [ - "resource-group:view" - ] - }, - "put": { - "operationId": "updateResourceGroup", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "group_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "resource-group:update", - "x-permissions": [ - "resource-group:update" - ] - } - }, - "/admin/resource-groups/{group_id}/disable": { - "post": { - "operationId": "disableResourceGroup", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "group_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "resource-group:update", - "x-permissions": [ - "resource-group:update" - ] - } - }, - "/admin/resource-groups/{group_id}/enable": { - "post": { - "operationId": "enableResourceGroup", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "group_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "resource-group:update", - "x-permissions": [ - "resource-group:update" - ] - } - }, - "/admin/resource-groups/{group_id}/items": { - "put": { - "operationId": "updateResourceGroupItems", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "group_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "resource-group:update", - "x-permissions": [ - "resource-group:update" - ] - } - }, - "/admin/resources": { - "get": { - "operationId": "listResources", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "resource:view", - "x-permissions": [ - "resource:view" - ] - }, - "post": { - "operationId": "createResource", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "resource:create", - "x-permissions": [ - "resource:create" - ] - } - }, - "/admin/resources/{resource_id}": { - "get": { - "operationId": "getResource", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "resource_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "resource:view", - "x-permissions": [ - "resource:view" - ] - }, - "put": { - "operationId": "updateResource", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "resource_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "resource:update", - "x-permissions": [ - "resource:update" - ] - } - }, - "/admin/resources/{resource_id}/disable": { - "post": { - "operationId": "disableResource", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "resource_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "resource:update", - "x-permissions": [ - "resource:update" - ] - } - }, - "/admin/resources/{resource_id}/enable": { - "post": { - "operationId": "enableResource", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "resource_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "resource:update", - "x-permissions": [ - "resource:update" - ] - } - }, - "/admin/rooms": { - "get": { - "operationId": "listRooms", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "room:view", - "x-permissions": [ - "room:view" - ] - } - }, - "/admin/rooms/{room_id}": { - "patch": { - "operationId": "updateRoom", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "room_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "room:update", - "x-permissions": [ - "room:update" - ] - }, - "delete": { - "operationId": "deleteRoom", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "in": "path", - "name": "room_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "x-permission": "room:delete", - "x-permissions": [ - "room:delete" - ] - } - }, - "/app/users": { - "get": { - "operationId": "appListUsers", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "app-user:view", - "x-permissions": [ - "app-user:view" - ] - } - }, - "/app/users/{id}": { - "get": { - "operationId": "appGetUser", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "x-permission": "app-user:view", - "x-permissions": [ - "app-user:view" - ] - }, - "patch": { - "operationId": "appUpdateUser", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "x-permission": "app-user:update", - "x-permissions": [ - "app-user:update" - ] - } - }, - "/app/users/{id}/ban": { - "post": { - "operationId": "appBanUser", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "x-permission": "app-user:status", - "x-permissions": [ - "app-user:status" - ] - } - }, - "/app/users/{id}/password": { - "post": { - "operationId": "appSetPassword", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "x-permission": "app-user:password", - "x-permissions": [ - "app-user:password" - ] - } - }, - "/app/users/{id}/unban": { - "post": { - "operationId": "appUnbanUser", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "x-permission": "app-user:status", - "x-permissions": [ - "app-user:status" - ] - } - }, - "/auth/change-password": { - "post": { - "operationId": "changePassword", - "requestBody": { - "$ref": "#/components/requestBodies/ChangePasswordRequest" - }, - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - } - } - }, - "/auth/login": { - "post": { - "operationId": "login", - "requestBody": { - "$ref": "#/components/requestBodies/LoginRequest" - }, - "responses": { - "200": { - "$ref": "#/components/responses/SessionResponse" - } - } - } - }, - "/auth/logout": { - "post": { - "operationId": "logout", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - } - } - }, - "/auth/me": { - "get": { - "operationId": "me", - "responses": { - "200": { - "$ref": "#/components/responses/SessionResponse" - } - } - } - }, - "/auth/refresh": { - "post": { - "operationId": "refresh", - "responses": { - "200": { - "$ref": "#/components/responses/SessionResponse" - } - } - } - }, - "/dashboard/overview": { - "get": { - "operationId": "dashboardOverview", - "x-permission": "overview:view", - "responses": { - "200": { - "$ref": "#/components/responses/DashboardOverviewResponse" - } - }, - "x-permissions": [ - "overview:view" - ] - } - }, - "/exports/users": { - "post": { - "operationId": "createUserExportJob", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "export:create", - "x-permissions": [ - "export:create" - ] - } - }, - "/jobs": { - "get": { - "operationId": "listJobs", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permission": "job:view", - "x-permissions": [ - "job:view" - ] - } - }, - "/jobs/{id}/artifact": { - "get": { - "operationId": "downloadJobArtifact", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "x-permission": "job:view", - "x-permissions": [ - "job:view" - ] - } - }, - "/jobs/{id}/cancel": { - "post": { - "operationId": "cancelJob", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "x-permission": "job:cancel", - "x-permissions": [ - "job:cancel" - ] - } - }, - "/logs/login": { - "get": { - "operationId": "listLoginLogs", - "x-permission": "log:view", - "parameters": [ - { - "$ref": "#/components/parameters/Page" - }, - { - "$ref": "#/components/parameters/PageSize" - }, - { - "$ref": "#/components/parameters/Keyword" - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/LogPageResponse" - } - }, - "x-permissions": [ - "log:view" - ] - } - }, - "/logs/login/export": { - "get": { - "operationId": "exportLoginLogs", - "x-permission": "log:export", - "responses": { - "200": { - "description": "CSV" - } - }, - "x-permissions": [ - "log:export" - ] - } - }, - "/logs/operations": { - "get": { - "operationId": "listOperationLogs", - "x-permission": "log:view", - "parameters": [ - { - "$ref": "#/components/parameters/Page" - }, - { - "$ref": "#/components/parameters/PageSize" - }, - { - "$ref": "#/components/parameters/Keyword" - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/LogPageResponse" - } - }, - "x-permissions": [ - "log:view" - ] - } - }, - "/logs/operations/export": { - "get": { - "operationId": "exportOperationLogs", - "x-permission": "log:export", - "responses": { - "200": { - "description": "CSV" - } - }, - "x-permissions": [ - "log:export" - ] - } - }, - "/navigation/menus": { - "get": { - "operationId": "navigationMenus", - "responses": { - "200": { - "$ref": "#/components/responses/MenuListResponse" - } - } - } - }, - "/notifications": { - "get": { - "operationId": "listNotifications", - "x-permission": "notification:view", - "responses": { - "200": { - "$ref": "#/components/responses/NotificationListResponse" - } - }, - "x-permissions": [ - "notification:view" - ] - } - }, - "/notifications/{id}": { - "delete": { - "operationId": "deleteNotification", - "x-permission": "notification:delete", - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permissions": [ - "notification:delete" - ] - } - }, - "/notifications/{id}/read": { - "patch": { - "operationId": "markNotificationRead", - "x-permission": "notification:read", - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/NotificationResponse" - } - }, - "x-permissions": [ - "notification:read" - ] - } - }, - "/notifications/read-all": { - "patch": { - "operationId": "markAllNotificationsRead", - "x-permission": "notification:read-all", - "responses": { - "200": { - "$ref": "#/components/responses/NotificationListResponse" - } - }, - "x-permissions": [ - "notification:read-all" - ] - } - }, - "/permissions": { - "get": { - "operationId": "listPermissions", - "x-permission": "permission:view", - "responses": { - "200": { - "$ref": "#/components/responses/PermissionListResponse" - } - }, - "x-permissions": [ - "permission:view", - "role:permission", - "role:manage" - ] - }, - "post": { - "operationId": "createPermission", - "x-permission": "permission:create", - "requestBody": { - "$ref": "#/components/requestBodies/PermissionRequest" - }, - "responses": { - "200": { - "$ref": "#/components/responses/PermissionResponse" - } - }, - "x-permissions": [ - "permission:create" - ] - } - }, - "/permissions/{id}": { - "patch": { - "operationId": "updatePermission", - "x-permission": "permission:update", - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "requestBody": { - "$ref": "#/components/requestBodies/PermissionRequest" - }, - "responses": { - "200": { - "$ref": "#/components/responses/PermissionResponse" - } - }, - "x-permissions": [ - "permission:update" - ] - }, - "delete": { - "operationId": "deletePermission", - "x-permission": "permission:delete", - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permissions": [ - "permission:delete" - ] - } - }, - "/permissions/sync": { - "post": { - "operationId": "syncPermissions", - "x-permission": "permission:sync", - "responses": { - "200": { - "$ref": "#/components/responses/PermissionListResponse" - } - }, - "x-permissions": [ - "permission:sync" - ] - } - }, - "/roles": { - "get": { - "operationId": "listRoles", - "x-permission": "role:view", - "responses": { - "200": { - "$ref": "#/components/responses/RoleListResponse" - } - }, - "x-permissions": [ - "role:view" - ] - }, - "post": { - "operationId": "createRole", - "x-permission": "role:create", - "requestBody": { - "$ref": "#/components/requestBodies/RoleRequest" - }, - "responses": { - "200": { - "$ref": "#/components/responses/RoleResponse" - } - }, - "x-permissions": [ - "role:create", - "role:manage" - ] - } - }, - "/roles/{id}": { - "patch": { - "operationId": "updateRole", - "x-permission": "role:update", - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "requestBody": { - "$ref": "#/components/requestBodies/RoleRequest" - }, - "responses": { - "200": { - "$ref": "#/components/responses/RoleResponse" - } - }, - "x-permissions": [ - "role:update", - "role:manage" - ] - }, - "delete": { - "operationId": "deleteRole", - "x-permission": "role:delete", - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permissions": [ - "role:delete", - "role:manage" - ] - } - }, - "/roles/{id}/data-scopes": { - "get": { - "operationId": "getRoleDataScopes", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "x-permission": "role:data-scope", - "x-permissions": [ - "role:data-scope" - ] - }, - "put": { - "operationId": "replaceRoleDataScopes", - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "x-permission": "role:data-scope", - "x-permissions": [ - "role:data-scope" - ] - } - }, - "/roles/{id}/permissions": { - "put": { - "operationId": "replaceRolePermissions", - "x-permission": "role:permission", - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "requestBody": { - "$ref": "#/components/requestBodies/RolePermissionRequest" - }, - "responses": { - "200": { - "$ref": "#/components/responses/RoleResponse" - } - }, - "x-permissions": [ - "role:permission", - "role:manage" - ] - } - }, - "/search": { - "get": { - "operationId": "search", - "parameters": [ - { - "$ref": "#/components/parameters/Keyword" - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/SearchResponse" - } - } - } - }, - "/system/menus": { - "get": { - "operationId": "listSystemMenus", - "x-permission": "menu:view", - "responses": { - "200": { - "$ref": "#/components/responses/MenuListResponse" - } - }, - "x-permissions": [ - "menu:view" - ] - }, - "post": { - "operationId": "createMenu", - "x-permission": "menu:create", - "requestBody": { - "$ref": "#/components/requestBodies/MenuRequest" - }, - "responses": { - "200": { - "$ref": "#/components/responses/MenuResponse" - } - }, - "x-permissions": [ - "menu:create" - ] - } - }, - "/system/menus/{id}": { - "patch": { - "operationId": "updateMenu", - "x-permission": "menu:update", - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "requestBody": { - "$ref": "#/components/requestBodies/MenuRequest" - }, - "responses": { - "200": { - "$ref": "#/components/responses/MenuResponse" - } - }, - "x-permissions": [ - "menu:update" - ] - }, - "delete": { - "operationId": "deleteMenu", - "x-permission": "menu:delete", - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permissions": [ - "menu:delete" - ] - } - }, - "/system/menus/{id}/visible": { - "patch": { - "operationId": "updateMenuVisible", - "x-permission": "menu:visible", - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "requestBody": { - "$ref": "#/components/requestBodies/MenuVisibleRequest" - }, - "responses": { - "200": { - "$ref": "#/components/responses/MenuResponse" - } - }, - "x-permissions": [ - "menu:visible" - ] - } - }, - "/system/menus/sort": { - "put": { - "operationId": "sortMenus", - "x-permission": "menu:sort", - "requestBody": { - "$ref": "#/components/requestBodies/MenuSortRequest" - }, - "responses": { - "200": { - "$ref": "#/components/responses/MenuListResponse" - } - }, - "x-permissions": [ - "menu:sort" - ] - } - }, - "/users": { - "get": { - "operationId": "listUsers", - "x-permission": "user:view", - "parameters": [ - { - "$ref": "#/components/parameters/Page" - }, - { - "$ref": "#/components/parameters/PageSize" - }, - { - "$ref": "#/components/parameters/Keyword" - }, - { - "name": "role", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "$ref": "#/components/parameters/Status" - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/UserPageResponse" - } - }, - "x-permissions": [ - "user:view" - ] - }, - "post": { - "operationId": "createUser", - "x-permission": "user:create", - "requestBody": { - "$ref": "#/components/requestBodies/UserRequest" - }, - "responses": { - "200": { - "$ref": "#/components/responses/CreateUserResponse" - } - }, - "x-permissions": [ - "user:create" - ] - } - }, - "/users/{id}": { - "get": { - "operationId": "getUser", - "x-permission": "user:view", - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/UserResponse" - } - }, - "x-permissions": [ - "user:view" - ] - }, - "patch": { - "operationId": "updateUser", - "x-permission": "user:update", - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "requestBody": { - "$ref": "#/components/requestBodies/UserRequest" - }, - "responses": { - "200": { - "$ref": "#/components/responses/UserResponse" - } - }, - "x-permissions": [ - "user:update" - ] - } - }, - "/users/{id}/reset-password": { - "post": { - "operationId": "resetUserPassword", - "x-permission": "user:reset-password", - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/ResetPasswordResponse" - } - }, - "x-permissions": [ - "user:reset-password" - ] - } - }, - "/users/{id}/status": { - "patch": { - "operationId": "updateUserStatus", - "x-permission": "user:status", - "parameters": [ - { - "$ref": "#/components/parameters/Id" - } - ], - "requestBody": { - "$ref": "#/components/requestBodies/StatusRequest" - }, - "responses": { - "200": { - "$ref": "#/components/responses/UserResponse" - } - }, - "x-permissions": [ - "user:status" - ] - } - }, - "/users/batch/status": { - "post": { - "operationId": "batchUpdateUserStatus", - "x-permission": "user:status", - "requestBody": { - "$ref": "#/components/requestBodies/BatchStatusRequest" - }, - "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" - } - }, - "x-permissions": [ - "user:status" - ] - } - }, - "/users/export": { - "get": { - "operationId": "exportUsers", - "x-permission": "user:export", - "responses": { - "200": { - "description": "CSV" - } - }, - "x-permissions": [ - "user:export" - ] - } - } - }, - "components": { - "parameters": { - "AgencyId": { - "name": "agency_id", - "in": "query", - "schema": { - "type": "integer" - } - }, - "Id": { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - }, - "Keyword": { - "name": "keyword", - "in": "query", - "schema": { - "type": "string" - } - }, - "Page": { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "minimum": 1 - } - }, - "PageSize": { - "name": "page_size", - "in": "query", - "schema": { - "type": "integer", - "minimum": 1 - } - }, - "ParentBdUserId": { - "name": "parent_bd_user_id", - "in": "query", - "schema": { - "type": "integer" - } - }, - "ParentLeaderUserId": { - "name": "parent_leader_user_id", - "in": "query", - "schema": { - "type": "integer" - } - }, - "RegionId": { - "name": "region_id", - "in": "query", - "schema": { - "type": "integer" - } - }, - "Status": { - "name": "status", - "in": "query", - "schema": { - "type": "string" - } - } - }, - "requestBodies": { - "BatchStatusRequest": { - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ids", - "status" - ], - "properties": { - "ids": { - "type": "array", - "items": { - "type": "integer" - } - }, - "status": { - "type": "string" - } - } - } - } - } - }, - "ChangePasswordRequest": { - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "oldPassword", - "newPassword" - ], - "properties": { - "oldPassword": { - "type": "string" - }, - "newPassword": { - "type": "string" - } - } - } - } - } - }, - "LoginRequest": { - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "username", - "password" - ], - "properties": { - "username": { - "type": "string" - }, - "password": { - "type": "string" - } - } - } - } - } - }, - "MenuRequest": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MenuInput" - } - } - } - }, - "MenuSortRequest": { - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "items" - ], - "properties": { - "items": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "sort" - ], - "properties": { - "id": { - "type": "integer" - }, - "sort": { - "type": "integer" - } + ], + "paths": { + "/admin/agencies": { + "get": { + "operationId": "listAgencies", + "parameters": [ + { + "$ref": "#/components/parameters/Page" + }, + { + "$ref": "#/components/parameters/PageSize" + }, + { + "$ref": "#/components/parameters/Keyword" + }, + { + "$ref": "#/components/parameters/Status" + }, + { + "$ref": "#/components/parameters/RegionId" + }, + { + "$ref": "#/components/parameters/ParentBdUserId" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/AgencyPageResponse" + } + }, + "x-permission": "agency:view", + "x-permissions": ["agency:view"] + }, + "post": { + "operationId": "createAgency", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "agency:create", + "x-permissions": ["agency:create"] + } + }, + "/admin/agencies/{agency_id}/close": { + "post": { + "operationId": "closeAgency", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "agency_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "agency:status", + "x-permissions": ["agency:status"] + } + }, + "/admin/agencies/{agency_id}/join-enabled": { + "post": { + "operationId": "setAgencyJoinEnabled", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "agency_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "agency:status", + "x-permissions": ["agency:status"] + } + }, + "/admin/app-config/banners": { + "get": { + "operationId": "listBanners", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "app-config:view", + "x-permissions": ["app-config:view"] + }, + "post": { + "operationId": "createBanner", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "app-config:update", + "x-permissions": ["app-config:update"] + } + }, + "/admin/app-config/banners/{banner_id}": { + "put": { + "operationId": "updateBanner", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "banner_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "app-config:update", + "x-permissions": ["app-config:update"] + }, + "delete": { + "operationId": "deleteBanner", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "banner_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "app-config:update", + "x-permissions": ["app-config:update"] + } + }, + "/admin/app-config/h5-links": { + "get": { + "operationId": "listH5Links", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "app-config:view", + "x-permissions": ["app-config:view"] + }, + "put": { + "operationId": "updateH5Links", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "app-config:update", + "x-permissions": ["app-config:update"] + } + }, + "/admin/apps": { + "get": { + "operationId": "listApps", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" } - } } - } } - } - } - }, - "MenuVisibleRequest": { - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "visible" - ], - "properties": { - "visible": { - "type": "boolean" + }, + "/admin/bd-leaders": { + "get": { + "operationId": "listBDLeaders", + "parameters": [ + { + "$ref": "#/components/parameters/Page" + }, + { + "$ref": "#/components/parameters/PageSize" + }, + { + "$ref": "#/components/parameters/Keyword" + }, + { + "$ref": "#/components/parameters/Status" + }, + { + "$ref": "#/components/parameters/RegionId" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/BDProfilePageResponse" + } + }, + "x-permission": "bd:view", + "x-permissions": ["bd:view"] + }, + "post": { + "operationId": "createBDLeader", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "bd:create", + "x-permissions": ["bd:create"] + } + }, + "/admin/bd-leaders/{user_id}/status": { + "patch": { + "operationId": "setBDLeaderStatus", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "user_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "bd:update", + "x-permissions": ["bd:update"] + } + }, + "/admin/bds": { + "get": { + "operationId": "listBDs", + "parameters": [ + { + "$ref": "#/components/parameters/Page" + }, + { + "$ref": "#/components/parameters/PageSize" + }, + { + "$ref": "#/components/parameters/Keyword" + }, + { + "$ref": "#/components/parameters/Status" + }, + { + "$ref": "#/components/parameters/RegionId" + }, + { + "$ref": "#/components/parameters/ParentLeaderUserId" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/BDProfilePageResponse" + } + }, + "x-permission": "bd:view", + "x-permissions": ["bd:view"] + }, + "post": { + "operationId": "createBD", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "bd:create", + "x-permissions": ["bd:create"] + } + }, + "/admin/bds/{user_id}/status": { + "patch": { + "operationId": "setBDStatus", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "user_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "bd:update", + "x-permissions": ["bd:update"] + } + }, + "/admin/coin-sellers": { + "get": { + "operationId": "listCoinSellers", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "coin-seller:view", + "x-permissions": ["coin-seller:view"] + }, + "post": { + "operationId": "createCoinSeller", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "coin-seller:create", + "x-permissions": ["coin-seller:create"] + } + }, + "/admin/coin-sellers/{user_id}/status": { + "patch": { + "operationId": "setCoinSellerStatus", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "user_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "coin-seller:update", + "x-permissions": ["coin-seller:update"] + } + }, + "/admin/coin-sellers/{user_id}/stock-credits": { + "post": { + "operationId": "creditCoinSellerStock", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "user_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "coin-seller:stock-credit", + "x-permissions": ["coin-seller:stock-credit"] + } + }, + "/admin/countries": { + "get": { + "operationId": "listCountries", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "country:view", + "x-permissions": ["country:view"] + }, + "post": { + "operationId": "createCountry", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "country:create", + "x-permissions": ["country:create"] + } + }, + "/admin/countries/{country_id}": { + "get": { + "operationId": "getCountry", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "country_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "country:view", + "x-permissions": ["country:view"] + }, + "patch": { + "operationId": "updateCountry", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "country_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "country:update", + "x-permissions": ["country:update"] + }, + "delete": { + "operationId": "deleteCountry", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "country_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "country:status", + "x-permissions": ["country:status"] + } + }, + "/admin/countries/{country_id}/disable": { + "post": { + "operationId": "disableCountry", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "country_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "country:status", + "x-permissions": ["country:status"] + } + }, + "/admin/countries/{country_id}/enable": { + "post": { + "operationId": "enableCountry", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "country_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "country:status", + "x-permissions": ["country:status"] + } + }, + "/admin/files/image/upload": { + "post": { + "operationId": "uploadImage", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "upload:create", + "x-permissions": ["upload:create"] + } + }, + "/admin/files/upload": { + "post": { + "operationId": "uploadFile", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "upload:create", + "x-permissions": ["upload:create"] + } + }, + "/admin/gifts": { + "get": { + "operationId": "listGifts", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "gift:view", + "x-permissions": ["gift:view"] + }, + "post": { + "operationId": "createGift", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "gift:create", + "x-permissions": ["gift:create"] + } + }, + "/admin/gifts/{gift_id}": { + "put": { + "operationId": "updateGift", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "gift_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "gift:update", + "x-permissions": ["gift:update"] + } + }, + "/admin/gifts/{gift_id}/disable": { + "post": { + "operationId": "disableGift", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "gift_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "gift:status", + "x-permissions": ["gift:status"] + } + }, + "/admin/gifts/{gift_id}/enable": { + "post": { + "operationId": "enableGift", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "gift_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "gift:status", + "x-permissions": ["gift:status"] + } + }, + "/admin/hosts": { + "get": { + "operationId": "listHosts", + "parameters": [ + { + "$ref": "#/components/parameters/Page" + }, + { + "$ref": "#/components/parameters/PageSize" + }, + { + "$ref": "#/components/parameters/Keyword" + }, + { + "$ref": "#/components/parameters/Status" + }, + { + "$ref": "#/components/parameters/RegionId" + }, + { + "$ref": "#/components/parameters/AgencyId" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/HostProfilePageResponse" + } + }, + "x-permission": "host:view", + "x-permissions": ["host:view"] + } + }, + "/admin/payment/recharge-bills": { + "get": { + "operationId": "listRechargeBills", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "payment-bill:view", + "x-permissions": ["payment-bill:view"] + } + }, + "/admin/regions": { + "get": { + "operationId": "listRegions", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "region:view", + "x-permissions": ["region:view"] + }, + "post": { + "operationId": "createRegion", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "region:create", + "x-permissions": ["region:create"] + } + }, + "/admin/regions/{region_id}": { + "get": { + "operationId": "getRegion", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "region_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "region:view", + "x-permissions": ["region:view"] + }, + "patch": { + "operationId": "updateRegion", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "region_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "region:update", + "x-permissions": ["region:update"] + }, + "delete": { + "operationId": "disableRegion", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "region_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "region:status", + "x-permissions": ["region:status"] + } + }, + "/admin/regions/{region_id}/countries": { + "put": { + "operationId": "replaceRegionCountries", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "region_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "region:update", + "x-permissions": ["region:update"] + } + }, + "/admin/regions/{region_id}/enable": { + "post": { + "operationId": "enableRegion", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "region_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "region:status", + "x-permissions": ["region:status"] + } + }, + "/admin/resource-grants": { + "get": { + "operationId": "listResourceGrants", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "resource-grant:view", + "x-permissions": ["resource-grant:view"] + } + }, + "/admin/resource-grants/group": { + "post": { + "operationId": "grantResourceGroup", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "resource-grant:create", + "x-permissions": ["resource-grant:create"] + } + }, + "/admin/resource-grants/resource": { + "post": { + "operationId": "grantResource", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "resource-grant:create", + "x-permissions": ["resource-grant:create"] + } + }, + "/admin/resource-groups": { + "get": { + "operationId": "listResourceGroups", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "resource-group:view", + "x-permissions": ["resource-group:view"] + }, + "post": { + "operationId": "createResourceGroup", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "resource-group:create", + "x-permissions": ["resource-group:create"] + } + }, + "/admin/resource-groups/{group_id}": { + "get": { + "operationId": "getResourceGroup", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "group_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "resource-group:view", + "x-permissions": ["resource-group:view"] + }, + "put": { + "operationId": "updateResourceGroup", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "group_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "resource-group:update", + "x-permissions": ["resource-group:update"] + } + }, + "/admin/resource-groups/{group_id}/disable": { + "post": { + "operationId": "disableResourceGroup", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "group_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "resource-group:update", + "x-permissions": ["resource-group:update"] + } + }, + "/admin/resource-groups/{group_id}/enable": { + "post": { + "operationId": "enableResourceGroup", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "group_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "resource-group:update", + "x-permissions": ["resource-group:update"] + } + }, + "/admin/resource-groups/{group_id}/items": { + "put": { + "operationId": "updateResourceGroupItems", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "group_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "resource-group:update", + "x-permissions": ["resource-group:update"] + } + }, + "/admin/resources": { + "get": { + "operationId": "listResources", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "resource:view", + "x-permissions": ["resource:view"] + }, + "post": { + "operationId": "createResource", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "resource:create", + "x-permissions": ["resource:create"] + } + }, + "/admin/resources/{resource_id}": { + "get": { + "operationId": "getResource", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "resource_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "resource:view", + "x-permissions": ["resource:view"] + }, + "put": { + "operationId": "updateResource", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "resource_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "resource:update", + "x-permissions": ["resource:update"] + } + }, + "/admin/resources/{resource_id}/disable": { + "post": { + "operationId": "disableResource", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "resource_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "resource:update", + "x-permissions": ["resource:update"] + } + }, + "/admin/resources/{resource_id}/enable": { + "post": { + "operationId": "enableResource", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "resource_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "resource:update", + "x-permissions": ["resource:update"] + } + }, + "/admin/rooms": { + "get": { + "operationId": "listRooms", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "room:view", + "x-permissions": ["room:view"] + } + }, + "/admin/rooms/{room_id}": { + "patch": { + "operationId": "updateRoom", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "room_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "room:update", + "x-permissions": ["room:update"] + }, + "delete": { + "operationId": "deleteRoom", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "room_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "room:delete", + "x-permissions": ["room:delete"] + } + }, + "/app/users": { + "get": { + "operationId": "appListUsers", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "app-user:view", + "x-permissions": ["app-user:view"] + } + }, + "/app/users/{id}": { + "get": { + "operationId": "appGetUser", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "x-permission": "app-user:view", + "x-permissions": ["app-user:view"] + }, + "patch": { + "operationId": "appUpdateUser", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "x-permission": "app-user:update", + "x-permissions": ["app-user:update"] + } + }, + "/app/users/{id}/ban": { + "post": { + "operationId": "appBanUser", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "x-permission": "app-user:status", + "x-permissions": ["app-user:status"] + } + }, + "/app/users/{id}/password": { + "post": { + "operationId": "appSetPassword", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "x-permission": "app-user:password", + "x-permissions": ["app-user:password"] + } + }, + "/app/users/{id}/unban": { + "post": { + "operationId": "appUnbanUser", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "x-permission": "app-user:status", + "x-permissions": ["app-user:status"] + } + }, + "/auth/change-password": { + "post": { + "operationId": "changePassword", + "requestBody": { + "$ref": "#/components/requestBodies/ChangePasswordRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } } - } } - } - } - }, - "PermissionRequest": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PermissionInput" + }, + "/auth/login": { + "post": { + "operationId": "login", + "requestBody": { + "$ref": "#/components/requestBodies/LoginRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/SessionResponse" + } + } + } + }, + "/auth/logout": { + "post": { + "operationId": "logout", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + } + } + }, + "/auth/me": { + "get": { + "operationId": "me", + "responses": { + "200": { + "$ref": "#/components/responses/SessionResponse" + } + } + } + }, + "/auth/refresh": { + "post": { + "operationId": "refresh", + "responses": { + "200": { + "$ref": "#/components/responses/SessionResponse" + } + } + } + }, + "/dashboard/overview": { + "get": { + "operationId": "dashboardOverview", + "x-permission": "overview:view", + "responses": { + "200": { + "$ref": "#/components/responses/DashboardOverviewResponse" + } + }, + "x-permissions": ["overview:view"] + } + }, + "/exports/users": { + "post": { + "operationId": "createUserExportJob", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "export:create", + "x-permissions": ["export:create"] + } + }, + "/jobs": { + "get": { + "operationId": "listJobs", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "job:view", + "x-permissions": ["job:view"] + } + }, + "/jobs/{id}/artifact": { + "get": { + "operationId": "downloadJobArtifact", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "x-permission": "job:view", + "x-permissions": ["job:view"] + } + }, + "/jobs/{id}/cancel": { + "post": { + "operationId": "cancelJob", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "x-permission": "job:cancel", + "x-permissions": ["job:cancel"] + } + }, + "/logs/login": { + "get": { + "operationId": "listLoginLogs", + "x-permission": "log:view", + "parameters": [ + { + "$ref": "#/components/parameters/Page" + }, + { + "$ref": "#/components/parameters/PageSize" + }, + { + "$ref": "#/components/parameters/Keyword" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/LogPageResponse" + } + }, + "x-permissions": ["log:view"] + } + }, + "/logs/login/export": { + "get": { + "operationId": "exportLoginLogs", + "x-permission": "log:export", + "responses": { + "200": { + "description": "CSV" + } + }, + "x-permissions": ["log:export"] + } + }, + "/logs/operations": { + "get": { + "operationId": "listOperationLogs", + "x-permission": "log:view", + "parameters": [ + { + "$ref": "#/components/parameters/Page" + }, + { + "$ref": "#/components/parameters/PageSize" + }, + { + "$ref": "#/components/parameters/Keyword" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/LogPageResponse" + } + }, + "x-permissions": ["log:view"] + } + }, + "/logs/operations/export": { + "get": { + "operationId": "exportOperationLogs", + "x-permission": "log:export", + "responses": { + "200": { + "description": "CSV" + } + }, + "x-permissions": ["log:export"] + } + }, + "/navigation/menus": { + "get": { + "operationId": "navigationMenus", + "responses": { + "200": { + "$ref": "#/components/responses/MenuListResponse" + } + } + } + }, + "/notifications": { + "get": { + "operationId": "listNotifications", + "x-permission": "notification:view", + "responses": { + "200": { + "$ref": "#/components/responses/NotificationListResponse" + } + }, + "x-permissions": ["notification:view"] + } + }, + "/notifications/{id}": { + "delete": { + "operationId": "deleteNotification", + "x-permission": "notification:delete", + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permissions": ["notification:delete"] + } + }, + "/notifications/{id}/read": { + "patch": { + "operationId": "markNotificationRead", + "x-permission": "notification:read", + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/NotificationResponse" + } + }, + "x-permissions": ["notification:read"] + } + }, + "/notifications/read-all": { + "patch": { + "operationId": "markAllNotificationsRead", + "x-permission": "notification:read-all", + "responses": { + "200": { + "$ref": "#/components/responses/NotificationListResponse" + } + }, + "x-permissions": ["notification:read-all"] + } + }, + "/permissions": { + "get": { + "operationId": "listPermissions", + "x-permission": "permission:view", + "responses": { + "200": { + "$ref": "#/components/responses/PermissionListResponse" + } + }, + "x-permissions": ["permission:view", "role:permission", "role:manage"] + }, + "post": { + "operationId": "createPermission", + "x-permission": "permission:create", + "requestBody": { + "$ref": "#/components/requestBodies/PermissionRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/PermissionResponse" + } + }, + "x-permissions": ["permission:create"] + } + }, + "/permissions/{id}": { + "patch": { + "operationId": "updatePermission", + "x-permission": "permission:update", + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/PermissionRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/PermissionResponse" + } + }, + "x-permissions": ["permission:update"] + }, + "delete": { + "operationId": "deletePermission", + "x-permission": "permission:delete", + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permissions": ["permission:delete"] + } + }, + "/permissions/sync": { + "post": { + "operationId": "syncPermissions", + "x-permission": "permission:sync", + "responses": { + "200": { + "$ref": "#/components/responses/PermissionListResponse" + } + }, + "x-permissions": ["permission:sync"] + } + }, + "/roles": { + "get": { + "operationId": "listRoles", + "x-permission": "role:view", + "responses": { + "200": { + "$ref": "#/components/responses/RoleListResponse" + } + }, + "x-permissions": ["role:view"] + }, + "post": { + "operationId": "createRole", + "x-permission": "role:create", + "requestBody": { + "$ref": "#/components/requestBodies/RoleRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/RoleResponse" + } + }, + "x-permissions": ["role:create", "role:manage"] + } + }, + "/roles/{id}": { + "patch": { + "operationId": "updateRole", + "x-permission": "role:update", + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/RoleRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/RoleResponse" + } + }, + "x-permissions": ["role:update", "role:manage"] + }, + "delete": { + "operationId": "deleteRole", + "x-permission": "role:delete", + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permissions": ["role:delete", "role:manage"] + } + }, + "/roles/{id}/data-scopes": { + "get": { + "operationId": "getRoleDataScopes", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "x-permission": "role:data-scope", + "x-permissions": ["role:data-scope"] + }, + "put": { + "operationId": "replaceRoleDataScopes", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "x-permission": "role:data-scope", + "x-permissions": ["role:data-scope"] + } + }, + "/roles/{id}/permissions": { + "put": { + "operationId": "replaceRolePermissions", + "x-permission": "role:permission", + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/RolePermissionRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/RoleResponse" + } + }, + "x-permissions": ["role:permission", "role:manage"] + } + }, + "/search": { + "get": { + "operationId": "search", + "parameters": [ + { + "$ref": "#/components/parameters/Keyword" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/SearchResponse" + } + } + } + }, + "/system/menus": { + "get": { + "operationId": "listSystemMenus", + "x-permission": "menu:view", + "responses": { + "200": { + "$ref": "#/components/responses/MenuListResponse" + } + }, + "x-permissions": ["menu:view"] + }, + "post": { + "operationId": "createMenu", + "x-permission": "menu:create", + "requestBody": { + "$ref": "#/components/requestBodies/MenuRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/MenuResponse" + } + }, + "x-permissions": ["menu:create"] + } + }, + "/system/menus/{id}": { + "patch": { + "operationId": "updateMenu", + "x-permission": "menu:update", + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/MenuRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/MenuResponse" + } + }, + "x-permissions": ["menu:update"] + }, + "delete": { + "operationId": "deleteMenu", + "x-permission": "menu:delete", + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permissions": ["menu:delete"] + } + }, + "/system/menus/{id}/visible": { + "patch": { + "operationId": "updateMenuVisible", + "x-permission": "menu:visible", + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/MenuVisibleRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/MenuResponse" + } + }, + "x-permissions": ["menu:visible"] + } + }, + "/system/menus/sort": { + "put": { + "operationId": "sortMenus", + "x-permission": "menu:sort", + "requestBody": { + "$ref": "#/components/requestBodies/MenuSortRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/MenuListResponse" + } + }, + "x-permissions": ["menu:sort"] + } + }, + "/users": { + "get": { + "operationId": "listUsers", + "x-permission": "user:view", + "parameters": [ + { + "$ref": "#/components/parameters/Page" + }, + { + "$ref": "#/components/parameters/PageSize" + }, + { + "$ref": "#/components/parameters/Keyword" + }, + { + "name": "role", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/Status" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/UserPageResponse" + } + }, + "x-permissions": ["user:view"] + }, + "post": { + "operationId": "createUser", + "x-permission": "user:create", + "requestBody": { + "$ref": "#/components/requestBodies/UserRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/CreateUserResponse" + } + }, + "x-permissions": ["user:create"] + } + }, + "/users/{id}": { + "get": { + "operationId": "getUser", + "x-permission": "user:view", + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/UserResponse" + } + }, + "x-permissions": ["user:view"] + }, + "patch": { + "operationId": "updateUser", + "x-permission": "user:update", + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/UserRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/UserResponse" + } + }, + "x-permissions": ["user:update"] + } + }, + "/users/{id}/reset-password": { + "post": { + "operationId": "resetUserPassword", + "x-permission": "user:reset-password", + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/ResetPasswordResponse" + } + }, + "x-permissions": ["user:reset-password"] + } + }, + "/users/{id}/status": { + "patch": { + "operationId": "updateUserStatus", + "x-permission": "user:status", + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/StatusRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/UserResponse" + } + }, + "x-permissions": ["user:status"] + } + }, + "/users/batch/status": { + "post": { + "operationId": "batchUpdateUserStatus", + "x-permission": "user:status", + "requestBody": { + "$ref": "#/components/requestBodies/BatchStatusRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permissions": ["user:status"] + } + }, + "/users/export": { + "get": { + "operationId": "exportUsers", + "x-permission": "user:export", + "responses": { + "200": { + "description": "CSV" + } + }, + "x-permissions": ["user:export"] } - } } - }, - "RolePermissionRequest": { - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "permissionIds" - ], - "properties": { - "permissionIds": { - "type": "array", - "items": { + }, + "components": { + "parameters": { + "AgencyId": { + "name": "agency_id", + "in": "query", + "schema": { "type": "integer" - } } - } - } - } - } - }, - "RoleRequest": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoleInput" - } - } - } - }, - "StatusRequest": { - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "status" - ], - "properties": { - "status": { - "type": "string" + }, + "Id": { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" } - } - } - } - } - }, - "UserRequest": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserInput" - } - } - } - } - }, - "responses": { - "AgencyPageResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseAgencyPage" - } - } - } - }, - "BDProfilePageResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseBDProfilePage" - } - } - } - }, - "CreateUserResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseCreateUserResult" - } - } - } - }, - "DashboardOverviewResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseDashboardOverview" - } - } - } - }, - "EmptyResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseEmpty" - } - } - } - }, - "HostProfilePageResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseHostProfilePage" - } - } - } - }, - "LogPageResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseLogPage" - } - } - } - }, - "MenuListResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseMenuList" - } - } - } - }, - "MenuResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseMenu" - } - } - } - }, - "NotificationListResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseNotificationList" - } - } - } - }, - "NotificationResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseNotification" - } - } - } - }, - "PermissionListResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponsePermissionList" - } - } - } - }, - "PermissionResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponsePermission" - } - } - } - }, - "ResetPasswordResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseResetPasswordResult" - } - } - } - }, - "RoleListResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseRoleList" - } - } - } - }, - "RoleResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseRole" - } - } - } - }, - "SearchResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseSearchList" - } - } - } - }, - "SessionResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseSession" - } - } - } - }, - "UserPageResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseUserPage" - } - } - } - }, - "UserResponse": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiResponseUser" - } - } - } - } - }, - "schemas": { - "ApiPageAgency": { - "type": "object", - "required": [ - "items", - "page", - "pageSize", - "total" - ], - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Agency" - } - }, - "page": { - "type": "integer" - }, - "pageSize": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - }, - "ApiPageBDProfile": { - "type": "object", - "required": [ - "items", - "page", - "pageSize", - "total" - ], - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BDProfile" - } - }, - "page": { - "type": "integer" - }, - "pageSize": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - }, - "ApiPageHostProfile": { - "type": "object", - "required": [ - "items", - "page", - "pageSize", - "total" - ], - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/HostProfile" - } - }, - "page": { - "type": "integer" - }, - "pageSize": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - }, - "ApiPageLog": { - "type": "object", - "required": [ - "items", - "page", - "pageSize", - "total" - ], - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Log" - } - }, - "page": { - "type": "integer" - }, - "pageSize": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - }, - "ApiPageUser": { - "type": "object", - "required": [ - "items", - "page", - "pageSize", - "total" - ], - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AdminUser" - } - }, - "page": { - "type": "integer" - }, - "pageSize": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - }, - "ApiResponseCreateUserResult": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/CreateUserResult" - } - } - } - ] - }, - "ApiResponseAgencyPage": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/ApiPageAgency" - } - } - } - ] - }, - "ApiResponseBDProfilePage": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/ApiPageBDProfile" - } - } - } - ] - }, - "ApiResponseDashboardOverview": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/DashboardOverview" - } - } - } - ] - }, - "ApiResponseEmpty": { - "$ref": "#/components/schemas/Envelope" - }, - "ApiResponseHostProfilePage": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/ApiPageHostProfile" - } - } - } - ] - }, - "ApiResponseLogPage": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/ApiPageLog" - } - } - } - ] - }, - "ApiResponseMenu": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Menu" - } - } - } - ] - }, - "ApiResponseMenuList": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Menu" + }, + "Keyword": { + "name": "keyword", + "in": "query", + "schema": { + "type": "string" } - } - } - } - ] - }, - "ApiResponseNotification": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Notification" - } - } - } - ] - }, - "ApiResponseNotificationList": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/NotificationList" - } - } - } - ] - }, - "ApiResponsePermission": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Permission" - } - } - } - ] - }, - "ApiResponsePermissionList": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Permission" + }, + "Page": { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 } - } - } - } - ] - }, - "ApiResponseResetPasswordResult": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/ResetPasswordResult" - } - } - } - ] - }, - "ApiResponseRole": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Role" - } - } - } - ] - }, - "ApiResponseRoleList": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Role" + }, + "PageSize": { + "name": "page_size", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 } - } - } - } - ] - }, - "ApiResponseSearchList": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SearchResult" + }, + "ParentBdUserId": { + "name": "parent_bd_user_id", + "in": "query", + "schema": { + "type": "integer" + } + }, + "ParentLeaderUserId": { + "name": "parent_leader_user_id", + "in": "query", + "schema": { + "type": "integer" + } + }, + "RegionId": { + "name": "region_id", + "in": "query", + "schema": { + "type": "integer" + } + }, + "Status": { + "name": "status", + "in": "query", + "schema": { + "type": "string" } - } } - } - ] - }, - "ApiResponseSession": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Session" - } + }, + "requestBodies": { + "BatchStatusRequest": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["ids", "status"], + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "status": { + "type": "string" + } + } + } + } + } + }, + "ChangePasswordRequest": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["oldPassword", "newPassword"], + "properties": { + "oldPassword": { + "type": "string" + }, + "newPassword": { + "type": "string" + } + } + } + } + } + }, + "LoginRequest": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["username", "password"], + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + } + } + } + } + }, + "MenuRequest": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MenuInput" + } + } + } + }, + "MenuSortRequest": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["items"], + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "sort"], + "properties": { + "id": { + "type": "integer" + }, + "sort": { + "type": "integer" + } + } + } + } + } + } + } + } + }, + "MenuVisibleRequest": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["visible"], + "properties": { + "visible": { + "type": "boolean" + } + } + } + } + } + }, + "PermissionRequest": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PermissionInput" + } + } + } + }, + "RolePermissionRequest": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["permissionIds"], + "properties": { + "permissionIds": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + } + } + }, + "RoleRequest": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleInput" + } + } + } + }, + "StatusRequest": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["status"], + "properties": { + "status": { + "type": "string" + } + } + } + } + } + }, + "UserRequest": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserInput" + } + } + } } - } - ] - }, - "ApiResponseUser": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/AdminUser" - } + }, + "responses": { + "AgencyPageResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseAgencyPage" + } + } + } + }, + "BDProfilePageResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseBDProfilePage" + } + } + } + }, + "CreateUserResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseCreateUserResult" + } + } + } + }, + "DashboardOverviewResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseDashboardOverview" + } + } + } + }, + "EmptyResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseEmpty" + } + } + } + }, + "HostProfilePageResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseHostProfilePage" + } + } + } + }, + "LogPageResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseLogPage" + } + } + } + }, + "MenuListResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseMenuList" + } + } + } + }, + "MenuResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseMenu" + } + } + } + }, + "NotificationListResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseNotificationList" + } + } + } + }, + "NotificationResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseNotification" + } + } + } + }, + "PermissionListResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponsePermissionList" + } + } + } + }, + "PermissionResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponsePermission" + } + } + } + }, + "ResetPasswordResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseResetPasswordResult" + } + } + } + }, + "RoleListResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseRoleList" + } + } + } + }, + "RoleResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseRole" + } + } + } + }, + "SearchResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseSearchList" + } + } + } + }, + "SessionResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseSession" + } + } + } + }, + "UserPageResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseUserPage" + } + } + } + }, + "UserResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseUser" + } + } + } } - } - ] - }, - "ApiResponseUserPage": { - "allOf": [ - { - "$ref": "#/components/schemas/Envelope" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/ApiPageUser" - } + }, + "schemas": { + "ApiPageAgency": { + "type": "object", + "required": ["items", "page", "pageSize", "total"], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Agency" + } + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "ApiPageBDProfile": { + "type": "object", + "required": ["items", "page", "pageSize", "total"], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BDProfile" + } + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "ApiPageHostProfile": { + "type": "object", + "required": ["items", "page", "pageSize", "total"], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HostProfile" + } + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "ApiPageLog": { + "type": "object", + "required": ["items", "page", "pageSize", "total"], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Log" + } + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "ApiPageUser": { + "type": "object", + "required": ["items", "page", "pageSize", "total"], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminUser" + } + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "ApiResponseCreateUserResult": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/CreateUserResult" + } + } + } + ] + }, + "ApiResponseAgencyPage": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ApiPageAgency" + } + } + } + ] + }, + "ApiResponseBDProfilePage": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ApiPageBDProfile" + } + } + } + ] + }, + "ApiResponseDashboardOverview": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/DashboardOverview" + } + } + } + ] + }, + "ApiResponseEmpty": { + "$ref": "#/components/schemas/Envelope" + }, + "ApiResponseHostProfilePage": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ApiPageHostProfile" + } + } + } + ] + }, + "ApiResponseLogPage": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ApiPageLog" + } + } + } + ] + }, + "ApiResponseMenu": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Menu" + } + } + } + ] + }, + "ApiResponseMenuList": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Menu" + } + } + } + } + ] + }, + "ApiResponseNotification": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Notification" + } + } + } + ] + }, + "ApiResponseNotificationList": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/NotificationList" + } + } + } + ] + }, + "ApiResponsePermission": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Permission" + } + } + } + ] + }, + "ApiResponsePermissionList": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Permission" + } + } + } + } + ] + }, + "ApiResponseResetPasswordResult": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ResetPasswordResult" + } + } + } + ] + }, + "ApiResponseRole": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Role" + } + } + } + ] + }, + "ApiResponseRoleList": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Role" + } + } + } + } + ] + }, + "ApiResponseSearchList": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SearchResult" + } + } + } + } + ] + }, + "ApiResponseSession": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Session" + } + } + } + ] + }, + "ApiResponseUser": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/AdminUser" + } + } + } + ] + }, + "ApiResponseUserPage": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ApiPageUser" + } + } + } + ] + }, + "AdminUser": { + "type": "object", + "required": ["account", "id", "name", "status"], + "properties": { + "account": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "lastLogin": { + "type": "string" + }, + "mfa": { + "type": "string" + }, + "mfaEnabled": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "role": { + "type": "string" + }, + "roleIds": { + "type": "array", + "items": { + "type": "integer" + } + }, + "status": { + "type": "string" + }, + "team": { + "type": "string" + } + } + }, + "Agency": { + "type": "object", + "required": ["agencyId"], + "properties": { + "agencyId": { + "type": "integer" + }, + "createdAtMs": { + "type": "integer" + }, + "createdByUserId": { + "type": "integer" + }, + "joinEnabled": { + "type": "boolean" + }, + "maxHosts": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "ownerUserId": { + "type": "integer" + }, + "parentBdUserId": { + "type": "integer" + }, + "regionId": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "updatedAtMs": { + "type": "integer" + } + } + }, + "BDProfile": { + "type": "object", + "required": ["userId"], + "properties": { + "createdAtMs": { + "type": "integer" + }, + "createdByUserId": { + "type": "integer" + }, + "parentLeaderUserId": { + "type": "integer" + }, + "regionId": { + "type": "integer" + }, + "role": { + "type": "string" + }, + "status": { + "type": "string" + }, + "updatedAtMs": { + "type": "integer" + }, + "userId": { + "type": "integer" + } + } + }, + "CreateUserResult": { + "type": "object", + "properties": { + "initialPassword": { + "type": "string" + } + } + }, + "DashboardOverview": { + "type": "object", + "additionalProperties": true + }, + "Envelope": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "message": { + "type": "string" + } + } + }, + "HostProfile": { + "type": "object", + "required": ["userId"], + "properties": { + "createdAtMs": { + "type": "integer" + }, + "currentAgencyId": { + "type": "integer" + }, + "currentMembershipId": { + "type": "integer" + }, + "firstBecameHostAtMs": { + "type": "integer" + }, + "regionId": { + "type": "integer" + }, + "source": { + "type": "string" + }, + "status": { + "type": "string" + }, + "updatedAtMs": { + "type": "integer" + }, + "userId": { + "type": "integer" + } + } + }, + "Log": { + "type": "object", + "additionalProperties": true + }, + "Menu": { + "type": "object", + "additionalProperties": true + }, + "MenuInput": { + "type": "object", + "required": ["code", "label"], + "additionalProperties": true + }, + "Notification": { + "type": "object", + "additionalProperties": true + }, + "NotificationList": { + "type": "object", + "required": ["items", "unread"], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Notification" + } + }, + "unread": { + "type": "integer" + } + } + }, + "Permission": { + "type": "object", + "additionalProperties": true + }, + "PermissionInput": { + "type": "object", + "required": ["code", "kind", "name"], + "additionalProperties": true + }, + "ResetPasswordResult": { + "type": "object", + "properties": { + "initialPassword": { + "type": "string" + } + } + }, + "Role": { + "type": "object", + "additionalProperties": true + }, + "RoleInput": { + "type": "object", + "required": ["code", "name"], + "additionalProperties": true + }, + "SearchResult": { + "type": "object", + "additionalProperties": true + }, + "Session": { + "type": "object", + "additionalProperties": true + }, + "UserInput": { + "type": "object", + "required": ["name"], + "additionalProperties": true } - } - ] - }, - "AdminUser": { - "type": "object", - "required": [ - "account", - "id", - "name", - "status" - ], - "properties": { - "account": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "lastLogin": { - "type": "string" - }, - "mfa": { - "type": "string" - }, - "mfaEnabled": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "role": { - "type": "string" - }, - "roleIds": { - "type": "array", - "items": { - "type": "integer" - } - }, - "status": { - "type": "string" - }, - "team": { - "type": "string" - } } - }, - "Agency": { - "type": "object", - "required": [ - "agencyId" - ], - "properties": { - "agencyId": { - "type": "integer" - }, - "createdAtMs": { - "type": "integer" - }, - "createdByUserId": { - "type": "integer" - }, - "joinEnabled": { - "type": "boolean" - }, - "maxHosts": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "ownerUserId": { - "type": "integer" - }, - "parentBdUserId": { - "type": "integer" - }, - "regionId": { - "type": "integer" - }, - "status": { - "type": "string" - }, - "updatedAtMs": { - "type": "integer" - } - } - }, - "BDProfile": { - "type": "object", - "required": [ - "userId" - ], - "properties": { - "createdAtMs": { - "type": "integer" - }, - "createdByUserId": { - "type": "integer" - }, - "parentLeaderUserId": { - "type": "integer" - }, - "regionId": { - "type": "integer" - }, - "role": { - "type": "string" - }, - "status": { - "type": "string" - }, - "updatedAtMs": { - "type": "integer" - }, - "userId": { - "type": "integer" - } - } - }, - "CreateUserResult": { - "type": "object", - "properties": { - "initialPassword": { - "type": "string" - } - } - }, - "DashboardOverview": { - "type": "object", - "additionalProperties": true - }, - "Envelope": { - "type": "object", - "properties": { - "code": { - "type": "integer" - }, - "data": {}, - "message": { - "type": "string" - } - } - }, - "HostProfile": { - "type": "object", - "required": [ - "userId" - ], - "properties": { - "createdAtMs": { - "type": "integer" - }, - "currentAgencyId": { - "type": "integer" - }, - "currentMembershipId": { - "type": "integer" - }, - "firstBecameHostAtMs": { - "type": "integer" - }, - "regionId": { - "type": "integer" - }, - "source": { - "type": "string" - }, - "status": { - "type": "string" - }, - "updatedAtMs": { - "type": "integer" - }, - "userId": { - "type": "integer" - } - } - }, - "Log": { - "type": "object", - "additionalProperties": true - }, - "Menu": { - "type": "object", - "additionalProperties": true - }, - "MenuInput": { - "type": "object", - "required": [ - "code", - "label" - ], - "additionalProperties": true - }, - "Notification": { - "type": "object", - "additionalProperties": true - }, - "NotificationList": { - "type": "object", - "required": [ - "items", - "unread" - ], - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Notification" - } - }, - "unread": { - "type": "integer" - } - } - }, - "Permission": { - "type": "object", - "additionalProperties": true - }, - "PermissionInput": { - "type": "object", - "required": [ - "code", - "kind", - "name" - ], - "additionalProperties": true - }, - "ResetPasswordResult": { - "type": "object", - "properties": { - "initialPassword": { - "type": "string" - } - } - }, - "Role": { - "type": "object", - "additionalProperties": true - }, - "RoleInput": { - "type": "object", - "required": [ - "code", - "name" - ], - "additionalProperties": true - }, - "SearchResult": { - "type": "object", - "additionalProperties": true - }, - "Session": { - "type": "object", - "additionalProperties": true - }, - "UserInput": { - "type": "object", - "required": [ - "name" - ], - "additionalProperties": true - } } - } } diff --git a/src/app/layout/AdminLayout.jsx b/src/app/layout/AdminLayout.jsx index b055c55..e5bb632 100644 --- a/src/app/layout/AdminLayout.jsx +++ b/src/app/layout/AdminLayout.jsx @@ -40,6 +40,7 @@ export function AdminLayout() {
setSidebarCollapsed((current) => !current)} /> diff --git a/src/app/layout/Header.jsx b/src/app/layout/Header.jsx index f216a6a..195719f 100644 --- a/src/app/layout/Header.jsx +++ b/src/app/layout/Header.jsx @@ -26,7 +26,7 @@ import { useToast } from "@/shared/ui/ToastProvider.jsx"; const SearchDialog = lazy(() => import("@/features/search/components/SearchDialog.jsx").then((module) => ({ default: module.SearchDialog }))); const emptyPasswordForm = { oldPassword: "", newPassword: "", confirmPassword: "" }; -export function Header({ activeLabel, isSidebarCollapsed, onRefresh, onToggleSidebar }) { +export function Header({ activeLabel, isSidebarCollapsed, menuItems = [], onRefresh, onToggleSidebar }) { const [searchOpen, setSearchOpen] = useState(false); const [searchMounted, setSearchMounted] = useState(false); const [userMenuAnchor, setUserMenuAnchor] = useState(null); @@ -186,7 +186,7 @@ export function Header({ activeLabel, isSidebarCollapsed, onRefresh, onToggleSid @@ -223,7 +223,7 @@ export function Header({ activeLabel, isSidebarCollapsed, onRefresh, onToggleSid {searchMounted ? ( - setSearchOpen(false)} onSelect={handleSearchSelect} /> + setSearchOpen(false)} onSelect={handleSearchSelect} /> ) : null} ({ - bannerType: "h5", - countryCode: "", - coverUrl: "", - description: "", - param: "", - platform: "android", - regionId: "", - sortOrder: "0", - status: "active" + bannerType: "h5", + countryCode: "", + coverUrl: "", + description: "", + param: "", + platform: "android", + regionId: "", + sortOrder: "0", + status: "active", }); export function useBannerConfigPage() { - const abilities = useAppConfigAbilities(); - const confirm = useConfirm(); - const { showToast } = useToast(); - const { countryOptions, loadingCountries } = useCountryOptions(); - const { loadingRegions, regionOptions } = useRegionOptions(); - const [keyword, setKeyword] = useState(""); - const [status, setStatus] = useState(""); - const [platform, setPlatform] = useState(""); - const [regionId, setRegionId] = useState(""); - const [countryCode, setCountryCode] = useState(""); - const [activeAction, setActiveAction] = useState(""); - const [editingItem, setEditingItem] = useState(null); - const [form, setFormState] = useState(emptyForm); - const [loadingAction, setLoadingAction] = useState(""); + const abilities = useAppConfigAbilities(); + const confirm = useConfirm(); + const { showToast } = useToast(); + const { countryOptions, loadingCountries } = useCountryOptions(); + const { loadingRegions, regionOptions } = useRegionOptions(); + const [keyword, setKeyword] = useState(""); + const [status, setStatus] = useState(""); + const [platform, setPlatform] = useState(""); + const [regionId, setRegionId] = useState(""); + const [countryCode, setCountryCode] = useState(""); + const [activeAction, setActiveAction] = useState(""); + const [editingItem, setEditingItem] = useState(null); + const [form, setFormState] = useState(emptyForm); + const [loadingAction, setLoadingAction] = useState(""); - const filters = useMemo( - () => ({ - countryCode, - keyword, - platform, - regionId, - status - }), - [countryCode, keyword, platform, regionId, status] - ); - const queryFn = useCallback(() => listBanners(filters), [filters]); - const { data = emptyData, error, loading, reload } = useAdminQuery(queryFn, { - errorMessage: "加载 BANNER 配置失败", - initialData: emptyData, - queryKey: ["app-config", "banners", filters] - }); - - const setForm = (patch) => { - setFormState((current) => ({ ...current, ...patch })); - }; - - const openCreate = () => { - setEditingItem(null); - setFormState(emptyForm()); - setActiveAction("create"); - }; - - const openEdit = (item) => { - setEditingItem(item); - setFormState(formFromBanner(item)); - setActiveAction("edit"); - }; - - const closeDialog = () => { - setActiveAction(""); - setEditingItem(null); - setFormState(emptyForm()); - }; - - const submitBanner = async (event) => { - event.preventDefault(); - const payload = parseForm(appBannerSchema, normalizeForm(form)); - const action = editingItem ? "edit" : "create"; - setLoadingAction(action); - try { - if (editingItem) { - await updateBanner(editingItem.id, payload); - showToast("BANNER 已更新", "success"); - } else { - await createBanner(payload); - showToast("BANNER 已新增", "success"); - } - closeDialog(); - await reload(); - } catch (err) { - showToast(err.message || "操作失败", "error"); - } finally { - setLoadingAction(""); - } - }; - - const removeBanner = async (item) => { - const ok = await confirm({ - confirmText: "删除", - message: item.description || item.param || `BANNER ${item.id}`, - title: "删除BANNER", - tone: "danger" + const filters = useMemo( + () => ({ + countryCode, + keyword, + platform, + regionId, + status, + }), + [countryCode, keyword, platform, regionId, status], + ); + const queryFn = useCallback(() => listBanners(filters), [filters]); + const { + data = emptyData, + error, + loading, + reload, + } = useAdminQuery(queryFn, { + errorMessage: "加载 BANNER 配置失败", + initialData: emptyData, + queryKey: ["app-config", "banners", filters], }); - if (!ok) { - return; - } - setLoadingAction(`delete:${item.id}`); - try { - await deleteBanner(item.id); - await reload(); - showToast("BANNER 已删除", "success"); - } catch (err) { - showToast(err.message || "删除失败", "error"); - } finally { - setLoadingAction(""); - } - }; - return { - abilities, - activeAction, - closeDialog, - countryCode, - countryOptions, - data, - editingItem, - error, - form, - keyword, - loading, - loadingAction, - loadingCountries, - loadingRegions, - openCreate, - openEdit, - platform, - regionId, - regionOptions, - reload, - removeBanner, - setCountryCode, - setForm, - setKeyword, - setPlatform, - setRegionId, - setStatus, - status, - submitBanner - }; + const setForm = (patch) => { + setFormState((current) => ({ ...current, ...patch })); + }; + + const openCreate = () => { + setEditingItem(null); + setFormState(emptyForm()); + setActiveAction("create"); + }; + + const openEdit = (item) => { + setEditingItem(item); + setFormState(formFromBanner(item)); + setActiveAction("edit"); + }; + + const closeDialog = () => { + setActiveAction(""); + setEditingItem(null); + setFormState(emptyForm()); + }; + + const resetFilters = () => { + setKeyword(""); + setStatus(""); + setPlatform(""); + setRegionId(""); + setCountryCode(""); + }; + + const submitBanner = async (event) => { + event.preventDefault(); + const payload = parseForm(appBannerSchema, normalizeForm(form)); + const action = editingItem ? "edit" : "create"; + setLoadingAction(action); + try { + if (editingItem) { + await updateBanner(editingItem.id, payload); + showToast("BANNER 已更新", "success"); + } else { + await createBanner(payload); + showToast("BANNER 已新增", "success"); + } + closeDialog(); + await reload(); + } catch (err) { + showToast(err.message || "操作失败", "error"); + } finally { + setLoadingAction(""); + } + }; + + const removeBanner = async (item) => { + const ok = await confirm({ + confirmText: "删除", + message: item.description || item.param || `BANNER ${item.id}`, + title: "删除BANNER", + tone: "danger", + }); + if (!ok) { + return; + } + setLoadingAction(`delete:${item.id}`); + try { + await deleteBanner(item.id); + await reload(); + showToast("BANNER 已删除", "success"); + } catch (err) { + showToast(err.message || "删除失败", "error"); + } finally { + setLoadingAction(""); + } + }; + + return { + abilities, + activeAction, + closeDialog, + countryCode, + countryOptions, + data, + editingItem, + error, + form, + keyword, + loading, + loadingAction, + loadingCountries, + loadingRegions, + openCreate, + openEdit, + platform, + regionId, + regionOptions, + reload, + removeBanner, + resetFilters, + setCountryCode, + setForm, + setKeyword, + setPlatform, + setRegionId, + setStatus, + status, + submitBanner, + }; } function normalizeForm(form) { - return { - ...form, - countryCode: form.countryCode || "", - regionId: form.regionId || 0, - sortOrder: form.sortOrder || 0 - }; + return { + ...form, + countryCode: form.countryCode || "", + regionId: form.regionId || 0, + sortOrder: form.sortOrder || 0, + }; } function formFromBanner(item) { - return { - bannerType: item.bannerType || "h5", - countryCode: item.countryCode || "", - coverUrl: item.coverUrl || "", - description: item.description || "", - param: item.param || "", - platform: item.platform || "android", - regionId: item.regionId ? String(item.regionId) : "", - sortOrder: String(item.sortOrder || 0), - status: item.status || "active" - }; + return { + bannerType: item.bannerType || "h5", + countryCode: item.countryCode || "", + coverUrl: item.coverUrl || "", + description: item.description || "", + param: item.param || "", + platform: item.platform || "android", + regionId: item.regionId ? String(item.regionId) : "", + sortOrder: String(item.sortOrder || 0), + status: item.status || "active", + }; } diff --git a/src/features/app-config/pages/BannerConfigPage.jsx b/src/features/app-config/pages/BannerConfigPage.jsx index 4dce46a..015b110 100644 --- a/src/features/app-config/pages/BannerConfigPage.jsx +++ b/src/features/app-config/pages/BannerConfigPage.jsx @@ -10,299 +10,362 @@ import styles from "@/features/app-config/app-config.module.css"; import { Button } from "@/shared/ui/Button.jsx"; import { AdminFormDialog } from "@/shared/ui/AdminFormDialog.jsx"; import { - AdminActionIconButton, - AdminFilterSelect, - AdminListBody, - AdminListPage, - AdminListToolbar, - AdminRowActions, - AdminSearchBox, - adminListClasses + AdminActionIconButton, + AdminFilterResetButton, + AdminListBody, + AdminListPage, + AdminListToolbar, + AdminRowActions, } from "@/shared/ui/AdminListLayout.jsx"; import { DataState } from "@/shared/ui/DataState.jsx"; import { DataTable } from "@/shared/ui/DataTable.jsx"; -import { createRegionColumnFilter, RegionFilterControl } from "@/shared/ui/RegionFilterControl.jsx"; +import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx"; import { RegionSelect } from "@/shared/ui/RegionSelect.jsx"; +import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; import { UploadField } from "@/shared/ui/UploadField.jsx"; import { formatMillis } from "@/shared/utils/time.js"; const statusOptions = [ - ["", "全部状态"], - ["active", "启用"], - ["disabled", "关闭"] + ["", "全部状态"], + ["active", "启用"], + ["disabled", "关闭"], ]; const platformOptions = [ - ["", "全部平台"], - ["android", "安卓"], - ["ios", "iOS"] + ["", "全部平台"], + ["android", "安卓"], + ["ios", "iOS"], ]; export function BannerConfigPage() { - const page = useBannerConfigPage(); - const items = page.data.items || []; - const columns = useMemo(() => bannerColumns(page), [page]); + const page = useBannerConfigPage(); + const items = page.data.items || []; + const columns = useMemo(() => bannerColumns(page), [page]); - return ( - - } variant="primary" onClick={page.openCreate}> - 新增BANNER - - ) : null - } - filters={ - <> - - - - + } + variant="primary" + onClick={page.openCreate} + > + 新增BANNER + + ) : null + } + filters={ + + } /> - - - } - /> - - - item.id} /> -
- {items.length} 条 -
-
-
+ + + item.id} /> +
+ {items.length} 条 +
+
+
- - page.setForm({ coverUrl })} - /> - page.setForm({ bannerType: event.target.value })}> - H5 - APP - - page.setForm({ param: event.target.value })} - /> - page.setForm({ status: event.target.value })}> - 启用 - 关闭 - - page.setForm({ platform: event.target.value })}> - 安卓 - iOS - - page.setForm({ sortOrder: event.target.value })} - /> - page.setForm({ regionId })} - /> - page.setForm({ countryCode })} - /> - page.setForm({ description: event.target.value })} - /> - -
- ); + + page.setForm({ coverUrl })} + /> + page.setForm({ bannerType: event.target.value })} + > + H5 + APP + + page.setForm({ param: event.target.value })} + /> + page.setForm({ status: event.target.value })} + > + 启用 + 关闭 + + page.setForm({ platform: event.target.value })} + > + 安卓 + iOS + + page.setForm({ sortOrder: event.target.value })} + /> + page.setForm({ regionId })} + /> + page.setForm({ countryCode })} + /> + page.setForm({ description: event.target.value })} + /> + + + ); } function bannerColumns(page) { - const columns = [ - { - key: "cover", - label: "封面图", - render: (item) => , - width: "minmax(112px, 0.55fr)" - }, - { - key: "target", - label: "类型 / 参数", - render: (item) => , - width: "minmax(260px, 1.3fr)" - }, - { - key: "state", - label: "平台 / 状态", - render: (item) => } />, - width: "minmax(140px, 0.75fr)" - }, - { - key: "scope", - label: "区域 / 国家", - filter: createRegionColumnFilter({ - loading: page.loadingRegions, - options: page.regionOptions, - value: page.regionId, - onChange: page.setRegionId - }), - render: (item) => , - width: "minmax(180px, 0.95fr)" - }, - { - key: "sortOrder", - label: "排序", - width: "minmax(80px, 0.4fr)" - }, - { - key: "description", - label: "描述", - render: (item) => item.description || "-", - width: "minmax(180px, 0.9fr)" - }, - { - key: "updatedAtMs", - label: "更新时间", - render: (item) => formatMillis(item.updatedAtMs), - width: "minmax(160px, 0.8fr)" - } - ]; + const columns = [ + { + key: "cover", + label: "封面图", + render: (item) => , + width: "minmax(112px, 0.55fr)", + }, + { + key: "target", + label: "类型 / 参数", + render: (item) => , + filter: createTextColumnFilter({ + placeholder: "搜索参数、国家、描述", + value: page.keyword, + onChange: page.setKeyword, + }), + width: "minmax(260px, 1.3fr)", + }, + { + key: "platform", + label: "平台", + render: (item) => platformLabel(item.platform), + filter: createOptionsColumnFilter({ + options: platformOptions, + placeholder: "搜索平台", + value: page.platform, + onChange: page.setPlatform, + }), + width: "minmax(110px, 0.6fr)", + }, + { + key: "status", + label: "状态", + render: (item) => , + filter: createOptionsColumnFilter({ + options: statusOptions, + placeholder: "搜索状态", + value: page.status, + onChange: page.setStatus, + }), + width: "minmax(100px, 0.55fr)", + }, + { + key: "region", + label: "区域", + render: (item) => regionLabel(page.regionOptions, item.regionId), + filter: createRegionColumnFilter({ + loading: page.loadingRegions, + options: page.regionOptions, + value: page.regionId, + onChange: page.setRegionId, + }), + width: "minmax(150px, 0.8fr)", + }, + { + key: "country", + label: "国家", + render: (item) => countryLabel(page.countryOptions, item.countryCode), + filter: createOptionsColumnFilter({ + emptyLabel: "全部国家", + loading: page.loadingCountries, + options: page.countryOptions, + placeholder: "搜索国家", + value: page.countryCode, + onChange: page.setCountryCode, + }), + width: "minmax(150px, 0.8fr)", + }, + { + key: "sortOrder", + label: "排序", + width: "minmax(80px, 0.4fr)", + }, + { + key: "description", + label: "描述", + render: (item) => item.description || "-", + width: "minmax(180px, 0.9fr)", + }, + { + key: "updatedAtMs", + label: "更新时间", + render: (item) => formatMillis(item.updatedAtMs), + width: "minmax(160px, 0.8fr)", + }, + ]; - if (!page.abilities.canUpdate) { - return columns; - } - - return [ - ...columns, - { - key: "actions", - label: "操作", - render: (item) => , - width: "minmax(96px, 0.45fr)" + if (!page.abilities.canUpdate) { + return columns; } - ]; + + return [ + ...columns, + { + key: "actions", + label: "操作", + render: (item) => , + width: "minmax(96px, 0.45fr)", + }, + ]; } function BannerCover({ src }) { - if (!src) { - return ( - - - - ); - } - return ; + if (!src) { + return ( + + + + ); + } + return ; } function BannerActions({ item, page }) { - const deleting = page.loadingAction === `delete:${item.id}`; - return ( - - page.openEdit(item)}> - - - page.removeBanner(item)}> - - - - ); + const deleting = page.loadingAction === `delete:${item.id}`; + return ( + + page.openEdit(item)} + > + + + page.removeBanner(item)} + > + + + + ); } function CountrySelect({ className, disabled, emptyLabel, label = "国家", onChange, options, value }) { - return ( - onChange(event.target.value)}> - {emptyLabel} - {options.map((option) => ( - - {option.label} - - ))} - - ); + return ( + onChange(event.target.value)} + > + {emptyLabel} + {options.map((option) => ( + + {option.label} + + ))} + + ); } function Stack({ primary, secondary }) { - return ( -
- {primary || "-"} - {secondary || "-"} -
- ); + return ( +
+ {primary || "-"} + {secondary || "-"} +
+ ); } function StatusBadge({ status }) { - const tone = status === "active" ? "succeeded" : "stopped"; - return ( - - - {status === "active" ? "启用" : "关闭"} - - ); + const tone = status === "active" ? "succeeded" : "stopped"; + return ( + + + {status === "active" ? "启用" : "关闭"} + + ); } function typeLabel(value) { - return value === "app" ? "APP" : "H5"; + return value === "app" ? "APP" : "H5"; } function platformLabel(value) { - if (value === "android") { - return "安卓"; - } - if (value === "ios") { - return "iOS"; - } - return value || "-"; + if (value === "android") { + return "安卓"; + } + if (value === "ios") { + return "iOS"; + } + return value || "-"; } function regionLabel(options, value) { - if (!value) { - return "全部区域"; - } - return options.find((option) => option.value === String(value))?.label || `区域 ${value}`; + if (!value) { + return "全部区域"; + } + return options.find((option) => option.value === String(value))?.label || `区域 ${value}`; } function countryLabel(options, value) { - if (!value) { - return "全部国家"; - } - return options.find((option) => option.value === value)?.label || value; + if (!value) { + return "全部国家"; + } + return options.find((option) => option.value === value)?.label || value; } diff --git a/src/features/app-users/hooks/useAppUsersPage.js b/src/features/app-users/hooks/useAppUsersPage.js index f99348e..0757bf1 100644 --- a/src/features/app-users/hooks/useAppUsersPage.js +++ b/src/features/app-users/hooks/useAppUsersPage.js @@ -3,13 +3,7 @@ import { parseForm } from "@/shared/forms/validation"; import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js"; import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; -import { - banAppUser, - listAppUsers, - setAppUserPassword, - unbanAppUser, - updateAppUser -} from "@/features/app-users/api"; +import { banAppUser, listAppUsers, setAppUserPassword, unbanAppUser, updateAppUser } from "@/features/app-users/api"; import { useAppUserAbilities } from "@/features/app-users/permissions.js"; import { appUserCountrySchema, appUserPasswordSchema, appUserUpdateSchema } from "@/features/app-users/schema"; import { useCountryOptions } from "@/features/host-org/hooks/useCountryOptions.js"; @@ -21,177 +15,189 @@ const emptyCountryForm = () => ({ country: "" }); const emptyPasswordForm = () => ({ password: "" }); export function useAppUsersPage() { - const abilities = useAppUserAbilities(); - const confirm = useConfirm(); - const { showToast } = useToast(); - const { countryOptions, loadingCountries } = useCountryOptions(); - const [query, setQuery] = useState(""); - const [status, setStatus] = useState(""); - const [page, setPage] = useState(1); - const [activeAction, setActiveAction] = useState(""); - const [activeUser, setActiveUser] = useState(null); - const [editForm, setEditForm] = useState(emptyEditForm); - const [countryForm, setCountryForm] = useState(emptyCountryForm); - const [passwordForm, setPasswordForm] = useState(emptyPasswordForm); - const [loadingAction, setLoadingAction] = useState(""); - const filters = useMemo( - () => ({ - keyword: query, - status - }), - [query, status] - ); - const { data = emptyData, error, loading, reload } = usePaginatedQuery({ - errorMessage: "加载用户列表失败", - fetcher: listAppUsers, - filters, - page, - pageSize, - queryKey: ["app-users", filters, page] - }); - - const changeQuery = (value) => { - setQuery(value); - setPage(1); - }; - - const changeStatus = (value) => { - setStatus(value); - setPage(1); - }; - - const openEdit = (user) => { - setActiveUser(user); - setEditForm({ - avatar: user.avatar || "", - gender: user.gender || "", - username: user.username || "" + const abilities = useAppUserAbilities(); + const confirm = useConfirm(); + const { showToast } = useToast(); + const { countryOptions, loadingCountries } = useCountryOptions(); + const [query, setQuery] = useState(""); + const [status, setStatus] = useState(""); + const [page, setPage] = useState(1); + const [activeAction, setActiveAction] = useState(""); + const [activeUser, setActiveUser] = useState(null); + const [editForm, setEditForm] = useState(emptyEditForm); + const [countryForm, setCountryForm] = useState(emptyCountryForm); + const [passwordForm, setPasswordForm] = useState(emptyPasswordForm); + const [loadingAction, setLoadingAction] = useState(""); + const filters = useMemo( + () => ({ + keyword: query, + status, + }), + [query, status], + ); + const { + data = emptyData, + error, + loading, + reload, + } = usePaginatedQuery({ + errorMessage: "加载用户列表失败", + fetcher: listAppUsers, + filters, + page, + pageSize, + queryKey: ["app-users", filters, page], }); - setActiveAction("edit"); - }; - const openPassword = (user) => { - setActiveUser(user); - setPasswordForm(emptyPasswordForm()); - setActiveAction("password"); - }; + const changeQuery = (value) => { + setQuery(value); + setPage(1); + }; - const openCountry = (user) => { - setActiveUser(user); - setCountryForm({ country: user.country || "" }); - setActiveAction("country"); - }; + const changeStatus = (value) => { + setStatus(value); + setPage(1); + }; - const closeAction = () => { - setActiveAction(""); - setActiveUser(null); - }; + const resetFilters = () => { + setQuery(""); + setStatus(""); + setPage(1); + }; - const submitEdit = async (event) => { - event.preventDefault(); - if (!activeUser) { - return; - } - await runAction("edit", "用户已更新", async () => { - const payload = parseForm(appUserUpdateSchema, editForm); - await updateAppUser(activeUser.userId, payload); - closeAction(); - await reload(); - }); - }; + const openEdit = (user) => { + setActiveUser(user); + setEditForm({ + avatar: user.avatar || "", + gender: user.gender || "", + username: user.username || "", + }); + setActiveAction("edit"); + }; - const submitCountry = async (event) => { - event.preventDefault(); - if (!activeUser) { - return; - } - await runAction("country", "国家已更新", async () => { - const payload = parseForm(appUserCountrySchema, countryForm); - await updateAppUser(activeUser.userId, payload); - closeAction(); - await reload(); - }); - }; + const openPassword = (user) => { + setActiveUser(user); + setPasswordForm(emptyPasswordForm()); + setActiveAction("password"); + }; - const submitPassword = async (event) => { - event.preventDefault(); - if (!activeUser) { - return; - } - await runAction("password", "密码已设置", async () => { - const payload = parseForm(appUserPasswordSchema, passwordForm); - await setAppUserPassword(activeUser.userId, payload); - closeAction(); - }); - }; + const openCountry = (user) => { + setActiveUser(user); + setCountryForm({ country: user.country || "" }); + setActiveAction("country"); + }; - const toggleBan = async (user) => { - const banned = isBanned(user); - const ok = await confirm({ - confirmText: banned ? "解封" : "封禁", - message: `${user.username || user.displayUserId || user.userId} 的状态会立即变更。`, - title: banned ? "解封用户" : "封禁用户", - tone: banned ? "primary" : "danger" - }); - if (!ok) { - return; - } - await runAction(`status-${user.userId}`, banned ? "用户已解封" : "用户已封禁", async () => { - if (banned) { - await unbanAppUser(user.userId); - } else { - await banAppUser(user.userId); - } - await reload(); - }); - }; + const closeAction = () => { + setActiveAction(""); + setActiveUser(null); + }; - const runAction = async (action, successMessage, submitter) => { - setLoadingAction(action); - try { - await submitter(); - showToast(successMessage, "success"); - } catch (err) { - showToast(err.message || "操作失败", "error"); - } finally { - setLoadingAction(""); - } - }; + const submitEdit = async (event) => { + event.preventDefault(); + if (!activeUser) { + return; + } + await runAction("edit", "用户已更新", async () => { + const payload = parseForm(appUserUpdateSchema, editForm); + await updateAppUser(activeUser.userId, payload); + closeAction(); + await reload(); + }); + }; - return { - abilities, - activeAction, - activeUser, - changeQuery, - changeStatus, - closeAction, - countryForm, - countryOptions, - data, - editForm, - error, - loading, - loadingAction, - loadingCountries, - openCountry, - openEdit, - openPassword, - page, - passwordForm, - query, - reload, - setCountryForm, - setEditForm, - setPage, - setPasswordForm, - status, - submitCountry, - submitEdit, - submitPassword, - toggleBan - }; + const submitCountry = async (event) => { + event.preventDefault(); + if (!activeUser) { + return; + } + await runAction("country", "国家已更新", async () => { + const payload = parseForm(appUserCountrySchema, countryForm); + await updateAppUser(activeUser.userId, payload); + closeAction(); + await reload(); + }); + }; + + const submitPassword = async (event) => { + event.preventDefault(); + if (!activeUser) { + return; + } + await runAction("password", "密码已设置", async () => { + const payload = parseForm(appUserPasswordSchema, passwordForm); + await setAppUserPassword(activeUser.userId, payload); + closeAction(); + }); + }; + + const toggleBan = async (user) => { + const banned = isBanned(user); + const ok = await confirm({ + confirmText: banned ? "解封" : "封禁", + message: `${user.username || user.displayUserId || user.userId} 的状态会立即变更。`, + title: banned ? "解封用户" : "封禁用户", + tone: banned ? "primary" : "danger", + }); + if (!ok) { + return; + } + await runAction(`status-${user.userId}`, banned ? "用户已解封" : "用户已封禁", async () => { + if (banned) { + await unbanAppUser(user.userId); + } else { + await banAppUser(user.userId); + } + await reload(); + }); + }; + + const runAction = async (action, successMessage, submitter) => { + setLoadingAction(action); + try { + await submitter(); + showToast(successMessage, "success"); + } catch (err) { + showToast(err.message || "操作失败", "error"); + } finally { + setLoadingAction(""); + } + }; + + return { + abilities, + activeAction, + activeUser, + changeQuery, + changeStatus, + closeAction, + countryForm, + countryOptions, + data, + editForm, + error, + loading, + loadingAction, + loadingCountries, + openCountry, + openEdit, + openPassword, + page, + passwordForm, + query, + reload, + resetFilters, + setCountryForm, + setEditForm, + setPage, + setPasswordForm, + status, + submitCountry, + submitEdit, + submitPassword, + toggleBan, + }; } function isBanned(user) { - return user.status === "banned" || user.status === "disabled"; + return user.status === "banned" || user.status === "disabled"; } diff --git a/src/features/app-users/pages/AppUserListPage.jsx b/src/features/app-users/pages/AppUserListPage.jsx index b236183..c684d6c 100644 --- a/src/features/app-users/pages/AppUserListPage.jsx +++ b/src/features/app-users/pages/AppUserListPage.jsx @@ -10,257 +10,307 @@ import Tooltip from "@mui/material/Tooltip"; import { AdminFormDialog } from "@/shared/ui/AdminFormDialog.jsx"; import { DataState } from "@/shared/ui/DataState.jsx"; import { DataTable } from "@/shared/ui/DataTable.jsx"; +import { AdminFilterResetButton } from "@/shared/ui/AdminListLayout.jsx"; import { IconButton } from "@/shared/ui/IconButton.jsx"; import { PaginationBar } from "@/shared/ui/PaginationBar.jsx"; -import { SearchBox } from "@/shared/ui/SearchBox.jsx"; -import { StatusSelect } from "@/shared/ui/StatusSelect.jsx"; +import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; import { UploadField } from "@/shared/ui/UploadField.jsx"; import { formatMillis } from "@/shared/utils/time.js"; -import { - appUserStatusFilters, - appUserStatusLabels, - genderOptions -} from "@/features/app-users/constants.js"; +import { appUserStatusFilters, appUserStatusLabels, genderOptions } from "@/features/app-users/constants.js"; import { useAppUsersPage } from "@/features/app-users/hooks/useAppUsersPage.js"; import styles from "@/features/app-users/app-users.module.css"; const columns = [ - { - key: "identity", - label: "用户", - width: "minmax(240px, 1.5fr)", - render: (user) => - }, - { key: "coin", label: "金币", width: "minmax(90px, 0.6fr)", render: (user) => formatNumber(user.coin) }, - { key: "diamond", label: "钻石", width: "minmax(90px, 0.6fr)", render: (user) => formatNumber(user.diamond) }, - { - key: "status", - label: "状态", - width: "minmax(92px, 0.6fr)", - render: (user) => - }, - { - key: "time", - label: "创建 / 活跃", - width: "minmax(180px, 1fr)", - render: (user) => ( -
- {formatMillis(user.createdAtMs)} - {formatMillis(user.lastActiveAtMs)} -
- ) - } + { + key: "identity", + label: "用户", + width: "minmax(240px, 1.5fr)", + render: (user) => , + }, + { key: "coin", label: "金币", width: "minmax(90px, 0.6fr)", render: (user) => formatNumber(user.coin) }, + { key: "diamond", label: "钻石", width: "minmax(90px, 0.6fr)", render: (user) => formatNumber(user.diamond) }, + { + key: "status", + label: "状态", + width: "minmax(92px, 0.6fr)", + render: (user) => , + }, + { + key: "time", + label: "创建 / 活跃", + width: "minmax(180px, 1fr)", + render: (user) => ( +
+ {formatMillis(user.createdAtMs)} + {formatMillis(user.lastActiveAtMs)} +
+ ), + }, ]; export function AppUserListPage() { - const page = useAppUsersPage(); - const items = page.data.items || []; - const total = page.data.total || 0; - const tableColumns = [ - columns[0], - { - key: "location", - label: "国家 / 区域", - width: "minmax(170px, 1fr)", - render: (user) => - }, - ...columns.slice(1), - { - key: "actions", - label: "操作", - width: "minmax(148px, 0.8fr)", - render: (user) => - } - ]; + const page = useAppUsersPage(); + const items = page.data.items || []; + const total = page.data.total || 0; + const tableColumns = [ + { + ...columns[0], + filter: createTextColumnFilter({ + placeholder: "搜索名称、短 ID、用户 ID", + value: page.query, + onChange: page.changeQuery, + }), + }, + { + key: "location", + label: "国家 / 区域", + width: "minmax(170px, 1fr)", + render: (user) => , + }, + ...columns.slice(1).map((column) => + column.key === "status" + ? { + ...column, + filter: createOptionsColumnFilter({ + options: appUserStatusFilters, + placeholder: "搜索状态", + value: page.status, + onChange: page.changeStatus, + }), + } + : column, + ), + { + key: "actions", + label: "操作", + width: "minmax(148px, 0.8fr)", + render: (user) => , + }, + ]; - return ( -
-
-
-
- - -
-
+ return ( +
+
+
+
+ +
+
- -
- user.userId} /> - {total > 0 ? : null} -
-
-
+ +
+ user.userId} + /> + {total > 0 ? ( + + ) : null} +
+
+
- - page.setEditForm({ ...page.editForm, username: event.target.value })} /> - page.setEditForm({ ...page.editForm, avatar })} /> - page.setEditForm({ ...page.editForm, gender: event.target.value })}> - {genderOptions.map(([value, label]) => ( - {label} - ))} - - + + page.setEditForm({ ...page.editForm, username: event.target.value })} + /> + page.setEditForm({ ...page.editForm, avatar })} + /> + page.setEditForm({ ...page.editForm, gender: event.target.value })} + > + {genderOptions.map(([value, label]) => ( + + {label} + + ))} + + - - option.label || ""} - isOptionEqualToValue={(option, value) => option.value === value.value} - loading={page.loadingCountries} - options={page.countryOptions} - renderInput={(params) => } - size="small" - value={page.countryOptions.find((option) => option.value === page.countryForm.country) || null} - onChange={(_, option) => page.setCountryForm({ country: option?.value || "" })} - /> - + + option.label || ""} + isOptionEqualToValue={(option, value) => option.value === value.value} + loading={page.loadingCountries} + options={page.countryOptions} + renderInput={(params) => } + size="small" + value={page.countryOptions.find((option) => option.value === page.countryForm.country) || null} + onChange={(_, option) => page.setCountryForm({ country: option?.value || "" })} + /> + - - page.setPasswordForm({ ...page.passwordForm, password: event.target.value })} /> - -
- ); + + page.setPasswordForm({ ...page.passwordForm, password: event.target.value })} + /> + + + ); } function UserLocation({ page, user }) { - const countryLabel = formatCountry(user); - const regionLabel = user.regionName || (user.regionId ? `区域 ${user.regionId}` : "-"); - return ( -
- {page.abilities.canUpdate ? ( - - ) : ( - {countryLabel} - )} - {regionLabel} -
- ); + const countryLabel = formatCountry(user); + const regionLabel = user.regionName || (user.regionId ? `区域 ${user.regionId}` : "-"); + return ( +
+ {page.abilities.canUpdate ? ( + + ) : ( + {countryLabel} + )} + {regionLabel} +
+ ); } function UserIdentity({ user }) { - const name = user.username || "-"; - const shortId = user.displayUserId || user.userId; - const gender = genderView(user.gender); - return ( -
- - {user.avatar ? : } - -
-
- {name} - {gender.symbol} + const name = user.username || "-"; + const shortId = user.displayUserId || user.userId; + const gender = genderView(user.gender); + return ( +
+ + {user.avatar ? : } + +
+
+ {name} + {gender.symbol} +
+
{shortId}
+
-
{shortId}
-
-
- ); + ); } function UserActions({ page, user }) { - const banned = user.status === "banned" || user.status === "disabled"; - return ( -
- {page.abilities.canUpdate ? ( - - - page.openEdit(user)}> - - - - - ) : null} - {page.abilities.canStatus ? ( - - - page.toggleBan(user)}> - {banned ? : } - - - - ) : null} - {page.abilities.canPassword ? ( - - - page.openPassword(user)}> - - - - - ) : null} -
- ); + const banned = user.status === "banned" || user.status === "disabled"; + return ( +
+ {page.abilities.canUpdate ? ( + + + page.openEdit(user)}> + + + + + ) : null} + {page.abilities.canStatus ? ( + + + page.toggleBan(user)} + > + {banned ? : } + + + + ) : null} + {page.abilities.canPassword ? ( + + + page.openPassword(user)}> + + + + + ) : null} +
+ ); } function UserStatus({ status }) { - const tone = status === "active" ? "running" : "danger"; - return ( - - - {appUserStatusLabels[status] || status || "-"} - - ); + const tone = status === "active" ? "running" : "danger"; + return ( + + + {appUserStatusLabels[status] || status || "-"} + + ); } function ActionModal({ children, disabled, loading, onClose, onSubmit, open, title }) { - return ( - - {children} - - ); + return ( + + {children} + + ); } function genderView(gender) { - const value = String(gender || "").toLowerCase(); - if (value === "male" || value === "m" || value === "男") { - return { className: "genderMale", symbol: "♂" }; - } - if (value === "female" || value === "f" || value === "女") { - return { className: "genderFemale", symbol: "♀" }; - } - return { className: "genderUnknown", symbol: "-" }; + const value = String(gender || "").toLowerCase(); + if (value === "male" || value === "m" || value === "男") { + return { className: "genderMale", symbol: "♂" }; + } + if (value === "female" || value === "f" || value === "女") { + return { className: "genderFemale", symbol: "♀" }; + } + return { className: "genderUnknown", symbol: "-" }; } function formatNumber(value) { - return Number(value || 0).toLocaleString("zh-CN"); + return Number(value || 0).toLocaleString("zh-CN"); } function formatCountry(user) { - return user.countryDisplayName || user.countryName || user.country || "-"; + return user.countryDisplayName || user.countryName || user.country || "-"; } diff --git a/src/features/host-org/api.test.ts b/src/features/host-org/api.test.ts index 2c9c22d..5194bf2 100644 --- a/src/features/host-org/api.test.ts +++ b/src/features/host-org/api.test.ts @@ -1,101 +1,118 @@ import { afterEach, expect, test, vi } from "vitest"; import { setAccessToken } from "@/shared/api/request"; import { - createCountry, - createCoinSeller, - createRegion, - disableRegion, - listAgencies, - listBDs, - listCoinSellers, - listCountries, - listHosts, - listRegions, - replaceRegionCountries, - setCoinSellerStatus, - updateCountry + createCountry, + createCoinSeller, + createRegion, + creditCoinSellerStock, + disableRegion, + listAgencies, + listBDs, + listCoinSellers, + listCountries, + listHosts, + listRegions, + replaceRegionCountries, + setCoinSellerStatus, + updateCountry, } from "./api"; afterEach(() => { - setAccessToken(""); - vi.unstubAllGlobals(); + setAccessToken(""); + vi.unstubAllGlobals(); }); test("host org list APIs use generated admin paths and filters", async () => { - vi.stubGlobal( - "fetch", - vi.fn(async () => new Response(JSON.stringify({ code: 0, data: { items: [], page: 1, pageSize: 10, total: 0 } }))) - ); + vi.stubGlobal( + "fetch", + vi.fn( + async () => new Response(JSON.stringify({ code: 0, data: { items: [], page: 1, pageSize: 10, total: 0 } })), + ), + ); - await listBDs({ page: 1, page_size: 10, parent_leader_user_id: 21, region_id: 7, status: "active" }); - await listAgencies({ keyword: "agency", page: 2, page_size: 10, parent_bd_user_id: 31 }); - await listHosts({ agency_id: 41, page: 1, page_size: 10 }); - await listCoinSellers({ page: 1, page_size: 10, region_id: 7, status: "active" }); - await createCoinSeller({ commandId: "coin-seller-test", reason: "contact", targetUserId: 1001 }); - await setCoinSellerStatus(1001, { commandId: "coin-seller-status-test", reason: "disable", status: "disabled" }); + await listBDs({ page: 1, page_size: 10, parent_leader_user_id: 21, region_id: 7, status: "active" }); + await listAgencies({ keyword: "agency", page: 2, page_size: 10, parent_bd_user_id: 31 }); + await listHosts({ agency_id: 41, page: 1, page_size: 10 }); + await listCoinSellers({ page: 1, page_size: 10, region_id: 7, status: "active" }); + await createCoinSeller({ commandId: "coin-seller-test", reason: "contact", targetUserId: 1001 }); + await setCoinSellerStatus(1001, { commandId: "coin-seller-status-test", reason: "disable", status: "disabled" }); + await creditCoinSellerStock(1001, { + coinAmount: 8000000, + commandId: "coin-seller-stock-test", + rechargeAmount: "100.000000", + type: "usdt_purchase", + }); - const [bdUrl, bdInit] = vi.mocked(fetch).mock.calls[0]; - const [agencyUrl] = vi.mocked(fetch).mock.calls[1]; - const [hostUrl] = vi.mocked(fetch).mock.calls[2]; - const [coinSellerUrl, coinSellerInit] = vi.mocked(fetch).mock.calls[3]; - const [coinSellerCreateUrl, coinSellerCreateInit] = vi.mocked(fetch).mock.calls[4]; - const [coinSellerStatusUrl, coinSellerStatusInit] = vi.mocked(fetch).mock.calls[5]; + const [bdUrl, bdInit] = vi.mocked(fetch).mock.calls[0]; + const [agencyUrl] = vi.mocked(fetch).mock.calls[1]; + const [hostUrl] = vi.mocked(fetch).mock.calls[2]; + const [coinSellerUrl, coinSellerInit] = vi.mocked(fetch).mock.calls[3]; + const [coinSellerCreateUrl, coinSellerCreateInit] = vi.mocked(fetch).mock.calls[4]; + const [coinSellerStatusUrl, coinSellerStatusInit] = vi.mocked(fetch).mock.calls[5]; + const [coinSellerStockUrl, coinSellerStockInit] = vi.mocked(fetch).mock.calls[6]; - expect(String(bdUrl)).toContain("/api/v1/admin/bds?"); - expect(String(bdUrl)).toContain("parent_leader_user_id=21"); - expect(String(bdUrl)).toContain("region_id=7"); - expect(String(bdUrl)).toContain("status=active"); - expect(bdInit?.method).toBe("GET"); - expect(String(agencyUrl)).toContain("/api/v1/admin/agencies?"); - expect(String(agencyUrl)).toContain("parent_bd_user_id=31"); - expect(String(hostUrl)).toContain("/api/v1/admin/hosts?"); - expect(String(hostUrl)).toContain("agency_id=41"); - expect(String(coinSellerUrl)).toContain("/api/v1/admin/coin-sellers?"); - expect(String(coinSellerUrl)).toContain("region_id=7"); - expect(String(coinSellerUrl)).toContain("status=active"); - expect(coinSellerInit?.method).toBe("GET"); - expect(String(coinSellerCreateUrl)).toContain("/api/v1/admin/coin-sellers"); - expect(coinSellerCreateInit?.method).toBe("POST"); - expect(String(coinSellerStatusUrl)).toContain("/api/v1/admin/coin-sellers/1001/status"); - expect(coinSellerStatusInit?.method).toBe("PATCH"); + expect(String(bdUrl)).toContain("/api/v1/admin/bds?"); + expect(String(bdUrl)).toContain("parent_leader_user_id=21"); + expect(String(bdUrl)).toContain("region_id=7"); + expect(String(bdUrl)).toContain("status=active"); + expect(bdInit?.method).toBe("GET"); + expect(String(agencyUrl)).toContain("/api/v1/admin/agencies?"); + expect(String(agencyUrl)).toContain("parent_bd_user_id=31"); + expect(String(hostUrl)).toContain("/api/v1/admin/hosts?"); + expect(String(hostUrl)).toContain("agency_id=41"); + expect(String(coinSellerUrl)).toContain("/api/v1/admin/coin-sellers?"); + expect(String(coinSellerUrl)).toContain("region_id=7"); + expect(String(coinSellerUrl)).toContain("status=active"); + expect(coinSellerInit?.method).toBe("GET"); + expect(String(coinSellerCreateUrl)).toContain("/api/v1/admin/coin-sellers"); + expect(coinSellerCreateInit?.method).toBe("POST"); + expect(String(coinSellerStatusUrl)).toContain("/api/v1/admin/coin-sellers/1001/status"); + expect(coinSellerStatusInit?.method).toBe("PATCH"); + expect(String(coinSellerStockUrl)).toContain("/api/v1/admin/coin-sellers/1001/stock-credits"); + expect(coinSellerStockInit?.method).toBe("POST"); }); test("country and region APIs use generated admin paths", async () => { - vi.stubGlobal( - "fetch", - vi.fn(async () => new Response(JSON.stringify({ code: 0, data: { items: [], total: 0 } }))) - ); + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response(JSON.stringify({ code: 0, data: { items: [], total: 0 } }))), + ); - await listCountries({ enabled: true }); - await createCountry({ countryCode: "US", countryDisplayName: "United States", countryName: "United States", enabled: true }); - await updateCountry(12, { countryDisplayName: "United States", countryName: "United States" }); - await listRegions({ status: "active" }); - await createRegion({ countries: ["US"], name: "North America", regionCode: "NA" }); - await replaceRegionCountries(7, { countries: ["US", "CA"] }); - await disableRegion(7); + await listCountries({ enabled: true }); + await createCountry({ + countryCode: "US", + countryDisplayName: "United States", + countryName: "United States", + enabled: true, + }); + await updateCountry(12, { countryDisplayName: "United States", countryName: "United States" }); + await listRegions({ status: "active" }); + await createRegion({ countries: ["US"], name: "North America", regionCode: "NA" }); + await replaceRegionCountries(7, { countries: ["US", "CA"] }); + await disableRegion(7); - const [countryListUrl, countryListInit] = vi.mocked(fetch).mock.calls[0]; - const [countryCreateUrl, countryCreateInit] = vi.mocked(fetch).mock.calls[1]; - const [countryUpdateUrl, countryUpdateInit] = vi.mocked(fetch).mock.calls[2]; - const [regionListUrl, regionListInit] = vi.mocked(fetch).mock.calls[3]; - const [regionCreateUrl, regionCreateInit] = vi.mocked(fetch).mock.calls[4]; - const [regionCountriesUrl, regionCountriesInit] = vi.mocked(fetch).mock.calls[5]; - const [regionDisableUrl, regionDisableInit] = vi.mocked(fetch).mock.calls[6]; + const [countryListUrl, countryListInit] = vi.mocked(fetch).mock.calls[0]; + const [countryCreateUrl, countryCreateInit] = vi.mocked(fetch).mock.calls[1]; + const [countryUpdateUrl, countryUpdateInit] = vi.mocked(fetch).mock.calls[2]; + const [regionListUrl, regionListInit] = vi.mocked(fetch).mock.calls[3]; + const [regionCreateUrl, regionCreateInit] = vi.mocked(fetch).mock.calls[4]; + const [regionCountriesUrl, regionCountriesInit] = vi.mocked(fetch).mock.calls[5]; + const [regionDisableUrl, regionDisableInit] = vi.mocked(fetch).mock.calls[6]; - expect(String(countryListUrl)).toContain("/api/v1/admin/countries?"); - expect(String(countryListUrl)).toContain("enabled=true"); - expect(countryListInit?.method).toBe("GET"); - expect(String(countryCreateUrl)).toContain("/api/v1/admin/countries"); - expect(countryCreateInit?.method).toBe("POST"); - expect(String(countryUpdateUrl)).toContain("/api/v1/admin/countries/12"); - expect(countryUpdateInit?.method).toBe("PATCH"); - expect(String(regionListUrl)).toContain("/api/v1/admin/regions?"); - expect(String(regionListUrl)).toContain("status=active"); - expect(regionListInit?.method).toBe("GET"); - expect(String(regionCreateUrl)).toContain("/api/v1/admin/regions"); - expect(regionCreateInit?.method).toBe("POST"); - expect(String(regionCountriesUrl)).toContain("/api/v1/admin/regions/7/countries"); - expect(regionCountriesInit?.method).toBe("PUT"); - expect(String(regionDisableUrl)).toContain("/api/v1/admin/regions/7"); - expect(regionDisableInit?.method).toBe("DELETE"); + expect(String(countryListUrl)).toContain("/api/v1/admin/countries?"); + expect(String(countryListUrl)).toContain("enabled=true"); + expect(countryListInit?.method).toBe("GET"); + expect(String(countryCreateUrl)).toContain("/api/v1/admin/countries"); + expect(countryCreateInit?.method).toBe("POST"); + expect(String(countryUpdateUrl)).toContain("/api/v1/admin/countries/12"); + expect(countryUpdateInit?.method).toBe("PATCH"); + expect(String(regionListUrl)).toContain("/api/v1/admin/regions?"); + expect(String(regionListUrl)).toContain("status=active"); + expect(regionListInit?.method).toBe("GET"); + expect(String(regionCreateUrl)).toContain("/api/v1/admin/regions"); + expect(regionCreateInit?.method).toBe("POST"); + expect(String(regionCountriesUrl)).toContain("/api/v1/admin/regions/7/countries"); + expect(regionCountriesInit?.method).toBe("PUT"); + expect(String(regionDisableUrl)).toContain("/api/v1/admin/regions/7"); + expect(regionDisableInit?.method).toBe("DELETE"); }); diff --git a/src/features/host-org/api.ts b/src/features/host-org/api.ts index abf24c7..b285663 100644 --- a/src/features/host-org/api.ts +++ b/src/features/host-org/api.ts @@ -9,6 +9,8 @@ import type { BDProfileDto, BDStatusPayload, CoinSellerDto, + CoinSellerStockCreditDto, + CoinSellerStockCreditPayload, CoinSellerStatusPayload, CountryDto, CountryPayload, @@ -25,21 +27,21 @@ import type { RegionCountriesPayload, RegionDto, RegionPayload, - RegionUpdatePayload + RegionUpdatePayload, } from "@/shared/api/types"; export function listCountries(query: PageQuery = {}): Promise> { const endpoint = API_ENDPOINTS.listCountries; return apiRequest>(apiEndpointPath(API_OPERATIONS.listCountries), { method: endpoint.method, - query + query, }); } export function getCountry(countryId: EntityId): Promise { const endpoint = API_ENDPOINTS.getCountry; return apiRequest(apiEndpointPath(API_OPERATIONS.getCountry, { country_id: countryId }), { - method: endpoint.method + method: endpoint.method, }); } @@ -47,43 +49,46 @@ export function createCountry(payload: CountryPayload): Promise { const endpoint = API_ENDPOINTS.createCountry; return apiRequest(apiEndpointPath(API_OPERATIONS.createCountry), { body: payload, - method: endpoint.method + method: endpoint.method, }); } export function updateCountry(countryId: EntityId, payload: CountryUpdatePayload): Promise { const endpoint = API_ENDPOINTS.updateCountry; - return apiRequest(apiEndpointPath(API_OPERATIONS.updateCountry, { country_id: countryId }), { - body: payload, - method: endpoint.method - }); + return apiRequest( + apiEndpointPath(API_OPERATIONS.updateCountry, { country_id: countryId }), + { + body: payload, + method: endpoint.method, + }, + ); } export function enableCountry(countryId: EntityId): Promise { const endpoint = API_ENDPOINTS.enableCountry; return apiRequest(apiEndpointPath(API_OPERATIONS.enableCountry, { country_id: countryId }), { - method: endpoint.method + method: endpoint.method, }); } export function disableCountry(countryId: EntityId): Promise { const endpoint = API_ENDPOINTS.disableCountry; return apiRequest(apiEndpointPath(API_OPERATIONS.disableCountry, { country_id: countryId }), { - method: endpoint.method + method: endpoint.method, }); } export function deleteCountry(countryId: EntityId): Promise { const endpoint = API_ENDPOINTS.deleteCountry; return apiRequest(apiEndpointPath(API_OPERATIONS.deleteCountry, { country_id: countryId }), { - method: endpoint.method + method: endpoint.method, }); } export function getRegion(regionId: EntityId): Promise { const endpoint = API_ENDPOINTS.getRegion; return apiRequest(apiEndpointPath(API_OPERATIONS.getRegion, { region_id: regionId }), { - method: endpoint.method + method: endpoint.method, }); } @@ -91,16 +96,19 @@ export function createRegion(payload: RegionPayload): Promise { const endpoint = API_ENDPOINTS.createRegion; return apiRequest(apiEndpointPath(API_OPERATIONS.createRegion), { body: payload, - method: endpoint.method + method: endpoint.method, }); } export function updateRegion(regionId: EntityId, payload: RegionUpdatePayload): Promise { const endpoint = API_ENDPOINTS.updateRegion; - return apiRequest(apiEndpointPath(API_OPERATIONS.updateRegion, { region_id: regionId }), { - body: payload, - method: endpoint.method - }); + return apiRequest( + apiEndpointPath(API_OPERATIONS.updateRegion, { region_id: regionId }), + { + body: payload, + method: endpoint.method, + }, + ); } export function replaceRegionCountries(regionId: EntityId, payload: RegionCountriesPayload): Promise { @@ -109,22 +117,22 @@ export function replaceRegionCountries(regionId: EntityId, payload: RegionCountr apiEndpointPath(API_OPERATIONS.replaceRegionCountries, { region_id: regionId }), { body: payload, - method: endpoint.method - } + method: endpoint.method, + }, ); } export function enableRegion(regionId: EntityId): Promise { const endpoint = API_ENDPOINTS.enableRegion; return apiRequest(apiEndpointPath(API_OPERATIONS.enableRegion, { region_id: regionId }), { - method: endpoint.method + method: endpoint.method, }); } export function disableRegion(regionId: EntityId): Promise { const endpoint = API_ENDPOINTS.disableRegion; return apiRequest(apiEndpointPath(API_OPERATIONS.disableRegion, { region_id: regionId }), { - method: endpoint.method + method: endpoint.method, }); } @@ -132,7 +140,7 @@ export function listBDLeaders(query: PageQuery = {}): Promise>(apiEndpointPath(API_OPERATIONS.listBDLeaders), { method: endpoint.method, - query + query, }); } @@ -140,7 +148,7 @@ export function listBDs(query: PageQuery = {}): Promise> { const endpoint = API_ENDPOINTS.listBDs; return apiRequest>(apiEndpointPath(API_OPERATIONS.listBDs), { method: endpoint.method, - query + query, }); } @@ -148,7 +156,7 @@ export function listAgencies(query: PageQuery = {}): Promise> const endpoint = API_ENDPOINTS.listAgencies; return apiRequest>(apiEndpointPath(API_OPERATIONS.listAgencies), { method: endpoint.method, - query + query, }); } @@ -156,7 +164,7 @@ export function listHosts(query: PageQuery = {}): Promise>(apiEndpointPath(API_OPERATIONS.listHosts), { method: endpoint.method, - query + query, }); } @@ -164,7 +172,7 @@ export function listCoinSellers(query: PageQuery = {}): Promise>(apiEndpointPath(API_OPERATIONS.listCoinSellers), { method: endpoint.method, - query + query, }); } @@ -172,7 +180,7 @@ export function createBDLeader(payload: CreateBDLeaderPayload): Promise(apiEndpointPath(API_OPERATIONS.createBDLeader), { body: payload, - method: endpoint.method + method: endpoint.method, }); } @@ -180,23 +188,32 @@ export function createBD(payload: CreateBDPayload): Promise { const endpoint = API_ENDPOINTS.createBD; return apiRequest(apiEndpointPath(API_OPERATIONS.createBD), { body: payload, - method: endpoint.method + method: endpoint.method, + }); +} + +export function deleteBDLeader(userId: EntityId): Promise { + return apiRequest(`/v1/admin/bd-leaders/${encodeURIComponent(String(userId))}`, { + method: "DELETE", }); } export function setBDLeaderStatus(userId: EntityId, payload: BDStatusPayload): Promise { const endpoint = API_ENDPOINTS.setBDLeaderStatus; - return apiRequest(apiEndpointPath(API_OPERATIONS.setBDLeaderStatus, { user_id: userId }), { - body: payload, - method: endpoint.method - }); + return apiRequest( + apiEndpointPath(API_OPERATIONS.setBDLeaderStatus, { user_id: userId }), + { + body: payload, + method: endpoint.method, + }, + ); } export function setBDStatus(userId: EntityId, payload: BDStatusPayload): Promise { const endpoint = API_ENDPOINTS.setBDStatus; return apiRequest(apiEndpointPath(API_OPERATIONS.setBDStatus, { user_id: userId }), { body: payload, - method: endpoint.method + method: endpoint.method, }); } @@ -204,7 +221,7 @@ export function createCoinSeller(payload: CreateCoinSellerPayload): Promise(apiEndpointPath(API_OPERATIONS.createCoinSeller), { body: payload, - method: endpoint.method + method: endpoint.method, }); } @@ -214,8 +231,22 @@ export function setCoinSellerStatus(userId: EntityId, payload: CoinSellerStatusP apiEndpointPath(API_OPERATIONS.setCoinSellerStatus, { user_id: userId }), { body: payload, - method: endpoint.method - } + method: endpoint.method, + }, + ); +} + +export function creditCoinSellerStock( + userId: EntityId, + payload: CoinSellerStockCreditPayload, +): Promise { + const endpoint = API_ENDPOINTS.creditCoinSellerStock; + return apiRequest( + apiEndpointPath(API_OPERATIONS.creditCoinSellerStock, { user_id: userId }), + { + body: payload, + method: endpoint.method, + }, ); } @@ -223,16 +254,19 @@ export function createAgency(payload: CreateAgencyPayload): Promise(apiEndpointPath(API_OPERATIONS.createAgency), { body: payload, - method: endpoint.method + method: endpoint.method, }); } export function closeAgency(agencyId: EntityId, payload: HostCommandPayload): Promise { const endpoint = API_ENDPOINTS.closeAgency; - return apiRequest(apiEndpointPath(API_OPERATIONS.closeAgency, { agency_id: agencyId }), { - body: payload, - method: endpoint.method - }); + return apiRequest( + apiEndpointPath(API_OPERATIONS.closeAgency, { agency_id: agencyId }), + { + body: payload, + method: endpoint.method, + }, + ); } export function setAgencyJoinEnabled(agencyId: EntityId, payload: AgencyJoinEnabledPayload): Promise { @@ -241,7 +275,7 @@ export function setAgencyJoinEnabled(agencyId: EntityId, payload: AgencyJoinEnab apiEndpointPath(API_OPERATIONS.setAgencyJoinEnabled, { agency_id: agencyId }), { body: payload, - method: endpoint.method - } + method: endpoint.method, + }, ); } diff --git a/src/features/host-org/components/HostOrgActionModal.jsx b/src/features/host-org/components/HostOrgActionModal.jsx index 6a38015..4f89886 100644 --- a/src/features/host-org/components/HostOrgActionModal.jsx +++ b/src/features/host-org/components/HostOrgActionModal.jsx @@ -7,6 +7,7 @@ export function HostOrgActionModal({ onClose, onSubmit, open, + size = "compact", submitLabel = "提交", submitVariant = "primary", title @@ -15,7 +16,7 @@ export function HostOrgActionModal({ - - -
- - {extraFilters.map((filter) => ( - filter.onChange(event.target.value)} - /> - ))} -
-
- ); + const hasActiveFilters = [query, regionId, status, ...extraFilters.map((filter) => filter.value)].some( + (value) => String(value || "").trim() !== "", + ); + + return ( +
+ {onReset ? : null} +
+ ); } diff --git a/src/features/host-org/components/HostOrgIdentity.jsx b/src/features/host-org/components/HostOrgIdentity.jsx new file mode 100644 index 0000000..dd432d6 --- /dev/null +++ b/src/features/host-org/components/HostOrgIdentity.jsx @@ -0,0 +1,47 @@ +import styles from "@/features/host-org/host-org.module.css"; + +export function HostOrgPerson({ avatar, displayUserId, fallbackId, username }) { + const name = username || "-"; + const meta = displayUserId || fallbackId || "-"; + + return ( +
+ {avatar ? ( + + ) : ( + + {String(name || meta) + .slice(0, 1) + .toUpperCase()} + + )} + + {name} + {meta} + +
+ ); +} + +export function HostOrgEntity({ id, name, prefix = "" }) { + if (!id && !name) { + return "-"; + } + return ( + + {name || "-"} + + {prefix} + {id || "-"} + + + ); +} + +export function hostOrgRegionName(item, regionOptions = []) { + if (item.regionName) { + return item.regionName; + } + const option = regionOptions.find((region) => String(region.value) === String(item.regionId)); + return option?.name || item.regionId || "-"; +} diff --git a/src/features/host-org/components/HostOrgTable.jsx b/src/features/host-org/components/HostOrgTable.jsx index a4f434c..6dc7c12 100644 --- a/src/features/host-org/components/HostOrgTable.jsx +++ b/src/features/host-org/components/HostOrgTable.jsx @@ -1,9 +1,9 @@ import { DataTable } from "@/shared/ui/DataTable.jsx"; export function HostOrgTable({ columns, regionFilter, ...props }) { - const tableColumns = regionFilter - ? columns.map((column) => (column.key === "regionId" ? { ...column, filter: regionFilter } : column)) - : columns; + const tableColumns = regionFilter + ? columns.map((column) => (column.key === "regionId" ? { ...column, filter: regionFilter } : column)) + : columns; - return ; + return ; } diff --git a/src/features/host-org/hooks/useHostAgenciesPage.js b/src/features/host-org/hooks/useHostAgenciesPage.js index 1715eb6..ccbb057 100644 --- a/src/features/host-org/hooks/useHostAgenciesPage.js +++ b/src/features/host-org/hooks/useHostAgenciesPage.js @@ -2,177 +2,182 @@ import { useMemo, useState } from "react"; import { parseForm } from "@/shared/forms/validation"; import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; -import { - closeAgency, - createAgency, - listAgencies, - setAgencyJoinEnabled -} from "@/features/host-org/api"; +import { closeAgency, createAgency, listAgencies, setAgencyJoinEnabled } from "@/features/host-org/api"; import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js"; import { useAgencyAbilities } from "@/features/host-org/permissions.js"; -import { - agencyCloseSchema, - agencyJoinEnabledSchema, - createAgencySchema -} from "@/features/host-org/schema"; +import { agencyCloseSchema, agencyJoinEnabledSchema, createAgencySchema } from "@/features/host-org/schema"; const emptyAgencyForm = () => ({ - commandId: makeCommandId("agency"), - joinEnabled: true, - maxHosts: 0, - name: "", - ownerUserId: "", - parentBdUserId: "", - reason: "" + commandId: makeCommandId("agency"), + joinEnabled: true, + maxHosts: 0, + name: "", + ownerUserId: "", + parentBdUserId: "", + reason: "", }); const emptyCloseForm = () => ({ agencyId: "", commandId: makeCommandId("agency-close"), reason: "" }); const emptyJoinEnabledForm = () => ({ - agencyId: "", - commandId: makeCommandId("agency-join"), - joinEnabled: true, - reason: "" + agencyId: "", + commandId: makeCommandId("agency-join"), + joinEnabled: true, + reason: "", }); const pageSize = 10; const emptyData = { items: [], page: 1, pageSize, total: 0 }; export function useHostAgenciesPage() { - const abilities = useAgencyAbilities(); - const { showToast } = useToast(); - const { loadingRegions, regionOptions } = useRegionOptions(); - const [query, setQuery] = useState(""); - const [status, setStatus] = useState(""); - const [regionId, setRegionId] = useState(""); - const [parentBdUserId, setParentBdUserId] = useState(""); - const [page, setPage] = useState(1); - const [agencyForm, setAgencyForm] = useState(emptyAgencyForm); - const [closeForm, setCloseForm] = useState(emptyCloseForm); - const [joinEnabledForm, setJoinEnabledForm] = useState(emptyJoinEnabledForm); - const [loadingAction, setLoadingAction] = useState(""); - const [activeAction, setActiveAction] = useState(""); - const filters = useMemo( - () => ({ - keyword: query, - parent_bd_user_id: parentBdUserId, - region_id: regionId, - status - }), - [parentBdUserId, query, regionId, status] - ); - const { data = emptyData, error, loading, reload } = usePaginatedQuery({ - errorMessage: "加载 Agency 列表失败", - fetcher: listAgencies, - filters, - page, - pageSize, - queryKey: ["host-org", "agencies", filters, page] - }); - - const changeQuery = (value) => { - setQuery(value); - setPage(1); - }; - - const changeStatus = (value) => { - setStatus(value); - setPage(1); - }; - - const changeRegionId = (value) => { - setRegionId(value); - setPage(1); - }; - - const changeParentBdUserId = (value) => { - setParentBdUserId(value); - setPage(1); - }; - - const submitAgency = async (event) => { - event.preventDefault(); - await runAction("agency", "Agency 已创建", async () => { - const payload = parseForm(createAgencySchema, agencyForm); - const data = await createAgency(payload); - setAgencyForm(emptyAgencyForm()); - setActiveAction(""); - await reload(); - return data; + const abilities = useAgencyAbilities(); + const { showToast } = useToast(); + const { loadingRegions, regionOptions } = useRegionOptions(); + const [query, setQuery] = useState(""); + const [status, setStatus] = useState(""); + const [regionId, setRegionId] = useState(""); + const [parentBdUserId, setParentBdUserId] = useState(""); + const [page, setPage] = useState(1); + const [agencyForm, setAgencyForm] = useState(emptyAgencyForm); + const [closeForm, setCloseForm] = useState(emptyCloseForm); + const [joinEnabledForm, setJoinEnabledForm] = useState(emptyJoinEnabledForm); + const [loadingAction, setLoadingAction] = useState(""); + const [activeAction, setActiveAction] = useState(""); + const filters = useMemo( + () => ({ + keyword: query, + parent_bd_user_id: parentBdUserId, + region_id: regionId, + status, + }), + [parentBdUserId, query, regionId, status], + ); + const { + data = emptyData, + error, + loading, + reload, + } = usePaginatedQuery({ + errorMessage: "加载 Agency 列表失败", + fetcher: listAgencies, + filters, + page, + pageSize, + queryKey: ["host-org", "agencies", filters, page], }); - }; - const submitClose = async (event) => { - event.preventDefault(); - await runAction("agency-close", "Agency 已关闭", async () => { - const payload = parseForm(agencyCloseSchema, closeForm); - const { agencyId, ...body } = payload; - const data = await closeAgency(agencyId, body); - setCloseForm(emptyCloseForm()); - setActiveAction(""); - await reload(); - return data; - }); - }; + const changeQuery = (value) => { + setQuery(value); + setPage(1); + }; - const submitJoinEnabled = async (event) => { - event.preventDefault(); - await runAction("agency-join", "入会开关已更新", async () => { - const payload = parseForm(agencyJoinEnabledSchema, joinEnabledForm); - const { agencyId, ...body } = payload; - const data = await setAgencyJoinEnabled(agencyId, body); - setJoinEnabledForm(emptyJoinEnabledForm()); - setActiveAction(""); - await reload(); - return data; - }); - }; + const changeStatus = (value) => { + setStatus(value); + setPage(1); + }; - const runAction = async (action, successMessage, submitter) => { - setLoadingAction(action); - try { - await submitter(); - showToast(successMessage, "success"); - } catch (err) { - showToast(err.message || "操作失败", "error"); - } finally { - setLoadingAction(""); - } - }; + const changeRegionId = (value) => { + setRegionId(value); + setPage(1); + }; - return { - abilities, - activeAction, - agencyForm, - changeParentBdUserId, - changeQuery, - changeRegionId, - changeStatus, - closeForm, - data, - error, - joinEnabledForm, - loading, - loadingAction, - loadingRegions, - page, - parentBdUserId, - query, - regionId, - regionOptions, - reload, - closeAction: () => setActiveAction(""), - openAgencyForm: () => setActiveAction("agency"), - openCloseForm: () => setActiveAction("agency-close"), - openJoinEnabledForm: () => setActiveAction("agency-join"), - setAgencyForm, - setCloseForm, - setJoinEnabledForm, - setPage, - status, - submitAgency, - submitClose, - submitJoinEnabled - }; + const changeParentBdUserId = (value) => { + setParentBdUserId(value); + setPage(1); + }; + + const resetFilters = () => { + setQuery(""); + setStatus(""); + setRegionId(""); + setParentBdUserId(""); + setPage(1); + }; + + const submitAgency = async (event) => { + event.preventDefault(); + await runAction("agency", "Agency 已创建", async () => { + const payload = parseForm(createAgencySchema, agencyForm); + const data = await createAgency(payload); + setAgencyForm(emptyAgencyForm()); + setActiveAction(""); + await reload(); + return data; + }); + }; + + const submitClose = async (event) => { + event.preventDefault(); + await runAction("agency-close", "Agency 已关闭", async () => { + const payload = parseForm(agencyCloseSchema, closeForm); + const { agencyId, ...body } = payload; + const data = await closeAgency(agencyId, body); + setCloseForm(emptyCloseForm()); + setActiveAction(""); + await reload(); + return data; + }); + }; + + const submitJoinEnabled = async (event) => { + event.preventDefault(); + await runAction("agency-join", "入会开关已更新", async () => { + const payload = parseForm(agencyJoinEnabledSchema, joinEnabledForm); + const { agencyId, ...body } = payload; + const data = await setAgencyJoinEnabled(agencyId, body); + setJoinEnabledForm(emptyJoinEnabledForm()); + setActiveAction(""); + await reload(); + return data; + }); + }; + + const runAction = async (action, successMessage, submitter) => { + setLoadingAction(action); + try { + await submitter(); + showToast(successMessage, "success"); + } catch (err) { + showToast(err.message || "操作失败", "error"); + } finally { + setLoadingAction(""); + } + }; + + return { + abilities, + activeAction, + agencyForm, + changeParentBdUserId, + changeQuery, + changeRegionId, + changeStatus, + closeForm, + data, + error, + joinEnabledForm, + loading, + loadingAction, + loadingRegions, + page, + parentBdUserId, + query, + regionId, + regionOptions, + reload, + resetFilters, + closeAction: () => setActiveAction(""), + openAgencyForm: () => setActiveAction("agency"), + openCloseForm: () => setActiveAction("agency-close"), + openJoinEnabledForm: () => setActiveAction("agency-join"), + setAgencyForm, + setCloseForm, + setJoinEnabledForm, + setPage, + status, + submitAgency, + submitClose, + submitJoinEnabled, + }; } function makeCommandId(prefix) { - return `${prefix}-${Date.now()}`; + return `${prefix}-${Date.now()}`; } diff --git a/src/features/host-org/hooks/useHostBdsPage.js b/src/features/host-org/hooks/useHostBdsPage.js index dfe3371..0194759 100644 --- a/src/features/host-org/hooks/useHostBdsPage.js +++ b/src/features/host-org/hooks/useHostBdsPage.js @@ -1,166 +1,344 @@ -import { useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { parseForm } from "@/shared/forms/validation"; import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js"; +import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; import { - createBD, - createBDLeader, - listBDLeaders, - listBDs, - setBDLeaderStatus, - setBDStatus + createBD, + createBDLeader, + deleteBDLeader, + listBDLeaders, + listBDs, + setBDLeaderStatus, + setBDStatus, } from "@/features/host-org/api"; +import { listAppUsers } from "@/features/app-users/api"; import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js"; import { useBDAbilities } from "@/features/host-org/permissions.js"; import { bdStatusSchema, createBDSchema, createBDLeaderSchema } from "@/features/host-org/schema"; const emptyBDLeaderForm = () => ({ commandId: makeCommandId("bd-leader"), reason: "", regionId: "", targetUserId: "" }); const emptyBDForm = () => ({ commandId: makeCommandId("bd"), parentLeaderUserId: "", reason: "", targetUserId: "" }); -const emptyBDStatusForm = (targetType = "bd") => ({ commandId: makeCommandId("bd-status"), reason: "", status: "active", targetType, targetUserId: "" }); +const emptyBDStatusForm = (targetType = "bd") => ({ + commandId: makeCommandId("bd-status"), + reason: "", + status: "active", + targetType, + targetUserId: "", +}); const pageSize = 10; const emptyData = { items: [], page: 1, pageSize, total: 0 }; +const emptyLookup = () => ({ error: "", loading: false, user: null }); export function useHostBdsPage({ profileType = "bd" } = {}) { - const abilities = useBDAbilities(); - const { showToast } = useToast(); - const { loadingRegions, regionOptions } = useRegionOptions(); - const [query, setQuery] = useState(""); - const [status, setStatus] = useState(""); - const [regionId, setRegionId] = useState(""); - const [parentLeaderUserId, setParentLeaderUserId] = useState(""); - const [page, setPage] = useState(1); - const [bdLeaderForm, setBDLeaderForm] = useState(emptyBDLeaderForm); - const [bdForm, setBDForm] = useState(emptyBDForm); - const [bdStatusForm, setBDStatusForm] = useState(() => emptyBDStatusForm(profileType)); - const [loadingAction, setLoadingAction] = useState(""); - const [activeAction, setActiveAction] = useState(""); - const filters = useMemo( - () => ({ - keyword: query, - parent_leader_user_id: profileType === "bd" ? parentLeaderUserId : "", - region_id: regionId, - status - }), - [parentLeaderUserId, profileType, query, regionId, status] - ); - const fetcher = profileType === "bd" ? listBDs : listBDLeaders; - const { data = emptyData, error, loading, reload } = usePaginatedQuery({ - errorMessage: profileType === "leader" ? "加载 BD Leader 列表失败" : "加载 BD 列表失败", - fetcher, - filters, - page, - pageSize, - queryKey: ["host-org", "bds", profileType, filters, page] - }); - - const changeQuery = (value) => { - setQuery(value); - setPage(1); - }; - - const changeStatus = (value) => { - setStatus(value); - setPage(1); - }; - - const changeRegionId = (value) => { - setRegionId(value); - setPage(1); - }; - - const changeParentLeaderUserId = (value) => { - setParentLeaderUserId(value); - setPage(1); - }; - - const submitBDLeader = async (event) => { - event.preventDefault(); - await runAction("bd-leader", "BD Leader 已创建", async () => { - const payload = parseForm(createBDLeaderSchema, bdLeaderForm); - const data = await createBDLeader(payload); - setBDLeaderForm(emptyBDLeaderForm()); - setActiveAction(""); - await reload(); - return data; + const abilities = useBDAbilities(); + const confirm = useConfirm(); + const { showToast } = useToast(); + const { loadingRegions, regionOptions } = useRegionOptions(); + const [query, setQuery] = useState(""); + const [status, setStatus] = useState(""); + const [regionId, setRegionId] = useState(""); + const [parentLeaderUserId, setParentLeaderUserId] = useState(""); + const [page, setPage] = useState(1); + const [bdLeaderForm, setBDLeaderForm] = useState(emptyBDLeaderForm); + const [bdForm, setBDForm] = useState(emptyBDForm); + const [bdStatusForm, setBDStatusForm] = useState(() => emptyBDStatusForm(profileType)); + const [bdUserLookup, setBDUserLookup] = useState(emptyLookup); + const [leaderBDs, setLeaderBDs] = useState(emptyData); + const [leaderBDsError, setLeaderBDsError] = useState(""); + const [leaderBDsLoading, setLeaderBDsLoading] = useState(false); + const [selectedLeader, setSelectedLeader] = useState(null); + const [loadingAction, setLoadingAction] = useState(""); + const [activeAction, setActiveAction] = useState(""); + const filters = useMemo( + () => ({ + keyword: query, + parent_leader_user_id: profileType === "bd" ? parentLeaderUserId : "", + region_id: regionId, + status, + }), + [parentLeaderUserId, profileType, query, regionId, status], + ); + const fetcher = profileType === "bd" ? listBDs : listBDLeaders; + const { + data = emptyData, + error, + loading, + reload, + } = usePaginatedQuery({ + errorMessage: profileType === "leader" ? "加载 BD Leader 列表失败" : "加载 BD 列表失败", + fetcher, + filters, + page, + pageSize, + queryKey: ["host-org", "bds", profileType, filters, page], }); - }; - const submitBD = async (event) => { - event.preventDefault(); - await runAction("bd", "BD 已创建", async () => { - const payload = parseForm(createBDSchema, bdForm); - const data = await createBD(payload); - setBDForm(emptyBDForm()); - setActiveAction(""); - await reload(); - return data; - }); - }; + const loadLeaderBDs = useCallback(async (leader) => { + if (!leader?.userId) { + setLeaderBDs(emptyData); + return; + } + setLeaderBDsError(""); + setLeaderBDsLoading(true); + try { + const data = await listBDs({ + page: 1, + page_size: 100, + parent_leader_user_id: leader.userId, + }); + setLeaderBDs(data || emptyData); + } catch (err) { + setLeaderBDsError(err.message || "加载下级 BD 失败"); + } finally { + setLeaderBDsLoading(false); + } + }, []); - const submitBDStatus = async (event) => { - event.preventDefault(); - await runAction("bd-status", profileType === "leader" ? "BD Leader 状态已更新" : "BD 状态已更新", async () => { - const payload = parseForm(bdStatusSchema, { ...bdStatusForm, targetType: profileType }); - const { targetType, targetUserId, ...body } = payload; - const data = targetType === "leader" - ? await setBDLeaderStatus(targetUserId, body) - : await setBDStatus(targetUserId, body); - setBDStatusForm(emptyBDStatusForm(profileType)); - setActiveAction(""); - await reload(); - return data; - }); - }; + useEffect(() => { + if (activeAction !== "leader-add-bd") { + return undefined; + } - const runAction = async (action, successMessage, submitter) => { - setLoadingAction(action); - try { - await submitter(); - showToast(successMessage, "success"); - } catch (err) { - showToast(err.message || "操作失败", "error"); - } finally { - setLoadingAction(""); - } - }; + const keyword = String(bdForm.targetUserId || "").trim(); + if (!keyword) { + setBDUserLookup(emptyLookup()); + return undefined; + } - return { - abilities, - activeAction, - bdForm, - bdLeaderForm, - bdStatusForm, - changeParentLeaderUserId, - changeQuery, - changeRegionId, - changeStatus, - data, - error, - loading, - loadingAction, - loadingRegions, - page, - parentLeaderUserId, - profileType, - query, - regionId, - regionOptions, - reload, - closeAction: () => setActiveAction(""), - openBDForm: () => setActiveAction("bd"), - openBDLeaderForm: () => setActiveAction("bd-leader"), - openBDStatusForm: () => setActiveAction("bd-status"), - setBDForm, - setBDLeaderForm, - setBDStatusForm, - setPage, - status, - submitBD, - submitBDLeader, - submitBDStatus - }; + let cancelled = false; + const timer = window.setTimeout(async () => { + setBDUserLookup({ error: "", loading: true, user: null }); + try { + const data = await listAppUsers({ keyword, page: 1, page_size: 5 }); + if (cancelled) { + return; + } + setBDUserLookup({ error: "", loading: false, user: pickUserMatch(data?.items || [], keyword) }); + } catch (err) { + if (cancelled) { + return; + } + setBDUserLookup({ error: err.message || "搜索用户失败", loading: false, user: null }); + } + }, 300); + + return () => { + cancelled = true; + window.clearTimeout(timer); + }; + }, [activeAction, bdForm.targetUserId]); + + const changeQuery = (value) => { + setQuery(value); + setPage(1); + }; + + const changeStatus = (value) => { + setStatus(value); + setPage(1); + }; + + const changeRegionId = (value) => { + setRegionId(value); + setPage(1); + }; + + const changeParentLeaderUserId = (value) => { + setParentLeaderUserId(value); + setPage(1); + }; + + const resetFilters = () => { + setQuery(""); + setStatus(""); + setRegionId(""); + setParentLeaderUserId(""); + setPage(1); + }; + + const submitBDLeader = async (event) => { + event.preventDefault(); + await runAction("bd-leader", "BD Leader 已创建", async () => { + const payload = parseForm(createBDLeaderSchema, bdLeaderForm); + const data = await createBDLeader(payload); + setBDLeaderForm(emptyBDLeaderForm()); + setActiveAction(""); + await reload(); + return data; + }); + }; + + const submitBD = async (event) => { + event.preventDefault(); + await runAction("bd", "BD 已创建", async () => { + const payload = parseForm(createBDSchema, bdForm); + const data = await createBD(payload); + setBDForm(emptyBDForm()); + setActiveAction(""); + await reload(); + return data; + }); + }; + + const submitLeaderBD = async (event) => { + event.preventDefault(); + await runAction("leader-add-bd", "下级 BD 已添加", async () => { + const payload = parseForm(createBDSchema, bdForm); + const data = await createBD(payload); + setBDForm(emptyBDForm()); + setBDUserLookup(emptyLookup()); + setActiveAction(""); + await reload(); + return data; + }); + }; + + const submitBDStatus = async (event) => { + event.preventDefault(); + await runAction("bd-status", profileType === "leader" ? "BD Leader 状态已更新" : "BD 状态已更新", async () => { + const payload = parseForm(bdStatusSchema, { ...bdStatusForm, targetType: profileType }); + const { targetType, targetUserId, ...body } = payload; + const data = + targetType === "leader" + ? await setBDLeaderStatus(targetUserId, body) + : await setBDStatus(targetUserId, body); + setBDStatusForm(emptyBDStatusForm(profileType)); + setActiveAction(""); + await reload(); + return data; + }); + }; + + const toggleBDLeader = async (leader, enabled) => { + const status = enabled ? "active" : "disabled"; + await runAction( + `bd-leader-status-${leader.userId}`, + enabled ? "BD Leader 已启用" : "BD Leader 已停用", + async () => { + const data = await setBDLeaderStatus(leader.userId, { + commandId: makeCommandId("bd-leader-status"), + reason: "", + status, + }); + await reload(); + return data; + }, + ); + }; + + const removeBDLeader = async (leader) => { + const ok = await confirm({ + confirmText: "删除", + message: `确认删除 BD Leader ${leader.displayUserId || leader.userId}?删除后该身份会从列表移除。`, + title: "删除 BD Leader", + tone: "danger", + }); + if (!ok) { + return; + } + + await runAction(`bd-leader-delete-${leader.userId}`, "BD Leader 已删除", async () => { + const data = await deleteBDLeader(leader.userId); + await reload(); + return data; + }); + }; + + const openLeaderBDs = (leader) => { + setSelectedLeader(leader); + setActiveAction("leader-bds"); + void loadLeaderBDs(leader); + }; + + const openAddLeaderBD = (leader) => { + setSelectedLeader(leader); + setBDUserLookup(emptyLookup()); + setBDForm({ + ...emptyBDForm(), + parentLeaderUserId: leader.displayUserId || String(leader.userId || ""), + }); + setActiveAction("leader-add-bd"); + }; + + const runAction = async (action, successMessage, submitter) => { + setLoadingAction(action); + try { + await submitter(); + showToast(successMessage, "success"); + } catch (err) { + showToast(err.message || "操作失败", "error"); + } finally { + setLoadingAction(""); + } + }; + + const closeAction = () => { + setActiveAction(""); + setBDUserLookup(emptyLookup()); + setLeaderBDsError(""); + }; + + return { + abilities, + activeAction, + bdForm, + bdLeaderForm, + bdStatusForm, + bdUserLookup, + changeParentLeaderUserId, + changeQuery, + changeRegionId, + changeStatus, + data, + error, + loading, + loadingAction, + loadingRegions, + leaderBDs, + leaderBDsError, + leaderBDsLoading, + page, + parentLeaderUserId, + profileType, + query, + regionId, + regionOptions, + reload, + removeBDLeader, + resetFilters, + closeAction, + loadLeaderBDs: () => loadLeaderBDs(selectedLeader), + openAddLeaderBD, + openBDForm: () => setActiveAction("bd"), + openBDLeaderForm: () => setActiveAction("bd-leader"), + openBDStatusForm: () => setActiveAction("bd-status"), + openLeaderBDs, + selectedLeader, + setBDForm, + setBDLeaderForm, + setBDStatusForm, + setPage, + status, + submitBD, + submitBDLeader, + submitLeaderBD, + submitBDStatus, + toggleBDLeader, + }; } function makeCommandId(prefix) { - return `${prefix}-${Date.now()}`; + return `${prefix}-${Date.now()}`; +} + +function pickUserMatch(items, keyword) { + const normalized = String(keyword || "").trim(); + return ( + items.find( + (item) => String(item.displayUserId || "") === normalized || String(item.userId || "") === normalized, + ) || + items[0] || + null + ); } diff --git a/src/features/host-org/hooks/useHostCoinSellersPage.js b/src/features/host-org/hooks/useHostCoinSellersPage.js index a633ba5..b32a06d 100644 --- a/src/features/host-org/hooks/useHostCoinSellersPage.js +++ b/src/features/host-org/hooks/useHostCoinSellersPage.js @@ -2,197 +2,234 @@ import { useMemo, useState } from "react"; import { parseForm } from "@/shared/forms/validation"; import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js"; import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js"; -import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; -import { createCoinSeller, listCoinSellers, setCoinSellerStatus } from "@/features/host-org/api"; +import { createCoinSeller, creditCoinSellerStock, listCoinSellers, setCoinSellerStatus } from "@/features/host-org/api"; import { useCoinSellerAbilities } from "@/features/host-org/permissions.js"; -import { coinSellerStatusSchema, createCoinSellerSchema } from "@/features/host-org/schema"; +import { + coinSellerStatusSchema, + coinSellerStockCreditSchema, + createCoinSellerSchema, +} from "@/features/host-org/schema"; const pageSize = 10; const emptyData = { items: [], page: 1, pageSize, total: 0 }; -const emptyCreateForm = () => ({ commandId: makeCommandId("coin-seller"), reason: "", targetUserId: "" }); -const emptyEditForm = () => ({ commandId: makeCommandId("coin-seller-status"), reason: "", status: "active", targetUserId: "" }); +const emptyCreateForm = () => ({ commandId: makeCommandId("coin-seller"), contact: "", reason: "", targetUserId: "" }); +const emptyEditForm = () => ({ + commandId: makeCommandId("coin-seller-status"), + contact: "", + reason: "", + status: "active", + targetUserId: "", +}); +const emptyStockForm = () => ({ + coinAmount: "", + commandId: makeCommandId("coin-seller-stock"), + reason: "", + rechargeAmount: "", + type: "usdt_purchase", +}); export function useHostCoinSellersPage() { - const abilities = useCoinSellerAbilities(); - const confirm = useConfirm(); - const { showToast } = useToast(); - const { loadingRegions, regionOptions } = useRegionOptions(); - const [query, setQuery] = useState(""); - const [status, setStatus] = useState(""); - const [regionId, setRegionId] = useState(""); - const [page, setPage] = useState(1); - const [activeAction, setActiveAction] = useState(""); - const [createForm, setCreateForm] = useState(emptyCreateForm); - const [editForm, setEditForm] = useState(emptyEditForm); - const [selectedSeller, setSelectedSeller] = useState(null); - const [loadingAction, setLoadingAction] = useState(""); - const filters = useMemo( - () => ({ - keyword: query, - region_id: regionId, - status - }), - [query, regionId, status] - ); - const { data = emptyData, error, loading, reload } = usePaginatedQuery({ - errorMessage: "加载币商列表失败", - fetcher: listCoinSellers, - filters, - page, - pageSize, - queryKey: ["host-org", "coin-sellers", filters, page] - }); - - const changeQuery = (value) => { - setQuery(value); - setPage(1); - }; - - const changeStatus = (value) => { - setStatus(value); - setPage(1); - }; - - const changeRegionId = (value) => { - setRegionId(value); - setPage(1); - }; - - const openCreateSeller = () => { - setCreateForm(emptyCreateForm()); - setSelectedSeller(null); - setActiveAction("create"); - }; - - const openEditSeller = (seller) => { - if (!seller?.userId) { - return; - } - setSelectedSeller(seller); - setEditForm({ - commandId: makeCommandId("coin-seller-status"), - reason: "", - status: seller.status || "active", - targetUserId: seller.userId + const abilities = useCoinSellerAbilities(); + const { showToast } = useToast(); + const { loadingRegions, regionOptions } = useRegionOptions(); + const [query, setQuery] = useState(""); + const [status, setStatus] = useState(""); + const [regionId, setRegionId] = useState(""); + const [page, setPage] = useState(1); + const [activeAction, setActiveAction] = useState(""); + const [createForm, setCreateForm] = useState(emptyCreateForm); + const [editForm, setEditForm] = useState(emptyEditForm); + const [stockForm, setStockForm] = useState(emptyStockForm); + const [selectedSeller, setSelectedSeller] = useState(null); + const [loadingAction, setLoadingAction] = useState(""); + const filters = useMemo( + () => ({ + keyword: query, + region_id: regionId, + status, + }), + [query, regionId, status], + ); + const { + data = emptyData, + error, + loading, + reload, + } = usePaginatedQuery({ + errorMessage: "加载币商列表失败", + fetcher: listCoinSellers, + filters, + page, + pageSize, + queryKey: ["host-org", "coin-sellers", filters, page], }); - setActiveAction("edit"); - }; - const closeAction = () => { - setActiveAction(""); - setSelectedSeller(null); - }; + const changeQuery = (value) => { + setQuery(value); + setPage(1); + }; - const submitCreateSeller = async (event) => { - event.preventDefault(); - await runAction("coin-seller-create", "币商已添加", async () => { - const payload = parseForm(createCoinSellerSchema, createForm); - await createCoinSeller(payload); - setCreateForm(emptyCreateForm()); - closeAction(); - await reload(); - }); - }; + const changeStatus = (value) => { + setStatus(value); + setPage(1); + }; - const submitEditSeller = async (event) => { - event.preventDefault(); - await runAction(`coin-seller-edit-${editForm.targetUserId || selectedSeller?.userId || ""}`, "币商已更新", async () => { - const payload = parseForm(coinSellerStatusSchema, editForm); - const { targetUserId, ...body } = payload; - await setCoinSellerStatus(targetUserId, body); - setEditForm(emptyEditForm()); - closeAction(); - await reload(); - }); - }; + const changeRegionId = (value) => { + setRegionId(value); + setPage(1); + }; - const toggleSeller = async (seller, nextEnabled = seller.status !== "active") => { - if (!abilities.canUpdate || !seller?.userId) { - return; - } - const nextStatus = nextEnabled ? "active" : "disabled"; - if (seller.status === nextStatus) { - return; - } - await runAction(`coin-seller-status-${seller.userId}`, nextEnabled ? "币商已启用" : "币商已停用", async () => { - await setCoinSellerStatus(seller.userId, { - commandId: makeCommandId("coin-seller-status"), - reason: "", - status: nextStatus - }); - await reload(); - }); - }; + const resetFilters = () => { + setQuery(""); + setStatus(""); + setRegionId(""); + setPage(1); + }; - const deleteSeller = async (seller) => { - if (!abilities.canUpdate || !seller?.userId) { - return; - } - const ok = await confirm({ - confirmText: "删除", - message: seller.username || seller.displayUserId || String(seller.userId), - title: "删除币商", - tone: "danger" - }); - if (!ok) { - return; - } - await runAction(`coin-seller-delete-${seller.userId}`, "币商已删除", async () => { - await setCoinSellerStatus(seller.userId, { - commandId: makeCommandId("coin-seller-delete"), - reason: "delete coin seller", - status: "disabled" - }); - await reload(); - }); - }; + const openCreateSeller = () => { + setCreateForm(emptyCreateForm()); + setSelectedSeller(null); + setActiveAction("create"); + }; - const runAction = async (action, successMessage, submitter) => { - setLoadingAction(action); - try { - await submitter(); - showToast(successMessage, "success"); - } catch (err) { - showToast(err.message || "操作失败", "error"); - } finally { - setLoadingAction(""); - } - }; + const openEditSeller = (seller) => { + if (!seller?.userId) { + return; + } + setSelectedSeller(seller); + setEditForm({ + commandId: makeCommandId("coin-seller-status"), + contact: seller.contact || "", + reason: "update coin seller contact", + status: seller.status || "active", + targetUserId: seller.userId, + }); + setActiveAction("edit"); + }; - return { - abilities, - activeAction, - changeQuery, - changeRegionId, - changeStatus, - closeAction, - createForm, - data, - deleteSeller, - editForm, - error, - loading, - loadingAction, - loadingRegions, - openCreateSeller, - openEditSeller, - page, - query, - regionId, - regionOptions, - reload, - selectedSeller, - setCreateForm, - setEditForm, - setPage, - status, - submitCreateSeller, - submitEditSeller, - toggleSeller - }; + const openStockCredit = (seller) => { + if (!seller?.userId || seller.status !== "active" || !abilities.canStockCredit) { + return; + } + setSelectedSeller(seller); + setStockForm(emptyStockForm()); + setActiveAction("stock"); + }; + + const closeAction = () => { + setActiveAction(""); + setSelectedSeller(null); + }; + + const submitCreateSeller = async (event) => { + event.preventDefault(); + await runAction("coin-seller-create", "币商已添加", async () => { + const payload = parseForm(createCoinSellerSchema, createForm); + await createCoinSeller(payload); + setCreateForm(emptyCreateForm()); + closeAction(); + await reload(); + }); + }; + + const submitEditSeller = async (event) => { + event.preventDefault(); + await runAction( + `coin-seller-edit-${editForm.targetUserId || selectedSeller?.userId || ""}`, + "币商已更新", + async () => { + const payload = parseForm(coinSellerStatusSchema, editForm); + const { targetUserId, ...body } = payload; + await setCoinSellerStatus(targetUserId, body); + setEditForm(emptyEditForm()); + closeAction(); + await reload(); + }, + ); + }; + + const submitStockCredit = async (event) => { + event.preventDefault(); + if (!selectedSeller?.userId) { + return; + } + await runAction(`coin-seller-stock-${selectedSeller.userId}`, "币商库存已入账", async () => { + const payload = parseForm(coinSellerStockCreditSchema, stockForm); + await creditCoinSellerStock(selectedSeller.userId, payload); + setStockForm(emptyStockForm()); + closeAction(); + await reload(); + }); + }; + + const toggleSeller = async (seller, nextEnabled = seller.status !== "active") => { + if (!abilities.canUpdate || !seller?.userId) { + return; + } + const nextStatus = nextEnabled ? "active" : "disabled"; + if (seller.status === nextStatus) { + return; + } + await runAction(`coin-seller-status-${seller.userId}`, nextEnabled ? "币商已启用" : "币商已停用", async () => { + await setCoinSellerStatus(seller.userId, { + commandId: makeCommandId("coin-seller-status"), + reason: "", + status: nextStatus, + }); + await reload(); + }); + }; + + const runAction = async (action, successMessage, submitter) => { + setLoadingAction(action); + try { + await submitter(); + showToast(successMessage, "success"); + } catch (err) { + showToast(err.message || "操作失败", "error"); + } finally { + setLoadingAction(""); + } + }; + + return { + abilities, + activeAction, + changeQuery, + changeRegionId, + changeStatus, + closeAction, + createForm, + data, + editForm, + error, + loading, + loadingAction, + loadingRegions, + openCreateSeller, + openEditSeller, + openStockCredit, + page, + query, + regionId, + regionOptions, + reload, + resetFilters, + selectedSeller, + setCreateForm, + setEditForm, + setPage, + setStockForm, + status, + stockForm, + submitCreateSeller, + submitEditSeller, + submitStockCredit, + toggleSeller, + }; } function makeCommandId(prefix) { - return `${prefix}-${Date.now()}`; + return `${prefix}-${Date.now()}`; } diff --git a/src/features/host-org/hooks/useHostCountriesPage.js b/src/features/host-org/hooks/useHostCountriesPage.js index 9018c27..45569cb 100644 --- a/src/features/host-org/hooks/useHostCountriesPage.js +++ b/src/features/host-org/hooks/useHostCountriesPage.js @@ -2,160 +2,163 @@ import { useCallback, useMemo, useState } from "react"; import { parseForm } from "@/shared/forms/validation"; import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; -import { - createCountry, - disableCountry, - enableCountry, - listCountries, - updateCountry -} from "@/features/host-org/api"; +import { createCountry, disableCountry, enableCountry, listCountries, updateCountry } from "@/features/host-org/api"; import { useCountryAbilities } from "@/features/host-org/permissions.js"; import { countryCreateSchema, countryUpdateSchema } from "@/features/host-org/schema"; const emptyCountryForm = () => ({ - countryCode: "", - countryDisplayName: "", - countryName: "", - enabled: true, - flag: "", - isoAlpha3: "", - isoNumeric: "", - phoneCountryCode: "", - sortOrder: 0 + countryCode: "", + countryDisplayName: "", + countryName: "", + enabled: true, + flag: "", + isoAlpha3: "", + isoNumeric: "", + phoneCountryCode: "", + sortOrder: 0, }); const emptyData = { items: [], total: 0 }; export function useHostCountriesPage() { - const abilities = useCountryAbilities(); - const { showToast } = useToast(); - const [query, setQuery] = useState(""); - const [enabledStatus, setEnabledStatus] = useState(""); - const [countryForm, setCountryForm] = useState(emptyCountryForm); - const [editingCountry, setEditingCountry] = useState(null); - const [loadingAction, setLoadingAction] = useState(""); - const [activeAction, setActiveAction] = useState(""); - const filters = useMemo( - () => ({ - enabled: enabledStatus === "enabled" ? true : enabledStatus === "disabled" ? false : undefined - }), - [enabledStatus] - ); - const queryFn = useCallback(() => listCountries(filters), [filters]); - const { data = emptyData, error, loading, reload } = useAdminQuery(queryFn, { - errorMessage: "加载国家列表失败", - initialData: emptyData, - keepPreviousData: true, - queryKey: ["host-org", "countries", filters] - }); - const items = useMemo(() => filterCountries(data?.items || [], query), [data?.items, query]); - - const changeQuery = (value) => { - setQuery(value); - }; - - const changeEnabledStatus = (value) => { - setEnabledStatus(value); - }; - - const openCreateCountry = () => { - setEditingCountry(null); - setCountryForm(emptyCountryForm()); - setActiveAction("create"); - }; - - const openEditCountry = (country) => { - setEditingCountry(country); - setCountryForm({ - countryCode: country.countryCode || "", - countryDisplayName: country.countryDisplayName || "", - countryName: country.countryName || "", - enabled: Boolean(country.enabled), - flag: country.flag || "", - isoAlpha3: country.isoAlpha3 || "", - isoNumeric: country.isoNumeric || "", - phoneCountryCode: country.phoneCountryCode || "", - sortOrder: country.sortOrder || 0 + const abilities = useCountryAbilities(); + const { showToast } = useToast(); + const [query, setQuery] = useState(""); + const [enabledStatus, setEnabledStatus] = useState(""); + const [countryForm, setCountryForm] = useState(emptyCountryForm); + const [editingCountry, setEditingCountry] = useState(null); + const [loadingAction, setLoadingAction] = useState(""); + const [activeAction, setActiveAction] = useState(""); + const filters = useMemo( + () => ({ + enabled: enabledStatus === "enabled" ? true : enabledStatus === "disabled" ? false : undefined, + }), + [enabledStatus], + ); + const queryFn = useCallback(() => listCountries(filters), [filters]); + const { + data = emptyData, + error, + loading, + reload, + } = useAdminQuery(queryFn, { + errorMessage: "加载国家列表失败", + initialData: emptyData, + keepPreviousData: true, + queryKey: ["host-org", "countries", filters], }); - setActiveAction("edit"); - }; + const items = useMemo(() => filterCountries(data?.items || [], query), [data?.items, query]); - const submitCountry = async (event) => { - event.preventDefault(); - const isEdit = activeAction === "edit"; - await runAction(isEdit ? "country-edit" : "country-create", isEdit ? "国家已更新" : "国家已创建", async () => { - const payload = parseForm(isEdit ? countryUpdateSchema : countryCreateSchema, countryForm); - const data = isEdit - ? await updateCountry(editingCountry.countryId, payload) - : await createCountry(payload); - setCountryForm(emptyCountryForm()); - setEditingCountry(null); - setActiveAction(""); - await reload(); - return data; - }); - }; + const changeQuery = (value) => { + setQuery(value); + }; - const toggleCountry = async (country, nextEnabled = !country.enabled) => { - if (!abilities.canStatus || Boolean(country.enabled) === nextEnabled) { - return; - } - await runAction(`country-status-${country.countryId}`, nextEnabled ? "国家已启用" : "国家已禁用", async () => { - const data = nextEnabled ? await enableCountry(country.countryId) : await disableCountry(country.countryId); - await reload(); - return data; - }); - }; + const changeEnabledStatus = (value) => { + setEnabledStatus(value); + }; - const runAction = async (action, successMessage, submitter) => { - setLoadingAction(action); - try { - await submitter(); - showToast(successMessage, "success"); - } catch (err) { - showToast(err.message || "操作失败", "error"); - } finally { - setLoadingAction(""); - } - }; + const resetFilters = () => { + setQuery(""); + setEnabledStatus(""); + }; - return { - abilities, - activeAction, - changeEnabledStatus, - changeQuery, - closeAction: () => setActiveAction(""), - countryForm, - data: { ...data, items, total: items.length }, - editingCountry, - enabledStatus, - error, - loading, - loadingAction, - openCreateCountry, - openEditCountry, - query, - reload, - setCountryForm, - submitCountry, - toggleCountry - }; + const openCreateCountry = () => { + setEditingCountry(null); + setCountryForm(emptyCountryForm()); + setActiveAction("create"); + }; + + const openEditCountry = (country) => { + setEditingCountry(country); + setCountryForm({ + countryCode: country.countryCode || "", + countryDisplayName: country.countryDisplayName || "", + countryName: country.countryName || "", + enabled: Boolean(country.enabled), + flag: country.flag || "", + isoAlpha3: country.isoAlpha3 || "", + isoNumeric: country.isoNumeric || "", + phoneCountryCode: country.phoneCountryCode || "", + sortOrder: country.sortOrder || 0, + }); + setActiveAction("edit"); + }; + + const submitCountry = async (event) => { + event.preventDefault(); + const isEdit = activeAction === "edit"; + await runAction(isEdit ? "country-edit" : "country-create", isEdit ? "国家已更新" : "国家已创建", async () => { + const payload = parseForm(isEdit ? countryUpdateSchema : countryCreateSchema, countryForm); + const data = isEdit ? await updateCountry(editingCountry.countryId, payload) : await createCountry(payload); + setCountryForm(emptyCountryForm()); + setEditingCountry(null); + setActiveAction(""); + await reload(); + return data; + }); + }; + + const toggleCountry = async (country, nextEnabled = !country.enabled) => { + if (!abilities.canStatus || Boolean(country.enabled) === nextEnabled) { + return; + } + await runAction(`country-status-${country.countryId}`, nextEnabled ? "国家已启用" : "国家已禁用", async () => { + const data = nextEnabled ? await enableCountry(country.countryId) : await disableCountry(country.countryId); + await reload(); + return data; + }); + }; + + const runAction = async (action, successMessage, submitter) => { + setLoadingAction(action); + try { + await submitter(); + showToast(successMessage, "success"); + } catch (err) { + showToast(err.message || "操作失败", "error"); + } finally { + setLoadingAction(""); + } + }; + + return { + abilities, + activeAction, + changeEnabledStatus, + changeQuery, + closeAction: () => setActiveAction(""), + countryForm, + data: { ...data, items, total: items.length }, + editingCountry, + enabledStatus, + error, + loading, + loadingAction, + openCreateCountry, + openEditCountry, + query, + reload, + resetFilters, + setCountryForm, + submitCountry, + toggleCountry, + }; } function filterCountries(items, query) { - const keyword = query.trim().toLowerCase(); - if (!keyword) { - return items; - } - return items.filter((item) => { - return [ - item.countryCode, - item.countryDisplayName, - item.countryName, - item.isoAlpha3, - item.isoNumeric, - item.phoneCountryCode - ] - .filter(Boolean) - .some((value) => String(value).toLowerCase().includes(keyword)); - }); + const keyword = query.trim().toLowerCase(); + if (!keyword) { + return items; + } + return items.filter((item) => { + return [ + item.countryCode, + item.countryDisplayName, + item.countryName, + item.isoAlpha3, + item.isoNumeric, + item.phoneCountryCode, + ] + .filter(Boolean) + .some((value) => String(value).toLowerCase().includes(keyword)); + }); } diff --git a/src/features/host-org/hooks/useHostHostsPage.js b/src/features/host-org/hooks/useHostHostsPage.js index 88e8e10..4637859 100644 --- a/src/features/host-org/hooks/useHostHostsPage.js +++ b/src/features/host-org/hooks/useHostHostsPage.js @@ -8,68 +8,82 @@ const pageSize = 10; const emptyData = { items: [], page: 1, pageSize, total: 0 }; export function useHostHostsPage() { - const abilities = useHostAbilities(); - const { loadingRegions, regionOptions } = useRegionOptions(); - const [query, setQuery] = useState(""); - const [status, setStatus] = useState(""); - const [regionId, setRegionId] = useState(""); - const [agencyId, setAgencyId] = useState(""); - const [page, setPage] = useState(1); - const filters = useMemo( - () => ({ - agency_id: agencyId, - keyword: query, - region_id: regionId, - status - }), - [agencyId, query, regionId, status] - ); - const { data = emptyData, error, loading, reload } = usePaginatedQuery({ - errorMessage: "加载主播列表失败", - fetcher: listHosts, - filters, - page, - pageSize, - queryKey: ["host-org", "hosts", filters, page] - }); + const abilities = useHostAbilities(); + const { loadingRegions, regionOptions } = useRegionOptions(); + const [query, setQuery] = useState(""); + const [status, setStatus] = useState(""); + const [regionId, setRegionId] = useState(""); + const [agencyId, setAgencyId] = useState(""); + const [page, setPage] = useState(1); + const filters = useMemo( + () => ({ + agency_id: agencyId, + keyword: query, + region_id: regionId, + status, + }), + [agencyId, query, regionId, status], + ); + const { + data = emptyData, + error, + loading, + reload, + } = usePaginatedQuery({ + errorMessage: "加载主播列表失败", + fetcher: listHosts, + filters, + page, + pageSize, + queryKey: ["host-org", "hosts", filters, page], + }); - const changeQuery = (value) => { - setQuery(value); - setPage(1); - }; + const changeQuery = (value) => { + setQuery(value); + setPage(1); + }; - const changeStatus = (value) => { - setStatus(value); - setPage(1); - }; + const changeStatus = (value) => { + setStatus(value); + setPage(1); + }; - const changeRegionId = (value) => { - setRegionId(value); - setPage(1); - }; + const changeRegionId = (value) => { + setRegionId(value); + setPage(1); + }; - const changeAgencyId = (value) => { - setAgencyId(value); - setPage(1); - }; + const changeAgencyId = (value) => { + setAgencyId(value); + setPage(1); + }; - return { - abilities, - agencyId, - changeAgencyId, - changeQuery, - changeRegionId, - changeStatus, - data, - error, - loading, - loadingRegions, - page, - query, - regionId, - regionOptions, - reload, - setPage, - status - }; + const resetFilters = () => { + setQuery(""); + setStatus(""); + setRegionId(""); + setAgencyId(""); + setPage(1); + }; + + return { + abilities, + agencyId, + changeAgencyId, + changeQuery, + changeRegionId, + changeStatus, + data, + error, + loading, + loadingRegions, + page, + query, + regionId, + regionOptions, + reload, + resetFilters, + setPage, + status, + }; } diff --git a/src/features/host-org/hooks/useHostRegionsPage.js b/src/features/host-org/hooks/useHostRegionsPage.js index 7b21f45..43fb81b 100644 --- a/src/features/host-org/hooks/useHostRegionsPage.js +++ b/src/features/host-org/hooks/useHostRegionsPage.js @@ -3,200 +3,225 @@ import { parseForm } from "@/shared/forms/validation"; import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; import { - createRegion, - disableRegion, - enableRegion, - listCountries, - listRegions, - replaceRegionCountries, - updateRegion + createRegion, + disableRegion, + enableRegion, + listCountries, + listRegions, + replaceRegionCountries, + updateRegion, } from "@/features/host-org/api"; import { useRegionAbilities } from "@/features/host-org/permissions.js"; import { regionCreateSchema, regionUpdateSchema } from "@/features/host-org/schema"; const emptyRegionForm = () => ({ - countries: [], - name: "", - regionCode: "", - sortOrder: 0 + countries: [], + name: "", + regionCode: "", + sortOrder: 0, }); const emptyData = { items: [], total: 0 }; const globalRegionCode = "GLOBAL"; export function useHostRegionsPage() { - const abilities = useRegionAbilities(); - const { showToast } = useToast(); - const [query, setQuery] = useState(""); - const [status, setStatus] = useState(""); - const [regionForm, setRegionFormState] = useState(emptyRegionForm); - const [editingRegion, setEditingRegion] = useState(null); - const [loadingAction, setLoadingAction] = useState(""); - const [activeAction, setActiveAction] = useState(""); - const queryFn = useCallback(() => listRegions(), []); - const { data = emptyData, error, loading, reload } = useAdminQuery(queryFn, { - errorMessage: "加载区域列表失败", - initialData: emptyData, - keepPreviousData: true, - queryKey: ["host-org", "regions"] - }); - const countryQueryFn = useCallback(() => listCountries(), []); - const { data: countriesData = emptyData, loading: loadingCountries } = useAdminQuery(countryQueryFn, { - errorMessage: "加载国家列表失败", - initialData: emptyData, - queryKey: ["host-org", "countries", "region-options"], - staleTime: 5 * 60 * 1000 - }); - const items = useMemo(() => filterRegions(data?.items || [], query, status), [data?.items, query, status]); - const assignedCountries = useMemo(() => { - const currentRegionId = editingRegion?.regionId; - const assigned = new Set(); - (data?.items || []).forEach((region) => { - if (region.status !== "active" || region.regionId === currentRegionId || isGlobalRegionCode(region.regionCode)) { - return; - } - (region.countries || []).forEach((country) => assigned.add(country)); + const abilities = useRegionAbilities(); + const { showToast } = useToast(); + const [query, setQuery] = useState(""); + const [status, setStatus] = useState(""); + const [regionForm, setRegionFormState] = useState(emptyRegionForm); + const [editingRegion, setEditingRegion] = useState(null); + const [loadingAction, setLoadingAction] = useState(""); + const [activeAction, setActiveAction] = useState(""); + const queryFn = useCallback(() => listRegions(), []); + const { + data = emptyData, + error, + loading, + reload, + } = useAdminQuery(queryFn, { + errorMessage: "加载区域列表失败", + initialData: emptyData, + keepPreviousData: true, + queryKey: ["host-org", "regions"], }); - return assigned; - }, [data?.items, editingRegion?.regionId]); - const countryOptions = useMemo(() => { - return [...new Set((countriesData?.items || []).map((country) => country.countryCode).filter(Boolean))] - .filter((countryCode) => !assignedCountries.has(countryCode)) - .sort(); - }, [assignedCountries, countriesData?.items]); - const countryLabels = useMemo(() => { - return (countriesData?.items || []).reduce((labels, country) => { - if (country.countryCode) { - labels[country.countryCode] = [country.countryCode, country.countryDisplayName || country.countryName].filter(Boolean).join(" · "); - } - return labels; - }, {}); - }, [countriesData?.items]); - const isGlobalRegionForm = isGlobalRegionCode(regionForm.regionCode); - - const setRegionForm = (nextForm) => { - const normalized = isGlobalRegionCode(nextForm.regionCode) ? { ...nextForm, countries: [] } : nextForm; - setRegionFormState(normalized); - }; - - const changeQuery = (value) => { - setQuery(value); - }; - - const changeStatus = (value) => { - setStatus(value); - }; - - const openCreateRegion = () => { - setEditingRegion(null); - setRegionFormState(emptyRegionForm()); - setActiveAction("create"); - }; - - const openEditRegion = (region) => { - setEditingRegion(region); - setRegionFormState({ - countries: region.countries || [], - name: region.name || "", - regionCode: region.regionCode || "", - sortOrder: region.sortOrder || 0 + const countryQueryFn = useCallback(() => listCountries(), []); + const { data: countriesData = emptyData, loading: loadingCountries } = useAdminQuery(countryQueryFn, { + errorMessage: "加载国家列表失败", + initialData: emptyData, + queryKey: ["host-org", "countries", "region-options"], + staleTime: 5 * 60 * 1000, }); - setActiveAction("edit"); - }; + const items = useMemo(() => filterRegions(data?.items || [], query, status), [data?.items, query, status]); + const assignedCountries = useMemo(() => { + const currentRegionId = editingRegion?.regionId; + const assigned = new Set(); + (data?.items || []).forEach((region) => { + if ( + region.status !== "active" || + region.regionId === currentRegionId || + isGlobalRegionCode(region.regionCode) + ) { + return; + } + (region.countries || []).forEach((country) => assigned.add(country)); + }); + return assigned; + }, [data?.items, editingRegion?.regionId]); + const countryOptions = useMemo(() => { + return [...new Set((countriesData?.items || []).map((country) => country.countryCode).filter(Boolean))] + .filter((countryCode) => !assignedCountries.has(countryCode)) + .sort(); + }, [assignedCountries, countriesData?.items]); + const countryLabels = useMemo(() => { + return (countriesData?.items || []).reduce((labels, country) => { + if (country.countryCode) { + labels[country.countryCode] = [country.countryCode, country.countryDisplayName || country.countryName] + .filter(Boolean) + .join(" · "); + } + return labels; + }, {}); + }, [countriesData?.items]); + const isGlobalRegionForm = isGlobalRegionCode(regionForm.regionCode); - const submitRegion = async (event) => { - event.preventDefault(); - const isEdit = activeAction === "edit"; - await runAction(isEdit ? "region-edit" : "region-create", isEdit ? "区域已更新" : "区域已创建", async () => { - const payload = parseForm(isEdit ? regionUpdateSchema : regionCreateSchema, regionForm); - const data = isEdit ? await updateExistingRegion(editingRegion, payload) : await createRegion(payload); - setRegionFormState(emptyRegionForm()); - setEditingRegion(null); - setActiveAction(""); - await reload(); - return data; - }); - }; + const setRegionForm = (nextForm) => { + const normalized = isGlobalRegionCode(nextForm.regionCode) ? { ...nextForm, countries: [] } : nextForm; + setRegionFormState(normalized); + }; - const toggleRegion = async (region, nextActive = region.status !== "active") => { - if (isGlobalRegionCode(region.regionCode) || !abilities.canStatus || (region.status === "active") === nextActive) { - return; - } - await runAction(`region-status-${region.regionId}`, nextActive ? "区域已启用" : "区域已停用", async () => { - const data = nextActive ? await enableRegion(region.regionId) : await disableRegion(region.regionId); - await reload(); - return data; - }); - }; + const changeQuery = (value) => { + setQuery(value); + }; - const runAction = async (action, successMessage, submitter) => { - setLoadingAction(action); - try { - await submitter(); - showToast(successMessage, "success"); - } catch (err) { - showToast(err.message || "操作失败", "error"); - } finally { - setLoadingAction(""); - } - }; + const changeStatus = (value) => { + setStatus(value); + }; - return { - abilities, - activeAction, - changeQuery, - changeStatus, - closeAction: () => setActiveAction(""), - countryLabels, - countryOptions, - data: { ...data, items, total: items.length }, - editingRegion, - error, - loading, - loadingAction, - loadingCountries, - isGlobalRegionForm, - openCreateRegion, - openEditRegion, - query, - regionForm, - reload, - setRegionForm, - status, - submitRegion, - toggleRegion - }; + const resetFilters = () => { + setQuery(""); + setStatus(""); + }; + + const openCreateRegion = () => { + setEditingRegion(null); + setRegionFormState(emptyRegionForm()); + setActiveAction("create"); + }; + + const openEditRegion = (region) => { + setEditingRegion(region); + setRegionFormState({ + countries: region.countries || [], + name: region.name || "", + regionCode: region.regionCode || "", + sortOrder: region.sortOrder || 0, + }); + setActiveAction("edit"); + }; + + const submitRegion = async (event) => { + event.preventDefault(); + const isEdit = activeAction === "edit"; + await runAction(isEdit ? "region-edit" : "region-create", isEdit ? "区域已更新" : "区域已创建", async () => { + const payload = parseForm(isEdit ? regionUpdateSchema : regionCreateSchema, regionForm); + const data = isEdit ? await updateExistingRegion(editingRegion, payload) : await createRegion(payload); + setRegionFormState(emptyRegionForm()); + setEditingRegion(null); + setActiveAction(""); + await reload(); + return data; + }); + }; + + const toggleRegion = async (region, nextActive = region.status !== "active") => { + if ( + isGlobalRegionCode(region.regionCode) || + !abilities.canStatus || + (region.status === "active") === nextActive + ) { + return; + } + await runAction(`region-status-${region.regionId}`, nextActive ? "区域已启用" : "区域已停用", async () => { + const data = nextActive ? await enableRegion(region.regionId) : await disableRegion(region.regionId); + await reload(); + return data; + }); + }; + + const runAction = async (action, successMessage, submitter) => { + setLoadingAction(action); + try { + await submitter(); + showToast(successMessage, "success"); + } catch (err) { + showToast(err.message || "操作失败", "error"); + } finally { + setLoadingAction(""); + } + }; + + return { + abilities, + activeAction, + changeQuery, + changeStatus, + closeAction: () => setActiveAction(""), + countryLabels, + countryOptions, + data: { ...data, items, total: items.length }, + editingRegion, + error, + loading, + loadingAction, + loadingCountries, + isGlobalRegionForm, + openCreateRegion, + openEditRegion, + query, + regionForm, + reload, + resetFilters, + setRegionForm, + status, + submitRegion, + toggleRegion, + }; } async function updateExistingRegion(region, payload) { - const { countries, ...regionPayload } = payload; - const updatedRegion = await updateRegion(region.regionId, regionPayload); - if (hasCountryChanges(region.countries || [], countries || [])) { - return replaceRegionCountries(region.regionId, { countries }); - } - return updatedRegion; + const { countries, ...regionPayload } = payload; + const updatedRegion = await updateRegion(region.regionId, regionPayload); + if (hasCountryChanges(region.countries || [], countries || [])) { + return replaceRegionCountries(region.regionId, { countries }); + } + return updatedRegion; } function hasCountryChanges(previousCountries, nextCountries) { - const previous = [...previousCountries].sort(); - const next = [...nextCountries].sort(); - return previous.length !== next.length || previous.some((country, index) => country !== next[index]); + const previous = [...previousCountries].sort(); + const next = [...nextCountries].sort(); + return previous.length !== next.length || previous.some((country, index) => country !== next[index]); } function filterRegions(items, query, status) { - const keyword = query.trim().toLowerCase(); - return items.filter((item) => { - if (status && item.status !== status) { - return false; - } - if (!keyword) { - return true; - } - return [item.regionCode, item.name, ...(item.countries || [])] - .filter(Boolean) - .some((value) => String(value).toLowerCase().includes(keyword)); - }); + const keyword = query.trim().toLowerCase(); + return items.filter((item) => { + if (status && item.status !== status) { + return false; + } + if (!keyword) { + return true; + } + return [item.regionCode, item.name, ...(item.countries || [])] + .filter(Boolean) + .some((value) => String(value).toLowerCase().includes(keyword)); + }); } function isGlobalRegionCode(regionCode) { - return String(regionCode || "").trim().toUpperCase() === globalRegionCode; + return ( + String(regionCode || "") + .trim() + .toUpperCase() === globalRegionCode + ); } diff --git a/src/features/host-org/host-org.module.css b/src/features/host-org/host-org.module.css index d601920..5eb67f4 100644 --- a/src/features/host-org/host-org.module.css +++ b/src/features/host-org/host-org.module.css @@ -1,202 +1,258 @@ .root { - display: flex; - height: calc(100vh - var(--header-height) - var(--space-8) - var(--space-4)); - height: calc(100dvh - var(--header-height) - var(--space-8) - var(--space-4)); - min-height: 0; - flex-direction: column; + display: flex; + height: calc(100vh - var(--header-height) - var(--space-8) - var(--space-4)); + height: calc(100dvh - var(--header-height) - var(--space-8) - var(--space-4)); + min-height: 0; + flex-direction: column; } .contentPanel { - display: flex; - flex: 1; - flex-direction: column; - min-height: 0; - border: 1px solid var(--border); - border-radius: var(--radius-card); - background: var(--bg-card); - overflow: hidden; + display: flex; + flex: 1; + flex-direction: column; + min-height: 0; + border: 1px solid var(--border); + border-radius: var(--radius-card); + background: var(--bg-card); + overflow: hidden; } .contentPanel :global(.data-state) { - flex: 1; - min-height: 0; - border: 0; - border-radius: 0; - background: transparent; + flex: 1; + min-height: 0; + border: 0; + border-radius: 0; + background: transparent; } .contentPanel :global(.table-frame) { - min-height: 0; - border: 0; - border-radius: 0; + min-height: 0; + border: 0; + border-radius: 0; } .contentPanel :global(.table-scroll) { - overflow: auto; + overflow: auto; } .contentPanel :global(.admin-row--head) { - position: sticky; - top: 0; - z-index: 1; + position: sticky; + top: 0; + z-index: 1; } .toolbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-4); - padding: var(--space-4) var(--space-5); - border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + padding: var(--space-4) var(--space-5); + border-bottom: 1px solid var(--border); } .toolbarLeft { - display: flex; - min-width: 0; - flex: 1; - align-items: center; - gap: var(--space-3); + display: flex; + min-width: 0; + flex: 1; + align-items: center; + gap: var(--space-3); } .toolbarActions { - display: flex; - flex: 0 0 auto; - align-items: center; - gap: var(--space-2); + display: flex; + flex: 0 0 auto; + align-items: center; + gap: var(--space-2); } .filters { - display: flex; - min-width: 0; - flex: 1; - align-items: center; - flex-wrap: wrap; - gap: var(--space-3); + display: flex; + min-width: 0; + flex: 1; + align-items: center; + flex-wrap: wrap; + gap: var(--space-3); } .search { - flex: 0 1 300px; + flex: 0 1 300px; } .statusSelect { - flex: 0 0 150px; + flex: 0 0 150px; } .filterFields { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: var(--space-3); + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--space-3); } .filterFields :global(.MuiTextField-root) { - width: 170px; + width: 170px; } .rowActions { - display: inline-flex; - align-items: center; - gap: var(--space-2); + display: inline-flex; + align-items: center; + gap: var(--space-2); } .flagCell { - font-size: var(--admin-font-size); + font-size: var(--admin-font-size); } .listBlock { - display: flex; - flex: 1; - flex-direction: column; - min-height: 0; - background: var(--bg-card); + display: flex; + flex: 1; + flex-direction: column; + min-height: 0; + background: var(--bg-card); } .listBlock :global(.pagination-bar) { - margin: var(--space-3) var(--space-5) var(--space-4); + margin: var(--space-3) var(--space-5) var(--space-4); } .inlineValue { - color: var(--text-primary); - font-weight: 650; + color: var(--text-primary); + font-weight: 650; } .sellerIdentity { - display: inline-flex; - min-width: 0; - align-items: center; - gap: var(--space-3); + display: inline-flex; + min-width: 0; + align-items: center; + gap: var(--space-3); } .sellerAvatar { - display: inline-flex; - width: 32px; - height: 32px; - flex: 0 0 auto; - align-items: center; - justify-content: center; - border: 1px solid var(--border); - border-radius: 50%; - background: var(--bg-input); - color: var(--text-secondary); - font-weight: 700; - object-fit: cover; + display: inline-flex; + width: 32px; + height: 32px; + flex: 0 0 auto; + align-items: center; + justify-content: center; + border: 1px solid var(--border); + border-radius: 50%; + background: var(--bg-input); + color: var(--text-secondary); + font-weight: 700; + object-fit: cover; } .sellerText { - display: inline-flex; - min-width: 0; - flex-direction: column; - gap: 2px; + display: inline-flex; + min-width: 0; + flex-direction: column; + gap: 2px; } .sellerName { - overflow: hidden; - color: var(--text-primary); - font-weight: 700; - text-overflow: ellipsis; - white-space: nowrap; + overflow: hidden; + color: var(--text-primary); + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; } .sellerMeta { - overflow: hidden; - color: var(--text-secondary); - font-size: var(--admin-font-size); - text-overflow: ellipsis; - white-space: nowrap; + overflow: hidden; + color: var(--text-secondary); + font-size: var(--admin-font-size); + text-overflow: ellipsis; + white-space: nowrap; +} + +.contactButton { + max-width: 100%; + padding: 0; + border: 0; + background: transparent; + color: var(--primary); + cursor: pointer; + font: inherit; + font-weight: 700; + overflow: hidden; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; +} + +.contactButton:hover { + color: var(--primary-strong); + text-decoration: underline; +} + +.contactPlaceholder { + color: var(--text-tertiary); +} + +.childBdList { + display: grid; + gap: var(--space-3); +} + +.childBdItem { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + padding: var(--space-3); + border: 1px solid var(--border); + border-radius: var(--radius-card); + background: var(--bg-input); +} + +.childBdStatus { + flex: 0 0 auto; +} + +.lookupPanel { + display: grid; + gap: var(--space-2); + min-height: 44px; + padding: var(--space-3); + border: 1px solid var(--border); + border-radius: var(--radius-control); + background: var(--bg-input); + color: var(--text-secondary); + font-size: var(--admin-font-size); } .positive { - color: var(--success); + color: var(--success); } .negative { - color: var(--danger); + color: var(--danger); } @media (max-width: 760px) { - .toolbar, - .toolbarLeft, - .filters, - .filterFields { - align-items: stretch; - flex-direction: column; - } + .toolbar, + .toolbarLeft, + .filters, + .filterFields { + align-items: stretch; + flex-direction: column; + } - .toolbarActions { - align-self: flex-end; - } + .toolbarActions { + align-self: flex-end; + } - .search { - flex: 1 1 auto; - width: 100%; - } + .search { + flex: 1 1 auto; + width: 100%; + } - .statusSelect { - flex: 1 1 auto; - width: 100%; - } + .statusSelect { + flex: 1 1 auto; + width: 100%; + } - .filterFields :global(.MuiTextField-root) { - width: 100%; - } + .filterFields :global(.MuiTextField-root) { + width: 100%; + } } diff --git a/src/features/host-org/pages/HostAgenciesPage.jsx b/src/features/host-org/pages/HostAgenciesPage.jsx index 78ff52d..4ed01ef 100644 --- a/src/features/host-org/pages/HostAgenciesPage.jsx +++ b/src/features/host-org/pages/HostAgenciesPage.jsx @@ -7,143 +7,324 @@ import TextField from "@mui/material/TextField"; import { formatMillis } from "@/shared/utils/time.js"; import { DataState } from "@/shared/ui/DataState.jsx"; import { PaginationBar } from "@/shared/ui/PaginationBar.jsx"; -import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx"; import { agencyStatusFilters } from "@/features/host-org/constants.js"; import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx"; +import { HostOrgEntity, HostOrgPerson, hostOrgRegionName } from "@/features/host-org/components/HostOrgIdentity.jsx"; import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx"; import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx"; import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx"; import { useHostAgenciesPage } from "@/features/host-org/hooks/useHostAgenciesPage.js"; +import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; import styles from "@/features/host-org/host-org.module.css"; const agencyColumns = [ - { key: "agencyId", label: "Agency ID", width: "minmax(110px, 0.8fr)" }, - { key: "name", label: "名称", width: "minmax(160px, 1.2fr)" }, - { key: "ownerUserId", label: "Owner 用户 ID" }, - { key: "parentBdUserId", label: "上级 BD 用户 ID" }, - { key: "regionId", label: "区域 ID", width: "minmax(90px, 0.7fr)" }, - { key: "status", label: "状态", render: (item) => , width: "minmax(90px, 0.7fr)" }, - { - key: "joinEnabled", - label: "入会", - render: (item) => {item.joinEnabled ? "允许" : "禁止"}, - width: "minmax(80px, 0.6fr)" - }, - { key: "maxHosts", label: "最大主播数", width: "minmax(90px, 0.7fr)" }, - { key: "updatedAtMs", label: "更新时间", render: (item) => formatMillis(item.updatedAtMs), width: "minmax(170px, 1fr)" } + { + key: "agency", + label: "Agency", + render: (item) => , + width: "minmax(190px, 1fr)", + }, + { + key: "ownerUserId", + label: "Owner", + render: (item) => , + width: "minmax(220px, 1.2fr)", + }, + { + key: "parentBdUserId", + label: "上级 BD", + render: (item) => , + width: "minmax(220px, 1.2fr)", + }, + { + key: "regionId", + label: "区域", + render: (item, _index, context) => hostOrgRegionName(item, context?.regionOptions), + width: "minmax(130px, 0.75fr)", + }, + { + key: "status", + label: "状态", + render: (item) => , + width: "minmax(90px, 0.7fr)", + }, + { + key: "joinEnabled", + label: "入会", + render: (item) => ( + + {item.joinEnabled ? "允许" : "禁止"} + + ), + width: "minmax(80px, 0.6fr)", + }, + { key: "maxHosts", label: "最大主播数", width: "minmax(100px, 0.7fr)" }, + { + key: "updatedAtMs", + label: "更新时间", + render: (item) => formatMillis(item.updatedAtMs), + width: "minmax(170px, 1fr)", + }, ]; export function HostAgenciesPage() { - const page = useHostAgenciesPage(); - const createDisabled = !page.abilities.canCreate; - const statusDisabled = !page.abilities.canStatus; - const items = page.data.items || []; - const total = page.data.total || 0; - const toolbarActions = [ - page.abilities.canStatus - ? { icon: , label: "入会开关", onClick: page.openJoinEnabledForm } - : null, - page.abilities.canStatus - ? { icon: , label: "关闭 Agency", onClick: page.openCloseForm, tone: "danger" } - : null, - page.abilities.canCreate - ? { icon: , label: "创建 Agency", onClick: page.openAgencyForm, variant: "primary" } - : null - ].filter(Boolean); - return ( -
-
- , label: "入会开关", onClick: page.openJoinEnabledForm } + : null, + page.abilities.canStatus + ? { + icon: , + label: "关闭 Agency", + onClick: page.openCloseForm, + tone: "danger", } - ], - loadingRegions: page.loadingRegions, - query: page.query, - regionId: page.regionId, - regionOptions: page.regionOptions, - searchPlaceholder: "搜索 Agency、Owner、用户...", - status: page.status, - statusOptions: agencyStatusFilters, - onQueryChange: page.changeQuery, - onRegionIdChange: page.changeRegionId, - onStatusChange: page.changeStatus - }} - /> + : null, + page.abilities.canCreate + ? { icon: , label: "创建 Agency", onClick: page.openAgencyForm, variant: "primary" } + : null, + ].filter(Boolean); + const columns = agencyColumns.map((column) => { + if (column.key === "agency") { + return { + ...column, + filter: createTextColumnFilter({ + placeholder: "搜索 Agency、Owner、用户", + value: page.query, + onChange: page.changeQuery, + }), + }; + } + if (column.key === "parentBdUserId") { + return { + ...column, + filter: createTextColumnFilter({ + placeholder: "搜索上级 BD 用户 ID", + value: page.parentBdUserId, + onChange: page.changeParentBdUserId, + }), + }; + } + if (column.key === "status") { + return { + ...column, + filter: createOptionsColumnFilter({ + options: agencyStatusFilters, + placeholder: "搜索状态", + value: page.status, + onChange: page.changeStatus, + }), + }; + } + return column; + }); - -
- item.agencyId} - /> - {total > 0 ? : null} -
-
-
+ return ( +
+
+ - - page.setAgencyForm({ ...page.agencyForm, name: event.target.value })} /> - page.setAgencyForm({ ...page.agencyForm, ownerUserId: event.target.value })} /> - page.setAgencyForm({ ...page.agencyForm, parentBdUserId: event.target.value })} /> - page.setAgencyForm({ ...page.agencyForm, maxHosts: event.target.value })} /> - page.setAgencyForm({ ...page.agencyForm, joinEnabled: event.target.checked })} />} - label={page.agencyForm.joinEnabled ? "允许入会" : "禁止入会"} - /> - page.setAgencyForm({ ...page.agencyForm, reason: event.target.value })} /> - + +
+ item.agencyId} + /> + {total > 0 ? ( + + ) : null} +
+
+
- - page.setJoinEnabledForm({ ...page.joinEnabledForm, agencyId: event.target.value })} /> - page.setJoinEnabledForm({ ...page.joinEnabledForm, joinEnabled: event.target.checked })} />} - label={page.joinEnabledForm.joinEnabled ? "允许入会" : "禁止入会"} - /> - page.setJoinEnabledForm({ ...page.joinEnabledForm, reason: event.target.value })} /> - + + page.setAgencyForm({ ...page.agencyForm, name: event.target.value })} + /> + page.setAgencyForm({ ...page.agencyForm, ownerUserId: event.target.value })} + /> + page.setAgencyForm({ ...page.agencyForm, parentBdUserId: event.target.value })} + /> + page.setAgencyForm({ ...page.agencyForm, maxHosts: event.target.value })} + /> + + page.setAgencyForm({ ...page.agencyForm, joinEnabled: event.target.checked }) + } + /> + } + label={page.agencyForm.joinEnabled ? "允许入会" : "禁止入会"} + /> + page.setAgencyForm({ ...page.agencyForm, reason: event.target.value })} + /> + - - page.setCloseForm({ ...page.closeForm, agencyId: event.target.value })} /> - page.setCloseForm({ ...page.closeForm, reason: event.target.value })} /> - + + + page.setJoinEnabledForm({ ...page.joinEnabledForm, agencyId: event.target.value }) + } + /> + + page.setJoinEnabledForm({ ...page.joinEnabledForm, joinEnabled: event.target.checked }) + } + /> + } + label={page.joinEnabledForm.joinEnabled ? "允许入会" : "禁止入会"} + /> + + page.setJoinEnabledForm({ ...page.joinEnabledForm, reason: event.target.value }) + } + /> + -
- ); + + page.setCloseForm({ ...page.closeForm, agencyId: event.target.value })} + /> + page.setCloseForm({ ...page.closeForm, reason: event.target.value })} + /> + +
+ ); +} + +function AgencyOwner({ item }) { + return ( + + ); +} + +function ParentBD({ item }) { + return ( + + ); } diff --git a/src/features/host-org/pages/HostBdLeadersPage.jsx b/src/features/host-org/pages/HostBdLeadersPage.jsx index 2938116..9b3df5d 100644 --- a/src/features/host-org/pages/HostBdLeadersPage.jsx +++ b/src/features/host-org/pages/HostBdLeadersPage.jsx @@ -1,11 +1,13 @@ import Add from "@mui/icons-material/Add"; -import SyncAltOutlined from "@mui/icons-material/SyncAltOutlined"; -import MenuItem from "@mui/material/MenuItem"; +import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined"; +import PersonAddAlt1Outlined from "@mui/icons-material/PersonAddAlt1Outlined"; +import Switch from "@mui/material/Switch"; import TextField from "@mui/material/TextField"; import { formatMillis } from "@/shared/utils/time.js"; import { DataState } from "@/shared/ui/DataState.jsx"; import { PaginationBar } from "@/shared/ui/PaginationBar.jsx"; -import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx"; +import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx"; +import { SideDrawer } from "@/shared/ui/SideDrawer.jsx"; import { bdStatusFilters } from "@/features/host-org/constants.js"; import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx"; import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx"; @@ -15,96 +17,323 @@ import { RegionSelect } from "@/shared/ui/RegionSelect.jsx"; import { useHostBdsPage } from "@/features/host-org/hooks/useHostBdsPage.js"; import styles from "@/features/host-org/host-org.module.css"; -const bdLeaderColumns = [ - { key: "userId", label: "用户 ID", width: "minmax(110px, 0.8fr)" }, - { key: "role", label: "角色", width: "minmax(110px, 0.8fr)" }, - { key: "regionId", label: "区域 ID", width: "minmax(90px, 0.7fr)" }, - { key: "status", label: "状态", render: (item) => , width: "minmax(90px, 0.7fr)" }, - { key: "createdByUserId", label: "创建人用户 ID" }, - { key: "createdAtMs", label: "创建时间", render: (item) => formatMillis(item.createdAtMs), width: "minmax(170px, 1fr)" }, - { key: "updatedAtMs", label: "更新时间", render: (item) => formatMillis(item.updatedAtMs), width: "minmax(170px, 1fr)" } +const bdLeaderBaseColumns = [ + { + key: "user", + label: "用户", + render: (item) => , + width: "minmax(220px, 1.25fr)", + }, + { + key: "regionId", + label: "区域", + render: (item, _index, context) => regionName(item, context?.regionOptions), + width: "minmax(130px, 0.75fr)", + }, + { + key: "status", + label: "状态", + render: (item, _index, context) => , + width: "minmax(90px, 0.7fr)", + }, + { + key: "createdByUserId", + label: "创建人", + render: (item) => , + width: "minmax(180px, 1fr)", + }, + { + key: "subBdCount", + label: "下级 BD 数量", + render: (item, _index, context) => , + width: "minmax(130px, 0.75fr)", + }, + { + key: "createdAtMs", + label: "创建时间", + render: (item) => formatMillis(item.createdAtMs), + width: "minmax(170px, 1fr)", + }, ]; export function HostBdLeadersPage() { - const page = useHostBdsPage({ profileType: "leader" }); - const createDisabled = !page.abilities.canCreate; - const updateDisabled = !page.abilities.canUpdate; - const items = page.data.items || []; - const total = page.data.total || 0; - const toolbarActions = [ - page.abilities.canUpdate - ? { icon: , label: "BD Leader 状态", onClick: page.openBDStatusForm } - : null, - page.abilities.canCreate - ? { icon: , label: "创建 BD Leader", onClick: page.openBDLeaderForm, variant: "primary" } - : null - ].filter(Boolean); - return ( -
-
- + const page = useHostBdsPage({ profileType: "leader" }); + const createDisabled = !page.abilities.canCreate; + const items = page.data.items || []; + const total = page.data.total || 0; + const bdLeaderColumns = [ + ...bdLeaderBaseColumns, + page.abilities.canCreate || page.abilities.canUpdate + ? { + key: "actions", + label: "操作", + render: (item) => , + width: "minmax(118px, 0.7fr)", + } + : null, + ].filter(Boolean); + const toolbarActions = page.abilities.canCreate + ? [ + { + icon: , + label: "创建 BD Leader", + onClick: page.openBDLeaderForm, + variant: "primary", + }, + ] + : []; - -
- `bd-leader-${item.userId}`} - /> - {total > 0 ? : null} -
-
-
+ return ( +
+
+ - - page.setBDLeaderForm({ ...page.bdLeaderForm, targetUserId: event.target.value })} /> - page.setBDLeaderForm({ ...page.bdLeaderForm, regionId: value })} /> - page.setBDLeaderForm({ ...page.bdLeaderForm, reason: event.target.value })} /> - + +
+ `bd-leader-${item.userId}`} + /> + {total > 0 ? ( + + ) : null} +
+
+
- - page.setBDStatusForm({ ...page.bdStatusForm, targetUserId: event.target.value })} /> - page.setBDStatusForm({ ...page.bdStatusForm, status: event.target.value })}> - 启用 - 停用 - - page.setBDStatusForm({ ...page.bdStatusForm, reason: event.target.value })} /> - + + + page.setBDLeaderForm({ ...page.bdLeaderForm, targetUserId: event.target.value }) + } + /> + page.setBDLeaderForm({ ...page.bdLeaderForm, regionId: value })} + /> + page.setBDLeaderForm({ ...page.bdLeaderForm, reason: event.target.value })} + /> + -
- ); + + + page.setBDForm({ ...page.bdForm, targetUserId: event.target.value })} + /> + + page.setBDForm({ ...page.bdForm, reason: event.target.value })} + /> + + + + +
+ {(page.leaderBDs.items || []).length ? ( + page.leaderBDs.items.map((item) => ( +
+ + + + +
+ )) + ) : ( +
暂无下级 BD
+ )} +
+
+
+
+ ); } + +function PersonIdentity({ item }) { + const name = item.username || "-"; + const meta = item.displayUserId || item.userId || "-"; + return ( +
+ {item.avatar ? ( + + ) : ( + + {String(name || meta) + .slice(0, 1) + .toUpperCase()} + + )} + + {name} + {meta} + +
+ ); +} + +function CreatorIdentity({ item }) { + if (!item.createdByUserId && !item.createdByDisplayUserId && !item.createdByUsername) { + return "-"; + } + return ( + + ); +} + +function BDLeaderStatusSwitch({ item, page }) { + return ( + page.toggleBDLeader(item, event.target.checked)} + /> + ); +} + +function SubBDCount({ item, page }) { + return ( + + ); +} + +function BDLeaderActions({ item, page }) { + return ( + + {page.abilities.canCreate ? ( + page.openAddLeaderBD(item)}> + + + ) : null} + {page.abilities.canUpdate ? ( + page.removeBDLeader(item)} + > + + + ) : null} + + ); +} + +function UserLookupPreview({ keyword, lookup }) { + const hasKeyword = String(keyword || "").trim(); + if (!hasKeyword) { + return
输入用户短 ID 后自动搜索用户
; + } + if (lookup.loading) { + return
搜索中...
; + } + if (lookup.error) { + return
{lookup.error}
; + } + if (!lookup.user) { + return
未找到匹配用户,提交时后端会再次校验
; + } + return ( +
+ + {lookup.user.regionName || "未分配区域"} +
+ ); +} + +function regionName(item, regionOptions = []) { + if (item.regionName) { + return item.regionName; + } + const option = regionOptions.find((region) => String(region.value) === String(item.regionId)); + return option?.name || item.regionId || "-"; +} + +const dangerActionSx = { + borderColor: "rgba(239, 68, 68, 0.3)", + backgroundColor: "rgba(239, 68, 68, 0.08)", + color: "var(--danger)", + "&:hover": { + borderColor: "rgba(239, 68, 68, 0.5)", + backgroundColor: "rgba(239, 68, 68, 0.14)", + color: "var(--danger)", + }, +}; diff --git a/src/features/host-org/pages/HostBdsPage.jsx b/src/features/host-org/pages/HostBdsPage.jsx index cdbc2b4..f4b7e78 100644 --- a/src/features/host-org/pages/HostBdsPage.jsx +++ b/src/features/host-org/pages/HostBdsPage.jsx @@ -5,114 +5,271 @@ import TextField from "@mui/material/TextField"; import { formatMillis } from "@/shared/utils/time.js"; import { DataState } from "@/shared/ui/DataState.jsx"; import { PaginationBar } from "@/shared/ui/PaginationBar.jsx"; -import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx"; import { bdStatusFilters } from "@/features/host-org/constants.js"; import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx"; +import { HostOrgPerson, hostOrgRegionName } from "@/features/host-org/components/HostOrgIdentity.jsx"; import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx"; import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx"; import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx"; import { useHostBdsPage } from "@/features/host-org/hooks/useHostBdsPage.js"; +import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; import styles from "@/features/host-org/host-org.module.css"; const bdColumns = [ - { key: "userId", label: "用户 ID", width: "minmax(110px, 0.8fr)" }, - { key: "role", label: "角色", width: "minmax(110px, 0.8fr)" }, - { key: "parentLeaderUserId", label: "上级 Leader 用户 ID", render: (item) => item.parentLeaderUserId || "-" }, - { key: "regionId", label: "区域 ID", width: "minmax(90px, 0.7fr)" }, - { key: "status", label: "状态", render: (item) => , width: "minmax(90px, 0.7fr)" }, - { key: "createdByUserId", label: "创建人用户 ID" }, - { key: "createdAtMs", label: "创建时间", render: (item) => formatMillis(item.createdAtMs), width: "minmax(170px, 1fr)" }, - { key: "updatedAtMs", label: "更新时间", render: (item) => formatMillis(item.updatedAtMs), width: "minmax(170px, 1fr)" } + { + key: "user", + label: "用户", + render: (item) => , + width: "minmax(220px, 1.25fr)", + }, + { key: "role", label: "角色", width: "minmax(100px, 0.65fr)" }, + { + key: "parentLeaderUserId", + label: "上级 Leader", + render: (item) => , + width: "minmax(220px, 1.25fr)", + }, + { + key: "regionId", + label: "区域", + render: (item, _index, context) => hostOrgRegionName(item, context?.regionOptions), + width: "minmax(130px, 0.75fr)", + }, + { + key: "status", + label: "状态", + render: (item) => , + width: "minmax(90px, 0.7fr)", + }, + { + key: "createdByUserId", + label: "创建人", + render: (item) => , + width: "minmax(180px, 1fr)", + }, + { + key: "createdAtMs", + label: "创建时间", + render: (item) => formatMillis(item.createdAtMs), + width: "minmax(170px, 1fr)", + }, + { + key: "updatedAtMs", + label: "更新时间", + render: (item) => formatMillis(item.updatedAtMs), + width: "minmax(170px, 1fr)", + }, ]; export function HostBdsPage() { - const page = useHostBdsPage({ profileType: "bd" }); - const createDisabled = !page.abilities.canCreate; - const updateDisabled = !page.abilities.canUpdate; - const items = page.data.items || []; - const total = page.data.total || 0; - const toolbarActions = [ - page.abilities.canUpdate - ? { icon: , label: "BD 状态", onClick: page.openBDStatusForm } - : null, - page.abilities.canCreate - ? { icon: , label: "创建 BD", onClick: page.openBDForm, variant: "primary" } - : null - ].filter(Boolean); - return ( -
-
- + const page = useHostBdsPage({ profileType: "bd" }); + const createDisabled = !page.abilities.canCreate; + const updateDisabled = !page.abilities.canUpdate; + const items = page.data.items || []; + const total = page.data.total || 0; + const toolbarActions = [ + page.abilities.canUpdate + ? { icon: , label: "BD 状态", onClick: page.openBDStatusForm } + : null, + page.abilities.canCreate + ? { icon: , label: "创建 BD", onClick: page.openBDForm, variant: "primary" } + : null, + ].filter(Boolean); + const columns = bdColumns.map((column) => { + if (column.key === "user") { + return { + ...column, + filter: createTextColumnFilter({ + placeholder: "搜索用户 ID、展示 ID、用户名", + value: page.query, + onChange: page.changeQuery, + }), + }; + } + if (column.key === "parentLeaderUserId") { + return { + ...column, + filter: createTextColumnFilter({ + placeholder: "搜索 Leader 用户 ID", + value: page.parentLeaderUserId, + onChange: page.changeParentLeaderUserId, + }), + }; + } + if (column.key === "status") { + return { + ...column, + filter: createOptionsColumnFilter({ + options: bdStatusFilters, + placeholder: "搜索状态", + value: page.status, + onChange: page.changeStatus, + }), + }; + } + return column; + }); - -
- `bd-${item.userId}`} - /> - {total > 0 ? : null} -
-
-
+ return ( +
+
+ - - page.setBDForm({ ...page.bdForm, targetUserId: event.target.value })} /> - page.setBDForm({ ...page.bdForm, parentLeaderUserId: event.target.value })} /> - page.setBDForm({ ...page.bdForm, reason: event.target.value })} /> - + +
+ `bd-${item.userId}`} + /> + {total > 0 ? ( + + ) : null} +
+
+
- - page.setBDStatusForm({ ...page.bdStatusForm, targetUserId: event.target.value })} /> - page.setBDStatusForm({ ...page.bdStatusForm, status: event.target.value })}> - 启用 - 停用 - - page.setBDStatusForm({ ...page.bdStatusForm, reason: event.target.value })} /> - + + page.setBDForm({ ...page.bdForm, targetUserId: event.target.value })} + /> + page.setBDForm({ ...page.bdForm, parentLeaderUserId: event.target.value })} + /> + page.setBDForm({ ...page.bdForm, reason: event.target.value })} + /> + -
- ); + + + page.setBDStatusForm({ ...page.bdStatusForm, targetUserId: event.target.value }) + } + /> + page.setBDStatusForm({ ...page.bdStatusForm, status: event.target.value })} + > + 启用 + 停用 + + page.setBDStatusForm({ ...page.bdStatusForm, reason: event.target.value })} + /> + +
+ ); +} + +function BDUser({ item }) { + return ( + + ); +} + +function ParentLeader({ item }) { + if (!item.parentLeaderUserId) { + return "-"; + } + return ( + + ); +} + +function Creator({ item }) { + if (!item.createdByUserId && !item.createdByDisplayUserId && !item.createdByUsername) { + return "-"; + } + return ( + + ); } diff --git a/src/features/host-org/pages/HostCoinSellersPage.jsx b/src/features/host-org/pages/HostCoinSellersPage.jsx index 8d8522f..d667922 100644 --- a/src/features/host-org/pages/HostCoinSellersPage.jsx +++ b/src/features/host-org/pages/HostCoinSellersPage.jsx @@ -1,182 +1,374 @@ import Add from "@mui/icons-material/Add"; -import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined"; -import EditOutlined from "@mui/icons-material/EditOutlined"; +import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined"; import MenuItem from "@mui/material/MenuItem"; import Switch from "@mui/material/Switch"; import TextField from "@mui/material/TextField"; +import { + AdminFormAmountField, + AdminFormFieldGrid, + AdminFormReadOnlyField, + AdminFormSection, +} from "@/shared/ui/AdminFormDialog.jsx"; import { formatMillis } from "@/shared/utils/time.js"; import { DataState } from "@/shared/ui/DataState.jsx"; import { PaginationBar } from "@/shared/ui/PaginationBar.jsx"; import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx"; -import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx"; import { coinSellerStatusFilters } from "@/features/host-org/constants.js"; import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx"; import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx"; import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx"; import { useHostCoinSellersPage } from "@/features/host-org/hooks/useHostCoinSellersPage.js"; +import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx"; +import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; import styles from "@/features/host-org/host-org.module.css"; const baseColumns = [ - { - key: "seller", - label: "币商", - render: (item) => , - width: "minmax(220px, 1.3fr)" - }, - { key: "regionId", label: "区域", render: (item, _index, context) => regionName(item, context?.regionOptions), width: "minmax(140px, 0.8fr)" }, - { key: "merchantAssetType", label: "资产类型", render: (item) => item.merchantAssetType || "-", width: "minmax(150px, 0.9fr)" }, - { key: "merchantBalance", label: "币商余额", render: (item) => formatNumber(item.merchantBalance), width: "minmax(120px, 0.8fr)" }, - { key: "createdByUserId", label: "创建人 ID", render: (item) => item.createdByUserId || "-", width: "minmax(110px, 0.7fr)" }, - { key: "updatedAtMs", label: "更新时间", render: (item) => formatMillis(item.updatedAtMs), width: "minmax(170px, 1fr)" } + { + key: "seller", + label: "币商", + render: (item) => , + width: "minmax(220px, 1.3fr)", + }, + { + key: "regionId", + label: "区域", + render: (item, _index, context) => regionName(item, context?.regionOptions), + width: "minmax(140px, 0.8fr)", + }, + { + key: "contact", + label: "联系方式", + render: (item, _index, context) => , + width: "minmax(170px, 1fr)", + }, + { + key: "merchantAssetType", + label: "资产类型", + render: (item) => item.merchantAssetType || "-", + width: "minmax(150px, 0.9fr)", + }, + { + key: "merchantBalance", + label: "币商余额", + render: (item) => formatNumber(item.merchantBalance), + width: "minmax(120px, 0.8fr)", + }, + { + key: "createdByUserId", + label: "创建人 ID", + render: (item) => item.createdByUserId || "-", + width: "minmax(110px, 0.7fr)", + }, + { + key: "updatedAtMs", + label: "更新时间", + render: (item) => formatMillis(item.updatedAtMs), + width: "minmax(170px, 1fr)", + }, ]; export function HostCoinSellersPage() { - const page = useHostCoinSellersPage(); - const items = page.data.items || []; - const total = page.data.total || 0; - const createDisabled = !page.abilities.canCreate; - const updateDisabled = !page.abilities.canUpdate; - const columns = [ - ...baseColumns.slice(0, 2), - { - key: "status", - label: "状态", - render: (item) => , - width: "minmax(90px, 0.6fr)" - }, - ...baseColumns.slice(2), - page.abilities.canUpdate - ? { - key: "actions", - label: "操作", - render: (item) => , - width: "minmax(120px, 0.8fr)" - } - : null - ].filter(Boolean); - const toolbarActions = page.abilities.canCreate - ? [{ icon: , label: "添加币商", onClick: page.openCreateSeller, variant: "primary" }] - : []; + const page = useHostCoinSellersPage(); + const items = page.data.items || []; + const total = page.data.total || 0; + const createDisabled = !page.abilities.canCreate; + const updateDisabled = !page.abilities.canUpdate; + const columns = [ + ...baseColumns.slice(0, 2), + { + key: "status", + label: "状态", + render: (item) => , + width: "minmax(90px, 0.6fr)", + }, + ...baseColumns.slice(2), + page.abilities.canStockCredit + ? { + key: "actions", + label: "操作", + render: (item) => , + width: "minmax(90px, 0.5fr)", + } + : null, + ] + .filter(Boolean) + .map((column) => { + if (column.key === "seller") { + return { + ...column, + filter: createTextColumnFilter({ + placeholder: "搜索用户 ID、展示 ID、用户名", + value: page.query, + onChange: page.changeQuery, + }), + }; + } + if (column.key === "status") { + return { + ...column, + filter: createOptionsColumnFilter({ + options: coinSellerStatusFilters, + placeholder: "搜索状态", + value: page.status, + onChange: page.changeStatus, + }), + }; + } + return column; + }); + const regionFilter = createRegionColumnFilter({ + loading: page.loadingRegions, + options: page.regionOptions, + value: page.regionId, + onChange: page.changeRegionId, + }); + const toolbarActions = page.abilities.canCreate + ? [{ icon: , label: "添加币商", onClick: page.openCreateSeller, variant: "primary" }] + : []; - return ( -
-
- + return ( +
+
+ - -
- item.userId} - /> - {total > 0 ? : null} -
-
-
+ +
+ item.userId} + /> + {total > 0 ? ( + + ) : null} +
+
+
- - page.setCreateForm({ ...page.createForm, targetUserId: event.target.value })} /> - page.setCreateForm({ ...page.createForm, reason: event.target.value })} /> - + + page.setCreateForm({ ...page.createForm, targetUserId: event.target.value })} + /> + page.setCreateForm({ ...page.createForm, contact: event.target.value })} + /> + - - - page.setEditForm({ ...page.editForm, status: event.target.value })}> - 启用 - 停用 - - page.setEditForm({ ...page.editForm, reason: event.target.value })} /> - -
- ); + + + page.setEditForm({ ...page.editForm, contact: event.target.value })} + /> + + + + + + + + + + + + page.setStockForm({ + ...page.stockForm, + type: event.target.value, + rechargeAmount: "", + }) + } + > + USDT进货 + 金币补偿 + + + + page.setStockForm({ ...page.stockForm, coinAmount: event.target.value }) + } + /> + {page.stockForm.type === "usdt_purchase" ? ( + + page.setStockForm({ ...page.stockForm, rechargeAmount: event.target.value }) + } + /> + ) : null} + + + + page.setStockForm({ ...page.stockForm, reason: event.target.value })} + /> + + + + ); } function SellerIdentity({ item }) { - const name = item.username || "-"; - const meta = item.displayUserId || item.userId; + const name = item.username || "-"; + const meta = item.displayUserId || item.userId; - return ( -
- {item.avatar ? : {name.slice(0, 1).toUpperCase()}} - - {name} - {meta} - -
- ); + return ( +
+ {item.avatar ? ( + + ) : ( + {name.slice(0, 1).toUpperCase()} + )} + + {name} + {meta} + +
+ ); } function SellerStatusSwitch({ item, page }) { - return ( - page.toggleSeller(item, event.target.checked)} - /> - ); + return ( + page.toggleSeller(item, event.target.checked)} + /> + ); +} + +function SellerContact({ item, page }) { + const contact = String(item.contact || "").trim(); + if (!page?.abilities.canUpdate) { + return {contact || "未填写"}; + } + + return ( + + ); } function SellerActions({ item, page }) { - const deleting = page.loadingAction === `coin-seller-delete-${item.userId}`; - - return ( - - page.openEditSeller(item)}> - - - page.deleteSeller(item)}> - - - - ); + return ( + + {page.abilities.canStockCredit ? ( + page.openStockCredit(item)} + > + + + ) : null} + + ); } function regionName(item, regionOptions = []) { - if (item.regionName) { - return item.regionName; - } - const option = regionOptions.find((region) => String(region.value) === String(item.regionId)); - return option?.name || item.regionId || "-"; + if (item.regionName) { + return item.regionName; + } + const option = regionOptions.find((region) => String(region.value) === String(item.regionId)); + return option?.name || item.regionId || "-"; } function formatNumber(value) { - return Number(value || 0).toLocaleString("zh-CN"); + return Number(value || 0).toLocaleString("zh-CN"); } + +const coinCreditActionSx = { + borderColor: "rgba(245, 158, 11, 0.45)", + backgroundColor: "rgba(251, 191, 36, 0.16)", + color: "#b45309", + "&:hover": { + borderColor: "rgba(245, 158, 11, 0.72)", + backgroundColor: "rgba(251, 191, 36, 0.24)", + color: "#92400e", + }, + "& .MuiSvgIcon-root": { + color: "#f59e0b", + }, +}; diff --git a/src/features/host-org/pages/HostCountriesPage.jsx b/src/features/host-org/pages/HostCountriesPage.jsx index df868b0..2c8aa58 100644 --- a/src/features/host-org/pages/HostCountriesPage.jsx +++ b/src/features/host-org/pages/HostCountriesPage.jsx @@ -6,138 +6,211 @@ import TextField from "@mui/material/TextField"; import { formatMillis } from "@/shared/utils/time.js"; import { DataState } from "@/shared/ui/DataState.jsx"; import { - AdminActionIconButton, - AdminFilterSelect, - AdminListBody, - AdminListPage, - AdminListToolbar, - AdminRowActions, - AdminSearchBox + AdminActionIconButton, + AdminFilterResetButton, + AdminListBody, + AdminListPage, + AdminListToolbar, + AdminRowActions, } from "@/shared/ui/AdminListLayout.jsx"; import { countryEnabledFilters } from "@/features/host-org/constants.js"; import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx"; import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx"; import { useHostCountriesPage } from "@/features/host-org/hooks/useHostCountriesPage.js"; +import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; import styles from "@/features/host-org/host-org.module.css"; const baseCountryColumns = [ - { key: "countryId", label: "国家 ID", width: "minmax(90px, 0.6fr)" }, - { key: "flag", label: "国旗", render: (item) => {item.flag || "-"}, width: "minmax(70px, 0.4fr)" }, - { key: "countryCode", label: "国家码", width: "minmax(90px, 0.7fr)" }, - { key: "countryDisplayName", label: "展示名称", width: "minmax(150px, 1fr)" }, - { key: "countryName", label: "国家名称", width: "minmax(150px, 1fr)" }, - { key: "phoneCountryCode", label: "电话区号", width: "minmax(90px, 0.7fr)" }, - { key: "sortOrder", label: "排序", width: "minmax(80px, 0.5fr)" }, - { key: "updatedAtMs", label: "更新时间", render: (item) => formatMillis(item.updatedAtMs), width: "minmax(170px, 1fr)" } + { key: "countryId", label: "国家 ID", width: "minmax(90px, 0.6fr)" }, + { + key: "flag", + label: "国旗", + render: (item) => {item.flag || "-"}, + width: "minmax(70px, 0.4fr)", + }, + { key: "countryCode", label: "国家码", width: "minmax(90px, 0.7fr)" }, + { key: "countryDisplayName", label: "展示名称", width: "minmax(150px, 1fr)" }, + { key: "countryName", label: "国家名称", width: "minmax(150px, 1fr)" }, + { key: "phoneCountryCode", label: "电话区号", width: "minmax(90px, 0.7fr)" }, + { key: "sortOrder", label: "排序", width: "minmax(80px, 0.5fr)" }, + { + key: "updatedAtMs", + label: "更新时间", + render: (item) => formatMillis(item.updatedAtMs), + width: "minmax(170px, 1fr)", + }, ]; export function HostCountriesPage() { - const page = useHostCountriesPage(); - const createDisabled = !page.abilities.canCreate; - const updateDisabled = !page.abilities.canUpdate; - const items = page.data.items || []; - const total = page.data.total || 0; - const countryColumns = [ - ...baseCountryColumns.slice(0, 6), - { - key: "enabled", - label: "状态", - render: (item) => , - width: "minmax(90px, 0.6fr)" - }, - ...baseCountryColumns.slice(6) - ]; - const columns = page.abilities.canUpdate - ? [ - ...countryColumns, - { - key: "actions", - label: "操作", - render: (item) => , - width: "minmax(104px, 0.7fr)" - } - ] - : countryColumns; - return ( - - - - , + filter: createOptionsColumnFilter({ + options: countryEnabledFilters, + placeholder: "搜索状态", + value: page.enabledStatus, + onChange: page.changeEnabledStatus, + }), + width: "minmax(90px, 0.6fr)", + }, + ...baseCountryColumns.slice(6), + ].map((column) => + column.key === "countryCode" + ? { + ...column, + filter: createTextColumnFilter({ + placeholder: "搜索国家码、国家名称", + value: page.query, + onChange: page.changeQuery, + }), + } + : column, + ); + const columns = page.abilities.canUpdate + ? [ + ...countryColumns, + { + key: "actions", + label: "操作", + render: (item) => , + width: "minmax(104px, 0.7fr)", + }, + ] + : countryColumns; + return ( + + + } + actions={ + page.abilities.canCreate ? ( + + + + ) : null + } /> - - )} - actions={page.abilities.canCreate ? ( - - - - ) : null} - /> - - - item.countryId} - /> -
- {total} 条 -
-
-
+ + + item.countryId} /> +
+ {total} 条 +
+
+
- - page.setCountryForm({ ...page.countryForm, countryCode: event.target.value })} /> - page.setCountryForm({ ...page.countryForm, countryDisplayName: event.target.value })} /> - page.setCountryForm({ ...page.countryForm, countryName: event.target.value })} /> - page.setCountryForm({ ...page.countryForm, isoAlpha3: event.target.value })} /> - page.setCountryForm({ ...page.countryForm, isoNumeric: event.target.value })} /> - page.setCountryForm({ ...page.countryForm, phoneCountryCode: event.target.value })} /> - page.setCountryForm({ ...page.countryForm, flag: event.target.value })} /> - page.setCountryForm({ ...page.countryForm, sortOrder: event.target.value })} /> - {page.activeAction === "create" ? ( - page.setCountryForm({ ...page.countryForm, enabled: event.target.checked })} />} - label={page.countryForm.enabled ? "启用" : "停用"} - /> - ) : null} - - -
- ); + + page.setCountryForm({ ...page.countryForm, countryCode: event.target.value })} + /> + + page.setCountryForm({ ...page.countryForm, countryDisplayName: event.target.value }) + } + /> + page.setCountryForm({ ...page.countryForm, countryName: event.target.value })} + /> + page.setCountryForm({ ...page.countryForm, isoAlpha3: event.target.value })} + /> + page.setCountryForm({ ...page.countryForm, isoNumeric: event.target.value })} + /> + + page.setCountryForm({ ...page.countryForm, phoneCountryCode: event.target.value }) + } + /> + page.setCountryForm({ ...page.countryForm, flag: event.target.value })} + /> + page.setCountryForm({ ...page.countryForm, sortOrder: event.target.value })} + /> + {page.activeAction === "create" ? ( + + page.setCountryForm({ ...page.countryForm, enabled: event.target.checked }) + } + /> + } + label={page.countryForm.enabled ? "启用" : "停用"} + /> + ) : null} + +
+ ); } function CountryActions({ item, page }) { - return ( - - {page.abilities.canUpdate ? ( - page.openEditCountry(item)}> - - - ) : null} - - ); + return ( + + {page.abilities.canUpdate ? ( + page.openEditCountry(item)}> + + + ) : null} + + ); } function CountryStatusSwitch({ item, page }) { - return ( - page.toggleCountry(item, event.target.checked)} - /> - ); + return ( + page.toggleCountry(item, event.target.checked)} + /> + ); } diff --git a/src/features/host-org/pages/HostHostsPage.jsx b/src/features/host-org/pages/HostHostsPage.jsx index 8785801..dca38b7 100644 --- a/src/features/host-org/pages/HostHostsPage.jsx +++ b/src/features/host-org/pages/HostHostsPage.jsx @@ -1,73 +1,170 @@ import { formatMillis } from "@/shared/utils/time.js"; import { DataState } from "@/shared/ui/DataState.jsx"; import { PaginationBar } from "@/shared/ui/PaginationBar.jsx"; -import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx"; import { hostSourceLabels, hostStatusFilters } from "@/features/host-org/constants.js"; +import { HostOrgEntity, HostOrgPerson, hostOrgRegionName } from "@/features/host-org/components/HostOrgIdentity.jsx"; import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx"; import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx"; import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx"; import { useHostHostsPage } from "@/features/host-org/hooks/useHostHostsPage.js"; +import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; import styles from "@/features/host-org/host-org.module.css"; const hostColumns = [ - { key: "userId", label: "用户 ID", width: "minmax(110px, 0.8fr)" }, - { key: "status", label: "状态", render: (item) => , width: "minmax(90px, 0.7fr)" }, - { key: "regionId", label: "区域 ID", width: "minmax(90px, 0.7fr)" }, - { key: "currentAgencyId", label: "当前 Agency ID", render: (item) => item.currentAgencyId || "-" }, - { key: "currentMembershipId", label: "当前 Membership ID", render: (item) => item.currentMembershipId || "-" }, - { key: "source", label: "来源", render: (item) => hostSourceLabels[item.source] || item.source || "-", width: "minmax(150px, 1fr)" }, - { key: "firstBecameHostAtMs", label: "首次成为主播", render: (item) => formatMillis(item.firstBecameHostAtMs), width: "minmax(170px, 1fr)" }, - { key: "updatedAtMs", label: "更新时间", render: (item) => formatMillis(item.updatedAtMs), width: "minmax(170px, 1fr)" } + { + key: "user", + label: "用户", + render: (item) => , + width: "minmax(220px, 1.25fr)", + }, + { + key: "status", + label: "状态", + render: (item) => , + width: "minmax(90px, 0.7fr)", + }, + { + key: "regionId", + label: "区域", + render: (item, _index, context) => hostOrgRegionName(item, context?.regionOptions), + width: "minmax(130px, 0.75fr)", + }, + { + key: "currentAgencyId", + label: "当前 Agency", + render: (item) => ( + + ), + width: "minmax(190px, 1fr)", + }, + { + key: "currentMembershipId", + label: "当前 Membership", + render: (item) => + item.currentMembershipId ? ( + + ) : ( + "-" + ), + width: "minmax(170px, 0.9fr)", + }, + { + key: "source", + label: "来源", + render: (item) => hostSourceLabels[item.source] || item.source || "-", + width: "minmax(150px, 1fr)", + }, + { + key: "firstBecameHostAtMs", + label: "首次成为主播", + render: (item) => formatMillis(item.firstBecameHostAtMs), + width: "minmax(170px, 1fr)", + }, + { + key: "updatedAtMs", + label: "更新时间", + render: (item) => formatMillis(item.updatedAtMs), + width: "minmax(170px, 1fr)", + }, ]; export function HostHostsPage() { - const page = useHostHostsPage(); - const items = page.data.items || []; - const total = page.data.total || 0; + const page = useHostHostsPage(); + const items = page.data.items || []; + const total = page.data.total || 0; + const columns = hostColumns.map((column) => { + if (column.key === "user") { + return { + ...column, + filter: createTextColumnFilter({ + placeholder: "搜索用户 ID、展示 ID、用户名、Agency", + value: page.query, + onChange: page.changeQuery, + }), + }; + } + if (column.key === "currentAgencyId") { + return { + ...column, + filter: createTextColumnFilter({ + placeholder: "搜索 Agency ID", + value: page.agencyId, + onChange: page.changeAgencyId, + }), + }; + } + if (column.key === "status") { + return { + ...column, + filter: createOptionsColumnFilter({ + options: hostStatusFilters, + placeholder: "搜索状态", + value: page.status, + onChange: page.changeStatus, + }), + }; + } + return column; + }); - return ( -
-
- + return ( +
+
+ - -
- item.userId} - /> - {total > 0 ? : null} -
-
-
-
- ); + +
+ item.userId} + /> + {total > 0 ? ( + + ) : null} +
+
+
+
+ ); +} + +function HostUser({ item }) { + return ( + + ); } diff --git a/src/features/host-org/pages/HostRegionsPage.jsx b/src/features/host-org/pages/HostRegionsPage.jsx index 57db3cc..456a01f 100644 --- a/src/features/host-org/pages/HostRegionsPage.jsx +++ b/src/features/host-org/pages/HostRegionsPage.jsx @@ -5,192 +5,231 @@ import TextField from "@mui/material/TextField"; import Tooltip from "@mui/material/Tooltip"; import { formatMillis } from "@/shared/utils/time.js"; import { DataState } from "@/shared/ui/DataState.jsx"; +import { AdminFilterResetButton } from "@/shared/ui/AdminListLayout.jsx"; import { IconButton } from "@/shared/ui/IconButton.jsx"; import { MultiValueAutocomplete } from "@/shared/ui/MultiValueAutocomplete.jsx"; -import { SearchBox } from "@/shared/ui/SearchBox.jsx"; -import { StatusSelect } from "@/shared/ui/StatusSelect.jsx"; import { regionStatusFilters } from "@/features/host-org/constants.js"; import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx"; import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx"; import { useHostRegionsPage } from "@/features/host-org/hooks/useHostRegionsPage.js"; +import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; import styles from "@/features/host-org/host-org.module.css"; const baseRegionColumns = [ - { key: "regionId", label: "区域 ID", width: "minmax(90px, 0.6fr)" }, - { key: "regionCode", label: "区域编码", width: "minmax(110px, 0.8fr)" }, - { key: "name", label: "区域名称", width: "minmax(150px, 1fr)" }, - { key: "countries", label: "国家码", render: (item) => , width: "minmax(220px, 1.5fr)" }, - { key: "sortOrder", label: "排序", width: "minmax(80px, 0.5fr)" }, - { key: "updatedAtMs", label: "更新时间", render: (item) => formatMillis(item.updatedAtMs), width: "minmax(170px, 1fr)" } + { key: "regionId", label: "区域 ID", width: "minmax(90px, 0.6fr)" }, + { key: "regionCode", label: "区域编码", width: "minmax(110px, 0.8fr)" }, + { key: "name", label: "区域名称", width: "minmax(150px, 1fr)" }, + { + key: "countries", + label: "国家码", + render: (item) => , + width: "minmax(220px, 1.5fr)", + }, + { key: "sortOrder", label: "排序", width: "minmax(80px, 0.5fr)" }, + { + key: "updatedAtMs", + label: "更新时间", + render: (item) => formatMillis(item.updatedAtMs), + width: "minmax(170px, 1fr)", + }, ]; export function HostRegionsPage() { - const page = useHostRegionsPage(); - const createDisabled = !page.abilities.canCreate; - const updateDisabled = !page.abilities.canUpdate; - const items = page.data.items || []; - const total = page.data.total || 0; - const regionColumns = [ - ...baseRegionColumns.slice(0, 3), - { - key: "status", - label: "状态", - render: (item) => , - width: "minmax(90px, 0.6fr)" - }, - ...baseRegionColumns.slice(3) - ]; - const columns = page.abilities.canUpdate - ? [ - ...regionColumns, - { - key: "actions", - label: "操作", - render: (item) => , - width: "minmax(80px, 0.5fr)" - } - ] - : regionColumns; - return ( -
-
-
-
-
- - -
-
+ const page = useHostRegionsPage(); + const createDisabled = !page.abilities.canCreate; + const updateDisabled = !page.abilities.canUpdate; + const items = page.data.items || []; + const total = page.data.total || 0; + const regionColumns = [ + ...baseRegionColumns.slice(0, 3), + { + key: "status", + label: "状态", + render: (item) => , + filter: createOptionsColumnFilter({ + options: regionStatusFilters, + placeholder: "搜索状态", + value: page.status, + onChange: page.changeStatus, + }), + width: "minmax(90px, 0.6fr)", + }, + ...baseRegionColumns.slice(3), + ].map((column) => + column.key === "regionCode" + ? { + ...column, + filter: createTextColumnFilter({ + placeholder: "搜索区域、国家码", + value: page.query, + onChange: page.changeQuery, + }), + } + : column, + ); + const columns = page.abilities.canUpdate + ? [ + ...regionColumns, + { + key: "actions", + label: "操作", + render: (item) => , + width: "minmax(80px, 0.5fr)", + }, + ] + : regionColumns; + return ( +
+
+
+
+
+ +
+
- {page.abilities.canCreate ? ( -
- - - - - - - + {page.abilities.canCreate ? ( +
+ + + + + + + +
+ ) : null} +
+ + +
+ item.regionId} /> +
+ {total} 条 +
+
+
- ) : null} -
- -
- item.regionId} - /> -
- {total} 条 -
-
-
-
- - - page.setRegionForm({ ...page.regionForm, regionCode: event.target.value })} /> - page.setRegionForm({ ...page.regionForm, name: event.target.value })} /> - page.setRegionForm({ ...page.regionForm, sortOrder: event.target.value })} /> - page.setRegionForm({ ...page.regionForm, countries })} - /> - - -
- ); + + page.setRegionForm({ ...page.regionForm, regionCode: event.target.value })} + /> + page.setRegionForm({ ...page.regionForm, name: event.target.value })} + /> + page.setRegionForm({ ...page.regionForm, sortOrder: event.target.value })} + /> + page.setRegionForm({ ...page.regionForm, countries })} + /> + + + ); } function RegionActions({ item, page }) { - if (item.regionCode === "GLOBAL") { - return "-"; - } - return ( -
- {page.abilities.canUpdate ? ( - - - page.openEditRegion(item)}> - - - - - ) : null} -
- ); + if (item.regionCode === "GLOBAL") { + return "-"; + } + return ( +
+ {page.abilities.canUpdate ? ( + + + page.openEditRegion(item)}> + + + + + ) : null} +
+ ); } function RegionStatusSwitch({ item, page }) { - const checked = item.status === "active"; - const isGlobal = item.regionCode === "GLOBAL"; - return ( - page.toggleRegion(item, event.target.checked)} - /> - ); + const checked = item.status === "active"; + const isGlobal = item.regionCode === "GLOBAL"; + return ( + page.toggleRegion(item, event.target.checked)} + /> + ); } function CountryCodeSelect({ disabled, label, labels, loading, onChange, options, value }) { - const selected = value || []; - const mergedOptions = [...new Set([...options, ...selected])].sort(); - return ( - - ); + const selected = value || []; + const mergedOptions = [...new Set([...options, ...selected])].sort(); + return ( + + ); } function CountryCodes({ countries = [] }) { - const items = Array.isArray(countries) ? countries : []; - if (!items.length) { - return "-"; - } - return ( -
- {items.map((country) => ( - - {country} - - ))} -
- ); + const items = Array.isArray(countries) ? countries : []; + if (!items.length) { + return "-"; + } + return ( +
+ {items.map((country) => ( + + {country} + + ))} +
+ ); } const primaryActionSx = { - borderColor: "var(--primary)", - backgroundColor: "var(--primary)", - color: "var(--active-contrast)", - "&:hover": { - borderColor: "var(--primary-strong)", - backgroundColor: "var(--primary-strong)", - color: "var(--active-contrast)" - } + borderColor: "var(--primary)", + backgroundColor: "var(--primary)", + color: "var(--active-contrast)", + "&:hover": { + borderColor: "var(--primary-strong)", + backgroundColor: "var(--primary-strong)", + color: "var(--active-contrast)", + }, }; diff --git a/src/features/host-org/permissions.js b/src/features/host-org/permissions.js index 816809a..09fa625 100644 --- a/src/features/host-org/permissions.js +++ b/src/features/host-org/permissions.js @@ -8,7 +8,7 @@ export function useCountryAbilities() { canCreate: can(PERMISSIONS.countryCreate), canStatus: can(PERMISSIONS.countryStatus), canUpdate: can(PERMISSIONS.countryUpdate), - canView: can(PERMISSIONS.countryView) + canView: can(PERMISSIONS.countryView), }; } @@ -19,7 +19,7 @@ export function useRegionAbilities() { canCreate: can(PERMISSIONS.regionCreate), canStatus: can(PERMISSIONS.regionStatus), canUpdate: can(PERMISSIONS.regionUpdate), - canView: can(PERMISSIONS.regionView) + canView: can(PERMISSIONS.regionView), }; } @@ -29,7 +29,7 @@ export function useAgencyAbilities() { return { canCreate: can(PERMISSIONS.agencyCreate), canStatus: can(PERMISSIONS.agencyStatus), - canView: can(PERMISSIONS.agencyView) + canView: can(PERMISSIONS.agencyView), }; } @@ -37,7 +37,7 @@ export function useHostAbilities() { const { can } = useAuth(); return { - canView: can(PERMISSIONS.hostView) + canView: can(PERMISSIONS.hostView), }; } @@ -47,7 +47,7 @@ export function useBDAbilities() { return { canCreate: can(PERMISSIONS.bdCreate), canUpdate: can(PERMISSIONS.bdUpdate), - canView: can(PERMISSIONS.bdView) + canView: can(PERMISSIONS.bdView), }; } @@ -56,7 +56,8 @@ export function useCoinSellerAbilities() { return { canCreate: can(PERMISSIONS.coinSellerCreate), + canStockCredit: can(PERMISSIONS.coinSellerStockCredit), canUpdate: can(PERMISSIONS.coinSellerUpdate), - canView: can(PERMISSIONS.coinSellerView) + canView: can(PERMISSIONS.coinSellerView), }; } diff --git a/src/features/host-org/schema.ts b/src/features/host-org/schema.ts index 31d20f5..a72fe36 100644 --- a/src/features/host-org/schema.ts +++ b/src/features/host-org/schema.ts @@ -2,12 +2,13 @@ import { z } from "zod"; const commandBaseSchema = z.object({ commandId: z.string().trim().min(1, "请输入 Command ID").max(120, "Command ID 不能超过 120 个字符"), - reason: z.string().trim().max(200, "原因不能超过 200 个字符").optional().default("") + reason: z.string().trim().max(200, "原因不能超过 200 个字符").optional().default(""), }); const commandContactSchema = z.object({ commandId: z.string().trim().min(1, "请输入 Command ID").max(120, "Command ID 不能超过 120 个字符"), - reason: z.string().trim().max(200, "联系方式不能超过 200 个字符").optional().default("") + contact: z.string().trim().max(128, "联系方式不能超过 128 个字符").optional().default(""), + reason: z.string().trim().max(200, "原因不能超过 200 个字符").optional().default(""), }); const userIdSchema = z.coerce.number().int().positive("请输入有效用户短 ID"); @@ -16,9 +17,7 @@ const sortOrderSchema = z.coerce.number().int().min(0, "排序不能小于 0").d const optionalTextSchema = (max: number, message: string) => z.string().trim().max(max, message).optional().default(""); const codeSchema = (label: string) => z.string().trim().min(1, `请输入${label}`).max(40, `${label}不能超过 40 个字符`); const countryCodesSchema = z.array(z.string()).transform((values, ctx) => { - const countries = values - .map((item) => item.trim().toUpperCase()) - .filter(Boolean); + const countries = values.map((item) => item.trim().toUpperCase()).filter(Boolean); if (new Set(countries).size !== countries.length) { ctx.addIssue({ code: "custom", message: "国家码不能重复" }); } @@ -31,74 +30,100 @@ const retiredRegionCodes = new Set([ "MICRONESIA", "POLYNESIA", "SOUTHERN AFRICA", - "UNSPECIFIED" + "UNSPECIFIED", ]); const requiredCountryCodesSchema = countryCodesSchema.superRefine((countries, ctx) => { if (!countries.length) { ctx.addIssue({ code: "custom", message: "请选择国家码" }); } }); -const regionBaseSchema = z.object({ - countries: countryCodesSchema, - name: z.string().trim().min(1, "请输入区域名称").max(80, "区域名称不能超过 80 个字符"), - regionCode: codeSchema("区域编码").transform((value) => value.toUpperCase()), - sortOrder: sortOrderSchema -}).superRefine((value, ctx) => { - if (retiredRegionCodes.has(value.regionCode)) { - ctx.addIssue({ code: "custom", message: "该区域已移除", path: ["regionCode"] }); - } - if (value.regionCode !== "GLOBAL" && !value.countries.length) { - ctx.addIssue({ code: "custom", message: "请选择国家码", path: ["countries"] }); - } - if (value.regionCode === "GLOBAL" && value.countries.length) { - ctx.addIssue({ code: "custom", message: "GLOBAL 区域不需要选择国家", path: ["countries"] }); - } -}); +const regionBaseSchema = z + .object({ + countries: countryCodesSchema, + name: z.string().trim().min(1, "请输入区域名称").max(80, "区域名称不能超过 80 个字符"), + regionCode: codeSchema("区域编码").transform((value) => value.toUpperCase()), + sortOrder: sortOrderSchema, + }) + .superRefine((value, ctx) => { + if (retiredRegionCodes.has(value.regionCode)) { + ctx.addIssue({ code: "custom", message: "该区域已移除", path: ["regionCode"] }); + } + if (value.regionCode !== "GLOBAL" && !value.countries.length) { + ctx.addIssue({ code: "custom", message: "请选择国家码", path: ["countries"] }); + } + if (value.regionCode === "GLOBAL" && value.countries.length) { + ctx.addIssue({ code: "custom", message: "GLOBAL 区域不需要选择国家", path: ["countries"] }); + } + }); export const createBDLeaderSchema = commandContactSchema.extend({ regionId: regionIdSchema, - targetUserId: userIdSchema + targetUserId: userIdSchema, }); export const createBDSchema = commandBaseSchema.extend({ parentLeaderUserId: userIdSchema, - targetUserId: userIdSchema + targetUserId: userIdSchema, }); export const bdStatusSchema = commandBaseSchema.extend({ status: z.string().trim().min(1, "请输入状态").max(40, "状态不能超过 40 个字符"), targetType: z.enum(["bd", "leader"]), - targetUserId: userIdSchema + targetUserId: userIdSchema, }); export const createCoinSellerSchema = commandContactSchema.extend({ - targetUserId: userIdSchema + targetUserId: userIdSchema, }); export const coinSellerStatusSchema = commandContactSchema.extend({ status: z.enum(["active", "disabled"]), - targetUserId: userIdSchema + targetUserId: userIdSchema, }); +export const coinSellerStockCreditSchema = z + .object({ + coinAmount: z.coerce.number().int().positive("请输入充值金币"), + commandId: z.string().trim().min(1, "请输入 Command ID").max(120, "Command ID 不能超过 120 个字符"), + reason: z.string().trim().max(512, "原因不能超过 512 个字符").optional().default(""), + rechargeAmount: z.string().trim().max(32, "充值金额不能超过 32 个字符").optional().default(""), + type: z.enum(["usdt_purchase", "coin_compensation"]), + }) + .superRefine((value, ctx) => { + if (value.type === "usdt_purchase") { + if (!/^(0|[1-9]\d*)(\.\d{1,6})?$/.test(value.rechargeAmount) || Number(value.rechargeAmount) <= 0) { + ctx.addIssue({ code: "custom", message: "请输入最多 6 位小数的 USDT 金额", path: ["rechargeAmount"] }); + } + return; + } + if (value.rechargeAmount) { + ctx.addIssue({ code: "custom", message: "金币补偿不能提交充值金额", path: ["rechargeAmount"] }); + } + }); + export const createAgencySchema = commandBaseSchema.extend({ joinEnabled: z.boolean().default(true), maxHosts: z.coerce.number().int().min(0, "最大主播数不能小于 0").default(0), name: z.string().trim().min(1, "请输入 Agency 名称").max(80, "Agency 名称不能超过 80 个字符"), ownerUserId: userIdSchema, - parentBdUserId: userIdSchema + parentBdUserId: userIdSchema, }); export const agencyCloseSchema = commandBaseSchema.extend({ - agencyId: z.coerce.number().int().positive("请输入有效 Agency ID") + agencyId: z.coerce.number().int().positive("请输入有效 Agency ID"), }); export const agencyJoinEnabledSchema = commandBaseSchema.extend({ agencyId: z.coerce.number().int().positive("请输入有效 Agency ID"), - joinEnabled: z.boolean().default(true) + joinEnabled: z.boolean().default(true), }); export const countryCreateSchema = z.object({ - countryCode: z.string().trim().regex(/^[A-Za-z]{2,3}$/, "国家码必须是 2-3 位字母").transform((value) => value.toUpperCase()), + countryCode: z + .string() + .trim() + .regex(/^[A-Za-z]{2,3}$/, "国家码必须是 2-3 位字母") + .transform((value) => value.toUpperCase()), countryDisplayName: z.string().trim().min(1, "请输入展示名称").max(80, "展示名称不能超过 80 个字符"), countryName: z.string().trim().min(1, "请输入国家名称").max(80, "国家名称不能超过 80 个字符"), enabled: z.boolean().default(true), @@ -106,12 +131,12 @@ export const countryCreateSchema = z.object({ isoAlpha3: optionalTextSchema(8, "ISO Alpha-3 不能超过 8 个字符").transform((value) => value.toUpperCase()), isoNumeric: optionalTextSchema(8, "ISO Numeric 不能超过 8 个字符"), phoneCountryCode: optionalTextSchema(16, "电话区号不能超过 16 个字符"), - sortOrder: sortOrderSchema + sortOrder: sortOrderSchema, }); export const countryUpdateSchema = countryCreateSchema.omit({ countryCode: true, - enabled: true + enabled: true, }); export const regionCreateSchema = regionBaseSchema; @@ -119,7 +144,7 @@ export const regionCreateSchema = regionBaseSchema; export const regionUpdateSchema = regionBaseSchema; export const regionCountriesSchema = z.object({ - countries: requiredCountryCodesSchema + countries: requiredCountryCodesSchema, }); export type CreateBDLeaderForm = z.infer; @@ -127,6 +152,7 @@ export type CreateBDForm = z.infer; export type BDStatusForm = z.infer; export type CreateCoinSellerForm = z.infer; export type CoinSellerStatusForm = z.infer; +export type CoinSellerStockCreditForm = z.infer; export type CreateAgencyForm = z.infer; export type AgencyCloseForm = z.infer; export type AgencyJoinEnabledForm = z.infer; diff --git a/src/features/logs/components/LogFilters.jsx b/src/features/logs/components/LogFilters.jsx index ad9b547..48b9606 100644 --- a/src/features/logs/components/LogFilters.jsx +++ b/src/features/logs/components/LogFilters.jsx @@ -1,9 +1,9 @@ -import { SearchBox } from "@/shared/ui/SearchBox.jsx"; +import { AdminFilterResetButton } from "@/shared/ui/AdminListLayout.jsx"; -export function LogFilters({ onQueryChange, query }) { - return ( -
- -
- ); +export function LogFilters({ onReset, query }) { + return ( +
+ +
+ ); } diff --git a/src/features/logs/components/LogTable.jsx b/src/features/logs/components/LogTable.jsx index a940510..7bd8758 100644 --- a/src/features/logs/components/LogTable.jsx +++ b/src/features/logs/components/LogTable.jsx @@ -1,10 +1,20 @@ import { DataTable } from "@/shared/ui/DataTable.jsx"; +import { createTextColumnFilter } from "@/shared/ui/tableFilters.js"; -export function LogTable({ data, isLogin }) { +export function LogTable({ data, isLogin, onQueryChange, query }) { const items = data.items || []; const columns = isLogin ? [ - { key: "username", label: "用户", width: "minmax(140px, 1fr)" }, + { + key: "username", + label: "用户", + width: "minmax(140px, 1fr)", + filter: createTextColumnFilter({ + placeholder: "搜索用户、IP、动作、资源", + value: query, + onChange: onQueryChange + }) + }, { key: "ip", label: "IP", width: "minmax(140px, 1fr)", render: (item) => {item.ip || "-"} }, { key: "status", label: "状态", width: "110px" }, { @@ -21,7 +31,16 @@ export function LogTable({ data, isLogin }) { } ] : [ - { key: "username", label: "用户", width: "minmax(140px, 1fr)" }, + { + key: "username", + label: "用户", + width: "minmax(140px, 1fr)", + filter: createTextColumnFilter({ + placeholder: "搜索用户、IP、动作、资源", + value: query, + onChange: onQueryChange + }) + }, { key: "action", label: "动作", width: "minmax(120px, 1fr)" }, { key: "resource", diff --git a/src/features/logs/hooks/useLogsPage.js b/src/features/logs/hooks/useLogsPage.js index 87295ce..e423ca1 100644 --- a/src/features/logs/hooks/useLogsPage.js +++ b/src/features/logs/hooks/useLogsPage.js @@ -9,46 +9,52 @@ const pageSize = 10; const emptyData = { items: [], total: 0, page: 1, pageSize }; export function useLogsPage(type) { - const [query, setQuery] = useState(""); - const [page, setPage] = useState(1); - const abilities = useLogAbilities(); - const isLogin = type === "login"; + const [query, setQuery] = useState(""); + const [page, setPage] = useState(1); + const abilities = useLogAbilities(); + const isLogin = type === "login"; - const requestQuery = useMemo(() => toPageQuery({ keyword: query, page, pageSize }), [page, query]); - const queryFn = useCallback(async () => { - const loader = isLogin ? listLoginLogs : listOperationLogs; - return loader(requestQuery); - }, [isLogin, requestQuery]); + const requestQuery = useMemo(() => toPageQuery({ keyword: query, page, pageSize }), [page, query]); + const queryFn = useCallback(async () => { + const loader = isLogin ? listLoginLogs : listOperationLogs; + return loader(requestQuery); + }, [isLogin, requestQuery]); - const { data, error, loading, reload } = useAdminQuery(queryFn, { - errorMessage: "加载日志失败", - initialData: emptyData, - keepPreviousData: true, - queryKey: ["logs", type, requestQuery] - }); + const { data, error, loading, reload } = useAdminQuery(queryFn, { + errorMessage: "加载日志失败", + initialData: emptyData, + keepPreviousData: true, + queryKey: ["logs", type, requestQuery], + }); - const changeQuery = (value) => { - setQuery(value); - setPage(1); - }; + const changeQuery = (value) => { + setQuery(value); + setPage(1); + }; - const downloadLogs = () => { - const exporter = isLogin ? exportLoginLogs : exportOperationLogs; - const filename = isLogin ? "hyapp-login-logs.csv" : "hyapp-operation-logs.csv"; - return downloadCsv(() => exporter({ keyword: query }), filename); - }; + const resetFilters = () => { + setQuery(""); + setPage(1); + }; - return { - abilities, - changeQuery, - data, - downloadLogs, - error, - isLogin, - loading, - page, - query, - reload, - setPage - }; + const downloadLogs = () => { + const exporter = isLogin ? exportLoginLogs : exportOperationLogs; + const filename = isLogin ? "hyapp-login-logs.csv" : "hyapp-operation-logs.csv"; + return downloadCsv(() => exporter({ keyword: query }), filename); + }; + + return { + abilities, + changeQuery, + data, + downloadLogs, + error, + isLogin, + loading, + page, + query, + reload, + resetFilters, + setPage, + }; } diff --git a/src/features/logs/pages/LogsPage.jsx b/src/features/logs/pages/LogsPage.jsx index b047ace..2480d5a 100644 --- a/src/features/logs/pages/LogsPage.jsx +++ b/src/features/logs/pages/LogsPage.jsx @@ -6,17 +6,22 @@ import { LogToolbar } from "@/features/logs/components/LogToolbar.jsx"; import { useLogsPage } from "@/features/logs/hooks/useLogsPage.js"; export function LogsPage({ type }) { - const page = useLogsPage(type); + const page = useLogsPage(type); - return ( - <> - - + return ( + <> + + - - - - - - ); + + + + + + ); } diff --git a/src/features/menus/components/MenuTable.jsx b/src/features/menus/components/MenuTable.jsx index 7c7211b..fec349e 100644 --- a/src/features/menus/components/MenuTable.jsx +++ b/src/features/menus/components/MenuTable.jsx @@ -10,8 +10,9 @@ import { adminListClasses } from "@/shared/ui/AdminListLayout.jsx"; import { DataTable } from "@/shared/ui/DataTable.jsx"; +import { createTextColumnFilter } from "@/shared/ui/tableFilters.js"; -export function MenuTable({ abilities, flatMenus, onEdit, onReorder, onRemove, onToggleVisible }) { +export function MenuTable({ abilities, flatMenus, onEdit, onQueryChange, onReorder, onRemove, onToggleVisible, query }) { const [draggingMenu, setDraggingMenu] = useState(null); const [dragOverId, setDragOverId] = useState(null); const columns = [ @@ -25,6 +26,11 @@ export function MenuTable({ abilities, flatMenus, onEdit, onReorder, onRemove, o key: "label", label: "菜单", width: "minmax(180px, 1.2fr)", + filter: createTextColumnFilter({ + placeholder: "搜索菜单、权限码", + value: query, + onChange: onQueryChange + }), render: (menu) => {menu.label} }, { key: "path", label: "路由", width: "minmax(180px, 1.2fr)", render: (menu) => {menu.path || "-"} }, diff --git a/src/features/menus/components/PermissionTable.jsx b/src/features/menus/components/PermissionTable.jsx index 8537ff9..bee7af3 100644 --- a/src/features/menus/components/PermissionTable.jsx +++ b/src/features/menus/components/PermissionTable.jsx @@ -3,14 +3,24 @@ import EditOutlined from "@mui/icons-material/EditOutlined"; import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx"; import { DataTable } from "@/shared/ui/DataTable.jsx"; import { kindLabel } from "@/features/menus/constants.js"; +import { createTextColumnFilter } from "@/shared/ui/tableFilters.js"; -export function PermissionTable({ abilities, onEdit, onRemove, permissions }) { +export function PermissionTable({ abilities, onEdit, onQueryChange, onRemove, permissions, query }) { if (!abilities.canLoadPermissions) { return null; } const columns = [ - { key: "name", label: "权限", width: "minmax(160px, 1fr)" }, + { + key: "name", + label: "权限", + width: "minmax(160px, 1fr)", + filter: createTextColumnFilter({ + placeholder: "搜索权限、权限码", + value: query, + onChange: onQueryChange + }) + }, { key: "code", label: "编码", width: "minmax(180px, 1.2fr)", render: (permission) => {permission.code} }, { key: "kind", label: "类型", width: "110px", render: (permission) => kindLabel(permission.kind) }, { diff --git a/src/features/menus/hooks/useMenusPage.js b/src/features/menus/hooks/useMenusPage.js index 13f718d..628d1a6 100644 --- a/src/features/menus/hooks/useMenusPage.js +++ b/src/features/menus/hooks/useMenusPage.js @@ -4,273 +4,293 @@ import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js"; import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; import { - createMenu, - createPermission, - deleteMenu, - deletePermission, - listPermissions, - listSystemMenus, - sortMenus, - syncPermissions, - updateMenu, - updateMenuVisible, - updatePermission + createMenu, + createPermission, + deleteMenu, + deletePermission, + listPermissions, + listSystemMenus, + sortMenus, + syncPermissions, + updateMenu, + updateMenuVisible, + updatePermission, } from "@/features/menus/api"; import { useMenuAbilities } from "@/features/menus/permissions.js"; import { menuFormSchema, permissionFormSchema } from "@/features/menus/schema"; const emptyMenu = { - code: "", - icon: "menu", - label: "", - parentId: "", - path: "", - permissionCode: "", - sort: 0, - visible: true + code: "", + icon: "menu", + label: "", + parentId: "", + path: "", + permissionCode: "", + sort: 0, + visible: true, }; const emptyPermission = { code: "", description: "", kind: "button", name: "" }; const emptyData = { menus: [], permissions: [] }; export function useMenusPage() { - const [query, setQuery] = useState(""); - const [menuDrawerOpen, setMenuDrawerOpen] = useState(false); - const [permissionDrawerOpen, setPermissionDrawerOpen] = useState(false); - const [editingMenu, setEditingMenu] = useState(null); - const [editingPermission, setEditingPermission] = useState(null); - const [menuForm, setMenuForm] = useState(emptyMenu); - const [permissionForm, setPermissionForm] = useState(emptyPermission); - const abilities = useMenuAbilities(); - const confirm = useConfirm(); - const { showToast } = useToast(); + const [query, setQuery] = useState(""); + const [menuDrawerOpen, setMenuDrawerOpen] = useState(false); + const [permissionDrawerOpen, setPermissionDrawerOpen] = useState(false); + const [editingMenu, setEditingMenu] = useState(null); + const [editingPermission, setEditingPermission] = useState(null); + const [menuForm, setMenuForm] = useState(emptyMenu); + const [permissionForm, setPermissionForm] = useState(emptyPermission); + const abilities = useMenuAbilities(); + const confirm = useConfirm(); + const { showToast } = useToast(); - const queryFn = useCallback(async () => { - const menuData = await listSystemMenus(); - const permissionData = abilities.canLoadPermissions ? await listPermissions() : []; - return { menus: menuData || [], permissions: permissionData || [] }; - }, [abilities.canLoadPermissions]); + const queryFn = useCallback(async () => { + const menuData = await listSystemMenus(); + const permissionData = abilities.canLoadPermissions ? await listPermissions() : []; + return { menus: menuData || [], permissions: permissionData || [] }; + }, [abilities.canLoadPermissions]); - const { data, error, loading, reload } = useAdminQuery(queryFn, { - errorMessage: "加载菜单权限失败", - initialData: emptyData, - keepPreviousData: true, - queryKey: ["menus", abilities.canLoadPermissions] - }); - - const flatMenus = useMemo(() => flattenMenus(data.menus || []), [data.menus]); - const filteredFlatMenus = useMemo(() => { - const keyword = query.trim().toLowerCase(); - if (!keyword) { - return flatMenus; - } - return flatMenus.filter((menu) => { - return [menu.label, menu.code, menu.path, menu.permissionCode].some((value) => String(value || "").toLowerCase().includes(keyword)); + const { data, error, loading, reload } = useAdminQuery(queryFn, { + errorMessage: "加载菜单权限失败", + initialData: emptyData, + keepPreviousData: true, + queryKey: ["menus", abilities.canLoadPermissions], }); - }, [flatMenus, query]); - const filteredPermissions = useMemo(() => { - const keyword = query.trim().toLowerCase(); - const items = data.permissions || []; - if (!keyword) { - return items; - } - return items.filter((permission) => { - return [permission.name, permission.code, permission.kind, permission.description].some((value) => String(value || "").toLowerCase().includes(keyword)); - }); - }, [data.permissions, query]); - const changeQuery = (value) => { - setQuery(value); - }; + const flatMenus = useMemo(() => flattenMenus(data.menus || []), [data.menus]); + const filteredFlatMenus = useMemo(() => { + const keyword = query.trim().toLowerCase(); + if (!keyword) { + return flatMenus; + } + return flatMenus.filter((menu) => { + return [menu.label, menu.code, menu.path, menu.permissionCode].some((value) => + String(value || "") + .toLowerCase() + .includes(keyword), + ); + }); + }, [flatMenus, query]); + const filteredPermissions = useMemo(() => { + const keyword = query.trim().toLowerCase(); + const items = data.permissions || []; + if (!keyword) { + return items; + } + return items.filter((permission) => { + return [permission.name, permission.code, permission.kind, permission.description].some((value) => + String(value || "") + .toLowerCase() + .includes(keyword), + ); + }); + }, [data.permissions, query]); - const openCreateMenu = () => { - setEditingMenu(null); - setMenuForm(emptyMenu); - setMenuDrawerOpen(true); - }; + const changeQuery = (value) => { + setQuery(value); + }; - const openEditMenu = (menu) => { - setEditingMenu(menu); - setMenuForm({ - code: menu.code, - icon: menu.icon || "menu", - label: menu.label, - parentId: menu.parentId ? String(menu.parentId) : "", - path: menu.path || "", - permissionCode: menu.permissionCode || "", - sort: menu.sort || 0, - visible: Boolean(menu.visible) - }); - setMenuDrawerOpen(true); - }; + const resetFilters = () => { + setQuery(""); + }; - const openCreatePermission = () => { - setEditingPermission(null); - setPermissionForm(emptyPermission); - setPermissionDrawerOpen(true); - }; + const openCreateMenu = () => { + setEditingMenu(null); + setMenuForm(emptyMenu); + setMenuDrawerOpen(true); + }; - const openEditPermission = (permission) => { - setEditingPermission(permission); - setPermissionForm({ - code: permission.code, - description: permission.description || "", - kind: permission.kind, - name: permission.name - }); - setPermissionDrawerOpen(true); - }; + const openEditMenu = (menu) => { + setEditingMenu(menu); + setMenuForm({ + code: menu.code, + icon: menu.icon || "menu", + label: menu.label, + parentId: menu.parentId ? String(menu.parentId) : "", + path: menu.path || "", + permissionCode: menu.permissionCode || "", + sort: menu.sort || 0, + visible: Boolean(menu.visible), + }); + setMenuDrawerOpen(true); + }; - const submitMenu = async (event) => { - event.preventDefault(); - try { - const payload = parseForm(menuFormSchema, menuForm); - if (editingMenu) { - await updateMenu(editingMenu.id, payload); - showToast("菜单已更新", "success"); - } else { - await createMenu(payload); - showToast("菜单已创建", "success"); - } - setMenuDrawerOpen(false); - reload(); - } catch (err) { - showToast(err.message || "保存菜单失败", "error"); - } - }; + const openCreatePermission = () => { + setEditingPermission(null); + setPermissionForm(emptyPermission); + setPermissionDrawerOpen(true); + }; - const submitPermission = async (event) => { - event.preventDefault(); - try { - const payload = parseForm(permissionFormSchema, permissionForm); - if (editingPermission) { - await updatePermission(editingPermission.id, payload); - showToast("权限已更新", "success"); - } else { - await createPermission(payload); - showToast("权限已创建", "success"); - } - setPermissionDrawerOpen(false); - reload(); - } catch (err) { - showToast(err.message || "保存权限失败", "error"); - } - }; + const openEditPermission = (permission) => { + setEditingPermission(permission); + setPermissionForm({ + code: permission.code, + description: permission.description || "", + kind: permission.kind, + name: permission.name, + }); + setPermissionDrawerOpen(true); + }; - const removeMenu = async (menu) => { - const ok = await confirm({ confirmText: "删除", message: `${menu.label} 删除前需要先删除子菜单。`, title: "删除菜单", tone: "danger" }); - if (!ok) { - return; - } - try { - await deleteMenu(menu.id); - showToast("菜单已删除", "success"); - reload(); - } catch (err) { - showToast(err.message || "删除菜单失败", "error"); - } - }; + const submitMenu = async (event) => { + event.preventDefault(); + try { + const payload = parseForm(menuFormSchema, menuForm); + if (editingMenu) { + await updateMenu(editingMenu.id, payload); + showToast("菜单已更新", "success"); + } else { + await createMenu(payload); + showToast("菜单已创建", "success"); + } + setMenuDrawerOpen(false); + reload(); + } catch (err) { + showToast(err.message || "保存菜单失败", "error"); + } + }; - const removePermission = async (permission) => { - const ok = await confirm({ confirmText: "删除", message: `${permission.name} 删除前不能被角色或菜单引用。`, title: "删除权限", tone: "danger" }); - if (!ok) { - return; - } - try { - await deletePermission(permission.id); - showToast("权限已删除", "success"); - reload(); - } catch (err) { - showToast(err.message || "删除权限失败", "error"); - } - }; + const submitPermission = async (event) => { + event.preventDefault(); + try { + const payload = parseForm(permissionFormSchema, permissionForm); + if (editingPermission) { + await updatePermission(editingPermission.id, payload); + showToast("权限已更新", "success"); + } else { + await createPermission(payload); + showToast("权限已创建", "success"); + } + setPermissionDrawerOpen(false); + reload(); + } catch (err) { + showToast(err.message || "保存权限失败", "error"); + } + }; - const toggleVisible = async (menu) => { - try { - await updateMenuVisible(menu.id, !menu.visible); - showToast("菜单状态已更新", "success"); - reload(); - } catch (err) { - showToast(err.message || "更新菜单失败", "error"); - } - }; + const removeMenu = async (menu) => { + const ok = await confirm({ + confirmText: "删除", + message: `${menu.label} 删除前需要先删除子菜单。`, + title: "删除菜单", + tone: "danger", + }); + if (!ok) { + return; + } + try { + await deleteMenu(menu.id); + showToast("菜单已删除", "success"); + reload(); + } catch (err) { + showToast(err.message || "删除菜单失败", "error"); + } + }; - const reorderMenu = async (source, target) => { - if (!source || !target || source.id === target.id || (source.parentId || null) !== (target.parentId || null)) { - return; - } - const siblings = flatMenus - .filter((item) => (item.parentId || null) === (source.parentId || null)) - .sort((a, b) => (a.sort || 0) - (b.sort || 0) || a.id - b.id); - const fromIndex = siblings.findIndex((item) => item.id === source.id); - const toIndex = siblings.findIndex((item) => item.id === target.id); - if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) { - return; - } - const reordered = [...siblings]; - const [moved] = reordered.splice(fromIndex, 1); - reordered.splice(toIndex, 0, moved); + const removePermission = async (permission) => { + const ok = await confirm({ + confirmText: "删除", + message: `${permission.name} 删除前不能被角色或菜单引用。`, + title: "删除权限", + tone: "danger", + }); + if (!ok) { + return; + } + try { + await deletePermission(permission.id); + showToast("权限已删除", "success"); + reload(); + } catch (err) { + showToast(err.message || "删除权限失败", "error"); + } + }; - try { - await sortMenus(reordered.map((item, index) => ({ id: item.id, sort: (index + 1) * 10 }))); - showToast("菜单排序已更新", "success"); - reload(); - } catch (err) { - showToast(err.message || "排序失败", "error"); - } - }; + const toggleVisible = async (menu) => { + try { + await updateMenuVisible(menu.id, !menu.visible); + showToast("菜单状态已更新", "success"); + reload(); + } catch (err) { + showToast(err.message || "更新菜单失败", "error"); + } + }; - const syncDefaultPermissions = async () => { - const ok = await confirm({ confirmText: "同步", message: "将同步系统默认权限定义。", title: "同步权限" }); - if (!ok) { - return; - } - try { - await syncPermissions(); - showToast("权限已同步", "success"); - reload(); - } catch (err) { - showToast(err.message || "同步权限失败", "error"); - } - }; + const reorderMenu = async (source, target) => { + if (!source || !target || source.id === target.id || (source.parentId || null) !== (target.parentId || null)) { + return; + } + const siblings = flatMenus + .filter((item) => (item.parentId || null) === (source.parentId || null)) + .sort((a, b) => (a.sort || 0) - (b.sort || 0) || a.id - b.id); + const fromIndex = siblings.findIndex((item) => item.id === source.id); + const toIndex = siblings.findIndex((item) => item.id === target.id); + if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) { + return; + } + const reordered = [...siblings]; + const [moved] = reordered.splice(fromIndex, 1); + reordered.splice(toIndex, 0, moved); - return { - abilities, - changeQuery, - closeMenuDrawer: () => setMenuDrawerOpen(false), - closePermissionDrawer: () => setPermissionDrawerOpen(false), - editingMenu, - editingPermission, - error, - flatMenus: filteredFlatMenus, - loading, - menuDrawerOpen, - menuForm, - menus: data.menus || [], - openCreateMenu, - openCreatePermission, - openEditMenu, - openEditPermission, - permissionDrawerOpen, - permissionForm, - permissions: filteredPermissions, - query, - reload, - removeMenu, - removePermission, - reorderMenu, - setMenuForm, - setPermissionForm, - submitMenu, - submitPermission, - syncDefaultPermissions, - toggleVisible - }; + try { + await sortMenus(reordered.map((item, index) => ({ id: item.id, sort: (index + 1) * 10 }))); + showToast("菜单排序已更新", "success"); + reload(); + } catch (err) { + showToast(err.message || "排序失败", "error"); + } + }; + + const syncDefaultPermissions = async () => { + const ok = await confirm({ confirmText: "同步", message: "将同步系统默认权限定义。", title: "同步权限" }); + if (!ok) { + return; + } + try { + await syncPermissions(); + showToast("权限已同步", "success"); + reload(); + } catch (err) { + showToast(err.message || "同步权限失败", "error"); + } + }; + + return { + abilities, + changeQuery, + closeMenuDrawer: () => setMenuDrawerOpen(false), + closePermissionDrawer: () => setPermissionDrawerOpen(false), + editingMenu, + editingPermission, + error, + flatMenus: filteredFlatMenus, + loading, + menuDrawerOpen, + menuForm, + menus: data.menus || [], + openCreateMenu, + openCreatePermission, + openEditMenu, + openEditPermission, + permissionDrawerOpen, + permissionForm, + permissions: filteredPermissions, + query, + reload, + resetFilters, + removeMenu, + removePermission, + reorderMenu, + setMenuForm, + setPermissionForm, + submitMenu, + submitPermission, + syncDefaultPermissions, + toggleVisible, + }; } function flattenMenus(menus, depth = 0) { - return menus.flatMap((menu) => [ - { ...menu, depth }, - ...flattenMenus(menu.children || [], depth + 1) - ]); + return menus.flatMap((menu) => [{ ...menu, depth }, ...flattenMenus(menu.children || [], depth + 1)]); } diff --git a/src/features/menus/pages/MenuManagementPage.jsx b/src/features/menus/pages/MenuManagementPage.jsx index 29c3e7f..8b5898e 100644 --- a/src/features/menus/pages/MenuManagementPage.jsx +++ b/src/features/menus/pages/MenuManagementPage.jsx @@ -2,11 +2,11 @@ import Add from "@mui/icons-material/Add"; import SyncOutlined from "@mui/icons-material/SyncOutlined"; import { DataState } from "@/shared/ui/DataState.jsx"; import { - AdminActionIconButton, - AdminListBody, - AdminListPage, - AdminListToolbar, - AdminSearchBox + AdminActionIconButton, + AdminFilterResetButton, + AdminListBody, + AdminListPage, + AdminListToolbar, } from "@/shared/ui/AdminListLayout.jsx"; import { MenuFormDrawer } from "@/features/menus/components/MenuFormDrawer.jsx"; import { MenuTable } from "@/features/menus/components/MenuTable.jsx"; @@ -15,72 +15,78 @@ import { PermissionTable } from "@/features/menus/components/PermissionTable.jsx import { useMenusPage } from "@/features/menus/hooks/useMenusPage.js"; export function MenuManagementPage() { - const page = useMenusPage(); + const page = useMenusPage(); - return ( - <> - - } - actions={( - <> - {page.abilities.canSyncPermission ? ( - - - - ) : null} - {page.abilities.canCreatePermission ? ( - - - - ) : null} - {page.abilities.canCreateMenu ? ( - - - - ) : null} - - )} - /> + return ( + <> + + + } + actions={ + <> + {page.abilities.canSyncPermission ? ( + + + + ) : null} + {page.abilities.canCreatePermission ? ( + + + + ) : null} + {page.abilities.canCreateMenu ? ( + + + + ) : null} + + } + /> - - -
- - -
-
-
-
+ + +
+ + +
+
+
+
- - - - ); + + + + ); } diff --git a/src/features/payment/pages/PaymentBillListPage.jsx b/src/features/payment/pages/PaymentBillListPage.jsx index a6daab9..ccca972 100644 --- a/src/features/payment/pages/PaymentBillListPage.jsx +++ b/src/features/payment/pages/PaymentBillListPage.jsx @@ -1,208 +1,281 @@ -import TextField from "@mui/material/TextField"; import { useMemo, useState } from "react"; import { listRechargeBills } from "@/features/payment/api"; import { - AdminFilterSelect, - AdminListBody, - AdminListPage, - AdminListToolbar, - AdminSearchBox, - adminListClasses + AdminFilterResetButton, + AdminListBody, + AdminListPage, + AdminListToolbar, } from "@/shared/ui/AdminListLayout.jsx"; import { DataState } from "@/shared/ui/DataState.jsx"; import { DataTable } from "@/shared/ui/DataTable.jsx"; import { PaginationBar } from "@/shared/ui/PaginationBar.jsx"; import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js"; +import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; import { formatMillis } from "@/shared/utils/time.js"; const pageSize = 20; const statusOptions = [ - ["", "全部状态"], - ["succeeded", "成功"] + ["", "全部状态"], + ["succeeded", "成功"], ]; const rechargeTypeOptions = [ - ["", "全部途径"], - ["coin_seller_transfer", "币商充值"] + ["", "全部途径"], + ["coin_seller_transfer", "币商充值"], ]; const columns = [ - { - key: "bill", - label: "账单", - width: "minmax(240px, 1.4fr)", - render: (bill) => ( - - ) - }, - { - key: "users", - label: "用户 / 来源", - width: "minmax(180px, 0.9fr)", - render: (bill) => ( - - ) - }, - { - key: "amount", - label: "金币 / 金额", - width: "minmax(150px, 0.75fr)", - render: (bill) => ( - - ) - }, - { - key: "policy", - label: "兑换口径", - width: "minmax(170px, 0.9fr)", - render: (bill) => ( - - ) - }, - { - key: "region", - label: "区域", - width: "minmax(110px, 0.55fr)", - render: (bill) => - }, - { - key: "state", - label: "途径 / 状态", - width: "minmax(150px, 0.75fr)", - render: (bill) => ( - } /> - ) - }, - { - key: "time", - label: "创建时间", - width: "minmax(160px, 0.8fr)", - render: (bill) => formatMillis(bill.createdAtMs) - } + { + key: "bill", + label: "账单", + width: "minmax(240px, 1.4fr)", + render: (bill) => , + }, + { + key: "userId", + label: "用户 ID", + width: "minmax(120px, 0.65fr)", + render: (bill) => bill.userId || "-", + }, + { + key: "sellerUserId", + label: "币商 ID", + width: "minmax(120px, 0.65fr)", + render: (bill) => bill.sellerUserId || "-", + }, + { + key: "amount", + label: "金币 / 金额", + width: "minmax(150px, 0.75fr)", + render: (bill) => ( + + ), + }, + { + key: "policy", + label: "兑换口径", + width: "minmax(170px, 0.9fr)", + render: (bill) => ( + + ), + }, + { + key: "region", + label: "区域", + width: "minmax(110px, 0.55fr)", + render: (bill) => ( + + ), + }, + { + key: "rechargeType", + label: "充值途径", + width: "minmax(130px, 0.7fr)", + render: (bill) => rechargeTypeLabel(bill.rechargeType), + }, + { + key: "status", + label: "状态", + width: "minmax(100px, 0.55fr)", + render: (bill) => , + }, + { + key: "time", + label: "创建时间", + width: "minmax(160px, 0.8fr)", + render: (bill) => formatMillis(bill.createdAtMs), + }, ]; export function PaymentBillListPage() { - const [page, setPage] = useState(1); - const [keyword, setKeyword] = useState(""); - const [status, setStatus] = useState(""); - const [rechargeType, setRechargeType] = useState(""); - const [userId, setUserId] = useState(""); - const [sellerUserId, setSellerUserId] = useState(""); + const [page, setPage] = useState(1); + const [keyword, setKeyword] = useState(""); + const [status, setStatus] = useState(""); + const [rechargeType, setRechargeType] = useState(""); + const [userId, setUserId] = useState(""); + const [sellerUserId, setSellerUserId] = useState(""); - const filters = useMemo( - () => ({ - keyword, - recharge_type: rechargeType, - seller_user_id: sellerUserId, - status, - user_id: userId - }), - [keyword, rechargeType, sellerUserId, status, userId] - ); - const query = usePaginatedQuery({ - errorMessage: "获取账单列表失败", - fetcher: listRechargeBills, - filters, - page, - pageSize, - queryKey: ["payment-bills", filters, page] - }); - const data = query.data || { items: [], total: 0, page, pageSize }; - const items = data.items || []; - const total = data.total || 0; + const filters = useMemo( + () => ({ + keyword, + recharge_type: rechargeType, + seller_user_id: sellerUserId, + status, + user_id: userId, + }), + [keyword, rechargeType, sellerUserId, status, userId], + ); + const query = usePaginatedQuery({ + errorMessage: "获取账单列表失败", + fetcher: listRechargeBills, + filters, + page, + pageSize, + queryKey: ["payment-bills", filters, page], + }); + const data = query.data || { items: [], total: 0, page, pageSize }; + const items = data.items || []; + const total = data.total || 0; - const resetPage = (setter) => (value) => { - setter(value); - setPage(1); - }; + const resetPage = (setter) => (value) => { + setter(value); + setPage(1); + }; - return ( - - - - resetPage(setUserId)(digitsOnly(event.target.value))} - /> - resetPage(setSellerUserId)(digitsOnly(event.target.value))} - /> - - - + const resetFilters = () => { + setKeyword(""); + setStatus(""); + setRechargeType(""); + setUserId(""); + setSellerUserId(""); + setPage(1); + }; + const tableColumns = columns.map((column) => { + if (column.key === "bill") { + return { + ...column, + filter: createTextColumnFilter({ + placeholder: "搜索账单号、命令号、外部单号", + value: keyword, + onChange: resetPage(setKeyword), + }), + }; } - /> - - - bill.transactionId} /> - {total > 0 ? : null} - - - - ); + if (column.key === "userId") { + return { + ...column, + filter: createTextColumnFilter({ + placeholder: "搜索用户 ID", + value: userId, + onChange: (value) => resetPage(setUserId)(digitsOnly(value)), + }), + }; + } + if (column.key === "sellerUserId") { + return { + ...column, + filter: createTextColumnFilter({ + placeholder: "搜索币商 ID", + value: sellerUserId, + onChange: (value) => resetPage(setSellerUserId)(digitsOnly(value)), + }), + }; + } + if (column.key === "rechargeType") { + return { + ...column, + filter: createOptionsColumnFilter({ + options: rechargeTypeOptions, + placeholder: "搜索充值途径", + value: rechargeType, + onChange: resetPage(setRechargeType), + }), + }; + } + if (column.key === "status") { + return { + ...column, + filter: createOptionsColumnFilter({ + options: statusOptions, + placeholder: "搜索状态", + value: status, + onChange: resetPage(setStatus), + }), + }; + } + return column; + }); + + return ( + + + } + /> + + + bill.transactionId} + /> + {total > 0 ? ( + + ) : null} + + + + ); } function Stack({ primary, secondary }) { - return ( -
- {primary || "-"} - {secondary || "-"} -
- ); + return ( +
+ {primary || "-"} + {secondary || "-"} +
+ ); } function StatusBadge({ status }) { - const tone = status === "succeeded" ? "succeeded" : status === "failed" ? "danger" : "stopped"; - return ( - - - {statusLabel(status)} - - ); + const tone = status === "succeeded" ? "succeeded" : status === "failed" ? "danger" : "stopped"; + return ( + + + {statusLabel(status)} + + ); } function rechargeTypeLabel(value) { - return rechargeTypeOptions.find(([optionValue]) => optionValue === value)?.[1] || value || "-"; + return rechargeTypeOptions.find(([optionValue]) => optionValue === value)?.[1] || value || "-"; } function statusLabel(value) { - if (value === "succeeded") { - return "成功"; - } - if (value === "failed") { - return "失败"; - } - return value || "-"; + if (value === "succeeded") { + return "成功"; + } + if (value === "failed") { + return "失败"; + } + return value || "-"; } function formatNumber(value) { - if (value === 0 || value) { - return Number(value).toLocaleString("zh-CN"); - } - return "-"; + if (value === 0 || value) { + return Number(value).toLocaleString("zh-CN"); + } + return "-"; } function formatMoney(value, currency = "USD") { - if (!(value === 0 || value)) { - return "-"; - } - return `${currency || "USD"} ${(Number(value) / 100).toLocaleString("zh-CN", { - maximumFractionDigits: 2, - minimumFractionDigits: 2 - })}`; + if (!(value === 0 || value)) { + return "-"; + } + return `${currency || "USD"} ${(Number(value) / 100).toLocaleString("zh-CN", { + maximumFractionDigits: 2, + minimumFractionDigits: 2, + })}`; } function digitsOnly(value) { - return value.replace(/\D/g, ""); + return value.replace(/\D/g, ""); } diff --git a/src/features/resources/hooks/useResourcePages.js b/src/features/resources/hooks/useResourcePages.js index aefce33..097c14f 100644 --- a/src/features/resources/hooks/useResourcePages.js +++ b/src/features/resources/hooks/useResourcePages.js @@ -221,6 +221,13 @@ export function useResourceListPage() { } }; + const resetFilters = () => { + setQuery(""); + setStatus(""); + setResourceType(""); + setPage(1); + }; + return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, @@ -234,6 +241,7 @@ export function useResourceListPage() { openEditResource, resourceType, selectedResource, + resetFilters, setForm, status, submitResource, @@ -385,6 +393,12 @@ export function useResourceGroupListPage() { } }; + const resetFilters = () => { + setQuery(""); + setStatus(""); + setPage(1); + }; + return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, @@ -401,6 +415,7 @@ export function useResourceGroupListPage() { resourceOptions, resourceOptionsLoading, selectedGroup, + resetFilters, setForm, status, submitGroup, @@ -534,6 +549,13 @@ export function useGiftListPage() { } }; + const resetFilters = () => { + setQuery(""); + setStatus(""); + setRegionId(""); + setPage(1); + }; + return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, @@ -551,6 +573,7 @@ export function useGiftListPage() { resourceOptions, resourceOptionsLoading, selectedGift, + resetFilters, setForm, status, submitGift, @@ -650,6 +673,12 @@ export function useResourceGrantListPage() { } }; + const resetFilters = () => { + setQuery(""); + setStatus(""); + setPage(1); + }; + return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, @@ -662,6 +691,7 @@ export function useResourceGrantListPage() { openCreateGrant, optionsLoading, resourceOptions, + resetFilters, setForm, status, submitGrant, diff --git a/src/features/resources/pages/GiftListPage.jsx b/src/features/resources/pages/GiftListPage.jsx index 56e2670..97a58f5 100644 --- a/src/features/resources/pages/GiftListPage.jsx +++ b/src/features/resources/pages/GiftListPage.jsx @@ -17,691 +17,716 @@ import { Button } from "@/shared/ui/Button.jsx"; import { DataState } from "@/shared/ui/DataState.jsx"; import { DataTable } from "@/shared/ui/DataTable.jsx"; import { MultiValueAutocomplete } from "@/shared/ui/MultiValueAutocomplete.jsx"; -import { createRegionColumnFilter, RegionFilterControl } from "@/shared/ui/RegionFilterControl.jsx"; import { - AdminActionIconButton, - AdminFilterSelect, - AdminListBody, - AdminListPage, - AdminRowActions, - AdminListToolbar, - AdminSearchBox + AdminActionIconButton, + AdminFilterResetButton, + AdminListBody, + AdminListPage, + AdminRowActions, + AdminListToolbar, } from "@/shared/ui/AdminListLayout.jsx"; import { PaginationBar } from "@/shared/ui/PaginationBar.jsx"; +import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx"; +import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; import { formatMillis } from "@/shared/utils/time.js"; import { resourceStatusFilters } from "@/features/resources/constants.js"; import { useGiftListPage } from "@/features/resources/hooks/useResourcePages.js"; import styles from "@/features/resources/resources.module.css"; const giftTypeOptions = [ - ["normal", "普通礼物"], - ["cp", "CP礼物"], - ["lucky", "幸运礼物"], - ["super_lucky", "超级幸运礼物"], - ["exclusive", "专属礼物"], - ["noble", "贵族礼物"], - ["flag", "国旗礼物"], - ["activity", "活动礼物"], - ["magic", "魔法礼物"], - ["custom", "定制礼物"] + ["normal", "普通礼物"], + ["cp", "CP礼物"], + ["lucky", "幸运礼物"], + ["super_lucky", "超级幸运礼物"], + ["exclusive", "专属礼物"], + ["noble", "贵族礼物"], + ["flag", "国旗礼物"], + ["activity", "活动礼物"], + ["magic", "魔法礼物"], + ["custom", "定制礼物"], ]; const chargeAssetOptions = [ - ["COIN", "金币"], - ["DIAMOND", "钻石"] + ["COIN", "金币"], + ["DIAMOND", "钻石"], ]; const effectOptions = [ - ["animation", "动画"], - ["music", "音乐"], - ["global_broadcast", "全局广播"] + ["animation", "动画"], + ["music", "音乐"], + ["global_broadcast", "全局广播"], ]; const effectLabels = Object.fromEntries(effectOptions); const weekLabels = ["一", "二", "三", "四", "五", "六", "日"]; const baseColumns = [ - { - key: "gift", - label: "礼物", - width: "minmax(260px, 1.4fr)", - render: (gift) => - }, - { - key: "resource", - label: "资源", - width: "minmax(220px, 1.1fr)", - render: (gift) => - }, - { - key: "price", - label: "类型 / 价格", - width: "minmax(150px, 0.75fr)", - render: (gift) => ( -
- {giftTypeLabel(gift.giftTypeCode)} - {chargeAssetLabel(gift.chargeAssetType)} · {formatNumber(gift.coinPrice)} -
- ) - }, - { - key: "income", - label: "积分 / 热度", - width: "minmax(140px, 0.8fr)", - render: (gift) => ( -
- {formatNumber(gift.giftPointAmount)} - {formatNumber(gift.heatValue)} -
- ) - }, - { - key: "regions", - label: "区域", - width: "minmax(160px, 0.8fr)" - }, - { - key: "effects", - label: "有效期 / 特效", - width: "minmax(210px, 1.1fr)", - render: (gift) => ( -
- {effectiveRangeLabel(gift)} - {effectTypesLabel(gift.effectTypes)} -
- ) - }, - { - key: "status", - label: "状态", - width: "minmax(92px, 0.55fr)" - }, - { - key: "time", - label: "更新时间", - width: "minmax(170px, 0.9fr)", - render: (gift) => formatMillis(gift.updatedAtMs || gift.createdAtMs) - }, - { - key: "actions", - label: "操作", - width: "minmax(76px, 0.4fr)" - } + { + key: "gift", + label: "礼物", + width: "minmax(260px, 1.4fr)", + render: (gift) => , + }, + { + key: "resource", + label: "资源", + width: "minmax(220px, 1.1fr)", + render: (gift) => , + }, + { + key: "price", + label: "类型 / 价格", + width: "minmax(150px, 0.75fr)", + render: (gift) => ( +
+ {giftTypeLabel(gift.giftTypeCode)} + + {chargeAssetLabel(gift.chargeAssetType)} · {formatNumber(gift.coinPrice)} + +
+ ), + }, + { + key: "income", + label: "积分 / 热度", + width: "minmax(140px, 0.8fr)", + render: (gift) => ( +
+ {formatNumber(gift.giftPointAmount)} + {formatNumber(gift.heatValue)} +
+ ), + }, + { + key: "regions", + label: "区域", + width: "minmax(160px, 0.8fr)", + }, + { + key: "effects", + label: "有效期 / 特效", + width: "minmax(210px, 1.1fr)", + render: (gift) => ( +
+ {effectiveRangeLabel(gift)} + {effectTypesLabel(gift.effectTypes)} +
+ ), + }, + { + key: "status", + label: "状态", + width: "minmax(92px, 0.55fr)", + }, + { + key: "time", + label: "更新时间", + width: "minmax(170px, 0.9fr)", + render: (gift) => formatMillis(gift.updatedAtMs || gift.createdAtMs), + }, + { + key: "actions", + label: "操作", + width: "minmax(76px, 0.4fr)", + }, ]; export function GiftListPage() { - const page = useGiftListPage(); - const items = page.data.items || []; - const total = page.data.total || 0; - const isEditing = page.activeAction === "edit"; - const giftRegionOptions = [{ label: "全局 · 0", value: "0" }, ...page.regionOptions]; - const regionLabelById = buildRegionLabelMap(giftRegionOptions); - const tableColumns = baseColumns.map((column) => { - if (column.key === "regions") { - return { - ...column, - filter: createRegionColumnFilter({ - loading: page.loadingRegions, - options: giftRegionOptions, - value: page.regionId, - onChange: page.changeRegionId - }), - render: (gift) => - }; - } - if (column.key === "status") { - return { - ...column, - render: (gift) => - }; - } - if (column.key === "actions") { - return { - ...column, - render: (gift) => - }; - } - return column; - }); + const page = useGiftListPage(); + const items = page.data.items || []; + const total = page.data.total || 0; + const isEditing = page.activeAction === "edit"; + const giftRegionOptions = [{ label: "全局 · 0", value: "0" }, ...page.regionOptions]; + const regionLabelById = buildRegionLabelMap(giftRegionOptions); + const tableColumns = baseColumns.map((column) => { + if (column.key === "regions") { + return { + ...column, + filter: createRegionColumnFilter({ + loading: page.loadingRegions, + options: giftRegionOptions, + value: page.regionId, + onChange: page.changeRegionId, + }), + render: (gift) => , + }; + } + if (column.key === "status") { + return { + ...column, + filter: createOptionsColumnFilter({ + options: resourceStatusFilters, + placeholder: "搜索状态", + value: page.status, + onChange: page.changeStatus, + }), + render: (gift) => , + }; + } + if (column.key === "gift") { + return { + ...column, + filter: createTextColumnFilter({ + placeholder: "搜索礼物名称、礼物 ID", + value: page.query, + onChange: page.changeQuery, + }), + }; + } + if (column.key === "actions") { + return { + ...column, + render: (gift) => , + }; + } + return column; + }); - return ( - - - - - + + } + actions={ + page.abilities.canCreateGift ? ( + + + + ) : null + } /> - - } - actions={ - page.abilities.canCreateGift ? ( - - - - ) : null - } - /> - - - gift.giftId} /> - {total > 0 ? ( - + + gift.giftId} /> + {total > 0 ? ( + + ) : null} + + + - ) : null} - - - - - ); + + ); } function GiftRowActions({ gift, page }) { - return ( - - page.openEditGift(gift)} - > - - - - ); + return ( + + page.openEditGift(gift)} + > + + + + ); } function GiftStatusSwitch({ gift, page }) { - const checked = gift.status === "active"; - return ( - page.toggleGift(gift, event.target.checked)} - /> - ); + const checked = gift.status === "active"; + return ( + page.toggleGift(gift, event.target.checked)} + /> + ); } function GiftFormDialog({ disabled, form, loading, mode, onClose, onSubmit, open, page, regionOptions }) { - const submitDisabled = disabled || loading || page.resourceOptionsLoading || page.loadingRegions; - return ( - - - page.setForm({ ...form, name: event.target.value })} - /> - page.setForm({ ...form, giftId: event.target.value })} - /> - page.setForm({ ...form, giftTypeCode: event.target.value })} + const submitDisabled = disabled || loading || page.resourceOptionsLoading || page.loadingRegions; + return ( + - {giftTypeOptions.map(([value, label]) => ( - {label} - ))} - - page.setForm({ ...form, resourceId: event.target.value })} - > - {page.resourceOptions.map((resource) => ( - - {resource.name || resource.resourceCode || resource.resourceId} - - ))} - - page.setForm({ ...form, chargeAssetType: event.target.value })} - > - {chargeAssetOptions.map(([value, label]) => ( - {label} - ))} - - page.setForm({ ...form, coinPrice: event.target.value })} - /> - page.setForm({ ...form, giftPointAmount: event.target.value })} - /> - page.setForm({ ...form, heatValue: event.target.value })} - /> - page.setForm({ ...form, sortOrder: event.target.value })} - /> - page.setForm({ ...form, regionIds })} - /> - page.setForm({ ...form, effectiveFrom })} - /> - page.setForm({ ...form, effectiveTo })} - /> - page.setForm({ ...form, effectTypes })} - /> - page.setForm({ ...form, enabled: event.target.checked })} - /> - } - label={form.enabled ? "启用" : "禁用"} - /> - - - ); + + page.setForm({ ...form, name: event.target.value })} + /> + page.setForm({ ...form, giftId: event.target.value })} + /> + page.setForm({ ...form, giftTypeCode: event.target.value })} + > + {giftTypeOptions.map(([value, label]) => ( + + {label} + + ))} + + page.setForm({ ...form, resourceId: event.target.value })} + > + {page.resourceOptions.map((resource) => ( + + {resource.name || resource.resourceCode || resource.resourceId} + + ))} + + page.setForm({ ...form, chargeAssetType: event.target.value })} + > + {chargeAssetOptions.map(([value, label]) => ( + + {label} + + ))} + + page.setForm({ ...form, coinPrice: event.target.value })} + /> + page.setForm({ ...form, giftPointAmount: event.target.value })} + /> + page.setForm({ ...form, heatValue: event.target.value })} + /> + page.setForm({ ...form, sortOrder: event.target.value })} + /> + page.setForm({ ...form, regionIds })} + /> + page.setForm({ ...form, effectiveFrom })} + /> + page.setForm({ ...form, effectiveTo })} + /> + page.setForm({ ...form, effectTypes })} + /> + page.setForm({ ...form, enabled: event.target.checked })} + /> + } + label={form.enabled ? "启用" : "禁用"} + /> + +
+ ); } function DateTimeField({ disabled, label, onChange, value }) { - const parsedValue = parseLocalDateTime(value); - const [anchorEl, setAnchorEl] = useState(null); - const [draftDate, setDraftDate] = useState(parsedValue || new Date()); - const [viewDate, setViewDate] = useState(parsedValue || new Date()); - const days = useMemo(() => calendarDays(viewDate), [viewDate]); - const open = Boolean(anchorEl); + const parsedValue = parseLocalDateTime(value); + const [anchorEl, setAnchorEl] = useState(null); + const [draftDate, setDraftDate] = useState(parsedValue || new Date()); + const [viewDate, setViewDate] = useState(parsedValue || new Date()); + const days = useMemo(() => calendarDays(viewDate), [viewDate]); + const open = Boolean(anchorEl); - const openPicker = (event) => { - if (disabled) { - return; - } - const baseDate = parseLocalDateTime(value) || new Date(); - setDraftDate(baseDate); - setViewDate(new Date(baseDate.getFullYear(), baseDate.getMonth(), 1)); - setAnchorEl(event.currentTarget); - }; + const openPicker = (event) => { + if (disabled) { + return; + } + const baseDate = parseLocalDateTime(value) || new Date(); + setDraftDate(baseDate); + setViewDate(new Date(baseDate.getFullYear(), baseDate.getMonth(), 1)); + setAnchorEl(event.currentTarget); + }; - const closePicker = () => setAnchorEl(null); - const changeMonth = (offset) => { - setViewDate((current) => new Date(current.getFullYear(), current.getMonth() + offset, 1)); - }; - const changeDay = (day) => { - setDraftDate((current) => new Date( - day.getFullYear(), - day.getMonth(), - day.getDate(), - current.getHours(), - current.getMinutes() - )); - setViewDate(new Date(day.getFullYear(), day.getMonth(), 1)); - }; - const changeTime = (unit, nextValue) => { - setDraftDate((current) => { - const nextDate = new Date(current); - if (unit === "hour") { - nextDate.setHours(Number(nextValue)); - } else { - nextDate.setMinutes(Number(nextValue)); - } - return nextDate; - }); - }; + const closePicker = () => setAnchorEl(null); + const changeMonth = (offset) => { + setViewDate((current) => new Date(current.getFullYear(), current.getMonth() + offset, 1)); + }; + const changeDay = (day) => { + setDraftDate( + (current) => + new Date(day.getFullYear(), day.getMonth(), day.getDate(), current.getHours(), current.getMinutes()), + ); + setViewDate(new Date(day.getFullYear(), day.getMonth(), 1)); + }; + const changeTime = (unit, nextValue) => { + setDraftDate((current) => { + const nextDate = new Date(current); + if (unit === "hour") { + nextDate.setHours(Number(nextValue)); + } else { + nextDate.setMinutes(Number(nextValue)); + } + return nextDate; + }); + }; - return ( - <> - - - - ) - }} - label={label} - placeholder="长期有效" - value={formatDateTimeInputLabel(value)} - onClick={openPicker} - /> - -
-
- changeMonth(-1)}> - - - {monthTitle(viewDate)} - changeMonth(1)}> - - -
-
- {weekLabels.map((weekday) => ( - {weekday} - ))} - {days.map((day) => { - const isOutside = day.getMonth() !== viewDate.getMonth(); - const isSelected = sameDay(day, draftDate); - return ( - - ); - })} -
-
+ return ( + <> changeTime("hour", event.target.value)} + className={styles.dateTimeField} + disabled={disabled} + inputProps={{ readOnly: true }} + InputProps={{ + endAdornment: ( + + + + ), + }} + label={label} + placeholder="长期有效" + value={formatDateTimeInputLabel(value)} + onClick={openPicker} + /> + - {Array.from({ length: 24 }, (_, hour) => ( - {twoDigits(hour)} - ))} - - changeTime("minute", event.target.value)} - > - {Array.from({ length: 60 }, (_, minute) => ( - {twoDigits(minute)} - ))} - -
-
- - -
-
-
- - ); +
+
+ changeMonth(-1)}> + + + {monthTitle(viewDate)} + changeMonth(1)}> + + +
+
+ {weekLabels.map((weekday) => ( + + {weekday} + + ))} + {days.map((day) => { + const isOutside = day.getMonth() !== viewDate.getMonth(); + const isSelected = sameDay(day, draftDate); + return ( + + ); + })} +
+
+ changeTime("hour", event.target.value)} + > + {Array.from({ length: 24 }, (_, hour) => ( + + {twoDigits(hour)} + + ))} + + changeTime("minute", event.target.value)} + > + {Array.from({ length: 60 }, (_, minute) => ( + + {twoDigits(minute)} + + ))} + +
+
+ + +
+
+ + + ); } function GiftRegionSelect({ disabled, labels, onChange, options, value }) { - const selected = value || []; - const mergedOptions = [...new Set([...options.map((option) => option.value), ...selected])].sort( - (left, right) => Number(left) - Number(right) - ); - return ( - - ); + const selected = value || []; + const mergedOptions = [...new Set([...options.map((option) => option.value), ...selected])].sort( + (left, right) => Number(left) - Number(right), + ); + return ( + + ); } function GiftEffectSelect({ disabled, onChange, value }) { - const selected = value || []; - return ( - optionValue)} - value={selected} - onChange={onChange} - /> - ); + const selected = value || []; + return ( + optionValue)} + value={selected} + onChange={onChange} + /> + ); } function RegionTags({ regionIds = [], regionLabelById }) { - const items = [...new Set(regionIds)].sort((left, right) => Number(left) - Number(right)); - if (!items.length) { - return "-"; - } - return ( -
- {items.map((regionId) => ( - {regionLabelById[String(regionId)] || regionId} - ))} -
- ); + const items = [...new Set(regionIds)].sort((left, right) => Number(left) - Number(right)); + if (!items.length) { + return "-"; + } + return ( +
+ {items.map((regionId) => ( + + {regionLabelById[String(regionId)] || regionId} + + ))} +
+ ); } function GiftIdentity({ gift }) { - const preview = imageURL(gift.resource?.previewUrl || gift.resource?.assetUrl); - return ( -
- - {preview ? : } - -
- {gift.name || "-"} - {gift.giftId} -
-
- ); + const preview = imageURL(gift.resource?.previewUrl || gift.resource?.assetUrl); + return ( +
+ + {preview ? : } + +
+ {gift.name || "-"} + {gift.giftId} +
+
+ ); } function ResourceRef({ gift }) { - const resource = gift.resource || {}; - return ( -
- {resource.name || "-"} - {resource.resourceCode || gift.resourceId || "-"} -
- ); + const resource = gift.resource || {}; + return ( +
+ {resource.name || "-"} + {resource.resourceCode || gift.resourceId || "-"} +
+ ); } function imageURL(value) { - const url = String(value || "").trim(); - if (!url) { - return ""; - } - return /\.(avif|gif|jpe?g|png|webp)(\?|#|$)/i.test(url) ? url : ""; + const url = String(value || "").trim(); + if (!url) { + return ""; + } + return /\.(avif|gif|jpe?g|png|webp)(\?|#|$)/i.test(url) ? url : ""; } function formatNumber(value) { - return Number(value || 0).toLocaleString("zh-CN"); + return Number(value || 0).toLocaleString("zh-CN"); } function giftTypeLabel(value) { - return giftTypeOptions.find(([optionValue]) => optionValue === value)?.[1] || value || "普通礼物"; + return giftTypeOptions.find(([optionValue]) => optionValue === value)?.[1] || value || "普通礼物"; } function chargeAssetLabel(value) { - return chargeAssetOptions.find(([optionValue]) => optionValue === value)?.[1] || value || "金币"; + return chargeAssetOptions.find(([optionValue]) => optionValue === value)?.[1] || value || "金币"; } function effectTypeLabel(value) { - return effectOptions.find(([optionValue]) => optionValue === value)?.[1] || value; + return effectOptions.find(([optionValue]) => optionValue === value)?.[1] || value; } -function effectTypesLabel(values = []) { - return values.length ? values.map(effectTypeLabel).join("、") : "无特效"; +function effectTypesLabel(values) { + const list = values || []; + return list.length ? list.map(effectTypeLabel).join("、") : "无特效"; } function parseLocalDateTime(value) { - const normalizedValue = String(value || "").trim(); - if (!normalizedValue) { - return null; - } - const [datePart, timePart = "00:00"] = normalizedValue.split("T"); - const [year, month, day] = datePart.split("-").map(Number); - const [hour = 0, minute = 0] = timePart.split(":").map(Number); - if (![year, month, day, hour, minute].every(Number.isFinite)) { - return null; - } - const date = new Date(year, month - 1, day, hour, minute); - return Number.isNaN(date.getTime()) ? null : date; + const normalizedValue = String(value || "").trim(); + if (!normalizedValue) { + return null; + } + const [datePart, timePart = "00:00"] = normalizedValue.split("T"); + const [year, month, day] = datePart.split("-").map(Number); + const [hour = 0, minute = 0] = timePart.split(":").map(Number); + if (![year, month, day, hour, minute].every(Number.isFinite)) { + return null; + } + const date = new Date(year, month - 1, day, hour, minute); + return Number.isNaN(date.getTime()) ? null : date; } function toDatetimeLocalValue(date) { - return `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())}T${twoDigits(date.getHours())}:${twoDigits(date.getMinutes())}`; + return `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())}T${twoDigits(date.getHours())}:${twoDigits(date.getMinutes())}`; } function formatDateTimeInputLabel(value) { - const date = parseLocalDateTime(value); - if (!date) { - return ""; - } - return `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())} ${twoDigits(date.getHours())}:${twoDigits(date.getMinutes())}`; + const date = parseLocalDateTime(value); + if (!date) { + return ""; + } + return `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())} ${twoDigits(date.getHours())}:${twoDigits(date.getMinutes())}`; } function calendarDays(viewDate) { - const firstDay = new Date(viewDate.getFullYear(), viewDate.getMonth(), 1); - const offset = (firstDay.getDay() + 6) % 7; - const startDate = new Date(firstDay); - startDate.setDate(firstDay.getDate() - offset); - return Array.from({ length: 42 }, (_, index) => { - const day = new Date(startDate); - day.setDate(startDate.getDate() + index); - return day; - }); + const firstDay = new Date(viewDate.getFullYear(), viewDate.getMonth(), 1); + const offset = (firstDay.getDay() + 6) % 7; + const startDate = new Date(firstDay); + startDate.setDate(firstDay.getDate() - offset); + return Array.from({ length: 42 }, (_, index) => { + const day = new Date(startDate); + day.setDate(startDate.getDate() + index); + return day; + }); } function sameDay(left, right) { - return left.getFullYear() === right.getFullYear() - && left.getMonth() === right.getMonth() - && left.getDate() === right.getDate(); + return ( + left.getFullYear() === right.getFullYear() && + left.getMonth() === right.getMonth() && + left.getDate() === right.getDate() + ); } function monthTitle(date) { - return `${date.getFullYear()}年 ${date.getMonth() + 1}月`; + return `${date.getFullYear()}年 ${date.getMonth() + 1}月`; } function twoDigits(value) { - return String(value).padStart(2, "0"); + return String(value).padStart(2, "0"); } function effectiveRangeLabel(gift) { - if (!gift.effectiveFromMs && !gift.effectiveToMs) { - return "长期有效"; - } - return `${shortDateTime(gift.effectiveFromMs) || "不限"} - ${shortDateTime(gift.effectiveToMs) || "不限"}`; + if (!gift.effectiveFromMs && !gift.effectiveToMs) { + return "长期有效"; + } + return `${shortDateTime(gift.effectiveFromMs) || "不限"} - ${shortDateTime(gift.effectiveToMs) || "不限"}`; } function shortDateTime(value) { - if (!value) { - return ""; - } - return new Intl.DateTimeFormat("zh-CN", { - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit" - }).format(new Date(Number(value))); + if (!value) { + return ""; + } + return new Intl.DateTimeFormat("zh-CN", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }).format(new Date(Number(value))); } function buildRegionLabelMap(options = []) { - return options.reduce((acc, option) => { - acc[option.value] = shortRegionLabel(option.label); - return acc; - }, {}); + return options.reduce((acc, option) => { + acc[option.value] = shortRegionLabel(option.label); + return acc; + }, {}); } function shortRegionLabel(label) { - return String(label || "").split(" · ")[0] || String(label || ""); + return String(label || "").split(" · ")[0] || String(label || ""); } diff --git a/src/features/resources/pages/ResourceGrantListPage.jsx b/src/features/resources/pages/ResourceGrantListPage.jsx index 7718756..9888a0e 100644 --- a/src/features/resources/pages/ResourceGrantListPage.jsx +++ b/src/features/resources/pages/ResourceGrantListPage.jsx @@ -6,13 +6,13 @@ import { DataState } from "@/shared/ui/DataState.jsx"; import { DataTable } from "@/shared/ui/DataTable.jsx"; import { AdminActionIconButton, - AdminFilterSelect, + AdminFilterResetButton, AdminListBody, AdminListPage, AdminListToolbar, - AdminSearchBox, } from "@/shared/ui/AdminListLayout.jsx"; import { PaginationBar } from "@/shared/ui/PaginationBar.jsx"; +import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; import { formatMillis } from "@/shared/utils/time.js"; import { resourceGrantStatusFilters, @@ -86,19 +86,36 @@ export function ResourceGrantListPage() { const page = useResourceGrantListPage(); const items = page.data.items || []; const total = page.data.total || 0; + const columns = grantColumns.map((column) => { + if (column.key === "target") { + return { + ...column, + filter: createTextColumnFilter({ + placeholder: "搜索用户 ID", + value: page.query, + onChange: page.changeQuery, + }), + }; + } + if (column.key === "status") { + return { + ...column, + filter: createOptionsColumnFilter({ + options: resourceGrantStatusFilters, + placeholder: "搜索状态", + value: page.status, + onChange: page.changeStatus, + }), + }; + } + return column; + }); return ( - - - + } actions={ page.abilities.canCreateGrant ? ( @@ -110,7 +127,12 @@ export function ResourceGrantListPage() { /> - grant.grantId} /> + grant.grantId} + /> {total > 0 ? ( - column.key === "status" + column.key === "group" ? { ...column, + filter: createTextColumnFilter({ + placeholder: "搜索资源组名称、资源组编码", + value: page.query, + onChange: page.changeQuery, + }), + } + : column.key === "status" + ? { + ...column, + filter: createOptionsColumnFilter({ + options: resourceStatusFilters, + placeholder: "搜索状态", + value: page.status, + onChange: page.changeStatus, + }), render: (group) => , } : column.key === "actions" @@ -100,18 +111,7 @@ export function ResourceGroupListPage() { - - - + } actions={ page.abilities.canCreateGroup ? ( @@ -135,7 +135,9 @@ export function ResourceGroupListPage() { - - - diff --git a/src/features/resources/pages/ResourceListPage.jsx b/src/features/resources/pages/ResourceListPage.jsx index 2b1b00f..fc5fd4a 100644 --- a/src/features/resources/pages/ResourceListPage.jsx +++ b/src/features/resources/pages/ResourceListPage.jsx @@ -11,14 +11,14 @@ import { DataTable } from "@/shared/ui/DataTable.jsx"; import { UploadField } from "@/shared/ui/UploadField.jsx"; import { AdminActionIconButton, - AdminFilterSelect, + AdminFilterResetButton, AdminListBody, AdminListPage, AdminRowActions, AdminListToolbar, - AdminSearchBox, } from "@/shared/ui/AdminListLayout.jsx"; import { PaginationBar } from "@/shared/ui/PaginationBar.jsx"; +import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; import { formatMillis } from "@/shared/utils/time.js"; import { grantStrategyLabels, @@ -80,9 +80,34 @@ export function ResourceListPage() { const total = page.data.total || 0; const createDisabled = !page.abilities.canCreate || !page.abilities.canUpload; const columns = baseColumns.map((column) => - column.key === "status" + column.key === "resource" ? { ...column, + filter: createTextColumnFilter({ + placeholder: "搜索资源名称、资源编码", + value: page.query, + onChange: page.changeQuery, + }), + } + : column.key === "type" + ? { + ...column, + filter: createOptionsColumnFilter({ + options: resourceTypeFilters, + placeholder: "搜索类型", + value: page.resourceType, + onChange: page.changeResourceType, + }), + } + : column.key === "status" + ? { + ...column, + filter: createOptionsColumnFilter({ + options: resourceStatusFilters, + placeholder: "搜索状态", + value: page.status, + onChange: page.changeStatus, + }), render: (resource) => , } : column.key === "actions" @@ -97,24 +122,10 @@ export function ResourceListPage() { - - - - + } actions={ page.abilities.canCreate ? ( diff --git a/src/features/roles/components/RoleTable.jsx b/src/features/roles/components/RoleTable.jsx index 41a127c..431f549 100644 --- a/src/features/roles/components/RoleTable.jsx +++ b/src/features/roles/components/RoleTable.jsx @@ -2,10 +2,20 @@ import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined"; import EditOutlined from "@mui/icons-material/EditOutlined"; import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx"; import { DataTable } from "@/shared/ui/DataTable.jsx"; +import { createTextColumnFilter } from "@/shared/ui/tableFilters.js"; -export function RoleTable({ abilities, onEdit, onRemove, roles }) { +export function RoleTable({ abilities, onEdit, onQueryChange, onRemove, query, roles }) { const columns = [ - { key: "name", label: "角色", width: "minmax(160px, 1fr)" }, + { + key: "name", + label: "角色", + width: "minmax(160px, 1fr)", + filter: createTextColumnFilter({ + placeholder: "搜索角色、编码", + value: query, + onChange: onQueryChange + }) + }, { key: "code", label: "编码", width: "minmax(180px, 1.2fr)", render: (role) => {role.code} }, { key: "permissionCount", label: "权限数量", width: "110px", render: (role) => role.permissions?.length || 0 }, { diff --git a/src/features/roles/hooks/useRolesPage.js b/src/features/roles/hooks/useRolesPage.js index 8b09c2e..9e6aacb 100644 --- a/src/features/roles/hooks/useRolesPage.js +++ b/src/features/roles/hooks/useRolesPage.js @@ -4,12 +4,12 @@ import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js"; import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; import { - createRole, - deleteRole, - listPermissions, - listRoles, - replaceRolePermissions, - updateRole + createRole, + deleteRole, + listPermissions, + listRoles, + replaceRolePermissions, + updateRole, } from "@/features/roles/api"; import { useRoleAbilities } from "@/features/roles/permissions.js"; import { roleFormSchema } from "@/features/roles/schema"; @@ -17,220 +17,233 @@ import { roleFormSchema } from "@/features/roles/schema"; const emptyRole = { code: "", description: "", name: "", permissionIds: [] }; const emptyData = { permissions: [], roles: [] }; const permissionGroupDefinitions = [ - { key: "overview", prefixes: ["overview"], title: "总览" }, - { key: "users", prefixes: ["user"], title: "用户管理" }, - { key: "roles", prefixes: ["role"], title: "角色配置" }, - { key: "menus", prefixes: ["menu", "permission"], title: "菜单权限" }, - { key: "app-users", prefixes: ["app-user"], title: "App 用户" }, - { key: "rooms", prefixes: ["room"], title: "房间管理" }, - { key: "countries", prefixes: ["country"], title: "国家管理" }, - { key: "regions", prefixes: ["region"], title: "区域管理" }, - { key: "agencies", prefixes: ["agency"], title: "Agency 管理" }, - { key: "bds", prefixes: ["bd"], title: "BD 管理" }, - { key: "hosts", prefixes: ["host"], title: "主播管理" }, - { key: "logs", prefixes: ["log"], title: "日志审计" }, - { key: "notifications", prefixes: ["notification"], title: "通知中心" }, - { key: "jobs", prefixes: ["job", "export"], title: "任务中心" }, - { key: "common", prefixes: ["upload"], title: "通用能力" } + { key: "overview", prefixes: ["overview"], title: "总览" }, + { key: "users", prefixes: ["user"], title: "用户管理" }, + { key: "roles", prefixes: ["role"], title: "角色配置" }, + { key: "menus", prefixes: ["menu", "permission"], title: "菜单权限" }, + { key: "app-users", prefixes: ["app-user"], title: "App 用户" }, + { key: "rooms", prefixes: ["room"], title: "房间管理" }, + { key: "countries", prefixes: ["country"], title: "国家管理" }, + { key: "regions", prefixes: ["region"], title: "区域管理" }, + { key: "agencies", prefixes: ["agency"], title: "Agency 管理" }, + { key: "bds", prefixes: ["bd"], title: "BD 管理" }, + { key: "hosts", prefixes: ["host"], title: "主播管理" }, + { key: "logs", prefixes: ["log"], title: "日志审计" }, + { key: "notifications", prefixes: ["notification"], title: "通知中心" }, + { key: "jobs", prefixes: ["job", "export"], title: "任务中心" }, + { key: "common", prefixes: ["upload"], title: "通用能力" }, ]; const fallbackPermissionGroup = { key: "other", title: "其他权限" }; const permissionKindOrder = { menu: 1, button: 2, api: 3 }; export function useRolesPage() { - const [query, setQuery] = useState(""); - const [drawerOpen, setDrawerOpen] = useState(false); - const [editingRole, setEditingRole] = useState(null); - const [form, setForm] = useState(emptyRole); - const abilities = useRoleAbilities(); - const confirm = useConfirm(); - const { showToast } = useToast(); + const [query, setQuery] = useState(""); + const [drawerOpen, setDrawerOpen] = useState(false); + const [editingRole, setEditingRole] = useState(null); + const [form, setForm] = useState(emptyRole); + const abilities = useRoleAbilities(); + const confirm = useConfirm(); + const { showToast } = useToast(); - const queryFn = useCallback(async () => { - const roleData = await listRoles(); - const permissionData = abilities.canLoadPermissions ? await listPermissions() : []; - return { permissions: permissionData || [], roles: roleData || [] }; - }, [abilities.canLoadPermissions]); + const queryFn = useCallback(async () => { + const roleData = await listRoles(); + const permissionData = abilities.canLoadPermissions ? await listPermissions() : []; + return { permissions: permissionData || [], roles: roleData || [] }; + }, [abilities.canLoadPermissions]); - const { data, error, loading, reload } = useAdminQuery(queryFn, { - errorMessage: "加载角色失败", - initialData: emptyData, - keepPreviousData: true, - queryKey: ["roles", abilities.canLoadPermissions] - }); - - const permissionGroups = useMemo(() => { - return groupPermissionsByMenu(data.permissions || []); - }, [data.permissions]); - - const roles = useMemo(() => { - const keyword = query.trim().toLowerCase(); - const items = data.roles || []; - if (!keyword) { - return items; - } - return items.filter((role) => { - return [role.name, role.code, role.description].some((value) => String(value || "").toLowerCase().includes(keyword)); + const { data, error, loading, reload } = useAdminQuery(queryFn, { + errorMessage: "加载角色失败", + initialData: emptyData, + keepPreviousData: true, + queryKey: ["roles", abilities.canLoadPermissions], }); - }, [data.roles, query]); - const changeQuery = (value) => { - setQuery(value); - }; + const permissionGroups = useMemo(() => { + return groupPermissionsByMenu(data.permissions || []); + }, [data.permissions]); - const openCreate = () => { - setEditingRole(null); - setForm(emptyRole); - setDrawerOpen(true); - }; - - const openEdit = (role) => { - setEditingRole(role); - setForm({ - code: role.code, - description: role.description || "", - name: role.name, - permissionIds: role.permissions?.map((permission) => permission.id) || [] - }); - setDrawerOpen(true); - }; - - const closeDrawer = () => { - setDrawerOpen(false); - }; - - const submit = async (event) => { - event.preventDefault(); - try { - const payload = parseForm(roleFormSchema, form); - if (editingRole) { - if (abilities.canUpdate) { - await updateRole(editingRole.id, { - code: payload.code, - description: payload.description, - name: payload.name - }); + const roles = useMemo(() => { + const keyword = query.trim().toLowerCase(); + const items = data.roles || []; + if (!keyword) { + return items; } - if (abilities.canGrant) { - await replaceRolePermissions(editingRole.id, payload.permissionIds); - } - showToast("角色已更新", "success"); - } else { - await createRole({ - code: payload.code, - description: payload.description, - name: payload.name, - permissionIds: abilities.canGrant ? payload.permissionIds : [] + return items.filter((role) => { + return [role.name, role.code, role.description].some((value) => + String(value || "") + .toLowerCase() + .includes(keyword), + ); }); - showToast("角色已创建", "success"); - } - setDrawerOpen(false); - reload(); - } catch (err) { - showToast(err.message || "保存角色失败", "error"); - } - }; + }, [data.roles, query]); - const remove = async (role) => { - const ok = await confirm({ - confirmText: "删除", - message: `${role.name} 删除前需要先解绑用户。`, - title: "删除角色", - tone: "danger" - }); - if (!ok) { - return; - } - try { - await deleteRole(role.id); - showToast("角色已删除", "success"); - reload(); - } catch (err) { - showToast(err.message || "删除角色失败", "error"); - } - }; + const changeQuery = (value) => { + setQuery(value); + }; - const togglePermission = (permissionId, checked) => { - if (!abilities.canGrant) { - return; - } - setForm((current) => ({ - ...current, - permissionIds: checked - ? Array.from(new Set([...current.permissionIds, permissionId])) - : current.permissionIds.filter((id) => id !== permissionId) - })); - }; + const resetFilters = () => { + setQuery(""); + }; - const togglePermissionGroup = (permissionIds, checked) => { - if (!abilities.canGrant) { - return; - } - setForm((current) => { - const targetIds = new Set(permissionIds); + const openCreate = () => { + setEditingRole(null); + setForm(emptyRole); + setDrawerOpen(true); + }; - return { - ...current, - permissionIds: checked - ? Array.from(new Set([...current.permissionIds, ...permissionIds])) - : current.permissionIds.filter((id) => !targetIds.has(id)) - }; - }); - }; + const openEdit = (role) => { + setEditingRole(role); + setForm({ + code: role.code, + description: role.description || "", + name: role.name, + permissionIds: role.permissions?.map((permission) => permission.id) || [], + }); + setDrawerOpen(true); + }; - return { - abilities, - changeQuery, - closeDrawer, - drawerOpen, - editingRole, - error, - form, - loading, - openCreate, - openEdit, - permissionGroups, - permissions: data.permissions || [], - query, - reload, - remove, - roles, - setForm, - submit, - togglePermission, - togglePermissionGroup - }; + const closeDrawer = () => { + setDrawerOpen(false); + }; + + const submit = async (event) => { + event.preventDefault(); + try { + const payload = parseForm(roleFormSchema, form); + if (editingRole) { + if (abilities.canUpdate) { + await updateRole(editingRole.id, { + code: payload.code, + description: payload.description, + name: payload.name, + }); + } + if (abilities.canGrant) { + await replaceRolePermissions(editingRole.id, payload.permissionIds); + } + showToast("角色已更新", "success"); + } else { + await createRole({ + code: payload.code, + description: payload.description, + name: payload.name, + permissionIds: abilities.canGrant ? payload.permissionIds : [], + }); + showToast("角色已创建", "success"); + } + setDrawerOpen(false); + reload(); + } catch (err) { + showToast(err.message || "保存角色失败", "error"); + } + }; + + const remove = async (role) => { + const ok = await confirm({ + confirmText: "删除", + message: `${role.name} 删除前需要先解绑用户。`, + title: "删除角色", + tone: "danger", + }); + if (!ok) { + return; + } + try { + await deleteRole(role.id); + showToast("角色已删除", "success"); + reload(); + } catch (err) { + showToast(err.message || "删除角色失败", "error"); + } + }; + + const togglePermission = (permissionId, checked) => { + if (!abilities.canGrant) { + return; + } + setForm((current) => ({ + ...current, + permissionIds: checked + ? Array.from(new Set([...current.permissionIds, permissionId])) + : current.permissionIds.filter((id) => id !== permissionId), + })); + }; + + const togglePermissionGroup = (permissionIds, checked) => { + if (!abilities.canGrant) { + return; + } + setForm((current) => { + const targetIds = new Set(permissionIds); + + return { + ...current, + permissionIds: checked + ? Array.from(new Set([...current.permissionIds, ...permissionIds])) + : current.permissionIds.filter((id) => !targetIds.has(id)), + }; + }); + }; + + return { + abilities, + changeQuery, + closeDrawer, + drawerOpen, + editingRole, + error, + form, + loading, + openCreate, + openEdit, + permissionGroups, + permissions: data.permissions || [], + query, + reload, + remove, + resetFilters, + roles, + setForm, + submit, + togglePermission, + togglePermissionGroup, + }; } function groupPermissionsByMenu(permissions) { - const groups = new Map(permissionGroupDefinitions.map((definition) => [definition.key, { ...definition, items: [] }])); - const prefixToGroup = new Map( - permissionGroupDefinitions.flatMap((definition) => definition.prefixes.map((prefix) => [prefix, definition.key])) - ); - const fallbackGroup = { ...fallbackPermissionGroup, items: [] }; + const groups = new Map( + permissionGroupDefinitions.map((definition) => [definition.key, { ...definition, items: [] }]), + ); + const prefixToGroup = new Map( + permissionGroupDefinitions.flatMap((definition) => + definition.prefixes.map((prefix) => [prefix, definition.key]), + ), + ); + const fallbackGroup = { ...fallbackPermissionGroup, items: [] }; - permissions.forEach((permission) => { - const prefix = getPermissionPrefix(permission.code); - const groupKey = prefixToGroup.get(prefix); - const group = groupKey ? groups.get(groupKey) : fallbackGroup; - group.items.push(permission); - }); + permissions.forEach((permission) => { + const prefix = getPermissionPrefix(permission.code); + const groupKey = prefixToGroup.get(prefix); + const group = groupKey ? groups.get(groupKey) : fallbackGroup; + group.items.push(permission); + }); - return [...groups.values(), fallbackGroup] - .filter((group) => group.items.length > 0) - .map((group) => ({ - ...group, - items: [...group.items].sort(comparePermissions) - })); + return [...groups.values(), fallbackGroup] + .filter((group) => group.items.length > 0) + .map((group) => ({ + ...group, + items: [...group.items].sort(comparePermissions), + })); } function getPermissionPrefix(code = "") { - return code.split(":")[0] || "other"; + return code.split(":")[0] || "other"; } function comparePermissions(left, right) { - const kindDiff = (permissionKindOrder[left.kind] || 9) - (permissionKindOrder[right.kind] || 9); - if (kindDiff !== 0) { - return kindDiff; - } - return left.code.localeCompare(right.code); + const kindDiff = (permissionKindOrder[left.kind] || 9) - (permissionKindOrder[right.kind] || 9); + if (kindDiff !== 0) { + return kindDiff; + } + return left.code.localeCompare(right.code); } diff --git a/src/features/roles/pages/RoleManagementPage.jsx b/src/features/roles/pages/RoleManagementPage.jsx index eeca37e..d932f10 100644 --- a/src/features/roles/pages/RoleManagementPage.jsx +++ b/src/features/roles/pages/RoleManagementPage.jsx @@ -1,53 +1,64 @@ import Add from "@mui/icons-material/Add"; import { DataState } from "@/shared/ui/DataState.jsx"; import { - AdminActionIconButton, - AdminListBody, - AdminListPage, - AdminListToolbar, - AdminSearchBox + AdminActionIconButton, + AdminFilterResetButton, + AdminListBody, + AdminListPage, + AdminListToolbar, } from "@/shared/ui/AdminListLayout.jsx"; import { RoleFormDrawer } from "@/features/roles/components/RoleFormDrawer.jsx"; import { RoleTable } from "@/features/roles/components/RoleTable.jsx"; import { useRolesPage } from "@/features/roles/hooks/useRolesPage.js"; export function RoleManagementPage() { - const page = useRolesPage(); + const page = useRolesPage(); - return ( - <> - - } - actions={page.abilities.canCreate ? ( - - - - ) : null} - /> + return ( + <> + + + } + actions={ + page.abilities.canCreate ? ( + + + + ) : null + } + /> - - - -
- {page.roles.length} 条 -
-
-
-
+ + + +
+ {page.roles.length} 条 +
+
+
+
- - - ); + + + ); } diff --git a/src/features/rooms/components/RoomDetailDrawer.jsx b/src/features/rooms/components/RoomDetailDrawer.jsx new file mode 100644 index 0000000..ac3c6e1 --- /dev/null +++ b/src/features/rooms/components/RoomDetailDrawer.jsx @@ -0,0 +1,120 @@ +import MeetingRoomOutlined from "@mui/icons-material/MeetingRoomOutlined"; +import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined"; +import { SideDrawer } from "@/shared/ui/SideDrawer.jsx"; +import { formatMillis } from "@/shared/utils/time.js"; +import { roomStatusLabels } from "@/features/rooms/constants.js"; +import styles from "@/features/rooms/rooms.module.css"; + +export function RoomDetailDrawer({ onClose, open, room }) { + const owner = room?.owner || {}; + const normalizedStatus = room?.status === "active" ? "active" : "closed"; + const ownerDisplayId = owner.displayUserId || room?.ownerUserId || "-"; + + return ( + + {room ? ( + <> +
+ + {room.coverUrl ? : } + +
+
+

{room.title || "-"}

+ +
+ 房间 ID {room.roomId} +
+
+ + + +
+

房主信息

+
+ + {owner.avatar ? : } + +
+ {owner.username || "-"} + {ownerDisplayId} +
+
+
+ + +
+
+ + + + ) : null} +
+ ); +} + +function DetailSection({ items, title }) { + return ( +
+

{title}

+
+ {items.map(([label, value]) => ( + + ))} +
+
+ ); +} + +function DetailItem({ label, value }) { + return ( +
+ {label} + {displayValue(value)} +
+ ); +} + +function StatusBadge({ status }) { + const tone = status === "active" ? "running" : "stopped"; + + return ( + + + {roomStatusLabels[status]} + + ); +} + +function displayValue(value) { + return value === 0 || value ? value : "-"; +} + +function formatNumber(value) { + return Number(value || 0).toLocaleString("zh-CN"); +} + +function formatRegion(value) { + return value ? `区域 ${value}` : "-"; +} diff --git a/src/features/rooms/hooks/useRoomsPage.js b/src/features/rooms/hooks/useRoomsPage.js index 72abd08..4785b42 100644 --- a/src/features/rooms/hooks/useRoomsPage.js +++ b/src/features/rooms/hooks/useRoomsPage.js @@ -11,166 +11,185 @@ import { roomStatusSchema, roomUpdateSchema } from "@/features/rooms/schema"; const pageSize = 10; const emptyData = { items: [], page: 1, pageSize, total: 0 }; const emptyForm = () => ({ - coverUrl: "", - description: "", - title: "", - visibleRegionId: 0 + coverUrl: "", + description: "", + title: "", + visibleRegionId: 0, }); const emptyStatusForm = () => ({ status: "active" }); export function useRoomsPage() { - const abilities = useRoomAbilities(); - const confirm = useConfirm(); - const { showToast } = useToast(); - const { loadingRegions, regionOptions } = useRegionOptions(); - const [query, setQuery] = useState(""); - const [status, setStatus] = useState(""); - const [regionId, setRegionId] = useState(""); - const [page, setPage] = useState(1); - const [activeAction, setActiveAction] = useState(""); - const [activeRoom, setActiveRoom] = useState(null); - const [form, setForm] = useState(emptyForm); - const [statusForm, setStatusForm] = useState(emptyStatusForm); - const [loadingAction, setLoadingAction] = useState(""); - const filters = useMemo( - () => ({ - keyword: query, - region_id: regionId, - status - }), - [query, regionId, status] - ); - const { data = emptyData, error, loading, reload } = usePaginatedQuery({ - errorMessage: "加载房间列表失败", - fetcher: listRooms, - filters, - page, - pageSize, - queryKey: ["rooms", filters, page] - }); - - const changeQuery = (value) => { - setQuery(value); - setPage(1); - }; - - const changeStatus = (value) => { - setStatus(value); - setPage(1); - }; - - const changeRegionId = (value) => { - setRegionId(value); - setPage(1); - }; - - const openEdit = (room) => { - setActiveRoom(room); - setForm({ - coverUrl: room.coverUrl || "", - description: "", - title: room.title || "", - visibleRegionId: room.visibleRegionId || 0 + const abilities = useRoomAbilities(); + const confirm = useConfirm(); + const { showToast } = useToast(); + const { loadingRegions, regionOptions } = useRegionOptions(); + const [query, setQuery] = useState(""); + const [status, setStatus] = useState(""); + const [regionId, setRegionId] = useState(""); + const [page, setPage] = useState(1); + const [activeAction, setActiveAction] = useState(""); + const [activeRoom, setActiveRoom] = useState(null); + const [form, setForm] = useState(emptyForm); + const [statusForm, setStatusForm] = useState(emptyStatusForm); + const [loadingAction, setLoadingAction] = useState(""); + const filters = useMemo( + () => ({ + keyword: query, + region_id: regionId, + status, + }), + [query, regionId, status], + ); + const { + data = emptyData, + error, + loading, + reload, + } = usePaginatedQuery({ + errorMessage: "加载房间列表失败", + fetcher: listRooms, + filters, + page, + pageSize, + queryKey: ["rooms", filters, page], }); - setActiveAction("edit"); - }; - const openStatus = (room) => { - setActiveRoom(room); - setStatusForm({ status: room.status === "closed" ? "closed" : "active" }); - setActiveAction("status"); - }; + const changeQuery = (value) => { + setQuery(value); + setPage(1); + }; - const closeAction = () => { - setActiveAction(""); - setActiveRoom(null); - }; + const changeStatus = (value) => { + setStatus(value); + setPage(1); + }; - const submitEdit = async (event) => { - event.preventDefault(); - if (!activeRoom) { - return; - } - await runAction("edit", "房间已更新", async () => { - const payload = parseForm(roomUpdateSchema, form); - if (!String(form.description || "").trim()) { - delete payload.description; - } - await updateRoom(activeRoom.roomId, payload); - closeAction(); - await reload(); - }); - }; + const changeRegionId = (value) => { + setRegionId(value); + setPage(1); + }; - const submitStatus = async (event) => { - event.preventDefault(); - if (!activeRoom) { - return; - } - await runAction("status", "房间状态已更新", async () => { - const payload = parseForm(roomStatusSchema, statusForm); - await updateRoom(activeRoom.roomId, payload); - closeAction(); - await reload(); - }); - }; + const resetFilters = () => { + setQuery(""); + setStatus(""); + setRegionId(""); + setPage(1); + }; - const removeRoom = async (room) => { - const ok = await confirm({ - confirmText: "删除", - message: `${room.title || room.roomId} 会从房间列表和房间状态中删除。`, - title: "删除房间", - tone: "danger" - }); - if (!ok) { - return; - } - await runAction(`delete-${room.roomId}`, "房间已删除", async () => { - await deleteRoom(room.roomId); - await reload(); - }); - }; + const openEdit = (room) => { + setActiveRoom(room); + setForm({ + coverUrl: room.coverUrl || "", + description: "", + title: room.title || "", + visibleRegionId: room.visibleRegionId || 0, + }); + setActiveAction("edit"); + }; - const runAction = async (action, successMessage, submitter) => { - setLoadingAction(action); - try { - await submitter(); - showToast(successMessage, "success"); - } catch (err) { - showToast(err.message || "操作失败", "error"); - } finally { - setLoadingAction(""); - } - }; + const openDetail = (room) => { + setActiveRoom(room); + setActiveAction("detail"); + }; - return { - abilities, - activeAction, - activeRoom, - changeQuery, - changeRegionId, - changeStatus, - closeAction, - data, - error, - form, - loading, - loadingAction, - loadingRegions, - openEdit, - openStatus, - page, - query, - regionId, - regionOptions, - reload, - removeRoom, - setForm, - setPage, - setStatusForm, - status, - statusForm, - submitStatus, - submitEdit - }; + const openStatus = (room) => { + setActiveRoom(room); + setStatusForm({ status: room.status === "closed" ? "closed" : "active" }); + setActiveAction("status"); + }; + + const closeAction = () => { + setActiveAction(""); + setActiveRoom(null); + }; + + const submitEdit = async (event) => { + event.preventDefault(); + if (!activeRoom) { + return; + } + await runAction("edit", "房间已更新", async () => { + const payload = parseForm(roomUpdateSchema, form); + if (!String(form.description || "").trim()) { + delete payload.description; + } + await updateRoom(activeRoom.roomId, payload); + closeAction(); + await reload(); + }); + }; + + const submitStatus = async (event) => { + event.preventDefault(); + if (!activeRoom) { + return; + } + await runAction("status", "房间状态已更新", async () => { + const payload = parseForm(roomStatusSchema, statusForm); + await updateRoom(activeRoom.roomId, payload); + closeAction(); + await reload(); + }); + }; + + const removeRoom = async (room) => { + const ok = await confirm({ + confirmText: "删除", + message: `${room.title || room.roomId} 会从房间列表和房间状态中删除。`, + title: "删除房间", + tone: "danger", + }); + if (!ok) { + return; + } + await runAction(`delete-${room.roomId}`, "房间已删除", async () => { + await deleteRoom(room.roomId); + await reload(); + }); + }; + + const runAction = async (action, successMessage, submitter) => { + setLoadingAction(action); + try { + await submitter(); + showToast(successMessage, "success"); + } catch (err) { + showToast(err.message || "操作失败", "error"); + } finally { + setLoadingAction(""); + } + }; + + return { + abilities, + activeAction, + activeRoom, + changeQuery, + changeRegionId, + changeStatus, + closeAction, + data, + error, + form, + loading, + loadingAction, + loadingRegions, + openDetail, + openEdit, + openStatus, + page, + query, + regionId, + regionOptions, + reload, + removeRoom, + resetFilters, + setForm, + setPage, + setStatusForm, + status, + statusForm, + submitStatus, + submitEdit, + }; } diff --git a/src/features/rooms/pages/RoomListPage.jsx b/src/features/rooms/pages/RoomListPage.jsx index 984170c..f6a4931 100644 --- a/src/features/rooms/pages/RoomListPage.jsx +++ b/src/features/rooms/pages/RoomListPage.jsx @@ -10,242 +10,319 @@ import { DataState } from "@/shared/ui/DataState.jsx"; import { DataTable } from "@/shared/ui/DataTable.jsx"; import { IconButton } from "@/shared/ui/IconButton.jsx"; import { PaginationBar } from "@/shared/ui/PaginationBar.jsx"; -import { createRegionColumnFilter, RegionFilterControl } from "@/shared/ui/RegionFilterControl.jsx"; +import { AdminFilterResetButton } from "@/shared/ui/AdminListLayout.jsx"; import { RegionSelect } from "@/shared/ui/RegionSelect.jsx"; -import { SearchBox } from "@/shared/ui/SearchBox.jsx"; -import { StatusSelect } from "@/shared/ui/StatusSelect.jsx"; +import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx"; +import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; import { UploadField } from "@/shared/ui/UploadField.jsx"; import { formatMillis } from "@/shared/utils/time.js"; +import { RoomDetailDrawer } from "@/features/rooms/components/RoomDetailDrawer.jsx"; import { roomStatusFilters, roomStatusLabels } from "@/features/rooms/constants.js"; import { useRoomsPage } from "@/features/rooms/hooks/useRoomsPage.js"; import styles from "@/features/rooms/rooms.module.css"; const columns = [ - { - key: "identity", - label: "房间", - width: "minmax(240px, 1.5fr)", - render: (room) => - }, - { - key: "owner", - label: "房主", - width: "minmax(210px, 1.2fr)", - render: (room) => - }, - { - key: "region", - label: "区域", - width: "minmax(140px, 0.8fr)", - render: (room) => room.regionName || (room.visibleRegionId ? `区域 ${room.visibleRegionId}` : "-") - }, - { - key: "onlineCount", - label: "房间人数", - width: "minmax(110px, 0.7fr)", - render: (room) => `${formatNumber(room.onlineCount)} 人` - }, - { - key: "status", - label: "状态", - width: "minmax(100px, 0.7fr)", - render: (room) => - }, - { - key: "time", - label: "创建时间", - width: "minmax(170px, 1fr)", - render: (room) => formatMillis(room.createdAtMs) - } + { + key: "identity", + label: "房间", + width: "minmax(240px, 1.5fr)", + render: (room) => , + }, + { + key: "owner", + label: "房主", + width: "minmax(210px, 1.2fr)", + render: (room) => , + }, + { + key: "region", + label: "区域", + width: "minmax(140px, 0.8fr)", + render: (room) => room.regionName || (room.visibleRegionId ? `区域 ${room.visibleRegionId}` : "-"), + }, + { + key: "onlineCount", + label: "房间人数", + width: "minmax(110px, 0.7fr)", + render: (room) => `${formatNumber(room.onlineCount)} 人`, + }, + { + key: "status", + label: "状态", + width: "minmax(100px, 0.7fr)", + render: (room) => , + }, + { + key: "time", + label: "创建时间", + width: "minmax(170px, 1fr)", + render: (room) => formatMillis(room.createdAtMs), + }, ]; export function RoomListPage() { - const page = useRoomsPage(); - const items = page.data.items || []; - const total = page.data.total || 0; - const tableColumns = [ - ...columns.map((column) => - column.key === "region" - ? { - ...column, - filter: createRegionColumnFilter({ - loading: page.loadingRegions, - options: page.regionOptions, - value: page.regionId, - onChange: page.changeRegionId - }) - } - : column.key === "status" - ? { - ...column, - render: (room) => ( - page.openStatus(room)} /> - ) - } - : column - ), - { - key: "actions", - label: "操作", - width: "minmax(104px, 0.6fr)", - render: (room) => - } - ]; + const page = useRoomsPage(); + const items = page.data.items || []; + const total = page.data.total || 0; + const tableColumns = [ + ...columns.map((column) => + column.key === "region" + ? { + ...column, + filter: createRegionColumnFilter({ + loading: page.loadingRegions, + options: page.regionOptions, + value: page.regionId, + onChange: page.changeRegionId, + }), + } + : column.key === "identity" + ? { + ...column, + filter: createTextColumnFilter({ + placeholder: "搜索房间、房主", + value: page.query, + onChange: page.changeQuery, + }), + render: (room) => page.openDetail(room)} />, + } + : column.key === "status" + ? { + ...column, + filter: createOptionsColumnFilter({ + options: roomStatusFilters, + placeholder: "搜索状态", + value: page.status, + onChange: page.changeStatus, + }), + render: (room) => ( + page.openStatus(room)} + /> + ), + } + : column, + ), + { + key: "actions", + label: "操作", + width: "minmax(104px, 0.6fr)", + render: (room) => , + }, + ]; - return ( -
-
-
-
- - - -
-
+ return ( +
+
+
+
+ +
+
- -
- room.roomId} /> - {total > 0 ? : null} -
-
-
+ +
+ room.roomId} + /> + {total > 0 ? ( + + ) : null} +
+
+
- - page.setForm({ ...page.form, title: event.target.value })} /> - page.setForm({ ...page.form, coverUrl })} /> - page.setForm({ ...page.form, visibleRegionId })} /> - page.setForm({ ...page.form, description: event.target.value })} /> - + - - page.setStatusForm({ status: event.target.value })}> - 正常 - 关闭 - - -
- ); + + page.setForm({ ...page.form, title: event.target.value })} + /> + page.setForm({ ...page.form, coverUrl })} + /> + page.setForm({ ...page.form, visibleRegionId })} + /> + page.setForm({ ...page.form, description: event.target.value })} + /> + + + + page.setStatusForm({ status: event.target.value })} + > + 正常 + 关闭 + + + + ); } -function RoomIdentity({ room }) { - return ( -
- - {room.coverUrl ? : } - -
-
{room.title || "-"}
-
{room.roomId}
-
-
- ); +function RoomIdentity({ onClick, room }) { + const content = ( +
+ + {room.coverUrl ? : } + +
+
{room.title || "-"}
+
{room.roomId}
+
+
+ ); + + if (!onClick) { + return content; + } + + return ( + + ); } function OwnerIdentity({ room }) { - const owner = room.owner || {}; - const ownerName = owner.username || "-"; - const ownerDisplayId = owner.displayUserId || room.ownerUserId || "-"; + const owner = room.owner || {}; + const ownerName = owner.username || "-"; + const ownerDisplayId = owner.displayUserId || room.ownerUserId || "-"; - return ( -
- - {owner.avatar ? : } - -
-
{ownerName}
-
{ownerDisplayId}
-
-
- ); + return ( +
+ + {owner.avatar ? : } + +
+
{ownerName}
+
{ownerDisplayId}
+
+
+ ); } function RoomActions({ page, room }) { - return ( -
- {page.abilities.canUpdate ? ( - - - page.openEdit(room)}> - - - - - ) : null} - {page.abilities.canDelete ? ( - - - page.removeRoom(room)}> - - - - - ) : null} -
- ); + return ( +
+ {page.abilities.canUpdate ? ( + + + page.openEdit(room)}> + + + + + ) : null} + {page.abilities.canDelete ? ( + + + page.removeRoom(room)} + > + + + + + ) : null} +
+ ); } function RoomStatus({ disabled, onClick, status }) { - const normalizedStatus = status === "active" ? "active" : "closed"; - const tone = normalizedStatus === "active" ? "running" : "stopped"; - const badge = ( - - - {roomStatusLabels[normalizedStatus]} - - ); - if (disabled) { - return badge; - } - return ( - - ); + const normalizedStatus = status === "active" ? "active" : "closed"; + const tone = normalizedStatus === "active" ? "running" : "stopped"; + const badge = ( + + + {roomStatusLabels[normalizedStatus]} + + ); + if (disabled) { + return badge; + } + return ( + + ); } function ActionModal({ children, disabled, loading, onClose, onSubmit, open, title }) { - return ( - - {children} - - ); + return ( + + {children} + + ); } function formatNumber(value) { - return Number(value || 0).toLocaleString("zh-CN"); + return Number(value || 0).toLocaleString("zh-CN"); } diff --git a/src/features/rooms/rooms.module.css b/src/features/rooms/rooms.module.css index 5c21d8a..28d9b96 100644 --- a/src/features/rooms/rooms.module.css +++ b/src/features/rooms/rooms.module.css @@ -78,6 +78,34 @@ gap: var(--space-3); } +.identityButton { + display: block; + width: 100%; + padding: 2px; + border: 0; + border-radius: var(--radius-control); + background: transparent; + text-align: left; + cursor: pointer; + transition: + background var(--motion-base) var(--ease-standard), + box-shadow var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.identityButton:hover { + background: var(--bg-hover); +} + +.identityButton:hover .name { + color: var(--primary); +} + +.identityButton:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + .cover { display: inline-flex; width: 36px; @@ -170,6 +198,156 @@ border-color: var(--primary-border); } +.detailHero { + display: flex; + min-width: 0; + align-items: center; + gap: var(--space-4); + padding-bottom: var(--space-4); + border-bottom: 1px solid var(--border); +} + +.detailCover { + display: inline-flex; + width: 72px; + height: 72px; + flex: 0 0 auto; + align-items: center; + justify-content: center; + overflow: hidden; + border: 1px solid var(--border); + border-radius: var(--radius-card); + background: var(--bg-card-strong); + color: var(--text-secondary); +} + +.detailCover img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.detailHeroText { + display: grid; + min-width: 0; + gap: var(--space-2); +} + +.detailTitleRow { + display: flex; + min-width: 0; + align-items: center; + flex-wrap: wrap; + gap: var(--space-2); +} + +.detailTitleRow h3 { + overflow: hidden; + margin: 0; + color: var(--text-primary); + font-size: 18px; + font-weight: 800; + text-overflow: ellipsis; + white-space: nowrap; +} + +.detailMeta { + color: var(--text-tertiary); + font-size: var(--admin-font-size); +} + +.detailSection { + display: grid; + gap: var(--space-3); + padding: var(--space-4); + border: 1px solid var(--border); + border-radius: var(--radius-card); + background: var(--bg-card-strong); +} + +.detailSectionTitle { + margin: 0; + color: var(--text-primary); + font-size: var(--admin-font-size); + font-weight: 800; +} + +.detailGrid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--space-3); +} + +.detailItem { + display: grid; + min-width: 0; + gap: 4px; +} + +.detailLabel { + color: var(--text-tertiary); + font-size: 12px; +} + +.detailValue { + overflow: hidden; + color: var(--text-primary); + font-size: var(--admin-font-size); + font-weight: 650; + text-overflow: ellipsis; + white-space: nowrap; +} + +.detailOwner { + display: flex; + min-width: 0; + align-items: center; + gap: var(--space-3); +} + +.detailOwnerAvatar { + display: inline-flex; + width: 40px; + height: 40px; + flex: 0 0 auto; + align-items: center; + justify-content: center; + overflow: hidden; + border: 1px solid var(--border); + border-radius: 50%; + background: var(--bg-card); + color: var(--text-secondary); +} + +.detailOwnerAvatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.detailOwnerText { + display: grid; + min-width: 0; + gap: 2px; +} + +.detailOwnerText strong, +.detailOwnerText span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.detailOwnerText strong { + color: var(--text-primary); + font-size: var(--admin-font-size); +} + +.detailOwnerText span { + color: var(--text-tertiary); + font-size: 12px; +} + @media (max-width: 760px) { .toolbar, .filters { @@ -183,4 +361,8 @@ flex: 1 1 auto; width: 100%; } + + .detailGrid { + grid-template-columns: 1fr; + } } diff --git a/src/features/search/components/SearchDialog.jsx b/src/features/search/components/SearchDialog.jsx index 9e77e53..259c441 100644 --- a/src/features/search/components/SearchDialog.jsx +++ b/src/features/search/components/SearchDialog.jsx @@ -2,14 +2,18 @@ import Dialog from "@mui/material/Dialog"; import List from "@mui/material/List"; import ListItemButton from "@mui/material/ListItemButton"; import ListItemText from "@mui/material/ListItemText"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { searchGlobal } from "@/features/search/api"; import { SearchBox } from "@/shared/ui/SearchBox.jsx"; -export function SearchDialog({ onClose, onSelect, open }) { +const maxMenuResults = 8; + +export function SearchDialog({ menuItems = [], onClose, onSelect, open }) { const [keyword, setKeyword] = useState(""); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); + const menuResults = useMemo(() => searchMenus(menuItems, keyword), [keyword, menuItems]); + const displayResults = useMemo(() => [...menuResults, ...results], [menuResults, results]); useEffect(() => { if (!open) { @@ -53,11 +57,11 @@ export function SearchDialog({ onClose, onSelect, open }) { slotProps={{ paper: { className: "search-dialog" } }} >
- +
- {results.map((item) => ( + {displayResults.map((item) => ( selectResult(item)}> ))} - {!results.length ?
{loading ? "搜索中..." : "暂无匹配结果"}
: null} + {!displayResults.length ?
{loading ? "搜索中..." : "暂无匹配结果"}
: null}
); } + +export function searchMenus(items = [], keyword = "") { + const normalizedKeyword = normalizeSearchText(keyword); + if (!normalizedKeyword) { + return []; + } + + return flattenMenuResults(items) + .filter((item) => item.searchText.includes(normalizedKeyword)) + .slice(0, maxMenuResults) + .map(({ searchText: _searchText, ...item }) => item); +} + +function flattenMenuResults(items = [], ancestors = []) { + return items.flatMap((item) => { + const label = item.label || item.title || item.code || ""; + const breadcrumb = [...ancestors, label].filter(Boolean); + const target = item.path || findFirstMenuPath(item.children); + const current = target + ? [ + { + id: item.code || item.id || breadcrumb.join("/"), + searchText: normalizeSearchText([label, item.code, item.path].filter(Boolean).join(" ")), + subtitle: breadcrumb.join(" / "), + target, + title: label, + type: "菜单" + } + ] + : []; + + return [...current, ...flattenMenuResults(item.children || [], breadcrumb)]; + }); +} + +function findFirstMenuPath(items = []) { + for (const item of items) { + if (item.path) { + return item.path; + } + + const childPath = findFirstMenuPath(item.children || []); + if (childPath) { + return childPath; + } + } + + return ""; +} + +function normalizeSearchText(value) { + return String(value || "").trim().toLowerCase(); +} diff --git a/src/features/search/components/SearchDialog.test.jsx b/src/features/search/components/SearchDialog.test.jsx new file mode 100644 index 0000000..aab6705 --- /dev/null +++ b/src/features/search/components/SearchDialog.test.jsx @@ -0,0 +1,42 @@ +import { describe, expect, test } from "vitest"; +import { searchMenus } from "./SearchDialog.jsx"; + +describe("searchMenus", () => { + const menus = [ + { + code: "overview", + label: "系统概览", + path: "/overview" + }, + { + code: "app-users", + label: "用户管理", + children: [{ code: "app-users-list", label: "用户列表", path: "/app/users" }] + }, + { + code: "system", + label: "后台设置", + children: [ + { code: "system-users", label: "后台用户", path: "/system/users" }, + { code: "system-roles", label: "角色配置", path: "/system/roles" } + ] + } + ]; + + test("matches visible menu labels and returns navigable targets", () => { + expect(searchMenus(menus, "后台")).toEqual([ + expect.objectContaining({ target: "/system/users", title: "后台设置", type: "菜单" }), + expect.objectContaining({ target: "/system/users", title: "后台用户", type: "菜单" }) + ]); + }); + + test("matches menu code and keeps breadcrumb for context", () => { + expect(searchMenus(menus, "app-users-list")).toEqual([ + expect.objectContaining({ subtitle: "用户管理 / 用户列表", target: "/app/users", title: "用户列表" }) + ]); + }); + + test("returns nothing for blank keyword", () => { + expect(searchMenus(menus, " ")).toEqual([]); + }); +}); diff --git a/src/features/users/components/UserFilters.jsx b/src/features/users/components/UserFilters.jsx deleted file mode 100644 index cb06db2..0000000 --- a/src/features/users/components/UserFilters.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Button } from "@/shared/ui/Button.jsx"; -import { FilterChip } from "@/shared/ui/FilterChip.jsx"; -import { SearchBox } from "@/shared/ui/SearchBox.jsx"; - -const statusFilters = [ - ["all", "全部"], - ["active", "启用"], - ["locked", "锁定"], - ["disabled", "停用"] -]; - -export function UserFilters({ - canBatchStatus, - onBatchStatus, - onQueryChange, - onRoleFilterChange, - onStatusFilterChange, - query, - roleFilter, - roleFilters, - statusFilter -}) { - return ( -
- -
- {roleFilters.map(([value, label]) => ( - onRoleFilterChange(value)} /> - ))} -
-
- {statusFilters.map(([value, label]) => ( - onStatusFilterChange(value)} /> - ))} -
- {canBatchStatus ? ( -
- - -
- ) : null} -
- ); -} diff --git a/src/features/users/components/UserTable.jsx b/src/features/users/components/UserTable.jsx index 41bc7cb..704bd05 100644 --- a/src/features/users/components/UserTable.jsx +++ b/src/features/users/components/UserTable.jsx @@ -8,6 +8,7 @@ import VerifiedUserOutlined from "@mui/icons-material/VerifiedUserOutlined"; import Checkbox from "@mui/material/Checkbox"; import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx"; import { DataTable } from "@/shared/ui/DataTable.jsx"; +import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; import { UserStatusBadge } from "./UserStatusBadge.jsx"; export function UserTable({ @@ -16,11 +17,19 @@ export function UserTable({ canStatus, onEdit, onOpenProfile = () => {}, + onQueryChange, onResetPassword, + onRoleFilterChange, + onStatusFilterChange, onToggleDisabled, onToggleLocked, + query, + roleFilter, + roleFilters = [], selectedIds, setSelectedIds, + statusFilter, + statusFilters = [], users }) { const allChecked = users.length > 0 && users.every((user) => selectedIds.includes(user.id)); @@ -51,28 +60,55 @@ export function UserTable({ key: "user", label: "用户", width: "minmax(180px, 1.4fr)", + filter: createTextColumnFilter({ + placeholder: "搜索用户、账号、角色、团队", + value: query, + onChange: onQueryChange + }), render: (user) => ( -
- -
-
{user.name}
-
{user.account}
-
-
+ + {user.name} + {user.account} + +
+ ) }, - { key: "role", label: "角色", width: "minmax(120px, 1fr)", render: (user) => user.role || "-" }, + { + key: "role", + label: "角色", + width: "minmax(120px, 1fr)", + filter: createOptionsColumnFilter({ + options: roleFilters, + placeholder: "搜索角色", + value: roleFilter, + onChange: onRoleFilterChange + }), + render: (user) => user.role || "-" + }, { key: "team", label: "团队", width: "minmax(120px, 1fr)", render: (user) => user.team || "-" }, - { key: "status", label: "状态", width: "minmax(96px, 0.72fr)", render: (user) => }, + { + key: "status", + label: "状态", + width: "minmax(96px, 0.72fr)", + filter: createOptionsColumnFilter({ + options: statusFilters, + placeholder: "搜索状态", + value: statusFilter, + onChange: onStatusFilterChange + }), + render: (user) => + }, { key: "mfa", label: "MFA", width: "minmax(80px, 0.72fr)" }, { key: "lastLogin", label: "最后登录", width: "minmax(120px, 0.9fr)", className: "muted" }, { diff --git a/src/features/users/components/UserTable.test.jsx b/src/features/users/components/UserTable.test.jsx index 7abeb72..abd9e59 100644 --- a/src/features/users/components/UserTable.test.jsx +++ b/src/features/users/components/UserTable.test.jsx @@ -14,7 +14,7 @@ const baseUser = { team: "平台" }; -test("opens user profile from avatar", async () => { +test("opens user profile from user identity", async () => { const onOpenProfile = vi.fn(); const user = userEvent.setup(); diff --git a/src/features/users/hooks/useUsersPage.js b/src/features/users/hooks/useUsersPage.js index 636fcd4..abc3420 100644 --- a/src/features/users/hooks/useUsersPage.js +++ b/src/features/users/hooks/useUsersPage.js @@ -8,13 +8,13 @@ import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; import { listRoles } from "@/features/roles/api"; import { - batchUpdateUserStatus, - createUser, - exportUsers, - listUsers, - resetUserPassword, - updateUser, - updateUserStatus + batchUpdateUserStatus, + createUser, + exportUsers, + listUsers, + resetUserPassword, + updateUser, + updateUserStatus, } from "@/features/users/api"; import { useUserAbilities } from "@/features/users/permissions.js"; import { userCreateFormSchema, userUpdateFormSchema } from "@/features/users/schema"; @@ -24,245 +24,253 @@ const emptyUsers = { items: [], total: 0, page: 1, pageSize }; const emptyData = { roles: [], users: emptyUsers }; const emptyForm = { - mfaEnabled: false, - name: "", - password: "", - roleIds: [], - status: "active", - team: "", - username: "" + mfaEnabled: false, + name: "", + password: "", + roleIds: [], + status: "active", + team: "", + username: "", }; export function useUsersPage() { - const [query, setQuery] = useState(""); - const [roleFilter, setRoleFilter] = useState(""); - const [statusFilter, setStatusFilter] = useState(""); - const [page, setPage] = useState(1); - const [selectedIds, setSelectedIds] = useState([]); - const [formOpen, setFormOpen] = useState(false); - const [editingUser, setEditingUser] = useState(null); - const [profileUser, setProfileUser] = useState(null); - const [form, setForm] = useState(emptyForm); - const abilities = useUserAbilities(); - const confirm = useConfirm(); - const { showToast } = useToast(); - const navigate = useNavigate(); + const [query, setQuery] = useState(""); + const [roleFilter, setRoleFilter] = useState(""); + const [statusFilter, setStatusFilter] = useState(""); + const [page, setPage] = useState(1); + const [selectedIds, setSelectedIds] = useState([]); + const [formOpen, setFormOpen] = useState(false); + const [editingUser, setEditingUser] = useState(null); + const [profileUser, setProfileUser] = useState(null); + const [form, setForm] = useState(emptyForm); + const abilities = useUserAbilities(); + const confirm = useConfirm(); + const { showToast } = useToast(); + const navigate = useNavigate(); - const requestQuery = useMemo( - () => toPageQuery({ keyword: query, page, pageSize, role: roleFilter, status: statusFilter }), - [page, query, roleFilter, statusFilter] - ); - - const queryFn = useCallback(async () => { - const [userData, roleData] = await Promise.all([ - listUsers(requestQuery), - abilities.canViewRoles ? listRoles() : [] - ]); - setSelectedIds([]); - return { roles: roleData || [], users: userData || emptyUsers }; - }, [abilities.canViewRoles, requestQuery]); - - const { data, error, loading, reload } = useAdminQuery(queryFn, { - errorMessage: "加载用户失败", - initialData: emptyData, - keepPreviousData: true, - queryKey: ["users", requestQuery, abilities.canViewRoles] - }); - - const users = data?.users || emptyUsers; - const roles = useMemo(() => data?.roles || [], [data?.roles]); - - const roleFilters = useMemo(() => [["", "全部角色"], ...roles.map((role) => [role.name, role.name])], [roles]); - - const stats = useMemo(() => { - const items = users.items || []; - const active = items.filter((user) => user.status === "active").length; - const locked = items.filter((user) => user.status === "locked").length; - const admin = items.filter((user) => String(user.role).includes("管理员")).length; - return { active, admin, locked }; - }, [users.items]); - - const changeQuery = (value) => { - setQuery(value); - setPage(1); - }; - - const changeRoleFilter = (value) => { - setRoleFilter(value); - setPage(1); - }; - - const changeStatusFilter = (value) => { - setStatusFilter(value); - setPage(1); - }; - - const openCreate = () => { - setEditingUser(null); - setForm(emptyForm); - setFormOpen(true); - }; - - const openEdit = (user) => { - setEditingUser(user); - setForm({ - mfaEnabled: user.mfaEnabled, - name: user.name, - password: "", - roleIds: user.roleIds || [], - status: user.status, - team: user.team || "", - username: user.account - }); - setFormOpen(true); - }; - - const closeForm = () => { - setFormOpen(false); - }; - - const openProfile = (user) => { - setProfileUser(user); - }; - - const closeProfile = () => { - setProfileUser(null); - }; - - const submitUser = async (event) => { - event.preventDefault(); - try { - if (editingUser) { - const payload = parseForm(userUpdateFormSchema, form); - await updateUser(editingUser.id, payload); - showToast("用户已更新", "success"); - } else { - const payload = parseForm(userCreateFormSchema, form); - const result = await createUser(payload); - showToast(`用户已创建,初始密码:${result.initialPassword}`, "success"); - } - setFormOpen(false); - reload(); - } catch (err) { - showToast(err.message || "保存用户失败", "error"); - } - }; - - const toggleLocked = async (user) => { - const nextStatus = user.status === "locked" ? "active" : "locked"; - const ok = await confirm({ - confirmText: user.status === "locked" ? "解锁" : "锁定", - message: `${user.name} 的登录状态会立即变更。`, - title: user.status === "locked" ? "解锁用户" : "锁定用户", - tone: user.status === "locked" ? "primary" : "danger" - }); - if (!ok) { - return; - } - await updateUserStatus(user.id, nextStatus); - showToast("用户状态已更新", "success"); - reload(); - }; - - const toggleDisabled = async (user) => { - const nextStatus = user.status === "disabled" ? "active" : "disabled"; - const ok = await confirm({ - confirmText: user.status === "disabled" ? "启用" : "停用", - message: `${user.name} 的账号状态会立即变更。`, - title: user.status === "disabled" ? "启用用户" : "停用用户", - tone: user.status === "disabled" ? "primary" : "danger" - }); - if (!ok) { - return; - } - await updateUserStatus(user.id, nextStatus); - showToast("用户状态已更新", "success"); - reload(); - }; - - const resetPassword = async (user) => { - const ok = await confirm({ - confirmText: "重置", - message: `${user.name} 的密码会重置为系统生成的初始密码。`, - title: "重置密码", - tone: "danger" - }); - if (!ok) { - return; - } - const result = await resetUserPassword(user.id); - showToast(`已重置,初始密码:${result.initialPassword}`, "success"); - }; - - const batchStatus = async (status) => { - if (!selectedIds.length) { - showToast("请先选择用户", "warning"); - return; - } - const ok = await confirm({ - confirmText: "确认", - message: `将更新 ${selectedIds.length} 个用户状态。`, - title: "批量更新状态", - tone: status === "active" ? "primary" : "danger" - }); - if (!ok) { - return; - } - await batchUpdateUserStatus(selectedIds, status); - showToast("批量状态已更新", "success"); - reload(); - }; - - const downloadUsers = () => { - return downloadCsv( - () => exportUsers({ keyword: query, role: roleFilter, status: statusFilter }), - "hyapp-users.csv" + const requestQuery = useMemo( + () => toPageQuery({ keyword: query, page, pageSize, role: roleFilter, status: statusFilter }), + [page, query, roleFilter, statusFilter], ); - }; - const setRoleChecked = (roleId, checked) => { - setForm((current) => ({ - ...current, - roleIds: checked ? [...current.roleIds, roleId] : current.roleIds.filter((id) => id !== roleId) - })); - }; + const queryFn = useCallback(async () => { + const [userData, roleData] = await Promise.all([ + listUsers(requestQuery), + abilities.canViewRoles ? listRoles() : [], + ]); + setSelectedIds([]); + return { roles: roleData || [], users: userData || emptyUsers }; + }, [abilities.canViewRoles, requestQuery]); - return { - abilities, - batchStatus, - changeQuery, - changeRoleFilter, - changeStatusFilter, - closeForm, - closeProfile, - downloadUsers, - editingUser, - error, - form, - formOpen, - loading, - navigate, - openCreate, - openEdit, - openProfile, - page, - profileUser, - query, - reload, - resetPassword, - roleFilter, - roleFilters, - roles, - selectedIds, - setForm, - setPage, - setRoleChecked, - setSelectedIds, - stats, - statusFilter, - submitUser, - toggleDisabled, - toggleLocked, - users - }; + const { data, error, loading, reload } = useAdminQuery(queryFn, { + errorMessage: "加载用户失败", + initialData: emptyData, + keepPreviousData: true, + queryKey: ["users", requestQuery, abilities.canViewRoles], + }); + + const users = data?.users || emptyUsers; + const roles = useMemo(() => data?.roles || [], [data?.roles]); + + const roleFilters = useMemo(() => [["", "全部角色"], ...roles.map((role) => [role.name, role.name])], [roles]); + + const stats = useMemo(() => { + const items = users.items || []; + const active = items.filter((user) => user.status === "active").length; + const locked = items.filter((user) => user.status === "locked").length; + const admin = items.filter((user) => String(user.role).includes("管理员")).length; + return { active, admin, locked }; + }, [users.items]); + + const changeQuery = (value) => { + setQuery(value); + setPage(1); + }; + + const changeRoleFilter = (value) => { + setRoleFilter(value); + setPage(1); + }; + + const changeStatusFilter = (value) => { + setStatusFilter(value); + setPage(1); + }; + + const resetFilters = () => { + setQuery(""); + setRoleFilter(""); + setStatusFilter(""); + setPage(1); + }; + + const openCreate = () => { + setEditingUser(null); + setForm(emptyForm); + setFormOpen(true); + }; + + const openEdit = (user) => { + setEditingUser(user); + setForm({ + mfaEnabled: user.mfaEnabled, + name: user.name, + password: "", + roleIds: user.roleIds || [], + status: user.status, + team: user.team || "", + username: user.account, + }); + setFormOpen(true); + }; + + const closeForm = () => { + setFormOpen(false); + }; + + const openProfile = (user) => { + setProfileUser(user); + }; + + const closeProfile = () => { + setProfileUser(null); + }; + + const submitUser = async (event) => { + event.preventDefault(); + try { + if (editingUser) { + const payload = parseForm(userUpdateFormSchema, form); + await updateUser(editingUser.id, payload); + showToast("用户已更新", "success"); + } else { + const payload = parseForm(userCreateFormSchema, form); + const result = await createUser(payload); + showToast(`用户已创建,初始密码:${result.initialPassword}`, "success"); + } + setFormOpen(false); + reload(); + } catch (err) { + showToast(err.message || "保存用户失败", "error"); + } + }; + + const toggleLocked = async (user) => { + const nextStatus = user.status === "locked" ? "active" : "locked"; + const ok = await confirm({ + confirmText: user.status === "locked" ? "解锁" : "锁定", + message: `${user.name} 的登录状态会立即变更。`, + title: user.status === "locked" ? "解锁用户" : "锁定用户", + tone: user.status === "locked" ? "primary" : "danger", + }); + if (!ok) { + return; + } + await updateUserStatus(user.id, nextStatus); + showToast("用户状态已更新", "success"); + reload(); + }; + + const toggleDisabled = async (user) => { + const nextStatus = user.status === "disabled" ? "active" : "disabled"; + const ok = await confirm({ + confirmText: user.status === "disabled" ? "启用" : "停用", + message: `${user.name} 的账号状态会立即变更。`, + title: user.status === "disabled" ? "启用用户" : "停用用户", + tone: user.status === "disabled" ? "primary" : "danger", + }); + if (!ok) { + return; + } + await updateUserStatus(user.id, nextStatus); + showToast("用户状态已更新", "success"); + reload(); + }; + + const resetPassword = async (user) => { + const ok = await confirm({ + confirmText: "重置", + message: `${user.name} 的密码会重置为系统生成的初始密码。`, + title: "重置密码", + tone: "danger", + }); + if (!ok) { + return; + } + const result = await resetUserPassword(user.id); + showToast(`已重置,初始密码:${result.initialPassword}`, "success"); + }; + + const batchStatus = async (status) => { + if (!selectedIds.length) { + showToast("请先选择用户", "warning"); + return; + } + const ok = await confirm({ + confirmText: "确认", + message: `将更新 ${selectedIds.length} 个用户状态。`, + title: "批量更新状态", + tone: status === "active" ? "primary" : "danger", + }); + if (!ok) { + return; + } + await batchUpdateUserStatus(selectedIds, status); + showToast("批量状态已更新", "success"); + reload(); + }; + + const downloadUsers = () => { + return downloadCsv( + () => exportUsers({ keyword: query, role: roleFilter, status: statusFilter }), + "hyapp-users.csv", + ); + }; + + const setRoleChecked = (roleId, checked) => { + setForm((current) => ({ + ...current, + roleIds: checked ? [...current.roleIds, roleId] : current.roleIds.filter((id) => id !== roleId), + })); + }; + + return { + abilities, + batchStatus, + changeQuery, + changeRoleFilter, + changeStatusFilter, + closeForm, + closeProfile, + downloadUsers, + editingUser, + error, + form, + formOpen, + loading, + navigate, + openCreate, + openEdit, + openProfile, + page, + profileUser, + query, + reload, + resetPassword, + resetFilters, + roleFilter, + roleFilters, + roles, + selectedIds, + setForm, + setPage, + setRoleChecked, + setSelectedIds, + stats, + statusFilter, + submitUser, + toggleDisabled, + toggleLocked, + users, + }; } diff --git a/src/features/users/pages/UserManagementPage.jsx b/src/features/users/pages/UserManagementPage.jsx index 871e34b..1290734 100644 --- a/src/features/users/pages/UserManagementPage.jsx +++ b/src/features/users/pages/UserManagementPage.jsx @@ -6,12 +6,11 @@ import VerifiedUserOutlined from "@mui/icons-material/VerifiedUserOutlined"; import { DataState } from "@/shared/ui/DataState.jsx"; import { PaginationBar } from "@/shared/ui/PaginationBar.jsx"; import { - AdminActionIconButton, - AdminFilterSelect, - AdminListBody, - AdminListPage, - AdminListToolbar, - AdminSearchBox + AdminActionIconButton, + AdminFilterResetButton, + AdminListBody, + AdminListPage, + AdminListToolbar, } from "@/shared/ui/AdminListLayout.jsx"; import { UserFormDrawer } from "@/features/users/components/UserFormDrawer.jsx"; import { UserProfileDrawer } from "@/features/users/components/UserProfileDrawer.jsx"; @@ -20,85 +19,97 @@ import { useUsersPage } from "@/features/users/hooks/useUsersPage.js"; import { userStatusFilters } from "@/features/users/constants.js"; export function UserManagementPage() { - const page = useUsersPage(); + const page = useUsersPage(); - return ( - <> - - - - - - - )} - actions={( - <> - {page.abilities.canViewRoles ? ( - page.navigate("/system/roles")}> - - - ) : null} - {page.abilities.canStatus ? ( - <> - page.batchStatus("active")}> - - - page.batchStatus("disabled")}> - - - - ) : null} - {page.abilities.canExport ? ( - - - - ) : null} - {page.abilities.canCreate ? ( - - - - ) : null} - - )} - /> + return ( + <> + + + } + actions={ + <> + {page.abilities.canViewRoles ? ( + page.navigate("/system/roles")}> + + + ) : null} + {page.abilities.canStatus ? ( + <> + page.batchStatus("active")}> + + + page.batchStatus("disabled")} + > + + + + ) : null} + {page.abilities.canExport ? ( + + + + ) : null} + {page.abilities.canCreate ? ( + + + + ) : null} + + } + /> - - - + + + + + + + + - - - - - - - - - ); + + + ); } diff --git a/src/features/users/routes.js b/src/features/users/routes.js index 1a56fdd..ae8d36b 100644 --- a/src/features/users/routes.js +++ b/src/features/users/routes.js @@ -2,7 +2,7 @@ import { MENU_CODES, PERMISSIONS } from "@/app/permissions"; export const usersRoutes = [ { - label: "用户管理", + label: "后台用户", loader: () => import("./pages/UserManagementPage.jsx").then((module) => module.UserManagementPage), menuCode: MENU_CODES.systemUsers, pageKey: "users", diff --git a/src/features/users/users.css b/src/features/users/users.css index 543d642..3e55ca8 100644 --- a/src/features/users/users.css +++ b/src/features/users/users.css @@ -19,6 +19,59 @@ gap: var(--space-3); } +.user-main-button { + display: block; + width: 100%; + padding: 2px; + border: 0; + border-radius: var(--radius-control); + background: transparent; + color: inherit; + text-align: left; + cursor: pointer; + transition: + background var(--motion-base) var(--ease-standard), + box-shadow var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.user-main-button:hover { + background: var(--bg-hover); +} + +.user-main-button:hover .service-name { + color: var(--primary); +} + +.user-main-button:focus-visible { + outline: 2px solid var(--primary-border-strong); + outline-offset: 2px; +} + +.user-main__text { + display: grid; + min-width: 0; + gap: 2px; +} + +.user-main__text .service-name, +.user-main__text .service-desc { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.user-main__text .service-name { + color: var(--text-primary); + font-weight: 700; +} + +.user-main__text .service-desc { + color: var(--text-secondary); + font-size: 12px; +} + .user-avatar { display: grid; flex: 0 0 auto; @@ -33,24 +86,6 @@ transform var(--motion-base) var(--ease-emphasized); } -.user-avatar-button { - display: grid; - flex: 0 0 auto; - width: 34px; - height: 34px; - padding: 0; - border: 0; - border-radius: var(--radius-control); - background: transparent; - color: inherit; - cursor: pointer; -} - -.user-avatar-button:focus-visible { - outline: 2px solid var(--primary-border-strong); - outline-offset: 2px; -} - .admin-row:hover .user-avatar { background: var(--primary-surface-strong); transform: scale(1.08); diff --git a/src/shared/api/generated/endpoints.ts b/src/shared/api/generated/endpoints.ts index 9873f05..45f5c31 100644 --- a/src/shared/api/generated/endpoints.ts +++ b/src/shared/api/generated/endpoints.ts @@ -5,935 +5,943 @@ import type { PermissionCode } from "@/app/permissions"; export type ApiHttpMethod = "DELETE" | "GET" | "PATCH" | "POST" | "PUT"; export interface ApiEndpoint { - method: ApiHttpMethod; - operationId: string; - path: string; - permission?: PermissionCode; - permissions?: PermissionCode[]; + method: ApiHttpMethod; + operationId: string; + path: string; + permission?: PermissionCode; + permissions?: PermissionCode[]; } export const API_OPERATIONS = { - appBanUser: "appBanUser", - appGetUser: "appGetUser", - appListUsers: "appListUsers", - appSetPassword: "appSetPassword", - appUnbanUser: "appUnbanUser", - appUpdateUser: "appUpdateUser", - batchUpdateUserStatus: "batchUpdateUserStatus", - cancelJob: "cancelJob", - changePassword: "changePassword", - closeAgency: "closeAgency", - createAgency: "createAgency", - createBanner: "createBanner", - createBD: "createBD", - createBDLeader: "createBDLeader", - createCoinSeller: "createCoinSeller", - createCountry: "createCountry", - createGift: "createGift", - createMenu: "createMenu", - createPermission: "createPermission", - createRegion: "createRegion", - createResource: "createResource", - createResourceGroup: "createResourceGroup", - createRole: "createRole", - createUser: "createUser", - createUserExportJob: "createUserExportJob", - dashboardOverview: "dashboardOverview", - deleteBanner: "deleteBanner", - deleteCountry: "deleteCountry", - deleteMenu: "deleteMenu", - deleteNotification: "deleteNotification", - deletePermission: "deletePermission", - deleteRole: "deleteRole", - deleteRoom: "deleteRoom", - disableCountry: "disableCountry", - disableGift: "disableGift", - disableRegion: "disableRegion", - disableResource: "disableResource", - disableResourceGroup: "disableResourceGroup", - downloadJobArtifact: "downloadJobArtifact", - enableCountry: "enableCountry", - enableGift: "enableGift", - enableRegion: "enableRegion", - enableResource: "enableResource", - enableResourceGroup: "enableResourceGroup", - exportLoginLogs: "exportLoginLogs", - exportOperationLogs: "exportOperationLogs", - exportUsers: "exportUsers", - getCountry: "getCountry", - getRegion: "getRegion", - getResource: "getResource", - getResourceGroup: "getResourceGroup", - getRoleDataScopes: "getRoleDataScopes", - getUser: "getUser", - grantResource: "grantResource", - grantResourceGroup: "grantResourceGroup", - listAgencies: "listAgencies", - listApps: "listApps", - listBanners: "listBanners", - listBDLeaders: "listBDLeaders", - listBDs: "listBDs", - listCoinSellers: "listCoinSellers", - listCountries: "listCountries", - listGifts: "listGifts", - listH5Links: "listH5Links", - listHosts: "listHosts", - listJobs: "listJobs", - listLoginLogs: "listLoginLogs", - listNotifications: "listNotifications", - listOperationLogs: "listOperationLogs", - listPermissions: "listPermissions", - listRechargeBills: "listRechargeBills", - listRegions: "listRegions", - listResourceGrants: "listResourceGrants", - listResourceGroups: "listResourceGroups", - listResources: "listResources", - listRoles: "listRoles", - listRooms: "listRooms", - listSystemMenus: "listSystemMenus", - listUsers: "listUsers", - login: "login", - logout: "logout", - markAllNotificationsRead: "markAllNotificationsRead", - markNotificationRead: "markNotificationRead", - me: "me", - navigationMenus: "navigationMenus", - refresh: "refresh", - replaceRegionCountries: "replaceRegionCountries", - replaceRoleDataScopes: "replaceRoleDataScopes", - replaceRolePermissions: "replaceRolePermissions", - resetUserPassword: "resetUserPassword", - search: "search", - setAgencyJoinEnabled: "setAgencyJoinEnabled", - setBDLeaderStatus: "setBDLeaderStatus", - setBDStatus: "setBDStatus", - setCoinSellerStatus: "setCoinSellerStatus", - sortMenus: "sortMenus", - syncPermissions: "syncPermissions", - updateBanner: "updateBanner", - updateCountry: "updateCountry", - updateGift: "updateGift", - updateH5Links: "updateH5Links", - updateMenu: "updateMenu", - updateMenuVisible: "updateMenuVisible", - updatePermission: "updatePermission", - updateRegion: "updateRegion", - updateResource: "updateResource", - updateResourceGroup: "updateResourceGroup", - updateResourceGroupItems: "updateResourceGroupItems", - updateRole: "updateRole", - updateRoom: "updateRoom", - updateUser: "updateUser", - updateUserStatus: "updateUserStatus", - uploadFile: "uploadFile", - uploadImage: "uploadImage" + appBanUser: "appBanUser", + appGetUser: "appGetUser", + appListUsers: "appListUsers", + appSetPassword: "appSetPassword", + appUnbanUser: "appUnbanUser", + appUpdateUser: "appUpdateUser", + batchUpdateUserStatus: "batchUpdateUserStatus", + cancelJob: "cancelJob", + changePassword: "changePassword", + closeAgency: "closeAgency", + createAgency: "createAgency", + createBanner: "createBanner", + createBD: "createBD", + createBDLeader: "createBDLeader", + createCoinSeller: "createCoinSeller", + createCountry: "createCountry", + createGift: "createGift", + createMenu: "createMenu", + createPermission: "createPermission", + createRegion: "createRegion", + createResource: "createResource", + createResourceGroup: "createResourceGroup", + createRole: "createRole", + createUser: "createUser", + createUserExportJob: "createUserExportJob", + creditCoinSellerStock: "creditCoinSellerStock", + dashboardOverview: "dashboardOverview", + deleteBanner: "deleteBanner", + deleteCountry: "deleteCountry", + deleteMenu: "deleteMenu", + deleteNotification: "deleteNotification", + deletePermission: "deletePermission", + deleteRole: "deleteRole", + deleteRoom: "deleteRoom", + disableCountry: "disableCountry", + disableGift: "disableGift", + disableRegion: "disableRegion", + disableResource: "disableResource", + disableResourceGroup: "disableResourceGroup", + downloadJobArtifact: "downloadJobArtifact", + enableCountry: "enableCountry", + enableGift: "enableGift", + enableRegion: "enableRegion", + enableResource: "enableResource", + enableResourceGroup: "enableResourceGroup", + exportLoginLogs: "exportLoginLogs", + exportOperationLogs: "exportOperationLogs", + exportUsers: "exportUsers", + getCountry: "getCountry", + getRegion: "getRegion", + getResource: "getResource", + getResourceGroup: "getResourceGroup", + getRoleDataScopes: "getRoleDataScopes", + getUser: "getUser", + grantResource: "grantResource", + grantResourceGroup: "grantResourceGroup", + listAgencies: "listAgencies", + listApps: "listApps", + listBanners: "listBanners", + listBDLeaders: "listBDLeaders", + listBDs: "listBDs", + listCoinSellers: "listCoinSellers", + listCountries: "listCountries", + listGifts: "listGifts", + listH5Links: "listH5Links", + listHosts: "listHosts", + listJobs: "listJobs", + listLoginLogs: "listLoginLogs", + listNotifications: "listNotifications", + listOperationLogs: "listOperationLogs", + listPermissions: "listPermissions", + listRechargeBills: "listRechargeBills", + listRegions: "listRegions", + listResourceGrants: "listResourceGrants", + listResourceGroups: "listResourceGroups", + listResources: "listResources", + listRoles: "listRoles", + listRooms: "listRooms", + listSystemMenus: "listSystemMenus", + listUsers: "listUsers", + login: "login", + logout: "logout", + markAllNotificationsRead: "markAllNotificationsRead", + markNotificationRead: "markNotificationRead", + me: "me", + navigationMenus: "navigationMenus", + refresh: "refresh", + replaceRegionCountries: "replaceRegionCountries", + replaceRoleDataScopes: "replaceRoleDataScopes", + replaceRolePermissions: "replaceRolePermissions", + resetUserPassword: "resetUserPassword", + search: "search", + setAgencyJoinEnabled: "setAgencyJoinEnabled", + setBDLeaderStatus: "setBDLeaderStatus", + setBDStatus: "setBDStatus", + setCoinSellerStatus: "setCoinSellerStatus", + sortMenus: "sortMenus", + syncPermissions: "syncPermissions", + updateBanner: "updateBanner", + updateCountry: "updateCountry", + updateGift: "updateGift", + updateH5Links: "updateH5Links", + updateMenu: "updateMenu", + updateMenuVisible: "updateMenuVisible", + updatePermission: "updatePermission", + updateRegion: "updateRegion", + updateResource: "updateResource", + updateResourceGroup: "updateResourceGroup", + updateResourceGroupItems: "updateResourceGroupItems", + updateRole: "updateRole", + updateRoom: "updateRoom", + updateUser: "updateUser", + updateUserStatus: "updateUserStatus", + uploadFile: "uploadFile", + uploadImage: "uploadImage", } as const; export type ApiOperationId = (typeof API_OPERATIONS)[keyof typeof API_OPERATIONS]; export const API_ENDPOINTS: Record = { - appBanUser: { - method: "POST", - operationId: API_OPERATIONS.appBanUser, - path: "/v1/app/users/{id}/ban", - permission: "app-user:status", - permissions: ["app-user:status"] - }, - appGetUser: { - method: "GET", - operationId: API_OPERATIONS.appGetUser, - path: "/v1/app/users/{id}", - permission: "app-user:view", - permissions: ["app-user:view"] - }, - appListUsers: { - method: "GET", - operationId: API_OPERATIONS.appListUsers, - path: "/v1/app/users", - permission: "app-user:view", - permissions: ["app-user:view"] - }, - appSetPassword: { - method: "POST", - operationId: API_OPERATIONS.appSetPassword, - path: "/v1/app/users/{id}/password", - permission: "app-user:password", - permissions: ["app-user:password"] - }, - appUnbanUser: { - method: "POST", - operationId: API_OPERATIONS.appUnbanUser, - path: "/v1/app/users/{id}/unban", - permission: "app-user:status", - permissions: ["app-user:status"] - }, - appUpdateUser: { - method: "PATCH", - operationId: API_OPERATIONS.appUpdateUser, - path: "/v1/app/users/{id}", - permission: "app-user:update", - permissions: ["app-user:update"] - }, - batchUpdateUserStatus: { - method: "POST", - operationId: API_OPERATIONS.batchUpdateUserStatus, - path: "/v1/users/batch/status", - permission: "user:status", - permissions: ["user:status"] - }, - cancelJob: { - method: "POST", - operationId: API_OPERATIONS.cancelJob, - path: "/v1/jobs/{id}/cancel", - permission: "job:cancel", - permissions: ["job:cancel"] - }, - changePassword: { - method: "POST", - operationId: API_OPERATIONS.changePassword, - path: "/v1/auth/change-password" - }, - closeAgency: { - method: "POST", - operationId: API_OPERATIONS.closeAgency, - path: "/v1/admin/agencies/{agency_id}/close", - permission: "agency:status", - permissions: ["agency:status"] - }, - createAgency: { - method: "POST", - operationId: API_OPERATIONS.createAgency, - path: "/v1/admin/agencies", - permission: "agency:create", - permissions: ["agency:create"] - }, - createBanner: { - method: "POST", - operationId: API_OPERATIONS.createBanner, - path: "/v1/admin/app-config/banners", - permission: "app-config:update", - permissions: ["app-config:update"] - }, - createBD: { - method: "POST", - operationId: API_OPERATIONS.createBD, - path: "/v1/admin/bds", - permission: "bd:create", - permissions: ["bd:create"] - }, - createBDLeader: { - method: "POST", - operationId: API_OPERATIONS.createBDLeader, - path: "/v1/admin/bd-leaders", - permission: "bd:create", - permissions: ["bd:create"] - }, - createCoinSeller: { - method: "POST", - operationId: API_OPERATIONS.createCoinSeller, - path: "/v1/admin/coin-sellers", - permission: "coin-seller:create", - permissions: ["coin-seller:create"] - }, - createCountry: { - method: "POST", - operationId: API_OPERATIONS.createCountry, - path: "/v1/admin/countries", - permission: "country:create", - permissions: ["country:create"] - }, - createGift: { - method: "POST", - operationId: API_OPERATIONS.createGift, - path: "/v1/admin/gifts", - permission: "gift:create", - permissions: ["gift:create"] - }, - createMenu: { - method: "POST", - operationId: API_OPERATIONS.createMenu, - path: "/v1/system/menus", - permission: "menu:create", - permissions: ["menu:create"] - }, - createPermission: { - method: "POST", - operationId: API_OPERATIONS.createPermission, - path: "/v1/permissions", - permission: "permission:create", - permissions: ["permission:create"] - }, - createRegion: { - method: "POST", - operationId: API_OPERATIONS.createRegion, - path: "/v1/admin/regions", - permission: "region:create", - permissions: ["region:create"] - }, - createResource: { - method: "POST", - operationId: API_OPERATIONS.createResource, - path: "/v1/admin/resources", - permission: "resource:create", - permissions: ["resource:create"] - }, - createResourceGroup: { - method: "POST", - operationId: API_OPERATIONS.createResourceGroup, - path: "/v1/admin/resource-groups", - permission: "resource-group:create", - permissions: ["resource-group:create"] - }, - createRole: { - method: "POST", - operationId: API_OPERATIONS.createRole, - path: "/v1/roles", - permission: "role:create", - permissions: ["role:create","role:manage"] - }, - createUser: { - method: "POST", - operationId: API_OPERATIONS.createUser, - path: "/v1/users", - permission: "user:create", - permissions: ["user:create"] - }, - createUserExportJob: { - method: "POST", - operationId: API_OPERATIONS.createUserExportJob, - path: "/v1/exports/users", - permission: "export:create", - permissions: ["export:create"] - }, - dashboardOverview: { - method: "GET", - operationId: API_OPERATIONS.dashboardOverview, - path: "/v1/dashboard/overview", - permission: "overview:view", - permissions: ["overview:view"] - }, - deleteBanner: { - method: "DELETE", - operationId: API_OPERATIONS.deleteBanner, - path: "/v1/admin/app-config/banners/{banner_id}", - permission: "app-config:update", - permissions: ["app-config:update"] - }, - deleteCountry: { - method: "DELETE", - operationId: API_OPERATIONS.deleteCountry, - path: "/v1/admin/countries/{country_id}", - permission: "country:status", - permissions: ["country:status"] - }, - deleteMenu: { - method: "DELETE", - operationId: API_OPERATIONS.deleteMenu, - path: "/v1/system/menus/{id}", - permission: "menu:delete", - permissions: ["menu:delete"] - }, - deleteNotification: { - method: "DELETE", - operationId: API_OPERATIONS.deleteNotification, - path: "/v1/notifications/{id}", - permission: "notification:delete", - permissions: ["notification:delete"] - }, - deletePermission: { - method: "DELETE", - operationId: API_OPERATIONS.deletePermission, - path: "/v1/permissions/{id}", - permission: "permission:delete", - permissions: ["permission:delete"] - }, - deleteRole: { - method: "DELETE", - operationId: API_OPERATIONS.deleteRole, - path: "/v1/roles/{id}", - permission: "role:delete", - permissions: ["role:delete","role:manage"] - }, - deleteRoom: { - method: "DELETE", - operationId: API_OPERATIONS.deleteRoom, - path: "/v1/admin/rooms/{room_id}", - permission: "room:delete", - permissions: ["room:delete"] - }, - disableCountry: { - method: "POST", - operationId: API_OPERATIONS.disableCountry, - path: "/v1/admin/countries/{country_id}/disable", - permission: "country:status", - permissions: ["country:status"] - }, - disableGift: { - method: "POST", - operationId: API_OPERATIONS.disableGift, - path: "/v1/admin/gifts/{gift_id}/disable", - permission: "gift:status", - permissions: ["gift:status"] - }, - disableRegion: { - method: "DELETE", - operationId: API_OPERATIONS.disableRegion, - path: "/v1/admin/regions/{region_id}", - permission: "region:status", - permissions: ["region:status"] - }, - disableResource: { - method: "POST", - operationId: API_OPERATIONS.disableResource, - path: "/v1/admin/resources/{resource_id}/disable", - permission: "resource:update", - permissions: ["resource:update"] - }, - disableResourceGroup: { - method: "POST", - operationId: API_OPERATIONS.disableResourceGroup, - path: "/v1/admin/resource-groups/{group_id}/disable", - permission: "resource-group:update", - permissions: ["resource-group:update"] - }, - downloadJobArtifact: { - method: "GET", - operationId: API_OPERATIONS.downloadJobArtifact, - path: "/v1/jobs/{id}/artifact", - permission: "job:view", - permissions: ["job:view"] - }, - enableCountry: { - method: "POST", - operationId: API_OPERATIONS.enableCountry, - path: "/v1/admin/countries/{country_id}/enable", - permission: "country:status", - permissions: ["country:status"] - }, - enableGift: { - method: "POST", - operationId: API_OPERATIONS.enableGift, - path: "/v1/admin/gifts/{gift_id}/enable", - permission: "gift:status", - permissions: ["gift:status"] - }, - enableRegion: { - method: "POST", - operationId: API_OPERATIONS.enableRegion, - path: "/v1/admin/regions/{region_id}/enable", - permission: "region:status", - permissions: ["region:status"] - }, - enableResource: { - method: "POST", - operationId: API_OPERATIONS.enableResource, - path: "/v1/admin/resources/{resource_id}/enable", - permission: "resource:update", - permissions: ["resource:update"] - }, - enableResourceGroup: { - method: "POST", - operationId: API_OPERATIONS.enableResourceGroup, - path: "/v1/admin/resource-groups/{group_id}/enable", - permission: "resource-group:update", - permissions: ["resource-group:update"] - }, - exportLoginLogs: { - method: "GET", - operationId: API_OPERATIONS.exportLoginLogs, - path: "/v1/logs/login/export", - permission: "log:export", - permissions: ["log:export"] - }, - exportOperationLogs: { - method: "GET", - operationId: API_OPERATIONS.exportOperationLogs, - path: "/v1/logs/operations/export", - permission: "log:export", - permissions: ["log:export"] - }, - exportUsers: { - method: "GET", - operationId: API_OPERATIONS.exportUsers, - path: "/v1/users/export", - permission: "user:export", - permissions: ["user:export"] - }, - getCountry: { - method: "GET", - operationId: API_OPERATIONS.getCountry, - path: "/v1/admin/countries/{country_id}", - permission: "country:view", - permissions: ["country:view"] - }, - getRegion: { - method: "GET", - operationId: API_OPERATIONS.getRegion, - path: "/v1/admin/regions/{region_id}", - permission: "region:view", - permissions: ["region:view"] - }, - getResource: { - method: "GET", - operationId: API_OPERATIONS.getResource, - path: "/v1/admin/resources/{resource_id}", - permission: "resource:view", - permissions: ["resource:view"] - }, - getResourceGroup: { - method: "GET", - operationId: API_OPERATIONS.getResourceGroup, - path: "/v1/admin/resource-groups/{group_id}", - permission: "resource-group:view", - permissions: ["resource-group:view"] - }, - getRoleDataScopes: { - method: "GET", - operationId: API_OPERATIONS.getRoleDataScopes, - path: "/v1/roles/{id}/data-scopes", - permission: "role:data-scope", - permissions: ["role:data-scope"] - }, - getUser: { - method: "GET", - operationId: API_OPERATIONS.getUser, - path: "/v1/users/{id}", - permission: "user:view", - permissions: ["user:view"] - }, - grantResource: { - method: "POST", - operationId: API_OPERATIONS.grantResource, - path: "/v1/admin/resource-grants/resource", - permission: "resource-grant:create", - permissions: ["resource-grant:create"] - }, - grantResourceGroup: { - method: "POST", - operationId: API_OPERATIONS.grantResourceGroup, - path: "/v1/admin/resource-grants/group", - permission: "resource-grant:create", - permissions: ["resource-grant:create"] - }, - listAgencies: { - method: "GET", - operationId: API_OPERATIONS.listAgencies, - path: "/v1/admin/agencies", - permission: "agency:view", - permissions: ["agency:view"] - }, - listApps: { - method: "GET", - operationId: API_OPERATIONS.listApps, - path: "/v1/admin/apps" - }, - listBanners: { - method: "GET", - operationId: API_OPERATIONS.listBanners, - path: "/v1/admin/app-config/banners", - permission: "app-config:view", - permissions: ["app-config:view"] - }, - listBDLeaders: { - method: "GET", - operationId: API_OPERATIONS.listBDLeaders, - path: "/v1/admin/bd-leaders", - permission: "bd:view", - permissions: ["bd:view"] - }, - listBDs: { - method: "GET", - operationId: API_OPERATIONS.listBDs, - path: "/v1/admin/bds", - permission: "bd:view", - permissions: ["bd:view"] - }, - listCoinSellers: { - method: "GET", - operationId: API_OPERATIONS.listCoinSellers, - path: "/v1/admin/coin-sellers", - permission: "coin-seller:view", - permissions: ["coin-seller:view"] - }, - listCountries: { - method: "GET", - operationId: API_OPERATIONS.listCountries, - path: "/v1/admin/countries", - permission: "country:view", - permissions: ["country:view"] - }, - listGifts: { - method: "GET", - operationId: API_OPERATIONS.listGifts, - path: "/v1/admin/gifts", - permission: "gift:view", - permissions: ["gift:view"] - }, - listH5Links: { - method: "GET", - operationId: API_OPERATIONS.listH5Links, - path: "/v1/admin/app-config/h5-links", - permission: "app-config:view", - permissions: ["app-config:view"] - }, - listHosts: { - method: "GET", - operationId: API_OPERATIONS.listHosts, - path: "/v1/admin/hosts", - permission: "host:view", - permissions: ["host:view"] - }, - listJobs: { - method: "GET", - operationId: API_OPERATIONS.listJobs, - path: "/v1/jobs", - permission: "job:view", - permissions: ["job:view"] - }, - listLoginLogs: { - method: "GET", - operationId: API_OPERATIONS.listLoginLogs, - path: "/v1/logs/login", - permission: "log:view", - permissions: ["log:view"] - }, - listNotifications: { - method: "GET", - operationId: API_OPERATIONS.listNotifications, - path: "/v1/notifications", - permission: "notification:view", - permissions: ["notification:view"] - }, - listOperationLogs: { - method: "GET", - operationId: API_OPERATIONS.listOperationLogs, - path: "/v1/logs/operations", - permission: "log:view", - permissions: ["log:view"] - }, - listPermissions: { - method: "GET", - operationId: API_OPERATIONS.listPermissions, - path: "/v1/permissions", - permission: "permission:view", - permissions: ["permission:view","role:permission","role:manage"] - }, - listRechargeBills: { - method: "GET", - operationId: API_OPERATIONS.listRechargeBills, - path: "/v1/admin/payment/recharge-bills", - permission: "payment-bill:view", - permissions: ["payment-bill:view"] - }, - listRegions: { - method: "GET", - operationId: API_OPERATIONS.listRegions, - path: "/v1/admin/regions", - permission: "region:view", - permissions: ["region:view"] - }, - listResourceGrants: { - method: "GET", - operationId: API_OPERATIONS.listResourceGrants, - path: "/v1/admin/resource-grants", - permission: "resource-grant:view", - permissions: ["resource-grant:view"] - }, - listResourceGroups: { - method: "GET", - operationId: API_OPERATIONS.listResourceGroups, - path: "/v1/admin/resource-groups", - permission: "resource-group:view", - permissions: ["resource-group:view"] - }, - listResources: { - method: "GET", - operationId: API_OPERATIONS.listResources, - path: "/v1/admin/resources", - permission: "resource:view", - permissions: ["resource:view"] - }, - listRoles: { - method: "GET", - operationId: API_OPERATIONS.listRoles, - path: "/v1/roles", - permission: "role:view", - permissions: ["role:view"] - }, - listRooms: { - method: "GET", - operationId: API_OPERATIONS.listRooms, - path: "/v1/admin/rooms", - permission: "room:view", - permissions: ["room:view"] - }, - listSystemMenus: { - method: "GET", - operationId: API_OPERATIONS.listSystemMenus, - path: "/v1/system/menus", - permission: "menu:view", - permissions: ["menu:view"] - }, - listUsers: { - method: "GET", - operationId: API_OPERATIONS.listUsers, - path: "/v1/users", - permission: "user:view", - permissions: ["user:view"] - }, - login: { - method: "POST", - operationId: API_OPERATIONS.login, - path: "/v1/auth/login" - }, - logout: { - method: "POST", - operationId: API_OPERATIONS.logout, - path: "/v1/auth/logout" - }, - markAllNotificationsRead: { - method: "PATCH", - operationId: API_OPERATIONS.markAllNotificationsRead, - path: "/v1/notifications/read-all", - permission: "notification:read-all", - permissions: ["notification:read-all"] - }, - markNotificationRead: { - method: "PATCH", - operationId: API_OPERATIONS.markNotificationRead, - path: "/v1/notifications/{id}/read", - permission: "notification:read", - permissions: ["notification:read"] - }, - me: { - method: "GET", - operationId: API_OPERATIONS.me, - path: "/v1/auth/me" - }, - navigationMenus: { - method: "GET", - operationId: API_OPERATIONS.navigationMenus, - path: "/v1/navigation/menus" - }, - refresh: { - method: "POST", - operationId: API_OPERATIONS.refresh, - path: "/v1/auth/refresh" - }, - replaceRegionCountries: { - method: "PUT", - operationId: API_OPERATIONS.replaceRegionCountries, - path: "/v1/admin/regions/{region_id}/countries", - permission: "region:update", - permissions: ["region:update"] - }, - replaceRoleDataScopes: { - method: "PUT", - operationId: API_OPERATIONS.replaceRoleDataScopes, - path: "/v1/roles/{id}/data-scopes", - permission: "role:data-scope", - permissions: ["role:data-scope"] - }, - replaceRolePermissions: { - method: "PUT", - operationId: API_OPERATIONS.replaceRolePermissions, - path: "/v1/roles/{id}/permissions", - permission: "role:permission", - permissions: ["role:permission","role:manage"] - }, - resetUserPassword: { - method: "POST", - operationId: API_OPERATIONS.resetUserPassword, - path: "/v1/users/{id}/reset-password", - permission: "user:reset-password", - permissions: ["user:reset-password"] - }, - search: { - method: "GET", - operationId: API_OPERATIONS.search, - path: "/v1/search" - }, - setAgencyJoinEnabled: { - method: "POST", - operationId: API_OPERATIONS.setAgencyJoinEnabled, - path: "/v1/admin/agencies/{agency_id}/join-enabled", - permission: "agency:status", - permissions: ["agency:status"] - }, - setBDLeaderStatus: { - method: "PATCH", - operationId: API_OPERATIONS.setBDLeaderStatus, - path: "/v1/admin/bd-leaders/{user_id}/status", - permission: "bd:update", - permissions: ["bd:update"] - }, - setBDStatus: { - method: "PATCH", - operationId: API_OPERATIONS.setBDStatus, - path: "/v1/admin/bds/{user_id}/status", - permission: "bd:update", - permissions: ["bd:update"] - }, - setCoinSellerStatus: { - method: "PATCH", - operationId: API_OPERATIONS.setCoinSellerStatus, - path: "/v1/admin/coin-sellers/{user_id}/status", - permission: "coin-seller:update", - permissions: ["coin-seller:update"] - }, - sortMenus: { - method: "PUT", - operationId: API_OPERATIONS.sortMenus, - path: "/v1/system/menus/sort", - permission: "menu:sort", - permissions: ["menu:sort"] - }, - syncPermissions: { - method: "POST", - operationId: API_OPERATIONS.syncPermissions, - path: "/v1/permissions/sync", - permission: "permission:sync", - permissions: ["permission:sync"] - }, - updateBanner: { - method: "PUT", - operationId: API_OPERATIONS.updateBanner, - path: "/v1/admin/app-config/banners/{banner_id}", - permission: "app-config:update", - permissions: ["app-config:update"] - }, - updateCountry: { - method: "PATCH", - operationId: API_OPERATIONS.updateCountry, - path: "/v1/admin/countries/{country_id}", - permission: "country:update", - permissions: ["country:update"] - }, - updateGift: { - method: "PUT", - operationId: API_OPERATIONS.updateGift, - path: "/v1/admin/gifts/{gift_id}", - permission: "gift:update", - permissions: ["gift:update"] - }, - updateH5Links: { - method: "PUT", - operationId: API_OPERATIONS.updateH5Links, - path: "/v1/admin/app-config/h5-links", - permission: "app-config:update", - permissions: ["app-config:update"] - }, - updateMenu: { - method: "PATCH", - operationId: API_OPERATIONS.updateMenu, - path: "/v1/system/menus/{id}", - permission: "menu:update", - permissions: ["menu:update"] - }, - updateMenuVisible: { - method: "PATCH", - operationId: API_OPERATIONS.updateMenuVisible, - path: "/v1/system/menus/{id}/visible", - permission: "menu:visible", - permissions: ["menu:visible"] - }, - updatePermission: { - method: "PATCH", - operationId: API_OPERATIONS.updatePermission, - path: "/v1/permissions/{id}", - permission: "permission:update", - permissions: ["permission:update"] - }, - updateRegion: { - method: "PATCH", - operationId: API_OPERATIONS.updateRegion, - path: "/v1/admin/regions/{region_id}", - permission: "region:update", - permissions: ["region:update"] - }, - updateResource: { - method: "PUT", - operationId: API_OPERATIONS.updateResource, - path: "/v1/admin/resources/{resource_id}", - permission: "resource:update", - permissions: ["resource:update"] - }, - updateResourceGroup: { - method: "PUT", - operationId: API_OPERATIONS.updateResourceGroup, - path: "/v1/admin/resource-groups/{group_id}", - permission: "resource-group:update", - permissions: ["resource-group:update"] - }, - updateResourceGroupItems: { - method: "PUT", - operationId: API_OPERATIONS.updateResourceGroupItems, - path: "/v1/admin/resource-groups/{group_id}/items", - permission: "resource-group:update", - permissions: ["resource-group:update"] - }, - updateRole: { - method: "PATCH", - operationId: API_OPERATIONS.updateRole, - path: "/v1/roles/{id}", - permission: "role:update", - permissions: ["role:update","role:manage"] - }, - updateRoom: { - method: "PATCH", - operationId: API_OPERATIONS.updateRoom, - path: "/v1/admin/rooms/{room_id}", - permission: "room:update", - permissions: ["room:update"] - }, - updateUser: { - method: "PATCH", - operationId: API_OPERATIONS.updateUser, - path: "/v1/users/{id}", - permission: "user:update", - permissions: ["user:update"] - }, - updateUserStatus: { - method: "PATCH", - operationId: API_OPERATIONS.updateUserStatus, - path: "/v1/users/{id}/status", - permission: "user:status", - permissions: ["user:status"] - }, - uploadFile: { - method: "POST", - operationId: API_OPERATIONS.uploadFile, - path: "/v1/admin/files/upload", - permission: "upload:create", - permissions: ["upload:create"] - }, - uploadImage: { - method: "POST", - operationId: API_OPERATIONS.uploadImage, - path: "/v1/admin/files/image/upload", - permission: "upload:create", - permissions: ["upload:create"] - } + appBanUser: { + method: "POST", + operationId: API_OPERATIONS.appBanUser, + path: "/v1/app/users/{id}/ban", + permission: "app-user:status", + permissions: ["app-user:status"], + }, + appGetUser: { + method: "GET", + operationId: API_OPERATIONS.appGetUser, + path: "/v1/app/users/{id}", + permission: "app-user:view", + permissions: ["app-user:view"], + }, + appListUsers: { + method: "GET", + operationId: API_OPERATIONS.appListUsers, + path: "/v1/app/users", + permission: "app-user:view", + permissions: ["app-user:view"], + }, + appSetPassword: { + method: "POST", + operationId: API_OPERATIONS.appSetPassword, + path: "/v1/app/users/{id}/password", + permission: "app-user:password", + permissions: ["app-user:password"], + }, + appUnbanUser: { + method: "POST", + operationId: API_OPERATIONS.appUnbanUser, + path: "/v1/app/users/{id}/unban", + permission: "app-user:status", + permissions: ["app-user:status"], + }, + appUpdateUser: { + method: "PATCH", + operationId: API_OPERATIONS.appUpdateUser, + path: "/v1/app/users/{id}", + permission: "app-user:update", + permissions: ["app-user:update"], + }, + batchUpdateUserStatus: { + method: "POST", + operationId: API_OPERATIONS.batchUpdateUserStatus, + path: "/v1/users/batch/status", + permission: "user:status", + permissions: ["user:status"], + }, + cancelJob: { + method: "POST", + operationId: API_OPERATIONS.cancelJob, + path: "/v1/jobs/{id}/cancel", + permission: "job:cancel", + permissions: ["job:cancel"], + }, + changePassword: { + method: "POST", + operationId: API_OPERATIONS.changePassword, + path: "/v1/auth/change-password", + }, + closeAgency: { + method: "POST", + operationId: API_OPERATIONS.closeAgency, + path: "/v1/admin/agencies/{agency_id}/close", + permission: "agency:status", + permissions: ["agency:status"], + }, + createAgency: { + method: "POST", + operationId: API_OPERATIONS.createAgency, + path: "/v1/admin/agencies", + permission: "agency:create", + permissions: ["agency:create"], + }, + createBanner: { + method: "POST", + operationId: API_OPERATIONS.createBanner, + path: "/v1/admin/app-config/banners", + permission: "app-config:update", + permissions: ["app-config:update"], + }, + createBD: { + method: "POST", + operationId: API_OPERATIONS.createBD, + path: "/v1/admin/bds", + permission: "bd:create", + permissions: ["bd:create"], + }, + createBDLeader: { + method: "POST", + operationId: API_OPERATIONS.createBDLeader, + path: "/v1/admin/bd-leaders", + permission: "bd:create", + permissions: ["bd:create"], + }, + createCoinSeller: { + method: "POST", + operationId: API_OPERATIONS.createCoinSeller, + path: "/v1/admin/coin-sellers", + permission: "coin-seller:create", + permissions: ["coin-seller:create"], + }, + createCountry: { + method: "POST", + operationId: API_OPERATIONS.createCountry, + path: "/v1/admin/countries", + permission: "country:create", + permissions: ["country:create"], + }, + createGift: { + method: "POST", + operationId: API_OPERATIONS.createGift, + path: "/v1/admin/gifts", + permission: "gift:create", + permissions: ["gift:create"], + }, + createMenu: { + method: "POST", + operationId: API_OPERATIONS.createMenu, + path: "/v1/system/menus", + permission: "menu:create", + permissions: ["menu:create"], + }, + createPermission: { + method: "POST", + operationId: API_OPERATIONS.createPermission, + path: "/v1/permissions", + permission: "permission:create", + permissions: ["permission:create"], + }, + createRegion: { + method: "POST", + operationId: API_OPERATIONS.createRegion, + path: "/v1/admin/regions", + permission: "region:create", + permissions: ["region:create"], + }, + createResource: { + method: "POST", + operationId: API_OPERATIONS.createResource, + path: "/v1/admin/resources", + permission: "resource:create", + permissions: ["resource:create"], + }, + createResourceGroup: { + method: "POST", + operationId: API_OPERATIONS.createResourceGroup, + path: "/v1/admin/resource-groups", + permission: "resource-group:create", + permissions: ["resource-group:create"], + }, + createRole: { + method: "POST", + operationId: API_OPERATIONS.createRole, + path: "/v1/roles", + permission: "role:create", + permissions: ["role:create", "role:manage"], + }, + createUser: { + method: "POST", + operationId: API_OPERATIONS.createUser, + path: "/v1/users", + permission: "user:create", + permissions: ["user:create"], + }, + createUserExportJob: { + method: "POST", + operationId: API_OPERATIONS.createUserExportJob, + path: "/v1/exports/users", + permission: "export:create", + permissions: ["export:create"], + }, + creditCoinSellerStock: { + method: "POST", + operationId: API_OPERATIONS.creditCoinSellerStock, + path: "/v1/admin/coin-sellers/{user_id}/stock-credits", + permission: "coin-seller:stock-credit", + permissions: ["coin-seller:stock-credit"], + }, + dashboardOverview: { + method: "GET", + operationId: API_OPERATIONS.dashboardOverview, + path: "/v1/dashboard/overview", + permission: "overview:view", + permissions: ["overview:view"], + }, + deleteBanner: { + method: "DELETE", + operationId: API_OPERATIONS.deleteBanner, + path: "/v1/admin/app-config/banners/{banner_id}", + permission: "app-config:update", + permissions: ["app-config:update"], + }, + deleteCountry: { + method: "DELETE", + operationId: API_OPERATIONS.deleteCountry, + path: "/v1/admin/countries/{country_id}", + permission: "country:status", + permissions: ["country:status"], + }, + deleteMenu: { + method: "DELETE", + operationId: API_OPERATIONS.deleteMenu, + path: "/v1/system/menus/{id}", + permission: "menu:delete", + permissions: ["menu:delete"], + }, + deleteNotification: { + method: "DELETE", + operationId: API_OPERATIONS.deleteNotification, + path: "/v1/notifications/{id}", + permission: "notification:delete", + permissions: ["notification:delete"], + }, + deletePermission: { + method: "DELETE", + operationId: API_OPERATIONS.deletePermission, + path: "/v1/permissions/{id}", + permission: "permission:delete", + permissions: ["permission:delete"], + }, + deleteRole: { + method: "DELETE", + operationId: API_OPERATIONS.deleteRole, + path: "/v1/roles/{id}", + permission: "role:delete", + permissions: ["role:delete", "role:manage"], + }, + deleteRoom: { + method: "DELETE", + operationId: API_OPERATIONS.deleteRoom, + path: "/v1/admin/rooms/{room_id}", + permission: "room:delete", + permissions: ["room:delete"], + }, + disableCountry: { + method: "POST", + operationId: API_OPERATIONS.disableCountry, + path: "/v1/admin/countries/{country_id}/disable", + permission: "country:status", + permissions: ["country:status"], + }, + disableGift: { + method: "POST", + operationId: API_OPERATIONS.disableGift, + path: "/v1/admin/gifts/{gift_id}/disable", + permission: "gift:status", + permissions: ["gift:status"], + }, + disableRegion: { + method: "DELETE", + operationId: API_OPERATIONS.disableRegion, + path: "/v1/admin/regions/{region_id}", + permission: "region:status", + permissions: ["region:status"], + }, + disableResource: { + method: "POST", + operationId: API_OPERATIONS.disableResource, + path: "/v1/admin/resources/{resource_id}/disable", + permission: "resource:update", + permissions: ["resource:update"], + }, + disableResourceGroup: { + method: "POST", + operationId: API_OPERATIONS.disableResourceGroup, + path: "/v1/admin/resource-groups/{group_id}/disable", + permission: "resource-group:update", + permissions: ["resource-group:update"], + }, + downloadJobArtifact: { + method: "GET", + operationId: API_OPERATIONS.downloadJobArtifact, + path: "/v1/jobs/{id}/artifact", + permission: "job:view", + permissions: ["job:view"], + }, + enableCountry: { + method: "POST", + operationId: API_OPERATIONS.enableCountry, + path: "/v1/admin/countries/{country_id}/enable", + permission: "country:status", + permissions: ["country:status"], + }, + enableGift: { + method: "POST", + operationId: API_OPERATIONS.enableGift, + path: "/v1/admin/gifts/{gift_id}/enable", + permission: "gift:status", + permissions: ["gift:status"], + }, + enableRegion: { + method: "POST", + operationId: API_OPERATIONS.enableRegion, + path: "/v1/admin/regions/{region_id}/enable", + permission: "region:status", + permissions: ["region:status"], + }, + enableResource: { + method: "POST", + operationId: API_OPERATIONS.enableResource, + path: "/v1/admin/resources/{resource_id}/enable", + permission: "resource:update", + permissions: ["resource:update"], + }, + enableResourceGroup: { + method: "POST", + operationId: API_OPERATIONS.enableResourceGroup, + path: "/v1/admin/resource-groups/{group_id}/enable", + permission: "resource-group:update", + permissions: ["resource-group:update"], + }, + exportLoginLogs: { + method: "GET", + operationId: API_OPERATIONS.exportLoginLogs, + path: "/v1/logs/login/export", + permission: "log:export", + permissions: ["log:export"], + }, + exportOperationLogs: { + method: "GET", + operationId: API_OPERATIONS.exportOperationLogs, + path: "/v1/logs/operations/export", + permission: "log:export", + permissions: ["log:export"], + }, + exportUsers: { + method: "GET", + operationId: API_OPERATIONS.exportUsers, + path: "/v1/users/export", + permission: "user:export", + permissions: ["user:export"], + }, + getCountry: { + method: "GET", + operationId: API_OPERATIONS.getCountry, + path: "/v1/admin/countries/{country_id}", + permission: "country:view", + permissions: ["country:view"], + }, + getRegion: { + method: "GET", + operationId: API_OPERATIONS.getRegion, + path: "/v1/admin/regions/{region_id}", + permission: "region:view", + permissions: ["region:view"], + }, + getResource: { + method: "GET", + operationId: API_OPERATIONS.getResource, + path: "/v1/admin/resources/{resource_id}", + permission: "resource:view", + permissions: ["resource:view"], + }, + getResourceGroup: { + method: "GET", + operationId: API_OPERATIONS.getResourceGroup, + path: "/v1/admin/resource-groups/{group_id}", + permission: "resource-group:view", + permissions: ["resource-group:view"], + }, + getRoleDataScopes: { + method: "GET", + operationId: API_OPERATIONS.getRoleDataScopes, + path: "/v1/roles/{id}/data-scopes", + permission: "role:data-scope", + permissions: ["role:data-scope"], + }, + getUser: { + method: "GET", + operationId: API_OPERATIONS.getUser, + path: "/v1/users/{id}", + permission: "user:view", + permissions: ["user:view"], + }, + grantResource: { + method: "POST", + operationId: API_OPERATIONS.grantResource, + path: "/v1/admin/resource-grants/resource", + permission: "resource-grant:create", + permissions: ["resource-grant:create"], + }, + grantResourceGroup: { + method: "POST", + operationId: API_OPERATIONS.grantResourceGroup, + path: "/v1/admin/resource-grants/group", + permission: "resource-grant:create", + permissions: ["resource-grant:create"], + }, + listAgencies: { + method: "GET", + operationId: API_OPERATIONS.listAgencies, + path: "/v1/admin/agencies", + permission: "agency:view", + permissions: ["agency:view"], + }, + listApps: { + method: "GET", + operationId: API_OPERATIONS.listApps, + path: "/v1/admin/apps", + }, + listBanners: { + method: "GET", + operationId: API_OPERATIONS.listBanners, + path: "/v1/admin/app-config/banners", + permission: "app-config:view", + permissions: ["app-config:view"], + }, + listBDLeaders: { + method: "GET", + operationId: API_OPERATIONS.listBDLeaders, + path: "/v1/admin/bd-leaders", + permission: "bd:view", + permissions: ["bd:view"], + }, + listBDs: { + method: "GET", + operationId: API_OPERATIONS.listBDs, + path: "/v1/admin/bds", + permission: "bd:view", + permissions: ["bd:view"], + }, + listCoinSellers: { + method: "GET", + operationId: API_OPERATIONS.listCoinSellers, + path: "/v1/admin/coin-sellers", + permission: "coin-seller:view", + permissions: ["coin-seller:view"], + }, + listCountries: { + method: "GET", + operationId: API_OPERATIONS.listCountries, + path: "/v1/admin/countries", + permission: "country:view", + permissions: ["country:view"], + }, + listGifts: { + method: "GET", + operationId: API_OPERATIONS.listGifts, + path: "/v1/admin/gifts", + permission: "gift:view", + permissions: ["gift:view"], + }, + listH5Links: { + method: "GET", + operationId: API_OPERATIONS.listH5Links, + path: "/v1/admin/app-config/h5-links", + permission: "app-config:view", + permissions: ["app-config:view"], + }, + listHosts: { + method: "GET", + operationId: API_OPERATIONS.listHosts, + path: "/v1/admin/hosts", + permission: "host:view", + permissions: ["host:view"], + }, + listJobs: { + method: "GET", + operationId: API_OPERATIONS.listJobs, + path: "/v1/jobs", + permission: "job:view", + permissions: ["job:view"], + }, + listLoginLogs: { + method: "GET", + operationId: API_OPERATIONS.listLoginLogs, + path: "/v1/logs/login", + permission: "log:view", + permissions: ["log:view"], + }, + listNotifications: { + method: "GET", + operationId: API_OPERATIONS.listNotifications, + path: "/v1/notifications", + permission: "notification:view", + permissions: ["notification:view"], + }, + listOperationLogs: { + method: "GET", + operationId: API_OPERATIONS.listOperationLogs, + path: "/v1/logs/operations", + permission: "log:view", + permissions: ["log:view"], + }, + listPermissions: { + method: "GET", + operationId: API_OPERATIONS.listPermissions, + path: "/v1/permissions", + permission: "permission:view", + permissions: ["permission:view", "role:permission", "role:manage"], + }, + listRechargeBills: { + method: "GET", + operationId: API_OPERATIONS.listRechargeBills, + path: "/v1/admin/payment/recharge-bills", + permission: "payment-bill:view", + permissions: ["payment-bill:view"], + }, + listRegions: { + method: "GET", + operationId: API_OPERATIONS.listRegions, + path: "/v1/admin/regions", + permission: "region:view", + permissions: ["region:view"], + }, + listResourceGrants: { + method: "GET", + operationId: API_OPERATIONS.listResourceGrants, + path: "/v1/admin/resource-grants", + permission: "resource-grant:view", + permissions: ["resource-grant:view"], + }, + listResourceGroups: { + method: "GET", + operationId: API_OPERATIONS.listResourceGroups, + path: "/v1/admin/resource-groups", + permission: "resource-group:view", + permissions: ["resource-group:view"], + }, + listResources: { + method: "GET", + operationId: API_OPERATIONS.listResources, + path: "/v1/admin/resources", + permission: "resource:view", + permissions: ["resource:view"], + }, + listRoles: { + method: "GET", + operationId: API_OPERATIONS.listRoles, + path: "/v1/roles", + permission: "role:view", + permissions: ["role:view"], + }, + listRooms: { + method: "GET", + operationId: API_OPERATIONS.listRooms, + path: "/v1/admin/rooms", + permission: "room:view", + permissions: ["room:view"], + }, + listSystemMenus: { + method: "GET", + operationId: API_OPERATIONS.listSystemMenus, + path: "/v1/system/menus", + permission: "menu:view", + permissions: ["menu:view"], + }, + listUsers: { + method: "GET", + operationId: API_OPERATIONS.listUsers, + path: "/v1/users", + permission: "user:view", + permissions: ["user:view"], + }, + login: { + method: "POST", + operationId: API_OPERATIONS.login, + path: "/v1/auth/login", + }, + logout: { + method: "POST", + operationId: API_OPERATIONS.logout, + path: "/v1/auth/logout", + }, + markAllNotificationsRead: { + method: "PATCH", + operationId: API_OPERATIONS.markAllNotificationsRead, + path: "/v1/notifications/read-all", + permission: "notification:read-all", + permissions: ["notification:read-all"], + }, + markNotificationRead: { + method: "PATCH", + operationId: API_OPERATIONS.markNotificationRead, + path: "/v1/notifications/{id}/read", + permission: "notification:read", + permissions: ["notification:read"], + }, + me: { + method: "GET", + operationId: API_OPERATIONS.me, + path: "/v1/auth/me", + }, + navigationMenus: { + method: "GET", + operationId: API_OPERATIONS.navigationMenus, + path: "/v1/navigation/menus", + }, + refresh: { + method: "POST", + operationId: API_OPERATIONS.refresh, + path: "/v1/auth/refresh", + }, + replaceRegionCountries: { + method: "PUT", + operationId: API_OPERATIONS.replaceRegionCountries, + path: "/v1/admin/regions/{region_id}/countries", + permission: "region:update", + permissions: ["region:update"], + }, + replaceRoleDataScopes: { + method: "PUT", + operationId: API_OPERATIONS.replaceRoleDataScopes, + path: "/v1/roles/{id}/data-scopes", + permission: "role:data-scope", + permissions: ["role:data-scope"], + }, + replaceRolePermissions: { + method: "PUT", + operationId: API_OPERATIONS.replaceRolePermissions, + path: "/v1/roles/{id}/permissions", + permission: "role:permission", + permissions: ["role:permission", "role:manage"], + }, + resetUserPassword: { + method: "POST", + operationId: API_OPERATIONS.resetUserPassword, + path: "/v1/users/{id}/reset-password", + permission: "user:reset-password", + permissions: ["user:reset-password"], + }, + search: { + method: "GET", + operationId: API_OPERATIONS.search, + path: "/v1/search", + }, + setAgencyJoinEnabled: { + method: "POST", + operationId: API_OPERATIONS.setAgencyJoinEnabled, + path: "/v1/admin/agencies/{agency_id}/join-enabled", + permission: "agency:status", + permissions: ["agency:status"], + }, + setBDLeaderStatus: { + method: "PATCH", + operationId: API_OPERATIONS.setBDLeaderStatus, + path: "/v1/admin/bd-leaders/{user_id}/status", + permission: "bd:update", + permissions: ["bd:update"], + }, + setBDStatus: { + method: "PATCH", + operationId: API_OPERATIONS.setBDStatus, + path: "/v1/admin/bds/{user_id}/status", + permission: "bd:update", + permissions: ["bd:update"], + }, + setCoinSellerStatus: { + method: "PATCH", + operationId: API_OPERATIONS.setCoinSellerStatus, + path: "/v1/admin/coin-sellers/{user_id}/status", + permission: "coin-seller:update", + permissions: ["coin-seller:update"], + }, + sortMenus: { + method: "PUT", + operationId: API_OPERATIONS.sortMenus, + path: "/v1/system/menus/sort", + permission: "menu:sort", + permissions: ["menu:sort"], + }, + syncPermissions: { + method: "POST", + operationId: API_OPERATIONS.syncPermissions, + path: "/v1/permissions/sync", + permission: "permission:sync", + permissions: ["permission:sync"], + }, + updateBanner: { + method: "PUT", + operationId: API_OPERATIONS.updateBanner, + path: "/v1/admin/app-config/banners/{banner_id}", + permission: "app-config:update", + permissions: ["app-config:update"], + }, + updateCountry: { + method: "PATCH", + operationId: API_OPERATIONS.updateCountry, + path: "/v1/admin/countries/{country_id}", + permission: "country:update", + permissions: ["country:update"], + }, + updateGift: { + method: "PUT", + operationId: API_OPERATIONS.updateGift, + path: "/v1/admin/gifts/{gift_id}", + permission: "gift:update", + permissions: ["gift:update"], + }, + updateH5Links: { + method: "PUT", + operationId: API_OPERATIONS.updateH5Links, + path: "/v1/admin/app-config/h5-links", + permission: "app-config:update", + permissions: ["app-config:update"], + }, + updateMenu: { + method: "PATCH", + operationId: API_OPERATIONS.updateMenu, + path: "/v1/system/menus/{id}", + permission: "menu:update", + permissions: ["menu:update"], + }, + updateMenuVisible: { + method: "PATCH", + operationId: API_OPERATIONS.updateMenuVisible, + path: "/v1/system/menus/{id}/visible", + permission: "menu:visible", + permissions: ["menu:visible"], + }, + updatePermission: { + method: "PATCH", + operationId: API_OPERATIONS.updatePermission, + path: "/v1/permissions/{id}", + permission: "permission:update", + permissions: ["permission:update"], + }, + updateRegion: { + method: "PATCH", + operationId: API_OPERATIONS.updateRegion, + path: "/v1/admin/regions/{region_id}", + permission: "region:update", + permissions: ["region:update"], + }, + updateResource: { + method: "PUT", + operationId: API_OPERATIONS.updateResource, + path: "/v1/admin/resources/{resource_id}", + permission: "resource:update", + permissions: ["resource:update"], + }, + updateResourceGroup: { + method: "PUT", + operationId: API_OPERATIONS.updateResourceGroup, + path: "/v1/admin/resource-groups/{group_id}", + permission: "resource-group:update", + permissions: ["resource-group:update"], + }, + updateResourceGroupItems: { + method: "PUT", + operationId: API_OPERATIONS.updateResourceGroupItems, + path: "/v1/admin/resource-groups/{group_id}/items", + permission: "resource-group:update", + permissions: ["resource-group:update"], + }, + updateRole: { + method: "PATCH", + operationId: API_OPERATIONS.updateRole, + path: "/v1/roles/{id}", + permission: "role:update", + permissions: ["role:update", "role:manage"], + }, + updateRoom: { + method: "PATCH", + operationId: API_OPERATIONS.updateRoom, + path: "/v1/admin/rooms/{room_id}", + permission: "room:update", + permissions: ["room:update"], + }, + updateUser: { + method: "PATCH", + operationId: API_OPERATIONS.updateUser, + path: "/v1/users/{id}", + permission: "user:update", + permissions: ["user:update"], + }, + updateUserStatus: { + method: "PATCH", + operationId: API_OPERATIONS.updateUserStatus, + path: "/v1/users/{id}/status", + permission: "user:status", + permissions: ["user:status"], + }, + uploadFile: { + method: "POST", + operationId: API_OPERATIONS.uploadFile, + path: "/v1/admin/files/upload", + permission: "upload:create", + permissions: ["upload:create"], + }, + uploadImage: { + method: "POST", + operationId: API_OPERATIONS.uploadImage, + path: "/v1/admin/files/image/upload", + permission: "upload:create", + permissions: ["upload:create"], + }, }; export const API_OPERATION_IDS = Object.values(API_OPERATIONS); export const API_PERMISSION_CODES = Array.from( - new Set( - Object.values(API_ENDPOINTS) - .flatMap((endpoint) => endpoint.permissions || (endpoint.permission ? [endpoint.permission] : [])) - .filter((permission): permission is PermissionCode => Boolean(permission)) - ) + new Set( + Object.values(API_ENDPOINTS) + .flatMap((endpoint) => endpoint.permissions || (endpoint.permission ? [endpoint.permission] : [])) + .filter((permission): permission is PermissionCode => Boolean(permission)), + ), ); export type ApiPathParams = Record; export function apiEndpointPath(operationId: ApiOperationId, params: ApiPathParams = {}) { - let endpointPath = API_ENDPOINTS[operationId].path; + let endpointPath = API_ENDPOINTS[operationId].path; - for (const [key, value] of Object.entries(params)) { - endpointPath = endpointPath.replace(`{${key}}`, encodeURIComponent(String(value))); - } + for (const [key, value] of Object.entries(params)) { + endpointPath = endpointPath.replace(`{${key}}`, encodeURIComponent(String(value))); + } - return endpointPath; + return endpointPath; } diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts index e724ffa..5402785 100644 --- a/src/shared/api/generated/schema.d.ts +++ b/src/shared/api/generated/schema.d.ts @@ -212,6 +212,22 @@ export interface paths { patch: operations["setCoinSellerStatus"]; trace?: never; }; + "/admin/coin-sellers/{user_id}/stock-credits": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["creditCoinSellerStock"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/admin/countries": { parameters: { query?: never; @@ -2103,6 +2119,20 @@ export interface operations { 200: components["responses"]["EmptyResponse"]; }; }; + creditCoinSellerStock: { + parameters: { + query?: never; + header?: never; + path: { + user_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["EmptyResponse"]; + }; + }; listCountries: { parameters: { query?: never; diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts index a29ceb3..6aed009 100644 --- a/src/shared/api/types.ts +++ b/src/shared/api/types.ts @@ -217,14 +217,25 @@ export interface RechargeBillDto { } export interface BDProfileDto { + avatar?: string; createdAtMs?: number; + createdByAvatar?: string; + createdByDisplayUserId?: string; + createdByUsername?: string; createdByUserId?: number; + displayUserId?: string; parentLeaderUserId?: number; + parentLeaderDisplayUserId?: string; + parentLeaderUsername?: string; + parentLeaderAvatar?: string; regionId?: number; + regionName?: string; role?: string; status?: string; + subBdCount?: number; updatedAtMs?: number; userId: number; + username?: string; } export interface AgencyDto { @@ -234,27 +245,40 @@ export interface AgencyDto { joinEnabled?: boolean; maxHosts?: number; name?: string; + ownerAvatar?: string; + ownerDisplayUserId?: string; + ownerUsername?: string; ownerUserId?: number; + parentBdAvatar?: string; + parentBdDisplayUserId?: string; + parentBdUsername?: string; parentBdUserId?: number; regionId?: number; + regionName?: string; status?: string; updatedAtMs?: number; } export interface HostProfileDto { + avatar?: string; createdAtMs?: number; currentAgencyId?: number; + currentAgencyName?: string; currentMembershipId?: number; + displayUserId?: string; firstBecameHostAtMs?: number; regionId?: number; + regionName?: string; source?: string; status?: string; updatedAtMs?: number; userId: number; + username?: string; } export interface CoinSellerDto { avatar?: string; + contact?: string; createdAtMs?: number; createdByUserId?: number; displayUserId?: string; @@ -268,6 +292,26 @@ export interface CoinSellerDto { username?: string; } +export interface CoinSellerStockCreditPayload { + coinAmount: number; + commandId: string; + reason?: string; + rechargeAmount?: string; + type: "usdt_purchase" | "coin_compensation"; +} + +export interface CoinSellerStockCreditDto { + balanceAfter: number; + coinAmount: number; + countsAsSellerRecharge: boolean; + createdAtMs: number; + paidAmountMicro: number; + paidCurrencyCode: string; + sellerUserId: string; + stockType: string; + transactionId: string; +} + export interface AppUserDto { avatar?: string; coin?: number; @@ -440,10 +484,13 @@ export interface BDStatusPayload extends HostCommandPayload { } export interface CreateCoinSellerPayload extends HostCommandPayload { + contact?: string; targetUserId: number; } -export type CoinSellerStatusPayload = BDStatusPayload; +export interface CoinSellerStatusPayload extends BDStatusPayload { + contact?: string; +} export interface CreateAgencyPayload extends HostCommandPayload { joinEnabled: boolean; diff --git a/src/shared/ui/AdminFormDialog.jsx b/src/shared/ui/AdminFormDialog.jsx index 0314038..de325eb 100644 --- a/src/shared/ui/AdminFormDialog.jsx +++ b/src/shared/ui/AdminFormDialog.jsx @@ -3,6 +3,8 @@ import Dialog from "@mui/material/Dialog"; import DialogActions from "@mui/material/DialogActions"; import DialogContent from "@mui/material/DialogContent"; import DialogTitle from "@mui/material/DialogTitle"; +import InputAdornment from "@mui/material/InputAdornment"; +import TextField from "@mui/material/TextField"; import { Button } from "@/shared/ui/Button.jsx"; import styles from "@/shared/ui/AdminFormDialog.module.css"; @@ -66,6 +68,43 @@ export function AdminFormFieldGrid({ children, columns }) { ); } +export function AdminFormReadOnlyField({ label, value }) { + const displayValue = value === undefined || value === null || value === "" ? "-" : value; + + return ( +
+ {label} + {displayValue} +
+ ); +} + +export function AdminFormAmountField({ className = "", inputProps, InputProps, slotProps, unit, ...props }) { + const inputSlotProps = { ...(slotProps?.input || {}), ...(InputProps || {}) }; + + return ( + + {unit} + + ) : ( + inputSlotProps.endAdornment + ) + } + }} + type={props.type || "text"} + /> + ); +} + export function AdminFormInlineActions({ children }) { return
{children}
; } diff --git a/src/shared/ui/AdminFormDialog.module.css b/src/shared/ui/AdminFormDialog.module.css index f93d37f..5732f1d 100644 --- a/src/shared/ui/AdminFormDialog.module.css +++ b/src/shared/ui/AdminFormDialog.module.css @@ -9,6 +9,7 @@ width: min(var(--admin-form-dialog-width), calc(100vw - 32px)) !important; max-width: min(var(--admin-form-dialog-width), calc(100vw - 32px)) !important; border-radius: var(--radius-card); + box-shadow: var(--shadow-dialog); } .compact { @@ -77,9 +78,14 @@ gap: var(--space-3); } +.section + .section { + padding-top: var(--space-4); + border-top: 1px solid var(--border-soft); +} + .sectionHeader { display: flex; - min-height: var(--control-height); + min-height: 20px; align-items: center; justify-content: flex-start; flex-wrap: wrap; @@ -87,9 +93,16 @@ } .sectionTitle { - color: var(--text-primary); - font-size: var(--admin-font-size); + color: var(--text-secondary); + font-size: 13px; font-weight: 700; + line-height: 18px; +} + +.section > :global(.MuiTextField-root), +.section > :global(.MuiFormControl-root), +.section > :global(.MuiAutocomplete-root) { + width: 100%; } .sectionActions, @@ -109,10 +122,62 @@ .fieldGrid > :global(.MuiTextField-root), .fieldGrid > :global(.MuiFormControl-root), -.fieldGrid > :global(.MuiAutocomplete-root) { +.fieldGrid > :global(.MuiAutocomplete-root), +.fieldGrid > .readOnlyField { width: 100%; } +.readOnlyField { + display: grid; + min-width: 0; + min-height: 56px; + align-content: center; + gap: var(--space-1); + padding: 8px var(--space-3); + border: 1px solid var(--border-soft); + border-radius: var(--radius-control); + background: var(--bg-card-strong); +} + +.readOnlyLabel { + overflow: hidden; + color: var(--text-tertiary); + font-size: 12px; + line-height: 16px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.readOnlyValue { + overflow: hidden; + color: var(--text-primary); + font-size: var(--admin-font-size); + font-weight: 760; + line-height: var(--admin-line-height); + text-overflow: ellipsis; + white-space: nowrap; +} + +.amountField :global(.MuiOutlinedInput-root) { + background: var(--bg-input-strong); +} + +.amountField :global(.MuiOutlinedInput-root.Mui-focused) { + box-shadow: 0 0 0 3px var(--focus-ring); +} + +.amountField :global(.MuiInputBase-input) { + text-align: right; + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +.inputUnit { + color: var(--text-tertiary); + font-size: 13px; + font-weight: 700; +} + .list { display: grid; gap: var(--space-2); @@ -135,8 +200,13 @@ } .actions { + position: sticky; + bottom: 0; + z-index: 1; justify-content: flex-end; gap: var(--space-2); + border-top: 1px solid var(--border-soft); + background: var(--bg-card); padding: var(--space-3) var(--admin-form-content-x) var(--space-5); } diff --git a/src/shared/ui/AdminListLayout.jsx b/src/shared/ui/AdminListLayout.jsx index 087adef..f54e407 100644 --- a/src/shared/ui/AdminListLayout.jsx +++ b/src/shared/ui/AdminListLayout.jsx @@ -1,70 +1,87 @@ +import RestartAltOutlined from "@mui/icons-material/RestartAltOutlined"; import Tooltip from "@mui/material/Tooltip"; +import { Button } from "@/shared/ui/Button.jsx"; import { IconButton } from "@/shared/ui/IconButton.jsx"; import { SearchBox } from "@/shared/ui/SearchBox.jsx"; import { StatusSelect } from "@/shared/ui/StatusSelect.jsx"; import styles from "@/shared/ui/AdminListLayout.module.css"; export function AdminListPage({ children }) { - return ( -
-
{children}
-
- ); + return ( +
+
{children}
+
+ ); } export function AdminListToolbar({ actions, filters }) { - return ( -
-
-
{filters}
-
- {actions ?
{actions}
: null} -
- ); + return ( +
+
+
{filters}
+
+ {actions ?
{actions}
: null} +
+ ); } export function AdminListBody({ children }) { - return
{children}
; + return
{children}
; } export function AdminRowActions({ children }) { - return
{children}
; + return
{children}
; } export function AdminSearchBox(props) { - return ; + return ; } export function AdminFilterSelect(props) { - return ; + return ( + + ); +} + +export function AdminFilterResetButton({ disabled = false, label = "重置", onClick }) { + return ( + + ); } export function AdminActionIconButton({ children, label, primary = false, tooltip = label, ...props }) { - return ( - - - - {children} - - - - ); + return ( + + + + {children} + + + + ); } export const adminListClasses = styles; const primaryActionSx = { - borderColor: "var(--primary)", - backgroundColor: "var(--primary)", - color: "var(--active-contrast)", - "&:hover": { - borderColor: "var(--primary-strong)", - backgroundColor: "var(--primary-strong)", - color: "var(--active-contrast)" - } + borderColor: "var(--primary)", + backgroundColor: "var(--primary)", + color: "var(--active-contrast)", + "&:hover": { + borderColor: "var(--primary-strong)", + backgroundColor: "var(--primary-strong)", + color: "var(--active-contrast)", + }, }; diff --git a/src/shared/ui/AdminListLayout.module.css b/src/shared/ui/AdminListLayout.module.css index 5e39a2e..daebaee 100644 --- a/src/shared/ui/AdminListLayout.module.css +++ b/src/shared/ui/AdminListLayout.module.css @@ -1,135 +1,139 @@ .root { - display: flex; - height: calc(100vh - var(--header-height) - var(--space-8) - var(--space-4)); - height: calc(100dvh - var(--header-height) - var(--space-8) - var(--space-4)); - min-height: 0; - flex-direction: column; + display: flex; + height: calc(100vh - var(--header-height) - var(--space-8) - var(--space-4)); + height: calc(100dvh - var(--header-height) - var(--space-8) - var(--space-4)); + min-height: 0; + flex-direction: column; } .contentPanel { - display: flex; - flex: 1; - flex-direction: column; - min-height: 0; - overflow: hidden; - border: 1px solid var(--border); - border-radius: var(--radius-card); - background: var(--bg-card); + display: flex; + flex: 1; + flex-direction: column; + min-height: 0; + overflow: hidden; + border: 1px solid var(--border); + border-radius: var(--radius-card); + background: var(--bg-card); } .contentPanel :global(.data-state), .contentPanel :global(.table-frame) { - flex: 1; - min-height: 0; - border: 0; - border-radius: 0; - background: transparent; + flex: 1; + min-height: 0; + border: 0; + border-radius: 0; + background: transparent; } .contentPanel :global(.table-scroll) { - overflow: auto; + overflow: auto; } .contentPanel :global(.admin-row--head) { - position: sticky; - top: 0; - z-index: 1; + position: sticky; + top: 0; + z-index: 1; } .toolbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-4); - padding: var(--space-4) var(--space-5); - border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + padding: var(--space-4) var(--space-5); + border-bottom: 1px solid var(--border); } .toolbarLeft { - display: flex; - min-width: 0; - flex: 1; - align-items: center; - gap: var(--space-3); + display: flex; + min-width: 0; + flex: 1; + align-items: center; + gap: var(--space-3); } .toolbarActions { - display: flex; - flex: 0 0 auto; - align-items: center; - gap: var(--space-2); + display: flex; + flex: 0 0 auto; + align-items: center; + gap: var(--space-2); } .filters { - display: flex; - min-width: 0; - flex: 1; - align-items: center; - flex-wrap: wrap; - gap: var(--space-3); + display: flex; + min-width: 0; + flex: 1; + align-items: center; + flex-wrap: wrap; + gap: var(--space-3); } .search { - flex: 0 1 300px; + flex: 0 1 300px; } .statusSelect { - flex: 0 0 150px; + flex: 0 0 150px; +} + +.resetButton { + flex: 0 0 auto; } .listBlock { - display: flex; - flex: 1; - min-height: 0; - flex-direction: column; - background: var(--bg-card); + display: flex; + flex: 1; + min-height: 0; + flex-direction: column; + background: var(--bg-card); } .listBlock :global(.pagination-bar) { - margin: var(--space-3) var(--space-5) var(--space-4); + margin: var(--space-3) var(--space-5) var(--space-4); } .rowActions { - display: inline-flex; - align-items: center; - gap: var(--space-2); + display: inline-flex; + align-items: center; + gap: var(--space-2); } .dragRow { - cursor: grab; + cursor: grab; } .dragRow:active { - cursor: grabbing; + cursor: grabbing; } .dragRowActive { - background: var(--primary-hover); + background: var(--primary-hover); } .dragHandle { - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--text-tertiary); - cursor: grab; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-tertiary); + cursor: grab; } @media (max-width: 760px) { - .toolbar, - .toolbarLeft, - .filters { - align-items: stretch; - flex-direction: column; - } + .toolbar, + .toolbarLeft, + .filters { + align-items: stretch; + flex-direction: column; + } - .toolbarActions { - align-self: flex-end; - } + .toolbarActions { + align-self: flex-end; + } - .search, - .statusSelect { - flex: 1 1 auto; - width: 100%; - } + .search, + .statusSelect { + flex: 1 1 auto; + width: 100%; + } } diff --git a/src/shared/ui/DataTable.jsx b/src/shared/ui/DataTable.jsx index 1ffa094..c5ed263 100644 --- a/src/shared/ui/DataTable.jsx +++ b/src/shared/ui/DataTable.jsx @@ -1,8 +1,10 @@ import SearchOutlined from "@mui/icons-material/SearchOutlined"; import MenuItem from "@mui/material/MenuItem"; +import MenuList from "@mui/material/MenuList"; import Popover from "@mui/material/Popover"; import TextField from "@mui/material/TextField"; import { useEffect, useMemo, useState } from "react"; +import { Button } from "@/shared/ui/Button.jsx"; const DEFAULT_COLUMN_WIDTH = "minmax(120px, 1fr)"; const DEFAULT_RESIZE_MIN_WIDTH = 72; @@ -43,6 +45,7 @@ export function DataTable({ const gridTemplate = preparedColumns.map((column) => column.width).join(" "); const activeFilterColumn = preparedColumns.find((column) => column.key === filterMenu.columnKey && column.filter) || null; const activeFilter = activeFilterColumn?.filter || null; + const activeFilterIsText = activeFilter?.type === "text"; const filterOptions = useMemo(() => filterVisibleOptions(activeFilter, filterQuery), [activeFilter, filterQuery]); useEffect(() => { @@ -112,7 +115,7 @@ export function DataTable({ } event.preventDefault(); setFilterMenu({ anchorEl: event.currentTarget, columnKey: column.key }); - setFilterQuery(""); + setFilterQuery(column.filter.type === "text" ? String(column.filter.value ?? "") : ""); }; const closeColumnFilter = () => { @@ -125,6 +128,17 @@ export function DataTable({ closeColumnFilter(); }; + const submitTextFilter = (event) => { + event.preventDefault(); + activeFilter?.onChange?.(filterQuery.trim()); + closeColumnFilter(); + }; + + const clearTextFilter = () => { + activeFilter?.onChange?.(""); + closeColumnFilter(); + }; + return (
{title || total !== undefined ? ( @@ -189,32 +203,54 @@ export function DataTable({ onClose={closeColumnFilter} > {activeFilterColumn ? ( -
- setFilterQuery(event.target.value)} - /> -
- {filterOptions.length ? ( - filterOptions.map((option) => ( - changeFilter(option.value)} - > - {option.label} - - )) - ) : ( -
{activeFilter.loading ? "加载中..." : "无匹配选项"}
- )} + activeFilterIsText ? ( +
+ setFilterQuery(event.target.value)} + /> +
+ + +
+ + ) : ( +
+ setFilterQuery(event.target.value)} + /> + + {filterOptions.length ? ( + filterOptions.map((option) => ( + changeFilter(option.value)} + > + {option.label} + + )) + ) : ( +
{activeFilter.loading ? "加载中..." : "无匹配选项"}
+ )} +
-
+ ) ) : null}
@@ -232,6 +268,7 @@ function HeaderLabel({ column, onOpenFilter }) { } const active = isFilterActive(column.filter); + const filterValueLabel = active ? filterDisplayValue(column.filter) : ""; return ( ); } @@ -320,6 +360,19 @@ function isFilterActive(filter) { return filter && filter.value !== undefined && filter.value !== null && String(filter.value) !== ""; } +function filterDisplayValue(filter) { + const value = String(filter?.value ?? ""); + if (!value) { + return ""; + } + if (filter?.type === "text") { + return value; + } + + const matchedOption = normalizeFilterOptions(filter).find((option) => option.value === value); + return matchedOption?.label || value; +} + function filterVisibleOptions(filter, query) { if (!filter) { return []; diff --git a/src/shared/ui/RegionFilterControl.jsx b/src/shared/ui/RegionFilterControl.jsx index 210eb66..7427d5d 100644 --- a/src/shared/ui/RegionFilterControl.jsx +++ b/src/shared/ui/RegionFilterControl.jsx @@ -1,65 +1,47 @@ -import RestartAltOutlined from "@mui/icons-material/RestartAltOutlined"; -import Tooltip from "@mui/material/Tooltip"; -import { IconButton } from "@/shared/ui/IconButton.jsx"; import { RegionSelect } from "@/shared/ui/RegionSelect.jsx"; +import { createOptionsColumnFilter } from "@/shared/ui/tableFilters.js"; export function RegionFilterControl({ - className = "", - disabled = false, - emptyLabel = "全部区域", - loading = false, - onChange, - options = [], - resetLabel = "重置区域", - selectClassName = "", - value, - ...props + className = "", + disabled = false, + emptyLabel = "全部区域", + loading = false, + onChange, + options = [], + selectClassName = "", + value, + ...props }) { - const selectedValue = value === undefined || value === null ? "" : String(value); - const canReset = selectedValue !== "" && !disabled && !loading; - - return ( -
- - - - onChange?.("")} - > - - - - -
- ); + return ( +
+ +
+ ); } export function createRegionColumnFilter({ - emptyLabel = "全部区域", - loading = false, - onChange, - options = [], - placeholder = "搜索区域", - value -}) { - return { - emptyLabel, - loading, + emptyLabel = "全部区域", + loading = false, onChange, - options, - placeholder, - value - }; + options = [], + placeholder = "搜索区域", + value, +}) { + return createOptionsColumnFilter({ + emptyLabel, + loading, + onChange, + options, + placeholder, + value, + }); } diff --git a/src/shared/ui/tableFilters.js b/src/shared/ui/tableFilters.js new file mode 100644 index 0000000..5154aae --- /dev/null +++ b/src/shared/ui/tableFilters.js @@ -0,0 +1,20 @@ +export function createTextColumnFilter({ loading = false, onChange, placeholder, value }) { + return { + loading, + onChange, + placeholder, + type: "text", + value + }; +} + +export function createOptionsColumnFilter({ emptyLabel, loading = false, onChange, options = [], placeholder, value }) { + return { + emptyLabel, + loading, + onChange, + options, + placeholder, + value + }; +} diff --git a/src/styles/shared-ui.css b/src/styles/shared-ui.css index 696c1d6..dfa3bf1 100644 --- a/src/styles/shared-ui.css +++ b/src/styles/shared-ui.css @@ -1,448 +1,442 @@ .page-head { - display: flex; - align-items: flex-end; - justify-content: space-between; - gap: var(--space-4); - margin-bottom: var(--space-5); + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: var(--space-4); + margin-bottom: var(--space-5); } .page-title { - margin: 0; - font-size: var(--admin-font-size); - font-weight: 700; + margin: 0; + font-size: var(--admin-font-size); + font-weight: 700; } .page-meta { - margin: var(--space-2) 0 0; - color: var(--text-tertiary); - font-size: var(--admin-font-size); + margin: var(--space-2) 0 0; + color: var(--text-tertiary); + font-size: var(--admin-font-size); } .page-actions { - display: flex; - align-items: center; - gap: var(--space-3); + display: flex; + align-items: center; + gap: var(--space-3); } .button { - display: inline-flex; - height: var(--control-height); - min-height: var(--control-height); - align-items: center; - justify-content: center; - gap: var(--space-2); - padding: 0 var(--space-3); - border: 1px solid var(--border); - border-radius: var(--radius-control); - background: var(--bg-card); - color: var(--text-secondary); - cursor: pointer; - font-size: var(--admin-font-size); - line-height: var(--admin-line-height); - white-space: nowrap; - transition: - background var(--motion-fast) var(--ease-standard), - border-color var(--motion-fast) var(--ease-standard), - box-shadow var(--motion-fast) var(--ease-standard), - color var(--motion-fast) var(--ease-standard), - transform var(--motion-base) var(--ease-emphasized); + display: inline-flex; + height: var(--control-height); + min-height: var(--control-height); + align-items: center; + justify-content: center; + gap: var(--space-2); + padding: 0 var(--space-3); + border: 1px solid var(--border); + border-radius: var(--radius-control); + background: var(--bg-card); + color: var(--text-secondary); + cursor: pointer; + font-size: var(--admin-font-size); + line-height: var(--admin-line-height); + white-space: nowrap; + transition: + background var(--motion-fast) var(--ease-standard), + border-color var(--motion-fast) var(--ease-standard), + box-shadow var(--motion-fast) var(--ease-standard), + color var(--motion-fast) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); } .button:hover { - border-color: var(--primary-border-strong); - background: var(--bg-hover); - box-shadow: var(--shadow-soft); - color: var(--text-primary); - transform: translateY(-1px); + border-color: var(--primary-border-strong); + background: var(--bg-hover); + box-shadow: var(--shadow-soft); + color: var(--text-primary); + transform: translateY(-1px); } .button:active { - box-shadow: none; - transform: translateY(0) scale(0.985); + box-shadow: none; + transform: translateY(0) scale(0.985); } .button:disabled, .icon-button:disabled { - cursor: not-allowed; - opacity: 0.42; + cursor: not-allowed; + opacity: 0.42; } .button--primary { - border-color: var(--primary); - background: var(--primary); - color: var(--active-contrast); + border-color: var(--primary); + background: var(--primary); + color: var(--active-contrast); } .button--success { - color: var(--success); + color: var(--success); } .button--danger { - color: var(--danger); + color: var(--danger); } .button.MuiButton-root { - height: var(--control-height); - min-height: var(--control-height); - padding: 0 var(--space-3); - border-radius: var(--radius-control); - font-size: var(--admin-font-size); - line-height: var(--admin-line-height); - text-transform: none; + height: var(--control-height); + min-height: var(--control-height); + padding: 0 var(--space-3); + border-radius: var(--radius-control); + font-size: var(--admin-font-size); + line-height: var(--admin-line-height); + text-transform: none; } .kpi-grid { - display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); - gap: var(--space-4); + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: var(--space-4); } .card { - border: 1px solid var(--border); - border-radius: var(--radius-card); - background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-card); + background: var(--bg-card); } .MuiTextField-root, .MuiFormControl-root { - --admin-control-height: var(--control-height); - --admin-control-font-size: var(--admin-font-size); - --admin-control-line-height: var(--admin-line-height); + --admin-control-height: var(--control-height); + --admin-control-font-size: var(--admin-font-size); + --admin-control-line-height: var(--admin-line-height); } .region-select.MuiTextField-root, .region-select.MuiFormControl-root { - width: 180px; - min-width: 180px; - flex: 0 0 180px; + width: 180px; + min-width: 180px; + flex: 0 0 180px; } .region-filter-control { - display: inline-flex; - min-width: 0; - flex: 0 0 auto; - align-items: center; - gap: var(--space-1); + display: inline-flex; + min-width: 0; + flex: 0 0 auto; + align-items: center; + gap: var(--space-1); } .region-filter-control .region-select.MuiTextField-root, .region-filter-control .region-select.MuiFormControl-root { - flex: 0 0 180px; -} - -.region-filter-control__reset.icon-button { - flex: 0 0 var(--control-height); - color: var(--text-tertiary); + flex: 0 0 180px; } .MuiInputBase-root.MuiOutlinedInput-root:not(.MuiInputBase-multiline) { - height: var(--control-height); - min-height: var(--control-height); + height: var(--control-height); + min-height: var(--control-height); } .MuiInputBase-root.MuiOutlinedInput-root { - background: var(--bg-input); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); + background: var(--bg-input); + backdrop-filter: var(--glass-blur); + -webkit-backdrop-filter: var(--glass-blur); } .MuiInputBase-root.MuiOutlinedInput-root:not(.MuiInputBase-multiline) .MuiInputBase-input { - box-sizing: border-box; - height: var(--control-height); - padding-top: 0; - padding-bottom: 0; - font-size: var(--admin-font-size); - line-height: var(--admin-line-height); + box-sizing: border-box; + height: var(--control-height); + padding-top: 0; + padding-bottom: 0; + font-size: var(--admin-font-size); + line-height: var(--admin-line-height); } .MuiInputBase-root .MuiInputBase-input::placeholder { - font-size: var(--admin-font-size); - line-height: var(--admin-line-height); + font-size: var(--admin-font-size); + line-height: var(--admin-line-height); } .MuiInputBase-input:focus { - outline: 0; + outline: 0; } .MuiTextField-root .MuiOutlinedInput-root, .MuiFormControl-root .MuiOutlinedInput-root { - border-radius: var(--radius-control); - background: var(--bg-input); - backdrop-filter: var(--glass-blur); - color: var(--text-primary); - font-size: var(--admin-control-font-size); - line-height: var(--admin-control-line-height); - -webkit-backdrop-filter: var(--glass-blur); + border-radius: var(--radius-control); + background: var(--bg-input); + backdrop-filter: var(--glass-blur); + color: var(--text-primary); + font-size: var(--admin-control-font-size); + line-height: var(--admin-control-line-height); + -webkit-backdrop-filter: var(--glass-blur); } .MuiTextField-root .MuiOutlinedInput-root:not(.MuiInputBase-multiline), .MuiFormControl-root .MuiOutlinedInput-root:not(.MuiInputBase-multiline) { - height: var(--admin-control-height); - min-height: var(--admin-control-height); + height: var(--admin-control-height); + min-height: var(--admin-control-height); } .MuiTextField-root .MuiOutlinedInput-notchedOutline, .MuiFormControl-root .MuiOutlinedInput-notchedOutline { - border-color: var(--border); + border-color: var(--border); } .MuiTextField-root .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline, .MuiFormControl-root .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline { - border-color: var(--primary-border); + border-color: var(--primary-border); } .MuiTextField-root .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline, .MuiFormControl-root .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline { - border-color: var(--primary); - border-width: 1px; + border-color: var(--primary); + border-width: 1px; } .MuiInputBase-root.Mui-focused { - box-shadow: none; + box-shadow: none; } .MuiTextField-root .MuiOutlinedInput-input, .MuiFormControl-root .MuiOutlinedInput-input { - box-sizing: border-box; - height: var(--admin-control-height); - padding: 0 var(--space-3); - color: var(--text-primary); - font-size: var(--admin-control-font-size); - line-height: var(--admin-control-line-height); + box-sizing: border-box; + height: var(--admin-control-height); + padding: 0 var(--space-3); + color: var(--text-primary); + font-size: var(--admin-control-font-size); + line-height: var(--admin-control-line-height); } .MuiTextField-root .MuiOutlinedInput-input::placeholder, .MuiFormControl-root .MuiOutlinedInput-input::placeholder { - color: var(--text-tertiary); - font-size: var(--admin-control-font-size); - line-height: var(--admin-control-line-height); - opacity: 1; + color: var(--text-tertiary); + font-size: var(--admin-control-font-size); + line-height: var(--admin-control-line-height); + opacity: 1; } .MuiTextField-root .MuiInputLabel-outlined, .MuiFormControl-root .MuiInputLabel-outlined { - color: var(--text-tertiary); - font-size: var(--admin-control-font-size); - line-height: var(--admin-control-line-height); - transform: translate(14px, 8px) scale(1); + color: var(--text-tertiary); + font-size: var(--admin-control-font-size); + line-height: var(--admin-control-line-height); + transform: translate(14px, 8px) scale(1); } .MuiTextField-root .MuiInputLabel-outlined.MuiInputLabel-shrink, .MuiFormControl-root .MuiInputLabel-outlined.MuiInputLabel-shrink { - transform: translate(14px, -8px) scale(0.75); + transform: translate(14px, -8px) scale(0.75); } .MuiTextField-root .MuiFormLabel-root.Mui-focused, .MuiFormControl-root .MuiFormLabel-root.Mui-focused { - color: var(--primary); + color: var(--primary); } .MuiTextField-root .MuiSelect-select, .MuiFormControl-root .MuiSelect-select { - display: flex; - box-sizing: border-box; - height: var(--admin-control-height); - min-height: var(--admin-control-height) !important; - align-items: center; - padding: 0 var(--space-8) 0 var(--space-3) !important; - font-size: var(--admin-control-font-size); - line-height: var(--admin-control-line-height); + display: flex; + box-sizing: border-box; + height: var(--admin-control-height); + min-height: var(--admin-control-height) !important; + align-items: center; + padding: 0 var(--space-8) 0 var(--space-3) !important; + font-size: var(--admin-control-font-size); + line-height: var(--admin-control-line-height); } .MuiTextField-root .MuiSelect-icon, .MuiFormControl-root .MuiSelect-icon { - top: 50%; - color: var(--text-tertiary); - transform: translateY(-50%); + top: 50%; + color: var(--text-tertiary); + transform: translateY(-50%); } .MuiAutocomplete-root { - --admin-autocomplete-input-height: 24px; + --admin-autocomplete-input-height: 24px; } .MuiAutocomplete-root .MuiOutlinedInput-root.MuiAutocomplete-inputRoot:not(.MuiInputBase-multiline) { - height: auto; - min-height: var(--control-height); - align-items: flex-start; - flex-wrap: wrap; - gap: var(--space-1); - padding: 5px 40px 5px var(--space-2); + height: auto; + min-height: var(--control-height); + align-items: flex-start; + flex-wrap: wrap; + gap: var(--space-1); + padding: 5px 40px 5px var(--space-2); } .MuiAutocomplete-root .MuiOutlinedInput-root.MuiAutocomplete-inputRoot .MuiAutocomplete-input { - flex: 1 0 96px; - width: auto !important; - min-width: 80px; - height: var(--admin-autocomplete-input-height); - padding: 0 !important; - line-height: var(--admin-autocomplete-input-height); + flex: 1 0 96px; + width: auto !important; + min-width: 80px; + height: var(--admin-autocomplete-input-height); + padding: 0 !important; + line-height: var(--admin-autocomplete-input-height); } .MuiAutocomplete-root .MuiOutlinedInput-root.MuiAutocomplete-inputRoot .MuiAutocomplete-endAdornment { - top: 50%; - right: var(--space-2); - transform: translateY(-50%); + top: 50%; + right: var(--space-2); + transform: translateY(-50%); } .MuiAutocomplete-root .MuiAutocomplete-tag { - max-width: calc(100% - var(--space-2)); - height: var(--admin-tag-height); - margin: 0; - border: 1px solid var(--admin-tag-border); - border-radius: var(--admin-tag-radius); - background: var(--admin-tag-bg); - color: var(--admin-tag-color); - font-size: var(--admin-tag-font-size); - font-weight: var(--admin-tag-font-weight); + max-width: calc(100% - var(--space-2)); + height: var(--admin-tag-height); + margin: 0; + border: 1px solid var(--admin-tag-border); + border-radius: var(--admin-tag-radius); + background: var(--admin-tag-bg); + color: var(--admin-tag-color); + font-size: var(--admin-tag-font-size); + font-weight: var(--admin-tag-font-weight); } .MuiAutocomplete-root .MuiAutocomplete-tag .MuiChip-label { - padding: 0 var(--admin-tag-padding-x); + padding: 0 var(--admin-tag-padding-x); } .MuiAutocomplete-root .MuiAutocomplete-tag .MuiChip-deleteIcon { - width: 18px; - height: 18px; - margin-right: var(--space-1); - color: var(--text-tertiary); + width: 18px; + height: 18px; + margin-right: var(--space-1); + color: var(--text-tertiary); } .admin-tag-list { - display: flex; - min-width: 0; - align-items: center; - flex-wrap: wrap; - gap: var(--space-2); + display: flex; + min-width: 0; + align-items: center; + flex-wrap: wrap; + gap: var(--space-2); } .admin-tag { - display: inline-flex; - max-width: 100%; - height: var(--admin-tag-height); - align-items: center; - overflow: hidden; - border: 1px solid var(--admin-tag-border); - border-radius: var(--admin-tag-radius); - background: var(--admin-tag-bg); - color: var(--admin-tag-color); - font-size: var(--admin-tag-font-size); - font-weight: var(--admin-tag-font-weight); - line-height: 1; - padding: 0 var(--admin-tag-padding-x); - text-overflow: ellipsis; - white-space: nowrap; + display: inline-flex; + max-width: 100%; + height: var(--admin-tag-height); + align-items: center; + overflow: hidden; + border: 1px solid var(--admin-tag-border); + border-radius: var(--admin-tag-radius); + background: var(--admin-tag-bg); + color: var(--admin-tag-color); + font-size: var(--admin-tag-font-size); + font-weight: var(--admin-tag-font-weight); + line-height: 1; + padding: 0 var(--admin-tag-padding-x); + text-overflow: ellipsis; + white-space: nowrap; } .MuiAutocomplete-popper .MuiAutocomplete-paper { - overflow: hidden; - border: 1px solid var(--border); - border-radius: var(--radius-control); - background: var(--bg-flyout); - box-shadow: var(--shadow-panel); - transform-origin: top center; - animation: autocomplete-pop-enter var(--motion-base) var(--ease-emphasized) both; + overflow: hidden; + border: 1px solid var(--border); + border-radius: var(--radius-control); + background: var(--bg-flyout); + box-shadow: var(--shadow-panel); + transform-origin: top center; + animation: autocomplete-pop-enter var(--motion-base) var(--ease-emphasized) both; } .MuiAutocomplete-popper[data-popper-placement*="top"] .MuiAutocomplete-paper { - transform-origin: bottom center; - animation-name: autocomplete-pop-enter-up; + transform-origin: bottom center; + animation-name: autocomplete-pop-enter-up; } .MuiAutocomplete-popper .MuiAutocomplete-listbox { - max-height: 280px; - padding: var(--space-1); + max-height: 280px; + padding: var(--space-1); } .MuiAutocomplete-popper .MuiAutocomplete-option { - min-height: var(--control-height); - border-radius: var(--radius-control); - color: var(--text-secondary); - font-size: var(--admin-font-size); - transition: - background var(--motion-fast) var(--ease-standard), - color var(--motion-fast) var(--ease-standard), - transform var(--motion-fast) var(--ease-emphasized); + min-height: var(--control-height); + border-radius: var(--radius-control); + color: var(--text-secondary); + font-size: var(--admin-font-size); + transition: + background var(--motion-fast) var(--ease-standard), + color var(--motion-fast) var(--ease-standard), + transform var(--motion-fast) var(--ease-emphasized); } .MuiAutocomplete-popper .MuiAutocomplete-option.Mui-focused, .MuiAutocomplete-popper .MuiAutocomplete-option[aria-selected="true"] { - background: var(--primary-hover); - color: var(--text-primary); - transform: translateX(2px); + background: var(--primary-hover); + color: var(--text-primary); + transform: translateX(2px); } .MuiTextField-root .MuiOutlinedInput-root.MuiInputBase-multiline, .MuiFormControl-root .MuiOutlinedInput-root.MuiInputBase-multiline { - align-items: flex-start; - padding: var(--space-2) var(--space-3); + align-items: flex-start; + padding: var(--space-2) var(--space-3); } .MuiTextField-root .MuiOutlinedInput-root.MuiInputBase-multiline .MuiOutlinedInput-input, .MuiFormControl-root .MuiOutlinedInput-root.MuiInputBase-multiline .MuiOutlinedInput-input { - height: auto; - min-height: calc(var(--admin-line-height) * 2); - padding: 0; - line-height: var(--admin-control-line-height); + height: auto; + min-height: calc(var(--admin-line-height) * 2); + padding: 0; + line-height: var(--admin-control-line-height); } .MuiFormHelperText-root { - margin: var(--space-1) 0 0; - color: var(--text-tertiary); - font-size: var(--admin-font-size); - line-height: var(--admin-line-height); + margin: var(--space-1) 0 0; + color: var(--text-tertiary); + font-size: var(--admin-font-size); + line-height: var(--admin-line-height); } .MuiMenuItem-root { - min-height: var(--control-height); - font-size: var(--admin-font-size); + min-height: var(--control-height); + font-size: var(--admin-font-size); } @media (max-width: 760px) { - .region-select.MuiTextField-root, - .region-select.MuiFormControl-root { - width: 100%; - min-width: 0; - flex: 1 1 100%; - } + .region-select.MuiTextField-root, + .region-select.MuiFormControl-root { + width: 100%; + min-width: 0; + flex: 1 1 100%; + } - .region-filter-control { - width: 100%; - flex: 1 1 100%; - } - - .region-filter-control .region-select.MuiTextField-root, - .region-filter-control .region-select.MuiFormControl-root { - width: auto; - min-width: 0; - flex: 1 1 auto; - } + .region-filter-control { + width: 100%; + flex: 1 1 100%; + } + .region-filter-control .region-select.MuiTextField-root, + .region-filter-control .region-select.MuiFormControl-root { + width: auto; + min-width: 0; + flex: 1 1 auto; + } } .kpi-card { - min-height: 132px; - padding: var(--space-5); - animation: panel-enter var(--motion-slow) var(--ease-emphasized) both; - transition: - border-color var(--motion-base) var(--ease-standard), - box-shadow var(--motion-base) var(--ease-standard), - transform var(--motion-base) var(--ease-emphasized); + min-height: 132px; + padding: var(--space-5); + animation: panel-enter var(--motion-slow) var(--ease-emphasized) both; + transition: + border-color var(--motion-base) var(--ease-standard), + box-shadow var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); } .kpi-grid .kpi-card:nth-child(2) { - animation-delay: 40ms; + animation-delay: 40ms; } .kpi-grid .kpi-card:nth-child(3) { - animation-delay: 80ms; + animation-delay: 80ms; } .kpi-grid .kpi-card:nth-child(4) { - animation-delay: 120ms; + animation-delay: 120ms; } .kpi-grid .kpi-card:nth-child(5) { - animation-delay: 160ms; + animation-delay: 160ms; } .kpi-card:hover, @@ -450,730 +444,739 @@ .table-card:hover, .server-rail:hover, .inline-inspector:hover { - border-color: var(--primary-border-strong); - box-shadow: var(--shadow-panel); - transform: translateY(-2px); + border-color: var(--primary-border-strong); + box-shadow: var(--shadow-panel); + transform: translateY(-2px); } .kpi-card__top { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-3); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); } .kpi-card__label, .card-title { - color: var(--text-secondary); - font-size: var(--admin-font-size); - font-weight: 650; + color: var(--text-secondary); + font-size: var(--admin-font-size); + font-weight: 650; } .metric-icon { - display: grid; - width: var(--control-height); - height: var(--control-height); - place-items: center; - border-radius: var(--radius-control); - background: var(--primary-surface); - color: var(--primary); - transition: - background var(--motion-base) var(--ease-standard), - transform var(--motion-base) var(--ease-emphasized); + display: grid; + width: var(--control-height); + height: var(--control-height); + place-items: center; + border-radius: var(--radius-control); + background: var(--primary-surface); + color: var(--primary); + transition: + background var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); } .kpi-card:hover .metric-icon { - transform: scale(1.08) rotate(-4deg); + transform: scale(1.08) rotate(-4deg); } .metric-icon--success { - background: var(--success-surface); - color: var(--success); + background: var(--success-surface); + color: var(--success); } .metric-icon--warning { - background: var(--warning-surface); - color: var(--warning); + background: var(--warning-surface); + color: var(--warning); } .metric-icon--info { - background: var(--info-surface); - color: var(--info); + background: var(--info-surface); + color: var(--info); } .kpi-value { - margin-top: var(--space-5); - color: var(--text-primary); - font-size: var(--admin-font-size); - font-weight: 760; - line-height: 1; + margin-top: var(--space-5); + color: var(--text-primary); + font-size: var(--admin-font-size); + font-weight: 760; + line-height: 1; } .kpi-value span { - margin-left: var(--space-1); - font-size: var(--admin-font-size); - font-weight: 600; + margin-left: var(--space-1); + font-size: var(--admin-font-size); + font-weight: 600; } .kpi-sub { - margin-top: var(--space-3); - color: var(--text-tertiary); - font-size: var(--admin-font-size); + margin-top: var(--space-3); + color: var(--text-tertiary); + font-size: var(--admin-font-size); } .status-ok { - color: var(--success); + color: var(--success); } .status-warn { - color: var(--warning); + color: var(--warning); } .status-danger { - color: var(--danger); + color: var(--danger); } .status-badge { - display: inline-flex; - height: var(--status-height); - align-items: center; - gap: var(--space-2); - padding: 0 var(--space-3); - border: 1px solid var(--border) !important; - border-radius: var(--radius-pill) !important; - background: var(--neutral-surface) !important; - color: var(--text-secondary) !important; - font-size: var(--admin-font-size) !important; + display: inline-flex; + height: var(--status-height); + align-items: center; + gap: var(--space-2); + padding: 0 var(--space-3); + border: 1px solid var(--border) !important; + border-radius: var(--radius-pill) !important; + background: var(--neutral-surface) !important; + color: var(--text-secondary) !important; + font-size: var(--admin-font-size) !important; } .status-point { - width: var(--space-2); - height: var(--space-2); - border-radius: var(--radius-pill); - background: var(--stopped); + width: var(--space-2); + height: var(--space-2); + border-radius: var(--radius-pill); + background: var(--stopped); } .status-badge--running, .status-badge--succeeded { - border-color: var(--success-border) !important; - background: var(--success-surface) !important; - color: var(--success) !important; + border-color: var(--success-border) !important; + background: var(--success-surface) !important; + color: var(--success) !important; } .status-badge--running .status-point, .status-badge--succeeded .status-point { - background: var(--success); - box-shadow: 0 0 var(--space-3) var(--success-glow); + background: var(--success); + box-shadow: 0 0 var(--space-3) var(--success-glow); } .status-badge--warning { - border-color: var(--warning-border) !important; - background: var(--warning-surface) !important; - color: var(--warning) !important; + border-color: var(--warning-border) !important; + background: var(--warning-surface) !important; + color: var(--warning) !important; } .status-badge--warning .status-point { - background: var(--warning); - box-shadow: 0 0 var(--space-3) var(--warning-glow); + background: var(--warning); + box-shadow: 0 0 var(--space-3) var(--warning-glow); } .status-badge--error, .status-badge--danger { - border-color: var(--danger-border) !important; - background: var(--danger-surface) !important; - color: var(--danger) !important; + border-color: var(--danger-border) !important; + background: var(--danger-surface) !important; + color: var(--danger) !important; } .status-badge--error .status-point, .status-badge--danger .status-point { - background: var(--danger); - box-shadow: 0 0 var(--space-3) var(--danger-glow); + background: var(--danger); + box-shadow: 0 0 var(--space-3) var(--danger-glow); } .status-badge--stopped { - background: var(--neutral-surface) !important; - color: var(--stopped) !important; + background: var(--neutral-surface) !important; + color: var(--stopped) !important; } .panel-grid { - display: grid; - grid-template-columns: minmax(0, 1.35fr) minmax(340px, 0.65fr); - gap: var(--space-4); - margin-top: var(--space-4); + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(340px, 0.65fr); + gap: var(--space-4); + margin-top: var(--space-4); } .table-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-4); - padding: var(--space-4) var(--space-5); - border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + padding: var(--space-4) var(--space-5); + border-bottom: 1px solid var(--border); } .filter-row { - display: flex; - align-items: center; - gap: var(--space-2); + display: flex; + align-items: center; + gap: var(--space-2); } .filter-chip { - height: var(--status-height); - padding: 0 var(--space-3); - border: 1px solid var(--border); - border-radius: var(--radius-pill); - background: transparent; - color: var(--text-tertiary); - cursor: pointer; - font-size: var(--admin-font-size); - transition: - background var(--motion-fast) var(--ease-standard), - border-color var(--motion-fast) var(--ease-standard), - box-shadow var(--motion-fast) var(--ease-standard), - color var(--motion-fast) var(--ease-standard), - transform var(--motion-base) var(--ease-emphasized); + height: var(--status-height); + padding: 0 var(--space-3); + border: 1px solid var(--border); + border-radius: var(--radius-pill); + background: transparent; + color: var(--text-tertiary); + cursor: pointer; + font-size: var(--admin-font-size); + transition: + background var(--motion-fast) var(--ease-standard), + border-color var(--motion-fast) var(--ease-standard), + box-shadow var(--motion-fast) var(--ease-standard), + color var(--motion-fast) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); } .filter-chip:hover { - box-shadow: var(--shadow-soft); - transform: translateY(-1px); + box-shadow: var(--shadow-soft); + transform: translateY(-1px); } .filter-chip--active { - border-color: var(--primary-border-strong); - background: var(--primary-surface); - color: var(--text-primary); + border-color: var(--primary-border-strong); + background: var(--primary-surface); + color: var(--text-primary); } .muted { - color: var(--text-tertiary); + color: var(--text-tertiary); } .cell-stack { - display: grid; - min-width: 0; - gap: var(--space-1); + display: grid; + min-width: 0; + gap: var(--space-1); } .cell-stack > * { - min-width: 0; - overflow-wrap: anywhere; + min-width: 0; + overflow-wrap: anywhere; } .row-actions { - display: flex; - align-items: center; - gap: var(--space-2); + display: flex; + align-items: center; + gap: var(--space-2); } .empty-state { - padding: var(--space-8) var(--space-5); - color: var(--text-tertiary); - text-align: center; + padding: var(--space-8) var(--space-5); + color: var(--text-tertiary); + text-align: center; } .login-page { - display: grid; - min-height: 100vh; - place-items: center; - padding: var(--space-6); - background: - var(--login-gradient), - var(--bg-page); + display: grid; + min-height: 100vh; + place-items: center; + padding: var(--space-6); + background: var(--login-gradient), var(--bg-page); } .login-panel { - display: grid; - width: min(420px, 100%); - gap: var(--space-6); - padding: var(--space-7); - border: 1px solid var(--border); - border-radius: var(--radius-card); - background: var(--bg-card); - box-shadow: var(--shadow-panel); - animation: panel-enter var(--motion-slow) var(--ease-emphasized) both; + display: grid; + width: min(420px, 100%); + gap: var(--space-6); + padding: var(--space-7); + border: 1px solid var(--border); + border-radius: var(--radius-card); + background: var(--bg-card); + box-shadow: var(--shadow-panel); + animation: panel-enter var(--motion-slow) var(--ease-emphasized) both; } .login-brand { - display: flex; - align-items: center; - gap: var(--space-3); + display: flex; + align-items: center; + gap: var(--space-3); } .login-brand h1 { - margin: 0; - color: var(--text-primary); - font-size: var(--admin-font-size); + margin: 0; + color: var(--text-primary); + font-size: var(--admin-font-size); } .login-brand p { - margin: var(--space-1) 0 0; - color: var(--text-tertiary); - font-size: var(--admin-font-size); + margin: var(--space-1) 0 0; + color: var(--text-tertiary); + font-size: var(--admin-font-size); } .login-form, .form-drawer { - display: grid; - gap: var(--space-4); + display: grid; + gap: var(--space-4); } .form-drawer { - width: min(420px, 100vw); - padding: var(--space-6); + width: min(420px, 100vw); + padding: var(--space-6); } .form-drawer--wide { - width: min(560px, 100vw); + width: min(560px, 100vw); } .form-drawer h2 { - margin: 0 0 var(--space-1); - color: var(--text-primary); - font-size: var(--admin-font-size); + margin: 0 0 var(--space-1); + color: var(--text-primary); + font-size: var(--admin-font-size); } .form-drawer__actions { - display: flex; - justify-content: flex-end; - gap: var(--space-3); - padding-top: var(--space-2); + display: flex; + justify-content: flex-end; + gap: var(--space-3); + padding-top: var(--space-2); } .side-drawer { - display: flex; - width: min(420px, 100vw); - min-height: 100%; - flex-direction: column; - gap: var(--space-4); - padding: var(--space-6); - background: var(--bg-card); + display: flex; + width: min(420px, 100vw); + min-height: 100%; + flex-direction: column; + gap: var(--space-4); + padding: var(--space-6); + background: var(--bg-card); } .side-drawer--wide { - width: min(560px, 100vw); + width: min(560px, 100vw); } .side-drawer__header { - display: flex; - min-height: var(--control-height); - align-items: center; - justify-content: space-between; - gap: var(--space-3); + display: flex; + min-height: var(--control-height); + align-items: center; + justify-content: space-between; + gap: var(--space-3); } .side-drawer__title { - margin: 0; - color: var(--text-primary); - font-size: var(--admin-font-size); - font-weight: 800; + margin: 0; + color: var(--text-primary); + font-size: var(--admin-font-size); + font-weight: 800; } .side-drawer__body { - display: grid; - gap: var(--space-4); + display: grid; + gap: var(--space-4); } .side-drawer__actions { - display: flex; - justify-content: flex-end; - gap: var(--space-3); - margin-top: auto; - padding-top: var(--space-2); + display: flex; + justify-content: flex-end; + gap: var(--space-3); + margin-top: auto; + padding-top: var(--space-2); } .form-section-title { - color: var(--text-secondary); - font-size: var(--admin-font-size); - font-weight: 700; + color: var(--text-secondary); + font-size: var(--admin-font-size); + font-weight: 700; } .permission-menu-list { - display: grid; - gap: var(--space-3); + display: grid; + gap: var(--space-3); } .permission-menu-section { - display: grid; - gap: var(--space-2); - padding: var(--space-3); - border: 1px solid var(--border); - border-radius: var(--radius-card); - background: var(--bg-card); + display: grid; + gap: var(--space-2); + padding: var(--space-3); + border: 1px solid var(--border); + border-radius: var(--radius-card); + background: var(--bg-card); } .permission-menu-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-3); - min-height: var(--control-height); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + min-height: var(--control-height); } .permission-menu-title { - color: var(--text-primary); - font-size: var(--admin-font-size); - font-weight: 700; + color: var(--text-primary); + font-size: var(--admin-font-size); + font-weight: 700; } .permission-menu-actions { - display: inline-flex; - align-items: center; - gap: var(--space-3); + display: inline-flex; + align-items: center; + gap: var(--space-3); } .permission-menu-toggle { - display: inline-flex; - align-items: center; - gap: var(--space-2); - color: var(--text-secondary); - font-size: var(--admin-font-size); - font-weight: 600; - white-space: nowrap; + display: inline-flex; + align-items: center; + gap: var(--space-2); + color: var(--text-secondary); + font-size: var(--admin-font-size); + font-weight: 600; + white-space: nowrap; } .permission-menu-toggle .MuiCheckbox-root, .permission-check .MuiCheckbox-root { - padding: 0; + padding: 0; } .permission-menu-count { - color: var(--text-tertiary); - font-size: var(--admin-font-size); - white-space: nowrap; + color: var(--text-tertiary); + font-size: var(--admin-font-size); + white-space: nowrap; } .permission-section { - display: grid; - gap: var(--space-3); + display: grid; + gap: var(--space-3); } .permission-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: var(--space-2); + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--space-2); } .permission-grid--compact { - gap: var(--space-2); + gap: var(--space-2); } .permission-check { - display: flex; - align-items: center; - gap: var(--space-2); - min-height: var(--control-height); - padding: 0 var(--space-3); - border: 1px solid var(--border); - border-radius: var(--radius-control); - background: var(--bg-card-strong); - color: var(--text-secondary); - font-size: var(--admin-font-size); + display: flex; + align-items: center; + gap: var(--space-2); + min-height: var(--control-height); + padding: 0 var(--space-3); + border: 1px solid var(--border); + border-radius: var(--radius-control); + background: var(--bg-card-strong); + color: var(--text-secondary); + font-size: var(--admin-font-size); } .batch-actions { - display: flex; - gap: var(--space-2); + display: flex; + gap: var(--space-2); } .pagination-bar { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-3); - margin-top: var(--space-3); - color: var(--text-tertiary); - font-size: var(--admin-font-size); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + margin-top: var(--space-3); + color: var(--text-tertiary); + font-size: var(--admin-font-size); } .pagination-bar__actions { - display: flex; - align-items: center; - gap: var(--space-2); + display: flex; + align-items: center; + gap: var(--space-2); } .data-state { - display: grid; - min-height: 180px; - place-items: center; - gap: var(--space-3); - border: 1px solid var(--border); - border-radius: var(--radius-card); - background: var(--bg-card); - color: var(--text-tertiary); + display: grid; + min-height: 180px; + place-items: center; + gap: var(--space-3); + border: 1px solid var(--border); + border-radius: var(--radius-card); + background: var(--bg-card); + color: var(--text-tertiary); } .data-state--loading { - padding: var(--space-4); + padding: var(--space-4); } .data-state--loading .page-skeleton__block { - min-height: 220px; + min-height: 220px; } .data-state__title { - color: var(--text-secondary); - font-size: var(--admin-font-size); - font-weight: 650; + color: var(--text-secondary); + font-size: var(--admin-font-size); + font-weight: 650; } .admin-table { - --admin-table-cell-x: var(--space-2); - display: flex; - flex-direction: column; - width: max(100%, var(--admin-table-min-width, 900px)); - min-height: 100%; - min-width: max(100%, var(--admin-table-min-width, 900px)); + --admin-table-cell-x: var(--space-4); + display: flex; + flex-direction: column; + width: max(100%, var(--admin-table-min-width, 900px)); + min-height: 100%; + min-width: max(100%, var(--admin-table-min-width, 900px)); } .admin-row { - display: grid; - flex: 0 0 auto; - align-items: center; - grid-template-columns: var(--admin-table-columns); - min-height: var(--table-row-height); - padding: 0; - border-bottom: 1px solid var(--row-border); - color: var(--text-secondary); - column-gap: 0; - font-size: var(--admin-font-size); - animation: row-enter var(--motion-slow) var(--ease-emphasized) both; + display: grid; + flex: 0 0 auto; + align-items: center; + grid-template-columns: var(--admin-table-columns); + min-height: var(--table-row-height); + padding: 0; + border-bottom: 1px solid var(--row-border); + color: var(--text-secondary); + column-gap: 0; + font-size: var(--admin-font-size); + animation: row-enter var(--motion-slow) var(--ease-emphasized) both; } .admin-row[role="button"] { - cursor: pointer; + cursor: pointer; } .admin-row--head { - min-height: var(--table-head-height); - background: var(--bg-card-strong); - color: var(--text-tertiary); - font-size: var(--admin-font-size); - font-weight: 650; + min-height: var(--table-head-height); + background: var(--bg-card-strong); + color: var(--text-tertiary); + font-size: var(--admin-font-size); + font-weight: 650; } .admin-cell { - box-sizing: border-box; - min-width: 0; - padding: 0 var(--admin-table-cell-x); - overflow-wrap: anywhere; + box-sizing: border-box; + min-width: 0; + padding: 0 var(--admin-table-cell-x); + overflow-wrap: anywhere; } .admin-cell:first-child { - padding-left: var(--admin-table-cell-x); + padding-left: var(--admin-table-cell-x); } .admin-cell:last-child { - padding-right: var(--admin-table-cell-x); + padding-right: var(--admin-table-cell-x); } .admin-cell--head { - position: relative; - display: flex; - min-height: var(--table-head-height); - align-items: center; - padding-right: calc(var(--admin-table-cell-x) + 12px); + position: relative; + display: flex; + min-height: var(--table-head-height); + align-items: center; + padding-right: calc(var(--admin-table-cell-x) + 12px); } .admin-cell--head:last-child { - padding-right: calc(var(--admin-table-cell-x) + 12px); + padding-right: calc(var(--admin-table-cell-x) + 12px); } .admin-cell__head-label { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.admin-cell__head-filter { + color: var(--primary); + font-weight: 700; } .admin-cell__head-trigger { - display: inline-flex; - max-width: 100%; - min-width: 0; - align-items: center; - gap: var(--space-1); - padding: 0; - border: 0; - background: transparent; - color: inherit; - cursor: pointer; - font: inherit; - font-weight: inherit; + display: inline-flex; + max-width: 100%; + min-width: 0; + align-items: center; + gap: var(--space-1); + padding: 0; + border: 0; + background: transparent; + color: inherit; + cursor: pointer; + font: inherit; + font-weight: inherit; } .admin-cell__head-trigger:hover, .admin-cell__head-trigger--active { - color: var(--text-primary); + color: var(--text-primary); } .admin-cell__head-icon { - flex: 0 0 auto; - color: var(--text-tertiary); - font-size: 16px; + flex: 0 0 auto; + color: var(--text-tertiary); + font-size: 16px; } .admin-cell__head-trigger--active .admin-cell__head-icon { - color: var(--primary); + color: var(--primary); } .admin-cell--fixed-left, .admin-cell--fixed-right { - position: sticky; - z-index: 2; - display: flex; - min-height: var(--table-row-height); - align-items: center; - background: var(--bg-card); + position: sticky; + z-index: 2; + display: flex; + min-height: var(--table-row-height); + align-items: center; + background: var(--bg-card); } .admin-cell--fixed-left { - left: 0; - box-shadow: 10px 0 18px -18px rgb(30 41 59 / 0.4); + left: 0; + box-shadow: 10px 0 18px -18px rgb(30 41 59 / 0.4); } .admin-cell--fixed-right { - right: 0; - box-shadow: -10px 0 18px -18px rgb(30 41 59 / 0.4); + right: 0; + box-shadow: -10px 0 18px -18px rgb(30 41 59 / 0.4); } .admin-row--head .admin-cell--fixed-left, .admin-row--head .admin-cell--fixed-right { - z-index: 4; - min-height: var(--table-head-height); - background: var(--bg-card-strong); + z-index: 4; + min-height: var(--table-head-height); + background: var(--bg-card-strong); } .admin-column-resizer { - position: absolute; - top: 9px; - right: 0; - bottom: 9px; - width: 10px; - padding: 0; - border: 0; - background: transparent; - cursor: col-resize; - touch-action: none; + position: absolute; + top: 9px; + right: 0; + bottom: 9px; + width: 10px; + padding: 0; + border: 0; + background: transparent; + cursor: col-resize; + touch-action: none; } .admin-column-resizer::after { - position: absolute; - top: 0; - right: 4px; - bottom: 0; - width: 1px; - border-radius: var(--radius-pill); - background: transparent; - content: ""; - transition: - background var(--motion-fast) var(--ease-standard), - box-shadow var(--motion-fast) var(--ease-standard); + position: absolute; + top: 0; + right: 4px; + bottom: 0; + width: 1px; + border-radius: var(--radius-pill); + background: transparent; + content: ""; + transition: + background var(--motion-fast) var(--ease-standard), + box-shadow var(--motion-fast) var(--ease-standard); } .admin-column-resizer:hover::after, .admin-cell--resizing .admin-column-resizer::after { - background: var(--primary); - box-shadow: 0 0 0 1px var(--primary-surface); + background: var(--primary); + box-shadow: 0 0 0 1px var(--primary-surface); } .admin-table--resizing { - cursor: col-resize; - user-select: none; + cursor: col-resize; + user-select: none; } .admin-table-filter-popover .MuiPopover-paper { - width: 240px; - overflow: hidden; - border: 1px solid var(--border); - border-radius: var(--radius-card); - background: var(--bg-flyout); - box-shadow: var(--shadow-panel); + width: 240px; + overflow: hidden; + border: 1px solid var(--border); + border-radius: var(--radius-card); + background: var(--bg-flyout); + box-shadow: var(--shadow-panel); } .admin-table-filter { - display: grid; - gap: var(--space-2); - padding: var(--space-2); + display: grid; + gap: var(--space-2); + padding: var(--space-2); } .admin-table-filter__search.MuiTextField-root { - width: 100%; + width: 100%; } .admin-table-filter__options { - max-height: 280px; - overflow: auto; + max-height: 280px; + overflow: auto; +} + +.admin-table-filter__actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--space-2); } .admin-table-filter__options .MuiMenuItem-root { - min-height: 34px; - border-radius: var(--radius-control); - color: var(--text-secondary); + min-height: 34px; + border-radius: var(--radius-control); + color: var(--text-secondary); } .admin-table-filter__options .MuiMenuItem-root.Mui-selected { - background: var(--primary-hover); - color: var(--text-primary); + background: var(--primary-hover); + color: var(--text-primary); } .admin-table-filter__empty { - padding: var(--space-4) var(--space-2); - color: var(--text-tertiary); - font-size: var(--admin-font-size); - text-align: center; + padding: var(--space-4) var(--space-2); + color: var(--text-tertiary); + font-size: var(--admin-font-size); + text-align: center; } .admin-table .empty-state--table { - flex: 1; + flex: 1; } .table-frame { - display: flex; - flex: 1; - min-height: 0; - flex-direction: column; - background: var(--bg-card); + display: flex; + flex: 1; + min-height: 0; + flex-direction: column; + background: var(--bg-card); } .table-frame .table-scroll { - flex: 1; - min-height: 0; - overflow: auto; + flex: 1; + min-height: 0; + overflow: auto; } .table-frame .admin-row--head { - position: sticky; - top: 0; - z-index: 1; + position: sticky; + top: 0; + z-index: 1; } .content-panel, .table-frame, .table-card { - overflow: hidden; - border: 1px solid var(--border); - border-radius: var(--radius-card); - background: var(--bg-card); + overflow: hidden; + border: 1px solid var(--border); + border-radius: var(--radius-card); + background: var(--bg-card); } .content-panel { - display: flex; - flex: 1; - flex-direction: column; - min-height: 520px; + display: flex; + flex: 1; + flex-direction: column; + min-height: 520px; } .empty-state--table { - min-height: 340px; - align-content: center; + min-height: 340px; + align-content: center; } .empty-state__title { - color: var(--text-secondary); - font-weight: 650; + color: var(--text-secondary); + font-weight: 650; } .empty-state__action { - display: flex; - justify-content: center; + display: flex; + justify-content: center; } diff --git a/src/theme.js b/src/theme.js index 4cd96ad..ced98d6 100644 --- a/src/theme.js +++ b/src/theme.js @@ -130,6 +130,13 @@ export const theme = createTheme({ }, input: { fontSize: 14, + "&[type='number']": { + MozAppearance: "textfield" + }, + "&[type='number']::-webkit-outer-spin-button, &[type='number']::-webkit-inner-spin-button": { + WebkitAppearance: "none", + margin: 0 + }, "&::placeholder": { color: token("text-tertiary"), opacity: 1 @@ -237,6 +244,10 @@ export const theme = createTheme({ styleOverrides: { root: { fontSize: 14 + }, + asterisk: { + color: token("danger"), + marginLeft: 2 } } },