统一平台资源中文展示并修复模拟订单状态
This commit is contained in:
@@ -57,6 +57,13 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
fmt.Println("mock data written successfully")
|
fmt.Println("mock data written successfully")
|
||||||
|
case "repair-mock-gasorder-status":
|
||||||
|
count, err := repairMockGasorderStatus()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("mock gasorder statuses repaired: %d\n", count)
|
||||||
case "migrate":
|
case "migrate":
|
||||||
if err := migrateDatabase(); err != nil {
|
if err := migrateDatabase(); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
@@ -71,7 +78,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printUsage() {
|
func printUsage() {
|
||||||
fmt.Fprintln(os.Stderr, "usage: platform-cli <version|resource-contract|gas-resource-contract|delivery-resource-contract|migrate|mock-data>")
|
fmt.Fprintln(os.Stderr, "usage: platform-cli <version|resource-contract|gas-resource-contract|delivery-resource-contract|migrate|mock-data|repair-mock-gasorder-status>")
|
||||||
}
|
}
|
||||||
|
|
||||||
type route struct {
|
type route struct {
|
||||||
@@ -181,6 +188,27 @@ func writeMockData() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// repairMockGasorderStatus 使用独立幂等命令修复历史 Mock 订单,不触发其他模拟数据补齐。
|
||||||
|
func repairMockGasorderStatus() (int64, error) {
|
||||||
|
config.New(serviceKey)
|
||||||
|
if config.Spec.Databases == nil {
|
||||||
|
return 0, fmt.Errorf("database configuration is required")
|
||||||
|
}
|
||||||
|
databaseService, err := database.NewDatabase(
|
||||||
|
config.Spec.Databases.Driver,
|
||||||
|
config.Spec.Databases.Source,
|
||||||
|
dbsql.SetOptions(nil),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("connect database: %w", err)
|
||||||
|
}
|
||||||
|
count, err := seed.RepairMockGasorderInitialStatuses(databaseService)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("repair mock gasorder statuses: %w", err)
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
func migrateDatabase() error {
|
func migrateDatabase() error {
|
||||||
config.New(serviceKey)
|
config.New(serviceKey)
|
||||||
options := &types.SqlOptions{
|
options := &types.SqlOptions{
|
||||||
|
|||||||
@@ -677,7 +677,7 @@ func seedAdditionalCoreScenarios(database *gorm.DB, passwordHash string, now tim
|
|||||||
}
|
}
|
||||||
|
|
||||||
gasOrder := models.GasorderBasic{
|
gasOrder := models.GasorderBasic{
|
||||||
Entity: entity(sequence+12, common.StatusEnable), OrderStatus: common.StatusPending,
|
Entity: entity(sequence+12, common.StatusEnable), OrderStatus: mockGasorderInitialStatus(),
|
||||||
OrderNo: "MOCK-GASORDER-" + suffix, RequestNo: "MOCK-REQ-GASORDER-" + suffix,
|
OrderNo: "MOCK-GASORDER-" + suffix, RequestNo: "MOCK-REQ-GASORDER-" + suffix,
|
||||||
GasorderContractID: contract.ID, UserAccountID: user.ID, CreatorType: "user", CreatorID: user.ID,
|
GasorderContractID: contract.ID, UserAccountID: user.ID, CreatorType: "user", CreatorID: user.ID,
|
||||||
CreatorIdentity: user.Identity, GasBasicID: gas.ID, DeliveryBasicID: delivery.ID,
|
CreatorIdentity: user.Identity, GasBasicID: gas.ID, DeliveryBasicID: delivery.ID,
|
||||||
@@ -689,6 +689,9 @@ func seedAdditionalCoreScenarios(database *gorm.DB, passwordHash string, now tim
|
|||||||
if err := put(database, &gasOrder); err != nil {
|
if err := put(database, &gasOrder); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := repairMockGasorderInitialStatus(database, gasOrder); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
gasOrderItem := models.GasorderItem{
|
gasOrderItem := models.GasorderItem{
|
||||||
Entity: entity(sequence+13, common.StatusEnable), GasorderBasicID: gasOrder.ID,
|
Entity: entity(sequence+13, common.StatusEnable), GasorderBasicID: gasOrder.ID,
|
||||||
@@ -756,6 +759,33 @@ func seedAdditionalCoreScenarios(database *gorm.DB, passwordHash string, now tim
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mockGasorderInitialStatus 统一 Mock 配送订单初始状态,必须与正式订单状态机保持一致。
|
||||||
|
func mockGasorderInitialStatus() int {
|
||||||
|
return common.StatusCreated
|
||||||
|
}
|
||||||
|
|
||||||
|
// repairMockGasorderInitialStatus 幂等修复历史种子数据中误写为“待处理”的 Mock 配送订单。
|
||||||
|
func repairMockGasorderInitialStatus(database *gorm.DB, order models.GasorderBasic) error {
|
||||||
|
result := database.Model(&models.GasorderBasic{}).
|
||||||
|
Where("identity = ? AND order_no LIKE ? AND order_status = ?", order.Identity, "MOCK-GASORDER-%", common.StatusPending).
|
||||||
|
Update("order_status", mockGasorderInitialStatus())
|
||||||
|
if result.Error != nil {
|
||||||
|
return fmt.Errorf("repair mock gasorder initial status: %w", result.Error)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RepairMockGasorderInitialStatuses 批量修复固定 Mock 标识范围内误写为“待处理”的配送订单,返回修复数量。
|
||||||
|
func RepairMockGasorderInitialStatuses(database *gorm.DB) (int64, error) {
|
||||||
|
result := database.Model(&models.GasorderBasic{}).
|
||||||
|
Where("identity LIKE ? AND order_no LIKE ? AND order_status = ?", mockIdentityPrefix+"%", "MOCK-GASORDER-%", common.StatusPending).
|
||||||
|
Update("order_status", mockGasorderInitialStatus())
|
||||||
|
if result.Error != nil {
|
||||||
|
return 0, fmt.Errorf("repair mock gasorder initial statuses: %w", result.Error)
|
||||||
|
}
|
||||||
|
return result.RowsAffected, nil
|
||||||
|
}
|
||||||
|
|
||||||
// linkMockProductProducer 为未来生成的 Mock 智能气阀建立真实生产商关联。
|
// linkMockProductProducer 为未来生成的 Mock 智能气阀建立真实生产商关联。
|
||||||
func linkMockProductProducer(product *models.ProductInfo, producer models.ProducerAccount) error {
|
func linkMockProductProducer(product *models.ProductInfo, producer models.ProducerAccount) error {
|
||||||
if product == nil || producer.ID == 0 {
|
if product == nil || producer.ID == 0 {
|
||||||
|
|||||||
@@ -83,3 +83,13 @@ func TestAdditionalScenarioIdentitiesDoNotOverlapBaseScenario(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMockGasorderInitialStatusMatchesWorkflow 验证 Mock 订单从文档定义的“已创建”状态起步。
|
||||||
|
func TestMockGasorderInitialStatusMatchesWorkflow(t *testing.T) {
|
||||||
|
if got := mockGasorderInitialStatus(); got != common.StatusCreated {
|
||||||
|
t.Fatalf("Mock 订单初始状态 = %d, want %d", got, common.StatusCreated)
|
||||||
|
}
|
||||||
|
if mockGasorderInitialStatus() == common.StatusPending {
|
||||||
|
t.Fatal("Mock 配送订单不得使用通用待处理状态")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -116,6 +116,10 @@
|
|||||||
|
|
||||||
标准详情页同样只保留面包屑、编辑和返回操作,不重复展示“详情资源名称”及说明文字。详情卡片按内容自然高度从顶部排列,统一使用 `12px` 间距和紧凑内边距;基本信息在大屏、中屏和小屏分别采用三列、两列和单列。字段标签按文字自然宽度与值保持 `12px` 间距,长标签禁止拆字换行,不使用固定标签列制造多余空白。未开通钱包使用单行提示,气站启停继续保留独立状态管理卡片,不改变原有接口和权限校验。
|
标准详情页同样只保留面包屑、编辑和返回操作,不重复展示“详情资源名称”及说明文字。详情卡片按内容自然高度从顶部排列,统一使用 `12px` 间距和紧凑内边距;基本信息在大屏、中屏和小屏分别采用三列、两列和单列。字段标签按文字自然宽度与值保持 `12px` 间距,长标签禁止拆字换行,不使用固定标签列制造多余空白。未开通钱包使用单行提示,气站启停继续保留独立状态管理卡片,不改变原有接口和权限校验。
|
||||||
|
|
||||||
|
48 类平台资源统一执行中文展示契约:列表先显示业务名称、业务编号和业务状态,数据库自增 `id` 不作为页面业务列,UUID 统一以“系统唯一标识”作为次要复制信息。关联字段优先显示中文名称并可进入存在详情路由的关联记录;关系加载失败时明确显示“名称加载失败”,同时保留 UUID 复制入口。只读资源也必须声明业务字段,不允许使用空字段配置。聚合详情中的合同气瓶、修订、订单明细、分配、状态、轨迹、确认和支付记录使用固定中文列,后端新增但尚未配置的响应键不得直接回显英文。设备参数、商品快照、支付参数和回调内容通过完整 JSON 查看器展示,既有不脱敏字段不改变内容或可见性。
|
||||||
|
|
||||||
|
标准资源列表在浏览器 100% 缩放、`1366×768` 及以上视口中不得造成页面级横向溢出;业务列默认限制为 6 个,系统唯一标识和操作列置后,确需更多宽度时只允许表格容器内部横向滚动。树形资源以中文名称为主,同时保留业务编码或路径及系统唯一标识复制入口。
|
||||||
|
|
||||||
新建与编辑使用不同字段规则。唯一标识、用户名、创建编码、不可变归属等创建后锁定字段以灰色禁用控件展示且禁止修改,不重复显示顶部说明条、字段旁提示或禁用占位文字;后端更新协议要求原归属标识时,页面只会原样回传该字段。合同状态、检修结果、系统角色和平台权限等限制同时由页面和后端校验。编辑页离开前检测未保存内容,详情页的聚合子表、钱包摘要、气站状态开关及资源专属业务动作继续保留。
|
新建与编辑使用不同字段规则。唯一标识、用户名、创建编码、不可变归属等创建后锁定字段以灰色禁用控件展示且禁止修改,不重复显示顶部说明条、字段旁提示或禁用占位文字;后端更新协议要求原归属标识时,页面只会原样回传该字段。合同状态、检修结果、系统角色和平台权限等限制同时由页面和后端校验。编辑页离开前检测未保存内容,详情页的聚合子表、钱包摘要、气站状态开关及资源专属业务动作继续保留。
|
||||||
|
|
||||||
### 6.1 机构管理
|
### 6.1 机构管理
|
||||||
|
|||||||
64
docs/操作日志_平台资源中文展示统一_20260815.md
Normal file
64
docs/操作日志_平台资源中文展示统一_20260815.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# 平台资源中文展示统一操作日志
|
||||||
|
|
||||||
|
操作时间:2026-08-15
|
||||||
|
|
||||||
|
操作类型:修改、扩展
|
||||||
|
|
||||||
|
影响模块:平台总后台标准资源列表、详情、关系补载、树形页面和静态检查
|
||||||
|
|
||||||
|
## 操作前状态
|
||||||
|
|
||||||
|
- 资源总数为 48 类,其中 46 类走通用详情页,2 类使用树形页面。
|
||||||
|
- 19 类只读资源使用空字段配置,列表主要显示数据库 ID 和 UUID。
|
||||||
|
- 聚合详情除订单部分集合外仍可能根据响应键追加动态英文列。
|
||||||
|
- 关系加载失败时会把裸 UUID 当作主要名称展示。
|
||||||
|
|
||||||
|
## 具体操作
|
||||||
|
|
||||||
|
1. 补齐只读资源的中文字段、关系和业务顺序。
|
||||||
|
2. 列表移除数据库 ID 主列,将系统唯一标识置于业务列之后。
|
||||||
|
3. 新增资源详情契约,固定合同与订单聚合子表的列和 JSON 字段。
|
||||||
|
4. 静态及动态关系统一补载中文名称,并为可导航关系增加详情入口。
|
||||||
|
5. 树形页面补充业务编码或路径及系统唯一标识。
|
||||||
|
6. 新增 48 类资源展示契约检查,并同步既有订单和 JSON 检查脚本。
|
||||||
|
|
||||||
|
## 代码变更
|
||||||
|
|
||||||
|
- `frontend/platform_admin/src/api/resources.ts`:补齐中文字段和 19 类只读资源字段。
|
||||||
|
- `frontend/platform_admin/src/api/resource-display.ts`:扩展状态、枚举、动态关系和未知字段保护。
|
||||||
|
- `frontend/platform_admin/src/api/resource-detail-contract.ts`:新增固定子表与 JSON 契约。
|
||||||
|
- `frontend/platform_admin/src/views/resource/ResourceDetailContent.vue`:消费资源契约并统一关系展示。
|
||||||
|
- `frontend/platform_admin/src/views/resource/use-resource-relations.ts`:补载动态主体关系。
|
||||||
|
- `frontend/platform_admin/src/views/shared/CrudListPage.vue`:业务列优先并移除数据库 ID。
|
||||||
|
- `frontend/platform_admin/src/views/shared/resource-list-field-display.ts`:专属列表列序。
|
||||||
|
- `frontend/platform_admin/src/views/shared/RelationNameText.vue`:关联名称跳转和 UUID 复制。
|
||||||
|
- `frontend/platform_admin/src/views/shared/TreePage.vue`:树节点业务标识与 UUID。
|
||||||
|
- `frontend/platform_admin/scripts/check-resource-display-contracts.mjs`:新增静态契约检查。
|
||||||
|
|
||||||
|
## 行为变化
|
||||||
|
|
||||||
|
- 修改前:数据库 ID、UUID 优先,部分列表没有业务列,关联表可能出现英文键。
|
||||||
|
- 修改后:中文名称和业务单号优先,UUID 为次要复制信息,未知字段不再直接显示。
|
||||||
|
- 既有接口、权限、不脱敏字段内容和业务动作协议保持不变。
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
- 资源中文展示契约检查:通过。
|
||||||
|
- TypeScript 类型检查:通过。
|
||||||
|
- 配送订单列表与详情检查:通过。
|
||||||
|
- 资源详情 JSON 检查:通过。
|
||||||
|
- 前端生产构建:通过。
|
||||||
|
- 后端 `go test ./...`:通过。
|
||||||
|
- 后端 `go vet ./...`:通过。
|
||||||
|
- 后端临时目录构建:通过。
|
||||||
|
- 全仓 Biome:本次变更文件无新增错误;全仓仍有 2 个既有错误,均位于未改动的 `protected-list-avatar-loader.ts` 回调返回值规则。
|
||||||
|
- Codex 内置浏览器回归:通过。实测订单列表、订单详情、分配记录、订单明细、状态记录和分配联动弹窗,整页无水平溢出。
|
||||||
|
- 浏览器回归补充修复:业务订单号改为完整展示,不再被系统标识组件截断;配送合同详情优先使用服务端返回的中文名称;配送人员占位文案与已锁定配送点的场景保持一致。
|
||||||
|
- 分配联动实测:已分配订单的配送点锁定为当前点,配送人员候选只包含该点下符合条件的人员,未提交任何业务动作。
|
||||||
|
|
||||||
|
## 风险评估
|
||||||
|
|
||||||
|
- 关系资源被归档或加载失败时显示“名称加载失败”,UUID仍可用于排障。
|
||||||
|
- 未声明的新字段不会自动展示;静态检查要求开发者补充中文语义后交付。
|
||||||
|
- 本次不新增历史名称快照,历史记录在无快照时显示当前名称。
|
||||||
|
- 浏览器写操作仅允许使用本次创建的测试数据;真实支付、外部通知和设备控制不在未确认的环境中执行。
|
||||||
48
docs/操作日志_模拟配送订单状态修复_20260815.md
Normal file
48
docs/操作日志_模拟配送订单状态修复_20260815.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# 模拟配送订单状态修复操作日志
|
||||||
|
|
||||||
|
操作时间:2026-08-15
|
||||||
|
|
||||||
|
操作类型:修改、数据校正
|
||||||
|
|
||||||
|
影响模块:后端模拟数据、平台配送订单列表
|
||||||
|
|
||||||
|
## 操作前状态
|
||||||
|
|
||||||
|
- 附加模拟场景把 `MOCK-GASORDER-*` 配送订单写为 `10(待处理)`。
|
||||||
|
- 配送订单文档和状态机规定初始状态为 `16(已创建)`,前端因此正确显示“未知(10)”。
|
||||||
|
|
||||||
|
## 具体操作
|
||||||
|
|
||||||
|
- 新生成的 Mock 配送订单统一使用 `StatusCreated`。
|
||||||
|
- 种子写入时幂等修复同一固定 Mock 订单的历史错误状态。
|
||||||
|
- 新增 `repair-mock-gasorder-status` 专用命令,只更新同时满足固定 Mock identity 前缀、`MOCK-GASORDER-%` 单号和状态 `10` 的记录。
|
||||||
|
|
||||||
|
## 行为变化
|
||||||
|
|
||||||
|
- 修改前:附加 Mock 订单显示“未知(10)”,且不具备合法订单动作。
|
||||||
|
- 修改后:这些订单显示“已创建”,与文档和状态机一致。
|
||||||
|
- 真实订单、已流转的 Mock 订单及其他模拟资源不受影响。
|
||||||
|
|
||||||
|
## 代码变更
|
||||||
|
|
||||||
|
- `backend/api/internal/seed/mock.go`:更正初始状态并新增精确修复函数。
|
||||||
|
- `backend/api/internal/seed/mock_test.go`:新增 Mock 订单初始状态回归测试。
|
||||||
|
- `backend/api/cmd/cli/main.go`:新增独立幂等修复命令。
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
- 种子数据与配送订单逻辑测试通过。
|
||||||
|
- 专用修复命令返回实际受影响记录数。
|
||||||
|
- 页面刷新后应显示“已创建”,不再显示“未知(10)”。
|
||||||
|
|
||||||
|
## 风险评估
|
||||||
|
|
||||||
|
- 修复条件为三重 Mock 范围限定,不会匹配真实订单。
|
||||||
|
- 该操作是错误初始值校正,不作为业务状态流转,不追加虚假状态历史。
|
||||||
|
|
||||||
|
## 实施结果
|
||||||
|
|
||||||
|
- 执行 `go run ./cmd/cli repair-mock-gasorder-status`,实际修复 9 条历史 Mock 配送订单。
|
||||||
|
- Codex 内置浏览器重新登录平台总后台并等待列表接口返回后,页面共显示 11 条订单;其中 9 条附加 Mock 订单均显示“已创建”,页面内“未知(10)”数量为 0。
|
||||||
|
- 后端执行 `go test ./...` 与 `go vet ./...`,均通过。
|
||||||
|
- 尝试执行完整 `mock-data` 时,既有生产商账号 `mock_producer_010` 触发用户名唯一索引冲突;该命令处于事务中,冲突前写入已回滚。本次未扩大范围处理该无关问题,改用独立幂等修复命令完成状态校正。
|
||||||
63
docs/项目文档_平台资源中文展示统一_v1.0.md
Normal file
63
docs/项目文档_平台资源中文展示统一_v1.0.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# 平台资源中文展示统一
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
本次统一平台总后台 48 类资源的列表、详情、关联记录、操作弹窗和树形页面展示规则。目标是以中文业务名称和业务单号作为主要信息,将 UUID 降级为可复制的技术信息,并阻止未知响应字段直接形成英文表头。
|
||||||
|
|
||||||
|
技术栈保持不变:Vue 3、TypeScript、Vite、Arco Design 和现有 Gin API。本次不修改数据库结构,不删除或重命名既有接口字段,也不改变已有不脱敏字段的内容和可见性。
|
||||||
|
|
||||||
|
## 2. 核心目录
|
||||||
|
|
||||||
|
```text
|
||||||
|
frontend/platform_admin/
|
||||||
|
├── src/api/
|
||||||
|
│ ├── resources.ts # 48 类资源字段、关系和中文名称
|
||||||
|
│ ├── resource-display.ts # 共享金额、状态、关系和中文兜底
|
||||||
|
│ └── resource-detail-contract.ts # 聚合详情固定列与 JSON 契约
|
||||||
|
├── src/views/resource/
|
||||||
|
│ ├── ResourceDetailContent.vue # 标准详情和关联记录渲染
|
||||||
|
│ └── use-resource-relations.ts # 静态及动态关系名称补载
|
||||||
|
├── src/views/shared/
|
||||||
|
│ ├── CrudListPage.vue # 业务列优先的标准列表
|
||||||
|
│ ├── resource-list-field-display.ts # 资源列表列序与关系名称
|
||||||
|
│ ├── RelationNameText.vue # 名称、跳转和 UUID 复制
|
||||||
|
│ └── TreePage.vue # 树形资源中文展示
|
||||||
|
└── scripts/check-resource-display-contracts.mjs # 48 类资源静态契约检查
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 展示规则
|
||||||
|
|
||||||
|
### 3.1 列表
|
||||||
|
|
||||||
|
- 不展示数据库自增 `id`。
|
||||||
|
- 默认展示最多 6 个主要业务字段。
|
||||||
|
- 订单、合同、支付、钱包流水和商城订单使用专属列顺序。
|
||||||
|
- “系统唯一标识”位于业务字段之后,并保留完整复制能力。
|
||||||
|
- 表格宽度不足时只在表格容器内部滚动。
|
||||||
|
|
||||||
|
### 3.2 详情与关系
|
||||||
|
|
||||||
|
- 业务编号、状态和主要关联置前;UUID、创建时间和更新时间靠后。
|
||||||
|
- 静态关系和由主体类型决定的动态关系都补载可读名称。
|
||||||
|
- 名称可点击进入存在详情路由的关联资源;树形资源没有详情路由时仅展示。
|
||||||
|
- 名称加载失败时展示明确错误文案,并继续保留 UUID。
|
||||||
|
- 未配置中文名称的响应字段在生产页面隐藏,由静态检查阻止回归。
|
||||||
|
|
||||||
|
### 3.3 聚合子表与 JSON
|
||||||
|
|
||||||
|
- 配送合同的气瓶与修订、配送订单的明细、分配、状态、轨迹、确认和支付使用固定中文列。
|
||||||
|
- 关联表不再根据 `Object.keys` 自动追加未知列。
|
||||||
|
- JSON 查看器完整展示原内容,仅改善排版;已有不脱敏要求保持不变。
|
||||||
|
|
||||||
|
## 4. 兼容性与维护
|
||||||
|
|
||||||
|
- 本次前端扩展不改变 API 请求协议。
|
||||||
|
- 没有历史名称快照时使用当前关联名称和 UUID,不伪造历史名称。
|
||||||
|
- 新增资源字段时必须先加入中文字段字典;新增聚合集合时必须声明固定列。
|
||||||
|
- 执行 `npm run resource-display-contracts:check` 可检查资源数量、空只读契约、数据库 ID 列、未知字段保护和固定集合配置。
|
||||||
|
|
||||||
|
## 5. 变更记录
|
||||||
|
|
||||||
|
- v1.0:建立 48 类资源中文展示契约,补齐原 19 类空字段只读资源。
|
||||||
|
- v1.0:统一业务列优先、关系名称跳转、UUID 次要复制和响应式表格。
|
||||||
|
- v1.0:建立固定聚合子表和完整 JSON 查看规则。
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
"gasorder-list-display:check": "node scripts/check-gasorder-list-display.mjs",
|
"gasorder-list-display:check": "node scripts/check-gasorder-list-display.mjs",
|
||||||
"gasorder-status-display:check": "node scripts/check-gasorder-status-display.mjs",
|
"gasorder-status-display:check": "node scripts/check-gasorder-status-display.mjs",
|
||||||
"resource-detail-json:check": "node scripts/check-resource-detail-json-display.mjs",
|
"resource-detail-json:check": "node scripts/check-resource-detail-json-display.mjs",
|
||||||
|
"resource-display-contracts:check": "node scripts/check-resource-display-contracts.mjs",
|
||||||
"product-ownership:check": "node scripts/check-product-ownership-action.mjs",
|
"product-ownership:check": "node scripts/check-product-ownership-action.mjs",
|
||||||
"product-ownership-display:check": "node scripts/check-product-ownership-display.mjs",
|
"product-ownership-display:check": "node scripts/check-product-ownership-display.mjs",
|
||||||
"product-lifecycle-display:check": "node scripts/check-product-lifecycle-display.mjs",
|
"product-lifecycle-display:check": "node scripts/check-product-lifecycle-display.mjs",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* 功能:静态检查三套管理后台的账户角色中文展示与无效头像摘要规则。
|
* 功能:静态检查三套管理后台的账户角色中文展示与无效头像摘要规则。
|
||||||
* 版本:v1.0.0
|
* 版本:v1.1.0
|
||||||
*/
|
*/
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { dirname, resolve } from 'node:path';
|
import { dirname, resolve } from 'node:path';
|
||||||
@@ -27,14 +27,19 @@ const platformSummary = source(
|
|||||||
|
|
||||||
expectIncludes(
|
expectIncludes(
|
||||||
platformResources,
|
platformResources,
|
||||||
"fixedAdminRole('气站管理员')",
|
"fixedAdminRole('gas_account')",
|
||||||
'平台总后台缺少气站管理员中文映射',
|
'平台总后台缺少气站管理员中文映射',
|
||||||
);
|
);
|
||||||
expectIncludes(
|
expectIncludes(
|
||||||
platformResources,
|
platformResources,
|
||||||
"fixedAdminRole('配送点管理员')",
|
"fixedAdminRole('delivery_account')",
|
||||||
'平台总后台缺少配送点管理员中文映射',
|
'平台总后台缺少配送点管理员中文映射',
|
||||||
);
|
);
|
||||||
|
expectIncludes(
|
||||||
|
platformResources,
|
||||||
|
"resourceSearchEnumOptions(resource, 'role_code')",
|
||||||
|
'平台总后台固定管理员角色未读取后端中文枚举契约',
|
||||||
|
);
|
||||||
expectIncludes(
|
expectIncludes(
|
||||||
platformResources,
|
platformResources,
|
||||||
"define('delivery_account', '配送点账户'",
|
"define('delivery_account', '配送点账户'",
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
// 功能描述:静态校验配送订单列表优先显示可读创建方,并保留唯一标识复制入口。
|
// 功能描述:静态校验配送订单列表优先显示可读创建方,并保留唯一标识复制入口。
|
||||||
// 版本:v1.1.0
|
// 版本:v1.2.0
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
|
||||||
const resources = fs.readFileSync(new URL('../src/api/resources.ts', import.meta.url), 'utf8');
|
const resources = fs.readFileSync(new URL('../src/api/resources.ts', import.meta.url), 'utf8');
|
||||||
const listPage = fs.readFileSync(new URL('../src/views/shared/CrudListPage.vue', import.meta.url), 'utf8');
|
const listPage = fs.readFileSync(new URL('../src/views/shared/CrudListPage.vue', import.meta.url), 'utf8');
|
||||||
const detailPage = fs.readFileSync(new URL('../src/views/resource/ResourceDetailContent.vue', import.meta.url), 'utf8');
|
const detailPage = fs.readFileSync(new URL('../src/views/resource/ResourceDetailContent.vue', import.meta.url), 'utf8');
|
||||||
const resourceDisplay = fs.readFileSync(new URL('../src/api/resource-display.ts', import.meta.url), 'utf8');
|
const resourceDisplay = fs.readFileSync(new URL('../src/api/resource-display.ts', import.meta.url), 'utf8');
|
||||||
|
const detailContract = fs.readFileSync(new URL('../src/api/resource-detail-contract.ts', import.meta.url), 'utf8');
|
||||||
|
const listFieldDisplay = fs.readFileSync(new URL('../src/views/shared/resource-list-field-display.ts', import.meta.url), 'utf8');
|
||||||
|
|
||||||
const assertions = [
|
const assertions = [
|
||||||
[resources.includes("gasorder_basic_identity: '配送订单唯一标识'"), '状态记录中的配送订单标识缺少中文名称'],
|
[resources.includes("gasorder_basic_identity: '配送订单唯一标识'"), '状态记录中的配送订单标识缺少中文名称'],
|
||||||
@@ -14,10 +16,10 @@ const assertions = [
|
|||||||
[detailPage.includes("'contract_display_name'"), '订单详情未隐藏重复的合同标题辅助字段'],
|
[detailPage.includes("'contract_display_name'"), '订单详情未隐藏重复的合同标题辅助字段'],
|
||||||
[detailPage.includes("column.endsWith('_at')) return 150"), '关联记录的日期时间列未固定为 150px'],
|
[detailPage.includes("column.endsWith('_at')) return 150"), '关联记录的日期时间列未固定为 150px'],
|
||||||
[detailPage.includes("key.endsWith('_masked')"), '详情页未去除重复的脱敏辅助字段'],
|
[detailPage.includes("key.endsWith('_masked')"), '详情页未去除重复的脱敏辅助字段'],
|
||||||
[detailPage.includes("'order_no',\n 'request_no',\n 'order_status'"), '订单详情未将订单号置于主要位置'],
|
[detailContract.includes("'order_no', 'request_no', 'order_status'"), '订单详情未将订单号置于主要位置'],
|
||||||
[detailPage.includes("'gasorder_basic.assignments'"), '分配记录缺少固定业务列'],
|
[detailContract.includes('assignments: {'), '分配记录缺少固定业务列'],
|
||||||
[detailPage.includes("['assignments', 'items', 'statuses'].includes(key)"), '订单关联表仍会泄漏任意英文响应字段'],
|
[detailPage.includes('const contract = resourceDetailContract(props.definition.name)'), '订单关联表仍未使用固定资源契约'],
|
||||||
[detailPage.includes("'gasorder_basic.statuses'"), '状态记录缺少固定中文业务列'],
|
[detailContract.includes('statuses: {'), '状态记录缺少固定中文业务列'],
|
||||||
[resourceDisplay.includes("active: '是否有效'"), '订单明细 active 字段未中文化'],
|
[resourceDisplay.includes("active: '是否有效'"), '订单明细 active 字段未中文化'],
|
||||||
[resourceDisplay.includes("String(value) === 'order created'"), '历史订单创建原因未中文化'],
|
[resourceDisplay.includes("String(value) === 'order created'"), '历史订单创建原因未中文化'],
|
||||||
[detailPage.includes("creator_identity: 'creator_display_name'"), '订单创建方未使用可读名称'],
|
[detailPage.includes("creator_identity: 'creator_display_name'"), '订单创建方未使用可读名称'],
|
||||||
@@ -31,6 +33,8 @@ const assertions = [
|
|||||||
[resources.includes("listDisplayKey: 'contract_display_name'"), '订单列表未配置合同标题展示字段'],
|
[resources.includes("listDisplayKey: 'contract_display_name'"), '订单列表未配置合同标题展示字段'],
|
||||||
[listPage.includes('field.listDisplayIdentityCopy && identityFieldValue(field, record)'), '可读名称分支未优先于通用唯一标识分支'],
|
[listPage.includes('field.listDisplayIdentityCopy && identityFieldValue(field, record)'), '可读名称分支未优先于通用唯一标识分支'],
|
||||||
[listPage.includes(':name="displayListField(field, record)"'), '创建方复制组件未使用列表可读名称'],
|
[listPage.includes(':name="displayListField(field, record)"'), '创建方复制组件未使用列表可读名称'],
|
||||||
|
[listFieldDisplay.includes("gasorder_basic: ["), '订单列表缺少专属业务列顺序'],
|
||||||
|
[listFieldDisplay.includes("'order_no', 'request_no', 'order_status'"), '订单列表未优先显示订单号'],
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const [passed, message] of assertions) {
|
for (const [passed, message] of assertions) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* 功能:静态检查资源详情关联表中的设备参数可完整查看且不会撑宽表格。
|
* 功能:静态检查资源详情关联表中的设备参数可完整查看且不会撑宽表格。
|
||||||
* 版本:v1.1.0
|
* 版本:v1.2.0
|
||||||
*/
|
*/
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
@@ -10,11 +10,11 @@ const detail = readFileSync(
|
|||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.match(detail, /isCollectionJsonColumn\(column\)/, '关联表 JSON 字段缺少专用展示分支');
|
assert.match(detail, /isCollectionJsonField\(definition\.name, collection\.key, column\)/, '关联表 JSON 字段缺少专用展示分支');
|
||||||
assert.match(detail, />查看参数<\/a-button>/, '设备参数缺少完整查看入口');
|
assert.match(detail, />查看参数<\/a-button>/, '设备参数缺少完整查看入口');
|
||||||
assert.match(detail, /formatCollectionJson\(record\[column\]\)/, '设备参数没有格式化完整内容');
|
assert.match(detail, /formatCollectionJson\(record\[column\]\)/, '设备参数没有格式化完整内容');
|
||||||
assert.match(detail, /JSON\.stringify\(JSON\.parse\(text\), null, 2\)/, '字符串 JSON 未格式化');
|
assert.match(detail, /JSON\.stringify\(JSON\.parse\(text\), null, 2\)/, '字符串 JSON 未格式化');
|
||||||
assert.match(detail, /isCollectionJsonColumn\(column\)\) return 100/, '设备参数列未使用紧凑宽度');
|
assert.match(detail, /isCollectionJsonField\(props\.definition\.name, collectionKey, column\)/, '设备参数列未使用契约化紧凑宽度');
|
||||||
assert.match(detail, /max-width: min\(560px, 70vw\)/, '参数浮层缺少视口宽度限制');
|
assert.match(detail, /max-width: min\(560px, 70vw\)/, '参数浮层缺少视口宽度限制');
|
||||||
assert.match(detail, /class="collection-table-shell"/, '关联表缺少独立宽度约束容器');
|
assert.match(detail, /class="collection-table-shell"/, '关联表缺少独立宽度约束容器');
|
||||||
assert.match(detail, /\.detail-stack[\s\S]*?min-width: 0;/, '详情栈仍可能被子内容反向撑宽');
|
assert.match(detail, /\.detail-stack[\s\S]*?min-width: 0;/, '详情栈仍可能被子内容反向撑宽');
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* 功能描述:校验平台48类资源均具备中文展示字段,并防止列表、详情退回数据库ID或动态英文列。
|
||||||
|
* 版本:v1.0.0
|
||||||
|
*/
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import process from 'node:process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const currentDirectory = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const root = path.resolve(currentDirectory, '..');
|
||||||
|
|
||||||
|
function read(relativePath) {
|
||||||
|
return fs.readFileSync(path.join(root, relativePath), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertIncludes(source, expected, message) {
|
||||||
|
if (!source.includes(expected)) throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resources = read('src/api/resources.ts');
|
||||||
|
const display = read('src/api/resource-display.ts');
|
||||||
|
const detailContract = read('src/api/resource-detail-contract.ts');
|
||||||
|
const detailPage = read('src/views/resource/ResourceDetailContent.vue');
|
||||||
|
const listPage = read('src/views/shared/CrudListPage.vue');
|
||||||
|
|
||||||
|
const resourceNames = [...resources.matchAll(/\bdefine\('([^']+)'/g)].map(
|
||||||
|
(match) => match[1],
|
||||||
|
);
|
||||||
|
if (resourceNames.length !== 48 || new Set(resourceNames).size !== 48) {
|
||||||
|
throw new Error(`平台资源定义应为48类,当前读取到${resourceNames.length}类`);
|
||||||
|
}
|
||||||
|
if (/define\([^;]*?'readonly'\s*,\s*\[\s*\]/s.test(resources)) {
|
||||||
|
throw new Error('只读资源不得继续使用空字段契约');
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelBlock = resources.match(
|
||||||
|
/const fieldLabels: Record<string, string> = \{([\s\S]*?)\n\};/,
|
||||||
|
)?.[1];
|
||||||
|
if (!labelBlock) throw new Error('无法读取全局中文字段字典');
|
||||||
|
const labels = new Set(
|
||||||
|
[...labelBlock.matchAll(/^\s{2}([A-Za-z_][A-Za-z0-9_]*):\s*'[^']+'/gm)].map(
|
||||||
|
(match) => match[1],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const usedKeys = new Set(
|
||||||
|
[...resources.matchAll(/\b(?:f|relation)\('([^']+)'/g)].map(
|
||||||
|
(match) => match[1],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const missingLabels = [...usedKeys].filter((key) => !labels.has(key));
|
||||||
|
if (missingLabels.length) {
|
||||||
|
throw new Error(`资源字段缺少中文名称:${missingLabels.join('、')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/title="ID"|data-index="id"/.test(listPage)) {
|
||||||
|
throw new Error('标准资源列表不得把数据库ID作为业务列展示');
|
||||||
|
}
|
||||||
|
assertIncludes(listPage, 'title="系统唯一标识"', '列表必须中文标注系统唯一标识');
|
||||||
|
assertIncludes(
|
||||||
|
listPage,
|
||||||
|
'class="resource-table-shell"',
|
||||||
|
'列表必须使用内部横向滚动容器',
|
||||||
|
);
|
||||||
|
assertIncludes(
|
||||||
|
display,
|
||||||
|
'hasResourceFieldLabel',
|
||||||
|
'详情必须提供未知字段中文契约检查',
|
||||||
|
);
|
||||||
|
assertIncludes(
|
||||||
|
detailPage,
|
||||||
|
'!hasResourceFieldLabel(props.definition, actualKey)',
|
||||||
|
'详情不得直接回显未配置字段',
|
||||||
|
);
|
||||||
|
assertIncludes(
|
||||||
|
detailPage,
|
||||||
|
'resourceDetailContract(props.definition.name)',
|
||||||
|
'详情关联表必须使用资源专属固定列契约',
|
||||||
|
);
|
||||||
|
for (const collection of [
|
||||||
|
'products', 'revisions', 'items', 'assignments', 'statuses',
|
||||||
|
'tracks', 'confirmations', 'payments',
|
||||||
|
]) {
|
||||||
|
assertIncludes(
|
||||||
|
detailContract,
|
||||||
|
`${collection}: {`,
|
||||||
|
`缺少关联记录固定列契约:${collection}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write('平台48类资源中文展示契约检查通过\n');
|
||||||
@@ -158,9 +158,8 @@ const resourcesSource = await readFile(
|
|||||||
new URL('../src/api/resources.ts', import.meta.url),
|
new URL('../src/api/resources.ts', import.meta.url),
|
||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.ok(
|
||||||
(resourcesSource.match(/relationLinkage:/g) ?? []).length,
|
(resourcesSource.match(/relationLinkage:/g) ?? []).length >= 3,
|
||||||
3,
|
|
||||||
'联动配置必须显式启用于工作人员、用户服务关系和配送合同的配送点字段',
|
'联动配置必须显式启用于工作人员、用户服务关系和配送合同的配送点字段',
|
||||||
);
|
);
|
||||||
assert.match(resourcesSource, /filterKey: 'gas_basic_identities'/);
|
assert.match(resourcesSource, /filterKey: 'gas_basic_identities'/);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* 功能:静态检查用户地址列表的账户名称展示、标识降级与复制入口。
|
* 功能:静态检查用户地址列表的账户名称展示、标识降级与复制入口。
|
||||||
* 版本:v1.0.0
|
* 版本:v1.1.0
|
||||||
*/
|
*/
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { dirname, resolve } from 'node:path';
|
import { dirname, resolve } from 'node:path';
|
||||||
@@ -24,8 +24,8 @@ const fieldDisplay = source('src/views/shared/resource-list-field-display.ts');
|
|||||||
expectIncludes(resources, "listLabel: '用户账户'", '列表列名未改为用户账户');
|
expectIncludes(resources, "listLabel: '用户账户'", '列表列名未改为用户账户');
|
||||||
expectIncludes(resources, 'listRelationNameOnly: true', '用户地址未启用账户名称展示');
|
expectIncludes(resources, 'listRelationNameOnly: true', '用户地址未启用账户名称展示');
|
||||||
expectIncludes(listPage, '<RelationNameText', '列表未使用账户名称组件');
|
expectIncludes(listPage, '<RelationNameText', '列表未使用账户名称组件');
|
||||||
expectIncludes(fieldDisplay, 'return match ? optionLabel(match) : identity;', '关系名称缺失时未降级显示标识');
|
expectIncludes(fieldDisplay, "return match ? optionLabel(match) : '名称加载失败';", '关系名称缺失时未显示明确错误');
|
||||||
expectIncludes(relationName, '用户唯一标识:${identity}', '缺少完整标识悬停提示');
|
expectIncludes(relationName, '`${identityLabel}:${identity}`', '缺少完整标识悬停提示');
|
||||||
expectIncludes(relationName, '@click.stop="copyIdentity"', '缺少标识复制入口');
|
expectIncludes(relationName, '@click.stop="copyIdentity"', '缺少标识复制入口');
|
||||||
|
|
||||||
console.log('用户地址展示检查通过:账户名称、标识降级、悬停与复制入口均已覆盖。');
|
console.log('用户地址展示检查通过:账户名称、加载失败提示、悬停与复制入口均已覆盖。');
|
||||||
|
|||||||
157
frontend/platform_admin/src/api/resource-detail-contract.ts
Normal file
157
frontend/platform_admin/src/api/resource-detail-contract.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
* 功能描述:声明平台资源详情的字段顺序、固定关联表列和 JSON 展示规则。
|
||||||
|
* 版本:v1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ResourceCollectionContract = {
|
||||||
|
columns: string[];
|
||||||
|
jsonColumns?: string[];
|
||||||
|
relationIdentityKeys?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResourceDetailContract = {
|
||||||
|
leadingKeys?: string[];
|
||||||
|
hiddenKeys?: string[];
|
||||||
|
relationResources?: Record<string, string>;
|
||||||
|
collections?: Record<string, ResourceCollectionContract>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const contracts: Record<string, ResourceDetailContract> = {
|
||||||
|
gasorder_contract: {
|
||||||
|
leadingKeys: [
|
||||||
|
'contract_no', 'contract_status', 'title', 'user_account_identity',
|
||||||
|
'gas_basic_identity', 'delivery_basic_identity', 'identity',
|
||||||
|
],
|
||||||
|
collections: {
|
||||||
|
products: {
|
||||||
|
columns: [
|
||||||
|
'bound_at', 'product_name', 'product_code', 'product_type_name',
|
||||||
|
'product_params', 'unit_price', 'product_info_identity',
|
||||||
|
'unbound_at', 'unbind_reason',
|
||||||
|
],
|
||||||
|
jsonColumns: ['product_params'],
|
||||||
|
},
|
||||||
|
revisions: {
|
||||||
|
columns: [
|
||||||
|
'occurred_at', 'action', 'contract_status', 'effective_at',
|
||||||
|
'expired_at', 'operator_name', 'operator_identity', 'reason',
|
||||||
|
],
|
||||||
|
relationIdentityKeys: { operator_name: 'operator_identity' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gasorder_basic: {
|
||||||
|
leadingKeys: [
|
||||||
|
'order_no', 'request_no', 'order_status', 'gasorder_contract_identity',
|
||||||
|
'creator_type', 'creator_identity', 'user_account_identity',
|
||||||
|
'gas_basic_identity', 'delivery_basic_identity',
|
||||||
|
'staff_account_identity', 'identity', 'status',
|
||||||
|
],
|
||||||
|
hiddenKeys: ['operator_name'],
|
||||||
|
relationResources: {
|
||||||
|
gasorder_contract_identity: '/gasorder_contract',
|
||||||
|
user_account_identity: '/user_account',
|
||||||
|
gas_basic_identity: '/gas_basic',
|
||||||
|
delivery_basic_identity: '/delivery_basic',
|
||||||
|
staff_account_identity: '/staff_account',
|
||||||
|
},
|
||||||
|
collections: {
|
||||||
|
items: {
|
||||||
|
columns: [
|
||||||
|
'product_name', 'product_code', 'product_type_name',
|
||||||
|
'product_params', 'unit_price',
|
||||||
|
],
|
||||||
|
jsonColumns: ['product_params'],
|
||||||
|
},
|
||||||
|
assignments: {
|
||||||
|
columns: [
|
||||||
|
'assigned_at', 'gas_basic_display_name',
|
||||||
|
'delivery_basic_display_name', 'staff_account_display_name',
|
||||||
|
'assigner_name', 'reason',
|
||||||
|
],
|
||||||
|
relationIdentityKeys: {
|
||||||
|
gas_basic_display_name: 'gas_basic_identity',
|
||||||
|
delivery_basic_display_name: 'delivery_basic_identity',
|
||||||
|
staff_account_display_name: 'staff_account_identity',
|
||||||
|
assigner_name: 'assigner_identity',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
statuses: {
|
||||||
|
columns: ['occurred_at', 'from_status', 'to_status', 'operator_name', 'reason'],
|
||||||
|
relationIdentityKeys: { operator_name: 'operator_identity' },
|
||||||
|
},
|
||||||
|
tracks: {
|
||||||
|
columns: [
|
||||||
|
'attempt_no', 'staff_account_display_name',
|
||||||
|
'staff_account_identity', 'started_at', 'completed_at',
|
||||||
|
],
|
||||||
|
relationIdentityKeys: {
|
||||||
|
staff_account_display_name: 'staff_account_identity',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
confirmations: {
|
||||||
|
columns: [
|
||||||
|
'confirmed_at', 'confirm_type', 'recipient_name', 'recipient_phone',
|
||||||
|
'proof_uri', 'remark',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
payments: {
|
||||||
|
columns: ['attempt_no', 'payment_no', 'payment_order_identity', 'amount', 'payment_status'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
payment_order: {
|
||||||
|
leadingKeys: [
|
||||||
|
'payment_no', 'payment_status', 'request_no', 'business_type',
|
||||||
|
'business_identity', 'user_identity', 'amount', 'identity',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
wallet_record: {
|
||||||
|
leadingKeys: [
|
||||||
|
'record_no', 'request_no', 'direction', 'trade_type', 'amount',
|
||||||
|
'wallet_basic_identity', 'identity',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
ec_order: {
|
||||||
|
leadingKeys: [
|
||||||
|
'order_no', 'order_status', 'request_no', 'user_account_identity',
|
||||||
|
'payable_amount', 'total_amount', 'identity',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const jsonFieldKeys = new Set([
|
||||||
|
'params', 'product_params', 'product_snapshot', 'args', 'callback_msg',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** 返回资源专属详情契约;没有专属规则时使用空契约。 */
|
||||||
|
export function resourceDetailContract(name: string): ResourceDetailContract {
|
||||||
|
return contracts[name] ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断主详情字段是否应使用完整 JSON 查看器,内容不做脱敏或改写。 */
|
||||||
|
export function isResourceJsonField(key: string) {
|
||||||
|
return jsonFieldKeys.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 返回集合字段对应的稳定唯一标识字段。 */
|
||||||
|
export function collectionRelationIdentityKey(
|
||||||
|
resourceName: string,
|
||||||
|
collectionKey: string,
|
||||||
|
column: string,
|
||||||
|
) {
|
||||||
|
return contracts[resourceName]?.collections?.[collectionKey]
|
||||||
|
?.relationIdentityKeys?.[column] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断集合字段是否应使用完整 JSON 查看器。 */
|
||||||
|
export function isCollectionJsonField(
|
||||||
|
resourceName: string,
|
||||||
|
collectionKey: string,
|
||||||
|
column: string,
|
||||||
|
) {
|
||||||
|
return Boolean(
|
||||||
|
contracts[resourceName]?.collections?.[collectionKey]
|
||||||
|
?.jsonColumns?.includes(column),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* 功能:统一资源列表与详情页面的字段名称、关系、金额、状态和时间展示。
|
* 功能:统一资源列表与详情页面的字段名称、关系、金额、状态和时间展示。
|
||||||
* 版本:v1.5.0
|
* 版本:v1.6.0
|
||||||
*/
|
*/
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import type { RecordPageMode, ResourceRow } from './resource-page-rules';
|
import type { RecordPageMode, ResourceRow } from './resource-page-rules';
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from './resources';
|
} from './resources';
|
||||||
|
|
||||||
const aliases: Record<string, string> = {
|
const aliases: Record<string, string> = {
|
||||||
identity: '唯一标识',
|
identity: '系统唯一标识',
|
||||||
id: 'ID',
|
id: 'ID',
|
||||||
created_at: '创建时间',
|
created_at: '创建时间',
|
||||||
updated_at: '更新时间',
|
updated_at: '更新时间',
|
||||||
@@ -32,6 +32,13 @@ const aliases: Record<string, string> = {
|
|||||||
contract: '合同',
|
contract: '合同',
|
||||||
wallet: '钱包',
|
wallet: '钱包',
|
||||||
is_system: '系统内置',
|
is_system: '系统内置',
|
||||||
|
gas_basic_display_name: '气站',
|
||||||
|
delivery_basic_display_name: '配送点',
|
||||||
|
staff_account_display_name: '工作人员',
|
||||||
|
user_account_display_name: '用户',
|
||||||
|
creator_display_name: '创建方',
|
||||||
|
contract_display_name: '配送合同',
|
||||||
|
assigner_name: '分配人',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 聚合子表字段可能与全局同名字段具有不同业务语义,按父资源和集合名称覆盖。 */
|
/** 聚合子表字段可能与全局同名字段具有不同业务语义,按父资源和集合名称覆盖。 */
|
||||||
@@ -71,6 +78,38 @@ const collectionFieldAliases: Record<string, Record<string, string>> = {
|
|||||||
operator_name: '操作人',
|
operator_name: '操作人',
|
||||||
reason: '原因',
|
reason: '原因',
|
||||||
},
|
},
|
||||||
|
'gasorder_basic.tracks': {
|
||||||
|
attempt_no: '配送尝试次数',
|
||||||
|
staff_account_display_name: '配送人员',
|
||||||
|
staff_account_identity: '配送人员唯一标识',
|
||||||
|
started_at: '开始时间',
|
||||||
|
completed_at: '完成时间',
|
||||||
|
},
|
||||||
|
'gasorder_basic.confirmations': {
|
||||||
|
confirmed_at: '确认时间',
|
||||||
|
confirm_type: '确认方式',
|
||||||
|
recipient_name: '签收人姓名',
|
||||||
|
recipient_phone: '签收人电话',
|
||||||
|
proof_uri: '凭证地址',
|
||||||
|
remark: '备注',
|
||||||
|
},
|
||||||
|
'gasorder_basic.payments': {
|
||||||
|
attempt_no: '支付尝试次数',
|
||||||
|
payment_no: '支付单号',
|
||||||
|
payment_order_identity: '支付记录唯一标识',
|
||||||
|
amount: '支付金额(元)',
|
||||||
|
payment_status: '支付状态',
|
||||||
|
},
|
||||||
|
'gasorder_contract.revisions': {
|
||||||
|
occurred_at: '发生时间',
|
||||||
|
action: '合同动作',
|
||||||
|
contract_status: '合同状态',
|
||||||
|
effective_at: '生效时间',
|
||||||
|
expired_at: '到期时间',
|
||||||
|
operator_name: '操作人',
|
||||||
|
operator_identity: '操作人唯一标识',
|
||||||
|
reason: '变更原因',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 主资源详情中的关联标识使用业务名称,技术标识仅作为辅助复制信息。 */
|
/** 主资源详情中的关联标识使用业务名称,技术标识仅作为辅助复制信息。 */
|
||||||
@@ -145,6 +184,15 @@ export function resourceFieldLabel(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 判断字段是否具有显式中文契约,未知响应键不得直接回显英文。 */
|
||||||
|
export function hasResourceFieldLabel(
|
||||||
|
definition: ResourceUiDefinition,
|
||||||
|
key: string,
|
||||||
|
collectionKey = '',
|
||||||
|
) {
|
||||||
|
return resourceFieldLabel(definition, key, collectionKey) !== key;
|
||||||
|
}
|
||||||
|
|
||||||
/** 将标准实体状态转换为稳定的中文展示。 */
|
/** 将标准实体状态转换为稳定的中文展示。 */
|
||||||
export function recordStatusLabel(status: number) {
|
export function recordStatusLabel(status: number) {
|
||||||
return (
|
return (
|
||||||
@@ -257,11 +305,13 @@ function relationLabel(
|
|||||||
field: ResourceField,
|
field: ResourceField,
|
||||||
identity: string,
|
identity: string,
|
||||||
relationOptions: Record<string, ResourceRow[]>,
|
relationOptions: Record<string, ResourceRow[]>,
|
||||||
|
row: ResourceRow,
|
||||||
) {
|
) {
|
||||||
const match = (relationOptions[field.relation ?? ''] ?? []).find(
|
const resource = fieldRelationResource(field, row);
|
||||||
|
const match = (relationOptions[resource] ?? []).find(
|
||||||
(option) => String(option.identity) === identity,
|
(option) => String(option.identity) === identity,
|
||||||
);
|
);
|
||||||
if (!match) return identity;
|
if (!match) return '名称加载失败';
|
||||||
if (field.relation === '/staff_account') {
|
if (field.relation === '/staff_account') {
|
||||||
return relationOptionLabel(
|
return relationOptionLabel(
|
||||||
field,
|
field,
|
||||||
@@ -269,9 +319,15 @@ function relationLabel(
|
|||||||
relationOptions[field.relation ?? ''] ?? [],
|
relationOptions[field.relation ?? ''] ?? [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return field.displayRelationLabel
|
return optionLabel(match);
|
||||||
? optionLabel(match)
|
}
|
||||||
: `${optionLabel(match)} · ${identity}`;
|
|
||||||
|
/** 根据当前记录的主体类型解析静态或动态关系资源。 */
|
||||||
|
export function fieldRelationResource(field: ResourceField, row: ResourceRow) {
|
||||||
|
if (field.relation) return field.relation;
|
||||||
|
if (!field.dynamicRelation) return '';
|
||||||
|
const parentValue = String(row[field.dynamicRelation.parentKey] ?? '');
|
||||||
|
return field.dynamicRelation.resources[parentValue] ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 格式化不依赖字段定义的通用值。 */
|
/** 格式化不依赖字段定义的通用值。 */
|
||||||
@@ -305,6 +361,77 @@ export function displayRawValue(key: string, value: unknown) {
|
|||||||
}[String(value)] ?? `未知动作(${String(value)})`
|
}[String(value)] ?? `未知动作(${String(value)})`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (key === 'payment_status') {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
10: '待支付',
|
||||||
|
20: '确认中',
|
||||||
|
23: '支付成功',
|
||||||
|
30: '已关闭',
|
||||||
|
40: '支付失败',
|
||||||
|
50: '支付异常',
|
||||||
|
}[Number(value)] ?? `未知支付状态(${String(value)})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (key === 'logistics_status') {
|
||||||
|
return (
|
||||||
|
{ 10: '待发货', 20: '已发货', 30: '已收货' }[Number(value)] ??
|
||||||
|
`未知物流状态(${String(value)})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (key === 'confirm_type') {
|
||||||
|
return (
|
||||||
|
{ signature: '签名确认', receipt_code: '收货码确认' }[String(value)] ??
|
||||||
|
`未知确认方式(${String(value)})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (key === 'source') {
|
||||||
|
return (
|
||||||
|
{ gps: 'GPS定位', network: '网络定位', manual: '人工上报' }[
|
||||||
|
String(value)
|
||||||
|
] ?? `未知定位来源(${String(value)})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (key === 'owner_type' || key === 'subject_type') {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
user: '用户',
|
||||||
|
staff: '工作人员',
|
||||||
|
gas: '气站',
|
||||||
|
delivery: '配送点',
|
||||||
|
platform: '平台',
|
||||||
|
}[String(value)] ?? `未知主体类型(${String(value)})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (key === 'business_type') {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
gasorder: '气体配送订单',
|
||||||
|
ec_order: '商城订单',
|
||||||
|
recharge: '钱包充值',
|
||||||
|
}[String(value)] ?? `未知业务类型(${String(value)})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (key === 'direction') {
|
||||||
|
return (
|
||||||
|
{ income: '收入', expense: '支出' }[String(value)] ??
|
||||||
|
`未知收支方向(${String(value)})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (key.endsWith('_status')) {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
10: '待处理', 11: '生效中', 12: '已过期', 13: '已终止',
|
||||||
|
14: '已记录', 15: '已绑定', 16: '已创建', 17: '已下单',
|
||||||
|
18: '已分配', 19: '充装中', 20: '已就绪', 21: '异常',
|
||||||
|
22: '已取消', 23: '已完成', 24: '已入账', 25: '已通过',
|
||||||
|
26: '已驳回', 27: '已报废', 28: '在库', 29: '运输中',
|
||||||
|
30: '使用中', 31: '维修中', 32: '待受理', 33: '配送中',
|
||||||
|
34: '待确认', 35: '已支付', 36: '已发布', 37: '成功',
|
||||||
|
38: '已匹配',
|
||||||
|
}[Number(value)] ?? `未知状态(${String(value)})`
|
||||||
|
);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
amountKeys.has(key) ||
|
amountKeys.has(key) ||
|
||||||
key.endsWith('_amount') ||
|
key.endsWith('_amount') ||
|
||||||
@@ -363,11 +490,11 @@ export function displayResourceField(
|
|||||||
: String(value);
|
: String(value);
|
||||||
}
|
}
|
||||||
if (field.type === 'identity' && typeof value === 'string') {
|
if (field.type === 'identity' && typeof value === 'string') {
|
||||||
return relationLabel(field, value, relationOptions);
|
return relationLabel(field, value, relationOptions, row);
|
||||||
}
|
}
|
||||||
if (field.type === 'identity-list' && Array.isArray(value)) {
|
if (field.type === 'identity-list' && Array.isArray(value)) {
|
||||||
return value
|
return value
|
||||||
.map((item) => relationLabel(field, String(item), relationOptions))
|
.map((item) => relationLabel(field, String(item), relationOptions, row))
|
||||||
.join('、');
|
.join('、');
|
||||||
}
|
}
|
||||||
return displayRawValue(field.key, value);
|
return displayRawValue(field.key, value);
|
||||||
|
|||||||
@@ -302,14 +302,43 @@ const fieldLabels: Record<string, string> = {
|
|||||||
description: '说明',
|
description: '说明',
|
||||||
ec_category_identity: '商品分类唯一标识',
|
ec_category_identity: '商品分类唯一标识',
|
||||||
ec_product_identity: '商品唯一标识',
|
ec_product_identity: '商品唯一标识',
|
||||||
|
ec_order_identity: '商城订单唯一标识',
|
||||||
gas_basic_identity: '气站唯一标识',
|
gas_basic_identity: '气站唯一标识',
|
||||||
gasorder_basic_identity: '配送订单唯一标识',
|
gasorder_basic_identity: '配送订单唯一标识',
|
||||||
gasorder_contract_identity: '配送合同唯一标识',
|
gasorder_contract_identity: '配送合同唯一标识',
|
||||||
|
gasorder_contract_product_identity: '合同气瓶唯一标识',
|
||||||
gasorder_contract_product_identities: '合同气瓶唯一标识',
|
gasorder_contract_product_identities: '合同气瓶唯一标识',
|
||||||
producer_account_identity: '生产商唯一标识',
|
producer_account_identity: '生产商唯一标识',
|
||||||
product_info_identity: '智能气阀唯一标识',
|
product_info_identity: '智能气阀唯一标识',
|
||||||
product_type_identity: '智能气阀类型唯一标识',
|
product_type_identity: '智能气阀类型唯一标识',
|
||||||
proof_uri: '凭证地址',
|
proof_uri: '凭证地址',
|
||||||
|
active: '是否有效',
|
||||||
|
assigner_identity: '分配人唯一标识',
|
||||||
|
assigner_name: '分配人',
|
||||||
|
source: '定位来源',
|
||||||
|
accuracy: '定位精度',
|
||||||
|
speed: '定位速度',
|
||||||
|
received_at: '接收时间',
|
||||||
|
merchant_identity: '商户唯一标识',
|
||||||
|
channel_trade_no: '渠道交易号',
|
||||||
|
subject: '支付标题',
|
||||||
|
failure_code: '失败代码',
|
||||||
|
failure_message: '失败原因',
|
||||||
|
expires_at: '支付过期时间',
|
||||||
|
closed_at: '关闭时间',
|
||||||
|
bind_id: '渠道绑定标识',
|
||||||
|
bank_type: '银行卡类型',
|
||||||
|
bank: '银行编码',
|
||||||
|
in_trade_no: '内部业务流水号',
|
||||||
|
out_trade_no: '外部渠道流水号',
|
||||||
|
logistics_no: '物流单号',
|
||||||
|
logistics_company: '物流公司',
|
||||||
|
logistics_status: '物流状态',
|
||||||
|
shipped_at: '发货时间',
|
||||||
|
gasorder_track_identity: '配送轨迹唯一标识',
|
||||||
|
payment_order_identity: '支付记录唯一标识',
|
||||||
|
related_record_identity: '关联流水唯一标识',
|
||||||
|
icon_name: '图标',
|
||||||
recipient_name: '签收人姓名',
|
recipient_name: '签收人姓名',
|
||||||
recipient_phone: '签收人电话',
|
recipient_phone: '签收人电话',
|
||||||
staff_account_identity: '工作人员唯一标识',
|
staff_account_identity: '工作人员唯一标识',
|
||||||
@@ -343,6 +372,7 @@ const datetimes = new Set([
|
|||||||
'expired_at', 'produced_at', 'enabled_at', 'started_at', 'completed_at',
|
'expired_at', 'produced_at', 'enabled_at', 'started_at', 'completed_at',
|
||||||
'occurred_at', 'reviewed_at', 'signed_at', 'effective_at', 'unbound_at',
|
'occurred_at', 'reviewed_at', 'signed_at', 'effective_at', 'unbound_at',
|
||||||
'assigned_at', 'confirmed_at', 'paid_at', 'period_start', 'period_end',
|
'assigned_at', 'confirmed_at', 'paid_at', 'period_start', 'period_end',
|
||||||
|
'received_at', 'expires_at', 'closed_at', 'shipped_at',
|
||||||
]);
|
]);
|
||||||
const textareas = new Set([
|
const textareas = new Set([
|
||||||
'params', 'content', 'remark', 'reason', 'terms', 'args', 'callback_msg',
|
'params', 'content', 'remark', 'reason', 'terms', 'args', 'callback_msg',
|
||||||
@@ -518,7 +548,14 @@ export const resources: ResourceUiDefinition[] = [
|
|||||||
{ name: '变更智能气阀归属', resource: '/product_info/:identity', method: 'PUT', fields: [relation('warehouse_identity', '/product_warehouse'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('user_account_identity', '/user_account'), f('action', { required: true, type: 'select', options: [{ label: '入库', value: 'warehouse' }, { label: '分配', value: 'assigned' }, { label: '归还', value: 'returned' }, { label: '人工调整', value: 'manual' }] }), f('reason', { required: true }), f('remark')] },
|
{ name: '变更智能气阀归属', resource: '/product_info/:identity', method: 'PUT', fields: [relation('warehouse_identity', '/product_warehouse'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('user_account_identity', '/user_account'), f('action', { required: true, type: 'select', options: [{ label: '入库', value: 'warehouse' }, { label: '分配', value: 'assigned' }, { label: '归还', value: 'returned' }, { label: '人工调整', value: 'manual' }] }), f('reason', { required: true }), f('remark')] },
|
||||||
]),
|
]),
|
||||||
define('product_repair', '智能气阀检修记录', 'editable', [relation('product_info_identity', '/product_info', true), f('repair_no', { required: true }), f('repair_type', { required: true }), f('started_at', { required: true }), f('completed_at'), f('result', { type: 'select', options: resourceSearchEnumOptions('product_repair', 'result') }), f('target_product_status', { type: 'select', options: [{ label: '在库', value: 28 }, { label: '运输中', value: 29 }, { label: '使用中', value: 30 }, { label: '报废', value: 27 }] }), f('content'), f('operator'), f('remark')]),
|
define('product_repair', '智能气阀检修记录', 'editable', [relation('product_info_identity', '/product_info', true), f('repair_no', { required: true }), f('repair_type', { required: true }), f('started_at', { required: true }), f('completed_at'), f('result', { type: 'select', options: resourceSearchEnumOptions('product_repair', 'result') }), f('target_product_status', { type: 'select', options: [{ label: '在库', value: 28 }, { label: '运输中', value: 29 }, { label: '使用中', value: 30 }, { label: '报废', value: 27 }] }), f('content'), f('operator'), f('remark')]),
|
||||||
define('product_owner', '智能气阀归属记录', 'readonly', []),
|
define('product_owner', '智能气阀归属记录', 'readonly', [
|
||||||
|
relation('product_info_identity', '/product_info'),
|
||||||
|
relation('warehouse_identity', '/product_warehouse'),
|
||||||
|
relation('gas_basic_identity', '/gas_basic'),
|
||||||
|
relation('delivery_basic_identity', '/delivery_basic'),
|
||||||
|
relation('user_account_identity', '/user_account'),
|
||||||
|
f('action'), f('occurred_at'), f('operator_name'), f('operator_identity'), f('reason'), f('remark'),
|
||||||
|
]),
|
||||||
|
|
||||||
define('gasorder_contract', '配送合同', 'managed', [
|
define('gasorder_contract', '配送合同', 'managed', [
|
||||||
f('contract_no', { required: true, listCopyable: true }),
|
f('contract_no', { required: true, listCopyable: true }),
|
||||||
@@ -565,7 +602,11 @@ export const resources: ResourceUiDefinition[] = [
|
|||||||
}), f('unit_price')], 'list', [
|
}), f('unit_price')], 'list', [
|
||||||
{ name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason },
|
{ name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason },
|
||||||
]),
|
]),
|
||||||
define('gasorder_contract_revision', '合同修订记录', 'readonly', []),
|
define('gasorder_contract_revision', '合同修订记录', 'readonly', [
|
||||||
|
relation('gasorder_contract_identity', '/gasorder_contract'),
|
||||||
|
f('action'), f('contract_status'), f('effective_at'), f('expired_at'),
|
||||||
|
f('operator_name'), f('operator_identity'), f('occurred_at'), f('reason'),
|
||||||
|
]),
|
||||||
define('gasorder_basic', '气体配送订单', 'append_only', [f('request_no', { required: true }), relation('gasorder_contract_identity', '/gasorder_contract', true, { label: '配送合同', listDisplayKey: 'contract_display_name', detailDisplayKey: 'contract_display_name', listDisplayIdentityCopy: true, showIdentityCopy: true, placeholder: '请选择当前可履约的生效合同', relationFilters: { candidate: 'order' }, relationStrictFilter: true, relationInvalidMessage: '所选配送合同已不可履约,已清空,请重新选择', relationEmptyText: '暂无可下单的生效合同,请先启用或续签配送合同' }), f('creator_type', { required: true, type: 'select', options: resourceSearchEnumOptions('gasorder_basic', 'creator_type') }), f('creator_identity', { label: '创建方', required: true, listDisplayKey: 'creator_display_name', listDisplayIdentityCopy: true, placeholder: '请先选择配送合同和创建方类型', readonlyRelationText: true, relationFilters: { status: '1' }, relationEmptyText: '当前合同下暂无可用的创建方', dynamicRelation: {
|
define('gasorder_basic', '气体配送订单', 'append_only', [f('request_no', { required: true }), relation('gasorder_contract_identity', '/gasorder_contract', true, { label: '配送合同', listDisplayKey: 'contract_display_name', detailDisplayKey: 'contract_display_name', listDisplayIdentityCopy: true, showIdentityCopy: true, placeholder: '请选择当前可履约的生效合同', relationFilters: { candidate: 'order' }, relationStrictFilter: true, relationInvalidMessage: '所选配送合同已不可履约,已清空,请重新选择', relationEmptyText: '暂无可下单的生效合同,请先启用或续签配送合同' }), f('creator_type', { required: true, type: 'select', options: resourceSearchEnumOptions('gasorder_basic', 'creator_type') }), f('creator_identity', { label: '创建方', required: true, listDisplayKey: 'creator_display_name', listDisplayIdentityCopy: true, placeholder: '请先选择配送合同和创建方类型', readonlyRelationText: true, relationFilters: { status: '1' }, relationEmptyText: '当前合同下暂无可用的创建方', dynamicRelation: {
|
||||||
parentKey: 'creator_type', parentLabel: '创建方类型', contextParentKey: 'gasorder_contract_identity',
|
parentKey: 'creator_type', parentLabel: '创建方类型', contextParentKey: 'gasorder_contract_identity',
|
||||||
resources: { user: '/user_account', staff: '/staff_account', delivery: '/delivery_basic', gas: '/gas_basic' },
|
resources: { user: '/user_account', staff: '/staff_account', delivery: '/delivery_basic', gas: '/gas_basic' },
|
||||||
@@ -587,7 +628,7 @@ export const resources: ResourceUiDefinition[] = [
|
|||||||
parentChangeMessage: '配送合同已变更,请重新选择该合同用户的收货地址',
|
parentChangeMessage: '配送合同已变更,请重新选择该合同用户的收货地址',
|
||||||
},
|
},
|
||||||
}), f('gasorder_contract_product_identities', { label: '合同气瓶', required: true, type: 'identity-list', listDisplayKey: 'contract_products_summary', listDetailLink: true, emptyText: '未填写', 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', [
|
}), f('gasorder_contract_product_identities', { label: '合同气瓶', required: true, type: 'identity-list', listDisplayKey: 'contract_products_summary', listDetailLink: true, emptyText: '未填写', 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, { label: '配送点', placeholder: '请选择合同履约范围内的配送点', relationFilters: { status: '1' } }), relation('staff_account_identity', '/staff_account', true, { label: '配送人员', placeholder: '请先选择配送点', relationEmptyText: '该配送点暂无在岗且资质有效的配送人员', staffRelation: { roles: ['delivery'], enabledOnly: true, workStatus: 'on_duty', validCredentialOnly: true } }), ...reason], visibleFor: { field: 'order_status', values: [16, 18] } },
|
{ name: '分配订单', resource: '/gasorder_basic/:identity/assign', fields: [relation('delivery_basic_identity', '/delivery_basic', true, { label: '配送点', placeholder: '请选择合同履约范围内的配送点', relationFilters: { status: '1' } }), relation('staff_account_identity', '/staff_account', true, { label: '配送人员', placeholder: '请选择当前配送点内的配送人员', relationEmptyText: '该配送点暂无在岗且资质有效的配送人员', staffRelation: { roles: ['delivery'], enabledOnly: true, workStatus: 'on_duty', validCredentialOnly: true } }), ...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/filling', fields: reason, visibleFor: { field: 'order_status', values: [18] } },
|
||||||
{ name: '待配送', resource: '/gasorder_basic/:identity/ready', fields: reason, visibleFor: { field: 'order_status', values: [19] } },
|
{ name: '待配送', resource: '/gasorder_basic/:identity/ready', fields: reason, visibleFor: { field: 'order_status', values: [19] } },
|
||||||
{ name: '开始配送', resource: '/gasorder_basic/:identity/delivering', fields: reason, visibleFor: { field: 'order_status', values: [20] } },
|
{ name: '开始配送', resource: '/gasorder_basic/:identity/delivering', fields: reason, visibleFor: { field: 'order_status', values: [20] } },
|
||||||
@@ -597,34 +638,109 @@ export const resources: ResourceUiDefinition[] = [
|
|||||||
{ name: '恢复订单', resource: '/gasorder_basic/:identity/recover', fields: reason, visibleFor: { field: 'order_status', values: [21] } },
|
{ name: '恢复订单', resource: '/gasorder_basic/:identity/recover', fields: reason, visibleFor: { field: 'order_status', values: [21] } },
|
||||||
{ name: '取消订单', resource: '/gasorder_basic/:identity/cancel', danger: true, fields: reason, visibleFor: { field: 'order_status', values: [16, 18] } },
|
{ name: '取消订单', resource: '/gasorder_basic/:identity/cancel', danger: true, fields: reason, visibleFor: { field: 'order_status', values: [16, 18] } },
|
||||||
]),
|
]),
|
||||||
define('gasorder_item', '订单明细', 'readonly', []),
|
define('gasorder_item', '订单明细', 'readonly', [
|
||||||
define('gasorder_assign', '分配记录', 'readonly', []),
|
relation('gasorder_basic_identity', '/gasorder_basic'),
|
||||||
define('gasorder_status', '状态记录', 'readonly', []),
|
relation('gasorder_contract_product_identity', '/gasorder_contract_product'),
|
||||||
define('gasorder_track', '运行轨迹', 'readonly', []),
|
relation('product_info_identity', '/product_info'),
|
||||||
define('gasorder_track_point', '轨迹点', 'readonly', []),
|
f('product_code'), f('product_type_name'), f('product_params'), f('unit_price'), f('active'),
|
||||||
define('gasorder_confirm', '确认记录', 'readonly', []),
|
]),
|
||||||
define('gasorder_payment', '支付记录', 'readonly', []),
|
define('gasorder_assign', '分配记录', 'readonly', [
|
||||||
|
relation('gasorder_basic_identity', '/gasorder_basic'),
|
||||||
|
relation('gas_basic_identity', '/gas_basic'),
|
||||||
|
relation('delivery_basic_identity', '/delivery_basic'),
|
||||||
|
relation('staff_account_identity', '/staff_account'),
|
||||||
|
f('assigner_name'), f('assigner_identity'), f('assigned_at'), f('reason'),
|
||||||
|
]),
|
||||||
|
define('gasorder_status', '状态记录', 'readonly', [
|
||||||
|
relation('gasorder_basic_identity', '/gasorder_basic'),
|
||||||
|
f('from_status'), f('to_status'), f('operator_name'), f('operator_identity'), f('occurred_at'), f('reason'),
|
||||||
|
]),
|
||||||
|
define('gasorder_track', '运行轨迹', 'readonly', [
|
||||||
|
relation('gasorder_basic_identity', '/gasorder_basic'),
|
||||||
|
relation('staff_account_identity', '/staff_account'),
|
||||||
|
f('attempt_no'), f('started_at'), f('completed_at'),
|
||||||
|
]),
|
||||||
|
define('gasorder_track_point', '轨迹点', 'readonly', [
|
||||||
|
relation('gasorder_track_identity', '/gasorder_track'),
|
||||||
|
f('request_no'), f('longitude'), f('latitude'), f('occurred_at'), f('received_at'),
|
||||||
|
f('source'), f('accuracy'), f('speed'), f('direction'),
|
||||||
|
]),
|
||||||
|
define('gasorder_confirm', '确认记录', 'readonly', [
|
||||||
|
relation('gasorder_basic_identity', '/gasorder_basic'),
|
||||||
|
f('request_no'), f('confirm_type'), f('recipient_name'), f('recipient_phone'),
|
||||||
|
f('proof_uri'), f('confirmed_at'), f('remark'),
|
||||||
|
]),
|
||||||
|
define('gasorder_payment', '支付记录', 'readonly', [
|
||||||
|
relation('gasorder_basic_identity', '/gasorder_basic'),
|
||||||
|
relation('payment_order_identity', '/payment_order'),
|
||||||
|
f('attempt_no'), f('amount'),
|
||||||
|
]),
|
||||||
|
|
||||||
define('ec_category', '商品分类', 'writable', [relation('parent_identity', '/ec_category'), f('name', { required: true }), f('sort_no')], 'tree'),
|
define('ec_category', '商品分类', 'writable', [relation('parent_identity', '/ec_category'), f('name', { required: true }), f('sort_no')], 'tree'),
|
||||||
define('ec_product', '商品', 'writable', [relation('ec_category_identity', '/ec_category', true), f('product_code', { required: true }), f('name', { required: true }), f('price_amount', { required: true }), f('stock_quantity')]),
|
define('ec_product', '商品', 'writable', [relation('ec_category_identity', '/ec_category', true), f('product_code', { required: true }), f('name', { required: true }), f('price_amount', { required: true }), f('stock_quantity')]),
|
||||||
define('ec_product_attribute', '商品属性', 'writable', [relation('ec_product_identity', '/ec_product', true), f('name', { required: true }), f('value', { required: true }), f('sort_no')]),
|
define('ec_product_attribute', '商品属性', 'writable', [relation('ec_product_identity', '/ec_product', true), f('name', { required: true }), f('value', { required: true }), f('sort_no')]),
|
||||||
define('ec_product_image', '商品图片', 'writable', [relation('ec_product_identity', '/ec_product', true), f('image_uri', { required: true }), f('sort_no'), f('is_cover')]),
|
define('ec_product_image', '商品图片', 'writable', [relation('ec_product_identity', '/ec_product', true), f('image_uri', { required: true }), f('sort_no'), f('is_cover')]),
|
||||||
define('ec_cart', '购物车', 'readonly', []),
|
define('ec_cart', '购物车', 'readonly', [
|
||||||
define('ec_order', '商城订单', 'readonly', []),
|
relation('user_account_identity', '/user_account'), relation('ec_product_identity', '/ec_product'),
|
||||||
define('ec_order_item', '商城订单明细', 'readonly', []),
|
f('quantity'), f('selected'),
|
||||||
define('ec_review', '商品评价', 'readonly', []),
|
]),
|
||||||
|
define('ec_order', '商城订单', 'readonly', [
|
||||||
|
f('order_no'), f('request_no'), f('order_status', { type: 'select', options: [
|
||||||
|
{ label: '待支付', value: 16 },
|
||||||
|
{ label: '已支付', value: 18 },
|
||||||
|
{ label: '已取消', value: 22 },
|
||||||
|
], unknownValueLabel: '未知订单状态' }),
|
||||||
|
relation('user_account_identity', '/user_account'), relation('gas_basic_identity', '/gas_basic'),
|
||||||
|
relation('delivery_basic_identity', '/delivery_basic'), relation('user_address_identity', '/user_address'),
|
||||||
|
f('contact_name'), f('contact_phone'), f('product_amount'), f('discount_amount'), f('payable_amount'),
|
||||||
|
f('total_amount'), f('paid_at'), f('logistics_no'), f('logistics_company'), f('logistics_status'),
|
||||||
|
f('shipped_at'), f('received_at'), f('remark'),
|
||||||
|
]),
|
||||||
|
define('ec_order_item', '商城订单明细', 'readonly', [
|
||||||
|
relation('ec_order_identity', '/ec_order'), relation('ec_product_identity', '/ec_product'),
|
||||||
|
f('product_snapshot'), f('quantity'), f('sale_amount'),
|
||||||
|
]),
|
||||||
|
define('ec_review', '商品评价', 'readonly', [
|
||||||
|
relation('ec_order_identity', '/ec_order'), relation('ec_product_identity', '/ec_product'),
|
||||||
|
relation('user_account_identity', '/user_account'), f('score'), f('content'),
|
||||||
|
]),
|
||||||
|
|
||||||
define('wallet_basic', '钱包', 'readonly', [f('owner_type'), f('owner_identity'), f('alipay_id'), f('alipay_name'), f('wxpay_id'), f('wxpay_name'), f('balance'), f('withdrawal_balance')], 'list', [
|
define('wallet_basic', '钱包', 'readonly', [f('owner_type'), f('owner_identity', {
|
||||||
|
type: 'identity', label: '归属主体', listRelationNameOnly: true,
|
||||||
|
displayRelationLabel: true, showIdentityCopy: true,
|
||||||
|
dynamicRelation: {
|
||||||
|
parentKey: 'owner_type', parentLabel: '归属类型',
|
||||||
|
resources: { user: '/user_account', staff: '/staff_account', gas: '/gas_basic', delivery: '/delivery_basic' },
|
||||||
|
},
|
||||||
|
}), f('alipay_id'), f('alipay_name'), f('wxpay_id'), f('wxpay_name'), f('balance'), f('withdrawal_balance')], 'list', [
|
||||||
{ name: '后台充值', resource: '/wallet_basic/:identity/recharge', fields: [f('request_no', { required: true }), f('amount', { required: true }), f('withdrawable'), ...reason, f('remark')] },
|
{ name: '后台充值', resource: '/wallet_basic/:identity/recharge', fields: [f('request_no', { required: true }), f('amount', { required: true }), f('withdrawable'), ...reason, f('remark')] },
|
||||||
{ name: '修改钱包状态', resource: '/wallet_basic/:identity/status', method: 'PATCH', fields: [f('status', { required: true, type: 'select', options: [{ label: '启用', value: 1 }, { label: '停用', value: 2 }, { label: '冻结', value: 4 }] })] },
|
{ name: '修改钱包状态', resource: '/wallet_basic/:identity/status', method: 'PATCH', fields: [f('status', { required: true, type: 'select', options: [{ label: '启用', value: 1 }, { label: '停用', value: 2 }, { label: '冻结', value: 4 }] })] },
|
||||||
]),
|
]),
|
||||||
define('wallet_bank', '银行卡', 'readonly', []),
|
define('wallet_bank', '银行卡', 'readonly', [
|
||||||
define('payment_order', '钱包支付记录', 'readonly', []),
|
relation('wallet_basic_identity', '/wallet_basic'), f('bank_name'), f('card_owner'),
|
||||||
define('wallet_record', '钱包流水', 'readonly', []),
|
f('card_no_last4'), f('bind_id'), f('bank_type'), f('bank'),
|
||||||
|
]),
|
||||||
|
define('payment_order', '钱包支付记录', 'readonly', [
|
||||||
|
f('payment_no'), f('request_no'), f('payment_status'), f('business_type'),
|
||||||
|
f('business_identity', { type: 'identity', label: '业务对象', listRelationNameOnly: true, displayRelationLabel: true, showIdentityCopy: true, dynamicRelation: {
|
||||||
|
parentKey: 'business_type', parentLabel: '业务类型', resources: { gasorder: '/gasorder_basic', ec_order: '/ec_order' },
|
||||||
|
} }), relation('user_identity', '/user_account'), f('merchant_identity'), f('channel'), f('pay_type'),
|
||||||
|
f('channel_trade_no'), f('amount'), f('subject'), f('failure_code'), f('failure_message'),
|
||||||
|
f('expires_at'), f('paid_at'), f('closed_at'),
|
||||||
|
]),
|
||||||
|
define('wallet_record', '钱包流水', 'readonly', [
|
||||||
|
relation('wallet_basic_identity', '/wallet_basic'), f('record_no'), f('request_no'),
|
||||||
|
f('direction'), f('trade_type'), f('amount'), f('fee'), f('balance_after'),
|
||||||
|
f('withdrawal_balance_after'), f('in_trade_no'), f('out_trade_no'), f('pay_channel'),
|
||||||
|
f('pay_type'), relation('related_record_identity', '/wallet_record'), f('operator_name'),
|
||||||
|
f('operator_identity'), f('ymd'), f('ym'), f('remark'),
|
||||||
|
]),
|
||||||
define('payment_refund', '退款审核', 'readonly', [
|
define('payment_refund', '退款审核', 'readonly', [
|
||||||
f('refund_no'), f('business_type'), f('business_identity'), f('user_identity'),
|
f('refund_no'), f('business_type'), f('business_identity', { type: 'identity', label: '业务对象', listRelationNameOnly: true, displayRelationLabel: true, showIdentityCopy: true, dynamicRelation: {
|
||||||
|
parentKey: 'business_type', parentLabel: '业务类型', resources: { gasorder: '/gasorder_basic', ec_order: '/ec_order' },
|
||||||
|
} }), relation('user_identity', '/user_account'),
|
||||||
f('amount'), f('reason'), f('description'), f('refund_status'),
|
f('amount'), f('reason'), f('description'), f('refund_status'),
|
||||||
f('reviewer_identity'), f('review_remark'), f('reviewed_at'), f('completed_at'),
|
relation('reviewer_identity', '/platform_account'), f('review_remark'), f('reviewed_at'), f('completed_at'),
|
||||||
], 'list', [
|
], 'list', [
|
||||||
{ name: '审核通过并退入钱包', resource: '/payment_refund/:identity/approve', fields: [f('remark')], visibleFor: { field: 'refund_status', values: [10] } },
|
{ name: '审核通过并退入钱包', resource: '/payment_refund/:identity/approve', fields: [f('remark')], visibleFor: { field: 'refund_status', values: [10] } },
|
||||||
{ name: '驳回退款', resource: '/payment_refund/:identity/reject', danger: true, fields: [f('remark', { required: true })], visibleFor: { field: 'refund_status', values: [10] } },
|
{ name: '驳回退款', resource: '/payment_refund/:identity/reject', danger: true, fields: [f('remark', { required: true })], visibleFor: { field: 'refund_status', values: [10] } },
|
||||||
@@ -652,16 +768,30 @@ export const resources: ResourceUiDefinition[] = [
|
|||||||
{ name: '标记处理完成', resource: '/wallet_apply_cash/:identity/complete', fields: [f('trade_no', { required: true }), f('callback_msg')], visibleFor: { field: 'apply_status', values: [25] } },
|
{ name: '标记处理完成', resource: '/wallet_apply_cash/:identity/complete', fields: [f('trade_no', { required: true }), f('callback_msg')], visibleFor: { field: 'apply_status', values: [25] } },
|
||||||
]),
|
]),
|
||||||
|
|
||||||
define('fin_payment', '财务支付记录', 'readonly', []),
|
define('fin_payment', '财务支付记录', 'readonly', [
|
||||||
define('fin_settlement', '财务结算', 'writable', [f('settlement_no', { required: true }), f('subject_type', { required: true }), f('subject_identity', { required: true }), f('period_start', { required: true }), f('period_end', { required: true })]),
|
relation('ec_order_identity', '/ec_order'), f('payment_status'), f('channel'), f('amount'), f('paid_at'),
|
||||||
define('fin_reconciliation', '财务对账', 'readonly', []),
|
]),
|
||||||
|
define('fin_settlement', '财务结算', 'writable', [f('settlement_no', { required: true }), f('subject_type', { required: true }), f('subject_identity', {
|
||||||
|
required: true, type: 'identity', label: '结算主体', listRelationNameOnly: true,
|
||||||
|
displayRelationLabel: true, showIdentityCopy: true,
|
||||||
|
dynamicRelation: {
|
||||||
|
parentKey: 'subject_type', parentLabel: '结算主体类型',
|
||||||
|
resources: { gas: '/gas_basic', delivery: '/delivery_basic' },
|
||||||
|
},
|
||||||
|
}), f('period_start', { required: true }), f('period_end', { required: true })]),
|
||||||
|
define('fin_reconciliation', '财务对账', 'readonly', [
|
||||||
|
f('reconciliation_status'), f('channel'), f('bill_date'), f('difference_amount'),
|
||||||
|
]),
|
||||||
define('cms_content', '内容', 'writable', [f('content_type', { required: true }), f('title', { required: true }), f('body', { required: true }), f('version_no'), f('publish_status')]),
|
define('cms_content', '内容', 'writable', [f('content_type', { required: true }), f('title', { required: true }), f('body', { required: true }), f('version_no'), f('publish_status')]),
|
||||||
define('cs_ticket', '客服工单', 'writable', [relation('user_account_identity', '/user_account', true), f('ticket_no', { required: true }), f('category', { required: true }), f('priority', { required: true })]),
|
define('cs_ticket', '客服工单', 'writable', [relation('user_account_identity', '/user_account', true), f('ticket_no', { required: true }), f('category', { required: true }), f('priority', { required: true })]),
|
||||||
define('platform_account', '平台账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('avatar'), f('platform_role_code', { required: true, unknownValueLabel: '未知角色' }), f('phone')]),
|
define('platform_account', '平台账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('avatar'), f('platform_role_code', { required: true, unknownValueLabel: '未知角色' }), f('phone')]),
|
||||||
define('platform_role', '平台角色', 'writable', [f('role_code', { required: true }), f('name', { required: true }), f('location_scope', { required: true, type: 'select', options: resourceSearchEnumOptions('platform_role', 'location_scope') })], 'list', [
|
define('platform_role', '平台角色', 'writable', [f('role_code', { required: true }), f('name', { required: true }), f('location_scope', { required: true, type: 'select', options: resourceSearchEnumOptions('platform_role', 'location_scope') })], 'list', [
|
||||||
{ name: '分配菜单', resource: '/platform_role/:identity/menu', method: 'PUT', fields: [f('menu_identities', { type: 'identity-list', relation: '/platform_menu' })] },
|
{ name: '分配菜单', resource: '/platform_role/:identity/menu', method: 'PUT', fields: [f('menu_identities', { type: 'identity-list', relation: '/platform_menu' })] },
|
||||||
]),
|
]),
|
||||||
define('platform_menu', '平台菜单', 'readonly', [], 'tree'),
|
define('platform_menu', '平台菜单', 'readonly', [
|
||||||
|
relation('parent_identity', '/platform_menu'), f('group_code'), f('name'), f('icon'),
|
||||||
|
f('path'), f('sort_no'),
|
||||||
|
], 'tree'),
|
||||||
];
|
];
|
||||||
|
|
||||||
export const resourceByPath = Object.fromEntries(
|
export const resourceByPath = Object.fromEntries(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<!--
|
<!--
|
||||||
功能:以响应式信息卡和子表页签展示标准资源详情。
|
功能:以响应式信息卡和子表页签展示标准资源详情。
|
||||||
版本:v1.7.0
|
版本:v1.8.0
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="detail-stack">
|
<div class="detail-stack">
|
||||||
@@ -8,12 +8,19 @@
|
|||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
<div v-for="entry in entries" :key="entry.key" class="detail-item" :class="{ 'detail-item-wide': entry.wide }">
|
<div v-for="entry in entries" :key="entry.key" class="detail-item" :class="{ 'detail-item-wide': entry.wide }">
|
||||||
<span class="detail-label">{{ entry.label }}</span>
|
<span class="detail-label">{{ entry.label }}</span>
|
||||||
<pre v-if="entry.objectValue" class="json-value">{{ entry.value }}</pre>
|
<a-popover v-if="entry.jsonValue" position="left">
|
||||||
|
<a-button type="text" size="mini">查看内容</a-button>
|
||||||
|
<template #content><pre class="json-value">{{ entry.value }}</pre></template>
|
||||||
|
</a-popover>
|
||||||
<IdentityText v-else-if="entry.key === 'identity'" :value="String(entry.value)" />
|
<IdentityText v-else-if="entry.key === 'identity'" :value="String(entry.value)" />
|
||||||
<div v-else-if="entry.identityValue" class="detail-relation-value">
|
<RelationNameText
|
||||||
<span class="detail-value">{{ entry.value }}</span>
|
v-else-if="entry.identityValue"
|
||||||
<IdentityText :value="entry.identityValue" />
|
:name="entry.value"
|
||||||
</div>
|
:identity="entry.identityValue"
|
||||||
|
:identity-label="entry.label"
|
||||||
|
:clickable="Boolean(entry.relationResource)"
|
||||||
|
@open="openRelation(entry.relationResource, entry.identityValue)"
|
||||||
|
/>
|
||||||
<span v-else class="detail-value">{{ entry.value }}</span>
|
<span v-else class="detail-value">{{ entry.value }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,20 +36,20 @@
|
|||||||
v-for="column in collection.columns"
|
v-for="column in collection.columns"
|
||||||
:key="column"
|
:key="column"
|
||||||
:title="resourceFieldLabel(definition, column, collection.key)"
|
:title="resourceFieldLabel(definition, column, collection.key)"
|
||||||
:width="collectionColumnWidth(column)"
|
:width="collectionColumnWidth(collection.key, column)"
|
||||||
ellipsis
|
ellipsis
|
||||||
tooltip
|
tooltip
|
||||||
>
|
>
|
||||||
<template #cell="{ record }">
|
<template #cell="{ record }">
|
||||||
<div v-if="collectionRelationIdentityKey(collection.key, column)" class="collection-relation-value">
|
<div v-if="collectionRelationIdentityKey(definition.name, collection.key, column)" class="collection-relation-value">
|
||||||
<span>{{ displayRawValue(column, record[column]) }}</span>
|
<span>{{ displayRawValue(column, record[column]) }}</span>
|
||||||
<IdentityText
|
<IdentityText
|
||||||
v-if="record[collectionRelationIdentityKey(collection.key, column)]"
|
v-if="record[collectionRelationIdentityKey(definition.name, collection.key, column)]"
|
||||||
:value="String(record[collectionRelationIdentityKey(collection.key, column)])"
|
:value="String(record[collectionRelationIdentityKey(definition.name, collection.key, column)])"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<IdentityText v-else-if="column.includes('identity') && record[column]" :value="String(record[column])" />
|
<IdentityText v-else-if="column.includes('identity') && record[column]" :value="String(record[column])" />
|
||||||
<a-popover v-else-if="isCollectionJsonColumn(column)" position="left">
|
<a-popover v-else-if="isCollectionJsonField(definition.name, collection.key, column)" position="left">
|
||||||
<a-button type="text" size="mini">查看参数</a-button>
|
<a-button type="text" size="mini">查看参数</a-button>
|
||||||
<template #content>
|
<template #content>
|
||||||
<pre class="collection-json-value">{{ formatCollectionJson(record[column]) }}</pre>
|
<pre class="collection-json-value">{{ formatCollectionJson(record[column]) }}</pre>
|
||||||
@@ -62,16 +69,25 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import {
|
import {
|
||||||
displayRawValue,
|
displayRawValue,
|
||||||
displayResourceField,
|
displayResourceField,
|
||||||
|
hasResourceFieldLabel,
|
||||||
isEmptyDeletedAt,
|
isEmptyDeletedAt,
|
||||||
primaryRecord,
|
primaryRecord,
|
||||||
resourceFieldLabel,
|
resourceFieldLabel,
|
||||||
} from '@/api/resource-display';
|
} from '@/api/resource-display';
|
||||||
|
import {
|
||||||
|
collectionRelationIdentityKey,
|
||||||
|
isCollectionJsonField,
|
||||||
|
isResourceJsonField,
|
||||||
|
resourceDetailContract,
|
||||||
|
} from '@/api/resource-detail-contract';
|
||||||
import type { ResourceRow } from '@/api/resource-page-rules';
|
import type { ResourceRow } from '@/api/resource-page-rules';
|
||||||
import type { ResourceUiDefinition } from '@/api/resources';
|
import type { ResourceUiDefinition } from '@/api/resources';
|
||||||
import IdentityText from '@/components/IdentityText.vue';
|
import IdentityText from '@/components/IdentityText.vue';
|
||||||
|
import RelationNameText from '@/views/shared/RelationNameText.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
definition: ResourceUiDefinition;
|
definition: ResourceUiDefinition;
|
||||||
@@ -88,22 +104,34 @@ type DetailEntry = {
|
|||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
objectValue: boolean;
|
jsonValue: boolean;
|
||||||
wide: boolean;
|
wide: boolean;
|
||||||
identityValue: string;
|
identityValue: string;
|
||||||
|
relationResource: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 统一关联记录表列宽:日期时间列保持紧凑,唯一标识列保留完整复制空间。 */
|
const router = useRouter();
|
||||||
function collectionColumnWidth(column: string) {
|
|
||||||
if (column.endsWith('_at')) return 150;
|
/** 打开关系资源详情;没有详情路由的树形资源保持只读。 */
|
||||||
if (column.includes('identity')) return 220;
|
function openRelation(resource: string, identity: string) {
|
||||||
if (isCollectionJsonColumn(column)) return 100;
|
if (!resource) return;
|
||||||
return 160;
|
const target = router
|
||||||
|
.getRoutes()
|
||||||
|
.find(
|
||||||
|
(item) =>
|
||||||
|
item.meta.resource === resource && item.meta.recordMode === 'detail',
|
||||||
|
);
|
||||||
|
if (!target?.name) return;
|
||||||
|
void router.push({ name: target.name, params: { identity } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 标识关联子表中需要完整查看的 JSON 快照字段。 */
|
/** 统一关联记录表列宽:日期时间列保持紧凑,唯一标识列保留完整复制空间。 */
|
||||||
function isCollectionJsonColumn(column: string) {
|
function collectionColumnWidth(collectionKey: string, column: string) {
|
||||||
return column === 'product_params';
|
if (column.endsWith('_at')) return 150;
|
||||||
|
if (column.includes('identity')) return 220;
|
||||||
|
if (isCollectionJsonField(props.definition.name, collectionKey, column))
|
||||||
|
return 100;
|
||||||
|
return 160;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将字符串或对象参数格式化为可读 JSON;历史非 JSON 文本保持原样。 */
|
/** 将字符串或对象参数格式化为可读 JSON;历史非 JSON 文本保持原样。 */
|
||||||
@@ -119,20 +147,16 @@ function formatCollectionJson(value: unknown) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 返回集合可读名称对应的稳定标识字段。 */
|
|
||||||
function collectionRelationIdentityKey(collectionKey: string, column: string) {
|
|
||||||
if (collectionKey !== 'assignments') return '';
|
|
||||||
return (
|
|
||||||
{
|
|
||||||
gas_basic_display_name: 'gas_basic_identity',
|
|
||||||
delivery_basic_display_name: 'delivery_basic_identity',
|
|
||||||
staff_account_display_name: 'staff_account_identity',
|
|
||||||
}[column] ?? ''
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 返回订单详情关联标识对应的服务端可读名称字段。 */
|
/** 返回订单详情关联标识对应的服务端可读名称字段。 */
|
||||||
function detailRelationDisplayName(row: ResourceRow, key: string) {
|
function detailRelationDisplayName(
|
||||||
|
row: ResourceRow,
|
||||||
|
key: string,
|
||||||
|
configuredDisplayKey = '',
|
||||||
|
) {
|
||||||
|
const conventionalKey = key.endsWith('_identity')
|
||||||
|
? `${key.slice(0, -9)}_display_name`
|
||||||
|
: '';
|
||||||
const displayKey =
|
const displayKey =
|
||||||
{
|
{
|
||||||
creator_identity: 'creator_display_name',
|
creator_identity: 'creator_display_name',
|
||||||
@@ -141,13 +165,16 @@ function detailRelationDisplayName(row: ResourceRow, key: string) {
|
|||||||
delivery_basic_identity: 'delivery_basic_display_name',
|
delivery_basic_identity: 'delivery_basic_display_name',
|
||||||
staff_account_identity: 'staff_account_display_name',
|
staff_account_identity: 'staff_account_display_name',
|
||||||
operator_identity: 'operator_name',
|
operator_identity: 'operator_name',
|
||||||
|
assigner_identity: 'assigner_name',
|
||||||
}[key] ?? '';
|
}[key] ?? '';
|
||||||
if (!displayKey) return '';
|
return String(
|
||||||
return String(row[displayKey] ?? '').trim();
|
row[configuredDisplayKey || displayKey || conventionalKey] ?? '',
|
||||||
|
).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = computed<DetailEntry[]>(() => {
|
const entries = computed<DetailEntry[]>(() => {
|
||||||
const row = primaryRecord(props.detail);
|
const row = primaryRecord(props.detail);
|
||||||
|
const contract = resourceDetailContract(props.definition.name);
|
||||||
const excluded = new Set([
|
const excluded = new Set([
|
||||||
'id',
|
'id',
|
||||||
'password',
|
'password',
|
||||||
@@ -156,27 +183,12 @@ const entries = computed<DetailEntry[]>(() => {
|
|||||||
'attachment',
|
'attachment',
|
||||||
// 合同可读名称只负责渲染“配送合同”,不作为独立业务字段重复展示。
|
// 合同可读名称只负责渲染“配送合同”,不作为独立业务字段重复展示。
|
||||||
'contract_display_name',
|
'contract_display_name',
|
||||||
|
...(contract.hiddenKeys ?? []),
|
||||||
]);
|
]);
|
||||||
if (props.accountSummary) {
|
if (props.accountSummary) {
|
||||||
for (const key of ['username', 'identity', 'created_at']) excluded.add(key);
|
for (const key of ['username', 'identity', 'created_at']) excluded.add(key);
|
||||||
}
|
}
|
||||||
const leadingKeys =
|
const leadingKeys = contract.leadingKeys ?? ['identity', 'status'];
|
||||||
props.definition.name === 'gasorder_basic'
|
|
||||||
? [
|
|
||||||
'order_no',
|
|
||||||
'request_no',
|
|
||||||
'order_status',
|
|
||||||
'gasorder_contract_identity',
|
|
||||||
'creator_type',
|
|
||||||
'creator_identity',
|
|
||||||
'user_account_identity',
|
|
||||||
'gas_basic_identity',
|
|
||||||
'delivery_basic_identity',
|
|
||||||
'staff_account_identity',
|
|
||||||
'identity',
|
|
||||||
'status',
|
|
||||||
]
|
|
||||||
: ['identity', 'status'];
|
|
||||||
const preferred = [
|
const preferred = [
|
||||||
...leadingKeys,
|
...leadingKeys,
|
||||||
...props.definition.fields.map((field) => field.key),
|
...props.definition.fields.map((field) => field.key),
|
||||||
@@ -202,7 +214,7 @@ const entries = computed<DetailEntry[]>(() => {
|
|||||||
excluded.has(key) ||
|
excluded.has(key) ||
|
||||||
key.endsWith('_id') ||
|
key.endsWith('_id') ||
|
||||||
key.endsWith('_display_name') ||
|
key.endsWith('_display_name') ||
|
||||||
(props.definition.name === 'gasorder_basic' && key === 'operator_name')
|
!hasResourceFieldLabel(props.definition, actualKey)
|
||||||
)
|
)
|
||||||
return [];
|
return [];
|
||||||
const value = row[actualKey];
|
const value = row[actualKey];
|
||||||
@@ -210,7 +222,12 @@ const entries = computed<DetailEntry[]>(() => {
|
|||||||
if (['deleted_at', 'DeletedAt'].includes(key) && isEmptyDeletedAt(value))
|
if (['deleted_at', 'DeletedAt'].includes(key) && isEmptyDeletedAt(value))
|
||||||
return [];
|
return [];
|
||||||
const field = props.definition.fields.find((item) => item.key === key);
|
const field = props.definition.fields.find((item) => item.key === key);
|
||||||
const relationDisplayName = detailRelationDisplayName(row, key);
|
// 资源已声明详情名称键时优先直接使用接口返回值,避免关系候选慢查询导致显示失败。
|
||||||
|
const relationDisplayName = detailRelationDisplayName(
|
||||||
|
row,
|
||||||
|
key,
|
||||||
|
field?.detailDisplayKey,
|
||||||
|
);
|
||||||
const display = relationDisplayName
|
const display = relationDisplayName
|
||||||
? relationDisplayName
|
? relationDisplayName
|
||||||
: field
|
: field
|
||||||
@@ -221,22 +238,32 @@ const entries = computed<DetailEntry[]>(() => {
|
|||||||
props.fieldOptions,
|
props.fieldOptions,
|
||||||
)
|
)
|
||||||
: displayRawValue(actualKey, value);
|
: displayRawValue(actualKey, value);
|
||||||
const objectValue = typeof value === 'object' && value !== null;
|
const jsonValue =
|
||||||
|
isResourceJsonField(key) ||
|
||||||
|
(typeof value === 'object' && value !== null);
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key,
|
key,
|
||||||
label: resourceFieldLabel(props.definition, actualKey),
|
label: resourceFieldLabel(props.definition, actualKey),
|
||||||
value: display,
|
value: jsonValue ? formatCollectionJson(value) : display,
|
||||||
objectValue,
|
jsonValue,
|
||||||
identityValue:
|
identityValue:
|
||||||
(relationDisplayName ||
|
(relationDisplayName ||
|
||||||
|
field?.type === 'identity' ||
|
||||||
field?.showIdentityCopy ||
|
field?.showIdentityCopy ||
|
||||||
field?.staffRelation?.showIdentityCopy) &&
|
field?.staffRelation?.showIdentityCopy) &&
|
||||||
value
|
value
|
||||||
? String(value)
|
? String(value)
|
||||||
: '',
|
: '',
|
||||||
|
relationResource:
|
||||||
|
field?.relation ??
|
||||||
|
field?.dynamicRelation?.resources[
|
||||||
|
String(row[field.dynamicRelation.parentKey] ?? '')
|
||||||
|
] ??
|
||||||
|
contract.relationResources?.[key] ??
|
||||||
|
'',
|
||||||
wide:
|
wide:
|
||||||
objectValue ||
|
jsonValue ||
|
||||||
/(address|terms|content|body|remark|reason|params|args)$/.test(key),
|
/(address|terms|content|body|remark|reason|params|args)$/.test(key),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -246,7 +273,10 @@ const entries = computed<DetailEntry[]>(() => {
|
|||||||
const collections = computed(() =>
|
const collections = computed(() =>
|
||||||
Object.entries(props.detail)
|
Object.entries(props.detail)
|
||||||
.filter(([, value]) => Array.isArray(value) && value.length > 0)
|
.filter(([, value]) => Array.isArray(value) && value.length > 0)
|
||||||
.map(([key, value]) => {
|
.flatMap(([key, value]) => {
|
||||||
|
const contract = resourceDetailContract(props.definition.name)
|
||||||
|
.collections?.[key];
|
||||||
|
if (!contract) return [];
|
||||||
const rows = value as ResourceRow[];
|
const rows = value as ResourceRow[];
|
||||||
const availableColumns = [
|
const availableColumns = [
|
||||||
...new Set(
|
...new Set(
|
||||||
@@ -261,56 +291,15 @@ const collections = computed(() =>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
const preferredColumns: Record<string, string[]> = {
|
const columns = contract.columns.filter((column) =>
|
||||||
'gasorder_contract.products': [
|
availableColumns.includes(column),
|
||||||
'bound_at',
|
);
|
||||||
'product_name',
|
return [{
|
||||||
'product_code',
|
|
||||||
'product_type_name',
|
|
||||||
'unit_price',
|
|
||||||
'product_info_identity',
|
|
||||||
'unbound_at',
|
|
||||||
'unbind_reason',
|
|
||||||
],
|
|
||||||
'gasorder_basic.items': [
|
|
||||||
'product_name',
|
|
||||||
'product_code',
|
|
||||||
'product_type_name',
|
|
||||||
'product_params',
|
|
||||||
'unit_price',
|
|
||||||
],
|
|
||||||
'gasorder_basic.assignments': [
|
|
||||||
'assigned_at',
|
|
||||||
'gas_basic_display_name',
|
|
||||||
'delivery_basic_display_name',
|
|
||||||
'staff_account_display_name',
|
|
||||||
'assigner_name',
|
|
||||||
'reason',
|
|
||||||
],
|
|
||||||
'gasorder_basic.statuses': [
|
|
||||||
'occurred_at',
|
|
||||||
'from_status',
|
|
||||||
'to_status',
|
|
||||||
'operator_name',
|
|
||||||
'reason',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const preferred =
|
|
||||||
preferredColumns[`${props.definition.name}.${key}`] ?? [];
|
|
||||||
const fixedColumns = ['assignments', 'items', 'statuses'].includes(key);
|
|
||||||
const columns = (
|
|
||||||
fixedColumns
|
|
||||||
? preferred
|
|
||||||
: [...new Set([...preferred, ...availableColumns])]
|
|
||||||
)
|
|
||||||
.filter((column) => availableColumns.includes(column))
|
|
||||||
.slice(0, 8);
|
|
||||||
return {
|
|
||||||
key,
|
key,
|
||||||
title: resourceFieldLabel(props.definition, key),
|
title: resourceFieldLabel(props.definition, key),
|
||||||
rows,
|
rows,
|
||||||
columns,
|
columns,
|
||||||
};
|
}];
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '@/api/resource-staff-relation';
|
} from '@/api/resource-staff-relation';
|
||||||
import type { ResourceField } from '@/api/resources';
|
import type { ResourceField } from '@/api/resources';
|
||||||
import type { ResourceRow } from '@/api/resource-page-rules';
|
import type { ResourceRow } from '@/api/resource-page-rules';
|
||||||
|
import { fieldRelationResource } from '@/api/resource-display';
|
||||||
import { createRelationRequestVersionGuard } from './resource-relation-linkage-policy';
|
import { createRelationRequestVersionGuard } from './resource-relation-linkage-policy';
|
||||||
|
|
||||||
export type ResourceRelationLoadOptions = {
|
export type ResourceRelationLoadOptions = {
|
||||||
@@ -133,13 +134,14 @@ export function useResourceRelations(
|
|||||||
/** 补载表单或详情中已经保存的关系值,避免分页和筛选导致回显裸标识。 */
|
/** 补载表单或详情中已经保存的关系值,避免分页和筛选导致回显裸标识。 */
|
||||||
async function ensureValues(fields: ResourceField[], values: ResourceRow) {
|
async function ensureValues(fields: ResourceField[], values: ResourceRow) {
|
||||||
const requests = fields.flatMap((field) => {
|
const requests = fields.flatMap((field) => {
|
||||||
if (!field.relation) return [];
|
const resource = fieldRelationResource(field, values);
|
||||||
|
if (!resource) return [];
|
||||||
const value = values[field.key];
|
const value = values[field.key];
|
||||||
const identities = Array.isArray(value) ? value : [value];
|
const identities = Array.isArray(value) ? value : [value];
|
||||||
return identities
|
return identities
|
||||||
.map((identity) => String(identity ?? ''))
|
.map((identity) => String(identity ?? ''))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((identity) => ensure(field.relation as string, identity));
|
.map((identity) => ensure(resource, identity));
|
||||||
});
|
});
|
||||||
await Promise.all(requests);
|
await Promise.all(requests);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/* 功能描述:标准资源列表工具栏、分页和辅助文本布局。版本:v1.0.0。 */
|
/* 功能描述:标准资源列表工具栏、响应式表格、分页和辅助文本布局。版本:v1.1.0。 */
|
||||||
.filters { flex: 1 1 420px; }
|
.filters { flex: 1 1 420px; }
|
||||||
.list-toolbar { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 12px 24px; margin-bottom: 16px; }
|
.list-toolbar { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 12px 24px; margin-bottom: 16px; }
|
||||||
.list-actions { margin-left: auto; }
|
.list-actions { margin-left: auto; }
|
||||||
.workflow-alert { margin-bottom: 16px; }
|
.workflow-alert { margin-bottom: 16px; }
|
||||||
.pagination { display: flex; justify-content: flex-end; margin-top: 16px; }
|
.pagination { display: flex; justify-content: flex-end; margin-top: 16px; }
|
||||||
.muted-text { color: var(--color-text-3); }
|
.muted-text { color: var(--color-text-3); }
|
||||||
|
.resource-table-shell { width: 100%; min-width: 0; max-width: 100%; overflow-x: auto; }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<!--
|
<!--
|
||||||
功能:展示标准资源列表,并将新建、详情和编辑入口导航到独立页面。
|
功能:展示标准资源列表,并将新建、详情和编辑入口导航到独立页面。
|
||||||
版本:v2.2.1
|
版本:v2.3.0
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<a-card :title="listTitle" :bordered="false">
|
<a-card :title="listTitle" :bordered="false">
|
||||||
@@ -32,12 +32,9 @@
|
|||||||
</a-space>
|
</a-space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="resource-table-shell">
|
||||||
<a-table :data="list" :loading="loading" :pagination="false" row-key="identity">
|
<a-table :data="list" :loading="loading" :pagination="false" row-key="identity">
|
||||||
<template #columns>
|
<template #columns>
|
||||||
<a-table-column title="ID" data-index="id" :width="80" />
|
|
||||||
<a-table-column title="唯一标识" :width="150">
|
|
||||||
<template #cell="{ record }"><IdentityText :value="String(record.identity)" /></template>
|
|
||||||
</a-table-column>
|
|
||||||
<a-table-column
|
<a-table-column
|
||||||
v-for="field in displayFields"
|
v-for="field in displayFields"
|
||||||
:key="field.key"
|
:key="field.key"
|
||||||
@@ -61,12 +58,16 @@
|
|||||||
:name="relationListName(field, record, relations.options)"
|
:name="relationListName(field, record, relations.options)"
|
||||||
:identity="identityFieldValue(field, record)"
|
:identity="identityFieldValue(field, record)"
|
||||||
:identity-label="field.label"
|
:identity-label="field.label"
|
||||||
|
:clickable="Boolean(fieldRelationResource(field, record))"
|
||||||
|
@open="openRelation(field, record)"
|
||||||
/>
|
/>
|
||||||
<RelationNameText
|
<RelationNameText
|
||||||
v-else-if="field.listDisplayIdentityCopy && identityFieldValue(field, record)"
|
v-else-if="field.listDisplayIdentityCopy && identityFieldValue(field, record)"
|
||||||
:name="displayListField(field, record)"
|
:name="displayListField(field, record)"
|
||||||
:identity="identityFieldValue(field, record)"
|
:identity="identityFieldValue(field, record)"
|
||||||
:identity-label="field.label"
|
:identity-label="field.label"
|
||||||
|
:clickable="Boolean(fieldRelationResource(field, record))"
|
||||||
|
@open="openRelation(field, record)"
|
||||||
/>
|
/>
|
||||||
<IdentityText
|
<IdentityText
|
||||||
v-else-if="field.type === 'identity' && !field.displayRelationLabel && identityFieldValue(field, record)"
|
v-else-if="field.type === 'identity' && !field.displayRelationLabel && identityFieldValue(field, record)"
|
||||||
@@ -110,6 +111,9 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
|
<a-table-column title="系统唯一标识" :width="170">
|
||||||
|
<template #cell="{ record }"><IdentityText :value="String(record.identity)" /></template>
|
||||||
|
</a-table-column>
|
||||||
<a-table-column title="操作" :width="definition.accountManagement ? 360 : 285" fixed="right">
|
<a-table-column title="操作" :width="definition.accountManagement ? 360 : 285" fixed="right">
|
||||||
<template #cell="{ record }">
|
<template #cell="{ record }">
|
||||||
<a-space>
|
<a-space>
|
||||||
@@ -143,6 +147,7 @@
|
|||||||
</a-table-column>
|
</a-table-column>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
|
</div>
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
<a-pagination
|
<a-pagination
|
||||||
:total="total"
|
:total="total"
|
||||||
@@ -182,6 +187,7 @@ import IdentityText from '@/components/IdentityText.vue';
|
|||||||
import {
|
import {
|
||||||
displayRawValue,
|
displayRawValue,
|
||||||
displayResourceField,
|
displayResourceField,
|
||||||
|
fieldRelationResource,
|
||||||
recordStatusColor,
|
recordStatusColor,
|
||||||
recordStatusLabel,
|
recordStatusLabel,
|
||||||
} from '@/api/resource-display';
|
} from '@/api/resource-display';
|
||||||
@@ -409,6 +415,24 @@ function openStatus(row: ResourceRow) {
|
|||||||
statusVisible.value = true;
|
statusVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 打开列表关系字段对应的详情页;树形资源没有详情路由时保持只读展示。 */
|
||||||
|
function openRelation(field: ResourceField, row: ResourceRow) {
|
||||||
|
const resource = fieldRelationResource(field, row);
|
||||||
|
if (!resource) return;
|
||||||
|
const target = router
|
||||||
|
.getRoutes()
|
||||||
|
.find(
|
||||||
|
(item) =>
|
||||||
|
item.meta.resource === resource &&
|
||||||
|
item.meta.recordMode === 'detail',
|
||||||
|
);
|
||||||
|
if (!target?.name) return;
|
||||||
|
void router.push({
|
||||||
|
name: target.name,
|
||||||
|
params: { identity: identityFieldValue(field, row) },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function saveStatus() {
|
async function saveStatus() {
|
||||||
if (![1, 2].includes(Number(statusTarget.value))) {
|
if (![1, 2].includes(Number(statusTarget.value))) {
|
||||||
Message.warning('请选择目标状态');
|
Message.warning('请选择目标状态');
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
<!-- 功能描述:列表以关系名称为主展示,并提供完整唯一标识的悬停查看与复制。版本:v1.0.0。 -->
|
<!-- 功能描述:以关系名称为主展示,支持进入关联详情并复制完整唯一标识。版本:v1.1.0。 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="relation-name-text">
|
<div class="relation-name-text">
|
||||||
<a-tooltip :content="`${identityLabel}:${identity}`">
|
<a-tooltip :content="`${identityLabel}:${identity}`">
|
||||||
<span class="relation-name">{{ displayName }}</span>
|
<button
|
||||||
|
v-if="clickable"
|
||||||
|
class="relation-name relation-link"
|
||||||
|
type="button"
|
||||||
|
@click.stop="emit('open')"
|
||||||
|
>{{ displayName }}</button>
|
||||||
|
<span v-else class="relation-name">{{ displayName }}</span>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip :content="`复制${identityLabel}`">
|
<a-tooltip :content="`复制${identityLabel}`">
|
||||||
<button
|
<button
|
||||||
@@ -26,7 +32,9 @@ const props = defineProps<{
|
|||||||
name: string;
|
name: string;
|
||||||
identity: string;
|
identity: string;
|
||||||
identityLabel?: string;
|
identityLabel?: string;
|
||||||
|
clickable?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
const emit = defineEmits<{ open: [] }>();
|
||||||
const displayName = computed(() => props.name || props.identity);
|
const displayName = computed(() => props.name || props.identity);
|
||||||
|
|
||||||
/** 复制当前关联记录的完整唯一标识,并给出明确操作反馈。 */
|
/** 复制当前关联记录的完整唯一标识,并给出明确操作反馈。 */
|
||||||
@@ -56,6 +64,16 @@ async function copyIdentity() {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.relation-link {
|
||||||
|
padding: 0;
|
||||||
|
color: rgb(var(--primary-6));
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.copy-button {
|
.copy-button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|||||||
@@ -9,7 +9,11 @@
|
|||||||
<a-tree :data="tree" :loading="loading" :field-names="{ key: 'identity', title: 'name', children: 'children' }">
|
<a-tree :data="tree" :loading="loading" :field-names="{ key: 'identity', title: 'name', children: 'children' }">
|
||||||
<template #title="node">
|
<template #title="node">
|
||||||
<a-space>
|
<a-space>
|
||||||
{{ node.title }}
|
<span>{{ node.title }}</span>
|
||||||
|
<span v-if="node.group_code || node.path" class="tree-business-key">
|
||||||
|
{{ node.group_code || node.path }}
|
||||||
|
</span>
|
||||||
|
<IdentityText :value="String(node.identity)" />
|
||||||
<a-button v-if="canEdit" size="mini" @click.stop="openEdit(node)">编辑</a-button>
|
<a-button v-if="canEdit" size="mini" @click.stop="openEdit(node)">编辑</a-button>
|
||||||
<a-button v-if="canChangeStatus" size="mini" @click.stop="confirmStatus(node)">{{ node.status === 1 ? '停用' : '启用' }}</a-button>
|
<a-button v-if="canChangeStatus" size="mini" @click.stop="confirmStatus(node)">{{ node.status === 1 ? '停用' : '启用' }}</a-button>
|
||||||
<a-button v-if="canArchive" size="mini" status="danger" @click.stop="confirmArchive(node)">归档</a-button>
|
<a-button v-if="canArchive" size="mini" status="danger" @click.stop="confirmArchive(node)">归档</a-button>
|
||||||
@@ -40,6 +44,7 @@ import { resourceApi } from '@/api/resource';
|
|||||||
import { buildResourcePayload, isMissingField } from '@/api/resource-form';
|
import { buildResourcePayload, isMissingField } from '@/api/resource-form';
|
||||||
import type { ResourceUiDefinition } from '@/api/resources';
|
import type { ResourceUiDefinition } from '@/api/resources';
|
||||||
import { useUserStore } from '@/store';
|
import { useUserStore } from '@/store';
|
||||||
|
import IdentityText from '@/components/IdentityText.vue';
|
||||||
|
|
||||||
type Node = Record<string, unknown> & {
|
type Node = Record<string, unknown> & {
|
||||||
identity: string;
|
identity: string;
|
||||||
@@ -185,3 +190,10 @@ async function load() {
|
|||||||
|
|
||||||
onMounted(load);
|
onMounted(load);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tree-business-key {
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,17 +1,82 @@
|
|||||||
/**
|
/**
|
||||||
* 功能描述:集中处理标准资源列表字段的关系名称、唯一标识和列宽展示。
|
* 功能描述:集中处理标准资源列表字段的关系名称、唯一标识和列宽展示。
|
||||||
* 版本:v1.0.0
|
* 版本:v1.1.0
|
||||||
*/
|
*/
|
||||||
import { optionLabel } from '@/api/resource-display';
|
import { fieldRelationResource, optionLabel } from '@/api/resource-display';
|
||||||
import type { ResourceRow } from '@/api/resource-page-rules';
|
import type { ResourceRow } from '@/api/resource-page-rules';
|
||||||
import type { ResourceField, ResourceUiDefinition } from '@/api/resources';
|
import type { ResourceField, ResourceUiDefinition } from '@/api/resources';
|
||||||
import { isProtectedListAvatarField } from './protected-list-avatar-loader';
|
import { isProtectedListAvatarField } from './protected-list-avatar-loader';
|
||||||
|
|
||||||
|
const resourceListPriorities: Record<string, string[]> = {
|
||||||
|
gasorder_basic: [
|
||||||
|
'order_no', 'request_no', 'order_status', 'gasorder_contract_identity',
|
||||||
|
'creator_identity', 'user_account_identity',
|
||||||
|
],
|
||||||
|
gasorder_contract: [
|
||||||
|
'contract_no', 'contract_status', 'title', 'user_account_identity',
|
||||||
|
'gas_basic_identity', 'delivery_basic_identity',
|
||||||
|
],
|
||||||
|
payment_order: [
|
||||||
|
'payment_no', 'payment_status', 'business_type', 'business_identity',
|
||||||
|
'amount', 'channel',
|
||||||
|
],
|
||||||
|
wallet_record: [
|
||||||
|
'record_no', 'direction', 'trade_type', 'amount',
|
||||||
|
'balance_after', 'operator_name',
|
||||||
|
],
|
||||||
|
ec_order: [
|
||||||
|
'order_no', 'order_status', 'user_account_identity', 'payable_amount',
|
||||||
|
'logistics_status', 'paid_at',
|
||||||
|
],
|
||||||
|
gasorder_item: [
|
||||||
|
'product_name', 'product_code', 'product_type_name', 'unit_price',
|
||||||
|
'gasorder_basic_identity', 'active',
|
||||||
|
],
|
||||||
|
gasorder_assign: [
|
||||||
|
'assigned_at', 'gasorder_basic_identity', 'gas_basic_identity',
|
||||||
|
'delivery_basic_identity', 'staff_account_identity', 'assigner_name',
|
||||||
|
],
|
||||||
|
gasorder_status: [
|
||||||
|
'occurred_at', 'gasorder_basic_identity', 'from_status', 'to_status',
|
||||||
|
'operator_name', 'reason',
|
||||||
|
],
|
||||||
|
gasorder_contract_revision: [
|
||||||
|
'occurred_at', 'gasorder_contract_identity', 'action', 'contract_status',
|
||||||
|
'operator_name', 'reason',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatedListFields: Record<string, ResourceField> = {
|
||||||
|
// 订单号是业务单号,必须完整展示,不得复用只显示末 12 位的系统标识组件。
|
||||||
|
order_no: { key: 'order_no', label: '订单号' },
|
||||||
|
order_status: { key: 'order_status', label: '订单状态' },
|
||||||
|
contract_status: { key: 'contract_status', label: '合同状态' },
|
||||||
|
product_name: { key: 'product_name', label: '智能气阀名称' },
|
||||||
|
user_account_identity: {
|
||||||
|
key: 'user_account_identity', label: '用户账户', type: 'identity',
|
||||||
|
relation: '/user_account', listRelationNameOnly: true,
|
||||||
|
displayRelationLabel: true, showIdentityCopy: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/** 返回标准列表实际渲染的业务字段,搜索提示与表格共同复用该规则。 */
|
/** 返回标准列表实际渲染的业务字段,搜索提示与表格共同复用该规则。 */
|
||||||
export function resourceListDisplayFields(
|
export function resourceListDisplayFields(
|
||||||
definition: ResourceUiDefinition,
|
definition: ResourceUiDefinition,
|
||||||
): ResourceField[] {
|
): ResourceField[] {
|
||||||
return definition.fields
|
const fieldsByKey = new Map(
|
||||||
|
[...Object.values(generatedListFields), ...definition.fields].map(
|
||||||
|
(field) => [field.key, field],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const priority = resourceListPriorities[definition.name] ?? [];
|
||||||
|
const ordered = [
|
||||||
|
...priority.flatMap((key) => {
|
||||||
|
const field = fieldsByKey.get(key);
|
||||||
|
return field ? [field] : [];
|
||||||
|
}),
|
||||||
|
...definition.fields.filter((field) => !priority.includes(field.key)),
|
||||||
|
];
|
||||||
|
return ordered
|
||||||
.filter((field) => field.key !== 'identity' && field.type !== 'password')
|
.filter((field) => field.key !== 'identity' && field.type !== 'password')
|
||||||
.filter(
|
.filter(
|
||||||
(field) =>
|
(field) =>
|
||||||
@@ -30,17 +95,18 @@ export function identityFieldValue(field: ResourceField, row: ResourceRow) {
|
|||||||
return String(row[field.key] ?? row[`${field.key}_masked`] ?? '');
|
return String(row[field.key] ?? row[`${field.key}_masked`] ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取列表关系的可读名称,关系未加载时降级为完整唯一标识。 */
|
/** 获取列表关系的可读名称;加载失败时由唯一标识复制入口保留排障能力。 */
|
||||||
export function relationListName(
|
export function relationListName(
|
||||||
field: ResourceField,
|
field: ResourceField,
|
||||||
row: ResourceRow,
|
row: ResourceRow,
|
||||||
relationOptions: Record<string, ResourceRow[]>,
|
relationOptions: Record<string, ResourceRow[]>,
|
||||||
) {
|
) {
|
||||||
const identity = identityFieldValue(field, row);
|
const identity = identityFieldValue(field, row);
|
||||||
const match = (relationOptions[field.relation ?? ''] ?? []).find(
|
const resource = fieldRelationResource(field, row);
|
||||||
|
const match = (relationOptions[resource] ?? []).find(
|
||||||
(option) => String(option.identity) === identity,
|
(option) => String(option.identity) === identity,
|
||||||
);
|
);
|
||||||
return match ? optionLabel(match) : identity;
|
return match ? optionLabel(match) : '名称加载失败';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按字段类型返回标准列表列宽。 */
|
/** 按字段类型返回标准列表列宽。 */
|
||||||
|
|||||||
Reference in New Issue
Block a user