diff --git a/backend/api/cmd/cli/main.go b/backend/api/cmd/cli/main.go index 1e91258..fd31d90 100644 --- a/backend/api/cmd/cli/main.go +++ b/backend/api/cmd/cli/main.go @@ -188,7 +188,8 @@ func writeDeliveryResourceContract(output io.Writer) error { contracts := make([]contract, 0, len(expected)) for _, item := range expected { contracts = append(contracts, contract{ - Domain: item.Domain, Name: item.Name, Path: item.Path, PageKind: item.PageKind, Mode: item.Mode, + Domain: item.Domain, Name: item.Name, Path: item.Path, + PageKind: item.PageKind, Mode: item.Mode, SearchFields: item.SearchFields, }) } return json.NewEncoder(output).Encode(manifest{Resources: contracts, Routes: routes}) diff --git a/backend/api/internal/logic/common/base.go b/backend/api/internal/logic/common/base.go index 1e663ce..1f3cfff 100644 --- a/backend/api/internal/logic/common/base.go +++ b/backend/api/internal/logic/common/base.go @@ -119,7 +119,7 @@ func ListPageFiltered[T any](ctx *gin.Context, filter func(*gorm.DB) *gorm.DB) { var keywordSafeColumns = map[string]bool{ "code": true, "name": true, "username": true, "display_name": true, "role_code": true, "delivery_code": true, "work_status": true, - "credential_type": true, "device_no": true, "model": true, + "credential_type": true, "credential_no": true, "device_no": true, "model": true, "online_status": true, "rule_code": true, "action": true, "event_code": true, "title": true, "result": true, "product_code": true, "value": true, "order_no": true, diff --git a/backend/api/internal/logic/common/keyword_search.go b/backend/api/internal/logic/common/keyword_search.go index 910c9e0..94b5de7 100644 --- a/backend/api/internal/logic/common/keyword_search.go +++ b/backend/api/internal/logic/common/keyword_search.go @@ -18,7 +18,7 @@ type KeywordSearchKind string const ( // KeywordSearchText 按数据库原始文本执行不区分大小写的包含匹配。 KeywordSearchText KeywordSearchKind = "text" - // KeywordSearchEnum 仅按页面展示的中文枚举名称匹配,不暴露内部英文编码。 + // KeywordSearchEnum 同时按页面中文名称和内部稳定编码匹配。 KeywordSearchEnum KeywordSearchKind = "enum" ) @@ -85,7 +85,7 @@ func useConfiguredKeywordSearch(ctx *gin.Context) bool { } // configuredKeywordConditions 将用户关键字编译为参数化 SQL 条件。 -// 枚举字段只接受中文展示名称,普通文本字段保持原有包含匹配行为。 +// 枚举字段接受中文展示名称和内部编码,普通文本字段保持原有包含匹配行为。 func configuredKeywordConditions(model any, keyword string) ([]string, []any) { fields := ConfiguredKeywordSearchFields(model) conditions := make([]string, 0, len(fields)) @@ -113,7 +113,8 @@ func configuredKeywordConditions(model any, keyword string) ([]string, []any) { func matchingKeywordEnumValues(values []KeywordSearchValue, keyword string) []string { matched := make([]string, 0, len(values)) for _, value := range values { - if strings.Contains(strings.ToLower(value.Label), keyword) { + if strings.Contains(strings.ToLower(value.Label), keyword) || + strings.Contains(strings.ToLower(value.Value), keyword) { matched = append(matched, value.Value) } } diff --git a/backend/api/internal/logic/common/keyword_search_test.go b/backend/api/internal/logic/common/keyword_search_test.go index 1d9d90e..8d3f4be 100644 --- a/backend/api/internal/logic/common/keyword_search_test.go +++ b/backend/api/internal/logic/common/keyword_search_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -// keywordSearchEnumModel 用于验证中文枚举别名不会退化为英文编码搜索。 +// keywordSearchEnumModel 用于验证中文枚举别名和稳定编码均可搜索。 type keywordSearchEnumModel struct { RoleCode string `gorm:"column:role_code"` } @@ -48,8 +48,9 @@ func TestConfiguredKeywordConditionsMatchChineseEnumLabels(t *testing.T) { } conditions, arguments = configuredKeywordConditions(&keywordSearchEnumModel{}, "delivery") - if len(conditions) != 0 || len(arguments) != 0 { - t.Fatalf("英文枚举编码不应继续可搜:conditions=%v arguments=%v", conditions, arguments) + if !reflect.DeepEqual(conditions, []string{`"role_code" IN (?)`}) || + !reflect.DeepEqual(arguments, []any{"delivery"}) { + t.Fatalf("英文枚举编码应保持可搜:conditions=%v arguments=%v", conditions, arguments) } } diff --git a/backend/api/internal/logic/delivery/resource_contract.go b/backend/api/internal/logic/delivery/resource_contract.go index 83a302d..b705cc4 100644 --- a/backend/api/internal/logic/delivery/resource_contract.go +++ b/backend/api/internal/logic/delivery/resource_contract.go @@ -1,33 +1,44 @@ package delivery +import "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common" + type ResourceContract struct { - Domain string `json:"domain"` - Name string `json:"name"` - Path string `json:"path"` - PageKind string `json:"pageKind"` - Mode string `json:"mode"` + Domain string `json:"domain"` + Name string `json:"name"` + Path string `json:"path"` + PageKind string `json:"pageKind"` + Mode string `json:"mode"` + SearchFields []common.KeywordSearchField `json:"searchFields,omitempty"` } func ExpectedResources() []ResourceContract { items := []ResourceContract{ - {"profile", "delivery_profile", "/delivery_profile", "list", "readonly"}, - {"staff", "staff_account", "/staff_account", "list", "writable"}, - {"staff", "staff_credential", "/staff_credential", "list", "writable"}, - {"user", "user_account", "/user_account", "list", "writable"}, - {"user", "user_address", "/user_address", "list", "writable"}, - {"contract", "gasorder_contract", "/gasorder_contract", "list", "managed"}, - {"contract", "gasorder_contract_product", "/gasorder_contract_product", "list", "append_only"}, - {"contract", "gasorder_contract_revision", "/gasorder_contract_revision", "list", "readonly"}, - {"contract", "product_info", "/product_info", "list", "readonly"}, - {"gasorder", "gasorder_basic", "/gasorder_basic", "list", "append_only"}, - {"finance", "wallet_basic", "/wallet_basic", "list", "readonly"}, - {"finance", "wallet_bank", "/wallet_bank", "list", "readonly"}, - {"finance", "payment_order", "/payment_order", "list", "readonly"}, - {"finance", "wallet_record", "/wallet_record", "list", "readonly"}, - {"finance", "payment_refund", "/payment_refund", "list", "readonly"}, - {"finance", "wallet_recharge", "/wallet_recharge", "list", "append_only"}, - {"finance", "wallet_apply_cash", "/wallet_apply_cash", "list", "append_only"}, - {"finance", "fin_settlement", "/fin_settlement", "list", "readonly"}, + resourceContract("profile", "delivery_profile", "readonly"), + resourceContract("staff", "staff_account", "writable"), + resourceContract("staff", "staff_credential", "writable"), + resourceContract("user", "user_account", "writable"), + resourceContract("user", "user_address", "writable"), + resourceContract("contract", "gasorder_contract", "managed"), + resourceContract("contract", "gasorder_contract_product", "append_only"), + resourceContract("contract", "gasorder_contract_revision", "readonly"), + resourceContract("contract", "product_info", "readonly"), + resourceContract("gasorder", "gasorder_basic", "append_only"), + resourceContract("finance", "wallet_basic", "readonly"), + resourceContract("finance", "wallet_bank", "readonly"), + resourceContract("finance", "payment_order", "readonly"), + resourceContract("finance", "wallet_record", "readonly"), + resourceContract("finance", "payment_refund", "readonly"), + resourceContract("finance", "wallet_recharge", "append_only"), + resourceContract("finance", "wallet_apply_cash", "append_only"), + resourceContract("finance", "fin_settlement", "readonly"), } return items } + +// resourceContract 创建配送端标准资源契约,并附加显式搜索字段。 +func resourceContract(domain, name, mode string) ResourceContract { + return ResourceContract{ + Domain: domain, Name: name, Path: "/" + name, PageKind: "list", Mode: mode, + SearchFields: resourceSearchFields(name), + } +} diff --git a/backend/api/internal/logic/delivery/resource_contract_test.go b/backend/api/internal/logic/delivery/resource_contract_test.go new file mode 100644 index 0000000..740d008 --- /dev/null +++ b/backend/api/internal/logic/delivery/resource_contract_test.go @@ -0,0 +1,26 @@ +// 功能描述:验证配送端资源搜索契约与页面能力保持一致。版本:v1.0.0。 +package delivery + +import "testing" + +// TestExpectedResourcesExposeSearchContract 验证有效搜索与隐藏搜索的资源边界。 +func TestExpectedResourcesExposeSearchContract(t *testing.T) { + resources := ExpectedResources() + byName := make(map[string]ResourceContract, len(resources)) + for _, resource := range resources { + byName[resource.Name] = resource + } + credential := byName["staff_credential"].SearchFields + if len(credential) != 2 || credential[0].Key != "credential_type" || credential[1].Key != "credential_no" { + t.Fatalf("人员资质搜索契约不完整:%#v", credential) + } + for _, name := range []string{"delivery_profile", "user_address", "wallet_bank"} { + if len(byName[name].SearchFields) != 0 { + t.Fatalf("%s 不应显示无效搜索:%#v", name, byName[name].SearchFields) + } + } + channel := byName["payment_order"].SearchFields[2] + if channel.Kind != "enum" || len(channel.Values) != 3 { + t.Fatalf("支付渠道中英文搜索契约不完整:%#v", channel) + } +} diff --git a/backend/api/internal/logic/delivery/resource_search.go b/backend/api/internal/logic/delivery/resource_search.go new file mode 100644 index 0000000..3fc93b7 --- /dev/null +++ b/backend/api/internal/logic/delivery/resource_search.go @@ -0,0 +1,62 @@ +// Package delivery 定义配送点管理端公开的资源搜索契约。 +// 版本:v1.0.0 +package delivery + +import ( + "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common" + "git.apinb.com/heqiapp/platforms/backend/api/internal/models" +) + +// deliveryResourceSearchFields 只公开页面能够解释且服务端确实执行的搜索字段。 +var deliveryResourceSearchFields = map[string][]common.KeywordSearchField{ + "staff_account": {searchText("username")}, + "staff_credential": {searchText("credential_type"), searchText("credential_no")}, + "user_account": {searchText("username")}, + "gasorder_contract": {searchText("contract_no"), searchText("title")}, + "gasorder_contract_product": {searchText("product_code"), searchText("product_type_name")}, + "gasorder_contract_revision": {searchEnum("action", searchOption("activate", "启用"), searchOption("renew", "续签"), searchOption("terminate", "终止"))}, + "product_info": {searchText("code"), searchText("name")}, + "gasorder_basic": {searchText("request_no"), searchEnum("creator_type", + searchOption("user", "用户"), searchOption("staff", "工作人员"), + searchOption("delivery", "配送点"), searchOption("gas", "气站"))}, + "payment_order": {searchText("payment_no"), searchText("request_no"), searchEnum("channel", + searchOption("wechat", "微信"), searchOption("alipay", "支付宝"), searchOption("mock", "模拟支付"))}, + "wallet_record": {searchText("record_no"), searchText("request_no")}, + "payment_refund": {searchText("refund_no"), searchText("request_no")}, + "wallet_recharge": {searchText("record_no"), searchText("request_no")}, + "wallet_apply_cash": {searchText("cash_no"), searchText("request_no"), searchEnum("channel", + searchOption("bank", "银行卡"), searchOption("alipay", "支付宝"), searchOption("wechat", "微信"))}, + "fin_settlement": {searchText("settlement_no")}, +} + +// init 只注册平台总后台尚未注册的模型;共享模型继续复用全局策略。 +func init() { + common.RegisterKeywordSearchPolicy(&models.GasorderContractProduct{}, deliveryResourceSearchFields["gasorder_contract_product"]) + common.RegisterKeywordSearchPolicy(&models.GasorderContractRevision{}, deliveryResourceSearchFields["gasorder_contract_revision"]) + common.RegisterKeywordSearchPolicy(&models.PaymentOrder{}, deliveryResourceSearchFields["payment_order"]) + common.RegisterKeywordSearchPolicy(&models.WalletRecord{}, deliveryResourceSearchFields["wallet_record"]) +} + +// resourceSearchFields 返回搜索契约副本,防止调用方修改全局定义。 +func resourceSearchFields(name string) []common.KeywordSearchField { + fields := deliveryResourceSearchFields[name] + result := make([]common.KeywordSearchField, 0, len(fields)) + for _, field := range fields { + copied := field + copied.Values = append([]common.KeywordSearchValue(nil), field.Values...) + result = append(result, copied) + } + return result +} + +func searchText(key string) common.KeywordSearchField { + return common.KeywordSearchField{Key: key, Kind: common.KeywordSearchText} +} + +func searchEnum(key string, values ...common.KeywordSearchValue) common.KeywordSearchField { + return common.KeywordSearchField{Key: key, Kind: common.KeywordSearchEnum, Values: values} +} + +func searchOption(value, label string) common.KeywordSearchValue { + return common.KeywordSearchValue{Value: value, Label: label} +} diff --git a/backend/api/internal/logic/delivery/staff.go b/backend/api/internal/logic/delivery/staff.go index 4b1a83a..d2b8131 100644 --- a/backend/api/internal/logic/delivery/staff.go +++ b/backend/api/internal/logic/delivery/staff.go @@ -1,6 +1,7 @@ package delivery import ( + "strings" "time" "git.apinb.com/bsm-sdk/core/errcode" @@ -187,17 +188,38 @@ func ArchiveStaff(ctx *gin.Context) { } func credentialQuery(point models.DeliveryBasic) *gorm.DB { - return common.ActiveRecords(db().Model(&models.StaffCredential{})). + return credentialQueryWithDB(db(), point) +} + +// credentialQueryWithDB 固定资质所属配送点、气站和配送角色范围。 +func credentialQueryWithDB(databaseService *gorm.DB, point models.DeliveryBasic) *gorm.DB { + return common.ActiveRecords(databaseService.Model(&models.StaffCredential{})). Joins("JOIN staff_account ON staff_account.id = staff_credential.staff_account_id"). Where("staff_account.delivery_basic_id = ? AND staff_account.gas_basic_id = ? AND staff_account.role_code = ?", point.ID, point.GasBasicID, "delivery") } +// scopedCredentialQuery 将资质列表进一步锁定到已验证的配送人员。 +func scopedCredentialQuery(databaseService *gorm.DB, point models.DeliveryBasic, staffID uint64) *gorm.DB { + return credentialQueryWithDB(databaseService, point). + Where("staff_credential.staff_account_id = ?", staffID) +} + func ListCredential(ctx *gin.Context) { point, _, ok := currentScope(ctx) - if ok { - listScoped(ctx, &models.StaffCredential{}, credentialQuery(point), "staff_credential.created_at desc") + if !ok { + return } + staffIdentity := strings.TrimSpace(ctx.Query("staff_account_identity")) + if staffIdentity == "" { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } + staff, found := scopedStaff(ctx, staffIdentity, point) + if !found { + return + } + listScoped(ctx, &models.StaffCredential{}, scopedCredentialQuery(db(), point, staff.ID), "staff_credential.created_at desc") } func GetCredential(ctx *gin.Context) { @@ -260,9 +282,13 @@ func UpdateCredential(ctx *gin.Context) { if !ok { return } + if staff.ID != existing.StaffAccountID { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } if err := db().Model(&existing).Updates(map[string]any{ - "staff_account_id": staff.ID, "credential_type": request.CredentialType, - "credential_no": request.CredentialNo, "expired_at": request.ExpiredAt, + "credential_type": request.CredentialType, "credential_no": request.CredentialNo, + "expired_at": request.ExpiredAt, }).Error; err != nil { infra.Response.Error(ctx, err) return diff --git a/backend/api/internal/logic/delivery/staff_test.go b/backend/api/internal/logic/delivery/staff_test.go index a5ad285..5b98a8a 100644 --- a/backend/api/internal/logic/delivery/staff_test.go +++ b/backend/api/internal/logic/delivery/staff_test.go @@ -31,3 +31,24 @@ func TestDeliveryStaffQueryKeepsRoleScope(t *testing.T) { } } } + +// TestScopedCredentialQueryKeepsOwnerAndRoleScope 验证资质列表同时限制人员和配送角色。 +func TestScopedCredentialQueryKeepsOwnerAndRoleScope(t *testing.T) { + connection, _, err := sqlmock.New() + if err != nil { + t.Fatalf("创建 SQL mock 失败:%v", err) + } + t.Cleanup(func() { _ = connection.Close() }) + databaseService, err := gorm.Open(postgres.New(postgres.Config{Conn: connection}), &gorm.Config{DryRun: true}) + if err != nil { + t.Fatalf("打开 GORM 失败:%v", err) + } + point := models.DeliveryBasic{Entity: models.Entity{ID: 22}, GasBasicID: 11} + statement := scopedCredentialQuery(databaseService, point, 33). + Find(&[]models.StaffCredential{}).Statement.SQL.String() + for _, required := range []string{"delivery_basic_id", "gas_basic_id", "role_code", "staff_account_id"} { + if !strings.Contains(statement, required) { + t.Fatalf("配送人员资质范围缺少 %s:%s", required, statement) + } + } +} diff --git a/backend/api/internal/logic/platform/resource_search.go b/backend/api/internal/logic/platform/resource_search.go index 111551c..1a5bb21 100644 --- a/backend/api/internal/logic/platform/resource_search.go +++ b/backend/api/internal/logic/platform/resource_search.go @@ -19,7 +19,7 @@ var resourceSearchDefinitions = []resourceSearchDefinition{ searchDefinition("delivery_basic", &models.DeliveryBasic{}, text("delivery_code"), text("name")), searchDefinition("delivery_account", &models.DeliveryAccount{}, text("username"), text("display_name"), enum("role_code", value("admin", "配送点管理员"))), searchDefinition("staff_account", &models.StaffAccount{}, text("username"), enum("role_code", value("installer", "安装人员"), value("delivery", "配送人员"), value("operations", "运维人员"))), - searchDefinition("staff_credential", &models.StaffCredential{}, text("credential_type")), + searchDefinition("staff_credential", &models.StaffCredential{}, text("credential_type"), text("credential_no")), searchDefinition("user_account", &models.UserAccount{}, text("username")), searchDefinition("producer_account", &models.ProducerAccount{}, text("name")), searchDefinition("product_type", &models.ProductType{}, text("code"), text("name")), @@ -36,8 +36,8 @@ var resourceSearchDefinitions = []resourceSearchDefinition{ searchDefinition("platform_account", &models.PlatformAccount{}, text("username"), text("display_name")), searchDefinition("platform_role", &models.PlatformRole{}, text("role_code"), text("name"), enum("location_scope", value("standard", "脱敏坐标"), value("precise", "精确坐标"))), searchDefinition("wallet_basic", &models.WalletBasic{}, text("owner_type")), - searchDefinition("payment_refund", &models.PaymentRefund{}, text("refund_no")), - searchDefinition("wallet_apply_cash", &models.WalletApplyCash{}, text("cash_no")), + searchDefinition("payment_refund", &models.PaymentRefund{}, text("refund_no"), text("request_no")), + searchDefinition("wallet_apply_cash", &models.WalletApplyCash{}, text("cash_no"), text("request_no"), enum("channel", value("bank", "银行卡"), value("alipay", "支付宝"), value("wechat", "微信"))), } func init() { diff --git a/backend/api/internal/routers/delivery.go b/backend/api/internal/routers/delivery.go index 1349bf9..4cdd345 100644 --- a/backend/api/internal/routers/delivery.go +++ b/backend/api/internal/routers/delivery.go @@ -4,6 +4,7 @@ import ( "fmt" sdkmiddleware "git.apinb.com/bsm-sdk/core/middleware" + "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common" deliverylogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/delivery" "github.com/gin-gonic/gin" ) @@ -16,6 +17,7 @@ func RegisterDelivery(serviceKey string, engine *gin.Engine) { protected := engine.Group(basePath) protected.Use(sdkmiddleware.JwtAuth(true)) protected.Use(deliverylogic.RequireDeliveryAdmin()) + protected.Use(common.EnableConfiguredKeywordSearch()) protected.GET("/auth/profile", deliverylogic.CurrentProfile) protected.PUT("/auth/password", deliverylogic.ChangePassword) protected.GET("/delivery_menu", deliverylogic.ListMenu) diff --git a/docs/07-配送点管理系统需求.md b/docs/07-配送点管理系统需求.md index f030a04..235236b 100644 --- a/docs/07-配送点管理系统需求.md +++ b/docs/07-配送点管理系统需求.md @@ -91,6 +91,9 @@ Global: - 新建人员的 `role_code` 由服务端固定为 `delivery`。 - 人员的气站及配送点归属由服务端固定为当前范围。 - 资质支持新增、编辑、启停和归档。 +- 人员资质必须从配送人员列表进入,服务端按当前气站、配送点、配送角色和人员标识强制过滤;缺少、无效或越权人员上下文时不得返回全部资质。 +- 资质列表显示服务端回查的人员姓名和可复制唯一标识;新建自动预填所属人员,创建和编辑均不得更换所属人员。 +- 标准资源搜索必须由后端显式字段契约驱动;没有有效搜索字段时不显示搜索区,枚举字段支持中文名称和内部稳定编码。 - 删除均为归档;存在未完成订单时禁止归档人员。 - 不允许创建安装、运维、仓管、调度员或质控角色账号。 - 配送人员列表不得展示数据库自增 ID;联系电话之后显示 32px 圆形头像缩略图,头像仅通过当前配送点 JWT 范围内的受保护接口懒加载。无头像或读取异常时显示默认头像,不得从列表响应读取或暴露头像 URI。 diff --git a/docs/操作日志_配送点独立资源页面_20260822.md b/docs/操作日志_配送点独立资源页面_20260822.md index 8cf3049..f89b83e 100644 --- a/docs/操作日志_配送点独立资源页面_20260822.md +++ b/docs/操作日志_配送点独立资源页面_20260822.md @@ -22,6 +22,10 @@ 10. 配送人员和用户账户列表增加 32px 受控头像缩略图,复用 5173 的懒加载、并发 6、当前页缓存、取消和 Blob URL 回收策略。 11. 删除全部标准资源列表的数据库自增 ID 列,只保留系统唯一标识;同时把该规则加入自动检查。 12. 配送人员头像读取补充 `role_code = delivery` 范围限制,并新增 SQL 范围测试。 +13. 修复配送人员资质列表未应用人员筛选的问题,增加气站、配送点、配送角色和人员 ID 四重范围校验。 +14. 资质列表新增人员姓名上下文、安全返回、重复人员列隐藏;新建预填所属人员,创建和编辑锁定归属。 +15. 新增后端生成的 `searchFields` 契约;无有效字段时隐藏搜索区,枚举字段支持中文名称和内部编码。 +16. 标准列表搜索标签统一为“模糊搜索”,输入框继续提示当前资源的具体可搜索字段。 ## 操作后状态 @@ -41,7 +45,7 @@ - 变更前:列表抽屉承载记录操作,浏览器地址不随记录变化。 - 变更后:每个允许的操作拥有独立 URL,详情页集中承载业务动作。 -- 兼容性:现有列表菜单地址、后端接口和配送点数据范围保持不变。 +- 兼容性:现有列表菜单地址和响应结构保持不变;人员资质列表现在必须提供人员标识,修复了原先可能扩大到本点全部资质的范围错误。 ## 验证结果 @@ -49,13 +53,15 @@ - `contract:check`:通过,18 个资源与后端契约一致。 - `profile:check`:通过,资料专用只读页未回退。 - `type:check`:通过。 -- `build`:通过,2632 个模块完成生产构建。 -- `go test ./internal/logic/delivery ./internal/routers`:通过,包含配送人员头像角色范围测试。 +- `build`:通过,2634 个模块完成生产构建。 +- `go test ./internal/logic/common ./internal/logic/delivery ./internal/logic/platform ./internal/routers ./cmd/cli`:通过,包含搜索枚举、配送人员头像角色范围和资质人员范围测试。 - `lint`:通过;仅报告项目既有警告,未产生失败项。 - 浏览器回归:工作人员列表、详情、编辑、正式新建地址、返回链路、未保存保护、配送订单新建页、支付列表均通过。 - 布局回归:工作人员详情、编辑和配送订单新建页已在应用内浏览器逐页截图检查,与 5173 的页面结构和响应式断点一致。 - 头像回归:配送人员新建、编辑页已确认不再显示头像文本框,头像选择按钮、格式大小提示、默认头像和身份摘要均正常;后端头像路由测试通过。 - 列表头像回归:配送人员、用户账户显示受控圆形头像,其他资源不生成无效头像列;数据库 ID 列已从全部标准列表移除;点击刷新后头像重新加载正常。 +- 资质关联回归:有效人员仅返回本人资质,标题和上下文显示服务端姓名及唯一标识;新建页预填并锁定人员,缺少上下文自动返回,伪造人员标识显示错误且不渲染表格。 +- 搜索回归:资质页提示“可搜索:资质类型、资质编号”;银行卡页不显示关键字、查询和重置,仅保留刷新。 ## 风险评估 diff --git a/docs/项目文档_配送点独立资源页面_v1.0.md b/docs/项目文档_配送点独立资源页面_v1.0.md index 39b698d..620f70d 100644 --- a/docs/项目文档_配送点独立资源页面_v1.0.md +++ b/docs/项目文档_配送点独立资源页面_v1.0.md @@ -25,6 +25,7 @@ frontend/delivery_admin/ ├── api/ │ ├── resource-display.ts # 中文字段、状态和详情值展示 │ ├── avatar.ts # 头像上传与配送点鉴权读取 + │ ├── resource-search-contract.ts # 后端生成的显式搜索字段契约 │ ├── resource-navigation.ts # 独立页面路由和安全返回地址 │ └── resource-record-form.ts # 表单初始化、字段白名单和校验 ├── router/routes/modules/ @@ -55,6 +56,10 @@ frontend/delivery_admin/ 配送人员和用户账户列表同样参考 5173:联系电话后显示 32px 圆形头像,最多并发 6 个鉴权请求,接近可视区域才加载;当前页缓存结果,刷新或离页时取消请求并释放 Blob URL。404、网络错误和图片解码失败均回退本地默认头像。全部标准资源列表隐藏数据库自增 ID,只展示可复制的系统唯一标识。 +配送人员资质列表必须由配送人员列表进入。页面先通过受保护人员详情接口回查姓名,再以当前气站、配送点、`delivery` 角色和人员 ID 四重条件加载资质;缺少、无效或越权人员上下文时停止加载,绝不回退为全部资质。标题和上下文区域显示人员姓名及可复制唯一标识,表格隐藏重复人员列。新建页自动预填所属人员,创建和编辑均锁定该关系,返回地址只接受安全站内路径。 + +资源搜索由后端 `searchFields` 契约驱动。没有有效字段的配送点资料、用户地址和银行卡页面不渲染搜索区域;其他页面显示“可搜索:具体字段”提示。枚举字段同时接受中文展示名称和内部稳定编码,查询、分页和重置始终保留人员上下文及安全来源参数。 + `resource-record-form.ts` 为五类可编辑资源声明后端更新字段白名单,防止用户名、合同编号等只读字段出现在编辑页或被无效提交。 支付与退款资源统一使用后端正式名称 `payment_order`、`payment_refund`,菜单地址仍保持 `/finance/payments`、`/finance/refunds`,避免接口路径不一致导致 404。 @@ -79,6 +84,8 @@ npm.cmd run build - 修复支付、退款资源与后端契约名称不一致的问题。 - 新增 17/9/5 页面能力矩阵自动检查。 - 新增配送人员、用户账户的受控头像缩略图,并移除全部标准列表的数据库自增 ID。 +- 新增配送人员资质的强制人员范围、上下文展示、关系预填锁定和安全返回链路。 +- 新增后端生成的资源搜索契约,隐藏无效搜索并支持枚举中文名称与稳定编码。 - 保持配送点资料专用只读页面和现有公共后端接口不变。 ## 7. 已知边界 diff --git a/frontend/delivery_admin/scripts/check-resource-pages.mjs b/frontend/delivery_admin/scripts/check-resource-pages.mjs index 3172abd..0b613a2 100644 --- a/frontend/delivery_admin/scripts/check-resource-pages.mjs +++ b/frontend/delivery_admin/scripts/check-resource-pages.mjs @@ -24,6 +24,7 @@ const creatable = new Set([ const editable = new Set([ 'staff_account', 'staff_credential', 'user_account', 'user_address', 'gasorder_contract', ]); +const resourcesByName = new Map(contract.resources.map((resource) => [resource.name, resource])); /** 在条件不成立时中止检查,并给出可直接定位的原因。 */ function assert(condition, message) { @@ -72,5 +73,20 @@ assert(listPage.includes('avatarLoader.reset()'), '列表刷新未清理头像 const avatarLoader = read('src/views/shared/protected-list-avatar-loader.ts'); assert(avatarLoader.includes('MAX_CONCURRENT_REQUESTS = 6'), '头像请求并发上限未与 5173 对齐'); assert(avatarLoader.includes("new Set(['staff_account', 'user_account'])"), '头像列表资源白名单不正确'); +assert( + resourcesByName.get('staff_credential').searchFields.map((field) => field.key).join(',') === + 'credential_type,credential_no', + '人员资质搜索契约必须包含资质类型和资质编号', +); +for (const name of ['delivery_profile', 'user_address', 'wallet_bank']) { + assert(!(resourcesByName.get(name).searchFields?.length), `${name} 不应显示无效搜索`); +} +assert(listPage.includes('v-if="searchEnabled"'), '列表搜索区域未按后端契约控制显示'); +assert(listPage.includes('label="模糊搜索"'), '列表搜索标签未明确说明模糊匹配'); +assert(!listPage.includes('label="关键字"'), '列表仍使用含义不清的关键字标签'); +assert(!listPage.includes('placeholder="关键字段模糊搜索"'), '列表仍使用无意义的通用搜索提示'); +assert(listPage.includes('ensureCredentialContext'), '人员资质列表缺少服务端人员上下文校验'); +assert(listPage.includes("field.key === 'staff_account_identity'"), '人员范围资质列表未隐藏重复人员列'); +assert(recordPage.includes('已锁定,不可更换'), '人员资质独立页未锁定所属配送人员'); console.log(`独立资源页面契约通过:详情 ${listResources.length},新建 ${creatable.size},编辑 ${editable.size}`); diff --git a/frontend/delivery_admin/src/api/resource-navigation.ts b/frontend/delivery_admin/src/api/resource-navigation.ts index 3baaa12..866a952 100644 --- a/frontend/delivery_admin/src/api/resource-navigation.ts +++ b/frontend/delivery_admin/src/api/resource-navigation.ts @@ -25,11 +25,12 @@ export function recordRouteLocation( mode: RecordNavigationMode, identity: string, returnTo: string, + context: Record = {}, ): RouteLocationRaw { return { name: `${listRouteName}-${mode}`, ...(mode === 'create' ? {} : { params: { identity } }), - query: returnTo ? { return_to: returnTo } : {}, + query: { ...context, ...(returnTo ? { return_to: returnTo } : {}) }, }; } diff --git a/frontend/delivery_admin/src/api/resource-search-contract.ts b/frontend/delivery_admin/src/api/resource-search-contract.ts new file mode 100644 index 0000000..05d0e38 --- /dev/null +++ b/frontend/delivery_admin/src/api/resource-search-contract.ts @@ -0,0 +1,29 @@ +/** + * 功能描述:读取配送端生成的资源搜索契约,统一搜索字段与枚举别名。 + * 版本:v1.0.0。 + */ +import deliveryContract from '@/contracts/delivery-resources.json'; + +export type ResourceSearchValue = { value: string; label: string }; +export type ResourceSearchField = { + key: string; + kind: 'text' | 'enum'; + values?: ResourceSearchValue[]; +}; + +type ContractResource = { name: string; searchFields?: ResourceSearchField[] }; + +const fieldsByResource = Object.fromEntries( + (deliveryContract.resources as ContractResource[]).map((resource) => [ + resource.name, + resource.searchFields ?? [], + ]), +) as Record; + +/** 返回后端确认可用的搜索字段副本。 */ +export function resourceSearchFields(name: string): ResourceSearchField[] { + return (fieldsByResource[name] ?? []).map((field) => ({ + ...field, + values: field.values?.map((value) => ({ ...value })), + })); +} diff --git a/frontend/delivery_admin/src/api/resources.ts b/frontend/delivery_admin/src/api/resources.ts index 50f8ffd..947ad12 100644 --- a/frontend/delivery_admin/src/api/resources.ts +++ b/frontend/delivery_admin/src/api/resources.ts @@ -2,6 +2,8 @@ * 功能描述:定义配送点后台资源能力、字段契约和受控业务动作。 * 版本:v1.1.0。 */ +import { resourceSearchFields, type ResourceSearchField } from './resource-search-contract'; + export type ResourceMode = | 'writable' | 'readonly' @@ -59,6 +61,7 @@ export type ResourceUiDefinition = { mode: ResourceMode; pageKind: ResourcePageKind; fields: ResourceField[]; + searchFields: ResourceSearchField[]; detailActions?: DetailAction[]; canCreate: boolean; canEdit: boolean; @@ -338,6 +341,7 @@ function define( mode, pageKind, fields, + searchFields: resourceSearchFields(name), ...defaults, ...capabilities, ...(detailActions ? { detailActions } : {}), diff --git a/frontend/delivery_admin/src/contracts/delivery-resources.json b/frontend/delivery_admin/src/contracts/delivery-resources.json index 6a92ac1..92e2c30 100644 --- a/frontend/delivery_admin/src/contracts/delivery-resources.json +++ b/frontend/delivery_admin/src/contracts/delivery-resources.json @@ -1 +1 @@ -{"resources":[{"domain":"profile","name":"delivery_profile","path":"/delivery_profile","pageKind":"list","mode":"readonly"},{"domain":"staff","name":"staff_account","path":"/staff_account","pageKind":"list","mode":"writable"},{"domain":"staff","name":"staff_credential","path":"/staff_credential","pageKind":"list","mode":"writable"},{"domain":"user","name":"user_account","path":"/user_account","pageKind":"list","mode":"writable"},{"domain":"user","name":"user_address","path":"/user_address","pageKind":"list","mode":"writable"},{"domain":"contract","name":"gasorder_contract","path":"/gasorder_contract","pageKind":"list","mode":"managed"},{"domain":"contract","name":"gasorder_contract_product","path":"/gasorder_contract_product","pageKind":"list","mode":"append_only"},{"domain":"contract","name":"gasorder_contract_revision","path":"/gasorder_contract_revision","pageKind":"list","mode":"readonly"},{"domain":"contract","name":"product_info","path":"/product_info","pageKind":"list","mode":"readonly"},{"domain":"gasorder","name":"gasorder_basic","path":"/gasorder_basic","pageKind":"list","mode":"append_only"},{"domain":"finance","name":"wallet_basic","path":"/wallet_basic","pageKind":"list","mode":"readonly"},{"domain":"finance","name":"wallet_bank","path":"/wallet_bank","pageKind":"list","mode":"readonly"},{"domain":"finance","name":"payment_order","path":"/payment_order","pageKind":"list","mode":"readonly"},{"domain":"finance","name":"wallet_record","path":"/wallet_record","pageKind":"list","mode":"readonly"},{"domain":"finance","name":"payment_refund","path":"/payment_refund","pageKind":"list","mode":"readonly"},{"domain":"finance","name":"wallet_recharge","path":"/wallet_recharge","pageKind":"list","mode":"append_only"},{"domain":"finance","name":"wallet_apply_cash","path":"/wallet_apply_cash","pageKind":"list","mode":"append_only"},{"domain":"finance","name":"fin_settlement","path":"/fin_settlement","pageKind":"list","mode":"readonly"}],"routes":[{"method":"POST","path":"/gasorder_basic"},{"method":"POST","path":"/gasorder_basic/:identity/assign"},{"method":"POST","path":"/gasorder_basic/:identity/adjust-amount"},{"method":"POST","path":"/gasorder_basic/:identity/reclaim"},{"method":"POST","path":"/gasorder_basic/:identity/recover"},{"method":"POST","path":"/gasorder_basic/:identity/exception"},{"method":"POST","path":"/gasorder_basic/:identity/cancel"},{"method":"POST","path":"/gasorder_contract"},{"method":"POST","path":"/gasorder_contract/:identity/activate"},{"method":"POST","path":"/gasorder_contract/:identity/renew"},{"method":"POST","path":"/gasorder_contract/:identity/terminate"},{"method":"POST","path":"/gasorder_contract_product"},{"method":"POST","path":"/gasorder_contract_product/:identity/unbind"},{"method":"POST","path":"/wallet_basic/recharge"},{"method":"POST","path":"/wallet_recharge"},{"method":"POST","path":"/wallet_apply_cash"},{"method":"POST","path":"/staff_account"},{"method":"POST","path":"/staff_credential"},{"method":"POST","path":"/user_account"},{"method":"POST","path":"/user_address"},{"method":"POST","path":"/auth/login"},{"method":"GET","path":"/wallet_basic"},{"method":"GET","path":"/wallet_basic/:identity"},{"method":"GET","path":"/wallet_bank"},{"method":"GET","path":"/wallet_bank/:identity"},{"method":"GET","path":"/wallet_recharge"},{"method":"GET","path":"/wallet_recharge/:identity"},{"method":"GET","path":"/wallet_record"},{"method":"GET","path":"/wallet_record/:identity"},{"method":"GET","path":"/wallet_apply_cash"},{"method":"GET","path":"/wallet_apply_cash/:identity"},{"method":"GET","path":"/gasorder_contract"},{"method":"GET","path":"/gasorder_contract_product"},{"method":"GET","path":"/gasorder_contract_product/:identity"},{"method":"GET","path":"/gasorder_contract_revision"},{"method":"GET","path":"/gasorder_contract_revision/:identity"},{"method":"GET","path":"/gasorder_contract/:identity"},{"method":"GET","path":"/gasorder_basic"},{"method":"GET","path":"/gasorder_basic/:identity"},{"method":"GET","path":"/payment_order"},{"method":"GET","path":"/payment_order/:identity"},{"method":"GET","path":"/payment_refund"},{"method":"GET","path":"/payment_refund/:identity"},{"method":"GET","path":"/product_info"},{"method":"GET","path":"/product_info/:identity"},{"method":"GET","path":"/staff_account"},{"method":"GET","path":"/staff_account/:identity"},{"method":"GET","path":"/staff_account/:identity/avatar"},{"method":"GET","path":"/staff_credential"},{"method":"GET","path":"/staff_credential/:identity"},{"method":"GET","path":"/user_account"},{"method":"GET","path":"/user_account/:identity"},{"method":"GET","path":"/user_account/:identity/avatar"},{"method":"GET","path":"/user_address"},{"method":"GET","path":"/user_address/:identity"},{"method":"GET","path":"/dashboard/overview"},{"method":"GET","path":"/dashboard/reports"},{"method":"GET","path":"/delivery_menu"},{"method":"GET","path":"/delivery_profile"},{"method":"GET","path":"/fin_settlement"},{"method":"GET","path":"/fin_settlement/:identity"},{"method":"GET","path":"/auth/profile"},{"method":"GET","path":"/invitation/qrcode"},{"method":"PUT","path":"/staff_account/:identity"},{"method":"PUT","path":"/staff_account/:identity/password"},{"method":"PUT","path":"/staff_credential/:identity"},{"method":"PUT","path":"/user_account/:identity"},{"method":"PUT","path":"/user_account/:identity/password"},{"method":"PUT","path":"/user_address/:identity"},{"method":"PUT","path":"/auth/password"},{"method":"PUT","path":"/gasorder_contract/:identity"},{"method":"PATCH","path":"/staff_account/:identity/status"},{"method":"PATCH","path":"/staff_credential/:identity/status"},{"method":"PATCH","path":"/user_account/:identity/status"},{"method":"PATCH","path":"/user_address/:identity/status"},{"method":"DELETE","path":"/staff_account/:identity"},{"method":"DELETE","path":"/staff_credential/:identity"},{"method":"DELETE","path":"/user_account/:identity"},{"method":"DELETE","path":"/user_address/:identity"}]} +{"resources":[{"domain":"profile","name":"delivery_profile","path":"/delivery_profile","pageKind":"list","mode":"readonly"},{"domain":"staff","name":"staff_account","path":"/staff_account","pageKind":"list","mode":"writable","searchFields":[{"key":"username","kind":"text"}]},{"domain":"staff","name":"staff_credential","path":"/staff_credential","pageKind":"list","mode":"writable","searchFields":[{"key":"credential_type","kind":"text"},{"key":"credential_no","kind":"text"}]},{"domain":"user","name":"user_account","path":"/user_account","pageKind":"list","mode":"writable","searchFields":[{"key":"username","kind":"text"}]},{"domain":"user","name":"user_address","path":"/user_address","pageKind":"list","mode":"writable"},{"domain":"contract","name":"gasorder_contract","path":"/gasorder_contract","pageKind":"list","mode":"managed","searchFields":[{"key":"contract_no","kind":"text"},{"key":"title","kind":"text"}]},{"domain":"contract","name":"gasorder_contract_product","path":"/gasorder_contract_product","pageKind":"list","mode":"append_only","searchFields":[{"key":"product_code","kind":"text"},{"key":"product_type_name","kind":"text"}]},{"domain":"contract","name":"gasorder_contract_revision","path":"/gasorder_contract_revision","pageKind":"list","mode":"readonly","searchFields":[{"key":"action","kind":"enum","values":[{"value":"activate","label":"启用"},{"value":"renew","label":"续签"},{"value":"terminate","label":"终止"}]}]},{"domain":"contract","name":"product_info","path":"/product_info","pageKind":"list","mode":"readonly","searchFields":[{"key":"code","kind":"text"},{"key":"name","kind":"text"}]},{"domain":"gasorder","name":"gasorder_basic","path":"/gasorder_basic","pageKind":"list","mode":"append_only","searchFields":[{"key":"request_no","kind":"text"},{"key":"creator_type","kind":"enum","values":[{"value":"user","label":"用户"},{"value":"staff","label":"工作人员"},{"value":"delivery","label":"配送点"},{"value":"gas","label":"气站"}]}]},{"domain":"finance","name":"wallet_basic","path":"/wallet_basic","pageKind":"list","mode":"readonly"},{"domain":"finance","name":"wallet_bank","path":"/wallet_bank","pageKind":"list","mode":"readonly"},{"domain":"finance","name":"payment_order","path":"/payment_order","pageKind":"list","mode":"readonly","searchFields":[{"key":"payment_no","kind":"text"},{"key":"request_no","kind":"text"},{"key":"channel","kind":"enum","values":[{"value":"wechat","label":"微信"},{"value":"alipay","label":"支付宝"},{"value":"mock","label":"模拟支付"}]}]},{"domain":"finance","name":"wallet_record","path":"/wallet_record","pageKind":"list","mode":"readonly","searchFields":[{"key":"record_no","kind":"text"},{"key":"request_no","kind":"text"}]},{"domain":"finance","name":"payment_refund","path":"/payment_refund","pageKind":"list","mode":"readonly","searchFields":[{"key":"refund_no","kind":"text"},{"key":"request_no","kind":"text"}]},{"domain":"finance","name":"wallet_recharge","path":"/wallet_recharge","pageKind":"list","mode":"append_only","searchFields":[{"key":"record_no","kind":"text"},{"key":"request_no","kind":"text"}]},{"domain":"finance","name":"wallet_apply_cash","path":"/wallet_apply_cash","pageKind":"list","mode":"append_only","searchFields":[{"key":"cash_no","kind":"text"},{"key":"request_no","kind":"text"},{"key":"channel","kind":"enum","values":[{"value":"bank","label":"银行卡"},{"value":"alipay","label":"支付宝"},{"value":"wechat","label":"微信"}]}]},{"domain":"finance","name":"fin_settlement","path":"/fin_settlement","pageKind":"list","mode":"readonly","searchFields":[{"key":"settlement_no","kind":"text"}]}],"routes":[{"method":"POST","path":"/gasorder_basic"},{"method":"POST","path":"/gasorder_basic/:identity/assign"},{"method":"POST","path":"/gasorder_basic/:identity/adjust-amount"},{"method":"POST","path":"/gasorder_basic/:identity/reclaim"},{"method":"POST","path":"/gasorder_basic/:identity/recover"},{"method":"POST","path":"/gasorder_basic/:identity/exception"},{"method":"POST","path":"/gasorder_basic/:identity/cancel"},{"method":"POST","path":"/gasorder_contract"},{"method":"POST","path":"/gasorder_contract/:identity/activate"},{"method":"POST","path":"/gasorder_contract/:identity/renew"},{"method":"POST","path":"/gasorder_contract/:identity/terminate"},{"method":"POST","path":"/gasorder_contract_product"},{"method":"POST","path":"/gasorder_contract_product/:identity/unbind"},{"method":"POST","path":"/wallet_basic/recharge"},{"method":"POST","path":"/wallet_recharge"},{"method":"POST","path":"/wallet_apply_cash"},{"method":"POST","path":"/staff_account"},{"method":"POST","path":"/staff_credential"},{"method":"POST","path":"/user_account"},{"method":"POST","path":"/user_address"},{"method":"POST","path":"/auth/login"},{"method":"GET","path":"/wallet_basic"},{"method":"GET","path":"/wallet_basic/:identity"},{"method":"GET","path":"/wallet_bank"},{"method":"GET","path":"/wallet_bank/:identity"},{"method":"GET","path":"/wallet_recharge"},{"method":"GET","path":"/wallet_recharge/:identity"},{"method":"GET","path":"/wallet_record"},{"method":"GET","path":"/wallet_record/:identity"},{"method":"GET","path":"/wallet_apply_cash"},{"method":"GET","path":"/wallet_apply_cash/:identity"},{"method":"GET","path":"/gasorder_contract"},{"method":"GET","path":"/gasorder_contract_product"},{"method":"GET","path":"/gasorder_contract_product/:identity"},{"method":"GET","path":"/gasorder_contract_revision"},{"method":"GET","path":"/gasorder_contract_revision/:identity"},{"method":"GET","path":"/gasorder_contract/:identity"},{"method":"GET","path":"/gasorder_basic"},{"method":"GET","path":"/gasorder_basic/:identity"},{"method":"GET","path":"/payment_order"},{"method":"GET","path":"/payment_order/:identity"},{"method":"GET","path":"/payment_refund"},{"method":"GET","path":"/payment_refund/:identity"},{"method":"GET","path":"/product_info"},{"method":"GET","path":"/product_info/:identity"},{"method":"GET","path":"/staff_account"},{"method":"GET","path":"/staff_account/:identity"},{"method":"GET","path":"/staff_account/:identity/avatar"},{"method":"GET","path":"/staff_credential"},{"method":"GET","path":"/staff_credential/:identity"},{"method":"GET","path":"/user_account"},{"method":"GET","path":"/user_account/:identity"},{"method":"GET","path":"/user_account/:identity/avatar"},{"method":"GET","path":"/user_address"},{"method":"GET","path":"/user_address/:identity"},{"method":"GET","path":"/dashboard/overview"},{"method":"GET","path":"/dashboard/reports"},{"method":"GET","path":"/delivery_menu"},{"method":"GET","path":"/delivery_profile"},{"method":"GET","path":"/fin_settlement"},{"method":"GET","path":"/fin_settlement/:identity"},{"method":"GET","path":"/auth/profile"},{"method":"GET","path":"/invitation/qrcode"},{"method":"PUT","path":"/staff_account/:identity"},{"method":"PUT","path":"/staff_account/:identity/password"},{"method":"PUT","path":"/staff_credential/:identity"},{"method":"PUT","path":"/user_account/:identity"},{"method":"PUT","path":"/user_account/:identity/password"},{"method":"PUT","path":"/user_address/:identity"},{"method":"PUT","path":"/auth/password"},{"method":"PUT","path":"/gasorder_contract/:identity"},{"method":"PATCH","path":"/staff_account/:identity/status"},{"method":"PATCH","path":"/staff_credential/:identity/status"},{"method":"PATCH","path":"/user_account/:identity/status"},{"method":"PATCH","path":"/user_address/:identity/status"},{"method":"DELETE","path":"/staff_account/:identity"},{"method":"DELETE","path":"/staff_credential/:identity"},{"method":"DELETE","path":"/user_account/:identity"},{"method":"DELETE","path":"/user_address/:identity"}]} diff --git a/frontend/delivery_admin/src/views/resource/ResourceRecordPage.less b/frontend/delivery_admin/src/views/resource/ResourceRecordPage.less index 1e28e31..892ef6a 100644 --- a/frontend/delivery_admin/src/views/resource/ResourceRecordPage.less +++ b/frontend/delivery_admin/src/views/resource/ResourceRecordPage.less @@ -66,3 +66,14 @@ .section-card :deep(.arco-card-header), .section-card :deep(.arco-card-body) { padding-right: 16px; padding-left: 16px; } } +.credential-owner-card :deep(.arco-card-body) { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.credential-owner-card strong { + display: block; + margin-bottom: 8px; +} diff --git a/frontend/delivery_admin/src/views/resource/ResourceRecordPage.vue b/frontend/delivery_admin/src/views/resource/ResourceRecordPage.vue index f868e8b..ab211a7 100644 --- a/frontend/delivery_admin/src/views/resource/ResourceRecordPage.vue +++ b/frontend/delivery_admin/src/views/resource/ResourceRecordPage.vue @@ -29,6 +29,18 @@