From 14263416e046a3dad6d7bb4bd48da361a92a9372 Mon Sep 17 00:00:00 2001 From: czl231 <3286836406@qq.com> Date: Fri, 14 Aug 2026 01:49:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=90=88=E5=90=8C=E6=B0=94?= =?UTF-8?q?=E7=93=B6=E8=AE=BE=E5=A4=87=E5=90=8D=E7=A7=B0=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../logic/platform/gasorder/gasorder.go | 45 ++--- .../logic/platform/gasorder/gasorder_test.go | 6 +- .../platform/gasorder/product_display.go | 155 ++++++++++++++++++ .../platform/gasorder/product_display_test.go | 69 ++++++++ docs/05-平台总后台需求.md | 2 +- .../项目文档_配送订单合同气瓶名称显示_v1.1.md | 60 +++++++ ...heck-gasorder-contract-product-display.mjs | 10 +- .../src/api/resource-display.ts | 45 ++++- frontend/platform_admin/src/api/resources.ts | 4 +- .../views/resource/ResourceDetailContent.vue | 32 +++- .../src/views/resource/ResourceFieldForm.vue | 18 +- 11 files changed, 400 insertions(+), 46 deletions(-) create mode 100644 backend/api/internal/logic/platform/gasorder/product_display.go create mode 100644 backend/api/internal/logic/platform/gasorder/product_display_test.go create mode 100644 docs/项目文档_配送订单合同气瓶名称显示_v1.1.md diff --git a/backend/api/internal/logic/platform/gasorder/gasorder.go b/backend/api/internal/logic/platform/gasorder/gasorder.go index d66711b..d890ffe 100644 --- a/backend/api/internal/logic/platform/gasorder/gasorder.go +++ b/backend/api/internal/logic/platform/gasorder/gasorder.go @@ -68,34 +68,14 @@ func filterGasorderContractCandidates(query *gorm.DB, candidate string, now time } func GetGasorderContract(ctx *gin.Context) { getGasorderContract(ctx) } -// ListGasorderContractProduct 在订单候选场景按合同收窄未解绑气瓶,未传合同时保留完整历史列表。 -func ListGasorderContractProduct(ctx *gin.Context) { - contractIdentity := strings.TrimSpace(ctx.Query("contract_identity")) - if contractIdentity == "" { - common.ListResource(ctx, &models.GasorderContractProduct{}) - return - } - contractID, err := common.ResolveIdentityID(&models.GasorderContract{}, contractIdentity, true) - if err != nil { - common.RespondRecordError(ctx, err) - return - } - common.ListPageFiltered[models.GasorderContractProduct](ctx, func(query *gorm.DB) *gorm.DB { - return filterGasorderContractProductCandidates(query, contractID) - }) -} - // filterGasorderContractProductCandidates 仅保留当前合同仍有效绑定的气瓶,并按类型和编码排序。 func filterGasorderContractProductCandidates(query *gorm.DB, contractID uint64) *gorm.DB { return query. - Where("gasorder_contract_id = ? AND unbound_at IS NULL", contractID). - Order("product_type_name asc"). - Order("product_code asc") + Where("gasorder_contract_product.gasorder_contract_id = ? AND gasorder_contract_product.unbound_at IS NULL", contractID). + Order("gasorder_contract_product.product_type_name asc"). + Order("gasorder_contract_product.product_code asc") } -func GetGasorderContractProduct(ctx *gin.Context) { - common.GetResource(ctx, &models.GasorderContractProduct{}) -} func ListGasorderContractRevision(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderContractRevision{}) } @@ -104,8 +84,6 @@ func GetGasorderContractRevision(ctx *gin.Context) { } func ListGasorderBasic(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderBasic{}) } func GetGasorderBasic(ctx *gin.Context) { getGasorderBasic(ctx) } -func ListGasorderItem(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderItem{}) } -func GetGasorderItem(ctx *gin.Context) { common.GetResource(ctx, &models.GasorderItem{}) } func ListGasorderAssign(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderAssign{}) } func GetGasorderAssign(ctx *gin.Context) { common.GetResource(ctx, &models.GasorderAssign{}) } func ListGasorderStatus(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderStatus{}) } @@ -125,9 +103,12 @@ func getGasorderContract(ctx *gin.Context) { common.RespondRecordError(ctx, err) return } - var products []models.GasorderContractProduct + var products []gasorderContractProductDisplay var revisions []models.GasorderContractRevision - if err := impl.DBService.Where("gasorder_contract_id = ?", contract.ID).Order("bound_at asc, id asc").Find(&products).Error; err != nil { + if err := contractProductDisplayQuery(impl.DBService, ""). + Where("gasorder_contract_product.gasorder_contract_id = ?", contract.ID). + Order("gasorder_contract_product.bound_at asc, gasorder_contract_product.id asc"). + Scan(&products).Error; err != nil { infra.Response.Error(ctx, err) return } @@ -152,17 +133,23 @@ func getGasorderBasic(ctx *gin.Context) { common.RespondRecordError(ctx, err) return } - var items []models.GasorderItem + var items []gasorderItemDisplay var assignments []models.GasorderAssign var statuses []models.GasorderStatus var tracks []models.GasorderTrack var confirmations []models.GasorderConfirm var payments []models.GasorderPayment + if err := gasorderItemDisplayQuery(impl.DBService, ""). + Where("gasorder_item.gasorder_basic_id = ?", order.ID). + Order("gasorder_item.id asc").Scan(&items).Error; err != nil { + infra.Response.Error(ctx, err) + return + } loaders := []struct { order string value any }{ - {"id asc", &items}, {"assigned_at asc, id asc", &assignments}, + {"assigned_at asc, id asc", &assignments}, {"occurred_at asc, id asc", &statuses}, {"attempt_no asc, id asc", &tracks}, {"confirmed_at asc, id asc", &confirmations}, {"attempt_no asc, id asc", &payments}, } diff --git a/backend/api/internal/logic/platform/gasorder/gasorder_test.go b/backend/api/internal/logic/platform/gasorder/gasorder_test.go index 76ea801..2b103e6 100644 --- a/backend/api/internal/logic/platform/gasorder/gasorder_test.go +++ b/backend/api/internal/logic/platform/gasorder/gasorder_test.go @@ -89,9 +89,9 @@ func TestFilterGasorderContractProductCandidates(t *testing.T) { ).Find(&products) }) for _, fragment := range []string{ - "gasorder_contract_id = 27", - "unbound_at IS NULL", - "ORDER BY product_type_name asc,product_code asc", + "gasorder_contract_product.gasorder_contract_id = 27", + "gasorder_contract_product.unbound_at IS NULL", + "ORDER BY gasorder_contract_product.product_type_name asc,gasorder_contract_product.product_code asc", } { if !strings.Contains(statement, fragment) { t.Fatalf("合同气瓶候选缺少 %q:%s", fragment, statement) diff --git a/backend/api/internal/logic/platform/gasorder/product_display.go b/backend/api/internal/logic/platform/gasorder/product_display.go new file mode 100644 index 0000000..c5d679e --- /dev/null +++ b/backend/api/internal/logic/platform/gasorder/product_display.go @@ -0,0 +1,155 @@ +// 功能描述:为合同气瓶和订单明细补充当前智能气阀名称,并保持合同类型、编码快照不变。 +// 版本:v1.0.0 +package gasorder + +import ( + "strings" + + "git.apinb.com/bsm-sdk/core/infra" + "git.apinb.com/heqiapp/platforms/backend/api/internal/impl" + "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common" + "git.apinb.com/heqiapp/platforms/backend/api/internal/models" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// gasorderContractProductDisplay 是合同绑定快照与当前智能气阀名称组成的只读响应。 +type gasorderContractProductDisplay struct { + models.GasorderContractProduct + ProductName string `gorm:"column:product_name" json:"product_name"` // 当前智能气阀名称,仅用于可读展示 +} + +// gasorderItemDisplay 是订单气瓶快照与当前智能气阀名称组成的只读响应。 +type gasorderItemDisplay struct { + models.GasorderItem + ProductName string `gorm:"column:product_name" json:"product_name"` // 当前智能气阀名称,仅用于可读展示 +} + +// contractProductDisplayQuery 关联未归档的当前智能气阀档案,并支持按名称、编码或绑定类型搜索。 +func contractProductDisplayQuery(database *gorm.DB, keyword string) *gorm.DB { + query := common.ActiveRecords(database.Model(&models.GasorderContractProduct{})). + Select("gasorder_contract_product.*, COALESCE(display_product.name, '') AS product_name"). + Joins("LEFT JOIN product_info AS display_product ON display_product.id = gasorder_contract_product.product_info_id AND display_product.deleted_at IS NULL AND display_product.status <> ?", common.StatusArchived) + return applyProductDisplayKeyword(query, keyword, "gasorder_contract_product") +} + +// gasorderItemDisplayQuery 关联未归档的当前智能气阀档案,并保留订单创建时的编码与类型快照。 +func gasorderItemDisplayQuery(database *gorm.DB, keyword string) *gorm.DB { + query := common.ActiveRecords(database.Model(&models.GasorderItem{})). + Select("gasorder_item.*, COALESCE(display_product.name, '') AS product_name"). + Joins("LEFT JOIN product_info AS display_product ON display_product.id = gasorder_item.product_info_id AND display_product.deleted_at IS NULL AND display_product.status <> ?", common.StatusArchived) + return applyProductDisplayKeyword(query, keyword, "gasorder_item") +} + +// applyProductDisplayKeyword 对可信固定列执行模糊搜索,不拼接用户输入到 SQL 结构中。 +func applyProductDisplayKeyword(query *gorm.DB, keyword, table string) *gorm.DB { + keyword = strings.ToLower(strings.TrimSpace(keyword)) + if keyword == "" { + return query + } + pattern := "%" + keyword + "%" + return query.Where( + "(LOWER(COALESCE(display_product.name, '')) LIKE ? OR LOWER("+table+".product_code) LIKE ? OR LOWER("+table+".product_type_name) LIKE ?)", + pattern, pattern, pattern, + ) +} + +// ListGasorderContractProduct 返回合同气瓶历史;订单候选场景仅返回指定合同尚未解绑的记录。 +func ListGasorderContractProduct(ctx *gin.Context) { + contractIdentity := strings.TrimSpace(ctx.Query("contract_identity")) + var contractID uint64 + var err error + if contractIdentity != "" { + contractID, err = common.ResolveIdentityID(&models.GasorderContract{}, contractIdentity, true) + if err != nil { + common.RespondRecordError(ctx, err) + return + } + } + page, size := common.PageSize(ctx) + query := contractProductDisplayQuery(impl.DBService, ctx.Query("keyword")) + if contractID > 0 { + query = filterGasorderContractProductCandidates(query, contractID) + } else { + query = query.Order("gasorder_contract_product.created_at desc") + } + var total int64 + if err := query.Count(&total).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + var list []gasorderContractProductDisplay + if err := query.Offset((page - 1) * size).Limit(size).Scan(&list).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + respondProductDisplayList(ctx, total, list, &models.GasorderContractProduct{}) +} + +// GetGasorderContractProduct 返回单条合同气瓶绑定及当前智能气阀名称。 +func GetGasorderContractProduct(ctx *gin.Context) { + var record gasorderContractProductDisplay + err := contractProductDisplayQuery(impl.DBService, ""). + Where("gasorder_contract_product.identity = ?", ctx.Param("identity")). + Scan(&record).Error + if err != nil || record.ID == 0 { + if err == nil { + err = gorm.ErrRecordNotFound + } + common.RespondRecordError(ctx, err) + return + } + respondProductDisplay(ctx, record, &models.GasorderContractProduct{}) +} + +// ListGasorderItem 返回订单明细及当前智能气阀名称,编码和类型仍使用订单快照。 +func ListGasorderItem(ctx *gin.Context) { + page, size := common.PageSize(ctx) + query := gasorderItemDisplayQuery(impl.DBService, ctx.Query("keyword")).Order("gasorder_item.created_at desc") + var total int64 + if err := query.Count(&total).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + var list []gasorderItemDisplay + if err := query.Offset((page - 1) * size).Limit(size).Scan(&list).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + respondProductDisplayList(ctx, total, list, &models.GasorderItem{}) +} + +// GetGasorderItem 返回单条订单明细及当前智能气阀名称。 +func GetGasorderItem(ctx *gin.Context) { + var record gasorderItemDisplay + err := gasorderItemDisplayQuery(impl.DBService, ""). + Where("gasorder_item.identity = ?", ctx.Param("identity")).Scan(&record).Error + if err != nil || record.ID == 0 { + if err == nil { + err = gorm.ErrRecordNotFound + } + common.RespondRecordError(ctx, err) + return + } + respondProductDisplay(ctx, record, &models.GasorderItem{}) +} + +// respondProductDisplayList 统一执行关系标识投影与位置保护后返回分页列表。 +func respondProductDisplayList(ctx *gin.Context, total int64, value, model any) { + response, err := common.PublicResourceResponse(value) + if err != nil { + infra.Response.Error(ctx, err) + return + } + infra.Response.Success(ctx, gin.H{"total": total, "list": common.ProtectPreciseLocation(ctx, model, response)}) +} + +// respondProductDisplay 统一执行关系标识投影与位置保护后返回单条记录。 +func respondProductDisplay(ctx *gin.Context, value, model any) { + response, err := common.PublicResourceResponse(value) + if err != nil { + infra.Response.Error(ctx, err) + return + } + infra.Response.Success(ctx, common.ProtectPreciseLocation(ctx, model, response)) +} diff --git a/backend/api/internal/logic/platform/gasorder/product_display_test.go b/backend/api/internal/logic/platform/gasorder/product_display_test.go new file mode 100644 index 0000000..7999637 --- /dev/null +++ b/backend/api/internal/logic/platform/gasorder/product_display_test.go @@ -0,0 +1,69 @@ +// 功能描述:验证合同气瓶和订单明细的设备名称派生、历史快照保留与组合搜索。 +// 版本:v1.0.0 +package gasorder + +import ( + "strings" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +// TestProductDisplayQueryUsesCurrentNameAndSnapshotFields 验证名称来自当前设备,类型和编码仍来自历史快照。 +func TestProductDisplayQueryUsesCurrentNameAndSnapshotFields(t *testing.T) { + sqlDatabase, _, err := sqlmock.New() + if err != nil { + t.Fatalf("创建模拟数据库失败:%v", err) + } + defer sqlDatabase.Close() + database, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDatabase}), &gorm.Config{}) + if err != nil { + t.Fatalf("创建 GORM 数据库失败:%v", err) + } + + statement := database.ToSQL(func(tx *gorm.DB) *gorm.DB { + var records []gasorderContractProductDisplay + return contractProductDisplayQuery(tx, "阀门 A").Scan(&records) + }) + for _, fragment := range []string{ + "LEFT JOIN product_info AS display_product", + "AS product_name", + "display_product.name", + "gasorder_contract_product.product_code", + "gasorder_contract_product.product_type_name", + } { + if !strings.Contains(statement, fragment) { + t.Fatalf("设备显示查询缺少 %q:%s", fragment, statement) + } + } +} + +// TestGasorderItemDisplayQueryKeepsOrderSnapshot 验证订单详情同样只派生当前名称,不覆盖订单编码和类型快照。 +func TestGasorderItemDisplayQueryKeepsOrderSnapshot(t *testing.T) { + sqlDatabase, _, err := sqlmock.New() + if err != nil { + t.Fatalf("创建模拟数据库失败:%v", err) + } + defer sqlDatabase.Close() + database, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDatabase}), &gorm.Config{}) + if err != nil { + t.Fatalf("创建 GORM 数据库失败:%v", err) + } + + statement := database.ToSQL(func(tx *gorm.DB) *gorm.DB { + var records []gasorderItemDisplay + return gasorderItemDisplayQuery(tx, "MOCK-CYLINDER").Scan(&records) + }) + for _, fragment := range []string{ + "gasorder_item.*", + "gasorder_item.product_code", + "gasorder_item.product_type_name", + "display_product.name", + } { + if !strings.Contains(statement, fragment) { + t.Fatalf("订单设备显示查询缺少 %q:%s", fragment, statement) + } + } +} diff --git a/docs/05-平台总后台需求.md b/docs/05-平台总后台需求.md index f29ab0c..64e3d9f 100644 --- a/docs/05-平台总后台需求.md +++ b/docs/05-平台总后台需求.md @@ -206,7 +206,7 @@ 创建订单页面必须先选择配送合同,收货地址下拉仅展示该合同签约用户的地址。地址选项以完整地址为主文案并标记默认地址;存在默认地址时自动选中,切换合同时清空原地址。若合同用户没有可用地址,页面应明确引导先维护用户地址并禁止提交。 -合同气瓶下拉仅展示所选合同中尚未解绑的气瓶,按气瓶类型和气瓶编码排序,并支持按气瓶编码或类型搜索。选项以气瓶类型名称展示;切换合同时清空原选择。若合同没有可用气瓶,页面应引导先为合同绑定气瓶并禁止提交。合同气瓶管理列表仍保留完整绑定和解绑历史。 +合同气瓶下拉仅展示所选合同中尚未解绑的具体智能气阀,按绑定时的设备类型和设备编码排序,并支持按当前智能气阀名称、设备类型或设备编码搜索。选项显示“智能气阀名称(设备类型)”;同一合同候选中名称和类型均相同时追加设备编码。当前名称缺失时降级显示“设备编码(设备类型)”,再降级为唯一标识。切换合同时清空原选择。合同详情和订单详情均展示当前智能气阀名称,同时保留绑定或下单时的类型、编码快照;设备改名后名称随当前档案变化,历史类型和编码不变。若合同没有可用气瓶,页面应引导先为合同绑定气瓶并禁止提交。合同气瓶管理列表仍保留完整绑定和解绑历史。 订单合同候选必须同时满足“生效中、已到生效时间、尚未到期且到期时间有效”。草稿、未到生效时间、已到期和已终止合同不得进入订单候选;没有可履约合同时应引导管理员先启用或续签合同并禁止提交。 diff --git a/docs/项目文档_配送订单合同气瓶名称显示_v1.1.md b/docs/项目文档_配送订单合同气瓶名称显示_v1.1.md new file mode 100644 index 0000000..1fc9306 --- /dev/null +++ b/docs/项目文档_配送订单合同气瓶名称显示_v1.1.md @@ -0,0 +1,60 @@ +# 项目文档_配送订单合同气瓶名称显示_v1.1 + +## 1. 项目概述 + +本次修复平台总后台配送订单中合同气瓶难以识别的问题。合同绑定对象始终是具体智能气阀,不是整个设备类型;原页面只显示类型快照,导致同类型的多台设备看起来完全相同。 + +## 2. 业务规则 + +- 合同气瓶候选显示“智能气阀名称(设备类型)”。 +- 同一合同候选中名称和类型均相同时,为全部重复项追加设备编码。 +- 名称取当前智能气阀档案;类型和编码使用合同绑定或订单创建时的历史快照。 +- 当前名称缺失或设备已归档时,降级显示“设备编码(设备类型)”;仍无可读字段时显示唯一标识。 +- 支持按当前名称、历史类型和历史编码搜索。 +- 创建订单下拉、已选标签、合同详情和订单详情使用一致的设备识别信息。 +- 不修改数据库表结构,也不回填或覆盖历史数据。 + +## 3. 接口说明 + +平台合同气瓶和订单明细只读响应增加派生字段: + +```json +{ + "product_name": "当前智能气阀名称", + "product_code": "绑定或下单时设备编码快照", + "product_type_name": "绑定或下单时设备类型快照" +} +``` + +`product_name` 通过关联当前 `product_info` 批量查询得到,不写入合同气瓶表或订单明细表。候选接口的 `keyword` 同时匹配名称、类型和编码。 + +## 4. 核心文件 + +- `backend/api/internal/logic/platform/gasorder/product_display.go`:派生当前设备名称,提供组合搜索和只读响应。 +- `backend/api/internal/logic/platform/gasorder/gasorder.go`:合同、订单详情接入名称派生查询。 +- `backend/api/internal/logic/platform/gasorder/product_display_test.go`:验证名称关联和三字段搜索。 +- `frontend/platform_admin/src/api/resources.ts`:配置合同气瓶组合标签和搜索提示。 +- `frontend/platform_admin/src/api/resource-display.ts`:生成名称、类型与同名编码消歧标签。 +- `frontend/platform_admin/src/views/resource/ResourceDetailContent.vue`:保证合同和订单详情优先展示名称、编码与类型。 + +## 5. 维护指南 + +- 设备改名只影响当前操作界面的名称,不得覆盖合同和订单历史快照。 +- 后续如需保留“绑定当时名称”,必须单独设计名称快照字段和历史迁移方案,不能复用当前派生名称冒充历史名称。 +- 服务端仍须按合同和未解绑状态过滤候选,订单创建时的最终归属校验不可删除。 + +## 6. 变更记录 + +操作时间:2026-08-14 + +操作类型:修改、扩展 + +影响模块:平台总后台、配送合同、配送订单、合同气瓶 + +操作前状态:合同绑定具体智能气阀,但订单候选只显示设备类型,同类型设备无法区分。 + +具体操作:服务端增加当前设备名称派生和名称、类型、编码组合搜索;前端统一显示名称与类型,同名同类型时追加编码;合同和订单详情增加名称列。 + +操作后状态:业务人员可识别具体智能气阀,同时继续查看合同和订单保存的类型、编码快照。 + +风险评估:设备改名后派生名称会变化,但历史类型和编码不变;数据库无变更;合同过滤和订单最终校验保持不变。 diff --git a/frontend/platform_admin/scripts/check-gasorder-contract-product-display.mjs b/frontend/platform_admin/scripts/check-gasorder-contract-product-display.mjs index 75a2cf0..1ccf424 100644 --- a/frontend/platform_admin/scripts/check-gasorder-contract-product-display.mjs +++ b/frontend/platform_admin/scripts/check-gasorder-contract-product-display.mjs @@ -1,6 +1,6 @@ /** * 功能:静态检查配送订单合同气瓶候选的可读显示与交互约束。 - * 版本:v1.0.0 + * 版本:v1.1.0 */ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; @@ -10,11 +10,15 @@ const display = readFileSync(new URL('../src/api/resource-display.ts', import.me const form = readFileSync(new URL('../src/views/resource/ResourceFieldForm.vue', import.meta.url), 'utf8'); assert.match(resources, /label: '合同气瓶'/, '订单气瓶字段必须使用业务名称'); -assert.match(resources, /relationOptionLabelKey: 'product_type_name'/, '气瓶候选必须显示气瓶类型名称'); +assert.match(resources, /relationOptionDisplay: 'product-name-type'/, '合同气瓶必须使用设备名称和类型组合显示'); +assert.match(resources, /请输入智能气阀名称、设备类型或设备编码搜索/, '合同气瓶必须提示可按名称、类型或编码搜索'); assert.match(resources, /该合同暂无可用气瓶,请先为合同绑定气瓶/, '缺少无可用气瓶的业务引导'); assert.match(resources, /filterKey: 'contract_identity'/, '订单气瓶请求必须携带合同标识'); assert.match(display, /field\.relationOptionLabelKey/, '关系选项必须支持指定业务展示字段'); +assert.match(display, /productNameTypeLabel/, '合同气瓶必须具备名称和类型组合标签'); +assert.match(display, /duplicateCount/, '同名同类型设备必须追加编码消歧'); +assert.match(display, /product_name: '智能气阀名称'/, '合同和订单详情必须展示智能气阀名称'); assert.doesNotMatch(form, /max-tag-count/, '合同气瓶多选必须完整展示全部已选标签'); assert.match(form, /'请先选择配送合同'/, '父合同未选时缺少操作顺序提示'); -console.log('合同气瓶显示检查通过:候选显示气瓶类型名称,并完整展示全部已选标签。'); +console.log('合同气瓶显示检查通过:候选显示智能气阀名称和设备类型,同名时追加编码,并完整展示全部已选标签。'); diff --git a/frontend/platform_admin/src/api/resource-display.ts b/frontend/platform_admin/src/api/resource-display.ts index 4a6c9fb..b505602 100644 --- a/frontend/platform_admin/src/api/resource-display.ts +++ b/frontend/platform_admin/src/api/resource-display.ts @@ -1,6 +1,6 @@ /** * 功能:统一资源列表与详情页面的字段名称、关系、金额、状态和时间展示。 - * 版本:v1.2.0 + * 版本:v1.3.0 */ import dayjs from 'dayjs'; import { staffRoleLabel } from './resource-staff-relation'; @@ -39,6 +39,7 @@ const collectionFieldAliases: Record> = { 'gasorder_contract.products': { gasorder_contract_identity: '配送合同唯一标识', product_info_identity: '气瓶唯一标识', + product_name: '智能气阀名称', product_code: '气瓶编码', product_type_name: '气瓶类型名称', product_params: '气瓶规格参数', @@ -47,6 +48,13 @@ const collectionFieldAliases: Record> = { unbind_reason: '解绑原因', unit_price: '约定充装单价(元)', }, + 'gasorder_basic.items': { + product_name: '智能气阀名称', + product_code: '设备编码', + product_type_name: '设备类型', + product_params: '设备参数', + unit_price: '成交单价(元)', + }, }; const amountKeys = new Set([ @@ -149,7 +157,14 @@ export function optionLabel(option: ResourceRow) { } /** 工作人员关系额外展示角色,其他关系保持原有可读名称。 */ -export function relationOptionLabel(field: ResourceField, option: ResourceRow) { +export function relationOptionLabel( + field: ResourceField, + option: ResourceRow, + options: ResourceRow[] = [], +) { + if (field.relationOptionDisplay === 'product-name-type') { + return productNameTypeLabel(option, options); + } const configuredLabel = field.relationOptionLabelKey ? option[field.relationOptionLabelKey] : undefined; @@ -167,6 +182,26 @@ export function relationOptionLabel(field: ResourceField, option: ResourceRow) { : label; } +/** 合同气瓶优先显示具体设备名称和绑定时类型;同名同类型时追加编码以便区分。 */ +function productNameTypeLabel(option: ResourceRow, options: ResourceRow[]) { + const name = String(option.product_name ?? '').trim(); + const type = String(option.product_type_name ?? '').trim(); + const code = String(option.product_code ?? '').trim(); + const identity = String(option.identity ?? '').trim(); + if (!name) { + if (code) return type ? `${code}(${type})` : code; + return identity || type || '-'; + } + const base = type ? `${name}(${type})` : name; + const duplicateCount = options.filter( + (item) => + String(item.product_name ?? '').trim() === name && + String(item.product_type_name ?? '').trim() === type, + ).length; + if (duplicateCount <= 1) return base; + return `${base} · ${code || identity || '未编号'}`; +} + function relationLabel( field: ResourceField, identity: string, @@ -177,7 +212,11 @@ function relationLabel( ); if (!match) return identity; if (field.relation === '/staff_account') { - return relationOptionLabel(field, match); + return relationOptionLabel( + field, + match, + relationOptions[field.relation ?? ''] ?? [], + ); } return field.displayRelationLabel ? optionLabel(match) diff --git a/frontend/platform_admin/src/api/resources.ts b/frontend/platform_admin/src/api/resources.ts index e01f57a..7a55dea 100644 --- a/frontend/platform_admin/src/api/resources.ts +++ b/frontend/platform_admin/src/api/resources.ts @@ -87,6 +87,8 @@ export type ResourceField = { dynamicRelation?: ResourceDynamicRelation; /** 关联选项优先使用的业务展示字段。 */ relationOptionLabelKey?: string; + /** 关联选项的组合展示方式;合同气瓶使用设备名称、绑定类型快照和编码消歧。 */ + relationOptionDisplay?: 'product-name-type'; /** 该字段为真时,在关联选项后追加“默认地址”。 */ relationOptionDefaultKey?: string; /** 候选加载完成后自动选中该布尔字段为真的选项。 */ @@ -568,7 +570,7 @@ export const resources: ResourceUiDefinition[] = [ filterKey: 'gasorder_contract_identity', filterOnly: true, requiresParent: true, parentChangeMessage: '配送合同已变更,请重新选择该合同用户的收货地址', }, - }), f('gasorder_contract_product_identities', { label: '合同气瓶', required: true, type: 'identity-list', relation: '/gasorder_contract_product', placeholder: '请输入气瓶编码或类型搜索', relationOptionLabelKey: 'product_type_name', relationEmptyText: '该合同暂无可用气瓶,请先为合同绑定气瓶', relationLinkage: { parentKey: 'gasorder_contract_identity', optionParentKey: '', filterKey: 'contract_identity', filterOnly: true, requiresParent: true, parentChangeMessage: '配送合同已变更,请重新选择该合同可用的气瓶' } }), f('contact_name', { required: true }), f('contact_phone', { required: true }), f('discount_amount'), f('remark')], 'list', [ + }), f('gasorder_contract_product_identities', { label: '合同气瓶', required: true, type: 'identity-list', relation: '/gasorder_contract_product', placeholder: '请输入智能气阀名称、设备类型或设备编码搜索', relationOptionDisplay: 'product-name-type', relationEmptyText: '该合同暂无可用气瓶,请先为合同绑定气瓶', relationLinkage: { parentKey: 'gasorder_contract_identity', optionParentKey: '', filterKey: 'contract_identity', filterOnly: true, requiresParent: true, parentChangeMessage: '配送合同已变更,请重新选择该合同可用的气瓶' } }), f('contact_name', { required: true }), f('contact_phone', { required: true }), f('discount_amount'), f('remark')], 'list', [ { name: '分配订单', resource: '/gasorder_basic/:identity/assign', fields: [relation('delivery_basic_identity', '/delivery_basic', true), relation('staff_account_identity', '/staff_account', true, { staffRelation: { roles: ['delivery'], enabledOnly: true, workStatus: 'on_duty' } }), ...reason], visibleFor: { field: 'order_status', values: [16, 18] } }, { name: '开始罐装', resource: '/gasorder_basic/:identity/filling', fields: reason, visibleFor: { field: 'order_status', values: [18] } }, { name: '待配送', resource: '/gasorder_basic/:identity/ready', fields: reason, visibleFor: { field: 'order_status', values: [19] } }, diff --git a/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue b/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue index 4400258..1a05b4b 100644 --- a/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue +++ b/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue @@ -1,6 +1,6 @@